From a34021bb5e01b21b14589fa5dcfb10c72ace7d3a Mon Sep 17 00:00:00 2001 From: MM674294 Date: Thu, 23 Apr 2026 21:08:41 +0100 Subject: [PATCH 001/103] Fix get_random_ingredients to return basil instead of parsley --- lumache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lumache.py b/lumache.py index 3ea7ce95c..a48ca9187 100644 --- a/lumache.py +++ b/lumache.py @@ -20,4 +20,4 @@ def get_random_ingredients(kind=None): :return: The ingredients list. :rtype: list[str] """ - return ["shells", "gorgonzola", "parsley"] + return ["shells", "gorgonzola", "basil"] From 7f61878d34cc009cc12225483b5346c238c5c616 Mon Sep 17 00:00:00 2001 From: MM674294 Date: Thu, 23 Apr 2026 21:56:40 +0100 Subject: [PATCH 002/103] Add sys.path configuration to Sphinx documentation builder --- docs/source/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 6e9e8c087..0a97e33b5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,5 +1,9 @@ # Configuration file for the Sphinx documentation builder. +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + # -- Project information project = 'Lumache' From 34026d699a72fd14be35b5bbcb7fe89494d4d1c3 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 23 Apr 2026 23:54:21 +0100 Subject: [PATCH 003/103] Update sys.path configuration for Sphinx documentation and enable autosummary generation --- docs/source/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 0a97e33b5..ae7342cae 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -2,7 +2,9 @@ import os import sys -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath('../..')) + +autosummary_generate = True # -- Project information From 0877d5ed23a992249be726ea7ffb593af599b015 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 23 Apr 2026 23:58:33 +0100 Subject: [PATCH 004/103] Add initial documentation for lumache module with function and exception summaries --- docs/source/generated/lumache.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/source/generated/lumache.rst diff --git a/docs/source/generated/lumache.rst b/docs/source/generated/lumache.rst new file mode 100644 index 000000000..f9d52dee3 --- /dev/null +++ b/docs/source/generated/lumache.rst @@ -0,0 +1,18 @@ +lumache +======= + +.. automodule:: lumache + + + .. rubric:: Functions + + .. autosummary:: + + get_random_ingredients + + .. rubric:: Exceptions + + .. autosummary:: + + InvalidKindError + \ No newline at end of file From 3f5a80291b61d178e9a14be68515bcf7431acf9b Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 00:19:06 +0100 Subject: [PATCH 005/103] Add backend modules to autosummary in API documentation --- docs/source/api.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index ec94338a6..41cb859c1 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -4,4 +4,7 @@ API .. autosummary:: :toctree: generated - lumache + + backend.views + backend.models + backend.serializer From 670e0d2011be32a46306b2adaeba19d5009f357a Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 00:23:58 +0100 Subject: [PATCH 006/103] Refactor API documentation structure and update sys.path for Sphinx Co-authored-by: Copilot --- docs/source/api.rst | 7 +++---- docs/source/conf.py | 2 +- docs/source/generated/lumache.rst | 18 ------------------ 3 files changed, 4 insertions(+), 23 deletions(-) delete mode 100644 docs/source/generated/lumache.rst diff --git a/docs/source/api.rst b/docs/source/api.rst index 41cb859c1..e0a39da1e 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -4,7 +4,6 @@ API .. autosummary:: :toctree: generated - - backend.views - backend.models - backend.serializer + views + models + serializer \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index ae7342cae..87ff239c5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -2,7 +2,7 @@ import os import sys -sys.path.insert(0, os.path.abspath('../..')) +sys.path.insert(0, os.path.abspath('C:\\Users\\stuti\\OneDrive\\SETAP\\SETAP CW\\TERM 2 CW\\UNIsoc\\backend')) autosummary_generate = True diff --git a/docs/source/generated/lumache.rst b/docs/source/generated/lumache.rst deleted file mode 100644 index f9d52dee3..000000000 --- a/docs/source/generated/lumache.rst +++ /dev/null @@ -1,18 +0,0 @@ -lumache -======= - -.. automodule:: lumache - - - .. rubric:: Functions - - .. autosummary:: - - get_random_ingredients - - .. rubric:: Exceptions - - .. autosummary:: - - InvalidKindError - \ No newline at end of file From 0e4c8182402385db4c47c1b1fba5ea6155631725 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 00:30:02 +0100 Subject: [PATCH 007/103] Refactor Sphinx configuration and update project metadata for UNIsoc --- docs/source/conf.py | 48 ++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 87ff239c5..0b35aedb8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,41 +1,39 @@ -# Configuration file for the Sphinx documentation builder. - import os import sys sys.path.insert(0, os.path.abspath('C:\\Users\\stuti\\OneDrive\\SETAP\\SETAP CW\\TERM 2 CW\\UNIsoc\\backend')) +# Mock all Django/DRF dependencies so Sphinx can import your code +from unittest.mock import MagicMock + +class Mock(MagicMock): + @classmethod + def __getattr__(cls, name): + return MagicMock() + +MOCK_MODULES = [ + 'django', 'django.db', 'django.db.models', 'django.utils', + 'django.utils.timezone', 'django.core', 'django.core.mail', + 'rest_framework', 'rest_framework.views', 'rest_framework.response', + 'rest_framework.permissions', 'rest_framework.exceptions', + 'rest_framework.generics', 'rest_framework', 'flask', + 'models', 'serializer', +] +sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) + autosummary_generate = True # -- Project information - -project = 'Lumache' -copyright = '2021, Graziella' -author = 'Graziella' - +project = 'UNIsoc' +copyright = '2024' +author = 'Your Team' release = '0.1' version = '0.1.0' -# -- General configuration - extensions = [ - 'sphinx.ext.duration', - 'sphinx.ext.doctest', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', - 'sphinx.ext.intersphinx', + 'sphinx.ext.duration', ] -intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), - 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), -} -intersphinx_disabled_domains = ['std'] - templates_path = ['_templates'] - -# -- Options for HTML output - -html_theme = 'sphinx_rtd_theme' - -# -- Options for EPUB output -epub_show_urls = 'footnote' +html_theme = 'sphinx_rtd_theme' \ No newline at end of file From b914fcd2d853d7e56153eae70e44b5b092428c04 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 00:31:13 +0100 Subject: [PATCH 008/103] Add initial documentation for models and serializer modules --- docs/source/generated/models.rst | 6 ++++++ docs/source/generated/serializer.rst | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 docs/source/generated/models.rst create mode 100644 docs/source/generated/serializer.rst diff --git a/docs/source/generated/models.rst b/docs/source/generated/models.rst new file mode 100644 index 000000000..23000cf83 --- /dev/null +++ b/docs/source/generated/models.rst @@ -0,0 +1,6 @@ +models +====== + +.. currentmodule:: models + +.. autodata:: models \ No newline at end of file diff --git a/docs/source/generated/serializer.rst b/docs/source/generated/serializer.rst new file mode 100644 index 000000000..a70dfdc62 --- /dev/null +++ b/docs/source/generated/serializer.rst @@ -0,0 +1,6 @@ +serializer +========== + +.. currentmodule:: serializer + +.. autodata:: serializer \ No newline at end of file From 4bd26b87af0c3580bb8e27c49e97ff897570a205 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 00:35:30 +0100 Subject: [PATCH 009/103] Update MOCK_MODULES to include 'views' and 'authentication' for improved Sphinx compatibility Co-authored-by: Copilot --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 0b35aedb8..afd6af2b9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,7 +16,7 @@ def __getattr__(cls, name): 'rest_framework', 'rest_framework.views', 'rest_framework.response', 'rest_framework.permissions', 'rest_framework.exceptions', 'rest_framework.generics', 'rest_framework', 'flask', - 'models', 'serializer', + 'models', 'serializer', 'views', 'authentication', 'authentication.models', ] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) From 5b113710eb1a6e114b13c273d196782f9206eac3 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 00:39:17 +0100 Subject: [PATCH 010/103] Add initial views documentation with autodata inclusion --- docs/source/generated/views.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/source/generated/views.rst diff --git a/docs/source/generated/views.rst b/docs/source/generated/views.rst new file mode 100644 index 000000000..e84eed1fe --- /dev/null +++ b/docs/source/generated/views.rst @@ -0,0 +1,6 @@ +views +===== + +.. currentmodule:: views + +.. autodata:: views \ No newline at end of file From 1bf26790312569ec291d8723a4d8a05c057aafd4 Mon Sep 17 00:00:00 2001 From: MM674294 Date: Fri, 24 Apr 2026 01:03:17 +0100 Subject: [PATCH 011/103] Add views documentation with currentmodule and autodata directives --- docs/source/generated/views.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/source/generated/views.rst diff --git a/docs/source/generated/views.rst b/docs/source/generated/views.rst new file mode 100644 index 000000000..e84eed1fe --- /dev/null +++ b/docs/source/generated/views.rst @@ -0,0 +1,6 @@ +views +===== + +.. currentmodule:: views + +.. autodata:: views \ No newline at end of file From 0b04ddc1eca9967c324d772d2f3b62ec0cea6a8a Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 01:38:29 +0100 Subject: [PATCH 012/103] Remove 'models', 'serializer', and 'views' from MOCK_MODULES for clarity in Sphinx documentation --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index afd6af2b9..a44a90a2a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,7 +16,7 @@ def __getattr__(cls, name): 'rest_framework', 'rest_framework.views', 'rest_framework.response', 'rest_framework.permissions', 'rest_framework.exceptions', 'rest_framework.generics', 'rest_framework', 'flask', - 'models', 'serializer', 'views', 'authentication', 'authentication.models', + 'authentication', 'authentication.models', ] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) From e06ff115c6364a0efa5ac78184cccff1090c14b0 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 01:44:17 +0100 Subject: [PATCH 013/103] Update sys.path in conf.py for relative path and add views.py for API documentation --- docs/source/conf.py | 2 +- docs/source/views.py | 1173 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1174 insertions(+), 1 deletion(-) create mode 100644 docs/source/views.py diff --git a/docs/source/conf.py b/docs/source/conf.py index a44a90a2a..fb831daa2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,6 +1,6 @@ import os import sys -sys.path.insert(0, os.path.abspath('C:\\Users\\stuti\\OneDrive\\SETAP\\SETAP CW\\TERM 2 CW\\UNIsoc\\backend')) +sys.path.insert(0, os.path.abspath('.')) # Mock all Django/DRF dependencies so Sphinx can import your code from unittest.mock import MagicMock diff --git a/docs/source/views.py b/docs/source/views.py new file mode 100644 index 000000000..fdb1c41d5 --- /dev/null +++ b/docs/source/views.py @@ -0,0 +1,1173 @@ +# from flask import request +# from rest_framework import generics +# from .models import User, Event, Society +# from .serializer import UserSerializer +# from .serializer import SocietySerializer +# from rest_framework.views import APIView +# from rest_framework.response import Response +# from rest_framework.permissions import IsAuthenticated +# from rest_framework import status +# from rest_framework.exceptions import PermissionDenied +# from .serializer import EventSerializer +# from .import serializer +# from django.utils.timezone import now +# from django.db.models import Count, Q +# from rest_framework.views import APIView +# from rest_framework.response import Response +# from rest_framework import status +# from rest_framework.permissions import IsAuthenticated +# from django.core.mail import send_mail +# from django.utils import timezone +# from datetime import timedelta + +# from .models import NotificationPreference, Society, Membership, Event + + +# class UserListView(generics.ListAPIView): +# serializer_class = UserSerializer + +# def get_queryset(self): +# queryset = User.objects.all().order_by('name') + +# search = self.request.query_params.get('search') +# letter = self.request.query_params.get('letter') + +# if search: +# queryset = queryset.filter(name__icontains=search) + +# if letter: +# queryset = queryset.filter(name__istartswith=letter) + +# return queryset + +# # class SocietyListView(generics.ListAPIView): +# # queryset = Society.objects.all().order_by('name') +# # serializer_class = SocietySerializer + +# class SocietyListSearchView(APIView): +# permission_classes = [IsAuthenticated] + +# def get(self, request): +# query = request.query_params.get("q", "").strip() + +# societies = Society.objects.filter(is_active=True) + +# if query: +# societies = societies.filter(name__icontains=query) + +# societies = societies.annotate( +# active_member_count=Count( +# 'membership', +# filter=Q(membership__left_at__isnull=True) +# ) +# ).order_by('name') + +# data = [{ +# "id": s.id, +# "name": s.name, +# "category": s.category, +# "description": s.description, +# "member_count": s.active_member_count, # ✅ fixed +# } for s in societies] + +# return Response(data) + +# class AddEventView(generics.CreateAPIView): +# serializer_class = EventSerializer +# permission_classes = [IsAuthenticated] + +# def perform_create(self, serializer): +# if self.request.user.role != "admin": +# raise PermissionDenied("Admins only") + +# society = Society.objects.get(admin=self.request.user) + +# serializer.save( +# created_by=self.request.user, +# society=society +# ) + +# class DeleteEventView(generics.DestroyAPIView): +# permission_classes = [IsAuthenticated] +# serializer_class = EventSerializer +# lookup_field = 'id' + +# def get_queryset(self): +# return Event.objects.filter(created_by=self.request.user) + + +# # class CreateEventView(APIView): +# # permission_classes = [IsAuthenticated] + +# # def post(self, request): + +# # if request.user.role != "admin": +# # return Response({"error": "Admins only"}, status=403) + +# # try: +# # society = Society.objects.get(admin=request.user) +# # except Society.DoesNotExist: +# # return Response({"error": "No society found"}, status=404) + +# # data = request.data.copy() +# # data["society"] = society.id +# # data["created_by"] = request.user.id + +# # serializer = EventSerializer(data=data) + +# # if serializer.is_valid(): +# # event = serializer.save() # capture the event + +# # send_event_confirmation(request.user, event) + +# # return Response(serializer.data, status=201) + +# # return Response(serializer.errors, status=400) + + +# # class CreateEventView(APIView): +# # permission_classes = [IsAuthenticated] + +# # def post(self, request): + +# # if request.user.role != "admin": +# # return Response({"error": "Admins only"}, status=403) + +# # try: +# # society = Society.objects.get(admin=request.user) +# # except Society.DoesNotExist: +# # return Response({"error": "No society found"}, status=404) + +# # data = request.data.copy() + +# # # 🔥 FIX capacity issue +# # if data.get("capacity_limit") in [0, "0", ""]: +# # data["capacity_limit"] = None + +# # serializer = EventSerializer(data=data) + +# # if serializer.is_valid(): +# # event = serializer.save( +# # society=society, # ✅ FIXES NULL ERROR +# # created_by=request.user # ✅ GOOD PRACTICE +# # ) + +# # send_event_confirmation(request.user, event) + +# # return Response(serializer.data, status=201) + +# # print(serializer.errors) # DEBUG +# # return Response(serializer.errors, status=400) + +# class SocietyEventView(APIView): +# permission_classes = [IsAuthenticated] + +# def get(self, request, society_id): + +# try: +# society = Society.objects.get(id=society_id) +# except Society.DoesNotExist: +# return Response({"error": "Society not found"}, status=404) + +# events = Event.objects.filter(society=society) +# serializer = EventSerializer(events, many=True) +# return Response(serializer.data) + +# def post(self, request, society_id): +# if request.user.role != "admin": +# return Response({"error": "Admins only"}, status=403) + +# try: +# society = Society.objects.get(id=society_id, admin=request.user) +# except Society.DoesNotExist: +# return Response({"error": "Society not found or not admin"}, status=404) + +# data = request.data.copy() + +# # ✅ Fix capacity issue +# if data.get("capacity_limit") in [0, "0", ""]: +# data["capacity_limit"] = None + +# serializer = EventSerializer(data=data) + +# if serializer.is_valid(): +# # 🔥 THIS IS THE FIX +# event = serializer.save( +# society=society, +# created_by=request.user +# ) + +# send_event_confirmation(request.user, event) + +# return Response(serializer.data, status=201) + +# print("❌ ERRORS:", serializer.errors) +# return Response(serializer.errors, status=400) + +# class EventDetailView(generics.RetrieveAPIView): +# permission_classes = [IsAuthenticated] +# queryset = Event.objects.all() +# serializer_class = EventSerializer +# lookup_field = 'id' + +# class UpdateEventView(generics.UpdateAPIView): +# permission_classes = [IsAuthenticated] +# queryset = Event.objects.all() +# serializer_class = EventSerializer +# lookup_field = 'id' + +# def get_queryset(self): +# return Event.objects.filter(created_by=self.request.user) + +# class MyEventsView(APIView): +# permission_classes = [IsAuthenticated] + +# def get(self, request): +# if request.user.role == "admin": +# society = Society.objects.get(admin=request.user) +# events = Event.objects.filter(society=society) +# else: +# events = Event.objects.filter( +# society__membership__user=request.user +# ).distinct() + +# serializer = EventSerializer(events, many=True) +# return Response(serializer.data) + +# class AllEventsView(APIView): +# permission_classes = [IsAuthenticated] + +# def get(self, request): +# events = Event.objects.all().order_by('-id')[:5] + +# serializer = EventSerializer(events, many=True) +# return Response(serializer.data) + +# # def get(self, request): +# # events = Event.objects.all().order_by('-created_at')[:5] +# # # events = Event.objects.filter( +# # # start_time__gte=now() # ✅ ONLY FUTURE EVENTS +# # # ).order_by('start_time')[:5] # ✅ SOONEST FIRST + +# # serializer = EventSerializer(events, many=True) +# # return Response(serializer.data) + +# class MyCreatedEventsView(APIView): +# permission_classes = [IsAuthenticated] + +# def get(self, request): +# events = Event.objects.filter(created_by=request.user).order_by('-created_at') +# serializer = EventSerializer(events, many=True) +# return Response(serializer.data) + +# class ChangePasswordView(APIView): +# permission_classes = [IsAuthenticated] + +# def post(self, request): +# user = request.user +# old_password = request.data.get("old_password") +# new_password = request.data.get("new_password") + +# if not user.check_password(old_password): +# return Response({"error": "Old password is incorrect"}, status=400) + +# user.set_password(new_password) +# user.save() +# return Response({"message": "Password changed successfully"}) + +# class ChangeEmailView(APIView): +# permission_classes = [IsAuthenticated] + +# def post(self, request): +# user = request.user +# new_email = request.data.get("new_email") + +# if not new_email: +# return Response({"error": "New email is required"}, status=400) + +# if User.objects.filter(email=new_email).exists(): +# return Response({"error": "Email already in use"}, status=400) + +# user.email = new_email +# user.save() +# return Response({"message": "Email changed successfully"}) + +# class User_ProfileView(APIView): +# permission_classes = [IsAuthenticated] + +# def get(self, request): +# user = request.user +# serializer = UserSerializer(user) + +# return Response(serializer.data) + +# def post(self, request): +# user = request.user +# new_name = request.data.get("name") + +# if not new_name: +# return Response({"error": "New name is required"}, status=400) + +# user.name = new_name +# user.save() +# return Response({"message": "Name changed successfully"}) + + +# class NotificationView(APIView): +# permission_classes = [IsAuthenticated] + +# # GET USER PREFERENCES +# def get(self, request): +# user = request.user +# preferences = NotificationPreference.objects.filter(user=user) + +# data = [] +# for pref in preferences: +# data.append({ +# "society": pref.society.name, +# "notify_new_events": pref.notify_new_events, # ✅ FIXED +# }) + +# return Response(data) + +# # UPDATE PREFERENCES +# def post(self, request): +# user = request.user +# society_id = request.data.get("society_id") + +# # safer boolean handling +# notify_new_events = str(request.data.get("event_notifications")).lower() == "true" + +# try: +# society = Society.objects.get(id=society_id) +# except Society.DoesNotExist: +# return Response({"error": "Society not found"}, status=404) + +# if not Membership.objects.filter(user=user, society=society).exists(): +# return Response({"error": "Not a member of this society"}, status=403) + +# pref, created = NotificationPreference.objects.update_or_create( +# user=user, +# society=society, +# defaults={ +# "notify_new_events": notify_new_events +# } +# ) + +# return Response({ +# "message": "Notification preferences updated", +# "society": society.name, +# "notify_new_events": pref.notify_new_events +# }) + + +# # def send_event_confirmation(user, event): +# # if not NotificationPreference.objects.filter( +# # user=user, +# # society=event.society, +# # notify_new_events=True +# # ).exists(): +# # return + +# # send_mail( +# # subject="Event Created Successfully", +# # message=f""" +# # Your event "{event.title}" has been created successfully. + +# # Date: {event.start_time} +# # Location: {event.location} +# # """, +# # from_email=None, +# # recipient_list=[user.email], +# # fail_silently=False, +# # ) + +# def send_event_confirmation(admin_user, event): +# """ +# Send emails to all users in the society who have opted in for new event notifications. +# """ +# # Get all NotificationPreferences for the society where users want new event emails +# prefs = NotificationPreference.objects.filter( +# society=event.society, +# notify_new_events=True +# ) + +# # Collect user emails +# recipient_emails = [pref.user.email for pref in prefs if pref.user.email] + +# if not recipient_emails: +# return # No one to notify + +# subject = f"New Event: {event.title}" +# message = f""" +# Hello, + +# A new event has been created in your society: {event.society.name} + +# Title: {event.title} +# Description: {event.description} +# Start: {event.start_time} +# End: {event.end_time} + +# Please check the portal for more details. +# """ + +# send_mail( +# subject=subject, +# message=message, +# from_email="no-reply@yoursite.com", # replace with your from email +# recipient_list=recipient_emails, +# fail_silently=False, +# ) + + +# def send_event_reminders(): +# now = timezone.now() +# upcoming = now + timedelta(hours=24) + +# events = Event.objects.filter(start_time__range=(now, upcoming)) + +# for event in events: +# admins = Membership.objects.filter( +# society=event.society, +# role="admin" +# ) + +# for member in admins: +# user = member.user + +# if not NotificationPreference.objects.filter( +# user=user, +# society=event.society, +# notify_24hr_reminder=True +# ).exists(): +# continue + +# send_mail( +# subject="Reminder: Event in 24 Hours", +# message=f""" +# Reminder: "{event.title}" is in 24 hours. + +# Date: {event.start_time} +# Location: {event.location} +# """, +# from_email=None, +# recipient_list=[user.email], +# fail_silently=False, +# ) + + +################################################################################### +# CODE DOCUMENTATION VIEWS BELOW +################################################################################### + + +from flask import request +from rest_framework import generics +from .models import User, Event, Society +from .serializer import UserSerializer +from .serializer import SocietySerializer +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from rest_framework import status +from .serializer import EventSerializer +from .import serializer +from django.utils.timezone import now +from django.db.models import Count, Q +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from django.core.mail import send_mail +from django.utils import timezone +from datetime import timedelta + +from .models import NotificationPreference, Society, Membership, Event + + + +class MySocietiesView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + try: + # Debug: Log the user making the request + print(f"Fetching societies for user: {request.user}") + + # Fetch societies the user has joined using the Membership model + memberships = Membership.objects.filter(user=request.user, left_at__isnull=True) + societies = [membership.society for membership in memberships] + + # Debug: Log the societies fetched + print(f"Societies fetched: {societies}") + + data = [ + { + "id": s.id, + "name": s.name, + "description": s.description, + "member_count": s.member_count, + } + for s in societies + ] + + return Response(data) + + except Exception as e: + # Debug: Log the error + print(f"Error in MySocietiesView: {e}") + return Response({"error": str(e)}, status=500) + + + + +class UserListView(generics.ListAPIView): + """API view to list all users, with optional search and letter filtering. + + Supports the following query parameters: + + - ``search``: Filter users whose name contains the search string (case-insensitive). + - ``letter``: Filter users whose name starts with the given letter (case-insensitive). + + Results are ordered alphabetically by name. + """ + + serializer_class = UserSerializer + + def get_queryset(self): + """Return a filtered and ordered queryset of all users. + + :return: Queryset of User objects filtered by search/letter params. + :rtype: QuerySet + """ + queryset = User.objects.all().order_by('name') + + search = self.request.query_params.get('search') + letter = self.request.query_params.get('letter') + + if search: + queryset = queryset.filter(name__icontains=search) + + if letter: + queryset = queryset.filter(name__istartswith=letter) + + return queryset + + +class SocietyListSearchView(APIView): + """API view to list and search active societies. + + Requires authentication. Supports an optional ``q`` query parameter + to filter societies by name. Results include the active member count + for each society and are ordered alphabetically by name. + """ + + permission_classes = [IsAuthenticated] + + def get(self, request): + """Return a list of active societies, optionally filtered by name. + + :param request: The HTTP request, optionally containing a ``q`` query param. + :type request: Request + :return: A list of society objects with id, name, category, description, and member count. + :rtype: Response + """ + query = request.query_params.get("q", "").strip() + + societies = Society.objects.filter(is_active=True) + + if query: + societies = societies.filter(name__icontains=query) + + societies = societies.annotate( + active_member_count=Count( + 'membership', + filter=Q(membership__left_at__isnull=True) + ) + ).order_by('name') + + data = [{ + "id": s.id, + "name": s.name, + "category": s.category, + "description": s.description, + "member_count": s.active_member_count, + } for s in societies] + + return Response(data) + + +class AddEventView(generics.CreateAPIView): + """API view to create a new event for the authenticated admin's society. + + Requires authentication. Only users with the ``admin`` role can create events. + The event is automatically linked to the society managed by the authenticated admin. + + :raises PermissionDenied: If the authenticated user is not an admin. + """ + + serializer_class = EventSerializer + permission_classes = [IsAuthenticated] + + def perform_create(self, serializer): + """Save the new event, associating it with the admin's society. + + :param serializer: The validated event serializer instance. + :type serializer: EventSerializer + :raises PermissionDenied: If the user does not have the admin role. + """ + if self.request.user.role != "admin": + raise PermissionDenied("Admins only") + + society = Society.objects.get(admin=self.request.user) + + serializer.save( + created_by=self.request.user, + society=society + ) + serializer_class = SocietySerializer + + +class DeleteEventView(generics.DestroyAPIView): + """API view to delete an event created by the authenticated user. + + Requires authentication. Users can only delete events they created themselves. + """ + + permission_classes = [IsAuthenticated] + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + """Return only events created by the authenticated user. + + :return: Queryset of Event objects created by the current user. + :rtype: QuerySet + """ + return Event.objects.filter(created_by=self.request.user) + + + serializer_class = SocietySerializer + +# class CreateEventView(APIView): +# permission_classes = [IsAuthenticated] + +# def post(self, request): + +# if request.user.role != "admin": +# return Response({"error": "Admins only"}, status=403) + +# try: +# society = Society.objects.get(admin=request.user) +# except Society.DoesNotExist: +# return Response({"error": "No society found"}, status=404) + +# data = request.data.copy() +# data["society"] = society.id +# data["created_by"] = request.user.id + +# serializer = EventSerializer(data=data) + +# if serializer.is_valid(): +# event = serializer.save() # capture the event + +# send_event_confirmation(request.user, event) + +# return Response(serializer.data, status=201) + +# return Response(serializer.errors, status=400) + + +# class CreateEventView(APIView): +# permission_classes = [IsAuthenticated] + +# def post(self, request): + +# if request.user.role != "admin": +# return Response({"error": "Admins only"}, status=403) + +# try: +# society = Society.objects.get(admin=request.user) +# except Society.DoesNotExist: +# return Response({"error": "No society found"}, status=404) + +# data = request.data.copy() + +# # 🔥 FIX capacity issue +# if data.get("capacity_limit") in [0, "0", ""]: +# data["capacity_limit"] = None + +# serializer = EventSerializer(data=data) + +# if serializer.is_valid(): +# event = serializer.save( +# society=society, # ✅ FIXES NULL ERROR +# created_by=request.user # ✅ GOOD PRACTICE +# ) + +# send_event_confirmation(request.user, event) + +# return Response(serializer.data, status=201) + +# print(serializer.errors) # DEBUG +# return Response(serializer.errors, status=400) + +class SocietyEventView(APIView): + """API view to retrieve or create events for a specific society. + + Requires authentication. + + - ``GET``: Returns all events belonging to the given society. + - ``POST``: Allows an admin of the society to create a new event. + """ + + permission_classes = [IsAuthenticated] + + def get(self, request, society_id): + """Return all events for the specified society. + + :param request: The HTTP request. + :type request: Request + :param society_id: The ID of the society to fetch events for. + :type society_id: int + :return: Serialized list of events, or 404 if society not found. + :rtype: Response + """ + try: + print(f"Fetching society with ID: {society_id}") + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + print(f"Society with ID {society_id} not found") + return Response({"error": "Society not found"}, status=404) + + print(f"Fetching events for society: {society.name}") + events = Event.objects.filter(society=society) + print(f"Events found: {events.count()}") + + serializer = EventSerializer(events, many=True) + print(f"Serialized events: {serializer.data}") + + return Response(serializer.data) + + def post(self, request, society_id): + """Create a new event for the specified society. + + Only the admin of the society can create events. + + :param request: The HTTP request containing event data. + :type request: Request + :param society_id: The ID of the society to add the event to. + :type society_id: int + :return: Serialized event data on success, or an error response. + :rtype: Response + """ + if request.user.role != "admin": + return Response({"error": "Admins only"}, status=403) + + try: + society = Society.objects.get(id=society_id, admin=request.user) + except Society.DoesNotExist: + return Response({"error": "Society not found or not admin"}, status=404) + + data = request.data.copy() + + if data.get("capacity_limit") in [0, "0", ""]: + data["capacity_limit"] = None + + serializer = EventSerializer(data=data) + + if serializer.is_valid(): + event = serializer.save( + society=society, + created_by=request.user + ) + + send_event_confirmation(request.user, event) + + return Response(serializer.data, status=201) + + return Response(serializer.errors, status=400) + + +class EventDetailView(generics.RetrieveAPIView): + """API view to retrieve details of a single event by ID. + + Requires authentication. Looks up the event using the ``id`` field. + """ + + permission_classes = [IsAuthenticated] + queryset = Event.objects.all() + serializer_class = EventSerializer + lookup_field = 'id' + + +class UpdateEventView(generics.UpdateAPIView): + """API view to update an event created by the authenticated user. + + Requires authentication. Users can only update events they created themselves. + Looks up the event using the ``id`` field. + """ + + permission_classes = [IsAuthenticated] + queryset = Event.objects.all() + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + """Return only events created by the authenticated user. + + :return: Queryset of Event objects created by the current user. + :rtype: QuerySet + """ + return Event.objects.filter(created_by=self.request.user) + + +class MyEventsView(APIView): + """API view to retrieve events relevant to the authenticated user. + + Requires authentication. + + - For **admins**: Returns all events belonging to their managed society. + - For **regular users**: Returns all events from societies they are members of. + """ + + permission_classes = [IsAuthenticated] + + def get(self, request): + """Return events relevant to the authenticated user. + + :param request: The HTTP request. + :type request: Request + :return: Serialized list of events. + :rtype: Response + """ + if request.user.role == "admin": + society = Society.objects.get(admin=request.user) + events = Event.objects.filter(society=society) + else: + events = Event.objects.filter( + society__membership__user=request.user + ).distinct() + + serializer = EventSerializer(events, many=True) + return Response(serializer.data) + + +class AllEventsView(APIView): + """API view to retrieve the 5 most recently added events. + + Requires authentication. Returns events ordered by descending ID. + """ + + permission_classes = [IsAuthenticated] + + def get(self, request): + """Return the 5 most recent events. + + :param request: The HTTP request. + :type request: Request + :return: Serialized list of up to 5 events. + :rtype: Response + """ + events = Event.objects.all().order_by('-id')[:5] + serializer = EventSerializer(events, many=True) + return Response(serializer.data) + + +class MyCreatedEventsView(APIView): + """API view to retrieve all events created by the authenticated user. + + Requires authentication. Results are ordered by most recently created first. + """ + + permission_classes = [IsAuthenticated] + + def get(self, request): + """Return all events created by the authenticated user. + + :param request: The HTTP request. + :type request: Request + :return: Serialized list of events created by the user. + :rtype: Response + """ + events = Event.objects.filter(created_by=request.user).order_by('-created_at') + serializer = EventSerializer(events, many=True) + return Response(serializer.data) + + +class ChangePasswordView(APIView): + """API view to allow an authenticated user to change their password. + + Requires authentication. The user must provide their current password + to verify their identity before setting a new one. + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + """Change the authenticated user's password. + + :param request: The HTTP request containing ``old_password`` and ``new_password``. + :type request: Request + :return: Success message, or 400 if the old password is incorrect. + :rtype: Response + """ + user = request.user + old_password = request.data.get("old_password") + new_password = request.data.get("new_password") + + if not user.check_password(old_password): + return Response({"error": "Old password is incorrect"}, status=400) + + user.set_password(new_password) + user.save() + return Response({"message": "Password changed successfully"}) + + +class ChangeEmailView(APIView): + """API view to allow an authenticated user to change their email address. + + Requires authentication. The new email must not already be in use by another account. + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + """Change the authenticated user's email address. + + :param request: The HTTP request containing ``new_email``. + :type request: Request + :return: Success message, or 400 if the email is missing or already in use. + :rtype: Response + """ + user = request.user + new_email = request.data.get("new_email") + + if not new_email: + return Response({"error": "New email is required"}, status=400) + + if User.objects.filter(email=new_email).exists(): + return Response({"error": "Email already in use"}, status=400) + + user.email = new_email + user.save() + return Response({"message": "Email changed successfully"}) + + +class User_ProfileView(APIView): + """API view to retrieve or update the authenticated user's profile. + + Requires authentication. + + - ``GET``: Returns the current user's profile data. + - ``POST``: Updates the current user's display name. + """ + + permission_classes = [IsAuthenticated] + + def get(self, request): + """Return the authenticated user's profile. + + :param request: The HTTP request. + :type request: Request + :return: Serialized user profile data. + :rtype: Response + """ + user = request.user + serializer = UserSerializer(user) + return Response(serializer.data) + + def post(self, request): + """Update the authenticated user's display name. + + :param request: The HTTP request containing ``name``. + :type request: Request + :return: Success message, or 400 if the name is missing. + :rtype: Response + """ + user = request.user + new_name = request.data.get("name") + + if not new_name: + return Response({"error": "New name is required"}, status=400) + + user.name = new_name + user.save() + return Response({"message": "Name changed successfully"}) + +# def send_event_confirmation(user, event): +# if not NotificationPreference.objects.filter( +# user=user, +# society=event.society, +# notify_new_events=True +# ).exists(): +# return + +# send_mail( +# subject="Event Created Successfully", +# message=f""" +# Your event "{event.title}" has been created successfully. + +# Date: {event.start_time} +# Location: {event.location} +# """, +# from_email=None, +# recipient_list=[user.email], +# fail_silently=False, +# ) + + +class NotificationView(APIView): + """API view to retrieve or update the authenticated user's notification preferences. + + Requires authentication. + + - ``GET``: Returns the user's notification preferences for each society they belong to. + - ``POST``: Updates the notification preference for a specific society. + """ + + permission_classes = [IsAuthenticated] + + def get(self, request): + """Return the authenticated user's notification preferences. + + :param request: The HTTP request. + :type request: Request + :return: List of societies and their notification settings for the user. + :rtype: Response + """ + user = request.user + preferences = NotificationPreference.objects.filter(user=user) + + data = [] + for pref in preferences: + data.append({ + "society": pref.society.name, + "notify_new_events": pref.notify_new_events, + }) + + return Response(data) + + def post(self, request): + """Update the authenticated user's notification preference for a society. + + :param request: The HTTP request containing ``society_id`` and ``event_notifications``. + :type request: Request + :return: Updated preference data, or an error if the society is not found or user is not a member. + :rtype: Response + """ + user = request.user + society_id = request.data.get("society_id") + + notify_new_events = str(request.data.get("event_notifications")).lower() == "true" + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response({"error": "Society not found"}, status=404) + + if not Membership.objects.filter(user=user, society=society).exists(): + return Response({"error": "Not a member of this society"}, status=403) + + pref, created = NotificationPreference.objects.update_or_create( + user=user, + society=society, + defaults={ + "notify_new_events": notify_new_events + } + ) + + return Response({ + "message": "Notification preferences updated", + "society": society.name, + "notify_new_events": pref.notify_new_events + }) + + +def send_event_confirmation(admin_user, event): + """Send a new event notification email to all opted-in society members. + + Finds all members of the event's society who have enabled new event + notifications and sends them an email with the event details. + + :param admin_user: The admin user who created the event. + :type admin_user: User + :param event: The newly created event to notify members about. + :type event: Event + """ + prefs = NotificationPreference.objects.filter( + society=event.society, + notify_new_events=True + ) + + recipient_emails = [pref.user.email for pref in prefs if pref.user.email] + + if not recipient_emails: + return + + subject = f"New Event: {event.title}" + message = f""" + Hello, + + A new event has been created in your society: {event.society.name} + + Title: {event.title} + Description: {event.description} + Start: {event.start_time} + End: {event.end_time} + + Please check the portal for more details. + """ + + send_mail( + subject=subject, + message=message, + from_email="no-reply@yoursite.com", + recipient_list=recipient_emails, + fail_silently=False, + ) + + +def send_event_reminders(): + """Send 24-hour reminder emails to admin members of upcoming events. + + Queries all events starting within the next 24 hours and sends reminder + emails to admin members of each event's society who have opted in to + 24-hour reminders via their notification preferences. + """ + now = timezone.now() + upcoming = now + timedelta(hours=24) + + events = Event.objects.filter(start_time__range=(now, upcoming)) + + for event in events: + admins = Membership.objects.filter( + society=event.society, + role="admin" + ) + + for member in admins: + user = member.user + + if not NotificationPreference.objects.filter( + user=user, + society=event.society, + notify_24hr_reminder=True + ).exists(): + continue + + send_mail( + subject="Reminder: Event in 24 Hours", + message=f""" +Reminder: "{event.title}" is in 24 hours. + +Date: {event.start_time} +Location: {event.location} +""", + from_email=None, + recipient_list=[user.email], + fail_silently=False, + ) + + From 51e71f9c59b99ee40419b399bbee6aa58d1d23bc Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 01:50:29 +0100 Subject: [PATCH 014/103] Refactor views documentation to include automodule directive with members, undoc-members, and show-inheritance options --- docs/source/generated/views.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/source/generated/views.rst b/docs/source/generated/views.rst index e84eed1fe..70bebb0c2 100644 --- a/docs/source/generated/views.rst +++ b/docs/source/generated/views.rst @@ -1,6 +1,7 @@ views ===== -.. currentmodule:: views - -.. autodata:: views \ No newline at end of file +.. automodule:: views + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file From 155016f467b947d3f2fbbb35107f46bb186b3ebc Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 01:51:50 +0100 Subject: [PATCH 015/103] Update serializer.rst to include automodule directives for models and serializer with members, undoc-members, and show-inheritance options --- docs/source/generated/serializer.rst | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/source/generated/serializer.rst b/docs/source/generated/serializer.rst index a70dfdc62..42c35f5a6 100644 --- a/docs/source/generated/serializer.rst +++ b/docs/source/generated/serializer.rst @@ -1,6 +1,15 @@ -serializer -========== +models +====== + +.. automodule:: models + :members: + :undoc-members: + :show-inheritance: -.. currentmodule:: serializer +serializer +========== -.. autodata:: serializer \ No newline at end of file +.. automodule:: serializer + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file From 05c0e8c255b9cc9ca355a080dd0398516537ca5d Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 01:59:33 +0100 Subject: [PATCH 016/103] Refactor views.py to replace imports with mocked dependencies for documentation purposes --- docs/source/views.py | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/docs/source/views.py b/docs/source/views.py index fdb1c41d5..e8ff69aba 100644 --- a/docs/source/views.py +++ b/docs/source/views.py @@ -461,31 +461,27 @@ # CODE DOCUMENTATION VIEWS BELOW ################################################################################### +from unittest.mock import MagicMock + +# Mocked dependencies for documentation purposes +request = MagicMock() +generics = MagicMock() +User = Event = Society = MagicMock() +UserSerializer = SocietySerializer = EventSerializer = MagicMock() +serializer = MagicMock() +APIView = MagicMock() +Response = MagicMock() +IsAuthenticated = MagicMock() +status = MagicMock() +now = MagicMock() +Count = Q = MagicMock() +send_mail = MagicMock() +timezone = MagicMock() +NotificationPreference = Membership = MagicMock() +PermissionDenied = MagicMock() -from flask import request -from rest_framework import generics -from .models import User, Event, Society -from .serializer import UserSerializer -from .serializer import SocietySerializer -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework import status -from .serializer import EventSerializer -from .import serializer -from django.utils.timezone import now -from django.db.models import Count, Q -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from django.core.mail import send_mail -from django.utils import timezone from datetime import timedelta -from .models import NotificationPreference, Society, Membership, Event - - class MySocietiesView(APIView): permission_classes = [IsAuthenticated] From 1afdbe3153ecde14003763fe7197e704151142d2 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 02:06:27 +0100 Subject: [PATCH 017/103] Add models and serializers for UNIsoc application --- docs/source/generated/views.rst | 12 +- docs/source/models.py | 296 ++++++++++++++++++++++++++++++++ docs/source/serializer.py | 57 ++++++ 3 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 docs/source/models.py create mode 100644 docs/source/serializer.py diff --git a/docs/source/generated/views.rst b/docs/source/generated/views.rst index 70bebb0c2..02562af78 100644 --- a/docs/source/generated/views.rst +++ b/docs/source/generated/views.rst @@ -2,6 +2,12 @@ ===== .. automodule:: views - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file + + + .. rubric:: Functions + + .. autosummary:: + + send_event_confirmation + send_event_reminders + \ No newline at end of file diff --git a/docs/source/models.py b/docs/source/models.py new file mode 100644 index 000000000..876ba406d --- /dev/null +++ b/docs/source/models.py @@ -0,0 +1,296 @@ +""" +Database models for the UNIsoc application. +""" + +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.core.validators import MinValueValidator +from django.contrib.auth.base_user import BaseUserManager +from django.conf import settings + + +class CustomUserManager(BaseUserManager): + def create_user(self, email, password=None, **extra_fields): + if not email: + raise ValueError("The Email field must be set") + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, password=None, **extra_fields): + extra_fields.setdefault('is_staff', True) + extra_fields.setdefault('is_superuser', True) + extra_fields.setdefault('is_active', True) + + if extra_fields.get('is_staff') is not True: + raise ValueError("Superuser must have is_staff=True.") + if extra_fields.get('is_superuser') is not True: + raise ValueError("Superuser must have is_superuser=True.") + + return self.create_user(email, password, **extra_fields) + +class User(AbstractUser): + username = None + + + email = models.EmailField(unique=True) + + up_number = models.CharField( + max_length=20, + unique=True, + null=True, + blank=True + ) + + role = models.CharField( + max_length=20, + default='user' + ) + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = [] + + objects = CustomUserManager() + + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.email + +class Society(models.Model): + name = models.CharField(max_length=100, unique=True) + category = models.CharField(max_length=50, blank=True) + description = models.TextField(blank=True) + + admin = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + limit_choices_to={'role': 'admin'}, + null=True, + blank=True + ) + + is_active = models.BooleanField(default=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + @property + def member_count(self): + return self.membership.filter(left_at__isnull=True).count() + + def __str__(self): + return self.name + +# class SocietyAdmin(models.Model): +# ROLE_CHOICES = [ +# ('president', 'President'), +# ('vice_president', 'Vice President'), +# ('treasurer', 'Treasurer'), +# ('moderator', 'Moderator'), +# ] + +# society = models.ForeignKey( +# Society, +# on_delete=models.CASCADE, +# related_name='admins' +# ) + +# user = models.ForeignKey( +# settings.AUTH_USER_MODEL, +# on_delete=models.CASCADE, +# related_name='admin_societies' +# ) + + # class Meta: + # unique_together = ('society', 'user') + + # def __str__(self): + # return f"{self.user.email} - {self.role}" + + +class Membership(models.Model): + ROLE_CHOICES = [ + ('member', 'Member'), + ('admin', 'Admin'), + ] + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE + ) + society = models.ForeignKey( + Society, + on_delete=models.CASCADE, + related_name="membership" + ) + + joined_at = models.DateTimeField(auto_now_add=True) + left_at = models.DateTimeField(null=True, blank=True) + + role = models.CharField(max_length=10, choices=ROLE_CHOICES, default='member') + + class Meta: + unique_together = ('user', 'society') + + def __str__(self): + return f"{self.user} -> {self.society}" + +class Event(models.Model): + STATUS_CHOICES = [ + ('upcoming', 'Upcoming'), + ('cancelled', 'Cancelled'), + ('completed', 'Completed'), + ] + + society = models.ForeignKey( + Society, + on_delete=models.CASCADE, + related_name='events' + ) + + title = models.CharField(max_length=100) + description = models.TextField(blank=True) + location = models.CharField(max_length=255, blank=True) + + start_time = models.DateTimeField() + end_time = models.DateTimeField() + + capacity_limit = models.IntegerField( + null=True, + blank=True, + validators=[MinValueValidator(1)] + ) + + created_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + related_name='created_events' + ) + + status = models.CharField( + max_length=20, + choices=STATUS_CHOICES, + default='upcoming' + ) + + created_at = models.DateTimeField(auto_now_add=True) + + def clean(self): + from django.core.exceptions import ValidationError + if self.end_time <= self.start_time: + raise ValidationError("End time must be after start time.") + + def __str__(self): + return self.title + + +class EventRSVP(models.Model): + RSVP_CHOICES = [ + ('attending', 'Attending'), + ('not_attending', 'Not Attending'), + ] + + event = models.ForeignKey( + Event, + on_delete=models.CASCADE, + related_name='rsvps' + ) + + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='event_rsvps' + ) + + rsvp_status = models.CharField( + max_length=20, + choices=RSVP_CHOICES, + default='attending' + ) + + rsvp_time = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('event', 'user') + + def __str__(self): + return f"{self.user} - {self.event}" + +class NotificationPreference(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='notification_preferences' + ) + + society = models.ForeignKey(Society, on_delete=models.CASCADE) + + # 👤 USER EMAIL SETTINGS + notify_new_events = models.BooleanField(default=True) + notify_cancellations = models.BooleanField(default=True) + + # 👑 ADMIN EMAIL SETTINGS + notify_event_created = models.BooleanField(default=True) + notify_24hr_reminder = models.BooleanField(default=True) + + class Meta: + unique_together = ('user', 'society') + + def __str__(self): + return f"{self.user} prefs for {self.society}" + +class Message(models.Model): + society = models.ForeignKey( + Society, + on_delete=models.CASCADE, + related_name='messages' + ) + + sender = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True + ) + + content = models.TextField() + + sent_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Message from {self.sender}" + +class AuditLog(models.Model): + user = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + blank=True + ) + + action = models.CharField(max_length=100) + description = models.TextField(blank=True) + + logged_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.action + + +class EventAttendance(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + event = models.ForeignKey(Event, on_delete=models.CASCADE) + joined_at = models.DateTimeField(auto_now_add=True) + left_at = models.DateTimeField(null=True, blank=True) + + class Meta: + unique_together = ("user", "event") + +#run in terminal +#python manage.py makemigrations +#python manage.py migrate diff --git a/docs/source/serializer.py b/docs/source/serializer.py new file mode 100644 index 000000000..3c4edc163 --- /dev/null +++ b/docs/source/serializer.py @@ -0,0 +1,57 @@ +""" +Serializers for converting UNIsoc models to JSON. +""" + +from rest_framework import serializers +from .models import NotificationPreference, Society, User +from .models import Event, NotificationPreference + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = Society + fields = '__all__' + +class SocietySerializer(serializers.ModelSerializer): + member_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Society + fields = '__all__' + + def get_member_count(self, obj): + return obj.membership.filter(left_at__isnull=True).count() + + + +from rest_framework import serializers +from .models import Event + +class EventSerializer(serializers.ModelSerializer): + attendee_count = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = Event + fields = [ + 'id', + 'title', + 'description', + 'location', + 'start_time', + 'end_time', + 'capacity_limit', + 'status', + 'attendee_count', + ] + read_only_fields = ['id', 'status', 'attendee_count'] + + def get_attendee_count(self, obj): + return obj.rsvps.count() + +class NotificationPreferenceSerializer(serializers.ModelSerializer): + class Meta: + model = NotificationPreference + fields = "__all__" + read_only_fields = ['user', 'id'] + + + From 08e1dbfe06b4255b2c685076d2684bfbcfa9aa4c Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 02:09:19 +0100 Subject: [PATCH 018/103] Refactor mock module list in conf.py to include missing Django auth modules --- docs/source/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index fb831daa2..6cb81a437 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,9 +13,10 @@ def __getattr__(cls, name): MOCK_MODULES = [ 'django', 'django.db', 'django.db.models', 'django.utils', 'django.utils.timezone', 'django.core', 'django.core.mail', + 'django.contrib', 'django.contrib.auth', 'django.contrib.auth.models', 'rest_framework', 'rest_framework.views', 'rest_framework.response', 'rest_framework.permissions', 'rest_framework.exceptions', - 'rest_framework.generics', 'rest_framework', 'flask', + 'rest_framework.generics', 'flask', 'authentication', 'authentication.models', ] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) From 8338b03ef40ecdff705985dcf28f48b54bc4e070 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 02:12:15 +0100 Subject: [PATCH 019/103] Refactor serializer.py to use MagicMock for models and serializers in tests --- docs/source/serializer.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/source/serializer.py b/docs/source/serializer.py index 3c4edc163..5b3d7fe45 100644 --- a/docs/source/serializer.py +++ b/docs/source/serializer.py @@ -2,9 +2,11 @@ Serializers for converting UNIsoc models to JSON. """ -from rest_framework import serializers -from .models import NotificationPreference, Society, User -from .models import Event, NotificationPreference +from unittest.mock import MagicMock + +NotificationPreference = Society = User = MagicMock() +Event = Membership = MagicMock() +serializers = MagicMock() class UserSerializer(serializers.ModelSerializer): class Meta: From be7b8e3c6805c7d838406c83d79a1e8e15336862 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 02:13:36 +0100 Subject: [PATCH 020/103] Update MOCK_MODULES in conf.py to include additional Django auth modules --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 6cb81a437..5f75eb9b7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,6 +14,7 @@ def __getattr__(cls, name): 'django', 'django.db', 'django.db.models', 'django.utils', 'django.utils.timezone', 'django.core', 'django.core.mail', 'django.contrib', 'django.contrib.auth', 'django.contrib.auth.models', + 'django.contrib.auth.base_user', 'django.core.validators', 'django.conf', 'rest_framework', 'rest_framework.views', 'rest_framework.response', 'rest_framework.permissions', 'rest_framework.exceptions', 'rest_framework.generics', 'flask', From 3dffe305186aca13a76acc5c7f5b90f34f18d4ec Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 02:18:42 +0100 Subject: [PATCH 021/103] Update models and serializers documentation with detailed descriptions and correct module references --- docs/source/generated/models.rst | 4 ++-- docs/source/serializer.py | 38 +++++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/docs/source/generated/models.rst b/docs/source/generated/models.rst index 23000cf83..1ab70bd36 100644 --- a/docs/source/generated/models.rst +++ b/docs/source/generated/models.rst @@ -1,6 +1,6 @@ models ====== -.. currentmodule:: models +.. automodule:: models -.. autodata:: models \ No newline at end of file + \ No newline at end of file diff --git a/docs/source/serializer.py b/docs/source/serializer.py index 5b3d7fe45..f826f8503 100644 --- a/docs/source/serializer.py +++ b/docs/source/serializer.py @@ -8,12 +8,22 @@ Event = Membership = MagicMock() serializers = MagicMock() + class UserSerializer(serializers.ModelSerializer): + """Serializer for the User model, returning all fields.""" + class Meta: model = Society fields = '__all__' + class SocietySerializer(serializers.ModelSerializer): + """Serializer for the Society model, including active member count. + + :param member_count: Read-only count of active members in the society. + :type member_count: int + """ + member_count = serializers.IntegerField(read_only=True) class Meta: @@ -21,14 +31,22 @@ class Meta: fields = '__all__' def get_member_count(self, obj): + """Return the number of active members in the society. + + :param obj: The society instance. + :return: Count of memberships where left_at is null. + :rtype: int + """ return obj.membership.filter(left_at__isnull=True).count() +class EventSerializer(serializers.ModelSerializer): + """Serializer for the Event model, including attendee count. -from rest_framework import serializers -from .models import Event + :param attendee_count: Read-only count of RSVPs for the event. + :type attendee_count: int + """ -class EventSerializer(serializers.ModelSerializer): attendee_count = serializers.SerializerMethodField(read_only=True) class Meta: @@ -47,13 +65,23 @@ class Meta: read_only_fields = ['id', 'status', 'attendee_count'] def get_attendee_count(self, obj): + """Return the number of RSVPs for this event. + + :param obj: The event instance. + :return: Count of RSVPs. + :rtype: int + """ return obj.rsvps.count() + class NotificationPreferenceSerializer(serializers.ModelSerializer): + """Serializer for the NotificationPreference model, returning all fields. + + The ``user`` and ``id`` fields are read-only. + """ + class Meta: model = NotificationPreference fields = "__all__" read_only_fields = ['user', 'id'] - - From d322cc8d44fe1455e3b759f452d5f30331b032bc Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 18:13:36 +0100 Subject: [PATCH 022/103] Remove outdated models section from serializer documentation --- docs/source/generated/serializer.rst | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/docs/source/generated/serializer.rst b/docs/source/generated/serializer.rst index 42c35f5a6..86e598841 100644 --- a/docs/source/generated/serializer.rst +++ b/docs/source/generated/serializer.rst @@ -1,15 +1,6 @@ -models -====== - -.. automodule:: models - :members: - :undoc-members: - :show-inheritance: - -serializer +serializer ========== .. automodule:: serializer - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file + + \ No newline at end of file From 25c0b7e2c0c45eb1840ef00d8c545a616a5f5d6d Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 22:56:10 +0100 Subject: [PATCH 023/103] Refactor conf.py and views.py to remove mock dependencies and implement actual Django models and serializers --- docs/source/conf.py | 47 ++- docs/source/views.py | 680 +++++++++++++++++++++++++++++++++---------- 2 files changed, 557 insertions(+), 170 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 5f75eb9b7..94df2d052 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,41 +1,40 @@ import os import sys -sys.path.insert(0, os.path.abspath('.')) - -# Mock all Django/DRF dependencies so Sphinx can import your code -from unittest.mock import MagicMock - -class Mock(MagicMock): - @classmethod - def __getattr__(cls, name): - return MagicMock() - -MOCK_MODULES = [ - 'django', 'django.db', 'django.db.models', 'django.utils', - 'django.utils.timezone', 'django.core', 'django.core.mail', - 'django.contrib', 'django.contrib.auth', 'django.contrib.auth.models', - 'django.contrib.auth.base_user', 'django.core.validators', 'django.conf', - 'rest_framework', 'rest_framework.views', 'rest_framework.response', - 'rest_framework.permissions', 'rest_framework.exceptions', - 'rest_framework.generics', 'flask', - 'authentication', 'authentication.models', -] -sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) -autosummary_generate = True +# Add project root (VERY IMPORTANT) +sys.path.insert(0, os.path.abspath('..')) -# -- Project information +# --- Django setup (preferred over mocking) --- +import django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project.settings') +django.setup() + +# -- Project information -- project = 'UNIsoc' -copyright = '2024' author = 'Your Team' release = '0.1' version = '0.1.0' +# -- General configuration -- extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', + 'sphinx.ext.napoleon', # 🔥 for your docstrings + 'sphinx.ext.viewcode', # 🔥 adds source code links 'sphinx.ext.duration', ] +autosummary_generate = True + +# Better autodoc output +autodoc_default_options = { + 'members': True, + 'undoc-members': True, + 'show-inheritance': True, +} + +# Templates templates_path = ['_templates'] + +# Theme html_theme = 'sphinx_rtd_theme' \ No newline at end of file diff --git a/docs/source/views.py b/docs/source/views.py index e8ff69aba..646e47253 100644 --- a/docs/source/views.py +++ b/docs/source/views.py @@ -461,61 +461,28 @@ # CODE DOCUMENTATION VIEWS BELOW ################################################################################### -from unittest.mock import MagicMock - -# Mocked dependencies for documentation purposes -request = MagicMock() -generics = MagicMock() -User = Event = Society = MagicMock() -UserSerializer = SocietySerializer = EventSerializer = MagicMock() -serializer = MagicMock() -APIView = MagicMock() -Response = MagicMock() -IsAuthenticated = MagicMock() -status = MagicMock() -now = MagicMock() -Count = Q = MagicMock() -send_mail = MagicMock() -timezone = MagicMock() -NotificationPreference = Membership = MagicMock() -PermissionDenied = MagicMock() +from rest_framework.authtoken.models import Token +from rest_framework import generics +from .models import EventAttendance, User, Event, Society +from .serializer import UserSerializer +from .serializer import SocietySerializer +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from rest_framework.exceptions import PermissionDenied +from .serializer import EventSerializer +from .import serializer +from django.utils.timezone import now +from django.db.models import Count, Q +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from django.core.mail import send_mail +from django.utils import timezone from datetime import timedelta +import re - -class MySocietiesView(APIView): - permission_classes = [IsAuthenticated] - - def get(self, request): - try: - # Debug: Log the user making the request - print(f"Fetching societies for user: {request.user}") - - # Fetch societies the user has joined using the Membership model - memberships = Membership.objects.filter(user=request.user, left_at__isnull=True) - societies = [membership.society for membership in memberships] - - # Debug: Log the societies fetched - print(f"Societies fetched: {societies}") - - data = [ - { - "id": s.id, - "name": s.name, - "description": s.description, - "member_count": s.member_count, - } - for s in societies - ] - - return Response(data) - - except Exception as e: - # Debug: Log the error - print(f"Error in MySocietiesView: {e}") - return Response({"error": str(e)}, status=500) - - +from .models import NotificationPreference, Society, Membership, Event class UserListView(generics.ListAPIView): @@ -622,7 +589,6 @@ def perform_create(self, serializer): created_by=self.request.user, society=society ) - serializer_class = SocietySerializer class DeleteEventView(generics.DestroyAPIView): @@ -644,71 +610,6 @@ def get_queryset(self): return Event.objects.filter(created_by=self.request.user) - serializer_class = SocietySerializer - -# class CreateEventView(APIView): -# permission_classes = [IsAuthenticated] - -# def post(self, request): - -# if request.user.role != "admin": -# return Response({"error": "Admins only"}, status=403) - -# try: -# society = Society.objects.get(admin=request.user) -# except Society.DoesNotExist: -# return Response({"error": "No society found"}, status=404) - -# data = request.data.copy() -# data["society"] = society.id -# data["created_by"] = request.user.id - -# serializer = EventSerializer(data=data) - -# if serializer.is_valid(): -# event = serializer.save() # capture the event - -# send_event_confirmation(request.user, event) - -# return Response(serializer.data, status=201) - -# return Response(serializer.errors, status=400) - - -# class CreateEventView(APIView): -# permission_classes = [IsAuthenticated] - -# def post(self, request): - -# if request.user.role != "admin": -# return Response({"error": "Admins only"}, status=403) - -# try: -# society = Society.objects.get(admin=request.user) -# except Society.DoesNotExist: -# return Response({"error": "No society found"}, status=404) - -# data = request.data.copy() - -# # 🔥 FIX capacity issue -# if data.get("capacity_limit") in [0, "0", ""]: -# data["capacity_limit"] = None - -# serializer = EventSerializer(data=data) - -# if serializer.is_valid(): -# event = serializer.save( -# society=society, # ✅ FIXES NULL ERROR -# created_by=request.user # ✅ GOOD PRACTICE -# ) - -# send_event_confirmation(request.user, event) - -# return Response(serializer.data, status=201) - -# print(serializer.errors) # DEBUG -# return Response(serializer.errors, status=400) - class SocietyEventView(APIView): """API view to retrieve or create events for a specific society. @@ -731,19 +632,12 @@ def get(self, request, society_id): :rtype: Response """ try: - print(f"Fetching society with ID: {society_id}") society = Society.objects.get(id=society_id) except Society.DoesNotExist: - print(f"Society with ID {society_id} not found") return Response({"error": "Society not found"}, status=404) - print(f"Fetching events for society: {society.name}") events = Event.objects.filter(society=society) - print(f"Events found: {events.count()}") - serializer = EventSerializer(events, many=True) - print(f"Serialized events: {serializer.data}") - return Response(serializer.data) def post(self, request, society_id): @@ -991,27 +885,6 @@ def post(self, request): user.name = new_name user.save() return Response({"message": "Name changed successfully"}) - -# def send_event_confirmation(user, event): -# if not NotificationPreference.objects.filter( -# user=user, -# society=event.society, -# notify_new_events=True -# ).exists(): -# return - -# send_mail( -# subject="Event Created Successfully", -# message=f""" -# Your event "{event.title}" has been created successfully. - -# Date: {event.start_time} -# Location: {event.location} -# """, -# from_email=None, -# recipient_list=[user.email], -# fail_silently=False, -# ) class NotificationView(APIView): @@ -1166,4 +1039,519 @@ def send_event_reminders(): fail_silently=False, ) +class SocietyAdminDetailView(APIView): + """ + API view to retrieve detailed information about a society, + including its events. + + Returns: + - Society details + - List of associated events + + Does not require admin privileges. + """ + permission_classes = [IsAuthenticated] + + # GET society details — used by both admin and user society page + def get(self, request, society_id): + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response({"error": "Society not found"}, status=404) + + return Response({ + "id": society.id, + "name": society.name, + "category": society.category, + "description": society.description, + }) + + # PATCH update society description — admin only + def patch(self, request, society_id): + if request.user.role != "admin": + return Response({"error": "Admin only"}, status=403) + + try: + society = Society.objects.get(id=society_id, admin=request.user) + except Society.DoesNotExist: + return Response({"error": "Society not found or not your society"}, status=404) + + description = request.data.get("description") + if description is not None: + society.description = description + society.save() + + return Response({ + "id": society.id, + "name": society.name, + "category": society.category, + "description": society.description, + "message": "Society updated successfully" + }) + + +class SocietyMembershipCheckView(APIView): + """ + Check if the authenticated user is an active member of a society. + """ + + permission_classes = [IsAuthenticated] + + def get(self, request, society_id): + # Check active membership (not left) + is_member = Membership.objects.filter( + user=request.user, + society_id=society_id, + left_at__isnull=True + ).exists() + + return Response({ + "society_id": society_id, + "is_member": is_member + }, status=status.HTTP_200_OK) + +class SocietyDetailView(APIView): + """ + Retrieve a society along with its events. + """ + def get(self, request, society_id): + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response({"error": "Society not found"}, status=404) + + events = Event.objects.filter(society=society) + + event_data = [] + for event in events: + event_data.append({ + "id": event.id, + "title": event.title, + "description": event.description, + "location": event.location, + "start_time": event.start_time, + }) + + return Response({ + "id": society.id, + "name": society.name, + "category": society.category, + "description": society.description, + "events": event_data + }) + +class RegisterView(APIView): + ''' + API view to handle user registration. + Accepts user details including first name, last name, email, + university number (UP number), and password. + + Validates: + - All required fields are provided + - Passwords match + - Password strength (length, uppercase, number, special character) + + Returns: + - 201 Created on success + - 400 Bad Request on validation failure + ''' + def post(self, request): + """ + Handle user registration. + + :param request: HTTP request containing user registration data + :type request: Request + :return: Success or error response + :rtype: Response + """ + first_name = request.data.get("first_name") + last_name = request.data.get("last_name") + email = request.data.get("email") + up_number = request.data.get("up_number") + password = request.data.get("password") + confirm_password = request.data.get("confirm_password") + + if not all([first_name, last_name, email, up_number, password, confirm_password]): + return Response( + {"error": "All fields are required"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if password != confirm_password: + return Response( + {"error": "Passwords do not match"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Password strength validation + if len(password) < 8: + return Response( + {"error": "Password must be at least 8 characters long"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not re.search(r"[A-Z]", password): + return Response( + {"error": "Password must contain at least one uppercase letter"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not re.search(r"[0-9]", password): + return Response( + {"error": "Password must contain at least one number"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password): + return Response( + {"error": "Password must contain at least one special character"}, + status=status.HTTP_400_BAD_REQUEST + ) + + + +class LoginView(APIView): + """ + API view to authenticate a user and return an auth token. + + Users can log in using either: + - Email + - University number (UP number) + + Returns: + - Auth token and user details on success + - 401 Unauthorized if credentials are invalid + """ + def post(self, request): + """ + Authenticate the user and generate a token. + + :param request: HTTP request containing login credentials + :type request: Request + :return: Authentication token and user info + :rtype: Response + """ + email = request.data.get("email") + up_number = request.data.get("up_number") + password = request.data.get("password") + + if not password: + return Response({"error": "Password required"}, status=400) + + try: + if email: + user = User.objects.get(email__iexact=email) + elif up_number: + up_number = up_number.lower() + if not up_number.startswith("up"): + up_number = f"up{up_number}" + user = User.objects.get(up_number__iexact=up_number) + else: + return Response({"error": "Email or UP number required"}, status=400) + + if user.check_password(password): + token, _ = Token.objects.get_or_create(user=user) + + society_id = None + society_name = None + + if user.role == "admin": + try: + society = Society.objects.get(admin=user) + society_id = society.id + society_name = society.name + except Society.DoesNotExist: + pass + + return Response({ + "token": token.key, + "role": user.role, + "email": user.email, + "up_number": user.up_number, + "society_id": society_id, + "society_name": society_name + }) + + except User.DoesNotExist: + pass + + return Response({"error": "Invalid credentials"}, status=401) + +class LeaveSocietyView(APIView): + """ + API view to allow a user to leave a society. + + Sets the `left_at` timestamp on the membership record + instead of deleting it. + + Requires authentication. + """ + permission_classes = [IsAuthenticated] + + def post(self, request, society_id): + user = request.user + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response( + {"error": "Society not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + try: + membership = Membership.objects.get( + user=user, + society=society, + left_at__isnull=True + + ) + except Membership.DoesNotExist: + return Response( + {"error": "You are not an active member"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + membership.left_at = timezone.now() + membership.save() + + return Response( + {"message": "Successfully left society"}, + status=status.HTTP_200_OK, + ) + +class LeaveEventView(APIView): + """ + API view to allow a user to leave an event. + + Marks attendance as inactive by setting `left_at`. + + Requires authentication. + """ + permission_classes = [IsAuthenticated] + + def post(self, request, event_id): + try: + attendance = EventAttendance.objects.get( + user=request.user, + event_id=event_id, + left_at__isnull=True + ) + except EventAttendance.DoesNotExist: + return Response({"error": "Not attending this event"}, status=400) + + attendance.left_at = timezone.now() + attendance.save() + + attendee_count = EventAttendance.objects.filter( + event_id=event_id, + left_at__isnull=True).count() + + return Response({"message": "Left event successfully"}) + +class JoinSocietyView(APIView): + """ + API view to allow a user to join a society. + + Behaviour: + - Creates a new membership if none exists + - Returns 'Already joined' if user is already active + - Re-activates membership if previously left + + Requires authentication. + """ + permission_classes = [IsAuthenticated] + + def post(self, request, society_id): + user = request.user + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response( + {"error": "Society not found"}, + status=status.HTTP_404_NOT_FOUND + ) + + membership, created = Membership.objects.get_or_create( + user=user, + society=society + ) + + if created: + return Response( + {"message": "Joined successfully"}, + status=status.HTTP_201_CREATED + ) + + if membership.left_at is None: + return Response({"message": "Already joined"}, status=200) + + # Rejoining + membership.left_at = None + membership.joined_at = timezone.now() + membership.save() + + return Response({"message": "Rejoined successfully"}, status=200) + +class JoinEventView(APIView): + """ + API view to allow a user to join an event. + + Behaviour: + - Prevents joining past events + - Creates attendance record if not existing + - Re-activates attendance if previously left + + Returns updated attendee count. + + Requires authentication. + """ + + permission_classes = [IsAuthenticated] + + def post(self, request, event_id): + + try: + event = Event.objects.get(id=event_id) + except Event.DoesNotExist: + return Response({"error": "Event not found"}, status=404) + + # prevent joining past events + if event.event_date < timezone.now(): + return Response( + {"error": "Event has already passed"}, + status=400 + ) + + attendance, created = EventAttendance.objects.get_or_create( + user=request.user, + event=event, + defaults={"left_at": None} + ) + + if not created: + if attendance.left_at is None: + return Response({"message": "Already attending"}, status=400) + else: + attendance.left_at = None + attendance.joined_at = timezone.now() + attendance.save() + + attendee_count = EventAttendance.objects.filter( + event=event, + left_at__isnull=True + ).count() + + return Response({ + "message": "Joined event", + "attendee_count": attendee_count + }) + +class AnalyticsView(APIView): + """ + API view to provide analytics for a society admin. + + Includes: + - Membership growth over time + - Total active members + - Total events + - Event attendance statistics + - Most popular event + + Query Parameters: + - period: 'week', 'month', '6months', 'year' + + Requires: + - Authenticated admin user + """ + permission_classes = [IsAuthenticated] + + def get(self, request): + + if request.user.role != "admin": + return Response({"error": "Admins only"}, status=403) + + period = request.query_params.get("period", "week") + + try: + society = Society.objects.get(admin=request.user) + except Society.DoesNotExist: + return Response({"error": "Society not found"}, status=404) + + now = timezone.now() + + # Decide grouping & range + if period == "week": + days_range = 7 + delta = timedelta(days=1) + label_format = "%a" # Mon Tue Wed + elif period == "month": + days_range = 30 + delta = timedelta(days=1) + label_format = "%d %b" + elif period == "6months": + days_range = 26 + delta = timedelta(weeks=1) + label_format = "Week %W" + elif period == "year": + days_range = 12 + delta = timedelta(days=30) + label_format = "%b" + else: + return Response({"error": "Invalid period"}, status=400) + + start_date = now - (delta * days_range) + + labels = [] + totals = [] + + current_date = start_date + + for _ in range(days_range): + + total = Membership.objects.filter( + society=society, + joined_at__lte=current_date + ).filter( + Q(left_at__isnull=True) | Q(left_at__gt=current_date) + ).count() + + labels.append(current_date.strftime(label_format)) + totals.append(total) + + current_date += delta + + society = Society.objects.get(admin=request.user) # gets admis society + total_events = society.events.count() # total events in that society + events_stats = society.events.annotate( + attendee_count = Count( + "eventattendance", + filter = Q(eventattendance__left_at__isnull=True) + ) + ).values("title", "attendee_count") + + #most popular event + most_popular = society.events.annotate( + attendee_count = Count( + "eventattendance", + filter = Q(eventattendance__left_at__isnull=True) + ) + ).order_by("-attendee_count").values("title", "attendee_count").first() + + live_count = Membership.objects.filter( + society=society, + left_at__isnull=True + ).count() + + return Response({ + "labels": labels, + "totals": totals, + "live_count": live_count, + "total_events": total_events, + "events_stats": list(events_stats), + "most_popular": most_popular, + "event_attendance": list(events_stats) + }) + \ No newline at end of file From 2f26811696aafbdd4d89b4e1809afc72f89d66c0 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 22:59:43 +0100 Subject: [PATCH 024/103] Update API reference documentation with correct module paths and formatting Co-authored-by: Copilot --- docs/source/api.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index e0a39da1e..b69d9b5aa 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,9 +1,10 @@ -API -=== +API Reference +============= .. autosummary:: - :toctree: generated + :toctree: generated/ + :recursive: - views - models - serializer \ No newline at end of file + authentication.views + authentication.models + authentication.serializer \ No newline at end of file From 34244d5bce8dc9fc29be516260650d1a263a388e Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 23:27:11 +0100 Subject: [PATCH 025/103] Update Django settings module path in conf.py for correct configuration --- docs/source/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 94df2d052..b703fbd5a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,7 +6,7 @@ # --- Django setup (preferred over mocking) --- import django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project.settings') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.config.settings') django.setup() # -- Project information -- @@ -19,8 +19,8 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', - 'sphinx.ext.napoleon', # 🔥 for your docstrings - 'sphinx.ext.viewcode', # 🔥 adds source code links + 'sphinx.ext.napoleon', + 'sphinx.ext.viewcode', 'sphinx.ext.duration', ] From 9338c80a3bfe64ff7559a5fd2139d0c97b2fafab Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 23:46:21 +0100 Subject: [PATCH 026/103] Fix project root path in conf.py for correct module resolution --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b703fbd5a..f19b6af09 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -2,7 +2,7 @@ import sys # Add project root (VERY IMPORTANT) -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath('../../..')) # --- Django setup (preferred over mocking) --- import django From 6df98876acde4b3714615041fc719e27917ae281 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 23:48:37 +0100 Subject: [PATCH 027/103] Add BASE_DIR to sys.path in conf.py for improved module resolution Co-authored-by: Copilot --- docs/source/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index f19b6af09..b8ead1178 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,8 +1,11 @@ import os import sys +from backend.config.settings import BASE_DIR + # Add project root (VERY IMPORTANT) sys.path.insert(0, os.path.abspath('../../..')) +sys.path.insert(0, BASE_DIR) # --- Django setup (preferred over mocking) --- import django From 692843e89611c5356b747f571c821f085694ece3 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 23:51:02 +0100 Subject: [PATCH 028/103] Refactor conf.py to define BASE_DIR directly for improved clarity and module resolution --- docs/source/conf.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b8ead1178..3476f616b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,13 +1,11 @@ import os import sys -from backend.config.settings import BASE_DIR +# Absolute path to your main project root (UNIsoc) +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../')) -# Add project root (VERY IMPORTANT) -sys.path.insert(0, os.path.abspath('../../..')) sys.path.insert(0, BASE_DIR) -# --- Django setup (preferred over mocking) --- import django os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.config.settings') django.setup() From 22daffac49b689b8254d1da28e220cd2020311fd Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 23:55:20 +0100 Subject: [PATCH 029/103] Update BASE_DIR in conf.py for correct project root path --- docs/source/conf.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 3476f616b..4c8e36d5f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,9 +1,7 @@ import os import sys -# Absolute path to your main project root (UNIsoc) -BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../')) - +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../')) sys.path.insert(0, BASE_DIR) import django From 9f6f921b16ef02652b49b8c7e814873fba068e93 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 24 Apr 2026 23:58:54 +0100 Subject: [PATCH 030/103] Fix BASE_DIR path in conf.py for correct project root resolution --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 4c8e36d5f..bbabb1033 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ import os import sys -BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../')) +BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../../')) sys.path.insert(0, BASE_DIR) import django From 30999c535e44bf16205c0c0015f218ed209c7252 Mon Sep 17 00:00:00 2001 From: stuti Date: Sat, 25 Apr 2026 00:10:11 +0100 Subject: [PATCH 031/103] Update BASE_DIR in conf.py to use absolute path for correct project root resolution --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index bbabb1033..4a6a3fc87 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ import os import sys -BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../../../')) +BASE_DIR = r"C:\Users\stuti\OneDrive\SETAP\SETAP CW\TERM 2 CW\UNIsoc" sys.path.insert(0, BASE_DIR) import django From f8c78078921710409311076e1166b2c4cef4085f Mon Sep 17 00:00:00 2001 From: stuti Date: Sat, 25 Apr 2026 00:11:07 +0100 Subject: [PATCH 032/103] Update API reference in api.rst for improved clarity and structure --- docs/source/api.rst | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index b69d9b5aa..6bc88aaf7 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,10 +1,26 @@ -API Reference -============= +API Documentation +================= -.. autosummary:: - :toctree: generated/ - :recursive: +Views +----- - authentication.views - authentication.models - authentication.serializer \ No newline at end of file +.. automodule:: backend.authentication.views + :members: + :undoc-members: + :show-inheritance: + +Models +------ + +.. automodule:: backend.authentication.models + :members: + :undoc-members: + :show-inheritance: + +Serializers +----------- + +.. automodule:: backend.authentication.serializer + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file From a57286680dd997affc48c2e2e4e4f266668e3e27 Mon Sep 17 00:00:00 2001 From: stuti Date: Sat, 25 Apr 2026 00:16:55 +0100 Subject: [PATCH 033/103] Add autosummary section to api.rst for improved module organization Co-authored-by: Copilot --- docs/source/api.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/source/api.rst b/docs/source/api.rst index 6bc88aaf7..3313d4fe2 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,6 +1,13 @@ API Documentation ================= +.. autosummary:: + :toctree: generated + + authentication.views + authentication.models + authentication.serializer + Views ----- From 15aa5df9b002940ca24467edccf352b78a8ee32a Mon Sep 17 00:00:00 2001 From: stuti Date: Sat, 25 Apr 2026 00:25:28 +0100 Subject: [PATCH 034/103] Refactor conf.py to remove Django setup and streamline mock module handling --- docs/source/conf.py | 47 +++++++++++++++------------------------------ 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 4a6a3fc87..4ca53c0d1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,39 +1,24 @@ import os import sys +from unittest.mock import MagicMock BASE_DIR = r"C:\Users\stuti\OneDrive\SETAP\SETAP CW\TERM 2 CW\UNIsoc" sys.path.insert(0, BASE_DIR) -import django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.config.settings') -django.setup() - -# -- Project information -- -project = 'UNIsoc' -author = 'Your Team' -release = '0.1' -version = '0.1.0' - -# -- General configuration -- -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', - 'sphinx.ext.duration', +class Mock(MagicMock): + @classmethod + def __getattr__(cls, name): + return MagicMock() + +MOCK_MODULES = [ + 'django', 'django.db', 'django.db.models', 'django.utils', + 'django.utils.timezone', 'django.core', 'django.core.mail', + 'django.contrib', 'django.contrib.auth', 'django.contrib.auth.models', + 'django.contrib.auth.base_user', 'django.core.validators', 'django.conf', + 'rest_framework', 'rest_framework.views', 'rest_framework.response', + 'rest_framework.permissions', 'rest_framework.exceptions', + 'rest_framework.generics', 'flask', + 'celery', 'config.celery' ] -autosummary_generate = True - -# Better autodoc output -autodoc_default_options = { - 'members': True, - 'undoc-members': True, - 'show-inheritance': True, -} - -# Templates -templates_path = ['_templates'] - -# Theme -html_theme = 'sphinx_rtd_theme' \ No newline at end of file +sys.modules.update((mod, Mock()) for mod in MOCK_MODULES) \ No newline at end of file From 6bc7e4745fc4958aa5ad0685b030679dbeb6d522 Mon Sep 17 00:00:00 2001 From: stuti Date: Sat, 25 Apr 2026 00:33:21 +0100 Subject: [PATCH 035/103] Refactor conf.py to streamline mock module handling and ensure correct backend path for Sphinx --- docs/source/conf.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 4ca53c0d1..c3cf87e78 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,24 +1,27 @@ import os import sys -from unittest.mock import MagicMock BASE_DIR = r"C:\Users\stuti\OneDrive\SETAP\SETAP CW\TERM 2 CW\UNIsoc" -sys.path.insert(0, BASE_DIR) -class Mock(MagicMock): - @classmethod - def __getattr__(cls, name): - return MagicMock() +# Point to backend so Sphinx can import your project +sys.path.insert(0, os.path.join(BASE_DIR, "backend")) -MOCK_MODULES = [ - 'django', 'django.db', 'django.db.models', 'django.utils', - 'django.utils.timezone', 'django.core', 'django.core.mail', - 'django.contrib', 'django.contrib.auth', 'django.contrib.auth.models', - 'django.contrib.auth.base_user', 'django.core.validators', 'django.conf', - 'rest_framework', 'rest_framework.views', 'rest_framework.response', - 'rest_framework.permissions', 'rest_framework.exceptions', - 'rest_framework.generics', 'flask', - 'celery', 'config.celery' +# ---- Sphinx extensions (IMPORTANT) ---- +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", ] -sys.modules.update((mod, Mock()) for mod in MOCK_MODULES) \ No newline at end of file +autosummary_generate = True + +autodoc_mock_imports = [ + "django", + "django.db", + "django.utils", + "django.core", + "django.contrib", + "rest_framework", + "flask", + "celery", + "config.celery", +] \ No newline at end of file From 1d124255f7494932b8864784a984e1e5f20bcc62 Mon Sep 17 00:00:00 2001 From: stuti Date: Sat, 25 Apr 2026 00:38:00 +0100 Subject: [PATCH 036/103] Add documentation for authentication models and serializers --- docs/source/conf.py | 3 ++- .../generated/authentication.models.rst | 21 +++++++++++++++++++ .../generated/authentication.serializer.rst | 15 +++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 docs/source/generated/authentication.models.rst create mode 100644 docs/source/generated/authentication.serializer.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index c3cf87e78..8e2d1e5e1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,7 +4,8 @@ BASE_DIR = r"C:\Users\stuti\OneDrive\SETAP\SETAP CW\TERM 2 CW\UNIsoc" # Point to backend so Sphinx can import your project -sys.path.insert(0, os.path.join(BASE_DIR, "backend")) +#sys.path.insert(0, os.path.join(BASE_DIR, "backend")) +sys.path.insert(0, BASE_DIR) # ---- Sphinx extensions (IMPORTANT) ---- extensions = [ diff --git a/docs/source/generated/authentication.models.rst b/docs/source/generated/authentication.models.rst new file mode 100644 index 000000000..ecb1029de --- /dev/null +++ b/docs/source/generated/authentication.models.rst @@ -0,0 +1,21 @@ +authentication.models +===================== + +.. automodule:: authentication.models + + + .. rubric:: Classes + + .. autosummary:: + + AuditLog + CustomUserManager + Event + EventAttendance + EventRSVP + Membership + Message + NotificationPreference + Society + User + \ No newline at end of file diff --git a/docs/source/generated/authentication.serializer.rst b/docs/source/generated/authentication.serializer.rst new file mode 100644 index 000000000..f6566d90d --- /dev/null +++ b/docs/source/generated/authentication.serializer.rst @@ -0,0 +1,15 @@ +authentication.serializer +========================= + +.. automodule:: authentication.serializer + + + .. rubric:: Classes + + .. autosummary:: + + EventSerializer + NotificationPreferenceSerializer + SocietySerializer + UserSerializer + \ No newline at end of file From 3c32ede849be540961c266b84bed0697abb8012d Mon Sep 17 00:00:00 2001 From: stuti Date: Sat, 25 Apr 2026 00:41:30 +0100 Subject: [PATCH 037/103] Fix autosummary paths in api.rst to include 'backend.' prefix for correct module referencing Co-authored-by: Copilot --- docs/source/api.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 3313d4fe2..62ab252a3 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -4,9 +4,9 @@ API Documentation .. autosummary:: :toctree: generated - authentication.views - authentication.models - authentication.serializer + backend.authentication.views + backend.authentication.models + backend.authentication.serializer Views ----- From 7f77b5ba0c46c57356e56b665a0c085b25df0cda Mon Sep 17 00:00:00 2001 From: stuti Date: Sat, 25 Apr 2026 00:47:47 +0100 Subject: [PATCH 038/103] Add generated documentation for authentication models and serializers --- .../backend.authentication.models.rst | 21 + .../backend.authentication.serializer.rst | 15 + docs/source/views.py | 463 ------------------ 3 files changed, 36 insertions(+), 463 deletions(-) create mode 100644 docs/source/generated/backend.authentication.models.rst create mode 100644 docs/source/generated/backend.authentication.serializer.rst diff --git a/docs/source/generated/backend.authentication.models.rst b/docs/source/generated/backend.authentication.models.rst new file mode 100644 index 000000000..082882eea --- /dev/null +++ b/docs/source/generated/backend.authentication.models.rst @@ -0,0 +1,21 @@ +backend.authentication.models +============================= + +.. automodule:: backend.authentication.models + + + .. rubric:: Classes + + .. autosummary:: + + AuditLog + CustomUserManager + Event + EventAttendance + EventRSVP + Membership + Message + NotificationPreference + Society + User + \ No newline at end of file diff --git a/docs/source/generated/backend.authentication.serializer.rst b/docs/source/generated/backend.authentication.serializer.rst new file mode 100644 index 000000000..7856f4f67 --- /dev/null +++ b/docs/source/generated/backend.authentication.serializer.rst @@ -0,0 +1,15 @@ +backend.authentication.serializer +================================= + +.. automodule:: backend.authentication.serializer + + + .. rubric:: Classes + + .. autosummary:: + + EventSerializer + NotificationPreferenceSerializer + SocietySerializer + UserSerializer + \ No newline at end of file diff --git a/docs/source/views.py b/docs/source/views.py index 646e47253..a3a1e8a19 100644 --- a/docs/source/views.py +++ b/docs/source/views.py @@ -1,466 +1,3 @@ -# from flask import request -# from rest_framework import generics -# from .models import User, Event, Society -# from .serializer import UserSerializer -# from .serializer import SocietySerializer -# from rest_framework.views import APIView -# from rest_framework.response import Response -# from rest_framework.permissions import IsAuthenticated -# from rest_framework import status -# from rest_framework.exceptions import PermissionDenied -# from .serializer import EventSerializer -# from .import serializer -# from django.utils.timezone import now -# from django.db.models import Count, Q -# from rest_framework.views import APIView -# from rest_framework.response import Response -# from rest_framework import status -# from rest_framework.permissions import IsAuthenticated -# from django.core.mail import send_mail -# from django.utils import timezone -# from datetime import timedelta - -# from .models import NotificationPreference, Society, Membership, Event - - -# class UserListView(generics.ListAPIView): -# serializer_class = UserSerializer - -# def get_queryset(self): -# queryset = User.objects.all().order_by('name') - -# search = self.request.query_params.get('search') -# letter = self.request.query_params.get('letter') - -# if search: -# queryset = queryset.filter(name__icontains=search) - -# if letter: -# queryset = queryset.filter(name__istartswith=letter) - -# return queryset - -# # class SocietyListView(generics.ListAPIView): -# # queryset = Society.objects.all().order_by('name') -# # serializer_class = SocietySerializer - -# class SocietyListSearchView(APIView): -# permission_classes = [IsAuthenticated] - -# def get(self, request): -# query = request.query_params.get("q", "").strip() - -# societies = Society.objects.filter(is_active=True) - -# if query: -# societies = societies.filter(name__icontains=query) - -# societies = societies.annotate( -# active_member_count=Count( -# 'membership', -# filter=Q(membership__left_at__isnull=True) -# ) -# ).order_by('name') - -# data = [{ -# "id": s.id, -# "name": s.name, -# "category": s.category, -# "description": s.description, -# "member_count": s.active_member_count, # ✅ fixed -# } for s in societies] - -# return Response(data) - -# class AddEventView(generics.CreateAPIView): -# serializer_class = EventSerializer -# permission_classes = [IsAuthenticated] - -# def perform_create(self, serializer): -# if self.request.user.role != "admin": -# raise PermissionDenied("Admins only") - -# society = Society.objects.get(admin=self.request.user) - -# serializer.save( -# created_by=self.request.user, -# society=society -# ) - -# class DeleteEventView(generics.DestroyAPIView): -# permission_classes = [IsAuthenticated] -# serializer_class = EventSerializer -# lookup_field = 'id' - -# def get_queryset(self): -# return Event.objects.filter(created_by=self.request.user) - - -# # class CreateEventView(APIView): -# # permission_classes = [IsAuthenticated] - -# # def post(self, request): - -# # if request.user.role != "admin": -# # return Response({"error": "Admins only"}, status=403) - -# # try: -# # society = Society.objects.get(admin=request.user) -# # except Society.DoesNotExist: -# # return Response({"error": "No society found"}, status=404) - -# # data = request.data.copy() -# # data["society"] = society.id -# # data["created_by"] = request.user.id - -# # serializer = EventSerializer(data=data) - -# # if serializer.is_valid(): -# # event = serializer.save() # capture the event - -# # send_event_confirmation(request.user, event) - -# # return Response(serializer.data, status=201) - -# # return Response(serializer.errors, status=400) - - -# # class CreateEventView(APIView): -# # permission_classes = [IsAuthenticated] - -# # def post(self, request): - -# # if request.user.role != "admin": -# # return Response({"error": "Admins only"}, status=403) - -# # try: -# # society = Society.objects.get(admin=request.user) -# # except Society.DoesNotExist: -# # return Response({"error": "No society found"}, status=404) - -# # data = request.data.copy() - -# # # 🔥 FIX capacity issue -# # if data.get("capacity_limit") in [0, "0", ""]: -# # data["capacity_limit"] = None - -# # serializer = EventSerializer(data=data) - -# # if serializer.is_valid(): -# # event = serializer.save( -# # society=society, # ✅ FIXES NULL ERROR -# # created_by=request.user # ✅ GOOD PRACTICE -# # ) - -# # send_event_confirmation(request.user, event) - -# # return Response(serializer.data, status=201) - -# # print(serializer.errors) # DEBUG -# # return Response(serializer.errors, status=400) - -# class SocietyEventView(APIView): -# permission_classes = [IsAuthenticated] - -# def get(self, request, society_id): - -# try: -# society = Society.objects.get(id=society_id) -# except Society.DoesNotExist: -# return Response({"error": "Society not found"}, status=404) - -# events = Event.objects.filter(society=society) -# serializer = EventSerializer(events, many=True) -# return Response(serializer.data) - -# def post(self, request, society_id): -# if request.user.role != "admin": -# return Response({"error": "Admins only"}, status=403) - -# try: -# society = Society.objects.get(id=society_id, admin=request.user) -# except Society.DoesNotExist: -# return Response({"error": "Society not found or not admin"}, status=404) - -# data = request.data.copy() - -# # ✅ Fix capacity issue -# if data.get("capacity_limit") in [0, "0", ""]: -# data["capacity_limit"] = None - -# serializer = EventSerializer(data=data) - -# if serializer.is_valid(): -# # 🔥 THIS IS THE FIX -# event = serializer.save( -# society=society, -# created_by=request.user -# ) - -# send_event_confirmation(request.user, event) - -# return Response(serializer.data, status=201) - -# print("❌ ERRORS:", serializer.errors) -# return Response(serializer.errors, status=400) - -# class EventDetailView(generics.RetrieveAPIView): -# permission_classes = [IsAuthenticated] -# queryset = Event.objects.all() -# serializer_class = EventSerializer -# lookup_field = 'id' - -# class UpdateEventView(generics.UpdateAPIView): -# permission_classes = [IsAuthenticated] -# queryset = Event.objects.all() -# serializer_class = EventSerializer -# lookup_field = 'id' - -# def get_queryset(self): -# return Event.objects.filter(created_by=self.request.user) - -# class MyEventsView(APIView): -# permission_classes = [IsAuthenticated] - -# def get(self, request): -# if request.user.role == "admin": -# society = Society.objects.get(admin=request.user) -# events = Event.objects.filter(society=society) -# else: -# events = Event.objects.filter( -# society__membership__user=request.user -# ).distinct() - -# serializer = EventSerializer(events, many=True) -# return Response(serializer.data) - -# class AllEventsView(APIView): -# permission_classes = [IsAuthenticated] - -# def get(self, request): -# events = Event.objects.all().order_by('-id')[:5] - -# serializer = EventSerializer(events, many=True) -# return Response(serializer.data) - -# # def get(self, request): -# # events = Event.objects.all().order_by('-created_at')[:5] -# # # events = Event.objects.filter( -# # # start_time__gte=now() # ✅ ONLY FUTURE EVENTS -# # # ).order_by('start_time')[:5] # ✅ SOONEST FIRST - -# # serializer = EventSerializer(events, many=True) -# # return Response(serializer.data) - -# class MyCreatedEventsView(APIView): -# permission_classes = [IsAuthenticated] - -# def get(self, request): -# events = Event.objects.filter(created_by=request.user).order_by('-created_at') -# serializer = EventSerializer(events, many=True) -# return Response(serializer.data) - -# class ChangePasswordView(APIView): -# permission_classes = [IsAuthenticated] - -# def post(self, request): -# user = request.user -# old_password = request.data.get("old_password") -# new_password = request.data.get("new_password") - -# if not user.check_password(old_password): -# return Response({"error": "Old password is incorrect"}, status=400) - -# user.set_password(new_password) -# user.save() -# return Response({"message": "Password changed successfully"}) - -# class ChangeEmailView(APIView): -# permission_classes = [IsAuthenticated] - -# def post(self, request): -# user = request.user -# new_email = request.data.get("new_email") - -# if not new_email: -# return Response({"error": "New email is required"}, status=400) - -# if User.objects.filter(email=new_email).exists(): -# return Response({"error": "Email already in use"}, status=400) - -# user.email = new_email -# user.save() -# return Response({"message": "Email changed successfully"}) - -# class User_ProfileView(APIView): -# permission_classes = [IsAuthenticated] - -# def get(self, request): -# user = request.user -# serializer = UserSerializer(user) - -# return Response(serializer.data) - -# def post(self, request): -# user = request.user -# new_name = request.data.get("name") - -# if not new_name: -# return Response({"error": "New name is required"}, status=400) - -# user.name = new_name -# user.save() -# return Response({"message": "Name changed successfully"}) - - -# class NotificationView(APIView): -# permission_classes = [IsAuthenticated] - -# # GET USER PREFERENCES -# def get(self, request): -# user = request.user -# preferences = NotificationPreference.objects.filter(user=user) - -# data = [] -# for pref in preferences: -# data.append({ -# "society": pref.society.name, -# "notify_new_events": pref.notify_new_events, # ✅ FIXED -# }) - -# return Response(data) - -# # UPDATE PREFERENCES -# def post(self, request): -# user = request.user -# society_id = request.data.get("society_id") - -# # safer boolean handling -# notify_new_events = str(request.data.get("event_notifications")).lower() == "true" - -# try: -# society = Society.objects.get(id=society_id) -# except Society.DoesNotExist: -# return Response({"error": "Society not found"}, status=404) - -# if not Membership.objects.filter(user=user, society=society).exists(): -# return Response({"error": "Not a member of this society"}, status=403) - -# pref, created = NotificationPreference.objects.update_or_create( -# user=user, -# society=society, -# defaults={ -# "notify_new_events": notify_new_events -# } -# ) - -# return Response({ -# "message": "Notification preferences updated", -# "society": society.name, -# "notify_new_events": pref.notify_new_events -# }) - - -# # def send_event_confirmation(user, event): -# # if not NotificationPreference.objects.filter( -# # user=user, -# # society=event.society, -# # notify_new_events=True -# # ).exists(): -# # return - -# # send_mail( -# # subject="Event Created Successfully", -# # message=f""" -# # Your event "{event.title}" has been created successfully. - -# # Date: {event.start_time} -# # Location: {event.location} -# # """, -# # from_email=None, -# # recipient_list=[user.email], -# # fail_silently=False, -# # ) - -# def send_event_confirmation(admin_user, event): -# """ -# Send emails to all users in the society who have opted in for new event notifications. -# """ -# # Get all NotificationPreferences for the society where users want new event emails -# prefs = NotificationPreference.objects.filter( -# society=event.society, -# notify_new_events=True -# ) - -# # Collect user emails -# recipient_emails = [pref.user.email for pref in prefs if pref.user.email] - -# if not recipient_emails: -# return # No one to notify - -# subject = f"New Event: {event.title}" -# message = f""" -# Hello, - -# A new event has been created in your society: {event.society.name} - -# Title: {event.title} -# Description: {event.description} -# Start: {event.start_time} -# End: {event.end_time} - -# Please check the portal for more details. -# """ - -# send_mail( -# subject=subject, -# message=message, -# from_email="no-reply@yoursite.com", # replace with your from email -# recipient_list=recipient_emails, -# fail_silently=False, -# ) - - -# def send_event_reminders(): -# now = timezone.now() -# upcoming = now + timedelta(hours=24) - -# events = Event.objects.filter(start_time__range=(now, upcoming)) - -# for event in events: -# admins = Membership.objects.filter( -# society=event.society, -# role="admin" -# ) - -# for member in admins: -# user = member.user - -# if not NotificationPreference.objects.filter( -# user=user, -# society=event.society, -# notify_24hr_reminder=True -# ).exists(): -# continue - -# send_mail( -# subject="Reminder: Event in 24 Hours", -# message=f""" -# Reminder: "{event.title}" is in 24 hours. - -# Date: {event.start_time} -# Location: {event.location} -# """, -# from_email=None, -# recipient_list=[user.email], -# fail_silently=False, -# ) - - -################################################################################### -# CODE DOCUMENTATION VIEWS BELOW -################################################################################### - from rest_framework.authtoken.models import Token from rest_framework import generics From ccfcb44613db35376c6e2bc3850caf8599e4bc84 Mon Sep 17 00:00:00 2001 From: stuti Date: Sat, 25 Apr 2026 00:58:25 +0100 Subject: [PATCH 039/103] Fix event date check in JoinEventView to use start_time for validation --- docs/source/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/views.py b/docs/source/views.py index a3a1e8a19..9232f5175 100644 --- a/docs/source/views.py +++ b/docs/source/views.py @@ -955,7 +955,7 @@ def post(self, request, event_id): return Response({"error": "Event not found"}, status=404) # prevent joining past events - if event.event_date < timezone.now(): + if event.start_time < timezone.now(): return Response( {"error": "Event has already passed"}, status=400 From 75358b7ec8c2508562bdf396dc9c0c21c02e7f9d Mon Sep 17 00:00:00 2001 From: stuti Date: Sat, 25 Apr 2026 01:16:41 +0100 Subject: [PATCH 040/103] Fix Sphinx import path in conf.py to correctly point to the backend directory --- docs/source/conf.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 8e2d1e5e1..ceb6f8b12 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -3,9 +3,7 @@ BASE_DIR = r"C:\Users\stuti\OneDrive\SETAP\SETAP CW\TERM 2 CW\UNIsoc" -# Point to backend so Sphinx can import your project -#sys.path.insert(0, os.path.join(BASE_DIR, "backend")) -sys.path.insert(0, BASE_DIR) +sys.path.insert(0, os.path.join(BASE_DIR, "backend")) # ✅ IMPORTANT # ---- Sphinx extensions (IMPORTANT) ---- extensions = [ From 3866e649891ce867d6aa51caad4e9cf94e8004b5 Mon Sep 17 00:00:00 2001 From: stuti Date: Sat, 25 Apr 2026 01:19:39 +0100 Subject: [PATCH 041/103] Refactor import statements in views.py for clarity and consistency --- docs/source/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/views.py b/docs/source/views.py index 9232f5175..c5f402ab6 100644 --- a/docs/source/views.py +++ b/docs/source/views.py @@ -1,4 +1,5 @@ + from rest_framework.authtoken.models import Token from rest_framework import generics from .models import EventAttendance, User, Event, Society @@ -8,8 +9,7 @@ from rest_framework.response import Response from rest_framework import status from rest_framework.exceptions import PermissionDenied -from .serializer import EventSerializer -from .import serializer +from .serializer import UserSerializer, SocietySerializer, EventSerializer from django.utils.timezone import now from django.db.models import Count, Q from rest_framework import status From de68dd4b4ab80dc88f80ac9225f268647303ee0a Mon Sep 17 00:00:00 2001 From: stuti Date: Sat, 25 Apr 2026 01:20:54 +0100 Subject: [PATCH 042/103] Set Django settings module and initialize Django in conf.py --- docs/source/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index ceb6f8b12..de4d0bd42 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -5,6 +5,10 @@ sys.path.insert(0, os.path.join(BASE_DIR, "backend")) # ✅ IMPORTANT +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") + +import django +django.setup() # ---- Sphinx extensions (IMPORTANT) ---- extensions = [ "sphinx.ext.autodoc", From 81048bb3b00501f491a5990b793f24299eb52eb0 Mon Sep 17 00:00:00 2001 From: Stuti Patel Date: Wed, 29 Apr 2026 19:42:45 +0100 Subject: [PATCH 043/103] Update table of contents in index.rst --- docs/source/index.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 03d09a55d..ab9ce8d41 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -17,6 +17,12 @@ Contents -------- .. toctree:: + :maxdepth: 2 + :caption: Contents: - usage + scope + requirements + implementation + setup + components api From 68399050632822debcf9494d325761437552487e Mon Sep 17 00:00:00 2001 From: Stuti Patel Date: Wed, 29 Apr 2026 19:48:56 +0100 Subject: [PATCH 044/103] Add project scope and objectives documentation Document the project scope and objectives for the system. --- docs/source/scope.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/source/scope.rst diff --git a/docs/source/scope.rst b/docs/source/scope.rst new file mode 100644 index 000000000..df8c63ac7 --- /dev/null +++ b/docs/source/scope.rst @@ -0,0 +1,20 @@ +Project Scope +============= + +This project aims to develop a system for University Students of Portsmouth University so they can access all society information. + +The system includes: +- User side +- Admin side +- Event information +- Live Analytics for admin society managment +- Featured and Top Societies +- Event Creation and Deletion +- Event and Society Browsing + + +Objectives +---------- +- Improve Society engagement +- Track society engagement for admins +- Make Society events more accessible From 653549ee08e729512988a21b52937e3d0a32839b Mon Sep 17 00:00:00 2001 From: Stuti Patel Date: Wed, 29 Apr 2026 20:27:50 +0100 Subject: [PATCH 045/103] Create requirements document for users and admins Added user, admin, and system requirements for the application. --- docs/source/requirements.rst | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/source/requirements.rst diff --git a/docs/source/requirements.rst b/docs/source/requirements.rst new file mode 100644 index 000000000..0e0c0aa00 --- /dev/null +++ b/docs/source/requirements.rst @@ -0,0 +1,57 @@ +User Requirements +================= + +Functional Requirements +---------------------- +- Users must be able to create an account using a secure registration process including email and password +- Users should be able to join/leave societies +- Users should be able to filter societies and view events clearly on a calendar interface for easy navigation. +- Users should be able to receive notifications for events and activities from societies they have joined +- Users can opt in or out of receiving notifications for events and activities from the societies they have joined. +- If a user leaves a society, they should no longer receive notifications. +- Users should be able to sign in if they have forgotten their password. +- Users should be able to see events for the society/societies they have joined +- Users should be able to see a description and location of an event when they click on it + + +Non-Functional Requirements +-------------------------- +- The users' passwords must be securely hashed and never stored in plain text +- Users should be able to communicate through a chat box with society's admins. +- All user actions related to joining/leaving societies and changing notification preferences should be logged for audit +purposes + +Admin Requirements +================== + +Functional Requirements +---------------------- +- Admins should be able to place events/remove onto the main calendar page +- Admins should be able to track attendance +- Admins should be able to create events +- Admins should be able to remove events +- Admins should be able to update their profile data, including password changes and notification preferences. +- Admins should be able to edit/update event details +- Admins should be able to update their profile data, including password changes and notification preferences. +- Admins should be able to manage society informatio +- Admins should be able to set capacity limits +- Admins should be able to generate or export attendance reports + +System Requirements +=================== + +Functional Requirements +---------------------- +- The system will allow users to to create an account +- The system will allow users and admins to log into the system +- The system will not allow users to join a society they have already joined twice +- The system will allow the user to unjoin a society if they wish to. +- The system should provide a search and filter functionality to find societies by name, type, or category. +- The system should be able to track membership status and allow users to view the societies they have joined. +- The system must send notifications to users about events and activities for societies they have joined. +- The system must display the availability of spaces for society events +- The system shall provide an account settings interface that allows users to update their password and notification +preferences. +- The system shall provide an account management interface that allows administrators to update their credentials and +notification preferences. + From e4c5a7706b5942f1a062caffe5c7414b2eaaeb79 Mon Sep 17 00:00:00 2001 From: Stuti Patel Date: Wed, 29 Apr 2026 20:29:47 +0100 Subject: [PATCH 046/103] Add implementation documentation Document implementation details including technologies used and example code. --- docs/source/implementation.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/source/implementation.rst diff --git a/docs/source/implementation.rst b/docs/source/implementation.rst new file mode 100644 index 000000000..e1f8e8c1e --- /dev/null +++ b/docs/source/implementation.rst @@ -0,0 +1,17 @@ +Implementation +============== + +Technologies Used +---------------- +- Python (backend) +- SQL Database +- Dart/Flutter (frontend) +- GitHub for version control + +Example Code +------------ + +.. code-block:: python + + def add_service(service): + return database.insert(service) From e679813457b989c2dbad0c2c3fdc770f6e2b3453 Mon Sep 17 00:00:00 2001 From: Stuti Patel Date: Wed, 29 Apr 2026 20:32:07 +0100 Subject: [PATCH 047/103] Implement User_ProfileView for user profile updates Added User_ProfileView class with GET and POST methods for user profile management. --- docs/source/implementation.rst | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/source/implementation.rst b/docs/source/implementation.rst index e1f8e8c1e..e3763bbb1 100644 --- a/docs/source/implementation.rst +++ b/docs/source/implementation.rst @@ -13,5 +13,26 @@ Example Code .. code-block:: python - def add_service(service): - return database.insert(service) + class User_ProfileView(APIView) + + permission_classes = [IsAuthenticated] + + def get(self, request): + + user = request.user + serializer = UserSerializer(user) + return Response(serializer.data) + + def post(self, request): + """Update the authenticated user's display name. + + user = request.user + new_name = request.data.get("name") + + if not new_name: + return Response({"error": "New name is required"}, status=400) + + user.name = new_name + user.save() + return Response({"message": "Name changed successfully"}) + From bcb3135cf8f5de5a616c7a317860f3ff7b50ff98 Mon Sep 17 00:00:00 2001 From: Stuti Patel Date: Wed, 29 Apr 2026 20:32:48 +0100 Subject: [PATCH 048/103] Remove comment in post method of implementation.rst Remove outdated comment about updating user's display name. --- docs/source/implementation.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/implementation.rst b/docs/source/implementation.rst index e3763bbb1..f8c319d6d 100644 --- a/docs/source/implementation.rst +++ b/docs/source/implementation.rst @@ -24,7 +24,6 @@ Example Code return Response(serializer.data) def post(self, request): - """Update the authenticated user's display name. user = request.user new_name = request.data.get("name") From d503f76d8b2bf144d20bcb868dc2b4dcb17b274a Mon Sep 17 00:00:00 2001 From: Stuti Patel Date: Wed, 29 Apr 2026 20:35:35 +0100 Subject: [PATCH 049/103] Add setup instructions to documentation Added setup instructions including requirements and installation steps. --- docs/source/setup.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/source/setup.rst diff --git a/docs/source/setup.rst b/docs/source/setup.rst new file mode 100644 index 000000000..64129c71b --- /dev/null +++ b/docs/source/setup.rst @@ -0,0 +1,24 @@ +Setup Instructions +================== + +Requirements +------------ +- Python 3.14 +- Git +- + +Installation +------------ + +.. code-block:: bash + + git clone https://github.com/your-repo + cd project + pip install -r requirements.txt + +Run the Project +--------------- + +.. code-block:: bash + + python main.py From 6251b43f0137109bff979e93b9f3fe59aaac1cd7 Mon Sep 17 00:00:00 2001 From: Stuti Patel Date: Wed, 29 Apr 2026 21:12:05 +0100 Subject: [PATCH 050/103] Add documentation for project components and services --- docs/source/components.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/source/components.rst diff --git a/docs/source/components.rst b/docs/source/components.rst new file mode 100644 index 000000000..0619dfa8e --- /dev/null +++ b/docs/source/components.rst @@ -0,0 +1,23 @@ +Project Components +================== + +Main Modules +------------ + +1. Identity Management Service + Handles Account creation, Login & Autheticaation, Password hashing & Verification + +2. Society Mangement Service + Manages creating, updating, searching and deleting societies + +3. Membership Service + Handles Joining and Leaving societies + +4. Event Service + Manages joining and leaving events and display of events + +5. Attendence Service + Manages attendence count of number of people in a society and joining an event. + +7. Notification Service + Manages RSVP perferences for upcoming events and information for joined society. From 692513f8445ea0c4905d2c151008def01c5324e7 Mon Sep 17 00:00:00 2001 From: Stuti Patel Date: Wed, 29 Apr 2026 21:26:48 +0100 Subject: [PATCH 051/103] Fix formatting of bash command in setup documentation --- docs/source/setup.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/setup.rst b/docs/source/setup.rst index 64129c71b..426590998 100644 --- a/docs/source/setup.rst +++ b/docs/source/setup.rst @@ -21,4 +21,4 @@ Run the Project .. code-block:: bash - python main.py + python main.py From c102e046337ed3524eaae6d6bf3ec07eee748e20 Mon Sep 17 00:00:00 2001 From: stuti Date: Wed, 29 Apr 2026 22:00:08 +0100 Subject: [PATCH 052/103] made documentation for setup.rst --- .DS_Store | Bin 0 -> 6148 bytes docs/source/conf.py | 30 - docs/source/models.py | 296 ---------- docs/source/serializer.py | 87 --- docs/source/setup.rst | 123 ++++- docs/source/views.py | 1094 ------------------------------------- 6 files changed, 117 insertions(+), 1513 deletions(-) create mode 100644 .DS_Store delete mode 100644 docs/source/conf.py delete mode 100644 docs/source/models.py delete mode 100644 docs/source/serializer.py delete mode 100644 docs/source/views.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..4c3711ce19ce8a34cd7a1a64c3bd9bbb2548caed GIT binary patch literal 6148 zcmeHKu};G<5WOQ2ip0{9(Gwd;9oa$^J^;!Gpf*ye)GC#LJtJSh#D6d|F*EQT`~vTM z*ASP?2*I6XKl|RDFL_ezn230CUyq4KL{y*&vJ3`H+=JAf1q;ct#%eL1&YSgSV3FVI zl4sB9gch`Q}(`p_4c~M*ByWSa{7F+!*6bS+GJ6poN8)l zPPa*2I{JupjqBHi|KW1GZpVMs9paO^91!B>3b+EUfGhCN6~LJ-RvajL?+UmAu0W@N z><`MqFvwD#|M2kLf`F5r~3#=L-CQ0-q3bLKXl3 literal 0 HcmV?d00001 diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index de4d0bd42..000000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -import sys - -BASE_DIR = r"C:\Users\stuti\OneDrive\SETAP\SETAP CW\TERM 2 CW\UNIsoc" - -sys.path.insert(0, os.path.join(BASE_DIR, "backend")) # ✅ IMPORTANT - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") - -import django -django.setup() -# ---- Sphinx extensions (IMPORTANT) ---- -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", -] - -autosummary_generate = True - -autodoc_mock_imports = [ - "django", - "django.db", - "django.utils", - "django.core", - "django.contrib", - "rest_framework", - "flask", - "celery", - "config.celery", -] \ No newline at end of file diff --git a/docs/source/models.py b/docs/source/models.py deleted file mode 100644 index 876ba406d..000000000 --- a/docs/source/models.py +++ /dev/null @@ -1,296 +0,0 @@ -""" -Database models for the UNIsoc application. -""" - -from django.db import models -from django.contrib.auth.models import AbstractUser -from django.core.validators import MinValueValidator -from django.contrib.auth.base_user import BaseUserManager -from django.conf import settings - - -class CustomUserManager(BaseUserManager): - def create_user(self, email, password=None, **extra_fields): - if not email: - raise ValueError("The Email field must be set") - email = self.normalize_email(email) - user = self.model(email=email, **extra_fields) - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, email, password=None, **extra_fields): - extra_fields.setdefault('is_staff', True) - extra_fields.setdefault('is_superuser', True) - extra_fields.setdefault('is_active', True) - - if extra_fields.get('is_staff') is not True: - raise ValueError("Superuser must have is_staff=True.") - if extra_fields.get('is_superuser') is not True: - raise ValueError("Superuser must have is_superuser=True.") - - return self.create_user(email, password, **extra_fields) - -class User(AbstractUser): - username = None - - - email = models.EmailField(unique=True) - - up_number = models.CharField( - max_length=20, - unique=True, - null=True, - blank=True - ) - - role = models.CharField( - max_length=20, - default='user' - ) - - USERNAME_FIELD = 'email' - REQUIRED_FIELDS = [] - - objects = CustomUserManager() - - is_active = models.BooleanField(default=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return self.email - -class Society(models.Model): - name = models.CharField(max_length=100, unique=True) - category = models.CharField(max_length=50, blank=True) - description = models.TextField(blank=True) - - admin = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - limit_choices_to={'role': 'admin'}, - null=True, - blank=True - ) - - is_active = models.BooleanField(default=True) - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - @property - def member_count(self): - return self.membership.filter(left_at__isnull=True).count() - - def __str__(self): - return self.name - -# class SocietyAdmin(models.Model): -# ROLE_CHOICES = [ -# ('president', 'President'), -# ('vice_president', 'Vice President'), -# ('treasurer', 'Treasurer'), -# ('moderator', 'Moderator'), -# ] - -# society = models.ForeignKey( -# Society, -# on_delete=models.CASCADE, -# related_name='admins' -# ) - -# user = models.ForeignKey( -# settings.AUTH_USER_MODEL, -# on_delete=models.CASCADE, -# related_name='admin_societies' -# ) - - # class Meta: - # unique_together = ('society', 'user') - - # def __str__(self): - # return f"{self.user.email} - {self.role}" - - -class Membership(models.Model): - ROLE_CHOICES = [ - ('member', 'Member'), - ('admin', 'Admin'), - ] - - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE - ) - society = models.ForeignKey( - Society, - on_delete=models.CASCADE, - related_name="membership" - ) - - joined_at = models.DateTimeField(auto_now_add=True) - left_at = models.DateTimeField(null=True, blank=True) - - role = models.CharField(max_length=10, choices=ROLE_CHOICES, default='member') - - class Meta: - unique_together = ('user', 'society') - - def __str__(self): - return f"{self.user} -> {self.society}" - -class Event(models.Model): - STATUS_CHOICES = [ - ('upcoming', 'Upcoming'), - ('cancelled', 'Cancelled'), - ('completed', 'Completed'), - ] - - society = models.ForeignKey( - Society, - on_delete=models.CASCADE, - related_name='events' - ) - - title = models.CharField(max_length=100) - description = models.TextField(blank=True) - location = models.CharField(max_length=255, blank=True) - - start_time = models.DateTimeField() - end_time = models.DateTimeField() - - capacity_limit = models.IntegerField( - null=True, - blank=True, - validators=[MinValueValidator(1)] - ) - - created_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - related_name='created_events' - ) - - status = models.CharField( - max_length=20, - choices=STATUS_CHOICES, - default='upcoming' - ) - - created_at = models.DateTimeField(auto_now_add=True) - - def clean(self): - from django.core.exceptions import ValidationError - if self.end_time <= self.start_time: - raise ValidationError("End time must be after start time.") - - def __str__(self): - return self.title - - -class EventRSVP(models.Model): - RSVP_CHOICES = [ - ('attending', 'Attending'), - ('not_attending', 'Not Attending'), - ] - - event = models.ForeignKey( - Event, - on_delete=models.CASCADE, - related_name='rsvps' - ) - - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='event_rsvps' - ) - - rsvp_status = models.CharField( - max_length=20, - choices=RSVP_CHOICES, - default='attending' - ) - - rsvp_time = models.DateTimeField(auto_now_add=True) - - class Meta: - unique_together = ('event', 'user') - - def __str__(self): - return f"{self.user} - {self.event}" - -class NotificationPreference(models.Model): - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name='notification_preferences' - ) - - society = models.ForeignKey(Society, on_delete=models.CASCADE) - - # 👤 USER EMAIL SETTINGS - notify_new_events = models.BooleanField(default=True) - notify_cancellations = models.BooleanField(default=True) - - # 👑 ADMIN EMAIL SETTINGS - notify_event_created = models.BooleanField(default=True) - notify_24hr_reminder = models.BooleanField(default=True) - - class Meta: - unique_together = ('user', 'society') - - def __str__(self): - return f"{self.user} prefs for {self.society}" - -class Message(models.Model): - society = models.ForeignKey( - Society, - on_delete=models.CASCADE, - related_name='messages' - ) - - sender = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True - ) - - content = models.TextField() - - sent_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return f"Message from {self.sender}" - -class AuditLog(models.Model): - user = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - blank=True - ) - - action = models.CharField(max_length=100) - description = models.TextField(blank=True) - - logged_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return self.action - - -class EventAttendance(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - event = models.ForeignKey(Event, on_delete=models.CASCADE) - joined_at = models.DateTimeField(auto_now_add=True) - left_at = models.DateTimeField(null=True, blank=True) - - class Meta: - unique_together = ("user", "event") - -#run in terminal -#python manage.py makemigrations -#python manage.py migrate diff --git a/docs/source/serializer.py b/docs/source/serializer.py deleted file mode 100644 index f826f8503..000000000 --- a/docs/source/serializer.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Serializers for converting UNIsoc models to JSON. -""" - -from unittest.mock import MagicMock - -NotificationPreference = Society = User = MagicMock() -Event = Membership = MagicMock() -serializers = MagicMock() - - -class UserSerializer(serializers.ModelSerializer): - """Serializer for the User model, returning all fields.""" - - class Meta: - model = Society - fields = '__all__' - - -class SocietySerializer(serializers.ModelSerializer): - """Serializer for the Society model, including active member count. - - :param member_count: Read-only count of active members in the society. - :type member_count: int - """ - - member_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Society - fields = '__all__' - - def get_member_count(self, obj): - """Return the number of active members in the society. - - :param obj: The society instance. - :return: Count of memberships where left_at is null. - :rtype: int - """ - return obj.membership.filter(left_at__isnull=True).count() - - -class EventSerializer(serializers.ModelSerializer): - """Serializer for the Event model, including attendee count. - - :param attendee_count: Read-only count of RSVPs for the event. - :type attendee_count: int - """ - - attendee_count = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = Event - fields = [ - 'id', - 'title', - 'description', - 'location', - 'start_time', - 'end_time', - 'capacity_limit', - 'status', - 'attendee_count', - ] - read_only_fields = ['id', 'status', 'attendee_count'] - - def get_attendee_count(self, obj): - """Return the number of RSVPs for this event. - - :param obj: The event instance. - :return: Count of RSVPs. - :rtype: int - """ - return obj.rsvps.count() - - -class NotificationPreferenceSerializer(serializers.ModelSerializer): - """Serializer for the NotificationPreference model, returning all fields. - - The ``user`` and ``id`` fields are read-only. - """ - - class Meta: - model = NotificationPreference - fields = "__all__" - read_only_fields = ['user', 'id'] - diff --git a/docs/source/setup.rst b/docs/source/setup.rst index 426590998..fb67ec276 100644 --- a/docs/source/setup.rst +++ b/docs/source/setup.rst @@ -1,24 +1,135 @@ Setup Instructions ================== +This project consists of a Flutter frontend and a Django REST backend. + Requirements ------------ -- Python 3.14 + +Make sure you have the following installed: + +Frontend: +- Flutter SDK (>= 3.x) +- Dart SDK (>= 3.9.0) + +Backend: +- Python (>= 3.10) +- PostgreSQL +- Redis (for Celery background tasks) + +Tools: - Git -- Installation ------------ +Clone the repository: + .. code-block:: bash git clone https://github.com/your-repo - cd project + cd your-repo + +----------------------------------- +Backend Setup (Django REST Framework) +----------------------------------- + +1. Create virtual environment: + +.. code-block:: bash + + python -m venv venv + source venv/bin/activate # Linux/Mac + venv\Scripts\activate # Windows + +2. Install dependencies: + +.. code-block:: bash + pip install -r requirements.txt -Run the Project ---------------- +3. Configure PostgreSQL database: + +Update your database settings in ``settings.py``: + +- Database name: unisoc_db +- User: unisoc_user +- Password: strongpassword + +4. Apply migrations: + +.. code-block:: bash + + python manage.py migrate + +5. Create superuser: .. code-block:: bash - python main.py + python manage.py createsuperuser + +6. Run backend server: + +.. code-block:: bash + + python manage.py runserver + +Backend runs at: +http://127.0.0.1:8000/ + +----------------------------------- +Frontend Setup (Flutter) +----------------------------------- + +1. Navigate to Flutter project: + +.. code-block:: bash + + cd frontend # adjust if different + +2. Install dependencies: + +.. code-block:: bash + + flutter pub get + +3. Run the app: + +.. code-block:: bash + + flutter run + +----------------------------------- +Celery & Redis (Background Tasks) +----------------------------------- + +Start Redis server: + +.. code-block:: bash + + redis-server + +Start Celery worker: + +.. code-block:: bash + + celery -A config worker --loglevel=info + +----------------------------------- +Environment Variables (Important) +----------------------------------- + +Update email configuration in ``settings.py``: + +- EMAIL_HOST_USER +- EMAIL_HOST_PASSWORD + +⚠️ Do not expose real credentials in production. + +----------------------------------- +Notes +----------------------------------- + +- Ensure PostgreSQL is running before starting Django +- Ensure Redis is running before starting Celery +- Flutter app communicates with backend via API endpoints \ No newline at end of file diff --git a/docs/source/views.py b/docs/source/views.py deleted file mode 100644 index c5f402ab6..000000000 --- a/docs/source/views.py +++ /dev/null @@ -1,1094 +0,0 @@ - - -from rest_framework.authtoken.models import Token -from rest_framework import generics -from .models import EventAttendance, User, Event, Society -from .serializer import UserSerializer -from .serializer import SocietySerializer -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.exceptions import PermissionDenied -from .serializer import UserSerializer, SocietySerializer, EventSerializer -from django.utils.timezone import now -from django.db.models import Count, Q -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from django.core.mail import send_mail -from django.utils import timezone -from datetime import timedelta -import re - -from .models import NotificationPreference, Society, Membership, Event - - -class UserListView(generics.ListAPIView): - """API view to list all users, with optional search and letter filtering. - - Supports the following query parameters: - - - ``search``: Filter users whose name contains the search string (case-insensitive). - - ``letter``: Filter users whose name starts with the given letter (case-insensitive). - - Results are ordered alphabetically by name. - """ - - serializer_class = UserSerializer - - def get_queryset(self): - """Return a filtered and ordered queryset of all users. - - :return: Queryset of User objects filtered by search/letter params. - :rtype: QuerySet - """ - queryset = User.objects.all().order_by('name') - - search = self.request.query_params.get('search') - letter = self.request.query_params.get('letter') - - if search: - queryset = queryset.filter(name__icontains=search) - - if letter: - queryset = queryset.filter(name__istartswith=letter) - - return queryset - - -class SocietyListSearchView(APIView): - """API view to list and search active societies. - - Requires authentication. Supports an optional ``q`` query parameter - to filter societies by name. Results include the active member count - for each society and are ordered alphabetically by name. - """ - - permission_classes = [IsAuthenticated] - - def get(self, request): - """Return a list of active societies, optionally filtered by name. - - :param request: The HTTP request, optionally containing a ``q`` query param. - :type request: Request - :return: A list of society objects with id, name, category, description, and member count. - :rtype: Response - """ - query = request.query_params.get("q", "").strip() - - societies = Society.objects.filter(is_active=True) - - if query: - societies = societies.filter(name__icontains=query) - - societies = societies.annotate( - active_member_count=Count( - 'membership', - filter=Q(membership__left_at__isnull=True) - ) - ).order_by('name') - - data = [{ - "id": s.id, - "name": s.name, - "category": s.category, - "description": s.description, - "member_count": s.active_member_count, - } for s in societies] - - return Response(data) - - -class AddEventView(generics.CreateAPIView): - """API view to create a new event for the authenticated admin's society. - - Requires authentication. Only users with the ``admin`` role can create events. - The event is automatically linked to the society managed by the authenticated admin. - - :raises PermissionDenied: If the authenticated user is not an admin. - """ - - serializer_class = EventSerializer - permission_classes = [IsAuthenticated] - - def perform_create(self, serializer): - """Save the new event, associating it with the admin's society. - - :param serializer: The validated event serializer instance. - :type serializer: EventSerializer - :raises PermissionDenied: If the user does not have the admin role. - """ - if self.request.user.role != "admin": - raise PermissionDenied("Admins only") - - society = Society.objects.get(admin=self.request.user) - - serializer.save( - created_by=self.request.user, - society=society - ) - - -class DeleteEventView(generics.DestroyAPIView): - """API view to delete an event created by the authenticated user. - - Requires authentication. Users can only delete events they created themselves. - """ - - permission_classes = [IsAuthenticated] - serializer_class = EventSerializer - lookup_field = 'id' - - def get_queryset(self): - """Return only events created by the authenticated user. - - :return: Queryset of Event objects created by the current user. - :rtype: QuerySet - """ - return Event.objects.filter(created_by=self.request.user) - - -class SocietyEventView(APIView): - """API view to retrieve or create events for a specific society. - - Requires authentication. - - - ``GET``: Returns all events belonging to the given society. - - ``POST``: Allows an admin of the society to create a new event. - """ - - permission_classes = [IsAuthenticated] - - def get(self, request, society_id): - """Return all events for the specified society. - - :param request: The HTTP request. - :type request: Request - :param society_id: The ID of the society to fetch events for. - :type society_id: int - :return: Serialized list of events, or 404 if society not found. - :rtype: Response - """ - try: - society = Society.objects.get(id=society_id) - except Society.DoesNotExist: - return Response({"error": "Society not found"}, status=404) - - events = Event.objects.filter(society=society) - serializer = EventSerializer(events, many=True) - return Response(serializer.data) - - def post(self, request, society_id): - """Create a new event for the specified society. - - Only the admin of the society can create events. - - :param request: The HTTP request containing event data. - :type request: Request - :param society_id: The ID of the society to add the event to. - :type society_id: int - :return: Serialized event data on success, or an error response. - :rtype: Response - """ - if request.user.role != "admin": - return Response({"error": "Admins only"}, status=403) - - try: - society = Society.objects.get(id=society_id, admin=request.user) - except Society.DoesNotExist: - return Response({"error": "Society not found or not admin"}, status=404) - - data = request.data.copy() - - if data.get("capacity_limit") in [0, "0", ""]: - data["capacity_limit"] = None - - serializer = EventSerializer(data=data) - - if serializer.is_valid(): - event = serializer.save( - society=society, - created_by=request.user - ) - - send_event_confirmation(request.user, event) - - return Response(serializer.data, status=201) - - return Response(serializer.errors, status=400) - - -class EventDetailView(generics.RetrieveAPIView): - """API view to retrieve details of a single event by ID. - - Requires authentication. Looks up the event using the ``id`` field. - """ - - permission_classes = [IsAuthenticated] - queryset = Event.objects.all() - serializer_class = EventSerializer - lookup_field = 'id' - - -class UpdateEventView(generics.UpdateAPIView): - """API view to update an event created by the authenticated user. - - Requires authentication. Users can only update events they created themselves. - Looks up the event using the ``id`` field. - """ - - permission_classes = [IsAuthenticated] - queryset = Event.objects.all() - serializer_class = EventSerializer - lookup_field = 'id' - - def get_queryset(self): - """Return only events created by the authenticated user. - - :return: Queryset of Event objects created by the current user. - :rtype: QuerySet - """ - return Event.objects.filter(created_by=self.request.user) - - -class MyEventsView(APIView): - """API view to retrieve events relevant to the authenticated user. - - Requires authentication. - - - For **admins**: Returns all events belonging to their managed society. - - For **regular users**: Returns all events from societies they are members of. - """ - - permission_classes = [IsAuthenticated] - - def get(self, request): - """Return events relevant to the authenticated user. - - :param request: The HTTP request. - :type request: Request - :return: Serialized list of events. - :rtype: Response - """ - if request.user.role == "admin": - society = Society.objects.get(admin=request.user) - events = Event.objects.filter(society=society) - else: - events = Event.objects.filter( - society__membership__user=request.user - ).distinct() - - serializer = EventSerializer(events, many=True) - return Response(serializer.data) - - -class AllEventsView(APIView): - """API view to retrieve the 5 most recently added events. - - Requires authentication. Returns events ordered by descending ID. - """ - - permission_classes = [IsAuthenticated] - - def get(self, request): - """Return the 5 most recent events. - - :param request: The HTTP request. - :type request: Request - :return: Serialized list of up to 5 events. - :rtype: Response - """ - events = Event.objects.all().order_by('-id')[:5] - serializer = EventSerializer(events, many=True) - return Response(serializer.data) - - -class MyCreatedEventsView(APIView): - """API view to retrieve all events created by the authenticated user. - - Requires authentication. Results are ordered by most recently created first. - """ - - permission_classes = [IsAuthenticated] - - def get(self, request): - """Return all events created by the authenticated user. - - :param request: The HTTP request. - :type request: Request - :return: Serialized list of events created by the user. - :rtype: Response - """ - events = Event.objects.filter(created_by=request.user).order_by('-created_at') - serializer = EventSerializer(events, many=True) - return Response(serializer.data) - - -class ChangePasswordView(APIView): - """API view to allow an authenticated user to change their password. - - Requires authentication. The user must provide their current password - to verify their identity before setting a new one. - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - """Change the authenticated user's password. - - :param request: The HTTP request containing ``old_password`` and ``new_password``. - :type request: Request - :return: Success message, or 400 if the old password is incorrect. - :rtype: Response - """ - user = request.user - old_password = request.data.get("old_password") - new_password = request.data.get("new_password") - - if not user.check_password(old_password): - return Response({"error": "Old password is incorrect"}, status=400) - - user.set_password(new_password) - user.save() - return Response({"message": "Password changed successfully"}) - - -class ChangeEmailView(APIView): - """API view to allow an authenticated user to change their email address. - - Requires authentication. The new email must not already be in use by another account. - """ - - permission_classes = [IsAuthenticated] - - def post(self, request): - """Change the authenticated user's email address. - - :param request: The HTTP request containing ``new_email``. - :type request: Request - :return: Success message, or 400 if the email is missing or already in use. - :rtype: Response - """ - user = request.user - new_email = request.data.get("new_email") - - if not new_email: - return Response({"error": "New email is required"}, status=400) - - if User.objects.filter(email=new_email).exists(): - return Response({"error": "Email already in use"}, status=400) - - user.email = new_email - user.save() - return Response({"message": "Email changed successfully"}) - - -class User_ProfileView(APIView): - """API view to retrieve or update the authenticated user's profile. - - Requires authentication. - - - ``GET``: Returns the current user's profile data. - - ``POST``: Updates the current user's display name. - """ - - permission_classes = [IsAuthenticated] - - def get(self, request): - """Return the authenticated user's profile. - - :param request: The HTTP request. - :type request: Request - :return: Serialized user profile data. - :rtype: Response - """ - user = request.user - serializer = UserSerializer(user) - return Response(serializer.data) - - def post(self, request): - """Update the authenticated user's display name. - - :param request: The HTTP request containing ``name``. - :type request: Request - :return: Success message, or 400 if the name is missing. - :rtype: Response - """ - user = request.user - new_name = request.data.get("name") - - if not new_name: - return Response({"error": "New name is required"}, status=400) - - user.name = new_name - user.save() - return Response({"message": "Name changed successfully"}) - - -class NotificationView(APIView): - """API view to retrieve or update the authenticated user's notification preferences. - - Requires authentication. - - - ``GET``: Returns the user's notification preferences for each society they belong to. - - ``POST``: Updates the notification preference for a specific society. - """ - - permission_classes = [IsAuthenticated] - - def get(self, request): - """Return the authenticated user's notification preferences. - - :param request: The HTTP request. - :type request: Request - :return: List of societies and their notification settings for the user. - :rtype: Response - """ - user = request.user - preferences = NotificationPreference.objects.filter(user=user) - - data = [] - for pref in preferences: - data.append({ - "society": pref.society.name, - "notify_new_events": pref.notify_new_events, - }) - - return Response(data) - - def post(self, request): - """Update the authenticated user's notification preference for a society. - - :param request: The HTTP request containing ``society_id`` and ``event_notifications``. - :type request: Request - :return: Updated preference data, or an error if the society is not found or user is not a member. - :rtype: Response - """ - user = request.user - society_id = request.data.get("society_id") - - notify_new_events = str(request.data.get("event_notifications")).lower() == "true" - - try: - society = Society.objects.get(id=society_id) - except Society.DoesNotExist: - return Response({"error": "Society not found"}, status=404) - - if not Membership.objects.filter(user=user, society=society).exists(): - return Response({"error": "Not a member of this society"}, status=403) - - pref, created = NotificationPreference.objects.update_or_create( - user=user, - society=society, - defaults={ - "notify_new_events": notify_new_events - } - ) - - return Response({ - "message": "Notification preferences updated", - "society": society.name, - "notify_new_events": pref.notify_new_events - }) - - -def send_event_confirmation(admin_user, event): - """Send a new event notification email to all opted-in society members. - - Finds all members of the event's society who have enabled new event - notifications and sends them an email with the event details. - - :param admin_user: The admin user who created the event. - :type admin_user: User - :param event: The newly created event to notify members about. - :type event: Event - """ - prefs = NotificationPreference.objects.filter( - society=event.society, - notify_new_events=True - ) - - recipient_emails = [pref.user.email for pref in prefs if pref.user.email] - - if not recipient_emails: - return - - subject = f"New Event: {event.title}" - message = f""" - Hello, - - A new event has been created in your society: {event.society.name} - - Title: {event.title} - Description: {event.description} - Start: {event.start_time} - End: {event.end_time} - - Please check the portal for more details. - """ - - send_mail( - subject=subject, - message=message, - from_email="no-reply@yoursite.com", - recipient_list=recipient_emails, - fail_silently=False, - ) - - -def send_event_reminders(): - """Send 24-hour reminder emails to admin members of upcoming events. - - Queries all events starting within the next 24 hours and sends reminder - emails to admin members of each event's society who have opted in to - 24-hour reminders via their notification preferences. - """ - now = timezone.now() - upcoming = now + timedelta(hours=24) - - events = Event.objects.filter(start_time__range=(now, upcoming)) - - for event in events: - admins = Membership.objects.filter( - society=event.society, - role="admin" - ) - - for member in admins: - user = member.user - - if not NotificationPreference.objects.filter( - user=user, - society=event.society, - notify_24hr_reminder=True - ).exists(): - continue - - send_mail( - subject="Reminder: Event in 24 Hours", - message=f""" -Reminder: "{event.title}" is in 24 hours. - -Date: {event.start_time} -Location: {event.location} -""", - from_email=None, - recipient_list=[user.email], - fail_silently=False, - ) - -class SocietyAdminDetailView(APIView): - """ - API view to retrieve detailed information about a society, - including its events. - - Returns: - - Society details - - List of associated events - - Does not require admin privileges. - """ - permission_classes = [IsAuthenticated] - - # GET society details — used by both admin and user society page - def get(self, request, society_id): - try: - society = Society.objects.get(id=society_id) - except Society.DoesNotExist: - return Response({"error": "Society not found"}, status=404) - - return Response({ - "id": society.id, - "name": society.name, - "category": society.category, - "description": society.description, - }) - - # PATCH update society description — admin only - def patch(self, request, society_id): - if request.user.role != "admin": - return Response({"error": "Admin only"}, status=403) - - try: - society = Society.objects.get(id=society_id, admin=request.user) - except Society.DoesNotExist: - return Response({"error": "Society not found or not your society"}, status=404) - - description = request.data.get("description") - if description is not None: - society.description = description - society.save() - - return Response({ - "id": society.id, - "name": society.name, - "category": society.category, - "description": society.description, - "message": "Society updated successfully" - }) - - -class SocietyMembershipCheckView(APIView): - """ - Check if the authenticated user is an active member of a society. - """ - - permission_classes = [IsAuthenticated] - - def get(self, request, society_id): - # Check active membership (not left) - is_member = Membership.objects.filter( - user=request.user, - society_id=society_id, - left_at__isnull=True - ).exists() - - return Response({ - "society_id": society_id, - "is_member": is_member - }, status=status.HTTP_200_OK) - -class SocietyDetailView(APIView): - """ - Retrieve a society along with its events. - """ - def get(self, request, society_id): - try: - society = Society.objects.get(id=society_id) - except Society.DoesNotExist: - return Response({"error": "Society not found"}, status=404) - - events = Event.objects.filter(society=society) - - event_data = [] - for event in events: - event_data.append({ - "id": event.id, - "title": event.title, - "description": event.description, - "location": event.location, - "start_time": event.start_time, - }) - - return Response({ - "id": society.id, - "name": society.name, - "category": society.category, - "description": society.description, - "events": event_data - }) - -class RegisterView(APIView): - ''' - API view to handle user registration. - Accepts user details including first name, last name, email, - university number (UP number), and password. - - Validates: - - All required fields are provided - - Passwords match - - Password strength (length, uppercase, number, special character) - - Returns: - - 201 Created on success - - 400 Bad Request on validation failure - ''' - def post(self, request): - """ - Handle user registration. - - :param request: HTTP request containing user registration data - :type request: Request - :return: Success or error response - :rtype: Response - """ - first_name = request.data.get("first_name") - last_name = request.data.get("last_name") - email = request.data.get("email") - up_number = request.data.get("up_number") - password = request.data.get("password") - confirm_password = request.data.get("confirm_password") - - if not all([first_name, last_name, email, up_number, password, confirm_password]): - return Response( - {"error": "All fields are required"}, - status=status.HTTP_400_BAD_REQUEST - ) - - if password != confirm_password: - return Response( - {"error": "Passwords do not match"}, - status=status.HTTP_400_BAD_REQUEST - ) - - # Password strength validation - if len(password) < 8: - return Response( - {"error": "Password must be at least 8 characters long"}, - status=status.HTTP_400_BAD_REQUEST - ) - - if not re.search(r"[A-Z]", password): - return Response( - {"error": "Password must contain at least one uppercase letter"}, - status=status.HTTP_400_BAD_REQUEST - ) - - if not re.search(r"[0-9]", password): - return Response( - {"error": "Password must contain at least one number"}, - status=status.HTTP_400_BAD_REQUEST - ) - - if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password): - return Response( - {"error": "Password must contain at least one special character"}, - status=status.HTTP_400_BAD_REQUEST - ) - - - -class LoginView(APIView): - """ - API view to authenticate a user and return an auth token. - - Users can log in using either: - - Email - - University number (UP number) - - Returns: - - Auth token and user details on success - - 401 Unauthorized if credentials are invalid - """ - def post(self, request): - """ - Authenticate the user and generate a token. - - :param request: HTTP request containing login credentials - :type request: Request - :return: Authentication token and user info - :rtype: Response - """ - email = request.data.get("email") - up_number = request.data.get("up_number") - password = request.data.get("password") - - if not password: - return Response({"error": "Password required"}, status=400) - - try: - if email: - user = User.objects.get(email__iexact=email) - elif up_number: - up_number = up_number.lower() - if not up_number.startswith("up"): - up_number = f"up{up_number}" - user = User.objects.get(up_number__iexact=up_number) - else: - return Response({"error": "Email or UP number required"}, status=400) - - if user.check_password(password): - token, _ = Token.objects.get_or_create(user=user) - - society_id = None - society_name = None - - if user.role == "admin": - try: - society = Society.objects.get(admin=user) - society_id = society.id - society_name = society.name - except Society.DoesNotExist: - pass - - return Response({ - "token": token.key, - "role": user.role, - "email": user.email, - "up_number": user.up_number, - "society_id": society_id, - "society_name": society_name - }) - - except User.DoesNotExist: - pass - - return Response({"error": "Invalid credentials"}, status=401) - -class LeaveSocietyView(APIView): - """ - API view to allow a user to leave a society. - - Sets the `left_at` timestamp on the membership record - instead of deleting it. - - Requires authentication. - """ - permission_classes = [IsAuthenticated] - - def post(self, request, society_id): - user = request.user - - try: - society = Society.objects.get(id=society_id) - except Society.DoesNotExist: - return Response( - {"error": "Society not found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - try: - membership = Membership.objects.get( - user=user, - society=society, - left_at__isnull=True - - ) - except Membership.DoesNotExist: - return Response( - {"error": "You are not an active member"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - membership.left_at = timezone.now() - membership.save() - - return Response( - {"message": "Successfully left society"}, - status=status.HTTP_200_OK, - ) - -class LeaveEventView(APIView): - """ - API view to allow a user to leave an event. - - Marks attendance as inactive by setting `left_at`. - - Requires authentication. - """ - permission_classes = [IsAuthenticated] - - def post(self, request, event_id): - try: - attendance = EventAttendance.objects.get( - user=request.user, - event_id=event_id, - left_at__isnull=True - ) - except EventAttendance.DoesNotExist: - return Response({"error": "Not attending this event"}, status=400) - - attendance.left_at = timezone.now() - attendance.save() - - attendee_count = EventAttendance.objects.filter( - event_id=event_id, - left_at__isnull=True).count() - - return Response({"message": "Left event successfully"}) - -class JoinSocietyView(APIView): - """ - API view to allow a user to join a society. - - Behaviour: - - Creates a new membership if none exists - - Returns 'Already joined' if user is already active - - Re-activates membership if previously left - - Requires authentication. - """ - permission_classes = [IsAuthenticated] - - def post(self, request, society_id): - user = request.user - - try: - society = Society.objects.get(id=society_id) - except Society.DoesNotExist: - return Response( - {"error": "Society not found"}, - status=status.HTTP_404_NOT_FOUND - ) - - membership, created = Membership.objects.get_or_create( - user=user, - society=society - ) - - if created: - return Response( - {"message": "Joined successfully"}, - status=status.HTTP_201_CREATED - ) - - if membership.left_at is None: - return Response({"message": "Already joined"}, status=200) - - # Rejoining - membership.left_at = None - membership.joined_at = timezone.now() - membership.save() - - return Response({"message": "Rejoined successfully"}, status=200) - -class JoinEventView(APIView): - """ - API view to allow a user to join an event. - - Behaviour: - - Prevents joining past events - - Creates attendance record if not existing - - Re-activates attendance if previously left - - Returns updated attendee count. - - Requires authentication. - """ - - permission_classes = [IsAuthenticated] - - def post(self, request, event_id): - - try: - event = Event.objects.get(id=event_id) - except Event.DoesNotExist: - return Response({"error": "Event not found"}, status=404) - - # prevent joining past events - if event.start_time < timezone.now(): - return Response( - {"error": "Event has already passed"}, - status=400 - ) - - attendance, created = EventAttendance.objects.get_or_create( - user=request.user, - event=event, - defaults={"left_at": None} - ) - - if not created: - if attendance.left_at is None: - return Response({"message": "Already attending"}, status=400) - else: - attendance.left_at = None - attendance.joined_at = timezone.now() - attendance.save() - - attendee_count = EventAttendance.objects.filter( - event=event, - left_at__isnull=True - ).count() - - return Response({ - "message": "Joined event", - "attendee_count": attendee_count - }) - -class AnalyticsView(APIView): - """ - API view to provide analytics for a society admin. - - Includes: - - Membership growth over time - - Total active members - - Total events - - Event attendance statistics - - Most popular event - - Query Parameters: - - period: 'week', 'month', '6months', 'year' - - Requires: - - Authenticated admin user - """ - permission_classes = [IsAuthenticated] - - def get(self, request): - - if request.user.role != "admin": - return Response({"error": "Admins only"}, status=403) - - period = request.query_params.get("period", "week") - - try: - society = Society.objects.get(admin=request.user) - except Society.DoesNotExist: - return Response({"error": "Society not found"}, status=404) - - now = timezone.now() - - # Decide grouping & range - if period == "week": - days_range = 7 - delta = timedelta(days=1) - label_format = "%a" # Mon Tue Wed - elif period == "month": - days_range = 30 - delta = timedelta(days=1) - label_format = "%d %b" - elif period == "6months": - days_range = 26 - delta = timedelta(weeks=1) - label_format = "Week %W" - elif period == "year": - days_range = 12 - delta = timedelta(days=30) - label_format = "%b" - else: - return Response({"error": "Invalid period"}, status=400) - - start_date = now - (delta * days_range) - - labels = [] - totals = [] - - current_date = start_date - - for _ in range(days_range): - - total = Membership.objects.filter( - society=society, - joined_at__lte=current_date - ).filter( - Q(left_at__isnull=True) | Q(left_at__gt=current_date) - ).count() - - labels.append(current_date.strftime(label_format)) - totals.append(total) - - current_date += delta - - society = Society.objects.get(admin=request.user) # gets admis society - total_events = society.events.count() # total events in that society - events_stats = society.events.annotate( - attendee_count = Count( - "eventattendance", - filter = Q(eventattendance__left_at__isnull=True) - ) - ).values("title", "attendee_count") - - #most popular event - most_popular = society.events.annotate( - attendee_count = Count( - "eventattendance", - filter = Q(eventattendance__left_at__isnull=True) - ) - ).order_by("-attendee_count").values("title", "attendee_count").first() - - live_count = Membership.objects.filter( - society=society, - left_at__isnull=True - ).count() - - return Response({ - "labels": labels, - "totals": totals, - "live_count": live_count, - "total_events": total_events, - "events_stats": list(events_stats), - "most_popular": most_popular, - "event_attendance": list(events_stats) - }) - - \ No newline at end of file From 2797765ce31cbad6c6e2a785f82fb41d46a30ee1 Mon Sep 17 00:00:00 2001 From: stuti Date: Wed, 29 Apr 2026 22:00:45 +0100 Subject: [PATCH 053/103] added all setup processes to setup.rst --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 4c3711ce19ce8a34cd7a1a64c3bd9bbb2548caed..2eb853552e50495229db0ab8b49c1249abc6e9cb 100644 GIT binary patch delta 147 zcmZoMXffEJ##lc$fPsO5g+Y%YogtH Date: Wed, 29 Apr 2026 22:01:16 +0100 Subject: [PATCH 054/103] added all commandds to setup.rst --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 2eb853552e50495229db0ab8b49c1249abc6e9cb..ed8caa1772822650939fdb1afba2ffca83f4283b 100644 GIT binary patch delta 106 zcmZoMXffEJ#u&R;fq{X6g+Y%YogtH Date: Wed, 29 Apr 2026 22:01:24 +0100 Subject: [PATCH 055/103] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index ed8caa1772822650939fdb1afba2ffca83f4283b..2c6cf273ecd6d36c9d4c02355a920bc17990aea8 100644 GIT binary patch delta 106 zcmZoMXffEJ#u&Szih+TFg+Y%YogtH Date: Wed, 29 Apr 2026 22:01:38 +0100 Subject: [PATCH 056/103] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 2c6cf273ecd6d36c9d4c02355a920bc17990aea8..d3afdb457b3b6631c2394463e8d20e5545b9a3ec 100644 GIT binary patch delta 106 zcmZoMXffEJ#u&TSje&uIg+Y%YogtH Date: Thu, 30 Apr 2026 18:43:38 +0100 Subject: [PATCH 057/103] updated index.rst --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/index.rst | 25 +++++++++++-------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.DS_Store b/.DS_Store index d3afdb457b3b6631c2394463e8d20e5545b9a3ec..b7f6878481dbcc9abd002de17af62c356ecac801 100644 GIT binary patch delta 155 zcmZoMXffEJ##n#nBLf2i3xgg*IzuKyNp8N2OHxjL5>SjIr9SBYlZ(e4QRP$c$`@o9 z1}Ep|76A1yFt96tNEU_^hJ1!(hT@!bBsm5>ZLr+rrHo=syiX_ZWt3%Z2oKwQmr-2| E0Qd?g_W%F@ delta 155 zcmZoMXffEJ##q1Bje&uIg+Y%YogtH`_ -and offers a *simple* and *intuitive* API. +UniSoc is a university society management system that allows students +to discover, join, and interact with societies and events. -Check out the :doc:`usage` section for further information, including -how to :ref:`installation` the project. - -.. note:: - - This project is under active development. +Features include: +- Society discovery and management +- Event creation and tracking +- User authentication and profiles +- Notifications and attendance tracking Contents -------- .. toctree:: :maxdepth: 2 - :caption: Contents: + :caption: Documentation: scope requirements implementation setup components - api + api \ No newline at end of file From 30cd4d13510b0a55278dc2a9f65d0265ff8b379c Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 18:44:23 +0100 Subject: [PATCH 058/103] updated and documented usage.rst --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/usage.rst | 46 ++++++++++++++++++++---------------------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/.DS_Store b/.DS_Store index b7f6878481dbcc9abd002de17af62c356ecac801..2644147be59c80746637dbaa3b277ff5f7601f68 100644 GIT binary patch delta 106 zcmZoMXffEJ#u)qL5Ca1P3xgg*IzuKyNp8N2OHxjL5>SjIH}#O(vx~SjIr9SBYlZ(e4QRP$c$`@o9 V1}Ep|76A1yFt96ZZf0y00|0!d9pnH2 diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 924afcf6c..ce198681e 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -1,34 +1,32 @@ -Usage -===== +Usage Guide +=========== -.. _installation: +This section explains how to use the UniSoc system. -Installation ------------- +User Features +------------- -To use Lumache, first install it using pip: +- Register and log into the system +- Browse and join societies +- View upcoming events +- Receive notifications -.. code-block:: console +Admin Features +-------------- - (.venv) $ pip install lumache +- Create and manage societies +- Create and manage events +- Track attendance +- Manage users -Creating recipes ----------------- +API Usage +--------- -To retrieve a list of random ingredients, -you can use the ``lumache.get_random_ingredients()`` function: +The frontend communicates with the backend using REST API endpoints. -.. autofunction:: lumache.get_random_ingredients +Example: -The ``kind`` parameter should be either ``"meat"``, ``"fish"``, -or ``"veggies"``. Otherwise, :py:func:`lumache.get_random_ingredients` -will raise an exception. - -.. autoexception:: lumache.InvalidKindError - -For example: - ->>> import lumache ->>> lumache.get_random_ingredients() -['shells', 'gorgonzola', 'parsley'] +.. code-block:: bash + GET /api/societies/ + POST /api/events/ \ No newline at end of file From b297a4ecaa080c93afd6ed236e254e1abbc1df2e Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 18:45:02 +0100 Subject: [PATCH 059/103] documented usage.rst --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 2644147be59c80746637dbaa3b277ff5f7601f68..0a590ca9a33f4efc6d41365c133f75102c728f78 100644 GIT binary patch delta 22 ecmZoMXffC@kC92Yd-4KCX(qF0oA)xhiva*ym Date: Thu, 30 Apr 2026 19:29:16 +0100 Subject: [PATCH 060/103] made documentation for user registartion --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/api.rst | 8 +- docs/source/backend/User_Registration.rst | 101 ++++++++++++++++++++++ docs/source/index.rst | 23 +++-- docs/source/usage.rst | 29 ++++--- 5 files changed, 133 insertions(+), 28 deletions(-) create mode 100644 docs/source/backend/User_Registration.rst diff --git a/.DS_Store b/.DS_Store index 0a590ca9a33f4efc6d41365c133f75102c728f78..89c84ea94f6c117ecf50f018380211156c563265 100644 GIT binary patch delta 170 zcmZoMXffEJ%2?0D+04Mez`~%%kj{|FP?DSP;*yk;p9B=+*yXdc]", password): + return Response( + {"error": "Password must contain at least one special character"}, + status=status.HTTP_400_BAD_REQUEST + ) + # Normalize UP number + up_number = up_number.lower() + if not up_number.startswith("up"): + up_number = f"up{up_number}" + # Check duplicates + if User.objects.filter(email=email).exists(): + return Response({"error": "Email already exists"}, status=400) + + if User.objects.filter(up_number=up_number).exists(): + return Response({"error": "UP number already exists"}, status=400) + # Create user + user = User.objects.create_user( + first_name=first_name, + last_name=last_name, + email=email, + up_number=up_number, + password=password + ) + return Response( + {"message": "User registered successfully"}, + status=status.HTTP_201_CREATED + ) + + +API view to handle user registration, accepts user details including first name, last name, email, + university number (UP number), and password. It validates: + - All required fields are provided + - Passwords match + - Password strength (length, uppercase, number, special character) + + Returns: + - 201 Created on success + - 400 Bad Request on validation failure diff --git a/docs/source/index.rst b/docs/source/index.rst index 34977fa75..895e78c76 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,14 +1,22 @@ UniSoc Documentation =================== -UniSoc is a university society management system that allows students -to discover, join, and interact with societies and events. +UniSoc is a full-stack university society management system designed to improve +student engagement and simplify the management of societies and events. -Features include: -- Society discovery and management -- Event creation and tracking -- User authentication and profiles -- Notifications and attendance tracking +The system enables students to discover societies, join events, and receive +notifications, while providing administrators with tools to manage societies, +track attendance, and analyse engagement. + +Project Architecture +-------------------- + +The system is composed of: + +- A Flutter frontend (mobile/web interface) +- A Django REST API backend +- A PostgreSQL database +- Redis and Celery for background processing Contents -------- @@ -22,4 +30,5 @@ Contents implementation setup components + usage api \ No newline at end of file diff --git a/docs/source/usage.rst b/docs/source/usage.rst index ce198681e..fbc7ed0f8 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -1,28 +1,29 @@ Usage Guide =========== -This section explains how to use the UniSoc system. +This section describes how to interact with the system. -User Features +User Workflow ------------- -- Register and log into the system -- Browse and join societies -- View upcoming events -- Receive notifications +1. Register an account +2. Log into the system +3. Browse available societies +4. Join societies of interest +5. View and attend events -Admin Features +Admin Workflow -------------- -- Create and manage societies -- Create and manage events -- Track attendance -- Manage users +1. Log into admin account +2. Create or manage societies +3. Create and manage events +4. Monitor attendance and engagement -API Usage ---------- +API Interaction +--------------- -The frontend communicates with the backend using REST API endpoints. +The frontend communicates with the backend via REST APIs. Example: From a8f50526f02710e1d10f9d2d1d6be2148fe4725f Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 19:31:41 +0100 Subject: [PATCH 061/103] made documenation for user and admin login --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/User_Login.rst | 83 +++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 docs/source/backend/User_Login.rst diff --git a/.DS_Store b/.DS_Store index 89c84ea94f6c117ecf50f018380211156c563265..1cf5153b5e81810df96439145f14791bcd4ab403 100644 GIT binary patch delta 168 zcmZoMXffEJ##k@u%fP_E!l1{H&XCDalAG`1l9ZF51Qg@&D*avm`r>g%RQVLR{O28# zb?EX18HT~h`MCu^Jq!$-7eFKnLkdGaLo!2gPCAktgY#Oj+~lQPu|NY2cqsW V%5MHD!q2=}faxF8W_FIh`~bKpE%pEa delta 168 zcmZoMXffEJ##qnU%)r3F!l1{H&XCDalAG`1l9ZF51Qg@g<+HQo#l_=}sPZXz Date: Thu, 30 Apr 2026 19:40:51 +0100 Subject: [PATCH 062/103] made documetation fpor user home page --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/User_Homepage.rst | 48 ++++++++++++++++++++++ docs/source/backend/User_Login.rst | 3 +- docs/source/backend/User_Registration.rst | 4 +- 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 docs/source/backend/User_Homepage.rst diff --git a/.DS_Store b/.DS_Store index 1cf5153b5e81810df96439145f14791bcd4ab403..de5484633fd506196b53c1e4cb653eb19c95124e 100644 GIT binary patch delta 146 zcmZoMXffEJ##pbC#=yY9!l1{H&XCDalAG`1l9ZF51Qg@&%$#uI^~K|ksPZXzg%RQVLR{O28# zb?EX18HT~h`MCu^Jq!$-7eFKnLkdGaLo!2gPCAktgY#Oj+~lQ Date: Thu, 30 Apr 2026 19:54:04 +0100 Subject: [PATCH 063/103] documentation for userhomepage --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/User_Homepage.rst | 35 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/.DS_Store b/.DS_Store index de5484633fd506196b53c1e4cb653eb19c95124e..9a6676ae177c50c54829793786d7ed945ca15e55 100644 GIT binary patch delta 162 zcmZoMXffEJ##nEv!N9=4!l1{H&XCDalAG`1l9ZF51Qg>a`}+9in~TRCQRP$c$`@o9 z1}Ep|76A1yFmPP}kt_@;4EYSn48=L=NOBDBbHH+wmol17-pwdD`3)l Date: Thu, 30 Apr 2026 19:58:20 +0100 Subject: [PATCH 064/103] documentation for user mysociety page --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/User_MySocietypage.rst | 143 +++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 docs/source/backend/User_MySocietypage.rst diff --git a/.DS_Store b/.DS_Store index 9a6676ae177c50c54829793786d7ed945ca15e55..2319e3bc2f37557312747e5579ccf87e8eea9c7e 100644 GIT binary patch delta 170 zcmZoMXffEJ##nFvo`HdZg+Y%YogtHQbtkchRyFM?`4z)({~wV851@$ QGO@8uEMVKr&heKY0MDo_rT_o{ delta 170 zcmZoMXffEJ##nEv!N9=4!l1{H&XCDalAG`1l9ZF51Qg>a`}+9in~TRCQRP$c$`@o9 z1}Ep|76A1yFmPP}kt_@;4EYSn48=L=NOBDBbHH+wmokbnGu(SUc`u_Zn7+#>%NVek Qk%^6MVgcJ`c8 Date: Thu, 30 Apr 2026 20:04:02 +0100 Subject: [PATCH 065/103] documentatipn for user my events page --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/User_MyEventspage.rst | 138 ++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 docs/source/backend/User_MyEventspage.rst diff --git a/.DS_Store b/.DS_Store index 2319e3bc2f37557312747e5579ccf87e8eea9c7e..258c1795b577d9d86f2ca6b4592abf8a203674bb 100644 GIT binary patch delta 170 zcmZoMXffEJ##rx@&A`CG!l1{H&XCDalAG`1l9ZF51Qg@YtJit)?&5JrRQVLV@&y@& z!O8i#1wcIv4BQ()Bnv|dLq0QbtkchRyFM?`4z)({~wV851@$ QGO@8uEMVKr&heKY0MDo_rT_o{ diff --git a/docs/source/backend/User_MyEventspage.rst b/docs/source/backend/User_MyEventspage.rst new file mode 100644 index 000000000..c271ad78b --- /dev/null +++ b/docs/source/backend/User_MyEventspage.rst @@ -0,0 +1,138 @@ +User My Events Page +=================== + +Overview +-------- + +Allows users to view, join, and leave events. + +Endpoints +--------- + +.. code-block:: http + + GET /api/my-events/ + POST /api/join-event/{id}/ + POST /api/leave-event/{id}/ + +Authentication +-------------- + +- Required + +Features +-------- + +- View joined events +- Join events +- Leave events +- Prevent joining past events + +Implementation +-------------- + +.. code-block:: python + + class MyEventsView(APIView): + + permission_classes = [IsAuthenticated] + + def get(self, request): + """Return events relevant to the authenticated user. + + :param request: The HTTP request. + :type request: Request + :return: Serialized list of events. + :rtype: Response + """ + if request.user.role == "admin": + society = Society.objects.get(admin=request.user) + events = Event.objects.filter(society=society) + else: + events = Event.objects.filter( + society__membership__user=request.user + ).distinct() + + serializer = EventSerializer(events, many=True) + return Response(serializer.data) + +API view to retrieve events relevant to the authenticated user. +- For **admins**: Returns all events belonging to their managed society. +- For **regular users**: Returns all events from societies they are members of. + +.. code-block:: python + class JoinEventView(APIView): + + permission_classes = [IsAuthenticated] + + def post(self, request, event_id): + + try: + event = Event.objects.get(id=event_id) + except Event.DoesNotExist: + return Response({"error": "Event not found"}, status=404) + + # prevent joining past events + if event.start_time < timezone.now(): + return Response( + {"error": "Event has already passed"}, + status=400 + ) + + attendance, created = EventAttendance.objects.get_or_create( + user=request.user, + event=event, + defaults={"left_at": None} + ) + + if not created: + if attendance.left_at is None: + return Response({"message": "Already attending"}, status=400) + else: + attendance.left_at = None + attendance.joined_at = timezone.now() + attendance.save() + + attendee_count = EventAttendance.objects.filter( + event=event, + left_at__isnull=True + ).count() + + return Response({ + "message": "Joined event", + "attendee_count": attendee_count + }) + +API view to allow a user to join an event. +Behaviour: + - Prevents joining past events + - Creates attendance record if not existing + - Re-activates attendance if previously left +Returns updated attendee count. + +.. code-block:: python + class LeaveEventView(APIView): + + permission_classes = [IsAuthenticated] + + def post(self, request, event_id): + try: + attendance = EventAttendance.objects.get( + user=request.user, + event_id=event_id, + left_at__isnull=True + ) + except EventAttendance.DoesNotExist: + return Response({"error": "Not attending this event"}, status=400) + + attendance.left_at = timezone.now() + attendance.save() + + attendee_count = EventAttendance.objects.filter( + event_id=event_id, + left_at__isnull=True).count() + + return Response({"message": "Left event successfully"}) + +API view to allow a user to leave an event. +Marks attendance as inactive by setting `left_at`. From b687385a49b2d735d7fb0ef1c59740dd5c368ad5 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 20:10:33 +0100 Subject: [PATCH 066/103] documenation for usersettings page --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/User_Settingspage.rst | 186 ++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 docs/source/backend/User_Settingspage.rst diff --git a/.DS_Store b/.DS_Store index 258c1795b577d9d86f2ca6b4592abf8a203674bb..36eb336a042ce8e1aa309a9c9e329ce7efad5832 100644 GIT binary patch delta 106 zcmZoMXffEJ#uyvf&%nUI!l1{H&XCDalAG`1l9ZF51Qg>?T`4;G{l(*ssPZXz Date: Thu, 30 Apr 2026 20:10:44 +0100 Subject: [PATCH 067/103] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 36eb336a042ce8e1aa309a9c9e329ce7efad5832..c14f32b68f7f58e0eee98b45808b83856c4acd5f 100644 GIT binary patch delta 28 kcmZoMXffDuo00MM Date: Thu, 30 Apr 2026 20:25:14 +0100 Subject: [PATCH 068/103] documentation for admin events page --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/Admin_Eventspage.rst | 107 +++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 docs/source/backend/Admin_Eventspage.rst diff --git a/.DS_Store b/.DS_Store index c14f32b68f7f58e0eee98b45808b83856c4acd5f..9b5412f15754e0272c1d680d0f695a5851c88ee8 100644 GIT binary patch delta 148 zcmZoMXffEJ##o=@!oa}5!l1{H&XCDalAG`1l9ZF51Qg@w_F#?qaPhb!s(cDw`GO3? x;N<+=0-zoS2A%^Tl7%6KA)g_cp*SZUNsggr0a$MGQbw`Kw;2UDpJB8R0|3W?BuD@N delta 146 zcmZoMXffEJ##kTO&%nUI!l1{H&XCDalAG`1l9ZF51Qg>?T`4;G{l(*ssPZXzX`kVJKdWZo4sCXj) diff --git a/docs/source/backend/Admin_Eventspage.rst b/docs/source/backend/Admin_Eventspage.rst new file mode 100644 index 000000000..57d50b987 --- /dev/null +++ b/docs/source/backend/Admin_Eventspage.rst @@ -0,0 +1,107 @@ +Admin Events Management +======================= + +Overview +-------- + +Allows administrators to create, update, and delete events +for their society. + +Endpoints +--------- + +.. code-block:: http + + POST /api/events/ + PATCH /api/events/{id}/ + DELETE /api/events/{id}/ + +Authentication +-------------- + +- Required (Admin only) + +Features +-------- + +- Create events +- Update events +- Delete events +- Associate events with a society + +Implementation +-------------- + +.. code-block:: python + + class AddEventView(generics.CreateAPIView): + """API view to create a new event for the authenticated admin's society. + + Requires authentication. Only users with the ``admin`` role can create events. + The event is automatically linked to the society managed by the authenticated admin. + + :raises PermissionDenied: If the authenticated user is not an admin. + """ + + serializer_class = EventSerializer + permission_classes = [IsAuthenticated] + + def perform_create(self, serializer): + """Save the new event, associating it with the admin's society. + + :param serializer: The validated event serializer instance. + :type serializer: EventSerializer + :raises PermissionDenied: If the user does not have the admin role. + """ + if self.request.user.role != "admin": + raise PermissionDenied("Admins only") + + society = Society.objects.get(admin=self.request.user) + + serializer.save( + created_by=self.request.user, + society=society + ) +API view to create a new event for the authenticated admin's society. +Only users with the ``admin`` role can create events. +The event is automatically linked to the society managed by the authenticated admin. +raises PermissionDenied: If the authenticated user is not an admin. + +.. code-block:: python + class UpdateEventView(generics.UpdateAPIView): + + permission_classes = [IsAuthenticated] + queryset = Event.objects.all() + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + """Return only events created by the authenticated user. + + :return: Queryset of Event objects created by the current user. + :rtype: QuerySet + """ + return Event.objects.filter(created_by=self.request.user) + +API view to update an event created by the authenticated user. +Admins can only update events they created themselves. +Looks up the event using the ``id`` field. + +.. code-block:: python + class DeleteEventView(generics.DestroyAPIView): + + permission_classes = [IsAuthenticated] + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + """Return only events created by the authenticated user. + + :return: Queryset of Event objects created by the current user. + :rtype: QuerySet + """ + return Event.objects.filter(created_by=self.request.user) + +API view to delete an event created by the authenticated user. +Admins can only delete events they created themselves. + From 42da3bc3a38975e84191af28543462b96b2c6486 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 20:25:20 +0100 Subject: [PATCH 069/103] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 9b5412f15754e0272c1d680d0f695a5851c88ee8..7786300f5d260538f21350ab5178832f5c697bd6 100644 GIT binary patch delta 38 rcmZoMXffEZi;=lu@yE$~8D+urT}D~P1)CX}*w`i(ux)1N_{$FfEXfWw delta 38 rcmZoMXffEZi; Date: Thu, 30 Apr 2026 20:28:06 +0100 Subject: [PATCH 070/103] cd --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/Admin_Eventspage.rst | 22 +--------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/.DS_Store b/.DS_Store index 7786300f5d260538f21350ab5178832f5c697bd6..c50cf8fce68c7dba7464814a9a3ba42b7922b9e2 100644 GIT binary patch delta 147 zcmZoMXffEJ##mp|$H2hA!l1{H&XCDalAG`1l9ZF51Qg>a|G8iA)5YVCsPZXz Date: Thu, 30 Apr 2026 20:36:23 +0100 Subject: [PATCH 071/103] documentation for analytics page --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/Admin_Analyticspage.rst | 142 ++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 docs/source/backend/Admin_Analyticspage.rst diff --git a/.DS_Store b/.DS_Store index c50cf8fce68c7dba7464814a9a3ba42b7922b9e2..e41d700756171f9d44bdc0182e6645ecddcbe3ff 100644 GIT binary patch delta 162 zcmZoMXffEJ##rC(!@$76!l1{H&XCDalAG`1l9ZF51Qg?FmUxl!>EdxmRQVLV@&y@& z!O8i#1wcIv47?XWBnv|dLq0XtCN{Q- M1#FwyIsWnk0GsS8B>(^b delta 161 zcmZoMXffEJ##mp|$H2hA!l1{H&XCDalAG`1l9ZF51Qg>a|G8iA)5YVCsPZXz Date: Thu, 30 Apr 2026 20:41:12 +0100 Subject: [PATCH 072/103] code documentation for event details --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/Event_Detailspage.rst | 35 ++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 docs/source/backend/Event_Detailspage.rst diff --git a/.DS_Store b/.DS_Store index e41d700756171f9d44bdc0182e6645ecddcbe3ff..7ea0b73b2e074735bd400df4f3c39d7ee11ea9ef 100644 GIT binary patch delta 114 zcmZoMXffEJ#uz*01_J{F3xgg*IzuKyNp8N2OHxjL5>Sle=f_VupD!MFM3qm$D_@Xd d7@VA+TL9FEdxmRQVLV@&y@& d!O8i#1wcIv47?X6H#3G$-o+@i`3<9l7y$p~AMgMG diff --git a/docs/source/backend/Event_Detailspage.rst b/docs/source/backend/Event_Detailspage.rst new file mode 100644 index 000000000..d1a7a35c8 --- /dev/null +++ b/docs/source/backend/Event_Detailspage.rst @@ -0,0 +1,35 @@ +Event Details +============= + +Overview +-------- + +Retrieves detailed information about a specific event, +including attendance data. + +Endpoint +-------- + +.. code-block:: http + + GET /api/events/{id}/ + +Authentication +-------------- + +- Required + +Implementation +-------------- + +.. code-block:: python + + class EventDetailView(generics.RetrieveAPIView): + + permission_classes = [IsAuthenticated] + queryset = Event.objects.all() + serializer_class = EventSerializer + lookup_field = 'id' + +API view to retrieve details of a single event by ID. +Requires authentication. Looks up the event using the ``id`` field. From c3f0188917c904c7b877ea95e7df0c30eb0099f3 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 20:44:40 +0100 Subject: [PATCH 073/103] cd for index.rst --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/index.rst | 20 +++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.DS_Store b/.DS_Store index 7ea0b73b2e074735bd400df4f3c39d7ee11ea9ef..3371a35e6b5c65c975c4ca94afc84b58093cfac2 100644 GIT binary patch delta 161 zcmZoMXffEJ##ld}k%57Mg+Y%YogtH delta 161 zcmZoMXffEJ##le)1_J{F3xgg*IzuKyNp8N2OHxjL5>Sle=f_VupD!MFM3qm$D_@Xd z7@VA+TL9FnUA9R$lwUdkxOB>!phUPfES4VxL6*w`i( Lux)1N_{$Ff)_N?4 diff --git a/docs/source/index.rst b/docs/source/index.rst index 895e78c76..d6f33fafe 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -31,4 +31,22 @@ Contents setup components usage - api \ No newline at end of file + api + +Pages +----- + +user_registration +admin_registration +user_login +admin_login +user_homepage +user_mysocietypage +user_myeventpage +user_settingspage +admin_homepage +admin_eventspage +admin_settingspage +admin_events +admin_analytics + From 39c427b694d91598df0651c4a358baba5a75dea5 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 20:54:43 +0100 Subject: [PATCH 074/103] added backend documentation to index.rst --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/index.rst | 27 +++++++++++---------------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/.DS_Store b/.DS_Store index 3371a35e6b5c65c975c4ca94afc84b58093cfac2..326c3549a9d53d91feb5035aae5b68f3dbe5bbd1 100644 GIT binary patch delta 156 zcmZoMXffEJ##n#+JOcv*3xgg*IzuKyNp8N2OHxjL5>SjIy!n0R*Nev;QRP$c$`@o9 z1}Ep|76A1yFbHe_kt_@;4EYSn48=L=NOBC*)WLF-mokbnGgN<_yq8gyx#8HC&375q F#Q;KgDrNux delta 156 zcmZoMXffEJ##ld}k%57Mg+Y%YogtHyumAu6 diff --git a/docs/source/index.rst b/docs/source/index.rst index d6f33fafe..faf19839d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -32,21 +32,16 @@ Contents components usage api + backend/User_Registration + backend/User_Login + backend/User_Homepage + backend/User_MySocietypage + backend/User_MyEventspage + backend/User_Settingspage + backend/Event_Detailspage + backend/Admin_Eventspage + backend/Admin_Analyticspage + + -Pages ------ - -user_registration -admin_registration -user_login -admin_login -user_homepage -user_mysocietypage -user_myeventpage -user_settingspage -admin_homepage -admin_eventspage -admin_settingspage -admin_events -admin_analytics From f75a76990cadd54d4429654e783f866fbd6d185c Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 20:55:10 +0100 Subject: [PATCH 075/103] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 326c3549a9d53d91feb5035aae5b68f3dbe5bbd1..38f200f5f7fb487363370c949f5f338781d87957 100644 GIT binary patch delta 106 zcmZoMXffEJ#u$6qoPmLXg+Y%YogtHl`qIJ V3{K9^Edc6aU=TFe+|1Z01^}NSA1eR= delta 106 zcmZoMXffEJ#u$72JOcv*3xgg*IzuKyNp8N2OHxjL5>SjIy!n0R*Nev;QRP$c$`@o9 V1}Ep|76A1yFbHhe+|1Z01^{wu9%KLj From 96adaf37b30a79644f9206db5a30c1d119a0fffc Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 21:35:22 +0100 Subject: [PATCH 076/103] added conf.py file that was deleted --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/api.rst | 30 +++++++++--------------------- docs/source/conf.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 docs/source/conf.py diff --git a/.DS_Store b/.DS_Store index 38f200f5f7fb487363370c949f5f338781d87957..a51432d96f2f09c288f6a8c550f167f5ea77785e 100644 GIT binary patch delta 135 zcmZoMXffEJ${5FN>&n2uz`~%%kj{|FP?DSP;*yk;p9B=+s5z=+^y}hrM^yO~yz&JZ zhQZ1CxdlKy3=CoplbabWn5O@nyq8gq$?@0ZyNt4o7dA68v9V1oVB5^j@s}R}Y(^z+ delta 135 zcmZoMXffEJ${5FV*_?rafrUYjA)O(Up(Hoo#U&{xKM5$t@n-e!&aW4bJEF>`;FT}P zFbq!4&n*DzVPFt6nB2^0!BqWq@?J(crej|w-({3#Jg}LOiH&Vy0o!JFj=%f>sjw+A diff --git a/docs/source/api.rst b/docs/source/api.rst index 31a8c0b85..91ccb917b 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,27 +1,15 @@ API Documentation ================= -.. autosummary:: - :toctree: generated +This section documents the backend API endpoints. - backend.authentication.views - backend.authentication.models - backend.authentication.serializer +Modules +------- -Views ------ +- Authentication (Login, Register) +- Events Management +- Society Management +- Notifications +- Analytics -.. automodule:: backend.authentication.views - :members: - -Models ------- - -.. automodule:: backend.authentication.models - :members: - -Serializers ------------ - -.. automodule:: backend.authentication.serializer - :members: \ No newline at end of file +For full endpoint details, see the Backend Documentation section. \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 000000000..7e2977309 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,36 @@ +import os +import sys + +# -- Path setup -------------------------------------------------------------- + +# Add project root to Python path (so Sphinx can find modules if needed) +sys.path.insert(0, os.path.abspath('..')) + +# -- Project information ----------------------------------------------------- + +project = 'UniSoc Documentation' +author = 'UniSoc Team' +release = '1.0' + +# -- General configuration --------------------------------------------------- + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.napoleon', +] + +templates_path = ['_templates'] +exclude_patterns = [] + +# Disable autosummary auto-generation to avoid import crashes +autosummary_generate = False + +# -- HTML output ------------------------------------------------------------- + +html_theme = 'alabaster' # simple and safe (works on ReadTheDocs) + +# If you want nicer UI later, you can switch to: +# html_theme = 'sphinx_rtd_theme' + +html_static_path = ['_static'] \ No newline at end of file From 1880cb3f74b73ad989147df3180640221fcd4fa8 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 21:55:51 +0100 Subject: [PATCH 077/103] updated the layout of cd --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/conf.py | 9 +++++---- docs/source/index.rst | 33 ++++++++++++++++++++++----------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/.DS_Store b/.DS_Store index a51432d96f2f09c288f6a8c550f167f5ea77785e..d1db84a8a5e47d8e067179a2472b736f1273e4ef 100644 GIT binary patch delta 154 zcmZoMXffEJ##o;cz`(%3!l1{H&XCDalAG`1l9ZF51Qg>4Nj;za=i+fkRQVLV@&y@& z!O8i#1wcIv3=#$)l7%6KA)g_cp*SZUNseJ=3|MaRQbtkchN@qa_cF>cR&Bn+s38Ub DFH*8@oRQVLV@&y@& z!O8i#1wcIv3}Ox-l7%6KA)g_cp*SZUNseKfI#_P DzOyCX diff --git a/docs/source/conf.py b/docs/source/conf.py index 7e2977309..01957ce35 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,12 +14,13 @@ # -- General configuration --------------------------------------------------- +html_theme = "sphinx_rtd_theme" + extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.napoleon', + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", ] - templates_path = ['_templates'] exclude_patterns = [] diff --git a/docs/source/index.rst b/docs/source/index.rst index faf19839d..9113e0cf6 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,29 +19,40 @@ The system is composed of: - Redis and Celery for background processing Contents --------- +======== .. toctree:: :maxdepth: 2 - :caption: Documentation: + :caption: Documentation scope requirements implementation setup - components - usage - api - backend/User_Registration - backend/User_Login + + +Backend +======= + +.. toctree:: + :maxdepth: 1 + :caption: Backend Pages + + backend/Admin_Analyticspage + backend/Admin_Eventspage + backend/Event_Detailspage backend/User_Homepage - backend/User_MySocietypage + backend/User_Login backend/User_MyEventspage + backend/User_MySocietypage + backend/User_Registration backend/User_Settingspage - backend/Event_Detailspage - backend/Admin_Eventspage - backend/Admin_Analyticspage +API +=== +.. toctree:: + :maxdepth: 1 + api \ No newline at end of file From bba72bce7076670853d282683a5b63d25db282a6 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 21:55:56 +0100 Subject: [PATCH 078/103] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index d1db84a8a5e47d8e067179a2472b736f1273e4ef..bd7af319dcb13f6b1575cadd64adbd2f96698af3 100644 GIT binary patch delta 21 dcmZoMXffC@pONv(2~-plAN1^`_X2weaG delta 22 ecmZoMXffC@pOG Date: Thu, 30 Apr 2026 22:07:54 +0100 Subject: [PATCH 079/103] styling cd --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/conf.py | 11 ++++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.DS_Store b/.DS_Store index bd7af319dcb13f6b1575cadd64adbd2f96698af3..f4f2c4a8d87d1a996e1e0acfb00063739bc6d525 100644 GIT binary patch delta 156 zcmZoMXffEJ##rC3%)r3F!l1{H&XCDalAG`1l9ZF51Qg@Qi7>wZ_u_F!RQVLR{AV^k z4Ecf#!{Frn+ybB;1_sFkAd-b4g(06InV~o*9Z8O1mK0cS@=``IW<`cellL*oFf)Aq KxA`ukx)=a delta 156 zcmZoMXffEJ##o;cz`(%3!l1{H&XCDalAG`1l9ZF51Qg>4Nj;za=i+fkRQVLV@&y@& z!O8i#1wcIv3=#$)l7%6KA)g_cp*SZUNseLW6|mgorHo?CoBmy#ypK_axuNRU=DUpQ FVgQ+|Di{C& diff --git a/docs/source/conf.py b/docs/source/conf.py index 01957ce35..1d9d958d3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,11 +16,12 @@ html_theme = "sphinx_rtd_theme" -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx.ext.napoleon", -] +html_theme_options = { + "style_nav_header_background": "#2980B9", + "collapse_navigation": False, + "navigation_depth": 3, +} + templates_path = ['_templates'] exclude_patterns = [] From d68f1078da28dc011d5cc17e3995482e74411268 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 22:08:14 +0100 Subject: [PATCH 080/103] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index f4f2c4a8d87d1a996e1e0acfb00063739bc6d525..607b924cd23f1a1d9ea45f1d487be51968987971 100644 GIT binary patch delta 106 zcmZoMXffEJ#uz*GF#`hw3xgg*IzuKyNp8N2OHxjL5>Sl8DD|V%zl+BmQRP$c$`@o9 V1}Ep|76A1yFh~__Zf0y00{|>m9RdIV delta 106 zcmZoMXffEJ#u(eK%)r3F!l1{H&XCDalAG`1l9ZF51Qg@Qi7>wZ_u_F!RQVLR{AV^k Z4Ecf#!{Frn+ybB;1_sFko0}OM#Q^v09QgnM From 89eb94a137a1cb5425ae325f548cd64b156f285e Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 23:18:05 +0100 Subject: [PATCH 081/103] added endpoint for admin evnts page --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/Admin_Analyticspage.rst | 4 ++-- docs/source/backend/Admin_Eventspage.rst | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.DS_Store b/.DS_Store index 607b924cd23f1a1d9ea45f1d487be51968987971..f3eb3e9423a51e2f8e31508a8ef2e0ccf132d445 100644 GIT binary patch delta 156 zcmZoMXffEJ##q1Z3j+fK3xgg*IzuKyNp8N2OHxjL5>Sj|{v8cp)=S47QRP$c$`@o9 z1}Ep|76A1yFi1}Tkt_@;4EYSn48=L=NOBCbmV@OcFJ%;EW(Z)Nyq8gqIg#Pg=6j54 FVgO0QCSCvl delta 156 zcmZoMXffEJ##le~F#`hw3xgg*IzuKyNp8N2OHxjL5>Sl8DD|V%zl+BmQRP$c$`@o9 z1}Ep|76A1yFi3$Z7KRjte1>F(;+%9OIfhwMU~?xgWfWy@P-K|Amr;)S^S_Im?=h;0 F0RXS>D763p diff --git a/docs/source/backend/Admin_Analyticspage.rst b/docs/source/backend/Admin_Analyticspage.rst index 96cb90d47..053f6dc19 100644 --- a/docs/source/backend/Admin_Analyticspage.rst +++ b/docs/source/backend/Admin_Analyticspage.rst @@ -17,9 +17,9 @@ Features Endpoint -------- -.. code-block:: http +.. code-block:: python - GET /api/analytics/?period=week + path("my-analytics/", AnalyticsView.as_view(), name="analytics") Authentication -------------- diff --git a/docs/source/backend/Admin_Eventspage.rst b/docs/source/backend/Admin_Eventspage.rst index dd94bf327..8ce4ad031 100644 --- a/docs/source/backend/Admin_Eventspage.rst +++ b/docs/source/backend/Admin_Eventspage.rst @@ -10,11 +10,10 @@ for their society. Endpoints --------- -.. code-block:: http +.. code-block:: python + path('events//update/', UpdateEventView.as_view(), name='update-event') + path('events//delete/', DeleteEventView.as_view(), name='delete-event') - POST /api/events/ - PATCH /api/events/{id}/ - DELETE /api/events/{id}/ Authentication -------------- From 3f7a05f6434f3e65715aa3956fab337d2b78c831 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 23:19:09 +0100 Subject: [PATCH 082/103] added endpoint for event detail view --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/Event_Detailspage.rst | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.DS_Store b/.DS_Store index f3eb3e9423a51e2f8e31508a8ef2e0ccf132d445..70d36e9b76d8306e92245112a0aea4091624ab54 100644 GIT binary patch delta 148 zcmZoMXffEJ##q0Bl7%6KA)g_cp*SZUNseLGBe2}$rHrD?4QXtf_cFSR0RY<$Bdq`c delta 148 zcmZoMXffEJ##q1Z3j+fK3xgg*IzuKyNp8N2OHxjL5>Sj|{v8cp)=S47QRP$c$`@o9 x1}Ep|76A1yFi1}Tkt_@;4EYSn48=L=NOBCbmV@OcFJ%;EW(Z*2yqD2k3;-a3Be?(o diff --git a/docs/source/backend/Event_Detailspage.rst b/docs/source/backend/Event_Detailspage.rst index d1a7a35c8..0be90e169 100644 --- a/docs/source/backend/Event_Detailspage.rst +++ b/docs/source/backend/Event_Detailspage.rst @@ -10,9 +10,9 @@ including attendance data. Endpoint -------- -.. code-block:: http +.. code-block:: python - GET /api/events/{id}/ + path('events//', EventDetailView.as_view(), name='event-detail') Authentication -------------- From ebfa324cd45fc162cf9add32e9bbf8e7f04a7473 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 23:20:40 +0100 Subject: [PATCH 083/103] added endpoint for user homepage --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/User_Homepage.rst | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.DS_Store b/.DS_Store index 70d36e9b76d8306e92245112a0aea4091624ab54..1854a37ac6bc12fd9c412de62c74ce8779a78f2e 100644 GIT binary patch delta 106 zcmZoMXffEJ#u$6Dl!1YPg+Y%YogtH Date: Thu, 30 Apr 2026 23:20:46 +0100 Subject: [PATCH 084/103] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 1854a37ac6bc12fd9c412de62c74ce8779a78f2e..61aef847788b3a1e9a06fe0a3a8598c0b9d5a28c 100644 GIT binary patch delta 22 ecmZoMXffC@kCAD1+~fs}QcMgLoA)q!hyegvItNJr delta 22 ecmZoMXffC@kCAECqsa>xrI;GhHt%8d5CZ^X^awft From 02adad4f2d5a17b276d6919e749588682a9e6b20 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 23:21:36 +0100 Subject: [PATCH 085/103] added endpoint for user login page --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/User_Login.rst | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.DS_Store b/.DS_Store index 61aef847788b3a1e9a06fe0a3a8598c0b9d5a28c..b6ca547547352f2194e4cfa9e2f6b20a3ab4f836 100644 GIT binary patch delta 147 zcmZoMXffEJ##n!2KLY~;3xgg*IzuKyNp8N2OHxjL5>Slef4}H_woAtyQRP$c$`@o9 w1}Ep|76A1yFvu!^NEU_^hJ1!(hT@!bBsqrJjbORSOBqF(8rE&z!{{Lf03~uHnE(I) delta 147 zcmZoMXffEJ##n!{l!1YPg+Y%YogtH Date: Thu, 30 Apr 2026 23:23:33 +0100 Subject: [PATCH 086/103] added endpoint to user my events page --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/User_MyEventspage.rst | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.DS_Store b/.DS_Store index b6ca547547352f2194e4cfa9e2f6b20a3ab4f836..03f6231c9beae7163a984e01895f57c3b432305a 100644 GIT binary patch delta 146 zcmZoMXffEJ##sMk3IhWJ3xgg*IzuKyNp8N2OHxjL5>Sj|v)ikkY?qEZqROYhSlef4}H_woAtyQRP$c$`@o9 v1}Ep|76A1yFvu!^NEU_^hJ1!(hT@!bBsqrJjbORSOBux&*KOXz=phCG09GU! diff --git a/docs/source/backend/User_MyEventspage.rst b/docs/source/backend/User_MyEventspage.rst index c271ad78b..307a02ba8 100644 --- a/docs/source/backend/User_MyEventspage.rst +++ b/docs/source/backend/User_MyEventspage.rst @@ -9,11 +9,11 @@ Allows users to view, join, and leave events. Endpoints --------- -.. code-block:: http +.. code-block:: python - GET /api/my-events/ - POST /api/join-event/{id}/ - POST /api/leave-event/{id}/ + path('events/my/', MyEventsView.as_view(), name='my-events') + path('events//join/', JoinEventView.as_view(), name='join-event') + path('events//leave/', LeaveEventView.as_view(), name='leave-event') Authentication -------------- From 89983f1444f23d6106ea113a936e47ddaf03ec63 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 23:25:59 +0100 Subject: [PATCH 087/103] added endpoimt to register --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/User_MySocietypage.rst | 8 ++++---- docs/source/backend/User_Registration.rst | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.DS_Store b/.DS_Store index 03f6231c9beae7163a984e01895f57c3b432305a..12dd8cc2ceeac75669fb9ec06b5214bfa3be21b1 100644 GIT binary patch delta 106 zcmZoMXffEJ#u)oaoPmLXg+Y%YogtHSj|v)ikkY?qEZqROYh/join/", JoinSocietyView.as_view(), name="join-society") + path("society//leave/", LeaveSocietyView.as_view(), name="leave-society") Authentication -------------- diff --git a/docs/source/backend/User_Registration.rst b/docs/source/backend/User_Registration.rst index 99a874932..38787d705 100644 --- a/docs/source/backend/User_Registration.rst +++ b/docs/source/backend/User_Registration.rst @@ -11,9 +11,9 @@ unique email and university number. Endpoint -------- -.. code-block:: http +.. code-block:: python - POST /api/register/ + path("user/register/", RegisterView.as_view(), name="register") Authentication -------------- From d75e0a0db445f0eeab5018013a420fbf1137ee70 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 23:26:14 +0100 Subject: [PATCH 088/103] added endpoint to login --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 12dd8cc2ceeac75669fb9ec06b5214bfa3be21b1..72e3ed1cefd4b7265a978dc804af4015d93a6e3f 100644 GIT binary patch delta 20 ccmZoMXffC@pONwH Date: Thu, 30 Apr 2026 23:37:07 +0100 Subject: [PATCH 089/103] changed to the right view --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/Admin_Eventspage.rst | 53 +++++++++++++++++----- docs/source/backend/User_Settingspage.rst | 4 +- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/.DS_Store b/.DS_Store index 72e3ed1cefd4b7265a978dc804af4015d93a6e3f..57050d7772a07a2e86c6b3bda930f11293a9c1f4 100644 GIT binary patch delta 107 zcmZoMXffEJ${5GO_JM(cfrUYjA)O(Up(Hoo#U&{xKM5$tu{|%tnf=mnM^yO~yz&JZ WhQZ1CxdlKy3=DD`Ha9aiiU9zzgBv&i delta 107 zcmZoMXffEJ${5G|Nt}U!frUYjA)O(Up(Hoo#U&{xKM5$t!NAb)jqTELM^yO~yz&JZ VhQZ1CxdlKy3=DD%o0}OM#Q=$+8RP%} diff --git a/docs/source/backend/Admin_Eventspage.rst b/docs/source/backend/Admin_Eventspage.rst index 8ce4ad031..37c4bee33 100644 --- a/docs/source/backend/Admin_Eventspage.rst +++ b/docs/source/backend/Admin_Eventspage.rst @@ -33,26 +33,57 @@ Implementation .. code-block:: python - class AddEventView(generics.CreateAPIView): + class SocietyEventView(APIView): - serializer_class = EventSerializer permission_classes = [IsAuthenticated] - def perform_create(self, serializer): + def get(self, request, society_id): + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response({"error": "Society not found"}, status=404) + + events = Event.objects.filter(society=society) + serializer = EventSerializer(events, many=True) + return Response(serializer.data) + + def post(self, request, society_id): + + if request.user.role != "admin": + return Response({"error": "Admins only"}, status=403) + + try: + society = Society.objects.get(id=society_id, admin=request.user) + except Society.DoesNotExist: + return Response({"error": "Society not found or not admin"}, status=404) - if self.request.user.role != "admin": - raise PermissionDenied("Admins only") + data = request.data.copy() - society = Society.objects.get(admin=self.request.user) + if data.get("capacity_limit") in [0, "0", ""]: + data["capacity_limit"] = None - serializer.save( - created_by=self.request.user, - society=society - ) -API view to create a new event for the authenticated admin's society. + serializer = EventSerializer(data=data) + + if serializer.is_valid(): + event = serializer.save( + society=society, + created_by=request.user + ) + + send_event_confirmation(request.user, event) + + return Response(serializer.data, status=201) + + return Response(serializer.errors, status=400) + +API view to retrieve or create events for a specific society. Only users with the ``admin`` role can create events. The event is automatically linked to the society managed by the authenticated admin. raises PermissionDenied: If the authenticated user is not an admin. + - ``GET``: Returns all events belonging to the given society. +- ``POST``: Allows an admin of the society to create a new event. + .. code-block:: python class UpdateEventView(generics.UpdateAPIView): diff --git a/docs/source/backend/User_Settingspage.rst b/docs/source/backend/User_Settingspage.rst index ec5a20ad5..7a401958e 100644 --- a/docs/source/backend/User_Settingspage.rst +++ b/docs/source/backend/User_Settingspage.rst @@ -12,8 +12,8 @@ Endpoints .. code-block:: http - POST /api/change-password/ - POST /api/change-email/ + path('change-password/', ChangePasswordView.as_view(), name='change-password') + path('change-email/', ChangeEmailView.as_view(), name='change-email') GET /api/profile/ PATCH /api/profile/ GET /api/notifications/ From 37dec29ab7faeaf948d5374da3aa54a0a0fab01a Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 23:40:52 +0100 Subject: [PATCH 090/103] added endpoint to user setttings page --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/User_Settingspage.rst | 34 +++++++++------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/.DS_Store b/.DS_Store index 57050d7772a07a2e86c6b3bda930f11293a9c1f4..e2c595ba3ddc6d218b8c5984b9cf22d568d6580a 100644 GIT binary patch delta 22 ecmZoMXffC@kCAC%=;Q^A(oDhZoA)xhiva*x!v`q< delta 22 ecmZoMXffC@kCAE4+sO+UrJ264ZQjf1E(QQ)mIzz` diff --git a/docs/source/backend/User_Settingspage.rst b/docs/source/backend/User_Settingspage.rst index 7a401958e..afcd8dc36 100644 --- a/docs/source/backend/User_Settingspage.rst +++ b/docs/source/backend/User_Settingspage.rst @@ -10,14 +10,12 @@ email, password, and notification preferences. Endpoints --------- -.. code-block:: http +.. code-block:: python path('change-password/', ChangePasswordView.as_view(), name='change-password') path('change-email/', ChangeEmailView.as_view(), name='change-email') - GET /api/profile/ - PATCH /api/profile/ - GET /api/notifications/ - POST /api/notifications/ + path('user/profile/', UserProfileView.as_view(), name='user-profile') + path('notifications/', NotificationView.as_view(), name='notifications') Authentication -------------- @@ -42,13 +40,7 @@ Implementation permission_classes = [IsAuthenticated] def post(self, request): - """Change the authenticated user's password. - :param request: The HTTP request containing ``old_password`` and ``new_password``. - :type request: Request - :return: Success message, or 400 if the old password is incorrect. - :rtype: Response - """ user = request.user old_password = request.data.get("old_password") new_password = request.data.get("new_password") @@ -62,20 +54,18 @@ Implementation API view to allow an authenticated user to change their password. The user must provide their current password to verify their identity before setting a new one. - + :param request: The HTTP request containing ``old_password`` and ``new_password``. + :type request: Request + :return: Success message, or 400 if the old password is incorrect. + :rtype: Response + .. code-block:: python class ChangeEmailView(APIView): permission_classes = [IsAuthenticated] def post(self, request): - """Change the authenticated user's email address. - - :param request: The HTTP request containing ``new_email``. - :type request: Request - :return: Success message, or 400 if the email is missing or already in use. - :rtype: Response - """ + user = request.user new_email = request.data.get("new_email") @@ -91,7 +81,11 @@ The user must provide their current password to verify their identity before set API view to allow an authenticated user to change their email address. The new email must not already be in use by another account. - + :param request: The HTTP request containing ``new_email``. + :type request: Request + :return: Success message, or 400 if the email is missing or already in use. + :rtype: Response + .. code-block:: python class UserProfileView(APIView): From 49af1d74af0315a6bea7e7fea5eb15a231d7d519 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 23:40:57 +0100 Subject: [PATCH 091/103] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index e2c595ba3ddc6d218b8c5984b9cf22d568d6580a..23146104d88ca5aaaf3c77eaaae8607f3530dd74 100644 GIT binary patch delta 147 zcmZoMXffEJ##k@8kAZ=Kg+Y%YogtH Date: Fri, 1 May 2026 01:06:49 +0100 Subject: [PATCH 092/103] edited setup.rst --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/setup.rst | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.DS_Store b/.DS_Store index 23146104d88ca5aaaf3c77eaaae8607f3530dd74..9589a5a8e4f96025ac84d0d13e088f3694d9745c 100644 GIT binary patch delta 155 zcmZoMXffEJ##mpyhJk^Bg+Y%YogtHcB{FQj%cw2} E0LXGB!2kdN diff --git a/docs/source/setup.rst b/docs/source/setup.rst index fb67ec276..d482e4dc4 100644 --- a/docs/source/setup.rst +++ b/docs/source/setup.rst @@ -27,8 +27,8 @@ Clone the repository: .. code-block:: bash - git clone https://github.com/your-repo - cd your-repo + git clone https://github.com/Unisoc + cd Unisoc ----------------------------------- Backend Setup (Django REST Framework) @@ -60,6 +60,7 @@ Update your database settings in ``settings.py``: .. code-block:: bash + python manage.py makemigrations python manage.py migrate 5. Create superuser: From 58532596f0412a605d7e9edfa3ba60fdd42144d3 Mon Sep 17 00:00:00 2001 From: stuti Date: Fri, 1 May 2026 02:45:22 +0100 Subject: [PATCH 093/103] checking syncing --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/backend/Admin_Eventspage.rst | 1 - docs/source/conf.py | 2 +- 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.DS_Store b/.DS_Store index 9589a5a8e4f96025ac84d0d13e088f3694d9745c..3376de3d18b14ac415cace7b19217e38c57ed398 100644 GIT binary patch delta 121 zcmZoMXffEJ#u&Tp1_J{F3xgg*IzuKyNp8N2OHxjL5>Sle|8|8&xl6|#QRP$c$`@o9 k1}Ep|76A1yFsL4w+{|df)GR-FFQY7T!+!bAcNx{i00d(p;Q#;t delta 121 zcmZoMXffEJ#u!_@hJk^Bg+Y%YogtHLZf3M#dM-41FQY6o!*rp|cNx{i04@w6{r~^~ diff --git a/docs/source/backend/Admin_Eventspage.rst b/docs/source/backend/Admin_Eventspage.rst index 37c4bee33..c44f23a5f 100644 --- a/docs/source/backend/Admin_Eventspage.rst +++ b/docs/source/backend/Admin_Eventspage.rst @@ -114,4 +114,3 @@ Looks up the event using the ``id`` field. API view to delete an event created by the authenticated user. Admins can only delete events they created themselves. - diff --git a/docs/source/conf.py b/docs/source/conf.py index 1d9d958d3..60b556fa2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -33,6 +33,6 @@ html_theme = 'alabaster' # simple and safe (works on ReadTheDocs) # If you want nicer UI later, you can switch to: -# html_theme = 'sphinx_rtd_theme' +# html_theme = 'sphinx_rtd_theme' seeing if this syncs to github html_static_path = ['_static'] \ No newline at end of file From 433dd4356fa5c8165fe2835be3e83dfdf1cea5b9 Mon Sep 17 00:00:00 2001 From: stuti Date: Tue, 5 May 2026 21:58:55 +0100 Subject: [PATCH 094/103] Enhance Admin Analytics documentation with detailed endpoint information, authentication requirements, and response structures --- docs/source/backend/Admin_Analyticspage.rst | 342 +++++++++++++------- 1 file changed, 224 insertions(+), 118 deletions(-) diff --git a/docs/source/backend/Admin_Analyticspage.rst b/docs/source/backend/Admin_Analyticspage.rst index 053f6dc19..19659c481 100644 --- a/docs/source/backend/Admin_Analyticspage.rst +++ b/docs/source/backend/Admin_Analyticspage.rst @@ -4,19 +4,22 @@ Admin Analytics Overview -------- -Provides analytical insights for society administrators. +The **Admin Analytics API** provides analytical insights for society administrators. +It aggregates membership trends, event engagement, and overall activity into a +single endpoint for dashboard visualisation. -Features --------- - -- Membership growth tracking -- Event attendance statistics -- Most popular event -- Live member count +This endpoint is designed to support admin dashboards with time-series data and +summary statistics. Endpoint -------- +.. code-block:: http + + GET /api/my-analytics/ + +**Django Route** + .. code-block:: python path("my-analytics/", AnalyticsView.as_view(), name="analytics") @@ -24,119 +27,222 @@ Endpoint Authentication -------------- -- Required (Admin only) +- **Required**: Yes +- **Access Level**: Admin users only + +Authorization Rules +~~~~~~~~~~~~~~~~~~~ + +- User must have ``role = "admin"`` +- User must be associated with a society + +Error Responses +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Status Code + - Description + * - 403 + - User is not an admin + * - 404 + - No society found for admin + +Query Parameters +---------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 20 20 40 + + * - Parameter + - Type + - Default + - Description + * - period + - string + - week + - Time range for analytics aggregation + +Allowed Values +~~~~~~~~~~~~~~ + +- ``week`` → Last 7 days (daily breakdown) +- ``month`` → Last 30 days (daily breakdown) +- ``6months`` → Last 6 months (weekly breakdown) +- ``year`` → Last 12 months (monthly breakdown) + +Example Request +~~~~~~~~~~~~~~~ + +.. code-block:: http + + GET /api/my-analytics/?period=month + +Response Structure +------------------ + +.. code-block:: json + + { + "labels": ["Mon", "Tue", "Wed"], + "totals": [10, 15, 18], + "live_count": 120, + "total_events": 25, + "events_stats": [ + { "title": "Welcome Event", "attendee_count": 50 } + ], + "most_popular": { + "title": "Welcome Event", + "attendee_count": 50 + }, + "event_attendance": [ + { "title": "Welcome Event", "attendee_count": 50 } + ] + } + +Response Fields Explained +------------------------ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Description + * - ``labels`` + - Time intervals (e.g. days, weeks, months) + * - ``totals`` + - Membership count at each interval + * - ``live_count`` + - Current active members + * - ``total_events`` + - Total number of events created + * - ``events_stats`` + - Attendance count per event + * - ``most_popular`` + - Event with highest attendance (or null) + * - ``event_attendance`` + - Duplicate of ``events_stats`` (for frontend compatibility) + +Data Flow & Logic +----------------- + +Membership Growth +~~~~~~~~~~~~~~~~~ + +Membership totals are calculated using: + +- ``joined_at <= current_date`` +- ``left_at IS NULL OR left_at > current_date`` + +This ensures historical accuracy and correct handling of users who have left. + +Time Bucketing Strategy +~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 20 20 40 + + * - Period + - Interval + - Data Points + - Label Format + * - week + - Daily + - 7 + - Mon, Tue + * - month + - Daily + - 30 + - 01 Jan + * - 6months + - Weekly + - 26 + - Week 12 + * - year + - Monthly + - 12 + - Jan + +Event Analytics +~~~~~~~~~~~~~~~ -Parameters ----------- +.. code-block:: python -- week -- month -- 6months -- year + Count("eventattendance", filter=Q(eventattendance__left_at__isnull=True)) -Implementation --------------- +Only active attendees are counted. + +Most Popular Event +~~~~~~~~~~~~~~~~~~ + +- Determined by highest attendee count +- Returns a single event +- Returns ``null`` if no events exist + +Implementation Notes +-------------------- + +Duplicate Query +~~~~~~~~~~~~~~~ .. code-block:: python - class AnalyticsView(APIView): - - permission_classes = [IsAuthenticated] - - def get(self, request): - - if request.user.role != "admin": - return Response({"error": "Admins only"}, status=403) - - period = request.query_params.get("period", "week") - - try: - society = Society.objects.get(admin=request.user) - except Society.DoesNotExist: - return Response({"error": "Society not found"}, status=404) - - now = timezone.now() - - # Decide grouping & range - if period == "week": - days_range = 7 - delta = timedelta(days=1) - label_format = "%a" # Mon Tue Wed - elif period == "month": - days_range = 30 - delta = timedelta(days=1) - label_format = "%d %b" - elif period == "6months": - days_range = 26 - delta = timedelta(weeks=1) - label_format = "Week %W" - elif period == "year": - days_range = 12 - delta = timedelta(days=30) - label_format = "%b" - else: - return Response({"error": "Invalid period"}, status=400) - - start_date = now - (delta * days_range) - - labels = [] - totals = [] - - current_date = start_date - - for _ in range(days_range): - - total = Membership.objects.filter( - society=society, - joined_at__lte=current_date - ).filter( - Q(left_at__isnull=True) | Q(left_at__gt=current_date) - ).count() - - labels.append(current_date.strftime(label_format)) - totals.append(total) - - current_date += delta - - society = Society.objects.get(admin=request.user) # gets admis society - total_events = society.events.count() # total events in that society - events_stats = society.events.annotate( - attendee_count = Count( - "eventattendance", - filter = Q(eventattendance__left_at__isnull=True) - ) - ).values("title", "attendee_count") - - #most popular event - most_popular = society.events.annotate( - attendee_count = Count( - "eventattendance", - filter = Q(eventattendance__left_at__isnull=True) - ) - ).order_by("-attendee_count").values("title", "attendee_count").first() - - live_count = Membership.objects.filter( - society=society, - left_at__isnull=True - ).count() - - return Response({ - "labels": labels, - "totals": totals, - "live_count": live_count, - "total_events": total_events, - "events_stats": list(events_stats), - "most_popular": most_popular, - "event_attendance": list(events_stats) - }) - -API view to provide analytics for a society admin. -Includes: - - Membership growth over time - - Total active members - - Total events - - Event attendance statistics - - Most popular event - -Query Parameters: - - period: 'week', 'month', '6months', 'year' + society = Society.objects.get(admin=request.user) + +This appears twice and should be reused to avoid unnecessary database calls. + +Redundant Field +~~~~~~~~~~~~~~~ + +.. code-block:: json + + "event_attendance": list(events_stats) + +Duplicates ``events_stats`` and may be removed unless required by the frontend. + +Performance Considerations +~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Membership calculation runs one query per time interval +- Event annotations are executed multiple times +- Consider optimisation using aggregation or caching + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - No society exists + - Returns 404 + * - No events + - ``most_popular = null`` + * - No members + - ``totals`` contains zeros + * - Invalid period + - Returns 400 + +Use Cases +--------- + +- Admin dashboard visualisation +- Membership growth tracking +- Event engagement analysis +- Identifying popular events + +Suggested Improvements +---------------------- + +- Remove duplicate fields +- Optimise database queries +- Add optional date range filters +- Implement caching (e.g. Redis) +- Include event IDs in responses \ No newline at end of file From 6b2a4df513fe1c586837c449c77b1d4557df8c70 Mon Sep 17 00:00:00 2001 From: stuti Date: Tue, 5 May 2026 22:00:10 +0100 Subject: [PATCH 095/103] Enhance Admin Events Management documentation with detailed endpoint descriptions, authentication rules, and error responses --- docs/source/backend/Admin_Eventspage.rst | 615 ++++++++++++++++++++--- 1 file changed, 551 insertions(+), 64 deletions(-) diff --git a/docs/source/backend/Admin_Eventspage.rst b/docs/source/backend/Admin_Eventspage.rst index c44f23a5f..8aa6f3bc3 100644 --- a/docs/source/backend/Admin_Eventspage.rst +++ b/docs/source/backend/Admin_Eventspage.rst @@ -4,113 +4,600 @@ Admin Events Management Overview -------- -Allows administrators to create, update, and delete events -for their society. +The **Admin Events Management API** מאפשר administrators to create, retrieve, +update, and delete events associated with their society. + +This module ensures that only authorised admins can manage events and that all +events are correctly linked to the society they oversee. Endpoints --------- +.. code-block:: http + + GET /api/societies//events/ + POST /api/societies//events/ + PUT /api/events//update/ + PATCH /api/events//update/ + DELETE /api/events//delete/ + +**Django Routes** + .. code-block:: python - path('events//update/', UpdateEventView.as_view(), name='update-event') - path('events//delete/', DeleteEventView.as_view(), name='delete-event') + path('events//update/', UpdateEventView.as_view(), name='update-event') + path('events//delete/', DeleteEventView.as_view(), name='delete-event') Authentication -------------- -- Required (Admin only) +- **Required**: Yes +- **Access Level**: Admin users only + +Authorization Rules +~~~~~~~~~~~~~~~~~~~ + +- User must have ``role = "admin"`` +- Admin must own the society to create events +- Admin can only update/delete events they created + +Error Responses +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Status Code + - Description + * - 403 + - User is not authorised (not an admin) + * - 404 + - Society or event not found + * - 400 + - Invalid request data Features -------- -- Create events -- Update events +- Create events linked to a society +- Retrieve all events for a society +- Update existing events - Delete events -- Associate events with a society +- Enforce ownership-based permissions +- Optional event capacity handling + +Request Handling +---------------- + +Create Event (POST) +~~~~~~~~~~~~~~~~~~~ + +Creates a new event for a society managed by the authenticated admin. + +Special Handling: +- ``capacity_limit`` values of ``0``, ``"0"``, or empty string are converted to ``null`` + +Example Request Body: + +.. code-block:: json + + { + "title": "Welcome Event", + "description": "Introduction for new members", + "date": "2026-05-10", + "location": "Main Hall", + "capacity_limit": 100 + } + +Response (201 Created): + +.. code-block:: json + + { + "id": 1, + "title": "Welcome Event", + "capacity_limit": 100 + } + +Retrieve Events (GET) +~~~~~~~~~~~~~~~~~~~~ + +Returns all events associated with a given society. + +Response: + +.. code-block:: json + + [ + { + "id": 1, + "title": "Welcome Event", + "date": "2026-05-10" + } + ] + +Update Event (PUT / PATCH) +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Updates an event created by the authenticated admin. + +- ``PUT`` replaces the entire resource +- ``PATCH`` updates partial fields + +Delete Event (DELETE) +~~~~~~~~~~~~~~~~~~~~ + +Deletes an event created by the authenticated admin. + +- Operation is irreversible +- Returns ``204 No Content`` on success + +Response Structure +------------------ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Description + * - ``id`` + - Unique identifier for the event + * - ``title`` + - Event name + * - ``description`` + - Event details + * - ``date`` + - Event date + * - ``location`` + - Event location + * - ``capacity_limit`` + - Maximum number of attendees (nullable) Implementation -------------- +Society Event View +~~~~~~~~~~~~~~~~~~ + .. code-block:: python class SocietyEventView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated] + + def get(self, request, society_id): + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response({"error": "Society not found"}, status=404) + + events = Event.objects.filter(society=society) + serializer = EventSerializer(events, many=True) + return Response(serializer.data) - def get(self, request, society_id): - - try: - society = Society.objects.get(id=society_id) - except Society.DoesNotExist: - return Response({"error": "Society not found"}, status=404) + def post(self, request, society_id): - events = Event.objects.filter(society=society) - serializer = EventSerializer(events, many=True) - return Response(serializer.data) + if request.user.role != "admin": + return Response({"error": "Admins only"}, status=403) - def post(self, request, society_id): - - if request.user.role != "admin": - return Response({"error": "Admins only"}, status=403) + try: + society = Society.objects.get(id=society_id, admin=request.user) + except Society.DoesNotExist: + return Response({"error": "Society not found or not admin"}, status=404) - try: - society = Society.objects.get(id=society_id, admin=request.user) - except Society.DoesNotExist: - return Response({"error": "Society not found or not admin"}, status=404) + data = request.data.copy() - data = request.data.copy() + if data.get("capacity_limit") in [0, "0", ""]: + data["capacity_limit"] = None - if data.get("capacity_limit") in [0, "0", ""]: - data["capacity_limit"] = None + serializer = EventSerializer(data=data) - serializer = EventSerializer(data=data) + if serializer.is_valid(): + event = serializer.save( + society=society, + created_by=request.user + ) - if serializer.is_valid(): - event = serializer.save( - society=society, - created_by=request.user - ) + send_event_confirmation(request.user, event) - send_event_confirmation(request.user, event) + return Response(serializer.data, status=201) - return Response(serializer.data, status=201) + return Response(serializer.errors, status=400) - return Response(serializer.errors, status=400) +Description: -API view to retrieve or create events for a specific society. -Only users with the ``admin`` role can create events. -The event is automatically linked to the society managed by the authenticated admin. -raises PermissionDenied: If the authenticated user is not an admin. - - ``GET``: Returns all events belonging to the given society. -- ``POST``: Allows an admin of the society to create a new event. +- ``GET`` → Returns all events for a society +- ``POST`` → Creates a new event (admin only) +- Automatically links event to the admin’s society +- Sends confirmation after successful creation +Update Event View +~~~~~~~~~~~~~~~~~ + +.. code-block:: python -.. code-block:: python class UpdateEventView(generics.UpdateAPIView): - permission_classes = [IsAuthenticated] - queryset = Event.objects.all() - serializer_class = EventSerializer - lookup_field = 'id' + permission_classes = [IsAuthenticated] + queryset = Event.objects.all() + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + return Event.objects.filter(created_by=self.request.user) - def get_queryset(self): +Description: - return Event.objects.filter(created_by=self.request.user) +- Allows admins to update only their own events +- Filters queryset by ``created_by`` + +Delete Event View +~~~~~~~~~~~~~~~~~ + +.. code-block:: python -API view to update an event created by the authenticated user. -Admins can only update events they created themselves. -Looks up the event using the ``id`` field. - -.. code-block:: python class DeleteEventView(generics.DestroyAPIView): - permission_classes = [IsAuthenticated] - serializer_class = EventSerializer - lookup_field = 'id' + permission_classes = [IsAuthenticated] + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + return Event.objects.filter(created_by=self.request.user) + +Description: + +- Allows admins to delete only their own events +- Ensures ownership-based access control + +Data Flow +--------- + +1. Admin sends request (authenticated) +2. System verifies admin role +3. System validates society ownership +4. Serializer validates input data +5. Event is created/updated/deleted +6. Response returned to client + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Non-admin user attempts action + - Returns 403 + * - Society not found + - Returns 404 + * - Event not found + - Returns 404 + * - Invalid input data + - Returns 400 + * - Capacity set to 0 + - Converted to ``null`` + +Implementation Notes +------------------- + +- ``capacity_limit`` normalization improves data consistency +- Ownership filtering prevents unauthorized modifications +- Confirmation email/function triggered on event creation +- Querysets are scoped per user for security + +Suggested Improvements +---------------------- + +- Add pagination for event listings +- Include event IDs in all responses (if not already) +- Add soft delete instead of permanent deletion +- Introduce event status (draft, published, cancelled) +- Add validation for date/time conflicts +- Log admin actions for audit trackingAdmin Events Management +======================= + +Overview +-------- + +The **Admin Events Management API** מאפשר administrators to create, retrieve, +update, and delete events associated with their society. + +This module ensures that only authorised admins can manage events and that all +events are correctly linked to the society they oversee. + +Endpoints +--------- + +.. code-block:: http + + GET /api/societies//events/ + POST /api/societies//events/ + PUT /api/events//update/ + PATCH /api/events//update/ + DELETE /api/events//delete/ + +**Django Routes** + +.. code-block:: python + + path('events//update/', UpdateEventView.as_view(), name='update-event') + path('events//delete/', DeleteEventView.as_view(), name='delete-event') + +Authentication +-------------- + +- **Required**: Yes +- **Access Level**: Admin users only + +Authorization Rules +~~~~~~~~~~~~~~~~~~~ + +- User must have ``role = "admin"`` +- Admin must own the society to create events +- Admin can only update/delete events they created + +Error Responses +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Status Code + - Description + * - 403 + - User is not authorised (not an admin) + * - 404 + - Society or event not found + * - 400 + - Invalid request data - def get_queryset(self): - - return Event.objects.filter(created_by=self.request.user) +Features +-------- + +- Create events linked to a society +- Retrieve all events for a society +- Update existing events +- Delete events +- Enforce ownership-based permissions +- Optional event capacity handling + +Request Handling +---------------- + +Create Event (POST) +~~~~~~~~~~~~~~~~~~~ + +Creates a new event for a society managed by the authenticated admin. + +Special Handling: +- ``capacity_limit`` values of ``0``, ``"0"``, or empty string are converted to ``null`` + +Example Request Body: + +.. code-block:: json + + { + "title": "Welcome Event", + "description": "Introduction for new members", + "date": "2026-05-10", + "location": "Main Hall", + "capacity_limit": 100 + } + +Response (201 Created): + +.. code-block:: json + + { + "id": 1, + "title": "Welcome Event", + "capacity_limit": 100 + } + +Retrieve Events (GET) +~~~~~~~~~~~~~~~~~~~~ + +Returns all events associated with a given society. + +Response: + +.. code-block:: json + + [ + { + "id": 1, + "title": "Welcome Event", + "date": "2026-05-10" + } + ] + +Update Event (PUT / PATCH) +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Updates an event created by the authenticated admin. + +- ``PUT`` replaces the entire resource +- ``PATCH`` updates partial fields + +Delete Event (DELETE) +~~~~~~~~~~~~~~~~~~~~ + +Deletes an event created by the authenticated admin. + +- Operation is irreversible +- Returns ``204 No Content`` on success + +Response Structure +------------------ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Description + * - ``id`` + - Unique identifier for the event + * - ``title`` + - Event name + * - ``description`` + - Event details + * - ``date`` + - Event date + * - ``location`` + - Event location + * - ``capacity_limit`` + - Maximum number of attendees (nullable) + +Implementation +-------------- + +Society Event View +~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + class SocietyEventView(APIView): + + permission_classes = [IsAuthenticated] + + def get(self, request, society_id): + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response({"error": "Society not found"}, status=404) + + events = Event.objects.filter(society=society) + serializer = EventSerializer(events, many=True) + return Response(serializer.data) + + def post(self, request, society_id): + + if request.user.role != "admin": + return Response({"error": "Admins only"}, status=403) + + try: + society = Society.objects.get(id=society_id, admin=request.user) + except Society.DoesNotExist: + return Response({"error": "Society not found or not admin"}, status=404) + + data = request.data.copy() + + if data.get("capacity_limit") in [0, "0", ""]: + data["capacity_limit"] = None + + serializer = EventSerializer(data=data) + + if serializer.is_valid(): + event = serializer.save( + society=society, + created_by=request.user + ) + + send_event_confirmation(request.user, event) + + return Response(serializer.data, status=201) + + return Response(serializer.errors, status=400) + +Description: + +- ``GET`` → Returns all events for a society +- ``POST`` → Creates a new event (admin only) +- Automatically links event to the admin’s society +- Sends confirmation after successful creation + +Update Event View +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + class UpdateEventView(generics.UpdateAPIView): + + permission_classes = [IsAuthenticated] + queryset = Event.objects.all() + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + return Event.objects.filter(created_by=self.request.user) + +Description: + +- Allows admins to update only their own events +- Filters queryset by ``created_by`` + +Delete Event View +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + class DeleteEventView(generics.DestroyAPIView): + + permission_classes = [IsAuthenticated] + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + return Event.objects.filter(created_by=self.request.user) + +Description: + +- Allows admins to delete only their own events +- Ensures ownership-based access control + +Data Flow +--------- -API view to delete an event created by the authenticated user. -Admins can only delete events they created themselves. +1. Admin sends request (authenticated) +2. System verifies admin role +3. System validates society ownership +4. Serializer validates input data +5. Event is created/updated/deleted +6. Response returned to client + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Non-admin user attempts action + - Returns 403 + * - Society not found + - Returns 404 + * - Event not found + - Returns 404 + * - Invalid input data + - Returns 400 + * - Capacity set to 0 + - Converted to ``null`` + +Implementation Notes +------------------- + +- ``capacity_limit`` normalization improves data consistency +- Ownership filtering prevents unauthorized modifications +- Confirmation email/function triggered on event creation +- Querysets are scoped per user for security + +Suggested Improvements +---------------------- + +- Add pagination for event listings +- Include event IDs in all responses (if not already) +- Add soft delete instead of permanent deletion +- Introduce event status (draft, published, cancelled) +- Add validation for date/time conflicts +- Log admin actions for audit tracking \ No newline at end of file From adc63070fe2f0622f7a29f61ff67a6d5684bdb92 Mon Sep 17 00:00:00 2001 From: stuti Date: Tue, 5 May 2026 22:01:22 +0100 Subject: [PATCH 096/103] Enhance Event Details API documentation with comprehensive endpoint information, authentication requirements, error responses, and response structure --- docs/source/backend/Event_Detailspage.rst | 145 ++++++++++++++++++++-- 1 file changed, 136 insertions(+), 9 deletions(-) diff --git a/docs/source/backend/Event_Detailspage.rst b/docs/source/backend/Event_Detailspage.rst index 0be90e169..e24f8448b 100644 --- a/docs/source/backend/Event_Detailspage.rst +++ b/docs/source/backend/Event_Detailspage.rst @@ -4,12 +4,20 @@ Event Details Overview -------- -Retrieves detailed information about a specific event, -including attendance data. +The **Event Details API** retrieves comprehensive information about a specific event, +including its metadata and associated attendance data. + +This endpoint is primarily used to display detailed event pages within the application. Endpoint -------- +.. code-block:: http + + GET /api/events// + +**Django Route** + .. code-block:: python path('events//', EventDetailView.as_view(), name='event-detail') @@ -17,7 +25,82 @@ Endpoint Authentication -------------- -- Required +- **Required**: Yes +- **Access Level**: Any authenticated user + +Authorization Rules +~~~~~~~~~~~~~~~~~~~ + +- User must be authenticated +- No admin privileges required +- Access is not restricted by event ownership + +Error Responses +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Status Code + - Description + * - 404 + - Event not found + * - 401 + - Authentication credentials missing or invalid + +Response Structure +------------------ + +.. code-block:: json + + { + "id": 1, + "title": "Welcome Event", + "description": "Introduction for new members", + "date": "2026-05-10", + "location": "Main Hall", + "capacity_limit": 100, + "society": 3, + "created_by": 5 + } + +Response Fields Explained +------------------------ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Description + * - ``id`` + - Unique identifier of the event + * - ``title`` + - Name of the event + * - ``description`` + - Detailed event information + * - ``date`` + - Scheduled date of the event + * - ``location`` + - Event location + * - ``capacity_limit`` + - Maximum number of attendees (nullable) + * - ``society`` + - ID of the associated society + * - ``created_by`` + - ID of the user who created the event + +Attendance Data +--------------- + +If included in the serializer, attendance-related fields may also be returned: + +- Total number of attendees +- List of attendees (optional) +- Attendance status for the current user + +(Implementation depends on ``EventSerializer`` configuration.) Implementation -------------- @@ -26,10 +109,54 @@ Implementation class EventDetailView(generics.RetrieveAPIView): - permission_classes = [IsAuthenticated] - queryset = Event.objects.all() - serializer_class = EventSerializer - lookup_field = 'id' + permission_classes = [IsAuthenticated] + queryset = Event.objects.all() + serializer_class = EventSerializer + lookup_field = 'id' + +Description: + +- Retrieves a single event using the ``id`` field +- Uses Django REST Framework's ``RetrieveAPIView`` +- Returns serialized event data +- Requires authentication for access + +Data Flow +--------- + +1. Client sends authenticated request with ``event_id`` +2. System queries database for matching event +3. Serializer formats event data +4. Response returned to client + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Event does not exist + - Returns 404 + * - User not authenticated + - Returns 401 + * - Event has no attendees + - Attendance fields return empty or zero values + +Implementation Notes +------------------- + +- Uses DRF generic view for simplicity and consistency +- Relies on ``EventSerializer`` for response structure +- Can be extended to include nested relationships (e.g. society details, attendees) + +Suggested Improvements +---------------------- -API view to retrieve details of a single event by ID. -Requires authentication. Looks up the event using the ``id`` field. +- Include nested society details instead of only ID +- Add attendee count directly in response +- Include user-specific attendance status (joined/not joined) +- Add caching for frequently accessed events +- Support public/private event visibility rules \ No newline at end of file From 5a20a09a0b0caef2678579ecdf5835c6404bc4fc Mon Sep 17 00:00:00 2001 From: stuti Date: Tue, 5 May 2026 22:02:38 +0100 Subject: [PATCH 097/103] Enhance User Homepage API documentation with detailed endpoint descriptions, authentication requirements, and response structures --- docs/source/backend/User_Homepage.rst | 254 +++++++++++++++++++++----- 1 file changed, 210 insertions(+), 44 deletions(-) diff --git a/docs/source/backend/User_Homepage.rst b/docs/source/backend/User_Homepage.rst index 9a1ae974b..4d0970052 100644 --- a/docs/source/backend/User_Homepage.rst +++ b/docs/source/backend/User_Homepage.rst @@ -1,84 +1,250 @@ -User HomePage -========== +User Homepage +============= Overview -------- -Displays key information for the user dashboard, including recent events -and searchable societies. +The **User Homepage API** provides core data required for the user dashboard. +It enables users to view recent events and search for societies. -Endpoint --------- +This endpoint supports dynamic content rendering for the homepage, including +event previews and searchable society listings. + +Endpoints +--------- + +.. code-block:: http + + GET /api/events/all/ + GET /api/search/?q= + +**Django Routes** .. code-block:: python path('events/all/', AllEventsView.as_view(), name='all-events') path("search/", SocietyListSearchView.as_view(), name="society-search") -Authentication +Authentication -------------- -- Required +- **Required**: Yes +- **Access Level**: Any authenticated user Features -------- -- View latest events -- Search societies -- View society details +- View the most recent events +- Search societies by name +- View society summaries (name, category, description, member count) + +--- + +All Events Endpoint +------------------ + +Retrieves the most recently created events. + +Request +~~~~~~~ + +.. code-block:: http + + GET /api/events/all/ + +Response +~~~~~~~~ + +.. code-block:: json + + [ + { + "id": 1, + "title": "Welcome Event", + "date": "2026-05-10", + "society": 3 + } + ] + +Behaviour +~~~~~~~~~ + +- Returns the **5 most recent events** +- Events are ordered by descending ID (latest first) +- Includes associated society data via ``select_related`` Implementation --------------- +~~~~~~~~~~~~~~ .. code-block:: python class AllEventsView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated] + + def get(self, request): + + events = Event.objects.select_related("society").order_by('-id')[:5] + serializer = EventSerializer(events, many=True) + return Response(serializer.data) + +--- + +Society Search Endpoint +---------------------- + +Retrieves a list of active societies, optionally filtered by a search query. + +Request +~~~~~~~ - def get(self, request): +.. code-block:: http - events = Event.objects.select_related("society").order_by('-id')[:5] - serializer = EventSerializer(events, many=True) - return Response(serializer.data) + GET /api/search/?q=music -API view to retrieve the 5 most recently added events. -Requires authentication. Returns events ordered by descending ID. -Returns the 5 most recent events. +Query Parameters +~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 30 50 + + * - Parameter + - Type + - Description + * - q + - string + - Optional search term used to filter societies by name + +Response +~~~~~~~~ + +.. code-block:: json + + [ + { + "id": 1, + "name": "Music Society", + "category": "Cultural", + "description": "A society for music lovers", + "member_count": 120 + } + ] + +Response Fields Explained +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Description + * - ``id`` + - Unique identifier of the society + * - ``name`` + - Name of the society + * - ``category`` + - Society category (e.g. Academic, Cultural) + * - ``description`` + - Brief description of the society + * - ``member_count`` + - Number of active members in the society + +Behaviour +~~~~~~~~~ + +- Returns only societies where ``is_active = True`` +- If ``q`` is provided: + - Filters societies using case-insensitive name matching +- Results are: + - Annotated with active member count + - Ordered alphabetically by name + +Implementation +~~~~~~~~~~~~~~ .. code-block:: python - class SocietyListSearchView(APIView): + class SocietyListSearchView(APIView): + + permission_classes = [IsAuthenticated] + + def get(self, request): + + query = request.query_params.get("q", "").strip() + + societies = Society.objects.filter(is_active=True) + + if query: + societies = societies.filter(name__icontains=query) + + societies = societies.annotate( + active_member_count=Count( + 'membership', + filter=Q(membership__left_at__isnull=True) + ) + ).order_by('name') + + data = [{ + "id": s.id, + "name": s.name, + "category": s.category, + "description": s.description, + "member_count": s.active_member_count, + } for s in societies] + + return Response(data) + +--- + +Data Flow +--------- + +1. User opens homepage +2. Frontend requests latest events +3. Frontend sends search queries as user types +4. Backend filters and returns matching societies +5. Results displayed dynamically on UI - permission_classes = [IsAuthenticated] +--- - def get(self, request): +Edge Cases +---------- - query = request.query_params.get("q", "").strip() +.. list-table:: + :header-rows: 1 + :widths: 30 70 - societies = Society.objects.filter(is_active=True) + * - Scenario + - Behaviour + * - No events exist + - Returns empty list + * - No societies match search + - Returns empty list + * - Missing query parameter + - Returns all active societies + * - User not authenticated + - Returns 401 - if query: - societies = societies.filter(name__icontains=query) +--- - societies = societies.annotate( - active_member_count=Count( - 'membership', - filter=Q(membership__left_at__isnull=True) - ) - ).order_by('name') +Implementation Notes +------------------- - data = [{ - "id": s.id, - "name": s.name, - "category": s.category, - "description": s.description, - "member_count": s.active_member_count, - } for s in societies] +- ``select_related("society")`` improves query performance +- Membership count uses conditional aggregation +- Search is case-insensitive for better usability +- Results are lightweight for fast frontend rendering - return Response(data) +--- -API view to list and search active societies. -Requires authentication. Supports an optional ``q`` query parameter to filter societies by name. Results include the active member count for each society and are ordered alphabetically by name. +Suggested Improvements +---------------------- -Return a list of active societies, optionally filtered by name. +- Add pagination for large society lists +- Implement debounce/throttling on frontend (already done in your UI) +- Add category-based filtering +- Include society images/logos in response +- Add trending or recommended societies +- Cache frequent search queries for performance \ No newline at end of file From 380cc30d7d7ec171ef16b9fadb33800ed54ed464 Mon Sep 17 00:00:00 2001 From: stuti Date: Tue, 5 May 2026 22:04:06 +0100 Subject: [PATCH 098/103] Enhance User Login API documentation with detailed endpoint descriptions, request and response structures, authentication requirements, and error handling --- docs/source/backend/User_Login.rst | 279 ++++++++++++++++++++++------- 1 file changed, 219 insertions(+), 60 deletions(-) diff --git a/docs/source/backend/User_Login.rst b/docs/source/backend/User_Login.rst index 5dbfbbd0c..704c92037 100644 --- a/docs/source/backend/User_Login.rst +++ b/docs/source/backend/User_Login.rst @@ -4,21 +4,121 @@ User Login Overview -------- -Authenticates a user using email or university number and returns -an authentication token. +The **User Login API** authenticates a user using either their email address +or university (UP) number and returns an authentication token. + +This token is required for accessing protected endpoints within the system. Endpoint -------- +.. code-block:: http + + POST /api/login/ + +**Django Route** + .. code-block:: python path("login/", LoginView.as_view(), name="login") +Authentication +-------------- + +- **Required**: No (public endpoint) +- **Access Level**: All users + +Request Body +------------ + +.. code-block:: json + + { + "email": "user@example.com", + "password": "password123" + } + +OR + +.. code-block:: json + + { + "up_number": "up1234567", + "password": "password123" + } + +Request Rules +~~~~~~~~~~~~~ + +- Either ``email`` or ``up_number`` must be provided +- ``password`` is required +- ``up_number`` is case-insensitive +- If ``up_number`` does not start with ``"up"``, it will be automatically prefixed + +--- + Response -------- -- Returns authentication token -- Returns user role and details +Success Response (200 OK) +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: json + + { + "token": "abc123xyz", + "role": "admin", + "email": "user@example.com", + "up_number": "up1234567", + "society_id": 1, + "society_name": "Music Society" + } + +Response Fields Explained +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Description + * - ``token`` + - Authentication token used for subsequent requests + * - ``role`` + - User role (e.g. admin, student) + * - ``email`` + - User email address + * - ``up_number`` + - University identifier + * - ``society_id`` + - ID of the society (only for admins, otherwise null) + * - ``society_name`` + - Name of the society (only for admins, otherwise null) + +Error Responses +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Status Code + - Description + * - 400 + - Missing required fields (e.g. password or login identifier) + * - 401 + - Invalid credentials + +Example Error: + +.. code-block:: json + + { + "error": "Invalid credentials" + } + +--- Implementation -------------- @@ -26,59 +126,118 @@ Implementation .. code-block:: python class LoginView(APIView): - def post(self, request): - email = request.data.get("email") - up_number = request.data.get("up_number") - password = request.data.get("password") - - if not password: - return Response({"error": "Password required"}, status=400) - - try: - if email: - user = User.objects.get(email__iexact=email) - elif up_number: - up_number = up_number.lower() - if not up_number.startswith("up"): - up_number = f"up{up_number}" - user = User.objects.get(up_number__iexact=up_number) - else: - return Response({"error": "Email or UP number required"}, status=400) - - if user.check_password(password): - token, _ = Token.objects.get_or_create(user=user) - - society_id = None - society_name = None - - if user.role == "admin": - try: - society = Society.objects.get(admin=user) - society_id = society.id - society_name = society.name - except Society.DoesNotExist: - pass - - return Response({ - "token": token.key, - "role": user.role, - "email": user.email, - "up_number": user.up_number, - "society_id": society_id, - "society_name": society_name - }) - - except User.DoesNotExist: - pass - - return Response({"error": "Invalid credentials"}, status=401) - - -API view to authenticate a user and return an auth token. -Users can log in using either: - - Email - - University number (UP number) -Returns: - - Auth token and user details on success - - 401 Unauthorized if credentials are invalid - \ No newline at end of file + + def post(self, request): + email = request.data.get("email") + up_number = request.data.get("up_number") + password = request.data.get("password") + + if not password: + return Response({"error": "Password required"}, status=400) + + try: + if email: + user = User.objects.get(email__iexact=email) + elif up_number: + up_number = up_number.lower() + if not up_number.startswith("up"): + up_number = f"up{up_number}" + user = User.objects.get(up_number__iexact=up_number) + else: + return Response({"error": "Email or UP number required"}, status=400) + + if user.check_password(password): + token, _ = Token.objects.get_or_create(user=user) + + society_id = None + society_name = None + + if user.role == "admin": + try: + society = Society.objects.get(admin=user) + society_id = society.id + society_name = society.name + except Society.DoesNotExist: + pass + + return Response({ + "token": token.key, + "role": user.role, + "email": user.email, + "up_number": user.up_number, + "society_id": society_id, + "society_name": society_name + }) + + except User.DoesNotExist: + pass + + return Response({"error": "Invalid credentials"}, status=401) + +Description +----------- + +- Authenticates user credentials against stored data +- Supports login via: + - Email (case-insensitive) + - University number (UP number) +- Automatically normalises UP numbers +- Generates or retrieves an authentication token +- Returns additional admin-specific data if applicable + +Data Flow +--------- + +1. User submits login credentials +2. System validates input fields +3. User is retrieved via email or UP number +4. Password is verified +5. Token is generated/retrieved +6. Response returned with user details + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Missing password + - Returns 400 + * - Missing email and UP number + - Returns 400 + * - Invalid credentials + - Returns 401 + * - Admin without society + - Returns null for society fields + +Implementation Notes +------------------- + +- Uses ``TokenAuthentication`` for session management +- Case-insensitive lookups improve usability +- Gracefully handles missing admin society +- Avoids user enumeration by returning generic error messages + +--- + +Security Considerations +---------------------- + +- Passwords are securely hashed and verified using Django's authentication system +- Token-based authentication is used for subsequent requests +- No sensitive data (e.g. passwords) is returned in responses +- Generic error messages prevent user enumeration attacks + +--- + +Suggested Improvements +---------------------- + +- Add rate limiting to prevent brute-force attacks +- Implement account lockout after multiple failed attempts +- Support refresh tokens or JWT authentication +- Add multi-factor authentication (MFA) +- Log login attempts for auditing \ No newline at end of file From a1866957ded706b2bb828a87a9770e3712505e3c Mon Sep 17 00:00:00 2001 From: stuti Date: Tue, 5 May 2026 22:05:57 +0100 Subject: [PATCH 099/103] Enhance User My Events API documentation with detailed endpoint descriptions, request and response structures, authentication requirements, and behavior explanations --- docs/source/backend/User_MyEventspage.rst | 338 ++++++++++++++++------ 1 file changed, 250 insertions(+), 88 deletions(-) diff --git a/docs/source/backend/User_MyEventspage.rst b/docs/source/backend/User_MyEventspage.rst index 307a02ba8..08ef8de93 100644 --- a/docs/source/backend/User_MyEventspage.rst +++ b/docs/source/backend/User_MyEventspage.rst @@ -4,11 +4,24 @@ User My Events Page Overview -------- -Allows users to view, join, and leave events. +The **User My Events API** allows authenticated users to view, join, and leave events. + +The endpoint adapts its behaviour based on the user role: + +- **Admins** → View all events belonging to their society +- **Regular users** → View events from societies they have joined Endpoints --------- +.. code-block:: http + + GET /api/events/my/ + POST /api/events//join/ + POST /api/events//leave/ + +**Django Routes** + .. code-block:: python path('events/my/', MyEventsView.as_view(), name='my-events') @@ -18,121 +31,270 @@ Endpoints Authentication -------------- -- Required +- **Required**: Yes +- **Access Level**: Any authenticated user Features -------- -- View joined events +- View relevant events based on user role - Join events - Leave events - Prevent joining past events +- Track event attendance dynamically + +--- + +My Events Endpoint +----------------- + +Retrieves events relevant to the authenticated user. + +Request +~~~~~~~ + +.. code-block:: http + + GET /api/events/my/ + +Response +~~~~~~~~ + +.. code-block:: json + + [ + { + "id": 1, + "title": "Welcome Event", + "date": "2026-05-10", + "society": 3 + } + ] + +Behaviour +~~~~~~~~~ + +- If user is **admin**: + - Returns all events for their society +- If user is **regular user**: + - Returns events from societies they are members of +- Uses ``distinct()`` to avoid duplicate results Implementation --------------- +~~~~~~~~~~~~~~ .. code-block:: python class MyEventsView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated] + + def get(self, request): + + if request.user.role == "admin": + society = Society.objects.get(admin=request.user) + events = Event.objects.filter(society=society) + else: + events = Event.objects.filter( + society__membership__user=request.user + ).distinct() + + serializer = EventSerializer(events, many=True) + return Response(serializer.data) + +--- - def get(self, request): - """Return events relevant to the authenticated user. +Join Event Endpoint +------------------ - :param request: The HTTP request. - :type request: Request - :return: Serialized list of events. - :rtype: Response - """ - if request.user.role == "admin": - society = Society.objects.get(admin=request.user) - events = Event.objects.filter(society=society) - else: - events = Event.objects.filter( - society__membership__user=request.user - ).distinct() +Allows a user to join an event. - serializer = EventSerializer(events, many=True) - return Response(serializer.data) +Request +~~~~~~~ -API view to retrieve events relevant to the authenticated user. -- For **admins**: Returns all events belonging to their managed society. -- For **regular users**: Returns all events from societies they are members of. +.. code-block:: http + + POST /api/events//join/ + +Response +~~~~~~~~ + +.. code-block:: json + + { + "message": "Joined event", + "attendee_count": 45 + } + +Behaviour +~~~~~~~~~ + +- Prevents joining events that have already started +- Creates a new attendance record if one does not exist +- If the user previously left: + - Reactivates attendance +- If already attending: + - Returns an error +- Returns updated attendee count + +Implementation +~~~~~~~~~~~~~~ .. code-block:: python + class JoinEventView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request, event_id): - - try: - event = Event.objects.get(id=event_id) - except Event.DoesNotExist: - return Response({"error": "Event not found"}, status=404) - - # prevent joining past events - if event.start_time < timezone.now(): - return Response( - {"error": "Event has already passed"}, - status=400 - ) - - attendance, created = EventAttendance.objects.get_or_create( - user=request.user, - event=event, - defaults={"left_at": None} - ) - - if not created: - if attendance.left_at is None: - return Response({"message": "Already attending"}, status=400) - else: - attendance.left_at = None - attendance.joined_at = timezone.now() - attendance.save() - - attendee_count = EventAttendance.objects.filter( - event=event, - left_at__isnull=True - ).count() - - return Response({ - "message": "Joined event", - "attendee_count": attendee_count - }) - -API view to allow a user to join an event. -Behaviour: - - Prevents joining past events - - Creates attendance record if not existing - - Re-activates attendance if previously left -Returns updated attendee count. + permission_classes = [IsAuthenticated] + + def post(self, request, event_id): + + try: + event = Event.objects.get(id=event_id) + except Event.DoesNotExist: + return Response({"error": "Event not found"}, status=404) + + if event.start_time < timezone.now(): + return Response( + {"error": "Event has already passed"}, + status=400 + ) + + attendance, created = EventAttendance.objects.get_or_create( + user=request.user, + event=event, + defaults={"left_at": None} + ) + + if not created: + if attendance.left_at is None: + return Response({"message": "Already attending"}, status=400) + else: + attendance.left_at = None + attendance.joined_at = timezone.now() + attendance.save() + + attendee_count = EventAttendance.objects.filter( + event=event, + left_at__isnull=True + ).count() + + return Response({ + "message": "Joined event", + "attendee_count": attendee_count + }) + +--- + +Leave Event Endpoint +------------------- + +Allows a user to leave an event. + +Request +~~~~~~~ + +.. code-block:: http + + POST /api/events//leave/ + +Response +~~~~~~~~ + +.. code-block:: json + + { + "message": "Left event successfully" + } + +Behaviour +~~~~~~~~~ + +- Only allows leaving if the user is currently attending +- Marks attendance as inactive by setting ``left_at`` +- Updates attendee count internally + +Implementation +~~~~~~~~~~~~~~ .. code-block:: python + class LeaveEventView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated] + + def post(self, request, event_id): - def post(self, request, event_id): - try: - attendance = EventAttendance.objects.get( - user=request.user, - event_id=event_id, - left_at__isnull=True - ) - except EventAttendance.DoesNotExist: - return Response({"error": "Not attending this event"}, status=400) + try: + attendance = EventAttendance.objects.get( + user=request.user, + event_id=event_id, + left_at__isnull=True + ) + except EventAttendance.DoesNotExist: + return Response({"error": "Not attending this event"}, status=400) - attendance.left_at = timezone.now() - attendance.save() + attendance.left_at = timezone.now() + attendance.save() - attendee_count = EventAttendance.objects.filter( - event_id=event_id, - left_at__isnull=True).count() + attendee_count = EventAttendance.objects.filter( + event_id=event_id, + left_at__isnull=True + ).count() - return Response({"message": "Left event successfully"}) + return Response({"message": "Left event successfully"}) + +--- + +Data Flow +--------- -API view to allow a user to leave an event. -Marks attendance as inactive by setting `left_at`. +1. User requests their events +2. System determines user role +3. Relevant events are retrieved +4. User joins or leaves events +5. Attendance records are updated +6. Updated data returned to frontend + +--- + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Event does not exist + - Returns 404 + * - User tries to join past event + - Returns 400 + * - User already attending + - Returns 400 + * - User tries to leave without joining + - Returns 400 + * - No events available + - Returns empty list + +--- + +Implementation Notes +------------------- + +- Uses ``get_or_create`` to simplify attendance logic +- Soft delete pattern used via ``left_at`` field +- ``distinct()`` prevents duplicate events in queries +- Time-based validation ensures logical consistency + +--- + +Suggested Improvements +---------------------- + +- Add capacity limit validation before joining +- Return updated attendee count on leave +- Add waitlist functionality for full events +- Include user attendance status in event responses +- Add pagination for large event lists +- Send notifications when joining/leaving events \ No newline at end of file From eef9500e413ffa79f4efe0881a7ed255f5044711 Mon Sep 17 00:00:00 2001 From: stuti Date: Tue, 5 May 2026 22:07:06 +0100 Subject: [PATCH 100/103] Enhance User My Societies API documentation with detailed endpoint descriptions, request and response structures, authentication requirements, and behavior explanations --- docs/source/backend/User_MySocietypage.rst | 341 +++++++++++++++------ 1 file changed, 252 insertions(+), 89 deletions(-) diff --git a/docs/source/backend/User_MySocietypage.rst b/docs/source/backend/User_MySocietypage.rst index 6873b4c20..fe5ece97b 100644 --- a/docs/source/backend/User_MySocietypage.rst +++ b/docs/source/backend/User_MySocietypage.rst @@ -4,11 +4,23 @@ User My Societies Page Overview -------- -Allows users to view and manage societies they are part of. +The **User My Societies API** allows authenticated users to view and manage +their society memberships. + +Users can join new societies, leave existing ones, and retrieve a list of +societies they are currently part of. Endpoints --------- +.. code-block:: http + + GET /api/my-societies/ + POST /api/society//join/ + POST /api/society//leave/ + +**Django Routes** + .. code-block:: python path("my-societies/", MySocietiesView.as_view(), name="my-societies") @@ -18,126 +30,277 @@ Endpoints Authentication -------------- -- Required +- **Required**: Yes +- **Access Level**: Any authenticated user Features -------- -- View joined societies -- Join new societies +- View currently joined societies +- Join societies - Leave societies +- Rejoin previously left societies +- Soft-delete memberships using timestamps + +--- + +My Societies Endpoint +-------------------- + +Retrieves all societies the user is currently a member of. + +Request +~~~~~~~ + +.. code-block:: http + + GET /api/my-societies/ + +Response +~~~~~~~~ + +.. code-block:: json + + [ + { + "id": 1, + "name": "Music Society", + "category": "Cultural", + "description": "A society for music lovers" + } + ] + +Behaviour +~~~~~~~~~ + +- Returns only active memberships (``left_at IS NULL``) +- Uses ``select_related`` for efficient querying +- Returns simplified society data Implementation --------------- +~~~~~~~~~~~~~~ .. code-block:: python class MySocietiesView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated] + + def get(self, request): + + memberships = Membership.objects.filter( + user=request.user, + left_at__isnull=True + ).select_related("society") + + societies = [] + for m in memberships: + s = m.society + societies.append({ + "id": s.id, + "name": s.name, + "category": s.category, + "description": s.description, + }) + + return Response(societies) + +--- + +Join Society Endpoint +-------------------- + +Allows a user to join a society. + +Request +~~~~~~~ + +.. code-block:: http + + POST /api/society//join/ - def get(self, request): - memberships = Membership.objects.filter( - user=request.user, - left_at__isnull=True - ).select_related("society") +Response +~~~~~~~~ - societies = [] - for m in memberships: - s = m.society - societies.append({ - "id": s.id, - "name": s.name, - "category": s.category, - "description": s.description, - }) +.. code-block:: json - return Response(societies) + { + "message": "Joined successfully" + } -Returns all societies the user is currently a member of. +Behaviour +~~~~~~~~~ + +- Creates a new membership if none exists +- If already a member: + - Returns ``Already joined`` +- If previously left: + - Reactivates membership + - Updates ``joined_at`` timestamp + +Implementation +~~~~~~~~~~~~~~ .. code-block:: python + class JoinSocietyView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated] + + def post(self, request, society_id): + + user = request.user + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response( + {"error": "Society not found"}, + status=status.HTTP_404_NOT_FOUND + ) + + membership, created = Membership.objects.get_or_create( + user=user, + society=society + ) + + if created: + return Response( + {"message": "Joined successfully"}, + status=status.HTTP_201_CREATED + ) + + if membership.left_at is None: + return Response({"message": "Already joined"}, status=200) + + membership.left_at = None + membership.joined_at = timezone.now() + membership.save() + + return Response({"message": "Rejoined successfully"}, status=200) + +--- - def post(self, request, society_id): - user = request.user +Leave Society Endpoint +--------------------- - try: - society = Society.objects.get(id=society_id) - except Society.DoesNotExist: - return Response( - {"error": "Society not found"}, - status=status.HTTP_404_NOT_FOUND - ) +Allows a user to leave a society. - membership, created = Membership.objects.get_or_create( - user=user, - society=society - ) +Request +~~~~~~~ - if created: - return Response( - {"message": "Joined successfully"}, - status=status.HTTP_201_CREATED - ) +.. code-block:: http - if membership.left_at is None: - return Response({"message": "Already joined"}, status=200) + POST /api/society//leave/ - # Rejoining - membership.left_at = None - membership.joined_at = timezone.now() - membership.save() +Response +~~~~~~~~ - return Response({"message": "Rejoined successfully"}, status=20 +.. code-block:: json + { + "message": "Successfully left society" + } -API view to allow a user to join a society. -Behaviour: - - Creates a new membership if none exists - - Returns 'Already joined' if user is already active - - Re-activates membership if previously left +Behaviour +~~~~~~~~~ + +- Only allows leaving if the user is an active member +- Uses soft delete by setting ``left_at`` +- Membership record is preserved for history + +Implementation +~~~~~~~~~~~~~~ .. code-block:: python class LeaveSocietyView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request, society_id): - user = request.user - - try: - society = Society.objects.get(id=society_id) - except Society.DoesNotExist: - return Response( - {"error": "Society not found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - try: - membership = Membership.objects.get( - user=user, - society=society, - left_at__isnull=True - - ) - except Membership.DoesNotExist: - return Response( - {"error": "You are not an active member"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - membership.left_at = timezone.now() - membership.save() - - return Response( - {"message": "Successfully left society"}, - status=status.HTTP_200_OK, - ) - -API view to allow a user to leave a society. -Sets the `left_at` timestamp on the membership record instead of deleting it. + permission_classes = [IsAuthenticated] + + def post(self, request, society_id): + + user = request.user + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response( + {"error": "Society not found"}, + status=status.HTTP_404_NOT_FOUND + ) + + try: + membership = Membership.objects.get( + user=user, + society=society, + left_at__isnull=True + ) + except Membership.DoesNotExist: + return Response( + {"error": "You are not an active member"}, + status=status.HTTP_400_BAD_REQUEST + ) + + membership.left_at = timezone.now() + membership.save() + + return Response( + {"message": "Successfully left society"}, + status=status.HTTP_200_OK + ) + +--- + +Data Flow +--------- +1. User requests their societies +2. System retrieves active memberships +3. User joins or leaves societies +4. Membership records are created or updated +5. Response returned to frontend + +--- + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Society does not exist + - Returns 404 + * - User already joined + - Returns message without duplication + * - User rejoins after leaving + - Membership reactivated + * - User leaves without being a member + - Returns 400 + * - No societies joined + - Returns empty list + +--- + +Implementation Notes +------------------- + +- Uses ``get_or_create`` for efficient membership handling +- Soft delete pattern implemented via ``left_at`` +- ``select_related`` improves database performance +- Prevents duplicate memberships + +--- + +Suggested Improvements +---------------------- + +- Add pagination for large society lists +- Include member count in response +- Add role within society (e.g. member, admin) +- Add notifications when joining/leaving +- Prevent joining inactive societies +- Add audit logging for membership changes \ No newline at end of file From 3b0434f9339d759c2ac162bdf303e6a4f862afe6 Mon Sep 17 00:00:00 2001 From: stuti Date: Tue, 5 May 2026 22:11:05 +0100 Subject: [PATCH 101/103] Enhance User Registration API documentation with detailed endpoint descriptions, request body examples, validation rules, response structures, and security considerations --- docs/source/backend/User_Registration.rst | 314 ++++++++++++++++------ 1 file changed, 235 insertions(+), 79 deletions(-) diff --git a/docs/source/backend/User_Registration.rst b/docs/source/backend/User_Registration.rst index 38787d705..ef5d28dc7 100644 --- a/docs/source/backend/User_Registration.rst +++ b/docs/source/backend/User_Registration.rst @@ -4,13 +4,20 @@ User Registration Overview -------- -This endpoint allows a new user to register an account in the system. -It validates user input, enforces password strength rules, and ensures -unique email and university number. +The **User Registration API** allows new users to create an account in the system. + +It validates user input, enforces password strength requirements, and ensures +that both email and university (UP) number are unique. Endpoint -------- +.. code-block:: http + + POST /api/user/register/ + +**Django Route** + .. code-block:: python path("user/register/", RegisterView.as_view(), name="register") @@ -18,7 +25,97 @@ Endpoint Authentication -------------- -- Not required +- **Required**: No +- **Access Level**: Public + +--- + +Request Body +------------ + +.. code-block:: json + + { + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com", + "up_number": "up1234567", + "password": "SecurePass1!", + "confirm_password": "SecurePass1!" + } + +Request Rules +~~~~~~~~~~~~~ + +- All fields are required +- ``password`` and ``confirm_password`` must match +- ``up_number`` is case-insensitive +- If ``up_number`` does not start with ``"up"``, it will be automatically prefixed + +--- + +Validation Rules +---------------- + +Password Requirements +~~~~~~~~~~~~~~~~~~~~~ + +- Minimum 8 characters +- At least one uppercase letter +- At least one number +- At least one special character + +Uniqueness Constraints +~~~~~~~~~~~~~~~~~~~~~~ + +- Email must be unique +- UP number must be unique + +--- + +Response +-------- + +Success Response (201 Created) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: json + + { + "message": "User registered successfully" + } + +Error Responses +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Status Code + - Description + * - 400 + - Missing fields or validation failure + +Example Errors: + +.. code-block:: json + + { "error": "All fields are required" } + +.. code-block:: json + + { "error": "Passwords do not match" } + +.. code-block:: json + + { "error": "Password must contain at least one uppercase letter" } + +.. code-block:: json + + { "error": "Email already exists" } + +--- Implementation -------------- @@ -26,78 +123,137 @@ Implementation .. code-block:: python class RegisterView(APIView): - - def post(self, request): - first_name = request.data.get("first_name") - last_name = request.data.get("last_name") - email = request.data.get("email") - up_number = request.data.get("up_number") - password = request.data.get("password") - confirm_password = request.data.get("confirm_password") - - # Check required fields - if not all([first_name, last_name, email, up_number, password, confirm_password]): - return Response( - {"error": "All fields are required"}, - status=status.HTTP_400_BAD_REQUEST - ) - # Password match - if password != confirm_password: - return Response( - {"error": "Passwords do not match"}, - status=status.HTTP_400_BAD_REQUEST - ) - # Password strength - if len(password) < 8: - return Response( - {"error": "Password must be at least 8 characters long"}, - status=status.HTTP_400_BAD_REQUEST - ) - if not re.search(r"[A-Z]", password): - return Response( - {"error": "Password must contain at least one uppercase letter"}, - status=status.HTTP_400_BAD_REQUEST - ) - if not re.search(r"[0-9]", password): - return Response( - {"error": "Password must contain at least one number"}, - status=status.HTTP_400_BAD_REQUEST - ) - if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password): - return Response( - {"error": "Password must contain at least one special character"}, - status=status.HTTP_400_BAD_REQUEST - ) - # Normalize UP number - up_number = up_number.lower() - if not up_number.startswith("up"): - up_number = f"up{up_number}" - # Check duplicates - if User.objects.filter(email=email).exists(): - return Response({"error": "Email already exists"}, status=400) - - if User.objects.filter(up_number=up_number).exists(): - return Response({"error": "UP number already exists"}, status=400) - # Create user - user = User.objects.create_user( - first_name=first_name, - last_name=last_name, - email=email, - up_number=up_number, - password=password - ) - return Response( - {"message": "User registered successfully"}, - status=status.HTTP_201_CREATED - ) - - -API view to handle user registration, accepts user details including first name, last name, email, - university number (UP number), and password. It validates: - - All required fields are provided - - Passwords match - - Password strength (length, uppercase, number, special character) - - Returns: - - 201 Created on success - - 400 Bad Request on validation failure + + def post(self, request): + + first_name = request.data.get("first_name") + last_name = request.data.get("last_name") + email = request.data.get("email") + up_number = request.data.get("up_number") + password = request.data.get("password") + confirm_password = request.data.get("confirm_password") + + if not all([first_name, last_name, email, up_number, password, confirm_password]): + return Response( + {"error": "All fields are required"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if password != confirm_password: + return Response( + {"error": "Passwords do not match"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if len(password) < 8: + return Response( + {"error": "Password must be at least 8 characters long"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not re.search(r"[A-Z]", password): + return Response( + {"error": "Password must contain at least one uppercase letter"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not re.search(r"[0-9]", password): + return Response( + {"error": "Password must contain at least one number"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password): + return Response( + {"error": "Password must contain at least one special character"}, + status=status.HTTP_400_BAD_REQUEST + ) + + up_number = up_number.lower() + if not up_number.startswith("up"): + up_number = f"up{up_number}" + + if User.objects.filter(email=email).exists(): + return Response({"error": "Email already exists"}, status=400) + + if User.objects.filter(up_number=up_number).exists(): + return Response({"error": "UP number already exists"}, status=400) + + user = User.objects.create_user( + first_name=first_name, + last_name=last_name, + email=email, + up_number=up_number, + password=password + ) + + return Response( + {"message": "User registered successfully"}, + status=status.HTTP_201_CREATED + ) + +--- + +Data Flow +--------- + +1. User submits registration form +2. System validates all required fields +3. Password rules are enforced +4. UP number is normalised +5. System checks for duplicate email and UP number +6. User account is created +7. Success response returned + +--- + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Missing required fields + - Returns 400 + * - Passwords do not match + - Returns 400 + * - Weak password + - Returns 400 + * - Email already exists + - Returns 400 + * - UP number already exists + - Returns 400 + +--- + +Implementation Notes +------------------- + +- Uses Django's ``create_user`` for secure password hashing +- Input validation handled manually in the view +- UP number normalisation ensures consistent storage +- Prevents duplicate user records + +--- + +Security Considerations +---------------------- + +- Passwords are never stored in plain text +- Strong password policy enforced +- Duplicate checks prevent account conflicts +- No sensitive data is returned in responses + +--- + +Suggested Improvements +---------------------- + +- Add email verification step +- Implement CAPTCHA to prevent bot registrations +- Add rate limiting to prevent abuse +- Return authentication token upon registration +- Move validation logic to serializers for cleaner design \ No newline at end of file From a78f6f1380cf05c841fcc7bdbbdbb3e5ea570237 Mon Sep 17 00:00:00 2001 From: stuti Date: Tue, 5 May 2026 22:12:24 +0100 Subject: [PATCH 102/103] Enhance User Settings API documentation with detailed endpoint descriptions, request and response structures, authentication requirements, and behavior explanations --- docs/source/backend/User_Settingspage.rst | 471 +++++++++++++++++----- 1 file changed, 365 insertions(+), 106 deletions(-) diff --git a/docs/source/backend/User_Settingspage.rst b/docs/source/backend/User_Settingspage.rst index afcd8dc36..8ddd86613 100644 --- a/docs/source/backend/User_Settingspage.rst +++ b/docs/source/backend/User_Settingspage.rst @@ -4,12 +4,27 @@ User Settings Page Overview -------- -Allows users to manage their account settings, including profile, -email, password, and notification preferences. +The **User Settings API** allows authenticated users to manage their account +settings, including password, email, profile information, and notification +preferences. + +This module provides secure endpoints for updating sensitive user data and +customising user-specific settings. Endpoints --------- +.. code-block:: http + + POST /api/change-password/ + POST /api/change-email/ + GET /api/user/profile/ + PATCH /api/user/profile/ + GET /api/notifications/ + POST /api/notifications/ + +**Django Routes** + .. code-block:: python path('change-password/', ChangePasswordView.as_view(), name='change-password') @@ -20,161 +35,405 @@ Endpoints Authentication -------------- -- Required +- **Required**: Yes +- **Access Level**: Any authenticated user Features -------- -- Change password -- Change email -- Update profile information -- Manage notification preferences +- Change password securely +- Update email address +- View and update profile information +- Manage notification preferences per society + +--- + +Change Password Endpoint +----------------------- + +Allows a user to update their password. + +Request +~~~~~~~ + +.. code-block:: http + + POST /api/change-password/ + +.. code-block:: json + + { + "old_password": "OldPass123!", + "new_password": "NewPass456!" + } + +Response +~~~~~~~~ + +.. code-block:: json + + { + "message": "Password changed successfully" + } + +Behaviour +~~~~~~~~~ + +- Verifies the current password before updating +- Updates password using Django's secure hashing Implementation --------------- +~~~~~~~~~~~~~~ .. code-block:: python class ChangePasswordView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated] + + def post(self, request): + + user = request.user + old_password = request.data.get("old_password") + new_password = request.data.get("new_password") + + if not user.check_password(old_password): + return Response({"error": "Old password is incorrect"}, status=400) + + user.set_password(new_password) + user.save() - def post(self, request): + return Response({"message": "Password changed successfully"}) - user = request.user - old_password = request.data.get("old_password") - new_password = request.data.get("new_password") +--- - if not user.check_password(old_password): - return Response({"error": "Old password is incorrect"}, status=400) +Change Email Endpoint +-------------------- - user.set_password(new_password) - user.save() - return Response({"message": "Password changed successfully"}) +Allows a user to update their email address. + +Request +~~~~~~~ + +.. code-block:: http + + POST /api/change-email/ + +.. code-block:: json + + { + "new_email": "new@example.com" + } + +Response +~~~~~~~~ + +.. code-block:: json + + { + "message": "Email changed successfully" + } + +Behaviour +~~~~~~~~~ + +- Requires a valid new email +- Ensures email is unique across users + +Implementation +~~~~~~~~~~~~~~ -API view to allow an authenticated user to change their password. -The user must provide their current password to verify their identity before setting a new one. - :param request: The HTTP request containing ``old_password`` and ``new_password``. - :type request: Request - :return: Success message, or 400 if the old password is incorrect. - :rtype: Response - .. code-block:: python + class ChangeEmailView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated] + + def post(self, request): + + user = request.user + new_email = request.data.get("new_email") + + if not new_email: + return Response({"error": "New email is required"}, status=400) + + if User.objects.filter(email=new_email).exists(): + return Response({"error": "Email already in use"}, status=400) + + user.email = new_email + user.save() + + return Response({"message": "Email changed successfully"}) + +--- + +User Profile Endpoint +-------------------- - def post(self, request): - - user = request.user - new_email = request.data.get("new_email") +Retrieve and update the authenticated user's profile. + +Retrieve Profile (GET) +~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: http + + GET /api/user/profile/ + +Response: + +.. code-block:: json + + { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com", + "up_number": "up1234567" + } + +Update Profile (PATCH) +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: http + + PATCH /api/user/profile/ + +Example Request: - if not new_email: - return Response({"error": "New email is required"}, status=400) +.. code-block:: json - if User.objects.filter(email=new_email).exists(): - return Response({"error": "Email already in use"}, status=400) + { + "first_name": "Jane", + "email": "jane@example.com" + } - user.email = new_email - user.save() - return Response({"message": "Email changed successfully"}) +Response: + +.. code-block:: json + + { + "message": "Profile updated successfully", + "user": { ... } + } + +Behaviour +~~~~~~~~~ + +- Allows partial updates using ``PATCH`` +- Validates email uniqueness +- Updates only provided fields + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python -API view to allow an authenticated user to change their email address. -The new email must not already be in use by another account. - :param request: The HTTP request containing ``new_email``. - :type request: Request - :return: Success message, or 400 if the email is missing or already in use. - :rtype: Response - -.. code-block:: python class UserProfileView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated] - def get(self, request): - serializer = UserSerializer(request.user) - return Response(serializer.data, status=status.HTTP_200_OK) + def get(self, request): + serializer = UserSerializer(request.user) + return Response(serializer.data, status=status.HTTP_200_OK) - def patch(self, request): - user = request.user + def patch(self, request): - data = request.data + user = request.user + data = request.data - - if "first_name" in request.data: - user.first_name = request.data["first_name"] + if "first_name" in data: + user.first_name = data["first_name"] - if "last_name" in request.data: - user.last_name = request.data["last_name"] + if "last_name" in data: + user.last_name = data["last_name"] - if "email" in data: - if User.objects.filter(email=data["email"]).exclude(id=user.id).exists(): - return Response({"error": "Email already in use"}, status=400) - user.email = data["email"] + if "email" in data: + if User.objects.filter(email=data["email"]).exclude(id=user.id).exists(): + return Response({"error": "Email already in use"}, status=400) + user.email = data["email"] - if "up_number" in data: - user.up_number = data["up_number"] + if "up_number" in data: + user.up_number = data["up_number"] - user.save() + user.save() - return Response({ - "message": "Profile updated successfully", - "user": UserSerializer(user).data - }, status=status.HTTP_200_OK) + return Response({ + "message": "Profile updated successfully", + "user": UserSerializer(user).data + }, status=status.HTTP_200_OK) -Retrieve and update the authenticated user's profile. +--- + +Notification Preferences Endpoint +-------------------------------- + +Retrieve and update notification preferences for societies. + +Retrieve Preferences (GET) +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: http + + GET /api/notifications/ + +Response: + +.. code-block:: json + + [ + { + "society": "Music Society", + "notify_new_events": true + } + ] + +Update Preferences (POST) +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: http + + POST /api/notifications/ + +Example Request: + +.. code-block:: json + + { + "society_id": 1, + "event_notifications": true + } + +Response: + +.. code-block:: json + + { + "message": "Notification preferences updated", + "society": "Music Society", + "notify_new_events": true + } + +Behaviour +~~~~~~~~~ + +- Users can only update preferences for societies they belong to +- Uses ``update_or_create`` to simplify preference management + +Implementation +~~~~~~~~~~~~~~ .. code-block:: python + class NotificationView(APIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated] - def get(self, request): + def get(self, request): - user = request.user - preferences = NotificationPreference.objects.filter(user=user) + user = request.user + preferences = NotificationPreference.objects.filter(user=user) - data = [] - for pref in preferences: - data.append({ - "society": pref.society.name, - "notify_new_events": pref.notify_new_events, - }) + data = [] + for pref in preferences: + data.append({ + "society": pref.society.name, + "notify_new_events": pref.notify_new_events, + }) - return Response(data) + return Response(data) - def post(self, request): + def post(self, request): - user = request.user - society_id = request.data.get("society_id") + user = request.user + society_id = request.data.get("society_id") - notify_new_events = str(request.data.get("event_notifications")).lower() == "true" + notify_new_events = str( + request.data.get("event_notifications") + ).lower() == "true" - try: - society = Society.objects.get(id=society_id) - except Society.DoesNotExist: - return Response({"error": "Society not found"}, status=404) + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response({"error": "Society not found"}, status=404) - if not Membership.objects.filter(user=user, society=society).exists(): - return Response({"error": "Not a member of this society"}, status=403) + if not Membership.objects.filter(user=user, society=society).exists(): + return Response({"error": "Not a member of this society"}, status=403) - pref, created = NotificationPreference.objects.update_or_create( - user=user, - society=society, - defaults={ - "notify_new_events": notify_new_events - } - ) + pref, created = NotificationPreference.objects.update_or_create( + user=user, + society=society, + defaults={ + "notify_new_events": notify_new_events + } + ) - return Response({ - "message": "Notification preferences updated", - "society": society.name, - "notify_new_events": pref.notify_new_events - }) + return Response({ + "message": "Notification preferences updated", + "society": society.name, + "notify_new_events": pref.notify_new_events + }) -API view to retrieve or update the authenticated user's notification preferences. +--- -- ``GET``: Returns the user's notification preferences for each society they belong to. -- ``POST``: Updates the notification preference for a specific society. -Updates the authenticated user's notification preference for a society. +Data Flow +--------- +1. User accesses settings page +2. System retrieves current profile and preferences +3. User submits updates +4. Backend validates and applies changes +5. Updated data returned to frontend + +--- + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Incorrect old password + - Returns 400 + * - Email already in use + - Returns 400 + * - Missing required fields + - Returns 400 + * - Society not found (notifications) + - Returns 404 + * - User not a member of society + - Returns 403 + +--- + +Implementation Notes +------------------- + +- Uses secure password hashing via ``set_password`` +- Email uniqueness enforced at update +- Partial updates handled via ``PATCH`` +- Notification preferences stored per society +- ``update_or_create`` simplifies database operations + +--- + +Security Considerations +---------------------- + +- Password changes require current password verification +- Sensitive data is never exposed +- Access is restricted to authenticated users +- Membership validation prevents unauthorized preference changes + +--- + +Suggested Improvements +---------------------- + +- Add password strength validation (same as registration) +- Implement email verification on change +- Add profile picture support +- Add notification types (email, push, SMS) +- Log account changes for auditing \ No newline at end of file From 2370b98a360a290fbc8848feba74f86772342f5f Mon Sep 17 00:00:00 2001 From: stuti Date: Tue, 5 May 2026 22:22:54 +0100 Subject: [PATCH 103/103] Remove suggested improvements section from User Login API documentation --- docs/source/backend/User_Login.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/source/backend/User_Login.rst b/docs/source/backend/User_Login.rst index 704c92037..bf143badc 100644 --- a/docs/source/backend/User_Login.rst +++ b/docs/source/backend/User_Login.rst @@ -231,13 +231,3 @@ Security Considerations - No sensitive data (e.g. passwords) is returned in responses - Generic error messages prevent user enumeration attacks ---- - -Suggested Improvements ----------------------- - -- Add rate limiting to prevent brute-force attacks -- Implement account lockout after multiple failed attempts -- Support refresh tokens or JWT authentication -- Add multi-factor authentication (MFA) -- Log login attempts for auditing \ No newline at end of file