diff --git a/conftest.py b/conftest.py index 34ec38b..771f510 100644 --- a/conftest.py +++ b/conftest.py @@ -18,3 +18,18 @@ def collected_static_files(): with override_settings(STATIC_ROOT=static_root): call_command('collectstatic', interactive=False, verbosity=0) yield + + +@pytest.fixture(scope='session', autouse=True) +def isolated_media_root(): + """Los tests que suben imágenes (PatternImage, portada de Pattern...) + escriben de verdad en MEDIA_ROOT; sin aislarlo, se acumulan en la + carpeta media/ real del proyecto de una ejecución a otra, y como + cover_image.py guarda cada variante con un nombre fijo (thumbnail.jpg, + large.jpg) dentro de una carpeta por mes, esas sobras podían chocar con + los nombres que espera un test concreto (ver + PatternCoverImageUploadViewTests) y hacerlo fallar según lo que hubiera + quedado de ejecuciones anteriores.""" + with tempfile.TemporaryDirectory() as media_root: + with override_settings(MEDIA_ROOT=media_root): + yield diff --git a/crochet/cover_image.py b/crochet/cover_image.py new file mode 100644 index 0000000..06c31b9 --- /dev/null +++ b/crochet/cover_image.py @@ -0,0 +1,52 @@ +import io + +from django.core.files.base import ContentFile +from PIL import Image, UnidentifiedImageError + +# Miniatura para las tarjetas de "Mis patrones" (ver account_home.html) y +# versión grande para una futura vista de detalle/producto: dos tamaños +# cubren los usos actuales sin generar variantes de más. +THUMBNAIL_MAX_SIZE = (400, 400) +LARGE_MAX_SIZE = (1200, 1200) + + +class InvalidCoverImage(Exception): + pass + + +def _resized_jpeg(image, max_size): + resized = image.copy() + # thumbnail() redimensiona conservando el aspect ratio, sin salirse de + # la caja (max_size), a diferencia de resize() (que deformaría la + # imagen si no coincide con esa proporción). + resized.thumbnail(max_size, Image.Resampling.LANCZOS) + buffer = io.BytesIO() + resized.save(buffer, format='JPEG', quality=85, optimize=True) + return buffer.getvalue() + + +def build_cover_image_variants(uploaded_file): + # Devuelve (original, thumbnail, large) como ContentFile, listos para + # asignar a cover_image/cover_image_thumbnail/cover_image_large. + raw_bytes = uploaded_file.read() + try: + image = Image.open(io.BytesIO(raw_bytes)) + image.load() + except UnidentifiedImageError as error: + raise InvalidCoverImage('El archivo no es una imagen válida.') from error + + # JPEG no admite transparencia: se aplana sobre fondo blanco antes de + # generar las variantes en vez de dejar que Pillow falle al guardar un + # PNG/RGBA como JPEG. + if image.mode in ('RGBA', 'LA', 'P'): + rgba = image.convert('RGBA') + flattened = Image.new('RGB', image.size, (255, 255, 255)) + flattened.paste(rgba, mask=rgba.split()[-1]) + image = flattened + else: + image = image.convert('RGB') + + original = ContentFile(raw_bytes, name=uploaded_file.name) + thumbnail = ContentFile(_resized_jpeg(image, THUMBNAIL_MAX_SIZE), name='thumbnail.jpg') + large = ContentFile(_resized_jpeg(image, LARGE_MAX_SIZE), name='large.jpg') + return original, thumbnail, large diff --git a/crochet/locale/en/LC_MESSAGES/django.po b/crochet/locale/en/LC_MESSAGES/django.po index b873078..5d8dbc3 100644 --- a/crochet/locale/en/LC_MESSAGES/django.po +++ b/crochet/locale/en/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: crochet\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-17 11:03+0000\n" +"POT-Creation-Date: 2026-07-17 11:17+0000\n" "Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -26,33 +26,33 @@ msgstr "Create pattern" 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 +#: crochet/templates/crochet/account_home.html:34 msgid "Patrón sin título" msgstr "Untitled pattern" -#: crochet/templates/crochet/account_home.html:30 +#: crochet/templates/crochet/account_home.html:37 #, python-format msgid "Actualizado el %(date)s" msgstr "Updated on %(date)s" -#: crochet/templates/crochet/account_home.html:33 +#: crochet/templates/crochet/account_home.html:40 #: crochet/templates/crochet/pattern.html:34 msgid "Ver patrón" msgstr "View pattern" -#: crochet/templates/crochet/account_home.html:34 +#: crochet/templates/crochet/account_home.html:41 msgid "Editar" msgstr "Edit" -#: crochet/templates/crochet/account_home.html:38 +#: crochet/templates/crochet/account_home.html:45 msgid "Eliminar" msgstr "Delete" -#: crochet/templates/crochet/account_home.html:47 +#: crochet/templates/crochet/account_home.html:54 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/account_home.html:60 #: crochet/templates/crochet/base.html:34 msgid "Cerrar sesión" msgstr "Log out" @@ -110,184 +110,192 @@ msgid "Autor..." msgstr "Author..." #: crochet/templates/crochet/pattern.html:67 -#: crochet/templates/crochet/pattern.html:151 +msgid "Imagen de portada" +msgstr "Cover image" + +#: crochet/templates/crochet/pattern.html:70 +msgid "No se ha podido subir la imagen de portada. Inténtalo de nuevo." +msgstr "Couldn't upload the cover image. Please try again." + +#: crochet/templates/crochet/pattern.html:78 +#: crochet/templates/crochet/pattern.html:162 msgid "Color del texto" msgstr "Text color" -#: crochet/templates/crochet/pattern.html:71 +#: crochet/templates/crochet/pattern.html:82 msgid "Color de acento (títulos)" msgstr "Accent color (headings)" -#: crochet/templates/crochet/pattern.html:75 -#: crochet/templates/crochet/pattern.html:151 +#: crochet/templates/crochet/pattern.html:86 +#: crochet/templates/crochet/pattern.html:162 msgid "Color de fondo" msgstr "Background color" -#: crochet/templates/crochet/pattern.html:79 +#: crochet/templates/crochet/pattern.html:90 msgid "Tipografía" msgstr "Font" -#: crochet/templates/crochet/pattern.html:85 +#: crochet/templates/crochet/pattern.html:96 msgid "Monoespaciada" msgstr "Monospace" -#: crochet/templates/crochet/pattern.html:89 +#: crochet/templates/crochet/pattern.html:100 msgid "Tamaño de letra" msgstr "Font size" -#: crochet/templates/crochet/pattern.html:91 +#: crochet/templates/crochet/pattern.html:102 msgid "Pequeño" msgstr "Small" -#: crochet/templates/crochet/pattern.html:92 +#: crochet/templates/crochet/pattern.html:103 msgid "Normal" msgstr "Normal" -#: crochet/templates/crochet/pattern.html:93 +#: crochet/templates/crochet/pattern.html:104 msgid "Grande" msgstr "Large" -#: crochet/templates/crochet/pattern.html:97 +#: crochet/templates/crochet/pattern.html:108 msgid "Alineación del texto" msgstr "Text alignment" -#: crochet/templates/crochet/pattern.html:99 +#: crochet/templates/crochet/pattern.html:110 msgid "Izquierda" msgstr "Left" -#: crochet/templates/crochet/pattern.html:100 +#: crochet/templates/crochet/pattern.html:111 msgid "Centrado" msgstr "Center" -#: crochet/templates/crochet/pattern.html:101 +#: crochet/templates/crochet/pattern.html:112 msgid "Justificado" msgstr "Justify" -#: crochet/templates/crochet/pattern.html:105 +#: crochet/templates/crochet/pattern.html:116 msgid "Tamaño de página" msgstr "Page size" -#: crochet/templates/crochet/pattern.html:110 +#: crochet/templates/crochet/pattern.html:121 msgid "Carta (Letter)" msgstr "Letter" -#: crochet/templates/crochet/pattern.html:115 +#: crochet/templates/crochet/pattern.html:126 msgid "Orientación" msgstr "Orientation" -#: crochet/templates/crochet/pattern.html:117 +#: crochet/templates/crochet/pattern.html:128 msgid "Vertical" msgstr "Portrait" -#: crochet/templates/crochet/pattern.html:118 +#: crochet/templates/crochet/pattern.html:129 msgid "Horizontal" msgstr "Landscape" -#: crochet/templates/crochet/pattern.html:126 -#: crochet/templates/crochet/pattern.html:134 +#: crochet/templates/crochet/pattern.html:137 +#: crochet/templates/crochet/pattern.html:145 msgid "Secciones" msgstr "Sections" -#: crochet/templates/crochet/pattern.html:127 -#: crochet/templates/crochet/pattern.html:173 +#: crochet/templates/crochet/pattern.html:138 +#: crochet/templates/crochet/pattern.html:184 msgid "Vista previa" msgstr "Preview" -#: crochet/templates/crochet/pattern.html:136 +#: crochet/templates/crochet/pattern.html:147 msgid "Colapsar todo" msgstr "Collapse all" -#: crochet/templates/crochet/pattern.html:136 +#: crochet/templates/crochet/pattern.html:147 msgid "Expandir todo" msgstr "Expand all" -#: crochet/templates/crochet/pattern.html:141 +#: crochet/templates/crochet/pattern.html:152 msgid "Añadir sección" msgstr "Add section" -#: crochet/templates/crochet/pattern.html:144 +#: crochet/templates/crochet/pattern.html:155 msgid "Título" msgstr "Title" -#: crochet/templates/crochet/pattern.html:144 +#: crochet/templates/crochet/pattern.html:155 msgid "Título..." msgstr "Title..." -#: crochet/templates/crochet/pattern.html:146 +#: crochet/templates/crochet/pattern.html:157 msgid "Subtítulo" msgstr "Subtitle" -#: crochet/templates/crochet/pattern.html:146 +#: crochet/templates/crochet/pattern.html:157 msgid "Subtítulo..." msgstr "Subtitle..." -#: crochet/templates/crochet/pattern.html:148 +#: crochet/templates/crochet/pattern.html:159 msgid "Texto" msgstr "Text" -#: crochet/templates/crochet/pattern.html:148 +#: crochet/templates/crochet/pattern.html:159 msgid "Escribe aquí..." msgstr "Write here..." -#: crochet/templates/crochet/pattern.html:150 -#: crochet/templates/crochet/pattern.html:151 +#: crochet/templates/crochet/pattern.html:161 +#: crochet/templates/crochet/pattern.html:162 msgid "Nota" msgstr "Note" -#: crochet/templates/crochet/pattern.html:150 +#: crochet/templates/crochet/pattern.html:161 msgid "Escribe una nota o consejo..." msgstr "Write a note or tip..." -#: crochet/templates/crochet/pattern.html:153 +#: crochet/templates/crochet/pattern.html:164 msgid "Materiales" msgstr "Materials" -#: crochet/templates/crochet/pattern.html:153 +#: crochet/templates/crochet/pattern.html:164 msgid "Añadir material..." msgstr "Add material..." -#: crochet/templates/crochet/pattern.html:153 +#: crochet/templates/crochet/pattern.html:164 msgid "Añadir línea" msgstr "Add line" -#: crochet/templates/crochet/pattern.html:155 -#: crochet/templates/crochet/pattern.html:157 +#: crochet/templates/crochet/pattern.html:166 +#: crochet/templates/crochet/pattern.html:168 msgid "Imagen" msgstr "Image" -#: crochet/templates/crochet/pattern.html:157 +#: crochet/templates/crochet/pattern.html:168 msgid "No se ha podido subir la imagen. Inténtalo de nuevo." msgstr "Couldn't upload the image. Please try again." -#: crochet/templates/crochet/pattern.html:159 +#: crochet/templates/crochet/pattern.html:170 msgid "Patrón" msgstr "Pattern" -#: crochet/templates/crochet/pattern.html:159 +#: crochet/templates/crochet/pattern.html:170 msgid "Elementos" msgstr "Elements" -#: crochet/templates/crochet/pattern.html:161 +#: crochet/templates/crochet/pattern.html:172 msgid "Grupo" msgstr "Group" -#: crochet/templates/crochet/pattern.html:165 +#: crochet/templates/crochet/pattern.html:176 msgid "Arrastrar para reordenar" msgstr "Drag to reorder" -#: crochet/templates/crochet/pattern.html:166 +#: crochet/templates/crochet/pattern.html:177 msgid "Colapsar / expandir" msgstr "Collapse / expand" -#: crochet/templates/crochet/pattern.html:167 +#: crochet/templates/crochet/pattern.html:178 msgid "Duplicar sección" msgstr "Duplicate section" -#: crochet/templates/crochet/pattern.html:168 +#: crochet/templates/crochet/pattern.html:179 msgid "Eliminar sección" msgstr "Delete section" -#: crochet/templates/crochet/pattern.html:169 +#: crochet/templates/crochet/pattern.html:180 msgid "" "¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las " "secciones que contiene." @@ -295,7 +303,7 @@ msgstr "" "Are you sure you want to delete this group? All sections inside it will also " "be deleted." -#: crochet/templates/crochet/pattern.html:175 +#: crochet/templates/crochet/pattern.html:186 msgid "Añade una sección para ver aquí el resultado." msgstr "Add a section to see the result here." @@ -324,51 +332,55 @@ msgstr "Create account" msgid "¿Ya tienes cuenta? Inicia sesión" msgstr "Already have an account? Log in" -#: crochet/urls.py:30 +#: crochet/urls.py:31 msgid "pattern//" msgstr "" -#: crochet/urls.py:31 +#: crochet/urls.py:32 msgid "pattern//edit/" msgstr "" -#: crochet/urls.py:32 +#: crochet/urls.py:33 msgid "pattern//save/" msgstr "" -#: crochet/urls.py:33 +#: crochet/urls.py:34 msgid "pattern//images/" msgstr "" -#: crochet/urls.py:34 -msgid "pattern//pdf/" -msgstr "" - #: crochet/urls.py:35 -msgid "pattern//delete/" +msgid "pattern//cover/" msgstr "" #: crochet/urls.py:36 -msgid "pattern/new/" +msgid "pattern//pdf/" msgstr "" #: crochet/urls.py:37 -msgid "account/" +msgid "pattern//delete/" msgstr "" #: crochet/urls.py:38 -msgid "account/register/" +msgid "pattern/new/" +msgstr "" + +#: crochet/urls.py:39 +msgid "account/" msgstr "" #: crochet/urls.py:40 +msgid "account/register/" +msgstr "" + +#: crochet/urls.py:42 msgid "account/login/" msgstr "" -#: crochet/urls.py:45 +#: crochet/urls.py:47 msgid "account/logout/" msgstr "" # crochet/views.py (PatternDetailView.EMPTY_MESSAGE) -#: crochet/views.py:23 +#: crochet/views.py:24 msgid "Este patrón todavía no tiene contenido." msgstr "This pattern doesn't have any content yet." diff --git a/crochet/locale/es/LC_MESSAGES/django.po b/crochet/locale/es/LC_MESSAGES/django.po index 4d676ec..f71cfe1 100644 --- a/crochet/locale/es/LC_MESSAGES/django.po +++ b/crochet/locale/es/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: crochet\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-17 11:03+0000\n" +"POT-Creation-Date: 2026-07-17 11:17+0000\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -26,33 +26,33 @@ msgstr "" msgid "¿Seguro que quieres eliminar este patrón? No podrás deshacerlo." msgstr "" -#: crochet/templates/crochet/account_home.html:27 +#: crochet/templates/crochet/account_home.html:34 msgid "Patrón sin título" msgstr "" -#: crochet/templates/crochet/account_home.html:30 +#: crochet/templates/crochet/account_home.html:37 #, python-format msgid "Actualizado el %(date)s" msgstr "" -#: crochet/templates/crochet/account_home.html:33 +#: crochet/templates/crochet/account_home.html:40 #: crochet/templates/crochet/pattern.html:34 msgid "Ver patrón" msgstr "" -#: crochet/templates/crochet/account_home.html:34 +#: crochet/templates/crochet/account_home.html:41 msgid "Editar" msgstr "" -#: crochet/templates/crochet/account_home.html:38 +#: crochet/templates/crochet/account_home.html:45 msgid "Eliminar" msgstr "" -#: crochet/templates/crochet/account_home.html:47 +#: crochet/templates/crochet/account_home.html:54 msgid "Todavía no tienes ningún patrón." msgstr "" -#: crochet/templates/crochet/account_home.html:53 +#: crochet/templates/crochet/account_home.html:60 #: crochet/templates/crochet/base.html:34 msgid "Cerrar sesión" msgstr "" @@ -109,190 +109,198 @@ msgid "Autor..." msgstr "" #: crochet/templates/crochet/pattern.html:67 -#: crochet/templates/crochet/pattern.html:151 +msgid "Imagen de portada" +msgstr "" + +#: crochet/templates/crochet/pattern.html:70 +msgid "No se ha podido subir la imagen de portada. Inténtalo de nuevo." +msgstr "" + +#: crochet/templates/crochet/pattern.html:78 +#: crochet/templates/crochet/pattern.html:162 msgid "Color del texto" msgstr "" -#: crochet/templates/crochet/pattern.html:71 +#: crochet/templates/crochet/pattern.html:82 msgid "Color de acento (títulos)" msgstr "" -#: crochet/templates/crochet/pattern.html:75 -#: crochet/templates/crochet/pattern.html:151 +#: crochet/templates/crochet/pattern.html:86 +#: crochet/templates/crochet/pattern.html:162 msgid "Color de fondo" msgstr "" -#: crochet/templates/crochet/pattern.html:79 +#: crochet/templates/crochet/pattern.html:90 msgid "Tipografía" msgstr "" -#: crochet/templates/crochet/pattern.html:85 +#: crochet/templates/crochet/pattern.html:96 msgid "Monoespaciada" msgstr "" -#: crochet/templates/crochet/pattern.html:89 +#: crochet/templates/crochet/pattern.html:100 msgid "Tamaño de letra" msgstr "" -#: crochet/templates/crochet/pattern.html:91 +#: crochet/templates/crochet/pattern.html:102 msgid "Pequeño" msgstr "" -#: crochet/templates/crochet/pattern.html:92 +#: crochet/templates/crochet/pattern.html:103 msgid "Normal" msgstr "" -#: crochet/templates/crochet/pattern.html:93 +#: crochet/templates/crochet/pattern.html:104 msgid "Grande" msgstr "" -#: crochet/templates/crochet/pattern.html:97 +#: crochet/templates/crochet/pattern.html:108 msgid "Alineación del texto" msgstr "" -#: crochet/templates/crochet/pattern.html:99 +#: crochet/templates/crochet/pattern.html:110 msgid "Izquierda" msgstr "" -#: crochet/templates/crochet/pattern.html:100 +#: crochet/templates/crochet/pattern.html:111 msgid "Centrado" msgstr "" -#: crochet/templates/crochet/pattern.html:101 +#: crochet/templates/crochet/pattern.html:112 msgid "Justificado" msgstr "" -#: crochet/templates/crochet/pattern.html:105 +#: crochet/templates/crochet/pattern.html:116 msgid "Tamaño de página" msgstr "" -#: crochet/templates/crochet/pattern.html:110 +#: crochet/templates/crochet/pattern.html:121 msgid "Carta (Letter)" msgstr "" -#: crochet/templates/crochet/pattern.html:115 +#: crochet/templates/crochet/pattern.html:126 msgid "Orientación" msgstr "" -#: crochet/templates/crochet/pattern.html:117 +#: crochet/templates/crochet/pattern.html:128 msgid "Vertical" msgstr "" -#: crochet/templates/crochet/pattern.html:118 +#: crochet/templates/crochet/pattern.html:129 msgid "Horizontal" msgstr "" -#: crochet/templates/crochet/pattern.html:126 -#: crochet/templates/crochet/pattern.html:134 +#: crochet/templates/crochet/pattern.html:137 +#: crochet/templates/crochet/pattern.html:145 msgid "Secciones" msgstr "" -#: crochet/templates/crochet/pattern.html:127 -#: crochet/templates/crochet/pattern.html:173 +#: crochet/templates/crochet/pattern.html:138 +#: crochet/templates/crochet/pattern.html:184 msgid "Vista previa" msgstr "" -#: crochet/templates/crochet/pattern.html:136 +#: crochet/templates/crochet/pattern.html:147 msgid "Colapsar todo" msgstr "" -#: crochet/templates/crochet/pattern.html:136 +#: crochet/templates/crochet/pattern.html:147 msgid "Expandir todo" msgstr "" -#: crochet/templates/crochet/pattern.html:141 +#: crochet/templates/crochet/pattern.html:152 msgid "Añadir sección" msgstr "" -#: crochet/templates/crochet/pattern.html:144 +#: crochet/templates/crochet/pattern.html:155 msgid "Título" msgstr "" -#: crochet/templates/crochet/pattern.html:144 +#: crochet/templates/crochet/pattern.html:155 msgid "Título..." msgstr "" -#: crochet/templates/crochet/pattern.html:146 +#: crochet/templates/crochet/pattern.html:157 msgid "Subtítulo" msgstr "" -#: crochet/templates/crochet/pattern.html:146 +#: crochet/templates/crochet/pattern.html:157 msgid "Subtítulo..." msgstr "" -#: crochet/templates/crochet/pattern.html:148 +#: crochet/templates/crochet/pattern.html:159 msgid "Texto" msgstr "" -#: crochet/templates/crochet/pattern.html:148 +#: crochet/templates/crochet/pattern.html:159 msgid "Escribe aquí..." msgstr "" -#: crochet/templates/crochet/pattern.html:150 -#: crochet/templates/crochet/pattern.html:151 +#: crochet/templates/crochet/pattern.html:161 +#: crochet/templates/crochet/pattern.html:162 msgid "Nota" msgstr "" -#: crochet/templates/crochet/pattern.html:150 +#: crochet/templates/crochet/pattern.html:161 msgid "Escribe una nota o consejo..." msgstr "" -#: crochet/templates/crochet/pattern.html:153 +#: crochet/templates/crochet/pattern.html:164 msgid "Materiales" msgstr "" -#: crochet/templates/crochet/pattern.html:153 +#: crochet/templates/crochet/pattern.html:164 msgid "Añadir material..." msgstr "" -#: crochet/templates/crochet/pattern.html:153 +#: crochet/templates/crochet/pattern.html:164 msgid "Añadir línea" msgstr "" -#: crochet/templates/crochet/pattern.html:155 -#: crochet/templates/crochet/pattern.html:157 +#: crochet/templates/crochet/pattern.html:166 +#: crochet/templates/crochet/pattern.html:168 msgid "Imagen" msgstr "" -#: crochet/templates/crochet/pattern.html:157 +#: crochet/templates/crochet/pattern.html:168 msgid "No se ha podido subir la imagen. Inténtalo de nuevo." msgstr "" -#: crochet/templates/crochet/pattern.html:159 +#: crochet/templates/crochet/pattern.html:170 msgid "Patrón" msgstr "" -#: crochet/templates/crochet/pattern.html:159 +#: crochet/templates/crochet/pattern.html:170 msgid "Elementos" msgstr "" -#: crochet/templates/crochet/pattern.html:161 +#: crochet/templates/crochet/pattern.html:172 msgid "Grupo" msgstr "" -#: crochet/templates/crochet/pattern.html:165 +#: crochet/templates/crochet/pattern.html:176 msgid "Arrastrar para reordenar" msgstr "" -#: crochet/templates/crochet/pattern.html:166 +#: crochet/templates/crochet/pattern.html:177 msgid "Colapsar / expandir" msgstr "" -#: crochet/templates/crochet/pattern.html:167 +#: crochet/templates/crochet/pattern.html:178 msgid "Duplicar sección" msgstr "" -#: crochet/templates/crochet/pattern.html:168 +#: crochet/templates/crochet/pattern.html:179 msgid "Eliminar sección" msgstr "" -#: crochet/templates/crochet/pattern.html:169 +#: crochet/templates/crochet/pattern.html:180 msgid "" "¿Seguro que quieres eliminar este grupo? Se eliminarán también todas las " "secciones que contiene." msgstr "" -#: crochet/templates/crochet/pattern.html:175 +#: crochet/templates/crochet/pattern.html:186 msgid "Añade una sección para ver aquí el resultado." msgstr "" @@ -323,51 +331,55 @@ msgstr "" # Rutas traducibles de crochet/urls.py: la base "pattern/" se traduce como # "patron/" (sin tilde, para no meter caracteres acentuados en la URL). -#: crochet/urls.py:30 +#: crochet/urls.py:31 msgid "pattern//" msgstr "patron//" -#: crochet/urls.py:31 +#: crochet/urls.py:32 msgid "pattern//edit/" msgstr "patron//editar/" -#: crochet/urls.py:32 +#: crochet/urls.py:33 msgid "pattern//save/" msgstr "patron//guardar/" -#: crochet/urls.py:33 +#: crochet/urls.py:34 msgid "pattern//images/" msgstr "patron//imagenes/" -#: crochet/urls.py:34 +#: crochet/urls.py:35 +msgid "pattern//cover/" +msgstr "patron//portada/" + +#: crochet/urls.py:36 msgid "pattern//pdf/" msgstr "patron//pdf/" -#: crochet/urls.py:35 +#: crochet/urls.py:37 msgid "pattern//delete/" msgstr "patron//eliminar/" -#: crochet/urls.py:36 +#: crochet/urls.py:38 msgid "pattern/new/" msgstr "patron/nuevo/" # Igual que "pattern/" arriba: "account/" se traduce como "cuenta/". -#: crochet/urls.py:37 +#: crochet/urls.py:39 msgid "account/" msgstr "cuenta/" -#: crochet/urls.py:38 +#: crochet/urls.py:40 msgid "account/register/" msgstr "cuenta/registro/" -#: crochet/urls.py:40 +#: crochet/urls.py:42 msgid "account/login/" msgstr "cuenta/entrar/" -#: crochet/urls.py:45 +#: crochet/urls.py:47 msgid "account/logout/" msgstr "cuenta/salir/" -#: crochet/views.py:23 +#: crochet/views.py:24 msgid "Este patrón todavía no tiene contenido." msgstr "" diff --git a/crochet/migrations/0007_pattern_cover_image.py b/crochet/migrations/0007_pattern_cover_image.py new file mode 100644 index 0000000..ff5f5fc --- /dev/null +++ b/crochet/migrations/0007_pattern_cover_image.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.7 on 2026-07-17 11:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('crochet', '0006_pattern_created_by'), + ] + + operations = [ + migrations.AddField( + model_name='pattern', + name='cover_image', + field=models.ImageField(blank=True, null=True, upload_to='patterns/covers/originals/%Y/%m/'), + ), + migrations.AddField( + model_name='pattern', + name='cover_image_large', + field=models.ImageField(blank=True, editable=False, null=True, upload_to='patterns/covers/large/%Y/%m/'), + ), + migrations.AddField( + model_name='pattern', + name='cover_image_thumbnail', + field=models.ImageField(blank=True, editable=False, null=True, upload_to='patterns/covers/thumbnails/%Y/%m/'), + ), + ] diff --git a/crochet/models.py b/crochet/models.py index fb3ed8a..7adb303 100644 --- a/crochet/models.py +++ b/crochet/models.py @@ -22,6 +22,20 @@ class Pattern(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + # Portada del patrón: cover_image guarda el archivo tal cual lo sube el + # usuario; cover_image_thumbnail/cover_image_large son las variantes + # redimensionadas (ver crochet/cover_image.py) que se muestran de verdad + # en la web (tarjetas de "Mis patrones", futura vista de detalle...), no + # editable=False porque las genera PatternCoverImageUploadView, no un + # formulario. + cover_image = models.ImageField(upload_to='patterns/covers/originals/%Y/%m/', blank=True, null=True) + cover_image_thumbnail = models.ImageField( + upload_to='patterns/covers/thumbnails/%Y/%m/', blank=True, null=True, editable=False, + ) + cover_image_large = models.ImageField( + upload_to='patterns/covers/large/%Y/%m/', blank=True, null=True, editable=False, + ) + def __str__(self): return self.page_settings.get('title') or str(self.uuid) diff --git a/crochet/pattern_render.py b/crochet/pattern_render.py index f6b2555..cfc749e 100644 --- a/crochet/pattern_render.py +++ b/crochet/pattern_render.py @@ -138,18 +138,22 @@ def _render_section(section, lang, stitch_types_by_id): return '' -def render_pattern_html(sections, page_settings, lang, stitch_types, empty_message): +def render_pattern_html(sections, page_settings, lang, stitch_types, empty_message, cover_image_url=None): """HTML (ya escapado y marcado como seguro) del contenido de un patrón para `lang`, en el mismo formato que genera renderOutput() en el - editor: título/autor del documento primero, y luego cada sección de - nivel superior en orden. `page_settings` debe venir ya validado por - sanitize_page_settings().""" + editor: título, portada y autor del documento primero, y luego cada + sección de nivel superior en orden. `page_settings` debe venir ya + validado por sanitize_page_settings().""" stitch_types_by_id = {st['id']: st for st in stitch_types} - parts = [ - _line('h1', page_settings['title'], 'text-[1.5em] font-bold mb-1'), - _line('p', page_settings['author'], 'text-[0.875em] opacity-70 mb-2'), - ] + parts = [_line('h1', page_settings['title'], 'text-[1.5em] font-bold mb-1')] + # Mismas clases que _render_image() (misma pinta que una imagen de + # sección) y que la portada que pinta render.js en el editor. + if cover_image_url: + parts.append(format_html( + '', cover_image_url, + )) + parts.append(_line('p', page_settings['author'], 'text-[0.875em] opacity-70 mb-2')) if not sections: parts.append(format_html('

