accrete 0.0.154__py3-none-any.whl → 0.0.156__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. accrete/channels.py +39 -0
  2. accrete/consumer.py +18 -0
  3. accrete/contrib/log/admin.py +0 -16
  4. accrete/contrib/log/migrations/0006_remove_log_accrete_log_model_cf888c_idx_and_more.py +24 -0
  5. accrete/contrib/log/models.py +3 -1
  6. accrete/contrib/log/signals.py +1 -3
  7. accrete/contrib/ui/__init__.py +2 -1
  8. accrete/contrib/ui/config.py +9 -0
  9. accrete/contrib/ui/filter.py +1 -1
  10. accrete/contrib/ui/forms.py +1 -0
  11. accrete/contrib/ui/migrations/0005_theme_base_theme.py +18 -0
  12. accrete/contrib/ui/models.py +9 -0
  13. accrete/contrib/ui/response.py +31 -8
  14. accrete/contrib/ui/static/js/htmx.min.js +1 -1
  15. accrete/contrib/ui/static/js/ws.js +1 -0
  16. accrete/contrib/ui/templates/ui/filter/query_params.html +5 -0
  17. accrete/contrib/ui/templates/ui/layout.html +35 -16
  18. accrete/contrib/ui/templates/ui/list.html +4 -2
  19. accrete/contrib/ui/templates/ui/message.html +5 -3
  20. accrete/contrib/ui/templates/ui/table.html +2 -0
  21. accrete/contrib/ui/templates/ui/templatetags/tenant_quick_switch.html +14 -0
  22. accrete/contrib/ui/templates/ui/widgets/model_search_select_multi.html +2 -2
  23. accrete/contrib/ui/templatetags/ui.py +45 -1
  24. accrete/contrib/user/templates/user/accrete_navbar_end_dropdown.html +1 -1
  25. accrete/contrib/user/templates/user/user_preferences.html +1 -0
  26. accrete/fields.py +13 -0
  27. accrete/middleware.py +1 -0
  28. accrete/migrations/0009_alter_accessgroup_name.py +18 -2
  29. accrete/migrations/0010_alter_accessgroup_description.py +46 -0
  30. accrete/models.py +3 -3
  31. accrete/tenant.py +1 -0
  32. accrete/utils/__init__.py +4 -1
  33. accrete/utils/models.py +18 -0
  34. accrete/utils/views.py +1 -1
  35. {accrete-0.0.154.dist-info → accrete-0.0.156.dist-info}/METADATA +1 -1
  36. {accrete-0.0.154.dist-info → accrete-0.0.156.dist-info}/RECORD +38 -30
  37. {accrete-0.0.154.dist-info → accrete-0.0.156.dist-info}/WHEEL +0 -0
  38. {accrete-0.0.154.dist-info → accrete-0.0.156.dist-info}/licenses/LICENSE +0 -0
@@ -4,7 +4,7 @@
4
4
  {% load partials %}
5
5
  {% load ui %}
6
6
 
7
- <html lang="en" style="overflow: hidden;" {% block theme %}{% if request.user.theme %}data-theme="{{ request.user.theme }}"{% endif %}{% endblock %}>
7
+ <html lang="en" style="overflow: hidden;" {% block theme %}data-theme="{% base_theme request.user %}"{% endblock %}>
8
8
 
9
9
  <head>
10
10
  {% block head %}
@@ -14,9 +14,11 @@
14
14
  <link rel="stylesheet" type="text/css" href="{% static "css/accrete.css" %}?v=0.0.154">
15
15
  <link rel="stylesheet" type="text/css" href="{% static "css/icons.css" %}">
16
16
  <link rel="stylesheet" type="text/css" href="{% static "css/fa.css" %}">
17
+ {% if style_template %}{% include style_template %}{% endif %}
17
18
  {% custom_theme request.user %}
18
19
  <script src="{% static "js/filter.js" %}" type="text/javascript"></script>
19
20
  <script src="{% static "js/htmx.min.js" %}" defer type="text/javascript"></script>
