Add username_type option (#3958)

* Add username_type option

* Update docs

Signed-off-by: Andrew-Chen-Wang <acwangpython@gmail.com>

* Fix errors and lint

Signed-off-by: Andrew-Chen-Wang <acwangpython@gmail.com>

* Try linting again

* Try linting again

* Handle signup

* Fix url reverse in template

* Fix migration file

* Remove extra line in factories

* E2E tests for email username

* Fix style in users api views and serializers

* Fix style in users api views and serializers

* Add conditions for fields in UserAdminCreationForm

* Consistent Jinja tag conditions

* Fix more empty lines issues

* Tweak options order

* Test username email

* Fix code style

* Fix kwargs to reverse user details view

* Remove comment in template

* Fix error messages dict in user form

* Fix line wrapping

* Move custom manager into a separate module and add tests

---------

Signed-off-by: Andrew-Chen-Wang <acwangpython@gmail.com>
Co-authored-by: Bruno Alla <alla.brunoo@gmail.com>
Co-authored-by: Bruno Alla <browniebroke@users.noreply.github.com>
This commit is contained in:
Andrew Chen Wang 2023-04-15 06:53:15 -04:00 committed by GitHub
parent 42c30dafcb
commit 1783257ed3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 296 additions and 18 deletions

View File

@ -38,7 +38,7 @@ jobs:
matrix: matrix:
script: script:
- name: Basic - name: Basic
args: "" args: "ci_tool=Gitlab"
- name: Celery & DRF - name: Celery & DRF
args: "use_celery=y use_drf=y" args: "use_celery=y use_drf=y"
- name: Gulp - name: Gulp
@ -73,7 +73,9 @@ jobs:
- name: Gulp - name: Gulp
args: "frontend_pipeline=Gulp" args: "frontend_pipeline=Gulp"
- name: Webpack - name: Webpack
args: "frontend_pipeline=Webpack" args: "frontend_pipeline=Webpack use_heroku=y"
- name: Email Username
args: "username_type=email ci_tool=Github"
name: "Bare metal ${{ matrix.script.name }}" name: "Bare metal ${{ matrix.script.name }}"
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -118,6 +118,10 @@ Answer the prompts with your own desired [options](http://cookiecutter-django.re
4 - Apache Software License 2.0 4 - Apache Software License 2.0
5 - Not open source 5 - Not open source
Choose from 1, 2, 3, 4, 5 [1]: 1 Choose from 1, 2, 3, 4, 5 [1]: 1
Select username_type:
1 - username
2 - email
Choose from 1, 2 [1]: 1
timezone [UTC]: America/Los_Angeles timezone [UTC]: America/Los_Angeles
windows [n]: n windows [n]: n
use_pycharm [n]: y use_pycharm [n]: y

View File

@ -13,6 +13,7 @@
"Apache Software License 2.0", "Apache Software License 2.0",
"Not open source" "Not open source"
], ],
"username_type": ["username", "email"],
"timezone": "UTC", "timezone": "UTC",
"windows": "n", "windows": "n",
"use_pycharm": "n", "use_pycharm": "n",

View File

@ -24,6 +24,13 @@ author_name:
email: email:
The email address you want to identify yourself in the project. The email address you want to identify yourself in the project.
username_type:
The type of username you want to use in the project. This can be either
``username`` or ``email``. If you choose ``username``, the ``email`` field
will be included. If you choose ``email``, the ``username`` field will be
excluded. It is best practice to always include an email field, so there is
no option for having just the ``username`` field.
domain_name: domain_name:
The domain name you plan to use for your project once it goes live. The domain name you plan to use for your project once it goes live.
Note that it can be safely changed later on whenever you need to. Note that it can be safely changed later on whenever you need to.

View File

