Compare commits
2 Commits
c68a0e37d6
...
778ec18b17
| Author | SHA1 | Date | |
|---|---|---|---|
| 778ec18b17 | |||
| 82399f1391 |
@@ -29,6 +29,7 @@ THIRD_PARTY_APPS = [
|
|||||||
'corsheaders',
|
'corsheaders',
|
||||||
'django_extensions',
|
'django_extensions',
|
||||||
'django_filters',
|
'django_filters',
|
||||||
|
'django_htmx',
|
||||||
'watchman',
|
'watchman',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'django_htmx.middleware.HtmxMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
if DEBUG:
|
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
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
# 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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: crochet\n"
|
"Project-Id-Version: crochet\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-07-17 11:03+0000\n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
# crochet/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."
|
#: crochet/templates/crochet/account_home.html:13
|
||||||
msgstr "Couldn't save. Please try again."
|
#, python-format
|
||||||
|
msgid "Hola, %(username)s"
|
||||||
|
msgstr "Hi, %(username)s"
|
||||||
|
|
||||||
msgid "Guardar"
|
#: crochet/templates/crochet/account_home.html:16
|
||||||
msgstr "Save"
|
msgid "Crear patrón"
|
||||||
|
msgstr "Create pattern"
|
||||||
|
|
||||||
msgid "Añadir sección"
|
#: crochet/templates/crochet/account_home.html:21
|
||||||
msgstr "Add section"
|
#: 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"
|
msgid "Ver patrón"
|
||||||
msgstr "View pattern"
|
msgstr "View pattern"
|
||||||
|
|
||||||
msgid "Descargar PDF"
|
#: crochet/templates/crochet/account_home.html:34
|
||||||
msgstr "Download PDF"
|
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"
|
msgid "Exportar a PDF"
|
||||||
msgstr "Export to 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"
|
msgid "Guardado"
|
||||||
msgstr "Saved"
|
msgstr "Saved"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:53
|
||||||
msgid "Personalización de página"
|
msgid "Personalización de página"
|
||||||
msgstr "Page customization"
|
msgstr "Page customization"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:57
|
||||||
msgid "Título del patrón"
|
msgid "Título del patrón"
|
||||||
msgstr "Pattern title"
|
msgstr "Pattern title"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:58
|
||||||
msgid "Título del patrón..."
|
msgid "Título del patrón..."
|
||||||
msgstr "Pattern title..."
|
msgstr "Pattern title..."
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:61
|
||||||
msgid "Autor"
|
msgid "Autor"
|
||||||
msgstr "Author"
|
msgstr "Author"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:62
|
||||||
msgid "Autor..."
|
msgid "Autor..."
|
||||||
msgstr "Author..."
|
msgstr "Author..."
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:67
|
||||||
|
#: crochet/templates/crochet/pattern.html:151
|
||||||
msgid "Color del texto"
|
msgid "Color del texto"
|
||||||
msgstr "Text color"
|
msgstr "Text color"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:71
|
||||||
msgid "Color de acento (títulos)"
|
msgid "Color de acento (títulos)"
|
||||||
msgstr "Accent color (headings)"
|
msgstr "Accent color (headings)"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:75
|
||||||
|
#: crochet/templates/crochet/pattern.html:151
|
||||||
msgid "Color de fondo"
|
msgid "Color de fondo"
|
||||||
msgstr "Background color"
|
msgstr "Background color"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:79
|
||||||
msgid "Tipografía"
|
msgid "Tipografía"
|
||||||
msgstr "Font"
|
msgstr "Font"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:85
|
||||||
msgid "Monoespaciada"
|
msgid "Monoespaciada"
|
||||||
msgstr "Monospace"
|
msgstr "Monospace"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:89
|
||||||
msgid "Tamaño de letra"
|
msgid "Tamaño de letra"
|
||||||
msgstr "Font size"
|
msgstr "Font size"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:91
|
||||||
msgid "Pequeño"
|
msgid "Pequeño"
|
||||||
msgstr "Small"
|
msgstr "Small"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:92
|
||||||
msgid "Normal"
|
msgid "Normal"
|
||||||
msgstr "Normal"
|
msgstr "Normal"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:93
|
||||||
msgid "Grande"
|
msgid "Grande"
|
||||||
msgstr "Large"
|
msgstr "Large"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:97
|
||||||
msgid "Alineación del texto"
|
msgid "Alineación del texto"
|
||||||
msgstr "Text alignment"
|
msgstr "Text alignment"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:99
|
||||||
msgid "Izquierda"
|
msgid "Izquierda"
|
||||||
msgstr "Left"
|
msgstr "Left"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:100
|
||||||
msgid "Centrado"
|
msgid "Centrado"
|
||||||
msgstr "Center"
|
msgstr "Center"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:101
|
||||||
msgid "Justificado"
|
msgid "Justificado"
|
||||||
msgstr "Justify"
|
msgstr "Justify"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:105
|
||||||
msgid "Tamaño de página"
|
msgid "Tamaño de página"
|
||||||
msgstr "Page size"
|
msgstr "Page size"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:110
|
||||||
msgid "Carta (Letter)"
|
msgid "Carta (Letter)"
|
||||||
msgstr "Letter"
|
msgstr "Letter"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:115
|
||||||
msgid "Orientación"
|
msgid "Orientación"
|
||||||
msgstr "Orientation"
|
msgstr "Orientation"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:117
|
||||||
msgid "Vertical"
|
msgid "Vertical"
|
||||||
msgstr "Portrait"
|
msgstr "Portrait"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:118
|
||||||
msgid "Horizontal"
|
msgid "Horizontal"
|
||||||
msgstr "Landscape"
|
msgstr "Landscape"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:126
|
||||||
|
#: crochet/templates/crochet/pattern.html:134
|
||||||
msgid "Secciones"
|
msgid "Secciones"
|
||||||
msgstr "Sections"
|
msgstr "Sections"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:127
|
||||||
|
#: crochet/templates/crochet/pattern.html:173
|
||||||
msgid "Vista previa"
|
msgid "Vista previa"
|
||||||
msgstr "Preview"
|
msgstr "Preview"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:136
|
||||||
msgid "Colapsar todo"
|
msgid "Colapsar todo"
|
||||||
msgstr "Collapse all"
|
msgstr "Collapse all"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:136
|
||||||
msgid "Expandir todo"
|
msgid "Expandir todo"
|
||||||
msgstr "Expand all"
|
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"
|
msgid "Título"
|
||||||
msgstr "Title"
|
msgstr "Title"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:144
|
||||||
msgid "Título..."
|
msgid "Título..."
|
||||||
msgstr "Title..."
|
msgstr "Title..."
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:146
|
||||||
msgid "Subtítulo"
|
msgid "Subtítulo"
|
||||||
msgstr "Subtitle"
|
msgstr "Subtitle"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:146
|
||||||
msgid "Subtítulo..."
|
msgid "Subtítulo..."
|
||||||
msgstr "Subtitle..."
|
msgstr "Subtitle..."
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:148
|
||||||
msgid "Texto"
|
msgid "Texto"
|
||||||
msgstr "Text"
|
msgstr "Text"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:148
|
||||||
msgid "Escribe aquí..."
|
msgid "Escribe aquí..."
|
||||||
msgstr "Write here..."
|
msgstr "Write here..."
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:150
|
||||||
|
#: crochet/templates/crochet/pattern.html:151
|
||||||
msgid "Nota"
|
msgid "Nota"
|
||||||
msgstr "Note"
|
msgstr "Note"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:150
|
||||||
msgid "Escribe una nota o consejo..."
|
msgid "Escribe una nota o consejo..."
|
||||||
msgstr "Write a note or tip..."
|
msgstr "Write a note or tip..."
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:153
|
||||||
msgid "Materiales"
|
msgid "Materiales"
|
||||||
msgstr "Materials"
|
msgstr "Materials"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:153
|
||||||
msgid "Añadir material..."
|
msgid "Añadir material..."
|
||||||
msgstr "Add material..."
|
msgstr "Add material..."
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:153
|
||||||
msgid "Añadir línea"
|
msgid "Añadir línea"
|
||||||
msgstr "Add line"
|
msgstr "Add line"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:155
|
||||||
|
#: crochet/templates/crochet/pattern.html:157
|
||||||
msgid "Imagen"
|
msgid "Imagen"
|
||||||
msgstr "Image"
|
msgstr "Image"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:157
|
||||||
msgid "No se ha podido subir la imagen. Inténtalo de nuevo."
|
msgid "No se ha podido subir la imagen. Inténtalo de nuevo."
|
||||||
msgstr "Couldn't upload the image. Please try again."
|
msgstr "Couldn't upload the image. Please try again."
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:159
|
||||||
msgid "Patrón"
|
msgid "Patrón"
|
||||||
msgstr "Pattern"
|
msgstr "Pattern"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:159
|
||||||
msgid "Elementos"
|
msgid "Elementos"
|
||||||
msgstr "Elements"
|
msgstr "Elements"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:161
|
||||||
msgid "Grupo"
|
msgid "Grupo"
|
||||||
msgstr "Group"
|
msgstr "Group"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:165
|
||||||
msgid "Arrastrar para reordenar"
|
msgid "Arrastrar para reordenar"
|
||||||
msgstr "Drag to reorder"
|
msgstr "Drag to reorder"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:166
|
||||||
msgid "Colapsar / expandir"
|
msgid "Colapsar / expandir"
|
||||||
msgstr "Collapse / expand"
|
msgstr "Collapse / expand"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:167
|
||||||
msgid "Duplicar sección"
|
msgid "Duplicar sección"
|
||||||
msgstr "Duplicate section"
|
msgstr "Duplicate section"
|
||||||
|
|
||||||
|
#: crochet/templates/crochet/pattern.html:168
|
||||||
msgid "Eliminar sección"
|
msgid "Eliminar sección"
|
||||||
msgstr "Delete section"
|
msgstr "Delete section"
|
||||||
|
|
||||||
msgid "¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las secciones que contiene."
|
#: crochet/templates/crochet/pattern.html:169
|
||||||
msgstr "Are you sure you want to delete this group? All sections inside it will also be deleted."
|
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."
|
msgid "Añade una sección para ver aquí el resultado."
|
||||||
msgstr "Add a section to see the result here."
|
msgstr "Add a section to see the result here."
|
||||||
|
|
||||||
# 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."
|
msgid "Este patrón todavía no tiene contenido."
|
||||||
msgstr "This pattern doesn't have any content yet."
|
msgstr "This pattern doesn't have any content yet."
|
||||||
|
|||||||
@@ -2,24 +2,372 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: crochet\n"
|
"Project-Id-Version: crochet\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-07-17 11:03+0000\n"
|
||||||
"Language: es\n"
|
"Language: es\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: crochet/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
|
# Rutas traducibles de crochet/urls.py: la base "pattern/" se traduce como
|
||||||
# "patron/" (sin tilde, para no meter caracteres acentuados en la URL).
|
# "patron/" (sin tilde, para no meter caracteres acentuados en la URL).
|
||||||
|
#: crochet/urls.py:30
|
||||||
msgid "pattern/<uuid:uuid>/"
|
msgid "pattern/<uuid:uuid>/"
|
||||||
msgstr "patron/<uuid:uuid>/"
|
msgstr "patron/<uuid:uuid>/"
|
||||||
|
|
||||||
|
#: crochet/urls.py:31
|
||||||
msgid "pattern/<uuid:uuid>/edit/"
|
msgid "pattern/<uuid:uuid>/edit/"
|
||||||
msgstr "patron/<uuid:uuid>/editar/"
|
msgstr "patron/<uuid:uuid>/editar/"
|
||||||
|
|
||||||
|
#: crochet/urls.py:32
|
||||||
msgid "pattern/<uuid:uuid>/save/"
|
msgid "pattern/<uuid:uuid>/save/"
|
||||||
msgstr "patron/<uuid:uuid>/guardar/"
|
msgstr "patron/<uuid:uuid>/guardar/"
|
||||||
|
|
||||||
|
#: crochet/urls.py:33
|
||||||
msgid "pattern/<uuid:uuid>/images/"
|
msgid "pattern/<uuid:uuid>/images/"
|
||||||
msgstr "patron/<uuid:uuid>/imagenes/"
|
msgstr "patron/<uuid:uuid>/imagenes/"
|
||||||
|
|
||||||
|
#: crochet/urls.py:34
|
||||||
msgid "pattern/<uuid:uuid>/pdf/"
|
msgid "pattern/<uuid:uuid>/pdf/"
|
||||||
msgstr "patron/<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
|
import uuid
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
@@ -11,6 +12,13 @@ class Pattern(models.Model):
|
|||||||
# front, aquí solo hace falta persistirla).
|
# front, aquí solo hace falta persistirla).
|
||||||
sections = models.JSONField(default=list, blank=True)
|
sections = models.JSONField(default=list, blank=True)
|
||||||
page_settings = models.JSONField(default=dict, 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)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=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> */
|
/* HTML: <div class="breadcrumb"></div> */
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
--s: 20px; /* control the shape */
|
--s: 20px; /* control the shape */
|
||||||
@@ -43,17 +67,18 @@
|
|||||||
.detail-btn {
|
.detail-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.5rem 1rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border: 1px solid #d1d5db;
|
border: 1px solid var(--brand-primary);
|
||||||
border-radius: 0.5rem;
|
border-radius: 999px;
|
||||||
color: inherit;
|
color: var(--brand-primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: #fff;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
.detail-btn:hover {
|
.detail-btn:hover {
|
||||||
background-color: #f3f4f6;
|
background-color: var(--brand-primary);
|
||||||
|
color: var(--brand-primary-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-box {
|
.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 %}
|
{% extends 'crochet/base.html' %}
|
||||||
<!DOCTYPE html>
|
{% load static i18n django_htmx %}
|
||||||
<html lang="{{ lang }}">
|
|
||||||
|
|
||||||
<head>
|
{% block title %}Crochet{% endblock %}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
{% 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. -->
|
<!-- Leído por storage.js para la cabecera X-CSRFToken al guardar por fetch. -->
|
||||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||||
<title>Crochet</title>
|
{% include 'crochet/_platform_assets.html' %}
|
||||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
{% htmx_script %}
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
<main>
|
||||||
|
|
||||||
<header class="flex flex-wrap items-center justify-between gap-2 p-4 no-print">
|
{% trans '¿Seguro que quieres eliminar este patrón? No podrás deshacerlo.' as delete_confirm_message %}
|
||||||
<h1 class="text-xl font-semibold">Crochet</h1>
|
<header class="flex flex-wrap items-center justify-end gap-2 p-4 no-print">
|
||||||
<div class="flex gap-2 items-center">
|
<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">
|
<select id="language-select" class="select select-sm select-bordered">
|
||||||
<option value="es" {% if lang == 'es' %}selected{% endif %}>Español</option>
|
<option value="es" {% if lang == 'es' %}selected{% endif %}>Español</option>
|
||||||
<option value="en" {% if lang == 'en' %}selected{% endif %}>English</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"
|
<button id="save-pattern" type="button" class="btn btn-sm btn-outline"
|
||||||
data-save-url="{% url 'crochet:pattern_save' pattern.uuid %}"
|
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>
|
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"
|
<a href="{% url 'crochet:pattern_detail' pattern.uuid %}" target="_blank" rel="noopener"
|
||||||
class="btn btn-sm btn-outline">{% trans 'Ver patrón' %}</a>
|
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>
|
</div>
|
||||||
</header>
|
</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 id="save-toast" class="toast toast-top toast-end no-print hidden">
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
<span data-message="{% trans 'Guardado' %}">{% trans 'Guardado' %}</span>
|
<span data-message="{% trans 'Guardado' %}">{% trans 'Guardado' %}</span>
|
||||||
@@ -55,10 +49,6 @@
|
|||||||
|
|
||||||
<div class="p-4 pt-0">
|
<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">
|
<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>
|
<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">
|
<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"
|
<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>
|
data-collapse-all-label="{% trans 'Colapsar todo' %}" data-expand-all-label="{% trans 'Expandir todo' %}">{% trans 'Colapsar todo' %}</button>
|
||||||
</div>
|
</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">
|
<div class="dropdown mb-4">
|
||||||
<button id="add-section-toggle" type="button" class="btn btn-sm" tabindex="0"
|
<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>
|
data-label="{% trans 'Añadir sección' %}">+ {% trans 'Añadir sección' %}</button>
|
||||||
@@ -195,17 +179,12 @@
|
|||||||
|
|
||||||
</div>
|
</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" }}
|
{{ 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" }}
|
{{ pattern_data|json_script:"pattern-data" }}
|
||||||
</main>
|
</main>
|
||||||
</body>
|
{% endblock %}
|
||||||
<script type="module" src="{% static 'js/main.js' %}"></script>
|
|
||||||
|
|
||||||
</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 %}
|
{% load static i18n %}
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="{{ lang }}">
|
|
||||||
|
|
||||||
<head>
|
{% block title %}{{ page_settings.title|default:"Crochet" }}{% endblock %}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
{% block extra_css %}
|
||||||
<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). -->
|
|
||||||
<link rel="stylesheet" href="{% static 'css/pattern-detail.css' %}">
|
<link rel="stylesheet" href="{% static 'css/pattern-detail.css' %}">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
@@ -24,19 +12,15 @@
|
|||||||
}
|
}
|
||||||
@page { size: {{ page_settings.pageSize }} {{ page_settings.orientation }}; }
|
@page { size: {{ page_settings.pageSize }} {{ page_settings.orientation }}; }
|
||||||
</style>
|
</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">
|
<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">
|
<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>
|
<a href="{% url 'crochet:pattern_pdf' pattern.uuid %}" class="detail-btn">{% trans 'Descargar PDF' %}</a>
|
||||||
{% if lang != 'es' %}
|
{% if lang != 'es' %}
|
||||||
{% language 'es' %}
|
{% language 'es' %}
|
||||||
@@ -57,6 +41,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
{% endblock %}
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -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
|
import io
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -32,6 +33,21 @@ class PatternModelTests(TestCase):
|
|||||||
second = Pattern.objects.create()
|
second = Pattern.objects.create()
|
||||||
self.assertNotEqual(first.uuid, second.uuid)
|
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):
|
class PatternImageModelTests(TestCase):
|
||||||
def test_deleted_with_pattern(self):
|
def test_deleted_with_pattern(self):
|
||||||
|
|||||||
+112
-2
@@ -1,6 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
@@ -13,13 +14,16 @@ NO_HTML_COMMENTS_RE = re.compile(r'<!--.*?-->', re.S)
|
|||||||
|
|
||||||
class PatternEditViewTests(TestCase):
|
class PatternEditViewTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.owner = User.objects.create_user(username='crocheter', password='a-very-uncommon-pw-1')
|
||||||
self.pattern = Pattern.objects.create(
|
self.pattern = Pattern.objects.create(
|
||||||
sections=[{'type': 'title', 'text': {'es': 'Ampharos', 'en': 'Ampharos'}}],
|
sections=[{'type': 'title', 'text': {'es': 'Ampharos', 'en': 'Ampharos'}}],
|
||||||
page_settings={'title': 'Ampharos'},
|
page_settings={'title': 'Ampharos'},
|
||||||
|
created_by=self.owner,
|
||||||
)
|
)
|
||||||
StitchType.objects.create(translations={'es': 'Punto bajo', 'en': 'SC'}, order=1)
|
StitchType.objects.create(translations={'es': 'Punto bajo', 'en': 'SC'}, order=1)
|
||||||
|
|
||||||
def test_get_in_spanish(self):
|
def test_get_in_spanish(self):
|
||||||
|
self.client.force_login(self.owner)
|
||||||
with translation.override('es'):
|
with translation.override('es'):
|
||||||
url = reverse('crochet:pattern_edit', args=[self.pattern.uuid])
|
url = reverse('crochet:pattern_edit', args=[self.pattern.uuid])
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
@@ -30,6 +34,7 @@ class PatternEditViewTests(TestCase):
|
|||||||
self.assertContains(response, 'Guardar')
|
self.assertContains(response, 'Guardar')
|
||||||
|
|
||||||
def test_get_in_english(self):
|
def test_get_in_english(self):
|
||||||
|
self.client.force_login(self.owner)
|
||||||
with translation.override('en'):
|
with translation.override('en'):
|
||||||
url = reverse('crochet:pattern_edit', args=[self.pattern.uuid])
|
url = reverse('crochet:pattern_edit', args=[self.pattern.uuid])
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
@@ -38,6 +43,7 @@ class PatternEditViewTests(TestCase):
|
|||||||
self.assertContains(response, 'Save')
|
self.assertContains(response, 'Save')
|
||||||
|
|
||||||
def test_context_includes_saved_pattern_and_stitch_catalog(self):
|
def test_context_includes_saved_pattern_and_stitch_catalog(self):
|
||||||
|
self.client.force_login(self.owner)
|
||||||
with translation.override('es'):
|
with translation.override('es'):
|
||||||
url = reverse('crochet:pattern_edit', args=[self.pattern.uuid])
|
url = reverse('crochet:pattern_edit', args=[self.pattern.uuid])
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
@@ -48,12 +54,39 @@ class PatternEditViewTests(TestCase):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def test_unknown_uuid_returns_404(self):
|
def test_unknown_uuid_returns_404(self):
|
||||||
|
self.client.force_login(self.owner)
|
||||||
with translation.override('es'):
|
with translation.override('es'):
|
||||||
url = reverse('crochet:pattern_edit', args=['00000000-0000-0000-0000-000000000000'])
|
url = reverse('crochet:pattern_edit', args=['00000000-0000-0000-0000-000000000000'])
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 404)
|
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):
|
class PatternDetailViewTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -170,7 +203,9 @@ class PatternI18nUrlTests(TestCase):
|
|||||||
|
|
||||||
class PatternSaveViewTests(TestCase):
|
class PatternSaveViewTests(TestCase):
|
||||||
def setUp(self):
|
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'):
|
with translation.override('es'):
|
||||||
self.url = reverse('crochet:pattern_save', args=[self.pattern.uuid])
|
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')
|
response = self.client.post(url, data=json.dumps({}), content_type='application/json')
|
||||||
self.assertEqual(response.status_code, 404)
|
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):
|
class PatternImageUploadViewTests(TestCase):
|
||||||
def setUp(self):
|
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'):
|
with translation.override('es'):
|
||||||
self.url = reverse('crochet:pattern_image_upload', args=[self.pattern.uuid])
|
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):
|
def test_missing_file_returns_bad_request(self):
|
||||||
response = self.client.post(self.url, data={})
|
response = self.client.post(self.url, data={})
|
||||||
self.assertEqual(response.status_code, 400)
|
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.urls import path
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from crochet.forms import StyledAuthenticationForm
|
||||||
from crochet.views import (
|
from crochet.views import (
|
||||||
|
AccountHomeView,
|
||||||
|
PatternCreateView,
|
||||||
|
PatternDeleteView,
|
||||||
PatternDetailView,
|
PatternDetailView,
|
||||||
PatternEditView,
|
PatternEditView,
|
||||||
PatternImageUploadView,
|
PatternImageUploadView,
|
||||||
PatternPdfView,
|
PatternPdfView,
|
||||||
PatternSaveView,
|
PatternSaveView,
|
||||||
|
RegisterView,
|
||||||
)
|
)
|
||||||
|
|
||||||
app_name = 'crochet'
|
app_name = 'crochet'
|
||||||
@@ -26,4 +32,15 @@ urlpatterns = [
|
|||||||
path(_('pattern/<uuid:uuid>/save/'), name='pattern_save', view=PatternSaveView.as_view()),
|
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>/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>/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 json
|
||||||
|
|
||||||
import weasyprint
|
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.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.template.loader import render_to_string
|
||||||
|
from django.urls import reverse_lazy
|
||||||
from django.utils.text import get_valid_filename
|
from django.utils.text import get_valid_filename
|
||||||
from django.utils.translation import get_language, gettext_lazy as _
|
from django.utils.translation import get_language, gettext_lazy as _
|
||||||
from django.views import View
|
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.models import Pattern, PatternImage, StitchType
|
||||||
from crochet.pattern_render import render_pattern_html, sanitize_page_settings
|
from crochet.pattern_render import render_pattern_html, sanitize_page_settings
|
||||||
|
|
||||||
@@ -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):
|
def _pattern_detail_context(pattern):
|
||||||
# Compartido por PatternDetailView (la pinta) y PatternPdfView (la
|
# Compartido por PatternDetailView (la pinta) y PatternPdfView (la
|
||||||
# convierte a PDF con WeasyPrint a partir del mismo HTML), para no
|
# 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
|
model = Pattern
|
||||||
template_name = 'crochet/pattern.html'
|
template_name = 'crochet/pattern.html'
|
||||||
context_object_name = 'pattern'
|
context_object_name = 'pattern'
|
||||||
slug_field = 'uuid'
|
slug_field = 'uuid'
|
||||||
slug_url_kwarg = '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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
# Resuelto por LocaleMiddleware a partir del prefijo de idioma de la
|
# Resuelto por LocaleMiddleware a partir del prefijo de idioma de la
|
||||||
@@ -135,8 +160,13 @@ class PatternPdfView(DetailView):
|
|||||||
|
|
||||||
|
|
||||||
class PatternSaveView(View):
|
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):
|
def post(self, request, uuid):
|
||||||
pattern = get_object_or_404(Pattern, uuid=uuid)
|
pattern = _get_owned_pattern_or_403(request, uuid)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
@@ -151,8 +181,9 @@ class PatternSaveView(View):
|
|||||||
|
|
||||||
|
|
||||||
class PatternImageUploadView(View):
|
class PatternImageUploadView(View):
|
||||||
|
# Mismo motivo que en PatternSaveView: lo llama sections.js por fetch.
|
||||||
def post(self, request, uuid):
|
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')
|
image_file = request.FILES.get('image')
|
||||||
if not image_file:
|
if not image_file:
|
||||||
@@ -160,3 +191,48 @@ class PatternImageUploadView(View):
|
|||||||
|
|
||||||
pattern_image = PatternImage.objects.create(pattern=pattern, image=image_file)
|
pattern_image = PatternImage.objects.create(pattern=pattern, image=image_file)
|
||||||
return JsonResponse({'id': pattern_image.id, 'url': pattern_image.image.url})
|
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-environ>=0.14.0",
|
||||||
"django-extensions>=4.1",
|
"django-extensions>=4.1",
|
||||||
"django-filter>=26.1",
|
"django-filter>=26.1",
|
||||||
|
"django-htmx>=1.28.0",
|
||||||
"django-silk>=5.5.0",
|
"django-silk>=5.5.0",
|
||||||
"django-storages>=1.14.6",
|
"django-storages>=1.14.6",
|
||||||
"ipython>=9.15.0",
|
"ipython>=9.15.0",
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ dependencies = [
|
|||||||
{ name = "django-environ" },
|
{ name = "django-environ" },
|
||||||
{ name = "django-extensions" },
|
{ name = "django-extensions" },
|
||||||
{ name = "django-filter" },
|
{ name = "django-filter" },
|
||||||
|
{ name = "django-htmx" },
|
||||||
{ name = "django-silk" },
|
{ name = "django-silk" },
|
||||||
{ name = "django-storages" },
|
{ name = "django-storages" },
|
||||||
{ name = "ipython" },
|
{ name = "ipython" },
|
||||||
@@ -268,6 +269,7 @@ requires-dist = [
|
|||||||
{ name = "django-environ", specifier = ">=0.14.0" },
|
{ name = "django-environ", specifier = ">=0.14.0" },
|
||||||
{ name = "django-extensions", specifier = ">=4.1" },
|
{ name = "django-extensions", specifier = ">=4.1" },
|
||||||
{ name = "django-filter", specifier = ">=26.1" },
|
{ name = "django-filter", specifier = ">=26.1" },
|
||||||
|
{ name = "django-htmx", specifier = ">=1.28.0" },
|
||||||
{ name = "django-silk", specifier = ">=5.5.0" },
|
{ name = "django-silk", specifier = ">=5.5.0" },
|
||||||
{ name = "django-storages", specifier = ">=1.14.6" },
|
{ name = "django-storages", specifier = ">=1.14.6" },
|
||||||
{ name = "ipython", specifier = ">=9.15.0" },
|
{ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "django-silk"
|
name = "django-silk"
|
||||||
version = "5.5.0"
|
version = "5.5.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user