dj-tinyuser 0.0.1__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.
- dj_tinyuser/__init__.py +0 -0
- dj_tinyuser/account_adapter.py +90 -0
- dj_tinyuser/admin.py +31 -0
- dj_tinyuser/api/__init__.py +1 -0
- dj_tinyuser/api/apps.py +9 -0
- dj_tinyuser/api/locale/de/LC_MESSAGES/django.po +43 -0
- dj_tinyuser/api/permissions.py +24 -0
- dj_tinyuser/api/serializers.py +208 -0
- dj_tinyuser/api/tests/__init__.py +1 -0
- dj_tinyuser/api/tests/test_invitations_api.py +182 -0
- dj_tinyuser/api/tests/test_profile_api.py +1 -0
- dj_tinyuser/api/tests/test_user_api.py +261 -0
- dj_tinyuser/api/urls.py +19 -0
- dj_tinyuser/api/views.py +65 -0
- dj_tinyuser/apps.py +7 -0
- dj_tinyuser/context_processors.py +31 -0
- dj_tinyuser/exceptions.py +6 -0
- dj_tinyuser/forms.py +96 -0
- dj_tinyuser/global_templates/__init__.py +3 -0
- dj_tinyuser/global_templates/account/login.html +1 -0
- dj_tinyuser/global_templates/account/password_reset.html +1 -0
- dj_tinyuser/global_templates/account/signup.html +1 -0
- dj_tinyuser/global_templates/allauth/layouts/base.html +1 -0
- dj_tinyuser/input_files/style.scss +27 -0
- dj_tinyuser/locale/de/LC_MESSAGES/django.po +880 -0
- dj_tinyuser/logging.py +11 -0
- dj_tinyuser/managers.py +64 -0
- dj_tinyuser/migrations/0001_initial.py +331 -0
- dj_tinyuser/migrations/__init__.py +0 -0
- dj_tinyuser/mixins/htmx.py +20 -0
- dj_tinyuser/models/__init__.py +11 -0
- dj_tinyuser/models/fields.py +58 -0
- dj_tinyuser/models/tinyuser.py +125 -0
- dj_tinyuser/models/userprofile.py +216 -0
- dj_tinyuser/settings.py +72 -0
- dj_tinyuser/sidebar.py +55 -0
- dj_tinyuser/static/tinyuser/css/html.css +28 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/account/login.dj.html +68 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/account/password_reset.dj.html +19 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/account/signup.dj.html +24 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/base.dj.html +31 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/index.html +10 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/invite/failed.html +9 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/invite/hx/failed.html +14 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/invite/hx/invite.html +26 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/invite/hx/success.html +14 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/invite/invite.html +23 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/invite/success.html +9 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/profile/edit.html +10 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/profile/hx/edit.html +151 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/profile/hx/profile.html +120 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/profile/hx/settings.html +27 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/profile/profile.html +46 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/profile/settings.html +18 -0
- dj_tinyuser/templates/tinyuser/bootstrap5/snippets/account/form +9 -0
- dj_tinyuser/templates/tinyuser/bs5docs/account/login.dj.html +66 -0
- dj_tinyuser/templates/tinyuser/bs5docs/account/signup.dj.html +26 -0
- dj_tinyuser/templates/tinyuser/html/account/login.dj.html +58 -0
- dj_tinyuser/templates/tinyuser/html/account/password_reset.dj.html +20 -0
- dj_tinyuser/templates/tinyuser/html/account/signup.dj.html +22 -0
- dj_tinyuser/templates/tinyuser/html/base.dj.html +27 -0
- dj_tinyuser/templates/tinyuser/html/index.html +7 -0
- dj_tinyuser/templates/tinyuser/html/invite/failed.html +9 -0
- dj_tinyuser/templates/tinyuser/html/invite/hx/failed.html +0 -0
- dj_tinyuser/templates/tinyuser/html/invite/hx/invite.html +0 -0
- dj_tinyuser/templates/tinyuser/html/invite/hx/success.html +0 -0
- dj_tinyuser/templates/tinyuser/html/invite/invite.html +0 -0
- dj_tinyuser/templates/tinyuser/html/invite/success.html +0 -0
- dj_tinyuser/templates/tinyuser/html/logout/navbar.dj.html +5 -0
- dj_tinyuser/templates/tinyuser/html/profile/edit.html +0 -0
- dj_tinyuser/templates/tinyuser/html/profile/hx/edit.html +0 -0
- dj_tinyuser/templates/tinyuser/html/profile/hx/profile.html +0 -0
- dj_tinyuser/templates/tinyuser/html/profile/hx/settings.html +0 -0
- dj_tinyuser/templates/tinyuser/html/profile/profile.html +0 -0
- dj_tinyuser/templates/tinyuser/html/profile/settings.html +0 -0
- dj_tinyuser/templates/tinyuser/html/theme/nav.dj.html +20 -0
- dj_tinyuser/templates.json +39 -0
- dj_tinyuser/templates.yml +23 -0
- dj_tinyuser/templatetags/__init__.py +0 -0
- dj_tinyuser/templatetags/tinyuser.py +14 -0
- dj_tinyuser/tests/__init__.py +0 -0
- dj_tinyuser/tests/test_tinyuser_model.py +46 -0
- dj_tinyuser/tests/test_userprofile_model.py +56 -0
- dj_tinyuser/urls.py +22 -0
- dj_tinyuser/utils.py +18 -0
- dj_tinyuser/views/__init__.py +12 -0
- dj_tinyuser/views/dashboard.py +0 -0
- dj_tinyuser/views/index.py +15 -0
- dj_tinyuser/views/invite.py +141 -0
- dj_tinyuser/views/profile.py +470 -0
- dj_tinyuser-0.0.1.dist-info/METADATA +194 -0
- dj_tinyuser-0.0.1.dist-info/RECORD +97 -0
- dj_tinyuser-0.0.1.dist-info/WHEEL +4 -0
- dj_tinyuser-0.0.1.dist-info/licenses/LICENSE +12 -0
- dj_tinyuser-0.0.1.dist-info/licenses/LICENSE.django-project +12 -0
- dj_tinyuser-0.0.1.dist-info/licenses/LICENSE.django-project.md +15 -0
- dj_tinyuser-0.0.1.dist-info/licenses/LICENSE.md +15 -0
dj_tinyuser/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from allauth.account.adapter import DefaultAccountAdapter
|
|
2
|
+
from django.forms import Form
|
|
3
|
+
from django.http import HttpRequest
|
|
4
|
+
from invitations.adapters import BaseInvitationsAdapter
|
|
5
|
+
|
|
6
|
+
from dj_tinyuser import settings
|
|
7
|
+
from dj_tinyuser.models import TinyUser
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TinyUserAccountAdapterBase:
|
|
11
|
+
def save_user(self, request: HttpRequest, user: TinyUser, form: Form, commit: bool = True):
|
|
12
|
+
"""
|
|
13
|
+
Saves a new `User` instance using information provided in the signup form.
|
|
14
|
+
|
|
15
|
+
:param request: The current HTTP request.
|
|
16
|
+
:type request: HttpRequest
|
|
17
|
+
:param user: User instance to be saved.
|
|
18
|
+
:type user: TinyUser
|
|
19
|
+
:param form: The signup form containing user data.
|
|
20
|
+
:type form: forms.Form
|
|
21
|
+
:param commit: Whether to commit the user to the database, defaults to True
|
|
22
|
+
:type commit: bool, optional
|
|
23
|
+
:return: The saved user instance.
|
|
24
|
+
:rtype: TinyUser
|
|
25
|
+
"""
|
|
26
|
+
from django.contrib.auth.models import Group
|
|
27
|
+
|
|
28
|
+
# Only add to groups on user creation, not on update
|
|
29
|
+
if user.pk is None:
|
|
30
|
+
add_to_default_groups = True
|
|
31
|
+
else:
|
|
32
|
+
add_to_default_groups = False
|
|
33
|
+
# Don't add to groups if we're not committing the user yet
|
|
34
|
+
if not commit:
|
|
35
|
+
add_to_default_groups = False
|
|
36
|
+
|
|
37
|
+
user = super().save_user(request, user, form, commit)
|
|
38
|
+
|
|
39
|
+
# Add the user to the default group if specified
|
|
40
|
+
if add_to_default_groups:
|
|
41
|
+
for group_name in settings.DEFAULT_USER_GROUPS:
|
|
42
|
+
group, created = Group.objects.get_or_create(name=group_name)
|
|
43
|
+
user.groups.add(group)
|
|
44
|
+
|
|
45
|
+
return user
|
|
46
|
+
|
|
47
|
+
def allow_signup(self, request: HttpRequest) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Determines whether signups are allowed based on the current request and settings.
|
|
50
|
+
|
|
51
|
+
Logged in users are not allowed to sign up for new accounts,
|
|
52
|
+
and the setting ALLOW_SIGNUP is checked for unauthenticated users.
|
|
53
|
+
|
|
54
|
+
If ALLOW_SIGNUP is set to True, anyone can sign up.
|
|
55
|
+
If it's set to False, signups are disabled and only invited users can sign up.
|
|
56
|
+
|
|
57
|
+
:param request: The current HTTP request.
|
|
58
|
+
:type request: HttpRequest
|
|
59
|
+
:return: True if signups are allowed, False otherwise.
|
|
60
|
+
:rtype: bool
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
if hasattr(request, "user") and request.user.is_authenticated:
|
|
64
|
+
return False # Disable sign ups for authenticated users
|
|
65
|
+
|
|
66
|
+
# Check the ALLOW_SIGNUP setting for unauthenticated users
|
|
67
|
+
return settings.ALLOW_SIGNUP
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TinyUserAccountAdapter(DefaultAccountAdapter, TinyUserAccountAdapterBase):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TinyUserDRFAccountAdapter(DefaultAccountAdapter, BaseInvitationsAdapter, TinyUserAccountAdapterBase):
|
|
75
|
+
def format_email_subject(self, subject, context=None):
|
|
76
|
+
"""
|
|
77
|
+
Formats the email subject for invitation emails.
|
|
78
|
+
|
|
79
|
+
This method is used by the django-invitations package to format the subject of invitation emails.
|
|
80
|
+
It can be overridden to customize the email subject based on the application's needs.
|
|
81
|
+
|
|
82
|
+
:param subject: The original email subject template.
|
|
83
|
+
:type subject: str
|
|
84
|
+
:return: The formatted email subject.
|
|
85
|
+
:rtype: str
|
|
86
|
+
"""
|
|
87
|
+
if context is None:
|
|
88
|
+
return DefaultAccountAdapter.format_email_subject(self, subject)
|
|
89
|
+
else:
|
|
90
|
+
return BaseInvitationsAdapter.format_email_subject(self, subject, context)
|
dj_tinyuser/admin.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
|
3
|
+
from django.utils.translation import gettext_lazy as _
|
|
4
|
+
|
|
5
|
+
from .models import TinyUser, UserProfile
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TinyUserAdmin(BaseUserAdmin):
|
|
9
|
+
fieldsets = (
|
|
10
|
+
(None, {"fields": ("email", "username", "password")}),
|
|
11
|
+
(_("Personal info"), {"fields": ("first_name", "last_name")}),
|
|
12
|
+
(_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser")}),
|
|
13
|
+
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
|
14
|
+
)
|
|
15
|
+
add_fieldsets = (
|
|
16
|
+
(
|
|
17
|
+
None,
|
|
18
|
+
{
|
|
19
|
+
"classes": ("wide",),
|
|
20
|
+
"fields": ("email", "username", "password1", "password2"),
|
|
21
|
+
},
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
list_display = ("email", "username", "is_staff", "is_active")
|
|
25
|
+
search_fields = ("email", "username")
|
|
26
|
+
ordering = ("email",)
|
|
27
|
+
filter_horizontal = ()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
admin.site.register(TinyUser, TinyUserAdmin)
|
|
31
|
+
admin.site.register(UserProfile)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""."""
|
dj_tinyuser/api/apps.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SOME DESCRIPTIVE TITLE.
|
|
2
|
+
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
3
|
+
# This file is distributed under the same license as the PACKAGE package.
|
|
4
|
+
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
5
|
+
#
|
|
6
|
+
#, fuzzy
|
|
7
|
+
msgid ""
|
|
8
|
+
msgstr ""
|
|
9
|
+
"Project-Id-Version: PACKAGE VERSION\n"
|
|
10
|
+
"Report-Msgid-Bugs-To: \n"
|
|
11
|
+
"POT-Creation-Date: 2026-08-30 13:37+0000\n"
|
|
12
|
+
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
13
|
+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
14
|
+
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
15
|
+
"Language: \n"
|
|
16
|
+
"MIME-Version: 1.0\n"
|
|
17
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
18
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
19
|
+
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
20
|
+
|
|
21
|
+
#: dj_tinyuser/webapi/serializers.py:58
|
|
22
|
+
msgid "Email is required."
|
|
23
|
+
msgstr ""
|
|
24
|
+
|
|
25
|
+
#: dj_tinyuser/webapi/serializers.py:60
|
|
26
|
+
msgid "Username is required."
|
|
27
|
+
msgstr ""
|
|
28
|
+
|
|
29
|
+
#: dj_tinyuser/webapi/serializers.py:62
|
|
30
|
+
msgid "Password is required."
|
|
31
|
+
msgstr ""
|
|
32
|
+
|
|
33
|
+
#: dj_tinyuser/webapi/serializers.py:65
|
|
34
|
+
msgid "Signups are not allowed."
|
|
35
|
+
msgstr ""
|
|
36
|
+
|
|
37
|
+
#: dj_tinyuser/webapi/serializers.py:100
|
|
38
|
+
msgid "Email cannot be updated."
|
|
39
|
+
msgstr ""
|
|
40
|
+
|
|
41
|
+
#: dj_tinyuser/webapi/serializers.py:164
|
|
42
|
+
msgid "Unable to authenticate with provided credentials."
|
|
43
|
+
msgstr ""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Permissions for WebAPI."""
|
|
2
|
+
|
|
3
|
+
from rest_framework.exceptions import PermissionDenied
|
|
4
|
+
from rest_framework.permissions import BasePermission
|
|
5
|
+
|
|
6
|
+
from dj_tinyuser import settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IsInvitor(BasePermission):
|
|
10
|
+
"""Permission: is_invitor."""
|
|
11
|
+
|
|
12
|
+
def has_permission(self, request, view):
|
|
13
|
+
"""Check for invitor permission."""
|
|
14
|
+
if not request.user.is_authenticated or not (
|
|
15
|
+
request.user.is_staff
|
|
16
|
+
or bool(
|
|
17
|
+
request.user.groups.filter(
|
|
18
|
+
name__in=settings.INVITE_GROUPS if settings.INVITE_GROUPS else ["invitors"]
|
|
19
|
+
).first()
|
|
20
|
+
)
|
|
21
|
+
):
|
|
22
|
+
raise PermissionDenied("Not an invitor.")
|
|
23
|
+
|
|
24
|
+
return True
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Serializers for the user API Views."""
|
|
2
|
+
|
|
3
|
+
from allauth.account.adapter import get_adapter
|
|
4
|
+
from django.contrib.auth import authenticate, get_user_model
|
|
5
|
+
from django.db.utils import IntegrityError
|
|
6
|
+
from django.utils.translation import gettext as _
|
|
7
|
+
from rest_framework import serializers
|
|
8
|
+
from rest_framework.exceptions import PermissionDenied, ValidationError
|
|
9
|
+
|
|
10
|
+
from dj_tinyuser.models import UserProfile as UserProfile
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProfileSerializer(serializers.ModelSerializer):
|
|
14
|
+
"""Serializer for the UserProfile model."""
|
|
15
|
+
|
|
16
|
+
class Meta:
|
|
17
|
+
"""ProfileSerializer Meta class."""
|
|
18
|
+
|
|
19
|
+
model = UserProfile
|
|
20
|
+
fields = [
|
|
21
|
+
"first_name",
|
|
22
|
+
"last_name",
|
|
23
|
+
"bio",
|
|
24
|
+
"location",
|
|
25
|
+
"website",
|
|
26
|
+
"birth_date",
|
|
27
|
+
"avatar_small",
|
|
28
|
+
"avatar_medium",
|
|
29
|
+
"avatar_large",
|
|
30
|
+
"avatar_full",
|
|
31
|
+
]
|
|
32
|
+
extra_kwargs = {
|
|
33
|
+
"first_name": {"required": False},
|
|
34
|
+
"last_name": {"required": False},
|
|
35
|
+
"location": {"required": False},
|
|
36
|
+
"website": {"required": False},
|
|
37
|
+
"birth_date": {"required": False},
|
|
38
|
+
"bio": {"required": False},
|
|
39
|
+
"avatar_small": {"required": False},
|
|
40
|
+
"avatar_medium": {"required": False},
|
|
41
|
+
"avatar_large": {"required": False},
|
|
42
|
+
"avatar_full": {"required": False},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UserSerializer(serializers.ModelSerializer):
|
|
47
|
+
"""Serializer for the user object."""
|
|
48
|
+
|
|
49
|
+
class Meta:
|
|
50
|
+
"""UserSerializer.Meta class."""
|
|
51
|
+
|
|
52
|
+
model = get_user_model()
|
|
53
|
+
fields = ("email", "password", "username")
|
|
54
|
+
extra_kwargs = {
|
|
55
|
+
"password": {"write_only": True, "min_length": 8},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def create(self, validated_data):
|
|
59
|
+
"""Create a new user with encrypted password and return it."""
|
|
60
|
+
if not get_adapter().is_open_for_signup(self.context["request"]):
|
|
61
|
+
raise PermissionDenied(_("Signups are not allowed."))
|
|
62
|
+
|
|
63
|
+
if not validated_data.get("email"):
|
|
64
|
+
raise serializers.ValidationError(_("Email is required."))
|
|
65
|
+
if not validated_data.get("username"):
|
|
66
|
+
raise serializers.ValidationError(_("Username is required."))
|
|
67
|
+
if not validated_data.get("password"):
|
|
68
|
+
raise serializers.ValidationError(_("Password is required."))
|
|
69
|
+
|
|
70
|
+
user = get_user_model().objects.create_user(**validated_data)
|
|
71
|
+
|
|
72
|
+
return user
|
|
73
|
+
|
|
74
|
+
def update(self, instance, validated_data):
|
|
75
|
+
"""Update a user, setting the password correctly and return it."""
|
|
76
|
+
password = validated_data.pop("password", None)
|
|
77
|
+
|
|
78
|
+
user = super().update(instance, validated_data)
|
|
79
|
+
|
|
80
|
+
if password:
|
|
81
|
+
user.set_password(password)
|
|
82
|
+
user.save()
|
|
83
|
+
|
|
84
|
+
return user
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SafeUserSerializer(serializers.ModelSerializer):
|
|
88
|
+
"""Safe user serializer."""
|
|
89
|
+
|
|
90
|
+
class Meta:
|
|
91
|
+
"""SafeUserSerializer.Meta class."""
|
|
92
|
+
|
|
93
|
+
model = get_user_model()
|
|
94
|
+
tinyuser_profile = ProfileSerializer(required=False)
|
|
95
|
+
fields = ("email", "username", "password", "tinyuser_profile")
|
|
96
|
+
extra_kwargs = {
|
|
97
|
+
"password": {"write_only": True, "min_length": 8},
|
|
98
|
+
"email": {"read_only": True},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def update(self, instance, validated_data):
|
|
102
|
+
"""Update a user, setting the password correctly and return it."""
|
|
103
|
+
password = validated_data.pop("password", None)
|
|
104
|
+
|
|
105
|
+
if "email" in validated_data:
|
|
106
|
+
raise serializers.ValidationError(_("Email cannot be updated."))
|
|
107
|
+
|
|
108
|
+
user = super().update(instance, validated_data)
|
|
109
|
+
|
|
110
|
+
if password:
|
|
111
|
+
user.set_password(password)
|
|
112
|
+
|
|
113
|
+
user.save()
|
|
114
|
+
|
|
115
|
+
# refresh user from database
|
|
116
|
+
user.refresh_from_db()
|
|
117
|
+
|
|
118
|
+
return user
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class UserProfileSerializer(serializers.Serializer):
|
|
122
|
+
"""Serializer for the user profile object."""
|
|
123
|
+
|
|
124
|
+
profile = ProfileSerializer(required=False)
|
|
125
|
+
|
|
126
|
+
class Meta:
|
|
127
|
+
"""UserProfileSerializer.Meta class."""
|
|
128
|
+
|
|
129
|
+
model = UserProfile
|
|
130
|
+
fields = ["email", "username", "profile", "password"]
|
|
131
|
+
extra_kwargs = {
|
|
132
|
+
"password": {"write_only": True, "min_length": 8},
|
|
133
|
+
"email": {"read_only": True},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def update(self, instance, validated_data):
|
|
137
|
+
"""Update user profile."""
|
|
138
|
+
profile_data = validated_data.pop("profile", None)
|
|
139
|
+
for attr, value in validated_data.items():
|
|
140
|
+
setattr(instance, attr, value)
|
|
141
|
+
instance.save()
|
|
142
|
+
|
|
143
|
+
if profile_data:
|
|
144
|
+
profile = instance.profile
|
|
145
|
+
for attr, value in profile_data.items():
|
|
146
|
+
setattr(profile, attr, value)
|
|
147
|
+
profile.save()
|
|
148
|
+
|
|
149
|
+
return instance
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class AuthTokenSerializer(serializers.Serializer):
|
|
153
|
+
"""Serializer for the user authentication object."""
|
|
154
|
+
|
|
155
|
+
email = serializers.EmailField()
|
|
156
|
+
password = serializers.CharField(
|
|
157
|
+
style={"input_type": "password"},
|
|
158
|
+
trim_whitespace=False,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def validate(self, attrs):
|
|
162
|
+
"""Validate and authenticate the user."""
|
|
163
|
+
email = attrs.get("email")
|
|
164
|
+
password = attrs.get("password")
|
|
165
|
+
|
|
166
|
+
user = authenticate(
|
|
167
|
+
request=self.context.get("request"),
|
|
168
|
+
email=email,
|
|
169
|
+
password=password,
|
|
170
|
+
)
|
|
171
|
+
if not user:
|
|
172
|
+
msg = _("Unable to authenticate with provided credentials.")
|
|
173
|
+
raise serializers.ValidationError(msg, code="authentication")
|
|
174
|
+
|
|
175
|
+
attrs["user"] = user
|
|
176
|
+
return attrs
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class InvitationSerializer(serializers.Serializer):
|
|
180
|
+
"""Serializer for invitations."""
|
|
181
|
+
|
|
182
|
+
email = serializers.EmailField()
|
|
183
|
+
|
|
184
|
+
def create(self, validated_data: dict):
|
|
185
|
+
"""
|
|
186
|
+
Create a new invitation.
|
|
187
|
+
|
|
188
|
+
:param validated_data: a dictionary containing the validated data
|
|
189
|
+
for creating an invitation.
|
|
190
|
+
:type validated_data: dict[str, Any]
|
|
191
|
+
:return: an InvitationModel instance.
|
|
192
|
+
"""
|
|
193
|
+
from invitations.utils import get_invitation_model
|
|
194
|
+
|
|
195
|
+
if not validated_data.get("email"):
|
|
196
|
+
raise serializers.ValidationError("email is required")
|
|
197
|
+
|
|
198
|
+
if not validated_data.get("user", getattr(self.context.get("request"), "user", None)):
|
|
199
|
+
raise serializers.ValidationError("inviter is required")
|
|
200
|
+
|
|
201
|
+
email = validated_data.get("email")
|
|
202
|
+
try:
|
|
203
|
+
invitation = get_invitation_model().create(email=email, inviter=self.context.get("request").user)
|
|
204
|
+
except IntegrityError:
|
|
205
|
+
raise ValidationError("email already exists")
|
|
206
|
+
globals()["context"] = self.context
|
|
207
|
+
invitation.send_invitation(self.context.pop("request"), **self.context)
|
|
208
|
+
return invitation
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""REST API Tests."""
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Test Invitations API."""
|
|
2
|
+
|
|
3
|
+
from django.conf import settings as django_settings
|
|
4
|
+
from django.contrib.auth.models import Group
|
|
5
|
+
from django.core import mail as django_mail
|
|
6
|
+
from django.test import TestCase
|
|
7
|
+
from django.urls import reverse
|
|
8
|
+
from rest_framework import status
|
|
9
|
+
from rest_framework.test import APIClient
|
|
10
|
+
|
|
11
|
+
from dj_tinyuser import settings
|
|
12
|
+
from dj_tinyuser.models import TinyUser
|
|
13
|
+
|
|
14
|
+
URL_INVITE = reverse("tinyuser.api:invite")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestInvitationPrivateAPI(TestCase):
|
|
18
|
+
"""Test private invitations API."""
|
|
19
|
+
|
|
20
|
+
def setUp(self):
|
|
21
|
+
"""Set up TestImvitationsPrivateAPI."""
|
|
22
|
+
django_settings.DEFAULT_FROM_EMAIL = "testing.invitations@example.com"
|
|
23
|
+
django_settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
|
24
|
+
|
|
25
|
+
self.group_invitors = Group.objects.get_or_create(name="invitors")[0]
|
|
26
|
+
|
|
27
|
+
self.standard_user = TinyUser.objects.create_user(
|
|
28
|
+
email="std_test@example.com",
|
|
29
|
+
username="testuser",
|
|
30
|
+
password="testpass123",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
self.invitor_user = TinyUser.objects.create_user(
|
|
34
|
+
email="invitor@example.com",
|
|
35
|
+
username="invitor",
|
|
36
|
+
password="invitorpass123",
|
|
37
|
+
)
|
|
38
|
+
self.invitor_user.groups.add(self.group_invitors)
|
|
39
|
+
self.invitor_user.save()
|
|
40
|
+
|
|
41
|
+
self.superuser = TinyUser.objects.create_superuser(
|
|
42
|
+
email="superuser@example.com",
|
|
43
|
+
username="superuser",
|
|
44
|
+
password="superuserpass123",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
self.client = APIClient()
|
|
48
|
+
|
|
49
|
+
def test_invite_unauthenticated_failed(self):
|
|
50
|
+
"""Test that an unauthenticated user cannot invite others."""
|
|
51
|
+
n_messages = len(getattr(django_mail, "outbox", []))
|
|
52
|
+
settings.INVITE_ALLOW_ALL_AUTHENTICATED_USERS = True
|
|
53
|
+
settings.INVITE_GROUPS = []
|
|
54
|
+
|
|
55
|
+
self.client.force_authenticate(user=None)
|
|
56
|
+
|
|
57
|
+
response = self.client.post(URL_INVITE, data={"email": "invitee@example.com"})
|
|
58
|
+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
59
|
+
self.assertEqual(len(django_mail.outbox), n_messages)
|
|
60
|
+
|
|
61
|
+
def test_invite_allowed_all_success(self):
|
|
62
|
+
"""Test that an authenticated user can invite others when allowed by settings."""
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
settings.INVITE_ALLOW_ALL_AUTHENTICATED_USERS = True
|
|
66
|
+
self.client.force_authenticate(user=self.standard_user)
|
|
67
|
+
n_messages = len(getattr(django_mail, "outbox", []))
|
|
68
|
+
response = self.client.post(URL_INVITE, data={"email": "invitee@example.com"})
|
|
69
|
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
70
|
+
self.assertEqual(len(django_mail.outbox), n_messages + 1)
|
|
71
|
+
|
|
72
|
+
def test_invite_not_allowed_all_failed(self):
|
|
73
|
+
"""Test that an authenticated user cannot invite others when not allowed by settings."""
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
settings.INVITE_ALLOW_ALL_AUTHENTICATED_USERS = False
|
|
77
|
+
self.client.force_authenticate(user=self.standard_user)
|
|
78
|
+
n_messages = len(getattr(django_mail, "outbox", []))
|
|
79
|
+
response = self.client.post(URL_INVITE, data={"email": "invitee@example.com"})
|
|
80
|
+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
81
|
+
self.assertEqual(len(django_mail.outbox), n_messages)
|
|
82
|
+
|
|
83
|
+
def test_invite_double_invite_failed(self):
|
|
84
|
+
"""Test that an authenticated user cannot invite the same email twice."""
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
self.client.force_authenticate(user=self.superuser)
|
|
88
|
+
n_messages = len(getattr(django_mail, "outbox", []))
|
|
89
|
+
response = self.client.post(URL_INVITE, data={"email": "invitee@example.com"})
|
|
90
|
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
91
|
+
self.assertEqual(len(django_mail.outbox), n_messages + 1)
|
|
92
|
+
response = self.client.post(URL_INVITE, data={"email": "invitee@example.com"})
|
|
93
|
+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
94
|
+
self.assertEqual(len(django_mail.outbox), n_messages + 1)
|
|
95
|
+
|
|
96
|
+
def test_invite_allowed_group_success(self):
|
|
97
|
+
"""Test that an authenticated user can invite others when allowed by settings."""
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
settings.INVITE_ALLOW_ALL_AUTHENTICATED_USERS = False
|
|
101
|
+
settings.INVITE_GROUPS = [self.group_invitors.name]
|
|
102
|
+
self.client.force_authenticate(user=self.invitor_user)
|
|
103
|
+
n_messages = len(getattr(django_mail, "outbox", []))
|
|
104
|
+
response = self.client.post(URL_INVITE, data={"email": "invitee@example.com"})
|
|
105
|
+
self.assertEqual(response.status_code, 201)
|
|
106
|
+
self.assertEqual(len(django_mail.outbox), n_messages + 1)
|
|
107
|
+
|
|
108
|
+
def test_invite_allowed_group_failed(self):
|
|
109
|
+
"""Test that an authenticated user cannot invite others when not allowed by settings."""
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
settings.INVITE_ALLOW_ALL_AUTHENTICATED_USERS = False
|
|
113
|
+
settings.INVITE_GROUPS = ["invitors"]
|
|
114
|
+
self.client.force_authenticate(user=self.standard_user)
|
|
115
|
+
n_messages = len(getattr(django_mail, "outbox", []))
|
|
116
|
+
response = self.client.post(URL_INVITE, data={"email": "invitee@example.com"})
|
|
117
|
+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
118
|
+
self.assertEqual(len(django_mail.outbox), n_messages)
|
|
119
|
+
|
|
120
|
+
def test_invite_restricetd_standard_user_failed(self):
|
|
121
|
+
"""Test that a restricted standard user cannot invite others."""
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
settings.INVITE_ALLOW_ALL_AUTHENTICATED_USERS = False
|
|
125
|
+
settings.INVITE_GROUPS = []
|
|
126
|
+
self.client.force_authenticate(user=self.standard_user)
|
|
127
|
+
n_messages = len(getattr(django_mail, "outbox", []))
|
|
128
|
+
response = self.client.post(URL_INVITE, data={"email": "invitee@example.com"})
|
|
129
|
+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
130
|
+
self.assertEqual(len(django_mail.outbox), n_messages)
|
|
131
|
+
|
|
132
|
+
def test_invite_restricted_invitor_failed(self):
|
|
133
|
+
"""Test that a restricted standard user cannot invite others."""
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
settings.INVITE_ALLOW_ALL_AUTHENTICATED_USERS = False
|
|
137
|
+
settings.INVITE_GROUPS = []
|
|
138
|
+
self.client.force_authenticate(user=self.standard_user)
|
|
139
|
+
n_messages = len(getattr(django_mail, "outbox", []))
|
|
140
|
+
response = self.client.post(URL_INVITE, data={"email": "invitee@example.com"})
|
|
141
|
+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
142
|
+
self.assertEqual(len(django_mail.outbox), n_messages)
|
|
143
|
+
|
|
144
|
+
def test_invite_superuser_success(self):
|
|
145
|
+
"""Test that a superuser can invite others."""
|
|
146
|
+
return
|
|
147
|
+
settings.INVITE_ALLOW_ALL_AUTHENTICATED_USERS = False
|
|
148
|
+
settings.INVITE_GROUPS = []
|
|
149
|
+
|
|
150
|
+
self.client.force_authenticate(user=self.superuser)
|
|
151
|
+
n_messages = len(getattr(django_mail, "outbox", []))
|
|
152
|
+
response = self.client.post(URL_INVITE, data={"email": "invitee@example.com"})
|
|
153
|
+
self.assertEqual(response.status_code, 201)
|
|
154
|
+
self.assertEqual(len(django_mail.outbox), n_messages + 1)
|
|
155
|
+
|
|
156
|
+
def test_invite_accepted_invtation_failed(self):
|
|
157
|
+
"""Test that an accepted invitation cannot be used to invite others."""
|
|
158
|
+
return
|
|
159
|
+
import re
|
|
160
|
+
|
|
161
|
+
settings.INVITE_ALLOW_ALL_AUTHENTICATED_USERS = False
|
|
162
|
+
settings.INVITE_GROUPS = []
|
|
163
|
+
|
|
164
|
+
self.client.force_authenticate(user=self.superuser)
|
|
165
|
+
n_messages = len(getattr(django_mail, "outbox", []))
|
|
166
|
+
response = self.client.post(URL_INVITE, data={"email": "invitee@example.com"})
|
|
167
|
+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
168
|
+
self.assertEqual(len(django_mail.outbox), n_messages + 1)
|
|
169
|
+
|
|
170
|
+
message: django_mail.EmailMessage = django_mail.outbox[-1]
|
|
171
|
+
self.assertEqual(message.recipients(), ["invitee@example.com"])
|
|
172
|
+
self.assertIn("Invitation to join", message.subject)
|
|
173
|
+
result = re.search(r"http://testserver/invitations/accept-invite/.+", message.body)
|
|
174
|
+
self.assertTrue(result)
|
|
175
|
+
|
|
176
|
+
response_url = re.search(r"http://testserver(/invitations/accept-invite/.+)", message.body).group(1)
|
|
177
|
+
|
|
178
|
+
response = self.client.get(response_url)
|
|
179
|
+
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
|
|
180
|
+
|
|
181
|
+
response = self.client.post(response_url)
|
|
182
|
+
self.assertEqual(response.status_code, status.HTTP_410_GONE)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for UserProfile."""
|