Uograde CC 240913

This commit is contained in:
Alejandro Franco 2024-09-13 16:34:02 -06:00
parent 50946eedbd
commit 7a07ca17a7
23 changed files with 197 additions and 61 deletions

View File

@ -74,8 +74,8 @@ jobs:
args: "frontend_pipeline=Gulp" args: "frontend_pipeline=Gulp"
- name: Webpack - name: Webpack
args: "frontend_pipeline=Webpack use_heroku=y" args: "frontend_pipeline=Webpack use_heroku=y"
- name: Email - name: Email Username
args: "ci_tool=Github project_name='Something superduper long - the great amazing project' project_slug=my_awesome_project" args: "username_type=email ci_tool=Github project_name='Something superduper long - the great amazing project' project_slug=my_awesome_project"
name: "Bare metal ${{ matrix.script.name }}" name: "Bare metal ${{ matrix.script.name }}"
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -116,6 +116,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
Select an editor to use. The choices are: Select an editor to use. The choices are:

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",
"editor": ["None", "PyCharm", "VS Code"], "editor": ["None", "PyCharm", "VS Code"],
@ -38,7 +39,6 @@
"use_sentry": "n", "use_sentry": "n",
"use_whitenoise": "n", "use_whitenoise": "n",
"use_heroku": "n", "use_heroku": "n",
"username_type": ["email"],
"ci_tool": ["None", "Travis", "Gitlab", "Github", "Drone"], "ci_tool": ["None", "Travis", "Gitlab", "Github", "Drone"],
"keep_local_envs_in_vcs": "y", "keep_local_envs_in_vcs": "y",
"debug": "n" "debug": "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

@ -449,8 +449,8 @@ 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": if "{{ cookiecutter.username_type }}" == "username":
# remove_custom_user_manager_files() remove_custom_user_manager_files()
if "{{ cookiecutter.editor }}" != "PyCharm": if "{{ cookiecutter.editor }}" != "PyCharm":
remove_pycharm_files() remove_pycharm_files()

View File

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

View File

@ -7,8 +7,7 @@ set -o nounset
{% if cookiecutter.use_celery == 'y' %} {% if cookiecutter.use_celery == 'y' %}
# N.B. If only .env files supported variable expansion... # N.B. If only .env files supported variable expansion...
export CELERY_BROKER_URL="${REDIS_URL}" export CELERY_BROKER_URL="${REDIS_URL}"
# Updated in .envs
{% endif %} {% endif %}
if [ -z "${POSTGRES_USER}" ]; then if [ -z "${POSTGRES_USER}" ]; then
@ -16,7 +15,6 @@ if [ -z "${POSTGRES_USER}" ]; then
export POSTGRES_USER="${base_postgres_image_default_user}" export POSTGRES_USER="${base_postgres_image_default_user}"
fi fi
export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
# Updated in .envs
python << END python << END
import sys import sys

View File

@ -325,13 +325,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://docs.allauth.org/en/latest/account/configuration.html # https://docs.allauth.org/en/latest/account/configuration.html
ACCOUNT_AUTHENTICATION_METHOD = "email" ACCOUNT_AUTHENTICATION_METHOD = "{{cookiecutter.username_type}}"
# https://docs.allauth.org/en/latest/account/configuration.html # https://docs.allauth.org/en/latest/account/configuration.html
ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_REQUIRED = True
{%- if cookiecutter.username_type == "email" %}
# https://docs.allauth.org/en/latest/account/configuration.html # https://docs.allauth.org/en/latest/account/configuration.html
ACCOUNT_USERNAME_REQUIRED = False ACCOUNT_USERNAME_REQUIRED = False
# https://docs.allauth.org/en/latest/account/configuration.html # https://docs.allauth.org/en/latest/account/configuration.html
ACCOUNT_USER_MODEL_USERNAME_FIELD = None ACCOUNT_USER_MODEL_USERNAME_FIELD = None
{%- endif %}
# https://docs.allauth.org/en/latest/account/configuration.html # https://docs.allauth.org/en/latest/account/configuration.html
ACCOUNT_EMAIL_VERIFICATION = "mandatory" ACCOUNT_EMAIL_VERIFICATION = "mandatory"
# https://docs.allauth.org/en/latest/account/configuration.html # https://docs.allauth.org/en/latest/account/configuration.html

