Repository navigation
added validation when create #41
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,63 @@ | ||
| from datetime import datetime, timezone | ||
| from typing import Union | ||
|
|
||
| from requests import Session | ||
|
|
||
| from modal_backend.exceptions import AlreadyExists, ForbiddenAction, ObjectNotFound | ||
| from modal_backend.exceptions import AlreadyExists, ForbiddenAction, ObjectNotFound, ValueError | ||
| from modal_backend.models.db import Group, ModalStatus, Note, Service | ||
| from modal_backend.schemas.base import StatusResponseModel | ||
| from modal_backend.schemas.models import GroupPost, ServicePost | ||
| from modal_backend.schemas.models import ( | ||
| GroupPost, | ||
| NoteChoicePost, | ||
| NoteImagePost, | ||
| NoteInfoPost, | ||
| NoteRatingPost, | ||
| NoteTextPost, | ||
| ServicePost, | ||
| ) | ||
|
|
||
|
|
||
| class NoteService: | ||
| """ | ||
| Сервис для работы с логикой Notifications и базой данных | ||
| """ | ||
|
|
||
| @classmethod | ||
| async def validate_note( | ||
| cls, db: Session, note: Union[NoteInfoPost, NoteRatingPost, NoteTextPost, NoteChoicePost, NoteImagePost] | ||
| ): | ||
| """Валидация полей при создании note""" | ||
| if note.is_always: | ||
| note.end_ts = None | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. а зачем такая проверка если в схеме прописано что дефолт none
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. и тогда сразу вопрос почему start_ts не делал?) только для конца |
||
| elif note.end_ts is None or (note.start_ts is not None and note.start_ts >= note.end_ts): | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. лучше просто or все сделать |
||
| raise ValueError("Invalid end_ts value") | ||
|
|
||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. нужна кстати проверка что start и end_ts>now иначе я могу сейчас создать модалку которая просрочена |
||
| if note.frequency < 1: | ||
| raise ValueError("Frequency must be greater than 0") | ||
| if not note.group_ids: | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. хочется для групп и сервисов сделать проверку на их существование реальное, надо в бд здесь будет обращаться |
||
| raise ValueError("Group ids must not be empty") | ||
| if not note.service_ids: | ||
| raise ValueError("Service ids must not be empty") | ||
|
|
||
| if isinstance(note, NoteRatingPost): | ||
| if note.rating_max is None or not (2 <= note.rating_max <= 10): | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://github.com/profcomff/modal-service-api/blob/main/modal_backend/settings.py#L11 |
||
| raise ValueError("Rating max must be between 2 and 10") | ||
| elif isinstance(note, NoteChoicePost): | ||
| if note.choice_options is None or len(note.choice_options) < 2: | ||
| raise ValueError("Choice options must contain at least 2 options") | ||
| if len({opt.id for opt in note.choice_options}) != len(note.choice_options): | ||
| raise ValueError("Choice options must have unique ids") | ||
| if not all( | ||
| opt.text for opt in note.choice_options | ||
| ): # я так понял, что текст из пробелов это тоже текст. Поправьте если нет | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. opt.text.strip()
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. пробелы в тексте лишние убрать |
||
| raise ValueError("Choice options must have non-empty text") | ||
| elif isinstance(note, NoteImagePost): | ||
| if not note.images: | ||
| raise ValueError("Images must not be empty") | ||
| elif isinstance(note, NoteTextPost): | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. хочется в порядке следования type_id прописать валидацию каждого объекта |
||
| if note.max_length is None or not (1 <= note.max_length <= 5000): | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. давай для 5000 тоже сделаем как с максимальным рейтингом |
||
| raise ValueError("Max length must be between 1 and 5000") | ||
|
|
||
| @classmethod | ||
| async def get_notes_by_filters( | ||
| cls, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,7 +38,7 @@ def resolve_items(n_list: list | Any, items: list) -> list | Any: | |
| "header": "string", | ||
| "group_ids": [0], # индексы | ||
| "service_ids": [0], # индексы | ||
| "frequency": 0, | ||
| "frequency": 1, | ||
| "start_ts": "2026-08-04T17:21:45.694Z", | ||
| "end_ts": "2026-08-04T17:22:45.694Z", | ||
| "is_always": False, | ||
|
|
@@ -53,11 +53,11 @@ def resolve_items(n_list: list | Any, items: list) -> list | Any: | |
| "header": "string", | ||
| "group_ids": [0], # индексы | ||
| "service_ids": [0], # индексы | ||
| "frequency": 0, | ||
| "frequency": 1, | ||
| "start_ts": "2026-08-04T17:21:45.694Z", | ||
| "end_ts": "2026-08-04T17:22:45.694Z", | ||
| "is_always": True, | ||
| "rating_max": 0, | ||
| "rating_max": 5, | ||
| }, | ||
| NoteRatingGet, | ||
| "/rating", | ||
|
|
@@ -68,7 +68,7 @@ def resolve_items(n_list: list | Any, items: list) -> list | Any: | |
| "header": "string", | ||
| "group_ids": [0], # индексы | ||
| "service_ids": [0], # индексы | ||
| "frequency": 0, | ||
| "frequency": 1, | ||
| "start_ts": "2026-08-04T17:21:45.694Z", | ||
| "end_ts": "2026-08-04T17:22:45.694Z", | ||
| "is_always": False, | ||
|
|
@@ -84,11 +84,11 @@ def resolve_items(n_list: list | Any, items: list) -> list | Any: | |
| "header": "string", | ||
| "group_ids": [0], # индексы | ||
| "service_ids": [0], # индексы | ||
| "frequency": 0, | ||
| "frequency": 1, | ||
| "start_ts": "2026-08-04T17:21:45.694Z", | ||
| "end_ts": "2026-08-04T17:22:45.694Z", | ||
| "is_always": True, | ||
| "choice_options": [{"id": 0, "text": "string"}], | ||
| "choice_options": [{"id": 0, "text": "string"}, {"id": 1, "text": "string"}], | ||
| "is_multiple": True, | ||
| }, | ||
| NoteChoiceGet, | ||
|
|
@@ -100,7 +100,7 @@ def resolve_items(n_list: list | Any, items: list) -> list | Any: | |
| "header": "string", | ||
| "group_ids": [0], # индексы | ||
| "service_ids": [0], # индексы | ||
| "frequency": 0, | ||
| "frequency": 1, | ||
| "start_ts": "2026-08-04T17:21:45.694Z", | ||
| "end_ts": "2026-08-04T17:22:45.694Z", | ||
| "is_always": False, | ||
|
|
@@ -211,11 +211,78 @@ def test_create_all_type_of_note(client, dbsession, groups, services, status_cod | |
| assert response_model.status == note.status | ||
| assert response_model.admin_id == note.admin_id | ||
|
|
||
| if json_body["is_always"]: | ||
| assert note.end_ts is None | ||
|
|
||
| if type_model is NoteTextGet: | ||
| assert len(note.text) <= note.max_length | ||
| dbsession.delete(note) | ||
|
|
||
|
|
||
| VALID_NOTE_BODIES: dict[str, dict] = { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. все тесты не то что нужно, ты делаешь тесты ради тестов
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. если что тесты ниже этого коммента удалить надо, те небольшие правки выше правильные |
||
| "/info": {"info_text": "string"}, | ||
| "/rating": {"rating_max": 5}, | ||
| "/text": {"text": "string", "max_length": 6}, | ||
| "/choice": {"choice_options": [{"id": 1, "text": "a"}, {"id": 2, "text": "b"}], "is_multiple": False}, | ||
| "/image": {"images": ["string"]}, | ||
| } | ||
| COMMON_NOTE_BODY: dict = { | ||
| "header": "string", | ||
| "group_ids": [1], | ||
| "service_ids": [1], | ||
| "frequency": 1, | ||
| "start_ts": "2026-08-04T17:21:45.694Z", | ||
| "end_ts": "2026-08-04T17:22:45.694Z", | ||
| "is_always": False, | ||
| } | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "path, overrides", | ||
| [ | ||
| pytest.param("/info", {"end_ts": None}, id="end_ts_required_if_not_always"), | ||
| pytest.param("/info", {"end_ts": "2026-08-04T17:21:45.694Z"}, id="start_ts_equals_end_ts"), | ||
| pytest.param("/info", {"end_ts": "2026-08-04T17:00:00.000Z"}, id="start_ts_after_end_ts"), | ||
| pytest.param("/info", {"frequency": 0}, id="frequency_zero"), | ||
| pytest.param("/info", {"frequency": -1}, id="frequency_negative"), | ||
| pytest.param("/info", {"group_ids": []}, id="group_ids_empty"), | ||
| pytest.param("/info", {"group_ids": None}, id="group_ids_null"), | ||
| pytest.param("/info", {"service_ids": []}, id="service_ids_empty"), | ||
| pytest.param("/info", {"service_ids": None}, id="service_ids_null"), | ||
| pytest.param("/rating", {"rating_max": 1}, id="rating_max_below_min"), | ||
| pytest.param("/rating", {"rating_max": 11}, id="rating_max_above_max"), | ||
| pytest.param("/rating", {"rating_max": None}, id="rating_max_missing"), | ||
| pytest.param("/text", {"max_length": 0}, id="max_length_below_min"), | ||
| pytest.param("/text", {"max_length": 5001}, id="max_length_above_max"), | ||
| pytest.param("/text", {"max_length": None}, id="max_length_missing"), | ||
| pytest.param("/choice", {"choice_options": [{"id": 1, "text": "a"}]}, id="choice_options_one_option"), | ||
| pytest.param("/choice", {"choice_options": None}, id="choice_options_missing"), | ||
| pytest.param( | ||
| "/choice", {"choice_options": [{"id": 1, "text": "a"}, {"id": 1, "text": "b"}]}, id="choice_options_same_id" | ||
| ), | ||
| pytest.param( | ||
| "/choice", | ||
| {"choice_options": [{"id": 1, "text": "a"}, {"id": 2, "text": ""}]}, | ||
| id="choice_options_empty_text", | ||
| ), | ||
| pytest.param("/image", {"images": []}, id="images_empty"), | ||
| pytest.param("/image", {"images": None}, id="images_missing"), | ||
| ], | ||
| ) | ||
| def test_create_note_validation_error(client, path, overrides): | ||
| """ | ||
| Нарушение правил валидации из ТЗ даёт 422 в формате StatusResponseModel. | ||
| Проверка `status == "Error"` отличает нашу валидацию от стандартной ошибки pydantic (`{"detail": [...]}`). | ||
| Ошибки возникают до обращения к БД, поэтому группы и сервисы в БД не нужны. | ||
| """ | ||
| body = {**COMMON_NOTE_BODY, **VALID_NOTE_BODIES[path], **overrides} | ||
|
|
||
| response = client.post(f"{url}{path}", json=body) | ||
|
|
||
| assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT | ||
| assert response.json()["status"] == "Error" | ||
|
|
||
|
|
||
| def calculate_expected_len( | ||
| all_notes, | ||
| type_id: int | None, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
коммент можно удалить
если для меня пр коммент нужен, то его лучше оставлять прям в гитхабе, как я оставляю текущий коммент тебе