{0}

', empty_message)) diff --git a/crochet/static/js/cover-image.js b/crochet/static/js/cover-image.js new file mode 100644 index 0000000..64dcb14 --- /dev/null +++ b/crochet/static/js/cover-image.js @@ -0,0 +1,36 @@ +// Subida de la portada del patrón (ver "Personalización de página" en +// pattern.html). Distinto de sections.js (esa sube imágenes DENTRO de una +// sección de tipo "Imagen"): esto es un atributo del patrón entero, así que +// se sube y aplica en cuanto se elige el archivo, sin esperar a "Guardar". + +import { coverImageInput, coverImageLargeUrlInput, coverImagePreview } from './dom-refs.js'; +import { getCsrfToken } from './csrf.js'; +import { renderOutput } from './render.js'; + +coverImageInput.addEventListener('change', async () => { + const file = coverImageInput.files[0]; + if (!file) return; + + const formData = new FormData(); + formData.append('cover_image', file); + + try { + const response = await fetch(coverImageInput.dataset.uploadUrl, { + method: 'POST', + headers: { 'X-CSRFToken': getCsrfToken() }, + body: formData, + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + + const data = await response.json(); + coverImagePreview.src = data.thumbnailUrl; + coverImagePreview.classList.remove('hidden'); + // "Vista previa" (ver render.js) pinta la portada a partir de este + // valor, igual que lee pageTitleInput/pageAuthorInput. + coverImageLargeUrlInput.value = data.largeUrl; + renderOutput(); + } catch (error) { + console.error('No se ha podido subir la imagen de portada:', error); + alert(coverImageInput.dataset.errorMessage); + } +}); diff --git a/crochet/static/js/dom-refs.js b/crochet/static/js/dom-refs.js index 1c8aa7c..ca5cb43 100644 --- a/crochet/static/js/dom-refs.js +++ b/crochet/static/js/dom-refs.js @@ -22,3 +22,9 @@ export const pageAlignSelect = document.getElementById('page-align-select'); export const pageSizeSelect = document.getElementById('page-size-select'); export const pageOrientationSelect = document.getElementById('page-orientation-select'); export const pageSizeStyle = document.getElementById('page-size-style'); + +// Portada del patrón (ver cover-image.js): configuración del documento +// entero, igual que título/autor de arriba, no una sección más. +export const coverImageInput = document.getElementById('cover-image-input'); +export const coverImagePreview = document.getElementById('cover-image-preview'); // Miniatura dentro del propio control de subida. +export const coverImageLargeUrlInput = document.getElementById('cover-image-large-url'); // Leído por render.js para "Vista previa". diff --git a/crochet/static/js/main.js b/crochet/static/js/main.js index 34caf10..cef1e3c 100644 --- a/crochet/static/js/main.js +++ b/crochet/static/js/main.js @@ -8,6 +8,7 @@ import { applyDefaultSkeleton, buildSectionsFrom } from './section-types.js'; import { setAllSectionsCollapsed } from './sections.js'; import './tabs.js'; import './storage.js'; +import './cover-image.js'; // Mismo punto de corte que .panel-hidden-mobile (ver css/main.css). const MOBILE_BREAKPOINT_QUERY = '(max-width: 767px)'; diff --git a/crochet/static/js/render.js b/crochet/static/js/render.js index f5e3f1c..e6d83f1 100644 --- a/crochet/static/js/render.js +++ b/crochet/static/js/render.js @@ -3,7 +3,7 @@ // repintarlo: al escribir, al cambiar de idioma, o al añadir/quitar/ // reordenar secciones o elementos soltados. -import { outputText, sectionsContainer, pageTitleInput, pageAuthorInput } from './dom-refs.js'; +import { outputText, sectionsContainer, pageTitleInput, pageAuthorInput, coverImageLargeUrlInput } from './dom-refs.js'; import { TRANSLATABLE_FIELDS_SELECTOR, setTranslation, setupLanguageSelect } from './i18n.js'; import { renderOutputSection, appendOutputLine } from './section-types.js'; @@ -12,13 +12,25 @@ import { renderOutputSection, appendOutputLine } from './section-types.js'; export function renderOutput() { outputText.innerHTML = ''; - // Título y autor del patrón (ver "Personalización de página"): son - // configuración de todo el documento, no una sección más, así que se + // Título, portada y autor del patrón (ver "Personalización de página"): + // son configuración de todo el documento, no una sección más, así que se // pintan siempre delante, haya o no secciones añadidas. // Tamaños en "em" (no las clases normales de Tailwind, en "rem") para que // escalen con el "Tamaño de letra" de #panel-output-text (ver // page-config.js): "rem" ignora el font-size de cualquier ancestro. appendOutputLine(outputText, 'h1', pageTitleInput.value.trim(), 'text-[1.5em] font-bold mb-1'); + + // Debajo del título: mismo tamaño "large" y mismas clases que pinta + // render_pattern_html() en la vista de solo lectura/PDF, para que ambos + // resultados sean visualmente idénticos (ver pattern_render.py). + if (coverImageLargeUrlInput.value) { + const coverImage = document.createElement('img'); + coverImage.src = coverImageLargeUrlInput.value; + coverImage.alt = ''; + coverImage.className = 'max-w-full max-h-96 object-contain rounded-2xl my-2'; + outputText.appendChild(coverImage); + } + appendOutputLine(outputText, 'p', pageAuthorInput.value.trim(), 'text-[0.875em] opacity-70 mb-2'); // Si aún no se ha añadido ninguna sección, avisar en vez de dejar el diff --git a/crochet/templates/crochet/account_home.html b/crochet/templates/crochet/account_home.html index 648b423..e98401b 100644 --- a/crochet/templates/crochet/account_home.html +++ b/crochet/templates/crochet/account_home.html @@ -22,6 +22,13 @@
{% for pattern in patterns %}
+
+ {% if pattern.cover_image_thumbnail %} + + {% else %} + 🧶 + {% endif %} +

{% if pattern.page_settings.title %}{{ pattern.page_settings.title }}{% else %}{% trans 'Patrón sin título' %}{% endif %} diff --git a/crochet/templates/crochet/pattern.html b/crochet/templates/crochet/pattern.html index b98e88c..a7bfed1 100644 --- a/crochet/templates/crochet/pattern.html +++ b/crochet/templates/crochet/pattern.html @@ -62,6 +62,23 @@

+
+ + + + +