diff --git a/.gitignore b/.gitignore index f51ac09..b3e360d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -.venv/ +*venv/ .vscode/ .in .out __pycache__/ +build/ diff --git a/db.py b/db.py index 2ea6c19..72b6e9f 100644 --- a/db.py +++ b/db.py @@ -1,3 +1,20 @@ +""" +MongoDB Initialization Module. + +This module sets up the connection to the MongoDB database. +It ensures that the required indexes are created. + +Attributes: + MONGO_USERNAME (str): An environment variable having MongoDB username. Defaults to "username". + MONGO_PASSWORD (str): An environment variable having MongoDB password. Defaults to "password". + MONGO_PORT (str): MongoDB port. Defaults to "27017". + MONGO_URI (str): MongoDB URI. + MONGO_DATABASE (str): MongoDB database name. + client (MongoClient): MongoDB client. + db (Database): MongoDB database. + clubsdb (Collection): MongoDB collection for clubs. +""" + from os import getenv from pymongo import MongoClient diff --git a/main.py b/main.py index 6934ef5..8ade6a4 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,16 @@ +""" +Main module for the Clubs Microservice. + +This module sets up the FastAPI application and integrates the Strawberry GraphQL schema. +It includes the configuration for queries, mutations, and context. + +Attributes: + GLOBAL_DEBUG (str): Environment variable that Enables or Disables debug mode. Defaults to "False". + DEBUG (bool): Indicates whether the application is running in debug mode. + gql_app (GraphQLRouter): The GraphQL router for handling GraphQL requests. + app (FastAPI): The FastAPI application instance. +""" + from os import getenv import strawberry @@ -20,7 +33,7 @@ Mutation = create_type("Mutation", mutations) -# override context getter +# Returns The custom context by overriding the context getter. async def get_context() -> Context: return Context() diff --git a/models.py b/models.py index 6bda7a0..da9604b 100644 --- a/models.py +++ b/models.py @@ -17,6 +17,9 @@ from pydantic_core import core_schema from pytz import timezone +""" +Annotated type and validator for HTTP URLs to be stored as strings. +""" http_url_adapter = TypeAdapter(HttpUrl) HttpUrlString = Annotated[ str, @@ -27,11 +30,18 @@ def create_utc_time(): + """ + Returns the current time according to UTC timezone. + """ return datetime.now(timezone("UTC")) # for handling mongo ObjectIds class PyObjectId(ObjectId): + """ + Class for handling MongoDB document ObjectIds for 'id' fields in Models. + """ + @classmethod def __get_pydantic_core_schema__(cls, source_type: Any, handler): return core_schema.union_schema( @@ -55,6 +65,18 @@ def __get_pydantic_json_schema__(cls, field_schema): def iiit_email_only(v: str) -> str: + """ + Validates emails according to the valid forms. + + Args: + v (str): The Email to be validated. + + Raises: + ValueError: If email is not valid. + + Returns: + str: Valid Email. + """ valid_domains = [ "@iiit.ac.in", "@students.iiit.ac.in", @@ -67,17 +89,22 @@ def iiit_email_only(v: str) -> str: def current_year() -> int: + """Returns the current year.""" return datetime.now().year @strawberry.enum class EnumStates(str, Enum): + """Enum for state of the club.""" + active = "active" deleted = "deleted" @strawberry.enum class EnumCategories(str, Enum): + """Enum for category of the club.""" + cultural = "cultural" technical = "technical" affinity = "affinity" @@ -87,6 +114,21 @@ class EnumCategories(str, Enum): class Social(BaseModel): + """ + Model for storing social handles of a Club + + Attributes: + website (HttpUrlString | None): Club Website URL. Defaults to None. + instagram (HttpUrlString | None): Club Instagram handle. Defaults to None. + facebook (HttpUrlString | None): Club Facebook. Defaults to None. + youtube (HttpUrlString | None): Club YouTube handle. Defaults to None. + twitter (HttpUrlString | None): Club Twitter handle. Defaults to None. + linkedin (HttpUrlString | None): Club LinkedIn handle. Defaults to None. + discord (HttpUrlString | None): Club Discord handle. Defaults to None. + whatsapp (HttpUrlString | None): Club WhatsApp handle. Defaults to None. + other_links (List[HttpUrlString]): List of other social handles and URLs + """ + website: HttpUrlString | None = None instagram: HttpUrlString | None = None facebook: HttpUrlString | None = None @@ -106,6 +148,28 @@ def validate_unique_links(cls, value): class Club(BaseModel): + """ + Model for storing Club details. + + Attributes: + id (PyObjectId): Unique ObjectId of the document of the Club. + cid (str): the Club ID. + code (str): Unique Short Code of Club. + state (EnumStates): State of the Club. + category (EnumCategories): Category of the Club. + student_body (bool): Is this a Student Body? + name (str): Name of the Club. + email (EmailStr): Email of the Club. + logo (str | None): Club Official Logo. Defaults to None. + banner (str | None): Club Long Banner. Defaults to None. + banner_square (str | None): Club Square Banner. Defaults to None. + tagline (str | None): Tagline of the Club. Defaults to None. + description (str | None): Club Description. Defaults to None. + socials (Social): Social Handles of the Club. + created_time (datetime): Time of creation of the Club. + updated_time (datetime): Time of last update to the Club. + """ + id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") cid: str = Field(..., description="Club ID") code: str = Field( diff --git a/mutations.py b/mutations.py index 3261a13..39b43cf 100644 --- a/mutations.py +++ b/mutations.py @@ -1,3 +1,7 @@ +""" +Mutations for Clubs +""" + import strawberry from fastapi.encoders import jsonable_encoder @@ -23,9 +27,22 @@ @strawberry.mutation def createClub(clubInput: FullClubInput, info: Info) -> SimpleClubType: """ - Create a new Club. - Checks for the 'cc' role, else raises an Error. - Checks for uniqueness of the club code. + Mutation for creation of a new club by CC. + + Args: + clubInput (FullClubInput): Full details of the club. + info (Info): User metadata and cookies. + + Returns: + SimpleClubType: Details of the created club. + + Raises: + Exception: Not Authenticated + Exception: A club with this cid already exists + Exception: A club with this short code already exists + Exception: Invalid Club ID/Club Email + Exception: Error in updating the role for the club + Exception: Not Authenticated to access this API """ user = info.context.user if user is None: @@ -67,7 +84,29 @@ def createClub(clubInput: FullClubInput, info: Info) -> SimpleClubType: @strawberry.mutation def editClub(clubInput: FullClubInput, info: Info) -> FullClubType: """ - Mutation for editing of the club details either by that specific club or the cc. + Mutation for editing of the club details either by that specific club or the cc + + This method is used for editing the club details. + CC can edit any club details, but the club can only edit its own details. + Only CC can change a clubs name/email and category. + + + Args: + clubInput (FullClubInput): Full details of the club to be updated to. + Info (Info): User metadata and cookies. + + Returns: + FullClubType: Full Details of the edited club. + + Raises: + Exception: Not Authenticated. + Exception: A club with this code does not exist. + Exception: Invalid Club ID/Club Email. + Exception: Error in updating the role/cid. + Exception: Authentication Error! (CLUB ID CHANGED). + Exception: You dont have permission to change the name/email of the club. Please contact CC for it. + Exception: Only CC is allowed to change the category of club. + Exception: Not Authenticated to access this API. """ # noqa: E501 user = info.context.user if user is None: @@ -195,6 +234,8 @@ def editClub(clubInput: FullClubInput, info: Info) -> FullClubType: } }, ) + + # also autofills the updated time clubsdb.update_one( {"cid": club_input["cid"]}, { @@ -218,6 +259,17 @@ def editClub(clubInput: FullClubInput, info: Info) -> FullClubType: def deleteClub(clubInput: SimpleClubInput, info: Info) -> SimpleClubType: """ Mutation for the cc to move a club to deleted state. + + Args: + clubInput (SimpleClubInput): The club cid. + info (Info): User metadata and cookies. + + Returns: + SimpleClubType: Details of the deleted club. + + Raises: + Exception: Not Authenticated. + Exception: Not Authenticated to access this API. """ user = info.context.user if user is None: @@ -229,6 +281,7 @@ def deleteClub(clubInput: SimpleClubInput, info: Info) -> SimpleClubType: if role not in ["cc"]: raise Exception("Not Authenticated to access this API") + # also autofills the updated time clubsdb.update_one( {"cid": club_input["cid"]}, {"$set": {"state": "deleted", "updated_time": create_utc_time()}}, @@ -246,7 +299,18 @@ def deleteClub(clubInput: SimpleClubInput, info: Info) -> SimpleClubType: @strawberry.mutation def restartClub(clubInput: SimpleClubInput, info: Info) -> SimpleClubType: """ - Mutation for cc to move a club from deleted state to asctive state. + Mutation for cc to move a club from deleted state to active state. + + Args: + clubInput (SimpleClubInput): The club cid. + info (Info): User metadata and cookies. + + Returns: + SimpleClubType: Details of the restarted clubs cid. + + Raises: + Exception: Not Authenticated. + Exception: Not Authenticated to access this API. """ user = info.context.user if user is None: @@ -258,6 +322,7 @@ def restartClub(clubInput: SimpleClubInput, info: Info) -> SimpleClubType: if role not in ["cc"]: raise Exception("Not Authenticated to access this API") + # also autofills the updated time clubsdb.update_one( {"cid": club_input["cid"]}, {"$set": {"state": "active", "updated_time": create_utc_time()}}, diff --git a/otypes.py b/otypes.py index c6e0e3b..042bfda 100644 --- a/otypes.py +++ b/otypes.py @@ -1,3 +1,7 @@ +""" +Types and Inputs for clubs subgraph +""" + import json from functools import cached_property from typing import Dict, List, Optional, Union @@ -12,6 +16,10 @@ # custom context class class Context(BaseContext): + """ + Class provides user metadata and cookies from request headers, has methods for doing this. + """ + @cached_property def user(self) -> Union[Dict, None]: if not self.request: @@ -29,10 +37,10 @@ def cookies(self) -> Union[Dict, None]: return cookies -# custom info type +"""custom info Type for user metadata""" Info = _Info[Context, RootValueType] -# serialize PyObjectId as a scalar type +"""A scalar Type for serializing PyObjectId, used for id field""" PyObjectIdType = strawberry.scalar( PyObjectId, serialize=str, parse_value=lambda v: PyObjectId(v) ) @@ -41,6 +49,10 @@ def cookies(self) -> Union[Dict, None]: # TYPES @strawberry.experimental.pydantic.type(model=Social) class SocialsType: + """ + Type used for return of social media handles of a club. + """ + website: Optional[str] = strawberry.UNSET instagram: Optional[str] = strawberry.UNSET facebook: Optional[str] = strawberry.UNSET @@ -69,6 +81,10 @@ class SocialsType: ], ) class SimpleClubType: + """ + Type used for return of user-provided club details except social handles. + """ + pass @@ -91,6 +107,10 @@ class SimpleClubType: ], ) class FullClubType: + """ + Type used for return of all user-provided club details. + """ + # socials: SocialsType pass @@ -98,11 +118,19 @@ class FullClubType: # CLUBS INPUTS @strawberry.experimental.pydantic.input(model=Social, all_fields=True) class SocialsInput: + """ + Input used for input of social media handles of a club. + """ + pass @strawberry.input class SimpleClubInput: + """ + Input used for input of cid(Club id) of a club. + """ + cid: str @@ -120,6 +148,10 @@ class SimpleClubInput: ], ) class FullClubInput: + """ + Input used for input of all user-provided club details, pictures are optional to fill. + """ + logo: Optional[str] = strawberry.UNSET banner: Optional[str] = strawberry.UNSET banner_square: Optional[str] = strawberry.UNSET diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml new file mode 100644 index 0000000..e4d523b --- /dev/null +++ b/pydoc-markdown.yml @@ -0,0 +1,36 @@ +loaders: +- type: python + search_path: [.] +renderer: + type: hugo + get_hugo: + enabled: true + version: '0.111' + extended: true + config: + title: Clubs + theme: {clone_url: "https://github.com/alex-shpak/hugo-book.git"} + # The "book" theme only renders pages in "content/docs" into the nav. + content_directory: content/docs + default_preamble: {menu: main} + pages: + - title: Home + name: index + source: README.md + - title: Main + contents: + - 'main' + - 'db' + - title: Models + contents: + - 'models' + - 'otypes' + - title: Mutations + contents: + - 'mutations' + - title: Queries + contents: + - 'queries' + - title: Utils + contents: + - 'utils' diff --git a/queries.py b/queries.py index 2c46a23..3592cd2 100644 --- a/queries.py +++ b/queries.py @@ -1,3 +1,7 @@ +""" +Queries for Clubs +""" + from typing import List import strawberry @@ -14,10 +18,13 @@ @strawberry.field def activeClubs(info: Info) -> List[SimpleClubType]: """ - Description: Returns all the currently active clubs. - Scope: Public - Return Type: List[SimpleClubType] - Input: None + Fetches all the currently active clubs and is accessible to all. + + Args: + info (Info): User metadata and cookies. + + Returns: + List[SimpleClubType]: List of active clubs. """ results = clubsdb.find({"state": "active"}, {"_id": 0}) clubs = [ @@ -31,14 +38,17 @@ def activeClubs(info: Info) -> List[SimpleClubType]: @strawberry.field def allClubs(info: Info) -> List[SimpleClubType]: """ - Description: - For CC: - Returns all the currently active/deleted clubs. - For Public: - Returns all the currently active clubs. - Scope: CC (For All Clubs), Public (For Active Clubs) - Return Type: List[SimpleClubType] - Input: None + Fetches all the clubs + + This method returns all the clubs when accessed CC. + When accessed by public, it returns only the active clubs. + Access to both public and CC(Clubs Council). + + Args: + info (Info): User metadata and cookies. + + Returns: + List[SimpleClubType]: List of all clubs. """ user = info.context.user if user is None: @@ -62,17 +72,22 @@ def allClubs(info: Info) -> List[SimpleClubType]: @strawberry.field def club(clubInput: SimpleClubInput, info: Info) -> FullClubType: """ - Description: Returns complete details of the club, from its club-id. - For CC: - Returns details even for deleted clubs. - For Public: - Returns details only for the active clubs. - Scope: Public - Return Type: FullClubType - Input: SimpleClubInput (cid) - Errors: - - cid doesn't exist - - if not cc and club is deleted + Fetches all Club Details of a club + + Used to get all the details of a deleted/active club by its cid. + Returns deleted clubs also for CC and not for public. + Accessible to both public and CC(Clubs Council). + + Args: + clubInput (SimpleClubInput): The club cid. + info (Info): User metadata and cookies. + + Returns: + FullClubType: Contains all the club details. + + Raises: + Exception: If the club is not found. + Exception: If the club is deleted and the user is not CC. """ user = info.context.user club_input = jsonable_encoder(clubInput) diff --git a/requirements.txt b/requirements.txt index 2da3a8b..8820073 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,3 +28,4 @@ tomli==2.1.0 typer==0.13.1 typing-extensions>=4.12.2 uvicorn==0.32.1 +pydoc-markdown==4.8.2 diff --git a/utils.py b/utils.py index fe64d10..5d12500 100644 --- a/utils.py +++ b/utils.py @@ -8,6 +8,17 @@ def update_role(uid, cookies=None, role="club"): """ Function to call the updateRole mutation + + Makes a mutation resolved by the `updateRole` method from Users Microservice. + Used to change a user's role field. + + Args: + uid (str): User ID. + cookies (dict): Cookies from the request. Defaults to None. + role (str): Role of the user to be updated to. Defaults to 'club'. + + Returns: + dict: Response from the mutation. """ try: query = """ @@ -42,8 +53,21 @@ def update_role(uid, cookies=None, role="club"): def update_events_members_cid(old_cid, new_cid, cookies=None) -> bool: """ Function to call the updateEventsCid & updateMembersCid mutation + + Makes a mutation resolved by the `updateEventsCid` method from Events Microservice. + Makes another mutation resolved by the `updateMembersCid` method from Members Microservice. + Used when club changes its cid to change its members and events data accordingly. + + Args: + old_cid (str): Old CID of the club. + new_cid (str): New CID of the club. + cookies (dict): Cookies from the request. Defaults to None. + + Returns: + bool: True if both mutations are successful, False otherwise. """ return1, return2 = None, None + # Update Events CID try: query = """ mutation UpdateEventsCid($oldCid: String!, $newCid: String!, $interCommunicationSecret: String) { @@ -71,6 +95,7 @@ def update_events_members_cid(old_cid, new_cid, cookies=None) -> bool: except Exception: return False + # Update Members CID try: query = """ mutation UpdateMembersCid($oldCid: String!, $newCid: String!, $interCommunicationSecret: String) { @@ -107,6 +132,16 @@ def update_events_members_cid(old_cid, new_cid, cookies=None) -> bool: def getUser(uid, cookies=None): """ Function to get a particular user details + + Makes a query resolved by the `userProfile` method from Users Microservice. + Used to get a users details. + + Args: + uid (str): User ID of the user to be fetched. + cookies (dict): Cookies from the request. Defaults to None. + + Returns: + dict: User details as a result of the query. """ try: query = """ @@ -138,6 +173,15 @@ def getUser(uid, cookies=None): def delete_file(filename): + """ + Method for deleting a file from the files microservice + + Args: + filename (str): Name of the file to be deleted + + Returns: + Response from the files microservice + """ response = requests.post( "http://files/delete-file", params={ @@ -153,6 +197,17 @@ def delete_file(filename): def check_remove_old_file(old_obj, new_obj, name="logo"): + """ + Method to remove old files. + + Args: + old_obj (dict): Old object containing the old file + new_obj (dict): New object containing the new file + name (str): Name of the file to be removed. Defaults to "logo" as mostly they are images of club logo's + + Returns: + bool: True if the old file is removed, False otherwise + """ old_file = old_obj.get(name) new_file = new_obj.get(name)