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.
- django_cfg/__init__.py +20 -448
- django_cfg/apps/accounts/README.md +3 -3
- django_cfg/apps/accounts/admin/__init__.py +0 -2
- django_cfg/apps/accounts/admin/activity.py +2 -9
- django_cfg/apps/accounts/admin/filters.py +0 -42
- django_cfg/apps/accounts/admin/inlines.py +8 -8
- django_cfg/apps/accounts/admin/otp.py +5 -5
- django_cfg/apps/accounts/admin/registration_source.py +1 -8
- django_cfg/apps/accounts/admin/user.py +12 -20
- django_cfg/apps/accounts/managers/user_manager.py +2 -129
- django_cfg/apps/accounts/migrations/0006_remove_twilioresponse_otp_secret_and_more.py +46 -0
- django_cfg/apps/accounts/models.py +3 -123
- django_cfg/apps/accounts/serializers/otp.py +40 -44
- django_cfg/apps/accounts/serializers/profile.py +0 -2
- django_cfg/apps/accounts/services/otp_service.py +98 -186
- django_cfg/apps/accounts/signals.py +25 -15
- django_cfg/apps/accounts/utils/auth_email_service.py +84 -0
- django_cfg/apps/accounts/views/otp.py +35 -36
- django_cfg/apps/agents/README.md +129 -0
- django_cfg/apps/agents/__init__.py +68 -0
- django_cfg/apps/agents/admin/__init__.py +17 -0
- django_cfg/apps/agents/admin/execution_admin.py +460 -0
- django_cfg/apps/agents/admin/registry_admin.py +360 -0
- django_cfg/apps/agents/admin/toolsets_admin.py +482 -0
- django_cfg/apps/agents/apps.py +29 -0
- django_cfg/apps/agents/core/__init__.py +20 -0
- django_cfg/apps/agents/core/agent.py +281 -0
- django_cfg/apps/agents/core/dependencies.py +154 -0
- django_cfg/apps/agents/core/exceptions.py +66 -0
- django_cfg/apps/agents/core/models.py +106 -0
- django_cfg/apps/agents/core/orchestrator.py +391 -0
- django_cfg/apps/agents/examples/__init__.py +3 -0
- django_cfg/apps/agents/examples/simple_example.py +161 -0
- django_cfg/apps/agents/integration/__init__.py +14 -0
- django_cfg/apps/agents/integration/middleware.py +80 -0
- django_cfg/apps/agents/integration/registry.py +345 -0
- django_cfg/apps/agents/integration/signals.py +50 -0
- django_cfg/apps/agents/management/__init__.py +3 -0
- django_cfg/apps/agents/management/commands/__init__.py +3 -0
- django_cfg/apps/agents/management/commands/create_agent.py +365 -0
- django_cfg/apps/agents/management/commands/orchestrator_status.py +191 -0
- django_cfg/apps/agents/managers/__init__.py +23 -0
- django_cfg/apps/agents/managers/execution.py +236 -0
- django_cfg/apps/agents/managers/registry.py +254 -0
- django_cfg/apps/agents/managers/toolsets.py +496 -0
- django_cfg/apps/agents/migrations/0001_initial.py +286 -0
- django_cfg/apps/agents/migrations/__init__.py +5 -0
- django_cfg/apps/agents/models/__init__.py +15 -0
- django_cfg/apps/agents/models/execution.py +215 -0
- django_cfg/apps/agents/models/registry.py +220 -0
- django_cfg/apps/agents/models/toolsets.py +305 -0
- django_cfg/apps/agents/patterns/__init__.py +24 -0
- django_cfg/apps/agents/patterns/content_agents.py +234 -0
- django_cfg/apps/agents/toolsets/__init__.py +15 -0
- django_cfg/apps/agents/toolsets/cache_toolset.py +285 -0
- django_cfg/apps/agents/toolsets/django_toolset.py +220 -0
- django_cfg/apps/agents/toolsets/file_toolset.py +324 -0
- django_cfg/apps/agents/toolsets/orm_toolset.py +319 -0
- django_cfg/apps/agents/urls.py +46 -0
- django_cfg/apps/knowbase/README.md +150 -0
- django_cfg/apps/knowbase/__init__.py +27 -0
- django_cfg/apps/knowbase/admin/__init__.py +23 -0
- django_cfg/apps/knowbase/admin/archive_admin.py +857 -0
- django_cfg/apps/knowbase/admin/chat_admin.py +386 -0
- django_cfg/apps/knowbase/admin/document_admin.py +650 -0
- django_cfg/apps/knowbase/admin/external_data_admin.py +685 -0
- django_cfg/apps/knowbase/apps.py +81 -0
- django_cfg/apps/knowbase/config/README.md +176 -0
- django_cfg/apps/knowbase/config/__init__.py +51 -0
- django_cfg/apps/knowbase/config/constance_fields.py +186 -0
- django_cfg/apps/knowbase/config/constance_settings.py +200 -0
- django_cfg/apps/knowbase/config/settings.py +444 -0
- django_cfg/apps/knowbase/examples/__init__.py +3 -0
- django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
- django_cfg/apps/knowbase/management/__init__.py +0 -0
- django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
- django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
- django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
- django_cfg/apps/knowbase/managers/__init__.py +22 -0
- django_cfg/apps/knowbase/managers/archive.py +426 -0
- django_cfg/apps/knowbase/managers/base.py +32 -0
- django_cfg/apps/knowbase/managers/chat.py +141 -0
- django_cfg/apps/knowbase/managers/document.py +203 -0
- django_cfg/apps/knowbase/managers/external_data.py +471 -0
- django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
- django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
- django_cfg/apps/knowbase/migrations/__init__.py +5 -0
- django_cfg/apps/knowbase/mixins/__init__.py +15 -0
- django_cfg/apps/knowbase/mixins/config.py +108 -0
- django_cfg/apps/knowbase/mixins/creator.py +81 -0
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
- django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
- django_cfg/apps/knowbase/mixins/service.py +362 -0
- django_cfg/apps/knowbase/models/__init__.py +41 -0
- django_cfg/apps/knowbase/models/archive.py +599 -0
- django_cfg/apps/knowbase/models/base.py +58 -0
- django_cfg/apps/knowbase/models/chat.py +157 -0
- django_cfg/apps/knowbase/models/document.py +267 -0
- django_cfg/apps/knowbase/models/external_data.py +376 -0
- django_cfg/apps/knowbase/serializers/__init__.py +68 -0
- django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
- django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
- django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
- django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
- django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
- django_cfg/apps/knowbase/services/__init__.py +40 -0
- django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
- django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
- django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
- django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
- django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
- django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
- django_cfg/apps/knowbase/services/base.py +53 -0
- django_cfg/apps/knowbase/services/chat_service.py +239 -0
- django_cfg/apps/knowbase/services/document_service.py +144 -0
- django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
- django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
- django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
- django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
- django_cfg/apps/knowbase/services/embedding/models.py +229 -0
- django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
- django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
- django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
- django_cfg/apps/knowbase/services/search_service.py +293 -0
- django_cfg/apps/knowbase/signals/__init__.py +21 -0
- django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
- django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
- django_cfg/apps/knowbase/signals/document_signals.py +143 -0
- django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
- django_cfg/apps/knowbase/tasks/__init__.py +39 -0
- django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
- django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
- django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
- django_cfg/apps/knowbase/urls.py +43 -0
- django_cfg/apps/knowbase/utils/__init__.py +12 -0
- django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
- django_cfg/apps/knowbase/utils/text_processing.py +375 -0
- django_cfg/apps/knowbase/utils/validation.py +99 -0
- django_cfg/apps/knowbase/views/__init__.py +28 -0
- django_cfg/apps/knowbase/views/archive_views.py +469 -0
- django_cfg/apps/knowbase/views/base.py +49 -0
- django_cfg/apps/knowbase/views/chat_views.py +181 -0
- django_cfg/apps/knowbase/views/document_views.py +183 -0
- django_cfg/apps/knowbase/views/public_views.py +129 -0
- django_cfg/apps/leads/admin.py +70 -0
- django_cfg/apps/newsletter/admin.py +234 -0
- django_cfg/apps/newsletter/admin_filters.py +124 -0
- django_cfg/apps/support/admin.py +196 -0
- django_cfg/apps/support/admin_filters.py +71 -0
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/apps/urls.py +5 -4
- django_cfg/cli/README.md +1 -1
- django_cfg/cli/commands/create_project.py +2 -2
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/config.py +44 -0
- django_cfg/core/config.py +29 -82
- django_cfg/core/environment.py +1 -1
- django_cfg/core/generation.py +19 -107
- django_cfg/{integration.py → core/integration.py} +18 -16
- django_cfg/core/validation.py +1 -1
- django_cfg/management/__init__.py +1 -1
- django_cfg/management/commands/__init__.py +1 -1
- django_cfg/management/commands/auto_generate.py +482 -0
- django_cfg/management/commands/migrator.py +19 -101
- django_cfg/management/commands/test_email.py +1 -1
- django_cfg/middleware/README.md +0 -158
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/api.py +145 -0
- django_cfg/models/base.py +287 -0
- django_cfg/models/cache.py +4 -4
- django_cfg/models/constance.py +25 -88
- django_cfg/models/database.py +9 -9
- django_cfg/models/drf.py +3 -36
- django_cfg/models/email.py +163 -0
- django_cfg/models/environment.py +276 -0
- django_cfg/models/limits.py +1 -1
- django_cfg/models/logging.py +366 -0
- django_cfg/models/revolution.py +41 -2
- django_cfg/models/security.py +125 -0
- django_cfg/models/services.py +1 -1
- django_cfg/modules/__init__.py +2 -56
- django_cfg/modules/base.py +78 -52
- django_cfg/modules/django_currency/service.py +2 -2
- django_cfg/modules/django_email.py +2 -2
- django_cfg/modules/django_health.py +267 -0
- django_cfg/modules/django_llm/llm/client.py +79 -17
- django_cfg/modules/django_llm/translator/translator.py +2 -2
- django_cfg/modules/django_logger.py +2 -2
- django_cfg/modules/django_ngrok.py +2 -2
- django_cfg/modules/django_tasks.py +68 -3
- django_cfg/modules/django_telegram.py +3 -3
- django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
- django_cfg/modules/django_twilio/service.py +2 -2
- django_cfg/modules/django_twilio/simple_service.py +2 -2
- django_cfg/modules/django_twilio/templates/guide.md +266 -0
- django_cfg/modules/django_twilio/twilio_service.py +2 -2
- django_cfg/modules/django_unfold/__init__.py +69 -0
- django_cfg/modules/{unfold → django_unfold}/callbacks.py +23 -22
- django_cfg/modules/django_unfold/dashboard.py +278 -0
- django_cfg/modules/django_unfold/icons/README.md +145 -0
- django_cfg/modules/django_unfold/icons/__init__.py +12 -0
- django_cfg/modules/django_unfold/icons/constants.py +2851 -0
- django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
- django_cfg/modules/django_unfold/models/__init__.py +42 -0
- django_cfg/modules/django_unfold/models/config.py +601 -0
- django_cfg/modules/django_unfold/models/dashboard.py +206 -0
- django_cfg/modules/django_unfold/models/dropdown.py +40 -0
- django_cfg/modules/django_unfold/models/navigation.py +73 -0
- django_cfg/modules/django_unfold/models/tabs.py +25 -0
- django_cfg/modules/{unfold → django_unfold}/system_monitor.py +2 -2
- django_cfg/modules/django_unfold/utils.py +140 -0
- django_cfg/registry/__init__.py +23 -0
- django_cfg/registry/core.py +61 -0
- django_cfg/registry/exceptions.py +11 -0
- django_cfg/registry/modules.py +12 -0
- django_cfg/registry/services.py +26 -0
- django_cfg/registry/third_party.py +52 -0
- django_cfg/routing/__init__.py +19 -0
- django_cfg/routing/callbacks.py +198 -0
- django_cfg/routing/routers.py +48 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
- django_cfg/templatetags/__init__.py +0 -0
- django_cfg/templatetags/django_cfg.py +33 -0
- django_cfg/urls.py +33 -0
- django_cfg/utils/path_resolution.py +1 -1
- django_cfg/utils/smart_defaults.py +7 -61
- django_cfg/utils/toolkit.py +663 -0
- {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/METADATA +83 -86
- django_cfg-1.2.0.dist-info/RECORD +441 -0
- django_cfg/apps/tasks/@docs/README.md +0 -195
- django_cfg/archive/django_sample.zip +0 -0
- django_cfg/models/unfold.py +0 -271
- django_cfg/modules/unfold/__init__.py +0 -29
- django_cfg/modules/unfold/dashboard.py +0 -318
- django_cfg/pyproject.toml +0 -370
- django_cfg/routers.py +0 -83
- django_cfg-1.1.81.dist-info/RECORD +0 -278
- /django_cfg/{exceptions.py → core/exceptions.py} +0 -0
- /django_cfg/modules/{unfold → django_unfold}/models.py +0 -0
- /django_cfg/modules/{unfold → django_unfold}/tailwind.py +0 -0
- /django_cfg/{version_check.py → utils/version_check.py} +0 -0
- {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/entry_points.txt +0 -0
- {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 '
|
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
|
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 =
|
29
|
+
base_module = BaseCfgModule()
|
30
30
|
|
31
|
-
#
|
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
|
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://
|
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
|