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/transactions/', views.TransactionListView.as_view(), name='transaction-list'),
|
||||||
path('financials/settlements/', views.SettlementListView.as_view(), name='settlement-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('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.payment_transaction_status = 'completed'
|
||||||
p.save(update_fields=['payment_transaction_status'])
|
p.save(update_fields=['payment_transaction_status'])
|
||||||
return Response(_serialize_settlement(p))
|
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