feat: added recover password view
CI / test (push) Successful in 1m3s
CI / build (push) Successful in 53s

This commit is contained in:
2026-07-21 12:47:01 +02:00
parent 2f58e23cd2
commit 3e95ee2239
14 changed files with 724 additions and 148 deletions
+1
View File
@@ -43,6 +43,7 @@ EMAIL_HOST = env.str('EMAIL_HOST', '')
EMAIL_HOST_USER = env.str('EMAIL_HOST_USER', '')
EMAIL_HOST_PASSWORD = env.str('EMAIL_HOST_PASSWORD', '')
EMAIL_PORT = env.int('EMAIL_PORT', 587)
DEFAULT_FROM_EMAIL = env.str('DEFAULT_FROM_EMAIL', 'webmaster@localhost')
# Nivel de log de la app y de Django (ver LOGGING en base.py). En producción
# (DEBUG=False) Django, por defecto, solo manda los errores por email a
+29 -1
View File
@@ -1,5 +1,11 @@
from django import forms
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, UserCreationForm
from django.contrib.auth.forms import (
AuthenticationForm,
PasswordChangeForm,
PasswordResetForm,
SetPasswordForm,
UserCreationForm,
)
from django.utils.translation import gettext_lazy as _
# Los formularios de auth de Django no traen ninguna clase CSS en sus
@@ -18,6 +24,14 @@ class StyledAuthenticationForm(AuthenticationForm):
class StyledUserCreationForm(UserCreationForm):
# UserCreationForm no pide email por defecto (piensa solo en
# usuario/contraseña); sin él, PasswordResetForm no tiene a qué
# dirección mandar el enlace de recuperación de estos usuarios.
email = forms.EmailField(label=_('Email'))
class Meta(UserCreationForm.Meta):
fields = ('username', 'email')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
@@ -31,6 +45,20 @@ class StyledPasswordChangeForm(PasswordChangeForm):
field.widget.attrs['class'] = INPUT_CLASSES
class StyledPasswordResetForm(PasswordResetForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = INPUT_CLASSES
class StyledSetPasswordForm(SetPasswordForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = INPUT_CLASSES
class EmailUpdateForm(forms.Form):
email = forms.EmailField(label=_('Email'), widget=forms.EmailInput(attrs={'class': INPUT_CLASSES}))
+188 -72
View File
@@ -2,13 +2,14 @@ msgid ""
msgstr ""
"Project-Id-Version: crochet\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-20 10:46+0000\n"
"POT-Creation-Date: 2026-07-21 10:20+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:35 crochet/templates/crochet/account_settings.html:22
#: crochet/forms.py:30 crochet/forms.py:63
#: crochet/templates/crochet/account_settings.html:22
msgid "Email"
msgstr "Email"
@@ -26,6 +27,7 @@ msgstr "Password updated."
#: crochet/templates/crochet/_account_password_form.html:19
#: crochet/templates/crochet/account_settings.html:29
#: crochet/templates/crochet/password_reset_confirm.html:30
msgid "Cambiar contraseña"
msgstr "Change password"
@@ -57,13 +59,12 @@ msgid "Actualizado el %(date)s"
msgstr "Updated on %(date)s"
#: crochet/templates/crochet/account_home.html:44
#: crochet/templates/crochet/pattern.html:158
#: crochet/templates/crochet/pattern.html:204
#: crochet/templates/crochet/pattern.html:163
msgid "Vista previa"
msgstr "Preview"
#: crochet/templates/crochet/account_home.html:48
#: crochet/templates/crochet/pattern.html:50
#: crochet/templates/crochet/pattern.html:55
msgid "Eliminar"
msgstr "Delete"
@@ -91,16 +92,12 @@ msgstr "Log out"
#: crochet/templates/crochet/base.html:62
#: crochet/templates/crochet/home.html:24
#: crochet/templates/crochet/password_reset_complete.html:18
#: crochet/templates/registration/login.html:4
#: crochet/templates/registration/login.html:14
msgid "Iniciar sesión"
msgstr "Log in"
#: crochet/templates/crochet/base.html:63
#: crochet/templates/registration/register.html:29
msgid "Registrarme"
msgstr "Sign up"
#: crochet/templates/crochet/home.html:4
msgid "Crochet — Crea y comparte tus patrones"
msgstr "Crochet — Create and share your patterns"
@@ -161,233 +158,328 @@ msgstr ""
"Send your pattern to anyone with a read-only link: no one else will be able "
"to edit it."
#: crochet/templates/crochet/password_reset_complete.html:4
#: crochet/templates/crochet/password_reset_complete.html:14
msgid "Contraseña actualizada"
msgstr "Password updated"
#: crochet/templates/crochet/password_reset_complete.html:16
msgid "Ya puedes iniciar sesión con tu nueva contraseña."
msgstr "You can now log in with your new password."
#: crochet/templates/crochet/password_reset_confirm.html:4
msgid "Elegir nueva contraseña"
msgstr "Choose new password"
#: crochet/templates/crochet/password_reset_confirm.html:15
msgid "Elige una contraseña nueva"
msgstr "Choose a new password"
#: crochet/templates/crochet/password_reset_confirm.html:33
msgid "Enlace no válido"
msgstr "Invalid link"
#: crochet/templates/crochet/password_reset_confirm.html:35
msgid ""
"El enlace para restablecer la contraseña no es válido, puede que ya se haya "
"usado. Solicita uno nuevo."
msgstr ""
"The password reset link is invalid, possibly because it has already been "
"used. Please request a new one."
#: crochet/templates/crochet/password_reset_confirm.html:38
msgid "Solicitar un enlace nuevo"
msgstr "Request a new link"
#: crochet/templates/crochet/password_reset_done.html:4
#: crochet/templates/crochet/password_reset_form.html:4
#: crochet/templates/crochet/password_reset_form.html:14
msgid "Recuperar contraseña"
msgstr "Reset password"
#: crochet/templates/crochet/password_reset_done.html:14
msgid "Revisa tu email"
msgstr "Check your email"
#: crochet/templates/crochet/password_reset_done.html:16
msgid ""
"Si existe una cuenta con ese email, te hemos enviado un enlace para elegir "
"una contraseña nueva."
msgstr ""
"If an account exists with that email, we've sent you a link to choose a new "
"password."
#: crochet/templates/crochet/password_reset_done.html:19
#: crochet/templates/crochet/password_reset_form.html:32
msgid "Volver a iniciar sesión"
msgstr "Back to login"
#: crochet/templates/crochet/password_reset_email.html:2
#, python-format
msgid ""
"Has recibido este email porque alguien ha solicitado restablecer la "
"contraseña de tu cuenta en %(site_name)s."
msgstr ""
"You're receiving this email because someone requested a password reset for "
"your account at %(site_name)s."
#: crochet/templates/crochet/password_reset_email.html:4
msgid "Sigue este enlace para elegir una contraseña nueva:"
msgstr "Follow this link to choose a new password:"
#: crochet/templates/crochet/password_reset_email.html:7
msgid "Tu usuario, por si lo has olvidado:"
msgstr "Your username, in case you've forgotten:"
#: crochet/templates/crochet/password_reset_email.html:9
msgid "Si no has solicitado este cambio, puedes ignorar este email."
msgstr "If you didn't request this change, you can ignore this email."
#: crochet/templates/crochet/password_reset_form.html:16
msgid ""
"Escribe tu email y te enviaremos un enlace para elegir una contraseña nueva."
msgstr "Enter your email and we'll send you a link to choose a new password."
#: crochet/templates/crochet/password_reset_form.html:29
msgid "Enviar enlace"
msgstr "Send link"
#: crochet/templates/crochet/password_reset_subject.txt:1
#, python-format
msgid "Recupera tu contraseña en %(site_name)s"
msgstr "Reset your password on %(site_name)s"
#: crochet/templates/crochet/pattern.html:26
msgid "Idioma del patrón"
msgstr "Pattern language"
# crochet/templates/crochet/pattern.html
#: crochet/templates/crochet/pattern.html:33
#: crochet/templates/crochet/pattern.html:38
msgid "No se ha podido guardar. Inténtalo de nuevo."
msgstr "Couldn't save. Please try again."
#: crochet/templates/crochet/pattern.html:33
#: crochet/templates/crochet/pattern.html:38
msgid "Guardar"
msgstr "Save"
#: crochet/templates/crochet/pattern.html:36
#: crochet/templates/crochet/pattern.html:41
msgid "Ver patrón"
msgstr "View pattern"
#: crochet/templates/crochet/pattern.html:37
#: crochet/templates/crochet/pattern.html:42
msgid "Exportar a PDF"
msgstr "Export to PDF"
#: crochet/templates/crochet/pattern.html:44
#: crochet/templates/crochet/pattern.html:49
msgid "Más opciones"
msgstr "More options"
#: crochet/templates/crochet/pattern.html:60
#: crochet/templates/crochet/pattern.html:65
msgid "Guardado"
msgstr "Saved"
#: crochet/templates/crochet/pattern.html:67
#: crochet/templates/crochet/pattern.html:72
msgid "Personalización de página"
msgstr "Page customization"
#: crochet/templates/crochet/pattern.html:71
#: crochet/templates/crochet/pattern.html:76
msgid "Título del patrón"
msgstr "Pattern title"
#: crochet/templates/crochet/pattern.html:72
#: crochet/templates/crochet/pattern.html:77
msgid "Título del patrón..."
msgstr "Pattern title..."
#: crochet/templates/crochet/pattern.html:75
#: crochet/templates/crochet/pattern.html:80
msgid "Autor"
msgstr "Author"
#: crochet/templates/crochet/pattern.html:76
#: crochet/templates/crochet/pattern.html:81
msgid "Autor..."
msgstr "Author..."
#: crochet/templates/crochet/pattern.html:81
#: crochet/templates/crochet/pattern.html:86
msgid "Imagen de portada"
msgstr "Cover image"
#: crochet/templates/crochet/pattern.html:84
#: crochet/templates/crochet/pattern.html:89
msgid "No se ha podido subir la imagen de portada. Inténtalo de nuevo."
msgstr "Couldn't upload the cover image. Please try again."
#: crochet/templates/crochet/pattern.html:98
#: crochet/templates/crochet/pattern.html:182
#: crochet/templates/crochet/pattern.html:103
#: crochet/templates/crochet/pattern.html:187
msgid "Color del texto"
msgstr "Text color"
#: crochet/templates/crochet/pattern.html:102
#: crochet/templates/crochet/pattern.html:107
msgid "Color de acento (títulos)"
msgstr "Accent color (headings)"
#: crochet/templates/crochet/pattern.html:106
#: crochet/templates/crochet/pattern.html:182
#: crochet/templates/crochet/pattern.html:111
#: crochet/templates/crochet/pattern.html:187
msgid "Color de fondo"
msgstr "Background color"
#: crochet/templates/crochet/pattern.html:110
#: crochet/templates/crochet/pattern.html:115
msgid "Tipografía"
msgstr "Font"
#: crochet/templates/crochet/pattern.html:116
#: crochet/templates/crochet/pattern.html:121
msgid "Monoespaciada"
msgstr "Monospace"
#: crochet/templates/crochet/pattern.html:120
#: crochet/templates/crochet/pattern.html:125
msgid "Tamaño de letra"
msgstr "Font size"
#: crochet/templates/crochet/pattern.html:122
#: crochet/templates/crochet/pattern.html:127
msgid "Pequeño"
msgstr "Small"
#: crochet/templates/crochet/pattern.html:123
#: crochet/templates/crochet/pattern.html:128
msgid "Normal"
msgstr "Normal"
#: crochet/templates/crochet/pattern.html:124
#: crochet/templates/crochet/pattern.html:129
msgid "Grande"
msgstr "Large"
#: crochet/templates/crochet/pattern.html:128
#: crochet/templates/crochet/pattern.html:133
msgid "Alineación del texto"
msgstr "Text alignment"
#: crochet/templates/crochet/pattern.html:130
#: crochet/templates/crochet/pattern.html:135
msgid "Izquierda"
msgstr "Left"
#: crochet/templates/crochet/pattern.html:131
#: crochet/templates/crochet/pattern.html:136
msgid "Centrado"
msgstr "Center"
#: crochet/templates/crochet/pattern.html:132
#: crochet/templates/crochet/pattern.html:137
msgid "Justificado"
msgstr "Justify"
#: crochet/templates/crochet/pattern.html:136
#: crochet/templates/crochet/pattern.html:141
msgid "Tamaño de página"
msgstr "Page size"
#: crochet/templates/crochet/pattern.html:141
#: crochet/templates/crochet/pattern.html:146
msgid "Carta (Letter)"
msgstr "Letter"
#: crochet/templates/crochet/pattern.html:146
#: crochet/templates/crochet/pattern.html:151
msgid "Orientación"
msgstr "Orientation"
#: crochet/templates/crochet/pattern.html:148
#: crochet/templates/crochet/pattern.html:153
msgid "Vertical"
msgstr "Portrait"
#: crochet/templates/crochet/pattern.html:149
#: crochet/templates/crochet/pattern.html:154
msgid "Horizontal"
msgstr "Landscape"
#: crochet/templates/crochet/pattern.html:157
#: crochet/templates/crochet/pattern.html:165
#: crochet/templates/crochet/pattern.html:162
#: crochet/templates/crochet/pattern.html:170
msgid "Secciones"
msgstr "Sections"
#: crochet/templates/crochet/pattern.html:167
#: crochet/templates/crochet/pattern.html:172
msgid "Colapsar todo"
msgstr "Collapse all"
#: crochet/templates/crochet/pattern.html:167
#: crochet/templates/crochet/pattern.html:172
msgid "Expandir todo"
msgstr "Expand all"
#: crochet/templates/crochet/pattern.html:172
#: crochet/templates/crochet/pattern.html:177
msgid "Añadir sección"
msgstr "Add section"
#: crochet/templates/crochet/pattern.html:175
#: crochet/templates/crochet/pattern.html:180
msgid "Título"
msgstr "Title"
#: crochet/templates/crochet/pattern.html:175
#: crochet/templates/crochet/pattern.html:180
msgid "Título..."
msgstr "Title..."
#: crochet/templates/crochet/pattern.html:177
#: crochet/templates/crochet/pattern.html:182
msgid "Subtítulo"
msgstr "Subtitle"
#: crochet/templates/crochet/pattern.html:177
#: crochet/templates/crochet/pattern.html:182
msgid "Subtítulo..."
msgstr "Subtitle..."
#: crochet/templates/crochet/pattern.html:179
#: crochet/templates/crochet/pattern.html:184
msgid "Texto"
msgstr "Text"
#: crochet/templates/crochet/pattern.html:179
#: crochet/templates/crochet/pattern.html:184
msgid "Escribe aquí..."
msgstr "Write here..."
#: crochet/templates/crochet/pattern.html:181
#: crochet/templates/crochet/pattern.html:182
#: crochet/templates/crochet/pattern.html:186
#: crochet/templates/crochet/pattern.html:187
msgid "Nota"
msgstr "Note"
#: crochet/templates/crochet/pattern.html:181
#: crochet/templates/crochet/pattern.html:186
msgid "Escribe una nota o consejo..."
msgstr "Write a note or tip..."
#: crochet/templates/crochet/pattern.html:184
#: crochet/templates/crochet/pattern.html:189
msgid "Materiales"
msgstr "Materials"
#: crochet/templates/crochet/pattern.html:184
#: crochet/templates/crochet/pattern.html:189
msgid "Añadir material..."
msgstr "Add material..."
#: crochet/templates/crochet/pattern.html:184
#: crochet/templates/crochet/pattern.html:189
msgid "Añadir línea"
msgstr "Add line"
#: crochet/templates/crochet/pattern.html:186
#: crochet/templates/crochet/pattern.html:188
#: crochet/templates/crochet/pattern.html:191
#: crochet/templates/crochet/pattern.html:193
msgid "Imagen"
msgstr "Image"
#: crochet/templates/crochet/pattern.html:188
#: crochet/templates/crochet/pattern.html:193
msgid "No se ha podido subir la imagen. Inténtalo de nuevo."
msgstr "Couldn't upload the image. Please try again."
#: crochet/templates/crochet/pattern.html:190
#: crochet/templates/crochet/pattern.html:195
msgid "Patrón"
msgstr "Pattern"
#: crochet/templates/crochet/pattern.html:190
#: crochet/templates/crochet/pattern.html:195
msgid "Elementos"
msgstr "Elements"
#: crochet/templates/crochet/pattern.html:192
#: crochet/templates/crochet/pattern.html:197
msgid "Grupo"
msgstr "Group"
#: crochet/templates/crochet/pattern.html:196
#: crochet/templates/crochet/pattern.html:201
msgid "Arrastrar para reordenar"
msgstr "Drag to reorder"
#: crochet/templates/crochet/pattern.html:197
#: crochet/templates/crochet/pattern.html:202
msgid "Colapsar / expandir"
msgstr "Collapse / expand"
#: crochet/templates/crochet/pattern.html:198
#: crochet/templates/crochet/pattern.html:203
msgid "Duplicar sección"
msgstr "Duplicate section"
#: crochet/templates/crochet/pattern.html:199
#: crochet/templates/crochet/pattern.html:204
msgid "Eliminar sección"
msgstr "Delete section"
#: crochet/templates/crochet/pattern.html:200
#: crochet/templates/crochet/pattern.html:205
msgid ""
"¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las "
"secciones que contiene."
@@ -395,7 +487,7 @@ msgstr ""
"Are you sure you want to delete this group? All sections inside it will also "
"be deleted."
#: crochet/templates/crochet/pattern.html:206
#: crochet/templates/crochet/pattern.html:210
msgid "Añade una sección para ver aquí el resultado."
msgstr "Add a section to see the result here."
@@ -412,6 +504,10 @@ msgid "Entrar"
msgstr "Log in"
#: crochet/templates/registration/login.html:32
msgid "¿Has olvidado tu contraseña?"
msgstr "Forgot your password?"
#: crochet/templates/registration/login.html:35
msgid "¿No tienes cuenta? Regístrate"
msgstr "Don't have an account? Sign up"
@@ -420,6 +516,10 @@ msgstr "Don't have an account? Sign up"
msgid "Crear cuenta"
msgstr "Create account"
#: crochet/templates/registration/register.html:29
msgid "Registrarme"
msgstr "Sign up"
#: crochet/templates/registration/register.html:32
msgid "¿Ya tienes cuenta? Inicia sesión"
msgstr "Already have an account? Log in"
@@ -484,6 +584,22 @@ msgstr ""
msgid "account/logout/"
msgstr ""
#: crochet/urls.py:60
msgid "account/password-reset/"
msgstr ""
#: crochet/urls.py:75
msgid "account/password-reset/done/"
msgstr ""
#: crochet/urls.py:79
msgid "account/reset/<uidb64>/<token>/"
msgstr ""
#: crochet/urls.py:87
msgid "account/reset/done/"
msgstr ""
# crochet/views.py (PatternDetailView.EMPTY_MESSAGE)
#: crochet/views.py:24
msgid "Este patrón todavía no tiene contenido."
+182 -72
View File
@@ -2,13 +2,14 @@ msgid ""
msgstr ""
"Project-Id-Version: crochet\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-20 10:46+0000\n"
"POT-Creation-Date: 2026-07-21 10:20+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:35 crochet/templates/crochet/account_settings.html:22
#: crochet/forms.py:30 crochet/forms.py:63
#: crochet/templates/crochet/account_settings.html:22
msgid "Email"
msgstr ""
@@ -26,6 +27,7 @@ msgstr ""
#: crochet/templates/crochet/_account_password_form.html:19
#: crochet/templates/crochet/account_settings.html:29
#: crochet/templates/crochet/password_reset_confirm.html:30
msgid "Cambiar contraseña"
msgstr ""
@@ -57,13 +59,12 @@ msgid "Actualizado el %(date)s"
msgstr ""
#: crochet/templates/crochet/account_home.html:44
#: crochet/templates/crochet/pattern.html:158
#: crochet/templates/crochet/pattern.html:204
#: crochet/templates/crochet/pattern.html:163
msgid "Vista previa"
msgstr ""
#: crochet/templates/crochet/account_home.html:48
#: crochet/templates/crochet/pattern.html:50
#: crochet/templates/crochet/pattern.html:55
msgid "Eliminar"
msgstr ""
@@ -91,16 +92,12 @@ msgstr ""
#: crochet/templates/crochet/base.html:62
#: crochet/templates/crochet/home.html:24
#: crochet/templates/crochet/password_reset_complete.html:18
#: crochet/templates/registration/login.html:4
#: crochet/templates/registration/login.html:14
msgid "Iniciar sesión"
msgstr ""
#: crochet/templates/crochet/base.html:63
#: crochet/templates/registration/register.html:29
msgid "Registrarme"
msgstr ""
#: crochet/templates/crochet/home.html:4
msgid "Crochet — Crea y comparte tus patrones"
msgstr ""
@@ -153,238 +150,327 @@ msgid ""
"podrá editarlo."
msgstr ""
#: crochet/templates/crochet/pattern.html:33
#: crochet/templates/crochet/password_reset_complete.html:4
#: crochet/templates/crochet/password_reset_complete.html:14
msgid "Contraseña actualizada"
msgstr ""
#: crochet/templates/crochet/password_reset_complete.html:16
msgid "Ya puedes iniciar sesión con tu nueva contraseña."
msgstr ""
#: crochet/templates/crochet/password_reset_confirm.html:4
msgid "Elegir nueva contraseña"
msgstr ""
#: crochet/templates/crochet/password_reset_confirm.html:15
msgid "Elige una contraseña nueva"
msgstr ""
#: crochet/templates/crochet/password_reset_confirm.html:33
msgid "Enlace no válido"
msgstr ""
#: crochet/templates/crochet/password_reset_confirm.html:35
msgid ""
"El enlace para restablecer la contraseña no es válido, puede que ya se haya "
"usado. Solicita uno nuevo."
msgstr ""
#: crochet/templates/crochet/password_reset_confirm.html:38
msgid "Solicitar un enlace nuevo"
msgstr ""
#: crochet/templates/crochet/password_reset_done.html:4
#: crochet/templates/crochet/password_reset_form.html:4
#: crochet/templates/crochet/password_reset_form.html:14
msgid "Recuperar contraseña"
msgstr ""
#: crochet/templates/crochet/password_reset_done.html:14
msgid "Revisa tu email"
msgstr ""
#: crochet/templates/crochet/password_reset_done.html:16
msgid ""
"Si existe una cuenta con ese email, te hemos enviado un enlace para elegir "
"una contraseña nueva."
msgstr ""
#: crochet/templates/crochet/password_reset_done.html:19
#: crochet/templates/crochet/password_reset_form.html:32
msgid "Volver a iniciar sesión"
msgstr ""
#: crochet/templates/crochet/password_reset_email.html:2
#, python-format
msgid ""
"Has recibido este email porque alguien ha solicitado restablecer la "
"contraseña de tu cuenta en %(site_name)s."
msgstr ""
#: crochet/templates/crochet/password_reset_email.html:4
msgid "Sigue este enlace para elegir una contraseña nueva:"
msgstr ""
#: crochet/templates/crochet/password_reset_email.html:7
msgid "Tu usuario, por si lo has olvidado:"
msgstr ""
#: crochet/templates/crochet/password_reset_email.html:9
msgid "Si no has solicitado este cambio, puedes ignorar este email."
msgstr ""
#: crochet/templates/crochet/password_reset_form.html:16
msgid ""
"Escribe tu email y te enviaremos un enlace para elegir una contraseña nueva."
msgstr ""
#: crochet/templates/crochet/password_reset_form.html:29
msgid "Enviar enlace"
msgstr ""
#: crochet/templates/crochet/password_reset_subject.txt:1
#, python-format
msgid "Recupera tu contraseña en %(site_name)s"
msgstr ""
#: crochet/templates/crochet/pattern.html:26
msgid "Idioma del patrón"
msgstr ""
#: crochet/templates/crochet/pattern.html:38
msgid "No se ha podido guardar. Inténtalo de nuevo."
msgstr ""
#: crochet/templates/crochet/pattern.html:33
#: crochet/templates/crochet/pattern.html:38
msgid "Guardar"
msgstr ""
#: crochet/templates/crochet/pattern.html:36
#: crochet/templates/crochet/pattern.html:41
msgid "Ver patrón"
msgstr ""
#: crochet/templates/crochet/pattern.html:37
#: crochet/templates/crochet/pattern.html:42
msgid "Exportar a PDF"
msgstr ""
#: crochet/templates/crochet/pattern.html:44
#: crochet/templates/crochet/pattern.html:49
msgid "Más opciones"
msgstr ""
#: crochet/templates/crochet/pattern.html:60
#: crochet/templates/crochet/pattern.html:65
msgid "Guardado"
msgstr ""
#: crochet/templates/crochet/pattern.html:67
#: crochet/templates/crochet/pattern.html:72
msgid "Personalización de página"
msgstr ""
#: crochet/templates/crochet/pattern.html:71
#: crochet/templates/crochet/pattern.html:76
msgid "Título del patrón"
msgstr ""
#: crochet/templates/crochet/pattern.html:72
#: crochet/templates/crochet/pattern.html:77
msgid "Título del patrón..."
msgstr ""
#: crochet/templates/crochet/pattern.html:75
#: crochet/templates/crochet/pattern.html:80
msgid "Autor"
msgstr ""
#: crochet/templates/crochet/pattern.html:76
#: crochet/templates/crochet/pattern.html:81
msgid "Autor..."
msgstr ""
#: crochet/templates/crochet/pattern.html:81
#: crochet/templates/crochet/pattern.html:86
msgid "Imagen de portada"
msgstr ""
#: crochet/templates/crochet/pattern.html:84
#: crochet/templates/crochet/pattern.html:89
msgid "No se ha podido subir la imagen de portada. Inténtalo de nuevo."
msgstr ""
#: crochet/templates/crochet/pattern.html:98
#: crochet/templates/crochet/pattern.html:182
#: crochet/templates/crochet/pattern.html:103
#: crochet/templates/crochet/pattern.html:187
msgid "Color del texto"
msgstr ""
#: crochet/templates/crochet/pattern.html:102
#: crochet/templates/crochet/pattern.html:107
msgid "Color de acento (títulos)"
msgstr ""
#: crochet/templates/crochet/pattern.html:106
#: crochet/templates/crochet/pattern.html:182
#: crochet/templates/crochet/pattern.html:111
#: crochet/templates/crochet/pattern.html:187
msgid "Color de fondo"
msgstr ""
#: crochet/templates/crochet/pattern.html:110
#: crochet/templates/crochet/pattern.html:115
msgid "Tipografía"
msgstr ""
#: crochet/templates/crochet/pattern.html:116
#: crochet/templates/crochet/pattern.html:121
msgid "Monoespaciada"
msgstr ""
#: crochet/templates/crochet/pattern.html:120
#: crochet/templates/crochet/pattern.html:125
msgid "Tamaño de letra"
msgstr ""
#: crochet/templates/crochet/pattern.html:122
#: crochet/templates/crochet/pattern.html:127
msgid "Pequeño"
msgstr ""
#: crochet/templates/crochet/pattern.html:123
#: crochet/templates/crochet/pattern.html:128
msgid "Normal"
msgstr ""
#: crochet/templates/crochet/pattern.html:124
#: crochet/templates/crochet/pattern.html:129
msgid "Grande"
msgstr ""
#: crochet/templates/crochet/pattern.html:128
#: crochet/templates/crochet/pattern.html:133
msgid "Alineación del texto"
msgstr ""
#: crochet/templates/crochet/pattern.html:130
#: crochet/templates/crochet/pattern.html:135
msgid "Izquierda"
msgstr ""
#: crochet/templates/crochet/pattern.html:131
#: crochet/templates/crochet/pattern.html:136
msgid "Centrado"
msgstr ""
#: crochet/templates/crochet/pattern.html:132
#: crochet/templates/crochet/pattern.html:137
msgid "Justificado"
msgstr ""
#: crochet/templates/crochet/pattern.html:136
#: crochet/templates/crochet/pattern.html:141
msgid "Tamaño de página"
msgstr ""
#: crochet/templates/crochet/pattern.html:141
#: crochet/templates/crochet/pattern.html:146
msgid "Carta (Letter)"
msgstr ""
#: crochet/templates/crochet/pattern.html:146
#: crochet/templates/crochet/pattern.html:151
msgid "Orientación"
msgstr ""
#: crochet/templates/crochet/pattern.html:148
#: crochet/templates/crochet/pattern.html:153
msgid "Vertical"
msgstr ""
#: crochet/templates/crochet/pattern.html:149
#: crochet/templates/crochet/pattern.html:154
msgid "Horizontal"
msgstr ""
#: crochet/templates/crochet/pattern.html:157
#: crochet/templates/crochet/pattern.html:165
#: crochet/templates/crochet/pattern.html:162
#: crochet/templates/crochet/pattern.html:170
msgid "Secciones"
msgstr ""
#: crochet/templates/crochet/pattern.html:167
#: crochet/templates/crochet/pattern.html:172
msgid "Colapsar todo"
msgstr ""
#: crochet/templates/crochet/pattern.html:167
#: crochet/templates/crochet/pattern.html:172
msgid "Expandir todo"
msgstr ""
#: crochet/templates/crochet/pattern.html:172
#: crochet/templates/crochet/pattern.html:177
msgid "Añadir sección"
msgstr ""
#: crochet/templates/crochet/pattern.html:175
#: crochet/templates/crochet/pattern.html:180
msgid "Título"
msgstr ""
#: crochet/templates/crochet/pattern.html:175
#: crochet/templates/crochet/pattern.html:180
msgid "Título..."
msgstr ""
#: crochet/templates/crochet/pattern.html:177
#: crochet/templates/crochet/pattern.html:182
msgid "Subtítulo"
msgstr ""
#: crochet/templates/crochet/pattern.html:177
#: crochet/templates/crochet/pattern.html:182
msgid "Subtítulo..."
msgstr ""
#: crochet/templates/crochet/pattern.html:179
#: crochet/templates/crochet/pattern.html:184
msgid "Texto"
msgstr ""
#: crochet/templates/crochet/pattern.html:179
#: crochet/templates/crochet/pattern.html:184
msgid "Escribe aquí..."
msgstr ""
#: crochet/templates/crochet/pattern.html:181
#: crochet/templates/crochet/pattern.html:182
#: crochet/templates/crochet/pattern.html:186
#: crochet/templates/crochet/pattern.html:187
msgid "Nota"
msgstr ""
#: crochet/templates/crochet/pattern.html:181
#: crochet/templates/crochet/pattern.html:186
msgid "Escribe una nota o consejo..."
msgstr ""
#: crochet/templates/crochet/pattern.html:184
#: crochet/templates/crochet/pattern.html:189
msgid "Materiales"
msgstr ""
#: crochet/templates/crochet/pattern.html:184
#: crochet/templates/crochet/pattern.html:189
msgid "Añadir material..."
msgstr ""
#: crochet/templates/crochet/pattern.html:184
#: crochet/templates/crochet/pattern.html:189
msgid "Añadir línea"
msgstr ""
#: crochet/templates/crochet/pattern.html:186
#: crochet/templates/crochet/pattern.html:188
#: crochet/templates/crochet/pattern.html:191
#: crochet/templates/crochet/pattern.html:193
msgid "Imagen"
msgstr ""
#: crochet/templates/crochet/pattern.html:188
#: crochet/templates/crochet/pattern.html:193
msgid "No se ha podido subir la imagen. Inténtalo de nuevo."
msgstr ""
#: crochet/templates/crochet/pattern.html:190
#: crochet/templates/crochet/pattern.html:195
msgid "Patrón"
msgstr ""
#: crochet/templates/crochet/pattern.html:190
#: crochet/templates/crochet/pattern.html:195
msgid "Elementos"
msgstr ""
#: crochet/templates/crochet/pattern.html:192
#: crochet/templates/crochet/pattern.html:197
msgid "Grupo"
msgstr ""
#: crochet/templates/crochet/pattern.html:196
#: crochet/templates/crochet/pattern.html:201
msgid "Arrastrar para reordenar"
msgstr ""
#: crochet/templates/crochet/pattern.html:197
#: crochet/templates/crochet/pattern.html:202
msgid "Colapsar / expandir"
msgstr ""
#: crochet/templates/crochet/pattern.html:198
#: crochet/templates/crochet/pattern.html:203
msgid "Duplicar sección"
msgstr ""
#: crochet/templates/crochet/pattern.html:199
#: crochet/templates/crochet/pattern.html:204
msgid "Eliminar sección"
msgstr ""
#: crochet/templates/crochet/pattern.html:200
#: crochet/templates/crochet/pattern.html:205
msgid ""
"¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las "
"secciones que contiene."
msgstr ""
#: crochet/templates/crochet/pattern.html:206
#: crochet/templates/crochet/pattern.html:210
msgid "Añade una sección para ver aquí el resultado."
msgstr ""
@@ -401,6 +487,10 @@ msgid "Entrar"
msgstr ""
#: crochet/templates/registration/login.html:32
msgid "¿Has olvidado tu contraseña?"
msgstr ""
#: crochet/templates/registration/login.html:35
msgid "¿No tienes cuenta? Regístrate"
msgstr ""
@@ -409,6 +499,10 @@ msgstr ""
msgid "Crear cuenta"
msgstr ""
#: crochet/templates/registration/register.html:29
msgid "Registrarme"
msgstr ""
#: crochet/templates/registration/register.html:32
msgid "¿Ya tienes cuenta? Inicia sesión"
msgstr ""
@@ -476,6 +570,22 @@ msgstr "cuenta/entrar/"
msgid "account/logout/"
msgstr "cuenta/salir/"
#: crochet/urls.py:60
msgid "account/password-reset/"
msgstr "cuenta/recuperar-contrasena/"
#: crochet/urls.py:75
msgid "account/password-reset/done/"
msgstr "cuenta/recuperar-contrasena/enviado/"
#: crochet/urls.py:79
msgid "account/reset/<uidb64>/<token>/"
msgstr "cuenta/restablecer/<uidb64>/<token>/"
#: crochet/urls.py:87
msgid "account/reset/done/"
msgstr "cuenta/restablecer/hecho/"
#: crochet/views.py:24
msgid "Este patrón todavía no tiene contenido."
msgstr ""
@@ -0,0 +1,22 @@
{% extends 'crochet/base.html' %}
{% load i18n %}
{% block title %}{% trans 'Contraseña actualizada' %}{% endblock %}
{% block extra_css %}
{% include 'crochet/_platform_assets.html' %}
{% endblock %}
{% block content %}
<main class="flex justify-center px-4 py-12">
<div class="card w-full max-w-sm bg-base-100 shadow-xl border border-base-300">
<div class="card-body">
<h1 class="card-title">{% trans 'Contraseña actualizada' %}</h1>
<p class="text-sm text-base-content/70">
{% trans 'Ya puedes iniciar sesión con tu nueva contraseña.' %}
</p>
<a href="{% url 'crochet:login' %}" class="btn btn-primary mt-2">{% trans 'Iniciar sesión' %}</a>
</div>
</div>
</main>
{% endblock %}
@@ -0,0 +1,44 @@
{% extends 'crochet/base.html' %}
{% load i18n %}
{% block title %}{% trans 'Elegir nueva contraseña' %}{% endblock %}
{% block extra_css %}
{% include 'crochet/_platform_assets.html' %}
{% endblock %}
{% block content %}
<main class="flex justify-center px-4 py-12">
<div class="card w-full max-w-sm bg-base-100 shadow-xl border border-base-300">
<div class="card-body">
{% if validlink %}
<h1 class="card-title">{% trans 'Elige una contraseña nueva' %}</h1>
<form method="post" class="flex flex-col gap-3">
{% csrf_token %}
{% for field in form %}
<label class="flex flex-col gap-1">
<span class="text-sm">{{ field.label }}</span>
{{ field }}
{% for error in field.errors %}
<span class="text-error text-xs">{{ error }}</span>
{% endfor %}
{% if field.help_text %}
<span class="text-xs text-base-content/60">{{ field.help_text }}</span>
{% endif %}
</label>
{% endfor %}
<button type="submit" class="btn btn-primary mt-2">{% trans 'Cambiar contraseña' %}</button>
</form>
{% else %}
<h1 class="card-title">{% trans 'Enlace no válido' %}</h1>
<p class="text-sm text-base-content/70">
{% trans 'El enlace para restablecer la contraseña no es válido, puede que ya se haya usado. Solicita uno nuevo.' %}
</p>
<p class="text-sm text-center mt-2">
<a href="{% url 'crochet:password_reset' %}" class="link link-primary">{% trans 'Solicitar un enlace nuevo' %}</a>
</p>
{% endif %}
</div>
</div>
</main>
{% endblock %}
@@ -0,0 +1,24 @@
{% extends 'crochet/base.html' %}
{% load i18n %}
{% block title %}{% trans 'Recuperar contraseña' %}{% endblock %}
{% block extra_css %}
{% include 'crochet/_platform_assets.html' %}
{% endblock %}
{% block content %}
<main class="flex justify-center px-4 py-12">
<div class="card w-full max-w-sm bg-base-100 shadow-xl border border-base-300">
<div class="card-body">
<h1 class="card-title">{% trans 'Revisa tu email' %}</h1>
<p class="text-sm text-base-content/70">
{% trans 'Si existe una cuenta con ese email, te hemos enviado un enlace para elegir una contraseña nueva.' %}
</p>
<p class="text-sm text-center mt-2">
<a href="{% url 'crochet:login' %}" class="link link-primary">{% trans 'Volver a iniciar sesión' %}</a>
</p>
</div>
</div>
</main>
{% endblock %}
@@ -0,0 +1,10 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}Has recibido este email porque alguien ha solicitado restablecer la contraseña de tu cuenta en {{ site_name }}.{% endblocktrans %}
{% trans 'Sigue este enlace para elegir una contraseña nueva:' %}
{{ protocol }}://{{ domain }}{% url 'crochet:password_reset_confirm' uidb64=uid token=token %}
{% trans 'Tu usuario, por si lo has olvidado:' %} {{ user.get_username }}
{% trans 'Si no has solicitado este cambio, puedes ignorar este email.' %}
{% endautoescape %}
@@ -0,0 +1,37 @@
{% extends 'crochet/base.html' %}
{% load i18n %}
{% block title %}{% trans 'Recuperar contraseña' %}{% endblock %}
{% block extra_css %}
{% include 'crochet/_platform_assets.html' %}
{% endblock %}
{% block content %}
<main class="flex justify-center px-4 py-12">
<div class="card w-full max-w-sm bg-base-100 shadow-xl border border-base-300">
<div class="card-body">
<h1 class="card-title">{% trans 'Recuperar contraseña' %}</h1>
<p class="text-sm text-base-content/70">
{% trans 'Escribe tu email y te enviaremos un enlace para elegir una contraseña nueva.' %}
</p>
<form method="post" class="flex flex-col gap-3">
{% csrf_token %}
{% for field in form %}
<label class="flex flex-col gap-1">
<span class="text-sm">{{ field.label }}</span>
{{ field }}
{% for error in field.errors %}
<span class="text-error text-xs">{{ error }}</span>
{% endfor %}
</label>
{% endfor %}
<button type="submit" class="btn btn-primary mt-2">{% trans 'Enviar enlace' %}</button>
</form>
<p class="text-sm text-center mt-2">
<a href="{% url 'crochet:login' %}" class="link link-primary">{% trans 'Volver a iniciar sesión' %}</a>
</p>
</div>
</div>
</main>
{% endblock %}
@@ -0,0 +1 @@
{% load i18n %}{% blocktrans %}Recupera tu contraseña en {{ site_name }}{% endblocktrans %}
@@ -29,6 +29,9 @@
<button type="submit" class="btn btn-primary mt-2">{% trans 'Entrar' %}</button>
</form>
<p class="text-sm text-center mt-2">
<a href="{% url 'crochet:password_reset' %}" class="link link-primary">{% trans '¿Has olvidado tu contraseña?' %}</a>
</p>
<p class="text-sm text-center">
<a href="{% url 'crochet:register' %}" class="link link-primary">{% trans '¿No tienes cuenta? Regístrate' %}</a>
</p>
</div>
+16 -1
View File
@@ -20,17 +20,20 @@ class RegisterViewTests(TestCase):
def test_post_creates_user_and_logs_in(self):
response = self.client.post(self.url, {
'username': 'crocheter',
'email': 'crocheter@example.com',
'password1': 'a-very-uncommon-pw-1',
'password2': 'a-very-uncommon-pw-1',
})
self.assertTrue(User.objects.filter(username='crocheter').exists())
user = User.objects.get(username='crocheter')
self.assertEqual(user.email, 'crocheter@example.com')
self.assertRedirects(response, reverse('crochet:account_home'))
self.assertIn('_auth_user_id', self.client.session)
def test_post_with_mismatched_passwords_does_not_create_user(self):
response = self.client.post(self.url, {
'username': 'crocheter',
'email': 'crocheter@example.com',
'password1': 'a-very-uncommon-pw-1',
'password2': 'something-else',
})
@@ -38,6 +41,18 @@ class RegisterViewTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertFalse(User.objects.filter(username='crocheter').exists())
def test_post_without_email_does_not_create_user(self):
# Sin email no habría a dónde mandar el enlace de recuperación de
# contraseña (ver PasswordResetForm/StyledUserCreationForm).
response = self.client.post(self.url, {
'username': 'crocheter',
'password1': 'a-very-uncommon-pw-1',
'password2': 'a-very-uncommon-pw-1',
})
self.assertEqual(response.status_code, 200)
self.assertFalse(User.objects.filter(username='crocheter').exists())
class LoginViewTests(TestCase):
def setUp(self):
+134
View File
@@ -0,0 +1,134 @@
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.core import mail
from django.test import TestCase
from django.urls import reverse
from django.utils import translation
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
class PasswordResetRequestViewTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='crocheter', password='the-old-password-1', email='crocheter@example.com',
)
with translation.override('es'):
self.url = reverse('crochet:password_reset')
def test_get_renders_the_form(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'crochet/password_reset_form.html')
def test_known_email_sends_a_reset_link(self):
response = self.client.post(self.url, {'email': 'crocheter@example.com'})
with translation.override('es'):
done_url = reverse('crochet:password_reset_done')
self.assertRedirects(response, done_url)
self.assertEqual(len(mail.outbox), 1)
self.assertIn('crocheter@example.com', mail.outbox[0].to)
self.assertIn('http://testserver', mail.outbox[0].body)
def test_unknown_email_does_not_send_anything_but_still_redirects(self):
# Ni la vista ni la plantilla deben delatar si el email existe o no
# en el sistema (comportamiento por defecto de PasswordResetForm):
# misma redirección tanto si existe como si no, para no permitir
# enumerar cuentas registradas probando emails.
response = self.client.post(self.url, {'email': 'nobody@example.com'})
with translation.override('es'):
done_url = reverse('crochet:password_reset_done')
self.assertRedirects(response, done_url)
self.assertEqual(len(mail.outbox), 0)
class PasswordResetDoneViewTests(TestCase):
def test_get_renders_the_confirmation_message(self):
with translation.override('es'):
url = reverse('crochet:password_reset_done')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'crochet/password_reset_done.html')
class PasswordResetConfirmViewTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='crocheter', password='the-old-password-1', email='crocheter@example.com',
)
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
token = default_token_generator.make_token(self.user)
with translation.override('es'):
self.confirm_url = reverse(
'crochet:password_reset_confirm', kwargs={'uidb64': uidb64, 'token': token},
)
def test_valid_link_allows_setting_a_new_password(self):
# PasswordResetConfirmView consume el token de la URL en el primer
# GET (redirige a una URL "set-password" en sesión) precisamente
# para que no quede reutilizable ni visible en el historial del
# navegador; hay que seguir esa redirección antes de poder enviar
# el formulario.
get_response = self.client.get(self.confirm_url, follow=True)
self.assertContains(get_response, 'id_new_password1')
response = self.client.post(get_response.redirect_chain[-1][0], {
'new_password1': 'the-new-password-2',
'new_password2': 'the-new-password-2',
})
with translation.override('es'):
complete_url = reverse('crochet:password_reset_complete')
self.assertRedirects(response, complete_url)
self.user.refresh_from_db()
self.assertTrue(self.user.check_password('the-new-password-2'))
def test_mismatched_passwords_do_not_change_the_password(self):
get_response = self.client.get(self.confirm_url, follow=True)
response = self.client.post(get_response.redirect_chain[-1][0], {
'new_password1': 'the-new-password-2',
'new_password2': 'something-else-3',
})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'class="text-error text-xs"')
self.user.refresh_from_db()
self.assertTrue(self.user.check_password('the-old-password-1'))
def test_invalid_token_shows_the_invalid_link_message(self):
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
with translation.override('es'):
bogus_url = reverse(
'crochet:password_reset_confirm', kwargs={'uidb64': uidb64, 'token': 'not-a-real-token'},
)
response = self.client.get(bogus_url, follow=True)
self.assertContains(response, 'Enlace no válido')
self.assertNotContains(response, 'id_new_password1')
class PasswordResetCompleteViewTests(TestCase):
def test_get_renders_the_success_message(self):
with translation.override('es'):
url = reverse('crochet:password_reset_complete')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'crochet/password_reset_complete.html')
class LoginPageForgotPasswordLinkTests(TestCase):
def test_login_page_links_to_password_reset(self):
with translation.override('es'):
login_url = reverse('crochet:login')
reset_url = reverse('crochet:password_reset')
response = self.client.get(login_url)
self.assertContains(response, f'href="{reset_url}"')
+33 -2
View File
@@ -1,8 +1,8 @@
from django.contrib.auth import views as auth_views
from django.urls import path
from django.urls import path, reverse_lazy
from django.utils.translation import gettext_lazy as _
from crochet.forms import StyledAuthenticationForm
from crochet.forms import StyledAuthenticationForm, StyledPasswordResetForm, StyledSetPasswordForm
from crochet.views import (
AccountEmailUpdateView,
AccountHomeView,
@@ -56,4 +56,35 @@ urlpatterns = [
),
),
path(_('account/logout/'), name='logout', view=auth_views.LogoutView.as_view()),
path(
_('account/password-reset/'), name='password_reset',
view=auth_views.PasswordResetView.as_view(
# No en registration/ (a diferencia de login.html/register.html):
# django.contrib.admin trae sus propias plantillas con estos
# mismos nombres bajo registration/ y, al ir antes que crochet
# en INSTALLED_APPS, el buscador de plantillas por APP_DIRS
# encontraría siempre las suyas primero, tapando las nuestras.
template_name='crochet/password_reset_form.html',
email_template_name='crochet/password_reset_email.html',
subject_template_name='crochet/password_reset_subject.txt',
form_class=StyledPasswordResetForm,
success_url=reverse_lazy('crochet:password_reset_done'),
),
),
path(
_('account/password-reset/done/'), name='password_reset_done',
view=auth_views.PasswordResetDoneView.as_view(template_name='crochet/password_reset_done.html'),
),
path(
_('account/reset/<uidb64>/<token>/'), name='password_reset_confirm',
view=auth_views.PasswordResetConfirmView.as_view(
template_name='crochet/password_reset_confirm.html',
form_class=StyledSetPasswordForm,
success_url=reverse_lazy('crochet:password_reset_complete'),
),
),
path(
_('account/reset/done/'), name='password_reset_complete',
view=auth_views.PasswordResetCompleteView.as_view(template_name='crochet/password_reset_complete.html'),
),
]