""" 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 = '' try: pic = r.reviewer.profile_picture if pic and pic.name and 'default.png' not in pic.name: profile_photo = pic.url else: profile_photo = '' except Exception: profile_photo = '' return { 'id': r.id, 'event_id': r.event_id, 'username': uname, 'display_name': display, 'profile_photo': profile_photo, '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), })