diff options
author | 2020-12-22 03:17:51 +0000 | |
---|---|---|
committer | 2020-12-22 03:17:51 +0000 | |
commit | a7dd78eda44473e9d9eb32290590eb66ace0753f (patch) | |
tree | 7636d44ecda9e04f9fbe22d4521101808bd28b31 | |
parent | Merge pull request #42 from python-discord/big-int-fix (diff) | |
parent | Updates Patching Strategy (diff) |
Merge pull request #43 from python-discord/modify-patch-behavior
-rw-r--r-- | SCHEMA.md | 13 | ||||
-rw-r--r-- | backend/constants.py | 4 | ||||
-rw-r--r-- | backend/models/form.py | 10 | ||||
-rw-r--r-- | backend/routes/forms/form.py | 22 | ||||
-rw-r--r-- | backend/routes/forms/index.py | 5 | ||||
-rw-r--r-- | backend/routes/forms/submit.py | 6 | ||||
-rw-r--r-- | poetry.lock | 48 | ||||
-rw-r--r-- | pyproject.toml | 1 |
8 files changed, 61 insertions, 48 deletions
@@ -16,10 +16,10 @@ In this document: | ------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------- | | `id` | Unique identifier | A user selected, unique, descriptive identifier (used in URL routes, so no spaces) | `"ban-appeals"` | | `features` | List of [form features](#form-features) | A list of features to change the behaviour of the form, described in the features section | `["OPEN", "COLLECT_EMAIL"]` | -| `meta` | Mapping of [meta options](#meta-options) | Meta properties for the form. | See meta-options section | | `questions` | List of [form questions](#form-question) | The list of questions to render on a specific form | Too long! See below | | `name` | String | Name of the form | `"Summer Code Jam 2100"` | | `description` | String | Form description | `"This is my amazing form description."` | +| `webhook` | [Webhook object](#webhooks) | An optional discord webhook. | See webhook documentation. | ### Form features @@ -32,10 +32,13 @@ In this document: | `DISABLE_ANTISPAM` | Disable the anti-spam checks from running on a form submission. | | `WEBHOOK_ENABLED` | The form should notify the webhook. Has no effect if no webhook is set. | -### Meta options -| Field | Description | Example | -| --------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| `webhook` | Mapping of webhook url and message. Message can use certain [context variables](#webhook-variables). | `"webhook": {"url": "https://discord.com/api/webhooks/id/key", "message": "{user} submitted a form."}` | +### Webhooks +Discord webhooks to send information upon form submission. + +| Field | Type | Description | +| ----------| ------ | --------------------------------------------------------------------------------------------------------- | +| `url` | String | Discord webhook URL. | +| `message` | String | An optional message to include before the embed. Can use certain [context variables](#webhook-variables). | #### Webhook Variables diff --git a/backend/constants.py b/backend/constants.py index bfcf261..bf0c33c 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -67,7 +67,3 @@ class FormFeatures(Enum): class WebHook(Enum): URL = "url" MESSAGE = "message" - - -class Meta(Enum): - WEB_HOOK = WebHook diff --git a/backend/models/form.py b/backend/models/form.py index d5e2ff5..57372ea 100644 --- a/backend/models/form.py +++ b/backend/models/form.py @@ -4,7 +4,7 @@ import httpx from pydantic import BaseModel, Field, validator from pydantic.error_wrappers import ErrorWrapper, ValidationError -from backend.constants import FormFeatures, Meta, WebHook +from backend.constants import FormFeatures, WebHook from .question import Question PUBLIC_FIELDS = ["id", "features", "questions", "name", "description"] @@ -24,11 +24,6 @@ class _WebHook(BaseModel): return url -class _FormMeta(BaseModel): - """Schema model for form meta data.""" - webhook: _WebHook = None - - class Form(BaseModel): """Schema model for form.""" @@ -37,7 +32,7 @@ class Form(BaseModel): questions: list[Question] name: str description: str - meta: _FormMeta = _FormMeta() + webhook: _WebHook = None class Config: allow_population_by_field_name = True @@ -124,7 +119,6 @@ async def validate_hook_url(url: str) -> t.Optional[ValidationError]: await validate() except Exception as e: loc = ( - Meta.__name__.lower(), WebHook.__name__.lower(), WebHook.URL.value ) diff --git a/backend/routes/forms/form.py b/backend/routes/forms/form.py index b87c7cf..8ecfdf6 100644 --- a/backend/routes/forms/form.py +++ b/backend/routes/forms/form.py @@ -1,15 +1,16 @@ """ Returns, updates or deletes a single form given an ID. """ +import deepmerge from pydantic import ValidationError from spectree.response import Response from starlette.authentication import requires from starlette.requests import Request from starlette.responses import JSONResponse -from backend.route import Route from backend.models import Form -from backend.validation import OkayResponse, api, ErrorMessage +from backend.route import Route +from backend.validation import ErrorMessage, OkayResponse, api class SingleForm(Route): @@ -53,15 +54,22 @@ class SingleForm(Route): """Updates form by ID.""" data = await request.json() - if raw_form := await request.state.db.forms.find_one( - {"_id": request.path_params["form_id"]} - ): + form_id = {"_id": request.path_params["form_id"]} + if raw_form := await request.state.db.forms.find_one(form_id): if "_id" in data or "id" in data: return JSONResponse({"error": "locked_field"}, status_code=400) - raw_form.update(data) + # Build Data Merger + merge_strategy = [ + (dict, ["merge"]) + ] + merger = deepmerge.Merger(merge_strategy, ["override"], ["override"]) + + # Merge Form Data + updated_form = merger.merge(raw_form, data) + try: - form = Form(**raw_form) + form = Form(**updated_form) except ValidationError as e: return JSONResponse(e.errors(), status_code=422) diff --git a/backend/routes/forms/index.py b/backend/routes/forms/index.py index 0e1dee8..5fd90ab 100644 --- a/backend/routes/forms/index.py +++ b/backend/routes/forms/index.py @@ -6,7 +6,7 @@ from starlette.authentication import requires from starlette.requests import Request from starlette.responses import JSONResponse -from backend.constants import Meta, WebHook +from backend.constants import WebHook from backend.models import Form, FormList from backend.models.form import validate_hook_url from backend.route import Route @@ -51,8 +51,7 @@ class FormsList(Route): # Verify Webhook try: # Get url from request - path = (Meta.__name__.lower(), WebHook.__name__.lower(), WebHook.URL.value) - url = form_data[path[0]][path[1]][path[2]] + url = form_data[WebHook.__name__.lower()][WebHook.URL.value] # Validate URL validation = await validate_hook_url(url) diff --git a/backend/routes/forms/submit.py b/backend/routes/forms/submit.py index 82caa81..8588a2d 100644 --- a/backend/routes/forms/submit.py +++ b/backend/routes/forms/submit.py @@ -155,7 +155,7 @@ class SubmitForm(Route): ) -> None: """Helper to send a submission message to a discord webhook.""" # Stop if webhook is not available - if form.meta.webhook is None: + if form.webhook is None: raise ValueError("Got empty webhook.") try: @@ -190,7 +190,7 @@ class SubmitForm(Route): } # Set hook message - message = form.meta.webhook.message + message = form.webhook.message if message: # Available variables, see SCHEMA.md ctx = { @@ -208,5 +208,5 @@ class SubmitForm(Route): # Post hook async with httpx.AsyncClient() as client: - r = await client.post(form.meta.webhook.url, json=hook) + r = await client.post(form.webhook.url, json=hook) r.raise_for_status() diff --git a/poetry.lock b/poetry.lock index 9a4c95d..785fec6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -23,6 +23,14 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] +name = "deepmerge" +version = "0.1.1" +description = "a toolset to deeply merge python dictionaries." +category = "main" +optional = false +python-versions = "*" + +[[package]] name = "flake8" version = "3.8.4" description = "the modular source code checker: pep8 pyflakes and co" @@ -55,10 +63,10 @@ optional = false python-versions = ">=3.4" [package.extras] -tornado = ["tornado (>=0.2)"] +eventlet = ["eventlet (>=0.9.7)"] gevent = ["gevent (>=0.13)"] setproctitle = ["setproctitle"] -eventlet = ["eventlet (>=0.9.7)"] +tornado = ["tornado (>=0.2)"] [[package]] name = "h11" @@ -165,8 +173,8 @@ python-versions = ">=3.6" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] -typing_extensions = ["typing-extensions (>=3.7.2)"] email = ["email-validator (>=1.0.3)"] +typing_extensions = ["typing-extensions (>=3.7.2)"] [[package]] name = "pyflakes" @@ -185,9 +193,9 @@ optional = false python-versions = "*" [package.extras] -test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"] crypto = ["cryptography (>=1.4)"] flake8 = ["flake8", "flake8-import-order", "pep8-naming"] +test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"] [[package]] name = "pymongo" @@ -198,14 +206,14 @@ optional = false python-versions = "*" [package.extras] -tls = ["ipaddress"] -encryption = ["pymongocrypt (<2.0.0)"] aws = ["pymongo-auth-aws (<2.0.0)"] +encryption = ["pymongocrypt (<2.0.0)"] gssapi = ["pykerberos"] +ocsp = ["pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] snappy = ["python-snappy"] srv = ["dnspython (>=1.16.0,<1.17.0)"] +tls = ["ipaddress"] zstd = ["zstandard"] -ocsp = ["pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] [[package]] name = "python-dotenv" @@ -260,9 +268,9 @@ python-versions = ">=3.6" pydantic = ">=1.2" [package.extras] -flask = ["flask"] -falcon = ["falcon"] dev = ["pytest (>=6)", "flake8 (>=3.8)", "black (>=20.8b1)", "isort (>=5.6)", "autoflake (>=1.4)"] +falcon = ["falcon"] +flask = ["flask"] starlette = ["starlette"] [[package]] @@ -278,21 +286,21 @@ full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "p [[package]] name = "uvicorn" -version = "0.13.1" +version = "0.13.2" description = "The lightning-fast ASGI server." category = "main" optional = false python-versions = "*" [package.dependencies] -PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} -uvloop = {version = ">=0.14.0", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +click = ">=7.0.0,<8.0.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} h11 = ">=0.8" -watchgod = {version = ">=0.6,<0.7", optional = true, markers = "extra == \"standard\""} httptools = {version = ">=0.1.0,<0.2.0", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} -colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -click = ">=7.0.0,<8.0.0" +PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.14.0", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchgod = {version = ">=0.6,<0.7", optional = true, markers = "extra == \"standard\""} websockets = {version = ">=8.0.0,<9.0.0", optional = true, markers = "extra == \"standard\""} [package.extras] @@ -325,7 +333,7 @@ python-versions = ">=3.6.1" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "ba96cffc23bdf274acd85b4a4134fa76d7dfa06d135184282f28929957fea82e" +content-hash = "7584e4eacc1b2615adce06899fe6e6b8d696955fea97533ccac2bd2c642e136f" [metadata.files] certifi = [ @@ -340,6 +348,10 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +deepmerge = [ + {file = "deepmerge-0.1.1-py2.py3-none-any.whl", hash = "sha256:190e133a6657303db37f9bb302aa853d8d2b15a0e055d41b99a362598e79206a"}, + {file = "deepmerge-0.1.1.tar.gz", hash = "sha256:fa1d44269786bcc12d30a7471b0b39478aa37a43703b134d7f12649792f92c1f"}, +] flake8 = [ {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, @@ -531,8 +543,8 @@ starlette = [ {file = "starlette-0.14.1.tar.gz", hash = "sha256:5268ef5d4904ec69582d5fd207b869a5aa0cd59529848ba4cf429b06e3ced99a"}, ] uvicorn = [ - {file = "uvicorn-0.13.1-py3-none-any.whl", hash = "sha256:6fcce74c00b77d4f4b3ed7ba1b2a370d27133bfdb46f835b7a76dfe0a8c110ae"}, - {file = "uvicorn-0.13.1.tar.gz", hash = "sha256:2a7b17f4d9848d6557ccc2274a5f7c97f1daf037d130a0c6918f67cd9bc8cdf5"}, + {file = "uvicorn-0.13.2-py3-none-any.whl", hash = "sha256:6707fa7f4dbd86fd6982a2d4ecdaad2704e4514d23a1e4278104311288b04691"}, + {file = "uvicorn-0.13.2.tar.gz", hash = "sha256:d19ca083bebd212843e01f689900e5c637a292c63bb336c7f0735a99300a5f38"}, ] uvloop = [ {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, diff --git a/pyproject.toml b/pyproject.toml index 49e0f43..642fb6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ httpx = "^0.16.1" gunicorn = "^20.0.4" pydantic = "^1.7.2" spectree = "^0.3.16" +deepmerge = "^0.1.1" [tool.poetry.dev-dependencies] flake8 = "^3.8.4" |