feat: improved UX
This commit is contained in:
@@ -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
@@ -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'])
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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)
|
||||
@@ -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'))
|
||||
@@ -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')
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user