mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 11:31:13 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
378
django-backend/apps/reviews/services.py
Normal file
378
django-backend/apps/reviews/services.py
Normal file
@@ -0,0 +1,378 @@
|
||||
"""
|
||||
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}")
|
||||
Reference in New Issue
Block a user