mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Automatic update checks (#434)
* Basic update checks * Fix issues pointed out by @rixx * First test * Add tests * Even more tests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
125
src/pretix/base/services/update_check.py
Normal file
125
src/pretix/base/services/update_check.py
Normal file
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -105,6 +105,13 @@
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if warning_update_available %}
|
||||
<li>
|
||||
<a href="{% url 'control:global.update' %}" class="danger">
|
||||
<i class="fa fa-bell"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{% url 'control:user.settings' %}">
|
||||
<i class="fa fa-user"></i> {{ request.user.get_full_name }}
|
||||
@@ -141,7 +148,8 @@
|
||||
</li>
|
||||
{% if request.user.is_superuser %}
|
||||
<li>
|
||||
<a href="{% url 'control:global-settings' %}" {% if "global-settings" in url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:global.settings' %}"
|
||||
{% if "global.settings" in url_name %}class="active"{% endif %}>
|
||||
<i class="fa fa-wrench fa-fw"></i>
|
||||
{% trans "Global settings" %}
|
||||
</a>
|
||||
@@ -173,6 +181,19 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if warning_update_check_active %}
|
||||
<div class="alert alert-info">
|
||||
<a href="{% url "control:global.update" %}">
|
||||
{% blocktrans trimmed %}
|
||||
Starting with version 1.2.0, pretix automatically checks for updates in the background.
|
||||
During this check, anonymous data is transmitted to servers operated by pretix'
|
||||
developers. Click on this message to find out more, disable this feature or enter your
|
||||
email address to get notified via email if a new update arrives. This message will
|
||||
disappear once you clicked it.
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if debug_warning %}
|
||||
<div class="alert alert-danger">
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% extends "pretixcontrol/global_settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}{% trans "Global settings" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Global settings" %}</h1>
|
||||
{% block inner %}
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}{% trans "Global settings" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Global settings" %}</h1>
|
||||
<ul class="nav nav-pills">
|
||||
<li {% if "global.settings" == url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:global.settings' %}">
|
||||
{% trans "General" %}
|
||||
</a>
|
||||
</li>
|
||||
<li {% if "global.update" == url_name %}class="active"{% endif %}>
|
||||
<a href="{% url 'control:global.update' %}">
|
||||
{% trans "Update check" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% block inner %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,90 @@
|
||||
{% extends "pretixcontrol/global_settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block inner %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Update check results" %}</legend>
|
||||
{% if not gs.settings.update_check_perform %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "Update checks are disabled." %}
|
||||
</div>
|
||||
{% elif not gs.settings.update_check_last %}
|
||||
<div class="alert alert-info">
|
||||
{% trans "No update check has been performed yet since the last update of this installation. Update checks are performed on a daily basis if your cronjob is set up properly." %}
|
||||
</div>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
<button type="submit" name="trigger" value="1" class="btn btn-default">
|
||||
{% trans "Check for updates now" %}
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
{% elif "error" in gs.settings.update_check_result %}
|
||||
<div class="alert alert-danger">
|
||||
{% trans "The last update check was not successful." %}
|
||||
{% if gs.settings.update_check_result.error == "http_error" %}
|
||||
{% trans "The pretix.eu server returned an error code." %}
|
||||
{% elif gs.settings.update_check_result.error == "unavailable" %}
|
||||
{% trans "The pretix.eu server could not be reached." %}
|
||||
{% elif gs.settings.update_check_result.error == "development" %}
|
||||
{% trans "This installation appears to be a development installation." %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
<button type="submit" name="trigger" value="1" class="btn btn-default">
|
||||
{% trans "Check for updates now" %}
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% blocktrans trimmed with date=gs.settings.update_check_last|date:"SHORT_DATETIME_FORMAT" %}
|
||||
Last updated: {{ date }}
|
||||
{% endblocktrans %}
|
||||
<button type="submit" name="trigger" value="1" class="btn btn-default btn-xs">
|
||||
{% trans "Check for updates now" %}
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Component" %}</th>
|
||||
<th>{% trans "Installed version" %}</th>
|
||||
<th>{% trans "Latest version" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in tbl %}
|
||||
<tr class="{% if row.3 %}danger{% elif row.2 == "?" %}warning{% else %}success{% endif %}">
|
||||
<td>{{ row.0 }}</td>
|
||||
<td>{{ row.1 }}</td>
|
||||
<td>{{ row.2 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Update check settings" %}</legend>
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_form form layout='horizontal' %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -14,9 +14,10 @@ urlpatterns = [
|
||||
url(r'^forgot$', auth.Forgot.as_view(), name='auth.forgot'),
|
||||
url(r'^forgot/recover$', auth.Recover.as_view(), name='auth.forgot.recover'),
|
||||
url(r'^$', dashboards.user_index, name='index'),
|
||||
url(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='global-settings'),
|
||||
url(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='global.settings'),
|
||||
url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
|
||||
url(r'^reauth/$', user.ReauthView.as_view(), name='user.reauth'),
|
||||
url(r'^settings$', user.UserSettings.as_view(), name='user.settings'),
|
||||
url(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
|
||||
url(r'^settings/2fa/$', user.User2FAMainView.as_view(), name='user.settings.2fa'),
|
||||
url(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
|
||||
url(r'^settings/2fa/add$', user.User2FADeviceAddView.as_view(), name='user.settings.2fa.add'),
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import reverse
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import FormView
|
||||
|
||||
from pretix.control.forms.global_settings import GlobalSettingsForm
|
||||
from pretix.base.services.update_check import check_result_table, update_check
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.control.forms.global_settings import (
|
||||
GlobalSettingsForm, UpdateSettingsForm,
|
||||
)
|
||||
from pretix.control.permissions import AdministratorPermissionRequiredMixin
|
||||
|
||||
|
||||
@@ -21,4 +25,34 @@ class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView):
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:global-settings')
|
||||
return reverse('control:global.settings')
|
||||
|
||||
|
||||
class UpdateCheckView(AdministratorPermissionRequiredMixin, FormView):
|
||||
template_name = 'pretixcontrol/global_update.html'
|
||||
form_class = UpdateSettingsForm
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'trigger' in request.POST:
|
||||
update_check.apply()
|
||||
return redirect(self.get_success_url())
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes have not been saved, see below for errors.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['gs'] = GlobalSettingsObject()
|
||||
ctx['gs'].settings.set('update_check_ack', True)
|
||||
ctx['tbl'] = check_result_table()
|
||||
return ctx
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:global.update')
|
||||
|
||||
@@ -27,6 +27,10 @@ nav.navbar {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
nav.navbar .danger, nav.navbar .danger:hover, nav.navbar .danger:active {
|
||||
background: $brand-danger !important;
|
||||
}
|
||||
|
||||
.navbar-header .navbar-events {
|
||||
color: white;
|
||||
padding-top: 6px;
|
||||
|
||||
Reference in New Issue
Block a user