Replace SCSS compilation with CSS variables (#4191)

* Replace SCSS compilation with CSS variables

* Update tests

* Update src/pretix/presale/style.py

Co-authored-by: Mira <weller@rami.io>

* Update src/pretix/presale/context.py

Co-authored-by: Mira <weller@rami.io>

* Update src/pretix/presale/views/widget.py

Co-authored-by: Mira <weller@rami.io>

* Update src/pretix/presale/context.py

Co-authored-by: Mira <weller@rami.io>

* Update src/pretix/static/pretixbase/scss/_variables.scss

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Last minor changes

* Rename file

---------

Co-authored-by: Mira <weller@rami.io>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2024-06-25 13:01:20 +02:00
committed by GitHub
parent 7672e6274d
commit f0a06cd9fe
72 changed files with 867 additions and 1600 deletions

View File

@@ -19,186 +19,23 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import gzip
import hashlib
import logging
import os
from urllib.parse import urljoin, urlsplit
import django_libsass
import sass
from compressor.filters.cssmin import CSSMinFilter
from django.conf import settings
from django.core.cache import cache
from django.core.files.base import ContentFile, File
from django.core.files.storage import default_storage
from django.contrib.staticfiles import finders
from django.dispatch import Signal
from django.templatetags.static import static as _static
from django.utils.timezone import now
from django_scopes import scope
from pretix.base.models import Event, Event_SettingsStore, Organizer
from pretix.base.services.tasks import (
TransactionAwareProfiledEventTask, TransactionAwareTask,
)
from pretix.base.models import Event, Organizer
from pretix.base.signals import EventPluginSignal
from pretix.celery_app import app
from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain,
)
from pretix.presale.signals import sass_postamble, sass_preamble
logger = logging.getLogger('pretix.presale.style')
affected_keys = ['primary_font', 'primary_color', 'theme_color_success', 'theme_color_danger', 'theme_color_background', 'theme_round_borders']
def compile_scss(object, file="main.scss", fonts=True):
sassdir = os.path.join(settings.STATIC_ROOT, 'pretixpresale/scss')
def static(path):
sp = _static(path)
if not settings.MEDIA_URL.startswith("/") and sp.startswith("/"):
if isinstance(object, Event):
domain = get_event_domain(object, fallback=True)
else:
domain = get_organizer_domain(object)
if domain:
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
domain = '%s:%d' % (domain, siteurlsplit.port)
sp = urljoin('%s://%s' % (siteurlsplit.scheme, domain), sp)
else:
sp = urljoin(settings.SITE_URL, sp)
return '"{}"'.format(sp)
sassrules = []
if object.settings.get('primary_color'):
sassrules.append('$brand-primary: {};'.format(object.settings.get('primary_color')))
if object.settings.get('theme_color_success'):
sassrules.append('$brand-success: {};'.format(object.settings.get('theme_color_success')))
if object.settings.get('theme_color_danger'):
sassrules.append('$brand-danger: {};'.format(object.settings.get('theme_color_danger')))
if object.settings.get('theme_color_background'):
sassrules.append('$body-bg: {};'.format(object.settings.get('theme_color_background')))
if not object.settings.get('theme_round_borders'):
sassrules.append('$border-radius-base: 0;')
sassrules.append('$border-radius-large: 0;')
sassrules.append('$border-radius-small: 0;')
font = object.settings.get('primary_font')
if font != 'Open Sans' and fonts:
sassrules.append(get_font_stylesheet(font, event=object if isinstance(object, Event) else None))
sassrules.append(
'$font-family-sans-serif: "{}", "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif '
'!default'.format(
font
))
if isinstance(object, Event):
for recv, resp in sass_preamble.send(object, filename=file):
sassrules.append(resp)
sassrules.append('@import "{}";'.format(file))
if isinstance(object, Event):
for recv, resp in sass_postamble.send(object, filename=file):
sassrules.append(resp)
sasssrc = "\n".join(sassrules)
srcchecksum = hashlib.sha1(sasssrc.encode('utf-8')).hexdigest()
cp = cache.get_or_set('sass_compile_prefix', now().isoformat())
css = cache.get('sass_compile_{}_{}'.format(cp, srcchecksum))
if css:
if isinstance(css, bytes) and css[0:2] == b'\x1f\x8b':
css = gzip.decompress(css).decode()
else:
cf = dict(django_libsass.CUSTOM_FUNCTIONS)
cf['static'] = static
css = sass.compile(
string=sasssrc,
include_paths=[sassdir], output_style='nested',
custom_functions=cf
)
cssf = CSSMinFilter(css)
css = cssf.output()
cache.set('sass_compile_{}_{}'.format(cp, srcchecksum), gzip.compress(css.encode()), 600)
checksum = hashlib.sha1(css.encode('utf-8')).hexdigest()
return css, checksum
def delete_old_file(fname):
if fname:
if isinstance(fname, File):
default_storage.delete(fname.name)
else:
default_storage.delete(fname)
@app.task(base=TransactionAwareProfiledEventTask)
def regenerate_css(event):
settings = event.settings._cache() # ignore organizer settings
# main.scss
css, checksum = compile_scss(event)
fname = 'pub/{}/{}/presale.{}.css'.format(event.organizer.slug, event.slug, checksum[:16])
if settings.get('presale_css_checksum', '') != checksum:
old_fname = settings.get('presale_css_file')
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
event.settings.set('presale_css_file', newname)
event.settings.set('presale_css_checksum', checksum)
if old_fname and old_fname != newname and f'/{event.slug}/' in old_fname:
delete_old_file(old_fname)
# widget.scss
css, checksum = compile_scss(event, file='widget.scss', fonts=False)
fname = 'pub/{}/{}/widget.{}.css'.format(event.organizer.slug, event.slug, checksum[:16])
if settings.get('presale_widget_css_checksum', '') != checksum:
old_fname = settings.get('presale_widget_css_file')
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
event.settings.set('presale_widget_css_file', newname)
event.settings.set('presale_widget_css_checksum', checksum)
if old_fname and old_fname != newname and f'/{event.slug}/' in old_fname:
delete_old_file(old_fname)
@app.task(base=TransactionAwareTask)
def regenerate_organizer_css(organizer_id: int, regenerate_events=True):
organizer = Organizer.objects.get(pk=organizer_id)
with scope(organizer=organizer):
# main.scss
css, checksum = compile_scss(organizer)
fname = 'pub/{}/presale.{}.css'.format(organizer.slug, checksum[:16])
if organizer.settings.get('presale_css_checksum', '') != checksum:
old_fname = organizer.settings.get('presale_css_file')
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
organizer.settings.set('presale_css_file', newname)
organizer.settings.set('presale_css_checksum', checksum)
if old_fname != newname:
delete_old_file(old_fname)
# widget.scss
css, checksum = compile_scss(organizer, file='widget.scss', fonts=False)
fname = 'pub/{}/widget.{}.css'.format(organizer.slug, checksum[:16])
if organizer.settings.get('presale_widget_css_checksum', '') != checksum:
old_fname = organizer.settings.get('presale_widget_css_file')
newname = default_storage.save(fname, ContentFile(css.encode('utf-8')))
organizer.settings.set('presale_widget_css_file', newname)
organizer.settings.set('presale_widget_css_checksum', checksum)
if old_fname != newname:
delete_old_file(old_fname)
if regenerate_events:
non_inherited_events = set(Event_SettingsStore.objects.filter(
object__organizer=organizer, key__in=affected_keys
).values_list('object_id', flat=True))
for event in organizer.events.all():
if event.pk not in non_inherited_events:
regenerate_css.apply_async(args=(event.pk,))
register_fonts = Signal()
@@ -289,7 +126,25 @@ def get_fonts(event: Event = None, pdf_support_required=False):
return f
def get_font_stylesheet(font_name, event: Event = None):
def get_font_stylesheet(font_name, organizer: Organizer = None, event: Event = None, absolute=True):
def static(path):
sp = _static(path)
if sp.startswith("/") and absolute:
if event:
domain = get_event_domain(event, fallback=True)
elif organizer:
domain = get_organizer_domain(organizer)
else:
domain = None
if domain:
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
domain = '%s:%d' % (domain, siteurlsplit.port)
sp = urljoin('%s://%s' % (siteurlsplit.scheme, domain), sp)
else:
sp = urljoin(settings.SITE_URL, sp)
return sp
stylesheet = []
font = get_fonts(event)[font_name]
for sty, formats in font.items():
@@ -312,8 +167,55 @@ def get_font_stylesheet(font_name, event: Event = None):
if formats[f].startswith('https'):
srcs.append(f"url('{formats[f]}') format('{f}')")
else:
srcs.append(f"url(static('{formats[f]}')) format('{f}')")
srcs.append(f"url('{static(formats[f])}') format('{f}')")
stylesheet.append("src: {};".format(", ".join(srcs)))
stylesheet.append("font-display: swap;")
stylesheet.append("}")
return "\n".join(stylesheet)
def get_theme_vars_css(obj, widget=False):
sassrules = []
if obj.settings.get("primary_color"):
sassrules.append("$in-brand-primary: {};".format(obj.settings.get("primary_color")))
if obj.settings.get("theme_color_success"):
sassrules.append("$in-brand-success: {};".format(obj.settings.get("theme_color_success")))
if obj.settings.get("theme_color_danger"):
sassrules.append("$in-brand-danger: {};".format(obj.settings.get("theme_color_danger")))
if obj.settings.get("theme_color_background"):
sassrules.append("$in-body-bg: {};".format(obj.settings.get("theme_color_background")))
if not obj.settings.get("theme_round_borders"):
sassrules.append("$in-border-radius-base: 0;")
sassrules.append("$in-border-radius-large: 0;")
sassrules.append("$in-border-radius-small: 0;")
font = obj.settings.get("primary_font")
if font != "Open Sans" and not widget:
sassrules.append(get_font_stylesheet(
font,
event=obj if isinstance(obj, Event) else None,
organizer=obj.organizer if isinstance(obj, Event) else obj,
absolute=False,
))
sassrules.append(
'$in-font-family-sans-serif: "{}", "Open Sans", "OpenSans", "Helvetica Neue", Helvetica, Arial, sans-serif'.format(
font
)
)
if widget:
sassrules.append("$widget: true;")
with open(finders.find("pretixbase/scss/_theme_variables.scss"), "r") as f:
source_scss = f.read()
sassrules.append(source_scss)
sassdir = os.path.join(settings.STATIC_ROOT, "pretixbase/scss")
sassrule = "\n".join(sassrules)
if not sassrule.strip():
return ""
css = sass.compile(
string=sassrule,
include_paths=[sassdir]
)
return css