Skip to content

Commit

Permalink
feat:(Loris): Endpoints for enabling and disabling integration for wo…
Browse files Browse the repository at this point in the history
…rkspaces(M2-7040) (#1420)

* M2-7040:api for enabling and disabling integration
  • Loading branch information
Damirkhon authored Jun 21, 2024
1 parent 80b08f7 commit dadcced
Show file tree
Hide file tree
Showing 15 changed files with 269 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/apps/integrations/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from apps.integrations.api.integrations import * # noqa: F401, F403
31 changes: 31 additions & 0 deletions src/apps/integrations/api/integrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from fastapi import Body, Depends

from apps.authentication.deps import get_current_user
from apps.integrations.domain import Integration, IntegrationFilter
from apps.integrations.service import IntegrationService
from apps.shared.domain import ResponseMulti
from apps.shared.query_params import QueryParams, parse_query_params
from apps.users.domain import User
from infrastructure.database import atomic
from infrastructure.database.deps import get_session

__all__ = ["enable_integration", "disable_integration"]


async def enable_integration(
user: User = Depends(get_current_user),
session=Depends(get_session),
integrations: list[Integration] = Body(...),
) -> ResponseMulti[Integration]:
async with atomic(session):
integrations = await IntegrationService(session, user).enable_integration(integrations)
return ResponseMulti(result=integrations, count=len(integrations))


async def disable_integration(
user: User = Depends(get_current_user),
session=Depends(get_session),
query_params: QueryParams = Depends(parse_query_params(IntegrationFilter)),
):
async with atomic(session):
await IntegrationService(session, user).disable_integration(query_params)
15 changes: 15 additions & 0 deletions src/apps/integrations/domain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from enum import Enum

from apps.shared.domain import InternalModel


class AvailableIntegrations(str, Enum):
LORIS = "LORIS"


class Integration(InternalModel):
integration_type: AvailableIntegrations


class IntegrationFilter(InternalModel):
integration_types: list[AvailableIntegrations] | None
7 changes: 7 additions & 0 deletions src/apps/integrations/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from gettext import gettext as _

from apps.shared.exception import ValidationError


class UniqueIntegrationError(ValidationError):
message = _("Integrations must be unique.")
30 changes: 30 additions & 0 deletions src/apps/integrations/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from fastapi.routing import APIRouter
from starlette import status

from apps.integrations.api import disable_integration, enable_integration
from apps.shared.domain.response import AUTHENTICATION_ERROR_RESPONSES, DEFAULT_OPENAPI_RESPONSE

router = APIRouter(prefix="/integrations", tags=["Integration"])


router.post(
"/",
description="This endpoint is used to enable integration\
options for a workspace",
status_code=status.HTTP_200_OK,
responses={
**DEFAULT_OPENAPI_RESPONSE,
**AUTHENTICATION_ERROR_RESPONSES,
},
)(enable_integration)

router.delete(
"/",
description="This endpoint is used to remove integrations\
from a workspace",
status_code=status.HTTP_204_NO_CONTENT,
responses={
**DEFAULT_OPENAPI_RESPONSE,
**AUTHENTICATION_ERROR_RESPONSES,
},
)(disable_integration)
1 change: 1 addition & 0 deletions src/apps/integrations/service/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from apps.integrations.service.integrations import * # noqa: F401, F403
42 changes: 42 additions & 0 deletions src/apps/integrations/service/integrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from apps.integrations.domain import AvailableIntegrations, Integration
from apps.integrations.errors import UniqueIntegrationError
from apps.shared.query_params import QueryParams
from apps.users.domain import User
from apps.workspaces.crud.workspaces import UserWorkspaceCRUD

__all__ = [
"IntegrationService",
]


class IntegrationService:
def __init__(self, session, user: User) -> None:
self.session = session
self.user = user

async def enable_integration(self, integrations: list[Integration]):
workspace = await UserWorkspaceCRUD(self.session).get_by_user_id(user_id_=self.user.id)
integration_names: list[AvailableIntegrations] = [integration.integration_type for integration in integrations]
if len(set(integration_names)) != len(integration_names):
raise UniqueIntegrationError()

workspace.integrations = [integration.dict() for integration in integrations]
workspace = await UserWorkspaceCRUD(self.session).save(workspace)
return [Integration.parse_obj(integration) for integration in workspace.integrations]

async def disable_integration(self, query: QueryParams):
workspace = await UserWorkspaceCRUD(self.session).get_by_user_id(user_id_=self.user.id)
print(query)
if query.filters:
workspace.integrations = (
[
integration
for integration in workspace.integrations
if integration["integration_type"] not in query.filters["integration_types"]
]
if workspace.integrations
else workspace.integrations
)
else:
workspace.integrations = None
await UserWorkspaceCRUD(self.session).save(workspace)
Empty file.
53 changes: 53 additions & 0 deletions src/apps/integrations/tests/test_integrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from apps.integrations.errors import UniqueIntegrationError
from apps.integrations.router import router as integration_router
from apps.shared.test import BaseTest
from apps.shared.test.client import TestClient
from apps.users.domain import User


class TestIntegrationRouter(BaseTest):
fixtures = [
"workspaces/fixtures/workspaces.json",
]
enable_integration_url = integration_router.url_path_for("enable_integration")
disable_integration_url = integration_router.url_path_for("disable_integration")

async def test_enable_integration(
self,
client: TestClient,
tom: User,
):
integration_data = [
{"integrationType": "LORIS"},
]
client.login(tom)
response = await client.post(self.enable_integration_url, data=integration_data)
assert response.status_code == 200

async def test_disable_integration(
self,
client: TestClient,
tom: User,
):
client.login(tom)
response = await client.delete(
self.disable_integration_url,
)
assert response.status_code == 204

async def test_enable_integration_unique_error(
self,
client: TestClient,
tom: User,
):
integration_data = [
{"integrationType": "LORIS"},
{"integrationType": "LORIS"},
]
client.login(tom)
response = await client.post(self.enable_integration_url, data=integration_data)
assert response.status_code == 400

result = response.json()["result"]
assert len(result) == 1
assert result[0]["message"] == UniqueIntegrationError.message
19 changes: 19 additions & 0 deletions src/apps/integrations/tests/unit/domain/test_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest

from apps.integrations.domain import Integration


def test_integration_model():
integration_data = {
"integration_type": "LORIS",
}
item = Integration(**integration_data)
assert item.integration_type == integration_data["integration_type"]


def test_integration_model_error():
integration_data = {
"integration_type": "MORRIS",
}
with pytest.raises(Exception):
Integration(**integration_data)
5 changes: 3 additions & 2 deletions src/apps/workspaces/db/schemas/user_workspace.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from sqlalchemy import ARRAY, Boolean, Column, ForeignKey, String, Unicode
from sqlalchemy import Boolean, Column, ForeignKey, Unicode
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy_utils import StringEncryptedType

from apps.shared.encryption import get_key
Expand All @@ -24,4 +25,4 @@ class UserWorkspaceSchema(Base):
storage_url = Column(StringEncryptedType(Unicode, get_key))
storage_bucket = Column(StringEncryptedType(Unicode, get_key))
use_arbitrary = Column(Boolean(), default=False)
integrations = Column(ARRAY(String(32)))
integrations = Column(JSONB(), default=dict())
5 changes: 3 additions & 2 deletions src/apps/workspaces/domain/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sqlalchemy_utils import StringEncryptedType

from apps.applets.domain.base import Encryption
from apps.integrations.domain import Integration
from apps.shared.domain import InternalModel, PublicModel
from apps.shared.encryption import get_key
from apps.workspaces.constants import StorageType
Expand Down Expand Up @@ -39,7 +40,7 @@ class PublicWorkspace(PublicModel):
"which is consists of 'first name', 'last name' of user "
"which is applet owner and prefix",
)
integrations: list[str] | None = Field(
integrations: list[Integration] | None = Field(
description="This field represents the list of integrations in which the workspace participates"
)

Expand All @@ -57,7 +58,7 @@ class UserWorkspace(InternalModel):
"which is consists of 'first name', 'last name' of user "
"which is applet owner and prefix",
)
integrations: list[str] | None = Field(
integrations: list[Integration] | None = Field(
description="This field represents the list of integrations in which the workspace participates"
)

Expand Down
26 changes: 24 additions & 2 deletions src/apps/workspaces/service/user_access.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import uuid
from gettext import gettext as _
from typing import List

import pydantic

import config
from apps.answers.crud.answers import AnswersCRUD
from apps.applets.crud import UserAppletAccessCRUD
from apps.integrations.domain import Integration
from apps.invitations.domain import ReviewerMeta
from apps.invitations.errors import RespondentsNotSet
from apps.shared.exception import AccessDeniedError, ValidationError
Expand Down Expand Up @@ -49,15 +53,33 @@ async def get_user_workspaces(self) -> list[UserWorkspace]:
user_ids.append(self._user_id)

workspaces = await UserWorkspaceCRUD(self.session).get_by_ids(user_ids)
return [UserWorkspace.from_orm(workspace) for workspace in workspaces]
return [
UserWorkspace(
user_id=workspace.user_id,
workspace_name=workspace.workspace_name,
integrations=pydantic.parse_obj_as(List[Integration], workspace.integrations)
if workspace.integrations
else None,
)
for workspace in workspaces
]

async def get_super_admin_workspaces(self) -> list[UserWorkspace]:
"""
Returns the super admins workspaces.
"""

workspaces = await UserWorkspaceCRUD(self.session).get_all()
return [UserWorkspace.from_orm(workspace) for workspace in workspaces]
return [
UserWorkspace(
user_id=workspace.user_id,
workspace_name=workspace.workspace_name,
integrations=pydantic.parse_obj_as(List[Integration], workspace.integrations)
if workspace.integrations
else None,
)
for workspace in workspaces
]

async def remove_manager_access(self, schema: RemoveManagerAccess):
"""Remove manager access from a specific user."""
Expand Down
2 changes: 2 additions & 0 deletions src/infrastructure/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import apps.folders.router as folders
import apps.healthcheck.router as healthcheck
import apps.integrations.loris.router as loris
import apps.integrations.router as integrations
import apps.invitations.router as invitations
import apps.library.router as library
import apps.logs.router as logs
Expand Down Expand Up @@ -63,6 +64,7 @@
ws_alerts.router,
subject_router.router,
loris.router,
integrations.router,
)

# Declare your middlewares here
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Change integrations field type in workspaces table
Revision ID: d877f29be2f0
Revises: 70c50aba13b7
Create Date: 2024-06-17 15:46:33.965375
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "d877f29be2f0"
down_revision = "70c50aba13b7"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column("users_workspaces", "integrations", type_=postgresql.JSONB(astext_type=sa.Text()),postgresql_using="array_to_json(integrations);")
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("users_workspaces", "integrations")

op.add_column(
"users_workspaces",
sa.Column(
"integrations",
sa.ARRAY(sa.String(length=32)),

nullable=True,
),
)
# ### end Alembic commands ###

0 comments on commit dadcced

Please sign in to comment.