Skip to content

Commit d5f425c

Browse files
authored
Merge pull request #39 from Adeoti-bliss/master
Add created_at, submission_deadline, venue, description, project, and…
2 parents 8e6dc33 + 835ff14 commit d5f425c

7 files changed

Lines changed: 166 additions & 61 deletions

File tree

admin_console/models.py

Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,41 @@
11
from django.db import models
2-
import pyotp
3-
from django.db import models
4-
from django.contrib.auth.models import AbstractUser, Group, Permission
5-
6-
# Custom user with roles
7-
class User(AbstractUser):
8-
ROLE_CHOICES = [
9-
('PlatformOwner','Platform Owner'),
10-
('SystemAdmin','System Admin'),
11-
('Organizer','Organizer'),
12-
('Judge','Judge'),
13-
('Participant','Participant')
14-
]
15-
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
16-
status = models.CharField(max_length=10, choices=[('active','Active'),('inactive','Inactive')], default='active')
17-
18-
# 2FA secret (stored securely)
19-
totp_secret = models.CharField(max_length=16, blank=True, null=True)
20-
21-
groups = models.ManyToManyField(Group, related_name='admin_console_user_set', blank=True)
22-
user_permissions = models.ManyToManyField(Permission, related_name='admin_console_user_permission_set', blank=True)
23-
24-
def generate_totp_secret(self):
25-
self.totp_secret = pyotp.random_base32()
26-
self.save()
27-
28-
def verify_totp(self, token):
29-
if not self.totp_secret:
30-
return False
31-
totp = pyotp.TOTP(self.totp_secret)
32-
return totp.verify(token)
33-
34-
35-
class Hackathon(models.Model):
36-
title = models.CharField(max_length=200)
37-
description = models.TextField()
38-
start_date = models.DateField()
39-
end_date = models.DateField()
40-
status = models.CharField(max_length=20, choices=[('active','Active'),('inactive','Inactive')], default='active')
41-
42-
43-
class Submission(models.Model):
44-
hackathon = models.ForeignKey(Hackathon, on_delete=models.CASCADE)
45-
participant = models.ForeignKey(User, on_delete=models.CASCADE)
46-
status = models.CharField(max_length=20, choices=[('pending','Pending'),('approved','Approved'),('rejected','Rejected')])
2+
from django.core.validators import MinValueValidator, MaxValueValidator
3+
from django.utils import timezone
4+
import secrets
5+
from accounts.models import User
6+
from hackathon.models import Hackathon, Submission, Theme
7+
from organization.models import Organization
8+
from team.models import Team
9+
from project.models import Project
10+
11+
12+
# Admin-specific Review model for submission scoring
13+
class Review(models.Model):
14+
submission = models.ForeignKey(Submission, related_name='admin_reviews', on_delete=models.CASCADE)
15+
judge = models.ForeignKey(User, related_name='admin_reviews', on_delete=models.CASCADE)
16+
innovation_score = models.IntegerField(null=False, blank=False, default=0, validators=[MinValueValidator(0), MaxValueValidator(10)])
17+
technical_score = models.IntegerField(null=False, blank=False, default=0, validators=[MinValueValidator(0), MaxValueValidator(10)])
18+
user_experience_score = models.IntegerField(null=False, blank=False, default=0, validators=[MinValueValidator(0), MaxValueValidator(10)])
19+
impact_score = models.IntegerField(null=False, blank=False, default=0, validators=[MinValueValidator(0), MaxValueValidator(10)])
20+
presentation_score = models.IntegerField(null=False, blank=False, default=0, validators=[MinValueValidator(0), MaxValueValidator(10)])
21+
overall_score = models.IntegerField(null=False, blank=False, default=0, validators=[MinValueValidator(0), MaxValueValidator(10)])
22+
review = models.TextField(null=True, blank=True)
4723
created_at = models.DateTimeField(auto_now_add=True)
24+
updated_at = models.DateTimeField(auto_now=True)
4825

