django-cfg 1.1.81__py3-none-any.whl → 1.2.0__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 (246) hide show
  1. django_cfg/__init__.py +20 -448
  2. django_cfg/apps/accounts/README.md +3 -3
  3. django_cfg/apps/accounts/admin/__init__.py +0 -2
  4. django_cfg/apps/accounts/admin/activity.py +2 -9
  5. django_cfg/apps/accounts/admin/filters.py +0 -42
  6. django_cfg/apps/accounts/admin/inlines.py +8 -8
  7. django_cfg/apps/accounts/admin/otp.py +5 -5
  8. django_cfg/apps/accounts/admin/registration_source.py +1 -8
  9. django_cfg/apps/accounts/admin/user.py +12 -20
  10. django_cfg/apps/accounts/managers/user_manager.py +2 -129
  11. django_cfg/apps/accounts/migrations/0006_remove_twilioresponse_otp_secret_and_more.py +46 -0
  12. django_cfg/apps/accounts/models.py +3 -123
  13. django_cfg/apps/accounts/serializers/otp.py +40 -44
  14. django_cfg/apps/accounts/serializers/profile.py +0 -2
  15. django_cfg/apps/accounts/services/otp_service.py +98 -186
  16. django_cfg/apps/accounts/signals.py +25 -15
  17. django_cfg/apps/accounts/utils/auth_email_service.py +84 -0
  18. django_cfg/apps/accounts/views/otp.py +35 -36
  19. django_cfg/apps/agents/README.md +129 -0
  20. django_cfg/apps/agents/__init__.py +68 -0
  21. django_cfg/apps/agents/admin/__init__.py +17 -0
  22. django_cfg/apps/agents/admin/execution_admin.py +460 -0
  23. django_cfg/apps/agents/admin/registry_admin.py +360 -0
  24. django_cfg/apps/agents/admin/toolsets_admin.py +482 -0
  25. django_cfg/apps/agents/apps.py +29 -0
  26. django_cfg/apps/agents/core/__init__.py +20 -0
  27. django_cfg/apps/agents/core/agent.py +281 -0
  28. django_cfg/apps/agents/core/dependencies.py +154 -0
  29. django_cfg/apps/agents/core/exceptions.py +66 -0
  30. django_cfg/apps/agents/core/models.py +106 -0
  31. django_cfg/apps/agents/core/orchestrator.py +391 -0
  32. django_cfg/apps/agents/examples/__init__.py +3 -0
  33. django_cfg/apps/agents/examples/simple_example.py +161 -0
  34. django_cfg/apps/agents/integration/__init__.py +14 -0
  35. django_cfg/apps/agents/integration/middleware.py +80 -0
  36. django_cfg/apps/agents/integration/registry.py +345 -0
  37. django_cfg/apps/agents/integration/signals.py +50 -0
  38. django_cfg/apps/agents/management/__init__.py +3 -0
  39. django_cfg/apps/agents/management/commands/__init__.py +3 -0
  40. django_cfg/apps/agents/management/commands/create_agent.py +365 -0
  41. django_cfg/apps/agents/management/commands/orchestrator_status.py +191 -0
  42. django_cfg/apps/agents/managers/__init__.py +23 -0
  43. django_cfg/apps/agents/managers/execution.py +236 -0
  44. django_cfg/apps/agents/managers/registry.py +254 -0
  45. django_cfg/apps/agents/managers/toolsets.py +496 -0
  46. django_cfg/apps/agents/migrations/0001_initial.py +286 -0
  47. django_cfg/apps/agents/migrations/__init__.py +5 -0
  48. django_cfg/apps/agents/models/__init__.py +15 -0
  49. django_cfg/apps/agents/models/execution.py +215 -0
  50. django_cfg/apps/agents/models/registry.py +220 -0
  51. django_cfg/apps/agents/models/toolsets.py +305 -0
  52. django_cfg/apps/agents/patterns/__init__.py +24 -0
  53. django_cfg/apps/agents/patterns/content_agents.py +234 -0
  54. django_cfg/apps/agents/toolsets/__init__.py +15 -0
  55. django_cfg/apps/agents/toolsets/cache_toolset.py +285 -0
  56. django_cfg/apps/agents/toolsets/django_toolset.py +220 -0
  57. django_cfg/apps/agents/toolsets/file_toolset.py +324 -0
  58. django_cfg/apps/agents/toolsets/orm_toolset.py +319 -0
  59. django_cfg/apps/agents/urls.py +46 -0
  60. django_cfg/apps/knowbase/README.md +150 -0
  61. django_cfg/apps/knowbase/__init__.py +27 -0
  62. django_cfg/apps/knowbase/admin/__init__.py +23 -0
  63. django_cfg/apps/knowbase/admin/archive_admin.py +857 -0
  64. django_cfg/apps/knowbase/admin/chat_admin.py +386 -0
  65. django_cfg/apps/knowbase/admin/document_admin.py +650 -0
  66. django_cfg/apps/knowbase/admin/external_data_admin.py +685 -0
  67. django_cfg/apps/knowbase/apps.py +81 -0
  68. django_cfg/apps/knowbase/config/README.md +176 -0
  69. django_cfg/apps/knowbase/config/__init__.py +51 -0
  70. django_cfg/apps/knowbase/config/constance_fields.py +186 -0
  71. django_cfg/apps/knowbase/config/constance_settings.py +200 -0
  72. django_cfg/apps/knowbase/config/settings.py +444 -0
  73. django_cfg/apps/knowbase/examples/__init__.py +3 -0
  74. django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
  75. django_cfg/apps/knowbase/management/__init__.py +0 -0
  76. django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
  77. django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
  78. django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
  79. django_cfg/apps/knowbase/managers/__init__.py +22 -0
  80. django_cfg/apps/knowbase/managers/archive.py +426 -0
  81. django_cfg/apps/knowbase/managers/base.py +32 -0
  82. django_cfg/apps/knowbase/managers/chat.py +141 -0
  83. django_cfg/apps/knowbase/managers/document.py +203 -0
  84. django_cfg/apps/knowbase/managers/external_data.py +471 -0
  85. django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
  86. django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
  87. django_cfg/apps/knowbase/migrations/__init__.py +5 -0
  88. django_cfg/apps/knowbase/mixins/__init__.py +15 -0
  89. django_cfg/apps/knowbase/mixins/config.py +108 -0
  90. django_cfg/apps/knowbase/mixins/creator.py +81 -0
  91. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
  92. django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
  93. django_cfg/apps/knowbase/mixins/service.py +362 -0
  94. django_cfg/apps/knowbase/models/__init__.py +41 -0
  95. django_cfg/apps/knowbase/models/archive.py +599 -0
  96. django_cfg/apps/knowbase/models/base.py +58 -0
  97. django_cfg/apps/knowbase/models/chat.py +157 -0
  98. django_cfg/apps/knowbase/models/document.py +267 -0
  99. django_cfg/apps/knowbase/models/external_data.py +376 -0
  100. django_cfg/apps/knowbase/serializers/__init__.py +68 -0
  101. django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
  102. django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
  103. django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
  104. django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
  105. django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
  106. django_cfg/apps/knowbase/services/__init__.py +40 -0
  107. django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
  108. django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
  109. django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
  110. django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
  111. django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
  112. django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
  113. django_cfg/apps/knowbase/services/base.py +53 -0
  114. django_cfg/apps/knowbase/services/chat_service.py +239 -0
  115. django_cfg/apps/knowbase/services/document_service.py +144 -0
  116. django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
  117. django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
  118. django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
  119. django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
  120. django_cfg/apps/knowbase/services/embedding/models.py +229 -0
  121. django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
  122. django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
  123. django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
  124. django_cfg/apps/knowbase/services/search_service.py +293 -0
  125. django_cfg/apps/knowbase/signals/__init__.py +21 -0
  126. django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
  127. django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
  128. django_cfg/apps/knowbase/signals/document_signals.py +143 -0
  129. django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
  130. django_cfg/apps/knowbase/tasks/__init__.py +39 -0
  131. django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
  132. django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
  133. django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
  134. django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
  135. django_cfg/apps/knowbase/urls.py +43 -0
  136. django_cfg/apps/knowbase/utils/__init__.py +12 -0
  137. django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
  138. django_cfg/apps/knowbase/utils/text_processing.py +375 -0
  139. django_cfg/apps/knowbase/utils/validation.py +99 -0
  140. django_cfg/apps/knowbase/views/__init__.py +28 -0
  141. django_cfg/apps/knowbase/views/archive_views.py +469 -0
  142. django_cfg/apps/knowbase/views/base.py +49 -0
  143. django_cfg/apps/knowbase/views/chat_views.py +181 -0
  144. django_cfg/apps/knowbase/views/document_views.py +183 -0
  145. django_cfg/apps/knowbase/views/public_views.py +129 -0
  146. django_cfg/apps/leads/admin.py +70 -0
  147. django_cfg/apps/newsletter/admin.py +234 -0
  148. django_cfg/apps/newsletter/admin_filters.py +124 -0
  149. django_cfg/apps/support/admin.py +196 -0
  150. django_cfg/apps/support/admin_filters.py +71 -0
  151. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  152. django_cfg/apps/urls.py +5 -4
  153. django_cfg/cli/README.md +1 -1
  154. django_cfg/cli/commands/create_project.py +2 -2
  155. django_cfg/cli/commands/info.py +1 -1
  156. django_cfg/config.py +44 -0
  157. django_cfg/core/config.py +29 -82
  158. django_cfg/core/environment.py +1 -1
  159. django_cfg/core/generation.py +19 -107
  160. django_cfg/{integration.py → core/integration.py} +18 -16
  161. django_cfg/core/validation.py +1 -1
  162. django_cfg/management/__init__.py +1 -1
  163. django_cfg/management/commands/__init__.py +1 -1
  164. django_cfg/management/commands/auto_generate.py +482 -0
  165. django_cfg/management/commands/migrator.py +19 -101
  166. django_cfg/management/commands/test_email.py +1 -1
  167. django_cfg/middleware/README.md +0 -158
  168. django_cfg/middleware/__init__.py +0 -2
  169. django_cfg/middleware/user_activity.py +3 -3
  170. django_cfg/models/api.py +145 -0
  171. django_cfg/models/base.py +287 -0
  172. django_cfg/models/cache.py +4 -4
  173. django_cfg/models/constance.py +25 -88
  174. django_cfg/models/database.py +9 -9
  175. django_cfg/models/drf.py +3 -36
  176. django_cfg/models/email.py +163 -0
  177. django_cfg/models/environment.py +276 -0
  178. django_cfg/models/limits.py +1 -1
  179. django_cfg/models/logging.py +366 -0
  180. django_cfg/models/revolution.py +41 -2
  181. django_cfg/models/security.py +125 -0
  182. django_cfg/models/services.py +1 -1
  183. django_cfg/modules/__init__.py +2 -56
  184. django_cfg/modules/base.py +78 -52
  185. django_cfg/modules/django_currency/service.py +2 -2
  186. django_cfg/modules/django_email.py +2 -2
  187. django_cfg/modules/django_health.py +267 -0
  188. django_cfg/modules/django_llm/llm/client.py +79 -17
  189. django_cfg/modules/django_llm/translator/translator.py +2 -2
  190. django_cfg/modules/django_logger.py +2 -2
  191. django_cfg/modules/django_ngrok.py +2 -2
  192. django_cfg/modules/django_tasks.py +68 -3
  193. django_cfg/modules/django_telegram.py +3 -3
  194. django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
  195. django_cfg/modules/django_twilio/service.py +2 -2
  196. django_cfg/modules/django_twilio/simple_service.py +2 -2
  197. django_cfg/modules/django_twilio/templates/guide.md +266 -0
  198. django_cfg/modules/django_twilio/twilio_service.py +2 -2
  199. django_cfg/modules/django_unfold/__init__.py +69 -0
  200. django_cfg/modules/{unfold → django_unfold}/callbacks.py +23 -22
  201. django_cfg/modules/django_unfold/dashboard.py +278 -0
  202. django_cfg/modules/django_unfold/icons/README.md +145 -0
  203. django_cfg/modules/django_unfold/icons/__init__.py +12 -0
  204. django_cfg/modules/django_unfold/icons/constants.py +2851 -0
  205. django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
  206. django_cfg/modules/django_unfold/models/__init__.py +42 -0
  207. django_cfg/modules/django_unfold/models/config.py +601 -0
  208. django_cfg/modules/django_unfold/models/dashboard.py +206 -0
  209. django_cfg/modules/django_unfold/models/dropdown.py +40 -0
  210. django_cfg/modules/django_unfold/models/navigation.py +73 -0
  211. django_cfg/modules/django_unfold/models/tabs.py +25 -0
  212. django_cfg/modules/{unfold → django_unfold}/system_monitor.py +2 -2
  213. django_cfg/modules/django_unfold/utils.py +140 -0
  214. django_cfg/registry/__init__.py +23 -0
  215. django_cfg/registry/core.py +61 -0
  216. django_cfg/registry/exceptions.py +11 -0
  217. django_cfg/registry/modules.py +12 -0
  218. django_cfg/registry/services.py +26 -0
  219. django_cfg/registry/third_party.py +52 -0
  220. django_cfg/routing/__init__.py +19 -0
  221. django_cfg/routing/callbacks.py +198 -0
  222. django_cfg/routing/routers.py +48 -0
  223. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
  224. django_cfg/templatetags/__init__.py +0 -0
  225. django_cfg/templatetags/django_cfg.py +33 -0
  226. django_cfg/urls.py +33 -0
  227. django_cfg/utils/path_resolution.py +1 -1
  228. django_cfg/utils/smart_defaults.py +7 -61
  229. django_cfg/utils/toolkit.py +663 -0
  230. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/METADATA +83 -86
  231. django_cfg-1.2.0.dist-info/RECORD +441 -0
  232. django_cfg/apps/tasks/@docs/README.md +0 -195
  233. django_cfg/archive/django_sample.zip +0 -0
  234. django_cfg/models/unfold.py +0 -271
  235. django_cfg/modules/unfold/__init__.py +0 -29
  236. django_cfg/modules/unfold/dashboard.py +0 -318
  237. django_cfg/pyproject.toml +0 -370
  238. django_cfg/routers.py +0 -83
  239. django_cfg-1.1.81.dist-info/RECORD +0 -278
  240. /django_cfg/{exceptions.py → core/exceptions.py} +0 -0
  241. /django_cfg/modules/{unfold → django_unfold}/models.py +0 -0
  242. /django_cfg/modules/{unfold → django_unfold}/tailwind.py +0 -0
  243. /django_cfg/{version_check.py → utils/version_check.py} +0 -0
  244. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/WHEEL +0 -0
  245. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/entry_points.txt +0 -0
  246. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,234 @@
