From d1a7973384d74ce24b9430d1e2bdf9623c013f19 Mon Sep 17 00:00:00 2001 From: Bhav Beri Date: Thu, 16 May 2024 22:36:43 +0530 Subject: [PATCH] Separated out Members from clubs microservice --- .github/workflows/formatting.yml | 44 ++++ .gitignore | 5 + Dockerfile | 19 ++ README.md | 17 ++ __init__.py | 0 db.py | 34 +++ entrypoint.sh | 4 + main.py | 50 ++++ models.py | 102 ++++++++ mutations.py | 392 +++++++++++++++++++++++++++++++ otypes.py | 77 ++++++ queries.py | 238 +++++++++++++++++++ requirements.txt | 28 +++ utils.py | 110 +++++++++ 14 files changed, 1120 insertions(+) create mode 100644 .github/workflows/formatting.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 __init__.py create mode 100644 db.py create mode 100755 entrypoint.sh create mode 100644 main.py create mode 100644 models.py create mode 100644 mutations.py create mode 100644 otypes.py create mode 100644 queries.py create mode 100644 requirements.txt create mode 100644 utils.py 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