diff options
Diffstat (limited to 'ctrack/users')
-rw-r--r-- | ctrack/users/__init__.py | 0 | ||||
-rw-r--r-- | ctrack/users/adapters.py | 16 | ||||
-rw-r--r-- | ctrack/users/admin.py | 17 | ||||
-rw-r--r-- | ctrack/users/apps.py | 13 | ||||
-rw-r--r-- | ctrack/users/forms.py | 30 | ||||
-rw-r--r-- | ctrack/users/migrations/0001_initial.py | 132 | ||||
-rw-r--r-- | ctrack/users/migrations/__init__.py | 0 | ||||
-rw-r--r-- | ctrack/users/models.py | 14 | ||||
-rw-r--r-- | ctrack/users/tests/__init__.py | 0 | ||||
-rw-r--r-- | ctrack/users/tests/factories.py | 27 | ||||
-rw-r--r-- | ctrack/users/tests/test_forms.py | 40 | ||||
-rw-r--r-- | ctrack/users/tests/test_models.py | 9 | ||||
-rw-r--r-- | ctrack/users/tests/test_urls.py | 24 | ||||
-rw-r--r-- | ctrack/users/tests/test_views.py | 46 | ||||
-rw-r--r-- | ctrack/users/urls.py | 14 | ||||
-rw-r--r-- | ctrack/users/views.py | 50 |
16 files changed, 432 insertions, 0 deletions
diff --git a/ctrack/users/__init__.py b/ctrack/users/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ctrack/users/__init__.py diff --git a/ctrack/users/adapters.py b/ctrack/users/adapters.py new file mode 100644 index 0000000..0d206fa --- /dev/null +++ b/ctrack/users/adapters.py @@ -0,0 +1,16 @@ +from typing import Any + +from allauth.account.adapter import DefaultAccountAdapter +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from django.conf import settings +from django.http import HttpRequest + + +class AccountAdapter(DefaultAccountAdapter): + def is_open_for_signup(self, request: HttpRequest): + return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) + + +class SocialAccountAdapter(DefaultSocialAccountAdapter): + def is_open_for_signup(self, request: HttpRequest, sociallogin: Any): + return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) diff --git a/ctrack/users/admin.py b/ctrack/users/admin.py new file mode 100644 index 0000000..120cc64 --- /dev/null +++ b/ctrack/users/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from django.contrib.auth import admin as auth_admin +from django.contrib.auth import get_user_model + +from ctrack.users.forms import UserChangeForm, UserCreationForm + +User = get_user_model() + + +@admin.register(User) +class UserAdmin(auth_admin.UserAdmin): + + form = UserChangeForm + add_form = UserCreationForm + fieldsets = (("User", {"fields": ("name",)}),) + auth_admin.UserAdmin.fieldsets + list_display = ["username", "name", "is_superuser"] + search_fields = ["name"] diff --git a/ctrack/users/apps.py b/ctrack/users/apps.py new file mode 100644 index 0000000..9f81f1d --- /dev/null +++ b/ctrack/users/apps.py @@ -0,0 +1,13 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class UsersConfig(AppConfig): + name = "ctrack.users" + verbose_name = _("Users") + + def ready(self): + try: + import ctrack.users.signals # noqa F401 + except ImportError: + pass diff --git a/ctrack/users/forms.py b/ctrack/users/forms.py new file mode 100644 index 0000000..250cc90 --- /dev/null +++ b/ctrack/users/forms.py @@ -0,0 +1,30 @@ +from django.contrib.auth import get_user_model, forms +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + +User = get_user_model() + + +class UserChangeForm(forms.UserChangeForm): + class Meta(forms.UserChangeForm.Meta): + model = User + + +class UserCreationForm(forms.UserCreationForm): + + error_message = forms.UserCreationForm.error_messages.update( + {"duplicate_username": _("This username has already been taken.")} + ) + + class Meta(forms.UserCreationForm.Meta): + model = User + + def clean_username(self): + username = self.cleaned_data["username"] + + try: + User.objects.get(username=username) + except User.DoesNotExist: + return username + + raise ValidationError(self.error_messages["duplicate_username"]) diff --git a/ctrack/users/migrations/0001_initial.py b/ctrack/users/migrations/0001_initial.py new file mode 100644 index 0000000..c9d8905 --- /dev/null +++ b/ctrack/users/migrations/0001_initial.py @@ -0,0 +1,132 @@ +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [("auth", "0008_alter_user_username_max_length")] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=30, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "name", + models.CharField( + blank=True, max_length=255, verbose_name="Name of User" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name_plural": "users", + "verbose_name": "user", + "abstract": False, + }, + managers=[("objects", django.contrib.auth.models.UserManager())], + ) + ] diff --git a/ctrack/users/migrations/__init__.py b/ctrack/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ctrack/users/migrations/__init__.py diff --git a/ctrack/users/models.py b/ctrack/users/models.py new file mode 100644 index 0000000..8f07b15 --- /dev/null +++ b/ctrack/users/models.py @@ -0,0 +1,14 @@ +from django.contrib.auth.models import AbstractUser +from django.db.models import CharField +from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ + + +class User(AbstractUser): + + # First Name and Last Name do not cover name patterns + # around the globe. + name = CharField(_("Name of User"), blank=True, max_length=255) + + def get_absolute_url(self): + return reverse("users:detail", kwargs={"username": self.username}) diff --git a/ctrack/users/tests/__init__.py b/ctrack/users/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ctrack/users/tests/__init__.py diff --git a/ctrack/users/tests/factories.py b/ctrack/users/tests/factories.py new file mode 100644 index 0000000..b537136 --- /dev/null +++ b/ctrack/users/tests/factories.py @@ -0,0 +1,27 @@ +from typing import Any, Sequence + +from django.contrib.auth import get_user_model +from factory import DjangoModelFactory, Faker, post_generation + + +class UserFactory(DjangoModelFactory): + + username = Faker("user_name") + email = Faker("email") + name = Faker("name") + + @post_generation + def password(self, create: bool, extracted: Sequence[Any], **kwargs): + password = Faker( + "password", + length=42, + special_chars=True, + digits=True, + upper_case=True, + lower_case=True, + ).generate(extra_kwargs={}) + self.set_password(password) + + class Meta: + model = get_user_model() + django_get_or_create = ["username"] diff --git a/ctrack/users/tests/test_forms.py b/ctrack/users/tests/test_forms.py new file mode 100644 index 0000000..11ef251 --- /dev/null +++ b/ctrack/users/tests/test_forms.py @@ -0,0 +1,40 @@ +import pytest + +from ctrack.users.forms import UserCreationForm +from ctrack.users.tests.factories import UserFactory + +pytestmark = pytest.mark.django_db + + +class TestUserCreationForm: + def test_clean_username(self): + # A user with proto_user params does not exist yet. + proto_user = UserFactory.build() + + form = UserCreationForm( + { + "username": proto_user.username, + "password1": proto_user._password, + "password2": proto_user._password, + } + ) + + assert form.is_valid() + assert form.clean_username() == proto_user.username + + # Creating a user. + form.save() + + # The user with proto_user params already exists, + # hence cannot be created. + form = UserCreationForm( + { + "username": proto_user.username, + "password1": proto_user._password, + "password2": proto_user._password, + } + ) + + assert not form.is_valid() + assert len(form.errors) == 1 + assert "username" in form.errors diff --git a/ctrack/users/tests/test_models.py b/ctrack/users/tests/test_models.py new file mode 100644 index 0000000..1b33961 --- /dev/null +++ b/ctrack/users/tests/test_models.py @@ -0,0 +1,9 @@ +import pytest + +from ctrack.users.models import User + +pytestmark = pytest.mark.django_db + + +def test_user_get_absolute_url(user: User): + assert user.get_absolute_url() == f"/users/{user.username}/" diff --git a/ctrack/users/tests/test_urls.py b/ctrack/users/tests/test_urls.py new file mode 100644 index 0000000..383a649 --- /dev/null +++ b/ctrack/users/tests/test_urls.py @@ -0,0 +1,24 @@ +import pytest +from django.urls import reverse, resolve + +from ctrack.users.models import User + +pytestmark = pytest.mark.django_db + + +def test_detail(user: User): + assert ( + reverse("users:detail", kwargs={"username": user.username}) + == f"/users/{user.username}/" + ) + assert resolve(f"/users/{user.username}/").view_name == "users:detail" + + +def test_update(): + assert reverse("users:update") == "/users/~update/" + assert resolve("/users/~update/").view_name == "users:update" + + +def test_redirect(): + assert reverse("users:redirect") == "/users/~redirect/" + assert resolve("/users/~redirect/").view_name == "users:redirect" diff --git a/ctrack/users/tests/test_views.py b/ctrack/users/tests/test_views.py new file mode 100644 index 0000000..3e7fbf7 --- /dev/null +++ b/ctrack/users/tests/test_views.py @@ -0,0 +1,46 @@ +import pytest +from django.test import RequestFactory + +from ctrack.users.models import User +from ctrack.users.views import UserRedirectView, UserUpdateView + +pytestmark = pytest.mark.django_db + + +class TestUserUpdateView: + """ + TODO: + extracting view initialization code as class-scoped fixture + would be great if only pytest-django supported non-function-scoped + fixture db access -- this is a work-in-progress for now: + https://github.com/pytest-dev/pytest-django/pull/258 + """ + + def test_get_success_url(self, user: User, request_factory: RequestFactory): + view = UserUpdateView() + request = request_factory.get("/fake-url/") + request.user = user + + view.request = request + + assert view.get_success_url() == f"/users/{user.username}/" + + def test_get_object(self, user: User, request_factory: RequestFactory): + view = UserUpdateView() + request = request_factory.get("/fake-url/") + request.user = user + + view.request = request + + assert view.get_object() == user + + +class TestUserRedirectView: + def test_get_redirect_url(self, user: User, request_factory: RequestFactory): + view = UserRedirectView() + request = request_factory.get("/fake-url") + request.user = user + + view.request = request + + assert view.get_redirect_url() == f"/users/{user.username}/" diff --git a/ctrack/users/urls.py b/ctrack/users/urls.py new file mode 100644 index 0000000..cc1d04c --- /dev/null +++ b/ctrack/users/urls.py @@ -0,0 +1,14 @@ +from django.urls import path + +from ctrack.users.views import ( + user_redirect_view, + user_update_view, + user_detail_view, +) + +app_name = "users" +urlpatterns = [ + path("~redirect/", view=user_redirect_view, name="redirect"), + path("~update/", view=user_update_view, name="update"), + path("<str:username>/", view=user_detail_view, name="detail"), +] diff --git a/ctrack/users/views.py b/ctrack/users/views.py new file mode 100644 index 0000000..5c0d5b5 --- /dev/null +++ b/ctrack/users/views.py @@ -0,0 +1,50 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse +from django.views.generic import DetailView, RedirectView, UpdateView +from django.contrib import messages +from django.utils.translation import ugettext_lazy as _ + +User = get_user_model() + + +class UserDetailView(LoginRequiredMixin, DetailView): + + model = User + slug_field = "username" + slug_url_kwarg = "username" + + +user_detail_view = UserDetailView.as_view() + + +class UserUpdateView(LoginRequiredMixin, UpdateView): + + model = User + fields = ["name"] + + def get_success_url(self): + return reverse("users:detail", kwargs={"username": self.request.user.username}) + + def get_object(self): + return User.objects.get(username=self.request.user.username) + + def form_valid(self, form): + messages.add_message( + self.request, messages.INFO, _("Infos successfully updated") + ) + return super().form_valid(form) + + +user_update_view = UserUpdateView.as_view() + + +class UserRedirectView(LoginRequiredMixin, RedirectView): + + permanent = False + + def get_redirect_url(self): + return reverse("users:detail", kwargs={"username": self.request.user.username}) + + +user_redirect_view = UserRedirectView.as_view() |