Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
aweiland committed Jul 22, 2024
2 parents 8d102ec + 7e0c052 commit 6372922
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 13 deletions.
2 changes: 1 addition & 1 deletion src/apps/answers/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ async def _get_answer_relation(
return Relation.admin
raise ValidationError("Subject relation not found")

return relation
return relation.relation

async def _create_answer(self, applet_answer: AppletAnswerCreate) -> AnswerSchema:
assert self.user_id
Expand Down
17 changes: 17 additions & 0 deletions src/apps/shared/subjects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from datetime import datetime

from apps.subjects.domain import SubjectRelation


def is_take_now_relation(relation: SubjectRelation | None) -> bool:
return relation is not None and relation.relation == "take-now" and relation.meta is not None


def is_valid_take_now_relation(relation: SubjectRelation | None) -> bool:
if is_take_now_relation(relation):
assert isinstance(relation, SubjectRelation)
assert isinstance(relation.meta, dict)
expires_at = datetime.fromisoformat(relation.meta["expiresAt"])
return expires_at > datetime.now()

return False
95 changes: 92 additions & 3 deletions src/apps/subjects/api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import uuid
from datetime import datetime, timedelta

from fastapi import Body, Depends
from fastapi.exceptions import RequestValidationError
from pydantic.error_wrappers import ErrorWrapper
from sqlalchemy.ext.asyncio import AsyncSession

from apps.answers.deps.preprocess_arbitrary import get_answer_session_by_subject
from apps.answers.deps.preprocess_arbitrary import get_answer_session, get_answer_session_by_subject
from apps.answers.service import AnswerService
from apps.applets.service import AppletService
from apps.authentication.deps import get_current_user
Expand All @@ -14,6 +15,7 @@
from apps.shared.domain import Response
from apps.shared.exception import NotFoundError, ValidationError
from apps.shared.response import EmptyResponse
from apps.shared.subjects import is_take_now_relation, is_valid_take_now_relation
from apps.subjects.domain import (
Subject,
SubjectCreate,
Expand Down Expand Up @@ -74,6 +76,11 @@ async def create_relation(
raise ValidationError("applet_id doesn't match")

await CheckAccessService(session, user.id).check_applet_invite_access(target_subject.applet_id)

existing_relation = await service.get_relation(source_subject_id, subject_id)
if is_take_now_relation(existing_relation):
await service.delete_relation(subject_id, source_subject_id)

async with atomic(session):
await service.create_relation(
subject_id,
Expand All @@ -83,6 +90,46 @@ async def create_relation(
return EmptyResponse()


async def create_temporary_multiinformant_relation(
subject_id: uuid.UUID,
source_subject_id: uuid.UUID,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
):
service = SubjectsService(session, user.id)
source_subject = await service.get_if_soft_exist(source_subject_id)
target_subject = await service.get_if_soft_exist(subject_id)
if not source_subject:
raise NotFoundError(f"Subject {source_subject_id} not found")
if not target_subject:
raise NotFoundError(f"Subject {subject_id} not found")
if source_subject.applet_id != target_subject.applet_id:
raise ValidationError("applet_id doesn't match")

check_access_service = CheckAccessService(session, user.id)

# Only owners and managers can initiate take now from the admin panel
await check_access_service.check_applet_manager_list_access(target_subject.applet_id)

existing_relation = await service.get_relation(source_subject_id, subject_id)
if existing_relation:
if existing_relation.relation != "take-now" or existing_relation.meta is None:
# There is already a non-temporary relation. Do nothing
return EmptyResponse()

expires_at = datetime.fromisoformat(existing_relation.meta["expiresAt"])
if expires_at > datetime.now():
# The current temporary relation is still valid. Do nothing
return EmptyResponse()

await service.delete_relation(subject_id, source_subject_id)

async with atomic(session):
expires_at = datetime.now() + timedelta(days=1)
await service.create_relation(subject_id, source_subject_id, "take-now", {"expiresAt": expires_at.isoformat()})
return EmptyResponse()


async def delete_relation(
subject_id: uuid.UUID,
source_subject_id: uuid.UUID,
Expand Down Expand Up @@ -179,10 +226,20 @@ async def get_subject(
session: AsyncSession = Depends(get_session),
arbitrary_session: AsyncSession | None = Depends(get_answer_session_by_subject),
) -> Response[SubjectReadResponse]:
subject = await SubjectsService(session, user.id).get(subject_id)
subjects_service = SubjectsService(session, user.id)
subject = await subjects_service.get(subject_id)
if not subject:
raise NotFoundError()
await CheckAccessService(session, user.id).check_subject_subject_access(subject.applet_id, subject_id)

user_subject = await subjects_service.get_by_user_and_applet(user.id, subject.applet_id)
if user_subject:
relation = await subjects_service.get_relation(user_subject.id, subject_id)
has_relation = relation is not None and (
relation.relation != "take-now" or is_valid_take_now_relation(relation)
)
if not has_relation:
await CheckAccessService(session, user.id).check_subject_subject_access(subject.applet_id, subject_id)

answer_dates = await AnswerService(
user_id=user.id, session=session, arbitrary_session=arbitrary_session
).get_last_answer_dates([subject.id], subject.applet_id)
Expand All @@ -198,3 +255,35 @@ async def get_subject(
user_id=subject.user_id,
)
)


async def get_my_subject(
applet_id: uuid.UUID,
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
arbitrary_session: AsyncSession | None = Depends(get_answer_session),
) -> Response[SubjectReadResponse]:
# Check if applet exists
await AppletService(session, user.id).exist_by_id(applet_id)

# Check if user has access to applet
await CheckAccessService(session, user.id).check_applet_detail_access(applet_id)

subject = await SubjectsService(session, user.id).get_by_user_and_applet(user.id, applet_id)
if not subject:
raise NotFoundError()

answer_dates = await AnswerService(
user_id=user.id, session=session, arbitrary_session=arbitrary_session
).get_last_answer_dates([subject.id], subject.applet_id)
return Response(
result=SubjectReadResponse(
id=subject.id,
secret_user_id=subject.secret_user_id,
nickname=subject.nickname,
last_seen=answer_dates.get(subject.id),
tag=subject.tag,
applet_id=subject.applet_id,
user_id=subject.user_id,
)
)
11 changes: 7 additions & 4 deletions src/apps/subjects/crud/subject.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from apps.invitations.constants import InvitationStatus
from apps.invitations.db import InvitationSchema
from apps.subjects.db.schemas import SubjectRelationSchema, SubjectSchema
from apps.subjects.domain import SubjectCreate
from apps.subjects.domain import SubjectCreate, SubjectRelation
from infrastructure.database.crud import BaseCRUD

__all__ = ["SubjectsCrud"]
Expand Down Expand Up @@ -117,14 +117,17 @@ async def get_relation(
self,
source_subject_id: uuid.UUID,
target_subject_id: uuid.UUID,
) -> str | None:
query: Query = select(SubjectRelationSchema.relation)
) -> SubjectRelation | None:
query: Query = select(SubjectRelationSchema)
query = query.where(
SubjectRelationSchema.source_subject_id == source_subject_id,
SubjectRelationSchema.target_subject_id == target_subject_id,
)
result = await self._execute(query)
return result.scalar_one_or_none()
schema = result.scalars().one_or_none()
if not schema:
return None
return SubjectRelation.from_orm(schema)

async def exist(self, subject_id: uuid.UUID, applet_id: uuid.UUID) -> bool:
query: Query = select(SubjectSchema.id)
Expand Down
2 changes: 2 additions & 0 deletions src/apps/subjects/db/schemas.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from sqlalchemy import Column, ForeignKey, Index, String, Unicode
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy_utils import StringEncryptedType

from apps.shared.encryption import get_key
Expand Down Expand Up @@ -42,6 +43,7 @@ class SubjectRelationSchema(Base):
index=True,
)
relation = Column(String(length=20), nullable=False)
meta = Column(JSONB(), nullable=True)

__table_args__ = (
Index(
Expand Down
7 changes: 7 additions & 0 deletions src/apps/subjects/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,10 @@ class SubjectReadResponse(SubjectUpdateRequest):
last_seen: datetime.datetime | None
applet_id: uuid.UUID
user_id: uuid.UUID | None


class SubjectRelation(InternalModel):
source_subject_id: uuid.UUID
target_subject_id: uuid.UUID
relation: str
meta: dict | None
12 changes: 12 additions & 0 deletions src/apps/subjects/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from apps.subjects.api import (
create_relation,
create_subject,
create_temporary_multiinformant_relation,
delete_relation,
delete_subject,
get_subject,
Expand Down Expand Up @@ -81,6 +82,17 @@ async def create_shell_account(
},
)(create_relation)

router.post(
"/{subject_id}/relations/{source_subject_id}/multiinformant-assessment",
response_model=Response[SubjectFull],
status_code=status.HTTP_200_OK,
responses={
status.HTTP_201_CREATED: {"model": Response[SubjectFull]},
**DEFAULT_OPENAPI_RESPONSE,
**AUTHENTICATION_ERROR_RESPONSES,
},
)(create_temporary_multiinformant_relation)


router.delete(
"/{subject_id}/relations/{source_subject_id}",
Expand Down
12 changes: 7 additions & 5 deletions src/apps/subjects/services/subjects.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import uuid
from typing import Any

from sqlalchemy.ext.asyncio import AsyncSession

Expand All @@ -7,7 +8,7 @@
from apps.invitations.crud import InvitationCRUD
from apps.subjects.crud import SubjectsCrud
from apps.subjects.db.schemas import SubjectRelationSchema, SubjectSchema
from apps.subjects.domain import Subject, SubjectCreate
from apps.subjects.domain import Subject, SubjectCreate, SubjectRelation

__all__ = ["SubjectsService"]

Expand Down Expand Up @@ -75,24 +76,25 @@ async def get_if_soft_exist(self, id_: uuid.UUID) -> Subject | None:
return None

async def create_relation(
self,
subject_id: uuid.UUID,
source_subject_id: uuid.UUID,
relation: str,
self, subject_id: uuid.UUID, source_subject_id: uuid.UUID, relation: str, meta: dict[str, Any] = {}
):
repository = SubjectsCrud(self.session)
await repository.create_relation(
SubjectRelationSchema(
source_subject_id=source_subject_id,
target_subject_id=subject_id,
relation=relation,
meta=meta,
)
)

async def delete_relation(self, subject_id: uuid.UUID, source_subject_id: uuid.UUID):
repository = SubjectsCrud(self.session)
await repository.delete_relation(subject_id, source_subject_id)

async def get_relation(self, source_subject_id: uuid.UUID, target_subject_id: uuid.UUID) -> SubjectRelation | None:
return await SubjectsCrud(self.session).get_relation(source_subject_id, target_subject_id)

async def get_by_secret_id(self, applet_id: uuid.UUID, secret_id: str) -> Subject | None:
subject = await SubjectsCrud(self.session).get_by_secret_id(applet_id, secret_id)
if subject:
Expand Down
Loading

0 comments on commit 6372922

Please sign in to comment.