Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚡🔨✨🚨 feat(settings): Add dynamic validators and upload path for attachment field with validation and tests #55

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions django_announcement/admin/announcement.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ class AnnouncementAdmin(BaseModelAdmin):
(
None,
{
"fields": ("title", "content", "category"),
"fields": ("title", "content", "category", "attachment"),
"description": _(
"Primary fields related to the announcement, including the title, content, and category."
"Primary fields related to the announcement, including the title, content, category and attachment."
),
},
),
Expand Down
8 changes: 5 additions & 3 deletions django_announcement/api/filters/announcement_filter.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from django.apps import apps
from django_filters import BooleanFilter
from django_filters.rest_framework import DateTimeFromToRangeFilter, FilterSet

from django_announcement.models.announcement import Announcement


class AnnouncementFilter(FilterSet):
"""Filter set for filtering announcements based on various criteria."""
Expand Down Expand Up @@ -33,7 +32,7 @@ class AnnouncementFilter(FilterSet):
)

class Meta:
model = Announcement
model = None
fields = {
"audience__id": ["exact"],
"category__id": ["exact"],
Expand All @@ -47,4 +46,7 @@ class Meta:
def filter_not_expired(self, queryset, name, value):
"""Filter announcements that are not expired (i.e., expires_at is None
or in the future)."""
from django_announcement.models.announcement import Announcement

self.Meta.model = Announcement
return queryset.active() if value else queryset
6 changes: 6 additions & 0 deletions django_announcement/constants/default_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
from typing import List, Optional


@dataclass(frozen=True)
class DefaultAttachmentSettings:
validators: Optional[List] = None
upload_path: str = "announcement_attachments/"


@dataclass(frozen=True)
class DefaultCommandSettings:
generate_audiences_exclude_apps: List[str] = field(default_factory=lambda: [])
Expand Down
6 changes: 5 additions & 1 deletion django_announcement/constants/types.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from typing import Iterable, Union
from typing import Any, Iterable, List, Optional, Type, Union

from django_announcement.models.announcement_category import AnnouncementCategory
from django_announcement.models.audience import Audience

# Type Alias for Announcement QuerySet
Audiences = Union[Audience, int, Iterable[Audience]]
Categories = Union[AnnouncementCategory, int, Iterable[AnnouncementCategory]]

# Type Alias for config class
DefaultPath = Optional[Union[str, List[str]]]
OptionalPaths = Optional[Union[Type[Any], List[Type[Any]]]]
4 changes: 3 additions & 1 deletion django_announcement/models/announcement.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from django_announcement.repository.manager.announcement import (
AnnouncementDataAccessLayer,
)
from django_announcement.settings.conf import config


class Announcement(TimeStampedModel):
Expand Down Expand Up @@ -89,7 +90,8 @@ class Announcement(TimeStampedModel):
verbose_name=_("Attachment"),
help_text=_("An optional file attachment for the announcement (e.g., flyer)."),
db_comment="Optional file attachment related to the announcement.",
upload_to="announcement_attachments/",
upload_to=config.attachment_upload_path,
validators=config.attachment_validators or [],
blank=True,
null=True,
)
Expand Down
28 changes: 20 additions & 8 deletions django_announcement/settings/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
from django_announcement.validators.config_validators import (
validate_boolean_setting,
validate_list_fields,
validate_optional_class_setting,
validate_optional_classes_setting,
validate_optional_path_setting,
validate_optional_paths_setting,
validate_throttle_rate,
validate_upload_path_setting,
)


