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 }}
- 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
+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
# 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'])
+129 -83
View File
@@ -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/<uuid:uuid>/"
msgstr ""
#: crochet/urls.py:34
#: crochet/urls.py:37
msgid "pattern/<uuid:uuid>/edit/"
msgstr ""
#: crochet/urls.py:35
#: crochet/urls.py:38
msgid "pattern/<uuid:uuid>/save/"
msgstr ""
#: crochet/urls.py:36
#: crochet/urls.py:39
msgid "pattern/<uuid:uuid>/images/"
msgstr ""
#: crochet/urls.py:37
#: crochet/urls.py:40
msgid "pattern/<uuid:uuid>/cover/"
msgstr ""
#: crochet/urls.py:38
#: crochet/urls.py:41
msgid "pattern/<uuid:uuid>/pdf/"
msgstr ""
#: crochet/urls.py:39
#: crochet/urls.py:42
msgid "pattern/<uuid:uuid>/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 ""
+127 -81
View File
@@ -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/<uuid:uuid>/"
msgstr "patron/<uuid:uuid>/"
#: crochet/urls.py:34
#: crochet/urls.py:37
msgid "pattern/<uuid:uuid>/edit/"
msgstr "patron/<uuid:uuid>/editar/"
#: crochet/urls.py:35
#: crochet/urls.py:38
msgid "pattern/<uuid:uuid>/save/"
msgstr "patron/<uuid:uuid>/guardar/"
#: crochet/urls.py:36
#: crochet/urls.py:39
msgid "pattern/<uuid:uuid>/images/"
msgstr "patron/<uuid:uuid>/imagenes/"
#: crochet/urls.py:37
#: crochet/urls.py:40
msgid "pattern/<uuid:uuid>/cover/"
msgstr "patron/<uuid:uuid>/portada/"
#: crochet/urls.py:38
#: crochet/urls.py:41
msgid "pattern/<uuid:uuid>/pdf/"
msgstr "patron/<uuid:uuid>/pdf/"
#: crochet/urls.py:39
#: crochet/urls.py:42
msgid "pattern/<uuid:uuid>/delete/"
msgstr "patron/<uuid:uuid>/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/"
@@ -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). #}
{# 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" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="{% static 'css/theme.css' %}">
<link rel="preconnect" href="https://fonts.googleapis.com">
<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">
{% htmx_script %}
@@ -54,10 +54,5 @@
<p>{% trans 'Todavía no tienes ningún patrón.' %}</p>
</div>
{% 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>
{% 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 %}
+29 -6
View File
@@ -1,4 +1,4 @@
{% load static i18n %}
{% load static i18n crochet_i18n %}
<!DOCTYPE html>
<!-- get_current_language (no la variable "lang" del contexto): la resuelve
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>
</div>
<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 %}
<span class="text-sm hidden sm:inline">{{ user.username }}</span>
<form method="post" action="{% url 'crochet:logout' %}">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-ghost">{% trans 'Cerrar sesión' %}</button>
</form>
<!-- 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' %}">
{% csrf_token %}
<button type="submit" class="w-full text-left">{% trans 'Cerrar sesión' %}</button>
</form>
</li>
</ul>
</div>
{% else %}
<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>
+1 -2
View File
@@ -1,5 +1,5 @@
{% extends 'crochet/base.html' %}
{% load static i18n django_htmx %}
{% load static i18n %}
{% block title %}Crochet{% endblock %}
@@ -9,7 +9,6 @@
<!-- Leído por storage.js para la cabecera X-CSRFToken al guardar por fetch. -->
<meta name="csrf-token" content="{{ csrf_token }}">
{% include 'crochet/_platform_assets.html' %}
{% htmx_script %}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
<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)
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):
def setUp(self):
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.views import (
AccountEmailUpdateView,
AccountHomeView,
AccountPasswordChangeView,
AccountSettingsView,
HomeView,
PatternCoverImageUploadView,
PatternCreateView,
@@ -39,6 +42,12 @@ urlpatterns = [
path(_('pattern/<uuid:uuid>/delete/'), name='pattern_delete', view=PatternDeleteView.as_view()),
path(_('pattern/new/'), name='pattern_create', view=PatternCreateView.as_view()),
path(_('account/'), name='account_home', view=AccountHomeView.as_view()),
path(_('account/settings/'), name='account_settings', view=AccountSettingsView.as_view()),
path(_('account/settings/email/'), name='account_settings_email', view=AccountEmailUpdateView.as_view()),
path(
_('account/settings/password/'), name='account_settings_password',
view=AccountPasswordChangeView.as_view(),
),
path(_('account/register/'), name='register', view=RegisterView.as_view()),
path(
_('account/login/'), name='login',
+54 -3
View File
@@ -1,11 +1,11 @@
import json
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.core.exceptions import PermissionDenied, SuspiciousFileOperation
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.urls import reverse_lazy
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 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.pattern_render import render_pattern_html, sanitize_page_settings
@@ -257,6 +257,57 @@ class AccountHomeView(LoginRequiredMixin, TemplateView):
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):
# 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.