mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
Remove limits extension.
This commit is contained in:
@@ -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')
|
||||
@@ -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"]]}}]
|
||||
@@ -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>
|
||||
"""]
|
||||
@@ -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')]),
|
||||
),
|
||||
]
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -1,6 +0,0 @@
|
||||
from django.conf.urls import patterns
|
||||
|
||||
urlpatterns = patterns(
|
||||
'modoboa.extensions.limits.views',
|
||||
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
# coding: utf-8
|
||||
@@ -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}
|
||||
@@ -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]
|
||||
@@ -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 %}
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user