diff --git a/modoboa/extensions/limits/__init__.py b/modoboa/extensions/limits/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/limits/app_settings.py b/modoboa/extensions/limits/app_settings.py deleted file mode 100644 index 2d759a3cd..000000000 --- a/modoboa/extensions/limits/app_settings.py +++ /dev/null @@ -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') diff --git a/modoboa/extensions/limits/controls.py b/modoboa/extensions/limits/controls.py deleted file mode 100644 index f5d00b4ac..000000000 --- a/modoboa/extensions/limits/controls.py +++ /dev/null @@ -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') diff --git a/modoboa/extensions/limits/fixtures/demo.json b/modoboa/extensions/limits/fixtures/demo.json deleted file mode 100644 index 52a9bc1a1..000000000 --- a/modoboa/extensions/limits/fixtures/demo.json +++ /dev/null @@ -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"]]}}] \ No newline at end of file diff --git a/modoboa/extensions/limits/forms.py b/modoboa/extensions/limits/forms.py deleted file mode 100644 index b96d10ef3..000000000 --- a/modoboa/extensions/limits/forms.py +++ /dev/null @@ -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() diff --git a/modoboa/extensions/limits/general_callbacks.py b/modoboa/extensions/limits/general_callbacks.py deleted file mode 100644 index 3f207eebc..000000000 --- a/modoboa/extensions/limits/general_callbacks.py +++ /dev/null @@ -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 [""" -"""] - return [""" - -"""] diff --git a/modoboa/extensions/limits/lib.py b/modoboa/extensions/limits/lib.py deleted file mode 100644 index c0135a234..000000000 --- a/modoboa/extensions/limits/lib.py +++ /dev/null @@ -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 diff --git a/modoboa/extensions/limits/migrations/0001_initial.py b/modoboa/extensions/limits/migrations/0001_initial.py deleted file mode 100644 index 82ed516e3..000000000 --- a/modoboa/extensions/limits/migrations/0001_initial.py +++ /dev/null @@ -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')]), - ), - ] diff --git a/modoboa/extensions/limits/migrations/__init__.py b/modoboa/extensions/limits/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/limits/models.py b/modoboa/extensions/limits/models.py deleted file mode 100644 index 60ced9325..000000000 --- a/modoboa/extensions/limits/models.py +++ /dev/null @@ -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 diff --git a/modoboa/extensions/limits/modo_extension.py b/modoboa/extensions/limits/modo_extension.py deleted file mode 100644 index 45a8659a6..000000000 --- a/modoboa/extensions/limits/modo_extension.py +++ /dev/null @@ -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) diff --git a/modoboa/extensions/limits/templates/limits/poolusage.html b/modoboa/extensions/limits/templates/limits/poolusage.html deleted file mode 100644 index 1665b6d31..000000000 --- a/modoboa/extensions/limits/templates/limits/poolusage.html +++ /dev/null @@ -1,12 +0,0 @@ -{% load i18n lib_tags %} - diff --git a/modoboa/extensions/limits/tests.py b/modoboa/extensions/limits/tests.py deleted file mode 100644 index 69b737714..000000000 --- a/modoboa/extensions/limits/tests.py +++ /dev/null @@ -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) diff --git a/modoboa/extensions/limits/urls.py b/modoboa/extensions/limits/urls.py deleted file mode 100644 index 9adc5e04a..000000000 --- a/modoboa/extensions/limits/urls.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.conf.urls import patterns - -urlpatterns = patterns( - 'modoboa.extensions.limits.views', - -) diff --git a/modoboa/extensions/limits/views.py b/modoboa/extensions/limits/views.py deleted file mode 100644 index 57d631c3f..000000000 --- a/modoboa/extensions/limits/views.py +++ /dev/null @@ -1 +0,0 @@ -# coding: utf-8 diff --git a/modoboa/extensions/stats/__init__.py b/modoboa/extensions/stats/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/stats/app_settings.py b/modoboa/extensions/stats/app_settings.py deleted file mode 100644 index 9c48d156e..000000000 --- a/modoboa/extensions/stats/app_settings.py +++ /dev/null @@ -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"}) - ) diff --git a/modoboa/extensions/stats/general_callbacks.py b/modoboa/extensions/stats/general_callbacks.py deleted file mode 100644 index e6112736d..000000000 --- a/modoboa/extensions/stats/general_callbacks.py +++ /dev/null @@ -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} diff --git a/modoboa/extensions/stats/graphics.py b/modoboa/extensions/stats/graphics.py deleted file mode 100644 index 80ca0af88..000000000 --- a/modoboa/extensions/stats/graphics.py +++ /dev/null @@ -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] diff --git a/modoboa/extensions/stats/lib.py b/modoboa/extensions/stats/lib.py deleted file mode 100644 index 9d78c039f..000000000 --- a/modoboa/extensions/stats/lib.py +++ /dev/null @@ -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)) diff --git a/modoboa/extensions/stats/management/__init__.py b/modoboa/extensions/stats/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/stats/management/commands/__init__.py b/modoboa/extensions/stats/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/extensions/stats/management/commands/logparser.py b/modoboa/extensions/stats/management/commands/logparser.py deleted file mode 100644 index 1f38ff6fa..000000000 --- a/modoboa/extensions/stats/management/commands/logparser.py +++ /dev/null @@ -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\w+)\s+(?P\d+)\s+(?P\d+):(?P\d+):(?P\d+)(?P.*)") - self._hp_date_expr = \ - re.compile( - r"(?P\d+)-(?P\d+)-(?P\d+)T(?P\d+):(?P\d+):(?P\d+)\.\d+\+\d+:\d+(?P.*)") - 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() diff --git a/modoboa/extensions/stats/modo_extension.py b/modoboa/extensions/stats/modo_extension.py deleted file mode 100644 index 926063168..000000000 --- a/modoboa/extensions/stats/modo_extension.py +++ /dev/null @@ -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) diff --git a/modoboa/extensions/stats/static/stats/css/stats.css b/modoboa/extensions/stats/static/stats/css/stats.css deleted file mode 100644 index edd761be7..000000000 --- a/modoboa/extensions/stats/static/stats/css/stats.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/modoboa/extensions/stats/static/stats/js/d3.legend.js b/modoboa/extensions/stats/static/stats/js/d3.legend.js deleted file mode 100644 index 52b267a6c..000000000 --- a/modoboa/extensions/stats/static/stats/js/d3.legend.js +++ /dev/null @@ -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; - }; -})(); diff --git a/modoboa/extensions/stats/static/stats/js/graphic.js b/modoboa/extensions/stats/static/stats/js/graphic.js deleted file mode 100644 index b650184f3..000000000 --- a/modoboa/extensions/stats/static/stats/js/graphic.js +++ /dev/null @@ -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 += "

" + d.name + ": " + point.y.toFixed(3) + "

"; - - 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("

" + date + "

" + 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; -} diff --git a/modoboa/extensions/stats/static/stats/js/stats.js b/modoboa/extensions/stats/static/stats/js/stats.js deleted file mode 100644 index 5258aecd1..000000000 --- a/modoboa/extensions/stats/static/stats/js/stats.js +++ /dev/null @@ -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(); - } -}; diff --git a/modoboa/extensions/stats/templates/stats/index.html b/modoboa/extensions/stats/templates/stats/index.html deleted file mode 100644 index 08e583a8b..000000000 --- a/modoboa/extensions/stats/templates/stats/index.html +++ /dev/null @@ -1,89 +0,0 @@ -{% extends "twocols.html" %}{% load i18n %}{% load url from future %} -{% load static %} - -{% block pagetitle %}{% trans "Graphical statistics" %}{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block extra_js %} - - - - - - - -{% endblock %} - -{% block leftcol %} - -{% endblock %} - -{% block apparea %} - - - - -
- {% for id, gset in graph_sets.items %} -
-
-
- {% endfor %} -
- -{% endblock %} diff --git a/modoboa/extensions/stats/urls.py b/modoboa/extensions/stats/urls.py deleted file mode 100644 index 00bbad712..000000000 --- a/modoboa/extensions/stats/urls.py +++ /dev/null @@ -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"), -) diff --git a/modoboa/extensions/stats/views.py b/modoboa/extensions/stats/views.py deleted file mode 100644 index de88d898c..000000000 --- a/modoboa/extensions/stats/views.py +++ /dev/null @@ -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)