Add comprehensive API documentation for ThrillWiki integration and features

- Introduced Next.js integration guide for ThrillWiki API, detailing authentication, core domain APIs, data structures, and implementation patterns.
- Documented the migration to Rich Choice Objects, highlighting changes for frontend developers and enhanced metadata availability.
- Fixed the missing `get_by_slug` method in the Ride model, ensuring proper functionality of ride detail endpoints.
- Created a test script to verify manufacturer syncing with ride models, ensuring data integrity across related models.
This commit is contained in:
pacnpal
2025-09-16 11:29:17 -04:00
parent 61d73a2147
commit c2c26cfd1d
98 changed files with 11476 additions and 4803 deletions

View File

@@ -18,6 +18,7 @@ from apps.accounts.models import (
UserNotification,
NotificationPreference,
)
from apps.core.choices.serializers import RichChoiceFieldSerializer
UserModel = get_user_model()
@@ -190,8 +191,10 @@ class CompleteUserSerializer(serializers.ModelSerializer):
class UserPreferencesSerializer(serializers.Serializer):
"""Serializer for user preferences and settings."""
theme_preference = serializers.ChoiceField(
choices=User.ThemePreference.choices, help_text="User's theme preference"
theme_preference = RichChoiceFieldSerializer(
choice_group="theme_preferences",
domain="accounts",
help_text="User's theme preference"
)
email_notifications = serializers.BooleanField(
default=True, help_text="Whether to receive email notifications"
@@ -199,12 +202,9 @@ class UserPreferencesSerializer(serializers.Serializer):
push_notifications = serializers.BooleanField(
default=False, help_text="Whether to receive push notifications"
)
privacy_level = serializers.ChoiceField(
choices=[
("public", "Public"),
("friends", "Friends Only"),
("private", "Private"),
],
privacy_level = RichChoiceFieldSerializer(
choice_group="privacy_levels",
domain="accounts",
default="public",
help_text="Profile visibility level",
)
@@ -321,12 +321,9 @@ class NotificationSettingsSerializer(serializers.Serializer):
class PrivacySettingsSerializer(serializers.Serializer):
"""Serializer for privacy and visibility settings."""
profile_visibility = serializers.ChoiceField(
choices=[
("public", "Public"),
("friends", "Friends Only"),
("private", "Private"),
],
profile_visibility = RichChoiceFieldSerializer(
choice_group="privacy_levels",
domain="accounts",
default="public",
help_text="Overall profile visibility",
)
@@ -363,12 +360,9 @@ class PrivacySettingsSerializer(serializers.Serializer):
search_visibility = serializers.BooleanField(
default=True, help_text="Allow profile to appear in search results"
)
activity_visibility = serializers.ChoiceField(
choices=[
("public", "Public"),
("friends", "Friends Only"),
("private", "Private"),
],
activity_visibility = RichChoiceFieldSerializer(
choice_group="privacy_levels",
domain="accounts",
default="friends",
help_text="Who can see your activity feed",
)

View File

@@ -12,7 +12,8 @@ from drf_spectacular.utils import (
OpenApiExample,
)
from .shared import CATEGORY_CHOICES, ModelChoices
from .shared import ModelChoices
from apps.core.choices.serializers import RichChoiceFieldSerializer
# === COMPANY SERIALIZERS ===
@@ -111,7 +112,10 @@ class RideModelDetailOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
description = serializers.CharField()
category = serializers.CharField()
category = RichChoiceFieldSerializer(
choice_group="categories",
domain="rides"
)
# Manufacturer info
manufacturer = serializers.SerializerMethodField()
@@ -136,7 +140,7 @@ class RideModelCreateInputSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255)
description = serializers.CharField(allow_blank=True, default="")
category = serializers.ChoiceField(choices=CATEGORY_CHOICES, required=False)
category = serializers.ChoiceField(choices=ModelChoices.get_ride_category_choices(), required=False)
manufacturer_id = serializers.IntegerField(required=False, allow_null=True)
@@ -145,5 +149,5 @@ class RideModelUpdateInputSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255, required=False)
description = serializers.CharField(allow_blank=True, required=False)
category = serializers.ChoiceField(choices=CATEGORY_CHOICES, required=False)
category = serializers.ChoiceField(choices=ModelChoices.get_ride_category_choices(), required=False)
manufacturer_id = serializers.IntegerField(required=False, allow_null=True)

View File

@@ -9,6 +9,8 @@ from rest_framework import serializers
from drf_spectacular.utils import (
extend_schema_field,
)
from .shared import ModelChoices
from apps.core.choices.serializers import RichChoiceFieldSerializer
# === STATISTICS SERIALIZERS ===
@@ -90,7 +92,10 @@ class ParkReviewOutputSerializer(serializers.Serializer):
class HealthCheckOutputSerializer(serializers.Serializer):
"""Output serializer for health check responses."""
status = serializers.ChoiceField(choices=["healthy", "unhealthy"])
status = RichChoiceFieldSerializer(
choice_group="health_statuses",
domain="core"
)
timestamp = serializers.DateTimeField()
version = serializers.CharField()
environment = serializers.CharField()
@@ -111,6 +116,9 @@ class PerformanceMetricsOutputSerializer(serializers.Serializer):
class SimpleHealthOutputSerializer(serializers.Serializer):
"""Output serializer for simple health check."""
status = serializers.ChoiceField(choices=["ok", "error"])
status = RichChoiceFieldSerializer(
choice_group="simple_health_statuses",
domain="core"
)
timestamp = serializers.DateTimeField()
error = serializers.CharField(required=False)

View File

@@ -15,6 +15,7 @@ from config.django import base as settings
from .shared import LocationOutputSerializer, CompanyOutputSerializer, ModelChoices
from apps.core.services.media_url_service import MediaURLService
from apps.core.choices.serializers import RichChoiceFieldSerializer
# === PARK SERIALIZERS ===
@@ -51,7 +52,10 @@ class ParkListOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
status = serializers.CharField()
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="parks"
)
description = serializers.CharField()
# Statistics
@@ -141,7 +145,10 @@ class ParkDetailOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
status = serializers.CharField()
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="parks"
)
description = serializers.CharField()
# Details

View File

@@ -14,6 +14,7 @@ from drf_spectacular.utils import (
from config.django import base as settings
from .shared import ModelChoices
from apps.core.choices.serializers import RichChoiceFieldSerializer
# Use dynamic imports to avoid circular import issues
@@ -132,14 +133,20 @@ class RideModelListOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
category = serializers.CharField()
category = RichChoiceFieldSerializer(
choice_group="categories",
domain="rides"
)
description = serializers.CharField()
# Manufacturer info
manufacturer = RideModelManufacturerOutputSerializer(allow_null=True)
# Market info
target_market = serializers.CharField()
target_market = RichChoiceFieldSerializer(
choice_group="target_markets",
domain="rides"
)
is_discontinued = serializers.BooleanField()
total_installations = serializers.IntegerField()
first_installation_year = serializers.IntegerField(allow_null=True)
@@ -386,15 +393,9 @@ class RideModelCreateInputSerializer(serializers.Serializer):
# Design features
notable_features = serializers.CharField(allow_blank=True, default="")
target_market = serializers.ChoiceField(
choices=[
("FAMILY", "Family"),
("THRILL", "Thrill"),
("EXTREME", "Extreme"),
("KIDDIE", "Kiddie"),
("ALL_AGES", "All Ages"),
],
choices=ModelChoices.get_target_market_choices(),
required=False,
allow_blank=True,
default="",
)
def validate(self, attrs):
@@ -496,13 +497,7 @@ class RideModelUpdateInputSerializer(serializers.Serializer):
# Design features
notable_features = serializers.CharField(allow_blank=True, required=False)
target_market = serializers.ChoiceField(
choices=[
("FAMILY", "Family"),
("THRILL", "Thrill"),
("EXTREME", "Extreme"),
("KIDDIE", "Kiddie"),
("ALL_AGES", "All Ages"),
],
choices=ModelChoices.get_target_market_choices(),
allow_blank=True,
required=False,
)
@@ -565,13 +560,7 @@ class RideModelFilterInputSerializer(serializers.Serializer):
# Market filter
target_market = serializers.MultipleChoiceField(
choices=[
("FAMILY", "Family"),
("THRILL", "Thrill"),
("EXTREME", "Extreme"),
("KIDDIE", "Kiddie"),
("ALL_AGES", "All Ages"),
],
choices=ModelChoices.get_target_market_choices(),
required=False,
)
@@ -724,16 +713,7 @@ class RideModelTechnicalSpecCreateInputSerializer(serializers.Serializer):
ride_model_id = serializers.IntegerField()
spec_category = serializers.ChoiceField(
choices=[
("DIMENSIONS", "Dimensions"),
("PERFORMANCE", "Performance"),
("CAPACITY", "Capacity"),
("SAFETY", "Safety Features"),
("ELECTRICAL", "Electrical Requirements"),
("FOUNDATION", "Foundation Requirements"),
("MAINTENANCE", "Maintenance"),
("OTHER", "Other"),
]
choices=ModelChoices.get_technical_spec_category_choices()
)
spec_name = serializers.CharField(max_length=100)
spec_value = serializers.CharField(max_length=255)
@@ -745,16 +725,7 @@ class RideModelTechnicalSpecUpdateInputSerializer(serializers.Serializer):
"""Input serializer for updating ride model technical specifications."""
spec_category = serializers.ChoiceField(
choices=[
("DIMENSIONS", "Dimensions"),
("PERFORMANCE", "Performance"),
("CAPACITY", "Capacity"),
("SAFETY", "Safety Features"),
("ELECTRICAL", "Electrical Requirements"),
("FOUNDATION", "Foundation Requirements"),
("MAINTENANCE", "Maintenance"),
("OTHER", "Other"),
],
choices=ModelChoices.get_technical_spec_category_choices(),
required=False,
)
spec_name = serializers.CharField(max_length=100, required=False)
@@ -774,13 +745,7 @@ class RideModelPhotoCreateInputSerializer(serializers.Serializer):
caption = serializers.CharField(max_length=500, allow_blank=True, default="")
alt_text = serializers.CharField(max_length=255, allow_blank=True, default="")
photo_type = serializers.ChoiceField(
choices=[
("PROMOTIONAL", "Promotional"),
("TECHNICAL", "Technical Drawing"),
("INSTALLATION", "Installation Example"),
("RENDERING", "3D Rendering"),
("CATALOG", "Catalog Image"),
],
choices=ModelChoices.get_photo_type_choices(),
default="PROMOTIONAL",
)
is_primary = serializers.BooleanField(default=False)
@@ -795,13 +760,7 @@ class RideModelPhotoUpdateInputSerializer(serializers.Serializer):
caption = serializers.CharField(max_length=500, allow_blank=True, required=False)
alt_text = serializers.CharField(max_length=255, allow_blank=True, required=False)
photo_type = serializers.ChoiceField(
choices=[
("PROMOTIONAL", "Promotional"),
("TECHNICAL", "Technical Drawing"),
("INSTALLATION", "Installation Example"),
("RENDERING", "3D Rendering"),
("CATALOG", "Catalog Image"),
],
choices=ModelChoices.get_photo_type_choices(),
required=False,
)
is_primary = serializers.BooleanField(required=False)

View File

@@ -13,6 +13,7 @@ from drf_spectacular.utils import (
)
from config.django import base as settings
from .shared import ModelChoices
from apps.core.choices.serializers import RichChoiceFieldSerializer
# === RIDE SERIALIZERS ===
@@ -24,6 +25,12 @@ class RideParkOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
url = serializers.SerializerMethodField()
@extend_schema_field(serializers.URLField())
def get_url(self, obj) -> str:
"""Generate the frontend URL for this park."""
return f"{settings.FRONTEND_DOMAIN}/parks/{obj.slug}/"
class RideModelOutputSerializer(serializers.Serializer):
@@ -73,8 +80,14 @@ class RideListOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
category = serializers.CharField()
status = serializers.CharField()
category = RichChoiceFieldSerializer(
choice_group="categories",
domain="rides"
)
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="rides"
)
description = serializers.CharField()
# Park info
@@ -164,9 +177,19 @@ class RideDetailOutputSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
category = serializers.CharField()
status = serializers.CharField()
post_closing_status = serializers.CharField(allow_null=True)
category = RichChoiceFieldSerializer(
choice_group="categories",
domain="rides"
)
status = RichChoiceFieldSerializer(
choice_group="statuses",
domain="rides"
)
post_closing_status = RichChoiceFieldSerializer(
choice_group="post_closing_statuses",
domain="rides",
allow_null=True
)
description = serializers.CharField()
# Park info
@@ -449,10 +472,10 @@ class RideCreateInputSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255)
description = serializers.CharField(allow_blank=True, default="")
category = serializers.ChoiceField(choices=[]) # Choices set dynamically
category = serializers.ChoiceField(choices=ModelChoices.get_ride_category_choices())
status = serializers.ChoiceField(
choices=[], default="OPERATING"
) # Choices set dynamically
choices=ModelChoices.get_ride_status_choices(), default="OPERATING"
)
# Required park
park_id = serializers.IntegerField()
@@ -531,11 +554,11 @@ class RideUpdateInputSerializer(serializers.Serializer):
name = serializers.CharField(max_length=255, required=False)
description = serializers.CharField(allow_blank=True, required=False)
category = serializers.ChoiceField(
choices=[], required=False
) # Choices set dynamically
choices=ModelChoices.get_ride_category_choices(), required=False
)
status = serializers.ChoiceField(
choices=[], required=False
) # Choices set dynamically
choices=ModelChoices.get_ride_status_choices(), required=False
)
post_closing_status = serializers.ChoiceField(
choices=ModelChoices.get_ride_post_closing_choices(),
required=False,
@@ -603,13 +626,13 @@ class RideFilterInputSerializer(serializers.Serializer):
# Category filter
category = serializers.MultipleChoiceField(
choices=[], required=False
) # Choices set dynamically
choices=ModelChoices.get_ride_category_choices(), required=False
)
# Status filter
status = serializers.MultipleChoiceField(
choices=[],
required=False, # Choices set dynamically
choices=ModelChoices.get_ride_status_choices(),
required=False,
)
# Park filter
@@ -695,12 +718,21 @@ class RollerCoasterStatsOutputSerializer(serializers.Serializer):
inversions = serializers.IntegerField()
ride_time_seconds = serializers.IntegerField(allow_null=True)
track_type = serializers.CharField()
track_material = serializers.CharField()
roller_coaster_type = serializers.CharField()
track_material = RichChoiceFieldSerializer(
choice_group="track_materials",
domain="rides"
)
roller_coaster_type = RichChoiceFieldSerializer(
choice_group="coaster_types",
domain="rides"
)
max_drop_height_ft = serializers.DecimalField(
max_digits=6, decimal_places=2, allow_null=True
)
launch_type = serializers.CharField()
launch_type = RichChoiceFieldSerializer(
choice_group="launch_systems",
domain="rides"
)
train_style = serializers.CharField()
trains_count = serializers.IntegerField(allow_null=True)
cars_per_train = serializers.IntegerField(allow_null=True)

