accrete 0.0.149__py3-none-any.whl → 0.0.151__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/contrib/log/queries.py +3 -1
- accrete/contrib/ui/admin.py +9 -1
- accrete/contrib/ui/forms.py +57 -0
- accrete/contrib/ui/migrations/0001_initial.py +39 -0
- accrete/contrib/ui/migrations/0002_alter_theme_color_danger_alter_theme_color_link_and_more.py +38 -0
- accrete/contrib/ui/migrations/0003_alter_theme_check_user_or_tenant.py +21 -0
- accrete/contrib/ui/migrations/0004_theme_force_tenant_theme.py +18 -0
- accrete/contrib/ui/models.py +115 -1
- accrete/contrib/ui/response.py +14 -5
- accrete/contrib/ui/static/css/accrete.css +23 -57
- accrete/contrib/ui/static/css/accrete.css.map +1 -1
- accrete/contrib/ui/static/css/accrete.scss +76 -55
- accrete/contrib/ui/templates/django/forms/widgets/input.html +1 -1
- accrete/contrib/ui/templates/ui/custom_theme.html +19 -0
- accrete/contrib/ui/templates/ui/layout.html +9 -9
- accrete/contrib/ui/templates/ui/list.html +2 -2
- accrete/contrib/ui/templates/ui/message.html +2 -2
- accrete/contrib/ui/templates/ui/modal.html +3 -3
- accrete/contrib/ui/templates/ui/table.html +5 -5
- accrete/contrib/ui/templates/ui/templatetags/field.html +50 -11
- accrete/contrib/ui/templates/ui/widgets/date_weekday.html +10 -0
- accrete/contrib/ui/templates/ui/widgets/model_search_select.html +4 -3
- accrete/contrib/ui/templates/ui/widgets/model_search_select_multi.html +5 -4
- accrete/contrib/ui/templatetags/ui.py +37 -5
- accrete/contrib/ui/views.py +90 -2
- accrete/contrib/ui/widgets/__init__.py +1 -0
- accrete/contrib/ui/widgets/date_weekday.py +6 -0
- accrete/contrib/ui/widgets/search_select.py +2 -2
- accrete/contrib/user/forms.py +1 -1
- accrete/contrib/user/migrations/0009_alter_user_theme.py +18 -0
- accrete/contrib/user/migrations/0010_alter_user_theme.py +18 -0
- accrete/contrib/user/models.py +5 -3
- accrete/contrib/user/templates/user/login.html +3 -11
- accrete/contrib/user/templates/user/user_preferences.html +27 -15
- accrete/contrib/user/views.py +7 -2
- accrete/fields.py +4 -2
- accrete/managers.py +9 -0
- accrete/migrations/0009_alter_accessgroup_name.py +30 -0
- accrete/models.py +6 -4
- accrete/views.py +32 -20
- {accrete-0.0.149.dist-info → accrete-0.0.151.dist-info}/METADATA +1 -1
- {accrete-0.0.149.dist-info → accrete-0.0.151.dist-info}/RECORD +44 -33
- {accrete-0.0.149.dist-info → accrete-0.0.151.dist-info}/WHEEL +0 -0
- {accrete-0.0.149.dist-info → accrete-0.0.151.dist-info}/licenses/LICENSE +0 -0
accrete/contrib/ui/views.py
CHANGED
@@ -1,9 +1,97 @@
|
|
1
1
|
import json
|
2
|
-
|
3
2
|
from django.contrib.auth.decorators import login_required
|
3
|
+
from django.views.generic import View
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
4
5
|
from django.apps import apps
|
5
6
|
from django.http import HttpResponse
|
6
|
-
from .
|
7
|
+
from accrete.views import TenantRequiredMixin
|
8
|
+
from accrete.models import AccessGroup
|
9
|
+
from accrete.contrib.ui.filter import Filter
|
10
|
+
from accrete.contrib.ui.response import WindowResponse, ModalResponse
|
11
|
+
|
12
|
+
|
13
|
+
class TenantView(TenantRequiredMixin, View):
|
14
|
+
|
15
|
+
"""
|
16
|
+
Base View that handles displaying access denied messages
|
17
|
+
to the user if member/tenant groups are missing.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def handle_tenant_group_not_set(self):
|
21
|
+
if not self._is_htmx():
|
22
|
+
return self._access_denied_page_response()
|
23
|
+
return self._access_denied_modal_response()
|
24
|
+
|
25
|
+
def handle_member_group_not_set(self):
|
26
|
+
if not self._is_htmx():
|
27
|
+
return self._access_denied_page_response()
|
28
|
+
return self._access_denied_modal_response()
|
29
|
+
|
30
|
+
def _access_denied_page_response(self):
|
31
|
+
return WindowResponse(
|
32
|
+
title=str(_('Access Denied')),
|
33
|
+
overview_template='mirox/base/group_not_set.html',
|
34
|
+
context=dict(groups=self._get_group_data()),
|
35
|
+
is_centered=True
|
36
|
+
).response(self.request)
|
37
|
+
|
38
|
+
def _access_denied_modal_response(self):
|
39
|
+
res = ModalResponse(
|
40
|
+
template='mirox/base/group_not_set_modal.html',
|
41
|
+
title=str(_('Access Denied')),
|
42
|
+
modal_id='group-missing-modal',
|
43
|
+
context=dict(groups=self._get_group_data())
|
44
|
+
).response(self.request)
|
45
|
+
res.headers['HX-Reswap'] = 'none'
|
46
|
+
res.headers['HX-Push-Url'] = 'false'
|
47
|
+
return res
|
48
|
+
|
49
|
+
def _get_group_data(self) -> dict:
|
50
|
+
data = {}
|
51
|
+
tenant_groups, member_groups = self._flat_groups()
|
52
|
+
if tenant_groups:
|
53
|
+
data.update(tenant_groups=[])
|
54
|
+
access_groups = AccessGroup.objects.filter(
|
55
|
+
code__in=tenant_groups,
|
56
|
+
apply_on='tenant'
|
57
|
+
).all()
|
58
|
+
group_data = {item[0]: item[1] for item in access_groups.values_list('code', 'name')}
|
59
|
+
for group in self.TENANT_GROUPS:
|
60
|
+
if isinstance(group, tuple):
|
61
|
+
data['tenant_groups'].append(' & '.join([group_data.get(g, g) for g in group]))
|
62
|
+
else:
|
63
|
+
data['tenant_groups'].append(group_data.get(group, group))
|
64
|
+
if member_groups:
|
65
|
+
data.update(member_groups=[])
|
66
|
+
access_groups = AccessGroup.objects.filter(
|
67
|
+
code__in=member_groups,
|
68
|
+
apply_on='member'
|
69
|
+
).all()
|
70
|
+
group_data = {item[0]: item[1] for item in access_groups.values_list('code', 'name')}
|
71
|
+
for group in self.MEMBER_GROUPS:
|
72
|
+
if isinstance(group, tuple):
|
73
|
+
data['member_groups'].append(' & '.join([group_data.get(g, g) for g in group]))
|
74
|
+
else:
|
75
|
+
data['member_groups'].append(group_data.get(group, group))
|
76
|
+
return data
|
77
|
+
|
78
|
+
def _flat_groups(self) -> tuple[list[str], list[str]]:
|
79
|
+
def group_list(g):
|
80
|
+
if isinstance(g, str):
|
81
|
+
return [g]
|
82
|
+
elif isinstance(g, tuple):
|
83
|
+
return [x for x in g]
|
84
|
+
return []
|
85
|
+
tenant_groups = []
|
86
|
+
member_groups = []
|
87
|
+
for group in self.TENANT_GROUPS:
|
88
|
+
tenant_groups.extend(group_list(group))
|
89
|
+
for group in self.MEMBER_GROUPS:
|
90
|
+
member_groups.extend(group_list(group))
|
91
|
+
return tenant_groups, member_groups
|
92
|
+
|
93
|
+
def _is_htmx(self):
|
94
|
+
return self.request.headers.get('HX-Request', 'false') == 'true'
|
7
95
|
|
8
96
|
|
9
97
|
@login_required
|
@@ -22,7 +22,7 @@ class ModelSearchSelect(widgets.NumberInput):
|
|
22
22
|
self.search_kwargs = search_kwargs or {}
|
23
23
|
self.search_parameter = search_parameter
|
24
24
|
self.limit = limit
|
25
|
-
self.hx_trigger_on_change = hx_trigger_on_change # trigger
|
25
|
+
self.hx_trigger_on_change = hx_trigger_on_change # trigger htmx request on change
|
26
26
|
self.choices = choices
|
27
27
|
|
28
28
|
def get_context(self, name, value, attrs):
|
@@ -82,7 +82,7 @@ class ModelSearchSelectMulti(widgets.SelectMultiple):
|
|
82
82
|
self.search_kwargs = search_kwargs or {}
|
83
83
|
self.search_parameter = search_parameter
|
84
84
|
self.limit = limit
|
85
|
-
self.hx_trigger_on_change = hx_trigger_on_change
|
85
|
+
self.hx_trigger_on_change = hx_trigger_on_change # trigger 'change' event on the actual input field
|
86
86
|
self.choices = choices
|
87
87
|
|
88
88
|
def get_context(self, name, value, attrs):
|
accrete/contrib/user/forms.py
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Generated by Django 5.2.1 on 2025-08-07 16:34
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('user', '0008_remove_user_no_email_for_managed_user_and_more'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AlterField(
|
14
|
+
model_name='user',
|
15
|
+
name='theme',
|
16
|
+
field=models.CharField(choices=[('light', 'Light'), ('dark', 'Dark'), ('custom', 'Custom')], default='light', max_length=50, verbose_name='Theme'),
|
17
|
+
),
|
18
|
+
]
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Generated by Django 5.2.1 on 2025-08-08 06:13
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('user', '0009_alter_user_theme'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AlterField(
|
14
|
+
model_name='user',
|
15
|
+
name='theme',
|
16
|
+
field=models.CharField(choices=[('preset', 'Preset'), ('light', 'Light'), ('dark', 'Dark'), ('custom', 'Custom')], default='preset', max_length=50, verbose_name='Theme'),
|
17
|
+
),
|
18
|
+
]
|
accrete/contrib/user/models.py
CHANGED
@@ -181,10 +181,12 @@ class User(AbstractBaseUser, PermissionsMixin):
|
|
181
181
|
verbose_name=_('Theme'),
|
182
182
|
max_length=50,
|
183
183
|
choices=[
|
184
|
-
('
|
185
|
-
('
|
184
|
+
('preset', _('Preset')),
|
185
|
+
('light', _('Light')),
|
186
|
+
('dark', _('Dark')),
|
187
|
+
('custom', _('Custom'))
|
186
188
|
],
|
187
|
-
default='
|
189
|
+
default='preset'
|
188
190
|
)
|
189
191
|
|
190
192
|
objects = UserManager()
|
@@ -1,6 +1,7 @@
|
|
1
1
|
{% extends 'ui/layout.html' %}
|
2
2
|
{% load static %}
|
3
3
|
{% load i18n %}
|
4
|
+
{% load ui %}
|
4
5
|
|
5
6
|
{% block title %}Login{% endblock %}
|
6
7
|
{% block theme %}data-theme="light"{% endblock %}
|
@@ -30,17 +31,8 @@
|
|
30
31
|
<div class="column box is-8-tablet is-offset-2-tablet is-6-desktop is-offset-3-desktop is-4-fullhd is-offset-4-fullhd p-3" data-theme="light">
|
31
32
|
<form method="POST" action="" hx-boost="false">
|
32
33
|
{% csrf_token %}
|
33
|
-
|
34
|
-
|
35
|
-
<span class="has-text-black">{{ form.username.label_tag }}</span>
|
36
|
-
{{ form.username }}
|
37
|
-
<span class="has-text-danger is-size-7">{{ form.username.errors }}</span>
|
38
|
-
</label>
|
39
|
-
<label class="label">
|
40
|
-
<span class="has-text-black">{{ form.password.label_tag }}</span>
|
41
|
-
{{ form.password }}
|
42
|
-
<span class="has-text-danger is-size-7">{{ form.password.errors }}</span>
|
43
|
-
</label>
|
34
|
+
{{ form.username|wrap_form_field }}
|
35
|
+
{{ form.password|wrap_form_field }}
|
44
36
|
<input type="submit" class="button is-fullwidth is-success is-medium"
|
45
37
|
value="{% translate 'Login' %}">
|
46
38
|
</form>
|
@@ -15,29 +15,41 @@
|
|
15
15
|
{% endpartialdef %}
|
16
16
|
|
17
17
|
{% partialdef data %}
|
18
|
-
<section class="box">
|
18
|
+
<section class="box" x-data="{themeType: '{{ form.theme.value }}'}">
|
19
19
|
<form id="user-form" hx-post="{% url 'user:detail' %}" hx-trigger="change changed" hx-select="#user-form" hx-swap="outerHTML" hx-select-oob="#navbar-user-name">
|
20
20
|
{% csrf_token %}
|
21
21
|
<div class="columns">
|
22
22
|
<div class="column">
|
23
23
|
{% include 'ui/form_error.html' %}
|
24
|
+
{% include 'ui/form_error.html' with form=theme_form %}
|
24
25
|
</div>
|
25
26
|
</div>
|
26
|
-
<div class="columns">
|
27
|
-
<div class="column is-6">
|
28
|
-
|
29
|
-
{{
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
<div class="column is-6">
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
{{ form.language_code|wrap_form_field }}
|
38
|
-
{{ form.theme|wrap_form_field }}
|
39
|
-
</div>
|
27
|
+
<div class="columns is-multiline">
|
28
|
+
<div class="column is-6">{{ form.username|wrap_form_field }}</div>
|
29
|
+
{% if not user.is_managed %}
|
30
|
+
<div class="column is-6">{{ user|wrap_model_field:'email' }}</div>
|
31
|
+
{% else %}
|
32
|
+
<div class="column is-6"></div>
|
33
|
+
{% endif %}
|
34
|
+
<div class="column is-6">{{ form.first_name|wrap_form_field }}</div>
|
35
|
+
<div class="column is-6">{{ form.last_name|wrap_form_field }}</div>
|
36
|
+
<div class="column is-6">{{ form.language_code|wrap_form_field }}</div>
|
37
|
+
<div class="column is-6">{{ form.theme|wrap_form_field }}</div>
|
40
38
|
</div>
|
41
39
|
</form>
|
40
|
+
<div id="theme" x-show="['custom'].includes(themeType)">
|
41
|
+
<p class="title is-size-5 mb-0 mt-4">{% translate 'Custom Theme' %}</p>
|
42
|
+
<hr class="mt-1"/>
|
43
|
+
<form id="theme-form" hx-post="{% url 'user:detail' %}" hx-trigger="change changed" hx-select="#theme-form" hx-swap="outerHTML">
|
44
|
+
{{ theme_form.user }}
|
45
|
+
<div class="columns is-multiline">
|
46
|
+
<div class="column">{{ theme_form.color_primary|wrap_form_field }}</div>
|
47
|
+
<div class="column">{{ theme_form.color_success|wrap_form_field }}</div>
|
48
|
+
<div class="column">{{ theme_form.color_link|wrap_form_field }}</div>
|
49
|
+
<div class="column">{{ theme_form.color_warning|wrap_form_field }}</div>
|
50
|
+
<div class="column">{{ theme_form.color_danger|wrap_form_field }}</div>
|
51
|
+
</div>
|
52
|
+
</form>
|
53
|
+
</div>
|
42
54
|
</section>
|
43
55
|
{% endpartialdef %}
|
accrete/contrib/user/views.py
CHANGED
@@ -8,6 +8,8 @@ from django.conf import settings
|
|
8
8
|
|
9
9
|
from accrete.utils import save_form
|
10
10
|
from accrete.contrib import ui
|
11
|
+
from accrete.contrib.ui.models import Theme
|
12
|
+
from accrete.contrib.ui.forms import ThemeForm
|
11
13
|
from .forms import UserForm, ChangePasswordForm, ChangeEmailForm
|
12
14
|
|
13
15
|
|
@@ -40,16 +42,19 @@ def user_detail(request):
|
|
40
42
|
initial={'language_code': request.user.language_code},
|
41
43
|
instance=request.user
|
42
44
|
)
|
45
|
+
theme = Theme.objects.filter(user=request.user).first()
|
46
|
+
theme_form = ThemeForm(instance=theme, prefix='theme', initial={'user': request.user})
|
43
47
|
refresh = False
|
44
48
|
if request.method == 'POST':
|
45
49
|
form = save_form(UserForm(request.POST, instance=request.user))
|
46
|
-
|
50
|
+
theme_form = save_form(ThemeForm(request.POST, instance=theme, prefix='theme', initial={'user': request.user}))
|
51
|
+
if (form.is_saved or not form.has_changed()) or (theme_form.is_saved or not theme_form.has_changed()):
|
47
52
|
refresh = True
|
48
53
|
res = ui.WindowResponse(
|
49
54
|
title=str(_('User Preferences')),
|
50
55
|
overview_template='user/user_preferences.html#data',
|
51
56
|
header_template='user/user_preferences.html#header',
|
52
|
-
context=dict(user=request.user, form=form),
|
57
|
+
context=dict(user=request.user, form=form, theme_form=theme_form),
|
53
58
|
is_centered=True
|
54
59
|
).response(request, replace_body=False)
|
55
60
|
if refresh:
|
accrete/fields.py
CHANGED
@@ -49,8 +49,10 @@ class TranslatedCharField(JSONField):
|
|
49
49
|
if new_val in [None, False, '']:
|
50
50
|
return {self.default_language: ''}
|
51
51
|
if isinstance(new_val, dict):
|
52
|
-
|
53
|
-
|
52
|
+
old_val.update(new_val)
|
53
|
+
# new_val = new_val.get(language, '')
|
54
|
+
else:
|
55
|
+
old_val.update({language: new_val})
|
54
56
|
if language != self.default_language and not old_val.get(self.default_language):
|
55
57
|
old_val.update({self.default_language: new_val})
|
56
58
|
return old_val
|
accrete/managers.py
CHANGED
@@ -47,3 +47,12 @@ class MemberManager(TenantManager):
|
|
47
47
|
def get_queryset(self):
|
48
48
|
queryset = super().get_queryset().select_related('tenant', 'user')
|
49
49
|
return queryset
|
50
|
+
|
51
|
+
|
52
|
+
class AccessGroupManager(models.Manager):
|
53
|
+
|
54
|
+
def tenant_groups(self):
|
55
|
+
return self.get_queryset().filter(apply_on='tenant')
|
56
|
+
|
57
|
+
def member_groups(self):
|
58
|
+
return self.get_queryset().filter(apply_on='member')
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Generated by Django 5.2.1 on 2025-07-31 05:12
|
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
|
+
group.name = json.dumps({default_lang: group.name})
|
14
|
+
group.save()
|
15
|
+
|
16
|
+
|
17
|
+
class Migration(migrations.Migration):
|
18
|
+
|
19
|
+
dependencies = [
|
20
|
+
('accrete', '0008_alter_member_access_groups_and_more'),
|
21
|
+
]
|
22
|
+
|
23
|
+
operations = [
|
24
|
+
migrations.RunPython(char_to_translated_char),
|
25
|
+
migrations.AlterField(
|
26
|
+
model_name='accessgroup',
|
27
|
+
name='name',
|
28
|
+
field=accrete.fields.TranslatedCharField(verbose_name='Name'),
|
29
|
+
),
|
30
|
+
]
|
accrete/models.py
CHANGED
@@ -3,7 +3,8 @@ from django.db import models
|
|
3
3
|
from django.conf import settings
|
4
4
|
from django.utils.translation import gettext_lazy as _
|
5
5
|
from accrete.tenant import get_tenant
|
6
|
-
from accrete.managers import TenantManager, MemberManager
|
6
|
+
from accrete.managers import TenantManager, MemberManager, AccessGroupManager
|
7
|
+
from accrete.fields import TranslatedCharField
|
7
8
|
|
8
9
|
|
9
10
|
class TenantModel(models.Model):
|
@@ -131,9 +132,10 @@ class AccessGroup(models.Model):
|
|
131
132
|
)
|
132
133
|
]
|
133
134
|
|
134
|
-
|
135
|
-
|
136
|
-
|
135
|
+
objects = AccessGroupManager()
|
136
|
+
|
137
|
+
name = TranslatedCharField(
|
138
|
+
verbose_name=_('Name')
|
137
139
|
)
|
138
140
|
|
139
141
|
description = models.TextField(
|
accrete/views.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
import os
|
2
2
|
from functools import wraps
|
3
|
-
from
|
3
|
+
from typing import Callable
|
4
|
+
|
5
|
+
from django.http import HttpResponse, HttpResponseNotFound, HttpResponseForbidden, HttpRequest
|
4
6
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
5
7
|
from django.contrib.auth.views import login_required
|
6
8
|
from django.core.exceptions import ImproperlyConfigured
|
@@ -104,6 +106,9 @@ class TenantRequiredMixin(LoginRequiredMixin):
|
|
104
106
|
def tenant_required(
|
105
107
|
tenant_groups: list[str | tuple[str]] = None,
|
106
108
|
member_groups: list[str | tuple[str]] = None,
|
109
|
+
group_missing_action: str | Callable[[
|
110
|
+
HttpRequest, list[str | tuple[str]], list[str | tuple[str]]
|
111
|
+
], HttpResponse] = None,
|
107
112
|
redirect_field_name: str = None,
|
108
113
|
login_url: str = None
|
109
114
|
):
|
@@ -114,30 +119,37 @@ def tenant_required(
|
|
114
119
|
login_url=login_url
|
115
120
|
)
|
116
121
|
def _wrapped_view(request, *args, **kwargs):
|
122
|
+
|
123
|
+
def handle_group_missing():
|
124
|
+
if callable(group_missing_action):
|
125
|
+
return group_missing_action(
|
126
|
+
request, tenant_groups, member_groups
|
127
|
+
)
|
128
|
+
return (
|
129
|
+
redirect(config.ACCRETE_GROUP_NOT_SET_URL)
|
130
|
+
if config.ACCRETE_GROUP_NOT_SET_URL
|
131
|
+
else HttpResponseForbidden()
|
132
|
+
)
|
133
|
+
|
117
134
|
tenant = request.tenant
|
118
135
|
if not tenant:
|
119
136
|
return redirect(config.ACCRETE_TENANT_NOT_SET_URL)
|
120
|
-
redirect_url = None
|
121
137
|
for tenant_group in (tenant_groups or []):
|
122
|
-
if (
|
123
|
-
(
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
redirect_url = config.ACCRETE_GROUP_NOT_SET_URL
|
138
|
+
if isinstance(tenant_group, tuple) and all([
|
139
|
+
tenant_has_group(g) for g in tenant_group
|
140
|
+
]):
|
141
|
+
break
|
142
|
+
elif isinstance(tenant_group, str) and tenant_has_group(tenant_group):
|
143
|
+
break
|
144
|
+
return handle_group_missing()
|
130
145
|
for member_group in (member_groups or []):
|
131
|
-
if (
|
132
|
-
(
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
redirect_url = config.ACCRETE_GROUP_NOT_SET_URL
|
139
|
-
if redirect_url:
|
140
|
-
return redirect(redirect_url)
|
146
|
+
if isinstance(member_group, tuple) and all([
|
147
|
+
member_has_group(g) for g in member_group
|
148
|
+
]):
|
149
|
+
break
|
150
|
+
elif isinstance(member_group, str) and member_has_group(member_group):
|
151
|
+
break
|
152
|
+
return handle_group_missing()
|
141
153
|
return f(request, *args, **kwargs)
|
142
154
|
return _wrapped_view
|
143
155
|
return decorator
|