diff --git a/.gitea/workflows/test-build.yaml b/.gitea/workflows/test-build.yaml new file mode 100644 index 0000000..0b61b18 --- /dev/null +++ b/.gitea/workflows/test-build.yaml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: [master] + +defaults: + run: + shell: sh + +jobs: + test: + runs-on: ubuntu-latest + container: + image: python:3.13-bookworm + steps: + - name: Checkout + run: | + git clone https://git.spookydev.com/${{ gitea.repository }}.git . + git checkout ${{ gitea.sha }} + + - name: Install uv + run: pip install uv --quiet + + - name: Install dependencies + run: uv pip install -r pyproject.toml --group=dev + + - name: Run tests + run: uv run pytest + + build: + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout + run: | + git clone https://git.spookydev.com/${{ gitea.repository }}.git . + git checkout ${{ gitea.sha }} + + - name: Build and push + run: | + docker build -t registry.spookydev.com/${{ gitea.repository }}:${{ gitea.sha }} . + docker tag registry.spookydev.com/${{ gitea.repository }}:${{ gitea.sha }} \ + registry.spookydev.com/${{ gitea.repository }}:latest + docker login registry.spookydev.com \ + -u ${{ secrets.REGISTRY_USER }} \ + -p ${{ secrets.REGISTRY_PASSWORD }} + docker push registry.spookydev.com/${{ gitea.repository }}:${{ gitea.sha }} + docker push registry.spookydev.com/${{ gitea.repository }}:latest diff --git a/.gitignore b/.gitignore index de6aefe..36e05d2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ local_settings.py db.sqlite3 db.sqlite3-journal media +config/static # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ # in your Git repository. Update and uncomment the following line accordingly. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c71eed3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.13-alpine3.23 AS builder +ENV UV_SYSTEM_PYTHON=1 +COPY pyproject.toml . +RUN pip install uv && uv pip install -r pyproject.toml && pip uninstall -y uv + +FROM python:3.13-alpine3.23 + +COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages + +WORKDIR /code + +# Set user and group +ARG user=apps +ARG uid=1001 +ARG gid=1001 + +RUN addgroup -S ${user} && adduser -S ${user} -G ${user} -u ${uid} -s /bin/sh + +# pango + ttf-dejavu: librerías nativas que weasyprint necesita en tiempo de +# ejecución (no al instalar el paquete Python, que ya funciona sin esto) para +# poder generar el PDF del patrón (ver PatternPdfView); sin fuentes +# instaladas, Pango no tiene nada que medir/dibujar y WeasyPrint termina +# haciendo segfault en vez de fallar con un error legible. +RUN apk update && apk add gcc gettext vim libpq-dev pango ttf-dejavu +RUN chown -R ${user}:${user} /code + +USER ${user} +COPY --chown=${user}:${user} . /code + +RUN ["sh", "./scripts/build.sh"] +CMD ["sh", "./scripts/run.sh"] diff --git a/config/asgi.py b/config/asgi.py index a08b215..ffbb5f5 100644 --- a/config/asgi.py +++ b/config/asgi.py @@ -11,6 +11,6 @@ import os from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.config') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') application = get_asgi_application() diff --git a/config/settings.py b/config/settings.py index 379ebbc..7b4f318 100644 --- a/config/settings.py +++ b/config/settings.py @@ -42,6 +42,13 @@ INSTALLED_APPS = [ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + # Sirve los archivos de STATIC_ROOT directamente desde la propia app: en + # el contenedor Docker no hay nginx/proxy delante, solo uvicorn (ver + # scripts/run.sh), así que sin esto no había nada que respondiera a + # /static/... aunque collectstatic ya los hubiera copiado ahí (ver + # scripts/build.sh). Va justo después de SecurityMiddleware, como pide + # WhiteNoise. + 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', # Debe ir después de SessionMiddleware y antes de CommonMiddleware (así # lo pide Django): resuelve el idioma activo a partir del prefijo de la @@ -129,6 +136,18 @@ USE_TZ = True STATIC_URL = 'static/' STATIC_ROOT = 'config/static' +# CompressedManifestStaticFilesStorage (WhiteNoise): sirve cada archivo ya +# comprimido (gzip/brotli) y con hash en el nombre para poder cachearlos +# "para siempre" sin arriesgarse a servir una versión vieja tras un deploy. +STORAGES = { + 'default': { + 'BACKEND': 'django.core.files.storage.FileSystemStorage', + }, + 'staticfiles': { + 'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage', + }, +} + # Archivos subidos por el usuario (imágenes de patrón), separados de los # estáticos del propio proyecto. MEDIA_URL = 'media/' diff --git a/config/wsgi.py b/config/wsgi.py index 9822948..4ced574 100644 --- a/config/wsgi.py +++ b/config/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.config') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') application = get_wsgi_application() diff --git a/crochet/management/commands/build_pattern_detail_css.py b/crochet/management/commands/build_pattern_detail_css.py index 56912ab..646712a 100644 --- a/crochet/management/commands/build_pattern_detail_css.py +++ b/crochet/management/commands/build_pattern_detail_css.py @@ -26,6 +26,15 @@ PLATFORM_ASSETS = { ('Darwin', 'arm64'): 'tailwindcss-macos-arm64', ('Windows', 'AMD64'): 'tailwindcss-windows-x64.exe', } +# La imagen Docker del proyecto es Alpine (musl, no glibc): el binario normal +# de Linux no tiene intérprete ELF válido ahí y ni siquiera se llega a +# ejecutar ("No such file or directory" pese a existir el archivo). +# platform.libc_ver()[0] viene vacío en musl (Python solo lo rellena en +# glibc), es la forma más simple de distinguirlos sin mirar rutas fijas. +PLATFORM_ASSETS_MUSL = { + ('Linux', 'x86_64'): 'tailwindcss-linux-x64-musl', + ('Linux', 'aarch64'): 'tailwindcss-linux-arm64-musl', +} TOOLS_DIR = settings.BASE_DIR / '.tools' TAILWIND_BIN = TOOLS_DIR / ('tailwindcss.exe' if platform.system() == 'Windows' else 'tailwindcss') @@ -64,7 +73,8 @@ class Command(BaseCommand): def _download_tailwind_cli(self): key = (platform.system(), platform.machine()) - asset = PLATFORM_ASSETS.get(key) + is_musl = key[0] == 'Linux' and not platform.libc_ver()[0] + asset = (PLATFORM_ASSETS_MUSL if is_musl else PLATFORM_ASSETS).get(key) if not asset: raise CommandError( f'No hay binario de tailwindcss conocido para {key}. ' diff --git a/crochet/static/css/main.css b/crochet/static/css/main.css index 73b2b67..f88c88f 100644 --- a/crochet/static/css/main.css +++ b/crochet/static/css/main.css @@ -71,6 +71,18 @@ .panel-hidden-mobile { display: none; } + + /* La flecha de colapsar/expandir una sección (▾/▸, ver .collapse-btn en + sections.js) usa btn-xs de daisyUI (~1.5rem), demasiado pequeña para + tocarla con precisión. Se agranda el área de toque (y el propio + glifo) solo en pantallas estrechas, sin tocar el tamaño en escritorio. + !important porque btn-xs (daisyUI) se carga después de este archivo + en y, a igual especificidad, ganaría por ir después. */ + .collapse-btn { + min-width: 2.75rem !important; + min-height: 2.75rem !important; + font-size: 1.25rem !important; + } } /* Al exportar/imprimir, solo se muestra #panel-output (y no sus propios diff --git a/pyproject.toml b/pyproject.toml index 35b030c..5fd5d5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,5 +7,12 @@ requires-python = ">=3.13" dependencies = [ "django>=6.0.7", "pillow>=12.3.0", + "uvicorn>=0.51.0", "weasyprint>=69.0", + "whitenoise>=6.12.0", +] + +[dependency-groups] +dev = [ + "pytest>=9.1.1", ] diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..9b101e6 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,3 @@ +python manage.py build_pattern_detail_css +python manage.py collectstatic --noinput +python manage.py compilemessages diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100644 index 0000000..86ea6a1 --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +ASGI_HOST=${WSGI_HOST:="0.0.0.0"} +ASGI_PORT=8000 +ASGI_WORKERS=${WSGI_WORKERS:=1} +WORKER_NUM_PROCESSES=${WORKER_NUM_PROCESSES:=1} + +RUN_SERVER=${RUN_SERVER:="FALSE"} +RUN_CELERY=${RUN_CELERY:="FALSE"} + +python -m uvicorn --workers $ASGI_WORKERS --host $ASGI_HOST --port $ASGI_PORT config.asgi:application diff --git a/uv.lock b/uv.lock index 2141b20..507390e 100644 --- a/uv.lock +++ b/uv.lock @@ -133,6 +133,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, ] +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + [[package]] name = "crochet" version = "0.1.0" @@ -140,16 +161,28 @@ source = { virtual = "." } dependencies = [ { name = "django" }, { name = "pillow" }, + { name = "uvicorn" }, { name = "weasyprint" }, + { name = "whitenoise" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, ] [package.metadata] requires-dist = [ { name = "django", specifier = ">=6.0.7" }, { name = "pillow", specifier = ">=12.3.0" }, + { name = "uvicorn", specifier = ">=0.51.0" }, { name = "weasyprint", specifier = ">=69.0" }, + { name = "whitenoise", specifier = ">=6.12.0" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.1.1" }] + [[package]] name = "cssselect2" version = "0.9.0" @@ -217,6 +250,33 @@ woff = [ { name = "zopfli" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + [[package]] name = "pillow" version = "12.3.0" @@ -279,6 +339,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -297,6 +366,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/11/47efe2f66ba848a107adfd490b508f5c0cedc82127950553dca44d29e6c4/pydyf-0.12.1-py3-none-any.whl", hash = "sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc", size = 8028, upload-time = "2025-12-02T14:52:12.938Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pyphen" version = "0.17.2" @@ -306,6 +384,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" }, ] +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + [[package]] name = "sqlparse" version = "0.5.5" @@ -348,6 +442,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, ] +[[package]] +name = "uvicorn" +version = "0.51.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, +] + [[package]] name = "weasyprint" version = "69.0" @@ -376,6 +483,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, ] +[[package]] +name = "whitenoise" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" }, +] + [[package]] name = "zopfli" version = "0.4.3"