Switch to offline compressed assets
This commit is contained in:
parent
3c64795f7a
commit
0fc99c2a0c
9 changed files with 117 additions and 24 deletions
37
articles/compressor.py
Normal file
37
articles/compressor.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class DummyArticleWithCode:
|
||||||
|
has_code = True
|
||||||
|
|
||||||
|
|
||||||
|
class DummyArticleNoCode:
|
||||||
|
has_code = False
|
||||||
|
|
||||||
|
|
||||||
|
class DummyNonAuthenticatedUser:
|
||||||
|
is_authenticated = False
|
||||||
|
|
||||||
|
|
||||||
|
class DummyAuthenticatedUser:
|
||||||
|
is_authenticated = True
|
||||||
|
|
||||||
|
|
||||||
|
def offline_context():
|
||||||
|
article_possibilities = [None, DummyArticleWithCode(), DummyArticleNoCode()]
|
||||||
|
user_possibilities = [DummyAuthenticatedUser(), DummyNonAuthenticatedUser()]
|
||||||
|
goatcounter_possibilities = [None, settings.GOATCOUNTER_DOMAIN]
|
||||||
|
all_possibilities = [
|
||||||
|
article_possibilities,
|
||||||
|
user_possibilities,
|
||||||
|
goatcounter_possibilities,
|
||||||
|
]
|
||||||
|
for _tuple in itertools.product(*all_possibilities):
|
||||||
|
yield {
|
||||||
|
"STATIC_URL": settings.STATIC_URL,
|
||||||
|
"article": _tuple[0],
|
||||||
|
"user": _tuple[1],
|
||||||
|
"goatcounter_domain": _tuple[2],
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
import random
|
import random
|
||||||
import uuid
|
import uuid
|
||||||
from functools import cached_property, reduce
|
from functools import cached_property
|
||||||
|
|
||||||
|
import rcssmin
|
||||||
import readtime
|
import readtime
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
@ -121,6 +122,10 @@ class Article(models.Model):
|
||||||
filter(None, map(lambda k: k.strip().lower(), self.keywords.split(",")))
|
filter(None, map(lambda k: k.strip().lower(), self.keywords.split(",")))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def get_minified_custom_css(self):
|
||||||
|
return rcssmin.cssmin(self.custom_css)
|
||||||
|
|
||||||
def get_admin_url(self):
|
def get_admin_url(self):
|
||||||
content_type = ContentType.objects.get_for_model(self.__class__)
|
content_type = ContentType.objects.get_for_model(self.__class__)
|
||||||
return reverse(
|
return reverse(
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
{% extends 'articles/base.html' %}
|
{% extends 'articles/base.html' %}
|
||||||
|
|
||||||
{% block append_css %}
|
{% block append_css %}
|
||||||
<style>
|
<style>{{ article.get_minified_custom_css }}</style>
|
||||||
{{ article.custom_css }}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{{ article.title }} | {% endblock %}
|
{% block title %}{{ article.title }} | {% endblock %}
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
{% extends 'articles/base.html' %}
|
{% extends 'articles/base.html' %}
|
||||||
|
|
||||||
{% block append_css %}
|
{% block append_css %}
|
||||||
<style>
|
<style>{{ article.get_minified_custom_css }}</style>
|
||||||
{{ article.custom_css }}
|
<style>.pagination{display:flex;justify-content:space-between}</style>
|
||||||
</style>
|
|
||||||
<style>
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{% endblock %}
|
{% block title %}{% endblock %}
|
||||||
|
|
|
@ -22,15 +22,10 @@
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<link rel="stylesheet" href="{% static "authenticated.css" %}">
|
<link rel="stylesheet" href="{% static "authenticated.css" %}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% block append_css %}
|
|
||||||
{% endblock %}
|
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
|
|
||||||
{% compress js inline %}
|
{% block append_css %}
|
||||||
{% if user.is_authenticated %}
|
{% endblock %}
|
||||||
<script src="{% static 'edit-keymap.js' %}" async></script>
|
|
||||||
{% endif %}
|
|
||||||
{% endcompress %}
|
|
||||||
|
|
||||||
{% include "articles/snippets/favicon.html" %}
|
{% include "articles/snippets/favicon.html" %}
|
||||||
</head>
|
</head>
|
||||||
|
@ -60,7 +55,14 @@
|
||||||
for ongoing builds <a href="{{ blog_pipelines_url }}">here</a>.
|
for ongoing builds <a href="{{ blog_pipelines_url }}">here</a>.
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{% include "articles/snippets/analytics.html" %}
|
{% include "articles/snippets/analytics.html" %}
|
||||||
|
{% compress js inline %}
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<script src="{% static 'edit-keymap.js' %}" async defer></script>
|
||||||
|
{% endif %}
|
||||||
|
{% endcompress %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.core.management import call_command
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from articles.models import Article, User
|
from articles.models import Article, User
|
||||||
|
@ -43,3 +44,14 @@ def unpublished_article(author: User) -> Article:
|
||||||
content="## some draft article markdown\n\n[a draft article link](https://article.com)",
|
content="## some draft article markdown\n\n[a draft article link](https://article.com)",
|
||||||
draft_key=uuid.uuid4(),
|
draft_key=uuid.uuid4(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def enable_compressor(settings):
|
||||||
|
settings.COMPRESS_ENABLED = True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True, scope="session")
|
||||||
|
def collect_static():
|
||||||
|
call_command("collectstatic", "--no-input", "--clear")
|
||||||
|
call_command("compress", "--force")
|
||||||
|
|
|
@ -39,3 +39,41 @@ def test_save_article_doesnt_change_existing_slug(published_article: Article):
|
||||||
published_article.title = "This is a brand new title"
|
published_article.title = "This is a brand new title"
|
||||||
published_article.save()
|
published_article.save()
|
||||||
assert published_article.slug == original_slug
|
assert published_article.slug == original_slug
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_empty_custom_css_minified(published_article):
|
||||||
|
published_article.custom_css = ""
|
||||||
|
assert published_article.get_minified_custom_css == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_simple_custom_css_minified(published_article):
|
||||||
|
published_article.custom_css = ".cls {\n background-color: red;\n}"
|
||||||
|
assert published_article.get_minified_custom_css == ".cls{background-color:red}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_larger_custom_css_minified(published_article):
|
||||||
|
published_article.custom_css = """\
|
||||||
|
.profile {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile img {
|
||||||
|
max-width: 200px;
|
||||||
|
min-width: 100px;
|
||||||
|
max-height: 200px;
|
||||||
|
min-height: 100px;
|
||||||
|
border-radius: 10%;
|
||||||
|
padding: 1rem;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 0;
|
||||||
|
padding: 0;
|
||||||
|
}"""
|
||||||
|
assert (
|
||||||
|
published_article.get_minified_custom_css
|
||||||
|
== ".profile{display:flex;justify-content:space-evenly;flex-wrap:wrap}.profile img{max-width:200px;min-width:100px;max-height:200px;min-height:100px;border-radius:10%;padding:1rem;flex-shrink:1;flex-grow:0;padding:0}"
|
||||||
|
)
|
||||||
|
|
|
@ -117,6 +117,13 @@ if MEMCACHED_LOCATION:
|
||||||
"LOCATION": MEMCACHED_LOCATION,
|
"LOCATION": MEMCACHED_LOCATION,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
CACHES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
|
||||||
|
"LOCATION": "cache",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
|
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
|
||||||
|
@ -203,10 +210,7 @@ GOATCOUNTER_DOMAIN = os.getenv("GOATCOUNTER_DOMAIN")
|
||||||
|
|
||||||
LOGIN_URL = "admin:login"
|
LOGIN_URL = "admin:login"
|
||||||
|
|
||||||
# ASSETS_AUTO_BUILD = DEBUG
|
# COMPRESS_ENABLED = True # Enable this if you want to force compression during dev
|
||||||
# ASSETS_DEBUG = False
|
|
||||||
|
|
||||||
COMPRESS_ENABLED = True
|
|
||||||
COMPRESS_FILTERS = {
|
COMPRESS_FILTERS = {
|
||||||
"css": [
|
"css": [
|
||||||
"compressor.filters.css_default.CssAbsoluteFilter",
|
"compressor.filters.css_default.CssAbsoluteFilter",
|
||||||
|
@ -218,3 +222,6 @@ COMPRESS_FILTERS = {
|
||||||
}
|
}
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
COMPRESS_DEBUG_TOGGLE = "nocompress"
|
COMPRESS_DEBUG_TOGGLE = "nocompress"
|
||||||
|
|
||||||
|
COMPRESS_OFFLINE = True
|
||||||
|
COMPRESS_OFFLINE_CONTEXT = "articles.compressor.offline_context"
|
||||||
|
|
|
@ -2,4 +2,5 @@
|
||||||
set -eux
|
set -eux
|
||||||
yes yes | python manage.py migrate
|
yes yes | python manage.py migrate
|
||||||
python manage.py collectstatic --noinput --clear
|
python manage.py collectstatic --noinput --clear
|
||||||
|
python manage.py compress
|
||||||
gunicorn blog.wsgi -b 0.0.0.0:8000 --log-file -
|
gunicorn blog.wsgi -b 0.0.0.0:8000 --log-file -
|
||||||
|
|
Reference in a new issue