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..053f6dc19 --- /dev/null +++ b/docs/source/backend/Admin_Analyticspage.rst @@ -0,0 +1,142 @@ +Admin Analytics +=============== + +Overview +-------- + +Provides analytical insights for society administrators. + +Features +-------- + +- Membership growth tracking +- Event attendance statistics +- Most popular event +- Live member count + +Endpoint +-------- + +.. code-block:: python + + path("my-analytics/", AnalyticsView.as_view(), name="analytics") + +Authentication +-------------- + +- Required (Admin only) + +Parameters +---------- + +- week +- month +- 6months +- year + +Implementation +-------------- + +.. code-block:: python + + class AnalyticsView(APIView): + + permission_classes = [IsAuthenticated] + + def get(self, request): + + if request.user.role != "admin": + return Response({"error": "Admins only"}, status=403) + + period = request.query_params.get("period", "week") + + try: + society = Society.objects.get(admin=request.user) + except Society.DoesNotExist: + return Response({"error": "Society not found"}, status=404) + + now = timezone.now() + + # Decide grouping & range + if period == "week": + days_range = 7 + delta = timedelta(days=1) + label_format = "%a" # Mon Tue Wed + elif period == "month": + days_range = 30 + delta = timedelta(days=1) + label_format = "%d %b" + elif period == "6months": + days_range = 26 + delta = timedelta(weeks=1) + label_format = "Week %W" + elif period == "year": + days_range = 12 + delta = timedelta(days=30) + label_format = "%b" + else: + return Response({"error": "Invalid period"}, status=400) + + start_date = now - (delta * days_range) + + labels = [] + totals = [] + + current_date = start_date + + for _ in range(days_range): + + total = Membership.objects.filter( + society=society, + joined_at__lte=current_date + ).filter( + Q(left_at__isnull=True) | Q(left_at__gt=current_date) + ).count() + + labels.append(current_date.strftime(label_format)) + totals.append(total) + + current_date += delta + + society = Society.objects.get(admin=request.user) # gets admis society + total_events = society.events.count() # total events in that society + events_stats = society.events.annotate( + attendee_count = Count( + "eventattendance", + filter = Q(eventattendance__left_at__isnull=True) + ) + ).values("title", "attendee_count") + + #most popular event + most_popular = society.events.annotate( + attendee_count = Count( + "eventattendance", + filter = Q(eventattendance__left_at__isnull=True) + ) + ).order_by("-attendee_count").values("title", "attendee_count").first() + + live_count = Membership.objects.filter( + society=society, + left_at__isnull=True + ).count() + + return Response({ + "labels": labels, + "totals": totals, + "live_count": live_count, + "total_events": total_events, + "events_stats": list(events_stats), + "most_popular": most_popular, + "event_attendance": list(events_stats) + }) + +API view to provide analytics for a society admin. +Includes: + - Membership growth over time + - Total active members + - Total events + - Event attendance statistics + - Most popular event + +Query Parameters: + - period: 'week', 'month', '6months', 'year' diff --git a/docs/source/backend/Admin_Eventspage.rst b/docs/source/backend/Admin_Eventspage.rst new file mode 100644 index 000000000..1a814ab52 --- /dev/null +++ b/docs/source/backend/Admin_Eventspage.rst @@ -0,0 +1,117 @@ +Admin Events Management +======================= + +Overview +-------- + +Allows administrators to create, update, and delete events +for their society. + +Endpoints +--------- + +.. code-block:: python + + path('events//update/', UpdateEventView.as_view(), name='update-event') + path('events//delete/', DeleteEventView.as_view(), name='delete-event') + + +Authentication +-------------- + +- Required (Admin only) + +Features +-------- + +- Create events +- Update events +- Delete events +- Associate events with a society + +Implementation +-------------- + +.. 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) + +API view to retrieve or create events for a specific society. +Only users with the ``admin`` role can create events. +The event is automatically linked to the society managed by the authenticated admin. +raises PermissionDenied: If the authenticated user is not an admin. + - ``GET``: Returns all events belonging to the given society. +- ``POST``: Allows an admin of the society to create a new event. + + +.. code-block:: python + class UpdateEventView(generics.UpdateAPIView): + + 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) + +API view to update an event created by the authenticated user. +Admins can only update events they created themselves. +Looks up the event using the ``id`` field. + +.. code-block:: python + class DeleteEventView(generics.DestroyAPIView): + + permission_classes = [IsAuthenticated] + serializer_class = EventSerializer + lookup_field = 'id' + + def get_queryset(self): + + return Event.objects.filter(created_by=self.request.user) + +API view to delete an event created by the authenticated user. +Admins can only delete events they created themselves. diff --git a/docs/source/backend/Event_Detailspage.rst b/docs/source/backend/Event_Detailspage.rst new file mode 100644 index 000000000..0be90e169 --- /dev/null +++ b/docs/source/backend/Event_Detailspage.rst @@ -0,0 +1,35 @@ +Event Details +============= + +Overview +-------- + +Retrieves detailed information about a specific event, +including attendance data. + +Endpoint +-------- + +.. code-block:: python + + path('events//', EventDetailView.as_view(), name='event-detail') + +Authentication +-------------- + +- Required + +Implementation +-------------- + +.. code-block:: python + + class EventDetailView(generics.RetrieveAPIView): + + permission_classes = [IsAuthenticated] + queryset = Event.objects.all() + serializer_class = EventSerializer + lookup_field = 'id' + +API view to retrieve details of a single event by ID. +Requires authentication. Looks up the event using the ``id`` field. diff --git a/docs/source/backend/User_Homepage.rst b/docs/source/backend/User_Homepage.rst new file mode 100644 index 000000000..9a1ae974b --- /dev/null +++ b/docs/source/backend/User_Homepage.rst @@ -0,0 +1,84 @@ +User HomePage +========== + +Overview +-------- + +Displays key information for the user dashboard, including recent events +and searchable societies. + +Endpoint +-------- + +.. code-block:: python + + path('events/all/', AllEventsView.as_view(), name='all-events') + path("search/", SocietyListSearchView.as_view(), name="society-search") + +Authentication +-------------- + +- Required + +Features +-------- + +- View latest events +- Search societies +- View society details + +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) + +API view to retrieve the 5 most recently added events. +Requires authentication. Returns events ordered by descending ID. +Returns the 5 most recent events. + +.. 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) + +API view to list and search active societies. +Requires authentication. Supports an optional ``q`` query parameter to filter societies by name. Results include the active member count for each society and are ordered alphabetically by name. + +Return a list of active societies, optionally filtered by name. diff --git a/docs/source/backend/User_Login.rst b/docs/source/backend/User_Login.rst new file mode 100644 index 000000000..5dbfbbd0c --- /dev/null +++ b/docs/source/backend/User_Login.rst @@ -0,0 +1,84 @@ +User Login +========== + +Overview +-------- + +Authenticates a user using email or university number and returns +an authentication token. + +Endpoint +-------- + +.. code-block:: python + + path("login/", LoginView.as_view(), name="login") + +Response +-------- + +- Returns authentication token +- Returns user role and details + +Implementation +-------------- + +.. code-block:: python + + class LoginView(APIView): + def post(self, request): + email = request.data.get("email") + up_number = request.data.get("up_number") + password = request.data.get("password") + + if not password: + return Response({"error": "Password required"}, status=400) + + try: + if email: + user = User.objects.get(email__iexact=email) + elif up_number: + up_number = up_number.lower() + if not up_number.startswith("up"): + up_number = f"up{up_number}" + user = User.objects.get(up_number__iexact=up_number) + else: + return Response({"error": "Email or UP number required"}, status=400) + + if user.check_password(password): + token, _ = Token.objects.get_or_create(user=user) + + society_id = None + society_name = None + + if user.role == "admin": + try: + society = Society.objects.get(admin=user) + society_id = society.id + society_name = society.name + except Society.DoesNotExist: + pass + + return Response({ + "token": token.key, + "role": user.role, + "email": user.email, + "up_number": user.up_number, + "society_id": society_id, + "society_name": society_name + }) + + except User.DoesNotExist: + pass + + return Response({"error": "Invalid credentials"}, status=401) + + +API view to authenticate a user and return an auth token. +Users can log in using either: + - Email + - University number (UP number) +Returns: + - Auth token and user details on success + - 401 Unauthorized if credentials are invalid + \ No newline at end of file diff --git a/docs/source/backend/User_MyEventspage.rst b/docs/source/backend/User_MyEventspage.rst new file mode 100644 index 000000000..307a02ba8 --- /dev/null +++ b/docs/source/backend/User_MyEventspage.rst @@ -0,0 +1,138 @@ +User My Events Page +=================== + +Overview +-------- + +Allows users to view, join, and leave events. + +Endpoints +--------- + +.. code-block:: 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 + +Features +-------- + +- View joined events +- Join events +- Leave events +- Prevent joining past events + +Implementation +-------------- + +.. code-block:: python + + class MyEventsView(APIView): + + permission_classes = [IsAuthenticated] + + def get(self, request): + """Return events relevant to the authenticated user. + + :param request: The HTTP request. + :type request: Request + :return: Serialized list of events. + :rtype: Response + """ + if request.user.role == "admin": + society = Society.objects.get(admin=request.user) + events = Event.objects.filter(society=society) + else: + events = Event.objects.filter( + society__membership__user=request.user + ).distinct() + + serializer = EventSerializer(events, many=True) + return Response(serializer.data) + +API view to retrieve events relevant to the authenticated user. +- For **admins**: Returns all events belonging to their managed society. +- For **regular users**: Returns all events from societies they are members of. + +.. code-block:: python + class JoinEventView(APIView): + + permission_classes = [IsAuthenticated] + + def post(self, request, event_id): + + try: + event = Event.objects.get(id=event_id) + except Event.DoesNotExist: + return Response({"error": "Event not found"}, status=404) + + # prevent joining past events + if event.start_time < timezone.now(): + return Response( + {"error": "Event has already passed"}, + status=400 + ) + + attendance, created = EventAttendance.objects.get_or_create( + user=request.user, + event=event, + defaults={"left_at": None} + ) + + if not created: + if attendance.left_at is None: + return Response({"message": "Already attending"}, status=400) + else: + attendance.left_at = None + attendance.joined_at = timezone.now() + attendance.save() + + attendee_count = EventAttendance.objects.filter( + event=event, + left_at__isnull=True + ).count() + + return Response({ + "message": "Joined event", + "attendee_count": attendee_count + }) + +API view to allow a user to join an event. +Behaviour: + - Prevents joining past events + - Creates attendance record if not existing + - Re-activates attendance if previously left +Returns updated attendee count. + +.. code-block:: python + class LeaveEventView(APIView): + + permission_classes = [IsAuthenticated] + + def post(self, request, event_id): + try: + attendance = EventAttendance.objects.get( + user=request.user, + event_id=event_id, + left_at__isnull=True + ) + except EventAttendance.DoesNotExist: + return Response({"error": "Not attending this event"}, status=400) + + attendance.left_at = timezone.now() + attendance.save() + + attendee_count = EventAttendance.objects.filter( + event_id=event_id, + left_at__isnull=True).count() + + return Response({"message": "Left event successfully"}) + +API view to allow a user to leave an event. +Marks attendance as inactive by setting `left_at`. diff --git a/docs/source/backend/User_MySocietypage.rst b/docs/source/backend/User_MySocietypage.rst new file mode 100644 index 000000000..6873b4c20 --- /dev/null +++ b/docs/source/backend/User_MySocietypage.rst @@ -0,0 +1,143 @@ +User My Societies Page +====================== + +Overview +-------- + +Allows users to view and manage societies they are part of. + +Endpoints +--------- + +.. 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 + +Features +-------- + +- View joined societies +- Join new societies +- Leave societies + +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) + +Returns all societies the user is currently a member of. + +.. 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) + + # Rejoining + membership.left_at = None + membership.joined_at = timezone.now() + membership.save() + + return Response({"message": "Rejoined successfully"}, status=20 + + +API view to allow a user to join a society. +Behaviour: + - Creates a new membership if none exists + - Returns 'Already joined' if user is already active + - Re-activates membership if previously left + +.. code-block:: python + + class LeaveSocietyView(APIView): + + permission_classes = [IsAuthenticated] + + def post(self, request, society_id): + user = request.user + + try: + society = Society.objects.get(id=society_id) + except Society.DoesNotExist: + return Response( + {"error": "Society not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + try: + membership = Membership.objects.get( + user=user, + society=society, + left_at__isnull=True + + ) + except Membership.DoesNotExist: + return Response( + {"error": "You are not an active member"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + membership.left_at = timezone.now() + membership.save() + + return Response( + {"message": "Successfully left society"}, + status=status.HTTP_200_OK, + ) + +API view to allow a user to leave a society. +Sets the `left_at` timestamp on the membership record instead of deleting it. + diff --git a/docs/source/backend/User_Registration.rst b/docs/source/backend/User_Registration.rst new file mode 100644 index 000000000..38787d705 --- /dev/null +++ b/docs/source/backend/User_Registration.rst @@ -0,0 +1,103 @@ +User Registration +================= + +Overview +-------- + +This endpoint allows a new user to register an account in the system. +It validates user input, enforces password strength rules, and ensures +unique email and university number. + +Endpoint +-------- + +.. code-block:: python + + path("user/register/", RegisterView.as_view(), name="register") + +Authentication +-------------- + +- Not required + +Implementation +-------------- + +.. code-block:: python + + class RegisterView(APIView): + + def post(self, request): + first_name = request.data.get("first_name") + last_name = request.data.get("last_name") + email = request.data.get("email") + up_number = request.data.get("up_number") + password = request.data.get("password") + confirm_password = request.data.get("confirm_password") + + # Check required fields + if not all([first_name, last_name, email, up_number, password, confirm_password]): + return Response( + {"error": "All fields are required"}, + status=status.HTTP_400_BAD_REQUEST + ) + # Password match + if password != confirm_password: + return Response( + {"error": "Passwords do not match"}, + status=status.HTTP_400_BAD_REQUEST + ) + # Password strength + if len(password) < 8: + return Response( + {"error": "Password must be at least 8 characters long"}, + status=status.HTTP_400_BAD_REQUEST + ) + if not re.search(r"[A-Z]", password): + return Response( + {"error": "Password must contain at least one uppercase letter"}, + status=status.HTTP_400_BAD_REQUEST + ) + if not re.search(r"[0-9]", password): + return Response( + {"error": "Password must contain at least one number"}, + status=status.HTTP_400_BAD_REQUEST + ) + if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password): + return Response( + {"error": "Password must contain at least one special character"}, + status=status.HTTP_400_BAD_REQUEST + ) + # Normalize UP number + up_number = up_number.lower() + if not up_number.startswith("up"): + up_number = f"up{up_number}" + # Check duplicates + if User.objects.filter(email=email).exists(): + return Response({"error": "Email already exists"}, status=400) + + if User.objects.filter(up_number=up_number).exists(): + return Response({"error": "UP number already exists"}, status=400) + # Create user + user = User.objects.create_user( + first_name=first_name, + last_name=last_name, + email=email, + up_number=up_number, + password=password + ) + return Response( + {"message": "User registered successfully"}, + status=status.HTTP_201_CREATED + ) + + +API view to handle user registration, accepts user details including first name, last name, email, + university number (UP number), and password. It validates: + - All required fields are provided + - Passwords match + - Password strength (length, uppercase, number, special character) + + Returns: + - 201 Created on success + - 400 Bad Request on validation failure diff --git a/docs/source/backend/User_Settingspage.rst b/docs/source/backend/User_Settingspage.rst new file mode 100644 index 000000000..afcd8dc36 --- /dev/null +++ b/docs/source/backend/User_Settingspage.rst @@ -0,0 +1,180 @@ +User Settings Page +================== + +Overview +-------- + +Allows users to manage their account settings, including profile, +email, password, and notification preferences. + +Endpoints +--------- + +.. 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 + +Features +-------- + +- Change password +- Change email +- Update profile information +- Manage notification preferences + +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"}) + +API view to allow an authenticated user to change their password. +The user must provide their current password to verify their identity before setting a new one. + :param request: The HTTP request containing ``old_password`` and ``new_password``. + :type request: Request + :return: Success message, or 400 if the old password is incorrect. + :rtype: Response + +.. code-block:: python + class ChangeEmailView(APIView): + + permission_classes = [IsAuthenticated] + + def post(self, request): + + 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"}) + +API view to allow an authenticated user to change their email address. +The new email must not already be in use by another account. + :param request: The HTTP request containing ``new_email``. + :type request: Request + :return: Success message, or 400 if the email is missing or already in use. + :rtype: Response + +.. code-block:: python + class UserProfileView(APIView): + + permission_classes = [IsAuthenticated] + + 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 request.data: + user.first_name = request.data["first_name"] + + if "last_name" in request.data: + user.last_name = request.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) + +Retrieve and update the authenticated user's profile. + +.. 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 + }) + +API view to retrieve or update the authenticated user's notification preferences. + +- ``GET``: Returns the user's notification preferences for each society they belong to. +- ``POST``: Updates the notification preference for a specific society. +Updates the authenticated user's notification preference for a society. + 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/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..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..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