1
+ from django import forms
2
+ from django.contrib import admin, messages
3
+ from django.urls import reverse
4
+ from django.utils.html import format_html
5
+ from django.http import HttpResponseRedirect
6
+ from unfold.admin import ModelAdmin
7
+ from unfold.decorators import action
8
+ from unfold.contrib.forms.widgets import WysiwygWidget
9
+ from unfold.enums import ActionVariant
10
+ from .models import EmailLog, Newsletter, NewsletterSubscription, NewsletterCampaign
11
+ from .admin_filters import UserEmailFilter, UserNameFilter, HasUserFilter, EmailOpenedFilter, EmailClickedFilter
12
+
13
+
14
+ @admin.register(EmailLog)
15
+ class EmailLogAdmin(ModelAdmin):
16
+ list_display = ('user', 'recipient', 'subject', 'newsletter_link', 'status', 'created_at', 'sent_at', 'tracking_status')
17
+ list_filter = ('status', 'created_at', 'sent_at', 'newsletter', EmailOpenedFilter, EmailClickedFilter, HasUserFilter, UserEmailFilter, UserNameFilter)
18
+ autocomplete_fields = ('user',)
19
+ search_fields = (
20
+ 'recipient',
21
+ 'subject',
22
+ 'body',
23
+ 'error_message',
24
+ 'user__username',
25
+ 'user__email',
26
+ 'newsletter__subject'
27
+ )
28
+ readonly_fields = ('created_at', 'sent_at', 'newsletter')
29
+ raw_id_fields = ('user', 'newsletter')
30
+
31
+ def newsletter_link(self, obj):
32
+ if obj.newsletter:
33
+ link = reverse("admin:django_cfg_newsletter_newsletter_change", args=[obj.newsletter.id])
34
+ return format_html('<a href="{}">{}</a>', link, obj.newsletter.title)
35
+ return "-"
36
+ newsletter_link.short_description = 'Newsletter'
37
+
38
+ def tracking_status(self, obj):
39
+ """Show clean tracking status."""
40
+ opened_status = "Opened" if obj.is_opened else "Not opened"
41
+ clicked_status = "Clicked" if obj.is_clicked else "Not clicked"
42
+
43
+ opened_color = "#28a745" if obj.is_opened else "#dc3545"
44
+ clicked_color = "#007bff" if obj.is_clicked else "#6c757d"
45
+
46
+ return format_html(
47
+ '<div style="display: flex; gap: 8px;">'
48
+ '<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span>'
49
+ '<span style="background: {}; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">{}</span>'
50
+ '</div>',
51
+ opened_color, opened_status,
52
+ clicked_color, clicked_status
53
+ )
54
+ tracking_status.short_description = "Tracking Status"
55
+
56
+
57
+ @admin.register(Newsletter)
58
+ class NewsletterAdmin(ModelAdmin):
59
+ list_display = ('title', 'description', 'is_active', 'auto_subscribe', 'subscribers_count', 'created_at')
60
+ list_filter = ('is_active', 'auto_subscribe', 'created_at')
61
+ search_fields = ('title', 'description')
62
+ readonly_fields = ('subscribers_count', 'created_at', 'updated_at')
63
+
64
+
65
+ class NewsletterSubscriptionInline(admin.TabularInline):
66
+ model = NewsletterSubscription
67
+ fields = ('email', 'user', 'is_active', 'subscribed_at')
68
+ readonly_fields = ('subscribed_at',)
69
+ extra = 0
70
+
71
+
72
+ @admin.register(NewsletterSubscription)
73
+ class NewsletterSubscriptionAdmin(ModelAdmin):
74
+ list_display = ('email', 'newsletter', 'user', 'is_active', 'subscribed_at', 'unsubscribed_at')
75
+ list_filter = ('is_active', 'newsletter', 'subscribed_at')
76
+ search_fields = ('email', 'user__email', 'newsletter__title')
77
+ readonly_fields = ('subscribed_at', 'unsubscribed_at')
78
+ autocomplete_fields = ('user', 'newsletter')
79
+
80
+
81
+ # --- Form for NewsletterCampaignAdmin with Unfold Wysiwyg --- #
82
+ class NewsletterCampaignAdminForm(forms.ModelForm):
83
+ main_html_content = forms.CharField(widget=WysiwygWidget(), required=False)
84
+
85
+ class Meta:
86
+ model = NewsletterCampaign
87
+ fields = '__all__'
88
+
89
+
90
+ # --- Inline for Email Logs within Campaign Admin --- #
91
+ class EmailLogInline(admin.TabularInline):
92
+ model = EmailLog
93
+ fk_name = 'campaign' # Specify which ForeignKey to use
94
+ fields = ('user', 'recipient', 'status', 'sent_at')
95
+ readonly_fields = ('user', 'recipient', 'status', 'created_at', 'sent_at')
96
+ can_delete = False
97
+ extra = 0
98
+ show_change_link = True
99
+ verbose_name = "Sent Email Log"
100
+ verbose_name_plural = "Sent Email Logs"
101
+
102
+ def has_add_permission(self, request, obj=None):
103
+ return False
104
+
105
+
106
+ @admin.register(NewsletterCampaign)
107
+ class NewsletterCampaignAdmin(ModelAdmin):
108
+ form = NewsletterCampaignAdminForm
109
+ inlines = [EmailLogInline]
110
+ list_display = (
111
+ 'newsletter',
112
+ 'subject',
113
+ 'status',
114
+ 'created_at',
115
+ 'sent_at',
116
+ 'recipient_count',
117
+ )
118
+ list_filter = ('status', 'newsletter', 'created_at')
119
+ readonly_fields = ('status', 'created_at', 'sent_at', 'recipient_count')
120
+ search_fields = ('subject', 'email_title', 'main_text')
121
+ autocomplete_fields = ('newsletter',)
122
+
123
+ # Django admin actions
124
+ actions = ["send_selected_campaigns"]
125
+
126
+ # Unfold actions configuration
127
+ actions_list = [] # Changelist actions (removed send_selected_campaigns)
128
+ actions_detail = ["send_campaign"] # Detail page actions
129
+ actions_submit_line = ["send_and_continue"] # Form submit line actions
130
+
131
+ @action(
132
+ description="Send Campaign",
133
+ icon="send",
134
+ variant=ActionVariant.SUCCESS,
135
+ permissions=["change"]
136
+ )
137
+ def send_campaign(self, request, object_id):
138
+ """Send individual campaign from detail page."""
139
+ try:
140
+ campaign = self.get_object(request, object_id)
141
+ if not campaign:
142
+ self.message_user(request, "Campaign not found.", messages.ERROR)
143
+ return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
144
+
145
+ if campaign.status != NewsletterCampaign.CampaignStatus.DRAFT:
146
+ self.message_user(
147
+ request,
148
+ f"Campaign '{campaign.subject}' is not in draft status.",
149
+ messages.WARNING
150
+ )
151
+ return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
152
+
153
+ success = campaign.send_campaign()
154
+ if success:
155
+ self.message_user(
156
+ request,
157
+ f"Campaign '{campaign.subject}' sent successfully.",
158
+ messages.SUCCESS
159
+ )
160
+ else:
161
+ self.message_user(
162
+ request,
163
+ f"Campaign '{campaign.subject}' failed to send.",
164
+ messages.ERROR
165
+ )
166
+
167
+ except Exception as e:
168
+ self.message_user(request, f"An error occurred: {e}", messages.ERROR)
169
+
170
+ return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
171
+
172
+ def send_selected_campaigns(self, request, queryset):
173
+ """Send multiple campaigns (standard Django admin action)."""
174
+ sent_count = 0
175
+ skipped_count = 0
176
+
177
+ for campaign in queryset:
178
+ if campaign.status == NewsletterCampaign.CampaignStatus.DRAFT:
179
+ success = campaign.send_campaign()
180
+ if success:
181
+ sent_count += 1
182
+ else:
183
+ skipped_count += 1
184
+ else:
185
+ skipped_count += 1
186
+ messages.warning(
187
+ request,
188
+ f"Campaign '{campaign.subject}' skipped (not in Draft status)."
189
+ )
190
+
191
+ if sent_count > 0:
192
+ self.message_user(
193
+ request,
194
+ f"Successfully sent {sent_count} campaigns.",
195
+ messages.SUCCESS
196
+ )
197
+
198
+ if skipped_count > 0:
199
+ self.message_user(
200
+ request,
201
+ f"{skipped_count} campaigns were skipped.",
202
+ messages.WARNING
203
+ )
204
+
205
+ send_selected_campaigns.short_description = "Send selected campaigns"
206
+
207
+ @action(
208
+ description="Send & Continue Editing",
209
+ icon="send",
210
+ variant=ActionVariant.INFO,
211
+ permissions=["change"]
212
+ )
213
+ def send_and_continue(self, request, obj):
214
+ """Send campaign and continue editing (submit line action)."""
215
+ if obj.status == NewsletterCampaign.CampaignStatus.DRAFT:
216
+ success = obj.send_campaign()
217
+ if success:
218
+ self.message_user(
219
+ request,
220
+ f"Campaign '{obj.subject}' sent successfully.",
221
+ messages.SUCCESS
222
+ )
223
+ else:
224
+ self.message_user(
225
+ request,
226
+ f"Campaign '{obj.subject}' failed to send.",
227
+ messages.ERROR
228
+ )
229
+ else:
230
+ self.message_user(
231
+ request,
232
+ f"Campaign '{obj.subject}' is not in draft status.",
233
+ messages.WARNING
234
+ )
@@ -0,0 +1,124 @@
1
+ """
2
+ Custom admin filters for newsletter app.
3
+ """
4
+
5
+ from django.contrib import admin
6
+ from django.contrib.auth import get_user_model
7
+ from django.db import models
8
+ from django.utils.translation import gettext_lazy as _
9
+
10
+ User = get_user_model()
11
+
12
+
13
+ class UserEmailFilter(admin.SimpleListFilter):
14
+ """
15
+ Filter by user email using text input instead of dropdown.
16
+ More efficient for large user bases.
17
+ """
18
+ title = _('User Email')
19
+ parameter_name = 'user_email'
20
+
21
+ def lookups(self, request, model_admin):
22
+ """Return empty lookups to show text input."""
23
+ return ()
24
+
25
+ def queryset(self, request, queryset):
26
+ """Filter queryset based on user email input."""
27
+ if self.value():
28
+ return queryset.filter(
29
+ models.Q(user__email__icontains=self.value()) |
30
+ models.Q(recipient__icontains=self.value())
31
+ )
32
+ return queryset
33
+
34
+
35
+ class UserNameFilter(admin.SimpleListFilter):
36
+ """
37
+ Filter by username using text input instead of dropdown.
38
+ More efficient for large user bases.
39
+ """
40
+ title = _('Username')
41
+ parameter_name = 'username'
42
+
43
+ def lookups(self, request, model_admin):
44
+ """Return empty lookups to show text input."""
45
+ return ()
46
+
47
+ def queryset(self, request, queryset):
48
+ """Filter queryset based on username input."""
49
+ if self.value():
50
+ return queryset.filter(
51
+ models.Q(user__username__icontains=self.value()) |
52
+ models.Q(user__first_name__icontains=self.value()) |
53
+ models.Q(user__last_name__icontains=self.value())
54
+ )
55
+ return queryset
56
+
57
+
58
+ class HasUserFilter(admin.SimpleListFilter):
59
+ """
60
+ Simple filter to show emails with or without associated users.
61
+ """
62
+ title = _('Has User Account')
63
+ parameter_name = 'has_user'
64
+
65
+ def lookups(self, request, model_admin):
66
+ """Return filter options."""
67
+ return (
68
+ ('yes', _('Has User Account')),
69
+ ('no', _('No User Account')),
70
+ )
71
+
72
+ def queryset(self, request, queryset):
73
+ """Filter queryset based on user presence."""
74
+ if self.value() == 'yes':
75
+ return queryset.filter(user__isnull=False)
76
+ elif self.value() == 'no':
77
+ return queryset.filter(user__isnull=True)
78
+ return queryset
79
+
80
+
81
+ class EmailOpenedFilter(admin.SimpleListFilter):
82
+ """
83
+ Filter emails by opened status.
84
+ """
85
+ title = _('Email Opened')
86
+ parameter_name = 'email_opened'
87
+
88
+ def lookups(self, request, model_admin):
89
+ """Return filter options."""
90
+ return (
91
+ ('yes', _('Opened')),
92
+ ('no', _('Not Opened')),
93
+ )
94
+
95
+ def queryset(self, request, queryset):
96
+ """Filter queryset based on opened status."""
97
+ if self.value() == 'yes':
98
+ return queryset.filter(opened_at__isnull=False)
99
+ elif self.value() == 'no':
100
+ return queryset.filter(opened_at__isnull=True)
101
+ return queryset
102
+
103
+
104
+ class EmailClickedFilter(admin.SimpleListFilter):
105
+ """
106
+ Filter emails by clicked status.
107
+ """
108
+ title = _('Link Clicked')
109
+ parameter_name = 'link_clicked'
110
+
111
+ def lookups(self, request, model_admin):
112
+ """Return filter options."""
113
+ return (
114
+ ('yes', _('Clicked')),
115
+ ('no', _('Not Clicked')),
116
+ )
117
+
118
+ def queryset(self, request, queryset):
119
+ """Filter queryset based on clicked status."""
120
+ if self.value() == 'yes':
121
+ return queryset.filter(clicked_at__isnull=False)
122
+ elif self.value() == 'no':
123
+ return queryset.filter(clicked_at__isnull=True)
124
+ return queryset
@@ -0,0 +1,196 @@
1
+ from django.contrib import admin
2
+ from unfold.admin import ModelAdmin, TabularInline
3
+ from unfold.decorators import action
4
+ from django.utils.timesince import timesince
5
+ from django.utils.html import format_html
6
+ from django.urls import reverse
7
+ from django.shortcuts import redirect
8
+ from django.http import HttpRequest
9
+ from django.utils.translation import gettext_lazy as _
10
+ from .models import Ticket, Message
11
+ from .admin_filters import TicketUserEmailFilter, TicketUserNameFilter, MessageSenderEmailFilter
12
+ from django import forms
13
+
14
+ class MessageInline(TabularInline):
15
+ """Read-only inline for viewing messages. Use Chat interface for replies."""
16
+
17
+ model = Message
18
+ extra = 0
19
+ fields = ("sender_avatar", "created_at", "text")
20
+ readonly_fields = ("sender_avatar", "created_at", "text")
21
+ show_change_link = False
22
+ classes = ('collapse',)
23
+
24
+ def has_add_permission(self, request, obj=None):
25
+ """Disable adding messages through admin - use chat interface instead."""
26
+ return False
27
+
28
+ def has_delete_permission(self, request, obj=None):
29
+ """Disable deleting messages through admin."""
30
+ return False
31
+
32
+ def sender_avatar(self, obj):
33
+ """Display sender avatar with fallback to initials."""
34
+ if obj.sender.avatar:
35
+ return format_html(
36
+ '<img src="{}" style="width: 24px; height: 24px; border-radius: 50%; object-fit: cover;" />',
37
+ obj.sender.avatar.url
38
+ )
39
+ else:
40
+ initials = obj.sender.__class__.objects.get_initials(obj.sender)
41
+ bg_color = '#0d6efd' if obj.sender.is_staff else '#6f42c1' if obj.sender.is_superuser else '#198754'
42
+
43
+ return format_html(
44
+ '<div style="width: 24px; height: 24px; border-radius: 50%; background: {}; '
45
+ 'color: white; display: flex; align-items: center; justify-content: center; '
46
+ 'font-weight: bold; font-size: 10px;">{}</div>',
47
+ bg_color, initials
48
+ )
49
+ sender_avatar.short_description = "Sender"
50
+
51
+
52
+ @admin.register(Ticket)
53
+ class TicketAdmin(ModelAdmin):
54
+ list_display = ("user_avatar", "uuid_link", "subject", "status", "last_message_short", "last_message_ago", "chat_link", "created_at")
55
+ list_display_links = ("subject",)
56
+ list_editable = ("status",)
57
+ search_fields = ("uuid", "user__username", "user__email", "subject")
58
+ list_filter = ("status", "created_at", TicketUserEmailFilter, TicketUserNameFilter)
59
+ ordering = ("-created_at",)
60
+ inlines = [MessageInline]
61
+ def get_readonly_fields(self, request, obj=None):
62
+ """Different readonly fields for add/change forms."""
63
+ if obj is None: # Adding new ticket
64
+ return ("uuid", "created_at")
65
+ else: # Editing existing ticket
66
+ return ("uuid", "user", "created_at")
67
+ actions_detail = ["open_chat"]
68
+ autocomplete_fields = ["user"]
69
+ def get_fieldsets(self, request, obj=None):
70
+ """Different fieldsets for add/change forms."""
71
+ if obj is None: # Adding new ticket
72
+ return (
73
+ (None, {
74
+ "fields": ("user", "subject", "status")
75
+ }),
76
+ )
77
+ else: # Editing existing ticket
78
+ return (
79
+ (None, {
80
+ "fields": (("uuid", "user"), "subject", "status", "created_at")
81
+ }),
82
+ ("💬 Chat Interface", {
83
+ "description": "Use the beautiful Chat interface to reply to this ticket. Click the '💬 Chat' button above.",
84
+ "fields": (),
85
+ "classes": ("collapse",)
86
+ }),
87
+ )
88
+
89
+
90
+ def user_avatar(self, obj):
91
+ """Display user avatar with fallback to initials."""
92
+ if obj.user.avatar:
93
+ return format_html(
94
+ '<img src="{}" style="width: 32px; height: 32px; border-radius: 50%; object-fit: cover;" />',
95
+ obj.user.avatar.url
96
+ )
97
+ else:
98
+ initials = obj.user.__class__.objects.get_initials(obj.user)
99
+ bg_color = '#0d6efd' if obj.user.is_staff else '#6f42c1' if obj.user.is_superuser else '#198754'
100
+
101
+ return format_html(
102
+ '<div style="width: 32px; height: 32px; border-radius: 50%; background: {}; '
103
+ 'color: white; display: flex; align-items: center; justify-content: center; '
104
+ 'font-weight: bold; font-size: 12px;">{}</div>',
105
+ bg_color, initials
106
+ )
107
+ user_avatar.short_description = "User"
108
+
109
+ def uuid_link(self, obj):
110
+ """Make UUID clickable link to ticket detail."""
111
+ url = reverse('admin:django_cfg_support_ticket_change', args=[obj.uuid])
112
+ return format_html(
113
+ '<a href="{}" style="font-family: monospace; font-size: 11px; background: #f8f9fa; '
114
+ 'padding: 2px 4px; border-radius: 3px; text-decoration: none; color: #0d6efd;">{}</a>',
115
+ url, str(obj.uuid)[:8] + '...'
116
+ )
117
+ uuid_link.short_description = "ID"
118
+
119
+
120
+ def last_message_short(self, obj):
121
+ msg = obj.last_message
122
+ if msg:
123
+ return (msg.text[:40] + '...') if len(msg.text) > 40 else msg.text
124
+ return "-"
125
+ last_message_short.short_description = "Last Message"
126
+
127
+ def last_message_ago(self, obj):
128
+ msg = obj.last_message
129
+ if msg:
130
+ return timesince(msg.created_at) + ' ago'
131
+ return "-"
132
+ last_message_ago.short_description = "Last Reply"
133
+
134
+ def chat_link(self, obj):
135
+ """Display chat link button in list view."""
136
+ chat_url = reverse('ticket-chat', kwargs={'ticket_uuid': obj.uuid})
137
+ return format_html(
138
+ '<a href="{}" target="_blank" class="btn btn-sm btn-primary" '
139
+ 'style="background: #0d6efd; color: white; padding: 4px 8px; '
140
+ 'border-radius: 4px; text-decoration: none; font-size: 11px; '
141
+ 'display: inline-flex; align-items: center; gap: 4px;">'
142
+ '<svg width="12" height="12" fill="currentColor" viewBox="0 0 16 16">'
143
+ '<path d="M2.678 11.894a1 1 0 0 1 .287.801 10.97 10.97 0 0 1-.398 2c1.395-.323 2.247-.697 2.634-.893a1 1 0 0 1 .71-.074A8.06 8.06 0 0 0 8 14c3.996 0 7-2.807 7-6 0-3.192-3.004-6-7-6S1 4.808 1 8c0 1.468.617 2.83 1.678 3.894zm-.493 3.905a21.682 21.682 0 0 1-.713.129c-.2.032-.352-.176-.273-.362a9.68 9.68 0 0 0 .244-.637l.003-.01c.248-.72.45-1.548.524-2.319C.743 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.52.263-1.639.742-3.468 1.105z"/>'
144
+ '</svg>Chat</a>',
145
+ chat_url
146
+ )
147
+ chat_link.short_description = "💬"
148
+
149
+ @action(
150
+ description=_("💬 Open Chat Interface"),
151
+ url_path="chat",
152
+ attrs={"target": "_blank", "class": "btn btn-primary"},
153
+ )
154
+ def open_chat(self, request: HttpRequest, object_id: int):
155
+ """Open the beautiful chat interface for this ticket."""
156
+ ticket = Ticket.objects.get(pk=object_id)
157
+ chat_url = reverse('ticket-chat', kwargs={'ticket_uuid': ticket.uuid})
158
+ return redirect(chat_url)
159
+
160
+ @admin.register(Message)
161
+ class MessageAdmin(ModelAdmin):
162
+ list_display = ("sender_avatar", "uuid", "ticket", "text_short", "created_at")
163
+ list_display_links = ("uuid", "ticket")
164
+ search_fields = ("uuid", "ticket__subject", "sender__username", "sender__email", "text")
165
+ list_filter = ("created_at", MessageSenderEmailFilter)
166
+ ordering = ("-created_at",)
167
+ readonly_fields = ("uuid", "ticket", "sender", "created_at")
168
+ fieldsets = (
169
+ (None, {
170
+ "fields": (("uuid", "ticket"), "sender", "text", "created_at")
171
+ }),
172
+ )
173
+
174
+ def sender_avatar(self, obj):
175
+ """Display sender avatar with fallback to initials."""
176
+ if obj.sender.avatar:
177
+ return format_html(
178
+ '<img src="{}" style="width: 32px; height: 32px; border-radius: 50%; object-fit: cover;" />',
179
+ obj.sender.avatar.url
180
+ )
181
+ else:
182
+ initials = obj.sender.__class__.objects.get_initials(obj.sender)
183
+ bg_color = '#0d6efd' if obj.sender.is_staff else '#6f42c1' if obj.sender.is_superuser else '#198754'
184
+
185
+ return format_html(
186
+ '<div style="width: 32px; height: 32px; border-radius: 50%; background: {}; '
187
+ 'color: white; display: flex; align-items: center; justify-content: center; '
188
+ 'font-weight: bold; font-size: 12px;">{}</div>',
189
+ bg_color, initials
190
+ )
191
+ sender_avatar.short_description = "Sender"
192
+
193
+ def text_short(self, obj):
194
+ """Show shortened message text."""
195
+ return (obj.text[:50] + '...') if len(obj.text) > 50 else obj.text
196
+ text_short.short_description = "Message"
@@ -0,0 +1,71 @@
1
+ """
2
+ Custom admin filters for support app.
3
+ """
4
+
5
+ from django.contrib import admin
6
+ from django.contrib.auth import get_user_model
7
+ from django.db import models
8
+ from django.utils.translation import gettext_lazy as _
9
+
10
+ User = get_user_model()
11
+
12
+
13
+ class TicketUserEmailFilter(admin.SimpleListFilter):
14
+ """
15
+ Filter tickets by user email using text input instead of dropdown.
16
+ More efficient for large user bases.
17
+ """
18
+ title = _('User Email')
19
+ parameter_name = 'user_email'
20
+
21
+ def lookups(self, request, model_admin):
22
+ """Return empty lookups to show text input."""
23
+ return ()
24
+
25
+ def queryset(self, request, queryset):
26
+ """Filter queryset based on user email input."""
27
+ if self.value():
28
+ return queryset.filter(user__email__icontains=self.value())
29
+ return queryset
30
+
31
+
32
+ class TicketUserNameFilter(admin.SimpleListFilter):
33
+ """
34
+ Filter tickets by username using text input instead of dropdown.
35
+ More efficient for large user bases.
36
+ """
37
+ title = _('Username')
38
+ parameter_name = 'username'
39
+
40
+ def lookups(self, request, model_admin):
41
+ """Return empty lookups to show text input."""
42
+ return ()
43
+
44
+ def queryset(self, request, queryset):
45
+ """Filter queryset based on username input."""
46
+ if self.value():
47
+ return queryset.filter(
48
+ models.Q(user__username__icontains=self.value()) |
49
+ models.Q(user__first_name__icontains=self.value()) |
50
+ models.Q(user__last_name__icontains=self.value())
51
+ )
52
+ return queryset
53
+
54
+
55
+ class MessageSenderEmailFilter(admin.SimpleListFilter):
56
+ """
57
+ Filter messages by sender email using text input instead of dropdown.
58
+ More efficient for large user bases.
59
+ """
60
+ title = _('Sender Email')
61
+ parameter_name = 'sender_email'
62
+
63
+ def lookups(self, request, model_admin):
64
+ """Return empty lookups to show text input."""
65
+ return ()
66
+
67
+ def queryset(self, request, queryset):
68
+ """Filter queryset based on sender email input."""
69
+ if self.value():
70
+ return queryset.filter(sender__email__icontains=self.value())
71
+ return queryset
@@ -197,7 +197,7 @@
197
197
  submitBtn.disabled = true;
