accrete 0.0.135__py3-none-any.whl → 0.0.138__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 (41) hide show
  1. accrete/contrib/ui/__init__.py +3 -2
  2. accrete/contrib/ui/context.py +28 -1
  3. accrete/contrib/ui/filter.py +7 -3
  4. accrete/contrib/ui/static/css/.sass-cache/15adf1eed05371361b08787c918a7f18fc15be79/accrete.scssc +0 -0
  5. accrete/contrib/ui/static/css/accrete.css +8 -0
  6. accrete/contrib/ui/static/css/accrete.css.map +1 -1
  7. accrete/contrib/ui/static/css/accrete.scss +12 -3
  8. accrete/contrib/ui/static/js/alpine-3.14.9.js +5 -0
  9. accrete/contrib/ui/static/js/alpine-focus-3.14.9.js +15 -0
  10. accrete/contrib/ui/templates/ui/content_right.html +12 -6
  11. accrete/contrib/ui/templates/ui/layout.html +7 -4
  12. accrete/contrib/ui/templates/ui/list.html +1 -1
  13. accrete/contrib/ui/templates/ui/message.html +1 -1
  14. accrete/contrib/ui/templates/ui/modal.html +6 -6
  15. accrete/contrib/ui/templates/ui/templatetags/field.html +6 -0
  16. accrete/contrib/ui/templates/ui/widgets/model_search_select.html +11 -12
  17. accrete/contrib/ui/templates/ui/widgets/model_search_select_multi.html +13 -12
  18. accrete/contrib/ui/templates/ui/widgets/model_search_select_options.html +1 -1
  19. accrete/contrib/ui/templatetags/ui.py +7 -0
  20. accrete/contrib/ui/utils.py +31 -4
  21. accrete/contrib/ui/widgets/search_select.py +4 -2
  22. accrete/contrib/user/admin.py +18 -2
  23. accrete/contrib/user/auth_backends.py +21 -0
  24. accrete/contrib/user/migrations/0003_alter_user_email.py +19 -0
  25. accrete/contrib/user/migrations/0004_user_login_alter_user_email_user_email_or_login_set.py +29 -0
  26. accrete/contrib/user/migrations/0005_remove_user_email_or_login_set_and_more.py +22 -0
  27. accrete/contrib/user/migrations/0006_remove_user_email_or_login_set_user_is_managed_and_more.py +31 -0
  28. accrete/contrib/user/migrations/0007_user_managed_login.py +18 -0
  29. accrete/contrib/user/models.py +67 -10
  30. accrete/contrib/user/views.py +7 -2
  31. accrete/middleware.py +3 -2
  32. accrete/models.py +1 -2
  33. accrete/utils/forms.py +2 -2
  34. accrete/utils/views.py +7 -1
  35. accrete/views.py +34 -21
  36. {accrete-0.0.135.dist-info → accrete-0.0.138.dist-info}/METADATA +1 -1
  37. {accrete-0.0.135.dist-info → accrete-0.0.138.dist-info}/RECORD +40 -32
  38. accrete/contrib/ui/static/js/alpine3.14.1.js +0 -5
  39. /accrete/contrib/ui/static/js/{alpine-sort3.14.1.js → alpine-sort-3.14.9.js} +0 -0
  40. {accrete-0.0.135.dist-info → accrete-0.0.138.dist-info}/WHEEL +0 -0
  41. {accrete-0.0.135.dist-info → accrete-0.0.138.dist-info}/licenses/LICENSE +0 -0
@@ -1,17 +1,44 @@
1
+ import ast
2
+ import json
1
3
  from django.http import HttpRequest, HttpResponse
2
4
  from django.shortcuts import render
3
5
  from .context import ModalContext
4
6
 
5
7
 
