From 06005f6412e969bd3bd973f730e04859465111c4 Mon Sep 17 00:00:00 2001 From: Agostina Date: Wed, 17 Nov 2021 16:26:01 +0100 Subject: [PATCH] Adding a new admin mixin which will allow us to have a new action available on MasterModel's pages on Django Admin Site --- README.md | 22 +++++++++++++++ dj_cqrs/admin.py | 52 ++++++++++++++++++++++++++++++++++ docs/admin.rst | 24 ++++++++++++++++ docs/index.rst | 1 + docs/reference.rst | 7 +++++ tests/test_admin.py | 68 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 174 insertions(+) create mode 100644 dj_cqrs/admin.py create mode 100644 docs/admin.rst create mode 100644 tests/test_admin.py diff --git a/README.md b/README.md index 8ba3254..89ddf94 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,28 @@ class FilteredSimplestModel(MasterMixin, models.Model): return len(str(self.name)) > 2 ``` +Django Admin +----------- + +Add action to synchronize master items from Django Admin page. + +```python +from django.db import models +from django.contrib import admin + +from dj_cqrs.admin_mixins import CQRSAdminMasterSyncMixin + + +class AccountAdmin(CQRSAdminMasterSyncMixin, admin.ModelAdmin): + ... + + +admin.site.register(models.Account, AccountAdmin) + +``` + +* If necessary, override ```_cqrs_sync_queryset``` from ```CQRSAdminMasterSyncMixin``` to adjust the QuerySet and use it for synchronization. + Utilities --------- diff --git a/dj_cqrs/admin.py b/dj_cqrs/admin.py new file mode 100644 index 0000000..6842b35 --- /dev/null +++ b/dj_cqrs/admin.py @@ -0,0 +1,52 @@ +# Copyright © 2021 Ingram Micro Inc. All rights reserved. + +from django.utils.translation import gettext_lazy + + +class CQRSAdminMasterSyncMixin: + """ + Mixin that includes a custom action in AdminModel. This action allows synchronizing + master's model items from Django Admin page, + """ + + def get_actions(self, request): + """ + Overriding method from AdminModel class; it is used to include the sync method in + the actions list. + """ + if self.actions is not None: + self.actions = self.actions + ['sync_items'] + return super().get_actions(request) + + def _cqrs_sync_queryset(self, queryset): + """ + This function is used to adjust the QuerySet before sending the sync signal. + + :param queryset: Original queryset + :type queryset: Queryset + :return: Updated queryset + :rtype: Queryset + """ + return queryset + + def sync_items(self, request, queryset): + """ + This method synchronizes selected items from the Admin Page. + It is registered as a custom action in Django Admin + """ + items_not_synced = [] + for item in self._cqrs_sync_queryset(queryset): + if not item.cqrs_sync(): + items_not_synced.append(item) + + total = len(queryset) + total_w_erros = len(items_not_synced) + total_sucess = total - total_w_erros + self.message_user( + request, + f'{total_sucess} successfully synced. {total_w_erros} failed: {items_not_synced}', + ) + + sync_items.short_description = gettext_lazy( + 'Synchronize selected %(verbose_name_plural)s via CQRS', + ) diff --git a/docs/admin.rst b/docs/admin.rst new file mode 100644 index 0000000..6007696 --- /dev/null +++ b/docs/admin.rst @@ -0,0 +1,24 @@ +Django Admin +==================== + +Synchronize items +----------------- + +Add action to synchronize master items from Django Admin page. + +.. code-block:: python + + from django.db import models + from django.contrib import admin + + from dj_cqrs.admin_mixins import CQRSAdminMasterSyncMixin + + + class AccountAdmin(CQRSAdminMasterSyncMixin, admin.ModelAdmin): + pass + + + admin.site.register(models.Account, AccountAdmin) + + +* If necessary, override ``_cqrs_sync_queryset`` from ``CQRSAdminMasterSyncMixin`` to adjust the QuerySet and use it for synchronization. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index cb5c927..cee7e6c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,6 +32,7 @@ and that's okay for non-critical business transactions. :caption: Contents: getting_started + admin custom_serialization track_fields_changes transports diff --git a/docs/reference.rst b/docs/reference.rst index a78eb57..c0748dc 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -2,6 +2,13 @@ API Reference ============= +Django Admin +------------ + +.. autoclass:: dj_cqrs.admin.CQRSAdminMasterSyncMixin + :members: sync_items, _cqrs_sync_queryset + + Mixins ------ diff --git a/tests/test_admin.py b/tests/test_admin.py new file mode 100644 index 0000000..3274c4c --- /dev/null +++ b/tests/test_admin.py @@ -0,0 +1,68 @@ +# Copyright © 2021 Ingram Micro Inc. All rights reserved. + +from dj_cqrs.admin import CQRSAdminMasterSyncMixin + +from django.contrib import admin + +import pytest + + +class MockAdminModel(CQRSAdminMasterSyncMixin, admin.ModelAdmin): + pass + + +@pytest.mark.parametrize( + ('total_sync', 'total_failed'), + ( + (2, 1), + (3, 0), + (0, 3), + (0, 0), + ), +) +def test_sync_items_function(total_sync, total_failed, mocker): + model_path = 'dj_cqrs.mixins.MasterMixin' + mock_model = mocker.patch(model_path) + request = None + + qs = [] + for _ in range(total_sync): + m = mocker.patch(model_path) + m.cqrs_sync.return_value = True + qs.append(m) + + failed_items = [] + for _ in range(total_failed): + m = mocker.patch(model_path) + m.cqrs_sync.return_value = False + qs.append(m) + failed_items.append(m) + + mixin = MockAdminModel(model=mock_model, admin_site=admin.sites.AdminSite()) + mixin.message_user = mocker.Mock() + mixin.sync_items(request, qs) + + mixin.message_user.assert_called_once_with( + request, + f'{total_sync} successfully synced. {total_failed} failed: {failed_items}', + ) + + +def test_admin_actions_enabled_with_sync_items_action(mocker): + mock_model = mocker.Mock() + request = mocker.patch('django.http.HttpRequest') + mixin = MockAdminModel(model=mock_model, admin_site=admin.sites.AdminSite()) + actions = mixin.get_actions(request) + + assert 'sync_items' in actions + assert 'delete_selected' in actions + + +def test_actions_not_enabled(mocker): + mock_model = mocker.Mock() + request = mocker.patch('django.http.HttpRequest') + mixin = MockAdminModel(model=mock_model, admin_site=admin.sites.AdminSite()) + mixin.actions = None + actions = mixin.get_actions(request) + + assert actions == {}