""" 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}")