mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
@@ -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
|
||||
=========
|
||||
|
||||
@@ -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",
|
||||
}]
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -10,3 +10,4 @@ argparse
|
||||
rfc6266
|
||||
factory-boy
|
||||
passlib
|
||||
-e git+https://github.com/tonioo/django-versionfield.git#egg=django-versionfield
|
||||
|
||||
Reference in New Issue
Block a user