21
+ <script src="{% static "js/ws.js" %}" type="text/javascript" defer></script>
20
22
  <script src="{% static "js/alpine-focus-3.14.9.js" %}" defer type="text/javascript"></script>
21
23
  <script src="{% static "js/alpine-sort-3.14.9.js" %}" defer type="text/javascript"></script>
22
24
  <script src="{% static "js/alpine-3.14.9.js" %}" defer type="text/javascript"></script>
@@ -41,6 +43,14 @@
41
43
  document.body.setAttribute('data-tenant-id', url.searchParams.get('tenant_id'));
42
44
  }
43
45
  })
46
+ {% if debug_htmx_ws %}
47
+ document.addEventListener("htmx:wsAfterMessage", function (evt) {
48
+ console.log("HTMX WS message received:", evt.detail.message);
49
+ });
50
+ document.addEventListener("htmx:wsAfterSend", function (evt) {
51
+ console.log("HTMX WS message sent: ", evt.detail.message);
52
+ });
53
+ {% endif %}
44
54
  </script>
45
55
  {% if script_template %}{% include script_template %}{% endif %}
46
56
  <title>{% block title %}{{ title }}{% endblock %}</title>
@@ -49,7 +59,7 @@
49
59
 
50
60
  {% block body %}
51
61
  <body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' data-tenant-id="{{ tenant.id }}" hx-boost="true" hx-history="false"
52
- x-data="{ showPanel: false, openPanel() { if(window.innerWidth >= 1216) {this.showPanel = true} } }"
62
+ x-data="{ showQuickSwitch: false, showPanel: false, openPanel() { if(window.innerWidth >= 1216) {this.showPanel = true} } }"
53
63
  >
54
64
  <nav id="navbar" class="navbar is-primary is-fixed-top" role="navigation" aria-label="main navigation">
55
65
  <div class="navbar-brand">
@@ -61,11 +71,11 @@
61
71
  </button>
62
72
  </div>
63
73
  {% endif %}
64
- <div class="navbar-item is-unselectable has-text-weight-bold"
65
- onclick="document.getElementById('tenant-quick-switch').classList.toggle('is-invisible')"
66
- >
67
- {{ request.tenant.name }}
68
- </div>
74
+ {% tenant_quick_switch request.user %}
75
+
76
+ {% if navbar_brand_template %}
77
+ {% include navbar_brand_template %}
78
+ {% endif %}
69
79
 
70
80
  <a id="navbar-burger" role="button" class="navbar-burger"
71
81
  aria-label="menu" aria-expanded="false"
@@ -82,7 +92,12 @@
82
92
 
83
93
  <div id="navbar-menu" class="navbar-menu is-fixed-top">
84
94
  <div class="navbar-start">
85
- {% combine_templates 'accrete_navbar.html' request %}
95
+ {% if navbar_start_template %}
96
+ {% include navbar_start_template %}
97
+ {% elif navbar_start_template is False %}
98
+ {% else %}
99
+ {% combine_templates 'accrete_navbar.html' request %}
100
+ {% endif %}
86
101
  </div>
87
102
 
88
103
  <div class="navbar-end">
@@ -113,10 +128,10 @@
113
128
  {% if panel_template %}{% include panel_template %}{% endif %}
114
129
  {% if ui_filter %}
115
130
  <div class="mt-4">
116
- <div class="mx-3 mb-1" style="border-bottom: 1px var(--bulma-grey) solid">
131
+ <div class="mb-1" style="border-bottom: 1px var(--bulma-grey) solid">
117
132
  <span>{% translate 'Filter' %}</span>
118
133
  </div>
119
- <div class="p-3">
134
+ <div class="">
120
135
  {% include 'ui/filter/filter.html' %}
121
136
  </div>
122
137
  </div>
@@ -131,9 +146,11 @@
131
146
  <div id="message" class="" style="position: fixed; top: calc(var(--bulma-navbar-height) + 5px); left: 50%; transform: translateX(-50%); z-index: 999">
