From e10bba2d4e1e7e2f0420252c0f206b800ae6243c Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 27 Mar 2012 12:02:46 +0200 Subject: [PATCH] wip: Global user interface improvment * The quarantine manager is done * The webmail is not yet completed * Cleanup * More... --- admin/forms.py | 2 +- admin/models.py | 2 +- extensions/amavis_quarantine/sql_listing.py | 5 +- .../static/css/quarantine.css | 27 +- .../amavis_quarantine/static/js/quarantine.js | 304 ++++++++++-------- .../templates/amavis_quarantine/index.html | 48 +-- .../amavis_quarantine/viewheader.html | 40 +-- .../templatetags/amextras.py | 39 ++- extensions/amavis_quarantine/views.py | 30 +- extensions/webmail/imaputils.py | 11 +- .../webmail/templates/webmail/compose.html | 4 +- .../webmail/templates/webmail/folders.html | 20 +- .../webmail/templates/webmail/headers.html | 1 - extensions/webmail/templatetags/webextras.py | 50 +-- extensions/webmail/views.py | 6 +- lib/email_listing.py | 37 ++- lib/tables.py | 9 +- lib/templatetags/libextras.py | 13 + static/css/custom.css | 25 ++ static/js/common.js | 15 - static/js/email_listing.js | 16 +- static/js/history.js | 50 ++- static/js/jquery.htmltable.js | 127 ++++++++ static/js/searchbar.js | 68 ++++ static/js/utils.js | 36 +++ templates/common/buttons_list.html | 11 + templates/common/email_searchbar.html | 18 +- templates/common/generic_modal.html | 12 + templates/common/mailheaders.html | 2 +- templates/common/menulist.html | 26 +- templates/common/pagination_bar.html | 8 +- templates/common/table_body.html | 3 +- templates/common/viewmail.html | 21 +- templates/fixed.html | 4 + templates/fluid.html | 8 + templates/nlayout.html | 101 +++--- templates/twocols.html | 4 +- userprefs/templatetags/uprefs_extras.py | 4 +- 38 files changed, 746 insertions(+), 461 deletions(-) create mode 100644 static/js/jquery.htmltable.js create mode 100644 static/js/searchbar.js create mode 100644 templates/common/buttons_list.html create mode 100644 templates/common/generic_modal.html create mode 100644 templates/fixed.html create mode 100644 templates/fluid.html diff --git a/admin/forms.py b/admin/forms.py index 5d9a5cb71..8f868dccd 100644 --- a/admin/forms.py +++ b/admin/forms.py @@ -246,11 +246,11 @@ class AccountFormGeneral(forms.ModelForm): role = self.cleaned_data["role"] else: role = "SimpleUsers" + account.groups.clear() if role == "SuperAdmins": account.is_superuser = True else: account.is_superuser = False - account.groups.clear() account.groups.add(Group.objects.get(name=role)) account.save() return account diff --git a/admin/models.py b/admin/models.py index 1e771ac3b..bb476e456 100644 --- a/admin/models.py +++ b/admin/models.py @@ -167,7 +167,7 @@ class User(DUser): @property def group(self): if self.is_superuser: - return "SuperAdmin" + return "SuperAdmins" try: return self.groups.all()[0].name except IndexError: diff --git a/extensions/amavis_quarantine/sql_listing.py b/extensions/amavis_quarantine/sql_listing.py index a1deb238c..6086d7452 100644 --- a/extensions/amavis_quarantine/sql_listing.py +++ b/extensions/amavis_quarantine/sql_listing.py @@ -76,14 +76,15 @@ class SQLconnector(MBconnector): return emails class SQLlisting(EmailListing): - tpl = "amavis_quarantine/index.html" + tpl = "amavis_quarantine/index2.html" tbltype = Qtable deflocation = "listing/" defcallback = "updatelisting" reset_wm_url = True + table_styles = "table-condensed" def __init__(self, user, msgs, filter, **kwargs): - if user.belongs_to_group('SimpleUsers'): + if user.group == 'SimpleUsers': Qtable.cols_order = ['type', 'rstatus', 'from_', 'subject', 'time'] else: Qtable.cols_order = ['type', 'rstatus', 'to', 'from_', 'subject', 'time'] diff --git a/extensions/amavis_quarantine/static/css/quarantine.css b/extensions/amavis_quarantine/static/css/quarantine.css index da2ff19ad..3c46ce99b 100644 --- a/extensions/amavis_quarantine/static/css/quarantine.css +++ b/extensions/amavis_quarantine/static/css/quarantine.css @@ -1,8 +1,29 @@ +#menubar { + position: fixed; + top: 60px; + padding-left: 15px; +} + +#listing { + position: fixed; + padding: 0 15px; + right: 0; + left: 0; +} + +#mailcontent { + border: 0; + margin: 0; + padding: 0; + width: 100%; + height: 100%; +} + thead tr { position: relative; z-index: 1; } -tr.table-tr-selected { - background: #ffffcc; -} \ No newline at end of file +td.unseen { + font-weight: bold; +} diff --git a/extensions/amavis_quarantine/static/js/quarantine.js b/extensions/amavis_quarantine/static/js/quarantine.js index 5a6347947..bfc349bf1 100644 --- a/extensions/amavis_quarantine/static/js/quarantine.js +++ b/extensions/amavis_quarantine/static/js/quarantine.js @@ -1,136 +1,180 @@ -function updatehash(evt) { - evt.stop(); - var url = $defined(this.get("href")) ? this.get("href") : ""; - current_anchor.parse_string(url, true).setparams(navparams); - current_anchor.update(); -} +var Quarantine = function(options) { + this.initialize(options); +}; -function appendhash(evt) { - evt.stop(); - location.hash += evt.target.get("href"); -} +Quarantine.prototype = { + constructor: Quarantine, -function updateparams(evt) { - evt.stop(); - current_anchor.updateparams(evt.target.get("href")); - current_anchor.update(); -} + defaults: { + deflocation: "listing", + defcallback: "listing_cb" + }, -function actioncallback(resp) { - $("listing").set("html", ""); - $("menubar").set("html", ""); - $("navbar").set("html", ""); - if (resp.status == "ok") { - infobox.info(resp.respmsg); - current_anchor.parse_string(resp.url, true).setparams(navparams); - current_anchor.update(1); - } else { - infobox.error(resp.respmsg); - } -} + initialize: function(options) { + this.options = $.extend({}, this.defaults, options); + this.navobj = new History(this.options); -function actionclick(evt) { - new Request.JSON.mdb({ - url: evt.target.get("href"), - onSuccess: actioncallback - }).get(); -} + this.register_navcallbacks(); + this.listen(); + }, -function deleteclick(evt) { - evt.stop(); - if (!confirm(gettext("Delete this message?"))) { - return false; + update_page: function(data) { + if (data.menu != undefined) { + $("#menubar").html(data.menu); + $("#searchfield").searchbar({navobj: this.navobj}); + } + if (data.navbar) { + $("#bottom-bar-in").html(data.navbar); + } + if (data.listing != undefined) { + $("#listing").html(data.listing); + $("#listing").css({ + top: $("#menubar").outerHeight() + 60 + "px", + bottom: $("#bottom-bar").outerHeight() + "px", + overflow: "auto" + }); + } + }, + + listen: function() { + $(document).on("dblclick", "tbody>tr", $.proxy(this.viewmail_loader, this)); + $(document).on("click", "a[name=selectmsgs]", $.proxy(this.selectmsgs, this)); + $(document).on("click", "a[name=release-multi]", + $.proxy(this.release_selection, this)); + $(document).on("click", "a[name=delete-multi]", + $.proxy(this.delete_selection, this)); + $(document).on("click", "#bottom-bar a", $.proxy(this.load_page, this)); + + $(document).on("click", "a[name=release]", $.proxy(this.release, this)); + $(document).on("click", "a[name=delete]", $.proxy(this.delete, this)); + $(document).on("click", "a[name=viewmode]", $.proxy(this.update_params, this)); + }, + + load_page: function(e) { + e.preventDefault(); + var $link = $(e.target).parent(); + + this.navobj.deleteParam("rcpt"); + this.navobj.from_string($link.attr("href")).update(); + }, + + viewmail_loader: function(e) { + e.preventDefault(); + var $tr = $(e.target).parent(); + var $to = $tr.find("td[name=to]"); + + this.navobj.baseurl($tr.attr("id")); + if ($to.length) { + this.navobj.setparam("rcpt", $to.html().trim()); + } + this.navobj.update(); + }, + + selectmsgs: function(e) { + e.preventDefault(); + var type = $(e.target).attr("href"); + + if (type == "") { + $("#emails").htmltable("clear_selection"); + return; + } + $("td[name=type]").each(function() { + var $this = $(this); + if ($this.html().trim() == type) { + $("#emails").htmltable("select_row", $this.parent()); + } + }); + }, + + _send_selection: function(e, name, message) { + var $link = $(e.target); + + e.preventDefault(); + if (!this.htmltable.current_selection().length || + !confirm(message)) { + return; + } + var selection = []; + + this.htmltable.current_selection().each(function() { + var $tr = $(this); + var $to = $tr.find("td[name=to]"); + + if ($to.length) { + selection.push($to.html().trim() + " " + $tr.attr("id")); + } else { + selection.push($tr.attr("id")); + } + }); + $.ajax({ + url: $link.attr("href"), + data: "action=" + name + "&selection=" + selection.join(","), + type: 'POST', + dataType: 'json', + success: $.proxy(this.action_cb, this) + }); + }, + + release_selection: function(e) { + this._send_selection(e, "release", gettext("Release this selection?")); + }, + + delete_selection: function(e) { + this._send_selection(e, "delete", gettext("Delete this selection?")); + }, + + _send_action: function(e, message) { + var $link = $(e.target); + + e.preventDefault(); + if (!confirm(message)) { + return; + } + $.ajax({ + url: $link.attr("href"), + dataType: 'json', + success: $.proxy(this.action_cb, this) + }); + }, + + release: function(e) { + this._send_action(e, gettext("Release this message?")); + }, + + delete: function(e) { + this._send_action(e, gettext("Delete this message?")); + }, + + update_params: function(e) { + var $link = $(e.target); + e.preventDefault(); + this.navobj.updateparams($link.attr("href")).update(); + }, + + register_navcallbacks: function() { + this.navobj.register_callback("_listing", + $.proxy(this.listing_cb, this)); + this.navobj.register_callback("viewmail", + $.proxy(this.viewmail_cb, this)); + }, + + listing_cb: function(data) { + this.update_page(data); + $("#emails").htmltable(); + this.htmltable = $("#emails").data("htmltable"); + }, + + viewmail_cb: function(data) { + this.update_page(data); + $("#listing").css("overflow", "hidden"); + }, + + action_cb: function(data) { + if (data.status == "ok") { + this.navobj.parse_string(data.url, true).update(true); + $("body").notify("success", data.respmsg, 2000); + } else { + $("body").notify("error", data.respmsg); + } } - actionclick(evt); -} - -function releaseclick(evt) { - evt.stop(); - if (!confirm(gettext("Release this message?"))) { - return false; - } - actionclick(evt); -} - -function viewmail_cs(evt) { - evt.stop(); - var to = this.getFirst("td[name=to]"); - - current_anchor.baseurl(this.get("id")); - if ($defined(to)) { - current_anchor.setparam("rcpt", to.get("html").trim()); - } - current_anchor.update(); -} - -/* - * Callback of the 'listing' action (the main screen) - */ -function listing_cb(resp) { - updatelisting(resp); - - var tbl = new HtmlTable($("emails"), { - useKeyboard: false, - selectable: true - }); - - if ($("toggleselect")) { - $("toggleselect").addEvent("click", function(evt) { - if ($$("tr[class*=table-tr-selected]").length) { - tbl.selectNone(); - } else { - tbl.selectAll(); - } - }); - } - $$("a[name=selectmsgs]").addEvent("click", function(evt) { - evt.stop(); - var type = evt.target.get("href"); - - if (!$chk(type)) { - $$("tr[class*=table-tr-selected]").each(function(item) { - item.removeClass("table-tr-selected"); - }); - if ($("toggleselect").checked) { - $("toggleselect").checked = false; - tbl.selectNone(); - } - return; - } - $$("td[name=type]").each(function(item) { - if (item.get("html").trim() == type) { - item.getParent().addClass("table-tr-selected"); - } - }); - }); - $$("a[name=release]").addEvent("click", function(event) { - if (!checkTableSelection(gettext("Release this selection?"))) { - event.stop(); - return; - } - mysubmit(event, "release"); - }); - $$("a[name=delete]").addEvent("click", function(event) { - if (!checkTableSelection(gettext("Delete this selection?"))) { - event.stop(); - return; - } - mysubmit(event, "delete"); - }); - searchbox_init(); - init_sortable_columns(); - update_sortable_column(); - $$("tbody>tr").addEvent("dblclick", viewmail_cs); -} - -/* - * Callback of the 'viewmail' action - */ -function viewmail_cb(resp) { - updatelisting(resp); - $$("a[name=back]").addEvent("click", updatehash); - $$("a[name=delete]").addEvent("click", deleteclick); - $$("a[name=release]").addEvent("click", releaseclick); - $$("a[name=viewmode]").addEvent("click", updateparams); - setDivHeight("mailcontent", 5, 0); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/extensions/amavis_quarantine/templates/amavis_quarantine/index.html b/extensions/amavis_quarantine/templates/amavis_quarantine/index.html index fb8ff89c9..de5331cd7 100644 --- a/extensions/amavis_quarantine/templates/amavis_quarantine/index.html +++ b/extensions/amavis_quarantine/templates/amavis_quarantine/index.html @@ -1,42 +1,22 @@ -{% extends "common/email_listing.html" %} -{% load i18n %} +{% extends "fixed.html" %} {% block html_head %} + + + -{{ block.super }} {% endblock %} -{% block js %} - current_anchor.register_callback("_listing", listing_cb); - current_anchor.register_callback("viewmail", viewmail_cb); +{% block fixed_content %} + +
+{% endblock %} +{% block footer %} {% endblock %} -{% block middletitle %}{% trans "Quarantine" %}{% endblock %} diff --git a/extensions/amavis_quarantine/templates/amavis_quarantine/viewheader.html b/extensions/amavis_quarantine/templates/amavis_quarantine/viewheader.html index 8b187503f..50c749ef8 100644 --- a/extensions/amavis_quarantine/templates/amavis_quarantine/viewheader.html +++ b/extensions/amavis_quarantine/templates/amavis_quarantine/viewheader.html @@ -1,43 +1,11 @@ -{% extends "layout_simple.html" %} +{% extends "common/generic_modal.html" %} {% load i18n %} -{% block html_head %} - -{% endblock %} - -{% block content %} - - +{% block modalbody %} +
{% trans "Message headers" %}
{% for k, v in headers %} {% endfor %}
{{ k }}{{ v }}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/extensions/amavis_quarantine/templatetags/amextras.py b/extensions/amavis_quarantine/templatetags/amextras.py index 8f8bb0c4e..c7bc2addc 100644 --- a/extensions/amavis_quarantine/templatetags/amextras.py +++ b/extensions/amavis_quarantine/templatetags/amextras.py @@ -24,46 +24,44 @@ def viewm_menu(selection, backurl, mail_id, rcpt, perms): entries = [ {"name" : "back", - "url" : backurl, - "img" : static_url("pics/back.png"), + "img" : "icon-arrow-left", + "url" : "javascript:history.go(-1);", "label" : _("Back to list")}, {"name" : "headers", "url" : reverse(amavis_quarantine.views.viewheaders, args=[mail_id]), - "img" : static_url("pics/add.png"), "label" : _("View full headers"), - "class" : "boxed", - "rel" : "{handler:'iframe',size:{x:600,y:500}}"}, + "modal" : True, + "autowidth" : True}, {"name" : "release", + "img" : "icon-ok", "url" : reverse(amavis_quarantine.views.release, args=[mail_id]) \ + "?rcpt=%s" % rcpt, - "img" : static_url("pics/release.png"), "label" : _("Release")}, {"name" : "delete", + "img" : "icon-remove", "url" : reverse(amavis_quarantine.views.delete, args=[mail_id]) \ + "?rcpt=%s" % rcpt, - "img" : static_url("pics/remove.png"), "label" : _("Delete")}, {"name" : "options", "label" : _("Options"), - "img" : static_url("pics/settings.png"), - "class" : "menubardropdown", + "img" : "icon-cog", "menu" : options_menu} ] - return render_to_string('common/menu.html', + return render_to_string('common/buttons_list.html', {"selection" : selection, "entries" : entries, "perms" : perms}) @register.simple_tag def quar_menu(selection, user): entries = [ - {"name" : "release", - "url" : "", - "img" : static_url("pics/release.png"), + {"name" : "release-multi", + "url" : reverse(amavis_quarantine.views.process), + "img" : "icon-ok", "label" : _("Release")}, - {"name" : "delete", - "url" : "", - "img" : static_url("pics/remove.png"), + {"name" : "delete-multi", + "img" : "icon-remove", + "url" : reverse(amavis_quarantine.views.process), "label" : _("Delete")}, {"name" : "select", "url" : "", @@ -87,9 +85,7 @@ def quar_menu(selection, user): } ] - menu = render_to_string('common/menu.html', - {"selection" : selection, "entries" : entries}) - if not user.belongs_to_group('SimpleUsers'): + if user.group != 'SimpleUsers': extraopts = [{"name" : "to", "label" : _("To")}] else: extraopts = [] @@ -97,4 +93,7 @@ def quar_menu(selection, user): "MEDIA_URL" : settings.MEDIA_URL, "extraopts" : extraopts }) - return menu + searchbar + + return render_to_string('common/buttons_list.html', dict( + selection=selection, entries=entries, extracontent=searchbar + )) diff --git a/extensions/amavis_quarantine/views.py b/extensions/amavis_quarantine/views.py index 009c2442a..a48973905 100644 --- a/extensions/amavis_quarantine/views.py +++ b/extensions/amavis_quarantine/views.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- - +# coding: utf-8 import email from django.http import HttpResponseRedirect, HttpResponse from django.template import Template, Context @@ -29,14 +28,14 @@ def __get_current_url(request): return res def empty_quarantine(request): - content = "
%s
" % _("Empty quarantine") + content = "
%s
" % _("Empty quarantine") ctx = getctx("ok", level=2, listing=content, navbar="", menu=quar_menu("", request.user)) return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @login_required def _listing(request): - filter = None + flt = None rcptfilter = None msgs = None @@ -60,11 +59,11 @@ def _listing(request): continue else: raise Exception("unsupported search criteria %s" % c) - filter = nfilter if filter is None else filter | nfilter + flt = nfilter if flt is None else flt | nfilter q = ~Q(rs='D') if not request.user.is_superuser: - if not request.user.belongs_to_group('SimpleUsers'): + if request.user.group != 'SimpleUsers': doms = request.user.get_domains() if not len(doms): return empty_quarantine(request) @@ -73,7 +72,7 @@ def _listing(request): else: q &= Q(rid__email=request.user.email) - if not request.user.belongs_to_group('SimpleUsers') \ + if request.user.group != 'SimpleUsers' \ and rcptfilter is not None: q &= Q(rid__email__contains=rcptfilter) msgs = Msgrcpt.objects.filter(q).values("mail_id") @@ -86,7 +85,7 @@ def _listing(request): del request.session["page"] pageid = 1 - lst = SQLlisting(request.user, msgs, filter, baseurl="listing/", + lst = SQLlisting(request.user, msgs, flt, navparams=request.session["navparams"], elems_per_page=int(parameters.get_user(request.user, "MESSAGES_PER_PAGE"))) @@ -95,14 +94,16 @@ def _listing(request): return empty_quarantine(request) content = lst.fetch(request, page.id_start, page.id_stop) - navbar = lst.render_navbar(page) + navbar = lst.render_navbar(page, "listing/") ctx = getctx("ok", listing=content, navbar=navbar, menu=quar_menu("", request.user)) return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @login_required def index(request): - return SQLlisting(request.user, None, None, empty=True).render(request) + return _render(request, "amavis_quarantine/index.html", dict( + deflocation="listing/", defcallback="listing_cb", selection="quarantine" + )) @login_required def getmailcontent(request, mail_id): @@ -123,7 +124,7 @@ def getmailcontent(request, mail_id): @login_required def viewmail(request, mail_id): - if not request.user.belongs_to_group('SimpleUsers'): + if request.user.group != 'SimpleUsers': rcpt = request.GET["rcpt"] else: mb = Mailbox.objects.get(user=request.user) @@ -137,7 +138,7 @@ def viewmail(request, mail_id): args += ["%s=%s" % (kw, request.GET[kw])] content = Template(""" - + """).render(Context({"url" : reverse(getmailcontent, args=[mail_id]) \ + "?%s" % "&".join(args)})) menu = viewm_menu("", __get_current_url(request), mail_id, rcpt, @@ -152,6 +153,7 @@ def viewheaders(request, mail_id): content += qm.mail_text msg = email.message_from_string(content) return _render(request, 'amavis_quarantine/viewheader.html', { + "title" : _("Message headers"), "headers" : msg.items() }) @@ -166,7 +168,7 @@ def check_mail_id(request, mail_id): @login_required def delete(request, mail_id): mail_id = check_mail_id(request, mail_id) - if request.user.belongs_to_group('SimpleUsers'): + if request.user.group == 'SimpleUsers': mb = Mailbox.objects.get(user=request.user) msgrcpts = Msgrcpt.objects.filter(mail__in=mail_id, rid__email=mb.full_address) msgrcpts.update(rs='D') @@ -186,7 +188,7 @@ def delete(request, mail_id): @login_required def release(request, mail_id): mail_id = check_mail_id(request, mail_id) - if request.user.belongs_to_group('SimpleUsers'): + if request.user.group == 'SimpleUsers': mb = Mailbox.objects.get(user=request.user) msgrcpts = Msgrcpt.objects.filter(mail__in=mail_id, rid__email=mb.full_address) else: diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py index cec5e83ab..ab74df89d 100644 --- a/extensions/webmail/imaputils.py +++ b/extensions/webmail/imaputils.py @@ -367,13 +367,13 @@ class IMAPconnector(object): if len(topmailbox): md_mailboxes = [] else: - md_mailboxes = [{"name" : "INBOX", "class" : "inbox"}, + md_mailboxes = [{"name" : "INBOX", "class" : "icon-home"}, {"name" : parameters.get_user(user, "DRAFTS_FOLDER"), - "class" : "drafts"}, + "class" : "icon-file"}, {"name" : 'Junk'}, {"name" : parameters.get_user(user, "SENT_FOLDER")}, {"name" : parameters.get_user(user, "TRASH_FOLDER"), - "class" : "trash"}] + "class" : "icon-trash"}] md_mailboxes += self._listfolders(topmailbox, md_mailboxes) if unseen_messages: for fd in md_mailboxes: @@ -476,7 +476,10 @@ class IMAPconnector(object): return data = self._cmd("GETQUOTAROOT", self._encodefolder(mailbox)) - print data + if data is None: + self.quota_limit = self.quota_actual = None + return + quotadef = data[1][0] m = re.match("[^\s]+ \(STORAGE (\d+) (\d+)\)", quotadef) if not m: diff --git a/extensions/webmail/templates/webmail/compose.html b/extensions/webmail/templates/webmail/compose.html index 5091355d8..5284049c4 100644 --- a/extensions/webmail/templates/webmail/compose.html +++ b/extensions/webmail/templates/webmail/compose.html @@ -1,8 +1,8 @@ -{% load i18n %} +{% load i18n libextras %}
- {% include "common/genericform.html" %} + {% render_form form %}
diff --git a/extensions/webmail/templates/webmail/folders.html b/extensions/webmail/templates/webmail/folders.html index 2cc64549a..eb875d26d 100644 --- a/extensions/webmail/templates/webmail/folders.html +++ b/extensions/webmail/templates/webmail/folders.html @@ -1,16 +1,12 @@ {% load i18n %} {% load webextras %} -{% if titlebar %} -
- - {% trans 'Add a new folder' %} - - -
-
-{% endif %} -
    + diff --git a/extensions/webmail/templates/webmail/headers.html b/extensions/webmail/templates/webmail/headers.html index 900065fa6..1b5616f6c 100644 --- a/extensions/webmail/templates/webmail/headers.html +++ b/extensions/webmail/templates/webmail/headers.html @@ -1,6 +1,5 @@ {% extends "common/mailheaders.html" %} {% load i18n %} - {% block extraheaders %} {% if attachments %} diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py index 43a03a12a..a7f348321 100644 --- a/extensions/webmail/templatetags/webextras.py +++ b/extensions/webmail/templatetags/webextras.py @@ -50,7 +50,7 @@ def viewmail_menu(selection, folder, user, mail_id=None): } ] - return render_to_string('common/menu.html', + return render_to_string('common/buttons_list.html', {"selection" : selection, "entries" : entries, "user" : user}) @@ -66,7 +66,7 @@ def compose_menu(selection, backurl, user): "img" : static_url("pics/send-receive.png"), "label" : _("Send")}, ] - return render_to_string('common/menu.html', + return render_to_string('common/buttons_list.html', {"selection" : selection, "entries" : entries, "user" : user}) @@ -74,13 +74,13 @@ def compose_menu(selection, backurl, user): def listmailbox_menu(selection, folder, user): entries = [ {"name" : "compose", - "url" : "compose", - "img" : static_url("pics/edit.png"), - "label" : _("New message")}, + "url" : "compose", + "label" : _("New message"), + "class" : "btn"}, {"name" : "mark", - "img" : static_url("pics/domains.png"), + "label" : _("Mark messages"), - "class" : "menubardropdown", + "class" : "btn", "menu" : [ {"name" : "mark-read", "label" : _("As read"), @@ -93,8 +93,7 @@ def listmailbox_menu(selection, folder, user): {"name" : "actions", "img" : static_url("pics/settings.png"), "label" : _("Actions"), - "class" : "menubardropdown", - "width" : "150", + "class" : "btn", "menu" : [ {"name" : "fdaction", "label" : _("Compress folder"), @@ -110,24 +109,25 @@ def listmailbox_menu(selection, folder, user): "img" : static_url("pics/clear.png"), "url" : reverse(webmail.views.empty, args=[folder])} ] - menu = render_to_string("common/menu.html", + menu = render_to_string("common/buttons_list.html", {"selection" : selection, "entries" : entries, - "user" : user}) - searchbar = render_to_string("common/email_searchbar.html", { - "MEDIA_URL" : settings.MEDIA_URL - }) - return menu + searchbar + "user" : user, "css" : "nav"}) + # searchbar = render_to_string("common/email_searchbar.html", { + # "MEDIA_URL" : settings.MEDIA_URL + # }) + # return menu + searchbar + return menu @register.simple_tag def print_folders(folders, selected=None, withunseen=False, withmenu=False): result = "" for fd in folders: - cssclass = fd["class"] if fd.has_key("class") else "folder" + cssclass = "" name = fd["path"] if fd.has_key("sub") else fd["name"] label = fd["name"] if selected == name: - cssclass += " selected" + cssclass = "active" result += "
  • \n" % (name, cssclass) if fd.has_key("sub"): if selected is not None and selected != name and selected.count(name): @@ -136,17 +136,21 @@ def print_folders(folders, selected=None, withunseen=False, withmenu=False): else: ul_state = "hidden" div_state = "collapsed" - result += "
     
    " % div_state - if withmenu: - result += "" \ - % settings.MEDIA_URL + result += "
    " % div_state + # if withmenu: + # result += "" \ + # % settings.MEDIA_URL cssclass = "block" if withunseen and fd.has_key("unseen"): label += " (%d)" % fd["unseen"] cssclass += " unseen" - result += "%s\n" \ - % (fd.has_key("path") and fd["path"] or fd["name"], cssclass, label) + iclass = fd["class"] if fd.has_key("class") else "icon-folder-close" + result += """ + + %s + +""" % (fd.has_key("path") and fd["path"] or fd["name"], cssclass, iclass, label) if fd.has_key("sub") and len(fd["sub"]): result += "
      " % (fd["path"], ul_state) \ + print_folders(fd["sub"], selected, withunseen, withmenu) + "
    \n" diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index 604d1dac0..c4d7f7c99 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -526,7 +526,7 @@ def viewmail(request): url = reverse(getmailcontent) + "?mbox=%s&mailid=%s&links=%d" % \ (request.session["mbox"], mailid, links) content = Template(""" - + """).render(Context({"url" : url})) return dict(listing=content, menuargs=dict(mail_id=mailid)) @@ -571,7 +571,7 @@ def newindex(request): else: if request.is_ajax(): raise WebmailError(_("Bad request")) - response = dict(deflocation="?action=listmailbox", + response = dict(selection="webmail", deflocation="?action=listmailbox", defcallback="wm_updatelisting") curmbox = request.session.get("mbox", "INBOX") @@ -582,7 +582,7 @@ def newindex(request): int(parameters.get_user(request.user, "REFRESH_INTERVAL")) * 1000 response["mboxes"] = render_mboxes_list(request, imapc) response["quota"] = ImapListing.computequota(imapc) - return _render(request, "webmail/index.html", response) + return _render(request, "webmail/index2.html", response) if action in ["reply", "forward"]: action = "compose" diff --git a/lib/email_listing.py b/lib/email_listing.py index 36795c2dd..c7389b10c 100644 --- a/lib/email_listing.py +++ b/lib/email_listing.py @@ -26,7 +26,7 @@ class Page(object): def __init__(self, pageid, id_start, id_stop, items, items_per_page, has_previous, has_next, baseurl=None): - self.pageid = pageid + self.number = pageid self.id_start = id_start self.id_stop = id_stop self.items = items @@ -35,15 +35,15 @@ class Page(object): self.has_next = has_next self.baseurl = baseurl - def previous_page(self): + def previous_page_number(self): if not self.has_previous: return False - return self.pageid - 1 + return self.number - 1 - def next_page(self): + def next_page_number(self): if not self.has_next: return False - return self.pageid + 1 + return self.number + 1 def last_page(self): lid = self.items / self.items_per_page @@ -57,7 +57,9 @@ class Paginator(object): def __init__(self, total, elems_per_page): self.total = total self.elems_per_page = elems_per_page - self.baseurl = None + self.num_pages = total / elems_per_page + if total % elems_per_page: + self.num_pages += 1 def _indexes(self, page): id_start = self.elems_per_page * page + 1 @@ -80,11 +82,12 @@ class Paginator(object): return None p = Page(page, id_start, id_stop, self.total, self.elems_per_page, has_previous, has_next) - if self.baseurl: - p.baseurl = self.baseurl + p.paginator = self return p class EmailListing(object): + table_styles = "table-striped table-condensed" + def __init__(self, baseurl=None, folder=None, elems_per_page=40, navparams={}, **kwargs): self.folder = folder @@ -102,15 +105,14 @@ class EmailListing(object): self.paginator = Paginator(self.mbc.messages_count(folder=self.folder, order=order), elems_per_page) - if baseurl: - self.paginator.baseurl = baseurl + self.baseurl = baseurl @staticmethod - def render_navbar(page): + def render_navbar(page, baseurl=None): if page is None: return "" - context = {"page" : page, "MEDIA_URL" : settings.MEDIA_URL} - return render_to_string("common/navbar.html", context) + context = {"page" : page, "MEDIA_URL" : settings.MEDIA_URL, "baseurl" : baseurl} + return render_to_string("common/pagination_bar.html", context) def fetch(self, request, id_start, id_stop): table = self.tbltype(request, @@ -121,7 +123,12 @@ class EmailListing(object): {{ table }}
  • """) - return tpl.render(Context({"table" : table.render()})) + return tpl.render( + Context( + {"table" : table.render(styles=self.table_styles, + withheader=False)} + ) + ) # def render(self, request, pageid=1, **kwargs): # listing = "" @@ -153,7 +160,7 @@ class EmailListing(object): listing = _("This folder contains no message") else: listing = self.fetch(request, page.id_start, page.id_stop) - return dict(listing=listing, navbar=self.render_navbar(page)) + return dict(listing=listing, navbar=self.render_navbar(page, self.baseurl)) def parse_search_parameters(request): if request.GET.has_key("pattern"): diff --git a/lib/tables.py b/lib/tables.py index 7b8e295ca..f0da1e170 100644 --- a/lib/tables.py +++ b/lib/tables.py @@ -188,16 +188,17 @@ class Table(object): def __str__(self): return self.render() - def render(self): + def render(self, styles="table-striped table-bordered", withheader=True): if len(self.rows): t = Template(""" - - {% include "common/table_head.html" %} +
    + {% if withheader %}{% include "common/table_head.html" %}{% endif %} {% include "common/table_body.html" %}
    """) return t.render(RequestContext(self.request, { - "table" : self, "tableid" : self.tableid + "table" : self, "tableid" : self.tableid, "styles" : styles, + "withheader" : withheader })) t = Template("""
    %s
    diff --git a/lib/templatetags/libextras.py b/lib/templatetags/libextras.py index b6336bb05..5d16a73c1 100644 --- a/lib/templatetags/libextras.py +++ b/lib/templatetags/libextras.py @@ -3,6 +3,7 @@ import os from django.conf import settings from django import template from django.contrib import messages +from django.template import Template, Context from django.template.loader import render_to_string from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ @@ -116,3 +117,15 @@ def alert(msg, typ): × %s
