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"
- name: Webpack
args: "frontend_pipeline=Webpack use_heroku=y"
- name: Email
args: "ci_tool=Github project_name='Something superduper long - the great amazing project' project_slug=my_awesome_project"
- name: Email Username
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 }}"
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
5 - Not open source
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
windows [n]: n
Select an editor to use. The choices are:

View File

@ -13,6 +13,7 @@
"Apache Software License 2.0",
"Not open source"
],
"username_type": ["username", "email"],
"timezone": "UTC",
"windows": "n",
"editor": ["None", "PyCharm", "VS Code"],
@ -38,7 +39,6 @@
"use_sentry": "n",
"use_whitenoise": "n",
"use_heroku": "n",
"username_type": ["email"],
"ci_tool": ["None", "Travis", "Gitlab", "Github", "Drone"],
"keep_local_envs_in_vcs": "y",
"debug": "n"

View File

@ -24,6 +24,13 @@ author_name:
email:
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:
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.

View File

@ -449,8 +449,8 @@ def main():
if "{{ cookiecutter.open_source_license}}" != "GPLv3":
remove_gplv3_files()
# if "{{ cookiecutter.username_type }}" == "username":
# remove_custom_user_manager_files()
if "{{ cookiecutter.username_type }}" == "username":
remove_custom_user_manager_files()
if "{{ cookiecutter.editor }}" != "PyCharm":
remove_pycharm_files()

View File

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

View File

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

View File

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

View File

@ -41,12 +41,12 @@ django_settings_module = "config.settings.test"
# ==== djLint ====
[tool.djlint]
blank_line_after_tag = "load,extends,endblock"
blank_line_after_tag = "load,extends"
close_void_tags = true
format_css = false
format_js = false
format_css = true
format_js = true
# 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"
indent = 2
max_line_length = 119
@ -65,10 +65,6 @@ extend-exclude = [
"*/migrations/*.py",
"staticfiles/*",
]
# Same as Django: https://github.com/cookiecutter/cookiecutter-django/issues/4792.
line-length = 88
indent-width = 4
target-version = "py312"
[tool.ruff.lint]
select = [

View File

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

View File

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

View File

@ -2,11 +2,28 @@
{% 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 %}
<h1>{{ user }}</h1>
<form class="form-horizontal" method="post" action="{% url 'users:update' %}">
<h1>
{% 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 %}
{{ form|crispy }}
<div class="control-group">
@ -14,6 +31,6 @@
<button type="submit" class="btn btn-primary">Update</button>
</div>
</div>
</form>
</form>
{% endblock content %}
{%- endraw %}

View File

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

View File

@ -21,9 +21,13 @@ class UserAdmin(auth_admin.UserAdmin):
add_form = UserAdminCreationForm
readonly_fields = ("uuid",)
fieldsets = (
{%- if cookiecutter.username_type == "email" %}
(None, {"fields": ("email", "password")}),
(_("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"),
{
@ -39,15 +43,16 @@ class UserAdmin(auth_admin.UserAdmin):
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
)
list_display = [
"email",
"{{cookiecutter.username_type}}",
"first_name",
"last_name",
"is_active",
"is_staff",
"is_superuser",
]
search_fields = ["email", "first_name", "last_name"]
ordering = ("email", "first_name", "last_name")
search_fields = ["{{cookiecutter.username_type}}", "first_name", "last_name"]
ordering = ("{{cookiecutter.username_type}}", "first_name", "last_name")
{%- if cookiecutter.username_type == "email" %}
add_fieldsets = (
(
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 Meta:
model = User
{%- if cookiecutter.username_type == "email" %}
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.socialaccount.forms import SignupForm as SocialSignupForm
from django import 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 _
User = get_user_model()
@ -11,7 +12,9 @@ User = get_user_model()
class UserAdminChangeForm(admin_forms.UserChangeForm):
class Meta(admin_forms.UserChangeForm.Meta): # type: ignore[name-defined]
model = User
fields = ("email", "first_name", "last_name")
{%- if cookiecutter.username_type == "email" %}
field_classes = {"email": EmailField}
{%- endif %}
class UserAdminCreationForm(admin_forms.UserCreationForm):
@ -22,14 +25,18 @@ class UserAdminCreationForm(admin_forms.UserCreationForm):
class Meta(admin_forms.UserCreationForm.Meta): # type: ignore[name-defined]
model = User
{%- if cookiecutter.username_type == "email" %}
fields = ("email", "first_name", "last_name")
field_classes = {"email": EmailField}
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 = {
"username": {"unique": _("This username has already been taken.")},
}
{%- endif %}
class UserSignupForm(SignupForm):

View File

@ -44,6 +44,36 @@ class Migration(migrations.Migration):
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",
models.BooleanField(
@ -66,12 +96,6 @@ class Migration(migrations.Migration):
default=django.utils.timezone.now, verbose_name="date joined",
),
),
(
"email",
models.EmailField(
max_length=254, unique=True, verbose_name="email address"
),
),
(
"uuid",
models.UUIDField(
@ -115,7 +139,11 @@ class Migration(migrations.Migration):
"abstract": False,
},
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
{%- if cookiecutter.username_type == "email" %}
from typing import ClassVar
from django.contrib.auth.base_user import BaseUserManager
{% endif -%}
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.utils.translation import gettext_lazy as _
{%- if cookiecutter.username_type == "email" %}
from .managers import UserManager
{%- endif %}
class User(AbstractUser):
@ -18,22 +23,23 @@ class User(AbstractUser):
check forms.SignupForm and forms.SocialSignupForms accordingly.
"""
email = models.EmailField(_("email address"), unique=True)
username = None
uuid = models.UUIDField(
unique=True, db_index=True, default=uuid_lib.uuid4, editable=False
)
first_name = models.CharField(_("first 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"
REQUIRED_FIELDS = [
"first_name",
"last_name",
]
objects: ClassVar[UserManager] = UserManager()
{%- endif %}
def get_absolute_url(self) -> str:
"""Get URL for user's detail view.
@ -45,7 +51,11 @@ class User(AbstractUser):
return reverse("users:detail", kwargs={"uuid": self.uuid})
def __str__(self):
{ % - if cookiecutter.username_type == "email" %}
return f"{self.first_name} {self.last_name}"
{ % - else %}
return f"{self.username}"
{ % - endif %}
def save(self, *args, **kwargs):
self.first_name = self.first_name.strip()

View File

@ -32,16 +32,28 @@ class TestUserAdmin:
data={
"first_name": "New",
"last_name": "Admin",
{%- if cookiecutter.username_type == "email" %}
"email": "new-admin@example.com",
{%- else %}
"username": "test",
{%- endif %}
"password1": "My_R@ndom-P@ssw0rd",
"password2": "My_R@ndom-P@ssw0rd",
},
)
assert response.status_code == HTTPStatus.FOUND
{%- if cookiecutter.username_type == "email" %}
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):
{%- if cookiecutter.username_type == "email" %}
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})
response = admin_client.get(url)
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]
assert response.data == {
{%- if cookiecutter.username_type == "username" %}
"username": user.username,
{%- endif %}
"email": user.email,
"uuid": str(user.uuid),
"first_name": user.first_name,

View File

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