accrete 0.0.142__py3-none-any.whl → 0.0.144__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 (40) hide show
  1. accrete/contrib/log/admin.py +1 -1
  2. accrete/contrib/system_mail/tasks.py +4 -5
  3. accrete/contrib/ui/__init__.py +16 -14
  4. accrete/contrib/ui/filter.py +22 -13
  5. accrete/contrib/ui/response.py +540 -0
  6. accrete/contrib/ui/static/css/accrete.css +69 -12
  7. accrete/contrib/ui/static/css/accrete.css.map +1 -1
  8. accrete/contrib/ui/static/css/accrete.scss +370 -300
  9. accrete/contrib/ui/templates/ui/content.html +0 -0
  10. accrete/contrib/ui/templates/ui/content_right.html +2 -2
  11. accrete/contrib/ui/templates/ui/detail.html +11 -0
  12. accrete/contrib/ui/templates/ui/filter/filter.html +5 -5
  13. accrete/contrib/ui/templates/ui/filter/query_tags.html +1 -1
  14. accrete/contrib/ui/templates/ui/form_error.html +1 -1
  15. accrete/contrib/ui/templates/ui/layout.html +69 -64
  16. accrete/contrib/ui/templates/ui/list.html +21 -19
  17. accrete/contrib/ui/templates/ui/list_update.html +9 -3
  18. accrete/contrib/ui/templates/ui/message.html +1 -1
  19. accrete/contrib/ui/templates/ui/modal.html +41 -32
  20. accrete/contrib/ui/templates/ui/table_row_update.html +2 -2
  21. accrete/contrib/ui/templates/ui/templatetags/field.html +19 -6
  22. accrete/contrib/ui/templates/ui/widgets/model_search_select.html +7 -7
  23. accrete/contrib/ui/templates/ui/widgets/model_search_select_multi.html +3 -3
  24. accrete/contrib/ui/templatetags/ui.py +24 -2
  25. accrete/contrib/user/templates/user/password_forgotten.html +1 -0
  26. accrete/contrib/user/templates/user/user_preferences.html +43 -0
  27. accrete/contrib/user/views.py +36 -37
  28. accrete/migrations/0008_alter_member_access_groups_and_more.py +23 -0
  29. accrete/utils/__init__.py +1 -0
  30. accrete/utils/forms.py +48 -28
  31. accrete/utils/views.py +1 -2
  32. accrete/views.py +2 -3
  33. {accrete-0.0.142.dist-info → accrete-0.0.144.dist-info}/METADATA +2 -2
  34. {accrete-0.0.142.dist-info → accrete-0.0.144.dist-info}/RECORD +36 -34
  35. accrete/contrib/ui/context.py +0 -123
  36. accrete/contrib/ui/static/css/.sass-cache/15adf1eed05371361b08787c918a7f18fc15be79/accrete.scssc +0 -0
  37. accrete/contrib/ui/utils.py +0 -77
  38. accrete/contrib/user/templates/user/user_form.html +0 -58
  39. {accrete-0.0.142.dist-info → accrete-0.0.144.dist-info}/WHEEL +0 -0
  40. {accrete-0.0.142.dist-info → accrete-0.0.144.dist-info}/licenses/LICENSE +0 -0
@@ -44,4 +44,4 @@ class LogAdmin(admin.ModelAdmin):
44
44
 
45
45
 
46
46
  admin.site.register(models.LogConfig, LogConfigAdmin)
47
- admin.site.register(models.Log, LogAdmin)
47
+ admin.site.register(models.Log, LogAdmin)
@@ -36,8 +36,7 @@ def run_mail_queue():
36
36
  _logger.error(f'Failed to send system mail\n{error_str}')
37
37
  email.error = error_str
38
38
  email.save()
39
- continue
40
-
41
- email.sent = True
42
- email.error = None
43
- email.save()
39
+ else:
40
+ email.sent = True
41
+ email.error = None
42
+ email.save()
@@ -1,17 +1,19 @@
1
1
  from . import widgets
2
2
  from .filter import Filter
