accrete 0.0.36__py3-none-any.whl → 0.0.38__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.
@@ -15,162 +15,164 @@
15
15
  <link rel="stylesheet" type="text/css" href="{% static "css/icons.css" %}">
16
16
  {% endblock %}
17
17
  {% block htmx %}
18
- <script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
18
+ <script src="{% static "js/htmx.min.js" %}" defer type="text/javascript"></script>
19
19
  {% endblock %}
20
20
  {% block script %}{% endblock %}
21
- <title>{% block title %}{{ title }}{% endblock %}</title>
21
+ {% block title_tag %}<title>{% block title %}{{ title }}{% endblock %}</title>{% endblock %}
22
22
  {% endblock %}
23
23
  </head>
24
24
 
25
25
 
26
26
  <body {% block body_attrs %}hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-boost="true" hx-history="false" {% endblock %}>
27
- {% block navbar %}
28
- <nav id="navbar" class="navbar is-success is-fixed-top" role="navigation" aria-label="main navigation">
29
- <div class="navbar-brand">
30
- {% block navbar_brand %}
31
- <div class="navbar-item is-unselectable">{{ request.tenant.name }}</div>
32
- {% endblock %}
33
- <a id="navbar-burger" role="button" class="navbar-burger"
34
- aria-label="menu" aria-expanded="false"
35
- onclick="this.classList.toggle('is-active'); document.getElementById('navbar-menu').classList.toggle('is-active');"
36
- >
37
- {# Display hamburger menu on mobile #}
38
- <span aria-hidden="true"></span>
39
- <span aria-hidden="true"></span>
40
- <span aria-hidden="true"></span>
41
- </a>
42
- </div>
43
-
44
- <div id="navbar-menu" class="navbar-menu is-fixed-top">
45
- <div class="navbar-start">
46
- {% block navbar_start %}
47
- {% combine_templates 'accrete_menu.html' %}
48
- {% endblock %}
49
- </div>
50
-
51
- <div class="navbar-end">
52
- {% block navbar_end %}
53
- <div class="navbar-item has-dropdown is-hoverable" onclick="this.classList.toggle('is-active');">
54
- <a class="navbar-link is-arrowless {% if request.member.name %}is-size-7 has-text-centered-desktop{% endif %}">
55
- {% if request.member.name %}
56
- {{ request.member }}<br>{{ user }}
57
- {% else %}
58
- {{ user }}
59
- {% endif %}
60
- </a>
61
- <div id="navbar-end-dropdown" class="navbar-dropdown is-right">
62
- {% combine_templates 'accrete_navbar_end_dropdown.html' request=request %}
63
- </div>
64
- </div>
27
+ {% block body %}
28
+ {% block navbar %}
29
+ <nav id="navbar" class="navbar is-success is-fixed-top" role="navigation" aria-label="main navigation">
30
+ <div class="navbar-brand">
31
+ {% block navbar_brand %}
32
+ <div class="navbar-item is-unselectable">{{ request.tenant.name }}</div>
65
33
  {% endblock %}
34
+ <a id="navbar-burger" role="button" class="navbar-burger"
35
+ aria-label="menu" aria-expanded="false"
36
+ onclick="this.classList.toggle('is-active'); document.getElementById('navbar-menu').classList.toggle('is-active');"
37
+ >
38
+ {# Display hamburger menu on mobile #}
39
+ <span aria-hidden="true"></span>
40
+ <span aria-hidden="true"></span>
41
+ <span aria-hidden="true"></span>
42
+ </a>
66
43
  </div>
67
- </div>
68
- </nav>
69
- {% endblock %}
70
44
 
71
- <div id="main" class="is-flex is-flex-direction-row">
72
- <div class="side-panel is-hidden-touch is-hidden-desktop-only is-hidden-widescreen-only p-0">
73
- <nav class="panel is-shadowless pb-5" style="height: 100%; overflow-y: auto; position: sticky; top: 0">
74
- {% if list_page %}
75
- <div class="panel-block pb-0">
76
- {% include 'ui/partials/pagination_list.html' %}
77
- </div>
78
- {% endif %}
79
- {% if detail_page %}
80
- <div class="panel-block pb-0">
81
- {% include 'ui/partials/pagination_detail.html' %}
45
+ <div id="navbar-menu" class="navbar-menu is-fixed-top">
46
+ <div class="navbar-start">
47
+ {% block navbar_start %}
48
+ {% combine_templates 'accrete_menu.html' %}
49
+ {% endblock %}
82
50
  </div>
83
- {% endif %}
84
- <div id="panel-actions">
85
- {% for action in actions %}
86
- <div class="panel-block pb-0">
87
- {% if action.submit %}
88
- <button class="button is-fullwidth {{ action.class_list|join:' ' }}"
89
- type="submit" form={{ action.form_id }} value="{{ action.name }}"
90
- {{ action.attrs_str }} style="padding-right: 0.7em;"
91
- >
92
- <div class="side-panel-action-content"><span>{{ action.name }}</span>
93
- {% if action.icon %}<i class="{{ action.icon.value }}"></i>{% endif %}
94
- </div>
95
- </button>
96
- {% else %}
97
- <a class="button is-fullwidth {{ action.class_list|join:' ' }}"
98
- {% if action.url %}
99
- {{ action.method.value }}="{{ action.url }}"
51
+
52
+ <div class="navbar-end">
53
+ {% block navbar_end %}
54
+ <div class="navbar-item has-dropdown is-hoverable" onclick="this.classList.toggle('is-active');">
55
+ <a class="navbar-link is-arrowless {% if request.member.name %}is-size-7 has-text-centered-desktop{% endif %}">
56
+ {% if request.member.name %}
57
+ {{ request.member }}<br>{{ user }}
58
+ {% else %}
59
+ {{ user }}
100
60
  {% endif %}
101
- {{ action.attrs_str }} style="padding-right: 0.7em;"
102
- >
103
- <div class="side-panel-action-content">
104
- <span>{{ action.name }}</span>
105
- {% if action.icon %}<i class="{{ action.icon.value }}"></i>{% endif %}
106
- </div>
107
61
  </a>
108
- {% endif %}
109
- </div>
110
- {% endfor %}
111
- </div>
112
- {% block filter %}
113
- {% if filter %}
114
- <div id="filter-panel" class="mt-2">
115
- <div class="panel-list mx-3 mb-1 has-text-centered" style="border-bottom: 1px #ccc solid">
116
- <span>{% translate 'Filter' %}</span>
62
+ <div id="navbar-end-dropdown" class="navbar-dropdown is-right">
63
+ {% combine_templates 'accrete_navbar_end_dropdown.html' request=request %}
64
+ </div>
117
65
  </div>
118
- {% include 'ui/partials/filter.html' %}
119
- </div>
120
- {% endif %}
121
- {% endblock %}
66
+ {% endblock %}
67
+ </div>
68
+ </div>
122
69
  </nav>
123
- </div>
70
+ {% endblock %}
124
71
 
125
- <div class="is-flex-grow-1" style="overflow-x: auto">
126
- <div class="is-flex is-flex-direction-column" style="height: 100%; overflow: hidden">
127
- <div>
128
- {% include 'ui/partials/header.html' %}
129
- </div>
130
- <div id="content" class="px-3 pb-4" style="overflow: auto; flex-grow: 1">
131
- {% block messages %}{% endblock %}
132
- {% block content %}{% endblock %}
133
- </div>
134
- <div>
135
- {% if list_page %}
136
- <div class="level is-hidden-tablet is-align-self-flex-start m-3">
137
- <div class="level-item is-align-content-flex-start">
138
- {% include 'ui/partials/pagination_list.html' %}
139
- </div>
72
+ <div id="main" class="is-flex is-flex-direction-row">
73
+ <div class="side-panel is-hidden-touch is-hidden-desktop-only is-hidden-widescreen-only p-0">
74
+ <nav class="panel is-shadowless pb-5" style="height: 100%; overflow-y: auto; position: sticky; top: 0">
75
+ {% if list_page.paginator %}
76
+ <div class="panel-block pb-0">
77
+ {% include 'ui/partials/pagination_list.html' %}
140
78
  </div>
141
79
  {% endif %}
142
80
  {% if detail_page %}
143
- <div class="level is-hidden-tablet is-align-self-flex-start m-3">
144
- <div class="level-item is-align-content-flex-start">
145
- {% include 'ui/partials/pagination_detail.html' %}
146
- </div>
81
+ <div class="panel-block pb-0">
82
+ {% include 'ui/partials/pagination_detail.html' %}
147
83
  </div>
148
84
  {% endif %}
85
+ <div id="panel-actions">
86
+ {% for action in actions %}
87
+ <div class="panel-block pb-0">
88
+ {% if action.submit %}
89
+ <button class="button is-fullwidth {{ action.class_list|join:' ' }}"
90
+ type="submit" form={{ action.form_id }} value="{{ action.name }}"
91
+ {{ action.attrs_str }} style="padding-right: 0.7em;"
92
+ >
93
+ <div class="side-panel-action-content"><span>{{ action.name }}</span>
94
+ {% if action.icon %}<i class="{{ action.icon.value }}"></i>{% endif %}
95
+ </div>
96
+ </button>
97
+ {% else %}
98
+ <a class="button is-fullwidth {{ action.class_list|join:' ' }}"
99
+ {% if action.url %}
100
+ {{ action.method.value }}="{{ action.url }}"
101
+ {% endif %}
102
+ {{ action.attrs_str }} style="padding-right: 0.7em;"
103
+ >
104
+ <div class="side-panel-action-content">
105
+ <span>{{ action.name }}</span>
106
+ {% if action.icon %}<i class="{{ action.icon.value }}"></i>{% endif %}
107
+ </div>
108
+ </a>
109
+ {% endif %}
110
+ </div>
111
+ {% endfor %}
112
+ </div>
113
+ {% block filter %}
114
+ {% if filter %}
115
+ <div id="filter-panel" class="mt-2">
116
+ <div class="panel-list mx-3 mb-1 has-text-centered" style="border-bottom: 1px #ccc solid">
117
+ <span>{% translate 'Filter' %}</span>
118
+ </div>
119
+ {% include 'ui/partials/filter.html' %}
120
+ </div>
121
+ {% endif %}
122
+ {% endblock %}
123
+ </nav>
124
+ </div>
125
+
126
+ <div class="is-flex-grow-1" style="overflow-x: auto">
127
+ <div class="is-flex is-flex-direction-column" style="height: 100%; overflow: hidden">
128
+ <div>
129
+ {% include 'ui/partials/header.html' %}
130
+ </div>
131
+ <div id="content" class="px-3 pb-4" style="overflow: auto; flex-grow: 1">
132
+ {% block messages %}{% endblock %}
133
+ {% block content %}{% endblock %}
134
+ </div>
135
+ <div>
136
+ {% if list_page.paginator %}
137
+ <div class="level is-hidden-tablet is-align-self-flex-start m-3">
138
+ <div class="level-item is-align-content-flex-start">
139
+ {% include 'ui/partials/pagination_list.html' %}
140
+ </div>
141
+ </div>
142
+ {% endif %}
143
+ {% if detail_page %}
144
+ <div class="level is-hidden-tablet is-align-self-flex-start m-3">
145
+ <div class="level-item is-align-content-flex-start">
146
+ {% include 'ui/partials/pagination_detail.html' %}
147
+ </div>
148
+ </div>
149
+ {% endif %}
150
+ </div>
149
151
  </div>
150
152
  </div>
151
153
  </div>
152
- </div>
153
154
 
154
- {% if filter %}
155
- <div id="filter-modal" class="modal">
156
- <div class="modal-background filter-modal-close" onclick="hideFilterModal()"></div>
157
- <div class="modal-card" style="height: 100%">
158
- <header class="modal-card-head">
159
- <p class="modal-card-title">
160
- {% block search_modal_title %}{% translate 'Filter' %}{% endblock %}</p>
161
- <button class="delete filter-modal-close" aria-label="close" onclick="hideFilterModal()"></button>
162
- </header>
163
- <section id="modal-content" class="modal-card-body px-0">
164
- </section>
165
- <footer class="modal-card-foot">
166
- <button id="applyFilterFromModalButton"
167
- class="button is-fullwidth is-success ml-1"
168
- onclick="hideFilterModal()"
169
- >{% translate 'Filter' %}</button>
170
- </footer>
155
+ {% if filter %}
156
+ <div id="filter-modal" class="modal">
157
+ <div class="modal-background filter-modal-close" onclick="hideFilterModal()"></div>
158
+ <div class="modal-card" style="height: 100%">
159
+ <header class="modal-card-head">
160
+ <p class="modal-card-title">
161
+ {% block search_modal_title %}{% translate 'Filter' %}{% endblock %}</p>
162
+ <button class="delete filter-modal-close" aria-label="close" onclick="hideFilterModal()"></button>
163
+ </header>
164
+ <section id="modal-content" class="modal-card-body px-0">
165
+ </section>
166
+ <footer class="modal-card-foot">
167
+ <button id="applyFilterFromModalButton"
168
+ class="button is-fullwidth is-success ml-1"
169
+ onclick="hideFilterModal()"
170
+ >{% translate 'Filter' %}</button>
171
+ </footer>
172
+ </div>
171
173
  </div>
172
- </div>
173
- {% endif %}
174
+ {% endif %}
174
175
 
175
- <div id="form-modal" class="modal"></div>
176
+ <div id="form-modal" class="modal"></div>
177
+ {% endblock %}
176
178
  </body>
@@ -5,8 +5,8 @@
5
5
  {% block content %}
6
6
  <div class="columns is-multiline">
7
7
  {% for obj in list_page %}
8
- <div class="list-column column {% block column_width %}is-4{% endblock %}-fullhd is-12-touch is-12-desktop is-12-widescreen"
9
- style="height: {% block column_height %}9rem{% endblock %}"
8
+ <div class="list-column column is-{{ column_width }}-fullhd is-12-touch is-12-desktop is-12-widescreen"
9
+ style="height: {{ column_height }}rem"
10
10
  {% if endless_scroll and forloop.last and page.has_next %}
11
11
  hx-get="{{ url_params }}&page={{ page.next_page_number }}"
12
12
  hx-trigger="intersect once"
@@ -5,10 +5,11 @@
5
5
 
6
6
  <div id="query-block" class="panel-block pt-0" style="position: relative; display: inline-block; width: 100%">
7
7
  <script src="{% static "js/filter.js" %}" defer type="text/javascript"></script>
8
+
8
9
  <div id="query-apply" hx-get="" hx-trigger="click" hx-replace-url="true"
9
10
  hx-select-oob="#content,#list-pagination,#panel-actions,#header-actions,#query-apply">
10
-
11
11
  </div>
12
+
12
13
  <div id="query-tags" class="is-flex-direction-column is-flex-grow-1 is-multiline mb-1"
13
14
  data-or-label="{% translate 'OR' %}" data-and-label="{% translate 'AND' %}" data-xor-label="{% translate 'XOR' %}">
14
15
  </div>
@@ -30,7 +31,7 @@
30
31
  </div>
31
32
  </div>
32
33
 
33
- <div id="query-input-fieldset" class="field has-addons pt-1">
34
+ <div id="query-input-fieldset" class="field has-addons pt-1 mb-0">
34
35
  <p id="query-input-control" class="control is-expanded">
35
36
  <input id="query-input"
36
37
  type="text"
@@ -50,7 +51,30 @@
50
51
  </div>
51
52
 
52
53
 
53
- <div id="query-params-dropdown" class="box mt-1 mx-0 p-1 is-hidden" tabindex="-1" style="z-index: 10; background: white; word-break: break-word; position: inherit; max-height: 300px; overflow-y: auto" onclick="toggleParams()">
54
- {{ filter.to_html }}
54
+ <div id="query-params-dropdown" class="box mt-0 mx-0 p-1 is-hidden is-fullwidth" tabindex="-1" style="z-index: 900; background: white; word-break: break-word; position: absolute; width: 300px; max-height: 300px; overflow-y: auto" onclick="toggleParams()">
55
+ {{ filter.to_html.params }}
56
+ </div>
57
+ <div id="field-selection" class="button mt-1" style="z-index: 10; position: relative" onclick="toggleFieldSelection()">
58
+ {% translate 'Fields' %}
59
+ </div>
60
+ <div id="field-paths" class="box mt-1 mx-0 p-1 is-hidden" tabindex="-1" style="z-index: 800; outline: none; background: white; word-break: break-word; position: absolute; width: 300px; max-height: 300px; overflow-y: auto">
61
+ <div class="field has-addons mr-1">
62
+ <p class="control is-expanded">
63
+ <input id="field-path-search-input" class="input is-small" type="text" aria-label="Query Input" oninput="filterFieldPaths()">
64
+ </p>
65
+ <p class="control">
66
+ <button id="field-path-search-clear" class="button is-small has-icon"><i class="icon-clear"></i></button>
67
+ </p>
68
+ <p class="control">
69
+ <button id="field-path-search-reset" class="button is-small has-icon"><i class="icon-delete-filter"></i></button>
70
+ </p>
71
+ </div>
72
+ <div id="field-path-checkboxes">
73
+ {{ filter.to_html.field_paths }}
74
+ </div>
75
+ <div id="field-path-change">
76
+
77
+ </div>
55
78
  </div>
79
+
56
80
  </div>
@@ -1,7 +1,7 @@
1
1
  {% load i18n %}
2
2
 
3
3
  <div id="header" class="pt-4">
4
- <div class="level level-is-shrinkable is-flex mb-2 pl-3 {% if detail_pagination %}is-mobile {% endif %}">
4
+ <div class="level level-is-shrinkable is-flex mb-2 pl-3 {% if detail_page %}is-mobile {% endif %}">
5
5
  <div class="level-left">
6
6
  <div class="level-item has-text-weight-bold">
7
7
  <nav id="breadcrumbs" class="breadcrumb" aria-label="breadcrumbs" style="white-space: unset; word-break: break-word">
@@ -16,12 +16,12 @@
16
16
  </nav>
17
17
  </div>
18
18
  </div>
19
- {% if page or detail_pagination %}
19
+ {% if list_page.paginator or detail_page %}
20
20
  <div class="level-right is-mobile is-hidden-mobile is-hidden-fullhd is-align-self-flex-start px-3">
21
21
  <div class="level-item">
22
- {% if list_pagination %}
22
+ {% if list_page.paginator %}
23
23
  {% include 'ui/partials/pagination_list.html' %}
24
- {% elif detail_pagination %}
24
+ {% elif detail_page %}
25
25
  {% include 'ui/partials/pagination_detail.html' %}
26
26
  {% endif %}
27
27
  </div>
@@ -40,7 +40,7 @@
40
40
  {% else %}
41
41
  <a class="button mr-2 {{ action.class_list|join:' ' }}"
42
42
  {% if action.url %}
43
- {{ action.method.value }}="{{ action.url }}{% if action.add_url_params %}{{ url_params|default_if_none:'?' }}{% else %}?{% endif %}{% if page %}&page={{ page.number }}{% endif %}{{ action.query_params }}"
43
+ {{ action.method.value }}="{{ action.url }}{% if action.add_url_params %}{{ url_params|default_if_none:'?' }}{% else %}?{% endif %}{% if list_page.paginator %}&page={{ list_page.number }}{% endif %}{{ action.query_params }}"
44
44
  {% endif %}
45
45
  {{ action.attrs_str }}
46
46
  >{{ action.name }}
@@ -4,7 +4,7 @@
4
4
  {% load accrete_ui %}
5
5
 
6
6
  {% block content %}
7
- <table class="table is-fullwidth is-hoverable" hx-indicator=".htmx-indicator">
7
+ <table class="table is-fullwidth is-hoverable hax-box" hx-indicator=".htmx-indicator">
8
8
  <thead style="position: sticky; top: 0; background: white; z-index: 10">
9
9
  {% block table_header_row %}
10
10
  <tr>
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  from django import template
3
+ from django.db.models import Model
3
4
  from django.core.cache import cache
4
5
  from django.conf import settings
5
6
  from django.template.loader import render_to_string
@@ -26,12 +27,15 @@ def combine_templates(template_name, request=None):
26
27
 
27
28
 
28
29
  @register.filter(name='get_attr')
29
- def get_attr_from_string(param, value):
30
- try:
31
- attr = getattr(param, value)
32
- except AttributeError:
33
- _logger.exception(f'Object {param} has no attribute {value}')
34
- return ''
30
+ def get_attr_from_string(param: object, value: str):
31
+ attr_name = value.split('__')
32
+ attr = param
33
+ for name in attr_name:
34
+ try:
35
+ attr = getattr(attr, name)
36
+ except AttributeError:
37
+ _logger.exception(f'Object {attr} has no attribute {name}')
38
+ return ''
35
39
  if callable(attr):
36
40
  return attr()
37
41
  else:
@@ -1,17 +1,11 @@
1
- <!doctype html>
1
+ {% extends 'ui/layout.html' %}
2
2
  {% load static %}
3
3
  {% load i18n %}
4
4
 
5
- <html lang="en">
6
- <head>
7
- <meta charset="utf-8">
8
- <meta name="viewport" content="width=device-width, initial-scale=1">
9
- <link rel="stylesheet" type="text/css" href="{% static "css/bulma.min.css" %}">
10
- <link rel="shortcut icon" type="image/png" href="{% static 'icons/accrete.svg' %}"/>
11
- <title>Login | Accrete</title>
12
- </head>
13
- <body>
14
- <section class="hero is-primary is-medium is-bold is-fullheight">
5
+ {% block title %}Login | Accrete{% endblock %}
6
+
7
+ {% block body %}
8
+ <section class="hero is-primary is-medium is-bold is-fullheight">
15
9
  <div class="hero-body">
16
10
  <div class="container">
17
11
  <div class="columns">
@@ -37,4 +31,4 @@
37
31
  </div>
38
32
  </div>
39
33
  </section>
40
- </body>
34
+ {% endblock %}
@@ -1,15 +1,16 @@
1
- from django.views.generic import View
1
+ from django.utils import timezone
2
2
  from django.contrib.auth.forms import AuthenticationForm
3
- from django.contrib.auth import logout, views, update_session_auth_hash
3
+ from django.contrib.auth import views, update_session_auth_hash
4
4
  from django.contrib.auth.decorators import login_required
5
+ from django.contrib.sessions.models import Session
5
6
  from django.contrib import messages
7
+ from django.db.models import Q
6
8
  from django.shortcuts import redirect, render, reverse, resolve_url
7
9
  from django.utils.translation import gettext_lazy as _
8
10
  from django.conf import settings
9
11
 
10
12
  from accrete.forms import save_form
11
13
  from accrete.contrib import ui
12
- from accrete.contrib.ui import FormContent, DetailContent, ClientAction
13
14
  from .forms import UserForm, ChangePasswordForm, ChangeEmailForm
14
15
 
15
16
 
@@ -43,9 +44,9 @@ def user_detail(request):
43
44
  object=request.user,
44
45
  breadcrumbs=[],
45
46
  actions=[
46
- ClientAction(_('Edit'), url=reverse('user:edit')),
47
- ClientAction(_('Change E-Mail'), url=reverse('user:edit_email')),
48
- ClientAction(_('Change Password'), url=reverse('user:edit_password'))
47
+ ui.ClientAction(_('Edit'), url=reverse('user:edit')),
48
+ ui.ClientAction(_('Change E-Mail'), url=reverse('user:edit_email')),
49
+ ui.ClientAction(_('Change Password'), url=reverse('user:edit_password'))
49
50
  ]
50
51
  )
51
52
  return render(request, 'user/user_detail.html', ctx)
@@ -65,7 +66,7 @@ def user_edit(request):
65
66
  title=_('Preferences'),
66
67
  form=form,
67
68
  form_id='form',
68
- actions=ui.default_form_actions(reverse('user:detail'))
69
+ actions=ui.form_actions(reverse('user:detail'))
69
70
  )
