mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 05:51:12 -05:00
379 lines
13 KiB
Python
379 lines
13 KiB
Python
"""
|
|
Review services for ThrillWiki.
|
|
|
|
This module provides business logic for review submissions through the Sacred Pipeline.
|
|
All reviews must flow through ModerationService to ensure consistency with the rest of the system.
|
|
"""
|
|
|
|
import logging
|
|
from django.db import transaction
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.exceptions import ValidationError
|
|
|
|
from apps.moderation.services import ModerationService
|
|
from apps.reviews.models import Review
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ReviewSubmissionService:
|
|
"""
|
|
Service class for creating and managing review submissions.
|
|
|
|
All reviews flow through the ContentSubmission pipeline, ensuring:
|
|
- Consistent moderation workflow
|
|
- FSM state machine transitions
|
|
- 15-minute lock mechanism
|
|
- Atomic transaction handling
|
|
- Automatic versioning via pghistory
|
|
- Audit trail via ContentSubmission
|
|
"""
|
|
|
|
@staticmethod
|
|
@transaction.atomic
|
|
def create_review_submission(
|
|
user,
|
|
entity,
|
|
rating,
|
|
title,
|
|
content,
|
|
visit_date=None,
|
|
wait_time_minutes=None,
|
|
**kwargs
|
|
):
|
|
"""
|
|
Create a review submission through the Sacred Pipeline.
|
|
|
|
This method creates a ContentSubmission with SubmissionItems for each review field.
|
|
If the user is a moderator, the submission is auto-approved and the Review is created immediately.
|
|
Otherwise, the submission enters the pending moderation queue.
|
|
|
|
Args:
|
|
user: User creating the review
|
|
entity: Entity being reviewed (Park or Ride)
|
|
rating: Rating from 1-5 stars
|
|
title: Review title
|
|
content: Review content text
|
|
visit_date: Optional date of visit
|
|
wait_time_minutes: Optional wait time in minutes
|
|
**kwargs: Additional metadata (source, ip_address, user_agent)
|
|
|
|
Returns:
|
|
tuple: (ContentSubmission, Review or None)
|
|
Review will be None if pending moderation
|
|
|
|
Raises:
|
|
ValidationError: If validation fails
|
|
"""
|
|
# Check if user is moderator (for bypass)
|
|
is_moderator = hasattr(user, 'role') and user.role.is_moderator if user else False
|
|
|
|
# Get entity ContentType
|
|
entity_type = ContentType.objects.get_for_model(entity)
|
|
|
|
# Check for duplicate review
|
|
existing = Review.objects.filter(
|
|
user=user,
|
|
content_type=entity_type,
|
|
object_id=entity.id
|
|
).first()
|
|
|
|
if existing:
|
|
raise ValidationError(
|
|
f"User has already reviewed this {entity_type.model}. "
|
|
f"Use update method to modify existing review."
|
|
)
|
|
|
|
# Build submission items for each review field
|
|
items_data = [
|
|
{
|
|
'field_name': 'rating',
|
|
'field_label': 'Rating',
|
|
'old_value': None,
|
|
'new_value': str(rating),
|
|
'change_type': 'add',
|
|
'is_required': True,
|
|
'order': 0
|
|
},
|
|
{
|
|
'field_name': 'title',
|
|
'field_label': 'Title',
|
|
'old_value': None,
|
|
'new_value': title,
|
|
'change_type': 'add',
|
|
'is_required': True,
|
|
'order': 1
|
|
},
|
|
{
|
|
'field_name': 'content',
|
|
'field_label': 'Review Content',
|
|
'old_value': None,
|
|
'new_value': content,
|
|
'change_type': 'add',
|
|
'is_required': True,
|
|
'order': 2
|
|
},
|
|
]
|
|
|
|
# Add optional fields if provided
|
|
if visit_date is not None:
|
|
items_data.append({
|
|
'field_name': 'visit_date',
|
|
'field_label': 'Visit Date',
|
|
'old_value': None,
|
|
'new_value': str(visit_date),
|
|
'change_type': 'add',
|
|
'is_required': False,
|
|
'order': 3
|
|
})
|
|
|
|
if wait_time_minutes is not None:
|
|
items_data.append({
|
|
'field_name': 'wait_time_minutes',
|
|
'field_label': 'Wait Time (minutes)',
|
|
'old_value': None,
|
|
'new_value': str(wait_time_minutes),
|
|
'change_type': 'add',
|
|
'is_required': False,
|
|
'order': 4
|
|
})
|
|
|
|
# Create submission through ModerationService
|
|
submission = ModerationService.create_submission(
|
|
user=user,
|
|
entity=entity,
|
|
submission_type='review',
|
|
title=f"Review: {title[:50]}",
|
|
description=f"User review for {entity_type.model}: {entity}",
|
|
items_data=items_data,
|
|
metadata={
|
|
'rating': rating,
|
|
'entity_type': entity_type.model,
|
|
},
|
|
auto_submit=True,
|
|
source=kwargs.get('source', 'api'),
|
|
ip_address=kwargs.get('ip_address'),
|
|
user_agent=kwargs.get('user_agent', '')
|
|
)
|
|
|
|
logger.info(
|
|
f"Review submission created: {submission.id} by {user.email} "
|
|
f"for {entity_type.model} {entity.id}"
|
|
)
|
|
|
|
# MODERATOR BYPASS: Auto-approve if user is moderator
|
|
review = None
|
|
if is_moderator:
|
|
logger.info(f"Moderator bypass: Auto-approving submission {submission.id}")
|
|
|
|
# Approve through ModerationService (this triggers atomic transaction)
|
|
submission = ModerationService.approve_submission(submission.id, user)
|
|
|
|
# Create the Review record
|
|
review = ReviewSubmissionService._create_review_from_submission(
|
|
submission=submission,
|
|
entity=entity,
|
|
user=user
|
|
)
|
|
|
|
logger.info(f"Review auto-created for moderator: {review.id}")
|
|
|
|
return submission, review
|
|
|
|
@staticmethod
|
|
@transaction.atomic
|
|
def _create_review_from_submission(submission, entity, user):
|
|
"""
|
|
Create a Review record from an approved ContentSubmission.
|
|
|
|
This is called internally when a submission is approved.
|
|
Extracts data from SubmissionItems and creates the Review.
|
|
|
|
Args:
|
|
submission: Approved ContentSubmission
|
|
entity: Entity being reviewed
|
|
user: User who created the review
|
|
|
|
Returns:
|
|
Review: Created review instance
|
|
"""
|
|
# Extract data from submission items
|
|
items = submission.items.all()
|
|
review_data = {}
|
|
|
|
for item in items:
|
|
if item.field_name == 'rating':
|
|
review_data['rating'] = int(item.new_value)
|
|
elif item.field_name == 'title':
|
|
review_data['title'] = item.new_value
|
|
elif item.field_name == 'content':
|
|
review_data['content'] = item.new_value
|
|
elif item.field_name == 'visit_date':
|
|
from datetime import datetime
|
|
review_data['visit_date'] = datetime.fromisoformat(item.new_value).date()
|
|
elif item.field_name == 'wait_time_minutes':
|
|
review_data['wait_time_minutes'] = int(item.new_value)
|
|
|
|
# Get entity ContentType
|
|
entity_type = ContentType.objects.get_for_model(entity)
|
|
|
|
# Create Review
|
|
review = Review.objects.create(
|
|
user=user,
|
|
content_type=entity_type,
|
|
object_id=entity.id,
|
|
submission=submission,
|
|
moderation_status=Review.MODERATION_APPROVED,
|
|
moderated_by=submission.reviewed_by,
|
|
moderated_at=submission.reviewed_at,
|
|
**review_data
|
|
)
|
|
|
|
# pghistory will automatically track this creation
|
|
|
|
logger.info(
|
|
f"Review created from submission: {review.id} "
|
|
f"(submission: {submission.id})"
|
|
)
|
|
|
|
return review
|
|
|
|
@staticmethod
|
|
@transaction.atomic
|
|
def update_review_submission(review, user, **update_data):
|
|
"""
|
|
Update an existing review by creating a new submission.
|
|
|
|
This follows the Sacred Pipeline by creating a new ContentSubmission
|
|
for the update, which must be approved before changes take effect.
|
|
|
|
Args:
|
|
review: Existing Review to update
|
|
user: User making the update (must be review owner)
|
|
**update_data: Fields to update (rating, title, content, etc.)
|
|
|
|
Returns:
|
|
ContentSubmission: The update submission
|
|
|
|
Raises:
|
|
ValidationError: If user is not the review owner
|
|
"""
|
|
# Verify ownership
|
|
if review.user != user:
|
|
raise ValidationError("Only the review owner can update their review")
|
|
|
|
# Check if user is moderator (for bypass)
|
|
is_moderator = hasattr(user, 'role') and user.role.is_moderator if user else False
|
|
|
|
# Get entity
|
|
entity = review.content_object
|
|
if not entity:
|
|
raise ValidationError("Reviewed entity no longer exists")
|
|
|
|
# Build submission items for changed fields
|
|
items_data = []
|
|
order = 0
|
|
|
|
for field_name, new_value in update_data.items():
|
|
if field_name in ['rating', 'title', 'content', 'visit_date', 'wait_time_minutes']:
|
|
old_value = getattr(review, field_name)
|
|
|
|
# Only include if value actually changed
|
|
if old_value != new_value:
|
|
items_data.append({
|
|
'field_name': field_name,
|
|
'field_label': field_name.replace('_', ' ').title(),
|
|
'old_value': str(old_value) if old_value else None,
|
|
'new_value': str(new_value),
|
|
'change_type': 'modify',
|
|
'is_required': field_name in ['rating', 'title', 'content'],
|
|
'order': order
|
|
})
|
|
order += 1
|
|
|
|
if not items_data:
|
|
raise ValidationError("No changes detected")
|
|
|
|
# Create update submission
|
|
submission = ModerationService.create_submission(
|
|
user=user,
|
|
entity=entity,
|
|
submission_type='update',
|
|
title=f"Review Update: {review.title[:50]}",
|
|
description=f"User updating review #{review.id}",
|
|
items_data=items_data,
|
|
metadata={
|
|
'review_id': str(review.id),
|
|
'update_type': 'review',
|
|
},
|
|
auto_submit=True,
|
|
source='api'
|
|
)
|
|
|
|
logger.info(f"Review update submission created: {submission.id}")
|
|
|
|
# MODERATOR BYPASS: Auto-approve if moderator
|
|
if is_moderator:
|
|
submission = ModerationService.approve_submission(submission.id, user)
|
|
|
|
# Apply updates to review
|
|
for item in submission.items.filter(status='approved'):
|
|
setattr(review, item.field_name, item.new_value)
|
|
|
|
review.moderation_status = Review.MODERATION_APPROVED
|
|
review.moderated_by = user
|
|
review.save()
|
|
|
|
logger.info(f"Review update auto-approved for moderator: {review.id}")
|
|
else:
|
|
# Regular user: mark review as pending
|
|
review.moderation_status = Review.MODERATION_PENDING
|
|
review.save()
|
|
|
|
return submission
|
|
|
|
@staticmethod
|
|
def apply_review_approval(submission):
|
|
"""
|
|
Apply an approved review submission.
|
|
|
|
This is called by ModerationService when a review submission is approved.
|
|
For new reviews, creates the Review record.
|
|
For updates, applies changes to existing Review.
|
|
|
|
Args:
|
|
submission: Approved ContentSubmission
|
|
|
|
Returns:
|
|
Review: The created or updated review
|
|
"""
|
|
entity = submission.entity
|
|
user = submission.user
|
|
|
|
if submission.submission_type == 'review':
|
|
# New review
|
|
return ReviewSubmissionService._create_review_from_submission(
|
|
submission, entity, user
|
|
)
|
|
elif submission.submission_type == 'update':
|
|
# Update existing review
|
|
review_id = submission.metadata.get('review_id')
|
|
if not review_id:
|
|
raise ValidationError("Missing review_id in submission metadata")
|
|
|
|
review = Review.objects.get(id=review_id)
|
|
|
|
# Apply approved changes
|
|
for item in submission.items.filter(status='approved'):
|
|
setattr(review, item.field_name, item.new_value)
|
|
|
|
review.moderation_status = Review.MODERATION_APPROVED
|
|
review.moderated_by = submission.reviewed_by
|
|
review.moderated_at = submission.reviewed_at
|
|
review.save()
|
|
|
|
logger.info(f"Review updated from submission: {review.id}")
|
|
return review
|
|
else:
|
|
raise ValidationError(f"Invalid submission type: {submission.submission_type}")
|