From 2011d2f2c30dc67c1f34e82e5593f8e4ce1243dd Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 04:44:08 +0000 Subject: Move some parts from Mixins file to utils --- pydis_site/apps/api/models/utils.py | 174 ++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 pydis_site/apps/api/models/utils.py (limited to 'pydis_site/apps/api/models/utils.py') diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py new file mode 100644 index 00000000..97a79507 --- /dev/null +++ b/pydis_site/apps/api/models/utils.py @@ -0,0 +1,174 @@ +from collections.abc import Mapping +from operator import itemgetter +from typing import Any, Dict + +from django.core.exceptions import ValidationError +from django.core.validators import MaxLengthValidator, MinLengthValidator + + +def is_bool_validator(value: Any) -> None: + """Validates if a given value is of type bool.""" + if not isinstance(value, bool): + raise ValidationError(f"This field must be of type bool, not {type(value)}.") + + +def validate_embed_fields(fields: dict) -> None: + """Raises a ValidationError if any of the given embed fields is invalid.""" + field_validators = { + 'name': (MaxLengthValidator(limit_value=256),), + 'value': (MaxLengthValidator(limit_value=1024),), + 'inline': (is_bool_validator,), + } + + required_fields = ('name', 'value') + + for field in fields: + if not isinstance(field, Mapping): + raise ValidationError("Embed fields must be a mapping.") + + if not all(required_field in field for required_field in required_fields): + raise ValidationError( + f"Embed fields must contain the following fields: {', '.join(required_fields)}." + ) + + for field_name, value in field.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed field field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_embed_footer(footer: Dict[str, str]) -> None: + """Raises a ValidationError if the given footer is invalid.""" + field_validators = { + 'text': ( + MinLengthValidator( + limit_value=1, + message="Footer text must not be empty." + ), + MaxLengthValidator(limit_value=2048) + ), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(footer, Mapping): + raise ValidationError("Embed footer must be a mapping.") + + for field_name, value in footer.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed footer field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_embed_author(author: Any) -> None: + """Raises a ValidationError if the given author is invalid.""" + field_validators = { + 'name': ( + MinLengthValidator( + limit_value=1, + message="Embed author name must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'url': (), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(author, Mapping): + raise ValidationError("Embed author must be a mapping.") + + for field_name, value in author.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed author field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_embed(embed: Any) -> None: + """ + Validate a JSON document containing an embed as possible to send on Discord. + + This attempts to rebuild the validation used by Discord + as well as possible by checking for various embed limits so we can + ensure that any embed we store here will also be accepted as a + valid embed by the Discord API. + + Using this directly is possible, although not intended - you usually + stick this onto the `validators` keyword argument of model fields. + + Example: + + >>> from django.contrib.postgres import fields as pgfields + >>> from django.db import models + >>> from pydis_site.apps.api.models.utils import validate_embed + >>> class MyMessage(models.Model): + ... embed = pgfields.JSONField( + ... validators=( + ... validate_embed, + ... ) + ... ) + ... # ... + ... + + Args: + embed (Any): + A dictionary describing the contents of this embed. + See the official documentation for a full reference + of accepted keys by this dictionary: + https://discordapp.com/developers/docs/resources/channel#embed-object + + Raises: + ValidationError: + In case the given embed is deemed invalid, a `ValidationError` + is raised which in turn will allow Django to display errors + as appropriate. + """ + all_keys = { + 'title', 'type', 'description', 'url', 'timestamp', + 'color', 'footer', 'image', 'thumbnail', 'video', + 'provider', 'author', 'fields' + } + one_required_of = {'description', 'fields', 'image', 'title', 'video'} + field_validators = { + 'title': ( + MinLengthValidator( + limit_value=1, + message="Embed title must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'description': (MaxLengthValidator(limit_value=2048),), + 'fields': ( + MaxLengthValidator(limit_value=25), + validate_embed_fields + ), + 'footer': (validate_embed_footer,), + 'author': (validate_embed_author,) + } + + if not embed: + raise ValidationError("Tag embed must not be empty.") + + elif not isinstance(embed, Mapping): + raise ValidationError("Tag embed must be a mapping.") + + elif not any(field in embed for field in one_required_of): + raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") + + for required_key in one_required_of: + if required_key in embed and not embed[required_key]: + raise ValidationError(f"Key {required_key!r} must not be empty.") + + for field_name, value in embed.items(): + if field_name not in all_keys: + raise ValidationError(f"Unknown field name: {field_name!r}") + + if field_name in field_validators: + for validator in field_validators[field_name]: + validator(value) -- cgit v1.2.3 From 1ab4ccbbbcf38dddace5855ed9521219821b7698 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 04:48:09 +0000 Subject: Remove unused import from models utils --- pydis_site/apps/api/models/utils.py | 1 - 1 file changed, 1 deletion(-) (limited to 'pydis_site/apps/api/models/utils.py') diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 97a79507..107231ba 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -1,5 +1,4 @@ from collections.abc import Mapping -from operator import itemgetter from typing import Any, Dict from django.core.exceptions import ValidationError -- cgit v1.2.3 From fd2909c936efb9ab2285896b297b405c4dd7a1fb Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:35:01 +0000 Subject: Move last parts from mixins to utils and delete mixins --- pydis_site/apps/api/models/mixins.py | 31 ------------------------------- pydis_site/apps/api/models/utils.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 31 deletions(-) delete mode 100644 pydis_site/apps/api/models/mixins.py (limited to 'pydis_site/apps/api/models/utils.py') diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py deleted file mode 100644 index 5d75b78b..00000000 --- a/pydis_site/apps/api/models/mixins.py +++ /dev/null @@ -1,31 +0,0 @@ -from operator import itemgetter - -from django.db import models - - -class ModelReprMixin: - """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" - - def __repr__(self): - """Returns the current model class name and initialisation parameters.""" - attributes = ' '.join( - f'{attribute}={value!r}' - for attribute, value in sorted( - self.__dict__.items(), - key=itemgetter(0) - ) - if not attribute.startswith('_') - ) - return f'<{self.__class__.__name__}({attributes})>' - - -class ModelTimestampMixin(models.Model): - """Mixin providing created_at and updated_at fields.""" - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - """Metaconfig for the mixin.""" - - abstract = True diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 107231ba..692d14f7 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -1,8 +1,10 @@ from collections.abc import Mapping +from operator import itemgetter from typing import Any, Dict from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator +from django.db import models def is_bool_validator(value: Any) -> None: @@ -171,3 +173,31 @@ def validate_embed(embed: Any) -> None: if field_name in field_validators: for validator in field_validators[field_name]: validator(value) + + +class ModelReprMixin: + """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" + + def __repr__(self): + """Returns the current model class name and initialisation parameters.""" + attributes = ' '.join( + f'{attribute}={value!r}' + for attribute, value in sorted( + self.__dict__.items(), + key=itemgetter(0) + ) + if not attribute.startswith('_') + ) + return f'<{self.__class__.__name__}({attributes})>' + + +class ModelTimestampMixin(models.Model): + """Mixin providing created_at and updated_at fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + """Metaconfig for the mixin.""" + + abstract = True -- cgit v1.2.3 From feafa0950d9132350e36f58fbae57121363d3277 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:47:47 +0000 Subject: Still move mixins back to its own file --- pydis_site/apps/api/models/bot/filter_list.py | 2 +- pydis_site/apps/api/models/mixins.py | 31 +++++++++++++++++++++++++++ pydis_site/apps/api/models/utils.py | 30 -------------------------- 3 files changed, 32 insertions(+), 31 deletions(-) create mode 100644 pydis_site/apps/api/models/mixins.py (limited to 'pydis_site/apps/api/models/utils.py') diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py index cb4acb68..d279e137 100644 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -1,6 +1,6 @@ from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin, ModelTimestampMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py new file mode 100644 index 00000000..5d75b78b --- /dev/null +++ b/pydis_site/apps/api/models/mixins.py @@ -0,0 +1,31 @@ +from operator import itemgetter + +from django.db import models + + +class ModelReprMixin: + """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" + + def __repr__(self): + """Returns the current model class name and initialisation parameters.""" + attributes = ' '.join( + f'{attribute}={value!r}' + for attribute, value in sorted( + self.__dict__.items(), + key=itemgetter(0) + ) + if not attribute.startswith('_') + ) + return f'<{self.__class__.__name__}({attributes})>' + + +class ModelTimestampMixin(models.Model): + """Mixin providing created_at and updated_at fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + """Metaconfig for the mixin.""" + + abstract = True diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 692d14f7..107231ba 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -1,10 +1,8 @@ from collections.abc import Mapping -from operator import itemgetter from typing import Any, Dict from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator -from django.db import models def is_bool_validator(value: Any) -> None: @@ -173,31 +171,3 @@ def validate_embed(embed: Any) -> None: if field_name in field_validators: for validator in field_validators[field_name]: validator(value) - - -class ModelReprMixin: - """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" - - def __repr__(self): - """Returns the current model class name and initialisation parameters.""" - attributes = ' '.join( - f'{attribute}={value!r}' - for attribute, value in sorted( - self.__dict__.items(), - key=itemgetter(0) - ) - if not attribute.startswith('_') - ) - return f'<{self.__class__.__name__}({attributes})>' - - -class ModelTimestampMixin(models.Model): - """Mixin providing created_at and updated_at fields.""" - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - """Metaconfig for the mixin.""" - - abstract = True -- cgit v1.2.3