aboutsummaryrefslogtreecommitdiffstats
path: root/ctrack/users
diff options
context:
space:
mode:
authorMatthew Lemon <lemon@matthewlemon.com>2020-01-19 15:57:06 +0000
committerMatthew Lemon <lemon@matthewlemon.com>2020-01-19 15:57:06 +0000
commit9d76a3c52b8310726ec09e0262813f0438c21df6 (patch)
tree4acf47dce6c3aa75f8ad7c5cb56fe6486c2d64a7 /ctrack/users
init commit - from cookiecutter
Diffstat (limited to 'ctrack/users')
-rw-r--r--ctrack/users/__init__.py0
-rw-r--r--ctrack/users/adapters.py16
-rw-r--r--ctrack/users/admin.py17
-rw-r--r--ctrack/users/apps.py13
-rw-r--r--ctrack/users/forms.py30
-rw-r--r--ctrack/users/migrations/0001_initial.py132
-rw-r--r--ctrack/users/migrations/__init__.py0
-rw-r--r--ctrack/users/models.py14
-rw-r--r--ctrack/users/tests/__init__.py0
-rw-r--r--ctrack/users/tests/factories.py27
-rw-r--r--ctrack/users/tests/test_forms.py40
-rw-r--r--ctrack/users/tests/test_models.py9
-rw-r--r--ctrack/users/tests/test_urls.py24
-rw-r--r--ctrack/users/tests/test_views.py46
-rw-r--r--ctrack/users/urls.py14
-rw-r--r--ctrack/users/views.py50
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()