Merge branch 'master' into feature/radicale

This commit is contained in:
Antoine Nguyen
2014-05-20 09:40:25 +02:00
20 changed files with 778 additions and 233 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ copyright = u'2014, Antoine Nguyen'
# The short X.Y version.
version = '1.1'
# The full version, including alpha/beta/rc tags.
release = '1.1.1'
release = '1.1.2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
+42 -13
View File
@@ -197,13 +197,48 @@ SQLite users
Replace values between ``<>`` with yours.
LDA
===
LMTP
====
The LDA is activated by default but you must define a *postmaster*
address. Open the :file:`conf.d/15-lda.conf` file modify the following line::
`Local Mail Transport Protocol
<http://en.wikipedia.org/wiki/Local_Mail_Transfer_Protocol>`_ is used
to let Postfix deliver messages to Dovecot.
postmaster_address = postmaster@<domain>
First, make sure the protocol is activated by looking at the
``protocols`` setting (generally inside
:file:`dovecot.conf`). It should be similar to the following example::
protocols = imap pop3 lmtp
Then, open the :file:`conf.d/10-master.conf`, look for ``lmtp``
service definition and add the following content inside::
service lmtp {
# stuff before
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
# stuff after
}
We assume here that Postfix is *chrooted* within
:file:`/var/spool/postfix`.
Finally, open the :file:`conf.d/20-lmtp.conf` and modify it as follows::
protocol lmtp {
postmaster_address = postmaster@<domain>
mail_plugins = $mail_plugins quota sieve
}
Replace ``<domain>`` by the appropriate value.
.. note::
If you don't plan to apply quota or to use filters, just adapt the
content of the ``mail_plugins`` setting.
.. _dovecot_quota:
@@ -454,8 +489,7 @@ Use the following configuration in the :file:`/etc/postfix/main.cf` file
(this is just one possible configuration)::
# Stuff before
virtual_transport = dovecot
dovecot_destination_recipient_limit = 1
virtual_transport = lmtp:unix:private/dovecot-lmtp
relay_domains =
virtual_mailbox_domains = <driver>:/etc/postfix/sql-domains.cf
@@ -468,16 +502,11 @@ Use the following configuration in the :file:`/etc/postfix/main.cf` file
...
check_recipient_access <driver>:/etc/postfix/sql-maintain.cf
permit_mynetworks
reject_unverified_recipient
...
# Stuff after
Replace ``<driver>`` by the name of the database you use.
Then, edit the :file:`/etc/postfix/master.cf` file and add the following
definition at the end::
dovecot unix - n n - - pipe
flags=DRhu user=vmail:vmail argv=/usr/lib/dovecot/deliver -f ${sender} -d ${recipient}
Restart Postfix.
+1 -1
View File
@@ -30,7 +30,7 @@ class AccountFormGeneral(forms.ModelForm):
help_text=ugettext_lazy("What level of permission this user will have")
)
password1 = forms.CharField(
label=_("Password"), widget=forms.PasswordInput
label=ugettext_lazy("Password"), widget=forms.PasswordInput
)
password2 = forms.CharField(
label=ugettext_lazy("Confirmation"),
+1 -1
View File
@@ -118,7 +118,7 @@ class SQLconnector(MBconnector):
rs = qm["mail__msgrcpt__rs"]
if rs == 'D':
continue
elif rs == '':
elif rs in ['', ' ']:
m["class"] = "unseen"
elif rs == 'R':
m["img_rstatus"] = static_url("pics/release.png")
+1 -1
View File
@@ -128,7 +128,7 @@ def viewmail(request, mail_id):
rcpt = request.GET["rcpt"]
if request.user.mailbox_set.count():
mb = Mailbox.objects.get(user=request.user)
if rcpt in mb.alias_addresses:
if rcpt == mb.full_address or rcpt in mb.alias_addresses:
get_wrapper().set_msgrcpt_status(rcpt, mail_id, 'V')
content = Template("""
@@ -15,8 +15,8 @@ def menu(target, user):
@events.observe("GetGraphSets")
def get_default_graph_sets():
from modoboa.extensions.stats.graph_templates import MailTraffic
def get_default_graphic_sets():
from modoboa.extensions.stats.graphics import MailTraffic
gset = MailTraffic()
return {gset.html_id: gset}
@@ -1,79 +0,0 @@
# coding: utf-8
from django.utils.translation import ugettext_lazy
class Graph(object):
width = 540
height = 120
cf = 'AVERAGE'
def __init__(self, **ds):
self.vars = {}
for dsname, dsdef in ds.iteritems():
self.vars[dsname] = dsdef
@property
def display_name(self):
return self.__class__.__name__.lower()
class AvgTraffic(Graph):
title = ugettext_lazy('Average normal traffic')
vertlabel = ugettext_lazy('msgs/min')
def __init__(self):
super(AvgTraffic, self).__init__(
sent={'type': 'AREA', 'color': '#00FF00',
"legend": ugettext_lazy("sent messages")},
recv={"type": "AREA", "color": "#0000FF",
"legend": ugettext_lazy("received messages")}
)
class AvgBadTraffic(Graph):
title = ugettext_lazy('Average bad traffic')
vertlabel = ugettext_lazy('msgs/min')
def __init__(self):
super(AvgBadTraffic, self).__init__(
bounced={'type': 'AREA', 'color': '#FFFF00',
"legend": ugettext_lazy("bounced messages")},
reject={"type": "AREA", "color": "#FF0000",
"legend": ugettext_lazy("rejected messages")}
)
class AvgTrafficSize(Graph):
title = ugettext_lazy('Average normal traffic size')
vertlabel = ugettext_lazy('bytes/min')
def __init__(self):
super(AvgTrafficSize, self).__init__(
size_recv={'type': 'AREA', 'color': '#FF9900',
"legend": ugettext_lazy("received size")},
size_sent={"type": "AREA", "color": "#339999",
"legend": ugettext_lazy("sent size")}
)
class GraphSet(object):
title = None
graphs = []
@property
def html_id(self):
return self.__class__.__name__.lower()
def get_graphs(self):
if not hasattr(self, '__graph_instances'):
self.__graph_instances = [gc() for gc in self.graphs]
return self.__graph_instances
def get_graph_names(self):
return [g.display_name for g in self.get_graphs()]
class MailTraffic(GraphSet):
title = ugettext_lazy('Mail traffic')
graphs = [AvgTraffic, AvgBadTraffic, AvgTrafficSize]
-94
View File
@@ -1,94 +0,0 @@
# coding: utf-8
import sys
import os
import time
from django.utils.translation import ugettext as _, ugettext_lazy
from django.conf import settings
from modoboa.lib import parameters
periods = [{"name": "day", "label": ugettext_lazy("Day")},
{"name": "week", "label": ugettext_lazy("Week")},
{"name": "month", "label": ugettext_lazy("Month")},
{"name": "year", "label": ugettext_lazy("Year")}]
def str2Time(y, M, d, h="00", m="00", s="00"):
"""Date conversion
Returns a date and a time in seconds from the epoch.
:return: an integer
"""
try:
if not M.isdigit():
local = time.strptime("%s %s %s %s:%s:%s" % (y, M, d, h, m, s),
"%Y %b %d %H:%M:%S")
else:
local = time.strptime("%s %s %s %s:%s:%s" % (y, M, d, h, m, s),
"%Y %m %d %H:%M:%S")
except ValueError:
print >> sys.stderr, "Error: failed to convert date and time"
return 0
return int(time.mktime(local))
class Grapher(object):
def __init__(self):
self.rrd_rootdir = parameters.get_admin("RRD_ROOTDIR")
self.img_rootdir = os.path.join(settings.MEDIA_ROOT, "stats")
def process(self, target, suffix, start, end, graph_tpl):
import rrdtool
rrdfile = "%s/%s.rrd" % (self.rrd_rootdir, target)
if not os.path.exists(rrdfile):
return
ext = "png"
path = "%s/%s_%s_%s_%s.%s" % (self.img_rootdir, graph_tpl.display_name,
target, graph_tpl.cf, suffix, ext)
start = str(start)
end = str(end)
defs = []
lines = []
for v, d in graph_tpl.vars.iteritems():
defs += [str('DEF:%s=%s:%s:%s' % (v, rrdfile, v, graph_tpl.cf)),
str('CDEF:%spm=%s,60,*' % (v, v)),
str('VDEF:%s_total=%s,TOTAL' % (v, v))]
type = d["type"] if "type" in d else "LINE"
lines += [str("%s:%spm%s:%s:STACK" % (type, v, d["color"],
(d["legend"].encode("utf8")).ljust(20)))]
lines.append('GPRINT:%s_total:%s%%7.0lf%%s\\l' % (v, _("Total").encode('utf-8')))
first_ds = graph_tpl.vars.keys()[0]
defs.append(str('VDEF:%s_first=%s,FIRST' % (first_ds, first_ds)))
defs.append(str('VDEF:%s_last=%s,LAST' % (first_ds, first_ds)))
defs.append('GPRINT:%s_first:%s %%c:strftime' % (first_ds, "From"))
defs.append('GPRINT:%s_last:%s %%c\\c:strftime' % (first_ds, "to"))
defs.append('COMMENT:\\s')
params = defs + lines
rrdtool.graph(
str(path),
"--imgformat", "PNG",
"--width", str(graph_tpl.width),
"--height", str(graph_tpl.height),
"--start", start,
"--end", end,
"--vertical-label", graph_tpl.vertlabel.encode("utf-8"),
"--lower-limit", "0",
"--slope-mode",
"--units-exponent", "0",
"--title", str("%s: %s" % (target, graph_tpl.title.encode("utf-8"))),
*params
)
if not os.path.exists(path):
print "[graph] Impossible to create %s graph" % path
def make_defaults(self, target, graph_tpl):
end = "%d" % int(time.mktime(time.localtime()))
self.process(target, "day", "-1day", end, graph_tpl)
self.process(target, "week", "-1week", end, graph_tpl)
self.process(target, "month", "-1month", end, graph_tpl)
self.process(target, "year", "-1year", end, graph_tpl)
+155
View File
@@ -0,0 +1,155 @@
import os
import inspect
from lxml import etree
from django.utils.translation import ugettext_lazy
from modoboa.lib import parameters
from modoboa.lib.sysutils import exec_cmd
class Curve(object):
"""Graphic curve.
Simple way to represent a graphic curve.
"""
def __init__(self, dsname, color, legend, cfunc="AVERAGE"):
"""Constructor.
"""
self.dsname = dsname
self.color = color
self.legend = legend.encode("utf-8")
self.cfunc = cfunc
def to_rrd_command_args(self, rrdfile):
"""Convert this curve to the approriate RRDtool command.
:param str rrdfile: RRD file name
:return: a list
"""
rrdfile = os.path.join(
parameters.get_admin("RRD_ROOTDIR"), "%s.rrd" % rrdfile
)
return [
'DEF:%s=%s:%s:%s' %
(self.dsname, rrdfile, self.dsname, self.cfunc),
'CDEF:%(ds)spm=%(ds)s,60,*' % {"ds": self.dsname},
'XPORT:%spm:"%s"' % (self.dsname, self.legend)
]
class Graphic(object):
"""Graphic.
"""
def __init__(self):
"""Constructor.
"""
self._curves = []
try:
order = getattr(self, "order")
except AttributeError:
for member in inspect.getmembers(self):
if isinstance(member[1], Curve):
self._curves += [member[1]]
else:
for name in order:
try:
curve = getattr(self, name)
except AttributeError:
continue
if not isinstance(curve, Curve):
continue
self._curves += [curve]
@property
def display_name(self):
return self.__class__.__name__.lower()
def export(self, rrdfile, start, end):
"""
"""
result = []
cmdargs = []
for curve in self._curves:
result += [{
"name": curve.legend, "color": curve.color, "data": []
}]
cmdargs += curve.to_rrd_command_args(rrdfile)
cmd = "rrdtool xport --start %s --end %s " % (str(start), str(end))
cmd += " ".join(cmdargs)
code, output = exec_cmd(cmd)
if code:
return []
tree = etree.fromstring(output)
for row in tree.xpath('/xport/data/row'):
timestamp = int(row.find('t').text)
for vindex, value in enumerate(row.findall('v')):
if value.text == 'NaN':
result[vindex]['data'].append({'x': timestamp, 'y': 0})
else:
result[vindex]['data'].append(
{'x': timestamp, 'y': float(value.text)}
)
return result
class AverageTraffic(Graphic):
"""Average traffic.
"""
title = ugettext_lazy('Average traffic')
vertlabel = ugettext_lazy('msgs/min')
# Curve definitions
sent = Curve("sent", "lawngreen", ugettext_lazy("sent messages"))
recv = Curve("recv", "steelblue", ugettext_lazy("received messages"))
bounced = Curve("bounced", "yellow", ugettext_lazy("bounced messages"))
reject = Curve("reject", "tomato", ugettext_lazy("rejected messages"))
order = ['reject', 'bounced', 'recv', 'sent']
class AverageTrafficSize(Graphic):
"""Average traffic size.
"""
title = ugettext_lazy('Average normal traffic size')
vertlabel = ugettext_lazy('bytes/min')
# Curve definitions
size_recv = Curve("size_recv", "orange", ugettext_lazy("received size"))
size_sent = Curve(
"size_sent", "mediumturquoise", ugettext_lazy("sent size")
)
class GraphicSet(object):
title = None
_graphics = []
def __init__(self):
self.__ginstances = []
@property
def html_id(self):
return self.__class__.__name__.lower()
@property
def graphics(self):
if not self.__ginstances:
self.__ginstances = [graphic() for graphic in self._graphics]
return self.__ginstances
def get_graphic_names(self):
return [graphic.display_name for graphic in self._graphics]
def export(self, rrdfile, start, end):
result = {}
for graph in self.graphics:
result[graph.display_name] = {
"title": graph.title.encode("utf-8"),
"curves": graph.export(rrdfile, start, end)
}
return result
class MailTraffic(GraphicSet):
title = ugettext_lazy('Mail traffic')
_graphics = [AverageTraffic, AverageTrafficSize]
+24
View File
@@ -0,0 +1,24 @@
# coding: utf-8
import sys
import time
def date_to_timestamp(timetuple):
"""Date conversion.
Returns a date and a time in seconds from the epoch.
:param list timetuple: list containing date
:return: an integer
"""
date = " ".join(
[("%d" % elem) if type(elem) is int else elem for elem in timetuple]
)
fmt = "%Y %m %d %H %M %S" \
if timetuple[1].isdigit() else "%Y %b %d %H %M %S"
try:
local = time.strptime(date, fmt)
except ValueError:
print >> sys.stderr, "Error: failed to convert date and time"
return 0
return int(time.mktime(local))
@@ -28,8 +28,8 @@ from optparse import make_option
from modoboa.lib import parameters
from modoboa.extensions.admin.models import Domain
from modoboa.extensions.stats import Stats
from modoboa.extensions.stats.grapher import str2Time, Grapher
from modoboa.extensions.stats.graph_templates import MailTraffic
from modoboa.extensions.stats.lib import date_to_timestamp
rrdstep = 60
xpoints = 540
@@ -123,7 +123,7 @@ class LogParser(object):
ye = match.group("year")
except IndexError:
ye = self.year(mo)
self.cur_t = str2Time(ye, mo, da, ho, mi, se)
self.cur_t = date_to_timestamp([ye, mo, da, ho, mi, se])
self.cur_t = self.cur_t - self.cur_t % rrdstep
self._prev_mi = mi
self._prev_ho = ho
@@ -329,15 +329,11 @@ class LogParser(object):
for line in self.f.readlines():
self._parse_line(line)
G = Grapher()
for dom, data in self.data.iteritems():
self._dprint("[rrd] dealing with domain %s" % dom)
for t in sorted(data.keys()):
self.update_rrd(dom, t)
for graph_tpl in MailTraffic().get_graphs():
G.make_defaults(dom, graph_tpl)
class Command(BaseCommand):
help = 'Log file parser'
@@ -0,0 +1,67 @@
.chart {
position: relative;
margin-bottom: 20px;
}
.grid .tick {
stroke: darkgrey;
stroke-dasharray: 1, 2;
opacity: 0.9;
}
.grid path {
stroke-width: 0;
}
.line_over {
stroke: darkgrey;
stroke-width: 1.5px;
stroke-dasharray: 1, 2;
display: none;
}
.axis {
font-size: 0.7em;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.area, .area2 {
fill-opacity: .6;
}
.brush .extent {
stroke: #fff;
fill-opacity: .125;
shape-rendering: crispEdges;
}
.legend rect {
fill: none;
stroke: black;
opacity: 0;
}
.tip {
position: absolute;
z-index: 20;
top: 60px;
display: none;
font-size: 0.9em;
padding: 5px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
border-radius: 5px;
}
.tip p {
margin: 0;
}
.tracker {
display: none;
}
@@ -0,0 +1,70 @@
// d3.legend.js
// (C) 2012 ziggy.jonsson.nyc@gmail.com
// MIT licence
(function() {
d3.legend = function(g, clicked) {
g.each(function() {
var g = d3.select(this),
items = {},
svg = d3.select(g.property("nearestViewportElement")),
legendPadding = g.attr("data-style-padding") || 5,
lb = g.selectAll(".legend-box").data([true]),
li = g.selectAll(".legend-items").data([true]);
lb.enter().append("rect").classed("legend-box", true);
li.enter().append("g").classed("legend-items", true);
svg.selectAll("[data-legend]").each(function() {
var self = d3.select(this);
items[self.attr("data-legend")] = {
pos: self.attr("data-legend-pos") || this.getBBox().y,
color: self.attr("data-legend-color") != undefined ? self.attr("data-legend-color") : self.style("fill") != 'none' ? self.style("fill") : self.style("stroke")
};
});
items = d3.entries(items).sort(function(a, b) {
return a.value.pos - b.value.pos;
});
li.selectAll("text")
.data(items, function(d) { return d.key; })
.call(function(d) { d.enter().append("text"); })
.call(function(d) { d.exit().remove(); })
.attr("y", function(d, i) { return i + "em"; })
.attr("x", "1em")
.text(function(d) { return d.key; });
li.selectAll("circle")
.data(items, function(d) { return d.key; })
.call(function(d) { d.enter().append("circle"); })
.call(function(d) { d.exit().remove(); })
.attr("cy", function(d, i) { return i - 0.25 + "em"; })
.attr("cx", 0)
.attr("r", "4px")
.style("stroke", function(d) { return d.value.color; })
.style("stroke-width", "1px")
.style("stroke-opacity", 0.8)
.style("fill", function(d) { return d.value.color; })
.on("click", function(d) {
var curve = d3.selectAll("[data-legend='" + d.key + "']");
var state = (curve.style("display") == "none")
? "block" : "none";
curve.style("display", state);
d3.select(this).style(
"fill", (state == "none") ? "none" : d.value.color
);
});
// Reposition and resize the box
var lbbox = li[0][0].getBBox();
lb.attr("x", (lbbox.x - legendPadding))
.attr("y", (lbbox.y - legendPadding))
.attr("height", (lbbox.height + 2 * legendPadding))
.attr("width", (lbbox.width + 2 * legendPadding));
});
return g;
};
})();
@@ -0,0 +1,344 @@
/**
* Creates an instance of ModoChart.
*
* @constructor
* @this {ModoChart}
* @param {string} selection add chart to this DOM element.
*
*/
function ModoChart(selection) {
var width = 0,
height = 300,
preview_height = 80,
brush_height = 7,
margin = {
left: 50,
top: 10,
bottom: 20
};
var container;
/**
* Create and render the chart.
*
* @this {ModoChart}
* @param {Object} data chart's data.
*/
function my(data) {
container = d3.select(selection)
.append("div")
.attr("class", "chart");
container.append("h6").html(data.title);
width = $(selection).width();
var svg = container
.append("svg")
.attr("width", width)
.attr("height", my.totalHeight());
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width - 2 * margin.left)
.attr("height", height + margin.top + margin.bottom);
var x = d3.time.scale().range([0, width - 2 * margin.left]),
x2 = d3.time.scale().range([0, width - 2 * margin.left]);
var xAxis = d3.svg.axis().scale(x).orient("bottom"),
xAxis2 = d3.svg.axis().scale(x2).orient("bottom");
var y = d3.scale.linear().range([height, 0]),
y2 = d3.scale.linear().range([preview_height, 0]);
var yAxis = d3.svg.axis().scale(y).orient("left");
var bisectDate = d3.bisector(function(d) { return d.x * 1000; }).left;
var brush = d3.svg.brush()
.x(x2)
.on("brush", brushed);
x.domain(
d3.extent(data.curves[0].data.map(function(d) { return d.x * 1000; }))
);
y.domain(
[d3.min(data.curves, function(d) { return d3.min(d.data, function(v) { return v.y; }); }),
d3.max(data.curves, function(d) { return d3.max(d.data, function(v) { return v.y; }); })
]
);
x2.domain(x.domain());
y2.domain(y.domain());
var area = d3.svg.area()
.interpolate("monotone")
.x(function(d) { return x(d.x * 1000); })
.y0(height)
.y1(function(d) { return y(d.y); });
var area2 = d3.svg.area()
.interpolate("monotone")
.x(function(d) { return x2(d.x * 1000); })
.y0(preview_height)
.y1(function(d) { return y2(d.y); });
var focus = svg.append("g")
.attr("class", "focus")
.attr("width", width - 2 * margin.left)
.attr("height", height)
.attr("pointer-events", "all")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
var context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + margin.left + "," + (height + margin.top + margin.bottom + brush_height) + ")");
var curvegroup = focus.append("g")
.attr('clip-path', 'url(#clip)');
var curve = curvegroup.selectAll(".curve")
.data(data.curves)
.enter().append("g")
.attr("class", "curve");
var curve2 = context.selectAll(".curve")
.data(data.curves)
.enter().append("g")
.attr("class", "curve");
/* Vertical line */
var line = focus.append("line")
.attr("class", "line_over")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", height);
/* Custom rectangle to catch mouse events */
var rect = focus.append("rect")
.attr("fill", "none")
.attr("width", width - 2 * margin.left)
.attr("height", height);
var tooltip = container
.append("div")
.attr("class", "remove")
.attr("class", "tip");
var points = [];
var path = curve.append("path")
.attr("class", "area")
.attr("d", function(d, i) {
var path = this;
points[path] = {};
d.data.forEach(function(d) {
points[path][d.x] = d.y;
});
focus.append("circle")
.attr("class", "tracker tracker-" + i)
.attr("r", 4);
return area(d.data);
})
.attr("data-legend", function(d) { return d.name; })
.attr("stroke", function(d) { return d.color; })
.attr("stroke-width", "2px")
.attr("stroke-opacity", 0.8)
.style("fill", function(d) { return d.color; });
var xrules = make_x_axis();
focus.append("g")
.attr("class", "grid")
.attr("transform", "translate(0," + height + ")")
.call(xrules);
focus.append("g")
.attr("class", "grid")
.call(make_y_axis);
focus.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
focus.append("g")
.attr("class", "y axis")
.call(yAxis);
rect.on("mouseover", function(d) {
line.style("display", "block");
tooltip.style("display", "block");
data.curves.forEach(function(d, cid) {
var display = d3.selectAll("[data-legend='" + d.name + "']")
.style("display");
if (display == "none") {
return;
}
focus.select(".tracker-" + cid).style("display", "block");
});
}).on("mousemove", function(d) {
var mousex = d3.mouse(this)[0],
mousey = d3.mouse(this)[1];
var date = x.invert(mousex);
var text = "";
line.attr("x1", mousex).attr("x2", mousex);
data.curves.forEach(function(d, cid) {
var display = d3.selectAll("[data-legend='" + d.name + "']")
.style("display");
if (display == "none") {
return;
}
var i = bisectDate(d.data, date.getTime(), 1),
d0 = d.data[i - 1],
d1 = d.data[i],
point = date.getTime() - (d0.x * 1000) > (d1.x * 1000) - date.getTime() ? d1 : d0;
text += "<p>" + d.name + ": " + point.y.toFixed(3) + "</p>";
var tracker_x = x(point.x * 1000),
tracker_y = y(point.y);
focus.selectAll(".tracker-" + cid)
.style("fill", d.color)
.attr("cx", tracker_x)
.attr("cy", tracker_y);
});
if (mousex + tooltip[0][0].clientWidth >= width - 2 * margin.left) {
var left = mousex - (tooltip[0][0].clientWidth - (margin.left - 20));
tooltip.style("left", left + "px");
} else {
tooltip.style("left", (mousex + margin.left + 20) + "px");
}
tooltip.html("<p>" + date + "</p>" + text);
}).on("mouseout", function(d) {
tooltip.style("display", "none");
line.style("display", "none");
focus.selectAll(".tracker").style("display", "none");
});
var legend = focus.append("g")
.attr("class", "legend")
.attr("transform", "translate(50,10)")
.style("font-size", "0.8em")
.call(d3.legend);
curve2.append("path")
.attr("class", "area2")
.attr("d", function(d) { return area2(d.data); })
.attr("stroke", function(d) { return d.color; })
.attr("stroke-width", "2px")
.attr("stroke-opacity", 0.8)
.attr("data-legend", function(d) { return d.name; })
.style("fill", function(d) { return d.color; });
context.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + preview_height + ")")
.call(xAxis2);
context.append("g")
.attr("class", "grid")
.attr("transform", "translate(0," + preview_height + ")")
.call(make_preview_x_axis);
context.append("g")
.attr("class", "x brush")
.call(brush)
.selectAll("rect")
.attr("y", -6)
.attr("height", preview_height + brush_height);
function make_x_axis() {
return d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(6)
.tickSize(-height, 0, 0)
.tickFormat("");
}
function make_y_axis() {
return d3.svg.axis()
.scale(y)
.orient("left")
.ticks(6)
.tickSize(-(width - 2 * margin.left), 0, 0)
.tickFormat("");
}
function make_preview_x_axis() {
return d3.svg.axis()
.scale(x2)
.orient("bottom")
.ticks(6)
.tickSize(-preview_height, 0, 0)
.tickFormat("");
}
function brushed() {
x.domain(brush.empty() ? x2.domain() : brush.extent());
focus.selectAll(".area")
.attr("d", function(d) { return area(d.data); });
focus.select(".x.axis").call(xAxis);
focus.select(".grid").call(xrules);
}
}
/**
* Set or get chart's width.
*
* @this {ModoChart}
* @param {integer} value new width.
* @return {number} current width.
*/
my.width = function(value) {
if (!arguments.length) return width;
width = value;
return my;
};
/**
* Set or get chart's height.
*
* @this {ModoChart}
* @param {integer} value new height.
* @return {number} current height.
*/
my.height = function(value) {
if (!arguments.length) return height;
height = value;
return my;
};
/**
* Set or get chart's margin.
*
* @this {ModoChart}
* @param {Object} value new margin.
* @return {Object} current margin.
*/
my.margin = function(value) {
if (!arguments.length) return margin;
margin = value;
return my;
};
/**
* Compute and return chart's total height.
*
* @this {ModoChart}
* @return {integer} total height.
*/
my.totalHeight = function() {
return height + 2 * margin.top
+ 2 * margin.bottom + preview_height
+ brush_height;
};
/**
* Update the chart with new data.
*
* @this {ModoChart}
* @param {Object} data new data
*/
my.update = function(data) {
container.select("svg").remove();
$(container[0][0]).empty();
my(data);
};
return my;
}
@@ -12,7 +12,7 @@ Stats.prototype = {
initialize: function(options) {
this.options = $.extend({}, this.defaults, options);
this.options.defcallback = $.proxy(this.graphs_cb, this);
this.options.defcallback = $.proxy(this.charts_cb, this);
var navobj = new History(this.options);
this.navobj = navobj;
@@ -63,12 +63,13 @@ Stats.prototype = {
listen: function() {
$(".period_selector").click($.proxy(this.change_period, this));
$("#customsend").on("click", $.proxy(this.customgraphs, this));
$("#customsend").on("click", $.proxy(this.custom_period, this));
$(window).resize($.proxy(this.resize_charts, this));
},
register_nav_callbacks: function() {
this.navobj.register_callback("graphs",
$.proxy(this.graphs_cb, this));
$.proxy(this.charts_cb, this));
},
change_period: function(e) {
@@ -79,9 +80,37 @@ Stats.prototype = {
this.navobj.setparam("period", $link.attr("data-period")).update();
},
graphs_cb: function(data) {
if (data.content) {
$(".tab-pane.active").html(data.content);
/**
* Update all charts on resize event.
*
* @this {Stats}
*/
resize_charts: function() {
var data = this.data;
$.each(this.charts, function(id, mychart) {
mychart.update(data.graphs[id]);
});
},
/**
* Create or update charts.
*
* @this {Stats}
* @param {Object} data
*/
charts_cb: function(data) {
this.data = data;
if (this.charts !== undefined) {
$.each(this.charts, function(id, mychart) {
mychart.update(data.graphs[id]);
});
} else {
this.charts = {};
$.each(data.graphs, $.proxy(function(id, graphdef) {
var mychart = ModoChart("#gset");
this.charts[id] = mychart;
mychart(graphdef);
}, this));
}
},
@@ -94,7 +123,7 @@ Stats.prototype = {
this.navobj.delparam("searchquery").update();
},
customgraphs: function(e) {
custom_period: function(e) {
e.preventDefault();
var $fromdate = $("#id_from");
var $todate = $("#id_to");
@@ -1,5 +0,0 @@
{% load i18n %}
{% for name in graphs %}
<p><img src="{{ MEDIA_URL }}stats/{{ name }}_{{ domain }}_AVERAGE_{% ifequal period 'custom' %}{{ period_name }}{% else %}{{ period }}{% endifequal %}.png"
alt="{% trans 'No statistics available' %}" /></p>
{% endfor %}
@@ -4,6 +4,9 @@
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}stats/css/stats.css" />
<script type="text/javascript" src="{{ STATIC_URL }}js/history.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/autocompleter.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/d3.v3.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}stats/js/d3.legend.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}stats/js/graphic.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}stats/js/stats.js"></script>
<script type="text/javascript">
function get_domains_list(container) {
@@ -69,7 +72,9 @@ $(document).ready(function() {
</li>
</ul>{% endfor %}
<div class="tab-content">{% for id, gset in graph_sets.items %}
<div class="tab-pane {% if forloop.first %}active{% endif %}" id="graphs_{{ gset.html_id }}"></div>
<div class="tab-pane {% if forloop.first %}active{% endif %}" id="graphs_{{ gset.html_id }}">
<div id="gset"></div>
</div>
</div>{% endfor %}
</div>
{% endblock %}
+19 -20
View File
@@ -1,5 +1,6 @@
# coding: utf-8
import re
import time
from django.shortcuts import render
from django.utils.translation import ugettext as _
from django.contrib.auth.decorators import (
@@ -8,12 +9,12 @@ from django.contrib.auth.decorators import (
from modoboa.lib import events
from modoboa.lib.exceptions import BadRequest, PermDeniedException, NotFound
from modoboa.lib.webutils import (
_render_to_string, render_to_json_response
render_to_json_response
)
from modoboa.extensions.admin.models import (
Domain
)
from modoboa.extensions.stats.grapher import periods, str2Time, Grapher
from modoboa.extensions.stats.lib import date_to_timestamp
@login_required
@@ -27,11 +28,13 @@ def index(request):
if not Domain.objects.get_for_admin(request.user).count():
raise NotFound(_("No statistics available"))
period = request.GET.get("period", "day")
graph_sets = events.raiseDictEvent('GetGraphSets')
periods = [{"name": "day", "label": _("Day")},
{"name": "week", "label": _("Week")},
{"name": "month", "label": _("Month")},
{"name": "year", "label": _("Year")}]
return render(request, 'stats/index.html', {
"periods": periods,
"period": period,
"selection": "stats",
"deflocation": deflocation,
"graph_sets": graph_sets
@@ -47,7 +50,7 @@ def graphs(request):
raise NotFound(_("Unknown graphic set"))
searchq = request.GET.get("searchquery", None)
period = request.GET.get("period", "day")
tplvars = dict(graphs=[], period=period)
tplvars = dict(graphs={}, period=period)
if searchq in [None, "global"]:
if not request.user.is_superuser:
if not Domain.objects.get_for_admin(request.user).count():
@@ -64,27 +67,23 @@ def graphs(request):
if not request.user.can_access(domain[0]):
raise PermDeniedException
tplvars.update(domain=domain[0].name)
if period == "custom":
if not "start" in request.GET or not "end" in request.GET:
raise BadRequest(_("Bad custom period"))
start = request.GET["start"]
end = request.GET["end"]
G = Grapher()
expr = re.compile(r'[:\- ]')
period_name = "%s_%s" % (expr.sub('', start), expr.sub('', end))
for tpl in gsets[gset].get_graphs():
tplvars['graphs'].append(tpl.display_name)
G.process(
tplvars["domain"], period_name, str2Time(*expr.split(start)),
str2Time(*expr.split(end)), tpl
)
tplvars["period_name"] = period_name
tplvars["start"] = start
tplvars["end"] = end
start = date_to_timestamp(expr.split(start))
end = date_to_timestamp(expr.split(end))
else:
tplvars['graphs'] = gsets[gset].get_graph_names()
end = int(time.mktime(time.localtime()))
start = "-1%s" % period
period_name = period
return render_to_json_response({
'content': _render_to_string(request, "stats/graphs.html", tplvars)
})
tplvars['graphs'] = gsets[gset].export(tplvars["domain"], start, end)
tplvars["period_name"] = period_name
tplvars["start"] = start
tplvars["end"] = end
return render_to_json_response(tplvars)
+5
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -54,7 +54,7 @@ def read(fname):
setup(
name="modoboa",
version='1.1.1',
version='1.1.2',
url='http://modoboa.org/',
license='MIT',
description="Mail hosting made simple",