django-cfg 1.3.5__py3-none-any.whl → 1.3.9__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.
Files changed (252) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/admin/__init__.py +24 -8
  3. django_cfg/apps/accounts/admin/activity_admin.py +146 -0
  4. django_cfg/apps/accounts/admin/filters.py +98 -22
  5. django_cfg/apps/accounts/admin/group_admin.py +86 -0
  6. django_cfg/apps/accounts/admin/inlines.py +42 -13
  7. django_cfg/apps/accounts/admin/otp_admin.py +115 -0
  8. django_cfg/apps/accounts/admin/registration_admin.py +173 -0
  9. django_cfg/apps/accounts/admin/resources.py +123 -19
  10. django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
  11. django_cfg/apps/accounts/admin/user_admin.py +362 -0
  12. django_cfg/apps/agents/admin/__init__.py +17 -4
  13. django_cfg/apps/agents/admin/execution_admin.py +204 -183
  14. django_cfg/apps/agents/admin/registry_admin.py +230 -255
  15. django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
  16. django_cfg/apps/agents/core/__init__.py +1 -1
  17. django_cfg/apps/agents/core/django_agent.py +221 -0
  18. django_cfg/apps/agents/core/exceptions.py +14 -0
  19. django_cfg/apps/agents/core/orchestrator.py +18 -3
  20. django_cfg/apps/knowbase/admin/__init__.py +1 -1
  21. django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
  22. django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
  23. django_cfg/apps/knowbase/admin/document_admin.py +269 -262
  24. django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
  25. django_cfg/apps/knowbase/config/settings.py +21 -4
  26. django_cfg/apps/knowbase/views/chat_views.py +3 -0
  27. django_cfg/apps/leads/admin/__init__.py +3 -1
  28. django_cfg/apps/leads/admin/leads_admin.py +235 -35
  29. django_cfg/apps/maintenance/admin/__init__.py +2 -2
  30. django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
  31. django_cfg/apps/maintenance/admin/log_admin.py +143 -61
  32. django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
  33. django_cfg/apps/maintenance/admin/site_admin.py +213 -352
  34. django_cfg/apps/newsletter/admin/__init__.py +29 -2
  35. django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
  36. django_cfg/apps/payments/admin/__init__.py +18 -27
  37. django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
  38. django_cfg/apps/payments/admin/balance_admin.py +166 -632
  39. django_cfg/apps/payments/admin/currencies_admin.py +235 -607
  40. django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
  41. django_cfg/apps/payments/admin/filters.py +83 -3
  42. django_cfg/apps/payments/admin/networks_admin.py +258 -0
  43. django_cfg/apps/payments/admin/payments_admin.py +171 -461
  44. django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
  45. django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
  46. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
  47. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
  48. django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
  49. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
  50. django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
  51. django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
  52. django_cfg/apps/payments/middleware/api_access.py +32 -6
  53. django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
  54. django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
  55. django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
  56. django_cfg/apps/payments/models/balance.py +12 -0
  57. django_cfg/apps/payments/models/currencies.py +106 -32
  58. django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
  59. django_cfg/apps/payments/services/core/currency_service.py +35 -28
  60. django_cfg/apps/payments/services/core/payment_service.py +1 -1
  61. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  62. django_cfg/apps/payments/services/providers/base.py +95 -39
  63. django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
  64. django_cfg/apps/payments/services/providers/models/base.py +122 -0
  65. django_cfg/apps/payments/services/providers/models/providers.py +87 -0
  66. django_cfg/apps/payments/services/providers/models/universal.py +48 -0
  67. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
  68. django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
  69. django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
  70. django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
  71. django_cfg/apps/payments/services/providers/{nowpayments.py → nowpayments/provider.py} +240 -209
  72. django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
  73. django_cfg/apps/payments/services/providers/registry.py +4 -32
  74. django_cfg/apps/payments/services/providers/sync_service.py +277 -0
  75. django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
  76. django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
  77. django_cfg/apps/payments/tasks/__init__.py +39 -0
  78. django_cfg/apps/payments/tasks/types.py +73 -0
  79. django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
  80. django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
  81. django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
  82. django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
  83. django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
  84. django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
  85. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
  86. django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
  87. django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
  88. django_cfg/apps/payments/urls_admin.py +1 -1
  89. django_cfg/apps/payments/views/api/currencies.py +5 -5
  90. django_cfg/apps/payments/views/overview/services.py +2 -2
  91. django_cfg/apps/payments/views/serializers/currencies.py +4 -3
  92. django_cfg/apps/support/admin/__init__.py +10 -1
  93. django_cfg/apps/support/admin/support_admin.py +338 -141
  94. django_cfg/apps/tasks/admin/__init__.py +11 -0
  95. django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
  96. django_cfg/apps/urls.py +1 -2
  97. django_cfg/config.py +1 -1
  98. django_cfg/core/config.py +10 -5
  99. django_cfg/core/generation.py +1 -1
  100. django_cfg/management/commands/__init__.py +13 -1
  101. django_cfg/management/commands/app_agent_diagnose.py +470 -0
  102. django_cfg/management/commands/app_agent_generate.py +342 -0
  103. django_cfg/management/commands/app_agent_info.py +308 -0
  104. django_cfg/management/commands/migrate_all.py +9 -3
  105. django_cfg/management/commands/migrator.py +11 -6
  106. django_cfg/management/commands/rundramatiq.py +3 -2
  107. django_cfg/middleware/__init__.py +0 -2
  108. django_cfg/models/api_keys.py +115 -0
  109. django_cfg/modules/django_admin/__init__.py +64 -0
  110. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  111. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  112. django_cfg/modules/django_admin/decorators/display.py +106 -0
  113. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  114. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  115. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  116. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  117. django_cfg/modules/django_admin/models/__init__.py +20 -0
  118. django_cfg/modules/django_admin/models/action_models.py +33 -0
  119. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  120. django_cfg/modules/django_admin/models/base.py +26 -0
  121. django_cfg/modules/django_admin/models/display_models.py +31 -0
  122. django_cfg/modules/django_admin/utils/badges.py +159 -0
  123. django_cfg/modules/django_admin/utils/displays.py +247 -0
  124. django_cfg/modules/django_app_agent/__init__.py +87 -0
  125. django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
  126. django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
  127. django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
  128. django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
  129. django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
  130. django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
  131. django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
  132. django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
  133. django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
  134. django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
  135. django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
  136. django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
  137. django_cfg/modules/django_app_agent/core/__init__.py +33 -0
  138. django_cfg/modules/django_app_agent/core/config.py +300 -0
  139. django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
  140. django_cfg/modules/django_app_agent/models/__init__.py +71 -0
  141. django_cfg/modules/django_app_agent/models/base.py +283 -0
  142. django_cfg/modules/django_app_agent/models/context.py +496 -0
  143. django_cfg/modules/django_app_agent/models/enums.py +481 -0
  144. django_cfg/modules/django_app_agent/models/requests.py +500 -0
  145. django_cfg/modules/django_app_agent/models/responses.py +585 -0
  146. django_cfg/modules/django_app_agent/pytest.ini +6 -0
  147. django_cfg/modules/django_app_agent/services/__init__.py +42 -0
  148. django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
  149. django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
  150. django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
  151. django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
  152. django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
  153. django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
  154. django_cfg/modules/django_app_agent/services/base.py +437 -0
  155. django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
  156. django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
  157. django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
  158. django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
  159. django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
  160. django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
  161. django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
  162. django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
  163. django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
  164. django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
  165. django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
  166. django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
  167. django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
  168. django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
  169. django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
  170. django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
  171. django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
  172. django_cfg/modules/django_app_agent/services/report_service.py +332 -0
  173. django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
  174. django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
  175. django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
  176. django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
  177. django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
  178. django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
  179. django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
  180. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
  181. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
  182. django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
  183. django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
  184. django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
  185. django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
  186. django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
  187. django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
  188. django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
  189. django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
  190. django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
  191. django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
  192. django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
  193. django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
  194. django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
  195. django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
  196. django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
  197. django_cfg/modules/django_app_agent/ui/cli.py +419 -0
  198. django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
  199. django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
  200. django_cfg/modules/django_app_agent/utils/logging.py +360 -0
  201. django_cfg/modules/django_app_agent/utils/validation.py +417 -0
  202. django_cfg/modules/django_currency/__init__.py +2 -2
  203. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  204. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  205. django_cfg/modules/django_currency/core/converter.py +12 -12
  206. django_cfg/modules/django_currency/database/__init__.py +2 -2
  207. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  208. django_cfg/modules/django_llm/llm/client.py +10 -2
  209. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  210. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  211. django_cfg/modules/django_unfold/dashboard.py +14 -13
  212. django_cfg/modules/django_unfold/models/config.py +1 -1
  213. django_cfg/registry/core.py +3 -0
  214. django_cfg/registry/third_party.py +2 -2
  215. django_cfg/template_archive/django_sample.zip +0 -0
  216. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/METADATA +2 -1
  217. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/RECORD +224 -118
  218. django_cfg/apps/accounts/admin/activity.py +0 -96
  219. django_cfg/apps/accounts/admin/group.py +0 -17
  220. django_cfg/apps/accounts/admin/otp.py +0 -59
  221. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  222. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  223. django_cfg/apps/accounts/admin/user.py +0 -300
  224. django_cfg/apps/agents/core/agent.py +0 -281
  225. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  226. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  227. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  228. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  229. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  230. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  231. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  232. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  233. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  234. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  235. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  236. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  237. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  238. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  239. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  240. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  241. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  242. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  243. django_cfg/apps/tasks/admin.py +0 -320
  244. django_cfg/middleware/static_nocache.py +0 -55
  245. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  246. /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
  247. /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
  248. /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
  249. /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
  250. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/WHEEL +0 -0
  251. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
  252. {django_cfg-1.3.5.dist-info → django_cfg-1.3.9.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 unfold.admin import ModelAdmin
7
- from unfold.decorators import action
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
- list_display = ('user', 'recipient', 'subject', 'newsletter_link', 'status', 'created_at', 'sent_at', 'tracking_status')
23
- list_filter = ('status', 'created_at', 'sent_at', 'newsletter', EmailOpenedFilter, EmailClickedFilter, HasUserFilter, UserEmailFilter, UserNameFilter)
24
- autocomplete_fields = ('user',)
25
- search_fields = (
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 = ('created_at', 'sent_at', 'newsletter')
35
- raw_id_fields = ('user', 'newsletter')
36
-
37
- def newsletter_link(self, obj):
38
- if obj.newsletter:
39
- link = reverse("admin:django_cfg_newsletter_newsletter_change", args=[obj.newsletter.id])
40
- return format_html('<a href="{}">{}</a>', link, obj.newsletter.title)
41
- return "-"
42
- newsletter_link.short_description = 'Newsletter'
43
-
44
- def tracking_status(self, obj):
45
- """Show clean tracking status."""
46
- opened_status = "Opened" if obj.is_opened else "Not opened"
47
- clicked_status = "Clicked" if obj.is_clicked else "Not clicked"
48
-
49
- opened_color = "#28a745" if obj.is_opened else "#dc3545"
50
- clicked_color = "#007bff" if obj.is_clicked else "#6c757d"
51
-
52
- return format_html(
53
- '<div style="display: flex; gap: 8px;">'
54
- '<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span>'
55
- '<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span>'
56
- '</div>',
57
- opened_color, opened_status,
58
- clicked_color, clicked_status
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
- tracking_status.short_description = "Tracking Status"
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
- list_display = ('title', 'description', 'is_active', 'auto_subscribe', 'subscribers_count', 'created_at')
70
- list_filter = ('is_active', 'auto_subscribe', 'created_at')
71
- search_fields = ('title', 'description')
72
- readonly_fields = ('subscribers_count', 'created_at', 'updated_at')
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(admin.TabularInline):
306
+ class NewsletterSubscriptionInline(TabularInline):
307
+ """Inline for newsletter subscriptions."""
308
+
76
309
  model = NewsletterSubscription
77
- fields = ('email', 'user', 'is_active', 'subscribed_at')
78
- readonly_fields = ('subscribed_at',)
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
- list_display = ('email', 'newsletter', 'user', 'is_active', 'subscribed_at', 'unsubscribed_at')
89
- list_filter = ('is_active', 'newsletter', 'subscribed_at')
90
- search_fields = ('email', 'user__email', 'newsletter__title')
91
- readonly_fields = ('subscribed_at', 'unsubscribed_at')
92
- autocomplete_fields = ('user', 'newsletter')
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
- # --- Form for NewsletterCampaignAdmin with Unfold Wysiwyg --- #
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
- inlines = [EmailLogInline]
124
- list_display = (
125
- 'newsletter',
126
- 'subject',
127
- 'status',
128
- 'created_at',
129
- 'sent_at',
130
- 'recipient_count',
131
- )
132
- list_filter = ('status', 'newsletter', 'created_at')
133
- readonly_fields = ('status', 'created_at', 'sent_at', 'recipient_count')
134
- search_fields = ('subject', 'email_title', 'main_text')
135
- autocomplete_fields = ('newsletter',)
136
-
137
- # Django admin actions
138
- actions = ["send_selected_campaigns"]
139
-
140
- # Unfold actions configuration
141
- actions_list = [] # Changelist actions (removed send_selected_campaigns)
142
- actions_detail = ["send_campaign"] # Detail page actions
143
- actions_submit_line = ["send_and_continue"] # Form submit line actions
144
-
145
- @action(
146
- description="Send Campaign",
147
- icon="send",
148
- variant=ActionVariant.SUCCESS,
149
- permissions=["change"]
150
- )
151
- def send_campaign(self, request, object_id):
152
- """Send individual campaign from detail page."""
153
- try:
154
- campaign = self.get_object(request, object_id)
155
- if not campaign:
156
- self.message_user(request, "Campaign not found.", messages.ERROR)
157
- return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
158
-
159
- if campaign.status != NewsletterCampaign.CampaignStatus.DRAFT:
160
- self.message_user(
161
- request,
162
- f"Campaign '{campaign.subject}' is not in draft status.",
163
- messages.WARNING
164
- )
165
- return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
166
-
167
- success = campaign.send_campaign()
168
- if success:
169
- self.message_user(
170
- request,
171
- f"Campaign '{campaign.subject}' sent successfully.",
172
- messages.SUCCESS
173
- )
174
- else:
175
- self.message_user(
176
- request,
177
- f"Campaign '{campaign.subject}' failed to send.",
178
- messages.ERROR
179
- )
180
-
181
- except Exception as e:
182
- self.message_user(request, f"An error occurred: {e}", messages.ERROR)
183
-
184
- return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
185
-
186
- def send_selected_campaigns(self, request, queryset):
187
- """Send multiple campaigns (standard Django admin action)."""
188
- sent_count = 0
189
- skipped_count = 0
190
-
191
- for campaign in queryset:
192
- if campaign.status == NewsletterCampaign.CampaignStatus.DRAFT:
193
- success = campaign.send_campaign()
194
- if success:
195
- sent_count += 1
196
- else:
197
- skipped_count += 1
198
- else:
199
- skipped_count += 1
200
- messages.warning(
201
- request,
202
- f"Campaign '{campaign.subject}' skipped (not in Draft status)."
203
- )
204
-
205
- if sent_count > 0:
206
- self.message_user(
207
- request,
208
- f"Successfully sent {sent_count} campaigns.",
209
- messages.SUCCESS
210
- )
211
-
212
- if skipped_count > 0:
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
- self.message_user(
245
- request,
246
- f"Campaign '{obj.subject}' is not in draft status.",
247
- messages.WARNING
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)