View File

@ -41,12 +41,12 @@ django_settings_module = "config.settings.test"
# ==== djLint ==== # ==== djLint ====
[tool.djlint] [tool.djlint]
blank_line_after_tag = "load,extends,endblock" blank_line_after_tag = "load,extends"
close_void_tags = true close_void_tags = true
format_css = false format_css = true
format_js = false format_js = true
# TODO: remove T002 when fixed https://github.com/djlint/djLint/issues/687 # TODO: remove T002 when fixed https://github.com/djlint/djLint/issues/687
ignore = "H006,H030,H031,T002,H020,H023,H033,D018" ignore = "H006,H030,H031,T002"
include = "H017,H035" include = "H017,H035"
indent = 2 indent = 2
max_line_length = 119 max_line_length = 119
@ -65,10 +65,6 @@ extend-exclude = [
"*/migrations/*.py", "*/migrations/*.py",
"staticfiles/*", "staticfiles/*",
] ]
# Same as Django: https://github.com/cookiecutter/cookiecutter-django/issues/4792.
line-length = 88
indent-width = 4
target-version = "py312"
[tool.ruff.lint] [tool.ruff.lint]
select = [ select = [

View File

@ -52,5 +52,5 @@ django-webpack-loader==3.1.1 # https://github.com/django-webpack/django-webpack
# Project # Project
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
fontawesomefree==6.5.2 # https://github.com/FortAwesome/Font-Awesome fontawesomefree==6.6.0 # https://github.com/FortAwesome/Font-Awesome
django-import-export==4.0.8 # https://github.com/django-import-export/django-import-export django-import-export==4.1.1 # https://github.com/django-import-export/django-import-export

View File

@ -4,7 +4,11 @@
{% block title %} {% block title %}
User: {% endraw %} User: {% endraw %}
{% if cookiecutter.username_type == "email" %}
{% raw %}{{ object.first_name }}{% endraw %} {% raw %}{{ object.first_name }}{% endraw %}
{% else %}
{% raw %}{{ object.username }}{% endraw %}
{% endif %}
{% raw %} {% raw %}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
@ -13,11 +17,20 @@
<div class="col-sm-12"> <div class="col-sm-12">
<h2> <h2>
{% endraw %} {% endraw %}
{% if cookiecutter.username_type == "email" %}
{% raw %}{{ object.first_name }}{% endraw %} {% raw %}{{ object.first_name }}{% endraw %}
{% else %}
{% raw %}{{ object.username }}{% endraw %}
{% endif %}
</h2> </h2>
{%- if cookiecutter.username_type == "username" %}
{%- raw %}
<p>{{ object.first_name }}</p>
{%- endraw %}
{%- endif %}
</div> </div>
</div> </div>
{% raw %} {%- raw %}
{% if object == request.user %} {% if object == request.user %}
<!-- Action buttons --> <!-- Action buttons -->
<div class="row"> <div class="row">
@ -36,4 +49,4 @@
{% endif %} {% endif %}
</div> </div>
{% endblock content %} {% endblock content %}
{% endraw %} {%- endraw %}

View File

@ -2,11 +2,28 @@
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title %}{{ user }}{% endblock title %} {% block title %}
{% endraw %}
{% if cookiecutter.username_type == "email" %}
{% raw %}{{ user.first_name }}{% endraw %}
{% else %}
{% raw %}{{ user.username }}{% endraw %}
{% endif %}
{% raw %}
{% endblock title %}
{% block content %} {% block content %}
<h1>{{ user }}</h1> <h1>
<form class="form-horizontal" method="post" action="{% url 'users:update' %}"> {% endraw %}
{% if cookiecutter.username_type == "email" %}
{% raw %}{{ user.first_name }}{% endraw %}
{% else %}
{% raw %}{{ user.username }}{% endraw %}
{% endif %}
{% raw %}
</h1>
<form class="form-horizontal"
method="post"
action="{% url 'users:update' %}">
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<div class="control-group"> <div class="control-group">
@ -14,6 +31,6 @@
<button type="submit" class="btn btn-primary">Update</button> <button type="submit" class="btn btn-primary">Update</button>
</div> </div>
</div> </div>
</form> </form>
{% endblock content %} {% endblock content %}
{%- endraw %} {%- endraw %}

View File

@ -55,10 +55,10 @@ class SocialAccountAdapter(DefaultSocialAccountAdapter):
if request.user.is_authenticated and request.user.email != verified_email.email: if request.user.is_authenticated and request.user.email != verified_email.email:
messages.error( messages.error(
request, request,
""" (
No es posible enlazar tu cuenta de {}, "No es posible enlazar tu cuenta de {}, "
ya que no coincide con tu correo en esta plataforma. "ya que no coincide con tu correo en esta plataforma."
""".format( ).format(
list(request._socialapp_cache.keys())[0].capitalize() list(request._socialapp_cache.keys())[0].capitalize()
), ),
) )

