mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-02 01:27:03 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -17,7 +17,7 @@ are registered via the callback configuration defined in each model's Meta class
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any, Union
|
||||
from typing import Any
|
||||
|
||||
import pghistory
|
||||
from django.conf import settings
|
||||
@@ -33,7 +33,7 @@ from apps.core.choices.fields import RichChoiceField
|
||||
from apps.core.history import TrackedModel
|
||||
from apps.core.state_machine import RichFSMField, StateMachineMixin
|
||||
|
||||
UserType = Union[AbstractBaseUser, AnonymousUser]
|
||||
UserType = AbstractBaseUser | AnonymousUser
|
||||
|
||||
|
||||
# Lazy callback imports to avoid circular dependencies
|
||||
@@ -45,11 +45,12 @@ def _get_notification_callbacks():
|
||||
SubmissionEscalatedNotification,
|
||||
SubmissionRejectedNotification,
|
||||
)
|
||||
|
||||
return {
|
||||
'approved': SubmissionApprovedNotification,
|
||||
'rejected': SubmissionRejectedNotification,
|
||||
'escalated': SubmissionEscalatedNotification,
|
||||
'moderation': ModerationNotificationCallback,
|
||||
"approved": SubmissionApprovedNotification,
|
||||
"rejected": SubmissionRejectedNotification,
|
||||
"escalated": SubmissionEscalatedNotification,
|
||||
"moderation": ModerationNotificationCallback,
|
||||
}
|
||||
|
||||
|
||||
@@ -59,9 +60,10 @@ def _get_cache_callbacks():
|
||||
CacheInvalidationCallback,
|
||||
ModerationCacheInvalidation,
|
||||
)
|
||||
|
||||
return {
|
||||
'generic': CacheInvalidationCallback,
|
||||
'moderation': ModerationCacheInvalidation,
|
||||
"generic": CacheInvalidationCallback,
|
||||
"moderation": ModerationCacheInvalidation,
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +71,7 @@ def _get_cache_callbacks():
|
||||
# Original EditSubmission Model (Preserved)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pghistory.track() # Track all changes by default
|
||||
class EditSubmission(StateMachineMixin, TrackedModel):
|
||||
"""Edit submission model with FSM-managed status transitions."""
|
||||
@@ -98,16 +101,11 @@ class EditSubmission(StateMachineMixin, TrackedModel):
|
||||
|
||||
# Type of submission
|
||||
submission_type = RichChoiceField(
|
||||
choice_group="submission_types",
|
||||
domain="moderation",
|
||||
max_length=10,
|
||||
default="EDIT"
|
||||
choice_group="submission_types", domain="moderation", max_length=10, default="EDIT"
|
||||
)
|
||||
|
||||
# The actual changes/data
|
||||
changes = models.JSONField(
|
||||
help_text="JSON representation of the changes or new object data"
|
||||
)
|
||||
changes = models.JSONField(help_text="JSON representation of the changes or new object data")
|
||||
|
||||
# Moderator's edited version of changes before approval
|
||||
moderator_changes = models.JSONField(
|
||||
@@ -118,14 +116,9 @@ class EditSubmission(StateMachineMixin, TrackedModel):
|
||||
|
||||
# Metadata
|
||||
reason = models.TextField(help_text="Why this edit/addition is needed")
|
||||
source = models.TextField(
|
||||
blank=True, help_text="Source of information (if applicable)"
|
||||
)
|
||||
source = models.TextField(blank=True, help_text="Source of information (if applicable)")
|
||||
status = RichFSMField(
|
||||
choice_group="edit_submission_statuses",
|
||||
domain="moderation",
|
||||
max_length=20,
|
||||
default="PENDING"
|
||||
choice_group="edit_submission_statuses", domain="moderation", max_length=20, default="PENDING"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -138,12 +131,8 @@ class EditSubmission(StateMachineMixin, TrackedModel):
|
||||
related_name="handled_submissions",
|
||||
help_text="Moderator who handled this submission",
|
||||
)
|
||||
handled_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When this submission was handled"
|
||||
)
|
||||
notes = models.TextField(
|
||||
blank=True, help_text="Notes from the moderator about this submission"
|
||||
)
|
||||
handled_at = models.DateTimeField(null=True, blank=True, help_text="When this submission was handled")
|
||||
notes = models.TextField(blank=True, help_text="Notes from the moderator about this submission")
|
||||
|
||||
# Claim tracking for concurrency control
|
||||
claimed_by = models.ForeignKey(
|
||||
@@ -154,9 +143,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
|
||||
related_name="claimed_edit_submissions",
|
||||
help_text="Moderator who has claimed this submission for review",
|
||||
)
|
||||
claimed_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When this submission was claimed"
|
||||
)
|
||||
claimed_at = models.DateTimeField(null=True, blank=True, help_text="When this submission was claimed")
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "Edit Submission"
|
||||
@@ -187,12 +174,12 @@ class EditSubmission(StateMachineMixin, 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) # type: ignore
|
||||
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" # type: ignore
|
||||
)
|
||||
f"Related object {field.related_model.__name__} with pk={value} does not exist" # type: ignore
|
||||
) from None
|
||||
except FieldDoesNotExist:
|
||||
# Field doesn't exist on model, skip it
|
||||
continue
|
||||
@@ -217,9 +204,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
if self.status != "PENDING":
|
||||
raise ValidationError(
|
||||
f"Cannot claim submission: current status is {self.status}, expected PENDING"
|
||||
)
|
||||
raise ValidationError(f"Cannot claim submission: current status is {self.status}, expected PENDING")
|
||||
|
||||
self.transition_to_claimed(user=user)
|
||||
self.claimed_by = user
|
||||
@@ -240,9 +225,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
if self.status != "CLAIMED":
|
||||
raise ValidationError(
|
||||
f"Cannot unclaim submission: current status is {self.status}, expected CLAIMED"
|
||||
)
|
||||
raise ValidationError(f"Cannot unclaim submission: current status is {self.status}, expected CLAIMED")
|
||||
|
||||
# Set status directly (not via FSM transition to avoid cycle)
|
||||
# This is intentional - the unclaim action is a special "rollback" operation
|
||||
@@ -274,9 +257,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
|
||||
|
||||
# Validate state - must be CLAIMED before approval
|
||||
if self.status != "CLAIMED":
|
||||
raise ValidationError(
|
||||
f"Cannot approve submission: must be CLAIMED first (current status: {self.status})"
|
||||
)
|
||||
raise ValidationError(f"Cannot approve submission: must be CLAIMED first (current status: {self.status})")
|
||||
|
||||
model_class = self.content_type.model_class()
|
||||
if not model_class:
|
||||
@@ -341,9 +322,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
|
||||
|
||||
# Validate state - must be CLAIMED before rejection
|
||||
if self.status != "CLAIMED":
|
||||
raise ValidationError(
|
||||
f"Cannot reject submission: must be CLAIMED first (current status: {self.status})"
|
||||
)
|
||||
raise ValidationError(f"Cannot reject submission: must be CLAIMED first (current status: {self.status})")
|
||||
|
||||
# Use FSM transition to update status
|
||||
self.transition_to_rejected(user=rejecter)
|
||||
@@ -369,9 +348,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
|
||||
|
||||
# Validate state - must be CLAIMED before escalation
|
||||
if self.status != "CLAIMED":
|
||||
raise ValidationError(
|
||||
f"Cannot escalate submission: must be CLAIMED first (current status: {self.status})"
|
||||
)
|
||||
raise ValidationError(f"Cannot escalate submission: must be CLAIMED first (current status: {self.status})")
|
||||
|
||||
# Use FSM transition to update status
|
||||
self.transition_to_escalated(user=escalator)
|
||||
@@ -395,6 +372,7 @@ class EditSubmission(StateMachineMixin, TrackedModel):
|
||||
# New Moderation System Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
class ModerationReport(StateMachineMixin, TrackedModel):
|
||||
"""
|
||||
@@ -407,43 +385,29 @@ class ModerationReport(StateMachineMixin, TrackedModel):
|
||||
state_field_name = "status"
|
||||
|
||||
# Report details
|
||||
report_type = RichChoiceField(
|
||||
choice_group="report_types",
|
||||
domain="moderation",
|
||||
max_length=50
|
||||
)
|
||||
report_type = RichChoiceField(choice_group="report_types", domain="moderation", max_length=50)
|
||||
status = RichFSMField(
|
||||
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'
|
||||
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(
|
||||
max_length=50, help_text="Type of entity being reported (park, ride, user, etc.)")
|
||||
reported_entity_id = models.PositiveIntegerField(
|
||||
help_text="ID of the entity being reported")
|
||||
content_type = models.ForeignKey(
|
||||
ContentType, on_delete=models.CASCADE, null=True, blank=True)
|
||||
max_length=50, help_text="Type of entity being reported (park, ride, user, etc.)"
|
||||
)
|
||||
reported_entity_id = models.PositiveIntegerField(help_text="ID of the entity being reported")
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
|
||||
|
||||
# Report content
|
||||
reason = models.CharField(max_length=200, help_text="Brief reason for the report")
|
||||
description = models.TextField(help_text="Detailed description of the issue")
|
||||
evidence_urls = models.JSONField(
|
||||
default=list, blank=True, help_text="URLs to evidence (screenshots, etc.)")
|
||||
evidence_urls = models.JSONField(default=list, blank=True, help_text="URLs to evidence (screenshots, etc.)")
|
||||
|
||||
# Users involved
|
||||
reported_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='moderation_reports_made',
|
||||
related_name="moderation_reports_made",
|
||||
help_text="User who made this report",
|
||||
)
|
||||
assigned_moderator = models.ForeignKey(
|
||||
@@ -451,40 +415,32 @@ class ModerationReport(StateMachineMixin, TrackedModel):
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='assigned_moderation_reports',
|
||||
related_name="assigned_moderation_reports",
|
||||
help_text="Moderator assigned to handle this report",
|
||||
)
|
||||
|
||||
# Resolution
|
||||
resolution_action = models.CharField(
|
||||
max_length=100, blank=True, help_text="Action taken to resolve")
|
||||
resolution_notes = models.TextField(
|
||||
blank=True, help_text="Notes about the resolution")
|
||||
resolved_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When this report was resolved"
|
||||
)
|
||||
resolution_action = models.CharField(max_length=100, blank=True, help_text="Action taken to resolve")
|
||||
resolution_notes = models.TextField(blank=True, help_text="Notes about the resolution")
|
||||
resolved_at = models.DateTimeField(null=True, blank=True, help_text="When this report was resolved")
|
||||
|
||||
# Timestamps
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this report was created"
|
||||
)
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True, help_text="When this report was last updated"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, help_text="When this report was created")
|
||||
updated_at = models.DateTimeField(auto_now=True, help_text="When this report was last updated")
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "Moderation Report"
|
||||
verbose_name_plural = "Moderation Reports"
|
||||
ordering = ['-created_at']
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=['status', 'priority']),
|
||||
models.Index(fields=['reported_by']),
|
||||
models.Index(fields=['assigned_moderator']),
|
||||
models.Index(fields=['created_at']),
|
||||
models.Index(fields=["status", "priority"]),
|
||||
models.Index(fields=["reported_by"]),
|
||||
models.Index(fields=["assigned_moderator"]),
|
||||
models.Index(fields=["created_at"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_report_type_display()} report by {self.reported_by.username}" # type: ignore
|
||||
return f"{self.get_report_type_display()} report by {self.reported_by.username}" # type: ignore
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
@@ -499,37 +455,20 @@ class ModerationQueue(StateMachineMixin, TrackedModel):
|
||||
state_field_name = "status"
|
||||
|
||||
# Queue item details
|
||||
item_type = RichChoiceField(
|
||||
choice_group="queue_item_types",
|
||||
domain="moderation",
|
||||
max_length=50
|
||||
)
|
||||
item_type = RichChoiceField(choice_group="queue_item_types", domain="moderation", max_length=50)
|
||||
status = RichFSMField(
|
||||
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'
|
||||
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(
|
||||
help_text="Detailed description of what needs to be done")
|
||||
description = models.TextField(help_text="Detailed description of what needs to be done")
|
||||
|
||||
# What entity this relates to
|
||||
entity_type = models.CharField(
|
||||
max_length=50, blank=True, help_text="Type of entity (park, ride, user, etc.)")
|
||||
entity_id = models.PositiveIntegerField(
|
||||
null=True, blank=True, help_text="ID of the related entity")
|
||||
entity_preview = models.JSONField(
|
||||
default=dict, blank=True, help_text="Preview data for the entity")
|
||||
content_type = models.ForeignKey(
|
||||
ContentType, on_delete=models.CASCADE, null=True, blank=True)
|
||||
entity_type = models.CharField(max_length=50, blank=True, help_text="Type of entity (park, ride, user, etc.)")
|
||||
entity_id = models.PositiveIntegerField(null=True, blank=True, help_text="ID of the related entity")
|
||||
entity_preview = models.JSONField(default=dict, blank=True, help_text="Preview data for the entity")
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
|
||||
|
||||
# Assignment and timing
|
||||
assigned_to = models.ForeignKey(
|
||||
@@ -537,14 +476,11 @@ class ModerationQueue(StateMachineMixin, TrackedModel):
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='assigned_queue_items',
|
||||
related_name="assigned_queue_items",
|
||||
help_text="Moderator assigned to this item",
|
||||
)
|
||||
assigned_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When this item was assigned"
|
||||
)
|
||||
estimated_review_time = models.PositiveIntegerField(
|
||||
default=30, help_text="Estimated time in minutes")
|
||||
assigned_at = models.DateTimeField(null=True, blank=True, help_text="When this item was assigned")
|
||||
estimated_review_time = models.PositiveIntegerField(default=30, help_text="Estimated time in minutes")
|
||||
|
||||
# Metadata
|
||||
flagged_by = models.ForeignKey(
|
||||
@@ -552,11 +488,10 @@ class ModerationQueue(StateMachineMixin, TrackedModel):
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='flagged_queue_items',
|
||||
related_name="flagged_queue_items",
|
||||
help_text="User who flagged this item",
|
||||
)
|
||||
tags = models.JSONField(default=list, blank=True,
|
||||
help_text="Tags for categorization")
|
||||
tags = models.JSONField(default=list, blank=True, help_text="Tags for categorization")
|
||||
|
||||
# Related objects
|
||||
related_report = models.ForeignKey(
|
||||
@@ -564,30 +499,26 @@ class ModerationQueue(StateMachineMixin, TrackedModel):
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='queue_items',
|
||||
related_name="queue_items",
|
||||
help_text="Related moderation report",
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this item was created"
|
||||
)
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True, help_text="When this item was last updated"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, help_text="When this item was created")
|
||||
updated_at = models.DateTimeField(auto_now=True, help_text="When this item was last updated")
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "Moderation Queue Item"
|
||||
verbose_name_plural = "Moderation Queue Items"
|
||||
ordering = ['priority', 'created_at']
|
||||
ordering = ["priority", "created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=['status', 'priority']),
|
||||
models.Index(fields=['assigned_to']),
|
||||
models.Index(fields=['created_at']),
|
||||
models.Index(fields=["status", "priority"]),
|
||||
models.Index(fields=["assigned_to"]),
|
||||
models.Index(fields=["created_at"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_item_type_display()}: {self.title}" # type: ignore
|
||||
return f"{self.get_item_type_display()}: {self.title}" # type: ignore
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
@@ -600,36 +531,28 @@ class ModerationAction(TrackedModel):
|
||||
"""
|
||||
|
||||
# Action details
|
||||
action_type = RichChoiceField(
|
||||
choice_group="moderation_action_types",
|
||||
domain="moderation",
|
||||
max_length=50
|
||||
)
|
||||
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")
|
||||
|
||||
# Duration (for temporary actions)
|
||||
duration_hours = models.PositiveIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Duration in hours for temporary actions"
|
||||
null=True, blank=True, help_text="Duration in hours for temporary actions"
|
||||
)
|
||||
expires_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When this action expires")
|
||||
is_active = models.BooleanField(
|
||||
default=True, help_text="Whether this action is currently active")
|
||||
expires_at = models.DateTimeField(null=True, blank=True, help_text="When this action expires")
|
||||
is_active = models.BooleanField(default=True, help_text="Whether this action is currently active")
|
||||
|
||||
# Users involved
|
||||
moderator = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='moderation_actions_taken',
|
||||
related_name="moderation_actions_taken",
|
||||
help_text="Moderator who took this action",
|
||||
)
|
||||
target_user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='moderation_actions_received',
|
||||
related_name="moderation_actions_received",
|
||||
help_text="User this action was taken against",
|
||||
)
|
||||
|
||||
@@ -639,31 +562,27 @@ class ModerationAction(TrackedModel):
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='actions_taken',
|
||||
related_name="actions_taken",
|
||||
help_text="Related moderation report",
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True, help_text="When this action was created"
|
||||
)
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True, help_text="When this action was last updated"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, help_text="When this action was created")
|
||||
updated_at = models.DateTimeField(auto_now=True, help_text="When this action was last updated")
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "Moderation Action"
|
||||
verbose_name_plural = "Moderation Actions"
|
||||
ordering = ['-created_at']
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=['target_user', 'is_active']),
|
||||
models.Index(fields=['moderator']),
|
||||
models.Index(fields=['expires_at']),
|
||||
models.Index(fields=['created_at']),
|
||||
models.Index(fields=["target_user", "is_active"]),
|
||||
models.Index(fields=["moderator"]),
|
||||
models.Index(fields=["expires_at"]),
|
||||
models.Index(fields=["created_at"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_action_type_display()} against {self.target_user.username} by {self.moderator.username}" # type: ignore
|
||||
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
|
||||
@@ -684,85 +603,56 @@ class BulkOperation(StateMachineMixin, TrackedModel):
|
||||
state_field_name = "status"
|
||||
|
||||
# Operation details
|
||||
operation_type = RichChoiceField(
|
||||
choice_group="bulk_operation_types",
|
||||
domain="moderation",
|
||||
max_length=50
|
||||
)
|
||||
status = RichFSMField(
|
||||
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'
|
||||
)
|
||||
operation_type = RichChoiceField(choice_group="bulk_operation_types", domain="moderation", max_length=50)
|
||||
status = RichFSMField(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
|
||||
parameters = models.JSONField(
|
||||
default=dict, help_text="Parameters for the operation")
|
||||
results = models.JSONField(default=dict, blank=True,
|
||||
help_text="Results and output from the operation")
|
||||
parameters = models.JSONField(default=dict, help_text="Parameters for the operation")
|
||||
results = models.JSONField(default=dict, blank=True, help_text="Results and output from the operation")
|
||||
|
||||
# Progress tracking
|
||||
total_items = models.PositiveIntegerField(
|
||||
default=0, help_text="Total number of items to process")
|
||||
processed_items = models.PositiveIntegerField(
|
||||
default=0, help_text="Number of items processed")
|
||||
failed_items = models.PositiveIntegerField(
|
||||
default=0, help_text="Number of items that failed")
|
||||
total_items = models.PositiveIntegerField(default=0, help_text="Total number of items to process")
|
||||
processed_items = models.PositiveIntegerField(default=0, help_text="Number of items processed")
|
||||
failed_items = models.PositiveIntegerField(default=0, help_text="Number of items that failed")
|
||||
|
||||
# Timing
|
||||
estimated_duration_minutes = models.PositiveIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Estimated duration in minutes"
|
||||
null=True, blank=True, help_text="Estimated duration in minutes"
|
||||
)
|
||||
schedule_for = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When to run this operation")
|
||||
schedule_for = models.DateTimeField(null=True, blank=True, help_text="When to run this operation")
|
||||
|
||||
# Control
|
||||
can_cancel = models.BooleanField(
|
||||
default=True, help_text="Whether this operation can be cancelled")
|
||||
can_cancel = models.BooleanField(default=True, help_text="Whether this operation can be cancelled")
|
||||
|
||||
# User who created the operation
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='bulk_operations_created',
|
||||
related_name="bulk_operations_created",
|
||||
help_text="User who created this operation",
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
started_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When this operation started"
|
||||
)
|
||||
completed_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When this operation completed"
|
||||
)
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True, help_text="When this operation was last updated"
|
||||
)
|
||||
started_at = models.DateTimeField(null=True, blank=True, help_text="When this operation started")
|
||||
completed_at = models.DateTimeField(null=True, blank=True, help_text="When this operation completed")
|
||||
updated_at = models.DateTimeField(auto_now=True, help_text="When this operation was last updated")
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "Bulk Operation"
|
||||
verbose_name_plural = "Bulk Operations"
|
||||
ordering = ['-created_at']
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=['status', 'priority']),
|
||||
models.Index(fields=['created_by']),
|
||||
models.Index(fields=['schedule_for']),
|
||||
models.Index(fields=['created_at']),
|
||||
models.Index(fields=["status", "priority"]),
|
||||
models.Index(fields=["created_by"]),
|
||||
models.Index(fields=["schedule_for"]),
|
||||
models.Index(fields=["created_at"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_operation_type_display()}: {self.description[:50]}" # type: ignore
|
||||
return f"{self.get_operation_type_display()}: {self.description[:50]}" # type: ignore
|
||||
|
||||
@property
|
||||
def progress_percentage(self):
|
||||
@@ -792,28 +682,21 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
|
||||
on_delete=models.CASCADE,
|
||||
help_text="Type of object this photo is for",
|
||||
)
|
||||
object_id = models.PositiveIntegerField(
|
||||
help_text="ID of object this photo is for"
|
||||
)
|
||||
object_id = models.PositiveIntegerField(help_text="ID of object this photo is for")
|
||||
content_object = GenericForeignKey("content_type", "object_id")
|
||||
|
||||
# The photo itself
|
||||
photo = models.ForeignKey(
|
||||
'django_cloudflareimages_toolkit.CloudflareImage',
|
||||
"django_cloudflareimages_toolkit.CloudflareImage",
|
||||
on_delete=models.CASCADE,
|
||||
help_text="Photo submission stored on Cloudflare Images"
|
||||
help_text="Photo submission stored on Cloudflare Images",
|
||||
)
|
||||
caption = models.CharField(max_length=255, blank=True, help_text="Photo caption")
|
||||
date_taken = models.DateField(
|
||||
null=True, blank=True, help_text="Date the photo was taken"
|
||||
)
|
||||
date_taken = models.DateField(null=True, blank=True, help_text="Date the photo was taken")
|
||||
|
||||
# Metadata
|
||||
status = RichFSMField(
|
||||
choice_group="photo_submission_statuses",
|
||||
domain="moderation",
|
||||
max_length=20,
|
||||
default="PENDING"
|
||||
choice_group="photo_submission_statuses", domain="moderation", max_length=20, default="PENDING"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -826,9 +709,7 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
|
||||
related_name="handled_photos",
|
||||
help_text="Moderator who handled this submission",
|
||||
)
|
||||
handled_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When this submission was handled"
|
||||
)
|
||||
handled_at = models.DateTimeField(null=True, blank=True, help_text="When this submission was handled")
|
||||
notes = models.TextField(
|
||||
blank=True,
|
||||
help_text="Notes from the moderator about this photo submission",
|
||||
@@ -843,9 +724,7 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
|
||||
related_name="claimed_photo_submissions",
|
||||
help_text="Moderator who has claimed this submission for review",
|
||||
)
|
||||
claimed_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="When this submission was claimed"
|
||||
)
|
||||
claimed_at = models.DateTimeField(null=True, blank=True, help_text="When this submission was claimed")
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
verbose_name = "Photo Submission"
|
||||
@@ -873,9 +752,7 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
if self.status != "PENDING":
|
||||
raise ValidationError(
|
||||
f"Cannot claim submission: current status is {self.status}, expected PENDING"
|
||||
)
|
||||
raise ValidationError(f"Cannot claim submission: current status is {self.status}, expected PENDING")
|
||||
|
||||
self.transition_to_claimed(user=user)
|
||||
self.claimed_by = user
|
||||
@@ -896,9 +773,7 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
if self.status != "CLAIMED":
|
||||
raise ValidationError(
|
||||
f"Cannot unclaim submission: current status is {self.status}, expected CLAIMED"
|
||||
)
|
||||
raise ValidationError(f"Cannot unclaim submission: current status is {self.status}, expected CLAIMED")
|
||||
|
||||
# Set status directly (not via FSM transition to avoid cycle)
|
||||
# This is intentional - the unclaim action is a special "rollback" operation
|
||||
|
||||
Reference in New Issue
Block a user