Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:(Loris): Endpoints for enabling and disabling integration for workspaces(M2-7040) #1420

Merged
merged 5 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
29 changes: 29 additions & 0 deletions src/apps/integrations/api/integrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from fastapi import Body, Depends

from apps.authentication.deps import get_current_user
from apps.integrations.domain import Integration
from apps.integrations.service import IntegrationService
from apps.shared.domain import ResponseMulti
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)


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

from apps.shared.domain import InternalModel


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


class Integration(InternalModel):
integration_type: AvailableIntegrations
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
31 changes: 31 additions & 0 deletions src/apps/integrations/service/integrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import json

from apps.integrations.domain import AvailableIntegrations, Integration
from apps.integrations.errors import UniqueIntegrationError
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it requirement that workspace owner the only person who can manage integrations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, there is no exact requirements regarding workspace integration accesses

integration_names: list[AvailableIntegrations] = [integration.integration_type for integration in integrations]
if len(set(integration_names)) != len(integration_names):
raise UniqueIntegrationError()

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

async def disable_integration(self):
workspace = await UserWorkspaceCRUD(self.session).get_by_user_id(user_id_=self.user.id)
workspace.integrations = None
await UserWorkspaceCRUD(self.session).save(workspace)
Empty file.
19 changes: 19 additions & 0 deletions src/apps/integrations/tests/test_domain.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)
53 changes: 53 additions & 0 deletions src/apps/integrations/tests/test_routes.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
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
20 changes: 18 additions & 2 deletions src/apps/workspaces/service/user_access.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import json
import uuid
from gettext import gettext as _

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 +51,29 @@ 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=[Integration.parse_obj(integration) for integration in json.loads(workspace.integrations)],
)
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=[Integration.parse_obj(integration) for integration in json.loads(workspace.integrations)],
)
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 ###
Loading