26+
class Meta:
27+
indexes = [
28+
models.Index(fields=['-created_at'], name='admin_rev_created_idx'),
29+
models.Index(fields=['judge', '-created_at'], name='admin_rev_judge_idx'),
30+
models.Index(fields=['submission', '-created_at'], name='admin_rev_submission_idx'),
31+
]
32+
ordering = ['-created_at']
4933

50-
class Organization(models.Model):
51-
name = models.CharField(max_length=200)
52-
email = models.EmailField()
53-
status = models.CharField(max_length=10, choices=[('active','Active'),('inactive','Inactive')], default='active')
34+
def __str__(self):
35+
return f"{self.judge.username}'s review for {self.submission.project.title}"
5436

5537

38+
# Audit log model for tracking admin actions
5639
class AuditLog(models.Model):
5740
admin = models.ForeignKey(User, on_delete=models.CASCADE)
5841
action = models.CharField(max_length=200)
@@ -61,6 +44,7 @@ class AuditLog(models.Model):
6144
timestamp = models.DateTimeField(auto_now_add=True)
6245

6346

47+
# Platform-wide settings for admins
6448
class PlatformSetting(models.Model):
6549
key = models.CharField(max_length=100, unique=True)
6650
value = models.TextField()

admin_console/serializers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from rest_framework import serializers
2-
from .models import User, Hackathon, Submission, Organization, AuditLog, PlatformSetting
2+
from accounts.models import User
3+
from hackathon.models import Hackathon, Submission
4+
from organization.models import Organization
5+
from .models import AuditLog, PlatformSetting
36

47
class UserSerializer(serializers.ModelSerializer):
58
class Meta:

