From 47c219fd1fcd17879bbd39956d0a936de69b8eae Mon Sep 17 00:00:00 2001 From: grey-cat-1908 <61203964+grey-cat-1908@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:21:23 +0000 Subject: [PATCH] answer model && some form updates --- models/__init__.py | 1 + models/answer.py | 102 +++++++++++++++++++++++++++++++++++++++++++++ models/form.py | 22 ++++++++++ routes/form.py | 19 ++++++++- 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 models/answer.py diff --git a/models/__init__.py b/models/__init__.py index f2dfbfd..d46c677 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -9,3 +9,4 @@ class BaseModel(pydantic.BaseModel): from .settings import settings from .user import * from .form import * +from .answer import * diff --git a/models/answer.py b/models/answer.py new file mode 100644 index 0000000..3c74fb7 --- /dev/null +++ b/models/answer.py @@ -0,0 +1,102 @@ +from enum import Enum +from uuid import UUID +from typing import TypeAlias + +from pydantic import field_validator + +from models import BaseModel, form + + +class AnswerError(Enum): + TOO_SHORT = "The text value is shorter than the minimum allowed length." + TOO_LONG = "The text value is longer than the maximum allowed length." + TOO_FEW_SELECTED = "The number of selected items is less than the minimum required." + TOO_MANY_SELECTED = "The number of selected items is more than the maximum allowed." + DUPLICATE_QUESTIONS = "Each value must correspond to a different question." + INCORRECT_IDS = "The ids for some questions are incorrect." + + +class BaseValue(BaseModel): + question_id: UUID + question_type: form.QuestionType + + +class TextValue(BaseValue): + question_type: form.QuestionType = form.QuestionType.text + value: str + + def validate(self, question: form.TextQuestion) -> None: + if question.min_length and len(self.value) < question.min_length: + raise ValueError(AnswerError.TOO_SHORT) + if question.max_length and len(self.value) > question.max_length: + raise ValueError(AnswerError.TOO_LONG) + + +class SelectorValue(BaseValue): + question_type: form.QuestionType = form.QuestionType.selector + values: set[int] + + def validate(self, question: form.SelectorQuestion) -> None: + min_values = ( + max(question.min_values, 1) + if question.min_values + else 1 + ) + max_values = ( + min(question.max_values, question.options) + if question.max_values + else len(question.options) + ) + + if len(self.values) < min_values: + raise ValueError(AnswerError.TOO_FEW_SELECTED) + if len(self.values) > max_values: + raise ValueError(AnswerError.TOO_MANY_SELECTED) + + +Value: TypeAlias = SelectorValue | TextValue + + +class AnswerData(BaseValue): + values: list[Value] + + @property + def question_uuids(self) -> dict[UUID, Value]: + return {value.question_id: value for value in self.values} + + @field_validator("values") + @classmethod + def validate_values(cls, v, info): + uuids = set() + for value in v: + uuids.add(value.question_id) + + if len(v) != len(uuids): + raise ValueError( + AnswerError.DUPLICATE_QUESTIONS + ) + +class Answer(BaseModel): + id: int + form: form.Form + data: AnswerData + + @field_validator("data") + @classmethod + def answer_validator( + cls, + v, + info + ): + uuids = v.question_uuids + questions = info.data["form"].data + for question in questions: + if question.required and question.id not in uuids: + raise ValueError(AnswerError.REQUIRED_QUIESTION_NOT_ANSWERED) + if question.question_type != uuids[question.id].question_type: + raise ValueError(AnswerError.REQUIRED_QUIESTION_NOT_ANSWERED) + del uuids[question.id] + + if len(uuids) > 0: + raise ValueError("Some questions are not known") + return v diff --git a/models/form.py b/models/form.py index ab3d705..3e78275 100644 --- a/models/form.py +++ b/models/form.py @@ -1,5 +1,6 @@ from enum import IntEnum, auto from typing import TypeAlias +from uuid import UUID, uuid4 from pydantic import Field, field_validator @@ -12,6 +13,7 @@ class QuestionType(IntEnum): class BaseQuestion(BaseModel): + id: UUID = Field(default_factory=uuid4) question_type: QuestionType label: str = Field(min_length=1) description: str | None = Field(None, min_length=1) @@ -80,6 +82,26 @@ class FormData(BaseModel): description: str | None = Field(None, min_length=1) questions: list[Question] = [] + @field_validator("questions") + @classmethod + def validate_questions( + cls, + v, + info + ): + questions = info.data.get("questions") + + uuids = set() + for question in questions: + uuids.add(question.question_id) + + if len(questions) != len(uuids): + raise ValueError( + "All questions must have different id's" + ) + + return v + class Form(BaseModel): id: int diff --git a/routes/form.py b/routes/form.py index 8c374c9..e46d97f 100644 --- a/routes/form.py +++ b/routes/form.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter +from fastapi import APIRouter, HTTPException from sqlalchemy import select import database @@ -26,6 +26,23 @@ async def create_form(user: User, form_data: FormData) -> CreateForm: return CreateForm(form_id=form.id) +@router.delete("/delete") +async def delete_form(user: User, id: int): + async with database.sessions.begin() as session: + stmt = select(database.Form).where( + database.Form.id == id + ) + db_request = await session.execute(stmt) + form = db_request.scalar_one_or_none() + + if form is None: + raise HTTPException(404, "No form was found") + if form.owner_id != user.id: + raise HTTPException(403, "Forbidden") + + await session.delete(form) + + @router.get("/my") async def user_forms(user: User): async with database.sessions.begin() as session: