django-cfg 1.3.7__py3-none-any.whl → 1.3.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/admin/__init__.py +24 -8
- django_cfg/apps/accounts/admin/activity_admin.py +146 -0
- django_cfg/apps/accounts/admin/filters.py +98 -22
- django_cfg/apps/accounts/admin/group_admin.py +86 -0
- django_cfg/apps/accounts/admin/inlines.py +42 -13
- django_cfg/apps/accounts/admin/otp_admin.py +115 -0
- django_cfg/apps/accounts/admin/registration_admin.py +173 -0
- django_cfg/apps/accounts/admin/resources.py +123 -19
- django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
- django_cfg/apps/accounts/admin/user_admin.py +362 -0
- django_cfg/apps/agents/admin/__init__.py +17 -4
- django_cfg/apps/agents/admin/execution_admin.py +204 -183
- django_cfg/apps/agents/admin/registry_admin.py +230 -255
- django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
- django_cfg/apps/agents/core/__init__.py +1 -1
- django_cfg/apps/agents/core/django_agent.py +221 -0
- django_cfg/apps/agents/core/exceptions.py +14 -0
- django_cfg/apps/agents/core/orchestrator.py +18 -3
- django_cfg/apps/knowbase/admin/__init__.py +1 -1
- django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
- django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
- django_cfg/apps/knowbase/admin/document_admin.py +269 -262
- django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
- django_cfg/apps/knowbase/config/settings.py +21 -4
- django_cfg/apps/knowbase/views/chat_views.py +3 -0
- django_cfg/apps/leads/admin/__init__.py +3 -1
- django_cfg/apps/leads/admin/leads_admin.py +235 -35
- django_cfg/apps/maintenance/admin/__init__.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
- django_cfg/apps/maintenance/admin/log_admin.py +143 -61
- django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
- django_cfg/apps/maintenance/admin/site_admin.py +213 -352
- django_cfg/apps/newsletter/admin/__init__.py +29 -2
- django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
- django_cfg/apps/payments/admin/__init__.py +18 -27
- django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
- django_cfg/apps/payments/admin/balance_admin.py +166 -632
- django_cfg/apps/payments/admin/currencies_admin.py +235 -607
- django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
- django_cfg/apps/payments/admin/filters.py +83 -3
- django_cfg/apps/payments/admin/networks_admin.py +269 -0
- django_cfg/apps/payments/admin/payments_admin.py +183 -460
- django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
- django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +153 -34
- django_cfg/apps/payments/admin_interface/templates/payments/components/payment_card.html +121 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/payment_qr_code.html +95 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/progress_bar.html +37 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_stats.html +60 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_badge.html +41 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_overview.html +83 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_detail.html +363 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +43 -17
- django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
- django_cfg/apps/payments/admin_interface/views/api/payments.py +102 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +109 -63
- django_cfg/apps/payments/admin_interface/views/forms.py +5 -1
- django_cfg/apps/payments/config/__init__.py +14 -15
- django_cfg/apps/payments/config/django_cfg_integration.py +59 -1
- django_cfg/apps/payments/config/helpers.py +8 -13
- django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
- django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
- django_cfg/apps/payments/middleware/api_access.py +32 -6
- django_cfg/apps/payments/migrations/0001_initial.py +33 -46
- django_cfg/apps/payments/migrations/0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more.py +46 -0
- django_cfg/apps/payments/migrations/0003_universalpayment_status_changed_at.py +25 -0
- django_cfg/apps/payments/models/balance.py +12 -0
- django_cfg/apps/payments/models/currencies.py +106 -32
- django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +142 -25
- django_cfg/apps/payments/models/payments.py +94 -0
- django_cfg/apps/payments/services/core/base.py +4 -4
- django_cfg/apps/payments/services/core/currency_service.py +35 -28
- django_cfg/apps/payments/services/core/payment_service.py +266 -39
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/base.py +303 -41
- django_cfg/apps/payments/services/providers/models/__init__.py +42 -0
- django_cfg/apps/payments/services/providers/models/base.py +145 -0
- django_cfg/apps/payments/services/providers/models/providers.py +87 -0
- django_cfg/apps/payments/services/providers/models/universal.py +48 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
- django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +557 -0
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
- django_cfg/apps/payments/services/providers/registry.py +9 -37
- django_cfg/apps/payments/services/providers/sync_service.py +277 -0
- django_cfg/apps/payments/services/types/requests.py +19 -7
- django_cfg/apps/payments/signals/payment_signals.py +31 -2
- django_cfg/apps/payments/static/payments/js/api-client.js +29 -6
- django_cfg/apps/payments/static/payments/js/payment-detail.js +167 -0
- django_cfg/apps/payments/static/payments/js/payment-form.js +98 -32
- django_cfg/apps/payments/tasks/__init__.py +39 -0
- django_cfg/apps/payments/tasks/types.py +73 -0
- django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
- django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
- django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
- django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
- django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
- django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
- django_cfg/apps/payments/templatetags/payment_tags.py +8 -0
- django_cfg/apps/payments/urls.py +3 -2
- django_cfg/apps/payments/urls_admin.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +8 -5
- django_cfg/apps/payments/views/overview/services.py +2 -2
- django_cfg/apps/payments/views/serializers/currencies.py +22 -8
- django_cfg/apps/support/admin/__init__.py +10 -1
- django_cfg/apps/support/admin/support_admin.py +338 -141
- django_cfg/apps/tasks/admin/__init__.py +11 -0
- django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
- django_cfg/apps/tasks/static/tasks/css/dashboard.css +68 -217
- django_cfg/apps/tasks/static/tasks/js/api.js +40 -84
- django_cfg/apps/tasks/static/tasks/js/components/DataManager.js +24 -0
- django_cfg/apps/tasks/static/tasks/js/components/TabManager.js +85 -0
- django_cfg/apps/tasks/static/tasks/js/components/TaskRenderer.js +216 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/main.mjs +245 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/overview.mjs +123 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/queues.mjs +120 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/tasks.mjs +350 -0
- django_cfg/apps/tasks/static/tasks/js/dashboard/workers.mjs +169 -0
- django_cfg/apps/tasks/tasks/__init__.py +10 -0
- django_cfg/apps/tasks/tasks/demo_tasks.py +133 -0
- django_cfg/apps/tasks/templates/tasks/components/management_actions.html +42 -45
- django_cfg/apps/tasks/templates/tasks/components/{status_cards.html → overview_content.html} +30 -11
- django_cfg/apps/tasks/templates/tasks/components/queues_content.html +19 -0
- django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +16 -10
- django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +51 -0
- django_cfg/apps/tasks/templates/tasks/components/workers_content.html +30 -0
- django_cfg/apps/tasks/templates/tasks/layout/base.html +117 -0
- django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +82 -0
- django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +40 -0
- django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +37 -0
- django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +41 -0
- django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +50 -0
- django_cfg/apps/tasks/urls.py +2 -2
- django_cfg/apps/tasks/urls_admin.py +2 -2
- django_cfg/apps/tasks/utils/__init__.py +1 -0
- django_cfg/apps/tasks/utils/simulator.py +356 -0
- django_cfg/apps/tasks/views/__init__.py +16 -0
- django_cfg/apps/tasks/views/api.py +569 -0
- django_cfg/apps/tasks/views/dashboard.py +58 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +10 -5
- django_cfg/core/generation.py +1 -1
- django_cfg/core/integration/__init__.py +21 -0
- django_cfg/management/commands/__init__.py +13 -1
- django_cfg/management/commands/migrate_all.py +9 -3
- django_cfg/management/commands/migrator.py +11 -6
- django_cfg/management/commands/rundramatiq.py +3 -2
- django_cfg/management/commands/rundramatiq_simulator.py +430 -0
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/models/api_keys.py +115 -0
- django_cfg/models/constance.py +0 -11
- django_cfg/models/payments.py +137 -3
- django_cfg/modules/django_admin/__init__.py +64 -0
- django_cfg/modules/django_admin/decorators/__init__.py +13 -0
- django_cfg/modules/django_admin/decorators/actions.py +106 -0
- django_cfg/modules/django_admin/decorators/display.py +106 -0
- django_cfg/modules/django_admin/mixins/__init__.py +14 -0
- django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
- django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
- django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
- django_cfg/modules/django_admin/models/__init__.py +20 -0
- django_cfg/modules/django_admin/models/action_models.py +33 -0
- django_cfg/modules/django_admin/models/badge_models.py +20 -0
- django_cfg/modules/django_admin/models/base.py +26 -0
- django_cfg/modules/django_admin/models/display_models.py +31 -0
- django_cfg/modules/django_admin/utils/badges.py +159 -0
- django_cfg/modules/django_admin/utils/displays.py +247 -0
- django_cfg/modules/django_currency/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
- django_cfg/modules/django_currency/core/converter.py +12 -12
- django_cfg/modules/django_currency/database/__init__.py +2 -2
- django_cfg/modules/django_currency/database/database_loader.py +93 -42
- django_cfg/modules/django_llm/llm/client.py +10 -2
- django_cfg/modules/django_tasks.py +54 -21
- django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
- django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
- django_cfg/modules/django_unfold/dashboard.py +14 -13
- django_cfg/modules/django_unfold/models/config.py +1 -1
- django_cfg/registry/core.py +7 -9
- django_cfg/registry/third_party.py +2 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/METADATA +2 -1
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/RECORD +198 -160
- django_cfg/apps/accounts/admin/activity.py +0 -96
- django_cfg/apps/accounts/admin/group.py +0 -17
- django_cfg/apps/accounts/admin/otp.py +0 -59
- django_cfg/apps/accounts/admin/registration_source.py +0 -97
- django_cfg/apps/accounts/admin/twilio_response.py +0 -227
- django_cfg/apps/accounts/admin/user.py +0 -300
- django_cfg/apps/agents/core/agent.py +0 -281
- django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
- django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
- django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
- django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
- django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
- django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
- django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
- django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
- django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
- django_cfg/apps/payments/config/constance/__init__.py +0 -22
- django_cfg/apps/payments/config/constance/config_service.py +0 -123
- django_cfg/apps/payments/config/constance/fields.py +0 -69
- django_cfg/apps/payments/config/constance/settings.py +0 -160
- django_cfg/apps/payments/services/providers/nowpayments.py +0 -478
- django_cfg/apps/tasks/admin.py +0 -320
- django_cfg/apps/tasks/static/tasks/js/dashboard.js +0 -614
- django_cfg/apps/tasks/static/tasks/js/modals.js +0 -452
- django_cfg/apps/tasks/static/tasks/js/notifications.js +0 -144
- django_cfg/apps/tasks/static/tasks/js/task-monitor.js +0 -454
- django_cfg/apps/tasks/static/tasks/js/theme.js +0 -77
- django_cfg/apps/tasks/templates/tasks/base.html +0 -96
- django_cfg/apps/tasks/templates/tasks/components/info_cards.html +0 -85
- django_cfg/apps/tasks/templates/tasks/components/overview_tab.html +0 -22
- django_cfg/apps/tasks/templates/tasks/components/queues_tab.html +0 -19
- django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -103
- django_cfg/apps/tasks/templates/tasks/components/tasks_tab.html +0 -32
- django_cfg/apps/tasks/templates/tasks/components/workers_tab.html +0 -29
- django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -29
- django_cfg/apps/tasks/views.py +0 -461
- django_cfg/management/commands/auto_generate.py +0 -486
- django_cfg/middleware/static_nocache.py +0 -55
- django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
- /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -1,28 +1,59 @@
|
|
1
|
+
"""
|
2
|
+
Newsletter admin interfaces using Django Admin Utilities.
|
3
|
+
|
4
|
+
Enhanced newsletter management with Material Icons and optimized queries.
|
5
|
+
"""
|
6
|
+
|
1
7
|
from django import forms
|
2
8
|
from django.contrib import admin, messages
|
3
9
|
from django.urls import reverse
|
4
|
-
from django.utils.html import format_html
|
5
10
|
from django.http import HttpResponseRedirect
|
6
|
-
from
|
7
|
-
from
|
11
|
+
from django.db import models
|
12
|
+
from django.db.models import Count, Q
|
13
|
+
from unfold.admin import ModelAdmin, TabularInline
|
8
14
|
from unfold.contrib.forms.widgets import WysiwygWidget
|
9
|
-
from unfold.enums import ActionVariant
|
10
15
|
from django_cfg import ImportExportModelAdmin, ExportMixin, ImportForm, ExportForm
|
11
16
|
|
17
|
+
from django_cfg.modules.django_admin import (
|
18
|
+
OptimizedModelAdmin,
|
19
|
+
DisplayMixin,
|
20
|
+
StatusBadgeConfig,
|
21
|
+
DateTimeDisplayConfig,
|
22
|
+
Icons,
|
23
|
+
ActionVariant,
|
24
|
+
display,
|
25
|
+
action
|
26
|
+
)
|
27
|
+
from django_cfg.modules.django_admin.utils.badges import StatusBadge
|
28
|
+
|
12
29
|
from ..models import EmailLog, Newsletter, NewsletterSubscription, NewsletterCampaign
|
13
30
|
from .filters import UserEmailFilter, UserNameFilter, HasUserFilter, EmailOpenedFilter, EmailClickedFilter
|
14
31
|
from .resources import NewsletterResource, NewsletterSubscriptionResource, EmailLogResource
|
15
32
|
|
16
33
|
|
17
34
|
@admin.register(EmailLog)
|
18
|
-
class EmailLogAdmin(ModelAdmin, ExportMixin):
|
35
|
+
class EmailLogAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin, ExportMixin):
|
36
|
+
"""Admin interface for EmailLog using Django Admin Utilities."""
|
37
|
+
|
38
|
+
# Performance optimization
|
39
|
+
select_related_fields = ['user', 'newsletter']
|
40
|
+
|
19
41
|
# Export-only configuration
|
20
42
|
resource_class = EmailLogResource
|
21
43
|
export_form_class = ExportForm
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
44
|
+
|
45
|
+
list_display = [
|
46
|
+
'user_display', 'recipient_display', 'subject_display', 'newsletter_display',
|
47
|
+
'status_display', 'created_at_display', 'sent_at_display', 'tracking_display'
|
48
|
+
]
|
49
|
+
list_display_links = ['subject_display']
|
50
|
+
ordering = ['-created_at']
|
51
|
+
list_filter = [
|
52
|
+
'status', 'created_at', 'sent_at', 'newsletter',
|
53
|
+
EmailOpenedFilter, EmailClickedFilter, HasUserFilter, UserEmailFilter, UserNameFilter
|
54
|
+
]
|
55
|
+
autocomplete_fields = ['user']
|
56
|
+
search_fields = [
|
26
57
|
'recipient',
|
27
58
|
'subject',
|
28
59
|
'body',
|
@@ -30,69 +61,365 @@ class EmailLogAdmin(ModelAdmin, ExportMixin):
|
|
30
61
|
'user__username',
|
31
62
|
'user__email',
|
32
63
|
'newsletter__subject'
|
33
|
-
|
34
|
-
readonly_fields =
|
35
|
-
raw_id_fields =
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
'
|
56
|
-
'
|
57
|
-
|
58
|
-
|
64
|
+
]
|
65
|
+
readonly_fields = ['created_at', 'sent_at', 'newsletter']
|
66
|
+
raw_id_fields = ['user', 'newsletter']
|
67
|
+
|
68
|
+
fieldsets = [
|
69
|
+
('📧 Email Information', {
|
70
|
+
'fields': ['recipient', 'subject', 'body'],
|
71
|
+
'classes': ('tab',)
|
72
|
+
}),
|
73
|
+
('👤 User & Newsletter', {
|
74
|
+
'fields': ['user', 'newsletter'],
|
75
|
+
'classes': ('tab',)
|
76
|
+
}),
|
77
|
+
('📊 Status & Tracking', {
|
78
|
+
'fields': ['status', 'is_opened', 'is_clicked'],
|
79
|
+
'classes': ('tab',)
|
80
|
+
}),
|
81
|
+
('❌ Error Details', {
|
82
|
+
'fields': ['error_message'],
|
83
|
+
'classes': ('tab', 'collapse')
|
84
|
+
}),
|
85
|
+
('⏰ Timestamps', {
|
86
|
+
'fields': ['created_at', 'sent_at'],
|
87
|
+
'classes': ('tab', 'collapse')
|
88
|
+
})
|
89
|
+
]
|
90
|
+
|
91
|
+
@display(description="User")
|
92
|
+
def user_display(self, obj: EmailLog) -> str:
|
93
|
+
"""Display user."""
|
94
|
+
if not obj.user:
|
95
|
+
return "—"
|
96
|
+
return self.display_user_simple(obj.user)
|
97
|
+
|
98
|
+
@display(description="Recipient", ordering="recipient")
|
99
|
+
def recipient_display(self, obj: EmailLog) -> str:
|
100
|
+
"""Display recipient email."""
|
101
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.EMAIL)
|
102
|
+
return StatusBadge.create(
|
103
|
+
text=obj.recipient,
|
104
|
+
variant="info",
|
105
|
+
config=config
|
106
|
+
)
|
107
|
+
|
108
|
+
@display(description="Subject", ordering="subject")
|
109
|
+
def subject_display(self, obj: EmailLog) -> str:
|
110
|
+
"""Display email subject."""
|
111
|
+
if not obj.subject:
|
112
|
+
return "—"
|
113
|
+
|
114
|
+
subject = obj.subject
|
115
|
+
if len(subject) > 50:
|
116
|
+
subject = subject[:47] + "..."
|
117
|
+
|
118
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.MAIL)
|
119
|
+
return StatusBadge.create(
|
120
|
+
text=subject,
|
121
|
+
variant="primary",
|
122
|
+
config=config
|
123
|
+
)
|
124
|
+
|
125
|
+
@display(description="Newsletter")
|
126
|
+
def newsletter_display(self, obj: EmailLog) -> str:
|
127
|
+
"""Display newsletter link."""
|
128
|
+
if not obj.newsletter:
|
129
|
+
return "—"
|
130
|
+
|
131
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.CAMPAIGN)
|
132
|
+
return StatusBadge.create(
|
133
|
+
text=obj.newsletter.title,
|
134
|
+
variant="secondary",
|
135
|
+
config=config
|
136
|
+
)
|
137
|
+
|
138
|
+
@display(description="Status")
|
139
|
+
def status_display(self, obj: EmailLog) -> str:
|
140
|
+
"""Display email status."""
|
141
|
+
status_config = StatusBadgeConfig(
|
142
|
+
custom_mappings={
|
143
|
+
'pending': 'warning',
|
144
|
+
'sent': 'success',
|
145
|
+
'failed': 'danger',
|
146
|
+
'bounced': 'secondary'
|
147
|
+
},
|
148
|
+
show_icons=True,
|
149
|
+
icon=Icons.SCHEDULE if obj.status == 'pending' else Icons.CHECK_CIRCLE if obj.status == 'sent' else Icons.ERROR if obj.status == 'failed' else Icons.BOUNCE_EMAIL
|
59
150
|
)
|
60
|
-
|
151
|
+
return self.display_status_auto(obj, 'status', status_config)
|
152
|
+
|
153
|
+
@display(description="Created")
|
154
|
+
def created_at_display(self, obj: EmailLog) -> str:
|
155
|
+
"""Created time with relative display."""
|
156
|
+
config = DateTimeDisplayConfig(show_relative=True)
|
157
|
+
return self.display_datetime_relative(obj, 'created_at', config)
|
158
|
+
|
159
|
+
@display(description="Sent")
|
160
|
+
def sent_at_display(self, obj: EmailLog) -> str:
|
161
|
+
"""Sent time with relative display."""
|
162
|
+
if not obj.sent_at:
|
163
|
+
return "Not sent"
|
164
|
+
config = DateTimeDisplayConfig(show_relative=True)
|
165
|
+
return self.display_datetime_relative(obj, 'sent_at', config)
|
166
|
+
|
167
|
+
@display(description="Tracking")
|
168
|
+
def tracking_display(self, obj: EmailLog) -> str:
|
169
|
+
"""Display tracking status with badges."""
|
170
|
+
badges = []
|
171
|
+
|
172
|
+
if obj.is_opened:
|
173
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.VISIBILITY)
|
174
|
+
badges.append(StatusBadge.create(text="Opened", variant="success", config=config))
|
175
|
+
else:
|
176
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.VISIBILITY_OFF)
|
177
|
+
badges.append(StatusBadge.create(text="Not Opened", variant="secondary", config=config))
|
178
|
+
|
179
|
+
if obj.is_clicked:
|
180
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.MOUSE)
|
181
|
+
badges.append(StatusBadge.create(text="Clicked", variant="info", config=config))
|
182
|
+
else:
|
183
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.TOUCH_APP)
|
184
|
+
badges.append(StatusBadge.create(text="Not Clicked", variant="secondary", config=config))
|
185
|
+
|
186
|
+
return " | ".join(badges)
|
61
187
|
|
62
188
|
|
63
189
|
@admin.register(Newsletter)
|
64
|
-
class NewsletterAdmin(ModelAdmin, ImportExportModelAdmin):
|
190
|
+
class NewsletterAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin, ImportExportModelAdmin):
|
191
|
+
"""Admin interface for Newsletter using Django Admin Utilities."""
|
192
|
+
|
65
193
|
# Import/Export configuration
|
66
194
|
resource_class = NewsletterResource
|
67
195
|
import_form_class = ImportForm
|
68
196
|
export_form_class = ExportForm
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
197
|
+
|
198
|
+
list_display = [
|
199
|
+
'title_display', 'description_display', 'active_display',
|
200
|
+
'auto_subscribe_display', 'subscribers_count_display', 'created_at_display'
|
201
|
+
]
|
202
|
+
list_display_links = ['title_display']
|
203
|
+
ordering = ['-created_at']
|
204
|
+
list_filter = ['is_active', 'auto_subscribe', 'created_at']
|
205
|
+
search_fields = ['title', 'description']
|
206
|
+
readonly_fields = ['subscribers_count', 'created_at', 'updated_at']
|
207
|
+
|
208
|
+
fieldsets = [
|
209
|
+
('📰 Newsletter Information', {
|
210
|
+
'fields': ['title', 'description'],
|
211
|
+
'classes': ('tab',)
|
212
|
+
}),
|
213
|
+
('⚙️ Settings', {
|
214
|
+
'fields': ['is_active', 'auto_subscribe'],
|
215
|
+
'classes': ('tab',)
|
216
|
+
}),
|
217
|
+
('📊 Statistics', {
|
218
|
+
'fields': ['subscribers_count'],
|
219
|
+
'classes': ('tab', 'collapse')
|
220
|
+
}),
|
221
|
+
('⏰ Timestamps', {
|
222
|
+
'fields': ['created_at', 'updated_at'],
|
223
|
+
'classes': ('tab', 'collapse')
|
224
|
+
})
|
225
|
+
]
|
226
|
+
|
227
|
+
actions = ['activate_newsletters', 'deactivate_newsletters', 'enable_auto_subscribe']
|
228
|
+
|
229
|
+
@display(description="Title", ordering="title")
|
230
|
+
def title_display(self, obj: Newsletter) -> str:
|
231
|
+
"""Display newsletter title."""
|
232
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.CAMPAIGN)
|
233
|
+
return StatusBadge.create(
|
234
|
+
text=obj.title,
|
235
|
+
variant="primary",
|
236
|
+
config=config
|
237
|
+
)
|
238
|
+
|
239
|
+
@display(description="Description")
|
240
|
+
def description_display(self, obj: Newsletter) -> str:
|
241
|
+
"""Display newsletter description."""
|
242
|
+
if not obj.description:
|
243
|
+
return "—"
|
244
|
+
|
245
|
+
description = obj.description
|
246
|
+
if len(description) > 100:
|
247
|
+
description = description[:97] + "..."
|
248
|
+
|
249
|
+
return description
|
250
|
+
|
251
|
+
@display(description="Active")
|
252
|
+
def active_display(self, obj: Newsletter) -> str:
|
253
|
+
"""Display active status."""
|
254
|
+
if obj.is_active:
|
255
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.CHECK_CIRCLE)
|
256
|
+
return StatusBadge.create(text="Active", variant="success", config=config)
|
257
|
+
else:
|
258
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.CANCEL)
|
259
|
+
return StatusBadge.create(text="Inactive", variant="secondary", config=config)
|
260
|
+
|
261
|
+
@display(description="Auto Subscribe")
|
262
|
+
def auto_subscribe_display(self, obj: Newsletter) -> str:
|
263
|
+
"""Display auto subscribe status."""
|
264
|
+
if obj.auto_subscribe:
|
265
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.AUTO_AWESOME)
|
266
|
+
return StatusBadge.create(text="Auto", variant="info", config=config)
|
267
|
+
else:
|
268
|
+
return "Manual"
|
269
|
+
|
270
|
+
@display(description="Subscribers")
|
271
|
+
def subscribers_count_display(self, obj: Newsletter) -> str:
|
272
|
+
"""Display subscribers count."""
|
273
|
+
count = obj.subscribers_count or 0
|
274
|
+
if count == 0:
|
275
|
+
return "No subscribers"
|
276
|
+
elif count == 1:
|
277
|
+
return "1 subscriber"
|
278
|
+
else:
|
279
|
+
return f"{count} subscribers"
|
280
|
+
|
281
|
+
@display(description="Created")
|
282
|
+
def created_at_display(self, obj: Newsletter) -> str:
|
283
|
+
"""Created time with relative display."""
|
284
|
+
config = DateTimeDisplayConfig(show_relative=True)
|
285
|
+
return self.display_datetime_relative(obj, 'created_at', config)
|
286
|
+
|
287
|
+
@action(description="Activate newsletters", variant=ActionVariant.SUCCESS)
|
288
|
+
def activate_newsletters(self, request, queryset):
|
289
|
+
"""Activate selected newsletters."""
|
290
|
+
count = queryset.update(is_active=True)
|
291
|
+
messages.success(request, f"Successfully activated {count} newsletters.")
|
292
|
+
|
293
|
+
@action(description="Deactivate newsletters", variant=ActionVariant.DANGER)
|
294
|
+
def deactivate_newsletters(self, request, queryset):
|
295
|
+
"""Deactivate selected newsletters."""
|
296
|
+
count = queryset.update(is_active=False)
|
297
|
+
messages.warning(request, f"Successfully deactivated {count} newsletters.")
|
298
|
+
|
299
|
+
@action(description="Enable auto subscribe", variant=ActionVariant.INFO)
|
300
|
+
def enable_auto_subscribe(self, request, queryset):
|
301
|
+
"""Enable auto subscribe for selected newsletters."""
|
302
|
+
count = queryset.update(auto_subscribe=True)
|
303
|
+
messages.info(request, f"Enabled auto subscribe for {count} newsletters.")
|
73
304
|
|
74
305
|
|
75
|
-
class NewsletterSubscriptionInline(
|
306
|
+
class NewsletterSubscriptionInline(TabularInline):
|
307
|
+
"""Inline for newsletter subscriptions."""
|
308
|
+
|
76
309
|
model = NewsletterSubscription
|
77
|
-
fields =
|
78
|
-
readonly_fields =
|
310
|
+
fields = ['email', 'user', 'is_active', 'subscribed_at']
|
311
|
+
readonly_fields = ['subscribed_at']
|
79
312
|
extra = 0
|
80
313
|
|
81
314
|
|
82
315
|
@admin.register(NewsletterSubscription)
|
83
|
-
class NewsletterSubscriptionAdmin(ModelAdmin, ImportExportModelAdmin):
|
316
|
+
class NewsletterSubscriptionAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin, ImportExportModelAdmin):
|
317
|
+
"""Admin interface for NewsletterSubscription using Django Admin Utilities."""
|
318
|
+
|
319
|
+
# Performance optimization
|
320
|
+
select_related_fields = ['user', 'newsletter']
|
321
|
+
|
84
322
|
# Import/Export configuration
|
85
323
|
resource_class = NewsletterSubscriptionResource
|
86
324
|
import_form_class = ImportForm
|
87
325
|
export_form_class = ExportForm
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
326
|
+
|
327
|
+
list_display = [
|
328
|
+
'email_display', 'newsletter_display', 'user_display', 'active_display',
|
329
|
+
'subscribed_at_display', 'unsubscribed_at_display'
|
330
|
+
]
|
331
|
+
list_display_links = ['email_display']
|
332
|
+
ordering = ['-subscribed_at']
|
333
|
+
list_filter = ['is_active', 'newsletter', 'subscribed_at']
|
334
|
+
search_fields = ['email', 'user__email', 'newsletter__title']
|
335
|
+
readonly_fields = ['subscribed_at', 'unsubscribed_at']
|
336
|
+
autocomplete_fields = ['user', 'newsletter']
|
337
|
+
|
338
|
+
fieldsets = [
|
339
|
+
('📧 Subscription Information', {
|
340
|
+
'fields': ['email', 'newsletter', 'user'],
|
341
|
+
'classes': ('tab',)
|
342
|
+
}),
|
343
|
+
('⚙️ Status', {
|
344
|
+
'fields': ['is_active'],
|
345
|
+
'classes': ('tab',)
|
346
|
+
}),
|
347
|
+
('⏰ Timestamps', {
|
348
|
+
'fields': ['subscribed_at', 'unsubscribed_at'],
|
349
|
+
'classes': ('tab',)
|
350
|
+
})
|
351
|
+
]
|
352
|
+
|
353
|
+
actions = ['activate_subscriptions', 'deactivate_subscriptions']
|
354
|
+
|
355
|
+
@display(description="Email", ordering="email")
|
356
|
+
def email_display(self, obj: NewsletterSubscription) -> str:
|
357
|
+
"""Display subscription email."""
|
358
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.EMAIL)
|
359
|
+
return StatusBadge.create(
|
360
|
+
text=obj.email,
|
361
|
+
variant="info",
|
362
|
+
config=config
|
363
|
+
)
|
364
|
+
|
365
|
+
@display(description="Newsletter")
|
366
|
+
def newsletter_display(self, obj: NewsletterSubscription) -> str:
|
367
|
+
"""Display newsletter."""
|
368
|
+
if not obj.newsletter:
|
369
|
+
return "—"
|
370
|
+
|
371
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.CAMPAIGN)
|
372
|
+
return StatusBadge.create(
|
373
|
+
text=obj.newsletter.title,
|
374
|
+
variant="primary",
|
375
|
+
config=config
|
376
|
+
)
|
377
|
+
|
378
|
+
@display(description="User")
|
379
|
+
def user_display(self, obj: NewsletterSubscription) -> str:
|
380
|
+
"""Display user."""
|
381
|
+
if not obj.user:
|
382
|
+
return "—"
|
383
|
+
return self.display_user_simple(obj.user)
|
384
|
+
|
385
|
+
@display(description="Active")
|
386
|
+
def active_display(self, obj: NewsletterSubscription) -> str:
|
387
|
+
"""Display active status."""
|
388
|
+
if obj.is_active:
|
389
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.CHECK_CIRCLE)
|
390
|
+
return StatusBadge.create(text="Active", variant="success", config=config)
|
391
|
+
else:
|
392
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.CANCEL)
|
393
|
+
return StatusBadge.create(text="Inactive", variant="secondary", config=config)
|
394
|
+
|
395
|
+
@display(description="Subscribed")
|
396
|
+
def subscribed_at_display(self, obj: NewsletterSubscription) -> str:
|
397
|
+
"""Subscribed time with relative display."""
|
398
|
+
config = DateTimeDisplayConfig(show_relative=True)
|
399
|
+
return self.display_datetime_relative(obj, 'subscribed_at', config)
|
400
|
+
|
401
|
+
@display(description="Unsubscribed")
|
402
|
+
def unsubscribed_at_display(self, obj: NewsletterSubscription) -> str:
|
403
|
+
"""Unsubscribed time with relative display."""
|
404
|
+
if not obj.unsubscribed_at:
|
405
|
+
return "—"
|
406
|
+
config = DateTimeDisplayConfig(show_relative=True)
|
407
|
+
return self.display_datetime_relative(obj, 'unsubscribed_at', config)
|
408
|
+
|
409
|
+
@action(description="Activate subscriptions", variant=ActionVariant.SUCCESS)
|
410
|
+
def activate_subscriptions(self, request, queryset):
|
411
|
+
"""Activate selected subscriptions."""
|
412
|
+
count = queryset.update(is_active=True)
|
413
|
+
messages.success(request, f"Successfully activated {count} subscriptions.")
|
414
|
+
|
415
|
+
@action(description="Deactivate subscriptions", variant=ActionVariant.DANGER)
|
416
|
+
def deactivate_subscriptions(self, request, queryset):
|
417
|
+
"""Deactivate selected subscriptions."""
|
418
|
+
count = queryset.update(is_active=False)
|
419
|
+
messages.warning(request, f"Successfully deactivated {count} subscriptions.")
|
93
420
|
|
94
421
|
|
95
|
-
#
|
422
|
+
# Form for NewsletterCampaignAdmin with Unfold Wysiwyg
|
96
423
|
class NewsletterCampaignAdminForm(forms.ModelForm):
|
97
424
|
main_html_content = forms.CharField(widget=WysiwygWidget(), required=False)
|
98
425
|
|
@@ -101,148 +428,159 @@ class NewsletterCampaignAdminForm(forms.ModelForm):
|
|
101
428
|
fields = '__all__'
|
102
429
|
|
103
430
|
|
104
|
-
# --- Inline for Email Logs within Campaign Admin --- #
|
105
|
-
class EmailLogInline(admin.TabularInline):
|
106
|
-
model = EmailLog
|
107
|
-
fk_name = 'campaign' # Specify which ForeignKey to use
|
108
|
-
fields = ('user', 'recipient', 'status', 'sent_at')
|
109
|
-
readonly_fields = ('user', 'recipient', 'status', 'created_at', 'sent_at')
|
110
|
-
can_delete = False
|
111
|
-
extra = 0
|
112
|
-
show_change_link = True
|
113
|
-
verbose_name = "Sent Email Log"
|
114
|
-
verbose_name_plural = "Sent Email Logs"
|
115
|
-
|
116
|
-
def has_add_permission(self, request, obj=None):
|
117
|
-
return False
|
118
|
-
|
119
|
-
|
120
431
|
@admin.register(NewsletterCampaign)
|
121
|
-
class NewsletterCampaignAdmin(ModelAdmin):
|
432
|
+
class NewsletterCampaignAdmin(OptimizedModelAdmin, DisplayMixin, ModelAdmin, ImportExportModelAdmin):
|
433
|
+
"""Admin interface for NewsletterCampaign using Django Admin Utilities."""
|
434
|
+
|
435
|
+
# Performance optimization
|
436
|
+
select_related_fields = ['newsletter']
|
437
|
+
|
122
438
|
form = NewsletterCampaignAdminForm
|
123
|
-
|
124
|
-
list_display =
|
125
|
-
'
|
126
|
-
'
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
if
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
self.message_user(
|
214
|
-
request,
|
215
|
-
f"{skipped_count} campaigns were skipped.",
|
216
|
-
messages.WARNING
|
217
|
-
)
|
218
|
-
|
219
|
-
send_selected_campaigns.short_description = "Send selected campaigns"
|
220
|
-
|
221
|
-
@action(
|
222
|
-
description="Send & Continue Editing",
|
223
|
-
icon="send",
|
224
|
-
variant=ActionVariant.INFO,
|
225
|
-
permissions=["change"]
|
226
|
-
)
|
227
|
-
def send_and_continue(self, request, obj):
|
228
|
-
"""Send campaign and continue editing (submit line action)."""
|
229
|
-
if obj.status == NewsletterCampaign.CampaignStatus.DRAFT:
|
230
|
-
success = obj.send_campaign()
|
231
|
-
if success:
|
232
|
-
self.message_user(
|
233
|
-
request,
|
234
|
-
f"Campaign '{obj.subject}' sent successfully.",
|
235
|
-
messages.SUCCESS
|
236
|
-
)
|
237
|
-
else:
|
238
|
-
self.message_user(
|
239
|
-
request,
|
240
|
-
f"Campaign '{obj.subject}' failed to send.",
|
241
|
-
messages.ERROR
|
242
|
-
)
|
439
|
+
|
440
|
+
list_display = [
|
441
|
+
'subject_display', 'newsletter_display', 'status_display',
|
442
|
+
'sent_at_display', 'recipient_count_display'
|
443
|
+
]
|
444
|
+
list_display_links = ['subject_display']
|
445
|
+
ordering = ['-created_at']
|
446
|
+
list_filter = ['status', 'newsletter', 'sent_at']
|
447
|
+
search_fields = ['subject', 'newsletter__title', 'main_html_content']
|
448
|
+
readonly_fields = ['sent_at', 'recipient_count', 'created_at']
|
449
|
+
autocomplete_fields = ['newsletter']
|
450
|
+
|
451
|
+
fieldsets = [
|
452
|
+
('📧 Campaign Information', {
|
453
|
+
'fields': ['subject', 'newsletter'],
|
454
|
+
'classes': ('tab',)
|
455
|
+
}),
|
456
|
+
('📝 Content', {
|
457
|
+
'fields': ['main_html_content'],
|
458
|
+
'classes': ('tab',)
|
459
|
+
}),
|
460
|
+
('📊 Status & Stats', {
|
461
|
+
'fields': ['status', 'recipient_count'],
|
462
|
+
'classes': ('tab',)
|
463
|
+
}),
|
464
|
+
('⏰ Timestamps', {
|
465
|
+
'fields': ['sent_at', 'created_at'],
|
466
|
+
'classes': ('tab', 'collapse')
|
467
|
+
})
|
468
|
+
]
|
469
|
+
|
470
|
+
actions = ['send_campaigns', 'schedule_campaigns', 'cancel_campaigns']
|
471
|
+
|
472
|
+
@display(description="Subject", ordering="subject")
|
473
|
+
def subject_display(self, obj: NewsletterCampaign) -> str:
|
474
|
+
"""Display campaign subject."""
|
475
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.MAIL)
|
476
|
+
return StatusBadge.create(
|
477
|
+
text=obj.subject,
|
478
|
+
variant="primary",
|
479
|
+
config=config
|
480
|
+
)
|
481
|
+
|
482
|
+
@display(description="Newsletter")
|
483
|
+
def newsletter_display(self, obj: NewsletterCampaign) -> str:
|
484
|
+
"""Display newsletter."""
|
485
|
+
if not obj.newsletter:
|
486
|
+
return "—"
|
487
|
+
|
488
|
+
config = StatusBadgeConfig(show_icons=True, icon=Icons.CAMPAIGN)
|
489
|
+
return StatusBadge.create(
|
490
|
+
text=obj.newsletter.title,
|
491
|
+
variant="secondary",
|
492
|
+
config=config
|
493
|
+
)
|
494
|
+
|
495
|
+
@display(description="Status")
|
496
|
+
def status_display(self, obj: NewsletterCampaign) -> str:
|
497
|
+
"""Display campaign status."""
|
498
|
+
status_config = StatusBadgeConfig(
|
499
|
+
custom_mappings={
|
500
|
+
'draft': 'secondary',
|
501
|
+
'scheduled': 'warning',
|
502
|
+
'sending': 'info',
|
503
|
+
'sent': 'success',
|
504
|
+
'failed': 'danger',
|
505
|
+
'cancelled': 'secondary'
|
506
|
+
},
|
507
|
+
show_icons=True,
|
508
|
+
icon=Icons.EDIT if obj.status == 'draft' else Icons.SCHEDULE if obj.status == 'scheduled' else Icons.SEND if obj.status == 'sending' else Icons.CHECK_CIRCLE if obj.status == 'sent' else Icons.ERROR if obj.status == 'failed' else Icons.CANCEL
|
509
|
+
)
|
510
|
+
return self.display_status_auto(obj, 'status', status_config)
|
511
|
+
|
512
|
+
|
513
|
+
@display(description="Sent")
|
514
|
+
def sent_at_display(self, obj: NewsletterCampaign) -> str:
|
515
|
+
"""Sent time with relative display."""
|
516
|
+
if not obj.sent_at:
|
517
|
+
return "Not sent"
|
518
|
+
config = DateTimeDisplayConfig(show_relative=True)
|
519
|
+
return self.display_datetime_relative(obj, 'sent_at', config)
|
520
|
+
|
521
|
+
@display(description="Recipients")
|
522
|
+
def recipient_count_display(self, obj: NewsletterCampaign) -> str:
|
523
|
+
"""Display recipients count."""
|
524
|
+
count = obj.recipient_count or 0
|
525
|
+
if count == 0:
|
526
|
+
return "No recipients"
|
527
|
+
elif count == 1:
|
528
|
+
return "1 recipient"
|
243
529
|
else:
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
530
|
+
return f"{count} recipients"
|
531
|
+
|
532
|
+
@action(description="Send campaigns", variant=ActionVariant.SUCCESS)
|
533
|
+
def send_campaigns(self, request, queryset):
|
534
|
+
"""Send selected campaigns."""
|
535
|
+
sendable_count = queryset.filter(status__in=['draft', 'scheduled']).count()
|
536
|
+
if sendable_count == 0:
|
537
|
+
messages.error(request, "No sendable campaigns selected.")
|
538
|
+
return
|
539
|
+
|
540
|
+
queryset.filter(status__in=['draft', 'scheduled']).update(status='sending')
|
541
|
+
messages.success(request, f"Started sending {sendable_count} campaigns.")
|
542
|
+
|
543
|
+
@action(description="Schedule campaigns", variant=ActionVariant.WARNING)
|
544
|
+
def schedule_campaigns(self, request, queryset):
|
545
|
+
"""Schedule selected campaigns."""
|
546
|
+
schedulable_count = queryset.filter(status='draft').count()
|
547
|
+
if schedulable_count == 0:
|
548
|
+
messages.error(request, "No draft campaigns selected.")
|
549
|
+
return
|
550
|
+
|
551
|
+
queryset.filter(status='draft').update(status='scheduled')
|
552
|
+
messages.warning(request, f"Scheduled {schedulable_count} campaigns.")
|
553
|
+
|
554
|
+
@action(description="Cancel campaigns", variant=ActionVariant.DANGER)
|
555
|
+
def cancel_campaigns(self, request, queryset):
|
556
|
+
"""Cancel selected campaigns."""
|
557
|
+
cancelable_count = queryset.filter(status__in=['draft', 'scheduled']).count()
|
558
|
+
if cancelable_count == 0:
|
559
|
+
messages.error(request, "No cancelable campaigns selected.")
|
560
|
+
return
|
561
|
+
|
562
|
+
queryset.filter(status__in=['draft', 'scheduled']).update(status='cancelled')
|
563
|
+
messages.error(request, f"Cancelled {cancelable_count} campaigns.")
|
564
|
+
|
565
|
+
def changelist_view(self, request, extra_context=None):
|
566
|
+
"""Add campaign statistics to changelist."""
|
567
|
+
extra_context = extra_context or {}
|
568
|
+
|
569
|
+
queryset = self.get_queryset(request)
|
570
|
+
stats = queryset.aggregate(
|
571
|
+
total_campaigns=Count('id'),
|
572
|
+
draft_campaigns=Count('id', filter=Q(status='draft')),
|
573
|
+
scheduled_campaigns=Count('id', filter=Q(status='scheduled')),
|
574
|
+
sent_campaigns=Count('id', filter=Q(status='sent')),
|
575
|
+
failed_campaigns=Count('id', filter=Q(status='failed'))
|
576
|
+
)
|
577
|
+
|
578
|
+
extra_context['campaign_stats'] = {
|
579
|
+
'total_campaigns': stats['total_campaigns'] or 0,
|
580
|
+
'draft_campaigns': stats['draft_campaigns'] or 0,
|
581
|
+
'scheduled_campaigns': stats['scheduled_campaigns'] or 0,
|
582
|
+
'sent_campaigns': stats['sent_campaigns'] or 0,
|
583
|
+
'failed_campaigns': stats['failed_campaigns'] or 0
|
584
|
+
}
|
585
|
+
|
586
|
+
return super().changelist_view(request, extra_context)
|