accrete 0.0.136__py3-none-any.whl → 0.0.139__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 (46) hide show
  1. accrete/contrib/ui/__init__.py +4 -2
  2. accrete/contrib/ui/context.py +35 -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/form_error.html +1 -1
  12. accrete/contrib/ui/templates/ui/layout.html +4 -3
  13. accrete/contrib/ui/templates/ui/list.html +5 -5
  14. accrete/contrib/ui/templates/ui/message.html +5 -4
  15. accrete/contrib/ui/templates/ui/modal.html +7 -7
  16. accrete/contrib/ui/templates/ui/templatetags/field.html +6 -0
  17. accrete/contrib/ui/templates/ui/widgets/model_search_select.html +11 -12
  18. accrete/contrib/ui/templates/ui/widgets/model_search_select_multi.html +13 -12
  19. accrete/contrib/ui/templates/ui/widgets/model_search_select_options.html +1 -1
  20. accrete/contrib/ui/templatetags/ui.py +7 -0
  21. accrete/contrib/ui/utils.py +31 -4
  22. accrete/contrib/ui/widgets/search_select.py +4 -2
  23. accrete/contrib/user/admin.py +18 -2
  24. accrete/contrib/user/auth_backends.py +21 -0
  25. accrete/contrib/user/migrations/0003_alter_user_email.py +19 -0
  26. accrete/contrib/user/migrations/0004_user_login_alter_user_email_user_email_or_login_set.py +29 -0
  27. accrete/contrib/user/migrations/0005_remove_user_email_or_login_set_and_more.py +22 -0
  28. accrete/contrib/user/migrations/0006_remove_user_email_or_login_set_user_is_managed_and_more.py +31 -0
  29. accrete/contrib/user/migrations/0007_user_managed_login.py +18 -0
  30. accrete/contrib/user/migrations/0008_remove_user_no_email_for_managed_user_and_more.py +23 -0
  31. accrete/contrib/user/models.py +68 -11
  32. accrete/contrib/user/views.py +7 -2
  33. accrete/middleware.py +3 -2
  34. accrete/migrations/0006_alter_member_user.py +21 -0
  35. accrete/migrations/0007_accessgroup_description.py +18 -0
  36. accrete/models.py +8 -3
  37. accrete/tenant.py +10 -6
  38. accrete/utils/forms.py +2 -2
  39. accrete/utils/views.py +10 -7
  40. accrete/views.py +42 -24
  41. {accrete-0.0.136.dist-info → accrete-0.0.139.dist-info}/METADATA +1 -1
  42. {accrete-0.0.136.dist-info → accrete-0.0.139.dist-info}/RECORD +45 -34
  43. accrete/contrib/ui/static/js/alpine3.14.1.js +0 -5
  44. /accrete/contrib/ui/static/js/{alpine-sort3.14.1.js → alpine-sort-3.14.9.js} +0 -0
  45. {accrete-0.0.136.dist-info → accrete-0.0.139.dist-info}/WHEEL +0 -0
  46. {accrete-0.0.136.dist-info → accrete-0.0.139.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
+ ]
@@ -0,0 +1,23 @@
1
+ # Generated by Django 5.1.7 on 2025-03-23 08: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', '0007_user_managed_login'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.RemoveConstraint(
15
+ model_name='user',
16
+ name='no_email_for_managed_user',
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='user',
20
+ name='language_code',
21
+ field=models.CharField(blank=True, default=accrete.contrib.user.models.default_language_code, max_length=10, null=True, verbose_name='Language'),
22
+ ),
23
+ ]
@@ -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,51 @@ 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
+
38
+ def default_language_code():
39
+ return settings.LANGUAGE_CODE
40
+
41
+
21
42
  class UserManager(BaseUserManager):
22
43
  use_in_migrations = True
23
44
 
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)
45
+ def _create_user(self, password, email=None, login=None, username=None, **extra_fields):
46
+ if not email and not login:
47
+ raise ValueError('The email or login must be set')
48
+ if not username and login:
49
+ username = login
50
+ elif not username and email:
51
+ username = email
52
+ user = self.model(**extra_fields)
53
+ if email:
54
+ user.email = self.normalize_email(email)
55
+ if login:
56
+ user.login = self.model.normalize_username(login)
57
+ if username:
58
+ user.username = self.model.normalize_username(username)
31
59
  user.set_password(password)
32
60
  user.save(using=self._db)
33
61
  return user
34
62
 
35
- def create_user(self, email, password=None, username=None, **extra_fields):
63
+ def create_user(self, password, email=None, login=None, username=None, **extra_fields):
36
64
  extra_fields.setdefault('is_staff', False)
37
65
  extra_fields.setdefault('is_superuser', False)
38
66
  extra_fields.setdefault('is_active', False)
39
- return self._create_user(email, password, username, **extra_fields)
67
+ return self._create_user(password, email, login, username, **extra_fields)
40
68
 
41
69
  def create_superuser(self, email, password, username=None, **extra_fields):
42
70
  extra_fields.setdefault('is_staff', True)
@@ -57,12 +85,25 @@ class User(AbstractBaseUser, PermissionsMixin):
57
85
  db_table = 'accrete_user'
58
86
  verbose_name = _('User')
59
87
  verbose_name_plural = _('Users')
