275 lines
9.7 KiB
Python
275 lines
9.7 KiB
Python
|
|
"""
|
||
|
|
Customer-facing review API views.
|
||
|
|
Writes to admin_api.Review so admin panel sees reviews immediately.
|
||
|
|
"""
|
||
|
|
import json
|
||
|
|
from django.http import JsonResponse
|
||
|
|
from django.utils.decorators import method_decorator
|
||
|
|
from django.views.decorators.csrf import csrf_exempt
|
||
|
|
from rest_framework.views import APIView
|
||
|
|
from rest_framework.permissions import AllowAny
|
||
|
|
from django.db.models import Avg, Count, Q
|
||
|
|
from admin_api.models import Review, ReviewInteraction
|
||
|
|
from events.models import Event
|
||
|
|
from mobile_api.utils import validate_token_and_get_user
|
||
|
|
|
||
|
|
|
||
|
|
_STATUS_TO_JSON = {'live': 'PUBLISHED', 'pending': 'PENDING', 'rejected': 'FLAGGED'}
|
||
|
|
_JSON_TO_STATUS = {'PUBLISHED': 'live', 'PENDING': 'pending', 'FLAGGED': 'rejected'}
|
||
|
|
|
||
|
|
|
||
|
|
def _serialize_review(r, user_interactions=None):
|
||
|
|
"""Serialize a Review to match the customer app's expected shape."""
|
||
|
|
interactions = user_interactions or {}
|
||
|
|
try:
|
||
|
|
display = r.display_name or r.reviewer.get_full_name() or r.reviewer.username
|
||
|
|
except Exception:
|
||
|
|
display = r.display_name or ''
|
||
|
|
try:
|
||
|
|
uname = r.reviewer.username
|
||
|
|
except Exception:
|
||
|
|
uname = ''
|
||
|
|
return {
|
||
|
|
'id': r.id,
|
||
|
|
'event_id': r.event_id,
|
||
|
|
'username': uname,
|
||
|
|
'display_name': display,
|
||
|
|
'rating': r.rating,
|
||
|
|
'comment': r.review_text,
|
||
|
|
'status': _STATUS_TO_JSON.get(r.status, r.status),
|
||
|
|
'is_verified': r.is_verified,
|
||
|
|
'helpful_count': r.helpful_count,
|
||
|
|
'flag_count': r.flag_count,
|
||
|
|
'has_helpful': interactions.get('HELPFUL', False),
|
||
|
|
'has_flagged': interactions.get('FLAG', False),
|
||
|
|
'created_at': r.submission_date.isoformat() if r.submission_date else '',
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _rating_distribution(event_id):
|
||
|
|
"""Return {1:count, 2:count, ..., 5:count} for live reviews."""
|
||
|
|
dist = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}
|
||
|
|
qs = Review.objects.filter(event_id=event_id, status='live').values('rating').annotate(c=Count('id'))
|
||
|
|
for row in qs:
|
||
|
|
dist[row['rating']] = row['c']
|
||
|
|
return dist
|
||
|
|
|
||
|
|
|
||
|
|
def _aggregates(event_id):
|
||
|
|
"""Return (average_rating, review_count) for live reviews."""
|
||
|
|
agg = Review.objects.filter(event_id=event_id, status='live').aggregate(
|
||
|
|
avg=Avg('rating'), cnt=Count('id')
|
||
|
|
)
|
||
|
|
return round(float(agg['avg'] or 0), 1), agg['cnt'] or 0
|
||
|
|
|
||
|
|
|
||
|
|
@method_decorator(csrf_exempt, name='dispatch')
|
||
|
|
class ReviewSubmitView(APIView):
|
||
|
|
"""POST /api/reviews/submit -- Submit or update a review."""
|
||
|
|
authentication_classes = []
|
||
|
|
permission_classes = [AllowAny]
|
||
|
|
|
||
|
|
def post(self, request):
|
||
|
|
user, token, data, error_response = validate_token_and_get_user(request)
|
||
|
|
if error_response:
|
||
|
|
return error_response
|
||
|
|
|
||
|
|
event_id = data.get('event_id')
|
||
|
|
rating = data.get('rating')
|
||
|
|
comment = data.get('comment', '') or ''
|
||
|
|
is_verified = bool(data.get('is_verified', False))
|
||
|
|
|
||
|
|
if not event_id or not rating:
|
||
|
|
return JsonResponse({'status': 'error', 'message': 'event_id and rating are required'}, status=400)
|
||
|
|
|
||
|
|
try:
|
||
|
|
rating = int(rating)
|
||
|
|
if rating < 1 or rating > 5:
|
||
|
|
raise ValueError
|
||
|
|
except (ValueError, TypeError):
|
||
|
|
return JsonResponse({'status': 'error', 'message': 'Rating must be 1-5'}, status=400)
|
||
|
|
|
||
|
|
try:
|
||
|
|
event = Event.objects.get(pk=event_id)
|
||
|
|
except Event.DoesNotExist:
|
||
|
|
return JsonResponse({'status': 'error', 'message': 'Event not found'}, status=404)
|
||
|
|
|
||
|
|
# Upsert: one review per user per event
|
||
|
|
review, created = Review.objects.update_or_create(
|
||
|
|
reviewer=user,
|
||
|
|
event=event,
|
||
|
|
defaults={
|
||
|
|
'rating': rating,
|
||
|
|
'review_text': comment,
|
||
|
|
'is_verified': is_verified,
|
||
|
|
'status': 'live', # auto-approve for customer submissions
|
||
|
|
'display_name': user.get_full_name() or user.username,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
avg_rating, review_count = _aggregates(event_id)
|
||
|
|
|
||
|
|
return JsonResponse({
|
||
|
|
'status': 'success',
|
||
|
|
'review': _serialize_review(review),
|
||
|
|
'average_rating': avg_rating,
|
||
|
|
'review_count': review_count,
|
||
|
|
})
|
||
|
|
|
||
|
|
|
||
|
|
@method_decorator(csrf_exempt, name='dispatch')
|
||
|
|
class MobileReviewListView(APIView):
|
||
|
|
"""POST /api/reviews/list -- Get published reviews for an event."""
|
||
|
|
authentication_classes = []
|
||
|
|
permission_classes = [AllowAny]
|
||
|
|
|
||
|
|
def post(self, request):
|
||
|
|
try:
|
||
|
|
body = json.loads(request.body) if request.body else {}
|
||
|
|
except json.JSONDecodeError:
|
||
|
|
body = {}
|
||
|
|
|
||
|
|
event_id = body.get('event_id')
|
||
|
|
username = body.get('username', '')
|
||
|
|
page = int(body.get('page', 1))
|
||
|
|
page_size = min(int(body.get('page_size', 10)), 50)
|
||
|
|
|
||
|
|
if not event_id:
|
||
|
|
return JsonResponse({'status': 'error', 'message': 'event_id is required'}, status=400)
|
||
|
|
|
||
|
|
qs = Review.objects.filter(event_id=event_id, status='live').select_related('reviewer')
|
||
|
|
qs = qs.order_by('-is_verified', '-helpful_count', '-submission_date')
|
||
|
|
|
||
|
|
total = qs.count()
|
||
|
|
reviews = list(qs[(page - 1) * page_size: page * page_size])
|
||
|
|
|
||
|
|
# Bulk lookup interactions for current user
|
||
|
|
user_interactions = {}
|
||
|
|
if username and reviews:
|
||
|
|
review_ids = [r.id for r in reviews]
|
||
|
|
interactions = ReviewInteraction.objects.filter(
|
||
|
|
username=username, review_id__in=review_ids
|
||
|
|
)
|
||
|
|
for inter in interactions:
|
||
|
|
if inter.review_id not in user_interactions:
|
||
|
|
user_interactions[inter.review_id] = {}
|
||
|
|
user_interactions[inter.review_id][inter.interaction_type] = True
|
||
|
|
|
||
|
|
formatted = [
|
||
|
|
_serialize_review(r, user_interactions.get(r.id, {}))
|
||
|
|
for r in reviews
|
||
|
|
]
|
||
|
|
|
||
|
|
avg_rating, review_count = _aggregates(event_id)
|
||
|
|
distribution = _rating_distribution(event_id)
|
||
|
|
|
||
|
|
# Get user's own review (any status)
|
||
|
|
user_review = None
|
||
|
|
if username:
|
||
|
|
try:
|
||
|
|
from accounts.models import User
|
||
|
|
u = User.objects.get(username=username)
|
||
|
|
ur = Review.objects.filter(event_id=event_id, reviewer=u).first()
|
||
|
|
if ur:
|
||
|
|
user_review = _serialize_review(ur)
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
return JsonResponse({
|
||
|
|
'status': 'success',
|
||
|
|
'reviews': formatted,
|
||
|
|
'total': total,
|
||
|
|
'page': page,
|
||
|
|
'page_size': page_size,
|
||
|
|
'average_rating': avg_rating,
|
||
|
|
'review_count': review_count,
|
||
|
|
'distribution': distribution,
|
||
|
|
'user_review': user_review,
|
||
|
|
})
|
||
|
|
|
||
|
|
|
||
|
|
@method_decorator(csrf_exempt, name='dispatch')
|
||
|
|
class ReviewHelpfulView(APIView):
|
||
|
|
"""POST /api/reviews/helpful -- Mark a review as helpful."""
|
||
|
|
authentication_classes = []
|
||
|
|
permission_classes = [AllowAny]
|
||
|
|
|
||
|
|
def post(self, request):
|
||
|
|
user, token, data, error_response = validate_token_and_get_user(request)
|
||
|
|
if error_response:
|
||
|
|
return error_response
|
||
|
|
|
||
|
|
review_id = data.get('review_id')
|
||
|
|
if not review_id:
|
||
|
|
return JsonResponse({'status': 'error', 'message': 'review_id is required'}, status=400)
|
||
|
|
|
||
|
|
try:
|
||
|
|
review = Review.objects.get(pk=review_id)
|
||
|
|
except Review.DoesNotExist:
|
||
|
|
return JsonResponse({'status': 'error', 'message': 'Review not found'}, status=404)
|
||
|
|
|
||
|
|
if review.reviewer == user:
|
||
|
|
return JsonResponse({'status': 'error', 'message': 'Cannot mark your own review as helpful'}, status=400)
|
||
|
|
|
||
|
|
_, created = ReviewInteraction.objects.get_or_create(
|
||
|
|
review=review,
|
||
|
|
username=user.username,
|
||
|
|
interaction_type='HELPFUL'
|
||
|
|
)
|
||
|
|
|
||
|
|
if created:
|
||
|
|
new_count = ReviewInteraction.objects.filter(review=review, interaction_type='HELPFUL').count()
|
||
|
|
review.helpful_count = new_count
|
||
|
|
review.save(update_fields=['helpful_count'])
|
||
|
|
|
||
|
|
return JsonResponse({
|
||
|
|
'status': 'success',
|
||
|
|
'helpful_count': review.helpful_count,
|
||
|
|
})
|
||
|
|
|
||
|
|
|
||
|
|
@method_decorator(csrf_exempt, name='dispatch')
|
||
|
|
class ReviewFlagView(APIView):
|
||
|
|
"""POST /api/reviews/flag -- Flag/report a review."""
|
||
|
|
authentication_classes = []
|
||
|
|
permission_classes = [AllowAny]
|
||
|
|
|
||
|
|
def post(self, request):
|
||
|
|
user, token, data, error_response = validate_token_and_get_user(request)
|
||
|
|
if error_response:
|
||
|
|
return error_response
|
||
|
|
|
||
|
|
review_id = data.get('review_id')
|
||
|
|
if not review_id:
|
||
|
|
return JsonResponse({'status': 'error', 'message': 'review_id is required'}, status=400)
|
||
|
|
|
||
|
|
try:
|
||
|
|
review = Review.objects.get(pk=review_id)
|
||
|
|
except Review.DoesNotExist:
|
||
|
|
return JsonResponse({'status': 'error', 'message': 'Review not found'}, status=404)
|
||
|
|
|
||
|
|
if review.reviewer == user:
|
||
|
|
return JsonResponse({'status': 'error', 'message': 'Cannot flag your own review'}, status=400)
|
||
|
|
|
||
|
|
_, created = ReviewInteraction.objects.get_or_create(
|
||
|
|
review=review,
|
||
|
|
username=user.username,
|
||
|
|
interaction_type='FLAG'
|
||
|
|
)
|
||
|
|
|
||
|
|
new_count = ReviewInteraction.objects.filter(review=review, interaction_type='FLAG').count()
|
||
|
|
new_status = review.status
|
||
|
|
if new_count >= 3 and review.status == 'live':
|
||
|
|
new_status = 'rejected'
|
||
|
|
review.status = 'rejected'
|
||
|
|
review.reject_reason = 'inappropriate'
|
||
|
|
|
||
|
|
review.flag_count = new_count
|
||
|
|
review.save(update_fields=['flag_count', 'status', 'reject_reason'])
|
||
|
|
|
||
|
|
return JsonResponse({
|
||
|
|
'status': 'success',
|
||
|
|
'flag_count': new_count,
|
||
|
|
'review_status': _STATUS_TO_JSON.get(new_status, new_status),
|
||
|
|
})
|