""" % (typ, msg) + +@register.simple_tag +def render_link(linkdef, mdclass=None): + t = Template(""" +{% if link.img %}{% endif %} +{{ link.label }} +""") + return t.render(Context(dict(link=linkdef, mdclass=mdclass))) diff --git a/static/css/custom.css b/static/css/custom.css index 28121a8d2..f9c4eaf1e 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -2,6 +2,14 @@ body { padding-top: 60px; } +#fixed_content { + position: absolute; + top: 60px; + right: 0; + left: 0; + overflow: hidden; +} + #bottom-bar { position: fixed; left: 0; @@ -46,6 +54,19 @@ body { margin-bottom: 8px; } +.dropdown-menu .radio { + padding: 3px 33px; +} + +.no-margin { + margin: 0; +} + +.dropdown-menu label:hover { + background-color: #0088CC; + color: white; +} + .spaced select, .spaced input[type="text"] { margin-bottom: 8px; @@ -62,3 +83,7 @@ body { .buttons_list .active { display: block; } + +.tr-selected { + background-color: #F5F5F5; +} \ No newline at end of file diff --git a/static/js/common.js b/static/js/common.js index 6011f746f..3f9c88ff7 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -233,21 +233,6 @@ function searchbox_init() { } }; -// The following code prevents a bug under IE7 because fullpath is -// returned instead of a relative one. (even if mootools uses -// getAttribute("href", 2), this is not working for AJAX requests) -gethref = function(obj) { - var url = obj.get("href"); - var re = new RegExp("^(https?):"); - var scheme = re.exec(url); - - if (scheme != null) { - var baseurl = scheme[0] + "://" + location.host + location.pathname; - return url.replace(baseurl, ""); - } - return url; -}; - /*function saveparams(id, message) { $("#update").click(function(evt) { evt.preventDefault(); diff --git a/static/js/email_listing.js b/static/js/email_listing.js index 89b170616..74bc06cd3 100644 --- a/static/js/email_listing.js +++ b/static/js/email_listing.js @@ -12,20 +12,20 @@ function navbarcallback(event) { } function updatelisting(resp) { - if ($defined(resp.url)) { - current_anchor.baseurl(resp.url).update(); + if (resp.url != undefined) { + history.baseurl(resp.url).update(); return; } - if ($defined(resp.menu)) { + /*if (resp.menu != undefined) { $("menubar").set("html", resp.menu); parse_menubar("menubar"); + }*/ + if (resp.listing != undefined) { + $("#listing").html(resp.listing); } - if ($defined(resp.listing)) { - $("listing").set("html", resp.listing); - } - if ($defined(resp.navbar)) { + /*if ($defined(resp.navbar)) { $("navbar").set("html", resp.navbar); $$("#navbar a").addEvent("click", navbarcallback); - } + }*/ } diff --git a/static/js/history.js b/static/js/history.js index ad78f783c..e25b979b2 100644 --- a/static/js/history.js +++ b/static/js/history.js @@ -1,16 +1,5 @@ var History = function(options) { - this.options = $.extend({}, this.defaults, options); - this.callbacks = {}; - this.params = {}; - this.base = ''; - this.serialized = null; - this.updatenext = true; - this.reset_loading_infos(); - if (this.options.defcallback) { - this.register_callback("default", this.options.defcallback); - this.check_id = - setInterval($.proxy(this.check, this), this.options.checkinterval); - } + this.initialize(options); }; History.prototype = { @@ -24,6 +13,34 @@ History.prototype = { defcallback: null }, + initialize: function(options) { + this.options = $.extend({}, this.defaults, options); + this.callbacks = {}; + this.base = ''; + this.serialized = null; + this.updatenext = true; + this.load_params(); + this.reset_loading_infos(); + if (this.options.defcallback) { + this.register_callback("default", this.options.defcallback); + this.check_id = + setInterval($.proxy(this.check, this), this.options.checkinterval); + } + }, + + load_params: function() { + var rawqs = window.location.hash.substr(1); + + if (rawqs.indexOf('?')) { + this.params = {}; + return; + } + var tmp = rawqs.split('?'); + + rawqs = (tmp.length == 1) ? tmp[0] : tmp[1]; + this.params = parse_qs(rawqs.split('&')); + }, + paramslength: function() { return Object.keys(this.params).length; }, @@ -34,7 +51,7 @@ History.prototype = { }, reset: function() { - this.base = null; + this.base = ""; this.params = {}; return this; }, @@ -100,7 +117,9 @@ History.prototype = { }, deleteParam: function(str) { - this.params.erase(str); + if (this.params[str] != undefined) { + delete this.params[str]; + } }, updateparams: function(str) { @@ -111,6 +130,7 @@ History.prototype = { for (var i = 0; i < elems.length; i++) { this.setparamfromstring(elems[i]); } + return this; }, setparamfromstring: function(str) { @@ -124,7 +144,7 @@ History.prototype = { }, setparams: function(params) { - this.params.extend(params); + this.params = $.extend({}, this.params, params); return this; }, diff --git a/static/js/jquery.htmltable.js b/static/js/jquery.htmltable.js new file mode 100644 index 000000000..89bcfe7c3 --- /dev/null +++ b/static/js/jquery.htmltable.js @@ -0,0 +1,127 @@ +(function($) { + var HtmlTable = function(element, options) { + this.initialize(element, options); + }; + + HtmlTable.prototype = { + constructor: HtmlTable, + + initialize: function(element, options) { + this.$element = $(element); + this.options = $.extend({}, $.fn.htmltable.defaults, options); + this.shift_pressed = false; + this.last_selection = null; + this.prev_dir = null; + this.listen(); + + this.$element.css({ + "-moz-user-select": "-moz-none", + "-khtml-user-select": "none", + "-webkit-user-select": "none", + "-ms-user-select": "none", + "user-select": "none" + }); + this.$element.attr("unselectable", "on"); + }, + + listen: function() { + $(document).on("click", "tbody>tr", + $.proxy(this.toggle_select, this)); + $(document).on("keydown", "body", $.proxy(this.keydown, this)); + $(document).on("keyup", "body", $.proxy(this.keyup, this)); + }, + + _toggle_select: function($tr) { + this.last_selection = $tr; + if ($tr.hasClass(this.options.tr_selected_class)) { + $tr.removeClass(this.options.tr_selected_class); + } else { + $tr.addClass(this.options.tr_selected_class); + } + }, + + toggle_select: function(e) { + e.preventDefault(); + var $tr = $(e.target).parent(); + + if (this.shift_pressed && this.last_selection) { + var itfunc = ($tr.index() >= this.last_selection.index()) + ? "next" : "prev"; + var $curtr = null; + + if (this.prev_dir && itfunc != this.prev_dir) { + $curtr = this.last_selection; + } else { + $curtr = this.last_selection[itfunc]("tr"); + } + for (; $curtr.length; $curtr = $curtr[itfunc]("tr")) { + this._toggle_select($curtr); + if ($tr.attr("id") == $curtr.attr("id")) { + break; + } + } + this.prev_dir = itfunc; + return; + } + this._toggle_select($tr); + }, + + current_selection: function() { + return $("tr[class*=" + this.options.tr_selected_class + "]"); + }, + + /* + * Cancel the current selection + */ + clear_selection: function() { + this.current_selection().removeClass(this.options.tr_selected_class); + }, + + /* + * Manually select a table row + */ + select_row: function($row) { + $row.addClass(this.options.tr_selected_class); + }, + + keydown: function(e) { + switch (e.which) { + case 16: + e.preventDefault(); + this.shift_pressed = true; + break; + } + }, + + keyup: function(e) { + switch (e.which) { + case 16: + e.preventDefault(); + this.shift_pressed = false; + break; + } + } + }; + + $.fn.htmltable = function(method) { + var args = arguments; + + return this.each(function() { + var $this = $(this), + data = $this.data('htmltable'), + options = typeof method === "object" && method; + + if (!data) { + $this.data('htmltable', new HtmlTable(this, options)); + data = $this.data('htmltable'); + } + if (typeof method === "string") { + data[method].apply(data, Array.prototype.slice.call(args, 1)); + } + }); + }; + + $.fn.htmltable.defaults = { + tr_selected_class: 'tr-selected' + }; +})(jQuery); \ No newline at end of file diff --git a/static/js/searchbar.js b/static/js/searchbar.js new file mode 100644 index 000000000..36bf020ec --- /dev/null +++ b/static/js/searchbar.js @@ -0,0 +1,68 @@ +(function($) { + var SearchBar = function(element, options) { + this.initialize(element, options); + }; + + SearchBar.prototype = { + constructor: SearchBar, + + initialize: function(element, options) { + this.$element = $(element); + this.options = $.extend({}, $.fn.searchbar.defaults, options); + + this.$element.change($.proxy(this.pattern_change, this)); + this.$element.click(function(e) { + $(this).select(); + }); + this.$element.blur(function(e) { + var $this = $(this); + if ($this.attr("value") == "") { + $this.attr("value", gettext("Search...")); + } + }); + if (this.options.navobj.params.pattern != undefined) { + this.$element.attr("value", this.options.navobj.params.pattern); + } + if (this.options.navobj.params.criteria != undefined) { + $("#crit_" + this.options.navobj.params.criteria).attr("checked", "checked"); + } + }, + + pattern_change: function(e) { + var $input = $(e.target); + var criteria = $("input[name=scriteria]:checked").attr("value"); + var pattern = $input.attr("value"); + + if (pattern != "") { + this.options.navobj.setparams({ + pattern: pattern, + criteria: criteria + }); + } else { + this.options.navobj.deleteParam("pattern"); + this.options.navobj.deleteParam("criteria"); + this.options.navobj.deleteParam("page"); + } + this.options.navobj.update(); + } + }; + + $.fn.searchbar = function(method) { + return this.each(function() { + var $this = $(this), + data = $this.data('searchbar'), + options = typeof method === "object" && method; + + if (!data) { + $this.data('searchbar', new SearchBar(this, options)); + } + if (typeof method === "string") { + data[method](); + } + }); + }; + + $.fn.searchbar.defaults = { + navobj: null + }; +})(jQuery); \ No newline at end of file diff --git a/static/js/utils.js b/static/js/utils.js index 03e509128..e4fd30e02 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -123,6 +123,42 @@ function simple_ajax_form_post(e, options) { }); } +/* + * The following code prevents a bug under IE7 because fullpath is + * returned instead of a relative one. (even if mootools uses + * getAttribute("href", 2), this is not working for AJAX requests) + */ +function gethref(obj) { + var url = $(obj).attr("href"); + var re = new RegExp("^(https?):"); + var scheme = re.exec(url); + + if (scheme != null) { + var baseurl = scheme[0] + "://" + location.host + location.pathname; + return url.replace(baseurl, ""); + } + return url; +}; + +/* + * Extract the current URL parameters into a dictionnary. + * + * Ref: + * http://stackoverflow.com/questions/901115/get-query-string-values-in-javascript + */ +function parse_qs(raw) { + if (raw == "") return {}; + var res = {}; + + for (var i = 0; i < raw.length; i++) { + var p = raw[i].split('='); + + if (p.length != 2) continue; + res[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " ")); + } + return res; +} + /* * Simple wrapper around Request.JSON. We just ensure that * redirections are correctly catched. (on session timeout for diff --git a/templates/common/buttons_list.html b/templates/common/buttons_list.html new file mode 100644 index 000000000..38692f397 --- /dev/null +++ b/templates/common/buttons_list.html @@ -0,0 +1,11 @@ +{% load libextras %} +
{% for entry in entries %} +
{% if entry.menu %} + {{ entry.label }} + + {% else %}{% render_link entry "btn" %}{% endif %} +
+{% endfor %}{{ extracontent }}
+ diff --git a/templates/common/email_searchbar.html b/templates/common/email_searchbar.html index d214763fe..15a254d94 100644 --- a/templates/common/email_searchbar.html +++ b/templates/common/email_searchbar.html @@ -1,13 +1,13 @@ {% load i18n %} -