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/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 diff --git a/src/unipoll_api/actions/poll.py b/src/unipoll_api/actions/poll.py index d681d7e..66b1ae3 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, @@ -18,11 +19,15 @@ async def get_polls(workspace: Workspace | None = None, except ResourceExceptions.UserNotAuthorized: poll: Poll for poll in workspace.polls: # type: ignore + try: + polls.append(await get_poll(poll, check_permissions)) # type: ignore + except ResourceExceptions.UserNotAuthorized: + continue + if poll.public: polls.append(poll) else: polls.append(await get_poll(poll, check_permissions)) # type: ignore - poll_list = [] # Build poll list and return the result for poll in polls: # type: ignore @@ -37,6 +42,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 @@ -55,6 +62,9 @@ async def create_poll(workspace: Workspace, # 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 @@ -76,10 +86,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)).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 @@ -110,7 +124,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 @@ -125,7 +145,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): diff --git a/src/unipoll_api/dependencies.py b/src/unipoll_api/dependencies.py index 8110615..4a7cf94 100644 --- a/src/unipoll_api/dependencies.py +++ b/src/unipoll_api/dependencies.py @@ -47,11 +47,10 @@ 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 - raise Exceptions.ResourceExceptions.ResourceNotFound("member", account.id) + raise Exceptions.ResourceExceptions.UserNotMember(resource, account) async def websocket_auth(session: Annotated[str | None, Cookie()] = None, @@ -103,7 +102,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 diff --git a/src/unipoll_api/exceptions/group.py b/src/unipoll_api/exceptions/group.py index a36aec9..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 @@ -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 @@ -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/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..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") @@ -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..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 @@ -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 @@ -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) 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 bc4ee60..7148332 100644 --- a/src/unipoll_api/routes/v1/group.py +++ b/src/unipoll_api/routes/v1/group.py @@ -5,34 +5,65 @@ from unipoll_api.actions.__interface__ 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() -query_params = list[Literal["policies", "members", "all"]] +query_params = list[Literal["workspace", "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 = {} 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)) @@ -40,12 +71,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 +86,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 +101,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 +116,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 +131,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 +146,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 +163,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 +185,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", +# 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_permissions(): +async def get_group_member_permissions(workspace: Workspace = Depends(Dependencies.get_workspace), + group: Group = Depends(Dependencies.get_group)): try: - return await PermissionsActions.get_group_permissions() + 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)) + 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 4ca397d..79f0f62 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.actions.__interface__ import WorkspaceActions, MembersActions, PolicyActions, GroupActions, PermissionsActions, PollActions +from unipoll_api import actions, AccountManager from unipoll_api.exceptions.resource import APIException from unipoll_api.documents import Workspace, ResourceID, Policy, Member -from unipoll_api.schemas import WorkspaceSchemas, PolicySchemas, GroupSchemas, MemberSchemas, PollSchemas +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,11 +238,20 @@ 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", +# 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: + #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")) + async def get_workspace_permissions(): try: return await PermissionsActions.get_workspace_permissions() diff --git a/src/unipoll_api/routes/v2/groups.py b/src/unipoll_api/routes/v2/groups.py index ea7860e..15ed389 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 diff --git a/src/unipoll_api/routes/v2/permissions.py b/src/unipoll_api/routes/v2/permissions.py index 8678c0b..6f935e6 100644 --- a/src/unipoll_api/routes/v2/permissions.py +++ b/src/unipoll_api/routes/v2/permissions.py @@ -1,7 +1,11 @@ -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, Depends +from unipoll_api import dependencies as Dependencies +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.__interface__ import PermissionsActions +from unipoll_api.actions import PermissionsActions +from unipoll_api.utils import Permissions router = APIRouter() @@ -26,3 +30,42 @@ 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)): + try: + + 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": { + "workspace": { + "id": str(workspace.id), + "permissions": Permissions.convert_permission_to_string(workspace_permissions, "Workspace"), + }, + "groups": [ { + "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() + ] + # } + } + + 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/v2/polls.py b/src/unipoll_api/routes/v2/polls.py index 041f6ab..0724f45 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)) diff --git a/src/unipoll_api/schemas/poll.py b/src/unipoll_api/schemas/poll.py index 28547b1..439e537 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,11 +68,23 @@ class PollList(BaseModel): class CreatePollRequest(BaseModel): name: str - workspace: ResourceID + workspace: Optional[ResourceID] = None description: str 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): 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): diff --git a/src/unipoll_api/schemas/workspace.py b/src/unipoll_api/schemas/workspace.py index f331242..7951c2e 100644 --- a/src/unipoll_api/schemas/workspace.py +++ b/src/unipoll_api/schemas/workspace.py @@ -61,8 +61,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", @@ -73,8 +73,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", @@ -90,6 +90,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", } diff --git a/src/unipoll_api/utils/permissions.py b/src/unipoll_api/utils/permissions.py index c9e292d..4bd2d05 100644 --- a/src/unipoll_api/utils/permissions.py +++ b/src/unipoll_api/utils/permissions.py @@ -1,10 +1,11 @@ from enum import IntFlag +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 @@ -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: str) -> list[str]: + permission_type = PermissionTypes[resource_type] + if permissions == permission_type(0): + return [] + 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] @@ -146,22 +154,31 @@ 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) - - 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] + 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 + 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)) 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