Widget: add versioning support and add v2 with improved a11y-support (#5136)

* Add support for versioning widget.js

* add versionable css

* add version deprecation + redirect

* use dynamic template_path instead of dynamic css_path

* remove dummy code from widget.v1.scss

* fix typo

* [A11y] fix input border & focus style (#5149)

* [A11y] fix input border & focus style

* Fix double semi-colon

* [A11y] make collapse-indicator a button (#5150)

* Fix source order for cart-exists-message (#5152)

* [A11y] underline links (#5151)

* [A11y] Move modal-dialogs to HTMLDialogElement (#5147)

* [A11y] move widget/iframe to html-dialog

* make lightbox a dialog

* move error-alert to dialog

* re-add crossorigin

* fix esc-handling and move animation to icon to enable focusing the button

* fix code-style issues

* block canceling loading iframe

* Escape/cancel blocking fix for Chrome

* add round focus-outline when dialog is loading

* Widget v2: change voucher-link to hash-based link (#5161)

* Fix variants toggle-button being submit-button

* Widget v2: make single-item-select button and always show custom-spinners (#5165)

* Widget v2: make single-item-select=button default

* remove native-spinners and single_item_select

* Stop suggesting old parameter

---------

Co-authored-by: Raphael Michel <michel@rami.io>

* Widget v2: add filter button to events metadata-filter (#5162)

* Widget v2: do not underline events in list and calendar (#5163)

* Fix checkbox button missing border radius (#5158)

* Widget v2: turn add-to-cart-button into resume-button if cart-exists and no items selected (#5160)

* Widget v2: make cart-alert live=polite

* Add resume-button if cart-exists and no items selected

* fix error handling with new-tab and later returning to old window

* Fix cart-message button being full height

* fix amount_selected recalc

* Fix broken v-model

* fix merge

* Widget v2: Remove link from variation-product title (#5159)

* Remove link from variation-product, focus associated input

* open variations onclick on product-title

* clickable elements should be focussable and interactive, so better remove click-handler on product-title

* Widget v2: Fix calendar events color contrast (#5164)

* Widget v2: Fix calendar events color contrast

* fix status-bubbles in list-view

* fix color in mobile

* add striped-background to calendar and week

* improve display of calendar for super small screens

* Fix meta-filter legend not being screen-reader accessible

* update version_default to 2

Co-authored-by: Raphael Michel <michel@rami.io>

---------

Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
Richard Schreiber
2025-05-28 15:02:39 +02:00
committed by GitHub
parent e46e689f01
commit 92f7456eca
11 changed files with 4130 additions and 500 deletions

View File

@@ -38,8 +38,10 @@ from django.core.files.base import ContentFile, File
from django.core.files.storage import default_storage
from django.db.models import Q
from django.http import FileResponse, Http404, HttpResponse, JsonResponse
from django.shortcuts import redirect
from django.template import Context, Engine
from django.template.loader import get_template
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.timezone import now
from django.utils.translation import get_language, gettext, pgettext
@@ -81,15 +83,22 @@ logger = logging.getLogger(__name__)
# we never change static source without restart, so we can cache this thread-wise
_source_cache_key = None
version_min = 1
version_max = 2
version_default = 2 # used for output in widget-embed-code
def _get_source_cache_key():
def _get_source_cache_key(version):
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, '')
template_path = 'pretixpresale/widget_dummy.html' if version == version_max else 'pretixpresale/widget_dummy.v{}.html'.format(version)
tpl = get_template(template_path)
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
@@ -99,29 +108,39 @@ def indent(s):
return s.replace('\n', '\n ')
def widget_css_etag(request, **kwargs):
def widget_css_etag(request, version, **kwargs):
# 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()}-'
return (f'{_get_source_cache_key(version)}-'
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()))}'
return f'{_get_source_cache_key(version)}-{request.organizer.cache.get_or_set("css_version", default=lambda: int(time.time()))}'
def widget_js_etag(request, lang, **kwargs):
def widget_js_etag(request, version, lang, **kwargs):
gs = GlobalSettingsObject()
return gs.settings.get('widget_checksum_{}'.format(lang))
return gs.settings.get('widget_checksum_{}_{}'.format(version, lang))
@gzip_page
@condition(etag_func=widget_css_etag)
@cache_page(60)
def widget_css(request, **kwargs):
def widget_css(request, version, **kwargs):
if version > version_max:
raise Http404()
if version < version_min:
return redirect(reverse('presale:event.widget.css' if hasattr(request, 'event') else 'organizer.widget.css', kwargs={
'version': version_min,
'organizer': request.organizer.slug,
'event': request.event.slug if hasattr(request, 'event') else None,
}))
o = getattr(request, 'event', request.organizer)
tpl = get_template('pretixpresale/widget_dummy.html')
et = html.fromstring(tpl.render({})).xpath('/html/head/link')[0].attrib['href'].replace(settings.STATIC_URL, '')
template_path = 'pretixpresale/widget_dummy.html' if version == version_max else 'pretixpresale/widget_dummy.v{}.html'.format(version)
tpl = get_template(template_path)
et = html.fromstring(tpl.render()).xpath('/html/head/link')[0].attrib['href'].replace(settings.STATIC_URL, '')
with open(finders.find(et), 'r') as f:
widget_css = f.read()
@@ -134,7 +153,7 @@ def widget_css(request, **kwargs):
return resp
def generate_widget_js(lang):
def generate_widget_js(version, lang):
code = []
with language(lang):
# Provide isolation
@@ -169,7 +188,7 @@ def generate_widget_js(lang):
'vuejs/vue.js' if settings.DEBUG else 'vuejs/vue.min.js',
'pretixpresale/js/widget/docready.js',
'pretixpresale/js/widget/floatformat.js',
'pretixpresale/js/widget/widget.js',
'pretixpresale/js/widget/widget.js' if version == version_max else 'pretixpresale/js/widget/widget.v{}.js'.format(version),
]
for fname in files:
f = finders.find(fname)
@@ -188,11 +207,17 @@ def generate_widget_js(lang):
@gzip_page
@condition(etag_func=widget_js_etag)
def widget_js(request, lang, **kwargs):
if lang not in [lc for lc, ll in settings.LANGUAGES]:
def widget_js(request, version, lang, **kwargs):
if version > version_max or lang not in [lc for lc, ll in settings.LANGUAGES]:
raise Http404()
cached_js = cache.get('widget_js_data_{}'.format(lang))
if version < version_min:
return redirect(reverse('presale:widget.js', kwargs={
'version': version_min,
'lang': lang,
}))
cached_js = cache.get('widget_js_data_{}_{}'.format(version, lang))
if cached_js and not settings.DEBUG:
resp = HttpResponse(cached_js, content_type='text/javascript')
resp._csp_ignore = True
@@ -200,7 +225,7 @@ def widget_js(request, lang, **kwargs):
return resp
gs = GlobalSettingsObject()
fname = gs.settings.get('widget_file_{}'.format(lang))
fname = gs.settings.get('widget_file_{}_{}'.format(version, lang))
resp = None
if fname and not settings.DEBUG:
if isinstance(fname, File):
@@ -208,21 +233,21 @@ def widget_js(request, lang, **kwargs):
try:
data = default_storage.open(fname).read()
resp = HttpResponse(data, content_type='text/javascript')
cache.set('widget_js_data_{}'.format(lang), data, 3600 * 4)
cache.set('widget_js_data_{}_{}'.format(version, lang), data, 3600 * 4)
except:
logger.exception('Failed to open widget.js')
if not resp:
data = generate_widget_js(lang).encode()
data = generate_widget_js(version, lang).encode()
checksum = hashlib.sha1(data).hexdigest()
if not settings.DEBUG:
newname = default_storage.save(
'widget/widget.{}.{}.js'.format(lang, checksum),
'widget/widget.{}.{}.{}.js'.format(version, lang, checksum),
ContentFile(data)
)
gs.settings.set('widget_file_{}'.format(lang), 'file://' + newname)
gs.settings.set('widget_checksum_{}'.format(lang), checksum)
cache.set('widget_js_data_{}'.format(lang), data, 3600 * 4)
gs.settings.set('widget_file_{}_{}'.format(version, lang), 'file://' + newname)
gs.settings.set('widget_checksum_{}_{}'.format(version, lang), checksum)
cache.set('widget_js_data_{}_{}'.format(version, lang), data, 3600 * 4)
resp = HttpResponse(data, content_type='text/javascript')
resp._csp_ignore = True
resp['Access-Control-Allow-Origin'] = '*'