@ -45,6 +45,24 @@ def remove_gplv3_files():
os.remove(file_name) os.remove(file_name)
def remove_custom_user_manager_files():
os.remove(
os.path.join(
"{{cookiecutter.project_slug}}",
"users",
"managers.py",
)
)
os.remove(
os.path.join(
"{{cookiecutter.project_slug}}",
"users",
"tests",
"test_managers.py",
)
)
def remove_pycharm_files(): def remove_pycharm_files():
idea_dir_path = ".idea" idea_dir_path = ".idea"
if os.path.exists(idea_dir_path): if os.path.exists(idea_dir_path):
@ -441,6 +459,9 @@ def main():
if "{{ cookiecutter.open_source_license}}" != "GPLv3": if "{{ cookiecutter.open_source_license}}" != "GPLv3":
remove_gplv3_files() remove_gplv3_files()
if "{{ cookiecutter.username_type }}" == "username":
remove_custom_user_manager_files()
if "{{ cookiecutter.use_pycharm }}".lower() == "n": if "{{ cookiecutter.use_pycharm }}".lower() == "n":
remove_pycharm_files() remove_pycharm_files()

View File

@ -36,6 +36,8 @@ def context():
SUPPORTED_COMBINATIONS = [ SUPPORTED_COMBINATIONS = [
{"username_type": "username"},
{"username_type": "email"},
{"open_source_license": "MIT"}, {"open_source_license": "MIT"},
{"open_source_license": "BSD"}, {"open_source_license": "BSD"},
{"open_source_license": "GPLv3"}, {"open_source_license": "GPLv3"},

View File

@ -312,9 +312,15 @@ CELERY_TASK_SEND_SENT_EVENT = True
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True) ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True)
# https://django-allauth.readthedocs.io/en/latest/configuration.html # https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_AUTHENTICATION_METHOD = "username" ACCOUNT_AUTHENTICATION_METHOD = "{{cookiecutter.username_type}}"
# https://django-allauth.readthedocs.io/en/latest/configuration.html # https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_REQUIRED = True
{%- if cookiecutter.username_type == "email" %}
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_USERNAME_REQUIRED = False
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
{%- endif %}
# https://django-allauth.readthedocs.io/en/latest/configuration.html # https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_EMAIL_VERIFICATION = "mandatory" ACCOUNT_EMAIL_VERIFICATION = "mandatory"
# https://django-allauth.readthedocs.io/en/latest/configuration.html # https://django-allauth.readthedocs.io/en/latest/configuration.html

View File

