Compare commits
2 Commits
c68a0e37d6
...
778ec18b17
| Author | SHA1 | Date | |
|---|---|---|---|
| 778ec18b17 | |||
| 82399f1391 |
@@ -29,6 +29,7 @@ THIRD_PARTY_APPS = [
|
||||
'corsheaders',
|
||||
'django_extensions',
|
||||
'django_filters',
|
||||
'django_htmx',
|
||||
'watchman',
|
||||
]
|
||||
|
||||
@@ -55,6 +56,7 @@ MIDDLEWARE = [
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django_htmx.middleware.HtmxMiddleware',
|
||||
]
|
||||
|
||||
if DEBUG:
|
||||
@@ -93,6 +95,14 @@ DATABASES = {
|
||||
}
|
||||
|
||||
|
||||
# Auth
|
||||
# https://docs.djangoproject.com/en/6.0/topics/auth/default/#all-authentication-views
|
||||
|
||||
LOGIN_URL = 'crochet:login'
|
||||
LOGIN_REDIRECT_URL = 'crochet:account_home'
|
||||
LOGOUT_REDIRECT_URL = 'crochet:login'
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
|
||||
|
||||
# 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
|
||||
# para que login/registro (ver registration/*.html) tengan la misma pinta
|
||||
# que el resto de inputs del sitio, sin depender de un paquete aparte
|
||||
# (django-widget-tweaks) solo para dos formularios de tres campos.
|
||||
INPUT_CLASSES = 'input input-bordered w-full'
|
||||
|
||||
|
||||
class StyledAuthenticationForm(AuthenticationForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs['class'] = INPUT_CLASSES
|
||||
|
||||
|
||||
class StyledUserCreationForm(UserCreationForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs['class'] = INPUT_CLASSES
|
||||
@@ -2,182 +2,373 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: crochet\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-17 11:03+0000\n"
|
||||
"Language: en\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
# crochet/templates/crochet/pattern.html
|
||||
#: crochet/templates/crochet/account_home.html:4
|
||||
msgid "Mis patrones"
|
||||
msgstr "My patterns"
|
||||
|
||||
msgid "No se ha podido guardar. Inténtalo de nuevo."
|
||||
msgstr "Couldn't save. Please try again."
|
||||
#: crochet/templates/crochet/account_home.html:13
|
||||
#, python-format
|
||||
msgid "Hola, %(username)s"
|
||||
msgstr "Hi, %(username)s"
|
||||
|
||||
msgid "Guardar"
|
||||
msgstr "Save"
|
||||
#: crochet/templates/crochet/account_home.html:16
|
||||
msgid "Crear patrón"
|
||||
msgstr "Create pattern"
|
||||
|
||||
msgid "Añadir sección"
|
||||
msgstr "Add section"
|
||||
#: crochet/templates/crochet/account_home.html:21
|
||||
#: crochet/templates/crochet/pattern.html:21
|
||||
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."
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:27
|
||||
msgid "Patrón sin título"
|
||||
msgstr "Untitled pattern"
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:30
|
||||
#, python-format
|
||||
msgid "Actualizado el %(date)s"
|
||||
msgstr "Updated on %(date)s"
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:33
|
||||
#: crochet/templates/crochet/pattern.html:34
|
||||
msgid "Ver patrón"
|
||||
msgstr "View pattern"
|
||||
|
||||
msgid "Descargar PDF"
|
||||
msgstr "Download PDF"
|
||||
#: crochet/templates/crochet/account_home.html:34
|
||||
msgid "Editar"
|
||||
msgstr "Edit"
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:38
|
||||
msgid "Eliminar"
|
||||
msgstr "Delete"
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:47
|
||||
msgid "Todavía no tienes ningún patrón."
|
||||
msgstr "You don't have any patterns yet."
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:53
|
||||
#: crochet/templates/crochet/base.html:34
|
||||
msgid "Cerrar sesión"
|
||||
msgstr "Log out"
|
||||
|
||||
#: crochet/templates/crochet/base.html:37
|
||||
#: 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/registration/register.html:29
|
||||
msgid "Registrarme"
|
||||
msgstr "Sign up"
|
||||
|
||||
# crochet/templates/crochet/pattern.html
|
||||
#: crochet/templates/crochet/pattern.html:31
|
||||
msgid "No se ha podido guardar. Inténtalo de nuevo."
|
||||
msgstr "Couldn't save. Please try again."
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:31
|
||||
msgid "Guardar"
|
||||
msgstr "Save"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:35
|
||||
msgid "Exportar a PDF"
|
||||
msgstr "Export to PDF"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:39
|
||||
msgid "Eliminar patrón"
|
||||
msgstr "Delete pattern"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:46
|
||||
msgid "Guardado"
|
||||
msgstr "Saved"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:53
|
||||
msgid "Personalización de página"
|
||||
msgstr "Page customization"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:57
|
||||
msgid "Título del patrón"
|
||||
msgstr "Pattern title"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:58
|
||||
msgid "Título del patrón..."
|
||||
msgstr "Pattern title..."
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:61
|
||||
msgid "Autor"
|
||||
msgstr "Author"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:62
|
||||
msgid "Autor..."
|
||||
msgstr "Author..."
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:67
|
||||
#: crochet/templates/crochet/pattern.html:151
|
||||
msgid "Color del texto"
|
||||
msgstr "Text color"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:71
|
||||
msgid "Color de acento (títulos)"
|
||||
msgstr "Accent color (headings)"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:75
|
||||
#: crochet/templates/crochet/pattern.html:151
|
||||
msgid "Color de fondo"
|
||||
msgstr "Background color"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:79
|
||||
msgid "Tipografía"
|
||||
msgstr "Font"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:85
|
||||
msgid "Monoespaciada"
|
||||
msgstr "Monospace"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:89
|
||||
msgid "Tamaño de letra"
|
||||
msgstr "Font size"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:91
|
||||
msgid "Pequeño"
|
||||
msgstr "Small"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:92
|
||||
msgid "Normal"
|
||||
msgstr "Normal"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:93
|
||||
msgid "Grande"
|
||||
msgstr "Large"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:97
|
||||
msgid "Alineación del texto"
|
||||
msgstr "Text alignment"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:99
|
||||
msgid "Izquierda"
|
||||
msgstr "Left"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:100
|
||||
msgid "Centrado"
|
||||
msgstr "Center"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:101
|
||||
msgid "Justificado"
|
||||
msgstr "Justify"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:105
|
||||
msgid "Tamaño de página"
|
||||
msgstr "Page size"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:110
|
||||
msgid "Carta (Letter)"
|
||||
msgstr "Letter"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:115
|
||||
msgid "Orientación"
|
||||
msgstr "Orientation"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:117
|
||||
msgid "Vertical"
|
||||
msgstr "Portrait"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:118
|
||||
msgid "Horizontal"
|
||||
msgstr "Landscape"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:126
|
||||
#: crochet/templates/crochet/pattern.html:134
|
||||
msgid "Secciones"
|
||||
msgstr "Sections"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:127
|
||||
#: crochet/templates/crochet/pattern.html:173
|
||||
msgid "Vista previa"
|
||||
msgstr "Preview"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:136
|
||||
msgid "Colapsar todo"
|
||||
msgstr "Collapse all"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:136
|
||||
msgid "Expandir todo"
|
||||
msgstr "Expand all"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:141
|
||||
msgid "Añadir sección"
|
||||
msgstr "Add section"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:144
|
||||
msgid "Título"
|
||||
msgstr "Title"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:144
|
||||
msgid "Título..."
|
||||
msgstr "Title..."
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:146
|
||||
msgid "Subtítulo"
|
||||
msgstr "Subtitle"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:146
|
||||
msgid "Subtítulo..."
|
||||
msgstr "Subtitle..."
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:148
|
||||
msgid "Texto"
|
||||
msgstr "Text"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:148
|
||||
msgid "Escribe aquí..."
|
||||
msgstr "Write here..."
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:150
|
||||
#: crochet/templates/crochet/pattern.html:151
|
||||
msgid "Nota"
|
||||
msgstr "Note"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:150
|
||||
msgid "Escribe una nota o consejo..."
|
||||
msgstr "Write a note or tip..."
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:153
|
||||
msgid "Materiales"
|
||||
msgstr "Materials"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:153
|
||||
msgid "Añadir material..."
|
||||
msgstr "Add material..."
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:153
|
||||
msgid "Añadir línea"
|
||||
msgstr "Add line"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:155
|
||||
#: crochet/templates/crochet/pattern.html:157
|
||||
msgid "Imagen"
|
||||
msgstr "Image"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:157
|
||||
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:159
|
||||
msgid "Patrón"
|
||||
msgstr "Pattern"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:159
|
||||
msgid "Elementos"
|
||||
msgstr "Elements"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:161
|
||||
msgid "Grupo"
|
||||
msgstr "Group"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:165
|
||||
msgid "Arrastrar para reordenar"
|
||||
msgstr "Drag to reorder"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:166
|
||||
msgid "Colapsar / expandir"
|
||||
msgstr "Collapse / expand"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:167
|
||||
msgid "Duplicar sección"
|
||||
msgstr "Duplicate section"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:168
|
||||
msgid "Eliminar sección"
|
||||
msgstr "Delete section"
|
||||
|
||||
msgid "¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las secciones que contiene."
|
||||
msgstr "Are you sure you want to delete this group? All sections inside it will also be deleted."
|
||||
#: crochet/templates/crochet/pattern.html:169
|
||||
msgid ""
|
||||
"¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las "
|
||||
"secciones que contiene."
|
||||
msgstr ""
|
||||
"Are you sure you want to delete this group? All sections inside it will also "
|
||||
"be deleted."
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:175
|
||||
msgid "Añade una sección para ver aquí el resultado."
|
||||
msgstr "Add a section to see the result here."
|
||||
|
||||
# crochet/views.py (PatternDetailView.EMPTY_MESSAGE)
|
||||
#: crochet/templates/crochet/pattern_detail.html:24
|
||||
msgid "Descargar PDF"
|
||||
msgstr "Download PDF"
|
||||
|
||||
#: crochet/templates/registration/login.html:17
|
||||
msgid "Usuario o contraseña incorrectos."
|
||||
msgstr "Incorrect username or password."
|
||||
|
||||
#: crochet/templates/registration/login.html:29
|
||||
msgid "Entrar"
|
||||
msgstr "Log in"
|
||||
|
||||
#: crochet/templates/registration/login.html:32
|
||||
msgid "¿No tienes cuenta? Regístrate"
|
||||
msgstr "Don't have an account? Sign up"
|
||||
|
||||
#: crochet/templates/registration/register.html:4
|
||||
#: crochet/templates/registration/register.html:14
|
||||
msgid "Crear cuenta"
|
||||
msgstr "Create account"
|
||||
|
||||
#: crochet/templates/registration/register.html:32
|
||||
msgid "¿Ya tienes cuenta? Inicia sesión"
|
||||
msgstr "Already have an account? Log in"
|
||||
|
||||
#: crochet/urls.py:30
|
||||
msgid "pattern/<uuid:uuid>/"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/urls.py:31
|
||||
msgid "pattern/<uuid:uuid>/edit/"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/urls.py:32
|
||||
msgid "pattern/<uuid:uuid>/save/"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/urls.py:33
|
||||
msgid "pattern/<uuid:uuid>/images/"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/urls.py:34
|
||||
msgid "pattern/<uuid:uuid>/pdf/"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/urls.py:35
|
||||
msgid "pattern/<uuid:uuid>/delete/"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/urls.py:36
|
||||
msgid "pattern/new/"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/urls.py:37
|
||||
msgid "account/"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/urls.py:38
|
||||
msgid "account/register/"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/urls.py:40
|
||||
msgid "account/login/"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/urls.py:45
|
||||
msgid "account/logout/"
|
||||
msgstr ""
|
||||
|
||||
# crochet/views.py (PatternDetailView.EMPTY_MESSAGE)
|
||||
#: crochet/views.py:23
|
||||
msgid "Este patrón todavía no tiene contenido."
|
||||
msgstr "This pattern doesn't have any content yet."
|
||||
|
||||
@@ -2,24 +2,372 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: crochet\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-17 11:03+0000\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:4
|
||||
msgid "Mis patrones"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:13
|
||||
#, python-format
|
||||
msgid "Hola, %(username)s"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:16
|
||||
msgid "Crear patrón"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:21
|
||||
#: crochet/templates/crochet/pattern.html:21
|
||||
msgid "¿Seguro que quieres eliminar este patrón? No podrás deshacerlo."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:27
|
||||
msgid "Patrón sin título"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:30
|
||||
#, python-format
|
||||
msgid "Actualizado el %(date)s"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:33
|
||||
#: crochet/templates/crochet/pattern.html:34
|
||||
msgid "Ver patrón"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:34
|
||||
msgid "Editar"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:38
|
||||
msgid "Eliminar"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:47
|
||||
msgid "Todavía no tienes ningún patrón."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:53
|
||||
#: crochet/templates/crochet/base.html:34
|
||||
msgid "Cerrar sesión"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/base.html:37
|
||||
#: 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/registration/register.html:29
|
||||
msgid "Registrarme"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:31
|
||||
msgid "No se ha podido guardar. Inténtalo de nuevo."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:31
|
||||
msgid "Guardar"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:35
|
||||
msgid "Exportar a PDF"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:39
|
||||
msgid "Eliminar patrón"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:46
|
||||
msgid "Guardado"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:53
|
||||
msgid "Personalización de página"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:57
|
||||
msgid "Título del patrón"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:58
|
||||
msgid "Título del patrón..."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:61
|
||||
msgid "Autor"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:62
|
||||
msgid "Autor..."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:67
|
||||
#: crochet/templates/crochet/pattern.html:151
|
||||
msgid "Color del texto"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:71
|
||||
msgid "Color de acento (títulos)"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:75
|
||||
#: crochet/templates/crochet/pattern.html:151
|
||||
msgid "Color de fondo"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:79
|
||||
msgid "Tipografía"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:85
|
||||
msgid "Monoespaciada"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:89
|
||||
msgid "Tamaño de letra"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:91
|
||||
msgid "Pequeño"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:92
|
||||
msgid "Normal"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:93
|
||||
msgid "Grande"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:97
|
||||
msgid "Alineación del texto"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:99
|
||||
msgid "Izquierda"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:100
|
||||
msgid "Centrado"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:101
|
||||
msgid "Justificado"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:105
|
||||
msgid "Tamaño de página"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:110
|
||||
msgid "Carta (Letter)"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:115
|
||||
msgid "Orientación"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:117
|
||||
msgid "Vertical"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:118
|
||||
msgid "Horizontal"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:126
|
||||
#: crochet/templates/crochet/pattern.html:134
|
||||
msgid "Secciones"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:127
|
||||
#: crochet/templates/crochet/pattern.html:173
|
||||
msgid "Vista previa"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:136
|
||||
msgid "Colapsar todo"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:136
|
||||
msgid "Expandir todo"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:141
|
||||
msgid "Añadir sección"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:144
|
||||
msgid "Título"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:144
|
||||
msgid "Título..."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:146
|
||||
msgid "Subtítulo"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:146
|
||||
msgid "Subtítulo..."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:148
|
||||
msgid "Texto"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:148
|
||||
msgid "Escribe aquí..."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:150
|
||||
#: crochet/templates/crochet/pattern.html:151
|
||||
msgid "Nota"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:150
|
||||
msgid "Escribe una nota o consejo..."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:153
|
||||
msgid "Materiales"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:153
|
||||
msgid "Añadir material..."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:153
|
||||
msgid "Añadir línea"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:155
|
||||
#: crochet/templates/crochet/pattern.html:157
|
||||
msgid "Imagen"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:157
|
||||
msgid "No se ha podido subir la imagen. Inténtalo de nuevo."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:159
|
||||
msgid "Patrón"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:159
|
||||
msgid "Elementos"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:161
|
||||
msgid "Grupo"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:165
|
||||
msgid "Arrastrar para reordenar"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:166
|
||||
msgid "Colapsar / expandir"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:167
|
||||
msgid "Duplicar sección"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:168
|
||||
msgid "Eliminar sección"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:169
|
||||
msgid ""
|
||||
"¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las "
|
||||
"secciones que contiene."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:175
|
||||
msgid "Añade una sección para ver aquí el resultado."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern_detail.html:24
|
||||
msgid "Descargar PDF"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/registration/login.html:17
|
||||
msgid "Usuario o contraseña incorrectos."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/registration/login.html:29
|
||||
msgid "Entrar"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/registration/login.html:32
|
||||
msgid "¿No tienes cuenta? Regístrate"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/registration/register.html:4
|
||||
#: crochet/templates/registration/register.html:14
|
||||
msgid "Crear cuenta"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/registration/register.html:32
|
||||
msgid "¿Ya tienes cuenta? Inicia sesión"
|
||||
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:30
|
||||
msgid "pattern/<uuid:uuid>/"
|
||||
msgstr "patron/<uuid:uuid>/"
|
||||
|
||||
#: crochet/urls.py:31
|
||||
msgid "pattern/<uuid:uuid>/edit/"
|
||||
msgstr "patron/<uuid:uuid>/editar/"
|
||||
|
||||
#: crochet/urls.py:32
|
||||
msgid "pattern/<uuid:uuid>/save/"
|
||||
msgstr "patron/<uuid:uuid>/guardar/"
|
||||
|
||||
#: crochet/urls.py:33
|
||||
msgid "pattern/<uuid:uuid>/images/"
|
||||
msgstr "patron/<uuid:uuid>/imagenes/"
|
||||
|
||||
#: crochet/urls.py:34
|
||||
msgid "pattern/<uuid:uuid>/pdf/"
|
||||
msgstr "patron/<uuid:uuid>/pdf/"
|
||||
|
||||
#: crochet/urls.py:35
|
||||
msgid "pattern/<uuid:uuid>/delete/"
|
||||
msgstr "patron/<uuid:uuid>/eliminar/"
|
||||
|
||||
#: crochet/urls.py:36
|
||||
msgid "pattern/new/"
|
||||
msgstr "patron/nuevo/"
|
||||
|
||||
# Igual que "pattern/" arriba: "account/" se traduce como "cuenta/".
|
||||
#: crochet/urls.py:37
|
||||
msgid "account/"
|
||||
msgstr "cuenta/"
|
||||
|
||||
#: crochet/urls.py:38
|
||||
msgid "account/register/"
|
||||
msgstr "cuenta/registro/"
|
||||
|
||||
#: crochet/urls.py:40
|
||||
msgid "account/login/"
|
||||
msgstr "cuenta/entrar/"
|
||||
|
||||
#: crochet/urls.py:45
|
||||
msgid "account/logout/"
|
||||
msgstr "cuenta/salir/"
|
||||
|
||||
#: crochet/views.py:23
|
||||
msgid "Este patrón todavía no tiene contenido."
|
||||
msgstr ""
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 6.0.7 on 2026-07-17 10:17
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('crochet', '0005_seed_more_stitch_types'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='pattern',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patterns', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
||||
@@ -11,6 +12,13 @@ class Pattern(models.Model):
|
||||
# front, aquí solo hace falta persistirla).
|
||||
sections = models.JSONField(default=list, blank=True)
|
||||
page_settings = models.JSONField(default=dict, blank=True)
|
||||
# Nullable porque los patrones creados antes de tener autenticación no
|
||||
# tienen propietario; SET_NULL (no CASCADE) para que borrar una cuenta no
|
||||
# se lleve por delante sus patrones.
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, null=True, blank=True,
|
||||
on_delete=models.SET_NULL, related_name='patterns',
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
/* Paleta de marca (estilo marketplace artesanal, tipo Etsy: naranja
|
||||
terracota + crema). Se define aquí, una sola vez, porque main.css lo
|
||||
carga toda plantilla (incluida pattern_detail.html, que no lleva
|
||||
daisyUI); theme.css (solo en páginas con daisyUI, ver
|
||||
_platform_assets.html) reusa estas mismas variables para el tema
|
||||
"crochet" en vez de repetir los valores hexadecimales. */
|
||||
:root {
|
||||
--brand-bg: #fffbf5;
|
||||
--brand-bg-soft: #f7efe1;
|
||||
--brand-border: #ede0c9;
|
||||
--brand-text: #2e2118;
|
||||
--brand-primary: #f1641e;
|
||||
--brand-primary-content: #ffffff;
|
||||
--brand-secondary: #2b6e63;
|
||||
--brand-accent: #e2a32d;
|
||||
|
||||
font-family: 'Poppins', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--brand-bg);
|
||||
color: var(--brand-text);
|
||||
}
|
||||
|
||||
/* HTML: <div class="breadcrumb"></div> */
|
||||
.breadcrumb {
|
||||
--s: 20px; /* control the shape */
|
||||
@@ -43,17 +67,18 @@
|
||||
.detail-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.375rem 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
color: inherit;
|
||||
border: 1px solid var(--brand-primary);
|
||||
border-radius: 999px;
|
||||
color: var(--brand-primary);
|
||||
text-decoration: none;
|
||||
background-color: #fff;
|
||||
background-color: transparent;
|
||||
}
|
||||
.detail-btn:hover {
|
||||
background-color: #f3f4f6;
|
||||
background-color: var(--brand-primary);
|
||||
color: var(--brand-primary-content);
|
||||
}
|
||||
|
||||
.note-box {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/* Tema daisyUI propio ("crochet"): paleta cálida de marketplace artesanal
|
||||
(naranja terracota + crema), inspirada en plataformas tipo Etsy. Reusa los
|
||||
valores de --brand-* definidos en main.css (que carga toda plantilla, con
|
||||
o sin daisyUI) en vez de repetir los hexadecimales.
|
||||
Se aplica vía data-theme="crochet" en <html> (ver base.html) y debe
|
||||
cargarse DESPUÉS del CDN de daisyUI (ver _platform_assets.html): ambas
|
||||
reglas tienen la misma especificidad ([data-theme=...] vs :root), así que
|
||||
gana la que aparece más tarde en la cascada. */
|
||||
[data-theme="crochet"] {
|
||||
color-scheme: light;
|
||||
|
||||
--color-base-100: var(--brand-bg);
|
||||
--color-base-200: var(--brand-bg-soft);
|
||||
--color-base-300: var(--brand-border);
|
||||
--color-base-content: var(--brand-text);
|
||||
|
||||
--color-primary: var(--brand-primary);
|
||||
--color-primary-content: var(--brand-primary-content);
|
||||
|
||||
--color-secondary: var(--brand-secondary);
|
||||
--color-secondary-content: #ffffff;
|
||||
|
||||
--color-accent: var(--brand-accent);
|
||||
--color-accent-content: var(--brand-text);
|
||||
|
||||
--color-neutral: var(--brand-text);
|
||||
--color-neutral-content: #fbf4ec;
|
||||
|
||||
--color-info: oklch(74% 0.16 232.661);
|
||||
--color-info-content: oklch(29% 0.066 243.157);
|
||||
--color-success: oklch(76% 0.177 163.223);
|
||||
--color-success-content: oklch(37% 0.077 168.94);
|
||||
--color-warning: oklch(82% 0.189 84.429);
|
||||
--color-warning-content: oklch(41% 0.112 45.904);
|
||||
--color-error: oklch(71% 0.194 13.428);
|
||||
--color-error-content: oklch(27% 0.105 12.094);
|
||||
|
||||
--radius-selector: 0.5rem;
|
||||
--radius-field: 0.75rem;
|
||||
--radius-box: 1rem;
|
||||
--size-selector: 0.25rem;
|
||||
--size-field: 0.25rem;
|
||||
--border: 1px;
|
||||
--depth: 1;
|
||||
--noise: 0;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% load static %}
|
||||
{# 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). #}
|
||||
<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">
|
||||
@@ -0,0 +1,56 @@
|
||||
{% extends 'crochet/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans 'Mis patrones' %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% include 'crochet/_platform_assets.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="max-w-4xl mx-auto px-4 py-8">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<h1 class="text-2xl font-semibold">{% blocktrans with username=user.username %}Hola, {{ username }}{% endblocktrans %}</h1>
|
||||
<form method="post" action="{% url 'crochet:pattern_create' %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">+ {% trans 'Crear patrón' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if patterns %}
|
||||
{% trans '¿Seguro que quieres eliminar este patrón? No podrás deshacerlo.' as delete_confirm_message %}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{% for pattern in patterns %}
|
||||
<div class="card bg-base-100 shadow border border-base-300">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-lg">
|
||||
{% if pattern.page_settings.title %}{{ pattern.page_settings.title }}{% else %}{% trans 'Patrón sin título' %}{% endif %}
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60">
|
||||
{% blocktrans with date=pattern.updated_at|date:'d/m/Y' %}Actualizado el {{ date }}{% endblocktrans %}
|
||||
</p>
|
||||
<div class="card-actions justify-end mt-2">
|
||||
<a href="{% url 'crochet:pattern_detail' pattern.uuid %}" class="btn btn-sm btn-outline">{% trans 'Ver patrón' %}</a>
|
||||
<a href="{% url 'crochet:pattern_edit' pattern.uuid %}" class="btn btn-sm btn-primary">{% trans 'Editar' %}</a>
|
||||
<form method="post" action="{% url 'crochet:pattern_delete' pattern.uuid %}"
|
||||
onsubmit="return confirm('{{ delete_confirm_message }}')">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-outline btn-error">{% trans 'Eliminar' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-16 text-base-content/60">
|
||||
<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,47 @@
|
||||
{% load static 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
|
||||
solo en las que la añaden a mano al contexto (PatternEditView,
|
||||
PatternDetailView). -->
|
||||
<html lang="{% get_current_language as html_lang %}{{ html_lang }}" data-theme="crochet">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Crochet{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body{% block body_attrs %}{% endblock %}>
|
||||
{% block navbar %}
|
||||
<!-- Cabecera de marca compartida por el editor y las páginas de cuenta
|
||||
(login/registro/mi cuenta): usa clases de daisyUI, así que solo
|
||||
tiene sentido en plantillas que incluyan _platform_assets.html.
|
||||
pattern_detail.html (vista de solo lectura/PDF) sobreescribe este
|
||||
bloque dejándolo vacío: no carga daisyUI ni debe llevar navegación,
|
||||
solo el propio patrón. no-print para que nunca salga en el PDF. -->
|
||||
<nav class="navbar bg-base-100 border-b border-base-300 px-4 no-print">
|
||||
<div class="flex-1">
|
||||
<a href="{% url 'crochet:account_home' %}" class="btn btn-ghost text-xl px-2">🧶 Crochet</a>
|
||||
</div>
|
||||
<div class="flex-none flex items-center gap-2">
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,33 +1,27 @@
|
||||
{% load static i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ lang }}">
|
||||
{% extends 'crochet/base.html' %}
|
||||
{% load static i18n django_htmx %}
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% block title %}Crochet{% endblock %}
|
||||
|
||||
{% block body_attrs %} hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<!-- Leído por storage.js para la cabecera X-CSRFToken al guardar por fetch. -->
|
||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||
<title>Crochet</title>
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<!-- Rellenada por js/page-settings.js con el @page (tamaño/orientación)
|
||||
elegido en "Personalización de página": un <style> aparte porque
|
||||
@page no se puede cambiar como una propiedad normal desde JS. -->
|
||||
<style id="page-size-style"></style>
|
||||
</head>
|
||||
{% include 'crochet/_platform_assets.html' %}
|
||||
{% htmx_script %}
|
||||
|
||||
<body>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
|
||||
<style id="page-size-style"></style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
|
||||
<header class="flex flex-wrap items-center justify-between gap-2 p-4 no-print">
|
||||
<h1 class="text-xl font-semibold">Crochet</h1>
|
||||
{% trans '¿Seguro que quieres eliminar este patrón? No podrás deshacerlo.' as delete_confirm_message %}
|
||||
<header class="flex flex-wrap items-center justify-end gap-2 p-4 no-print">
|
||||
<div class="flex gap-2 items-center">
|
||||
<!-- Idioma del contenido (título/subtítulo/texto de cada sección), no
|
||||
de la interfaz: la estructura de secciones es la misma para
|
||||
todos los idiomas, solo cambian los textos que el usuario ha
|
||||
escrito para el idioma seleccionado. Añadir un idioma nuevo es
|
||||
solo cuestión de añadir una <option> aquí. -->
|
||||
|
||||
<select id="language-select" class="select select-sm select-bordered">
|
||||
<option value="es" {% if lang == 'es' %}selected{% endif %}>Español</option>
|
||||
<option value="en" {% if lang == 'en' %}selected{% endif %}>English</option>
|
||||
@@ -35,18 +29,18 @@
|
||||
<button id="save-pattern" type="button" class="btn btn-sm btn-outline"
|
||||
data-save-url="{% url 'crochet:pattern_save' pattern.uuid %}"
|
||||
data-error-message="{% trans 'No se ha podido guardar. Inténtalo de nuevo.' %}">{% trans 'Guardar' %}</button>
|
||||
<!-- Enlace normal, no un botón con JS: solo navega a la vista de
|
||||
solo lectura (ver PatternDetailView), que se guarda tal cual
|
||||
desde la última vez que se pulsó "Guardar". target="_blank"
|
||||
para no perder el editor con sus cambios sin guardar. -->
|
||||
|
||||
<a href="{% url 'crochet:pattern_detail' pattern.uuid %}" target="_blank" rel="noopener"
|
||||
class="btn btn-sm btn-outline">{% trans 'Ver patrón' %}</a>
|
||||
<button id="export-pdf" type="button" class="btn btn-sm">{% trans 'Exportar a PDF' %}</button>
|
||||
<button id="export-pdf" type="button" class="btn btn-sm btn-primary">{% trans 'Exportar a PDF' %}</button>
|
||||
<form method="post" action="{% url 'crochet:pattern_delete' pattern.uuid %}"
|
||||
onsubmit="return confirm('{{ delete_confirm_message }}')">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-outline btn-error">{% trans 'Eliminar patrón' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Aviso breve de que "Guardar" ha funcionado: sin esto, guardar en
|
||||
localStorage no da ninguna señal de que ha ocurrido algo. -->
|
||||
<div id="save-toast" class="toast toast-top toast-end no-print hidden">
|
||||
<div class="alert alert-success">
|
||||
<span data-message="{% trans 'Guardado' %}">{% trans 'Guardado' %}</span>
|
||||
@@ -55,10 +49,6 @@
|
||||
|
||||
<div class="p-4 pt-0">
|
||||
|
||||
<!-- Configuración única para todo el documento (no es una sección más:
|
||||
no se repite, no se reordena ni se duplica), por eso vive fuera
|
||||
de #panel-top-sections. <details> nativo para no necesitar JS
|
||||
solo para plegarla/desplegarla. -->
|
||||
<details class="border border-base-300 rounded-box p-4 mb-4 no-print">
|
||||
<summary class="text-lg font-semibold cursor-pointer select-none">{% trans 'Personalización de página' %}</summary>
|
||||
<div class="flex flex-col gap-4 mt-4">
|
||||
@@ -145,13 +135,7 @@
|
||||
<button id="toggle-collapse-all" type="button" class="btn btn-xs btn-outline"
|
||||
data-collapse-all-label="{% trans 'Colapsar todo' %}" data-expand-all-label="{% trans 'Expandir todo' %}">{% trans 'Colapsar todo' %}</button>
|
||||
</div>
|
||||
<!-- Un único botón con menú (en vez de 7-8 botones sueltos que se
|
||||
partían en varias filas en pantallas estrechas): menos ruido
|
||||
visual antes de llegar al contenido real. daisyUI abre
|
||||
.dropdown-content con :focus-within, sin JS propio; el mismo
|
||||
botón (clonado) se reutiliza dentro de cada "Grupo" (ver
|
||||
addGroupSection en section-types.js), así que su
|
||||
data-label también viaja con él. -->
|
||||
|
||||
<div class="dropdown mb-4">
|
||||
<button id="add-section-toggle" type="button" class="btn btn-sm" tabindex="0"
|
||||
data-label="{% trans 'Añadir sección' %}">+ {% trans 'Añadir sección' %}</button>
|
||||
@@ -195,17 +179,12 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Catálogo de tipos de punto arrastrables (StitchType), leído por
|
||||
dnd-items.js. Antes era una lista fija en este HTML; ahora viene de
|
||||
la base de datos, con sus propias traducciones por idioma. -->
|
||||
{{ stitch_types|json_script:"stitch-types" }}
|
||||
|
||||
<!-- Contenido guardado de este patrón (sections/pageSettings), leído
|
||||
por main.js al arrancar. json_script escapa el JSON de forma
|
||||
segura, así que nunca hay que interpolarlo a mano en un <script>. -->
|
||||
{{ pattern_data|json_script:"pattern-data" }}
|
||||
</main>
|
||||
</body>
|
||||
<script type="module" src="{% static 'js/main.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
</html>
|
||||
{% block extra_js %}
|
||||
<script type="module" src="{% static 'js/main.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
{% extends 'crochet/base.html' %}
|
||||
{% load static i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ lang }}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ page_settings.title|default:"Crochet" }}</title>
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
<!-- CSS de Tailwind precompilado (ver .tools/build/ y css/README-pattern-detail.md),
|
||||
no el CDN con compilador JIT en JS que usa pattern.html: WeasyPrint
|
||||
(ver PatternPdfView) no ejecuta JavaScript, así que ese CDN no
|
||||
generaría ningún estilo al convertir esta misma página a PDF. Ninguna
|
||||
<script> propia tampoco: esta página es puramente de lectura,
|
||||
generada del todo en el servidor (ver pattern_render.py). No hay
|
||||
ningún <input>/<button> de edición que "revelar" quitando una clase
|
||||
desde las herramientas de desarrollo, porque ese marcado no existe
|
||||
aquí (a diferencia de la vista de edición, que sí lo necesita). -->
|
||||
{% block title %}{{ page_settings.title|default:"Crochet" }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/pattern-detail.css' %}">
|
||||
<style>
|
||||
:root {
|
||||
@@ -24,19 +12,15 @@
|
||||
}
|
||||
@page { size: {{ page_settings.pageSize }} {{ page_settings.orientation }}; }
|
||||
</style>
|
||||
</head>
|
||||
{% endblock %}
|
||||
|
||||
<body>
|
||||
<!-- Sin daisyUI aquí (ver arriba), así que el navbar de base.html no tiene
|
||||
ningún estilo que aplicarle: se deja vacío a propósito. -->
|
||||
{% block navbar %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="p-4">
|
||||
<!-- Enlaces normales (sin JS): el tag "language" de i18n activa
|
||||
temporalmente ese idioma para que "url" resuelva la ruta ya
|
||||
traducida (ver crochet/urls.py), igual que hace LocaleMiddleware
|
||||
por petición. no-print para que no aparezca en el PDF exportado
|
||||
(impresión nativa del navegador sobre esta misma página). -->
|
||||
<div class="flex justify-end gap-2 mb-4 no-print">
|
||||
<!-- Genera el PDF en el servidor con WeasyPrint (ver PatternPdfView):
|
||||
renderiza esta misma plantilla (ya con el CSS precompilado, sin
|
||||
depender de ningún JS) directamente a PDF. -->
|
||||
<a href="{% url 'crochet:pattern_pdf' pattern.uuid %}" class="detail-btn">{% trans 'Descargar PDF' %}</a>
|
||||
{% if lang != 'es' %}
|
||||
{% language 'es' %}
|
||||
@@ -57,6 +41,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{% extends 'crochet/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans 'Iniciar sesión' %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% include 'crochet/_platform_assets.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="flex justify-center px-4 py-12">
|
||||
<div class="card w-full max-w-sm bg-base-100 shadow-xl border border-base-300">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">{% trans 'Iniciar sesión' %}</h1>
|
||||
{% if form.errors %}
|
||||
<div role="alert" class="alert alert-error alert-soft">
|
||||
<span>{% trans 'Usuario o contraseña incorrectos.' %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post" class="flex flex-col gap-3">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm">{{ field.label }}</span>
|
||||
{{ field }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
<button type="submit" class="btn btn-primary mt-2">{% trans 'Entrar' %}</button>
|
||||
</form>
|
||||
<p class="text-sm text-center mt-2">
|
||||
<a href="{% url 'crochet:register' %}" class="link link-primary">{% trans '¿No tienes cuenta? Regístrate' %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,37 @@
|
||||
{% extends 'crochet/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans 'Crear cuenta' %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% include 'crochet/_platform_assets.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="flex justify-center px-4 py-12">
|
||||
<div class="card w-full max-w-sm bg-base-100 shadow-xl border border-base-300">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">{% trans 'Crear cuenta' %}</h1>
|
||||
<form method="post" class="flex flex-col gap-3">
|
||||
{% csrf_token %}
|
||||
{% for field in 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 %}
|
||||
{% if field.help_text %}
|
||||
<span class="text-xs text-base-content/60">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary mt-2">{% trans 'Registrarme' %}</button>
|
||||
</form>
|
||||
<p class="text-sm text-center mt-2">
|
||||
<a href="{% url 'crochet:login' %}" class="link link-primary">{% trans '¿Ya tienes cuenta? Inicia sesión' %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,154 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import translation
|
||||
|
||||
from crochet.models import Pattern
|
||||
|
||||
|
||||
class RegisterViewTests(TestCase):
|
||||
def setUp(self):
|
||||
with translation.override('es'):
|
||||
self.url = reverse('crochet:register')
|
||||
|
||||
def test_get_renders_form(self):
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'registration/register.html')
|
||||
|
||||
def test_post_creates_user_and_logs_in(self):
|
||||
response = self.client.post(self.url, {
|
||||
'username': 'crocheter',
|
||||
'password1': 'a-very-uncommon-pw-1',
|
||||
'password2': 'a-very-uncommon-pw-1',
|
||||
})
|
||||
|
||||
self.assertTrue(User.objects.filter(username='crocheter').exists())
|
||||
self.assertRedirects(response, reverse('crochet:account_home'))
|
||||
self.assertIn('_auth_user_id', self.client.session)
|
||||
|
||||
def test_post_with_mismatched_passwords_does_not_create_user(self):
|
||||
response = self.client.post(self.url, {
|
||||
'username': 'crocheter',
|
||||
'password1': 'a-very-uncommon-pw-1',
|
||||
'password2': 'something-else',
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(User.objects.filter(username='crocheter').exists())
|
||||
|
||||
|
||||
class LoginViewTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
|
||||
with translation.override('es'):
|
||||
self.url = reverse('crochet:login')
|
||||
|
||||
def test_get_renders_form(self):
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'registration/login.html')
|
||||
|
||||
def test_post_with_correct_credentials_logs_in(self):
|
||||
response = self.client.post(self.url, {
|
||||
'username': 'crocheter',
|
||||
'password': 'a-very-uncommon-pw-1',
|
||||
})
|
||||
|
||||
self.assertRedirects(response, reverse('crochet:account_home'))
|
||||
self.assertIn('_auth_user_id', self.client.session)
|
||||
|
||||
def test_post_with_wrong_password_does_not_log_in(self):
|
||||
response = self.client.post(self.url, {
|
||||
'username': 'crocheter',
|
||||
'password': 'wrong-password',
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn('_auth_user_id', self.client.session)
|
||||
|
||||
|
||||
class LogoutViewTests(TestCase):
|
||||
def test_post_logs_out(self):
|
||||
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
|
||||
self.client.force_login(user)
|
||||
|
||||
with translation.override('es'):
|
||||
url = reverse('crochet:logout')
|
||||
response = self.client.post(url)
|
||||
|
||||
self.assertRedirects(response, reverse('crochet:login'))
|
||||
self.assertNotIn('_auth_user_id', self.client.session)
|
||||
|
||||
|
||||
class AccountHomeViewTests(TestCase):
|
||||
def setUp(self):
|
||||
with translation.override('es'):
|
||||
self.url = reverse('crochet:account_home')
|
||||
|
||||
def test_anonymous_user_is_redirected_to_login(self):
|
||||
response = self.client.get(self.url)
|
||||
|
||||
with translation.override('es'):
|
||||
login_url = reverse('crochet:login')
|
||||
self.assertRedirects(response, f'{login_url}?next={self.url}')
|
||||
|
||||
def test_logged_in_user_sees_their_username(self):
|
||||
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
|
||||
self.client.force_login(user)
|
||||
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'crocheter')
|
||||
|
||||
def test_only_lists_own_patterns(self):
|
||||
owner = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
|
||||
other = User.objects.create_user(username='other', password='a-very-uncommon-pw-1')
|
||||
own_pattern = Pattern.objects.create(created_by=owner, page_settings={'title': 'Bufanda'})
|
||||
Pattern.objects.create(created_by=other, page_settings={'title': 'Gorro'})
|
||||
Pattern.objects.create() # sin propietario
|
||||
|
||||
self.client.force_login(owner)
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(list(response.context['patterns']), [own_pattern])
|
||||
self.assertContains(response, 'Bufanda')
|
||||
self.assertNotContains(response, 'Gorro')
|
||||
|
||||
|
||||
class PatternCreateViewTests(TestCase):
|
||||
def setUp(self):
|
||||
with translation.override('es'):
|
||||
self.url = reverse('crochet:pattern_create')
|
||||
|
||||
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}')
|
||||
self.assertEqual(Pattern.objects.count(), 0)
|
||||
|
||||
def test_get_is_not_allowed(self):
|
||||
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
|
||||
self.client.force_login(user)
|
||||
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
def test_post_creates_pattern_owned_by_current_user_and_redirects_to_editor(self):
|
||||
user = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
|
||||
self.client.force_login(user)
|
||||
|
||||
response = self.client.post(self.url)
|
||||
|
||||
self.assertEqual(Pattern.objects.count(), 1)
|
||||
pattern = Pattern.objects.get()
|
||||
self.assertEqual(pattern.created_by, user)
|
||||
with translation.override('es'):
|
||||
edit_url = reverse('crochet:pattern_edit', args=[pattern.uuid])
|
||||
self.assertRedirects(response, edit_url)
|
||||
@@ -1,5 +1,6 @@
|
||||
import io
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
from PIL import Image
|
||||
@@ -32,6 +33,21 @@ class PatternModelTests(TestCase):
|
||||
second = Pattern.objects.create()
|
||||
self.assertNotEqual(first.uuid, second.uuid)
|
||||
|
||||
def test_created_by_defaults_to_none(self):
|
||||
pattern = Pattern.objects.create()
|
||||
self.assertIsNone(pattern.created_by)
|
||||
|
||||
def test_created_by_survives_owner_deletion(self):
|
||||
# SET_NULL (no CASCADE): borrar la cuenta no debe llevarse por delante
|
||||
# el patrón, solo desasociarlo.
|
||||
owner = User.objects.create_user(username='crocheter', password='s3cret-pw')
|
||||
pattern = Pattern.objects.create(created_by=owner)
|
||||
|
||||
owner.delete()
|
||||
pattern.refresh_from_db()
|
||||
|
||||
self.assertIsNone(pattern.created_by)
|
||||
|
||||
|
||||
class PatternImageModelTests(TestCase):
|
||||
def test_deleted_with_pattern(self):
|
||||
|
||||
+112
-2
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import translation
|
||||
@@ -13,13 +14,16 @@ NO_HTML_COMMENTS_RE = re.compile(r'<!--.*?-->', re.S)
|
||||
|
||||
class PatternEditViewTests(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
|
||||
self.pattern = Pattern.objects.create(
|
||||
sections=[{'type': 'title', 'text': {'es': 'Ampharos', 'en': 'Ampharos'}}],
|
||||
page_settings={'title': 'Ampharos'},
|
||||
created_by=self.owner,
|
||||
)
|
||||
StitchType.objects.create(translations={'es': 'Punto bajo', 'en': 'SC'}, order=1)
|
||||
|
||||
def test_get_in_spanish(self):
|
||||
self.client.force_login(self.owner)
|
||||
with translation.override('es'):
|
||||
url = reverse('crochet:pattern_edit', args=[self.pattern.uuid])
|
||||
response = self.client.get(url)
|
||||
@@ -30,6 +34,7 @@ class PatternEditViewTests(TestCase):
|
||||
self.assertContains(response, 'Guardar')
|
||||
|
||||
def test_get_in_english(self):
|
||||
self.client.force_login(self.owner)
|
||||
with translation.override('en'):
|
||||
url = reverse('crochet:pattern_edit', args=[self.pattern.uuid])
|
||||
response = self.client.get(url)
|
||||
@@ -38,6 +43,7 @@ class PatternEditViewTests(TestCase):
|
||||
self.assertContains(response, 'Save')
|
||||
|
||||
def test_context_includes_saved_pattern_and_stitch_catalog(self):
|
||||
self.client.force_login(self.owner)
|
||||
with translation.override('es'):
|
||||
url = reverse('crochet:pattern_edit', args=[self.pattern.uuid])
|
||||
response = self.client.get(url)
|
||||
@@ -48,12 +54,39 @@ class PatternEditViewTests(TestCase):
|
||||
])
|
||||
|
||||
def test_unknown_uuid_returns_404(self):
|
||||
self.client.force_login(self.owner)
|
||||
with translation.override('es'):
|
||||
url = reverse('crochet:pattern_edit', args=['00000000-0000-0000-0000-000000000000'])
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_anonymous_user_is_redirected_to_login(self):
|
||||
with translation.override('es'):
|
||||
url = reverse('crochet:pattern_edit', args=[self.pattern.uuid])
|
||||
login_url = reverse('crochet:login')
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertRedirects(response, f'{login_url}?next={url}')
|
||||
|
||||
def test_other_user_gets_403(self):
|
||||
User.objects.create_user(username='other', password='a-very-uncommon-pw-1')
|
||||
self.client.login(username='other', password='a-very-uncommon-pw-1')
|
||||
with translation.override('es'):
|
||||
url = reverse('crochet:pattern_edit', args=[self.pattern.uuid])
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_pattern_without_owner_is_blocked_even_for_logged_in_user(self):
|
||||
orphan_pattern = Pattern.objects.create()
|
||||
self.client.force_login(self.owner)
|
||||
with translation.override('es'):
|
||||
url = reverse('crochet:pattern_edit', args=[orphan_pattern.uuid])
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class PatternDetailViewTests(TestCase):
|
||||
def setUp(self):
|
||||
@@ -170,7 +203,9 @@ class PatternI18nUrlTests(TestCase):
|
||||
|
||||
class PatternSaveViewTests(TestCase):
|
||||
def setUp(self):
|
||||
self.pattern = Pattern.objects.create()
|
||||
self.owner = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
|
||||
self.pattern = Pattern.objects.create(created_by=self.owner)
|
||||
self.client.force_login(self.owner)
|
||||
with translation.override('es'):
|
||||
self.url = reverse('crochet:pattern_save', args=[self.pattern.uuid])
|
||||
|
||||
@@ -205,10 +240,30 @@ class PatternSaveViewTests(TestCase):
|
||||
response = self.client.post(url, data=json.dumps({}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_anonymous_user_gets_403(self):
|
||||
self.client.logout()
|
||||
response = self.client.post(self.url, data=json.dumps({}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_other_user_gets_403(self):
|
||||
User.objects.create_user(username='other', password='a-very-uncommon-pw-1')
|
||||
self.client.login(username='other', password='a-very-uncommon-pw-1')
|
||||
response = self.client.post(self.url, data=json.dumps({}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_pattern_without_owner_is_blocked_even_for_logged_in_user(self):
|
||||
orphan_pattern = Pattern.objects.create()
|
||||
with translation.override('es'):
|
||||
url = reverse('crochet:pattern_save', args=[orphan_pattern.uuid])
|
||||
response = self.client.post(url, data=json.dumps({}), content_type='application/json')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class PatternImageUploadViewTests(TestCase):
|
||||
def setUp(self):
|
||||
self.pattern = Pattern.objects.create()
|
||||
self.owner = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
|
||||
self.pattern = Pattern.objects.create(created_by=self.owner)
|
||||
self.client.force_login(self.owner)
|
||||
with translation.override('es'):
|
||||
self.url = reverse('crochet:pattern_image_upload', args=[self.pattern.uuid])
|
||||
|
||||
@@ -228,3 +283,58 @@ class PatternImageUploadViewTests(TestCase):
|
||||
def test_missing_file_returns_bad_request(self):
|
||||
response = self.client.post(self.url, data={})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_other_user_gets_403(self):
|
||||
User.objects.create_user(username='other', password='a-very-uncommon-pw-1')
|
||||
self.client.login(username='other', password='a-very-uncommon-pw-1')
|
||||
response = self.client.post(self.url, data={'image': make_test_image_file()})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class PatternDeleteViewTests(TestCase):
|
||||
def setUp(self):
|
||||
self.owner = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
|
||||
self.pattern = Pattern.objects.create(created_by=self.owner)
|
||||
with translation.override('es'):
|
||||
self.url = reverse('crochet:pattern_delete', args=[self.pattern.uuid])
|
||||
|
||||
def test_owner_can_delete(self):
|
||||
self.client.force_login(self.owner)
|
||||
response = self.client.post(self.url)
|
||||
|
||||
with translation.override('es'):
|
||||
account_home_url = reverse('crochet:account_home')
|
||||
self.assertRedirects(response, account_home_url)
|
||||
self.assertFalse(Pattern.objects.filter(pk=self.pattern.pk).exists())
|
||||
|
||||
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}')
|
||||
self.assertTrue(Pattern.objects.filter(pk=self.pattern.pk).exists())
|
||||
|
||||
def test_other_user_gets_403(self):
|
||||
User.objects.create_user(username='other', password='a-very-uncommon-pw-1')
|
||||
self.client.login(username='other', password='a-very-uncommon-pw-1')
|
||||
response = self.client.post(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertTrue(Pattern.objects.filter(pk=self.pattern.pk).exists())
|
||||
|
||||
def test_pattern_without_owner_is_blocked_even_for_logged_in_user(self):
|
||||
orphan_pattern = Pattern.objects.create()
|
||||
self.client.force_login(self.owner)
|
||||
with translation.override('es'):
|
||||
url = reverse('crochet:pattern_delete', args=[orphan_pattern.uuid])
|
||||
response = self.client.post(url)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertTrue(Pattern.objects.filter(pk=orphan_pattern.pk).exists())
|
||||
|
||||
def test_get_is_not_allowed(self):
|
||||
self.client.force_login(self.owner)
|
||||
response = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.urls import path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from crochet.forms import StyledAuthenticationForm
|
||||
from crochet.views import (
|
||||
AccountHomeView,
|
||||
PatternCreateView,
|
||||
PatternDeleteView,
|
||||
PatternDetailView,
|
||||
PatternEditView,
|
||||
PatternImageUploadView,
|
||||
PatternPdfView,
|
||||
PatternSaveView,
|
||||
RegisterView,
|
||||
)
|
||||
|
||||
app_name = 'crochet'
|
||||
@@ -26,4 +32,15 @@ urlpatterns = [
|
||||
path(_('pattern/<uuid:uuid>/save/'), name='pattern_save', view=PatternSaveView.as_view()),
|
||||
path(_('pattern/<uuid:uuid>/images/'), name='pattern_image_upload', view=PatternImageUploadView.as_view()),
|
||||
path(_('pattern/<uuid:uuid>/pdf/'), name='pattern_pdf', view=PatternPdfView.as_view()),
|
||||
path(_('pattern/<uuid:uuid>/delete/'), name='pattern_delete', view=PatternDeleteView.as_view()),
|
||||
path(_('pattern/new/'), name='pattern_create', view=PatternCreateView.as_view()),
|
||||
path(_('account/'), name='account_home', view=AccountHomeView.as_view()),
|
||||
path(_('account/register/'), name='register', view=RegisterView.as_view()),
|
||||
path(
|
||||
_('account/login/'), name='login',
|
||||
view=auth_views.LoginView.as_view(
|
||||
template_name='registration/login.html', authentication_form=StyledAuthenticationForm,
|
||||
),
|
||||
),
|
||||
path(_('account/logout/'), name='logout', view=auth_views.LogoutView.as_view()),
|
||||
]
|
||||
|
||||
+82
-6
@@ -1,15 +1,19 @@
|
||||
import json
|
||||
|
||||
import weasyprint
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
from django.contrib.auth import login
|
||||
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
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.text import get_valid_filename
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic import DetailView
|
||||
from django.views.generic import CreateView, DetailView, TemplateView
|
||||
|
||||
from crochet.forms import StyledUserCreationForm
|
||||
from crochet.models import Pattern, PatternImage, StitchType
|
||||
from crochet.pattern_render import render_pattern_html, sanitize_page_settings
|
||||
|
||||
@@ -28,6 +32,19 @@ def _stitch_types_data():
|
||||
]
|
||||
|
||||
|
||||
def _get_owned_pattern_or_403(request, uuid):
|
||||
# Compartida por todo lo que modifica un patrón (guardar, subir imagen,
|
||||
# borrar) y por PatternEditView: created_by=None son patrones de antes de
|
||||
# tener autenticación, sin dueño al que atribuírselos, así que se
|
||||
# bloquea su edición para todo el mundo en vez de dejarlos abiertos o
|
||||
# asignárselos a quien los toque primero.
|
||||
pattern = get_object_or_404(Pattern, uuid=uuid)
|
||||
if pattern.created_by_id is None or not request.user.is_authenticated \
|
||||
or request.user.id != pattern.created_by_id:
|
||||
raise PermissionDenied
|
||||
return pattern
|
||||
|
||||
|
||||
def _pattern_detail_context(pattern):
|
||||
# Compartido por PatternDetailView (la pinta) y PatternPdfView (la
|
||||
# convierte a PDF con WeasyPrint a partir del mismo HTML), para no
|
||||
@@ -44,13 +61,21 @@ def _pattern_detail_context(pattern):
|
||||
}
|
||||
|
||||
|
||||
class PatternEditView(DetailView):
|
||||
class PatternEditView(LoginRequiredMixin, DetailView):
|
||||
model = Pattern
|
||||
template_name = 'crochet/pattern.html'
|
||||
context_object_name = 'pattern'
|
||||
slug_field = 'uuid'
|
||||
slug_url_kwarg = 'uuid'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
pattern = super().get_object(queryset)
|
||||
# LoginRequiredMixin ya garantiza que request.user está autenticado
|
||||
# aquí (si no, redirige a login antes de llegar a get_object).
|
||||
if pattern.created_by_id is None or pattern.created_by_id != self.request.user.id:
|
||||
raise PermissionDenied
|
||||
return pattern
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Resuelto por LocaleMiddleware a partir del prefijo de idioma de la
|
||||
@@ -135,8 +160,13 @@ class PatternPdfView(DetailView):
|
||||
|
||||
|
||||
class PatternSaveView(View):
|
||||
# Sin LoginRequiredMixin a propósito: esto lo llama storage.js por
|
||||
# fetch, y su redirección a login (una respuesta 200 con el HTML del
|
||||
# formulario) haría que "response.ok" diera un falso "guardado" en vez
|
||||
# de disparar el aviso de error. _get_owned_pattern_or_403 ya cubre
|
||||
# tanto "sin sesión" como "de otro usuario" con un 403 real.
|
||||
def post(self, request, uuid):
|
||||
pattern = get_object_or_404(Pattern, uuid=uuid)
|
||||
pattern = _get_owned_pattern_or_403(request, uuid)
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
@@ -151,8 +181,9 @@ class PatternSaveView(View):
|
||||
|
||||
|
||||
class PatternImageUploadView(View):
|
||||
# Mismo motivo que en PatternSaveView: lo llama sections.js por fetch.
|
||||
def post(self, request, uuid):
|
||||
pattern = get_object_or_404(Pattern, uuid=uuid)
|
||||
pattern = _get_owned_pattern_or_403(request, uuid)
|
||||
|
||||
image_file = request.FILES.get('image')
|
||||
if not image_file:
|
||||
@@ -160,3 +191,48 @@ class PatternImageUploadView(View):
|
||||
|
||||
pattern_image = PatternImage.objects.create(pattern=pattern, image=image_file)
|
||||
return JsonResponse({'id': pattern_image.id, 'url': pattern_image.image.url})
|
||||
|
||||
|
||||
class RegisterView(CreateView):
|
||||
# UserCreationForm ya pide justo nombre de usuario + contraseña (dos
|
||||
# veces, para confirmarla): no hace falta un formulario propio, solo la
|
||||
# versión con clases de daisyUI en sus widgets (ver crochet/forms.py).
|
||||
form_class = StyledUserCreationForm
|
||||
template_name = 'registration/register.html'
|
||||
success_url = reverse_lazy('crochet:account_home')
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
# Registrarse ya cuenta como haber probado usuario/contraseña, así
|
||||
# que se inicia sesión directamente en vez de mandar al usuario a
|
||||
# loguearse otra vez con lo que acaba de escribir.
|
||||
login(self.request, self.object)
|
||||
return response
|
||||
|
||||
|
||||
class AccountHomeView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'crochet/account_home.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['patterns'] = self.request.user.patterns.order_by('-updated_at')
|
||||
return context
|
||||
|
||||
|
||||
class PatternCreateView(LoginRequiredMixin, View):
|
||||
# Solo POST (ver el <form> en account_home.html): crear un patrón no
|
||||
# debería poder dispararse desde un simple enlace GET sin token CSRF.
|
||||
def post(self, request):
|
||||
pattern = Pattern.objects.create(created_by=request.user)
|
||||
return redirect('crochet:pattern_edit', uuid=pattern.uuid)
|
||||
|
||||
|
||||
class PatternDeleteView(LoginRequiredMixin, View):
|
||||
# Con LoginRequiredMixin (a diferencia de PatternSaveView/
|
||||
# PatternImageUploadView): esto lo dispara un <form> normal (ver
|
||||
# account_home.html y pattern.html), no fetch, así que a un anónimo le
|
||||
# viene mejor la redirección a login que un 403 a secas.
|
||||
def post(self, request, uuid):
|
||||
pattern = _get_owned_pattern_or_403(request, uuid)
|
||||
pattern.delete()
|
||||
return redirect('crochet:account_home')
|
||||
|
||||
@@ -10,6 +10,7 @@ dependencies = [
|
||||
"django-environ>=0.14.0",
|
||||
"django-extensions>=4.1",
|
||||
"django-filter>=26.1",
|
||||
"django-htmx>=1.28.0",
|
||||
"django-silk>=5.5.0",
|
||||
"django-storages>=1.14.6",
|
||||
"ipython>=9.15.0",
|
||||
|
||||
@@ -239,6 +239,7 @@ dependencies = [
|
||||
{ name = "django-environ" },
|
||||
{ name = "django-extensions" },
|
||||
{ name = "django-filter" },
|
||||
{ name = "django-htmx" },
|
||||
{ name = "django-silk" },
|
||||
{ name = "django-storages" },
|
||||
{ name = "ipython" },
|
||||
@@ -268,6 +269,7 @@ requires-dist = [
|
||||
{ name = "django-environ", specifier = ">=0.14.0" },
|
||||
{ name = "django-extensions", specifier = ">=4.1" },
|
||||
{ name = "django-filter", specifier = ">=26.1" },
|
||||
{ name = "django-htmx", specifier = ">=1.28.0" },
|
||||
{ name = "django-silk", specifier = ">=5.5.0" },
|
||||
{ name = "django-storages", specifier = ">=1.14.6" },
|
||||
{ name = "ipython", specifier = ">=9.15.0" },
|
||||
@@ -398,6 +400,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/01/afffed1e3c4540fb75bf550a18b6176a9f6371b5f3e52b69a28995b6480c/django_filter-26.1-py3-none-any.whl", hash = "sha256:7d98ef2899218e6242619b532cb1b95af14e09dfcf74844aecb550ad27b59ff2", size = 94069, upload-time = "2026-07-11T09:27:01.012Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-htmx"
|
||||
version = "1.28.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/d9/b31339fae8ede6a7cb0b9facaefe2ee4fc01d9f37a31a3dc03759765a2c3/django_htmx-1.28.0.tar.gz", hash = "sha256:699e229de0bbf19b3ee1a5d419702f5c154fa956e6921fec89faa7401b99e536", size = 100946, upload-time = "2026-07-12T22:19:19.589Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/d5/71b3a206f7a2135e2ab4a789be143a00222b52d22729c84f480915049e94/django_htmx-1.28.0-py3-none-any.whl", hash = "sha256:e6911acccf3e7905d511c4fd6ab86fe06572205e80a0f32ca7aa94a115ae0619", size = 98588, upload-time = "2026-07-12T22:19:18.225Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-silk"
|
||||
version = "5.5.0"
|
||||
|
||||
Reference in New Issue
Block a user