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
@@ -5,21 +5,14 @@ Registration Source admin configuration.
|
|
5
5
|
from django.contrib import admin
|
6
6
|
from django.contrib.humanize.templatetags.humanize import naturaltime
|
7
7
|
from unfold.admin import ModelAdmin
|
8
|
-
from import_export.admin import ImportExportModelAdmin
|
9
|
-
from unfold.contrib.import_export.forms import ImportForm, ExportForm
|
10
8
|
|
11
9
|
from ..models import RegistrationSource, UserRegistrationSource
|
12
10
|
from .filters import RegistrationSourceStatusFilter
|
13
11
|
from .inlines import RegistrationSourceInline
|
14
|
-
from .resources import RegistrationSourceResource
|
15
12
|
|
16
13
|
|
17
14
|
@admin.register(RegistrationSource)
|
18
|
-
class RegistrationSourceAdmin(ModelAdmin
|
19
|
-
# Import/Export configuration
|
20
|
-
resource_class = RegistrationSourceResource
|
21
|
-
import_form_class = ImportForm
|
22
|
-
export_form_class = ExportForm
|
15
|
+
class RegistrationSourceAdmin(ModelAdmin):
|
23
16
|
list_display = ["name", "url", "status", "users_count", "created"]
|
24
17
|
list_display_links = ["name", "url"]
|
25
18
|
list_filter = [RegistrationSourceStatusFilter, "is_active", "created_at"]
|
@@ -12,26 +12,18 @@ from unfold.admin import ModelAdmin
|
|
12
12
|
from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
|
13
13
|
from unfold.decorators import action
|
14
14
|
from unfold.enums import ActionVariant
|
15
|
-
from import_export.admin import ImportExportModelAdmin
|
16
|
-
from unfold.contrib.import_export.forms import ImportForm, ExportForm
|
17
15
|
|
18
16
|
from ..models import CustomUser
|
19
17
|
from .filters import UserStatusFilter
|
20
18
|
from .inlines import UserRegistrationSourceInline, UserActivityInline, UserEmailLogInline, UserSupportTicketsInline
|
21
|
-
from .resources import CustomUserResource
|
22
19
|
|
23
20
|
|
24
21
|
@admin.register(CustomUser)
|
25
|
-
class CustomUserAdmin(BaseUserAdmin, ModelAdmin
|
22
|
+
class CustomUserAdmin(BaseUserAdmin, ModelAdmin):
|
26
23
|
# Forms loaded from `unfold.forms`
|
27
24
|
form = UserChangeForm
|
28
25
|
add_form = UserCreationForm
|
29
26
|
change_password_form = AdminPasswordChangeForm
|
30
|
-
|
31
|
-
# Import/Export configuration
|
32
|
-
resource_class = CustomUserResource
|
33
|
-
import_form_class = ImportForm
|
34
|
-
export_form_class = ExportForm
|
35
27
|
|
36
28
|
list_display = [
|
37
29
|
"avatar",
|
@@ -56,8 +48,8 @@ class CustomUserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin):
|
|
56
48
|
|
57
49
|
# Add email log inline if newsletter app is enabled
|
58
50
|
try:
|
59
|
-
from django_cfg.modules.base import
|
60
|
-
base_module =
|
51
|
+
from django_cfg.modules.base import BaseCfgModule
|
52
|
+
base_module = BaseCfgModule()
|
61
53
|
if base_module.is_newsletter_enabled():
|
62
54
|
inlines.append(UserEmailLogInline)
|
63
55
|
if base_module.is_support_enabled():
|
@@ -80,7 +72,7 @@ class CustomUserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin):
|
|
80
72
|
(
|
81
73
|
"Contact Information",
|
82
74
|
{
|
83
|
-
"fields": ("company", "phone", "
|
75
|
+
"fields": ("company", "phone", "position"),
|
84
76
|
},
|
85
77
|
),
|
86
78
|
(
|
@@ -159,8 +151,8 @@ class CustomUserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin):
|
|
159
151
|
def emails_count(self, obj):
|
160
152
|
"""Show count of emails sent to user (if newsletter app is enabled)."""
|
161
153
|
try:
|
162
|
-
from django_cfg.modules.base import
|
163
|
-
base_module =
|
154
|
+
from django_cfg.modules.base import BaseCfgModule
|
155
|
+
base_module = BaseCfgModule()
|
164
156
|
|
165
157
|
if not base_module.is_newsletter_enabled():
|
166
158
|
return "—"
|
@@ -178,8 +170,8 @@ class CustomUserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin):
|
|
178
170
|
def tickets_count(self, obj):
|
179
171
|
"""Show count of support tickets for user (if support app is enabled)."""
|
180
172
|
try:
|
181
|
-
from django_cfg.modules.base import
|
182
|
-
base_module =
|
173
|
+
from django_cfg.modules.base import BaseCfgModule
|
174
|
+
base_module = BaseCfgModule()
|
183
175
|
|
184
176
|
if not base_module.is_support_enabled():
|
185
177
|
return "—"
|
@@ -241,8 +233,8 @@ class CustomUserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin):
|
|
241
233
|
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
242
234
|
|
243
235
|
# Check if newsletter app is enabled
|
244
|
-
from django_cfg.modules.base import
|
245
|
-
base_module =
|
236
|
+
from django_cfg.modules.base import BaseCfgModule
|
237
|
+
base_module = BaseCfgModule()
|
246
238
|
|
247
239
|
if not base_module.is_newsletter_enabled():
|
248
240
|
self.message_user(
|
@@ -279,8 +271,8 @@ class CustomUserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin):
|
|
279
271
|
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
280
272
|
|
281
273
|
# Check if support app is enabled
|
282
|
-
from django_cfg.modules.base import
|
283
|
-
base_module =
|
274
|
+
from django_cfg.modules.base import BaseCfgModule
|
275
|
+
base_module = BaseCfgModule()
|
284
276
|
|
285
277
|
if not base_module.is_support_enabled():
|
286
278
|
self.message_user(
|
@@ -83,8 +83,8 @@ class UserManager(UserManager):
|
|
83
83
|
f"Attempting to get_or_create user with email: {email}, username: {username}"
|
84
84
|
)
|
85
85
|
|
86
|
-
# Create or get user using self
|
87
|
-
user, created = self.get_or_create(
|
86
|
+
# Create or get user using self.model instead of importing CustomUser
|
87
|
+
user, created = self.model.objects.get_or_create(
|
88
88
|
email=email, defaults=defaults
|
89
89
|
)
|
90
90
|
|
@@ -287,130 +287,3 @@ class UserManager(UserManager):
|
|
287
287
|
elif user.last_name:
|
288
288
|
return user.last_name
|
289
289
|
return user.email.split("@")[0].capitalize()
|
290
|
-
|
291
|
-
# === NOTIFICATION-AWARE USER OPERATIONS ===
|
292
|
-
|
293
|
-
def create_user_with_notifications(self, email, password=None, send_notifications=True, **extra_fields):
|
294
|
-
"""
|
295
|
-
Create user with optional notifications.
|
296
|
-
|
297
|
-
Args:
|
298
|
-
email: User's email address
|
299
|
-
password: User's password
|
300
|
-
send_notifications: Whether to send email/telegram notifications
|
301
|
-
**extra_fields: Additional fields for user creation
|
302
|
-
|
303
|
-
Returns:
|
304
|
-
User: Created user instance
|
305
|
-
"""
|
306
|
-
# Create user normally
|
307
|
-
user = self.create_user(email, password, **extra_fields)
|
308
|
-
|
309
|
-
# Send notifications if requested
|
310
|
-
if send_notifications:
|
311
|
-
from ..utils.notifications import AccountNotifications
|
312
|
-
AccountNotifications.send_welcome_email(user)
|
313
|
-
|
314
|
-
return user
|
315
|
-
|
316
|
-
def register_user_with_notifications(self, email, username=None, source_url=None, send_notifications=True, **extra_fields):
|
317
|
-
"""
|
318
|
-
Register user with optional notifications (extends existing register_user method).
|
319
|
-
|
320
|
-
Args:
|
321
|
-
email: User's email address
|
322
|
-
username: Optional username
|
323
|
-
source_url: Optional source URL for tracking
|
324
|
-
send_notifications: Whether to send notifications
|
325
|
-
**extra_fields: Additional fields for user creation
|
326
|
-
|
327
|
-
Returns:
|
328
|
-
tuple: (user, created) where created is True if user was created
|
329
|
-
"""
|
330
|
-
# Use existing register_user method
|
331
|
-
user, created = self.register_user(email, username, source_url, **extra_fields)
|
332
|
-
|
333
|
-
# Send notifications only for new users if requested
|
334
|
-
if created and send_notifications:
|
335
|
-
from ..utils.notifications import AccountNotifications
|
336
|
-
AccountNotifications.send_welcome_email(user)
|
337
|
-
|
338
|
-
return user, created
|
339
|
-
|
340
|
-
def update_user_profile(self, user, send_notifications=True, **update_fields):
|
341
|
-
"""
|
342
|
-
Update user profile with optional notifications.
|
343
|
-
|
344
|
-
Args:
|
345
|
-
user: User instance to update
|
346
|
-
send_notifications: Whether to send notifications for important changes
|
347
|
-
**update_fields: Fields to update
|
348
|
-
|
349
|
-
Returns:
|
350
|
-
User: Updated user instance
|
351
|
-
"""
|
352
|
-
if not update_fields:
|
353
|
-
return user
|
354
|
-
|
355
|
-
# Track important changes for notifications
|
356
|
-
important_changes = []
|
357
|
-
old_values = {}
|
358
|
-
|
359
|
-
if send_notifications:
|
360
|
-
# Store old values for comparison
|
361
|
-
for field in ['email', 'username', 'first_name', 'last_name']:
|
362
|
-
if field in update_fields:
|
363
|
-
old_values[field] = getattr(user, field)
|
364
|
-
|
365
|
-
# Update user fields
|
366
|
-
for field, value in update_fields.items():
|
367
|
-
setattr(user, field, value)
|
368
|
-
|
369
|
-
user.save()
|
370
|
-
|
371
|
-
# Check for important changes and send notifications
|
372
|
-
if send_notifications and old_values:
|
373
|
-
if 'email' in old_values and old_values['email'] != user.email:
|
374
|
-
important_changes.append("email address")
|
375
|
-
|
376
|
-
if 'username' in old_values and old_values['username'] != user.username:
|
377
|
-
important_changes.append("username")
|
378
|
-
|
379
|
-
if (('first_name' in old_values and old_values['first_name'] != user.first_name) or
|
380
|
-
('last_name' in old_values and old_values['last_name'] != user.last_name)):
|
381
|
-
important_changes.append("name")
|
382
|
-
|
383
|
-
# Send notifications if there were important changes
|
384
|
-
if important_changes:
|
385
|
-
from ..utils.notifications import AccountNotifications
|
386
|
-
AccountNotifications.send_profile_update_notification(user, important_changes)
|
387
|
-
|
388
|
-
return user
|
389
|
-
|
390
|
-
def update_user_status(self, user, is_active=None, send_notifications=True, reason=None):
|
391
|
-
"""
|
392
|
-
Update user active status with optional notifications.
|
393
|
-
|
394
|
-
Args:
|
395
|
-
user: User instance to update
|
396
|
-
is_active: New active status (True/False)
|
397
|
-
send_notifications: Whether to send notifications
|
398
|
-
reason: Reason for status change
|
399
|
-
|
400
|
-
Returns:
|
401
|
-
User: Updated user instance
|
402
|
-
"""
|
403
|
-
if is_active is None:
|
404
|
-
return user
|
405
|
-
|
406
|
-
old_status = user.is_active
|
407
|
-
user.is_active = is_active
|
408
|
-
user.save()
|
409
|
-
|
410
|
-
# Send notifications if status changed
|
411
|
-
if send_notifications and old_status != is_active:
|
412
|
-
from ..utils.notifications import AccountNotifications
|
413
|
-
status_type = "activated" if is_active else "deactivated"
|
414
|
-
AccountNotifications.send_account_status_change(user, status_type, reason)
|
415
|
-
|
416
|
-
return user
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Generated by Django 5.2.6 on 2025-09-20 16:09
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('django_cfg_accounts', '0005_twilioresponse'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.RemoveField(
|
14
|
+
model_name='twilioresponse',
|
15
|
+
name='otp_secret',
|
16
|
+
),
|
17
|
+
migrations.RemoveIndex(
|
18
|
+
model_name='otpsecret',
|
19
|
+
name='django_cfg__recipie_f9f5aa_idx',
|
20
|
+
),
|
21
|
+
migrations.RemoveField(
|
22
|
+
model_name='customuser',
|
23
|
+
name='phone_verified',
|
24
|
+
),
|
25
|
+
migrations.RemoveField(
|
26
|
+
model_name='otpsecret',
|
27
|
+
name='channel_type',
|
28
|
+
),
|
29
|
+
migrations.RemoveField(
|
30
|
+
model_name='otpsecret',
|
31
|
+
name='recipient',
|
32
|
+
),
|
33
|
+
migrations.AddField(
|
34
|
+
model_name='otpsecret',
|
35
|
+
name='email',
|
36
|
+
field=models.EmailField(db_index=True, default=1, max_length=254),
|
37
|
+
preserve_default=False,
|
38
|
+
),
|
39
|
+
migrations.AddIndex(
|
40
|
+
model_name='otpsecret',
|
41
|
+
index=models.Index(fields=['email', 'is_used', 'expires_at'], name='django_cfg__email_f595c8_idx'),
|
42
|
+
),
|
43
|
+
migrations.DeleteModel(
|
44
|
+
name='TwilioResponse',
|
45
|
+
),
|
46
|
+
]
|
@@ -71,7 +71,6 @@ class CustomUser(AbstractUser):
|
|
71
71
|
last_name = models.CharField(max_length=50, blank=True)
|
72
72
|
company = models.CharField(max_length=100, blank=True)
|
73
73
|
phone = models.CharField(max_length=20, blank=True)
|
74
|
-
phone_verified = models.BooleanField(default=False, help_text="Whether the phone number has been verified")
|
75
74
|
position = models.CharField(max_length=100, blank=True)
|
76
75
|
avatar = models.ImageField(upload_to=user_avatar_path, blank=True, null=True)
|
77
76
|
|
@@ -83,12 +82,6 @@ class CustomUser(AbstractUser):
|
|
83
82
|
|
84
83
|
USERNAME_FIELD = "email"
|
85
84
|
REQUIRED_FIELDS = ["username"]
|
86
|
-
|
87
|
-
def get_identifier_for_otp(self, channel='email'):
|
88
|
-
"""Get identifier for OTP based on channel."""
|
89
|
-
if channel == 'phone':
|
90
|
-
return self.phone
|
91
|
-
return self.email
|
92
85
|
|
93
86
|
def __str__(self):
|
94
87
|
return self.email
|
@@ -135,16 +128,8 @@ class CustomUser(AbstractUser):
|
|
135
128
|
|
136
129
|
class OTPSecret(models.Model):
|
137
130
|
"""Stores One-Time Passwords for authentication."""
|
138
|
-
|
139
|
-
CHANNEL_CHOICES = [
|
140
|
-
('email', 'Email'),
|
141
|
-
('phone', 'Phone'),
|
142
|
-
]
|
143
131
|
|
144
|
-
|
145
|
-
channel_type = models.CharField(max_length=10, choices=CHANNEL_CHOICES, default='email')
|
146
|
-
recipient = models.CharField(max_length=255, db_index=True, blank=True, help_text="Email address or phone number")
|
147
|
-
|
132
|
+
email = models.EmailField(db_index=True)
|
148
133
|
secret = models.CharField(max_length=6)
|
149
134
|
created_at = models.DateTimeField(auto_now_add=True)
|
150
135
|
expires_at = models.DateTimeField()
|
@@ -153,34 +138,12 @@ class OTPSecret(models.Model):
|
|
153
138
|
def save(self, *args, **kwargs):
|
154
139
|
if not self.expires_at:
|
155
140
|
self.expires_at = timezone.now() + timedelta(minutes=10)
|
156
|
-
|
157
|
-
|
158
141
|
super().save(*args, **kwargs)
|
159
142
|
|
160
143
|
@staticmethod
|
161
144
|
def generate_otp(length=6):
|
162
145
|
"""Generate random numeric OTP."""
|
163
146
|
return "".join(random.choices(string.digits, k=length))
|
164
|
-
|
165
|
-
@classmethod
|
166
|
-
def create_for_email(cls, email, **kwargs):
|
167
|
-
"""Create OTP for email channel."""
|
168
|
-
return cls.objects.create(
|
169
|
-
channel_type='email',
|
170
|
-
recipient=email,
|
171
|
-
secret=cls.generate_otp(),
|
172
|
-
**kwargs
|
173
|
-
)
|
174
|
-
|
175
|
-
@classmethod
|
176
|
-
def create_for_phone(cls, phone, **kwargs):
|
177
|
-
"""Create OTP for phone channel."""
|
178
|
-
return cls.objects.create(
|
179
|
-
channel_type='phone',
|
180
|
-
recipient=phone,
|
181
|
-
secret=cls.generate_otp(),
|
182
|
-
**kwargs
|
183
|
-
)
|
184
147
|
|
185
148
|
@property
|
186
149
|
def is_valid(self):
|
@@ -193,13 +156,13 @@ class OTPSecret(models.Model):
|
|
193
156
|
self.save(update_fields=["is_used"])
|
194
157
|
|
195
158
|
def __str__(self):
|
196
|
-
return f"OTP for {self.
|
159
|
+
return f"OTP for {self.email}"
|
197
160
|
|
198
161
|
class Meta:
|
199
162
|
app_label = 'django_cfg_accounts'
|
200
163
|
ordering = ["-created_at"]
|
201
164
|
indexes = [
|
202
|
-
models.Index(fields=["
|
165
|
+
models.Index(fields=["email", "is_used", "expires_at"]),
|
203
166
|
]
|
204
167
|
|
205
168
|
|
@@ -237,86 +200,3 @@ class UserActivity(models.Model):
|
|
237
200
|
|
238
201
|
def __str__(self):
|
239
202
|
return f"{self.user.username} - {self.get_activity_type_display()}"
|
240
|
-
|
241
|
-
|
242
|
-
class TwilioResponse(models.Model):
|
243
|
-
"""Model for storing Twilio API responses and webhook data."""
|
244
|
-
|
245
|
-
class ResponseType(models.TextChoices):
|
246
|
-
API_SEND = 'api_send', 'API Send Request'
|
247
|
-
API_VERIFY = 'api_verify', 'API Verify Request'
|
248
|
-
WEBHOOK_STATUS = 'webhook_status', 'Webhook Status Update'
|
249
|
-
WEBHOOK_DELIVERY = 'webhook_delivery', 'Webhook Delivery Report'
|
250
|
-
|
251
|
-
class ServiceType(models.TextChoices):
|
252
|
-
WHATSAPP = 'whatsapp', 'WhatsApp'
|
253
|
-
SMS = 'sms', 'SMS'
|
254
|
-
VOICE = 'voice', 'Voice'
|
255
|
-
EMAIL = 'email', 'Email'
|
256
|
-
VERIFY = 'verify', 'Verify API'
|
257
|
-
|
258
|
-
# Response metadata
|
259
|
-
response_type = models.CharField(max_length=20, choices=ResponseType.choices)
|
260
|
-
service_type = models.CharField(max_length=10, choices=ServiceType.choices)
|
261
|
-
|
262
|
-
# Twilio identifiers
|
263
|
-
message_sid = models.CharField(max_length=34, blank=True, help_text="Twilio Message SID")
|
264
|
-
verification_sid = models.CharField(max_length=34, blank=True, help_text="Twilio Verification SID")
|
265
|
-
|
266
|
-
# Request/Response data
|
267
|
-
request_data = models.JSONField(default=dict, help_text="Original request parameters")
|
268
|
-
response_data = models.JSONField(default=dict, help_text="Twilio API response")
|
269
|
-
|
270
|
-
# Status and error information
|
271
|
-
status = models.CharField(max_length=20, blank=True, help_text="Message/Verification status")
|
272
|
-
error_code = models.CharField(max_length=10, blank=True, help_text="Twilio error code")
|
273
|
-
error_message = models.TextField(blank=True, help_text="Error description")
|
274
|
-
|
275
|
-
# Contact information
|
276
|
-
to_number = models.CharField(max_length=20, blank=True, help_text="Recipient phone/email")
|
277
|
-
from_number = models.CharField(max_length=20, blank=True, help_text="Sender phone/email")
|
278
|
-
|
279
|
-
# Pricing information
|
280
|
-
price = models.DecimalField(max_digits=10, decimal_places=6, null=True, blank=True)
|
281
|
-
price_unit = models.CharField(max_length=3, blank=True, help_text="Currency code")
|
282
|
-
|
283
|
-
# Timestamps
|
284
|
-
created_at = models.DateTimeField(auto_now_add=True)
|
285
|
-
updated_at = models.DateTimeField(auto_now=True)
|
286
|
-
twilio_created_at = models.DateTimeField(null=True, blank=True, help_text="Timestamp from Twilio")
|
287
|
-
|
288
|
-
# Relations
|
289
|
-
otp_secret = models.ForeignKey(
|
290
|
-
OTPSecret,
|
291
|
-
on_delete=models.SET_NULL,
|
292
|
-
null=True,
|
293
|
-
blank=True,
|
294
|
-
related_name='twilio_responses',
|
295
|
-
help_text="Related OTP if applicable"
|
296
|
-
)
|
297
|
-
|
298
|
-
class Meta:
|
299
|
-
app_label = 'django_cfg_accounts'
|
300
|
-
verbose_name = 'Twilio Response'
|
301
|
-
verbose_name_plural = 'Twilio Responses'
|
302
|
-
ordering = ['-created_at']
|
303
|
-
indexes = [
|
304
|
-
models.Index(fields=['message_sid']),
|
305
|
-
models.Index(fields=['verification_sid']),
|
306
|
-
models.Index(fields=['status', 'created_at']),
|
307
|
-
models.Index(fields=['response_type', 'service_type']),
|
308
|
-
]
|
309
|
-
|
310
|
-
def __str__(self):
|
311
|
-
identifier = self.message_sid or self.verification_sid or f"#{self.id}"
|
312
|
-
return f"{self.get_service_type_display()} {self.get_response_type_display()} - {identifier}"
|
313
|
-
|
314
|
-
@property
|
315
|
-
def has_error(self):
|
316
|
-
"""Check if response has error."""
|
317
|
-
return bool(self.error_code or self.error_message)
|
318
|
-
|
319
|
-
@property
|
320
|
-
def is_successful(self):
|
321
|
-
"""Check if response is successful."""
|
322
|
-
return not self.has_error and self.status in ['sent', 'delivered', 'approved']
|
@@ -8,12 +8,12 @@ class OTPSerializer(serializers.ModelSerializer):
|
|
8
8
|
|
9
9
|
class Meta:
|
10
10
|
model = OTPSecret
|
11
|
-
fields = ["
|
11
|
+
fields = ["email", "secret"]
|
12
12
|
read_only_fields = ["secret"]
|
13
13
|
|
14
14
|
|
15
15
|
class OTPRequestSerializer(serializers.Serializer):
|
16
|
-
"""Serializer for OTP request
|
16
|
+
"""Serializer for OTP request."""
|
17
17
|
|
18
18
|
identifier = serializers.CharField(
|
19
19
|
help_text="Email address or phone number for OTP delivery"
|
@@ -21,38 +21,36 @@ class OTPRequestSerializer(serializers.Serializer):
|
|
21
21
|
channel = serializers.ChoiceField(
|
22
22
|
choices=[('email', 'Email'), ('phone', 'Phone')],
|
23
23
|
required=False,
|
24
|
-
help_text="Delivery channel
|
24
|
+
help_text="Delivery channel: 'email' or 'phone'. Auto-detected if not provided."
|
25
25
|
)
|
26
26
|
source_url = serializers.URLField(
|
27
27
|
required=False,
|
28
28
|
allow_blank=True,
|
29
|
-
help_text="Source URL for tracking registration (e.g., https://
|
29
|
+
help_text="Source URL for tracking registration (e.g., https://dashboard.unrealon.com)",
|
30
30
|
)
|
31
31
|
|
32
32
|
def validate_identifier(self, value):
|
33
33
|
"""Validate identifier format."""
|
34
34
|
if not value:
|
35
|
-
raise serializers.ValidationError("Identifier
|
35
|
+
raise serializers.ValidationError("Identifier is required.")
|
36
36
|
|
37
|
-
|
37
|
+
value = value.strip()
|
38
|
+
if not value:
|
39
|
+
raise serializers.ValidationError("Identifier cannot be empty.")
|
40
|
+
|
41
|
+
# Auto-detect if it's email or phone
|
38
42
|
if '@' in value:
|
43
|
+
# Basic email validation
|
44
|
+
if not value.count('@') == 1:
|
45
|
+
raise serializers.ValidationError("Invalid email format.")
|
39
46
|
return value.lower()
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
if not attrs.get('channel'):
|
48
|
-
if '@' in identifier:
|
49
|
-
attrs['channel'] = 'email'
|
50
|
-
elif identifier.startswith('+') or identifier.replace(' ', '').replace('-', '').isdigit():
|
51
|
-
attrs['channel'] = 'phone'
|
52
|
-
else:
|
53
|
-
attrs['channel'] = 'email' # Default
|
54
|
-
|
55
|
-
return attrs
|
47
|
+
else:
|
48
|
+
# Assume it's a phone number - basic validation
|
49
|
+
# Remove common phone number characters for validation
|
50
|
+
clean_phone = ''.join(c for c in value if c.isdigit() or c in '+')
|
51
|
+
if len(clean_phone) < 10:
|
52
|
+
raise serializers.ValidationError("Phone number must be at least 10 digits.")
|
53
|
+
return value
|
56
54
|
|
57
55
|
def validate_source_url(self, value):
|
58
56
|
"""Validate source URL format."""
|
@@ -62,7 +60,7 @@ class OTPRequestSerializer(serializers.Serializer):
|
|
62
60
|
|
63
61
|
|
64
62
|
class OTPVerifySerializer(serializers.Serializer):
|
65
|
-
"""Serializer for OTP verification
|
63
|
+
"""Serializer for OTP verification."""
|
66
64
|
|
67
65
|
identifier = serializers.CharField(
|
68
66
|
help_text="Email address or phone number used for OTP request"
|
@@ -71,44 +69,42 @@ class OTPVerifySerializer(serializers.Serializer):
|
|
71
69
|
channel = serializers.ChoiceField(
|
72
70
|
choices=[('email', 'Email'), ('phone', 'Phone')],
|
73
71
|
required=False,
|
74
|
-
help_text="Delivery channel
|
72
|
+
help_text="Delivery channel: 'email' or 'phone'. Auto-detected if not provided."
|
75
73
|
)
|
76
74
|
source_url = serializers.URLField(
|
77
75
|
required=False,
|
78
76
|
allow_blank=True,
|
79
|
-
help_text="Source URL for tracking login (e.g., https://
|
77
|
+
help_text="Source URL for tracking login (e.g., https://dashboard.unrealon.com)",
|
80
78
|
)
|
81
79
|
|
82
80
|
def validate_identifier(self, value):
|
83
81
|
"""Validate identifier format."""
|
84
82
|
if not value:
|
85
|
-
raise serializers.ValidationError("Identifier
|
83
|
+
raise serializers.ValidationError("Identifier is required.")
|
86
84
|
|
87
|
-
|
85
|
+
value = value.strip()
|
86
|
+
if not value:
|
87
|
+
raise serializers.ValidationError("Identifier cannot be empty.")
|
88
|
+
|
89
|
+
# Auto-detect if it's email or phone
|
88
90
|
if '@' in value:
|
91
|
+
# Basic email validation
|
92
|
+
if not value.count('@') == 1:
|
93
|
+
raise serializers.ValidationError("Invalid email format.")
|
89
94
|
return value.lower()
|
90
|
-
|
95
|
+
else:
|
96
|
+
# Assume it's a phone number - basic validation
|
97
|
+
# Remove common phone number characters for validation
|
98
|
+
clean_phone = ''.join(c for c in value if c.isdigit() or c in '+')
|
99
|
+
if len(clean_phone) < 10:
|
100
|
+
raise serializers.ValidationError("Phone number must be at least 10 digits.")
|
101
|
+
return value
|
91
102
|
|
92
103
|
def validate_otp(self, value):
|
93
104
|
"""Validate OTP format."""
|
94
105
|
if not value.isdigit():
|
95
106
|
raise serializers.ValidationError("OTP must contain only digits.")
|
96
107
|
return value
|
97
|
-
|
98
|
-
def validate(self, attrs):
|
99
|
-
"""Auto-detect channel if not specified."""
|
100
|
-
identifier = attrs.get('identifier')
|
101
|
-
|
102
|
-
# Auto-detect channel if not specified
|
103
|
-
if not attrs.get('channel'):
|
104
|
-
if '@' in identifier:
|
105
|
-
attrs['channel'] = 'email'
|
106
|
-
elif identifier.startswith('+') or identifier.replace(' ', '').replace('-', '').isdigit():
|
107
|
-
attrs['channel'] = 'phone'
|
108
|
-
else:
|
109
|
-
attrs['channel'] = 'email' # Default
|
110
|
-
|
111
|
-
return attrs
|
112
108
|
|
113
109
|
def validate_source_url(self, value):
|
114
110
|
"""Validate source URL format."""
|
@@ -134,4 +130,4 @@ class OTPRequestResponseSerializer(serializers.Serializer):
|
|
134
130
|
class OTPErrorResponseSerializer(serializers.Serializer):
|
135
131
|
"""Error response for OTP operations."""
|
136
132
|
|
137
|
-
error = serializers.CharField(help_text="Error message")
|
133
|
+
error = serializers.CharField(help_text="Error message")
|
@@ -23,7 +23,6 @@ class UserSerializer(serializers.ModelSerializer):
|
|
23
23
|
"display_username",
|
24
24
|
"company",
|
25
25
|
"phone",
|
26
|
-
"phone_verified",
|
27
26
|
"position",
|
28
27
|
"avatar",
|
29
28
|
"is_staff",
|
@@ -35,7 +34,6 @@ class UserSerializer(serializers.ModelSerializer):
|
|
35
34
|
read_only_fields = [
|
36
35
|
"id",
|
37
36
|
"email",
|
38
|
-
"phone_verified",
|
39
37
|
"is_staff",
|
40
38
|
"is_superuser",
|
41
39
|
"date_joined",
|