@ -92,8 +92,7 @@
</li> </li>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<li class="nav-item"> <li class="nav-item">
{# URL provided by django-allauth/account/urls.py #} <a class="nav-link" href="{% endraw %}{% if cookiecutter.username_type == "email" %}{% raw %}{% url 'users:detail' request.user.pk %}{% endraw %}{% else %}{% raw %}{% url 'users:detail' request.user.username %}{% endraw %}{% endif %}{% raw %}">{% translate "My Profile" %}</a>
<a class="nav-link" href="{% url 'users:detail' request.user.username %}">{% translate "My Profile" %}</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
{# URL provided by django-allauth/account/urls.py #} {# URL provided by django-allauth/account/urls.py #}

View File

@ -1,7 +1,7 @@
{% raw %}{% extends "base.html" %} {% raw %}{% extends "base.html" %}
{% load static %} {% load static %}
{% block title %}User: {{ object.username }}{% endblock %} {% block title %}User: {% endraw %}{% if cookiecutter.username_type == "email" %}{% raw %}{{ object.name }}{% endraw %}{% else %}{% raw %}{{ object.username }}{% endraw %}{% endif %}{% raw %}{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
@ -9,7 +9,7 @@
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<h2>{{ object.username }}</h2> <h2>{% endraw %}{% if cookiecutter.username_type == "email" %}{% raw %}{{ object.name }}{% endraw %}{% else %}{% raw %}{{ object.username }}{% endraw %}{% endif %}{% raw %}</h2>
{% if object.name %} {% if object.name %}
<p>{{ object.name }}</p> <p>{{ object.name }}</p>
{% endif %} {% endif %}

View File

@ -1,10 +1,10 @@
{% raw %}{% extends "base.html" %} {% raw %}{% extends "base.html" %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title %}{{ user.username }}{% endblock %} {% block title %}{% endraw %}{% if cookiecutter.username_type == "email" %}{% raw %}{{ user.name }}{% endraw %}{% else %}{% raw %}{{ user.username }}{% endraw %}{% endif %}{% raw %}{% endblock %}
{% block content %} {% block content %}
<h1>{{ user.username }}</h1> <h1>{% endraw %}{% if cookiecutter.username_type == "email" %}{% raw %}{{ user.name }}{% endraw %}{% else %}{% raw %}{{ user.username }}{% endraw %}{% endif %}{% raw %}</h1>
<form class="form-horizontal" method="post" action="{% url 'users:update' %}"> <form class="form-horizontal" method="post" action="{% url 'users:update' %}">
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}

View File

@ -13,8 +13,13 @@ class UserAdmin(auth_admin.UserAdmin):
form = UserAdminChangeForm form = UserAdminChangeForm
add_form = UserAdminCreationForm add_form = UserAdminCreationForm
fieldsets = ( fieldsets = (
{%- if cookiecutter.username_type == "email" %}
(None, {"fields": ("email", "password")}),
(_("Personal info"), {"fields": ("name",)}),
{%- else %}
(None, {"fields": ("username", "password")}), (None, {"fields": ("username", "password")}),
(_("Personal info"), {"fields": ("name", "email")}), (_("Personal info"), {"fields": ("name", "email")}),
{%- endif %}
( (
_("Permissions"), _("Permissions"),
{ {
@ -29,5 +34,17 @@ class UserAdmin(auth_admin.UserAdmin):
), ),
(_("Important dates"), {"fields": ("last_login", "date_joined")}), (_("Important dates"), {"fields": ("last_login", "date_joined")}),
) )
list_display = ["username", "name", "is_superuser"] list_display = ["{{cookiecutter.username_type}}", "name", "is_superuser"]
search_fields = ["name"] search_fields = ["name"]
{%- if cookiecutter.username_type == "email" %}
ordering = ["id"]
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("email", "password1", "password2"),
},
),
)
{%- endif %}

View File

@ -7,8 +7,16 @@ User = get_user_model()
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
{%- if cookiecutter.username_type == "email" %}
fields = ["name", "url"]
extra_kwargs = {
"url": {"view_name": "api:user-detail", "lookup_field": "pk"}
}
{%- else %}
fields = ["username", "name", "url"] fields = ["username", "name", "url"]
extra_kwargs = { extra_kwargs = {
"url": {"view_name": "api:user-detail", "lookup_field": "username"} "url": {"view_name": "api:user-detail", "lookup_field": "username"}
} }
{%- endif %}

View File

@ -13,7 +13,11 @@ User = get_user_model()
class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet): class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet):
serializer_class = UserSerializer serializer_class = UserSerializer
queryset = User.objects.all() queryset = User.objects.all()
{%- if cookiecutter.username_type == "email" %}
lookup_field = "pk"
{%- else %}
lookup_field = "username" lookup_field = "username"
{%- endif %}
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
assert isinstance(self.request.user.id, int) assert isinstance(self.request.user.id, int)

View File

