diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 40ba2c1..5e5aec2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -83,8 +83,8 @@ repos: files: ^(docs/(.*/)*.*\.rst) additional_dependencies: [ Sphinx==6.2.1 ] -# - repo: local -# hooks: + - repo: local + hooks: # - id: pytest # name: Pytest # entry: poetry run pytest -v @@ -94,15 +94,15 @@ repos: # pass_filenames: false # always_run: true -# - id: pylint -# name: pylint -# entry: pylint -# language: system -# types: [ python ] -# require_serial: true -# args: -# - "-rn" -# - "-sn" -# - "--rcfile=pyproject.toml" -# files: ^django_notification/ -# exclude: (migrations/|tests/|docs/).* + - id: pylint + name: pylint + entry: pylint + language: system + types: [ python ] + require_serial: true + args: + - "-rn" + - "-sn" + - "--rcfile=pyproject.toml" + files: ^django_notification/ + exclude: (migrations/|tests/|docs/).* diff --git a/django_notification/api/serializers/group.py b/django_notification/api/serializers/group.py index be14918..3afd11e 100644 --- a/django_notification/api/serializers/group.py +++ b/django_notification/api/serializers/group.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import Group, Permission from rest_framework import serializers +from django_notification.settings.conf import config from django_notification.utils.serialization.field_filters import ( filter_non_empty_fields, ) @@ -50,7 +51,7 @@ def get_permissions(self, obj: Group) -> List[Dict]: def to_representation(self, instance: Group) -> Dict: """Customize the representation of the Group instance by filtering out - non-empty fields. + non-empty fields, if the exclude_serializer_null_fields flag is True. Args: instance (Group): The group instance being serialized. @@ -60,4 +61,8 @@ def to_representation(self, instance: Group) -> Dict: """ data = super().to_representation(instance) - return filter_non_empty_fields(data) + + if config.exclude_serializer_null_fields: + return filter_non_empty_fields(data) + + return data diff --git a/django_notification/api/serializers/notification.py b/django_notification/api/serializers/notification.py index e18a86f..9f6c4b3 100644 --- a/django_notification/api/serializers/notification.py +++ b/django_notification/api/serializers/notification.py @@ -7,6 +7,7 @@ user_serializer_class, ) from django_notification.models.notification import Notification +from django_notification.settings.conf import config from django_notification.utils.serialization.field_filters import ( filter_non_empty_fields, ) @@ -83,4 +84,8 @@ def to_representation(self, instance: Notification) -> Dict: """ data = super().to_representation(instance) - return filter_non_empty_fields(data) + + if config.exclude_serializer_null_fields: + return filter_non_empty_fields(data) + + return data diff --git a/django_notification/api/serializers/simple_notification.py b/django_notification/api/serializers/simple_notification.py index 1d6ae8a..e131888 100644 --- a/django_notification/api/serializers/simple_notification.py +++ b/django_notification/api/serializers/simple_notification.py @@ -3,6 +3,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField from django_notification.models.notification import Notification +from django_notification.settings.conf import config from django_notification.utils.serialization import ( filter_non_empty_fields, generate_title, @@ -69,4 +70,8 @@ def to_representation(self, instance: Dict[str, Any]) -> Dict[str, str]: """ data = super().to_representation(instance) - return filter_non_empty_fields(data) + + if config.exclude_serializer_null_fields: + return filter_non_empty_fields(data) + + return data diff --git a/django_notification/constants/default_settings.py b/django_notification/constants/default_settings.py index 4a95c9b..1e32010 100644 --- a/django_notification/constants/default_settings.py +++ b/django_notification/constants/default_settings.py @@ -14,7 +14,8 @@ class DefaultSerializerSettings: @dataclass(frozen=True) -class DefaultAdminPermSettings: +class DefaultAdminSettings: + admin_site_class: Optional[str] = None admin_has_add_permission: bool = False admin_has_change_permission: bool = False admin_has_delete_permission: bool = False @@ -30,7 +31,7 @@ class DefaultThrottleSettings: @dataclass(frozen=True) class DefaultPaginationAndFilteringSettings: pagination_class: str = "django_notification.api.paginations.limit_offset_pagination.DefaultLimitOffSetPagination" - filterset_class: str = None + filterset_class: Optional[str] = None ordering_fields: List[str] = field( default_factory=lambda: ["id", "timestamp", "public"] ) @@ -44,6 +45,7 @@ class DefaultAPISettings: allow_list: bool = True allow_retrieve: bool = True include_serializer_full_details: bool = False + exclude_serializer_none_fields: bool = False extra_permission_class: Optional[str] = None parser_classes: List[str] = field( default_factory=lambda: [ diff --git a/django_notification/settings/checks.py b/django_notification/settings/checks.py index 911918a..e1b9c41 100644 --- a/django_notification/settings/checks.py +++ b/django_notification/settings/checks.py @@ -38,119 +38,131 @@ def check_notification_settings(app_configs: Any, **kwargs: Any) -> List[Error]: # Validate boolean settings errors.extend( validate_boolean_setting( - config.include_soft_delete, "DJANGO_NOTIFICATION_API_INCLUDE_SOFT_DELETE" + config.include_soft_delete, f"{config.prefix}API_INCLUDE_SOFT_DELETE" ) ) errors.extend( validate_boolean_setting( - config.include_hard_delete, "DJANGO_NOTIFICATION_API_INCLUDE_HARD_DELETE" + config.include_hard_delete, f"{config.prefix}API_INCLUDE_HARD_DELETE" ) ) errors.extend( validate_boolean_setting( config.admin_has_add_permission, - "DJANGO_NOTIFICATION_ADMIN_HAS_ADD_PERMISSION", + f"{config.prefix}ADMIN_HAS_ADD_PERMISSION", ) ) errors.extend( validate_boolean_setting( config.admin_has_change_permission, - "DJANGO_NOTIFICATION_ADMIN_HAS_CHANGE_PERMISSION", + f"{config.prefix}ADMIN_HAS_CHANGE_PERMISSION", ) ) errors.extend( validate_boolean_setting( config.admin_has_delete_permission, - "DJANGO_NOTIFICATION_ADMIN_HAS_DELETE_PERMISSION", + f"{config.prefix}ADMIN_HAS_DELETE_PERMISSION", ) ) errors.extend( validate_boolean_setting( config.include_serializer_full_details, - "DJANGO_NOTIFICATION_SERIALIZER_INCLUDE_FULL_DETAILS", + f"{config.prefix}SERIALIZER_INCLUDE_FULL_DETAILS", ) ) errors.extend( validate_boolean_setting( - config.api_allow_list, "DJANGO_NOTIFICATION_API_ALLOW_LIST" + config.exclude_serializer_null_fields, + f"{config.prefix}SERIALIZER_EXCLUDE_NULL_FIELDS", ) ) errors.extend( validate_boolean_setting( - config.api_allow_retrieve, "DJANGO_NOTIFICATION_API_ALLOW_RETRIEVE" + config.api_allow_list, f"{config.prefix}API_ALLOW_LIST" + ) + ) + errors.extend( + validate_boolean_setting( + config.api_allow_retrieve, f"{config.prefix}API_ALLOW_RETRIEVE" ) ) errors.extend( validate_list_fields( - config.user_serializer_fields, "DJANGO_NOTIFICATION_USER_SERIALIZER_FIELDS" + config.user_serializer_fields, f"{config.prefix}USER_SERIALIZER_FIELDS" ) ) errors.extend( validate_list_fields( - config.api_ordering_fields, "DJANGO_NOTIFICATION_API_ORDERING_FIELDS" + config.api_ordering_fields, f"{config.prefix}API_ORDERING_FIELDS" ) ) errors.extend( validate_list_fields( - config.api_search_fields, "DJANGO_NOTIFICATION_API_SEARCH_FIELDS" + config.api_search_fields, f"{config.prefix}API_SEARCH_FIELDS" ) ) errors.extend( validate_throttle_rate( config.staff_user_throttle_rate, - "DJANGO_NOTIFICATION_STAFF_USER_THROTTLE_RATE", + f"{config.prefix}STAFF_USER_THROTTLE_RATE", ) ) errors.extend( validate_throttle_rate( config.authenticated_user_throttle_rate, - "DJANGO_NOTIFICATION_AUTHENTICATED_USER_THROTTLE_RATE", + f"{config.prefix}AUTHENTICATED_USER_THROTTLE_RATE", ) ) errors.extend( validate_optional_class_setting( - config.get_setting("DJANGO_NOTIFICATION_USER_SERIALIZER_CLASS", None), - "DJANGO_NOTIFICATION_USER_SERIALIZER_CLASS", + config.get_setting(f"{config.prefix}USER_SERIALIZER_CLASS", None), + f"{config.prefix}USER_SERIALIZER_CLASS", ) ) errors.extend( validate_optional_class_setting( - config.get_setting("DJANGO_NOTIFICATION_GROUP_SERIALIZER_CLASS", None), - "DJANGO_NOTIFICATION_GROUP_SERIALIZER_CLASS", + config.get_setting(f"{config.prefix}GROUP_SERIALIZER_CLASS", None), + f"{config.prefix}GROUP_SERIALIZER_CLASS", ) ) errors.extend( validate_optional_class_setting( - config.get_setting("DJANGO_NOTIFICATION_API_THROTTLE_CLASS", None), - "DJANGO_NOTIFICATION_API_THROTTLE_CLASS", + config.get_setting(f"{config.prefix}API_THROTTLE_CLASS", None), + f"{config.prefix}API_THROTTLE_CLASS", ) ) errors.extend( validate_optional_class_setting( - config.get_setting("DJANGO_NOTIFICATION_API_PAGINATION_CLASS", None), - "DJANGO_NOTIFICATION_API_PAGINATION_CLASS", + config.get_setting(f"{config.prefix}API_PAGINATION_CLASS", None), + f"{config.prefix}API_PAGINATION_CLASS", ) ) errors.extend( validate_optional_classes_setting( - config.get_setting("DJANGO_NOTIFICATION_API_PARSER_CLASSES", []), - "DJANGO_NOTIFICATION_API_PARSER_CLASSES", + config.get_setting(f"{config.prefix}API_PARSER_CLASSES", []), + f"{config.prefix}API_PARSER_CLASSES", + ) + ) + errors.extend( + validate_optional_class_setting( + config.get_setting(f"{config.prefix}API_FILTERSET_CLASS", None), + f"{config.prefix}API_FILTERSET_CLASS", ) ) errors.extend( validate_optional_class_setting( - config.get_setting("DJANGO_NOTIFICATION_API_FILTERSET_CLASS", None), - "DJANGO_NOTIFICATION_API_FILTERSET_CLASS", + config.get_setting(f"{config.prefix}API_EXTRA_PERMISSION_CLASS", None), + f"{config.prefix}API_EXTRA_PERMISSION_CLASS", ) ) errors.extend( validate_optional_class_setting( - config.get_setting("DJANGO_NOTIFICATION_API_EXTRA_PERMISSION_CLASS", None), - "DJANGO_NOTIFICATION_API_EXTRA_PERMISSION_CLASS", + config.get_setting(f"{config.prefix}ADMIN_SITE_CLASS", None), + f"{config.prefix}ADMIN_SITE_CLASS", ) ) diff --git a/django_notification/settings/conf.py b/django_notification/settings/conf.py index 67a87c5..2d2b88e 100644 --- a/django_notification/settings/conf.py +++ b/django_notification/settings/conf.py @@ -76,6 +76,10 @@ def __init__(self) -> None: f"{self.prefix}SERIALIZER_INCLUDE_FULL_DETAILS", self.default_api_settings.include_serializer_full_details, ) + self.exclude_serializer_null_fields: bool = self.get_setting( + f"{self.prefix}SERIALIZER_EXCLUDE_NULL_FIELDS", + self.default_api_settings.exclude_serializer_none_fields, + ) self.api_allow_list: bool = self.get_setting( f"{self.prefix}API_ALLOW_LIST", self.default_api_settings.allow_list diff --git a/django_notification/tests/api/serializers/test_group.py b/django_notification/tests/api/serializers/test_group.py index f4071e1..9a9353e 100644 --- a/django_notification/tests/api/serializers/test_group.py +++ b/django_notification/tests/api/serializers/test_group.py @@ -1,9 +1,11 @@ import sys +from unittest.mock import patch import pytest from django.contrib.auth.models import Group from rest_framework.exceptions import ValidationError from django_notification.api.serializers.group import GroupSerializer +from django_notification.settings.conf import config from django_notification.utils.serialization.field_filters import ( filter_non_empty_fields, ) @@ -23,6 +25,7 @@ class TestGroupSerializer: Test the GroupSerializer and PermissionSerializer functionality. """ + @patch.object(config, "exclude_serializer_null_fields", False) def test_group_serializer_with_valid_data(self, group_with_perm: Group) -> None: """ Test that the GroupSerializer correctly serializes a group with permissions. diff --git a/django_notification/tests/api/serializers/test_simple_notification_serializer.py b/django_notification/tests/api/serializers/test_simple_notification_serializer.py index 1331d10..7196967 100644 --- a/django_notification/tests/api/serializers/test_simple_notification_serializer.py +++ b/django_notification/tests/api/serializers/test_simple_notification_serializer.py @@ -1,11 +1,13 @@ import sys from typing import Dict, Any +from unittest.mock import patch import pytest from django_notification.api.serializers.simple_notification import ( SimpleNotificationSerializer, ) +from django_notification.settings.conf import config from django_notification.utils.serialization.field_filters import ( filter_non_empty_fields, ) @@ -52,6 +54,7 @@ def test_serializer_fields(self, notification_dict: Dict[str, Any]) -> None: serializer = SimpleNotificationSerializer(notification_dict) assert set(serializer.data.keys()) == set(expected_fields) + @patch.object(config, "exclude_serializer_null_fields", False) @mark.django_db def test_title_generation(self, notification_dict: Dict[str, Any]) -> None: """ diff --git a/django_notification/tests/api/views/test_notification.py b/django_notification/tests/api/views/test_notification.py index 4b4de07..4f80780 100644 --- a/django_notification/tests/api/views/test_notification.py +++ b/django_notification/tests/api/views/test_notification.py @@ -36,6 +36,7 @@ def setup_method(self) -> None: """ self.client = APIClient() + @patch.object(config, "exclude_serializer_null_fields", False) def test_get_queryset_for_staff( self, admin_user: Type[User], notification: Notification ) -> None: diff --git a/django_notification/tests/constants.py b/django_notification/tests/constants.py index c2cde2a..766fa3f 100644 --- a/django_notification/tests/constants.py +++ b/django_notification/tests/constants.py @@ -1,2 +1,2 @@ -PYTHON_VERSION = (3, 8) -PYTHON_VERSION_REASON = "Requires Python 3.8 or higher" +PYTHON_VERSION = (3, 9) +PYTHON_VERSION_REASON = "Requires Python 3.9 or higher" diff --git a/django_notification/tests/settings/test_checks.py b/django_notification/tests/settings/test_checks.py index da4a56c..de26707 100644 --- a/django_notification/tests/settings/test_checks.py +++ b/django_notification/tests/settings/test_checks.py @@ -33,6 +33,7 @@ def test_valid_settings(self, mock_config: MagicMock) -> None: mock_config.admin_has_change_permission = False mock_config.admin_has_delete_permission = False mock_config.include_serializer_full_details = True + mock_config.exclude_serializer_null_fields = True mock_config.api_allow_list = True mock_config.api_allow_retrieve = False mock_config.user_serializer_fields = ["id", "username"] @@ -66,7 +67,8 @@ def test_invalid_boolean_settings(self, mock_config: MagicMock) -> None: mock_config.admin_has_add_permission = "not_boolean" mock_config.admin_has_change_permission = "not_boolean" mock_config.admin_has_delete_permission = "not_boolean" - mock_config.include_serializer_full_details = True + mock_config.include_serializer_full_details = "not_bool" + mock_config.exclude_serializer_null_fields = "not_boolean" mock_config.user_serializer_fields = ["id", "username"] mock_config.api_ordering_fields = ["created_at"] mock_config.api_search_fields = ["title"] @@ -78,31 +80,39 @@ def test_invalid_boolean_settings(self, mock_config: MagicMock) -> None: errors = check_notification_settings(None) - # Expect 6 errors for invalid boolean values - assert len(errors) == 6 + # Expect 8 errors for invalid boolean values + assert len(errors) == 8 assert ( errors[0].id - == "django_notification.E001_DJANGO_NOTIFICATION_API_INCLUDE_SOFT_DELETE" + == f"django_notification.E001_{mock_config.prefix}API_INCLUDE_SOFT_DELETE" ) assert ( errors[1].id - == "django_notification.E001_DJANGO_NOTIFICATION_API_INCLUDE_HARD_DELETE" + == f"django_notification.E001_{mock_config.prefix}API_INCLUDE_HARD_DELETE" ) assert ( errors[2].id - == "django_notification.E001_DJANGO_NOTIFICATION_ADMIN_HAS_ADD_PERMISSION" + == f"django_notification.E001_{mock_config.prefix}ADMIN_HAS_ADD_PERMISSION" ) assert ( errors[3].id - == "django_notification.E001_DJANGO_NOTIFICATION_ADMIN_HAS_CHANGE_PERMISSION" + == f"django_notification.E001_{mock_config.prefix}ADMIN_HAS_CHANGE_PERMISSION" ) assert ( errors[4].id - == "django_notification.E001_DJANGO_NOTIFICATION_ADMIN_HAS_DELETE_PERMISSION" + == f"django_notification.E001_{mock_config.prefix}ADMIN_HAS_DELETE_PERMISSION" ) assert ( - errors[5].id - == "django_notification.E001_DJANGO_NOTIFICATION_API_ALLOW_LIST" + errors[5].id + == f"django_notification.E001_{mock_config.prefix}SERIALIZER_INCLUDE_FULL_DETAILS" + ) + assert ( + errors[6].id + == f"django_notification.E001_{mock_config.prefix}SERIALIZER_EXCLUDE_NULL_FIELDS" + ) + assert ( + errors[7].id + == f"django_notification.E001_{mock_config.prefix}API_ALLOW_LIST" ) @patch("django_notification.settings.checks.config") @@ -125,6 +135,7 @@ def test_invalid_list_settings(self, mock_config: MagicMock) -> None: mock_config.admin_has_change_permission = False mock_config.admin_has_delete_permission = False mock_config.include_serializer_full_details = True + mock_config.exclude_serializer_null_fields = True mock_config.api_allow_list = True mock_config.api_allow_retrieve = False mock_config.user_serializer_fields = [] @@ -140,15 +151,15 @@ def test_invalid_list_settings(self, mock_config: MagicMock) -> None: assert len(errors) == 3 assert ( errors[0].id - == "django_notification.E003_DJANGO_NOTIFICATION_USER_SERIALIZER_FIELDS" + == f"django_notification.E003_{mock_config.prefix}USER_SERIALIZER_FIELDS" ) assert ( errors[1].id - == "django_notification.E003_DJANGO_NOTIFICATION_API_ORDERING_FIELDS" + == f"django_notification.E003_{mock_config.prefix}API_ORDERING_FIELDS" ) assert ( errors[2].id - == "django_notification.E004_DJANGO_NOTIFICATION_API_SEARCH_FIELDS" + == f"django_notification.E004_{mock_config.prefix}API_SEARCH_FIELDS" ) @patch("django_notification.settings.checks.config") @@ -171,6 +182,7 @@ def test_invalid_throttle_rate(self, mock_config: MagicMock) -> None: mock_config.admin_has_change_permission = False mock_config.admin_has_delete_permission = False mock_config.include_serializer_full_details = True + mock_config.exclude_serializer_null_fields = True mock_config.api_allow_list = True mock_config.api_allow_retrieve = False mock_config.user_serializer_fields = ["id", "username"] @@ -207,6 +219,7 @@ def test_invalid_class_import(self, mock_config: MagicMock) -> None: mock_config.admin_has_change_permission = False mock_config.admin_has_delete_permission = False mock_config.include_serializer_full_details = True + mock_config.exclude_serializer_null_fields = True mock_config.api_allow_list = True mock_config.api_allow_retrieve = False mock_config.user_serializer_fields = ["id", "username"] @@ -220,33 +233,37 @@ def test_invalid_class_import(self, mock_config: MagicMock) -> None: errors = check_notification_settings(None) - # Expect 7 errors for invalid class imports - assert len(errors) == 7 + # Expect 8 errors for invalid class imports + assert len(errors) == 8 assert ( errors[0].id - == "django_notification.E010_DJANGO_NOTIFICATION_USER_SERIALIZER_CLASS" + == f"django_notification.E010_{mock_config.prefix}USER_SERIALIZER_CLASS" ) assert ( errors[1].id - == "django_notification.E010_DJANGO_NOTIFICATION_GROUP_SERIALIZER_CLASS" + == f"django_notification.E010_{mock_config.prefix}GROUP_SERIALIZER_CLASS" ) assert ( errors[2].id - == "django_notification.E010_DJANGO_NOTIFICATION_API_THROTTLE_CLASS" + == f"django_notification.E010_{mock_config.prefix}API_THROTTLE_CLASS" ) assert ( errors[3].id - == "django_notification.E010_DJANGO_NOTIFICATION_API_PAGINATION_CLASS" + == f"django_notification.E010_{mock_config.prefix}API_PAGINATION_CLASS" ) assert ( errors[4].id - == "django_notification.E011_DJANGO_NOTIFICATION_API_PARSER_CLASSES" + == f"django_notification.E011_{mock_config.prefix}API_PARSER_CLASSES" ) assert ( errors[5].id - == "django_notification.E010_DJANGO_NOTIFICATION_API_FILTERSET_CLASS" + == f"django_notification.E010_{mock_config.prefix}API_FILTERSET_CLASS" ) assert ( errors[6].id - == "django_notification.E010_DJANGO_NOTIFICATION_API_EXTRA_PERMISSION_CLASS" + == f"django_notification.E010_{mock_config.prefix}API_EXTRA_PERMISSION_CLASS" + ) + assert ( + errors[7].id + == f"django_notification.E010_{mock_config.prefix}ADMIN_SITE_CLASS" ) diff --git a/django_notification/tests/setup.py b/django_notification/tests/setup.py index 4fa12ff..c39d29f 100644 --- a/django_notification/tests/setup.py +++ b/django_notification/tests/setup.py @@ -93,6 +93,7 @@ def configure_django_settings() -> None: DJANGO_NOTIFICATION_ADMIN_HAS_ADD_PERMISSION=False, DJANGO_NOTIFICATION_ADMIN_HAS_CHANGE_PERMISSION=False, DJANGO_NOTIFICATION_ADMIN_HAS_DELETE_PERMISSION=False, + DJANGO_NOTIFICATION_SERIALIZER_EXCLUDE_NULL_FIELDS=True, DJANGO_NOTIFICATION_API_ALLOW_LIST=True, DJANGO_NOTIFICATION_API_ALLOW_RETRIEVE=True, DJANGO_NOTIFICATION_AUTHENTICATED_USER_THROTTLE_RATE="20/minute",