View File

@ -21,9 +21,13 @@ class UserAdmin(auth_admin.UserAdmin):
add_form = UserAdminCreationForm add_form = UserAdminCreationForm
readonly_fields = ("uuid",) readonly_fields = ("uuid",)
fieldsets = ( fieldsets = (
{%- if cookiecutter.username_type == "email" %}
(None, {"fields": ("email", "password")}), (None, {"fields": ("email", "password")}),
(_("Personal info"), {"fields": ("first_name", "last_name")}), (_("Personal info"), {"fields": ("first_name", "last_name")}),
(None, {"fields": ("uuid",)}), {%- else %}
(None, {"fields": ("username", "password")}),
(_("Personal info"), {"fields": ("first_name", "last_name", "email")}),
{%- endif %}
( (
_("Permissions"), _("Permissions"),
{ {
@ -39,15 +43,16 @@ class UserAdmin(auth_admin.UserAdmin):
(_("Important dates"), {"fields": ("last_login", "date_joined")}), (_("Important dates"), {"fields": ("last_login", "date_joined")}),
) )
list_display = [ list_display = [
"email", "{{cookiecutter.username_type}}",
"first_name", "first_name",
"last_name", "last_name",
"is_active", "is_active",
"is_staff", "is_staff",
"is_superuser", "is_superuser",
] ]
search_fields = ["email", "first_name", "last_name"] search_fields = ["{{cookiecutter.username_type}}", "first_name", "last_name"]
ordering = ("email", "first_name", "last_name") ordering = ("{{cookiecutter.username_type}}", "first_name", "last_name")
{%- if cookiecutter.username_type == "email" %}
add_fieldsets = ( add_fieldsets = (
( (
None, None,
@ -63,3 +68,21 @@ class UserAdmin(auth_admin.UserAdmin):
}, },
), ),
) )
{%- else %}
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"username",
"first_name",
"last_name",
"email",
"password1",
"password2",
),
},
),
)
{%- endif %}

View File

@ -6,5 +6,11 @@ from {{ cookiecutter.project_slug }}.users.models import User
class UserSerializer(serializers.ModelSerializer[User]): class UserSerializer(serializers.ModelSerializer[User]):
class Meta: class Meta:
model = User model = User
{%- if cookiecutter.username_type == "email" %}
fields = ["email", "uuid", "first_name", "last_name", "url"] fields = ["email", "uuid", "first_name", "last_name", "url"]
extra_kwargs = {"url": {"view_name": "api:user-detail", "lookup_field": "uuid"}} {%- else %}
fields = ["username", "email", "uuid", "first_name", "last_name", "url"]
{%- endif %}
extra_kwargs = {
"url": {"view_name": "api:user-detail", "lookup_field": "uuid"}
}