@ -2,6 +2,9 @@ from allauth.account.forms import SignupForm
from allauth.socialaccount.forms import SignupForm as SocialSignupForm from allauth.socialaccount.forms import SignupForm as SocialSignupForm
from django.contrib.auth import forms as admin_forms from django.contrib.auth import forms as admin_forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
{%- if cookiecutter.username_type == "email" %}
from django.forms import EmailField
{%- endif %}
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
User = get_user_model() User = get_user_model()
@ -10,6 +13,9 @@ User = get_user_model()
class UserAdminChangeForm(admin_forms.UserChangeForm): class UserAdminChangeForm(admin_forms.UserChangeForm):
class Meta(admin_forms.UserChangeForm.Meta): class Meta(admin_forms.UserChangeForm.Meta):
model = User model = User
{%- if cookiecutter.username_type == "email" %}
field_classes = {"email": EmailField}
{%- endif %}
class UserAdminCreationForm(admin_forms.UserCreationForm): class UserAdminCreationForm(admin_forms.UserCreationForm):
@ -20,10 +26,17 @@ class UserAdminCreationForm(admin_forms.UserCreationForm):
class Meta(admin_forms.UserCreationForm.Meta): class Meta(admin_forms.UserCreationForm.Meta):
model = User model = User
{%- if cookiecutter.username_type == "email" %}
fields = ("email",)
field_classes = {"email": EmailField}
error_messages = { error_messages = {
"username": {"unique": _("This username has already been taken.")} "email": {"unique": _("This email has already been taken.")},
} }
{%- else %}
error_messages = {
"username": {"unique": _("This username has already been taken.")},
}
{%- endif %}
class UserSignupForm(SignupForm): class UserSignupForm(SignupForm):

View File

@ -0,0 +1,34 @@
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import UserManager as DjangoUserManager
class UserManager(DjangoUserManager):
"""Custom manager for the User model."""
def _create_user(self, email: str, password: str | None, **extra_fields):
"""
Create and save a user with the given email and password.
"""
if not email:
raise ValueError("The given email must be set")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.password = make_password(password)
user.save(using=self._db)
return user
def create_user(self, email: str, password: str | None = None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email: str, password: str | None = None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self._create_user(email, password, **extra_fields)

View File

@ -3,6 +3,8 @@ import django.contrib.auth.validators
from django.db import migrations, models from django.db import migrations, models
import django.utils.timezone import django.utils.timezone
import {{cookiecutter.project_slug}}.users.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -40,6 +42,7 @@ class Migration(migrations.Migration):
verbose_name="superuser status", verbose_name="superuser status",
), ),
), ),
{%- if cookiecutter.username_type == "username" -%}
( (
"username", "username",
models.CharField( models.CharField(
@ -61,6 +64,14 @@ class Migration(migrations.Migration):
blank=True, max_length=254, verbose_name="email address" blank=True, max_length=254, verbose_name="email address"
), ),
), ),
{%- else %}
(
"email",
models.EmailField(
unique=True, max_length=254, verbose_name="email address"
),
),
{%- endif %}
( (
"is_staff", "is_staff",
models.BooleanField( models.BooleanField(
@ -118,7 +129,11 @@ class Migration(migrations.Migration):
"abstract": False, "abstract": False,
}, },
managers=[ managers=[
{%- if cookiecutter.username_type == "email" %}
("objects", {{cookiecutter.project_slug}}.users.models.UserManager()),
{%- else %}
("objects", django.contrib.auth.models.UserManager()), ("objects", django.contrib.auth.models.UserManager()),
{%- endif %}
], ],
), ),
] ]

View File

