django-cfg 1.1.69__py3-none-any.whl → 1.1.71__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/admin/__init__.py +2 -0
- django_cfg/apps/accounts/admin/activity.py +9 -2
- django_cfg/apps/accounts/admin/filters.py +42 -0
- django_cfg/apps/accounts/admin/registration_source.py +8 -1
- django_cfg/apps/accounts/admin/resources.py +271 -0
- django_cfg/apps/accounts/admin/twilio_response.py +7 -1
- django_cfg/apps/accounts/admin/user.py +9 -1
- django_cfg/apps/accounts/migrations/0005_twilioresponse.py +43 -0
- django_cfg/apps/accounts/models.py +83 -0
- django_cfg/apps/leads/admin/__init__.py +9 -0
- django_cfg/apps/leads/{admin.py → admin/leads_admin.py} +10 -2
- django_cfg/apps/leads/admin/resources.py +119 -0
- django_cfg/apps/newsletter/admin/__init__.py +12 -0
- django_cfg/apps/newsletter/{admin.py → admin/newsletter_admin.py} +20 -5
- django_cfg/apps/newsletter/admin/resources.py +241 -0
- django_cfg/apps/support/admin/__init__.py +10 -0
- django_cfg/apps/support/admin/resources.py +183 -0
- django_cfg/apps/support/{admin.py → admin/support_admin.py} +16 -6
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/apps/urls.py +1 -2
- django_cfg/archive/django_sample.zip +0 -0
- django_cfg/core/config.py +1 -0
- django_cfg/integration.py +14 -16
- django_cfg/management/commands/migrator.py +49 -13
- {django_cfg-1.1.69.dist-info → django_cfg-1.1.71.dist-info}/METADATA +1 -1
- {django_cfg-1.1.69.dist-info → django_cfg-1.1.71.dist-info}/RECORD +32 -24
- /django_cfg/apps/newsletter/{admin_filters.py → admin/filters.py} +0 -0
- /django_cfg/apps/support/{admin_filters.py → admin/filters.py} +0 -0
- {django_cfg-1.1.69.dist-info → django_cfg-1.1.71.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.69.dist-info → django_cfg-1.1.71.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.1.69.dist-info → django_cfg-1.1.71.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
@@ -38,7 +38,7 @@ default_app_config = "django_cfg.apps.DjangoCfgConfig"
|
|
38
38
|
from typing import TYPE_CHECKING
|
39
39
|
|
40
40
|
# Version information
|
41
|
-
__version__ = "1.1.
|
41
|
+
__version__ = "1.1.71"
|
42
42
|
__author__ = "Unrealos Team"
|
43
43
|
__email__ = "info@unrealos.com"
|
44
44
|
__license__ = "MIT"
|
@@ -7,6 +7,7 @@ from .otp import OTPSecretAdmin
|
|
7
7
|
from .registration_source import RegistrationSourceAdmin, UserRegistrationSourceAdmin
|
8
8
|
from .activity import UserActivityAdmin
|
9
9
|
from .group import GroupAdmin
|
10
|
+
from .twilio_response import TwilioResponseAdmin
|
10
11
|
|
11
12
|
__all__ = [
|
12
13
|
'CustomUserAdmin',
|
@@ -15,4 +16,5 @@ __all__ = [
|
|
15
16
|
'UserRegistrationSourceAdmin',
|
16
17
|
'UserActivityAdmin',
|
17
18
|
'GroupAdmin',
|
19
|
+
'TwilioResponseAdmin',
|
18
20
|
]
|
@@ -6,14 +6,21 @@ from django.contrib import admin
|
|
6
6
|
from django.utils.html import format_html
|
7
7
|
from django.contrib.humanize.templatetags.humanize import naturaltime
|
8
8
|
from unfold.admin import ModelAdmin
|
9
|
+
from import_export.admin import ExportMixin
|
10
|
+
from unfold.contrib.import_export.forms import ExportForm
|
9
11
|
|
10
12
|
from ..models import UserActivity
|
11
13
|
from .filters import ActivityTypeFilter
|
14
|
+
from .resources import UserActivityResource
|
12
15
|
|
13
16
|
|
14
17
|
@admin.register(UserActivity)
|
15
|
-
class UserActivityAdmin(ModelAdmin):
|
16
|
-
"""Enhanced admin for UserActivity model."""
|
18
|
+
class UserActivityAdmin(ModelAdmin, ExportMixin):
|
19
|
+
"""Enhanced admin for UserActivity model with export functionality."""
|
20
|
+
|
21
|
+
# Export-only configuration
|
22
|
+
resource_class = UserActivityResource
|
23
|
+
export_form_class = ExportForm
|
17
24
|
|
18
25
|
list_display = [
|
19
26
|
'user_display',
|
@@ -4,6 +4,7 @@ Custom admin filters for Accounts app.
|
|
4
4
|
|
5
5
|
from django.contrib import admin
|
6
6
|
from django.utils import timezone
|
7
|
+
from django.db import models
|
7
8
|
from datetime import timedelta
|
8
9
|
|
9
10
|
|
@@ -96,3 +97,44 @@ class ActivityTypeFilter(admin.SimpleListFilter):
|
|
96
97
|
elif self.value():
|
97
98
|
return queryset.filter(activity_type=self.value())
|
98
99
|
return queryset
|
100
|
+
|
101
|
+
|
102
|
+
class TwilioResponseStatusFilter(admin.SimpleListFilter):
|
103
|
+
title = "Response Status"
|
104
|
+
parameter_name = "twilio_status"
|
105
|
+
|
106
|
+
def lookups(self, request, model_admin):
|
107
|
+
return (
|
108
|
+
("successful", "Successful"),
|
109
|
+
("error", "Has Error"),
|
110
|
+
("recent", "Recent (24h)"),
|
111
|
+
)
|
112
|
+
|
113
|
+
def queryset(self, request, queryset):
|
114
|
+
now = timezone.now()
|
115
|
+
if self.value() == "successful":
|
116
|
+
return queryset.filter(error_code__isnull=True, error_message__isnull=True)
|
117
|
+
elif self.value() == "error":
|
118
|
+
return queryset.filter(
|
119
|
+
models.Q(error_code__isnull=False) | models.Q(error_message__isnull=False)
|
120
|
+
)
|
121
|
+
elif self.value() == "recent":
|
122
|
+
return queryset.filter(created_at__gte=now - timedelta(hours=24))
|
123
|
+
return queryset
|
124
|
+
|
125
|
+
|
126
|
+
class TwilioResponseTypeFilter(admin.SimpleListFilter):
|
127
|
+
title = "Response Type"
|
128
|
+
parameter_name = "twilio_response_type"
|
129
|
+
|
130
|
+
def lookups(self, request, model_admin):
|
131
|
+
return (
|
132
|
+
("sms", "SMS"),
|
133
|
+
("verification", "Verification"),
|
134
|
+
("call", "Call"),
|
135
|
+
)
|
136
|
+
|
137
|
+
def queryset(self, request, queryset):
|
138
|
+
if self.value():
|
139
|
+
return queryset.filter(response_type=self.value())
|
140
|
+
return queryset
|
@@ -5,14 +5,21 @@ 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
|
8
10
|
|
9
11
|
from ..models import RegistrationSource, UserRegistrationSource
|
10
12
|
from .filters import RegistrationSourceStatusFilter
|
11
13
|
from .inlines import RegistrationSourceInline
|
14
|
+
from .resources import RegistrationSourceResource
|
12
15
|
|
13
16
|
|
14
17
|
@admin.register(RegistrationSource)
|
15
|
-
class RegistrationSourceAdmin(ModelAdmin):
|
18
|
+
class RegistrationSourceAdmin(ModelAdmin, ImportExportModelAdmin):
|
19
|
+
# Import/Export configuration
|
20
|
+
resource_class = RegistrationSourceResource
|
21
|
+
import_form_class = ImportForm
|
22
|
+
export_form_class = ExportForm
|
16
23
|
list_display = ["name", "url", "status", "users_count", "created"]
|
17
24
|
list_display_links = ["name", "url"]
|
18
25
|
list_filter = [RegistrationSourceStatusFilter, "is_active", "created_at"]
|
@@ -0,0 +1,271 @@
|
|
1
|
+
"""
|
2
|
+
Import/Export resources for Accounts app.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from import_export import resources, fields
|
6
|
+
from import_export.widgets import ForeignKeyWidget, DateTimeWidget, BooleanWidget, ManyToManyWidget
|
7
|
+
from django.contrib.auth.models import Group
|
8
|
+
|
9
|
+
from ..models import CustomUser, UserActivity, RegistrationSource, TwilioResponse
|
10
|
+
|
11
|
+
|
12
|
+
class CustomUserResource(resources.ModelResource):
|
13
|
+
"""Resource for importing/exporting users."""
|
14
|
+
|
15
|
+
# Custom fields for better export/import
|
16
|
+
full_name = fields.Field(
|
17
|
+
column_name='full_name',
|
18
|
+
attribute='get_full_name',
|
19
|
+
readonly=True
|
20
|
+
)
|
21
|
+
|
22
|
+
groups = fields.Field(
|
23
|
+
column_name='groups',
|
24
|
+
attribute='groups',
|
25
|
+
widget=ManyToManyWidget(Group, field='name', separator='|')
|
26
|
+
)
|
27
|
+
|
28
|
+
last_login = fields.Field(
|
29
|
+
column_name='last_login',
|
30
|
+
attribute='last_login',
|
31
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
32
|
+
)
|
33
|
+
|
34
|
+
date_joined = fields.Field(
|
35
|
+
column_name='date_joined',
|
36
|
+
attribute='date_joined',
|
37
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
38
|
+
)
|
39
|
+
|
40
|
+
is_active = fields.Field(
|
41
|
+
column_name='is_active',
|
42
|
+
attribute='is_active',
|
43
|
+
widget=BooleanWidget()
|
44
|
+
)
|
45
|
+
|
46
|
+
is_staff = fields.Field(
|
47
|
+
column_name='is_staff',
|
48
|
+
attribute='is_staff',
|
49
|
+
widget=BooleanWidget()
|
50
|
+
)
|
51
|
+
|
52
|
+
phone_verified = fields.Field(
|
53
|
+
column_name='phone_verified',
|
54
|
+
attribute='phone_verified',
|
55
|
+
widget=BooleanWidget()
|
56
|
+
)
|
57
|
+
|
58
|
+
class Meta:
|
59
|
+
model = CustomUser
|
60
|
+
fields = (
|
61
|
+
'id',
|
62
|
+
'email',
|
63
|
+
'first_name',
|
64
|
+
'last_name',
|
65
|
+
'full_name',
|
66
|
+
'company',
|
67
|
+
'phone',
|
68
|
+
'phone_verified',
|
69
|
+
'position',
|
70
|
+
'is_active',
|
71
|
+
'is_staff',
|
72
|
+
'is_superuser',
|
73
|
+
'groups',
|
74
|
+
'last_login',
|
75
|
+
'date_joined',
|
76
|
+
)
|
77
|
+
export_order = fields
|
78
|
+
import_id_fields = ('email',) # Use email as unique identifier
|
79
|
+
skip_unchanged = True
|
80
|
+
report_skipped = True
|
81
|
+
|
82
|
+
def before_import_row(self, row, **kwargs):
|
83
|
+
"""Process row before import."""
|
84
|
+
# Ensure email is lowercase
|
85
|
+
if 'email' in row:
|
86
|
+
row['email'] = row['email'].lower().strip()
|
87
|
+
|
88
|
+
def skip_row(self, instance, original, row, import_validation_errors=None):
|
89
|
+
"""Skip rows with validation errors."""
|
90
|
+
if import_validation_errors:
|
91
|
+
return True
|
92
|
+
return super().skip_row(instance, original, row, import_validation_errors)
|
93
|
+
|
94
|
+
|
95
|
+
class UserActivityResource(resources.ModelResource):
|
96
|
+
"""Resource for exporting user activity (export only)."""
|
97
|
+
|
98
|
+
user_email = fields.Field(
|
99
|
+
column_name='user_email',
|
100
|
+
attribute='user__email',
|
101
|
+
readonly=True
|
102
|
+
)
|
103
|
+
|
104
|
+
user_full_name = fields.Field(
|
105
|
+
column_name='user_full_name',
|
106
|
+
attribute='user__get_full_name',
|
107
|
+
readonly=True
|
108
|
+
)
|
109
|
+
|
110
|
+
activity_type_display = fields.Field(
|
111
|
+
column_name='activity_type_display',
|
112
|
+
attribute='get_activity_type_display',
|
113
|
+
readonly=True
|
114
|
+
)
|
115
|
+
|
116
|
+
created_at = fields.Field(
|
117
|
+
column_name='created_at',
|
118
|
+
attribute='created_at',
|
119
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
120
|
+
)
|
121
|
+
|
122
|
+
class Meta:
|
123
|
+
model = UserActivity
|
124
|
+
fields = (
|
125
|
+
'id',
|
126
|
+
'user_email',
|
127
|
+
'user_full_name',
|
128
|
+
'activity_type',
|
129
|
+
'activity_type_display',
|
130
|
+
'description',
|
131
|
+
'ip_address',
|
132
|
+
'user_agent',
|
133
|
+
'object_id',
|
134
|
+
'object_type',
|
135
|
+
'created_at',
|
136
|
+
)
|
137
|
+
export_order = fields
|
138
|
+
# No import - this is export only
|
139
|
+
|
140
|
+
def get_queryset(self):
|
141
|
+
"""Optimize queryset for export."""
|
142
|
+
return super().get_queryset().select_related('user')
|
143
|
+
|
144
|
+
|
145
|
+
class RegistrationSourceResource(resources.ModelResource):
|
146
|
+
"""Resource for importing/exporting registration sources."""
|
147
|
+
|
148
|
+
is_active = fields.Field(
|
149
|
+
column_name='is_active',
|
150
|
+
attribute='is_active',
|
151
|
+
widget=BooleanWidget()
|
152
|
+
)
|
153
|
+
|
154
|
+
created_at = fields.Field(
|
155
|
+
column_name='created_at',
|
156
|
+
attribute='created_at',
|
157
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
158
|
+
)
|
159
|
+
|
160
|
+
updated_at = fields.Field(
|
161
|
+
column_name='updated_at',
|
162
|
+
attribute='updated_at',
|
163
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
164
|
+
)
|
165
|
+
|
166
|
+
users_count = fields.Field(
|
167
|
+
column_name='users_count',
|
168
|
+
readonly=True
|
169
|
+
)
|
170
|
+
|
171
|
+
class Meta:
|
172
|
+
model = RegistrationSource
|
173
|
+
fields = (
|
174
|
+
'id',
|
175
|
+
'url',
|
176
|
+
'name',
|
177
|
+
'description',
|
178
|
+
'is_active',
|
179
|
+
'users_count',
|
180
|
+
'created_at',
|
181
|
+
'updated_at',
|
182
|
+
)
|
183
|
+
export_order = fields
|
184
|
+
import_id_fields = ('url',) # Use URL as unique identifier
|
185
|
+
skip_unchanged = True
|
186
|
+
report_skipped = True
|
187
|
+
|
188
|
+
def dehydrate_users_count(self, registration_source):
|
189
|
+
"""Calculate users count for export."""
|
190
|
+
return registration_source.user_registration_sources.count()
|
191
|
+
|
192
|
+
def before_import_row(self, row, **kwargs):
|
193
|
+
"""Process row before import."""
|
194
|
+
# Ensure URL is properly formatted
|
195
|
+
if 'url' in row and row['url']:
|
196
|
+
url = row['url'].strip()
|
197
|
+
if not url.startswith(('http://', 'https://')):
|
198
|
+
row['url'] = f'https://{url}'
|
199
|
+
else:
|
200
|
+
row['url'] = url
|
201
|
+
|
202
|
+
|
203
|
+
class TwilioResponseResource(resources.ModelResource):
|
204
|
+
"""Resource for exporting Twilio responses (export only)."""
|
205
|
+
|
206
|
+
otp_recipient = fields.Field(
|
207
|
+
column_name='otp_recipient',
|
208
|
+
attribute='otp_secret__recipient',
|
209
|
+
readonly=True
|
210
|
+
)
|
211
|
+
|
212
|
+
created_at = fields.Field(
|
213
|
+
column_name='created_at',
|
214
|
+
attribute='created_at',
|
215
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
216
|
+
)
|
217
|
+
|
218
|
+
updated_at = fields.Field(
|
219
|
+
column_name='updated_at',
|
220
|
+
attribute='updated_at',
|
221
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
222
|
+
)
|
223
|
+
|
224
|
+
twilio_created_at = fields.Field(
|
225
|
+
column_name='twilio_created_at',
|
226
|
+
attribute='twilio_created_at',
|
227
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
228
|
+
)
|
229
|
+
|
230
|
+
has_error = fields.Field(
|
231
|
+
column_name='has_error',
|
232
|
+
attribute='has_error',
|
233
|
+
widget=BooleanWidget(),
|
234
|
+
readonly=True
|
235
|
+
)
|
236
|
+
|
237
|
+
is_successful = fields.Field(
|
238
|
+
column_name='is_successful',
|
239
|
+
attribute='is_successful',
|
240
|
+
widget=BooleanWidget(),
|
241
|
+
readonly=True
|
242
|
+
)
|
243
|
+
|
244
|
+
class Meta:
|
245
|
+
model = TwilioResponse
|
246
|
+
fields = (
|
247
|
+
'id',
|
248
|
+
'response_type',
|
249
|
+
'service_type',
|
250
|
+
'status',
|
251
|
+
'message_sid',
|
252
|
+
'verification_sid',
|
253
|
+
'to_number',
|
254
|
+
'from_number',
|
255
|
+
'otp_recipient',
|
256
|
+
'error_code',
|
257
|
+
'error_message',
|
258
|
+
'price',
|
259
|
+
'price_unit',
|
260
|
+
'has_error',
|
261
|
+
'is_successful',
|
262
|
+
'created_at',
|
263
|
+
'updated_at',
|
264
|
+
'twilio_created_at',
|
265
|
+
)
|
266
|
+
export_order = fields
|
267
|
+
# No import - this is export only
|
268
|
+
|
269
|
+
def get_queryset(self):
|
270
|
+
"""Optimize queryset for export."""
|
271
|
+
return super().get_queryset().select_related('otp_secret')
|
@@ -6,9 +6,12 @@ from django.contrib import admin
|
|
6
6
|
from django.contrib.humanize.templatetags.humanize import naturaltime
|
7
7
|
from django.utils.html import format_html
|
8
8
|
from unfold.admin import ModelAdmin
|
9
|
+
from import_export.admin import ExportMixin
|
10
|
+
from unfold.contrib.import_export.forms import ExportForm
|
9
11
|
|
10
12
|
from ..models import TwilioResponse
|
11
13
|
from .filters import TwilioResponseStatusFilter, TwilioResponseTypeFilter
|
14
|
+
from .resources import TwilioResponseResource
|
12
15
|
|
13
16
|
|
14
17
|
class TwilioResponseInline(admin.TabularInline):
|
@@ -23,7 +26,10 @@ class TwilioResponseInline(admin.TabularInline):
|
|
23
26
|
|
24
27
|
|
25
28
|
@admin.register(TwilioResponse)
|
26
|
-
class TwilioResponseAdmin(ModelAdmin):
|
29
|
+
class TwilioResponseAdmin(ModelAdmin, ExportMixin):
|
30
|
+
# Export-only configuration
|
31
|
+
resource_class = TwilioResponseResource
|
32
|
+
export_form_class = ExportForm
|
27
33
|
list_display = [
|
28
34
|
'identifier',
|
29
35
|
'service_type',
|
@@ -12,18 +12,26 @@ 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
|
15
17
|
|
16
18
|
from ..models import CustomUser
|
17
19
|
from .filters import UserStatusFilter
|
18
20
|
from .inlines import UserRegistrationSourceInline, UserActivityInline, UserEmailLogInline, UserSupportTicketsInline
|
21
|
+
from .resources import CustomUserResource
|
19
22
|
|
20
23
|
|
21
24
|
@admin.register(CustomUser)
|
22
|
-
class CustomUserAdmin(BaseUserAdmin, ModelAdmin):
|
25
|
+
class CustomUserAdmin(BaseUserAdmin, ModelAdmin, ImportExportModelAdmin):
|
23
26
|
# Forms loaded from `unfold.forms`
|
24
27
|
form = UserChangeForm
|
25
28
|
add_form = UserCreationForm
|
26
29
|
change_password_form = AdminPasswordChangeForm
|
30
|
+
|
31
|
+
# Import/Export configuration
|
32
|
+
resource_class = CustomUserResource
|
33
|
+
import_form_class = ImportForm
|
34
|
+
export_form_class = ExportForm
|
27
35
|
|
28
36
|
list_display = [
|
29
37
|
"avatar",
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Generated by Django 5.2.6 on 2025-09-17 19:28
|
2
|
+
|
3
|
+
import django.db.models.deletion
|
4
|
+
from django.db import migrations, models
|
5
|
+
|
6
|
+
|
7
|
+
class Migration(migrations.Migration):
|
8
|
+
|
9
|
+
dependencies = [
|
10
|
+
('django_cfg_accounts', '0004_delete_twilioresponse'),
|
11
|
+
]
|
12
|
+
|
13
|
+
operations = [
|
14
|
+
migrations.CreateModel(
|
15
|
+
name='TwilioResponse',
|
16
|
+
fields=[
|
17
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
18
|
+
('response_type', models.CharField(choices=[('api_send', 'API Send Request'), ('api_verify', 'API Verify Request'), ('webhook_status', 'Webhook Status Update'), ('webhook_delivery', 'Webhook Delivery Report')], max_length=20)),
|
19
|
+
('service_type', models.CharField(choices=[('whatsapp', 'WhatsApp'), ('sms', 'SMS'), ('voice', 'Voice'), ('email', 'Email'), ('verify', 'Verify API')], max_length=10)),
|
20
|
+
('message_sid', models.CharField(blank=True, help_text='Twilio Message SID', max_length=34)),
|
21
|
+
('verification_sid', models.CharField(blank=True, help_text='Twilio Verification SID', max_length=34)),
|
22
|
+
('request_data', models.JSONField(default=dict, help_text='Original request parameters')),
|
23
|
+
('response_data', models.JSONField(default=dict, help_text='Twilio API response')),
|
24
|
+
('status', models.CharField(blank=True, help_text='Message/Verification status', max_length=20)),
|
25
|
+
('error_code', models.CharField(blank=True, help_text='Twilio error code', max_length=10)),
|
26
|
+
('error_message', models.TextField(blank=True, help_text='Error description')),
|
27
|
+
('to_number', models.CharField(blank=True, help_text='Recipient phone/email', max_length=20)),
|
28
|
+
('from_number', models.CharField(blank=True, help_text='Sender phone/email', max_length=20)),
|
29
|
+
('price', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True)),
|
30
|
+
('price_unit', models.CharField(blank=True, help_text='Currency code', max_length=3)),
|
31
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
32
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
33
|
+
('twilio_created_at', models.DateTimeField(blank=True, help_text='Timestamp from Twilio', null=True)),
|
34
|
+
('otp_secret', models.ForeignKey(blank=True, help_text='Related OTP if applicable', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='twilio_responses', to='django_cfg_accounts.otpsecret')),
|
35
|
+
],
|
36
|
+
options={
|
37
|
+
'verbose_name': 'Twilio Response',
|
38
|
+
'verbose_name_plural': 'Twilio Responses',
|
39
|
+
'ordering': ['-created_at'],
|
40
|
+
'indexes': [models.Index(fields=['message_sid'], name='django_cfg__message_c37dcd_idx'), models.Index(fields=['verification_sid'], name='django_cfg__verific_7de689_idx'), models.Index(fields=['status', 'created_at'], name='django_cfg__status_95d8c8_idx'), models.Index(fields=['response_type', 'service_type'], name='django_cfg__respons_20ca26_idx')],
|
41
|
+
},
|
42
|
+
),
|
43
|
+
]
|
@@ -237,3 +237,86 @@ class UserActivity(models.Model):
|
|
237
237
|
|
238
238
|
def __str__(self):
|
239
239
|
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']
|
@@ -4,11 +4,19 @@ from django.utils.html import format_html
|
|
4
4
|
from django.http import HttpResponseRedirect
|
5
5
|
from unfold.admin import ModelAdmin
|
6
6
|
from unfold.decorators import action
|
7
|
-
from .
|
7
|
+
from import_export.admin import ImportExportModelAdmin
|
8
|
+
from unfold.contrib.import_export.forms import ImportForm, ExportForm
|
9
|
+
|
10
|
+
from ..models import Lead
|
11
|
+
from .resources import LeadResource
|
8
12
|
|
9
13
|
|
10
14
|
@admin.register(Lead)
|
11
|
-
class LeadAdmin(ModelAdmin):
|
15
|
+
class LeadAdmin(ModelAdmin, ImportExportModelAdmin):
|
16
|
+
# Import/Export configuration
|
17
|
+
resource_class = LeadResource
|
18
|
+
import_form_class = ImportForm
|
19
|
+
export_form_class = ExportForm
|
12
20
|
list_display = [
|
13
21
|
'name', 'email', 'company', 'contact_type', 'contact_value',
|
14
22
|
'subject', 'status_display', 'created_at'
|