Add username_type option

This commit is contained in:
Andrew-Chen-Wang 2022-11-22 13:35:35 -05:00
parent c59e3b337f
commit a3f03ccdc0
21 changed files with 153 additions and 14 deletions

View File

@ -5,6 +5,7 @@
"author_name": "Daniel Roy Greenfeld", "author_name": "Daniel Roy Greenfeld",
"domain_name": "example.com", "domain_name": "example.com",
"email": "{{ cookiecutter.author_name.lower()|replace(' ', '-') }}@example.com", "email": "{{ cookiecutter.author_name.lower()|replace(' ', '-') }}@example.com",
"username_type": ["username", "email"],
"version": "0.1.0", "version": "0.1.0",
"open_source_license": [ "open_source_license": [
"MIT", "MIT",

View File

@ -305,7 +305,7 @@ CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
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
# https://django-allauth.readthedocs.io/en/latest/configuration.html # https://django-allauth.readthedocs.io/en/latest/configuration.html

View File

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

@ -14,8 +14,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"),
{ {
@ -30,5 +35,5 @@ 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"]

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": "id"}
}
{%- 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 = "id"
{%- 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,7 +2,6 @@ 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
from django.utils.translation import gettext_lazy as _
User = get_user_model() User = get_user_model()
@ -21,10 +20,6 @@ class UserAdminCreationForm(admin_forms.UserCreationForm):
class Meta(admin_forms.UserCreationForm.Meta): class Meta(admin_forms.UserCreationForm.Meta):
model = User model = User
error_messages = {
"username": {"unique": _("This username has already been taken.")}
}
class UserSignupForm(SignupForm): class UserSignupForm(SignupForm):
""" """

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):
@ -118,7 +120,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,9 +1,45 @@
{% if cookiecutter.username_type == "email" -%}
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractUser, UserManager as DjangoUserManager
{%- else %}
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
{%- endif %}
from django.db.models import CharField from django.db.models import CharField
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 _
class UserManager(DjangoUserManager):
def _create_user(self, email, password, **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, password=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, password=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)
class User(AbstractUser): class User(AbstractUser):
""" """
Default custom user model for {{cookiecutter.project_name}}. Default custom user model for {{cookiecutter.project_name}}.
@ -13,9 +49,16 @@ class User(AbstractUser):
#: 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)
{% if cookiecutter.username_type == "email" -%}
username = None # type: ignore
{%- endif %}
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" -%}
USERNAME_FIELD = "email"
{%- endif %}
def get_absolute_url(self): def get_absolute_url(self):
"""Get url for user's detail view. """Get url for user's detail view.
@ -23,4 +66,8 @@ class User(AbstractUser):
str: URL for user detail. str: URL for user detail.
""" """
{% if cookiecutter.username_type == "email" -%}
return reverse("users:detail", kwargs={"id": self.id})
{%- else %}
return reverse("users:detail", kwargs={"username": self.username}) return reverse("users:detail", kwargs={"username": self.username})
{%- endif %}

View File

@ -8,7 +8,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")
@ -30,4 +32,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,13 +22,21 @@ class TestUserAdmin:
response = admin_client.post( response = admin_client.post(
url, url,
data={ data={
{% if cookiecutter.username_type == "email" -%}
"email": "test@test.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="test@test.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):
user = User.objects.get(username="admin") user = User.objects.get(username="admin")

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

@ -24,7 +24,12 @@ class TestUserViewSet:
response = view.me(request) response = view.me(request)
assert response.data == { assert response.data == {
{% if cookiecutter.username_type == "email" -%}
"email": user.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

@ -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,19 @@ 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:id>/", 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

@ -11,8 +11,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()
@ -42,7 +47,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()