From 286dce257306b805307f8a2e6cef0bfeb25daa3c Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 29 Nov 2023 19:48:56 -0700 Subject: [PATCH 01/33] feat: Extended workspace v1 API Added endpoints to get, update, and delete polls --- src/unipoll_api/routes/v1/workspace.py | 58 +++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/src/unipoll_api/routes/v1/workspace.py b/src/unipoll_api/routes/v1/workspace.py index 0fe83bd..fa553fb 100644 --- a/src/unipoll_api/routes/v1/workspace.py +++ b/src/unipoll_api/routes/v1/workspace.py @@ -4,7 +4,7 @@ from unipoll_api import dependencies as Dependencies from unipoll_api import actions from unipoll_api.exceptions.resource import APIException -from unipoll_api.documents import Workspace, ResourceID, Policy, Member +from unipoll_api.documents import Poll, Workspace, ResourceID, Policy, Member from unipoll_api.schemas import WorkspaceSchemas, PolicySchemas, GroupSchemas, MemberSchemas, PollSchemas @@ -281,6 +281,62 @@ async def create_poll(workspace: Workspace = Depends(Dependencies.get_workspace) raise HTTPException(status_code=e.code, detail=str(e)) +# Get a poll in the workspace +@router.get("/{workspace_id}/polls/{poll_id}", + tags=["Polls"], + response_description="Poll data", + response_model=PollSchemas.PollResponse) +async def get_poll(workspace: Workspace = Depends(Dependencies.get_workspace), + poll: Poll = Depends(Dependencies.get_poll), + include: Annotated[list[Literal["all", "policies", "questions"]] | None, Query()] = None): + try: + params = {} + if include: + if "all" in include: + params = {"include_groups": True, + "include_members": True, + "include_policies": True, + "include_polls": True} + else: + if "groups" in include: + params["include_groups"] = True + if "members" in include: + params["include_members"] = True + if "policies" in include: + params["include_policies"] = True + if "polls" in include: + params["include_polls"] = True + return await actions.PollActions.get_poll(poll, **params) + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) + + +# Update a poll in the workspace +@router.patch("/{workspace_id}/polls/{poll_id}", + tags=["Polls"], + response_description="Updated poll", + response_model=PollSchemas.PollResponse) +async def update_poll(poll: Poll = Depends(Dependencies.get_poll), + input_data: PollSchemas.UpdatePollRequest = Body(...)): + try: + return await actions.PollActions.update_poll(poll, input_data) + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) + + +# Delete a poll in the workspace +@router.delete("/{workspace_id}/polls/{poll_id}", + tags=["Polls"], + response_description="Deleted poll", + status_code=204) +async def delete_poll(poll: Poll = Depends(Dependencies.get_poll)): + try: + await actions.PollActions.delete_poll(poll) + return status.HTTP_204_NO_CONTENT + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) + + # List all groups in the workspace @router.get("/{workspace_id}/groups", tags=["Groups"], From 2f19afd666d0bcab513121637a3cdebf41e92151 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 29 Nov 2023 19:49:52 -0700 Subject: [PATCH 02/33] refactor: Removed endpoints to get permissions from v1 API --- src/unipoll_api/routes/v1/group.py | 18 +++++++++--------- src/unipoll_api/routes/v1/workspace.py | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/unipoll_api/routes/v1/group.py b/src/unipoll_api/routes/v1/group.py index 07f1c19..080138d 100644 --- a/src/unipoll_api/routes/v1/group.py +++ b/src/unipoll_api/routes/v1/group.py @@ -149,12 +149,12 @@ async def set_group_policy(group: Group = Depends(Dependencies.get_group), # Get All Group Permissions -@router.get("/permissions", - tags=["Groups"], - response_description="List of all Group permissions", - response_model=PolicySchemas.PermissionList) -async def get_group_permissions(): - try: - return await PermissionsActions.get_group_permissions() - except APIException as e: - raise HTTPException(status_code=e.code, detail=str(e)) +# @router.get("/permissions", +# tags=["Groups"], +# response_description="List of all Group permissions", +# response_model=PolicySchemas.PermissionList) +# async def get_group_permissions(): +# try: +# return await PermissionsActions.get_group_permissions() +# except APIException as e: +# raise HTTPException(status_code=e.code, detail=str(e)) diff --git a/src/unipoll_api/routes/v1/workspace.py b/src/unipoll_api/routes/v1/workspace.py index fa553fb..a7ef7ae 100644 --- a/src/unipoll_api/routes/v1/workspace.py +++ b/src/unipoll_api/routes/v1/workspace.py @@ -243,15 +243,15 @@ async def set_workspace_policy(workspace: Workspace = Depends(Dependencies.get_w # Get All Workspace Permissions -@router.get("/permissions", - tags=["Workspaces"], - response_description="List of all workspace permissions", - response_model=PolicySchemas.PermissionList) -async def get_workspace_permissions(): - try: - return await actions.PermissionsActions.get_workspace_permissions() - except APIException as e: - raise HTTPException(status_code=e.code, detail=str(e)) +# @router.get("/permissions", +# tags=["Workspaces"], +# response_description="List of all workspace permissions", +# response_model=PolicySchemas.PermissionList) +# async def get_workspace_permissions(): +# try: +# return await actions.PermissionsActions.get_workspace_permissions() +# except APIException as e: +# raise HTTPException(status_code=e.code, detail=str(e)) # Get Workspace Polls From 38db23c8343ff6b19d8f28d430896f071334a486 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 6 Dec 2023 22:09:05 -0700 Subject: [PATCH 03/33] feat: Added endpoint to get member permissions This route returns list of all permissions a member has in a workspace and permissions for each group in the workspace that a member is a part of --- src/unipoll_api/routes/v2/permissions.py | 35 +++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/unipoll_api/routes/v2/permissions.py b/src/unipoll_api/routes/v2/permissions.py index 55914ba..635be8f 100644 --- a/src/unipoll_api/routes/v2/permissions.py +++ b/src/unipoll_api/routes/v2/permissions.py @@ -1,7 +1,12 @@ -from fastapi import APIRouter, HTTPException +from click import group +from fastapi import APIRouter, HTTPException, Depends +from unipoll_api import dependencies as Dependencies +from unipoll_api.documents import Account, Workspace, Member from unipoll_api.schemas import PolicySchemas from unipoll_api.exceptions.resource import APIException from unipoll_api.actions import PermissionsActions +from unipoll_api.utils import Permissions + router = APIRouter() @@ -26,3 +31,31 @@ async def get_group_permissions(): return await PermissionsActions.get_group_permissions() except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) + + +@router.get("/members/{member_id}", + response_description="List of all member permissions") +async def get_member_permissions(member: Member = Depends(Dependencies.get_member), + workspace: Workspace = Depends(Dependencies.get_workspace)): + try: + workspace_permissions = await Permissions.get_all_permissions(member, workspace) + group_permissions = {} + for group in member.groups: + group_permissions[group.id] = await Permissions.get_all_permissions(member, group) + + return { + "permissions": { + "workspace": { + "id": workspace.id, + "permissions": workspace_permissions, + }, + "groups": [ { + "id": group.id, + "permissions": group_permissions[group.id] + } for group in member.groups + ] + } + } + + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) \ No newline at end of file From 3a2423e7f5813a3bc77b741c2d9a9368cb3cca12 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 6 Dec 2023 22:38:08 -0700 Subject: [PATCH 04/33] feat: Added function to convert Permission type to string list --- src/unipoll_api/utils/permissions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/unipoll_api/utils/permissions.py b/src/unipoll_api/utils/permissions.py index 19c23b3..5c00c3c 100644 --- a/src/unipoll_api/utils/permissions.py +++ b/src/unipoll_api/utils/permissions.py @@ -1,4 +1,5 @@ from enum import IntFlag +import re import unipoll_api from unipoll_api import exceptions @@ -130,6 +131,13 @@ async def get_all_permissions(resource, member) -> Permissions: return permission_sum # type: ignore +def convert_permission_to_string(permissions: Permissions, resource_type) -> list[str]: + # return eval(get_document_type().capitalize() + "Permissions")[string] + print(permissions) + permission_type = PermissionTypes[resource_type] + return permission_type(permissions).name.split('|') # type: ignore + + def convert_string_to_permission(resource_type: str, string: str): try: # return eval(get_document_type().capitalize() + "Permissions")[string] From 6d3147b13c2b930f48342ed3b022ea3e3a782742 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 6 Dec 2023 22:40:31 -0700 Subject: [PATCH 05/33] fix: Fixed get_member_permissions Fixed issues with permissions fetching and formatted the output --- src/unipoll_api/routes/v2/permissions.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/unipoll_api/routes/v2/permissions.py b/src/unipoll_api/routes/v2/permissions.py index 635be8f..aba9c09 100644 --- a/src/unipoll_api/routes/v2/permissions.py +++ b/src/unipoll_api/routes/v2/permissions.py @@ -38,21 +38,23 @@ async def get_group_permissions(): async def get_member_permissions(member: Member = Depends(Dependencies.get_member), workspace: Workspace = Depends(Dependencies.get_workspace)): try: - workspace_permissions = await Permissions.get_all_permissions(member, workspace) + # await member.fetch_all_links() + + workspace_permissions = await Permissions.get_all_permissions(workspace, member) group_permissions = {} - for group in member.groups: - group_permissions[group.id] = await Permissions.get_all_permissions(member, group) + for group in workspace.groups: + group_permissions[group.id] = await Permissions.get_all_permissions(group, member) return { "permissions": { "workspace": { - "id": workspace.id, - "permissions": workspace_permissions, + "id": str(workspace.id), + "permissions": Permissions.convert_permission_to_string(workspace_permissions, "Workspace"), }, "groups": [ { - "id": group.id, - "permissions": group_permissions[group.id] - } for group in member.groups + "id": str(group_id), + "permissions": Permissions.convert_permission_to_string(permissions, "Group") + } for group_id, permissions in group_permissions.items() ] } } From 6f355ea8fd5e2f183b383161ac804eab3b027dcb Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 6 Dec 2023 22:41:16 -0700 Subject: [PATCH 06/33] refactor: Removed debug print statement --- src/unipoll_api/utils/permissions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/unipoll_api/utils/permissions.py b/src/unipoll_api/utils/permissions.py index 5c00c3c..7e5a3d7 100644 --- a/src/unipoll_api/utils/permissions.py +++ b/src/unipoll_api/utils/permissions.py @@ -132,8 +132,6 @@ async def get_all_permissions(resource, member) -> Permissions: def convert_permission_to_string(permissions: Permissions, resource_type) -> list[str]: - # return eval(get_document_type().capitalize() + "Permissions")[string] - print(permissions) permission_type = PermissionTypes[resource_type] return permission_type(permissions).name.split('|') # type: ignore From ff077e4ee5a11af1f6456f52ba562edd22daf119 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Fri, 8 Dec 2023 22:07:53 -0700 Subject: [PATCH 07/33] feat: Added owner policy when creating a poll --- src/unipoll_api/actions/poll.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/unipoll_api/actions/poll.py b/src/unipoll_api/actions/poll.py index d681d7e..0c2513e 100644 --- a/src/unipoll_api/actions/poll.py +++ b/src/unipoll_api/actions/poll.py @@ -1,9 +1,10 @@ from beanie import WriteRules -from unipoll_api.documents import Poll, Workspace +from unipoll_api.account_manager import active_user +from unipoll_api.documents import Member, Poll, Workspace from unipoll_api.schemas import PollSchemas, QuestionSchemas, WorkspaceSchemas from unipoll_api.utils import Permissions from unipoll_api.exceptions import ResourceExceptions, PollExceptions -from unipoll_api import actions +from unipoll_api import actions, dependencies async def get_polls(workspace: Workspace | None = None, @@ -37,6 +38,8 @@ async def create_poll(workspace: Workspace, # Check if the user has permission to create polls await Permissions.check_permissions(workspace, "create_polls", check_permissions) + member: Member = await dependencies.get_member_by_account(active_user.get(), workspace) + # Check if poll name is unique poll: Poll # For type hinting, until Link type is supported for poll in workspace.polls: # type: ignore @@ -44,17 +47,20 @@ async def create_poll(workspace: Workspace, raise PollExceptions.NonUniqueName(poll) # Create a new poll - new_poll = await Poll(name=input_data.name, + new_poll: Poll = Poll(name=input_data.name, description=input_data.description, workspace=workspace, # type: ignore public=input_data.public, published=input_data.published, questions=input_data.questions, - policies=[]).save() + policies=[]) # Check if poll was created if not new_poll: raise PollExceptions.ErrorWhileCreating(new_poll) + + # Add the user as the owner of the poll + await new_poll.add_policy(member, Permissions.POLL_ALL_PERMISSIONS) # Add the poll to the workspace workspace.polls.append(new_poll) # type: ignore From 5958e0829d1289d86882215596382bbc0eec0699 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Fri, 8 Dec 2023 23:35:34 -0700 Subject: [PATCH 08/33] fix: Fixed query parameters for get_poll --- src/unipoll_api/routes/v1/workspace.py | 12 +++--------- src/unipoll_api/routes/v2/polls.py | 4 ++-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/unipoll_api/routes/v1/workspace.py b/src/unipoll_api/routes/v1/workspace.py index a7ef7ae..4428032 100644 --- a/src/unipoll_api/routes/v1/workspace.py +++ b/src/unipoll_api/routes/v1/workspace.py @@ -293,19 +293,13 @@ async def get_poll(workspace: Workspace = Depends(Dependencies.get_workspace), params = {} if include: if "all" in include: - params = {"include_groups": True, - "include_members": True, - "include_policies": True, - "include_polls": True} + params = {"include_questions": True, + "include_policies": True} else: if "groups" in include: - params["include_groups"] = True - if "members" in include: - params["include_members"] = True + params["include_questions"] = True if "policies" in include: params["include_policies"] = True - if "polls" in include: - params["include_polls"] = True return await actions.PollActions.get_poll(poll, **params) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) diff --git a/src/unipoll_api/routes/v2/polls.py b/src/unipoll_api/routes/v2/polls.py index 5298586..eb9da50 100644 --- a/src/unipoll_api/routes/v2/polls.py +++ b/src/unipoll_api/routes/v2/polls.py @@ -30,9 +30,9 @@ async def get_poll(poll: Poll = Depends(Dependencies.get_poll), params = {"include_questions": True, "include_policies": True} else: if "questions" in include: - params = {"include_questions": True} + params["include_questions"] = True if "policies" in include: - params = {"include_policies": True} + params["include_policies"] = True return await PollActions.get_poll(poll, **params) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) From 37c1925cdf68042eab34667f1efd407d6ed2c1a8 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Fri, 8 Dec 2023 23:39:11 -0700 Subject: [PATCH 09/33] refactor: Refactored PollResponse and CreatePollRequest schemas --- src/unipoll_api/schemas/poll.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/unipoll_api/schemas/poll.py b/src/unipoll_api/schemas/poll.py index 28547b1..354bae4 100644 --- a/src/unipoll_api/schemas/poll.py +++ b/src/unipoll_api/schemas/poll.py @@ -1,13 +1,15 @@ from typing import Optional, Any -from pydantic import ConfigDict, BaseModel +from pydantic import ConfigDict, BaseModel, model_validator from unipoll_api.documents import ResourceID +from unipoll_api.schemas import question from unipoll_api.schemas.question import Question +from unipoll_api.schemas.workspace import Workspace +from unipoll_api.schemas.policy import Policy class PollResponse(BaseModel): - id: Optional[ResourceID] = None - # workspace: Optional[Union['Workspace', 'WorkspaceShort']] - workspace: Optional[Any] = None + id: ResourceID + workspace: Any name: str description: str public: bool @@ -66,7 +68,7 @@ class PollList(BaseModel): class CreatePollRequest(BaseModel): name: str - workspace: ResourceID + workspace: Optional[ResourceID] = None description: str public: bool published: bool From af47bb9b508c12681d2d6500318a83607d7dd66c Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Fri, 8 Dec 2023 23:41:37 -0700 Subject: [PATCH 10/33] feat: Added validator to CreatePollRequest schema The validator check that published polls have questions, and that questions' ids are in correct format --- src/unipoll_api/schemas/poll.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/unipoll_api/schemas/poll.py b/src/unipoll_api/schemas/poll.py index 354bae4..439e537 100644 --- a/src/unipoll_api/schemas/poll.py +++ b/src/unipoll_api/schemas/poll.py @@ -73,6 +73,18 @@ class CreatePollRequest(BaseModel): public: bool published: bool questions: list[Question] + + @model_validator(mode='after') + def validate_questions(self) -> 'CreatePollRequest': + if len(self.questions) == 0 and self.published: + raise ValueError('Poll must have at least one question') + if self.questions: + for question in self.questions: + if question.id > len(self.questions): + raise ValueError('Question ID cannot be greater than the number of questions') + if len(self.questions) != len(set([question.id for question in self.questions])): + raise ValueError('Question IDs must be unique') + return self class UpdatePollRequest(BaseModel): From 0fbeb4677ee63d097f2b9501fcc0ad0adb7f11ee Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Fri, 8 Dec 2023 23:45:03 -0700 Subject: [PATCH 11/33] feat: Added validators for question Added validators to check if question id is positive, correct options are postitive, unique, and in the possible range --- src/unipoll_api/schemas/question.py | 45 +++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/unipoll_api/schemas/question.py b/src/unipoll_api/schemas/question.py index 84dc3d4..53104a7 100644 --- a/src/unipoll_api/schemas/question.py +++ b/src/unipoll_api/schemas/question.py @@ -1,12 +1,53 @@ -from pydantic import BaseModel +from typing import Any, Literal +from pydantic import BaseModel, ValidationInfo, field_validator, model_validator + + +question_types = Literal['single-choice', 'multiple-choice'] class Question(BaseModel): id: int question: str - question_type: str + question_type: question_types options: list[str] correct_answer: list[int] + + @field_validator('id') + @classmethod + def id_positive(cls, v: int): + if v <= 0: + raise ValueError('ID cannot be negative or zero') + return v + + @field_validator('correct_answer') + @classmethod + def correct_answer_positive(cls, v: list[int]): + for i in v: + if i < 0: + raise ValueError('Correct answer cannot be negative') + return v + + @field_validator('correct_answer') + @classmethod + def correct_answer_unique(cls, v: list[int]): + if len(v) != len(set(v)): + raise ValueError('Correct answer must be unique') + return v + + @model_validator(mode='after') + # @classmethod + def validate_correct_answer(self) -> 'Question': + if len(self.correct_answer) > 1 and self.question_type == 'single-choice': + raise ValueError('Single choice question cannot have multiple correct answers') + # if len(v) == 0 and values['question_type'] != 'open': + # raise ValueError('Question must have at least one correct answer') + if len(self.correct_answer) > len(self.options): + raise ValueError('Number of Correct options cannot be greater than the number of options') + for i in self.correct_answer: + if i >= len(self.options): + raise ValueError('Correct answer cannot be greater than the number of options') + + return self class SingleChoiceQuestion(Question): From 14309ce6893e67719285ed57fd4376077c1d04b0 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Fri, 8 Dec 2023 23:46:00 -0700 Subject: [PATCH 12/33] refactor: Added support for checking poll policies Since polls do not have members there needs to be a condition to get member from workspace insted --- src/unipoll_api/utils/permissions.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/unipoll_api/utils/permissions.py b/src/unipoll_api/utils/permissions.py index 2402266..f2aab3b 100644 --- a/src/unipoll_api/utils/permissions.py +++ b/src/unipoll_api/utils/permissions.py @@ -1,11 +1,11 @@ from enum import IntFlag -import re +from typing import TYPE_CHECKING import unipoll_api from unipoll_api import exceptions +# from unipoll_api.dependencies import get_member_by_account -# import functools -# import ast -# from pathlib import Path +if TYPE_CHECKING: + from unipoll_api.documents import Resource # Define the permissions base class as an IntFlag Enum @@ -131,7 +131,7 @@ async def get_all_permissions(resource, member) -> Permissions: return permission_sum # type: ignore -def convert_permission_to_string(permissions: Permissions, resource_type) -> list[str]: +def convert_permission_to_string(permissions: Permissions, resource_type: str) -> list[str]: permission_type = PermissionTypes[resource_type] return permission_type(permissions).name.split('|') # type: ignore @@ -152,12 +152,16 @@ def convert_string_to_permission(resource_type: str, string: str): raise ValueError("Invalid permission string") -async def check_permissions(resource, required_permissions: str | list[str] | None = None, permission_check=True): +async def check_permissions(resource: "Resource", + required_permissions: str | list[str] | None = None, + permission_check=True): if permission_check and required_permissions: account = unipoll_api.AccountManager.active_user.get() # Get the active user - from unipoll_api.dependencies import get_member_by_account - member = await get_member_by_account(account, resource) + if resource.get_document_type() == "Poll": + member = await unipoll_api.Dependencies.get_member_by_account(account, resource.workspace) + else: + member = await unipoll_api.Dependencies.get_member_by_account(account, resource) # type: ignore user_permissions = await get_all_permissions(resource, member) # Get the user permissions if isinstance(required_permissions, str): # If only one permission is required @@ -171,3 +175,5 @@ async def check_permissions(resource, required_permissions: str | list[str] | No raise exceptions.ResourceExceptions.UserNotAuthorized(account, f"{resource.get_document_type()} {resource.id}", actions) + +# Resource = Pydantic.update_model() \ No newline at end of file From 72f0571c496e295f9522515bba50074c943df7a3 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Fri, 8 Dec 2023 23:46:55 -0700 Subject: [PATCH 13/33] fix: Replaced group exception with poll exception in get_poll --- src/unipoll_api/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unipoll_api/dependencies.py b/src/unipoll_api/dependencies.py index b8d017d..c619345 100644 --- a/src/unipoll_api/dependencies.py +++ b/src/unipoll_api/dependencies.py @@ -101,7 +101,7 @@ async def get_poll(poll_id: ResourceID) -> Poll: poll = await Poll.get(poll_id, fetch_links=True) if poll: return poll - raise Exceptions.GroupExceptions.GroupNotFound(poll_id) + raise Exceptions.PollExceptions.PollNotFound(poll_id) # Dependency to get a policy by id and verify it exists From 0846c0f98f32b12f54a4b64d0f11dacd9a66578b Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sun, 10 Dec 2023 23:09:31 -0700 Subject: [PATCH 14/33] fix: Fixed updated poll response When updating poll the response would return backling objet to the workspace instead of the workspace schema, the issue was caused by saving the poll and subsequently replacing mongo document, to resolve the issue the workspace is stored before updating the poll --- src/unipoll_api/actions/poll.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/unipoll_api/actions/poll.py b/src/unipoll_api/actions/poll.py index 0c2513e..06b83b6 100644 --- a/src/unipoll_api/actions/poll.py +++ b/src/unipoll_api/actions/poll.py @@ -85,7 +85,7 @@ async def get_poll(poll: Poll, await Permissions.check_permissions(poll, "get_poll", check_permissions) # Fetch the resources if the user has the required permissions - questions = (await get_poll_questions(poll)).questions if include_questions else None + questions = (await get_poll_questions(poll, check_permissions)).questions if include_questions else None policies = (await actions.PolicyActions.get_policies(resource=poll)).policies if include_policies else None workspace = WorkspaceSchemas.WorkspaceShort(**poll.workspace.model_dump()) # type: ignore @@ -116,7 +116,13 @@ async def get_poll_questions(poll: Poll, return QuestionSchemas.QuestionList(questions=question_list) -async def update_poll(poll: Poll, data: PollSchemas.UpdatePollRequest) -> PollSchemas.PollResponse: +async def update_poll(poll: Poll, data: PollSchemas.UpdatePollRequest, check_permissions: bool = True) -> PollSchemas.PollResponse: + await Permissions.check_permissions(poll, "update_poll", check_permissions) + + # BUG: After updating the poll, the workspace turns into a Link + # HACK: Save the workspace before updating the poll + workspace = WorkspaceSchemas.WorkspaceShort(**poll.workspace.model_dump()) # type: ignore + # Update the poll if data.name: poll.name = data.name @@ -131,7 +137,15 @@ async def update_poll(poll: Poll, data: PollSchemas.UpdatePollRequest) -> PollSc # Save the updated poll await Poll.save(poll) - return await get_poll(poll, include_questions=True) + + # Return the workspace with the fetched resources + return PollSchemas.PollResponse(id=poll.id, + name=poll.name, + description=poll.description, + public=poll.public, + published=poll.published, + workspace=workspace, + questions=poll.questions) async def delete_poll(poll: Poll): From 305c90478434c8c36b0e4097a21eccf1675aeb5d Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sun, 10 Dec 2023 23:10:16 -0700 Subject: [PATCH 15/33] refactor: Removed redundant query, added polls' permissions --- src/unipoll_api/routes/v2/permissions.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/unipoll_api/routes/v2/permissions.py b/src/unipoll_api/routes/v2/permissions.py index aba9c09..eef0534 100644 --- a/src/unipoll_api/routes/v2/permissions.py +++ b/src/unipoll_api/routes/v2/permissions.py @@ -35,15 +35,19 @@ async def get_group_permissions(): @router.get("/members/{member_id}", response_description="List of all member permissions") -async def get_member_permissions(member: Member = Depends(Dependencies.get_member), - workspace: Workspace = Depends(Dependencies.get_workspace)): +async def get_member_permissions(member: Member = Depends(Dependencies.get_member)): try: - # await member.fetch_all_links() + + await member.fetch_all_links() + workspace = member.workspace workspace_permissions = await Permissions.get_all_permissions(workspace, member) group_permissions = {} + poll_permissions = {} for group in workspace.groups: group_permissions[group.id] = await Permissions.get_all_permissions(group, member) + for poll in workspace.polls: + poll_permissions[poll.id] = await Permissions.get_all_permissions(poll, member) return { "permissions": { @@ -55,6 +59,11 @@ async def get_member_permissions(member: Member = Depends(Dependencies.get_membe "id": str(group_id), "permissions": Permissions.convert_permission_to_string(permissions, "Group") } for group_id, permissions in group_permissions.items() + ], + "polls": [ { + "id": str(poll_id), + "permissions": Permissions.convert_permission_to_string(permissions, "Poll") + } for poll_id, permissions in poll_permissions.items() ] } } From b5bcb370134a1916d99118cbb3fb6f29c6a20139 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Tue, 12 Dec 2023 21:01:03 -0700 Subject: [PATCH 16/33] refactor: Changed response for get_member_permissions --- src/unipoll_api/routes/v2/permissions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/unipoll_api/routes/v2/permissions.py b/src/unipoll_api/routes/v2/permissions.py index eef0534..c5d49b1 100644 --- a/src/unipoll_api/routes/v2/permissions.py +++ b/src/unipoll_api/routes/v2/permissions.py @@ -50,7 +50,7 @@ async def get_member_permissions(member: Member = Depends(Dependencies.get_membe poll_permissions[poll.id] = await Permissions.get_all_permissions(poll, member) return { - "permissions": { + # "permissions": { "workspace": { "id": str(workspace.id), "permissions": Permissions.convert_permission_to_string(workspace_permissions, "Workspace"), @@ -65,7 +65,7 @@ async def get_member_permissions(member: Member = Depends(Dependencies.get_membe "permissions": Permissions.convert_permission_to_string(permissions, "Poll") } for poll_id, permissions in poll_permissions.items() ] - } + # } } except APIException as e: From 044ab45a6072015771cdd9420a770838c6260e97 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Tue, 12 Dec 2023 22:38:37 -0700 Subject: [PATCH 17/33] feat: Added new endpoints Added an endpoint to get member permissions in a workspace and a group --- src/unipoll_api/routes/v2/permissions.py | 28 ++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/unipoll_api/routes/v2/permissions.py b/src/unipoll_api/routes/v2/permissions.py index c5d49b1..5032bb5 100644 --- a/src/unipoll_api/routes/v2/permissions.py +++ b/src/unipoll_api/routes/v2/permissions.py @@ -1,7 +1,7 @@ -from click import group from fastapi import APIRouter, HTTPException, Depends from unipoll_api import dependencies as Dependencies -from unipoll_api.documents import Account, Workspace, Member +from unipoll_api.account_manager import active_user +from unipoll_api.documents import Account, Workspace, Member, Group from unipoll_api.schemas import PolicySchemas from unipoll_api.exceptions.resource import APIException from unipoll_api.actions import PermissionsActions @@ -22,6 +22,18 @@ async def get_workspace_permissions(): raise HTTPException(status_code=e.code, detail=str(e)) +@router.get("/workspaces/{workspace_id}", + response_description="List of all member permissions in the workspace") +async def get_workspace_member_permissions(workspace: Workspace = Depends(Dependencies.get_workspace)): + try: + account = active_user.get() + member = await Dependencies.get_member_by_account(account, workspace) + workspace_permissions = await Permissions.get_all_permissions(workspace, member) + return Permissions.convert_permission_to_string(workspace_permissions, "Workspace") + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) + + # Get All Group Permissions @router.get("/groups", response_description="List of all Group permissions", @@ -31,6 +43,18 @@ async def get_group_permissions(): return await PermissionsActions.get_group_permissions() except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) + + +@router.get("/groups/{group_id}", + response_description="List of all member permissions in the workspace") +async def get_group_member_permissions(group: Group = Depends(Dependencies.get_group)): + try: + account = active_user.get() + member = await Dependencies.get_member_by_account(account, group.workspace) + group_permissions = await Permissions.get_all_permissions(group, member) + return Permissions.convert_permission_to_string(group_permissions, "Group") + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) @router.get("/members/{member_id}", From 8d429501870ac03e9f406b16ec02e5485ea55bd2 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 13 Dec 2023 23:54:14 -0700 Subject: [PATCH 18/33] feat: Added endpoints to get member permissions --- src/unipoll_api/routes/v1/__init__.py | 7 +- src/unipoll_api/routes/v1/group.py | 94 +++++++++++------ src/unipoll_api/routes/v1/poll.py | 108 ++++++++++++++++++++ src/unipoll_api/routes/v1/workspace.py | 133 +++---------------------- 4 files changed, 193 insertions(+), 149 deletions(-) create mode 100644 src/unipoll_api/routes/v1/poll.py diff --git a/src/unipoll_api/routes/v1/__init__.py b/src/unipoll_api/routes/v1/__init__.py index 5fe946d..3a8ce38 100644 --- a/src/unipoll_api/routes/v1/__init__.py +++ b/src/unipoll_api/routes/v1/__init__.py @@ -6,6 +6,7 @@ from . import authentication as AuthenticationRoutes from . import group as GroupRoutes from . import workspace as WorkspaceRoutes +from . import poll as PollRoutes # Create main router router: APIRouter = APIRouter() @@ -22,6 +23,6 @@ prefix="/workspaces", dependencies=[Depends(Dependencies.set_active_user)]) router.include_router(GroupRoutes.router, - prefix="/workspaces/{workspace_id}/groups", - dependencies=[Depends(Dependencies.set_active_user), - Depends(Dependencies.get_workspace)]) + dependencies=[Depends(Dependencies.set_active_user)]) +router.include_router(PollRoutes.router, + dependencies=[Depends(Dependencies.set_active_user)]) diff --git a/src/unipoll_api/routes/v1/group.py b/src/unipoll_api/routes/v1/group.py index 080138d..5b33d27 100644 --- a/src/unipoll_api/routes/v1/group.py +++ b/src/unipoll_api/routes/v1/group.py @@ -5,7 +5,9 @@ from unipoll_api.actions import GroupActions, PermissionsActions, MembersActions, PolicyActions from unipoll_api.exceptions.resource import APIException from unipoll_api.schemas import GroupSchemas, PolicySchemas, MemberSchemas -from unipoll_api.documents import Group, Policy, ResourceID, Member +from unipoll_api.documents import Group, Policy, ResourceID, Member, Workspace +from unipoll_api.utils import Permissions +from unipoll_api import AccountManager router = APIRouter() @@ -13,15 +15,41 @@ query_params = list[Literal["policies", "members", "all"]] -# Get group info by id +# List all groups in the workspace +@router.get("/{workspace_id}/groups", + tags=["Groups"], + response_description="List of all groups", + response_model=GroupSchemas.GroupList) +async def get_groups(workspace: Workspace = Depends(Dependencies.get_workspace)): + try: + return await GroupActions.get_groups(workspace) + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) + + +# List all groups in the workspace +@router.post("/{workspace_id}/groups", + status_code=201, + tags=["Groups"], + response_description="Created Group", + response_model=GroupSchemas.GroupCreateOutput) +async def create_group(workspace: Workspace = Depends(Dependencies.get_workspace), + input_data: GroupSchemas.GroupCreateInput = Body(...)): + try: + return await GroupActions.create_group(workspace, input_data.name, input_data.description) + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) + -@router.get("/{group_id}", +# Get group info by id +@router.get("/workspaces/{workspace_id}/groups/{group_id}", tags=["Groups"], response_description="Get a group", response_model=GroupSchemas.Group, response_model_exclude_defaults=True, response_model_exclude_none=True) -async def get_group(group: Group = Depends(Dependencies.get_group), +async def get_group(workspace: Workspace = Depends(Dependencies.get_workspace), + group: Group = Depends(Dependencies.get_group), include: Annotated[query_params | None, Query()] = None): try: params = {} @@ -40,12 +68,13 @@ async def get_group(group: Group = Depends(Dependencies.get_group), # Update group info -@router.patch("/{group_id}", +@router.patch("/workspaces/{workspace_id}/groups/{group_id}", tags=["Groups"], response_description="Update a group", response_model=GroupSchemas.GroupShort) async def update_group(group_data: GroupSchemas.GroupUpdateRequest, - group: Group = Depends(Dependencies.get_group)): + group: Group = Depends(Dependencies.get_group), + workspace: Workspace = Depends(Dependencies.get_workspace)): try: return await GroupActions.update_group(group, group_data) except APIException as e: @@ -54,11 +83,12 @@ async def update_group(group_data: GroupSchemas.GroupUpdateRequest, # Delete a group -@router.delete("/{group_id}", +@router.delete("/workspaces/{workspace_id}/groups/{group_id}", tags=["Groups"], status_code=status.HTTP_204_NO_CONTENT, response_description="Delete a group") -async def delete_group(group: Group = Depends(Dependencies.get_group)): +async def delete_group(workspace: Workspace = Depends(Dependencies.get_workspace), + group: Group = Depends(Dependencies.get_group)): try: await GroupActions.delete_group(group) return status.HTTP_204_NO_CONTENT @@ -68,12 +98,13 @@ async def delete_group(group: Group = Depends(Dependencies.get_group)): # Get a list of group members -@router.get("/{group_id}/members", +@router.get("/workspaces/{workspace_id}/groups/{group_id}/members", tags=["Group Members"], response_description="List of group members", response_model=MemberSchemas.MemberList, response_model_exclude_unset=True) -async def get_group_members(group: Group = Depends(Dependencies.get_group)): +async def get_group_members(workspace: Workspace = Depends(Dependencies.get_workspace), + group: Group = Depends(Dependencies.get_group)): try: return await MembersActions.get_members(group) except APIException as e: @@ -82,11 +113,12 @@ async def get_group_members(group: Group = Depends(Dependencies.get_group)): # Add member to group -@router.post("/{group_id}/members", +@router.post("/workspaces/{workspace_id}/groups/{group_id}/members", tags=["Group Members"], response_description="List of group members", response_model=MemberSchemas.MemberList) async def add_group_members(member_data: MemberSchemas.AddMembers, + workspace: Workspace = Depends(Dependencies.get_workspace), group: Group = Depends(Dependencies.get_group)): try: return await MembersActions.add_members(group, member_data.accounts) @@ -96,11 +128,12 @@ async def add_group_members(member_data: MemberSchemas.AddMembers, # Remove members from the workspace -@router.delete("/{group_id}/members/{member_id}", +@router.delete("/workspaces/{workspace_id}/groups/{group_id}/members/{member_id}", tags=["Group Members"], response_description="Updated list removed members", response_model_exclude_unset=True) -async def remove_group_member(group: Group = Depends(Dependencies.get_group), +async def remove_group_member(workspace: Workspace = Depends(Dependencies.get_workspace), + group: Group = Depends(Dependencies.get_group), member: Member = Depends(Dependencies.get_member)): try: return await MembersActions.remove_member(group, member) @@ -110,11 +143,12 @@ async def remove_group_member(group: Group = Depends(Dependencies.get_group), # List all policies in the workspace -@router.get("/{group_id}/policies", +@router.get("/workspaces/{workspace_id}/groups/{group_id}/policies", tags=["Group Policies"], response_description="List of all policies", response_model=PolicySchemas.PolicyList) -async def get_group_policies(group: Group = Depends(Dependencies.get_group), +async def get_group_policies(workspace: Workspace = Depends(Dependencies.get_workspace), + group: Group = Depends(Dependencies.get_group), account_id: ResourceID = Query(None)) -> PolicySchemas.PolicyList: try: account = await Dependencies.get_account(account_id) if account_id else None @@ -126,11 +160,12 @@ async def get_group_policies(group: Group = Depends(Dependencies.get_group), # Set permissions for a user in a group -@router.put("/{group_id}/policies/{policy_id}", +@router.put("/workspaces/{workspace_id}/groups/{group_id}/policies/{policy_id}", tags=["Group Policies"], response_description="Updated policy", response_model=PolicySchemas.PolicyOutput) -async def set_group_policy(group: Group = Depends(Dependencies.get_group), +async def set_group_policy(workspace: Workspace = Depends(Dependencies.get_workspace), + group: Group = Depends(Dependencies.get_group), policy: Policy = Depends(Dependencies.get_policy), permissions: PolicySchemas.PolicyInput = Body(...)): """ @@ -147,14 +182,17 @@ async def set_group_policy(group: Group = Depends(Dependencies.get_group), raise HTTPException(status_code=e.code, detail=str(e)) -# Get All Group Permissions - -# @router.get("/permissions", -# tags=["Groups"], -# response_description="List of all Group permissions", -# response_model=PolicySchemas.PermissionList) -# async def get_group_permissions(): -# try: -# return await PermissionsActions.get_group_permissions() -# except APIException as e: -# raise HTTPException(status_code=e.code, detail=str(e)) +# Get All Member Permissions in the Group +@router.get("/workspaces/{workspace_id}/groups/{group_id}/permissions", + tags=["Group Permissions"], + response_description="List of all member permissions in the workspace", + response_model=PolicySchemas.PermissionList) +async def get_group_member_permissions(workspace: Workspace = Depends(Dependencies.get_workspace), + group: Group = Depends(Dependencies.get_group)): + try: + account = AccountManager.active_user.get() + member = await Dependencies.get_member_by_account(account, group.workspace) # type: ignore + group_permissions = await Permissions.get_all_permissions(group, member) + return PolicySchemas.PermissionList(permissions=Permissions.convert_permission_to_string(group_permissions, "Group")) + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) \ No newline at end of file diff --git a/src/unipoll_api/routes/v1/poll.py b/src/unipoll_api/routes/v1/poll.py new file mode 100644 index 0000000..c5e0584 --- /dev/null +++ b/src/unipoll_api/routes/v1/poll.py @@ -0,0 +1,108 @@ +# FastAPI +from typing import Annotated, Literal +from fastapi import APIRouter, Body, Depends, HTTPException, Query, status +from unipoll_api import dependencies as Dependencies +from unipoll_api import actions, AccountManager +from unipoll_api.exceptions.resource import APIException +from unipoll_api.documents import Poll, Workspace +from unipoll_api.schemas import PollSchemas, PolicySchemas +from unipoll_api.utils import Permissions + + +router: APIRouter = APIRouter() + + +# Get Workspace Polls +@router.get("/workspaces/{workspace_id}/polls", + tags=["Polls"], + response_description="List of all polls in the workspace", + response_model=PollSchemas.PollList, + response_model_exclude_none=True) +async def get_polls(workspace: Workspace = Depends(Dependencies.get_workspace)): + try: + return await actions.PollActions.get_polls(workspace) + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) + + +# Create a new poll in the workspace +@router.post("/workspaces/{workspace_id}/polls", + tags=["Polls"], + response_description="Created poll", + status_code=201, + response_model=PollSchemas.PollResponse) +async def create_poll(workspace: Workspace = Depends(Dependencies.get_workspace), + input_data: PollSchemas.CreatePollRequest = Body(...)): + try: + return await actions.PollActions.create_poll(workspace, input_data) + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) + + +# Get a poll in the workspace +@router.get("/workspaces/{workspace_id}/polls/{poll_id}", + tags=["Polls"], + response_description="Poll data", + response_model=PollSchemas.PollResponse) +async def get_poll(workspace: Workspace = Depends(Dependencies.get_workspace), + poll: Poll = Depends(Dependencies.get_poll), + include: Annotated[list[Literal["all", "policies", "questions"]] | None, Query()] = None): + try: + params = {} + if include: + if "all" in include: + params = {"include_questions": True, + "include_policies": True} + else: + if "groups" in include: + params["include_questions"] = True + if "policies" in include: + params["include_policies"] = True + return await actions.PollActions.get_poll(poll, **params) + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) + + +# Update a poll in the workspace +@router.patch("/workspaces/{workspace_id}/polls/{poll_id}", + tags=["Polls"], + response_description="Updated poll", + response_model=PollSchemas.PollResponse) +async def update_poll(poll: Poll = Depends(Dependencies.get_poll), + input_data: PollSchemas.UpdatePollRequest = Body(...)): + try: + return await actions.PollActions.update_poll(poll, input_data) + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) + + +# Delete a poll in the workspace +@router.delete("/workspaces/{workspace_id}/polls/{poll_id}", + tags=["Polls"], + response_description="Deleted poll", + status_code=204) +async def delete_poll(poll: Poll = Depends(Dependencies.get_poll)): + try: + await actions.PollActions.delete_poll(poll) + return status.HTTP_204_NO_CONTENT + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) + + +#TODO: Add poll policies endpoints + + +# Get All Member Permissions in the Group +@router.get("/workspaces/{workspace_id}/polls/{poll_id}/permissions", + tags=["Poll Permissions"], + response_description="List of all member permissions in the workspace", + response_model=PolicySchemas.PermissionList) +async def get_poll_member_permissions(poll: Poll = Depends(Dependencies.get_poll)): + try: + #TODO: Create an Action for this + account = AccountManager.active_user.get() + member = await Dependencies.get_member_by_account(account, poll.workspace) # type: ignore + poll_permissions = await Permissions.get_all_permissions(poll, member) + return PolicySchemas.PermissionList(permissions=Permissions.convert_permission_to_string(poll_permissions, "Poll")) + except APIException as e: + raise HTTPException(status_code=e.code, detail=str(e)) \ No newline at end of file diff --git a/src/unipoll_api/routes/v1/workspace.py b/src/unipoll_api/routes/v1/workspace.py index 4428032..6e61b33 100644 --- a/src/unipoll_api/routes/v1/workspace.py +++ b/src/unipoll_api/routes/v1/workspace.py @@ -2,18 +2,14 @@ from typing import Annotated, Literal from fastapi import APIRouter, Body, Depends, HTTPException, Query, status from unipoll_api import dependencies as Dependencies -from unipoll_api import actions +from unipoll_api import actions, AccountManager from unipoll_api.exceptions.resource import APIException -from unipoll_api.documents import Poll, Workspace, ResourceID, Policy, Member -from unipoll_api.schemas import WorkspaceSchemas, PolicySchemas, GroupSchemas, MemberSchemas, PollSchemas +from unipoll_api.documents import Workspace, ResourceID, Policy, Member +from unipoll_api.schemas import WorkspaceSchemas, PolicySchemas, GroupSchemas, MemberSchemas +from unipoll_api.utils import Permissions router: APIRouter = APIRouter() -workspace_router: APIRouter = APIRouter(tags=["Workspaces"]) -workspace_groups_router: APIRouter = APIRouter(tags=["Workspace Groups"]) -workspace_members_router: APIRouter = APIRouter(tags=["Workspace Members"]) -workspace_policies_router: APIRouter = APIRouter(tags=["Workspace Policies"]) -workspace_polls_router: APIRouter = APIRouter(tags=["Workspace Polls"]) # TODO: Move to open router to a separate file @@ -242,116 +238,17 @@ async def set_workspace_policy(workspace: Workspace = Depends(Dependencies.get_w raise HTTPException(status_code=e.code, detail=str(e)) -# Get All Workspace Permissions -# @router.get("/permissions", -# tags=["Workspaces"], -# response_description="List of all workspace permissions", -# response_model=PolicySchemas.PermissionList) -# async def get_workspace_permissions(): -# try: -# return await actions.PermissionsActions.get_workspace_permissions() -# except APIException as e: -# raise HTTPException(status_code=e.code, detail=str(e)) - - -# Get Workspace Polls -@router.get("/{workspace_id}/polls", - tags=["Polls"], - response_description="List of all polls in the workspace", - response_model=PollSchemas.PollList, - response_model_exclude_none=True) -async def get_polls(workspace: Workspace = Depends(Dependencies.get_workspace)): - try: - return await actions.PollActions.get_polls(workspace) - except APIException as e: - raise HTTPException(status_code=e.code, detail=str(e)) - - -# Create a new poll in the workspace -@router.post("/{workspace_id}/polls", - tags=["Polls"], - response_description="Created poll", - status_code=201, - response_model=PollSchemas.PollResponse) -async def create_poll(workspace: Workspace = Depends(Dependencies.get_workspace), - input_data: PollSchemas.CreatePollRequest = Body(...)): - try: - return await actions.PollActions.create_poll(workspace, input_data) - except APIException as e: - raise HTTPException(status_code=e.code, detail=str(e)) - - -# Get a poll in the workspace -@router.get("/{workspace_id}/polls/{poll_id}", - tags=["Polls"], - response_description="Poll data", - response_model=PollSchemas.PollResponse) -async def get_poll(workspace: Workspace = Depends(Dependencies.get_workspace), - poll: Poll = Depends(Dependencies.get_poll), - include: Annotated[list[Literal["all", "policies", "questions"]] | None, Query()] = None): - try: - params = {} - if include: - if "all" in include: - params = {"include_questions": True, - "include_policies": True} - else: - if "groups" in include: - params["include_questions"] = True - if "policies" in include: - params["include_policies"] = True - return await actions.PollActions.get_poll(poll, **params) - except APIException as e: - raise HTTPException(status_code=e.code, detail=str(e)) - - -# Update a poll in the workspace -@router.patch("/{workspace_id}/polls/{poll_id}", - tags=["Polls"], - response_description="Updated poll", - response_model=PollSchemas.PollResponse) -async def update_poll(poll: Poll = Depends(Dependencies.get_poll), - input_data: PollSchemas.UpdatePollRequest = Body(...)): - try: - return await actions.PollActions.update_poll(poll, input_data) - except APIException as e: - raise HTTPException(status_code=e.code, detail=str(e)) - - -# Delete a poll in the workspace -@router.delete("/{workspace_id}/polls/{poll_id}", - tags=["Polls"], - response_description="Deleted poll", - status_code=204) -async def delete_poll(poll: Poll = Depends(Dependencies.get_poll)): - try: - await actions.PollActions.delete_poll(poll) - return status.HTTP_204_NO_CONTENT - except APIException as e: - raise HTTPException(status_code=e.code, detail=str(e)) - - -# List all groups in the workspace -@router.get("/{workspace_id}/groups", - tags=["Groups"], - response_description="List of all groups", - response_model=GroupSchemas.GroupList) -async def get_groups(workspace: Workspace = Depends(Dependencies.get_workspace)): - try: - return await actions.GroupActions.get_groups(workspace) - except APIException as e: - raise HTTPException(status_code=e.code, detail=str(e)) - - -# List all groups in the workspace -@router.post("/{workspace_id}/groups", - status_code=201, - tags=["Groups"], - response_description="Created Group", - response_model=GroupSchemas.GroupCreateOutput) -async def create_group(workspace: Workspace = Depends(Dependencies.get_workspace), - input_data: GroupSchemas.GroupCreateInput = Body(...)): +# Get Member Permissions in the workspace +@router.get("/{workspace_id}/permissions", + tags=["Workspace Permissions"], + response_description="List of all member permissions in the workspace", + response_model=PolicySchemas.PermissionList) +async def get_workspace_member_permissions(workspace: Workspace = Depends(Dependencies.get_workspace)): try: - return await actions.GroupActions.create_group(workspace, input_data.name, input_data.description) + #TODO: Create an action + account = AccountManager.active_user.get() + member = await Dependencies.get_member_by_account(account, workspace) + workspace_permissions = await Permissions.get_all_permissions(workspace, member) + return PolicySchemas.PermissionList(permissions=Permissions.convert_permission_to_string(workspace_permissions, "Workspace")) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) From e115ba97d00301c2e01a6db418de3ff03c40ef14 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Wed, 13 Dec 2023 23:54:51 -0700 Subject: [PATCH 19/33] refactor: Removed routes to get permissions in group/workspace --- src/unipoll_api/routes/v2/permissions.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/unipoll_api/routes/v2/permissions.py b/src/unipoll_api/routes/v2/permissions.py index 5032bb5..12c34a8 100644 --- a/src/unipoll_api/routes/v2/permissions.py +++ b/src/unipoll_api/routes/v2/permissions.py @@ -22,18 +22,6 @@ async def get_workspace_permissions(): raise HTTPException(status_code=e.code, detail=str(e)) -@router.get("/workspaces/{workspace_id}", - response_description="List of all member permissions in the workspace") -async def get_workspace_member_permissions(workspace: Workspace = Depends(Dependencies.get_workspace)): - try: - account = active_user.get() - member = await Dependencies.get_member_by_account(account, workspace) - workspace_permissions = await Permissions.get_all_permissions(workspace, member) - return Permissions.convert_permission_to_string(workspace_permissions, "Workspace") - except APIException as e: - raise HTTPException(status_code=e.code, detail=str(e)) - - # Get All Group Permissions @router.get("/groups", response_description="List of all Group permissions", @@ -45,18 +33,6 @@ async def get_group_permissions(): raise HTTPException(status_code=e.code, detail=str(e)) -@router.get("/groups/{group_id}", - response_description="List of all member permissions in the workspace") -async def get_group_member_permissions(group: Group = Depends(Dependencies.get_group)): - try: - account = active_user.get() - member = await Dependencies.get_member_by_account(account, group.workspace) - group_permissions = await Permissions.get_all_permissions(group, member) - return Permissions.convert_permission_to_string(group_permissions, "Group") - except APIException as e: - raise HTTPException(status_code=e.code, detail=str(e)) - - @router.get("/members/{member_id}", response_description="List of all member permissions") async def get_member_permissions(member: Member = Depends(Dependencies.get_member)): From 4dfe32eb39d5f2433205f355e5fe5c2f6f5d39ed Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Thu, 14 Dec 2023 23:03:46 -0700 Subject: [PATCH 20/33] fix: Fixed issue with convert_permission_to_string When user has no permissions, the function should return empty list --- src/unipoll_api/utils/permissions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/unipoll_api/utils/permissions.py b/src/unipoll_api/utils/permissions.py index f2aab3b..2cb4e6b 100644 --- a/src/unipoll_api/utils/permissions.py +++ b/src/unipoll_api/utils/permissions.py @@ -133,6 +133,8 @@ async def get_all_permissions(resource, member) -> Permissions: def convert_permission_to_string(permissions: Permissions, resource_type: str) -> list[str]: permission_type = PermissionTypes[resource_type] + if permissions == permission_type(0): + return [] return permission_type(permissions).name.split('|') # type: ignore From d6a69b00f0c87f21bf25845da889b9335e407cd3 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Thu, 14 Dec 2023 23:05:01 -0700 Subject: [PATCH 21/33] fix: Fixed issue with get_polls If a user does not have a policy, the function would raise HTTP Exception, so an additional try-excpept block is required --- src/unipoll_api/actions/poll.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/unipoll_api/actions/poll.py b/src/unipoll_api/actions/poll.py index 06b83b6..e6d5336 100644 --- a/src/unipoll_api/actions/poll.py +++ b/src/unipoll_api/actions/poll.py @@ -19,10 +19,11 @@ async def get_polls(workspace: Workspace | None = None, except ResourceExceptions.UserNotAuthorized: poll: Poll for poll in workspace.polls: # type: ignore - if poll.public: - polls.append(poll) - else: + try: polls.append(await get_poll(poll, check_permissions)) # type: ignore + except ResourceExceptions.UserNotAuthorized: + continue + poll_list = [] # Build poll list and return the result From ca8f72328504b97a004e120f45d649e518407089 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Thu, 14 Dec 2023 23:06:23 -0700 Subject: [PATCH 22/33] fix: Fixed error when getting list of policies for a poll When getting poll policies, we should use workspace for getting member list, since poll does not have members --- src/unipoll_api/actions/policy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/unipoll_api/actions/policy.py b/src/unipoll_api/actions/policy.py index 2cc3f6c..0cd7c81 100644 --- a/src/unipoll_api/actions/policy.py +++ b/src/unipoll_api/actions/policy.py @@ -17,7 +17,10 @@ async def get_policies_from_resource(resource: Resource) -> list[Policy]: except ResourceExceptions.UserNotAuthorized: print("User not authorized") account = AccountManager.active_user.get() - member = await get_member_by_account(account, resource) + if resource.get_document_type() == "Poll": + member = await get_member_by_account(account, resource.workspace) + else: + member = await get_member_by_account(account, resource) for policy in resource.policies: if policy.policy_holder.ref.id == member.id: # type: ignore policies.append(policy) # type: ignore From ba22bdca39047690d87888810a7f623d3a0e10c1 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sat, 16 Dec 2023 01:49:28 -0700 Subject: [PATCH 23/33] refactor: Refactor UserNotAuthorized exception Changed resource type --- src/unipoll_api/exceptions/group.py | 2 +- src/unipoll_api/exceptions/poll.py | 2 +- src/unipoll_api/exceptions/resource.py | 4 ++-- src/unipoll_api/exceptions/workspace.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/unipoll_api/exceptions/group.py b/src/unipoll_api/exceptions/group.py index a36aec9..99ee7df 100644 --- a/src/unipoll_api/exceptions/group.py +++ b/src/unipoll_api/exceptions/group.py @@ -23,7 +23,7 @@ def __init__(self, group_id: ResourceID): # Not authorized class UserNotAuthorized(resource.UserNotAuthorized): def __init__(self, account: Account, group: Group, action: str): - super().__init__(account, f'group {group.name}', action) + super().__init__(account, group, action) # Exception for when a Group was not deleted successfully diff --git a/src/unipoll_api/exceptions/poll.py b/src/unipoll_api/exceptions/poll.py index 71bb436..58b8897 100644 --- a/src/unipoll_api/exceptions/poll.py +++ b/src/unipoll_api/exceptions/poll.py @@ -23,7 +23,7 @@ def __init__(self, poll_id: ResourceID): # Not authorized class UserNotAuthorized(resource.UserNotAuthorized): def __init__(self, account: Account, poll: Poll, action: str): - super().__init__(account, f'poll {poll.name}', action) + super().__init__(account, poll, action) # Action not found diff --git a/src/unipoll_api/exceptions/resource.py b/src/unipoll_api/exceptions/resource.py index 3559401..231545f 100644 --- a/src/unipoll_api/exceptions/resource.py +++ b/src/unipoll_api/exceptions/resource.py @@ -46,9 +46,9 @@ def __init__(self, resource: str, resource_id: ResourceID): # Not authorized class UserNotAuthorized(APIException): - def __init__(self, account: Account, resource: str, action: str = "perform this action"): + def __init__(self, account: Account, resource: Resource, action: str = "perform this action"): super().__init__(code=status.HTTP_403_FORBIDDEN, - detail=f"User {account.email} is not authorized to {action} in {resource}") + detail=f"User {account.email} is not authorized to {action} in {resource.get_document_type()} {resource.name}") # Action not found diff --git a/src/unipoll_api/exceptions/workspace.py b/src/unipoll_api/exceptions/workspace.py index c9f2e2f..739e544 100644 --- a/src/unipoll_api/exceptions/workspace.py +++ b/src/unipoll_api/exceptions/workspace.py @@ -41,7 +41,7 @@ def __init__(self, workspace: Workspace, user: Account): # Not authorized class UserNotAuthorized(resource.UserNotAuthorized): def __init__(self, account: Account, workspace: Workspace, action: str = "perform this action in"): - super().__init__(account, f"workspace {workspace.name}", action) + super().__init__(account, workspace, action) # Action not found From f6a1ddc7d972316ccb6bcd706f83dd870b06ba52 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sat, 16 Dec 2023 01:49:44 -0700 Subject: [PATCH 24/33] refactor: Refactor get_member_by_account to raise UserNotMember exception --- src/unipoll_api/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unipoll_api/dependencies.py b/src/unipoll_api/dependencies.py index c619345..f0e6bf3 100644 --- a/src/unipoll_api/dependencies.py +++ b/src/unipoll_api/dependencies.py @@ -50,7 +50,7 @@ async def get_member_by_account(account: Account, resource: Workspace | Group) - for member in resource.members: if member.account.id == account.id: # type: ignore return member # type: ignore - raise Exceptions.ResourceExceptions.ResourceNotFound("member", account.id) + raise Exceptions.ResourceExceptions.UserNotMember(resource, account) async def websocket_auth(websocket: WebSocket, From 0c6013d446210897e5543e411ddda8cd9613e394 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sat, 16 Dec 2023 01:50:47 -0700 Subject: [PATCH 25/33] refactor: Change exceptions in check_permissions function Raise UserNotAuthorized if user is not a member --- src/unipoll_api/utils/permissions.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/unipoll_api/utils/permissions.py b/src/unipoll_api/utils/permissions.py index 2cb4e6b..0cf0ab4 100644 --- a/src/unipoll_api/utils/permissions.py +++ b/src/unipoll_api/utils/permissions.py @@ -160,10 +160,14 @@ async def check_permissions(resource: "Resource", if permission_check and required_permissions: account = unipoll_api.AccountManager.active_user.get() # Get the active user - if resource.get_document_type() == "Poll": - member = await unipoll_api.Dependencies.get_member_by_account(account, resource.workspace) - else: - member = await unipoll_api.Dependencies.get_member_by_account(account, resource) # type: ignore + try: + if resource.get_document_type() == "Poll": + member = await unipoll_api.Dependencies.get_member_by_account(account, resource.workspace) + else: + member = await unipoll_api.Dependencies.get_member_by_account(account, resource) # type: ignore + except exceptions.ResourceExceptions.UserNotMember: + actions = ", ".join([" ".join([j.capitalize() for j in i.split("_")]) for i in required_permissions]) + raise exceptions.ResourceExceptions.UserNotAuthorized(account, resource, actions) user_permissions = await get_all_permissions(resource, member) # Get the user permissions if isinstance(required_permissions, str): # If only one permission is required @@ -174,8 +178,6 @@ async def check_permissions(resource: "Resource", if not compare_permissions(user_permissions, required_permission): actions = ", ".join([" ".join([j.capitalize() for j in i.split("_")]) for i in required_permissions]) - raise exceptions.ResourceExceptions.UserNotAuthorized(account, - f"{resource.get_document_type()} {resource.id}", - actions) + raise exceptions.ResourceExceptions.UserNotAuthorized(account, resource, actions) # Resource = Pydantic.update_model() \ No newline at end of file From 7286478118efa3d21931e0b8eb4e70139b950444 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sat, 16 Dec 2023 01:51:55 -0700 Subject: [PATCH 26/33] fix: Update ErrorWhileRemovingMember exception to use Member instead of Account --- src/unipoll_api/exceptions/group.py | 6 +++--- src/unipoll_api/exceptions/workspace.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/unipoll_api/exceptions/group.py b/src/unipoll_api/exceptions/group.py index 99ee7df..02c693e 100644 --- a/src/unipoll_api/exceptions/group.py +++ b/src/unipoll_api/exceptions/group.py @@ -1,4 +1,4 @@ -from unipoll_api.documents import ResourceID, Account, Group +from unipoll_api.documents import Member, ResourceID, Account, Group from unipoll_api.exceptions import resource @@ -52,5 +52,5 @@ def __init__(self, group: Group, user: Account): # Error while removing a member class ErrorWhileRemovingMember(resource.ErrorWhileRemovingMember): - def __init__(self, group: Group, user: Account): - super().__init__(group, user) + def __init__(self, group: Group, member: Member): + super().__init__(group, member) diff --git a/src/unipoll_api/exceptions/workspace.py b/src/unipoll_api/exceptions/workspace.py index 739e544..1c3e810 100644 --- a/src/unipoll_api/exceptions/workspace.py +++ b/src/unipoll_api/exceptions/workspace.py @@ -1,4 +1,4 @@ -from unipoll_api.documents import ResourceID, Workspace, Account +from unipoll_api.documents import Member, ResourceID, Workspace, Account from unipoll_api.exceptions import resource @@ -52,5 +52,5 @@ def __init__(self, action: str): # Error while removing member class ErrorWhileRemovingMember(resource.ErrorWhileRemovingMember): - def __init__(self, workspace: Workspace, user: Account): - super().__init__(workspace, user) + def __init__(self, workspace: Workspace, member: Member): + super().__init__(workspace, member) From 09fd39b964abbcfd3e9acb5c45416da2a6b8f9ae Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sat, 16 Dec 2023 01:56:23 -0700 Subject: [PATCH 27/33] fix: Fix actions output in UserNotAuthorized exception --- src/unipoll_api/utils/permissions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/unipoll_api/utils/permissions.py b/src/unipoll_api/utils/permissions.py index 0cf0ab4..4bd2d05 100644 --- a/src/unipoll_api/utils/permissions.py +++ b/src/unipoll_api/utils/permissions.py @@ -160,6 +160,9 @@ async def check_permissions(resource: "Resource", if permission_check and required_permissions: account = unipoll_api.AccountManager.active_user.get() # Get the active user + if isinstance(required_permissions, str): # If only one permission is required + required_permissions = [required_permissions] + try: if resource.get_document_type() == "Poll": member = await unipoll_api.Dependencies.get_member_by_account(account, resource.workspace) @@ -170,8 +173,6 @@ async def check_permissions(resource: "Resource", raise exceptions.ResourceExceptions.UserNotAuthorized(account, resource, actions) user_permissions = await get_all_permissions(resource, member) # Get the user permissions - if isinstance(required_permissions, str): # If only one permission is required - required_permissions = [required_permissions] permissions_list = [convert_string_to_permission(resource.get_document_type(), p) for p in required_permissions] required_permission = eval(resource.get_document_type() + "Permissions")(sum(permissions_list)) From cc48200f47673e01a6469f12e52379a1dc41d696 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sat, 16 Dec 2023 18:01:54 -0700 Subject: [PATCH 28/33] fix: Improved permission handling when getting poll User now is able to get polls with get_poll or get_polls(workspace) permission --- src/unipoll_api/actions/poll.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/unipoll_api/actions/poll.py b/src/unipoll_api/actions/poll.py index e6d5336..6529a35 100644 --- a/src/unipoll_api/actions/poll.py +++ b/src/unipoll_api/actions/poll.py @@ -23,8 +23,6 @@ async def get_polls(workspace: Workspace | None = None, polls.append(await get_poll(poll, check_permissions)) # type: ignore except ResourceExceptions.UserNotAuthorized: continue - - poll_list = [] # Build poll list and return the result for poll in polls: # type: ignore @@ -83,10 +81,14 @@ async def get_poll(poll: Poll, include_policies: bool = False, check_permissions: bool = True) -> PollSchemas.PollResponse: if not poll.public: - await Permissions.check_permissions(poll, "get_poll", check_permissions) + # await Permissions.check_permissions(poll, "get_poll", check_permissions) + try: + await Permissions.check_permissions(poll.workspace, "get_polls", check_permissions) + except ResourceExceptions.UserNotAuthorized: + await Permissions.check_permissions(poll, "get_poll", check_permissions) # Fetch the resources if the user has the required permissions - questions = (await get_poll_questions(poll, check_permissions)).questions if include_questions else None + questions = (await get_poll_questions(poll, False)).questions if include_questions else None policies = (await actions.PolicyActions.get_policies(resource=poll)).policies if include_policies else None workspace = WorkspaceSchemas.WorkspaceShort(**poll.workspace.model_dump()) # type: ignore From 85e5e5b7bd53f80383d4412bd298f421e45ad288 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sat, 16 Dec 2023 18:02:40 -0700 Subject: [PATCH 29/33] fix: Updated request body schema to create/update workspace Added name and description field validation --- src/unipoll_api/schemas/workspace.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/unipoll_api/schemas/workspace.py b/src/unipoll_api/schemas/workspace.py index 8a0805d..be4b25f 100644 --- a/src/unipoll_api/schemas/workspace.py +++ b/src/unipoll_api/schemas/workspace.py @@ -51,8 +51,8 @@ class WorkspaceList(BaseModel): # Schema for the request to create a workspace class WorkspaceCreateInput(BaseModel): - name: str = Field(title="Name") - description: str = Field(title="Description") + name: str = Field(title="Name", description="Name of the resource", min_length=3, max_length=50) + description: str = Field(default="", title="Description", max_length=1000) model_config = ConfigDict(json_schema_extra={ "example": { "name": "Workspace 01", @@ -63,8 +63,8 @@ class WorkspaceCreateInput(BaseModel): # Schema for the request to update a workspace class WorkspaceUpdateRequest(BaseModel): - name: Optional[str] = Field(None, title="Name") - description: Optional[str] = Field(None, title="Description") + name: Optional[str] = Field(default=None, title="Name", description="Name of the resource", min_length=3, max_length=50) + description: Optional[str] = Field(default=None, title="Description", max_length=1000) model_config = ConfigDict(json_schema_extra={ "example": { "name": "Workspace 01", From f2ca6422fa0ba1099ea7762d4ad3da1c9008e087 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sun, 17 Dec 2023 21:42:47 -0700 Subject: [PATCH 30/33] refactor: Changed NonUniqueName exception status code to 409 --- src/unipoll_api/exceptions/resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unipoll_api/exceptions/resource.py b/src/unipoll_api/exceptions/resource.py index 231545f..517ea66 100644 --- a/src/unipoll_api/exceptions/resource.py +++ b/src/unipoll_api/exceptions/resource.py @@ -22,7 +22,7 @@ def __init__(self, detail: str): class NonUniqueName(APIException): def __init__(self, resource: str, resource_name: str): - super().__init__(code=status.HTTP_400_BAD_REQUEST, + super().__init__(code=status.HTTP_409_CONFLICT, detail=f"{resource} with name {resource_name} already exists") From a40a4783cb8fa9ee00152c2cc8e02b275fbc6b72 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sun, 17 Dec 2023 21:43:32 -0700 Subject: [PATCH 31/33] feat: Added include=workspace query parameter to get_group --- src/unipoll_api/actions/group.py | 8 +++++--- src/unipoll_api/routes/v1/group.py | 7 +++++-- src/unipoll_api/routes/v2/groups.py | 5 +++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/unipoll_api/actions/group.py b/src/unipoll_api/actions/group.py index 145d9f7..48bfb99 100644 --- a/src/unipoll_api/actions/group.py +++ b/src/unipoll_api/actions/group.py @@ -3,6 +3,7 @@ from bson import DBRef from unipoll_api import AccountManager +from unipoll_api.actions import workspace from unipoll_api.documents import Policy, Workspace, Group, Account from unipoll_api import actions from unipoll_api.schemas import GroupSchemas, WorkspaceSchemas @@ -81,6 +82,7 @@ async def create_group(workspace: Workspace, async def get_group(group: Group, include_members: bool = False, include_policies: bool = False, + include_workspace: bool = False, check_permissions: bool = True) -> GroupSchemas.Group: try: await Permissions.check_permissions(group.workspace, "get_groups", check_permissions) @@ -89,9 +91,9 @@ async def get_group(group: Group, members = (await actions.MembersActions.get_members(group)).members if include_members else None policies = (await actions.PolicyActions.get_policies(resource=group)).policies if include_policies else None - workspace = WorkspaceSchemas.Workspace(**group.workspace.model_dump(exclude={"members", # type: ignore - "policies", - "groups"})) + # workspace = (await actions.WorkspaceActions.get_workspace(group.workspace, True, True, True, True)) if include_workspace else None + workspace = WorkspaceSchemas.Workspace(**group.workspace.model_dump(include={'id', 'name', 'description'})) if include_workspace else None + # Return the workspace with the fetched resources return GroupSchemas.Group(id=group.id, name=group.name, diff --git a/src/unipoll_api/routes/v1/group.py b/src/unipoll_api/routes/v1/group.py index 5b33d27..09376ad 100644 --- a/src/unipoll_api/routes/v1/group.py +++ b/src/unipoll_api/routes/v1/group.py @@ -12,7 +12,7 @@ router = APIRouter() -query_params = list[Literal["policies", "members", "all"]] +query_params = list[Literal["workspace", "policies", "members", "all"]] # List all groups in the workspace @@ -55,12 +55,15 @@ async def get_group(workspace: Workspace = Depends(Dependencies.get_workspace), params = {} if include: if "all" in include: - params = {"include_members": True, "include_policies": True} + # params = {"include_members": True, "include_policies": True} + params = {"include_" + param: True for param in ["members", "policies", "workspace"]} else: if "members" in include: params["include_members"] = True if "policies" in include: params["include_policies"] = True + if "workspaces" in include: + params["include_workspaces"] = True return await GroupActions.get_group(group, **params) except APIException as e: raise HTTPException(status_code=e.code, detail=str(e)) diff --git a/src/unipoll_api/routes/v2/groups.py b/src/unipoll_api/routes/v2/groups.py index 931f03d..51217cc 100644 --- a/src/unipoll_api/routes/v2/groups.py +++ b/src/unipoll_api/routes/v2/groups.py @@ -35,7 +35,7 @@ async def create_group(input_data: GroupSchemas.GroupCreateRequest = Body(...)): raise HTTPException(status_code=e.code, detail=str(e)) -query_params = list[Literal["policies", "members", "all"]] +query_params = list[Literal["workspace", "policies", "members", "all"]] # Get group info by id @@ -50,7 +50,8 @@ async def get_group(group: Group = Depends(Dependencies.get_group), params = {} if include: if "all" in include: - params = {"include_members": True, "include_policies": True} + # params = {"include_members": True, "include_policies": True} + params = {"include_" + param: True for param in ["members", "policies", "workspace"]} else: if "members" in include: params["include_members"] = True From 4ad4f7b590e8a4b8b140d409d7e138f891424f7c Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sun, 17 Dec 2023 21:44:17 -0700 Subject: [PATCH 32/33] refactor: Added workspace ID to WorkspaceCreateOutput example --- src/unipoll_api/schemas/workspace.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/unipoll_api/schemas/workspace.py b/src/unipoll_api/schemas/workspace.py index be4b25f..fcffb0c 100644 --- a/src/unipoll_api/schemas/workspace.py +++ b/src/unipoll_api/schemas/workspace.py @@ -80,6 +80,7 @@ class WorkspaceCreateOutput(BaseModel): description: str = Field(title="Description") model_config = ConfigDict(json_schema_extra={ "example": { + "id": "5eb7cf5a86d9755df3a6c593", "name": "Workspace 01", "description": "This is an example workspace", } From dcdccbb18ace5bb5c41b2517f247a67a6a6389a5 Mon Sep 17 00:00:00 2001 From: Michael Pisman Date: Sun, 17 Dec 2023 21:44:45 -0700 Subject: [PATCH 33/33] style: Remove unnecessary blank line in get_member_by_account --- src/unipoll_api/dependencies.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/unipoll_api/dependencies.py b/src/unipoll_api/dependencies.py index f0e6bf3..cd5c713 100644 --- a/src/unipoll_api/dependencies.py +++ b/src/unipoll_api/dependencies.py @@ -46,7 +46,6 @@ async def get_member_by_account(account: Account, resource: Workspace | Group) - """ Returns a member with the given id. """ - for member in resource.members: if member.account.id == account.id: # type: ignore return member # type: ignore