6
- def update_modal(
8
+ def modal_response(
7
9
  request: HttpRequest,
8
10
  template: str,
9
- context: ModalContext | dict
11
+ context: ModalContext | dict,
12
+ update: bool = False
10
13
  ) -> HttpResponse:
11
14
 
12
15
  if isinstance(context, ModalContext):
13
16
  context = context.dict()
14
17
  res = render(request, template, context)
15
- res.headers['HX-Retarget'] = f'#{context["modal_id"]}'
16
- res.headers['HX-Reswap'] = 'outerHTML'
18
+ if update:
19
+ res.headers['HX-Retarget'] = f'#{context["modal_id"]}'
20
+ res.headers['HX-Reswap'] = 'outerHTML'
21
+ return res
22
+ res.headers['HX-Retarget'] = 'body'
23
+ res.headers['HX-Reswap'] = 'beforeend'
17
24
  return res
25
+
26
+
27
+ def add_trigger(
28
+ response: [HttpResponse],
29
+ trigger: dict | str,
30
+ header: str = 'HX-Trigger'
31
+ ) -> HttpResponse:
32
+ if isinstance(trigger, str):
33
+ trigger = {trigger: ''}
34
+ res_trigger = response.headers.get(header)
35
+ if not res_trigger:
36
+ response.headers[header] = json.dumps(trigger)
37
+ return response
38
+ try:
39
+ res_trigger = ast.literal_eval(response.headers.get(header, '{}'))
40
+ except SyntaxError:
41
+ res_trigger = {response.headers[header]: ''}
42
+ res_trigger.update(trigger)
43
+ response.headers[header] = json.dumps(header)
44
+ return response
@@ -38,7 +38,7 @@ class ModelSearchSelect(widgets.NumberInput):
38
38
  'value_display': self.value_display(value),
39
39
  "attrs": self.build_attrs(self.attrs, attrs),
40
40
  "template_name": self.template_name,
41
- 'search_url': self.search_url,
41
+ 'search_url': resolve_url(self.search_url),
42
42
  'search_parameter': self.search_parameter,
43
43
  'hx_trigger_on_change': self.hx_trigger_on_change,
44
44
  'uuid': uuid
@@ -69,6 +69,7 @@ class ModelSearchSelectMulti(widgets.SelectMultiple):
69
69
  def __init__(
70
70
  self,
71
71
  search_url: str,
72
+ search_kwargs: dict = None,
72
73
  search_parameter: str = 'search',
73
74
  limit: int | None = 5,
74
75
  hx_trigger_on_change: bool = False,
@@ -76,6 +77,7 @@ class ModelSearchSelectMulti(widgets.SelectMultiple):
76
77
  ):
77
78
  super().__init__()
78
79
  self.search_url = search_url
80
+ self.search_kwargs = search_kwargs or {}
79
81
  self.search_parameter = search_parameter
80
82
  self.limit = limit
81
83
  self.hx_trigger_on_change = hx_trigger_on_change
@@ -95,7 +97,7 @@ class ModelSearchSelectMulti(widgets.SelectMultiple):
95
97
  "value": self.format_value(value),
96
98
  "attrs": self.build_attrs(self.attrs, attrs),
97
99
  "template_name": self.template_name,
98
- 'search_url': resolve_url(self.search_url),
100
+ 'search_url': resolve_url(self.search_url, **self.search_kwargs),
99
101
  'search_parameter': self.search_parameter,
100
102
  'hx_trigger_on_change': self.hx_trigger_on_change,
101
103
  'uuid': uuid
@@ -1,18 +1,33 @@
1
1
  from django.contrib import admin
2
+ from django import forms
2
3
  from .models import User
3
4
 
4
5
 
6
+ class UserForm(forms.ModelForm):
7
+
8
+ login = forms.CharField(
9
+ required=False
10
+ )
11
+
12
+ email = forms.EmailField(
13
+ required=False
14
+ )
15
+
16
+
5
17
  class UserAdmin(admin.ModelAdmin):
6
18
 
7
19
  model = User
20
+ form = UserForm
8
21
  list_display = (
9
- 'email',
10
22
  'username',
23
+ 'email',
24
+ 'login',
11
25
  'first_name',
12
26
  'last_name'
13
27
  )
14
28
  search_fields = [
15
29
  'email',
30
+ 'login',
16
31
  'username',
17
32
  'first_name',
18
33
  'last_name'
@@ -20,7 +35,8 @@ class UserAdmin(admin.ModelAdmin):
20
35
  list_filter = [
21
36
  'is_superuser',
22
37
  'is_staff',
23
- 'is_active'
38
+ 'is_active',
39
+ 'is_managed'
24
40
  ]
25
41
 
26
42
 
@@ -0,0 +1,21 @@
1
+ from django.contrib.auth import get_user_model
2
+ from django.contrib.auth.backends import ModelBackend
3
+
4
+ User = get_user_model()
5
+
6
+
7
+ class LoginModelBackend(ModelBackend):
8
+ """
9
+ This is a ModelBacked that allows authentication with the login field.
10
+ """
11
+
12
+ def authenticate(self, request, username=None, password=None, **kwargs):
13
+ if username is None:
14
+ return None
15
+ try:
16
+ user = User.objects.get(login=username)
17
+ except User.DoesNotExist:
18
+ return None
19
+ if user.check_password(password):
20
+ return user
21
+ return None
@@ -0,0 +1,19 @@
1
+ # Generated by Django 5.1.7 on 2025-03-16 18:11
2
+
3
+ import accrete.contrib.user.models
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('user', '0002_user_theme'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name='user',
16
+ name='email',
17
+ field=models.EmailField(max_length=254, unique=True, validators=[accrete.contrib.user.models.validate_member_login], verbose_name='Email Address'),
18
+ ),
19
+ ]
@@ -0,0 +1,29 @@
1
+ # Generated by Django 5.1.7 on 2025-03-16 19:12
2
+
3
+ import accrete.contrib.user.models
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('auth', '0012_alter_user_first_name_max_length'),
11
+ ('user', '0003_alter_user_email'),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AddField(
16
+ model_name='user',
17
+ name='login',
18
+ field=models.CharField(max_length=254, null=True, unique=True, validators=[accrete.contrib.user.models.validate_member_login], verbose_name='Login'),
19
+ ),
20
+ migrations.AlterField(
21
+ model_name='user',
22
+ name='email',
23
+ field=models.EmailField(max_length=254, null=True, unique=True, verbose_name='Email Address'),
24
+ ),
25
+ migrations.AddConstraint(
26
+ model_name='user',
27
+ constraint=models.CheckConstraint(condition=models.Q(('email__isnull', False), ('login__isnull', False), _connector='XOR'), name='email_or_login_set', violation_error_message='email or login must be set'),
28
+ ),
29
+ ]
@@ -0,0 +1,22 @@
1
+ # Generated by Django 5.1.7 on 2025-03-16 19:37
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('auth', '0012_alter_user_first_name_max_length'),
10
+ ('user', '0004_user_login_alter_user_email_user_email_or_login_set'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.RemoveConstraint(
15
+ model_name='user',
16
+ name='email_or_login_set',
17
+ ),
18
+ migrations.AddConstraint(
19
+ model_name='user',
20
+ constraint=models.CheckConstraint(condition=models.Q(('email__isnull', False), ('login__isnull', False), _connector='OR'), name='email_or_login_set', violation_error_message='email or login must be set'),
21
+ ),
22
+ ]
@@ -0,0 +1,31 @@
1
+ # Generated by Django 5.1.7 on 2025-03-17 18:09
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('auth', '0012_alter_user_first_name_max_length'),
10
+ ('user', '0005_remove_user_email_or_login_set_and_more'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.RemoveConstraint(
15
+ model_name='user',
16
+ name='email_or_login_set',
17
+ ),
18
+ migrations.AddField(
19
+ model_name='user',
20
+ name='is_managed',
21
+ field=models.BooleanField(default=False, help_text='User with restricted functionality.', verbose_name='Is Managed'),
22
+ ),
23
+ migrations.AddConstraint(
24
+ model_name='user',
25
+ constraint=models.CheckConstraint(condition=models.Q(('email__isnull', False), ('login__isnull', False), _connector='OR'), name='email_or_login_set', violation_error_message='E-Mail or Login must be set'),
26
+ ),
27
+ migrations.AddConstraint(
28
+ model_name='user',
29
+ constraint=models.CheckConstraint(condition=models.Q(models.Q(('email__isnull', False), ('is_managed', False)), models.Q(('email__isnull', True), ('is_managed', True)), _connector='OR'), name='no_email_for_managed_user', violation_error_message='Managed users must not have an E-Mail address'),
30
+ ),
31
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 5.1.7 on 2025-03-19 17:53
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('auth', '0012_alter_user_first_name_max_length'),
10
+ ('user', '0006_remove_user_email_or_login_set_user_is_managed_and_more'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddConstraint(
15
+ model_name='user',
16
+ constraint=models.CheckConstraint(condition=models.Q(models.Q(('is_managed', True), ('login__isnull', False)), ('is_managed', False), _connector='OR'), name='managed_login', violation_error_message='Managed users must have a login'),
17
+ ),
18
+ ]
@@ -1,4 +1,5 @@
1
1
  from django.conf import settings
2
+ from django.core.exceptions import ValidationError
2
3
  from django.db import models
3
4
  from django.contrib.auth.models import (
4
5
  AbstractBaseUser,
@@ -6,6 +7,7 @@ from django.contrib.auth.models import (
6
7
  )
7
8
  from django.contrib.auth.validators import UnicodeUsernameValidator
8
9
  from django.contrib.auth.models import BaseUserManager
10
+ from django.db.models import Q
9
11
  from django.utils.translation import gettext_lazy as _
10
12
  from django.utils import timezone
11
13
  from django.shortcuts import resolve_url
@@ -18,25 +20,47 @@ LANGUAGE_DISPLAY = {
18
20
  }
19
21
 
20
22
 
23
+ def validate_member_login(login: str) -> None:
24
+ if '@' in login:
25
+ raise ValidationError(_(
26
+ 'Login must not be an E-Mail address, use the field email instead'
27
+ ))
28
+ message = _(
29
+ 'Login must consist of username and domain seperated by a colon(":")'
30
+ )
31
+ if ':' not in login:
32
+ raise ValidationError(message)
33
+ member_login, tenant_login = login.split(':', 1)
34
+ if not member_login or not tenant_login:
35
+ raise ValidationError(message)
36
+
37
+
21
38
  class UserManager(BaseUserManager):
22
39
  use_in_migrations = True
23
40
 
24
- def _create_user(self, email, password, username=None, **extra_fields):
25
- if not email:
26
- raise ValueError('The email must be set')
27
-
28
- email = self.normalize_email(email)
29
- username = self.model.normalize_username(username)
30
- user = self.model(email=email, username=username, **extra_fields)
41
+ def _create_user(self, password, email=None, login=None, username=None, **extra_fields):
42
+ if not email and not login:
43
+ raise ValueError('The email or login must be set')
44
+ if not username and login:
45
+ username = login
46
+ elif not username and email:
47
+ username = email
48
+ user = self.model(**extra_fields)
49
+ if email:
50
+ user.email = self.normalize_email(email)
51
+ if login:
52
+ user.login = self.model.normalize_username(login)
53
+ if username:
54
+ user.username = self.model.normalize_username(username)
31
55
  user.set_password(password)
32
56
  user.save(using=self._db)
33
57
  return user
34
58
 
35
- def create_user(self, email, password=None, username=None, **extra_fields):
59
+ def create_user(self, password, email=None, login=None, username=None, **extra_fields):
36
60
  extra_fields.setdefault('is_staff', False)
37
61
  extra_fields.setdefault('is_superuser', False)
38
62
  extra_fields.setdefault('is_active', False)
39
- return self._create_user(email, password, username, **extra_fields)
63
+ return self._create_user(password, email, login, username, **extra_fields)
40
64
 
41
65
  def create_superuser(self, email, password, username=None, **extra_fields):
42
66
  extra_fields.setdefault('is_staff', True)
@@ -57,12 +81,30 @@ class User(AbstractBaseUser, PermissionsMixin):
57
81
  db_table = 'accrete_user'
58
82
  verbose_name = _('User')
59
83
  verbose_name_plural = _('Users')
84
+ constraints = [
85
+ models.CheckConstraint(
86
+ condition=Q(email__isnull=False) | Q(login__isnull=False),
87
+ name='email_or_login_set',
88
+ violation_error_message='E-Mail or Login must be set'
89
+ ),
90
+ models.CheckConstraint(
91
+ condition=Q(is_managed=False, email__isnull=False) | Q(is_managed=True, email__isnull=True),
92
+ name='no_email_for_managed_user',
93
+ violation_error_message='Managed users must not have an E-Mail address'
94
+ ),
95
+ models.CheckConstraint(
96
+ condition=Q(is_managed=True, login__isnull=False) | Q(is_managed=False),
97
+ name='managed_login',
98
+ violation_error_message='Managed users must have a login'
99
+ )
100
+ ]
60
101
 
61
102
  filter_exclude = [
62
103
  'password'
63
104
  ]
64
105
 
65
106
  username_validator = UnicodeUsernameValidator()
107
+ login_validator = validate_member_login
66
108
 
67
109
  username = models.CharField(
68
110
  verbose_name=_('Username'),
@@ -92,7 +134,16 @@ class User(AbstractBaseUser, PermissionsMixin):
92
134
 
93
135
  email = models.EmailField(
94
136
  verbose_name=_('Email Address'),
95
- unique=True
137
+ unique=True,
138
+ null=True
139
+ )
140
+
141
+ login = models.CharField(
142
+ verbose_name=_('Login'),
143
+ max_length=254,
144
+ validators=[login_validator],
145
+ unique=True,
146
+ null=True
96
147
  )
97
148
 
98
149
  is_staff = models.BooleanField(
@@ -112,6 +163,12 @@ class User(AbstractBaseUser, PermissionsMixin):
112
163
  ),
113
164
  )
114
165
 
166
+ is_managed = models.BooleanField(
167
+ verbose_name=_('Is Managed'),
168
+ default=False,
169
+ help_text=_('User with restricted functionality.')
170
+ )
171
+
115
172
  date_joined = models.DateTimeField(
116
173
  verbose_name=_('Date Joined'),
117
174
  default=timezone.now
@@ -1,6 +1,7 @@
1
1
  from django.contrib.auth.forms import AuthenticationForm
2
2
  from django.contrib.auth import views, update_session_auth_hash
3
3
  from django.contrib.auth.decorators import login_required
4
+ from django.http import HttpResponseForbidden
4
5
  from django.shortcuts import redirect, render, resolve_url
5
6
  from django.utils.translation import gettext_lazy as _
6
7
  from django.conf import settings
@@ -58,6 +59,8 @@ def user_detail(request):
58
59
 
59
60
  @login_required()
60
61
  def user_change_password(request):
62
+ if request.user.is_managed:
63
+ return HttpResponseForbidden()
61
64
  form = ChangePasswordForm(instance=request.user)
62
65
  ctx = ui.ModalContext(
63
66
  title=_('Change Password'),
@@ -77,12 +80,14 @@ def user_change_password(request):
77
80
  + f'?{request.GET.urlencode()}'
78
81
  )
79
82
  ctx.update(form=form)
80
- return ui.update_modal(request, 'user/change_password.html', ctx)
83
+ return ui.modal_response(request, 'user/change_password.html', ctx)
81
84
  return render(request, 'user/change_password.html', ctx)
82
85
 
83
86
 
84
87
  @login_required()
85
88
  def user_change_email(request):
89
+ if request.user.is_managed:
90
+ return HttpResponseForbidden()
86
91
  form = ChangeEmailForm(instance=request.user)
87
92
  ctx = ui.ModalContext(
88
93
  title=_('Change E-Mail'),
@@ -98,5 +103,5 @@ def user_change_email(request):
98
103
  if form.is_saved:
99
104
  return redirect('user:detail')
100
105
  ctx.update(form=form)
101
- return ui.update_modal(request, 'user/change_email.html', ctx)
106
+ return ui.modal_response(request, 'user/change_email.html', ctx)
102
107
  return render(request, 'user/change_email.html', ctx)
accrete/middleware.py CHANGED
@@ -39,13 +39,14 @@ class TenantMiddleware(MiddlewareMixin):
39
39
  if tenant_id:
40
40
  tenant = Tenant.objects.get(pk=tenant_id)
41
41
  memberships = memberships.filter(tenant=tenant)
42
- if memberships.count() == 1:
42
+ membership_count = memberships.count()
43
+ if membership_count == 1:
43
44
  request.member = memberships.first()
44
45
  request.tenant = request.member.tenant
45
46
  set_member(request.member)
46
47
  self.update_post_data(request)
47
48
  return
48
- if memberships.count() > 1:
49
+ if membership_count > 1:
49
50
  set_member(None)
50
51
  return
51
52
  if request.user.is_staff and tenant:
accrete/models.py CHANGED
@@ -148,8 +148,7 @@ class AccessGroup(models.Model):
148
148
  ('tenant', _('Tenant')),
149
149
  ('member', _('Member'))
150
150
  ],
151
- default='tenant',
152
- help_text=_('')
151
+ default='tenant'
153
152
  )
154
153
 
155
154
  def __str__(self):
accrete/utils/forms.py CHANGED
@@ -8,7 +8,7 @@ from django.forms import BaseFormSet, Form, ModelForm
8
8
  _logger = logging.getLogger(__name__)
9
9
 
10
10
 
11
- def save_form(form: [Form|ModelForm], commit=True, reraise=False) -> [Form|ModelForm]:
11
+ def save_form(form: [Form|ModelForm], commit=True, reraise=False) -> [Form | ModelForm]:
12
12
  if not hasattr(form, 'save'):
13
13
  raise AttributeError('Form must have method "save" implemented.')
14
14
  form.is_saved = False
@@ -30,7 +30,7 @@ def save_form(form: [Form|ModelForm], commit=True, reraise=False) -> [Form|Model
30
30
  return form
31
31
 
32
32
 
33
- def save_forms(form, inline_formsets: list = None, commit=True, reraise: bool = False):
33
+ def save_forms(form, inline_formsets: list = None, commit=True, reraise: bool = False) -> [Form | ModelForm]:
34
34
 
35
35
  def handle_error(error):
36
36
  form.save_error = repr(error)
accrete/utils/views.py CHANGED
@@ -21,12 +21,16 @@ QUERYSTRING_KEY_MAP = {
21
21
 
22
22
  def page_from_querystring(
23
23
  model: type[Model], query_dict: dict, key_map: dict = None,
24
- select_related: list = None, prefetch_related: list = None
24
+ select_related: list = None, prefetch_related: list = None,
25
+ query: Q = None
25
26
  ) -> paginator.Page:
26
27
 
27
28
  key_map = key_map or QUERYSTRING_KEY_MAP
29
+ query = query or Q()
28
30
  queryset = filter_from_querystring(
29
31
  model, query_dict, key_map
32
+ ).filter(
33
+ query
30
34
  ).select_related(
31
35
  *select_related or []
32
36
  ).prefetch_related(
@@ -62,7 +66,9 @@ def filter_from_querystring(
62
66
  def parse_querystring(model: type[Model], query_string: str) -> Q:
63
67
  """
64
68
  param: query_string: JSON serializable string
69
+ Parses
65
70
  [{"term": value}, "&", [{t"erm": value}, "|", {"~term": value}]]
71
+ to
66
72
  Q(term=value) & (Q(term=value) | ~Q(term=value))
67
73
  """
68
74
 
accrete/views.py CHANGED
@@ -12,18 +12,12 @@ from accrete.tenant import get_tenant, tenant_has_group, member_has_group
12
12
  from . import config
13
13
 
14
14
 
15
- class GroupType(Enum):
16
-
17
- TENANT: str = 'tenant'
18
- MEMBER: str = 'member'
19
-
20
-
21
15
  class TenantRequiredMixin(LoginRequiredMixin):
22
16
 
23
17
  TENANT_NOT_SET_URL = None
24
18
  GROUP_NOT_SET_URL = None
25
- TENANT_GROUPS = []
26
- MEMBER_GROUPS = []
19
+ TENANT_GROUPS: list[str | tuple[str]] = []
20
+ MEMBER_GROUPS: list[str | tuple[str]] = []
27
21
 
28
22
  def dispatch(self, request, *args, **kwargs):
29
23
  res = super().dispatch(request, *args, **kwargs)
@@ -40,10 +34,10 @@ class TenantRequiredMixin(LoginRequiredMixin):
40
34
  return redirect(self.get_tenant_not_set_url())
41
35
 
42
36
  def handle_tenant_group_not_set(self):
43
- return redirect(self.get_group_not_set_url(GroupType.TENANT))
37
+ return redirect(self.get_group_not_set_url())
44
38
 
45
39
  def handle_member_group_not_set(self):
46
- return redirect(self.get_group_not_set_url(GroupType.MEMBER))
40
+ return redirect(self.get_group_not_set_url())
47
41
 
48
42
  def get_tenant_not_set_url(self):
49
43
  tenant_not_set_url = (
@@ -60,10 +54,10 @@ class TenantRequiredMixin(LoginRequiredMixin):
60
54
  )
61
55
  return tenant_not_set_url
62
56
 
63
- def get_group_not_set_url(self, group_type: GroupType):
57
+ def get_group_not_set_url(self):
64
58
  group_not_set_url = (
65
- self.GROUP_NOT_SET_URL
66
- or settings.ACCRETE_GROUP_NOT_SET_URL
59
+ self.GROUP_NOT_SET_URL
60
+ or settings.ACCRETE_GROUP_NOT_SET_URL
67
61
  )
68
62
  if not group_not_set_url:
69
63
  cls_name = self.__class__.__name__
@@ -79,7 +73,9 @@ class TenantRequiredMixin(LoginRequiredMixin):
79
73
  if not self.TENANT_GROUPS:
80
74
  return True
81
75
  for group in self.TENANT_GROUPS:
82
- if tenant_has_group(group):
76
+ if isinstance(group, tuple) and all([tenant_has_group(g) for g in group]):
77
+ return True
78
+ elif tenant_has_group(group):
83
79
  return True
84
80
  return False
85
81
 
@@ -87,7 +83,9 @@ class TenantRequiredMixin(LoginRequiredMixin):
87
83
  if not self.MEMBER_GROUPS:
88
84
  return True
89
85
  for group in self.MEMBER_GROUPS:
90
- if member_has_group(group):
86
+ if isinstance(group, tuple) and all([member_has_group(g) for g in group]):
87
+ return True
88
+ elif member_has_group(group):
91
89
  return True
92
90
  return False
93
91
 
@@ -97,8 +95,8 @@ class TenantRequiredMixin(LoginRequiredMixin):
97
95
 
98
96
 
99
97
  def tenant_required(
100
- tenant_groups: list[str] = None,
101
- member_groups: list[str] = None,
98
+ tenant_groups: list[str | tuple[str]] = None,
99
+ member_groups: list[str | tuple[str]] = None,
102
100
  redirect_field_name: str = None,
103
101
  login_url: str = None
104
102
  ):
@@ -112,12 +110,27 @@ def tenant_required(
112
110
  tenant = request.tenant
113
111
  if not tenant:
114
112
  return redirect(config.ACCRETE_TENANT_NOT_SET_URL)
113
+ redirect_url = None
115
114
  for tenant_group in (tenant_groups or []):
116
- if not any([tenant_has_group(tenant_group)]):
117
- return redirect(config.ACCRETE_GROUP_NOT_SET_URL)
115
+ if (
116
+ (isinstance(tenant_group, tuple) and all([
117
+ tenant_has_group(g) for g in tenant_group
118
+ ]))
119
+ or tenant_has_group(tenant_group)
120
+ ):
121
+ return f(request, *args, **kwargs)
122
+ redirect_url = config.ACCRETE_GROUP_NOT_SET_URL
118
123
  for member_group in (member_groups or []):
119
- if not any([member_has_group(member_group)]):
120
- return redirect(config.ACCRETE_GROUP_NOT_SET_URL)
124
+ if (
125
+ (isinstance(member_group, tuple) and all([
126
+ member_has_group(g) for g in member_group
127
+ ]))
128
+ or member_has_group(member_group)
129
+ ):
130
+ return f(request, *args, **kwargs)
131
+ redirect_url = config.ACCRETE_GROUP_NOT_SET_URL
132
+ if redirect_url:
133
+ return redirect(redirect_url)
121
134
  return f(request, *args, **kwargs)
122
135
  return _wrapped_view
123
136
  return decorator
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: accrete
3
- Version: 0.0.135
3
+ Version: 0.0.138
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