diff --git a/admin_console/models.py b/admin_console/models.py index 73bff21..ed2cf67 100644 --- a/admin_console/models.py +++ b/admin_console/models.py @@ -1,58 +1,41 @@ from django.db import models -import pyotp -from django.db import models -from django.contrib.auth.models import AbstractUser, Group, Permission - -# Custom user with roles -class User(AbstractUser): - ROLE_CHOICES = [ - ('PlatformOwner','Platform Owner'), - ('SystemAdmin','System Admin'), - ('Organizer','Organizer'), - ('Judge','Judge'), - ('Participant','Participant') - ] - role = models.CharField(max_length=20, choices=ROLE_CHOICES) - status = models.CharField(max_length=10, choices=[('active','Active'),('inactive','Inactive')], default='active') - - # 2FA secret (stored securely) - totp_secret = models.CharField(max_length=16, blank=True, null=True) - - groups = models.ManyToManyField(Group, related_name='admin_console_user_set', blank=True) - user_permissions = models.ManyToManyField(Permission, related_name='admin_console_user_permission_set', blank=True) - - def generate_totp_secret(self): - self.totp_secret = pyotp.random_base32() - self.save() - - def verify_totp(self, token): - if not self.totp_secret: - return False - totp = pyotp.TOTP(self.totp_secret) - return totp.verify(token) - - -class Hackathon(models.Model): - title = models.CharField(max_length=200) - description = models.TextField() - start_date = models.DateField() - end_date = models.DateField() - status = models.CharField(max_length=20, choices=[('active','Active'),('inactive','Inactive')], default='active') - - -class Submission(models.Model): - hackathon = models.ForeignKey(Hackathon, on_delete=models.CASCADE) - participant = models.ForeignKey(User, on_delete=models.CASCADE) - status = models.CharField(max_length=20, choices=[('pending','Pending'),('approved','Approved'),('rejected','Rejected')]) +from django.core.validators import MinValueValidator, MaxValueValidator +from django.utils import timezone +import secrets +from accounts.models import User +from hackathon.models import Hackathon, Submission, Theme +from organization.models import Organization +from team.models import Team +from project.models import Project + + +# Admin-specific Review model for submission scoring +class Review(models.Model): + submission = models.ForeignKey(Submission, related_name='admin_reviews', on_delete=models.CASCADE) + judge = models.ForeignKey(User, related_name='admin_reviews', on_delete=models.CASCADE) + innovation_score = models.IntegerField(null=False, blank=False, default=0, validators=[MinValueValidator(0), MaxValueValidator(10)]) + technical_score = models.IntegerField(null=False, blank=False, default=0, validators=[MinValueValidator(0), MaxValueValidator(10)]) + user_experience_score = models.IntegerField(null=False, blank=False, default=0, validators=[MinValueValidator(0), MaxValueValidator(10)]) + impact_score = models.IntegerField(null=False, blank=False, default=0, validators=[MinValueValidator(0), MaxValueValidator(10)]) + presentation_score = models.IntegerField(null=False, blank=False, default=0, validators=[MinValueValidator(0), MaxValueValidator(10)]) + overall_score = models.IntegerField(null=False, blank=False, default=0, validators=[MinValueValidator(0), MaxValueValidator(10)]) + review = models.TextField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + class Meta: + indexes = [ + models.Index(fields=['-created_at'], name='admin_rev_created_idx'), + models.Index(fields=['judge', '-created_at'], name='admin_rev_judge_idx'), + models.Index(fields=['submission', '-created_at'], name='admin_rev_submission_idx'), + ] + ordering = ['-created_at'] -class Organization(models.Model): - name = models.CharField(max_length=200) - email = models.EmailField() - status = models.CharField(max_length=10, choices=[('active','Active'),('inactive','Inactive')], default='active') + def __str__(self): + return f"{self.judge.username}'s review for {self.submission.project.title}" +# Audit log model for tracking admin actions class AuditLog(models.Model): admin = models.ForeignKey(User, on_delete=models.CASCADE) action = models.CharField(max_length=200) @@ -61,6 +44,7 @@ class AuditLog(models.Model): timestamp = models.DateTimeField(auto_now_add=True) +# Platform-wide settings for admins class PlatformSetting(models.Model): key = models.CharField(max_length=100, unique=True) value = models.TextField() diff --git a/admin_console/serializers.py b/admin_console/serializers.py index 69f8509..c6e76cb 100644 --- a/admin_console/serializers.py +++ b/admin_console/serializers.py @@ -1,5 +1,8 @@ from rest_framework import serializers -from .models import User, Hackathon, Submission, Organization, AuditLog, PlatformSetting +from accounts.models import User +from hackathon.models import Hackathon, Submission +from organization.models import Organization +from .models import AuditLog, PlatformSetting class UserSerializer(serializers.ModelSerializer): class Meta: diff --git a/admin_console/views.py b/admin_console/views.py index 3f439d6..403f250 100644 --- a/admin_console/views.py +++ b/admin_console/views.py @@ -10,7 +10,10 @@ from django.utils import timezone from django.utils.dateparse import parse_datetime from django.db import models -from .models import User, Hackathon, Submission, Organization, AuditLog, PlatformSetting +from accounts.models import User +from hackathon.models import Hackathon, Submission +from organization.models import Organization +from .models import Review, AuditLog, PlatformSetting from .serializers import ( UserSerializer, HackathonSerializer, SubmissionSerializer, AdminOrganizationSerializer, AuditLogSerializer, PlatformSettingSerializer @@ -19,8 +22,7 @@ from .throttles import AdminRateThrottle from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi -from django.db.models import Count -from admin_console.models import User, Hackathon, Submission, Organization +from django.db.models import Count, Avg @@ -175,8 +177,49 @@ def get_queryset(self): queryset = queryset.filter(start_date__gte=start_date) if end_date: queryset = queryset.filter(end_date__lte=end_date) + + is_approved = self.request.query_params.get('is_approved') + is_suspended = self.request.query_params.get('is_suspended') + if is_approved is not None: + queryset = queryset.filter(is_approved=is_approved.lower() == 'true') + if is_suspended is not None: + queryset = queryset.filter(is_suspended=is_suspended.lower() == 'true') + return queryset.order_by("-start_date") + @action(detail=True, methods=['patch']) + def approve(self, request, pk=None): + hackathon = self.get_object() + hackathon.is_approved = True + hackathon.is_suspended = False + hackathon.save() + self.log_action('APPROVE_HACKATHON', target_id=pk) + return Response({'message': 'Hackathon approved'}) + + @action(detail=True, methods=['patch']) + def reject(self, request, pk=None): + hackathon = self.get_object() + hackathon.is_approved = False + hackathon.save() + self.log_action('REJECT_HACKATHON', target_id=pk) + return Response({'message': 'Hackathon rejected'}) + + @action(detail=True, methods=['patch']) + def suspend(self, request, pk=None): + hackathon = self.get_object() + hackathon.is_suspended = True + hackathon.save() + self.log_action('SUSPEND_HACKATHON', target_id=pk) + return Response({'message': 'Hackathon suspended'}) + + @action(detail=True, methods=['patch']) + def restore(self, request, pk=None): + hackathon = self.get_object() + hackathon.is_suspended = False + hackathon.save() + self.log_action('RESTORE_HACKATHON', target_id=pk) + return Response({'message': 'Hackathon restored'}) + def log_action(self, action, target_id=None): AuditLog.objects.create( admin=getattr(self.request, 'user', None), @@ -255,6 +298,38 @@ def get_queryset(self): return queryset + @action(detail=False, methods=['get'], url_path='score-overview') + def score_overview(self, request): + hackathon_id = request.query_params.get('hackathon_id') + submissions = Submission.objects.all() + if hackathon_id: + submissions = submissions.filter(hackathon_id=hackathon_id) + + score_stats = submissions.aggregate( + total_submissions=Count('id'), + reviewed_submissions=Count('id', filter=models.Q(status='reviewed')), + approved_submissions=Count('id', filter=models.Q(status='approved')), + rejected_submissions=Count('id', filter=models.Q(status='rejected')), + average_overall_score=Avg('reviews__overall_score'), + average_technical_score=Avg('reviews__technical_score'), + average_innovation_score=Avg('reviews__innovation_score') + ) + + submission_scores = submissions.annotate( + avg_overall=Avg('reviews__overall_score'), + avg_technical=Avg('reviews__technical_score'), + avg_innovation=Avg('reviews__innovation_score'), + review_count=Count('reviews') + ).values( + 'id', 'project__title', 'hackathon__title', 'status', 'approved', + 'avg_overall', 'avg_technical', 'avg_innovation', 'review_count' + ) + + return Response({ + 'score_stats': score_stats, + 'submissions': list(submission_scores) + }) + def log_action(self, action, target_id=None): AuditLog.objects.create( admin=getattr(self.request, 'user', None), @@ -268,6 +343,7 @@ def log_action(self, action, target_id=None): def approve(self, request, pk=None): submission = self.get_object() submission.status = "approved" + submission.approved = True submission.save() self.log_action("APPROVE_SUBMISSION", target_id=pk) return Response({"message": "Submission approved"}) @@ -276,6 +352,7 @@ def approve(self, request, pk=None): def reject(self, request, pk=None): submission = self.get_object() submission.status = "rejected" + submission.approved = False submission.save() self.log_action("REJECT_SUBMISSION", target_id=pk) return Response({"message": "Submission rejected"}) @@ -322,9 +399,48 @@ def get_queryset(self): queryset = queryset.filter(name__icontains=name) if is_active is not None: queryset = queryset.filter(is_active=is_active.lower() == 'true') + is_approved = self.request.query_params.get('is_approved') + is_suspended = self.request.query_params.get('is_suspended') + if is_approved is not None: + queryset = queryset.filter(is_approved=is_approved.lower() == 'true') + if is_suspended is not None: + queryset = queryset.filter(is_suspended=is_suspended.lower() == 'true') return queryset + @action(detail=True, methods=['patch']) + def approve(self, request, pk=None): + organization = self.get_object() + organization.is_approved = True + organization.is_suspended = False + organization.save() + self.log_action('APPROVE_ORGANIZATION', target_id=pk) + return Response({'message': 'Organization approved'}) + + @action(detail=True, methods=['patch']) + def reject(self, request, pk=None): + organization = self.get_object() + organization.is_approved = False + organization.save() + self.log_action('REJECT_ORGANIZATION', target_id=pk) + return Response({'message': 'Organization rejected'}) + + @action(detail=True, methods=['patch']) + def suspend(self, request, pk=None): + organization = self.get_object() + organization.is_suspended = True + organization.save() + self.log_action('SUSPEND_ORGANIZATION', target_id=pk) + return Response({'message': 'Organization suspended'}) + + @action(detail=True, methods=['patch']) + def restore(self, request, pk=None): + organization = self.get_object() + organization.is_suspended = False + organization.save() + self.log_action('RESTORE_ORGANIZATION', target_id=pk) + return Response({'message': 'Organization restored'}) + def log_action(self, action, target_id=None): AuditLog.objects.create( admin=getattr(self.request, 'user', None), @@ -354,8 +470,10 @@ def destroy(self, request, *args, **kwargs): from rest_framework.response import Response from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi -from django.db.models import Count -from admin_console.models import User, Hackathon, Submission, Organization +from django.db.models import Count, Avg +from accounts.models import User +from hackathon.models import Hackathon, Submission +from organization.models import Organization class AnalyticsView(APIView): permission_classes = [IsAdminUser] diff --git a/hackathon/serializers.py b/hackathon/serializers.py index 333c310..abea874 100644 --- a/hackathon/serializers.py +++ b/hackathon/serializers.py @@ -2,7 +2,8 @@ from rest_framework.exceptions import AuthenticationFailed, ValidationError from django.utils import timezone from team.models import Team -from .models import Hackathon, Theme, Submission, Review, HackathonParticipant +from .models import Hackathon, Theme, Submission, HackathonParticipant +from admin_console.models import Review from accounts.models import User diff --git a/hackathon/views.py b/hackathon/views.py index 10d767c..55eeda6 100644 --- a/hackathon/views.py +++ b/hackathon/views.py @@ -11,7 +11,6 @@ from rest_framework.views import APIView from drf_yasg import openapi from notifications.services import NotificationService - from team.models import Team from team.serializers import TeamSerializer from .models import Hackathon, Theme, Submission, Review, HackathonParticipant @@ -256,7 +255,7 @@ def post(self, request, hackathon_id): # Create judge invitations for all emails from .models import JudgeInvitation from accounts.utils import send_judge_invitation_email - + successful_invitations = [] failed_invitations = [] diff --git a/team/views.py b/team/views.py index 5c9a474..42b11c4 100644 --- a/team/views.py +++ b/team/views.py @@ -9,7 +9,7 @@ from django.conf import settings from notifications.services import NotificationService from .serializers import CreateTeamSerializer, TeamSerializer, UpdateTeamSerializer, AddMemberSerializer, RemoveMemberSerializer, LeaveTeamSerializer, AcceptTeamInvitationSerializer, TeamInvitationSerializer, TeamJoinRequestSerializer -from .models import Team +from admin_console.models import Team from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from .models import Team, TeamJoinRequest diff --git a/vortexis_backend/settings.py b/vortexis_backend/settings.py index 0474ca0..f6eb6a3 100644 --- a/vortexis_backend/settings.py +++ b/vortexis_backend/settings.py @@ -123,7 +123,7 @@ 'PASSWORD': config('PGPASSWORD'), 'HOST': config('PGHOST'), 'PORT': config('DB_PORT', default='5432'), - } + }, } @@ -280,7 +280,7 @@ def build_redis_url(host, port, db=0): } # Add password to channels config if set if REDIS_PASSWORD_SET: - CHANNEL_LAYERS_CONFIG["password"] = REDIS_PASSWORD + CHANNEL_LAYERS_CONFIG["password"] = REDIS_PASSWORD CHANNEL_LAYERS = { 'default': {