Phase 7: Reviews Moderation — Review model + migration + 4 admin endpoints (metrics, list, moderate, delete)
This commit is contained in:
36
admin_api/migrations/0001_initial.py
Normal file
36
admin_api/migrations/0001_initial.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 4.2.21 on 2026-03-25 02:17
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('events', '0011_dashboard_indexes'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Review',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('rating', models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)])),
|
||||
('review_text', models.TextField()),
|
||||
('submission_date', models.DateTimeField(auto_now_add=True)),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('live', 'Live'), ('rejected', 'Rejected')], default='pending', max_length=10)),
|
||||
('reject_reason', models.CharField(blank=True, choices=[('spam', 'Spam'), ('inappropriate', 'Inappropriate'), ('fake', 'Fake')], max_length=15, null=True)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admin_reviews', to='events.event')),
|
||||
('reviewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='admin_reviews', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-submission_date'],
|
||||
'indexes': [models.Index(fields=['status'], name='admin_api_r_status_2f6c07_idx'), models.Index(fields=['submission_date'], name='admin_api_r_submiss_02d0c1_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
admin_api/migrations/__init__.py
Normal file
0
admin_api/migrations/__init__.py
Normal file
46
admin_api/models.py
Normal file
46
admin_api/models.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from django.db import models
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
|
||||
|
||||
class Review(models.Model):
|
||||
STATUS_PENDING = 'pending'
|
||||
STATUS_LIVE = 'live'
|
||||
STATUS_REJECTED = 'rejected'
|
||||
STATUS_CHOICES = [
|
||||
(STATUS_PENDING, 'Pending'),
|
||||
(STATUS_LIVE, 'Live'),
|
||||
(STATUS_REJECTED, 'Rejected'),
|
||||
]
|
||||
REJECT_CHOICES = [
|
||||
('spam', 'Spam'),
|
||||
('inappropriate', 'Inappropriate'),
|
||||
('fake', 'Fake'),
|
||||
]
|
||||
|
||||
reviewer = models.ForeignKey(
|
||||
'accounts.User', on_delete=models.CASCADE, related_name='admin_reviews'
|
||||
)
|
||||
event = models.ForeignKey(
|
||||
'events.Event', on_delete=models.CASCADE, related_name='admin_reviews'
|
||||
)
|
||||
rating = models.IntegerField(
|
||||
validators=[MinValueValidator(1), MaxValueValidator(5)]
|
||||
)
|
||||
review_text = models.TextField()
|
||||
submission_date = models.DateTimeField(auto_now_add=True)
|
||||
status = models.CharField(
|
||||
max_length=10, choices=STATUS_CHOICES, default=STATUS_PENDING
|
||||
)
|
||||
reject_reason = models.CharField(
|
||||
max_length=15, choices=REJECT_CHOICES, null=True, blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-submission_date']
|
||||
indexes = [
|
||||
models.Index(fields=['status']),
|
||||
models.Index(fields=['submission_date']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f'Review #{self.pk} by {self.reviewer_id} — {self.status}'
|
||||
@@ -31,4 +31,9 @@ urlpatterns = [
|
||||
path('financials/transactions/', views.TransactionListView.as_view(), name='transaction-list'),
|
||||
path('financials/settlements/', views.SettlementListView.as_view(), name='settlement-list'),
|
||||
path('financials/settlements/<int:pk>/release/', views.SettlementReleaseView.as_view(), name='settlement-release'),
|
||||
]
|
||||
|
||||
path('reviews/metrics/', views.ReviewMetricsView.as_view(), name='review-metrics'),
|
||||
path('reviews/', views.ReviewListView.as_view(), name='review-list'),
|
||||
path('reviews/<int:pk>/moderate/', views.ReviewModerationView.as_view(), name='review-moderate'),
|
||||
path('reviews/<int:pk>/', views.ReviewDeleteView.as_view(), name='review-delete'),
|
||||
]
|
||||
@@ -841,3 +841,122 @@ class SettlementReleaseView(APIView):
|
||||
p.payment_transaction_status = 'completed'
|
||||
p.save(update_fields=['payment_transaction_status'])
|
||||
return Response(_serialize_settlement(p))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 7: Reviews Moderation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _reviewer_rank(total_reviews):
|
||||
if total_reviews >= 41:
|
||||
return 'Legend'
|
||||
if total_reviews >= 21:
|
||||
return 'Champion'
|
||||
if total_reviews >= 11:
|
||||
return 'Enthusiast'
|
||||
if total_reviews >= 4:
|
||||
return 'Contributor'
|
||||
return 'Explorer'
|
||||
|
||||
|
||||
def _serialize_review(r):
|
||||
from admin_api.models import Review
|
||||
try:
|
||||
name = r.reviewer.get_full_name() or r.reviewer.username
|
||||
email = r.reviewer.email
|
||||
total = Review.objects.filter(reviewer=r.reviewer, status='live').count()
|
||||
except Exception:
|
||||
name, email, total = '', '', 0
|
||||
try:
|
||||
event_name = getattr(r.event, 'title', None) or getattr(r.event, 'name', '') or ''
|
||||
event_id = str(r.event.id)
|
||||
start = getattr(r.event, 'start_date', None)
|
||||
event_date = start.isoformat() if start else ''
|
||||
except Exception:
|
||||
event_name, event_id, event_date = '', '', ''
|
||||
return {
|
||||
'id': str(r.id),
|
||||
'reviewerName': name,
|
||||
'reviewerEmail': email,
|
||||
'reviewerAvatar': '',
|
||||
'eventName': event_name,
|
||||
'eventId': event_id,
|
||||
'eventDate': event_date,
|
||||
'rating': r.rating,
|
||||
'reviewText': r.review_text,
|
||||
'submissionDate': r.submission_date.isoformat(),
|
||||
'status': r.status,
|
||||
'reviewerRank': _reviewer_rank(total),
|
||||
'reviewerTotalReviews': total,
|
||||
'rejectReason': r.reject_reason or '',
|
||||
}
|
||||
|
||||
|
||||
class ReviewMetricsView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
from admin_api.models import Review
|
||||
return Response({
|
||||
'totalPending': Review.objects.filter(status='pending').count(),
|
||||
'liveReviews': Review.objects.filter(status='live').count(),
|
||||
'rejected': Review.objects.filter(status='rejected').count(),
|
||||
})
|
||||
|
||||
|
||||
class ReviewListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
from admin_api.models import Review
|
||||
qs = Review.objects.select_related('reviewer', 'event').order_by('-submission_date')
|
||||
status = request.GET.get('status')
|
||||
if status in ('pending', 'live', 'rejected'):
|
||||
qs = qs.filter(status=status)
|
||||
try:
|
||||
page = max(1, int(request.GET.get('page', 1)))
|
||||
page_size = min(100, int(request.GET.get('page_size', 20)))
|
||||
except (ValueError, TypeError):
|
||||
page, page_size = 1, 20
|
||||
total = qs.count()
|
||||
reviews = qs[(page - 1) * page_size: page * page_size]
|
||||
return Response({'count': total, 'results': [_serialize_review(r) for r in reviews]})
|
||||
|
||||
|
||||
class ReviewModerationView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def patch(self, request, pk):
|
||||
from admin_api.models import Review
|
||||
from django.shortcuts import get_object_or_404
|
||||
review = get_object_or_404(Review, pk=pk)
|
||||
action = request.data.get('action')
|
||||
if action == 'approve':
|
||||
review.status = 'live'
|
||||
elif action == 'reject':
|
||||
rr = request.data.get('reject_reason', 'spam')
|
||||
valid_reasons = [c[0] for c in Review.REJECT_CHOICES]
|
||||
if rr not in valid_reasons:
|
||||
return Response({'error': 'Invalid reject_reason'}, status=400)
|
||||
review.status = 'rejected'
|
||||
review.reject_reason = rr
|
||||
elif action == 'save_and_approve':
|
||||
review.review_text = request.data.get('review_text', review.review_text)
|
||||
review.status = 'live'
|
||||
elif action == 'save_live':
|
||||
review.review_text = request.data.get('review_text', review.review_text)
|
||||
else:
|
||||
return Response({'error': 'Invalid action'}, status=400)
|
||||
review.save()
|
||||
return Response(_serialize_review(review))
|
||||
|
||||
|
||||
class ReviewDeleteView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def delete(self, request, pk):
|
||||
from admin_api.models import Review
|
||||
from django.shortcuts import get_object_or_404
|
||||
review = get_object_or_404(Review, pk=pk)
|
||||
review.delete()
|
||||
return Response(status=204)
|
||||
|
||||
Reference in New Issue
Block a user