132
147
  {% for message in messages %}
133
148
  <div class="mb-2" style="min-width: 330px; max-width: 330px" x-data="{ show: false }" x-show="show" x-cloak="" x-init="show = true; setTimeout(() => show = false, 4000)" x-transition.duration.200ms>
134
- <div class="notification is-light has-text-centered" style="box-shadow: 2px 2px 5px">
149
+ <article class="message {{ message|message_class }}" style="box-shadow: 2px 2px 5px">
150
+ <div class="message-body">
135
151
  {{ message }}
136
- </div>
152
+ <button class="delete" x-on:click="show = false;" style="position: absolute; top: 5px; right: 5px"></button>
153
+ </div></article>
137
154
  </div>
138
155
  {% endfor %}
139
156
  </div>
@@ -146,8 +163,9 @@
146
163
  >
147
164
 
148
165
  <div id="overview-container" class="table-container is-flex is-flex-grow-1 is-flex-direction-column mb-0" x-bind:class="showContentRight ? 'is-hidden-mobile' : ''">
149
- <div id="overview-header">
150
- <div class="is-flex is-justify-content-space-between is-flex-wrap-wrap {% if is_centered %}container{% endif %}">
166
+ {% if config.display_header %}
167
+ <div id="overview-header">
168
+ <div class="is-flex is-justify-content-space-between is-flex-wrap-wrap {% if is_centered %}container{% endif %}">
151
169
  <div class="is-flex is-flex-wrap-wrap is-flex-grow-5 is-align-self-center py-2 mx-1 header-items">
152
170
  {% block overview_header %}
153
171
  <div class="" style="align-self: center">
@@ -189,8 +207,9 @@
189
207
  {% endpartialdef %}
190
208
  </div>
191
209
  {% endif %}
210
+ </div>
192
211
  </div>
193
- </div>
212
+ {% endif %}
194
213
  <div id="overview" class="is-flex-grow-1" style="overflow-y: auto;">
195
214
  <section class="{% if is_centered %}container{% endif %}">
196
215
  {% if overview_template %}{% include overview_template %}{% endif %}
@@ -225,4 +244,4 @@
225
244
  </div>
226
245
  </div>
227
246
  </body>
228
- {% endblock %}
247
+ {% endblock %}
@@ -2,11 +2,12 @@
2
2
  {% load ui %}
3
3
  {% load partials %}
4
4
 
5
+ {% block list %}
5
6
  <div class="fixed-grid has-{{ column_count }}-cols has-1-cols-mobile m-2">
6
7
  <div id="list-grid" class="grid m-0">
7
8
  {% for instance in page.object_list %}
8
9
  {% partialdef entry inline=True %}
9
- <div id="list-entry-{{ instance.pk }}" class="list-entry cell pb-0" style="height: {{ column_height }}">
10
+ <div id="list-entry-{{ instance.pk }}" class="list-entry cell pb-0" style="{% if column_height %}min-height: {{ column_height }}{% endif %}">
10
11
  <div class="box p-3"
11
12
  style="word-break: break-word; height: 100%; overflow-y: auto; {% if instance.get_absolute_url and detail_enabled %}cursor:pointer;{% endif %}"
12
13
  {% if instance.get_absolute_url and detail_enabled %}
@@ -15,7 +16,7 @@
15
16
  {% endif %}
16
17
  x-data="{selected: false}" @unselect-list-entry.window="selected = false"
17
18
  >
18
- <div id="list-data-{{ instance.pk }}">
19
+ <div id="list-data-{{ instance.pk }}" style="height: 100%">
19
20
  {% if list_entry_template %}
20
21
  {% include list_entry_template %}
21
22
  {% else %}
@@ -40,3 +41,4 @@
40
41
  <progress id="endless-scroll-indicator" class="htmx-indicator progress is-small is-success" max="100">15%</progress>
41
42
  {% endif %}
42
43
  </div>
44
+ {% endblock %}
@@ -4,9 +4,11 @@
4
4
  {% for message in messages %}
