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.
- accrete/contrib/ui/__init__.py +3 -2
- accrete/contrib/ui/context.py +28 -1
- accrete/contrib/ui/filter.py +7 -3
- accrete/contrib/ui/static/css/.sass-cache/15adf1eed05371361b08787c918a7f18fc15be79/accrete.scssc +0 -0
- accrete/contrib/ui/static/css/accrete.css +8 -0
- accrete/contrib/ui/static/css/accrete.css.map +1 -1
- accrete/contrib/ui/static/css/accrete.scss +12 -3
- accrete/contrib/ui/static/js/alpine-3.14.9.js +5 -0
- accrete/contrib/ui/static/js/alpine-focus-3.14.9.js +15 -0
- accrete/contrib/ui/templates/ui/content_right.html +12 -6
- accrete/contrib/ui/templates/ui/layout.html +7 -4
- accrete/contrib/ui/templates/ui/list.html +1 -1
- accrete/contrib/ui/templates/ui/message.html +1 -1
- accrete/contrib/ui/templates/ui/modal.html +6 -6
- accrete/contrib/ui/templates/ui/templatetags/field.html +6 -0
- accrete/contrib/ui/templates/ui/widgets/model_search_select.html +11 -12
- accrete/contrib/ui/templates/ui/widgets/model_search_select_multi.html +13 -12
- accrete/contrib/ui/templates/ui/widgets/model_search_select_options.html +1 -1
- accrete/contrib/ui/templatetags/ui.py +7 -0
- accrete/contrib/ui/utils.py +31 -4
- accrete/contrib/ui/widgets/search_select.py +4 -2
- accrete/contrib/user/admin.py +18 -2
- accrete/contrib/user/auth_backends.py +21 -0
- accrete/contrib/user/migrations/0003_alter_user_email.py +19 -0
- accrete/contrib/user/migrations/0004_user_login_alter_user_email_user_email_or_login_set.py +29 -0
- accrete/contrib/user/migrations/0005_remove_user_email_or_login_set_and_more.py +22 -0
- accrete/contrib/user/migrations/0006_remove_user_email_or_login_set_user_is_managed_and_more.py +31 -0
- accrete/contrib/user/migrations/0007_user_managed_login.py +18 -0
- accrete/contrib/user/models.py +67 -10
- accrete/contrib/user/views.py +7 -2
- accrete/middleware.py +3 -2
- accrete/models.py +1 -2
- accrete/utils/forms.py +2 -2
- accrete/utils/views.py +7 -1
- accrete/views.py +34 -21
- {accrete-0.0.135.dist-info → accrete-0.0.138.dist-info}/METADATA +1 -1
- {accrete-0.0.135.dist-info → accrete-0.0.138.dist-info}/RECORD +40 -32
- accrete/contrib/ui/static/js/alpine3.14.1.js +0 -5
- /accrete/contrib/ui/static/js/{alpine-sort3.14.1.js → alpine-sort-3.14.9.js} +0 -0
- {accrete-0.0.135.dist-info → accrete-0.0.138.dist-info}/WHEEL +0 -0
- {accrete-0.0.135.dist-info → accrete-0.0.138.dist-info}/licenses/LICENSE +0 -0
accrete/contrib/ui/utils.py
CHANGED
@@ -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
|
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
|
-
|
16
|
-
|
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
|
accrete/contrib/user/admin.py
CHANGED
@@ -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
|
+
]
|
accrete/contrib/user/migrations/0006_remove_user_email_or_login_set_user_is_managed_and_more.py
ADDED
@@ -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
|
+
]
|
accrete/contrib/user/models.py
CHANGED
@@ -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,
|
25
|
-
if not email:
|
26
|
-
raise ValueError('The email must be set')
|
27
|
-
|
28
|
-
|
29
|
-
username
|
30
|
-
|
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,
|
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,
|
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
|
accrete/contrib/user/views.py
CHANGED
@@ -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.
|
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.
|
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
|
-
|
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
|
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
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(
|
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(
|
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
|
57
|
+
def get_group_not_set_url(self):
|
64
58
|
group_not_set_url = (
|
65
|
-
|
66
|
-
|
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
|
117
|
-
|
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
|
120
|
-
|
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
|