diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..3376de3d1 Binary files /dev/null and b/.DS_Store differ diff --git a/docs/source/api.rst b/docs/source/api.rst index ec94338a6..91ccb917b 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. \ No newline at end of file diff --git a/docs/source/backend/Admin_Analyticspage.rst b/docs/source/backend/Admin_Analyticspage.rst new file mode 100644 index 000000000..19659c481 --- /dev/null +++ b/docs/source/backend/Admin_Analyticspage.rst @@ -0,0 +1,248 @@ +Admin Analytics +=============== + +Overview +-------- + +The **Admin Analytics API** provides analytical insights for society administrators. +It aggregates membership trends, event engagement, and overall activity into a +single endpoint for dashboard visualisation. + +This endpoint is designed to support admin dashboards with time-series data and +summary statistics. + +Endpoint +-------- + +.. code-block:: http + + GET /api/my-analytics/ + +**Django Route** + +.. code-block:: python + + path("my-analytics/", AnalyticsView.as_view(), name="analytics") + +Authentication +-------------- + +- **Required**: Yes +- **Access Level**: Admin users only + +Authorization Rules +~~~~~~~~~~~~~~~~~~~ + +- User must have ``role = "admin"`` +- User must be associated with a society + +Error Responses +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Status Code + - Description + * - 403 + - User is not an admin + * - 404 + - No society found for admin + +Query Parameters +---------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 20 20 40 + + * - Parameter + - Type + - Default + - Description + * - period + - string + - week + - Time range for analytics aggregation + +Allowed Values +~~~~~~~~~~~~~~ + +- ``week`` → Last 7 days (daily breakdown) +- ``month`` → Last 30 days (daily breakdown) +- ``6months`` → Last 6 months (weekly breakdown) +- ``year`` → Last 12 months (monthly breakdown) + +Example Request +~~~~~~~~~~~~~~~ + +.. code-block:: http + + GET /api/my-analytics/?period=month + +Response Structure +------------------ + +.. code-block:: json + + { + "labels": ["Mon", "Tue", "Wed"], + "totals": [10, 15, 18], + "live_count": 120, + "total_events": 25, + "events_stats": [ + { "title": "Welcome Event", "attendee_count": 50 } + ], + "most_popular": { + "title": "Welcome Event", + "attendee_count": 50 + }, + "event_attendance": [ + { "title": "Welcome Event", "attendee_count": 50 } + ] + } + +Response Fields Explained +------------------------ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Description + * - ``labels`` + - Time intervals (e.g. days, weeks, months) + * - ``totals`` + - Membership count at each interval + * - ``live_count`` + - Current active members + * - ``total_events`` + - Total number of events created + * - ``events_stats`` + - Attendance count per event + * - ``most_popular`` + - Event with highest attendance (or null) + * - ``event_attendance`` + - Duplicate of ``events_stats`` (for frontend compatibility) + +Data Flow & Logic +----------------- + +Membership Growth +~~~~~~~~~~~~~~~~~ + +Membership totals are calculated using: + +- ``joined_at <= current_date`` +- ``left_at IS NULL OR left_at > current_date`` + +This ensures historical accuracy and correct handling of users who have left. + +Time Bucketing Strategy +~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 20 20 40 + + * - Period + - Interval + - Data Points + - Label Format + * - week + - Daily + - 7 + - Mon, Tue + * - month + - Daily + - 30 + - 01 Jan + * - 6months + - Weekly + - 26 + - Week 12 + * - year + - Monthly + - 12 + - Jan + +Event Analytics +~~~~~~~~~~~~~~~ + +.. code-block:: python + + Count("eventattendance", filter=Q(eventattendance__left_at__isnull=True)) + +Only active attendees are counted. + +Most Popular Event +~~~~~~~~~~~~~~~~~~ + +- Determined by highest attendee count +- Returns a single event +- Returns ``null`` if no events exist + +Implementation Notes +-------------------- + +Duplicate Query +~~~~~~~~~~~~~~~ + +.. code-block:: python + + society = Society.objects.get(admin=request.user) + +This appears twice and should be reused to avoid unnecessary database calls. + +Redundant Field +~~~~~~~~~~~~~~~ + +.. code-block:: json + + "event_attendance": list(events_stats) + +Duplicates ``events_stats`` and may be removed unless required by the frontend. + +Performance Considerations +~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Membership calculation runs one query per time interval +- Event annotations are executed multiple times +- Consider optimisation using aggregation or caching + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - No society exists + - Returns 404 + * - No events + - ``most_popular = null`` + * - No members + - ``totals`` contains zeros + * - Invalid period + - Returns 400 + +Use Cases +--------- + +- Admin dashboard visualisation +- Membership growth tracking +- Event engagement analysis +- Identifying popular events + +Suggested Improvements +---------------------- + +- Remove duplicate fields +- Optimise database queries +- Add optional date range filters +- Implement caching (e.g. Redis) +- Include event IDs in responses \ No newline at end of file diff --git a/docs/source/backend/Admin_Eventspage.rst b/docs/source/backend/Admin_Eventspage.rst new file mode 100644 index 000000000..8aa6f3bc3 --- /dev/null +++ b/docs/source/backend/Admin_Eventspage.rst @@ -0,0 +1,603 @@ +Admin Events Management +======================= + +Overview +-------- + +The **Admin Events Management API** מאפשר administrators to create, retrieve, +update, and delete events associated with their society. + +This module ensures that only authorised admins can manage events and that all +events are correctly linked to the society they oversee. + +Endpoints +--------- + +.. code-block:: http + + GET /api/societies//events/ + POST /api/societies//events/ + PUT /api/events//update/ + PATCH /api/events//update/ + DELETE /api/events//delete/ + +**Django Routes** + +.. code-block:: python + + path('events//update/', UpdateEventView.as_view(), name='update-event') + path('events//delete/', DeleteEventView.as_view(), name='delete-event') + +Authentication +-------------- + +- **Required**: Yes +- **Access Level**: Admin users only + +Authorization Rules +~~~~~~~~~~~~~~~~~~~ + +- User must have ``role = "admin"`` +- Admin must own the society to create events +- Admin can only update/delete events they created + +Error Responses +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Status Code + - Description + * - 403 + - User is not authorised (not an admin) + * - 404 + - Society or event not found + * - 400 + - Invalid request data + +Features +-------- + +- Create events linked to a society +- Retrieve all events for a society +- Update existing events +- Delete events +- Enforce ownership-based permissions +- Optional event capacity handling + +Request Handling +---------------- + +Create Event (POST) +~~~~~~~~~~~~~~~~~~~ + +Creates a new event for a society managed by the authenticated admin. + +Special Handling: +- ``capacity_limit`` values of ``0``, ``"0"``, or empty string are converted to ``null`` + +Example Request Body: + +.. code-block:: json + + { + "title": "Welcome Event", + "description": "Introduction for new members", + "date": "2026-05-10", + "location": "Main Hall", + "capacity_limit": 100 + } + +Response (201 Created): + +.. code-block:: json + + { + "id": 1, + "title": "Welcome Event", + "capacity_limit": 100 + } + +Retrieve Events (GET) +~~~~~~~~~~~~~~~~~~~~ + +Returns all events associated with a given society. + +Response: + +.. code-block:: json + + [ + { + "id": 1, + "title": "Welcome Event", + "date": "2026-05-10" + } + ] + +Update Event (PUT / PATCH) +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Updates an event created by the authenticated admin. + +- ``PUT`` replaces the entire resource +- ``PATCH`` updates partial fields + +Delete Event (DELETE) +~~~~~~~~~~~~~~~~~~~~ + +Deletes an event created by the authenticated admin. + +- Operation is irreversible +- Returns ``204 No Content`` on success + +Response Structure +------------------ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Description + * - ``id`` + - Unique identifier for the event + * - ``title`` + - Event name + * - ``description`` + - Event details + * - ``date`` + - Event date + * - ``location`` + - Event location + * - ``capacity_limit`` + - Maximum number of attendees (nullable) + +Implementation +-------------- + +Society Event View +~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + class SocietyEventView(APIView): + + permission_classes = [IsAuthenticated] + + def get(self, request, society_id): + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response({"error": "Society not found"}, status=404) + + events = Event.objects.filter(society=society) + serializer = EventSerializer(events, many=True) + return Response(serializer.data) + + def post(self, request, society_id): + + if request.user.role != "admin": + return Response({"error": "Admins only"}, status=403) + + try: + society = Society.objects.get(id=society_id, admin=request.user) + except Society.DoesNotExist: + return Response({"error": "Society not found or not admin"}, status=404) + + data = request.data.copy() + + if data.get("capacity_limit") in [0, "0", ""]: + data["capacity_limit"] = None + + serializer = EventSerializer(data=data) + + if serializer.is_valid(): + event = serializer.save( + society=society, + created_by=request.user + ) + + send_event_confirmation(request.user, event) + + return Response(serializer.data, status=201) + + return Response(serializer.errors, status=400) + +Description: + +- ``GET`` → Returns all events for a society +- ``POST`` → Creates a new event (admin only) +- Automatically links event to the admin’s society +- Sends confirmation after successful creation + +Update Event View +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + class UpdateEventView(generics.UpdateAPIView): + + permission_classes = [IsAuthenticated] + queryset = Event.objects.all() + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + return Event.objects.filter(created_by=self.request.user) + +Description: + +- Allows admins to update only their own events +- Filters queryset by ``created_by`` + +Delete Event View +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + class DeleteEventView(generics.DestroyAPIView): + + permission_classes = [IsAuthenticated] + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + return Event.objects.filter(created_by=self.request.user) + +Description: + +- Allows admins to delete only their own events +- Ensures ownership-based access control + +Data Flow +--------- + +1. Admin sends request (authenticated) +2. System verifies admin role +3. System validates society ownership +4. Serializer validates input data +5. Event is created/updated/deleted +6. Response returned to client + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Non-admin user attempts action + - Returns 403 + * - Society not found + - Returns 404 + * - Event not found + - Returns 404 + * - Invalid input data + - Returns 400 + * - Capacity set to 0 + - Converted to ``null`` + +Implementation Notes +------------------- + +- ``capacity_limit`` normalization improves data consistency +- Ownership filtering prevents unauthorized modifications +- Confirmation email/function triggered on event creation +- Querysets are scoped per user for security + +Suggested Improvements +---------------------- + +- Add pagination for event listings +- Include event IDs in all responses (if not already) +- Add soft delete instead of permanent deletion +- Introduce event status (draft, published, cancelled) +- Add validation for date/time conflicts +- Log admin actions for audit trackingAdmin Events Management +======================= + +Overview +-------- + +The **Admin Events Management API** מאפשר administrators to create, retrieve, +update, and delete events associated with their society. + +This module ensures that only authorised admins can manage events and that all +events are correctly linked to the society they oversee. + +Endpoints +--------- + +.. code-block:: http + + GET /api/societies//events/ + POST /api/societies//events/ + PUT /api/events//update/ + PATCH /api/events//update/ + DELETE /api/events//delete/ + +**Django Routes** + +.. code-block:: python + + path('events//update/', UpdateEventView.as_view(), name='update-event') + path('events//delete/', DeleteEventView.as_view(), name='delete-event') + +Authentication +-------------- + +- **Required**: Yes +- **Access Level**: Admin users only + +Authorization Rules +~~~~~~~~~~~~~~~~~~~ + +- User must have ``role = "admin"`` +- Admin must own the society to create events +- Admin can only update/delete events they created + +Error Responses +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Status Code + - Description + * - 403 + - User is not authorised (not an admin) + * - 404 + - Society or event not found + * - 400 + - Invalid request data + +Features +-------- + +- Create events linked to a society +- Retrieve all events for a society +- Update existing events +- Delete events +- Enforce ownership-based permissions +- Optional event capacity handling + +Request Handling +---------------- + +Create Event (POST) +~~~~~~~~~~~~~~~~~~~ + +Creates a new event for a society managed by the authenticated admin. + +Special Handling: +- ``capacity_limit`` values of ``0``, ``"0"``, or empty string are converted to ``null`` + +Example Request Body: + +.. code-block:: json + + { + "title": "Welcome Event", + "description": "Introduction for new members", + "date": "2026-05-10", + "location": "Main Hall", + "capacity_limit": 100 + } + +Response (201 Created): + +.. code-block:: json + + { + "id": 1, + "title": "Welcome Event", + "capacity_limit": 100 + } + +Retrieve Events (GET) +~~~~~~~~~~~~~~~~~~~~ + +Returns all events associated with a given society. + +Response: + +.. code-block:: json + + [ + { + "id": 1, + "title": "Welcome Event", + "date": "2026-05-10" + } + ] + +Update Event (PUT / PATCH) +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Updates an event created by the authenticated admin. + +- ``PUT`` replaces the entire resource +- ``PATCH`` updates partial fields + +Delete Event (DELETE) +~~~~~~~~~~~~~~~~~~~~ + +Deletes an event created by the authenticated admin. + +- Operation is irreversible +- Returns ``204 No Content`` on success + +Response Structure +------------------ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Description + * - ``id`` + - Unique identifier for the event + * - ``title`` + - Event name + * - ``description`` + - Event details + * - ``date`` + - Event date + * - ``location`` + - Event location + * - ``capacity_limit`` + - Maximum number of attendees (nullable) + +Implementation +-------------- + +Society Event View +~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + class SocietyEventView(APIView): + + permission_classes = [IsAuthenticated] + + def get(self, request, society_id): + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response({"error": "Society not found"}, status=404) + + events = Event.objects.filter(society=society) + serializer = EventSerializer(events, many=True) + return Response(serializer.data) + + def post(self, request, society_id): + + if request.user.role != "admin": + return Response({"error": "Admins only"}, status=403) + + try: + society = Society.objects.get(id=society_id, admin=request.user) + except Society.DoesNotExist: + return Response({"error": "Society not found or not admin"}, status=404) + + data = request.data.copy() + + if data.get("capacity_limit") in [0, "0", ""]: + data["capacity_limit"] = None + + serializer = EventSerializer(data=data) + + if serializer.is_valid(): + event = serializer.save( + society=society, + created_by=request.user + ) + + send_event_confirmation(request.user, event) + + return Response(serializer.data, status=201) + + return Response(serializer.errors, status=400) + +Description: + +- ``GET`` → Returns all events for a society +- ``POST`` → Creates a new event (admin only) +- Automatically links event to the admin’s society +- Sends confirmation after successful creation + +Update Event View +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + class UpdateEventView(generics.UpdateAPIView): + + permission_classes = [IsAuthenticated] + queryset = Event.objects.all() + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + return Event.objects.filter(created_by=self.request.user) + +Description: + +- Allows admins to update only their own events +- Filters queryset by ``created_by`` + +Delete Event View +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + class DeleteEventView(generics.DestroyAPIView): + + permission_classes = [IsAuthenticated] + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + return Event.objects.filter(created_by=self.request.user) + +Description: + +- Allows admins to delete only their own events +- Ensures ownership-based access control + +Data Flow +--------- + +1. Admin sends request (authenticated) +2. System verifies admin role +3. System validates society ownership +4. Serializer validates input data +5. Event is created/updated/deleted +6. Response returned to client + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Non-admin user attempts action + - Returns 403 + * - Society not found + - Returns 404 + * - Event not found + - Returns 404 + * - Invalid input data + - Returns 400 + * - Capacity set to 0 + - Converted to ``null`` + +Implementation Notes +------------------- + +- ``capacity_limit`` normalization improves data consistency +- Ownership filtering prevents unauthorized modifications +- Confirmation email/function triggered on event creation +- Querysets are scoped per user for security + +Suggested Improvements +---------------------- + +- Add pagination for event listings +- Include event IDs in all responses (if not already) +- Add soft delete instead of permanent deletion +- Introduce event status (draft, published, cancelled) +- Add validation for date/time conflicts +- Log admin actions for audit tracking \ No newline at end of file diff --git a/docs/source/backend/Event_Detailspage.rst b/docs/source/backend/Event_Detailspage.rst new file mode 100644 index 000000000..e24f8448b --- /dev/null +++ b/docs/source/backend/Event_Detailspage.rst @@ -0,0 +1,162 @@ +Event Details +============= + +Overview +-------- + +The **Event Details API** retrieves comprehensive information about a specific event, +including its metadata and associated attendance data. + +This endpoint is primarily used to display detailed event pages within the application. + +Endpoint +-------- + +.. code-block:: http + + GET /api/events// + +**Django Route** + +.. code-block:: python + + path('events//', EventDetailView.as_view(), name='event-detail') + +Authentication +-------------- + +- **Required**: Yes +- **Access Level**: Any authenticated user + +Authorization Rules +~~~~~~~~~~~~~~~~~~~ + +- User must be authenticated +- No admin privileges required +- Access is not restricted by event ownership + +Error Responses +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Status Code + - Description + * - 404 + - Event not found + * - 401 + - Authentication credentials missing or invalid + +Response Structure +------------------ + +.. code-block:: json + + { + "id": 1, + "title": "Welcome Event", + "description": "Introduction for new members", + "date": "2026-05-10", + "location": "Main Hall", + "capacity_limit": 100, + "society": 3, + "created_by": 5 + } + +Response Fields Explained +------------------------ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Description + * - ``id`` + - Unique identifier of the event + * - ``title`` + - Name of the event + * - ``description`` + - Detailed event information + * - ``date`` + - Scheduled date of the event + * - ``location`` + - Event location + * - ``capacity_limit`` + - Maximum number of attendees (nullable) + * - ``society`` + - ID of the associated society + * - ``created_by`` + - ID of the user who created the event + +Attendance Data +--------------- + +If included in the serializer, attendance-related fields may also be returned: + +- Total number of attendees +- List of attendees (optional) +- Attendance status for the current user + +(Implementation depends on ``EventSerializer`` configuration.) + +Implementation +-------------- + +.. code-block:: python + + class EventDetailView(generics.RetrieveAPIView): + + permission_classes = [IsAuthenticated] + queryset = Event.objects.all() + serializer_class = EventSerializer + lookup_field = 'id' + +Description: + +- Retrieves a single event using the ``id`` field +- Uses Django REST Framework's ``RetrieveAPIView`` +- Returns serialized event data +- Requires authentication for access + +Data Flow +--------- + +1. Client sends authenticated request with ``event_id`` +2. System queries database for matching event +3. Serializer formats event data +4. Response returned to client + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Event does not exist + - Returns 404 + * - User not authenticated + - Returns 401 + * - Event has no attendees + - Attendance fields return empty or zero values + +Implementation Notes +------------------- + +- Uses DRF generic view for simplicity and consistency +- Relies on ``EventSerializer`` for response structure +- Can be extended to include nested relationships (e.g. society details, attendees) + +Suggested Improvements +---------------------- + +- Include nested society details instead of only ID +- Add attendee count directly in response +- Include user-specific attendance status (joined/not joined) +- Add caching for frequently accessed events +- Support public/private event visibility rules \ No newline at end of file diff --git a/docs/source/backend/User_Homepage.rst b/docs/source/backend/User_Homepage.rst new file mode 100644 index 000000000..4d0970052 --- /dev/null +++ b/docs/source/backend/User_Homepage.rst @@ -0,0 +1,250 @@ +User Homepage +============= + +Overview +-------- + +The **User Homepage API** provides core data required for the user dashboard. +It enables users to view recent events and search for societies. + +This endpoint supports dynamic content rendering for the homepage, including +event previews and searchable society listings. + +Endpoints +--------- + +.. code-block:: http + + GET /api/events/all/ + GET /api/search/?q= + +**Django Routes** + +.. code-block:: python + + path('events/all/', AllEventsView.as_view(), name='all-events') + path("search/", SocietyListSearchView.as_view(), name="society-search") + +Authentication +-------------- + +- **Required**: Yes +- **Access Level**: Any authenticated user + +Features +-------- + +- View the most recent events +- Search societies by name +- View society summaries (name, category, description, member count) + +--- + +All Events Endpoint +------------------ + +Retrieves the most recently created events. + +Request +~~~~~~~ + +.. code-block:: http + + GET /api/events/all/ + +Response +~~~~~~~~ + +.. code-block:: json + + [ + { + "id": 1, + "title": "Welcome Event", + "date": "2026-05-10", + "society": 3 + } + ] + +Behaviour +~~~~~~~~~ + +- Returns the **5 most recent events** +- Events are ordered by descending ID (latest first) +- Includes associated society data via ``select_related`` + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python + + class AllEventsView(APIView): + + permission_classes = [IsAuthenticated] + + def get(self, request): + + events = Event.objects.select_related("society").order_by('-id')[:5] + serializer = EventSerializer(events, many=True) + return Response(serializer.data) + +--- + +Society Search Endpoint +---------------------- + +Retrieves a list of active societies, optionally filtered by a search query. + +Request +~~~~~~~ + +.. code-block:: http + + GET /api/search/?q=music + +Query Parameters +~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 30 50 + + * - Parameter + - Type + - Description + * - q + - string + - Optional search term used to filter societies by name + +Response +~~~~~~~~ + +.. code-block:: json + + [ + { + "id": 1, + "name": "Music Society", + "category": "Cultural", + "description": "A society for music lovers", + "member_count": 120 + } + ] + +Response Fields Explained +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Description + * - ``id`` + - Unique identifier of the society + * - ``name`` + - Name of the society + * - ``category`` + - Society category (e.g. Academic, Cultural) + * - ``description`` + - Brief description of the society + * - ``member_count`` + - Number of active members in the society + +Behaviour +~~~~~~~~~ + +- Returns only societies where ``is_active = True`` +- If ``q`` is provided: + - Filters societies using case-insensitive name matching +- Results are: + - Annotated with active member count + - Ordered alphabetically by name + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python + + class SocietyListSearchView(APIView): + + permission_classes = [IsAuthenticated] + + def get(self, request): + + query = request.query_params.get("q", "").strip() + + societies = Society.objects.filter(is_active=True) + + if query: + societies = societies.filter(name__icontains=query) + + societies = societies.annotate( + active_member_count=Count( + 'membership', + filter=Q(membership__left_at__isnull=True) + ) + ).order_by('name') + + data = [{ + "id": s.id, + "name": s.name, + "category": s.category, + "description": s.description, + "member_count": s.active_member_count, + } for s in societies] + + return Response(data) + +--- + +Data Flow +--------- + +1. User opens homepage +2. Frontend requests latest events +3. Frontend sends search queries as user types +4. Backend filters and returns matching societies +5. Results displayed dynamically on UI + +--- + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - No events exist + - Returns empty list + * - No societies match search + - Returns empty list + * - Missing query parameter + - Returns all active societies + * - User not authenticated + - Returns 401 + +--- + +Implementation Notes +------------------- + +- ``select_related("society")`` improves query performance +- Membership count uses conditional aggregation +- Search is case-insensitive for better usability +- Results are lightweight for fast frontend rendering + +--- + +Suggested Improvements +---------------------- + +- Add pagination for large society lists +- Implement debounce/throttling on frontend (already done in your UI) +- Add category-based filtering +- Include society images/logos in response +- Add trending or recommended societies +- Cache frequent search queries for performance \ No newline at end of file diff --git a/docs/source/backend/User_Login.rst b/docs/source/backend/User_Login.rst new file mode 100644 index 000000000..bf143badc --- /dev/null +++ b/docs/source/backend/User_Login.rst @@ -0,0 +1,233 @@ +User Login +========== + +Overview +-------- + +The **User Login API** authenticates a user using either their email address +or university (UP) number and returns an authentication token. + +This token is required for accessing protected endpoints within the system. + +Endpoint +-------- + +.. code-block:: http + + POST /api/login/ + +**Django Route** + +.. code-block:: python + + path("login/", LoginView.as_view(), name="login") + +Authentication +-------------- + +- **Required**: No (public endpoint) +- **Access Level**: All users + +Request Body +------------ + +.. code-block:: json + + { + "email": "user@example.com", + "password": "password123" + } + +OR + +.. code-block:: json + + { + "up_number": "up1234567", + "password": "password123" + } + +Request Rules +~~~~~~~~~~~~~ + +- Either ``email`` or ``up_number`` must be provided +- ``password`` is required +- ``up_number`` is case-insensitive +- If ``up_number`` does not start with ``"up"``, it will be automatically prefixed + +--- + +Response +-------- + +Success Response (200 OK) +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: json + + { + "token": "abc123xyz", + "role": "admin", + "email": "user@example.com", + "up_number": "up1234567", + "society_id": 1, + "society_name": "Music Society" + } + +Response Fields Explained +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Description + * - ``token`` + - Authentication token used for subsequent requests + * - ``role`` + - User role (e.g. admin, student) + * - ``email`` + - User email address + * - ``up_number`` + - University identifier + * - ``society_id`` + - ID of the society (only for admins, otherwise null) + * - ``society_name`` + - Name of the society (only for admins, otherwise null) + +Error Responses +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Status Code + - Description + * - 400 + - Missing required fields (e.g. password or login identifier) + * - 401 + - Invalid credentials + +Example Error: + +.. code-block:: json + + { + "error": "Invalid credentials" + } + +--- + +Implementation +-------------- + +.. code-block:: python + + class LoginView(APIView): + + def post(self, request): + email = request.data.get("email") + up_number = request.data.get("up_number") + password = request.data.get("password") + + if not password: + return Response({"error": "Password required"}, status=400) + + try: + if email: + user = User.objects.get(email__iexact=email) + elif up_number: + up_number = up_number.lower() + if not up_number.startswith("up"): + up_number = f"up{up_number}" + user = User.objects.get(up_number__iexact=up_number) + else: + return Response({"error": "Email or UP number required"}, status=400) + + if user.check_password(password): + token, _ = Token.objects.get_or_create(user=user) + + society_id = None + society_name = None + + if user.role == "admin": + try: + society = Society.objects.get(admin=user) + society_id = society.id + society_name = society.name + except Society.DoesNotExist: + pass + + return Response({ + "token": token.key, + "role": user.role, + "email": user.email, + "up_number": user.up_number, + "society_id": society_id, + "society_name": society_name + }) + + except User.DoesNotExist: + pass + + return Response({"error": "Invalid credentials"}, status=401) + +Description +----------- + +- Authenticates user credentials against stored data +- Supports login via: + - Email (case-insensitive) + - University number (UP number) +- Automatically normalises UP numbers +- Generates or retrieves an authentication token +- Returns additional admin-specific data if applicable + +Data Flow +--------- + +1. User submits login credentials +2. System validates input fields +3. User is retrieved via email or UP number +4. Password is verified +5. Token is generated/retrieved +6. Response returned with user details + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Missing password + - Returns 400 + * - Missing email and UP number + - Returns 400 + * - Invalid credentials + - Returns 401 + * - Admin without society + - Returns null for society fields + +Implementation Notes +------------------- + +- Uses ``TokenAuthentication`` for session management +- Case-insensitive lookups improve usability +- Gracefully handles missing admin society +- Avoids user enumeration by returning generic error messages + +--- + +Security Considerations +---------------------- + +- Passwords are securely hashed and verified using Django's authentication system +- Token-based authentication is used for subsequent requests +- No sensitive data (e.g. passwords) is returned in responses +- Generic error messages prevent user enumeration attacks + diff --git a/docs/source/backend/User_MyEventspage.rst b/docs/source/backend/User_MyEventspage.rst new file mode 100644 index 000000000..08ef8de93 --- /dev/null +++ b/docs/source/backend/User_MyEventspage.rst @@ -0,0 +1,300 @@ +User My Events Page +=================== + +Overview +-------- + +The **User My Events API** allows authenticated users to view, join, and leave events. + +The endpoint adapts its behaviour based on the user role: + +- **Admins** → View all events belonging to their society +- **Regular users** → View events from societies they have joined + +Endpoints +--------- + +.. code-block:: http + + GET /api/events/my/ + POST /api/events//join/ + POST /api/events//leave/ + +**Django Routes** + +.. code-block:: python + + path('events/my/', MyEventsView.as_view(), name='my-events') + path('events//join/', JoinEventView.as_view(), name='join-event') + path('events//leave/', LeaveEventView.as_view(), name='leave-event') + +Authentication +-------------- + +- **Required**: Yes +- **Access Level**: Any authenticated user + +Features +-------- + +- View relevant events based on user role +- Join events +- Leave events +- Prevent joining past events +- Track event attendance dynamically + +--- + +My Events Endpoint +----------------- + +Retrieves events relevant to the authenticated user. + +Request +~~~~~~~ + +.. code-block:: http + + GET /api/events/my/ + +Response +~~~~~~~~ + +.. code-block:: json + + [ + { + "id": 1, + "title": "Welcome Event", + "date": "2026-05-10", + "society": 3 + } + ] + +Behaviour +~~~~~~~~~ + +- If user is **admin**: + - Returns all events for their society +- If user is **regular user**: + - Returns events from societies they are members of +- Uses ``distinct()`` to avoid duplicate results + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python + + class MyEventsView(APIView): + + permission_classes = [IsAuthenticated] + + 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) + +--- + +Join Event Endpoint +------------------ + +Allows a user to join an event. + +Request +~~~~~~~ + +.. code-block:: http + + POST /api/events//join/ + +Response +~~~~~~~~ + +.. code-block:: json + + { + "message": "Joined event", + "attendee_count": 45 + } + +Behaviour +~~~~~~~~~ + +- Prevents joining events that have already started +- Creates a new attendance record if one does not exist +- If the user previously left: + - Reactivates attendance +- If already attending: + - Returns an error +- Returns updated attendee count + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python + + class JoinEventView(APIView): + + permission_classes = [IsAuthenticated] + + def post(self, request, event_id): + + try: + event = Event.objects.get(id=event_id) + except Event.DoesNotExist: + return Response({"error": "Event not found"}, status=404) + + if event.start_time < timezone.now(): + return Response( + {"error": "Event has already passed"}, + status=400 + ) + + attendance, created = EventAttendance.objects.get_or_create( + user=request.user, + event=event, + defaults={"left_at": None} + ) + + if not created: + if attendance.left_at is None: + return Response({"message": "Already attending"}, status=400) + else: + attendance.left_at = None + attendance.joined_at = timezone.now() + attendance.save() + + attendee_count = EventAttendance.objects.filter( + event=event, + left_at__isnull=True + ).count() + + return Response({ + "message": "Joined event", + "attendee_count": attendee_count + }) + +--- + +Leave Event Endpoint +------------------- + +Allows a user to leave an event. + +Request +~~~~~~~ + +.. code-block:: http + + POST /api/events//leave/ + +Response +~~~~~~~~ + +.. code-block:: json + + { + "message": "Left event successfully" + } + +Behaviour +~~~~~~~~~ + +- Only allows leaving if the user is currently attending +- Marks attendance as inactive by setting ``left_at`` +- Updates attendee count internally + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python + + class LeaveEventView(APIView): + + permission_classes = [IsAuthenticated] + + 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"}) + +--- + +Data Flow +--------- + +1. User requests their events +2. System determines user role +3. Relevant events are retrieved +4. User joins or leaves events +5. Attendance records are updated +6. Updated data returned to frontend + +--- + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Event does not exist + - Returns 404 + * - User tries to join past event + - Returns 400 + * - User already attending + - Returns 400 + * - User tries to leave without joining + - Returns 400 + * - No events available + - Returns empty list + +--- + +Implementation Notes +------------------- + +- Uses ``get_or_create`` to simplify attendance logic +- Soft delete pattern used via ``left_at`` field +- ``distinct()`` prevents duplicate events in queries +- Time-based validation ensures logical consistency + +--- + +Suggested Improvements +---------------------- + +- Add capacity limit validation before joining +- Return updated attendee count on leave +- Add waitlist functionality for full events +- Include user attendance status in event responses +- Add pagination for large event lists +- Send notifications when joining/leaving events \ No newline at end of file diff --git a/docs/source/backend/User_MySocietypage.rst b/docs/source/backend/User_MySocietypage.rst new file mode 100644 index 000000000..fe5ece97b --- /dev/null +++ b/docs/source/backend/User_MySocietypage.rst @@ -0,0 +1,306 @@ +User My Societies Page +====================== + +Overview +-------- + +The **User My Societies API** allows authenticated users to view and manage +their society memberships. + +Users can join new societies, leave existing ones, and retrieve a list of +societies they are currently part of. + +Endpoints +--------- + +.. code-block:: http + + GET /api/my-societies/ + POST /api/society//join/ + POST /api/society//leave/ + +**Django Routes** + +.. code-block:: python + + path("my-societies/", MySocietiesView.as_view(), name="my-societies") + path("society//join/", JoinSocietyView.as_view(), name="join-society") + path("society//leave/", LeaveSocietyView.as_view(), name="leave-society") + +Authentication +-------------- + +- **Required**: Yes +- **Access Level**: Any authenticated user + +Features +-------- + +- View currently joined societies +- Join societies +- Leave societies +- Rejoin previously left societies +- Soft-delete memberships using timestamps + +--- + +My Societies Endpoint +-------------------- + +Retrieves all societies the user is currently a member of. + +Request +~~~~~~~ + +.. code-block:: http + + GET /api/my-societies/ + +Response +~~~~~~~~ + +.. code-block:: json + + [ + { + "id": 1, + "name": "Music Society", + "category": "Cultural", + "description": "A society for music lovers" + } + ] + +Behaviour +~~~~~~~~~ + +- Returns only active memberships (``left_at IS NULL``) +- Uses ``select_related`` for efficient querying +- Returns simplified society data + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python + + class MySocietiesView(APIView): + + permission_classes = [IsAuthenticated] + + def get(self, request): + + memberships = Membership.objects.filter( + user=request.user, + left_at__isnull=True + ).select_related("society") + + societies = [] + for m in memberships: + s = m.society + societies.append({ + "id": s.id, + "name": s.name, + "category": s.category, + "description": s.description, + }) + + return Response(societies) + +--- + +Join Society Endpoint +-------------------- + +Allows a user to join a society. + +Request +~~~~~~~ + +.. code-block:: http + + POST /api/society//join/ + +Response +~~~~~~~~ + +.. code-block:: json + + { + "message": "Joined successfully" + } + +Behaviour +~~~~~~~~~ + +- Creates a new membership if none exists +- If already a member: + - Returns ``Already joined`` +- If previously left: + - Reactivates membership + - Updates ``joined_at`` timestamp + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python + + class JoinSocietyView(APIView): + + permission_classes = [IsAuthenticated] + + def post(self, request, society_id): + + user = request.user + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response( + {"error": "Society not found"}, + status=status.HTTP_404_NOT_FOUND + ) + + membership, created = Membership.objects.get_or_create( + user=user, + society=society + ) + + if created: + return Response( + {"message": "Joined successfully"}, + status=status.HTTP_201_CREATED + ) + + if membership.left_at is None: + return Response({"message": "Already joined"}, status=200) + + membership.left_at = None + membership.joined_at = timezone.now() + membership.save() + + return Response({"message": "Rejoined successfully"}, status=200) + +--- + +Leave Society Endpoint +--------------------- + +Allows a user to leave a society. + +Request +~~~~~~~ + +.. code-block:: http + + POST /api/society//leave/ + +Response +~~~~~~~~ + +.. code-block:: json + + { + "message": "Successfully left society" + } + +Behaviour +~~~~~~~~~ + +- Only allows leaving if the user is an active member +- Uses soft delete by setting ``left_at`` +- Membership record is preserved for history + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python + + class LeaveSocietyView(APIView): + + permission_classes = [IsAuthenticated] + + def post(self, request, society_id): + + user = request.user + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response( + {"error": "Society not found"}, + status=status.HTTP_404_NOT_FOUND + ) + + try: + membership = Membership.objects.get( + user=user, + society=society, + left_at__isnull=True + ) + except Membership.DoesNotExist: + return Response( + {"error": "You are not an active member"}, + status=status.HTTP_400_BAD_REQUEST + ) + + membership.left_at = timezone.now() + membership.save() + + return Response( + {"message": "Successfully left society"}, + status=status.HTTP_200_OK + ) + +--- + +Data Flow +--------- + +1. User requests their societies +2. System retrieves active memberships +3. User joins or leaves societies +4. Membership records are created or updated +5. Response returned to frontend + +--- + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Society does not exist + - Returns 404 + * - User already joined + - Returns message without duplication + * - User rejoins after leaving + - Membership reactivated + * - User leaves without being a member + - Returns 400 + * - No societies joined + - Returns empty list + +--- + +Implementation Notes +------------------- + +- Uses ``get_or_create`` for efficient membership handling +- Soft delete pattern implemented via ``left_at`` +- ``select_related`` improves database performance +- Prevents duplicate memberships + +--- + +Suggested Improvements +---------------------- + +- Add pagination for large society lists +- Include member count in response +- Add role within society (e.g. member, admin) +- Add notifications when joining/leaving +- Prevent joining inactive societies +- Add audit logging for membership changes \ No newline at end of file diff --git a/docs/source/backend/User_Registration.rst b/docs/source/backend/User_Registration.rst new file mode 100644 index 000000000..ef5d28dc7 --- /dev/null +++ b/docs/source/backend/User_Registration.rst @@ -0,0 +1,259 @@ +User Registration +================= + +Overview +-------- + +The **User Registration API** allows new users to create an account in the system. + +It validates user input, enforces password strength requirements, and ensures +that both email and university (UP) number are unique. + +Endpoint +-------- + +.. code-block:: http + + POST /api/user/register/ + +**Django Route** + +.. code-block:: python + + path("user/register/", RegisterView.as_view(), name="register") + +Authentication +-------------- + +- **Required**: No +- **Access Level**: Public + +--- + +Request Body +------------ + +.. code-block:: json + + { + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com", + "up_number": "up1234567", + "password": "SecurePass1!", + "confirm_password": "SecurePass1!" + } + +Request Rules +~~~~~~~~~~~~~ + +- All fields are required +- ``password`` and ``confirm_password`` must match +- ``up_number`` is case-insensitive +- If ``up_number`` does not start with ``"up"``, it will be automatically prefixed + +--- + +Validation Rules +---------------- + +Password Requirements +~~~~~~~~~~~~~~~~~~~~~ + +- Minimum 8 characters +- At least one uppercase letter +- At least one number +- At least one special character + +Uniqueness Constraints +~~~~~~~~~~~~~~~~~~~~~~ + +- Email must be unique +- UP number must be unique + +--- + +Response +-------- + +Success Response (201 Created) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: json + + { + "message": "User registered successfully" + } + +Error Responses +~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Status Code + - Description + * - 400 + - Missing fields or validation failure + +Example Errors: + +.. code-block:: json + + { "error": "All fields are required" } + +.. code-block:: json + + { "error": "Passwords do not match" } + +.. code-block:: json + + { "error": "Password must contain at least one uppercase letter" } + +.. code-block:: json + + { "error": "Email already exists" } + +--- + +Implementation +-------------- + +.. code-block:: python + + class RegisterView(APIView): + + def post(self, request): + + first_name = request.data.get("first_name") + last_name = request.data.get("last_name") + email = request.data.get("email") + up_number = request.data.get("up_number") + password = request.data.get("password") + confirm_password = request.data.get("confirm_password") + + if not all([first_name, last_name, email, up_number, password, confirm_password]): + return Response( + {"error": "All fields are required"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if password != confirm_password: + return Response( + {"error": "Passwords do not match"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if len(password) < 8: + return Response( + {"error": "Password must be at least 8 characters long"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not re.search(r"[A-Z]", password): + return Response( + {"error": "Password must contain at least one uppercase letter"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not re.search(r"[0-9]", password): + return Response( + {"error": "Password must contain at least one number"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password): + return Response( + {"error": "Password must contain at least one special character"}, + status=status.HTTP_400_BAD_REQUEST + ) + + up_number = up_number.lower() + if not up_number.startswith("up"): + up_number = f"up{up_number}" + + if User.objects.filter(email=email).exists(): + return Response({"error": "Email already exists"}, status=400) + + if User.objects.filter(up_number=up_number).exists(): + return Response({"error": "UP number already exists"}, status=400) + + user = User.objects.create_user( + first_name=first_name, + last_name=last_name, + email=email, + up_number=up_number, + password=password + ) + + return Response( + {"message": "User registered successfully"}, + status=status.HTTP_201_CREATED + ) + +--- + +Data Flow +--------- + +1. User submits registration form +2. System validates all required fields +3. Password rules are enforced +4. UP number is normalised +5. System checks for duplicate email and UP number +6. User account is created +7. Success response returned + +--- + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Missing required fields + - Returns 400 + * - Passwords do not match + - Returns 400 + * - Weak password + - Returns 400 + * - Email already exists + - Returns 400 + * - UP number already exists + - Returns 400 + +--- + +Implementation Notes +------------------- + +- Uses Django's ``create_user`` for secure password hashing +- Input validation handled manually in the view +- UP number normalisation ensures consistent storage +- Prevents duplicate user records + +--- + +Security Considerations +---------------------- + +- Passwords are never stored in plain text +- Strong password policy enforced +- Duplicate checks prevent account conflicts +- No sensitive data is returned in responses + +--- + +Suggested Improvements +---------------------- + +- Add email verification step +- Implement CAPTCHA to prevent bot registrations +- Add rate limiting to prevent abuse +- Return authentication token upon registration +- Move validation logic to serializers for cleaner design \ No newline at end of file diff --git a/docs/source/backend/User_Settingspage.rst b/docs/source/backend/User_Settingspage.rst new file mode 100644 index 000000000..8ddd86613 --- /dev/null +++ b/docs/source/backend/User_Settingspage.rst @@ -0,0 +1,439 @@ +User Settings Page +================== + +Overview +-------- + +The **User Settings API** allows authenticated users to manage their account +settings, including password, email, profile information, and notification +preferences. + +This module provides secure endpoints for updating sensitive user data and +customising user-specific settings. + +Endpoints +--------- + +.. code-block:: http + + POST /api/change-password/ + POST /api/change-email/ + GET /api/user/profile/ + PATCH /api/user/profile/ + GET /api/notifications/ + POST /api/notifications/ + +**Django Routes** + +.. code-block:: python + + path('change-password/', ChangePasswordView.as_view(), name='change-password') + path('change-email/', ChangeEmailView.as_view(), name='change-email') + path('user/profile/', UserProfileView.as_view(), name='user-profile') + path('notifications/', NotificationView.as_view(), name='notifications') + +Authentication +-------------- + +- **Required**: Yes +- **Access Level**: Any authenticated user + +Features +-------- + +- Change password securely +- Update email address +- View and update profile information +- Manage notification preferences per society + +--- + +Change Password Endpoint +----------------------- + +Allows a user to update their password. + +Request +~~~~~~~ + +.. code-block:: http + + POST /api/change-password/ + +.. code-block:: json + + { + "old_password": "OldPass123!", + "new_password": "NewPass456!" + } + +Response +~~~~~~~~ + +.. code-block:: json + + { + "message": "Password changed successfully" + } + +Behaviour +~~~~~~~~~ + +- Verifies the current password before updating +- Updates password using Django's secure hashing + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python + + class ChangePasswordView(APIView): + + permission_classes = [IsAuthenticated] + + 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"}) + +--- + +Change Email Endpoint +-------------------- + +Allows a user to update their email address. + +Request +~~~~~~~ + +.. code-block:: http + + POST /api/change-email/ + +.. code-block:: json + + { + "new_email": "new@example.com" + } + +Response +~~~~~~~~ + +.. code-block:: json + + { + "message": "Email changed successfully" + } + +Behaviour +~~~~~~~~~ + +- Requires a valid new email +- Ensures email is unique across users + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python + + 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"}) + +--- + +User Profile Endpoint +-------------------- + +Retrieve and update the authenticated user's profile. + +Retrieve Profile (GET) +~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: http + + GET /api/user/profile/ + +Response: + +.. code-block:: json + + { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com", + "up_number": "up1234567" + } + +Update Profile (PATCH) +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: http + + PATCH /api/user/profile/ + +Example Request: + +.. code-block:: json + + { + "first_name": "Jane", + "email": "jane@example.com" + } + +Response: + +.. code-block:: json + + { + "message": "Profile updated successfully", + "user": { ... } + } + +Behaviour +~~~~~~~~~ + +- Allows partial updates using ``PATCH`` +- Validates email uniqueness +- Updates only provided fields + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python + + class UserProfileView(APIView): + + permission_classes = [IsAuthenticated] + + def get(self, request): + serializer = UserSerializer(request.user) + return Response(serializer.data, status=status.HTTP_200_OK) + + def patch(self, request): + + user = request.user + data = request.data + + if "first_name" in data: + user.first_name = data["first_name"] + + if "last_name" in data: + user.last_name = data["last_name"] + + if "email" in data: + if User.objects.filter(email=data["email"]).exclude(id=user.id).exists(): + return Response({"error": "Email already in use"}, status=400) + user.email = data["email"] + + if "up_number" in data: + user.up_number = data["up_number"] + + user.save() + + return Response({ + "message": "Profile updated successfully", + "user": UserSerializer(user).data + }, status=status.HTTP_200_OK) + +--- + +Notification Preferences Endpoint +-------------------------------- + +Retrieve and update notification preferences for societies. + +Retrieve Preferences (GET) +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: http + + GET /api/notifications/ + +Response: + +.. code-block:: json + + [ + { + "society": "Music Society", + "notify_new_events": true + } + ] + +Update Preferences (POST) +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: http + + POST /api/notifications/ + +Example Request: + +.. code-block:: json + + { + "society_id": 1, + "event_notifications": true + } + +Response: + +.. code-block:: json + + { + "message": "Notification preferences updated", + "society": "Music Society", + "notify_new_events": true + } + +Behaviour +~~~~~~~~~ + +- Users can only update preferences for societies they belong to +- Uses ``update_or_create`` to simplify preference management + +Implementation +~~~~~~~~~~~~~~ + +.. code-block:: python + + class NotificationView(APIView): + + permission_classes = [IsAuthenticated] + + 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, + }) + + return Response(data) + + def post(self, request): + + 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 + }) + +--- + +Data Flow +--------- + +1. User accesses settings page +2. System retrieves current profile and preferences +3. User submits updates +4. Backend validates and applies changes +5. Updated data returned to frontend + +--- + +Edge Cases +---------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Scenario + - Behaviour + * - Incorrect old password + - Returns 400 + * - Email already in use + - Returns 400 + * - Missing required fields + - Returns 400 + * - Society not found (notifications) + - Returns 404 + * - User not a member of society + - Returns 403 + +--- + +Implementation Notes +------------------- + +- Uses secure password hashing via ``set_password`` +- Email uniqueness enforced at update +- Partial updates handled via ``PATCH`` +- Notification preferences stored per society +- ``update_or_create`` simplifies database operations + +--- + +Security Considerations +---------------------- + +- Password changes require current password verification +- Sensitive data is never exposed +- Access is restricted to authenticated users +- Membership validation prevents unauthorized preference changes + +--- + +Suggested Improvements +---------------------- + +- Add password strength validation (same as registration) +- Implement email verification on change +- Add profile picture support +- Add notification types (email, push, SMS) +- Log account changes for auditing \ No newline at end of file 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. diff --git a/docs/source/conf.py b/docs/source/conf.py index 6e9e8c087..60b556fa2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,35 +1,38 @@ -# Configuration file for the Sphinx documentation builder. +import os +import sys -# -- Project information +# -- Path setup -------------------------------------------------------------- -project = 'Lumache' -copyright = '2021, Graziella' -author = 'Graziella' +# Add project root to Python path (so Sphinx can find modules if needed) +sys.path.insert(0, os.path.abspath('..')) -release = '0.1' -version = '0.1.0' +# -- Project information ----------------------------------------------------- -# -- General configuration +project = 'UniSoc Documentation' +author = 'UniSoc Team' +release = '1.0' -extensions = [ - 'sphinx.ext.duration', - 'sphinx.ext.doctest', - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.intersphinx', -] +# -- General configuration --------------------------------------------------- -intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), - 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), +html_theme = "sphinx_rtd_theme" + +html_theme_options = { + "style_nav_header_background": "#2980B9", + "collapse_navigation": False, + "navigation_depth": 3, } -intersphinx_disabled_domains = ['std'] templates_path = ['_templates'] +exclude_patterns = [] + +# Disable autosummary auto-generation to avoid import crashes +autosummary_generate = False + +# -- HTML output ------------------------------------------------------------- -# -- Options for HTML output +html_theme = 'alabaster' # simple and safe (works on ReadTheDocs) -html_theme = 'sphinx_rtd_theme' +# If you want nicer UI later, you can switch to: +# html_theme = 'sphinx_rtd_theme' seeing if this syncs to github -# -- Options for EPUB output -epub_show_urls = 'footnote' +html_static_path = ['_static'] \ No newline at end of file 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 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/generated/models.rst b/docs/source/generated/models.rst new file mode 100644 index 000000000..1ab70bd36 --- /dev/null +++ b/docs/source/generated/models.rst @@ -0,0 +1,6 @@ +models +====== + +.. automodule:: 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..86e598841 --- /dev/null +++ b/docs/source/generated/serializer.rst @@ -0,0 +1,6 @@ +serializer +========== + +.. automodule:: serializer + + \ No newline at end of file diff --git a/docs/source/generated/views.rst b/docs/source/generated/views.rst new file mode 100644 index 000000000..02562af78 --- /dev/null +++ b/docs/source/generated/views.rst @@ -0,0 +1,13 @@ +views +===== + +.. automodule:: views + + + .. rubric:: Functions + + .. autosummary:: + + send_event_confirmation + send_event_reminders + \ No newline at end of file diff --git a/docs/source/implementation.rst b/docs/source/implementation.rst new file mode 100644 index 000000000..f8c319d6d --- /dev/null +++ b/docs/source/implementation.rst @@ -0,0 +1,37 @@ +Implementation +============== + +Technologies Used +---------------- +- Python (backend) +- SQL Database +- Dart/Flutter (frontend) +- GitHub for version control + +Example Code +------------ + +.. code-block:: python + + 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"}) + diff --git a/docs/source/index.rst b/docs/source/index.rst index 03d09a55d..9113e0cf6 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,22 +1,58 @@ -Welcome to Lumache's documentation! -=================================== +UniSoc Documentation +=================== -**Lumache** (/lu'make/) is a Python library for cooks and food lovers -that creates recipes mixing random ingredients. -It pulls data from the `Open Food Facts database `_ -and offers a *simple* and *intuitive* API. +UniSoc is a full-stack university society management system designed to improve +student engagement and simplify the management of societies and events. -Check out the :doc:`usage` section for further information, including -how to :ref:`installation` the project. +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. -.. note:: +Project Architecture +-------------------- - This project is under active development. +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 --------- +======== + +.. toctree:: + :maxdepth: 2 + :caption: Documentation + + scope + requirements + implementation + setup + + +Backend +======= + +.. toctree:: + :maxdepth: 1 + :caption: Backend Pages + + backend/Admin_Analyticspage + backend/Admin_Eventspage + backend/Event_Detailspage + backend/User_Homepage + backend/User_Login + backend/User_MyEventspage + backend/User_MySocietypage + backend/User_Registration + backend/User_Settingspage + + +API +=== .. toctree:: + :maxdepth: 1 - usage - api + api \ No newline at end of file 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. + 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 diff --git a/docs/source/setup.rst b/docs/source/setup.rst new file mode 100644 index 000000000..d482e4dc4 --- /dev/null +++ b/docs/source/setup.rst @@ -0,0 +1,136 @@ +Setup Instructions +================== + +This project consists of a Flutter frontend and a Django REST backend. + +Requirements +------------ + +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/Unisoc + cd Unisoc + +----------------------------------- +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 + +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 makemigrations + 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: + +.. 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/usage.rst b/docs/source/usage.rst index 924afcf6c..fbc7ed0f8 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -1,34 +1,33 @@ -Usage -===== +Usage Guide +=========== -.. _installation: +This section describes how to interact with the system. -Installation ------------- +User Workflow +------------- -To use Lumache, first install it using pip: +1. Register an account +2. Log into the system +3. Browse available societies +4. Join societies of interest +5. View and attend events -.. code-block:: console +Admin Workflow +-------------- - (.venv) $ pip install lumache +1. Log into admin account +2. Create or manage societies +3. Create and manage events +4. Monitor attendance and engagement -Creating recipes ----------------- +API Interaction +--------------- -To retrieve a list of random ingredients, -you can use the ``lumache.get_random_ingredients()`` function: +The frontend communicates with the backend via REST APIs. -.. 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 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"]