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 CHANGED
@@ -1,4 +1,4 @@
1
- from django.db.models import Field, QuerySet
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
- @staticmethod
38
- def add_annotations(queryset: QuerySet):
39
- model = queryset.model
40
- if not hasattr(model, 'get_annotations'):
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
+ }
@@ -8,7 +8,7 @@ from .context import (
8
8
  list_page,
9
9
  detail_page,
10
10
  cast_param,
11
- prepare_url_params,
11
+ url_param_dict,
12
12
  extract_url_params,
13
13
  exclude_params,
14
14
  url_param_str,
@@ -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 prepare_url_params(get_params: dict) -> dict:
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 = prepare_url_params(extract_url_params(params, extract))
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
- print(e)
244
+ _logger.error(e)
237
245
  pass
246
+
238
247
  return table_fields
@@ -69,6 +69,7 @@ class TableField:
69
69
  suffix: str = ''
70
70
  truncate_after: int = 0
71
71
  template: str = None
72
+ is_relation: bool = False
72
73
 
73
74
 
74
75
  @dataclass
@@ -298,14 +298,25 @@ class Filter:
298
298
 
299
299
  def field_path_selection(self):
300
300
  html = ''
301
- for field in sorted(self.field_paths, key=lambda f: (len(f.split('__')), f.lower())):
302
- label = self.get_field_path_label(field)
303
- if not label:
304
- continue
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
- {label}
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-primary');
256
+ button.classList.add('is-success');
253
257
  }
254
258
  else {
255
- button.classList.remove('is-primary');
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 input = document.getElementById('field-path-search-input');
450
- const paths = document.getElementById('field-path-checkboxes').children;
451
- console.log(paths)
452
- console.log(paths.length)
453
- for (let i = 0; paths.length < i; i++) {
454
- if (!paths[i].firstElementChild.name.contains(input.value)) {
455
- paths[i].classList.add('is-hidden');
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
- // paths.forEach(el => {
459
- // if (!el.getAttribute('name').contains(input.value)) {
460
- // el.classList.add('is-hidden');
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
- <progress class="htmx-indicator progress is-small is-primary" max="100">15%</progress>
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
- <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 }}
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-path-change">
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
- </div>
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.prefix }}
39
- {% block td_content %}
40
- {% if field.truncate_after > 0 %}
41
- {{ val|truncatechars:field.truncate_after }}
42
- {% else %}
43
- {{ val }}
44
- {% endif %}
45
- {% endblock %}
46
- {{ field.suffix }}
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-select-oob="#list-pagination-end-index,#list-pagination-next-button,#endless-scroller"
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-primary" max="100">15%</progress>
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
- 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 ''
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')
@@ -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.forms import save_form
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, AnnotationManagerMixin
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
@@ -0,0 +1,4 @@
1
+ from . import dates
2
+ from .forms import save_form
3
+ from .http import filter_from_querystring
4
+ from .models import get_related_model
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], query_string: str, order: list[str] = None
19
+ model: type[Model], get_params: dict, key_map: dict = None
13
20
  ) -> QuerySet:
14
21
 
15
- order = order or model._meta.ordering
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, query_string)
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[len(x):]): value
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: accrete
3
- Version: 0.0.38
3
+ Version: 0.0.40
4
4
  Summary: Django Shared Schema Multi Tenant
5
5
  Author-email: Benedikt Jilek <benedikt.jilek@pm.me>
6
6
  License: Copyright (c) 2023 Benedikt Jilek
@@ -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=-nYqDQPOixkD9qdzVZDCIA4lETH2fAZcCSxNmaY2s80,1251
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=nPDgSZao-vuVFRam2Py18yiet3Bar-A-qkWjwbeUDlg,11235
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=ruPKqNeKYzqSpYRhpQ12S82iJDYxy0EjSEtNSKWeRz4,6126
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=D_QP3FW_4IzP2c_4WTWyuRIENDA1AFrSOXWL_FNeSMY,445
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=IV66Dtac-MoHelVWrCmFlXQnHZ1XMfwBEYsdZ59-sbg,6870
40
- accrete/contrib/ui/elements.py,sha256=g6wksl7hHGJb3CKmkio5rWgcpr1Eyr2vxbKVD-59lkc,1570
41
- accrete/contrib/ui/filter.py,sha256=0E5dSd58s1mdDhz8WqIX19e0jPBUlEPi5NWFL8PImgA,13050
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=wMK4rIx4J8ps3uB6J1z2wRAZ5MzkAN6D8EhFQiXybjM,22864
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=dcPS1aSN9GlsljJsTNhKMqgPe8goF1uwknZ4DIbElLU,1212
141
- accrete/contrib/ui/templates/ui/table.html,sha256=xVSFKrjO_2YkSEO3nspHqMj8mQU4ZV8GSAQFSsAk5SI,3868
142
- accrete/contrib/ui/templates/ui/partials/filter.html,sha256=Y2-9fwV_sjrsu-Jvuh8hseku0mQTjsnePSHemps3Tqg,3936
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=9h-I5PfjViomZjoeBUZIwmx7tgWodCs4bDtjOdsPQ8c,1379
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=NBwRhaqojUGSZal7WI-Ugzl-Qiw6joygarP7B3CsfZc,1462
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=82-RPqabxE6o7cglnfZmCUxNNRii4pmVFGiTThE4Zp0,3727
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=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
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.38.dist-info/METADATA,sha256=qCGJ5Bvnu6mpV3aC_R8hV6bMl2VYNjmeX2T_AhoHFqA,4892
194
- accrete-0.0.38.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
195
- accrete-0.0.38.dist-info/licenses/LICENSE,sha256=_7laeMIHnsd3Y2vJEXDYXq_PEXxIcjgJsGt8UIKTRWc,1057
196
- accrete-0.0.38.dist-info/RECORD,,
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()