From f831584314bb785a69e0c4690d26552257a0d9ad Mon Sep 17 00:00:00 2001 From: rithikreddypalla Date: Sat, 4 Jan 2025 22:59:34 +0530 Subject: [PATCH] documentation done for the members --- db.py | 16 ++++++ main.py | 8 +++ models.py | 98 ++++++++++++++++++++++++++++++++++++ mutations.py | 126 +++++++++++++++++++++++++++++++++++++++++++++- otypes.py | 19 +++++++ queries.py | 138 +++++++++++++++++++++++++++++++++++++++------------ utils.py | 27 +++++++++- 7 files changed, 397 insertions(+), 35 deletions(-) diff --git a/db.py b/db.py index 41a74af..584d959 100644 --- a/db.py +++ b/db.py @@ -1,3 +1,19 @@ +""" +MongoDB Initialization Module + +This module sets up a connection to a MongoDB database and ensures that the required indexes are created. +This module connects to the MongoDB database using environment variables for authentication. +Ensures that a 'unique_members' index is present on the `cid` and 'uid' fields in the Members collection. +It specifically exports the members collection of the database. + +Environment Variables: + `MONGO_USERNAME` (str): MongoDB username. Defaults to "username". + `MONGO_PASSWORD` (str): MongoDB password. Defaults to "password". + `MONGO_PORT` (str): MongoDB port. Defaults to "27017". + `MONGO_DATABASE` (str): MongoDB database name. Defaults to "default". + +""" + from os import getenv from pymongo import MongoClient diff --git a/main.py b/main.py index 19a20d8..4e2b294 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,11 @@ +""" +Final Setup + +This file is used to setup the final schema for the subgraph. +It imports the resolvers from the queries and mutations files and creates a final GraphQL schema. +It sets up the Fast API for the Members Microservice. +""" + from os import getenv import strawberry diff --git a/models.py b/models.py index 57817b7..e9feebf 100644 --- a/models.py +++ b/models.py @@ -1,3 +1,15 @@ +""" +Data Models for Members Microservice + +This file decides what and how a Members's information is stored in its MongoDB document. +One user could be a part of multiple clubs. +his membership in each club is stored in a separate document. + +It defines 2 models: + Member : Used for storing members information. + Roles : Used for storing a member's roles within the same club. Used within Member model. +""" + from typing import Any, List from bson import ObjectId @@ -13,8 +25,28 @@ # for handling mongo ObjectIds class PyObjectId(ObjectId): + """ + MongoDB ObjectId handler + + This class contains clasmethods to validate and serialize ObjectIds. + ObjectIds of documents under the Clubs collection are stored under the 'id' field. + """ + @classmethod def __get_pydantic_core_schema__(cls, source_type: Any, handler): + """ + Defines custom schema for Pydantic validation + + This method is used to define the schema for the Pydantic model. + + Args: + source_type (Any): The source type. + handler: The handler. + + Returns: + dict: The schema for the Pydantic model. + """ + return core_schema.union_schema( [ # check if it's an instance first before doing any further work @@ -26,16 +58,64 @@ def __get_pydantic_core_schema__(cls, source_type: Any, handler): @classmethod def validate(cls, v): + """ + Validates the given ObjectId + + Args: + v (Any): The value to validate. + + Returns: + ObjectId: The validated ObjectId. + + Raises: + ValueError: If the given value is not a valid ObjectId. + """ + if not ObjectId.is_valid(v): raise ValueError("Invalid ObjectId") return ObjectId(v) @classmethod def __get_pydantic_json_schema__(cls, field_schema): + """ + Generates JSON schema + + This method is used to generate the JSON schema for the Pydantic model. + + Args: + field_schema (dict): The field schema. + """ + field_schema.update(type="string") class Roles(BaseModel): + """ + Model for storing a member's roles + + This model defines the structure to store a member's roles. + + Attributes: + rid (str): Unique Identifier for a role, a role id. + name (str): Name of the role + start_year (int): Year the role started + end_year (Optional[int]): Year the role ended + approved (bool): Whether the role is approved + approval_time (Optional[str]): Time the role was approved + rejected (bool): Whether the role was rejected + rejection_time (Optional[str]): Time the role was rejected + deleted (bool): Whether the role is deleted + + Field Validators: + check_end_year: Validates the end_year field based on the start_year field.checks if the end_year is smaller than the start_year. + check_status: Validates the status of the role based on the approved and rejected fields. + + Raises Errors: + ValueError: If the end_year is smaller than the start_year. + ValueError: If the status of the role is not valid.If both approved and rejeted are True. + + """ + rid: str | None = Field(None, description="Unique Identifier for a role") name: str = Field(..., min_length=1, max_length=99) start_year: int = Field(..., ge=2010, le=2050) @@ -71,6 +151,24 @@ def check_status(cls, value, info: ValidationInfo): class Member(BaseModel): + """ + Model for storing a member's information + + This model defines the structure to store a member's information. + + Attributes: + id (PyObjectId): Stores the ObjectId of the member's document. + cid (str): Unique Identifier for a club, a club id. + uid (str): Unique Identifier for a user, a user id. + creation_time (str): Time the member was created. + last_edited_time (str): Time the member's information was last edited. + roles (List[Roles]): List of Roles for that specific person. + poc (bool): Whether the member is a POC(Point of Contact) for the club. + + Field Validators: + transform_uid: Transforms the uid field text to lowercase. + """ + id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") cid: str = Field(..., description="Club ID") uid: str = Field(..., description="User ID") diff --git a/mutations.py b/mutations.py index 02a744c..a0f2af4 100644 --- a/mutations.py +++ b/mutations.py @@ -1,3 +1,17 @@ +""" +Mutation resolvers + +It contains resolvers to create, edit and delete members and to approve/reject their roles. + +Resolvers: + createMember: Creates a new member. + editMember: Edits an existing member. + deleteMember: Deletes an existing member. + approveMember: Approves a role of a member. + rejectMember: Rejects a role of a member. + UpdateMembersCid: Updates all members of old_cid to new_cid. +""" + from datetime import datetime from os import getenv @@ -20,7 +34,27 @@ def createMember(memberInput: FullMemberInput, info: Info) -> MemberType: """ Mutation to create a new member by that specific 'club' or cc + + This method creates a new member in the database. + + Inputs: + memberInput (FullMemberInput): Contains the details of the member. + info (Info): Contains the logged in user's details. + + Returns: + MemberType: Contains the details of the member. + + Accessibility: + CC, original club accounts has full access. + + Raises Exception: + Not Authenticated/Not Authenticated to access this API: If the user is not authenticated. + A record with same uid and cid already exists: If a member with the same uid and cid already exists in the database. + Invalid User id: If there does not exist a user with the given uid. + Roles cannot be empty: If the roles list is empty. + Start year cannot be greater than end year: If the start year is greater than the end year. """ + user = info.context.user if user is None: raise Exception("Not Authenticated") @@ -94,7 +128,26 @@ def createMember(memberInput: FullMemberInput, info: Info) -> MemberType: def editMember(memberInput: FullMemberInput, info: Info) -> MemberType: """ Mutation to edit an already existing member+roles of that specific 'club' + + This method edits an existing member in the database. + It can only be done by the club or cc account. + + Inputs: + memberInput (FullMemberInput): Contains the details of the member. + info (Info): Contains the logged in user's details. + + Returns: + MemberType: Contains the details of the member. + + Accessibility: + CC, original club accounts has full access. + + Raises Exception: + Not Authenticated/Not Authenticated to access this API: If the user is not authenticated. + Start year cannot be greater than end year: If the start year is greater than the end year. + No such record : If there is no record with the given uid and cid. """ + user = info.context.user if user is None: raise Exception("Not Authenticated") @@ -193,7 +246,25 @@ def editMember(memberInput: FullMemberInput, info: Info) -> MemberType: def deleteMember(memberInput: SimpleMemberInput, info: Info) -> MemberType: """ Mutation to delete an already existing member (role) of that specific 'club' + + This method deletes an existing member in the database. + It can only be done by the club or cc account. + + Inputs: + memberInput (SimpleMemberInput): Contains the details of the member. + info (Info): Contains the logged in user's details. + + Returns: + MemberType: Contains the details of the member. + + Accessibility: + CC, original club accounts has full access. + + Raises Exception: + Not Authenticated/Not Authenticated to access this API: If the user is not authenticated. + No such record : If there is no record with the given uid and cid. """ # noqa: E501 + user = info.context.user if user is None: raise Exception("Not Authenticated") @@ -256,7 +327,24 @@ def deleteMember(memberInput: SimpleMemberInput, info: Info) -> MemberType: def approveMember(memberInput: SimpleMemberInput, info: Info) -> MemberType: """ Mutation to approve a member role by 'cc' + + This method approves a member role in the database. + + Inputs: + memberInput (SimpleMemberInput): Contains the details of the member.cid, uid and rid. + info (Info): Contains the logged in user's details. + + Returns: + MemberType: Contains the details of the member. + + Accessibility: + only CC has full access. + + Raises Exception: + Not Authenticated/Not Authenticated to access this API: If the user is not authenticated. + No such record : If there is no record with the given uid and cid """ + user = info.context.user if user is None: raise Exception("Not Authenticated") @@ -284,6 +372,7 @@ def approveMember(memberInput: SimpleMemberInput, info: Info) -> MemberType: current_time = datetime.now(ist) time_str = current_time.strftime("%d-%m-%Y %I:%M %p IST") + # approves the role along with entering approval time roles = [] for i in existing_data["roles"]: if not member_input["rid"] or i["rid"] == member_input["rid"]: @@ -313,7 +402,24 @@ def approveMember(memberInput: SimpleMemberInput, info: Info) -> MemberType: def rejectMember(memberInput: SimpleMemberInput, info: Info) -> MemberType: """ Mutation to reject a member role by 'cc' + + This method rejects a member role in the database. + + Inputs: + memberInput (SimpleMemberInput): Contains the details of the member.cid, uid and rid. + info (Info): Contains the logged in user's details. + + Returns: + MemberType: Contains the details of the member. + + Accessibility: + only CC has full access. + + Raises Exception: + Not Authenticated/Not Authenticated to access this API: If the user is not authenticated. + No such record : If there is no record with the given uid and cid """ + user = info.context.user if user is None: raise Exception("Not Authenticated") @@ -405,8 +511,26 @@ def updateMembersCid( inter_communication_secret: str | None = None, ) -> int: """ - update all memberd of old_cid to new_cid + update all members of old_cid to new_cid + + It updates all members with old_cid to new_cid + + Input: + old_cid: the old cid + new_cid: the new cid + inter_communication_secret (str | None): The inter communication secret. + + Returns: + int: The number of updated members. + + Accessibility: + only CC has full access. + + Raises Exception: + Not Authenticated: If the user is not authenticated. + Authentication Error! Invalid secret!: If the inter communication secret is invalid. """ + user = info.context.user if user is None or user["role"] not in ["cc"]: diff --git a/otypes.py b/otypes.py index b543855..23656d8 100644 --- a/otypes.py +++ b/otypes.py @@ -1,3 +1,22 @@ +""" +Types and Inputs + +It contains both Inputs and Types for taking inputs and returning outputs. +It also contains the Context class which is used to pass the user details to the resolvers. + +Types: + Info : used to pass the user details to the resolvers. + PyObjectId : used to return ObjectId of a document. + RolesType : used to return all the details of a role. + MemberType : used to return all the details of a member. + +Inputs: + RolesInput : used to input name, start year and end year of the role. + FullMemberInput : used to input cid, uid, roles and poc(Optional) fields of the member. + SimpleMemberInput : used to input cid, uid and rid(Optional) of the member. + SimpleClubInput : used to input cid of the club. +""" + import json from functools import cached_property from typing import Dict, Optional, Union diff --git a/queries.py b/queries.py index b382754..40b46bf 100644 --- a/queries.py +++ b/queries.py @@ -1,3 +1,16 @@ +""" +Query resolvers + +This file contains the query resolvers. + +Resolvers: + member: Returns the details of a member of a specific club. + memberRoles: Returns the member with his current roles from all clubs. + members: Returns the details of all the members of a specific club. + currentMembers: Returns the details of all the current members of a specific club. + pendingMembers: Returns the details of all the pending members. +""" + from typing import List import strawberry @@ -9,20 +22,31 @@ # import all models and types from otypes import Info, MemberType, SimpleClubInput, SimpleMemberInput -""" -Member Queries -""" @strawberry.field def member(memberInput: SimpleMemberInput, info: Info) -> MemberType: """ - Description: - Returns member details for a specific club - Scope: CC & Specific Club - Return Type: MemberType - Input: SimpleMemberInput (cid, uid) + Details of a member of a specific club + + This method fetches the details of a member of a specific club. + The member is searched in the database using the cid and uid of the member. + + Inputs: + memberInput (SimpleMemberInput): Contains the cid and uid of the member. + info (Info): Contains the logged in user's details. + + Returns: + MemberType: Contains the details of the member. + + Accessibility: + CC and club both have full access. + + Raises Exception: + Not Authenticated/Not Authenticated to access this API: If the user is not authenticated. + No such Record: If the member with the given uid, cid is not found in the database. """ + user = info.context.user if user is None: raise Exception("Not Authenticated") @@ -53,12 +77,27 @@ def member(memberInput: SimpleMemberInput, info: Info) -> MemberType: @strawberry.field def memberRoles(uid: str, info: Info) -> List[MemberType]: """ - Description: - Returns member roles from each club - Scope: CC & Specific Club - Return Type: uid (str) - Input: SimpleMemberInput (cid, uid, roles) + Returns memeber along with his roles + + A user can be part of many clubs. + And therefore have multiple roles, each in a different club. + This method searches member documents having the given uid. + It returns the member details in each club along with their current roles. + + Inputs: + uid (str): The uid of the member. + info (Info): Contains the logged in user's details. + + Returns: + List[MemberType]: Contains a list of member with their current roles. + + Accessibility: + Public + + Raises Exception: + No Member Result/s Found: If no member is found with the given uid. """ + user = info.context.user if user is None: role = "public" @@ -95,17 +134,27 @@ def memberRoles(uid: str, info: Info) -> List[MemberType]: @strawberry.field def members(clubInput: SimpleClubInput, info: Info) -> List[MemberType]: """ - Description: - For CC: - Returns all the non-deleted members. - For Specific Club: - Returns all the non-deleted members of that club. - For Public: - Returns all the non-deleted and approved members. - Scope: CC + Club (For All Members), Public (For Approved Members) - Return Type: List[MemberType] - Input: SimpleClubInput (cid) + Returns all the members of a club. + + This method fetches all the members of a specific club. + For CC and the club, it returns all the members with their current non-deleted, approved and pending roles. + For public, it returns all the members with their current non-deleted and approved roles. + + Inputs: + clubInput (SimpleClubInput): Contains the cid of the club. + info (Info): Contains the logged in user's details. + + Returns: + List[MemberType]: Contains a list of members. + + Accessibility: + CC and the same club both have full access. + Public has partial access. + + Raises Exception: + No Member Result/s Found: If no member is found with the given cid. """ + user = info.context.user if user is None: role = "public" @@ -151,14 +200,24 @@ def members(clubInput: SimpleClubInput, info: Info) -> List[MemberType]: @strawberry.field def currentMembers(clubInput: SimpleClubInput, info: Info) -> List[MemberType]: """ - Description: - For Everyone: - Returns all the current non-deleted and approved members of the given clubid. + Returns the current members of a club. + + Returns all the members with their current non-deleted and approved roles for the given clubid. + + Input: + clubInput (SimpleClubInput): Contains the cid of the club. + info (Info): Contains the logged in user's details. - Scope: Anyone (Non-Admin Function) - Return Type: List[MemberType] - Input: SimpleClubInput (cid) + Returns: + List[MemberType]: Contains a list of members. + + Accessibility: + Public. + + Raises Exception: + No Member Result/s Found: If no member is found with the given cid. """ # noqa: E501 + user = info.context.user if user is None: role = "public" @@ -202,11 +261,24 @@ def currentMembers(clubInput: SimpleClubInput, info: Info) -> List[MemberType]: @strawberry.field def pendingMembers(info: Info) -> List[MemberType]: """ - Description: Returns all the non-deleted and non-approved members. - Scope: CC - Return Type: List[MemberType] - Input: None + Returns the pending members of all clubs. + + Returns all the members with their current non-deleted and pending roles from all clubs. + + Input: + info (Info): Contains the logged in user's details. + + Returns: + List[MemberType]: Contains a list of members. + + Accessibility: + Only CC has access. + + Raises Exception: + No Member Result/s Found: If no member is found. + """ + user = info.context.user if user is None or user["role"] not in ["cc"]: raise Exception("Not Authenticated") diff --git a/utils.py b/utils.py index ab2a74f..48a39f4 100644 --- a/utils.py +++ b/utils.py @@ -12,10 +12,22 @@ def non_deleted_members(member_input) -> MemberType: """ - Function to return non-deleted members for a particular cid, uid + Returns the non-deleted member + + Function to return a non-deleted member for a particular cid, uid Only to be used in admin functions, as it returns both approved/non-approved members. + + Inputs: + member_input (dict): Contains the cid and uid of the member. + + Returns: + MemberType: Contains the details of the member. + + Raises Exception: + No such Record: If the member with the given uid, cid is not found in the database. """ + updated_sample = membersdb.find_one( { "$and": [ @@ -42,6 +54,7 @@ def unique_roles_id(uid, cid): """ Function to give unique ids for each of the role in roles list """ + pipeline = [ { "$set": { @@ -82,7 +95,19 @@ def unique_roles_id(uid, cid): def getUser(uid, cookies=None): """ Function to get a particular user details + + This method makes a query to the user-service to get the details of a particular user. + It is used to get the details of a user. + It is resolved by the userProfile resolver. + + Inputs: + uid (str): The uid of the user. + cookies (dict): The cookies of the user. + + Returns: + dict: The details of the user. """ + try: query = """ query GetUserProfile($userInput: UserInput!) {