feat: added delete account and new display name
CI / test (push) Successful in 1m28s
CI / build (push) Successful in 29s

This commit is contained in:
2026-07-22 10:03:30 +02:00
parent 2839d60923
commit 1a1c8786a3
18 changed files with 630 additions and 160 deletions
+1 -1
View File
@@ -99,7 +99,7 @@ DATABASES = {
# https://docs.djangoproject.com/en/6.0/topics/auth/default/#all-authentication-views
LOGIN_URL = 'crochet:login'
LOGIN_REDIRECT_URL = 'crochet:account_home'
LOGIN_REDIRECT_URL = 'crochet:home'
LOGOUT_REDIRECT_URL = 'crochet:login'
+48
View File
@@ -116,3 +116,51 @@ class EmailUpdateForm(forms.Form):
'crochet/email/email_changed_notice.html',
context, old_email,
)
class DisplayNameForm(forms.Form):
# Se guarda en el propio first_name de auth.User (no se usa para nada
# más en la app): no hace falta un modelo de perfil aparte solo para un
# campo de texto opcional. Se propone como autor por defecto al crear
# un patrón nuevo (ver PatternCreateView) y se muestra en el navbar/
# saludo de "Mis patrones" en vez del username, si está puesto.
display_name = forms.CharField(
label=_('Nombre para mostrar'), required=False, max_length=150,
widget=forms.TextInput(attrs={'class': INPUT_CLASSES}),
)
def __init__(self, *args, user, **kwargs):
self.user = user
super().__init__(*args, **kwargs)
def save(self):
self.user.first_name = self.cleaned_data['display_name']
self.user.save(update_fields=['first_name'])
class AccountDeleteForm(forms.Form):
password = forms.CharField(
label=_('Contraseña'), widget=forms.PasswordInput(attrs={'class': INPUT_CLASSES}),
)
def __init__(self, *args, user, **kwargs):
self.user = user
super().__init__(*args, **kwargs)
def clean_password(self):
password = self.cleaned_data['password']
if not self.user.check_password(password):
raise forms.ValidationError(_('Contraseña incorrecta.'))
return password
def notify_account_deleted(self):
# Antes de borrar la cuenta (ver AccountDeleteView), mientras el
# email todavía existe. Sin email no hay a quién avisar.
if self.user.email:
context = {'user': self.user, 'site_name': 'Crochet', 'support_email': settings.SUPPORT_EMAIL}
_send_account_notice(
'crochet/email/account_deleted_subject.txt',
'crochet/email/account_deleted_notice.txt',
'crochet/email/account_deleted_notice.html',
context, self.user.email,
)
+115 -53
View File
@@ -2,17 +2,50 @@ msgid ""
msgstr ""
"Project-Id-Version: crochet\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-21 12:32+0000\n"
"POT-Creation-Date: 2026-07-22 08:00+0000\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: crochet/forms.py:48 crochet/forms.py:96
#: crochet/templates/crochet/account_settings.html:22
#: crochet/templates/crochet/account_settings.html:29
msgid "Email"
msgstr "Email"
#: crochet/forms.py:128 crochet/templates/crochet/account_settings.html:22
msgid "Nombre para mostrar"
msgstr "Display name"
#: crochet/forms.py:143
msgid "Contraseña"
msgstr "Password"
#: crochet/forms.py:153
msgid "Contraseña incorrecta."
msgstr "Incorrect password."
#: crochet/templates/crochet/_account_delete_form.html:9
msgid ""
"¿Seguro que quieres eliminar tu cuenta? Se borrarán también todos tus "
"patrones. Esta acción no se puede deshacer."
msgstr ""
"Are you sure you want to delete your account? All your patterns will also be "
"deleted. This action cannot be undone."
#: crochet/templates/crochet/_account_delete_form.html:19
#: crochet/templates/crochet/account_settings.html:48
msgid "Eliminar cuenta"
msgstr "Delete account"
#: crochet/templates/crochet/_account_display_name_form.html:7
msgid "Nombre actualizado."
msgstr "Name updated."
#: crochet/templates/crochet/_account_display_name_form.html:17
msgid "Guardar nombre"
msgstr "Save name"
#: crochet/templates/crochet/_account_email_form.html:10
msgid "Email actualizado."
msgstr "Email updated."
@@ -26,7 +59,7 @@ msgid "Contraseña actualizada."
msgstr "Password updated."
#: crochet/templates/crochet/_account_password_form.html:19
#: crochet/templates/crochet/account_settings.html:29
#: crochet/templates/crochet/account_settings.html:36
#: crochet/templates/crochet/password_reset_confirm.html:30
msgid "Cambiar contraseña"
msgstr "Change password"
@@ -49,7 +82,6 @@ msgid "Entendido"
msgstr "Agree"
#: crochet/templates/crochet/account_home.html:4
#: crochet/templates/crochet/pattern_detail.html:43
msgid "Mis patrones"
msgstr "My patterns"
@@ -96,10 +128,14 @@ msgstr "You don't have any patterns yet."
msgid "Ajustes de la cuenta"
msgstr "Account settings"
#: crochet/templates/crochet/account_settings.html:34
#: crochet/templates/crochet/account_settings.html:41
msgid "Volver a mis patrones"
msgstr "Back to my patterns"
#: crochet/templates/crochet/account_settings.html:50
msgid "Esto borrará tu cuenta y todos tus patrones de forma permanente."
msgstr "This will permanently delete your account and all your patterns."
#: crochet/templates/crochet/base.html:39
msgid "Cambiar idioma"
msgstr "Change language"
@@ -109,9 +145,8 @@ msgid "Cerrar sesión"
msgstr "Log out"
#: crochet/templates/crochet/base.html:62
#: crochet/templates/crochet/home.html:24
#: crochet/templates/crochet/home.html:25
#: crochet/templates/crochet/password_reset_complete.html:18
#: crochet/templates/crochet/pattern_detail.html:45
#: crochet/templates/registration/login.html:4
#: crochet/templates/registration/login.html:14
msgid "Iniciar sesión"
@@ -186,6 +221,45 @@ msgstr ""
msgid "Volver al inicio"
msgstr "Back to home"
#: crochet/templates/crochet/email/account_deleted_notice.html:4
#: crochet/templates/crochet/email/account_deleted_subject.txt:1
#, python-format
msgid "Tu cuenta en %(site_name)s ha sido eliminada"
msgstr "Your %(site_name)s account has been deleted"
#: crochet/templates/crochet/email/account_deleted_notice.html:8
#: crochet/templates/crochet/email/account_deleted_notice.txt:2
#, python-format
msgid ""
"Tu cuenta en %(site_name)s y todos tus patrones se han eliminado de forma "
"permanente."
msgstr ""
"Your %(site_name)s account and all your patterns have been permanently "
"deleted."
#: crochet/templates/crochet/email/account_deleted_notice.html:12
#: crochet/templates/crochet/email/account_deleted_notice.txt:4
#: crochet/templates/crochet/email/email_changed_notice.html:12
#: crochet/templates/crochet/email/email_changed_notice.txt:4
#: crochet/templates/crochet/email/password_changed_notice.html:12
#: crochet/templates/crochet/email/password_changed_notice.txt:4
msgid "Tu usuario:"
msgstr "Your username:"
#: crochet/templates/crochet/email/account_deleted_notice.html:16
#: crochet/templates/crochet/email/account_deleted_notice.txt:6
#: crochet/templates/crochet/email/email_changed_notice.html:16
#: crochet/templates/crochet/email/email_changed_notice.txt:6
#: crochet/templates/crochet/email/password_changed_notice.html:16
#: crochet/templates/crochet/email/password_changed_notice.txt:6
#, python-format
msgid ""
"Si no has sido tú quien ha hecho este cambio, contacta cuanto antes con "
"soporte técnico en %(support_email)s."
msgstr ""
"If you didn't make this change, please contact technical support at "
"%(support_email)s as soon as possible."
#: crochet/templates/crochet/email/base.html:43
msgid ""
"Este email se ha enviado automáticamente, no respondas a esta dirección."
@@ -207,25 +281,6 @@ msgstr ""
"The email on your %(site_name)s account has changed. This address "
"(%(old_email)s) is no longer associated with your account."
#: crochet/templates/crochet/email/email_changed_notice.html:12
#: crochet/templates/crochet/email/email_changed_notice.txt:4
#: crochet/templates/crochet/email/password_changed_notice.html:12
#: crochet/templates/crochet/email/password_changed_notice.txt:4
msgid "Tu usuario:"
msgstr "Your username:"
#: crochet/templates/crochet/email/email_changed_notice.html:16
#: crochet/templates/crochet/email/email_changed_notice.txt:6
#: crochet/templates/crochet/email/password_changed_notice.html:16
#: crochet/templates/crochet/email/password_changed_notice.txt:6
#, python-format
msgid ""
"Si no has sido tú quien ha hecho este cambio, contacta cuanto antes con "
"soporte técnico en %(support_email)s."
msgstr ""
"If you didn't make this change, please contact technical support at "
"%(support_email)s as soon as possible."
#: crochet/templates/crochet/email/password_changed_notice.html:4
#: crochet/templates/crochet/email/password_changed_subject.txt:1
#, python-format
@@ -286,11 +341,7 @@ msgstr ""
"Design your patterns with a visual editor, export them to PDF, and share "
"them with anyone, no installation needed."
#: crochet/templates/crochet/home.html:21
msgid "Ir a mis patrones"
msgstr "Go to my patterns"
#: crochet/templates/crochet/home.html:23
#: crochet/templates/crochet/home.html:24
msgid "Crear cuenta gratis"
msgstr "Create a free account"
@@ -636,7 +687,7 @@ msgstr ""
msgid "Añade una sección para ver aquí el resultado."
msgstr "Add a section to see the result here."
#: crochet/templates/crochet/pattern_detail.html:31
#: crochet/templates/crochet/pattern_detail.html:29
msgid "Descargar PDF"
msgstr "Download PDF"
@@ -669,44 +720,40 @@ msgstr "Sign up"
msgid "¿Ya tienes cuenta? Inicia sesión"
msgstr "Already have an account? Log in"
#: crochet/urls.py:37
#: crochet/urls.py:38
msgid "cookies/"
msgstr ""
#: crochet/urls.py:38
#: crochet/urls.py:39
msgid "pattern/<uuid:uuid>/"
msgstr ""
#: crochet/urls.py:39
#: crochet/urls.py:40
msgid "pattern/<uuid:uuid>/edit/"
msgstr ""
#: crochet/urls.py:40
#: crochet/urls.py:41
msgid "pattern/<uuid:uuid>/save/"
msgstr ""
#: crochet/urls.py:41
#: crochet/urls.py:42
msgid "pattern/<uuid:uuid>/images/"
msgstr ""
#: crochet/urls.py:42
#: crochet/urls.py:43
msgid "pattern/<uuid:uuid>/cover/"
msgstr ""
#: crochet/urls.py:43
#: crochet/urls.py:44
msgid "pattern/<uuid:uuid>/pdf/"
msgstr ""
#: crochet/urls.py:44
#: crochet/urls.py:45
msgid "pattern/<uuid:uuid>/delete/"
msgstr ""
#: crochet/urls.py:45
msgid "pattern/new/"
msgstr ""
#: crochet/urls.py:46
msgid "account/"
msgid "pattern/new/"
msgstr ""
#: crochet/urls.py:47
@@ -721,39 +768,54 @@ msgstr ""
msgid "account/settings/password/"
msgstr ""
#: crochet/urls.py:53
#: crochet/urls.py:54
msgid "account/settings/display-name/"
msgstr ""
#: crochet/urls.py:57
msgid "account/settings/delete/"
msgstr ""
#: crochet/urls.py:58
msgid "account/register/"
msgstr ""
#: crochet/urls.py:55
#: crochet/urls.py:60
msgid "account/login/"
msgstr ""
#: crochet/urls.py:60
#: crochet/urls.py:65
msgid "account/logout/"
msgstr ""
#: crochet/urls.py:62
#: crochet/urls.py:67
msgid "account/password-reset/"
msgstr ""
#: crochet/urls.py:81
#: crochet/urls.py:86
msgid "account/password-reset/done/"
msgstr ""
#: crochet/urls.py:85
#: crochet/urls.py:90
msgid "account/reset/<uidb64>/<token>/"
msgstr ""
#: crochet/urls.py:93
#: crochet/urls.py:98
msgid "account/reset/done/"
msgstr ""
# crochet/views.py (PatternDetailView.EMPTY_MESSAGE)
#: crochet/views.py:24
#: crochet/views.py:29
msgid "Este patrón todavía no tiene contenido."
msgstr "This pattern doesn't have any content yet."
#: crochet/views.py:350
msgid "Tu cuenta se ha eliminado correctamente."
msgstr "Your account has been deleted successfully."
#~ msgid "Ir a mis patrones"
#~ msgstr "Go to my patterns"
#~ msgid "Idioma"
#~ msgstr "Language"
+110 -52
View File
@@ -2,17 +2,48 @@ msgid ""
msgstr ""
"Project-Id-Version: crochet\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-21 12:32+0000\n"
"POT-Creation-Date: 2026-07-22 08:00+0000\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: crochet/forms.py:48 crochet/forms.py:96
#: crochet/templates/crochet/account_settings.html:22
#: crochet/templates/crochet/account_settings.html:29
msgid "Email"
msgstr ""
#: crochet/forms.py:128 crochet/templates/crochet/account_settings.html:22
msgid "Nombre para mostrar"
msgstr ""
#: crochet/forms.py:143
msgid "Contraseña"
msgstr ""
#: crochet/forms.py:153
msgid "Contraseña incorrecta."
msgstr ""
#: crochet/templates/crochet/_account_delete_form.html:9
msgid ""
"¿Seguro que quieres eliminar tu cuenta? Se borrarán también todos tus "
"patrones. Esta acción no se puede deshacer."
msgstr ""
#: crochet/templates/crochet/_account_delete_form.html:19
#: crochet/templates/crochet/account_settings.html:48
msgid "Eliminar cuenta"
msgstr ""
#: crochet/templates/crochet/_account_display_name_form.html:7
msgid "Nombre actualizado."
msgstr ""
#: crochet/templates/crochet/_account_display_name_form.html:17
msgid "Guardar nombre"
msgstr ""
#: crochet/templates/crochet/_account_email_form.html:10
msgid "Email actualizado."
msgstr ""
@@ -26,7 +57,7 @@ msgid "Contraseña actualizada."
msgstr ""
#: crochet/templates/crochet/_account_password_form.html:19
#: crochet/templates/crochet/account_settings.html:29
#: crochet/templates/crochet/account_settings.html:36
#: crochet/templates/crochet/password_reset_confirm.html:30
msgid "Cambiar contraseña"
msgstr ""
@@ -47,7 +78,6 @@ msgid "Entendido"
msgstr ""
#: crochet/templates/crochet/account_home.html:4
#: crochet/templates/crochet/pattern_detail.html:43
msgid "Mis patrones"
msgstr ""
@@ -94,10 +124,14 @@ msgstr ""
msgid "Ajustes de la cuenta"
msgstr ""
#: crochet/templates/crochet/account_settings.html:34
#: crochet/templates/crochet/account_settings.html:41
msgid "Volver a mis patrones"
msgstr ""
#: crochet/templates/crochet/account_settings.html:50
msgid "Esto borrará tu cuenta y todos tus patrones de forma permanente."
msgstr ""
#: crochet/templates/crochet/base.html:39
msgid "Cambiar idioma"
msgstr ""
@@ -107,9 +141,8 @@ msgid "Cerrar sesión"
msgstr ""
#: crochet/templates/crochet/base.html:62
#: crochet/templates/crochet/home.html:24
#: crochet/templates/crochet/home.html:25
#: crochet/templates/crochet/password_reset_complete.html:18
#: crochet/templates/crochet/pattern_detail.html:45
#: crochet/templates/registration/login.html:4
#: crochet/templates/registration/login.html:14
msgid "Iniciar sesión"
@@ -177,6 +210,41 @@ msgstr ""
msgid "Volver al inicio"
msgstr ""
#: crochet/templates/crochet/email/account_deleted_notice.html:4
#: crochet/templates/crochet/email/account_deleted_subject.txt:1
#, python-format
msgid "Tu cuenta en %(site_name)s ha sido eliminada"
msgstr ""
#: crochet/templates/crochet/email/account_deleted_notice.html:8
#: crochet/templates/crochet/email/account_deleted_notice.txt:2
#, python-format
msgid ""
"Tu cuenta en %(site_name)s y todos tus patrones se han eliminado de forma "
"permanente."
msgstr ""
#: crochet/templates/crochet/email/account_deleted_notice.html:12
#: crochet/templates/crochet/email/account_deleted_notice.txt:4
#: crochet/templates/crochet/email/email_changed_notice.html:12
#: crochet/templates/crochet/email/email_changed_notice.txt:4
#: crochet/templates/crochet/email/password_changed_notice.html:12
#: crochet/templates/crochet/email/password_changed_notice.txt:4
msgid "Tu usuario:"
msgstr ""
#: crochet/templates/crochet/email/account_deleted_notice.html:16
#: crochet/templates/crochet/email/account_deleted_notice.txt:6
#: crochet/templates/crochet/email/email_changed_notice.html:16
#: crochet/templates/crochet/email/email_changed_notice.txt:6
#: crochet/templates/crochet/email/password_changed_notice.html:16
#: crochet/templates/crochet/email/password_changed_notice.txt:6
#, python-format
msgid ""
"Si no has sido tú quien ha hecho este cambio, contacta cuanto antes con "
"soporte técnico en %(support_email)s."
msgstr ""
#: crochet/templates/crochet/email/base.html:43
msgid ""
"Este email se ha enviado automáticamente, no respondas a esta dirección."
@@ -196,23 +264,6 @@ msgid ""
"(%(old_email)s) ha dejado de estar asociada a tu cuenta."
msgstr ""
#: crochet/templates/crochet/email/email_changed_notice.html:12
#: crochet/templates/crochet/email/email_changed_notice.txt:4
#: crochet/templates/crochet/email/password_changed_notice.html:12
#: crochet/templates/crochet/email/password_changed_notice.txt:4
msgid "Tu usuario:"
msgstr ""
#: crochet/templates/crochet/email/email_changed_notice.html:16
#: crochet/templates/crochet/email/email_changed_notice.txt:6
#: crochet/templates/crochet/email/password_changed_notice.html:16
#: crochet/templates/crochet/email/password_changed_notice.txt:6
#, python-format
msgid ""
"Si no has sido tú quien ha hecho este cambio, contacta cuanto antes con "
"soporte técnico en %(support_email)s."
msgstr ""
#: crochet/templates/crochet/email/password_changed_notice.html:4
#: crochet/templates/crochet/email/password_changed_subject.txt:1
#, python-format
@@ -269,11 +320,7 @@ msgid ""
"quien quieras, sin instalar nada."
msgstr ""
#: crochet/templates/crochet/home.html:21
msgid "Ir a mis patrones"
msgstr ""
#: crochet/templates/crochet/home.html:23
#: crochet/templates/crochet/home.html:24
msgid "Crear cuenta gratis"
msgstr ""
@@ -606,7 +653,7 @@ msgstr ""
msgid "Añade una sección para ver aquí el resultado."
msgstr ""
#: crochet/templates/crochet/pattern_detail.html:31
#: crochet/templates/crochet/pattern_detail.html:29
msgid "Descargar PDF"
msgstr ""
@@ -639,49 +686,44 @@ msgstr ""
msgid "¿Ya tienes cuenta? Inicia sesión"
msgstr ""
#: crochet/urls.py:37
#: crochet/urls.py:38
msgid "cookies/"
msgstr ""
# Rutas traducibles de crochet/urls.py: la base "pattern/" se traduce como
# "patron/" (sin tilde, para no meter caracteres acentuados en la URL).
#: crochet/urls.py:38
#: crochet/urls.py:39
msgid "pattern/<uuid:uuid>/"
msgstr "patron/<uuid:uuid>/"
#: crochet/urls.py:39
#: crochet/urls.py:40
msgid "pattern/<uuid:uuid>/edit/"
msgstr "patron/<uuid:uuid>/editar/"
#: crochet/urls.py:40
#: crochet/urls.py:41
msgid "pattern/<uuid:uuid>/save/"
msgstr "patron/<uuid:uuid>/guardar/"
#: crochet/urls.py:41
#: crochet/urls.py:42
msgid "pattern/<uuid:uuid>/images/"
msgstr "patron/<uuid:uuid>/imagenes/"
#: crochet/urls.py:42
#: crochet/urls.py:43
msgid "pattern/<uuid:uuid>/cover/"
msgstr "patron/<uuid:uuid>/portada/"
#: crochet/urls.py:43
#: crochet/urls.py:44
msgid "pattern/<uuid:uuid>/pdf/"
msgstr "patron/<uuid:uuid>/pdf/"
#: crochet/urls.py:44
#: crochet/urls.py:45
msgid "pattern/<uuid:uuid>/delete/"
msgstr "patron/<uuid:uuid>/eliminar/"
#: crochet/urls.py:45
#: crochet/urls.py:46
msgid "pattern/new/"
msgstr "patron/nuevo/"
# Igual que "pattern/" arriba: "account/" se traduce como "cuenta/".
#: crochet/urls.py:46
msgid "account/"
msgstr "cuenta/"
#: crochet/urls.py:47
msgid "account/settings/"
msgstr "cuenta/ajustes/"
@@ -694,34 +736,50 @@ msgstr "cuenta/ajustes/email/"
msgid "account/settings/password/"
msgstr "cuenta/ajustes/contrasena/"
#: crochet/urls.py:53
#: crochet/urls.py:54
msgid "account/settings/display-name/"
msgstr "cuenta/ajustes/nombre/"
#: crochet/urls.py:57
msgid "account/settings/delete/"
msgstr "cuenta/ajustes/eliminar/"
#: crochet/urls.py:58
msgid "account/register/"
msgstr "cuenta/registro/"
#: crochet/urls.py:55
#: crochet/urls.py:60
msgid "account/login/"
msgstr "cuenta/entrar/"
#: crochet/urls.py:60
#: crochet/urls.py:65
msgid "account/logout/"
msgstr "cuenta/salir/"
#: crochet/urls.py:62
#: crochet/urls.py:67
msgid "account/password-reset/"
msgstr "cuenta/recuperar-contrasena/"
#: crochet/urls.py:81
#: crochet/urls.py:86
msgid "account/password-reset/done/"
msgstr "cuenta/recuperar-contrasena/enviado/"
#: crochet/urls.py:85
#: crochet/urls.py:90
msgid "account/reset/<uidb64>/<token>/"
msgstr "cuenta/restablecer/<uidb64>/<token>/"
#: crochet/urls.py:93
#: crochet/urls.py:98
msgid "account/reset/done/"
msgstr "cuenta/restablecer/hecho/"
#: crochet/views.py:24
#: crochet/views.py:29
msgid "Este patrón todavía no tiene contenido."
msgstr ""
#: crochet/views.py:350
msgid "Tu cuenta se ha eliminado correctamente."
msgstr ""
# Igual que "pattern/" arriba: "account/" se traduce como "cuenta/".
#~ msgid "account/"
#~ msgstr "cuenta/"
@@ -0,0 +1,20 @@
{% load i18n %}
<!-- hx-confirm (no un onsubmit propio): htmx cancela la petición si se
rechaza el diálogo, sin depender de si un onsubmit normal llega a
ejecutarse antes que el propio listener de htmx. En éxito, la
respuesta no trae fragmento que pintar: manda la cabecera HX-Redirect
(ver AccountDeleteView) y htmx navega de verdad a esa URL, ya sin
sesión que mostrar en esta misma página. -->
<form hx-post="{% url 'crochet:account_settings_delete' %}" hx-target="this" hx-swap="outerHTML"
hx-confirm="{% trans '¿Seguro que quieres eliminar tu cuenta? Se borrarán también todos tus patrones. Esta acción no se puede deshacer.' %}"
class="flex flex-col gap-3">
{% csrf_token %}
<label class="flex flex-col gap-1">
<span class="text-sm">{{ delete_form.password.label }}</span>
{{ delete_form.password }}
{% for error in delete_form.password.errors %}
<span class="text-error text-xs">{{ error }}</span>
{% endfor %}
</label>
<button type="submit" class="btn btn-error self-start">{% trans 'Eliminar cuenta' %}</button>
</form>
@@ -0,0 +1,18 @@
{% load i18n %}
<form hx-post="{% url 'crochet:account_settings_display_name' %}" hx-target="this" hx-swap="outerHTML"
class="flex flex-col gap-3">
{% csrf_token %}
{% if display_name_updated %}
<div role="alert" class="alert alert-success alert-soft">
<span>{% trans 'Nombre actualizado.' %}</span>
</div>
{% endif %}
<label class="flex flex-col gap-1">
<span class="text-sm">{{ display_name_form.display_name.label }}</span>
{{ display_name_form.display_name }}
{% for error in display_name_form.display_name.errors %}
<span class="text-error text-xs">{{ error }}</span>
{% endfor %}
</label>
<button type="submit" class="btn btn-primary self-start">{% trans 'Guardar nombre' %}</button>
</form>
+1 -1
View File
@@ -10,7 +10,7 @@
{% block content %}
<main class="max-w-4xl mx-auto px-4 py-8">
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
<h1 class="text-2xl font-semibold">{% blocktrans with username=user.username %}Hola, {{ username }}{% endblocktrans %}</h1>
<h1 class="text-2xl font-semibold">{% blocktrans with username=user.get_full_name|default:user.username %}Hola, {{ username }}{% endblocktrans %}</h1>
<form method="post" action="{% url 'crochet:pattern_create' %}">
{% csrf_token %}
<button type="submit" class="btn btn-primary">+ {% trans 'Crear patrón' %}</button>
@@ -17,6 +17,13 @@
<main class="max-w-lg mx-auto px-4 py-8 flex flex-col gap-8">
<h1 class="text-2xl font-semibold">{% trans 'Ajustes de la cuenta' %}</h1>
<section class="card bg-base-100 shadow border border-base-300">
<div class="card-body">
<h2 class="card-title text-lg">{% trans 'Nombre para mostrar' %}</h2>
{% include 'crochet/_account_display_name_form.html' %}
</div>
</section>
<section class="card bg-base-100 shadow border border-base-300">
<div class="card-body">
<h2 class="card-title text-lg">{% trans 'Email' %}</h2>
@@ -31,6 +38,19 @@
</div>
</section>
<a href="{% url 'crochet:account_home' %}" class="link link-primary">{% trans 'Volver a mis patrones' %}</a>
<a href="{% url 'crochet:home' %}" class="link link-primary">{% trans 'Volver a mis patrones' %}</a>
<!-- Zona de peligro, aparte del resto (border-error): que se note a
simple vista que esto es irreversible, distinto de los ajustes de
arriba. -->
<section class="card bg-base-100 shadow border border-error">
<div class="card-body">
<h2 class="card-title text-lg text-error">{% trans 'Eliminar cuenta' %}</h2>
<p class="text-sm text-base-content/70">
{% trans 'Esto borrará tu cuenta y todos tus patrones de forma permanente.' %}
</p>
{% include 'crochet/_account_delete_form.html' %}
</div>
</section>
</main>
{% endblock %}
+16 -1
View File
@@ -47,7 +47,7 @@
<!-- Dropdown CSS puro de daisyUI (sin JS propio): se abre con
:focus-within al pulsar/tabular al botón disparador. -->
<div class="dropdown dropdown-end">
<button type="button" tabindex="0" class="btn btn-sm btn-ghost">{{ user.username }}</button>
<button type="button" tabindex="0" class="btn btn-sm btn-ghost">{{ user.get_full_name|default:user.username }}</button>
<ul tabindex="0" class="dropdown-content menu menu-sm bg-base-100 border border-base-300 rounded-box z-10 w-52 p-2 shadow">
<li><a href="{% url 'crochet:account_settings' %}">{% trans 'Ajustes de la cuenta' %}</a></li>
<li>
@@ -64,6 +64,21 @@
</div>
</nav>
{% endblock %}
<!-- Mensajes de una sola vez tras una redirección (ver messages.success
en AccountDeleteView): no hay ninguna página a la que volver con el
resultado de la acción (la sesión ya no existe), así que se muestran
aquí, en la página a la que se redirige. django.contrib.messages ya
estaba instalado (middleware + procesador de contexto) pero sin
usar hasta ahora. -->
{% if messages %}
<div class="max-w-2xl mx-auto px-4 pt-4 flex flex-col gap-2 no-print">
{% for message in messages %}
<div role="alert" class="alert alert-soft{% if message.tags %} alert-{{ message.tags }}{% endif %}">
<span>{{ message }}</span>
</div>
{% endfor %}
</div>
{% endif %}
{% block content %}{% endblock %}
<!-- pattern_detail.html (vista de solo lectura/PDF) sobreescribe este
bloque dejándolo vacío, igual que el navbar: no carga daisyUI y no
@@ -0,0 +1,18 @@
{% extends 'crochet/email/base.html' %}
{% load i18n %}
{% block title %}{% blocktrans %}Tu cuenta en {{ site_name }} ha sido eliminada{% endblocktrans %}{% endblock %}
{% block content %}
<p style="margin:0 0 16px 0;">
{% blocktrans %}Tu cuenta en {{ site_name }} y todos tus patrones se han eliminado de forma permanente.{% endblocktrans %}
</p>
<p style="margin:0 0 16px 0; font-size:13px; color:#2e2118;">
{% trans 'Tu usuario:' %} {{ user.get_username }}
</p>
<p style="margin:0; font-weight:600;">
{% blocktrans %}Si no has sido tú quien ha hecho este cambio, contacta cuanto antes con soporte técnico en {{ support_email }}.{% endblocktrans %}
</p>
{% endblock %}
@@ -0,0 +1,7 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}Tu cuenta en {{ site_name }} y todos tus patrones se han eliminado de forma permanente.{% endblocktrans %}
{% trans 'Tu usuario:' %} {{ user.get_username }}
{% blocktrans %}Si no has sido tú quien ha hecho este cambio, contacta cuanto antes con soporte técnico en {{ support_email }}.{% endblocktrans %}
{% endautoescape %}
@@ -0,0 +1 @@
{% load i18n %}{% blocktrans %}Tu cuenta en {{ site_name }} ha sido eliminada{% endblocktrans %}
+4 -4
View File
@@ -16,13 +16,13 @@
<p class="py-6 text-lg text-base-content/70">
{% trans 'Diseña tus patrones con un editor visual, expórtalos a PDF y compártelos con quien quieras, sin instalar nada.' %}
</p>
<!-- Sin el "if user.is_authenticated" que había aquí antes: con
sesión iniciada, HomeView ya no llega a renderizar esta
plantilla en absoluto (ver get_template_names), así que esa
rama nunca se alcanzaba. -->
<div class="flex flex-wrap justify-center gap-4">
{% if user.is_authenticated %}
<a href="{% url 'crochet:account_home' %}" class="btn btn-primary btn-lg">{% trans 'Ir a mis patrones' %}</a>
{% else %}
<a href="{% url 'crochet:register' %}" class="btn btn-primary btn-lg">{% trans 'Crear cuenta gratis' %}</a>
<a href="{% url 'crochet:login' %}" class="btn btn-outline btn-lg">{% trans 'Iniciar sesión' %}</a>
{% endif %}
</div>
</div>
</div>
+120 -1
View File
@@ -4,6 +4,8 @@ from django.test import TestCase
from django.urls import reverse
from django.utils import translation
from crochet.models import Pattern
class AccountSettingsViewTests(TestCase):
def setUp(self):
@@ -146,7 +148,7 @@ class AccountPasswordChangeViewTests(TestCase):
# update_session_auth_hash: si no se llamara, esta petición
# (sesión ya cargada con el hash viejo) habría quedado deslogueada.
with translation.override('es'):
account_home_url = reverse('crochet:account_home')
account_home_url = reverse('crochet:home')
still_logged_in_response = self.client.get(account_home_url)
self.assertEqual(still_logged_in_response.status_code, 200)
@@ -219,3 +221,120 @@ class AccountPasswordChangeViewTests(TestCase):
})
self.assertEqual(len(mail.outbox), 0)
class AccountDisplayNameUpdateViewTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
with translation.override('es'):
self.url = reverse('crochet:account_settings_display_name')
def test_anonymous_user_is_redirected_to_login(self):
response = self.client.post(self.url, {'display_name': 'Ana'})
with translation.override('es'):
login_url = reverse('crochet:login')
self.assertRedirects(response, f'{login_url}?next={self.url}')
def test_valid_name_updates_user_and_shows_success(self):
self.client.force_login(self.user)
response = self.client.post(self.url, {'display_name': 'Ana'})
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'crochet/_account_display_name_form.html')
self.user.refresh_from_db()
self.assertEqual(self.user.first_name, 'Ana')
self.assertContains(response, 'Nombre actualizado.')
def test_blank_name_is_allowed_and_clears_it(self):
self.user.first_name = 'Ana'
self.user.save(update_fields=['first_name'])
self.client.force_login(self.user)
response = self.client.post(self.url, {'display_name': ''})
self.assertEqual(response.status_code, 200)
self.user.refresh_from_db()
self.assertEqual(self.user.first_name, '')
def test_only_updates_the_requesting_users_own_name(self):
other = User.objects.create_user(username='other', password='a-very-uncommon-pw-1')
self.client.force_login(self.user)
self.client.post(self.url, {'display_name': 'Ana'})
other.refresh_from_db()
self.assertEqual(other.first_name, '')
class AccountDeleteViewTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='crocheter', password='the-password-1', email='crocheter@example.com',
)
with translation.override('es'):
self.url = reverse('crochet:account_settings_delete')
def test_anonymous_user_is_redirected_to_login(self):
response = self.client.post(self.url, {'password': 'the-password-1'})
with translation.override('es'):
login_url = reverse('crochet:login')
self.assertRedirects(response, f'{login_url}?next={self.url}')
def test_wrong_password_does_not_delete_the_account(self):
self.client.force_login(self.user)
response = self.client.post(self.url, {'password': 'wrong-password'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'class="text-error text-xs"')
self.assertTrue(User.objects.filter(pk=self.user.pk).exists())
self.assertEqual(len(mail.outbox), 0)
def test_correct_password_deletes_the_account_and_its_patterns(self):
pattern = Pattern.objects.create(created_by=self.user)
self.client.force_login(self.user)
response = self.client.post(self.url, {'password': 'the-password-1'})
self.assertFalse(User.objects.filter(pk=self.user.pk).exists())
self.assertFalse(Pattern.objects.filter(pk=pattern.pk).exists())
with translation.override('es'):
home_url = reverse('crochet:home')
self.assertEqual(response['HX-Redirect'], home_url)
def test_deleting_the_account_logs_out_the_session(self):
self.client.force_login(self.user)
self.client.post(self.url, {'password': 'the-password-1'})
self.assertNotIn('_auth_user_id', self.client.session)
# El índice ya no requiere sesión (a diferencia del extinto
# account_home): tras el borrado, vuelve a mostrar la landing de
# marketing en vez del panel de patrones.
with translation.override('es'):
home_url = reverse('crochet:home')
response = self.client.get(home_url)
self.assertTemplateUsed(response, 'crochet/home.html')
def test_deleting_the_account_notifies_the_users_email(self):
self.client.force_login(self.user)
self.client.post(self.url, {'password': 'the-password-1'})
self.assertEqual(len(mail.outbox), 1)
message = mail.outbox[0]
self.assertEqual(message.to, ['crocheter@example.com'])
self.assertIn('crocheter', message.body)
self.assertIn('soporte@localhost', message.body)
html_bodies = [content for content, mimetype in message.alternatives if mimetype == 'text/html']
self.assertEqual(len(html_bodies), 1)
def test_only_deletes_the_requesting_users_own_account(self):
other = User.objects.create_user(username='other', password='a-very-uncommon-pw-1')
self.client.force_login(self.user)
self.client.post(self.url, {'password': 'the-password-1'})
self.assertTrue(User.objects.filter(pk=other.pk).exists())
+34 -11
View File
@@ -27,7 +27,7 @@ class RegisterViewTests(TestCase):
user = User.objects.get(username='crocheter')
self.assertEqual(user.email, 'crocheter@example.com')
self.assertRedirects(response, reverse('crochet:account_home'))
self.assertRedirects(response, reverse('crochet:home'))
self.assertIn('_auth_user_id', self.client.session)
def test_post_with_mismatched_passwords_does_not_create_user(self):
@@ -72,7 +72,7 @@ class LoginViewTests(TestCase):
'password': 'a-very-uncommon-pw-1',
})
self.assertRedirects(response, reverse('crochet:account_home'))
self.assertRedirects(response, reverse('crochet:home'))
self.assertIn('_auth_user_id', self.client.session)
def test_post_with_wrong_password_does_not_log_in(self):
@@ -98,17 +98,14 @@ class LogoutViewTests(TestCase):
self.assertNotIn('_auth_user_id', self.client.session)
class AccountHomeViewTests(TestCase):
class HomeViewDashboardTests(TestCase):
# El índice ("/") ya no requiere sesión (a diferencia del extinto
# account_home): para quien no ha iniciado sesión enseña la landing de
# marketing (ver HomeViewTests en test_views.py); estos tests cubren
# justo la otra rama, el contenido que ve quien sí la tiene.
def setUp(self):
with translation.override('es'):
self.url = reverse('crochet:account_home')
def test_anonymous_user_is_redirected_to_login(self):
response = self.client.get(self.url)
with translation.override('es'):
login_url = reverse('crochet:login')
self.assertRedirects(response, f'{login_url}?next={self.url}')
self.url = reverse('crochet:home')
def test_logged_in_user_sees_their_username(self):
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
@@ -119,6 +116,14 @@ class AccountHomeViewTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'crocheter')
def test_greeting_uses_display_name_when_set(self):
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1', first_name='Ana')
self.client.force_login(user)
response = self.client.get(self.url)
self.assertContains(response, 'Hola, Ana')
def test_only_lists_own_patterns(self):
owner = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
other = User.objects.create_user(username='other', password='a-very-uncommon-pw-1')
@@ -167,3 +172,21 @@ class PatternCreateViewTests(TestCase):
with translation.override('es'):
edit_url = reverse('crochet:pattern_edit', args=[pattern.uuid])
self.assertRedirects(response, edit_url)
def test_new_pattern_defaults_author_to_the_users_display_name(self):
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1', first_name='Ana')
self.client.force_login(user)
self.client.post(self.url)
pattern = Pattern.objects.get()
self.assertEqual(pattern.page_settings.get('author'), 'Ana')
def test_new_pattern_leaves_author_blank_without_a_display_name(self):
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
self.client.force_login(user)
self.client.post(self.url)
pattern = Pattern.objects.get()
self.assertNotIn('author', pattern.page_settings)
+18 -5
View File
@@ -35,16 +35,20 @@ class HomeViewTests(TestCase):
self.assertContains(response, f'href="{register_url}"')
self.assertContains(response, f'href="{login_url}"')
def test_logged_in_user_sees_link_to_their_patterns(self):
def test_logged_in_user_sees_their_patterns_dashboard_instead_of_the_landing_page(self):
# El índice ("/") ya no enseña la landing de marketing a quien ya
# tiene cuenta: directamente "Mis patrones" (ver HomeView.get),
# sin cambiar de URL.
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
self.client.force_login(user)
with translation.override('es'):
url = reverse('crochet:home')
account_home_url = reverse('crochet:account_home')
response = self.client.get(url)
self.assertContains(response, f'href="{account_home_url}"')
self.assertNotContains(response, 'Crear cuenta gratis')
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'crochet/account_home.html')
self.assertContains(response, 'Hola, crocheter')
self.assertNotContains(response, 'Crea y comparte tus patrones de crochet')
def test_available_at_root_for_both_languages(self):
response_es = self.client.get('/es/')
@@ -144,6 +148,15 @@ class NavbarAccountDropdownTests(TestCase):
self.assertContains(response, f'href="{settings_url}"')
self.assertContains(response, f'action="{logout_url}"')
def test_dropdown_shows_display_name_when_set(self):
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1', first_name='Ana')
self.client.force_login(user)
response = self.client.get('/es/')
self.assertContains(response, '>Ana<')
self.assertNotContains(response, '>crocheter<')
def test_anonymous_user_does_not_see_the_account_dropdown(self):
with translation.override('es'):
settings_url = reverse('crochet:account_settings')
@@ -554,7 +567,7 @@ class PatternDeleteViewTests(TestCase):
response = self.client.post(self.url)
with translation.override('es'):
account_home_url = reverse('crochet:account_home')
account_home_url = reverse('crochet:home')
self.assertRedirects(response, account_home_url)
self.assertFalse(Pattern.objects.filter(pk=self.pattern.pk).exists())
+7 -2
View File
@@ -4,8 +4,9 @@ from django.utils.translation import gettext_lazy as _
from crochet.forms import StyledAuthenticationForm, StyledPasswordResetForm, StyledSetPasswordForm
from crochet.views import (
AccountDeleteView,
AccountDisplayNameUpdateView,
AccountEmailUpdateView,
AccountHomeView,
AccountPasswordChangeView,
AccountSettingsView,
CookiePolicyView,
@@ -43,13 +44,17 @@ urlpatterns = [
path(_('pattern/<uuid:uuid>/pdf/'), name='pattern_pdf', view=PatternPdfView.as_view()),
path(_('pattern/<uuid:uuid>/delete/'), name='pattern_delete', view=PatternDeleteView.as_view()),
path(_('pattern/new/'), name='pattern_create', view=PatternCreateView.as_view()),
path(_('account/'), name='account_home', view=AccountHomeView.as_view()),
path(_('account/settings/'), name='account_settings', view=AccountSettingsView.as_view()),
path(_('account/settings/email/'), name='account_settings_email', view=AccountEmailUpdateView.as_view()),
path(
_('account/settings/password/'), name='account_settings_password',
view=AccountPasswordChangeView.as_view(),
),
path(
_('account/settings/display-name/'), name='account_settings_display_name',
view=AccountDisplayNameUpdateView.as_view(),
),
path(_('account/settings/delete/'), name='account_settings_delete', view=AccountDeleteView.as_view()),
path(_('account/register/'), name='register', view=RegisterView.as_view()),
path(
_('account/login/'), name='login',
+68 -25
View File
@@ -1,26 +1,31 @@
import json
import weasyprint
from django.contrib.auth import login, update_session_auth_hash
from django.contrib import messages
from django.contrib.auth import login, logout, update_session_auth_hash
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied, SuspiciousFileOperation
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.urls import reverse, reverse_lazy
from django.utils.text import get_valid_filename
from django.utils.translation import get_language, gettext_lazy as _
from django.views import View
from django.views.generic import CreateView, DetailView, TemplateView
from crochet.cover_image import InvalidCoverImage, build_cover_image_variants
from crochet.forms import EmailUpdateForm, StyledPasswordChangeForm, StyledUserCreationForm
from crochet.forms import (
AccountDeleteForm,
DisplayNameForm,
EmailUpdateForm,
StyledPasswordChangeForm,
StyledUserCreationForm,
)
from crochet.models import Pattern, PatternImage, StitchType
from crochet.pattern_render import render_pattern_html, sanitize_page_settings
# gettext_lazy (no gettext): se evalúa al construir el HTML de salida (dentro
# de render_pattern_html, vía format_html), no aquí al importar el módulo, así
# que respeta el idioma activo de cada petición aunque esto se cree una vez.
PATTERN_EMPTY_MESSAGE = _('Este patrón todavía no tiene contenido.')
@@ -65,6 +70,20 @@ def _pattern_detail_context(pattern):
class HomeView(TemplateView):
template_name = 'crochet/home.html'
account_template_name = 'crochet/account_home.html'
def get_template_names(self):
if self.request.user.is_authenticated:
return [self.account_template_name]
return [self.template_name]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.user.is_authenticated:
context['patterns'] = self.request.user.patterns.order_by('-updated_at')
return context
class CookiePolicyView(TemplateView):
@@ -80,16 +99,14 @@ class PatternEditView(LoginRequiredMixin, DetailView):
def get_object(self, queryset=None):
pattern = super().get_object(queryset)
# LoginRequiredMixin ya garantiza que request.user está autenticado
# aquí (si no, redirige a login antes de llegar a get_object).
if pattern.created_by_id is None or pattern.created_by_id != self.request.user.id:
raise PermissionDenied
return pattern
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Resuelto por LocaleMiddleware a partir del prefijo de idioma de la
# URL (/es/..., /en/...; ver i18n_patterns en config/urls.py).
context['lang'] = get_language()
# Se pasa tal cual a la plantilla con {{ pattern_data|json_script:"..." }}
# para que main.js reconstruya el editor con el contenido guardado
@@ -241,26 +258,14 @@ class RegisterView(CreateView):
# versión con clases de daisyUI en sus widgets (ver crochet/forms.py).
form_class = StyledUserCreationForm
template_name = 'registration/register.html'
success_url = reverse_lazy('crochet:account_home')
success_url = reverse_lazy('crochet:home')
def form_valid(self, form):
response = super().form_valid(form)
# Registrarse ya cuenta como haber probado usuario/contraseña, así
# que se inicia sesión directamente en vez de mandar al usuario a
# loguearse otra vez con lo que acaba de escribir.
login(self.request, self.object)
return response
class AccountHomeView(LoginRequiredMixin, TemplateView):
template_name = 'crochet/account_home.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['patterns'] = self.request.user.patterns.order_by('-updated_at')
return context
class AccountSettingsView(LoginRequiredMixin, TemplateView):
template_name = 'crochet/account_settings.html'
@@ -268,6 +273,10 @@ class AccountSettingsView(LoginRequiredMixin, TemplateView):
context = super().get_context_data(**kwargs)
context['email_form'] = EmailUpdateForm(user=self.request.user, initial={'email': self.request.user.email})
context['password_form'] = StyledPasswordChangeForm(user=self.request.user)
context['display_name_form'] = DisplayNameForm(
user=self.request.user, initial={'display_name': self.request.user.first_name},
)
context['delete_form'] = AccountDeleteForm(user=self.request.user)
return context
@@ -312,11 +321,45 @@ class AccountPasswordChangeView(LoginRequiredMixin, View):
)
class AccountDisplayNameUpdateView(LoginRequiredMixin, View):
def post(self, request):
form = DisplayNameForm(request.POST, user=request.user)
if form.is_valid():
form.save()
form = DisplayNameForm(user=request.user, initial={'display_name': request.user.first_name})
display_name_updated = True
else:
display_name_updated = False
return render(
request, 'crochet/_account_display_name_form.html',
{'display_name_form': form, 'display_name_updated': display_name_updated},
)
class AccountDeleteView(LoginRequiredMixin, View):
def post(self, request):
form = AccountDeleteForm(request.POST, user=request.user)
if not form.is_valid():
return render(request, 'crochet/_account_delete_form.html', {'delete_form': form})
form.notify_account_deleted()
user = request.user
user.patterns.all().delete()
logout(request)
user.delete()
messages.success(request, _('Tu cuenta se ha eliminado correctamente.'))
response = HttpResponse()
response['HX-Redirect'] = reverse('crochet:home')
return response
class PatternCreateView(LoginRequiredMixin, View):
# Solo POST (ver el <form> en account_home.html): crear un patrón no
# debería poder dispararse desde un simple enlace GET sin token CSRF.
def post(self, request):
pattern = Pattern.objects.create(created_by=request.user)
page_settings = {'author': request.user.first_name} if request.user.first_name else {}
pattern = Pattern.objects.create(created_by=request.user, page_settings=page_settings)
return redirect('crochet:pattern_edit', uuid=pattern.uuid)
@@ -328,4 +371,4 @@ class PatternDeleteView(LoginRequiredMixin, View):
def post(self, request, uuid):
pattern = _get_owned_pattern_or_403(request, uuid)
pattern.delete()
return redirect('crochet:account_home')
return redirect('crochet:home')