diff --git a/admin/templatetags/admin_extras.py b/admin/templatetags/admin_extras.py index b377b3a2c..cdc0a1dec 100644 --- a/admin/templatetags/admin_extras.py +++ b/admin/templatetags/admin_extras.py @@ -67,6 +67,20 @@ def settings_menu(selection, perms): {"selection" : selection, "entries" : entries, "perms" : perms}) +@register.simple_tag +def domains_menu(selection, perms): + entries = [ + {"name" : "domains", + "url" : reverse(admin.views.domains), + "label" : _("Domains"), + "img" : "/static/pics/domains.png"} + ] + entries += events.raiseQueryEvent("AdminMenuDisplay", target="admin_menu_box", + perms=perms) + return render_to_string('main/menulist.html', + {"menu" : entries, "selection" : selection, + "perms" : perms}) + @register.simple_tag def loadadminextmenu(perms): menu = events.raiseQueryEvent("AdminMenuDisplay", target="admin_menu_box", diff --git a/admin/views.py b/admin/views.py index 7b917765f..5127f1c74 100644 --- a/admin/views.py +++ b/admin/views.py @@ -49,7 +49,7 @@ def domains(request): return mailboxes(request, dom_id=mb.domain.id) return HttpResponseRedirect(reverse(main.views.index)) - + domains = Domain.objects.all() counters = {} for dom in domains: diff --git a/auth/__init__.py b/auth/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/auth/forms.py b/auth/forms.py new file mode 100644 index 000000000..909d7d3a6 --- /dev/null +++ b/auth/forms.py @@ -0,0 +1,7 @@ +from django import forms +from django.utils.translation import ugettext as _ + +class LoginForm(forms.Form): + username = forms.CharField(label=_("Username")) + password = forms.CharField(label=_("Password"), widget=forms.PasswordInput, + help_text=_("Password used to authenticate")) diff --git a/auth/models.py b/auth/models.py new file mode 100755 index 000000000..71a836239 --- /dev/null +++ b/auth/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/auth/tests.py b/auth/tests.py new file mode 100755 index 000000000..2247054b3 --- /dev/null +++ b/auth/tests.py @@ -0,0 +1,23 @@ +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/auth/views.py b/auth/views.py new file mode 100755 index 000000000..7cd567b80 --- /dev/null +++ b/auth/views.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from django.contrib.auth import authenticate, login +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render_to_response +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ +import mailng.admin +from forms import LoginForm + +def dologin(request): + error = None + if request.method == "POST": + form = LoginForm(request.POST) + user = authenticate(username=request.POST["username"], + password=request.POST["password"]) + if user: + if user.is_active: + login(request, user) + request.session["password"] = request.POST["password"] + next = request.POST["next"] + if next == "None": + next = reverse(mailng.admin.views.domains) + return HttpResponseRedirect(next) + else: + error = _("Account is disabled.") + error = _("Your username and password didn't match. Please try again.") + else: + form = LoginForm() + next = request.GET.has_key("next") and request.GET["next"] or None + return render_to_response("registration/login.html", { + "form" : form, "error" : error, "next" : next + }) diff --git a/extensions/amavis_quarantine/main.py b/extensions/amavis_quarantine/main.py index 3ffd5b9e6..c6f319ce2 100644 --- a/extensions/amavis_quarantine/main.py +++ b/extensions/amavis_quarantine/main.py @@ -2,15 +2,19 @@ """ """ +from datetime import datetime +import time +import email +import re +import os +import lxml.html +from lxml.html.clean import Cleaner from django.http import HttpResponseRedirect, HttpResponse -from django.shortcuts import get_object_or_404, render_to_response -from django.template.loader import render_to_string from django.template import Template from django.utils import simplejson from django.conf.urls.defaults import * from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse -from django.core.paginator import Paginator from django.contrib.auth.decorators \ import login_required from mailng.lib import events, parameters @@ -18,12 +22,7 @@ from mailng.lib import _render, _ctx_ok, _ctx_ko, decode from mailng.lib import db from mailng.admin.models import Mailbox from lib import AMrelease -from datetime import datetime -import email -import re -import os -import lxml.html -from lxml.html.clean import Cleaner +from sql_listing import * attached_map = {} @@ -53,62 +52,22 @@ def menu(**kwargs): ] return [] -def lookup(*args): - filter = "" - for a in args: - if not a: - continue - if a[0] == "&": - filter += " AND " - else: - filter += " OR " - filter += a[1:] - conn = db.getconnection("amavis_quarantine") - status, cursor = db.execute(conn, """ -SELECT msgs.from_addr, maddr.email, msgs.subject, msgs.content, quarantine.mail_id, - msgs.time_num, msgs.content -FROM quarantine, maddr, msgrcpt, msgs -WHERE quarantine.mail_id=msgrcpt.mail_id -AND msgrcpt.rid=maddr.id -AND msgrcpt.mail_id=msgs.mail_id -AND quarantine.chunk_ind=1 -%s -ORDER BY msgs.time_num DESC -""" % filter) - return cursor - -def render_listing(request, cursor): - emails = [] - rows = cursor.fetchall() - for row in rows: - emails.append({"from" : row[0], "to" : row[1], - "subject" : row[2], "content" : row[3], - "mailid" : row[4], "time" : datetime.fromtimestamp(row[5]), - "type" : row[6]}) - paginator = Paginator(emails, 10) - try: - pagenum = int(request.GET.get('page', '1')) - except ValueError: - pagenum = 1 - page = paginator.page(pagenum) - return render_to_string("amavis_quarantine/listing.html", { - "count" : len(rows), - "emails" : page, "current_page" : pagenum, - "last_page" : paginator._get_num_pages() - }) - @login_required def index(request, message=None): + start = time.time() if not request.user.is_superuser: mb = Mailbox.objects.get(user=request.user.id) - filter = "&maddr.email='%s'" % mb.full_address + filter = ["&maddr.email='%s'" % mb.full_address] else: filter = None - cursor = lookup(filter) - - return _render(request, 'amavis_quarantine/index.html', { - "content" : render_listing(request, cursor) - }) + try: + pageid = request.GET["page"] + if pageid == "": + pageid = 1 + except KeyError: + pageid = 1 + lst = SQLlisting(filter) + return lst.render(request, pageid=int(pageid), start=start) @login_required def viewmail_plain(request, content): @@ -311,7 +270,19 @@ def search(request): filter += " OR " filter += "msgs.%s LIKE '%%%s%%'" % (c, pattern) filter = "&(%s)" % filter - cursor = lookup(filter) - ctx = {"status" : "ok", "content" : render_listing(request, cursor)} - return HttpResponse(simplejson.dumps(ctx), - mimetype="application/json") + lst = SQLlisting([filter]) + try: + pageid = request.GET["page"] + if pageid == "": + pageid = 1 + except KeyError: + pageid = 1 + page = lst.paginator.getpage(pageid) + if page: + content = lst.fetch(page.id_start, page.id_stop).render(request) + navbar = lst.render_navbar(page) + else: + content = "Empty quarantine" + navbar = "" + ctx = {"status" : "ok", "listing" : content, "navbar" : navbar} + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") diff --git a/extensions/amavis_quarantine/sql_listing.py b/extensions/amavis_quarantine/sql_listing.py new file mode 100644 index 000000000..f4744adef --- /dev/null +++ b/extensions/amavis_quarantine/sql_listing.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime +from django.utils.translation import ugettext as _ +from mailng.lib import tables, db +from mailng.lib.email_listing import MBconnector, EmailListing + +class Qtable(tables.Table): + idkey = "mailid" + selection = tables.SelectionColumn("selection", first=True) + type = tables.Column("type", label=_("Type")) + from_ = tables.Column("from", label=_("From")) + subject = tables.Column("subject", label=_("Subject")) + time = tables.Column("date", label=_("Date")) + + def parse_date(self, value): + return datetime.fromtimestamp(value) + +class SQLconnector(MBconnector): + def __init__(self, filter=None): + self.conn = db.getconnection("amavis_quarantine") + self.filter = "" + if filter: + for a in filter: + if not a: + continue + if a[0] == "&": + self.filter += " AND " + else: + self.filter += " OR " + self.filter += a[1:] + query = self._get_query() + status, cursor = db.execute(self.conn, """ +SELECT count(quarantine.mail_id) AS total +%s +""" % query) + if not status: + print cursor + self.count = 0 + else: + self.count = int(cursor.fetchone()[0]) + + def _get_query(self): + return """ +FROM quarantine, maddr, msgrcpt, msgs +WHERE quarantine.mail_id=msgrcpt.mail_id +AND msgrcpt.rid=maddr.id +AND msgrcpt.mail_id=msgs.mail_id +AND quarantine.chunk_ind=1 +%s +ORDER BY msgs.time_num DESC +""" % self.filter + + def messages_count(self, folder=None): + return self.count + + def fetch(self, start=None, stop=None, **kwargs): + query = self._get_query() + status, cursor = db.execute(self.conn, """ +SELECT msgs.from_addr, maddr.email, msgs.subject, msgs.content, quarantine.mail_id, + msgs.time_num, msgs.content +%s +LIMIT %d,%d +""" % (query, start - 1, stop - 1)) + if not status: + print cursor + return [] + emails = [] + rows = cursor.fetchall() + for row in rows: + emails.append({"from" : row[0], "to" : row[1], + "subject" : row[2], "content" : row[3], + "mailid" : row[4], "date" : row[5], + "type" : row[6]}) + return emails + +class SQLlisting(EmailListing): + tpl = "amavis_quarantine/index.html" + + def __init__(self, filter, **kwargs): + self.mbc = SQLconnector(filter) + EmailListing.__init__(self, **kwargs) + + def fetch(self, id_start, id_stop): + return Qtable(self.mbc.fetch(start=id_start, stop=id_stop)) diff --git a/extensions/amavis_quarantine/templates/index.html b/extensions/amavis_quarantine/templates/index.html index c7da1ca4c..3d780c5d7 100644 --- a/extensions/amavis_quarantine/templates/index.html +++ b/extensions/amavis_quarantine/templates/index.html @@ -1,49 +1,11 @@ -{% extends "layout.html" %} +{% extends "common/email_listing.html" %} {% load i18n %} {% load amextras %} -{% block html_head %} - - - - +$$("a[name=release]").addEvent("click", function(event) { + if (!checkSelection("selection", "{% trans "Release this selection?" %}")) { + event.stop(); + return; + } + mysubmit(event, "release"); +}); + + +var myFx = new Fx.Slide($("searchparams")); +myFx.hide(); +$("searchopts").addEvent("click", function(event) { + myFx.cancel(); + myFx.toggle(); +}); +$$("input[name=scriteria]").addEvent("click", function(event) { + myFx.cancel(); + myFx.toggle(); +}); +$("searchfield").addEvent("change", function(event) { + var criteria = ""; + $$("input[name=scriteria]:checked").each(function(obj) { + criteria = obj.value; + }); + {% url extensions.amavis_quarantine.main.search as s_url %} + new Request.JSON({url: "{{ s_url }}", onSuccess: function(resp) { + $("listing").set("html", resp.listing); + $("navbar").set("html", resp.navbar); + {% url extensions.amavis_quarantine.main.viewmail as view_url %} + bindRows("{{ view_url }}", "{{ current_page }}"); + }}).get({"pattern" : $("searchfield").value, "criteria" : criteria}); +}); +$("searchfield").addEvent("click", function(event) { + $("searchfield").select(); +}); + +$("searchfield").addEvent("blur", function(event) { + if ($("searchfield").value == "") { + $("searchfield").set("value", "{% trans "Search..." %}"); + } +}); +{% url extensions.amavis_quarantine.main.viewmail as view_url %} +bindRows("{{ view_url }}", "{{ current_page }}"); {% endblock %} {% block middletitle %}{% trans "Quarantine" %}{% endblock %} {% block menu %} @@ -162,15 +101,3 @@ window.addEvent("domready", function() { {% endblock %} -{% block content %} -
-{% url extensions.amavis_quarantine.main.process as actionurl %} -
- - -
- {{ content }} -
-
-
-{% endblock %} diff --git a/extensions/amavis_quarantine/templates/listing.html b/extensions/amavis_quarantine/templates/listing.html deleted file mode 100644 index 9ce78ec9c..000000000 --- a/extensions/amavis_quarantine/templates/listing.html +++ /dev/null @@ -1,51 +0,0 @@ -{% load i18n %} - -{% if not count %} -
{% trans "No messages in quarantine" %}
-{% else %} -
-
- Messages {{ emails.start_index }} à {{ emails.end_index }} sur {{ count }} -
- -
- - - - - - - - - - {% for m in emails.object_list %} - - - - - {% url extensions.amavis_quarantine.main.viewmail m.mailid as view_url %} - - - - {% endfor %} -
{% trans "Type" %}{% trans "From" %}{% trans "Subject" %}{% trans "Date" %}
{{ m.type }}{{ m.from }}{{ m.subject }}{{ m.time }}
- -{% endif %} diff --git a/extensions/amavis_quarantine/templates/viewmail.html b/extensions/amavis_quarantine/templates/viewmail.html index b0f59790e..d1f9c0539 100644 --- a/extensions/amavis_quarantine/templates/viewmail.html +++ b/extensions/amavis_quarantine/templates/viewmail.html @@ -1,11 +1,11 @@ -{% extends "layout.html" %} +{% extends "nlayout.html" %} {% load i18n %} {% block html_head %} - + + + {% endblock %} {% block middletitle %}{% trans "Mail content" %}{% endblock %} {% block menu %} diff --git a/extensions/amavis_quarantine/urls.py b/extensions/amavis_quarantine/urls.py index 515bd274a..06806aa1f 100644 --- a/extensions/amavis_quarantine/urls.py +++ b/extensions/amavis_quarantine/urls.py @@ -2,7 +2,9 @@ from django.conf.urls.defaults import * urlpatterns = patterns('mailng.extensions.amavis_quarantine.main', (r'^$', 'index'), - (r'^viewmail/(?P[\w\-\+]+)/$', 'viewmail'), + url(r'^viewmail/(?P[\w\-\+]+)/$', 'viewmail', + name="viewmail_std"), + url(r'^viewmail/$', 'viewmail', name="viewmail_null"), (r'^getmailcontent/(?P[\w\-\+]+)/$', 'getmailcontent'), (r'^viewheaders/(?P[\w\-\+]+)/$', 'viewheaders'), (r'^process/$', 'process'), diff --git a/extensions/webmail/__init__.py b/extensions/webmail/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/webmail/imap_listing.py b/extensions/webmail/imap_listing.py new file mode 100644 index 000000000..b6ff4ad19 --- /dev/null +++ b/extensions/webmail/imap_listing.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +import re +import time +from datetime import datetime, timedelta +from email.header import decode_header +import email.utils +from django.utils.translation import ugettext as _ +from mailng.lib import decode, tables, imap_utf7 +from mailng.lib.email_listing import MBconnector, EmailListing +from mailng.lib import tables, imap_utf7, parameters + +class WMtable(tables.Table): + idkey = "imapid" + selection = tables.SelectionColumn("selection", first=True) + subject = tables.Column("subject", label=_("Subject")) + from_ = tables.Column("from", label=_("From")) + date = tables.Column("date", label=_("Date")) + from_exp = re.compile("([^<]+)<([^>]+)>") + name_exp = re.compile("\"(.+)\"") + + def parse_from(self, value): + m = self.from_exp.match(value) + if m: + name = m.group(1) + name = name.strip() + m2 = self.name_exp.match(name) + if m2: + return m2.group(1) + return m.group(1) + return value + + def parse_date(self, value): + ndate = datetime(*(email.utils.parsedate_tz(value))[:7]) + now = datetime.now() + if now - ndate > timedelta(7): + return ndate.strftime("%d.%m.%Y %H:%M") + return ndate.strftime("%a %H:%M") + + def parse_subject(self, value): + res = "" + dcd = decode_header(value) + for part in dcd: + if res != "": + res += " " + res += part[0].strip(" ") + return decode(res) + + +class IMAPconnector(MBconnector): + def login(self, user, passwd): + import imaplib + + try: + self.m = imaplib.IMAP4_SSL(self.address) + self.m.login(user, passwd) + except imaplib.IMAP4.error, test: + return False, test + return True, None + + def _encodefolder(self, folder): + if not folder: + return "INBOX" + return folder.encode("imap4-utf-7") + + def messages_count(self, folder=None): + (status, data) = self.m.select(self._encodefolder(folder)) + return int(data[0]) + + def listfolders(self, topfolder='INBOX', md_folders=[]): + (status, data) = self.m.list() + result = [] + for mb in data: + attrs, truc, name = mb.split() + name = name.strip('"').decode("imap4-utf-7") + present = False + for mdf in md_folders: + if mdf["name"] == name: + present = True + break + if not present: + result += [name] + return sorted(result) + + def fetch(self, start=None, stop=None, folder=None): + if not start or not stop: + return [] + result = [] + self.m.select(self._encodefolder(folder)) + range = "%d:%d" % (start, stop) + typ, data = self.m.fetch(range, '(BODY[HEADER.FIELDS (DATE FROM SUBJECT)])') + imapid = start + for response_part in data: + if isinstance(response_part, tuple): + msg = email.message_from_string(response_part[1]) + msg["imapid"] = imapid + result += [msg] + imapid += 1 + return result + +class ImapListing(EmailListing): + tpl = "webmail/index.html" + + def __init__(self, user, password, **kwargs): + self.mbc = IMAPconnector(parameters.get("webmail", "SERVER_ADDRESS"), 143) + status, text = self.mbc.login(user, password) + if not status: + print "Login error: %s" % text + EmailListing.__init__(self, **kwargs) + + def getfolders(self): + md_folders = [{"name" : "INBOX", "icon" : "overview.png"}, + {"name" : 'Drafts'}, + {"name" : 'Junk'}, + {"name" : 'Sent'}, + {"name" : 'Trash', "icon" : "trash.png"}] + folders = self.mbc.listfolders(md_folders=md_folders) + for fd in folders: + md_folders += [{"name" : fd}] + return md_folders + + def fetch(self, id_start, id_stop): + return WMtable(self.mbc.fetch(start=id_start, stop=id_stop, + folder=self.folder)) diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py new file mode 100644 index 000000000..684587404 --- /dev/null +++ b/extensions/webmail/main.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +import time +from django.http import HttpResponse +from django.utils import simplejson +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +from django.conf.urls.defaults import include +from django.contrib.auth.decorators import login_required +from mailng.admin.models import Mailbox +from mailng.lib import events, parameters +from mailng.lib import _render +from imap_listing import * + +def init(): + events.register("UserMenuDisplay", menu) + parameters.register("webmail", "SERVER_ADDRESS", "string", "127.0.0.1", + help=_("Mailboxes server address")) + +def urls(): + return (r'^mailng/webmail/', + include('mailng.extensions.webmail.urls')) + +def menu(**kwargs): + if kwargs["target"] != "user_menu_box": + return [] + return [ + {"name" : _("Webmail"), + "url" : reverse(index), + "img" : "/static/pics/webmail.png"} + ] + +@login_required +def index(request): + start = time.time() + lst = ImapListing(request.user.username, request.session["password"]) + try: + pageid = request.GET["page"] + except KeyError: + pageid = 1 + return lst.render(request, pageid=int(pageid), start=start) + +@login_required +def folder(request, folder): + lst = ImapListing(folder=folder) + try: + pageid = request.GET["page"] + except KeyError: + pageid = 1 + page = lst.paginator.getpage(pageid) + if page: + content = lst.fetch(page.id_start, page.id_stop).render(request) + navbar = lst.render_navbar(page) + else: + content = "Empty folder" + navbar = "" + ctx = {"status" : "ok", "listing" : content, "navbar" : navbar} + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") diff --git a/extensions/webmail/templates/index.html b/extensions/webmail/templates/index.html new file mode 100644 index 000000000..af4971081 --- /dev/null +++ b/extensions/webmail/templates/index.html @@ -0,0 +1,85 @@ +{% extends "common/email_listing.html" %} +{% load i18n %} +{% block css %} +#leftcol { + float: left; + width: 150px; +} + +.box h3 { + font-size: small; + margin: 2px 0px 2px 0px; + text-align: center; +} + +.box ul { + list-style: none; + padding: 5px 5px; + margin: 0; +} + +.box li { + clear: both; + margin-bottom: 4px; +} + +.box li.selected { + background: #ffffcc; +} + +.box a:link, .box a:visited { + background: transparent; + color: black; + font-weight: bold; + text-decoration: none; +} + +.box a:hover { + color: #6AB0C5; + text-decoration: none; +} + +.box img { + float: left; + margin-right: 2px; + border: 0; +} +{% endblock %} +{% block js %} + $$("a[name=loadfolder]").addEvent("click", function(event) { + event.stop(); + $$("a[name=loadfolder]").each(function(item) { + item.setStyle("background", "white"); + }); + event.target.setStyle("background", "#FFFFCC"); + new Request.JSON({url: event.target.get("href"), onSuccess: function(resp) { + $("listing").set("html", resp.listing); + $("navbar").set("html", resp.navbar); + }}).get(); + }); +{% endblock %} +{% block middletitle %}{% trans "Webmail" %}{% endblock %} +{% block menu %} + +{% endblock %} +{% block leftcol %} +
+

{% trans "Folders" %}

+ +
+{% endblock %} diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py new file mode 100644 index 000000000..c61787ee9 --- /dev/null +++ b/extensions/webmail/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('mailng.extensions.webmail.main', + (r'^$', "index"), + (r'^(?P.+)/$', "folder"), + ) diff --git a/lib/__init__.py b/lib/__init__.py index ff9fab9b6..25b629862 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -63,7 +63,7 @@ def _ctx_ok(url): def _ctx_ko(tpl, ctx): return {"status" : "ko", "content" : render_to_string(tpl, ctx)} -def decode(s, encodings=('ascii', 'utf8', 'latin1')): +def decode(s, encodings=('utf8', 'latin1', 'windows-1252', 'ascii')): for encoding in encodings: try: return s.decode(encoding) diff --git a/lib/email_listing.py b/lib/email_listing.py new file mode 100644 index 000000000..e56867631 --- /dev/null +++ b/lib/email_listing.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +import time +from django.template.loader import render_to_string +from mailng.lib import _render + +class MBconnector(object): + def __init__(self, address, port): + self.address = address + self.port = port + +class Page(object): + def __init__(self, pageid, id_start, id_stop, items, + items_per_page, has_previous, has_next): + self.pageid = pageid + self.id_start = id_start + self.id_stop = id_stop + self.items = items + self.items_per_page = items_per_page + self.has_previous = has_previous + self.has_next = has_next + + def previous_page(self): + if not self.has_previous: + return False + return self.pageid - 1 + + def next_page(self): + if not self.has_next: + return False + return self.pageid + 1 + + def last_page(self): + lid = self.items / self.items_per_page + if not lid: + return 1 + if self.items % self.items_per_page: + lid += 1 + return lid + +class Paginator(object): + def __init__(self, total, elems_per_page): + self.total = total + self.elems_per_page = elems_per_page + + def _indexes(self, page): + id_start = self.elems_per_page * page + 1 + id_stop = id_start + self.elems_per_page - 1 + return (id_start, id_stop) + + def getpage(self, page): + if page < 1: + return None + id_start, id_stop = self._indexes(page - 1) + has_previous = has_next = False + if id_start < self.total: + if page > 1: + has_previous = True + if id_stop < self.total: + has_next = True + else: + id_stop = self.total + else: + return None + return Page(page, id_start, id_stop, self.total, + self.elems_per_page, has_previous, has_next) + +class EmailListing: + def __init__(self, folder=None, elems_per_page=40, **kwargs): + self.folder = folder + self.elems_per_page = elems_per_page + self.paginator = Paginator(self.mbc.messages_count(self.folder), + elems_per_page) + + def render_navbar(self, page): + return render_to_string("common/navbar.html", { + "page" : page + }) + + def getfolders(self): + return None + + def render(self, request, pageid=1, **kwargs): + page = self.paginator.getpage(pageid) + if not page: + listing = "Empty folder" + else: + listing = self.fetch(page.id_start, page.id_stop).render(request) + elapsed = kwargs.has_key("start") and time.time() - kwargs["start"] or 0 + return _render(request, self.tpl, { + "listing" : listing, "elapsed" : elapsed, + "navbar" : self.render_navbar(page), + "folders" : self.getfolders(), + "selection" : self.folder, + "current_page" : pageid + }) diff --git a/lib/imap_utf7.py b/lib/imap_utf7.py new file mode 100644 index 000000000..516a45d7f --- /dev/null +++ b/lib/imap_utf7.py @@ -0,0 +1,210 @@ +# -*- coding: latin-1 -*- + +""" +Imap folder names are encoded using a special version of utf-7 as defined in RFC +2060 section 5.1.3. + +5.1.3. Mailbox International Naming Convention + + By convention, international mailbox names are specified using a + modified version of the UTF-7 encoding described in [UTF-7]. The + purpose of these modifications is to correct the following problems + with UTF-7: + + 1) UTF-7 uses the "+" character for shifting; this conflicts with + the common use of "+" in mailbox names, in particular USENET + newsgroup names. + + 2) UTF-7's encoding is BASE64 which uses the "/" character; this + conflicts with the use of "/" as a popular hierarchy delimiter. + + 3) UTF-7 prohibits the unencoded usage of "\"; this conflicts with + the use of "\" as a popular hierarchy delimiter. + + 4) UTF-7 prohibits the unencoded usage of "~"; this conflicts with + the use of "~" in some servers as a home directory indicator. + + 5) UTF-7 permits multiple alternate forms to represent the same + string; in particular, printable US-ASCII chararacters can be + represented in encoded form. + + In modified UTF-7, printable US-ASCII characters except for "&" + represent themselves; that is, characters with octet values 0x20-0x25 + and 0x27-0x7e. The character "&" (0x26) is represented by the two- + octet sequence "&-". + + All other characters (octet values 0x00-0x1f, 0x7f-0xff, and all + Unicode 16-bit octets) are represented in modified BASE64, with a + further modification from [UTF-7] that "," is used instead of "/". + Modified BASE64 MUST NOT be used to represent any printing US-ASCII + character which can represent itself. + + "&" is used to shift to modified BASE64 and "-" to shift back to US- + ASCII. All names start in US-ASCII, and MUST end in US-ASCII (that + is, a name that ends with a Unicode 16-bit octet MUST end with a "- + "). + + For example, here is a mailbox name which mixes English, Japanese, + and Chinese text: ~peter/mail/&ZeVnLIqe-/&U,BTFw- + +Found here: +http://svn.plone.org/svn/collective/mxmImapClient/trunk/imapUTF7.py + +""" +import binascii +import codecs + +# encoding + +def modified_base64(s): + s = s.encode('utf-16be') + return binascii.b2a_base64(s).rstrip('\n=').replace('/', ',') + +def doB64(_in, r): + if _in: + r.append('&%s-' % modified_base64(''.join(_in))) + del _in[:] + +def encoder(s): + r = [] + _in = [] + for c in s: + ordC = ord(c) + if 0x20 <= ordC <= 0x25 or 0x27 <= ordC <= 0x7e: + doB64(_in, r) + r.append(c) + elif c == '&': + doB64(_in, r) + r.append('&-') + else: + _in.append(c) + doB64(_in, r) + return (str(''.join(r)), len(s)) + + +# decoding + +def modified_unbase64(s): + b = binascii.a2b_base64(s.replace(',', '/') + '===') + return unicode(b, 'utf-16be') + + +def decoder(s): + r = [] + decode = [] + for c in s: + if c == '&' and not decode: + decode.append('&') + elif c == '-' and decode: + if len(decode) == 1: + r.append('&') + else: + r.append(modified_unbase64(''.join(decode[1:]))) + decode = [] + elif decode: + decode.append(c) + else: + r.append(c) + if decode: + r.append(modified_unbase64(''.join(decode[1:]))) + bin_str = ''.join(r) + return (bin_str, len(s)) + + +class StreamReader(codecs.StreamReader): + def decode(self, s, errors='strict'): + return decoder(s) + + +class StreamWriter(codecs.StreamWriter): + def decode(self, s, errors='strict'): + return encoder(s) + + +def imap4_utf_7(name): + if name == 'imap4-utf-7': + return (encoder, decoder, StreamReader, StreamWriter) +codecs.register(imap4_utf_7) + + +## testing methods + +def imapUTF7Encode(ust): + "Returns imap utf-7 encoded version of string" + return ust.encode('imap4-utf-7') + +def imapUTF7EncodeSequence(seq): + "Returns imap utf-7 encoded version of strings in sequence" + return [imapUTF7Encode(itm) for itm in seq] + + +def imapUTF7Decode(st): + "Returns utf7 encoded version of imap utf-7 string" + return st.decode('imap4-utf-7') + +def imapUTF7DecodeSequence(seq): + "Returns utf7 encoded version of imap utf-7 strings in sequence" + return [imapUTF7Decode(itm) for itm in seq] + + +def utf8Decode(st): + "Returns utf7 encoded version of imap utf-7 string" + return st.decode('utf-8') + + +def utf7SequenceToUTF8(seq): + "Returns utf7 encoded version of imap utf-7 strings in sequence" + return [itm.decode('imap4-utf-7').encode('utf-8') for itm in seq] + + +__all__ = ['imapUTF7Encode', 'imapUTF7Decode', ] + +if __name__=='__main__': + +## print u'bøx'.encode('imap4-utf-7') +## print 'expected b&APg-x' +## +## print u'båx'.encode('imap4-utf-7') +## print 'expected b&AOU-x' +## + print '#######' + print 'bøx' + e = imapUTF7Encode(u'bøx') + print e + print imapUTF7Decode(e).encode('latin-1') + + print '#######' + print 'båx' + e = imapUTF7Encode(u'båx') + print e + print imapUTF7Decode(e).encode('latin-1') + + print '#######' + print '~/bågø' + e = imapUTF7Encode(u'~/bågø') + print e + print imapUTF7Decode(e).encode('latin-1') + + print '#######' + print 'Ting & Såger' + e = imapUTF7Encode(u'Ting & Såger') + print e + print imapUTF7Decode(e).encode('latin-1') + +## e = imapUTF7Decode('b&AOU-x') +## print e.encode('latin-1') +## +## e = imapUTF7Decode('b&APg-x') +## print e.encode('latin-1') +## + print '#######' + print '~/Følder/mailbåx & stuff + more' + n = u'~/Følder/mailbåx & stuff + more' + e = imapUTF7Encode(n) + print e + print imapUTF7Decode(e).encode('latin-1') + + print '#######' + print '~peter/mail/&ZeVnLIqe-/&U,BTFw-' + print imapUTF7Decode('~peter/mail/&ZeVnLIqe-/&U,BTFw-').encode('utf-8') + diff --git a/lib/parameters.py b/lib/parameters.py index dfaf50f27..e7b1e484f 100644 --- a/lib/parameters.py +++ b/lib/parameters.py @@ -24,14 +24,14 @@ class NotDefined(Exception): return "Application '%s' and/or parameter '%s' not defined" % (self.app, self.name) -def register(app, name, type="string", default=None, help=None): +def register(app, name, type="string", deflt=None, help=None): """Register a new parameter. app corresponds to a core component (admin, main) or an extension. """ if not app in _params.keys(): _params[app] = {} - _params[app][name] = {"type" : type, "default" : default, "help" : help} + _params[app][name] = {"type" : type, "default" : deflt, "help" : help} def save(app, name, value): if not app in _params.keys() or not name in _params[app].keys(): @@ -53,5 +53,5 @@ def get(app, name): try: p = Parameter.objects.get(name="%s.%s" % (app, name)) except Parameter.DoesNotExist: - return _params[app][name]["defaut"] + return _params[app][name]["default"] return p.value diff --git a/lib/tables.py b/lib/tables.py new file mode 100644 index 000000000..a83b806a3 --- /dev/null +++ b/lib/tables.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +import inspect +import lxml +from django.template.loader import render_to_string + +class Column: + def __init__(self, name, **kwargs): + self.name = name + for k, v in kwargs.iteritems(): + setattr(self, k, v) + + def __str__(self): + try: + label = getattr(self, "label") + except KeyError: + label = getattr(self, "name") + return label + +class SelectionColumn(Column): + def __str__(self): + return "" + + def render(self, value): + return "" \ + % (self.name, value) + +class Table(object): + def __init__(self, rows={}): + self.columns = [] + for m in inspect.getmembers(self): + if isinstance(m[1], Column): + try: + if getattr(m[1], "first"): + self.columns = [m[1]] + self.columns + except AttributeError: + self.columns += [m[1]] + self.rows = [] + for row in rows: + nrow = {"id" : row[self.idkey], "cols" : []} + for c in self.columns: + if isinstance(c, SelectionColumn): + nrow["cols"] += [{"value" : c.render(row[self.idkey]), "safe" : True}] + continue + if row.has_key(c.name): + try: + nrow["cols"] += [{"value" : getattr(self, "parse_%s" % c.name)(row[c.name])}] + except AttributeError: + nrow["cols"] += [{"value" : row[c.name]}] + self.rows += [nrow] + + def render(self, request): + return render_to_string("common/tables.html", { + "table" : self + }) + + diff --git a/main/forms.py b/main/forms.py index f267eb72e..e038c46e6 100644 --- a/main/forms.py +++ b/main/forms.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext as _ class ARmessageForm(forms.ModelForm): class Meta: model = ARmessage - fields = ('subject', 'content', 'enabled') + fields = ('subject', 'content', 'enabled', 'untildate') def __init__(self, *args, **kwargs): super(ARmessageForm, self).__init__(*args, **kwargs) diff --git a/main/models.py b/main/models.py index 4f4348057..a3e5329f0 100644 --- a/main/models.py +++ b/main/models.py @@ -7,6 +7,7 @@ class ARmessage(models.Model): subject = models.CharField(_('subject'), max_length=255) content = models.TextField(_('content')) enabled = models.BooleanField(_('enabled')) +# untildate = models.DateTimeField(_('until')) class ARhistoric(models.Model): armessage = models.ForeignKey(ARmessage) diff --git a/static/js/common.js b/static/js/common.js index 4f1e387e4..17bcf706a 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -1,6 +1,14 @@ window.addEvent('domready', function(){ - if ($('menubar')) { - $('menubar').getElements('li.dropdown').each( function( elem ){ + parse_menubar('topmenubar'); + parse_menubar('menubar'); + SqueezeBox.assign($$('a.boxed'), { + parse: 'rel', + }); +}); + +parse_menubar = function(id) { + if ($(id)) { + $(id).getElements('li.dropdown').each( function( elem ){ var list = elem.getElement('ul.links'); var myFx = new Fx.Slide(list).hide(); elem.addEvents({ @@ -15,10 +23,7 @@ window.addEvent('domready', function(){ }); }); } - SqueezeBox.assign($$('a.boxed'), { - parse: 'rel', - }); -}); +} get_iframe_body = function(id) { var saf = navigator.userAgent.match(/Safari/i); @@ -65,3 +70,38 @@ confirmation = function(question, action, callback) { }); SqueezeBox.open("/mailng/main/confirm/?question=" + question); } + +setContentSize = function(cellsize) { + var contentsize = $(document.body).getSize().y - $("topmenubar").getSize().y + - $("header").getSize().y; + + contentsize -= $("navbar").getSize().y * 2; + contentsize -= contentsize % cellsize; + $("listing").setStyle("height", contentsize + "px"); +} + +bindRows = function(rooturl, page) { + $$("tr").addEvent("click", function(event) { + if (event.target.parentNode.id) { + var url = rooturl + event.target.parentNode.id + "?page=" + page; + event.target.parentNode.setStyle("background", "#ffffcc"); + location.href = url; + } + }); +} + +toggleSelection = function(id, value) { + $$("input[name=" + id + "]").each(function(obj) { + obj.checked = value; + }); +} + +checkSelection = function(id, text) { + var getout = $$("input[name=" + id + "]").every(function(item, index) { + return item.checked == false; + }); + if (getout) { + return false; + } + return confirm(text); +} \ No newline at end of file diff --git a/static/js/domains.js b/static/js/domains.js index e59eafc99..286515bac 100644 --- a/static/js/domains.js +++ b/static/js/domains.js @@ -40,7 +40,7 @@ window.addEvent('domready', function() { }); - $('body').addEvent('click', function(event) { + $(document.body).addEvent('click', function(event) { target = event.target; if (target.get('tag') == 'a') { name = target.get('name'); @@ -54,7 +54,7 @@ window.addEvent('domready', function() { } }); - $('body').addEvent('submit', function(event) { + $(document.body).addEvent('submit', function(event) { target = event.target; if (target.get('tag') == 'form') { name = target.get('name'); diff --git a/static/js/main.js b/static/js/main.js index c89c7f9ba..97c3c0340 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -2,4 +2,9 @@ window.addEvent('domready', function() { SqueezeBox.assign($$('a.boxed'), { parse: 'rel' }); + + new DatePicker("#untildate", { + pickerClass: "datepicker_vista", + allowEmpty: true + }); }); diff --git a/static/js/mootools-more.js b/static/js/mootools-more.js index 2337e8bba..6dce4406c 100644 --- a/static/js/mootools-more.js +++ b/static/js/mootools-more.js @@ -182,6 +182,101 @@ Element.implement({ }); +/* +--- + +script: Scroller.js + +description: Class which scrolls the contents of any Element (including the window) when the mouse reaches the Element's boundaries. + +license: MIT-style license + +authors: +- Valerio Proietti + +requires: +- core:1.2.4/Events +- core:1.2.4/Options +- core:1.2.4/Element.Event +- core:1.2.4/Element.Dimensions + +provides: [Scroller] + +... +*/ + +var Scroller = new Class({ + + Implements: [Events, Options], + + options: { + area: 20, + velocity: 1, + onChange: function(x, y){ + this.element.scrollTo(x, y); + }, + fps: 50 + }, + + initialize: function(element, options){ + this.setOptions(options); + this.element = document.id(element); + this.listener = ($type(this.element) != 'element') ? document.id(this.element.getDocument().body) : this.element; + this.timer = null; + this.bound = { + attach: this.attach.bind(this), + detach: this.detach.bind(this), + getCoords: this.getCoords.bind(this) + }; + }, + + start: function(){ + this.listener.addEvents({ + mouseover: this.bound.attach, + mouseout: this.bound.detach + }); + }, + + stop: function(){ + this.listener.removeEvents({ + mouseover: this.bound.attach, + mouseout: this.bound.detach + }); + this.detach(); + this.timer = $clear(this.timer); + }, + + attach: function(){ + this.listener.addEvent('mousemove', this.bound.getCoords); + }, + + detach: function(){ + this.listener.removeEvent('mousemove', this.bound.getCoords); + this.timer = $clear(this.timer); + }, + + getCoords: function(event){ + this.page = (this.listener.get('tag') == 'body') ? event.client : event.page; + if (!this.timer) this.timer = this.scroll.periodical(Math.round(1000 / this.options.fps), this); + }, + + scroll: function(){ + var size = this.element.getSize(), + scroll = this.element.getScroll(), + pos = this.element.getOffsets(), + scrollSize = this.element.getScrollSize(), + change = {x: 0, y: 0}; + for (var z in this.page){ + if (this.page[z] < (this.options.area + pos[z]) && scroll[z] != 0) + change[z] = (this.page[z] - this.options.area - pos[z]) * this.options.velocity; + else if (this.page[z] + this.options.area > (size[z] + pos[z]) && scroll[z] + size[z] != scrollSize[z]) + change[z] = (this.page[z] - size[z] + this.options.area - pos[z]) * this.options.velocity; + } + if (change.y || change.x) this.fireEvent('change', [scroll.x + change.x, scroll.y + change.y]); + } + +}); + /* --- @@ -211,167 +306,159 @@ provides: [Tips] (function(){ var read = function(option, element){ - return (option) ? ($type(option) == 'function' ? option(element) : element.get(option)) : ''; + return (option) ? ($type(option) == 'function' ? option(element) : element.get(option)) : ''; }; this.Tips = new Class({ - Implements: [Events, Options], + Implements: [Events, Options], - options: { - /* - onAttach: $empty(element), - onDetach: $empty(element), - */ - onShow: function(){ - this.tip.setStyle('display', 'block'); - }, - onHide: function(){ - this.tip.setStyle('display', 'none'); - }, - title: 'title', - text: function(element){ - return element.get('rel') || element.get('href'); - }, - showDelay: 100, - hideDelay: 100, - className: 'tip-wrap', - offset: {x: 16, y: 16}, - fixed: false - }, + options: { + /* + onAttach: $empty(element), + onDetach: $empty(element), + */ + onShow: function(){ + this.tip.setStyle('display', 'block'); + }, + onHide: function(){ + this.tip.setStyle('display', 'none'); + }, + title: 'title', + text: function(element){ + return element.get('rel') || element.get('href'); + }, + showDelay: 100, + hideDelay: 100, + className: 'tip-wrap', + offset: {x: 16, y: 16}, + fixed: false + }, - initialize: function(){ - var params = Array.link(arguments, {options: Object.type, elements: $defined}); - this.setOptions(params.options); - document.id(this); - - if (params.elements) this.attach(params.elements); - }, + initialize: function(){ + var params = Array.link(arguments, {options: Object.type, elements: $defined}); + this.setOptions(params.options); + document.id(this); + + if (params.elements) this.attach(params.elements); + }, - toElement: function(){ - if (this.tip) return this.tip; - - this.container = new Element('div', {'class': 'tip'}); - return this.tip = new Element('div', { - 'class': this.options.className, - styles: { - position: 'absolute', - top: 0, - left: 0, - visibility: 'hidden', - display: 'none' - } - }).adopt( - new Element('div', {'class': 'tip-top'}), - this.container, - new Element('div', {'class': 'tip-bottom'}) - ).inject(document.body); - }, - - attach: function(elements){ - $$(elements).each(function(element){ - var title = read(this.options.title, element), - text = read(this.options.text, element); - - element.erase('title').store('tip:native', title).retrieve('tip:title', title); - element.retrieve('tip:text', text); - this.fireEvent('attach', [element]); - - var events = ['enter', 'leave']; - if (!this.options.fixed) events.push('move'); + toElement: function(){ + if (this.tip) return this.tip; + + this.container = new Element('div', {'class': 'tip'}); + return this.tip = new Element('div', { + 'class': this.options.className, + styles: { + position: 'absolute', + top: 0, + left: 0 + } + }).adopt( + new Element('div', {'class': 'tip-top'}), + this.container, + new Element('div', {'class': 'tip-bottom'}) + ).inject(document.body); + }, - events.each(function(value){ - var event = element.retrieve('tip:' + value); - if (!event) event = this['element' + value.capitalize()].bindWithEvent(this, element); - element.store('tip:' + value, event).addEvent('mouse' + value, event); - }, this); - - element.addEvent('mouseout', this.hide.bind(this, element)); // firefox doesn't handle mouseleave correct - - }, this); - - return this; - }, - - detach: function(elements){ - $$(elements).each(function(element){ - ['enter', 'leave', 'move'].each(function(value){ - element.removeEvent('mouse' + value, element.retrieve('tip:' + value)).eliminate('tip:' + value); - }); - - this.fireEvent('detach', [element]); - - if (this.options.title == 'title'){ // This is necessary to check if we can revert the title - var original = element.retrieve('tip:native'); - if (original) element.set('title', original); - } - }, this); - - return this; - }, - - elementEnter: function(event, element){ - this.container.empty(); - - ['title', 'text'].each(function(value){ - var content = element.retrieve('tip:' + value); - if (content) this.fill(new Element('div', {'class': 'tip-' + value}).inject(this.container), content); - }, this); - - $clear(this.timer); - this.timer = this.show.delay(this.options.showDelay, this, [element, event]); - }, - - elementLeave: function(event, element){ - this.tip.setStyle('visibility', 'hidden'); - $clear(this.timer); - this.timer = this.hide.delay(this.options.hideDelay, this, element); - this.fireForParent(event, element); - }, - - fireForParent: function(event, element){ - var element = $(element); - if (!element) return; - parentNode = element.getParent(); - if (parentNode == document.body) return; - if (parentNode.retrieve('tip:enter')) parentNode.fireEvent('mouseenter', event); - else this.fireForParent(event, parentNode); - }, - - elementMove: function(event, element) { - this.position(event); - }, + attach: function(elements){ + $$(elements).each(function(element){ + var title = read(this.options.title, element), + text = read(this.options.text, element); + + element.erase('title').store('tip:native', title).retrieve('tip:title', title); + element.retrieve('tip:text', text); + this.fireEvent('attach', [element]); + + var events = ['enter', 'leave']; + if (!this.options.fixed) events.push('move'); + + events.each(function(value){ + var event = element.retrieve('tip:' + value); + if (!event) event = this['element' + value.capitalize()].bindWithEvent(this, element); + + element.store('tip:' + value, event).addEvent('mouse' + value, event); + }, this); + }, this); + + return this; + }, + + detach: function(elements){ + $$(elements).each(function(element){ + ['enter', 'leave', 'move'].each(function(value){ + element.removeEvent('mouse' + value, element.retrieve('tip:' + value)).eliminate('tip:' + value); + }); + + this.fireEvent('detach', [element]); + + if (this.options.title == 'title'){ // This is necessary to check if we can revert the title + var original = element.retrieve('tip:native'); + if (original) element.set('title', original); + } + }, this); + + return this; + }, + + elementEnter: function(event, element){ + this.container.empty(); + + ['title', 'text'].each(function(value){ + var content = element.retrieve('tip:' + value); + if (content) this.fill(new Element('div', {'class': 'tip-' + value}).inject(this.container), content); + }, this); + + $clear(this.timer); + this.timer = this.show.delay(this.options.showDelay, this, element); + this.position((this.options.fixed) ? {page: element.getPosition()} : event); + }, + + elementLeave: function(event, element){ + $clear(this.timer); + this.timer = this.hide.delay(this.options.hideDelay, this, element); + this.fireForParent(event, element); + }, + + fireForParent: function(event, element){ + if (!element) return; + parentNode = element.getParent(); + if (parentNode == document.body) return; + if (parentNode.retrieve('tip:enter')) parentNode.fireEvent('mouseenter', event); + else this.fireForParent(parentNode, event); + }, + + elementMove: function(event, element){ + this.position(event); + }, + + position: function(event){ + var size = window.getSize(), scroll = window.getScroll(), + tip = {x: this.tip.offsetWidth, y: this.tip.offsetHeight}, + props = {x: 'left', y: 'top'}, + obj = {}; + + for (var z in props){ + obj[props[z]] = event.page[z] + this.options.offset[z]; + if ((obj[props[z]] + tip[z] - scroll[z]) > size[z]) obj[props[z]] = event.page[z] - this.options.offset[z] - tip[z]; + } + + this.tip.setStyles(obj); + }, + + fill: function(element, contents){ + if(typeof contents == 'string') element.set('html', contents); + else element.adopt(contents); + }, + + show: function(element){ + this.fireEvent('show', [this.tip, element]); + }, + + hide: function(element){ + this.fireEvent('hide', [this.tip, element]); + } - position: function(event){ - if (this.tip.getStyle('display') != 'block') return; - var size = window.getSize(), scroll = window.getScroll(), - tip = {x: this.tip.offsetWidth, y: this.tip.offsetHeight}, - props = {x: 'left', y: 'top'}, - obj = {}; - - for (var z in props){ - obj[props[z]] = event.page[z] + this.options.offset[z]; - if ((obj[props[z]] + tip[z] - scroll[z]) > size[z]) obj[props[z]] = event.page[z] - this.options.offset[z] - tip[z]; - } - - this.tip.setStyles(obj); - this.tip.setStyle('visibility', 'visible'); - }, - - fill: function(element, contents){ - if(typeof contents == 'string') element.set('html', contents); - else element.adopt(contents); - }, - - show: function(element, event) { - this.fireEvent('show', [this.tip, element]); - this.position((this.options.fixed) ? {page: element.getPosition()} : event); - }, - - hide: function(element){ - this.fireEvent('hide', [this.tip, element]); - } - }); })(); \ No newline at end of file diff --git a/static/pics/folder.png b/static/pics/folder.png new file mode 100644 index 000000000..901edc962 Binary files /dev/null and b/static/pics/folder.png differ diff --git a/static/pics/overview.png b/static/pics/overview.png new file mode 100644 index 000000000..13d2c00d0 Binary files /dev/null and b/static/pics/overview.png differ diff --git a/static/pics/webmail.png b/static/pics/webmail.png new file mode 100644 index 000000000..859251fe0 Binary files /dev/null and b/static/pics/webmail.png differ diff --git a/static/stylesheets/basic.css b/static/stylesheets/basic.css index 6529c8112..58de59d91 100644 --- a/static/stylesheets/basic.css +++ b/static/stylesheets/basic.css @@ -2,6 +2,7 @@ body { font-family: arial, helvetica, verdana, tahoma, sans-serif; font-size: small; background: #153F65; + margin: 0; } #first-container { @@ -342,3 +343,13 @@ body { margin: 20px; font-weight: bold; } + +/* nouveau style */ +#topmenubar { + width: 100%; + margin: 0 auto; + padding: 0; + background: #072342; + border-bottom: 2px solid #333; + height: 30px; +} diff --git a/static/stylesheets/default.css b/static/stylesheets/default.css new file mode 100644 index 000000000..84ef58b6c --- /dev/null +++ b/static/stylesheets/default.css @@ -0,0 +1,254 @@ +html, body { + font-family: arial, helvetica, verdana, tahoma, sans-serif; + font-size: small; + background: #153F65; + margin: 0; + padding: 0; + +} + +#topmenubar { + width: 100%; + margin: 0 auto; + padding: 0; + background: #072342; + border-bottom: 2px solid #333; + height: 30px; +} + +#menubar { + margin: 5px; + display: table; + width: 700px; + margin-top: 35px; + /*position: absolute;*/ +} + +#topmenubar a, #topmenubar a:visited { + color: white; + font-weight: bold; +} + +#topmenubar label { + color: white; + font-weight: bold; +} + +#menubar a, #menubar a:visited { + color: black; + font-weight: bold; +} + +#menubar label { + color: black; + font-weight: bold; +} + +#page { + border: 2px solid #333; + background: white; + min-height: 92%; + margin: 0 15px; +} + +#content { + overflow: auto; + padding: 5px; +} + +#header { + width: 100%; + padding: 5px 0; +} + +#header h1 { + font-size: medium; + width: 50%; + margin: 0 0 0 5px; +} + +#loginbox { + margin: 100px auto; + width: 610px; + padding-bottom: 15px; + border: 2px solid #333; + background: white; +} + +#loginbox h2 { + margin: 30px 10px 0 10px; + font-size: small; +} + +#loginbox h3 { + font-size: small; + text-align: center; +} + +#logo { + float: left; + text-align: center; + width: 300px; + margin-top: 10px; + border-right: 1px solid #ccc; +} + +#logo img { + height: 90px; +} + +#test { + float: right; + width: 300px; +} + +.menubar a, .menubar a:visited { + text-decoration: none; +} + +.menubar a:hover { + text-decoration: underline; +} + +.menubar ul { + margin: 0; + padding: 0; + list-style: none; + width: 100%; + color: #2a4069; +} + +.menubar li { + float: left; + display: inline; + margin: 5px 5px; +} + +.menubar li.selected { + background: #ffff77; +} + +.menubar img { + float: left; + margin: 0px 3px; + border: 0; +} + +.menubar .title { + color: white; +} + +.left { + text-align: left; +} + +.right { + float: right; +} + +.logo { + float: right; + /*margin: 5px 0 0 5px;*/ + height: 90px; +} + +.dropdown { + position: absolute; +} + +.dropdown ul { + display: block; + background: #ffffcc; + width: 150px; + height: 90px; + margin: 0px; + padding: 0px; + padding-top: 10px; +} + +.dropdown a, .dropdown a:visited { + color: black !important; +} + +.spacer { + clear: both; +} + +/* Mootools tooltips */ +.tip { + color: #fff; + width: 139px; + z-index: 13000; + border: 1px solid #cccccc; +} + +.tip-title { + font-weight: bold; + font-size: 11px; + margin: 0; + color: #9FD4FF; + padding: 8px 8px 4px; + background: #eee; +} + +.tip-text { + font-size: 11px; + padding: 4px 8px 8px; + background: #eee; + color: black; +} + +table { + margin: 0 auto; + width: 100%; + border-collapse: collapse; + /*border-spacing: 20px;*/ +} + +tr.odd { + background: white; +} + +tr.even { + background: #F4F6F8; +} + +th, td { + height: 20px; +} + +table th { + background: #CEDBEC; + text-align: left; + padding: 5px; +} + +table th.right { + text-align: right; +} + +table td { + border-bottom: 1px solid #C8D6E8; + /*padding: 5px;*/ +} + +input { + border: 1px solid #cccccc; + color: inherit; + /*background-color: #e9e9e9;*/ + background-color: #eee; + padding: 2px; +} + +input:hover { + color: inherit; + background-color: #ffffcc; +} + +.hr td.odd { + background: #F7F9Fb; +} + +.hr td.even { + +} \ No newline at end of file diff --git a/static/stylesheets/login.css b/static/stylesheets/login.css new file mode 100644 index 000000000..11060e5c0 --- /dev/null +++ b/static/stylesheets/login.css @@ -0,0 +1,70 @@ +html, body { + font-family: arial, helvetica, verdana, tahoma, sans-serif; + font-size: small; + background: #153F65; + margin: 0; + padding: 0; +} + +#loginbox { + margin: 100px auto; + width: 610px; + padding-bottom: 15px; + border: 5px solid #333; + background: white; +} + +#loginbox h2 { + margin: 30px 10px 0 10px; + font-size: small; +} + +#loginbox h3 { + font-size: small; + text-align: center; +} + +#logo { + float: left; + text-align: center; + width: 300px; + margin-top: 10px; + border-right: 1px solid #ccc; +} + +#logo img { + height: 90px; +} + +#test { + float: right; + width: 300px; +} + +.spacer { + clear: both; +} + +/* Mootools tooltips */ +.tip { + color: #fff; + width: 139px; + z-index: 13000; + border: 1px solid #cccccc; +} + +.tip-title { + font-weight: bold; + font-size: 11px; + margin: 0; + color: #9FD4FF; + padding: 8px 8px 4px; + background: #eee; +} + +.tip-text { + font-size: 11px; + padding: 4px 8px 8px; + background: #eee; + color: black; +} diff --git a/static/stylesheets/mailng.png b/static/stylesheets/mailng.png new file mode 100644 index 000000000..f12629da5 Binary files /dev/null and b/static/stylesheets/mailng.png differ diff --git a/templates/admin/addpermission.html b/templates/admin/addpermission.html index dfbbee887..c5108058d 100644 --- a/templates/admin/addpermission.html +++ b/templates/admin/addpermission.html @@ -26,7 +26,7 @@ window.addEvent("domready", function() { {% if error %}
{{ error }}
{% endif %} {% url admin.views.addpermission as addperm_url %}
- {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/domain_header.html b/templates/admin/domain_header.html index 347ebf4ee..d2f8e20b7 100644 --- a/templates/admin/domain_header.html +++ b/templates/admin/domain_header.html @@ -1,4 +1,4 @@ -{% extends "layout.html" %} +{% extends "nlayout.html" %} {% load i18n %} {% block html_head %} diff --git a/templates/admin/domains.html b/templates/admin/domains.html index 79c8c7e40..6e7c12414 100644 --- a/templates/admin/domains.html +++ b/templates/admin/domains.html @@ -1,9 +1,17 @@ -{% extends "layout.html" %} +{% extends "nlayout.html" %} {% load i18n %} +{% load admin_extras %} {% block html_head %} + {% endblock %} {% block middletitle %}{% trans "Available domains" %}{% endblock %} +{% block menu %}{% domains_menu "domains" perms %}{% endblock %} {% block content %}
{% if not domains %} @@ -12,13 +20,13 @@ - - + + {% for dom in domains %} - + {% url admin.views.mailboxes dom.id as domurl %} - + diff --git a/templates/admin/editalias.html b/templates/admin/editalias.html index 5993301b5..4393fd8fc 100644 --- a/templates/admin/editalias.html +++ b/templates/admin/editalias.html @@ -8,7 +8,7 @@ {% if error %}
{{ error }}
{% endif %} {% url admin.views.editalias alias.mbox.domain.id alias.id as edit_url %} - {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/editdomain.html b/templates/admin/editdomain.html index c7e71c2cf..61666f565 100644 --- a/templates/admin/editdomain.html +++ b/templates/admin/editdomain.html @@ -9,7 +9,7 @@ {% if error %}
{{ error }}
{% endif %} {% url admin.views.editdomain domain.id as dom_url %} - {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/editmailbox.html b/templates/admin/editmailbox.html index 7233ebe1d..6cbf7d6e1 100644 --- a/templates/admin/editmailbox.html +++ b/templates/admin/editmailbox.html @@ -9,7 +9,7 @@ {% if error %}
{{ error }}
{% endif %} {% url admin.views.editmailbox mbox.domain_id mbox.id as box_url %} - {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/newalias.html b/templates/admin/newalias.html index 7062ec0e1..834529f4a 100644 --- a/templates/admin/newalias.html +++ b/templates/admin/newalias.html @@ -8,7 +8,7 @@ {% if error %}
{{ error }}
{% endif %} {% url admin.views.newalias mbox.domain_id mbox.id as box_url %} - {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/newdomain.html b/templates/admin/newdomain.html index 4a06830ae..16ddfef35 100644 --- a/templates/admin/newdomain.html +++ b/templates/admin/newdomain.html @@ -9,7 +9,7 @@ {% if error %}
{{ error }}
{% endif %} {% url admin.views.newdomain as dom_url %} - {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/newmailbox.html b/templates/admin/newmailbox.html index b640b9bf6..446b78585 100644 --- a/templates/admin/newmailbox.html +++ b/templates/admin/newmailbox.html @@ -9,7 +9,7 @@ {% if error %}
{{ error }}
{% endif %} {% url admin.views.newmailbox domain.id as box_url %} - {% include "admin/genericform.html" %} + {% include "common/genericform.html" %}
diff --git a/templates/admin/parameters.html b/templates/admin/parameters.html index 9a9c3711b..523fe20cd 100644 --- a/templates/admin/parameters.html +++ b/templates/admin/parameters.html @@ -1,8 +1,4 @@ {% extends "admin/settings_header.html" %} -{% block html_head %} - - -{% endblock %} {% block menu %} {% load admin_extras %} {% settings_menu "parameters" perms %} @@ -11,7 +7,7 @@ {% load i18n %} {% url admin.views.saveparameters as action %} -
{% trans "Actions" %}{% trans "Name" %}{% trans "Mailboxes" %}{% trans "Name" %}{% trans "Mailboxes" %} {% trans "Quota" %} {% trans "Enabled" %}
{% url admin.views.editdomain dom.id as edit_url %} {{ dom.name }}{{ dom.name }} {{ dom.mboxcounter }} {{ dom.quota }} {% trans "MB" %} {{ dom.enabled|yesno:_("yes,no") }}
+
diff --git a/templates/admin/settings_header.html b/templates/admin/settings_header.html index 9748e53fd..814bfa687 100644 --- a/templates/admin/settings_header.html +++ b/templates/admin/settings_header.html @@ -1,4 +1,4 @@ -{% extends "layout.html" %} +{% extends "nlayout.html" %} {% load i18n %} {% block html_head %} diff --git a/templates/common/email_listing.html b/templates/common/email_listing.html new file mode 100644 index 000000000..3c0459666 --- /dev/null +++ b/templates/common/email_listing.html @@ -0,0 +1,57 @@ +{% extends "nlayout.html" %} +{% load i18n %} +{% block html_head %} + + +{% endblock %} +{% block content %} +{% block leftcol %}{% endblock %} + + + +
{{ listing }}
+ +
+ +{% endblock %} diff --git a/templates/admin/genericform.html b/templates/common/genericform.html similarity index 87% rename from templates/admin/genericform.html rename to templates/common/genericform.html index 5f993d07c..d07b5af55 100644 --- a/templates/admin/genericform.html +++ b/templates/common/genericform.html @@ -1,7 +1,8 @@ {% for field in form %}
{% if field.help_text %} - {% endif %} + + {% endif %} {{ field.label_tag }} {{ field }} {% ifequal noerrors False %}{{ field.errors }}{% endifequal %}
{% endfor %} diff --git a/templates/common/navbar.html b/templates/common/navbar.html new file mode 100644 index 000000000..9d6ed8b4e --- /dev/null +++ b/templates/common/navbar.html @@ -0,0 +1,19 @@ +{% load i18n %} + + +{% if page.previous_page %} + + + + + + +{% endif %} +{% if page.next_page %} + + + + + + +{% endif %} diff --git a/templates/common/tables.html b/templates/common/tables.html new file mode 100644 index 000000000..3906302ee --- /dev/null +++ b/templates/common/tables.html @@ -0,0 +1,18 @@ +
{% trans "Name" %} {% trans "Value" %}
+ + {% for c in table.columns %} + + {% endfor %} + + {% for row in table.rows %} + + {% for cell in row.cols %} + {% if cell.safe %} + + {% else %} + + {% endif %} + {% endfor %} + + {% endfor %} +
{{ c|safe }}
{{ cell.value|safe }}{{ cell.value }}
diff --git a/templates/genericform.html b/templates/genericform.html deleted file mode 100644 index 728f1c91a..000000000 --- a/templates/genericform.html +++ /dev/null @@ -1,5 +0,0 @@ -{% for field in form %} -
- {{ field.label_tag }} {{ field }} {% ifequal noerrors False %}{{ field.errors }}{% endifequal %} -
-{% endfor %} diff --git a/templates/main/autoreply.html b/templates/main/autoreply.html index 512235191..029ff881f 100644 --- a/templates/main/autoreply.html +++ b/templates/main/autoreply.html @@ -1,8 +1,10 @@ {% extends "main/header.html" %} {% load i18n %} {% block html_head %} - + + + {% endblock %} {% block menu %} {% load main_extras %} @@ -10,11 +12,11 @@ {% endblock %} {% block subcontent %}
- {% include "genericform.html" %} + {% include "common/genericform.html" %}
- +
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/main/chpassword.html b/templates/main/chpassword.html index 93aeac38b..47656ab04 100644 --- a/templates/main/chpassword.html +++ b/templates/main/chpassword.html @@ -9,10 +9,10 @@ {% if error %}
{{ error }}
{% endif %} {% url main.views.changepassword as action_url %}
- {% include "genericform.html" %} + {% include "common/genericform.html" %}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/main/header.html b/templates/main/header.html index 37d898d43..f915c2319 100644 --- a/templates/main/header.html +++ b/templates/main/header.html @@ -1,7 +1,9 @@ {% extends "layout.html" %} {% load i18n %} {% block html_head %} + + {% endblock %} {% block middletitle %}{% trans "General" %}{% endblock %} {% block content %} diff --git a/templates/main/menu.html b/templates/main/menu.html index 083ae8336..86111face 100644 --- a/templates/main/menu.html +++ b/templates/main/menu.html @@ -4,7 +4,7 @@ {% if entry.menu %}