3
- from . context import (
4
- Context,
5
- ListContext,
6
- ListUpdateContext,
7
- TableContext,
8
- TableRowContext,
9
- ModalContext,
10
- OobContext,
11
- MessageContext
12
- )
13
- from .utils import (
14
- modal_response,
15
- detail_response,
16
- add_trigger
3
+ from .response import (
4
+ Response,
5
+ WindowResponse,
6
+ ListResponse,
7
+ ListEntryResponse,
8
+ TableResponse,
9
+ TableRowResponse,
10
+ DetailResponse,
11
+ ModalResponse,
12
+ OobResponse,
13
+ TriggerResponse,
14
+ ClientTrigger,
15
+ search_select_response,
16
+ message_response,
17
+ add_trigger,
18
+ update
17
19
  )
@@ -46,12 +46,21 @@ class Filter:
46
46
  TYPES_TIME = ['TimeField']
47
47
 
48
48
  def __init__(
49
- self, model, query: QueryDict, default_lookup: str = None
49
+ self,
50
+ model,
51
+ query_dict: QueryDict,
52
+ default_lookup: str = None,
53
+ query: Q = None,
54
+ select_related: list[str] = None,
55
+ prefetch_related: list[str] = None
50
56
  ):
51
57
  self.model = model
52
- self.query = query
58
+ self.query_dict = query_dict
53
59
  self.model_name = f'{self.model._meta.app_label}.{self.model._meta.model_name}'
54
- seen_models = query.get('models', '').split(',')
60
+ self.query = query or Q()
61
+ self.select_related = select_related
62
+ self.prefetch_related = prefetch_related
63
+ seen_models = query_dict.get('models', '').split(',')
55
64
  self.seen_models = [
56
65
  model for model in seen_models
57
66
  if model and model != self.model_name
@@ -67,14 +76,14 @@ class Filter:
67
76
  ) -> paginator.Page:
68
77
  return page_from_querystring(
69
78
  self.model,
70
- self.query,
71
- select_related=select_related,
72
- prefetch_related=prefetch_related,
73
- query=query
79
+ self.query_dict,
80
+ select_related=select_related or self.select_related,
81
+ prefetch_related=prefetch_related or self.prefetch_related,
82
+ query=query or self.query
74
83
  )
75
84
 
76
85
  def get_queryset(self) -> QuerySet:
77
- return filter_from_querystring(self.model, self.query)
86
+ return filter_from_querystring(self.model, self.query_dict)
78
87
 
79
88
  def query_params(self):
80
89
  fields = filter(
@@ -82,7 +91,7 @@ class Filter:
82
91
  self.model._meta.get_fields()
83
92
  )
84
93
  params = []
85
- path = self.query.get('path', '')
94
+ path = self.query_dict.get('path', '')
86
95
  for field in fields:
87
96
  field_params = self._get_field_params(field)
88
97
  if not field.is_relation or field_params['model_name'] not in self.seen_models:
@@ -115,7 +124,7 @@ class Filter:
115
124
  if not lookup:
116
125
  ctx = {
117
126
  'verbose_lookup': str(_('Select an attribute')),
118
- 'query_dict': self.query,
127
+ 'query_dict': self.query_dict,
119
128
  'model_name': self.model_name
120
129
  }
121
130
  return render_to_string('ui/filter/query_input.html', ctx)
@@ -133,14 +142,14 @@ class Filter:
133
142
  'lookup': prefix + lookup,
134
143
  'input': input_params,
135
144
  'lookup_operator': lookup_operator,
136
- 'query_dict': self.query,
145
+ 'query_dict': self.query_dict,
137
146
  'model_name': self.model_name
138
147
  }
139
148
  return render_to_string('ui/filter/query_input.html', ctx)
140
149
 
141
150
  def query_tags(self, query: list | dict = None, operator: str = None):
142
151
  if not query:
143
- query = json.loads(self.query.get('q', '[]'))
152
+ query = json.loads(self.query_dict.get('q', '[]'))
144
153
  if isinstance(query, dict):
145
154
  query = [query]
146
155
  operator = operator or '&'
@@ -291,7 +300,7 @@ class Filter:
291
300
  return str(labels[lookup])
292
301
 
293
302
  def _get_field_params(self, field):
294
- path = self.query.get('path', '')
303
+ path = self.query_dict.get('path', '')
295
304
  label = ''
296
305
  name = field.name
297
306
  if path:
