accrete 0.0.38__py3-none-any.whl → 0.0.40__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.
- accrete/annotation.py +7 -10
- accrete/contrib/ui/__init__.py +1 -1
- accrete/contrib/ui/context.py +19 -10
- accrete/contrib/ui/elements.py +1 -0
- accrete/contrib/ui/filter.py +18 -7
- accrete/contrib/ui/static/js/filter.js +71 -16
- accrete/contrib/ui/templates/ui/list.html +12 -2
- accrete/contrib/ui/templates/ui/partials/filter.html +21 -21
- accrete/contrib/ui/templates/ui/partials/pagination_list.html +1 -1
- accrete/contrib/ui/templates/ui/table.html +17 -14
- accrete/contrib/ui/templatetags/accrete_ui.py +18 -10
- accrete/contrib/user/views.py +1 -4
- accrete/forms.py +0 -19
- accrete/managers.py +35 -0
- accrete/models.py +2 -33
- accrete/utils/__init__.py +4 -0
- accrete/utils/forms.py +24 -0
- accrete/{querystring.py → utils/http.py} +15 -5
- {accrete-0.0.38.dist-info → accrete-0.0.40.dist-info}/METADATA +1 -1
- {accrete-0.0.38.dist-info → accrete-0.0.40.dist-info}/RECORD +22 -21
- accrete/queries.py +0 -18
- {accrete-0.0.38.dist-info → accrete-0.0.40.dist-info}/WHEEL +0 -0
- {accrete-0.0.38.dist-info → accrete-0.0.40.dist-info}/licenses/LICENSE +0 -0
accrete/annotation.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from django.db.models import Field
|
1
|
+
from django.db.models import Field
|
2
2
|
from django.db.models.expressions import Func
|
3
3
|
from django.db.models.aggregates import Aggregate
|
4
4
|
|
@@ -34,13 +34,10 @@ class AnnotationModelMixin:
|
|
34
34
|
|
35
35
|
class AnnotationManagerMixin:
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
return queryset
|
42
|
-
return queryset.annotate(**{
|
37
|
+
def get_annotations(self):
|
38
|
+
if not hasattr(self.model, 'get_annotations'):
|
39
|
+
return {}
|
40
|
+
return {
|
43
41
|
annotation['name']: annotation['annotation'].function
|
44
|
-
for annotation in model.get_annotations()
|
45
|
-
}
|
46
|
-
|
42
|
+
for annotation in self.model.get_annotations()
|
43
|
+
}
|
accrete/contrib/ui/__init__.py
CHANGED
accrete/contrib/ui/context.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import logging
|
2
|
+
from urllib.parse import quote_plus
|
2
3
|
from dataclasses import dataclass, field
|
3
4
|
from typing import TypedDict
|
4
5
|
from django.utils.translation import gettext_lazy as _t
|
@@ -56,6 +57,7 @@ class TableContext(Context):
|
|
56
57
|
endless_scroll: bool
|
57
58
|
filter: Filter
|
58
59
|
object_param_str: str = field(default='', kw_only=True)
|
60
|
+
field_selection: bool = True
|
59
61
|
|
60
62
|
|
61
63
|
@dataclass
|
@@ -69,6 +71,7 @@ class ListContext(Context):
|
|
69
71
|
endless_scroll: bool = True
|
70
72
|
column_height: int = 4
|
71
73
|
column_width: int = 12
|
74
|
+
field_selection: bool = False
|
72
75
|
|
73
76
|
def __post_init__(self):
|
74
77
|
super().__post_init__()
|
@@ -107,8 +110,8 @@ def cast_param(params: dict, param: str, cast_to: callable, default):
|
|
107
110
|
return default
|
108
111
|
|
109
112
|
|
110
|
-
def
|
111
|
-
return {key: f'&{key}={value}' for key, value in get_params.items()}
|
113
|
+
def url_param_dict(get_params: dict) -> dict:
|
114
|
+
return {key: f'&{key}={quote_plus(value)}' for key, value in get_params.items()}
|
112
115
|
|
113
116
|
|
114
117
|
def url_param_str(params: dict, extract: list[str] = None) -> str:
|
@@ -118,9 +121,9 @@ def url_param_str(params: dict, extract: list[str] = None) -> str:
|
|
118
121
|
them, so that each value is formatted e.g. {'page': '&page=1'}
|
119
122
|
"""
|
120
123
|
if extract:
|
121
|
-
params =
|
124
|
+
params = extract_url_params(params, extract)
|
122
125
|
param_str = (
|
123
|
-
"".join(str(value) for value in params.values())
|
126
|
+
"".join(str(value) for value in url_param_dict(params).values())
|
124
127
|
.replace('&&', '&')
|
125
128
|
.replace('?&', '?')
|
126
129
|
.strip('?&')
|
@@ -185,7 +188,7 @@ def get_table_fields(
|
|
185
188
|
field_definition: dict[str | TableField] = None
|
186
189
|
) -> list[TableField]:
|
187
190
|
|
188
|
-
def get_alignment(field_attr):
|
191
|
+
def get_alignment(field_attr) -> TableFieldAlignment:
|
189
192
|
number_field_types = (
|
190
193
|
models.DecimalField, models.IntegerField, models.FloatField
|
191
194
|
)
|
@@ -193,12 +196,11 @@ def get_table_fields(
|
|
193
196
|
isinstance(field_attr, number_field_types)
|
194
197
|
or (
|
195
198
|
isinstance(field_attr, Annotation)
|
196
|
-
and field_attr.field in number_field_types
|
197
|
-
)
|
199
|
+
and field_attr.field in number_field_types)
|
200
|
+
):
|
198
201
|
return TableFieldAlignment.RIGHT
|
199
202
|
return TableFieldAlignment.LEFT
|
200
203
|
|
201
|
-
|
202
204
|
def get_field_definition(f_name: str) -> TableField:
|
203
205
|
if definition := field_definition.get(f_name):
|
204
206
|
return definition
|
@@ -217,12 +219,18 @@ def get_table_fields(
|
|
217
219
|
elif hasattr(attr, 'field'):
|
218
220
|
attr = attr.field
|
219
221
|
names.append(attr.verbose_name)
|
222
|
+
is_relation = (
|
223
|
+
attr.related_model
|
224
|
+
and hasattr(attr.related_model, 'get_absolute_url')
|
225
|
+
or False
|
226
|
+
)
|
220
227
|
return TableField(
|
221
228
|
label=' / '.join([str(name) for name in names[1:]]),
|
222
229
|
name=f_name,
|
223
230
|
header_info=str(attr.help_text),
|
224
231
|
truncate_after=50,
|
225
|
-
alignment=get_alignment(attr)
|
232
|
+
alignment=get_alignment(attr),
|
233
|
+
is_relation=is_relation
|
226
234
|
)
|
227
235
|
|
228
236
|
if field_definition is None:
|
@@ -233,6 +241,7 @@ def get_table_fields(
|
|
233
241
|
try:
|
234
242
|
table_fields.append(get_field_definition(field_name))
|
235
243
|
except AttributeError as e:
|
236
|
-
|
244
|
+
_logger.error(e)
|
237
245
|
pass
|
246
|
+
|
238
247
|
return table_fields
|
accrete/contrib/ui/elements.py
CHANGED
accrete/contrib/ui/filter.py
CHANGED
@@ -298,14 +298,25 @@ class Filter:
|
|
298
298
|
|
299
299
|
def field_path_selection(self):
|
300
300
|
html = ''
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
301
|
+
filter_exclude = getattr(self.model, 'filter_exclude', [])
|
302
|
+
filter_exclude.extend(self.default_exclude)
|
303
|
+
foreign_keys = [x.name for x in filter(
|
304
|
+
lambda x: x.is_relation and x.name not in filter_exclude,
|
305
|
+
self.model._meta.get_fields()
|
306
|
+
)]
|
307
|
+
field_paths = [
|
308
|
+
(self.get_field_path_label(field_path), field_path)
|
309
|
+
for field_path in filter(
|
310
|
+
lambda x: len(x.split('__')) <= 1,
|
311
|
+
self.field_paths + foreign_keys
|
312
|
+
)
|
313
|
+
]
|
314
|
+
sorted_field_paths = sorted(filter(lambda x: x[0], field_paths), key=lambda x: x[0].lower())
|
315
|
+
for field in sorted_field_paths:
|
305
316
|
html += f"""
|
306
|
-
<label class="checkbox my-1" style="width: 100%">
|
307
|
-
<input type="checkbox" name="{field}">
|
308
|
-
{
|
317
|
+
<label class="checkbox is-unselectable my-1" style="width: 100%">
|
318
|
+
<input type="checkbox" name="{field[1]}" data-label="{field[0].lower()}">
|
319
|
+
<span>{field[0]}</span>
|
309
320
|
</label>
|
310
321
|
"""
|
311
322
|
return html
|
@@ -1,6 +1,10 @@
|
|
1
|
+
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
2
|
+
checkSelectedFields();
|
3
|
+
});
|
1
4
|
window.addEventListener('click', showHide);
|
2
5
|
buildQueryTagsFromUrl();
|
3
6
|
resetInput();
|
7
|
+
checkSelectedFields();
|
4
8
|
|
5
9
|
function buildQueryTagsFromUrl() {
|
6
10
|
const queryTags = document.getElementById('query-tags');
|
@@ -249,11 +253,30 @@ function toggleFilterModalButton() {
|
|
249
253
|
const button = document.getElementById('modal-filter-button');
|
250
254
|
const query = document.getElementById('query-tags');
|
251
255
|
if (query.firstElementChild && query.firstElementChild.childElementCount) {
|
252
|
-
button.classList.add('is-
|
256
|
+
button.classList.add('is-success');
|
253
257
|
}
|
254
258
|
else {
|
255
|
-
button.classList.remove('is-
|
256
|
-
}
|
259
|
+
button.classList.remove('is-success');
|
260
|
+
}
|
261
|
+
}
|
262
|
+
|
263
|
+
function checkSelectedFields() {
|
264
|
+
const table = document.getElementById('content-table');
|
265
|
+
const header = table.querySelectorAll('th')
|
266
|
+
document.getElementById(
|
267
|
+
'field-path-checkboxes'
|
268
|
+
).querySelectorAll(
|
269
|
+
'input:checked'
|
270
|
+
).forEach((el) => {el.checked = ''});
|
271
|
+
header.forEach((th) => {
|
272
|
+
const name = th.getAttribute('data-name');
|
273
|
+
if (name) {
|
274
|
+
document.getElementById(
|
275
|
+
'field-path-checkboxes'
|
276
|
+
).querySelector(`input[name=${name}]`
|
277
|
+
).checked = 'checked';
|
278
|
+
}
|
279
|
+
})
|
257
280
|
}
|
258
281
|
|
259
282
|
//-----------------------------------------------------------------------------
|
@@ -441,25 +464,57 @@ function changeOperator(event) {
|
|
441
464
|
|
442
465
|
function toggleFieldSelection() {
|
443
466
|
const fieldPaths = document.getElementById('field-paths');
|
467
|
+
const input = document.getElementById('field-path-search-input')
|
468
|
+
const checkBoxes = document.getElementById('field-path-checkboxes');
|
444
469
|
this.event.target.classList.toggle('is-active');
|
445
470
|
fieldPaths.classList.toggle('is-hidden');
|
471
|
+
input.value = ''
|
472
|
+
checkBoxes.querySelectorAll(':scope * input').forEach(
|
473
|
+
(el) => {
|
474
|
+
el.parentElement.classList.remove('is-hidden')
|
475
|
+
});
|
446
476
|
}
|
447
477
|
|
448
478
|
function filterFieldPaths() {
|
449
|
-
const
|
450
|
-
const
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
479
|
+
const value = document.getElementById('field-path-search-input').value.toString().toLowerCase();
|
480
|
+
const checkBoxes = document.getElementById('field-path-checkboxes');
|
481
|
+
if (!value) {
|
482
|
+
checkBoxes.querySelectorAll(':scope * input').forEach(
|
483
|
+
(el) => {
|
484
|
+
el.parentElement.classList.remove('is-hidden')
|
485
|
+
});
|
486
|
+
return
|
457
487
|
}
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
488
|
+
const pathsToShow = checkBoxes.querySelectorAll(`:scope * input[data-label*=${value}]`);
|
489
|
+
const pathsToHide = checkBoxes.querySelectorAll(`:scope * input:not([data-label*=${value}])`);
|
490
|
+
pathsToShow.forEach((el) => {el.parentElement.classList.remove('is-hidden')});
|
491
|
+
pathsToHide.forEach((el) => {el.parentElement.classList.add('is-hidden')})
|
492
|
+
}
|
493
|
+
|
494
|
+
function changeFieldSelection() {
|
495
|
+
const fieldPathApply = document.getElementById('field-path-apply');
|
496
|
+
const fieldPathApplyEvent = new Event('click');
|
497
|
+
const url = new URL(window.location)
|
498
|
+
const fieldPaths = document.getElementById('field-path-checkboxes');
|
499
|
+
const selected = fieldPaths.querySelectorAll('input:checked')
|
500
|
+
let fields = []
|
501
|
+
selected.forEach((input) => {
|
502
|
+
fields.push(input.name)
|
503
|
+
})
|
504
|
+
url.searchParams.set('fields', JSON.stringify(fields));
|
505
|
+
fieldPathApply.setAttribute('hx-get', url.toString());
|
506
|
+
htmx.process(fieldPathApply);
|
507
|
+
fieldPathApply.dispatchEvent(fieldPathApplyEvent);
|
508
|
+
}
|
509
|
+
|
510
|
+
function resetFieldPaths() {
|
511
|
+
const fieldPathApply = document.getElementById('field-path-apply');
|
512
|
+
const fieldPathApplyEvent = new Event('click');
|
513
|
+
const url = new URL(window.location)
|
514
|
+
url.searchParams.delete('fields');
|
515
|
+
fieldPathApply.setAttribute('hx-get', url.toString());
|
516
|
+
htmx.process(fieldPathApply);
|
517
|
+
fieldPathApply.dispatchEvent(fieldPathApplyEvent);
|
463
518
|
}
|
464
519
|
|
465
520
|
//-----------------------------------------------------------------------------
|
@@ -11,7 +11,7 @@
|
|
11
11
|
hx-get="{{ url_params }}&page={{ page.next_page_number }}"
|
12
12
|
hx-trigger="intersect once"
|
13
13
|
hx-select=".list-column"
|
14
|
-
hx-select-oob="#list-pagination-end-index,#list-pagination-next-button"
|
14
|
+
hx-select-oob="#list-pagination-end-index,#list-pagination-next-button,#list-pagination-prev-button"
|
15
15
|
hx-swap="afterend"
|
16
16
|
{% endif %}
|
17
17
|
>
|
@@ -24,6 +24,16 @@
|
|
24
24
|
{% endfor %}
|
25
25
|
</div>
|
26
26
|
{% if endless_scroll %}
|
27
|
-
<
|
27
|
+
<div id="endless-scroller"
|
28
|
+
{% if list_page.has_next %}
|
29
|
+
hx-get="{{ url_params }}&page={{ page.next_page_number }}"
|
30
|
+
hx-trigger="intersect once"
|
31
|
+
hx-select=".list-column"
|
32
|
+
hx-select-oob="#list-pagination-end-index,#list-pagination-next-button,#list-pagination-prev-button,#endless-scroller"
|
33
|
+
hx-swap="afterend"
|
34
|
+
{% endif %}
|
35
|
+
>
|
36
|
+
</div>
|
37
|
+
<progress class="htmx-indicator progress is-small is-success" max="100">15%</progress>
|
28
38
|
{% endif %}
|
29
39
|
{% endblock %}
|
@@ -54,27 +54,27 @@
|
|
54
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
55
|
{{ filter.to_html.params }}
|
56
56
|
</div>
|
57
|
-
|
58
|
-
|
59
|
-
|
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 }}
|
57
|
+
{% if field_selection %}
|
58
|
+
<div id="field-selection" class="button mt-1" style="z-index: 10; position: relative" onclick="toggleFieldSelection()">
|
59
|
+
{% translate 'Fields' %}
|
74
60
|
</div>
|
75
|
-
<div id="field-
|
76
|
-
|
61
|
+
<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">
|
62
|
+
<div class="field has-addons mr-1">
|
63
|
+
<p class="control is-expanded">
|
64
|
+
<input id="field-path-search-input" class="input is-small" type="text" aria-label="Query Input" oninput="filterFieldPaths()">
|
65
|
+
</p>
|
66
|
+
<p class="control">
|
67
|
+
<button id="field-path-search-reset" class="button is-small has-icon" onclick="resetFieldPaths()">
|
68
|
+
<i class="icon-delete-filter"></i>
|
69
|
+
</button>
|
70
|
+
</p>
|
71
|
+
</div>
|
72
|
+
<div id="field-path-checkboxes" onchange="changeFieldSelection()">
|
73
|
+
{{ filter.to_html.field_paths }}
|
74
|
+
</div>
|
75
|
+
<div id="field-path-apply" hx-get="" hx-trigger="click" hx-replace-url="true"
|
76
|
+
hx-select-oob="#content,#field-path-apply,#list-pagination">
|
77
|
+
</div>
|
77
78
|
</div>
|
78
|
-
|
79
|
-
|
79
|
+
{% endif %}
|
80
80
|
</div>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
{% load i18n %}
|
2
2
|
<div id="list-pagination" class="field has-addons" style="min-width: 100%; max-width: 100%">
|
3
3
|
<p class="control">
|
4
|
-
<button class="button"
|
4
|
+
<button id="list-pagination-prev-button" class="button"
|
5
5
|
hx-get="{{ pagination_param_str }}&page={% if list_page.has_previous %}{{ list_page.previous_page_number }}{% else %}{{ list_page.paginator.num_pages }}{% endif %}"
|
6
6
|
hx-replace-url="true"
|
7
7
|
hx-select-oob="#content,#list-pagination,#panel-actions,#header-actions,#breadcrumbs"
|
@@ -4,14 +4,14 @@
|
|
4
4
|
{% load accrete_ui %}
|
5
5
|
|
6
6
|
{% block content %}
|
7
|
-
<table class="table is-fullwidth is-hoverable hax-box" hx-indicator=".htmx-indicator">
|
7
|
+
<table id="content-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>
|
11
11
|
{% block table_header %}
|
12
12
|
<th>{{ object_label }}</th>
|
13
13
|
{% for field in fields %}
|
14
|
-
<th style="text-align: {% firstof field.header_alignment.value field.alignment.value %}">{{ field.label }}{% if field.header_info %}({{ field.header_info }}){% endif %}</th>
|
14
|
+
<th data-name="{{ field.name }}" style="text-align: {% firstof field.header_alignment.value field.alignment.value %}">{{ field.label }}{% if field.header_info %}({{ field.header_info }}){% endif %}</th>
|
15
15
|
{% endfor %}
|
16
16
|
{% if unselect_button %}
|
17
17
|
<th></th>
|
@@ -32,18 +32,20 @@
|
|
32
32
|
{% if field.template %}
|
33
33
|
{% include field.template %}
|
34
34
|
{% else %}
|
35
|
-
{% with val=obj|get_attr:field.name|default_if_none:'---'
|
35
|
+
{% with val=obj|get_attr:field.name|default_if_none:'---' is_relation=field.is_relation %}
|
36
36
|
<td style="text-align: {{ field.alignment.value }}" {% if val|length > field.truncate_after %}title="{{ val }}"{% endif %}>
|
37
37
|
<span class="responsive-heading has-text-weight-light" style="margin-right: .2rem">{{ field.label }}:</span>
|
38
|
-
{{ field.
|
39
|
-
|
40
|
-
{%
|
41
|
-
{
|
42
|
-
|
43
|
-
{
|
44
|
-
|
45
|
-
|
46
|
-
|
38
|
+
{% if is_relation %}<a href="{{ obj|related_obj_url:field.name }}" class="is-underlined">{% endif %}
|
39
|
+
{{ field.prefix }}
|
40
|
+
{% block td_content %}
|
41
|
+
{% if field.truncate_after > 0 %}
|
42
|
+
{{ val|truncatechars:field.truncate_after }}
|
43
|
+
{% else %}
|
44
|
+
{{ val }}
|
45
|
+
{% endif %}
|
46
|
+
{% endblock %}
|
47
|
+
{{ field.suffix }}
|
48
|
+
{% if is_relation %}</a>{% endif %}
|
47
49
|
</td>
|
48
50
|
{% endwith %}
|
49
51
|
{% endif %}
|
@@ -65,10 +67,11 @@
|
|
65
67
|
hx-select="tbody > tr"
|
66
68
|
hx-target="#content-table-body"
|
67
69
|
hx-swap="beforeend"
|
68
|
-
hx-
|
70
|
+
hx-replace-url="true"
|
71
|
+
hx-select-oob="#list-pagination-end-index,#list-pagination-next-button,#list-pagination-prev-button,#endless-scroller"
|
69
72
|
{% endif %}
|
70
73
|
>
|
71
74
|
</div>
|
72
|
-
<progress class="htmx-indicator progress is-small is-
|
75
|
+
<progress class="htmx-indicator progress is-small is-success" max="100">15%</progress>
|
73
76
|
{% endif %}
|
74
77
|
{% endblock %}
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import logging
|
2
|
+
import operator
|
2
3
|
from django import template
|
3
4
|
from django.db.models import Model
|
4
5
|
from django.core.cache import cache
|
@@ -28,18 +29,25 @@ def combine_templates(template_name, request=None):
|
|
28
29
|
|
29
30
|
@register.filter(name='get_attr')
|
30
31
|
def get_attr_from_string(param: object, value: str):
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
return ''
|
32
|
+
try:
|
33
|
+
attr = operator.attrgetter(value)(param)
|
34
|
+
except AttributeError:
|
35
|
+
_logger.exception(f'Object {param} has no attribute {value}')
|
36
|
+
return ''
|
37
|
+
if attr is None:
|
38
|
+
return None
|
39
39
|
if callable(attr):
|
40
|
-
return attr()
|
40
|
+
return mark_safe(attr())
|
41
41
|
else:
|
42
|
-
return attr
|
42
|
+
return mark_safe(attr)
|
43
|
+
|
44
|
+
|
45
|
+
@register.filter(name='related_obj_url')
|
46
|
+
def related_obj_url(param: object, value: str):
|
47
|
+
attr = getattr(param, value, False)
|
48
|
+
if attr:
|
49
|
+
return attr.get_absolute_url()
|
50
|
+
return '#'
|
43
51
|
|
44
52
|
|
45
53
|
@register.filter(name='message_class')
|
accrete/contrib/user/views.py
CHANGED
@@ -1,15 +1,12 @@
|
|
1
|
-
from django.utils import timezone
|
2
1
|
from django.contrib.auth.forms import AuthenticationForm
|
3
2
|
from django.contrib.auth import views, update_session_auth_hash
|
4
3
|
from django.contrib.auth.decorators import login_required
|
5
|
-
from django.contrib.sessions.models import Session
|
6
4
|
from django.contrib import messages
|
7
|
-
from django.db.models import Q
|
8
5
|
from django.shortcuts import redirect, render, reverse, resolve_url
|
9
6
|
from django.utils.translation import gettext_lazy as _
|
10
7
|
from django.conf import settings
|
11
8
|
|
12
|
-
from accrete.
|
9
|
+
from accrete.utils import save_form
|
13
10
|
from accrete.contrib import ui
|
14
11
|
from .forms import UserForm, ChangePasswordForm, ChangeEmailForm
|
15
12
|
|
accrete/forms.py
CHANGED
@@ -290,22 +290,3 @@ class TenantModelForm(One2ManyModelForm):
|
|
290
290
|
self.save_o2m()
|
291
291
|
|
292
292
|
return self.instance
|
293
|
-
|
294
|
-
|
295
|
-
def save_form(form, reraise=False):
|
296
|
-
form.is_saved = False
|
297
|
-
form.save_error = None
|
298
|
-
form.save_error_id = None
|
299
|
-
try:
|
300
|
-
if form.is_valid():
|
301
|
-
with transaction.atomic():
|
302
|
-
form.save()
|
303
|
-
form.is_saved = True
|
304
|
-
except Exception as e:
|
305
|
-
form.save_error = repr(e)
|
306
|
-
error_id = str(uuid4())[:8]
|
307
|
-
_logger.exception(f'{error_id}: {e}')
|
308
|
-
form.save_error_id = error_id
|
309
|
-
if reraise:
|
310
|
-
raise e
|
311
|
-
return form
|
accrete/managers.py
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from accrete.tenant import get_tenant
|
3
|
+
from accrete.annotation import AnnotationManagerMixin
|
4
|
+
|
5
|
+
|
6
|
+
class TenantManager(models.Manager, AnnotationManagerMixin):
|
7
|
+
|
8
|
+
def get_queryset(self):
|
9
|
+
queryset = super().get_queryset()
|
10
|
+
tenant = get_tenant()
|
11
|
+
if tenant:
|
12
|
+
queryset = queryset.filter(tenant=tenant)
|
13
|
+
return queryset.annotate(**self.get_annotations())
|
14
|
+
|
15
|
+
def bulk_create(
|
16
|
+
self,
|
17
|
+
objs,
|
18
|
+
batch_size=None,
|
19
|
+
ignore_conflicts=False,
|
20
|
+
update_conflicts=False,
|
21
|
+
update_fields=None,
|
22
|
+
unique_fields=None,
|
23
|
+
):
|
24
|
+
tenant = get_tenant()
|
25
|
+
if tenant is None and not all(obj.tenant_id for obj in objs):
|
26
|
+
raise ValueError(
|
27
|
+
'Tenant must be set for all objects when calling bulk_create'
|
28
|
+
)
|
29
|
+
for obj in objs:
|
30
|
+
obj.tenant_id = tenant.pk
|
31
|
+
return super().bulk_create(
|
32
|
+
objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts,
|
33
|
+
update_conflicts=update_conflicts, update_fields=update_fields,
|
34
|
+
unique_fields=unique_fields
|
35
|
+
)
|
accrete/models.py
CHANGED
@@ -3,39 +3,8 @@ 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
|
7
|
-
|
8
|
-
|
9
|
-
class TenantManager(models.Manager, AnnotationManagerMixin):
|
10
|
-
|
11
|
-
def get_queryset(self):
|
12
|
-
queryset = super().get_queryset()
|
13
|
-
tenant = get_tenant()
|
14
|
-
if tenant:
|
15
|
-
queryset = queryset.filter(tenant=tenant)
|
16
|
-
return self.add_annotations(queryset)
|
17
|
-
|
18
|
-
def bulk_create(
|
19
|
-
self,
|
20
|
-
objs,
|
21
|
-
batch_size=None,
|
22
|
-
ignore_conflicts=False,
|
23
|
-
update_conflicts=False,
|
24
|
-
update_fields=None,
|
25
|
-
unique_fields=None,
|
26
|
-
):
|
27
|
-
tenant = get_tenant()
|
28
|
-
if tenant is None and not all(obj.tenant_id for obj in objs):
|
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
|
34
|
-
return super().bulk_create(
|
35
|
-
objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts,
|
36
|
-
update_conflicts=update_conflicts, update_fields=update_fields,
|
37
|
-
unique_fields=unique_fields
|
38
|
-
)
|
6
|
+
from accrete.annotation import AnnotationModelMixin
|
7
|
+
from accrete.managers import TenantManager
|
39
8
|
|
40
9
|
|
41
10
|
class TenantModel(models.Model, AnnotationModelMixin):
|
accrete/utils/__init__.py
CHANGED
accrete/utils/forms.py
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
import logging
|
2
|
+
from uuid import uuid4
|
3
|
+
from django.db import transaction
|
4
|
+
|
5
|
+
_logger = logging.getLogger(__name__)
|
6
|
+
|
7
|
+
|
8
|
+
def save_form(form, reraise=False):
|
9
|
+
form.is_saved = False
|
10
|
+
form.save_error = None
|
11
|
+
form.save_error_id = None
|
12
|
+
try:
|
13
|
+
if form.is_valid():
|
14
|
+
with transaction.atomic():
|
15
|
+
form.save()
|
16
|
+
form.is_saved = True
|
17
|
+
except Exception as e:
|
18
|
+
form.save_error = repr(e)
|
19
|
+
error_id = str(uuid4())[:8]
|
20
|
+
_logger.exception(f'{error_id}: {e}')
|
21
|
+
form.save_error_id = error_id
|
22
|
+
if reraise:
|
23
|
+
raise e
|
24
|
+
return form
|
@@ -7,14 +7,24 @@ from accrete.annotation import Annotation
|
|
7
7
|
|
8
8
|
_logger = logging.getLogger(__name__)
|
9
9
|
|
10
|
+
Querystring_KEY_MAP = {
|
11
|
+
'querystring': 'q',
|
12
|
+
'order': 'order',
|
13
|
+
'paginate_by': 'paginate_by',
|
14
|
+
'page': 'page'
|
15
|
+
}
|
16
|
+
|
10
17
|
|
11
18
|
def filter_from_querystring(
|
12
|
-
model: type[Model],
|
19
|
+
model: type[Model], get_params: dict, key_map: dict = None
|
13
20
|
) -> QuerySet:
|
14
21
|
|
15
|
-
|
22
|
+
key_map = key_map or Querystring_KEY_MAP
|
23
|
+
querystring = get_params.get(key_map['querystring'], '[]')
|
24
|
+
order = get_params.get(key_map['order']) or model._meta.ordering
|
25
|
+
|
16
26
|
return model.objects.filter(
|
17
|
-
parse_querystring(model,
|
27
|
+
parse_querystring(model, querystring)
|
18
28
|
).order_by(*order).distinct()
|
19
29
|
|
20
30
|
|
@@ -44,7 +54,7 @@ def parse_querystring(model: type[Model], query_string: str) -> Q:
|
|
44
54
|
return ~expression if invert else expression
|
45
55
|
|
46
56
|
objects = related_model.objects.filter(Q(**{
|
47
|
-
'__'.join(parts[
|
57
|
+
'__'.join(parts[-2:]): value
|
48
58
|
}))
|
49
59
|
expression = Q(**{
|
50
60
|
f'{rel_path}{"__" if rel_path else ""}id__in':
|
@@ -86,4 +96,4 @@ def parse_querystring(model: type[Model], query_string: str) -> Q:
|
|
86
96
|
|
87
97
|
ops = {'&': operator.and_, '|': operator.or_, '^': operator.xor}
|
88
98
|
query = parse_query_block(query_data)
|
89
|
-
return query
|
99
|
+
return query
|
@@ -1,13 +1,12 @@
|
|
1
1
|
accrete/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
accrete/admin.py,sha256=MUYUmCFlGYPowiXTbwl4_Q6Cq0-neiL53WW4P76JCLs,1174
|
3
|
-
accrete/annotation.py,sha256
|
3
|
+
accrete/annotation.py,sha256=P85kNgf_ka3U8i5cwaiKaAiSm21U-xY9PKmXMZR2ulU,1160
|
4
4
|
accrete/apps.py,sha256=F7ynMLHJr_6bRujWtZVUzCliY2CGKiDvyUmL4F68L2E,146
|
5
5
|
accrete/config.py,sha256=eJUbvyBO3DvAD6xkVKjTAzlXy7V7EK9bVyb91girfUs,299
|
6
|
-
accrete/forms.py,sha256=
|
6
|
+
accrete/forms.py,sha256=2LobDn8EjHIaqBcgE4_Xk-nbfN6CPQasey-l22cQcT0,10740
|
7
|
+
accrete/managers.py,sha256=EvWURMnGadnH3d-wXwv0-sLJKD6uGY03tSCiRSGsKbo,1170
|
7
8
|
accrete/middleware.py,sha256=RWeHHcYCfpVO4EnG5HMS52F1y5OKRzNCuidMeq6b0zY,3176
|
8
|
-
accrete/models.py,sha256=
|
9
|
-
accrete/queries.py,sha256=bchiqLzE1DmR9kvQ6Yyog6cNlYK_lxpztQMOx6Hy_0c,410
|
10
|
-
accrete/querystring.py,sha256=jtPBs3VYQ5_NVwCfjDQV-orv44KHi9b1AAzajvjwYvc,2997
|
9
|
+
accrete/models.py,sha256=akv8BAT6Q3kxvSujgysg36nxWTizl_D9wAd14DzYhFM,5109
|
11
10
|
accrete/tenant.py,sha256=g3ZuTrQr2zqmIopNBRQeCmHEK2R3dlUme_hOV765J6U,1778
|
12
11
|
accrete/tests.py,sha256=Agltbzwwh5htvq_Qi9vqvxutzmg_GwgPS_N19xJZRlw,7197
|
13
12
|
accrete/views.py,sha256=9-sgCFe_CyG-wllAcIOLujyueiq66C-zg0U7Uf5Y2wU,2954
|
@@ -33,12 +32,12 @@ accrete/contrib/system_mail/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2
|
|
33
32
|
accrete/contrib/system_mail/views.py,sha256=xc1IQHrsij7j33TUbo-_oewy3vs03pw_etpBWaMYJl0,63
|
34
33
|
accrete/contrib/system_mail/migrations/0001_initial.py,sha256=6cwkkRXGjXvwXoMjjgmWmcPyXSTlUbhW1vMiHObk9MQ,1074
|
35
34
|
accrete/contrib/system_mail/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
36
|
-
accrete/contrib/ui/__init__.py,sha256=
|
35
|
+
accrete/contrib/ui/__init__.py,sha256=B_SJ3S9v4CQLNnQKqnWRGhddq6M5gn3vEg4KKuaKGPA,441
|
37
36
|
accrete/contrib/ui/admin.py,sha256=suMo4x8I3JBxAFBVIdE-5qnqZ6JAZV0FESABHOSc-vg,63
|
38
37
|
accrete/contrib/ui/apps.py,sha256=E0ao2ox6PQ3ldfeR17FXJUUJuGiWjm2DPCxHbPXGzls,152
|
39
|
-
accrete/contrib/ui/context.py,sha256=
|
40
|
-
accrete/contrib/ui/elements.py,sha256=
|
41
|
-
accrete/contrib/ui/filter.py,sha256=
|
38
|
+
accrete/contrib/ui/context.py,sha256=cs0wlZ2_E82bH4odOReHKEwX6KSe9ZkIORPsdCubWtw,7207
|
39
|
+
accrete/contrib/ui/elements.py,sha256=yHWPZMb68j0b-nAMHDla-ENpIN1IArXxP3sy5jZN9PQ,1600
|
40
|
+
accrete/contrib/ui/filter.py,sha256=cooq8c-ioa2do6Xr4FhU5PrMKyPFOJf6kfWSsHxbw1c,13600
|
42
41
|
accrete/contrib/ui/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
|
43
42
|
accrete/contrib/ui/urls.py,sha256=TUBlz_CGs9InTZoxM78GSnucA73I8knoh_obt12RUHM,186
|
44
43
|
accrete/contrib/ui/views.py,sha256=WpBKMsxFFG8eG4IN7TW_TPE6i3OFF7gnLDTK7JMKti8,191
|
@@ -124,7 +123,7 @@ accrete/contrib/ui/static/css/accrete.scss,sha256=KCY4QZNNVtIvogyNYkSwxeFzCCHs1M
|
|
124
123
|
accrete/contrib/ui/static/css/icons.css,sha256=IMdNurvI1cUwpGG8n3UXRkxl8MqP2Eqkvyh2maeyghw,6301
|
125
124
|
accrete/contrib/ui/static/icons/Logo.svg,sha256=hGZuxrAa-LRpFavFiF8Lnc7X9OQcqmb6Xl_dxx-27hM,1861
|
126
125
|
accrete/contrib/ui/static/icons/accrete.svg,sha256=CWHJKIgk3hxL7xIaFSz2j1cK-eF1TroCbjcF58bgOIs,1024
|
127
|
-
accrete/contrib/ui/static/js/filter.js,sha256=
|
126
|
+
accrete/contrib/ui/static/js/filter.js,sha256=vUnA0Q767uEtCgnQuUBVuv649lXUw6XmTHJnSBlQXiE,25082
|
128
127
|
accrete/contrib/ui/static/js/htmx.min.js,sha256=s73PXHQYl6U2SLEgf_8EaaDWGQFCm6H26I-Y69hOZp4,47755
|
129
128
|
accrete/contrib/ui/static/js/list.js,sha256=OX_81ifRmawE-1QBU5Qpq_E6sHiiNwIPleETAn9EOJw,4280
|
130
129
|
accrete/contrib/ui/templates/django/forms/widgets/attrs.html,sha256=zNxjU4Ta_eWZkh1WhrF_VIwNZ0lZyl980gSSijUK51k,195
|
@@ -137,21 +136,21 @@ accrete/contrib/ui/templates/django/forms/widgets/textarea.html,sha256=c9BTedqb3
|
|
137
136
|
accrete/contrib/ui/templates/ui/detail.html,sha256=0sqsW_XbtLVIquXA8157ZWamnwRc4Wp-6_aZgZ5HN64,1051
|
138
137
|
accrete/contrib/ui/templates/ui/form.html,sha256=kIIG4zzNbxb5fJAVr3lI05oa3yoxyAwstMeQWLa2Z0Y,409
|
139
138
|
accrete/contrib/ui/templates/ui/layout.html,sha256=qxLTBR16uh3ed1qm_eC-GrFkN6RYZZx0k1bX07Dcfz4,9247
|
140
|
-
accrete/contrib/ui/templates/ui/list.html,sha256=
|
141
|
-
accrete/contrib/ui/templates/ui/table.html,sha256=
|
142
|
-
accrete/contrib/ui/templates/ui/partials/filter.html,sha256=
|
139
|
+
accrete/contrib/ui/templates/ui/list.html,sha256=oDv8I9xxsxuwoNywRqt0n0leuKocnvViFCC0OFFENDk,1695
|
140
|
+
accrete/contrib/ui/templates/ui/table.html,sha256=Lcqd9SdvaNLoWEzW2Nli9sl__AVwi1g2UmzF9u-Nkdg,4274
|
141
|
+
accrete/contrib/ui/templates/ui/partials/filter.html,sha256=QbfeZQyBAiwXfHqiKK8fCgCy4ofr1KuTfz2ackTVJu4,4115
|
143
142
|
accrete/contrib/ui/templates/ui/partials/form_errors.html,sha256=1_TQvTdiejsn-43YSyp2YfnP52P-MFYb-HGY0DLm4oA,991
|
144
143
|
accrete/contrib/ui/templates/ui/partials/form_modal.html,sha256=FFDfI5qjOCUBSGqDjBXa8tcqN2q94wOOCNFDaiyplHQ,1032
|
145
144
|
accrete/contrib/ui/templates/ui/partials/header.html,sha256=Fr65wv84RgcjpRr1jRdGzChaX08SVhmFhcdEmkTBmyI,2859
|
146
145
|
accrete/contrib/ui/templates/ui/partials/onchange_form.html,sha256=K5twTGqRUW1iM2dGtdWntjsJvJVo5EIzKxX2HK-H1yw,160
|
147
146
|
accrete/contrib/ui/templates/ui/partials/pagination_detail.html,sha256=58nA3X7Il0FAD4VcYyr7tTGWRiVf_FN1TkImmKEpKHU,1014
|
148
|
-
accrete/contrib/ui/templates/ui/partials/pagination_list.html,sha256
|
147
|
+
accrete/contrib/ui/templates/ui/partials/pagination_list.html,sha256=-wGY-DYMAR-m1TfA3a4tN9XfpkjE-RHBfWGzTgm_OW0,1412
|
149
148
|
accrete/contrib/ui/templates/ui/partials/table_field.html,sha256=4oQw0na9UgHP8lo8KttyWBuDhCEeaZ8Vt9jJYJdZ5Gs,641
|
150
149
|
accrete/contrib/ui/templates/ui/partials/table_field_float.html,sha256=GH_jFdpk8wEJXv4QfO6c3URYrZGGLeuSyWwWl2cWxwQ,45
|
151
150
|
accrete/contrib/ui/templates/ui/partials/table_field_monetary.html,sha256=Wtod9vel2dD4vG8lUVLbtls4aU_2EG3p0E1QRRUSH10,166
|
152
151
|
accrete/contrib/ui/templates/ui/partials/table_field_string.html,sha256=GH_jFdpk8wEJXv4QfO6c3URYrZGGLeuSyWwWl2cWxwQ,45
|
153
152
|
accrete/contrib/ui/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
154
|
-
accrete/contrib/ui/templatetags/accrete_ui.py,sha256=
|
153
|
+
accrete/contrib/ui/templatetags/accrete_ui.py,sha256=qogDSrFdJWvnTp4-VOKeh3KjWmU_DJ2C7PRcuU4a7O8,1657
|
155
154
|
accrete/contrib/user/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
156
155
|
accrete/contrib/user/admin.py,sha256=YS4iApli7XUaIl9GsEJxys2j8sepX0by88omYHjff-E,85
|
157
156
|
accrete/contrib/user/apps.py,sha256=oHDrAiHf-G57mZLyxqGJzRY2DbPprGFD-QgyVJG_ruI,156
|
@@ -160,7 +159,7 @@ accrete/contrib/user/middleware.py,sha256=qblcujwJsthopagyT-hPFq4HsMyGt-VvqZw5TQ
|
|
160
159
|
accrete/contrib/user/models.py,sha256=SFEXG9G-XY7Nuss7DT51abDv8BWLHKYJocOhQDI_1Lw,3926
|
161
160
|
accrete/contrib/user/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
|
162
161
|
accrete/contrib/user/urls.py,sha256=ktQJ3vZxDlKNUfzOxReeDLOduSdoW5z5Sz0LVFpxZGU,460
|
163
|
-
accrete/contrib/user/views.py,sha256=
|
162
|
+
accrete/contrib/user/views.py,sha256=oNY6HrZpUyCn0NmCRcA93JDwO64dA910F8INkvTo4Hk,3611
|
164
163
|
accrete/contrib/user/locale/de/LC_MESSAGES/django.mo,sha256=p3rgUg6WltAVIMkQsjvjBqTsd_usLhSr1GH4Cyltc2c,433
|
165
164
|
accrete/contrib/user/locale/de/LC_MESSAGES/django.po,sha256=f_Nxpo3HTm2L3f3zoHLfeWsZ-4IQp_EEVSku6TCZSvw,1870
|
166
165
|
accrete/contrib/user/migrations/0001_initial.py,sha256=JWfM9PcMDfkJUdCjLWuWieGs6643qP0KdbCyr5uAZoY,2950
|
@@ -187,10 +186,12 @@ accrete/contrib/user_registration/templates/user_registration/mail_templates/con
|
|
187
186
|
accrete/migrations/0001_initial.py,sha256=azThbc8otEhxJwo8BIgOt5eC30mxXhKJLBAazZFe3BA,4166
|
188
187
|
accrete/migrations/0002_initial.py,sha256=dFOM7kdHlx7pVAh8cTDlZMtciN4O9Z547HAzEKnygZE,1628
|
189
188
|
accrete/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
190
|
-
accrete/utils/__init__.py,sha256=
|
189
|
+
accrete/utils/__init__.py,sha256=BGXx2NwF4Sc6uaenKNvPZEmHuHVY1u97PPECrd1seA8,129
|
191
190
|
accrete/utils/dates.py,sha256=apM6kt6JhGrKgoT0jfav1W-8AUVTxNc9xt3fJQ2n0JI,1492
|
191
|
+
accrete/utils/forms.py,sha256=FHR9IP76dkULCtX4H68xbQG5uvkBQZnDEXSlHyz34iU,606
|
192
|
+
accrete/utils/http.py,sha256=dR4p-Q8xoTlrjfx0sN--vkY4ZGtYUsqqquAfPNxsXx8,3249
|
192
193
|
accrete/utils/models.py,sha256=EEhv7-sQVtQD24PEb3XcDUAh3VVhVFoMMLyFrDjGEaI,706
|
193
|
-
accrete-0.0.
|
194
|
-
accrete-0.0.
|
195
|
-
accrete-0.0.
|
196
|
-
accrete-0.0.
|
194
|
+
accrete-0.0.40.dist-info/METADATA,sha256=51Qq-N3iNatoNK4Ot5au42pWHRyxydqijYO5kqk-PUE,4892
|
195
|
+
accrete-0.0.40.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
|
196
|
+
accrete-0.0.40.dist-info/licenses/LICENSE,sha256=_7laeMIHnsd3Y2vJEXDYXq_PEXxIcjgJsGt8UIKTRWc,1057
|
197
|
+
accrete-0.0.40.dist-info/RECORD,,
|
accrete/queries.py
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
from accrete import models
|
3
|
-
from accrete.tenant import get_tenant
|
4
|
-
|
5
|
-
_logger = logging.getLogger(__name__)
|
6
|
-
|
7
|
-
|
8
|
-
def is_member(tenant, user):
|
9
|
-
return tenant.members.filter(user=user, is_active=True).exists()
|
10
|
-
|
11
|
-
|
12
|
-
def members_for_current_tenant():
|
13
|
-
tenant = get_tenant()
|
14
|
-
return tenant and tenant.members or models.Member.objects.none()
|
15
|
-
|
16
|
-
|
17
|
-
def all_tenants():
|
18
|
-
return models.Tenant.objects.all()
|
File without changes
|
File without changes
|