feat: improved UX
CI / test (push) Successful in 58s
CI / build (push) Successful in 29s

This commit is contained in:
2026-07-20 11:21:29 +02:00
parent 96738bf7d5
commit 73109a268b
17 changed files with 657 additions and 192 deletions
+1 -8
View File
@@ -20,13 +20,6 @@ jobs:
git checkout ${{ gitea.sha }} git checkout ${{ gitea.sha }}
- name: Install system dependencies - 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: | run: |
apt-get update -qq apt-get update -qq
apt-get install -y --no-install-recommends libpango-1.0-0 libpangocairo-1.0-0 fonts-dejavu-core gettext 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 run: uv run python manage.py compilemessages
- name: Run tests - name: Run tests
run: uv run pytest run: uv run pytest --cov
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+22 -1
View File
@@ -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 # 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 # 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) super().__init__(*args, **kwargs)
for field in self.fields.values(): for field in self.fields.values():
field.widget.attrs['class'] = INPUT_CLASSES 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'])
+129 -83
View File
@@ -2,12 +2,33 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: crochet\n" "Project-Id-Version: crochet\n"
"Report-Msgid-Bugs-To: \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" "Language: en\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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 #: crochet/templates/crochet/account_home.html:4
msgid "Mis patrones" msgid "Mis patrones"
msgstr "My patterns" msgstr "My patterns"
@@ -22,7 +43,7 @@ msgid "Crear patrón"
msgstr "Create pattern" msgstr "Create pattern"
#: crochet/templates/crochet/account_home.html:21 #: 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." 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." 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" msgstr "Updated on %(date)s"
#: crochet/templates/crochet/account_home.html:40 #: crochet/templates/crochet/account_home.html:40
#: crochet/templates/crochet/pattern.html:34 #: crochet/templates/crochet/pattern.html:33
msgid "Ver patrón" msgid "Ver patrón"
msgstr "View pattern" msgstr "View pattern"
@@ -52,19 +73,32 @@ msgstr "Delete"
msgid "Todavía no tienes ningún patrón." msgid "Todavía no tienes ningún patrón."
msgstr "You don't have any patterns yet." msgstr "You don't have any patterns yet."
#: crochet/templates/crochet/account_home.html:60 #: crochet/templates/crochet/account_settings.html:4
#: crochet/templates/crochet/base.html:34 #: 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" msgid "Cerrar sesión"
msgstr "Log out" msgstr "Log out"
#: crochet/templates/crochet/base.html:37 #: crochet/templates/crochet/base.html:60
#: crochet/templates/crochet/home.html:24 #: crochet/templates/crochet/home.html:24
#: crochet/templates/registration/login.html:4 #: crochet/templates/registration/login.html:4
#: crochet/templates/registration/login.html:14 #: crochet/templates/registration/login.html:14
msgid "Iniciar sesión" msgid "Iniciar sesión"
msgstr "Log in" msgstr "Log in"
#: crochet/templates/crochet/base.html:38 #: crochet/templates/crochet/base.html:61
#: crochet/templates/registration/register.html:29 #: crochet/templates/registration/register.html:29
msgid "Registrarme" msgid "Registrarme"
msgstr "Sign up" msgstr "Sign up"
@@ -114,8 +148,8 @@ msgid ""
"Descarga tu patrón listo para imprimir, con tu propia portada, colores y " "Descarga tu patrón listo para imprimir, con tu propia portada, colores y "
"tipografía." "tipografía."
msgstr "" msgstr ""
"Download your pattern ready to print, with your own cover image, colors, " "Download your pattern ready to print, with your own cover image, colors, and "
"and typography." "typography."
#: crochet/templates/crochet/home.html:53 #: crochet/templates/crochet/home.html:53
msgid "Comparte con un enlace" msgid "Comparte con un enlace"
@@ -130,233 +164,233 @@ msgstr ""
"to edit it." "to edit it."
# crochet/templates/crochet/pattern.html # 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." msgid "No se ha podido guardar. Inténtalo de nuevo."
msgstr "Couldn't save. Please try again." msgstr "Couldn't save. Please try again."
#: crochet/templates/crochet/pattern.html:31 #: crochet/templates/crochet/pattern.html:30
msgid "Guardar" msgid "Guardar"
msgstr "Save" msgstr "Save"
#: crochet/templates/crochet/pattern.html:35 #: crochet/templates/crochet/pattern.html:34
msgid "Exportar a PDF" msgid "Exportar a PDF"
msgstr "Export to PDF" msgstr "Export to PDF"
#: crochet/templates/crochet/pattern.html:39 #: crochet/templates/crochet/pattern.html:38
msgid "Eliminar patrón" msgid "Eliminar patrón"
msgstr "Delete pattern" msgstr "Delete pattern"
#: crochet/templates/crochet/pattern.html:46 #: crochet/templates/crochet/pattern.html:45
msgid "Guardado" msgid "Guardado"
msgstr "Saved" msgstr "Saved"
#: crochet/templates/crochet/pattern.html:53 #: crochet/templates/crochet/pattern.html:52
msgid "Personalización de página" msgid "Personalización de página"
msgstr "Page customization" msgstr "Page customization"
#: crochet/templates/crochet/pattern.html:57 #: crochet/templates/crochet/pattern.html:56
msgid "Título del patrón" msgid "Título del patrón"
msgstr "Pattern title" msgstr "Pattern title"
#: crochet/templates/crochet/pattern.html:58 #: crochet/templates/crochet/pattern.html:57
msgid "Título del patrón..." msgid "Título del patrón..."
msgstr "Pattern title..." msgstr "Pattern title..."
#: crochet/templates/crochet/pattern.html:61 #: crochet/templates/crochet/pattern.html:60
msgid "Autor" msgid "Autor"
msgstr "Author" msgstr "Author"
#: crochet/templates/crochet/pattern.html:62 #: crochet/templates/crochet/pattern.html:61
msgid "Autor..." msgid "Autor..."
msgstr "Author..." msgstr "Author..."
#: crochet/templates/crochet/pattern.html:67 #: crochet/templates/crochet/pattern.html:66
msgid "Imagen de portada" msgid "Imagen de portada"
msgstr "Cover image" 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." msgid "No se ha podido subir la imagen de portada. Inténtalo de nuevo."
msgstr "Couldn't upload the cover image. Please try again." msgstr "Couldn't upload the cover image. Please try again."
#: crochet/templates/crochet/pattern.html:84 #: crochet/templates/crochet/pattern.html:83
#: crochet/templates/crochet/pattern.html:168 #: crochet/templates/crochet/pattern.html:167
msgid "Color del texto" msgid "Color del texto"
msgstr "Text color" msgstr "Text color"
#: crochet/templates/crochet/pattern.html:88 #: crochet/templates/crochet/pattern.html:87
msgid "Color de acento (títulos)" msgid "Color de acento (títulos)"
msgstr "Accent color (headings)" msgstr "Accent color (headings)"
#: crochet/templates/crochet/pattern.html:92 #: crochet/templates/crochet/pattern.html:91
#: crochet/templates/crochet/pattern.html:168 #: crochet/templates/crochet/pattern.html:167
msgid "Color de fondo" msgid "Color de fondo"
msgstr "Background color" msgstr "Background color"
#: crochet/templates/crochet/pattern.html:96 #: crochet/templates/crochet/pattern.html:95
msgid "Tipografía" msgid "Tipografía"
msgstr "Font" msgstr "Font"
#: crochet/templates/crochet/pattern.html:102 #: crochet/templates/crochet/pattern.html:101
msgid "Monoespaciada" msgid "Monoespaciada"
msgstr "Monospace" msgstr "Monospace"
#: crochet/templates/crochet/pattern.html:106 #: crochet/templates/crochet/pattern.html:105
msgid "Tamaño de letra" msgid "Tamaño de letra"
msgstr "Font size" msgstr "Font size"
#: crochet/templates/crochet/pattern.html:108 #: crochet/templates/crochet/pattern.html:107
msgid "Pequeño" msgid "Pequeño"
msgstr "Small" msgstr "Small"
#: crochet/templates/crochet/pattern.html:109 #: crochet/templates/crochet/pattern.html:108
msgid "Normal" msgid "Normal"
msgstr "Normal" msgstr "Normal"
#: crochet/templates/crochet/pattern.html:110 #: crochet/templates/crochet/pattern.html:109
msgid "Grande" msgid "Grande"
msgstr "Large" msgstr "Large"
#: crochet/templates/crochet/pattern.html:114 #: crochet/templates/crochet/pattern.html:113
msgid "Alineación del texto" msgid "Alineación del texto"
msgstr "Text alignment" msgstr "Text alignment"
#: crochet/templates/crochet/pattern.html:116 #: crochet/templates/crochet/pattern.html:115
msgid "Izquierda" msgid "Izquierda"
msgstr "Left" msgstr "Left"
#: crochet/templates/crochet/pattern.html:117 #: crochet/templates/crochet/pattern.html:116
msgid "Centrado" msgid "Centrado"
msgstr "Center" msgstr "Center"
#: crochet/templates/crochet/pattern.html:118 #: crochet/templates/crochet/pattern.html:117
msgid "Justificado" msgid "Justificado"
msgstr "Justify" msgstr "Justify"
#: crochet/templates/crochet/pattern.html:122 #: crochet/templates/crochet/pattern.html:121
msgid "Tamaño de página" msgid "Tamaño de página"
msgstr "Page size" msgstr "Page size"
#: crochet/templates/crochet/pattern.html:127 #: crochet/templates/crochet/pattern.html:126
msgid "Carta (Letter)" msgid "Carta (Letter)"
msgstr "Letter" msgstr "Letter"
#: crochet/templates/crochet/pattern.html:132 #: crochet/templates/crochet/pattern.html:131
msgid "Orientación" msgid "Orientación"
msgstr "Orientation" msgstr "Orientation"
#: crochet/templates/crochet/pattern.html:134 #: crochet/templates/crochet/pattern.html:133
msgid "Vertical" msgid "Vertical"
msgstr "Portrait" msgstr "Portrait"
#: crochet/templates/crochet/pattern.html:135 #: crochet/templates/crochet/pattern.html:134
msgid "Horizontal" msgid "Horizontal"
msgstr "Landscape" msgstr "Landscape"
#: crochet/templates/crochet/pattern.html:143 #: crochet/templates/crochet/pattern.html:142
#: crochet/templates/crochet/pattern.html:151 #: crochet/templates/crochet/pattern.html:150
msgid "Secciones" msgid "Secciones"
msgstr "Sections" msgstr "Sections"
#: crochet/templates/crochet/pattern.html:144 #: crochet/templates/crochet/pattern.html:143
#: crochet/templates/crochet/pattern.html:190 #: crochet/templates/crochet/pattern.html:189
msgid "Vista previa" msgid "Vista previa"
msgstr "Preview" msgstr "Preview"
#: crochet/templates/crochet/pattern.html:153 #: crochet/templates/crochet/pattern.html:152
msgid "Colapsar todo" msgid "Colapsar todo"
msgstr "Collapse all" msgstr "Collapse all"
#: crochet/templates/crochet/pattern.html:153 #: crochet/templates/crochet/pattern.html:152
msgid "Expandir todo" msgid "Expandir todo"
msgstr "Expand all" msgstr "Expand all"
#: crochet/templates/crochet/pattern.html:158 #: crochet/templates/crochet/pattern.html:157
msgid "Añadir sección" msgid "Añadir sección"
msgstr "Add section" msgstr "Add section"
#: crochet/templates/crochet/pattern.html:161 #: crochet/templates/crochet/pattern.html:160
msgid "Título" msgid "Título"
msgstr "Title" msgstr "Title"
#: crochet/templates/crochet/pattern.html:161 #: crochet/templates/crochet/pattern.html:160
msgid "Título..." msgid "Título..."
msgstr "Title..." msgstr "Title..."
#: crochet/templates/crochet/pattern.html:163 #: crochet/templates/crochet/pattern.html:162
msgid "Subtítulo" msgid "Subtítulo"
msgstr "Subtitle" msgstr "Subtitle"
#: crochet/templates/crochet/pattern.html:163 #: crochet/templates/crochet/pattern.html:162
msgid "Subtítulo..." msgid "Subtítulo..."
msgstr "Subtitle..." msgstr "Subtitle..."
#: crochet/templates/crochet/pattern.html:165 #: crochet/templates/crochet/pattern.html:164
msgid "Texto" msgid "Texto"
msgstr "Text" msgstr "Text"
#: crochet/templates/crochet/pattern.html:165 #: crochet/templates/crochet/pattern.html:164
msgid "Escribe aquí..." msgid "Escribe aquí..."
msgstr "Write here..." msgstr "Write here..."
#: crochet/templates/crochet/pattern.html:166
#: crochet/templates/crochet/pattern.html:167 #: crochet/templates/crochet/pattern.html:167
#: crochet/templates/crochet/pattern.html:168
msgid "Nota" msgid "Nota"
msgstr "Note" msgstr "Note"
#: crochet/templates/crochet/pattern.html:167 #: crochet/templates/crochet/pattern.html:166
msgid "Escribe una nota o consejo..." msgid "Escribe una nota o consejo..."
msgstr "Write a note or tip..." msgstr "Write a note or tip..."
#: crochet/templates/crochet/pattern.html:170 #: crochet/templates/crochet/pattern.html:169
msgid "Materiales" msgid "Materiales"
msgstr "Materials" msgstr "Materials"
#: crochet/templates/crochet/pattern.html:170 #: crochet/templates/crochet/pattern.html:169
msgid "Añadir material..." msgid "Añadir material..."
msgstr "Add material..." msgstr "Add material..."
#: crochet/templates/crochet/pattern.html:170 #: crochet/templates/crochet/pattern.html:169
msgid "Añadir línea" msgid "Añadir línea"
msgstr "Add line" msgstr "Add line"
#: crochet/templates/crochet/pattern.html:172 #: crochet/templates/crochet/pattern.html:171
#: crochet/templates/crochet/pattern.html:174 #: crochet/templates/crochet/pattern.html:173
msgid "Imagen" msgid "Imagen"
msgstr "Image" 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." msgid "No se ha podido subir la imagen. Inténtalo de nuevo."
msgstr "Couldn't upload the image. Please try again." msgstr "Couldn't upload the image. Please try again."
#: crochet/templates/crochet/pattern.html:176 #: crochet/templates/crochet/pattern.html:175
msgid "Patrón" msgid "Patrón"
msgstr "Pattern" msgstr "Pattern"
#: crochet/templates/crochet/pattern.html:176 #: crochet/templates/crochet/pattern.html:175
msgid "Elementos" msgid "Elementos"
msgstr "Elements" msgstr "Elements"
#: crochet/templates/crochet/pattern.html:178 #: crochet/templates/crochet/pattern.html:177
msgid "Grupo" msgid "Grupo"
msgstr "Group" msgstr "Group"
#: crochet/templates/crochet/pattern.html:182 #: crochet/templates/crochet/pattern.html:181
msgid "Arrastrar para reordenar" msgid "Arrastrar para reordenar"
msgstr "Drag to reorder" msgstr "Drag to reorder"
#: crochet/templates/crochet/pattern.html:183 #: crochet/templates/crochet/pattern.html:182
msgid "Colapsar / expandir" msgid "Colapsar / expandir"
msgstr "Collapse / expand" msgstr "Collapse / expand"
#: crochet/templates/crochet/pattern.html:184 #: crochet/templates/crochet/pattern.html:183
msgid "Duplicar sección" msgid "Duplicar sección"
msgstr "Duplicate section" msgstr "Duplicate section"
#: crochet/templates/crochet/pattern.html:185 #: crochet/templates/crochet/pattern.html:184
msgid "Eliminar sección" msgid "Eliminar sección"
msgstr "Delete section" msgstr "Delete section"
#: crochet/templates/crochet/pattern.html:186 #: crochet/templates/crochet/pattern.html:185
msgid "" msgid ""
"¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las " "¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las "
"secciones que contiene." "secciones que contiene."
@@ -364,7 +398,7 @@ msgstr ""
"Are you sure you want to delete this group? All sections inside it will also " "Are you sure you want to delete this group? All sections inside it will also "
"be deleted." "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." msgid "Añade una sección para ver aquí el resultado."
msgstr "Add a section to see the result here." msgstr "Add a section to see the result here."
@@ -393,51 +427,63 @@ msgstr "Create account"
msgid "¿Ya tienes cuenta? Inicia sesión" msgid "¿Ya tienes cuenta? Inicia sesión"
msgstr "Already have an account? Log in" msgstr "Already have an account? Log in"
#: crochet/urls.py:33 #: crochet/urls.py:36
msgid "pattern/<uuid:uuid>/" msgid "pattern/<uuid:uuid>/"
msgstr "" msgstr ""
#: crochet/urls.py:34 #: crochet/urls.py:37
msgid "pattern/<uuid:uuid>/edit/" msgid "pattern/<uuid:uuid>/edit/"
msgstr "" msgstr ""
#: crochet/urls.py:35 #: crochet/urls.py:38
msgid "pattern/<uuid:uuid>/save/" msgid "pattern/<uuid:uuid>/save/"
msgstr "" msgstr ""
#: crochet/urls.py:36 #: crochet/urls.py:39
msgid "pattern/<uuid:uuid>/images/" msgid "pattern/<uuid:uuid>/images/"
msgstr "" msgstr ""
#: crochet/urls.py:37 #: crochet/urls.py:40
msgid "pattern/<uuid:uuid>/cover/" msgid "pattern/<uuid:uuid>/cover/"
msgstr "" msgstr ""
#: crochet/urls.py:38 #: crochet/urls.py:41
msgid "pattern/<uuid:uuid>/pdf/" msgid "pattern/<uuid:uuid>/pdf/"
msgstr "" msgstr ""
#: crochet/urls.py:39 #: crochet/urls.py:42
msgid "pattern/<uuid:uuid>/delete/" msgid "pattern/<uuid:uuid>/delete/"
msgstr "" msgstr ""
#: crochet/urls.py:40 #: crochet/urls.py:43
msgid "pattern/new/" msgid "pattern/new/"
msgstr "" msgstr ""
#: crochet/urls.py:41 #: crochet/urls.py:44
msgid "account/" msgid "account/"
msgstr "" 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/" msgid "account/register/"
msgstr "" msgstr ""
#: crochet/urls.py:44 #: crochet/urls.py:53
msgid "account/login/" msgid "account/login/"
msgstr "" msgstr ""
#: crochet/urls.py:49 #: crochet/urls.py:58
msgid "account/logout/" msgid "account/logout/"
msgstr "" msgstr ""
+127 -81
View File
@@ -2,12 +2,33 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: crochet\n" "Project-Id-Version: crochet\n"
"Report-Msgid-Bugs-To: \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" "Language: es\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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 #: crochet/templates/crochet/account_home.html:4
msgid "Mis patrones" msgid "Mis patrones"
msgstr "" msgstr ""
@@ -22,7 +43,7 @@ msgid "Crear patrón"
msgstr "" msgstr ""
#: crochet/templates/crochet/account_home.html:21 #: 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." msgid "¿Seguro que quieres eliminar este patrón? No podrás deshacerlo."
msgstr "" msgstr ""
@@ -36,7 +57,7 @@ msgid "Actualizado el %(date)s"
msgstr "" msgstr ""
#: crochet/templates/crochet/account_home.html:40 #: crochet/templates/crochet/account_home.html:40
#: crochet/templates/crochet/pattern.html:34 #: crochet/templates/crochet/pattern.html:33
msgid "Ver patrón" msgid "Ver patrón"
msgstr "" msgstr ""
@@ -52,19 +73,32 @@ msgstr ""
msgid "Todavía no tienes ningún patrón." msgid "Todavía no tienes ningún patrón."
msgstr "" msgstr ""
#: crochet/templates/crochet/account_home.html:60 #: crochet/templates/crochet/account_settings.html:4
#: crochet/templates/crochet/base.html:34 #: 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" msgid "Cerrar sesión"
msgstr "" msgstr ""
#: crochet/templates/crochet/base.html:37 #: crochet/templates/crochet/base.html:60
#: crochet/templates/crochet/home.html:24 #: crochet/templates/crochet/home.html:24
#: crochet/templates/registration/login.html:4 #: crochet/templates/registration/login.html:4
#: crochet/templates/registration/login.html:14 #: crochet/templates/registration/login.html:14
msgid "Iniciar sesión" msgid "Iniciar sesión"
msgstr "" msgstr ""
#: crochet/templates/crochet/base.html:38 #: crochet/templates/crochet/base.html:61
#: crochet/templates/registration/register.html:29 #: crochet/templates/registration/register.html:29
msgid "Registrarme" msgid "Registrarme"
msgstr "" msgstr ""
@@ -121,239 +155,239 @@ msgid ""
"podrá editarlo." "podrá editarlo."
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:31 #: crochet/templates/crochet/pattern.html:30
msgid "No se ha podido guardar. Inténtalo de nuevo." msgid "No se ha podido guardar. Inténtalo de nuevo."
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:31 #: crochet/templates/crochet/pattern.html:30
msgid "Guardar" msgid "Guardar"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:35 #: crochet/templates/crochet/pattern.html:34
msgid "Exportar a PDF" msgid "Exportar a PDF"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:39 #: crochet/templates/crochet/pattern.html:38
msgid "Eliminar patrón" msgid "Eliminar patrón"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:46 #: crochet/templates/crochet/pattern.html:45
msgid "Guardado" msgid "Guardado"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:53 #: crochet/templates/crochet/pattern.html:52
msgid "Personalización de página" msgid "Personalización de página"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:57 #: crochet/templates/crochet/pattern.html:56
msgid "Título del patrón" msgid "Título del patrón"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:58 #: crochet/templates/crochet/pattern.html:57
msgid "Título del patrón..." msgid "Título del patrón..."
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:61 #: crochet/templates/crochet/pattern.html:60
msgid "Autor" msgid "Autor"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:62 #: crochet/templates/crochet/pattern.html:61
msgid "Autor..." msgid "Autor..."
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:67 #: crochet/templates/crochet/pattern.html:66
msgid "Imagen de portada" msgid "Imagen de portada"
msgstr "" 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." msgid "No se ha podido subir la imagen de portada. Inténtalo de nuevo."
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:84 #: crochet/templates/crochet/pattern.html:83
#: crochet/templates/crochet/pattern.html:168 #: crochet/templates/crochet/pattern.html:167
msgid "Color del texto" msgid "Color del texto"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:88 #: crochet/templates/crochet/pattern.html:87
msgid "Color de acento (títulos)" msgid "Color de acento (títulos)"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:92 #: crochet/templates/crochet/pattern.html:91
#: crochet/templates/crochet/pattern.html:168 #: crochet/templates/crochet/pattern.html:167
msgid "Color de fondo" msgid "Color de fondo"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:96 #: crochet/templates/crochet/pattern.html:95
msgid "Tipografía" msgid "Tipografía"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:102 #: crochet/templates/crochet/pattern.html:101
msgid "Monoespaciada" msgid "Monoespaciada"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:106 #: crochet/templates/crochet/pattern.html:105
msgid "Tamaño de letra" msgid "Tamaño de letra"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:108 #: crochet/templates/crochet/pattern.html:107
msgid "Pequeño" msgid "Pequeño"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:109 #: crochet/templates/crochet/pattern.html:108
msgid "Normal" msgid "Normal"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:110 #: crochet/templates/crochet/pattern.html:109
msgid "Grande" msgid "Grande"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:114 #: crochet/templates/crochet/pattern.html:113
msgid "Alineación del texto" msgid "Alineación del texto"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:116 #: crochet/templates/crochet/pattern.html:115
msgid "Izquierda" msgid "Izquierda"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:117 #: crochet/templates/crochet/pattern.html:116
msgid "Centrado" msgid "Centrado"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:118 #: crochet/templates/crochet/pattern.html:117
msgid "Justificado" msgid "Justificado"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:122 #: crochet/templates/crochet/pattern.html:121
msgid "Tamaño de página" msgid "Tamaño de página"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:127 #: crochet/templates/crochet/pattern.html:126
msgid "Carta (Letter)" msgid "Carta (Letter)"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:132 #: crochet/templates/crochet/pattern.html:131
msgid "Orientación" msgid "Orientación"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:134 #: crochet/templates/crochet/pattern.html:133
msgid "Vertical" msgid "Vertical"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:135 #: crochet/templates/crochet/pattern.html:134
msgid "Horizontal" msgid "Horizontal"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:143 #: crochet/templates/crochet/pattern.html:142
#: crochet/templates/crochet/pattern.html:151 #: crochet/templates/crochet/pattern.html:150
msgid "Secciones" msgid "Secciones"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:144 #: crochet/templates/crochet/pattern.html:143
#: crochet/templates/crochet/pattern.html:190 #: crochet/templates/crochet/pattern.html:189
msgid "Vista previa" msgid "Vista previa"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:153 #: crochet/templates/crochet/pattern.html:152
msgid "Colapsar todo" msgid "Colapsar todo"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:153 #: crochet/templates/crochet/pattern.html:152
msgid "Expandir todo" msgid "Expandir todo"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:158 #: crochet/templates/crochet/pattern.html:157
msgid "Añadir sección" msgid "Añadir sección"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:161 #: crochet/templates/crochet/pattern.html:160
msgid "Título" msgid "Título"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:161 #: crochet/templates/crochet/pattern.html:160
msgid "Título..." msgid "Título..."
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:163 #: crochet/templates/crochet/pattern.html:162
msgid "Subtítulo" msgid "Subtítulo"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:163 #: crochet/templates/crochet/pattern.html:162
msgid "Subtítulo..." msgid "Subtítulo..."
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:165 #: crochet/templates/crochet/pattern.html:164
msgid "Texto" msgid "Texto"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:165 #: crochet/templates/crochet/pattern.html:164
msgid "Escribe aquí..." msgid "Escribe aquí..."
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:166
#: crochet/templates/crochet/pattern.html:167 #: crochet/templates/crochet/pattern.html:167
#: crochet/templates/crochet/pattern.html:168
msgid "Nota" msgid "Nota"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:167 #: crochet/templates/crochet/pattern.html:166
msgid "Escribe una nota o consejo..." msgid "Escribe una nota o consejo..."
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:170 #: crochet/templates/crochet/pattern.html:169
msgid "Materiales" msgid "Materiales"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:170 #: crochet/templates/crochet/pattern.html:169
msgid "Añadir material..." msgid "Añadir material..."
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:170 #: crochet/templates/crochet/pattern.html:169
msgid "Añadir línea" msgid "Añadir línea"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:172 #: crochet/templates/crochet/pattern.html:171
#: crochet/templates/crochet/pattern.html:174 #: crochet/templates/crochet/pattern.html:173
msgid "Imagen" msgid "Imagen"
msgstr "" 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." msgid "No se ha podido subir la imagen. Inténtalo de nuevo."
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:176 #: crochet/templates/crochet/pattern.html:175
msgid "Patrón" msgid "Patrón"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:176 #: crochet/templates/crochet/pattern.html:175
msgid "Elementos" msgid "Elementos"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:178 #: crochet/templates/crochet/pattern.html:177
msgid "Grupo" msgid "Grupo"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:182 #: crochet/templates/crochet/pattern.html:181
msgid "Arrastrar para reordenar" msgid "Arrastrar para reordenar"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:183 #: crochet/templates/crochet/pattern.html:182
msgid "Colapsar / expandir" msgid "Colapsar / expandir"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:184 #: crochet/templates/crochet/pattern.html:183
msgid "Duplicar sección" msgid "Duplicar sección"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:185 #: crochet/templates/crochet/pattern.html:184
msgid "Eliminar sección" msgid "Eliminar sección"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:186 #: crochet/templates/crochet/pattern.html:185
msgid "" msgid ""
"¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las " "¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las "
"secciones que contiene." "secciones que contiene."
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:192 #: crochet/templates/crochet/pattern.html:191
msgid "Añade una sección para ver aquí el resultado." msgid "Añade una sección para ver aquí el resultado."
msgstr "" msgstr ""
@@ -384,52 +418,64 @@ msgstr ""
# Rutas traducibles de crochet/urls.py: la base "pattern/" se traduce como # Rutas traducibles de crochet/urls.py: la base "pattern/" se traduce como
# "patron/" (sin tilde, para no meter caracteres acentuados en la URL). # "patron/" (sin tilde, para no meter caracteres acentuados en la URL).
#: crochet/urls.py:33 #: crochet/urls.py:36
msgid "pattern/<uuid:uuid>/" msgid "pattern/<uuid:uuid>/"
msgstr "patron/<uuid:uuid>/" msgstr "patron/<uuid:uuid>/"
#: crochet/urls.py:34 #: crochet/urls.py:37
msgid "pattern/<uuid:uuid>/edit/" msgid "pattern/<uuid:uuid>/edit/"
msgstr "patron/<uuid:uuid>/editar/" msgstr "patron/<uuid:uuid>/editar/"
#: crochet/urls.py:35 #: crochet/urls.py:38
msgid "pattern/<uuid:uuid>/save/" msgid "pattern/<uuid:uuid>/save/"
msgstr "patron/<uuid:uuid>/guardar/" msgstr "patron/<uuid:uuid>/guardar/"
#: crochet/urls.py:36 #: crochet/urls.py:39
msgid "pattern/<uuid:uuid>/images/" msgid "pattern/<uuid:uuid>/images/"
msgstr "patron/<uuid:uuid>/imagenes/" msgstr "patron/<uuid:uuid>/imagenes/"
#: crochet/urls.py:37 #: crochet/urls.py:40
msgid "pattern/<uuid:uuid>/cover/" msgid "pattern/<uuid:uuid>/cover/"
msgstr "patron/<uuid:uuid>/portada/" msgstr "patron/<uuid:uuid>/portada/"
#: crochet/urls.py:38 #: crochet/urls.py:41
msgid "pattern/<uuid:uuid>/pdf/" msgid "pattern/<uuid:uuid>/pdf/"
msgstr "patron/<uuid:uuid>/pdf/" msgstr "patron/<uuid:uuid>/pdf/"
#: crochet/urls.py:39 #: crochet/urls.py:42
msgid "pattern/<uuid:uuid>/delete/" msgid "pattern/<uuid:uuid>/delete/"
msgstr "patron/<uuid:uuid>/eliminar/" msgstr "patron/<uuid:uuid>/eliminar/"
#: crochet/urls.py:40 #: crochet/urls.py:43
msgid "pattern/new/" msgid "pattern/new/"
msgstr "patron/nuevo/" msgstr "patron/nuevo/"
# Igual que "pattern/" arriba: "account/" se traduce como "cuenta/". # Igual que "pattern/" arriba: "account/" se traduce como "cuenta/".
#: crochet/urls.py:41 #: crochet/urls.py:44
msgid "account/" msgid "account/"
msgstr "cuenta/" 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/" msgid "account/register/"
msgstr "cuenta/registro/" msgstr "cuenta/registro/"
#: crochet/urls.py:44 #: crochet/urls.py:53
msgid "account/login/" msgid "account/login/"
msgstr "cuenta/entrar/" msgstr "cuenta/entrar/"
#: crochet/urls.py:49 #: crochet/urls.py:58
msgid "account/logout/" msgid "account/logout/"
msgstr "cuenta/salir/" msgstr "cuenta/salir/"
@@ -0,0 +1,21 @@
{% load i18n %}
<!-- hx-target="this"/hx-swap="outerHTML": la respuesta (éxito o con
errores de validación) sustituye este mismo <form>, así que no hace
falta JS propio para pintar el resultado. -->
<form hx-post="{% url 'crochet:account_settings_email' %}" hx-target="this" hx-swap="outerHTML"
class="flex flex-col gap-3">
{% csrf_token %}
{% if email_updated %}
<div role="alert" class="alert alert-success alert-soft">
<span>{% trans 'Email actualizado.' %}</span>
</div>
{% endif %}
<label class="flex flex-col gap-1">
<span class="text-sm">{{ email_form.email.label }}</span>
{{ email_form.email }}
{% for error in email_form.email.errors %}
<span class="text-error text-xs">{{ error }}</span>
{% endfor %}
</label>
<button type="submit" class="btn btn-primary self-start">{% trans 'Guardar email' %}</button>
</form>
@@ -0,0 +1,20 @@
{% load i18n %}
<form hx-post="{% url 'crochet:account_settings_password' %}" hx-target="this" hx-swap="outerHTML"
class="flex flex-col gap-3">
{% csrf_token %}
{% if password_updated %}
<div role="alert" class="alert alert-success alert-soft">
<span>{% trans 'Contraseña actualizada.' %}</span>
</div>
{% endif %}
{% for field in password_form %}
<label class="flex flex-col gap-1">
<span class="text-sm">{{ field.label }}</span>
{{ field }}
{% for error in field.errors %}
<span class="text-error text-xs">{{ error }}</span>
{% endfor %}
</label>
{% endfor %}
<button type="submit" class="btn btn-primary self-start">{% trans 'Cambiar contraseña' %}</button>
</form>
@@ -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). #} {# 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). #}
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" /> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="{% static 'css/theme.css' %}"> <link rel="stylesheet" href="{% static 'css/theme.css' %}">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
{% htmx_script %}
@@ -54,10 +54,5 @@
<p>{% trans 'Todavía no tienes ningún patrón.' %}</p> <p>{% trans 'Todavía no tienes ningún patrón.' %}</p>
</div> </div>
{% endif %} {% endif %}
<form method="post" action="{% url 'crochet:logout' %}" class="mt-8">
{% csrf_token %}
<button type="submit" class="btn btn-outline btn-sm">{% trans 'Cerrar sesión' %}</button>
</form>
</main> </main>
{% endblock %} {% endblock %}
@@ -0,0 +1,36 @@
{% extends 'crochet/base.html' %}
{% load i18n %}
{% block title %}{% trans 'Ajustes de la cuenta' %}{% endblock %}
<!-- Igual que pattern.html: la cabecera X-CSRFToken que exige Django en
todo POST, leída del propio csrf_token en vez de la cookie (ver
_account_email_form.html/_account_password_form.html, que llegan por
hx-post). -->
{% block body_attrs %} hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'{% endblock %}
{% block extra_css %}
{% include 'crochet/_platform_assets.html' %}
{% endblock %}
{% block content %}
<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 'Email' %}</h2>
{% include 'crochet/_account_email_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 'Cambiar contraseña' %}</h2>
{% include 'crochet/_account_password_form.html' %}
</div>
</section>
<a href="{% url 'crochet:account_home' %}" class="link link-primary">{% trans 'Volver a mis patrones' %}</a>
</main>
{% endblock %}
+26 -3
View File
@@ -1,4 +1,4 @@
{% load static i18n %} {% load static i18n crochet_i18n %}
<!DOCTYPE html> <!DOCTYPE html>
<!-- get_current_language (no la variable "lang" del contexto): la resuelve <!-- get_current_language (no la variable "lang" del contexto): la resuelve
LocaleMiddleware a partir del prefijo de la URL en cualquier vista, no LocaleMiddleware a partir del prefijo de la URL en cualquier vista, no
@@ -27,12 +27,35 @@
<a href="{% url 'crochet:home' %}" class="btn btn-ghost text-xl px-2">🧶 Crochet</a> <a href="{% url 'crochet:home' %}" class="btn btn-ghost text-xl px-2">🧶 Crochet</a>
</div> </div>
<div class="flex-none flex items-center gap-2"> <div class="flex-none flex items-center gap-2">
<!-- Cambia de idioma sin perder la página en la que estás: cada
ruta traducible (ver crochet/urls.py) cambia entera de un
idioma a otro, no solo el prefijo (/es/, /en/), así que no
basta con sustituirlo a mano en request.path (ver
crochet_i18n.translate_url). onchange en vez de un <form> normal
porque cada <option> lleva ya la URL completa de destino (no
hay un único action al que enviar un valor). -->
{% get_current_language as CURRENT_LANGUAGE %}
{% get_available_languages as AVAILABLE_LANGUAGES %}
<select class="select select-sm select-bordered" onchange="window.location.href=this.value" aria-label="{% trans 'Idioma' %}">
{% for lang_code, lang_name in AVAILABLE_LANGUAGES %}
<option value="{% translate_url request.path lang_code %}"{% if lang_code == CURRENT_LANGUAGE %} selected{% endif %}>{{ lang_name }}</option>
{% endfor %}
</select>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<span class="text-sm hidden sm:inline">{{ user.username }}</span> <!-- 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>
<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>
<form method="post" action="{% url 'crochet:logout' %}"> <form method="post" action="{% url 'crochet:logout' %}">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn btn-sm btn-ghost">{% trans 'Cerrar sesión' %}</button> <button type="submit" class="w-full text-left">{% trans 'Cerrar sesión' %}</button>
</form> </form>
</li>
</ul>
</div>
{% else %} {% else %}
<a href="{% url 'crochet:login' %}" class="btn btn-sm btn-ghost">{% trans 'Iniciar sesión' %}</a> <a href="{% url 'crochet:login' %}" class="btn btn-sm btn-ghost">{% trans 'Iniciar sesión' %}</a>
<a href="{% url 'crochet:register' %}" class="btn btn-sm btn-primary">{% trans 'Registrarme' %}</a> <a href="{% url 'crochet:register' %}" class="btn btn-sm btn-primary">{% trans 'Registrarme' %}</a>
+1 -2
View File
@@ -1,5 +1,5 @@
{% extends 'crochet/base.html' %} {% extends 'crochet/base.html' %}
{% load static i18n django_htmx %} {% load static i18n %}
{% block title %}Crochet{% endblock %} {% block title %}Crochet{% endblock %}
@@ -9,7 +9,6 @@
<!-- Leído por storage.js para la cabecera X-CSRFToken al guardar por fetch. --> <!-- Leído por storage.js para la cabecera X-CSRFToken al guardar por fetch. -->
<meta name="csrf-token" content="{{ csrf_token }}"> <meta name="csrf-token" content="{{ csrf_token }}">
{% include 'crochet/_platform_assets.html' %} {% include 'crochet/_platform_assets.html' %}
{% htmx_script %}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
<style id="page-size-style"></style> <style id="page-size-style"></style>
View File
+15
View File
@@ -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/<uuid>/editar/ frente a
# /en/pattern/<uuid>/edit/).
return django_translate_url(path, lang_code)
+142
View File
@@ -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'))
+47
View File
@@ -53,6 +53,53 @@ class HomeViewTests(TestCase):
self.assertEqual(response_en.status_code, 200) 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, '<option value="/en/"')
def test_translated_route_switcher_preserves_the_current_view(self):
# No solo cambia el prefijo (/es/ -> /en/): toda la ruta traducible
# cambia de idioma (ver crochet/urls.py: "cuenta/entrar/" frente a
# "account/login/"), así que la opción debe apuntar al equivalente
# en inglés de esta MISMA vista, no a request.path con el prefijo
# sustituido a mano.
with translation.override('es'):
es_url = reverse('crochet:login')
with translation.override('en'):
en_url = reverse('crochet:login')
response = self.client.get(es_url)
self.assertContains(response, f'value="{en_url}"')
def test_current_language_is_preselected(self):
response = self.client.get('/es/')
self.assertContains(response, '<option value="/es/" selected>')
self.assertContains(response, '<option value="/en/">')
class NavbarAccountDropdownTests(TestCase):
def test_logged_in_user_sees_settings_and_logout_inside_the_dropdown(self):
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
self.client.force_login(user)
with translation.override('es'):
settings_url = reverse('crochet:account_settings')
logout_url = reverse('crochet:logout')
response = self.client.get('/es/')
self.assertContains(response, 'dropdown dropdown-end')
self.assertContains(response, '>crocheter<')
self.assertContains(response, f'href="{settings_url}"')
self.assertContains(response, f'action="{logout_url}"')
def test_anonymous_user_does_not_see_the_dropdown(self):
response = self.client.get('/es/')
self.assertNotContains(response, 'dropdown dropdown-end')
class PatternEditViewTests(TestCase): class PatternEditViewTests(TestCase):
def setUp(self): def setUp(self):
self.owner = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1') self.owner = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
+9
View File
@@ -4,7 +4,10 @@ from django.utils.translation import gettext_lazy as _
from crochet.forms import StyledAuthenticationForm from crochet.forms import StyledAuthenticationForm
from crochet.views import ( from crochet.views import (
AccountEmailUpdateView,
AccountHomeView, AccountHomeView,
AccountPasswordChangeView,
AccountSettingsView,
HomeView, HomeView,
PatternCoverImageUploadView, PatternCoverImageUploadView,
PatternCreateView, PatternCreateView,
@@ -39,6 +42,12 @@ urlpatterns = [
path(_('pattern/<uuid:uuid>/delete/'), name='pattern_delete', view=PatternDeleteView.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(_('pattern/new/'), name='pattern_create', view=PatternCreateView.as_view()),
path(_('account/'), name='account_home', view=AccountHomeView.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/register/'), name='register', view=RegisterView.as_view()), path(_('account/register/'), name='register', view=RegisterView.as_view()),
path( path(
_('account/login/'), name='login', _('account/login/'), name='login',
+54 -3
View File
@@ -1,11 +1,11 @@
import json import json
import weasyprint import weasyprint
from django.contrib.auth import login from django.contrib.auth import login, update_session_auth_hash
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied, SuspiciousFileOperation from django.core.exceptions import PermissionDenied, SuspiciousFileOperation
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.text import get_valid_filename from django.utils.text import get_valid_filename
@@ -14,7 +14,7 @@ from django.views import View
from django.views.generic import CreateView, DetailView, TemplateView from django.views.generic import CreateView, DetailView, TemplateView
from crochet.cover_image import InvalidCoverImage, build_cover_image_variants from crochet.cover_image import InvalidCoverImage, build_cover_image_variants
from crochet.forms import StyledUserCreationForm from crochet.forms import EmailUpdateForm, StyledPasswordChangeForm, StyledUserCreationForm
from crochet.models import Pattern, PatternImage, StitchType from crochet.models import Pattern, PatternImage, StitchType
from crochet.pattern_render import render_pattern_html, sanitize_page_settings from crochet.pattern_render import render_pattern_html, sanitize_page_settings
@@ -257,6 +257,57 @@ class AccountHomeView(LoginRequiredMixin, TemplateView):
return context return context
class AccountSettingsView(LoginRequiredMixin, TemplateView):
template_name = 'crochet/account_settings.html'
def get_context_data(self, **kwargs):
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)
return context
class AccountEmailUpdateView(LoginRequiredMixin, View):
# Fragmento HTMX (ver _account_email_form.html, hx-post con
# hx-target="this"/hx-swap="outerHTML" en el propio <form>): en
# cualquier caso (éxito o error de validación) se devuelve el mismo
# parcial con 200, para que htmx reemplace el formulario con la
# versión actualizada sin recargar la página. htmx, por defecto, NO
# sustituye el contenido si la respuesta no es 2xx (lo trata como un
# error de red, sin pintar nada), así que un 400 aquí dejaría los
# errores de validación escritos en la respuesta pero invisibles.
def post(self, request):
form = EmailUpdateForm(request.POST, user=request.user)
if form.is_valid():
form.save()
form = EmailUpdateForm(user=request.user, initial={'email': request.user.email})
email_updated = True
else:
email_updated = False
return render(request, 'crochet/_account_email_form.html', {'email_form': form, 'email_updated': email_updated})
class AccountPasswordChangeView(LoginRequiredMixin, View):
# Mismo patrón que AccountEmailUpdateView (200 siempre, ver el porqué
# ahí). update_session_auth_hash es imprescindible: cambiar la
# contraseña invalida el hash de sesión que AuthenticationMiddleware
# comprueba en cada petición, así que sin esto el usuario se quedaría
# deslogueado justo después de cambiarla.
def post(self, request):
form = StyledPasswordChangeForm(user=request.user, data=request.POST)
if form.is_valid():
form.save()
update_session_auth_hash(request, form.user)
form = StyledPasswordChangeForm(user=request.user)
password_updated = True
else:
password_updated = False
return render(
request, 'crochet/_account_password_form.html',
{'password_form': form, 'password_updated': password_updated},
)
class PatternCreateView(LoginRequiredMixin, View): class PatternCreateView(LoginRequiredMixin, View):
# Solo POST (ver el <form> en account_home.html): crear un patrón no # 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. # debería poder dispararse desde un simple enlace GET sin token CSRF.