New feature: check for new versions.

fix #551
This commit is contained in:
Antoine Nguyen
2014-07-26 15:09:02 +02:00
parent 39f2386414
commit cf84644fab
18 changed files with 293 additions and 83 deletions
+17 -3
View File
@@ -736,16 +736,30 @@ this user is disabled).
TopNotifications
================
Let extensions add custom content into the top bar.
Lets extensions subscribe to the global notification service (located
inside the top bar).
*Callback prototype*::
def callback(user): pass
def callback(user, include_all): pass
* ``user`` is a ``User`` instance corresponding to the currently
logged in user
* ``include_all`` is a boolean indicating if empty notifications must
be included into the result or not
Callbacks listening to this event must return a list of string.
Callbacks listening to this event must return a list of dictionary,
each dictionary containing at least the following entries::
{"id": "<notification entry ID>",
"url": "<associated URL>",
"text": "<text to display>"}
If your notification needs a counter, you can specify it by adding the
two following entries in the dictionary:
{"counter": <associated counter>,
"level": "<info|success|warning|error>"}
UserLogin
=========
+25 -3
View File
@@ -1,7 +1,12 @@
from django.utils.translation import ugettext_lazy
import os
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.core.utils import new_version_available
from modoboa.lib import parameters, events
base_events = [
BASE_EVENTS = [
"CanCreate",
"AccountCreated",
@@ -51,7 +56,7 @@ def load_core_settings():
parameters.register(GeneralParametersForm, ugettext_lazy("General"))
parameters.register(UserSettings, ugettext_lazy("General"))
events.declare(base_events)
events.declare(BASE_EVENTS)
@events.observe("ExtDisabled")
@@ -63,3 +68,20 @@ def unset_default_topredirection(extension):
topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION")
if topredirection == extension.name:
parameters.save_admin("DEFAULT_TOP_REDIRECTION", "core")
@events.observe("TopNotifications")
def check_for_new_version(user, include_all):
"""
Check if a new version of Modoboa is available.
"""
if not user.is_superuser:
return []
if new_version_available() is None:
return [{"id": "newversionavailable"}] if include_all else []
return [{
"id": "newversionavailable",
"url": reverse("admin_index") + "#info/",
"text": _("New Modoboa version available"),
"level": "info",
}]
+13
View File
@@ -171,6 +171,19 @@ class GeneralParametersForm(parameters.AdminParametersForm):
sep3 = SeparatorField(label=ugettext_lazy("Miscellaneous"))
top_notifications_check_interval = forms.IntegerField(
label=_("Top notifications check interval"),
initial=30,
help_text=_("Interval between two top notification checks")
)
check_new_versions = YesNoField(
label=ugettext_lazy("Check for new versions"),
initial="yes",
help_text=ugettext_lazy(
"Automatically checks if newer versions are available")
)
log_maximum_age = forms.IntegerField(
label=ugettext_lazy("Maximum log record age"),
initial=365,
@@ -103,6 +103,18 @@ TEMPLATE_LOADERS = (
# 'django.template.loaders.eggs.Loader',
)
TEMPLATE_CONTEXT_PROCESSORS = (
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
'django.core.context_processors.static',
'django.core.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'modoboa.core.context_processors.top_notifications',
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
@@ -174,6 +186,8 @@ SOUTH_TESTS_MIGRATE = False
#DOVECOT_LOOKUP_PATH = ('/path/to/dovecot', )
MODOBOA_API_URL = 'https://api.modoboa.org/1/'
AUTHENTICATION_BACKENDS = (
'modoboa.lib.authbackends.SimpleBackend',
)
+18
View File
@@ -0,0 +1,18 @@
"""
Custom context processors.
"""
from modoboa.lib import events, parameters
def top_notifications(request):
"""
A context processor to include top notifications.
"""
if request.user.is_anonymous():
return {}
return {
"notifications_check_interval":
int(parameters.get_admin("TOP_NOTIFICATIONS_CHECK_INTERVAL")) * 1000,
"top_notifications": events.raiseQueryEvent(
"TopNotifications", request.user, False)
}
@@ -1,5 +1,13 @@
{% load i18n core_tags lib_tags %}
<h2>{% trans "Information" %} <small>{% trans "Various information about Modoboa" %}</small></h2><hr>
{% if new_version %}
<div class="alert alert-block alert-success">
<h4>{% trans "An update is available!" %}</h4>
{% blocktrans with version=new_version.version changelog_url=new_version.changelog_url %}
Modoboa <strong>{{ version }}</strong> is available for download. The list of all changes is available <a href="{{ changelog_url }}" target="_blank">here</a>.
{% endblocktrans %}
</div>
{% endif %}
<div class="alert alert-info">Modoboa <strong>{% get_version %}</strong> {% trans "is currently in use." %}</div>
<h3>{% trans "Active users" %}</h3>
-6
View File
@@ -211,12 +211,6 @@ def load_optionalmenu(user):
)
@register.simple_tag
def load_notifications(user):
content = events.raiseQueryEvent("TopNotifications", user)
return "".join(content)
@register.simple_tag
def display_messages(msgs):
text = ""
+8 -3
View File
@@ -1,4 +1,4 @@
from django.conf.urls import patterns
from django.conf.urls import patterns, url
urlpatterns = patterns(
@@ -7,13 +7,18 @@ urlpatterns = patterns(
(r'^accounts/login/$', 'modoboa.core.views.auth.dologin'),
(r'^accounts/logout/$', 'modoboa.core.views.auth.dologout'),
(r'^core/$', 'modoboa.core.views.admin.viewsettings'),
url(r'^core/$', 'modoboa.core.views.admin.viewsettings',
name="admin_index"),
(r'^core/parameters/$', 'modoboa.core.views.admin.viewparameters'),
(r'^core/parameters/save/$', 'modoboa.core.views.admin.saveparameters'),
(r'^core/extensions/$', 'modoboa.core.views.admin.viewextensions'),
(r'^core/extensions/save/$', 'modoboa.core.views.admin.saveextensions'),
(r'^core/info/$', 'modoboa.core.views.admin.information'),
url(r'^core/info/$', 'modoboa.core.views.admin.information',
name="information"),
(r'^core/logs/$', 'modoboa.core.views.admin.logs'),
url(r'^core/top_notifications/check/$',
'modoboa.core.views.admin.check_top_notifications',
name='check_top_notifications'),
(r'^user/$', 'modoboa.core.views.user.index'),
(r'^user/preferences/$', 'modoboa.core.views.user.preferences'),
+32
View File
@@ -0,0 +1,32 @@
"""
Utility functions.
"""
import os
import pkg_resources
import requests
from django.conf import settings
from versionfield.constants import DEFAULT_NUMBER_BITS
from versionfield.version import Version
from modoboa.lib import parameters
def new_version_available():
"""Check if a new version of Modoboa is available."""
if parameters.get_admin("CHECK_NEW_VERSIONS") == "no":
return None
local_version = pkg_resources.get_distribution("modoboa").version
url = os.path.join(settings.MODOBOA_API_URL, "current_version/")
resp = requests.get(url, params={
"client_version": pkg_resources.get_distribution("modoboa").version
})
if resp.status_code != 200:
return None
resp = resp.json()
version = Version(resp["version"], DEFAULT_NUMBER_BITS)
if version <= Version(local_version, DEFAULT_NUMBER_BITS):
return None
return resp
+14 -2
View File
@@ -4,12 +4,13 @@ from django.utils.translation import ugettext as _
from django.contrib.auth.decorators import (
login_required, user_passes_test
)
from modoboa.lib import parameters
from modoboa.lib import events, parameters
from modoboa.lib.webutils import (
_render_to_string, render_to_json_response
)
from modoboa.lib.listing import get_sort_order, get_listing_page
from modoboa.core.models import Extension, Log
from modoboa.core.utils import new_version_available
from modoboa.core.tables import ExtensionsTable
@@ -94,7 +95,9 @@ def saveextensions(request):
@user_passes_test(lambda u: u.is_superuser)
def information(request, tplname="core/information.html"):
return render_to_json_response({
"content": render_to_string(tplname, {})
"content": render_to_string(tplname, {
"new_version": new_version_available()
}),
})
@@ -118,3 +121,12 @@ def logs(request, tplname="core/logs.html"):
"page": page.number,
"paginbar": pagination_bar(page),
})
@login_required
def check_top_notifications(request):
"""
AJAX service to check for new top notifications to display.
"""
return render_to_json_response(
events.raiseQueryEvent("TopNotifications", request.user, True)
)
+1 -7
View File
@@ -17,7 +17,7 @@ class ParametersForm(AdminParametersForm):
)
sep1 = SeparatorField(label=_("Messages releasing"))
released_msgs_cleanup = YesNoField(
label=_("Remove released messages"),
initial="no",
@@ -49,12 +49,6 @@ class ParametersForm(AdminParametersForm):
initial="/var/amavis/amavisd.sock",
help_text=_("Path to the PDP server socket (if unix mode)")
)
check_requests_interval = forms.IntegerField(
label=_("Check requests interval"),
initial=30,
help_text=_("Interval between two release requests checks")
)
user_can_release = YesNoField(
label=_("Allow direct release"),
+11 -50
View File
@@ -53,35 +53,6 @@ def extra_static_content(caller, user):
if user.group == "SimpleUsers":
return []
if caller == 'top' and parameters.get_admin("USER_CAN_RELEASE") == 'no':
tpl = Template("""<script type="text/javascript">
$(document).ready(function() {
var poller = new Poller("{{ url }}", {
interval: {{ interval }},
success_cb: function(data) {
var $link = $("#nbrequests");
var $maincounter = $("#alerts-counter");
if (data.requests > 0) {
$maincounter.html(data.requests);
$link.children("span").html(data.requests);
$maincounter.closest('div').removeClass('hidden');
} else {
$maincounter.closest('div').addClass('hidden');
}
}
});
});
</script>
""")
return [tpl.render(
Context({
'url': reverse("modoboa.extensions.amavis.views.nbrequests"),
'interval': int(parameters.get_admin("CHECK_REQUESTS_INTERVAL")) * 1000,
'text': _("pending requests"),
})
)]
if caller == 'domains':
tpl = Template("""<script type="text/javascript">
$(document).bind('domform_init', function() {
@@ -95,35 +66,25 @@ $(document).bind('domform_init', function() {
@events.observe("TopNotifications")
def display_requests(user):
def check_for_pending_requests(user, include_all):
"""
Check if release requests are pending.
"""
from .sql_listing import get_wrapper
if parameters.get_admin("USER_CAN_RELEASE") == "yes" \
or user.group == "SimpleUsers":
return []
nbrequests = get_wrapper().get_pending_requests(user)
if not nbrequests:
return [{"id": "nbrequests", "counter": 0}] if include_all \
else []
url = reverse("modoboa.extensions.amavis.views.index")
url += "#listing/?viewrequests=1"
tpl = Template("""<ul class="nav pull-right {{ css }}">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-white icon-bell"></i> <span id="alerts-counter" class="label label-important">{{ nbrequests }}</span>
</a>
<ul class="dropdown-menu">
<li>
<a id="nbrequests" href="{{ url }}">
<span class="label label-important">{{ nbrequests }}</span> {{ label }}
</a>
</li>
</ul>
</li>
</ul>""")
css = "hidden" if nbrequests == 0 else ""
return [tpl.render(Context(dict(
label=_("Pending requests"), url=url, css=css,
nbrequests=nbrequests
)))]
return [{
"id": "nbrequests", "url": url, "text": _("Pending requests"),
"counter": nbrequests, "level": "important"
}]
def send_amavis_form():
-1
View File
@@ -6,7 +6,6 @@ urlpatterns = patterns(
(r'^listing/$', '_listing'),
(r'^getmailcontent/(?P<mail_id>[\w\-\+]+)/$', 'getmailcontent'),
(r'^process/$', 'process'),
(r'^nbrequests/$', 'nbrequests'),
(r'^delete/(?P<mail_id>[\w\-\+]+)/$', 'delete'),
(r'^release/(?P<mail_id>[\w\-\+]+)/$', 'release'),
(r'^(?P<mail_id>[\w\-\+]+)/$', 'viewmail'),
-7
View File
@@ -288,10 +288,3 @@ def process(request):
if request.POST["action"] == "delete":
return delete(request, ids)
@login_required
@user_passes_test(lambda u: u.group != 'SimpleUsers')
def nbrequests(request):
result = get_wrapper().get_pending_requests(request.user)
return render_to_json_response({'requests': result})
+101
View File
@@ -0,0 +1,101 @@
/**
* Top notifications poller.
*/
var TopNotifications = function(options) {
this.initialize(options);
};
TopNotifications.prototype = {
constructor: TopNotifications,
defaults: {
container_id: "#top_notifications",
global_counter_id: "#alerts-counter",
url: null,
interval: 1000
},
/**
* Constructor.
*/
initialize: function(options) {
this.options = $.extend({}, this.defaults, options);
this.poller = new Poller(this.options.url, {
interval: this.options.interval,
success_cb: $.proxy(this.update, this)
});
this.$container = $(this.options.container_id);
},
/**
* Build a notification entry content.
*/
build_link_content: function(data) {
var content = "";
if (data.counter) {
content = "<span class='label label-" +
data.level + "'>" + data.counter + "</span> " +
data.text;
} else {
content = "<i class='icon-info-sign'></i> " + data.text;
}
return content;
},
/**
* Append an entry to the notification list.
*/
append_entry: function(data) {
if ((data.counter !== undefined && data.counter === 0) ||
(data.counter === undefined && data.text === undefined)) {
return;
}
var $li = $("<li />");
$li.append(
$("<a />", {
id: data.id, href: data.url,
html: this.build_link_content(data)
})
);
this.$container.find(".dropdown-menu").append($li);
},
/**
* Update an existing entry in the notification list.
*/
update_entry: function(data) {
var $link = $("#" + data.id);
if ((data.counter !== undefined && data.counter === 0) ||
(data.counter === undefined && data.text === undefined)) {
$link.parent().remove();
return;
}
$link.html(this.build_link_content(data));
},
/**
* Poller callback.
*/
update: function(data) {
for (var cpt = 0; cpt < data.length; cpt++) {
var $link = $("#" + data[cpt].id);
if (!$link.length) {
this.append_entry(data[cpt]);
} else {
this.update_entry(data[cpt]);
}
}
var $entries = this.$container.find(".dropdown-menu li");
if ($entries.length) {
this.$container.removeClass("hidden");
$(this.options.global_counter_id).html($entries.length);
} else {
this.$container.addClass("hidden");
}
}
};
@@ -0,0 +1,20 @@
<ul id="top_notifications" class="nav pull-right{% if not top_notifications %} hidden{% endif %}">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-white icon-bell"></i> <span id="alerts-counter" class="label label-important">{{ top_notifications|length }}</span>
</a>
<ul class="dropdown-menu">
{% for notification in top_notifications %}
<li>
<a id="{{ notification.id }}" href="{{ notification.url }}">
{% if notification.counter %}
<span class="label label-{{ notification.level }}">{{ notification.counter }}</span> {{ notification.text|safe }}
{% else %}
<i class="icon-info-sign"></i> {{ notification.text|safe }}
{% endif %}
</a>
</li>
{% endfor %}
</ul>
</li>
</ul>
+10 -1
View File
@@ -18,10 +18,19 @@
<script src="{{ STATIC_URL }}js/confirmation.js" type="text/javascript"></script>
<script src="{{ STATIC_URL }}js/notify.js" type="text/javascript"></script>
<script src="{{ STATIC_URL }}js/poller.js" type="text/javascript"></script>
<script src="{{ STATIC_URL }}js/top_notifications.js" type="text/javascript"></script>
{% url "django.views.i18n.javascript_catalog" as catalog_url %}
<script type="text/javascript" src="{{ catalog_url }}"></script>
<script type="text/javascript">
var top_notifications;
set_static_url("{{ STATIC_URL }}");
$(document).ready(function() {
top_notifications = new TopNotifications({
url: "{% url 'check_top_notifications' %}",
interval: {{ notifications_check_interval }}
});
});
</script>
{% extra_static_content "top" user %}
{% block html_head %}{% endblock %}
@@ -46,7 +55,7 @@
<ul class="nav pull-right">
{% if selection %}{% user_menu user selection %}{% else %}{% user_menu user "" %}{% endif %}
</ul>
<div class="pull-right">{% load_notifications user %}</div>
<div class="pull-right">{% include "common/top_notifications.html" %}</div>
</div>
</div>
</div>
+1
View File
@@ -10,3 +10,4 @@ argparse
rfc6266
factory-boy
passlib
-e git+https://github.com/tonioo/django-versionfield.git#egg=django-versionfield