feat: added delete account and new display name
This commit is contained in:
@@ -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'
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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())
|
||||
|
||||
+35
-12
@@ -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')
|
||||
@@ -166,4 +171,22 @@ class PatternCreateViewTests(TestCase):
|
||||
self.assertEqual(pattern.created_by, user)
|
||||
with translation.override('es'):
|
||||
edit_url = reverse('crochet:pattern_edit', args=[pattern.uuid])
|
||||
self.assertRedirects(response, edit_url)
|
||||
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)
|
||||
@@ -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
@@ -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
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user