Expand Down Expand Up @@ -101,6 +102,11 @@ def check_announcement_settings(app_configs: Any, **kwargs: Any) -> List[Error]:
config.api_allow_retrieve, f"{config.prefix}API_ALLOW_RETRIEVE"
)
)
errors.extend(
validate_upload_path_setting(
config.attachment_upload_path, f"{config.prefix}ATTACHMENT_UPLOAD_PATH"
)
)
errors.extend(
validate_list_fields(
config.api_ordering_fields, f"{config.prefix}API_ORDERING_FIELDS"
Expand Down Expand Up @@ -138,37 +144,43 @@ def check_announcement_settings(app_configs: Any, **kwargs: Any) -> List[Error]:
)
)
errors.extend(
validate_optional_class_setting(
validate_optional_path_setting(
config.get_setting(f"{config.prefix}API_THROTTLE_CLASS", None),
f"{config.prefix}API_THROTTLE_CLASS",
)
)
errors.extend(
validate_optional_class_setting(
validate_optional_path_setting(
config.get_setting(f"{config.prefix}API_PAGINATION_CLASS", None),
f"{config.prefix}API_PAGINATION_CLASS",
)
)
errors.extend(
validate_optional_classes_setting(
validate_optional_paths_setting(
config.get_setting(f"{config.prefix}API_PARSER_CLASSES", []),
f"{config.prefix}API_PARSER_CLASSES",
)
)
errors.extend(
validate_optional_class_setting(
validate_optional_paths_setting(
config.get_setting(f"{config.prefix}ATTACHMENT_VALIDATORS", []),
f"{config.prefix}ATTACHMENT_VALIDATORS",
)
)
errors.extend(
validate_optional_path_setting(
config.get_setting(f"{config.prefix}API_FILTERSET_CLASS", None),
f"{config.prefix}API_FILTERSET_CLASS",
)
)
errors.extend(
validate_optional_class_setting(
validate_optional_path_setting(
config.get_setting(f"{config.prefix}API_EXTRA_PERMISSION_CLASS", None),
f"{config.prefix}API_EXTRA_PERMISSION_CLASS",
)
)
errors.extend(
validate_optional_class_setting(
validate_optional_path_setting(
config.get_setting(f"{config.prefix}ADMIN_SITE_CLASS", None),
f"{config.prefix}ADMIN_SITE_CLASS",
)
Expand Down
63 changes: 33 additions & 30 deletions django_announcement/settings/conf.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
from typing import Any, List, Optional, Type, Union
from typing import Any, List

from django.conf import settings
from django.utils.module_loading import import_string

from django_announcement.constants.default_settings import (
DefaultAdminSettings,
DefaultAPISettings,
DefaultAttachmentSettings,
DefaultCommandSettings,
DefaultPaginationAndFilteringSettings,
DefaultSerializerSettings,
DefaultThrottleSettings,
)
from django_announcement.constants.types import DefaultPath, OptionalPaths


# pylint: disable=too-many-instance-attributes
Expand Down Expand Up @@ -56,6 +58,7 @@ class AnnouncementConfig:
)
default_throttle_settings: DefaultThrottleSettings = DefaultThrottleSettings()
default_command_settings: DefaultCommandSettings = DefaultCommandSettings()
default_attachment_settings: DefaultAttachmentSettings = DefaultAttachmentSettings()

def __init__(self) -> None:
"""Initialize the AnnouncementConfig, loading values from Django
Expand Down Expand Up @@ -114,6 +117,14 @@ def __init__(self) -> None:
f"{self.prefix}GENERATE_AUDIENCES_EXCLUDE_MODELS",
self.default_command_settings.generate_audiences_exclude_models,
)
self.attachment_upload_path: str = self.get_setting(
f"{self.prefix}ATTACHMENT_UPLOAD_PATH",
self.default_attachment_settings.upload_path,
)
self.attachment_validators: OptionalPaths = self.get_optional_paths(
f"{self.prefix}ATTACHMENT_VALIDATORS",
self.default_attachment_settings.validators,
)
self.authenticated_user_throttle_rate: str = self.get_setting(
f"{self.prefix}AUTHENTICATED_USER_THROTTLE_RATE",
self.default_throttle_settings.authenticated_user_throttle_rate,
Expand All @@ -122,25 +133,23 @@ def __init__(self) -> None:
f"{self.prefix}STAFF_USER_THROTTLE_RATE",
self.default_throttle_settings.staff_user_throttle_rate,
)
self.api_throttle_class: Optional[Type[Any]] = self.get_optional_classes(
self.api_throttle_class: OptionalPaths = self.get_optional_paths(
f"{self.prefix}API_THROTTLE_CLASS",
self.default_throttle_settings.throttle_class,
)
self.api_pagination_class: Optional[Type[Any]] = self.get_optional_classes(
self.api_pagination_class: OptionalPaths = self.get_optional_paths(
f"{self.prefix}API_PAGINATION_CLASS",
self.default_pagination_and_filter_settings.pagination_class,
)
self.api_extra_permission_class: Optional[Type[Any]] = (
self.get_optional_classes(
f"{self.prefix}API_EXTRA_PERMISSION_CLASS",
self.default_api_settings.extra_permission_class,
)
self.api_extra_permission_class: OptionalPaths = self.get_optional_paths(
f"{self.prefix}API_EXTRA_PERMISSION_CLASS",
self.default_api_settings.extra_permission_class,
)
self.api_parser_classes: Optional[List[Type[Any]]] = self.get_optional_classes(
self.api_parser_classes: OptionalPaths = self.get_optional_paths(
f"{self.prefix}API_PARSER_CLASSES",
self.default_api_settings.parser_classes,
)
self.api_filterset_class: Optional[Type[Any]] = self.get_optional_classes(
self.api_filterset_class: OptionalPaths = self.get_optional_paths(
f"{self.prefix}API_FILTERSET_CLASS",
self.default_pagination_and_filter_settings.filterset_class,
)
Expand All @@ -152,7 +161,7 @@ def __init__(self) -> None:
f"{self.prefix}API_SEARCH_FIELDS",
self.default_pagination_and_filter_settings.search_fields,
)
self.admin_site_class: Optional[Type[Any]] = self.get_optional_classes(
self.admin_site_class: OptionalPaths = self.get_optional_paths(
f"{self.prefix}ADMIN_SITE_CLASS",
self.default_admin_settings.admin_site_class,
)
Expand All @@ -170,39 +179,33 @@ def get_setting(self, setting_name: str, default_value: Any) -> Any:
"""
return getattr(settings, setting_name, default_value)

def get_optional_classes(
def get_optional_paths(
self,
setting_name: str,
default_path: Optional[Union[str, List[str]]],
) -> Optional[Union[Type[Any], List[Type[Any]]]]:
"""Dynamically load a class based on a setting, or return None if the
setting is None or invalid.
default_path: DefaultPath,
) -> OptionalPaths:
"""Dynamically load a method or class path on a setting, or return None
if the setting is None or invalid.

Args:
setting_name (str): The name of the setting for the class path.
default_path (Optional[Union[str, List[str]]): The default import path for the class.
setting_name (str): The name of the setting for the method or class path.
default_path (Optional[Union[str, List[str]]): The default import path for the method or class.

Returns:
Optional[Union[Type[Any], List[Type[Any]]]]: The imported class or None
Optional[Union[Type[Any], List[Type[Any]]]]: The imported method or class or None
if import fails or the path is invalid.

"""
class_path: Optional[Union[str, List[str]]] = self.get_setting(
setting_name, default_path
)
_path: DefaultPath = self.get_setting(setting_name, default_path)

if class_path and isinstance(class_path, str):
if _path and isinstance(_path, str):
try:
return import_string(class_path)
return import_string(_path)
except ImportError:
return None
elif class_path and isinstance(class_path, list):
elif _path and isinstance(_path, list):
try:
return [
import_string(cls_path)
for cls_path in class_path
if isinstance(cls_path, str)
]
return [import_string(path) for path in _path if isinstance(path, str)]
except ImportError:
return []

Expand Down
4 changes: 2 additions & 2 deletions django_announcement/tests/admin/test_announcement.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ def test_fieldsets(self, announcement_admin: AnnouncementAdmin) -> None:
(
None,
{
"fields": ("title", "content", "category"),
"fields": ("title", "content", "category", "attachment"),
"description": "Primary fields related to the announcement,"
" including the title, content, and category.",
" including the title, content, category and attachment.",
},
),
(
Expand Down
Loading