From 0a3353ffbc0a2275973b717e4a0b941f2282adc1 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 11 Oct 2014 16:10:21 +0200 Subject: [PATCH] Infinite scroll for the quarantine. see #585 --- .../extensions/amavis/general_callbacks.py | 2 +- modoboa/extensions/amavis/lib.py | 9 ++ modoboa/extensions/amavis/sql_connector.py | 8 +- .../amavis/static/amavis/css/quarantine.css | 21 ++-- .../amavis/static/amavis/js/quarantine.js | 108 +++++++++++++++--- .../amavis/templates/amavis/email_list.html | 31 +++++ .../amavis/templates/amavis/emails_page.html | 26 +++++ .../amavis/templates/amavis/index.html | 12 +- .../templates/amavis/main_action_bar.html | 104 +++++++++-------- .../amavis/templatetags/amavis_tags.py | 19 ++- modoboa/extensions/amavis/urls.py | 1 + modoboa/extensions/amavis/views.py | 67 ++++++++--- .../webmail/templates/webmail/index.html | 8 +- modoboa/static/css/custom.css | 7 +- modoboa/static/js/history.js | 6 + modoboa/static/js/jquery.infinitescroll.js | 3 + modoboa/static/js/listing.js | 47 +++++--- modoboa/templates/common/email_searchbar.html | 32 +++++- .../templates/common/top_notifications.html | 8 +- modoboa/templates/nlayout.html | 8 +- modoboa/templates/twocols.html | 2 +- 21 files changed, 380 insertions(+), 149 deletions(-) create mode 100644 modoboa/extensions/amavis/templates/amavis/email_list.html create mode 100644 modoboa/extensions/amavis/templates/amavis/emails_page.html diff --git a/modoboa/extensions/amavis/general_callbacks.py b/modoboa/extensions/amavis/general_callbacks.py index 58add79de..aaf022733 100644 --- a/modoboa/extensions/amavis/general_callbacks.py +++ b/modoboa/extensions/amavis/general_callbacks.py @@ -86,7 +86,7 @@ def check_for_pending_requests(user, include_all): url += "#listing/?viewrequests=1" return [{ "id": "nbrequests", "url": url, "text": _("Pending requests"), - "counter": nbrequests, "level": "important" + "counter": nbrequests, "level": "danger" }] diff --git a/modoboa/extensions/amavis/lib.py b/modoboa/extensions/amavis/lib.py index e66a7bcd4..0fcd1fb18 100644 --- a/modoboa/extensions/amavis/lib.py +++ b/modoboa/extensions/amavis/lib.py @@ -92,6 +92,15 @@ class QuarantineNavigationParameters(NavigationParameters): ('msgtype', None, False), ('viewrequests', None, False) ] + def _store_page(self): + """Specific method to store the current page.""" + if self.request.GET.get("reset_page", None) or "page" not in self: + self["page"] = 1 + else: + page = self.request.GET.get("page", None) + if page is not None: + self["page"] = int(page) + def back_to_listing(self): """Return the current listing URL. diff --git a/modoboa/extensions/amavis/sql_connector.py b/modoboa/extensions/amavis/sql_connector.py index 026483d31..9a27c0a1c 100644 --- a/modoboa/extensions/amavis/sql_connector.py +++ b/modoboa/extensions/amavis/sql_connector.py @@ -1,6 +1,8 @@ """ SQL connector module. """ +import datetime + from django.db.models import Q from modoboa.extensions.admin.models import Domain @@ -124,7 +126,7 @@ class SQLconnector(object): messages = self._apply_extra_select_filters(messages) return messages - def messages_count(self, **kwargs): + def messages_count(self): """Return the total number of messages living in the quarantine. We also store the built queryset for a later use. @@ -148,7 +150,7 @@ class SQLconnector(object): return self._messages_count - def fetch(self, start=None, stop=None, **kwargs): + def fetch(self, start=None, stop=None): """Fetch a range of messages from the internal cache. """ @@ -158,7 +160,7 @@ class SQLconnector(object): "to": qm["rid__email"], "subject": qm["mail__subject"], "mailid": qm["mail__mail_id"], - "date": qm["mail__time_num"], + "date": datetime.datetime.fromtimestamp(qm["mail__time_num"]), "type": qm["content"], "score": qm["bspam_level"]} rs = qm["rs"] diff --git a/modoboa/extensions/amavis/static/amavis/css/quarantine.css b/modoboa/extensions/amavis/static/amavis/css/quarantine.css index c5e08286e..0d0d2e6cb 100644 --- a/modoboa/extensions/amavis/static/amavis/css/quarantine.css +++ b/modoboa/extensions/amavis/static/amavis/css/quarantine.css @@ -40,21 +40,14 @@ margin: 0; } -#listing.fluid-content-quarantine { - position: relative; - padding-top: 30px; - height: 0; - overflow: hidden; - padding-bottom: 56.25%; /* proportion value to aspect ratio 16:9 (9 / 16 = 0.5625 or 56.25%) */ -} - - -#listing.fluid-content-quarantine iframe { +#listing { position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; + top: 125px; + bottom: 0; + margin: 0; + overflow: auto; + padding-left: 15px; + padding-right: 15px; } #mailcontent { diff --git a/modoboa/extensions/amavis/static/amavis/js/quarantine.js b/modoboa/extensions/amavis/static/amavis/js/quarantine.js index 0807a667b..dbb3506e4 100644 --- a/modoboa/extensions/amavis/static/amavis/js/quarantine.js +++ b/modoboa/extensions/amavis/static/amavis/js/quarantine.js @@ -1,3 +1,9 @@ +/** + * Return an instance of Quarantine. + * + * @constructor + * @param {Object} options - instance options + */ var Quarantine = function(options) { Listing.call(this, options); }; @@ -6,7 +12,11 @@ Quarantine.prototype = { constructor: Quarantine, defaults: { - deflocation: "listing" + deflocation: "listing", + main_table_id: "emails", + scroll_container: "#listing", + navigation_params: ["sort_order", "pattern", "criteria", "msgtype"], + eor_message: gettext("No more message to show") }, initialize: function(options) { @@ -18,10 +28,22 @@ Quarantine.prototype = { this.set_msgtype(); $(document).on('click', '#selectall', this.toggle_selection); - $("#searchfield").searchbar({navobj: this.navobj}); + this.initialize_searchfield(); }, - /* + /** + * Search field initialization. + */ + initialize_searchfield: function() { + $("#searchfield").searchbar({ + navobj: this.navobj, + pattern_changed: function(navobj) { + navobj.setparam("reset_page", "true"); + } + }); + }, + + /** * Activate the button corresponding to the current message type * filter. If no message type is found, the 'All' button is * selected. @@ -29,28 +51,40 @@ Quarantine.prototype = { set_msgtype: function() { var msgtype = this.navobj.getparam('msgtype'); - $("button[name*=msgtype]").removeClass('active'); - if (msgtype != undefined) { - $("button[name=msgtype_" + msgtype + "]").addClass('active'); + $("input[name*=msgtype]").attr("checked", false) + .parent().removeClass('active'); + + if (msgtype !== undefined) { + $("input[name=msgtype_" + msgtype + "]").attr("checked", true) + .parent().addClass('active'); } else { - $("button[name=msgtype_all]").addClass('active'); + $("button[name=msgtype_all]").attr("checked", true) + .parent().addClass('active'); } }, + /** + * Update the main page content. + * + * @this Quarantine + * @param {Object} data - new content + */ update_page: function(data) { - if (data.menu != undefined) { + if (data.menu !== undefined) { $("#menubar").html(data.menu); - $("#searchfield").searchbar({navobj: this.navobj}); + this.initialize_searchfield(); } - if (data.listing != undefined) { - - + if (data.listing !== undefined) { $("#listing").html(data.listing); - $("#listing:has(iframe)").addClass("fluid-content-quarantine"); - } - this.update_listing(data); + if (this.navobj.getbaseurl() == "listing") { + this.update_listing(data); + $(this.options.scroll_container).scrollTop(10); + } else { + this.update_listing(data, false); + $(this.options.scroll_container).infinite_scroll("pause"); + } }, listen: function() { @@ -66,7 +100,7 @@ Quarantine.prototype = { $(document).on("click", "a[name=delete]", $.proxy(this.delete, this)); $(document).on("click", "a[name=headers]", $.proxy(this.headers, this)); $(document).on("click", "td[name=type] span", $.proxy(this.filter_by_type, this)); - $(document).on("click", "#filters button", $.proxy(this.filter_by_type, this)); + $(document).on("click", "#filters label", $.proxy(this.filter_by_type, this)); }, load_page: function(e) { @@ -74,6 +108,32 @@ Quarantine.prototype = { this.navobj.delparam("rcpt").update(); }, + /** + * Return extra arguments used to fetch a page. + * + * @this Listing + */ + get_load_page_args: function() { + var args = Listing.prototype.get_load_page_args.call(this); + + args.scroll = true; + return args; + }, + + /** + * Calculate the bottom position of the scroll container. + * + * @param {Object} $element - scroll container object + */ + calculate_bottom: function($element) { + var emails_height = $("#emails").height(); + + if (!emails_height) { + return undefined; + } + return $("#emails").height() - $element.height(); + }, + view_requests: function(e) { e.preventDefault(); this.navobj.setparam("viewrequests", "1").update(); @@ -132,8 +192,10 @@ Quarantine.prototype = { } }, - /* + /** * Filter listing by message type (spam, virus, etc.) + * + * @param {Object} evt - event object */ filter_by_type: function(evt) { var $target = $(evt.target); @@ -142,7 +204,7 @@ Quarantine.prototype = { if ($target.is("span")) { msgtype = $target.html().trim(); } else { - msgtype = $target.attr("name").replace("msgtype_", ""); + msgtype = $target.children("input").attr("name").replace("msgtype_", ""); } if (msgtype != 'all') { @@ -272,6 +334,12 @@ Quarantine.prototype = { } }, + /** + * Navigation callback: listing. + * + * @this Quarantine + * @param {Object} data - ajax call response (JSON) + */ listing_cb: function(data) { this.update_page(data); this.navobj.delparam("rcpt").update(); @@ -282,11 +350,13 @@ Quarantine.prototype = { }); this.htmltable = $("#emails").data("htmltable"); this.deactivate_buttons(); + if (this.navobj.hasparam("reset_page")) { + this.navobj.delparam("reset_page").update(false, true); + } }, viewmail_cb: function(data) { this.update_page(data); - /*$("#listing").css("overflow", "hidden");*/ $("body").css("overflow", "hidden"); }, diff --git a/modoboa/extensions/amavis/templates/amavis/email_list.html b/modoboa/extensions/amavis/templates/amavis/email_list.html new file mode 100644 index 000000000..439d214c0 --- /dev/null +++ b/modoboa/extensions/amavis/templates/amavis/email_list.html @@ -0,0 +1,31 @@ +{% load i18n %} + +
+ + + + + + + + + + + + + + + {% include "amavis/emails_page.html" %} + +
+ {% trans "Score" %} + + {% trans "To" %} + + {% trans "From" %} + + {% trans "Subject" %} + + {% trans "Date" %} +
+
diff --git a/modoboa/extensions/amavis/templates/amavis/emails_page.html b/modoboa/extensions/amavis/templates/amavis/emails_page.html new file mode 100644 index 000000000..0e7b2c3da --- /dev/null +++ b/modoboa/extensions/amavis/templates/amavis/emails_page.html @@ -0,0 +1,26 @@ +{% load amavis_tags %} + +{% for email in email_list %} + + + + + + {{ email.type|msgtype_to_html }} + + + {% if email.img_rstatus %}{% endif %} + + {{ email.score }} + {{ email.to }} + + {{ email.from|truncatechars:30 }} + + + {{ email.subject|truncatechars:30 }} + + + {{ email.date|date:"SHORT_DATETIME_FORMAT" }} + + +{% endfor %} diff --git a/modoboa/extensions/amavis/templates/amavis/index.html b/modoboa/extensions/amavis/templates/amavis/index.html index 674d7de91..c63d1442a 100644 --- a/modoboa/extensions/amavis/templates/amavis/index.html +++ b/modoboa/extensions/amavis/templates/amavis/index.html @@ -4,6 +4,7 @@ + @@ -12,17 +13,18 @@ {% endblock %} {% block fixed_content %} -
- +
-
+
{% endblock %} diff --git a/modoboa/extensions/amavis/templates/amavis/main_action_bar.html b/modoboa/extensions/amavis/templates/amavis/main_action_bar.html index bcc6af815..a4cb5cf06 100644 --- a/modoboa/extensions/amavis/templates/amavis/main_action_bar.html +++ b/modoboa/extensions/amavis/templates/amavis/main_action_bar.html @@ -1,49 +1,61 @@ {% load lib_tags i18n %} -
- -
-
- -
-
diff --git a/modoboa/extensions/amavis/templatetags/amavis_tags.py b/modoboa/extensions/amavis/templatetags/amavis_tags.py index 676c5ab0d..8848b1812 100644 --- a/modoboa/extensions/amavis/templatetags/amavis_tags.py +++ b/modoboa/extensions/amavis/templatetags/amavis_tags.py @@ -1,9 +1,9 @@ from django import template -from django.template.loader import render_to_string -from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse -from django.conf import settings -from modoboa.lib.webutils import _render_to_string +from django.template.loader import render_to_string +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext as _ + register = template.Library() @@ -12,7 +12,7 @@ register = template.Library() def viewm_menu(mail_id, rcpt): entries = [ {"name": "back", - "url": "javascript:history.go(-2);", + "url": "javascript:history.go(-1);", "img": "fa fa-arrow-left", "class": "btn-primary", "label": _("Back")}, @@ -82,3 +82,12 @@ def quar_menu(): return render_to_string('amavis/main_action_bar.html', { 'extraopts': extraopts }) + + +@register.filter +def msgtype_to_html(msgtype): + """Transform a message type to a bootstrap label.""" + color = 'danger' if msgtype in ['S', 'V'] else 'warning' + return mark_safe( + '%s' % (color, msgtype) + ) diff --git a/modoboa/extensions/amavis/urls.py b/modoboa/extensions/amavis/urls.py index 5931ed4ee..b1fb87f3c 100644 --- a/modoboa/extensions/amavis/urls.py +++ b/modoboa/extensions/amavis/urls.py @@ -5,6 +5,7 @@ urlpatterns = patterns( url(r'^$', 'index', name="index"), url(r'^listing/$', '_listing', name="_mail_list"), + url(r'^listing/page/$', 'listing_page', name="mail_page"), url(r'^getmailcontent/(?P[\w\-\+]+)/$', 'getmailcontent', name="mailcontent_get"), url(r'^process/$', 'process', name="mail_process"), diff --git a/modoboa/extensions/amavis/views.py b/modoboa/extensions/amavis/views.py index 6eea2f82e..198d8024b 100644 --- a/modoboa/extensions/amavis/views.py +++ b/modoboa/extensions/amavis/views.py @@ -1,4 +1,7 @@ # coding: utf-8 +""" +Amavis quarantine views. +""" import email from django.shortcuts import render @@ -10,10 +13,10 @@ from django.contrib.auth.decorators import login_required from modoboa.lib import parameters from modoboa.lib.exceptions import BadRequest +from modoboa.lib.paginator import Paginator from modoboa.lib.webutils import ( - getctx, ajax_response, render_to_json_response + getctx, ajax_response, render_to_json_response, _render_to_string ) -from modoboa.lib.templatetags.lib_tags import pagination_bar from modoboa.extensions.admin.models import Mailbox, Domain from modoboa.extensions.amavis.templatetags.amavis_tags import ( quar_menu, viewm_menu @@ -21,15 +24,51 @@ from modoboa.extensions.amavis.templatetags.amavis_tags import ( from .lib import selfservice, AMrelease, QuarantineNavigationParameters from .models import Msgrcpt from .sql_connector import get_connector -from .sql_listing import SQLlisting, SQLemail +from .sql_listing import SQLemail def empty_quarantine(request): + """Shortcut to use when no content can be displayed.""" content = "
%s
" % _("Empty quarantine") ctx = getctx("ok", level=2, listing=content) return render_to_json_response(ctx) +def get_listing_page(request, connector): + """.""" + paginator = Paginator( + connector.messages_count(), + int(parameters.get_user(request.user, "MESSAGES_PER_PAGE")) + ) + page_id = int(connector.navparams.get("page")) + page = paginator.getpage(page_id) + if not page: + return None + return page + + +@login_required +def listing_page(request): + """Return a listing page.""" + navparams = QuarantineNavigationParameters(request) + previous_page_id = int(navparams["page"]) if "page" in navparams else None + navparams.store() + + connector = get_connector(user=request.user, navparams=navparams) + page = get_listing_page(request, connector) + if page is None: + context = {"length": 0} + navparams["page"] = previous_page_id + else: + context = { + "rows": _render_to_string(request, "amavis/emails_page.html", { + "email_list": connector.fetch(page.id_start, page.id_stop) + }), + "page": page.number + } + return render_to_json_response(context) + + @login_required def _listing(request): if not request.user.is_superuser and request.user.group != 'SimpleUsers': @@ -39,21 +78,15 @@ def _listing(request): navparams = QuarantineNavigationParameters(request) navparams.store() - lst = SQLlisting( - request.user, - navparams=navparams, - elems_per_page=int( - parameters.get_user(request.user, "MESSAGES_PER_PAGE") - ) - ) - page = lst.paginator.getpage(navparams.get('page')) - if not page: + connector = get_connector(user=request.user, navparams=navparams) + page = get_listing_page(request, connector) + if page is None: return empty_quarantine(request) - content = lst.fetch(request, page.id_start, page.id_stop) - ctx = getctx( - "ok", listing=content, paginbar=pagination_bar(page), page=page.number - ) + content = _render_to_string(request, "amavis/email_list.html", { + "email_list": connector.fetch(page.id_start, page.id_stop), + }) + ctx = getctx("ok", listing=content, page=page.number) if request.session.get('location', 'listing') != 'listing': ctx['menu'] = quar_menu() request.session['location'] = 'listing' @@ -62,8 +95,8 @@ def _listing(request): @login_required def index(request): + """Default view.""" return render(request, "amavis/index.html", dict( - deflocation="listing/?sort_order=-date", defcallback="listing_cb", selection="quarantine" )) diff --git a/modoboa/extensions/webmail/templates/webmail/index.html b/modoboa/extensions/webmail/templates/webmail/index.html index 954310f82..3bc8c5418 100644 --- a/modoboa/extensions/webmail/templates/webmail/index.html +++ b/modoboa/extensions/webmail/templates/webmail/index.html @@ -34,7 +34,7 @@ $(document).ready(function() { {% block fixed_content %}
-