View File

@ -1,8 +1,9 @@
from allauth.account.forms import SignupForm from allauth.account.forms import SignupForm
from allauth.socialaccount.forms import SignupForm as SocialSignupForm from allauth.socialaccount.forms import SignupForm as SocialSignupForm
from django import forms
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 {%- 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()
@ -11,7 +12,9 @@ User = get_user_model()
class UserAdminChangeForm(admin_forms.UserChangeForm): class UserAdminChangeForm(admin_forms.UserChangeForm):
class Meta(admin_forms.UserChangeForm.Meta): # type: ignore[name-defined] class Meta(admin_forms.UserChangeForm.Meta): # type: ignore[name-defined]
model = User model = User
fields = ("email", "first_name", "last_name") {%- if cookiecutter.username_type == "email" %}
field_classes = {"email": EmailField}
{%- endif %}
class UserAdminCreationForm(admin_forms.UserCreationForm): class UserAdminCreationForm(admin_forms.UserCreationForm):
@ -22,14 +25,18 @@ class UserAdminCreationForm(admin_forms.UserCreationForm):
class Meta(admin_forms.UserCreationForm.Meta): # type: ignore[name-defined] class Meta(admin_forms.UserCreationForm.Meta): # type: ignore[name-defined]
model = User model = User
{%- if cookiecutter.username_type == "email" %}
fields = ("email", "first_name", "last_name") fields = ("email", "first_name", "last_name")
field_classes = {"email": EmailField}
error_messages = { error_messages = {
"email": {"unique": _("This email has already been taken.")} "email": {"unique": _("This email has already been taken.")},
} }
{%- else %}
fields = ("username", "first_name", "last_name")
error_messages = { error_messages = {
"username": {"unique": _("This username has already been taken.")}, "username": {"unique": _("This username has already been taken.")},
} }
{%- endif %}
class UserSignupForm(SignupForm): class UserSignupForm(SignupForm):

View File

@ -44,6 +44,36 @@ class Migration(migrations.Migration):
verbose_name="superuser status", verbose_name="superuser status",
), ),
), ),
{%- if cookiecutter.username_type == "username" -%}
(
"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",
),
),
(
"email",
models.EmailField(
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(
@ -66,12 +96,6 @@ class Migration(migrations.Migration):
default=django.utils.timezone.now, verbose_name="date joined", default=django.utils.timezone.now, verbose_name="date joined",
), ),
), ),
(
"email",
models.EmailField(
max_length=254, unique=True, verbose_name="email address"
),
),
( (
"uuid", "uuid",
models.UUIDField( models.UUIDField(
@ -115,7 +139,11 @@ class Migration(migrations.Migration):
"abstract": False, "abstract": False,
}, },
managers=[ managers=[
('objects', {{ cookiecutter.project_slug }}.users.models.UserManager()), {%- if cookiecutter.username_type == "email" %}
("objects", {{cookiecutter.project_slug}}.users.models.UserManager()),
{%- else %}
("objects", django.contrib.auth.models.UserManager()),
{%- endif %}
], ],
), ),
] ]

View File

