django-cfg 1.1.67__py3-none-any.whl → 1.1.70__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/core/generation.py +2 -1
- django_cfg/integration.py +14 -16
- django_cfg/management/commands/migrator.py +49 -13
- django_cfg/utils/smart_defaults.py +11 -1
- {django_cfg-1.1.67.dist-info → django_cfg-1.1.70.dist-info}/METADATA +2 -1
- {django_cfg-1.1.67.dist-info → django_cfg-1.1.70.dist-info}/RECORD +34 -26
- /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.67.dist-info → django_cfg-1.1.70.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.67.dist-info → django_cfg-1.1.70.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.1.67.dist-info → django_cfg-1.1.70.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,119 @@
|
|
1
|
+
"""
|
2
|
+
Import/Export resources for Leads app.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from import_export import resources, fields
|
6
|
+
from import_export.widgets import ForeignKeyWidget, DateTimeWidget, BooleanWidget, JSONWidget
|
7
|
+
from django.contrib.auth import get_user_model
|
8
|
+
|
9
|
+
from ..models import Lead
|
10
|
+
|
11
|
+
User = get_user_model()
|
12
|
+
|
13
|
+
|
14
|
+
class LeadResource(resources.ModelResource):
|
15
|
+
"""Resource for importing/exporting leads."""
|
16
|
+
|
17
|
+
# Custom fields for better export/import
|
18
|
+
user_email = fields.Field(
|
19
|
+
column_name='user_email',
|
20
|
+
attribute='user__email',
|
21
|
+
widget=ForeignKeyWidget(User, field='email'),
|
22
|
+
readonly=False
|
23
|
+
)
|
24
|
+
|
25
|
+
status_display = fields.Field(
|
26
|
+
column_name='status_display',
|
27
|
+
attribute='get_status_display',
|
28
|
+
readonly=True
|
29
|
+
)
|
30
|
+
|
31
|
+
contact_type_display = fields.Field(
|
32
|
+
column_name='contact_type_display',
|
33
|
+
attribute='get_contact_type_display',
|
34
|
+
readonly=True
|
35
|
+
)
|
36
|
+
|
37
|
+
created_at = fields.Field(
|
38
|
+
column_name='created_at',
|
39
|
+
attribute='created_at',
|
40
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
41
|
+
)
|
42
|
+
|
43
|
+
updated_at = fields.Field(
|
44
|
+
column_name='updated_at',
|
45
|
+
attribute='updated_at',
|
46
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
47
|
+
)
|
48
|
+
|
49
|
+
extra = fields.Field(
|
50
|
+
column_name='extra',
|
51
|
+
attribute='extra',
|
52
|
+
widget=JSONWidget()
|
53
|
+
)
|
54
|
+
|
55
|
+
class Meta:
|
56
|
+
model = Lead
|
57
|
+
fields = (
|
58
|
+
'id',
|
59
|
+
'name',
|
60
|
+
'email',
|
61
|
+
'company',
|
62
|
+
'company_site',
|
63
|
+
'contact_type',
|
64
|
+
'contact_type_display',
|
65
|
+
'contact_value',
|
66
|
+
'subject',
|
67
|
+
'message',
|
68
|
+
'extra',
|
69
|
+
'site_url',
|
70
|
+
'user_agent',
|
71
|
+
'ip_address',
|
72
|
+
'status',
|
73
|
+
'status_display',
|
74
|
+
'user_email',
|
75
|
+
'admin_notes',
|
76
|
+
'created_at',
|
77
|
+
'updated_at',
|
78
|
+
)
|
79
|
+
export_order = fields
|
80
|
+
import_id_fields = ('email', 'site_url', 'created_at') # Composite unique identifier
|
81
|
+
skip_unchanged = True
|
82
|
+
report_skipped = True
|
83
|
+
|
84
|
+
def before_import_row(self, row, **kwargs):
|
85
|
+
"""Process row before import."""
|
86
|
+
# Ensure email is lowercase
|
87
|
+
if 'email' in row:
|
88
|
+
row['email'] = row['email'].lower().strip()
|
89
|
+
|
90
|
+
# Handle user assignment by email
|
91
|
+
if 'user_email' in row and row['user_email']:
|
92
|
+
try:
|
93
|
+
user = User.objects.get(email=row['user_email'].lower().strip())
|
94
|
+
row['user'] = user.pk
|
95
|
+
except User.DoesNotExist:
|
96
|
+
# Clear user field if email not found
|
97
|
+
row['user'] = None
|
98
|
+
|
99
|
+
# Validate status
|
100
|
+
if 'status' in row and row['status']:
|
101
|
+
valid_statuses = [choice[0] for choice in Lead.StatusChoices.choices]
|
102
|
+
if row['status'] not in valid_statuses:
|
103
|
+
row['status'] = Lead.StatusChoices.NEW
|
104
|
+
|
105
|
+
# Validate contact_type
|
106
|
+
if 'contact_type' in row and row['contact_type']:
|
107
|
+
valid_types = [choice[0] for choice in Lead.ContactTypeChoices.choices]
|
108
|
+
if row['contact_type'] not in valid_types:
|
109
|
+
row['contact_type'] = Lead.ContactTypeChoices.EMAIL
|
110
|
+
|
111
|
+
def skip_row(self, instance, original, row, import_validation_errors=None):
|
112
|
+
"""Skip rows with validation errors."""
|
113
|
+
if import_validation_errors:
|
114
|
+
return True
|
115
|
+
return super().skip_row(instance, original, row, import_validation_errors)
|
116
|
+
|
117
|
+
def get_queryset(self):
|
118
|
+
"""Optimize queryset for export."""
|
119
|
+
return super().get_queryset().select_related('user')
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"""
|
2
|
+
Admin configuration for Newsletter app.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .newsletter_admin import EmailLogAdmin, NewsletterAdmin, NewsletterSubscriptionAdmin, NewsletterCampaignAdmin
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
'EmailLogAdmin',
|
9
|
+
'NewsletterAdmin',
|
10
|
+
'NewsletterSubscriptionAdmin',
|
11
|
+
'NewsletterCampaignAdmin',
|
12
|
+
]
|
@@ -7,12 +7,19 @@ from unfold.admin import ModelAdmin
|
|
7
7
|
from unfold.decorators import action
|
8
8
|
from unfold.contrib.forms.widgets import WysiwygWidget
|
9
9
|
from unfold.enums import ActionVariant
|
10
|
-
from .
|
11
|
-
from .
|
10
|
+
from import_export.admin import ImportExportModelAdmin, ExportMixin
|
11
|
+
from unfold.contrib.import_export.forms import ImportForm, ExportForm
|
12
|
+
|
13
|
+
from ..models import EmailLog, Newsletter, NewsletterSubscription, NewsletterCampaign
|
14
|
+
from .filters import UserEmailFilter, UserNameFilter, HasUserFilter, EmailOpenedFilter, EmailClickedFilter
|
15
|
+
from .resources import NewsletterResource, NewsletterSubscriptionResource, EmailLogResource
|
12
16
|
|
13
17
|
|
14
18
|
@admin.register(EmailLog)
|
15
|
-
class EmailLogAdmin(ModelAdmin):
|
19
|
+
class EmailLogAdmin(ModelAdmin, ExportMixin):
|
20
|
+
# Export-only configuration
|
21
|
+
resource_class = EmailLogResource
|
22
|
+
export_form_class = ExportForm
|
16
23
|
list_display = ('user', 'recipient', 'subject', 'newsletter_link', 'status', 'created_at', 'sent_at', 'tracking_status')
|
17
24
|
list_filter = ('status', 'created_at', 'sent_at', 'newsletter', EmailOpenedFilter, EmailClickedFilter, HasUserFilter, UserEmailFilter, UserNameFilter)
|
18
25
|
autocomplete_fields = ('user',)
|
@@ -55,7 +62,11 @@ class EmailLogAdmin(ModelAdmin):
|
|
55
62
|
|
56
63
|
|
57
64
|
@admin.register(Newsletter)
|
58
|
-
class NewsletterAdmin(ModelAdmin):
|
65
|
+
class NewsletterAdmin(ModelAdmin, ImportExportModelAdmin):
|
66
|
+
# Import/Export configuration
|
67
|
+
resource_class = NewsletterResource
|
68
|
+
import_form_class = ImportForm
|
69
|
+
export_form_class = ExportForm
|
59
70
|
list_display = ('title', 'description', 'is_active', 'auto_subscribe', 'subscribers_count', 'created_at')
|
60
71
|
list_filter = ('is_active', 'auto_subscribe', 'created_at')
|
61
72
|
search_fields = ('title', 'description')
|
@@ -70,7 +81,11 @@ class NewsletterSubscriptionInline(admin.TabularInline):
|
|
70
81
|
|
71
82
|
|
72
83
|
@admin.register(NewsletterSubscription)
|
73
|
-
class NewsletterSubscriptionAdmin(ModelAdmin):
|
84
|
+
class NewsletterSubscriptionAdmin(ModelAdmin, ImportExportModelAdmin):
|
85
|
+
# Import/Export configuration
|
86
|
+
resource_class = NewsletterSubscriptionResource
|
87
|
+
import_form_class = ImportForm
|
88
|
+
export_form_class = ExportForm
|
74
89
|
list_display = ('email', 'newsletter', 'user', 'is_active', 'subscribed_at', 'unsubscribed_at')
|
75
90
|
list_filter = ('is_active', 'newsletter', 'subscribed_at')
|
76
91
|
search_fields = ('email', 'user__email', 'newsletter__title')
|
@@ -0,0 +1,241 @@
|
|
1
|
+
"""
|
2
|
+
Import/Export resources for Newsletter app.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from import_export import resources, fields
|
6
|
+
from import_export.widgets import ForeignKeyWidget, DateTimeWidget, BooleanWidget
|
7
|
+
from django.contrib.auth import get_user_model
|
8
|
+
|
9
|
+
from ..models import Newsletter, NewsletterSubscription, EmailLog
|
10
|
+
|
11
|
+
User = get_user_model()
|
12
|
+
|
13
|
+
|
14
|
+
class NewsletterResource(resources.ModelResource):
|
15
|
+
"""Resource for importing/exporting newsletters."""
|
16
|
+
|
17
|
+
subscribers_count = fields.Field(
|
18
|
+
column_name='subscribers_count',
|
19
|
+
attribute='subscribers_count',
|
20
|
+
readonly=True
|
21
|
+
)
|
22
|
+
|
23
|
+
is_active = fields.Field(
|
24
|
+
column_name='is_active',
|
25
|
+
attribute='is_active',
|
26
|
+
widget=BooleanWidget()
|
27
|
+
)
|
28
|
+
|
29
|
+
auto_subscribe = fields.Field(
|
30
|
+
column_name='auto_subscribe',
|
31
|
+
attribute='auto_subscribe',
|
32
|
+
widget=BooleanWidget()
|
33
|
+
)
|
34
|
+
|
35
|
+
created_at = fields.Field(
|
36
|
+
column_name='created_at',
|
37
|
+
attribute='created_at',
|
38
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
39
|
+
)
|
40
|
+
|
41
|
+
updated_at = fields.Field(
|
42
|
+
column_name='updated_at',
|
43
|
+
attribute='updated_at',
|
44
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
45
|
+
)
|
46
|
+
|
47
|
+
class Meta:
|
48
|
+
model = Newsletter
|
49
|
+
fields = (
|
50
|
+
'id',
|
51
|
+
'title',
|
52
|
+
'description',
|
53
|
+
'is_active',
|
54
|
+
'auto_subscribe',
|
55
|
+
'subscribers_count',
|
56
|
+
'created_at',
|
57
|
+
'updated_at',
|
58
|
+
)
|
59
|
+
export_order = fields
|
60
|
+
import_id_fields = ('title',) # Use title as unique identifier
|
61
|
+
skip_unchanged = True
|
62
|
+
report_skipped = True
|
63
|
+
|
64
|
+
def before_import_row(self, row, **kwargs):
|
65
|
+
"""Process row before import."""
|
66
|
+
# Ensure title is not empty
|
67
|
+
if 'title' in row:
|
68
|
+
row['title'] = row['title'].strip()
|
69
|
+
if not row['title']:
|
70
|
+
raise ValueError("Newsletter title cannot be empty")
|
71
|
+
|
72
|
+
|
73
|
+
class NewsletterSubscriptionResource(resources.ModelResource):
|
74
|
+
"""Resource for importing/exporting newsletter subscriptions."""
|
75
|
+
|
76
|
+
newsletter_title = fields.Field(
|
77
|
+
column_name='newsletter_title',
|
78
|
+
attribute='newsletter__title',
|
79
|
+
widget=ForeignKeyWidget(Newsletter, field='title'),
|
80
|
+
readonly=False
|
81
|
+
)
|
82
|
+
|
83
|
+
user_email = fields.Field(
|
84
|
+
column_name='user_email',
|
85
|
+
attribute='user__email',
|
86
|
+
widget=ForeignKeyWidget(User, field='email'),
|
87
|
+
readonly=False
|
88
|
+
)
|
89
|
+
|
90
|
+
is_active = fields.Field(
|
91
|
+
column_name='is_active',
|
92
|
+
attribute='is_active',
|
93
|
+
widget=BooleanWidget()
|
94
|
+
)
|
95
|
+
|
96
|
+
subscribed_at = fields.Field(
|
97
|
+
column_name='subscribed_at',
|
98
|
+
attribute='subscribed_at',
|
99
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
100
|
+
)
|
101
|
+
|
102
|
+
unsubscribed_at = fields.Field(
|
103
|
+
column_name='unsubscribed_at',
|
104
|
+
attribute='unsubscribed_at',
|
105
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
106
|
+
)
|
107
|
+
|
108
|
+
class Meta:
|
109
|
+
model = NewsletterSubscription
|
110
|
+
fields = (
|
111
|
+
'id',
|
112
|
+
'newsletter_title',
|
113
|
+
'user_email',
|
114
|
+
'email',
|
115
|
+
'is_active',
|
116
|
+
'subscribed_at',
|
117
|
+
'unsubscribed_at',
|
118
|
+
)
|
119
|
+
export_order = fields
|
120
|
+
import_id_fields = ('newsletter_title', 'email') # Composite unique identifier
|
121
|
+
skip_unchanged = True
|
122
|
+
report_skipped = True
|
123
|
+
|
124
|
+
def before_import_row(self, row, **kwargs):
|
125
|
+
"""Process row before import."""
|
126
|
+
# Ensure email is lowercase
|
127
|
+
if 'email' in row:
|
128
|
+
row['email'] = row['email'].lower().strip()
|
129
|
+
|
130
|
+
# Handle newsletter assignment by title
|
131
|
+
if 'newsletter_title' in row and row['newsletter_title']:
|
132
|
+
try:
|
133
|
+
newsletter = Newsletter.objects.get(title=row['newsletter_title'].strip())
|
134
|
+
row['newsletter'] = newsletter.pk
|
135
|
+
except Newsletter.DoesNotExist:
|
136
|
+
raise ValueError(f"Newsletter '{row['newsletter_title']}' not found")
|
137
|
+
|
138
|
+
# Handle user assignment by email (optional)
|
139
|
+
if 'user_email' in row and row['user_email']:
|
140
|
+
try:
|
141
|
+
user = User.objects.get(email=row['user_email'].lower().strip())
|
142
|
+
row['user'] = user.pk
|
143
|
+
except User.DoesNotExist:
|
144
|
+
# Clear user field if email not found
|
145
|
+
row['user'] = None
|
146
|
+
|
147
|
+
def get_queryset(self):
|
148
|
+
"""Optimize queryset for export."""
|
149
|
+
return super().get_queryset().select_related('newsletter', 'user')
|
150
|
+
|
151
|
+
|
152
|
+
class EmailLogResource(resources.ModelResource):
|
153
|
+
"""Resource for exporting email logs (export only)."""
|
154
|
+
|
155
|
+
user_email = fields.Field(
|
156
|
+
column_name='user_email',
|
157
|
+
attribute='user__email',
|
158
|
+
readonly=True
|
159
|
+
)
|
160
|
+
|
161
|
+
newsletter_title = fields.Field(
|
162
|
+
column_name='newsletter_title',
|
163
|
+
attribute='newsletter__title',
|
164
|
+
readonly=True
|
165
|
+
)
|
166
|
+
|
167
|
+
campaign_subject = fields.Field(
|
168
|
+
column_name='campaign_subject',
|
169
|
+
attribute='campaign__subject',
|
170
|
+
readonly=True
|
171
|
+
)
|
172
|
+
|
173
|
+
status_display = fields.Field(
|
174
|
+
column_name='status_display',
|
175
|
+
attribute='get_status_display',
|
176
|
+
readonly=True
|
177
|
+
)
|
178
|
+
|
179
|
+
is_opened = fields.Field(
|
180
|
+
column_name='is_opened',
|
181
|
+
attribute='is_opened',
|
182
|
+
widget=BooleanWidget(),
|
183
|
+
readonly=True
|
184
|
+
)
|
185
|
+
|
186
|
+
is_clicked = fields.Field(
|
187
|
+
column_name='is_clicked',
|
188
|
+
attribute='is_clicked',
|
189
|
+
widget=BooleanWidget(),
|
190
|
+
readonly=True
|
191
|
+
)
|
192
|
+
|
193
|
+
created_at = fields.Field(
|
194
|
+
column_name='created_at',
|
195
|
+
attribute='created_at',
|
196
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
197
|
+
)
|
198
|
+
|
199
|
+
sent_at = fields.Field(
|
200
|
+
column_name='sent_at',
|
201
|
+
attribute='sent_at',
|
202
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
203
|
+
)
|
204
|
+
|
205
|
+
opened_at = fields.Field(
|
206
|
+
column_name='opened_at',
|
207
|
+
attribute='opened_at',
|
208
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
209
|
+
)
|
210
|
+
|
211
|
+
clicked_at = fields.Field(
|
212
|
+
column_name='clicked_at',
|
213
|
+
attribute='clicked_at',
|
214
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
215
|
+
)
|
216
|
+
|
217
|
+
class Meta:
|
218
|
+
model = EmailLog
|
219
|
+
fields = (
|
220
|
+
'id',
|
221
|
+
'user_email',
|
222
|
+
'newsletter_title',
|
223
|
+
'campaign_subject',
|
224
|
+
'recipient',
|
225
|
+
'subject',
|
226
|
+
'status',
|
227
|
+
'status_display',
|
228
|
+
'is_opened',
|
229
|
+
'is_clicked',
|
230
|
+
'created_at',
|
231
|
+
'sent_at',
|
232
|
+
'opened_at',
|
233
|
+
'clicked_at',
|
234
|
+
'error_message',
|
235
|
+
)
|
236
|
+
export_order = fields
|
237
|
+
# No import - this is export only
|
238
|
+
|
239
|
+
def get_queryset(self):
|
240
|
+
"""Optimize queryset for export."""
|
241
|
+
return super().get_queryset().select_related('user', 'newsletter', 'campaign')
|
@@ -0,0 +1,183 @@
|
|
1
|
+
"""
|
2
|
+
Import/Export resources for Support app.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from import_export import resources, fields
|
6
|
+
from import_export.widgets import ForeignKeyWidget, DateTimeWidget, BooleanWidget
|
7
|
+
from django.contrib.auth import get_user_model
|
8
|
+
|
9
|
+
from ..models import Ticket, Message
|
10
|
+
|
11
|
+
User = get_user_model()
|
12
|
+
|
13
|
+
|
14
|
+
class TicketResource(resources.ModelResource):
|
15
|
+
"""Resource for exporting tickets (export only)."""
|
16
|
+
|
17
|
+
user_email = fields.Field(
|
18
|
+
column_name='user_email',
|
19
|
+
attribute='user__email',
|
20
|
+
readonly=True
|
21
|
+
)
|
22
|
+
|
23
|
+
user_full_name = fields.Field(
|
24
|
+
column_name='user_full_name',
|
25
|
+
attribute='user__get_full_name',
|
26
|
+
readonly=True
|
27
|
+
)
|
28
|
+
|
29
|
+
status_display = fields.Field(
|
30
|
+
column_name='status_display',
|
31
|
+
attribute='get_status_display',
|
32
|
+
readonly=True
|
33
|
+
)
|
34
|
+
|
35
|
+
messages_count = fields.Field(
|
36
|
+
column_name='messages_count',
|
37
|
+
readonly=True
|
38
|
+
)
|
39
|
+
|
40
|
+
last_message_text = fields.Field(
|
41
|
+
column_name='last_message_text',
|
42
|
+
readonly=True
|
43
|
+
)
|
44
|
+
|
45
|
+
last_message_at = fields.Field(
|
46
|
+
column_name='last_message_at',
|
47
|
+
readonly=True,
|
48
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
49
|
+
)
|
50
|
+
|
51
|
+
unanswered_messages_count = fields.Field(
|
52
|
+
column_name='unanswered_messages_count',
|
53
|
+
attribute='unanswered_messages_count',
|
54
|
+
readonly=True
|
55
|
+
)
|
56
|
+
|
57
|
+
created_at = fields.Field(
|
58
|
+
column_name='created_at',
|
59
|
+
attribute='created_at',
|
60
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
61
|
+
)
|
62
|
+
|
63
|
+
class Meta:
|
64
|
+
model = Ticket
|
65
|
+
fields = (
|
66
|
+
'uuid',
|
67
|
+
'user_email',
|
68
|
+
'user_full_name',
|
69
|
+
'subject',
|
70
|
+
'status',
|
71
|
+
'status_display',
|
72
|
+
'messages_count',
|
73
|
+
'last_message_text',
|
74
|
+
'last_message_at',
|
75
|
+
'unanswered_messages_count',
|
76
|
+
'created_at',
|
77
|
+
)
|
78
|
+
export_order = fields
|
79
|
+
# No import - this is export only
|
80
|
+
|
81
|
+
def dehydrate_messages_count(self, ticket):
|
82
|
+
"""Calculate messages count for export."""
|
83
|
+
return ticket.messages.count()
|
84
|
+
|
85
|
+
def dehydrate_last_message_text(self, ticket):
|
86
|
+
"""Get last message text for export."""
|
87
|
+
last_message = ticket.last_message
|
88
|
+
if last_message:
|
89
|
+
# Truncate long messages
|
90
|
+
text = last_message.text
|
91
|
+
return text[:100] + '...' if len(text) > 100 else text
|
92
|
+
return ''
|
93
|
+
|
94
|
+
def dehydrate_last_message_at(self, ticket):
|
95
|
+
"""Get last message timestamp for export."""
|
96
|
+
last_message = ticket.last_message
|
97
|
+
return last_message.created_at if last_message else None
|
98
|
+
|
99
|
+
def get_queryset(self):
|
100
|
+
"""Optimize queryset for export."""
|
101
|
+
return super().get_queryset().select_related('user').prefetch_related('messages')
|
102
|
+
|
103
|
+
|
104
|
+
class MessageResource(resources.ModelResource):
|
105
|
+
"""Resource for exporting messages (export only)."""
|
106
|
+
|
107
|
+
ticket_uuid = fields.Field(
|
108
|
+
column_name='ticket_uuid',
|
109
|
+
attribute='ticket__uuid',
|
110
|
+
readonly=True
|
111
|
+
)
|
112
|
+
|
113
|
+
ticket_subject = fields.Field(
|
114
|
+
column_name='ticket_subject',
|
115
|
+
attribute='ticket__subject',
|
116
|
+
readonly=True
|
117
|
+
)
|
118
|
+
|
119
|
+
sender_email = fields.Field(
|
120
|
+
column_name='sender_email',
|
121
|
+
attribute='sender__email',
|
122
|
+
readonly=True
|
123
|
+
)
|
124
|
+
|
125
|
+
sender_full_name = fields.Field(
|
126
|
+
column_name='sender_full_name',
|
127
|
+
attribute='sender__get_full_name',
|
128
|
+
readonly=True
|
129
|
+
)
|
130
|
+
|
131
|
+
is_from_author = fields.Field(
|
132
|
+
column_name='is_from_author',
|
133
|
+
attribute='is_from_author',
|
134
|
+
widget=BooleanWidget(),
|
135
|
+
readonly=True
|
136
|
+
)
|
137
|
+
|
138
|
+
is_from_staff = fields.Field(
|
139
|
+
column_name='is_from_staff',
|
140
|
+
readonly=True,
|
141
|
+
widget=BooleanWidget()
|
142
|
+
)
|
143
|
+
|
144
|
+
text_preview = fields.Field(
|
145
|
+
column_name='text_preview',
|
146
|
+
readonly=True
|
147
|
+
)
|
148
|
+
|
149
|
+
created_at = fields.Field(
|
150
|
+
column_name='created_at',
|
151
|
+
attribute='created_at',
|
152
|
+
widget=DateTimeWidget(format='%Y-%m-%d %H:%M:%S')
|
153
|
+
)
|
154
|
+
|
155
|
+
class Meta:
|
156
|
+
model = Message
|
157
|
+
fields = (
|
158
|
+
'uuid',
|
159
|
+
'ticket_uuid',
|
160
|
+
'ticket_subject',
|
161
|
+
'sender_email',
|
162
|
+
'sender_full_name',
|
163
|
+
'is_from_author',
|
164
|
+
'is_from_staff',
|
165
|
+
'text',
|
166
|
+
'text_preview',
|
167
|
+
'created_at',
|
168
|
+
)
|
169
|
+
export_order = fields
|
170
|
+
# No import - this is export only
|
171
|
+
|
172
|
+
def dehydrate_is_from_staff(self, message):
|
173
|
+
"""Check if message is from staff member."""
|
174
|
+
return message.sender.is_staff
|
175
|
+
|
176
|
+
def dehydrate_text_preview(self, message):
|
177
|
+
"""Get truncated text preview for export."""
|
178
|
+
text = message.text
|
179
|
+
return text[:200] + '...' if len(text) > 200 else text
|
180
|
+
|
181
|
+
def get_queryset(self):
|
182
|
+
"""Optimize queryset for export."""
|
183
|
+
return super().get_queryset().select_related('ticket', 'sender')
|
@@ -7,8 +7,12 @@ from django.urls import reverse
|
|
7
7
|
from django.shortcuts import redirect
|
8
8
|
from django.http import HttpRequest
|
9
9
|
from django.utils.translation import gettext_lazy as _
|
10
|
-
from .
|
11
|
-
from .
|
10
|
+
from import_export.admin import ExportMixin
|
11
|
+
from unfold.contrib.import_export.forms import ExportForm
|
12
|
+
|
13
|
+
from ..models import Ticket, Message
|
14
|
+
from .filters import TicketUserEmailFilter, TicketUserNameFilter, MessageSenderEmailFilter
|
15
|
+
from .resources import TicketResource, MessageResource
|
12
16
|
from django import forms
|
13
17
|
|
14
18
|
class MessageInline(TabularInline):
|
@@ -50,7 +54,10 @@ class MessageInline(TabularInline):
|
|
50
54
|
|
51
55
|
|
52
56
|
@admin.register(Ticket)
|
53
|
-
class TicketAdmin(ModelAdmin):
|
57
|
+
class TicketAdmin(ModelAdmin, ExportMixin):
|
58
|
+
# Export-only configuration
|
59
|
+
resource_class = TicketResource
|
60
|
+
export_form_class = ExportForm
|
54
61
|
list_display = ("user_avatar", "uuid_link", "subject", "status", "last_message_short", "last_message_ago", "chat_link", "created_at")
|
55
62
|
list_display_links = ("subject",)
|
56
63
|
list_editable = ("status",)
|
@@ -133,7 +140,7 @@ class TicketAdmin(ModelAdmin):
|
|
133
140
|
|
134
141
|
def chat_link(self, obj):
|
135
142
|
"""Display chat link button in list view."""
|
136
|
-
chat_url = reverse('ticket-chat', kwargs={'ticket_uuid': obj.uuid})
|
143
|
+
chat_url = reverse('cfg_support:ticket-chat', kwargs={'ticket_uuid': obj.uuid})
|
137
144
|
return format_html(
|
138
145
|
'<a href="{}" target="_blank" class="btn btn-sm btn-primary" '
|
139
146
|
'style="background: #0d6efd; color: white; padding: 4px 8px; '
|
@@ -154,11 +161,14 @@ class TicketAdmin(ModelAdmin):
|
|
154
161
|
def open_chat(self, request: HttpRequest, object_id: int):
|
155
162
|
"""Open the beautiful chat interface for this ticket."""
|
156
163
|
ticket = Ticket.objects.get(pk=object_id)
|
157
|
-
chat_url = reverse('ticket-chat', kwargs={'ticket_uuid': ticket.uuid})
|
164
|
+
chat_url = reverse('cfg_support:ticket-chat', kwargs={'ticket_uuid': ticket.uuid})
|
158
165
|
return redirect(chat_url)
|
159
166
|
|
160
167
|
@admin.register(Message)
|
161
|
-
class MessageAdmin(ModelAdmin):
|
168
|
+
class MessageAdmin(ModelAdmin, ExportMixin):
|
169
|
+
# Export-only configuration
|
170
|
+
resource_class = MessageResource
|
171
|
+
export_form_class = ExportForm
|
162
172
|
list_display = ("sender_avatar", "uuid", "ticket", "text_short", "created_at")
|
163
173
|
list_display_links = ("uuid", "ticket")
|
164
174
|
search_fields = ("uuid", "ticket__subject", "sender__username", "sender__email", "text")
|
@@ -197,7 +197,7 @@
|
|
197
197
|
submitBtn.disabled = true;
|
198
198
|
|
199
199
|
try {
|
200
|
-
const response = await fetch(`{% url 'send-message-ajax' ticket_uuid=ticket.uuid %}`, {
|
200
|
+
const response = await fetch(`{% url 'cfg_support: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
@@ -28,8 +28,7 @@ def get_django_cfg_urlpatterns() -> List[URLPattern]:
|
|
28
28
|
# Use BaseModule to check enabled applications
|
29
29
|
base_module = BaseModule()
|
30
30
|
|
31
|
-
#
|
32
|
-
# to maintain consistency and enable client generation
|
31
|
+
# Support URLs - needed for admin interface chat links
|
33
32
|
# if base_module.is_support_enabled():
|
34
33
|
# patterns.append(path('support/', include('django_cfg.apps.support.urls')))
|
35
34
|
#
|
Binary file
|