feat: Implement initial schema and add various API, service, and management command enhancements across the application.

This commit is contained in:
pacnpal
2026-01-01 15:13:01 -05:00
parent c95f99ca10
commit b243b17af7
413 changed files with 11164 additions and 17433 deletions

View File

@@ -68,9 +68,7 @@ class EditSubmissionSerializer(serializers.ModelSerializer):
submitted_by = UserBasicSerializer(source="user", read_only=True)
claimed_by = UserBasicSerializer(read_only=True)
content_type_name = serializers.CharField(
source="content_type.model", 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()
@@ -117,10 +115,10 @@ class EditSubmissionSerializer(serializers.ModelSerializer):
def get_status_color(self, obj) -> str:
"""Return hex color based on status for UI badges."""
colors = {
"PENDING": "#f59e0b", # Amber
"CLAIMED": "#3b82f6", # Blue
"APPROVED": "#10b981", # Emerald
"REJECTED": "#ef4444", # Red
"PENDING": "#f59e0b", # Amber
"CLAIMED": "#3b82f6", # Blue
"APPROVED": "#10b981", # Emerald
"REJECTED": "#ef4444", # Red
"ESCALATED": "#8b5cf6", # Violet
}
return colors.get(obj.status, "#6b7280") # Default gray
@@ -154,15 +152,9 @@ class EditSubmissionSerializer(serializers.ModelSerializer):
class EditSubmissionListSerializer(serializers.ModelSerializer):
"""Optimized serializer for EditSubmission lists."""
submitted_by_username = serializers.CharField(
source="user.username", read_only=True
)
claimed_by_username = serializers.CharField(
source="claimed_by.username", read_only=True, allow_null=True
)
content_type_name = serializers.CharField(
source="content_type.model", read_only=True
)
submitted_by_username = serializers.CharField(source="user.username", read_only=True)
claimed_by_username = serializers.CharField(source="claimed_by.username", read_only=True, allow_null=True)
content_type_name = serializers.CharField(source="content_type.model", read_only=True)
status_color = serializers.SerializerMethodField()
status_icon = serializers.SerializerMethodField()
@@ -218,13 +210,9 @@ class ModerationReportSerializer(serializers.ModelSerializer):
# Computed fields
is_overdue = serializers.SerializerMethodField()
time_since_created = serializers.SerializerMethodField()
priority_display = serializers.CharField(
source="get_priority_display", read_only=True
)
priority_display = serializers.CharField(source="get_priority_display", read_only=True)
status_display = serializers.CharField(source="get_status_display", read_only=True)
report_type_display = serializers.CharField(
source="get_report_type_display", read_only=True
)
report_type_display = serializers.CharField(source="get_report_type_display", read_only=True)
class Meta:
model = ModerationReport
@@ -318,17 +306,13 @@ class CreateModerationReportSerializer(serializers.ModelSerializer):
valid_entity_types = ["park", "ride", "review", "photo", "user", "comment"]
if attrs["reported_entity_type"] not in valid_entity_types:
raise serializers.ValidationError(
{
"reported_entity_type": f'Must be one of: {", ".join(valid_entity_types)}'
}
{"reported_entity_type": f'Must be one of: {", ".join(valid_entity_types)}'}
)
# Validate evidence URLs
evidence_urls = attrs.get("evidence_urls", [])
if not isinstance(evidence_urls, list):
raise serializers.ValidationError(
{"evidence_urls": "Must be a list of URLs"}
)
raise serializers.ValidationError({"evidence_urls": "Must be a list of URLs"})
return attrs
@@ -351,9 +335,7 @@ class CreateModerationReportSerializer(serializers.ModelSerializer):
if entity_type in app_label_map:
try:
content_type = ContentType.objects.get(
app_label=app_label_map[entity_type], model=entity_type
)
content_type = ContentType.objects.get(app_label=app_label_map[entity_type], model=entity_type)
validated_data["content_type"] = content_type
except ContentType.DoesNotExist:
pass
@@ -377,9 +359,7 @@ class UpdateModerationReportSerializer(serializers.ModelSerializer):
def validate_status(self, value):
"""Validate status transitions."""
if self.instance and self.instance.status == "RESOLVED" and value != "RESOLVED":
raise serializers.ValidationError(
"Cannot change status of resolved report"
)
raise serializers.ValidationError("Cannot change status of resolved report")
return value
def update(self, instance, validated_data):
@@ -462,13 +442,9 @@ class ModerationQueueSerializer(serializers.ModelSerializer):
def get_estimated_completion(self, obj) -> str:
"""Estimated completion time."""
if obj.assigned_at:
completion_time = obj.assigned_at + timedelta(
minutes=obj.estimated_review_time
)
completion_time = obj.assigned_at + timedelta(minutes=obj.estimated_review_time)
else:
completion_time = timezone.now() + timedelta(
minutes=obj.estimated_review_time
)
completion_time = timezone.now() + timedelta(minutes=obj.estimated_review_time)
return completion_time.isoformat()
@@ -484,12 +460,10 @@ class AssignQueueItemSerializer(serializers.Serializer):
user = User.objects.get(id=value)
user_role = getattr(user, "role", "USER")
if user_role not in ["MODERATOR", "ADMIN", "SUPERUSER"]:
raise serializers.ValidationError(
"User must be a moderator, admin, or superuser"
)
raise serializers.ValidationError("User must be a moderator, admin, or superuser")
return value
except User.DoesNotExist:
raise serializers.ValidationError("Moderator not found")
raise serializers.ValidationError("Moderator not found") from None
class CompleteQueueItemSerializer(serializers.Serializer):
@@ -514,9 +488,7 @@ class CompleteQueueItemSerializer(serializers.Serializer):
# Require notes for certain actions
if action in ["USER_WARNING", "USER_SUSPENDED", "USER_BANNED"] and not notes:
raise serializers.ValidationError(
{"notes": f"Notes are required for action: {action}"}
)
raise serializers.ValidationError({"notes": f"Notes are required for action: {action}"})
return attrs
@@ -536,9 +508,7 @@ class ModerationActionSerializer(serializers.ModelSerializer):
# Computed fields
is_expired = serializers.SerializerMethodField()
time_remaining = serializers.SerializerMethodField()
action_type_display = serializers.CharField(
source="get_action_type_display", read_only=True
)
action_type_display = serializers.CharField(source="get_action_type_display", read_only=True)
class Meta:
model = ModerationAction
@@ -620,7 +590,7 @@ class CreateModerationActionSerializer(serializers.ModelSerializer):
User.objects.get(id=value)
return value
except User.DoesNotExist:
raise serializers.ValidationError("Target user not found")
raise serializers.ValidationError("Target user not found") from None
def validate_related_report_id(self, value):
"""Validate related report exists."""
@@ -629,7 +599,7 @@ class CreateModerationActionSerializer(serializers.ModelSerializer):
ModerationReport.objects.get(id=value)
return value
except ModerationReport.DoesNotExist:
raise serializers.ValidationError("Related report not found")
raise serializers.ValidationError("Related report not found") from None
return value
def validate(self, attrs):
@@ -640,17 +610,11 @@ class CreateModerationActionSerializer(serializers.ModelSerializer):
# Validate duration for temporary actions
temporary_actions = ["USER_SUSPENSION", "CONTENT_RESTRICTION"]
if action_type in temporary_actions and not duration_hours:
raise serializers.ValidationError(
{"duration_hours": f"Duration is required for {action_type}"}
)
raise serializers.ValidationError({"duration_hours": f"Duration is required for {action_type}"})
# Validate duration range
if duration_hours and (
duration_hours < 1 or duration_hours > 8760
): # 1 hour to 1 year
raise serializers.ValidationError(
{"duration_hours": "Duration must be between 1 and 8760 hours (1 year)"}
)
if duration_hours and (duration_hours < 1 or duration_hours > 8760): # 1 hour to 1 year
raise serializers.ValidationError({"duration_hours": "Duration must be between 1 and 8760 hours (1 year)"})
return attrs
@@ -668,9 +632,7 @@ class CreateModerationActionSerializer(serializers.ModelSerializer):
# Set expiration time for temporary actions
if validated_data.get("duration_hours"):
validated_data["expires_at"] = timezone.now() + timedelta(
hours=validated_data["duration_hours"]
)
validated_data["expires_at"] = timezone.now() + timedelta(hours=validated_data["duration_hours"])
return super().create(validated_data)
@@ -688,9 +650,7 @@ class BulkOperationSerializer(serializers.ModelSerializer):
# Computed fields
progress_percentage = serializers.SerializerMethodField()
estimated_completion = serializers.SerializerMethodField()
operation_type_display = serializers.CharField(
source="get_operation_type_display", read_only=True
)
operation_type_display = serializers.CharField(source="get_operation_type_display", read_only=True)
status_display = serializers.CharField(source="get_status_display", read_only=True)
class Meta:
@@ -741,17 +701,13 @@ class BulkOperationSerializer(serializers.ModelSerializer):
if obj.status == "COMPLETED":
return obj.completed_at.isoformat() if obj.completed_at else None
if obj.status == "RUNNING" and obj.started_at:
if obj.status == "RUNNING" and obj.started_at: # noqa: SIM102
# Calculate based on current progress
if obj.processed_items > 0:
elapsed_minutes = (timezone.now() - obj.started_at).total_seconds() / 60
rate = obj.processed_items / elapsed_minutes
remaining_items = obj.total_items - obj.processed_items
remaining_minutes = (
remaining_items / rate
if rate > 0
else obj.estimated_duration_minutes
)
remaining_minutes = remaining_items / rate if rate > 0 else obj.estimated_duration_minutes
completion_time = timezone.now() + timedelta(minutes=remaining_minutes)
return completion_time.isoformat()
@@ -759,9 +715,7 @@ class BulkOperationSerializer(serializers.ModelSerializer):
if obj.schedule_for:
return obj.schedule_for.isoformat()
elif obj.estimated_duration_minutes:
completion_time = timezone.now() + timedelta(
minutes=obj.estimated_duration_minutes
)
completion_time = timezone.now() + timedelta(minutes=obj.estimated_duration_minutes)
return completion_time.isoformat()
return None
@@ -801,9 +755,7 @@ class CreateBulkOperationSerializer(serializers.ModelSerializer):
if operation_type in required_params:
for param in required_params[operation_type]:
if param not in value:
raise serializers.ValidationError(
f'Parameter "{param}" is required for {operation_type}'
)
raise serializers.ValidationError(f'Parameter "{param}" is required for {operation_type}')
return value
@@ -902,27 +854,28 @@ class UserModerationProfileSerializer(serializers.Serializer):
class StateLogSerializer(serializers.ModelSerializer):
"""Serializer for FSM transition history."""
user = serializers.CharField(source='by.username', read_only=True)
model = serializers.CharField(source='content_type.model', read_only=True)
from_state = serializers.CharField(source='source_state', read_only=True)
to_state = serializers.CharField(source='state', read_only=True)
reason = serializers.CharField(source='description', read_only=True)
user = serializers.CharField(source="by.username", read_only=True)
model = serializers.CharField(source="content_type.model", read_only=True)
from_state = serializers.CharField(source="source_state", read_only=True)
to_state = serializers.CharField(source="state", read_only=True)
reason = serializers.CharField(source="description", read_only=True)
class Meta:
from django_fsm_log.models import StateLog
model = StateLog
fields = [
'id',
'timestamp',
'model',
'object_id',
'state',
'from_state',
'to_state',
'transition',
'user',
'description',
'reason',
"id",
"timestamp",
"model",
"object_id",
"state",
"from_state",
"to_state",
"transition",
"user",
"description",
"reason",
]
read_only_fields = fields
@@ -931,9 +884,7 @@ class PhotoSubmissionSerializer(serializers.ModelSerializer):
"""Serializer for PhotoSubmission."""
submitted_by = UserBasicSerializer(source="user", read_only=True)
content_type_name = serializers.CharField(
source="content_type.model", read_only=True
)
content_type_name = serializers.CharField(source="content_type.model", read_only=True)
photo_url = serializers.SerializerMethodField()
# UI Metadata
@@ -1012,4 +963,3 @@ class PhotoSubmissionSerializer(serializers.ModelSerializer):
else:
minutes = diff.seconds // 60
return f"{minutes} minutes ago"