from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from model_utils.models import TimeStampedModel import pghistory @pghistory.track() class Review(TimeStampedModel): """ User reviews for parks or rides. Users can leave reviews with ratings, text, photos, and metadata like visit date. Reviews support helpful voting and go through moderation workflow. """ # User who created the review user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='reviews' ) # Generic relation - can review either a Park or a Ride content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, limit_choices_to={'model__in': ('park', 'ride')} ) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') # Review content title = models.CharField(max_length=200) content = models.TextField() rating = models.IntegerField( validators=[MinValueValidator(1), MaxValueValidator(5)], help_text="Rating from 1 to 5 stars" ) # Visit metadata visit_date = models.DateField( null=True, blank=True, help_text="Date the user visited" ) wait_time_minutes = models.PositiveIntegerField( null=True, blank=True, help_text="Wait time in minutes" ) # Helpful voting system helpful_votes = models.PositiveIntegerField( default=0, help_text="Number of users who found this review helpful" ) total_votes = models.PositiveIntegerField( default=0, help_text="Total number of votes (helpful + not helpful)" ) # Moderation status MODERATION_PENDING = 'pending' MODERATION_APPROVED = 'approved' MODERATION_REJECTED = 'rejected' MODERATION_STATUS_CHOICES = [ (MODERATION_PENDING, 'Pending'), (MODERATION_APPROVED, 'Approved'), (MODERATION_REJECTED, 'Rejected'), ] moderation_status = models.CharField( max_length=20, choices=MODERATION_STATUS_CHOICES, default=MODERATION_PENDING, db_index=True ) moderation_notes = models.TextField( blank=True, help_text="Notes from moderator" ) moderated_at = models.DateTimeField(null=True, blank=True) moderated_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='moderated_reviews' ) # Link to ContentSubmission (Sacred Pipeline integration) submission = models.ForeignKey( 'moderation.ContentSubmission', on_delete=models.SET_NULL, null=True, blank=True, related_name='reviews', help_text="ContentSubmission that created this review" ) # Photos related to this review (via media.Photo model with generic relation) photos = GenericRelation('media.Photo') class Meta: ordering = ['-created'] indexes = [ models.Index(fields=['content_type', 'object_id']), models.Index(fields=['user', 'created']), models.Index(fields=['moderation_status', 'created']), models.Index(fields=['rating']), ] # A user can only review a specific park/ride once unique_together = [['user', 'content_type', 'object_id']] def __str__(self): entity_type = self.content_type.model return f"{self.user.username}'s review of {entity_type} #{self.object_id}" @property def helpful_percentage(self): """Calculate percentage of helpful votes.""" if self.total_votes == 0: return None return (self.helpful_votes / self.total_votes) * 100 @property def is_approved(self): """Check if review is approved.""" return self.moderation_status == self.MODERATION_APPROVED @property def is_pending(self): """Check if review is pending moderation.""" return self.moderation_status == self.MODERATION_PENDING class ReviewHelpfulVote(TimeStampedModel): """ Track individual helpful votes to prevent duplicate voting. """ review = models.ForeignKey( Review, on_delete=models.CASCADE, related_name='vote_records' ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='review_votes' ) is_helpful = models.BooleanField( help_text="True if user found review helpful, False if not helpful" ) class Meta: unique_together = [['review', 'user']] indexes = [ models.Index(fields=['review', 'user']), ] def __str__(self): vote_type = "helpful" if self.is_helpful else "not helpful" return f"{self.user.username} voted {vote_type} on review #{self.review.id}" def save(self, *args, **kwargs): """Update review vote counts when saving.""" is_new = self.pk is None old_is_helpful = None if not is_new: # Get old value before update old_vote = ReviewHelpfulVote.objects.get(pk=self.pk) old_is_helpful = old_vote.is_helpful super().save(*args, **kwargs) # Update review vote counts if is_new: # New vote self.review.total_votes += 1 if self.is_helpful: self.review.helpful_votes += 1 self.review.save() elif old_is_helpful != self.is_helpful: # Vote changed if self.is_helpful: self.review.helpful_votes += 1 else: self.review.helpful_votes -= 1 self.review.save() def delete(self, *args, **kwargs): """Update review vote counts when deleting.""" self.review.total_votes -= 1 if self.is_helpful: self.review.helpful_votes -= 1 self.review.save() super().delete(*args, **kwargs)