5
5
  <div hx-swap-oob="{% if append %}beforeend:#message{% else %}innerHTML:#message{% endif %}">
6
6
  <div class="mb-2" style="min-width: 340px; max-width: 340px" x-data="{ show: false }" x-show="show" x-cloak="" x-init="show = true; {% if not persistent %}setTimeout(() => show = false, 4000){% endif %}" x-transition.duration.200ms>
7
- <div class="notification has-text-centered " style="box-shadow: 2px 2px 5px">
8
- <button class="delete" x-on:click="show = false;"></button>
9
- {{ message }}
7
+ <article class="message {{ message|message_class }}" style="box-shadow: 2px 2px 5px">
8
+ <div class="message-body">
9
+ {{ message }}
10
+ <button class="delete" x-on:click="show = false;" style="position: absolute; top: 5px; right: 5px"></button>
11
+ </div></article>
10
12
  </div>
11
13
  </div>
12
14
  </div>
@@ -2,6 +2,7 @@
2
2
  {% load ui %}
3
3
  {% load partials %}
4
4
 
5
+ {% block table %}
5
6
  <table id="content-table" class="table can-compact is-fullwidth is-hoverable is-narrow my-0" hx-indicator=".htmx-indicator" x-data="">
6
7
  <thead style="position: sticky; top: 0; z-index: 10; background-color: var(--bulma-scheme-main)">
7
8
  <tr>
@@ -73,3 +74,4 @@
73
74
  >
74
75
  </div>
75
76
  <progress id="endless-scroll-indicator" class="htmx-indicator progress is-small is-primary" max="100">15%</progress>
77
+ {% endblock %}
@@ -0,0 +1,14 @@
1
+ <div class="navbar-item is-unselectable has-text-weight-bold" style="{% if switch_url %}cursor: pointer;{% endif %}" x-on:click="showQuickSwitch = !showQuickSwitch;">
2
+ {{ tenant.name }}
3
+ </div>
4
+ {% if tenants and switch_url %}
5
+ <article class="p-1 has-text-weight-bold" style="background-color: var(--bulma-body-background-color); position: fixed; top: var(--bulma-navbar-height); left: 0; box-shadow: 2px 2px 5px; border-bottom-right-radius: var(--bulma-radius); min-width: 150px;"
6
+ x-show="showQuickSwitch" x-cloak="" x-on:click.outside="showQuickSwitch = false"
7
+ >
8
+ <ul class="hoverable mr-1">
9
+ {% for t in tenants %}
10
+ <li class="title is-6 mb-1 p-2" style="cursor: pointer; border-radius: var(--bulma-radius);"><a href="{{ switch_url }}?tenant_id={{ t.pk }}" hx-boost="false" style="width: 100%; display: inline-block">{{ t.name }}</a></li>
11
+ {% endfor %}
12
+ </ul>
13
+ </article>
14
+ {% endif %}
@@ -10,8 +10,8 @@
10
10
  <option selected value="{{ obj.pk }}"></option>
11
11
  {% endfor %}
12
12
  </select>
13
- <div id="{{ widget.attrs.id }}_display" class="input select py-1" tabindex="0" x-ref="inputDisplay" aria-label="inputDisplay"
14
- style="padding-right: 30px; min-height: var(--bulma-control-height); height: fit-content; width: 100%"
13
+ <div id="{{ widget.attrs.id }}_display" class="input select pt-1 pb-0" tabindex="0" x-ref="inputDisplay" aria-label="inputDisplay"
14
+ style="padding-left: 5px; padding-right: 30px; min-height: var(--bulma-control-height); height: fit-content; width: 100%"
15
15
  x-on:keydown="if (![9, 27, 38, 40].includes($event.keyCode)) {$refs.searchInput.focus();}"
16
16
  x-on:click.stop="if (!$event.target.classList.contains('delete')) {if (open) {open = false; } else {open = true; $refs.searchInput.focus();}}"
17
17
  >
