From fdec4158b1dc93759e118f4cf39c135bc80e39ca Mon Sep 17 00:00:00 2001 From: Mubarakah Adio Date: Tue, 31 Mar 2026 15:52:29 +0100 Subject: [PATCH 1/3] Add created_at, submission_deadline, venue, description, project, and team fields with migrations --- admin_console/models.py | 340 +++++++++++++++++++++++++++++++++-- hackathon/serializers.py | 4 +- hackathon/views.py | 4 +- organization/serializers.py | 2 +- organization/views.py | 2 +- project/serializers.py | 2 +- project/views.py | 2 +- team/serializers.py | 8 +- team/views.py | 4 +- vortexis_backend/settings.py | 2 +- 10 files changed, 343 insertions(+), 27 deletions(-) diff --git a/admin_console/models.py b/admin_console/models.py index 73bff21..d27cd21 100644 --- a/admin_console/models.py +++ b/admin_console/models.py @@ -1,7 +1,10 @@ from django.db import models import pyotp -from django.db import models from django.contrib.auth.models import AbstractUser, Group, Permission +from django.core.validators import MinValueValidator, MaxValueValidator, URLValidator +from django.utils import timezone +from datetime import timedelta +import secrets # Custom user with roles class User(AbstractUser): @@ -32,25 +35,338 @@ def verify_totp(self, token): return totp.verify(token) +class Skill(models.Model): + name = models.CharField(max_length=50, null=False, blank=False) + + def __str__(self): + return self.name + + +class Organization(models.Model): + name = models.CharField(max_length=128, unique=True) + description = models.TextField(default="No description provided.") + website = models.URLField(max_length=200, blank=True, null=True, validators=[URLValidator()]) + logo = models.URLField(max_length=500, blank=True, null=True) + location = models.CharField(max_length=32, blank=True, null=True) + tagline = models.CharField(max_length=200, blank=True, null=True) + about = models.TextField(max_length=5000, blank=True, null=True) + organizer = models.ForeignKey(User, related_name='organizations', on_delete=models.SET_NULL, null=True) + moderators = models.ManyToManyField(User, related_name='moderating_organization', blank=True) + is_approved = models.BooleanField(default=False) + created_at = models.DateTimeField(default=timezone.now) + updated_at = models.DateTimeField(auto_now=True) + + def clean(self): + # (No additional field-specific validation required at the moment) + pass + + class Meta: + indexes = [ + models.Index(fields=['-created_at'], name='admin_org_created_idx'), + models.Index(fields=['is_approved', '-created_at'], name='admin_org_approved_idx'), + models.Index(fields=['organizer', '-created_at'], name='admin_org_organizer_idx'), + ] + ordering = ['-created_at'] + + def save(self, *args, **kwargs): + self.full_clean() + super().save(*args, **kwargs) + + def __str__(self): + return self.name + + 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') + title = models.CharField(max_length=100, null=False, blank=False) + description = models.TextField(null=False, blank=False) + banner_image = models.URLField(max_length=500, null=True, blank=True) + venue = models.CharField(max_length=100, default="Default Venue Name") + details = models.TextField(null=True, blank=True) + skills = models.ManyToManyField('Skill', related_name='hackathons', blank=True) + themes = models.ManyToManyField('Theme', related_name='hackathons', blank=True) + grand_prize = models.IntegerField('grand prize', null=False, default=0) + start_date = models.DateField(null=False, blank=False) + visibility = models.BooleanField(default=False, null=False) + end_date = models.DateField(null=False, blank=False) + submission_deadline = models.DateTimeField(default=timezone.now() + timedelta(days=7)) + judges = models.ManyToManyField(User, related_name='judged_hackathons', blank=True) + min_team_size = models.IntegerField('minimum team size', null=False, default=1, validators=[MinValueValidator(1), MaxValueValidator(100)]) + max_team_size = models.IntegerField('maximum team size', null=False, default=5, validators=[MinValueValidator(1), MaxValueValidator(100)]) + organization = models.ForeignKey(Organization, related_name='hackathons', null=True, on_delete=models.SET_NULL) + created_at = models.DateTimeField(default=timezone.now) + updated_at = models.DateTimeField(auto_now=True) + rules = models.TextField(blank=True, help_text="Enter hackathon rules (one per line or as formatted text)") + prizes = models.TextField(blank=True, help_text="Enter prize information (one per line or as formatted text)") + evaluation_criteria = models.TextField(blank=True, help_text="Evaluation criteria for judges (only visible to judges and organizers)") + + def __str__(self): + return self.title + + @property + def participants(self): + return self.teams.all() + + +class Theme(models.Model): + name = models.CharField(max_length=50, null=False, blank=False) + description = models.TextField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name 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')]) + STATUS_CHOICES = [ + ('pending', 'Pending'), + ('reviewed', 'Reviewed'), + ('rejected', 'Rejected'), + ] + + project = models.ForeignKey('Project', related_name='submission', on_delete=models.CASCADE, null=True, blank=True) + hackathon = models.ForeignKey(Hackathon, related_name='submissions', on_delete=models.CASCADE) + team = models.ForeignKey('Team', related_name='submissions', on_delete=models.CASCADE, null=True, blank=True) + approved = models.BooleanField(default=False) + status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='pending') 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_sub_created_idx'), + models.Index(fields=['hackathon', '-created_at'], name='admin_sub_hackathon_idx'), + models.Index(fields=['team', '-created_at'], name='admin_sub_team_idx'), + models.Index(fields=['status', '-created_at'], name='admin_sub_status_idx'), + ] + ordering = ['-created_at'] -class Organization(models.Model): - name = models.CharField(max_length=200) + def __str__(self): + return self.project.title + + +class Review(models.Model): + submission = models.ForeignKey(Submission, related_name='reviews', on_delete=models.CASCADE) + judge = models.ForeignKey(User, related_name='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'] + + def __str__(self): + return f"{self.judge.username}'s review for {self.submission.project.title}" + + +class HackathonParticipant(models.Model): + hackathon = models.ForeignKey(Hackathon, related_name='individual_participants', on_delete=models.CASCADE) + user = models.ForeignKey(User, related_name='hackathon_participations', on_delete=models.CASCADE) + team = models.ForeignKey('Team', related_name='hackathon_participants', on_delete=models.SET_NULL, null=True, blank=True) + looking_for_team = models.BooleanField(default=True) + skills_offered = models.ManyToManyField('Skill', related_name='participant_offerings', blank=True) + bio = models.TextField(max_length=500, blank=True, help_text="Brief bio to help with team matching") + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = ['hackathon', 'user'] + verbose_name = 'Hackathon Participant' + verbose_name_plural = 'Hackathon Participants' + + def __str__(self): + return f"{self.user.username} in {self.hackathon.title}" + + @property + def has_team(self): + return self.team is not None + + +class JudgeInvitation(models.Model): + hackathon = models.ForeignKey(Hackathon, related_name='judge_invitations', on_delete=models.CASCADE) email = models.EmailField() - status = models.CharField(max_length=10, choices=[('active','Active'),('inactive','Inactive')], default='active') + token = models.CharField(max_length=100, unique=True) + invited_by = models.ForeignKey(User, related_name='sent_judge_invitations', on_delete=models.CASCADE) + is_accepted = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() + accepted_at = models.DateTimeField(null=True, blank=True) + + class Meta: + unique_together = ['hackathon', 'email'] + verbose_name = 'Judge Invitation' + verbose_name_plural = 'Judge Invitations' + + def save(self, *args, **kwargs): + if not self.token: + self.token = secrets.token_urlsafe(32) + if not self.expires_at: + self.expires_at = timezone.now() + timedelta(days=7) # 7 days to accept + super().save(*args, **kwargs) + + def is_expired(self): + return timezone.now() > self.expires_at + + def is_valid(self): + return not self.is_accepted and not self.is_expired() + + def __str__(self): + return f"Judge invitation for {self.email} to {self.hackathon.title}" + + +class Project(models.Model): + title = models.CharField(max_length=100, null=False, blank=False) + description = models.TextField(null=False, blank=False) + github_url = models.URLField("github url", blank=False) + demo_video_url = models.URLField("Project video link", blank=True) + live_link = models.URLField("Project live link", blank=True) + presentation_link = models.URLField("Project presentation link", blank=True) + team = models.ForeignKey('Team', related_name='projects', null=True, on_delete=models.SET_NULL) + hackathon = models.ForeignKey(Hackathon, related_name='projects', null=False, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = [('team', 'hackathon')] + indexes = [ + models.Index(fields=['-created_at'], name='admin_proj_created_idx'), + models.Index(fields=['hackathon', '-created_at'], name='admin_proj_hackathon_idx'), + models.Index(fields=['team', '-created_at'], name='admin_proj_team_idx'), + ] + ordering = ['-created_at'] + + def __str__(self): + return self.title + + +class ModeratorInvitation(models.Model): + PENDING = 'pending' + ACCEPTED = 'accepted' + DECLINED = 'declined' + EXPIRED = 'expired' + + STATUS_CHOICES = [ + (PENDING, 'Pending'), + (ACCEPTED, 'Accepted'), + (DECLINED, 'Declined'), + (EXPIRED, 'Expired'), + ] + + organization = models.ForeignKey(Organization, related_name='moderator_invitations', on_delete=models.CASCADE) + inviter = models.ForeignKey(User, related_name='sent_moderator_invitations', on_delete=models.CASCADE) + email = models.EmailField() + invitee = models.ForeignKey(User, related_name='received_moderator_invitations', on_delete=models.SET_NULL, null=True, blank=True) + token = models.CharField(max_length=100, unique=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=PENDING) + message = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() + responded_at = models.DateTimeField(null=True, blank=True) + + class Meta: + unique_together = ['organization', 'email'] + ordering = ['-created_at'] + + def save(self, *args, **kwargs): + if not self.token: + self.token = secrets.token_urlsafe(32) + if not self.expires_at: + self.expires_at = timezone.now() + timedelta(days=7) + super().save(*args, **kwargs) + + def is_expired(self): + return timezone.now() > self.expires_at + + def is_valid(self): + return self.status == self.PENDING and not self.is_expired() + + def expire(self): + if self.is_expired() and self.status == self.PENDING: + self.status = self.EXPIRED + self.save() + + def __str__(self): + return f"Moderator invitation for {self.email} to {self.organization.name}" + + +class Team(models.Model): + name = models.CharField(max_length=50, null=False, blank=False) + members = models.ManyToManyField(User, related_name='teams') + organizer = models.ForeignKey(User, related_name='organized_teams', null=True, on_delete=models.SET_NULL) + hackathon = models.ForeignKey(Hackathon, related_name='teams', null=False, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = [('name', 'hackathon'), ('organizer', 'hackathon')] + indexes = [ + models.Index(fields=['-created_at'], name='admin_team_created_idx'), + models.Index(fields=['hackathon', '-created_at'], name='admin_team_hackathon_idx'), + models.Index(fields=['organizer', '-created_at'], name='admin_team_organizer_idx'), + ] + ordering = ['-created_at'] + + def __str__(self): + return f"{self.name} - {self.hackathon.title}" + + def get_projects(self): + return self.projects.all() + + def get_submissions(self): + return self.submissions.all() + + def get_prizes(self): + # Return empty queryset since there's no Prize model related to Team + from django.db.models import QuerySet + return QuerySet().none() + + +class TeamInvitation(models.Model): + team = models.ForeignKey(Team, related_name='invitations', on_delete=models.CASCADE) + email = models.EmailField() + invited_by = models.ForeignKey(User, related_name='sent_team_invitations', on_delete=models.CASCADE) + token = models.CharField(max_length=64, unique=True) + is_accepted = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + accepted_at = models.DateTimeField(null=True, blank=True) + + class Meta: + unique_together = [('team', 'email')] + + def save(self, *args, **kwargs): + if not self.token: + self.token = secrets.token_urlsafe(32) + super().save(*args, **kwargs) + + def __str__(self): + return f"Invitation to {self.team.name} for {self.email}" + + +class TeamJoinRequest(models.Model): + team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="join_requests") + user = models.ForeignKey(User, on_delete=models.CASCADE) + status = models.CharField(max_length=10, choices=[ + ('pending', 'Pending'), + ('approved', 'Approved'), + ('rejected', 'Rejected') + ], + default='pending' + ) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('team', 'user') class AuditLog(models.Model): diff --git a/hackathon/serializers.py b/hackathon/serializers.py index 333c310..0fb249e 100644 --- a/hackathon/serializers.py +++ b/hackathon/serializers.py @@ -1,8 +1,8 @@ from rest_framework import serializers 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 admin_console.models import Team +from admin_console.models import Hackathon, Theme, Submission, Review, HackathonParticipant from accounts.models import User diff --git a/hackathon/views.py b/hackathon/views.py index 10d767c..ef5b045 100644 --- a/hackathon/views.py +++ b/hackathon/views.py @@ -14,7 +14,7 @@ from team.models import Team from team.serializers import TeamSerializer -from .models import Hackathon, Theme, Submission, Review, HackathonParticipant +from admin_console.models import Hackathon, Theme, Submission, Review, HackathonParticipant from .serializers import ( HackathonSerializer, CreateHackathonSerializer, SubmitProjectSerializer, UpdateHackathonSerializer, RegisterHackathonSerializer, ThemeSerializer, @@ -254,7 +254,7 @@ def post(self, request, hackathon_id): emails = serializer.validated_data['emails'] # Create judge invitations for all emails - from .models import JudgeInvitation + from admin_console.models import JudgeInvitation from accounts.utils import send_judge_invitation_email successful_invitations = [] diff --git a/organization/serializers.py b/organization/serializers.py index 4f62d30..bdb8358 100644 --- a/organization/serializers.py +++ b/organization/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Organization, ModeratorInvitation +from admin_console.models import Organization, ModeratorInvitation from accounts.models import User from notifications.services import NotificationService from django.utils import timezone diff --git a/organization/views.py b/organization/views.py index 21af352..48fcd9c 100644 --- a/organization/views.py +++ b/organization/views.py @@ -12,7 +12,7 @@ ModeratorInvitationSerializer, CreateModeratorInvitationSerializer, AcceptInvitationSerializer, DeclineInvitationSerializer ) -from .models import Organization, ModeratorInvitation +from admin_console.models import Organization, ModeratorInvitation from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi diff --git a/project/serializers.py b/project/serializers.py index 61f1139..4d5cb18 100644 --- a/project/serializers.py +++ b/project/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from rest_framework.exceptions import AuthenticationFailed from team.models import Team -from .models import Project +from admin_console.models import Project from hackathon.models import Submission diff --git a/project/views.py b/project/views.py index e319604..2b761e3 100644 --- a/project/views.py +++ b/project/views.py @@ -8,7 +8,7 @@ from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from .serializers import CreateProjectSerializer, ProjectSerializer, UpdateProjectSerializer -from .models import Project +from admin_console.models import Project from django.shortcuts import get_object_or_404 from rest_framework.permissions import AllowAny diff --git a/team/serializers.py b/team/serializers.py index 1a45d4e..acdf52e 100644 --- a/team/serializers.py +++ b/team/serializers.py @@ -5,7 +5,7 @@ from notifications.services import NotificationService from accounts.models import User -from .models import Team, TeamInvitation, TeamJoinRequest +from admin_console.models import Team, TeamInvitation, TeamJoinRequest class CreateTeamSerializer(serializers.ModelSerializer): @@ -321,7 +321,7 @@ class AddMemberSerializer(serializers.Serializer): def validate_member_email(self, value): from hackathon.models import HackathonParticipant - from .models import TeamInvitation + from admin_console.models import TeamInvitation request = self.context.get('request') if not request: @@ -371,7 +371,7 @@ def validate_member_email(self, value): def save(self): from django.core.mail import send_mail from django.conf import settings - from .models import TeamInvitation + from admin_console.models import TeamInvitation email = self.validated_data['member_email'] team = self.instance @@ -585,7 +585,7 @@ class AcceptTeamInvitationSerializer(serializers.Serializer): token = serializers.CharField() def validate_token(self, value): - from .models import TeamInvitation + from admin_console.models import TeamInvitation try: invitation = TeamInvitation.objects.get(token=value) diff --git a/team/views.py b/team/views.py index 5c9a474..d874fcf 100644 --- a/team/views.py +++ b/team/views.py @@ -9,10 +9,10 @@ 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 +from admin_console.models import Team, TeamJoinRequest from django.shortcuts import get_object_or_404 from notifications.services import NotificationService diff --git a/vortexis_backend/settings.py b/vortexis_backend/settings.py index 0474ca0..92564d5 100644 --- a/vortexis_backend/settings.py +++ b/vortexis_backend/settings.py @@ -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': { From af89acdbddb04b6438832f3aa4132a869bffe01e Mon Sep 17 00:00:00 2001 From: Mubarakah Adio Date: Thu, 2 Apr 2026 01:35:03 +0100 Subject: [PATCH 2/3] Implement admin oversight endpoints: submission oversight, hackathon management, organization management, and user management using existing database tables --- admin_console/models.py | 354 ++--------------------------------- admin_console/serializers.py | 5 +- admin_console/views.py | 128 ++++++++++++- hackathon/serializers.py | 5 +- hackathon/views.py | 2 +- organization/serializers.py | 2 +- organization/views.py | 2 +- project/serializers.py | 2 +- project/views.py | 2 +- team/serializers.py | 8 +- team/views.py | 2 +- vortexis_backend/settings.py | 2 +- 12 files changed, 152 insertions(+), 362 deletions(-) diff --git a/admin_console/models.py b/admin_console/models.py index d27cd21..ed2cf67 100644 --- a/admin_console/models.py +++ b/admin_console/models.py @@ -1,153 +1,18 @@ from django.db import models -import pyotp -from django.contrib.auth.models import AbstractUser, Group, Permission -from django.core.validators import MinValueValidator, MaxValueValidator, URLValidator +from django.core.validators import MinValueValidator, MaxValueValidator from django.utils import timezone -from datetime import timedelta import secrets - -# 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 Skill(models.Model): - name = models.CharField(max_length=50, null=False, blank=False) - - def __str__(self): - return self.name - - -class Organization(models.Model): - name = models.CharField(max_length=128, unique=True) - description = models.TextField(default="No description provided.") - website = models.URLField(max_length=200, blank=True, null=True, validators=[URLValidator()]) - logo = models.URLField(max_length=500, blank=True, null=True) - location = models.CharField(max_length=32, blank=True, null=True) - tagline = models.CharField(max_length=200, blank=True, null=True) - about = models.TextField(max_length=5000, blank=True, null=True) - organizer = models.ForeignKey(User, related_name='organizations', on_delete=models.SET_NULL, null=True) - moderators = models.ManyToManyField(User, related_name='moderating_organization', blank=True) - is_approved = models.BooleanField(default=False) - created_at = models.DateTimeField(default=timezone.now) - updated_at = models.DateTimeField(auto_now=True) - - def clean(self): - # (No additional field-specific validation required at the moment) - pass - - class Meta: - indexes = [ - models.Index(fields=['-created_at'], name='admin_org_created_idx'), - models.Index(fields=['is_approved', '-created_at'], name='admin_org_approved_idx'), - models.Index(fields=['organizer', '-created_at'], name='admin_org_organizer_idx'), - ] - ordering = ['-created_at'] - - def save(self, *args, **kwargs): - self.full_clean() - super().save(*args, **kwargs) - - def __str__(self): - return self.name - - -class Hackathon(models.Model): - title = models.CharField(max_length=100, null=False, blank=False) - description = models.TextField(null=False, blank=False) - banner_image = models.URLField(max_length=500, null=True, blank=True) - venue = models.CharField(max_length=100, default="Default Venue Name") - details = models.TextField(null=True, blank=True) - skills = models.ManyToManyField('Skill', related_name='hackathons', blank=True) - themes = models.ManyToManyField('Theme', related_name='hackathons', blank=True) - grand_prize = models.IntegerField('grand prize', null=False, default=0) - start_date = models.DateField(null=False, blank=False) - visibility = models.BooleanField(default=False, null=False) - end_date = models.DateField(null=False, blank=False) - submission_deadline = models.DateTimeField(default=timezone.now() + timedelta(days=7)) - judges = models.ManyToManyField(User, related_name='judged_hackathons', blank=True) - min_team_size = models.IntegerField('minimum team size', null=False, default=1, validators=[MinValueValidator(1), MaxValueValidator(100)]) - max_team_size = models.IntegerField('maximum team size', null=False, default=5, validators=[MinValueValidator(1), MaxValueValidator(100)]) - organization = models.ForeignKey(Organization, related_name='hackathons', null=True, on_delete=models.SET_NULL) - created_at = models.DateTimeField(default=timezone.now) - updated_at = models.DateTimeField(auto_now=True) - rules = models.TextField(blank=True, help_text="Enter hackathon rules (one per line or as formatted text)") - prizes = models.TextField(blank=True, help_text="Enter prize information (one per line or as formatted text)") - evaluation_criteria = models.TextField(blank=True, help_text="Evaluation criteria for judges (only visible to judges and organizers)") - - def __str__(self): - return self.title - - @property - def participants(self): - return self.teams.all() - - -class Theme(models.Model): - name = models.CharField(max_length=50, null=False, blank=False) - description = models.TextField(null=True, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return self.name - - -class Submission(models.Model): - STATUS_CHOICES = [ - ('pending', 'Pending'), - ('reviewed', 'Reviewed'), - ('rejected', 'Rejected'), - ] - - project = models.ForeignKey('Project', related_name='submission', on_delete=models.CASCADE, null=True, blank=True) - hackathon = models.ForeignKey(Hackathon, related_name='submissions', on_delete=models.CASCADE) - team = models.ForeignKey('Team', related_name='submissions', on_delete=models.CASCADE, null=True, blank=True) - approved = models.BooleanField(default=False) - status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='pending') - 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_sub_created_idx'), - models.Index(fields=['hackathon', '-created_at'], name='admin_sub_hackathon_idx'), - models.Index(fields=['team', '-created_at'], name='admin_sub_team_idx'), - models.Index(fields=['status', '-created_at'], name='admin_sub_status_idx'), - ] - ordering = ['-created_at'] - - def __str__(self): - return self.project.title +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='reviews', on_delete=models.CASCADE) - judge = models.ForeignKey(User, related_name='reviews', on_delete=models.CASCADE) + 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)]) @@ -170,205 +35,7 @@ def __str__(self): return f"{self.judge.username}'s review for {self.submission.project.title}" -class HackathonParticipant(models.Model): - hackathon = models.ForeignKey(Hackathon, related_name='individual_participants', on_delete=models.CASCADE) - user = models.ForeignKey(User, related_name='hackathon_participations', on_delete=models.CASCADE) - team = models.ForeignKey('Team', related_name='hackathon_participants', on_delete=models.SET_NULL, null=True, blank=True) - looking_for_team = models.BooleanField(default=True) - skills_offered = models.ManyToManyField('Skill', related_name='participant_offerings', blank=True) - bio = models.TextField(max_length=500, blank=True, help_text="Brief bio to help with team matching") - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - unique_together = ['hackathon', 'user'] - verbose_name = 'Hackathon Participant' - verbose_name_plural = 'Hackathon Participants' - - def __str__(self): - return f"{self.user.username} in {self.hackathon.title}" - - @property - def has_team(self): - return self.team is not None - - -class JudgeInvitation(models.Model): - hackathon = models.ForeignKey(Hackathon, related_name='judge_invitations', on_delete=models.CASCADE) - email = models.EmailField() - token = models.CharField(max_length=100, unique=True) - invited_by = models.ForeignKey(User, related_name='sent_judge_invitations', on_delete=models.CASCADE) - is_accepted = models.BooleanField(default=False) - created_at = models.DateTimeField(auto_now_add=True) - expires_at = models.DateTimeField() - accepted_at = models.DateTimeField(null=True, blank=True) - - class Meta: - unique_together = ['hackathon', 'email'] - verbose_name = 'Judge Invitation' - verbose_name_plural = 'Judge Invitations' - - def save(self, *args, **kwargs): - if not self.token: - self.token = secrets.token_urlsafe(32) - if not self.expires_at: - self.expires_at = timezone.now() + timedelta(days=7) # 7 days to accept - super().save(*args, **kwargs) - - def is_expired(self): - return timezone.now() > self.expires_at - - def is_valid(self): - return not self.is_accepted and not self.is_expired() - - def __str__(self): - return f"Judge invitation for {self.email} to {self.hackathon.title}" - - -class Project(models.Model): - title = models.CharField(max_length=100, null=False, blank=False) - description = models.TextField(null=False, blank=False) - github_url = models.URLField("github url", blank=False) - demo_video_url = models.URLField("Project video link", blank=True) - live_link = models.URLField("Project live link", blank=True) - presentation_link = models.URLField("Project presentation link", blank=True) - team = models.ForeignKey('Team', related_name='projects', null=True, on_delete=models.SET_NULL) - hackathon = models.ForeignKey(Hackathon, related_name='projects', null=False, on_delete=models.CASCADE) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - unique_together = [('team', 'hackathon')] - indexes = [ - models.Index(fields=['-created_at'], name='admin_proj_created_idx'), - models.Index(fields=['hackathon', '-created_at'], name='admin_proj_hackathon_idx'), - models.Index(fields=['team', '-created_at'], name='admin_proj_team_idx'), - ] - ordering = ['-created_at'] - - def __str__(self): - return self.title - - -class ModeratorInvitation(models.Model): - PENDING = 'pending' - ACCEPTED = 'accepted' - DECLINED = 'declined' - EXPIRED = 'expired' - - STATUS_CHOICES = [ - (PENDING, 'Pending'), - (ACCEPTED, 'Accepted'), - (DECLINED, 'Declined'), - (EXPIRED, 'Expired'), - ] - - organization = models.ForeignKey(Organization, related_name='moderator_invitations', on_delete=models.CASCADE) - inviter = models.ForeignKey(User, related_name='sent_moderator_invitations', on_delete=models.CASCADE) - email = models.EmailField() - invitee = models.ForeignKey(User, related_name='received_moderator_invitations', on_delete=models.SET_NULL, null=True, blank=True) - token = models.CharField(max_length=100, unique=True) - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=PENDING) - message = models.TextField(blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True) - expires_at = models.DateTimeField() - responded_at = models.DateTimeField(null=True, blank=True) - - class Meta: - unique_together = ['organization', 'email'] - ordering = ['-created_at'] - - def save(self, *args, **kwargs): - if not self.token: - self.token = secrets.token_urlsafe(32) - if not self.expires_at: - self.expires_at = timezone.now() + timedelta(days=7) - super().save(*args, **kwargs) - - def is_expired(self): - return timezone.now() > self.expires_at - - def is_valid(self): - return self.status == self.PENDING and not self.is_expired() - - def expire(self): - if self.is_expired() and self.status == self.PENDING: - self.status = self.EXPIRED - self.save() - - def __str__(self): - return f"Moderator invitation for {self.email} to {self.organization.name}" - - -class Team(models.Model): - name = models.CharField(max_length=50, null=False, blank=False) - members = models.ManyToManyField(User, related_name='teams') - organizer = models.ForeignKey(User, related_name='organized_teams', null=True, on_delete=models.SET_NULL) - hackathon = models.ForeignKey(Hackathon, related_name='teams', null=False, on_delete=models.CASCADE) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - unique_together = [('name', 'hackathon'), ('organizer', 'hackathon')] - indexes = [ - models.Index(fields=['-created_at'], name='admin_team_created_idx'), - models.Index(fields=['hackathon', '-created_at'], name='admin_team_hackathon_idx'), - models.Index(fields=['organizer', '-created_at'], name='admin_team_organizer_idx'), - ] - ordering = ['-created_at'] - - def __str__(self): - return f"{self.name} - {self.hackathon.title}" - - def get_projects(self): - return self.projects.all() - - def get_submissions(self): - return self.submissions.all() - - def get_prizes(self): - # Return empty queryset since there's no Prize model related to Team - from django.db.models import QuerySet - return QuerySet().none() - - -class TeamInvitation(models.Model): - team = models.ForeignKey(Team, related_name='invitations', on_delete=models.CASCADE) - email = models.EmailField() - invited_by = models.ForeignKey(User, related_name='sent_team_invitations', on_delete=models.CASCADE) - token = models.CharField(max_length=64, unique=True) - is_accepted = models.BooleanField(default=False) - created_at = models.DateTimeField(auto_now_add=True) - accepted_at = models.DateTimeField(null=True, blank=True) - - class Meta: - unique_together = [('team', 'email')] - - def save(self, *args, **kwargs): - if not self.token: - self.token = secrets.token_urlsafe(32) - super().save(*args, **kwargs) - - def __str__(self): - return f"Invitation to {self.team.name} for {self.email}" - - -class TeamJoinRequest(models.Model): - team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="join_requests") - user = models.ForeignKey(User, on_delete=models.CASCADE) - status = models.CharField(max_length=10, choices=[ - ('pending', 'Pending'), - ('approved', 'Approved'), - ('rejected', 'Rejected') - ], - default='pending' - ) - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - unique_together = ('team', 'user') - - +# 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) @@ -377,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 0fb249e..abea874 100644 --- a/hackathon/serializers.py +++ b/hackathon/serializers.py @@ -1,8 +1,9 @@ from rest_framework import serializers from rest_framework.exceptions import AuthenticationFailed, ValidationError from django.utils import timezone -from admin_console.models import Team -from admin_console.models import Hackathon, Theme, Submission, Review, HackathonParticipant +from team.models import Team +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 ef5b045..24241e5 100644 --- a/hackathon/views.py +++ b/hackathon/views.py @@ -256,7 +256,7 @@ def post(self, request, hackathon_id): # Create judge invitations for all emails from admin_console.models import JudgeInvitation from accounts.utils import send_judge_invitation_email - + successful_invitations = [] failed_invitations = [] diff --git a/organization/serializers.py b/organization/serializers.py index bdb8358..4f62d30 100644 --- a/organization/serializers.py +++ b/organization/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from admin_console.models import Organization, ModeratorInvitation +from .models import Organization, ModeratorInvitation from accounts.models import User from notifications.services import NotificationService from django.utils import timezone diff --git a/organization/views.py b/organization/views.py index 48fcd9c..21af352 100644 --- a/organization/views.py +++ b/organization/views.py @@ -12,7 +12,7 @@ ModeratorInvitationSerializer, CreateModeratorInvitationSerializer, AcceptInvitationSerializer, DeclineInvitationSerializer ) -from admin_console.models import Organization, ModeratorInvitation +from .models import Organization, ModeratorInvitation from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi diff --git a/project/serializers.py b/project/serializers.py index 4d5cb18..61f1139 100644 --- a/project/serializers.py +++ b/project/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from rest_framework.exceptions import AuthenticationFailed from team.models import Team -from admin_console.models import Project +from .models import Project from hackathon.models import Submission diff --git a/project/views.py b/project/views.py index 2b761e3..e319604 100644 --- a/project/views.py +++ b/project/views.py @@ -8,7 +8,7 @@ from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from .serializers import CreateProjectSerializer, ProjectSerializer, UpdateProjectSerializer -from admin_console.models import Project +from .models import Project from django.shortcuts import get_object_or_404 from rest_framework.permissions import AllowAny diff --git a/team/serializers.py b/team/serializers.py index acdf52e..1a45d4e 100644 --- a/team/serializers.py +++ b/team/serializers.py @@ -5,7 +5,7 @@ from notifications.services import NotificationService from accounts.models import User -from admin_console.models import Team, TeamInvitation, TeamJoinRequest +from .models import Team, TeamInvitation, TeamJoinRequest class CreateTeamSerializer(serializers.ModelSerializer): @@ -321,7 +321,7 @@ class AddMemberSerializer(serializers.Serializer): def validate_member_email(self, value): from hackathon.models import HackathonParticipant - from admin_console.models import TeamInvitation + from .models import TeamInvitation request = self.context.get('request') if not request: @@ -371,7 +371,7 @@ def validate_member_email(self, value): def save(self): from django.core.mail import send_mail from django.conf import settings - from admin_console.models import TeamInvitation + from .models import TeamInvitation email = self.validated_data['member_email'] team = self.instance @@ -585,7 +585,7 @@ class AcceptTeamInvitationSerializer(serializers.Serializer): token = serializers.CharField() def validate_token(self, value): - from admin_console.models import TeamInvitation + from .models import TeamInvitation try: invitation = TeamInvitation.objects.get(token=value) diff --git a/team/views.py b/team/views.py index d874fcf..42b11c4 100644 --- a/team/views.py +++ b/team/views.py @@ -12,7 +12,7 @@ from admin_console.models import Team from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi -from admin_console.models import Team, TeamJoinRequest +from .models import Team, TeamJoinRequest from django.shortcuts import get_object_or_404 from notifications.services import NotificationService diff --git a/vortexis_backend/settings.py b/vortexis_backend/settings.py index 92564d5..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'), - } + }, } From 835ff14fb5e60c0f97ce5202743247b7cad0eb6f Mon Sep 17 00:00:00 2001 From: Mubarakah Adio Date: Thu, 2 Apr 2026 06:03:12 +0100 Subject: [PATCH 3/3] fix import --- hackathon/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hackathon/views.py b/hackathon/views.py index 24241e5..55eeda6 100644 --- a/hackathon/views.py +++ b/hackathon/views.py @@ -11,10 +11,9 @@ 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 admin_console.models import Hackathon, Theme, Submission, Review, HackathonParticipant +from .models import Hackathon, Theme, Submission, Review, HackathonParticipant from .serializers import ( HackathonSerializer, CreateHackathonSerializer, SubmitProjectSerializer, UpdateHackathonSerializer, RegisterHackathonSerializer, ThemeSerializer, @@ -254,7 +253,7 @@ def post(self, request, hackathon_id): emails = serializer.validated_data['emails'] # Create judge invitations for all emails - from admin_console.models import JudgeInvitation + from .models import JudgeInvitation from accounts.utils import send_judge_invitation_email successful_invitations = []