70
71
  return render(request, 'user/user_form.html', ctx)
71
72
 
@@ -87,7 +88,7 @@ def user_change_password(request):
87
88
  title=_('Change Password'),
88
89
  form=form,
89
90
  form_id='form',
90
- actions=ui.default_form_actions(reverse('user:detail'))
91
+ actions=ui.form_actions(reverse('user:detail'))
91
92
  )
92
93
  return render(request, 'user/change_password.html', ctx)
93
94
 
@@ -109,6 +110,6 @@ def user_change_email(request):
109
110
  title=_('Change Email'),
110
111
  form=form,
111
112
  form_id='form',
112
- actions=ui.default_form_actions(reverse('user:detail'))
113
+ actions=ui.form_actions(reverse('user:detail'))
113
114
  )
114
115
  return render(request, 'user/change_email.html', ctx)
accrete/middleware.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from django.utils.deprecation import MiddlewareMixin
2
2
  from django.conf import settings
3
+ from django.http.response import HttpResponseRedirect, HttpResponsePermanentRedirect
3
4
  from accrete.tenant import set_tenant, set_member
4
5
 
5
6
  from .models import Tenant
@@ -73,3 +74,17 @@ class TenantMiddleware(MiddlewareMixin):
73
74
  max_age=31536000
74
75
  )