88
+ constraints = [
89
+ models.CheckConstraint(
90
+ condition=Q(email__isnull=False) | Q(login__isnull=False),
91
+ name='email_or_login_set',
92
+ violation_error_message='E-Mail or Login must be set'
93
+ ),
94
+ models.CheckConstraint(
95
+ condition=Q(is_managed=True, login__isnull=False) | Q(is_managed=False),
96
+ name='managed_login',
97
+ violation_error_message='Managed users must have a login'
98
+ )
99
+ ]
60
100
 
61
101
  filter_exclude = [
62
102
  'password'
63
103
  ]
64
104
 
65
105
  username_validator = UnicodeUsernameValidator()
106
+ login_validator = validate_member_login
66
107
 
67
108
  username = models.CharField(
68
109
  verbose_name=_('Username'),
@@ -92,7 +133,16 @@ class User(AbstractBaseUser, PermissionsMixin):
92
133
 
93
134
  email = models.EmailField(
94
135
  verbose_name=_('Email Address'),
95
- unique=True
136
+ unique=True,
137
+ null=True
138
+ )
139
+
140
+ login = models.CharField(
141
+ verbose_name=_('Login'),
142
+ max_length=254,
143
+ validators=[login_validator],
144
+ unique=True,
145
+ null=True
96
146
  )
97
147
 
98
148
  is_staff = models.BooleanField(
@@ -112,6 +162,12 @@ class User(AbstractBaseUser, PermissionsMixin):
112
162
  ),
113
163
  )
114
164
 
165
+ is_managed = models.BooleanField(
166
+ verbose_name=_('Is Managed'),
167
+ default=False,
168
+ help_text=_('User with restricted functionality.')
169
+ )
170
+
115
171
  date_joined = models.DateTimeField(
116
172
  verbose_name=_('Date Joined'),
117
173
  default=timezone.now
@@ -121,7 +177,8 @@ class User(AbstractBaseUser, PermissionsMixin):
121
177
  verbose_name=_('Language'),
122
178
  max_length=10,
123
179
  null=True,
124
- blank=True
180
+ blank=True,
181
+ default=default_language_code
125
182
  )
126
183
 
127
184
  theme = models.CharField(
@@ -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:
@@ -0,0 +1,21 @@
1
+ # Generated by Django 5.1.7 on 2025-03-23 10:21
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('accrete', '0005_accessgroup_apply_on_alter_member_access_groups_and_more'),
12
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.AlterField(
17
+ model_name='member',
18
+ name='user',
19
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to=settings.AUTH_USER_MODEL),
20
+ ),
21
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 5.1.7 on 2025-03-31 20:23
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('accrete', '0006_alter_member_user'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='accessgroup',
15
+ name='description',
16
+ field=models.TextField(blank=True, null=True, verbose_name='Description'),
17
+ ),
18
+ ]
accrete/models.py CHANGED
@@ -90,7 +90,7 @@ class Member(models.Model):
90
90
  user = models.ForeignKey(
91
91
  to=settings.AUTH_USER_MODEL,
92
92
  related_name='memberships',
93
- on_delete=models.PROTECT
93
+ on_delete=models.CASCADE
94
94
  )
95
95
 
96
96
  tenant = models.ForeignKey(
@@ -136,6 +136,12 @@ class AccessGroup(models.Model):
136
136
  max_length=255
137
137
  )
138
138
 
139
+ description = models.TextField(
140
+ verbose_name=_('Description'),
141
+ null=True,
142
+ blank=True
143
+ )
144
+
139
145
  code = models.CharField(
140
146
  verbose_name=_('Code'),
141
147
  max_length=100
@@ -148,8 +154,7 @@ class AccessGroup(models.Model):
148
154
  ('tenant', _('Tenant')),
149
155
  ('member', _('Member'))
150
156
  ],
151
- default='tenant',
152
- help_text=_('')
157
+ default='tenant'
153
158
  )
154
159
 
155
160
  def __str__(self):
accrete/tenant.py CHANGED
@@ -39,11 +39,6 @@ class Unscoped:
39
39
  self.tenant = tenant
40
40
 
41
41
  def __enter__(self):
42
- if self.tenant is None:
43
- _logger.warning(
44
- 'Entering unscoped context manager with tenant already set to None!',
45
- stack_info=True
46
- )
47
42
  set_tenant(False)
48
43
 
49
44
  def __exit__(self, exc_type, exc_val, exc_tb):
@@ -57,7 +52,7 @@ def unscoped():
57
52
  def per_tenant(include: Q = None, exclude: Q = None):
58
53
  def decorator(f):
59
54
  def wrapper(*args, **kwargs):
60
- tenants: QuerySet = apps.get_model('accrete', 'Tenant').objects.all()
55
+ tenants = apps.get_model('accrete', 'Tenant').objects.all()
61
56
  if include is not None:
62
57
  tenants = tenants.filter(include)
63
58
  if exclude is not None:
@@ -74,6 +69,15 @@ def per_tenant(include: Q = None, exclude: Q = None):
74
69
  return decorator
75
70
 
76
71
 
72
+ def unscope():
73
+ def decorator(f):
74
+ def wrapper(*args, **kwargs):
75
+ with unscoped():
76
+ f(*args, **kwargs)
77
+ return wrapper
78
+ return decorator
79
+
80
+
77
81
  def tenant_has_group(access_group_code: str) -> bool:
78
82
  tenant = get_tenant()
79
83
  if not tenant:
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)