@ -1,7 +1,11 @@
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db.models import CharField from django.db.models import CharField{% if cookiecutter.username_type == "email" %}, EmailField{% endif %}
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
{%- if cookiecutter.username_type == "email" %}
from {{ cookiecutter.project_slug }}.users.managers import UserManager
{%- endif %}
class User(AbstractUser): class User(AbstractUser):
@ -11,16 +15,29 @@ class User(AbstractUser):
check forms.SignupForm and forms.SocialSignupForms accordingly. check forms.SignupForm and forms.SocialSignupForms accordingly.
""" """
#: First and last name do not cover name patterns around the globe # First and last name do not cover name patterns around the globe
name = CharField(_("Name of User"), blank=True, max_length=255) name = CharField(_("Name of User"), blank=True, max_length=255)
first_name = None # type: ignore first_name = None # type: ignore
last_name = None # type: ignore last_name = None # type: ignore
{%- if cookiecutter.username_type == "email" %}
email = EmailField(_("email address"), unique=True)
username = None # type: ignore
def get_absolute_url(self): USERNAME_FIELD = "email"
"""Get url for user's detail view. REQUIRED_FIELDS = []
objects = UserManager()
{%- endif %}
def get_absolute_url(self) -> str:
"""Get URL for user's detail view.
Returns: Returns:
str: URL for user detail. str: URL for user detail.
""" """
{%- if cookiecutter.username_type == "email" %}
return reverse("users:detail", kwargs={"pk": self.id})
{%- else %}
return reverse("users:detail", kwargs={"username": self.username}) return reverse("users:detail", kwargs={"username": self.username})
{%- endif %}

View File

@ -7,7 +7,9 @@ from factory.django import DjangoModelFactory
class UserFactory(DjangoModelFactory): class UserFactory(DjangoModelFactory):
{%- if cookiecutter.username_type == "username" %}
username = Faker("user_name") username = Faker("user_name")
{%- endif %}
email = Faker("email") email = Faker("email")
name = Faker("name") name = Faker("name")
@ -29,4 +31,4 @@ class UserFactory(DjangoModelFactory):
class Meta: class Meta:
model = get_user_model() model = get_user_model()
django_get_or_create = ["username"] django_get_or_create = ["{{cookiecutter.username_type}}"]

View File

@ -22,16 +22,28 @@ class TestUserAdmin:
response = admin_client.post( response = admin_client.post(
url, url,
data={ data={
{%- if cookiecutter.username_type == "email" %}
"email": "new-admin@example.com",
{%- else %}
"username": "test", "username": "test",
{%- endif %}
"password1": "My_R@ndom-P@ssw0rd", "password1": "My_R@ndom-P@ssw0rd",
"password2": "My_R@ndom-P@ssw0rd", "password2": "My_R@ndom-P@ssw0rd",
}, },
) )
assert response.status_code == 302 assert response.status_code == 302
{%- if cookiecutter.username_type == "email" %}
assert User.objects.filter(email="new-admin@example.com").exists()
{%- else %}
assert User.objects.filter(username="test").exists() assert User.objects.filter(username="test").exists()
{%- endif %}
def test_view_user(self, admin_client): def test_view_user(self, admin_client):
{%- if cookiecutter.username_type == "email" %}
user = User.objects.get(email="admin@example.com")
{%- else %}
user = User.objects.get(username="admin") user = User.objects.get(username="admin")
{%- endif %}
url = reverse("admin:users_user_change", kwargs={"object_id": user.pk}) url = reverse("admin:users_user_change", kwargs={"object_id": user.pk})
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == 200

View File

@ -4,11 +4,19 @@ from {{ cookiecutter.project_slug }}.users.models import User
def test_user_detail(user: User): def test_user_detail(user: User):
{%- if cookiecutter.username_type == "email" %}
assert (
reverse("api:user-detail", kwargs={"pk": user.pk})
== f"/api/users/{user.pk}/"
)
assert resolve(f"/api/users/{user.pk}/").view_name == "api:user-detail"
{%- else %}
assert ( assert (
reverse("api:user-detail", kwargs={"username": user.username}) reverse("api:user-detail", kwargs={"username": user.username})
== f"/api/users/{user.username}/" == f"/api/users/{user.username}/"
) )
assert resolve(f"/api/users/{user.username}/").view_name == "api:user-detail" assert resolve(f"/api/users/{user.username}/").view_name == "api:user-detail"
{%- endif %}
def test_user_list(): def test_user_list():

View File

