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

@@ -0,0 +1,2 @@
# Import choices to trigger auto-registration with the global registry
from . import choices # noqa: F401

View File

@@ -0,0 +1,935 @@
"""
Rich Choice Objects for Moderation Domain
This module defines all choice options for the moderation system using the Rich Choice Objects pattern.
All choices include rich metadata for UI styling, business logic, and enhanced functionality.
"""
from apps.core.choices.base import RichChoice, ChoiceCategory
from apps.core.choices.registry import register_choices
# ============================================================================
# EditSubmission Choices
# ============================================================================
EDIT_SUBMISSION_STATUSES = [
RichChoice(
value="PENDING",
label="Pending",
description="Submission awaiting moderator review",
metadata={
'color': 'yellow',
'icon': 'clock',
'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
'sort_order': 1,
'can_transition_to': ['APPROVED', 'REJECTED', 'ESCALATED'],
'requires_moderator': True,
'is_actionable': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="APPROVED",
label="Approved",
description="Submission has been approved and changes applied",
metadata={
'color': 'green',
'icon': 'check-circle',
'css_class': 'bg-green-100 text-green-800 border-green-200',
'sort_order': 2,
'can_transition_to': [],
'requires_moderator': True,
'is_actionable': False,
'is_final': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="REJECTED",
label="Rejected",
description="Submission has been rejected and will not be applied",
metadata={
'color': 'red',
'icon': 'x-circle',
'css_class': 'bg-red-100 text-red-800 border-red-200',
'sort_order': 3,
'can_transition_to': [],
'requires_moderator': True,
'is_actionable': False,
'is_final': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="ESCALATED",
label="Escalated",
description="Submission has been escalated for higher-level review",
metadata={
'color': 'purple',
'icon': 'arrow-up',
'css_class': 'bg-purple-100 text-purple-800 border-purple-200',
'sort_order': 4,
'can_transition_to': ['APPROVED', 'REJECTED'],
'requires_moderator': True,
'is_actionable': True,
'escalation_level': 'admin'
},
category=ChoiceCategory.STATUS
),
]
SUBMISSION_TYPES = [
RichChoice(
value="EDIT",
label="Edit Existing",
description="Modification to existing content",
metadata={
'color': 'blue',
'icon': 'pencil',
'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
'sort_order': 1,
'requires_existing_object': True,
'complexity_level': 'medium'
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="CREATE",
label="Create New",
description="Creation of new content",
metadata={
'color': 'green',
'icon': 'plus-circle',
'css_class': 'bg-green-100 text-green-800 border-green-200',
'sort_order': 2,
'requires_existing_object': False,
'complexity_level': 'high'
},
category=ChoiceCategory.CLASSIFICATION
),
]
# ============================================================================
# ModerationReport Choices
# ============================================================================
MODERATION_REPORT_STATUSES = [
RichChoice(
value="PENDING",
label="Pending Review",
description="Report awaiting initial moderator review",
metadata={
'color': 'yellow',
'icon': 'clock',
'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
'sort_order': 1,
'can_transition_to': ['UNDER_REVIEW', 'DISMISSED'],
'requires_assignment': False,
'is_actionable': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="UNDER_REVIEW",
label="Under Review",
description="Report is actively being investigated by a moderator",
metadata={
'color': 'blue',
'icon': 'eye',
'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
'sort_order': 2,
'can_transition_to': ['RESOLVED', 'DISMISSED'],
'requires_assignment': True,
'is_actionable': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="RESOLVED",
label="Resolved",
description="Report has been resolved with appropriate action taken",
metadata={
'color': 'green',
'icon': 'check-circle',
'css_class': 'bg-green-100 text-green-800 border-green-200',
'sort_order': 3,
'can_transition_to': [],
'requires_assignment': True,
'is_actionable': False,
'is_final': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="DISMISSED",
label="Dismissed",
description="Report was reviewed but no action was necessary",
metadata={
'color': 'gray',
'icon': 'x-circle',
'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
'sort_order': 4,
'can_transition_to': [],
'requires_assignment': True,
'is_actionable': False,
'is_final': True
},
category=ChoiceCategory.STATUS
),
]
PRIORITY_LEVELS = [
RichChoice(
value="LOW",
label="Low",
description="Low priority - can be handled in regular workflow",
metadata={
'color': 'green',
'icon': 'arrow-down',
'css_class': 'bg-green-100 text-green-800 border-green-200',
'sort_order': 1,
'sla_hours': 168, # 7 days
'escalation_threshold': 240, # 10 days
'urgency_level': 1
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="MEDIUM",
label="Medium",
description="Medium priority - standard response time expected",
metadata={
'color': 'yellow',
'icon': 'minus',
'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
'sort_order': 2,
'sla_hours': 72, # 3 days
'escalation_threshold': 120, # 5 days
'urgency_level': 2
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="HIGH",
label="High",
description="High priority - requires prompt attention",
metadata={
'color': 'orange',
'icon': 'arrow-up',
'css_class': 'bg-orange-100 text-orange-800 border-orange-200',
'sort_order': 3,
'sla_hours': 24, # 1 day
'escalation_threshold': 48, # 2 days
'urgency_level': 3
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="URGENT",
label="Urgent",
description="Urgent priority - immediate attention required",
metadata={
'color': 'red',
'icon': 'exclamation',
'css_class': 'bg-red-100 text-red-800 border-red-200',
'sort_order': 4,
'sla_hours': 4, # 4 hours
'escalation_threshold': 8, # 8 hours
'urgency_level': 4,
'requires_immediate_notification': True
},
category=ChoiceCategory.CLASSIFICATION
),
]
REPORT_TYPES = [
RichChoice(
value="SPAM",
label="Spam",
description="Unwanted or repetitive content",
metadata={
'color': 'yellow',
'icon': 'ban',
'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
'sort_order': 1,
'default_priority': 'MEDIUM',
'auto_actions': ['content_review'],
'severity_level': 2
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="HARASSMENT",
label="Harassment",
description="Targeted harassment or bullying behavior",
metadata={
'color': 'red',
'icon': 'shield-exclamation',
'css_class': 'bg-red-100 text-red-800 border-red-200',
'sort_order': 2,
'default_priority': 'HIGH',
'auto_actions': ['user_review', 'content_review'],
'severity_level': 4,
'requires_user_action': True
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="INAPPROPRIATE_CONTENT",
label="Inappropriate Content",
description="Content that violates community guidelines",
metadata={
'color': 'orange',
'icon': 'exclamation-triangle',
'css_class': 'bg-orange-100 text-orange-800 border-orange-200',
'sort_order': 3,
'default_priority': 'HIGH',
'auto_actions': ['content_review'],
'severity_level': 3
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="MISINFORMATION",
label="Misinformation",
description="False or misleading information",
metadata={
'color': 'purple',
'icon': 'information-circle',
'css_class': 'bg-purple-100 text-purple-800 border-purple-200',
'sort_order': 4,
'default_priority': 'HIGH',
'auto_actions': ['content_review', 'fact_check'],
'severity_level': 3,
'requires_expert_review': True
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="COPYRIGHT",
label="Copyright Violation",
description="Unauthorized use of copyrighted material",
metadata={
'color': 'indigo',
'icon': 'document-duplicate',
'css_class': 'bg-indigo-100 text-indigo-800 border-indigo-200',
'sort_order': 5,
'default_priority': 'HIGH',
'auto_actions': ['content_review', 'legal_review'],
'severity_level': 4,
'requires_legal_review': True
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="PRIVACY",
label="Privacy Violation",
description="Unauthorized sharing of private information",
metadata={
'color': 'pink',
'icon': 'lock-closed',
'css_class': 'bg-pink-100 text-pink-800 border-pink-200',
'sort_order': 6,
'default_priority': 'URGENT',
'auto_actions': ['content_removal', 'user_review'],
'severity_level': 5,
'requires_immediate_action': True
},
category=ChoiceCategory.SECURITY
),
RichChoice(
value="HATE_SPEECH",
label="Hate Speech",
description="Content promoting hatred or discrimination",
metadata={
'color': 'red',
'icon': 'fire',
'css_class': 'bg-red-100 text-red-800 border-red-200',
'sort_order': 7,
'default_priority': 'URGENT',
'auto_actions': ['content_removal', 'user_suspension'],
'severity_level': 5,
'requires_immediate_action': True,
'zero_tolerance': True
},
category=ChoiceCategory.SECURITY
),
RichChoice(
value="VIOLENCE",
label="Violence or Threats",
description="Content containing violence or threatening behavior",
metadata={
'color': 'red',
'icon': 'exclamation',
'css_class': 'bg-red-100 text-red-800 border-red-200',
'sort_order': 8,
'default_priority': 'URGENT',
'auto_actions': ['content_removal', 'user_ban', 'law_enforcement_notification'],
'severity_level': 5,
'requires_immediate_action': True,
'zero_tolerance': True,
'requires_law_enforcement': True
},
category=ChoiceCategory.SECURITY
),
RichChoice(
value="OTHER",
label="Other",
description="Other issues not covered by specific categories",
metadata={
'color': 'gray',
'icon': 'dots-horizontal',
'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
'sort_order': 9,
'default_priority': 'MEDIUM',
'auto_actions': ['manual_review'],
'severity_level': 1,
'requires_manual_categorization': True
},
category=ChoiceCategory.CLASSIFICATION
),
]
# ============================================================================
# ModerationQueue Choices
# ============================================================================
MODERATION_QUEUE_STATUSES = [
RichChoice(
value="PENDING",
label="Pending",
description="Queue item awaiting assignment or action",
metadata={
'color': 'yellow',
'icon': 'clock',
'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
'sort_order': 1,
'can_transition_to': ['IN_PROGRESS', 'CANCELLED'],
'requires_assignment': False,
'is_actionable': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="IN_PROGRESS",
label="In Progress",
description="Queue item is actively being worked on",
metadata={
'color': 'blue',
'icon': 'play',
'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
'sort_order': 2,
'can_transition_to': ['COMPLETED', 'CANCELLED'],
'requires_assignment': True,
'is_actionable': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="COMPLETED",
label="Completed",
description="Queue item has been successfully completed",
metadata={
'color': 'green',
'icon': 'check-circle',
'css_class': 'bg-green-100 text-green-800 border-green-200',
'sort_order': 3,
'can_transition_to': [],
'requires_assignment': True,
'is_actionable': False,
'is_final': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="CANCELLED",
label="Cancelled",
description="Queue item was cancelled and will not be completed",
metadata={
'color': 'gray',
'icon': 'x-circle',
'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
'sort_order': 4,
'can_transition_to': [],
'requires_assignment': False,
'is_actionable': False,
'is_final': True
},
category=ChoiceCategory.STATUS
),
]
QUEUE_ITEM_TYPES = [
RichChoice(
value="CONTENT_REVIEW",
label="Content Review",
description="Review of user-submitted content for policy compliance",
metadata={
'color': 'blue',
'icon': 'document-text',
'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
'sort_order': 1,
'estimated_time_minutes': 15,
'required_permissions': ['content_moderation'],
'complexity_level': 'medium'
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="USER_REVIEW",
label="User Review",
description="Review of user account or behavior",
metadata={
'color': 'purple',
'icon': 'user',
'css_class': 'bg-purple-100 text-purple-800 border-purple-200',
'sort_order': 2,
'estimated_time_minutes': 30,
'required_permissions': ['user_moderation'],
'complexity_level': 'high'
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="BULK_ACTION",
label="Bulk Action",
description="Large-scale administrative operation",
metadata={
'color': 'indigo',
'icon': 'collection',
'css_class': 'bg-indigo-100 text-indigo-800 border-indigo-200',
'sort_order': 3,
'estimated_time_minutes': 60,
'required_permissions': ['bulk_operations'],
'complexity_level': 'high'
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="POLICY_VIOLATION",
label="Policy Violation",
description="Investigation of potential policy violations",
metadata={
'color': 'red',
'icon': 'shield-exclamation',
'css_class': 'bg-red-100 text-red-800 border-red-200',
'sort_order': 4,
'estimated_time_minutes': 45,
'required_permissions': ['policy_enforcement'],
'complexity_level': 'high'
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="APPEAL",
label="Appeal",
description="Review of user appeal against moderation action",
metadata={
'color': 'orange',
'icon': 'scale',
'css_class': 'bg-orange-100 text-orange-800 border-orange-200',
'sort_order': 5,
'estimated_time_minutes': 30,
'required_permissions': ['appeal_review'],
'complexity_level': 'high'
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="OTHER",
label="Other",
description="Other moderation tasks not covered by specific types",
metadata={
'color': 'gray',
'icon': 'dots-horizontal',
'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
'sort_order': 6,
'estimated_time_minutes': 20,
'required_permissions': ['general_moderation'],
'complexity_level': 'medium'
},
category=ChoiceCategory.CLASSIFICATION
),
]
# ============================================================================
# ModerationAction Choices
# ============================================================================
MODERATION_ACTION_TYPES = [
RichChoice(
value="WARNING",
label="Warning",
description="Formal warning issued to user",
metadata={
'color': 'yellow',
'icon': 'exclamation-triangle',
'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
'sort_order': 1,
'severity_level': 1,
'is_temporary': False,
'affects_privileges': False,
'escalation_path': ['USER_SUSPENSION']
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="USER_SUSPENSION",
label="User Suspension",
description="Temporary suspension of user account",
metadata={
'color': 'orange',
'icon': 'pause',
'css_class': 'bg-orange-100 text-orange-800 border-orange-200',
'sort_order': 2,
'severity_level': 3,
'is_temporary': True,
'affects_privileges': True,
'requires_duration': True,
'escalation_path': ['USER_BAN']
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="USER_BAN",
label="User Ban",
description="Permanent ban of user account",
metadata={
'color': 'red',
'icon': 'ban',
'css_class': 'bg-red-100 text-red-800 border-red-200',
'sort_order': 3,
'severity_level': 5,
'is_temporary': False,
'affects_privileges': True,
'is_permanent': True,
'requires_admin_approval': True
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="CONTENT_REMOVAL",
label="Content Removal",
description="Removal of specific content",
metadata={
'color': 'red',
'icon': 'trash',
'css_class': 'bg-red-100 text-red-800 border-red-200',
'sort_order': 4,
'severity_level': 2,
'is_temporary': False,
'affects_privileges': False,
'is_content_action': True
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="CONTENT_EDIT",
label="Content Edit",
description="Modification of content to comply with policies",
metadata={
'color': 'blue',
'icon': 'pencil',
'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
'sort_order': 5,
'severity_level': 1,
'is_temporary': False,
'affects_privileges': False,
'is_content_action': True,
'preserves_content': True
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="CONTENT_RESTRICTION",
label="Content Restriction",
description="Restriction of content visibility or access",
metadata={
'color': 'purple',
'icon': 'eye-off',
'css_class': 'bg-purple-100 text-purple-800 border-purple-200',
'sort_order': 6,
'severity_level': 2,
'is_temporary': True,
'affects_privileges': False,
'is_content_action': True,
'requires_duration': True
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="ACCOUNT_RESTRICTION",
label="Account Restriction",
description="Restriction of specific account privileges",
metadata={
'color': 'indigo',
'icon': 'lock-closed',
'css_class': 'bg-indigo-100 text-indigo-800 border-indigo-200',
'sort_order': 7,
'severity_level': 3,
'is_temporary': True,
'affects_privileges': True,
'requires_duration': True,
'escalation_path': ['USER_SUSPENSION']
},
category=ChoiceCategory.CLASSIFICATION
),
RichChoice(
value="OTHER",
label="Other",
description="Other moderation actions not covered by specific types",
metadata={
'color': 'gray',
'icon': 'dots-horizontal',
'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
'sort_order': 8,
'severity_level': 1,
'is_temporary': False,
'affects_privileges': False,
'requires_manual_review': True
},
category=ChoiceCategory.CLASSIFICATION
),
]
# ============================================================================
# BulkOperation Choices
# ============================================================================
BULK_OPERATION_STATUSES = [
RichChoice(
value="PENDING",
label="Pending",
description="Operation is queued and waiting to start",
metadata={
'color': 'yellow',
'icon': 'clock',
'css_class': 'bg-yellow-100 text-yellow-800 border-yellow-200',
'sort_order': 1,
'can_transition_to': ['RUNNING', 'CANCELLED'],
'is_actionable': True,
'can_cancel': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="RUNNING",
label="Running",
description="Operation is currently executing",
metadata={
'color': 'blue',
'icon': 'play',
'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
'sort_order': 2,
'can_transition_to': ['COMPLETED', 'FAILED', 'CANCELLED'],
'is_actionable': True,
'can_cancel': True,
'shows_progress': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="COMPLETED",
label="Completed",
description="Operation completed successfully",
metadata={
'color': 'green',
'icon': 'check-circle',
'css_class': 'bg-green-100 text-green-800 border-green-200',
'sort_order': 3,
'can_transition_to': [],
'is_actionable': False,
'can_cancel': False,
'is_final': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="FAILED",
label="Failed",
description="Operation failed with errors",
metadata={
'color': 'red',
'icon': 'x-circle',
'css_class': 'bg-red-100 text-red-800 border-red-200',
'sort_order': 4,
'can_transition_to': [],
'is_actionable': False,
'can_cancel': False,
'is_final': True,
'requires_investigation': True
},
category=ChoiceCategory.STATUS
),
RichChoice(
value="CANCELLED",
label="Cancelled",
description="Operation was cancelled before completion",
metadata={
'color': 'gray',
'icon': 'stop',
'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
'sort_order': 5,
'can_transition_to': [],
'is_actionable': False,
'can_cancel': False,
'is_final': True
},
category=ChoiceCategory.STATUS
),
]
BULK_OPERATION_TYPES = [
RichChoice(
value="UPDATE_PARKS",
label="Update Parks",
description="Bulk update operations on park data",
metadata={
'color': 'green',
'icon': 'map',
'css_class': 'bg-green-100 text-green-800 border-green-200',
'sort_order': 1,
'estimated_duration_minutes': 30,
'required_permissions': ['bulk_park_operations'],
'affects_data': ['parks'],
'risk_level': 'medium'
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="UPDATE_RIDES",
label="Update Rides",
description="Bulk update operations on ride data",
metadata={
'color': 'blue',
'icon': 'cog',
'css_class': 'bg-blue-100 text-blue-800 border-blue-200',
'sort_order': 2,
'estimated_duration_minutes': 45,
'required_permissions': ['bulk_ride_operations'],
'affects_data': ['rides'],
'risk_level': 'medium'
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="IMPORT_DATA",
label="Import Data",
description="Import data from external sources",
metadata={
'color': 'purple',
'icon': 'download',
'css_class': 'bg-purple-100 text-purple-800 border-purple-200',
'sort_order': 3,
'estimated_duration_minutes': 60,
'required_permissions': ['data_import'],
'affects_data': ['parks', 'rides', 'users'],
'risk_level': 'high'
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="EXPORT_DATA",
label="Export Data",
description="Export data for backup or analysis",
metadata={
'color': 'indigo',
'icon': 'upload',
'css_class': 'bg-indigo-100 text-indigo-800 border-indigo-200',
'sort_order': 4,
'estimated_duration_minutes': 20,
'required_permissions': ['data_export'],
'affects_data': [],
'risk_level': 'low'
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="MODERATE_CONTENT",
label="Moderate Content",
description="Bulk moderation actions on content",
metadata={
'color': 'orange',
'icon': 'shield-check',
'css_class': 'bg-orange-100 text-orange-800 border-orange-200',
'sort_order': 5,
'estimated_duration_minutes': 40,
'required_permissions': ['bulk_moderation'],
'affects_data': ['content', 'users'],
'risk_level': 'high'
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="USER_ACTIONS",
label="User Actions",
description="Bulk actions on user accounts",
metadata={
'color': 'red',
'icon': 'users',
'css_class': 'bg-red-100 text-red-800 border-red-200',
'sort_order': 6,
'estimated_duration_minutes': 50,
'required_permissions': ['bulk_user_operations'],
'affects_data': ['users'],
'risk_level': 'high'
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="CLEANUP",
label="Cleanup",
description="System cleanup and maintenance operations",
metadata={
'color': 'gray',
'icon': 'trash',
'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
'sort_order': 7,
'estimated_duration_minutes': 25,
'required_permissions': ['system_maintenance'],
'affects_data': ['system'],
'risk_level': 'low'
},
category=ChoiceCategory.TECHNICAL
),
RichChoice(
value="OTHER",
label="Other",
description="Other bulk operations not covered by specific types",
metadata={
'color': 'gray',
'icon': 'dots-horizontal',
'css_class': 'bg-gray-100 text-gray-800 border-gray-200',
'sort_order': 8,
'estimated_duration_minutes': 30,
'required_permissions': ['general_operations'],
'affects_data': [],
'risk_level': 'medium'
},
category=ChoiceCategory.TECHNICAL
),
]
# ============================================================================
# PhotoSubmission Choices (Shared with EditSubmission)
# ============================================================================
# PhotoSubmission uses the same STATUS_CHOICES as EditSubmission
PHOTO_SUBMISSION_STATUSES = EDIT_SUBMISSION_STATUSES
# ============================================================================
# Choice Registration
# ============================================================================
# Register all choice groups with the global registry
register_choices("edit_submission_statuses", EDIT_SUBMISSION_STATUSES, "moderation", "Edit submission status options")
register_choices("submission_types", SUBMISSION_TYPES, "moderation", "Submission type classifications")
register_choices("moderation_report_statuses", MODERATION_REPORT_STATUSES, "moderation", "Moderation report status options")
register_choices("priority_levels", PRIORITY_LEVELS, "moderation", "Priority level classifications")
register_choices("report_types", REPORT_TYPES, "moderation", "Report type classifications")
register_choices("moderation_queue_statuses", MODERATION_QUEUE_STATUSES, "moderation", "Moderation queue status options")
register_choices("queue_item_types", QUEUE_ITEM_TYPES, "moderation", "Queue item type classifications")
register_choices("moderation_action_types", MODERATION_ACTION_TYPES, "moderation", "Moderation action type classifications")
register_choices("bulk_operation_statuses", BULK_OPERATION_STATUSES, "moderation", "Bulk operation status options")
register_choices("bulk_operation_types", BULK_OPERATION_TYPES, "moderation", "Bulk operation type classifications")
register_choices("photo_submission_statuses", PHOTO_SUBMISSION_STATUSES, "moderation", "Photo submission status options")

View File

@@ -17,6 +17,7 @@ from .models import (
ModerationAction,
BulkOperation,
)
from apps.core.choices.registry import get_choices
User = get_user_model()
@@ -26,17 +27,20 @@ class ModerationReportFilter(django_filters.FilterSet):
# Status filters
status = django_filters.ChoiceFilter(
choices=ModerationReport.STATUS_CHOICES, help_text="Filter by report status"
choices=lambda: [(choice.value, choice.label) for choice in get_choices("moderation_report_statuses", "moderation")],
help_text="Filter by report status"
)
# Priority filters
priority = django_filters.ChoiceFilter(
choices=ModerationReport.PRIORITY_CHOICES, help_text="Filter by report priority"
choices=lambda: [(choice.value, choice.label) for choice in get_choices("priority_levels", "moderation")],
help_text="Filter by report priority"
)
# Report type filters
report_type = django_filters.ChoiceFilter(
choices=ModerationReport.REPORT_TYPE_CHOICES, help_text="Filter by report type"
choices=lambda: [(choice.value, choice.label) for choice in get_choices("report_types", "moderation")],
help_text="Filter by report type"
)
# User filters
@@ -125,7 +129,11 @@ class ModerationReportFilter(django_filters.FilterSet):
overdue_ids = []
for report in queryset.filter(status__in=["PENDING", "UNDER_REVIEW"]):
hours_since_created = (now - report.created_at).total_seconds() / 3600
if hours_since_created > sla_hours.get(report.priority, 24):
if report.priority in sla_hours:
threshold = sla_hours[report.priority]
else:
raise ValueError(f"Unknown priority level: {report.priority}")
if hours_since_created > threshold:
overdue_ids.append(report.id)
return queryset.filter(id__in=overdue_ids)
@@ -146,18 +154,20 @@ class ModerationQueueFilter(django_filters.FilterSet):
# Status filters
status = django_filters.ChoiceFilter(
choices=ModerationQueue.STATUS_CHOICES, help_text="Filter by queue item status"
choices=lambda: [(choice.value, choice.label) for choice in get_choices("moderation_queue_statuses", "moderation")],
help_text="Filter by queue item status"
)
# Priority filters
priority = django_filters.ChoiceFilter(
choices=ModerationQueue.PRIORITY_CHOICES,
choices=lambda: [(choice.value, choice.label) for choice in get_choices("priority_levels", "moderation")],
help_text="Filter by queue item priority",
)
# Item type filters
item_type = django_filters.ChoiceFilter(
choices=ModerationQueue.ITEM_TYPE_CHOICES, help_text="Filter by queue item type"
choices=lambda: [(choice.value, choice.label) for choice in get_choices("queue_item_types", "moderation")],
help_text="Filter by queue item type"
)
# Assignment filters
@@ -236,7 +246,8 @@ class ModerationActionFilter(django_filters.FilterSet):
# Action type filters
action_type = django_filters.ChoiceFilter(
choices=ModerationAction.ACTION_TYPE_CHOICES, help_text="Filter by action type"
choices=lambda: [(choice.value, choice.label) for choice in get_choices("moderation_action_types", "moderation")],
help_text="Filter by action type"
)
# User filters
@@ -332,18 +343,20 @@ class BulkOperationFilter(django_filters.FilterSet):
# Status filters
status = django_filters.ChoiceFilter(
choices=BulkOperation.STATUS_CHOICES, help_text="Filter by operation status"
choices=lambda: [(choice.value, choice.label) for choice in get_choices("bulk_operation_statuses", "moderation")],
help_text="Filter by operation status"
)
# Operation type filters
operation_type = django_filters.ChoiceFilter(
choices=BulkOperation.OPERATION_TYPE_CHOICES,
choices=lambda: [(choice.value, choice.label) for choice in get_choices("bulk_operation_types", "moderation")],
help_text="Filter by operation type",
)
# Priority filters
priority = django_filters.ChoiceFilter(
choices=BulkOperation.PRIORITY_CHOICES, help_text="Filter by operation priority"
choices=lambda: [(choice.value, choice.label) for choice in get_choices("priority_levels", "moderation")],
help_text="Filter by operation priority"
)
# User filters

View File

@@ -0,0 +1,470 @@
# Generated by Django 5.2.5 on 2025-09-15 17:35
import apps.core.choices.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("moderation", "0005_remove_photosubmission_insert_insert_and_more"),
]
operations = [
migrations.AlterField(
model_name="bulkoperation",
name="operation_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="bulk_operation_types",
choices=[
("UPDATE_PARKS", "Update Parks"),
("UPDATE_RIDES", "Update Rides"),
("IMPORT_DATA", "Import Data"),
("EXPORT_DATA", "Export Data"),
("MODERATE_CONTENT", "Moderate Content"),
("USER_ACTIONS", "User Actions"),
("CLEANUP", "Cleanup"),
("OTHER", "Other"),
],
domain="moderation",
max_length=50,
),
),
migrations.AlterField(
model_name="bulkoperation",
name="priority",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="priority_levels",
choices=[
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
("URGENT", "Urgent"),
],
default="MEDIUM",
domain="moderation",
max_length=10,
),
),
migrations.AlterField(
model_name="bulkoperation",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="bulk_operation_statuses",
choices=[
("PENDING", "Pending"),
("RUNNING", "Running"),
("COMPLETED", "Completed"),
("FAILED", "Failed"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
domain="moderation",
max_length=20,
),
),
migrations.AlterField(
model_name="bulkoperationevent",
name="operation_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="bulk_operation_types",
choices=[
("UPDATE_PARKS", "Update Parks"),
("UPDATE_RIDES", "Update Rides"),
("IMPORT_DATA", "Import Data"),
("EXPORT_DATA", "Export Data"),
("MODERATE_CONTENT", "Moderate Content"),
("USER_ACTIONS", "User Actions"),
("CLEANUP", "Cleanup"),
("OTHER", "Other"),
],
domain="moderation",
max_length=50,
),
),
migrations.AlterField(
model_name="bulkoperationevent",
name="priority",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="priority_levels",
choices=[
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
("URGENT", "Urgent"),
],
default="MEDIUM",
domain="moderation",
max_length=10,
),
),
migrations.AlterField(
model_name="bulkoperationevent",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="bulk_operation_statuses",
choices=[
("PENDING", "Pending"),
("RUNNING", "Running"),
("COMPLETED", "Completed"),
("FAILED", "Failed"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
domain="moderation",
max_length=20,
),
),
migrations.AlterField(
model_name="editsubmission",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="edit_submission_statuses",
choices=[
("PENDING", "Pending"),
("APPROVED", "Approved"),
("REJECTED", "Rejected"),
("ESCALATED", "Escalated"),
],
default="PENDING",
domain="moderation",
max_length=20,
),
),
migrations.AlterField(
model_name="editsubmission",
name="submission_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="submission_types",
choices=[("EDIT", "Edit Existing"), ("CREATE", "Create New")],
default="EDIT",
domain="moderation",
max_length=10,
),
),
migrations.AlterField(
model_name="editsubmissionevent",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="edit_submission_statuses",
choices=[
("PENDING", "Pending"),
("APPROVED", "Approved"),
("REJECTED", "Rejected"),
("ESCALATED", "Escalated"),
],
default="PENDING",
domain="moderation",
max_length=20,
),
),
migrations.AlterField(
model_name="editsubmissionevent",
name="submission_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="submission_types",
choices=[("EDIT", "Edit Existing"), ("CREATE", "Create New")],
default="EDIT",
domain="moderation",
max_length=10,
),
),
migrations.AlterField(
model_name="moderationaction",
name="action_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="moderation_action_types",
choices=[
("WARNING", "Warning"),
("USER_SUSPENSION", "User Suspension"),
("USER_BAN", "User Ban"),
("CONTENT_REMOVAL", "Content Removal"),
("CONTENT_EDIT", "Content Edit"),
("CONTENT_RESTRICTION", "Content Restriction"),
("ACCOUNT_RESTRICTION", "Account Restriction"),
("OTHER", "Other"),
],
domain="moderation",
max_length=50,
),
),
migrations.AlterField(
model_name="moderationactionevent",
name="action_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="moderation_action_types",
choices=[
("WARNING", "Warning"),
("USER_SUSPENSION", "User Suspension"),
("USER_BAN", "User Ban"),
("CONTENT_REMOVAL", "Content Removal"),
("CONTENT_EDIT", "Content Edit"),
("CONTENT_RESTRICTION", "Content Restriction"),
("ACCOUNT_RESTRICTION", "Account Restriction"),
("OTHER", "Other"),
],
domain="moderation",
max_length=50,
),
),
migrations.AlterField(
model_name="moderationqueue",
name="item_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="queue_item_types",
choices=[
("CONTENT_REVIEW", "Content Review"),
("USER_REVIEW", "User Review"),
("BULK_ACTION", "Bulk Action"),
("POLICY_VIOLATION", "Policy Violation"),
("APPEAL", "Appeal"),
("OTHER", "Other"),
],
domain="moderation",
max_length=50,
),
),
migrations.AlterField(
model_name="moderationqueue",
name="priority",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="priority_levels",
choices=[
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
("URGENT", "Urgent"),
],
default="MEDIUM",
domain="moderation",
max_length=10,
),
),
migrations.AlterField(
model_name="moderationqueue",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="moderation_queue_statuses",
choices=[
("PENDING", "Pending"),
("IN_PROGRESS", "In Progress"),
("COMPLETED", "Completed"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
domain="moderation",
max_length=20,
),
),
migrations.AlterField(
model_name="moderationqueueevent",
name="item_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="queue_item_types",
choices=[
("CONTENT_REVIEW", "Content Review"),
("USER_REVIEW", "User Review"),
("BULK_ACTION", "Bulk Action"),
("POLICY_VIOLATION", "Policy Violation"),
("APPEAL", "Appeal"),
("OTHER", "Other"),
],
domain="moderation",
max_length=50,
),
),
migrations.AlterField(
model_name="moderationqueueevent",
name="priority",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="priority_levels",
choices=[
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
("URGENT", "Urgent"),
],
default="MEDIUM",
domain="moderation",
max_length=10,
),
),
migrations.AlterField(
model_name="moderationqueueevent",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="moderation_queue_statuses",
choices=[
("PENDING", "Pending"),
("IN_PROGRESS", "In Progress"),
("COMPLETED", "Completed"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
domain="moderation",
max_length=20,
),
),
migrations.AlterField(
model_name="moderationreport",
name="priority",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="priority_levels",
choices=[
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
("URGENT", "Urgent"),
],
default="MEDIUM",
domain="moderation",
max_length=10,
),
),
migrations.AlterField(
model_name="moderationreport",
name="report_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="report_types",
choices=[
("SPAM", "Spam"),
("HARASSMENT", "Harassment"),
("INAPPROPRIATE_CONTENT", "Inappropriate Content"),
("MISINFORMATION", "Misinformation"),
("COPYRIGHT", "Copyright Violation"),
("PRIVACY", "Privacy Violation"),
("HATE_SPEECH", "Hate Speech"),
("VIOLENCE", "Violence or Threats"),
("OTHER", "Other"),
],
domain="moderation",
max_length=50,
),
),
migrations.AlterField(
model_name="moderationreport",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="moderation_report_statuses",
choices=[
("PENDING", "Pending Review"),
("UNDER_REVIEW", "Under Review"),
("RESOLVED", "Resolved"),
("DISMISSED", "Dismissed"),
],
default="PENDING",
domain="moderation",
max_length=20,
),
),
migrations.AlterField(
model_name="moderationreportevent",
name="priority",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="priority_levels",
choices=[
("LOW", "Low"),
("MEDIUM", "Medium"),
("HIGH", "High"),
("URGENT", "Urgent"),
],
default="MEDIUM",
domain="moderation",
max_length=10,
),
),
migrations.AlterField(
model_name="moderationreportevent",
name="report_type",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="report_types",
choices=[
("SPAM", "Spam"),
("HARASSMENT", "Harassment"),
("INAPPROPRIATE_CONTENT", "Inappropriate Content"),
("MISINFORMATION", "Misinformation"),
("COPYRIGHT", "Copyright Violation"),
("PRIVACY", "Privacy Violation"),
("HATE_SPEECH", "Hate Speech"),
("VIOLENCE", "Violence or Threats"),
("OTHER", "Other"),
],
domain="moderation",
max_length=50,
),
),
migrations.AlterField(
model_name="moderationreportevent",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="moderation_report_statuses",
choices=[
("PENDING", "Pending Review"),
("UNDER_REVIEW", "Under Review"),
("RESOLVED", "Resolved"),
("DISMISSED", "Dismissed"),
],
default="PENDING",
domain="moderation",
max_length=20,
),
),
migrations.AlterField(
model_name="photosubmission",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="photo_submission_statuses",
choices=[
("PENDING", "Pending"),
("APPROVED", "Approved"),
("REJECTED", "Rejected"),
("ESCALATED", "Escalated"),
],
default="PENDING",
domain="moderation",
max_length=20,
),
),
migrations.AlterField(
model_name="photosubmissionevent",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="photo_submission_statuses",
choices=[
("PENDING", "Pending"),
("APPROVED", "Approved"),
("REJECTED", "Rejected"),
("ESCALATED", "Escalated"),
],
default="PENDING",
domain="moderation",
max_length=20,
),
),
]

View File

@@ -23,6 +23,7 @@ from django.contrib.auth.models import AnonymousUser
from datetime import timedelta
import pghistory
from apps.core.history import TrackedModel
from apps.core.choices.fields import RichChoiceField
UserType = Union[AbstractBaseUser, AnonymousUser]
@@ -33,17 +34,6 @@ UserType = Union[AbstractBaseUser, AnonymousUser]
@pghistory.track() # Track all changes by default
class EditSubmission(TrackedModel):
STATUS_CHOICES = [
("PENDING", "Pending"),
("APPROVED", "Approved"),
("REJECTED", "Rejected"),
("ESCALATED", "Escalated"),
]
SUBMISSION_TYPE_CHOICES = [
("EDIT", "Edit Existing"),
("CREATE", "Create New"),
]
# Who submitted the edit
user = models.ForeignKey(
@@ -60,8 +50,11 @@ class EditSubmission(TrackedModel):
content_object = GenericForeignKey("content_type", "object_id")
# Type of submission
submission_type = models.CharField(
max_length=10, choices=SUBMISSION_TYPE_CHOICES, default="EDIT"
submission_type = RichChoiceField(
choice_group="submission_types",
domain="moderation",
max_length=10,
default="EDIT"
)
# The actual changes/data
@@ -81,7 +74,12 @@ class EditSubmission(TrackedModel):
source = models.TextField(
blank=True, help_text="Source of information (if applicable)"
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING")
status = RichChoiceField(
choice_group="edit_submission_statuses",
domain="moderation",
max_length=20,
default="PENDING"
)
created_at = models.DateTimeField(auto_now_add=True)
# Review details
@@ -124,11 +122,11 @@ class EditSubmission(TrackedModel):
field = model_class._meta.get_field(field_name)
if isinstance(field, models.ForeignKey) and value is not None:
try:
related_obj = field.related_model.objects.get(pk=value)
related_obj = field.related_model.objects.get(pk=value) # type: ignore
resolved_data[field_name] = related_obj
except ObjectDoesNotExist:
raise ValueError(
f"Related object {field.related_model.__name__} with pk={value} does not exist"
f"Related object {field.related_model.__name__} with pk={value} does not exist" # type: ignore
)
except FieldDoesNotExist:
# Field doesn't exist on model, skip it
@@ -258,37 +256,24 @@ class ModerationReport(TrackedModel):
or behavior that needs moderator attention.
"""
STATUS_CHOICES = [
('PENDING', 'Pending Review'),
('UNDER_REVIEW', 'Under Review'),
('RESOLVED', 'Resolved'),
('DISMISSED', 'Dismissed'),
]
PRIORITY_CHOICES = [
('LOW', 'Low'),
('MEDIUM', 'Medium'),
('HIGH', 'High'),
('URGENT', 'Urgent'),
]
REPORT_TYPE_CHOICES = [
('SPAM', 'Spam'),
('HARASSMENT', 'Harassment'),
('INAPPROPRIATE_CONTENT', 'Inappropriate Content'),
('MISINFORMATION', 'Misinformation'),
('COPYRIGHT', 'Copyright Violation'),
('PRIVACY', 'Privacy Violation'),
('HATE_SPEECH', 'Hate Speech'),
('VIOLENCE', 'Violence or Threats'),
('OTHER', 'Other'),
]
# Report details
report_type = models.CharField(max_length=50, choices=REPORT_TYPE_CHOICES)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
priority = models.CharField(
max_length=10, choices=PRIORITY_CHOICES, default='MEDIUM')
report_type = RichChoiceField(
choice_group="report_types",
domain="moderation",
max_length=50
)
status = RichChoiceField(
choice_group="moderation_report_statuses",
domain="moderation",
max_length=20,
default='PENDING'
)
priority = RichChoiceField(
choice_group="priority_levels",
domain="moderation",
max_length=10,
default='MEDIUM'
)
# What is being reported
reported_entity_type = models.CharField(
@@ -339,7 +324,7 @@ class ModerationReport(TrackedModel):
]
def __str__(self):
return f"{self.get_report_type_display()} report by {self.reported_by.username}"
return f"{self.get_report_type_display()} report by {self.reported_by.username}" # type: ignore
@pghistory.track()
@@ -351,34 +336,24 @@ class ModerationQueue(TrackedModel):
separate from the initial reports.
"""
STATUS_CHOICES = [
('PENDING', 'Pending'),
('IN_PROGRESS', 'In Progress'),
('COMPLETED', 'Completed'),
('CANCELLED', 'Cancelled'),
]
PRIORITY_CHOICES = [
('LOW', 'Low'),
('MEDIUM', 'Medium'),
('HIGH', 'High'),
('URGENT', 'Urgent'),
]
ITEM_TYPE_CHOICES = [
('CONTENT_REVIEW', 'Content Review'),
('USER_REVIEW', 'User Review'),
('BULK_ACTION', 'Bulk Action'),
('POLICY_VIOLATION', 'Policy Violation'),
('APPEAL', 'Appeal'),
('OTHER', 'Other'),
]
# Queue item details
item_type = models.CharField(max_length=50, choices=ITEM_TYPE_CHOICES)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
priority = models.CharField(
max_length=10, choices=PRIORITY_CHOICES, default='MEDIUM')
item_type = RichChoiceField(
choice_group="queue_item_types",
domain="moderation",
max_length=50
)
status = RichChoiceField(
choice_group="moderation_queue_statuses",
domain="moderation",
max_length=20,
default='PENDING'
)
priority = RichChoiceField(
choice_group="priority_levels",
domain="moderation",
max_length=10,
default='MEDIUM'
)
title = models.CharField(max_length=200, help_text="Brief title for the queue item")
description = models.TextField(
@@ -439,7 +414,7 @@ class ModerationQueue(TrackedModel):
]
def __str__(self):
return f"{self.get_item_type_display()}: {self.title}"
return f"{self.get_item_type_display()}: {self.title}" # type: ignore
@pghistory.track()
@@ -451,19 +426,12 @@ class ModerationAction(TrackedModel):
warnings, suspensions, content removal, etc.
"""
ACTION_TYPE_CHOICES = [
('WARNING', 'Warning'),
('USER_SUSPENSION', 'User Suspension'),
('USER_BAN', 'User Ban'),
('CONTENT_REMOVAL', 'Content Removal'),
('CONTENT_EDIT', 'Content Edit'),
('CONTENT_RESTRICTION', 'Content Restriction'),
('ACCOUNT_RESTRICTION', 'Account Restriction'),
('OTHER', 'Other'),
]
# Action details
action_type = models.CharField(max_length=50, choices=ACTION_TYPE_CHOICES)
action_type = RichChoiceField(
choice_group="moderation_action_types",
domain="moderation",
max_length=50
)
reason = models.CharField(max_length=200, help_text="Brief reason for the action")
details = models.TextField(help_text="Detailed explanation of the action")
@@ -513,7 +481,7 @@ class ModerationAction(TrackedModel):
]
def __str__(self):
return f"{self.get_action_type_display()} against {self.target_user.username} by {self.moderator.username}"
return f"{self.get_action_type_display()} against {self.target_user.username} by {self.moderator.username}" # type: ignore
def save(self, *args, **kwargs):
# Set expiration time if duration is provided
@@ -531,37 +499,24 @@ class BulkOperation(TrackedModel):
imports, exports, or mass moderation actions.
"""
STATUS_CHOICES = [
('PENDING', 'Pending'),
('RUNNING', 'Running'),
('COMPLETED', 'Completed'),
('FAILED', 'Failed'),
('CANCELLED', 'Cancelled'),
]
PRIORITY_CHOICES = [
('LOW', 'Low'),
('MEDIUM', 'Medium'),
('HIGH', 'High'),
('URGENT', 'Urgent'),
]
OPERATION_TYPE_CHOICES = [
('UPDATE_PARKS', 'Update Parks'),
('UPDATE_RIDES', 'Update Rides'),
('IMPORT_DATA', 'Import Data'),
('EXPORT_DATA', 'Export Data'),
('MODERATE_CONTENT', 'Moderate Content'),
('USER_ACTIONS', 'User Actions'),
('CLEANUP', 'Cleanup'),
('OTHER', 'Other'),
]
# Operation details
operation_type = models.CharField(max_length=50, choices=OPERATION_TYPE_CHOICES)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
priority = models.CharField(
max_length=10, choices=PRIORITY_CHOICES, default='MEDIUM')
operation_type = RichChoiceField(
choice_group="bulk_operation_types",
domain="moderation",
max_length=50
)
status = RichChoiceField(
choice_group="bulk_operation_statuses",
domain="moderation",
max_length=20,
default='PENDING'
)
priority = RichChoiceField(
choice_group="priority_levels",
domain="moderation",
max_length=10,
default='MEDIUM'
)
description = models.TextField(help_text="Description of what this operation does")
# Operation parameters and results
@@ -614,7 +569,7 @@ class BulkOperation(TrackedModel):
]
def __str__(self):
return f"{self.get_operation_type_display()}: {self.description[:50]}"
return f"{self.get_operation_type_display()}: {self.description[:50]}" # type: ignore
@property
def progress_percentage(self):
@@ -626,12 +581,6 @@ class BulkOperation(TrackedModel):
@pghistory.track() # Track all changes by default
class PhotoSubmission(TrackedModel):
STATUS_CHOICES = [
("PENDING", "Pending"),
("APPROVED", "Approved"),
("REJECTED", "Rejected"),
("ESCALATED", "Escalated"),
]
# Who submitted the photo
user = models.ForeignKey(
@@ -655,7 +604,12 @@ class PhotoSubmission(TrackedModel):
date_taken = models.DateField(null=True, blank=True)
# Metadata
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING")
status = RichChoiceField(
choice_group="photo_submission_statuses",
domain="moderation",
max_length=20,
default="PENDING"
)
created_at = models.DateTimeField(auto_now_add=True)
# Review details

View File

@@ -127,8 +127,13 @@ class ModerationReportSerializer(serializers.ModelSerializer):
# Define SLA hours by priority
sla_hours = {"URGENT": 2, "HIGH": 8, "MEDIUM": 24, "LOW": 72}
if obj.priority in sla_hours:
threshold = sla_hours[obj.priority]
else:
raise ValueError(f"Unknown priority level: {obj.priority}")
return hours_since_created > sla_hours.get(obj.priority, 24)
return hours_since_created > threshold
def get_time_since_created(self, obj) -> str:
"""Human-readable time since creation."""
@@ -345,12 +350,12 @@ class CompleteQueueItemSerializer(serializers.Serializer):
action = serializers.ChoiceField(
choices=[
"NO_ACTION",
"CONTENT_REMOVED",
"CONTENT_EDITED",
"USER_WARNING",
"USER_SUSPENDED",
"USER_BANNED",
("NO_ACTION", "No Action Required"),
("CONTENT_REMOVED", "Content Removed"),
("CONTENT_EDITED", "Content Edited"),
("USER_WARNING", "User Warning Issued"),
("USER_SUSPENDED", "User Suspended"),
("USER_BANNED", "User Banned"),
]
)
notes = serializers.CharField(required=False, allow_blank=True)
@@ -722,7 +727,14 @@ class UserModerationProfileSerializer(serializers.Serializer):
active_restrictions = serializers.IntegerField()
# Risk assessment
risk_level = serializers.ChoiceField(choices=["LOW", "MEDIUM", "HIGH", "CRITICAL"])
risk_level = serializers.ChoiceField(
choices=[
("LOW", "Low Risk"),
("MEDIUM", "Medium Risk"),
("HIGH", "High Risk"),
("CRITICAL", "Critical Risk"),
]
)
risk_factors = serializers.ListField(child=serializers.CharField())
# Recent activity

View File

@@ -181,7 +181,11 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
for report in queryset.filter(status__in=["PENDING", "UNDER_REVIEW"]):
sla_hours = {"URGENT": 2, "HIGH": 8, "MEDIUM": 24, "LOW": 72}
hours_since_created = (now - report.created_at).total_seconds() / 3600
if hours_since_created > sla_hours.get(report.priority, 24):
if report.priority in sla_hours:
threshold = sla_hours[report.priority]
else:
raise ValueError(f"Unknown priority level: {report.priority}")
if hours_since_created > threshold:
overdue_reports += 1
# Reports by priority and type