diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..7dfa3d782 Binary files /dev/null and b/.DS_Store differ 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. diff --git a/docs/source/backend/Admin_Analyticspage.rst b/docs/source/backend/Admin_Analyticspage.rst new file mode 100644 index 000000000..952a7bcbd --- /dev/null +++ b/docs/source/backend/Admin_Analyticspage.rst @@ -0,0 +1,240 @@ +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 + diff --git a/docs/source/backend/Admin_Eventspage.rst b/docs/source/backend/Admin_Eventspage.rst new file mode 100644 index 000000000..a0b268879 --- /dev/null +++ b/docs/source/backend/Admin_Eventspage.rst @@ -0,0 +1,594 @@ +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 + diff --git a/docs/source/backend/Event_Detailspage.rst b/docs/source/backend/Event_Detailspage.rst new file mode 100644 index 000000000..91cfe962d --- /dev/null +++ b/docs/source/backend/Event_Detailspage.rst @@ -0,0 +1,154 @@ +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) + diff --git a/docs/source/backend/User_Homepage.rst b/docs/source/backend/User_Homepage.rst new file mode 100644 index 000000000..6c65b3532 --- /dev/null +++ b/docs/source/backend/User_Homepage.rst @@ -0,0 +1,240 @@ +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 + +--- \ 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..32e5e3793 --- /dev/null +++ b/docs/source/backend/User_MyEventspage.rst @@ -0,0 +1,290 @@ +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 + +--- diff --git a/docs/source/backend/User_MySocietypage.rst b/docs/source/backend/User_MySocietypage.rst new file mode 100644 index 000000000..c44073c08 --- /dev/null +++ b/docs/source/backend/User_MySocietypage.rst @@ -0,0 +1,297 @@ +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 + +--- + diff --git a/docs/source/backend/User_Registration.rst b/docs/source/backend/User_Registration.rst new file mode 100644 index 000000000..0081b24ff --- /dev/null +++ b/docs/source/backend/User_Registration.rst @@ -0,0 +1,250 @@ +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 + +--- diff --git a/docs/source/backend/User_Settingspage.rst b/docs/source/backend/User_Settingspage.rst new file mode 100644 index 000000000..ae0c5de6f --- /dev/null +++ b/docs/source/backend/User_Settingspage.rst @@ -0,0 +1,429 @@ +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 + diff --git a/docs/source/components.rst b/docs/source/components.rst new file mode 100644 index 000000000..2f43cce1a --- /dev/null +++ b/docs/source/components.rst @@ -0,0 +1,22 @@ +Project Components +================== + +The system is divided into several core services: + +1. Identity Management Service + Handles user registration, login, and authentication. + +2. Society Management Service + Manages creation, updating, and deletion of societies. + +3. Membership Service + Handles user participation in societies. + +4. Event Service + Manages event creation, display, and participation. + +5. Attendance Service + Tracks user attendance at events. + +6. Notification Service + Sends updates and manages user notification preferences. \ No newline at end of file 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/frontend/user_homepage.rst b/docs/source/frontend/user_homepage.rst new file mode 100644 index 000000000..a87671f35 --- /dev/null +++ b/docs/source/frontend/user_homepage.rst @@ -0,0 +1,196 @@ +User Homepage +============= + +Overview +-------- + +The user homepage (``home_page.dart``) is the main screen for student users after login. +It provides a central hub for discovering societies, searching for content, viewing +featured societies, and browsing upcoming events from societies the user has joined. + +The page is composed of two main widgets: + +- ``HomePage`` — a stateless scaffold that hosts the page layout. +- ``HomeHeader`` — a stateful widget that handles all data fetching, state management, and UI rendering. + +Components +---------- + +Search Bar +~~~~~~~~~~ + +A debounced ``TextField`` that queries the ``/search?q=`` endpoint as the user types. +Results appear in a dropdown below the search bar with a 300ms debounce to prevent +excessive API requests. A loading spinner is shown in the suffix of the search field +while results are being fetched. + +- **Endpoint**: ``/api/search?q=`` +- **Debounce**: 300ms +- **Loading indicator**: Shown while request is in flight + +Search Dropdown (``_buildSearchDropdown``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Displays search results beneath the search bar. Each result shows an icon and name. +Tapping a result navigates to ``UserSocietyPage`` for that society and clears the +dropdown. + +.. code-block:: dart + + onTap: () { + setState(() => _searchResults = []); + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => UserSocietyPage( + societyId: item['id'], + societyName: item['name'] ?? '', + description: item['description'] ?? '', + ), + ), + ); + } + +Featured Societies Carousel +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A horizontal ``PageView`` displaying the top 3 societies auto-advancing every 5 seconds. +Each card is a ``_SocietyLogoCard`` widget that navigates to ``UserSocietyPage`` when tapped. + +- **Auto-advance interval**: 5 seconds +- **Societies shown**: Top 3 from the full society list +- **Navigation**: Tapping opens ``UserSocietyPage`` + +All Societies (A–Z) List +~~~~~~~~~~~~~~~~~~~~~~~~ + +A scrollable list of all societies inside a styled container. Supports: + +- **Sort by**: A-Z, Z-A, Most Members, Least Members +- **Filter by**: All, Academic, Cultural, Sports, Religious, Extra-curricular + +Tapping any society in the list navigates to ``UserSocietyPage``. + +Upcoming Events Carousel +~~~~~~~~~~~~~~~~~~~~~~~~ + +A horizontal ``ListView`` showing upcoming events from societies the user has joined. +Each event card is wrapped in a ``GestureDetector`` — tapping navigates to the +``UserSocietyPage`` of the society that owns the event. + +Event data is fetched via ``ApiService.getEventsForJoinedSocieties()``, which loops +through the user's joined societies and tags each event with ``society_id`` and +``society_name`` at fetch time so navigation is possible without a backend change. + +.. code-block:: dart + + GestureDetector( + onTap: () { + if (societyId == null) return; + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => UserSocietyPage( + societyId: societyId, + societyName: event['society_name'] ?? '', + description: '', + ), + ), + ); + }, + child: _EventCard(...), + ) + +Data Flow +--------- + +On initialisation, ``_loadData()`` is called which fetches: + +1. All societies via ``ApiService.getSocieties()`` → ``/api/societies/`` +2. Upcoming events via ``ApiService.getEventsForJoinedSocieties()`` → loops ``/api/societies//events/`` per joined society + +Both are fetched in parallel and stored in local state. A loading spinner is shown +until both calls complete. + +State Variables +--------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Variable + - Purpose + * - ``_societies`` + - Full list of all societies + * - ``_filteredSocieties`` + - Filtered/sorted subset used in the A-Z list + * - ``_topSocieties`` + - Top 3 societies shown in the featured carousel + * - ``_events`` + - Upcoming events from joined societies + * - ``_searchResults`` + - Live search results shown in the dropdown + * - ``_loading`` + - Controls loading spinner visibility + * - ``_isSearching`` + - Controls search field loading indicator + * - ``selectedCategory`` + - Currently selected filter category + * - ``sortBy`` + - Currently selected sort option + +Navigation +---------- + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Trigger + - Destination + * - Tap society in search dropdown + - ``UserSocietyPage`` + * - Tap society in featured carousel + - ``UserSocietyPage`` + * - Tap society in A-Z list + - ``UserSocietyPage`` + * - Tap event card in upcoming events + - ``UserSocietyPage`` (of owning society) + +API Endpoints Used +------------------ + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Endpoint + - Purpose + * - ``GET /api/societies/`` + - Fetch all societies for the A-Z list and featured carousel + * - ``GET /api/search?q=`` + - Live search for societies + * - ``GET /api/my-societies/`` + - Fetch societies the user has joined + * - ``GET /api/societies//events/`` + - Fetch events per joined society + +Helper Widgets +-------------- + +``_SocietyLogoCard`` +~~~~~~~~~~~~~~~~~~~~ + +A small card widget displaying a society icon and name. Used in the featured +societies carousel. Accepts an optional ``UserSocietyPage`` widget to navigate +to on tap. + +``_EventCard`` +~~~~~~~~~~~~~~ + +A styled card displaying event title, date, and location. Used inside the +upcoming events carousel. Navigation is handled by the parent ``GestureDetector`` +in the ``itemBuilder``. + + diff --git a/docs/source/frontend/user_loginpage.rst b/docs/source/frontend/user_loginpage.rst new file mode 100644 index 000000000..0ff8b4cd2 --- /dev/null +++ b/docs/source/frontend/user_loginpage.rst @@ -0,0 +1,335 @@ +User LoginPage +======================================== + +1. Overview +----------- + +``LoginScreenUser`` is a ``StatefulWidget`` responsible for authenticating regular +(non-admin) users of the UniSoc application. It presents a form to collect the +user's UP number and password, submits credentials to the backend REST API, and +navigates to the home page upon successful authentication. + +**Source file:** ``lib/screens/auth/login_screen_user.dart`` + +---- + +2. Imports +---------- + +.. list-table:: + :widths: 35 65 + :header-rows: 1 + + * - Package / File + - Purpose + * - ``dart:convert`` + - JSON encoding/decoding (``jsonEncode``, ``jsonDecode``) + * - ``dart:async`` + - ``TimeoutException`` handling + * - ``package:flutter/material.dart`` + - Core Flutter widgets and Material design + * - ``package:flutter/services.dart`` + - ``FilteringTextInputFormatter``, ``LengthLimitingTextInputFormatter`` + * - ``package:http/http.dart`` + - HTTP client for API requests + * - ``screens/user/user_home_page.dart`` + - Destination screen after successful login + * - ``login_screen.admin.dart`` + - Admin login screen (navigation target) + * - ``forgotten_password_screen.dart`` + - Forgotten password screen (navigation target) + * - ``signup_user_page.dart`` + - User signup screen (navigation target) + * - ``services/api_services.dart`` + - ``ApiService`` class — base URL and auth token storage + +---- + +3. Class Structure +------------------ + +.. list-table:: + :widths: 30 35 35 + :header-rows: 1 + + * - Class + - Type + - Description + * - ``LoginScreenUser`` + - ``StatefulWidget`` + - Root widget; instantiated with ``const`` constructor + * - ``_LoginScreenUserState`` + - ``State`` + - Holds controllers, loading flag, and all business logic + +---- + +4. State Variables +------------------ + +.. list-table:: + :widths: 30 30 40 + :header-rows: 1 + + * - Variable + - Type + - Purpose + * - ``upnumberController`` + - ``TextEditingController`` + - Captures the UP number field input + * - ``passwordController`` + - ``TextEditingController`` + - Captures the password field input + * - ``isLoading`` + - ``bool`` + - Toggles between the login button and ``CircularProgressIndicator`` + +---- + +5. Methods +---------- + +5.1 ``_showError(String message)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Displays a transient ``SnackBar`` at the bottom of the screen with the given +error message. Guards against calls after widget disposal using a ``mounted`` +check. + +**Signature:** + +.. code-block:: dart + + void _showError(String message) + +**Behaviour:** + +- Checks ``mounted`` before accessing ``context`` to avoid calling ``setState`` + on a disposed widget. +- Calls ``ScaffoldMessenger.of(context).showSnackBar`` with a ``SnackBar`` + containing the message text. + +---- + +5.2 ``loginUser()`` +~~~~~~~~~~~~~~~~~~~~ + +Core async method that validates input, sends a ``POST`` request to the login +endpoint, handles the response, and navigates to the home screen on success. + +**Signature:** + +.. code-block:: dart + + Future loginUser() async + +**Step-by-step flow:** + +.. list-table:: + :widths: 8 92 + :header-rows: 1 + + * - Step + - Description + * - 1 + - Read and trim ``upnumberController.text``; read ``passwordController.text``. + * - 2 + - Validate: if either field is empty, call ``_showError`` and return early. + * - 3 + - Set ``isLoading = true`` via ``setState`` to show the loading indicator. + * - 4 + - Build the target URI: ``"${ApiService.baseUrl}/login/"``. + * - 5 + - ``POST`` JSON body ``{"up_number": upNumber, "password": password}`` with + ``Content-Type: application/json``. A 10-second timeout is applied. + * - 6 + - Check ``mounted`` after ``await``; if ``false``, return without touching context. + * - 7 + - Handle response by status code (see table below). + * - 8 + - Catch ``TimeoutException`` and generic exceptions, showing appropriate error messages. + * - 9 + - ``finally``: set ``isLoading = false`` if still mounted. + +**HTTP response handling:** + +.. list-table:: + :widths: 20 42 38 + :header-rows: 1 + + * - Status Code + - Action + - User-Facing Message + * - ``200 OK`` + - Decode token, store in ``ApiService.authToken``, navigate to ``HomePage`` + via ``pushReplacement`` + - *(navigates — no message)* + * - ``401 Unauthorized`` + - Call ``_showError`` + - ``"Incorrect password"`` + * - ``404 Not Found`` + - Call ``_showError`` + - ``"UP number not found"`` + * - Other + - Call ``_showError`` with status code + - ``"Login failed ()"`` + * - ``TimeoutException`` + - Call ``_showError``; log to console + - ``"Login request timed out. Check server connection."`` + * - Other exception + - Call ``_showError``; log to console + - ``"Network or server error"`` + +---- + +5.3 ``build(BuildContext context)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Constructs and returns the widget tree. Returns a ``Scaffold`` with an +``AppBar`` and a centred, padded ``Column`` containing all form elements. + +**Signature:** + +.. code-block:: dart + + @override + Widget build(BuildContext context) + +---- + +6. UI Components +---------------- + +.. list-table:: + :widths: 28 25 47 + :header-rows: 1 + + * - Widget + - Type / Config + - Description + * - AppBar + - ``title: "User Login"`` + - Top application bar + * - ``"Login"`` label + - ``Text``, 28pt bold + - Section heading at top of the form + * - UP Number field + - ``TextField`` (numeric) + - Digits only, max 7 chars, prefixed with ``"UP"``. Bound to + ``upnumberController``. + * - Password field + - ``TextField`` (obscured) + - Password input, ``obscureText: true``. Bound to ``passwordController``. + * - ``"Forgot Password?"`` button + - ``TextButton`` + - Navigates to ``ForgottenPasswordScreen`` via ``push``. + * - ``"Login"`` button + - ``ElevatedButton`` / ``CircularProgressIndicator`` + - Full-width. Calls ``loginUser()``. Replaced by spinner when + ``isLoading`` is ``true``. + * - ``"Signup"`` button + - ``ElevatedButton`` + - Full-width. Navigates to ``SignupUserPage`` via ``push``. + * - ``"Admin"`` button + - ``ElevatedButton`` + - Full-width. Navigates to ``LoginScreenAdmin`` via ``push``. + +---- + +7. Input Validation & Formatting +--------------------------------- + +7.1 UP Number Field +~~~~~~~~~~~~~~~~~~~~ + +- ``FilteringTextInputFormatter.digitsOnly`` — rejects any non-digit keystroke. +- ``LengthLimitingTextInputFormatter(7)`` — enforces a maximum of 7 digits. +- The prefix ``"UP"`` is displayed via ``prefixText`` but is **not** part of + the stored controller value. +- The raw numeric string is sent to the backend; the ``LoginView`` on the + backend prepends ``"up"`` if absent and lowercases the result. + + + +7.2 Client-Side Pre-submission Check +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Both fields must be non-empty; otherwise ``_showError`` is called and the +network request is skipped entirely. + + +.. note:: + + No further client-side validation (format, length semantics, password + strength) is performed. All deeper validation is delegated to the backend. + +---- + + +8. API Integration +------------------ + +.. list-table:: + :widths: 22 78 + :header-rows: 1 + +* - Field + - Value +* - Method + - ``POST`` +* - URL + - ``${ApiService.baseUrl}/login/`` → ``http://10.128.5.248:8000/api/login/`` +* - Headers + - ``Content-Type: application/json`` +* - Request Body + - ``{"up_number": "", "password": ""}`` +* - Timeout + - 10 seconds +* - Success Response + - ``HTTP 200`` — ``{"token": "", "role": ..., "email": ..., ...}`` + +8.1 Token Storage +~~~~~~~~~~~~~~~~~~ + +On success the token string from ``responseData["token"]`` is written to the +static field ``ApiService.authToken``. This value is then automatically +included as ``Authorization: Token `` in the headers map for all +subsequent authenticated requests via ``ApiService.headers``. + +---- + +9. Navigation Map +----------------- + +.. list-table:: + :widths: 35 30 35 + :header-rows: 1 + +* - Trigger + - Destination Screen + - Method +* - Successful login (``HTTP 200``) + - ``HomePage`` + - ``Navigator.pushReplacement`` (removes login screen from stack) +* - ``"Forgot Password?"`` tapped + - ``ForgottenPasswordScreen`` + - ``Navigator.push`` +* - ``"Signup"`` tapped + - ``SignupUserPage`` + - ``Navigator.push`` +* - ``"Admin"`` tapped + - ``LoginScreenAdmin`` + - ``Navigator.push`` + + +---- + + +10. Error Handling Summary +-------------------------- + +.. list-table:: + :widths: 30 70 + :header-rows: 1 + + diff --git a/docs/source/implementation.rst b/docs/source/implementation.rst new file mode 100644 index 000000000..b83b1a71f --- /dev/null +++ b/docs/source/implementation.rst @@ -0,0 +1,46 @@ +Implementation +============== + +Technologies Used +---------------- + +- Python (Django REST Framework backend) +- Dart / Flutter (frontend) +- PostgreSQL (database) +- Redis & Celery (background processing) +- GitHub (version control) + +System Architecture +------------------- + +The system follows a client-server architecture: + +- The Flutter frontend communicates with the backend via REST APIs +- The Django backend processes requests and interacts with the database +- Redis and Celery handle asynchronous background tasks + +Example API View +---------------- + +.. 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"}) \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 03d09a55d..8b37a9d04 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,22 +1,71 @@ -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 + +Frontend +======= + + +.. toctree:: + :maxdepth: 1 + :caption: Frontend Pages + + frontend/user_homepage + frontend/user_loginpage + + + + +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..3ec55eef3 --- /dev/null +++ b/docs/source/requirements.rst @@ -0,0 +1,39 @@ +User Requirements +================= + +Functional Requirements +---------------------- + +- Users must be able to register and log into the system securely +- Users must be able to join and leave societies +- Users must be able to view and filter societies +- Users must be able to view events in a calendar interface +- Users must receive notifications for relevant events +- Users must be able to manage notification preferences +- Users must be able to reset forgotten passwords + +Non-Functional Requirements +-------------------------- + +- Passwords must be securely hashed +- The system must be responsive and user-friendly +- Data must be handled securely +- System actions should be logged for auditing + +Admin Requirements +================== + +- Admins must be able to create, update, and delete events +- Admins must be able to manage societies +- Admins must be able to track attendance +- Admins must be able to export attendance reports +- Admins must be able to set event capacity limits + +System Requirements +=================== + +- The system must prevent duplicate society memberships +- The system must track membership status +- The system must support search and filtering +- The system must display event availability +- The system must provide account management interfaces \ No newline at end of file diff --git a/docs/source/scope.rst b/docs/source/scope.rst new file mode 100644 index 000000000..3e6801490 --- /dev/null +++ b/docs/source/scope.rst @@ -0,0 +1,25 @@ +Project Scope +============= + +This project aims to develop a digital platform for students at the University +of Portsmouth to discover, join, and engage with university societies. + +The system provides both user-facing and administrative functionality, +supporting the full lifecycle of society and event management. + +Key Features +------------ + +- Society discovery and browsing +- Event creation and participation +- User authentication and profile management +- Real-time notifications +- Attendance tracking and analytics + +Objectives +---------- + +- Improve student engagement in university societies +- Provide administrators with actionable insights +- Simplify event organisation and participation +- Centralise society-related information in one platform \ No newline at end of file diff --git a/docs/source/setup.rst b/docs/source/setup.rst new file mode 100644 index 000000000..2e5aface2 --- /dev/null +++ b/docs/source/setup.rst @@ -0,0 +1,70 @@ +Setup Instructions +================== + +This project consists of a Flutter frontend and a Django REST backend. + +Requirements +------------ + +Frontend: +- Flutter SDK (>= 3.x) +- Dart SDK (>= 3.9.0) + +Backend: +- Python (>= 3.10) +- PostgreSQL +- Redis + +Tools: +- Git + +Installation +------------ + +.. 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 + 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 + python manage.py runserver + +Frontend Setup +-------------- + +.. code-block:: bash + + cd frontend + flutter pub get + flutter run + +Notes +----- + +- Ensure PostgreSQL and Redis are running +- Update environment variables before deployment \ 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