From 73109a268b40c333aa3c7519041d4ffaba29fcc6 Mon Sep 17 00:00:00 2001 From: Pablo Moreno Date: Mon, 20 Jul 2026 11:21:29 +0200 Subject: [PATCH] feat: improved UX --- .gitea/workflows/test-build.yaml | 9 +- crochet/forms.py | 23 +- crochet/locale/en/LC_MESSAGES/django.po | 212 +++++++++++------- crochet/locale/es/LC_MESSAGES/django.po | 208 ++++++++++------- .../crochet/_account_email_form.html | 21 ++ .../crochet/_account_password_form.html | 20 ++ .../templates/crochet/_platform_assets.html | 7 +- crochet/templates/crochet/account_home.html | 5 - .../templates/crochet/account_settings.html | 36 +++ crochet/templates/crochet/base.html | 35 ++- crochet/templates/crochet/pattern.html | 3 +- crochet/templatetags/__init__.py | 0 crochet/templatetags/crochet_i18n.py | 15 ++ crochet/tests/test_account_settings.py | 142 ++++++++++++ crochet/tests/test_views.py | 47 ++++ crochet/urls.py | 9 + crochet/views.py | 57 ++++- 17 files changed, 657 insertions(+), 192 deletions(-) create mode 100644 crochet/templates/crochet/_account_email_form.html create mode 100644 crochet/templates/crochet/_account_password_form.html create mode 100644 crochet/templates/crochet/account_settings.html create mode 100644 crochet/templatetags/__init__.py create mode 100644 crochet/templatetags/crochet_i18n.py create mode 100644 crochet/tests/test_account_settings.py diff --git a/.gitea/workflows/test-build.yaml b/.gitea/workflows/test-build.yaml index 6c5f196..cb81495 100644 --- a/.gitea/workflows/test-build.yaml +++ b/.gitea/workflows/test-build.yaml @@ -20,13 +20,6 @@ jobs: git checkout ${{ gitea.sha }} - name: Install system dependencies - # libpango/libpangocairo + fuentes: las necesita WeasyPrint en - # tiempo de ejecución para generar el PDF del patrón (ver - # PatternPdfView y crochet/tests/test_pdf.py); sin fuentes - # instaladas, Pango no tiene nada que medir/dibujar y WeasyPrint - # termina haciendo segfault en vez de fallar con un error legible - # (mismo motivo por el que el Dockerfile instala pango + ttf-dejavu - # en Alpine). run: | apt-get update -qq apt-get install -y --no-install-recommends libpango-1.0-0 libpangocairo-1.0-0 fonts-dejavu-core gettext @@ -41,7 +34,7 @@ jobs: run: uv run python manage.py compilemessages - name: Run tests - run: uv run pytest + run: uv run pytest --cov build: runs-on: ubuntu-latest diff --git a/crochet/forms.py b/crochet/forms.py index e338dcd..5c0e56a 100644 --- a/crochet/forms.py +++ b/crochet/forms.py @@ -1,4 +1,6 @@ -from django.contrib.auth.forms import AuthenticationForm, UserCreationForm +from django import forms +from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, UserCreationForm +from django.utils.translation import gettext_lazy as _ # Los formularios de auth de Django no traen ninguna clase CSS en sus # widgets (piensan en HTML sin estilar): se añade aquí la clase de daisyUI @@ -20,3 +22,22 @@ class StyledUserCreationForm(UserCreationForm): super().__init__(*args, **kwargs) for field in self.fields.values(): field.widget.attrs['class'] = INPUT_CLASSES + + +class StyledPasswordChangeForm(PasswordChangeForm): + 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})) + + def __init__(self, *args, user, **kwargs): + self.user = user + super().__init__(*args, **kwargs) + + def save(self): + self.user.email = self.cleaned_data['email'] + self.user.save(update_fields=['email']) diff --git a/crochet/locale/en/LC_MESSAGES/django.po b/crochet/locale/en/LC_MESSAGES/django.po index f645701..60fce59 100644 --- a/crochet/locale/en/LC_MESSAGES/django.po +++ b/crochet/locale/en/LC_MESSAGES/django.po @@ -2,12 +2,33 @@ msgid "" msgstr "" "Project-Id-Version: crochet\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-17 11:52+0000\n" +"POT-Creation-Date: 2026-07-20 09:18+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 +msgid "Email" +msgstr "Email" + +#: crochet/templates/crochet/_account_email_form.html:10 +msgid "Email actualizado." +msgstr "Email updated." + +#: crochet/templates/crochet/_account_email_form.html:20 +msgid "Guardar email" +msgstr "Save email" + +#: crochet/templates/crochet/_account_password_form.html:7 +msgid "Contraseña actualizada." +msgstr "Password updated." + +#: crochet/templates/crochet/_account_password_form.html:19 +#: crochet/templates/crochet/account_settings.html:29 +msgid "Cambiar contraseña" +msgstr "Change password" + #: crochet/templates/crochet/account_home.html:4 msgid "Mis patrones" msgstr "My patterns" @@ -22,7 +43,7 @@ msgid "Crear patrón" msgstr "Create pattern" #: crochet/templates/crochet/account_home.html:21 -#: crochet/templates/crochet/pattern.html:21 +#: crochet/templates/crochet/pattern.html:20 msgid "¿Seguro que quieres eliminar este patrón? No podrás deshacerlo." msgstr "Are you sure you want to delete this pattern? This cannot be undone." @@ -36,7 +57,7 @@ msgid "Actualizado el %(date)s" msgstr "Updated on %(date)s" #: crochet/templates/crochet/account_home.html:40 -#: crochet/templates/crochet/pattern.html:34 +#: crochet/templates/crochet/pattern.html:33 msgid "Ver patrón" msgstr "View pattern" @@ -52,19 +73,32 @@ msgstr "Delete" msgid "Todavía no tienes ningún patrón." msgstr "You don't have any patterns yet." -#: crochet/templates/crochet/account_home.html:60 -#: crochet/templates/crochet/base.html:34 +#: crochet/templates/crochet/account_settings.html:4 +#: crochet/templates/crochet/account_settings.html:18 +#: crochet/templates/crochet/base.html:50 +msgid "Ajustes de la cuenta" +msgstr "Account settings" + +#: crochet/templates/crochet/account_settings.html:34 +msgid "Volver a mis patrones" +msgstr "Back to my patterns" + +#: crochet/templates/crochet/base.html:39 +msgid "Idioma" +msgstr "Language" + +#: crochet/templates/crochet/base.html:54 msgid "Cerrar sesión" msgstr "Log out" -#: crochet/templates/crochet/base.html:37 +#: crochet/templates/crochet/base.html:60 #: crochet/templates/crochet/home.html:24 #: crochet/templates/registration/login.html:4 #: crochet/templates/registration/login.html:14 msgid "Iniciar sesión" msgstr "Log in" -#: crochet/templates/crochet/base.html:38 +#: crochet/templates/crochet/base.html:61 #: crochet/templates/registration/register.html:29 msgid "Registrarme" msgstr "Sign up" @@ -114,8 +148,8 @@ msgid "" "Descarga tu patrón listo para imprimir, con tu propia portada, colores y " "tipografía." msgstr "" -"Download your pattern ready to print, with your own cover image, colors, " -"and typography." +"Download your pattern ready to print, with your own cover image, colors, and " +"typography." #: crochet/templates/crochet/home.html:53 msgid "Comparte con un enlace" @@ -130,233 +164,233 @@ msgstr "" "to edit it." # crochet/templates/crochet/pattern.html -#: crochet/templates/crochet/pattern.html:31 +#: crochet/templates/crochet/pattern.html:30 msgid "No se ha podido guardar. Inténtalo de nuevo." msgstr "Couldn't save. Please try again." -#: crochet/templates/crochet/pattern.html:31 +#: crochet/templates/crochet/pattern.html:30 msgid "Guardar" msgstr "Save" -#: crochet/templates/crochet/pattern.html:35 +#: crochet/templates/crochet/pattern.html:34 msgid "Exportar a PDF" msgstr "Export to PDF" -#: crochet/templates/crochet/pattern.html:39 +#: crochet/templates/crochet/pattern.html:38 msgid "Eliminar patrón" msgstr "Delete pattern" -#: crochet/templates/crochet/pattern.html:46 +#: crochet/templates/crochet/pattern.html:45 msgid "Guardado" msgstr "Saved" -#: crochet/templates/crochet/pattern.html:53 +#: crochet/templates/crochet/pattern.html:52 msgid "Personalización de página" msgstr "Page customization" -#: crochet/templates/crochet/pattern.html:57 +#: crochet/templates/crochet/pattern.html:56 msgid "Título del patrón" msgstr "Pattern title" -#: crochet/templates/crochet/pattern.html:58 +#: crochet/templates/crochet/pattern.html:57 msgid "Título del patrón..." msgstr "Pattern title..." -#: crochet/templates/crochet/pattern.html:61 +#: crochet/templates/crochet/pattern.html:60 msgid "Autor" msgstr "Author" -#: crochet/templates/crochet/pattern.html:62 +#: crochet/templates/crochet/pattern.html:61 msgid "Autor..." msgstr "Author..." -#: crochet/templates/crochet/pattern.html:67 +#: crochet/templates/crochet/pattern.html:66 msgid "Imagen de portada" msgstr "Cover image" -#: crochet/templates/crochet/pattern.html:70 +#: crochet/templates/crochet/pattern.html:69 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:84 -#: crochet/templates/crochet/pattern.html:168 +#: crochet/templates/crochet/pattern.html:83 +#: crochet/templates/crochet/pattern.html:167 msgid "Color del texto" msgstr "Text color" -#: crochet/templates/crochet/pattern.html:88 +#: crochet/templates/crochet/pattern.html:87 msgid "Color de acento (títulos)" msgstr "Accent color (headings)" -#: crochet/templates/crochet/pattern.html:92 -#: crochet/templates/crochet/pattern.html:168 +#: crochet/templates/crochet/pattern.html:91 +#: crochet/templates/crochet/pattern.html:167 msgid "Color de fondo" msgstr "Background color" -#: crochet/templates/crochet/pattern.html:96 +#: crochet/templates/crochet/pattern.html:95 msgid "Tipografía" msgstr "Font" -#: crochet/templates/crochet/pattern.html:102 +#: crochet/templates/crochet/pattern.html:101 msgid "Monoespaciada" msgstr "Monospace" -#: crochet/templates/crochet/pattern.html:106 +#: crochet/templates/crochet/pattern.html:105 msgid "Tamaño de letra" msgstr "Font size" -#: crochet/templates/crochet/pattern.html:108 +#: crochet/templates/crochet/pattern.html:107 msgid "Pequeño" msgstr "Small" -#: crochet/templates/crochet/pattern.html:109 +#: crochet/templates/crochet/pattern.html:108 msgid "Normal" msgstr "Normal" -#: crochet/templates/crochet/pattern.html:110 +#: crochet/templates/crochet/pattern.html:109 msgid "Grande" msgstr "Large" -#: crochet/templates/crochet/pattern.html:114 +#: crochet/templates/crochet/pattern.html:113 msgid "Alineación del texto" msgstr "Text alignment" -#: crochet/templates/crochet/pattern.html:116 +#: crochet/templates/crochet/pattern.html:115 msgid "Izquierda" msgstr "Left" -#: crochet/templates/crochet/pattern.html:117 +#: crochet/templates/crochet/pattern.html:116 msgid "Centrado" msgstr "Center" -#: crochet/templates/crochet/pattern.html:118 +#: crochet/templates/crochet/pattern.html:117 msgid "Justificado" msgstr "Justify" -#: crochet/templates/crochet/pattern.html:122 +#: crochet/templates/crochet/pattern.html:121 msgid "Tamaño de página" msgstr "Page size" -#: crochet/templates/crochet/pattern.html:127 +#: crochet/templates/crochet/pattern.html:126 msgid "Carta (Letter)" msgstr "Letter" -#: crochet/templates/crochet/pattern.html:132 +#: crochet/templates/crochet/pattern.html:131 msgid "Orientación" msgstr "Orientation" -#: crochet/templates/crochet/pattern.html:134 +#: crochet/templates/crochet/pattern.html:133 msgid "Vertical" msgstr "Portrait" -#: crochet/templates/crochet/pattern.html:135 +#: crochet/templates/crochet/pattern.html:134 msgid "Horizontal" msgstr "Landscape" -#: crochet/templates/crochet/pattern.html:143 -#: crochet/templates/crochet/pattern.html:151 +#: crochet/templates/crochet/pattern.html:142 +#: crochet/templates/crochet/pattern.html:150 msgid "Secciones" msgstr "Sections" -#: crochet/templates/crochet/pattern.html:144 -#: crochet/templates/crochet/pattern.html:190 +#: crochet/templates/crochet/pattern.html:143 +#: crochet/templates/crochet/pattern.html:189 msgid "Vista previa" msgstr "Preview" -#: crochet/templates/crochet/pattern.html:153 +#: crochet/templates/crochet/pattern.html:152 msgid "Colapsar todo" msgstr "Collapse all" -#: crochet/templates/crochet/pattern.html:153 +#: crochet/templates/crochet/pattern.html:152 msgid "Expandir todo" msgstr "Expand all" -#: crochet/templates/crochet/pattern.html:158 +#: crochet/templates/crochet/pattern.html:157 msgid "Añadir sección" msgstr "Add section" -#: crochet/templates/crochet/pattern.html:161 +#: crochet/templates/crochet/pattern.html:160 msgid "Título" msgstr "Title" -#: crochet/templates/crochet/pattern.html:161 +#: crochet/templates/crochet/pattern.html:160 msgid "Título..." msgstr "Title..." -#: crochet/templates/crochet/pattern.html:163 +#: crochet/templates/crochet/pattern.html:162 msgid "Subtítulo" msgstr "Subtitle" -#: crochet/templates/crochet/pattern.html:163 +#: crochet/templates/crochet/pattern.html:162 msgid "Subtítulo..." msgstr "Subtitle..." -#: crochet/templates/crochet/pattern.html:165 +#: crochet/templates/crochet/pattern.html:164 msgid "Texto" msgstr "Text" -#: crochet/templates/crochet/pattern.html:165 +#: crochet/templates/crochet/pattern.html:164 msgid "Escribe aquí..." msgstr "Write here..." +#: crochet/templates/crochet/pattern.html:166 #: crochet/templates/crochet/pattern.html:167 -#: crochet/templates/crochet/pattern.html:168 msgid "Nota" msgstr "Note" -#: crochet/templates/crochet/pattern.html:167 +#: crochet/templates/crochet/pattern.html:166 msgid "Escribe una nota o consejo..." msgstr "Write a note or tip..." -#: crochet/templates/crochet/pattern.html:170 +#: crochet/templates/crochet/pattern.html:169 msgid "Materiales" msgstr "Materials" -#: crochet/templates/crochet/pattern.html:170 +#: crochet/templates/crochet/pattern.html:169 msgid "Añadir material..." msgstr "Add material..." -#: crochet/templates/crochet/pattern.html:170 +#: crochet/templates/crochet/pattern.html:169 msgid "Añadir línea" msgstr "Add line" -#: crochet/templates/crochet/pattern.html:172 -#: crochet/templates/crochet/pattern.html:174 +#: crochet/templates/crochet/pattern.html:171 +#: crochet/templates/crochet/pattern.html:173 msgid "Imagen" msgstr "Image" -#: crochet/templates/crochet/pattern.html:174 +#: crochet/templates/crochet/pattern.html:173 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:176 +#: crochet/templates/crochet/pattern.html:175 msgid "Patrón" msgstr "Pattern" -#: crochet/templates/crochet/pattern.html:176 +#: crochet/templates/crochet/pattern.html:175 msgid "Elementos" msgstr "Elements" -#: crochet/templates/crochet/pattern.html:178 +#: crochet/templates/crochet/pattern.html:177 msgid "Grupo" msgstr "Group" -#: crochet/templates/crochet/pattern.html:182 +#: crochet/templates/crochet/pattern.html:181 msgid "Arrastrar para reordenar" msgstr "Drag to reorder" -#: crochet/templates/crochet/pattern.html:183 +#: crochet/templates/crochet/pattern.html:182 msgid "Colapsar / expandir" msgstr "Collapse / expand" -#: crochet/templates/crochet/pattern.html:184 +#: crochet/templates/crochet/pattern.html:183 msgid "Duplicar sección" msgstr "Duplicate section" -#: crochet/templates/crochet/pattern.html:185 +#: crochet/templates/crochet/pattern.html:184 msgid "Eliminar sección" msgstr "Delete section" -#: crochet/templates/crochet/pattern.html:186 +#: crochet/templates/crochet/pattern.html:185 msgid "" "¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las " "secciones que contiene." @@ -364,7 +398,7 @@ msgstr "" "Are you sure you want to delete this group? All sections inside it will also " "be deleted." -#: crochet/templates/crochet/pattern.html:192 +#: crochet/templates/crochet/pattern.html:191 msgid "Añade una sección para ver aquí el resultado." msgstr "Add a section to see the result here." @@ -393,51 +427,63 @@ msgstr "Create account" msgid "¿Ya tienes cuenta? Inicia sesión" msgstr "Already have an account? Log in" -#: crochet/urls.py:33 +#: crochet/urls.py:36 msgid "pattern//" msgstr "" -#: crochet/urls.py:34 +#: crochet/urls.py:37 msgid "pattern//edit/" msgstr "" -#: crochet/urls.py:35 +#: crochet/urls.py:38 msgid "pattern//save/" msgstr "" -#: crochet/urls.py:36 +#: crochet/urls.py:39 msgid "pattern//images/" msgstr "" -#: crochet/urls.py:37 +#: crochet/urls.py:40 msgid "pattern//cover/" msgstr "" -#: crochet/urls.py:38 +#: crochet/urls.py:41 msgid "pattern//pdf/" msgstr "" -#: crochet/urls.py:39 +#: crochet/urls.py:42 msgid "pattern//delete/" msgstr "" -#: crochet/urls.py:40 +#: crochet/urls.py:43 msgid "pattern/new/" msgstr "" -#: crochet/urls.py:41 +#: crochet/urls.py:44 msgid "account/" msgstr "" -#: crochet/urls.py:42 +#: crochet/urls.py:45 +msgid "account/settings/" +msgstr "" + +#: crochet/urls.py:46 +msgid "account/settings/email/" +msgstr "" + +#: crochet/urls.py:48 +msgid "account/settings/password/" +msgstr "" + +#: crochet/urls.py:51 msgid "account/register/" msgstr "" -#: crochet/urls.py:44 +#: crochet/urls.py:53 msgid "account/login/" msgstr "" -#: crochet/urls.py:49 +#: crochet/urls.py:58 msgid "account/logout/" msgstr "" diff --git a/crochet/locale/es/LC_MESSAGES/django.po b/crochet/locale/es/LC_MESSAGES/django.po index e131cf0..91a97a8 100644 --- a/crochet/locale/es/LC_MESSAGES/django.po +++ b/crochet/locale/es/LC_MESSAGES/django.po @@ -2,12 +2,33 @@ msgid "" msgstr "" "Project-Id-Version: crochet\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-17 11:52+0000\n" +"POT-Creation-Date: 2026-07-20 09:18+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 +msgid "Email" +msgstr "" + +#: crochet/templates/crochet/_account_email_form.html:10 +msgid "Email actualizado." +msgstr "" + +#: crochet/templates/crochet/_account_email_form.html:20 +msgid "Guardar email" +msgstr "" + +#: crochet/templates/crochet/_account_password_form.html:7 +msgid "Contraseña actualizada." +msgstr "" + +#: crochet/templates/crochet/_account_password_form.html:19 +#: crochet/templates/crochet/account_settings.html:29 +msgid "Cambiar contraseña" +msgstr "" + #: crochet/templates/crochet/account_home.html:4 msgid "Mis patrones" msgstr "" @@ -22,7 +43,7 @@ msgid "Crear patrón" msgstr "" #: crochet/templates/crochet/account_home.html:21 -#: crochet/templates/crochet/pattern.html:21 +#: crochet/templates/crochet/pattern.html:20 msgid "¿Seguro que quieres eliminar este patrón? No podrás deshacerlo." msgstr "" @@ -36,7 +57,7 @@ msgid "Actualizado el %(date)s" msgstr "" #: crochet/templates/crochet/account_home.html:40 -#: crochet/templates/crochet/pattern.html:34 +#: crochet/templates/crochet/pattern.html:33 msgid "Ver patrón" msgstr "" @@ -52,19 +73,32 @@ msgstr "" msgid "Todavía no tienes ningún patrón." msgstr "" -#: crochet/templates/crochet/account_home.html:60 -#: crochet/templates/crochet/base.html:34 +#: crochet/templates/crochet/account_settings.html:4 +#: crochet/templates/crochet/account_settings.html:18 +#: crochet/templates/crochet/base.html:50 +msgid "Ajustes de la cuenta" +msgstr "" + +#: crochet/templates/crochet/account_settings.html:34 +msgid "Volver a mis patrones" +msgstr "" + +#: crochet/templates/crochet/base.html:39 +msgid "Idioma" +msgstr "" + +#: crochet/templates/crochet/base.html:54 msgid "Cerrar sesión" msgstr "" -#: crochet/templates/crochet/base.html:37 +#: crochet/templates/crochet/base.html:60 #: crochet/templates/crochet/home.html:24 #: crochet/templates/registration/login.html:4 #: crochet/templates/registration/login.html:14 msgid "Iniciar sesión" msgstr "" -#: crochet/templates/crochet/base.html:38 +#: crochet/templates/crochet/base.html:61 #: crochet/templates/registration/register.html:29 msgid "Registrarme" msgstr "" @@ -121,239 +155,239 @@ msgid "" "podrá editarlo." msgstr "" -#: crochet/templates/crochet/pattern.html:31 +#: crochet/templates/crochet/pattern.html:30 msgid "No se ha podido guardar. Inténtalo de nuevo." msgstr "" -#: crochet/templates/crochet/pattern.html:31 +#: crochet/templates/crochet/pattern.html:30 msgid "Guardar" msgstr "" -#: crochet/templates/crochet/pattern.html:35 +#: crochet/templates/crochet/pattern.html:34 msgid "Exportar a PDF" msgstr "" -#: crochet/templates/crochet/pattern.html:39 +#: crochet/templates/crochet/pattern.html:38 msgid "Eliminar patrón" msgstr "" -#: crochet/templates/crochet/pattern.html:46 +#: crochet/templates/crochet/pattern.html:45 msgid "Guardado" msgstr "" -#: crochet/templates/crochet/pattern.html:53 +#: crochet/templates/crochet/pattern.html:52 msgid "Personalización de página" msgstr "" -#: crochet/templates/crochet/pattern.html:57 +#: crochet/templates/crochet/pattern.html:56 msgid "Título del patrón" msgstr "" -#: crochet/templates/crochet/pattern.html:58 +#: crochet/templates/crochet/pattern.html:57 msgid "Título del patrón..." msgstr "" -#: crochet/templates/crochet/pattern.html:61 +#: crochet/templates/crochet/pattern.html:60 msgid "Autor" msgstr "" -#: crochet/templates/crochet/pattern.html:62 +#: crochet/templates/crochet/pattern.html:61 msgid "Autor..." msgstr "" -#: crochet/templates/crochet/pattern.html:67 +#: crochet/templates/crochet/pattern.html:66 msgid "Imagen de portada" msgstr "" -#: crochet/templates/crochet/pattern.html:70 +#: crochet/templates/crochet/pattern.html:69 msgid "No se ha podido subir la imagen de portada. Inténtalo de nuevo." msgstr "" -#: crochet/templates/crochet/pattern.html:84 -#: crochet/templates/crochet/pattern.html:168 +#: crochet/templates/crochet/pattern.html:83 +#: crochet/templates/crochet/pattern.html:167 msgid "Color del texto" msgstr "" -#: crochet/templates/crochet/pattern.html:88 +#: crochet/templates/crochet/pattern.html:87 msgid "Color de acento (títulos)" msgstr "" -#: crochet/templates/crochet/pattern.html:92 -#: crochet/templates/crochet/pattern.html:168 +#: crochet/templates/crochet/pattern.html:91 +#: crochet/templates/crochet/pattern.html:167 msgid "Color de fondo" msgstr "" -#: crochet/templates/crochet/pattern.html:96 +#: crochet/templates/crochet/pattern.html:95 msgid "Tipografía" msgstr "" -#: crochet/templates/crochet/pattern.html:102 +#: crochet/templates/crochet/pattern.html:101 msgid "Monoespaciada" msgstr "" -#: crochet/templates/crochet/pattern.html:106 +#: crochet/templates/crochet/pattern.html:105 msgid "Tamaño de letra" msgstr "" -#: crochet/templates/crochet/pattern.html:108 +#: crochet/templates/crochet/pattern.html:107 msgid "Pequeño" msgstr "" -#: crochet/templates/crochet/pattern.html:109 +#: crochet/templates/crochet/pattern.html:108 msgid "Normal" msgstr "" -#: crochet/templates/crochet/pattern.html:110 +#: crochet/templates/crochet/pattern.html:109 msgid "Grande" msgstr "" -#: crochet/templates/crochet/pattern.html:114 +#: crochet/templates/crochet/pattern.html:113 msgid "Alineación del texto" msgstr "" -#: crochet/templates/crochet/pattern.html:116 +#: crochet/templates/crochet/pattern.html:115 msgid "Izquierda" msgstr "" -#: crochet/templates/crochet/pattern.html:117 +#: crochet/templates/crochet/pattern.html:116 msgid "Centrado" msgstr "" -#: crochet/templates/crochet/pattern.html:118 +#: crochet/templates/crochet/pattern.html:117 msgid "Justificado" msgstr "" -#: crochet/templates/crochet/pattern.html:122 +#: crochet/templates/crochet/pattern.html:121 msgid "Tamaño de página" msgstr "" -#: crochet/templates/crochet/pattern.html:127 +#: crochet/templates/crochet/pattern.html:126 msgid "Carta (Letter)" msgstr "" -#: crochet/templates/crochet/pattern.html:132 +#: crochet/templates/crochet/pattern.html:131 msgid "Orientación" msgstr "" -#: crochet/templates/crochet/pattern.html:134 +#: crochet/templates/crochet/pattern.html:133 msgid "Vertical" msgstr "" -#: crochet/templates/crochet/pattern.html:135 +#: crochet/templates/crochet/pattern.html:134 msgid "Horizontal" msgstr "" -#: crochet/templates/crochet/pattern.html:143 -#: crochet/templates/crochet/pattern.html:151 +#: crochet/templates/crochet/pattern.html:142 +#: crochet/templates/crochet/pattern.html:150 msgid "Secciones" msgstr "" -#: crochet/templates/crochet/pattern.html:144 -#: crochet/templates/crochet/pattern.html:190 +#: crochet/templates/crochet/pattern.html:143 +#: crochet/templates/crochet/pattern.html:189 msgid "Vista previa" msgstr "" -#: crochet/templates/crochet/pattern.html:153 +#: crochet/templates/crochet/pattern.html:152 msgid "Colapsar todo" msgstr "" -#: crochet/templates/crochet/pattern.html:153 +#: crochet/templates/crochet/pattern.html:152 msgid "Expandir todo" msgstr "" -#: crochet/templates/crochet/pattern.html:158 +#: crochet/templates/crochet/pattern.html:157 msgid "Añadir sección" msgstr "" -#: crochet/templates/crochet/pattern.html:161 +#: crochet/templates/crochet/pattern.html:160 msgid "Título" msgstr "" -#: crochet/templates/crochet/pattern.html:161 +#: crochet/templates/crochet/pattern.html:160 msgid "Título..." msgstr "" -#: crochet/templates/crochet/pattern.html:163 +#: crochet/templates/crochet/pattern.html:162 msgid "Subtítulo" msgstr "" -#: crochet/templates/crochet/pattern.html:163 +#: crochet/templates/crochet/pattern.html:162 msgid "Subtítulo..." msgstr "" -#: crochet/templates/crochet/pattern.html:165 +#: crochet/templates/crochet/pattern.html:164 msgid "Texto" msgstr "" -#: crochet/templates/crochet/pattern.html:165 +#: crochet/templates/crochet/pattern.html:164 msgid "Escribe aquí..." msgstr "" +#: crochet/templates/crochet/pattern.html:166 #: crochet/templates/crochet/pattern.html:167 -#: crochet/templates/crochet/pattern.html:168 msgid "Nota" msgstr "" -#: crochet/templates/crochet/pattern.html:167 +#: crochet/templates/crochet/pattern.html:166 msgid "Escribe una nota o consejo..." msgstr "" -#: crochet/templates/crochet/pattern.html:170 +#: crochet/templates/crochet/pattern.html:169 msgid "Materiales" msgstr "" -#: crochet/templates/crochet/pattern.html:170 +#: crochet/templates/crochet/pattern.html:169 msgid "Añadir material..." msgstr "" -#: crochet/templates/crochet/pattern.html:170 +#: crochet/templates/crochet/pattern.html:169 msgid "Añadir línea" msgstr "" -#: crochet/templates/crochet/pattern.html:172 -#: crochet/templates/crochet/pattern.html:174 +#: crochet/templates/crochet/pattern.html:171 +#: crochet/templates/crochet/pattern.html:173 msgid "Imagen" msgstr "" -#: crochet/templates/crochet/pattern.html:174 +#: crochet/templates/crochet/pattern.html:173 msgid "No se ha podido subir la imagen. Inténtalo de nuevo." msgstr "" -#: crochet/templates/crochet/pattern.html:176 +#: crochet/templates/crochet/pattern.html:175 msgid "Patrón" msgstr "" -#: crochet/templates/crochet/pattern.html:176 +#: crochet/templates/crochet/pattern.html:175 msgid "Elementos" msgstr "" -#: crochet/templates/crochet/pattern.html:178 +#: crochet/templates/crochet/pattern.html:177 msgid "Grupo" msgstr "" -#: crochet/templates/crochet/pattern.html:182 +#: crochet/templates/crochet/pattern.html:181 msgid "Arrastrar para reordenar" msgstr "" -#: crochet/templates/crochet/pattern.html:183 +#: crochet/templates/crochet/pattern.html:182 msgid "Colapsar / expandir" msgstr "" -#: crochet/templates/crochet/pattern.html:184 +#: crochet/templates/crochet/pattern.html:183 msgid "Duplicar sección" msgstr "" -#: crochet/templates/crochet/pattern.html:185 +#: crochet/templates/crochet/pattern.html:184 msgid "Eliminar sección" msgstr "" -#: crochet/templates/crochet/pattern.html:186 +#: crochet/templates/crochet/pattern.html:185 msgid "" "¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las " "secciones que contiene." msgstr "" -#: crochet/templates/crochet/pattern.html:192 +#: crochet/templates/crochet/pattern.html:191 msgid "Añade una sección para ver aquí el resultado." msgstr "" @@ -384,52 +418,64 @@ 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:33 +#: crochet/urls.py:36 msgid "pattern//" msgstr "patron//" -#: crochet/urls.py:34 +#: crochet/urls.py:37 msgid "pattern//edit/" msgstr "patron//editar/" -#: crochet/urls.py:35 +#: crochet/urls.py:38 msgid "pattern//save/" msgstr "patron//guardar/" -#: crochet/urls.py:36 +#: crochet/urls.py:39 msgid "pattern//images/" msgstr "patron//imagenes/" -#: crochet/urls.py:37 +#: crochet/urls.py:40 msgid "pattern//cover/" msgstr "patron//portada/" -#: crochet/urls.py:38 +#: crochet/urls.py:41 msgid "pattern//pdf/" msgstr "patron//pdf/" -#: crochet/urls.py:39 +#: crochet/urls.py:42 msgid "pattern//delete/" msgstr "patron//eliminar/" -#: crochet/urls.py:40 +#: crochet/urls.py:43 msgid "pattern/new/" msgstr "patron/nuevo/" # Igual que "pattern/" arriba: "account/" se traduce como "cuenta/". -#: crochet/urls.py:41 +#: crochet/urls.py:44 msgid "account/" msgstr "cuenta/" -#: crochet/urls.py:42 +#: crochet/urls.py:45 +msgid "account/settings/" +msgstr "cuenta/ajustes/" + +#: crochet/urls.py:46 +msgid "account/settings/email/" +msgstr "cuenta/ajustes/email/" + +#: crochet/urls.py:48 +msgid "account/settings/password/" +msgstr "cuenta/ajustes/contrasena/" + +#: crochet/urls.py:51 msgid "account/register/" msgstr "cuenta/registro/" -#: crochet/urls.py:44 +#: crochet/urls.py:53 msgid "account/login/" msgstr "cuenta/entrar/" -#: crochet/urls.py:49 +#: crochet/urls.py:58 msgid "account/logout/" msgstr "cuenta/salir/" diff --git a/crochet/templates/crochet/_account_email_form.html b/crochet/templates/crochet/_account_email_form.html new file mode 100644 index 0000000..73846ce --- /dev/null +++ b/crochet/templates/crochet/_account_email_form.html @@ -0,0 +1,21 @@ +{% load i18n %} + +
+ {% csrf_token %} + {% if email_updated %} + + {% endif %} + + +
diff --git a/crochet/templates/crochet/_account_password_form.html b/crochet/templates/crochet/_account_password_form.html new file mode 100644 index 0000000..4df326b --- /dev/null +++ b/crochet/templates/crochet/_account_password_form.html @@ -0,0 +1,20 @@ +{% load i18n %} +
+ {% csrf_token %} + {% if password_updated %} + + {% endif %} + {% for field in password_form %} + + {% endfor %} + +
diff --git a/crochet/templates/crochet/_platform_assets.html b/crochet/templates/crochet/_platform_assets.html index 0f6aee5..27ffe4c 100644 --- a/crochet/templates/crochet/_platform_assets.html +++ b/crochet/templates/crochet/_platform_assets.html @@ -1,9 +1,10 @@ -{% load static %} +{% load static django_htmx %} {# Un comentario Django de una sola línea (su lexer no cruza saltos de línea al buscar el cierre, y tampoco admite el propio delimitador de comentario dentro del texto). #} -{# Incluido por cada plantilla con look de "plataforma" (editor, cuenta): daisyUI + Tailwind vía CDN + el tema propio "crochet" (ver theme.css). pattern_detail.html NO lo incluye a propósito (ver ese archivo). #} +{# Incluido por cada plantilla con look de "plataforma" (editor, cuenta): daisyUI + Tailwind vía CDN + el tema propio "crochet" (ver theme.css) + htmx. pattern_detail.html NO lo incluye a propósito (ver ese archivo). #} - \ No newline at end of file + +{% htmx_script %} \ No newline at end of file diff --git a/crochet/templates/crochet/account_home.html b/crochet/templates/crochet/account_home.html index e98401b..595e3b5 100644 --- a/crochet/templates/crochet/account_home.html +++ b/crochet/templates/crochet/account_home.html @@ -54,10 +54,5 @@

{% trans 'Todavía no tienes ningún patrón.' %}

{% endif %} - -
- {% csrf_token %} - -
{% endblock %} diff --git a/crochet/templates/crochet/account_settings.html b/crochet/templates/crochet/account_settings.html new file mode 100644 index 0000000..5aa2dfb --- /dev/null +++ b/crochet/templates/crochet/account_settings.html @@ -0,0 +1,36 @@ +{% extends 'crochet/base.html' %} +{% load i18n %} + +{% block title %}{% trans 'Ajustes de la cuenta' %}{% endblock %} + + +{% block body_attrs %} hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'{% endblock %} + +{% block extra_css %} + {% include 'crochet/_platform_assets.html' %} +{% endblock %} + +{% block content %} +
+

{% trans 'Ajustes de la cuenta' %}

+ +
+
+

{% trans 'Email' %}

+ {% include 'crochet/_account_email_form.html' %} +
+
+ +
+
+

{% trans 'Cambiar contraseña' %}

+ {% include 'crochet/_account_password_form.html' %} +
+
+ + {% trans 'Volver a mis patrones' %} +
+{% endblock %} diff --git a/crochet/templates/crochet/base.html b/crochet/templates/crochet/base.html index fd7f088..4b9d6aa 100644 --- a/crochet/templates/crochet/base.html +++ b/crochet/templates/crochet/base.html @@ -1,4 +1,4 @@ -{% load static i18n %} +{% load static i18n crochet_i18n %} + {% get_current_language as CURRENT_LANGUAGE %} + {% get_available_languages as AVAILABLE_LANGUAGES %} + {% if user.is_authenticated %} - -
- {% csrf_token %} - -
+ + {% else %} {% trans 'Iniciar sesión' %} {% trans 'Registrarme' %} diff --git a/crochet/templates/crochet/pattern.html b/crochet/templates/crochet/pattern.html index a7bfed1..a685b1d 100644 --- a/crochet/templates/crochet/pattern.html +++ b/crochet/templates/crochet/pattern.html @@ -1,5 +1,5 @@ {% extends 'crochet/base.html' %} -{% load static i18n django_htmx %} +{% load static i18n %} {% block title %}Crochet{% endblock %} @@ -9,7 +9,6 @@ {% include 'crochet/_platform_assets.html' %} - {% htmx_script %} diff --git a/crochet/templatetags/__init__.py b/crochet/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/crochet/templatetags/crochet_i18n.py b/crochet/templatetags/crochet_i18n.py new file mode 100644 index 0000000..61d9c05 --- /dev/null +++ b/crochet/templatetags/crochet_i18n.py @@ -0,0 +1,15 @@ +from django import template +from django.urls import translate_url as django_translate_url + +register = template.Library() + + +@register.simple_tag +def translate_url(path, lang_code): + # django.urls.translate_url: resuelve `path` con el idioma actual (el de + # la propia petición) y lo vuelve a construir con `lang_code`, en vez de + # cambiar solo el prefijo (/es/, /en/) a mano: cada ruta traducible (ver + # crochet/urls.py) cambia entera de un idioma a otro, no solo el + # prefijo (p.ej. /es/patron//editar/ frente a + # /en/pattern//edit/). + return django_translate_url(path, lang_code) diff --git a/crochet/tests/test_account_settings.py b/crochet/tests/test_account_settings.py new file mode 100644 index 0000000..fa8b7de --- /dev/null +++ b/crochet/tests/test_account_settings.py @@ -0,0 +1,142 @@ +from django.contrib.auth.models import User +from django.test import TestCase +from django.urls import reverse +from django.utils import translation + + +class AccountSettingsViewTests(TestCase): + def setUp(self): + self.user = User.objects.create_user( + username='crocheter', password='a-very-uncommon-pw-1', email='old@example.com', + ) + with translation.override('es'): + self.url = reverse('crochet:account_settings') + + 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}') + + def test_get_renders_both_forms_with_current_email(self): + self.client.force_login(self.user) + response = self.client.get(self.url) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'crochet/account_settings.html') + self.assertContains(response, 'old@example.com') + self.assertContains(response, 'id_old_password') + self.assertContains(response, 'id_new_password1') + + +class AccountEmailUpdateViewTests(TestCase): + def setUp(self): + self.user = User.objects.create_user( + username='crocheter', password='a-very-uncommon-pw-1', email='old@example.com', + ) + with translation.override('es'): + self.url = reverse('crochet:account_settings_email') + + def test_anonymous_user_is_redirected_to_login(self): + response = self.client.post(self.url, {'email': 'new@example.com'}) + + with translation.override('es'): + login_url = reverse('crochet:login') + self.assertRedirects(response, f'{login_url}?next={self.url}') + + def test_valid_email_updates_user_and_shows_success(self): + self.client.force_login(self.user) + response = self.client.post(self.url, {'email': 'new@example.com'}) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'crochet/_account_email_form.html') + self.user.refresh_from_db() + self.assertEqual(self.user.email, 'new@example.com') + self.assertContains(response, 'Email actualizado.') + + def test_invalid_email_does_not_update_user(self): + # 200, no 400: htmx no sustituye el contenido en respuestas que no + # sean 2xx (lo trata como fallo de red), así que el fragmento con + # el error de validación no se vería en el navegador con un 400. + self.client.force_login(self.user) + response = self.client.post(self.url, {'email': 'not-an-email'}) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'class="text-error text-xs"') + self.user.refresh_from_db() + self.assertEqual(self.user.email, 'old@example.com') + + def test_only_updates_the_requesting_users_own_email(self): + other = User.objects.create_user(username='other', password='a-very-uncommon-pw-1', email='other@example.com') + self.client.force_login(self.user) + + self.client.post(self.url, {'email': 'new@example.com'}) + + other.refresh_from_db() + self.assertEqual(other.email, 'other@example.com') + + +class AccountPasswordChangeViewTests(TestCase): + def setUp(self): + self.user = User.objects.create_user(username='crocheter', password='the-old-password-1') + with translation.override('es'): + self.url = reverse('crochet:account_settings_password') + + def test_anonymous_user_is_redirected_to_login(self): + response = self.client.post(self.url, {}) + + with translation.override('es'): + login_url = reverse('crochet:login') + self.assertRedirects(response, f'{login_url}?next={self.url}') + + def test_valid_password_change_updates_password_and_keeps_session(self): + self.client.force_login(self.user) + + response = self.client.post(self.url, { + 'old_password': 'the-old-password-1', + 'new_password1': 'the-new-password-2', + 'new_password2': 'the-new-password-2', + }) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'crochet/_account_password_form.html') + self.assertContains(response, 'Contraseña actualizada.') + self.user.refresh_from_db() + self.assertTrue(self.user.check_password('the-new-password-2')) + # 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') + still_logged_in_response = self.client.get(account_home_url) + self.assertEqual(still_logged_in_response.status_code, 200) + + def test_wrong_old_password_does_not_change_password(self): + # 200, no 400: ver el comentario equivalente en + # AccountEmailUpdateViewTests.test_invalid_email_does_not_update_user. + self.client.force_login(self.user) + + response = self.client.post(self.url, { + 'old_password': 'wrong-password', + 'new_password1': 'the-new-password-2', + 'new_password2': 'the-new-password-2', + }) + + 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_mismatched_new_passwords_does_not_change_password(self): + self.client.force_login(self.user) + + response = self.client.post(self.url, { + 'old_password': 'the-old-password-1', + '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')) diff --git a/crochet/tests/test_views.py b/crochet/tests/test_views.py index 3758075..98116b2 100644 --- a/crochet/tests/test_views.py +++ b/crochet/tests/test_views.py @@ -53,6 +53,53 @@ class HomeViewTests(TestCase): self.assertEqual(response_en.status_code, 200) +class LanguageSwitcherTests(TestCase): + def test_home_page_switcher_offers_the_same_page_in_english(self): + response = self.client.get('/es/') + self.assertContains(response, '