Remove limits extension.

This commit is contained in:
Antoine Nguyen
2015-02-11 13:39:40 +01:00
parent ec441a5c07
commit 704a046b5b
31 changed files with 0 additions and 2733 deletions
-55
View File
@@ -1,55 +0,0 @@
"""Custom settings."""
from django import forms
from django.utils.translation import ugettext_lazy as _
from modoboa.lib.form_utils import SeparatorField
from modoboa.lib.parameters import AdminParametersForm
class ParametersForm(AdminParametersForm):
app = "limits"
defv_sep = SeparatorField(label=_("Default limits"))
deflt_domain_admins_limit = forms.IntegerField(
label=_("Domain admins"),
initial=0,
help_text=_(
"Maximum number of allowed domain administrators for a new "
"administrator"
),
widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"})
)
deflt_domains_limit = forms.IntegerField(
label=_("Domains"),
initial=0,
help_text=_(
"Maximum number of allowed domains for a new administrator"),
widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"})
)
deflt_domain_aliases_limit = forms.IntegerField(
label=_("Domain aliases"),
initial=0,
help_text=_(
"Maximum number of allowed domain aliases for a new administrator"),
widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"})
)
deflt_mailboxes_limit = forms.IntegerField(
label=_("Mailboxes"),
initial=0,
help_text=_(
"Maximum number of allowed mailboxes for a new administrator"),
widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"})
)
deflt_mailbox_aliases_limit = forms.IntegerField(
label=_("Mailbox aliases"),
initial=0,
help_text=_(
"Maximum number of allowed aliases for a new administrator"),
widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"})
)
def __init__(self, *args, **kwargs):
super(AdminParametersForm, self).__init__(*args, **kwargs)
self._load_extra_parameters('A')
-219
View File
@@ -1,219 +0,0 @@
# coding: utf-8
"""
:mod:`controls` --- provides event handlers that check if limits are reached
----------------------------------------------------------------------------
"""
from .lib import LimitReached, inc_limit_usage, dec_limit_usage
from .models import LimitTemplates, LimitsPool
from modoboa.lib import events
from modoboa.lib.permissions import get_object_owner
def check_limit(user, lname, count=1):
"""Check if a user has reached a defined limit
We check if the user will not reach the given limit after this
creation.
:param user: a ``User`` object
:param int count: the number of objects the user tries to create
:raises: ``LimitReached``
"""
try:
if user.limitspool.will_be_reached(lname, count):
raise LimitReached(user.limitspool.get_limit(lname))
except LimitsPool.DoesNotExist:
pass
def move_pool_resource(owner, user):
"""Move resource from one pool to another
When an account doesn't need a pool anymore, we move the
associated resource to the pool of its owner.
"""
try:
pool = user.limitspool
except LimitsPool.DoesNotExist:
return
if not owner.is_superuser:
for ltpl in LimitTemplates().templates:
l = user.limitspool.get_limit(ltpl[0])
if l.maxvalue < 0:
continue
ol = owner.limitspool.get_limit(ltpl[0])
ol.curvalue += l.curvalue
ol.maxvalue += l.maxvalue
ol.save()
user.limitspool.delete()
@events.observe('DomainCreated')
def inc_nb_domains(user, domain):
inc_limit_usage(user, 'domains_limit')
@events.observe('DomainDeleted')
def dec_nb_domains(domain, owner=None):
if owner is None:
owner = get_object_owner(domain)
dec_limit_usage(owner, 'domains_limit')
for domalias in domain.domainalias_set.all():
dec_nb_domaliases(domalias)
for mailbox in domain.mailbox_set.all():
dec_nb_mailboxes(mailbox)
for mbalias in domain.alias_set.all():
dec_nb_mbaliases(mbalias)
@events.observe('DomainAliasCreated')
def inc_nb_domaliases(user, domalias):
inc_limit_usage(user, 'domain_aliases_limit')
@events.observe('DomainAliasDeleted')
def dec_nb_domaliases(domainaliases):
from modoboa.extensions.admin.models import DomainAlias
if isinstance(domainaliases, DomainAlias):
domainaliases = [domainaliases]
for domainalias in domainaliases:
owner = get_object_owner(domainalias)
dec_limit_usage(owner, 'domain_aliases_limit')
@events.observe('MailboxCreated')
def inc_nb_mailboxes(user, mailbox):
inc_limit_usage(user, 'mailboxes_limit')
@events.observe('MailboxDeleted')
def dec_nb_mailboxes(mailboxes):
from modoboa.extensions.admin.models import Mailbox
if isinstance(mailboxes, Mailbox):
mailboxes = [mailboxes]
for mailbox in mailboxes:
owner = get_object_owner(mailbox)
dec_limit_usage(owner, 'mailboxes_limit')
@events.observe('MailboxAliasCreated')
def inc_nb_mbaliases(user, mailboxalias):
inc_limit_usage(user, 'mailbox_aliases_limit')
@events.observe('MailboxAliasDeleted')
def dec_nb_mbaliases(mailboxaliases):
from modoboa.extensions.admin.models import Alias
if isinstance(mailboxaliases, Alias):
mailboxaliases = [mailboxaliases]
for alias in mailboxaliases:
owner = get_object_owner(alias)
dec_limit_usage(owner, 'mailbox_aliases_limit')
@events.observe('CanCreate')
def can_create_new_object(user, objtype, count=1):
check_limit(user, '%s_limit' % objtype, count)
@events.observe("AccountCreated")
def create_pool(user):
owner = get_object_owner(user)
if owner.group not in ['SuperAdmins', 'Resellers']:
return
if user.group == 'DomainAdmins':
check_limit(owner, 'domain_admins_limit')
inc_limit_usage(owner, 'domain_admins_limit')
if user.group in ['DomainAdmins', 'Resellers']:
p, created = LimitsPool.objects.get_or_create(user=user)
p.create_limits(owner)
@events.observe("UserCanSetRole")
def user_can_set_role(user, role, account=None):
"""Check if the user can still set this role.
The only interesting case concerns resellers defining new domain
administrators. We want to check if they are allowed to do this
operation before any modification is made to :keyword:`account`.
:param ``User`` user: connected user
:param ``User`` account: account modified (None on creation)
:param str newrole: role to check
"""
if role == 'DomainAdmins':
lname = 'domain_admins_limit'
else:
return [True]
if user.is_superuser or not user.limitspool.will_be_reached(lname):
return [True]
if account is not None and account.group == role:
return [True]
return [False]
@events.observe("AccountModified")
def on_account_modified(old, new):
"""Update limits when roles are updated"""
owner = get_object_owner(old)
if owner.group not in ["SuperAdmins", "Resellers"]:
# Domain admins can't change the role so nothing to check.
return
if new.group != "SuperAdmins":
# Check if account needs a pool (case: a superadmin is
# downgraded)
try:
pool = new.limitspool
except LimitsPool.DoesNotExist:
p = LimitsPool(user=new)
p.save()
p.create_limits(owner)
if new.group not in ["DomainAdmins", "Resellers"]:
move_pool_resource(owner, new)
if old.oldgroup == "DomainAdmins":
if new.group != "DomainAdmins":
dec_limit_usage(owner, 'domain_admins_limit')
return
if new.group == "DomainAdmins":
check_limit(owner, 'domain_admins_limit')
inc_limit_usage(owner, 'domain_admins_limit')
@events.observe("AccountDeleted")
def on_account_deleted(account, byuser, **kwargs):
owner = get_object_owner(account)
if owner.group not in ["SuperAdmins", "Resellers"]:
return
move_pool_resource(owner, account)
if account.group == "DomainAdmins":
dec_limit_usage(owner, 'domain_admins_limit')
@events.observe('DomainOwnershipRemoved')
def domain_ownership_removed(reseller, domain):
"""DomainOwnershipRemoved listener.
The access :keyword:`reseller` had to :keyword:`domain` has been
removed by a super adminstrator. We must decrement all limit
usages.
:param ``User`` reseller: reseller that created :keyword:`domain`
:param ``Domain`` domain: domain
"""
dec_nb_domains(domain, reseller)
for dadmin in domain.admins:
if reseller.is_owner(dadmin):
dec_limit_usage(reseller, 'domain_admins_limit')
@@ -1 +0,0 @@
[{"pk": 1, "model": "limits.limitspool", "fields": {"user": 2}}, {"pk": 1, "model": "limits.limit", "fields": {"curvalue": 0, "name": "domain_admins_limit", "pool": 1, "maxvalue": -2}}, {"pk": 2, "model": "limits.limit", "fields": {"curvalue": 0, "name": "domains_limit", "pool": 1, "maxvalue": -2}}, {"pk": 3, "model": "limits.limit", "fields": {"curvalue": 0, "name": "domain_aliases_limit", "pool": 1, "maxvalue": -2}}, {"pk": 4, "model": "limits.limit", "fields": {"curvalue": 0, "name": "mailboxes_limit", "pool": 1, "maxvalue": 5}}, {"pk": 5, "model": "limits.limit", "fields": {"curvalue": 0, "name": "mailbox_aliases_limit", "pool": 1, "maxvalue": 5}},{"pk": 3, "model": "auth.group", "fields": {"name": "Resellers", "permissions": [["add_user", "core", "user"], ["change_user", "core", "user"], ["delete_user", "core", "user"], ["view_domains", "admin", "domain"], ["add_mailbox", "admin", "mailbox"], ["change_mailbox", "admin", "mailbox"], ["delete_mailbox", "admin", "mailbox"], ["add_alias", "admin", "alias"], ["change_alias", "admin", "alias"], ["delete_alias", "admin", "alias"], ["view_mailboxes", "admin", "mailbox"], ["view_aliases", "admin", "alias"], ["view_domaliases", "admin", "domainalias"], ["add_domainalias", "admin", "domainalias"], ["change_domainalias", "admin", "domainalias"], ["delete_domainalias", "admin", "domainalias"], ["view_domains", "admin", "domain"], ["add_domain", "admin", "domain"], ["change_domain", "admin", "domain"], ["delete_domain", "admin", "domain"]]}}]
-91
View File
@@ -1,91 +0,0 @@
# coding: utf-8
"""Custom forms."""
from django import forms
from django.utils.translation import ugettext as _
from .lib import BadLimitValue, UnsufficientResource
from .models import LimitTemplates
from modoboa.lib import parameters
class ResourcePoolForm(forms.Form):
def __init__(self, *args, **kwargs):
self.account = None
if "instance" in kwargs:
self.account = kwargs["instance"]
del kwargs["instance"]
super(ResourcePoolForm, self).__init__(*args, **kwargs)
for tpl in LimitTemplates().templates:
if len(tpl) > 3:
if self.account is not None and self.account.group != tpl[3]:
continue
self.fields[tpl[0]] = forms.IntegerField(
label=tpl[1], help_text=tpl[2]
)
if hasattr(self, "account"):
self.load_from_user(self.account)
def check_limit_value(self, lname):
if self.cleaned_data[lname] < -1:
raise forms.ValidationError(_("Invalid limit"))
return self.cleaned_data[lname]
def clean(self):
cleaned_data = super(ResourcePoolForm, self).clean()
for lname in self.fields.keys():
if lname not in self._errors and cleaned_data[lname] < -1:
self.add_error(lname, _("Invalid limit"))
return cleaned_data
def load_from_user(self, user):
for lname in self.fields.keys():
self.fields[lname].initial = user.limitspool.getmaxvalue(lname)
# The following lines will become useless in a near
# future.
if self.fields[lname].initial == -2:
self.fields[lname].initial = parameters.get_admin(
"DEFLT_%s" %
lname.upper())
def allocate_from_pool(self, limit, pool):
"""Allocate resource using an existing pool.
When a reseller creates a domain administrator, he generally
assigns him resource to create new objetcs. As a reseller may
also be limited, the resource he gives is taken from its own
pool.
"""
ol = pool.get_limit(limit.name)
if ol.maxvalue == -2:
raise BadLimitValue(_("Your resources are not initialized yet"))
newvalue = self.cleaned_data[limit.name]
if newvalue == -1 and ol.maxvalue != -1:
raise BadLimitValue(
_("You're not allowed to define unlimited values")
)
if limit.maxvalue > -1:
newvalue -= limit.maxvalue
if newvalue == 0:
return
remain = ol.maxvalue - ol.curvalue
if newvalue > remain:
raise UnsufficientResource(ol)
ol.maxvalue -= newvalue
ol.save()
def save(self):
from modoboa.lib.permissions import get_object_owner
owner = get_object_owner(self.account)
for ltpl in LimitTemplates().templates:
if ltpl[0] not in self.cleaned_data:
continue
l = self.account.limitspool.limit_set.get(name=ltpl[0])
if not owner.is_superuser:
self.allocate_from_pool(l, owner.limitspool)
l.maxvalue = self.cleaned_data[ltpl[0]]
l.save()
@@ -1,123 +0,0 @@
"""General event callbacks."""
from django.utils.translation import ugettext as _
from modoboa.core import PERMISSIONS as CORE_PERMS
from modoboa.extensions.admin.modo_extension import PERMISSIONS as ADMIN_PERMS
from modoboa.extensions.limits.forms import ResourcePoolForm
from modoboa.extensions.limits.models import LimitTemplates
from modoboa.lib import events
PERMISSIONS = {
"Resellers": (
CORE_PERMS.get("DomainAdmins") +
ADMIN_PERMS.get("DomainAdmins") +
[["admin", "domain", "view_domains"],
["admin", "domain", "add_domain"],
["admin", "domain", "change_domain"],
["admin", "domain", "delete_domain"]]
)
}
@events.observe("GetExtraRoles")
def get_extra_roles(user):
if user.is_superuser:
return [("Resellers", _("Reseller")), ]
return []
@events.observe("GetExtraRolePermissions")
def extra_permissions(rolename):
"""Return extra permissions for Resellers."""
return PERMISSIONS.get(rolename, [])
@events.observe("ExtraAdminContent")
def display_pool_usage(user, target, currentpage):
from django.template.loader import render_to_string
if target != "leftcol" or user.is_superuser:
return []
if currentpage == "identities":
names = ["mailboxes_limit", "mailbox_aliases_limit"]
if user.has_perm("admin.add_domain"):
names += ["domain_admins_limit"]
else:
names = [
tpl[0] for tpl in LimitTemplates().templates
if tpl[0] not in ["domain_admins_limit", "mailboxes_limit",
"mailbox_aliases_limit"]
and (len(tpl) == 3 or tpl[3] == user.group)
]
limits = user.limitspool.limit_set.filter(name__in=names, maxvalue__gt=0)
if len(limits) == 0:
return []
return [render_to_string("limits/poolusage.html", dict(limits=limits))]
@events.observe("ExtraAccountForm")
def extra_account_form(user, account=None):
if user.group not in ["SuperAdmins", "Resellers"]:
return []
if account is not None and \
account.group not in ["Resellers", "DomainAdmins"]:
return []
return [
dict(
id="resources", title=_("Resources"), cls=ResourcePoolForm
)
]
@events.observe("CheckExtraAccountForm")
def check_form_access(account, form):
if form["id"] != "resources":
return [True]
if not account.belongs_to_group("Resellers") and \
not account.belongs_to_group("DomainAdmins"):
return [False]
return [True]
@events.observe("FillAccountInstances")
def fill_account_instances(user, account, instances):
if not user.is_superuser and not user.belongs_to_group("Resellers"):
return
if not account.belongs_to_group("Resellers") and \
not account.belongs_to_group("DomainAdmins"):
return
instances["resources"] = account
@events.observe("GetStaticContent")
def get_static_content(caller, st_type, user):
if caller not in ['domains', 'identities']:
return []
if user.group == "SimpleUsers":
return []
if st_type == "css":
return ["""<style>
.resource {
padding: 10px 15px;
}
.resource .progress {
margin-bottom: 0px;
}
.resource .progress .bar {
color: #000000;
}
</style>
"""]
return ["""
<script type="text/javascript">
$(document).ready(function() {
$(".progress").tooltip();
});
</script>
"""]
-53
View File
@@ -1,53 +0,0 @@
# coding: utf-8
"""
:mod:`lib` --- public functions
-------------------------------
"""
from django.utils.translation import ugettext as _
from modoboa.extensions.limits.models import LimitsPool
from modoboa.lib.exceptions import ModoboaException
class LimitReached(ModoboaException):
http_code = 403
def __init__(self, limit):
self.limit = limit
def __str__(self):
return _("%s: limit reached") % self.limit.label
class UnsufficientResource(ModoboaException):
http_code = 424
def __init__(self, limit):
self.limit = limit
def __str__(self):
return _("Not enough resources")
class BadLimitValue(ModoboaException):
http_code = 400
def inc_limit_usage(user, lname):
"""Increase a given limit usage.
"""
try:
user.limitspool.inc_curvalue(lname)
except LimitsPool.DoesNotExist:
pass
def dec_limit_usage(user, lname):
if user is None:
return
try:
user.limitspool.dec_curvalue(lname)
except LimitsPool.DoesNotExist:
pass
@@ -1,47 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Limit',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=255)),
('curvalue', models.IntegerField(default=0)),
('maxvalue', models.IntegerField(default=-2)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='LimitsPool',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
],
options={
},
bases=(models.Model,),
),
migrations.AddField(
model_name='limit',
name='pool',
field=models.ForeignKey(to='limits.LimitsPool'),
preserve_default=True,
),
migrations.AlterUniqueTogether(
name='limit',
unique_together=set([('name', 'pool')]),
),
]
-148
View File
@@ -1,148 +0,0 @@
# coding: utf-8
"""Models for the limits extensions."""
from django.conf import settings
from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import parameters, events
from modoboa.lib.singleton import Singleton
class LimitTemplates(Singleton):
def __init__(self):
self.__templates = [
("domain_admins_limit", ugettext_lazy("Domain admins"),
ugettext_lazy("Maximum number of domain administrators this user "
"can create"),
'Resellers'),
("domains_limit", ugettext_lazy("Domains"),
ugettext_lazy("Maximum number of domains this user can create"),
'Resellers'),
("domain_aliases_limit", ugettext_lazy("Domain aliases"),
ugettext_lazy(
"Maximum number of domain aliases this user can create"),
'Resellers'),
("mailboxes_limit", ugettext_lazy("Mailboxes"),
ugettext_lazy("Maximum number of mailboxes this user can create")),
("mailbox_aliases_limit", ugettext_lazy("Mailbox aliases"),
ugettext_lazy("Maximum number of mailbox aliases this user "
"can create"))
]
@property
def templates(self):
return self.__templates + \
events.raiseQueryEvent('GetExtraLimitTemplates')
class LimitsPool(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
def create_limits(self, creator):
"""Create limits for this pool.
All limits defined into ``LimitTemplates`` will be created. If
the creator is a super administrator, the default maximum values
will be used.
:param ``User`` creator: user creating this pool
"""
for ltpl in LimitTemplates().templates:
try:
Limit.objects.get(name=ltpl[0], pool=self)
except Limit.DoesNotExist:
maxvalue = (
int(parameters.get_admin("DEFLT_%s" % ltpl[0].upper()))
if creator.is_superuser else 0
)
Limit.objects.create(
name=ltpl[0], pool=self, maxvalue=maxvalue
)
def getcurvalue(self, lname):
l = self.limit_set.get(name=lname)
return l.curvalue
def getmaxvalue(self, lname):
l = self.limit_set.get(name=lname)
return l.maxvalue
def set_maxvalue(self, lname, value):
l = self.limit_set.get(name=lname)
l.maxvalue = value
l.save()
def inc_curvalue(self, lname, nb=1):
l = self.limit_set.get(name=lname)
l.curvalue += nb
l.save()
def dec_curvalue(self, lname, nb=1):
l = self.limit_set.get(name=lname)
if not l.curvalue:
return
l.curvalue -= nb
l.save()
def dec_limit(self, lname, nb=1):
l = self.limit_set.get(name=lname)
l.curvalue -= nb
if l.maxvalue > -1:
l.maxvalue -= nb
l.save()
def inc_limit(self, lname, nb=1):
l = self.limit_set.get(name=lname)
l.curvalue += nb
if l.maxvalue > -1:
l.maxvalue += nb
l.save()
def will_be_reached(self, lname, nb=1):
l = self.limit_set.get(name=lname)
if l.maxvalue <= -1:
return False
if l.curvalue + nb > l.maxvalue:
return True
return False
def get_limit(self, lname):
try:
return self.limit_set.get(name=lname)
except Limit.DoesNotExist:
return None
class Limit(models.Model):
name = models.CharField(max_length=255)
curvalue = models.IntegerField(default=0)
maxvalue = models.IntegerField(default=-2)
pool = models.ForeignKey(LimitsPool)
class Meta:
unique_together = (("name", "pool"),)
@property
def usage(self):
if self.maxvalue < 0:
return -1
if self.maxvalue == 0:
return 0
return int(float(self.curvalue) / self.maxvalue * 100)
@property
def label(self):
for l in LimitTemplates().templates:
if l[0] == self.name:
return l[1]
return ""
def __str__(self):
if self.maxvalue == -2:
return _("undefined")
if self.maxvalue == -1:
return _("unlimited")
return "%d%%" % self.usage
@@ -1,44 +0,0 @@
# coding: utf-8
"""
The *limits* extension
----------------------
"""
from django.utils.translation import ugettext_lazy
from modoboa.core.extensions import ModoExtension, exts_pool
from modoboa.core.models import User
from modoboa.lib import events, parameters
from .models import LimitTemplates, LimitsPool, Limit
EVENTS = [
'GetExtraLimitTemplates'
]
class Limits(ModoExtension):
name = "limits"
label = "Limits"
version = "1.0"
description = ugettext_lazy(
"Per administrator resources to limit the number of objects "
"they can create"
)
def load(self):
from modoboa.extensions.limits.app_settings import ParametersForm
from modoboa.extensions.limits import controls
parameters.register(ParametersForm, ugettext_lazy("Limits"))
events.declare(EVENTS)
from modoboa.extensions.limits import general_callbacks
def load_initial_data(self):
"""Create pools for existing accounts."""
for user in User.objects.filter(groups__name="DomainAdmins"):
pool, created = LimitsPool.objects.get_or_create(user=user)
for tpl in LimitTemplates().templates:
Limit.objects.get_or_create(name=tpl[0], pool=pool, maxvalue=0)
exts_pool.register_extension(Limits)
@@ -1,12 +0,0 @@
{% load i18n lib_tags %}
<ul class="nav nav-sidebar">
<li class="nav-header">{% trans "Resources usage" %}</li>
{% for l in limits %}
<li class="resource">
{{ l.label }} ({{ l }})
<div rel="tooltip" title="{{ l.curvalue }}/{{ l.maxvalue }}" class="progress">
<div class="{% progress_color l.usage %}" style="width: {{ l }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</li>
{% endfor %}
</ul>
-394
View File
@@ -1,394 +0,0 @@
"""Test cases for the limits extension."""
from django.core.urlresolvers import reverse
from modoboa.core.factories import UserFactory
from modoboa.core.models import User
from modoboa.extensions.admin.factories import (
populate_database
)
from modoboa.extensions.admin.models import Alias, Domain
from modoboa.extensions.limits.models import LimitTemplates
from modoboa.lib import parameters
from modoboa.lib.tests import ModoTestCase
class PermissionsTestCase(ModoTestCase):
fixtures = ["initial_users.json"]
def setUp(self):
super(PermissionsTestCase, self).setUp()
populate_database()
def test_domainadmin_deletes_reseller(self):
"""Check if a domain admin can delete a reseller.
Expected result: no.
"""
values = dict(
username="reseller@test.com", first_name="Reseller", last_name="",
password1="toto", password2="toto", role="Resellers",
is_active=True, email="reseller@test.com", stepid='step2'
)
self.ajax_post(reverse("admin:account_add"), values)
account = User.objects.get(username="reseller@test.com")
self.clt.logout()
self.clt.login(username="admin@test.com", password="toto")
resp = self.ajax_post(
reverse("admin:account_delete", args=[account.id]),
{}, status=403
)
self.assertEqual(resp, "Permission denied")
class ResourceTestCase(ModoTestCase):
fixtures = ["initial_users.json"]
def setUp(self):
"""Custom setUp method.
The 'limits' is manually loaded to ensure extra parameters
provided by 'postfix_relay_domains' are properly received.
"""
super(ResourceTestCase, self).setUp()
for tpl in LimitTemplates().templates:
parameters.save_admin('DEFLT_%s' % tpl[0].upper(), 2, app='limits')
populate_database()
def _create_account(self, username, role='SimpleUsers', status=200):
values = dict(
username=username, first_name="Tester", last_name="Toto",
password1="toto", password2="toto", role=role,
quota_act=True,
is_active=True, email=username, stepid='step2',
)
return self.ajax_post(
reverse("admin:account_add"), values, status
)
def _create_alias(self, email, rcpt='user@test.com', status=200):
values = dict(
email=email, recipients=rcpt, enabled=True
)
return self.ajax_post(
reverse("admin:alias_add"), values, status
)
def _create_domain(self, name, status=200, withtpl=False):
values = {
"name": name, "quota": 100, "create_dom_admin": "no",
"create_aliases": "no", "stepid": 'step2'
}
if withtpl:
values['create_dom_admin'] = 'yes'
values['dom_admin_username'] = 'admin'
values['create_aliases'] = 'yes'
return self.ajax_post(
reverse("admin:domain_add"), values, status
)
def _domain_alias_operation(self, optype, domain, name, status=200):
dom = Domain.objects.get(name=domain)
values = {
'name': dom.name, 'quota': dom.quota, 'enabled': dom.enabled,
}
aliases = [alias.name for alias in dom.domainalias_set.all()]
if optype == 'add':
aliases.append(name)
else:
aliases.remove(name)
for cpt, alias in enumerate(aliases):
fname = 'aliases' if not cpt else 'aliases_%d' % cpt
values[fname] = alias
self.ajax_post(
reverse("admin:domain_change", args=[dom.id]),
values, status
)
def _check_limit(self, name, curvalue, maxvalue):
l = self.user.limitspool.get_limit('%s_limit' % name)
self.assertEqual(l.curvalue, curvalue)
self.assertEqual(l.maxvalue, maxvalue)
class DomainAdminTestCase(ResourceTestCase):
def setUp(self):
super(DomainAdminTestCase, self).setUp()
self.user = User.objects.get(username='admin@test.com')
self.user.limitspool.set_maxvalue('mailboxes_limit', 2)
self.user.limitspool.set_maxvalue('mailbox_aliases_limit', 2)
self.clt.logout()
self.clt.login(username='admin@test.com', password='toto')
def test_mailboxes_limit(self):
self._create_account('tester1@test.com')
self._check_limit('mailboxes', 1, 2)
self._create_account('tester2@test.com')
self._check_limit('mailboxes', 2, 2)
resp = self._create_account('tester3@test.com', status=403)
self._check_limit('mailboxes', 2, 2)
self.ajax_post(
reverse('admin:account_delete',
args=[User.objects.get(username='tester2@test.com').id]),
{}
)
self._check_limit('mailboxes', 1, 2)
def test_aliases_limit(self):
self._create_alias('alias1@test.com')
self._check_limit('mailbox_aliases', 1, 2)
self._create_alias('alias2@test.com')
self._check_limit('mailbox_aliases', 2, 2)
resp = self._create_alias('alias3@test.com', status=403)
self._check_limit('mailbox_aliases', 2, 2)
self.ajax_post(
reverse('admin:alias_delete') + '?selection=%d'
% Alias.objects.get(address='alias2', domain__name='test.com').id,
{}
)
self._check_limit('mailbox_aliases', 1, 2)
def test_aliases_limit_through_account_form(self):
user = User.objects.get(username='user@test.com')
values = dict(
username=user.username, role=user.group,
is_active=user.is_active, email=user.email, quota_act=True,
aliases="alias1@test.com", aliases_1="alias2@test.com"
)
self.ajax_post(
reverse("admin:account_change", args=[user.id]),
values
)
Alias.objects.get(address='alias1', domain__name='test.com')
self._check_limit('mailbox_aliases', 2, 2)
class ResellerTestCase(ResourceTestCase):
def setUp(self):
super(ResellerTestCase, self).setUp()
self.user = UserFactory.create(
username='reseller', groups=('Resellers',)
)
self.clt.logout()
self.clt.login(username='reseller', password='toto')
def test_domains_limit(self):
self._create_domain('domain1.tld')
self._check_limit('domains', 1, 2)
self._create_domain('domain2.tld')
self._check_limit('domains', 2, 2)
resp = self._create_domain('domain3.tld', 403)
self._check_limit('domains', 2, 2)
self.ajax_post(
reverse('admin:domain_delete',
args=[Domain.objects.get(name='domain2.tld').id]),
{}
)
self._check_limit('domains', 1, 2)
def test_domain_aliases_limit(self):
self._create_domain('pouet.com')
self._domain_alias_operation('add', 'pouet.com', 'domain_alias1.tld')
self._check_limit('domain_aliases', 1, 2)
self._domain_alias_operation('add', 'pouet.com', 'domain_alias2.tld')
self._check_limit('domain_aliases', 2, 2)
resp = self._domain_alias_operation(
'add', 'pouet.com', 'domain_alias3.tld', 403
)
self._check_limit('domain_aliases', 2, 2)
self._domain_alias_operation('delete', 'pouet.com', 'domain_alias2.tld')
self._check_limit('domain_aliases', 1, 2)
def test_domain_admins_limit(self):
self._create_domain('domain.tld')
self._create_account('admin1@domain.tld', role='DomainAdmins')
self._check_limit('domain_admins', 1, 2)
self._create_account('admin2@domain.tld', role='DomainAdmins')
self._check_limit('domain_admins', 2, 2)
resp = self._create_account(
'admin3@domain.tld',
role='DomainAdmins',
status=400)
self.assertEqual(
resp['form_errors']['role'][0],
'Select a valid choice. DomainAdmins is not one of the available '
'choices.'
)
self._check_limit('domain_admins', 2, 2)
self.user.limitspool.set_maxvalue('mailboxes_limit', 3)
self._create_account('user1@domain.tld')
user = User.objects.get(username='user1@domain.tld')
values = {
'username': user.username, 'role': 'DomainAdmins',
'quota_act': True, 'is_active': user.is_active,
'email': user.email
}
resp = self.ajax_post(
reverse("admin:account_change", args=[user.id]),
values, status=400
)
self.assertEqual(
resp['form_errors']['role'][0],
'Select a valid choice. DomainAdmins is not one of the available '
'choices.'
)
self._check_limit('domain_admins', 2, 2)
def test_domain_admin_resource_are_empty(self):
self._create_domain('domain.tld')
self._create_account('admin1@domain.tld', role='DomainAdmins')
domadmin = User.objects.get(username='admin1@domain.tld')
for l in ['mailboxes', 'mailbox_aliases']:
self.assertEqual(
domadmin.limitspool.get_limit('%s_limit' % l).maxvalue, 0
)
def test_domain_admins_limit_from_domain_tpl(self):
self.user.limitspool.set_maxvalue('domains_limit', 3)
self._create_domain('domain1.tld', withtpl=True)
self._create_domain('domain2.tld', withtpl=True)
self._check_limit('domain_admins', 2, 2)
self._check_limit('domains', 2, 3)
resp = self._create_domain('domain3.tld', status=200, withtpl=True)
self._check_limit('domain_admins', 2, 2)
self._check_limit('domains', 3, 3)
def test_reseller_deletes_domain(self):
"""Check if all resources are restored after the deletion.
"""
self._create_domain('domain.tld', withtpl=True)
dom = Domain.objects.get(name="domain.tld")
self.ajax_post(
reverse("admin:domain_delete", args=[dom.id]),
{}
)
self._check_limit('domains', 0, 2)
self._check_limit('domain_admins', 1, 2)
self._check_limit('mailboxes', 0, 2)
self._check_limit('mailbox_aliases', 0, 2)
def test_sadmin_removes_ownership(self):
self._create_domain('domain.tld', withtpl=True)
dom = Domain.objects.get(name="domain.tld")
self.clt.logout()
self.clt.login(username='admin', password='password')
self.ajax_get(
"{0}?domid={1}&daid={2}".format(
reverse('admin:permission_remove'), dom.id, self.user.id
), {}
)
self._check_limit('domains', 0, 2)
self._check_limit('domain_admins', 0, 2)
self._check_limit('mailboxes', 0, 2)
self._check_limit('mailbox_aliases', 0, 2)
def test_allocate_from_pool(self):
self._create_domain('domain.tld')
self._create_account('admin1@domain.tld', role='DomainAdmins')
user = User.objects.get(username='admin1@domain.tld')
# Give 1 mailbox and 2 aliases to the admin -> should work
values = {
'username': user.username, 'role': user.group, 'quota_act': True,
'is_active': user.is_active, 'email': user.email,
'mailboxes_limit': 1, 'mailbox_aliases_limit': 2
}
self.ajax_post(
reverse("admin:account_change", args=[user.id]),
values
)
self._check_limit('mailboxes', 1, 1)
self._check_limit('mailbox_aliases', 0, 0)
# Delete the admin -> resources should go back to the
# reseller's pool
self.ajax_post(
reverse("admin:account_delete", args=[user.id]),
{}
)
self._check_limit('mailboxes', 0, 2)
self._check_limit('mailbox_aliases', 0, 2)
def test_restore_resources(self):
self._create_domain('domain.tld')
dom = Domain.objects.get(name='domain.tld')
self._create_account('admin1@domain.tld', role='DomainAdmins')
user = User.objects.get(username='admin1@domain.tld')
values = {
'username': user.username, 'role': user.group, 'quota_act': True,
'is_active': user.is_active, 'email': user.email,
'mailboxes_limit': 1, 'mailbox_aliases_limit': 2
}
self.ajax_post(
reverse("admin:account_change", args=[user.id]),
values
)
dom.add_admin(user)
self.clt.logout()
self.clt.login(username='admin1@domain.tld', password='toto')
self._create_account('user1@domain.tld')
self._create_alias('alias1@domain.tld', 'user1@domain.tld')
self._create_alias('alias2@domain.tld', 'user1@domain.tld')
self.clt.logout()
self.clt.login(username='reseller', password='toto')
# Delete the admin -> resources should go back to the
# reseller's pool
self.ajax_post(
reverse("admin:account_delete", args=[user.id]),
{}
)
self._check_limit('mailboxes', 1, 2)
self._check_limit('mailbox_aliases', 2, 2)
def test_change_role(self):
self._create_domain('domain.tld')
self._create_account('admin1@domain.tld', role='DomainAdmins')
user = User.objects.get(username='admin1@domain.tld')
# Give 1 mailbox and 2 aliases to the admin -> should work
values = {
'username': user.username, 'role': user.group, 'quota_act': True,
'is_active': user.is_active, 'email': user.email,
'mailboxes_limit': 1, 'mailbox_aliases_limit': 2
}
self.ajax_post(
reverse("admin:account_change", args=[user.id]),
values
)
self._check_limit('mailboxes', 1, 1)
self._check_limit('mailbox_aliases', 0, 0)
# Change admin role to SimpleUser -> resources should go back
# to the reseller.
values = {
'username': user.username, 'role': 'SimpleUsers', 'quota_act': True,
'is_active': user.is_active, 'email': user.email,
}
self.ajax_post(
reverse("admin:account_change", args=[user.id]),
values
)
self._check_limit('mailboxes', 1, 2)
self._check_limit('mailbox_aliases', 0, 2)
def test_allocate_too_much(self):
self._create_domain('domain.tld')
self._create_account('admin1@domain.tld', role='DomainAdmins')
user = User.objects.get(username='admin1@domain.tld')
# Give 2 mailboxes and 3 aliases to the admin -> should fail.
values = {
'username': user.username, 'role': user.group, 'quota_act': True,
'is_active': user.is_active, 'email': user.email,
'mailboxes_limit': 2, 'mailbox_aliases_limit': 3
}
resp = self.ajax_post(
reverse("admin:account_change", args=[user.id]),
values, 424
)
self.assertEqual(resp, 'Not enough resources')
self._check_limit('mailboxes', 1, 2)
self._check_limit('mailbox_aliases', 0, 2)
-6
View File
@@ -1,6 +0,0 @@
from django.conf.urls import patterns
urlpatterns = patterns(
'modoboa.extensions.limits.views',
)
-1
View File
@@ -1 +0,0 @@
# coding: utf-8
-25
View File
@@ -1,25 +0,0 @@
# coding: utf-8
from django.utils.translation import ugettext_lazy
from django import forms
from modoboa.lib.parameters import AdminParametersForm
from modoboa.lib.form_utils import SeparatorField
class ParametersForm(AdminParametersForm):
app = "stats"
general_sep = SeparatorField(label=ugettext_lazy("General"))
logfile = forms.CharField(
label=ugettext_lazy("Path to the log file"),
initial="/var/log/mail.log",
help_text=ugettext_lazy("Path to log file used to collect statistics"),
widget=forms.TextInput(attrs={"class": "form-control"})
)
rrd_rootdir = forms.CharField(
label=ugettext_lazy("Directory to store RRD files"),
initial="/tmp/modoboa",
help_text=ugettext_lazy("Path to directory where RRD files are stored"),
widget=forms.TextInput(attrs={"class": "form-control"})
)
@@ -1,22 +0,0 @@
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from modoboa.lib import events
@events.observe("UserMenuDisplay")
def menu(target, user):
if target != "top_menu" or user.group == "SimpleUsers":
return []
return [
{"name": "stats",
"label": _("Statistics"),
"url": reverse('stats:fullindex')}
]
@events.observe("GetGraphSets")
def get_default_graphic_sets():
from modoboa.extensions.stats.graphics import MailTraffic
gset = MailTraffic()
return {gset.html_id: gset}
-161
View File
@@ -1,161 +0,0 @@
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.decode("utf-8"))
]
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)
if isinstance(cmd, unicode):
cmd = cmd.encode("utf-8")
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 (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 (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
@@ -1,24 +0,0 @@
# 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 isinstance(elem, 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))
@@ -1,378 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
"""
Postfix log parser.
This scripts parses a log file produced by postfix (or using the same
format). It looks for predefined events and build statistics about
their occurence rate.
At the, somes default graphics are generated using the grapher module.
(see grapher.py)
Predefined events are:
* Per domain sent/received messages,
* Per domain received bad messages (bounced, reject for now),
* Per domain sent/received traffics size,
* Global consolidation of all previous events.
"""
import time
import sys
import os
import re
import string
from optparse import make_option
import rrdtool
from django.core.management.base import BaseCommand
from modoboa.core.extensions import exts_pool
from modoboa.core.management.commands import CloseConnectionMixin
from modoboa.extensions.admin.models import Domain
from modoboa.extensions.stats.lib import date_to_timestamp
from modoboa.lib import parameters
rrdstep = 60
xpoints = 540
points_per_sample = 3
variables = ["sent", "recv", "bounced", "reject", "spam", "virus",
"size_sent", "size_recv"]
class LogParser(object):
def __init__(self, options, workdir, year=None):
"""Constructor
"""
self.logfile = options["logfile"]
self.debug = options["debug"]
self.verbose = options["verbose"]
try:
self.f = open(self.logfile)
except IOError as errno:
self._dprint("%s" % errno)
sys.exit(1)
self.workdir = workdir
self.__year = year
self.cfs = ['AVERAGE', 'MAX']
curtime = time.localtime()
if not self.__year:
self.__year = curtime.tm_year
self.curmonth = curtime.tm_mon
self.data = {}
self.domains = []
self._load_domain_list()
self.data["global"] = {}
self.workdict = {}
self.lupdates = {}
self._s_date_expr = \
re.compile(
r"(?P<month>\w+)\s+(?P<day>\d+)\s+(?P<hour>\d+):(?P<min>\d+):(?P<sec>\d+)(?P<eol>.*)")
self._hp_date_expr = \
re.compile(
r"(?P<year>\d+)-(?P<month>\d+)-(?P<day>\d+)T(?P<hour>\d+):(?P<min>\d+):(?P<sec>\d+)\.\d+\+\d+:\d+(?P<eol>.*)")
self.date_expr = None
self.line_expr = \
re.compile(r"\s+([-\w]+)\s+(\w+)/?\w*[[](\d+)[]]:\s+(.*)")
self._id_expr = re.compile(r"(\w+): (.*)")
self._prev_se = -1
self._prev_mi = -1
self._prev_ho = -1
self.cur_t = 0
def _load_domain_list(self):
"""Load the list of allowed domains.
Since the relay domains feature is an extension of the admin
panel, we don't use an event to get the list of all supported
domains...
"""
for dom in Domain.objects.all():
self.domains += [str(dom.name)]
self.data[str(dom.name)] = {}
if not exts_pool.is_extension_installed(
"modoboa.extensions.postfix_relay_domains"):
return
from modoboa.extensions.postfix_relay_domains.models import RelayDomain
for rdom in RelayDomain.objects.all():
self.domains += [str(rdom.name)]
self.data[str(rdom.name)] = {}
def _dprint(self, msg):
"""Print a debug message if required.
:param str msg: debug message
"""
if not self.debug:
return
print msg
def _parse_date(self, line):
"""Try to match a date inside :kw:`line` and to convert it to
a timestamp.
We try different date format until we find valid one. We then
store it for future use.
:param str line: a log entry
:return: the remaining part of the line or None
"""
match = None
if self.date_expr is None:
for expr in [self._s_date_expr, self._hp_date_expr]:
match = expr.match(line)
if match is not None:
self.date_expr = expr
break
else:
match = self.date_expr.match(line)
if match is None:
return None
ho = match.group("hour")
mi = match.group("min")
se = match.group("sec")
se = int(int(se) / rrdstep) # rrd step is one-minute => se = 0
if self._prev_se != se or self._prev_mi != mi or self._prev_ho != ho:
mo = match.group("month")
da = match.group("day")
try:
ye = match.group("year")
except IndexError:
ye = self.year(mo)
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
self._prev_se = se
return match.group('eol')
def init_rrd(self, fname, m):
"""init_rrd
Set-up Data Sources (DS)
Set-up Round Robin Archives (RRA):
- day,week,month and year archives
- 2 types : AVERAGE and MAX
parameter : start time
return : last epoch recorded
"""
ds_type = 'ABSOLUTE'
rows = xpoints / points_per_sample
realrows = int(rows * 1.1) # ensure that the full range is covered
day_steps = int(3600 * 24 / (rrdstep * rows))
week_steps = day_steps * 7
month_steps = week_steps * 5
year_steps = month_steps * 12
# Set up data sources for our RRD
params = []
for v in variables:
params += ['DS:%s:%s:%s:0:U' % (v, ds_type, rrdstep * 2)]
# Set up RRD to archive data
for cf in ['AVERAGE', 'MAX']:
for step in [day_steps, week_steps, month_steps, year_steps]:
params += ['RRA:%s:0.5:%s:%s' % (cf, step, realrows)]
# With those setup, we can now created the RRD
rrdtool.create(str(fname),
'--start', str(m),
'--step', str(rrdstep),
*params)
return m
def update_rrd(self, dom, t):
"""update_rrd
Update RRD with records at t time.
True : if data are up-to-date for current minute
False : syslog may have probably been already recorded
or something wrong
"""
fname = "%s/%s.rrd" % (self.workdir, dom)
m = t - (t % rrdstep)
if not os.path.exists(fname):
self.lupdates[fname] = self.init_rrd(fname, m)
self._dprint("[rrd] create new RRD file %s" % fname)
else:
if fname not in self.lupdates:
self.lupdates[fname] = rrdtool.last(str(fname))
if m <= self.lupdates[fname]:
if self.verbose:
print "[rrd] VERBOSE events at %s already recorded in RRD" % m
return False
tpl = ""
for v in variables:
if tpl != "":
tpl += ":"
tpl += v
# Missing some RRD steps
# Est ce vraiment nécessaire... ?
if m > self.lupdates[fname] + rrdstep:
values = ""
for v in variables:
if values != "":
values += ":"
values += "0"
for p in range(self.lupdates[fname] + rrdstep, m, rrdstep):
if self.verbose:
print "[rrd] VERBOSE update -t %s %s:%s (SKIP)" \
% (tpl, p, values)
rrdtool.update(str(fname), "-t", tpl, "%s:%s" % (p, values))
values = "%s" % m
tpl = ""
for v in variables:
values += ":"
values += str(self.data[dom][m][v])
if tpl != "":
tpl += ":"
tpl += v
if self.verbose:
print "[rrd] VERBOSE update -t %s %s" % (tpl, values)
rrdtool.update(str(fname), "-t", tpl, values)
self.lupdates[fname] = m
return True
def initcounters(self, dom):
init = {}
for v in variables:
init[v] = 0
self.data[dom][self.cur_t] = init
def inc_counter(self, dom, counter, val=1):
if dom is not None and dom in self.domains:
if self.cur_t not in self.data[dom]:
self.initcounters(dom)
self.data[dom][self.cur_t][counter] += val
if self.cur_t not in self.data["global"]:
self.initcounters("global")
self.data["global"][self.cur_t][counter] += val
def year(self, month):
"""Return the appropriate year
Date used in log files don't always embark the year so we need
to guess it :p
This method tries to deal with year changes in a simply
(ugly?) way: if we currently are in january and the given
month is different, return the current year -1. Otherwise,
return the current year.
Obviously, this method only works for year to year + 1
changes.
:param month: the month of the current record beeing parsed
:return: an integer
"""
month = time.strptime(month, "%b").tm_mon if not month.isdigit() \
else int(month)
if self.curmonth == 1 and month != self.curmonth:
return self.__year - 1
return self.__year
def _parse_line(self, line):
"""Parse a single log line.
:param str line: log line
"""
line = self._parse_date(line)
if line is None:
return
m = self.line_expr.match(line)
if not m:
return
host, prog, pid, log = m.groups()
m = self._id_expr.match(log)
if m is None:
self._dprint("Unknown line format: %s" % log)
return
(line_id, line_log) = m.groups()
if line_id == "NOQUEUE":
addrto = re.match(
"reject: .*from=<.*> to=<[^@]+@([^>]+)>",
line_log)
if addrto and addrto.group(1) in self.domains:
self.inc_counter(addrto.group(1), 'reject')
return
m = re.search("message-id=<([^>]*)>", line_log)
if m is not None:
self.workdict[line_id] = {'from': m.group(1), 'size': 0}
return
m = re.search("from=<([^>]*)>, size=(\d+)", line_log)
if m is not None:
self.workdict[line_id] = {
'from': m.group(1), 'size': string.atoi(m.group(2))
}
return
m = re.search("to=<([^>]*)>.*status=(\S+)", line_log)
if m is not None:
if line_id not in self.workdict:
self._dprint("Inconsistent mail (%s: %s), skipping"
% (line_id, m.group(1)))
return
if not m.group(2) in variables:
self._dprint("Unsupported status %s, skipping" % m.group(2))
return
addrfrom = re.match("([^@]+)@(.+)", self.workdict[line_id]['from'])
if addrfrom is not None and addrfrom.group(2) in self.domains:
self.inc_counter(addrfrom.group(2), 'sent')
self.inc_counter(addrfrom.group(2), 'size_sent',
self.workdict[line_id]['size'])
addrto = re.match("([^@]+)@(.+)", m.group(1))
domname = addrto.group(2) if addrto is not None else None
if m.group(2) == "sent":
self.inc_counter(domname, 'recv')
self.inc_counter(domname, 'size_recv',
self.workdict[line_id]['size'])
else:
self.inc_counter(domname, m.group(2))
return
self._dprint("Unknown line format: %s" % line_log)
def process(self):
"""Process the log file.
We parse it and then generate standard graphics (day, week,
month).
"""
for line in self.f.readlines():
self._parse_line(line)
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)
class Command(BaseCommand, CloseConnectionMixin):
help = 'Log file parser'
option_list = BaseCommand.option_list + (
make_option("--logfile", default=None,
help="postfix log in syslog format", metavar="FILE"),
make_option("--verbose", default=False, action="store_true",
dest="verbose", help="Set verbose mode"),
make_option("--debug", default=False, action="store_true",
help="Set debug mode")
)
def handle(self, *args, **options):
if options["logfile"] is None:
options["logfile"] = parameters.get_admin("LOGFILE", app="stats")
p = LogParser(options, parameters.get_admin("RRD_ROOTDIR", app="stats"))
p.process()
@@ -1,35 +0,0 @@
# coding: utf-8
"""
Graphical statistics about emails traffic using RRDtool
This module provides support to retrieve statistics from postfix log :
sent, received, bounced, rejected
"""
import sys
from django.utils.translation import ugettext_lazy
from modoboa.core.extensions import ModoExtension, exts_pool
from modoboa.lib import events, parameters
class Stats(ModoExtension):
name = "stats"
label = "Statistics"
version = "1.0"
description = ugettext_lazy(
"Graphical statistics about emails traffic using RRDtool"
)
needs_media = True
def load(self):
from modoboa.extensions.stats.app_settings import ParametersForm
events.declare(["GetGraphSets"])
parameters.register(
ParametersForm, ugettext_lazy("Graphical statistics")
)
from modoboa.extensions.stats import general_callbacks
exts_pool.register_extension(Stats)
@@ -1,71 +0,0 @@
#menubar {
padding-left: 15px;
}
.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;
}
@@ -1,70 +0,0 @@
// 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;
};
})();
@@ -1,344 +0,0 @@
/**
* 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;
}
@@ -1,176 +0,0 @@
/**
* Create an instance of Stats.
*
* @constructor
*/
var Stats = function(options) {
this.initialize(options);
};
Stats.prototype = {
constructor: Stats,
defaults: {
deflocation: "graphs/",
language: "en",
domain_list_url: null
},
initialize: function(options) {
this.options = $.extend({}, this.defaults, options);
this.options.defcallback = $.proxy(this.charts_cb, this);
var navobj = new History(this.options);
this.navobj = navobj;
if (navobj.params.searchquery !== undefined) {
$("#searchquery").val(navobj.params.searchquery);
}
$("#searchquery").focus(function() {
var $this = $(this);
if ($this.val() === undefined) {
return;
}
$this.data("oldvalue", $this.val());
$this.val("");
}).blur(function() {
var $this = $(this);
if ($this.val() === "") {
if ($this.data("oldvalue")) {
$this.val($this.data('oldvalue'));
$this.data("oldvalue", null);
} else {
$this.val(gettext("Search a domain"));
}
}
});
if (navobj.params.start) {
$("#id_from").val(navobj.params.start);
}
if (navobj.params.end) {
$("#id_to").val(navobj.params.end);
}
if (navobj.params.period) {
$("input[data-period={0}]".format(navobj.params.period))
.attr("checked", true)
.parent().addClass("active");
if (navobj.params.period == "custom") {
$("#custom_period").removeClass("hidden");
}
}
$("#custom-period .datetime_picker").datetimepicker({
format: 'YYYY-MM-DD hh:mm:ss',
language: this.options.language
});
$("#searchquery").autocompleter({
choices: $.proxy(this.get_domain_list, this),
choice_selected: $.proxy(this.search_domain, this),
empty_choice: $.proxy(this.reset_search, this)
});
this.register_nav_callbacks();
this.listen();
},
listen: function() {
$(".period_selector").click($.proxy(this.change_period, 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.charts_cb, this));
},
/**
* Retrieve a list of domain from the server.
*/
get_domain_list: function() {
var result;
$.ajax({
url: this.options.domain_list_url,
dataType: "json",
async: false
}).done(function(data) {
result = data;
});
return result;
},
change_period: function(e) {
e.preventDefault();
var $link = $(e.target).children("input");
var period = $link.attr("data-period");
if (period != "custom") {
$("#custom_period").addClass("hidden");
this.navobj.delparam("start").delparam("end");
this.navobj.setparam("period", $link.attr("data-period")).update();
} else {
$("#custom_period").removeClass("hidden");
}
},
/**
* 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) {
var menuid = "menu_" + this.navobj.getparam("gset");
$("#" + menuid).addClass("active");
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));
}
},
search_domain: function(value) {
this.navobj.setparam("searchquery", value).update();
},
reset_search: function() {
$("#searchquery").data("oldvalue", null);
this.navobj.delparam("searchquery").update();
},
custom_period: function(e) {
e.preventDefault();
var $fromdate = $("#id_from");
var $todate = $("#id_to");
if ($fromdate.val() === "" || $todate.val() === "") {
return;
}
this.navobj.setparams({
period: "custom",
start: $fromdate.val(),
end: $todate.val()
}).update();
}
};
@@ -1,89 +0,0 @@
{% extends "twocols.html" %}{% load i18n %}{% load url from future %}
{% load static %}
{% block pagetitle %}{% trans "Graphical statistics" %}{% endblock %}
{% block extra_css %}
<link rel="stylesheet" type="text/css" href="{% static 'stats/css/stats.css' %}" />
{% endblock %}
{% block extra_js %}
<script type="text/javascript" src="{% static 'js/history.js' %}"></script>
<script type="text/javascript" src="{% static 'js/autocompleter.js' %}"></script>
<script type="text/javascript" src="{% static 'd3/d3.min.js' %}"></script>
<script type="text/javascript" src="{% static 'stats/js/d3.legend.js' %}"></script>
<script type="text/javascript" src="{% static 'stats/js/graphic.js' %}"></script>
<script type="text/javascript" src="{% static 'stats/js/stats.js' %}"></script>
<script type="text/javascript">
$(document).ready(function() {
var stats = new Stats({
graphurl: "{% url 'stats:graph_list' %}",
domain_list_url: "{% url 'stats:domain_list' %}",
deflocation: "{{ deflocation|safe }}",
language: "{{ LANGUAGE_CODE }}"
});
});
</script>
{% endblock %}
{% block leftcol %}
<ul class="nav nav-sidebar">
{% for id, gset in graph_sets.items %}
<li id="menu_{{ gset.html_id }}"><a href="#">{{ gset.title }}</a></li>
{% endfor %}
</ul>
{% endblock %}
{% block apparea %}
<div id="menubar" class="row">
<div class="col-sm-5">
<form class="form-horizontal" role="form">
<div class="form-group has-feedback">
<input class="form-control" id="searchquery" type="text" value="{% trans 'Search a domain' %}">
<span class="fa fa-search form-control-feedback"></span>
</div>
</form>
</div>
<div class="col-sm-7">
<div class="btn-group" data-toggle="buttons">
{% for p in periods %}
<label class="btn btn-primary period_selector">
<input type="radio" data-period="{{ p.name }}"> {% trans p.label %}
</label>
{% endfor %}
</div>
</div>
</div>
<div id="custom_period" class="row hidden">
<div class="col-sm-7">
<form class="form-inline">
<span id="custom-period">
<div class="form-group">
<div class="input-group datetime_picker">
<div class="input-group-addon"><span class="fa fa-outdent"></span></div>
<input id="id_from" name="date_form" class="form-control" value="{{ start }}" type="text" readonly="readonly">
</div>
</div>
<div class="form-group">
<div class="input-group datetime_picker">
<div class="input-group-addon"><span class="fa fa-outdent"></span></div>
<input id="id_to" name="date_to" value="{{ end }}" class="form-control" type="text" readonly="readonly" />
</div>
</div>
<button type="submit" class="btn btn-primary" id="customsend">{% trans "Apply" %}</button>
</span>
</form>
</div>
</div>
<div id="graphs">
{% for id, gset in graph_sets.items %}
<div class="tab-pane {% if forloop.first %}active{% endif %}" id="graphs_{{ gset.html_id }}">
<div id="gset"></div>
</div>
{% endfor %}
</div>
{% endblock %}
-10
View File
@@ -1,10 +0,0 @@
from django.conf.urls import patterns, url
urlpatterns = patterns(
'modoboa.extensions.stats.views',
url(r'^$', 'index', name='fullindex'),
url(r'^graphs/$', "graphs", name="graph_list"),
url(r'^domains/$', "get_domain_list", name="domain_list"),
)
-134
View File
@@ -1,134 +0,0 @@
# coding: utf-8
"""Custom views."""
import re
import time
from django.shortcuts import render
from django.utils.translation import ugettext as _
from django.contrib.auth.decorators import (
login_required, user_passes_test, permission_required
)
from modoboa.core.extensions import exts_pool
from modoboa.lib import events
from modoboa.lib.exceptions import BadRequest, PermDeniedException, NotFound
from modoboa.lib.web_utils import (
render_to_json_response
)
from modoboa.extensions.admin.models import (
Domain
)
from modoboa.extensions.stats.lib import date_to_timestamp
@login_required
@permission_required("admin.view_mailboxes")
def index(request):
"""
FIXME: how to select a default graph set ?
"""
deflocation = "graphs/?gset=mailtraffic"
if not request.user.is_superuser:
if not Domain.objects.get_for_admin(request.user).count():
raise NotFound(_("No statistics available"))
graph_sets = events.raiseDictEvent('GetGraphSets')
periods = [
{"name": "day", "label": _("Day")},
{"name": "week", "label": _("Week")},
{"name": "month", "label": _("Month")},
{"name": "year", "label": _("Year")},
{"name": "custom", "label": _("Custom")}
]
return render(request, 'stats/index.html', {
"periods": periods,
"selection": "stats",
"deflocation": deflocation,
"graph_sets": graph_sets
})
def check_domain_access(user, pattern):
"""Check if an administrator can access a domain/relay domain.
If a non super administrator asks for the global view, we give him
a view on the first domain he manage instead.
:return: a domain name (str) or None.
"""
targets = [Domain]
if exts_pool.is_extension_installed(
"modoboa.extensions.postfix_relay_domains"):
from modoboa.extensions.postfix_relay_domains.models import RelayDomain
targets.append(RelayDomain)
if pattern in [None, "global"]:
if not user.is_superuser:
for target in targets:
if not target.objects.get_for_admin(user).count():
continue
return target.objects.get_for_admin(user)[0].name
return None
return "global"
for target in targets:
results = target.objects.filter(name__startswith=pattern)
if results.count() != 1:
continue
if not user.can_access(results[0]):
raise PermDeniedException
return results[0].name
return None
@login_required
@user_passes_test(lambda u: u.group != "SimpleUsers")
def graphs(request):
gset = request.GET.get("gset", None)
gsets = events.raiseDictEvent("GetGraphSets")
if gset not in gsets:
raise NotFound(_("Unknown graphic set"))
searchq = request.GET.get("searchquery", None)
period = request.GET.get("period", "day")
tplvars = dict(graphs={}, period=period)
domain = check_domain_access(request.user, searchq)
if domain is None:
return render_to_json_response({})
tplvars["domain"] = domain
if period == "custom":
if "start" not in request.GET or "end" not in request.GET:
raise BadRequest(_("Bad custom period"))
start = request.GET["start"]
end = request.GET["end"]
expr = re.compile(r'[:\- ]')
period_name = "%s_%s" % (expr.sub('', start), expr.sub('', end))
start = date_to_timestamp(expr.split(start))
end = date_to_timestamp(expr.split(end))
else:
end = int(time.mktime(time.localtime()))
start = "-1%s" % period
period_name = period
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)
@login_required
@user_passes_test(lambda u: u.group != "SimpleUsers")
def get_domain_list(request):
"""Get the list of domains (and relay domains) the user can see."""
doms = [dom.name for dom in Domain.objects.get_for_admin(request.user)]
if exts_pool.is_extension_installed(
"modoboa.extensions.postfix_relay_domains"):
from modoboa.extensions.postfix_relay_domains.models import RelayDomain
doms += [
rdom.name for rdom in
RelayDomain.objects.get_for_admin(request.user)
]
return render_to_json_response(doms)