feat: added SPA support and speed up tests
This commit is contained in:
@@ -18,10 +18,27 @@ from django.conf import settings
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.static import serve
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
# Sin prefijo de idioma (fuera de i18n_patterns, ver más abajo) ni bajo
|
||||
# STATIC_URL: sw.js necesita servirse desde la raíz para que su ámbito
|
||||
# (qué rutas puede interceptar un Service Worker) cubra el origen
|
||||
# entero, no solo /static/ o /es/ (ver crochet/templates/sw.js). Se
|
||||
# renderiza como plantilla (no un archivo estático de whitenoise) para
|
||||
# que {% static %}/{% url %} resuelvan ahí mismo las rutas (con hash)
|
||||
# vigentes en cada respuesta.
|
||||
path(
|
||||
'sw.js', name='service_worker',
|
||||
view=TemplateView.as_view(template_name='sw.js', content_type='application/javascript'),
|
||||
),
|
||||
path(
|
||||
'manifest.json', name='manifest',
|
||||
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')),
|
||||
]
|
||||
|
||||
# i18n_patterns antepone el prefijo de idioma (/es/..., /en/...) a cada URL,
|
||||
|
||||
+15
@@ -6,6 +6,21 @@ from django.core.management import call_command
|
||||
from django.test import override_settings
|
||||
|
||||
|
||||
@pytest.fixture(scope='session', autouse=True)
|
||||
def fast_password_hasher():
|
||||
"""PBKDF2PasswordHasher (por defecto, ver AUTH_PASSWORD_VALIDATORS) es
|
||||
deliberadamente lento -es justo lo que lo hace seguro en producción-,
|
||||
pero eso mismo hace bastante caro cada create_user()/check_password()/
|
||||
login de los tests: con cientos de ellos (y los de rate limiting, que
|
||||
encima repiten el login 10-11 veces seguidas) esto es la mayor parte
|
||||
del tiempo de toda la suite. MD5PasswordHasher no vale para producción,
|
||||
pero aquí no protege nada real -las contraseñas son literales fijos
|
||||
como 'a-very-uncommon-pw-1'-, así que no hay ninguna pérdida de
|
||||
seguridad en usarlo solo para tests."""
|
||||
with override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.MD5PasswordHasher']):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_ratelimit_cache():
|
||||
"""django-ratelimit (ver LOGIN_RATE/REGISTER_RATE/PASSWORD_RESET_RATE en
|
||||
|
||||
@@ -2,7 +2,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: crochet\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-16 11:16+0000\n"
|
||||
"POT-Creation-Date: 2026-09-16 14:05+0000\n"
|
||||
"Language: en\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -46,7 +46,7 @@ msgstr ""
|
||||
"made in a row from here."
|
||||
|
||||
#: crochet/templates/403.html:24 crochet/templates/404.html:19
|
||||
#: crochet/templates/500.html:64
|
||||
#: crochet/templates/500.html:65
|
||||
#: crochet/templates/crochet/cookie_policy.html:54
|
||||
msgid "Volver al inicio"
|
||||
msgstr "Back to home"
|
||||
@@ -63,11 +63,11 @@ msgstr "The page you're looking for doesn't exist or has moved."
|
||||
msgid "Error del servidor"
|
||||
msgstr "Server error"
|
||||
|
||||
#: crochet/templates/500.html:62
|
||||
#: crochet/templates/500.html:63
|
||||
msgid "Vaya, algo ha ido mal"
|
||||
msgstr "Oops, something went wrong"
|
||||
|
||||
#: crochet/templates/500.html:63
|
||||
#: crochet/templates/500.html:64
|
||||
msgid "Ha ocurrido un error inesperado. Prueba a recargar en un momento."
|
||||
msgstr "An unexpected error occurred. Try reloading in a moment."
|
||||
|
||||
@@ -225,9 +225,8 @@ msgid "Actualizado el %(date)s"
|
||||
msgstr "Updated on %(date)s"
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:44
|
||||
#: crochet/templates/crochet/pattern.html:156
|
||||
msgid "Vista previa"
|
||||
msgstr "Preview"
|
||||
msgid "Editar"
|
||||
msgstr "Edit"
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:48
|
||||
#: crochet/templates/crochet/pattern.html:46
|
||||
@@ -240,7 +239,7 @@ msgstr "You don't have any patterns yet."
|
||||
|
||||
#: crochet/templates/crochet/account_settings.html:4
|
||||
#: crochet/templates/crochet/account_settings.html:18
|
||||
#: crochet/templates/crochet/base.html:62
|
||||
#: crochet/templates/crochet/base.html:71
|
||||
msgid "Ajustes de la cuenta"
|
||||
msgstr "Account settings"
|
||||
|
||||
@@ -274,19 +273,19 @@ msgstr ""
|
||||
msgid "Esto borrará tu cuenta y todos tus patrones de forma permanente."
|
||||
msgstr "This will permanently delete your account and all your patterns."
|
||||
|
||||
#: crochet/templates/crochet/base.html:37
|
||||
#: crochet/templates/crochet/base.html:46
|
||||
msgid "Cambiar tema"
|
||||
msgstr "Change theme"
|
||||
|
||||
#: crochet/templates/crochet/base.html:49
|
||||
#: crochet/templates/crochet/base.html:58
|
||||
msgid "Cambiar idioma"
|
||||
msgstr "Change language"
|
||||
|
||||
#: crochet/templates/crochet/base.html:66
|
||||
#: crochet/templates/crochet/base.html:75
|
||||
msgid "Cerrar sesión"
|
||||
msgstr "Log out"
|
||||
|
||||
#: crochet/templates/crochet/base.html:72
|
||||
#: crochet/templates/crochet/base.html:81
|
||||
#: crochet/templates/crochet/home.html:25
|
||||
#: crochet/templates/crochet/password_reset_complete.html:18
|
||||
#: crochet/templates/registration/login.html:4
|
||||
@@ -758,6 +757,10 @@ msgstr "Landscape"
|
||||
msgid "Secciones"
|
||||
msgstr "Sections"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:156
|
||||
msgid "Vista previa"
|
||||
msgstr "Preview"
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:166
|
||||
msgid "Idioma del patrón"
|
||||
msgstr "Pattern language"
|
||||
@@ -906,6 +909,22 @@ msgstr ""
|
||||
msgid "Confirmar"
|
||||
msgstr "Confirm"
|
||||
|
||||
#: crochet/templates/offline.html:20 crochet/templates/offline.html:70
|
||||
msgid "Sin conexión"
|
||||
msgstr "No connection"
|
||||
|
||||
#: crochet/templates/offline.html:71
|
||||
msgid ""
|
||||
"No se ha podido cargar esta página porque no tienes conexión a internet. "
|
||||
"Comprueba tu conexión e inténtalo de nuevo."
|
||||
msgstr ""
|
||||
"This page couldn't be loaded because you're not connected to the internet. "
|
||||
"Check your connection and try again."
|
||||
|
||||
#: crochet/templates/offline.html:72
|
||||
msgid "Reintentar"
|
||||
msgstr "Retry"
|
||||
|
||||
#: crochet/templates/registration/login.html:17
|
||||
msgid "Usuario o contraseña incorrectos."
|
||||
msgstr "Incorrect username or password."
|
||||
@@ -1109,8 +1128,5 @@ msgstr "Your account has been deleted successfully."
|
||||
#~ msgid "Idioma"
|
||||
#~ msgstr "Language"
|
||||
|
||||
#~ msgid "Editar"
|
||||
#~ msgstr "Edit"
|
||||
|
||||
#~ msgid "Eliminar patrón"
|
||||
#~ msgstr "Delete pattern"
|
||||
|
||||
@@ -2,7 +2,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: crochet\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-16 11:16+0000\n"
|
||||
"POT-Creation-Date: 2026-09-16 14:05+0000\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -44,7 +44,7 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/403.html:24 crochet/templates/404.html:19
|
||||
#: crochet/templates/500.html:64
|
||||
#: crochet/templates/500.html:65
|
||||
#: crochet/templates/crochet/cookie_policy.html:54
|
||||
msgid "Volver al inicio"
|
||||
msgstr ""
|
||||
@@ -61,11 +61,11 @@ msgstr ""
|
||||
msgid "Error del servidor"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/500.html:62
|
||||
#: crochet/templates/500.html:63
|
||||
msgid "Vaya, algo ha ido mal"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/500.html:63
|
||||
#: crochet/templates/500.html:64
|
||||
msgid "Ha ocurrido un error inesperado. Prueba a recargar en un momento."
|
||||
msgstr ""
|
||||
|
||||
@@ -215,8 +215,7 @@ msgid "Actualizado el %(date)s"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:44
|
||||
#: crochet/templates/crochet/pattern.html:156
|
||||
msgid "Vista previa"
|
||||
msgid "Editar"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_home.html:48
|
||||
@@ -230,7 +229,7 @@ msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/account_settings.html:4
|
||||
#: crochet/templates/crochet/account_settings.html:18
|
||||
#: crochet/templates/crochet/base.html:62
|
||||
#: crochet/templates/crochet/base.html:71
|
||||
msgid "Ajustes de la cuenta"
|
||||
msgstr ""
|
||||
|
||||
@@ -262,19 +261,19 @@ msgstr ""
|
||||
msgid "Esto borrará tu cuenta y todos tus patrones de forma permanente."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/base.html:37
|
||||
#: crochet/templates/crochet/base.html:46
|
||||
msgid "Cambiar tema"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/base.html:49
|
||||
#: crochet/templates/crochet/base.html:58
|
||||
msgid "Cambiar idioma"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/base.html:66
|
||||
#: crochet/templates/crochet/base.html:75
|
||||
msgid "Cerrar sesión"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/base.html:72
|
||||
#: crochet/templates/crochet/base.html:81
|
||||
#: crochet/templates/crochet/home.html:25
|
||||
#: crochet/templates/crochet/password_reset_complete.html:18
|
||||
#: crochet/templates/registration/login.html:4
|
||||
@@ -718,6 +717,10 @@ msgstr ""
|
||||
msgid "Secciones"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:156
|
||||
msgid "Vista previa"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/crochet/pattern.html:166
|
||||
msgid "Idioma del patrón"
|
||||
msgstr ""
|
||||
@@ -860,6 +863,20 @@ msgstr ""
|
||||
msgid "Confirmar"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/offline.html:20 crochet/templates/offline.html:70
|
||||
msgid "Sin conexión"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/offline.html:71
|
||||
msgid ""
|
||||
"No se ha podido cargar esta página porque no tienes conexión a internet. "
|
||||
"Comprueba tu conexión e inténtalo de nuevo."
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/offline.html:72
|
||||
msgid "Reintentar"
|
||||
msgstr ""
|
||||
|
||||
#: crochet/templates/registration/login.html:17
|
||||
msgid "Usuario o contraseña incorrectos."
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 490 B |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
@@ -0,0 +1,18 @@
|
||||
// Registra el Service Worker de la PWA (ver crochet/templates/sw.js) para
|
||||
// que la app sea instalable y tenga soporte offline básico. La ruta va a
|
||||
// pelo ('/sw.js', no bajo /static/ ni con prefijo de idioma) porque tiene
|
||||
// que coincidir exactamente con dónde lo sirve config/urls.py: el ámbito
|
||||
// de un Service Worker es la carpeta de su propia URL y las de debajo, así
|
||||
// que solo sirviéndolo (y registrándolo) desde la raíz cubre el origen
|
||||
// entero en vez de quedarse limitado a /static/ o /es/.
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js').catch(() => {
|
||||
// Sin conexión, en un navegador sin soporte real pese a exponer el
|
||||
// objeto, o serviceWorker.register() con documento no seguro (http
|
||||
// sin ser localhost): la app sigue funcionando con normalidad, solo
|
||||
// sin lo que aporta la PWA (instalación, caché offline), así que no
|
||||
// hace falta avisar a quien la usa.
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Error del servidor' %}</title>
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
<link rel="icon" href="{% static 'img/favicon.ico' %}">
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
|
||||
@@ -18,6 +18,15 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Crochet{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
<!-- PWA: instalable (manifest + iconos) y con soporte offline básico
|
||||
(ver crochet/templates/sw.js, registrado al final de este documento).
|
||||
pattern_detail.html (vista de solo lectura/PDF) hereda esto también:
|
||||
no hace daño -no vuelve a registrar nada, el navegador ya lo tiene
|
||||
activo- e instalar la PWA desde ahí sigue llevando al mismo shell. -->
|
||||
<link rel="manifest" href="{% url 'manifest' %}">
|
||||
<meta name="theme-color" content="#f1641e">
|
||||
<link rel="icon" href="{% static 'img/favicon.ico' %}">
|
||||
<link rel="apple-touch-icon" href="{% static 'img/icon-192.png' %}">
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
|
||||
@@ -75,6 +84,7 @@
|
||||
</nav>
|
||||
{% endblock %}
|
||||
<script defer src="{% static 'js/theme-toggle.js' %}"></script>
|
||||
<script defer src="{% static 'js/sw-register.js' %}"></script>
|
||||
<!-- Mensajes de una sola vez tras una redirección (ver messages.success
|
||||
en AccountDeleteView): no hay ninguna página a la que volver con el
|
||||
resultado de la acción (la sesión ya no existe), así que se muestran
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{% load static %}{
|
||||
"name": "Crochet",
|
||||
"short_name": "Crochet",
|
||||
"description": "Diseña patrones de crochet con un editor visual y expórtalos a PDF.",
|
||||
"lang": "es",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#fffbf5",
|
||||
"theme_color": "#f1641e",
|
||||
"icons": [
|
||||
{
|
||||
"src": "{% static 'img/icon-192.png' %}",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "{% static 'img/icon-512.png' %}",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{% load i18n static %}
|
||||
<!DOCTYPE html>
|
||||
<!-- Autónoma, sin extender crochet/base.html: la sirve el Service Worker
|
||||
(ver sw.js) como respuesta de reserva cuando falla una navegación sin
|
||||
conexión, así que no puede depender de nada que a su vez necesite red
|
||||
(el CDN de daisyUI/Tailwind, Google Fonts) ni de context processors -
|
||||
mismo motivo que 500.html, que ya sigue este mismo patrón. -->
|
||||
<html lang="es" data-theme="crochet">
|
||||
|
||||
<head>
|
||||
<script>
|
||||
try {
|
||||
if (localStorage.getItem('theme') === 'crochet-dark') {
|
||||
document.documentElement.dataset.theme = 'crochet-dark';
|
||||
}
|
||||
} catch {}
|
||||
</script>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% trans 'Sin conexión' %}</title>
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
<link rel="icon" href="{% static 'img/favicon.ico' %}">
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.box {
|
||||
max-width: 24rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background: var(--brand-bg-soft);
|
||||
border: 1px solid var(--brand-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.box h1 {
|
||||
margin: 0 0 .5rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.box p {
|
||||
margin: 0 0 1rem;
|
||||
opacity: .7;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.box button {
|
||||
border: none;
|
||||
background-color: var(--brand-primary);
|
||||
color: var(--brand-primary-content);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
padding: .5rem 1.5rem;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-size: .9rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="box">
|
||||
<h1>{% trans 'Sin conexión' %}</h1>
|
||||
<p>{% trans 'No se ha podido cargar esta página porque no tienes conexión a internet. Comprueba tu conexión e inténtalo de nuevo.' %}</p>
|
||||
<button type="button" onclick="location.reload()">{% trans 'Reintentar' %}</button>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,96 @@
|
||||
{% load static %}
|
||||
// Service Worker de la PWA: soporte offline básico, no un cache completo de
|
||||
// la aplicación (los patrones son contenido dinámico, guardado en base de
|
||||
// datos, y no se cachean aquí). Se limita a dos cosas:
|
||||
// 1. Cachear (cache-first) los estáticos propios de la app shell
|
||||
// (main.css, theme.css, iconos), para que carguen al instante y sigan
|
||||
// disponibles sin conexión.
|
||||
// 2. Si una navegación (clic en un enlace, recargar la página) falla por
|
||||
// no haber red, responder con la página de "sin conexión" (ver
|
||||
// crochet/templates/offline.html) en vez del error genérico del
|
||||
// navegador.
|
||||
// Todo lo demás (peticiones a /media/, fetch de la propia app a
|
||||
// pattern_save/pattern_image_upload, el CDN de daisyUI/Tailwind/Google
|
||||
// Fonts) se deja pasar sin interceptar: ni se cachea ni se sirve un
|
||||
// fallback, el navegador hace lo de siempre.
|
||||
//
|
||||
// Esta plantilla la sirve el TemplateView "service_worker" (ver
|
||||
// config/urls.py) con content_type=application/javascript, no whitenoise:
|
||||
// así el tag "static" de Django resuelve aquí el nombre real (con hash) de
|
||||
// cada archivo en el momento de servir esta respuesta, en vez de tener que
|
||||
// mantenerlo a mano.
|
||||
//
|
||||
// CACHE_NAME es fijo a propósito (no calculado a partir del hash de los
|
||||
// estáticos): el propio hash cambia el contenido de este archivo cada vez
|
||||
// que cambia main.css/theme.css, y los navegadores comparan el Service
|
||||
// Worker byte a byte en cada visita para detectar actualizaciones -eso ya
|
||||
// basta para que se reinstale solo y su "install" vuelva a poblar el cache
|
||||
// con las URLs (con hash) nuevas-. Subir el número a mano de vez en cuando
|
||||
// (aquí y en ningún otro sitio) sirve solo para forzar que "activate"
|
||||
// limpie del todo cachés de una versión muy antigua, ver más abajo.
|
||||
const CACHE_NAME = 'crochet-shell-v1';
|
||||
|
||||
const OFFLINE_URL = '{% url "offline" %}';
|
||||
|
||||
const APP_SHELL = [
|
||||
OFFLINE_URL,
|
||||
'{% static "css/main.css" %}',
|
||||
'{% static "css/theme.css" %}',
|
||||
'{% static "img/icon-192.png" %}',
|
||||
'{% static "img/icon-512.png" %}',
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then((cache) => cache.addAll(APP_SHELL))
|
||||
// skipWaiting: sin esto, un Service Worker nuevo se queda en
|
||||
// "waiting" hasta que se cierran todas las pestañas con el antiguo
|
||||
// todavía activo, así que la próxima visita seguiría sirviendo el
|
||||
// shell (y el offline.html) de la versión vieja.
|
||||
.then(() => self.skipWaiting()),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))))
|
||||
.then(() => self.clients.claim()),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
|
||||
// mode 'navigate': una petición de navegación de verdad (clic en un
|
||||
// enlace, recargar, escribir la URL), no un fetch/XHR de main.js/
|
||||
// storage.js ni la petición de una imagen/script. Es justo lo único
|
||||
// para lo que tiene sentido el fallback de offline.html.
|
||||
if (request.mode === 'navigate') {
|
||||
event.respondWith(
|
||||
fetch(request).catch(() => caches.match(OFFLINE_URL)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
// Mismo origen y bajo STATIC_URL únicamente: ni /media/ (portadas/
|
||||
// imágenes de patrón, que pueden pesar bastante y no forman parte del
|
||||
// "shell") ni el CDN externo de daisyUI/Tailwind/Google Fonts, que ya
|
||||
// tiene su propio cache HTTP de navegador y no conviene duplicar aquí.
|
||||
if (url.origin === self.location.origin && url.pathname.startsWith('/static/')) {
|
||||
event.respondWith(
|
||||
caches.match(request).then((cached) => {
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
return fetch(request).then((response) => {
|
||||
const responseCopy = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(request, responseCopy));
|
||||
return response;
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class ServiceWorkerViewTests(TestCase):
|
||||
def test_served_at_the_domain_root(self):
|
||||
response = self.client.get('/sw.js')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'application/javascript')
|
||||
|
||||
def test_references_the_offline_page_and_own_static_assets(self):
|
||||
response = self.client.get('/sw.js')
|
||||
|
||||
content = response.content.decode()
|
||||
self.assertIn("OFFLINE_URL = '/offline/'", content)
|
||||
|
||||
self.assertIn('/static/css/main.', content)
|
||||
self.assertIn('/static/css/theme.', content)
|
||||
self.assertIn('/static/img/icon-192.', content)
|
||||
self.assertIn('/static/img/icon-512.', content)
|
||||
|
||||
|
||||
class ManifestViewTests(TestCase):
|
||||
def test_served_with_the_manifest_content_type(self):
|
||||
response = self.client.get('/manifest.json')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'application/manifest+json')
|
||||
|
||||
def test_references_both_icon_sizes(self):
|
||||
response = self.client.get('/manifest.json')
|
||||
|
||||
self.assertContains(response, '/static/img/icon-192.')
|
||||
self.assertContains(response, '/static/img/icon-512.')
|
||||
|
||||
|
||||
class OfflineViewTests(TestCase):
|
||||
def test_renders_the_offline_page(self):
|
||||
response = self.client.get(reverse('offline'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Sin conexión')
|
||||
Reference in New Issue
Block a user