aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Joe Banks <[email protected]>2020-12-22 03:17:51 +0000
committerGravatar GitHub <[email protected]>2020-12-22 03:17:51 +0000
commita7dd78eda44473e9d9eb32290590eb66ace0753f (patch)
tree7636d44ecda9e04f9fbe22d4521101808bd28b31
parentMerge pull request #42 from python-discord/big-int-fix (diff)
parentUpdates Patching Strategy (diff)
Merge pull request #43 from python-discord/modify-patch-behavior
-rw-r--r--SCHEMA.md13
-rw-r--r--backend/constants.py4
-rw-r--r--backend/models/form.py10
-rw-r--r--backend/routes/forms/form.py22
-rw-r--r--backend/routes/forms/index.py5
-rw-r--r--backend/routes/forms/submit.py6
-rw-r--r--poetry.lock48
-rw-r--r--pyproject.toml1
8 files changed, 61 insertions, 48 deletions
diff --git a/SCHEMA.md b/SCHEMA.md
index fa5f247..c3edf35 100644
--- a/SCHEMA.md
+++ b/SCHEMA.md
@@ -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"