chore: fix pghistory migration deps and improve htmx utilities

- Update pghistory dependency from 0007 to 0006 in account migrations
- Add docstrings and remove unused imports in htmx_forms.py
- Add DJANGO_SETTINGS_MODULE bash commands to Claude settings
- Add state transition definitions for ride statuses
This commit is contained in:
pacnpal
2025-12-21 17:33:24 -05:00
parent b9063ff4f8
commit 7ba0004c93
74 changed files with 11134 additions and 198 deletions

View File

@@ -24,6 +24,7 @@ from datetime import timedelta
import pghistory
from apps.core.history import TrackedModel
from apps.core.choices.fields import RichChoiceField
from apps.core.state_machine import RichFSMField, StateMachineMixin
UserType = Union[AbstractBaseUser, AnonymousUser]
@@ -33,7 +34,10 @@ UserType = Union[AbstractBaseUser, AnonymousUser]
# ============================================================================
@pghistory.track() # Track all changes by default
class EditSubmission(TrackedModel):
class EditSubmission(StateMachineMixin, TrackedModel):
"""Edit submission model with FSM-managed status transitions."""
state_field_name = "status"
# Who submitted the edit
user = models.ForeignKey(
@@ -74,7 +78,7 @@ class EditSubmission(TrackedModel):
source = models.TextField(
blank=True, help_text="Source of information (if applicable)"
)
status = RichChoiceField(
status = RichFSMField(
choice_group="edit_submission_statuses",
domain="moderation",
max_length=20,
@@ -138,12 +142,14 @@ class EditSubmission(TrackedModel):
"""Get the final changes to apply (moderator changes if available, otherwise original changes)"""
return self.moderator_changes or self.changes
def approve(self, moderator: UserType) -> Optional[models.Model]:
def approve(self, moderator: UserType, user=None) -> Optional[models.Model]:
"""
Approve this submission and apply the changes.
Wrapper method that preserves business logic while using FSM.
Args:
moderator: The user approving the submission
user: Alternative parameter for FSM compatibility
Returns:
The created or updated model instance
@@ -152,9 +158,9 @@ class EditSubmission(TrackedModel):
ValueError: If submission cannot be approved
ValidationError: If the data is invalid
"""
if self.status != "PENDING":
raise ValueError(f"Cannot approve submission with status {self.status}")
# Use user parameter if provided (FSM convention)
approver = user or moderator
model_class = self.content_type.model_class()
if not model_class:
raise ValueError("Could not resolve model class")
@@ -181,55 +187,64 @@ class EditSubmission(TrackedModel):
obj.full_clean()
obj.save()
# Mark submission as approved
self.status = "APPROVED"
self.handled_by = moderator
# Use FSM transition to update status
self.transition_to_approved(user=approver)
self.handled_by = approver
self.handled_at = timezone.now()
self.save()
return obj
except Exception as e:
# Mark as rejected on any error
self.status = "REJECTED"
self.handled_by = moderator
self.handled_at = timezone.now()
# On error, record the issue and attempt rejection transition
self.notes = f"Approval failed: {str(e)}"
self.save()
try:
self.transition_to_rejected(user=approver)
self.handled_by = approver
self.handled_at = timezone.now()
self.save()
except Exception:
pass
raise
def reject(self, moderator: UserType, reason: str) -> None:
def reject(self, moderator: UserType = None, reason: str = "", user=None) -> None:
"""
Reject this submission.
Wrapper method that preserves business logic while using FSM.
Args:
moderator: The user rejecting the submission
reason: Reason for rejection
user: Alternative parameter for FSM compatibility
"""
if self.status != "PENDING":
raise ValueError(f"Cannot reject submission with status {self.status}")
self.status = "REJECTED"
self.handled_by = moderator
# Use user parameter if provided (FSM convention)
rejecter = user or moderator
# Use FSM transition to update status
self.transition_to_rejected(user=rejecter)
self.handled_by = rejecter
self.handled_at = timezone.now()
self.notes = f"Rejected: {reason}"
self.notes = f"Rejected: {reason}" if reason else "Rejected"
self.save()
def escalate(self, moderator: UserType, reason: str) -> None:
def escalate(self, moderator: UserType = None, reason: str = "", user=None) -> None:
"""
Escalate this submission for higher-level review.
Wrapper method that preserves business logic while using FSM.
Args:
moderator: The user escalating the submission
reason: Reason for escalation
user: Alternative parameter for FSM compatibility
"""
if self.status != "PENDING":
raise ValueError(f"Cannot escalate submission with status {self.status}")
self.status = "ESCALATED"
self.handled_by = moderator
# Use user parameter if provided (FSM convention)
escalator = user or moderator
# Use FSM transition to update status
self.transition_to_escalated(user=escalator)
self.handled_by = escalator
self.handled_at = timezone.now()
self.notes = f"Escalated: {reason}"
self.notes = f"Escalated: {reason}" if reason else "Escalated"
self.save()
@property
@@ -248,13 +263,15 @@ class EditSubmission(TrackedModel):
# ============================================================================
@pghistory.track()
class ModerationReport(TrackedModel):
class ModerationReport(StateMachineMixin, TrackedModel):
"""
Model for tracking user reports about content, users, or behavior.
This handles the initial reporting phase where users flag content
or behavior that needs moderator attention.
"""
state_field_name = "status"
# Report details
report_type = RichChoiceField(
@@ -262,7 +279,7 @@ class ModerationReport(TrackedModel):
domain="moderation",
max_length=50
)
status = RichChoiceField(
status = RichFSMField(
choice_group="moderation_report_statuses",
domain="moderation",
max_length=20,
@@ -328,13 +345,15 @@ class ModerationReport(TrackedModel):
@pghistory.track()
class ModerationQueue(TrackedModel):
class ModerationQueue(StateMachineMixin, TrackedModel):
"""
Model for managing moderation workflow and task assignment.
This represents items in the moderation queue that need attention,
separate from the initial reports.
"""
state_field_name = "status"
# Queue item details
item_type = RichChoiceField(
@@ -342,7 +361,7 @@ class ModerationQueue(TrackedModel):
domain="moderation",
max_length=50
)
status = RichChoiceField(
status = RichFSMField(
choice_group="moderation_queue_statuses",
domain="moderation",
max_length=20,
@@ -491,13 +510,15 @@ class ModerationAction(TrackedModel):
@pghistory.track()
class BulkOperation(TrackedModel):
class BulkOperation(StateMachineMixin, TrackedModel):
"""
Model for tracking bulk administrative operations.
This handles large-scale operations like bulk updates,
imports, exports, or mass moderation actions.
"""
state_field_name = "status"
# Operation details
operation_type = RichChoiceField(
@@ -505,7 +526,7 @@ class BulkOperation(TrackedModel):
domain="moderation",
max_length=50
)
status = RichChoiceField(
status = RichFSMField(
choice_group="bulk_operation_statuses",
domain="moderation",
max_length=20,
@@ -580,7 +601,10 @@ class BulkOperation(TrackedModel):
@pghistory.track() # Track all changes by default
class PhotoSubmission(TrackedModel):
class PhotoSubmission(StateMachineMixin, TrackedModel):
"""Photo submission model with FSM-managed status transitions."""
state_field_name = "status"
# Who submitted the photo
user = models.ForeignKey(
@@ -604,7 +628,7 @@ class PhotoSubmission(TrackedModel):
date_taken = models.DateField(null=True, blank=True)
# Metadata
status = RichChoiceField(
status = RichFSMField(
choice_group="photo_submission_statuses",
domain="moderation",
max_length=20,
@@ -636,16 +660,22 @@ class PhotoSubmission(TrackedModel):
def __str__(self) -> str:
return f"Photo submission by {self.user.username} for {self.content_object}"
def approve(self, moderator: UserType, notes: str = "") -> None:
"""Approve the photo submission"""
def approve(self, moderator: UserType = None, notes: str = "", user=None) -> None:
"""
Approve the photo submission.
Wrapper method that preserves business logic while using FSM.
Args:
moderator: The user approving the submission
notes: Optional approval notes
user: Alternative parameter for FSM compatibility
"""
from apps.parks.models.media import ParkPhoto
from apps.rides.models.media import RidePhoto
self.status = "APPROVED"
self.handled_by = moderator # type: ignore
self.handled_at = timezone.now()
self.notes = notes
# Use user parameter if provided (FSM convention)
approver = user or moderator
# Determine the correct photo model based on the content type
model_class = self.content_type.model_class()
if model_class.__name__ == "Park":
@@ -663,13 +693,30 @@ class PhotoSubmission(TrackedModel):
caption=self.caption,
is_approved=True,
)
# Use FSM transition to update status
self.transition_to_approved(user=approver)
self.handled_by = approver # type: ignore
self.handled_at = timezone.now()
self.notes = notes
self.save()
def reject(self, moderator: UserType, notes: str) -> None:
"""Reject the photo submission"""
self.status = "REJECTED"
self.handled_by = moderator # type: ignore
def reject(self, moderator: UserType = None, notes: str = "", user=None) -> None:
"""
Reject the photo submission.
Wrapper method that preserves business logic while using FSM.
Args:
moderator: The user rejecting the submission
notes: Rejection reason
user: Alternative parameter for FSM compatibility
"""
# Use user parameter if provided (FSM convention)
rejecter = user or moderator
# Use FSM transition to update status
self.transition_to_rejected(user=rejecter)
self.handled_by = rejecter # type: ignore
self.handled_at = timezone.now()
self.notes = notes
self.save()
@@ -683,10 +730,22 @@ class PhotoSubmission(TrackedModel):
if user_role in ["MODERATOR", "ADMIN", "SUPERUSER"]:
self.approve(self.user)
def escalate(self, moderator: UserType, notes: str = "") -> None:
"""Escalate the photo submission to admin"""
self.status = "ESCALATED"
self.handled_by = moderator # type: ignore
def escalate(self, moderator: UserType = None, notes: str = "", user=None) -> None:
"""
Escalate the photo submission to admin.
Wrapper method that preserves business logic while using FSM.
Args:
moderator: The user escalating the submission
notes: Escalation reason
user: Alternative parameter for FSM compatibility
"""
# Use user parameter if provided (FSM convention)
escalator = user or moderator
# Use FSM transition to update status
self.transition_to_escalated(user=escalator)
self.handled_by = escalator # type: ignore
self.handled_at = timezone.now()
self.notes = notes
self.save()