Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 33 additions & 49 deletions admin_console/models.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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()
Expand Down
5 changes: 4 additions & 1 deletion admin_console/serializers.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
128 changes: 123 additions & 5 deletions admin_console/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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



Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand All @@ -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"})
Expand All @@ -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"})
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion hackathon/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
3 changes: 1 addition & 2 deletions hackathon/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = []

Expand Down
2 changes: 1 addition & 1 deletion team/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions vortexis_backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
'PASSWORD': config('PGPASSWORD'),
'HOST': config('PGHOST'),
'PORT': config('DB_PORT', default='5432'),
}
},
}


Expand Down Expand Up @@ -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': {
Expand Down