diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml new file mode 100644 index 0000000..f7548d9 --- /dev/null +++ b/.github/workflows/formatting.yml @@ -0,0 +1,44 @@ +name: Auto Linting and Formatting + +on: + push: + branches: + - master + +jobs: + linting: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + + - name: Lint with ruff + run: ruff check --fix *.py + + - name: Format with ruff + run: ruff format *.py + + - name: Remove ruff cache + run: rm -rf .ruff_cache + + - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: Apply Linting & Formatting Fixes + + # - name: Remove Linting Branch + # run: | + # if git rev-parse --verify linting >/dev/null 2>&1; then + # git push origin --delete linting + # fi + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f51ac09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.venv/ +.vscode/ +.in +.out +__pycache__/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..327772f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# cache dependencies +FROM python:3.12 AS python_cache +ENV VIRTUAL_ENV=/venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" +WORKDIR /cache/ +COPY requirements.txt . +RUN python -m venv /venv +RUN pip install -r requirements.txt + +# build and start +FROM python:3.12-slim AS build +EXPOSE 80 +WORKDIR /app +ENV VIRTUAL_ENV=/venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" +COPY --from=python_cache /venv /venv +COPY . . +RUN strawberry export-schema main > schema.graphql +ENTRYPOINT [ "./entrypoint.sh" ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..68cca9f --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# _MEMBERS_ MICRO-SERVICE + +This is a microservice using FastAPI + Strawberry + MongoDB. +Contains code for queries, mutations, types and models for the members data and services specifically. + +## How to use +1. Go to [https://github.com/Clubs-Council-IIITH/services](https://github.com/Clubs-Council-IIITH/services) +2. Follow the steps given in the README.md file at that page. + +--- + +## FOR DEVELOPERS +_URL_ -> http://members/graphql (For using in single gateway) + +> ### QUERIES + +> ### MUTATIONS diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/db.py b/db.py new file mode 100644 index 0000000..f71da0b --- /dev/null +++ b/db.py @@ -0,0 +1,34 @@ +from os import getenv + +from pymongo import MongoClient + + +# get mongodb URI and database name from environment variale +MONGO_URI = "mongodb://{}:{}@mongo:{}/".format( + getenv("MONGO_USERNAME", default="username"), + getenv("MONGO_PASSWORD", default="password"), + getenv("MONGO_PORT", default="27017"), +) +MONGO_DATABASE = getenv("MONGO_DATABASE", default="default") + +# instantiate mongo client +client = MongoClient(MONGO_URI) + +# get database +db = client[MONGO_DATABASE] +membersdb = db.members + +try: + # check if the members index exists + if "unique_members" in membersdb.index_information(): + print("The members index exists.") + else: + # create the index + membersdb.create_index( + [("cid", 1), ("uid", 1)], unique=True, name="unique_members" + ) + print("The members index was created.") + + print(membersdb.index_information()) +except Exception: + pass diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..6b75b7e --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +cp ./schema.graphql /subgraphs/clubs.graphql +uvicorn main:app --host 0.0.0.0 --port 80 diff --git a/main.py b/main.py new file mode 100644 index 0000000..701d4af --- /dev/null +++ b/main.py @@ -0,0 +1,50 @@ +import strawberry +from strawberry.tools import create_type +from strawberry.fastapi import GraphQLRouter + +from fastapi import FastAPI + +from os import getenv + +# override PyObjectId and Context scalars +from models import PyObjectId +from otypes import Context, PyObjectIdType + +# import all queries and mutations +from queries import queries +from mutations import mutations + + +# check whether running in debug mode +DEBUG = int(getenv("GLOBAL_DEBUG", 0)) + +# create query types +Query = create_type("Query", queries) + +# create mutation types +Mutation = create_type("Mutation", mutations) + + +# override context getter +async def get_context() -> Context: + return Context() + + +# initialize federated schema +schema = strawberry.federation.Schema( + query=Query, + mutation=Mutation, + enable_federation_2=True, + scalar_overrides={PyObjectId: PyObjectIdType}, +) + +DEBUG = getenv("SERVICES_DEBUG", "False").lower() in ("true", "1", "t") + +# serve API with FastAPI router +gql_app = GraphQLRouter(schema, graphiql=True, context_getter=get_context) +app = FastAPI( + debug=DEBUG, + title="CC Clubs Microservice", + desciption="Handles Data of Clubs and Members", +) +app.include_router(gql_app, prefix="/graphql") diff --git a/models.py b/models.py new file mode 100644 index 0000000..d27823d --- /dev/null +++ b/models.py @@ -0,0 +1,102 @@ +from bson import ObjectId +from pydantic import ( + BaseModel, + ConfigDict, + Field, + field_validator, + ValidationInfo, +) +from pydantic_core import core_schema +from typing import Any, List + + +# for handling mongo ObjectIds +class PyObjectId(ObjectId): + @classmethod + def __get_pydantic_core_schema__(cls, source_type: Any, handler): + return core_schema.union_schema( + [ + # check if it's an instance first before doing any further work + core_schema.is_instance_schema(ObjectId), + core_schema.no_info_plain_validator_function(cls.validate), + ], + serialization=core_schema.to_string_ser_schema(), + ) + + @classmethod + def validate(cls, v): + if not ObjectId.is_valid(v): + raise ValueError("Invalid ObjectId") + return ObjectId(v) + + @classmethod + def __get_pydantic_json_schema__(cls, field_schema): + field_schema.update(type="string") + + +class Roles(BaseModel): + 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) + end_year: int | None = Field(None, gt=2010, le=2051) + approved: bool = False + rejected: bool = False + deleted: bool = False + + # Validators + @field_validator("end_year") + def check_end_year(cls, value, info: ValidationInfo): + if value is not None and value < info.data["start_year"]: + return None + return value + + @field_validator("rejected") + def check_status(cls, value, info: ValidationInfo): + if info.data["approved"] is True and value is True: + raise ValueError("Role cannot be both approved and rejected") + return value + + model_config = ConfigDict( + arbitrary_types_allowed=True, + str_max_length=100, + validate_assignment=True, + validate_default=True, + validate_return=True, + extra="forbid", + str_strip_whitespace=True, + ) + + +class Member(BaseModel): + id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") + cid: str = Field(..., description="Club ID") + uid: str = Field(..., description="User ID") + roles: List[Roles] = Field( + ..., description="List of Roles for that specific person" + ) + + poc: bool = Field(default_factory=(lambda: 0 == 1), description="Club POC") + + @field_validator("uid", mode="before") + @classmethod + def transform_uid(cls, v): + return v.lower() + + # TODO[pydantic]: The following keys were removed: `json_encoders`. + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. + model_config = ConfigDict( + arbitrary_types_allowed=True, + str_strip_whitespace=True, + str_max_length=600, + validate_assignment=True, + validate_default=True, + validate_return=True, + extra="forbid", + json_encoders={ObjectId: str}, + populate_by_name=True, + ) + + # Separate Coordinator & other members roles option in frontend, for better filtering for all_members_query + + +# TODO: ADD Descriptions for non-direct fields diff --git a/mutations.py b/mutations.py new file mode 100644 index 0000000..5a80ba6 --- /dev/null +++ b/mutations.py @@ -0,0 +1,392 @@ +import strawberry + +from fastapi.encoders import jsonable_encoder +from datetime import datetime +from os import getenv + +from db import membersdb +from utils import unique_roles_id, non_deleted_members + +# import all models and types +from otypes import Info +from models import Member +from otypes import FullMemberInput, SimpleMemberInput, MemberType +from utils import getUser + +inter_communication_secret_global = getenv("INTER_COMMUNICATION_SECRET") + +@strawberry.mutation +def createMember(memberInput: FullMemberInput, info: Info) -> MemberType: + """ + Mutation to create a new member by that specific 'club' or cc + """ + user = info.context.user + if user is None: + raise Exception("Not Authenticated") + + role = user["role"] + uid = user["uid"] + member_input = jsonable_encoder(memberInput.to_pydantic()) + + if (member_input["cid"] != uid or user["role"] != "club") and user["role"] != "cc": + raise Exception("Not Authenticated to access this API") + + if membersdb.find_one( + { + "$and": [ + {"cid": member_input["cid"]}, + {"uid": member_input["uid"]}, + ] + } + ): + raise Exception("A record with same uid and cid already exists") + + # Check whether this uid is valid or not + userMember = getUser(member_input["uid"], info.context.cookies) + if userMember is None: + raise Exception("Invalid User ID") + + if len(member_input["roles"]) == 0: + raise Exception("Roles cannot be empty") + + for i in member_input["roles"]: + if i["end_year"] and i["start_year"] > i["end_year"]: + raise Exception("Start year cannot be greater than end year") + + roles0 = [] + for role in member_input["roles"]: + if role["start_year"] > datetime.now().year: + role["start_year"] = datetime.now().year + role["end_year"] = None + roles0.append(role) + + roles = [] + for role in roles0: + role["approved"] = user["role"] == "cc" + roles.append(role) + + member_input["roles"] = roles + + # DB STUFF + created_id = membersdb.insert_one(member_input).inserted_id + unique_roles_id(member_input["uid"], member_input["cid"]) + + created_sample = Member.parse_obj( + membersdb.find_one({"_id": created_id}, {"_id": 0}) + ) + + return MemberType.from_pydantic(created_sample) + + +@strawberry.mutation +def editMember(memberInput: FullMemberInput, info: Info) -> MemberType: + """ + Mutation to edit an already existing member+roles of that specific 'club' + """ + user = info.context.user + if user is None: + raise Exception("Not Authenticated") + + uid = user["uid"] + member_input = jsonable_encoder(memberInput.to_pydantic()) + + if (member_input["cid"] != uid or user["role"] != "club") and user["role"] != "cc": + raise Exception("Not Authenticated to access this API") + + if len(member_input["roles"]) == 0: + raise Exception("Roles cannot be empty") + + for i in member_input["roles"]: + if i["end_year"] and i["start_year"] > i["end_year"]: + raise Exception("Start year cannot be greater than end year") + + member_ref = membersdb.find_one( + { + "$and": [ + {"cid": member_input["cid"]}, + {"uid": member_input["uid"]}, + ] + } + ) + + if member_ref is None: + raise Exception("No such Record!") + else: + member_ref = Member.parse_obj(member_ref) + + member_roles = member_ref.roles + + roles = [] + for role in member_input["roles"]: + if role["start_year"] > datetime.now().year: + role["start_year"] = datetime.now().year + role["end_year"] = None + role_new = role.copy() + + # if role's start_year, end_year, name is same as existing role, then keep the existing approved status + found_existing_role = False + for i in member_roles: + if ( + i.start_year == role_new["start_year"] + and i.end_year == role_new["end_year"] + and i.name == role_new["name"] + ): + role_new["approved"] = i.approved + role_new["rejected"] = i.rejected + role_new["deleted"] = i.deleted + + found_existing_role = True + + # Remove the existing role from member_roles + member_roles.remove(i) + break + + if not found_existing_role: + role_new["approved"] = user["role"] == "cc" + roles.append(role_new) + + # DB STUFF + membersdb.update_one( + { + "$and": [ + {"cid": member_input["cid"]}, + {"uid": member_input["uid"]}, + ] + }, + {"$set": {"roles": roles, "poc": member_input["poc"]}}, + ) + + unique_roles_id(member_input["uid"], member_input["cid"]) + + return non_deleted_members(member_input) + + +@strawberry.mutation +def deleteMember(memberInput: SimpleMemberInput, info: Info) -> MemberType: + """ + Mutation to delete an already existing member (role) of that specific 'club' + """ + user = info.context.user + if user is None: + raise Exception("Not Authenticated") + + uid = user["uid"] + member_input = jsonable_encoder(memberInput) + + if (member_input["cid"] != uid or user["role"] != "club") and user["role"] != "cc": + raise Exception("Not Authenticated to access this API") + + existing_data = membersdb.find_one( + { + "$and": [ + {"cid": member_input["cid"]}, + {"uid": member_input["uid"]}, + ] + }, + {"_id": 0}, + ) + if existing_data is None: + raise Exception("No such Record") + + if "rid" not in member_input or not member_input["rid"]: + membersdb.delete_one( + { + "$and": [ + {"cid": member_input["cid"]}, + {"uid": member_input["uid"]}, + ] + } + ) + + return MemberType.from_pydantic(Member.parse_obj(existing_data)) + + roles = [] + for i in existing_data["roles"]: + if i["rid"] == member_input["rid"]: + i["deleted"] = True + roles.append(i) + + # DB STUFF + membersdb.update_one( + { + "$and": [ + {"cid": member_input["cid"]}, + {"uid": member_input["uid"]}, + ] + }, + {"$set": {"roles": roles}}, + ) + + unique_roles_id(member_input["uid"], member_input["cid"]) + + return non_deleted_members(member_input) + + +@strawberry.mutation +def approveMember(memberInput: SimpleMemberInput, info: Info) -> MemberType: + """ + Mutation to approve a member role by 'cc' + """ + user = info.context.user + if user is None: + raise Exception("Not Authenticated") + + member_input = jsonable_encoder(memberInput) + + if user["role"] != "cc": + raise Exception("Not Authenticated to access this API") + + existing_data = membersdb.find_one( + { + "$and": [ + {"cid": member_input["cid"]}, + {"uid": member_input["uid"]}, + ] + }, + {"_id": 0}, + ) + if existing_data is None: + raise Exception("No such Record") + + # if "rid" not in member_input: + # raise Exception("rid is required") + + roles = [] + for i in existing_data["roles"]: + if not member_input["rid"] or i["rid"] == member_input["rid"]: + i["approved"] = True + i["rejected"] = False + roles.append(i) + + # DB STUFF + membersdb.update_one( + { + "$and": [ + {"cid": member_input["cid"]}, + {"uid": member_input["uid"]}, + ] + }, + {"$set": {"roles": roles}}, + ) + + unique_roles_id(member_input["uid"], member_input["cid"]) + + return non_deleted_members(member_input) + + +@strawberry.mutation +def rejectMember(memberInput: SimpleMemberInput, info: Info) -> MemberType: + """ + Mutation to reject a member role by 'cc' + """ + user = info.context.user + if user is None: + raise Exception("Not Authenticated") + + member_input = jsonable_encoder(memberInput) + + if user["role"] != "cc": + raise Exception("Not Authenticated to access this API") + + existing_data = membersdb.find_one( + { + "$and": [ + {"cid": member_input["cid"]}, + {"uid": member_input["uid"]}, + ] + }, + {"_id": 0}, + ) + if existing_data is None: + raise Exception("No such Record") + + # if "rid" not in member_input: + # raise Exception("rid is required") + + roles = [] + for i in existing_data["roles"]: + if not member_input["rid"] or i["rid"] == member_input["rid"]: + i["approved"] = False + i["rejected"] = True + roles.append(i) + + # DB STUFF + membersdb.update_one( + { + "$and": [ + {"cid": member_input["cid"]}, + {"uid": member_input["uid"]}, + ] + }, + {"$set": {"roles": roles}}, + ) + + unique_roles_id(member_input["uid"], member_input["cid"]) + + return non_deleted_members(member_input) + + +# @strawberry.mutation +# def leaveClubMember(memberInput: SimpleMemberInput, info: Info) -> MemberType: +# user = info.context.user +# if user is None: +# raise Exception("Not Authenticated") + +# role = user["role"] +# uid = user["uid"] +# member_input = jsonable_encoder(memberInput.to_pydantic()) + +# if member_input["cid"] != uid and role != "club": +# raise Exception("Not Authenticated to access this API") + +# created_id = clubsdb.update_one( +# { +# "$and": [ +# {"cid": member_input["cid"]}, +# {"uid": member_input["uid"]}, +# {"start_year": member_input["start_year"]}, +# {"deleted": False}, +# ] +# }, +# {"$set": {"end_year": datetime.now().year}}, +# ) + +# created_sample = Member.parse_obj(membersdb.find_one({"_id": created_id})) +# return MemberType.from_pydantic(created_sample) + +@strawberry.mutation +def updateMembersCid( + info: Info, + old_cid: str, + new_cid: str, + inter_communication_secret: str | None = None, +) -> int: + """ + update all memberd of old_cid to new_cid + """ + user = info.context.user + + if user is None or user["role"] not in ["cc"]: + raise Exception("Not Authenticated!") + + if inter_communication_secret != inter_communication_secret_global: + raise Exception("Authentication Error! Invalid secret!") + + updation = { + "$set": { + "cid": new_cid, + } + } + + upd_ref = membersdb.update_many({"cid": old_cid}, updation) + return upd_ref.modified_count + +# register all mutations +mutations = [ + createMember, + editMember, + deleteMember, + approveMember, + rejectMember, + updateMembersCid, +] diff --git a/otypes.py b/otypes.py new file mode 100644 index 0000000..6807450 --- /dev/null +++ b/otypes.py @@ -0,0 +1,77 @@ +import json +import strawberry + +from strawberry.fastapi import BaseContext +from strawberry.types import Info as _Info +from strawberry.types.info import RootValueType + +from typing import Union, Dict, Optional +from functools import cached_property + +from models import PyObjectId, Member, Roles + + +# custom context class +class Context(BaseContext): + @cached_property + def user(self) -> Union[Dict, None]: + if not self.request: + return None + + user = json.loads(self.request.headers.get("user", "{}")) + return user + + @cached_property + def cookies(self) -> Union[Dict, None]: + if not self.request: + return None + + cookies = json.loads(self.request.headers.get("cookies", "{}")) + return cookies + + +# custom info type +Info = _Info[Context, RootValueType] + +# serialize PyObjectId as a scalar type +PyObjectIdType = strawberry.scalar( + PyObjectId, serialize=str, parse_value=lambda v: PyObjectId(v) +) + + +# TYPES +@strawberry.experimental.pydantic.type(model=Roles, all_fields=True) +class RolesType: + pass + + +@strawberry.experimental.pydantic.type( + model=Member, fields=["id", "cid", "uid", "roles", "poc"] +) +class MemberType: + pass + + +# INPUTS +@strawberry.experimental.pydantic.input( + model=Roles, fields=["name", "start_year", "end_year"] +) +class RolesInput: + pass + + +@strawberry.experimental.pydantic.input(model=Member, fields=["cid", "uid", "roles"]) +class FullMemberInput: + poc: Optional[bool] = strawberry.UNSET + + +@strawberry.input +class SimpleMemberInput: + cid: str + uid: str + rid: Optional[str] + + +@strawberry.input +class SimpleClubInput: + cid: str diff --git a/queries.py b/queries.py new file mode 100644 index 0000000..e55e737 --- /dev/null +++ b/queries.py @@ -0,0 +1,238 @@ +import strawberry + +from fastapi.encoders import jsonable_encoder +from typing import List + +from db import membersdb + +# import all models and types +from otypes import Info + +from models import Member +from otypes import SimpleClubInput, SimpleMemberInput +from otypes import MemberType + +""" +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) + """ + user = info.context.user + if user is None: + raise Exception("Not Authenticated") + + uid = user["uid"] + member_input = jsonable_encoder(memberInput) + + if (member_input["cid"] != uid or user["role"] != "club") and user["role"] != "cc": + raise Exception("Not Authenticated to access this API") + + member = membersdb.find_one( + { + "$and": [ + {"cid": member_input["cid"]}, + {"uid": member_input["uid"]}, + ] + }, + {"_id": 0}, + ) + if member is None: + raise Exception("No such Record") + + return MemberType.from_pydantic(Member.parse_obj(member)) + + +@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) + """ + user = info.context.user + if user is None: + role = "public" + else: + role = user["role"] + + results = membersdb.find({"uid": uid}, {"_id": 0}) + + if not results: + raise Exception("No Member Result/s Found") + + members = [] + for result in results: + roles = result["roles"] + roles_result = [] + + for i in roles: + if i["deleted"] is True: + continue + if role != "cc": + if i["approved"] is False: + continue + roles_result.append(i) + + if len(roles_result) > 0: + result["roles"] = roles_result + members.append(MemberType.from_pydantic(Member.parse_obj(result))) + + return members + + +@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) + """ + user = info.context.user + if user is None: + role = "public" + else: + role = user["role"] + + club_input = jsonable_encoder(clubInput) + + if role not in ["cc"] or club_input["cid"] != "clubs": + results = membersdb.find({"cid": club_input["cid"]}, {"_id": 0}) + else: + results = membersdb.find({}, {"_id": 0}) + + if results: + members = [] + for result in results: + roles = result["roles"] + roles_result = [] + + for i in roles: + if i["deleted"] is True: + continue + if not ( + role in ["cc"] + or (role in ["club"] and user["uid"] == club_input["cid"]) + ): + if i["approved"] is False: + continue + roles_result.append(i) + + if len(roles_result) > 0: + result["roles"] = roles_result + members.append(MemberType.from_pydantic(Member.parse_obj(result))) + + return members + + else: + raise Exception("No Member Result/s Found") + + +@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. + + Scope: Anyone (Non-Admin Function) + Return Type: List[MemberType] + Input: SimpleClubInput (cid) + """ + user = info.context.user + if user is None: + role = "public" + else: + role = user["role"] + + club_input = jsonable_encoder(clubInput) + + if club_input["cid"] == "clubs": + if role != "cc": + raise Exception("Not Authenticated") + + results = membersdb.find({}, {"_id": 0}) + else: + results = membersdb.find({"cid": club_input["cid"]}, {"_id": 0}) + + if results: + members = [] + for result in results: + roles = result["roles"] + roles_result = [] + + for i in roles: + if i["deleted"] is True or i["end_year"] is not None: + continue + if i["approved"] is False: + continue + roles_result.append(i) + + if len(roles_result) > 0: + result["roles"] = roles_result + members.append(MemberType.from_pydantic(Member.parse_obj(result))) + + return members + else: + raise Exception("No Member Result/s Found") + + +@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 + """ + user = info.context.user + if user is None or user["role"] not in ["cc"]: + raise Exception("Not Authenticated") + + results = membersdb.find({}, {"_id": 0}) + + if results: + members = [] + for result in results: + roles = result["roles"] + roles_result = [] + + for i in roles: + if i["deleted"] or i["approved"] or i["rejected"]: + continue + roles_result.append(i) + + if len(roles_result) > 0: + result["roles"] = roles_result + members.append(MemberType.from_pydantic(Member.parse_obj(result))) + + return members + else: + raise Exception("No Member Result/s Found") + + +# register all queries +queries = [ + member, + memberRoles, + members, + currentMembers, + pendingMembers, +] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b3ae782 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,28 @@ +anyio>=3.7.1,<4.0.0 +click==8.1.7 +dnspython==2.5.0 +fastapi==0.109.2 +graphql-core==3.2.3 +greenlet==3.0.3 +h11==0.14.0 +idna==3.6 +libcst==1.1.0 +msgpack==1.0.7 +mypy-extensions==1.0.0 +pathspec==0.12.1 +platformdirs==4.2.0 +pydantic>=2.6.0,<3.0.0 +Pygments==2.17.2 +pymongo==4.6.1 +python-dateutil==2.8.2 +python-multipart >=0.0.7 +requests>=2.31.0 +rich==13.7.0 +six==1.16.0 +sniffio==1.3.0 +starlette>=0.36.3,<0.37.0 +strawberry-graphql[fastapi]==0.219.2 +tomli==2.0.1 +typer==0.9.0 +typing-extensions>=4.9.0 +uvicorn==0.27.1 diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..bc23d07 --- /dev/null +++ b/utils.py @@ -0,0 +1,110 @@ +import requests +from datetime import datetime +import os + +from db import membersdb + +from models import Member +from otypes import MemberType + +inter_communication_secret = os.getenv("INTER_COMMUNICATION_SECRET") + + +def non_deleted_members(member_input) -> MemberType: + """ + Function to return non-deleted members for a particular cid, uid + Only to be used in admin functions, as it returns both approved/non-approved members. + """ + updated_sample = membersdb.find_one( + { + "$and": [ + {"cid": member_input["cid"]}, + {"uid": member_input["uid"]}, + ] + }, + {"_id": 0}, + ) + if updated_sample is None: + raise Exception("No such Record") + + roles = [] + for i in updated_sample["roles"]: + if i["deleted"] is True: + continue + roles.append(i) + updated_sample["roles"] = roles + + return MemberType.from_pydantic(Member.parse_obj(updated_sample)) + + +def unique_roles_id(uid, cid): + """ + Function to give unique ids for each of the role in roles list + """ + pipeline = [ + { + "$set": { + "roles": { + "$map": { + "input": {"$range": [0, {"$size": "$roles"}]}, + "in": { + "$mergeObjects": [ + {"$arrayElemAt": ["$roles", "$$this"]}, + { + "rid": { + "$toString": { + "$add": [ + {"$toLong": datetime.now()}, + "$$this", + ] + } + } + }, + ] + }, + } + } + } + } + ] + membersdb.update_one( + { + "$and": [ + {"cid": cid}, + {"uid": uid}, + ] + }, + pipeline, + ) + + +def getUser(uid, cookies=None): + """ + Function to get a particular user details + """ + try: + query = """ + query GetUserProfile($userInput: UserInput!) { + userProfile(userInput: $userInput) { + firstName + lastName + email + rollno + } + } + """ + variable = {"userInput": {"uid": uid}} + if cookies: + request = requests.post( + "http://gateway/graphql", + json={"query": query, "variables": variable}, + cookies=cookies, + ) + else: + request = requests.post( + "http://gateway/graphql", json={"query": query, "variables": variable} + ) + + return request.json()["data"]["userProfile"] + except Exception: + return None