Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
49 changes: 48 additions & 1 deletion modal_backend/routes/notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from fastapi_sqlalchemy import db

from modal_backend import settings
from modal_backend.models.db import ModalStatus, Note, NoteTypeEnum
from modal_backend.models.db import ModalStatus, Note, NoteResponse, NoteTypeEnum
from modal_backend.schemas.base import StatusResponseModel
from modal_backend.schemas.models import (
NoteChoiceGet,
Expand All @@ -17,6 +17,9 @@
NoteInfoPost,
NoteRatingGet,
NoteRatingPost,
NoteResponseChoiceGet,
NoteResponseRatingGet,
NoteResponseTextGet,
NoteStatus,
NoteTextGet,
NoteTextPost,
Expand Down Expand Up @@ -209,6 +212,50 @@ async def create_note_images(
return NoteImageGet.model_validate(new_note)


@note.get(
"/{id}/responses",
response_model=list[Union[NoteResponseRatingGet, NoteResponseTextGet, NoteResponseChoiceGet]],
response_model_exclude_none=True,
)
async def get_note_responses(
id: int,
limit: int = Query(10, ge=0, description="Лимит записей"),
offset: int = Query(0, ge=0, description="Смещение записей на N+offset, где N - первая запись"),
user=Depends(UnionAuth(scopes=["modal.note.read"])),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

если делаешь новый скоуп, обязательно указывай в превью к пр в комменте первом желательно жирным)
чтобы не упустить и мне вовремя создать нужный скоуп

) -> list[Union[NoteResponseRatingGet, NoteResponseTextGet, NoteResponseChoiceGet]]:
"""
Возвращает ответы пользователя по модалке.

Для типов `type_id=2`, `type_id=3`, `type_id=4` возвращаются только те поля,
которые заполнены в записи ответа: `rating`, `text` или `selected_choices`.
Для типов `type_id=1` и `type_id=5` пустой список.

`limit` - максимальное количество возвращаемых модалок

`offset` - смещение, определяющее, с какой по порядку модалки начинать выборку.
Если без смещения возвращается модалка с условным номером N,
то при значении offset = X будет возвращаться модалка с номером N + X

Права: `["modal.note.read"]`
"""
note_obj = Note.get(session=db.session, id=id)
if note_obj.type_id in (NoteTypeEnum.INFO, NoteTypeEnum.IMAGE):
return []

schema_type = {
NoteTypeEnum.RATING: NoteResponseRatingGet,
NoteTypeEnum.TEXT: NoteResponseTextGet,
NoteTypeEnum.CHOICE: NoteResponseChoiceGet,
}.get(note_obj.type_id)
if schema_type is None:
return []

responses = (
NoteResponse.query(session=db.session).filter(NoteResponse.note_id == id).offset(offset).limit(limit).all()
)
return [schema_type.model_validate(response) for response in responses]


@note.patch("/{id}/status", response_model=NoteStatus)
async def update_note_status(id: int, user=Depends(UnionAuth(scopes=["modal.note.patch"]))) -> NoteStatus:
"""
Expand Down
19 changes: 19 additions & 0 deletions modal_backend/schemas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,22 @@ class GroupPost(Base):
class NoteStatus(Base):
id: int
status: ModalStatus


class NoteResponseGet(Base):
id: int
note_id: int
user_id: int
created_at: datetime.datetime


class NoteResponseRatingGet(NoteResponseGet): # type_id=2
rating: int | None = None


class NoteResponseTextGet(NoteResponseGet): # type_id=3
text: str | None = None


class NoteResponseChoiceGet(NoteResponseGet): # type_id=4
selected_choices: list[dict] | None = None
139 changes: 139 additions & 0 deletions tests/test_routes/test_note_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
from datetime import datetime, timezone

import pytest
from starlette import status

from modal_backend.models.db import NoteResponse
from modal_backend.schemas.models import NoteResponseChoiceGet, NoteResponseRatingGet, NoteResponseTextGet

url: str = "/notification"


@pytest.mark.parametrize(
"status_code, note_index, response_data, response_model, response_field, response_value, absent_fields",
[
(
status.HTTP_200_OK,
1,
{"rating": 5},
NoteResponseRatingGet,
"rating",
5,
{"text", "selected_choices"},
),
(
status.HTTP_200_OK,
2,
{"text": "Тестовый ответ"},
NoteResponseTextGet,
"text",
"Тестовый ответ",
{"rating", "selected_choices"},
),
(
status.HTTP_200_OK,
3,
{"selected_choices": [{"id": 1, "text": "Да"}]},
NoteResponseChoiceGet,
"selected_choices",
[{"id": 1, "text": "Да"}],
{"rating", "text"},
),
],
)
def test_get_note_responses_by_note_type(
client,
dbsession,
notes,
status_code,
note_index,
response_data,
response_model,
response_field,
response_value,
absent_fields,
):
note = notes[note_index]
note_response = NoteResponse(
note_id=note.id,
user_id=101,
created_at=datetime.now(timezone.utc).replace(tzinfo=None),
**response_data,
)
dbsession.add(note_response)
dbsession.commit()

try:
response = client.get(f"{url}/{note.id}/responses")
assert response.status_code == status_code

response_data = response.json()
assert len(response_data) == 1
response_model.model_validate(response_data[0], extra="forbid")
response_item = response_data[0]
assert response_item[response_field] == response_value
assert absent_fields.isdisjoint(response_item)
assert all(value is not None for value in response_item.values())
finally:
dbsession.delete(note_response)
dbsession.commit()


@pytest.mark.parametrize("note_index", [0, 4])
def test_get_note_responses_returns_empty_for_info_and_image(client, dbsession, notes, note_index):
note = notes[note_index]
note_response = NoteResponse(
note_id=note.id,
user_id=101,
text="Ответ не должен возвращаться",
created_at=datetime.now(timezone.utc).replace(tzinfo=None),
)
dbsession.add(note_response)
dbsession.commit()

try:
response = client.get(f"{url}/{note.id}/responses")
assert response.status_code == status.HTTP_200_OK
assert response.json() == []
finally:
dbsession.delete(note_response)
dbsession.commit()


@pytest.mark.parametrize(
"status_code, limit, offset",
[
(status.HTTP_200_OK, 1, 1),
(status.HTTP_200_OK, 10, 0),
(status.HTTP_200_OK, 0, 0),
(status.HTTP_422_UNPROCESSABLE_CONTENT, -1, 0),
(status.HTTP_422_UNPROCESSABLE_CONTENT, 10, -1),
],
)
def test_get_note_responses_limit_and_offset(client, dbsession, notes, status_code, limit, offset):
note = notes[2]
note_responses = [
NoteResponse(
note_id=note.id,
user_id=user_id,
text=f"Ответ {user_id}",
created_at=datetime.now(timezone.utc).replace(tzinfo=None),
)
for user_id in [101, 102, 103]
]
dbsession.add_all(note_responses)
dbsession.commit()

try:
response = client.get(f"{url}/{note.id}/responses", params={"limit": limit, "offset": offset})
assert response.status_code == status_code

if status_code == status.HTTP_200_OK:
response_data = response.json()
assert len(response_data) == min(limit, max(0, len(note_responses) - offset))
for response_item in response_data:
NoteResponseTextGet.model_validate(response_item, extra="forbid")
finally:
for note_response in note_responses:
dbsession.delete(note_response)
dbsession.commit()
Loading