@@ -3,6 +3,7 @@ import re
3
3
  from datetime import datetime, date, timedelta
4
4
 
5
5
  from django.contrib.auth import get_user_model
6
+ from django.shortcuts import resolve_url
6
7
  from django.utils.translation import gettext_lazy as _
7
8
  from django import template
8
9
  from django.db.models import (
@@ -13,7 +14,9 @@ from django.template.loader import render_to_string
13
14
  from django.utils.safestring import mark_safe
14
15
  from django.forms import widgets
15
16
  from accrete.contrib.ui.models import Theme
16
- from accrete.tenant import get_tenant
17
+ from accrete.tenant import get_tenant, unscoped
18
+ from accrete.models import Tenant, Member
19
+ from accrete.contrib.ui import config
17
20
 
18
21
  _logger = logging.getLogger(__name__)
19
22
  register = template.Library()
@@ -123,6 +126,7 @@ def message_class(message):
123
126
  return 'is-warning'
124
127
  if message.level == 40:
125
128
  return 'is-danger'
129
+ return ''
126
130
 
127
131
 
128
132
  @register.filter(name='timedelta_cast')
@@ -211,3 +215,43 @@ def custom_theme(user: User) -> str:
211
215
  if user.theme == 'preset' and tenant_theme:
212
216
  return mark_safe(tenant_theme.theme_markup)
213
217
  return ''
218
+
219
+
220
+ @register.simple_tag(name='base_theme')
221
+ def base_theme(user: User) -> str:
222
+ if user.is_anonymous:
223
+ return 'light'
224
+ tenant = get_tenant()
225
+ tenant_theme = Theme.objects.filter(
226
+ tenant=tenant, tenant__isnull=False
227
+ ).first()
228
+ if tenant_theme and tenant_theme.force_tenant_theme:
229
+ return tenant_theme.base_theme
230
+ if user.theme == 'custom':
231
+ theme = Theme.objects.filter(user=user).first()
232
+ return theme and theme.base_theme or 'light'
233
+ if user.theme == 'preset' and tenant_theme:
234
+ return tenant_theme.base_theme
235
+ return user.theme
236
+
237
+
238
+ @register.simple_tag(name='tenant_quick_switch')
239
+ def tenant_quick_switch(user: User):
240
+ templ = 'ui/templatetags/tenant_quick_switch.html'
241
+ tenant = get_tenant()
242
+ with unscoped():
243
+ tenants = Tenant.objects.filter(
244
+ members__in=Member.objects.filter(user=user)
245
+ )
246
+ if tenant:
247
+ tenants = tenants.exclude(pk=tenant.pk)
248
+ switch_url = getattr(config, 'ACCRETE_TENANT_QUICK_SWITCH_URL')
249
+ if switch_url:
250
+ switch_url = resolve_url(switch_url)
251
+ return mark_safe(render_to_string(
252
+ templ, context={
253
+ 'tenant': tenant,
254
+ 'tenants': tenants,
255
+ 'switch_url': switch_url
256
+ }
257
+ ))
@@ -1,6 +1,6 @@
1
1
  {% load i18n %}
2
2
 
3
- <a class="navbar-item" href="{% url 'user:detail' %}">{% translate 'Preferences' %}</a>
3
+ <a class="navbar-item" href="{% url 'user:detail' %}" hx-boost="false">{% translate 'Preferences' %}</a>
4
4
  {% if request.user.is_staff %}
5
5
  <a class="navbar-item" hx-boost="false" href="/admin/">Admin</a>
6
6
  {% endif %}
@@ -43,6 +43,7 @@
43
43
  <form id="theme-form" hx-post="{% url 'user:detail' %}" hx-trigger="change changed" hx-select="#theme-form" hx-swap="outerHTML">
44
44
  {{ theme_form.user }}
45
45
  <div class="columns is-multiline">
46
+ <div class="column">{{ theme_form.base_theme|wrap_form_field }}</div>
46
47
  <div class="column">{{ theme_form.color_primary|wrap_form_field }}</div>
47
48
  <div class="column">{{ theme_form.color_success|wrap_form_field }}</div>
48
49
  <div class="column">{{ theme_form.color_link|wrap_form_field }}</div>
accrete/fields.py CHANGED
@@ -129,3 +129,16 @@ class TranslatedCharField(JSONField):
129
129
  if form_class is None:
130
130
  form_class = forms.CharField
131
131
  return form_class(**defaults)
132
+
133
+
134
+ class TranslatedTextField(TranslatedCharField):
135
+
136
+ def formfield(self, form_class=None, choices_form_class=None, **kwargs):
137
+ if form_class is None:
138
+ form_class = forms.CharField
139
+ kwargs.update(widget=forms.Textarea)
140
+ return super().formfield(
141
+ form_class=form_class,
142
+ choices_form_class=choices_form_class,
143
+ **kwargs
144
+ )
accrete/middleware.py CHANGED
@@ -9,6 +9,7 @@ _logger = logging.getLogger(__name__)
9
9
 
10
10
 
11
11
  class TenantMiddleware(MiddlewareMixin):
12
+
12
13
  @staticmethod
13
14
  def get_tenant_id_from_request(request):
14
15
  tenant_id = (
@@ -10,10 +10,26 @@ def char_to_translated_char(apps, schema):
10
10
  AccessGroup = apps.get_model('accrete', 'AccessGroup')
11
11
  default_lang = getattr(settings, 'LANGUAGE_CODE', 'en-us')
12
12
  for group in AccessGroup.objects.all():
13
- group.name = json.dumps({default_lang: group.name})
13
+ try:
14
+ name = json.dumps(json.loads(group.name).get(default_lang))
15
+ if not name:
16
+ name = None
17
+ except Exception:
18
+ name = group.name or None
19
+ group.name = name
14
20
  group.save()
15
21
 
16
22
 
23
+ def translated_to_char(apps, schema):
24
+ AccessGroup = apps.get_model('accrete', 'AccessGroup')
25
+ default_lang = getattr(settings, 'LANGUAGE_CODE', 'en-us')
26
+ for group in AccessGroup.objects.all():
27
+ try:
28
+ group.name = json.loads(group.name).get(default_lang, '')
29
+ except Exception:
30
+ continue
31
+
32
+
17
33
  class Migration(migrations.Migration):
18
34
 
19
35
  dependencies = [
@@ -21,7 +37,7 @@ class Migration(migrations.Migration):
21
37
  ]
22
38
 
23
39
  operations = [
24
- migrations.RunPython(char_to_translated_char),
40
+ migrations.RunPython(char_to_translated_char, translated_to_char),
25
41
  migrations.AlterField(
26
42
  model_name='accessgroup',
27
43
  name='name',
@@ -0,0 +1,46 @@
1
+ # Generated by Django 5.2.7 on 2025-10-04 06:20
2
+
3
+ import json
4
+ import accrete.fields
5
+ from django.db import migrations
6
+ from django.conf import settings
7
+
8
+
9
+ def char_to_translated_char(apps, schema):
10
+ AccessGroup = apps.get_model('accrete', 'AccessGroup')
11
+ default_lang = getattr(settings, 'LANGUAGE_CODE', 'en-us')
12
+ for group in AccessGroup.objects.all():
13
+ try:
14
+ description = json.dumps(json.loads(group.description).get(default_lang))
15
+ if not description:
16
+ description = None
17
+ except Exception:
18
+ description = group.description or None
19
+ group.description = description
20
+ group.save()
21
+
22
+
23
+ def translated_to_char(apps, schema):
24
+ AccessGroup = apps.get_model('accrete', 'AccessGroup')
25
+ default_lang = getattr(settings, 'LANGUAGE_CODE', 'en-us')
26
+ for group in AccessGroup.objects.all():
27
+ try:
28
+ group.description = json.loads(group.description).get(default_lang, '')
29
+ except Exception:
30
+ continue
31
+
32
+
33
+ class Migration(migrations.Migration):
34
+
35
+ dependencies = [
36
+ ('accrete', '0009_alter_accessgroup_name'),
37
+ ]
38
+
39
+ operations = [
40
+ migrations.RunPython(char_to_translated_char, translated_to_char),
41
+ migrations.AlterField(
42
+ model_name='accessgroup',
43
+ name='description',
44
+ field=accrete.fields.TranslatedTextField(blank=True, null=True, verbose_name='Description'),
45
+ ),
46
+ ]
accrete/models.py CHANGED
@@ -4,7 +4,7 @@ from django.conf import settings
4
4
  from django.utils.translation import gettext_lazy as _
5
5
  from accrete.tenant import get_tenant
6
6
  from accrete.managers import TenantManager, MemberManager, AccessGroupManager
7
- from accrete.fields import TranslatedCharField
7
+ from accrete.fields import TranslatedCharField, TranslatedTextField
8
8
 
9
9
 
10
10
  class TenantModel(models.Model):
@@ -138,7 +138,7 @@ class AccessGroup(models.Model):
138
138
  verbose_name=_('Name')
139
139
  )
140
140
 
141
- description = models.TextField(
141
+ description = TranslatedTextField(
142
142
  verbose_name=_('Description'),
143
143
  null=True,
144
144
  blank=True
@@ -160,7 +160,7 @@ class AccessGroup(models.Model):
160
160
  )
161
161
 
162
162
  def __str__(self):
163
- return self.name
163
+ return f'{self.name} ({self.get_apply_on_display()})'
164
164
 
165
165
 
166
166
  class MemberAccessGroupRel(models.Model):
accrete/tenant.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import contextvars
2
2
  import logging
3
3
  import time
4
+
4
5
  from django.apps import apps
5
6
  from django.db.models import Q, QuerySet
6
7
 
accrete/utils/__init__.py CHANGED
@@ -14,4 +14,7 @@ from .views import (
14
14
  method_not_allowed,
15
15
  render_templates
16
16
  )
17
- from .models import get_related_model
17
+ from .models import (
18
+ get_related_model,
19
+ translated_db_value
20
+ )
accrete/utils/models.py CHANGED
@@ -1,3 +1,5 @@
1
+ import json
2
+ from django.db import connection
1
3
  from django.db.models import Model, Field
2
4
 
3
5
 
@@ -25,3 +27,19 @@ def get_related_field(model: type[Model], field_path: str) -> Field:
25
27
  rel_path = '__'.join(parts[:-1])
26
28
  rel_model, _ = get_related_model(model, rel_path)
27
29
  return rel_model._meta.get_field(parts[-1])
30
+
31
+
32
+ def translated_db_value(instance: Model, field: str) -> dict:
33
+ if not instance.pk:
34
+ return dict()
35
+ with connection.cursor() as cr:
36
+ query = """SELECT %s FROM %s WHERE id = %s""" % (
37
+ field,
38
+ instance._meta.db_table,
39
+ instance.pk
40
+ )
41
+ cr.execute(query)
42
+ row = cr.fetchone()
43
+ row = row and row[0]
44
+ db_value = row and json.loads(row) or dict()
45
+ return db_value
accrete/utils/views.py CHANGED
@@ -67,7 +67,7 @@ def parse_querystring(model: type[Model], query_string: str) -> Q:
67
67
  """
68
68
  param: query_string: JSON serializable string
69
69
  Parses
70
- [{"term": value}, "&", [{t"erm": value}, "|", {"~term": value}]]
70
+ [{"term": value}, "&", [{"term": value}, "|", {"~term": value}]]
71
71
  to
72
72
  Q(term=value) & (Q(term=value) | ~Q(term=value))
73
73
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: accrete
3
- Version: 0.0.154
3
+ Version: 0.0.156
4
4
  Summary: Django Shared Schema Multi Tenant
5
5
  Author-email: Benedikt Jilek <benedikt.jilek@pm.me>
6
6
  License: Copyright (c) 2025 Benedikt Jilek