@ -29,7 +29,11 @@ class TestUserViewSet:
response = view.me(request) # type: ignore response = view.me(request) # type: ignore
assert response.data == { assert response.data == {
{%- if cookiecutter.username_type == "email" %}
"url": f"http://testserver/api/users/{user.pk}/",
{%- else %}
"username": user.username, "username": user.username,
"name": user.name,
"url": f"http://testserver/api/users/{user.username}/", "url": f"http://testserver/api/users/{user.username}/",
{%- endif %}
"name": user.name,
} }

View File

@ -24,7 +24,11 @@ class TestUserAdminCreationForm:
# hence cannot be created. # hence cannot be created.
form = UserAdminCreationForm( form = UserAdminCreationForm(
{ {
{%- if cookiecutter.username_type == "email" %}
"email": user.email,
{%- else %}
"username": user.username, "username": user.username,
{%- endif %}
"password1": user.password, "password1": user.password,
"password2": user.password, "password2": user.password,
} }
@ -32,5 +36,10 @@ class TestUserAdminCreationForm:
assert not form.is_valid() assert not form.is_valid()
assert len(form.errors) == 1 assert len(form.errors) == 1
{%- if cookiecutter.username_type == "email" %}
assert "email" in form.errors
assert form.errors["email"][0] == _("This email has already been taken.")
{%- else %}
assert "username" in form.errors assert "username" in form.errors
assert form.errors["username"][0] == _("This username has already been taken.") assert form.errors["username"][0] == _("This username has already been taken.")
{%- endif %}

View File

@ -0,0 +1,55 @@
from io import StringIO
import pytest
from django.core.management import call_command
from {{ cookiecutter.project_slug }}.users.models import User
@pytest.mark.django_db
class TestUserManager:
def test_create_user(self):
user = User.objects.create_user(
email="john@example.com",
password="something-r@nd0m!",
)
assert user.email == "john@example.com"
assert not user.is_staff
assert not user.is_superuser
assert user.check_password("something-r@nd0m!")
assert user.username is None
def test_create_superuser(self):
user = User.objects.create_superuser(
email="admin@example.com",
password="something-r@nd0m!",
)
assert user.email == "admin@example.com"
assert user.is_staff
assert user.is_superuser
assert user.username is None
def test_create_superuser_username_ignored(self):
user = User.objects.create_superuser(
email="test@example.com",
password="something-r@nd0m!",
)
assert user.username is None
@pytest.mark.django_db
def test_createsuperuser_command():
"""Ensure createsuperuser command works with our custom manager."""
out = StringIO()
command_result = call_command(
"createsuperuser",
"--email",
"henry@example.com",
interactive=False,
stdout=out,
)
assert command_result is None
assert out.getvalue() == "Superuser created successfully.\n"
user = User.objects.get(email="henry@example.com")
assert not user.has_usable_password()

View File

@ -2,4 +2,8 @@ from {{ cookiecutter.project_slug }}.users.models import User
def test_user_get_absolute_url(user: User): def test_user_get_absolute_url(user: User):
{%- if cookiecutter.username_type == "email" %}
assert user.get_absolute_url() == f"/users/{user.pk}/"
{%- else %}
assert user.get_absolute_url() == f"/users/{user.username}/" assert user.get_absolute_url() == f"/users/{user.username}/"
{%- endif %}

View File

@ -4,11 +4,16 @@ from {{ cookiecutter.project_slug }}.users.models import User
def test_detail(user: User): def test_detail(user: User):
{%- if cookiecutter.username_type == "email" %}
assert reverse("users:detail", kwargs={"pk": user.pk}) == f"/users/{user.pk}/"
assert resolve(f"/users/{user.pk}/").view_name == "users:detail"
{%- else %}
assert ( assert (
reverse("users:detail", kwargs={"username": user.username}) reverse("users:detail", kwargs={"username": user.username})
== f"/users/{user.username}/" == f"/users/{user.username}/"
) )
assert resolve(f"/users/{user.username}/").view_name == "users:detail" assert resolve(f"/users/{user.username}/").view_name == "users:detail"
{%- endif %}
def test_update(): def test_update():

View File

@ -39,7 +39,11 @@ class TestUserUpdateView:
view.request = request view.request = request
{%- if cookiecutter.username_type == "email" %}
assert view.get_success_url() == f"/users/{user.pk}/"
{%- else %}
assert view.get_success_url() == f"/users/{user.username}/" assert view.get_success_url() == f"/users/{user.username}/"
{%- endif %}
def test_get_object(self, user: User, rf: RequestFactory): def test_get_object(self, user: User, rf: RequestFactory):
view = UserUpdateView() view = UserUpdateView()
@ -79,7 +83,11 @@ class TestUserRedirectView:
view.request = request view.request = request
{%- if cookiecutter.username_type == "email" %}
assert view.get_redirect_url() == f"/users/{user.pk}/"
{%- else %}
assert view.get_redirect_url() == f"/users/{user.username}/" assert view.get_redirect_url() == f"/users/{user.username}/"
{%- endif %}
class TestUserDetailView: class TestUserDetailView:
@ -87,7 +95,11 @@ class TestUserDetailView:
request = rf.get("/fake-url/") request = rf.get("/fake-url/")
request.user = UserFactory() request.user = UserFactory()
{%- if cookiecutter.username_type == "email" %}
response = user_detail_view(request, pk=user.pk)
{%- else %}
response = user_detail_view(request, username=user.username) response = user_detail_view(request, username=user.username)
{%- endif %}
assert response.status_code == 200 assert response.status_code == 200
@ -95,7 +107,11 @@ class TestUserDetailView:
request = rf.get("/fake-url/") request = rf.get("/fake-url/")
request.user = AnonymousUser() request.user = AnonymousUser()
{%- if cookiecutter.username_type == "email" %}
response = user_detail_view(request, pk=user.pk)
{%- else %}
response = user_detail_view(request, username=user.username) response = user_detail_view(request, username=user.username)
{%- endif %}
login_url = reverse(settings.LOGIN_URL) login_url = reverse(settings.LOGIN_URL)
assert isinstance(response, HttpResponseRedirect) assert isinstance(response, HttpResponseRedirect)

View File

@ -10,5 +10,9 @@ app_name = "users"
urlpatterns = [ urlpatterns = [
path("~redirect/", view=user_redirect_view, name="redirect"), path("~redirect/", view=user_redirect_view, name="redirect"),
path("~update/", view=user_update_view, name="update"), path("~update/", view=user_update_view, name="update"),
{%- if cookiecutter.username_type == "email" %}
path("<int:pk>/", view=user_detail_view, name="detail"),
{%- else %}
path("<str:username>/", view=user_detail_view, name="detail"), path("<str:username>/", view=user_detail_view, name="detail"),
{%- endif %}
] ]

View File

@ -10,8 +10,13 @@ User = get_user_model()
class UserDetailView(LoginRequiredMixin, DetailView): class UserDetailView(LoginRequiredMixin, DetailView):
model = User model = User
{%- if cookiecutter.username_type == "email" %}
slug_field = "id"
slug_url_kwarg = "id"
{%- else %}
slug_field = "username" slug_field = "username"
slug_url_kwarg = "username" slug_url_kwarg = "username"
{%- endif %}
user_detail_view = UserDetailView.as_view() user_detail_view = UserDetailView.as_view()
@ -39,7 +44,11 @@ class UserRedirectView(LoginRequiredMixin, RedirectView):
permanent = False permanent = False
def get_redirect_url(self): def get_redirect_url(self):
{%- if cookiecutter.username_type == "email" %}
return reverse("users:detail", kwargs={"pk": self.request.user.pk})
{%- else %}
return reverse("users:detail", kwargs={"username": self.request.user.username}) return reverse("users:detail", kwargs={"username": self.request.user.username})
{%- endif %}
user_redirect_view = UserRedirectView.as_view() user_redirect_view = UserRedirectView.as_view()