@@ -0,0 +1,540 @@
1
+ import re
2
+ import ast
3
+ import json
4
+ from dataclasses import dataclass
5
+
6
+ from django.core import paginator
7
+ from django.db.models import Model
8
+ from django.http import HttpResponse
9
+ from django.template.loader import render_to_string
10
+ from django.utils.translation import gettext_lazy as _
11
+ from accrete.contrib.ui import Filter
12
+
13
+
14
+ class Response:
15
+
16
+ def __init__(self, *, template: str, context: dict):
17
+ self.template = template
18
+ self.context = context
19
+
20
+ @staticmethod
21
+ def add_trigger(response):
22
+ pass
23
+
24
+ def render(self, request) -> str:
25
+ return render_to_string(
26
+ template_name=self.template, context=self.context, request=request
27
+ )
28
+
29
+ def response(self, request, extra_content: str = None, replace_body: bool = False) -> HttpResponse:
30
+ extra_content = extra_content or ''
31
+ res = HttpResponse(content=(
32
+ self.render(request)
33
+ + render_to_string('ui/message.html', request=request)
34
+ + extra_content
35
+ ))
36
+ self.add_trigger(res)
37
+ if replace_body:
38
+ res.headers['HX-Retarget'] = 'body'
39
+ res.headers['HX-Reswap'] = 'innerHTML'
40
+ res.headers['HX-Push-Url'] = request.path
41
+ return res
42
+
43
+
44
+ class OobResponse(Response):
45
+
46
+ oob_template = 'ui/oob.html'
47
+
48
+ def __init__(self, *, template: str, context: dict, swap: str, tag: str = 'div'):
49
+ super().__init__(template=self.oob_template, context=context)
50
+ self.context.update({'oob': {
51
+ 'template': template,
52
+ 'swap': swap,
53
+ 'tag': tag
54
+ }})
55
+
56
+
57
+ class WindowResponse(Response):
58
+
59
+ base_template = 'ui/layout.html'
60
+
61
+ def __init__(
62
+ self, *,
63
+ title: str,
64
+ context: dict,
65
+ overview_template: str = None,
66
+ header_template: str = None,
67
+ panel_template: str = None,
68
+ is_centered: bool = False
69
+ ):
70
+ super().__init__(template=self.base_template, context=context)
71
+ self.panel_template = panel_template
72
+ if 'has_panel' not in self.context.keys():
73
+ self.context.update(has_panel=self._has_panel())
74
+ self.context.update({
75
+ 'title': title,
76
+ 'overview_template': overview_template,
77
+ 'header_template': header_template,
78
+ 'panel_template': panel_template,
79
+ 'is_centered': is_centered
80
+ })
81
+
82
+ def _has_panel(self):
83
+ return bool(self.panel_template)
84
+
85
+ def response(self, request, extra_content: str = None, replace_body: bool = True) -> HttpResponse:
86
+ return super().response(request, extra_content, replace_body)
87
+
88
+
89
+ class ListResponse(WindowResponse):
90
+
91
+ def __init__(
92
+ self, *,
93
+ title: str,
94
+ context: dict,
95
+ list_entry_template: str = None,
96
+ page: paginator.Page = None,
97
+ ui_filter: Filter = None,
98
+ endless_scroll: bool = True,
99
+ header_template: str = None,
100
+ panel_template: str = None,
101
+ column_count: int = 1,
102
+ column_height: str = '150px',
103
+ overview_template: str = 'ui/list.html'
104
+ ):
105
+ assert page is not None or ui_filter is not None, _(
106
+ 'Argument page or ui_filter must be supplied'
107
+ )
108
+ self.ui_filter = ui_filter
109
+ self.overview_template = overview_template
110
+ super().__init__(
111
+ title=title,
112
+ context=context,
113
+ overview_template=self.overview_template,
114
+ header_template=header_template,
115
+ panel_template=panel_template,
116
+ )
117
+ if ui_filter and not page:
118
+ page = ui_filter.get_page()
119
+ self.context.update({
120
+ 'list_entry_template': list_entry_template,
121
+ 'page': page,
122
+ 'ui_filter': ui_filter,
123
+ 'endless_scroll': endless_scroll,
124
+ 'column_count': column_count,
125
+ 'column_height': column_height
126
+ })
127
+
128
+ def _has_panel(self):
129
+ return bool(self.panel_template or self.ui_filter)
130
+
131
+ def response(self, request, extra_content: str = None, replace_body: bool = False) -> HttpResponse:
132
+ return super().response(request, extra_content, replace_body)
133
+
134
+
135
+ class ListEntryResponse(Response):
136
+
137
+ base_template = 'ui/list_update.html'
138
+
139
+ def __init__(
140
+ self, *,
141
+ instance: Model,
142
+ list_entry_template: str,
143
+ context: dict = None,
144
+ page: paginator.Page,
145
+ is_new: bool,
146
+ column_count: int = 1,
147
+ column_height: str = '150px',
148
+ ):
149
+ self.page = page
150
+ super().__init__(template=self.base_template, context=context or {})
151
+ self.context.update({
152
+ 'instance': instance,
153
+ 'list_entry_template': list_entry_template,
154
+ 'is_new': is_new,
155
+ 'column_count': column_count,
156
+ 'column_height': column_height
157
+ })
158
+
159
+ def render(self, request) -> str:
160
+ res = super().render(request)
161
+ if self.page:
162
+ pagination_update = OobResponse(
163
+ template='ui/layout.html#pagination',
164
+ swap='innerHTML:#pagination',
165
+ context=dict(page=self.page)
166
+ ).render(request)
167
+ res += pagination_update
168
+ return res
169
+
170
+
171
+ class TableResponse(WindowResponse):
172
+
173
+ def __init__(
174
+ self, *,
175
+ title: str,
176
+ context: dict,
177
+ object_label: str,
178
+ fields: list[str],
179
+ footer: dict = None,
180
+ page: paginator.Page = None,
181
+ ui_filter: Filter = None,
182
+ endless_scroll: bool = True,
183
+ header_template: str = None,
184
+ panel_template: str = None,
185
+ overview_template: str = 'ui/table.html'
186
+ ):
187
+ assert page is not None or ui_filter is not None, _(
188
+ 'Argument page or ui_filter must be supplied'
189
+ )
190
+ self.ui_filter = ui_filter
191
+ self.overview_template = overview_template
192
+ super().__init__(
193
+ title=title,
194
+ context=context,
195
+ overview_template=self.overview_template,
196
+ header_template=header_template,
197
+ panel_template=panel_template
198
+ )
199
+ if ui_filter and not page:
200
+ page = ui_filter.get_page()
201
+ self.context.update({
202
+ 'page': page,
203
+ 'ui_filter': ui_filter,
204
+ 'endless_scroll': endless_scroll,
205
+ 'fields': fields,
206
+ 'object_label': object_label,
207
+ 'footer': footer
208
+ })
209
+
210
+ def _has_panel(self):
211
+ return bool(self.panel_template or self.ui_filter)
212
+
213
+ def response(self, request, extra_content: str = None, replace_body: bool = False) -> HttpResponse:
214
+ return super().response(request, extra_content, replace_body)
215
+
216
+
217
+ class TableRowResponse(Response):
218
+
219
+ base_template = 'ui/table_row_update.html'
220
+
221
+ def __init__(
222
+ self, *,
223
+ instance: Model,
224
+ fields: list[str],
225
+ footer: dict = None,
226
+ page: paginator.Page = None,
227
+ ):
228
+ self.page = page
229
+ context = {
230
+ 'instance': instance,
231
+ 'fields': fields,
232
+ 'footer': footer,
233
+ }
234
+ super().__init__(template=self.base_template, context=context)
235
+
236
+ def render(self, request) -> str:
237
+ res = super().render(request)
238
+ if self.page:
239
+ pagination_update = OobResponse(
240
+ template='ui/layout.html#pagination',
241
+ swap='innerHTML:#pagination',
242
+ context=dict(page=self.page)
243
+ ).render(request)
244
+ res += pagination_update
245
+ return res
246
+
247
+
248
+ class DetailResponse(Response):
249
+
250
+ base_template = 'ui/detail.html'
251
+
252
+ def __init__(
253
+ self, *,
254
+ context: dict,
255
+ detail_header_template: str = None,
256
+ detail_data_template: str = None
257
+ ):
258
+ super().__init__(template=self.base_template, context=context)
259
+ self.context.update({
260
+ 'detail_header_template': detail_header_template,
261
+ 'detail_data_template': detail_data_template
262
+ })
263
+
264
+ @staticmethod
265
+ def add_trigger(response):
266
+ add_trigger(response, 'activate-detail')
267
+
268
+
269
+ class ModalResponse(Response):
270
+
271
+ def __init__(
272
+ self, *,
273
+ modal_id: str,
274
+ template: str,
275
+ context: dict,
276
+ title: str = None,
277
+ is_update: bool = False,
278
+ is_blocking: bool = False,
279
+ modal_width: str = None
280
+
281
+ ):
282
+ super().__init__(template=template, context=context)
283
+ self.context.update({
284
+ 'title': title,
285
+ 'modal_id': re.sub(r'[^A-Za-z-]+', '', modal_id).strip('-'),
286
+ 'is_update': is_update,
287
+ 'is_blocking': is_blocking,
288
+ 'modal_width': modal_width
289
+ })
290
+
291
+
292
+ @dataclass
293
+ class ClientTrigger:
294
+
295
+ trigger: dict | str
296
+ header: str = 'HX-Trigger'
297
+
298
+
299
+ class TriggerResponse:
300
+
301
+ def __init__(self, trigger: list[ClientTrigger]):
302
+ self.trigger = trigger
303
+
304
+ def response(self):
305
+ res = HttpResponse()
306
+ res.headers['HX-Reswap'] = 'None'
307
+ for trigger in self.trigger:
308
+ add_trigger(res, trigger.trigger, trigger.header)
309
+ return res
310
+
311
+
312
+ # @dataclass(kw_only=True)
313
+ # class Response:
314
+ #
315
+ # template: str
316
+ # context: dict = field(default_factory=dict)
317
+ #
318
+ # base_template = None
319
+ #
320
+ # def __post_init__(self):
321
+ # for key, value in self.context.items():
322
+ # setattr(self, key, value)
323
+ # if self.base_template is None:
324
+ # self.base_template = self.template
325
+ #
326
+ # def dict(self):
327
+ # return {
328
+ # attr: getattr(self, attr, None) for attr
329
+ # in filter(lambda x: not x.startswith('_'), self.__dict__)
330
+ # }
331
+ #
332
+ # @staticmethod
333
+ # def add_trigger(response):
334
+ # pass
335
+ #
336
+ # def render(self, request) -> str:
337
+ # return render_to_string(
338
+ # template_name=self.base_template, context=self.dict(), request=request
339
+ # )
340
+ #
341
+ # def response(self, request, extra_content: str = None) -> HttpResponse:
342
+ # extra_content = extra_content or ''
343
+ # res = HttpResponse(content=(
344
+ # self.render(request)
345
+ # + render_to_string('ui/message.html', self.dict(), request)
346
+ # + extra_content
347
+ # ))
348
+ # self.add_trigger(res)
349
+ # return res
350
+ #
351
+ #
352
+ # @dataclass(kw_only=True)
353
+ # class OobResponse(Response):
354
+ #
355
+ # swap: str
356
+ # tag: str = 'div'
357
+ # id: str = None
358
+ #
359
+ # base_template = 'ui/oob.html'
360
+ #
361
+ # def dict(self) -> dict:
362
+ # attributes = filter(
363
+ # lambda x: not x.startswith('_') and x not in ['template', 'swap', 'tag'],
364
+ # self.__dict__
365
+ # )
366
+ # res = {attr: getattr(self, attr, None) for attr in attributes}
367
+ # res.update({'oob': {
368
+ # 'template': self.template,
369
+ # 'id': self.id,
370
+ # 'swap': self.swap,
371
+ # 'tag': self.tag
372
+ # }})
373
+ # return res
374
+ #
375
+ # # def render(self, request) -> str:
376
+ # # return render_to_string(
377
+ # # template_name='ui/oob.html', context=self.dict(), request=request
378
+ # # )
379
+ #
380
+ #
381
+ # @dataclass(kw_only=True)
382
+ # class WindowResponse(Response):
383
+ #
384
+ # title: str = ''
385
+ # panel_template: str = None
386
+ # overview_header_template: str = None
387
+ # overview_data_template: str = None
388
+ #
389
+ # base_template = 'ui/layout.html'
390
+ #
391
+ # def __post_init__(self):
392
+ # if 'has_panel' not in self.context.keys():
393
+ # self.context.update(has_panel=self._has_panel())
394
+ # super().__post_init__()
395
+ # if self.template:
396
+ # self.base_template = self.template
397
+ #
398
+ # def _has_panel(self) -> bool:
399
+ # return bool(self.panel_template)
400
+ #
401
+ #
402
+ # @dataclass(kw_only=True)
403
+ # class PageResponse(WindowResponse):
404
+ #
405
+ # page: paginator.Page = None
406
+ # ui_filter: Filter = None
407
+ # endless_scroll: bool = True
408
+ #
409
+ # def __post_init__(self):
410
+ # assert self.page is not None or self.ui_filter is not None, _(
411
+ # 'Argument page or ui_filter must be supplied'
412
+ # )
413
+ # if self.ui_filter and not self.page:
414
+ # self.page = self.ui_filter.get_page()
415
+ # super().__post_init__()
416
+ #
417
+ # def _has_panel(self) -> bool:
418
+ # return bool(self.panel_template or self.ui_filter)
419
+ #
420
+ #
421
+ # @dataclass(kw_only=True)
422
+ # class ListResponse(PageResponse):
423
+ #
424
+ # list_entry_template: str = None
425
+ # column_count: int = 1
426
+ # column_height: str = '150px'
427
+ # overview_data_template:str = 'ui/list.html'
428
+ #
429
+ #
430
+ # @dataclass(kw_only=True)
431
+ # class ListEntryResponse(Response):
432
+ #
433
+ # object: Model
434
+ # list_entry_template: str
435
+ # page: paginator.Page = None
436
+ # new_entry: bool = False
437
+ # template: str = 'ui/list_update.html'
438
+ #
439
+ # def render(self, request) -> str:
440
+ # res = super().render(request)
441
+ # if self.page:
442
+ # pagination_update = OobResponse(
443
+ # template='ui/layout.html#pagination',
444
+ # swap='innerHTML:#pagination',
445
+ # context=dict(page=self.page)
446
+ # ).render(request)
447
+ # res += pagination_update
448
+ # return res
449
+ #
450
+ #
451
+ # @dataclass(kw_only=True)
452
+ # class TableResponse(PageResponse):
453
+ #
454
+ # object_label: str
455
+ # fields: list[str]
456
+ # footer: dict = field(default_factory=dict)
457
+ # template = 'ui/table.html'
458
+ #
459
+ #
460
+ # @dataclass(kw_only=True)
461
+ # class TableRowResponse(Response):
462
+ #
463
+ # object: Model
464
+ # fields: list[str]
465
+ # footer: dict = field(default_factory=dict)
466
+ # page: paginator.Page = None
467
+ #
468
+ # base_template = 'ui/table_row_update.html'
469
+ #
470
+ #
471
+ # @dataclass(kw_only=True)
472
+ # class DetailResponse(Response):
473
+ #
474
+ # detail_data_template: str = None
475
+ # detail_header_template: str = None
476
+ #
477
+ # base_template = 'ui/detail.html'
478
+ #
479
+ # @staticmethod
480
+ # def add_trigger(response):
481
+ # add_trigger(response, 'activate-detail')
482
+ #
483
+ #
484
+ # @dataclass(kw_only=True)
485
+ # class ModalResponse(Response):
486
+ #
487
+ # title: str
488
+ # modal_id: str
489
+ # template: str
490
+ # modal_update: bool = False
491
+ # blocking: bool = False
492
+ # modal_width: str = None
493
+ #
494
+ # def __post_init__(self):
495
+ # super().__post_init__()
496
+ # self.modal_id = re.sub(r'[^A-Za-z-]+', '', self.modal_id).strip('-')
497
+
498
+
499
+ def search_select_response(queryset) -> HttpResponse:
500
+ return HttpResponse(render_to_string(
501
+ 'ui/widgets/model_search_select_options.html',
502
+ {'options': queryset}
503
+ ))
504
+
505
+
506
+ def message_response(request, persistent: bool = False):
507
+ return HttpResponse(content=(render_to_string(
508
+ 'ui/message.html', context={'persistent': persistent}, request=request
509
+ )))
510
+
511
+
512
+ def add_trigger(
513
+ response: HttpResponse,
514
+ trigger: dict | str,
515
+ header: str = 'HX-Trigger'
516
+ ) -> HttpResponse:
517
+ if isinstance(trigger, str):
518
+ trigger = {trigger: ''}
519
+ res_trigger = response.headers.get(header)
520
+ if not res_trigger:
521
+ response.headers[header] = json.dumps(trigger)
522
+ return response
523
+ try:
524
+ res_trigger = ast.literal_eval(response.headers.get(header, '{}'))
525
+ except SyntaxError:
526
+ res_trigger = {response.headers[header]: ''}
527
+ res_trigger.update(trigger)
528
+ response.headers[header] = json.dumps(res_trigger)
529
+ return response
530
+
531
+
532
+ def update(request, ui_responses: list[Response]) -> HttpResponse:
533
+ response = HttpResponse()
534
+ content = ''
535
+ for res in ui_responses:
536
+ content += res.render(request)
537
+ res.add_trigger(response)
538
+ content += render_to_string('ui/message.html', request=request)
539
+ response.content = content
540
+ return response