diff --git a/Dockerfile b/Dockerfile index 189453505..403e18caf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,4 +46,4 @@ RUN make production EXPOSE 80 ENTRYPOINT ["pretix"] -CMD ["web"] +CMD ["all"] diff --git a/deployment/docker/pretix.bash b/deployment/docker/pretix.bash index 08d24bd31..b2edfc5cf 100644 --- a/deployment/docker/pretix.bash +++ b/deployment/docker/pretix.bash @@ -40,5 +40,9 @@ if [ "$1" == "shell" ]; then exec python3 -m pretix shell fi -echo "Specify argument: all|cron|webworker|taskworker|shell" +if [ "$1" == "upgrade" ]; then + exec python3 -m pretix updatestyles +fi + +echo "Specify argument: all|cron|webworker|taskworker|shell|upgrade" exit 1 diff --git a/doc/admin/installation/docker_smallscale.rst b/doc/admin/installation/docker_smallscale.rst index 99efa7b2f..3dc34c868 100644 --- a/doc/admin/installation/docker_smallscale.rst +++ b/doc/admin/installation/docker_smallscale.rst @@ -224,6 +224,7 @@ Updates are fairly simple, but require at least a short downtime:: # docker pull pretix/standalone # systemctl restart pretix.service + # docker exec -it pretix.service pretix upgrade Restarting the service can take a few seconds, especially if the update requires changes to the database. diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 938d53b1a..98af10fbc 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -205,6 +205,18 @@ Your {event} team""")) 'default': 'False', 'type': bool }, + 'primary_color': { + 'default': '#8E44B3', + 'type': str + }, + 'presale_css_file': { + 'default': None, + 'type': str + }, + 'presale_css_checksum': { + 'default': None, + 'type': str + }, } diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 2f9edd2fa..9a86907a7 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -1,6 +1,7 @@ from django import forms from django.conf import settings from django.core.exceptions import ValidationError +from django.core.validators import RegexValidator from django.utils.translation import ugettext_lazy as _ from pytz import common_timezones @@ -349,6 +350,17 @@ class MailSettingsForm(SettingsForm): raise ValidationError(_('You can activate either SSL or STARTTLS security, but not both at the same time.')) +class DisplaySettingsForm(SettingsForm): + primary_color = forms.CharField( + label=_("Primary color"), + required=False, + validators=[ + RegexValidator(regex='^#[0-9a-fA-F]{6}$', + message=_('Please enter the hexadecimal code of a color, e.g. #990000.')) + ] + ) + + class TicketSettingsForm(SettingsForm): ticket_download = forms.BooleanField( label=_("Use feature"), diff --git a/src/pretix/control/templates/pretixcontrol/event/display.html b/src/pretix/control/templates/pretixcontrol/event/display.html new file mode 100644 index 000000000..f10e84b1d --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/event/display.html @@ -0,0 +1,18 @@ +{% extends "pretixcontrol/event/settings_base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block inside %} +
+ {% csrf_token %} + {% bootstrap_form_errors form %} +
+ {% trans "Display settings" %} + {% bootstrap_field form.primary_color layout="horizontal" %} +
+
+ +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/event/settings_base.html b/src/pretix/control/templates/pretixcontrol/event/settings_base.html index 7de9661fa..842c1a8ba 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings_base.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings_base.html @@ -21,6 +21,11 @@ {% trans "Plugins" %} +
  • + + {% trans "Display" %} + +
  • {% trans "Tickets" %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 9f34dae00..b6001ea1a 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -29,6 +29,7 @@ urlpatterns = [ url(r'^settings/tickets$', event.TicketSettings.as_view(), name='event.settings.tickets'), url(r'^settings/email$', event.MailSettings.as_view(), name='event.settings.mail'), url(r'^settings/invoice$', event.InvoiceSettings.as_view(), name='event.settings.invoice'), + url(r'^settings/display', event.DisplaySettings.as_view(), name='event.settings.display'), url(r'^items/$', item.ItemList.as_view(), name='event.items'), url(r'^items/add$', item.ItemCreate.as_view(), name='event.items.add'), url(r'^items/(?P\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'), diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index fbad102d5..3bed64137 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -21,10 +21,12 @@ from pretix.base.signals import ( register_payment_providers, register_ticket_outputs, ) from pretix.control.forms.event import ( - EventSettingsForm, EventUpdateForm, InvoiceSettingsForm, MailSettingsForm, - PaymentSettingsForm, ProviderForm, TicketSettingsForm, + DisplaySettingsForm, EventSettingsForm, EventUpdateForm, + InvoiceSettingsForm, MailSettingsForm, PaymentSettingsForm, ProviderForm, + TicketSettingsForm, ) from pretix.control.permissions import EventPermissionRequiredMixin +from pretix.presale.style import regenerate_css from . import UpdateView @@ -241,7 +243,7 @@ class EventSettingsFormView(EventPermissionRequiredMixin, FormView): self.request.event.log_action( 'pretix.event.settings', user=self.request.user, data={ k: form.cleaned_data.get(k) for k in form.changed_data - } + } ) messages.success(self.request, _('Your changes have been saved.')) return redirect(self.get_success_url()) @@ -262,6 +264,38 @@ class InvoiceSettings(EventSettingsFormView): }) +class DisplaySettings(EventSettingsFormView): + model = Event + form_class = DisplaySettingsForm + template_name = 'pretixcontrol/event/display.html' + permission = 'can_change_settings' + + def get_success_url(self) -> str: + return reverse('control:event.settings.display', kwargs={ + 'organizer': self.request.event.organizer.slug, + 'event': self.request.event.slug + }) + + @transaction.atomic() + def post(self, request, *args, **kwargs): + form = self.get_form() + if form.is_valid(): + form.save() + if form.has_changed(): + self.request.event.log_action( + 'pretix.event.settings', user=self.request.user, data={ + k: form.cleaned_data.get(k) for k in form.changed_data + } + ) + regenerate_css(self.request.event.pk) + messages.success(self.request, _('Your changes have been saved. Please note that it can ' + 'take a short period of time until your changes become ' + 'active.')) + return redirect(self.get_success_url()) + else: + return self.get(request) + + class MailSettings(EventSettingsFormView): model = Event form_class = MailSettingsForm diff --git a/src/pretix/presale/__init__.py b/src/pretix/presale/__init__.py index 23e13910d..ad5bce31f 100644 --- a/src/pretix/presale/__init__.py +++ b/src/pretix/presale/__init__.py @@ -5,4 +5,8 @@ class PretixPresaleConfig(AppConfig): name = 'pretix.presale' label = 'pretixpresale' + def ready(self): + from . import style # noqa + + default_app_config = 'pretix.presale.PretixPresaleConfig' diff --git a/src/pretix/presale/context.py b/src/pretix/presale/context.py index e2b4a4322..414d1551d 100644 --- a/src/pretix/presale/context.py +++ b/src/pretix/presale/context.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.core.files.storage import default_storage from django.core.urlresolvers import Resolver404, resolve from .signals import footer_link, html_head @@ -16,6 +17,7 @@ def contextprocessor(request): return {} ctx = { + 'css_file': None } _html_head = [] _footer = [] @@ -24,6 +26,10 @@ def contextprocessor(request): _html_head.append(response) for receiver, response in footer_link.send(request.event, request=request): _footer.append(response) + + if request.event.settings.presale_css_file: + ctx['css_file'] = default_storage.url(request.event.settings.presale_css_file) + ctx['html_head'] = "".join(_html_head) ctx['footer'] = _footer ctx['site_url'] = settings.SITE_URL diff --git a/src/pretix/presale/management/__init__.py b/src/pretix/presale/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/pretix/presale/management/commands/__init__.py b/src/pretix/presale/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/pretix/presale/management/commands/updatestyles.py b/src/pretix/presale/management/commands/updatestyles.py new file mode 100644 index 000000000..81c3edff0 --- /dev/null +++ b/src/pretix/presale/management/commands/updatestyles.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand + +from pretix.base.models import EventSetting + +from ...style import regenerate_css + + +class Command(BaseCommand): + help = "Re-generate all custom stylesheets" + + def handle(self, *args, **options): + for es in EventSetting.objects.filter(key="presale_css_file"): + regenerate_css(es.object_id) diff --git a/src/pretix/presale/style.py b/src/pretix/presale/style.py new file mode 100644 index 000000000..e0bc12471 --- /dev/null +++ b/src/pretix/presale/style.py @@ -0,0 +1,47 @@ +import hashlib +import logging +import os + +import django_libsass +import sass +from django.conf import settings +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage + +from pretix.base.models import Event + +logger = logging.getLogger('pretix.presale.style') + + +def regenerate_css(event_id: int): + event = Event.objects.select_related('organizer').get(pk=event_id) + sassdir = os.path.join(settings.STATIC_ROOT, 'pretixpresale/scss') + + sassrules = [ + '$brand-primary: {};'.format(event.settings.get('primary_color')), + '@import "main.scss";', + ] + + css = sass.compile( + string="\n".join(sassrules), + include_paths=[sassdir], output_style='compressed', + custom_functions=django_libsass.CUSTOM_FUNCTIONS + ) + checksum = hashlib.sha1(css.encode('utf-8')).hexdigest() + fname = '{}/{}/presale.{}.css'.format( + event.organizer.slug, event.slug, checksum[:16] + ) + + if event.settings.get('presale_css_checksum', '') != checksum: + newname = default_storage.save(fname, ContentFile(css)) + event.settings.set('presale_css_file', newname) + event.settings.set('presale_css_checksum', checksum) + + +if settings.HAS_CELERY: + from pretix.celery import app + + regenerate_css_task = app.task(regenerate_css) + + def regenerate_css(*args, **kwargs): + regenerate_css_task.apply_async(args=args, kwargs=kwargs) diff --git a/src/pretix/presale/templates/pretixpresale/base.html b/src/pretix/presale/templates/pretixpresale/base.html index 24f5571cb..8da1b9902 100644 --- a/src/pretix/presale/templates/pretixpresale/base.html +++ b/src/pretix/presale/templates/pretixpresale/base.html @@ -7,8 +7,14 @@ {% block thetitle %}{% endblock %} {% compress css %} - {% endcompress %} + {% if css_file %} + + {% else %} + {% compress css %} + + {% endcompress %} + {% endif %} {% compress js %} diff --git a/src/requirements/production.txt b/src/requirements/production.txt index 925887fef..5141e9025 100644 --- a/src/requirements/production.txt +++ b/src/requirements/production.txt @@ -9,6 +9,7 @@ reportlab>=3.2,<3.3 git+https://github.com/pretix/PyPDF2.git@pretix#egg=PyPDF2 easy-thumbnails>=2.2,<3 django-libsass +libsass # Deployment / static file compilation requirements BeautifulSoup4