admin_console/views.py

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
from django.utils import timezone
1111
from django.utils.dateparse import parse_datetime
1212
from django.db import models
13-
from .models import User, Hackathon, Submission, Organization, AuditLog, PlatformSetting
13+
from accounts.models import User
14+
from hackathon.models import Hackathon, Submission
15+
from organization.models import Organization
16+
from .models import Review, AuditLog, PlatformSetting
1417
from .serializers import (
1518
UserSerializer, HackathonSerializer, SubmissionSerializer,
1619
AdminOrganizationSerializer, AuditLogSerializer, PlatformSettingSerializer
@@ -19,8 +22,7 @@
1922
from .throttles import AdminRateThrottle
2023
from drf_yasg.utils import swagger_auto_schema
2124
from drf_yasg import openapi
22-
from django.db.models import Count
23-
from admin_console.models import User, Hackathon, Submission, Organization
25+
from django.db.models import Count, Avg
2426

2527

2628

@@ -175,8 +177,49 @@ def get_queryset(self):
175177
queryset = queryset.filter(start_date__gte=start_date)
176178
if end_date:
177179
queryset = queryset.filter(end_date__lte=end_date)
180+
181+
is_approved = self.request.query_params.get('is_approved')
182+
is_suspended = self.request.query_params.get('is_suspended')
183+
if is_approved is not None:
184+
queryset = queryset.filter(is_approved=is_approved.lower() == 'true')
185+
if is_suspended is not None:
186+
queryset = queryset.filter(is_suspended=is_suspended.lower() == 'true')
187+
178188
return queryset.order_by("-start_date")
179189

190+
@action(detail=True, methods=['patch'])
191+
def approve(self, request, pk=None):
192+
hackathon = self.get_object()
193+
hackathon.is_approved = True
194+
hackathon.is_suspended = False
195+
hackathon.save()
196+
self.log_action('APPROVE_HACKATHON', target_id=pk)
197+
return Response({'message': 'Hackathon approved'})
198+
199+
@action(detail=True, methods=['patch'])
200+
def reject(self, request, pk=None):
201+
hackathon = self.get_object()
202+
hackathon.is_approved = False
203+
hackathon.save()
204+
self.log_action('REJECT_HACKATHON', target_id=pk)
205+
return Response({'message': 'Hackathon rejected'})
206+
207+
@action(detail=True, methods=['patch'])
208+
def suspend(self, request, pk=None):
209+
hackathon = self.get_object()
210+
hackathon.is_suspended = True
211+
hackathon.save()
212+
self.log_action('SUSPEND_HACKATHON', target_id=pk)
213+
return Response({'message': 'Hackathon suspended'})
214+
215+
@action(detail=True, methods=['patch'])
216+
def restore(self, request, pk=None):
217+
hackathon = self.get_object()
218+
hackathon.is_suspended = False
219+
hackathon.save()
220+
self.log_action('RESTORE_HACKATHON', target_id=pk)
221+
return Response({'message': 'Hackathon restored'})
222+
180223
def log_action(self, action, target_id=None):
181224
AuditLog.objects.create(
182225
admin=getattr(self.request, 'user', None),
@@ -255,6 +298,38 @@ def get_queryset(self):
255298

256299
return queryset
257300

301+
@action(detail=False, methods=['get'], url_path='score-overview')
302+
def score_overview(self, request):
303+
hackathon_id = request.query_params.get('hackathon_id')
304+
submissions = Submission.objects.all()
305+
if hackathon_id:
306+
submissions = submissions.filter(hackathon_id=hackathon_id)
307+
308+
score_stats = submissions.aggregate(
309+
total_submissions=Count('id'),
310+
reviewed_submissions=Count('id', filter=models.Q(status='reviewed')),
311+
approved_submissions=Count('id', filter=models.Q(status='approved')),
312+
rejected_submissions=Count('id', filter=models.Q(status='rejected')),
313+
average_overall_score=Avg('reviews__overall_score'),
314+
average_technical_score=Avg('reviews__technical_score'),
315+
average_innovation_score=Avg('reviews__innovation_score')
316+
)
317+
318+
submission_scores = submissions.annotate(
319+
avg_overall=Avg('reviews__overall_score'),
320+
avg_technical=Avg('reviews__technical_score'),
321+
avg_innovation=Avg('reviews__innovation_score'),
322+
review_count=Count('reviews')
323+
).values(
324+
'id', 'project__title', 'hackathon__title', 'status', 'approved',
325+
'avg_overall', 'avg_technical', 'avg_innovation', 'review_count'
326+
)
327+
328+
return Response({
329+
'score_stats': score_stats,
330+
'submissions': list(submission_scores)
331+
})
332+
258333
def log_action(self, action, target_id=None):
259334
AuditLog.objects.create(
260335
admin=getattr(self.request, 'user', None),
@@ -268,6 +343,7 @@ def log_action(self, action, target_id=None):
268343
def approve(self, request, pk=None):
269344
submission = self.get_object()
270345
submission.status = "approved"
346+
submission.approved = True
271347
submission.save()
272348
self.log_action("APPROVE_SUBMISSION", target_id=pk)
273349
return Response({"message": "Submission approved"})
@@ -276,6 +352,7 @@ def approve(self, request, pk=None):
276352
def reject(self, request, pk=None):
277353
submission = self.get_object()
278354
submission.status = "rejected"
355+
submission.approved = False
279356
submission.save()
280357
self.log_action("REJECT_SUBMISSION", target_id=pk)
281358
return Response({"message": "Submission rejected"})
@@ -322,9 +399,48 @@ def get_queryset(self):
322399
queryset = queryset.filter(name__icontains=name)
323400
if is_active is not None:
324401
queryset = queryset.filter(is_active=is_active.lower() == 'true')
402+
is_approved = self.request.query_params.get('is_approved')
403+
is_suspended = self.request.query_params.get('is_suspended')
404+
if is_approved is not None:
405+
queryset = queryset.filter(is_approved=is_approved.lower() == 'true')
406+
if is_suspended is not None:
407+
queryset = queryset.filter(is_suspended=is_suspended.lower() == 'true')
325408

326409
return queryset
327410

411+
@action(detail=True, methods=['patch'])
412+
def approve(self, request, pk=None):
413+
organization = self.get_object()
414+
organization.is_approved = True
415+
organization.is_suspended = False
416+
organization.save()
417+
self.log_action('APPROVE_ORGANIZATION', target_id=pk)
418+
return Response({'message': 'Organization approved'})
419+
420+
@action(detail=True, methods=['patch'])
421+
def reject(self, request, pk=None):
422+
organization = self.get_object()
423+
organization.is_approved = False
424+
organization.save()
425+
self.log_action('REJECT_ORGANIZATION', target_id=pk)
426+
return Response({'message': 'Organization rejected'})
427+
428+
@action(detail=True, methods=['patch'])
429+
def suspend(self, request, pk=None):
430+
organization = self.get_object()
431+
organization.is_suspended = True
432+
organization.save()
433+
self.log_action('SUSPEND_ORGANIZATION', target_id=pk)
434+
return Response({'message': 'Organization suspended'})
435+
436+
@action(detail=True, methods=['patch'])
437+
def restore(self, request, pk=None):
438+
organization = self.get_object()
439+
organization.is_suspended = False
440+
organization.save()
441+
self.log_action('RESTORE_ORGANIZATION', target_id=pk)
442+
return Response({'message': 'Organization restored'})
443+
328444
def log_action(self, action, target_id=None):
329445
AuditLog.objects.create(
330446
admin=getattr(self.request, 'user', None),
@@ -354,8 +470,10 @@ def destroy(self, request, *args, **kwargs):
354470
from rest_framework.response import Response
355471
from drf_yasg.utils import swagger_auto_schema
356472
from drf_yasg import openapi
357-
from django.db.models import Count
358-
from admin_console.models import User, Hackathon, Submission, Organization
473+
from django.db.models import Count, Avg
474+
from accounts.models import User
475+
from hackathon.models import Hackathon, Submission
476+
from organization.models import Organization
359477

360478
class AnalyticsView(APIView):
361479
permission_classes = [IsAdminUser]

hackathon/serializers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from rest_framework.exceptions import AuthenticationFailed, ValidationError
33
from django.utils import timezone
44
from team.models import Team
5-
from .models import Hackathon, Theme, Submission, Review, HackathonParticipant
5+
from .models import Hackathon, Theme, Submission, HackathonParticipant
6+
from admin_console.models import Review
67
from accounts.models import User
78

89

hackathon/views.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from rest_framework.views import APIView
1212
from drf_yasg import openapi
1313
from notifications.services import NotificationService
14-
1514
from team.models import Team
1615
from team.serializers import TeamSerializer
1716
from .models import Hackathon, Theme, Submission, Review, HackathonParticipant
@@ -256,7 +255,7 @@ def post(self, request, hackathon_id):
256255
# Create judge invitations for all emails
257256
from .models import JudgeInvitation
258257
from accounts.utils import send_judge_invitation_email
259-
258+
260259
successful_invitations = []
261260
failed_invitations = []
262261

team/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from django.conf import settings
1010
from notifications.services import NotificationService
1111
from .serializers import CreateTeamSerializer, TeamSerializer, UpdateTeamSerializer, AddMemberSerializer, RemoveMemberSerializer, LeaveTeamSerializer, AcceptTeamInvitationSerializer, TeamInvitationSerializer, TeamJoinRequestSerializer
12-
from .models import Team
12+
from admin_console.models import Team
1313
from drf_yasg.utils import swagger_auto_schema
1414
from drf_yasg import openapi
1515
from .models import Team, TeamJoinRequest

vortexis_backend/settings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
'PASSWORD': config('PGPASSWORD'),
124124
'HOST': config('PGHOST'),
125125
'PORT': config('DB_PORT', default='5432'),
126-
}
126+
},
127127
}
128128

129129

@@ -280,7 +280,7 @@ def build_redis_url(host, port, db=0):
280280
}
281281
# Add password to channels config if set
282282
if REDIS_PASSWORD_SET:
283-
CHANNEL_LAYERS_CONFIG["password"] = REDIS_PASSWORD
283+
CHANNEL_LAYERS_CONFIG["password"] = REDIS_PASSWORD
284284

285285
CHANNEL_LAYERS = {
286286
'default': {

0 commit comments

Comments
 (0)