Skip to content

Commit

Permalink
Merge pull request #55 from MEHRSHAD-MIRSHEKARY/feat/dynamic-attachme…
Browse files Browse the repository at this point in the history
…nt-config

⚡ 🔨 ✨ 🚨  feat(settings): Add dynamic validators and upload path for attachment field with validation and tests
  • Loading branch information
ARYAN-NIKNEZHAD authored Oct 25, 2024
2 parents 66140d4 + 8dc5db2 commit 89f8653
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 100 deletions.
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

0 comments on commit 89f8653

Please sign in to comment.