diff --git a/.hgignore b/.hgignore index 9fd5fab25..06861c4f7 100644 --- a/.hgignore +++ b/.hgignore @@ -1,3 +1,4 @@ .*~$ ^[^\.]+\.pyc static/tmp +static/ckeditor diff --git a/admin/models.py b/admin/models.py index 732ec50a8..cb9e65a8f 100644 --- a/admin/models.py +++ b/admin/models.py @@ -115,3 +115,7 @@ class Alias(models.Model): permissions = ( ("view_aliases", "View aliases"), ) + +class Parameter(models.Model): + name = models.CharField(max_length=255) + value = models.CharField(max_length=255) diff --git a/admin/templatetags/admin_extras.py b/admin/templatetags/admin_extras.py index 4e5cbf15c..6eb5fab93 100644 --- a/admin/templatetags/admin_extras.py +++ b/admin/templatetags/admin_extras.py @@ -46,9 +46,40 @@ def domain_menu(domain_id, selection, perms): "perms" : perms}) @register.simple_tag -def settings_menu(selection): - return render_to_string('admin/settings_menu.html', - {"selection" : selection}) +def settings_menu(selection, perms): + entries = [ + {"name" : "permissions", + "url" : reverse(admin.views.settings), + "label" : _("Permissions"), + "img" : "/static/pics/permissions.png"}, + {"name" : "addperm", + "url" : reverse(admin.views.addpermission), + "img" : "/static/pics/add.png", + "label" : _("Add permission"), + "class" : "boxed", + "rel" : "{handler:'iframe',size:{x:320,y:210}}"}, + {"name" : "parameters", + "url" : reverse(admin.views.viewparameters), + "img" : "/static/pics/domains.png", + "label" : _("Parameters")}, + ] + return render_to_string('main/menu.html', + {"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): @@ -57,6 +88,46 @@ def loadadminextmenu(perms): return render_to_string('main/menulist.html', {"menu" : menu, "perms" : perms}) +@register.simple_tag +def param(app, definition): + result = """
+ """ % definition["name"] + name = "%s.%s" % (app, definition["name"]) + if definition["type"] in ["string", "int"]: + value = definition.has_key("value") \ + and definition["value"] or definition["default"] + result += """ + """ % (name, name, value) + if definition["type"] in ["list", "list_yesno"]: + result += """ + +""" + + if definition.has_key("help"): + result += """ + + + """ % (definition["help"], _("Help")) + result += """ +
+""" + return result + @register.filter def gender(value, target): if genders.has_key(value): diff --git a/admin/urls.py b/admin/urls.py index 770268375..cb296bf2f 100644 --- a/admin/urls.py +++ b/admin/urls.py @@ -18,5 +18,7 @@ urlpatterns = patterns('mailng.admin.views', (r'^domains/(?P\d+)/delalias/(?P\d+)/$', 'delalias'), (r'^settings/$', 'settings'), (r'^settings/addperm/$', 'addpermission'), - (r'^settings/delperm/(?P\d+)/(?P\w+)/$', 'deletepermission') + (r'^settings/delperm/(?P\d+)/(?P\w+)/$', 'deletepermission'), + (r'^settings/parameters/$', 'viewparameters'), + (r'^settings/parameters/save/$', 'saveparameters'), ) diff --git a/admin/views.py b/admin/views.py index 5c1cfa794..2d8492211 100644 --- a/admin/views.py +++ b/admin/views.py @@ -11,11 +11,11 @@ from django.contrib.auth.decorators \ import login_required, permission_required, user_passes_test from django.contrib.auth.models import User, Group from mailng import admin, main -from mailng.admin.models import Domain, Mailbox, Alias +from mailng.admin.models import Domain, Mailbox, Alias, Parameter from forms import MailboxForm, DomainForm, AliasForm, PermissionForm from mailng.lib.authbackends import crypt_password from mailng.lib import _render, _ctx_ok, _ctx_ko -from mailng.lib import events +from mailng.lib import events, parameters import string import copy @@ -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: @@ -404,3 +404,33 @@ def deletepermission(request, mbox_id, group): mbox.user.groups.remove(grp) mbox.user.save() return HttpResponseRedirect(reverse(admin.views.settings)) + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def viewparameters(request): + apps = sorted(parameters._params.keys()) + gparams = [] + for app in apps: + tmp = {"name" : app, "params" : []} + for p in sorted(parameters._params[app]): + tmp["params"] += [{"name" : p, + "value" : parameters.get(app, p), + "help" : parameters._params[app][p]["help"], + "default" : parameters._params[app][p]["default"], + "type" : parameters._params[app][p]["type"]}] + gparams += [tmp] + + return _render(request, 'admin/parameters.html', { + "gparams" : gparams + }) + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def saveparameters(request): + for pname, v in request.POST.iteritems(): + if pname == "update": + continue + app, name = pname.split('.') + parameters.save(app, name, v) + request.user.message_set.create(message=_("Configuration saved.")) + return HttpResponseRedirect(reverse(admin.views.viewparameters)) 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..52d127346 --- /dev/null +++ b/auth/views.py @@ -0,0 +1,37 @@ +# -*- 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 _ +from mailng.lib import events +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) + + events.raiseEvent("UserLogin", request=request, + username=request.POST["username"], + 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/sql_listing.py b/extensions/amavis_quarantine/sql_listing.py new file mode 100644 index 000000000..5e29fc3e7 --- /dev/null +++ b/extensions/amavis_quarantine/sql_listing.py @@ -0,0 +1,89 @@ +# -*- 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) + + def parse(self, header, value): + try: + return getattr(self, "parse_%s" % header)(value) + except AttributeError: + return 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" + tbltype = Qtable + + def __init__(self, filter, **kwargs): + self.mbc = SQLconnector(filter) + EmailListing.__init__(self, **kwargs) 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/stats/grapher.py b/extensions/stats/grapher.py index 439e1273e..3761456c9 100644 --- a/extensions/stats/grapher.py +++ b/extensions/stats/grapher.py @@ -49,6 +49,10 @@ size_avg_template = { } } +tpl = {'traffic':traffic_avg_template, + 'badtraffic':badtraffic_avg_template, + 'size':size_avg_template} + class Grapher(object): def __init__(self): self.rrd_rootdir = getoption("RRD_ROOTDIR", "/tmp") @@ -57,9 +61,10 @@ class Grapher(object): def process(self, target, suffix, start, end, tpl=traffic_avg_template): rrdfile = "%s/%s.rrd" % (self.rrd_rootdir, target) if not os.path.exists(rrdfile): + print "[graph] no rrd file %s detected" %rrdfile return ext = "png" - path = "%s/%s_%s_%s_%s.%s" % (self.img_rootdir, tpl['name'], + path = "%s/%s_%s_%s_%s.%s" % (self.img_rootdir, tpl['name'], target, tpl['cf'], suffix, ext) start = str(start) end = str(end) @@ -69,10 +74,10 @@ class Grapher(object): defs += ['DEF:%s=%s:%s:%s' % (v, rrdfile, v, tpl['cf']), 'CDEF:%spm=%s,60,*' % (v, v)] type = d.has_key("type") and d["type"] or "LINE" - lines += ["%s:%spm%s:%s" % (type, v, d["color"], + lines += ["%s:%spm%s:%s" % (type, v, d["color"], d["legend"].encode("utf-8"))] params = defs + lines - rrdtool.graph(path, + rrdtool.graph(str(path), "--imgformat", "PNG", "--width", tpl["width"], "--height", tpl["height"], @@ -84,6 +89,11 @@ class Grapher(object): "--units-exponent", "0", *params) + if not os.path.exists(path): + print "[graph] Impossible to create %s graph" %path + else: + print "[graph] %s graph created" %path + def make_defaults(self, target, tpl=traffic_avg_template): end = "%d" % int(time.mktime(time.localtime())) self.process(target, "day", "-1day", end, tpl) diff --git a/extensions/stats/logparser.py b/extensions/stats/logparser.py index 375deedf6..a3953de81 100755 --- a/extensions/stats/logparser.py +++ b/extensions/stats/logparser.py @@ -5,7 +5,7 @@ import sys import os import re import rrdtool -import string +import string, pdb from optparse import OptionParser from mailng.lib import getoption from mailng.admin.models import Domain @@ -35,6 +35,25 @@ points_per_sample = 3 variables = ["sent", "recv", "bounced", "reject", "spam", "virus", "size_sent", "size_recv"] +def str2Time(y, M, d, h ="0", m ="0", s="0"): + """str2Time + + return epoch time from Year Month Day Hour:Minute:Second time format + """ + try: + local = time.strptime("%s %s %s %s:%s:%s" %(y, M, d, h, m, s), \ + "%Y %b %d %H:%M:%S") + except: + # try with 01-12 month format + try: + local = time.strptime("%s %s %s %s:%s:%s" %(y, M, d, h, m, s), \ + "%Y %m %d %H:%M:%S") + except: + print "[rrd] ERROR unrecognized %s time format" %(y, M, d, h, m, s) + return 0 + return int(time.mktime(local)) + + class LogParser(object): def __init__(self, logfile, workdir, year=None, debug=False, verbose=False): @@ -42,17 +61,20 @@ class LogParser(object): try: self.f = open(logfile) except IOError, errno: - print "%s" % errno + if self.debug: + print "%s" % errno sys.exit(1) self.workdir = workdir self.year = year self.debug = debug self.verbose = verbose self.cfs = ['AVERAGE', 'MAX'] - + self.last_month = None if not self.year: self.year = time.localtime().tm_year + if self.debug: + print "[rrd] Dealing with year %s" %self.year self.data = {} domains = Domain.objects.all() self.domains = [] @@ -65,19 +87,6 @@ class LogParser(object): self.lupdates = {} self.line_expr = re.compile("(\w+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(\w+)\s+(\w+)/?\w*[[](\d+)[]]:\s+(.*)") - def str2Time(self, y, M, d, h, m, s): - """str2Time - - return epoch time from Year Month Day Hour:Minute:Second time format - """ - try: - local = time.strptime("%s %s %s %s:%s:%s" %(y, M, d, h, m, s), \ - "%Y %b %d %H:%M:%S") - except: - print "[rrd] ERROR unrecognized %s time format" %(y, M, d, h, m, s) - return 0 - return int(time.mktime(local)) - def init_rrd(self, fname, m): """init_rrd @@ -128,7 +137,8 @@ class LogParser(object): m = t - (t % rrdstep) if not os.path.exists(fname): self.lupdates[fname] = self.init_rrd(fname, m) - print "[rrd] create new RRD file" + if self.debug: + print "[rrd] create new RRD file %s" %fname else: if not self.lupdates.has_key(fname): self.lupdates[fname] = rrdtool.last(fname) @@ -186,60 +196,76 @@ class LogParser(object): self.data["global"][cur_t][counter] += val def process(self): + id_expr = re.compile("(\w{9,18}): (.*)") + prev_se = -1 + prev_mi = -1 + prev_ho = -1 for line in self.f.readlines(): m = self.line_expr.match(line) if not m: continue (mo, da, ho, mi, se, host, prog, pid, log) = m.groups() - se = int(int(se) / rrdstep) # rrd step is one-minute => se = 0 - cur_t = self.str2Time(self.year, mo, da, ho, mi, se) - cur_t = cur_t - cur_t % rrdstep - m = re.search("(\w{10}): message-id=<([^>]*)>", log) + if prev_se != se or prev_mi != mi or prev_ho != ho: + cur_t = str2Time(self.year, mo, da, ho, mi, se) + cur_t = cur_t - cur_t % rrdstep + prev_mi = mi + prev_ho = ho + prev_se = se + m = id_expr.search(log) if m: - self.workdict[m.group(1)] = {'from' : m.group(2), 'size' : 0} - continue + (line_id, line_log) = m.groups() - m = re.search("(\w{10}): from=<([^>]*)>, size=(\d+)", log) - if m: - self.workdict[m.group(1)] = {'from' : m.group(2), - 'size' : string.atoi(m.group(3))} - continue - - m = re.search("(\w{10}): to=<([^>]*)>.*status=(\S+)", log) - if m: - if not self.workdict.has_key(m.group(1)): - print "Inconsistent mail (%s: %s), skipping" % (m.group(1), m.group(2)) + m = re.search("message-id=<([^>]*)>", line_log) + if m: + self.workdict[line_id] = {'from' : m.group(1), 'size' : 0} continue - if not m.group(3) in variables: - print "Unsupported status %s, skipping" % m.group(3) + + m = re.search("from=<([^>]*)>, size=(\d+)", line_log) + if m: + self.workdict[line_id] = {'from' : m.group(1), + 'size' : string.atoi(m.group(2))} continue - addrfrom = re.match("([^@]+)@(.+)", self.workdict[m.group(1)]['from']) - if addrfrom and addrfrom.group(2) in self.domains: - self.inc_counter(addrfrom.group(2), cur_t, 'sent') - self.inc_counter(addrfrom.group(2), cur_t, 'size_sent', - self.workdict[m.group(1)]['size']) - addrto = re.match("([^@]+)@(.+)", m.group(2)) - if addrto.group(2) in self.domains: - if m.group(3) == "sent": - self.inc_counter(addrto.group(2), cur_t, 'recv') - self.inc_counter(addrto.group(2), cur_t, 'size_recv', - self.workdict[m.group(1)]['size']) - else: - self.inc_counter(addrto.group(2), cur_t, m.group(3)) - continue - - m = re.search("NOQUEUE: reject: .*from=<(.*)> to=<([^>]*)>", log) - if m: - addrto = re.match("([^@]+)@(.+)", m.group(2)) - if addrto and addrto.group(2) in self.domains: - self.inc_counter(addrto.group(2), cur_t, 'reject') - continue - + + m = re.search("to=<([^>]*)>.*status=(\S+)", line_log) + if m: + if not self.workdict.has_key(line_id): + if self.debug: + print "Inconsistent mail (%s: %s), skipping" % (line_id, m.group(1)) + continue + if not m.group(2) in variables: + if self.debug: + print "Unsupported status %s, skipping" % m.group(2) + continue + + addrfrom = re.match("([^@]+)@(.+)", self.workdict[line_id]['from']) + if addrfrom and addrfrom.group(2) in self.domains: + self.inc_counter(addrfrom.group(2), cur_t, 'sent') + self.inc_counter(addrfrom.group(2), cur_t, 'size_sent', + self.workdict[line_id]['size']) + addrto = re.match("([^@]+)@(.+)", m.group(1)) + if addrto.group(2) in self.domains: + if m.group(2) == "sent": + self.inc_counter(addrto.group(2), cur_t, 'recv') + self.inc_counter(addrto.group(2), cur_t, 'size_recv', + self.workdict[line_id]['size']) + else: + self.inc_counter(addrto.group(2), cur_t, m.group(2)) + continue + else: + m = re.search("NOQUEUE: reject: .*from=<(.*)> to=<([^>]*)>", log) + if m: + addrto = re.match("([^@]+)@(.+)", m.group(2)) + if addrto and addrto.group(2) in self.domains: + self.inc_counter(addrto.group(2), cur_t, 'reject') + continue + # Sort everything by time G = grapher.Grapher() for dom, data in self.data.iteritems(): + if self.debug: + print "[rrd] dealing with domain %s" %dom sortedData = {} sortedData = [ (i, data[i]) for i in sorted(data.keys()) ] for t, dict in sortedData: @@ -256,12 +282,12 @@ if __name__ == "__main__": parser = OptionParser() parser.add_option("-l","--logFile", default=log_file, help="postfix log in syslog format", metavar="FILE") - parser.add_option("-v","--verbose", default=False, action="store_true", + parser.add_option("-v","--verbose", default=False, action="store_true", dest="verbose", help="set verbose mode") - parser.add_option("-d","--debug", default=False, action="store_true", + parser.add_option("-d","--debug", default=False, action="store_true", dest="debug", help="set debug mode") (options, args) = parser.parse_args() - + P = LogParser(options.logFile, rrd_rootdir, debug=options.debug, verbose=options.verbose) P.process() diff --git a/extensions/stats/main.py b/extensions/stats/main.py index a7d1f65a9..d08aa7947 100644 --- a/extensions/stats/main.py +++ b/extensions/stats/main.py @@ -7,19 +7,33 @@ This module provides rrdtool support to retreive statistics from postfix log : sent, received, bounced, rejected """ +import calendar from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from django.conf.urls.defaults import * -from mailng.lib import events, _render +from mailng.lib import events, parameters, _render from mailng.admin.views import good_domain from mailng.admin.models import Domain, Mailbox from django.contrib.auth.decorators \ import login_required, user_passes_test, permission_required +from extensions.stats.grapher import Grapher, tpl +from extensions.stats.logparser import str2Time +from mailng.lib import getoption graph_types = ['AVERAGE', 'MAX'] +graph_list = [{"name" : "traffic", "label" : _("Average normal traffic")}, + {"name" : "badtraffic", "label" : _("Average bad traffic")}, + {"name" : "size", "label" : _("Average normal traffic size")}] + def init(): events.register("AdminMenuDisplay", menu) + parameters.register("stats", "LOGFILE", "string", "/var/log/mail.log", + help=_("Path to log file used to collect statistics")) + parameters.register("stats", "RRD_ROOTDIR", "string", "/tmp/mailng", + help=_("Path to directory where RRD files are stored")) + parameters.register("stats", "IMG_ROOTDIR", "string", "/tmp/mailng", + help=_("Path to directory where PNG files are stored")) def urls(): return (r'^mailng/stats/', @@ -77,26 +91,87 @@ def graph_display(request,dom_id,graph_t=graph_types): return _render(request, 'stats/index.html', { "page" : "Domain statistics", "graph" : graph_nature, - "domain":domain,"domains" : domains, "messages" : errors , - "types" : graph_type, "tmp_path" : tmp_path}) + "domain": domain, "domains" : domains, "messages" : errors, + "types" : graph_type, "tmp_path" : tmp_path, + "img_rootdir" : getoption("IMG_ROOTDIR", "/tmp")}) @login_required @user_passes_test(lambda u: u.is_superuser) def adminindex(request): + CH = calendar.HTMLCalendar(calendar.MONDAY) + CH_print = CH.formatmonth(2009,11) period = request.GET.has_key("period") and request.GET["period"] or "day" domain = request.GET.has_key("domain") and request.GET["domain"] or "global" if domain != "global": domain = Domain.objects.get(pk=domain) domain = domain.name domains = Domain.objects.all() + G = Grapher() + for name, t in tpl.iteritems(): + G.make_defaults(domain, t) + return _render(request, 'stats/adminindex.html', { + "domains" : domains, "domain" : domain, + "graphs" : graph_list, + "periods" : ["day", "week", "month", "year","Custom"], + "period" : period, "cal" : CH_print, + "img_rootdir" : getoption("IMG_ROOTDIR", "/tmp")}) + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def custom_period(request): + start = None + end = None + start_ref = None + end_ref = None + domains = Domain.objects.all() + domain = request.GET.has_key("domain") and request.GET["domain"] or "global" + period = request.GET.has_key("period") and request.GET["period"] or "Custom" + + if domain != "global": + domain = Domain.objects.get(pk=domain) + domain = domain.name + try: + start_ref = str(request.POST.get('start')) + start = start_ref.split(', ')[1] + end_ref = str(request.POST.get('end')) + end = end_ref.split(', ')[1] + except: + pass + + if not end or not start: + return _render(request, 'stats/adminindex.html', { "domains" : domains, "domain" : domain, - "graphs" : [{"name" : "traffic", "label" : _("Average normal traffic")}, - {"name" : "badtraffic", "label" : _("Average bad traffic")}, - {"name" : "size", "label" : _("Average normal traffic size")}], - "periods" : ["day", "week", "month", "year"], - "period" : period - }) + "graphs" : graph_list, + "period" : period, + "periods" : ["day", "week", "month", "year","Custom"], + "period_name":None, + "start" : start_ref, + "end" : end_ref, + "messages" : ["Custom period not selected"], + "img_rootdir" : getoption("IMG_ROOTDIR", "/tmp")}) + + period_name = "%s_%s" %(start.replace('/',''),end.replace('/','')) + G = Grapher() + for tpl_name in graph_list: + G.process(domain, + period_name, + str2Time(*start.split('/')), + str2Time(*end.split('/')), + tpl[tpl_name['name']]) + + print "[stats] ", period + return _render(request, 'stats/adminindex.html', { + "domains" : domains, "domain" : domain, + "graphs" : graph_list, + "period" : period, + "periods" : ["day", "week", "month", "year","Custom"], + "period_name": period_name, + "start" : start_ref, + "end" : end_ref, + "messages" : None, + "img_rootdir" : getoption("IMG_ROOTDIR", "/tmp")}) + @login_required @good_domain @@ -108,10 +183,8 @@ def index(request, dom_id=None): domain = Domain.objects.get(pk=dom_id) return _render(request, 'stats/index.html', { - "domain" : domain, - "graphs" : [{"name" : "traffic", "label" : _("Average normal traffic")}, - {"name" : "badtraffic", "label" : _("Average bad traffic")}, - {"name" : "size", "label" : _("Average normal traffic size")}], - "periods" : ["day", "week", "month", "year"], - "period" : period - }) + "domain" : domain, + "graphs" : graph_list, + "periods" : ["day", "week", "month", "year","Custom"], + "period" : period, + "img_rootdir" : getoption("IMG_ROOTDIR", "/tmp")}) diff --git a/extensions/stats/templates/adminindex.html b/extensions/stats/templates/adminindex.html index 25cf9ff8e..2e7a6f1ce 100644 --- a/extensions/stats/templates/adminindex.html +++ b/extensions/stats/templates/adminindex.html @@ -1,9 +1,9 @@ -{% extends "layout.html" %} +{% extends "nlayout.html" %} {% load i18n %} {% load stats_extra %} {% block html_head %} - + @@ -27,6 +27,7 @@ window.addEvent("domready", function() { }); }); + {% endblock %} {% block middletitle %}{% trans "Global statistics" %}{% endblock %} {% block content %} @@ -46,7 +47,15 @@ window.addEvent("domready", function() { {% endfor %} + {% ifequal period "Custom" %} + {% include "stats/calendar.html" %} + {% endifequal %} + {% if start or end %} +

{% trans "Custom period : from" %} {{ start }} {% trans "to" %} {{ end }}

+ {% endif %} +
{% include "stats/graphs.html" %} +
{% endblock %} diff --git a/extensions/stats/templates/calendar.html b/extensions/stats/templates/calendar.html new file mode 100644 index 000000000..9add75625 --- /dev/null +++ b/extensions/stats/templates/calendar.html @@ -0,0 +1,29 @@ +{% load i18n %} +
+ {% url extensions.stats.main.custom_period as c_stats_url %} +
+ {% trans "Start" %}: + {% trans "End" %}: + +
+
+ + + diff --git a/extensions/stats/templates/graphs.html b/extensions/stats/templates/graphs.html index 4643d2192..c3d6a2a19 100644 --- a/extensions/stats/templates/graphs.html +++ b/extensions/stats/templates/graphs.html @@ -1,6 +1,6 @@ {% load i18n %} {% for elems in graphs %}

{{ elems.label }}

-

{% trans 'No statistics available' %}

{% endfor %} diff --git a/extensions/stats/templates/index.html b/extensions/stats/templates/index.html index 374e780cb..9273a3b4d 100644 --- a/extensions/stats/templates/index.html +++ b/extensions/stats/templates/index.html @@ -2,10 +2,10 @@ {% load i18n %} {% block html_head %} - + @@ -17,6 +17,7 @@ window.addEvent("domready", function() { }); }); + {% endblock %} {% block menu %} @@ -33,7 +34,15 @@ window.addEvent("domready", function() { {% endfor %} + {% ifequal period "Custom" %} + {% include "stats/calendar.html" %} + {% endifequal %} + {% if start or end %} +

{% trans "Custom period : from" %} {{ start }} {% trans "to" %} {{ end }}

+ {% endif %} +
{% include "stats/graphs.html" %} +
{% endblock %} diff --git a/extensions/stats/urls.py b/extensions/stats/urls.py index 904b014c4..b72709229 100644 --- a/extensions/stats/urls.py +++ b/extensions/stats/urls.py @@ -2,6 +2,7 @@ from django.conf.urls.defaults import * urlpatterns = patterns('mailng.extensions.stats.main', url(r'^$', 'adminindex', name='fullindex'), + url(r'^custom_period$', 'custom_period'), url(r'^(?P\d+)/$', 'index', name='domindex'), (r'^display/(?P\d+)/(?P\w+)/$', 'graph_display'), (r'^domain/(?P\d+)/$', 'domain'), diff --git a/extensions/webmail/__init__.py b/extensions/webmail/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/webmail/forms.py b/extensions/webmail/forms.py new file mode 100644 index 000000000..21c2bd86f --- /dev/null +++ b/extensions/webmail/forms.py @@ -0,0 +1,8 @@ +from django import forms +from django.utils.translation import ugettext as _ + +class ComposeMailForm(forms.Form): + from_ = forms.CharField(label=_("From")) + to = forms.CharField(label=_("To"), widget=forms.Textarea()) + subject = forms.CharField(label=_("Subject"), max_length=255) + diff --git a/extensions/webmail/imap_listing.py b/extensions/webmail/imap_listing.py new file mode 100644 index 000000000..8656cda15 --- /dev/null +++ b/extensions/webmail/imap_listing.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- + +import re +import time +import imaplib +import ssl +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, Singleton +from mailng.lib.email_listing import MBconnector, EmailListing +from mailng.lib import tables, imap_utf7, parameters + + +class WMtable(tables.Table): + tableid = "emails" + idkey = "imapid" +# selection = tables.SelectionColumn("selection", width="20px", first=True) + _1_flags = tables.ImgColumn("flags", width="6%") + _2_subject = tables.Column("subject", label=_("Subject"), + cssclass="draggable", width="50%") + _3_from_ = tables.Column("from", width="20%", label=_("From")) + _4_date = tables.Column("date", width="20%", label=_("Date")) + + + def parse(self, header, value): + try: + return getattr(IMAPheader, "parse_%s" % header)(value) + except AttributeError: + return value + +class IMAPheader(object): + addr_exp = re.compile("([^<\(]+)<|\(([^>\)]+)>|\)") + name_exp = re.compile("\"(.+)\"") + + @staticmethod + def parse_address(value): + m = IMAPheader.addr_exp.match(value) + if m: + name = m.group(1) + name = name.strip() + m2 = IMAPheader.name_exp.match(name) + if m2: + return IMAPheader.parse_subject(m2.group(1)) + return IMAPheader.parse_subject(name) + return value + + @staticmethod + def parse_address_list(values): + lst = values.split(",") + result = "" + for addr in lst: + if result != "": + result += ", " + result += IMAPheader.parse_address(addr) + return result + + @staticmethod + def parse_from(value): + return IMAPheader.parse_address(value) + + @staticmethod + def parse_to(value): + return IMAPheader.parse_address_list(value) + + @staticmethod + def parse_cc(value): + return IMAPheader.parse_address_list(value) + + @staticmethod + def parse_date(value): + tmp = email.utils.parsedate_tz(value) + if not tmp: + return value + ndate = datetime(*(tmp)[:7]) + now = datetime.now() + if now - ndate > timedelta(7): + return ndate.strftime("%d.%m.%Y %H:%M") + return ndate.strftime("%a %H:%M") + + @staticmethod + def parse_subject(value): + res = "" + dcd = decode_header(value) + for part in dcd: + if res != "": + res += " " + res += part[0].strip(" ") + return decode(res) + +class IMAPconnector(object): + __metaclass__ = Singleton + + def __init__(self, request=None, user=None, password=None): + self.address = parameters.get("webmail", "IMAP_SERVER") + self.port = int(parameters.get("webmail", "IMAP_PORT")) + if request: + status, msg = self.login(request.user.username, + request.session["password"]) + else: + status, msg = self.login(user, password) + if not status: + raise Exception(msg) + + def login(self, user, passwd): + import imaplib + try: + secured = parameters.get("webmail", "IMAP_SECURED") + if secured == "yes": + self.m = imaplib.IMAP4_SSL(self.address, self.port) + else: + self.m = imaplib.IMAP4(self.address, self.port) + except (imaplib.IMAP4.error, ssl.SSLError), error: + return False, _("Connection to IMAP server failed, check your configuration") + try: + self.m.login(user, passwd) + except (imaplib.IMAP4.error, ssl.SSLError), error: + return False, _("Authentication failed, check your configuration") + + return True, None + + def _encodefolder(self, folder): + if not folder: + return "INBOX" + return folder.encode("imap4-utf-7") + + def messages_count(self, folder=None, order=None): + if order: + sign = order[:1] + criterion = order[1:].upper() + if sign == '-': + criterion = "REVERSE %s" % criterion + else: + criterion = "REVERSE DATE" + print criterion + + (status, data) = self.m.select(self._encodefolder(folder)) + (status, data) = self.m.sort("(%s)" % criterion, "UTF-8", "(NOT DELETED)") + self.messages = data[0].split() + return len(self.messages) + + 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 msgseen(self, imapid): + folder, id = imapid.split("/") + self.m.store(id, "+FLAGS", "\\Seen") + + def move(self, msgset, oldfolder, newfolder): + self.m.select(self._encodefolder(oldfolder), True) + self.m.copy(msgset, newfolder) + self.m.store(msgset, "+FLAGS", "\\Deleted") + + def empty(self, folder): + self.m.select(self._encodefolder(folder)) + typ, data = self.m.search(None, 'ALL') + for num in data[0].split(): + self.m.store(num, "+FLAGS", "\\Deleted") + self.m.expunge() + + def fetch(self, start=None, stop=None, folder=None, all=False): + if not start and not stop: + return [] + result = [] + self.m.select(self._encodefolder(folder), True) + if start and stop: + submessages = self.messages[start - 1:stop] + range = ",".join(submessages) + else: + submessages = [start] + range = start + if not all: + query = '(FLAGS BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])' + else: + query = '(RFC822)' + typ, data = self.m.fetch(range, query) + if not folder: + folder = "INBOX" + tmpdict = {} + for response_part in data: + if isinstance(response_part, tuple): + imapid = response_part[0].split()[0] + flags = imaplib.ParseFlags(response_part[0]) + msg = email.message_from_string(response_part[1]) + msg["imapid"] = "%s/%s" % (folder, imapid) + if not "\\Seen" in flags: + msg["class"] = "unseen" + if "\\Answered" in flags: + msg["img_flags"] = "/static/pics/answered.png" + tmpdict[imapid] = msg + for id in submessages: + result += [tmpdict[id]] + return result + +class ImapListing(EmailListing): + tpl = "webmail/index.html" + tbltype = WMtable + + def __init__(self, user, password, baseurl=None, navparams={}, **kwargs): + self.mbc = IMAPconnector(user=user, password=password) + self.navparams = navparams + EmailListing.__init__(self, **kwargs) + if baseurl: + self.paginator.baseurl = baseurl + + 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 diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py new file mode 100644 index 000000000..2f5a3c058 --- /dev/null +++ b/extensions/webmail/main.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +import time +import sys +from django.http import HttpResponse +from django.template.loader import render_to_string +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, _render, _render_error +from mailng.lib.email_listing import Email +from imap_listing import * +from forms import * +from templatetags.webextras import * + +def init(): + events.register("UserMenuDisplay", menu) + events.register("UserLogin", userlogin) + parameters.register("webmail", "IMAP_SERVER", "string", "127.0.0.1", + help=_("Address of your IMAP server")) + parameters.register("webmail", "IMAP_SECURED", "list_yesno", "no", + help=_("Use a secure connexion to access IMAP server")) + parameters.register("webmail", "IMAP_PORT", "int", "143", + help=_("Listening port of your IMAP server")) + parameters.register("webmail", "SMTP_SERVER", "string", "127.0.0.1", + help=_("Address of your SMTP server")) + parameters.register("webmail", "SMTP_PORT", "int", "25", + help=_("Listening port of your SMTP server")) + +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"} + ] + +def userlogin(**kwargs): + kwargs["request"].session["password"] = kwargs["password"] + +def getctx(status, level=1, **kwargs): + callername = sys._getframe(level).f_code.co_name + ctx = {"status" : status, "callback" : callername} + for kw, v in kwargs.iteritems(): + ctx[kw] = v + return ctx + +@login_required +def folder(request, name, updatenav=True): + if not name: + name = "INBOX" + order = request.GET.has_key("order") and request.GET["order"] or None + if updatenav: + pageid = request.GET.has_key("page") and int(request.GET["page"]) or 1 + if not "navparams" in request.session.keys(): + request.session["navparams"] = {} + request.session["folder"] = name + request.session["page"] = pageid + if order: + request.session["navparams"]["order"] = order + + lst = ImapListing(request.user.username, request.session["password"], + name, folder=name, order=order) + page = lst.paginator.getpage(request.session["page"]) + if page: + content = lst.fetch(request, page.id_start, page.id_stop) + navbar = lst.render_navbar(page) + else: + content = "Empty folder" + navbar = "" + ctx = getctx("ok", listing=content, navbar=navbar, + menu=listing_menu("", name, request.user.get_all_permissions())) + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") + +@login_required +def index(request): + try: + navp = request.session.has_key("navparams") \ + and request.session["navparams"] or {} + lst = ImapListing(request.user.username, request.session["password"], + "INBOX", navparams=navp, folder="INBOX") + except Exception as exp: + return _render_error(request, {"error" : exp}) + return lst.render(request, empty=True) + +def fetchmail(request, folder, mail_id, all=False): + res = IMAPconnector(request).fetch(start=mail_id, folder=folder, all=all) + if len(res): + return res[0] + return None + +@login_required +def viewmail(request, folder, mail_id=None): + header = fetchmail(request, folder, mail_id) + if header: + from templatetags.webextras import viewm_menu + try: + pageid = request.session["page"] + except KeyError: + pageid = "1" + menu = viewm_menu("", request.session, mail_id, + request.user.get_all_permissions()) + folder, imapid = header["imapid"].split("/") + mailcontent = render_to_string("webmail/viewmail.html", { + "header" : header, "folder" : folder, "imapid" : imapid + }) + ctx = getctx("ok", menu=menu, listing=mailcontent) + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") + +@login_required +def getmailcontent(request, folder, mail_id): + from mailng.lib.email_listing import Email + + msg = fetchmail(request, folder, mail_id, True) + if "class" in msg.keys() and msg["class"] == "unseen": + IMAPconnector(request).msgseen(msg["imapid"]) + email = Email(msg, mode="html", links="1") + return email.render(request) + +@login_required +def move(request): + for arg in ["msgset", "to"]: + if not request.GET.has_key(arg): + return + folder, id = request.GET["msgset"].split("/") + mbc = IMAPconnector(request) + mbc.move(id, folder, request.GET["to"]) + return HttpResponse(simplejson.dumps({"status" : "ok"}), + mimetype="application/json") + +@login_required +def empty(request, name): + mbc = IMAPconnector(request) + mbc.empty(name) + return folder(request, name, False) + +def render_compose(request, form, bodyheader=None, body=None): + menu = compose_menu("", request.session, request.user.get_all_permissions()) + content = render_to_string("webmail/compose.html", { + "form" : form, "bodyheader" : bodyheader, "body" : body + }) + ctx = getctx("ok", level=2, menu=menu, listing=content) + return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") + +@login_required +def compose(request): + if request.method == "POST": + form = ComposeMailForm(request.POST) + if form.is_valid(): + from email.mime.text import MIMEText + import smtplib + + msg = MIMEText(request.POST["id_body"].encode("utf-8"), + _charset="utf-8") + msg["Subject"] = request.POST["subject"] + msg["From"] = request.POST["from_"] + msg["to"] = request.POST["to"] + s = smtplib.SMTP(parameters.get("webmail", "SMTP_SERVER")) + s.sendmail(msg['From'], [msg['To']], msg.as_string()) + s.quit() + return folder(request, request.session["navparams"]["folder"], False) + + form = ComposeMailForm() + form.fields["from_"].initial = request.user.username + return render_compose(request, form) + +@login_required +def reply(request, folder, mail_id): + if request.method == "POST": + return + msg = fetchmail(request, folder, mail_id, True) + email = Email(msg) + lines = email.body.split('\n') + body = "" + for l in lines: + if body != "": + body += "\n" + body += ">%s" % l + form = ComposeMailForm() + form.fields["from_"].initial = request.user.username + form.fields["to"].initial = msg["From"] + m = re.match("re\s*:\s*.+", msg["Subject"].lower()) + if m: + form.fields["subject"].initial = msg["Subject"] + else: + form.fields["subject"].initial = "Re: %s" % msg["Subject"] + textheader = msg["From"] + " " + _("wrote:") + return render_compose(request, form, textheader, body) diff --git a/extensions/webmail/templates/compose.html b/extensions/webmail/templates/compose.html new file mode 100644 index 000000000..ef6a5e458 --- /dev/null +++ b/extensions/webmail/templates/compose.html @@ -0,0 +1,50 @@ +{% load i18n %} + + +{% url extensions.webmail.main.compose as compurl %} +
+
+
+ {% include "common/genericform.html" %} +
+ +
+
diff --git a/extensions/webmail/templates/headers.html b/extensions/webmail/templates/headers.html new file mode 100644 index 000000000..244d108df --- /dev/null +++ b/extensions/webmail/templates/headers.html @@ -0,0 +1,5 @@ + + {% for hdr in headers %} + + {% endfor %} +
{{ hdr.name }}{{ hdr.value }}
diff --git a/extensions/webmail/templates/index.html b/extensions/webmail/templates/index.html new file mode 100644 index 000000000..9417c65a6 --- /dev/null +++ b/extensions/webmail/templates/index.html @@ -0,0 +1,297 @@ +{% extends "common/email_listing.html" %} +{% load i18n %} +{% block css %} +#leftcol { + float: left; + width: 150px; +} + +thead tr { + position: relative; + top: expression(offsetParent.scrollTop); /*IE5+ only*/ + z-index: 1; +} + +table#emails > tbody { + position: fixed; + bottom: 35px; + top: 155px; + left: 155px; + right: 0px; + overflow-x: hidden; + overflow-y: auto; +} + +#emails th { + text-align: center; +} + +#emails tr { + width: 100%; +} + +.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; +} + +td { + position: relative; +} + +th { + -moz-box-shadow: 0px -15px 10px -10px rgba(0,0,0,0.6) inset; + -webkit-box-shadow: 0px -15px 10px -10px rgba(0,0,0,0.6) inset; +} + +th.selected { + background: #EEEEEE; +} + +.draggable { + cursor: move; +} + +.dragging { + background: #6AB0C5; +} + +tr { + cursor: pointer; +} +{% endblock %} +{% block js %} + navparams = new Hash({ + {% for k, v in navparams.items %} + {{ k }} : "{{ v }}", + {% endfor %} + }); + + updatelisting = function(resp, overflow) { + /*if (!$defined(overflow)) { + overflow = "auto"; + }*/ + if ($defined(resp.menu)) { + $("menubar").set("html", resp.menu); + } + $("listing").set("html", resp.listing); + /*if ($defined(resp.navbar)) { + $("navbar").set("html", resp.navbar); + $("navbar").setStyle("display", "block"); + $("navbar").getChildren("a").addEvent("click", navbarcallback); + }*/ + $$("a[name=compose]").addEvent("click", loadCompose); + $$("a[name=empty]").addEvent("click", empty); + } + + empty = function(event) { + event.stop(); + new Request.JSON({url : event.target.get("href"), onSuccess : function(resp) { + updatelisting(resp); + }}).get(); + } + + select = function(obj) { + $$("a[name=loadfolder]").each(function(item) { + item.getParent().removeClass("selected"); + item.getParent().addClass("droppable"); + }); + obj.getParent().removeClass("droppable"); + obj.getParent().addClass("selected"); + } + + {% url extensions.webmail.main.folder as view_url %} + viewmail = function(event) { + event.stop(); + if (event.target.parentNode.id) { + var url = event.target.parentNode.id + "/"; + event.target.parentNode.setStyle("background", "#ffffcc"); + current_anchor.baseurl(url).update(); + } + } + register_callback("viewmail", function(resp) { + updatelisting(resp, "hidden"); + $$("a[name=back]").addEvent("click", loadFolder); + $$("a[name=reply]").addEvent("click", reply); + setDivHeight("mailcontent", $("table-container").getSize().y, 0); + }); + + compose_callback = function(resp) { + updatelisting(resp, "hidden"); + $$("a[name=back]").addEvent("click", loadFolder); + $$("a[name=sendmail]").addEvent("click", function(evt) { + evt.stop(); + $("composemail").set("send", { + onSuccess: function(res) { + updatelisting(JSON.decode(res)); + $$("tr").addEvent("click", viewmail); + } + }); + $("composemail").send(); + }); + setDivHeight("id_body", $("mailheader").getSize().y + 20, 0); + } + + loadCompose = function(event) { + event.stop(); + current_anchor.baseurl("compose/").update(); + } + register_callback("compose", compose_callback); + + reply = function(event) { + event.stop(); + location.hash += event.target.get("href"); + } + register_callback("reply", compose_callback); + + loadFolder = function(event, obj) { + if (!$defined(obj)) { + obj = event.target; + } + event.stop(); + current_anchor.parse_string(obj.get("href"), true).setparams(navparams); + current_anchor.update(); + } + register_callback("folder", function(resp) { + updatelisting(resp, "auto"); + if (navparams.has("order")) { + src = navparams.get("order").charAt(0) == "-" ? "down_arrow.png" : "up_arrow.png"; + th = $$("th[id=" + navparams.get("order").substring(1) + "]"); + th.getElement("img").set("src", "/static/pics/" + src); + th.addClass("selected"); + } + /*$$("td[class*=draggable]").each(function(item) { + item.makeDraggable({ + droppables: ".droppable", + }).addEvents({ + "enter" : function(item, droppable) { + droppable.addClass("dragging"); + }, + "leave" : function(item, droppable) { + droppable.removeClass("dragging"); + }, + "drop" : function(item, droppable, event) { + item.dispose(); + delete(item); + if (!droppable) { + this.element = this.oldelement; + return; + } + droppable.removeClass("dragging"); + this.oldelement.getParent().dispose(); + to = droppable.getFirst("a").get("href"); + {% url extensions.webmail.main.move as moveurl %} + new Request.JSON({url: "{{ moveurl }}", onSuccess: function(resp) { + + }}).get({msgset : this.oldelement.getParent().get("id"), + to : to}); + }, + "beforeStart": function(item) { + var clone = item.clone() + .setStyles({"position" : "absolute", + "opacity" : 0.6, + "width" : "300px", + "border" : "1px solid #333", + "background" : "white"}) + .setStyles(item.getCoordinates()) + .inject(document.body); + this.oldelement = this.element; + this.element = clone; + }, + "cancel": function(item) { + if ($defined(this.oldelement)) { + item.dispose(); + delete(item); + this.element = this.oldelement; + } + } + }); + });*/ + $$("tr").addEvent("dblclick", viewmail); + $$("th[class*=sortable]").addEvent("click", function(evt) { + var tmp = "?"; + var order = (navparams.has("order")) ? navparams.get("order") : ""; + + if (order == "") { + order = current_anchor.getparam("order"); + } + if (order.substring(1) == evt.target.id) { + var sign = (order.charAt(0) == '+') ? '-' : '+'; + order = sign + evt.target.id; + } else { + order = "-" + evt.target.id; + } + navparams.set("order", order); + current_anchor.setparams(navparams).update(); + }); + }); + + /*navbarcallback = function(event) { + event.stop(); + current_anchor.setparams(navparams); + current_anchor.parse_string(event.target.getParent().get("href")).update(); + }*/ + + foldercallback = function(event) { + select(event.target); + loadFolder(event, event.target); + } + + ajaxListener("INBOX/", updatelisting); + $$("a[name=loadfolder]").addEvent("click", foldercallback); + /*$("navbar").getChildren("a").addEvent("click", navbarcallback);*/ + +{% endblock %} +{% block middletitle %}{% trans "Webmail" %}{% endblock %} +{% block leftcol %} +
+

{% trans "Folders" %}

+ +
+{% endblock %} diff --git a/extensions/webmail/templates/viewmail.html b/extensions/webmail/templates/viewmail.html new file mode 100644 index 000000000..97637a322 --- /dev/null +++ b/extensions/webmail/templates/viewmail.html @@ -0,0 +1,25 @@ +{% load i18n %} +{% load webextras %} + + +
+ {% render_headers header %} +
+ +{% url extensions.webmail.main.getmailcontent folder imapid as geturl %} + diff --git a/extensions/webmail/templatetags/__init__.py b/extensions/webmail/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py new file mode 100644 index 000000000..826424f01 --- /dev/null +++ b/extensions/webmail/templatetags/webextras.py @@ -0,0 +1,101 @@ +from django import template +from django.template.loader import render_to_string +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +from mailng.extensions import webmail +from mailng.extensions.webmail.imap_listing import IMAPheader + +register = template.Library() + +def backurl(session, **kwargs): + res = session["folder"] + params = "" + for key in ["page"]: + if key in session.keys(): + if params != "": + params += "&" + params += "%s=%s" % (key, session[key]) + for k, v in kwargs.iteritems(): + if params != "": + params += "&" + params += "%s=%s" % (k, v) + if params != "": + res += "?%s" % params + return res + +@register.simple_tag +def viewm_menu(selection, session, mail_id, perms): + entries = [ + {"name" : "back", + "url" : "%s?page=%s" % (session["folder"], session["page"]), + "img" : "/static/pics/back.png", + "label" : _("Back")}, + {"name" : "reply", + "url" : "reply/", + "img" : "/static/pics/reply.png", + "label" : _("Reply")}, + {"name" : "reply-all", + "url" : "", + "img" : "/static/pics/reply-all.png", + "label" : _("Reply all")}, + {"name" : "forward", + "url" : "", + "img" : "/static/pics/alias.png", + "label" : _("Forward")}, + ] + + return render_to_string('main/menu.html', + {"selection" : selection, "entries" : entries, + "perms" : perms}) + +@register.simple_tag +def compose_menu(selection, session, perms): + entries = [ + {"name" : "back", + "url" : "%s?page=%s" % (session["folder"], session["page"]), + "img" : "/static/pics/back.png", + "label" : _("Back")}, + {"name" : "sendmail", + "url" : "", + "img" : "/static/pics/send-receive.png", + "label" : _("Send")}, + ] + return render_to_string('main/menu.html', + {"selection" : selection, "entries" : entries, + "perms" : perms}) + +@register.simple_tag +def listing_menu(selection, folder, perms): + entries = [ + {"name" : "compose", + "url" : reverse(webmail.main.compose), + "img" : "/static/pics/edit.png", + "label" : _("New message")} + ] + if folder in ["Trash"]: + entries += [ + {"name" : "empty", + "url" : reverse(webmail.main.empty, args=[folder]), + "img" : "/static/pics/clear.png", + "label" : _("Empty trash")} + ] + + return render_to_string("main/menu.html", + {"selection" : selection, "entries" : entries, + "perms" : perms}) + +@register.simple_tag +def render_headers(headers): + fields = ["Subject", "From", "To", "Cc", "Date"] + result = [] + for f in fields: + if not f in headers.keys(): + continue + try: + result += [{"name" : f, "value" : getattr(IMAPheader, "parse_%s" % f.lower())(headers[f])}] + except AttributeError: + result += [{"name" : f, "value" : headers[f]}] + return render_to_string("webmail/headers.html", { + "headers" : result + }) + diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py new file mode 100644 index 000000000..7ef16e31b --- /dev/null +++ b/extensions/webmail/urls.py @@ -0,0 +1,12 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('mailng.extensions.webmail.main', + (r'^$', "index"), + (r'^compose/$', "compose"), + (r'^move/$', "move"), + (r'^empty/(?P.+)/', "empty"), + (r'^(?P.+)/(?P[\w\-\+]+)/content/$', 'getmailcontent'), + (r'^(?P.+)/(?P[\w\-\+]+)/reply/$', 'reply'), + (r'^(?P.+)/(?P[\w\-\+]+)/$', 'viewmail'), + (r'^(?P.+)/$', "folder"), + ) diff --git a/lib/__init__.py b/lib/__init__.py index ff9fab9b6..c2582c4bd 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -50,6 +50,10 @@ def _render(request, tpl, user_context): return render_to_response(tpl, user_context, context_instance=RequestContext(request)) +def _render_error(request, user_context): + return render_to_response("common/error.html", user_context, + context_instance=RequestContext(request)) + def exec_as_vuser(cmd): code, output = exec_pipe("sudo -u %s %s" % (settings.VIRTUAL_UID, cmd)) if code: @@ -63,7 +67,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) @@ -78,3 +82,14 @@ def getoption(name, default=None): except AttributeError: res = default return res + + +class Singleton(type): + def __init__(cls, name, bases, dict): + super(Singleton, cls).__init__(name, bases, dict) + cls.instance = None + + def __call__(cls, *args, **kwargs): + if cls.instance is None: + cls.instance = super(Singleton, cls).__call__(*args, **kwargs) + return cls.instance diff --git a/lib/email_listing.py b/lib/email_listing.py new file mode 100644 index 000000000..c6c827fab --- /dev/null +++ b/lib/email_listing.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +import time +import re +import lxml +import lxml.html +from django.template.loader import render_to_string +from django.template import Template, Context +from mailng.lib import _render, decode + +attached_map = {} + +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, + baseurl=None): + 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 + self.baseurl = baseurl + + 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 + self.baseurl = None + + 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 + p = Page(page, id_start, id_stop, self.total, + self.elems_per_page, has_previous, has_next) + if self.baseurl: + p.baseurl = self.baseurl + return p + +class EmailListing: + def __init__(self, folder=None, elems_per_page=40, **kwargs): + self.folder = folder + self.elems_per_page = elems_per_page + order = "order" in kwargs.keys() and kwargs["order"] or None + self.paginator = Paginator(self.mbc.messages_count(self.folder, order), + elems_per_page) + + def render_navbar(self, page): + return render_to_string("common/navbar.html", { + "page" : page + }) + + def getfolders(self): + return None + + def fetch(self, request, id_start, id_stop): + table = self.tbltype(self.mbc.fetch(start=id_start, stop=id_stop, + folder=self.folder)) + tpl = Template(""" +
+ {{ table }} +
""") + return tpl.render(Context({"table" : table.render(request)})) + + def render(self, request, pageid=1, **kwargs): + page = self.paginator.getpage(pageid) + thead = listing = "" + if "empty" in kwargs.keys() and not kwargs["empty"]: + if not page: + listing = "Empty folder" + else: + listing = self.fetch(request, page.id_start, page.id_stop) + 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, + "navparams" : self.navparams, + "current_page" : pageid + }) + +class Email(object): + def __init__(self, msg, mode="plain", links="0"): + self.attached_map = {} + contents = {"html" : "", "plain" : ""} + for part in msg.walk(): + if part.get_content_maintype() == 'multipart': + continue + if part.get_content_type() in ("text/html", "text/plain"): + contents[part.get_content_subtype()] += part.get_payload(decode=True) + if mode != "html" or links == "0": + continue + + if part.get_content_maintype() == "image": + if part.has_key("Content-Location"): + fname = part["Content-Location"] + if re.match("^http:", fname): + path = fname + else: + path = "/%s" % os.path.join("static/tmp", fname) + fp = open(path, "wb") + fp.write(part.get_payload(decode=True)) + fp.close() + m = re.match("<(.+)>", part["Content-ID"]) + if m: + self.attached_map[m.group(1)] = path + else: + self.attached_map[part["Content-ID"]] = path + + if not contents.has_key(mode) or contents[mode] == "": + if mode == "html": + mode = "plain" + else: + mode = "html" + self.pre, self.body = \ + getattr(self, "viewmail_%s" % mode)(contents[mode], links=links) + + def map_cid(self, url): + import re + + map = globals()["attached_map"] + m = re.match(".*cid:([^\"]+)", url) + if m: + if map.has_key(m.group(1)): + return map[m.group(1)] + return url + + def render(self, request): + return _render(request, 'common/getmailcontent.html', { + "body" : self.body, "pre" : self.pre + }) + + def viewmail_plain(self, content, **kwargs): + body = decode(content) + return (True, body) + + def viewmail_html(self, content, **kwargs): + links = kwargs.has_key("links") and kwargs["links"] or "0" + html = lxml.html.fromstring(content) + if links == "0": + html.rewrite_links(lambda x: None) + else: + html.rewrite_links(self.map_cid) + body = lxml.html.tostring(html) + body = Template(decode(body)).render({}) + return (False, body) diff --git a/lib/events.py b/lib/events.py index 9e04a9900..4e9dcf493 100644 --- a/lib/events.py +++ b/lib/events.py @@ -13,7 +13,9 @@ events = ["CreateDomain", "ModifyMailbox", "UserMenuDisplay", - "AdminMenuDisplay"] + "AdminMenuDisplay", + + "UserLogin"] callbacks = {} 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 new file mode 100644 index 000000000..cdcfe2ec0 --- /dev/null +++ b/lib/parameters.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + +import string +from mailng.admin.models import Parameter + +""" +This interface provides a simple way to declare and store parameters +in MailNG's database. + +Core components or extensions can register their own parameters, which +will be available and modifiable directly from the web interface. + +Only super users will be able to acces this part of the web interface. +""" + +_params = {} + +class NotDefined(Exception): + def __init__(self, app, name): + self.app = app + self.name = name + + def __str__(self): + return "Application '%s' and/or parameter '%s' not defined" % (self.app, + self.name) + +def register(app, name, type="string", deflt=None, help=None, **kwargs): + """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" : deflt, "help" : help} + +def save(app, name, value): + if not app in _params.keys() or not name in _params[app].keys(): + raise NotDefined(app, name) + fullname = "%s.%s" % (app, name) + try: + p = Parameter.objects.get(name=fullname) + except Parameter.DoesNotExist: + p = Parameter() + p.name = fullname + if p.value != value: + p.value = value + p.save() + return True + +def get(app, name): + if not app in _params.keys() or not name in _params[app].keys(): + raise NotDefined(app, name) + try: + p = Parameter.objects.get(name="%s.%s" % (app, name)) + except Parameter.DoesNotExist: + return _params[app][name]["default"] + return p.value diff --git a/lib/tables.py b/lib/tables.py new file mode 100644 index 000000000..938df9cad --- /dev/null +++ b/lib/tables.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +import inspect +import lxml +from django.template import Template, Context +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 ImgColumn(Column): + def __str__(self): + return "" + + def render(self, value): + return "" % value + +class Table(object): + tableid = "" + + 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 isinstance(c, ImgColumn): + key = "img_%s" % c.name + if key in row.keys(): + nrow["cols"] += [{"value" : c.render(row[key]), "safe" : True, "width" : c.width}] + else: + nrow["cols"] += [{"width" : c.width}] + continue + if row.has_key(c.name): + try: + cssclass = getattr(c, "cssclass") + except AttributeError: + cssclass = "" + if "class" in row.keys(): + cssclass += (cssclass != "") and ", %s" % row["class"] or row["class"] + + nrow["cols"] += [{"value" : self.parse(c.name, row[c.name]), + "class" : cssclass, + "width" : c.width}] + self.rows += [nrow] + + def render(self, request): + t = Template(""" + + {% include "common/table_head.html" %} + {% include "common/table_body.html" %} +
+""") + return t.render(Context({ + "table" : self, "tableid" : self.tableid + })) + + def render_head(self, request): + t = Template(""" + + {% include "common/table_head.html" %} +
+""") + return t.render(Context({ + "table" : self, "tableid" : "%s_head" % self.tableid + })) + + def render_body(self, request): + t = Template(""" + + {% include "common/table_body.html" %} +
+""") + return t.render(Context({ + "table" : self, "tableid" : "%s_body" % self.tableid + })) + diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index f9beba823..ae55d93e6 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -478,7 +478,7 @@ msgstr "Domaines" #: templates/layout.html:87 templates/admin/settings_header.html:6 msgid "Settings" -msgstr "Paramètres" +msgstr "Configuration" #: templates/admin/addpermission.html:24 templates/admin/settings_menu.html:15 msgid "Add permission" 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/calendar.js b/static/js/calendar.js new file mode 100644 index 000000000..ab4935248 --- /dev/null +++ b/static/js/calendar.js @@ -0,0 +1,1344 @@ +/** + * Calendar + * + * version 1.1.2 + * - added option onValuesStart + * version 1.1.1 + * - added bounds option + * - fixes chrome toggle bug + * version 1.1 + * - fix: events in fx not work bug + * - fix: ie6 iframe bug + * - added displayHead and displayFooter abstract render function + * version 1.0 + * - new option dayLabelLength + * - IE6 Fix works with variable content height + */ + +/** + * Calendar: a Javascript class for Mootools + * + * adds accessible and unobtrusive date pickers to your form elements + * based on a work by Aeron Glemann + * modifyed by Jens Klose + * + * @licence MIT Style License + */ +var Calendar = new Class({ + + Implements: [Events,Options], + + options: { + dayLabelLength: 2, + blocked: [], // blocked dates + bounds: {start:null , end:null}, // + classes: { + calendar:'calendar', + prev:'prev', + next:'next', + month:'month', + year:'year', + today:'today', + invalid:'invalid', + valid:'valid', + inactive:'inactive', + active:'active', + hover:'hover', + hilite:'hilite' + }, + days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], // days of the week starting at sunday + direction: 0, // -1 past, 0 past + future, 1 future + draggable: true, + months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + navigation: 1, // 0 = no nav; 1 = single nav for month; 2 = dual nav for month and year + offset: 0, // first day of the week: 0 = sunday, 1 = monday, etc.. + onHideStart: $empty, + onHideComplete: $empty, + onShowStart: $empty, + onShowComplete: $empty, + onValuesStart: $empty, + pad: 1, // padding between multiple calendars + tweak: { + x: 0, + y: 0 + } // tweak calendar positioning + }, + + /** + * initialize: calendar constructor + * + * @param obj (obj) a js object containing the form elements and format strings { id: 'format', id: 'format' etc } + * @param props (obj) optional properties + */ + initialize: function(obj, options) { + // basic error checking + if (!obj) { + return false; + } + + this.setOptions(options); + this.classes = this.options.classes; + + // create cal element with css styles required for proper cal functioning + this.calendar = new Element('div', { + 'class':this.classes.calendar, + 'styles': { + left: '-1000px', + opacity: 0, + position: 'absolute', + top: '-1000px', + zIndex: 1000 + } + }).inject(document.body); + + this.calendar.coord = this.calendar.getCoordinates(); + + // iex 6 needs a transparent iframe underneath the calendar in order to not allow select elements to render through + if (Browser.Engine.trident4) { + this.iframe = new IFrame({ + 'styles': { + left: '-1000px', + position: 'absolute', + top: '-1000px', + zIndex: 999 + } + }).inject(document.body); + this.iframe.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)'; + } + + // initialize fade method + this.fx = new Fx.Tween(this.calendar,{ + link : 'chain', + duration : 'short', + onStart: function() { + if (this.calendar.getStyle('opacity') == 0) { // show + //this.calendar.setStyle('display', 'block'); + this.fireEvent('onShowStart', this.element); + } + else { // hide + this.fireEvent('onHideStart', this.element); + } + }.bind(this), + onComplete: function() { + if (this.calendar.getStyle('opacity') == 0) { // hidden + //this.calendar.setStyle('display', 'none'); + if (Browser.Engine.trident4) { + this.iframe.setStyles({ + left: '-1000px', + top: '-1000px' + }); + } + this.fireEvent('onHideComplete', this.element); + } + else { // shown + this.fireEvent('onShowComplete', this.element); + } + }.bind(this) + }); + + // initialize drag method + if (window.Drag && this.options.draggable) { + this.drag = new Drag.Move(this.calendar, { + onDrag: function() { + if (Browser.Engine.trident4) { + this.iframe.setStyles({ + left: this.calendar.getStyle('left'), + top: this.calendar.getStyle('top') + }); + } + }.bind(this) + }); + } + + // create calendars array + this.calendars = []; + var id = 0; + var d = new Date(); // today + d.setDate(d.getDate() + this.options.direction.toInt()); // correct today for directional offset + + for (var i in obj) { + var cal = { + button: new Element('button', { + 'type': 'button' + }), + el: $(i), + els: [], + id: id++, + month: d.getMonth(), + visible: false, + year: d.getFullYear() + }; + + // fix for bad element (naughty, naughty element!) + if (!this.element(i, obj[i], cal)) { + continue; + } + // show cal on click + this.fn = function(e, cal) { + e.stop(); + this.toggle(cal); + }.create({ + 'arguments': cal, + 'bind': this, + 'event': true + }); + + cal.el.addClass(this.classes.calendar); + // create cal button + cal.button.addClass(this.classes.calendar) + .addEvent('click', this.fn) + .inject(cal.el,'after'); + // read in default value + cal.val = this.read(cal); + $extend(cal, this.bounds(cal)); // abs bounds of calendar + $extend(cal, this.values(cal)); // valid days, months, years + this.rebuild(cal); + this.calendars.push(cal); // add to cals array + } + return this; + }, + + /** + * blocked: returns an array of blocked days for the month / year + * + * @param cal (obj) + * @returns blocked days (array) + */ + blocked: function(cal) { + var blocked = []; + var offset = new Date(cal.year, cal.month, 1).getDay(); // day of the week (offset) + var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month + + this.options.blocked.each(function(date) { + var values = date.split(' '); + + for (var i = 0; i < 3; i++) { + if (!values[i]) { + values[i] = '*'; + } // make sure blocked date contains values for at least d, m and y + values[i] = values[i].contains(',') ? values[i].split(',') : new Array(values[i]); // split multiple values + } + + if (values[2].contains(cal.year + '') || values[2].contains('*')) { + if (values[1].contains(cal.month + 1 + '') || values[1].contains('*')) { + values[0].each(function(val) { // if blocked value indicates this month / year + if (val > 0) { + blocked.push(val.toInt()); + } // add date to blocked array + }); + + if (values[3]) { // optional value for day of week + values[3] = values[3].contains(',') ? values[3].split(',') : new Array(values[3]); + + for (var i = 0; i < last; i++) { + var day = (i + offset) % 7; + + if (values[3].contains(day + '')) { + blocked.push(i + 1); // add every date that corresponds to the blocked day of the week to the blocked array + } + } + } + } + } + }, this); + + return blocked; + }, + + /** + * bounds: returns the start / end bounds of the calendar + * + * @param cal (obj) + * @returns obj + */ + bounds: function(cal) { + // check external settings + if ($defined(this.options.bounds.start)&&$defined(this.options.bounds.end)) { + if ($type(this.options.bounds.end) == 'date' && + $type(this.options.bounds.start) == 'date' && + this.options.bounds.end>=this.options.bounds.start) { + return { + 'start': this.options.bounds.start, + 'end': this.options.bounds.end + } + } + } + + // 1. first we assume the calendar has no bounds (or a thousand years in either direction) + + // by default the calendar will accept a millennium in either direction + var start = new Date(1000, 0, 1); // jan 1, 1000 + var end = new Date(2999, 11, 31); // dec 31, 2999 + + // 2. but if the cal is one directional we adjust accordingly + var date = new Date().getDate() + this.options.direction.toInt(); + + if (this.options.direction > 0) { + start = new Date(); + start.setDate(date + this.options.pad * cal.id); + } + + if (this.options.direction < 0) { + end = new Date(); + end.setDate(date - this.options.pad * (this.calendars.length - cal.id - 1)); + } + + // 3. then we can further filter the limits by using the pre-existing values in the selects + cal.els.each(function(el) { + if (el.get('tag') == 'select') { + if (el.retrieve('format').test('(y|Y)')) { // search for a year select + var years = []; + + el.getChildren().each(function(option) { // get options + var values = this.unformat(option.get('value'), el.retrieve('format')); + + if (!years.contains(values[0])) { + years.push(values[0]); + } // add to years array + }, this); + + years.sort(this.sort); + + if (years[0] > start.getFullYear()) { + d = new Date(years[0], start.getMonth() + 1, 0); // last day of new month + + if (start.getDate() > d.getDate()) { + start.setDate(d.getDate()); + } + + start.setYear(years[0]); + } + + if (years.getLast() < end.getFullYear()) { + d = new Date(years.getLast(), end.getMonth() + 1, 0); // last day of new month + + if (end.getDate() > d.getDate()) { + end.setDate(d.getDate()); + } + + end.setYear(years.getLast()); + } + } + + if (el.retrieve('format').test('(F|m|M|n)')) { // search for a month select + var months_start = []; + var months_end = []; + + el.getChildren().each(function(option) { // get options + var values = this.unformat(option.get('value'), el.retrieve('format')); + + if ($type(values[0]) != 'number' || values[0] == years[0]) { // if it's a year / month combo for curr year, or simply a month select + if (!months_start.contains(values[1])) { + months_start.push(values[1]); + } // add to months array + } + + if ($type(values[0]) != 'number' || values[0] == years.getLast()) { // if it's a year / month combo for curr year, or simply a month select + if (!months_end.contains(values[1])) { + months_end.push(values[1]); + } // add to months array + } + }, this); + + months_start.sort(this.sort); + months_end.sort(this.sort); + + if (months_start[0] > start.getMonth()) { + d = new Date(start.getFullYear(), months_start[0] + 1, 0); // last day of new month + + if (start.getDate() > d.getDate()) { + start.setDate(d.getDate()); + } + + start.setMonth(months_start[0]); + } + + if (months_end.getLast() < end.getMonth()) { + d = new Date(start.getFullYear(), months_end.getLast() + 1, 0); // last day of new month + + if (end.getDate() > d.getDate()) { + end.setDate(d.getDate()); + } + + end.setMonth(months_end.getLast()); + } + } + } + }, this); + + return { + 'start': start, + 'end': end + }; + }, + + /** + * caption: returns the caption element with header and navigation + * + * @param cal (obj) + * @returns caption (element) + */ + caption: function(cal) { + // start by assuming navigation is allowed + var navigation = { + prev: { + 'month': true, + 'year': true + }, + next: { + 'month': true, + 'year': true + } + }; + + // if we're in an out of bounds year + if (cal.year == cal.start.getFullYear()) { + navigation.prev.year = false; + if (cal.month == cal.start.getMonth() && this.options.navigation == 1) { + navigation.prev.month = false; + } + } + if (cal.year == cal.end.getFullYear()) { + navigation.next.year = false; + if (cal.month == cal.end.getMonth() && this.options.navigation == 1) { + navigation.next.month = false; + } + } + + // special case of improved navigation but months array with only 1 month we can disable all month navigation + if ($type(cal.months) == 'array') { + if (cal.months.length == 1 && this.options.navigation == 2) { + navigation.prev.month = navigation.next.month = false; + } + } + + var caption = new Element('caption'); + + var prev = new Element('a').addClass(this.classes.prev).appendText('\x3c'); // < + var next = new Element('a').addClass(this.classes.next).appendText('\x3e'); // > + + if (this.options.navigation == 2) { + var month = new Element('span').addClass(this.classes.month).inject(caption); + + if (navigation.prev.month) { + prev.clone().addEvent('click', function(cal) { + this.navigate(cal, 'm', -1); + }.pass(cal, this)).inject(month); + } + + month.adopt(new Element('span').appendText(this.options.months[cal.month])); + + if (navigation.next.month) { + next.clone().addEvent('click', function(cal) { + this.navigate(cal, 'm', 1); + }.pass(cal, this)).inject(month); + } + + var year = new Element('span').addClass(this.classes.year).inject(caption); + + if (navigation.prev.year) { + prev.clone().addEvent('click', function(cal) { + this.navigate(cal, 'y', -1); + }.pass(cal, this)).inject(year); + } + + year.adopt(new Element('span').appendText(cal.year)); + + if (navigation.next.year) { + next.clone().addEvent('click', function(cal) { + this.navigate(cal, 'y', 1); + }.pass(cal, this)).inject(year); + } + } + else { // 1 or 0 + if (navigation.prev.month && this.options.navigation) { + prev.clone().addEvent('click', function(cal) { + this.navigate(cal, 'm', -1); + }.pass(cal, this)).inject(caption); + } + + caption.adopt(new Element('span').addClass(this.classes.month).appendText(this.options.months[cal.month])); + + caption.adopt(new Element('span').addClass(this.classes.year).appendText(cal.year)); + + if (navigation.next.month && this.options.navigation) { + next.clone().addEvent('click', function(cal) { + this.navigate(cal, 'm', 1); + }.pass(cal, this)).inject(caption); + } + + } + + return caption; + }, + + /** + * changed: run when a select value is changed + * + * @param cal (obj) + * @returns caption (element) + */ + changed: function(cal) { + cal.val = this.read(cal); // update calendar val from inputs + + $extend(cal, this.values(cal)); // update bounds - based on curr month + + this.rebuild(cal); // rebuild days select + + if (!cal.val) { + return; + } // in case the same date was clicked the cal has no set date we should exit + + if (cal.val.getDate() < cal.days[0]) { + cal.val.setDate(cal.days[0]); + } + if (cal.val.getDate() > cal.days.getLast()) { + cal.val.setDate(cal.days.getLast()); + } + + cal.els.each(function(el) { // then we can set the value to the field + el.set('value',this.format(cal.val, el.retrieve('format'))); + }, this); + + this.check(cal); // checks other cals + + this.calendars.each(function(kal) { // update cal graphic if visible + if (kal.visible) { + this.display(kal); + } + }, this); + }, + + /** + * check: checks other calendars to make sure no overlapping values + * + * @param cal (obj) + */ + check: function(cal) { + this.calendars.each(function(kal, i) { + if (kal.val) { // if calendar has value set + var change = false; + + if (i < cal.id) { // preceding calendar + var bound = new Date(Date.parse(cal.val)); + + bound.setDate(bound.getDate() - (this.options.pad * (cal.id - i))); + + if (bound < kal.val) { + change = true; + } + } + if (i > cal.id) { // following calendar + var bound = new Date(Date.parse(cal.val)); + + bound.setDate(bound.getDate() + (this.options.pad * (i - cal.id))); + + if (bound > kal.val) { + change = true; + } + } + + if (change) { + if (kal.start > bound) { + bound = kal.start; + } + if (kal.end < bound) { + bound = kal.end; + } + + kal.month = bound.getMonth(); + kal.year = bound.getFullYear(); + + $extend(kal, this.values(kal)); + + // TODO - IN THE CASE OF SELECT MOVE TO NEAREST VALID VALUE + // IN THE CASE OF INPUT DISABLE + + // if new date is not valid better unset cal value + // otherwise it would mean incrementally checking to find the nearest valid date which could be months / years away + kal.val = kal.days.contains(bound.getDate()) ? bound : null; + + this.write(kal); + + if (kal.visible) { + this.display(kal); + } // update cal graphic if visible + } + } + }, this); + }, + + /** + * clicked: run when a valid day is clicked in the calendar + * + * @param td (obj) + * @param day (int) + * @param cal (obj) + */ + clicked: function(td, day, cal) { + cal.val = (this.value(cal) == day) ? null : new Date(cal.year, cal.month, day); // set new value - if same then disable + + this.write(cal); + + // ok - in the special case that it's all selects and there's always a date no matter what (at least as far as the form is concerned) + // we can't let the calendar undo a date selection - it's just not possible!! + if (!cal.val) { + cal.val = this.read(cal); + } + + if (cal.val) { + this.check(cal); // checks other cals + this.toggle(cal); // hide cal + } + else { // remove active class and replace with valid + td.addClass(this.classes.valid); + td.removeClass(this.classes.active); + } + }, + + /** + * display: create calendar element + * + * @param cal (obj) + */ + display: function(cal) { + // 1. header and navigation + this.calendar.empty(); // init div + this.calendar.className = this.classes.calendar + ' ' + this.options.months[cal.month].toLowerCase(); + // render optional head content + var extra = this.displayHead(cal); + if ($defined(extra) && $defined(extra.inject)) extra.inject(this.calendar); + //this.displayHead(cal).inject(this.calendar, 'top'); + + var div = new Element('div').inject(this.calendar); // a wrapper div to help correct browser css problems with the caption element + var table = new Element('table').inject(div).adopt(this.caption(cal)); + // 2. day names + var thead = new Element('thead').inject(table); + var tr = new Element('tr').inject(thead); + + for (var i = 0; i <= 6; i++) { + var th = this.options.days[(i + this.options.offset) % 7]; + + tr.adopt(new Element('th', { + 'title': th + }).appendText(th.substr(0, this.options.dayLabelLength))); + } + + // 3. day numbers + var tbody = new Element('tbody').inject(table); + var tr = new Element('tr').inject(tbody); + var d = new Date(cal.year, cal.month, 1); + var offset = ((d.getDay() - this.options.offset) + 7) % 7; // day of the week (offset) + var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month + var prev = new Date(cal.year, cal.month, 0).getDate(); // last day of previous month + var active = this.value(cal); // active date (if set and within curr month) + var valid = cal.days; // valid days for curr month + var inactive = []; // active dates set by other calendars + var hilited = []; + this.calendars.each(function(kal, i) { + if (kal != cal && kal.val) { + if (cal.year == kal.val.getFullYear() && cal.month == kal.val.getMonth()) { + inactive.push(kal.val.getDate()); + } + if (cal.val) { + for (var day = 1; day <= last; day++) { + d.setDate(day); + if ((i < cal.id && d > kal.val && d < cal.val) || (i > cal.id && d > cal.val && d < kal.val)) { + if (!hilited.contains(day)) { + hilited.push(day); + } + } + } + } + } + }, this); + var d = new Date(); + var today = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); // today obv + + for (var i = 1; i < 43; i++) { // 1 to 42 (6 x 7 or 6 weeks) + if ((i - 1) % 7 == 0) { + tr = new Element('tr').inject(tbody); + } // each week is it's own table row + var td = new Element('td').inject(tr); + var day = i - offset; + var date = new Date(cal.year, cal.month, day); + var cls = ''; + + if (day === active) { + cls = this.classes.active; + } // active + else if (inactive.contains(day)) { + cls = this.classes.inactive; + } // inactive + else if (valid.contains(day)) { + cls = this.classes.valid; + } // valid + else if (day >= 1 && day <= last) { + cls = this.classes.invalid; + } // invalid + + if (date.getTime() == today) { + cls = cls + ' ' + this.classes.today; + } // adds class for today + if (hilited.contains(day)) { + cls = cls + ' ' + this.classes.hilite; + } // adds class if hilited + td.addClass(cls); + + if (valid.contains(day)) { // if it's a valid - clickable - day we add interaction + td.set('title', this.format(date, 'D M jS Y')); + td.addEvents({ + 'click': function(td, day, cal) { + this.clicked(td, day, cal); + }.pass([td, day, cal], this), + 'mouseover': function(td, cls) { + td.addClass(cls); + }.pass([td, this.classes.hover]), + 'mouseout': function(td, cls) { + td.removeClass(cls); + }.pass([td, this.classes.hover]) + }); + } + + // pad calendar with last days of prev month and first days of next month + if (day < 1) { + day = prev + day; + } + else if (day > last) { + day = day - last; + } + td.appendText(day); + } + // render optional footer content + extra = this.displayFooter(cal) + if ($defined(extra) && $defined(extra.inject)) extra.inject(this.calendar); + + this.calendar.coord = this.calendar.getCoordinates(); + }, + + /** + * displayHead + * + * @abstract + */ + displayHead: function(cal) {}, + + /** + * displayFooter + * + * @abstract + */ + displayFooter: function(cal) {}, + + /** + * element: helper function + * + * @param el (string) element id + * @param f (string) format string + * @param cal (obj) + */ + element: function(el, f, cal) { + if ($type(f) == 'object') { // in the case of multiple inputs per calendar + for (var i in f) { + if (!this.element(i, f[i], cal)) { + return false; + } + } + + return true; + } + + el = $(el); + + if (!el) { + return false; + } + + el.store('format',f); + + if (el.get('tag') == 'select') { // select elements allow the user to manually set the date via select option + el.addEvent('change', function(cal) { + this.changed(cal); + }.pass(cal, this)); + } + else { // input (type text) elements restrict the user to only setting the date via the calendar + el.set('readonly','readonly'); + el.addEvent('focus', function(cal) { + this.toggle(cal); + }.pass(cal, this)); + } + + cal.els.push(el); + + return true; + }, + + /** + * format: formats a date object according to passed in instructions + * + * @param date (obj) + * @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y + * @returns string + */ + format: function(date, f) { + var g = ''; + + if (date) { + var d = date.getDate(); // 1 - 31 + var day = this.options.days[date.getDay()]; // Sunday - Saturday + var m = date.getMonth() + 1; // 1 - 12 + var month = this.options.months[date.getMonth()]; // January - December + var y = date.getFullYear() + ''; // 19xx - 20xx + + for (var i = 0; i < f.length; i++) { + var c = f.charAt(i); // format char + switch(c) { + // year cases + case 'y': // xx - xx + y = y.substr(2); + case 'Y': // 19xx - 20xx + g += y; + break; + + // month cases + case 'm': // 01 - 12 + if (m < 10) { + m = '0' + m; + } + case 'n': // 1 - 12 + g += m; + break; + + case 'M': // Jan - Dec + month = month.substr(0, 3); + case 'F': // January - December + g += month; + break; + + // day cases + case 'd': // 01 - 31 + if (d < 10) { + d = '0' + d; + } + case 'j': // 1 - 31 + g += d; + break; + + case 'D': // Sun - Sat + day = day.substr(0, 3); + case 'l': // Sunday - Saturday + g += day; + break; + + case 'S': // st, nd, rd or th (works well with j) + if (d % 10 == 1 && d != '11') { + g += 'st'; + } + else if (d % 10 == 2 && d != '12') { + g += 'nd'; + } + else if (d % 10 == 3 && d != '13') { + g += 'rd'; + } + else { + g += 'th'; + } + break; + + default: + g += c; + } + } + } + return g; // return format with values replaced + }, + + /** + * navigate: calendar navigation + * + * @param cal (obj) + * @param type (str) m or y for month or year + * @param n (int) + or - for next or prev + */ + navigate: function(cal, type, n) { + switch (type) { + case 'm': // month + if ($type(cal.months) == 'array') { + var i = cal.months.indexOf(cal.month) + n; // index of current month + + if (i < 0 || i == cal.months.length) { // out of range + if (this.options.navigation == 1) { // if type 1 nav we'll need to increment the year + this.navigate(cal, 'y', n); + } + + i = (i < 0) ? cal.months.length - 1 : 0; + } + + cal.month = cal.months[i]; + } + else { + var i = cal.month + n; + + if (i < 0 || i == 12) { + if (this.options.navigation == 1) { + this.navigate(cal, 'y', n); + } + + i = (i < 0) ? 11 : 0; + } + + cal.month = i; + } + break; + + case 'y': // year + if ($type(cal.years) == 'array') { + var i = cal.years.indexOf(cal.year) + n; + + cal.year = cal.years[i]; + } + else { + cal.year += n; + } + break; + } + $extend(cal, this.values(cal)); + if ($type(cal.months) == 'array') { // if the calendar has a months select + var i = cal.months.indexOf(cal.month); // and make sure the curr months exists for the new year + + if (i < 0) { + cal.month = cal.months[0]; + } // otherwise we'll reset the month + } + this.display(cal); + }, + + /** + * read: compiles cal value based on array of inputs passed in + * + * @param cal (obj) + * @returns date (obj) or (null) + */ + read: function(cal) { + var arr = [null, null, null]; + cal.els.each(function(el) { + // returns an array which may contain empty values + var values = this.unformat(el.get('value'), el.retrieve('format')); + + values.each(function(val, i) { + if ($type(val) == 'number') { + arr[i] = val; + } + }); + }, this); + // we can update the cals month and year values + if ($type(arr[0]) == 'number') { + cal.year = arr[0]; + } + if ($type(arr[1]) == 'number') { + cal.month = arr[1]; + } + var val = null; + if (arr.every(function(i) { + return $type(i) == 'number'; + })) { // if valid date + var last = new Date(arr[0], arr[1] + 1, 0).getDate(); // last day of month + + if (arr[2] > last) { + arr[2] = last; + } // make sure we stay within the month (ex in case default day of select is 31 and month is feb) + val = new Date(arr[0], arr[1], arr[2]); + } + return (cal.val == val) ? null : val; // if new date matches old return null (same date clicked twice = disable) + }, + + /** + * rebuild: rebuilds days + months selects + * + * @param cal (obj) + */ + rebuild: function(cal) { + cal.els.each(function(el) { + /* + if (el.getTag() == 'select' && el.format.test('^(F|m|M|n)$')) { // special case for months-only select + if (!cal.options) { cal.options = el.clone(); } // clone a copy of months select + + var val = (cal.val) ? cal.val.getMonth() : el.value.toInt(); + + el.empty(); // initialize select + + cal.months.each(function(month) { + // create an option element + var option = new Element('option', { + 'selected': (val == month), + 'value': this.format(new Date(1, month, 1), el.format); + }).appendText(day).injectInside(el); + }, this); + } + */ + if (el.get('tag') == 'select' && el.retrieve('format').test('^(d|j)$')) { // special case for days-only select + var d = this.value(cal); + + if (!d) { + d = el.get('value').toInt(); + } // if the calendar doesn't have a set value, try to use value from select + + el.empty(); // initialize select + + cal.days.each(function(day) { + // create an option element + var option = new Element('option', { + 'value': ((el.retrieve('format') == 'd' && day < 10) ? '0' + day : day) + }).appendText(day).inject(el); + if(d == day){ + option.set('selected','selected'); + } + }, this); + } + }, this); + }, + + /** + * sort: helper function for numerical sorting + * + * @param a (int) + * @param b (int) + */ + sort: function(a, b) { + return a - b; + }, + + /** + * toggle: show / hide calendar + * + * @param cal (obj) + */ + toggle: function(cal) { + document.removeEvent('mousedown', this.fn); // always remove the current mousedown script first + + if (cal.visible) { // simply hide curr cal + cal.visible = false; + cal.button.removeClass(this.classes.active); // active + this.fx.start('opacity', 0); //*RAL - Fx.Tween.start([property], from, [to]) + } + else { // otherwise show (may have to hide others) + // hide cal on out-of-bounds click + this.fn = function(e, cal) { + var event = new Event(e); + var el = $(event.target); + var stop = false; + + while (el != document.body && el.nodeType == 1) { + if (el == this.calendar) { + stop = true; + } + this.calendars.each(function(kal) { + if (kal.button == el || kal.els.contains(el)) { + stop = true; + } + }); + + if (stop) { + e.stop(); + return false; + } + else { + el = el.getParent(); + } + } + + this.toggle(cal); + }.create({ + 'arguments': cal, + 'bind': this, + 'event': true + }); + + document.addEvent('mousedown', this.fn); + + this.calendars.each(function(kal) { + if (kal == cal) { + kal.visible = true; + kal.button.addClass(this.classes.active); // css c-icon-active + } + else { + kal.visible = false; + kal.button.removeClass(this.classes.active); // css c-icon-active + } + }, this); + + this.display(cal); + + var size = window.getScrollSize(); + var coord = cal.button.getCoordinates(); + var x = coord.right + this.options.tweak.x; + var y = coord.top + this.options.tweak.y; + + // make sure the calendar doesn't open off screen (does't work in safari!?) + if (x + this.calendar.coord.width > size.x) { + x -= (x + this.calendar.coord.width - size.x); + } + if (y + this.calendar.coord.height > size.y) { + y -= (y + this.calendar.coord.height - size.y); + } + + this.calendar.setStyles({ + left: x + 'px', + top: y + 'px' + }); + // IE6 Iframe Fix + if (Browser.Engine.trident4) { + this.iframe.setStyles({ + left: x + 'px', + top: y + 'px', + height: this.calendar.coord.height + 'px', + width: this.calendar.coord.width + 'px' + }); + } + this.fx.start('opacity', 1); //*RAL - Fx.Tween.start([property], from, [to]) + } + }, + + /** + * unformat: takes a value from an input and parses the d, m and y elements + * + * @param val (string) + * @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y + * @returns array + */ + unformat: function(val, f) { + f = f.escapeRegExp(); + + var re = { + d: '([0-9]{2})', + j: '([0-9]{1,2})', + D: '(' + this.options.days.map(function(day) { + return day.substr(0, 3); + }).join('|') + ')', + l: '(' + this.options.days.join('|') + ')', + S: '(st|nd|rd|th)', + F: '(' + this.options.months.join('|') + ')', + m: '([0-9]{2})', + M: '(' + this.options.months.map(function(month) { + return month.substr(0, 3); + }).join('|') + ')', + N: '([0-9]{1,2})', + n: '([0-9]{1,2})', + Y: '([0-9]{4})', + y: '([0-9]{2})' + }; + + var arr = []; // array of indexes + var g = ''; + + // convert our format string to regexp + for (var i = 0; i < f.length; i++) { + var c = f.charAt(i); + if (re[c]) { + arr.push(c); + g += re[c]; + } + else { + g += c; + } + } + + // match against date + var matches = val.match('^' + g + '$'); + var dates = new Array(3); + if (matches) { + matches = matches.slice(1); // remove first match which is the date + arr.each(function(c, i) { + i = matches[i]; + + switch(c) { + // year cases + case 'y': + i = '19' + i; // 2 digit year assumes 19th century (same as JS) + case 'Y': + dates[0] = i.toInt(); + break; + + // month cases + case 'F': + i = i.substr(0, 3); + case 'M': + i = this.options.months.map(function(month) { + return month.substr(0, 3); + }).indexOf(i) + 1; + case 'm': + case 'n': + dates[1] = i.toInt() - 1; + break; + + // day cases + case 'd': + case 'j': + dates[2] = i.toInt(); + break; + } + }, this); + } + return dates; + }, + + /** + * value: returns day value of calendar if set + * + * @param cal (obj) + * @returns day (int) or null + */ + value: function(cal) { + var day = null; + if (cal.val) { + if (cal.year == cal.val.getFullYear() && cal.month == cal.val.getMonth()) { + day = cal.val.getDate(); + } + } + return day; + }, + + /** + * values: returns the years, months (for curr year) and days (for curr month and year) for the calendar + * + * @param cal (obj) + * @returns obj + */ + values: function(cal) { + // make it possible + this.fireEvent('onValuesStart', this.element); + var years, months, days; + + cal.els.each(function(el) { + if (el.get('tag') == 'select') { + if (el.retrieve('format').test('(y|Y)')) { // search for a year select + years = []; + + el.getChildren().each(function(option) { // get options + var values = this.unformat(option.get('value'), el.retrieve('format')); + + if (!years.contains(values[0])) { + years.push(values[0]); + } // add to years array + }, this); + + years.sort(this.sort); + } + + if (el.retrieve('format').test('(F|m|M|n|N)')) { // search for a month select + months = []; // 0 - 11 should be + + el.getChildren().each(function(option) { // get options + var values = this.unformat(option.get('value'), el.retrieve('format')); + + if ($type(values[0]) != 'number' || values[0] == cal.year) { // if it's a year / month combo for curr year, or simply a month select + if (!months.contains(values[1])) { + months.push(values[1]); + } // add to months array + } + }, this); + + months.sort(this.sort); + } + + if (el.retrieve('format').test('(d|j)') && !el.retrieve('format').test('^(d|j)$')) { // search for a day select, but NOT a days only select + days = []; // 1 - 31 + + el.getChildren().each(function(option) { // get options + var values = this.unformat(option.get('value'), el.retrieve('format')); + + // in the special case of days we dont want the value if its a days only select + // otherwise that will screw up the options rebuilding + // we will take the values if they are exact dates though + if (values[0] == cal.year && values[1] == cal.month) { + if (!days.contains(values[2])) { + days.push(values[2]); + } // add to days array + } + }, this); + } + } + }, this); + + // we start with what would be the first and last days were there no restrictions + var first = 1; + var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of the month + + // if we're in an out of bounds year + if (cal.year == cal.start.getFullYear()) { + // in the special case of improved navigation but no months array, we'll need to construct one + if (months == null && this.options.navigation == 2) { + months = []; + + for (var i = 0; i < 12; i ++) { + if (i >= cal.start.getMonth()) { + months.push(i); + } + } + } + + // if we're in an out of bounds month + if (cal.month == cal.start.getMonth()) { + first = cal.start.getDate(); // first day equals day of bound + } + } + if (cal.year == cal.end.getFullYear()) { + // in the special case of improved navigation but no months array, we'll need to construct one + if (months == null && this.options.navigation == 2) { + months = []; + + for (var i = 0; i < 12; i ++) { + if (i <= cal.end.getMonth()) { + months.push(i); + } + } + } + + if (cal.month == cal.end.getMonth()) { + last = cal.end.getDate(); // last day equals day of bound + } + } + + // let's get our invalid days + var blocked = this.blocked(cal); + + // finally we can prepare all the valid days in a neat little array + if ($type(days) == 'array') { // somewhere there was a days select + days = days.filter(function(day) { + if (day >= first && day <= last && !blocked.contains(day)) { + return day; + } + }); + } + else { // no days select we'll need to construct a valid days array + days = []; + + for (var i = first; i <= last; i++) { + if (!blocked.contains(i)) { + days.push(i); + } + } + } + + days.sort(this.sort); // sorting our days will give us first and last of month + + return { + 'days': days, + 'months': months, + 'years': years + }; + }, + + /** + * write: sets calendars value to form elements + * + * @param cal (obj) + */ + write: function(cal) { + this.rebuild(cal); // in the case of options, we'll need to make sure we have the correct number of days available + + cal.els.each(function(el) { // then we can set the value to the field + el.set('value',this.format(cal.val, el.retrieve('format'))); + }, this); + } +}); \ No newline at end of file diff --git a/static/js/common.js b/static/js/common.js index 4f1e387e4..b859c883a 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -1,6 +1,140 @@ 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', + });*/ +}); + +callbacks = {}; + +register_callback = function(name, callback) { + callbacks[name] = callback; +} + +current_anchor = null; + +check_anchor = function() { + if (current_anchor.serialized == location.hash) { + return; + } + current_anchor.from_string(location.hash); + if (!current_anchor.serialized) { + query = current_anchor.deflocation; + } else { + query = current_anchor.serialized.substring(1); + } + new Request.JSON({url: query, onSuccess: function(resp) { + callback = ($defined(resp.callback)) ? resp.callback : "default"; + callbacks[callback](resp); + }}).get(); +} + +ajaxListener = function(deflocation, defcallback) { + current_anchor = new HashWrapper(deflocation); + register_callback("default", defcallback); + check_anchor.periodical(300); +} + +/* +* +*/ +function HashWrapper(deflocation) { + this.deflocation = deflocation; + this.params = $H(); + this.base = ''; + this.serialized = null; + + this.reset = function() { + this.base = null; + this.params.empty(); + return this; + }; + + this.parse_string = function(value, reset) { + var splits = (value.charAt(0) == '#') + ? value.substring(1).split('?') : value.split('?'); + + if (splits.length == 0) { + return this; + } + if (!$defined(reset)) { + reset = false; + } + if (reset) { + this.reset(); + } + this.base = (splits[0] == "") ? this.deflocation : splits[0]; + if (this.base[this.base.length - 1] != '/') { + this.base += '/'; + } + if (splits.length > 1) { + params = splits[1].split('&'); + for (var i = 0; i < params.length; i++) { + tmp = params[i].split('='); + this.setparam(tmp[0], tmp[1]); + } + } + return this; + }; + + this.from_string = function(value, reset) { + this.parse_string(value); + this.serialized = value; + return this; + }; + + this.serialize = function() { + var res = this.base; + + if (this.params.getLength() != 0) { + res += "?" + this.params.toQueryString(); + } + return res; + }; + + this.update = function() { + location.hash = this.serialize(); + }; + + this.setparam = function(name, value) { + this.params.set(name, value); + return this; + }; + + this.setparams = function(params) { + this.params.extend(params); + return this; + }; + + this.getparam = function(name) { + if (!this.params.has(name)) { + return ""; + } + return this.params.get(name); + } + + this.baseurl = function(value) { + this.reset(); + this.base = value; + if (this.base[this.base.length - 1] != '/') { + this.base += '/'; + } + return this; + }; + + this.delparam = function(name) { + if (!this.params.has(name)) { + return false; + } + this.params.erase(name); + return this; + }; +}; + +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 +149,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 +196,47 @@ confirmation = function(question, action, callback) { }); SqueezeBox.open("/mailng/main/confirm/?question=" + question); } + +setDivHeight = function(id, extrah, modulo) { + var contentsize = $(document.body).getSize().y + - $("topmenubar").getSize().y + - $("header").getSize().y + - extrah; + + if ($("navbar")) { + contentsize -= $("navbar").getSize().y; + } + if ($("thead")) { + contentsize -= $("thead").getSize().y; + } + if (modulo) { + contentsize -= contentsize % modulo; + } + $(id).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..5898b43bd 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'); @@ -66,6 +66,7 @@ window.addEvent('domready', function() { } } }); + setDivHeight("content", 20, 0); }); newdom = function() { 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..3a4b1bffc 100644 --- a/static/js/mootools-more.js +++ b/static/js/mootools-more.js @@ -30,6 +30,471 @@ MooTools.More = { /* --- +script: Log.js + +description: Provides basic logging functionality for plugins to implement. + +license: MIT-style license + +authors: +- Guillermo Rauch +- Thomas Aylott +- Scott Kyle + +requires: +- core:1.2.4/Class +- /MooTools.More + +provides: [Log] + +... +*/ + +(function(){ + +var global = this; + +var log = function(){ + if (global.console && console.log){ + try { + console.log.apply(console, arguments); + } catch(e) { + console.log(Array.slice(arguments)); + } + } else { + Log.logged.push(arguments); + } + return this; +}; + +var disabled = function(){ + this.logged.push(arguments); + return this; +}; + +this.Log = new Class({ + + logged: [], + + log: disabled, + + resetLog: function(){ + this.logged.empty(); + return this; + }, + + enableLog: function(){ + this.log = log; + this.logged.each(function(args){ + this.log.apply(this, args); + }, this); + return this.resetLog(); + }, + + disableLog: function(){ + this.log = disabled; + return this; + } + +}); + +Log.extend(new Log).enableLog(); + +// legacy +Log.logger = function(){ + return this.log.apply(this, arguments); +}; + +})(); + +/* +--- + +script: Class.Refactor.js + +description: Extends a class onto itself with new property, preserving any items attached to the class's namespace. + +license: MIT-style license + +authors: +- Aaron Newton + +requires: +- core:1.2.4/Class +- /MooTools.More + +provides: [Class.refactor] + +... +*/ + +Class.refactor = function(original, refactors){ + + $each(refactors, function(item, name){ + var origin = original.prototype[name]; + if (origin && (origin = origin._origin) && typeof item == 'function') original.implement(name, function(){ + var old = this.previous; + this.previous = origin; + var value = item.apply(this, arguments); + this.previous = old; + return value; + }); else original.implement(name, item); + }); + + return original; + +}; + +/* +--- + +script: Class.Occlude.js + +description: Prevents a class from being applied to a DOM element twice. + +license: MIT-style license. + +authors: +- Aaron Newton + +requires: +- core/1.2.4/Class +- core:1.2.4/Element +- /MooTools.More + +provides: [Class.Occlude] + +... +*/ + +Class.Occlude = new Class({ + + occlude: function(property, element){ + element = document.id(element || this.element); + var instance = element.retrieve(property || this.property); + if (instance && !$defined(this.occluded)) + return this.occluded = instance; + + this.occluded = false; + element.store(property || this.property, this); + return this.occluded; + } + +}); + +/* +--- + +script: Element.Delegation.js + +description: Extends the Element native object to include the delegate method for more efficient event management. + +credits: +- "Event checking based on the work of Daniel Steigerwald. License: MIT-style license. Copyright: Copyright (c) 2008 Daniel Steigerwald, daniel.steigerwald.cz" + +license: MIT-style license + +authors: +- Aaron Newton +- Daniel Steigerwald + +requires: +- core:1.2.4/Element.Event +- core:1.2.4/Selectors +- /MooTools.More + +provides: [Element.Delegation] + +... +*/ +(function(){ + + var match = /(.*?):relay\(([^)]+)\)$/, + combinators = /[+>~\s]/, + splitType = function(type){ + var bits = type.match(match); + return !bits ? {event: type} : { + event: bits[1], + selector: bits[2] + }; + }, + check = function(e, selector){ + var t = e.target; + if (combinators.test(selector = selector.trim())){ + var els = this.getElements(selector); + for (var i = els.length; i--; ){ + var el = els[i]; + if (t == el || el.hasChild(t)) return el; + } + } else { + for ( ; t && t != this; t = t.parentNode){ + if (Element.match(t, selector)) return document.id(t); + } + } + return null; + }; + + var oldAddEvent = Element.prototype.addEvent, + oldRemoveEvent = Element.prototype.removeEvent; + + Element.implement({ + + addEvent: function(type, fn){ + var splitted = splitType(type); + if (splitted.selector){ + var monitors = this.retrieve('$moo:delegateMonitors', {}); + if (!monitors[type]){ + var monitor = function(e){ + var el = check.call(this, e, splitted.selector); + if (el) this.fireEvent(type, [e, el], 0, el); + }.bind(this); + monitors[type] = monitor; + oldAddEvent.call(this, splitted.event, monitor); + } + } + return oldAddEvent.apply(this, arguments); + }, + + removeEvent: function(type, fn){ + var splitted = splitType(type); + if (splitted.selector){ + var events = this.retrieve('events'); + if (!events || !events[type] || (fn && !events[type].keys.contains(fn))) return this; + + if (fn) oldRemoveEvent.apply(this, [type, fn]); + else oldRemoveEvent.apply(this, type); + + events = this.retrieve('events'); + if (events && events[type] && events[type].length == 0){ + var monitors = this.retrieve('$moo:delegateMonitors', {}); + oldRemoveEvent.apply(this, [splitted.event, monitors[type]]); + delete monitors[type]; + } + return this; + } + + return oldRemoveEvent.apply(this, arguments); + }, + + fireEvent: function(type, args, delay, bind){ + var events = this.retrieve('events'); + if (!events || !events[type]) return this; + events[type].keys.each(function(fn){ + fn.create({bind: bind || this, delay: delay, arguments: args})(); + }, this); + return this; + } + + }); + +})(); + +/* +--- + +script: Fx.Elements.js + +description: Effect to change any number of CSS properties of any number of Elements. + +license: MIT-style license + +authors: +- Valerio Proietti + +requires: +- core:1.2.4/Fx.CSS +- /MooTools.More + +provides: [Fx.Elements] + +... +*/ + +Fx.Elements = new Class({ + + Extends: Fx.CSS, + + initialize: function(elements, options){ + this.elements = this.subject = $$(elements); + this.parent(options); + }, + + compute: function(from, to, delta){ + var now = {}; + for (var i in from){ + var iFrom = from[i], iTo = to[i], iNow = now[i] = {}; + for (var p in iFrom) iNow[p] = this.parent(iFrom[p], iTo[p], delta); + } + return now; + }, + + set: function(now){ + for (var i in now){ + var iNow = now[i]; + for (var p in iNow) this.render(this.elements[i], p, iNow[p], this.options.unit); + } + return this; + }, + + start: function(obj){ + if (!this.check(obj)) return this; + var from = {}, to = {}; + for (var i in obj){ + var iProps = obj[i], iFrom = from[i] = {}, iTo = to[i] = {}; + for (var p in iProps){ + var parsed = this.prepare(this.elements[i], p, iProps[p]); + iFrom[p] = parsed.from; + iTo[p] = parsed.to; + } + } + return this.parent(from, to); + } + +}); + +/* +--- + +script: Fx.Accordion.js + +description: An Fx.Elements extension which allows you to easily create accordion type controls. + +license: MIT-style license + +authors: +- Valerio Proietti + +requires: +- core:1.2.4/Element.Event +- /Fx.Elements + +provides: [Fx.Accordion] + +... +*/ + +var Accordion = Fx.Accordion = new Class({ + + Extends: Fx.Elements, + + options: {/* + onActive: $empty(toggler, section), + onBackground: $empty(toggler, section), + fixedHeight: false, + fixedWidth: false, + */ + display: 0, + show: false, + height: true, + width: false, + opacity: true, + alwaysHide: false, + trigger: 'click', + initialDisplayFx: true, + returnHeightToAuto: true + }, + + initialize: function(){ + var params = Array.link(arguments, {'container': Element.type, 'options': Object.type, 'togglers': $defined, 'elements': $defined}); + this.parent(params.elements, params.options); + this.togglers = $$(params.togglers); + this.container = document.id(params.container); + this.previous = -1; + this.internalChain = new Chain(); + if (this.options.alwaysHide) this.options.wait = true; + if ($chk(this.options.show)){ + this.options.display = false; + this.previous = this.options.show; + } + if (this.options.start){ + this.options.display = false; + this.options.show = false; + } + this.effects = {}; + if (this.options.opacity) this.effects.opacity = 'fullOpacity'; + if (this.options.width) this.effects.width = this.options.fixedWidth ? 'fullWidth' : 'offsetWidth'; + if (this.options.height) this.effects.height = this.options.fixedHeight ? 'fullHeight' : 'scrollHeight'; + for (var i = 0, l = this.togglers.length; i < l; i++) this.addSection(this.togglers[i], this.elements[i]); + this.elements.each(function(el, i){ + if (this.options.show === i){ + this.fireEvent('active', [this.togglers[i], el]); + } else { + for (var fx in this.effects) el.setStyle(fx, 0); + } + }, this); + if ($chk(this.options.display)) this.display(this.options.display, this.options.initialDisplayFx); + this.addEvent('complete', this.internalChain.callChain.bind(this.internalChain)); + }, + + addSection: function(toggler, element){ + toggler = document.id(toggler); + element = document.id(element); + var test = this.togglers.contains(toggler); + this.togglers.include(toggler); + this.elements.include(element); + var idx = this.togglers.indexOf(toggler); + var displayer = this.display.bind(this, idx); + toggler.store('accordion:display', displayer); + toggler.addEvent(this.options.trigger, displayer); + if (this.options.height) element.setStyles({'padding-top': 0, 'border-top': 'none', 'padding-bottom': 0, 'border-bottom': 'none'}); + if (this.options.width) element.setStyles({'padding-left': 0, 'border-left': 'none', 'padding-right': 0, 'border-right': 'none'}); + element.fullOpacity = 1; + if (this.options.fixedWidth) element.fullWidth = this.options.fixedWidth; + if (this.options.fixedHeight) element.fullHeight = this.options.fixedHeight; + element.setStyle('overflow', 'hidden'); + if (!test){ + for (var fx in this.effects) element.setStyle(fx, 0); + } + return this; + }, + + detach: function(){ + this.togglers.each(function(toggler) { + toggler.removeEvent(this.options.trigger, toggler.retrieve('accordion:display')); + }, this); + }, + + display: function(index, useFx){ + if (!this.check(index, useFx)) return this; + useFx = $pick(useFx, true); + if (this.options.returnHeightToAuto){ + var prev = this.elements[this.previous]; + if (prev && !this.selfHidden){ + for (var fx in this.effects){ + prev.setStyle(fx, prev[this.effects[fx]]); + } + } + } + index = ($type(index) == 'element') ? this.elements.indexOf(index) : index; + if ((this.timer && this.options.wait) || (index === this.previous && !this.options.alwaysHide)) return this; + this.previous = index; + var obj = {}; + this.elements.each(function(el, i){ + obj[i] = {}; + var hide; + if (i != index){ + hide = true; + } else if (this.options.alwaysHide && ((el.offsetHeight > 0 && this.options.height) || el.offsetWidth > 0 && this.options.width)){ + hide = true; + this.selfHidden = true; + } + this.fireEvent(hide ? 'background' : 'active', [this.togglers[i], el]); + for (var fx in this.effects) obj[i][fx] = hide ? 0 : el[this.effects[fx]]; + }, this); + this.internalChain.chain(function(){ + if (this.options.returnHeightToAuto && !this.selfHidden){ + var el = this.elements[index]; + if (el) el.setStyle('height', 'auto'); + }; + }.bind(this)); + return useFx ? this.start(obj) : this.set(obj); + } + +}); + +/* +--- + script: Fx.Slide.js description: Effect to slide an element in and out of view. @@ -182,6 +647,1142 @@ Element.implement({ }); +/* +--- + +script: Drag.js + +description: The base Drag Class. Can be used to drag and resize Elements using mouse events. + +license: MIT-style license + +authors: +- Valerio Proietti +- Tom Occhinno +- Jan Kassens + +requires: +- core:1.2.4/Events +- core:1.2.4/Options +- core:1.2.4/Element.Event +- core:1.2.4/Element.Style +- /MooTools.More + +provides: [Drag] + +*/ + +var Drag = new Class({ + + Implements: [Events, Options], + + options: {/* + onBeforeStart: $empty(thisElement), + onStart: $empty(thisElement, event), + onSnap: $empty(thisElement) + onDrag: $empty(thisElement, event), + onCancel: $empty(thisElement), + onComplete: $empty(thisElement, event),*/ + snap: 6, + unit: 'px', + grid: false, + style: true, + limit: false, + handle: false, + invert: false, + preventDefault: false, + stopPropagation: false, + modifiers: {x: 'left', y: 'top'} + }, + + initialize: function(){ + var params = Array.link(arguments, {'options': Object.type, 'element': $defined}); + this.element = document.id(params.element); + this.document = this.element.getDocument(); + this.setOptions(params.options || {}); + var htype = $type(this.options.handle); + this.handles = ((htype == 'array' || htype == 'collection') ? $$(this.options.handle) : document.id(this.options.handle)) || this.element; + this.mouse = {'now': {}, 'pos': {}}; + this.value = {'start': {}, 'now': {}}; + + this.selection = (Browser.Engine.trident) ? 'selectstart' : 'mousedown'; + + this.bound = { + start: this.start.bind(this), + check: this.check.bind(this), + drag: this.drag.bind(this), + stop: this.stop.bind(this), + cancel: this.cancel.bind(this), + eventStop: $lambda(false) + }; + this.attach(); + }, + + attach: function(){ + this.handles.addEvent('mousedown', this.bound.start); + return this; + }, + + detach: function(){ + this.handles.removeEvent('mousedown', this.bound.start); + return this; + }, + + start: function(event){ + if (event.rightClick) return; + if (this.options.preventDefault) event.preventDefault(); + if (this.options.stopPropagation) event.stopPropagation(); + this.mouse.start = event.page; + this.fireEvent('beforeStart', this.element); + var limit = this.options.limit; + this.limit = {x: [], y: []}; + for (var z in this.options.modifiers){ + if (!this.options.modifiers[z]) continue; + if (this.options.style) this.value.now[z] = this.element.getStyle(this.options.modifiers[z]).toInt(); + else this.value.now[z] = this.element[this.options.modifiers[z]]; + if (this.options.invert) this.value.now[z] *= -1; + this.mouse.pos[z] = event.page[z] - this.value.now[z]; + if (limit && limit[z]){ + for (var i = 2; i--; i){ + if ($chk(limit[z][i])) this.limit[z][i] = $lambda(limit[z][i])(); + } + } + } + if ($type(this.options.grid) == 'number') this.options.grid = {x: this.options.grid, y: this.options.grid}; + this.document.addEvents({mousemove: this.bound.check, mouseup: this.bound.cancel}); + this.document.addEvent(this.selection, this.bound.eventStop); + }, + + check: function(event){ + if (this.options.preventDefault) event.preventDefault(); + var distance = Math.round(Math.sqrt(Math.pow(event.page.x - this.mouse.start.x, 2) + Math.pow(event.page.y - this.mouse.start.y, 2))); + if (distance > this.options.snap){ + this.cancel(); + this.document.addEvents({ + mousemove: this.bound.drag, + mouseup: this.bound.stop + }); + this.fireEvent('start', [this.element, event]).fireEvent('snap', this.element); + } + }, + + drag: function(event){ + if (this.options.preventDefault) event.preventDefault(); + this.mouse.now = event.page; + for (var z in this.options.modifiers){ + if (!this.options.modifiers[z]) continue; + this.value.now[z] = this.mouse.now[z] - this.mouse.pos[z]; + if (this.options.invert) this.value.now[z] *= -1; + if (this.options.limit && this.limit[z]){ + if ($chk(this.limit[z][1]) && (this.value.now[z] > this.limit[z][1])){ + this.value.now[z] = this.limit[z][1]; + } else if ($chk(this.limit[z][0]) && (this.value.now[z] < this.limit[z][0])){ + this.value.now[z] = this.limit[z][0]; + } + } + if (this.options.grid[z]) this.value.now[z] -= ((this.value.now[z] - (this.limit[z][0]||0)) % this.options.grid[z]); + if (this.options.style) { + this.element.setStyle(this.options.modifiers[z], this.value.now[z] + this.options.unit); + } else { + this.element[this.options.modifiers[z]] = this.value.now[z]; + } + } + this.fireEvent('drag', [this.element, event]); + }, + + cancel: function(event){ + this.document.removeEvent('mousemove', this.bound.check); + this.document.removeEvent('mouseup', this.bound.cancel); + if (event){ + this.document.removeEvent(this.selection, this.bound.eventStop); + this.fireEvent('cancel', this.element); + } + }, + + stop: function(event){ + this.document.removeEvent(this.selection, this.bound.eventStop); + this.document.removeEvent('mousemove', this.bound.drag); + this.document.removeEvent('mouseup', this.bound.stop); + if (event) this.fireEvent('complete', [this.element, event]); + } + +}); + +Element.implement({ + + makeResizable: function(options){ + var drag = new Drag(this, $merge({modifiers: {x: 'width', y: 'height'}}, options)); + this.store('resizer', drag); + return drag.addEvent('drag', function(){ + this.fireEvent('resize', drag); + }.bind(this)); + } + +}); + + +/* +--- + +script: Drag.Move.js + +description: A Drag extension that provides support for the constraining of draggables to containers and droppables. + +license: MIT-style license + +authors: +- Valerio Proietti +- Tom Occhinno +- Jan Kassens +- Aaron Newton +- Scott Kyle + +requires: +- core:1.2.4/Element.Dimensions +- /Drag + +provides: [Drag.Move] + +... +*/ + +Drag.Move = new Class({ + + Extends: Drag, + + options: {/* + onEnter: $empty(thisElement, overed), + onLeave: $empty(thisElement, overed), + onDrop: $empty(thisElement, overed, event),*/ + droppables: [], + container: false, + precalculate: false, + includeMargins: true, + checkDroppables: true + }, + + initialize: function(element, options){ + this.parent(element, options); + element = this.element; + + this.droppables = $$(this.options.droppables); + this.container = document.id(this.options.container); + + if (this.container && $type(this.container) != 'element') + this.container = document.id(this.container.getDocument().body); + + var styles = element.getStyles('left', 'right', 'position'); + if (styles.left == 'auto' || styles.top == 'auto') + element.setPosition(element.getPosition(element.getOffsetParent())); + + if (styles.position == 'static') + element.setStyle('position', 'absolute'); + + this.addEvent('start', this.checkDroppables, true); + + this.overed = null; + }, + + start: function(event){ + if (this.container) this.options.limit = this.calculateLimit(); + + if (this.options.precalculate){ + this.positions = this.droppables.map(function(el){ + return el.getCoordinates(); + }); + } + + this.parent(event); + }, + + calculateLimit: function(){ + var offsetParent = this.element.getOffsetParent(), + containerCoordinates = this.container.getCoordinates(offsetParent), + containerBorder = {}, + elementMargin = {}, + elementBorder = {}, + containerMargin = {}, + offsetParentPadding = {}; + + ['top', 'right', 'bottom', 'left'].each(function(pad){ + containerBorder[pad] = this.container.getStyle('border-' + pad).toInt(); + elementBorder[pad] = this.element.getStyle('border-' + pad).toInt(); + elementMargin[pad] = this.element.getStyle('margin-' + pad).toInt(); + containerMargin[pad] = this.container.getStyle('margin-' + pad).toInt(); + offsetParentPadding[pad] = offsetParent.getStyle('padding-' + pad).toInt(); + }, this); + + var width = this.element.offsetWidth + elementMargin.left + elementMargin.right, + height = this.element.offsetHeight + elementMargin.top + elementMargin.bottom, + left = 0, + top = 0, + right = containerCoordinates.right - containerBorder.right - width, + bottom = containerCoordinates.bottom - containerBorder.bottom - height; + + if (this.options.includeMargins){ + left += elementMargin.left; + top += elementMargin.top; + } else { + right += elementMargin.right; + bottom += elementMargin.bottom; + } + + if (this.element.getStyle('position') == 'relative'){ + var coords = this.element.getCoordinates(offsetParent); + coords.left -= this.element.getStyle('left').toInt(); + coords.top -= this.element.getStyle('top').toInt(); + + left += containerBorder.left - coords.left; + top += containerBorder.top - coords.top; + right += elementMargin.left - coords.left; + bottom += elementMargin.top - coords.top; + + if (this.container != offsetParent){ + left += containerMargin.left + offsetParentPadding.left; + top += (Browser.Engine.trident4 ? 0 : containerMargin.top) + offsetParentPadding.top; + } + } else { + left -= elementMargin.left; + top -= elementMargin.top; + + if (this.container == offsetParent){ + right -= containerBorder.left; + bottom -= containerBorder.top; + } else { + left += containerCoordinates.left + containerBorder.left; + top += containerCoordinates.top + containerBorder.top; + } + } + + return { + x: [left, right], + y: [top, bottom] + }; + }, + + checkAgainst: function(el, i){ + el = (this.positions) ? this.positions[i] : el.getCoordinates(); + var now = this.mouse.now; + return (now.x > el.left && now.x < el.right && now.y < el.bottom && now.y > el.top); + }, + + checkDroppables: function(){ + var overed = this.droppables.filter(this.checkAgainst, this).getLast(); + if (this.overed != overed){ + if (this.overed) this.fireEvent('leave', [this.element, this.overed]); + if (overed) this.fireEvent('enter', [this.element, overed]); + this.overed = overed; + } + }, + + drag: function(event){ + this.parent(event); + if (this.options.checkDroppables && this.droppables.length) this.checkDroppables(); + }, + + stop: function(event){ + this.checkDroppables(); + this.fireEvent('drop', [this.element, this.overed, event]); + this.overed = null; + return this.parent(event); + } + +}); + +Element.implement({ + + makeDraggable: function(options){ + var drag = new Drag.Move(this, options); + this.store('dragger', drag); + return drag; + } + +}); + + +/* +--- + +script: Sortables.js + +description: Class for creating a drag and drop sorting interface for lists of items. + +license: MIT-style license + +authors: +- Tom Occhino + +requires: +- /Drag.Move + +provides: [Slider] + +... +*/ + +var Sortables = new Class({ + + Implements: [Events, Options], + + options: {/* + onSort: $empty(element, clone), + onStart: $empty(element, clone), + onComplete: $empty(element),*/ + snap: 4, + opacity: 1, + clone: false, + revert: false, + handle: false, + constrain: false + }, + + initialize: function(lists, options){ + this.setOptions(options); + this.elements = []; + this.lists = []; + this.idle = true; + + this.addLists($$(document.id(lists) || lists)); + if (!this.options.clone) this.options.revert = false; + if (this.options.revert) this.effect = new Fx.Morph(null, $merge({duration: 250, link: 'cancel'}, this.options.revert)); + }, + + attach: function(){ + this.addLists(this.lists); + return this; + }, + + detach: function(){ + this.lists = this.removeLists(this.lists); + return this; + }, + + addItems: function(){ + Array.flatten(arguments).each(function(element){ + this.elements.push(element); + var start = element.retrieve('sortables:start', this.start.bindWithEvent(this, element)); + (this.options.handle ? element.getElement(this.options.handle) || element : element).addEvent('mousedown', start); + }, this); + return this; + }, + + addLists: function(){ + Array.flatten(arguments).each(function(list){ + this.lists.push(list); + this.addItems(list.getChildren()); + }, this); + return this; + }, + + removeItems: function(){ + return $$(Array.flatten(arguments).map(function(element){ + this.elements.erase(element); + var start = element.retrieve('sortables:start'); + (this.options.handle ? element.getElement(this.options.handle) || element : element).removeEvent('mousedown', start); + + return element; + }, this)); + }, + + removeLists: function(){ + return $$(Array.flatten(arguments).map(function(list){ + this.lists.erase(list); + this.removeItems(list.getChildren()); + + return list; + }, this)); + }, + + getClone: function(event, element){ + if (!this.options.clone) return new Element('div').inject(document.body); + if ($type(this.options.clone) == 'function') return this.options.clone.call(this, event, element, this.list); + return element.clone(true).setStyles({ + margin: '0px', + position: 'absolute', + visibility: 'hidden', + 'width': element.getStyle('width') + }).inject(this.list).setPosition(element.getPosition(element.getOffsetParent())); + }, + + getDroppables: function(){ + var droppables = this.list.getChildren(); + if (!this.options.constrain) droppables = this.lists.concat(droppables).erase(this.list); + return droppables.erase(this.clone).erase(this.element); + }, + + insert: function(dragging, element){ + var where = 'inside'; + if (this.lists.contains(element)){ + this.list = element; + this.drag.droppables = this.getDroppables(); + } else { + where = this.element.getAllPrevious().contains(element) ? 'before' : 'after'; + } + this.element.inject(element, where); + this.fireEvent('sort', [this.element, this.clone]); + }, + + start: function(event, element){ + if (!this.idle) return; + this.idle = false; + this.element = element; + this.opacity = element.get('opacity'); + this.list = element.getParent(); + this.clone = this.getClone(event, element); + + this.drag = new Drag.Move(this.clone, { + snap: this.options.snap, + container: this.options.constrain && this.element.getParent(), + droppables: this.getDroppables(), + onSnap: function(){ + event.stop(); + this.clone.setStyle('visibility', 'visible'); + this.element.set('opacity', this.options.opacity || 0); + this.fireEvent('start', [this.element, this.clone]); + }.bind(this), + onEnter: this.insert.bind(this), + onCancel: this.reset.bind(this), + onComplete: this.end.bind(this) + }); + + this.clone.inject(this.element, 'before'); + this.drag.start(event); + }, + + end: function(){ + this.drag.detach(); + this.element.set('opacity', this.opacity); + if (this.effect){ + var dim = this.element.getStyles('width', 'height'); + var pos = this.clone.computePosition(this.element.getPosition(this.clone.offsetParent)); + this.effect.element = this.clone; + this.effect.start({ + top: pos.top, + left: pos.left, + width: dim.width, + height: dim.height, + opacity: 0.25 + }).chain(this.reset.bind(this)); + } else { + this.reset(); + } + }, + + reset: function(){ + this.idle = true; + this.clone.destroy(); + this.fireEvent('complete', this.element); + }, + + serialize: function(){ + var params = Array.link(arguments, {modifier: Function.type, index: $defined}); + var serial = this.lists.map(function(list){ + return list.getChildren().map(params.modifier || function(element){ + return element.get('id'); + }, this); + }, this); + + var index = params.index; + if (this.lists.length == 1) index = 0; + return $chk(index) && index >= 0 && index < this.lists.length ? serial[index] : serial; + } + +}); + + +/* +--- + +script: HtmlTable.js + +description: Builds table elements with methods to add rows. + +license: MIT-style license + +authors: +- Aaron Newton + +requires: +- core:1.2.4/Options +- core:1.2.4/Events +- /Class.Occlude + +provides: [HtmlTable] + +... +*/ + +var HtmlTable = new Class({ + + Implements: [Options, Events, Class.Occlude], + + options: { + properties: { + cellpadding: 0, + cellspacing: 0, + border: 0 + }, + rows: [], + headers: [], + footers: [] + }, + + property: 'HtmlTable', + + initialize: function(){ + var params = Array.link(arguments, {options: Object.type, table: Element.type}); + this.setOptions(params.options); + this.element = params.table || new Element('table', this.options.properties); + if (this.occlude()) return this.occluded; + this.build(); + }, + + build: function(){ + this.element.store('HtmlTable', this); + + this.body = document.id(this.element.tBodies[0]) || new Element('tbody').inject(this.element); + $$(this.body.rows); + + if (this.options.headers.length) this.setHeaders(this.options.headers); + else this.thead = document.id(this.element.tHead); + if (this.thead) this.head = document.id(this.thead.rows[0]); + + if (this.options.footers.length) this.setFooters(this.options.footers); + this.tfoot = document.id(this.element.tFoot); + if (this.tfoot) this.foot = document.id(this.thead.rows[0]); + + this.options.rows.each(function(row){ + this.push(row); + }, this); + + ['adopt', 'inject', 'wraps', 'grab', 'replaces', 'dispose'].each(function(method){ + this[method] = this.element[method].bind(this.element); + }, this); + }, + + toElement: function(){ + return this.element; + }, + + empty: function(){ + this.body.empty(); + return this; + }, + + setHeaders: function(headers){ + this.thead = (document.id(this.element.tHead) || new Element('thead').inject(this.element, 'top')).empty(); + this.push(headers, this.thead, 'th'); + this.head = document.id(this.thead.rows[0]); + return this; + }, + + setFooters: function(footers){ + this.tfoot = (document.id(this.element.tFoot) || new Element('tfoot').inject(this.element, 'top')).empty(); + this.push(footers, this.tfoot); + this.foot = document.id(this.thead.rows[0]); + return this; + }, + + push: function(row, target, tag){ + var tds = row.map(function(data){ + var td = new Element(tag || 'td', data.properties), + type = data.content || data || '', + element = document.id(type); + + if(element) td.adopt(element); + else td.set('html', type); + + return td; + }); + + return { + tr: new Element('tr').inject(target || this.body).adopt(tds), + tds: tds + }; + } + +}); + + +/* +--- + +script: HtmlTable.Select.js + +description: Builds a stripy, sortable table with methods to add rows. Rows can be selected with the mouse or keyboard navigation. + +license: MIT-style license + +authors: +- Harald Kirschner +- Aaron Newton + +requires: +- /Keyboard +- /HtmlTable +- /Class.refactor +- /Element.Delegation + +provides: [HtmlTable.Select] + +... +*/ + +HtmlTable = Class.refactor(HtmlTable, { + + options: { + /*onRowSelect: $empty, + onRowUnselect: $empty,*/ + useKeyboard: true, + classRowSelected: 'table-tr-selected', + classRowHovered: 'table-tr-hovered', + classSelectable: 'table-selectable', + allowMultiSelect: true, + selectable: false + }, + + initialize: function(){ + this.previous.apply(this, arguments); + if (this.occluded) return this.occluded; + this.selectedRows = new Elements(); + this.bound = { + mouseleave: this.mouseleave.bind(this), + focusRow: this.focusRow.bind(this) + }; + if (this.options.selectable) this.enableSelect(); + }, + + enableSelect: function(){ + this.selectEnabled = true; + this.attachSelects(); + this.element.addClass(this.options.classSelectable); + }, + + disableSelect: function(){ + this.selectEnabled = false; + this.attach(false); + this.element.removeClass(this.options.classSelectable); + }, + + attachSelects: function(attach){ + attach = $pick(attach, true); + var method = attach ? 'addEvents' : 'removeEvents'; + this.element[method]({ + mouseleave: this.bound.mouseleave + }); + this.body[method]({ + 'click:relay(tr)': this.bound.focusRow + }); + if (this.options.useKeyboard || this.keyboard){ + if (!this.keyboard) this.keyboard = new Keyboard({ + events: { + down: function(e) { + e.preventDefault(); + this.shiftFocus(1); + }.bind(this), + up: function(e) { + e.preventDefault(); + this.shiftFocus(-1); + }.bind(this), + enter: function(e) { + e.preventDefault(); + if (this.hover) this.focusRow(this.hover); + }.bind(this) + }, + active: true + }); + this.keyboard[attach ? 'activate' : 'deactivate'](); + } + this.updateSelects(); + }, + + mouseleave: function(){ + if (this.hover) this.leaveRow(this.hover); + }, + + focus: function(){ + if (this.keyboard) this.keyboard.activate(); + }, + + blur: function(){ + if (this.keyboard) this.keyboard.deactivate(); + }, + + push: function(){ + var ret = this.previous.apply(this, arguments); + this.updateSelects(); + return ret; + }, + + updateSelects: function(){ + Array.each(this.body.rows, function(row){ + var binders = row.retrieve('binders'); + if ((binders && this.selectEnabled) || (!binders && !this.selectEnabled)) return; + if (!binders){ + binders = { + mouseenter: this.enterRow.bind(this, [row]), + mouseleave: this.leaveRow.bind(this, [row]) + }; + row.store('binders', binders).addEvents(binders); + } else { + row.removeEvents(binders); + } + }, this); + }, + + enterRow: function(row){ + if (this.hover) this.hover = this.leaveRow(this.hover); + this.hover = row.addClass(this.options.classRowHovered); + }, + + shiftFocus: function(offset){ + if (!this.hover) return this.enterRow(this.body.rows[0]); + var to = Array.indexOf(this.body.rows, this.hover) + offset; + if (to < 0) to = 0; + if (to >= this.body.rows.length) to = this.body.rows.length - 1; + if (this.hover == this.body.rows[to]) return this; + this.enterRow(this.body.rows[to]); + }, + + leaveRow: function(row){ + row.removeClass(this.options.classRowHovered); + }, + + focusRow: function(){ + var row = arguments[1] || arguments[0]; //delegation passes the event first + if (!this.body.getChildren().contains(row)) return; + var unfocus = function(row){ + this.selectedRows.erase(row); + row.removeClass(this.options.classRowSelected); + this.fireEvent('rowUnfocus', [row, this.selectedRows]); + }.bind(this); + if (!this.options.allowMultiSelect) this.selectedRows.each(unfocus); + if (!this.selectedRows.contains(row)) { + this.selectedRows.push(row); + row.addClass(this.options.classRowSelected); + this.fireEvent('rowFocus', [row, this.selectedRows]); + } else { + unfocus(row); + } + return false; + }, + + selectAll: function(status){ + status = $pick(status, true); + if (!this.options.allowMultiSelect && status) return; + if (!status) this.selectedRows.removeClass(this.options.classRowSelected).empty(); + else this.selectedRows.combine(this.body.rows).addClass(this.options.classRowSelected); + return this; + }, + + selectNone: function(){ + return this.selectAll(false); + } + +}); + +/* +--- + +script: Keyboard.js + +description: KeyboardEvents used to intercept events on a class for keyboard and format modifiers in a specific order so as to make alt+shift+c the same as shift+alt+c. + +license: MIT-style license + +authors: +- Perrin Westrich +- Aaron Newton +- Scott Kyle + +requires: +- core:1.2.4/Events +- core:1.2.4/Options +- core:1.2.4/Element.Event +- /Log + +provides: [Keyboard] + +... +*/ + +(function(){ + + var parsed = {}; + var modifiers = ['shift', 'control', 'alt', 'meta']; + var regex = /^(?:shift|control|ctrl|alt|meta)$/; + + var parse = function(type, eventType){ + type = type.toLowerCase().replace(/^(keyup|keydown):/, function($0, $1){ + eventType = $1; + return ''; + }); + + if (!parsed[type]){ + var key = '', mods = {}; + type.split('+').each(function(part){ + if (regex.test(part)) mods[part] = true; + else key = part; + }); + + mods.control = mods.control || mods.ctrl; // allow both control and ctrl + var match = ''; + modifiers.each(function(mod){ + if (mods[mod]) match += mod + '+'; + }); + + parsed[type] = match + key; + } + + return eventType + ':' + parsed[type]; + }; + + this.Keyboard = new Class({ + + Extends: Events, + + Implements: [Options, Log], + + options: { + /* + onActivate: $empty, + onDeactivate: $empty, + */ + defaultEventType: 'keydown', + active: false, + events: {} + }, + + initialize: function(options){ + this.setOptions(options); + //if this is the root manager, nothing manages it + if (Keyboard.manager) Keyboard.manager.manage(this); + this.setup(); + }, + + setup: function(){ + this.addEvents(this.options.events); + if (this.options.active) this.activate(); + }, + + handle: function(event, type){ + //Keyboard.stop(event) prevents key propagation + if (!this.active || event.preventKeyboardPropagation) return; + + var bubbles = !!this.manager; + if (bubbles && this.activeKB){ + this.activeKB.handle(event, type); + if (event.preventKeyboardPropagation) return; + } + this.fireEvent(type, event); + + if (!bubbles && this.activeKB) this.activeKB.handle(event, type); + }, + + addEvent: function(type, fn, internal) { + return this.parent(parse(type, this.options.defaultEventType), fn, internal); + }, + + removeEvent: function(type, fn) { + return this.parent(parse(type, this.options.defaultEventType), fn); + }, + + activate: function(){ + this.active = true; + return this.enable(); + }, + + deactivate: function(){ + this.active = false; + return this.fireEvent('deactivate'); + }, + + toggleActive: function(){ + return this[this.active ? 'deactivate' : 'activate'](); + }, + + enable: function(instance){ + if (instance) { + //if we're stealing focus, store the last keyboard to have it so the relenquish command works + if (instance != this.activeKB) this.previous = this.activeKB; + //if we're enabling a child, assign it so that events are now passed to it + this.activeKB = instance.fireEvent('activate'); + } else if (this.manager) { + //else we're enabling ourselves, we must ask our parent to do it for us + this.manager.enable(this); + } + return this; + }, + + relenquish: function(){ + if (this.previous) this.enable(this.previous); + }, + + //management logic + manage: function(instance) { + if (instance.manager) instance.manager.drop(instance); + this.instances.push(instance); + instance.manager = this; + if (!this.activeKB) this.enable(instance); + else this._disable(instance); + }, + + _disable: function(instance) { + if (this.activeKB == instance) this.activeKB = null; + }, + + drop: function(instance) { + this._disable(instance); + this.instances.erase(instance); + }, + + instances: [], + + trace: function(){ + this.enableLog(); + var item = this; + this.log('the following items have focus: '); + while (item) { + this.log(document.id(item.widget) || item.widget || item, 'active: ' + this.active); + item = item.activeKB; + } + } + + }); + + Keyboard.stop = function(event) { + event.preventKeyboardPropagation = true; + }; + + Keyboard.manager = new this.Keyboard({ + active: true + }); + + Keyboard.trace = function(){ + Keyboard.manager.trace(); + }; + + var handler = function(event){ + var mods = ''; + modifiers.each(function(mod){ + if (event[mod]) mods += mod + '+'; + }); + Keyboard.manager.handle(event, event.type + ':' + mods + event.key); + }; + + document.addEvents({ + 'keyup': handler, + 'keydown': handler + }); + + Event.Keys.extend({ + 'pageup': 33, + 'pagedown': 34, + 'end': 35, + 'home': 36, + 'capslock': 20, + 'numlock': 144, + 'scrolllock': 145 + }); + +})(); + + +/* +--- + +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 +1812,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/js/settings.js b/static/js/settings.js index 014a8b98f..5552a095d 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -3,10 +3,25 @@ window.addEvent('domready', function() { parse: 'rel', }); + setDivHeight("content", 20, 0); if ($$("a[name=deletePerm]")) $$("a[name=deletePerm]").addEvent('click', function(event) { if (!confirm(gettext("Remove this permission?"))) event.stop(); }); + + var tip = new Tips($$(".Tips")); + + var accordion = new Accordion('h3.atStart', 'div.atStart', { + opacity: false, + onActive: function(toggler, element){ + toggler.setStyle('color', '#ff3300'); + }, + + onBackground: function(toggler, element){ + toggler.setStyle('color', '#222'); + } + }, $('accordion')); + }); \ No newline at end of file diff --git a/static/pics/answered.png b/static/pics/answered.png new file mode 100644 index 000000000..9d31e533f Binary files /dev/null and b/static/pics/answered.png differ diff --git a/static/pics/clear.png b/static/pics/clear.png new file mode 100644 index 000000000..e6c8e8b9f Binary files /dev/null and b/static/pics/clear.png differ diff --git a/static/pics/down_arrow.png b/static/pics/down_arrow.png new file mode 100644 index 000000000..f5f128b26 Binary files /dev/null and b/static/pics/down_arrow.png differ 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/reply-all.png b/static/pics/reply-all.png new file mode 100644 index 000000000..2017b0af0 Binary files /dev/null and b/static/pics/reply-all.png differ diff --git a/static/pics/reply.png b/static/pics/reply.png new file mode 100644 index 000000000..a61974159 Binary files /dev/null and b/static/pics/reply.png differ diff --git a/static/pics/send-receive.png b/static/pics/send-receive.png new file mode 100644 index 000000000..3eb6a9ce3 Binary files /dev/null and b/static/pics/send-receive.png differ diff --git a/static/pics/up_arrow.png b/static/pics/up_arrow.png new file mode 100644 index 000000000..524fb5ebd Binary files /dev/null and b/static/pics/up_arrow.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/box.css b/static/stylesheets/box.css index 3368b9ac5..6b1331ce6 100644 --- a/static/stylesheets/box.css +++ b/static/stylesheets/box.css @@ -31,6 +31,14 @@ input:hover { background-color: #ffffcc; } +td.odd { + background: #F7F9Fb; +} + +td.even { + +} + .title { float: left; width: 100%; diff --git a/static/stylesheets/calendar.css b/static/stylesheets/calendar.css new file mode 100644 index 000000000..b9c6db02d --- /dev/null +++ b/static/stylesheets/calendar.css @@ -0,0 +1,91 @@ +/* + Document : calendar.css + Created on : 29.07.2009 + Author : jens klose + Description: + example styles for calendar +*/ +.calendarForm{ + float:right; +} +.calendar{ + font-family:verdana,sans-serif; + font-size:10px; + float:None; +} +button.calendar{ + position:relative; + top:-3px; + left:1px; + background-color:transparent; + background-image:url(../pics/calendar.gif); + border:none; + margin: 0px; + padding:0px; + margin-right:3px; + cursor:pointer; + width:16px; + height:16px; +} +input.calendar{ + cursor:pointer; + border: 1px solid #87847F; + height:12px; + width:100px; + color: #87847F; +} +div.calendar{ + background:white; + border: 1px solid #87847F; + width:180px +} +a.footer{ + color:#474747; + padding:2px; +} +.calendar table{ + width:100%; +} +.calendar th, +.calendar td{ + text-align:center; + font-size:10px; + padding: 0.1em 0.25em 0.1em 0.3em; +} +.calendar tbody td, +.calendar .invalid{ + color:silver; +} +.calendar .valid{ + color:blue; + font-weight:bold; + cursor:pointer; +} +.calendar .valid.hover{ + background:#FFFF77; + font-weight:bold; +} +.calendar caption a, +.calendar .next{ + color:blue; + font-weight:bold; + padding:2px; + cursor:pointer; +} +.calendar caption a, +.calendar .prev{ + color:blue; + font-weight:bold; + padding:2px; +} +.calendar .active, +.calendar .hilite, +.calendar .inactive{ + color:white; + background-color:#BE3916; + font-weight:bold; +} +.calendar .active, +.calendar .inactive{ + background-color:red; +} \ No newline at end of file diff --git a/static/stylesheets/default.css b/static/stylesheets/default.css new file mode 100644 index 000000000..008f1f96e --- /dev/null +++ b/static/stylesheets/default.css @@ -0,0 +1,329 @@ +html, body { + font-family: arial, helvetica, verdana, tahoma, sans-serif; + font-size: small; + background: #153F65; + margin: 0; + padding: 0; + height: 100%; + overflow: hidden; +} + +div#container { + height: auto !important; + height: 100%; + min-height: 100%; + position: relative; + background: white; +} + +@media screen { + body > div#footer { + position: fixed; + } +} + +div#topmenubar { + position: fixed; + border-bottom: 5px solid #333; + height: 30px; + bottom: auto; +} + +div#footer { + position: absolute; + height: 30px; + bottom: 0px; + border-top: 5px solid #333; +} + +#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 { + width: 100%; + top: 35px; + bottom: 35px; + position: absolute; +} + +#content { + position: fixed; + overflow: auto; + padding: 5px; + top: 130px; + bottom: 35px; + width: 100%; +} + +#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; +} + +.uibar { + background: #153F65; + padding: 0; + margin: 0 auto; + width: 100%; +} + +.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 { + float: 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; +} + + +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; +} + +table.hr { + width: 80%; + margin: 10px auto; +} + +.hr td.odd { + background: #F7F9Fb; +} + +.hr td.even { + +} + +.unseen { + font-weight: bold; +} + +.toggler { + margin: 5px auto; + padding: 2px 5px; + background: #CEDBEC; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + /*border: 1px solid #CCC;*/ + font-size: 1.1em; +} + +.element { + padding-left: 10px; +} + +div.row { + margin: 10px; +} + +div.row label { + float: left; + width: 200px; + padding: 3px 0; +} + +.error { + margin: 0 auto; + padding: 5px; + background: red; + width: 500px; + text-align: center; + background-color: #F9D2D2; + border: 3px solid #FF2A2A; + color: #FF2A00; +} \ No newline at end of file diff --git a/static/stylesheets/login.css b/static/stylesheets/login.css new file mode 100644 index 000000000..798736283 --- /dev/null +++ b/static/stylesheets/login.css @@ -0,0 +1,75 @@ +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; + background: white; + + border: 5px solid #333; + /*-moz-box-shadow: 0 10px 30px #222;*/ + -webkit-box-shadow: 0 10px 30px #222; + -moz-border-radius: 10px; + -webkit-border-radius: 5px; +} + +#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/layout.html b/templates/admin/layout.html index 2566b41b3..b63af4b4f 100644 --- a/templates/admin/layout.html +++ b/templates/admin/layout.html @@ -1,4 +1,4 @@ -{% extends "layout.html" %} +{% extends "nlayout.html" %} {% block content %} {% load i18n %}
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 new file mode 100644 index 000000000..f8f01c661 --- /dev/null +++ b/templates/admin/parameters.html @@ -0,0 +1,22 @@ +{% extends "admin/settings_header.html" %} +{% block menu %} +{% load admin_extras %} +{% settings_menu "parameters" perms %} +{% endblock %} +{% block setcontent %} +{% load i18n %} +{% url admin.views.saveparameters as action %} + +
+ {% for app in gparams %} +

{{ app.name }}

+
+ {% for p in app.params %} + {% param app.name p %} + {% endfor %} +
+ {% endfor %} +
+

+ +{% endblock %} diff --git a/templates/admin/permissions.html b/templates/admin/permissions.html index 1ecd1dee8..6295edf95 100644 --- a/templates/admin/permissions.html +++ b/templates/admin/permissions.html @@ -1,7 +1,7 @@ {% extends "admin/settings_header.html" %} {% block menu %} {% load admin_extras %} -{% settings_menu "permissions" %} +{% settings_menu "permissions" perms %} {% endblock %} {% block setcontent %} {% load i18n %} 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/admin/settings_menu.html b/templates/admin/settings_menu.html deleted file mode 100644 index 5463b8fd3..000000000 --- a/templates/admin/settings_menu.html +++ /dev/null @@ -1,18 +0,0 @@ -{% load i18n %} - diff --git a/templates/common/email_listing.html b/templates/common/email_listing.html new file mode 100644 index 000000000..959388161 --- /dev/null +++ b/templates/common/email_listing.html @@ -0,0 +1,61 @@ +{% extends "nlayout.html" %} +{% load i18n %} +{% block html_head %} + + +{% endblock %} +{% block content %} +{% block leftcol %}{% endblock %} +
{{ listing }}
+
+ +{% endblock %} diff --git a/templates/common/error.html b/templates/common/error.html new file mode 100644 index 000000000..11677caf5 --- /dev/null +++ b/templates/common/error.html @@ -0,0 +1,6 @@ +{% extends "nlayout.html" %} +{% load i18n %} +{% block middletitle %}{% trans "Error" %}{% endblock %} +{% block content %} +

{{ error }}

+{% 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/getmailcontent.html b/templates/common/getmailcontent.html new file mode 100644 index 000000000..872bdaf88 --- /dev/null +++ b/templates/common/getmailcontent.html @@ -0,0 +1 @@ +{% if pre %}
{% endif %}{{ body }}{% if pre %}
{% endif %} diff --git a/templates/common/navbar.html b/templates/common/navbar.html new file mode 100644 index 000000000..668152024 --- /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/table_body.html b/templates/common/table_body.html new file mode 100644 index 000000000..425f5d962 --- /dev/null +++ b/templates/common/table_body.html @@ -0,0 +1,13 @@ +
+ {% for row in table.rows %} + + {% for cell in row.cols %} + {% if cell.safe %} + + {% else %} + + {% endif %} + {% endfor %} + + {% endfor %} + diff --git a/templates/common/table_head.html b/templates/common/table_head.html new file mode 100644 index 000000000..79bc8f15c --- /dev/null +++ b/templates/common/table_head.html @@ -0,0 +1,10 @@ + + + {% for c in table.columns %} + + {% endfor %} + + diff --git a/templates/common/tables.html b/templates/common/tables.html new file mode 100644 index 000000000..37fa156d3 --- /dev/null +++ b/templates/common/tables.html @@ -0,0 +1,26 @@ +
{% 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") }}
{{ cell.value|safe }}{{ cell.value }}
+ {{ c|safe }} + +
+ + + {% 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..a9bdddaf9 100644 --- a/templates/main/header.html +++ b/templates/main/header.html @@ -1,7 +1,9 @@ -{% extends "layout.html" %} +{% extends "nlayout.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 %}