Files
thrillwiki_django_no_react/moderation/services.py
pacnpal 66ed4347a9 Refactor test utilities and enhance ASGI settings
- Cleaned up and standardized assertions in ApiTestMixin for API response validation.
- Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE.
- Removed unused imports and improved formatting in settings.py.
- Refactored URL patterns in urls.py for better readability and organization.
- Enhanced view functions in views.py for consistency and clarity.
- Added .flake8 configuration for linting and style enforcement.
- Introduced type stubs for django-environ to improve type checking with Pylance.
2025-08-20 19:51:59 -04:00

231 lines
7.1 KiB
Python

"""
Services for moderation functionality.
Following Django styleguide pattern for business logic encapsulation.
"""
from typing import Optional, Dict, Any, Union
from django.db import transaction
from django.utils import timezone
from django.contrib.auth.models import User
from django.db.models import QuerySet
from .models import EditSubmission
class ModerationService:
"""Service for handling content moderation workflows."""
@staticmethod
def approve_submission(
*, submission_id: int, moderator: User, notes: Optional[str] = None
) -> Union[object, None]:
"""
Approve a content submission and apply changes.
Args:
submission_id: ID of the submission to approve
moderator: User performing the approval
notes: Optional notes about the approval
Returns:
The created/updated object or None if approval failed
Raises:
EditSubmission.DoesNotExist: If submission doesn't exist
ValidationError: If submission data is invalid
ValueError: If submission cannot be processed
"""
with transaction.atomic():
submission = EditSubmission.objects.select_for_update().get(
id=submission_id
)
if submission.status != "PENDING":
raise ValueError(f"Submission {submission_id} is not pending approval")
try:
# Call the model's approve method which handles the business
# logic
obj = submission.approve(moderator)
# Add moderator notes if provided
if notes:
if submission.notes:
submission.notes += f"\n[Moderator]: {notes}"
else:
submission.notes = f"[Moderator]: {notes}"
submission.save()
return obj
except Exception as e:
# Mark as rejected on any error
submission.status = "REJECTED"
submission.handled_by = moderator
submission.handled_at = timezone.now()
submission.notes = f"Approval failed: {str(e)}"
submission.save()
raise
@staticmethod
def reject_submission(
*, submission_id: int, moderator: User, reason: str
) -> EditSubmission:
"""
Reject a content submission.
Args:
submission_id: ID of the submission to reject
moderator: User performing the rejection
reason: Reason for rejection
Returns:
Updated submission object
Raises:
EditSubmission.DoesNotExist: If submission doesn't exist
ValueError: If submission cannot be rejected
"""
with transaction.atomic():
submission = EditSubmission.objects.select_for_update().get(
id=submission_id
)
if submission.status != "PENDING":
raise ValueError(f"Submission {submission_id} is not pending review")
submission.status = "REJECTED"
submission.handled_by = moderator
submission.handled_at = timezone.now()
submission.notes = f"Rejected: {reason}"
# Call full_clean before saving - CRITICAL STYLEGUIDE FIX
submission.full_clean()
submission.save()
return submission
@staticmethod
def create_edit_submission(
*,
content_object: object,
changes: Dict[str, Any],
submitter: User,
submission_type: str = "UPDATE",
notes: Optional[str] = None,
) -> EditSubmission:
"""
Create a new edit submission for moderation.
Args:
content_object: The object being edited
changes: Dictionary of field changes
submitter: User submitting the changes
submission_type: Type of submission ("CREATE" or "UPDATE")
notes: Optional notes about the submission
Returns:
Created EditSubmission object
Raises:
ValidationError: If submission data is invalid
"""
submission = EditSubmission(
content_object=content_object,
changes=changes,
submitted_by=submitter,
submission_type=submission_type,
notes=notes or "",
)
# Call full_clean before saving - CRITICAL STYLEGUIDE FIX
submission.full_clean()
submission.save()
return submission
@staticmethod
def update_submission_changes(
*,
submission_id: int,
moderator_changes: Dict[str, Any],
moderator: User,
) -> EditSubmission:
"""
Update submission with moderator changes before approval.
Args:
submission_id: ID of the submission to update
moderator_changes: Dictionary of moderator modifications
moderator: User making the changes
Returns:
Updated submission object
Raises:
EditSubmission.DoesNotExist: If submission doesn't exist
ValueError: If submission cannot be modified
"""
with transaction.atomic():
submission = EditSubmission.objects.select_for_update().get(
id=submission_id
)
if submission.status != "PENDING":
raise ValueError(f"Submission {submission_id} is not pending review")
submission.moderator_changes = moderator_changes
# Add note about moderator changes
note = f"[Moderator changes by {moderator.username}]"
if submission.notes:
submission.notes += f"\n{note}"
else:
submission.notes = note
# Call full_clean before saving - CRITICAL STYLEGUIDE FIX
submission.full_clean()
submission.save()
return submission
@staticmethod
def get_pending_submissions_for_moderator(
*,
moderator: User,
content_type: Optional[str] = None,
limit: Optional[int] = None,
) -> QuerySet:
"""
Get pending submissions for a moderator to review.
Args:
moderator: The moderator user
content_type: Optional filter by content type
limit: Maximum number of submissions to return
Returns:
QuerySet of pending submissions
"""
from .selectors import pending_submissions_for_review
return pending_submissions_for_review(content_type=content_type, limit=limit)
@staticmethod
def get_submission_statistics(
*, days: int = 30, moderator: Optional[User] = None
) -> Dict[str, Any]:
"""
Get moderation statistics for a time period.
Args:
days: Number of days to analyze
moderator: Optional filter by specific moderator
Returns:
Dictionary containing moderation statistics
"""
from .selectors import moderation_statistics_summary
return moderation_statistics_summary(days=days, moderator=moderator)