From a34021bb5e01b21b14589fa5dcfb10c72ace7d3a Mon Sep 17 00:00:00 2001 From: MM674294 Date: Thu, 23 Apr 2026 21:08:41 +0100 Subject: [PATCH 01/95] 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 02/95] 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 03/95] 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 04/95] 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 05/95] 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 06/95] 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 07/95] 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 08/95] 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 09/95] 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 10/95] 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 11/95] 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 12/95] 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 13/95] 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 14/95] 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 15/95] 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 16/95] 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 17/95] 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 18/95] 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 19/95] 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 20/95] 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 21/95] 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 22/95] 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 23/95] 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 24/95] 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 25/95] 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 26/95] 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 27/95] 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 28/95] 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 29/95] 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 30/95] 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 31/95] 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 32/95] 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 33/95] 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 34/95] 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 35/95] 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 36/95] 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 37/95] 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 38/95] 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 39/95] 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 40/95] 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 41/95] 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 42/95] 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 43/95] 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 44/95] 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 45/95] 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 46/95] 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 47/95] 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 48/95] 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 49/95] 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 50/95] 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 51/95] 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 52/95] 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 53/95] 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 54/95] 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 55/95] 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 56/95] 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: Wed, 29 Apr 2026 22:09:04 +0100 Subject: [PATCH 57/95] merging setup.rst --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index d3afdb457b3b6631c2394463e8d20e5545b9a3ec..4aa5ac97a7e56a7e13be52d601f8e2613c7797fc 100644 GIT binary patch delta 121 zcmZoMXffEJ#u&R(nSp_Ug+Y%YogtH Date: Wed, 29 Apr 2026 22:10:30 +0100 Subject: [PATCH 58/95] Refactor documentation and remove unused model and serializer code - Cleaned up generated documentation files for authentication models, serializers, and views by removing unnecessary sections. - Deleted models.py and serializer.py files as they contained outdated code. - Updated lumache.py to change ingredient list from "basil" to "parsley". --- docs/source/api.rst | 32 +- docs/source/conf.py | 51 +- .../generated/authentication.models.rst | 22 +- .../generated/authentication.serializer.rst | 16 +- .../backend.authentication.models.rst | 22 +- .../backend.authentication.serializer.rst | 16 +- docs/source/generated/models.rst | 7 +- docs/source/generated/serializer.rst | 7 +- docs/source/generated/views.rst | 14 +- docs/source/models.py | 296 ----- docs/source/serializer.py | 87 -- docs/source/views.py | 1094 ----------------- lumache.py | 2 +- 13 files changed, 39 insertions(+), 1627 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 62ab252a3..ec94338a6 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,33 +1,7 @@ -API Documentation -================= +API +=== .. autosummary:: :toctree: generated - backend.authentication.views - backend.authentication.models - backend.authentication.serializer - -Views ------ - -.. 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 + lumache diff --git a/docs/source/conf.py b/docs/source/conf.py index de4d0bd42..6e9e8c087 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,30 +1,35 @@ -import os -import sys +# Configuration file for the Sphinx documentation builder. -BASE_DIR = r"C:\Users\stuti\OneDrive\SETAP\SETAP CW\TERM 2 CW\UNIsoc" +# -- Project information -sys.path.insert(0, os.path.join(BASE_DIR, "backend")) # ✅ IMPORTANT +project = 'Lumache' +copyright = '2021, Graziella' +author = 'Graziella' -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") +release = '0.1' +version = '0.1.0' + +# -- General configuration -import django -django.setup() -# ---- Sphinx extensions (IMPORTANT) ---- extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", + 'sphinx.ext.duration', + 'sphinx.ext.doctest', + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.intersphinx', ] -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 +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' diff --git a/docs/source/generated/authentication.models.rst b/docs/source/generated/authentication.models.rst index ecb1029de..5f282702b 100644 --- a/docs/source/generated/authentication.models.rst +++ b/docs/source/generated/authentication.models.rst @@ -1,21 +1 @@ -authentication.models -===================== - -.. automodule:: authentication.models - - - .. rubric:: Classes - - .. autosummary:: - - AuditLog - CustomUserManager - Event - EventAttendance - EventRSVP - Membership - Message - NotificationPreference - Society - User - \ No newline at end of file + \ No newline at end of file diff --git a/docs/source/generated/authentication.serializer.rst b/docs/source/generated/authentication.serializer.rst index f6566d90d..5f282702b 100644 --- a/docs/source/generated/authentication.serializer.rst +++ b/docs/source/generated/authentication.serializer.rst @@ -1,15 +1 @@ -authentication.serializer -========================= - -.. automodule:: authentication.serializer - - - .. rubric:: Classes - - .. autosummary:: - - EventSerializer - NotificationPreferenceSerializer - SocietySerializer - UserSerializer - \ No newline at end of file + \ No newline at end of file diff --git a/docs/source/generated/backend.authentication.models.rst b/docs/source/generated/backend.authentication.models.rst index 082882eea..5f282702b 100644 --- a/docs/source/generated/backend.authentication.models.rst +++ b/docs/source/generated/backend.authentication.models.rst @@ -1,21 +1 @@ -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 + \ No newline at end of file diff --git a/docs/source/generated/backend.authentication.serializer.rst b/docs/source/generated/backend.authentication.serializer.rst index 7856f4f67..5f282702b 100644 --- a/docs/source/generated/backend.authentication.serializer.rst +++ b/docs/source/generated/backend.authentication.serializer.rst @@ -1,15 +1 @@ -backend.authentication.serializer -================================= - -.. automodule:: backend.authentication.serializer - - - .. rubric:: Classes - - .. autosummary:: - - EventSerializer - NotificationPreferenceSerializer - SocietySerializer - UserSerializer - \ No newline at end of file + \ No newline at end of file diff --git a/docs/source/generated/models.rst b/docs/source/generated/models.rst index 1ab70bd36..5f282702b 100644 --- a/docs/source/generated/models.rst +++ b/docs/source/generated/models.rst @@ -1,6 +1 @@ -models -====== - -.. automodule:: models - - \ No newline at end of file + \ No newline at end of file diff --git a/docs/source/generated/serializer.rst b/docs/source/generated/serializer.rst index 86e598841..5f282702b 100644 --- a/docs/source/generated/serializer.rst +++ b/docs/source/generated/serializer.rst @@ -1,6 +1 @@ -serializer -========== - -.. automodule:: serializer - - \ No newline at end of file + \ No newline at end of file diff --git a/docs/source/generated/views.rst b/docs/source/generated/views.rst index 02562af78..5f282702b 100644 --- a/docs/source/generated/views.rst +++ b/docs/source/generated/views.rst @@ -1,13 +1 @@ -views -===== - -.. automodule:: views - - - .. rubric:: Functions - - .. autosummary:: - - send_event_confirmation - send_event_reminders - \ No newline at end of file + \ No newline at end of file diff --git a/docs/source/models.py b/docs/source/models.py index 876ba406d..e69de29bb 100644 --- a/docs/source/models.py +++ b/docs/source/models.py @@ -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 index f826f8503..e69de29bb 100644 --- a/docs/source/serializer.py +++ b/docs/source/serializer.py @@ -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/views.py b/docs/source/views.py index c5f402ab6..e69de29bb 100644 --- a/docs/source/views.py +++ b/docs/source/views.py @@ -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 diff --git a/lumache.py b/lumache.py index a48ca9187..3ea7ce95c 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", "basil"] + return ["shells", "gorgonzola", "parsley"] From d29167b631ea296f1957fd7b28767c718c521a5b Mon Sep 17 00:00:00 2001 From: stuti Date: Wed, 29 Apr 2026 22:11:36 +0100 Subject: [PATCH 59/95] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 4aa5ac97a7e56a7e13be52d601f8e2613c7797fc..8744094316b3a949845bb38429bfe5f69497383a 100644 GIT binary patch delta 106 zcmZoMXffEJ#u&SI9RmXc3xgg*IzuKyNp8N2OHxjL5>Sj|b+}YT=*8oXsPZXz Date: Thu, 30 Apr 2026 18:43:38 +0100 Subject: [PATCH 60/95] 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 61/95] 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 62/95] 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 18:51:03 +0100 Subject: [PATCH 63/95] cd --- .DS_Store | Bin 6148 -> 6148 bytes docs/source/components.rst | 27 +++++---- docs/source/implementation.rst | 51 ++++++++++------- docs/source/requirements.rst | 68 ++++++++-------------- docs/source/scope.rst | 29 ++++++---- docs/source/setup.rst | 102 ++++----------------------------- 6 files changed, 95 insertions(+), 182 deletions(-) diff --git a/.DS_Store b/.DS_Store index 71fac7dac58999e4c7d2d26e7e5983e2ec9cc75d..7baf521e72b299f76839aee47f7237d59836240e 100644 GIT binary patch delta 168 zcmZoMXffEJ##sODEdv7s3xgg*IzuKyNp8N2OHxjL5>Skz-FfG~XBUqgd3xgg*IzuKyNp8N2OHxjL5>Sj|^3O^7VHb}(qROY>l`qIJ z3{K9^Edc6aU|=%= 3.x) - Dart SDK (>= 3.9.0) @@ -15,7 +13,7 @@ Frontend: Backend: - Python (>= 3.10) - PostgreSQL -- Redis (for Celery background tasks) +- Redis Tools: - Git @@ -23,113 +21,33 @@ Tools: Installation ------------ -Clone the repository: - .. code-block:: bash git clone https://github.com/your-repo cd your-repo ------------------------------------ -Backend Setup (Django REST Framework) ------------------------------------ - -1. Create virtual environment: +Backend Setup +------------- .. code-block:: bash python -m venv venv - source venv/bin/activate # Linux/Mac - venv\Scripts\activate # Windows - -2. Install dependencies: - -.. code-block:: bash - + source venv/bin/activate pip install -r requirements.txt - -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 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: +Frontend Setup +-------------- .. code-block:: bash + cd frontend 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 +- Ensure PostgreSQL and Redis are running +- Update environment variables before deployment \ No newline at end of file From c86ba1013048b44a006034bc08af7a478aa280e1 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 19:29:16 +0100 Subject: [PATCH 64/95] 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 65/95] 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 66/95] 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 67/95] 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 68/95] 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 69/95] 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 70/95] 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 71/95] 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 72/95] 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 73/95] 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 74/95] 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 75/95] 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 76/95] 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 77/95] 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 78/95] 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 79/95] 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 05a59e836d3dd1a2636ac4dd1cf6a14128f30d02 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 21:08:01 +0100 Subject: [PATCH 80/95] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index c15cf366d0955614ec9586fec06fc393824c933b..8d21767d4607a750671f83b7b4628103025316b2 100644 GIT binary patch delta 106 zcmZoMXffEJ#u)p}nt_3Vg+Y%YogtHvluNRLyqROYh Date: Thu, 30 Apr 2026 21:10:20 +0100 Subject: [PATCH 81/95] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 8d21767d4607a750671f83b7b4628103025316b2..96aff371c547208254c5eed28408ea99f924d04c 100644 GIT binary patch delta 121 zcmZoMXffEJ${5GY_?m%%frUYjA)O(Up(Hoo#U&{xKM5$tk-kVG@cYH%j;Qh}aCx?e pyTs7t3o;CYlk;;6fO;4hgbOA&Gg>f7f1kXUQ4UCLzRRdC1^}K3Auj*` delta 121 zcmZoMXffEJ${5G=&6S16I`Y^ef(SoVx>*T$RazJYHT}E{=02sI<=>Px# From 6a5c2853e4a11c0440b958b7a79079952199023e Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 21:14:05 +0100 Subject: [PATCH 82/95] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 96aff371c547208254c5eed28408ea99f924d04c..ed983eb427b5271cfdf0c8bfbc3908c972eb840f 100644 GIT binary patch delta 106 zcmZoMXffEJ#u&>c%D}+D!l1{H&XCDalAG`1l9ZF51Qg?NwVoRB{o-*)RQVLV@&y@& V!O8i#1wcIv48jXGH#0Vh0RWD&8&&`S delta 106 zcmZoMXffEJ#u&@^nt_3Vg+Y%YogtHNFfi From be1d9024877c8712cb741fff53fcefc1c555bb37 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 21:14:23 +0100 Subject: [PATCH 83/95] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index ed983eb427b5271cfdf0c8bfbc3908c972eb840f..c938212e170c4ea3abe74fa1fbcae13b0c3cc954 100644 GIT binary patch delta 106 zcmZoMXffEJ#u&?fgn@y9g+Y%YogtHc%D}+D!l1{H&XCDalAG`1l9ZF51Qg?NwVoRB{o-*)RQVLV@&y@& V!O8i#1wcIv48jXGH#0Vh0RWD&8&&`S From 9d2d212db5d20a1bd5614f3ea232a89d84435940 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 21:14:44 +0100 Subject: [PATCH 84/95] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index c938212e170c4ea3abe74fa1fbcae13b0c3cc954..6c3f7ab12d957889ab3cf3e1e3c26b4e5736fead 100644 GIT binary patch delta 106 zcmZoMXffEJ#u&@@ih+TFg+Y%YogtH7w9 Date: Thu, 30 Apr 2026 21:16:31 +0100 Subject: [PATCH 85/95] syncing --- docs/source/api.rst | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 22bd6d324..ec94338a6 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -4,24 +4,4 @@ API .. autosummary:: :toctree: generated - backend.authentication.views - backend.authentication.models - backend.authentication.serializer - -Views ------ - -.. automodule:: backend.authentication.views - :members: - -Models ------- - -.. automodule:: backend.authentication.models - :members: - -Serializers ------------ - -.. automodule:: backend.authentication.serializer - :members: + lumache From a289cf959beffc0eb5828f84f9c766793867427b Mon Sep 17 00:00:00 2001 From: MM674294 Date: Thu, 30 Apr 2026 21:17:27 +0100 Subject: [PATCH 86/95] remove empty generated documentation files --- docs/source/generated/authentication.models.rst | 1 - docs/source/generated/authentication.serializer.rst | 1 - docs/source/generated/backend.authentication.models.rst | 1 - docs/source/generated/backend.authentication.serializer.rst | 1 - docs/source/generated/models.rst | 1 - docs/source/generated/serializer.rst | 1 - docs/source/generated/views.rst | 1 - 7 files changed, 7 deletions(-) delete mode 100644 docs/source/generated/authentication.models.rst delete mode 100644 docs/source/generated/authentication.serializer.rst delete mode 100644 docs/source/generated/backend.authentication.models.rst delete mode 100644 docs/source/generated/backend.authentication.serializer.rst delete mode 100644 docs/source/generated/models.rst delete mode 100644 docs/source/generated/serializer.rst delete mode 100644 docs/source/generated/views.rst diff --git a/docs/source/generated/authentication.models.rst b/docs/source/generated/authentication.models.rst deleted file mode 100644 index 5f282702b..000000000 --- a/docs/source/generated/authentication.models.rst +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/source/generated/authentication.serializer.rst b/docs/source/generated/authentication.serializer.rst deleted file mode 100644 index 5f282702b..000000000 --- a/docs/source/generated/authentication.serializer.rst +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/source/generated/backend.authentication.models.rst b/docs/source/generated/backend.authentication.models.rst deleted file mode 100644 index 5f282702b..000000000 --- a/docs/source/generated/backend.authentication.models.rst +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/source/generated/backend.authentication.serializer.rst b/docs/source/generated/backend.authentication.serializer.rst deleted file mode 100644 index 5f282702b..000000000 --- a/docs/source/generated/backend.authentication.serializer.rst +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/source/generated/models.rst b/docs/source/generated/models.rst deleted file mode 100644 index 5f282702b..000000000 --- a/docs/source/generated/models.rst +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/source/generated/serializer.rst b/docs/source/generated/serializer.rst deleted file mode 100644 index 5f282702b..000000000 --- a/docs/source/generated/serializer.rst +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/source/generated/views.rst b/docs/source/generated/views.rst deleted file mode 100644 index 5f282702b..000000000 --- a/docs/source/generated/views.rst +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 1705f11a5ce392c7cd28c9a4dd6919991f1609d9 Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 21:18:32 +0100 Subject: [PATCH 87/95] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 6c3f7ab12d957889ab3cf3e1e3c26b4e5736fead..0c0b155c7c406db92de1bd9d9508d80df8a6cb84 100644 GIT binary patch delta 168 zcmZoMXffEJ##k?9%)r3F!l1{H&XCDalAG`1l9ZF51Qg@g`f#?xkBi40QRP$c$`@o9 z1}Ep|76A1yFo Date: Thu, 30 Apr 2026 21:20:02 +0100 Subject: [PATCH 88/95] remove unused documentation files --- docs/source/conf.py | 35 ----------------------------------- docs/source/models.py | 0 docs/source/serializer.py | 0 docs/source/views.py | 0 4 files changed, 35 deletions(-) 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/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 6e9e8c087..000000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,35 +0,0 @@ -# Configuration file for the Sphinx documentation builder. - -# -- Project information - -project = 'Lumache' -copyright = '2021, Graziella' -author = 'Graziella' - -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', -] - -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' diff --git a/docs/source/models.py b/docs/source/models.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/source/serializer.py b/docs/source/serializer.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/source/views.py b/docs/source/views.py deleted file mode 100644 index e69de29bb..000000000 From c107a68b5d5dc1577bc54754d73dd4ae4edbb45f Mon Sep 17 00:00:00 2001 From: MM674294 Date: Thu, 30 Apr 2026 21:28:14 +0100 Subject: [PATCH 89/95] update API documentation structure and content --- docs/source/api.rst | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index ec94338a6..969545fee 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,7 +1,15 @@ -API -=== +API Documentation +================= -.. autosummary:: - :toctree: generated +This section documents the backend API endpoints. - lumache +Modules +------- + +- Authentication (Login, Register) +- Events Management +- Society Management +- Notifications +- Analytics + +For full endpoint details, see the Backend Documentation section. From c866ad6e76c3e5b162a736780eb376b9dc7c9f2c Mon Sep 17 00:00:00 2001 From: stuti Date: Thu, 30 Apr 2026 21:34:32 +0100 Subject: [PATCH 90/95] cd --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index 0c0b155c7c406db92de1bd9d9508d80df8a6cb84..f687feb303fc2b1cc2d2da0908de2a231932c90f 100644 GIT binary patch delta 169 zcmZoMXffEJ##pbL$iTqB!l1{H&XCDalAG`1l9ZF51Qg@==k!nG*Tv(GsPZXzLPkE~6}C Tz-C4!HnxccY@69R{_+C=8Cfkz delta 169 zcmZoMXffEJ##k?9%)r3F!l1{H&XCDalAG`1l9ZF51Qg@g`f#?xkBi40QRP$c$`@o9 z1}Ep|76A1yFo Date: Thu, 30 Apr 2026 21:35:22 +0100 Subject: [PATCH 91/95] 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 92/95] 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 93/95] 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 94/95] 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 95/95] 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