75
76
  return response
77
+
78
+
79
+ class HtmxRedirectMiddleware(MiddlewareMixin):
80
+
81
+ @staticmethod
82
+ def process_response(request, response):
83
+ is_htmx = request.headers.get('HX-Request', 'false') == 'true'
84
+ is_redirect = isinstance(
85
+ response, (HttpResponseRedirect, HttpResponsePermanentRedirect)
86
+ )
87
+ if is_htmx and is_redirect:
88
+ response['HX-Redirect'] = response['Location']
89
+ response.status_code = 200
90
+ return response
accrete/models.py CHANGED
@@ -3,16 +3,17 @@ from django.conf import settings
3
3
  from django.utils.translation import gettext_lazy as _
4
4
  from django.contrib.auth.validators import UnicodeUsernameValidator
5
5
  from accrete.tenant import get_tenant
6
+ from accrete.annotation import AnnotationModelMixin, AnnotationManagerMixin
6
7
 
7
8
 
8
- class TenantManager(models.Manager):
9
+ class TenantManager(models.Manager, AnnotationManagerMixin):
9
10
 
10
11
  def get_queryset(self):
11
12
  queryset = super().get_queryset()
12
13
  tenant = get_tenant()
13
14
  if tenant:
14
15
  queryset = queryset.filter(tenant=tenant)
15
- return queryset
16
+ return self.add_annotations(queryset)
16
17
 
17
18
  def bulk_create(
18
19
  self,
@@ -25,10 +26,11 @@ class TenantManager(models.Manager):
25
26
  ):
26
27
  tenant = get_tenant()
27
28
  if tenant is None and not all(obj.tenant_id for obj in objs):
28
- raise ValueError('Tenant must be set for all objects when calling bulk_create')
29
- else:
30
- for obj in objs:
31
- obj.tenant_id = tenant.pk
29
+ raise ValueError(
30
+ 'Tenant must be set for all objects when calling bulk_create'
31
+ )
32
+ for obj in objs:
33
+ obj.tenant_id = tenant.pk
32
34
  return super().bulk_create(
33
35
  objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts,
34
36
  update_conflicts=update_conflicts, update_fields=update_fields,
@@ -36,7 +38,7 @@ class TenantManager(models.Manager):
36
38
  )
37
39
 
38
40
 
39
- class TenantModel(models.Model):
41
+ class TenantModel(models.Model, AnnotationModelMixin):
40
42
 
41
43
  class Meta:
42
44
  abstract = True