feat: added improvements from django-htmx-template
This commit is contained in:
@@ -35,9 +35,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Compile translations
|
- name: Compile translations
|
||||||
run: uv run python manage.py compilemessages
|
run: uv run python manage.py compilemessages
|
||||||
|
env:
|
||||||
|
SECRET_KEY: 'super-secret-key'
|
||||||
|
ALLOWED_HOSTS: '*'
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: uv run pytest --cov
|
run: uv run pytest --cov
|
||||||
|
env:
|
||||||
|
SECRET_KEY: 'super-secret-key'
|
||||||
|
ALLOWED_HOSTS: '*'
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
+8
-2
@@ -1,7 +1,9 @@
|
|||||||
FROM python:3.13-alpine3.23 AS builder
|
FROM python:3.13-alpine3.23 AS builder
|
||||||
ENV UV_SYSTEM_PYTHON=1
|
ENV UV_SYSTEM_PYTHON=1
|
||||||
|
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:0.11 /uv /usr/local/bin/uv
|
||||||
COPY pyproject.toml .
|
COPY pyproject.toml .
|
||||||
RUN pip install uv && uv pip install -r pyproject.toml && pip uninstall -y uv
|
RUN uv pip install -r pyproject.toml
|
||||||
|
|
||||||
FROM python:3.13-alpine3.23
|
FROM python:3.13-alpine3.23
|
||||||
|
|
||||||
@@ -21,8 +23,12 @@ RUN chown -R ${user}:${user} /code
|
|||||||
|
|
||||||
COPY --chown=${user}:${user} . /code
|
COPY --chown=${user}:${user} . /code
|
||||||
|
|
||||||
|
# SECRET_KEY/ALLOWED_HOSTS provisionales para la construcción de la imagen
|
||||||
|
# (build.sh carga settings, que ya no traen un valor por defecto para
|
||||||
|
# ninguna de las dos, ver config/settings/env.py). En producción no se
|
||||||
|
# heredan: solo existen en esta RUN, no quedan en la imagen final.
|
||||||
RUN apk add --no-cache --virtual .tailwind-runtime libstdc++ libgcc \
|
RUN apk add --no-cache --virtual .tailwind-runtime libstdc++ libgcc \
|
||||||
&& su ${user} -c "sh ./scripts/build.sh" \
|
&& SECRET_KEY=build-only ALLOWED_HOSTS='*' su ${user} -c "sh ./scripts/build.sh" \
|
||||||
&& apk del .tailwind-runtime
|
&& apk del .tailwind-runtime
|
||||||
|
|
||||||
USER ${user}
|
USER ${user}
|
||||||
|
|||||||
@@ -65,16 +65,20 @@ uv sync --group dev
|
|||||||
```
|
```
|
||||||
|
|
||||||
Crea un archivo `.env` en la raíz del proyecto (ver
|
Crea un archivo `.env` en la raíz del proyecto (ver
|
||||||
[Variables de entorno](#variables-de-entorno) más abajo). Para desarrollo
|
[Variables de entorno](#variables-de-entorno) más abajo). `SECRET_KEY` y
|
||||||
local basta con:
|
`ALLOWED_HOSTS` son las únicas que no tienen un valor por defecto (a
|
||||||
|
propósito: una clave/lista de hosts insegura conocida de antemano no
|
||||||
|
protege nada), así que hacen falta incluso en desarrollo local:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
DEBUG=True
|
DEBUG=True
|
||||||
|
SECRET_KEY=cualquier-cadena-larga-y-aleatoria
|
||||||
|
ALLOWED_HOSTS=*
|
||||||
```
|
```
|
||||||
|
|
||||||
Con eso, la app arranca con SQLite, `SECRET_KEY` de desarrollo, envío de
|
Con eso, la app arranca con SQLite, envío de email al backend de consola
|
||||||
email al backend de consola (se imprime en la terminal en vez de mandarse
|
(se imprime en la terminal en vez de mandarse de verdad) y sin
|
||||||
de verdad) y sin credenciales adicionales.
|
credenciales adicionales.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run python manage.py migrate
|
uv run python manage.py migrate
|
||||||
@@ -164,12 +168,17 @@ commit.
|
|||||||
|
|
||||||
Todas se leen en `config/settings/env.py`; sin `.env`, o si falta alguna,
|
Todas se leen en `config/settings/env.py`; sin `.env`, o si falta alguna,
|
||||||
se usan los valores por defecto indicados (pensados para desarrollo local).
|
se usan los valores por defecto indicados (pensados para desarrollo local).
|
||||||
|
`SECRET_KEY` y `ALLOWED_HOSTS` son la excepción: no tienen valor por
|
||||||
|
defecto, así que el arranque falla con un error claro
|
||||||
|
(`ImproperlyConfigured`) si faltan, en vez de servir tráfico con una clave
|
||||||
|
insegura conocida de antemano o con el check de `Host` desactivado de
|
||||||
|
hecho.
|
||||||
|
|
||||||
| Variable | Por defecto | Descripción |
|
| Variable | Por defecto | Descripción |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `SECRET_KEY` | clave insegura de desarrollo | **Cámbiala en producción.** |
|
| `SECRET_KEY` | *(obligatoria)* | Clave única y secreta. **Distinta en cada entorno, nunca la de otro despliegue.** |
|
||||||
| `DEBUG` | `False` | Activa páginas de error detalladas, Django Debug Toolbar y Silk. No debe estar activo en producción. |
|
| `DEBUG` | `False` | Activa páginas de error detalladas, Django Debug Toolbar y Silk. No debe estar activo en producción. |
|
||||||
| `ALLOWED_HOSTS` | `*` | Lista separada por comas. Debe restringirse a los dominios reales en producción. |
|
| `ALLOWED_HOSTS` | *(obligatoria)* | Lista separada por comas. `*` vale para desarrollo local; restringida a los dominios reales en producción. |
|
||||||
| `CSRF_TRUSTED_ORIGINS` | `http://localhost:8000` | Lista separada por comas. Necesario si la app se sirve detrás de un dominio/HTTPS distinto. |
|
| `CSRF_TRUSTED_ORIGINS` | `http://localhost:8000` | Lista separada por comas. Necesario si la app se sirve detrás de un dominio/HTTPS distinto. |
|
||||||
| `DATABASE_URL` | `sqlite:///db.sqlite3` | Formato `django-environ` (p. ej. `postgres://usuario:password@host:5432/nombre_bd`). |
|
| `DATABASE_URL` | `sqlite:///db.sqlite3` | Formato `django-environ` (p. ej. `postgres://usuario:password@host:5432/nombre_bd`). |
|
||||||
| `S3_ENABLED` | `False` | Si es `True`, los archivos subidos (imágenes de portada/patrón) se guardan en S3 en vez de en el filesystem local. |
|
| `S3_ENABLED` | `False` | Si es `True`, los archivos subidos (imágenes de portada/patrón) se guardan en S3 en vez de en el filesystem local. |
|
||||||
@@ -219,7 +228,10 @@ La imagen Docker ya ejecuta `collectstatic`, `compilemessages` y el
|
|||||||
CSS precompilado al construirse, y sirve la app con Uvicorn (variables
|
CSS precompilado al construirse, y sirve la app con Uvicorn (variables
|
||||||
`WSGI_HOST`/`WSGI_WORKERS` para ajustar host/nº de workers). WhiteNoise se
|
`WSGI_HOST`/`WSGI_WORKERS` para ajustar host/nº de workers). WhiteNoise se
|
||||||
encarga de servir los estáticos directamente desde la propia app, sin
|
encarga de servir los estáticos directamente desde la propia app, sin
|
||||||
necesidad de un servidor/proxy de estáticos aparte.
|
necesidad de un servidor/proxy de estáticos aparte; también sirve
|
||||||
|
`/media/` (portadas/imágenes de patrón) cuando `S3_ENABLED=False` (ver
|
||||||
|
`crochet/middleware.py`), así que tampoco hace falta nada aparte para eso
|
||||||
|
salvo que uses S3.
|
||||||
|
|
||||||
Al hacer push a `master` se dispara la integración continua (tests) y,
|
Al hacer push a `master` se dispara la integración continua (tests) y,
|
||||||
si pasan, se construye y publica una imagen Docker (ver
|
si pasan, se construye y publica una imagen Docker (ver
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ if DEBUG:
|
|||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
'crochet.middleware.WhiteNoiseWithMediaMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
@@ -141,6 +141,11 @@ LANGUAGES = [
|
|||||||
('en', 'English'),
|
('en', 'English'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Un año (en vez del default de Django, de sesión: se perdía al cerrar el
|
||||||
|
# navegador). La deja la vista "set_language" (ver config/urls.py) al elegir
|
||||||
|
# idioma desde el selector del navbar (ver crochet/base.html).
|
||||||
|
LANGUAGE_COOKIE_AGE = 60 * 60 * 24 * 365
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|||||||
@@ -12,11 +12,17 @@ env_file = BASE_DIR / '.env'
|
|||||||
if os.path.exists(env_file):
|
if os.path.exists(env_file):
|
||||||
environ.Env.read_env(env_file)
|
environ.Env.read_env(env_file)
|
||||||
|
|
||||||
SECRET_KEY = env.str('SECRET_KEY', 'this-is-the-default-secret-key')
|
SECRET_KEY = env.str('SECRET_KEY')
|
||||||
DEBUG = env.bool('DEBUG', False)
|
DEBUG = env.bool('DEBUG', False)
|
||||||
|
|
||||||
CORS_ORIGIN_WHITELIST = env.str('CORS_ORIGIN_WHITELIST', 'http://localhost:8000').split(',')
|
CORS_ORIGIN_WHITELIST = env.str('CORS_ORIGIN_WHITELIST', 'http://localhost:8000').split(',')
|
||||||
CSRF_TRUSTED_ORIGINS = env.str('CSRF_TRUSTED_ORIGINS', 'http://localhost:8000').split(',')
|
CSRF_TRUSTED_ORIGINS = env.str('CSRF_TRUSTED_ORIGINS', 'http://localhost:8000').split(',')
|
||||||
|
# La cookie CSRF solo hace falta leerla desde JS en sitios que la mandan a
|
||||||
|
# mano en una cabecera (fetch/XHR); aquí no la lee ningún script (los POST
|
||||||
|
# de HTMX/fetch usan el token embebido en el HTML vía csrf_token, ver
|
||||||
|
# account_settings.html/csrf.js), así que no hay motivo para exponerla al
|
||||||
|
# DOM y sí para quitarle esa superficie a un XSS.
|
||||||
|
CSRF_COOKIE_HTTPONLY = True
|
||||||
|
|
||||||
DATABASE_URL = env.db_url('DATABASE_URL', 'sqlite:///db.sqlite3')
|
DATABASE_URL = env.db_url('DATABASE_URL', 'sqlite:///db.sqlite3')
|
||||||
|
|
||||||
@@ -24,7 +30,7 @@ REDIS_HOST = env.str('REDIS_HOST', 'redis')
|
|||||||
REDIS_PORT = env.str('REDIS_PORT', 6379)
|
REDIS_PORT = env.str('REDIS_PORT', 6379)
|
||||||
|
|
||||||
PAGE_SIZE = env.int('PAGE_SIZE', 20)
|
PAGE_SIZE = env.int('PAGE_SIZE', 20)
|
||||||
ALLOWED_HOSTS = env.str('ALLOWED_HOSTS', '*').split(',')
|
ALLOWED_HOSTS = env.str('ALLOWED_HOSTS').split(',')
|
||||||
|
|
||||||
CELERY_BROKER_URL = env.str('CELERY_BROKER_URL', 'redis://172.17.0.1:6379/0')
|
CELERY_BROKER_URL = env.str('CELERY_BROKER_URL', 'redis://172.17.0.1:6379/0')
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ urlpatterns = [
|
|||||||
view=TemplateView.as_view(template_name='manifest.json', content_type='application/manifest+json'),
|
view=TemplateView.as_view(template_name='manifest.json', content_type='application/manifest+json'),
|
||||||
),
|
),
|
||||||
path('offline/', name='offline', view=TemplateView.as_view(template_name='offline.html')),
|
path('offline/', name='offline', view=TemplateView.as_view(template_name='offline.html')),
|
||||||
|
path('i18n/', include('django.conf.urls.i18n')),
|
||||||
]
|
]
|
||||||
|
|
||||||
# i18n_patterns antepone el prefijo de idioma (/es/..., /en/...) a cada URL,
|
# i18n_patterns antepone el prefijo de idioma (/es/..., /en/...) a cada URL,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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-09-16 14:05+0000\n"
|
"POT-Creation-Date: 2026-09-21 12:22+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"
|
||||||
@@ -239,7 +239,7 @@ msgstr "You don't have any patterns yet."
|
|||||||
|
|
||||||
#: crochet/templates/crochet/account_settings.html:4
|
#: crochet/templates/crochet/account_settings.html:4
|
||||||
#: crochet/templates/crochet/account_settings.html:18
|
#: crochet/templates/crochet/account_settings.html:18
|
||||||
#: crochet/templates/crochet/base.html:71
|
#: crochet/templates/crochet/base.html:82
|
||||||
msgid "Ajustes de la cuenta"
|
msgid "Ajustes de la cuenta"
|
||||||
msgstr "Account settings"
|
msgstr "Account settings"
|
||||||
|
|
||||||
@@ -277,15 +277,15 @@ msgstr "This will permanently delete your account and all your patterns."
|
|||||||
msgid "Cambiar tema"
|
msgid "Cambiar tema"
|
||||||
msgstr "Change theme"
|
msgstr "Change theme"
|
||||||
|
|
||||||
#: crochet/templates/crochet/base.html:58
|
#: crochet/templates/crochet/base.html:62
|
||||||
msgid "Cambiar idioma"
|
msgid "Cambiar idioma"
|
||||||
msgstr "Change language"
|
msgstr "Change language"
|
||||||
|
|
||||||
#: crochet/templates/crochet/base.html:75
|
#: crochet/templates/crochet/base.html:86
|
||||||
msgid "Cerrar sesión"
|
msgid "Cerrar sesión"
|
||||||
msgstr "Log out"
|
msgstr "Log out"
|
||||||
|
|
||||||
#: crochet/templates/crochet/base.html:81
|
#: crochet/templates/crochet/base.html:92
|
||||||
#: crochet/templates/crochet/home.html:25
|
#: crochet/templates/crochet/home.html:25
|
||||||
#: crochet/templates/crochet/password_reset_complete.html:18
|
#: crochet/templates/crochet/password_reset_complete.html:18
|
||||||
#: crochet/templates/registration/login.html:4
|
#: crochet/templates/registration/login.html:4
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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-09-16 14:05+0000\n"
|
"POT-Creation-Date: 2026-09-21 12:22+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"
|
||||||
@@ -229,7 +229,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: crochet/templates/crochet/account_settings.html:4
|
#: crochet/templates/crochet/account_settings.html:4
|
||||||
#: crochet/templates/crochet/account_settings.html:18
|
#: crochet/templates/crochet/account_settings.html:18
|
||||||
#: crochet/templates/crochet/base.html:71
|
#: crochet/templates/crochet/base.html:82
|
||||||
msgid "Ajustes de la cuenta"
|
msgid "Ajustes de la cuenta"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -265,15 +265,15 @@ msgstr ""
|
|||||||
msgid "Cambiar tema"
|
msgid "Cambiar tema"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: crochet/templates/crochet/base.html:58
|
#: crochet/templates/crochet/base.html:62
|
||||||
msgid "Cambiar idioma"
|
msgid "Cambiar idioma"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: crochet/templates/crochet/base.html:75
|
#: crochet/templates/crochet/base.html:86
|
||||||
msgid "Cerrar sesión"
|
msgid "Cerrar sesión"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: crochet/templates/crochet/base.html:81
|
#: crochet/templates/crochet/base.html:92
|
||||||
#: crochet/templates/crochet/home.html:25
|
#: crochet/templates/crochet/home.html:25
|
||||||
#: crochet/templates/crochet/password_reset_complete.html:18
|
#: crochet/templates/crochet/password_reset_complete.html:18
|
||||||
#: crochet/templates/registration/login.html:4
|
#: crochet/templates/registration/login.html:4
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from whitenoise.middleware import WhiteNoiseMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
class WhiteNoiseWithMediaMiddleware(WhiteNoiseMiddleware):
|
||||||
|
# Además de los estáticos de siempre (ver WhiteNoiseMiddleware), sirve
|
||||||
|
# también MEDIA_ROOT (portadas/imágenes de patrón) cuando no hay S3
|
||||||
|
# configurado: sin esto, en un despliegue sin S3_ENABLED no hay nada
|
||||||
|
# que sirva /media/ (STORAGES['default'] es FileSystemStorage, que solo
|
||||||
|
# sabe guardar/leer en disco, no exponerlo por HTTP), así que las
|
||||||
|
# imágenes se guardarían pero no se verían. add_files() en __init__ (no
|
||||||
|
# en cada petición) para que WhiteNoise las indexe una sola vez, igual
|
||||||
|
# que hace con STATIC_ROOT.
|
||||||
|
def __init__(self, get_response):
|
||||||
|
super().__init__(get_response)
|
||||||
|
if not settings.S3_ENABLED and os.path.isdir(settings.MEDIA_ROOT):
|
||||||
|
self.add_files(str(settings.MEDIA_ROOT), prefix=settings.MEDIA_URL)
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<section data-settings-panel="profile" class="flex flex-col gap-8">
|
<section data-settings-panel="profile" class="flex flex-col gap-8">
|
||||||
<section class="card bg-base-100 shadow border border-base-300">
|
<section class="card bg-base-100 shadow border border-base-300 mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-lg">{% trans 'Nombre para mostrar' %}</h2>
|
<h2 class="card-title text-lg">{% trans 'Nombre para mostrar' %}</h2>
|
||||||
{% include 'crochet/_account_display_name_form.html' %}
|
{% include 'crochet/_account_display_name_form.html' %}
|
||||||
@@ -44,14 +44,14 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section data-settings-panel="security" class="hidden flex-col gap-8">
|
<section data-settings-panel="security" class="hidden flex-col gap-8">
|
||||||
<section class="card bg-base-100 shadow border border-base-300">
|
<section class="card bg-base-100 shadow border border-base-300 mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-lg">{% trans 'Email' %}</h2>
|
<h2 class="card-title text-lg">{% trans 'Email' %}</h2>
|
||||||
{% include 'crochet/_account_email_form.html' %}
|
{% include 'crochet/_account_email_form.html' %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card bg-base-100 shadow border border-base-300">
|
<section class="card bg-base-100 shadow border border-base-300 mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-lg">{% trans 'Cambiar contraseña' %}</h2>
|
<h2 class="card-title text-lg">{% trans 'Cambiar contraseña' %}</h2>
|
||||||
{% include 'crochet/_account_password_form.html' %}
|
{% include 'crochet/_account_password_form.html' %}
|
||||||
|
|||||||
@@ -51,14 +51,25 @@
|
|||||||
<!-- El idioma es una preferencia de la página, no de la cuenta: va
|
<!-- El idioma es una preferencia de la página, no de la cuenta: va
|
||||||
siempre en el mismo sitio del navbar, tanto si has iniciado
|
siempre en el mismo sitio del navbar, tanto si has iniciado
|
||||||
sesión como si no, en vez de vivir dentro del dropdown de
|
sesión como si no, en vez de vivir dentro del dropdown de
|
||||||
usuario (que no existe para anónimos). Cada opción es un <a>
|
usuario (que no existe para anónimos). POST a la vista
|
||||||
normal (no un <select>), así que navegar con teclado entre
|
"set_language" de Django (ver config/urls.py), no un simple
|
||||||
ellas no dispara nada hasta que se pulsa/activa una. -->
|
enlace a la URL traducida con el tag "translate_url": eso
|
||||||
|
cambia el prefijo de idioma de esta URL, pero no deja la
|
||||||
|
cookie LANGUAGE_COOKIE_NAME, así que LocaleMiddleware volvía a
|
||||||
|
decidir por Accept-Language en la siguiente visita a "/" en
|
||||||
|
vez de recordar lo elegido. -->
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<button type="button" tabindex="0" class="btn btn-sm btn-ghost" aria-label="{% trans 'Cambiar idioma' %}">{{ CURRENT_LANGUAGE|language_flag }} {{ CURRENT_LANGUAGE|upper }}</button>
|
<button type="button" tabindex="0" class="btn btn-sm btn-ghost" aria-label="{% trans 'Cambiar idioma' %}">{{ CURRENT_LANGUAGE|language_flag }} {{ CURRENT_LANGUAGE|upper }}</button>
|
||||||
<ul tabindex="0" class="dropdown-content menu menu-sm bg-base-100 border border-base-300 rounded-box z-10 w-40 p-2 shadow">
|
<ul tabindex="0" class="dropdown-content menu menu-sm bg-base-100 border border-base-300 rounded-box z-10 w-40 p-2 shadow">
|
||||||
{% for lang_code, lang_name in AVAILABLE_LANGUAGES %}
|
{% for lang_code, lang_name in AVAILABLE_LANGUAGES %}
|
||||||
<li><a href="{% translate_url request.path lang_code %}"{% if lang_code == CURRENT_LANGUAGE %} class="menu-active"{% endif %}>{{ lang_code|language_flag }} {{ lang_name }}</a></li>
|
<li>
|
||||||
|
<form method="post" action="{% url 'set_language' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="language" value="{{ lang_code }}">
|
||||||
|
<input type="hidden" name="next" value="{% translate_url request.path lang_code %}">
|
||||||
|
<button type="submit" class="w-full text-left cursor-pointer my-2">{{ lang_code|language_flag }} {{ lang_name }}</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,11 +79,11 @@
|
|||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<button type="button" tabindex="0" class="btn btn-sm btn-ghost">{{ user.get_full_name|default:user.username }}</button>
|
<button type="button" tabindex="0" class="btn btn-sm btn-ghost">{{ user.get_full_name|default:user.username }}</button>
|
||||||
<ul tabindex="0" class="dropdown-content menu menu-sm bg-base-100 border border-base-300 rounded-box z-10 w-52 p-2 shadow">
|
<ul tabindex="0" class="dropdown-content menu menu-sm bg-base-100 border border-base-300 rounded-box z-10 w-52 p-2 shadow">
|
||||||
<li><a href="{% url 'crochet:account_settings' %}">{% trans 'Ajustes de la cuenta' %}</a></li>
|
<li><a class="my-2" href="{% url 'crochet:account_settings' %}">{% trans 'Ajustes de la cuenta' %}</a></li>
|
||||||
<li>
|
<li>
|
||||||
<form method="post" action="{% url 'crochet:logout' %}">
|
<form method="post" action="{% url 'crochet:logout' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="w-full text-left">{% trans 'Cerrar sesión' %}</button>
|
<button type="submit" class="w-full text-left my-2">{% trans 'Cerrar sesión' %}</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -93,8 +93,14 @@ class CookieBannerTests(TestCase):
|
|||||||
|
|
||||||
class LanguageSwitcherTests(TestCase):
|
class LanguageSwitcherTests(TestCase):
|
||||||
def test_home_page_switcher_offers_the_same_page_in_english(self):
|
def test_home_page_switcher_offers_the_same_page_in_english(self):
|
||||||
|
# POST a la vista "set_language" (ver config/urls.py), no un <a>
|
||||||
|
# normal: un <a href="{% translate_url %}"> cambia el prefijo de la
|
||||||
|
# URL pero no deja la cookie LANGUAGE_COOKIE_NAME, así que
|
||||||
|
# LocaleMiddleware volvía a decidir por Accept-Language en la
|
||||||
|
# siguiente visita en vez de recordar lo elegido.
|
||||||
response = self.client.get('/es/')
|
response = self.client.get('/es/')
|
||||||
self.assertContains(response, 'href="/en/"')
|
self.assertContains(response, 'action="/i18n/setlang/"')
|
||||||
|
self.assertContains(response, 'value="/en/"')
|
||||||
|
|
||||||
def test_translated_route_switcher_preserves_the_current_view(self):
|
def test_translated_route_switcher_preserves_the_current_view(self):
|
||||||
# No solo cambia el prefijo (/es/ -> /en/): toda la ruta traducible
|
# No solo cambia el prefijo (/es/ -> /en/): toda la ruta traducible
|
||||||
@@ -109,7 +115,7 @@ class LanguageSwitcherTests(TestCase):
|
|||||||
|
|
||||||
response = self.client.get(es_url)
|
response = self.client.get(es_url)
|
||||||
|
|
||||||
self.assertContains(response, f'href="{en_url}"')
|
self.assertContains(response, f'value="{en_url}"')
|
||||||
|
|
||||||
def test_current_language_is_shown_on_the_trigger_button(self):
|
def test_current_language_is_shown_on_the_trigger_button(self):
|
||||||
response = self.client.get('/es/')
|
response = self.client.get('/es/')
|
||||||
|
|||||||
Reference in New Issue
Block a user