diff --git a/Dockerfile b/Dockerfile
index 189453505b..403e18caf8 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 08d24bd31b..b2edfc5cf7 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 99efa7b2fa..3dc34c868a 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 938d53b1a6..98af10fbcf 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 2f9edd2fa6..9a86907a74 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 0000000000..f10e84b1d6
--- /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 %}
+
+{% endblock %}
diff --git a/src/pretix/control/templates/pretixcontrol/event/settings_base.html b/src/pretix/control/templates/pretixcontrol/event/settings_base.html
index 7de9661fa8..842c1a8bac 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 9f34dae003..b6001ea1ab 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 fbad102d57..3bed641374 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 23e13910d7..ad5bce31f7 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 e2b4a4322a..414d1551d8 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 0000000000..e69de29bb2
diff --git a/src/pretix/presale/management/commands/__init__.py b/src/pretix/presale/management/commands/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/pretix/presale/management/commands/updatestyles.py b/src/pretix/presale/management/commands/updatestyles.py
new file mode 100644
index 0000000000..81c3edff06
--- /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 0000000000..e0bc124717
--- /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 24f5571cbb..8da1b9902d 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 925887fefc..5141e9025b 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