From f4f201f2829a914b967e393631ed2b7940ac679b Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 30 Nov 2020 18:58:20 +0200 Subject: Create models project module --- backend/models/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/models/__init__.py (limited to 'backend') diff --git a/backend/models/__init__.py b/backend/models/__init__.py new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3 From 4a1f8efc162b33079cbf84c0e6ead89b1f737796 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 30 Nov 2020 18:59:45 +0200 Subject: Add ObjectId type for MongoDB Pydantic models --- backend/models/_object_id.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 backend/models/_object_id.py (limited to 'backend') diff --git a/backend/models/_object_id.py b/backend/models/_object_id.py new file mode 100644 index 0000000..f0e47cf --- /dev/null +++ b/backend/models/_object_id.py @@ -0,0 +1,23 @@ +import typing as t +from bson import ObjectId as OriginalObjectId + + +class ObjectId(OriginalObjectId): + """ObjectId implementation for Pydantic.""" + + @classmethod + def __get_validators__(cls) -> t.Generator[t.Callable, None, None]: + """Get validators for Pydantic.""" + yield cls.validate + + @classmethod + def validate(cls, value: t.Any) -> t.Optional["ObjectId"]: + """Checks value validity to become ObjectId and if valid, return ObjectId.""" + if OriginalObjectId.is_valid(value): + raise ValueError(f"Invalid value '{value}' for ObjectId.") + return ObjectId(value) + + @classmethod + def __modify_schema__(cls, field_schema: t.Dict[str, t.Any]) -> None: + """Update data type to string.""" + field_schema.update(type="string") -- cgit v1.2.3 From f73e8c4001e09c3b9d03655155b073c591447592 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 30 Nov 2020 19:12:15 +0200 Subject: Move ObjectId to special types directory --- backend/models/_object_id.py | 23 ----------------------- backend/models/types/__init__.py | 3 +++ backend/models/types/object_id.py | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+), 23 deletions(-) delete mode 100644 backend/models/_object_id.py create mode 100644 backend/models/types/__init__.py create mode 100644 backend/models/types/object_id.py (limited to 'backend') diff --git a/backend/models/_object_id.py b/backend/models/_object_id.py deleted file mode 100644 index f0e47cf..0000000 --- a/backend/models/_object_id.py +++ /dev/null @@ -1,23 +0,0 @@ -import typing as t -from bson import ObjectId as OriginalObjectId - - -class ObjectId(OriginalObjectId): - """ObjectId implementation for Pydantic.""" - - @classmethod - def __get_validators__(cls) -> t.Generator[t.Callable, None, None]: - """Get validators for Pydantic.""" - yield cls.validate - - @classmethod - def validate(cls, value: t.Any) -> t.Optional["ObjectId"]: - """Checks value validity to become ObjectId and if valid, return ObjectId.""" - if OriginalObjectId.is_valid(value): - raise ValueError(f"Invalid value '{value}' for ObjectId.") - return ObjectId(value) - - @classmethod - def __modify_schema__(cls, field_schema: t.Dict[str, t.Any]) -> None: - """Update data type to string.""" - field_schema.update(type="string") diff --git a/backend/models/types/__init__.py b/backend/models/types/__init__.py new file mode 100644 index 0000000..ae408d7 --- /dev/null +++ b/backend/models/types/__init__.py @@ -0,0 +1,3 @@ +from .object_id import ObjectId + +__all__ = ["ObjectId"] diff --git a/backend/models/types/object_id.py b/backend/models/types/object_id.py new file mode 100644 index 0000000..f0e47cf --- /dev/null +++ b/backend/models/types/object_id.py @@ -0,0 +1,23 @@ +import typing as t +from bson import ObjectId as OriginalObjectId + + +class ObjectId(OriginalObjectId): + """ObjectId implementation for Pydantic.""" + + @classmethod + def __get_validators__(cls) -> t.Generator[t.Callable, None, None]: + """Get validators for Pydantic.""" + yield cls.validate + + @classmethod + def validate(cls, value: t.Any) -> t.Optional["ObjectId"]: + """Checks value validity to become ObjectId and if valid, return ObjectId.""" + if OriginalObjectId.is_valid(value): + raise ValueError(f"Invalid value '{value}' for ObjectId.") + return ObjectId(value) + + @classmethod + def __modify_schema__(cls, field_schema: t.Dict[str, t.Any]) -> None: + """Update data type to string.""" + field_schema.update(type="string") -- cgit v1.2.3 From d10cf30456669cc841d79221f2f74f8385530dfd Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 30 Nov 2020 19:26:17 +0200 Subject: Add allowed question types constant --- backend/constants.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'backend') diff --git a/backend/constants.py b/backend/constants.py index 746e277..c7db8e6 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -15,3 +15,14 @@ OAUTH2_REDIRECT_URI = os.getenv( ) SECRET_KEY = os.getenv("SECRET_KEY", binascii.hexlify(os.urandom(30)).decode()) + +QUESTION_TYPES = [ + "radio", + "checkbox", + "select", + "short_text", + "textarea", + "code", + "range", + "section", +] -- cgit v1.2.3 From 11d6b2e9fdbc97d6dabe3cc6f52859acced48e45 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 30 Nov 2020 19:44:39 +0200 Subject: Add question types data requirements to constants --- backend/constants.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'backend') diff --git a/backend/constants.py b/backend/constants.py index c7db8e6..1816354 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -26,3 +26,21 @@ QUESTION_TYPES = [ "range", "section", ] + +REQUIRED_QUESTION_TYPE_DATA = { + "radio": { + "options": list, + }, + "select": { + "options": list, + }, + "code": { + "language": str, + }, + "range": { + "options": list, + }, + "section": { + "text": str, + }, +} -- cgit v1.2.3 From 1628c3a019866caae54d89058bc812ec91be2bc1 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 30 Nov 2020 20:01:26 +0200 Subject: Create model for form question --- backend/models/question.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 backend/models/question.py (limited to 'backend') diff --git a/backend/models/question.py b/backend/models/question.py new file mode 100644 index 0000000..ac585f9 --- /dev/null +++ b/backend/models/question.py @@ -0,0 +1,55 @@ +import typing as t + +from pydantic import BaseModel, Field, validator + +from backend.constants import QUESTION_TYPES, REQUIRED_QUESTION_TYPE_DATA +from backend.models.types import ObjectId + + +class Question(BaseModel): + """Schema model for form question.""" + + id: ObjectId = Field(alias="_id") + name: str + type: str + data: t.Dict[str, t.Any] + + @validator("type", pre=True) + def validate_question_type(self, value: str) -> t.Optional[str]: + """Checks if question type in currently allowed types list.""" + value = value.lower() + if value not in QUESTION_TYPES: + raise ValueError( + f"{value} is not valid question type. " + f"Allowed question types: {QUESTION_TYPES}." + ) + + return value + + @validator("data") + def validate_question_data( + self, + value: t.Dict[str, t.Any] + ) -> t.Optional[t.Dict[str, t.Any]]: + """Check does required data exists for question type and remove other data.""" + # When question type don't need data, don't add anything to keep DB clean. + if self.type not in REQUIRED_QUESTION_TYPE_DATA: + return {} + + # Required keys (and values) will be stored to here + # to remove all unnecessary stuff + result = {} + + for key, data_type in REQUIRED_QUESTION_TYPE_DATA[self.type].items(): + if key not in value: + raise ValueError(f"Required question data key '{key}' not provided.") + + if not isinstance(value[key], data_type): + raise ValueError( + f"Question data key '{key}' expects {data_type.__name__}, " + f"got {type(value[key]).__name__} instead." + ) + + result[key] = value[key] + + return result -- cgit v1.2.3 From 0ec52e5b8349674bccd449d46a8ea2ce5b011f99 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 30 Nov 2020 20:15:03 +0200 Subject: Add Enum for form features --- backend/constants.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'backend') diff --git a/backend/constants.py b/backend/constants.py index 1816354..3b8bec8 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -3,6 +3,8 @@ load_dotenv() import os # noqa import binascii # noqa +from enum import Enum # noqa + DATABASE_URL = os.getenv("DATABASE_URL") MONGO_DATABASE = os.getenv("MONGO_DATABASE", "pydis_forms") @@ -44,3 +46,13 @@ REQUIRED_QUESTION_TYPE_DATA = { "text": str, }, } + + +class FormFeatures(Enum): + """Lists form features. Read more in SCHEMA.md.""" + + DISCOVERABLE = "DISCOVERABLE" + REQUIRES_LOGIN = "REQUIRES_LOGIN" + OPEN = "OPEN" + COLLECT_EMAIL = "COLLECT_EMAIL" + DISABLE_ANTISPAM = "DISABLE_ANTISPAM" -- cgit v1.2.3 From 53b3bebeca248e115f2d0e73e913aa34d224c2d3 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 30 Nov 2020 20:26:01 +0200 Subject: Create form object model --- backend/models/form.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 backend/models/form.py (limited to 'backend') diff --git a/backend/models/form.py b/backend/models/form.py new file mode 100644 index 0000000..6e1a9ab --- /dev/null +++ b/backend/models/form.py @@ -0,0 +1,25 @@ +import typing as t + +from pydantic import BaseModel, Field, validator + +from backend.constants import FormFeatures +from backend.models import Question +from backend.models.types import ObjectId + + +class Form(BaseModel): + """Schema model for form.""" + + id: ObjectId = Field(alias="_id") + features: t.List[str] + questions: t.List[Question] + + @validator("features") + def validate_features(self, value: t.List[str]) -> t.Optional[t.List[str]]: + """Validates is all features in allowed list.""" + # Uppercase everything to avoid mixed case in DB + value = [v.upper() for v in value] + if not all(v in FormFeatures.__members__.values() for v in value): + raise ValueError("Form features list contains one or more invalid values.") + + return value -- cgit v1.2.3 From a3c6268ddc8a42c3718157ce7cdbce548629dd7f Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 30 Nov 2020 20:26:15 +0200 Subject: Add question and form models to __init__.py --- backend/models/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'backend') diff --git a/backend/models/__init__.py b/backend/models/__init__.py index e69de29..80abf6f 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -0,0 +1,4 @@ +from .form import Form +from .question import Question + +__all__ = ["Form", "Question"] -- cgit v1.2.3 From 11a9f41a54a245114b3b253ee1892549ec4b7f8f Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 1 Dec 2020 22:33:43 +0200 Subject: Use plain string as type for question id --- backend/models/question.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'backend') diff --git a/backend/models/question.py b/backend/models/question.py index ac585f9..2324a47 100644 --- a/backend/models/question.py +++ b/backend/models/question.py @@ -3,13 +3,12 @@ import typing as t from pydantic import BaseModel, Field, validator from backend.constants import QUESTION_TYPES, REQUIRED_QUESTION_TYPE_DATA -from backend.models.types import ObjectId class Question(BaseModel): """Schema model for form question.""" - id: ObjectId = Field(alias="_id") + id: str = Field(alias="_id") name: str type: str data: t.Dict[str, t.Any] -- cgit v1.2.3 From d42708483e2a00a50c659b0333f2a201463a1805 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 1 Dec 2020 22:34:37 +0200 Subject: Use plain string for form model id --- backend/models/form.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'backend') diff --git a/backend/models/form.py b/backend/models/form.py index 6e1a9ab..3c42067 100644 --- a/backend/models/form.py +++ b/backend/models/form.py @@ -4,13 +4,12 @@ from pydantic import BaseModel, Field, validator from backend.constants import FormFeatures from backend.models import Question -from backend.models.types import ObjectId class Form(BaseModel): """Schema model for form.""" - id: ObjectId = Field(alias="_id") + id: str = Field(alias="_id") features: t.List[str] questions: t.List[Question] -- cgit v1.2.3 From 9e9ce9f6ec601098b84d75d022336b16280c7f1a Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 1 Dec 2020 22:34:57 +0200 Subject: Delete __init__.py --- backend/models/types/__init__.py | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 backend/models/types/__init__.py (limited to 'backend') diff --git a/backend/models/types/__init__.py b/backend/models/types/__init__.py deleted file mode 100644 index ae408d7..0000000 --- a/backend/models/types/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .object_id import ObjectId - -__all__ = ["ObjectId"] -- cgit v1.2.3 From ac1661fcba10cb603a6527523632c8cce24523f8 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 1 Dec 2020 22:35:11 +0200 Subject: Delete object_id.py --- backend/models/types/object_id.py | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 backend/models/types/object_id.py (limited to 'backend') diff --git a/backend/models/types/object_id.py b/backend/models/types/object_id.py deleted file mode 100644 index f0e47cf..0000000 --- a/backend/models/types/object_id.py +++ /dev/null @@ -1,23 +0,0 @@ -import typing as t -from bson import ObjectId as OriginalObjectId - - -class ObjectId(OriginalObjectId): - """ObjectId implementation for Pydantic.""" - - @classmethod - def __get_validators__(cls) -> t.Generator[t.Callable, None, None]: - """Get validators for Pydantic.""" - yield cls.validate - - @classmethod - def validate(cls, value: t.Any) -> t.Optional["ObjectId"]: - """Checks value validity to become ObjectId and if valid, return ObjectId.""" - if OriginalObjectId.is_valid(value): - raise ValueError(f"Invalid value '{value}' for ObjectId.") - return ObjectId(value) - - @classmethod - def __modify_schema__(cls, field_schema: t.Dict[str, t.Any]) -> None: - """Update data type to string.""" - field_schema.update(type="string") -- cgit v1.2.3 From 0627b7cf2ca1b00af005591fb719151e4163f64d Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 1 Dec 2020 22:44:24 +0200 Subject: Add validating emails collecting login --- backend/models/form.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'backend') diff --git a/backend/models/form.py b/backend/models/form.py index 3c42067..de9f69f 100644 --- a/backend/models/form.py +++ b/backend/models/form.py @@ -21,4 +21,10 @@ class Form(BaseModel): if not all(v in FormFeatures.__members__.values() for v in value): raise ValueError("Form features list contains one or more invalid values.") + if ( + FormFeatures.COLLECT_EMAIL in value + and FormFeatures.REQUIRES_LOGIN not in value + ): + raise ValueError("COLLECT_EMAIL feature require REQUIRES_LOGIN feature.") + return value -- cgit v1.2.3 From 857dcf54eb2ff9695b6926354706743bc7c41b15 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 1 Dec 2020 22:46:07 +0200 Subject: Fix linting --- backend/models/form.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'backend') diff --git a/backend/models/form.py b/backend/models/form.py index de9f69f..e490268 100644 --- a/backend/models/form.py +++ b/backend/models/form.py @@ -22,8 +22,8 @@ class Form(BaseModel): raise ValueError("Form features list contains one or more invalid values.") if ( - FormFeatures.COLLECT_EMAIL in value - and FormFeatures.REQUIRES_LOGIN not in value + FormFeatures.COLLECT_EMAIL in value and + FormFeatures.REQUIRES_LOGIN not in value ): raise ValueError("COLLECT_EMAIL feature require REQUIRES_LOGIN feature.") -- cgit v1.2.3 From e98dd185776532583cd5730b548caf8129a0c076 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 1 Dec 2020 22:54:21 +0200 Subject: Ignore too long line for if statement --- backend/models/form.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'backend') diff --git a/backend/models/form.py b/backend/models/form.py index e490268..d0f0a3c 100644 --- a/backend/models/form.py +++ b/backend/models/form.py @@ -21,10 +21,7 @@ class Form(BaseModel): if not all(v in FormFeatures.__members__.values() for v in value): raise ValueError("Form features list contains one or more invalid values.") - if ( - FormFeatures.COLLECT_EMAIL in value and - FormFeatures.REQUIRES_LOGIN not in value - ): + if FormFeatures.COLLECT_EMAIL in value and FormFeatures.REQUIRES_LOGIN not in value: # noqa raise ValueError("COLLECT_EMAIL feature require REQUIRES_LOGIN feature.") return value -- cgit v1.2.3