diff --git a/src/pretix/base/__init__.py b/src/pretix/base/__init__.py index d30036e03f..db8b41e7e8 100644 --- a/src/pretix/base/__init__.py +++ b/src/pretix/base/__init__.py @@ -9,7 +9,7 @@ class PretixBaseConfig(AppConfig): from . import exporter # NOQA from . import payment # NOQA from . import exporters # NOQA - from .services import export, mail, tickets, cart, orders, cleanup # NOQA + from .services import export, mail, tickets, cart, orders, cleanup, update_check # NOQA try: from .celery_app import app as celery_app # NOQA diff --git a/src/pretix/base/management/commands/rebuild.py b/src/pretix/base/management/commands/rebuild.py index d04edd4f11..16e0e49c1f 100644 --- a/src/pretix/base/management/commands/rebuild.py +++ b/src/pretix/base/management/commands/rebuild.py @@ -1,6 +1,8 @@ from django.core.management import call_command from django.core.management.base import BaseCommand +from pretix.base.settings import GlobalSettingsObject + class Command(BaseCommand): help = "Rebuild static files and language files" @@ -10,3 +12,7 @@ class Command(BaseCommand): call_command('compilejsi18n', verbosity=1, interactive=False) call_command('collectstatic', verbosity=1, interactive=False) call_command('compress', verbosity=1, interactive=False) + gs = GlobalSettingsObject() + del gs.settings.update_check_last + del gs.settings.update_check_result + del gs.settings.update_check_result_warning diff --git a/src/pretix/base/services/update_check.py b/src/pretix/base/services/update_check.py new file mode 100644 index 0000000000..c43781ebb8 --- /dev/null +++ b/src/pretix/base/services/update_check.py @@ -0,0 +1,125 @@ +import sys +import uuid +from datetime import timedelta + +import requests +from django.dispatch import receiver +from django.utils.timezone import now +from django.utils.translation import ugettext_lazy as _, ugettext_noop +from i18nfield.strings import LazyI18nString + +from pretix import __version__ +from pretix.base.models import Event +from pretix.base.plugins import get_all_plugins +from pretix.base.services.mail import mail +from pretix.base.settings import GlobalSettingsObject +from pretix.base.signals import periodic_task +from pretix.celery_app import app +from pretix.helpers.urls import build_absolute_uri + + +@receiver(signal=periodic_task) +def run_update_check(sender, **kwargs): + gs = GlobalSettingsObject() + if not gs.settings.update_check_perform: + return + + if not gs.settings.update_check_last or now() - gs.settings.update_check_last > timedelta(hours=23): + update_check.apply_async() + + +@app.task +def update_check(): + gs = GlobalSettingsObject() + if not gs.settings.update_check_perform: + return + + if not gs.settings.update_check_id: + gs.settings.set('update_check_id', uuid.uuid4().hex) + + if 'runserver' in sys.argv: + gs.settings.set('update_check_last', now()) + gs.settings.set('update_check_result', { + 'error': 'development' + }) + return + + check_payload = { + 'id': gs.settings.get('update_check_id'), + 'version': __version__, + 'events': { + 'total': Event.objects.count(), + 'live': Event.objects.filter(live=True).count(), + }, + 'plugins': [ + { + 'name': p.module, + 'version': p.version + } for p in get_all_plugins() + ] + } + try: + r = requests.post('https://pretix.eu/.update_check/', json=check_payload) + gs.settings.set('update_check_last', now()) + if r.status_code != 200: + gs.settings.set('update_check_result', { + 'error': 'http_error' + }) + else: + rdata = r.json() + update_available = rdata['version']['updatable'] or any(p['updatable'] for p in rdata['plugins'].values()) + gs.settings.set('update_check_result_warning', update_available) + if update_available and rdata != gs.settings.update_check_result: + send_update_notification_email() + gs.settings.set('update_check_result', rdata) + except requests.RequestException: + gs.settings.set('update_check_last', now()) + gs.settings.set('update_check_result', { + 'error': 'unavailable' + }) + + +def send_update_notification_email(): + gs = GlobalSettingsObject() + if not gs.settings.update_check_email: + return + + mail( + gs.settings.update_check_email, + _('pretix update available'), + LazyI18nString.from_gettext( + ugettext_noop( + 'Hi!\n\nAn update is available for pretix or for one of the plugins you installed in your ' + 'pretix installation. Please click on the following link for more information:\n\n {url} \n\n' + 'You can always find information on the latest updates on the pretix.eu blog:\n\n' + 'https://pretix.eu/about/en/blog/' + '\n\nBest,\n\nyour pretix developers' + ) + ), + { + 'url': build_absolute_uri('control:global.update') + }, + ) + + +def check_result_table(): + gs = GlobalSettingsObject() + res = gs.settings.update_check_result + if not res: + return { + 'error': 'no_result' + } + + if 'error' in res: + return res + + table = [] + table.append(('pretix', __version__, res['version']['latest'], res['version']['updatable'])) + for p in get_all_plugins(): + if p.module in res['plugins']: + pdata = res['plugins'][p.module] + table.append((_('Plugin: %s') % p.name, p.version, pdata['latest'], pdata['updatable'])) + else: + table.append((_('Plugin: %s') % p.name, p.version, '?', False)) + + return table diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 0e2f8661c1..341526e3fe 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -351,6 +351,34 @@ Your {event} team""")) 'frontpage_text': { 'default': '', 'type': LazyI18nString + }, + 'update_check_ack': { + 'default': 'False', + 'type': bool + }, + 'update_check_email': { + 'default': '', + 'type': str + }, + 'update_check_perform': { + 'default': 'True', + 'type': bool + }, + 'update_check_result': { + 'default': None, + 'type': dict + }, + 'update_check_result_warning': { + 'default': 'False', + 'type': bool + }, + 'update_check_last': { + 'default': None, + 'type': datetime + }, + 'update_check_id': { + 'default': None, + 'type': str } } diff --git a/src/pretix/control/context.py b/src/pretix/control/context.py index d7edd15dd6..83027432c1 100644 --- a/src/pretix/control/context.py +++ b/src/pretix/control/context.py @@ -3,6 +3,8 @@ import sys from django.conf import settings from django.core.urlresolvers import Resolver404, get_script_prefix, resolve +from pretix.base.settings import GlobalSettingsObject + from .signals import html_head, nav_event, nav_topbar from .utils.i18n import get_javascript_format, get_moment_locale @@ -53,4 +55,13 @@ def contextprocessor(request): elif 'runserver' in sys.argv: ctx['development_warning'] = True + ctx['warning_update_available'] = False + ctx['warning_update_check_active'] = False + if request.user.is_superuser: + gs = GlobalSettingsObject() + if gs.settings.update_check_result_warning: + ctx['warning_update_available'] = True + if not gs.settings.update_check_ack and 'runserver' not in sys.argv: + ctx['warning_update_check_active'] = True + return ctx diff --git a/src/pretix/control/forms/global_settings.py b/src/pretix/control/forms/global_settings.py index 275d2ec9a5..a5bbbde339 100644 --- a/src/pretix/control/forms/global_settings.py +++ b/src/pretix/control/forms/global_settings.py @@ -1,5 +1,6 @@ from collections import OrderedDict +from django import forms from django.utils.translation import ugettext_lazy as _ from i18nfield.forms import I18nFormField, I18nTextInput @@ -32,3 +33,26 @@ class GlobalSettingsForm(SettingsForm): for key, value in response.items(): # We need to be this explicit, since OrderedDict.update does not retain ordering self.fields[key] = value + + +class UpdateSettingsForm(SettingsForm): + update_check_perform = forms.BooleanField( + required=False, + label=_("Perform update checks"), + help_text=_("During the update check, pretix will report an anonymous, unique installation ID, " + "the current version of pretix and your installed plugins and the number of active and " + "inactive events in your installation to servers operated by the pretix developers. We " + "will only store anonymous data, never any IP adresses and we will not know who you are " + "or where to find your instance. You can disable this behaviour here at any time.") + ) + update_check_email = forms.EmailField( + required=False, + label=_("E-mail notifications"), + help_text=_("We will notify you at this address if we detect that a new update is available. This " + "address will not be transmitted to pretix.eu, the emails will be sent by this server " + "locally.") + ) + + def __init__(self, *args, **kwargs): + self.obj = GlobalSettingsObject() + super().__init__(*args, obj=self.obj, **kwargs) diff --git a/src/pretix/control/templates/pretixcontrol/base.html b/src/pretix/control/templates/pretixcontrol/base.html index c33d1f104b..24eab7c44c 100644 --- a/src/pretix/control/templates/pretixcontrol/base.html +++ b/src/pretix/control/templates/pretixcontrol/base.html @@ -105,6 +105,13 @@ {% endfor %} + {% if warning_update_available %} +