@ -1,14 +1,19 @@
import uuid as uuid_lib import uuid as uuid_lib
{%- if cookiecutter.username_type == "email" %}
from typing import ClassVar from typing import ClassVar
from django.contrib.auth.base_user import BaseUserManager {% endif -%}
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db.models import CharField
{%- if cookiecutter.username_type == "email" %}
from django.db.models import 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 .managers import UserManager from .managers import UserManager
{%- endif %}
class User(AbstractUser): class User(AbstractUser):
@ -18,22 +23,23 @@ class User(AbstractUser):
check forms.SignupForm and forms.SocialSignupForms accordingly. check forms.SignupForm and forms.SocialSignupForms accordingly.
""" """
email = models.EmailField(_("email address"), unique=True)
username = None
uuid = models.UUIDField( uuid = models.UUIDField(
unique=True, db_index=True, default=uuid_lib.uuid4, editable=False unique=True, db_index=True, default=uuid_lib.uuid4, editable=False
) )
first_name = models.CharField(_("first name"), max_length=150) first_name = models.CharField(_("first name"), max_length=150)
last_name = models.CharField(_("last name"), max_length=150) last_name = models.CharField(_("last name"), max_length=150)
{%- if cookiecutter.username_type == "email" %}
email = EmailField(_("email address"), unique=True)
username = None # type: ignore[assignment]
USERNAME_FIELD = "email" USERNAME_FIELD = "email"
REQUIRED_FIELDS = [ REQUIRED_FIELDS = [
"first_name", "first_name",
"last_name", "last_name",
] ]
objects: ClassVar[UserManager] = UserManager() objects: ClassVar[UserManager] = UserManager()
{%- endif %}
def get_absolute_url(self) -> str: def get_absolute_url(self) -> str:
"""Get URL for user's detail view. """Get URL for user's detail view.
@ -45,7 +51,11 @@ class User(AbstractUser):
return reverse("users:detail", kwargs={"uuid": self.uuid}) return reverse("users:detail", kwargs={"uuid": self.uuid})
def __str__(self): def __str__(self):
{ % - if cookiecutter.username_type == "email" %}
return f"{self.first_name} {self.last_name}" return f"{self.first_name} {self.last_name}"
{ % - else %}
return f"{self.username}"
{ % - endif %}
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.first_name = self.first_name.strip() self.first_name = self.first_name.strip()

View File

@ -32,16 +32,28 @@ class TestUserAdmin:
data={ data={
"first_name": "New", "first_name": "New",
"last_name": "Admin", "last_name": "Admin",
{%- if cookiecutter.username_type == "email" %}
"email": "new-admin@example.com", "email": "new-admin@example.com",
{%- else %}
"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 == HTTPStatus.FOUND assert response.status_code == HTTPStatus.FOUND
{%- if cookiecutter.username_type == "email" %}
assert User.objects.filter(email="new-admin@example.com").exists() assert User.objects.filter(email="new-admin@example.com").exists()
{%- else %}
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") user = User.objects.get(email="admin@example.com")
{%- else %}
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 == HTTPStatus.OK assert response.status_code == HTTPStatus.OK

View File

@ -29,6 +29,9 @@ class TestUserViewSet:
response = view.me(request) # type: ignore[call-arg, arg-type, misc] response = view.me(request) # type: ignore[call-arg, arg-type, misc]
assert response.data == { assert response.data == {
{%- if cookiecutter.username_type == "username" %}
"username": user.username,
{%- endif %}
"email": user.email, "email": user.email,
"uuid": str(user.uuid), "uuid": str(user.uuid),
"first_name": user.first_name, "first_name": user.first_name,

View File

@ -23,7 +23,11 @@ class TestUserAdminCreationForm:
# hence cannot be created. # hence cannot be created.
form = UserAdminCreationForm( form = UserAdminCreationForm(
{ {
{%- if cookiecutter.username_type == "email" %}
"email": user.email, "email": user.email,
{%- else %}
"username": user.username,
{%- endif %}
"first_name": user.first_name, "first_name": user.first_name,
"last_name": user.last_name, "last_name": user.last_name,
"password1": user.password, "password1": user.password,
@ -33,5 +37,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 "email" in form.errors
assert form.errors["email"][0] == _("This email has already been taken.") assert form.errors["email"][0] == _("This email has already been taken.")
{%- else %}
assert "username" in form.errors
assert form.errors["username"][0] == _("This username has already been taken.")
{%- endif %}