View File

@@ -6,6 +6,8 @@ and other search functionality.
"""
from rest_framework import serializers
from ..shared import ModelChoices
from apps.core.choices.serializers import RichChoiceFieldSerializer
# === CORE ENTITY SEARCH SERIALIZERS ===
@@ -16,7 +18,9 @@ class EntitySearchInputSerializer(serializers.Serializer):
query = serializers.CharField(max_length=255, help_text="Search query string")
entity_types = serializers.ListField(
child=serializers.ChoiceField(choices=["park", "ride", "company", "user"]),
child=serializers.ChoiceField(
choices=ModelChoices.get_entity_type_choices()
),
required=False,
help_text="Types of entities to search for",
)
@@ -34,7 +38,10 @@ class EntitySearchResultSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
type = serializers.CharField()
type = RichChoiceFieldSerializer(
choice_group="entity_types",
domain="core"
)
description = serializers.CharField()
relevance_score = serializers.FloatField()

View File

@@ -147,7 +147,12 @@ class ModerationSubmissionSerializer(serializers.Serializer):
"""Serializer for moderation submissions."""
submission_type = serializers.ChoiceField(
choices=["EDIT", "PHOTO", "REVIEW"], help_text="Type of submission"
choices=[
("EDIT", "Edit Submission"),
("PHOTO", "Photo Submission"),
("REVIEW", "Review Submission"),
],
help_text="Type of submission"
)
content_type = serializers.CharField(help_text="Content type being modified")
object_id = serializers.IntegerField(help_text="ID of object being modified")

View File

@@ -9,7 +9,7 @@ for common data structures used throughout the API.
"""
from rest_framework import serializers
from typing import Dict, Any, List, Optional
from typing import Dict, Any, List
class FilterOptionSerializer(serializers.Serializer):
@@ -316,107 +316,124 @@ class CompanyOutputSerializer(serializers.Serializer):
)
# Category choices for ride models
CATEGORY_CHOICES = [
('RC', 'Roller Coaster'),
('DR', 'Dark Ride'),
('FR', 'Flat Ride'),
('WR', 'Water Ride'),
('TR', 'Transport Ride'),
]
class ModelChoices:
"""
Utility class to provide model choices for serializers.
This prevents circular imports while providing access to model choices.
Utility class to provide model choices for serializers using Rich Choice Objects.
This prevents circular imports while providing access to model choices from the registry.
NO FALLBACKS - All choices must be properly defined in Rich Choice Objects.
"""
@staticmethod
def get_park_status_choices():
"""Get park status choices."""
return [
('OPERATING', 'Operating'),
('CLOSED_TEMP', 'Temporarily Closed'),
('CLOSED_PERM', 'Permanently Closed'),
('UNDER_CONSTRUCTION', 'Under Construction'),
('PLANNED', 'Planned'),
]
"""Get park status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("statuses", "parks")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_ride_status_choices():
"""Get ride status choices."""
return [
('OPERATING', 'Operating'),
('CLOSED_TEMP', 'Temporarily Closed'),
('CLOSED_PERM', 'Permanently Closed'),
('SBNO', 'Standing But Not Operating'),
('UNDER_CONSTRUCTION', 'Under Construction'),
('RELOCATED', 'Relocated'),
('DEMOLISHED', 'Demolished'),
]
"""Get ride status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("statuses", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_company_role_choices():
"""Get company role choices."""
return [
('MANUFACTURER', 'Manufacturer'),
('OPERATOR', 'Operator'),
('DESIGNER', 'Designer'),
('PROPERTY_OWNER', 'Property Owner'),
]
"""Get company role choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
# Get rides domain company roles (MANUFACTURER, DESIGNER)
rides_choices = get_choices("company_roles", "rides")
# Get parks domain company roles (OPERATOR, PROPERTY_OWNER)
parks_choices = get_choices("company_roles", "parks")
all_choices = list(rides_choices) + list(parks_choices)
return [(choice.value, choice.label) for choice in all_choices]
@staticmethod
def get_ride_category_choices():
"""Get ride category choices."""
return CATEGORY_CHOICES
"""Get ride category choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_ride_post_closing_choices():
"""Get ride post-closing status choices."""
return [
('RELOCATED', 'Relocated'),
('DEMOLISHED', 'Demolished'),
('STORED', 'Stored'),
('UNKNOWN', 'Unknown'),
]
"""Get ride post-closing status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("post_closing_statuses", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_coaster_track_choices():
"""Get coaster track type choices."""
return [
('STEEL', 'Steel'),
('WOOD', 'Wood'),
('HYBRID', 'Hybrid'),
]
"""Get coaster track material choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("track_materials", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_coaster_type_choices():
"""Get coaster type choices."""
return [
('SIT_DOWN', 'Sit Down'),
('INVERTED', 'Inverted'),
('FLOORLESS', 'Floorless'),
('FLYING', 'Flying'),
('STAND_UP', 'Stand Up'),
('SPINNING', 'Spinning'),
('WING', 'Wing'),
('DIVE', 'Dive'),
('LAUNCHED', 'Launched'),
]
"""Get coaster type choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("coaster_types", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_launch_choices():
"""Get launch system choices."""
return [
('NONE', 'None'),
('LIM', 'Linear Induction Motor'),
('LSM', 'Linear Synchronous Motor'),
('HYDRAULIC', 'Hydraulic'),
('PNEUMATIC', 'Pneumatic'),
('CABLE', 'Cable'),
('FLYWHEEL', 'Flywheel'),
]
"""Get launch system choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("launch_systems", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_photo_type_choices():
"""Get photo type choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("photo_types", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_spec_category_choices():
"""Get technical specification category choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("spec_categories", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_technical_spec_category_choices():
"""Get technical specification category choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("spec_categories", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_target_market_choices():
"""Get target market choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("target_markets", "rides")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_entity_type_choices():
"""Get entity type choices for search functionality."""
from apps.core.choices.registry import get_choices
choices = get_choices("entity_types", "core")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_health_status_choices():
"""Get health check status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("health_statuses", "core")
return [(choice.value, choice.label) for choice in choices]
@staticmethod
def get_simple_health_status_choices():
"""Get simple health check status choices from Rich Choice registry."""
from apps.core.choices.registry import get_choices
choices = get_choices("simple_health_statuses", "core")
return [(choice.value, choice.label) for choice in choices]
class EntityReferenceSerializer(serializers.Serializer):
@@ -593,12 +610,12 @@ def ensure_filter_option_format(options: List[Any]) -> List[Dict[str, Any]]:
'count': option.get('count'),
'selected': option.get('selected', False)
}
elif isinstance(option, (list, tuple)) and len(option) >= 2:
# Tuple format: (value, label) or (value, label, count)
elif hasattr(option, 'value') and hasattr(option, 'label'):
# RichChoice object format
standardized_option = {
'value': str(option[0]),
'label': str(option[1]),
'count': option[2] if len(option) > 2 else None,
'value': str(option.value),
'label': str(option.label),
'count': None,
'selected': False
}
else: