diff --git a/.gitignore b/.gitignore index b5bfc8b4..006aa81f 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,9 @@ frontend/.env django-forwardemail/ frontend/ frontend -.snapshots \ No newline at end of file +.snapshots +web/next-env.d.ts +web/.next/types/cache-life.d.ts +.gitignore +web/.next/types/routes.d.ts +web/.next/types/validator.ts diff --git a/backend/apps/moderation/serializers.py b/backend/apps/moderation/serializers.py index 06592bbd..7bc69245 100644 --- a/backend/apps/moderation/serializers.py +++ b/backend/apps/moderation/serializers.py @@ -21,6 +21,7 @@ from .models import ( ModerationQueue, ModerationAction, BulkOperation, + EditSubmission, ) User = get_user_model() @@ -60,6 +61,137 @@ class ContentTypeSerializer(serializers.ModelSerializer): # ============================================================================ +class EditSubmissionSerializer(serializers.ModelSerializer): + """Serializer for EditSubmission with UI metadata for Nuxt frontend.""" + + submitted_by = UserBasicSerializer(source="user", read_only=True) + content_type_name = serializers.CharField( + source="content_type.model", read_only=True + ) + + # UI Metadata fields for Nuxt rendering + status_color = serializers.SerializerMethodField() + status_icon = serializers.SerializerMethodField() + status_display = serializers.CharField(source="get_status_display", read_only=True) + time_since_created = serializers.SerializerMethodField() + + class Meta: + model = EditSubmission + fields = [ + "id", + "status", + "status_display", + "status_color", + "status_icon", + "content_type", + "content_type_name", + "object_id", + "changes", + "moderator_changes", + "rejection_reason", + "submitted_by", + "reviewed_by", + "created_at", + "updated_at", + "time_since_created", + ] + read_only_fields = [ + "id", + "created_at", + "updated_at", + "submitted_by", + "status_color", + "status_icon", + "status_display", + "content_type_name", + "time_since_created", + ] + + def get_status_color(self, obj) -> str: + """Return hex color based on status for UI badges.""" + colors = { + "PENDING": "#f59e0b", # Amber + "APPROVED": "#10b981", # Emerald + "REJECTED": "#ef4444", # Red + "ESCALATED": "#8b5cf6", # Violet + } + return colors.get(obj.status, "#6b7280") # Default gray + + def get_status_icon(self, obj) -> str: + """Return Heroicons icon name based on status.""" + icons = { + "PENDING": "heroicons:clock", + "APPROVED": "heroicons:check-circle", + "REJECTED": "heroicons:x-circle", + "ESCALATED": "heroicons:arrow-up-circle", + } + return icons.get(obj.status, "heroicons:question-mark-circle") + + def get_time_since_created(self, obj) -> str: + """Human-readable time since creation.""" + now = timezone.now() + diff = now - obj.created_at + + if diff.days > 0: + return f"{diff.days} days ago" + elif diff.seconds > 3600: + hours = diff.seconds // 3600 + return f"{hours} hours ago" + else: + minutes = diff.seconds // 60 + return f"{minutes} minutes ago" + + +class EditSubmissionListSerializer(serializers.ModelSerializer): + """Optimized serializer for EditSubmission lists.""" + + submitted_by_username = serializers.CharField( + source="user.username", read_only=True + ) + content_type_name = serializers.CharField( + source="content_type.model", read_only=True + ) + status_color = serializers.SerializerMethodField() + status_icon = serializers.SerializerMethodField() + + class Meta: + model = EditSubmission + fields = [ + "id", + "status", + "content_type_name", + "object_id", + "submitted_by_username", + "status_color", + "status_icon", + "created_at", + ] + read_only_fields = fields + + def get_status_color(self, obj) -> str: + colors = { + "PENDING": "#f59e0b", + "APPROVED": "#10b981", + "REJECTED": "#ef4444", + "ESCALATED": "#8b5cf6", + } + return colors.get(obj.status, "#6b7280") + + def get_status_icon(self, obj) -> str: + icons = { + "PENDING": "heroicons:clock", + "APPROVED": "heroicons:check-circle", + "REJECTED": "heroicons:x-circle", + "ESCALATED": "heroicons:arrow-up-circle", + } + return icons.get(obj.status, "heroicons:question-mark-circle") + + +# ============================================================================ +# Moderation Report Serializers +# ============================================================================ + + class ModerationReportSerializer(serializers.ModelSerializer): """Full moderation report serializer with all details.""" diff --git a/backend/apps/parks/admin.py b/backend/apps/parks/admin.py index 505111af..e2bc454e 100644 --- a/backend/apps/parks/admin.py +++ b/backend/apps/parks/admin.py @@ -101,7 +101,7 @@ class ParkLocationAdmin(QueryOptimizationMixin, GISModelAdmin): "country", "street_address", ) - readonly_fields = ("latitude", "longitude", "coordinates", "created_at", "updated_at") + readonly_fields = ("latitude", "longitude", "coordinates") autocomplete_fields = ["park"] list_per_page = 50 show_full_result_count = False @@ -156,13 +156,7 @@ class ParkLocationAdmin(QueryOptimizationMixin, GISModelAdmin): "description": "OpenStreetMap identifiers for data synchronization.", }, ), - ( - "Metadata", - { - "fields": ("created_at", "updated_at"), - "classes": ("collapse",), - }, - ), + ) @admin.display(description="Park") @@ -234,7 +228,7 @@ class ParkAdmin( "operator__name", ) readonly_fields = ("created_at", "updated_at", "ride_count", "average_rating") - autocomplete_fields = ["operator", "property_owner", "banner_image", "card_image"] + autocomplete_fields = ["operator", "property_owner"] date_hierarchy = "created_at" ordering = ("-created_at",) inlines = [ParkLocationInline, ParkAreaInline] diff --git a/backend/apps/rides/admin.py b/backend/apps/rides/admin.py index 108f07e8..367c6db6 100644 --- a/backend/apps/rides/admin.py +++ b/backend/apps/rides/admin.py @@ -242,8 +242,8 @@ class RideAdmin( "manufacturer", "designer", "ride_model", - "banner_image", - "card_image", + # "banner_image", + # "card_image", ] inlines = [RideLocationInline, RollerCoasterStatsInline] date_hierarchy = "opening_date" diff --git a/backend/apps/rides/services/__init__.py b/backend/apps/rides/services/__init__.py index 26bf88ac..3cb7b0d8 100644 --- a/backend/apps/rides/services/__init__.py +++ b/backend/apps/rides/services/__init__.py @@ -1,4 +1,7 @@ from .location_service import RideLocationService from .media_service import RideMediaService +# Import from the services_core module (was services.py, renamed to avoid package collision) +from ..services_core import RideService + +__all__ = ["RideLocationService", "RideMediaService", "RideService"] -__all__ = ["RideLocationService", "RideMediaService"] diff --git a/backend/apps/rides/services.py b/backend/apps/rides/services_core.py similarity index 100% rename from backend/apps/rides/services.py rename to backend/apps/rides/services_core.py diff --git a/backend/config/settings/third_party.py b/backend/config/settings/third_party.py index 60bd9658..c7de1f5d 100644 --- a/backend/config/settings/third_party.py +++ b/backend/config/settings/third_party.py @@ -36,6 +36,7 @@ ACCOUNT_LOGIN_METHODS = {"email", "username"} ACCOUNT_EMAIL_VERIFICATION = config( "ACCOUNT_EMAIL_VERIFICATION", default="mandatory" ) +ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_CHANGE = True ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_RESEND = True