Phase 7: Reviews Moderation — Review model + migration + 4 admin endpoints (metrics, list, moderate, delete)

This commit is contained in:
2026-03-25 02:46:50 +00:00
parent 3103eff949
commit 54315408eb
5 changed files with 207 additions and 1 deletions

View 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')],
},
),
]

View File

46
admin_api/models.py Normal file
View 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}'

View File

@@ -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'),
]

View File

@@ -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)