From c201d25e0911349f036ed833a8910f94ccc16f81 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 16 Apr 2012 09:48:15 +0200 Subject: [PATCH] wip: global UI refactoring. see #204 --- extensions/amavis_quarantine/__init__.py | 12 +- extensions/amavis_quarantine/views.py | 2 +- extensions/sievefilters/forms.py | 21 +- .../sievefilters/static/js/dynamic_fields.js | 208 ------------------ .../static/js/jquery.dynfields.js | 7 +- .../templates/sievefilters/index.html | 3 +- .../templatetags/sfilters_tags.py | 4 +- extensions/sievefilters/urls.py | 1 - extensions/sievefilters/views.py | 10 - extensions/webmail/imaputils.py | 112 ++++++---- extensions/webmail/templatetags/webextras.py | 6 +- extensions/webmail/urls.py | 1 + extensions/webmail/views.py | 106 +++++---- lib/email_listing.py | 2 +- lib/tables.py | 6 +- static/js/poller.js | 62 ++++++ templates/nlayout.html | 1 + 17 files changed, 228 insertions(+), 336 deletions(-) delete mode 100644 extensions/sievefilters/static/js/dynamic_fields.js create mode 100644 static/js/poller.js diff --git a/extensions/amavis_quarantine/__init__.py b/extensions/amavis_quarantine/__init__.py index d19402022..a93a74156 100644 --- a/extensions/amavis_quarantine/__init__.py +++ b/extensions/amavis_quarantine/__init__.py @@ -103,11 +103,9 @@ def extra_static_content(user): return [] tpl = Template("""""") url = reverse("modoboa.extensions.amavis_quarantine.views.nbrequests") diff --git a/extensions/amavis_quarantine/views.py b/extensions/amavis_quarantine/views.py index b2c23e500..b7c664679 100644 --- a/extensions/amavis_quarantine/views.py +++ b/extensions/amavis_quarantine/views.py @@ -303,4 +303,4 @@ def process(request): @user_passes_test(lambda u: u.group != 'SimpleUsers') def nbrequests(request): nbrequests = get_nb_requests(request.user) - return ajax_simple_response(dict(requests=nbrequests)) + return ajax_simple_response(dict(status="ok", requests=nbrequests)) diff --git a/extensions/sievefilters/forms.py b/extensions/sievefilters/forms.py index 549c9934d..f9e529142 100644 --- a/extensions/sievefilters/forms.py +++ b/extensions/sievefilters/forms.py @@ -150,23 +150,26 @@ class FilterForm(forms.Form): def _build_fileinto_field(self, request, value): self._build_action_field(request, "fileinto", value) - def __build_folders_list(self, folders): + def __build_folders_list(self, folders, user, imapc, parentmb=None): ret = [] for fd in folders: value = fd["path"] if fd.has_key("path") else fd["name"] - ret += [(value, fd["name"])] - if fd.has_key("sub") and len(fd["sub"]): - ret += self.__build_folders_list(fd["sub"]) + if parentmb: + ret += [(value, fd["name"].replace("%s." % parentmb, ""))] + else: + ret += [(value, fd["name"])] + if fd.has_key("sub"): + submboxes = imapc.getmboxes(user, value, unseen_messages=False) + ret += self.__build_folders_list(submboxes, user, imapc, value) return ret def userfolders(self, request): - from modoboa.extensions.webmail.lib import IMAPconnector + from modoboa.extensions.webmail.imaputils import get_imapconnector - mbc = IMAPconnector(user=request.user.username, - password=request.session["password"]) - ret = mbc.getfolders(request.user, unseen_messages=False) + mbc = get_imapconnector(request) + ret = mbc.getmboxes(request.user, unseen_messages=False) - folders = self.__build_folders_list(ret) + folders = self.__build_folders_list(ret, request.user, mbc) return folders def tofilter(self): diff --git a/extensions/sievefilters/static/js/dynamic_fields.js b/extensions/sievefilters/static/js/dynamic_fields.js deleted file mode 100644 index 0dcafb5f4..000000000 --- a/extensions/sievefilters/static/js/dynamic_fields.js +++ /dev/null @@ -1,208 +0,0 @@ - -var DynamicCondition = new Class({ - - Extends: DynamicTextInput, - - options: { - defaultcond: "Subject", - templates: [] - }, - - loadoperators: function(container, tpl) { - container.empty(); - for (var ocpt = 0; ocpt < tpl["operators"].length; ocpt++) { - var opt = new Element("option", { - value: tpl["operators"][ocpt][0], - html: tpl["operators"][ocpt][1] - }); - opt.inject(container); - } - }, - - onTargetChange: function(event) { - var parts = event.target.get("name").split("_"); - var container = $("id_cond_operator_" + parts[2]); - - for (var cpt = 0; cpt < this.options.templates.length; cpt++) { - var tpl = this.options.templates[cpt]; - if (tpl["name"] != event.target.get("value")) { - continue; - } - this.loadoperators(container, tpl); - break; - } - }, - - createfield: function() { - var div = new Element("div", {"class": "item"}); - var condtarget = new Element("select", { - id: "id_cond_target_" + this.nextid, - name: "cond_target_" + this.nextid - }); - var condoperator = new Element("select", { - id: "id_cond_operator_" + this.nextid, - name: "cond_operator_" + this.nextid - }); - var condvalue = new Element("input", { - id: "id_cond_value_" + this.nextid, - name: "cond_value_" + this.nextid, - type: "text" - }); - - for (var cpt = 0; cpt < this.options.templates.length; cpt++) { - var tpl = this.options.templates[cpt]; - var opt = new Element("option", { - value: tpl["name"], - html: tpl["label"] - }); - opt.inject(condtarget); - - if (tpl["name"] != this.options.defaultcond) { - continue; - } - this.loadoperators(condoperator, tpl); - } - condtarget.inject(div); - condtarget.addEvent("change", this.onTargetChange.bind(this)); - condoperator.inject(div); - condvalue.inject(div); - return [div, condvalue]; - }, - - removeelement: function(event) { - var id = (event.target.get("id").split("_"))[1]; - - event.stop(); - $("id_cond_target_" + id).destroy(); - $("id_cond_operator_" + id).destroy(); - $("id_cond_value_" + id).destroy(); - event.target.destroy(); - this.fields_cnt--; - } -}); - -var DynamicAction = new Class({ - Extends: DynamicTextInput, - - options: { - defaultaction: "fileinto", - templates: [], - ufolders_url: null - }, - - initialize: function(targets, options) { - this.parent(targets, options); - $("id_action_arg_0_0").getElements("option").each(function(item) { - var cpt = 1; - var start = 0; - var value = item.get("value"); - - while (true) { - var id = value.indexOf(".", start); - if (id == -1) { - break; - } - item.setStyle("margin-left", (cpt * 20) + "px"); - cpt++; - start = id + 1; - } - }); - this.fdoptions = $("id_action_arg_0_0").getElements("option"); - }, - - loadargs: function(id, tpl) { - var res = new Elements(); - - for (var acpt = 0; acpt < tpl["args"].length; acpt++) { - var argname = "action_arg_" + id + "_" + acpt; - var child = null; - - if (tpl["args"][acpt]["type"] == "list") { - child = new Element("select", {id: "id_" + argname, name: argname}); - if ($defined(tpl["args"][acpt]["vloader"])) { - this[tpl["args"][acpt]["vloader"]](child); - } - } else if (tpl["args"][acpt]["type"] == "string") { - child = new Element("input", {type: "text", - id: "id_" + argname, name: argname}); - } - res.push(child); - } - return res; - }, - - deletePrevArgs: function(id) { - for (var cpt = 0; true; cpt++) { - var arg = $("id_action_arg_" + id + "_" + cpt); - if (!$defined(arg)) { - break; - } - arg.destroy(); - } - }, - - onNameChange: function(event) { - var parts = event.target.get("name").split("_"); - var container = event.target.getParent(); - - event.stop(); - this.deletePrevArgs(parts[2]); - for (var cpt = 0; cpt < this.options.templates.length; cpt++) { - var tpl = this.options.templates[cpt]; - if (tpl["name"] != event.target.get("value")) { - continue; - } - var args = this.loadargs(parts[2], tpl); - args.inject(container); - } - }, - - createfield: function() { - var div = new Element("div", {"class": "item"}); - var actionname = new Element("select", { - id: "id_action_name_" + this.nextid, - name: "action_name_" + this.nextid - }); - var args = null; - - for (var cpt = 0; cpt < this.options.templates.length; cpt++) { - var tpl = this.options.templates[cpt]; - var opt = new Element("option", { - value: tpl["name"], - html: tpl["label"] - }); - opt.inject(actionname); - - if (tpl["name"] != this.options.defaultaction) { - continue; - } - args = this.loadargs(this.nextid, tpl); - } - actionname.inject(div); - actionname.addEvent("change", this.onNameChange.bind(this)); - args.inject(div); - return [div, actionname]; - }, - - removeelement: function(event) { - var id = (event.target.get("id").split("_"))[1]; - - event.stop(); - $("id_action_name_" + id).destroy(); - for (var cpt = 0; true; cpt++) { - var arg = $("id_action_arg_" + id + "_" + cpt); - if (!$defined(arg)) { - break; - } - arg.destroy(); - } - event.target.destroy(); - this.fields_cnt--; - }, - - userfolders: function(container) { - this.fdoptions.each(function(item) { - item.clone().inject(container); - }); - } -}); \ No newline at end of file diff --git a/extensions/sievefilters/static/js/jquery.dynfields.js b/extensions/sievefilters/static/js/jquery.dynfields.js index eb3c61264..7980d9c6b 100644 --- a/extensions/sievefilters/static/js/jquery.dynfields.js +++ b/extensions/sievefilters/static/js/jquery.dynfields.js @@ -54,7 +54,7 @@ return $('', { href: '#', id: 'condition_' + this.nextid + '_rmbtn', - html: '' + html: '' }).click($.proxy(this.removefields, this)); }, @@ -250,7 +250,7 @@ return $('', { href: '#', id: 'action_' + this.nextid + '_rmbtn', - html: '' + html: '' }).click($.proxy(this.removefields, this)); }, @@ -322,7 +322,6 @@ $.fn.dynaction.defaults = { defaultaction: "fileinto", - templates: [], - ufolders_url: null + templates: [] }; })(jQuery); \ No newline at end of file diff --git a/extensions/sievefilters/templates/sievefilters/index.html b/extensions/sievefilters/templates/sievefilters/index.html index 03dcc4eb5..783b74be8 100644 --- a/extensions/sievefilters/templates/sievefilters/index.html +++ b/extensions/sievefilters/templates/sievefilters/index.html @@ -22,8 +22,7 @@ function filterform_cb() { templates: cond_templates }).data('dyncondition'); var $da = $("#action_0").dynaction({ - templates: action_templates, - ufolders_url: "{% url modoboa.extensions.sievefilters.views.get_user_folders %}" + templates: action_templates }).data('dynaction'); $("input[name=match_type]").click(function(e) { diff --git a/extensions/sievefilters/templatetags/sfilters_tags.py b/extensions/sievefilters/templatetags/sfilters_tags.py index b717b9a31..dbed8e525 100644 --- a/extensions/sievefilters/templatetags/sfilters_tags.py +++ b/extensions/sievefilters/templatetags/sfilters_tags.py @@ -53,7 +53,7 @@ def fset_menu(mode, setname): "url" : reverse("modoboa.extensions.sievefilters.views.activate_filters_set", args=[setname])}, {"name" : "removefs", - "img" : "icon-remove", + "img" : "icon-trash", "label" : _("Remove filters set"), "url" : reverse("modoboa.extensions.sievefilters.views.remove_filters_set", args=[setname])}, @@ -81,7 +81,7 @@ def filter_actions(setname, f, position, islast): {"name" : "removefilter", "url" : reverse("modoboa.extensions.sievefilters.views.removefilter", args=[setname, f["name"]]), - "img" : "icon-remove", + "img" : "icon-trash", "title" : _("Remove this filter")} ] if position != 1: diff --git a/extensions/sievefilters/urls.py b/extensions/sievefilters/urls.py index d728be9c0..406c84fd5 100644 --- a/extensions/sievefilters/urls.py +++ b/extensions/sievefilters/urls.py @@ -8,7 +8,6 @@ urlpatterns = patterns('modoboa.extensions.sievefilters.views', (r'^activatefs/(?P.+)/$', 'activate_filters_set'), (r'^downloadfs/(?P.+)/$', 'download_filters_set'), (r'^templates/(?P\w+)/$', 'get_templates'), - (r'^userfolders/$', 'get_user_folders'), (r'^(?P.+)/newfilter/$', 'newfilter'), (r'^(?P.+)/editfilter/(?P.+)/$', 'editfilter'), (r'^(?P.+)/removefilter/(?P.+)/$', 'removefilter'), diff --git a/extensions/sievefilters/views.py b/extensions/sievefilters/views.py index 259bc9015..4fe598e44 100644 --- a/extensions/sievefilters/views.py +++ b/extensions/sievefilters/views.py @@ -48,16 +48,6 @@ def get_templates(request, ftype): return ajax_simple_response(FilterForm([], [], request).cond_templates) return ajax_simple_response(FilterForm([], [], request).action_templates) -@login_required -@needs_mailbox() -def get_user_folders(request): - from modoboa.extensions.webmail.lib import IMAPconnector - - mbc = IMAPconnector(user=request.user.username, - password=request.session["password"]) - ret = mbc.getfolders(request.user, unseen_messages=False) - return ajax_simple_response(ret) - @login_required @needs_mailbox() def getfs(request, name): diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py index 26978081b..232dd6d63 100644 --- a/extensions/webmail/imaputils.py +++ b/extensions/webmail/imaputils.py @@ -149,7 +149,7 @@ class IMAPconnector(object): :param name: the command's name :return: the command's result """ - if name in ['FETCH', 'SORT', 'STORE', 'COPY']: + if name in ['FETCH', 'SORT', 'STORE', 'COPY', 'SEARCH']: try: typ, data = self.m.uid(name, *args) except imaplib.IMAP4.error, e: @@ -211,12 +211,15 @@ class IMAPconnector(object): """ if self.m is not None: try: - self.m.select() + self._cmd("CHECK") + except ImapError, e: + if hasattr(self, "current_mailbox"): + del self.current_mailbox + else: return - except imaplib.IMAP4.error, error: - print error - print self.login(user, password) - + status, msg = self.login(user, password) + if not status: + raise ImapError(msg) def login(self, user, passwd): import socket @@ -263,14 +266,18 @@ class IMAPconnector(object): else: criterion = "REVERSE DATE" folder = kwargs.has_key("folder") and kwargs["folder"] or None - self.select_mailbox(folder, False) + + # FIXME: pourquoi suis je obligé de faire un SELECT ici? un + # EXAMINE plante mais je pense que c'est du à une mauvaise + # lecture des réponses de ma part... + self.select_mailbox(folder, readonly=False) data = self._cmd("SORT", "(%s)" % criterion, "UTF-8", "(NOT DELETED)", *self.criterions) self.messages = data[0].split() self.getquota(folder) return len(self.messages) - def select_mailbox(self, name, readonly=True): + def select_mailbox(self, name, readonly=True, force=False): """Issue a SELECT/EXAMINE command to the server The given name is first 'imap-utf7' encoded. @@ -278,11 +285,16 @@ class IMAPconnector(object): :param name: mailbox's name :param readonly: """ + if hasattr(self, "current_mailbox"): + if self.current_mailbox == name and not force: + return + self.current_mailbox = name name = name.encode("imap4-utf-7") if readonly: self._cmd("EXAMINE", name) else: self._cmd("SELECT", name) + self.m.state = "SELECTED" def unseen_messages(self, mailbox): """Return the number of unseen messages @@ -290,21 +302,18 @@ class IMAPconnector(object): :param mailbox: the mailbox's name :return: an integer """ - # self.select_mailbox(mailbox) - # data = self._cmd("SEARCH", "UTF-8", "(NOT DELETED UNSEEN)") - # return len(data[0].split()) data = self._cmd("STATUS", mailbox.encode("imap4-utf-7"), "(UNSEEN)") m = self.unseen_pattern.match(data[0]) if m is None: return 0 return int(m.group(1)) - def _encodefolder(self, folder): + def _encode_mbox_name(self, folder): if not folder: return "INBOX" return folder.encode("imap4-utf-7") - def _parse_folder_name(self, descr, prefix, delimiter, parts): + def _parse_mailbox_name(self, descr, prefix, delimiter, parts): if not len(parts): return False path = "%s%s%s" % (prefix, delimiter, parts[0]) @@ -316,11 +325,11 @@ class IMAPconnector(object): if sdescr is None: sdescr = {"name" : parts[0], "path" : path, "sub" : []} descr += [sdescr] - if self._parse_folder_name(sdescr["sub"], path, delimiter, parts[1:]): + if self._parse_mailbox_name(sdescr["sub"], path, delimiter, parts[1:]): sdescr["class"] = "subfolders" return True - def _listfolders_simple(self, topmailbox='INBOX', md_mailboxes=[]): + def _listmboxes_simple(self, topmailbox='INBOX', md_mailboxes=[]): (status, data) = self.m.list() result = [] for mb in data: @@ -331,8 +340,8 @@ class IMAPconnector(object): if not descr.has_key("path"): descr["path"] = parts[0] descr["sub"] = [] - if self._parse_folder_name(descr["sub"], parts[0], delimiter, - parts[1:]): + if self._parse_mailbox_name(descr["sub"], parts[0], delimiter, + parts[1:]): descr["class"] = "subfolders" continue present = False @@ -347,31 +356,35 @@ class IMAPconnector(object): return sorted(result, key=itemgetter("name")) @capability('LIST-EXTENDED', '_listfolders_simple') - def _listfolders(self, topmailbox='', md_mailboxes=[]): + def _listmboxes(self, topmailbox='', mailboxes=[]): pattern = ("%s.%%" % topmailbox.encode("imap4-utf-7")) if len(topmailbox) else "%" resp = self._cmd("LIST", "", pattern, "RETURN", "(CHILDREN)") - result = [] + newmboxes = [] for mb in resp: flags, delimiter, name, childinfo = \ self.listextended_response_pattern.match(mb).groups() flags = flags.split(' ') name = name.decode("imap4-utf-7") - present = False - for mdm in md_mailboxes: + pos = -1 + for idx, mdm in enumerate(mailboxes): if mdm["name"] == name: - present = True + pos = idx break - if present: - continue - descr = dict(name=name) + if pos == -1: + descr = dict(name=name) + newmboxes += [descr] + else: + descr = mailboxes[idx] + + if r'\Marked' in flags or not r'\UnMarked' in flags: + descr["send_status"] = True if r'\HasChildren' in flags: descr["path"] = name descr["sub"] = [] - result += [descr] from operator import itemgetter - return sorted(result, key=itemgetter("name")) + mailboxes += sorted(newmboxes, key=itemgetter("name")) - def getfolders(self, user, topmailbox='', unseen_messages=True): + def getmboxes(self, user, topmailbox='', unseen_messages=True): if len(topmailbox): md_mailboxes = [] else: @@ -382,14 +395,18 @@ class IMAPconnector(object): {"name" : parameters.get_user(user, "SENT_FOLDER")}, {"name" : parameters.get_user(user, "TRASH_FOLDER"), "class" : "icon-trash"}] - md_mailboxes += self._listfolders(topmailbox, md_mailboxes) + self._listmboxes(topmailbox, md_mailboxes) + if unseen_messages: - for fd in md_mailboxes: - key = fd.has_key("path") and "path" or "name" - count = self.unseen_messages(fd[key]) + for mb in md_mailboxes: + if not mb.has_key("send_status"): + continue + del mb["send_status"] + key = mb.has_key("path") and "path" or "name" + count = self.unseen_messages(mb[key]) if count == 0: continue - fd["unseen"] = count + mb["unseen"] = count return md_mailboxes def _add_flag(self, mbox, msgset, flag): @@ -432,20 +449,18 @@ class IMAPconnector(object): """ self.select_mailbox(oldmailbox, False) self._cmd("COPY", msgset, newmailbox.encode("imap4-utf-7")) - self._cmd("STORE", msgset, "+FLAGS", r'(\Deleted)') + self._cmd("STORE", msgset, "+FLAGS", r'(\Deleted \Seen)') def push_mail(self, folder, msg): now = imaplib.Time2Internaldate(time.time()) - self.m.append(self._encodefolder(folder), r'(\Seen)', now, str(msg)) + self.m.append(self._encode_mbox_name(folder), r'(\Seen)', now, str(msg)) def empty(self, mbox): self.select_mailbox(mbox, False) resp = self._cmd("SEARCH", "ALL") - print resp - - # for num in data[0].split(): - # self.m.store(num, "+FLAGS", r'(\Deleted)') - # self.m.expunge() + seq = ",".join(resp[0].split()) + self._cmd("STORE", seq, "+FLAGS", r'(\Deleted)') + self._cmd("EXPUNGE") def compact(self, mbox): """Compact a specific mailbox @@ -460,20 +475,20 @@ class IMAPconnector(object): def create_folder(self, name, parent=None): if parent is not None: name = "%s.%s" % (parent, name) - typ, data = self.m.create(self._encodefolder(name)) + typ, data = self.m.create(self._encode_mbox_name(name)) if typ == "NO": raise WebmailError(data[0]) return True def rename_folder(self, oldname, newname): - typ, data = self.m.rename(self._encodefolder(oldname), - self._encodefolder(newname)) + typ, data = self.m.rename(self._encode_mbox_name(oldname), + self._encode_mbox_name(newname)) if typ == "NO": raise WebmailError(data[0], ajax=True) return True def delete_folder(self, name): - typ, data = self.m.delete(self._encodefolder(name)) + typ, data = self.m.delete(self._encode_mbox_name(name)) if typ == "NO": raise WebmailError(data[0]) return True @@ -483,7 +498,7 @@ class IMAPconnector(object): self.quota_limit = self.quota_actual = None return - data = self._cmd("GETQUOTAROOT", self._encodefolder(mailbox), + data = self._cmd("GETQUOTAROOT", self._encode_mbox_name(mailbox), responses=["QUOTAROOT", "QUOTA"]) if data is None: self.quota_limit = self.quota_actual = None @@ -526,7 +541,7 @@ class IMAPconnector(object): msg = email.message_from_string(data[int(uid)]['BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)]']) msg['imapid'] = uid if not r'\Seen' in data[int(uid)]['FLAGS']: - msg['class'] = 'unseen' + msg['style'] = 'unseen' if r'\Answered' in data[int(uid)]['FLAGS']: msg['img_flags'] = static_url('pics/answered.png') if r'$Forwarded' in data[int(uid)]['FLAGS']: @@ -551,6 +566,7 @@ class IMAPconnector(object): :param readonly: :param extraheaders: """ + print mbox self.select_mailbox(mbox, readonly) if headers is None: headers = ['DATE', 'FROM', 'TO', 'CC', 'SUBJECT'] @@ -562,6 +578,10 @@ class IMAPconnector(object): return data[int(mailid)] def get_imapconnector(request): + """Simple shortcut to create a connector + + :param request: a ``Request`` object + """ imapc = IMAPconnector(user=request.user.username, password=request.session["password"]) return imapc diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py index 105dd1d46..3d4b92302 100644 --- a/extensions/webmail/templatetags/webextras.py +++ b/extensions/webmail/templatetags/webextras.py @@ -143,16 +143,18 @@ def print_folders(folders, selected=None, withunseen=False, selectonly=False): result += "
" % div_state cssclass = "block" + extra_attrs = "" if withunseen and fd.has_key("unseen"): label += " (%d)" % fd["unseen"] cssclass += " unseen" + extra_attrs = ' data-toggle="%d"' % fd["unseen"] iclass = fd["class"] if fd.has_key("class") else "icon-folder-close" - result += """
+ result += """ %s """ % (fd.has_key("path") and fd["path"] or fd["name"], cssclass, - 'selectfolder' if selectonly else 'loadfolder', iclass, label) + 'selectfolder' if selectonly else 'loadfolder', extra_attrs, iclass, label) if fd.has_key("sub") and len(fd["sub"]): result += "\n" diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py index 64a27c006..9e31054d7 100644 --- a/extensions/webmail/urls.py +++ b/extensions/webmail/urls.py @@ -11,6 +11,7 @@ urlpatterns = patterns( (r'^$', "newindex"), (r'^submailboxes', "submailboxes"), (r'^getmailcontent', 'getmailcontent'), + (r'^unseenmsgs', 'check_unseen_messages'), # (r'^compose/$', "compose"), (r'^delete/$', 'delete'), diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index db5b98354..f7f93146c 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -63,7 +63,7 @@ def __render_common_components(request, folder_name, lst=None, content=None, men "folders" : _render_to_string(request, "webmail/folders.html", { "titlebar" : True, "selected" : request.session["folder"], - "folders" : mbc.getfolders(request.user), + "folders" : mbc.getmboxes(request.user), "withunseen" : True }), "menu" : menu, @@ -125,38 +125,50 @@ def index(request): @login_required @needs_mailbox() def getattachment(request, folder, mail_id): - if request.GET.has_key("partnumber"): - headers = {"Content-Type" : "text/plain", - "Content-Transfer-Encoding" : None} - icon = IMAPconnector(user=request.user.username, - password=request.session["password"]) - part = icon.fetchpart(mail_id, folder, request.GET["partnumber"]) - if part is not None: - if part.get_content_maintype() == "message": - payload = part.get_payload(0) - else: - payload = part.get_payload(decode=True) - resp = HttpResponse(payload) - for hdr, default in headers.iteritems(): - if not part.has_key(hdr): - if default is not None: - resp[hdr] = default - else: - continue - resp[hdr] = re.sub("\s", "", part[hdr]) - # I would add this part into the previous loop if I was - # able to use functions as default values... but I'm a bit - # lazy :p - if part.has_key("Content-Disposition"): - resp["Content-Disposition"] = \ - re.sub("\s", "", part["Content-Disposition"]) - else: - resp["Content-Disposition"] = \ - "attachment; filename=%s" % request.GET["fname"] - resp["Content-Length"] = len(payload) - return resp - raise Http404 + """Fetch a message attachment + + Problème lié aux optimisations apportées : la méthode fetchpart du + client IMAP ne renvoie que le payload. Pour obtenir les headers, + deux solutions : + * Conserver le bodystructure parsé précédemment (nécessite du caching) + + * Reparser le bodystructure :p + """ + pnum = request.GET.get("partnumber", None) + if not pnum: + raise Http404 + + headers = {"Content-Type" : "text/plain", + "Content-Transfer-Encoding" : None} + imapc = get_imapconnector(request) + part = imapc.fetchpart(mail_id, folder, pnum) + if part is None: + raise Http404 + if part.get_content_maintype() == "message": + payload = part.get_payload(0) + else: + payload = part.get_payload(decode=True) + resp = HttpResponse(payload) + for hdr, default in headers.iteritems(): + if not part.has_key(hdr): + if default is not None: + resp[hdr] = default + else: + continue + resp[hdr] = re.sub("\s", "", part[hdr]) + # I would add this part into the previous loop if I was + # able to use functions as default values... but I'm a bit + # lazy :p + if part.has_key("Content-Disposition"): + resp["Content-Disposition"] = \ + re.sub("\s", "", part["Content-Disposition"]) + else: + resp["Content-Disposition"] = \ + "attachment; filename=%s" % request.GET["fname"] + resp["Content-Length"] = len(payload) + return resp + @login_required @needs_mailbox() def move(request): @@ -204,7 +216,10 @@ def mark(request, name): def empty(request, name): if name == parameters.get_user(request.user, "TRASH_FOLDER"): get_imapconnector(request).empty(name) - return ajax_simple_response(dict(status="ok")) + content = "
%s
" % _("Empty mailbox") + return ajax_simple_response(dict( + status="ok", listing=content + )) @login_required @needs_mailbox() @@ -239,7 +254,7 @@ def newfolder(request, tplname="webmail/folder2.html"): "withunseen" : False, "selectonly" : True} - ctx["folders"] = mbc.getfolders(request.user) + ctx["folders"] = mbc.getmboxes(request.user) if request.method == "POST": form = FolderForm(request.POST) if form.is_valid(): @@ -267,7 +282,7 @@ def editfolder(request, tplname="webmail/folder2.html"): "action_classes" : "submit", "withunseen" : False, "selectonly" : True} - ctx["folders"] = mbc.getfolders(request.user) + ctx["folders"] = mbc.getmboxes(request.user) if request.method == "POST": form = FolderForm(request.POST) if form.is_valid(): @@ -388,7 +403,7 @@ def render_mboxes_list(request, imapc): curmbox = request.session.get("mbox", "INBOX") return _render_to_string(request, "webmail/folders.html", { "selected" : curmbox, - "folders" : imapc.getfolders(request.user), + "folders" : imapc.getmboxes(request.user), "withunseen" : True }) @@ -544,9 +559,22 @@ def viewmail(request): @needs_mailbox() def submailboxes(request): topmailbox = request.GET.get('topmailbox', '') - mboxes = get_imapconnector(request).getfolders(request.user, topmailbox) + mboxes = get_imapconnector(request).getmboxes(request.user, topmailbox) return ajax_simple_response(dict(status="ok", mboxes=mboxes)) +@login_required +@needs_mailbox() +def check_unseen_messages(request): + mboxes = request.GET.get("mboxes", None) + if not mboxes: + raise WebmailError(_("Invalid request")) + mboxes = mboxes.split(",") + counters = dict() + imapc = get_imapconnector(request) + for mb in mboxes: + counters[mb] = imapc.unseen_messages(mb) + return ajax_simple_response(dict(status="ok", counters=counters)) + @login_required @needs_mailbox() def newindex(request): @@ -587,10 +615,10 @@ def newindex(request): if not request.is_ajax(): request.session["lastaction"] = None imapc = get_imapconnector(request) + response["mboxes"] = render_mboxes_list(request, imapc) imapc.getquota(curmbox) response["refreshrate"] = \ - int(parameters.get_user(request.user, "REFRESH_INTERVAL")) * 1000 - response["mboxes"] = render_mboxes_list(request, imapc) + int(parameters.get_user(request.user, "REFRESH_INTERVAL")) response["quota"] = ImapListing.computequota(imapc) if response["quota"] < 50: response["quotalevel"] = "success" diff --git a/lib/email_listing.py b/lib/email_listing.py index 0d505bb89..bceb357b5 100644 --- a/lib/email_listing.py +++ b/lib/email_listing.py @@ -155,7 +155,7 @@ class EmailListing(object): def render(self, request, pageid=1, **kwargs): page = self.paginator.getpage(pageid) if not page: - listing = _("This folder contains no message") + listing = "
%s
" % _("Empty mailbox") else: listing = self.fetch(request, page.id_start, page.id_stop) return dict(listing=listing, navbar=self.render_navbar(page, self.baseurl)) diff --git a/lib/tables.py b/lib/tables.py index c7425df9d..81ccbfd06 100644 --- a/lib/tables.py +++ b/lib/tables.py @@ -120,8 +120,10 @@ class Table(object): trcpt = 0 for row in rows: nrow = {"id" : row[self.idkey], "cols" : [], "trcpt" : trcpt} - if row.has_key("options"): - nrow["options"] = row["options"] + for name in ["style", "options"]: + if row.has_key(name): + nrow[name] = row[name] + for c in self.columns: newcol = {"name" : c.name} for key in ["width", "align", "cssclass"]: diff --git a/static/js/poller.js b/static/js/poller.js new file mode 100644 index 000000000..b715ca333 --- /dev/null +++ b/static/js/poller.js @@ -0,0 +1,62 @@ +/* + * Simple poller plugin. + * + * Sends a request at a given frequency. + */ +var Poller = function(url, options) { + this.initialize(url, options); +}; + +Poller.prototype = { + constructor: Poller, + + defaults: { + interval: 2000, + success_cb: null, + error_cb: null + }, + + initialize: function(url, options) { + this.url = url; + this.options = $.extend({}, this.defaults, options); + this.running_request = false; + this.reset(); + }, + + reset: function() { + setTimeout($.proxy(this.send_request, this), this.options.interval); + }, + + send_request: function() { + var poller = this; + var args = ""; + + if (this.options.args != undefined) { + args = "?"; + if (typeof this.options.args === "function") { + args += this.options.args(); + } else { + args += this.options.args; + } + } + this.running_request = true; + $.ajax({ + url: this.url + args, + dataType: 'json', + success: function(data) { + poller.running_request = false; + if (data.status == "ok") { + if (poller.options.success_cb) { + poller.options.success_cb(data); + } + poller.reset(); + } else { + $("body").notify("error", data.respmsg); + if (poller.options.error_cb) { + poller.options.error_cb(data); + } + } + } + }); + } +}; \ No newline at end of file diff --git a/templates/nlayout.html b/templates/nlayout.html index 402792db1..be4aecb3e 100644 --- a/templates/nlayout.html +++ b/templates/nlayout.html @@ -16,6 +16,7 @@ + {% url django.views.i18n.javascript_catalog as catalog_url %}