feat: added UI
This commit is contained in:
@@ -95,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 %}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
{% load static %}
|
{% load static i18n %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ lang }}">
|
<!-- 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>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
@@ -11,6 +15,31 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body{% block body_attrs %}{% endblock %}>
|
<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 content %}{% endblock %}
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
{% block extra_css %}
|
{% 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 }}">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
{% include 'crochet/_platform_assets.html' %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
||||||
{% htmx_script %}
|
{% htmx_script %}
|
||||||
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
|
||||||
@@ -19,8 +18,8 @@
|
|||||||
{% block content %}
|
{% 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">
|
||||||
|
|
||||||
<select id="language-select" class="select select-sm select-bordered">
|
<select id="language-select" class="select select-sm select-bordered">
|
||||||
@@ -33,7 +32,12 @@
|
|||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
<!-- 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 %}
|
{% block content %}
|
||||||
<main class="p-4">
|
<main class="p-4">
|
||||||
<div class="flex justify-end gap-2 mb-4 no-print">
|
<div class="flex justify-end gap-2 mb-4 no-print">
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user