198
198
 
199
199
  try {
200
- const response = await fetch(`{% url 'cfg_support:send-message-ajax' ticket_uuid=ticket.uuid %}`, {
200
+ const response = await fetch(`{% url 'send-message-ajax' ticket_uuid=ticket.uuid %}`, {
201
201
  method: 'POST',
202
202
  headers: {
203
203
  'Content-Type': 'application/json',
django_cfg/apps/urls.py CHANGED
@@ -16,7 +16,7 @@ def get_django_cfg_urlpatterns() -> List[URLPattern]:
16
16
  Returns:
17
17
  List of URL patterns for enabled django_cfg applications
18
18
  """
19
- from django_cfg.modules.base import BaseModule
19
+ from django_cfg.modules.base import BaseCfgModule
20
20
 
21
21
  patterns = [
22
22
  # Core APIs (always enabled)
@@ -26,9 +26,10 @@ def get_django_cfg_urlpatterns() -> List[URLPattern]:
26
26
 
27
27
  try:
28
28
  # Use BaseModule to check enabled applications
29
- base_module = BaseModule()
29
+ base_module = BaseCfgModule()
30
30
 
31
- # Support URLs - needed for admin interface chat links
31
+ # All business logic apps are handled by Django Revolution zones
32
+ # to maintain consistency and enable client generation
32
33
  # if base_module.is_support_enabled():
33
34
  # patterns.append(path('support/', include('django_cfg.apps.support.urls')))
34
35
  #
@@ -43,7 +44,7 @@ def get_django_cfg_urlpatterns() -> List[URLPattern]:
43
44
  # if base_module.is_leads_enabled():
44
45
  # patterns.append(path('leads/', include('django_cfg.apps.leads.urls')))
45
46
 
46
- # Tasks (Dramatiq) management URLs - Web interface for task management
47
+ # Tasks app - enabled when knowbase or agents are enabled
47
48
  if base_module.is_tasks_enabled():
48
49
  patterns.append(path('tasks/', include('django_cfg.apps.tasks.urls')))
49
50
 
django_cfg/cli/README.md CHANGED
@@ -505,7 +505,7 @@ print(f"Django installed: {deps['django']}")
505
505
 
506
506
  ## 📚 Documentation
507
507
 
508
- - **Django CFG**: https://django-cfg.unrealos.com
508
+ - **Django CFG**: https://djangocfg.com
509
509
  - **GitHub**: https://github.com/unrealos/django-cfg
510
510
  - **PyPI**: https://pypi.org/project/django-cfg/
511
511
  - **Examples**: https://github.com/unrealos/django-cfg/tree/main/examples