feat: Add blog, media, and support apps, implement ride credits and image API, and remove toplist feature.

This commit is contained in:
pacnpal
2025-12-26 15:15:28 -05:00
parent cd8868a591
commit 00699d53b4
77 changed files with 7274 additions and 538 deletions

View File

@@ -143,6 +143,19 @@ class EditSubmission(StateMachineMixin, TrackedModel):
blank=True, help_text="Notes from the moderator about this submission"
)
# Claim tracking for concurrency control
claimed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
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"
)
class Meta(TrackedModel.Meta):
verbose_name = "Edit Submission"
verbose_name_plural = "Edit Submissions"
@@ -188,6 +201,54 @@ class EditSubmission(StateMachineMixin, TrackedModel):
"""Get the final changes to apply (moderator changes if available, otherwise original changes)"""
return self.moderator_changes or self.changes
def claim(self, user: UserType) -> None:
"""
Claim this submission for review.
Transition: PENDING -> CLAIMED
Args:
user: The moderator claiming this submission
Raises:
ValidationError: If submission is not in PENDING state
"""
from django.core.exceptions import ValidationError
if self.status != "PENDING":
raise ValidationError(
f"Cannot claim submission: current status is {self.status}, expected PENDING"
)
self.transition_to_claimed(user=user)
self.claimed_by = user
self.claimed_at = timezone.now()
self.save()
def unclaim(self, user: UserType = None) -> None:
"""
Release claim on this submission.
Transition: CLAIMED -> PENDING
Args:
user: The user initiating the unclaim (for audit)
Raises:
ValidationError: If submission is not in CLAIMED state
"""
from django.core.exceptions import ValidationError
if self.status != "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
self.status = "PENDING"
self.claimed_by = None
self.claimed_at = None
self.save()
def approve(self, moderator: UserType, user=None) -> Optional[models.Model]:
"""
Approve this submission and apply the changes.
@@ -204,9 +265,17 @@ class EditSubmission(StateMachineMixin, TrackedModel):
ValueError: If submission cannot be approved
ValidationError: If the data is invalid
"""
from django.core.exceptions import ValidationError
# Use user parameter if provided (FSM convention)
approver = user or moderator
# 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})"
)
model_class = self.content_type.model_class()
if not model_class:
raise ValueError("Could not resolve model class")
@@ -263,9 +332,17 @@ class EditSubmission(StateMachineMixin, TrackedModel):
reason: Reason for rejection
user: Alternative parameter for FSM compatibility
"""
from django.core.exceptions import ValidationError
# Use user parameter if provided (FSM convention)
rejecter = user or moderator
# 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})"
)
# Use FSM transition to update status
self.transition_to_rejected(user=rejecter)
self.handled_by = rejecter
@@ -283,9 +360,17 @@ class EditSubmission(StateMachineMixin, TrackedModel):
reason: Reason for escalation
user: Alternative parameter for FSM compatibility
"""
from django.core.exceptions import ValidationError
# Use user parameter if provided (FSM convention)
escalator = user or moderator
# 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})"
)
# Use FSM transition to update status
self.transition_to_escalated(user=escalator)
self.handled_by = escalator
@@ -747,6 +832,19 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
help_text="Notes from the moderator about this photo submission",
)
# Claim tracking for concurrency control
claimed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
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"
)
class Meta(TrackedModel.Meta):
verbose_name = "Photo Submission"
verbose_name_plural = "Photo Submissions"
@@ -759,6 +857,54 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
def __str__(self) -> str:
return f"Photo submission by {self.user.username} for {self.content_object}"
def claim(self, user: UserType) -> None:
"""
Claim this photo submission for review.
Transition: PENDING -> CLAIMED
Args:
user: The moderator claiming this submission
Raises:
ValidationError: If submission is not in PENDING state
"""
from django.core.exceptions import ValidationError
if self.status != "PENDING":
raise ValidationError(
f"Cannot claim submission: current status is {self.status}, expected PENDING"
)
self.transition_to_claimed(user=user)
self.claimed_by = user
self.claimed_at = timezone.now()
self.save()
def unclaim(self, user: UserType = None) -> None:
"""
Release claim on this photo submission.
Transition: CLAIMED -> PENDING
Args:
user: The user initiating the unclaim (for audit)
Raises:
ValidationError: If submission is not in CLAIMED state
"""
from django.core.exceptions import ValidationError
if self.status != "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
self.status = "PENDING"
self.claimed_by = None
self.claimed_at = None
self.save()
def approve(self, moderator: UserType = None, notes: str = "", user=None) -> None:
"""
Approve the photo submission.
@@ -771,10 +917,17 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
"""
from apps.parks.models.media import ParkPhoto
from apps.rides.models.media import RidePhoto
from django.core.exceptions import ValidationError
# Use user parameter if provided (FSM convention)
approver = user or moderator
# Validate state - must be CLAIMED before approval
if self.status != "CLAIMED":
raise ValidationError(
f"Cannot approve photo submission: must be CLAIMED first (current status: {self.status})"
)
# Determine the correct photo model based on the content type
model_class = self.content_type.model_class()
if model_class.__name__ == "Park":
@@ -810,9 +963,17 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
notes: Rejection reason
user: Alternative parameter for FSM compatibility
"""
from django.core.exceptions import ValidationError
# Use user parameter if provided (FSM convention)
rejecter = user or moderator
# Validate state - must be CLAIMED before rejection
if self.status != "CLAIMED":
raise ValidationError(
f"Cannot reject photo submission: must be CLAIMED first (current status: {self.status})"
)
# Use FSM transition to update status
self.transition_to_rejected(user=rejecter)
self.handled_by = rejecter # type: ignore
@@ -839,9 +1000,17 @@ class PhotoSubmission(StateMachineMixin, TrackedModel):
notes: Escalation reason
user: Alternative parameter for FSM compatibility
"""
from django.core.exceptions import ValidationError
# Use user parameter if provided (FSM convention)
escalator = user or moderator
# Validate state - must be CLAIMED before escalation
if self.status != "CLAIMED":
raise ValidationError(
f"Cannot escalate photo submission: must be CLAIMED first (current status: {self.status})"
)
# Use FSM transition to update status
self.transition_to_escalated(user=escalator)
self.handled_by = escalator # type: ignore