mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 03:31:13 -05:00
- Added Review model with fields for user, content type, title, content, rating, visit metadata, helpful votes, moderation status, and timestamps. - Created ReviewHelpfulVote model to track user votes on reviews. - Implemented moderation workflow for reviews with approve and reject methods. - Developed admin interface for managing reviews and helpful votes, including custom display methods and actions for bulk approval/rejection. - Added migrations for the new models and their relationships. - Ensured unique constraints and indexes for efficient querying.
209 lines
6.5 KiB
Python
209 lines
6.5 KiB
Python
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
|
|
|
|
|
|
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'
|
|
)
|
|
|
|
# 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
|
|
|
|
def approve(self, moderator, notes=''):
|
|
"""Approve the review."""
|
|
from django.utils import timezone
|
|
self.moderation_status = self.MODERATION_APPROVED
|
|
self.moderated_by = moderator
|
|
self.moderated_at = timezone.now()
|
|
self.moderation_notes = notes
|
|
self.save()
|
|
|
|
def reject(self, moderator, notes=''):
|
|
"""Reject the review."""
|
|
from django.utils import timezone
|
|
self.moderation_status = self.MODERATION_REJECTED
|
|
self.moderated_by = moderator
|
|
self.moderated_at = timezone.now()
|
|
self.moderation_notes = notes
|
|
self.save()
|
|
|
|
|
|
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)
|