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.
Files changed (97) hide show
  1. dj_tinyuser/__init__.py +0 -0
  2. dj_tinyuser/account_adapter.py +90 -0
  3. dj_tinyuser/admin.py +31 -0
  4. dj_tinyuser/api/__init__.py +1 -0
  5. dj_tinyuser/api/apps.py +9 -0
  6. dj_tinyuser/api/locale/de/LC_MESSAGES/django.po +43 -0
  7. dj_tinyuser/api/permissions.py +24 -0
  8. dj_tinyuser/api/serializers.py +208 -0
  9. dj_tinyuser/api/tests/__init__.py +1 -0
  10. dj_tinyuser/api/tests/test_invitations_api.py +182 -0
  11. dj_tinyuser/api/tests/test_profile_api.py +1 -0
  12. dj_tinyuser/api/tests/test_user_api.py +261 -0
  13. dj_tinyuser/api/urls.py +19 -0
  14. dj_tinyuser/api/views.py +65 -0
  15. dj_tinyuser/apps.py +7 -0
  16. dj_tinyuser/context_processors.py +31 -0
  17. dj_tinyuser/exceptions.py +6 -0
  18. dj_tinyuser/forms.py +96 -0
  19. dj_tinyuser/global_templates/__init__.py +3 -0
  20. dj_tinyuser/global_templates/account/login.html +1 -0
  21. dj_tinyuser/global_templates/account/password_reset.html +1 -0
  22. dj_tinyuser/global_templates/account/signup.html +1 -0
  23. dj_tinyuser/global_templates/allauth/layouts/base.html +1 -0
  24. dj_tinyuser/input_files/style.scss +27 -0
  25. dj_tinyuser/locale/de/LC_MESSAGES/django.po +880 -0
  26. dj_tinyuser/logging.py +11 -0
  27. dj_tinyuser/managers.py +64 -0
  28. dj_tinyuser/migrations/0001_initial.py +331 -0
  29. dj_tinyuser/migrations/__init__.py +0 -0
  30. dj_tinyuser/mixins/htmx.py +20 -0
  31. dj_tinyuser/models/__init__.py +11 -0
  32. dj_tinyuser/models/fields.py +58 -0
  33. dj_tinyuser/models/tinyuser.py +125 -0
  34. dj_tinyuser/models/userprofile.py +216 -0
  35. dj_tinyuser/settings.py +72 -0
  36. dj_tinyuser/sidebar.py +55 -0
  37. dj_tinyuser/static/tinyuser/css/html.css +28 -0
  38. dj_tinyuser/templates/tinyuser/bootstrap5/account/login.dj.html +68 -0
  39. dj_tinyuser/templates/tinyuser/bootstrap5/account/password_reset.dj.html +19 -0
  40. dj_tinyuser/templates/tinyuser/bootstrap5/account/signup.dj.html +24 -0
  41. dj_tinyuser/templates/tinyuser/bootstrap5/base.dj.html +31 -0
  42. dj_tinyuser/templates/tinyuser/bootstrap5/index.html +10 -0
  43. dj_tinyuser/templates/tinyuser/bootstrap5/invite/failed.html +9 -0
  44. dj_tinyuser/templates/tinyuser/bootstrap5/invite/hx/failed.html +14 -0
  45. dj_tinyuser/templates/tinyuser/bootstrap5/invite/hx/invite.html +26 -0
  46. dj_tinyuser/templates/tinyuser/bootstrap5/invite/hx/success.html +14 -0
  47. dj_tinyuser/templates/tinyuser/bootstrap5/invite/invite.html +23 -0
  48. dj_tinyuser/templates/tinyuser/bootstrap5/invite/success.html +9 -0
  49. dj_tinyuser/templates/tinyuser/bootstrap5/profile/edit.html +10 -0
  50. dj_tinyuser/templates/tinyuser/bootstrap5/profile/hx/edit.html +151 -0
  51. dj_tinyuser/templates/tinyuser/bootstrap5/profile/hx/profile.html +120 -0
  52. dj_tinyuser/templates/tinyuser/bootstrap5/profile/hx/settings.html +27 -0
  53. dj_tinyuser/templates/tinyuser/bootstrap5/profile/profile.html +46 -0
  54. dj_tinyuser/templates/tinyuser/bootstrap5/profile/settings.html +18 -0
  55. dj_tinyuser/templates/tinyuser/bootstrap5/snippets/account/form +9 -0
  56. dj_tinyuser/templates/tinyuser/bs5docs/account/login.dj.html +66 -0
  57. dj_tinyuser/templates/tinyuser/bs5docs/account/signup.dj.html +26 -0
  58. dj_tinyuser/templates/tinyuser/html/account/login.dj.html +58 -0
  59. dj_tinyuser/templates/tinyuser/html/account/password_reset.dj.html +20 -0
  60. dj_tinyuser/templates/tinyuser/html/account/signup.dj.html +22 -0
  61. dj_tinyuser/templates/tinyuser/html/base.dj.html +27 -0
  62. dj_tinyuser/templates/tinyuser/html/index.html +7 -0
  63. dj_tinyuser/templates/tinyuser/html/invite/failed.html +9 -0
  64. dj_tinyuser/templates/tinyuser/html/invite/hx/failed.html +0 -0
  65. dj_tinyuser/templates/tinyuser/html/invite/hx/invite.html +0 -0
  66. dj_tinyuser/templates/tinyuser/html/invite/hx/success.html +0 -0
  67. dj_tinyuser/templates/tinyuser/html/invite/invite.html +0 -0
  68. dj_tinyuser/templates/tinyuser/html/invite/success.html +0 -0
  69. dj_tinyuser/templates/tinyuser/html/logout/navbar.dj.html +5 -0
  70. dj_tinyuser/templates/tinyuser/html/profile/edit.html +0 -0
  71. dj_tinyuser/templates/tinyuser/html/profile/hx/edit.html +0 -0
  72. dj_tinyuser/templates/tinyuser/html/profile/hx/profile.html +0 -0
  73. dj_tinyuser/templates/tinyuser/html/profile/hx/settings.html +0 -0
  74. dj_tinyuser/templates/tinyuser/html/profile/profile.html +0 -0
  75. dj_tinyuser/templates/tinyuser/html/profile/settings.html +0 -0
  76. dj_tinyuser/templates/tinyuser/html/theme/nav.dj.html +20 -0
  77. dj_tinyuser/templates.json +39 -0
  78. dj_tinyuser/templates.yml +23 -0
  79. dj_tinyuser/templatetags/__init__.py +0 -0
  80. dj_tinyuser/templatetags/tinyuser.py +14 -0
  81. dj_tinyuser/tests/__init__.py +0 -0
  82. dj_tinyuser/tests/test_tinyuser_model.py +46 -0
  83. dj_tinyuser/tests/test_userprofile_model.py +56 -0
  84. dj_tinyuser/urls.py +22 -0
  85. dj_tinyuser/utils.py +18 -0
  86. dj_tinyuser/views/__init__.py +12 -0
  87. dj_tinyuser/views/dashboard.py +0 -0
  88. dj_tinyuser/views/index.py +15 -0
  89. dj_tinyuser/views/invite.py +141 -0
  90. dj_tinyuser/views/profile.py +470 -0
  91. dj_tinyuser-0.0.1.dist-info/METADATA +194 -0
  92. dj_tinyuser-0.0.1.dist-info/RECORD +97 -0
  93. dj_tinyuser-0.0.1.dist-info/WHEEL +4 -0
  94. dj_tinyuser-0.0.1.dist-info/licenses/LICENSE +12 -0
  95. dj_tinyuser-0.0.1.dist-info/licenses/LICENSE.django-project +12 -0
  96. dj_tinyuser-0.0.1.dist-info/licenses/LICENSE.django-project.md +15 -0
  97. dj_tinyuser-0.0.1.dist-info/licenses/LICENSE.md +15 -0
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
+ """."""
@@ -0,0 +1,9 @@
1
+ """."""
2
+
3
+ from django.apps import AppConfig
4
+
5
+
6
+ class TinyuserApiConfig(AppConfig):
7
+ """dj_tinyuser.api app config."""
8
+
9
+ name = "dj_tinyuser.api"
@@ -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."""