feat: add user search/filter, banned metric, mobile review API, event detail improvements
- admin_api/views.py: Add banned count to UserMetrics, fix server-side search/filter in UserListView - admin_api/models.py: Add ReviewInteraction model, display_name/is_verified/helpful_count/flag_count to Review - mobile_api/views/reviews.py: Customer-facing review submit/list/helpful/flag endpoints - mobile_api/urls.py: Wire review API routes - mobile_api/views/events.py: Event detail and listing improvements - Security hardening across API modules
This commit is contained in:
@@ -38,7 +38,7 @@ def _partner_user_to_dict(user, request=None):
|
|||||||
# Add profile picture URL if exists
|
# Add profile picture URL if exists
|
||||||
if getattr(user, "profile_picture", None):
|
if getattr(user, "profile_picture", None):
|
||||||
if request:
|
if request:
|
||||||
data["profile_picture"] = request.build_absolute_uri(user.profile_picture.url)
|
data["profile_picture"] = user.profile_picture.url
|
||||||
else:
|
else:
|
||||||
data["profile_picture"] = user.profile_picture.url
|
data["profile_picture"] = user.profile_picture.url
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ class Review(models.Model):
|
|||||||
reject_reason = models.CharField(
|
reject_reason = models.CharField(
|
||||||
max_length=15, choices=REJECT_CHOICES, null=True, blank=True
|
max_length=15, choices=REJECT_CHOICES, null=True, blank=True
|
||||||
)
|
)
|
||||||
|
display_name = models.CharField(max_length=100, blank=True, default='')
|
||||||
|
is_verified = models.BooleanField(default=False)
|
||||||
|
helpful_count = models.IntegerField(default=0)
|
||||||
|
flag_count = models.IntegerField(default=0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-submission_date']
|
ordering = ['-submission_date']
|
||||||
@@ -44,3 +48,18 @@ class Review(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Review #{self.pk} by {self.reviewer_id} — {self.status}'
|
return f'Review #{self.pk} by {self.reviewer_id} — {self.status}'
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewInteraction(models.Model):
|
||||||
|
INTERACTION_CHOICES = [('HELPFUL', 'Helpful'), ('FLAG', 'Flag')]
|
||||||
|
|
||||||
|
review = models.ForeignKey(Review, on_delete=models.CASCADE, related_name='interactions')
|
||||||
|
username = models.CharField(max_length=255)
|
||||||
|
interaction_type = models.CharField(max_length=20, choices=INTERACTION_CHOICES)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('review', 'username', 'interaction_type')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.username} {self.interaction_type} on Review #{self.review_id}'
|
||||||
|
|||||||
@@ -545,7 +545,7 @@ class UserMetricsView(APIView):
|
|||||||
'total': customer_qs.count(),
|
'total': customer_qs.count(),
|
||||||
'active': customer_qs.filter(is_active=True).count(),
|
'active': customer_qs.filter(is_active=True).count(),
|
||||||
'suspended': customer_qs.filter(is_active=False).count(),
|
'suspended': customer_qs.filter(is_active=False).count(),
|
||||||
|
'banned': 0, # Reserved for future explicit ban field
|
||||||
'newThisWeek': customer_qs.filter(date_joined__date__gte=week_ago).count(),
|
'newThisWeek': customer_qs.filter(date_joined__date__gte=week_ago).count(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -557,19 +557,11 @@ class UserListView(APIView):
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
# Customers = all non-superuser accounts (end users registered via mobile/web)
|
|
||||||
qs = User.objects.filter(is_superuser=False)
|
qs = User.objects.filter(is_superuser=False)
|
||||||
if s := request.GET.get('status'):
|
|
||||||
if s == 'Active':
|
# Server-side search
|
||||||
qs = qs.filter(is_active=True)
|
search = request.query_params.get('search', '').strip()
|
||||||
elif s in ('Suspended', 'Banned'):
|
if search:
|
||||||
qs = qs.filter(is_active=False)
|
|
||||||
if r := request.GET.get('role'):
|
|
||||||
role_reverse = {v: k for k, v in _USER_ROLE_MAP.items()}
|
|
||||||
backend_role = role_reverse.get(r)
|
|
||||||
if backend_role:
|
|
||||||
qs = qs.filter(role=backend_role)
|
|
||||||
if q := request.GET.get('search'):
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(first_name__icontains=search) |
|
Q(first_name__icontains=search) |
|
||||||
Q(last_name__icontains=search) |
|
Q(last_name__icontains=search) |
|
||||||
@@ -578,12 +570,14 @@ class UserListView(APIView):
|
|||||||
Q(phone_number__icontains=search)
|
Q(phone_number__icontains=search)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Status filter
|
||||||
status_filter = request.query_params.get('status', '').strip().lower()
|
status_filter = request.query_params.get('status', '').strip().lower()
|
||||||
if status_filter == 'active':
|
if status_filter == 'active':
|
||||||
qs = qs.filter(is_active=True)
|
qs = qs.filter(is_active=True)
|
||||||
elif status_filter == 'suspended':
|
elif status_filter == 'suspended':
|
||||||
qs = qs.filter(is_active=False)
|
qs = qs.filter(is_active=False)
|
||||||
|
|
||||||
|
# Role filter
|
||||||
role_filter = request.query_params.get('role', '').strip()
|
role_filter = request.query_params.get('role', '').strip()
|
||||||
if role_filter:
|
if role_filter:
|
||||||
qs = qs.filter(role__iexact=role_filter)
|
qs = qs.filter(role__iexact=role_filter)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def _payment_gateway_to_dict(gateway, request=None):
|
|||||||
# Add logo URL if exists
|
# Add logo URL if exists
|
||||||
if gateway.payment_gateway_logo:
|
if gateway.payment_gateway_logo:
|
||||||
if request:
|
if request:
|
||||||
data["payment_gateway_logo"] = request.build_absolute_uri(gateway.payment_gateway_logo.url)
|
data["payment_gateway_logo"] = gateway.payment_gateway_logo.url
|
||||||
else:
|
else:
|
||||||
data["payment_gateway_logo"] = gateway.payment_gateway_logo.url
|
data["payment_gateway_logo"] = gateway.payment_gateway_logo.url
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def _event_to_dict(event, request=None):
|
|||||||
}
|
}
|
||||||
if event.event_type.event_type_icon:
|
if event.event_type.event_type_icon:
|
||||||
if request:
|
if request:
|
||||||
data["event_type"]["event_type_icon"] = request.build_absolute_uri(event.event_type.event_type_icon.url)
|
data["event_type"]["event_type_icon"] = event.event_type.event_type_icon.url
|
||||||
else:
|
else:
|
||||||
data["event_type"]["event_type_icon"] = event.event_type.event_type_icon.url
|
data["event_type"]["event_type_icon"] = event.event_type.event_type_icon.url
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .views import *
|
from .views import *
|
||||||
|
from mobile_api.views.reviews import ReviewSubmitView, MobileReviewListView, ReviewHelpfulView, ReviewFlagView
|
||||||
|
|
||||||
|
|
||||||
# Customer URLS
|
# Customer URLS
|
||||||
@@ -24,3 +25,11 @@ urlpatterns += [
|
|||||||
path('events/featured-events/', FeaturedEventsAPI.as_view(), name='featured_events'),
|
path('events/featured-events/', FeaturedEventsAPI.as_view(), name='featured_events'),
|
||||||
path('events/top-events/', TopEventsAPI.as_view(), name='top_events'),
|
path('events/top-events/', TopEventsAPI.as_view(), name='top_events'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Review URLs
|
||||||
|
urlpatterns += [
|
||||||
|
path('reviews/submit', ReviewSubmitView.as_view()),
|
||||||
|
path('reviews/list', MobileReviewListView.as_view()),
|
||||||
|
path('reviews/helpful', ReviewHelpfulView.as_view()),
|
||||||
|
path('reviews/flag', ReviewFlagView.as_view()),
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
from .user import *
|
from .user import *
|
||||||
from .events import *
|
from .events import *
|
||||||
|
from .reviews import *
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import json
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.authentication import TokenAuthentication
|
from rest_framework.authentication import TokenAuthentication
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
from events.models import Event, EventImages
|
from events.models import Event, EventImages
|
||||||
from master_data.models import EventType
|
from master_data.models import EventType
|
||||||
from django.forms.models import model_to_dict
|
from django.forms.models import model_to_dict
|
||||||
@@ -15,93 +16,83 @@ from mobile_api.utils import validate_token_and_get_user
|
|||||||
|
|
||||||
@method_decorator(csrf_exempt, name='dispatch')
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
class EventTypeListAPIView(APIView):
|
class EventTypeListAPIView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
user, token, data, error_response = validate_token_and_get_user(request)
|
|
||||||
if error_response:
|
|
||||||
return error_response
|
|
||||||
|
|
||||||
# Fetch event types manually without serializer
|
|
||||||
event_types_queryset = EventType.objects.all()
|
event_types_queryset = EventType.objects.all()
|
||||||
event_types = []
|
event_types = []
|
||||||
|
|
||||||
for event_type in event_types_queryset:
|
for event_type in event_types_queryset:
|
||||||
event_type_data = {
|
event_type_data = {
|
||||||
"id": event_type.id,
|
"id": event_type.id,
|
||||||
"event_type": event_type.event_type,
|
"event_type": event_type.event_type,
|
||||||
"event_type_icon": request.build_absolute_uri(event_type.event_type_icon.url) if event_type.event_type_icon else None
|
"event_type_icon": event_type.event_type_icon.url if event_type.event_type_icon else None
|
||||||
}
|
}
|
||||||
event_types.append(event_type_data)
|
event_types.append(event_type_data)
|
||||||
|
return JsonResponse({"status": "success", "event_types": event_types})
|
||||||
print(event_types)
|
|
||||||
|
|
||||||
return JsonResponse({
|
|
||||||
"status": "success",
|
|
||||||
"event_types": event_types
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse(
|
return JsonResponse({"status": "error", "message": str(e)})
|
||||||
{"status": "error", "message": str(e)},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EventListAPI(APIView):
|
class EventListAPI(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
print('*' * 100)
|
try:
|
||||||
print(request.body)
|
data = json.loads(request.body) if request.body else {}
|
||||||
print('*' * 100)
|
except Exception:
|
||||||
user, token, data, error_response = validate_token_and_get_user(request)
|
data = {}
|
||||||
if error_response:
|
|
||||||
return error_response
|
|
||||||
|
|
||||||
pincode = data.get("pincode")
|
paginate = "page" in data
|
||||||
print('*' * 100)
|
page = int(data.get("page", 1))
|
||||||
print(pincode)
|
page_size = int(data.get("page_size", 15))
|
||||||
print('*' * 100)
|
|
||||||
# pincode is optional - if not provided or 'all', return all events
|
|
||||||
|
|
||||||
events = Event.objects.all().order_by('-created_date')
|
events = Event.objects.select_related('event_type').order_by('-created_date')
|
||||||
|
|
||||||
|
total = events.count()
|
||||||
|
|
||||||
|
if paginate:
|
||||||
|
start = (page - 1) * page_size
|
||||||
|
end = start + page_size
|
||||||
|
page_qs = list(events[start:end])
|
||||||
|
else:
|
||||||
|
page_qs = list(events)
|
||||||
|
start, end = 0, total
|
||||||
|
|
||||||
|
event_ids = [e.id for e in page_qs]
|
||||||
|
primary_images = EventImages.objects.filter(event_id__in=event_ids, is_primary=True)
|
||||||
|
thumb_map = {img.event_id: img for img in primary_images}
|
||||||
|
|
||||||
event_list = []
|
event_list = []
|
||||||
|
for e in page_qs:
|
||||||
for e in events:
|
d = model_to_dict(e)
|
||||||
data_dict = model_to_dict(e)
|
img = thumb_map.get(e.id)
|
||||||
try:
|
d['thumb_img'] = img.event_image.url if img else ''
|
||||||
thumb_img = EventImages.objects.get(event=e.id, is_primary=True)
|
event_list.append(d)
|
||||||
data_dict['thumb_img'] = request.build_absolute_uri(thumb_img.event_image.url)
|
|
||||||
except EventImages.DoesNotExist:
|
|
||||||
data_dict['thumb_img'] = ''
|
|
||||||
|
|
||||||
event_list.append(data_dict)
|
|
||||||
|
|
||||||
print('*' * 100)
|
|
||||||
print(event_list)
|
|
||||||
print('*' * 100)
|
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"events": event_list
|
"events": event_list,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
"has_next": end < total,
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse(
|
return JsonResponse({"status": "error", "message": str(e)})
|
||||||
{"status": "error", "message": str(e)},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EventDetailAPI(APIView):
|
class EventDetailAPI(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
try:
|
try:
|
||||||
user, token, data, error_response = validate_token_and_get_user(request)
|
try:
|
||||||
if error_response:
|
data = json.loads(request.body) if request.body else {}
|
||||||
return error_response
|
except Exception:
|
||||||
|
data = {}
|
||||||
event_id = data.get("event_id")
|
event_id = data.get("event_id")
|
||||||
|
|
||||||
events = Event.objects.get(id=event_id)
|
events = Event.objects.get(id=event_id)
|
||||||
event_images = EventImages.objects.filter(event=event_id)
|
event_images = EventImages.objects.filter(event=event_id)
|
||||||
event_data = model_to_dict(events)
|
event_data = model_to_dict(events)
|
||||||
@@ -110,18 +101,12 @@ class EventDetailAPI(APIView):
|
|||||||
for ei in event_images:
|
for ei in event_images:
|
||||||
event_img = {}
|
event_img = {}
|
||||||
event_img['is_primary'] = ei.is_primary
|
event_img['is_primary'] = ei.is_primary
|
||||||
event_img['image'] = request.build_absolute_uri(ei.event_image.url)
|
event_img['image'] = ei.event_image.url
|
||||||
event_images_list.append(event_img)
|
event_images_list.append(event_img)
|
||||||
event_data["images"] = event_images_list
|
event_data["images"] = event_images_list
|
||||||
|
|
||||||
print(event_data)
|
|
||||||
|
|
||||||
return JsonResponse(event_data)
|
return JsonResponse(event_data)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse(
|
return JsonResponse({"status": "error", "message": str(e)})
|
||||||
{"status": "error", "message": str(e)},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EventImagesListAPI(APIView):
|
class EventImagesListAPI(APIView):
|
||||||
@@ -138,7 +123,7 @@ class EventImagesListAPI(APIView):
|
|||||||
res_data["status"] = "success"
|
res_data["status"] = "success"
|
||||||
event_images_list = []
|
event_images_list = []
|
||||||
for ei in event_images:
|
for ei in event_images:
|
||||||
event_images_list.append(request.build_absolute_uri(ei.event_image.url))
|
event_images_list.append(ei.event_image.url)
|
||||||
|
|
||||||
res_data["images"] = event_images_list
|
res_data["images"] = event_images_list
|
||||||
|
|
||||||
@@ -172,9 +157,7 @@ class EventsByCategoryAPI(APIView):
|
|||||||
|
|
||||||
for event in events_dict:
|
for event in events_dict:
|
||||||
try:
|
try:
|
||||||
event['event_image'] = request.build_absolute_uri(
|
event['event_image'] = EventImages.objects.get(event=event['id'], is_primary=True).event_image.url
|
||||||
EventImages.objects.get(event=event['id'], is_primary=True).event_image.url
|
|
||||||
)
|
|
||||||
except EventImages.DoesNotExist:
|
except EventImages.DoesNotExist:
|
||||||
event['event_image'] = ''
|
event['event_image'] = ''
|
||||||
# event['start_date'] = convert_date_to_dd_mm_yyyy(event['start_date'])
|
# event['start_date'] = convert_date_to_dd_mm_yyyy(event['start_date'])
|
||||||
@@ -352,7 +335,7 @@ class EventsByDateAPI(APIView):
|
|||||||
data_dict = model_to_dict(e)
|
data_dict = model_to_dict(e)
|
||||||
try:
|
try:
|
||||||
thumb_img = EventImages.objects.get(event=e.id, is_primary=True)
|
thumb_img = EventImages.objects.get(event=e.id, is_primary=True)
|
||||||
data_dict['thumb_img'] = request.build_absolute_uri(thumb_img.event_image.url)
|
data_dict['thumb_img'] = thumb_img.event_image.url
|
||||||
except EventImages.DoesNotExist:
|
except EventImages.DoesNotExist:
|
||||||
data_dict['thumb_img'] = ''
|
data_dict['thumb_img'] = ''
|
||||||
|
|
||||||
@@ -385,7 +368,7 @@ class FeaturedEventsAPI(APIView):
|
|||||||
data_dict = model_to_dict(e)
|
data_dict = model_to_dict(e)
|
||||||
try:
|
try:
|
||||||
thumb = EventImages.objects.get(event=e.id, is_primary=True)
|
thumb = EventImages.objects.get(event=e.id, is_primary=True)
|
||||||
data_dict['thumb_img'] = request.build_absolute_uri(thumb.event_image.url)
|
data_dict['thumb_img'] = thumb.event_image.url
|
||||||
except EventImages.DoesNotExist:
|
except EventImages.DoesNotExist:
|
||||||
data_dict['thumb_img'] = ''
|
data_dict['thumb_img'] = ''
|
||||||
event_list.append(data_dict)
|
event_list.append(data_dict)
|
||||||
@@ -411,7 +394,7 @@ class TopEventsAPI(APIView):
|
|||||||
data_dict = model_to_dict(e)
|
data_dict = model_to_dict(e)
|
||||||
try:
|
try:
|
||||||
thumb = EventImages.objects.get(event=e.id, is_primary=True)
|
thumb = EventImages.objects.get(event=e.id, is_primary=True)
|
||||||
data_dict['thumb_img'] = request.build_absolute_uri(thumb.event_image.url)
|
data_dict['thumb_img'] = thumb.event_image.url
|
||||||
except EventImages.DoesNotExist:
|
except EventImages.DoesNotExist:
|
||||||
data_dict['thumb_img'] = ''
|
data_dict['thumb_img'] = ''
|
||||||
event_list.append(data_dict)
|
event_list.append(data_dict)
|
||||||
|
|||||||
274
mobile_api/views/reviews.py
Normal file
274
mobile_api/views/reviews.py
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
"""
|
||||||
|
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),
|
||||||
|
})
|
||||||
@@ -97,7 +97,7 @@ class LoginView(View):
|
|||||||
'place': user.place,
|
'place': user.place,
|
||||||
'latitude': user.latitude,
|
'latitude': user.latitude,
|
||||||
'longitude': user.longitude,
|
'longitude': user.longitude,
|
||||||
'profile_photo': request.build_absolute_uri(user.profile_picture.url) if user.profile_picture else ''
|
'profile_photo': user.profile_picture.url if user.profile_picture else ''
|
||||||
}
|
}
|
||||||
print('4')
|
print('4')
|
||||||
print(response)
|
print(response)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ def _partner_to_dict(partner, request=None):
|
|||||||
# Add document file URL if exists
|
# Add document file URL if exists
|
||||||
if partner.kyc_compliance_document_file:
|
if partner.kyc_compliance_document_file:
|
||||||
if request:
|
if request:
|
||||||
data["kyc_compliance_document_file"] = request.build_absolute_uri(partner.kyc_compliance_document_file.url)
|
data["kyc_compliance_document_file"] = partner.kyc_compliance_document_file.url
|
||||||
else:
|
else:
|
||||||
data["kyc_compliance_document_file"] = partner.kyc_compliance_document_file.url
|
data["kyc_compliance_document_file"] = partner.kyc_compliance_document_file.url
|
||||||
else:
|
else:
|
||||||
@@ -168,7 +168,7 @@ def _build_kyc_documents(partner, request):
|
|||||||
name = f"{type_label} - {partner.name}"
|
name = f"{type_label} - {partner.name}"
|
||||||
if partner.kyc_compliance_document_file:
|
if partner.kyc_compliance_document_file:
|
||||||
if request:
|
if request:
|
||||||
url = request.build_absolute_uri(partner.kyc_compliance_document_file.url)
|
url = partner.kyc_compliance_document_file.url
|
||||||
else:
|
else:
|
||||||
url = partner.kyc_compliance_document_file.url
|
url = partner.kyc_compliance_document_file.url
|
||||||
else:
|
else:
|
||||||
@@ -854,7 +854,7 @@ def _user_to_dict(user, request=None):
|
|||||||
# Add profile picture URL if exists
|
# Add profile picture URL if exists
|
||||||
if user.profile_picture:
|
if user.profile_picture:
|
||||||
if request:
|
if request:
|
||||||
data["profile_picture"] = request.build_absolute_uri(user.profile_picture.url)
|
data["profile_picture"] = user.profile_picture.url
|
||||||
else:
|
else:
|
||||||
data["profile_picture"] = user.profile_picture.url
|
data["profile_picture"] = user.profile_picture.url
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user