forked from CGM_Public/pretix_original
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:
@@ -32,9 +32,9 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import logging
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.utils import translation
|
||||
from django.utils.translation import get_language_info
|
||||
from django_scopes import get_scope
|
||||
@@ -46,12 +46,14 @@ from pretix.helpers.i18n import (
|
||||
)
|
||||
|
||||
from ..base.i18n import get_language_without_region
|
||||
from ..multidomain.urlreverse import eventreverse
|
||||
from .cookies import get_cookie_providers
|
||||
from .signals import (
|
||||
footer_link, global_footer_link, global_html_footer, global_html_head,
|
||||
global_html_page_header, html_footer, html_head, html_page_header,
|
||||
)
|
||||
from .views.cart import cart_session, get_or_create_cart_id
|
||||
from .views.theme import _get_source_cache_key
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -70,7 +72,6 @@ def _default_context(request):
|
||||
return {}
|
||||
|
||||
ctx = {
|
||||
'css_file': None,
|
||||
'DEBUG': settings.DEBUG,
|
||||
}
|
||||
_html_head = []
|
||||
@@ -80,10 +81,22 @@ def _default_context(request):
|
||||
|
||||
if hasattr(request, 'event') and request.event:
|
||||
pretix_settings = request.event.settings
|
||||
|
||||
# This makes sure a new version of the theme is loaded whenever settings or the source files have changed
|
||||
theme_css_version = (f'{_get_source_cache_key()}-'
|
||||
f'{request.organizer.cache.get_or_set("css_version", default=lambda: int(time.time()))}-'
|
||||
f'{request.event.cache.get_or_set("css_version", default=lambda: int(time.time()))}')
|
||||
ctx['css_theme'] = eventreverse(request.event, "presale:event.theme.css") + "?version=" + theme_css_version
|
||||
|
||||
elif hasattr(request, 'organizer') and request.organizer:
|
||||
pretix_settings = request.organizer.settings
|
||||
|
||||
# This makes sure a new version of the theme is loaded whenever settings or the source files have changed
|
||||
theme_css_version = f'{_get_source_cache_key()}-{request.organizer.cache.get_or_set("css_version", default=lambda: int(time.time()))}'
|
||||
ctx['css_theme'] = eventreverse(request.organizer, "presale:organizer.theme.css") + "?version=" + theme_css_version
|
||||
else:
|
||||
pretix_settings = GlobalSettingsObject().settings
|
||||
ctx['css_theme'] = None
|
||||
|
||||
text = pretix_settings.get('footer_text', as_type=LazyI18nString)
|
||||
link = pretix_settings.get('footer_link', as_type=LazyI18nString)
|
||||
@@ -124,9 +137,6 @@ def _default_context(request):
|
||||
for fl in request.event.footer_links.all()
|
||||
], timeout=300)
|
||||
|
||||
if request.event.settings.presale_css_file:
|
||||
ctx['css_file'] = default_storage.url(request.event.settings.presale_css_file)
|
||||
|
||||
ctx['event_logo'] = request.event.settings.get('logo_image', as_type=str, default='')[7:]
|
||||
ctx['event_logo_image_large'] = request.event.settings.logo_image_large
|
||||
ctx['event_logo_show_title'] = request.event.settings.logo_show_title
|
||||
@@ -158,8 +168,6 @@ def _default_context(request):
|
||||
ctx['languages'] = [get_language_info(code) for code in request.organizer.settings.locales]
|
||||
|
||||
if request.resolver_match and hasattr(request, 'organizer'):
|
||||
if request.organizer.settings.presale_css_file and not hasattr(request, 'event'):
|
||||
ctx['css_file'] = default_storage.url(request.organizer.settings.presale_css_file)
|
||||
ctx['organizer_logo'] = request.organizer.settings.get('organizer_logo_image', as_type=str, default='')[7:]
|
||||
ctx['organizer_homepage_text'] = request.organizer.settings.get('organizer_homepage_text', as_type=LazyI18nString)
|
||||
ctx['organizer'] = request.organizer
|
||||
|
||||
61
src/pretix/presale/management/commands/updateassets.py
Normal file
61
src/pretix/presale/management/commands/updateassets.py
Normal file
@@ -0,0 +1,61 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# 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 hashlib
|
||||
|
||||
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.core.management.base import BaseCommand
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.presale.views.widget import generate_widget_js
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Re-generate runtime-generated assets and scripts"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--organizer', action='store', type=str)
|
||||
parser.add_argument('--event', action='store', type=str)
|
||||
|
||||
@scopes_disabled()
|
||||
def handle(self, *args, **options):
|
||||
gs = GlobalSettingsObject()
|
||||
for lc, ll in settings.LANGUAGES:
|
||||
data = generate_widget_js(lc).encode()
|
||||
checksum = hashlib.sha1(data).hexdigest()
|
||||
fname = gs.settings.get('widget_file_{}'.format(lc))
|
||||
if not fname or gs.settings.get('widget_checksum_{}'.format(lc), '') != checksum:
|
||||
newname = default_storage.save(
|
||||
'pub/widget/widget.{}.{}.js'.format(lc, checksum),
|
||||
ContentFile(data)
|
||||
)
|
||||
gs.settings.set('widget_file_{}'.format(lc), 'file://' + newname)
|
||||
gs.settings.set('widget_checksum_{}'.format(lc), checksum)
|
||||
cache.delete('widget_js_data_{}'.format(lc))
|
||||
if fname:
|
||||
if isinstance(fname, File):
|
||||
default_storage.delete(fname.name)
|
||||
else:
|
||||
default_storage.delete(fname)
|
||||
@@ -19,22 +19,10 @@
|
||||
# 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 hashlib
|
||||
|
||||
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.core.management.base import BaseCommand
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import Event_SettingsStore, Organizer_SettingsStore
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.presale.views.widget import generate_widget_js
|
||||
|
||||
from ...style import regenerate_css, regenerate_organizer_css
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Re-generate all custom stylesheets and scripts"
|
||||
@@ -45,38 +33,7 @@ class Command(BaseCommand):
|
||||
|
||||
@scopes_disabled()
|
||||
def handle(self, *args, **options):
|
||||
# Reset compile cache
|
||||
cache.set('sass_compile_prefix', now().isoformat())
|
||||
from .updateassets import Command as UCommand
|
||||
|
||||
ostore = Organizer_SettingsStore.objects.filter(key="presale_css_file")
|
||||
if options.get('organizer'):
|
||||
ostore = ostore.filter(object__slug=options['organizer'])
|
||||
for es in ostore:
|
||||
regenerate_organizer_css.apply_async(args=(es.object_id,), kwargs={'regenerate_events': False})
|
||||
|
||||
estore = Event_SettingsStore.objects.filter(key="presale_css_file").order_by('-object__date_from')
|
||||
if options.get('event'):
|
||||
estore = estore.filter(object__slug=options['event'])
|
||||
if options.get('organizer'):
|
||||
estore = estore.filter(object__organizer__slug=options['event'])
|
||||
for es in estore:
|
||||
regenerate_css.apply_async(args=(es.object_id,))
|
||||
|
||||
gs = GlobalSettingsObject()
|
||||
for lc, ll in settings.LANGUAGES:
|
||||
data = generate_widget_js(lc).encode()
|
||||
checksum = hashlib.sha1(data).hexdigest()
|
||||
fname = gs.settings.get('widget_file_{}'.format(lc))
|
||||
if not fname or gs.settings.get('widget_checksum_{}'.format(lc), '') != checksum:
|
||||
newname = default_storage.save(
|
||||
'pub/widget/widget.{}.{}.js'.format(lc, checksum),
|
||||
ContentFile(data)
|
||||
)
|
||||
gs.settings.set('widget_file_{}'.format(lc), 'file://' + newname)
|
||||
gs.settings.set('widget_checksum_{}'.format(lc), checksum)
|
||||
cache.delete('widget_js_data_{}'.format(lc))
|
||||
if fname:
|
||||
if isinstance(fname, File):
|
||||
default_storage.delete(fname.name)
|
||||
else:
|
||||
default_storage.delete(fname)
|
||||
self.stdout.write(self.style.WARNING("Command 'updatestyles' is deprecated, use 'updateassets' instead."))
|
||||
UCommand().handle(*args, **options)
|
||||
|
||||
@@ -102,33 +102,6 @@ of every page in the frontend. You will get the request as the keyword argument
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
sass_preamble = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``filename``
|
||||
|
||||
This signal allows you to put SASS code at the beginning of the event-specific
|
||||
stylesheet. Keep in mind that this will only be called/rebuilt when the user changes
|
||||
display settings or pretix gets updated. You will get the filename that is being
|
||||
generated (usually "main.scss" or "widget.scss"). This SASS code will be loaded *after*
|
||||
setting of user-defined variables like colors and fonts but *before* pretix' SASS
|
||||
code.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
sass_postamble = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``filename``
|
||||
|
||||
This signal allows you to put SASS code at the end of the event-specific
|
||||
stylesheet. Keep in mind that this will only be called/rebuilt when the user changes
|
||||
display settings or pretix gets updated. You will get the filename that is being
|
||||
generated (usually "main.scss" or "widget.scss"). This SASS code will be loaded *after*
|
||||
all of pretix' SASS code.
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
footer_link = EventPluginSignal()
|
||||
"""
|
||||
Arguments: ``request``
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
<html{% if rtl %} dir="rtl" class="rtl"{% endif %} lang="{{ html_locale }}">
|
||||
<head>
|
||||
<title>{% block thetitle %}{% endblock %}</title>
|
||||
{% if css_file %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}" />
|
||||
{% else %}
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/main.scss" %}"/>
|
||||
{% endcompress %}
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/main.scss" %}"/>
|
||||
{% endcompress %}
|
||||
{% if css_theme %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
|
||||
{% endif %}
|
||||
|
||||
{% include "pretixpresale/fragment_js.html" %}
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% if css_file %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_file }}"/>
|
||||
{% else %}
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/main.scss" %}"/>
|
||||
{% endcompress %}
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/main.scss" %}"/>
|
||||
{% endcompress %}
|
||||
{% if css_theme %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
|
||||
{% endif %}
|
||||
{% include "pretixpresale/fragment_js.html" %}
|
||||
<meta name="referrer" content="origin">
|
||||
|
||||
@@ -176,6 +176,8 @@ event_patterns = [
|
||||
re_path(r'^(?P<subevent>\d+)/widget/product_list$', pretix.presale.views.widget.WidgetAPIProductList.as_view(),
|
||||
name='event.widget.productlist'),
|
||||
|
||||
re_path(r'^theme.css$', pretix.presale.views.theme.theme_css, name='event.theme.css'),
|
||||
|
||||
re_path(r'timemachine/$', pretix.presale.views.event.EventTimeMachine.as_view(), name='event.timemachine'),
|
||||
|
||||
# Account management is done on org level, but we at least need a logout
|
||||
@@ -195,6 +197,8 @@ organizer_patterns = [
|
||||
name='organizer.widget.productlist'),
|
||||
re_path(r'^widget/v1.css$', pretix.presale.views.widget.widget_css, name='organizer.widget.css'),
|
||||
|
||||
re_path(r'^theme.css$', pretix.presale.views.theme.theme_css, name='organizer.theme.css'),
|
||||
|
||||
re_path(r'^account/login/(?P<provider>[0-9]+)/$', pretix.presale.views.customer.SSOLoginView.as_view(), name='organizer.customer.login'),
|
||||
re_path(r'^account/login/(?P<provider>[0-9]+)/return$', pretix.presale.views.customer.SSOLoginReturnView.as_view(), name='organizer.customer.login.return'),
|
||||
re_path(r'^account/login$', pretix.presale.views.customer.LoginView.as_view(), name='organizer.customer.login'),
|
||||
|
||||
@@ -19,9 +19,29 @@
|
||||
# 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 hashlib
|
||||
import time
|
||||
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.http import HttpResponse
|
||||
from django.templatetags.static import static
|
||||
from django.utils.http import http_date
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.views.decorators.http import condition
|
||||
|
||||
from pretix.presale.style import get_theme_vars_css
|
||||
|
||||
# we never change static source without restart, so we can cache this thread-wise
|
||||
_source_cache_key = None
|
||||
|
||||
|
||||
def _get_source_cache_key():
|
||||
global _source_cache_key
|
||||
if not _source_cache_key:
|
||||
with open(finders.find("pretixbase/scss/_theme_variables.scss"), "r") as f:
|
||||
_source_cache_key = hashlib.sha256(f.read().encode()).hexdigest()[:12]
|
||||
return _source_cache_key
|
||||
|
||||
|
||||
@cache_page(3600)
|
||||
@@ -69,3 +89,16 @@ def webmanifest(request):
|
||||
static('pretixbase/img/icons/android-chrome-512x512.png'),
|
||||
), content_type='text/json'
|
||||
)
|
||||
|
||||
|
||||
@gzip_page
|
||||
@condition(etag_func=lambda request, **kwargs: request.GET.get("version"))
|
||||
def theme_css(request, **kwargs):
|
||||
obj = getattr(request, "event", request.organizer)
|
||||
css = get_theme_vars_css(obj, widget=False)
|
||||
resp = HttpResponse(css, content_type="text/css")
|
||||
resp._csp_ignore = True
|
||||
resp["Access-Control-Allow-Origin"] = "*"
|
||||
if "version" in request.GET:
|
||||
resp["Expires"] = http_date(time.time() + 3600 * 24 * 30)
|
||||
return resp
|
||||
|
||||
@@ -23,6 +23,7 @@ import calendar
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime, timedelta
|
||||
from urllib.parse import urljoin
|
||||
@@ -65,6 +66,7 @@ from pretix.helpers.daterange import daterange
|
||||
from pretix.helpers.thumb import get_thumbnail
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.presale.forms.organizer import meta_filtersets
|
||||
from pretix.presale.style import get_theme_vars_css
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
from pretix.presale.views.event import (
|
||||
get_grouped_items, item_group_by_category,
|
||||
@@ -76,14 +78,35 @@ from pretix.presale.views.organizer import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# we never change static source without restart, so we can cache this thread-wise
|
||||
_source_cache_key = None
|
||||
|
||||
|
||||
def _get_source_cache_key():
|
||||
global _source_cache_key
|
||||
checksum = hashlib.sha256()
|
||||
if not _source_cache_key:
|
||||
with open(finders.find("pretixbase/scss/_theme_variables.scss"), "r") as f:
|
||||
checksum.update(f.read().encode())
|
||||
tpl = get_template('pretixpresale/widget_dummy.html')
|
||||
et = html.fromstring(tpl.render({})).xpath('/html/head/link')[0].attrib['href'].replace(settings.STATIC_URL, '')
|
||||
checksum.update(et.encode())
|
||||
_source_cache_key = checksum.hexdigest()[:12]
|
||||
return _source_cache_key
|
||||
|
||||
|
||||
def indent(s):
|
||||
return s.replace('\n', '\n ')
|
||||
|
||||
|
||||
def widget_css_etag(request, **kwargs):
|
||||
o = getattr(request, 'event', request.organizer)
|
||||
return o.settings.presale_widget_css_checksum or o.settings.presale_widget_css_checksum
|
||||
# This makes sure a new version of the theme is loaded whenever settings or the source files have changed
|
||||
if hasattr(request, 'event'):
|
||||
return (f'{_get_source_cache_key()}-'
|
||||
f'{request.organizer.cache.get_or_set("css_version", default=lambda: int(time.time()))}-'
|
||||
f'{request.event.cache.get_or_set("css_version", default=lambda: int(time.time()))}')
|
||||
else:
|
||||
return f'{_get_source_cache_key()}-{request.organizer.cache.get_or_set("css_version", default=lambda: int(time.time()))}'
|
||||
|
||||
|
||||
def widget_js_etag(request, lang, **kwargs):
|
||||
@@ -96,18 +119,16 @@ def widget_js_etag(request, lang, **kwargs):
|
||||
@cache_page(60)
|
||||
def widget_css(request, **kwargs):
|
||||
o = getattr(request, 'event', request.organizer)
|
||||
if o.settings.presale_widget_css_file:
|
||||
try:
|
||||
resp = FileResponse(default_storage.open(o.settings.presale_widget_css_file),
|
||||
content_type='text/css')
|
||||
resp['Access-Control-Allow-Origin'] = '*'
|
||||
return resp
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
tpl = get_template('pretixpresale/widget_dummy.html')
|
||||
et = html.fromstring(tpl.render({})).xpath('/html/head/link')[0].attrib['href'].replace(settings.STATIC_URL, '')
|
||||
f = finders.find(et)
|
||||
resp = FileResponse(open(f, 'rb'), content_type='text/css')
|
||||
with open(finders.find(et), 'r') as f:
|
||||
widget_css = f.read()
|
||||
|
||||
theme_css = get_theme_vars_css(o, widget=True)
|
||||
css = theme_css + widget_css
|
||||
|
||||
resp = FileResponse(css, content_type='text/css')
|
||||
resp._csp_ignore = True
|
||||
resp['Access-Control-Allow-Origin'] = '*'
|
||||
return resp
|
||||
|
||||
Reference in New Issue
Block a user