django-cfg 1.1.69__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.
Files changed (32) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/admin/__init__.py +2 -0
  3. django_cfg/apps/accounts/admin/activity.py +9 -2
  4. django_cfg/apps/accounts/admin/filters.py +42 -0
  5. django_cfg/apps/accounts/admin/registration_source.py +8 -1
  6. django_cfg/apps/accounts/admin/resources.py +271 -0
  7. django_cfg/apps/accounts/admin/twilio_response.py +7 -1
  8. django_cfg/apps/accounts/admin/user.py +9 -1
  9. django_cfg/apps/accounts/migrations/0005_twilioresponse.py +43 -0
  10. django_cfg/apps/accounts/models.py +83 -0
  11. django_cfg/apps/leads/admin/__init__.py +9 -0
  12. django_cfg/apps/leads/{admin.py → admin/leads_admin.py} +10 -2
  13. django_cfg/apps/leads/admin/resources.py +119 -0
  14. django_cfg/apps/newsletter/admin/__init__.py +12 -0
  15. django_cfg/apps/newsletter/{admin.py → admin/newsletter_admin.py} +20 -5
  16. django_cfg/apps/newsletter/admin/resources.py +241 -0
  17. django_cfg/apps/support/admin/__init__.py +10 -0
  18. django_cfg/apps/support/admin/resources.py +183 -0
  19. django_cfg/apps/support/{admin.py → admin/support_admin.py} +16 -6
  20. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  21. django_cfg/apps/urls.py +1 -2
  22. django_cfg/archive/django_sample.zip +0 -0
  23. django_cfg/core/config.py +1 -0
  24. django_cfg/integration.py +14 -16
  25. django_cfg/management/commands/migrator.py +49 -13
  26. {django_cfg-1.1.69.dist-info → django_cfg-1.1.70.dist-info}/METADATA +1 -1
  27. {django_cfg-1.1.69.dist-info → django_cfg-1.1.70.dist-info}/RECORD +32 -24
  28. /django_cfg/apps/newsletter/{admin_filters.py → admin/filters.py} +0 -0
  29. /django_cfg/apps/support/{admin_filters.py → admin/filters.py} +0 -0
  30. {django_cfg-1.1.69.dist-info → django_cfg-1.1.70.dist-info}/WHEEL +0 -0
  31. {django_cfg-1.1.69.dist-info → django_cfg-1.1.70.dist-info}/entry_points.txt +0 -0
  32. {django_cfg-1.1.69.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 .models import EmailLog, Newsletter, NewsletterSubscription, NewsletterCampaign
11
- from .admin_filters import UserEmailFilter, UserNameFilter, HasUserFilter, EmailOpenedFilter, EmailClickedFilter
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,10 @@
1
+ """
2
+ Admin configuration for Support app.
3
+ """
4
+
5
+ from .support_admin import TicketAdmin, MessageAdmin
6
+
7
+ __all__ = [
8
+ 'TicketAdmin',
9
+ 'MessageAdmin',
10
+ ]
@@ -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 .models import Ticket, Message
11
- from .admin_filters import TicketUserEmailFilter, TicketUserNameFilter, MessageSenderEmailFilter
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
- # All business logic apps are handled by Django Revolution zones
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