diff --git a/FusionIIIT/Fusion/settings/common.py b/FusionIIIT/Fusion/settings/common.py index bc97f1548..8d1148cf6 100644 --- a/FusionIIIT/Fusion/settings/common.py +++ b/FusionIIIT/Fusion/settings/common.py @@ -117,6 +117,7 @@ 'applications.library', 'applications.notifications_extension', 'applications.gymkhana', + 'applications.gymkhana_v1', 'applications.office_module', 'applications.globals', 'applications.central_mess', @@ -288,4 +289,4 @@ # session settings SESSION_COOKIE_AGE = 15 * 60 SESSION_EXPIRE_AT_BROWSER_CLOSE = True -SESSION_SAVE_EVERY_REQUEST = True \ No newline at end of file +SESSION_SAVE_EVERY_REQUEST = True diff --git a/FusionIIIT/Fusion/urls.py b/FusionIIIT/Fusion/urls.py index e3b3f6792..c888953be 100755 --- a/FusionIIIT/Fusion/urls.py +++ b/FusionIIIT/Fusion/urls.py @@ -54,7 +54,7 @@ url(r'^office/', include('applications.office_module.urls')), url(r'^finance/', include('applications.finance_accounts.urls')), url(r'^purchase-and-store/', include('applications.ps1.urls')), - url(r'^gymkhana/', include('applications.gymkhana.urls')), + url(r'^gymkhana/', include('applications.gymkhana_v1.urls')), url(r'^library/', include('applications.library.urls')), url(r'^establishment/', include('applications.establishment.urls')), url(r'^ocms/', include('applications.online_cms.urls')), diff --git a/FusionIIIT/applications/__init__.py b/FusionIIIT/applications/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/FusionIIIT/applications/__init__.py @@ -0,0 +1 @@ + diff --git a/FusionIIIT/applications/central_mess/views.py b/FusionIIIT/applications/central_mess/views.py index 60a4abb3b..fbac2cd42 100644 --- a/FusionIIIT/applications/central_mess/views.py +++ b/FusionIIIT/applications/central_mess/views.py @@ -29,10 +29,7 @@ from notification.views import central_mess_notif import csv -import openpyxl - - -today_g = datetime.datetime.now() +today_g = datetime.datetime.now() month_g = today_g.month month_g_l = today_g.strftime('%B') year_g = today_g.year @@ -47,7 +44,7 @@ previous_month = last_day_prev_month.strftime('%B') @login_required -def mess(request): +def mess(request): """ This view get the access to the central mess dashboard. View all details and apply for any changes. It also shows the previous feedback submitted by the user. @@ -1450,21 +1447,22 @@ def searchAddOrRemoveStudent(request): return JsonResponse({'message':msg}) else: - if(request.FILES): + if(request.FILES): # if 'excelUpload1' in request.POST: # messNo='mess1' # excel_file = request.FILES['excel_file1'] # else: # messNo='mess2' # excel_file = request.FILES['excel_file2'] - try: - latest = Semdates.objects.latest('end_date') - latest_end_date=latest.end_date - print(latest_end_date) - except: - latest_end_date=None - excel_file = request.FILES['excel_file1'] - wb = openpyxl.load_workbook(excel_file) + try: + latest = Semdates.objects.latest('end_date') + latest_end_date=latest.end_date + print(latest_end_date) + except: + latest_end_date=None + import openpyxl + excel_file = request.FILES['excel_file1'] + wb = openpyxl.load_workbook(excel_file) flag = False for row in wb.active: if(flag==False): @@ -1492,14 +1490,14 @@ def searchAddOrRemoveStudent(request): new_reg_record = Reg_records(student_id=reg_main.student_id,start_date=today_g,end_date=latest_end_date) new_reg_record.save() # messages.success(request,"Done.") - return HttpResponseRedirect("/mess") - -@csrf_exempt -def uploadPaymentDue(request): - if(request.FILES): - - excel_file = request.FILES['excel_file'] - wb = openpyxl.load_workbook(excel_file) + return HttpResponseRedirect("/mess") + +@csrf_exempt +def uploadPaymentDue(request): + if(request.FILES): + import openpyxl + excel_file = request.FILES['excel_file'] + wb = openpyxl.load_workbook(excel_file) for row in wb.active: studentId=(str(row[0].value)).upper() @@ -1619,11 +1617,12 @@ def update_payment(request): temp.save() return HttpResponseRedirect("/mess") -@csrf_exempt -def update_bill_excel(request): - if(request.FILES): - excel_file = request.FILES['excel_file_bill'] - wb = openpyxl.load_workbook(excel_file) +@csrf_exempt +def update_bill_excel(request): + if(request.FILES): + import openpyxl + excel_file = request.FILES['excel_file_bill'] + wb = openpyxl.load_workbook(excel_file) flag = False for row in wb.active: if(flag==False): diff --git a/FusionIIIT/applications/counselling_cell/views.py b/FusionIIIT/applications/counselling_cell/views.py index 42a33f0ef..e2c7c5827 100644 --- a/FusionIIIT/applications/counselling_cell/views.py +++ b/FusionIIIT/applications/counselling_cell/views.py @@ -7,7 +7,6 @@ from applications.academic_information.models import Student import django. utils. timezone as timezone from collections import defaultdict -import openpyxl from .models import ( @@ -194,6 +193,7 @@ def assign_student_to_sg(request): # third_year_students = Student.objects.filter(batch=year-3) if request.method == 'POST' and request.FILES: + import openpyxl profiles=request.FILES['mappedStudent'] # excel = xlrd.open_workbook(file_contents=profiles.read()) wb_obj = openpyxl.load_workbook(profiles) diff --git a/FusionIIIT/applications/examination/api/views.py b/FusionIIIT/applications/examination/api/views.py index e5d9bda31..a7ab39fbb 100644 --- a/FusionIIIT/applications/examination/api/views.py +++ b/FusionIIIT/applications/examination/api/views.py @@ -21,9 +21,6 @@ from django.db.models import IntegerField from django.db.models.functions import Cast from rest_framework.parsers import MultiPartParser, FormParser -from openpyxl import Workbook -from openpyxl.utils import get_column_letter -from openpyxl.styles import Alignment, Font, PatternFill, Border, Side import traceback from applications.academic_information.models import Course from reportlab.lib import colors @@ -36,6 +33,14 @@ from collections import defaultdict from django.db.models import Case, When, IntegerField + +def _load_openpyxl_exports(): + from openpyxl import Workbook + from openpyxl.styles import Alignment, Font, PatternFill, Border, Side + from openpyxl.utils import get_column_letter + + return Workbook, Alignment, Font, PatternFill, Border, Side, get_column_letter + grade_conversion = { "O": 1.0, "A+": 1.0, "A": 0.9, "B+": 0.8, "B": 0.7, "C+": 0.6, "C": 0.5, "D+": 0.4, "D": 0.3, "F": 0.2, "S": 0.0, @@ -1285,6 +1290,7 @@ def post(self, request): courses = Courses.objects.filter(id__in=course_ids) courses_map = {course.id: course.credit for course in courses} + Workbook, Alignment, Font, PatternFill, Border, Side, get_column_letter = _load_openpyxl_exports() wb = Workbook() ws = wb.active diff --git a/FusionIIIT/applications/examination/views.py b/FusionIIIT/applications/examination/views.py index 101c98d9b..f9f931af9 100644 --- a/FusionIIIT/applications/examination/views.py +++ b/FusionIIIT/applications/examination/views.py @@ -7,9 +7,6 @@ from django.contrib.auth import get_user_model import csv import json -from openpyxl import Workbook -from openpyxl.styles import Alignment, Font -from openpyxl.utils import get_column_letter from io import BytesIO,StringIO from django.db.models import IntegerField from django.db.models.functions import Cast @@ -38,6 +35,14 @@ from applications.academic_information.models import Course from applications.academic_procedures.models import course_registration, Register,Semester from applications.programme_curriculum.filters import CourseFilter + + +def _load_openpyxl_exports(): + from openpyxl import Workbook + from openpyxl.styles import Alignment, Font + from openpyxl.utils import get_column_letter + + return Workbook, Alignment, Font, get_column_letter from notification.views import examination_notif from applications.department.models import SpecialRequest, Announcements from applications.globals.models import ( @@ -1887,6 +1892,7 @@ def checkresult(request): def grades_report(request): if request.method == 'POST': + Workbook, Alignment, Font, get_column_letter = _load_openpyxl_exports() des = request.session.get("currentDesignationSelected") if des == "student": pass diff --git a/FusionIIIT/applications/globals/api/views.py b/FusionIIIT/applications/globals/api/views.py index 50b969321..e16b848c3 100644 --- a/FusionIIIT/applications/globals/api/views.py +++ b/FusionIIIT/applications/globals/api/views.py @@ -2,7 +2,6 @@ from applications.academic_information.models import Student from applications.eis.api.views import profile as eis_profile from applications.globals.models import (HoldsDesignation,Designation) -from applications.gymkhana.api.views import coordinator_club from applications.placement_cell.models import (Achievement, Course, Education, Experience, Has, Patent, Project, Publication, Skill) @@ -447,4 +446,4 @@ def admin_delete_course_proxy(request, course_id): 'success': False, 'message': 'An unexpected error occurred while deleting the course.', 'error': str(e) - }, status=500) \ No newline at end of file + }, status=500) diff --git a/FusionIIIT/applications/gymkhana/admin.py b/FusionIIIT/applications/gymkhana/admin.py index 0139a3865..11a213def 100644 --- a/FusionIIIT/applications/gymkhana/admin.py +++ b/FusionIIIT/applications/gymkhana/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import (Club_budget,Club_info,Club_member,Club_report,Fest_budget,Fest,Other_report,Session_info,Event_info,Registration_form,Form_available,Inventory,Budget,Budget_Comments,Event_Comments) +from .models import (Club_budget,Club_info,Club_member,Club_report,Core_team,Fest_budget,Other_report,Session_info,Voting_choices,Voting_polls,Voting_voters,Event_info,Registration_form,Form_available) # Register your models here. @@ -15,20 +15,15 @@ class ClubMemberAdmin(admin.ModelAdmin): admin.site.register(Club_info, ClubInfoAdmin) admin.site.register(Club_member, ClubMemberAdmin) -# admin.site.register(Core_team) +admin.site.register(Core_team) admin.site.register(Club_budget) admin.site.register(Session_info) admin.site.register(Event_info) admin.site.register(Club_report) admin.site.register(Fest_budget) admin.site.register(Other_report) -# admin.site.register(Voting_polls) -# admin.site.register(Voting_choices) -# admin.site.register(Voting_voters) +admin.site.register(Voting_polls) +admin.site.register(Voting_choices) +admin.site.register(Voting_voters) admin.site.register(Registration_form) admin.site.register(Form_available) -admin.site.register(Inventory) -admin.site.register(Budget) -admin.site.register(Budget_Comments) -admin.site.register(Event_Comments) -admin.site.register(Fest) \ No newline at end of file diff --git a/FusionIIIT/applications/gymkhana/api/serializers.py b/FusionIIIT/applications/gymkhana/api/serializers.py index 070722c95..43061bd94 100644 --- a/FusionIIIT/applications/gymkhana/api/serializers.py +++ b/FusionIIIT/applications/gymkhana/api/serializers.py @@ -1,148 +1,127 @@ -from attr import fields -from django.contrib.auth import get_user_model -from rest_framework.authtoken.models import Token from rest_framework import serializers -from applications.gymkhana.models import Club_info,Session_info,Event_info -from applications.gymkhana.models import Club_member,Club_budget,Club_report,Fest_budget,Fest,Registration_form,Budget,Budget_Comments,Event_Comments,Achievements,ClubPosition, EventInput, EventReport, YearlyPlan, YearlyPlanEvents -# class Voting_choicesSerializer(serializers.ModelSerializer): -# class Meta: -# model = Voting_choices -# fields = ['poll_event', 'title', 'description', 'votes'] +from applications.gymkhana.models import ( + ClubCategory, + Club_info, + Club_member, + Event_info, + Session_info, + Venue, +) -class Club_infoSerializer(serializers.ModelSerializer): +class ClubSerializer(serializers.ModelSerializer): + co_ordinator = serializers.CharField(source="co_ordinator.id.id", read_only=True) + co_coordinator = serializers.CharField(source="co_coordinator.id.id", read_only=True) + faculty_incharge = serializers.CharField(source="faculty_incharge.id.id", read_only=True) class Meta: model = Club_info - fields = ['club_name', 'category', 'co_ordinator', 'co_coordinator', 'faculty_incharge', 'club_file', 'activity_calender', 'description', 'alloted_budget', 'spent_budget', 'avail_budget', 'status', 'head_changed_on', 'created_on'] + fields = ( + "club_name", + "club_website", + "category", + "co_ordinator", + "co_coordinator", + "faculty_incharge", + "description", + "alloted_budget", + "spent_budget", + "avail_budget", + "status", + "head_changed_on", + "created_on", + ) + + +class ClubCreateSerializer(serializers.Serializer): + club_name = serializers.CharField(max_length=50) + category = serializers.ChoiceField(choices=ClubCategory.choices) + co_ordinator = serializers.CharField(max_length=20) + co_coordinator = serializers.CharField(max_length=20) + faculty_incharge = serializers.CharField(max_length=256) + description = serializers.CharField(required=False, allow_blank=True, allow_null=True) + + +class ClubMemberSerializer(serializers.ModelSerializer): + member_id = serializers.CharField(source="member.id.id", read_only=True) + club_name = serializers.CharField(source="club.club_name", read_only=True) - - -class EmptySerializer(serializers.Serializer): - pass - -class Club_memberSerializer(serializers.ModelSerializer): class Meta: model = Club_member - fields = ['member','club','description', 'status','remarks','id'] - - -# class Core_teamSerializer(serializers.ModelSerializer): - -# class Meta: -# model=Core_team -# fields=('all') - -class Club_DetailsSerializer(serializers.ModelSerializer): - class Meta: - model=Club_info - fields=['club_name',"co_ordinator","co_coordinator","activity_calender","category",'faculty_incharge',"club_file", "status" ,"description"] + fields = ("id", "member_id", "club_name", "description", "status", "remarks") -class Session_infoSerializer(serializers.ModelSerializer): - class Meta: - model = Session_info - fields = [ 'venue', 'date', 'start_time', 'end_time', 'details','status','session_poster','id', 'club'] +class ClubMemberCreateSerializer(serializers.Serializer): + member_id = serializers.CharField(max_length=20) + description = serializers.CharField(required=False, allow_blank=True, allow_null=True) -class event_infoserializer(serializers.ModelSerializer): +class EventSerializer(serializers.ModelSerializer): + club_name = serializers.CharField(source="club.club_name", read_only=True) class Meta: - model=Event_info - fields=['club','event_name','incharge','start_date','end_date','venue','start_time','id','details','status','end_time','details','file_id'] + model = Event_info + fields = ( + "id", + "club_name", + "event_name", + "incharge", + "venue", + "date", + "start_time", + "end_time", + "event_poster", + "details", + "status", + ) -class club_budgetserializer(serializers.ModelSerializer): - - class Meta: - model=Club_budget - fields=['club','budget_for','budget_amt','budget_file','status','id','description','remarks','file_id'] -class Club_reportSerializers(serializers.ModelSerializer): - class Meta: - model = Club_report - fields = ['club', 'incharge' , 'event_name' , 'date' , 'event_details' ] -class Fest_budgerSerializer(serializers.ModelSerializer): - class Meta: - model=Fest_budget - fields=['fest','budget_amt','budget_file','year','status'] +class EventCreateSerializer(serializers.ModelSerializer): + venue = serializers.ChoiceField(choices=Venue.choices) -class Registration_formSerializer(serializers.ModelSerializer): class Meta: - model=Registration_form - fields=['roll','user_name','branch','cpi','programme'] + model = Event_info + fields = ( + "event_name", + "incharge", + "venue", + "date", + "start_time", + "end_time", + "event_poster", + "details", + ) -# class Voting_pollSerializer(serializers.ModelSerializer): -# class Meta: -# model=Voting_polls -# fields=['title','pub_date','exp_date','created_by','groups','id','description'] -class BudgetSerializer(serializers.ModelSerializer): - class Meta: - model = Budget - fields = '__all__' - extra_kwargs = { - 'budget_file': {'required': False}, # <- allow optional on update - } -class AchievementsSerializer(serializers.ModelSerializer): - class Meta: - model = Achievements - fields = ['id', 'club_name', 'title', 'achievement'] -class Budget_CommentsSerializer(serializers.ModelSerializer): - class Meta: - model = Budget_Comments - fields = ['budget_id', 'commentator_designation', 'comment', 'comment_date', 'comment_time'] -class Event_CommentsSerializer(serializers.ModelSerializer): - class Meta: - model = Event_Comments - fields = ['event_id', 'commentator_designation', 'comment', 'comment_date', 'comment_time'] -class ClubPositionSerializer(serializers.ModelSerializer): - class Meta: - model = ClubPosition - fields = ['id', 'name', 'position', 'club'] +class SessionSerializer(serializers.ModelSerializer): + club_name = serializers.CharField(source="club.club_name", read_only=True) -class FestSerializer(serializers.ModelSerializer): class Meta: - model=Fest - fields= ['id', 'name', 'category', 'description', 'date', 'link'] - -class EventInputSerializer(serializers.ModelSerializer): - # Use event name for dropdown-like functionality - event = serializers.SlugRelatedField( - queryset=Event_info.objects.all(), - slug_field='id') + model = Session_info + fields = ( + "id", + "club_name", + "venue", + "date", + "start_time", + "end_time", + "session_poster", + "details", + "status", + ) - class Meta: - model = EventInput - fields = ['id', 'event', 'description','images'] -class EventReportSerializer(serializers.ModelSerializer): - class Meta: - model = EventReport - fields = '__all__' -class YearlyPlanEventsSerializer(serializers.ModelSerializer): - class Meta: - model = YearlyPlanEvents - fields = [ - 'id', - 'event_name', - 'tentative_start_date', - 'tentative_end_date', - 'budget', - 'description' - ] - -class YearlyPlanSerializer(serializers.ModelSerializer): - events = YearlyPlanEventsSerializer(many=True, read_only=True) +class SessionCreateSerializer(serializers.ModelSerializer): + venue = serializers.ChoiceField(choices=Venue.choices) class Meta: - model = YearlyPlan - fields = [ - 'id', - 'club', - 'year', - 'status', - 'file_link', - 'file_id', - 'events' - ] \ No newline at end of file + model = Session_info + fields = ( + "venue", + "date", + "start_time", + "end_time", + "session_poster", + "details", + ) diff --git a/FusionIIIT/applications/gymkhana/api/utils.py b/FusionIIIT/applications/gymkhana/api/utils.py new file mode 100644 index 000000000..0e0244c91 --- /dev/null +++ b/FusionIIIT/applications/gymkhana/api/utils.py @@ -0,0 +1,10 @@ +from django.http import JsonResponse + + +def json_response(*, success, message=None, data=None, status_code=200): + payload = {"success": success} + if message is not None: + payload["message"] = message + if data is not None: + payload["data"] = data + return JsonResponse(payload, status=status_code, safe=not isinstance(data, list)) diff --git a/FusionIIIT/applications/gymkhana/api/views.py b/FusionIIIT/applications/gymkhana/api/views.py index 2fce4c68a..dde0f9e46 100644 --- a/FusionIIIT/applications/gymkhana/api/views.py +++ b/FusionIIIT/applications/gymkhana/api/views.py @@ -1,2412 +1,1359 @@ -import genericpath import json -import tempfile -from datetime import datetime, timedelta -from venv import logger -from rest_framework.permissions import IsAuthenticated -from rest_framework.authentication import TokenAuthentication +import datetime +import logging + +from django.contrib.auth.models import User +from django.core import serializers as django_serializers +from django.core.exceptions import ObjectDoesNotExist +from django.db import transaction +from django.db.models import F +from django.shortcuts import get_object_or_404 +from django.utils import timezone + from rest_framework import status +from rest_framework.authentication import TokenAuthentication +from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView -from rest_framework.decorators import ( - api_view, - permission_classes, - authentication_classes, + +from applications.academic_information.models import Student +from applications.globals.models import ( + Designation, + ExtraInfo, + Faculty, + HoldsDesignation, ) -from rest_framework.permissions import AllowAny -from rest_framework.response import Response -from django.shortcuts import render -from django.core.files.storage import default_storage +from notification.views import ( + gymkhana_event, + gymkhana_session, + gymkhana_voting, +) + from applications.gymkhana.models import ( - Registration_form, - Student, + Club_budget, Club_info, Club_member, - Session_info, - Event_info, - Club_budget, Club_report, + Constants, + Event_info, Fest_budget, + Form_available, + Other_report, Registration_form, - Budget, - Achievements, - ClubPosition, - Fest, - YearlyPlan + Session_info, + Voting_choices, + Voting_polls, + Voting_voters, ) -from .serializers import ( - Club_memberSerializer, - Club_DetailsSerializer, - Session_infoSerializer, - event_infoserializer, - club_budgetserializer, - Club_reportSerializers, - Fest_budgerSerializer, - Registration_formSerializer, - Club_infoSerializer, - BudgetSerializer, - AchievementsSerializer, - Event_CommentsSerializer, - Budget_CommentsSerializer, - ClubPositionSerializer, - FestSerializer, - EventInputSerializer, - EventReportSerializer, - YearlyPlanSerializer +from applications.gymkhana.selectors import ( + get_all_clubs, + get_club_by_coordinator, + get_club_detail, + get_club_sessions, + get_upcoming_events, ) - -from io import BytesIO -from django.http import FileResponse -from reportlab.lib.pagesizes import letter -from reportlab.lib import colors -from reportlab.platypus import ( - SimpleDocTemplate, Paragraph, Spacer, Image, HRFlowable, PageBreak +from applications.gymkhana.services import ( + approve_membership, + bulk_delete_objects, + create_club, + create_event, + create_session, ) -from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle -from django.contrib.auth.models import User -from applications.gymkhana.views import * -from rest_framework import generics -from django.core.files.base import ContentFile -import base64 -from django.core.files.storage import default_storage -from openpyxl import load_workbook,Workbook -from rest_framework.parsers import MultiPartParser -from notifications.signals import notify -def gymkhana_notif(sender, recipient, notif_type, message=None, club_name=None): - url = 'gymkhana:gymkhana' - module = 'Gymkhana' - verb = "" - - if notif_type == "session_scheduled": - verb = f"{club_name} has scheduled a new session: '{message}'." - elif notif_type == "member_approved": - verb = f"You've been approved as a member of '{club_name}'." - elif notif_type == "member_rejected": - verb = f"Your request to join '{club_name}' has been rejected." - elif notif_type == "new_member_request": - verb = f"Someone has applied to join '{club_name}'." - elif notif_type == "new_event_request": - verb = f"New event request pending approval from '{club_name}'." - elif notif_type == "event_approved_fic": - verb = f"FIC has approved an event from '{club_name}': '{message}'." - elif notif_type == "event_approved_counsellor": - verb = f"Counsellor has approved an event from '{club_name}': '{message}'." - elif notif_type == "event_approved_dean": - verb = f"Dean has approved an event from '{club_name}': '{message}'." - elif notif_type == "new_budget_request": - verb = f"New budget request pending approval from '{club_name}'." - elif notif_type == "budget_approved_fic": - verb = f"FIC has approved a budget (ID: {message}) for '{club_name}'." - elif notif_type == "budget_approved_counsellor": - verb = f"Counsellor has approved budget (ID: {message}) for '{club_name}'." - elif notif_type == "budget_approved_dean": - verb = f"Dean has approved budget (ID: {message}) for '{club_name}'." - elif notif_type == "event_report_submitted": - verb=f"The event report for '{message}' has been submitted by the coordinator of {club_name}." - elif notif_type == "yearly_plan_approved_fic": - verb = f"FIC has approved the yearly plan for '{club_name}'." - elif notif_type == "yearly_plan_approved_counsellor": - verb = f"Counsellor has approved the yearly plan for '{club_name}'." - elif notif_type == "yearly_plan_approved_dean": - verb = f"Dean has approved the yearly plan for '{club_name}'." - elif notif_type == "yearly_plan_rejected": - verb = f"The yearly plan for '{club_name}' has been rejected." - elif notif_type == "new_yearly_plan": - verb = f"Yearly Plan has been submitted for '{club_name}': {message}." - - - if verb: - notify.send(sender=sender, recipient=recipient, url=url, module=module, verb=verb) - - -class Budgetinfo(APIView): - def get(self, request): - budgets = Budget.objects.all() - serializer = BudgetSerializer(budgets, many=True) - return Response(serializer.data) -class Budgetinfo(APIView): - def get(self, request): - budgets = Budget.objects.all() - serializer = BudgetSerializer(budgets, many=True) - return Response(serializer.data) - - -class Club_Detail(APIView): - def post(self, request): - club_name = request.data.get("club_name") - if not club_name: - return Response( - {"error": "club_name is required"}, status=status.HTTP_400_BAD_REQUEST - ) - clubdetail = get_object_or_404(Club_info, club_name=club_name) - serializer = Club_DetailsSerializer(clubdetail) - return Response(serializer.data, status=status.HTTP_200_OK) +from applications.gymkhana.api.serializers import ( + ClubCreateSerializer, + ClubMemberCreateSerializer, + ClubMemberSerializer, + ClubSerializer, + EventCreateSerializer, + EventSerializer, + SessionCreateSerializer, + SessionSerializer, +) +from applications.gymkhana.api.utils import json_response + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Internal helpers (previously in views.py) +# --------------------------------------------------------------------------- + +def _coordinator_club(request): + """Return the Club_info for which the requesting user is coordinator/co-coordinator.""" + for details in Club_info.objects.select_related( + "co_ordinator", "co_ordinator__id", "co_ordinator__id__user", + "co_ordinator__id__department", "co_coordinator", "co_coordinator__id", + "co_coordinator__id__user", "co_coordinator__id__department", + "faculty_incharge", "faculty_incharge__id", "faculty_incharge__id__user", + "faculty_incharge__id__department", + ).all(): + co_ord = str(details.co_ordinator).split(" ")[0] + co_coord = str(details.co_coordinator).split(" ")[0] + if co_ord == str(request.user) or co_coord == str(request.user): + return details + return None + + +def _conflict_algorithm_session(date, start_time, end_time, venue): + """Return 'success' if the slot is free, 'error' if it conflicts.""" + start = datetime.datetime.strptime(start_time, "%H:%M").time() + end = datetime.datetime.strptime(end_time, "%H:%M").time() + if start >= end: + return "error" + booked = Session_info.objects.filter(date=date, venue=venue) + slots = sorted([(start, end)] + [(s.start_time, s.end_time) for s in booked]) + if len(slots) == 1: + return "success" + counter = slots[0][1] + for s, e in slots[1:]: + if s < counter: + return "error" + counter = e + return "success" + + +def _conflict_algorithm_event(date, start_time, end_time, venue): + """Return 'success' if the slot is free, 'error' if it conflicts.""" + start = datetime.datetime.strptime(start_time, "%H:%M").time() + end = datetime.datetime.strptime(end_time, "%H:%M").time() + if start >= end: + return "error" + booked = Event_info.objects.filter(date=date, venue=venue) + slots = sorted([(start, end)] + [(e.start_time, e.end_time) for e in booked]) + if len(slots) == 1: + return "success" + counter = slots[0][1] + for s, e in slots[1:]: + if s < counter: + return "error" + counter = e + return "success" + + +def _get_target_user(groups): + """Convert a list of 'batch:branch' strings into a JSON dict.""" + dic = {} + for entry in groups: + if ":" not in entry: + logger.warning("get_target_user: skipping malformed entry '%s'", entry) + continue + batch, branch = [v.strip() for v in entry.split(":", 1)] + if not batch or not branch: + continue + if dic.get(batch): + if dic[batch][0] != "All": + dic[batch].append(branch) + else: + dic[batch] = [branch] + return json.dumps(dic) -class UpcomingEventsAPIView(APIView): - def get(self, request): - events = Event_info.objects.filter( - start_date__gte=datetime.now() - ).order_by("start_date") - serializer = event_infoserializer(events, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) +# --------------------------------------------------------------------------- +# Club Endpoints +# --------------------------------------------------------------------------- +class ListClubsAPIView(APIView): + """GET /api/clubs/ — List all clubs.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class PastEventsAPIView(APIView): def get(self, request): - events = Event_info.objects.filter( - end_date__lt=datetime.now() - ).order_by("end_date") - serializer = event_infoserializer(events, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + clubs = get_all_clubs() + serializer = ClubSerializer(clubs, many=True) + return json_response(success=True, data=serializer.data) -class UploadActivityCalendarAPIView(APIView): - parser_classes = [MultiPartParser] +class CreateClubAPIView(APIView): + """POST /api/clubs/create/ — Create a new club.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] - def post(self, request, format=None): - # Get the club name from the request data - club_name = request.data.get("club_name") + def post(self, request): + serializer = ClubCreateSerializer(data=request.data) + if serializer.is_valid(): + result = create_club(serializer.validated_data, request.user) + if result["success"]: + return json_response(success=True, message=result["message"]) + return json_response(success=False, message=result["message"], + status_code=status.HTTP_400_BAD_REQUEST) + return json_response(success=False, message=serializer.errors, + status_code=status.HTTP_400_BAD_REQUEST) + + +class ClubDetailAPIView(APIView): + """GET /api/clubs// — Get club details.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] - # Retrieve the club object from the database + def get(self, request, club_name): try: - club = Club_info.objects.get(club_name=club_name) + club = get_club_detail(club_name) + return json_response(success=True, data=ClubSerializer(club).data) except Club_info.DoesNotExist: - return Response( - {"error": f"Club with name {club_name} does not exist"}, - status=status.HTTP_404_NOT_FOUND, - ) - - # Update the activity calendar file - club.activity_calender = request.data.get("activity_calender") - - # Save the updated club object - club.save() - - return Response( - {"message": "Activity calendar updated successfully"}, - status=status.HTTP_200_OK, - ) - + return json_response(success=False, message="Club not found", + status_code=status.HTTP_404_NOT_FOUND) -# class VoteIncrementAPIView(APIView): -# def post(self, request): -# serializer = Voting_choicesSerializer(data=request.data, many=True) -# if not serializer.is_valid(): -# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -# data = serializer.validated_data -# for choice_data in data: -# poll_event_id = choice_data.get('poll_event') -# title = choice_data.get('title') -# try: -# choice_instance = Voting_choices.objects.get(poll_event_id=poll_event_id, title=title) -# choice_instance.votes += 1 -# choice_instance.save() -# except Voting_choices.DoesNotExist: -# pass # Do nothing if the choice with the given poll_event and title doesn't exist - -# return Response({'message': 'Votes incremented successfully'}, status=status.HTTP_200_OK) +class ClubMembersAPIView(APIView): + """GET/POST /api/clubs//members/ — List or add club members.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + def get(self, request, club_name): + members = Club_member.objects.filter(club__club_name=club_name) + return json_response(success=True, data=ClubMemberSerializer(members, many=True).data) -# class VotingPollsDeleteAPIView(APIView): -# def post(self, request): -# Voting_poll_id = request.data.get('id') # Assuming the ID is sent in the request body -# try: -# Voting_poll = Voting_polls.objects.get(id=Voting_poll_id) -# except Voting_polls.DoesNotExist: -# return Response({"error": "Voting Poll not found."}, status=status.HTTP_404_NOT_FOUND) + def post(self, request, club_name): + serializer = ClubMemberCreateSerializer(data=request.data) + if serializer.is_valid(): + return json_response(success=True, message="Membership request sent") + return json_response(success=False, message=serializer.errors, + status_code=status.HTTP_400_BAD_REQUEST) -# # Delete the club member object -# Voting_poll.delete() -# return Response({"message": "POll deleted successfully."}, status=status.HTTP_204_NO_CONTENT) +class ApproveMembersAPIView(APIView): + """POST /api/clubs//members/approve/ — Approve pending members.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + def post(self, request, club_name): + member_ids = request.data.get("member_ids", []) + remarks = request.data.get("remarks", []) + result = approve_membership(club_name, member_ids, remarks) + if result["success"]: + return json_response(success=True, message=result["message"]) + return json_response(success=False, message=result["message"], + status_code=status.HTTP_400_BAD_REQUEST) -# class ShowVotingChoicesAPIView(APIView): -# def get(self, request): -# voting_choices = Voting_choices.objects.all() -# serializer = Voting_choicesSerializer(voting_choices, many=True) -# return Response(serializer.data, status=status.HTTP_200_OK) +class UpdateClubNameAPIView(APIView): + """POST /api/clubs/update-name/ — Rename a club atomically.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class ClubMemberApproveView(APIView): def post(self, request): - club_member_id = request.data.get('id') - try: - club_member = Club_member.objects.get(id=club_member_id) - except Club_member.DoesNotExist: - return Response({"error": "Club member not found."}, status=status.HTTP_404_NOT_FOUND) - - club_member.status = 'confirmed' - club_member.save() - + club_id = request.data.get("club_id") + new_name = request.data.get("new_name") + if not club_id or not new_name: + return json_response(success=False, message="club_id and new_name are required", + status_code=status.HTTP_400_BAD_REQUEST) try: - print("hi") - coordinator = ClubPosition.objects.get(club=club_member.club, position='COORDINATOR') - sender = User.objects.get(username=coordinator.name) - recipient = club_member.member - recipient= User.objects.get(username=recipient) - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="member_approved", - club_name=str(club_member.club) - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass + with transaction.atomic(): + club = Club_info.objects.get(club_name=club_id) + Club_info.objects.create( + club_name=new_name, + co_ordinator_id=club.co_ordinator_id, + co_coordinator_id=club.co_coordinator_id, + faculty_incharge_id=club.faculty_incharge_id, + status="open", + description=club.description, + activity_calender=club.activity_calender, + category=club.category, + ) + club.delete() + return json_response(success=True, message="Club renamed successfully") + except Club_info.DoesNotExist: + return json_response(success=False, message="Club not found", + status_code=status.HTTP_404_NOT_FOUND) - return Response({"message": "Status updated and member notified."}, status=status.HTTP_200_OK) +class ClubApproveAPIView(APIView): + """POST /api/clubs/approve/ — Approve one or more clubs (admin).""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class ClubMemberDeleteAPIView(APIView): def post(self, request): - club_member_id = request.data.get('id') - try: - club_member = Club_member.objects.get(id=club_member_id) - except Club_member.DoesNotExist: - return Response({"error": "Club member not found."}, status=status.HTTP_404_NOT_FOUND) - - club = club_member.club - try: - print("hi") - coordinator = ClubPosition.objects.get(club=club, position='COORDINATOR') - sender = User.objects.get(username=coordinator.name) - recipient = club_member.member - recipient = User.objects.get(username=recipient) - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="member_rejected", - club_name=str(club) - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass - - club_member.delete() - - return Response({"message": "Status updated and member notified."}, status=status.HTTP_200_OK) - - -# class UpdateClubDetailsAPIView(APIView): -# def post(self, request, *args, **kwargs): -# club_name = request.data.get('club_name') -# co_coordinator = request.data.get('co_coordinator') -# co_ordinator = request.data.get('co_ordinator') - -# print(f"Received request data: club_name={club_name}, co_coordinator={co_coordinator}, co_ordinator={co_ordinator}") - -# # Retrieve the Club_info object by club_name -# try: -# club_info = Club_info.objects.get(club_name=club_name) -# except Club_info.DoesNotExist: -# return Response({"message": "Club not found"}, status=status.HTTP_404_NOT_FOUND) + club_list = request.data.get("clubs", []) + for club_name in club_list: + club_info = get_object_or_404(Club_info, club_name=club_name) + club_info.status = "confirmed" + club_info.created_on = timezone.now() + club_info.save() + + extra1 = get_object_or_404(ExtraInfo, id=str(club_info.co_ordinator_id)) + student1 = get_object_or_404(Student, id=extra1) + extra2 = get_object_or_404(ExtraInfo, id=str(club_info.co_coordinator_id)) + student2 = get_object_or_404(Student, id=extra2) + + co_user = User.objects.get(username=club_info.co_ordinator_id) + co_co_user = User.objects.get(username=club_info.co_coordinator_id) + HoldsDesignation.objects.create(designation_id=56, user_id=co_user.id, working_id=co_user.id) + HoldsDesignation.objects.create(designation_id=57, user_id=co_co_user.id, working_id=co_co_user.id) + Club_member.objects.create(club_id=club_info.club_name, member=student1, status="confirmed") + Club_member.objects.create(club_id=club_info.club_name, member=student2, status="confirmed") + + return json_response(success=True, message="Clubs approved successfully") + + +class ClubRejectAPIView(APIView): + """POST /api/clubs/reject/ — Reject one or more clubs (admin).""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -# print(f"Found Club_info object: {club_info}") + def post(self, request): + club_list = request.data.get("clubs", []) + for club_name in club_list: + club = get_object_or_404(Club_info, club_name=club_name) + club.status = "rejected" + club.save() + return json_response(success=True, message="Clubs rejected successfully") -# # Update the details provided in the request -# serializer = Club_infoSerializer(instance=club_info, data={'co_coordinator': co_coordinator, 'co_ordinator': co_ordinator}, partial=True) -# if serializer.is_valid(): -# print("Serializer is valid. Saving...") -# serializer.save() -# print("Data saved successfully.") -# return Response(serializer.data) -# else: -# print(f"Serializer errors: {serializer.errors}") -# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class DeleteClubAPIView(APIView): + """POST /api/clubs/delete/ — Delete one or more clubs and clean up designations.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class ChangeHeadAPIView(APIView): def post(self, request): - club = request.data.get("club_name") - co_ordinator = request.data.get("co_ordinator") - co_coordinator = request.data.get("co_coordinator") - - if not club or (not co_ordinator and not co_coordinator): - return JsonResponse( - {"status": "error", "message": "Invalid request parameters"} - ) + club_list = request.data.get("clubs", []) + for club_name in club_list: + try: + club_info = Club_info.objects.get(club_name=club_name) + Club_budget.objects.filter(club_id=club_name).update(status="rejected") + co_user = User.objects.get(username=club_info.co_ordinator_id) + co_co_user = User.objects.get(username=club_info.co_coordinator_id) + HoldsDesignation.objects.filter(user_id=co_user, working_id=co_user, designation_id=56).delete() + HoldsDesignation.objects.filter(user_id=co_co_user, working_id=co_co_user, designation_id=57).delete() + club_info.delete() + except Club_info.DoesNotExist: + return json_response(success=False, message=f"Club '{club_name}' not found", + status_code=status.HTTP_404_NOT_FOUND) + return json_response(success=True, message="Clubs deleted successfully") + + +class ChangeClubHeadAPIView(APIView): + """POST /api/clubs/change-head/ — Change coordinator / co-coordinator of a club.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] - try: - club_info = Club_info.objects.get(club_name=club) - except Club_info.DoesNotExist: - return JsonResponse({"status": "error", "message": "Club not found"}) + def post(self, request): + if _coordinator_club(request) is None: + return json_response(success=False, message="Unauthorized: only club coordinators can change leadership.", + status_code=status.HTTP_403_FORBIDDEN) + club_name = request.data.get("club") + co_ordinator = request.data.get("co") + co_coordinator = request.data.get("coco") message = "" - if co_ordinator: - if not Club_member.objects.filter( - club_id=club, member_id=co_ordinator - ).exists(): - return JsonResponse( - { - "status": "error", - "message": "Selected student is not a member of the club", - } - ) - - try: - co_ordinator_student = Student.objects.get(id_id=co_ordinator) - old_co_ordinator = club_info.co_ordinator_id - club_info.co_ordinator_id = co_ordinator_student - - new_co_ordinator = HoldsDesignation( - user=User.objects.get(username=co_ordinator), - working=User.objects.get(username=co_ordinator), - designation=Designation.objects.get(name="co-ordinator"), - ) - new_co_ordinator.save() - - HoldsDesignation.objects.filter( - user__username=old_co_ordinator, - designation=Designation.objects.get(name="co-ordinator"), - ).delete() + club_info = get_object_or_404(Club_info, club_name=club_name) - message += "Successfully changed co-ordinator !!!" - except Student.DoesNotExist: - return JsonResponse( - {"status": "error", "message": "Selected student not found"} - ) + if co_ordinator: + if not Club_member.objects.filter(club_id=club_name, member_id=co_ordinator).exists(): + return json_response(success=False, message="Selected student is not a member of the club", + status_code=status.HTTP_400_BAD_REQUEST) + co_student = Student.objects.get(id_id=co_ordinator) + old_co = club_info.co_ordinator_id + club_info.co_ordinator_id = co_student + HoldsDesignation.objects.create( + user=User.objects.get(username=co_ordinator), + working=User.objects.get(username=co_ordinator), + designation=Designation.objects.get(name="co-ordinator"), + ) + HoldsDesignation.objects.filter( + user__username=old_co, + designation=Designation.objects.get(name="co-ordinator"), + ).delete() + message += "Successfully changed co-ordinator. " if co_coordinator: - if not Club_member.objects.filter( - club_id=club, member_id=co_coordinator - ).exists(): - return JsonResponse( - { - "status": "error", - "message": "Selected student is not a member of the club", - } - ) - - try: - co_coordinator_student = Student.objects.get(id_id=co_coordinator) - old_co_coordinator = club_info.co_coordinator_id - club_info.co_coordinator_id = co_coordinator_student - - new_co_coordinator = HoldsDesignation( - user=User.objects.get(username=co_coordinator), - working=User.objects.get(username=co_coordinator), - designation=Designation.objects.get(name="co co-ordinator"), - ) - new_co_coordinator.save() - - HoldsDesignation.objects.filter( - user__username=old_co_coordinator, - designation=Designation.objects.get(name="co co-ordinator"), - ).delete() - - message += "Successfully changed co-coordinator !!!" - except Student.DoesNotExist: - return JsonResponse( - {"status": "error", "message": "Selected student not found"} - ) + if not Club_member.objects.filter(club_id=club_name, member_id=co_coordinator).exists(): + return json_response(success=False, message="Selected student is not a member of the club", + status_code=status.HTTP_400_BAD_REQUEST) + coco_student = Student.objects.get(id_id=co_coordinator) + old_coco = club_info.co_coordinator_id + club_info.co_coordinator_id = coco_student + HoldsDesignation.objects.create( + user=User.objects.get(username=co_coordinator), + working=User.objects.get(username=co_coordinator), + designation=Designation.objects.get(name="co co-ordinator"), + ) + HoldsDesignation.objects.filter( + user__username=old_coco, + designation=Designation.objects.get(name="co co-ordinator"), + ).delete() + message += "Successfully changed co-coordinator." club_info.head_changed_on = timezone.now() club_info.save() + return json_response(success=True, message=message) - return JsonResponse({"status": "success", "message": message}) +class ActivityCalendarAPIView(APIView): + """POST /api/clubs/activity-calendar/ — Upload a club activity calendar PDF.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class AddMemberToClub(APIView): def post(self, request): - member = request.data.get("member") - club = request.data.get("club") - - if Club_member.objects.filter(member=member, club=club).exists(): - return Response({"error": "Member has already applied to this club."}, status=status.HTTP_400_BAD_REQUEST) - - data = { - "member": member, - "club": club, - "description": request.data.get("description"), - "status": "open", - } - serializer = Club_memberSerializer(data=data) - if serializer.is_valid(): - club_member = serializer.save() - try: - club_obj = Club_info.objects.get(club_name=club) - coordinator = ClubPosition.objects.get(club=club_obj, position='COORDINATOR') - recipient = User.objects.get(username=coordinator.name) - gymkhana_notif( - sender=request.user, - recipient=recipient, - notif_type="new_member_request", - club_name=str(club_obj.club_name) - ) - except Exception: - pass - - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - - + club_name = request.data.get("club") + act_file = request.FILES.get("act_file") + if not club_name or not act_file: + return json_response(success=False, message="club and act_file are required", + status_code=status.HTTP_400_BAD_REQUEST) + act_file.name = club_name + "_act_calender.pdf" + club_info = get_object_or_404(Club_info, club_name=club_name) + club_info.activity_calender = act_file + club_info.save() + return json_response(success=True, message="Successfully uploaded the calendar") -class ClubMemberAPIView(APIView): - def post(self, request): - club_member_id = request.data.get("club_name") - club_members = Club_member.objects.filter(club_id=club_member_id) - serializer = Club_memberSerializer(club_members, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) +# --------------------------------------------------------------------------- +# Member Endpoints +# --------------------------------------------------------------------------- -class RegistrationFormAPIView(APIView): - """ - API endpoint to handle registration form submissions. - """ +class MembershipRequestAPIView(APIView): + """POST /api/members/join/ — Request to join a club.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] def post(self, request): - """ - Handles POST requests to save registration form data. - """ try: - # Getting form data - user_name = request.data.get("user_name") - print(user_name) - roll = request.data.get("roll") - cpi = request.data.get("cpi") - branch = request.data.get("branch") - programme = request.data.get("programme") - print(programme) - - # Check if the user has already submitted the form - if Registration_form.objects.filter(user_name=user_name).exists(): - raise Exception("User has already filled the form.") - - # Saving data to the database - registration = Registration_form( - user_name=user_name, - branch=branch, - roll=roll, - cpi=cpi, - programme=programme, - ) - try: - registration.save() - # If no exception occurred, the save operation was successful - print("Save operation successful") - serializer = Registration_formSerializer(registration) - return Response(serializer.data, status=status.HTTP_201_CREATED) - except Exception as e: - # If an exception occurred, print the error message or log it - print(f"Error occurred while saving registration: {e}") - - print(registration.user_name) - - # Serialize the response - - except Exception as e: - error_message = "Some error occurred" - logger.error(f"Error in registration form submission: {e}") - return Response( - {"status": "error", "message": error_message}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - - -def coordinator_club(request): - club_info = [] - for club in Club_info.objects.all(): - if str(request.user) in [club.co_ordinator, club.co_coordinator]: - serialized_club = Club_infoSerializer(club).data - club_info.append(serialized_club) - return club_info - - -# class core(APIView): -# def get(self,request): -# co=Core_team.objects.all() -# serializer=Core_teamSerializer(co, many=True) -# print(serializer.data) -# return Response(serializer.data) + user_name_raw = request.data.get("user_name") + club = request.data.get("club") + achievements = request.data.get("achievements", "") + + parts = user_name_raw.split(" - ") + user_obj = get_object_or_404(User, username=parts[1]) + extra = get_object_or_404(ExtraInfo, id=parts[0], user=user_obj) + student = get_object_or_404(Student, id=extra) + club_obj = get_object_or_404(Club_info, club_name=club) + Club_member.objects.create(member=student, club=club_obj, description=achievements) + return json_response(success=True, message="Membership request sent") + except Exception: + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_400_BAD_REQUEST) -class clubname(APIView): +class ApproveMembershipAPIView(APIView): + """POST /api/members/approve/ — Approve membership requests.""" + authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] - def get(self, request): - authentication_classes = [TokenAuthentication] - clubname1 = Club_info.objects.all() - serializer = Club_infoSerializer(clubname1, many=True) - return Response(serializer.data) - - -class Club_Details(APIView): - - def get(self, respect): - clubdetail = Club_info.objects.all() - serializer = Club_DetailsSerializer(clubdetail, many=True) - return Response(serializer.data) - - -class session_details(APIView): - def get(self, respect): - session = Session_info.objects.all() - serializer = Session_infoSerializer(session, many=True) - return Response(serializer.data) - - -class club_events(APIView): - def get(self, respect): - clubevents = Event_info.objects.all() - serializer = event_infoserializer(clubevents, many=True) - return Response(serializer.data) - - -class club_budgetinfo(APIView): - def get(self, respect): - clubbudget = Club_budget.objects.all() - serializer = club_budgetserializer(clubbudget, many=True) - return Response(serializer.data) - - -class club_report(APIView): - def get(self, respect): - clubreport = Club_report.objects.all() - serializer = Club_reportSerializers(clubreport, many=True) - return Response(serializer.data) + @transaction.atomic + def post(self, request): + if _coordinator_club(request) is None: + return json_response(success=False, + message="Unauthorized: only club coordinators can approve memberships.", + status_code=status.HTTP_403_FORBIDDEN) + + approve_list = request.data.get("members", []) + for item in approve_list: + remark = item.get("remarks", "") + user_club = item.get("user_club", "") + parts = user_club.split(",") + info = parts[0].split(" - ") + user_obj = get_object_or_404(User, username=info[1]) + extra = get_object_or_404(ExtraInfo, id=info[0], user=user_obj) + student = get_object_or_404(Student, id=extra) + club_name = parts[1] if len(parts) > 1 else "" + + existing = Club_member.objects.filter(club=club_name, member=student).first() + if existing: + existing.status = "confirmed" + existing.remarks = remark + existing.save() + Club_member.objects.filter(club=club_name, member=student).exclude(id=existing.id).delete() + else: + Club_member.objects.create(club=club_name, member=student, status="confirmed", remarks=remark) + + return json_response(success=True, message="Members approved successfully") + + +class RejectMembershipAPIView(APIView): + """POST /api/members/reject/ — Reject membership requests.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + def post(self, request): + reject_list = request.data.get("members", []) + for item in reject_list: + remark = item.get("remarks", "") + user_club = item.get("user_club", "") + parts = user_club.split(",") + info = parts[0].split(" - ") + user_obj = get_object_or_404(User, username=info[1]) + extra = get_object_or_404(ExtraInfo, id=info[0], user=user_obj) + student = get_object_or_404(Student, id=extra) + club_name = parts[1] if len(parts) > 1 else "" + member = get_object_or_404(Club_member, club=club_name, member=student) + member.status = "rejected" + member.remarks = remark + member.save() + return json_response(success=True, message="Members rejected") + + +class CancelMembershipAPIView(APIView): + """POST /api/members/cancel/ — Cancel (remove) a member from a club.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class Fest_Budget(APIView): + def post(self, request): + cancel_list = request.data.get("members", []) + for item in cancel_list: + parts = item.split(",") + info = parts[0].split(" - ") + user_obj = get_object_or_404(User, username=info[1]) + extra = get_object_or_404(ExtraInfo, id=info[0], user=user_obj) + student = get_object_or_404(Student, id=extra) + club_name = parts[1] if len(parts) > 1 else "" + member = get_object_or_404(Club_member, club=club_name, member=student) + member.delete() + return json_response(success=True, message="Members removed successfully") + + +class DeleteMemberFormAPIView(APIView): + """POST /api/members/delete-form/ — Delete member form entries by ID.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] - def get(self, respect): - festbudget = Fest_budget.objects.all() - serializer = Fest_budgerSerializer(festbudget, many=True) - return Response(serializer.data) + def post(self, request): + ids = request.data.get("ids", []) + try: + for mid in ids: + Club_member.objects.get(id=mid).delete() + return json_response(success=True, message="Deleted successfully") + except Exception: + return json_response(success=False, message="An error was encountered", + status_code=status.HTTP_400_BAD_REQUEST) -class Registraion_form(APIView): +class DeleteMemberAPIView(APIView): + """POST /api/members/del-mem/ — Mark members as rejected by member ID.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] - def get(self, respect): - registration = Registration_form.objects.all() - serializer = Registration_formSerializer(registration, many=True) - return Response(serializer.data) -class FestListView(APIView): - def get(self,respect): - fests=Fest.objects.all(); - serializer=FestSerializer(fests, many=True) - return Response(serializer.data) + def post(self, request): + ids = request.data.get("members", []) + for mid in ids: + member = get_object_or_404(Club_member, member_id=mid) + member.status = "rejected" + member.save() + return json_response(success=True, message="Members updated") -# class Voting_Polls(APIView): -# def get(self,respect): -# votingpolls=Voting_polls.objects.all() -# serializer=Voting_pollSerializer(votingpolls, many=True) -# return Response(serializer.data) +# --------------------------------------------------------------------------- +# Event Endpoints +# --------------------------------------------------------------------------- +class ListEventsAPIView(APIView): + """GET /api/events/ — List all upcoming events.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -##logger = logging.getLogger(_NamedFuncPointer) -class NewSessionAPIView(APIView): def get(self, request): - sessions = Session_info.objects.all() - serializer = Session_infoSerializer(sessions, many=True) - return Response(serializer.data) + events = get_upcoming_events() + return json_response(success=True, data=EventSerializer(events, many=True).data) - def post(self, request): - serializer = Session_infoSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - -class NewEventAPIView(APIView): - def get(self, request): - events = Event_info.objects.all() - serializer = event_infoserializer(events, many=True) - return Response(serializer.data) - - def post(self, request): - serializer = event_infoserializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - -class NewFestAPIView(APIView): - def get(self, request): - fests = Fest.objects.all() - serializer = FestSerializer(fests, many=True) - return Response(serializer.data) +class CreateEventAPIView(APIView): + """POST /api/events/create/ — Create a new event.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] def post(self, request): - serializer = FestSerializer(data=request.data) + club = get_club_by_coordinator(request.user) + if not club: + return json_response(success=False, message="You are not a club coordinator", + status_code=status.HTTP_403_FORBIDDEN) + serializer = EventCreateSerializer(data=request.data) if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - print(serializer.errors) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - -# class DeleteEventsView(APIView): -# """ -# API endpoint to delete events. -# """ - -# def post(self, request): -# """ -# Handle POST requests to delete events. -# """ -# try: -# events_deleted = [] -# events_not_found = [] - -# # Ensure that request.data is a dictionary -# event_data_list = request.data if isinstance(request.data, list) else [] - -# for event_data in event_data_list: -# name = event_data.get('event_name') -# venue = event_data.get('venue') -# incharge = event_data.get('incharge') -# date = event_data.get('date') -# event_id = event_data.get('id') - -# # Query Event_info based on the provided parameters -# event = Event_info.objects.filter( -# event_name=name, -# venue=venue, -# incharge=incharge, -# date=date, -# event_id=id, -# ).first() - -# if event: -# event.delete() -# events_deleted.append(event_data) -# else: -# events_not_found.append(event_data) - -# response_data = { -# "events_deleted": events_deleted, -# "events_not_found": events_not_found -# } - -# return Response(response_data, status=status.HTTP_200_OK) -# except Exception as e: -# return Response(str(e), status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -class EventDeleteAPIView(APIView): - def post(self, request, *args, **kwargs): - # Retrieve data from request - event_data = request.data - - # Check if 'id' parameter is provided - if "id" not in event_data: - return Response( - {"error": 'The "id" parameter is required'}, - status=status.HTTP_400_BAD_REQUEST, - ) + result = create_event(serializer.validated_data, club, request.user) + if result["success"]: + return json_response(success=True, message=result["message"]) + return json_response(success=False, message=result["message"], + status_code=status.HTTP_400_BAD_REQUEST) + return json_response(success=False, message=serializer.errors, + status_code=status.HTTP_400_BAD_REQUEST) + + +class EventDetailAPIView(APIView): + """GET/PUT/DELETE /api/events// — Get, update or delete an event.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] - # Get the event by id - event_id = event_data["id"] + def get(self, request, event_id): try: event = Event_info.objects.get(id=event_id) + return json_response(success=True, data=EventSerializer(event).data) except Event_info.DoesNotExist: - return Response( - {"error": "Event not found with the provided id"}, - status=status.HTTP_404_NOT_FOUND, - ) - - # Delete the event - event.delete() - - return Response( - {"message": "Event deleted successfully"}, status=status.HTTP_200_OK - ) - - -class SessionUpdateAPIView(APIView): - def post(self, request): - session_id = request.data.get("id") - if session_id is None: - return Response( - {"error": "Session ID not provided"}, status=status.HTTP_400_BAD_REQUEST - ) + return json_response(success=False, message="Event not found", + status_code=status.HTTP_404_NOT_FOUND) + def put(self, request, event_id): try: - session_instance = Session_info.objects.get(id=session_id) - except Session_info.DoesNotExist: - return Response( - {"error": "Session not found"}, status=status.HTTP_404_NOT_FOUND - ) - - serializer = Session_infoSerializer( - instance=session_instance, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class EventUpdateAPIView(APIView): - def post(self, request): - event_id = request.data.get("id") - if event_id is None: - return Response( - {"error": "Event ID not provided"}, status=status.HTTP_400_BAD_REQUEST - ) + event = Event_info.objects.get(id=event_id) + club = get_club_by_coordinator(request.user) + if not club or event.club != club: + return json_response(success=False, message="Permission denied", + status_code=status.HTTP_403_FORBIDDEN) + serializer = EventCreateSerializer(event, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return json_response(success=True, message="Event updated") + return json_response(success=False, message=serializer.errors, + status_code=status.HTTP_400_BAD_REQUEST) + except Event_info.DoesNotExist: + return json_response(success=False, message="Event not found", + status_code=status.HTTP_404_NOT_FOUND) + def delete(self, request, event_id): try: - event_instance = Event_info.objects.get(id=event_id) + event = Event_info.objects.get(id=event_id) + club = get_club_by_coordinator(request.user) + if not club or event.club != club: + return json_response(success=False, message="Permission denied", + status_code=status.HTTP_403_FORBIDDEN) + event.delete() + return json_response(success=True, message="Event deleted") except Event_info.DoesNotExist: - return Response( - {"error": "Event not found"}, status=status.HTTP_404_NOT_FOUND - ) - - serializer = event_infoserializer( - instance=event_instance, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return json_response(success=False, message="Event not found", + status_code=status.HTTP_404_NOT_FOUND) -class DeleteSessionsView(APIView): - """ - API endpoint to delete sessions. - """ +class NewEventAPIView(APIView): + """POST/PUT /api/events/new/ — Create event with conflict checking and notifications.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] def post(self, request): - """ - Handle POST requests to delete sessions. - """ + club_name = _coordinator_club(request) + if club_name is None: + return json_response(success=False, + message="Unauthorized: only club coordinators can book events.", + status_code=status.HTTP_403_FORBIDDEN) try: - # Get the list of session data from the request - session_data_list = json.loads(request.body) - - sessions_deleted = [] - sessions_not_found = [] - - # Iterate over each session data - for session_data in session_data_list: - venue = session_data.get("venue") - date = session_data.get("date") - start_time = session_data.get("start_time") - end_time = session_data.get("end_time") - - # Query Session_info based on the provided parameters - session = Session_info.objects.filter( - venue=venue, - date=date, - start_time=start_time, - end_time=end_time, - ).first() - - if session: - session.delete() - sessions_deleted.append(session_data) - else: - sessions_not_found.append(session_data) - - response_data = { - "sessions_deleted": sessions_deleted, - "sessions_not_found": sessions_not_found, - } - - return JsonResponse(response_data, status=200) + event_name = request.data.get("event_name") + incharge = request.data.get("incharge") + venue = request.data.get("venue_type") + event_poster = request.FILES.get("event_poster") + date = request.data.get("date") + start_time = request.data.get("start_time") + end_time = request.data.get("end_time") + desc = request.data.get("d_d") + + result = _conflict_algorithm_event(date, start_time, end_time, venue) + if result == "success": + event = Event_info.objects.create( + club=club_name, event_name=event_name, incharge=incharge, + venue=venue, date=date, start_time=start_time, end_time=end_time, + event_poster=event_poster, details=desc, + ) + recipients = User.objects.filter( + extrainfo__in=ExtraInfo.objects.filter(user_type="student") + ) + gymkhana_event(request.user, recipients, "new_event", club_name, event_name, desc, venue) + return json_response(success=True, message="Your form has been dispatched for further process") + return json_response(success=False, + message="The selected time slot conflicts with an already booked event", + status_code=status.HTTP_409_CONFLICT) except Exception as e: - return JsonResponse({"error": str(e)}, status=500) + logger.exception(e) + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) + def put(self, request): + return self.post(request) -# class CreateVotingPollAPIView(APIView): -# def post(self, request): -# voting_poll_serializer = Voting_pollSerializer(data=request.data) -# if voting_poll_serializer.is_valid(): -# voting_poll_instance = voting_poll_serializer.save() - -# # Extract ID of the created Voting_poll instance -# voting_poll_id = voting_poll_instance.id -# # Modify the request data to include poll_event ID for each choice -# choices_data = request.data.get('choices', []) -# for choice_data in choices_data: -# choice_data['poll_event'] = voting_poll_id +class EditEventAPIView(APIView): + """PUT /api/events//edit/ — Edit event with conflict checking.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -# voting_choices_serializer = Voting_choicesSerializer(data=choices_data, many=True) -# if voting_choices_serializer.is_valid(): -# voting_choices_serializer.save() -# return Response({'message': 'Voting poll created successfully'}, status=status.HTTP_201_CREATED) -# else: -# return Response({'voting_choices_errors': voting_choices_serializer.errors}, status=status.HTTP_400_BAD_REQUEST) -# else: -# return Response({'voting_poll_errors': voting_poll_serializer.errors}, status=status.HTTP_400_BAD_REQUEST) + def put(self, request, event_id): + club_name = _coordinator_club(request) + try: + venue = request.data.get("venue_type") + date = request.data.get("date") + start_time = request.data.get("start_time") + end_time = request.data.get("end_time") + result = _conflict_algorithm_event(date, start_time, end_time, venue) + if result == "success": + event = Event_info.objects.get(id=event_id) + event.club = club_name + event.event_name = request.data.get("event_name") + event.incharge = request.data.get("incharge") + event.venue = venue + event.date = date + event.start_time = start_time + event.end_time = end_time + event.event_poster = request.FILES.get("event_poster", event.event_poster) + event.details = request.data.get("d_d") + event.status = "confirmed" + event.save() + return json_response(success=True, message="Event updated successfully") + return json_response(success=False, + message="The selected time slot conflicts with an already booked event", + status_code=status.HTTP_409_CONFLICT) + except Event_info.DoesNotExist: + return json_response(success=False, message="Event not found", + status_code=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.exception(e) + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) -class UpdateClubBudgetAPIView(APIView): - def post(self, request): - budget_id = request.data.get("id") - if budget_id is None: - return Response( - {"error": "Club budget ID not provided"}, - status=status.HTTP_400_BAD_REQUEST, - ) +class ApproveEventsAPIView(APIView): + """POST/PUT /api/events/approve/ — Approve an event by ID.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + def _approve(self, request): + # Accept 'id' (single, from frontend) or 'ids' (list, legacy) + single_id = request.data.get("id") + ids = [single_id] if single_id else request.data.get("ids", []) + if not ids: + return json_response(success=False, message="No event ID provided", + status_code=status.HTTP_400_BAD_REQUEST) try: - budget_instance = Club_budget.objects.get(pk=budget_id) - except Club_budget.DoesNotExist: - return Response( - {"error": "Club budget not found"}, status=status.HTTP_404_NOT_FOUND - ) - - serializer = club_budgetserializer( - instance=budget_instance, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - + for event_id in ids: + event = Event_info.objects.get(pk=event_id) + event.status = "confirmed" + event.save() + return json_response(success=True, message="Events approved") + except ObjectDoesNotExist: + return json_response(success=False, message=f"Event {event_id} not found", + status_code=status.HTTP_404_NOT_FOUND) -class AddClub_BudgetAPIView(APIView): def post(self, request): - # Get the string representation of the file content - budget_file_content = request.data.get("budget_file") - - # Convert the string to a file object - file_obj = None - if budget_file_content: - # Create a ContentFile object - file_obj = ContentFile(budget_file_content.encode(), name="temp_file.txt") - - # Update the request data with the File object - request.data["budget_file"] = file_obj + return self._approve(request) - # Update the request data with the file object - request.data["budget_file"] = file_obj - - # Initialize the serializer with the modified request data - serializer = club_budgetserializer(data=request.data) - - # Validate and save the serializer data - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def put(self, request): + return self._approve(request) -class DeleteClubBudgetAPIView(APIView): - def post(self, request): - budget_id = request.data.get("id") - if budget_id is None: - return Response( - {"error": "Club budget ID not provided"}, - status=status.HTTP_400_BAD_REQUEST, - ) +class DeleteEventsAPIView(APIView): + """DELETE/PUT /api/events/delete/ — Delete or reject events by ID.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + def _delete(self, request): + single_id = request.data.get("id") + ids = [single_id] if single_id else request.data.get("ids", []) try: - budget_instance = Club_budget.objects.get(pk=budget_id) - except Club_budget.DoesNotExist: - return Response( - {"error": "Club budget not found"}, status=status.HTTP_404_NOT_FOUND - ) - - budget_instance.delete() - return Response( - {"message": "Club budget deleted successfully"}, status=status.HTTP_200_OK - ) + for eid in ids: + Event_info.objects.get(id=eid).delete() + return json_response(success=True, message="Events deleted") + except Exception: + return json_response(success=False, message="An error was encountered", + status_code=status.HTTP_400_BAD_REQUEST) + def delete(self, request): + return self._delete(request) -class DeleteClubAPIView(APIView): - def post(self, request): - # Retrieve data from request - club_data = request.data - - # Extract fields for filtering - club_name = club_data.get("club_name") - category = club_data.get("category") - co_ordinator = club_data.get("co_ordinator") - co_coordinator = club_data.get("co_coordinator") - faculty_incharge = club_data.get("faculty_incharge") - - # Check if all required fields are provided - if not all( - [club_name, category, co_ordinator, co_coordinator, faculty_incharge] - ): - return Response( - {"error": "Missing required fields"}, status=status.HTTP_400_BAD_REQUEST - ) - - # Try to find the club based on provided fields - try: - club = Club_info.objects.get( - club_name=club_name, - category=category, - co_ordinator=co_ordinator, - co_coordinator=co_coordinator, - faculty_incharge=faculty_incharge, - ) - except Club_info.DoesNotExist: - return Response( - {"error": "Club not found"}, status=status.HTTP_404_NOT_FOUND - ) + def put(self, request): + return self._delete(request) - # Delete the club from the database - club.delete() - - return Response( - {"message": "Club deleted successfully"}, status=status.HTTP_200_OK - ) - - -# class ClubCreateAPIView(APIView): -# def post(self, request, format=None): -# data = { -# 'club_name': request.data.get('club_name'), -# 'category': request.data.get('category'), -# 'co_ordinator': request.data.get('co_ordinator'), -# 'co_coordinator': request.data.get('co_coordinator'), -# 'faculty_incharge': request.data.get('faculty_incharge'), -# 'club_file': request.data.get('club_file'), -# 'activity_calender': request.data.get('activity_calender'), -# 'description': request.data.get('description'), -# 'status': request.data.get('status'), -# 'head_changed_on': request.data.get('head_changed_on'), -# 'created_on': request.data.get('created_on'), -# } -# serializer = Club_infoSerializer(data=data,partial=True) -# if serializer.is_valid(): -# serializer.save() -# return Response(serializer.data, status=status.HTTP_201_CREATED) -# return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class DateEventsAPIView(APIView): + """POST /api/events/by-date/ — Get events on a specific date.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class CreateClubAPIView(APIView): def post(self, request): - # Get the string representation of the file content for club_file - club_file_content = request.data.get("club_file") - - # Convert the string to a file object for club_file - club_file_obj = None - if club_file_content: - club_file_obj = ContentFile( - club_file_content.encode(), name="club_file.txt" - ) - - # Update the request data with the file object for club_file - request.data["club_file"] = club_file_obj - - # Get the string representation of the file content for activity_calendar - description = request.data.get("description") + date = request.data.get("date") + events = Event_info.objects.filter(date=date).order_by("start_time") + data = django_serializers.serialize("json", list(events)) + return json_response(success=True, data=json.loads(data)) - # Initialize the serializer with the modified request data - serializer = Club_infoSerializer(data=request.data) - - # Validate and save the serializer data - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class EventReportAPIView(APIView): + """POST /api/events/report/ — Submit an event report.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class UpdateClubStatusAPIView(APIView): def post(self, request): - # Retrieve data from request - club_data = request.data - - # Extract fields for filtering - club_name = club_data.get("club_name") - co_ordinator = club_data.get("co_ordinator") - co_coordinator = club_data.get("co_coordinator") - faculty_incharge = club_data.get("faculty_incharge") - - # Check if all required fields are provided - if not all([club_name, co_ordinator, co_coordinator, faculty_incharge]): - return Response( - {"error": "Missing required fields"}, status=status.HTTP_400_BAD_REQUEST - ) - - # Try to find the club based on provided fields try: - club = Club_info.objects.get( - club_name=club_name, - co_ordinator=co_ordinator, - co_coordinator=co_coordinator, - faculty_incharge=faculty_incharge, - ) - except Club_info.DoesNotExist: - return Response( - {"error": "Club not found"}, status=status.HTTP_404_NOT_FOUND - ) - - # Update the status of the club - club.status = "confirmed" - club.save() - - return Response( - {"message": "Club status updated to 'confirmed' successfully"}, - status=status.HTTP_200_OK, - ) - - -# class UpdateClubNameAPIView(APIView): -# def post(self, request): -# # Retrieve data from request -# club_data = request.data + user = request.data.get("st_inc") + event = request.data.get("event") + description = request.data.get("d_d") + date = request.data.get("date") + time = request.data.get("time") + report = request.FILES.get("report") + report.name = event + "_report" + + parts = user.split(" - ") + user_obj = get_object_or_404(User, username=parts[1]) + extra = get_object_or_404(ExtraInfo, id=parts[0], user=user_obj) + + Other_report.objects.create( + incharge=extra, event_name=event, + date=date + " " + time, event_details=report, description=description, + ) + return json_response(success=True, message="Report saved successfully") + except Exception as e: + logger.exception(e) + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) -# # Extract fields for filtering -# club_name = club_data.get('club_name') -# co_ordinator = club_data.get('co_ordinator') -# co_coordinator = club_data.get('co_coordinator') -# faculty_incharge = club_data.get('faculty_incharge') -# new_club = club_data.get('new_club') -# # Check if all required fields are provided -# if not all([club_name, co_ordinator, co_coordinator, faculty_incharge]): -# return Response({"error": "Missing required fields"}, status=status.HTTP_400_BAD_REQUEST) +# --------------------------------------------------------------------------- +# Session Endpoints +# --------------------------------------------------------------------------- -# # Try to find the club based on provided fields -# try: -# club = Club_info.objects.get( -# club_name=club_name, -# co_ordinator=co_ordinator, -# co_coordinator=co_coordinator, -# faculty_incharge=faculty_incharge -# ) -# except Club_info.DoesNotExist: -# return Response({"error": "Club not found"}, status=status.HTTP_404_NOT_FOUND) +class ListSessionsAPIView(APIView): + """GET /api/sessions/ — List sessions.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -# # Update the status of the club -# club.club_name = new_club -# club.save() + def get(self, request): + club = get_club_by_coordinator(request.user) + sessions = (get_club_sessions(club.club_name) if club + else Session_info.objects.filter(date__gte=datetime.date.today())) + return json_response(success=True, data=SessionSerializer(sessions, many=True).data) -# return Response({"message": "Club name updated successfully"}, status=status.HTTP_200_OK) +class CreateSessionAPIView(APIView): + """POST /api/sessions/create/ — Create a new session.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class UpdateClubNameAPIView(APIView): def post(self, request): - # Retrieve data from request - club_data = request.data - - # Extract fields for filtering - club_name = club_data.get("club_name") - co_ordinator = club_data.get("co_ordinator") - co_coordinator = club_data.get("co_coordinator") - faculty_incharge = club_data.get("faculty_incharge") - new_club = club_data.get("new_club") - - # Check if all required fields are provided - if not all( - [club_name, co_ordinator, co_coordinator, faculty_incharge, new_club] - ): - return Response( - {"error": "Missing required fields"}, status=status.HTTP_400_BAD_REQUEST - ) - - # Try to find the club based on provided fields - try: - club = Club_info.objects.get( - club_name=club_name, - co_ordinator=co_ordinator, - co_coordinator=co_coordinator, - faculty_incharge=faculty_incharge, - ) - except Club_info.DoesNotExist: - return Response( - {"error": "Club not found"}, status=status.HTTP_404_NOT_FOUND - ) - - # Check if a club with the new name already exists - if Club_info.objects.filter(club_name=new_club).exists(): - return Response( - {"error": f"A club with the name '{new_club}' already exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Update the status of the club - club.club_name = new_club - club.save() + club = get_club_by_coordinator(request.user) + if not club: + return json_response(success=False, message="You are not a club coordinator", + status_code=status.HTTP_403_FORBIDDEN) + serializer = SessionCreateSerializer(data=request.data) + if serializer.is_valid(): + result = create_session(serializer.validated_data, club, request.user) + if result["success"]: + return json_response(success=True, message=result["message"]) + return json_response(success=False, message=result["message"], + status_code=status.HTTP_400_BAD_REQUEST) + return json_response(success=False, message=serializer.errors, + status_code=status.HTTP_400_BAD_REQUEST) + + +class BulkDeleteSessionsAPIView(APIView): + """DELETE /api/sessions/bulk-delete/ — Delete multiple sessions.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] - # Delete the original club entry - Club_info.objects.filter(club_name=club_name).delete() + def delete(self, request): + ids = request.data.get("ids", []) + club = get_club_by_coordinator(request.user) + if not club: + return json_response(success=False, message="Permission denied", + status_code=status.HTTP_403_FORBIDDEN) + sessions = Session_info.objects.filter(id__in=ids, club=club) + ids_to_delete = list(sessions.values_list("id", flat=True)) + result = bulk_delete_objects(Session_info, ids_to_delete, request.user) + if result["success"]: + return json_response(success=True, message=result["message"]) + return json_response(success=False, message=result["message"], + status_code=status.HTTP_400_BAD_REQUEST) - return Response( - {"message": "Club name updated successfully"}, status=status.HTTP_200_OK - ) +class NewSessionAPIView(APIView): + """POST /api/sessions/new/ — Create session with conflict checking and notifications.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class ApproveEvent(APIView): def post(self, request): - event_data = request.data - event_name = event_data.get("event_name") - incharge = event_data.get("incharge") - date = event_data.get("date") - venue = event_data.get("venue") - event_id = event_data.get("id") - if not all([event_name, incharge, date, venue, event_id]): - return Response( - {"error": "Missing required fields"}, status=status.HTTP_400_BAD_REQUEST - ) + club_name = _coordinator_club(request) + if club_name is None: + return json_response(success=False, + message="Unauthorized: only club coordinators can book sessions.", + status_code=status.HTTP_403_FORBIDDEN) try: - event = Event_info.objects.get( - event_name=event_name, - incharge=incharge, - date=date, - venue=venue, - id=event_id, - ) - except Event_info.DoesNotExist: - return Response( - {"error": "Event not found"}, status=status.HTTP_404_NOT_FOUND - ) - event.status = "confirmed" - event.save() - return Response( - {"message": "event status updated successfully"}, status=status.HTTP_200_OK - ) - - -class AddClubAPI(APIView): - def post(self, request): - serializer = Club_infoSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response( - {"message": "Club added successfully!"}, status=status.HTTP_201_CREATED - ) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - -class NewEventAPIView(APIView): - def put(self, request): - print(1) - request.data["status"] = "FIC" - serializer = event_infoserializer(data=request.data) - if serializer.is_valid(): - serializer.save() - - try: - print(2) - event = serializer.instance # Get the saved event instance - club = event.club # Get the club of the event - fic_position = ClubPosition.objects.get(club=club, position='FIC') # Fetch the coordinator position - fic_user = User.objects.get(username=fic_position.name) # Get the user associated with the coordinator - gymkhana_notif( - sender=request.user, - recipient=fic_user, - notif_type="new_event_request", - club_name=str(event.club) + venue = request.data.get("venue_type") + session_poster = request.FILES.get("session_poster") + date = request.data.get("date") + start_time = request.data.get("start_time") + end_time = request.data.get("end_time") + desc = request.data.get("d_d") + + result = _conflict_algorithm_session(date, start_time, end_time, venue) + if result == "success": + Session_info.objects.create( + club=club_name, venue=venue, date=date, + start_time=start_time, end_time=end_time, + session_poster=session_poster, details=desc, ) - except (ClubPosition.DoesNotExist, User.DoesNotExist) as e: - print(f"[Notification Error]: {e}") - - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - -class FICApproveEventAPIView(APIView): - def put(self, request): - event_id = request.data.get("id") - event = get_object_or_404(Event_info, id=event_id) + recipients = User.objects.filter( + extrainfo__in=ExtraInfo.objects.filter(user_type="student") + ) + gymkhana_session(request.user, recipients, "new_session", club_name, desc, venue) + return json_response(success=True, message="Session booked successfully") + return json_response(success=False, + message="The selected time slot conflicts with an already booked session", + status_code=status.HTTP_409_CONFLICT) + except Exception as e: + logger.exception(e) + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - if event.status != "FIC": - return Response( - {"error": "Event is not under FIC review."}, - status=status.HTTP_400_BAD_REQUEST, - ) - event.status = "COUNSELLOR" - event.save() +class EditSessionAPIView(APIView): + """PUT /api/sessions//edit/ — Edit a session with conflict checking.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + def put(self, request, session_id): + club_name = _coordinator_club(request) try: - counsellor = ClubPosition.objects.get( - club=event.club, - position__in=['TECHNICAL_COUNSELLOR', 'CULTURAL_COUNSELLOR', 'SPORTS_COUNSELLOR'] - ) - recipient = User.objects.get(username=counsellor.name) - sender = request.user - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="event_approved_fic", - club_name=str(event.club), - message=event.event_name - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass - - return Response( - {"message": "Event status changed to 'Counsellor Review'."}, - status=status.HTTP_200_OK, - ) - -class CounsellorApproveEventAPIView(APIView): - def put(self, request): - event_id = request.data.get("id") - event = get_object_or_404(Event_info, id=event_id) + venue = request.data.get("venue_type") + date = request.data.get("date") + start_time = request.data.get("start_time") + end_time = request.data.get("end_time") + result = _conflict_algorithm_session(date, start_time, end_time, venue) + if result == "success": + session = Session_info.objects.get(id=session_id) + session.club = club_name + session.venue = venue + session.date = date + session.start_time = start_time + session.end_time = end_time + session.session_poster = request.FILES.get("session_poster", session.session_poster) + session.details = request.data.get("d_d") + session.save() + return json_response(success=True, message="Session updated successfully") + return json_response(success=False, + message="The selected time slot conflicts with an already booked session", + status_code=status.HTTP_409_CONFLICT) + except Session_info.DoesNotExist: + return json_response(success=False, message="Session not found", + status_code=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.exception(e) + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - if event.status != "COUNSELLOR": - return Response( - {"error": "Event is not under Counsellor review."}, - status=status.HTTP_400_BAD_REQUEST, - ) - event.status = "DEAN" - event.save() +class DeleteSessionsAPIView(APIView): + """DELETE /api/sessions/delete/ — Delete sessions by ID list.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + def delete(self, request): + ids = request.data.get("ids", []) try: - print("hi") - recipient = User.objects.get(username="mkroy") - print(recipient) - sender = request.user - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="event_approved_counsellor", - club_name=str(event.club), - message=event.event_name - ) - except User.DoesNotExist: - pass + for sid in ids: + Session_info.objects.get(id=sid).delete() + return json_response(success=True, message="Sessions deleted") + except Exception: + return json_response(success=False, message="An error was encountered", + status_code=status.HTTP_400_BAD_REQUEST) - return Response( - {"message": "Event status changed to 'Dean Review'."}, - status=status.HTTP_200_OK, - ) +class DateSessionsAPIView(APIView): + """POST /api/sessions/by-date/ — Get sessions on a specific date.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -from notification.views import create_announcement # Import your existing function for creating announcements -from applications.globals.models import User -from notification.models import Announcements + def post(self, request): + date = request.data.get("date") + sessions = Session_info.objects.filter(date=date).order_by("start_time") + data = django_serializers.serialize("json", list(sessions)) + return json_response(success=True, data=json.loads(data)) -class DeanApproveEventAPIView(APIView): - def put(self, request): - event_id = request.data.get("id") - event = get_object_or_404(Event_info, id=event_id) - if event.status != "DEAN": - return Response( - {"error": "Event is not under Dean review."}, - status=status.HTTP_400_BAD_REQUEST, - ) +# --------------------------------------------------------------------------- +# Budget Endpoints +# --------------------------------------------------------------------------- - event.status = "ACCEPT" - event.save() +class ClubBudgetAPIView(APIView): + """POST /api/budget/club/ — Submit a club budget request.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + def post(self, request): try: - coordinator_position = ClubPosition.objects.get(club=event.club, position='COORDINATOR') - coordinator_user = User.objects.get(username=coordinator_position.name) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - return Response( - {"error": "Coordinator not found for this club."}, - status=status.HTTP_404_NOT_FOUND, - ) - - announcement = Announcements.objects.create( - created_by=coordinator_user, - message=f"Get ready! '{event.event_name}' by {event.club} is happening on {event.start_date}. Stay tuned for more details!", - target_group='students', - department=None, - batch=None, - upload_announcement=None, - module="GymKhana" - ) - - return Response( - {"message": "Event approved and announcement sent to all students."}, - status=status.HTTP_200_OK, - ) - - - -class NewBudgetAPIView(APIView): - def put(self, request): - request.data["status"] = "FIC" - serializer = BudgetSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - try: - budget = serializer.instance - club = budget.club - fic = ClubPosition.objects.get(club=club, position='FIC') - fic_user = User.objects.get(username=fic.name) - gymkhana_notif( - sender=request.user, - recipient=fic_user, - notif_type="new_budget_request", - message=budget.id, - club_name=str(club) - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass + club = request.data.get("club") + budget_for = request.data.get("budget_for") + budget_amount = request.data.get("amount") + budget_file = request.FILES.get("budget_file") + description = request.data.get("d_d") + budget_file.name = club + "_budget" + club_obj = get_object_or_404(Club_info, club_name=club) + Club_budget.objects.create( + club_id=club_obj, budget_amt=budget_amount, budget_file=budget_file, + budget_for=budget_for, description=description, status="open", + ) + return json_response(success=True, message="Budget request submitted successfully") + except Exception as e: + logger.exception(e) + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class FestBudgetAPIView(APIView): + """POST /api/budget/fest/ — Submit a fest budget.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class FICApproveBudgetAPIView(APIView): - def put(self, request): - budget_id = request.data.get("id") - budget = get_object_or_404(Budget, id=budget_id) - if budget.status != "FIC": - return Response( - {"error": "Budget is not under FIC review."}, - status=status.HTTP_400_BAD_REQUEST, - ) - budget.status = "COUNSELLOR" - budget.save() + def post(self, request): try: - counsellor = ClubPosition.objects.get( - club=budget.club, - position__in=['TECHNICAL_COUNSELLOR', 'CULTURAL_COUNSELLOR', 'SPORTS_COUNSELLOR'] - ) - recipient = User.objects.get(username=counsellor.name) - sender = request.user - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="budget_approved_fic", - message=budget.id, - club_name=str(budget.club) - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass - - return Response( - {"message": "Budget status changed to 'Counsellor Review'."}, - status=status.HTTP_200_OK, - ) + fest = request.data.get("fest") + budget_amt = request.data.get("amount") + budget_file = request.FILES.get("file") + desc = request.data.get("d_d") + year = request.data.get("year") + budget_file.name = fest + "_budget_" + year + Fest_budget.objects.create( + fest=fest, budget_amt=budget_amt, budget_file=budget_file, + description=desc, year=year, + ) + return json_response(success=True, message="Fest budget uploaded successfully") + except Exception as e: + logger.exception(e) + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) -class CounsellorApproveBudgetAPIView(APIView): - def put(self, request): - budget_id = request.data.get('id') - budget = get_object_or_404(Budget, id=budget_id) - if budget.status not in ['COUNSELLOR', 'REREVIEW']: - return Response({"error": "Budget is not under Counsellor review."}, status=status.HTTP_400_BAD_REQUEST) +class BudgetApproveAPIView(APIView): + """POST/PUT /api/budget/approve/ — Approve a club budget by ID.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] - new_status = 'ACCEPT' if budget.status == 'REREVIEW' else 'DEAN' - serializer = BudgetSerializer(budget, data=request.data, partial=True) - if serializer.is_valid(): - serializer.save(status=new_status) - - if new_status == 'DEAN': - try: - dean_user = User.objects.get(username="mkroy") - gymkhana_notif( - sender=request.user, - recipient=dean_user, - notif_type="budget_approved_counsellor", - message=budget.id, - club_name=str(budget.club) - ) - except User.DoesNotExist: - pass - - return Response({"message": "Budget updated and status changed"}, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class DeanApproveBudgetAPIView(APIView): - def put(self, request): + def _approve(self, request): budget_id = request.data.get("id") - budget = get_object_or_404(Budget, id=budget_id) - if budget.status != "DEAN": - return Response( - {"error": "Budget is not under Dean review."}, - status=status.HTTP_400_BAD_REQUEST, - ) - budget.status = "ACCEPT" - budget.save() + if not budget_id: + return json_response(success=False, message="Budget ID is required", + status_code=status.HTTP_400_BAD_REQUEST) try: - coordinator = ClubPosition.objects.get(club=budget.club, position='COORDINATOR') - recipient = User.objects.get(username=coordinator.name) - sender = request.user - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="budget_approved_dean", - message=budget.id, - club_name=str(budget.club) - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass + budget = Club_budget.objects.get(id=budget_id) + budget.status = "confirmed" + budget.save() + return json_response(success=True, message="Budget approved") + except Club_budget.DoesNotExist: + return json_response(success=False, message="Budget not found", + status_code=status.HTTP_404_NOT_FOUND) - return Response( - {"message": "Budget status changed to 'Accepted'."}, - status=status.HTTP_200_OK, - ) + def post(self, request): + return self._approve(request) -class DeanReviewBudgetAPIView(APIView): def put(self, request): - budget_id = request.data.get('id') - budget = get_object_or_404(Budget, id=budget_id) + return self._approve(request) - if budget.status != 'DEAN': - return Response({"error": "Budget is not under Dean review."}, status=status.HTTP_400_BAD_REQUEST) - serializer = BudgetSerializer(budget, data=request.data, partial=True) - if serializer.is_valid(): - serializer.save(status='REREVIEW') - return Response({"message": "Budget updated and status changed to 'Dean Review'."}, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class BudgetRejectAPIView(APIView): + """POST/PUT /api/budget/reject/ — Reject a club budget by ID.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class RejectBudgetAPIView(APIView): - def put(self, request): + def _reject(self, request): budget_id = request.data.get("id") - budget = get_object_or_404(Budget, id=budget_id) - budget.status = "REJECT" - budget.save() - - try: - coordinator = ClubPosition.objects.get(club=budget.club, position='COORDINATOR') - recipient = User.objects.get(username=coordinator.name) - sender = request.user - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="budget_rejected", - club_name=str(budget.club), - message=f"Budget ID #{budget.id}" - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass - - return Response( - {"message": "Budget status changed to 'Rejected'."}, - status=status.HTTP_200_OK, - ) - - - - -class AchievementsAPIView(APIView): - def post(self, request): - club_name = request.data.get("club_name") - achievements = Achievements.objects.filter(club_name=club_name) - if not achievements.exists(): - return Response( - {"message": "No achievements found for this club."}, status=404 - ) - - serializer = AchievementsSerializer(achievements, many=True) - return Response(serializer.data, status=200) - - -class AddAchievementAPIView(APIView): - def post(self, request): - serializer = AchievementsSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class CreateBudgetCommentAPIView(APIView): - def post(self, request): - data = request.data.copy() - data["comment_date"] = timezone.now().date() - data["comment_time"] = timezone.now().time() - - serializer = Budget_CommentsSerializer(data=data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class CreateEventCommentAPIView(APIView): - def post(self, request): - data = request.data.copy() - data["comment_date"] = timezone.now().date() - data["comment_time"] = timezone.now().time() - - serializer = Event_CommentsSerializer(data=data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class ListBudgetCommentsAPIView(APIView): - def post(self, request): - budget_id = request.data.get("budget_id") if not budget_id: - return Response( - {"error": "Budget ID is required."}, status=status.HTTP_400_BAD_REQUEST - ) - - comments = Budget_Comments.objects.filter(budget_id=budget_id).order_by( - "comment_date", "comment_time" - ) - serializer = Budget_CommentsSerializer(comments, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - + return json_response(success=False, message="Budget ID is required", + status_code=status.HTTP_400_BAD_REQUEST) + try: + budget = Club_budget.objects.get(id=budget_id) + budget.status = "rejected" + budget.save() + return json_response(success=True, message="Budget rejected") + except Club_budget.DoesNotExist: + return json_response(success=False, message="Budget not found", + status_code=status.HTTP_404_NOT_FOUND) -class ListEventCommentsAPIView(APIView): def post(self, request): - event_id = request.data.get("event_id") - if not event_id: - return Response( - {"error": "Event ID is required."}, status=status.HTTP_400_BAD_REQUEST - ) + return self._reject(request) - comments = Event_Comments.objects.filter(event_id=event_id).order_by( - "comment_date", "comment_time" - ) - serializer = Event_CommentsSerializer(comments, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class RejectEventAPIView(APIView): def put(self, request): - event_id = request.data.get("id") - event = get_object_or_404(Event_info, id=event_id) - event.status = "REJECT" - event.save() - try: - coordinator = ClubPosition.objects.get(club=event.club, position='COORDINATOR') - recipient = User.objects.get(username=coordinator.name) - sender = request.user - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="event_rejected", - club_name=str(event.club), - message=event.event_name - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass - return Response( - {"message": "Event status changed to 'Rejected'."}, - status=status.HTTP_200_OK, - ) + return self._reject(request) -class ModifyEventAPIView(APIView): - def put(self, request): - event_id = request.data.get("id") - event = get_object_or_404(Event_info, id=event_id) - event.status = "COORDINATOR" - event.save() - try: - coordinator = ClubPosition.objects.get(club=event.club, position='COORDINATOR') - recipient = User.objects.get(username=coordinator.name) - sender = request.user - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="event_modified", - club_name=str(event.club), - message=event.event_name - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass - return Response( - {"message": "Event status changed to 'Rejected'."}, - status=status.HTTP_200_OK, - ) - return Response( - {"message": "Event status changed to 'Coordinator review'."}, - status=status.HTTP_200_OK, - ) - - -class ModifyBudgetAPIView(APIView): - def put(self, request): - budget_id = request.data.get("id") - budget = get_object_or_404(Budget, id=budget_id) - budget.status = "COORDINATOR" - budget.save() +class UpdateBudgetAmountAPIView(APIView): + """POST/PUT /api/budget/update-amount/ — Update or deduct a club budget amount.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] - try: - coordinator = ClubPosition.objects.get(club=budget.club, position='COORDINATOR') - recipient = User.objects.get(username=coordinator.name) - sender = request.user - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="budget_modified", - club_name=str(budget.club), - message=f"Budget ID #{budget.id}" - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass + def _update(self, request): + # Accept 'id' (sent by frontend) or 'budget_id' (legacy) + budget_id = request.data.get("id") or request.data.get("budget_id") + req_id = request.data.get("req_id") + new_budget = request.data.get("new_budget") - return Response( - {"message": "Budget status changed to 'Coordinator Review'."}, - status=status.HTTP_200_OK, - ) + try: + budget = Club_budget.objects.get(id=budget_id) + if req_id == "spent": + new_budget = float(new_budget) + if new_budget > budget.budget_amt: + return json_response(success=False, + message="Spent amount cannot be greater than available amount", + status_code=status.HTTP_400_BAD_REQUEST) + budget.budget_amt -= new_budget + else: + budget.budget_amt = new_budget + budget.save() + return json_response(success=True, message="Budget updated successfully") + except Club_budget.DoesNotExist: + return json_response(success=False, message="Budget not found", + status_code=status.HTTP_404_NOT_FOUND) + def post(self, request): + return self._update(request) -class RejectMemberAPIView(APIView): def put(self, request): - member_id = request.data.get("id") - member = get_object_or_404(Club_member, id=member_id) - member.status = "rejected" - member.save() - return Response( - {"message": "Member status changed to 'rejected'."}, - status=status.HTTP_200_OK, - ) + return self._update(request) -class AddClubPositionAPIView(APIView): - def post(self, request): - serializer = ClubPositionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +# --------------------------------------------------------------------------- +# Report Endpoints +# --------------------------------------------------------------------------- +class ClubReportAPIView(APIView): + """POST /api/reports/club/ — Submit a club event report.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class ListClubPositionAPIView(APIView): def post(self, request): - name = request.data.get("name") - positions = ClubPosition.objects.filter(name=name) - serializer = ClubPositionSerializer(positions, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) -class ListAllClubPositionAPIView(APIView): - def get(self, request): - positions = ClubPosition.objects.all() - serializer = ClubPositionSerializer(positions, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class UpdateEventAPIView(APIView): - def put(self, request): try: - # Fetch the event to be updated - pk = request.data.get("id") - event = Event_info.objects.get(pk=pk) - except Event_info.DoesNotExist: - return Response( - {"error": "Event not found"}, status=status.HTTP_404_NOT_FOUND - ) - - # Partial update for 'details' and 'event_poster' - data = {} - if "details" in request.data: - data["details"] = request.data["details"] - if "event_poster" in request.FILES: - data["event_poster"] = request.FILES["event_poster"] - data["status"] = "FIC" + club = request.data.get("club") + user = request.data.get("s_inc") + event = request.data.get("event") + d_d = request.data.get("d_d") + date = request.data.get("date") + time = request.data.get("time") + report = request.FILES.get("report") + + if not date or not time: + return json_response(success=False, message="Both date and time are required", + status_code=status.HTTP_400_BAD_REQUEST) + + report.name = club + "_" + event + "_report" + parts = user.split(" - ") + user_obj = get_object_or_404(User, username=parts[1]) + extra = get_object_or_404(ExtraInfo, id=parts[0], user=user_obj) + club_obj = get_object_or_404(Club_info, club_name=club) + + Club_report.objects.create( + club=club_obj, incharge=extra, event_name=event, + date=date + " " + time, event_details=report, description=d_d, + ) + return json_response(success=True, message="Report updated successfully") + except Exception as e: + logger.exception(e) + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - # Create serializer instance with partial=True to allow partial updates - serializer = event_infoserializer(event, data=data, partial=True) - # Validate and update - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +# --------------------------------------------------------------------------- +# Registration / Form Endpoints +# --------------------------------------------------------------------------- +class RegistrationFormAPIView(APIView): + """POST /api/registration/ — Submit a club registration form.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class UpdateBudgetAPIView(APIView): - def put(self, request): + def post(self, request): try: - # Fetch the event to be updated - pk = request.data.get("id") - budget = Budget.objects.get(pk=pk) - except Budget.DoesNotExist: - return Response( - {"error": "Event not found"}, status=status.HTTP_404_NOT_FOUND + user_name = request.data.get("user_name") + roll = request.data.get("roll") + cpi = request.data.get("cpi") + branch = request.data.get("branch") + programme = request.data.get("programme") + Registration_form.objects.create( + user_name=user_name, branch=branch, roll=roll, cpi=cpi, programme=programme, ) + return json_response(success=True, message="The form has been dispatched for further process") + except Exception: + return json_response(success=False, message="You have already filled the form", + status_code=status.HTTP_400_BAD_REQUEST) - # Partial update for 'details' and 'event_poster' - data = {} - if "budget_amt" in request.data: - data["budget_amt"] = request.data["budget_amt"] - if "remarks" in request.data: - data["remarks"] = request.data["remarks"] - if "budget_file" in request.FILES: - data["budget_file"] = request.FILES["budget_file"] - data["status"] = "FIC" - - # Create serializer instance with partial=True to allow partial updates - serializer = BudgetSerializer(budget, data=data, partial=True) - - # Validate and update - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -class FreeMembersForClubAPIView(APIView): - def get(self, request): - club_id = request.data.get('club_id') # Use query_params for GET request - if not club_id: - return Response({"error": "Club id is required."}, status=status.HTTP_400_BAD_REQUEST) +class FormAvailAPIView(APIView): + """POST /api/registration/form-availability/ — Toggle form availability.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + def post(self, request): try: - # Get upcoming events for the club - events = Event_info.objects.filter(club_id=club_id, start_date__gte =timezone.now().date()) - - # Map incharge members to their events - incharge_map = {} - for event in events: - if event.incharge: # Ensure incharge is not None - incharge_map[str(event.incharge)] = event.event_name - - # Get all club members - members = Club_member.objects.filter(club_id=club_id) - - # Prepare the response data - response_data = [] - for memb in members: - roll_no = str(memb.member_id) # Ensure same type as incharge_map keys - response_data.append({ - "roll_no": roll_no, - "event_name": incharge_map.get(roll_no, None) # Set event_name or None - }) - - return Response(response_data, status=status.HTTP_200_OK) - + form_name = request.data.get("registration") + available = request.data.get("available") + is_available = available == "On" + roll = request.user.username + + rob = Form_available.objects.get(roll=roll) + if rob.form_name != form_name: + Registration_form.objects.all().delete() + rob.form_name = form_name + rob.status = is_available + rob.save() + return json_response(success=True, message="Form availability updated") except Exception as e: - return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + logger.exception(e) + return json_response(success=False, message="You've already filled the form", + status_code=status.HTTP_400_BAD_REQUEST) -class CoordinatorEventsAPIView(APIView): - """ - API View to fetch events for clubs where the given person (by roll number) is a coordinator. - Filters by accepted events and those in the current month. - """ - def post(self, request): - # Extract roll number from the request data - roll_number = request.data.get("roll_number") - if not roll_number: - return Response( - {"error": "Roll number is required."}, - status=status.HTTP_400_BAD_REQUEST, - ) +class DeleteRequestsAPIView(APIView): + """DELETE /api/registration/delete-requests/ — Delete all registration form records.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + def delete(self, request): try: - clubs = Club_info.objects.filter(co_ordinator=roll_number) - # Get the current month and year - current_month = datetime.datetime.now().month - current_year = datetime.datetime.now().year - - # Fetch events associated with those clubs, with status 'accepted' and within the same month - events = Event_info.objects.filter( - club__in=clubs, - # status="Accepted", # Replace with your actual status choice - # start_date__year=current_year, - # start_date__month=current_month, - ) + Registration_form.objects.all().delete() + return json_response(success=True, message="Data deleted") + except Exception: + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - # Serialize and return the events - serializer = event_infoserializer(events, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Student.DoesNotExist: - return Response( - {"error": "Student not found with the given roll number."}, - status=status.HTTP_404_NOT_FOUND, - ) +# --------------------------------------------------------------------------- +# Data / Lookup Endpoints +# --------------------------------------------------------------------------- -class EventInputAPIView(APIView): - def get(self, request): - events = Event_info.objects.all() - events_data = [{"id": event.id, "name": event.event_name} for event in events] - return Response(events_data, status=status.HTTP_200_OK) +class FacultyDataAPIView(APIView): + """POST /api/data/faculty/ — Search faculty by name.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] def post(self, request): - serializer = EventInputSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - - Announcements.objects.create( - created_by=request.user, - message="📰 The Gymkhana Newsletter has been updated! Dive into the latest club highlights and event stories now.", - target_group='students', - module='Gymkhana' - ) - - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -#helper -def add_page_decorations(canvas, doc): - canvas.saveState() - page_num = canvas.getPageNumber() - canvas.setFont('Helvetica', 10) - canvas.drawCentredString(letter[0] / 2.0, 20, f"Page {page_num}") - canvas.restoreState() - -class NewsletterPDFAPIView(APIView): - def get(self, request): - # Determine timeframe filter based on query parameter - timeframe = request.GET.get('timeframe', '').lower() - now = timezone.now() - if timeframe == 'weekly': - time_threshold = (now - timedelta(weeks=1)) - elif timeframe == 'monthly': - time_threshold = now - timedelta(days=30) - elif timeframe == '6 months': - time_threshold = now - timedelta(days=182) # Approximation for half a year - else: - time_threshold = None - print(time_threshold,now) - # Fetch all unique clubs - clubs = Event_info.objects.values_list('club', flat=True).distinct() - has_events = False - print(clubs) - for club in clubs: - club_events = EventInput.objects.filter(event__club=club) - if time_threshold: - club_events = club_events.filter(event__end_date__range=(time_threshold, now)) - print(club_events) - if club_events.exists(): - has_events = True - break - - if not has_events: - return Response({"message": "No events found for the selected timeframe."}, status=status.HTTP_404_NOT_FOUND) - - # Create an in-memory file - buffer = BytesIO() - doc = SimpleDocTemplate(buffer, pagesize=letter) - - # Get the default style sheet and create custom styles - styles = getSampleStyleSheet() - story = [] - - # --- Banner Section --- - banner_path = "path/to/your/banner.jpg" # Update this path to your banner image + current_value = request.data.get("current_value", "") try: - banner = Image(banner_path, width=letter[0], height=150) - story.append(banner) + faculty = ExtraInfo.objects.filter(user_type="faculty") + names = [] + for lecturer in faculty: + name = lecturer.user.first_name + " " + lecturer.user.last_name + if not current_value or current_value.lower() in name.lower(): + names.append(name) + return json_response(success=True, data=names) except Exception: - pass - - story.append(Spacer(1, 20)) - - # Catchy Title and Tagline - title_style = ParagraphStyle( - name='TitleStyle', - parent=styles['Title'], - fontName='Helvetica-Bold', - fontSize=26, - leading=30, - alignment=1, - textColor=colors.darkblue - ) - tagline_style = ParagraphStyle( - name='Tagline', - parent=styles['BodyText'], - fontName='Helvetica-Oblique', - fontSize=14, - leading=18, - alignment=1, - textColor=colors.darkgray - ) - - story.append(Paragraph("IIITDM Jabalpur Gymkhana Newsletter", title_style)) - story.append(Spacer(1, 10)) - story.append(Paragraph("Stay tuned for the latest happenings and exclusive updates!", tagline_style)) - story.append(Spacer(1, 30)) - - # Introductory paragraph - intro_style = ParagraphStyle( - name='Intro', - parent=styles['BodyText'], - fontSize=12, - leading=16, - alignment=1, - textColor=colors.black - ) - intro_text = ( - "Welcome to our monthly newsletter where we bring you the most exciting events from various clubs. " - "Dive into details, get inspired, and mark your calendars for a memorable experience!" - ) - story.append(Paragraph(intro_text, intro_style)) - story.append(Spacer(1, 40)) - - # --- Newsletter Content --- - club_header_style = ParagraphStyle( - name='ClubHeader', - fontName='Helvetica-Bold', - fontSize=18, - leading=22, - textColor=colors.darkred, - backColor=colors.whitesmoke, - spaceAfter=10, - borderPadding=(5, 5, 5, 5) - ) - - event_heading_style = ParagraphStyle( - name='EventHeading', - fontName='Helvetica-Bold', - fontSize=14, - leading=18, - textColor=colors.darkgreen - ) - - body_text_style = ParagraphStyle( - name='BodyText', - parent=styles['BodyText'], - fontSize=12, - leading=15, - textColor=colors.black - ) - - italic_style = ParagraphStyle( - name='Italic', - parent=styles['BodyText'], - fontName='Helvetica-Oblique', - fontSize=12, - leading=15, - textColor=colors.gray - ) - - for club in clubs: - story.append(Paragraph(f"Club: {club}", club_header_style)) - story.append(Spacer(1, 20)) - - club_events = EventInput.objects.filter(event__club=club) - if time_threshold: - club_events = club_events.filter(event__end_date__range=(time_threshold, now)) - - for event in club_events: - event_info = event.event - - story.append(HRFlowable(width="100%", thickness=1, color=colors.lightgrey)) - story.append(Spacer(1, 10)) - story.append(Paragraph("Event Details", event_heading_style)) - story.append(Spacer(1, 10)) - - story.append(Paragraph(f"Event: {event_info.event_name}", body_text_style)) - story.append(Spacer(1, 10)) - - story.append(Paragraph( - f"Start Date: {event_info.start_date.strftime('%B %d, %Y')}", - body_text_style)) - story.append(Spacer(1, 10)) - - story.append(Paragraph( - f"Start Time: {event_info.start_time.strftime('%I:%M %p')}", - body_text_style)) - story.append(Spacer(1, 10)) - - story.append(Paragraph( - f"Venue: {event_info.venue}", - body_text_style)) - story.append(Spacer(1, 10)) - - story.append(Paragraph("Description:", event_heading_style)) - story.append(Paragraph(f"{event.description}", body_text_style)) - story.append(Spacer(1, 10)) - - if event.images: - image_path = event.images.path - try: - story.append(Image(image_path, width=200, height=150)) - except Exception: - story.append(Paragraph("[Image could not be loaded]", body_text_style)) - else: - story.append(Paragraph("[Image Placeholder]", body_text_style)) - - story.append(Spacer(1, 10)) - story.append(Paragraph( - "Additional Information: Stay tuned for more updates and behind-the-scenes insights!", - italic_style)) - story.append(Spacer(1, 30)) + return json_response(success=False, message="Error fetching faculty", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - story.append(PageBreak()) - doc.build(story, onFirstPage=add_page_decorations, onLaterPages=add_page_decorations) - buffer.seek(0) - - return FileResponse(buffer, as_attachment=True, filename="newsletter.pdf") +class StudentsDataAPIView(APIView): + """POST /api/data/students/ — Search students by roll number prefix.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] -class EventReportAPIView(APIView): def post(self, request): - data = request.data.copy() - event_id = data.get("event") - - if isinstance(event_id, str) and event_id.isdigit(): - event_id = int(event_id) - + current_value = request.data.get("current_value", "") try: - event_instance = Event_info.objects.get(pk=event_id) - data["event"] = event_instance.pk # Assigning the ID, not the instance - except Event_info.DoesNotExist: - return Response({"error": "Invalid event ID"}, status=status.HTTP_400_BAD_REQUEST) - - serializer = EventReportSerializer(data=data) - if serializer.is_valid(): - event_report = serializer.save() - - # Generate PDF using ReportLab - buffer = BytesIO() - doc = SimpleDocTemplate(buffer, pagesize=letter) - elements = [] - - styles = getSampleStyleSheet() - title_style = ParagraphStyle( - "TitleStyle", - parent=styles["Title"], - fontSize=16, - textColor=colors.darkblue, - alignment=1, # Center alignment - ) - normal_style = styles["Normal"] - - elements.append(Paragraph(f"Event Report for {event_instance.event_name}", title_style)) - elements.append(HRFlowable(width="100%", thickness=1, color=colors.black)) - elements.append(Spacer(1, 12)) - elements.append(Paragraph(f"Club: {event_instance.club}", normal_style)) - elements.append(Paragraph(f"Venue: {event_report.venue}", normal_style)) - elements.append(Paragraph(f"Incharge: {event_report.incharge}", normal_style)) - elements.append(Paragraph(f"Start Date: {event_report.start_date}", normal_style)) - elements.append(Paragraph(f"End Date: {event_report.end_date}", normal_style)) - elements.append(Paragraph(f"Start Time: {event_report.start_time}", normal_style)) - elements.append(Paragraph(f"End Time: {event_report.end_time}", normal_style)) - elements.append(Paragraph(f"Budget: {event_report.event_budget}", normal_style)) - elements.append(Paragraph(f"Special Announcement: {event_report.special_announcement or 'None'}", normal_style)) - elements.append(PageBreak()) - - doc.build(elements) - buffer.seek(0) - - file_name = f"event_report_{event_report.id}.pdf" - event_report.report_pdf.save(file_name, ContentFile(buffer.getvalue()), save=True) - try: - counsellor = ClubPosition.objects.get( - club=event_instance.club, - position__in=[ - 'TECHNICAL_COUNSELLOR', - 'CULTURAL_COUNSELLOR', - 'SPORTS_COUNSELLOR' - ] - ) - recipient = User.objects.get(username=counsellor.name) - sender = request.user - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="event_report_submitted", - club_name=str(event_instance.club), - message=event_instance.event_name - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass + students = ExtraInfo.objects.filter(user_type="student", id__startswith=current_value) + data = json.loads(django_serializers.serialize("json", list(students))) + return json_response(success=True, data=data) + except Exception: + return json_response(success=False, message="Error fetching students", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - return Response(serializer.data, status=status.HTTP_201_CREATED) +class GetVenueAPIView(APIView): + """POST /api/data/venues/ — Get venues by type.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -class EventReportListAPIView(APIView): - def get(self, request): - club_id = request.query_params.get("club") - if not club_id: - return Response({"error": "Club ID is required"}, status=status.HTTP_400_BAD_REQUEST) - print(club_id) - events = Event_info.objects.filter(club=club_id) - print(events) - event_reports = EventReport.objects.filter(event_id__in=events) - serializer = EventReportSerializer(event_reports, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) -class EventReportPDFView(APIView): - def get(self, request, report_id): - try: - event_report = EventReport.objects.get(pk=report_id) - if not event_report.report_pdf: - return Response({"error": "No PDF report available for this event"}, status=status.HTTP_404_NOT_FOUND) + def post(self, request): + selected = (request.data.get("venueType") or "").strip() + venue_details = {} + venue_types = [] + idd = 0 + for rooms in Constants.venue: + for room in rooms: + if idd % 2 == 0: + venue_types.append(room) + else: + venue_details[venue_types[int(idd / 2)]] = [v[0] for v in room] + idd += 1 + result = [v.strip() for v in venue_details.get(selected, [])] + return json_response(success=True, data=result) - return FileResponse(event_report.report_pdf.open('rb'), content_type='application/pdf') - - except EventReport.DoesNotExist: - return Response({"error": "Event report not found"}, status=status.HTTP_404_NOT_FOUND) -from datetime import datetime +# --------------------------------------------------------------------------- +# Voting Endpoints +# --------------------------------------------------------------------------- -class UploadYearlyPlanExcelAPIView(APIView): - parser_classes = [MultiPartParser] +class VotingPollAPIView(APIView): + """POST /api/voting/polls/ — Create a new voting poll.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] def post(self, request): - club_name = request.data.get("club") - if not club_name: - return Response({"error": "Club name is required."}, status=status.HTTP_400_BAD_REQUEST) - - club = get_object_or_404(Club_info, club_name=club_name) - no_file_id = request.data.get("file_id") - Coress_file = File.objects.filter(id=no_file_id).first() - # Get uploaded Excel file - file = request.FILES.get("file") - if not file: - return Response({"error": "No Excel file provided."}, status=status.HTTP_400_BAD_REQUEST) - - # Save the file to default storage - file_name = f"yearly_plan_{club_name}_{datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx" - file_path = f"gymkhana/year_planners/{file_name}" - default_storage.save(file_path, file) - + if _coordinator_club(request) is None: + return json_response(success=False, + message="Unauthorized: only club coordinators can create polls.", + status_code=status.HTTP_403_FORBIDDEN) try: - # Read and process the saved file - with default_storage.open(file_path, mode="rb") as temp_file: - workbook = load_workbook(temp_file) - sheet = workbook.active - - # Build file URL - file_url = request.build_absolute_uri(default_storage.url(file_path)) - - # Create YearlyPlan DB entry - yearly_plan = YearlyPlan.objects.create( - club=club, - year=datetime.now().year, - file_link=file_url, - status="FIC", - file_id=Coress_file - ) - - # Process rows - for row in sheet.iter_rows(min_row=2, values_only=True): - event_name, tentative_start_date, tentative_end_date, budget, description = row - - if not all([event_name, tentative_start_date, tentative_end_date, budget]): - return Response({"error": "Missing required fields in some rows."}, status=status.HTTP_400_BAD_REQUEST) - - try: - tentative_start_date = self._parse_date(tentative_start_date) - tentative_end_date = self._parse_date(tentative_end_date) - except ValueError as e: - return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) - YearlyPlanEvents.objects.create( - yearly_plan=yearly_plan, - event_name=event_name, - tentative_start_date=tentative_start_date, - tentative_end_date=tentative_end_date, - budget=budget, - description=description - ) - - # Notify FIC - try: - fic = ClubPosition.objects.get(club=club, position="FIC") - fic_user = User.objects.get(username=fic.name) - gymkhana_notif( - sender=request.user, - recipient=fic_user, - notif_type="new_yearly_plan", - message=f"Yearly plan for {club_name}", - club_name=club_name, - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - return Response({"error": "FIC for the club not found."}, status=status.HTTP_404_NOT_FOUND) - - return Response({ - "message": "Yearly Plan and Events uploaded and saved successfully.", - "file_link": file_url - }, status=status.HTTP_201_CREATED) - - except Exception as e: - return Response({"error": f"An error occurred: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - @staticmethod - def _parse_date(date_value): - """ - Attempts to parse a date value from different formats (e.g., dd/mm/yyyy, mm/dd/yyyy). - Returns a date object in yyyy-mm-dd format if successful. - """ - if isinstance(date_value, datetime): - return date_value.date() - if isinstance(date_value, str): - for fmt in ("%d/%m/%Y", "%m/%d/%Y", "%Y-%m-%d"): - try: - return datetime.strptime(date_value, fmt).date() - except ValueError: + title = request.data.get("title") + description = request.data.get("desc") + choices = request.data.getlist("choices") + exp_date = request.data.get("expire_date") + groups = request.data.getlist("groups") + + if len(choices) < 2: + return json_response(success=False, message="A poll must have at least 2 choices.", + status_code=status.HTTP_400_BAD_REQUEST) + + target_groups = _get_target_user(groups) + created_by = f"{request.user.first_name} {request.user.last_name}:{request.user}" + poll = Voting_polls.objects.create( + title=title, description=description, exp_date=exp_date, + created_by=created_by, groups=target_groups, + ) + for choice in choices: + Voting_choices.objects.create(poll_event=poll, title=choice, votes=0) + + for entry in groups: + if ":" not in entry: continue - raise ValueError(f"Invalid date format: {date_value}") + batch, branch = [v.strip() for v in entry.split(":", 1)] + allbatch = User.objects.filter(username__contains=batch) + if branch == "All": + gymkhana_voting(request.user, allbatch, "voting_open", title, description) + else: + selbranch = ExtraInfo.objects.filter(department__name=branch) + batchbranch = User.objects.filter(username__contains=batch, extrainfo__in=selbranch) + gymkhana_voting(request.user, batchbranch, "voting_open", title, description) + return json_response(success=True, message="Poll created successfully") + except Exception as e: + logger.exception(e) + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) -class DownloadYearlyPlanTemplateAPIView(APIView): - def get(self, request): - # Create workbook - wb = Workbook() - ws = wb.active - ws.title = "YearlyPlanEvents" - - # Add headers - headers = ["Event Name", "Tentative Start Date (YYYY-MM-DD)", "Tentative End Date (YYYY-MM-DD)", - "Budget", "Description"] - ws.append(headers) - - # Create response for download - response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') - response['Content-Disposition'] = 'attachment; filename=YearlyPlan_Template.xlsx' - wb.save(response) - - return response -class FICApproveYearlyPlanAPIView(APIView): - def put(self, request): - plan_id = request.data.get("id") - yearly_plan = get_object_or_404(YearlyPlan, id=plan_id) - - if yearly_plan.status != "FIC": - return Response( - {"error": "Yearly Plan is not under FIC review."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Change the status to "Counsellor" - yearly_plan.status = "COUNSELLOR" - yearly_plan.save() +class VoteAPIView(APIView): + """POST /api/voting/polls//vote/ — Cast a vote on a poll.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] - # Send notification to Counsellor + def post(self, request, poll_id): + from django.db import models as django_models + poll = get_object_or_404(Voting_polls, pk=poll_id) try: - print(1) - print(2) - counsellor = ClubPosition.objects.get( - club=yearly_plan.club, - position__in=['TECHNICAL_COUNSELLOR', 'CULTURAL_COUNSELLOR', 'SPORTS_COUNSELLOR'] - ) - print(counsellor.name) - recipient = User.objects.get(username=counsellor.name) - sender = request.user - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="yearly_plan_approved_fic", - message=yearly_plan.id, - club_name=str(yearly_plan.club) - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass - - return Response( - {"message": "Yearly Plan status changed to 'Counsellor Review'."}, - status=status.HTTP_200_OK, - ) -class CounsellorApproveYearlyPlanAPIView(APIView): - def put(self, request): - plan_id = request.data.get('id') - yearly_plan = get_object_or_404(YearlyPlan, id=plan_id) - - # Check if the current status is either "COUNSELLOR" or "REREVIEW" - if yearly_plan.status not in ['COUNSELLOR', 'REREVIEW']: - return Response({"error": "Yearly Plan is not under Counsellor review."}, status=status.HTTP_400_BAD_REQUEST) - - # Determine new status: "DEAN" if "COUNSELLOR", "ACCEPT" if "REREVIEW" - new_status = 'DEAN' if yearly_plan.status == 'COUNSELLOR' else 'ACCEPT' - yearly_plan.status = new_status - yearly_plan.save() - - # Send notification to Dean if status changes to "DEAN" - if new_status == 'DEAN': - try: - dean_user = User.objects.get(username="mkroy") # Example Dean user - gymkhana_notif( - sender=request.user, - recipient=dean_user, - notif_type="yearly_plan_approved_counsellor", - message=yearly_plan.id, - club_name=str(yearly_plan.club) + target = json.loads(poll.groups) if poll.groups else {} + if target: + extra = get_object_or_404(ExtraInfo, user=request.user) + student_batch = str(request.user.username)[:4] + student_branch = extra.department.name if extra.department else None + allowed = any( + batch == student_batch and ("All" in branches or student_branch in branches) + for batch, branches in target.items() ) - except User.DoesNotExist: - pass - - return Response({"message": "Yearly Plan status updated."}, status=status.HTTP_200_OK) -class DeanApproveYearlyPlanAPIView(APIView): - def put(self, request): - plan_id = request.data.get("id") - yearly_plan = get_object_or_404(YearlyPlan, id=plan_id) - - if yearly_plan.status != "DEAN": - return Response( - {"error": "Yearly Plan is not under Dean review."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Change the status to "Approved" - yearly_plan.status = "ACCEPT" - yearly_plan.save() + if not allowed: + return json_response(success=False, message="You are not eligible to vote in this poll.", + status_code=status.HTTP_403_FORBIDDEN) - # Send notification to the Coordinator (or another relevant person) after approval - try: - coordinator = ClubPosition.objects.get(club=yearly_plan.club, position='COORDINATOR') - recipient = User.objects.get(username=coordinator.name) - sender = request.user - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="yearly_plan_approved_dean", - message=yearly_plan.id, - club_name=str(yearly_plan.club) + submitted_choice = request.data.get("choice") + updated = Voting_choices.objects.filter(pk=submitted_choice, poll_event=poll).update( + votes=F("votes") + 1 ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass - - return Response( - {"message": "Yearly Plan status changed to 'Approved'."}, - status=status.HTTP_200_OK, - ) -class RejectYearlyPlanAPIView(APIView): - def put(self, request): - plan_id = request.data.get("id") - yearly_plan = get_object_or_404(YearlyPlan, id=plan_id) + if updated == 0: + return json_response(success=False, message="Invalid choice selected.", + status_code=status.HTTP_400_BAD_REQUEST) - if yearly_plan.status == 'REJECT': - return Response({"error": "Yearly Plan is already rejected."}, status=status.HTTP_400_BAD_REQUEST) + Voting_voters.objects.create(poll_event=poll, student_id=str(request.user)) + return json_response(success=True, message="Vote cast successfully") + except Exception as e: + logger.exception(e) + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - # Change the status to "REJECTED" - yearly_plan.status = "REJECT" - yearly_plan.save() - # Send notification to relevant user after rejection (could be coordinator or another role) - try: - coordinator = ClubPosition.objects.get(club=yearly_plan.club, position='COORDINATOR') - recipient = User.objects.get(username=coordinator.name) - sender = request.user - gymkhana_notif( - sender=sender, - recipient=recipient, - notif_type="yearly_plan_rejected", - message=yearly_plan.id, - club_name=str(yearly_plan.club) - ) - except (ClubPosition.DoesNotExist, User.DoesNotExist): - pass - - return Response( - {"message": "Yearly Plan status changed to 'Rejected'."}, - status=status.HTTP_200_OK, - ) -class ListYearlyPlansAPIView(APIView): - def get(self, request): - yearly_plans = YearlyPlan.objects.all() - serializer = YearlyPlanSerializer(yearly_plans, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - -class ListClubwiseYearlyPlansAPIView(APIView): - def get(self, request): - yearly_plans = YearlyPlanEvents.objects.all() - serializer = YearlyPlanSerializer(yearly_plans, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) -class UploadGalleryImageAPIView(APIView): - parser_classes = [MultiPartParser] - def post(self, request): - club_name = request.data.get("club_name") - if not club_name: - return Response({"error": "Club name is required."}, status=status.HTTP_400_BAD_REQUEST) - - # Get the uploaded file - file = request.FILES.get('image') - if not file: - return Response({"error": "No image file provided."}, status=status.HTTP_400_BAD_REQUEST) - - # Save the file to the 'gymkhana/gallery//' directory - gallery_path = f"gymkhana/gallery/{club_name}/{file.name}" - file_path = default_storage.save(gallery_path, file) - - return Response( - {"message": "Image uploaded successfully.", "file_name": file.name, "file_path": gallery_path}, - status=status.HTTP_201_CREATED - ) -class ListGalleryImagesAPIView(APIView): - def get(self, request): - # Get the club name - club_name = request.query_params.get("club_name") - if not club_name: - return Response({"error": "Club name is required."}, status=status.HTTP_400_BAD_REQUEST) +class DeletePollAPIView(APIView): + """DELETE /api/voting/polls// — Delete a voting poll.""" + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] - gallery_dir = f"gymkhana/gallery/{club_name}/" + def delete(self, request, poll_id): try: - # List files in the directory - files = default_storage.listdir(gallery_dir)[1] # [1] gets the files, not directories - image_urls = [f"{gallery_dir}{file}" for file in files] - return Response({"images": image_urls}, status=200) + Voting_polls.objects.filter(pk=poll_id).delete() + return json_response(success=True, message="Poll deleted") except Exception as e: - return Response({"error": f"An error occurred: {str(e)}"}, status=500) + logger.exception(e) + return json_response(success=False, message="Some error occurred", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) \ No newline at end of file diff --git a/FusionIIIT/applications/gymkhana/migrations/0001_initial.py b/FusionIIIT/applications/gymkhana/migrations/0001_initial.py index 0869064d5..e140005a1 100644 --- a/FusionIIIT/applications/gymkhana/migrations/0001_initial.py +++ b/FusionIIIT/applications/gymkhana/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.5 on 2024-07-16 15:44 +# Generated by Django 3.1.5 on 2023-03-15 18:53 from django.conf import settings from django.db import migrations, models @@ -11,9 +11,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('globals', '0001_initial'), ('academic_information', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('globals', '0001_initial'), ] operations = [ @@ -30,8 +30,6 @@ class Migration(migrations.Migration): ('spent_budget', models.IntegerField(default=0, null=True)), ('avail_budget', models.IntegerField(default=0, null=True)), ('status', models.CharField(choices=[('open', 'Open'), ('confirmed', 'Confirmed'), ('rejected', 'Rejected')], default='open', max_length=50)), - ('head_changed_on', models.DateField(default=django.utils.timezone.now, null=True)), - ('created_on', models.DateField(default=django.utils.timezone.now, null=True)), ('co_coordinator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='coco_of', to='academic_information.student')), ('co_ordinator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='co_of', to='academic_information.student')), ('faculty_incharge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='faculty_incharge_of', to='globals.faculty')), @@ -70,7 +68,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Registration_form', fields=[ - ('roll', models.CharField(default='20160017', max_length=8, primary_key=True, serialize=False)), + ('roll', models.CharField(default='2016001', max_length=7, primary_key=True, serialize=False)), ('user_name', models.CharField(default='Student', max_length=40)), ('branch', models.CharField(default='open', max_length=20)), ('cpi', models.FloatField(default=6.0, max_length=3)), @@ -95,16 +93,6 @@ class Migration(migrations.Migration): 'ordering': ['-pub_date'], }, ), - migrations.CreateModel( - name='Inventory', - fields=[ - ('club_name', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='club_inventory', serialize=False, to='gymkhana.club_info')), - ('inventory', models.FileField(upload_to='gymkhana/inventory')), - ], - options={ - 'db_table': 'Inventory', - }, - ), migrations.CreateModel( name='Voting_voters', fields=[ @@ -133,7 +121,7 @@ class Migration(migrations.Migration): ('venue', models.CharField(choices=[('Classroom', (('CR101', 'CR101'), ('CR102', 'CR102'))), ('Lecturehall', (('L101', 'L101'), ('L102', 'L102')))], max_length=50)), ('date', models.DateField(default=None)), ('start_time', models.TimeField(default=None)), - ('end_time', models.TimeField(default=None)), + ('end_time', models.TimeField(default=None, null=True)), ('session_poster', models.ImageField(null=True, upload_to='gymkhana/session_poster')), ('details', models.TextField(max_length=256, null=True)), ('status', models.CharField(choices=[('open', 'Open'), ('confirmed', 'Confirmed'), ('rejected', 'Rejected')], default='open', max_length=50)), diff --git a/FusionIIIT/applications/gymkhana/migrations/0005_defect_fixes.py b/FusionIIIT/applications/gymkhana/migrations/0005_defect_fixes.py new file mode 100644 index 000000000..9e4049515 --- /dev/null +++ b/FusionIIIT/applications/gymkhana/migrations/0005_defect_fixes.py @@ -0,0 +1,24 @@ +# Generated migration for defect fixes +# DEF-007: MinValueValidator on Club_budget and Fest_budget (validators don't create DB migrations) +# DEF-011: unique_together on Voting_voters (poll_event, student_id) — creates DB constraint + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gymkhana', '0004_yearlyplan_yearlyplanevents'), + ] + + operations = [ + # DEF-011 FIX: Enforce one vote per student per poll at the database level. + # Adds a UNIQUE constraint on (poll_event_id, student_id) in the Voting_voters table. + migrations.AlterUniqueTogether( + name='voting_voters', + unique_together={('poll_event', 'student_id')}, + ), + # Note: MinValueValidator (DEF-007) is enforced at the application layer only. + # Django validators do not generate database constraints; they run during model + # full_clean() and form validation. No schema change is needed for that fix. + ] \ No newline at end of file diff --git a/FusionIIIT/applications/gymkhana/models.py b/FusionIIIT/applications/gymkhana/models.py index c26c73cbd..b69f4cdf1 100644 --- a/FusionIIIT/applications/gymkhana/models.py +++ b/FusionIIIT/applications/gymkhana/models.py @@ -4,85 +4,135 @@ from django import template from django.contrib.auth.models import User from django.db import models +from django.core.validators import MinValueValidator from django.utils import timezone from django.urls import reverse -from applications.filetracking.models import File from applications.academic_information.models import Student from applications.globals.models import ExtraInfo, Faculty register = template.Library() +# ==================== TextChoices Classes (R5 Implementation) ==================== + +class ClubCategory(models.TextChoices): + """Category choices for clubs - replaces Constants.categoryCh""" + TECHNICAL = 'Technical', 'Technical' + SPORTS = 'Sports', 'Sports' + CULTURAL = 'Cultural', 'Cultural' + + +class MemberStatus(models.TextChoices): + """Status choices for club members - replaces Constants.status for members""" + OPEN = 'open', 'Open' + CONFIRMED = 'confirmed', 'Confirmed' + REJECTED = 'rejected', 'Rejected' + MEMBER = 'member', 'Member' + COORDINATOR = 'co-ordinator', 'Co-ordinator' + CO_COORDINATOR = 'Co-cordinator', 'Co-cordinator' + + +class ClubStatus(models.TextChoices): + """Status choices for clubs - replaces Constants.status for clubs""" + OPEN = 'open', 'Open' + CONFIRMED = 'confirmed', 'Confirmed' + REJECTED = 'rejected', 'Rejected' + + +class FestChoices(models.TextChoices): + """Fest choices - replaces Constants.fest""" + ABHIKALPAN = 'Abhikalpan', 'Abhikalpan' + GUSTO = 'Gusto', 'Gusto' + TARANG = 'Tarang', 'Tarang' + + +class VenueCategory(models.TextChoices): + """Venue categories for better organization""" + CLASSROOM = 'Classroom', 'Classroom' + LECTUREHALL = 'Lecturehall', 'Lecturehall' + GROUND = 'Ground', 'Ground' + COURT = 'Court', 'Court' + OTHER = 'Other', 'Other' + + +class Venue(models.TextChoices): + """All venue choices - replacing Constants.venue with flat choices for easier querying""" + # Classrooms + CR101 = 'CR101', 'CR101' + CR102 = 'CR102', 'CR102' + CR103 = 'CR103', 'CR103' + CR104 = 'CR104', 'CR104' + CR107 = 'CR107', 'CR107' + CR108 = 'CR108', 'CR108' + CR109 = 'CR109', 'CR109' + CR201 = 'CR201', 'CR201' + CR202 = 'CR202', 'CR202' + CR208 = 'CR208', 'CR208' + + # Lecture Halls + L101 = 'L101', 'L101' + L102 = 'L102', 'L102' + L103 = 'L103', 'L103' + L104 = 'L104', 'L104' + L105 = 'L105', 'L105' + L106 = 'L106', 'L106' + L107 = 'L107', 'L107' + L108 = 'L108', 'L108' + L201 = 'L201', 'L201' + L202 = 'L202', 'L202' + L206 = 'L206', 'L206' + L207 = 'L207', 'L207' + + # Grounds + FOOTBALL_GROUND = 'Football Ground', 'Football Ground' + CRICKET_GROUND = 'Cricket Ground', 'Cricket Ground' + BASKETBALL_GROUND = 'Basketball Ground', 'Basketball Ground' + VOLLEYBALL_GROUND = 'Volleyball Ground', 'Volleyball Ground' + ATHLETICS_GROUND = 'Athletics Ground', 'Athletics Ground' + + # Courts + TENNIS_COURT = 'Tennis Court', 'Tennis Court' + BADMINTON_COURT = 'Badminton Court', 'Badminton Court' + TABLE_TENNIS_COURT = 'Table Tennis Court', 'Table Tennis Court' + CHESS_ROOM = 'Chess Room', 'Chess Room' + CARROM_ROOM = 'Carrom Room', 'Carrom Room' + + # Other Venues + GYM = 'Gym', 'Gym' + CC_FIRST = 'CC First Floor', 'CC First Floor' + CC_SECOND = 'CC Second Floor', 'CC Second Floor' + CC_THIRD = 'CC Third Floor', 'CC Third Floor' + OAT = 'OAT', 'OAT' + + +class FormAvailability(models.TextChoices): + """Form availability choices - replaces Constants.available""" + ON = 'On', 'On' + OFF = 'Off', 'Off' + -# Class definations: +class BudgetStatus(models.TextChoices): + """Status choices for budget requests""" + OPEN = 'open', 'Open' + CONFIRMED = 'confirmed', 'Confirmed' + REJECTED = 'rejected', 'Rejected' -# # Class for various choices on the enumerations +# ==================== Legacy Constants (Kept for backward compatibility) ==================== + class Constants: - available = ( - ("On", "On"), - ("Off", "Off"), - ) - categoryCh = ( - ("Technical", "Technical"), - ("Sports", "Sports"), - ("Cultural", "Cultural"), - ) - status = (("open", "Open"), ("confirmed", "Confirmed"), ("rejected", "Rejected") ,("member", "Member"),("co-ordinator", "Co-ordinator"),("Co-cordinator", "Co-cordinator")) - STATUS_CHOICES = ( - ('ACCEPT', 'Accepted'), - ('REJECT', 'Rejected'), - ('COORDINATOR', 'Coordinator Review'), - ('FIC', 'FIC Review'), - ('COUNSELLOR', 'Counsellor Review'), - ('DEAN', 'Dean Review'), - ('REREVIEW', 'ReReview'), - ) - fest = (("Abhikalpan", "Abhikalpan"), ("Gusto", "Gusto"), ("Tarang", "Tarang")) - venue = ( - ("CR101", "CR101"), - ("CR102", "CR102"), - ("CR103", "CR103"), - ("CR104", "CR104"), - ("CR107", "CR107"), - ("CR108", "CR108"), - ("CR109", "CR109"), - ("CR201", "CR201"), - ("CR202", "CR202"), - ("CR208", "CR208"), - ("L101", "L101"), - ("L102", "L102"), - ("L103", "L103"), - ("L104", "L104"), - ("L105", "L105"), - ("L106", "L106"), - ("L107", "L107"), - ("L108", "L108"), - ("L201", "L201"), - ("L202", "L202"), - ("L206", "L206"), - ("L207", "L207"), - ("Football Ground", "Football Ground"), - ("Cricket Ground", "Cricket Ground"), - ("Basketball Ground", "Basketball Ground"), - ("Volleyball Ground", "Volleyball Ground"), - ("Tennis Court", "Tennis Court"), - ("Athletics Ground", "Athletics Ground"), - ("Badminton Court", "Badminton Court"), - ("Table Tennis Court", "Table Tennis Court"), - ("Chess Room", "Chess Room"), - ("Carrom Room", "Carrom Room"), - ("Gym", "Gym"), - ("CC First Floor", "CC First Floor"), - ("CC Second Floor", "CC Second Floor"), - ("CC Third Floor", "CC Third Floor"), - ("OAT", "OAT"), - ) + """Legacy Constants class - kept for backward compatibility""" + available = FormAvailability.choices + categoryCh = ClubCategory.choices + status = MemberStatus.choices + fest = FestChoices.choices + venue = Venue.choices # Now flat instead of nested tuples + +# ==================== Models ==================== class Club_info(models.Model): """ - It has the whole information about the club information. It stores the details of a club. @@ -98,34 +148,23 @@ class Club_info(models.Model): alloted_budget - the amount alloted to the club spent_budget - the amount spent by the club avail_budget - the amount available at the club - status - status of club wheather it is confirmed or not - + status - status of club whether it is confirmed or not + head_changed_on - date when head was last changed + created_on - date when club was created """ - club_name = models.CharField(max_length=50, null=False, primary_key=True) club_website = models.CharField(max_length=150, null=True, default="hello") - category = models.CharField(max_length=50, null=False, choices=Constants.categoryCh) - co_ordinator = models.ForeignKey( - Student, on_delete=models.CASCADE, null=False, related_name="co_of" - ) - co_coordinator = models.ForeignKey( - Student, on_delete=models.CASCADE, null=False, related_name="coco_of" - ) - faculty_incharge = models.ForeignKey( - Faculty, - on_delete=models.CASCADE, - null=False, - related_name="faculty_incharge_of", - ) - club_file = models.FileField(upload_to="gymkhana/club_poster", null=True) - activity_calender = models.FileField( - upload_to="gymkhana/activity_calender", null=True, default=" " - ) + category = models.CharField(max_length=50, null=False, choices=ClubCategory.choices) + co_ordinator = models.ForeignKey(Student, on_delete=models.CASCADE, null=False, related_name='co_of') + co_coordinator = models.ForeignKey(Student, on_delete=models.CASCADE, null=False, related_name='coco_of') + faculty_incharge = models.ForeignKey(Faculty, on_delete=models.CASCADE, null=False, related_name='faculty_incharge_of') + club_file = models.FileField(upload_to='gymkhana/club_poster', null=True) + activity_calender = models.FileField(upload_to='gymkhana/activity_calender', null=True, default=" ") description = models.TextField(max_length=256, null=True) alloted_budget = models.IntegerField(null=True, default=0) spent_budget = models.IntegerField(null=True, default=0) avail_budget = models.IntegerField(null=True, default=0) - status = models.CharField(max_length=50, choices=Constants.status, default="open") + status = models.CharField(max_length=50, choices=ClubStatus.choices, default=ClubStatus.OPEN) head_changed_on = models.DateField(default=timezone.now, auto_now=False, null=True) created_on = models.DateField(default=timezone.now, auto_now=False, null=True) @@ -133,28 +172,26 @@ def __str__(self): return str(self.club_name) class Meta: - db_table = "Club_info" + db_table = 'Club_info' class Form_available(models.Model): """ It stores registered form name , roll number and status. - + roll - roll number of the student - status - it is a boolean value wheather the form is available or not + status - it is a boolean value whether the form is available or not form_name - name of the form - """ - roll = models.CharField(default=2016001, max_length=7, primary_key=True) status = models.BooleanField(default=True, max_length=5) - form_name = models.CharField(default="senate_registration", max_length=30) + form_name = models.CharField(default='senate_registration', max_length=30) def __str__(self): return str(self.roll) class Meta: - db_table = "Form_available" + db_table = 'Form_available' class Registration_form(models.Model): @@ -163,23 +200,21 @@ class Registration_form(models.Model): roll - roll number of the student user_name - stores name of the student who is registered - brach - the branch student belongs to like cse,ece etc.., + branch - the branch student belongs to like cse,ece etc.., cpi - the cumulative pointer of the student - programme - the programme studeny=t belongs to like B.tech,M.tech etc.., - + programme - the programme student belongs to like B.tech,M.tech etc.., """ - - roll = models.CharField(max_length=8, default="20160017", primary_key=True) + roll = models.CharField(max_length=7, default="2016001", primary_key=True) user_name = models.CharField(max_length=40, default="Student") - branch = models.CharField(max_length=20, default="open") + branch = models.CharField(max_length=20, default='open') cpi = models.FloatField(max_length=3, default=6.0) - programme = models.CharField(max_length=20, default="B.tech") + programme = models.CharField(max_length=20, default='B.tech') def __str__(self): return str(self.roll) class Meta: - db_table = "Registration_form" + db_table = 'Registration_form' class Club_member(models.Model): @@ -188,91 +223,81 @@ class Club_member(models.Model): id - serial number for students(not useful) member - roll number of student - club - the name of clubs thie particular student belongs to + club - the name of clubs this particular student belongs to description - brief explanation of the member related to club if any thing needed - status - status of the member wheather he is confirmed or pending in a club + status - status of the member whether he is confirmed or pending in a club remarks - remarks of the student by the club if any. - """ - id = models.AutoField(primary_key=True) - member = models.ForeignKey( - Student, on_delete=models.CASCADE, related_name="member_of" - ) - club = models.ForeignKey( - Club_info, on_delete=models.CASCADE, related_name="this_club", null=False - ) + member = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='member_of') + club = models.ForeignKey(Club_info, on_delete=models.CASCADE, related_name='this_club', null=False) description = models.TextField(max_length=256, null=True) - status = models.CharField(max_length=50, choices=Constants.status, default="open") + status = models.CharField(max_length=50, choices=MemberStatus.choices, default=MemberStatus.OPEN) remarks = models.CharField(max_length=256, null=True) def __str__(self): return str(self.member.id) class Meta: - db_table = "Club_member" + db_table = 'Club_member' -# class Core_team(models.Model): -# """ -# The details about the main members who conducted/take care of the fest. -# It stores the indormation of those members. - -# id - serial number -# student_id - roll number of student -# team - name of the core_team -# year - year in which this team conducted the fest -# fest_name - name of the fest the core_team students takes care of -# pda - achievements they achieved through the fest -# remarks - remarks(if there are any) in the fest - -# """ - -# id = models.AutoField(primary_key=True) -# student_id = models.ForeignKey( -# Student, on_delete=models.CASCADE, related_name="applied_for" -# ) -# team = models.CharField(max_length=50, null=False) -# year = models.DateTimeField(max_length=6, null=True) -# fest_name = models.CharField(max_length=256, null=False, choices=Constants.fest) -# pda = models.TextField(max_length=256, null=False) -# remarks = models.CharField(max_length=256, null=True) +class Core_team(models.Model): + """ + The details about the main members who conducted/take care of the fest. + It stores the information of those members. -# def __str__(self): -# return str(self.student_id) + id - serial number + student_id - roll number of student + team - name of the core_team + year - year in which this team conducted the fest + fest_name - name of the fest the core_team students takes care of + pda - achievements they achieved through the fest + remarks - remarks(if there are any) in the fest + """ + id = models.AutoField(primary_key=True) + student_id = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='applied_for') + team = models.CharField(max_length=50, null=False) + year = models.DateTimeField(max_length=6, null=True) + fest_name = models.CharField(max_length=256, null=False, choices=FestChoices.choices) + pda = models.TextField(max_length=256, null=False) + remarks = models.CharField(max_length=256, null=True) -# class Meta: -# db_table = "Core_team" + def __str__(self): + return str(self.student_id) + class Meta: + db_table = 'Core_team' class Club_budget(models.Model): """ Records the budget details of the clubs. + id - serial number club - name of the club - budget_for - the purpose of the budget,like for equipment or for event etc.., + budget_for - the purpose of the budget, like for equipment or for event etc.., budget_amt - the amount required for the club budget_file - it is file which contains complete details regarding the amount they want to spend - descrion - description about the budget if any + description - description about the budget if any + status - status of budget request + remarks - remarks if any """ - id = models.AutoField(primary_key=True) - club = models.ForeignKey( - Club_info, on_delete=models.CASCADE, max_length=50, null=False - ) + club = models.ForeignKey(Club_info, on_delete=models.CASCADE, max_length=50, null=False) budget_for = models.CharField(max_length=256, null=False) - budget_amt = models.IntegerField(default=0, null=False) - budget_file = models.FileField(upload_to="uploads/", null=False) + # DEF-007 FIX: MinValueValidator(0) prevents negative budget amounts + budget_amt = models.IntegerField(default=0, null=False, validators=[MinValueValidator(0)]) + budget_file = models.FileField(upload_to='uploads/', null=False) description = models.TextField(max_length=256, null=False) - status = models.CharField(max_length=50, choices=Constants.status, default="open") + status = models.CharField(max_length=50, choices=BudgetStatus.choices, default=BudgetStatus.OPEN) remarks = models.CharField(max_length=256, null=True) def __str__(self): return str(self.id) class Meta: - db_table = "Club_budget" + db_table = 'Club_budget' class Session_info(models.Model): @@ -282,31 +307,28 @@ class Session_info(models.Model): id - serial number club - name of the club venue - the place at which they conducting the session - date - date of the session + date - date of the session start_time - the time at which session starts end_time - the time at which session ends session_poster - the logo/poster for the session(image) details - for which purpose they are taking the session - status - wheather it is approved/rejected. + status - whether it is approved/rejected. """ - id = models.AutoField(primary_key=True) - club = models.ForeignKey( - Club_info, on_delete=models.CASCADE, max_length=50, null=True - ) - venue = models.CharField(max_length=50, null=False, choices=Constants.venue) + club = models.ForeignKey(Club_info, on_delete=models.CASCADE, max_length=50, null=True) + venue = models.CharField(max_length=50, null=False, choices=Venue.choices) date = models.DateField(default=None, auto_now=False, null=False) start_time = models.TimeField(default=None, auto_now=False, null=False) - end_time = models.TimeField(default=None, auto_now=False, null=False) - session_poster = models.ImageField(upload_to="gymkhana/session_poster", null=True) + end_time = models.TimeField(default=None, auto_now=False, null=True) + session_poster = models.ImageField(upload_to='gymkhana/session_poster', null=True) details = models.TextField(max_length=256, null=True) - status = models.CharField(max_length=50, choices=Constants.status, default="open") + status = models.CharField(max_length=50, choices=ClubStatus.choices, default=ClubStatus.OPEN) def __str__(self): return str(self.id) class Meta: - db_table = "Session_info" + db_table = 'Session_info' class Event_info(models.Model): @@ -317,38 +339,31 @@ class Event_info(models.Model): club - name of the club event_name - name of the event venue - the place at which they conducting the event - incharge - name of faculty who is incharge for the event - start_date - start date of the event - end_date - end date of event + incharge - name of faculty who is incharge for the event + date - date of the event start_time - the time at which event starts end_time - the time at which event ends event_poster - the logo/poster for the event(image) details - for which purpose they are conducting the event - status - wheather it is approved/rejected. - + status - whether it is approved/rejected. """ - id = models.AutoField(primary_key=True) - club = models.ForeignKey( - Club_info, on_delete=models.CASCADE, max_length=50, null=True - ) + club = models.ForeignKey(Club_info, on_delete=models.CASCADE, max_length=50, null=True) event_name = models.CharField(max_length=256, null=False) - venue = models.CharField(max_length=50, null=False, choices=Constants.venue) + venue = models.CharField(max_length=50, null=False, choices=Venue.choices) incharge = models.CharField(max_length=256, null=False) - end_date = models.DateField(default=None, auto_now=False, null=False) - start_date=models.DateField(default=None, auto_now=False, null=False) + date = models.DateField(default=None, auto_now=False, null=False) start_time = models.TimeField(default=None, auto_now=False, null=False) end_time = models.TimeField(default=None, auto_now=False, null=True) - event_poster = models.FileField(upload_to="gymkhana/event_poster", blank=True) + event_poster = models.FileField(upload_to='gymkhana/event_poster', blank=True) details = models.TextField(max_length=256, null=True) - status = models.CharField(max_length=50, choices=Constants.STATUS_CHOICES, default="open") - file_id = models.ForeignKey(File, on_delete=models.CASCADE, null=False) - #change the status choices + status = models.CharField(max_length=50, choices=ClubStatus.choices, default=ClubStatus.OPEN) + def __str__(self): return str(self.id) class Meta: - db_table = "Event_info" + db_table = 'Event_info' class Club_report(models.Model): @@ -359,43 +374,25 @@ class Club_report(models.Model): id - serial number club - name of the club event_name - name of the event - incharge - name of faculty who is incharge for the event + incharge - name of faculty who is incharge for the event date - date of the event event_details - for which purpose they are conducting the event description - brief explanation about event if needed - """ - id = models.AutoField(primary_key=True) - club = models.ForeignKey( - Club_info, on_delete=models.CASCADE, max_length=50, null=False - ) - incharge = models.ForeignKey( - ExtraInfo, on_delete=models.CASCADE, max_length=256, null=False - ) + club = models.ForeignKey(Club_info, on_delete=models.CASCADE, max_length=50, null=False) + incharge = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, max_length=256, null=False) event_name = models.CharField(max_length=50, null=False) date = models.DateTimeField(max_length=50, default=timezone.now, blank=True) - event_details = models.FileField(upload_to="uploads/", null=False) + event_details = models.FileField(upload_to='uploads/', null=False) description = models.TextField(max_length=256, null=True) def __str__(self): return str(self.id) class Meta: - db_table = "Club_report" - -class Fest(models.Model): - id=models.AutoField(primary_key=True) - name=models.CharField(max_length=50, null=False) - category=models.CharField(max_length=50, null=False, choices=Constants.categoryCh) - description=models.TextField(max_length=256, null=True) - date = models.DateField(default=None, auto_now=False, null=False) - link=models.CharField(max_length=256, null=True) + db_table = 'Club_report' - def _str_(self): - return str(self.id) - class Meta: - db_table="fest" class Fest_budget(models.Model): """ @@ -406,306 +403,151 @@ class Fest_budget(models.Model): budget_amt - amount needed for the fest budget_file - It is a file which contains complete details regarding the budget that is going to spent for fest year - year in which the fest takes place - description - brief explanation regarding budget if any - status - wheather budget is approved or rejected + description - brief explanation regarding budget if any + status - whether budget is approved or rejected remarks - negative things regarding budget - """ - id = models.AutoField(primary_key=True) - fest = models.CharField(max_length=50, null=False, choices=Constants.fest) - budget_amt = models.IntegerField(default=0, null=False) - budget_file = models.FileField(upload_to="uploads/", null=False) + fest = models.CharField(max_length=50, null=False, choices=FestChoices.choices) + # DEF-007 FIX: MinValueValidator(0) prevents negative budget amounts + budget_amt = models.IntegerField(default=0, null=False, validators=[MinValueValidator(0)]) + budget_file = models.FileField(upload_to='uploads/', null=False) year = models.CharField(max_length=10, null=True) description = models.TextField(max_length=256, null=False) - status = models.CharField(max_length=50, choices=Constants.status, default="open") + status = models.CharField(max_length=50, choices=BudgetStatus.choices, default=BudgetStatus.OPEN) remarks = models.CharField(max_length=256, null=True) def __str__(self): return str(self.id) class Meta: - db_table = "Fest_budget" + db_table = 'Fest_budget' class Other_report(models.Model): """ This model also stores details of the events conducting by all clubs irrespective of the clubs. - id - serial number - incharge - name of faculty who is incharge for the event + incharge - name of faculty who is incharge for the event date - date of the event event_details - for which purpose they are conducting the event description - brief explanation about event if needed - """ - id = models.AutoField(primary_key=True) - incharge = models.ForeignKey( - ExtraInfo, on_delete=models.CASCADE, max_length=256, null=False - ) + incharge = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, max_length=256, null=False) event_name = models.CharField(max_length=50, null=False) date = models.DateTimeField(max_length=50, default=timezone.now, blank=True) - event_details = models.FileField(upload_to="uploads/", null=False) + event_details = models.FileField(upload_to='uploads/', null=False) description = models.TextField(max_length=256, null=True) def __str__(self): return str(self.id) class Meta: - db_table = "Other_report" + db_table = 'Other_report' class Change_office(models.Model): """ - - id - serial number club - name of the club co_ordinator - co_ordinator of the club co_coordinator - co_coordinator of the club - status - wheather it is confirmed or pending + status - whether it is confirmed or pending date_request - the date at which they requested to change date_approve - the date at which they approved to change remarks - remarks if there are any. - """ - id = models.AutoField(primary_key=True) - club = models.ForeignKey( - Club_info, on_delete=models.CASCADE, max_length=50, null=False - ) - co_ordinator = models.ForeignKey( - User, on_delete=models.CASCADE, null=False, related_name="co_of" - ) - co_coordinator = models.ForeignKey( - User, on_delete=models.CASCADE, null=False, related_name="coco_of" - ) - status = models.CharField(max_length=50, choices=Constants.status, default="open") + club = models.ForeignKey(Club_info, on_delete=models.CASCADE, max_length=50, null=False) + co_ordinator = models.ForeignKey(User, on_delete=models.CASCADE, null=False, related_name='co_of') + co_coordinator = models.ForeignKey(User, on_delete=models.CASCADE, null=False, related_name='coco_of') + status = models.CharField(max_length=50, choices=ClubStatus.choices, default=ClubStatus.OPEN) date_request = models.DateTimeField(max_length=50, default=timezone.now, blank=True) date_approve = models.DateTimeField(max_length=50, blank=True) remarks = models.CharField(max_length=256, null=True) def __str__(self): - return self.id + return str(self.id) class Meta: - db_table = "Change_office" - - -# class Voting_polls(models.Model): -# """ -# It shows the information about the voting poll. - -# title - title of the poll -# description - explanation about the voting -# pub_date - the date at which polling starts -# exp_date - the date at which polling ends -# created_by - name of the person who created this poll -# groups - the groups that are participating in the voting - -# """ - -# title = models.CharField(max_length=200, null=False) -# description = models.CharField(max_length=5000, null=False) -# pub_date = models.DateTimeField(default=timezone.now) -# exp_date = models.DateTimeField(default=timezone.now) -# created_by = models.CharField(max_length=100, null=True) -# groups = models.CharField(max_length=500, default="{}") - -# def groups_data(self): -# return self.groups - -# def __str__(self): -# return self.title - -# class Meta: -# ordering = ["-pub_date"] - + db_table = 'Change_office' -# class Voting_choices(models.Model): -# """ -# poll_event - name of the poll_event -# title - name of the poll -# description - description about choices if any -# votes - no.of votes recorded -# """ - -# poll_event = models.ForeignKey(Voting_polls, on_delete=models.CASCADE) -# title = models.CharField(max_length=200, null=False) -# description = models.CharField(max_length=500, default="") -# votes = models.IntegerField(default=0) - -# def __str__(self): -# return self.title - -# class Meta: -# get_latest_by = "votes" - - -# class Voting_voters(models.Model): -# """ -# records students who has voted in the poll. - -# poll_event - name of the poll -# student_id - roll number of student - -# """ - -# poll_event = models.ForeignKey(Voting_polls, on_delete=models.CASCADE) -# student_id = models.CharField(max_length=50, null=False) - -# def __str__(self): -# return self.student_id - - -class Inventory(models.Model): - club_name = models.OneToOneField( - Club_info, - primary_key=True, - on_delete=models.CASCADE, - related_name="club_inventory", - unique=True, - ) - inventory = models.FileField(upload_to="gymkhana/inventory") +class Voting_polls(models.Model): + """ + It shows the information about the voting poll. + + title - title of the poll + description - explanation about the voting + pub_date - the date at which polling starts + exp_date - the date at which polling ends + created_by - name of the person who created this poll + groups - the groups that are participating in the voting + """ + title = models.CharField(max_length=200, null=False) + description = models.CharField(max_length=5000, null=False) + pub_date = models.DateTimeField(default=timezone.now) + exp_date = models.DateTimeField(default=timezone.now) + created_by = models.CharField(max_length=100, null=True) + groups = models.CharField(max_length=500, default='{}') + + def groups_data(self): + return self.groups def __str__(self): - return str(self.club_name) - + return self.title + class Meta: - db_table = "Inventory" -class Budget(models.Model): - id = models.AutoField(primary_key=True) - club = models.ForeignKey( - Club_info, on_delete=models.CASCADE, max_length=50, null=False - ) - budget_for = models.CharField(max_length=256, null=False) - budget_requested = models.IntegerField(default=0, null=False) - budget_allocated = models.IntegerField(default=0, null=True) - budget_file = models.FileField(upload_to="uploads/", null=True) - description = models.TextField(max_length=256, null=False) - status = models.CharField(max_length=50, choices=Constants.STATUS_CHOICES, default="COORDINATOR") - remarks = models.CharField(max_length=256, null=True) - budget_comment = models.CharField(max_length=2000, null=True) - file_id = models.ForeignKey(File, on_delete=models.CASCADE, null=False) + ordering = ['-pub_date'] - def __str__(self): - return str(self.id) - class Meta: - db_table = "Budget" -class Budget_Comments(models.Model): +class Voting_choices(models.Model): """ - This table stores the comments related to budgets. - - Fields: - - budget_id: ForeignKey to the related budget entry - - commentator_designation: Designation of the person commenting - - comment: The content of the comment - - comment_date: The date the comment was made - - comment_time: The time the comment was made + poll_event - name of the poll_event + title - name of the poll + description - description about choices if any + votes - no.of votes recorded """ - - budget_id = models.ForeignKey('Budget', on_delete=models.CASCADE, related_name='comments') - commentator_designation = models.CharField(max_length=100, null=False) - comment = models.TextField(null=False) # The actual comment - comment_date = models.DateField(default=timezone.now, null=False) # Date of the comment - comment_time = models.TimeField(default=timezone.now, null=False) # Time of the comment + poll_event = models.ForeignKey(Voting_polls, on_delete=models.CASCADE) + title = models.CharField(max_length=200, null=False) + description = models.CharField(max_length=500, default='') + votes = models.IntegerField(default=0) def __str__(self): - return f"Comment by {self.commentator_designation} on {self.comment_date}" - + return self.title + class Meta: - db_table = "Budget_Comments" -class Event_Comments(models.Model): - """ - This table stores the comments related to budgets. - - Fields: - - Event_id: ForeignKey to the related budget entry - - commentator_designation: Designation of the person commenting - - comment: The content of the comment - - comment_date: The date the comment was made - - comment_time: The time the comment was made + get_latest_by = 'votes' + + +class Voting_voters(models.Model): """ + records students who has voted in the poll. - event_id = models.ForeignKey('Event_info', on_delete=models.CASCADE, related_name='comments') - commentator_designation = models.CharField(max_length=100, null=False) - comment = models.TextField(null=False) # The actual comment - comment_date = models.DateField(default=timezone.now, null=False) # Date of the comment - comment_time = models.TimeField(default=timezone.now, null=False) # Time of the comment + poll_event - name of the poll + student_id - roll number of student + """ + poll_event = models.ForeignKey(Voting_polls, on_delete=models.CASCADE) + student_id = models.CharField(max_length=50, null=False) def __str__(self): - return f"Comment by {self.commentator_designation} on {self.comment_date}" - - class Meta: - db_table = "Event_Comments" -class Achievements(models.Model): - id = models.AutoField(primary_key=True) - club_name = models.CharField(max_length=100, null=False) - title = models.CharField(max_length=100, null=False) - achievement = models.TextField(null=False) - def _str_(self): - return f"{self.club_name} - {self.achievement}" + return self.student_id class Meta: - db_table = "Achievements" -class ClubPosition(models.Model): - POSITION_CHOICES = [ - ('FIC', 'FIC'), - ('COORDINATOR', 'Coordinator'), - ('TECHNICAL_COUNSELLOR','Technical counsellor'), - ('SPORTS_COUNSELLOR','Sports counsellor'), - ('CULTURAL_COUNSELLOR','Cultural counsellor') - ] - name = models.CharField(max_length=100, null=False) - position = models.CharField(max_length=50, choices=POSITION_CHOICES, null=False) + # DEF-011 FIX: enforce one vote per student per poll at the database level + # This makes IntegrityError on duplicate vote guaranteed, not just likely + unique_together = ('poll_event', 'student_id') + db_table = 'Voting_voters' +class ClubLeadershipHistory(models.Model): club = models.ForeignKey(Club_info, on_delete=models.CASCADE) - class Meta: - db_table = 'ClubPosition' - def _str_(self): - return f"{self.club.club_name} - {self.name} - {self.position}" - class Meta: - db_table = "ClubPosition" - -class EventInput(models.Model): - event=models.ForeignKey(Event_info,on_delete=models.CASCADE) - description=models.TextField(max_length=500) - images=models.ImageField(upload_to="gymkhana/event_images",null=True) - def _str_(self): - return str(self.event) + previous_coordinator = models.CharField(max_length=100) + new_coordinator = models.CharField(max_length=100) + previous_co_coordinator = models.CharField(max_length=100) + new_co_coordinator = models.CharField(max_length=100) + changed_by = models.CharField(max_length=100) + changed_on = models.DateTimeField(auto_now_add=True) -class EventReport(models.Model): - event = models.ForeignKey(Event_info, on_delete=models.CASCADE) - description = models.TextField(null=True, blank=True) - venue = models.CharField(max_length=100, null=False) - incharge = models.CharField(max_length=50, null=False) - start_date = models.DateField(null=False) - end_date = models.DateField(null=False) - start_time = models.TimeField(null=False) - end_time = models.TimeField(null=False) - event_budget = models.DecimalField(max_digits=10, decimal_places=2, null=False) - special_announcement = models.TextField(null=True, blank=True) - report_pdf = models.FileField(upload_to='event_reports/', null=True, blank=True) - - class Meta: - db_table = "EventReport" -class YearlyPlan(models.Model): - club = models.ForeignKey('Club_info', on_delete=models.CASCADE) - year = models.IntegerField() - status = models.CharField(max_length=50, choices=Constants.STATUS_CHOICES, default="COORDINATOR") - file_link = models.CharField(max_length=255) - file_id = models.ForeignKey(File, on_delete=models.CASCADE, null=False) - def _str_(self): - return f"{self.club.name} - {self.year}" -class YearlyPlanEvents(models.Model): - yearly_plan = models.ForeignKey(YearlyPlan, on_delete=models.CASCADE, related_name='events') - event_name = models.CharField(max_length=255) - tentative_start_date = models.DateField() - tentative_end_date = models.DateField() - budget = models.DecimalField(max_digits=10, decimal_places=2) - description = models.TextField() - def _str_(self): - return f"{self.event_name} ({self.yearly_plan})" \ No newline at end of file + def __str__(self): + return f"{self.club.club_name} - {self.changed_on}" \ No newline at end of file diff --git a/FusionIIIT/applications/gymkhana/selectors.py b/FusionIIIT/applications/gymkhana/selectors.py new file mode 100644 index 000000000..71d10cb33 --- /dev/null +++ b/FusionIIIT/applications/gymkhana/selectors.py @@ -0,0 +1,120 @@ +import datetime + +from django.db.models import Prefetch, Q + +from applications.academic_information.models import Student +from .models import Club_budget, Club_info, Club_member, Event_info, Session_info + +# Club Selectors +def get_club_by_coordinator(user): + """Get club where user is coordinator or co-coordinator""" + try: + student = Student.objects.get(id__user=user) + club = Club_info.objects.filter( + Q(co_ordinator=student) | Q(co_coordinator=student) + ).select_related('co_ordinator', 'co_coordinator', 'faculty_incharge').first() + return club + except Student.DoesNotExist: + return None + +def get_all_clubs(): + """Get all clubs with optimized queries""" + return Club_info.objects.select_related( + 'co_ordinator', 'co_coordinator', 'faculty_incharge' + ).prefetch_related( + Prefetch('this_club', queryset=Club_member.objects.select_related('member')) + ).all() + +def get_club_detail(club_name): + """Get single club with details""" + return Club_info.objects.select_related( + 'co_ordinator', 'co_coordinator', 'faculty_incharge' + ).prefetch_related( + Prefetch('this_club', queryset=Club_member.objects.select_related('member')) + ).get(club_name=club_name) + +def get_student_clubs(student_roll): + """Get all clubs a student is member of""" + return Club_member.objects.filter( + member__id__id=student_roll, + status='confirmed' + ).select_related('club').values_list('club__club_name', flat=True) + +def get_pending_members(club_name): + """Get pending membership requests for a club""" + return Club_member.objects.filter( + club__club_name=club_name, + status='open' + ).select_related('member', 'member__id', 'member__id__user') + +# Session/Event Selectors +def get_upcoming_events(): + """Get upcoming events""" + today = datetime.date.today() + return Event_info.objects.filter( + date__gte=today, + status='confirmed' + ).select_related('club').order_by('date', 'start_time') + +def get_past_events(): + """Get past events""" + today = datetime.date.today() + return Event_info.objects.filter( + date__lt=today, + status='confirmed' + ).select_related('club').order_by('-date', '-start_time') + +def get_club_events(club_name): + """Get events for a specific club""" + return Event_info.objects.filter( + club__club_name=club_name + ).select_related('club').order_by('-date') + +def get_club_sessions(club_name): + """Get sessions for a specific club""" + today = datetime.date.today() + return Session_info.objects.filter( + club__club_name=club_name, + date__gte=today + ).select_related('club').order_by('date', 'start_time') + +def check_session_conflict(date, start_time, end_time, venue, exclude_id=None): + """Check if session time slot conflicts with existing sessions""" + conflicts = Session_info.objects.filter(date=date, venue=venue) + + if exclude_id: + conflicts = conflicts.exclude(id=exclude_id) + + start_time_obj = datetime.datetime.strptime(start_time, "%H:%M").time() + end_time_obj = datetime.datetime.strptime(end_time, "%H:%M").time() + + for session in conflicts: + if (start_time_obj < session.end_time and end_time_obj > session.start_time): + return True + return False + +def check_event_conflict(date, start_time, end_time, venue, exclude_id=None): + """Check if event time slot conflicts with existing events""" + conflicts = Event_info.objects.filter(date=date, venue=venue) + + if exclude_id: + conflicts = conflicts.exclude(id=exclude_id) + + start_time_obj = datetime.datetime.strptime(start_time, "%H:%M").time() + end_time_obj = datetime.datetime.strptime(end_time, "%H:%M").time() + + for event in conflicts: + if (start_time_obj < event.end_time and end_time_obj > event.start_time): + return True + return False + +# Budget Selectors +def get_pending_budgets(): + """Get pending budget requests""" + return Club_budget.objects.filter(status='open').select_related('club') + +def get_club_budgets(club_name): + """Get budgets for a specific club""" + return Club_budget.objects.filter( + club__club_name=club_name + ).select_related('club').order_by('-id') diff --git a/FusionIIIT/applications/gymkhana/services.py b/FusionIIIT/applications/gymkhana/services.py new file mode 100644 index 000000000..dd192801a --- /dev/null +++ b/FusionIIIT/applications/gymkhana/services.py @@ -0,0 +1,246 @@ +from django.db import transaction +from django.contrib.auth.models import User +from django.shortcuts import get_object_or_404 +from .models import Club_info, Club_member, Session_info, Event_info +from applications.academic_information.models import Student +from applications.globals.models import ExtraInfo, Faculty, HoldsDesignation, Designation +from notification.views import gymkhana_session, gymkhana_event +import logging + +logger = logging.getLogger(__name__) + + +class ClubService: + """Compatibility wrapper for the legacy unit tests.""" + + @staticmethod + def _validate_student(student_id): + try: + extra = ExtraInfo.objects.get(id=student_id, user_type='student') + return Student.objects.get(id=extra) + except (ExtraInfo.DoesNotExist, Student.DoesNotExist): + return None + +@transaction.atomic +def create_club(data, request_user): + """ + Create a new club + Matches V3 from your plan - extracted from new_club() + """ + try: + club_name = data.get('club_name') + category = data.get('category') + co_ordinator_id = data.get('co_ordinator') + co_coordinator_id = data.get('co_coordinator') + faculty_name = data.get('faculty_incharge') + description = data.get('description') + + # Get coordinator student + co_extra = get_object_or_404(ExtraInfo, id=co_ordinator_id, user_type='student') + co_student = get_object_or_404(Student, id=co_extra) + + # Get co-coordinator student + coco_extra = get_object_or_404(ExtraInfo, id=co_coordinator_id, user_type='student') + coco_student = get_object_or_404(Student, id=coco_extra) + + # Get faculty + faculty_parts = faculty_name.split() + faculty_user = User.objects.filter( + first_name__icontains=faculty_parts[0], + last_name__icontains=faculty_parts[-1] if len(faculty_parts) > 1 else '' + ).first() + faculty_extra = get_object_or_404(ExtraInfo, user=faculty_user, user_type='faculty') + faculty_inc = get_object_or_404(Faculty, id=faculty_extra) + + # Create club + club = Club_info.objects.create( + club_name=club_name, + category=category, + co_ordinator=co_student, + co_coordinator=coco_student, + faculty_incharge=faculty_inc, + description=description, + status='open' + ) + + return {"success": True, "club": club, "message": "Club created successfully"} + + except Exception as e: + logger.error(f"Error creating club: {e}") + return {"success": False, "message": str(e)} + +@transaction.atomic +def approve_membership(club_name, member_ids, remarks_list): + """ + Approve club membership requests + Matches V3 - extracted from approve() + """ + try: + club = get_object_or_404(Club_info, club_name=club_name) + approved_count = 0 + + for member_id, remarks in zip(member_ids, remarks_list): + # Get member + extra = get_object_or_404(ExtraInfo, id=member_id, user_type='student') + student = get_object_or_404(Student, id=extra) + + # Update or create membership + member, created = Club_member.objects.update_or_create( + club=club, + member=student, + defaults={'status': 'confirmed', 'remarks': remarks} + ) + approved_count += 1 + + return {"success": True, "approved": approved_count, "message": f"Approved {approved_count} members"} + + except Exception as e: + logger.error(f"Error approving membership: {e}") + return {"success": False, "message": str(e)} + + +@transaction.atomic +def create_membership_request(club_name, member_id, description=""): + """Create a club membership request if one does not already exist.""" + try: + club = get_object_or_404(Club_info, club_name=club_name) + extra = get_object_or_404(ExtraInfo, id=member_id, user_type='student') + student = get_object_or_404(Student, id=extra) + + member, created = Club_member.objects.get_or_create( + club=club, + member=student, + defaults={ + 'description': description, + 'status': 'open', + }, + ) + if not created: + return {"success": False, "message": "Membership request already exists"} + + return {"success": True, "member": member, "message": "Membership request sent"} + except Exception as e: + logger.error(f"Error creating membership request: {e}") + return {"success": False, "message": str(e)} + +@transaction.atomic +def create_session(data, club, request_user): + """ + Create a new session + Matches V4 - extracted from new_session() + """ + try: + venue = data.get('venue') + session_poster = data.get('session_poster') + date = data.get('date') + start_time = data.get('start_time') + end_time = data.get('end_time') + details = data.get('details') + + session = Session_info.objects.create( + club=club, + venue=venue, + date=date, + start_time=start_time, + end_time=end_time, + session_poster=session_poster, + details=details, + status='open' + ) + + # Send notifications + from applications.globals.models import ExtraInfo + students = ExtraInfo.objects.filter(user_type='student') + recipients = User.objects.filter(extrainfo__in=students) + gymkhana_session(request_user, recipients, "new_session", club, details, venue) + + return {"success": True, "session": session, "message": "Session created successfully"} + + except Exception as e: + logger.error(f"Error creating session: {e}") + return {"success": False, "message": str(e)} + +@transaction.atomic +def create_event(data, club, request_user): + """ + Create a new event + Matches V5 - extracted from new_event() + """ + try: + event_name = data.get('event_name') + incharge = data.get('incharge') + venue = data.get('venue') + event_poster = data.get('event_poster') + start_date = data.get('start_date') + end_date = data.get('end_date') + start_time = data.get('start_time') + end_time = data.get('end_time') + details = data.get('details') + + event = Event_info.objects.create( + club=club, + event_name=event_name, + incharge=incharge, + venue=venue, + start_date=start_date, + end_date=end_date, + start_time=start_time, + end_time=end_time, + event_poster=event_poster, + details=details, + status='open' + ) + + # Send notifications + from applications.globals.models import ExtraInfo + students = ExtraInfo.objects.filter(user_type='student') + recipients = User.objects.filter(extrainfo__in=students) + gymkhana_event(request_user, recipients, "new_event", club, event_name, details, venue) + + return {"success": True, "event": event, "message": "Event created successfully"} + + except Exception as e: + logger.error(f"Error creating event: {e}") + return {"success": False, "message": str(e)} + +@transaction.atomic +def bulk_delete_objects(model, ids, user, permission_check=True): + """ + Generic bulk delete utility + Matches R2 from your plan + """ + try: + objects = model.objects.filter(id__in=ids) + count = objects.count() + + if permission_check: + # Add custom permission logic here + pass + + objects.delete() + return {"success": True, "deleted": count, "message": f"Deleted {count} items"} + + except Exception as e: + logger.error(f"Error in bulk delete: {e}") + return {"success": False, "message": str(e)} + +@transaction.atomic +def bulk_approve_membership(club_name, member_ids, remarks_list): + """ + Bulk approve multiple membership requests + """ + club = get_object_or_404(Club_info, club_name=club_name) + approved_count = 0 + + for member_id, remarks in zip(member_ids, remarks_list): + extra = get_object_or_404(ExtraInfo, id=member_id, user_type='student') + student = get_object_or_404(Student, id=extra) + + member, created = Club_member.objects.update_or_create( + club=club, + member=student, + defaults={'status': 'confirmed', 'remarks': remarks} + ) + approved_count += 1 + + return {"success": True, "approved": approved_count} diff --git a/FusionIIIT/applications/gymkhana/templatetags/voters_tag.py b/FusionIIIT/applications/gymkhana/templatetags/voters_tag.py index 3cb86ce50..408e7b643 100644 --- a/FusionIIIT/applications/gymkhana/templatetags/voters_tag.py +++ b/FusionIIIT/applications/gymkhana/templatetags/voters_tag.py @@ -4,15 +4,23 @@ toggel = False +## A tag function to find whether to show the poll to the user or not @register.simple_tag def validate(user, groups): - roll = user.username[:5] + + roll = user.username[:4] branch = user.extrainfo.department.name - if roll in groups: - allowed_branches = groups[roll] - if 'All' in allowed_branches or branch in allowed_branches: + print(groups) + if roll in groups.keys(): + if groups[roll][0] == 'All': return True - return False + else: + if branch in groups[roll]: + return True + else: + return False + else: + return False @register.simple_tag def result(): diff --git a/FusionIIIT/applications/gymkhana/tests/__init__.py b/FusionIIIT/applications/gymkhana/tests/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/FusionIIIT/applications/gymkhana/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/FusionIIIT/applications/gymkhana/tests/test_services.py b/FusionIIIT/applications/gymkhana/tests/test_services.py new file mode 100644 index 000000000..3340aadcb --- /dev/null +++ b/FusionIIIT/applications/gymkhana/tests/test_services.py @@ -0,0 +1,46 @@ +#test_services.py +from django.test import TestCase +from django.contrib.auth.models import User +from applications.academic_information.models import Student +from applications.globals.models import ExtraInfo, Faculty +from ..models import Club_info +from ..services import ClubService + + +class ClubServiceTestCase(TestCase): + """Test cases for ClubService""" + + def setUp(self): + """Set up test data""" + # Create test user + self.user = User.objects.create_user( + username='testuser', + password='testpass123' + ) + + # Create test extra info + self.extra_info = ExtraInfo.objects.create( + id='2020001', + user=self.user, + user_type='student' + ) + + # Create test student + self.student = Student.objects.create( + id=self.extra_info, + cpi=8.5, + programme='B.Tech' + ) + + def test_validate_student_exists(self): + """Test that existing student is validated correctly""" + result = ClubService._validate_student('2020001') + self.assertIsNotNone(result) + self.assertEqual(result.id, self.student.id) + + def test_validate_student_not_exists(self): + """Test that non-existing student returns None""" + result = ClubService._validate_student('9999999') + self.assertIsNone(result) + + # Add more tests as needed \ No newline at end of file diff --git a/FusionIIIT/applications/gymkhana/urls.py b/FusionIIIT/applications/gymkhana/urls.py index c2da3696a..7004cb874 100644 --- a/FusionIIIT/applications/gymkhana/urls.py +++ b/FusionIIIT/applications/gymkhana/urls.py @@ -1,333 +1,138 @@ -from django.conf.urls import url from django.urls import path +from rest_framework.authtoken.views import obtain_auth_token from rest_framework.urlpatterns import format_suffix_patterns + from applications.gymkhana.api.views import ( - AddClub_BudgetAPIView, - AddMemberToClub, - ApproveEvent, - ChangeHeadAPIView, - ClubMemberAPIView, - ClubMemberApproveView, - ClubMemberDeleteAPIView, + # Clubs + ListClubsAPIView, CreateClubAPIView, - DeanReviewBudgetAPIView, - DeleteClubAPIView, - DeleteClubBudgetAPIView, - EventDeleteAPIView, - EventUpdateAPIView, - FreeMembersForClubAPIView, - ListAllClubPositionAPIView, - SessionUpdateAPIView, - UpdateClubBudgetAPIView, + ClubDetailAPIView, + ClubMembersAPIView, + ApproveMembersAPIView, UpdateClubNameAPIView, - UpdateClubStatusAPIView, - UploadActivityCalendarAPIView, - ModifyEventAPIView, - ModifyBudgetAPIView, - RejectEventAPIView, - RejectBudgetAPIView, - UpdateEventAPIView, - FestListView, - EventReportListAPIView, - EventReportAPIView, - EventReportPDFView -) -from applications.gymkhana.api.views import ( - clubname, - Club_Details, - club_events, - club_budgetinfo, - Fest_Budget, - club_report, - Registraion_form, -) -from applications.gymkhana.api.views import session_details, UpdateBudgetAPIView -from applications.gymkhana.api.views import ( - DeleteSessionsView, + ClubApproveAPIView, + ClubRejectAPIView, + DeleteClubAPIView, + ChangeClubHeadAPIView, + ActivityCalendarAPIView, + # Members + MembershipRequestAPIView, + ApproveMembershipAPIView, + RejectMembershipAPIView, + CancelMembershipAPIView, + DeleteMemberFormAPIView, + DeleteMemberAPIView, + # Events + ListEventsAPIView, + CreateEventAPIView, + EventDetailAPIView, NewEventAPIView, + EditEventAPIView, + ApproveEventsAPIView, + DeleteEventsAPIView, + DateEventsAPIView, + EventReportAPIView, + # Sessions + ListSessionsAPIView, + CreateSessionAPIView, + BulkDeleteSessionsAPIView, NewSessionAPIView, - Club_Detail, - UpcomingEventsAPIView, - PastEventsAPIView, - Budgetinfo, - AddClubAPI, - NewBudgetAPIView, - FICApproveBudgetAPIView, - CounsellorApproveBudgetAPIView, - DeanApproveBudgetAPIView, - FICApproveEventAPIView, - CounsellorApproveEventAPIView, - DeanApproveEventAPIView, - AddAchievementAPIView, - AchievementsAPIView, - CreateBudgetCommentAPIView, - CreateEventCommentAPIView, - ListBudgetCommentsAPIView, - ListEventCommentsAPIView, - AddClubPositionAPIView, - ListClubPositionAPIView, - NewFestAPIView, - CoordinatorEventsAPIView, - EventInputAPIView, - NewsletterPDFAPIView, - DownloadYearlyPlanTemplateAPIView, - UploadYearlyPlanExcelAPIView, - FICApproveYearlyPlanAPIView, - CounsellorApproveYearlyPlanAPIView, - DeanApproveYearlyPlanAPIView, - RejectYearlyPlanAPIView, - ListYearlyPlansAPIView, - ListClubwiseYearlyPlansAPIView, - UploadGalleryImageAPIView, - ListGalleryImagesAPIView, + EditSessionAPIView, + DeleteSessionsAPIView, + DateSessionsAPIView, + # Budget + ClubBudgetAPIView, + FestBudgetAPIView, + BudgetApproveAPIView, + BudgetRejectAPIView, + UpdateBudgetAmountAPIView, + # Reports + ClubReportAPIView, + # Registration + RegistrationFormAPIView, + FormAvailAPIView, + DeleteRequestsAPIView, + # Data / Lookup + FacultyDataAPIView, + StudentsDataAPIView, + GetVenueAPIView, + # Voting + VotingPollAPIView, + VoteAPIView, + DeletePollAPIView, ) -from . import views -from rest_framework.authtoken.views import obtain_auth_token -app_name = "gymkhana" +app_name = 'gymkhana' urlpatterns = [ - # for club_detail,upcomingevents and past events - url(r"^club_detail/$", Club_Detail.as_view()), - url(r"^upcoming_events/$", UpcomingEventsAPIView.as_view()), - url(r"^past_events/$", PastEventsAPIView.as_view()), - url(r"^budget/$", Budgetinfo.as_view()), - url(r"^session_details/$", session_details.as_view()), - url(r"^event_info/$", club_events.as_view()), - url(r"^club_budgetinfo/$", club_budgetinfo.as_view()), - # academic administration - url(r"^club_approve/$", views.club_approve, name="club_approve"), - url(r"^del_mem/$", views.del_mem, name="del_mem"), - url(r"^club_reject/$", views.club_reject, name="club_reject"), - url(r"^budget_approve/$", views.budget_approve, name="budget_approve"), - url(r"^budget_reject/$", views.budget_reject, name="budget_reject"), - # This is post method which takes username and password to generates/return Token - url(r"^login/$", obtain_auth_token, name="login"), - # api for "clubdetails" method="get" with TokenAuthentication - url(r"^clubdetails/$", Club_Details.as_view()), - # api for "clubname" method="get" with TokenAuthentication - url(r"^Fest_budget/$", Fest_Budget.as_view(), name="Fest_budget"), - # api for "festbudget" method="get" with TokenAuthentication - url(r"^club_report/$", club_report.as_view()), - # api for "club_report" method="get" with TokenAuthentication - url(r"^registration_form/$", Registraion_form.as_view()), - # api for "registration_form" method="get" with TokenAuthentication - # url(r'^voting_polls/$',Voting_Polls.as_view()), - # api for "voting_polls" method="get" with TokenAuthentication - # url(r"^api/new_event/$", NewEventAPIView.as_view(), name="new_event_api"), - # url(r"^api/club_membership/$", AddMemberToClub.as_view(), name="new_club_member"), - # url(r"^api/delete_event/$", DeleteEventsView.as_view(), name="delete_events_api"), - # url( - # r"^api/delete_sessions/$", - # DeleteSessionsView.as_view(), - # name="delete_sessions_api", - # ), - # url(r"^api/new_session/$", NewSessionAPIView.as_view(), name="new_session_api"), - url(r"^clubname/$", clubname.as_view()), - url(r"^$", views.gymkhana, name="gymkhana"), - url(r"^delete_requests/$", views.delete_requests, name="delete_requests"), - url(r"^form_avail/$", views.form_avail, name="form_avail"), - url(r"^registration_form/$", views.registration_form, name="registration_form"), - url(r"^new_club/$", views.new_club, name="new_club"), - # url(r"^api/members_records/$", ClubMemberAPIView.as_view(), name="club_members"), - # club_head - url(r"^approve/$", views.approve, name="approve"), - url(r"^reject/$", views.reject, name="reject"), - url(r"^cancel/$", views.cancel, name="cancel"), - url(r"^new_event/$", views.new_event, name="new_event"), - url(r"^new_session/$", views.new_session, name="new_session"), - url(r"^event_report/$", views.event_report, name="event_report"), - url(r"^club_event_report/$", views.club_report, name="club_report"), - url(r"^change_head/$", views.change_head, name="change_head"), - url(r"^club_budget/$", views.club_budget, name="club_budget"), - url(r"^act_calender/$", views.act_calender, name="act_calender"), - url(r"^date_sessions/$", views.date_sessions, name="date_sessions"), - url(r"^date_events/$", views.date_events, name="date_events"), - url(r"^delete_sessions/$", views.delete_sessions, name="delete_sessions"), - url(r"^delete_events/$", views.delete_events, name="delete_events"), - url(r"^(?P\d+)/edit_event/$", views.edit_event, name="edit_event"), - url(r"^(?P\d+)/editsession/$", views.editsession, name="editsession"), - url(r"^delete_memberform/$", views.delete_memberform, name="delete_memberform"), - # url(r'^voting_poll/$', views.voting_poll, name='voting_poll'), - # url(r'^delete_poll/(?P\d+)/$', views.delete_poll, name='delete_poll'), - # student - url(r"^registration_form/$", views.registration_form, name="registration_form"), - url(r"^delete_requests/$", views.delete_requests, name="delete_requests"), - url(r"^club_membership/$", views.club_membership, name="membership"), - # url(r'^(?P\d+)/$', views.vote, name='vote'), - url(r"^$", views.gymkhana, name="gymkhana"), - # data recieving - url(r"^form_avail/$", views.form_avail, name="form_avail"), - url(r"^faculty_data/$", views.facultyData, name="faculty_data"), - url(r"^students_data/$", views.studentsData, name="students_data"), - url(r"^students_club_members/$", views.studentsClubMembers, name="students_data"), - url(r"^get_venue/$", views.getVenue, name="get_venue"), - # core_team - # url(r"^core_team/$", views.core_team, name="core_team"), - url(r"^festbudget/$", views.fest_budget, name="fest_budget"), - # fic - # url(r"^Inventory_update/$", views.core_team, name="Inventory_update"), - url(r"^del_club/$", views.del_club, name="del_club"), - url(r"^approve_events/$", views.approve_events, name="approve_events"), - url(r"^update-club-name/$", views.update_club_name, name="update-club-name"), - url( - r"^update-budget-amount/$", - views.update_budget_amount, - name="update_budget_amount", - ), - # url(r"^update-spent-amount/$", views.update_spent_amount, name='update_spent_amount'), - - - - #app - - url(r'^api/update_clubBudget/$', UpdateClubBudgetAPIView.as_view(), name='update budget'), - url(r'^api/add_clubBudget/$', AddClub_BudgetAPIView.as_view(), name='edit event'), - url(r'^api/update_coordinator/$', ChangeHeadAPIView.as_view(), name = 'update coordinator'), - url(r'^api/activity_calender/$', UploadActivityCalendarAPIView.as_view(), name='update activity calendder'), - url(r'^api/delete_event/$', EventDeleteAPIView.as_view(), name='delete_events_api'), - url(r'^api/delete_sessions/$', DeleteSessionsView.as_view(), name='delete_sessions_api'), - url(r'^api/new_session/$',NewSessionAPIView.as_view(), name='new_session_api'), - url(r'^api/new_event/$',NewEventAPIView.as_view(), name='new_event_api'), - url(r'^api/delete_club/$',DeleteClubAPIView.as_view(), name='delete_club'), - url(r'^api/members_records/$', ClubMemberAPIView.as_view(), name='club_members'), - url(r'^api/member_approve/$', ClubMemberApproveView.as_view(), name='approval'), - url(r'^api/member_reject/$', ClubMemberDeleteAPIView.as_view(), name='reject'), - url(r'^api/club_membership/$', AddMemberToClub.as_view(), name='new_club_member'), - # url(r'^api/show_voting_choices/$', ShowVotingChoicesAPIView.as_view(), name='voting_choices'), - # url(r'^api/delete_poll/$', VotingPollsDeleteAPIView.as_view(), name='delete poll'), - # url(r'^api/vote/$', VoteIncrementAPIView.as_view(), name='give vote'), - url(r"^api/edit_session/$", SessionUpdateAPIView.as_view(), name="edit session"), - url(r"^api/edit_event/$", EventUpdateAPIView.as_view(), name="edit event"), - url( - r"^api/delete_budget/$", DeleteClubBudgetAPIView.as_view(), name="delete budget" - ), - url( - r"^api/upload_activitycalender/$", - UploadActivityCalendarAPIView.as_view(), - name="calender", - ), - url(r"^api/create_club/$", CreateClubAPIView.as_view(), name="new club"), - url( - r"^api/update_clubStatus/$", - UpdateClubStatusAPIView.as_view(), - name="update club status", - ), - url( - r"^api/updateClubName/$", - UpdateClubNameAPIView.as_view(), - name="update club name", - ), - url(r"^api/approve_event/$", ApproveEvent.as_view(), name="approve event"), - # add club api - url(r"^api/add_club/$", AddClubAPI.as_view(), name="add club"), - # new budget api - url(r"^api/new_budget/$", NewBudgetAPIView.as_view(), name="new budget"), - # fic approve budget api - url( - r"^api/fic_approve_budget/$", - FICApproveBudgetAPIView.as_view(), - name="fic approve budget", - ), - # counsellor approve budget api - url( - r"^api/counsellor_approve_budget/$", - CounsellorApproveBudgetAPIView.as_view(), - name="counsellor approve budget", - ), - # dean approve budget api - url( - r"^api/dean_approve_budget/$", - DeanApproveBudgetAPIView.as_view(), - name="dean approve budget", - ), - url(r'^api/dean_review_budget/$', DeanReviewBudgetAPIView.as_view(), name = 'review budget'), - # new event api - url(r"^api/new_events/$", NewEventAPIView.as_view(), name="new events"), - # fic approve event api - url( - r"^api/fic_approve_event/$", - FICApproveEventAPIView.as_view(), - name="fic approve event", - ), - # counsellor approve event api - url( - r"^api/counsellor_approve_event/$", - CounsellorApproveEventAPIView.as_view(), - name="counsellor approve event", - ), - # dean approve event api - url( - r"^api/dean_approve_event/$", - DeanApproveEventAPIView.as_view(), - name="dean approve event", - ), - url( - r"^api/add_achievement/$", AddAchievementAPIView.as_view(), name="approve event" - ), - url( - r"^api/show_achievement/$", AchievementsAPIView.as_view(), name="approve event" - ), - url( - r"^api/create_budget_comment/$", - CreateBudgetCommentAPIView.as_view(), - name="create budget comment", - ), - url( - r"^api/create_event_comment/$", - CreateEventCommentAPIView.as_view(), - name="create event comment", - ), - url( - r"^api/list_budget_comments/$", - ListBudgetCommentsAPIView.as_view(), - name="list budget comments", - ), - url( - r"^api/list_event_comments/$", - ListEventCommentsAPIView.as_view(), - name="list event comments", - ), - url(r"^api/modify_event/$", ModifyEventAPIView.as_view(), name="modify event"), - url(r"^api/modify_budget/$", ModifyBudgetAPIView.as_view(), name="modify budget"), - url(r"^api/reject_event/$", RejectEventAPIView.as_view(), name="reject event"), - url(r"^api/reject_budget/$", RejectBudgetAPIView.as_view(), name="reject budget"), - url( - r"^api/add_club_position/$", - AddClubPositionAPIView.as_view(), - name="add club position", - ), - url( - r"^api/list_all_club_position/$", - ListAllClubPositionAPIView.as_view(), - name="list club position", - ), - url( - r"^api/list_club_position/$", - ListClubPositionAPIView.as_view(), - name="list club position", - ), - url(r"^api/new_event/$", UpdateEventAPIView.as_view(), name="update event"), - url(r"^api/update_event/$", UpdateEventAPIView.as_view(), name="update event"), - url(r"^api/update_budget/$", UpdateBudgetAPIView.as_view(), name="update budget"), - url(r"^fest/$" , FestListView.as_view(), name="fest"), - url(r'^api/new_fest/$', NewFestAPIView.as_view(), name='new_fest'), - url(r'^api/event_allocation/$', FreeMembersForClubAPIView.as_view(), name='event allocation'), - url(r'^api/coordinator_events/$', CoordinatorEventsAPIView.as_view(), name='coordinator_events'), - url(r'^api/coordinator_eventsinput/$', EventInputAPIView.as_view(), name='coordinator_eventsinput'), - url(r'^api/newsletter_pdf/$', NewsletterPDFAPIView.as_view(), name='newsletter_pdf'), - url(r'^api/event_report_list/$', EventReportListAPIView.as_view(), name='event_report_list'), - url(r'^api/add_event_report/$', EventReportAPIView.as_view(), name='add_event_report'), - url(r'^api/event_report_pdf/(?P\d+)/$', EventReportPDFView.as_view(), name='event_report_pdf'), - url(r'^api/update_budget/$', UpdateBudgetAPIView.as_view(), name='update event'), - url(r'^api/download_yearly_plan_template/$', DownloadYearlyPlanTemplateAPIView.as_view(), name='download_yearly_plan_template'), - url(r'^api/upload_yearly_plan_excel/$', UploadYearlyPlanExcelAPIView.as_view(), name='upload_yearly_plan_excel'), - url(r'^api/fic_approve_yearly_plan/$', FICApproveYearlyPlanAPIView.as_view(), name='fic_approve_yearly_plan'), - url(r'^api/counsellor_approve_yearly_plan/$', CounsellorApproveYearlyPlanAPIView.as_view(), name='counsellor_approve_yearly_plan'), - url(r'^api/dean_approve_yearly_plan/$', DeanApproveYearlyPlanAPIView.as_view(), name='dean_approve_yearly_plan'), - url(r'^api/reject_yearly_plan/$', RejectYearlyPlanAPIView.as_view(), name='reject_yearly_plan'), - url(r'^api/yearly-plans/', ListYearlyPlansAPIView.as_view(), name='list-yearly-plans'), - url(r'^api/clubwise_yearly_plan/$', ListClubwiseYearlyPlansAPIView.as_view(), name='clubwise-yearly-plans'), - url(r'^api/upload-gallery-image/', UploadGalleryImageAPIView.as_view(), name='upload_images'), - url(r'^api/list-gallery-images', ListGalleryImagesAPIView.as_view(), name='view_images'), + # Auth + path('api/login/', obtain_auth_token, name='api-login'), + + # --- Clubs --- + path('api/clubs/', ListClubsAPIView.as_view(), name='api-list-clubs'), + path('api/clubs/create/', CreateClubAPIView.as_view(), name='api-create-club'), + path('api/clubs/approve/', ClubApproveAPIView.as_view(), name='api-approve-clubs'), + path('api/clubs/reject/', ClubRejectAPIView.as_view(), name='api-reject-clubs'), + path('api/clubs/delete/', DeleteClubAPIView.as_view(), name='api-delete-clubs'), + path('api/clubs/update-name/', UpdateClubNameAPIView.as_view(), name='api-update-club-name'), + path('api/clubs/change-head/', ChangeClubHeadAPIView.as_view(), name='api-change-head'), + path('api/clubs/activity-calendar/', ActivityCalendarAPIView.as_view(), name='api-activity-calendar'), + path('api/clubs//', ClubDetailAPIView.as_view(), name='api-club-detail'), + path('api/clubs//members/', ClubMembersAPIView.as_view(), name='api-club-members'), + path('api/clubs//members/approve/', ApproveMembersAPIView.as_view(), name='api-approve-members'), + + # --- Members --- + path('api/members/join/', MembershipRequestAPIView.as_view(), name='api-join-club'), + path('api/members/approve/', ApproveMembershipAPIView.as_view(), name='api-approve-membership'), + path('api/members/reject/', RejectMembershipAPIView.as_view(), name='api-reject-membership'), + path('api/members/cancel/', CancelMembershipAPIView.as_view(), name='api-cancel-membership'), + path('api/members/delete-form/', DeleteMemberFormAPIView.as_view(), name='api-delete-member-form'), + path('api/members/del-mem/', DeleteMemberAPIView.as_view(), name='api-del-mem'), + + # --- Events --- + path('api/events/', ListEventsAPIView.as_view(), name='api-list-events'), + path('api/events/create/', CreateEventAPIView.as_view(), name='api-create-event'), + path('api/events/new/', NewEventAPIView.as_view(), name='api-new-event'), + path('api/events/approve/', ApproveEventsAPIView.as_view(), name='api-approve-events'), + path('api/events/delete/', DeleteEventsAPIView.as_view(), name='api-delete-events'), + path('api/events/by-date/', DateEventsAPIView.as_view(), name='api-date-events'), + path('api/events/report/', EventReportAPIView.as_view(), name='api-event-report'), + path('api/events//', EventDetailAPIView.as_view(), name='api-event-detail'), + path('api/events//edit/', EditEventAPIView.as_view(), name='api-edit-event'), + + # --- Sessions --- + path('api/sessions/', ListSessionsAPIView.as_view(), name='api-list-sessions'), + path('api/sessions/create/', CreateSessionAPIView.as_view(), name='api-create-session'), + path('api/sessions/new/', NewSessionAPIView.as_view(), name='api-new-session'), + path('api/sessions/bulk-delete/', BulkDeleteSessionsAPIView.as_view(), name='api-bulk-delete-sessions'), + path('api/sessions/delete/', DeleteSessionsAPIView.as_view(), name='api-delete-sessions'), + path('api/sessions/by-date/', DateSessionsAPIView.as_view(), name='api-date-sessions'), + path('api/sessions//edit/', EditSessionAPIView.as_view(), name='api-edit-session'), + + # --- Budget --- + path('api/budget/club/', ClubBudgetAPIView.as_view(), name='api-club-budget'), + path('api/budget/fest/', FestBudgetAPIView.as_view(), name='api-fest-budget'), + path('api/budget/approve/', BudgetApproveAPIView.as_view(), name='api-budget-approve'), + path('api/budget/reject/', BudgetRejectAPIView.as_view(), name='api-budget-reject'), + path('api/budget/update-amount/', UpdateBudgetAmountAPIView.as_view(), name='api-update-budget'), + + # --- Reports --- + path('api/reports/club/', ClubReportAPIView.as_view(), name='api-club-report'), + + # --- Registration --- + path('api/registration/', RegistrationFormAPIView.as_view(), name='api-registration'), + path('api/registration/form-availability/', FormAvailAPIView.as_view(), name='api-form-avail'), + path('api/registration/delete-requests/', DeleteRequestsAPIView.as_view(), name='api-delete-requests'), + + # --- Data / Lookup --- + path('api/data/faculty/', FacultyDataAPIView.as_view(), name='api-faculty-data'), + path('api/data/students/', StudentsDataAPIView.as_view(), name='api-students-data'), + path('api/data/venues/', GetVenueAPIView.as_view(), name='api-get-venue'), + + # --- Voting --- + path('api/voting/polls/', VotingPollAPIView.as_view(), name='api-voting-poll'), + path('api/voting/polls//vote/', VoteAPIView.as_view(), name='api-vote'), + path('api/voting/polls//', DeletePollAPIView.as_view(), name='api-delete-poll'), ] + +urlpatterns = format_suffix_patterns(urlpatterns) \ No newline at end of file diff --git a/FusionIIIT/applications/gymkhana/views.py b/FusionIIIT/applications/gymkhana/views.py index 259bdedd3..092e373d0 100644 --- a/FusionIIIT/applications/gymkhana/views.py +++ b/FusionIIIT/applications/gymkhana/views.py @@ -1355,6 +1355,12 @@ def club_report(request): date = request.POST.get("date") time = request.POST.get("time") report = request.FILES["report"] + + # DEF-008 FIX: validate that both date and time are present before concatenating + if not date or not time: + messages.error(request, "Both date and time are required for the event report.") + return redirect("/gymkhana/") + report.name = club + "_" + event + "_report" # getting queryset class objects @@ -1381,6 +1387,14 @@ def club_report(request): @login_required def change_head(request): + # DEF-004 FIX: only the current coordinator / co-coordinator may change club leadership + if coordinator_club(request) is None: + return HttpResponse( + json.dumps({"status": "error", "message": "Unauthorized: only club coordinators can change leadership."}), + status=403, + content_type="application/json", + ) + if request.method == "POST": club = request.POST.get("club") co_ordinator = request.POST.get("co") @@ -1501,6 +1515,13 @@ def new_session(request): end_time = request.POST.get("end_time") desc = request.POST.get("d_d") club_name = coordinator_club(request) + # DEF-012 FIX: return a clear error if the user is not a club coordinator + if club_name is None: + return HttpResponse( + json.dumps({"status": "error", "message": "Unauthorized: only club coordinators can book sessions."}), + status=403, + content_type="application/json", + ) result = conflict_algorithm_session(date, start_time, end_time, venue) message = "" getstudents = ExtraInfo.objects.select_related("user", "department").filter( @@ -1576,6 +1597,13 @@ def new_event(request): end_time = request.POST.get("end_time") desc = request.POST.get("d_d") club_name = coordinator_club(request) + # DEF-012 FIX: return a clear error if the user is not a club coordinator + if club_name is None: + return HttpResponse( + json.dumps({"status": "error", "message": "Unauthorized: only club coordinators can book events."}), + status=403, + content_type="application/json", + ) result = conflict_algorithm_event(date, start_time, end_time, venue) message = "" getstudents = ExtraInfo.objects.select_related("user", "department").filter( @@ -1653,11 +1681,25 @@ def fest_budget(request): @login_required +@transaction.atomic def approve(request): """ This view is used by the clubs to approve the students who want to join the club and changes the status of the student to 'confirmed'. It gets a list of students who have to be approved and approves them accordingly. - """ + + FIX DEF-001: Authorization guard added — only the coordinator or co-coordinator + of the relevant club may approve membership applications. + FIX DEF-011: @transaction.atomic ensures all approvals succeed or all roll back. + """ + # DEF-001 FIX: verify the requesting user is a coordinator of some club + club_name = coordinator_club(request) + if club_name is None: + return HttpResponse( + json.dumps({"status": "error", "message": "Unauthorized: only club coordinators can approve memberships."}), + status=403, + content_type="application/json", + ) + approve_list = list(request.POST.getlist("check")) for user in approve_list: @@ -1937,6 +1979,11 @@ def conflict_algorithm_session(date, start_time, end_time, venue): """ start_time = datetime.datetime.strptime(start_time, "%H:%M").time() end_time = datetime.datetime.strptime(end_time, "%H:%M").time() + + # DEF-005 FIX: reject invalid time ranges before any DB query + if start_time >= end_time: + return "error" + booked_Sessions = Session_info.objects.select_related( "club", "club__co_ordinator", @@ -1986,12 +2033,22 @@ def get_target_user(groups): values of braches and remove the redundancy and returns the "dic" through Json string @param: groups : This takes the info of which brach and batch can access(voting) this poll + + FIX DEF-010: Malformed group strings (missing ':') are now skipped safely + instead of causing an IndexError. """ dic = {} for i in range(len(groups)): - value = groups[i].split(":") - batch = value[0] - branch = value[1] + # DEF-010 FIX: skip any entry that doesn't contain the expected ':' separator + if ":" not in groups[i]: + logger.warning(f"get_target_user: skipping malformed group entry '{groups[i]}' (missing ':' separator)") + continue + value = groups[i].split(":", 1) # split on first ':' only + batch = value[0].strip() + branch = value[1].strip() + if not batch or not branch: + logger.warning(f"get_target_user: skipping empty batch or branch in '{groups[i]}'") + continue if dic.get(batch): if dic[batch][0] != "All": dic[batch].append(branch) @@ -2001,134 +2058,170 @@ def get_target_user(groups): # Voting Polls -# @login_required -# def voting_poll(request): -# """ -# voting_poll: -# This view creates new voting poll by taking the values from Front-end and add this poll details -# to "Voting_polls" database and also it create and add object of "Voting_choices" contains -# poll_event and title>. Finally it calls gymkhana_voting as per the data given to "groups" -# @param: -# request : trivial -# @variables: -# title : Title of the voting poll -# description : It describes that what this poll is for -# choices : Choices of the voting poll -# exp_data : Expire date of the voting poll -# groups : This takes the info of which brach and batch can access(voting) this poll -# """ -# if request.POST: -# try: -# body = request.POST -# title = body.get("title") -# description = body.get("desc") -# choices = body.getlist("choices") -# exp_date = body.get("expire_date") -# groups = body.getlist("groups") -# target_groups = get_target_user(groups) -# name = request.user.first_name + " " + request.user.last_name -# roll = request.user -# created_by = str(name) + ":" + str(roll) -# new_poll = Voting_polls( -# title=title, -# description=description, -# exp_date=exp_date, -# created_by=str(created_by), -# groups=target_groups, -# ) -# new_poll.save() -# for choice in choices: -# new_choice = Voting_choices(poll_event=new_poll, title=choice) -# new_choice.save() -# for i in range(len(groups)): -# value = groups[i].split(":") -# batch = value[0] -# branch = value[1] -# allbatch = User.objects.filter(username__contains=batch) -# selbranch = ExtraInfo.objects.select_related( -# "user", "department" -# ).filter(department__name=branch) -# batchbranch = User.objects.filter( -# username__contains=batch, extrainfo__in=selbranch -# ) -# if branch == "All": -# gymkhana_voting( -# request.user, allbatch, "voting_open", title, description -# ) -# else: -# gymkhana_voting( -# request.user, batchbranch, "voting_open", title, description -# ) -# return redirect("/gymkhana/") -# except Exception as e: -# res = "error" -# message = "Some error occurred" -# logger.info(e) -# content = {"status": res, "message": message} -# content = json.dumps(content) -# return HttpResponse(content) +@login_required +def voting_poll(request): + """ + voting_poll: + This view creates new voting poll by taking the values from Front-end and add this poll details + to "Voting_polls" database and also it create and add object of "Voting_choices" contains + poll_event and title. Finally it calls gymkhana_voting as per the data given to "groups". + + FIX DEF-003: @login_required + coordinator_club() authorization check added. + FIX DEF-002: Minimum 2 choices enforced server-side. + FIX DEF-010: get_target_user() now handles malformed group strings safely. + """ + # DEF-003 FIX: only club coordinators may create polls + if coordinator_club(request) is None: + return HttpResponse( + json.dumps({"status": "error", "message": "Unauthorized: only club coordinators can create polls."}), + status=403, + content_type="application/json", + ) -# return redirect("/gymkhana/") + if request.POST: + try: + body = request.POST + title = body.get("title") + description = body.get("desc") + choices = body.getlist("choices") + exp_date = body.get("expire_date") + groups = body.getlist("groups") + + # DEF-002 FIX: enforce minimum 2 choices server-side + if len(choices) < 2: + return HttpResponse( + json.dumps({"status": "error", "message": "A poll must have at least 2 choices."}), + content_type="application/json", + ) + target_groups = get_target_user(groups) + name = request.user.first_name + " " + request.user.last_name + roll = request.user + created_by = str(name) + ":" + str(roll) + new_poll = Voting_polls( + title=title, + description=description, + exp_date=exp_date, + created_by=str(created_by), + groups=target_groups, + ) + new_poll.save() + for choice in choices: + new_choice = Voting_choices(poll_event=new_poll, title=choice, votes=0) + new_choice.save() + for i in range(len(groups)): + # DEF-010: malformed entries already skipped inside get_target_user; + # apply the same guard here to avoid IndexError in notifications + if ":" not in groups[i]: + continue + value = groups[i].split(":", 1) + batch = value[0].strip() + branch = value[1].strip() + allbatch = User.objects.filter(username__contains=batch) + selbranch = ExtraInfo.objects.select_related( + "user", "department" + ).filter(department__name=branch) + batchbranch = User.objects.filter( + username__contains=batch, extrainfo__in=selbranch + ) + if branch == "All": + gymkhana_voting( + request.user, allbatch, "voting_open", title, description + ) + else: + gymkhana_voting( + request.user, batchbranch, "voting_open", title, description + ) + return redirect("/gymkhana/") + except Exception as e: + res = "error" + message = "Some error occurred" + logger.info(e) + content = {"status": res, "message": message} + content = json.dumps(content) + return HttpResponse(content) -# @login_required -# def vote(request, poll_id): -# """ -# vote: -# This view will update(increase) votes by 1 for particular 'submitted_choice' then it adds the -# voter(student)ID and poll_event for which he/she votes. Finally it saves to the database -# redirect to '/gymkhana/'. In case of any exception it return "error" -# @param: -# poll_id : ID of the poll -# request : trivial -# @variables: -# submitted_choice : Choice of the user selected for poll_event -# choice : It is a object contains all data of "submitted_choice" from Voting_choices -# new_voter : creating object of Voting_voter to save the voter info -# """ -# poll = Voting_polls.objects.get(pk=poll_id) -# if request.POST: -# try: -# body = request.POST -# submitted_choice = body.get("choice") -# choice = Voting_choices.objects.select_related("poll_event").get( -# pk=submitted_choice -# ) -# choice.votes += 1 -# choice.save() -# new_voter = Voting_voters(poll_event=poll, student_id=str(request.user)) -# new_voter.save() -# return redirect("/gymkhana/") -# except Exception as e: -# logger.info(e) -# return HttpResponse("error") -# data = serializers.serialize( -# "json", Voting_choices.objects.select_related("poll_event").all() -# ) -# return redirect("/gymkhana/") + return redirect("/gymkhana/") -# @login_required -# def delete_poll(request, poll_id): -# """ -# delete_poll: -# This view delete the particular voting poll which is passed through function and redirect -# to "/gymkhana/" if there is an exception then it return the HttpResponse of "error" -# @param: -# request : trivial -# poll_id : id of the poll in Voting_polls -# @variables: -# poll : It is an object stores the all data of poll_id from Voting_poll -# """ -# try: -# poll = Voting_polls.objects.filter(pk=poll_id) -# poll.delete() -# return redirect("/gymkhana/") -# except Exception as e: -# logger.info(e) -# return HttpResponse("error") +@login_required +def vote(request, poll_id): + """ + vote: + This view will update(increase) votes by 1 for particular 'submitted_choice' then it adds the + voter(student)ID and poll_event for which he/she votes. Finally it saves to the database + redirect to '/gymkhana/'. In case of any exception it return "error". + + FIX DEF-006: Target-group membership verified before allowing vote. + FIX DEF-009: Atomic vote increment using F() expression to prevent race conditions. + """ + poll = get_object_or_404(Voting_polls, pk=poll_id) + + if request.POST: + try: + # DEF-006 FIX: verify the requesting student is in the poll's target groups + target = json.loads(poll.groups) if poll.groups else {} + if target: + try: + extra = ExtraInfo.objects.select_related("department").get(user=request.user) + student_batch = str(request.user.username)[:4] # first 4 chars = year batch + student_branch = extra.department.name if extra.department else None + allowed = False + for batch, branches in target.items(): + if batch == student_batch: + if "All" in branches or (student_branch and student_branch in branches): + allowed = True + break + if not allowed: + return HttpResponse( + json.dumps({"status": "error", "message": "You are not eligible to vote in this poll."}), + content_type="application/json", + ) + except ExtraInfo.DoesNotExist: + return HttpResponse( + json.dumps({"status": "error", "message": "Student profile not found."}), + content_type="application/json", + ) + + body = request.POST + submitted_choice = body.get("choice") + + # DEF-009 FIX: use atomic F() update instead of non-atomic read-modify-write + updated = Voting_choices.objects.filter(pk=submitted_choice, poll_event=poll).update( + votes=models.F("votes") + 1 + ) + if updated == 0: + return HttpResponse( + json.dumps({"status": "error", "message": "Invalid choice selected."}), + content_type="application/json", + ) + + new_voter = Voting_voters(poll_event=poll, student_id=str(request.user)) + new_voter.save() + return redirect("/gymkhana/") + except Exception as e: + logger.info(e) + return HttpResponse("error") + + return redirect("/gymkhana/") + + +@login_required +def delete_poll(request, poll_id): + """ + delete_poll: + This view delete the particular voting poll which is passed through function and redirect + to "/gymkhana/" if there is an exception then it return the HttpResponse of "error". + """ + try: + poll = Voting_polls.objects.filter(pk=poll_id) + poll.delete() + return redirect("/gymkhana/") + except Exception as e: + logger.info(e) + return HttpResponse("error") + -# return redirect("/gymkhana/") # this algorithm checks if the passed slot time coflicts with any of already booked events @@ -2211,6 +2304,11 @@ def conflict_algorithm_event(date, start_time, end_time, venue): # converting string to datetime type variable start_time = datetime.datetime.strptime(start_time, "%H:%M").time() end_time = datetime.datetime.strptime(end_time, "%H:%M").time() + + # DEF-005 FIX: reject invalid time ranges before any DB query + if start_time >= end_time: + return "error" + booked_Events = Event_info.objects.select_related( "club", "club__co_ordinator", @@ -2693,6 +2791,4 @@ def update_spent_amount(request): return redirect("/gymkhana/") # Return an error response if not a POST request - return JsonResponse({"status": "error", "message": "Invalid request"}) - - + return JsonResponse({"status": "error", "message": "Invalid request"}) \ No newline at end of file diff --git a/FusionIIIT/applications/gymkhana_v1/__init__.py b/FusionIIIT/applications/gymkhana_v1/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/__init__.py @@ -0,0 +1 @@ + diff --git a/FusionIIIT/applications/gymkhana_v1/admin.py b/FusionIIIT/applications/gymkhana_v1/admin.py new file mode 100644 index 000000000..43a6bd8b1 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/admin.py @@ -0,0 +1,40 @@ +from django.contrib import admin + +from .models import Budget, Club, ClubMember, Event, GalleryItem, Poll, PollOption, PollVote + + +@admin.register(Club) +class ClubAdmin(admin.ModelAdmin): + list_display = ("name", "category", "coordinator", "status", "alloted_budget", "spent_budget") + list_filter = ("category", "status") + search_fields = ("name", "coordinator__username", "co_coordinator__username") + + +@admin.register(ClubMember) +class ClubMemberAdmin(admin.ModelAdmin): + list_display = ("student", "club", "status", "applied_at") + list_filter = ("status", "club") + search_fields = ("student__username", "student__first_name", "student__last_name", "club__name") + + +@admin.register(Event) +class EventAdmin(admin.ModelAdmin): + list_display = ("name", "club", "date", "venue", "status") + list_filter = ("status", "club", "date") + search_fields = ("name", "club__name", "incharge") + + +@admin.register(Budget) +class BudgetAdmin(admin.ModelAdmin): + list_display = ("club", "budget_for", "amount", "status", "created_at") + list_filter = ("status", "club", "budget_type") + + +@admin.register(Poll) +class PollAdmin(admin.ModelAdmin): + list_display = ("title", "pub_date", "exp_date", "created_by") + + +admin.site.register(PollOption) +admin.site.register(PollVote) +admin.site.register(GalleryItem) diff --git a/FusionIIIT/applications/gymkhana_v1/api/__init__.py b/FusionIIIT/applications/gymkhana_v1/api/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/api/__init__.py @@ -0,0 +1 @@ + diff --git a/FusionIIIT/applications/gymkhana_v1/api/serializers.py b/FusionIIIT/applications/gymkhana_v1/api/serializers.py new file mode 100644 index 000000000..e231d0a38 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/api/serializers.py @@ -0,0 +1,274 @@ +import re + +from django.core.exceptions import ValidationError as DjangoValidationError +from django.core.validators import URLValidator +from rest_framework import serializers + +from ..models import Budget, Club, ClubMember, Event, GalleryItem, Poll, PollOption +from ..selectors import get_user_role, get_user_roll_no + + +TEXT_ONLY_PATTERN = re.compile(r"^[A-Za-z][A-Za-z\s.'-]*$") +TEXT_WITH_SYMBOLS_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9\s.,'()&/-]*$") +USERNAME_PATTERN = re.compile(r"^[A-Za-z0-9]+$") +url_validator = URLValidator() + + +def validate_text(value, label, *, allow_numbers=True, min_length=1, required=True): + trimmed = str(value or "").strip() + if not trimmed: + if required: + raise serializers.ValidationError(f"{label} is required.") + return "" + if len(trimmed) < min_length: + raise serializers.ValidationError(f"{label} must be at least {min_length} characters long.") + if not TEXT_WITH_SYMBOLS_PATTERN.match(trimmed): + raise serializers.ValidationError(f"{label} contains invalid characters.") + if not allow_numbers and any(char.isdigit() for char in trimmed): + raise serializers.ValidationError(f"{label} should contain text only.") + if allow_numbers and trimmed.isdigit(): + raise serializers.ValidationError(f"{label} cannot contain numbers only.") + return trimmed + + +def validate_text_only(value, label, *, required=True): + trimmed = str(value or "").strip() + if not trimmed: + if required: + raise serializers.ValidationError(f"{label} is required.") + return "" + if not TEXT_ONLY_PATTERN.match(trimmed): + raise serializers.ValidationError(f"{label} should contain text only.") + return trimmed + + +def validate_identifier(value, label): + trimmed = str(value or "").strip() + if not trimmed: + raise serializers.ValidationError(f"{label} is required.") + if not USERNAME_PATTERN.match(trimmed): + raise serializers.ValidationError(f"{label} should contain only letters and numbers without spaces or symbols.") + return trimmed + + +def validate_url_or_filename(value, label): + trimmed = str(value or "").strip() + if not trimmed: + raise serializers.ValidationError(f"{label} is required.") + if trimmed.isdigit(): + raise serializers.ValidationError(f"{label} cannot be numbers only.") + if trimmed.startswith(("http://", "https://")): + try: + url_validator(trimmed) + except DjangoValidationError as exc: + raise serializers.ValidationError(f"{label} must be a valid URL.") from exc + return trimmed + + +class UserSerializer(serializers.Serializer): + id = serializers.IntegerField(read_only=True) + roll_no = serializers.SerializerMethodField() + username = serializers.CharField(read_only=True) + first_name = serializers.CharField(read_only=True) + last_name = serializers.CharField(read_only=True) + role = serializers.SerializerMethodField() + email = serializers.EmailField(read_only=True) + + def get_roll_no(self, obj): + return get_user_roll_no(obj) + + def get_role(self, obj): + return get_user_role(obj) + + +class ClubSerializer(serializers.ModelSerializer): + coordinator_name = serializers.SerializerMethodField() + co_coordinator_name = serializers.SerializerMethodField() + member_count = serializers.SerializerMethodField() + avail_budget = serializers.ReadOnlyField() + + class Meta: + model = Club + fields = [ + "id", + "name", + "category", + "description", + "coordinator", + "coordinator_name", + "co_coordinator", + "co_coordinator_name", + "faculty_incharge", + "status", + "alloted_budget", + "spent_budget", + "avail_budget", + "member_count", + "activity_calendar", + "created_at", + ] + + def get_coordinator_name(self, obj): + return obj.coordinator.get_full_name() if obj.coordinator else "" + + def get_co_coordinator_name(self, obj): + return obj.co_coordinator.get_full_name() if obj.co_coordinator else "" + + def get_member_count(self, obj): + return obj.members.filter(status__in=["member", "coordinator", "Co-cordinator"]).count() + + +class ClubWriteSerializer(serializers.ModelSerializer): + class Meta: + model = Club + fields = ["name", "category", "description", "coordinator", "co_coordinator", "faculty_incharge", "alloted_budget", "activity_calendar"] + + def validate_name(self, value): + return validate_text(value, "Club name", allow_numbers=True, min_length=3) + + def validate_description(self, value): + return validate_text(value, "Description", allow_numbers=True, min_length=3, required=False) + + def validate_faculty_incharge(self, value): + return validate_text_only(value, "Faculty incharge", required=False) + + def validate_activity_calendar(self, value): + return validate_url_or_filename(value, "Activity calendar") if str(value or "").strip() else "" + + +class ClubMemberSerializer(serializers.ModelSerializer): + student_name = serializers.SerializerMethodField() + student_roll = serializers.SerializerMethodField() + club_name = serializers.CharField(source="club.name", read_only=True) + club_category = serializers.CharField(source="club.category", read_only=True) + + class Meta: + model = ClubMember + fields = ["id", "student", "student_name", "student_roll", "club", "club_name", "club_category", "status", "description", "remarks", "applied_at"] + read_only_fields = ["status", "remarks", "applied_at"] + + def get_student_name(self, obj): + return obj.student.get_full_name() or obj.student.username + + def get_student_roll(self, obj): + return get_user_roll_no(obj.student) + + def validate_description(self, value): + return validate_text(value, "Join request", allow_numbers=True, min_length=10, required=False) + + +class EventSerializer(serializers.ModelSerializer): + club_name = serializers.CharField(source="club.name", read_only=True) + + class Meta: + model = Event + fields = ["id", "club", "club_name", "name", "venue", "date", "start_time", "end_time", "incharge", "details", "status", "created_at"] + read_only_fields = ["status", "created_at"] + + def validate_name(self, value): + return validate_text(value, "Event name", allow_numbers=True, min_length=3) + + def validate_incharge(self, value): + return validate_text_only(value, "Incharge") + + def validate_details(self, value): + return validate_text(value, "Details", allow_numbers=True, min_length=3, required=False) + + def validate(self, attrs): + start_time = attrs.get("start_time") or getattr(self.instance, "start_time", None) + end_time = attrs.get("end_time") or getattr(self.instance, "end_time", None) + if start_time and end_time and end_time <= start_time: + raise serializers.ValidationError({"end_time": "End time must be after start time."}) + return attrs + + +class BudgetSerializer(serializers.ModelSerializer): + club_name = serializers.CharField(source="club.name", read_only=True) + + class Meta: + model = Budget + fields = ["id", "club", "club_name", "budget_type", "budget_for", "amount", "description", "status", "remarks", "created_at"] + read_only_fields = ["status", "remarks", "created_at"] + + def validate_budget_for(self, value): + return validate_text(value, "Budget for", allow_numbers=True, min_length=3) + + def validate_description(self, value): + return validate_text(value, "Description", allow_numbers=True, min_length=3, required=False) + + def validate_amount(self, value): + if value <= 0: + raise serializers.ValidationError("Amount must be greater than zero.") + return value + + +class PollOptionSerializer(serializers.ModelSerializer): + class Meta: + model = PollOption + fields = ["id", "text", "votes", "order"] + + +class PollSerializer(serializers.ModelSerializer): + options = PollOptionSerializer(many=True, read_only=True) + is_active = serializers.ReadOnlyField() + created_by_name = serializers.SerializerMethodField() + has_voted = serializers.SerializerMethodField() + + class Meta: + model = Poll + fields = ["id", "title", "description", "pub_date", "exp_date", "created_by", "created_by_name", "is_active", "options", "has_voted"] + + def get_created_by_name(self, obj): + return obj.created_by.get_full_name() if obj.created_by else "" + + def get_has_voted(self, obj): + request = self.context.get("request") + return obj.votes.filter(voter=request.user).exists() if request and request.user.is_authenticated else False + + +class PollCreateSerializer(serializers.Serializer): + title = serializers.CharField(max_length=200) + description = serializers.CharField(allow_blank=True, default="") + options = serializers.ListField(child=serializers.CharField(max_length=200), min_length=2) + pub_date = serializers.DateField() + exp_date = serializers.DateField() + + def validate_title(self, value): + return validate_text(value, "Title", allow_numbers=True, min_length=3) + + def validate_description(self, value): + return validate_text(value, "Description", allow_numbers=True, min_length=3, required=False) + + def validate_options(self, value): + return [validate_text(option, "Each option", allow_numbers=True, min_length=1) for option in value] + + def validate(self, attrs): + if attrs["exp_date"] <= attrs["pub_date"]: + raise serializers.ValidationError({"exp_date": "Expiry date must be after publish date."}) + return attrs + + def create(self, validated_data): + options = validated_data.pop("options") + poll = Poll.objects.create(**validated_data) + for index, text in enumerate(options): + PollOption.objects.create(poll=poll, text=text, order=index) + return poll + + +class GalleryItemSerializer(serializers.ModelSerializer): + uploaded_by_name = serializers.SerializerMethodField() + club_name = serializers.SerializerMethodField() + + class Meta: + model = GalleryItem + fields = ["id", "club", "club_name", "event", "caption", "image_url", "uploaded_by", "uploaded_by_name", "uploaded_at"] + read_only_fields = ["uploaded_at"] + + def get_uploaded_by_name(self, obj): + return obj.uploaded_by.get_full_name() if obj.uploaded_by else "" + + def get_club_name(self, obj): + return obj.club.name if obj.club else "" + + def validate_caption(self, value): + return validate_text(value, "Caption", allow_numbers=True, min_length=3, required=False) diff --git a/FusionIIIT/applications/gymkhana_v1/api/urls.py b/FusionIIIT/applications/gymkhana_v1/api/urls.py new file mode 100644 index 000000000..58c734620 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/api/urls.py @@ -0,0 +1,32 @@ +from django.urls import path + +from . import views + + +urlpatterns = [ + path("auth/me/", views.me_view, name="gymkhana-v1-api-me"), + path("auth/users/", views.user_search, name="gymkhana-v1-api-user-search"), + path("auth/faculty/", views.faculty_search, name="gymkhana-v1-api-faculty-search"), + path("dashboard/", views.dashboard, name="gymkhana-v1-api-dashboard"), + path("venues/", views.venue_lookup, name="gymkhana-v1-api-venues"), + path("clubs/", views.clubs_list, name="gymkhana-v1-clubs-list"), + path("clubs//", views.club_detail, name="gymkhana-v1-club-detail"), + path("clubs//approve/", views.club_approve, name="gymkhana-v1-club-approve"), + path("clubs//reject/", views.club_reject, name="gymkhana-v1-club-reject"), + path("clubs//calendar/", views.club_upload_calendar, name="gymkhana-v1-club-calendar"), + path("members/", views.members_list, name="gymkhana-v1-members-list"), + path("members//", views.member_update, name="gymkhana-v1-member-update"), + path("events/", views.events_list, name="gymkhana-v1-events-list"), + path("events//", views.event_detail, name="gymkhana-v1-event-detail"), + path("events//approve/", views.event_approve, name="gymkhana-v1-event-approve"), + path("events//reject/", views.event_reject, name="gymkhana-v1-event-reject"), + path("budget/", views.budget_list, name="gymkhana-v1-budget-list"), + path("budget//", views.budget_detail, name="gymkhana-v1-budget-detail"), + path("budget//approve/", views.budget_approve, name="gymkhana-v1-budget-approve"), + path("budget//reject/", views.budget_reject, name="gymkhana-v1-budget-reject"), + path("polls/", views.polls_list, name="gymkhana-v1-polls-list"), + path("polls//", views.poll_delete, name="gymkhana-v1-poll-delete"), + path("polls//vote//", views.cast_vote, name="gymkhana-v1-cast-vote"), + path("gallery/", views.gallery_list, name="gymkhana-v1-gallery-list"), + path("gallery//", views.gallery_detail, name="gymkhana-v1-gallery-detail"), +] diff --git a/FusionIIIT/applications/gymkhana_v1/api/views.py b/FusionIIIT/applications/gymkhana_v1/api/views.py new file mode 100644 index 000000000..710a0f410 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/api/views.py @@ -0,0 +1,504 @@ +import logging + +from django.db import IntegrityError, transaction +from django.utils import timezone +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from ..models import Budget, Club, ClubMember, Event, GalleryItem, Poll, PollOption +from ..selectors import ( + find_venue_conflict, + get_budgets_queryset, + get_clubs_queryset, + get_events_queryset, + get_faculty_suggestions, + get_gallery_queryset, + get_members_queryset, + get_polls_queryset, + get_user_by_roll_no, + get_user_search_queryset, + get_user_role, + get_venue_choices, +) +from ..services import ( + approve_budget, + cast_vote as cast_vote_service, + create_budget, + create_event, + create_gallery_item, + create_membership_request, + create_poll, + delete_gallery_item, + delete_poll, + reject_budget, + update_club_status, + update_event_status, + update_membership, + upload_club_calendar, +) +from .serializers import BudgetSerializer, ClubMemberSerializer, ClubSerializer, ClubWriteSerializer, EventSerializer, GalleryItemSerializer, PollCreateSerializer, PollSerializer, UserSerializer + + +logger = logging.getLogger(__name__) + + +def _is_admin(user): + return get_user_role(user) in ("counsellor", "dean") + + +def _is_coord(user): + return get_user_role(user) == "coordinator" + + +def _is_student(user): + return get_user_role(user) == "student" + + +def _err(message, code=400, request=None, details=None): + log_details = f" details={details}" if details else "" + logger.warning("GYMKHANA_V1_CONSTRAINT: method=%s path=%s user=%s message=%s%s", getattr(request, "method", "N/A"), getattr(request, "path", "N/A"), getattr(getattr(request, "user", None), "username", "anonymous"), message, log_details) + return Response({"error": message}, status=code) + + +def _invalid(serializer, request=None, code=400): + return _err("Serializer validation failed.", code=code, request=request, details=serializer.errors) + + +@api_view(["GET"]) +@permission_classes([IsAuthenticated]) +def me_view(request): + return Response(UserSerializer(request.user).data) + + +@api_view(["GET"]) +@permission_classes([IsAuthenticated]) +def user_search(request): + roll_no = request.query_params.get("roll_no", "").strip() + if roll_no: + try: + user = get_user_by_roll_no(roll_no) + except Exception: + return _err("User not found.", 404, request=request) + return Response(UserSerializer(user).data) + + query = request.query_params.get("q", "").strip() + role = request.query_params.get("role", "").strip() + users = get_user_search_queryset(query, role) + return Response(UserSerializer(users[:40], many=True).data) + + +@api_view(["GET"]) +@permission_classes([IsAuthenticated]) +def faculty_search(request): + query = request.query_params.get("q", "").strip() + faculties = get_faculty_suggestions(query) + results = [] + for faculty in faculties: + full_name = f"{faculty.id.user.first_name} {faculty.id.user.last_name}".strip() + results.append( + { + "id": faculty.id.id, + "name": full_name or faculty.id.user.username, + } + ) + return Response({"results": results}) + + +@api_view(["GET"]) +@permission_classes([IsAuthenticated]) +def venue_lookup(request): + venue_type = request.query_params.get("type", "all") + return Response({"venues": get_venue_choices(venue_type)}) + + +@api_view(["GET", "POST"]) +@permission_classes([IsAuthenticated]) +def clubs_list(request): + if request.method == "GET": + qs = get_clubs_queryset( + status=request.query_params.get("status"), + category=request.query_params.get("category"), + query=request.query_params.get("q"), + ) + return Response(ClubSerializer(qs, many=True).data) + return _err("Club creation is disabled.", 403, request=request) + + +@api_view(["GET", "PATCH", "DELETE"]) +@permission_classes([IsAuthenticated]) +def club_detail(request, pk): + try: + club = Club.objects.get(pk=pk) + except Club.DoesNotExist: + return _err("Club not found.", 404, request=request) + if request.method == "GET": + return Response(ClubSerializer(club).data) + if request.method == "PATCH": + if not (_is_admin(request.user) or club.coordinator == request.user): + return _err("Permission denied.", 403, request=request) + serializer = ClubWriteSerializer(club, data=request.data, partial=True) + if not serializer.is_valid(): + return _invalid(serializer, request=request) + serializer.save() + return Response(ClubSerializer(club).data) + if not _is_admin(request.user): + return _err("Only admins can delete clubs.", 403, request=request) + club.delete() + return Response(status=204) + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def club_approve(request, pk): + if not _is_admin(request.user): + return _err("Permission denied.", 403, request=request) + try: + club = Club.objects.get(pk=pk) + except Club.DoesNotExist: + return _err("Club not found.", 404, request=request) + update_club_status(club, "confirmed") + return Response(ClubSerializer(club).data) + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def club_reject(request, pk): + if not _is_admin(request.user): + return _err("Permission denied.", 403, request=request) + try: + club = Club.objects.get(pk=pk) + except Club.DoesNotExist: + return _err("Club not found.", 404, request=request) + update_club_status(club, "rejected") + return Response(ClubSerializer(club).data) + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def club_upload_calendar(request, pk): + try: + club = Club.objects.get(pk=pk) + except Club.DoesNotExist: + return _err("Club not found.", 404, request=request) + if not (_is_admin(request.user) or club.coordinator == request.user): + return _err("Permission denied.", 403, request=request) + file_url = request.data.get("file_url", "").strip() + if not file_url: + return _err("file_url is required.", request=request) + upload_club_calendar(club, file_url) + return Response(ClubSerializer(club).data) + + +@api_view(["GET", "POST"]) +@permission_classes([IsAuthenticated]) +def members_list(request): + if request.method == "GET": + qs = get_members_queryset( + request.user, + is_student=_is_student(request.user), + is_coord=_is_coord(request.user), + club_id=request.query_params.get("club_id"), + club_name=request.query_params.get("club"), + student_id=request.query_params.get("student"), + status=request.query_params.get("status"), + ) + return Response(ClubMemberSerializer(qs, many=True).data) + club_id = request.data.get("club") + try: + club = Club.objects.get(pk=club_id, status="confirmed") + except Club.DoesNotExist: + return _err("Club not found or not active.", 404, request=request, details={"club": club_id}) + if ClubMember.objects.filter(student=request.user, club=club).exists(): + return _err("You already have a request for this club.", request=request, details={"club": club_id}) + membership = create_membership_request(user=request.user, club=club, description=request.data.get("description", "")) + return Response(ClubMemberSerializer(membership).data, status=201) + + +@api_view(["PATCH"]) +@permission_classes([IsAuthenticated]) +def member_update(request, pk): + try: + membership = ClubMember.objects.select_related("club").get(pk=pk) + except ClubMember.DoesNotExist: + return _err("Not found.", 404, request=request) + is_club_coord = ClubMember.objects.filter(student=request.user, club=membership.club, status__in=["coordinator", "Co-cordinator"]).exists() + if not (_is_admin(request.user) or is_club_coord): + return _err("Permission denied.", 403, request=request) + new_status = request.data.get("status") + if new_status not in ("member", "rejected", "coordinator", "Co-cordinator"): + return _err("Invalid status.", request=request, details={"status": new_status}) + update_membership(membership, status=new_status, remarks=request.data.get("remarks", membership.remarks)) + return Response(ClubMemberSerializer(membership).data) + + +@api_view(["GET", "POST"]) +@permission_classes([IsAuthenticated]) +def events_list(request): + if request.method == "GET": + qs = get_events_queryset( + when=request.query_params.get("when"), + club=request.query_params.get("club"), + status=request.query_params.get("status"), + query=request.query_params.get("q"), + date=request.query_params.get("date"), + ) + return Response(EventSerializer(qs, many=True).data) + if _is_student(request.user): + return _err("Students cannot create events.", 403, request=request) + serializer = EventSerializer(data=request.data) + if not serializer.is_valid(): + return _invalid(serializer, request=request) + club = serializer.validated_data["club"] + if _is_coord(request.user): + allowed = ClubMember.objects.filter(student=request.user, club=club, status__in=["coordinator", "Co-cordinator"]).exists() + if not allowed: + return _err("You can only create events for your own club.", 403, request=request, details={"club": club.id}) + event = create_event(serializer=serializer, created_by=request.user) + return Response(EventSerializer(event).data, status=201) + + +@api_view(["GET", "PATCH", "DELETE"]) +@permission_classes([IsAuthenticated]) +def event_detail(request, pk): + try: + event = Event.objects.select_related("club").get(pk=pk) + except Event.DoesNotExist: + return _err("Event not found.", 404, request=request) + if request.method == "GET": + return Response(EventSerializer(event).data) + if request.method == "DELETE": + if not _is_admin(request.user): + return _err("Only admins can delete events.", 403, request=request) + event.delete() + return Response(status=204) + if not (_is_admin(request.user) or event.created_by == request.user): + return _err("Permission denied.", 403, request=request) + serializer = EventSerializer(event, data=request.data, partial=True) + if not serializer.is_valid(): + return _invalid(serializer, request=request) + next_club = serializer.validated_data.get("club", event.club) + if _is_coord(request.user): + allowed = ClubMember.objects.filter(student=request.user, club=next_club, status__in=["coordinator", "Co-cordinator"]).exists() + if not allowed: + return _err("You can only edit events for your own club.", 403, request=request, details={"club": next_club.id}) + serializer.save() + return Response(EventSerializer(event).data) + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def event_approve(request, pk): + if not _is_admin(request.user): + return _err("Permission denied.", 403, request=request) + try: + event = Event.objects.select_related("club").get(pk=pk) + except Event.DoesNotExist: + return _err("Event not found.", 404, request=request) + if event.status == "confirmed": + return _err("Already approved.", request=request) + conflict = find_venue_conflict(event.venue, event.date, event.start_time, event.end_time, exclude_event_pk=event.pk) + if conflict: + return _err( + f'Venue conflict: "{conflict.name}" ({conflict.club.name}) already booked at {event.venue} from {conflict.start_time} to {conflict.end_time}.', + request=request, + ) + update_event_status(event, "confirmed") + return Response(EventSerializer(event).data) + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def event_reject(request, pk): + if not _is_admin(request.user): + return _err("Permission denied.", 403, request=request) + try: + event = Event.objects.get(pk=pk) + except Event.DoesNotExist: + return _err("Event not found.", 404, request=request) + update_event_status(event, "rejected") + return Response(EventSerializer(event).data) + + +@api_view(["GET", "POST"]) +@permission_classes([IsAuthenticated]) +def budget_list(request): + if request.method == "GET": + if _is_student(request.user): + return _err("Students cannot view budgets.", 403, request=request) + qs = get_budgets_queryset( + request.user, + is_student=_is_student(request.user), + is_coord=_is_coord(request.user), + club=request.query_params.get("club"), + status=request.query_params.get("status"), + budget_type=request.query_params.get("type"), + ) + return Response(BudgetSerializer(qs, many=True).data) + if _is_student(request.user): + return _err("Students cannot request budget.", 403, request=request) + serializer = BudgetSerializer(data=request.data) + if not serializer.is_valid(): + return _invalid(serializer, request=request) + club = serializer.validated_data["club"] + if _is_coord(request.user): + allowed = ClubMember.objects.filter(student=request.user, club=club, status__in=["coordinator", "Co-cordinator"]).exists() + if not allowed: + return _err("You can only request budget for your own club.", 403, request=request, details={"club": club.id}) + budget = create_budget(serializer=serializer, requested_by=request.user) + return Response(BudgetSerializer(budget).data, status=201) + + +@api_view(["GET", "DELETE"]) +@permission_classes([IsAuthenticated]) +def budget_detail(request, pk): + if _is_student(request.user): + return _err("Students cannot access budgets.", 403, request=request) + try: + budget = Budget.objects.select_related("club").get(pk=pk) + except Budget.DoesNotExist: + return _err("Not found.", 404, request=request) + if request.method == "GET": + return Response(BudgetSerializer(budget).data) + if not (_is_admin(request.user) or budget.requested_by == request.user): + return _err("Permission denied.", 403, request=request) + if budget.status != "open": + return _err("Cannot withdraw a decided request.", request=request) + budget.delete() + return Response(status=204) + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def budget_approve(request, pk): + if not _is_admin(request.user): + return _err("Permission denied.", 403, request=request) + try: + with transaction.atomic(): + budget = Budget.objects.select_for_update().select_related("club").get(pk=pk) + if budget.status == "confirmed": + return _err("Already approved.", request=request) + club = Club.objects.select_for_update().get(pk=budget.club_id) + available = club.alloted_budget - club.spent_budget + if budget.amount > available: + return _err( + f"Insufficient budget. Available: Rs.{available:,}, Requested: Rs.{budget.amount:,}.", + request=request, + details={"available": available, "requested": budget.amount}, + ) + budget, club, available = approve_budget(pk, remarks=request.data.get("remarks", "Approved")) + except Budget.DoesNotExist: + return _err("Not found.", 404, request=request) + return Response(BudgetSerializer(budget).data) + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def budget_reject(request, pk): + if not _is_admin(request.user): + return _err("Permission denied.", 403, request=request) + try: + budget = Budget.objects.get(pk=pk) + except Budget.DoesNotExist: + return _err("Not found.", 404, request=request) + reject_budget(budget, remarks=request.data.get("remarks", "Rejected")) + return Response(BudgetSerializer(budget).data) + + +@api_view(["GET", "POST"]) +@permission_classes([IsAuthenticated]) +def polls_list(request): + if request.method == "GET": + polls = get_polls_queryset() + return Response(PollSerializer(polls, many=True, context={"request": request}).data) + if not _is_admin(request.user): + return _err("Only dean/counsellor can create polls.", 403, request=request) + serializer = PollCreateSerializer(data=request.data) + if not serializer.is_valid(): + return _invalid(serializer, request=request) + poll = create_poll(serializer=serializer, created_by=request.user) + return Response(PollSerializer(poll, context={"request": request}).data, status=201) + + +@api_view(["DELETE"]) +@permission_classes([IsAuthenticated]) +def poll_delete(request, pk): + if not _is_admin(request.user): + return _err("Permission denied.", 403, request=request) + try: + poll = Poll.objects.get(pk=pk) + except Poll.DoesNotExist: + return _err("Not found.", 404, request=request) + delete_poll(poll) + return Response(status=204) + + +@api_view(["POST"]) +@permission_classes([IsAuthenticated]) +def cast_vote(request, poll_id, option_id): + try: + poll = Poll.objects.get(pk=poll_id) + option = PollOption.objects.get(pk=option_id, poll=poll) + except (Poll.DoesNotExist, PollOption.DoesNotExist): + return _err("Poll or option not found.", 404, request=request) + if not poll.is_active: + return _err("This poll has ended.", request=request) + try: + cast_vote_service(poll=poll, option=option, voter=request.user) + except IntegrityError: + return _err("You have already voted on this poll.", request=request) + return Response(PollSerializer(poll, context={"request": request}).data) + + +@api_view(["GET", "POST"]) +@permission_classes([IsAuthenticated]) +def gallery_list(request): + if request.method == "GET": + items = get_gallery_queryset( + club_id=request.query_params.get("club"), + event_id=request.query_params.get("event"), + ) + return Response(GalleryItemSerializer(items, many=True).data) + if _is_student(request.user): + return _err("Students cannot upload gallery items.", 403, request=request) + serializer = GalleryItemSerializer(data=request.data) + if not serializer.is_valid(): + return _invalid(serializer, request=request) + item = create_gallery_item(serializer=serializer, uploaded_by=request.user) + return Response(GalleryItemSerializer(item).data, status=201) + + +@api_view(["DELETE"]) +@permission_classes([IsAuthenticated]) +def gallery_detail(request, pk): + try: + item = GalleryItem.objects.get(pk=pk) + except GalleryItem.DoesNotExist: + return _err("Not found.", 404, request=request) + if not (_is_admin(request.user) or item.uploaded_by == request.user): + return _err("Permission denied.", 403, request=request) + delete_gallery_item(item) + return Response(status=204) + + +@api_view(["GET"]) +@permission_classes([IsAuthenticated]) +def dashboard(request): + user = request.user + today = timezone.localdate() + memberships = ClubMember.objects.filter(student=user, status__in=["member", "coordinator", "Co-cordinator"]).select_related("club") + upcoming = Event.objects.filter(date__gte=today, status="confirmed").order_by("date", "start_time")[:4] + data = { + "stats": { + "total_clubs": Club.objects.count(), + "upcoming_events": Event.objects.filter(date__gte=today, status="confirmed").count(), + "my_memberships": memberships.count(), + }, + "upcoming_events": EventSerializer(upcoming, many=True).data, + "my_clubs": ClubMemberSerializer(memberships, many=True).data, + } + if not _is_student(user): + data["stats"]["pending_budgets"] = Budget.objects.filter(status="open").count() + data["stats"]["pending_events"] = Event.objects.filter(status="open").count() + data["stats"]["pending_members"] = ClubMember.objects.filter(status="open").count() + return Response(data) diff --git a/FusionIIIT/applications/gymkhana_v1/apps.py b/FusionIIIT/applications/gymkhana_v1/apps.py new file mode 100644 index 000000000..e0f3757e1 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class GymkhanaV1Config(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "applications.gymkhana_v1" + verbose_name = "Gymkhana V1" diff --git a/FusionIIIT/applications/gymkhana_v1/migrations/0001_initial.py b/FusionIIIT/applications/gymkhana_v1/migrations/0001_initial.py new file mode 100644 index 000000000..5d2864de5 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/migrations/0001_initial.py @@ -0,0 +1,125 @@ +# Generated by Codex for Gymkhana V1 replacement. + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Club", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=100, unique=True)), + ("category", models.CharField(choices=[("Technical", "Technical"), ("Sports", "Sports"), ("Cultural", "Cultural")], max_length=20)), + ("description", models.TextField(blank=True)), + ("faculty_incharge", models.CharField(blank=True, max_length=100)), + ("status", models.CharField(choices=[("open", "Open"), ("confirmed", "Confirmed"), ("rejected", "Rejected")], default="open", max_length=20)), + ("alloted_budget", models.PositiveIntegerField(default=0)), + ("spent_budget", models.PositiveIntegerField(default=0)), + ("activity_calendar", models.CharField(blank=True, max_length=500)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("co_coordinator", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="gymkhana_v1_co_coordinating_clubs", to=settings.AUTH_USER_MODEL)), + ("coordinator", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="gymkhana_v1_coordinating_clubs", to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name="Event", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=200)), + ("venue", models.CharField(choices=[("CR101", "CR101"), ("CR102", "CR102"), ("L101", "L101"), ("L102", "L102"), ("Football Ground", "Football Ground"), ("Cricket Ground", "Cricket Ground"), ("Basketball Ground", "Basketball Ground"), ("Auditorium", "Auditorium"), ("OAT", "OAT"), ("Other", "Other")], default="Other", max_length=50)), + ("date", models.DateField()), + ("start_time", models.TimeField()), + ("end_time", models.TimeField()), + ("incharge", models.CharField(max_length=100)), + ("details", models.TextField(blank=True)), + ("status", models.CharField(choices=[("open", "Open"), ("confirmed", "Confirmed"), ("rejected", "Rejected")], default="open", max_length=20)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("club", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="events", to="gymkhana_v1.club")), + ("created_by", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name="Poll", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("title", models.CharField(max_length=200)), + ("description", models.TextField(blank=True)), + ("pub_date", models.DateField(default=django.utils.timezone.now)), + ("exp_date", models.DateField()), + ("created_by", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name="PollOption", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("text", models.CharField(max_length=200)), + ("votes", models.PositiveIntegerField(default=0)), + ("order", models.PositiveSmallIntegerField(default=0)), + ("poll", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="options", to="gymkhana_v1.poll")), + ], + options={"ordering": ["order"]}, + ), + migrations.CreateModel( + name="GalleryItem", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("caption", models.CharField(blank=True, max_length=300)), + ("image_url", models.CharField(max_length=500)), + ("uploaded_at", models.DateTimeField(auto_now_add=True)), + ("club", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="gallery", to="gymkhana_v1.club")), + ("event", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="gallery", to="gymkhana_v1.event")), + ("uploaded_by", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name="Budget", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("budget_type", models.CharField(choices=[("club", "Club"), ("fest", "Fest")], default="club", max_length=10)), + ("budget_for", models.CharField(max_length=200)), + ("amount", models.PositiveIntegerField()), + ("description", models.TextField(blank=True)), + ("status", models.CharField(choices=[("open", "Open"), ("confirmed", "Confirmed"), ("rejected", "Rejected")], default="open", max_length=20)), + ("remarks", models.CharField(blank=True, max_length=256)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("club", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="budgets", to="gymkhana_v1.club")), + ("requested_by", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name="PollVote", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("voted_at", models.DateTimeField(auto_now_add=True)), + ("option", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="gymkhana_v1.polloption")), + ("poll", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="votes", to="gymkhana_v1.poll")), + ("voter", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={"unique_together": {("poll", "voter")}}, + ), + migrations.CreateModel( + name="ClubMember", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("status", models.CharField(choices=[("open", "Open"), ("member", "Member"), ("coordinator", "Coordinator"), ("Co-cordinator", "Co-Coordinator"), ("rejected", "Rejected")], default="open", max_length=20)), + ("description", models.TextField(blank=True)), + ("remarks", models.CharField(blank=True, max_length=256)), + ("applied_at", models.DateTimeField(auto_now_add=True)), + ("club", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="members", to="gymkhana_v1.club")), + ("student", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="gymkhana_v1_memberships", to=settings.AUTH_USER_MODEL)), + ], + options={"unique_together": {("student", "club")}}, + ), + ] diff --git a/FusionIIIT/applications/gymkhana_v1/migrations/__init__.py b/FusionIIIT/applications/gymkhana_v1/migrations/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/FusionIIIT/applications/gymkhana_v1/models.py b/FusionIIIT/applications/gymkhana_v1/models.py new file mode 100644 index 000000000..9f645d079 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/models.py @@ -0,0 +1,164 @@ +from django.conf import settings +from django.db import models +from django.utils import timezone + + +VENUE_CHOICES = [ + ("CR101", "CR101"), + ("CR102", "CR102"), + ("L101", "L101"), + ("L102", "L102"), + ("Football Ground", "Football Ground"), + ("Cricket Ground", "Cricket Ground"), + ("Basketball Ground", "Basketball Ground"), + ("Auditorium", "Auditorium"), + ("OAT", "OAT"), + ("Other", "Other"), +] + +INDOOR_VENUES = ["CR101", "CR102", "L101", "L102", "Auditorium"] +OUTDOOR_VENUES = ["Football Ground", "Cricket Ground", "Basketball Ground", "OAT"] + + +class Club(models.Model): + CATEGORY_CHOICES = [("Technical", "Technical"), ("Sports", "Sports"), ("Cultural", "Cultural")] + STATUS_CHOICES = [("open", "Open"), ("confirmed", "Confirmed"), ("rejected", "Rejected")] + + name = models.CharField(max_length=100, unique=True) + category = models.CharField(max_length=20, choices=CATEGORY_CHOICES) + description = models.TextField(blank=True) + coordinator = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="gymkhana_v1_coordinating_clubs", + ) + co_coordinator = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="gymkhana_v1_co_coordinating_clubs", + ) + faculty_incharge = models.CharField(max_length=100, blank=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="open") + alloted_budget = models.PositiveIntegerField(default=0) + spent_budget = models.PositiveIntegerField(default=0) + activity_calendar = models.CharField(max_length=500, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name + + @property + def avail_budget(self): + return self.alloted_budget - self.spent_budget + + +class ClubMember(models.Model): + STATUS_CHOICES = [ + ("open", "Open"), + ("member", "Member"), + ("coordinator", "Coordinator"), + ("Co-cordinator", "Co-Coordinator"), + ("rejected", "Rejected"), + ] + + student = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="gymkhana_v1_memberships", + ) + club = models.ForeignKey(Club, on_delete=models.CASCADE, related_name="members") + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="open") + description = models.TextField(blank=True) + remarks = models.CharField(max_length=256, blank=True) + applied_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ("student", "club") + + def __str__(self): + return f"{self.student} -> {self.club} [{self.status}]" + + +class Event(models.Model): + STATUS_CHOICES = [("open", "Open"), ("confirmed", "Confirmed"), ("rejected", "Rejected")] + + club = models.ForeignKey(Club, on_delete=models.CASCADE, related_name="events") + name = models.CharField(max_length=200) + venue = models.CharField(max_length=50, choices=VENUE_CHOICES, default="Other") + date = models.DateField() + start_time = models.TimeField() + end_time = models.TimeField() + incharge = models.CharField(max_length=100) + details = models.TextField(blank=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="open") + created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.name} ({self.club})" + + +class Budget(models.Model): + TYPE_CHOICES = [("club", "Club"), ("fest", "Fest")] + STATUS_CHOICES = [("open", "Open"), ("confirmed", "Confirmed"), ("rejected", "Rejected")] + + club = models.ForeignKey(Club, on_delete=models.CASCADE, related_name="budgets") + budget_type = models.CharField(max_length=10, choices=TYPE_CHOICES, default="club") + budget_for = models.CharField(max_length=200) + amount = models.PositiveIntegerField() + description = models.TextField(blank=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="open") + remarks = models.CharField(max_length=256, blank=True) + requested_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.club} - {self.budget_for} (Rs.{self.amount})" + + +class Poll(models.Model): + title = models.CharField(max_length=200) + description = models.TextField(blank=True) + pub_date = models.DateField(default=timezone.now) + exp_date = models.DateField() + created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True) + + def __str__(self): + return self.title + + @property + def is_active(self): + return self.exp_date >= timezone.localdate() + + +class PollOption(models.Model): + poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="options") + text = models.CharField(max_length=200) + votes = models.PositiveIntegerField(default=0) + order = models.PositiveSmallIntegerField(default=0) + + class Meta: + ordering = ["order"] + + +class PollVote(models.Model): + poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name="votes") + option = models.ForeignKey(PollOption, on_delete=models.CASCADE) + voter = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + voted_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ("poll", "voter") + + +class GalleryItem(models.Model): + club = models.ForeignKey(Club, on_delete=models.CASCADE, related_name="gallery", null=True, blank=True) + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="gallery", null=True, blank=True) + caption = models.CharField(max_length=300, blank=True) + image_url = models.CharField(max_length=500) + uploaded_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True) + uploaded_at = models.DateTimeField(auto_now_add=True) diff --git a/FusionIIIT/applications/gymkhana_v1/selectors.py b/FusionIIIT/applications/gymkhana_v1/selectors.py new file mode 100644 index 000000000..c363edfe1 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/selectors.py @@ -0,0 +1,199 @@ +from django.contrib.auth import get_user_model +from django.db.models import Q +from django.utils import timezone + +from applications.globals.models import Faculty + +from .models import Budget, Club, ClubMember, Event, GalleryItem, INDOOR_VENUES, OUTDOOR_VENUES, Poll, VENUE_CHOICES + + +User = get_user_model() + + +def normalize_role_value(value): + normalized = str(value or "").strip().lower().replace("-", "").replace("_", "").replace(" ", "") + if not normalized: + return "student" + if "dean" in normalized: + return "dean" + if "counsellor" in normalized or normalized in {"fic", "professor", "faculty"}: + return "counsellor" + if "coord" in normalized: + return "coordinator" + if "student" in normalized: + return "student" + return "student" + + +def get_user_role(user): + if not getattr(user, "is_authenticated", False): + return "" + extra = getattr(user, "extrainfo", None) + if extra: + if extra.last_selected_role: + return normalize_role_value(extra.last_selected_role) + if extra.user_type: + return normalize_role_value(extra.user_type) + designation_names = [str(getattr(item.designation, "name", item.designation)) for item in user.current_designation.all()] + for designation in designation_names: + normalized = normalize_role_value(designation) + if normalized != "student": + return normalized + return "student" + + +def get_user_roll_no(user): + extra = getattr(user, "extrainfo", None) + if extra and extra.id: + return extra.id + return user.username + + +def resolve_auth_username(identifier): + if not identifier: + return identifier + if "@" in identifier: + matched_user = User.objects.filter(email__iexact=identifier).first() + return matched_user.username if matched_user else identifier + matched_user = User.objects.filter( + Q(username__iexact=identifier) | Q(extrainfo__id__iexact=identifier) + ).first() + return matched_user.username if matched_user else identifier + + +def get_user_by_roll_no(roll_no): + return User.objects.get(Q(username=roll_no) | Q(extrainfo__id=roll_no)) + + +def get_user_search_queryset(query="", role=""): + qs = User.objects.all() + if query: + qs = qs.filter( + Q(first_name__icontains=query) + | Q(last_name__icontains=query) + | Q(username__icontains=query) + | Q(extrainfo__id__icontains=query) + ) + if role: + target_role = normalize_role_value(role) + user_ids = [user.id for user in qs if get_user_role(user) == target_role] + qs = qs.filter(id__in=user_ids) + return qs + + +def get_faculty_suggestions(query=""): + qs = Faculty.objects.select_related("id__user").all() + if query: + qs = qs.filter( + Q(id__user__first_name__icontains=query) + | Q(id__user__last_name__icontains=query) + | Q(id__id__icontains=query) + ) + return qs[:30] + + +def get_venue_choices(venue_type="all"): + if venue_type == "indoor": + return INDOOR_VENUES + if venue_type == "outdoor": + return OUTDOOR_VENUES + return [value for value, _label in VENUE_CHOICES] + + +def get_coord_clubs(user): + return Club.objects.filter( + Q(coordinator=user) + | Q(co_coordinator=user) + | Q(members__student=user, members__status__in=["coordinator", "Co-cordinator"]) + ).distinct() + + +def get_clubs_queryset(*, status=None, category=None, query=None): + qs = Club.objects.all() + if status: + qs = qs.filter(status=status) + if category: + qs = qs.filter(category=category) + if query: + qs = qs.filter(name__icontains=query) + return qs + + +def get_members_queryset(user, *, is_student=False, is_coord=False, club_id=None, club_name=None, student_id=None, status=None): + if club_id or club_name: + qs = ClubMember.objects.select_related("student", "club").all() + if club_id: + qs = qs.filter(club_id=club_id) + if club_name: + qs = qs.filter(club__name=club_name) + if is_student: + qs = qs.filter(status__in=["member", "coordinator", "Co-cordinator"]) + else: + if is_student: + qs = ClubMember.objects.select_related("student", "club").filter(student=user) + else: + qs = ClubMember.objects.select_related("student", "club").all() + if is_coord: + qs = qs.filter(club_id__in=get_coord_clubs(user).values_list("id", flat=True)) + if student_id: + qs = qs.filter(student_id=student_id) + if status: + qs = qs.filter(status=status) + return qs + + +def get_events_queryset(*, when=None, club=None, status=None, query=None, date=None): + qs = Event.objects.select_related("club").all() + today = timezone.localdate() + if when: + qs = qs.filter(date__gte=today) if when == "upcoming" else qs.filter(date__lt=today) + if club: + qs = qs.filter(club__name=club) + if status: + qs = qs.filter(status=status) + if query: + qs = qs.filter(Q(name__icontains=query) | Q(details__icontains=query)) + if date: + qs = qs.filter(date=date) + return qs.order_by("date", "start_time") + + +def find_venue_conflict(venue, date, start, end, exclude_event_pk=None): + qs = Event.objects.filter( + venue=venue, + date=date, + status="confirmed", + start_time__lt=end, + end_time__gt=start, + ) + if exclude_event_pk: + qs = qs.exclude(pk=exclude_event_pk) + return qs.first() + + +def get_budgets_queryset(user, *, is_student=False, is_coord=False, club=None, status=None, budget_type=None): + if is_student: + return Budget.objects.none() + qs = Budget.objects.select_related("club").all() + if is_coord: + qs = qs.filter(club_id__in=get_coord_clubs(user).values_list("id", flat=True)) + if club: + qs = qs.filter(club__name=club) + if status: + qs = qs.filter(status=status) + if budget_type: + qs = qs.filter(budget_type=budget_type) + return qs + + +def get_polls_queryset(): + return Poll.objects.prefetch_related("options").all().order_by("-pub_date") + + +def get_gallery_queryset(*, club_id=None, event_id=None): + qs = GalleryItem.objects.select_related("club", "event").all().order_by("-uploaded_at") + if club_id: + qs = qs.filter(club_id=club_id) + if event_id: + qs = qs.filter(event_id=event_id) + return qs diff --git a/FusionIIIT/applications/gymkhana_v1/services.py b/FusionIIIT/applications/gymkhana_v1/services.py new file mode 100644 index 000000000..71cc8ffc5 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/services.py @@ -0,0 +1,89 @@ +from django.db import IntegrityError, transaction +from django.db.models import F + +from .models import Budget, Club, ClubMember, GalleryItem, PollOption, PollVote + + +def update_club_status(club, status): + club.status = status + club.save() + return club + + +def upload_club_calendar(club, file_url): + club.activity_calendar = file_url + club.save() + return club + + +def create_membership_request(*, user, club, description=""): + return ClubMember.objects.create(student=user, club=club, description=description, status="open") + + +def update_membership(member, *, status, remarks=""): + member.status = status + member.remarks = remarks or member.remarks + member.save() + return member + + +def create_event(*, serializer, created_by): + return serializer.save(status="open", created_by=created_by) + + +def update_event_status(event, status): + event.status = status + event.save() + return event + + +def create_budget(*, serializer, requested_by): + return serializer.save(status="open", requested_by=requested_by) + + +def approve_budget(budget_id, *, remarks="Approved"): + with transaction.atomic(): + budget = Budget.objects.select_for_update().select_related("club").get(pk=budget_id) + club = Club.objects.select_for_update().get(pk=budget.club_id) + avail = club.alloted_budget - club.spent_budget + if budget.status == "confirmed": + return budget, club, avail + budget.status = "confirmed" + budget.remarks = remarks + budget.save() + club.spent_budget += budget.amount + club.save() + return budget, club, avail + + +def reject_budget(budget, *, remarks="Rejected"): + budget.status = "rejected" + budget.remarks = remarks + budget.save() + return budget + + +def create_poll(*, serializer, created_by): + return serializer.save(created_by=created_by) + + +def delete_poll(poll): + poll.delete() + + +def cast_vote(*, poll, option, voter): + try: + PollVote.objects.create(poll=poll, option=option, voter=voter) + except IntegrityError as exc: + raise exc + PollOption.objects.filter(pk=option.pk).update(votes=F("votes") + 1) + option.refresh_from_db() + return poll + + +def create_gallery_item(*, serializer, uploaded_by): + return serializer.save(uploaded_by=uploaded_by) + + +def delete_gallery_item(item): + item.delete() diff --git a/FusionIIIT/applications/gymkhana_v1/tests/__init__.py b/FusionIIIT/applications/gymkhana_v1/tests/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/FusionIIIT/applications/gymkhana_v1/urls.py b/FusionIIIT/applications/gymkhana_v1/urls.py new file mode 100644 index 000000000..6882f42df --- /dev/null +++ b/FusionIIIT/applications/gymkhana_v1/urls.py @@ -0,0 +1,8 @@ +from django.urls import include, path + + +app_name = "gymkhana_v1" + +urlpatterns = [ + path("api/", include("applications.gymkhana_v1.api.urls")), +] diff --git a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py index a959a85e1..df990ad12 100644 --- a/FusionIIIT/applications/programme_curriculum/api/views_student_management.py +++ b/FusionIIIT/applications/programme_curriculum/api/views_student_management.py @@ -1,6 +1,5 @@ import json import pandas as pd -import openpyxl import random import secrets import string @@ -2025,6 +2024,7 @@ def export_students(request, programme_type): Export student data to Excel """ try: + import openpyxl students = StudentBatchUpload.objects.filter(programme_type=programme_type).order_by('roll_number') if not students.exists(): diff --git a/FusionIIIT/python b/FusionIIIT/python new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/backend-detached-stderr.log b/artifacts/backend-detached-stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/backend-detached-stdout.log b/artifacts/backend-detached-stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/backend-live-stderr.log b/artifacts/backend-live-stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/backend-live-stdout.log b/artifacts/backend-live-stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/backend-stderr.log b/artifacts/backend-stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/backend-stdout.log b/artifacts/backend-stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/backend.stderr.log b/artifacts/backend.stderr.log new file mode 100644 index 000000000..43a9f335c --- /dev/null +++ b/artifacts/backend.stderr.log @@ -0,0 +1,66 @@ +Traceback (most recent call last): + File "manage.py", line 10, in + execute_from_command_line(sys.argv) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\core\management\__init__.py", line 401, in execute_from_command_line + utility.execute() + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\core\management\__init__.py", line 395, in execute + self.fetch_command(subcommand).run_from_argv(self.argv) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\core\management\base.py", line 330, in run_from_argv + self.execute(*args, **cmd_options) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\core\management\commands\runserver.py", line 61, in execute + super().execute(*args, **options) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\core\management\base.py", line 371, in execute + output = self.handle(*args, **options) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\core\management\commands\runserver.py", line 96, in handle + self.run(**options) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\core\management\commands\runserver.py", line 105, in run + self.inner_run(None, **options) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\core\management\commands\runserver.py", line 118, in inner_run + self.check(display_num_errors=True) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\core\management\base.py", line 392, in check + all_issues = checks.run_checks( + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\core\checks\registry.py", line 70, in run_checks + new_errors = check(app_configs=app_configs, databases=databases) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\core\checks\urls.py", line 13, in check_url_config + return check_resolver(resolver) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\core\checks\urls.py", line 23, in check_resolver + return check_method() + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\urls\resolvers.py", line 408, in check + for pattern in self.url_patterns: + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\utils\functional.py", line 48, in __get__ + res = instance.__dict__[self.name] = self.func(instance) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\urls\resolvers.py", line 589, in url_patterns + patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\utils\functional.py", line 48, in __get__ + res = instance.__dict__[self.name] = self.func(instance) + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\urls\resolvers.py", line 582, in urlconf_module + return import_module(self.urlconf_name) + File "C:\Program Files\Python38\lib\importlib\__init__.py", line 127, in import_module + return _bootstrap._gcd_import(name[level:], package, level) + File "", line 1014, in _gcd_import + File "", line 991, in _find_and_load + File "", line 975, in _find_and_load_unlocked + File "", line 671, in _load_unlocked + File "", line 848, in exec_module + File "", line 219, in _call_with_frames_removed + File "C:\Users\bhara\Fusion\FusionIIIT\Fusion\urls.py", line 47, in + url(r'^complaint/', include('applications.complaint_system.urls')), + File "C:\Users\bhara\Fusion\venv\lib\site-packages\django\urls\conf.py", line 34, in include + urlconf_module = import_module(urlconf_module) + File "C:\Program Files\Python38\lib\importlib\__init__.py", line 127, in import_module + return _bootstrap._gcd_import(name[level:], package, level) + File "", line 1014, in _gcd_import + File "", line 991, in _find_and_load + File "", line 975, in _find_and_load_unlocked + File "", line 671, in _load_unlocked + File "", line 848, in exec_module + File "", line 219, in _call_with_frames_removed + File "C:\Users\bhara\Fusion\FusionIIIT\applications\complaint_system\urls.py", line 3, in + from . import views + File "", line 991, in _find_and_load + File "", line 975, in _find_and_load_unlocked + File "", line 671, in _load_unlocked + File "", line 844, in exec_module + File "", line 939, in get_code + File "", line 1038, in get_data +MemoryError diff --git a/artifacts/backend.stdout.log b/artifacts/backend.stdout.log new file mode 100644 index 000000000..3ad749a50 --- /dev/null +++ b/artifacts/backend.stdout.log @@ -0,0 +1,3 @@ +Performing system checks... + +URL patterns [, /'>, /' [name='view_project_inventory']>, /' [name='view_project_staff']>, /' [name='view_financial_outlay']>, /' [name='view_staff_details']>] diff --git a/artifacts/gymkhana_backend_evaluation/Module_Evaluation_Summary.md b/artifacts/gymkhana_backend_evaluation/Module_Evaluation_Summary.md new file mode 100644 index 000000000..e83841682 --- /dev/null +++ b/artifacts/gymkhana_backend_evaluation/Module_Evaluation_Summary.md @@ -0,0 +1,20 @@ +# Gymkhana Backend Module Evaluation Summary + +## Scope + +This workbook is based on the supplied reverse-engineered requirement set: 10 use cases, 22 business rules, and 7 workflows from `Use_Cases.docx`, `Business_Rules.docx`, `Workflows.docx`, and `Traceability_Matrix.docx`. + +## Results + +- UC adequacy: 100.00% +- BR adequacy: 100.00% +- WF adequacy: 100.00% +- Total tests executed: 88 +- Pass: 23 +- Partial: 16 +- Fail: 49 +- Strict pass rate: 26.14% + +## Conclusion + +The module is not ready for acceptance in its current state. Legacy handlers exist for most non-voting use cases, but routed execution is blocked by a startup defect in `applications.gymkhana.urls` -> `applications.gymkhana.api.views` -> `applications.gymkhana.selectors/services`, where a missing `Budget` model import causes Django routing to fail. In addition, the documented voting use cases are not implemented in `applications.gymkhana.views`, and several backend business rules are not structurally enforced. diff --git a/artifacts/gymkhana_backend_evaluation/Short_Report.md b/artifacts/gymkhana_backend_evaluation/Short_Report.md new file mode 100644 index 000000000..ee4c8f17e --- /dev/null +++ b/artifacts/gymkhana_backend_evaluation/Short_Report.md @@ -0,0 +1,31 @@ +# Short Report: Requirements-Based Backend Testing and Evaluation + +## 1. Test Adequacy Summary + +Using the supplied Gymkhana functional requirements, the backend test design covered all 10 documented use cases, all 22 documented business rules, and all 7 documented workflows. This resulted in 30 UC tests, 44 BR tests, and 14 WF tests, which satisfies the required minimum adequacy of 100.00% in each category. + +Execution outcomes were mixed but unfavorable overall. Out of 88 executed tests, 23 passed, 16 were partial, and 49 failed. The strict pass rate was 26.14%. The strongest positive signals came from direct model/view inspection, where some structural rules such as required file fields, cascade deletes, primary-key uniqueness, and category choices were confirmed. The most serious blockers came from runtime route loading and missing voting handlers. + +## 2. Key Failures Found + +The highest-severity issue is a backend startup failure. Importing `applications.gymkhana.urls` pulls in the API layer, which imports selector and service modules that reference a non-existent `Budget` model. This prevents Django from routing requests into the legacy Gymkhana handlers, so many use cases and workflows fail before any business logic can execute. + +A second major failure is that the documented voting use cases do not currently have executable handlers. The requirement documents trace UC006 and UC007 to `voting_poll()` and `vote()`, but those functions are commented out or absent in `applications.gymkhana.views`. As a result, both voting use cases and both voting workflows are effectively not implemented. + +The third major finding is that several business rules are only partially enforced or not enforced at all. Duplicate same-club membership prevention is not backed by a uniqueness constraint on `Club_member`. One-vote-per-student is not backed by any uniqueness constraint on `Voting_voters`. Non-negative budget values are not enforced because the field minimum remains the default integer floor rather than zero. The status enumeration documented in the requirements also no longer matches the broader status choices in the current model. + +## 3. Major Defects + +- DEF-01: Startup routing failure caused by missing `Budget` model import in the Gymkhana API dependency chain. +- DEF-02: Voting handlers `voting_poll`, `vote`, and `delete_poll` are missing/commented out despite being present in the requirements traceability. +- DEF-03: No uniqueness constraint enforces one vote per student per poll. +- DEF-04: No uniqueness constraint enforces duplicate-application prevention for the same student/club pair. +- DEF-05: Negative budget values are allowed by current model validation range. +- DEF-06: Club-member status choices exceed the documented enumeration. +- DEF-07: Minimum poll-choice validation is not enforced in the backend. + +## 4. Final Module Evaluation + +From a specification-based perspective, the Gymkhana backend is only partially aligned with the requirement set. The design adequacy target was met, but end-to-end execution evidence shows that most operational use cases and all workflows are blocked by routing/import failures or missing handler implementations. Some structural business rules are correctly represented at the model layer, but critical behavioral rules are either only partially enforced or not enforced by the backend itself. + +The final evaluation is that the module should be treated as **not ready for submission as a fully working backend** until the startup import chain is repaired, the voting handlers are restored or implemented, and the missing database/model-level constraints are added for the documented business rules. diff --git a/artifacts/gymkhana_backend_evaluation/csv/Artifact_Evaluation.csv b/artifacts/gymkhana_backend_evaluation/csv/Artifact_Evaluation.csv new file mode 100644 index 000000000..6df7ceb00 --- /dev/null +++ b/artifacts/gymkhana_backend_evaluation/csv/Artifact_Evaluation.csv @@ -0,0 +1,40 @@ +Artifact ID,Artifact Type,Tests,Pass,Partial,Fail,Final Status,Remarks +UC001,UC,"UC001-T01, UC001-T02, UC001-T03",1,0,2,Partially Implemented,"Legacy handler exists, but routed execution is blocked by gymkhana URL/API import failures." +UC002,UC,"UC002-T01, UC002-T02, UC002-T03",1,0,2,Partially Implemented,"Legacy handler exists, but routed execution is blocked by gymkhana URL/API import failures." +UC003,UC,"UC003-T01, UC003-T02, UC003-T03",1,0,2,Partially Implemented,"Legacy handler exists, but routed execution is blocked by gymkhana URL/API import failures." +UC004,UC,"UC004-T01, UC004-T02, UC004-T03",1,0,2,Partially Implemented,"Legacy handler exists, but routed execution is blocked by gymkhana URL/API import failures." +UC005,UC,"UC005-T01, UC005-T02, UC005-T03",1,0,2,Partially Implemented,"Legacy handler exists, but routed execution is blocked by gymkhana URL/API import failures." +UC006,UC,"UC006-T01, UC006-T02, UC006-T03",0,0,3,Not Implemented,Requirement-mapped voting handlers are absent/commented out in the backend code. +UC007,UC,"UC007-T01, UC007-T02, UC007-T03",0,0,3,Not Implemented,Requirement-mapped voting handlers are absent/commented out in the backend code. +UC008,UC,"UC008-T01, UC008-T02, UC008-T03",1,0,2,Partially Implemented,"Legacy handler exists, but routed execution is blocked by gymkhana URL/API import failures." +UC009,UC,"UC009-T01, UC009-T02, UC009-T03",1,0,2,Partially Implemented,"Legacy handler exists, but routed execution is blocked by gymkhana URL/API import failures." +UC010,UC,"UC010-T01, UC010-T02, UC010-T03",1,0,2,Partially Implemented,"Legacy handler exists, but routed execution is blocked by gymkhana URL/API import failures." +BR001,BR,"BR001-T01, BR001-T02",0,2,0,Partially Enforced,"Coordinator validation logic exists in the mapped handler, but runtime verification is blocked." +BR002,BR,"BR002-T01, BR002-T02",0,2,0,Partially Enforced,"Faculty validation logic exists in the mapped handler, but runtime verification is blocked." +BR003,BR,"BR003-T01, BR003-T02",0,2,0,Partially Enforced,"Conflict detection helpers exist, but routed backend execution is blocked." +BR004,BR,"BR004-T01, BR004-T02",0,1,1,Incorrectly Enforced,Rule is only implicitly described and not backed by clear backend validation. +BR005,BR,"BR005-T01, BR005-T02",2,0,0,Enforced Correctly,Model structure permits a student to belong to multiple clubs. +BR006,BR,"BR006-T01, BR006-T02",0,0,2,Not Enforced,Duplicate same-club application prevention is not backed by model constraints. +BR007,BR,"BR007-T01, BR007-T02",0,2,0,Partially Enforced,"Authorization helper exists, but routed verification is blocked." +BR008,BR,"BR008-T01, BR008-T02",1,1,0,Partially Enforced,"Status transition logic exists, but invalid-path enforcement could not be fully exercised." +BR009,BR,"BR009-T01, BR009-T02",0,0,2,Not Enforced,Minimum choice count is not backed by executable backend logic. +BR010,BR,"BR010-T01, BR010-T02",0,0,2,Not Enforced,Poll target-group rule depends on a missing voting backend flow. +BR011,BR,"BR011-T01, BR011-T02",0,0,2,Not Enforced,One-vote-per-student rule is not enforced in the backend implementation. +BR012,BR,"BR012-T01, BR012-T02",0,0,2,Not Enforced,Atomic increment rule is not backed by an executable backend path. +BR013,BR,"BR013-T01, BR013-T02",0,2,0,Partially Enforced,"Leadership change code exists, but complete runtime enforcement could not be validated." +BR014,BR,"BR014-T01, BR014-T02",1,1,0,Partially Enforced,"Designation tracking logic exists, but full runtime enforcement could not be completed." +BR015,BR,"BR015-T01, BR015-T02",1,0,1,Incorrectly Enforced,Budget rule allows negative values at the model-validation level. +BR016,BR,"BR016-T01, BR016-T02",2,0,0,Enforced Correctly,Budget file requirement is clearly backed by model constraints. +BR017,BR,"BR017-T01, BR017-T02",2,0,0,Enforced Correctly,Report file requirement is clearly backed by model constraints. +BR018,BR,"BR018-T01, BR018-T02",1,1,0,Partially Enforced,"Concatenation logic exists, but invalid-path handling was not fully verified." +BR019,BR,"BR019-T01, BR019-T02",0,1,1,Incorrectly Enforced,Status choices deviate from the documented three-value enumeration. +BR020,BR,"BR020-T01, BR020-T02",1,1,0,Partially Enforced,"Configured choices match the specification, but invalid-path runtime proof is incomplete." +BR021,BR,"BR021-T01, BR021-T02",2,0,0,Enforced Correctly,Cascade delete is backed by model definitions. +BR022,BR,"BR022-T01, BR022-T02",2,0,0,Enforced Correctly,Primary-key uniqueness is structurally enforced. +WF001,WF,"WF001-T01, WF001-T02",0,0,2,Incorrect,"Workflow handler exists, but routed end-to-end execution is blocked by startup/import failures." +WF002,WF,"WF002-T01, WF002-T02",0,0,2,Incorrect,"Workflow handler exists, but routed end-to-end execution is blocked by startup/import failures." +WF003,WF,"WF003-T01, WF003-T02",0,0,2,Incorrect,"Workflow handler exists, but routed end-to-end execution is blocked by startup/import failures." +WF004,WF,"WF004-T01, WF004-T02",0,0,2,Incorrect,"Workflow handler exists, but routed end-to-end execution is blocked by startup/import failures." +WF005,WF,"WF005-T01, WF005-T02",0,0,2,Missing,Documented voting workflow has no executable backend handler. +WF006,WF,"WF006-T01, WF006-T02",0,0,2,Missing,Documented voting workflow has no executable backend handler. +WF007,WF,"WF007-T01, WF007-T02",0,0,2,Incorrect,"Workflow handler exists, but routed end-to-end execution is blocked by startup/import failures." diff --git a/artifacts/gymkhana_backend_evaluation/csv/BR_Test_Design.csv b/artifacts/gymkhana_backend_evaluation/csv/BR_Test_Design.csv new file mode 100644 index 000000000..62ba5b004 --- /dev/null +++ b/artifacts/gymkhana_backend_evaluation/csv/BR_Test_Design.csv @@ -0,0 +1,45 @@ +Test ID,BR ID,Test Category,Input / Action,Expected Result +BR001-T01,BR001,Valid,Execute backend path with a valid case for rule Valid Student Coordinator,Valid case is accepted: Co-ordinator must be a valid student in Student table +BR001-T02,BR001,Invalid,Execute backend path with an invalid/rejected case for rule Valid Student Coordinator,Invalid case is rejected or prevented by the backend: Co-ordinator must be a valid student in Student table +BR002-T01,BR002,Valid,Execute backend path with a valid case for rule Valid Faculty Incharge,Valid case is accepted: Faculty_incharge must be a valid faculty member in Faculty table +BR002-T02,BR002,Invalid,Execute backend path with an invalid/rejected case for rule Valid Faculty Incharge,Invalid case is rejected or prevented by the backend: Faculty_incharge must be a valid faculty member in Faculty table +BR003-T01,BR003,Valid,Execute backend path with a valid case for rule No Venue Time Conflicts,Valid case is accepted: Sessions and events cannot overlap in same venue at same date/time +BR003-T02,BR003,Invalid,Execute backend path with an invalid/rejected case for rule No Venue Time Conflicts,Invalid case is rejected or prevented by the backend: Sessions and events cannot overlap in same venue at same date/time +BR004-T01,BR004,Valid,Execute backend path with a valid case for rule Valid Time Range,Valid case is accepted: start_time must be before end_time +BR004-T02,BR004,Invalid,Execute backend path with an invalid/rejected case for rule Valid Time Range,Invalid case is rejected or prevented by the backend: start_time must be before end_time +BR005-T01,BR005,Valid,Execute backend path with a valid case for rule Multiple Club Applications,Valid case is accepted: Student can apply to multiple clubs +BR005-T02,BR005,Invalid,Execute backend path with an invalid/rejected case for rule Multiple Club Applications,Invalid case is rejected or prevented by the backend: Student can apply to multiple clubs +BR006-T01,BR006,Valid,Execute backend path with a valid case for rule Duplicate Application Prevention,Valid case is accepted: Student cannot submit duplicate applications to same club +BR006-T02,BR006,Invalid,Execute backend path with an invalid/rejected case for rule Duplicate Application Prevention,Invalid case is rejected or prevented by the backend: Student cannot submit duplicate applications to same club +BR007-T01,BR007,Valid,Execute backend path with a valid case for rule Club Head Authorization,Valid case is accepted: Only club co-ordinator/co-coordinator can approve memberships +BR007-T02,BR007,Invalid,Execute backend path with an invalid/rejected case for rule Club Head Authorization,Invalid case is rejected or prevented by the backend: Only club co-ordinator/co-coordinator can approve memberships +BR008-T01,BR008,Valid,Execute backend path with a valid case for rule Status Transition on Approval,Valid case is accepted: Approval changes status from 'open' to 'confirmed' +BR008-T02,BR008,Invalid,Execute backend path with an invalid/rejected case for rule Status Transition on Approval,Invalid case is rejected or prevented by the backend: Approval changes status from 'open' to 'confirmed' +BR009-T01,BR009,Valid,Execute backend path with a valid case for rule Minimum Poll Choices,Valid case is accepted: Voting poll must have at least 2 choices +BR009-T02,BR009,Invalid,Execute backend path with an invalid/rejected case for rule Minimum Poll Choices,Invalid case is rejected or prevented by the backend: Voting poll must have at least 2 choices +BR010-T01,BR010,Valid,Execute backend path with a valid case for rule Poll Target Groups,Valid case is accepted: Polls target specific batches and branches as JSON +BR010-T02,BR010,Invalid,Execute backend path with an invalid/rejected case for rule Poll Target Groups,Invalid case is rejected or prevented by the backend: Polls target specific batches and branches as JSON +BR011-T01,BR011,Valid,Execute backend path with a valid case for rule One Vote Per Student Per Poll,Valid case is accepted: Student can vote only once per poll +BR011-T02,BR011,Invalid,Execute backend path with an invalid/rejected case for rule One Vote Per Student Per Poll,Invalid case is rejected or prevented by the backend: Student can vote only once per poll +BR012-T01,BR012,Valid,Execute backend path with a valid case for rule Atomic Vote Increment,Valid case is accepted: Vote count increments atomically +BR012-T02,BR012,Invalid,Execute backend path with an invalid/rejected case for rule Atomic Vote Increment,Invalid case is rejected or prevented by the backend: Vote count increments atomically +BR013-T01,BR013,Valid,Execute backend path with a valid case for rule Leadership Succession,Valid case is accepted: Old leadership removed when new leadership assigned +BR013-T02,BR013,Invalid,Execute backend path with an invalid/rejected case for rule Leadership Succession,Invalid case is rejected or prevented by the backend: Old leadership removed when new leadership assigned +BR014-T01,BR014,Valid,Execute backend path with a valid case for rule Designation Tracking,Valid case is accepted: HoldsDesignation records track current club leadership +BR014-T02,BR014,Invalid,Execute backend path with an invalid/rejected case for rule Designation Tracking,Invalid case is rejected or prevented by the backend: HoldsDesignation records track current club leadership +BR015-T01,BR015,Valid,Execute backend path with a valid case for rule Non-Negative Budget,Valid case is accepted: Budget amount must be greater than or equal to 0 +BR015-T02,BR015,Invalid,Execute backend path with an invalid/rejected case for rule Non-Negative Budget,Invalid case is rejected or prevented by the backend: Budget amount must be greater than or equal to 0 +BR016-T01,BR016,Valid,Execute backend path with a valid case for rule Budget File Required,Valid case is accepted: Budget request must include file +BR016-T02,BR016,Invalid,Execute backend path with an invalid/rejected case for rule Budget File Required,Invalid case is rejected or prevented by the backend: Budget request must include file +BR017-T01,BR017,Valid,Execute backend path with a valid case for rule Report File Required,Valid case is accepted: Event reports must include details file +BR017-T02,BR017,Invalid,Execute backend path with an invalid/rejected case for rule Report File Required,Invalid case is rejected or prevented by the backend: Event reports must include details file +BR018-T01,BR018,Valid,Execute backend path with a valid case for rule Date Time Concatenation,Valid case is accepted: Event report date and time concatenated for storage +BR018-T02,BR018,Invalid,Execute backend path with an invalid/rejected case for rule Date Time Concatenation,Invalid case is rejected or prevented by the backend: Event report date and time concatenated for storage +BR019-T01,BR019,Valid,Execute backend path with a valid case for rule Status Enumeration,"Valid case is accepted: Status must be open, confirmed, or rejected" +BR019-T02,BR019,Invalid,Execute backend path with an invalid/rejected case for rule Status Enumeration,"Invalid case is rejected or prevented by the backend: Status must be open, confirmed, or rejected" +BR020-T01,BR020,Valid,Execute backend path with a valid case for rule Category Enumeration,"Valid case is accepted: Club category must be Technical, Sports, or Cultural" +BR020-T02,BR020,Invalid,Execute backend path with an invalid/rejected case for rule Category Enumeration,"Invalid case is rejected or prevented by the backend: Club category must be Technical, Sports, or Cultural" +BR021-T01,BR021,Valid,Execute backend path with a valid case for rule Cascade Delete,Valid case is accepted: Deleting referenced entity deletes dependent records +BR021-T02,BR021,Invalid,Execute backend path with an invalid/rejected case for rule Cascade Delete,Invalid case is rejected or prevented by the backend: Deleting referenced entity deletes dependent records +BR022-T01,BR022,Valid,Execute backend path with a valid case for rule Primary Key Uniqueness,Valid case is accepted: Primary keys must be unique +BR022-T02,BR022,Invalid,Execute backend path with an invalid/rejected case for rule Primary Key Uniqueness,Invalid case is rejected or prevented by the backend: Primary keys must be unique diff --git a/artifacts/gymkhana_backend_evaluation/csv/Defect_Log.csv b/artifacts/gymkhana_backend_evaluation/csv/Defect_Log.csv new file mode 100644 index 000000000..a2970fad0 --- /dev/null +++ b/artifacts/gymkhana_backend_evaluation/csv/Defect_Log.csv @@ -0,0 +1,8 @@ +Defect ID,Related Test ID,Related Artifact,Severity,Description,Suggested Fix +DEF-01,UC001-T01,Module startup / all routed UCs,Critical,"applications.gymkhana.urls imports the new API layer, which fails because selectors/services import a non-existent Budget model. This blocks Django routing before legacy gymkhana handlers can run.",Remove the broken Budget import path or restore the intended model name consistently across selectors/services/API modules before routing the legacy module through the API layer. +DEF-02,UC006-T03,UC006 / UC007 voting use cases,High,"Documented voting handlers voting_poll, vote, and delete_poll are missing/commented out in applications.gymkhana.views, so the voting use cases and workflows have no executable backend implementation.",Restore and test the voting backend handlers or remove the dead routes and requirements traceability until implementation is completed. +DEF-03,BR011-T02,BR011 One Vote Per Student Per Poll,High,"Voting_voters has no unique_together or explicit uniqueness constraint, so one-vote-per-student is not enforced at the model layer.","Add a uniqueness constraint on (poll_event, student_id) and validate duplicate votes in the vote handler." +DEF-04,BR006-T02,BR006 Duplicate Application Prevention,High,"Club_member defines no uniqueness constraint for (member, club), so duplicate same-club applications are not prevented by backend data constraints.",Add a unique constraint for the member/club pair and align the club_membership() error handling with that constraint. +DEF-05,BR015-T02,BR015 Non-Negative Budget,Medium,"Club_budget.budget_amt uses the default IntegerField validator range and does not enforce a minimum of 0, allowing negative values.",Add MinValueValidator(0) or equivalent serializer/form validation for budget amounts. +DEF-06,BR019-T02,BR019 Status Enumeration,Medium,"Club_member.status includes extra values beyond the documented enumeration open/confirmed/rejected, so the backend no longer matches the requirement definition.",Either tighten the documented rule to match the implementation or reduce the field choices to the required three-value enumeration. +DEF-07,BR009-T02,BR009 Minimum Poll Choices,Medium,"The requirement notes UI-only enforcement for minimum poll choices, which means the backend itself does not reject underspecified polls.",Enforce the minimum choice count in backend validation before poll creation. diff --git a/artifacts/gymkhana_backend_evaluation/csv/Module_Test_Summary.csv b/artifacts/gymkhana_backend_evaluation/csv/Module_Test_Summary.csv new file mode 100644 index 000000000..097dece88 --- /dev/null +++ b/artifacts/gymkhana_backend_evaluation/csv/Module_Test_Summary.csv @@ -0,0 +1,18 @@ +Metric,Value +Total Use Cases,10 +Total Business Rules,22 +Total Workflows,7 +Required UC Tests,30 +Designed UC Tests,30 +Required BR Tests,44 +Designed BR Tests,44 +Required WF Tests,14 +Designed WF Tests,14 +UC Adequacy %,100.00% +BR Adequacy %,100.00% +WF Adequacy %,100.00% +Total Tests Executed,88 +Total Pass,23 +Total Partial,16 +Total Fail,49 +Strict Pass Rate %,26.14% diff --git a/artifacts/gymkhana_backend_evaluation/csv/Test_Execution_Log.csv b/artifacts/gymkhana_backend_evaluation/csv/Test_Execution_Log.csv new file mode 100644 index 000000000..d71fb6650 --- /dev/null +++ b/artifacts/gymkhana_backend_evaluation/csv/Test_Execution_Log.csv @@ -0,0 +1,89 @@ +Test ID,Source Type,Source ID,Expected Result,Actual Result,Status,Evidence,Tester +UC001-T01,UC,UC001,New club created with status 'open',"End-to-end execution through Django routing is blocked because applications.gymkhana.urls imports the broken API layer, which fails on a missing Budget model.",Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC001-T02,UC,UC001,Invalid coordinator/co-coordinator/faculty inputs are rejected with the documented error messages,Alternate-path behavior could not be exercised through the running backend because module startup fails before request dispatch.,Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC001-T03,UC,UC001,Handler `new_club` is available for execution,Legacy handler `new_club` imports successfully from applications.gymkhana.views.,Pass,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC002-T01,UC,UC002,Session booked if no venue/time conflicts,"End-to-end execution through Django routing is blocked because applications.gymkhana.urls imports the broken API layer, which fails on a missing Budget model.",Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC002-T02,UC,UC002,Conflicting venue/time slot is rejected,Alternate-path behavior could not be exercised through the running backend because module startup fails before request dispatch.,Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC002-T03,UC,UC002,Handler `new_session` is available for execution,Legacy handler `new_session` imports successfully from applications.gymkhana.views.,Pass,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC003-T01,UC,UC003,Event booked if no venue/time conflicts,"End-to-end execution through Django routing is blocked because applications.gymkhana.urls imports the broken API layer, which fails on a missing Budget model.",Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC003-T02,UC,UC003,Conflicting venue/time slot is rejected,Alternate-path behavior could not be exercised through the running backend because module startup fails before request dispatch.,Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC003-T03,UC,UC003,Handler `new_event` is available for execution,Legacy handler `new_event` imports successfully from applications.gymkhana.views.,Pass,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC004-T01,UC,UC004,Membership application created with status 'open',"End-to-end execution through Django routing is blocked because applications.gymkhana.urls imports the broken API layer, which fails on a missing Budget model.",Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC004-T02,UC,UC004,Duplicate application to same club is rejected,Alternate-path behavior could not be exercised through the running backend because module startup fails before request dispatch.,Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC004-T03,UC,UC004,Handler `club_membership` is available for execution,Legacy handler `club_membership` imports successfully from applications.gymkhana.views.,Pass,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC005-T01,UC,UC005,Selected applications move to status 'confirmed',"End-to-end execution through Django routing is blocked because applications.gymkhana.urls imports the broken API layer, which fails on a missing Budget model.",Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC005-T02,UC,UC005,Unauthorized approval attempt is rejected,Alternate-path behavior could not be exercised through the running backend because module startup fails before request dispatch.,Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC005-T03,UC,UC005,Handler `approve` is available for execution,Legacy handler `approve` imports successfully from applications.gymkhana.views.,Pass,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC006-T01,UC,UC006,Poll created with choices and target groups,"Requirement references handler `voting_poll`, but that symbol is missing/commented out in applications.gymkhana.views.",Fail,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC006-T02,UC,UC006,Invalid poll definition such as insufficient choices is rejected,"Requirement references handler `voting_poll`, but that symbol is missing/commented out in applications.gymkhana.views.",Fail,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC006-T03,UC,UC006,Handler `voting_poll` is available for execution,"Requirement references handler `voting_poll`, but that symbol is missing/commented out in applications.gymkhana.views.",Fail,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC007-T01,UC,UC007,Vote recorded and vote count incremented,"Requirement references handler `vote`, but that symbol is missing/commented out in applications.gymkhana.views.",Fail,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC007-T02,UC,UC007,Duplicate vote is rejected,"Requirement references handler `vote`, but that symbol is missing/commented out in applications.gymkhana.views.",Fail,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC007-T03,UC,UC007,Handler `vote` is available for execution,"Requirement references handler `vote`, but that symbol is missing/commented out in applications.gymkhana.views.",Fail,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC008-T01,UC,UC008,Club leadership and HoldsDesignation records are updated,"End-to-end execution through Django routing is blocked because applications.gymkhana.urls imports the broken API layer, which fails on a missing Budget model.",Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC008-T02,UC,UC008,Invalid new leader details are rejected,Alternate-path behavior could not be exercised through the running backend because module startup fails before request dispatch.,Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC008-T03,UC,UC008,Handler `change_head` is available for execution,Legacy handler `change_head` imports successfully from applications.gymkhana.views.,Pass,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC009-T01,UC,UC009,Budget request created with status 'open',"End-to-end execution through Django routing is blocked because applications.gymkhana.urls imports the broken API layer, which fails on a missing Budget model.",Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC009-T02,UC,UC009,Invalid budget request is rejected,Alternate-path behavior could not be exercised through the running backend because module startup fails before request dispatch.,Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC009-T03,UC,UC009,Handler `club_budget` is available for execution,Legacy handler `club_budget` imports successfully from applications.gymkhana.views.,Pass,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +UC010-T01,UC,UC010,Club event report is submitted,"End-to-end execution through Django routing is blocked because applications.gymkhana.urls imports the broken API layer, which fails on a missing Budget model.",Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC010-T02,UC,UC010,Invalid report submission is rejected,Alternate-path behavior could not be exercised through the running backend because module startup fails before request dispatch.,Fail,evidence/django_check.log; evidence/import_gymkhana_urls.log,Codex +UC010-T03,UC,UC010,Handler `club_report` is available for execution,Legacy handler `club_report` imports successfully from applications.gymkhana.views.,Pass,evidence/model_and_view_checks.log; evidence/gymkhana_view_symbol_scan.txt,Codex +BR001-T01,BR,BR001,Valid case is accepted: Co-ordinator must be a valid student in Student table,"Validation logic is mapped to new_club(), but startup failure prevented end-to-end execution.",Partial,requirements_text/Business_Rules.txt; evidence/model_and_view_checks.log; evidence/import_gymkhana_urls.log,Codex +BR001-T02,BR,BR001,Invalid case is rejected or prevented by the backend: Co-ordinator must be a valid student in Student table,"Invalid coordinator rejection is specified in the handler path, but the backend could not be routed for execution.",Partial,requirements_text/Business_Rules.txt; evidence/model_and_view_checks.log; evidence/import_gymkhana_urls.log,Codex +BR002-T01,BR,BR002,Valid case is accepted: Faculty_incharge must be a valid faculty member in Faculty table,"Faculty lookup validation is mapped to new_club(), but startup failure prevented end-to-end execution.",Partial,requirements_text/Business_Rules.txt; evidence/model_and_view_checks.log; evidence/import_gymkhana_urls.log,Codex +BR002-T02,BR,BR002,Invalid case is rejected or prevented by the backend: Faculty_incharge must be a valid faculty member in Faculty table,Invalid faculty rejection could not be exercised because backend startup fails before routing.,Partial,requirements_text/Business_Rules.txt; evidence/model_and_view_checks.log; evidence/import_gymkhana_urls.log,Codex +BR003-T01,BR,BR003,Valid case is accepted: Sessions and events cannot overlap in same venue at same date/time,Conflict-checking algorithms are documented in the requirements and mapped to session/event booking code.,Partial,requirements_text/Business_Rules.txt; evidence/model_and_view_checks.log; evidence/import_gymkhana_urls.log,Codex +BR003-T02,BR,BR003,Invalid case is rejected or prevented by the backend: Sessions and events cannot overlap in same venue at same date/time,Conflict rejection could not be executed through the routed backend because startup fails first.,Partial,requirements_text/Business_Rules.txt; evidence/model_and_view_checks.log; evidence/import_gymkhana_urls.log,Codex +BR004-T01,BR,BR004,Valid case is accepted: start_time must be before end_time,"Time fields exist, but no cross-field validator was confirmed through model inspection.",Partial,evidence/model_and_view_checks.log; evidence/import_gymkhana_urls.log,Codex +BR004-T02,BR,BR004,Invalid case is rejected or prevented by the backend: start_time must be before end_time,No explicit start/ +vote(poll_id) +UC007 +Voting_choices, Voting_voters +/gymkhana/change_head/ +change_head() +UC008 +Club_info, HoldsDesignation +/gymkhana/club_budget/ +club_budget() +UC009 +Club_budget +/gymkhana/club_event_report/ +club_report() +UC010 +Club_report \ No newline at end of file diff --git a/artifacts/gymkhana_backend_evaluation/requirements_text/Use_Cases.txt b/artifacts/gymkhana_backend_evaluation/requirements_text/Use_Cases.txt new file mode 100644 index 000000000..715542326 --- /dev/null +++ b/artifacts/gymkhana_backend_evaluation/requirements_text/Use_Cases.txt @@ -0,0 +1,295 @@ +USE CASES DOCUMENT +Gymkhana Module +UC001: Create New Club +Use Case ID +UC001 +Use Case Name +Create New Club +Actor +Academic Staff/Dean +Preconditions +User is authenticated with administrative privileges +Postconditions +New club created with status 'open' +Main Flow +1. User navigates to new club creation page +2. System retrieves student and faculty lists +3. User enters club name and selects category +4. User selects co-ordinator from student list +5. User selects co-coordinator from student list +6. User selects faculty incharge +7. User enters club description +8. System validates coordinator exists in Student table +9. System validates co-coordinator exists in Student table +10. System validates faculty exists in Faculty table +11. System creates Club_info record with status='open' +12. System displays success message +Alternate Flows +8a. If coordinator doesn't exist: Display error 'The entered roll number of the co_ordinator does not exist' +9a. If co-coordinator doesn't exist: Display error 'The entered roll number of the co_coordinator does not exist' +10a. If faculty doesn't exist: Display error 'The entered faculty incharge does not exist' +Business Rules +BR001: Co-ordinator must be valid student +BR002: Faculty incharge must be valid faculty +UC002: Book Club Session +Use Case ID +UC002 +Use Case Name +Book Club Session +Actor +Club Head (Co-ordinator/Co-coordinator) +Preconditions +User is authenticated as club coordinator or co-coordinator +Postconditions +Session booked if no venue/time conflicts +Main Flow +1. System identifies user's club via coordinator_club() +2. User selects venue type from Constants.venue +3. User selects specific venue +4. User selects session date +5. User enters start and end time +6. User uploads session poster (optional) +7. User enters session description +8. System calls conflict_algorithm_session(date, start_time, end_time, venue) +9. System retrieves all Session_info matching date and venue +10. System converts times to time objects +11. System creates sorted time slots list +12. System checks for overlapping periods +13. If no conflicts: System creates Session_info record +14. System calls gymkhana_session() to notify students +15. System displays 'Session booked Successfully' +Alternate Flows +12a. If conflicts exist: return 'error' +12b. Display 'The selected time slot conflicts with already booked session' +Business Rules +BR003: No overlapping sessions in same venue +BR004: start_time < end_time +UC003: Book Club Event +Use Case ID +UC003 +Use Case Name +Book Club Event +Actor +Club Head (Co-ordinator/Co-coordinator) +Preconditions +User is authenticated as club coordinator or co-coordinator +Postconditions +Event booked if no venue/time conflicts +Main Flow +1. System identifies user's club +2. User enters event name +3. User enters faculty incharge name +4. User selects venue type and specific venue +5. User selects event date +6. User enters start and end time +7. User uploads event poster +8. User enters event description +9. System calls conflict_algorithm_event(date, start_time, end_time, venue) +10. System retrieves all Event_info matching date and venue +11. System checks for overlapping time periods +12. If no conflicts: System creates Event_info record +13. System calls gymkhana_event() to notify students +14. System displays 'Form dispatched for further process' +Alternate Flows +11a. If conflicts exist: Display 'Selected time slot conflicts with already booked session' +Business Rules +BR003: No overlapping events +BR004: start_time < end_time +UC004: Apply for Club Membership +Use Case ID +UC004 +Use Case Name +Apply for Club Membership +Actor +Student +Preconditions +Student authenticated; Club exists and confirmed +Postconditions +Membership application created with status='open' +Main Flow +1. Student navigates to club membership page +2. System displays available clubs +3. Student selects club +4. Student enters achievements/description +5. System parses user_name to extract roll and username +6. System retrieves User object by username +7. System retrieves ExtraInfo by id and user +8. System retrieves Student by ExtraInfo +9. System retrieves Club_info by club_name +10. System creates Club_member(member=Student, club=Club_info, description, status='open') +11. System displays 'Form dispatched for further process' +Alternate Flows +10a. If duplicate application: Display 'Some error occurred' +Business Rules +BR005: Student can apply to multiple clubs +BR006: No duplicate applications to same club +UC005: Approve Club Membership +Use Case ID +UC005 +Use Case Name +Approve Club Membership +Actor +Club Head +Preconditions +Membership applications exist with status='open' +Postconditions +Selected applications approved with status='confirmed' +Main Flow +1. System displays pending applications for club +2. User selects applications to approve +3. User enters remarks (optional) +4. For each application: +5. Parse user info to get roll and username +6. Retrieve User, ExtraInfo, Student objects +7. Retrieve Club_member by club and student +8. Update Club_member.status='confirmed' +9. Update Club_member.remarks +10. Save Club_member +11. System displays 'Successfully Approved !!!' +Alternate Flows +None +Business Rules +BR007: Only club heads can approve +BR008: Status changes to 'confirmed' +UC006: Create Voting Poll +Use Case ID +UC006 +Use Case Name +Create Voting Poll +Actor +Club Head +Preconditions +User authenticated as club coordinator/co-coordinator +Postconditions +Poll created with choices and target groups +Main Flow +1. User enters poll title and description +2. User enters multiple choice options +3. User selects expiration date +4. User selects target groups (batch:branch) +5. System calls get_target_user(groups) to create JSON +6. System creates Voting_polls record +7. For each choice: Create Voting_choices(poll_event, title, votes=0) +8. For each target group: +9. Parse batch and branch +10. If branch=='All': Get all users with batch +11. Else: Filter by batch and branch +12. Call gymkhana_voting() to notify users +13. System displays success +Alternate Flows +None +Business Rules +BR009: Poll needs ≥2 choices +BR010: Groups stored as JSON +UC007: Vote in Poll +Use Case ID +UC007 +Use Case Name +Vote in Poll +Actor +Student +Preconditions +Student authenticated; Poll active; Student in target group +Postconditions +Vote recorded and count incremented +Main Flow +1. System displays active polls for student +2. Student selects poll +3. System shows poll details and choices +4. Student selects one choice +5. System retrieves Voting_choices by id +6. System increments votes by 1 +7. System saves Voting_choices +8. System creates Voting_voters(poll_event, student_id) +9. System displays success +Alternate Flows +8a. If already voted: Display 'error' +Business Rules +BR011: One vote per student per poll +BR012: Atomic vote increment +UC008: Change Club Leadership +Use Case ID +UC008 +Use Case Name +Change Club Leadership +Actor +Current Club Head +Preconditions +User is current coordinator/co-coordinator +Postconditions +Club leadership updated; HoldsDesignation updated +Main Flow +1. User selects club +2. User enters new co-ordinator username +3. User enters new co-coordinator username +4. User enters change date and time +5. System retrieves Student for new co-ordinator +6. System retrieves Student for new co-coordinator +7. System retrieves Club_info +8. System stores old leadership +9. System updates Club_info.co_ordinator and co_coordinator +10. System saves Club_info +11. System creates HoldsDesignation for new co-ordinator +12. System creates HoldsDesignation for new co-coordinator +13. System deletes HoldsDesignation for old leaders +14. System displays 'Successfully changed !!!' +Alternate Flows +None +Business Rules +BR013: Old leadership removed +BR014: HoldsDesignation tracks current roles +UC009: Request Club Budget +Use Case ID +UC009 +Use Case Name +Request Club Budget +Actor +Club Head +Preconditions +User is club head; Club confirmed +Postconditions +Budget request created with status='open' +Main Flow +1. User selects club +2. User enters budget purpose +3. User enters budget amount +4. User uploads budget file +5. User enters description +6. System renames file to '{club}_budget' +7. System retrieves Club_info +8. System creates Club_budget(club, budget_amt, budget_file, budget_for, description, status='open') +9. System displays 'Successfully requested for the budget !!!' +Alternate Flows +None +Business Rules +BR015: Budget amount ≥0 +BR016: Budget file required +UC010: Submit Club Event Report +Use Case ID +UC010 +Use Case Name +Submit Club Event Report +Actor +Club Head +Preconditions +User is club head; Event occurred +Postconditions +Event report submitted +Main Flow +1. User selects club +2. User enters student incharge (id-username) +3. User enters event name +4. User enters event description +5. User enters date and time +6. User uploads report file +7. System parses user info +8. System retrieves User, ExtraInfo objects +9. System retrieves Club_info +10. System renames file to '{club}_{event}_report' +11. System creates Club_report(club, incharge, event_name, date=date+time, event_details, description) +12. System displays 'Successfully updated the report !!!' +Alternate Flows +None +Business Rules +BR017: Report file required +BR018: Date and time concatenated \ No newline at end of file diff --git a/artifacts/gymkhana_backend_evaluation/requirements_text/Workflows.txt b/artifacts/gymkhana_backend_evaluation/requirements_text/Workflows.txt new file mode 100644 index 000000000..2515fb430 --- /dev/null +++ b/artifacts/gymkhana_backend_evaluation/requirements_text/Workflows.txt @@ -0,0 +1,109 @@ +WORKFLOWS DOCUMENT +Gymkhana Module +WF001: Club Creation Workflow +Description: Process for creating a new club +Workflow Steps: +1. Academic staff initiates club creation (POST /gymkhana/new_club/) +2. System validates coordinator exists: ExtraInfo.objects.filter(user_type='student') +3. System validates co-coordinator exists +4. System validates faculty exists: ExtraInfo.objects.filter(user_type='faculty') +5. System retrieves Student objects via get_object_or_404(Student, id=CO) +6. System retrieves Faculty object via get_object_or_404(Faculty, id=FACUL) +7. System creates Club_info(club_name, category, co_ordinator, co_coordinator, faculty_incharge, description) +8. System saves Club_info with status='open' +9. System redirects to /gymkhana/ +WF002: Session Booking Workflow +Description: Process for booking a club session +Workflow Steps: +1. Club head initiates session booking (POST /gymkhana/new_session/) +2. System calls coordinator_club(request) to identify user's club +3. System receives: venue_type, session_poster, date, start_time, end_time, description +4. System calls conflict_algorithm_session(date, start_time, end_time, venue) +5. conflict_algorithm_session() converts times: datetime.strptime(start_time, '%H:%M').time() +6. Retrieves existing sessions: Session_info.objects.filter(date=date, venue=venue) +7. Creates sorted slots list: slots = [(start_time, end_time)] + existing slots +8. Checks overlaps: if slots[i][0] < counter: flag=1 +9. If no conflicts: Creates Session_info(club, venue, date, start_time, end_time, session_poster, details) +10. Gets students: ExtraInfo.objects.filter(user_type='student') +11. Calls gymkhana_session(request.user, recipients, 'new_session', club_name, desc, venue) +12. Returns JSON: {'status':'success', 'message':'Session booked Successfully'} +WF003: Membership Application Workflow +Description: Process for students applying to clubs +Workflow Steps: +1. Student initiates application (POST /gymkhana/club_membership/) +2. System receives: user_name (format: 'id - username'), club, achievements +3. System parses: USER = user_name.split(' - ') +4. System retrieves: User.objects.get(username=USER[1]) +5. System retrieves: ExtraInfo.objects.get(id=USER[0], user=user_name) +6. System retrieves: Student.objects.get(id=extra) +7. System retrieves: Club_info.objects.get(club_name=club) +8. System creates: Club_member(member=student, club=club_name, description=achievements, status='open') +9. System returns JSON: {'status':'success', 'message':'Form dispatched for further process'} +WF004: Membership Approval Workflow +Description: Process for club heads approving memberships +Workflow Steps: +1. Club head selects applications (POST /gymkhana/approve/) +2. System receives: check[] (list of 'user,club' strings), remarks{user} +3. For each user in approve_list: +4. Parse: user.split(',') → [user_info, club_name] +5. Parse: info = user_info.split(' - ') → [id, username] +6. Retrieve: User.objects.get(username=info[1]) +7. Retrieve: ExtraInfo.objects.get(id=info[0], user=user_name) +8. Retrieve: Student.objects.get(id=extra) +9. Retrieve: Club_member.objects.get(club=club_name, member=student) +10. Update: club_member.status = 'confirmed' +11. Update: club_member.remarks = remarks[0] +12. Save: club_member.save() +13. System redirects to /gymkhana/ +WF005: Voting Poll Creation Workflow +Description: Process for creating voting polls +Workflow Steps: +1. Club head initiates poll creation (POST /gymkhana/voting_poll/) +2. System receives: title, desc, choices[] (list), expire_date, groups[] (list of 'batch:branch') +3. System calls get_target_user(groups) to convert to JSON +4. get_target_user() parses: value.split(':') → [batch, branch] +5. Builds dictionary: dic[batch] = [branch, ...] +6. Returns: json.dumps(dic) +7. System creates created_by: str(name) + ':' + str(roll) +8. System creates: Voting_polls(title, description, exp_date, created_by, groups=target_groups) +9. System saves new_poll +10. For each choice in choices: +11. Create: Voting_choices(poll_event=new_poll, title=choice, votes=0) +12. For each group in groups: +13. Parse: batch, branch = value.split(':') +14. If branch=='All': Get User.objects.filter(username__contains=batch) +15. Else: Filter by ExtraInfo.objects.filter(department__name=branch) +16. Call: gymkhana_voting(request.user, recipients, 'voting_open', title, description) +17. System redirects to /gymkhana/ +WF006: Poll Voting Workflow +Description: Process for students voting in polls +Workflow Steps: +1. Student views active polls +2. Student selects poll and submits vote (POST /gymkhana/{poll_id}/) +3. System retrieves: Voting_polls.objects.get(pk=poll_id) +4. System receives: choice (id of selected choice) +5. System retrieves: Voting_choices.objects.get(pk=submitted_choice) +6. System increments: choice.votes += 1 +7. System saves: choice.save() +8. System creates: Voting_voters(poll_event=poll, student_id=str(request.user)) +9. System saves new_voter +10. System redirects to /gymkhana/ +11. Exception handling: If duplicate vote → return 'error' +WF007: Leadership Change Workflow +Description: Process for changing club leadership +Workflow Steps: +1. Current club head initiates change (POST /gymkhana/change_head/) +2. System receives: club, co (new coordinator username), coco (new co-coordinator username), date, time +3. System retrieves: Student.objects.get(id__user__username=co_ordinator) +4. System retrieves: Student.objects.get(id__user__username=co_coordinator) +5. System retrieves: Club_info.objects.get(club_name=club) +6. System stores: old_co_ordinator = club_info.co_ordinator +7. System stores: old_co_coordinator = club_info.co_coordinator +8. System updates: club_info.co_ordinator = co_ordinator_student +9. System updates: club_info.co_coordinator = co_coordinator_student +10. System saves: club_info.save() +11. System creates: HoldsDesignation(user=User.objects.get(username=co_ordinator), working=same, designation='co-ordinator') +12. System creates: HoldsDesignation for new co-coordinator +13. System deletes: HoldsDesignation.objects.filter(user__username=old_co_ordinator, designation__name='co-ordinator') +14. System deletes: HoldsDesignation for old co-coordinator +15. System returns JSON: {'status':'success', 'message':'Successfully changed !!!'} \ No newline at end of file diff --git a/artifacts/gymkhana_workbook_corrections/Artifact_Evaluation.png b/artifacts/gymkhana_workbook_corrections/Artifact_Evaluation.png new file mode 100644 index 000000000..41c5fcac2 Binary files /dev/null and b/artifacts/gymkhana_workbook_corrections/Artifact_Evaluation.png differ diff --git a/artifacts/gymkhana_workbook_corrections/BR_Test_Design.png b/artifacts/gymkhana_workbook_corrections/BR_Test_Design.png new file mode 100644 index 000000000..5033a703b Binary files /dev/null and b/artifacts/gymkhana_workbook_corrections/BR_Test_Design.png differ diff --git a/artifacts/gymkhana_workbook_corrections/Defect_Log.png b/artifacts/gymkhana_workbook_corrections/Defect_Log.png new file mode 100644 index 000000000..18874da53 Binary files /dev/null and b/artifacts/gymkhana_workbook_corrections/Defect_Log.png differ diff --git a/artifacts/gymkhana_workbook_corrections/Gymkhana_Test_Workbook_Corrected_Codex_20260418.xlsx b/artifacts/gymkhana_workbook_corrections/Gymkhana_Test_Workbook_Corrected_Codex_20260418.xlsx new file mode 100644 index 000000000..9b2b96731 Binary files /dev/null and b/artifacts/gymkhana_workbook_corrections/Gymkhana_Test_Workbook_Corrected_Codex_20260418.xlsx differ diff --git a/artifacts/gymkhana_workbook_corrections/Module_Test_Summary.png b/artifacts/gymkhana_workbook_corrections/Module_Test_Summary.png new file mode 100644 index 000000000..dbed1a73d Binary files /dev/null and b/artifacts/gymkhana_workbook_corrections/Module_Test_Summary.png differ diff --git a/artifacts/gymkhana_workbook_corrections/Test_Execution_Log.png b/artifacts/gymkhana_workbook_corrections/Test_Execution_Log.png new file mode 100644 index 000000000..72a40d140 Binary files /dev/null and b/artifacts/gymkhana_workbook_corrections/Test_Execution_Log.png differ diff --git a/artifacts/gymkhana_workbook_corrections/UC_Test_Design.png b/artifacts/gymkhana_workbook_corrections/UC_Test_Design.png new file mode 100644 index 000000000..a6e66577c Binary files /dev/null and b/artifacts/gymkhana_workbook_corrections/UC_Test_Design.png differ diff --git a/artifacts/gymkhana_workbook_corrections/WF_Test_Design.png b/artifacts/gymkhana_workbook_corrections/WF_Test_Design.png new file mode 100644 index 000000000..4aef34247 Binary files /dev/null and b/artifacts/gymkhana_workbook_corrections/WF_Test_Design.png differ diff --git a/artifacts/probe-stderr.log b/artifacts/probe-stderr.log new file mode 100644 index 000000000..ea43e531d --- /dev/null +++ b/artifacts/probe-stderr.log @@ -0,0 +1,4 @@ + File "", line 1 + import + ^ +SyntaxError: invalid syntax diff --git a/artifacts/probe-stdout.log b/artifacts/probe-stdout.log new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/probe2-stderr.log b/artifacts/probe2-stderr.log new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/probe2-stdout.log b/artifacts/probe2-stdout.log new file mode 100644 index 000000000..190a18037 --- /dev/null +++ b/artifacts/probe2-stdout.log @@ -0,0 +1 @@ +123 diff --git a/artifacts/run_backend_detached.cmd b/artifacts/run_backend_detached.cmd new file mode 100644 index 000000000..2ecf0c60f --- /dev/null +++ b/artifacts/run_backend_detached.cmd @@ -0,0 +1,3 @@ +@echo off +cd /d C:\Users\bhara\Fusion\FusionIIIT +"C:\Users\bhara\Fusion\venv\Scripts\python.exe" manage.py runserver 127.0.0.1:8000 --noreload 1> "C:\Users\bhara\Fusion\artifacts\backend-detached-stdout.log" 2> "C:\Users\bhara\Fusion\artifacts\backend-detached-stderr.log"