mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 09:31:08 -05:00
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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user