diff --git a/.travis.yml b/.travis.yml index 40e25fa3b..92f9b63fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ before_script: script: - python ./tests.py - cd test_project - - coverage run --source ../modoboa manage.py test modoboa.core modoboa.lib modoboa.admin.tests modoboa.limits modoboa.relaydomains + - coverage run --source ../modoboa manage.py test modoboa.core modoboa.admin.tests modoboa.limits modoboa.relaydomains after_success: - codecov diff --git a/doc/contributing/events.rst b/doc/contributing/events.rst index 6e7e48889..26d5eb707 100644 --- a/doc/contributing/events.rst +++ b/doc/contributing/events.rst @@ -563,20 +563,6 @@ Must return a list of set. Each set must contain at least three entries:: [('', '', '')] -GetExtraParameters -================== - -Raised to request extra parameters for a given parameters form. - -*Callback prototype*:: - - def callback(application, level): pass - -* ``application`` is the name of the form's application (ie. admin, amavis, etc.) -* ``level`` is the form's level: ``A`` for admin or ``U`` for user - -Must return a dictionary. Each entry must be a valid Django form field. - .. _getextrarolepermissions: GetExtraRolePermissions diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index 922ac3731..f695ee673 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -6,11 +6,11 @@ from django import forms from django.conf import settings from django.utils.translation import ugettext_lazy -from ..lib import events -from modoboa.lib import parameters from modoboa.lib.form_utils import YesNoField, SeparatorField from modoboa.lib.sysutils import exec_cmd +from modoboa.parameters import forms as param_forms +from ..lib import events ADMIN_EVENTS = [ "DomainCreated", @@ -48,14 +48,14 @@ ADMIN_EVENTS = [ ] -class AdminParametersForm(parameters.AdminParametersForm): +class AdminParametersForm(param_forms.AdminParametersForm): app = "admin" dom_sep = SeparatorField(label=ugettext_lazy("Domains")) enable_mx_checks = YesNoField( label=ugettext_lazy("Enable MX checks"), - initial="yes", + initial=True, help_text=ugettext_lazy( "Check that every domain has a valid MX record" ) @@ -75,7 +75,7 @@ class AdminParametersForm(parameters.AdminParametersForm): enable_dnsbl_checks = YesNoField( label=ugettext_lazy("Enable DNSBL checks"), - initial="yes", + initial=True, help_text=ugettext_lazy( "Check every domain against major DNSBL providers" ) @@ -85,7 +85,7 @@ class AdminParametersForm(parameters.AdminParametersForm): handle_mailboxes = YesNoField( label=ugettext_lazy("Handle mailboxes on filesystem"), - initial="no", + initial=False, help_text=ugettext_lazy( "Rename or remove mailboxes on the filesystem when they get" " renamed or removed within Modoboa" @@ -111,14 +111,14 @@ class AdminParametersForm(parameters.AdminParametersForm): auto_account_removal = YesNoField( label=ugettext_lazy("Automatic account removal"), - initial="no", + initial=False, help_text=ugettext_lazy( "When a mailbox is removed, also remove the associated account") ) auto_create_domain_and_mailbox = YesNoField( label=ugettext_lazy("Automatic domain/mailbox creation"), - initial="yes", + initial=True, help_text=ugettext_lazy( "Create a domain and a mailbox when an account is automatically " "created." @@ -127,8 +127,8 @@ class AdminParametersForm(parameters.AdminParametersForm): # Visibility rules visibility_rules = { - "valid_mxs": "enable_mx_checks=yes", - "mailboxes_owner": "handle_mailboxes=yes", + "valid_mxs": "enable_mx_checks=True", + "mailboxes_owner": "handle_mailboxes=True", } def __init__(self, *args, **kwargs): @@ -174,9 +174,9 @@ class AdminParametersForm(parameters.AdminParametersForm): def load_admin_settings(): """Load admin settings.""" - from .app_settings import AdminParametersForm + from modoboa.parameters import tools as param_tools - parameters.register( - AdminParametersForm, ugettext_lazy("Administration")) + param_tools.registry.add( + "global", AdminParametersForm, ugettext_lazy("Administration")) events.declare(ADMIN_EVENTS) from . import callbacks diff --git a/modoboa/admin/callbacks.py b/modoboa/admin/callbacks.py index f65df4147..59e59313a 100644 --- a/modoboa/admin/callbacks.py +++ b/modoboa/admin/callbacks.py @@ -4,9 +4,10 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.core import signals as core_signals -from modoboa.lib import parameters, events +from modoboa.lib import events from modoboa.lib.email_utils import split_mailbox from modoboa.lib.exceptions import PermDeniedException, BadRequest, Conflict +from modoboa.parameters import tools as param_tools from .models import ( Domain, DomainAlias, Mailbox, Alias @@ -154,7 +155,7 @@ def account_auto_created(user): from modoboa.lib.permissions import grant_access_to_object from .lib import check_if_domain_exists - if parameters.get_admin("AUTO_CREATE_DOMAIN_AND_MAILBOX") == "no": + if not param_tools.get_global_parameter("auto_create_domain_and_mailbox"): return localpart, domname = split_mailbox(user.username) if user.role != 'SimpleUsers' and domname is None: diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 36246ba38..920b0cd10 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -9,9 +9,9 @@ from django.utils.translation import ugettext as _, ugettext_lazy from passwords.fields import PasswordField -from modoboa.core.models import User from modoboa.core import signals as core_signals -from modoboa.lib import events, parameters +from modoboa.core.models import User +from modoboa.lib import events from modoboa.lib.email_utils import split_mailbox from modoboa.lib import exceptions as lib_exceptions from modoboa.lib import fields as lib_fields @@ -21,6 +21,7 @@ from modoboa.lib.form_utils import ( from modoboa.lib.permissions import get_account_roles from modoboa.lib.validators import validate_utf8_email from modoboa.lib.web_utils import render_to_json_response +from modoboa.parameters import tools as param_tools from .. import models @@ -111,8 +112,8 @@ class AccountFormGeneral(forms.ModelForm): self.fields["role"].initial = self.instance.role condition = ( not self.instance.is_local and - parameters.get_admin( - "LDAP_AUTH_METHOD", app="core") == "directbind") + param_tools.get_global_parameter( + "ldap_auth_method", app="core") == "directbind") if condition: del self.fields["password1"] del self.fields["password2"] diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index f499dbc0e..a1c649526 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -5,15 +5,16 @@ from django.core.urlresolvers import reverse from django.http import QueryDict from django.utils.translation import ugettext as _, ugettext_lazy -from modoboa.core.models import User from modoboa.core import signals as core_signals -from modoboa.lib import events, parameters +from modoboa.core.models import User +from modoboa.lib import events from modoboa.lib.exceptions import Conflict from modoboa.lib.fields import DomainNameField from modoboa.lib.form_utils import ( YesNoField, WizardForm, WizardStep, DynamicForm, TabForms ) from modoboa.lib.web_utils import render_to_json_response +from modoboa.parameters import tools as param_tools from ..lib import check_if_domain_exists from ..models import ( @@ -75,11 +76,12 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): self._create_field(forms.CharField, name, dalias.name, 3) def clean_quota(self): - if self.cleaned_data['quota'] is None: - return int(parameters.get_admin('DEFAULT_DOMAIN_QUOTA')) - if self.cleaned_data['quota'] < 0: + """Check quota value.""" + if self.cleaned_data["quota"] is None: + return param_tools.get_global_parameter("default_domain_quota") + if self.cleaned_data["quota"] < 0: raise forms.ValidationError(_("Must be a positive integer")) - return self.cleaned_data['quota'] + return self.cleaned_data["quota"] def clean(self): """Custom fields validation. @@ -164,7 +166,7 @@ class DomainFormOptions(forms.Form): create_dom_admin = YesNoField( label=ugettext_lazy("Create a domain administrator"), - initial="no", + initial=False, help_text=ugettext_lazy( "Automatically create an administrator for this domain" ) @@ -182,7 +184,7 @@ class DomainFormOptions(forms.Form): create_aliases = YesNoField( label=ugettext_lazy("Create aliases"), - initial="yes", + initial=True, help_text=ugettext_lazy( "Automatically create standard aliases for this domain" ), @@ -191,25 +193,33 @@ class DomainFormOptions(forms.Form): def __init__(self, user, *args, **kwargs): super(DomainFormOptions, self).__init__(*args, **kwargs) - result = events.raiseQueryEvent('UserCanSetRole', user, 'DomainAdmins') + result = events.raiseQueryEvent("UserCanSetRole", user, "DomainAdmins") if False in result: self.fields = {} return - if args: - if args[0].get("create_dom_admin", "no") == "yes": - self.fields["dom_admin_username"].required = True - self.fields["create_aliases"].required = True def clean_dom_admin_username(self): """Ensure admin username is an email address.""" - if '@' in self.cleaned_data["dom_admin_username"]: + if "@" in self.cleaned_data["dom_admin_username"]: raise forms.ValidationError(_("Invalid format")) return self.cleaned_data["dom_admin_username"] + def clean(self): + """Check required values.""" + cleaned_data = super(DomainFormOptions, self).clean() + if cleaned_data.get("create_dom_admin"): + if not cleaned_data.get("dom_admin_username"): + self.add_error( + "dom_admin_username", _("This field is required.")) + if "create_aliases" not in cleaned_data: + self.add_error( + "create_aliases", _("This field is required.")) + return cleaned_data + def save(self, *args, **kwargs): if not self.fields: return - if self.cleaned_data["create_dom_admin"] == "no": + if not self.cleaned_data["create_dom_admin"]: return user = kwargs.pop("user") domain = kwargs.pop("domain") @@ -237,8 +247,7 @@ class DomainFormOptions(forms.Form): override_rules=user.has_perm("admin.change_domain")) mb.save(creator=user) - if domain.type == "domain" and \ - self.cleaned_data["create_aliases"] == "yes": + if domain.type == "domain" and self.cleaned_data["create_aliases"]: core_signals.can_create_object.send( self.__class__, context=user, object_type="mailbox_aliases") alias = Alias( diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index c2a8dee7b..c9bc1230d 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -1,3 +1,4 @@ +"""A management command to apply mailbox operations.""" import logging from optparse import make_option @@ -5,7 +6,7 @@ import os from django.core.management.base import BaseCommand -from modoboa.lib import parameters +from param_tools import tools as param_tools from modoboa.lib.sysutils import exec_cmd from modoboa.lib.exceptions import InternalError @@ -14,22 +15,26 @@ from ...models import MailboxOperation class OperationError(Exception): + """Custom exception.""" + pass class Command(BaseCommand): - help = 'Handles rename and delete operations on mailboxes' + """Command definition""" + + help = "Handles rename and delete operations on mailboxes" option_list = BaseCommand.option_list + ( make_option( - '--pidfile', default='/tmp/handle_mailbox_operations.pid', - help='Path to the file that will contain the PID of this process' + "--pidfile", default="/tmp/handle_mailbox_operations.pid", + help="Path to the file that will contain the PID of this process" ), ) def __init__(self, *args, **kwargs): super(Command, self).__init__(*args, **kwargs) - self.logger = logging.getLogger('modoboa.admin') + self.logger = logging.getLogger("modoboa.admin") def rename_mailbox(self, operation): if not os.path.exists(operation.argument): @@ -75,22 +80,23 @@ class Command(BaseCommand): return True def handle(self, *args, **options): + """Command entry point.""" load_admin_settings() - if parameters.get_admin("HANDLE_MAILBOXES") == 'no': + if not param_tools.get_global_parameter("handle_mailboxes"): return - if not self.check_pidfile(options['pidfile']): + if not self.check_pidfile(options["pidfile"]): return for ope in MailboxOperation.objects.all(): try: - f = getattr(self, '%s_mailbox' % ope.type) + f = getattr(self, "%s_mailbox" % ope.type) except AttributeError: continue try: f(ope) except (OperationError, InternalError) as e: - self.logger.critical('%s failed (reason: %s)', - ope, str(e).encode('utf-8')) + self.logger.critical("%s failed (reason: %s)", + ope, str(e).encode("utf-8")) else: - self.logger.info('%s succeed', ope) + self.logger.info("%s succeed", ope) ope.delete() - os.unlink(options['pidfile']) + os.unlink(options["pidfile"]) diff --git a/modoboa/admin/management/commands/subcommands/_mx.py b/modoboa/admin/management/commands/subcommands/_mx.py index 09fe918fe..13c8bfb77 100644 --- a/modoboa/admin/management/commands/subcommands/_mx.py +++ b/modoboa/admin/management/commands/subcommands/_mx.py @@ -17,8 +17,8 @@ from django.utils.translation import ugettext as _ from modoboa.admin import constants from modoboa.admin import models -from modoboa.lib import parameters from modoboa.lib import email_utils +from modoboa.parameters import tools as param_tools class CheckMXRecords(BaseCommand): @@ -36,12 +36,12 @@ class CheckMXRecords(BaseCommand): @cached_property def sender(self): """Return sender address for notifications.""" - return parameters.get_admin("SENDER_ADDRESS", app="core") + return param_tools.get_global_parameter("sender_address", app="core") @cached_property def valid_mxs(self): """Return valid MXs set in admin.""" - valid_mxs = parameters.get_admin("VALID_MXS", app="admin") + valid_mxs = param_tools.get_global_parameter("valid_mxs") return [ipaddress.ip_network(u"{}".format(v.strip())) for v in valid_mxs.split() if v.strip()] @@ -217,11 +217,11 @@ class CheckMXRecords(BaseCommand): """Check specified domain.""" mx_list = list(self.get_mx_records_for_domain(domain, ttl=ttl)) - if parameters.get_admin("ENABLE_MX_CHECKS") != "no": + if param_tools.get_global_parameter("enable_mx_checks"): self.check_valid_mx(domain, mx_list, **options) condition = ( - parameters.get_admin("ENABLE_DNSBL_CHECKS") == "no" or + not param_tools.get_global_parameter("enable_dnsbl_checks") or options["no_dnsbl"] is True) if condition or not mx_list: return diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index 12ea14b0b..476764e00 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -13,10 +13,11 @@ from django.contrib.contenttypes.fields import GenericRelation from reversion import revisions as reversion -from modoboa.core.models import User, ObjectAccess from modoboa.core import signals as core_signals -from modoboa.lib import events, parameters +from modoboa.core.models import User, ObjectAccess +from modoboa.lib import events from modoboa.lib.exceptions import BadRequest, Conflict +from modoboa.parameters import tools as param_tools from .base import AdminObject @@ -185,11 +186,11 @@ class Domain(AdminObject): if self.alias_set.count(): events.raiseEvent("MailboxAliasDeleted", self.alias_set.all()) ungrant_access_to_objects(self.alias_set.all()) - if parameters.get_admin("AUTO_ACCOUNT_REMOVAL") == "yes": + if param_tools.get_global_parameter("auto_account_removal"): for account in User.objects.filter(mailbox__domain=self): account.delete(fromuser, keepdir) elif self.mailbox_set.count(): - Quota.objects.filter(username__contains='@%s' % self.name).delete() + Quota.objects.filter(username__contains="@%s" % self.name).delete() events.raiseEvent("MailboxDeleted", self.mailbox_set.all()) ungrant_access_to_objects(self.mailbox_set.all()) super(Domain, self).delete() @@ -242,7 +243,7 @@ class MXQuerySet(models.QuerySet): def has_valids(self): """Return managed results.""" - if parameters.get_admin("VALID_MXS", app="admin").strip(): + if param_tools.get_global_parameter("valid_mxs").strip(): return self.filter(managed=True).exists() return self.exists() @@ -259,12 +260,12 @@ class MXRecord(models.Model): objects = models.Manager.from_queryset(MXQuerySet)() def is_managed(self): - if not parameters.get_admin("ENABLE_MX_CHECKS"): + if not param_tools.get_global_parameter("enable_mx_checks"): return False - return bool(parameters.get_admin("VALID_MXS", app="admin").strip()) + return bool(param_tools.get_global_parameter("valid_mxs").strip()) def __unicode__(self): - return u'{0.name} ({0.address}) for {0.domain} '.format(self) + return u"{0.name} ({0.address}) for {0.domain} ".format(self) class DNSBLQuerySet(models.QuerySet): diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index 90b6ba261..3e7c10f8f 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -12,13 +12,15 @@ from django.utils.translation import ugettext as _, ugettext_lazy from reversion import revisions as reversion -from .base import AdminObject -from .domain import Domain from modoboa.core.models import User -from modoboa.lib import events, parameters +from modoboa.lib import events from modoboa.lib import exceptions as lib_exceptions from modoboa.lib.email_utils import split_mailbox from modoboa.lib.sysutils import exec_cmd +from modoboa.parameters import tools as param_tools + +from .base import AdminObject +from .domain import Domain class Quota(models.Model): @@ -131,15 +133,15 @@ class Mailbox(AdminObject): several patterns to understand and we don't want to implement them. """ - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm is None or hm == "no": + admin_params = dict(param_tools.get_global_parameters("admin")) + if not admin_params.get("handle_mailboxes"): return None if self.__mail_home is None: curuser = pwd.getpwuid(os.getuid()).pw_name - mbowner = parameters.get_admin("MAILBOXES_OWNER") + mbowner = admin_params["mailboxes_owner"] options = {} if curuser != mbowner: - options['sudo_user'] = mbowner + options["sudo_user"] = mbowner code, output = exec_cmd( "doveadm user %s -f home" % self.full_address, **options ) @@ -180,8 +182,9 @@ class Mailbox(AdminObject): def rename_dir(self, old_mail_home): """Rename local directory if needed.""" - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm is None or hm == "no": + hm = param_tools.get_global_parameter( + "handle_mailboxes", raise_exception=False) + if not hm: return MailboxOperation.objects.create( mailbox=self, type="rename", argument=old_mail_home @@ -210,10 +213,11 @@ class Mailbox(AdminObject): self.rename_dir(old_mail_home) def delete_dir(self): - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm is None or hm == "no": + hm = param_tools.get_global_parameter( + "handle_mailboxes", raise_exception=False) + if not hm: return - MailboxOperation.objects.create(type='delete', argument=self.mail_home) + MailboxOperation.objects.create(type="delete", argument=self.mail_home) def set_quota(self, value=None, override_rules=False): """Set or update quota's value for this mailbox. diff --git a/modoboa/admin/static/admin/js/admin.js b/modoboa/admin/static/admin/js/admin.js index 191860f40..0c31a4380 100644 --- a/modoboa/admin/static/admin/js/admin.js +++ b/modoboa/admin/static/admin/js/admin.js @@ -146,7 +146,7 @@ Domains.prototype = { * @param {Object} data - options */ init_domain_links: function(data) { - var deloptions = (data.handle_mailboxes === "yes") ? + var deloptions = (data.handle_mailboxes) ? {keepdir: gettext("Do not delete domain directory")} : {}; var warnmsg = (data.auto_account_removal && data.auto_account_removal === "yes") ? gettext("This operation will remove ALL data associated to this domain.") @@ -329,7 +329,7 @@ Identities.prototype = { init_identity_links: function(data) { var deloptions = {}; - if (data.handle_mailboxes === "yes") { + if (data.handle_mailboxes) { deloptions = {keepdir: gettext("Do not delete mailbox directory")}; } diff --git a/modoboa/admin/templates/admin/domain_detail.html b/modoboa/admin/templates/admin/domain_detail.html index 578f48fd9..d21bd6cfd 100644 --- a/modoboa/admin/templates/admin/domain_detail.html +++ b/modoboa/admin/templates/admin/domain_detail.html @@ -21,16 +21,16 @@

{% trans "Summary" %}

- {% if enable_mx_checks == "yes" or enable_dnsbl_checks == "yes" %} + {% if enable_mx_checks or enable_dnsbl_checks %}
{% trans "DNS status" %} {% if domain.awaiting_checks %} {% trans "Awaiting checks" %} {% else %} - {% if enable_mx_checks == "yes" %} + {% if enable_mx_checks %} MX {% endif %} - {% if enable_dnsbl_checks == "yes" %} + {% if enable_dnsbl_checks %} DNSBL {% endif %} {% endif %} diff --git a/modoboa/admin/templates/admin/domains.html b/modoboa/admin/templates/admin/domains.html index 08440de2f..f9e0a6b4e 100644 --- a/modoboa/admin/templates/admin/domains.html +++ b/modoboa/admin/templates/admin/domains.html @@ -34,7 +34,7 @@ $(document).ready(function() { {% trans "Name" %} {% trans "Alias(es)" %} {% trans "Tags" %} - {% if enable_mx_checks == "yes" or enable_dnsbl_checks == "yes" %} + {% if enable_mx_checks or enable_dnsbl_checks %} {% trans "DNS status" %} {% endif %} {% trans "Actions" %} diff --git a/modoboa/admin/templates/admin/domains_table.html b/modoboa/admin/templates/admin/domains_table.html index 5278c1829..cc1940d1e 100644 --- a/modoboa/admin/templates/admin/domains_table.html +++ b/modoboa/admin/templates/admin/domains_table.html @@ -11,10 +11,10 @@ {% if domain.awaiting_checks %} {% trans "Awaiting checks" %} {% else %} - {% if enable_mx_checks == "yes" %} + {% if enable_mx_checks %} MX {% endif %} - {% if enable_dnsbl_checks == "yes" %} + {% if enable_dnsbl_checks %} DNSBL {% endif %} {% endif %} diff --git a/modoboa/admin/tests/test_account.py b/modoboa/admin/tests/test_account.py index e44914df6..6266efcb1 100644 --- a/modoboa/admin/tests/test_account.py +++ b/modoboa/admin/tests/test_account.py @@ -8,9 +8,9 @@ from django.test import override_settings from modoboa.core import factories as core_factories from modoboa.core.tests import test_core from modoboa.core.models import User -from modoboa.lib import parameters from modoboa.lib.tests import ModoTestCase from modoboa.lib.tests import NO_LDAP +from modoboa.limits import utils as limits_utils from .. import factories from .. import models @@ -201,8 +201,7 @@ class AccountTestCase(ModoTestCase): def test_account_detail_view(self): """Test account detail view.""" - parameters.save_admin( - "ENABLE_ADMIN_LIMITS", "no", app="limits") + self.set_global_parameter("enable_admin_limits", False, app="limits") account = User.objects.get(username="admin@test.com") url = reverse("admin:account_detail", args=[account.pk]) response = self.client.get(url) @@ -210,8 +209,7 @@ class AccountTestCase(ModoTestCase): self.assertIn("Summary", response.content) self.assertIn("Administered domains", response.content) self.assertNotIn("Resources usage", response.content) - parameters.save_admin( - "ENABLE_ADMIN_LIMITS", "yes", app="limits") + self.set_global_parameter("enable_admin_limits", True, app="limits") response = self.client.get(url) self.assertIn("Resources usage", response.content) @@ -228,7 +226,7 @@ class LDAPAccountTestCase(test_core.LDAPTestCaseMixin, ModoTestCase): """Check if objects are not created as expected.""" self.activate_ldap_authentication() self.searchbind_mode() - parameters.save_admin("AUTO_CREATE_DOMAIN_AND_MAILBOX", "no") + self.set_global_parameter("auto_create_domain_and_mailbox", False) username = "testuser@example.com" self.authenticate(username, "test") self.assertFalse( @@ -242,15 +240,12 @@ class PermissionsTestCase(ModoTestCase): @classmethod def setUpTestData(cls): """Create test data.""" - from modoboa.lib import parameters - from modoboa.limits import utils as limits_utils - super(PermissionsTestCase, cls).setUpTestData() + parameters = {} for name, tpl in limits_utils.get_user_limit_templates(): - parameters.save_admin( - "DEFLT_USER_{0}_LIMIT".format(name.upper()), 2, - app="limits" - ) + parameters["deflt_user_{0}_limit".format(name)] = 2 + cls.localconfig.parameters.set_values(parameters, app="limits") + cls.localconfig.save() factories.populate_database() cls.reseller = core_factories.UserFactory(username="reseller") cls.reseller.role = "Resellers" diff --git a/modoboa/admin/tests/test_api.py b/modoboa/admin/tests/test_api.py index 157b92e2d..c6d09f489 100644 --- a/modoboa/admin/tests/test_api.py +++ b/modoboa/admin/tests/test_api.py @@ -10,7 +10,6 @@ from rest_framework.authtoken.models import Token from modoboa.admin import models as admin_models from modoboa.core import models as core_models -from modoboa.lib import parameters from modoboa.lib.tests import ModoAPITestCase from .. import factories @@ -196,8 +195,10 @@ class AccountAPITestCase(ModoAPITestCase): def setUp(self): """Test setup.""" super(AccountAPITestCase, self).setUp() - parameters.save_admin("ENABLE_ADMIN_LIMITS", "no", app="limits") - parameters.save_admin("ENABLE_DOMAIN_LIMITS", "no", app="limits") + self.set_global_parameters({ + "enable_admin_limits": False, + "enable_domain_limits": False + }, app="limits") def test_get_accounts(self): """Retrieve a list of accounts.""" @@ -461,7 +462,9 @@ class AliasAPITestCase(ModoAPITestCase): def setUpTestData(cls): """Create test data.""" super(AliasAPITestCase, cls).setUpTestData() - parameters.save_admin("ENABLE_ADMIN_LIMITS", "no", app="limits") + cls.localconfig.parameters.set_value( + "enable_admin_limits", False, app="limits") + cls.localconfig.save() factories.populate_database() cls.da_token = Token.objects.create( user=core_models.User.objects.get(username="admin@test.com")) diff --git a/modoboa/admin/tests/test_domain.py b/modoboa/admin/tests/test_domain.py index 39577811f..bdf153d78 100644 --- a/modoboa/admin/tests/test_domain.py +++ b/modoboa/admin/tests/test_domain.py @@ -7,7 +7,6 @@ import json from django.core.urlresolvers import reverse from modoboa.core.models import User -from modoboa.lib import parameters from modoboa.lib.tests import ModoTestCase from .. import factories @@ -15,7 +14,6 @@ from ..models import Domain, Alias class DomainTestCase(ModoTestCase): - """Test case for Domain.""" @classmethod @@ -27,7 +25,7 @@ class DomainTestCase(ModoTestCase): def test_create(self): """Test the creation of a domain.""" values = { - "name": "pouet.com", "quota": 100, "create_dom_admin": "no", + "name": "pouet.com", "quota": 100, "create_dom_admin": False, "type": "domain", "stepid": "step3" } self.ajax_post(reverse("admin:domain_add"), values) @@ -40,7 +38,7 @@ class DomainTestCase(ModoTestCase): def test_create_utf8(self): """Test the creation of a domain with non-ASCII characters.""" values = { - "name": "pouét.com", "quota": 100, "create_dom_admin": "no", + "name": "pouét.com", "quota": 100, "create_dom_admin": False, "type": "domain", "stepid": "step3" } self.ajax_post(reverse("admin:domain_add"), values) @@ -51,12 +49,10 @@ class DomainTestCase(ModoTestCase): self.assertFalse(dom.admins) def test_create_with_template(self): - """Test the creation of a domain with a template - - """ + """Test the creation of a domain with a template.""" values = { - "name": "pouet.com", "quota": 100, "create_dom_admin": "yes", - "dom_admin_username": "toto", "create_aliases": "yes", + "name": "pouet.com", "quota": 100, "create_dom_admin": True, + "dom_admin_username": "toto", "create_aliases": True, "type": "domain", "stepid": 'step3' } self.ajax_post( @@ -76,8 +72,8 @@ class DomainTestCase(ModoTestCase): def test_create_with_template_and_empty_quota(self): """Test the creation of a domain with a template and no quota""" values = { - "name": "pouet.com", "quota": 0, "create_dom_admin": "yes", - "dom_admin_username": "toto", "create_aliases": "yes", + "name": "pouet.com", "quota": 0, "create_dom_admin": True, + "dom_admin_username": "toto", "create_aliases": True, "type": "domain", "stepid": 'step3' } self.ajax_post( @@ -95,10 +91,10 @@ class DomainTestCase(ModoTestCase): self.assertTrue(da.can_access(al)) def test_create_using_default_quota(self): - parameters.save_admin('DEFAULT_DOMAIN_QUOTA', 50, app='admin') + self.set_global_parameter("default_domain_quota", 50) values = { - "name": "pouet.com", "create_dom_admin": "yes", - "dom_admin_username": "toto", "create_aliases": "yes", + "name": "pouet.com", "create_dom_admin": True, + "dom_admin_username": "toto", "create_aliases": True, "type": "domain", "stepid": 'step3' } self.ajax_post( @@ -149,8 +145,7 @@ class DomainTestCase(ModoTestCase): def test_domain_detail_view(self): """Test Domain detail view.""" - parameters.save_admin( - "ENABLE_DOMAIN_LIMITS", "no", app="limits") + self.set_global_parameter("enable_domain_limits", False, app="limits") domain = Domain.objects.get(name="test.com") url = reverse("admin:domain_detail", args=[domain.pk]) response = self.client.get(url) @@ -158,8 +153,7 @@ class DomainTestCase(ModoTestCase): self.assertIn("Summary", response.content) self.assertIn("Administrators", response.content) self.assertNotIn("Resources usage", response.content) - parameters.save_admin( - "ENABLE_DOMAIN_LIMITS", "yes", app="limits") + self.set_global_parameter("enable_domain_limits", True, app="limits") response = self.client.get(url) self.assertIn("Resources usage", response.content) diff --git a/modoboa/admin/tests/test_import_.py b/modoboa/admin/tests/test_import_.py index c9f7f4027..4fe2adf88 100644 --- a/modoboa/admin/tests/test_import_.py +++ b/modoboa/admin/tests/test_import_.py @@ -3,7 +3,6 @@ from django.core.files.base import ContentFile from django.core.urlresolvers import reverse from modoboa.core.models import User -from modoboa.lib import parameters from modoboa.lib.tests import ModoTestCase from .. import factories @@ -16,7 +15,9 @@ class ImportTestCase(ModoTestCase): def setUpTestData(cls): """Create test data.""" super(ImportTestCase, cls).setUpTestData() - parameters.save_admin("ENABLE_ADMIN_LIMITS", "no", app="limits") + cls.localconfig.parameters.set_value( + "enable_admin_limits", False, app="limits") + cls.localconfig.save() factories.populate_database() def test_domains_import(self): diff --git a/modoboa/admin/tests/test_mx.py b/modoboa/admin/tests/test_mx.py index 427123802..8513bcb4a 100644 --- a/modoboa/admin/tests/test_mx.py +++ b/modoboa/admin/tests/test_mx.py @@ -5,7 +5,6 @@ from django.core.urlresolvers import reverse from django.test import override_settings from modoboa.lib.tests import ModoTestCase -from modoboa.lib import parameters from .. import factories from .. import models @@ -21,7 +20,8 @@ class MXTestCase(ModoTestCase): cls.domain = factories.DomainFactory(name="modoboa.org") # should not exist cls.bad_domain = factories.DomainFactory(name="pouet.com") - parameters.save_admin("VALID_MXS", "127.0.0.1") + cls.localconfig.parameters.set_value("valid_mxs", "127.0.0.1") + cls.localconfig.save() models.MXRecord.objects.all().delete() def test_management_command(self): diff --git a/modoboa/admin/tests/test_password_schemes.py b/modoboa/admin/tests/test_password_schemes.py index f19ba7bf3..113c5d90b 100644 --- a/modoboa/admin/tests/test_password_schemes.py +++ b/modoboa/admin/tests/test_password_schemes.py @@ -2,7 +2,6 @@ from django.core.urlresolvers import reverse from modoboa.core.models import User from modoboa.lib.tests import ModoTestCase -from modoboa.lib import parameters from .. import factories @@ -28,7 +27,7 @@ class PasswordSchemesTestCase(ModoTestCase): ) def _test_scheme(self, name, startpattern): - parameters.save_admin('PASSWORD_SCHEME', name, app='core') + self.set_global_parameter("password_scheme", name, app="core") self._create_account() account = User.objects.get(username='tester@test.com') self.assertTrue(account.password.startswith(startpattern)) diff --git a/modoboa/admin/views/domain.py b/modoboa/admin/views/domain.py index bd1a7a4df..3c2b2dea5 100644 --- a/modoboa/admin/views/domain.py +++ b/modoboa/admin/views/domain.py @@ -16,7 +16,7 @@ from reversion import revisions as reversion from modoboa.core.models import User from modoboa.core import signals as core_signals -from modoboa.lib import parameters, events +from modoboa.lib import events from modoboa.lib.web_utils import ( _render_to_string, render_to_json_response ) @@ -61,20 +61,22 @@ def _domains(request): domainlist = sorted(domainlist, key=lambda d: d.tags[0], reverse=sort_dir == '-') context = { - "handle_mailboxes": parameters.get_admin( - "HANDLE_MAILBOXES", raise_error=False), - "auto_account_removal": parameters.get_admin("AUTO_ACCOUNT_REMOVAL"), + "handle_mailboxes": request.localconfig.parameters.get_value( + "handle_mailboxes", raise_exception=False), + "auto_account_removal": request.localconfig.parameters.get_value( + "auto_account_removal"), } page = get_listing_page(domainlist, request.GET.get("page", 1)) if page is None: context["length"] = 0 else: + parameters = request.localconfig.parameters context["rows"] = _render_to_string( request, "admin/domains_table.html", { "domains": page.object_list, - "enable_mx_checks": parameters.get_admin("ENABLE_MX_CHECKS"), + "enable_mx_checks": parameters.get_value("enable_mx_checks"), "enable_dnsbl_checks": ( - parameters.get_admin("ENABLE_DNSBL_CHECKS")), + parameters.get_value("enable_dnsbl_checks")) } ) context["pages"] = [page.number] @@ -90,10 +92,11 @@ def domains(request, tplname="admin/domains.html"): reverse("admin:identity_list") ) return HttpResponseRedirect(reverse("core:user_index")) + parameters = request.localconfig.parameters return render(request, tplname, { "selection": "domains", - "enable_mx_checks": parameters.get_admin("ENABLE_MX_CHECKS"), - "enable_dnsbl_checks": parameters.get_admin("ENABLE_DNSBL_CHECKS") + "enable_mx_checks": parameters.get_value("enable_mx_checks"), + "enable_dnsbl_checks": parameters.get_value("enable_dnsbl_checks") }) @@ -189,10 +192,11 @@ class DomainDetailView( context = super(DomainDetailView, self).get_context_data(**kwargs) result = signals.extra_domain_dashboard_widgets.send( self.__class__, user=self.request.user, domain=self.object) + parameters = self.request.localconfig.parameters context.update({ "templates": {"left": [], "right": []}, - "enable_mx_checks": parameters.get_admin("ENABLE_MX_CHECKS"), - "enable_dnsbl_checks": parameters.get_admin("ENABLE_DNSBL_CHECKS"), + "enable_mx_checks": parameters.get_value("enable_mx_checks"), + "enable_dnsbl_checks": parameters.get_value("enable_dnsbl_checks"), }) for receiver, widgets in result: for widget in widgets: diff --git a/modoboa/admin/views/identity.py b/modoboa/admin/views/identity.py index 23035c087..466ced291 100644 --- a/modoboa/admin/views/identity.py +++ b/modoboa/admin/views/identity.py @@ -13,7 +13,7 @@ from django.contrib.auth.decorators import ( from reversion import revisions as reversion from modoboa.core.models import User -from modoboa.lib import parameters, events +from modoboa.lib import events from modoboa.lib.exceptions import ( PermDeniedException, BadRequest ) @@ -49,8 +49,8 @@ def _identities(request): objects = sorted(idents_list, key=lambda o: o.tags[0], reverse=sort_dir == '-') context = { - "handle_mailboxes": parameters.get_admin( - "HANDLE_MAILBOXES", raise_error=False) + "handle_mailboxes": request.localconfig.parameters.get_value( + "handle_mailboxes", raise_exception=False) } page = get_listing_page(objects, request.GET.get("page", 1)) if page is None: diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 28d1f1fe5..471b36478 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -3,25 +3,19 @@ from django import forms from django.conf import settings from django.utils.translation import ugettext as _, ugettext_lazy -from modoboa.lib import parameters from modoboa.lib import fields as lib_fields from modoboa.lib.cryptutils import random_key from modoboa.lib.form_utils import ( YesNoField, SeparatorField, InlineRadioSelect ) +from modoboa.parameters import forms as param_forms +from modoboa.parameters import tools as param_tools from . import constants def enabled_applications(): - """Return the list of installed extensions. - - We check if the table exists before trying to fetch activated - extensions because the admin module is always imported by Django, - even before the database exists (example: the first ``syncdb``). - - :return: a list - """ + """Return the list of installed extensions.""" from modoboa.core.extensions import exts_pool result = [("user", "user")] @@ -30,8 +24,7 @@ def enabled_applications(): return sorted(result, key=lambda e: e[0]) -class GeneralParametersForm(parameters.AdminParametersForm): - +class GeneralParametersForm(param_forms.AdminParametersForm): """General parameters.""" app = "core" @@ -99,7 +92,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): ldap_secured = YesNoField( label=ugettext_lazy("Use a secured connection"), - initial="no", + initial=False, help_text=ugettext_lazy( "Use an SSL/TLS connection to access the LDAP server") ) @@ -177,7 +170,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): ldap_is_active_directory = YesNoField( label=ugettext_lazy("Active Directory"), - initial="no", + initial=False, help_text=ugettext_lazy( "Tell if the LDAP server is an Active Directory one") ) @@ -224,21 +217,21 @@ class GeneralParametersForm(parameters.AdminParametersForm): enable_api_communication = YesNoField( label=ugettext_lazy("Enable communication"), - initial="yes", + initial=True, help_text=ugettext_lazy( "Enable communication with Modoboa public API") ) check_new_versions = YesNoField( label=ugettext_lazy("Check new versions"), - initial="yes", + initial=True, help_text=ugettext_lazy( "Automatically checks if a newer version is available") ) send_statistics = YesNoField( label=ugettext_lazy("Send statistics"), - initial="yes", + initial=True, help_text=ugettext_lazy( "Send statistics to Modoboa public API " "(counters and used extensions)") @@ -296,8 +289,8 @@ class GeneralParametersForm(parameters.AdminParametersForm): "ldap_admin_groups": "authentication_type=ldap", "ldap_group_type": "authentication_type=ldap", "ldap_groups_search_base": "authentication_type=ldap", - "check_new_versions": "enable_api_communication=yes", - "send_statistics": "enable_api_communication=yes", + "check_new_versions": "enable_api_communication=True", + "send_statistics": "enable_api_communication=True", } def __init__(self, *args, **kwargs): @@ -348,7 +341,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): return cleaned_data def to_django_settings(self): - """Apply LDAP related parameters to Django settings + """Apply LDAP related parameters to Django settings. Doing so, we can use the django_auth_ldap module. """ @@ -360,7 +353,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): except ImportError: ldap_available = False - values = self.get_current_values() + values = dict(param_tools.get_global_parameters("core")) if not ldap_available or values["authentication_type"] != "ldap": return if not hasattr(settings, "AUTH_LDAP_USER_ATTR_MAP"): @@ -369,7 +362,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): "email": "mail", "last_name": "sn" }) - ldap_uri = 'ldaps://' if values["ldap_secured"] == "yes" else "ldap://" + ldap_uri = "ldaps://" if values["ldap_secured"] else "ldap://" ldap_uri += "%s:%s" % ( values["ldap_server_address"], values["ldap_server_port"]) setattr(settings, "AUTH_LDAP_SERVER_URI", ldap_uri) @@ -400,7 +393,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): settings, "AUTH_LDAP_USER_DN_TEMPLATE", values["ldap_user_dn_template"] ) - if values["ldap_is_active_directory"] == "yes": + if values["ldap_is_active_directory"]: if not hasattr(settings, "AUTH_LDAP_GLOBAL_OPTIONS"): setattr(settings, "AUTH_LDAP_GLOBAL_OPTIONS", { ldap.OPT_REFERRALS: False diff --git a/modoboa/core/apps.py b/modoboa/core/apps.py index 1b8e3cf17..df124bd32 100644 --- a/modoboa/core/apps.py +++ b/modoboa/core/apps.py @@ -2,7 +2,7 @@ from django.apps import AppConfig from django.db.models import signals - +from django.utils.translation import ugettext_lazy BASE_EVENTS = [ "AccountCreated", @@ -36,7 +36,6 @@ BASE_EVENTS = [ "ExtraUprefsRoutes", "ExtraUprefsJS", - "GetExtraParameters", "ExtraFormFields", "SaveExtraFormFields", ] @@ -48,11 +47,12 @@ def load_core_settings(): This function must be manually called (see :file:`urls.py`) in order to load base settings. """ - from django.utils.translation import ugettext_lazy - from modoboa.lib import parameters, events + from modoboa.lib import events + from modoboa.parameters import tools as param_tools from .app_settings import GeneralParametersForm - parameters.register(GeneralParametersForm, ugettext_lazy("General")) + param_tools.registry.add( + "global", GeneralParametersForm, ugettext_lazy("General")) events.declare(BASE_EVENTS) diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index 5eb1d00e0..1ed22bc62 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -63,6 +63,7 @@ MODOBOA_APPS = ( 'modoboa.admin', 'modoboa.relaydomains', 'modoboa.limits', + 'modoboa.parameters', # Modoboa extensions here. {% for extension in extensions %} '{{ extension }}', {% endfor %} @@ -81,6 +82,7 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'modoboa.core.middleware.LocalConfigMiddleware', 'modoboa.lib.middleware.AjaxLoginRedirect', 'modoboa.lib.middleware.CommonExceptionCatcher', 'modoboa.lib.middleware.RequestCatcherMiddleware', diff --git a/modoboa/core/context_processors.py b/modoboa/core/context_processors.py index 94f24385a..69e3a338a 100644 --- a/modoboa/core/context_processors.py +++ b/modoboa/core/context_processors.py @@ -1,18 +1,16 @@ -""" -Custom context processors. -""" -from modoboa.lib import events, parameters +"""Custom context processors.""" + +from modoboa.lib import events def top_notifications(request): - """ - A context processor to include top notifications. - """ + """A context processor to include top notifications.""" if request.user.is_anonymous(): return {} + interval = request.localconfig.parameters.get_value( + "top_notifications_check_interval") return { - "notifications_check_interval": - int(parameters.get_admin("TOP_NOTIFICATIONS_CHECK_INTERVAL")) * 1000, + "notifications_check_interval": interval * 1000, "top_notifications": events.raiseQueryEvent( "TopNotifications", request, False) } diff --git a/modoboa/core/forms.py b/modoboa/core/forms.py index 45bdb5f55..09ccd9ee5 100644 --- a/modoboa/core/forms.py +++ b/modoboa/core/forms.py @@ -8,11 +8,10 @@ from django.utils.translation import ugettext as _, ugettext_lazy from passwords.fields import PasswordField from modoboa.core.models import User -from modoboa.lib import parameters +from modoboa.parameters import tools as param_tools class LoginForm(forms.Form): - """User login form.""" username = forms.CharField( @@ -30,7 +29,6 @@ class LoginForm(forms.Form): class ProfileForm(forms.ModelForm): - """Form to update User profile.""" oldpassword = forms.CharField( @@ -51,8 +49,8 @@ class ProfileForm(forms.ModelForm): fields = ("first_name", "last_name", "language", "phone_number", "secondary_email") widgets = { - 'first_name': forms.TextInput(attrs={'class': 'form-control'}), - 'last_name': forms.TextInput(attrs={'class': 'form-control'}) + "first_name": forms.TextInput(attrs={"class": "form-control"}), + "last_name": forms.TextInput(attrs={"class": "form-control"}) } def __init__(self, update_password, *args, **kwargs): @@ -66,7 +64,7 @@ class ProfileForm(forms.ModelForm): if self.cleaned_data["oldpassword"] == "": return self.cleaned_data["oldpassword"] - if parameters.get_admin("AUTHENTICATION_TYPE") != "local": + if param_tools.get_global_parameter("authentication_type") != "local": return self.cleaned_data["oldpassword"] if not self.instance.check_password(self.cleaned_data["oldpassword"]): @@ -93,7 +91,6 @@ class ProfileForm(forms.ModelForm): class APIAccessForm(forms.Form): - """Form to control API access.""" enable_api_access = forms.BooleanField( diff --git a/modoboa/core/management/commands/cleanlogs.py b/modoboa/core/management/commands/cleanlogs.py index 5cb7147a7..033d51957 100644 --- a/modoboa/core/management/commands/cleanlogs.py +++ b/modoboa/core/management/commands/cleanlogs.py @@ -7,7 +7,8 @@ from django.core.management.base import BaseCommand from django.utils import timezone from modoboa.core.models import Log -from modoboa.lib import parameters + +from ... import parameters class Command(BaseCommand): @@ -38,7 +39,7 @@ class Command(BaseCommand): l.addHandler(logging.StreamHandler()) self.verbose = options["verbose"] - log_maximum_age = int(parameters.get_admin("LOG_MAXIMUM_AGE")) + log_maximum_age = parameters.get_global_parameter("log_maximum_age") self.__vprint("Deleting logs older than %d days..." % log_maximum_age) limit = timezone.now() - datetime.timedelta(log_maximum_age) Log.objects.filter(date_created__lt=limit).delete() diff --git a/modoboa/core/management/commands/communicate_with_public_api.py b/modoboa/core/management/commands/communicate_with_public_api.py index e06a45956..dcd003a54 100644 --- a/modoboa/core/management/commands/communicate_with_public_api.py +++ b/modoboa/core/management/commands/communicate_with_public_api.py @@ -4,7 +4,7 @@ from django.core.management.base import BaseCommand, CommandError from modoboa.admin import models as admin_models from modoboa.core.extensions import exts_pool -from modoboa.lib import api_client, parameters +from modoboa.lib import api_client from ... import models @@ -16,18 +16,18 @@ class Command(BaseCommand): def handle(self, *args, **options): """Command entry point.""" - if parameters.get_admin("ENABLE_API_COMMUNICATION") == "no": - return - self.client = api_client.ModoAPIClient() local_config = ( models.LocalConfig.objects.select_related("site").first()) + if not local_config.parameters.get_value("enable_api_communication"): + return + self.client = api_client.ModoAPIClient() if not local_config.api_pk: pk = self.client.register_instance(local_config.site.domain) if pk is None: raise CommandError("Instance registration failed.") local_config.api_pk = pk - if parameters.get_admin("CHECK_NEW_VERSIONS") == "yes": + if local_config.parameters.get_value("check_new_versions"): versions = self.client.versions() if versions is None: raise CommandError("Failed to retrieve versions from the API.") @@ -35,7 +35,7 @@ class Command(BaseCommand): local_config.save() - if parameters.get_admin("SEND_STATISTICS") == "no": + if not local_config.parameters.get_value("send_statistics"): return extensions = [ext["name"] for ext in exts_pool.list_all()] data = { diff --git a/modoboa/core/management/commands/load_initial_data.py b/modoboa/core/management/commands/load_initial_data.py index bdda736c3..09dbcd7bb 100644 --- a/modoboa/core/management/commands/load_initial_data.py +++ b/modoboa/core/management/commands/load_initial_data.py @@ -10,13 +10,13 @@ from django.core.management.base import BaseCommand from modoboa.core import PERMISSIONS from modoboa.core.extensions import exts_pool -from modoboa.core.models import User, ObjectAccess from modoboa.lib.cryptutils import random_key from modoboa.lib import events -from modoboa.lib import models as lib_models from modoboa.lib.permissions import add_permissions_to_group import modoboa.relaydomains.models as relay_models +from ... import models + class Command(BaseCommand): @@ -37,25 +37,26 @@ class Command(BaseCommand): def handle(self, *args, **options): """Command entry point.""" - if not User.objects.filter(is_superuser=True).count(): - admin = User(username=options["admin_username"], is_superuser=True) + if not models.User.objects.filter(is_superuser=True).count(): + admin = models.User( + username=options["admin_username"], is_superuser=True) admin.set_password("password") admin.save() - ObjectAccess.objects.create( + models.ObjectAccess.objects.create( user=admin, content_object=admin, is_owner=True) - param_name = "core.SECRET_KEY" - qset = lib_models.Parameter.objects.filter(name=param_name) - if not qset.exists(): - lib_models.Parameter.objects.create( - name=param_name, value=random_key()) + lc = models.LocalConfig.objects.first() + secret_key = lc.parameters.get_value("secret_key") + if not secret_key: + lc.parameters.set_value("secret_key", random_key()) + lc.save() - for service_name in ['relay', 'smtp']: + for service_name in ["relay", "smtp"]: relay_models.Service.objects.get_or_create(name=service_name) exts_pool.load_all() - superadmin = User.objects.filter(is_superuser=True).first() + superadmin = models.User.objects.filter(is_superuser=True).first() groups = PERMISSIONS.keys() + [ role[0] for role in events.raiseQueryEvent("GetExtraRoles", superadmin, None) @@ -75,6 +76,6 @@ class Command(BaseCommand): extension.load_initial_data() events.raiseEvent("InitialDataLoaded", extname) - if options['extra_fixtures']: + if options["extra_fixtures"]: from modoboa.admin import factories factories.populate_database() diff --git a/modoboa/core/middleware.py b/modoboa/core/middleware.py new file mode 100644 index 000000000..c1463be56 --- /dev/null +++ b/modoboa/core/middleware.py @@ -0,0 +1,11 @@ +"""Core middlewares.""" + +from . import models + + +class LocalConfigMiddleware(object): + """A middleware to inject LocalConfig into request.""" + + def process_request(self, request): + """Inject LocalConfig instance to request.""" + request.localconfig = models.LocalConfig.objects.first() diff --git a/modoboa/core/migrations/0010_auto_20161026_1011.py b/modoboa/core/migrations/0010_auto_20161026_1011.py new file mode 100644 index 000000000..64961f261 --- /dev/null +++ b/modoboa/core/migrations/0010_auto_20161026_1011.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2016-10-26 08:11 +from __future__ import unicode_literals + +from django.db import migrations +import jsonfield.fields + + +def set_parameter(store, parameter): + """Add parameter to the specified store.""" + app, name = parameter.name.split(".") + if app not in store: + store[app] = {} + # decode value + value = parameter.value.decode("unicode_escape") + if value == "yes": + value = True + elif value == "no": + value = False + elif value.isdigit(): + value = int(value) + store[app][name.lower()] = value + + +def move_parameters(apps, schema_editor): + """Move global and user parameters.""" + Parameter = apps.get_model("lib", "Parameter") + LocalConfig = apps.get_model("core", "LocalConfig") + parameters = {} + for parameter in Parameter.objects.all(): + set_parameter(parameters, parameter) + LocalConfig.objects.all().update(_parameters=parameters) + + User = apps.get_model("core", "User") + for user in User.objects.prefetch_related("userparameter_set"): + for parameter in user.userparameter_set.all(): + set_parameter(user._parameters, parameter) + user.save() + + +def clear_parameters(apps, schema_editor): + """Reverse operation.""" + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_auto_20161026_1003'), + ('lib', '0005_auto_20160416_1449'), + ] + + operations = [ + migrations.AddField( + model_name='localconfig', + name='_parameters', + field=jsonfield.fields.JSONField(default={}), + preserve_default=False, + ), + migrations.AddField( + model_name='user', + name='_parameters', + field=jsonfield.fields.JSONField(default={}), + preserve_default=False, + ), + migrations.RunPython(move_parameters, clear_parameters) + ] diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 9b24424b0..0fbda4dff 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -20,10 +20,11 @@ import jsonfield from reversion import revisions as reversion from modoboa.core.password_hashers import get_password_hasher -from modoboa.lib import events, parameters +from modoboa.lib import events from modoboa.lib.exceptions import ( PermDeniedException, InternalError, BadRequest, Conflict ) +from modoboa.parameters import tools as param_tools from . import constants @@ -67,7 +68,8 @@ class User(PermissionsMixin): ugettext_lazy('last login'), blank=True, null=True ) - language = models.CharField(ugettext_lazy("language"), + language = models.CharField( + ugettext_lazy("language"), max_length=10, default="en", choices=constants.LANGUAGES, help_text=ugettext_lazy( "Prefered language to display pages." @@ -81,6 +83,7 @@ class User(PermissionsMixin): help_text=ugettext_lazy( "An alternative e-mail address, can be used for recovery needs.") ) + _parameters = jsonfield.JSONField(default={}) objects = UserManager() @@ -95,6 +98,11 @@ class User(PermissionsMixin): password_expr = re.compile(r'\{([\w\-]+)\}(.+)') + def __init__(self, *args, **kwargs): + """Load parameter manager.""" + super(User, self).__init__(*args, **kwargs) + self.parameters = param_tools.Manager("user", self._parameters) + def delete(self, fromuser, *args, **kwargs): """Custom delete method @@ -138,13 +146,13 @@ class User(PermissionsMixin): one more time. """ - try: - scheme = parameters.get_admin("PASSWORD_SCHEME") - except parameters.NotDefined: + scheme = param_tools.get_global_parameter( + "password_scheme", raise_exception=False) + if scheme is None: from modoboa.core.apps import load_core_settings load_core_settings() - scheme = parameters.get_admin("PASSWORD_SCHEME") - + scheme = param_tools.get_global_parameter( + "password_scheme", raise_exception=False) if isinstance(raw_value, unicode): raw_value = raw_value.encode("utf-8") return get_password_hasher(scheme.upper())().encrypt(raw_value) @@ -505,3 +513,10 @@ class LocalConfig(models.Model): # API results cache api_versions = jsonfield.JSONField() + + _parameters = jsonfield.JSONField(default={}) + + def __init__(self, *args, **kwargs): + """Load parameter manager.""" + super(LocalConfig, self).__init__(*args, **kwargs) + self.parameters = param_tools.Manager("global", self._parameters) diff --git a/modoboa/core/password_hashers/advanced.py b/modoboa/core/password_hashers/advanced.py index e4c4c54b9..c14245e29 100644 --- a/modoboa/core/password_hashers/advanced.py +++ b/modoboa/core/password_hashers/advanced.py @@ -3,9 +3,12 @@ Advanced (ie. stronger) password hashers. This module relies on `passlib` to provide more secure hashers. """ + from passlib.hash import bcrypt, md5_crypt, sha256_crypt, sha512_crypt -from modoboa.core.password_hashers.base import PasswordHasher -from modoboa.lib import parameters + +from modoboa.parameters import tools as param_tools + +from .base import PasswordHasher class BLFCRYPTHasher(PasswordHasher): @@ -26,7 +29,7 @@ class BLFCRYPTHasher(PasswordHasher): # Using the parameter for rounds will generate the error # ValueError: rounds too high (bcrypt requires <= 31 rounds) # when using the bcrypt hasher. - # rounds = int(parameters.get_admin('ROUNDS_NUMBER')) + # rounds = parameters.get_global_parameter("rounds_number") # To get around this, I use the default of 12. rounds = 12 return bcrypt.encrypt(clearvalue, rounds=rounds) @@ -72,7 +75,7 @@ class SHA256CRYPTHasher(PasswordHasher): return pwhash def _encrypt(self, clearvalue, salt=None): - rounds = int(parameters.get_admin('ROUNDS_NUMBER')) + rounds = param_tools.get_global_parameter("rounds_number") return sha256_crypt.encrypt(clearvalue, rounds=rounds) def verify(self, clearvalue, hashed_value): @@ -95,7 +98,7 @@ class SHA512CRYPTHasher(PasswordHasher): return pwhash def _encrypt(self, clearvalue, salt=None): - rounds = int(parameters.get_admin('ROUNDS_NUMBER')) + rounds = param_tools.get_global_parameter("rounds_number") return sha512_crypt.encrypt(clearvalue, rounds=rounds) def verify(self, clearvalue, hashed_value): diff --git a/modoboa/core/static/core/js/settings.js b/modoboa/core/static/core/js/settings.js index a20ebac58..0e926fbcc 100644 --- a/modoboa/core/static/core/js/settings.js +++ b/modoboa/core/static/core/js/settings.js @@ -4,6 +4,8 @@ * @constructor * @param {Object} options - instance options */ +/* global TwocolsNav */ + var Settings = function(options) { TwocolsNav.call(this, options); }; @@ -40,7 +42,7 @@ Settings.prototype = { * Navigation callback: extensions. */ extensions_callback: function(data) { - this.select_left_menu(); + this.select_left_menu(); if (data.content) { $('#' + this.options.divid).html(data.content); } diff --git a/modoboa/core/templates/core/parameters.html b/modoboa/core/templates/core/parameters.html index a4b8538e4..b0efc3da8 100644 --- a/modoboa/core/templates/core/parameters.html +++ b/modoboa/core/templates/core/parameters.html @@ -1,8 +1,7 @@ {% load i18n lib_tags %}

{% trans "Parameters" %} {% trans "Configure Modoboa" %}


-
{% csrf_token %} - {% include "common/parameters.html" %} +{% csrf_token %} + {% include "parameters/base_form.html" %}
- +
- diff --git a/modoboa/core/templates/core/user_preferences.html b/modoboa/core/templates/core/user_preferences.html index a13dcf1ed..f38ba518f 100644 --- a/modoboa/core/templates/core/user_preferences.html +++ b/modoboa/core/templates/core/user_preferences.html @@ -3,11 +3,11 @@
{% if forms %}
+ id="parameters_form"> {% csrf_token %} - {% include "common/parameters.html" %} + {% include "parameters/base_form.html" %}
- +
{% else %}
diff --git a/modoboa/core/tests/test_core.py b/modoboa/core/tests/test_core.py index 598124fd9..3e8de2b6f 100644 --- a/modoboa/core/tests/test_core.py +++ b/modoboa/core/tests/test_core.py @@ -7,9 +7,9 @@ import httmock from django.core import management from django.core.urlresolvers import reverse from django.test import override_settings, TestCase +from django.utils.functional import cached_property from modoboa.lib import exceptions -from modoboa.lib import parameters from modoboa.lib.tests import ModoTestCase from modoboa.lib.tests import NO_LDAP @@ -58,62 +58,28 @@ class ChangeDefaultAdminTestCase(TestCase): self.client.login(username="modoadmin", password="password")) -class DashboardTestCase(ModoTestCase): - """Dashboard tests.""" - - @classmethod - def setUpTestData(cls): - """Create some data.""" - super(DashboardTestCase, cls).setUpTestData() - cls.dadmin = factories.UserFactory( - username="admin@test.com", groups=("DomainAdmins",) - ) - cls.user = factories.UserFactory( - username="user@test.com", groups=("SimpleUsers",) - ) - - def test_access(self): - """Load dashboard.""" - url = reverse("core:dashboard") - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertIn("Latest news", response.content) - self.client.logout() - self.client.login(username=self.dadmin.username, password="toto") - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertIn("Latest news", response.content) - self.client.logout() - self.client.login(username=self.user.username, password="toto") - response = self.client.get(url) - self.assertEqual(response.status_code, 302) - - def test_root_dispatch(self): - """Check root dispatching.""" - url = reverse("core:root") - response = self.client.get(url) - self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith(reverse("core:dashboard"))) - - @skipIf(NO_LDAP, 'No ldap module installed') class LDAPTestCaseMixin(object): """Set of methods used to test LDAP features.""" + @cached_property + def ldapauthbackend(self): + """Return LDAPAuthBackend instance.""" + from modoboa.lib.ldap_utils import LDAPAuthBackend + return LDAPAuthBackend() + def activate_ldap_authentication(self): """Modify settings.""" - parameters.save_admin( - "AUTHENTICATION_TYPE", "ldap") - parameters.save_admin("LDAP_SERVER_PORT", "3389") + self.set_global_parameters({ + "authentication_type": "ldap", + "ldap_server_port": 3389 + }) def restore_user_password(self, username, new_password): """Restore user password to its initial state.""" - from modoboa.lib.ldap_utils import LDAPAuthBackend - - backend = LDAPAuthBackend() for password in ["Toto1234", "test"]: try: - backend.update_user_password( + self.ldapauthbackend.update_user_password( username, password, new_password) except exceptions.InternalError: pass @@ -131,16 +97,19 @@ class LDAPTestCaseMixin(object): def searchbind_mode(self): """Apply settings required by the searchbind mode.""" - parameters.save_admin("LDAP_AUTH_METHOD", "searchbind") - parameters.save_admin("LDAP_BIND_DN", "cn=admin,dc=example,dc=com") - parameters.save_admin("LDAP_BIND_PASSWORD", "test") - parameters.save_admin("LDAP_SEARCH_BASE", "ou=users,dc=example,dc=com") + self.set_global_parameters({ + "ldap_auth_method": "searchbind", + "ldap_bind_dn": "cn=admin,dc=example,dc=com", + "ldap_bind_password": "test", + "ldap_search_base": "ou=users,dc=example,dc=com" + }) def directbind_mode(self): """Apply settings required by the directbind mode.""" - parameters.save_admin("LDAP_AUTH_METHOD", "directbind") - parameters.save_admin( - "LDAP_USER_DN_TEMPLATE", "cn=%(user)s,ou=users,dc=example,dc=com") + self.set_global_parameters({ + "ldap_auth_method": "directbind", + "ldap_user_dn_template": "cn=%(user)s,ou=users,dc=example,dc=com" + }) @override_settings(AUTHENTICATION_BACKENDS=( @@ -148,7 +117,6 @@ class LDAPTestCaseMixin(object): 'modoboa.lib.authbackends.SimpleBackend', )) class LDAPAuthenticationTestCase(LDAPTestCaseMixin, ModoTestCase): - """Validate LDAP authentication scenarios.""" def setUp(self): @@ -172,11 +140,11 @@ class LDAPAuthenticationTestCase(LDAPTestCaseMixin, ModoTestCase): username = "testuser@example.com" self.authenticate(username, "test") self.check_created_user(username) - self.client.logout() - parameters.save_admin("LDAP_ADMIN_GROUPS", "admins") - parameters.save_admin( - "LDAP_GROUPS_SEARCH_BASE", "ou=groups,dc=example,dc=com") + self.set_global_parameters({ + "ldap_admin_groups": "admins", + "ldap_groups_search_base": "ou=groups,dc=example,dc=com" + }) username = "mailadmin@example.com" self.authenticate(username, "test", False) self.check_created_user(username, "DomainAdmins") @@ -194,9 +162,10 @@ class LDAPAuthenticationTestCase(LDAPTestCaseMixin, ModoTestCase): # 1: must work because usernames of domain admins are not # always email addresses - parameters.save_admin("LDAP_ADMIN_GROUPS", "admins") - parameters.save_admin( - "LDAP_GROUPS_SEARCH_BASE", "ou=groups,dc=example,dc=com") + self.set_global_parameters({ + "ldap_admin_groups": "admins", + "ldap_groups_search_base": "ou=groups,dc=example,dc=com" + }) username = "mailadmin" self.authenticate(username, "test", False) self.check_created_user(username, "DomainAdmins", False) diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py new file mode 100644 index 000000000..e50dbe12c --- /dev/null +++ b/modoboa/core/tests/test_views.py @@ -0,0 +1,162 @@ +"""Core views test cases.""" + +from testfixtures import compare + +from django import forms +from django.core.urlresolvers import reverse + +from modoboa.lib.tests import ModoTestCase +from modoboa.parameters import forms as param_forms +from modoboa.parameters import tools as param_tools + +from .. import factories +from .. import models + + +class DashboardTestCase(ModoTestCase): + """Dashboard tests.""" + + @classmethod + def setUpTestData(cls): + """Create some data.""" + super(DashboardTestCase, cls).setUpTestData() + cls.dadmin = factories.UserFactory( + username="admin@test.com", groups=("DomainAdmins",) + ) + cls.user = factories.UserFactory( + username="user@test.com", groups=("SimpleUsers",) + ) + + def test_access(self): + """Load dashboard.""" + url = reverse("core:dashboard") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn("Latest news", response.content) + self.client.logout() + self.client.login(username=self.dadmin.username, password="toto") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn("Latest news", response.content) + self.client.logout() + self.client.login(username=self.user.username, password="toto") + response = self.client.get(url) + self.assertEqual(response.status_code, 302) + + def test_root_dispatch(self): + """Check root dispatching.""" + url = reverse("core:root") + response = self.client.get(url) + self.assertEqual(response.status_code, 302) + self.assertTrue(response.url.endswith(reverse("core:dashboard"))) + + +class SettingsTestCase(ModoTestCase): + """Settings tests.""" + + settings_sample = { + "core-check_new_versions": "True", + "core-log_maximum_age": "365", + "core-ldap_auth_method": "searchbind", + "limits-deflt_domain_mailbox_aliases_limit": "0", + "core-send_statistics": "True", + "core-default_top_redirection": "user", + "core-sender_address": "noreply@modoboa.org", + "admin-auto_account_removal": "False", + "core-ldap_server_port": "389", + "core-rounds_number": "70000", + "core-ldap_bind_password": "", + "limits-deflt_domain_domain_aliases_limit": "0", + "core-secret_key": ":?j3]QPWo!.'_c4n", + "relaydomains-master_cf_path": "/etc/postfix/master.cf", + "limits-deflt_domain_domain_admins_limit": "0", + "limits-enable_admin_limits": "True", + "core-ldap_bind_dn": "", + "core-enable_api_communication": "True", + "limits-deflt_user_domain_admins_limit": "0", + "limits-enable_domain_limits": "False", + "csrfmiddlewaretoken": "SGgMVZsA4TPqoiV786TMST6xgOlhAf4F", + "limits-deflt_user_mailboxes_limit": "0", + "core-password_scheme": "sha512crypt", + "core-items_per_page": "30", + "limits-deflt_user_mailbox_aliases_limit": "0", + "limits-deflt_domain_mailboxes_limit": "0", + "core-ldap_is_active_directory": "False", + "core-ldap_group_type": "posixgroup", + "limits-deflt_user_domain_aliases_limit": "0", + "core-top_notifications_check_interval": "30", + "core-ldap_password_attribute": "userPassword", + "admin-auto_create_domain_and_mailbox": "True", + "admin-enable_dnsbl_checks": "True", + "admin-default_domain_quota": "0", + "core-ldap_server_address": "localhost", + "core-authentication_type": "local", + "core-ldap_admin_groups": "", + "core-ldap_groups_search_base": "", + "admin-enable_mx_checks": "True", + "core-ldap_search_base": "", + "admin-valid_mxs": "", + "limits-deflt_user_domains_limit": "0", + "core-ldap_search_filter": "(mail=%(user)s)", + "core-ldap_secured": "False", + "core-ldap_user_dn_template": "" + } + + def test_get_settings(self): + """Test settings display.""" + url = reverse("core:parameters") + response = self.ajax_get(url) + for app in ["core", "admin", "limits", "relaydomains"]: + self.assertIn('data-app="{}"'.format(app), response["content"]) + + def test_save_settings(self): + """Test settings save.""" + url = reverse("core:parameters") + response = self.client.post(url, self.settings_sample, format="json") + self.assertEqual(response.status_code, 200) + self.settings_sample["core-rounds_number"] = "" + response = self.client.post(url, self.settings_sample, format="json") + self.assertEqual(response.status_code, 400) + compare(response.json(), { + "form_errors": {"rounds_number": ["This field is required."]}, + "prefix": "core" + }) + + +class UserSettings(param_forms.UserParametersForm): + """Stupid user settings form.""" + + app = "core" + + test = forms.CharField() + + +class PreferencesTestCase(ModoTestCase): + """Test user preferences.""" + + @classmethod + def setUpClass(cls): + """Register user form.""" + super(PreferencesTestCase, cls).setUpClass() + param_tools.registry.add("user", UserSettings, "Test") + + @classmethod + def tearDownClass(cls): + """Remove user class.""" + super(PreferencesTestCase, cls).tearDownClass() + del param_tools.registry._registry["user"]["core"] + + def test_get_preferences(self): + """Test preferences display.""" + url = reverse("core:user_preferences") + self.ajax_get(url) + + def test_save_preferences(self): + """Test preferences save.""" + url = reverse("core:user_preferences") + response = self.client.post(url, {"core-test": "toto"}, format="json") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), "Preferences saved") + + admin = models.User.objects.get(username="admin") + self.assertEqual(admin.parameters.get_value("test"), "toto") diff --git a/modoboa/core/urls.py b/modoboa/core/urls.py index f2050f9f4..39f5b53b3 100644 --- a/modoboa/core/urls.py +++ b/modoboa/core/urls.py @@ -17,10 +17,8 @@ urlpatterns = patterns( url(r'^core/$', 'modoboa.core.views.admin.viewsettings', name="index"), - url(r'^core/parameters/$', 'modoboa.core.views.admin.viewparameters', - name="parameter_listl"), - url(r'^core/parameters/save/$', 'modoboa.core.views.admin.saveparameters', - name="parameter_save"), + url(r'^core/parameters/$', 'modoboa.core.views.admin.parameters', + name="parameters"), url(r'^core/info/$', 'modoboa.core.views.admin.information', name="information"), url(r'^core/logs/$', 'modoboa.core.views.admin.logs', name="log_list"), diff --git a/modoboa/core/views/admin.py b/modoboa/core/views/admin.py index 9d1fc83b6..aab2ce6b6 100644 --- a/modoboa/core/views/admin.py +++ b/modoboa/core/views/admin.py @@ -10,11 +10,12 @@ from django.utils.translation import ugettext as _ from modoboa.core.models import Log from modoboa.core.utils import check_for_updates -from modoboa.lib import events, parameters +from modoboa.lib import events from modoboa.lib.listing import get_sort_order, get_listing_page from modoboa.lib.web_utils import ( _render_to_string, render_to_json_response ) +from modoboa.parameters import tools as param_tools @login_required @@ -27,30 +28,31 @@ def viewsettings(request, tplname='core/settings_header.html'): @login_required @user_passes_test(lambda u: u.is_superuser) -def viewparameters(request, tplname='core/parameters.html'): +def parameters(request, tplname="core/parameters.html"): + """View to display and save global parameters.""" + if request.method == "POST": + forms = param_tools.registry.get_forms( + "global", request.POST, localconfig=request.localconfig) + for formdef in forms: + form = formdef["form"] + if form.is_valid(): + form.save() + form.to_django_settings() + continue + return render_to_json_response( + {"form_errors": form.errors, "prefix": form.app}, status=400 + ) + request.localconfig.save() + return render_to_json_response(_("Parameters saved")) return render_to_json_response({ "left_selection": "parameters", "content": _render_to_string(request, tplname, { - "forms": parameters.get_admin_forms + "forms": param_tools.registry.get_forms( + "global", localconfig=request.localconfig) }) }) -@login_required -@user_passes_test(lambda u: u.is_superuser) -def saveparameters(request): - for formdef in parameters.get_admin_forms(request.POST): - form = formdef["form"] - if form.is_valid(): - form.save() - form.to_django_settings() - continue - return render_to_json_response( - {'form_errors': form.errors, 'prefix': form.app}, status=400 - ) - return render_to_json_response(_("Parameters saved")) - - @login_required @user_passes_test(lambda u: u.is_superuser) def information(request, tplname="core/information.html"): diff --git a/modoboa/core/views/base.py b/modoboa/core/views/base.py index bb61722e9..be3708641 100644 --- a/modoboa/core/views/base.py +++ b/modoboa/core/views/base.py @@ -5,8 +5,6 @@ from django.views import generic from django.contrib.auth import mixins as auth_mixins -from modoboa.lib import parameters - from ..extensions import exts_pool @@ -18,7 +16,8 @@ def find_nextlocation(request, user): nextlocation = request.POST.get("next", None) if nextlocation is None or nextlocation == "None": if request.user.role == "SimpleUsers": - topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION") + topredir = request.localconfig.parameters.get_value( + "default_top_redirection") if topredir != "user": infos = exts_pool.get_extension_infos(topredir) nextlocation = infos["url"] if infos["url"] else infos["name"] diff --git a/modoboa/core/views/user.py b/modoboa/core/views/user.py index 149dc91a3..d040cf8d2 100644 --- a/modoboa/core/views/user.py +++ b/modoboa/core/views/user.py @@ -8,11 +8,13 @@ from django.contrib.auth.decorators import login_required, user_passes_test from rest_framework.authtoken.models import Token -from modoboa.lib import events, parameters +from modoboa.lib import events from modoboa.lib.cryptutils import encrypt from modoboa.lib.web_utils import ( _render_to_string, render_to_json_response ) +from modoboa.parameters import tools as param_tools + from ..forms import ProfileForm, APIAccessForm @@ -59,7 +61,9 @@ def profile(request, tplname='core/user_profile.html'): @login_required def preferences(request): if request.method == "POST": - for formdef in parameters.get_user_forms(request.user, request.POST): + forms = param_tools.registry.get_forms( + "user", request.POST, user=request.user) + for formdef in forms: form = formdef["form"] if form.is_valid(): form.save() @@ -67,12 +71,13 @@ def preferences(request): return render_to_json_response({ "prefix": form.app, "form_errors": form.errors }, status=400) - + request.user.save() return render_to_json_response(_("Preferences saved")) return render_to_json_response({ "content": _render_to_string(request, "core/user_preferences.html", { - "forms": parameters.get_user_forms(request.user) + "forms": param_tools.registry.get_forms( + "user", user=request.user, first_app="general") }) }) diff --git a/modoboa/lib/authbackends.py b/modoboa/lib/authbackends.py index 956a4f314..b603855f3 100644 --- a/modoboa/lib/authbackends.py +++ b/modoboa/lib/authbackends.py @@ -3,12 +3,11 @@ from django.contrib.auth.backends import ModelBackend from modoboa.core.models import User -from modoboa.lib import parameters from modoboa.lib.email_utils import split_mailbox +from modoboa.parameters import tools as param_tools class SimpleBackend(ModelBackend): - """Simple authentication backend.""" def authenticate(self, username=None, password=None): @@ -36,8 +35,10 @@ try: def __init__(self, *args, **kwargs): """Load LDAP settings.""" - parameters.apply_to_django_settings() + param_tools.apply_to_django_settings() super(LDAPBackend, self).__init__(*args, **kwargs) + self.global_params = dict( + param_tools.get_global_parameters("core")) def get_or_create_user(self, username, ldap_user): """ @@ -46,12 +47,11 @@ try: the user. ldap_user.dn is the user's DN and ldap_user.attrs contains all of their LDAP attributes. """ - group = 'SimpleUsers' - admin_groups = parameters \ - .get_admin('LDAP_ADMIN_GROUPS', app='core').split(';') + group = "SimpleUsers" + admin_groups = self.global_params["ldap_admin_groups"].split(";") for grp in admin_groups: if grp.strip() in ldap_user.group_names: - group = 'DomainAdmins' + group = "DomainAdmins" break if group == 'SimpleUsers': lpart, domain = split_mailbox(username) @@ -75,8 +75,7 @@ try: return user def authenticate(self, username, password): - auth_type = parameters.get_admin("AUTHENTICATION_TYPE", app="core") - if auth_type == "ldap": + if self.global_params["authentication_type"] == "ldap": return super(LDAPBackend, self).authenticate( username, password) return None diff --git a/modoboa/lib/cryptutils.py b/modoboa/lib/cryptutils.py index c98ebff1a..f0401ded7 100644 --- a/modoboa/lib/cryptutils.py +++ b/modoboa/lib/cryptutils.py @@ -7,7 +7,7 @@ import string from Crypto.Cipher import AES -from modoboa.lib import parameters +from modoboa.parameters import tools as param_tools def random_key(l=16): @@ -25,7 +25,7 @@ def random_key(l=16): def encrypt(clear): - key = parameters.get_admin("SECRET_KEY", app="core") + key = param_tools.get_global_parameter("secret_key", app="core") obj = AES.new(key, AES.MODE_ECB) if type(clear) is unicode: clear = clear.encode("utf-8") @@ -38,7 +38,8 @@ def encrypt(clear): def decrypt(ciph): obj = AES.new( - parameters.get_admin("SECRET_KEY", app="core"), AES.MODE_ECB + param_tools.get_global_parameter( + "secret_key", app="core"), AES.MODE_ECB ) ciph = base64.b64decode(ciph) clear = obj.decrypt(ciph) diff --git a/modoboa/lib/form_utils.py b/modoboa/lib/form_utils.py index b7abb9c3b..d8914c4d4 100644 --- a/modoboa/lib/form_utils.py +++ b/modoboa/lib/form_utils.py @@ -6,7 +6,7 @@ import abc from collections import OrderedDict import re -from django.forms import ChoiceField +from django.forms import TypedChoiceField from django.forms.fields import Field from django.forms.widgets import RadioSelect from django.forms.widgets import RadioChoiceInput @@ -232,9 +232,8 @@ class DynamicForm(object): class TabForms(object): - """ - Simple forms container + Simple forms container. This class tries to encapsulate multiple forms that will be displayed using tabs. It is different from a classical formset @@ -371,19 +370,23 @@ class TabForms(object): class CustomRadioInput(RadioChoiceInput): + """Custom radio input.""" + def __unicode__(self): - if 'id' in self.attrs: - label_for = ' for="%s"' % self.attrs['id'] + if "id" in self.attrs: + label_for = ' for="%s"' % self.attrs["id"] else: - label_for = '' + label_for = "" choice_label = conditional_escape(force_unicode(self.choice_label)) return mark_safe( - u'' + u"" % (label_for, self.tag(), choice_label) ) class InlineRadioRenderer(RadioSelect.renderer): + """Custom inline radio renderer.""" + def __iter__(self): for i, choice in enumerate(self.choices): yield CustomRadioInput( @@ -392,24 +395,35 @@ class InlineRadioRenderer(RadioSelect.renderer): def render(self): return mark_safe( - u'\n'.join([u'%s\n' % force_unicode(w) for w in self]) + u"\n".join([u"%s\n" % force_unicode(w) for w in self]) ) class InlineRadioSelect(RadioSelect): + """Custom inline radio widget.""" + renderer = InlineRadioRenderer class SeparatorField(Field): + """Custom field to represent a separator.""" + def __init__(self, *args, **kwargs): kwargs["required"] = False super(SeparatorField, self).__init__(*args, **kwargs) -class YesNoField(ChoiceField): +class YesNoField(TypedChoiceField): + """A yes/no form field.""" + def __init__(self, *args, **kwargs): - kwargs["choices"] = [ - ("yes", ugettext_lazy("Yes")), ("no", ugettext_lazy("No")) - ] - kwargs["widget"] = InlineRadioSelect + """Constructor.""" + kwargs.update({ + "choices": ( + (True, ugettext_lazy("Yes")), + (False, ugettext_lazy("No")) + ), + "widget": InlineRadioSelect, + "coerce": lambda x: x == "True" + }) super(YesNoField, self).__init__(*args, **kwargs) diff --git a/modoboa/lib/ldap_utils.py b/modoboa/lib/ldap_utils.py index 42db29d44..8fc210eb2 100644 --- a/modoboa/lib/ldap_utils.py +++ b/modoboa/lib/ldap_utils.py @@ -1,8 +1,7 @@ # coding: utf-8 """ -:mod:`ldaputils` --- a collection of LDAP based classes/functions ------------------------------------------------------------------ +A collection of LDAP based classes/functions. For a first version, the LDAP support offered by Modoboa only supports one global server definition : the one the django-auth-ldap uses. @@ -39,14 +38,16 @@ from django.conf import settings from django.utils.translation import ugettext as _ from modoboa.core.password_hashers import get_password_hasher -from modoboa.lib import parameters from modoboa.lib.exceptions import InternalError +from modoboa.parameters import tools as param_tools class LDAPAuthBackend(object): + """LDAP authentication backend.""" def __init__(self): - parameters.apply_to_django_settings() + param_tools.apply_to_django_settings() + self.global_params = dict(param_tools.get_global_parameters("core")) self.server_uri = self._setting( "AUTH_LDAP_SERVER_URI", "ldap://localhost" ) @@ -74,7 +75,7 @@ class LDAPAuthBackend(object): """Connect to the server according to configuration.""" if self.conn is not None: return - mode = parameters.get_admin("LDAP_AUTH_METHOD", app="core") + mode = self.global_params["ldap_auth_method"] if mode == "searchbind": bind_dn = self._setting("AUTH_LDAP_BIND_DN", "") bind_pwd = self._setting("AUTH_LDAP_BIND_PASSWORD", "") @@ -86,8 +87,8 @@ class LDAPAuthBackend(object): def _find_user_dn(self, user): """Find the DN of the given user.""" - sbase = parameters.get_admin("LDAP_SEARCH_BASE", app="core") - sfilter = parameters.get_admin("LDAP_SEARCH_FILTER", app="core") + sbase = self.global_params["ldap_search_base"] + sfilter = self.global_params["ldap_search_filter"] sfilter = sfilter % {"user": user} res = self.conn.search_s(sbase, ldap.SCOPE_SUBTREE, sfilter) try: @@ -102,8 +103,8 @@ class LDAPAuthBackend(object): :param clearpassword: the clear password :return: the encrypted password """ - scheme = parameters.get_admin("PASSWORD_SCHEME", app="core") - hasher = get_password_hasher(scheme.upper())('ldap') + scheme = self.global_params["password_scheme"] + hasher = get_password_hasher(scheme.upper())("ldap") return hasher.encrypt(clearpassword) def update_user_password(self, user, password, newpassword): diff --git a/modoboa/lib/listing.py b/modoboa/lib/listing.py index 3ae82d832..e8f38f84d 100644 --- a/modoboa/lib/listing.py +++ b/modoboa/lib/listing.py @@ -1,6 +1,6 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from modoboa.lib import parameters +from modoboa.parameters import tools as param_tools def get_sort_order(qdict, default, allowed_values=None): @@ -34,7 +34,8 @@ def get_listing_page(objects, pagenum): :return: a ``Page`` object """ paginator = Paginator( - objects, int(parameters.get_admin("ITEMS_PER_PAGE", app="core")), + objects, + param_tools.get_global_parameter("items_per_page", app="core"), allow_empty_first_page=False ) try: diff --git a/modoboa/lib/parameters.py b/modoboa/lib/parameters.py deleted file mode 100644 index dc23d4538..000000000 --- a/modoboa/lib/parameters.py +++ /dev/null @@ -1,371 +0,0 @@ -# coding: utf-8 - -""" -This interface provides a simple way to declare and store parameters -in Modoboa's database. - -Core components or extensions can register their own parameters, which -will be available and modifiable directly from the web interface. - -Only super users will be able to access this part of the web interface. -""" - -from django import forms - -from modoboa.lib import events -from modoboa.lib.exceptions import ModoboaException -from modoboa.lib.sysutils import guess_extension_name - -from . import db_utils - - -_params = {'A': {}, 'U': {}} - - -class NotDefined(ModoboaException): - http_code = 404 - - def __init__(self, app, name): - self.app = app - self.name = name - - def __str__(self): - return "Application '%s' and/or parameter '%s' not defined" \ - % (self.app, self.name) - - -class GenericParametersForm(forms.Form): - """Base class for parameter forms. - - Each extension has the possibility to define global parameters. - """ - app = None - visibility_rules = None - - def __init__(self, *args, **kwargs): - if self.app is None: - raise NotImplementedError - - kwargs["prefix"] = self.app - super(GenericParametersForm, self).__init__(*args, **kwargs) - - self.visirules = {} - if self.visibility_rules is not None: - for key, rule in self.visibility_rules.items(): - field, value = rule.split("=") - visibility = { - "field": "id_%s-%s" % (self.app, field), "value": value - } - self.visirules["%s-%s" % (self.app, key)] = visibility - - if not args: - self._load_initial_values() - - def _load_initial_values(self): - raise NotImplementedError - - def _decode_value(self, value): - return value.decode('unicode_escape').replace('\\r\\n', '\n') - - def _load_extra_parameters(self, level): - params = events.raiseDictEvent('GetExtraParameters', self.app, level) - for pname, pdef in params.items(): - self.fields[pname] = pdef - - def _save_parameter(self, p, name, value): - if p.value == value: - return - name = name.lower() - if hasattr(self, "update_%s" % name): - getattr(self, "update_%s" % name)(value) - if type(value) is unicode: - p.value = value.encode("unicode_escape").strip() - else: - p.value = str(value) - p.save() - - def save(self): - raise NotImplementedError - - -class AdminParametersForm(GenericParametersForm): - - def _load_initial_values(self): - from .models import Parameter - - if not db_utils.db_table_exists("lib_parameter"): - return - names = [ - "%s.%s" % (self.app, name.upper()) for name in self.fields.keys() - ] - for p in Parameter.objects.filter(name__in=names): - self.fields[p.shortname].initial = self._decode_value(p.value) - - def save(self): - from .models import Parameter - from modoboa.lib.form_utils import SeparatorField - - for name, value in self.cleaned_data.items(): - if type(self.fields[name]) is SeparatorField: - continue - fullname = "%s.%s" % (self.app, name.upper()) - try: - p = Parameter.objects.get(name=fullname) - except Parameter.DoesNotExist: - p = Parameter() - p.name = fullname - self._save_parameter(p, name, value) - - def to_django_settings(self): - pass - - def get_current_values(self): - values = {} - for key in self.fields.keys(): - try: - values[key] = get_admin(key.upper(), app=self.app) - except NotDefined: - pass - return values - - -class UserParametersForm(GenericParametersForm): - - def __init__(self, *args, **kwargs): - self.user = kwargs.pop("user") if "user" in kwargs else None - super(UserParametersForm, self).__init__(*args, **kwargs) - - def _load_initial_values(self): - if not db_utils.db_table_exists("lib_userparameter"): - return - if self.user is None: - return - from .models import UserParameter - - names = [ - "%s.%s" % (self.app, name.upper()) for name in self.fields.keys() - ] - for p in UserParameter.objects.filter(user=self.user, name__in=names): - self.fields[p.shortname].initial = self._decode_value(p.value) - - @staticmethod - def has_access(user): - return True - - def save(self): - from .models import UserParameter - from modoboa.lib.form_utils import SeparatorField - - for name, value in self.cleaned_data.items(): - if type(self.fields[name]) is SeparatorField: - continue - fullname = "%s.%s" % (self.app, name.upper()) - try: - p = UserParameter.objects.get(user=self.user, name=fullname) - except UserParameter.DoesNotExist: - p = UserParameter() - p.user = self.user - p.name = fullname - self._save_parameter(p, name, value) - - -def register(formclass, label): - """Register a form class containing parameters - - formclass must inherit from ``AdminParametersForm`` or - ``UserParametersForm``. - - :param formclass: a form class - :param string label: the label to display in parameters or settings pages - """ - from modoboa.lib.form_utils import SeparatorField - - if issubclass(formclass, AdminParametersForm): - level = 'A' - elif issubclass(formclass, UserParametersForm): - level = 'U' - else: - raise RuntimeError("Unknown parameter class") - _params[level][formclass.app] = { - "label": label, "form": formclass, "defaults": {} - } - form = formclass() - for name, field in form.fields.items(): - if type(field) is SeparatorField: - continue - _params[level][formclass.app]["defaults"][name.upper()] = field.initial - - -def unregister(app=None): - """Unregister an application - - All parameters associated to this application will also be - removed. - - :param app: the application's name (string) - """ - if app is None: - app = guess_extension_name() - for lvlparams in _params.values(): - if app in lvlparams: - del lvlparams[app] - - -def __is_defined(app, level, name): - not_defined = ( - level not in ['A', 'U'] or - app not in _params[level] or - name not in _params[level][app]["defaults"] - ) - if not_defined: - raise NotDefined(app, name) - - -def save_admin(name, value, app=None): - """Set a new value for a given parameter.""" - from .models import Parameter - - if app is None: - app = guess_extension_name() - __is_defined(app, 'A', name) - fullname = "%s.%s" % (app, name) - try: - p = Parameter.objects.get(name=fullname) - except Parameter.DoesNotExist: - p = Parameter() - p.name = fullname - p.value = None - f = get_parameter_form('A', name, app) - f()._save_parameter(p, name, value) - - -def save_user(user, name, value, app=None): - from .models import UserParameter - - if app is None: - app = guess_extension_name() - __is_defined(app, 'U', name) - fullname = "%s.%s" % (app, name) - try: - p = UserParameter.objects.get(user=user, name=fullname) - except UserParameter.DoesNotExist: - p = UserParameter() - p.user = user - p.name = fullname - f = get_parameter_form('U', name, app) - f()._save_parameter(p, name, value) - - -def get_admin(name, app=None, raise_error=True): - """Return an administrative parameter - - A ``NotDefined`` exception if the parameter doesn't exist. - - :param name: the parameter's name - :param app: the application owning the parameter - :return: the corresponding value as a string - """ - from .models import Parameter - - if app is None: - app = guess_extension_name() - try: - __is_defined(app, "A", name) - except NotDefined: - if raise_error: - raise - return None - try: - p = Parameter.objects.get(name="%s.%s" % (app, name)) - except Parameter.DoesNotExist: - return _params["A"][app]["defaults"][name] - return p.value.decode("unicode_escape").replace('\\r\\n', '\n') - - -def get_user(user, name, app=None, raise_error=True): - """Return a parameter for a specific user - - A ``NotDefined`` exception if the parameter doesn't exist. - - :param ``User`` user: the desired user - :param name: the parameter's name - :param app: the application owning the parameter - :return: the corresponding value as a string - """ - from .models import UserParameter - - if app is None: - app = guess_extension_name() - try: - __is_defined(app, "U", name) - except NotDefined: - if raise_error: - raise - return None - try: - p = UserParameter.objects.get(user=user, name="%s.%s" % (app, name)) - except UserParameter.DoesNotExist: - return _params["U"][app]["defaults"][name] - return p.value.decode("unicode_escape") - - -def get_sorted_apps(level, first="core"): - """Retrieve the sorted list of all registerd applications. - - :param str level: application level - :param str first: force the first item of the result - :rtype: list - """ - sorted_apps = [] - if first in _params[level]: - sorted_apps.append(first) - sorted_apps += sorted( - [app for app in _params[level].keys() if app != first], - key=lambda app: _params[level][app]["label"] - ) - return sorted_apps - - -def get_admin_forms(*args, **kwargs): - """Get all admin level forms. - - Generates an instance of each declared form. - """ - for app in get_sorted_apps('A'): - formdef = _params['A'][app] - yield {"label": formdef["label"], - "form": formdef["form"](*args, **kwargs)} - - -def get_user_forms(user, *args, **kwargs): - """Return an instance of each user-level forms.""" - kwargs["user"] = user - sorted_apps = get_sorted_apps('U', first="general") - result = [] - for app in sorted_apps: - formdef = _params["U"][app] - if not formdef["form"].has_access(user): - continue - result.append({ - "label": formdef["label"], - "form": formdef["form"](*args, **kwargs)}) - return result - - -def get_parameter_form(level, name, app=None): - """Return the form containing a specific parameter - - :param string level: associated level - :param string name: paremeter's name - :param string app: parameter's application - :return: a form class - """ - if app is None: - app = guess_extension_name() - __is_defined(app, level, name) - return _params[level][app]["form"] - - -def apply_to_django_settings(): - for form in get_admin_forms(): - form["form"].to_django_settings() diff --git a/modoboa/lib/tests.py b/modoboa/lib/tests.py index 271e972e7..c2544717c 100644 --- a/modoboa/lib/tests.py +++ b/modoboa/lib/tests.py @@ -3,16 +3,16 @@ import json -from django import forms from django.core import management from django.test import TestCase from rest_framework.authtoken.models import Token from rest_framework.test import APITestCase -from modoboa.lib import parameters from modoboa.core import models as core_models +from . import sysutils + try: import ldap # NOQA NO_LDAP = False @@ -20,16 +20,41 @@ except ImportError: NO_LDAP = True -class ModoTestCase(TestCase): +class ParametersMixin(object): + """Add tools to manage parameters.""" + @classmethod + def setUpTestData(cls): + """Set LocalConfig instance.""" + super(ParametersMixin, cls).setUpTestData() + cls.localconfig = core_models.LocalConfig.objects.first() + + def set_global_parameter(self, name, value, app=None): + """Set global parameter for the given app.""" + if app is None: + app = sysutils.guess_extension_name() + self.localconfig.parameters.set_value(name, value, app=app) + self.localconfig.save() + + def set_global_parameters(self, parameters, app=None): + """Set/update global parameters for the given app.""" + if app is None: + app = sysutils.guess_extension_name() + self.localconfig.parameters.set_values(parameters, app=app) + self.localconfig.save() + + +class ModoTestCase(ParametersMixin, TestCase): """All test cases must inherit from this one.""" @classmethod def setUpTestData(cls): """Create a default user.""" + super(ModoTestCase, cls).setUpTestData() management.call_command("load_initial_data") def setUp(self, username="admin", password="password"): + """Initiate test context.""" self.assertEqual( self.client.login(username=username, password=password), True) @@ -54,13 +79,13 @@ class ModoTestCase(TestCase): return self.ajax_request('get', *args, **kwargs) -class ModoAPITestCase(APITestCase): - +class ModoAPITestCase(ParametersMixin, APITestCase): """All test cases must inherit from this one.""" @classmethod def setUpTestData(cls): """Create a default user.""" + super(ModoAPITestCase, cls).setUpTestData() management.call_command("load_initial_data") cls.token = Token.objects.create( user=core_models.User.objects.get(username="admin")) @@ -69,76 +94,3 @@ class ModoAPITestCase(APITestCase): """Setup.""" super(ModoAPITestCase, self).setUp() self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key) - - -class TestParams(parameters.AdminParametersForm): - app = "test" - - param1 = forms.CharField(label="Test", initial="toto") - param2 = forms.IntegerField(label="Int", initial=42) - - def update_param2(self, value): - raise RuntimeError("new value %d" % value) - - -class TestUserParams(parameters.UserParametersForm): - app = "test" - - param1 = forms.CharField(label="Test", initial="titi") - param2 = forms.IntegerField(label="Int", initial=42) - - def update_param2(self, value): - raise RuntimeError("new value %d" % value) - - -class ParameterTestCase(TestCase): - """Simple test cases for ``modoboa.lib.parameters`` module. - """ - - @classmethod - def setUpTestData(cls): - super(ParameterTestCase, cls).setUpTestData() - cls.user = core_models.User.objects.create(username="tester") - - def setUp(self): - """Initialize tests.""" - super(ParameterTestCase, self).setUp() - parameters.register(TestParams, "Test") - parameters.register(TestUserParams, "TestUser") - - def test_register_form(self): - self.assertIn("test", parameters._params['A']) - - def test_unregister(self): - parameters.unregister("test") - self.assertNotIn("test", parameters._params['A']) - - def test_get_parameter_form(self): - self.assertIs( - parameters.get_parameter_form('A', 'PARAM1', 'test'), - TestParams - ) - - def test_save_admin(self): - parameters.save_admin("PARAM1", "45", app="test") - self.assertEqual(parameters.get_admin("PARAM1", app="test"), "45") - - def test_update_param(self): - with self.assertRaisesRegexp(RuntimeError, "new value 35"): - parameters.save_admin("PARAM2", 35, app="test") - - def test_get_admin(self): - self.assertEqual(parameters.get_admin("PARAM1", "test"), "toto") - with self.assertRaises(parameters.NotDefined): - parameters.get_admin("TOTO", "test") - - def test_get_user(self): - self.assertEqual( - parameters.get_user(self.user, "PARAM1", "test"), "titi") - with self.assertRaises(parameters.NotDefined): - parameters.get_user(self.user, "TOTO", "test") - - def test_save_user(self): - parameters.save_user(self.user, "PARAM1", "pouet", "test") - self.assertEqual( - parameters.get_user(self.user, "PARAM1", "test"), "pouet") diff --git a/modoboa/limits/app_settings.py b/modoboa/limits/app_settings.py index ff729a668..b194f688a 100644 --- a/modoboa/limits/app_settings.py +++ b/modoboa/limits/app_settings.py @@ -4,10 +4,10 @@ from django import forms from django.utils.translation import ugettext_lazy as _ from modoboa.lib.form_utils import SeparatorField, YesNoField -from modoboa.lib.parameters import AdminParametersForm +from modoboa.parameters import forms as param_forms -class ParametersForm(AdminParametersForm): +class ParametersForm(param_forms.AdminParametersForm): """Available admin parameters.""" app = "limits" @@ -16,7 +16,7 @@ class ParametersForm(AdminParametersForm): enable_admin_limits = YesNoField( label=_("Enable per-admin limits"), - initial="yes", + initial=True, help_text=_("Enable or disable per-admin limits") ) @@ -77,7 +77,7 @@ class ParametersForm(AdminParametersForm): enable_domain_limits = YesNoField( label=_("Enable per-domain limits"), - initial="no", + initial=False, help_text=_("Enable or disable per-domain limits") ) @@ -125,17 +125,13 @@ class ParametersForm(AdminParametersForm): ) visibility_rules = { - "deflt_user_domains_limit": "enable_admin_limits=yes", - "deflt_user_domain_aliases_limit": "enable_admin_limits=yes", - "deflt_user_mailboxes_limit": "enable_admin_limits=yes", - "deflt_user_mailbox_aliases_limit": "enable_admin_limits=yes", - "deflt_user_domain_admins_limit": "enable_admin_limits=yes", - "deflt_domain_mailboxes_limit": "enable_domain_limits=yes", - "deflt_domain_mailbox_aliases_limit": "enable_domain_limits=yes", - "deflt_domain_domain_aliases_limit": "enable_domain_limits=yes", - "deflt_domain_domain_admins_limit": "enable_domain_limits=yes", + "deflt_user_domains_limit": "enable_admin_limits=True", + "deflt_user_domain_aliases_limit": "enable_admin_limits=True", + "deflt_user_mailboxes_limit": "enable_admin_limits=True", + "deflt_user_mailbox_aliases_limit": "enable_admin_limits=True", + "deflt_user_domain_admins_limit": "enable_admin_limits=True", + "deflt_domain_mailboxes_limit": "enable_domain_limits=True", + "deflt_domain_mailbox_aliases_limit": "enable_domain_limits=True", + "deflt_domain_domain_aliases_limit": "enable_domain_limits=True", + "deflt_domain_domain_admins_limit": "enable_domain_limits=True", } - - def __init__(self, *args, **kwargs): - super(AdminParametersForm, self).__init__(*args, **kwargs) - self._load_extra_parameters('A') diff --git a/modoboa/limits/apps.py b/modoboa/limits/apps.py index 37d31581f..1152b3b8b 100644 --- a/modoboa/limits/apps.py +++ b/modoboa/limits/apps.py @@ -1,20 +1,20 @@ """App config for limits.""" from django.apps import AppConfig - +from django.utils.translation import ugettext as _ EVENTS = [ - 'GetExtraLimitTemplates' + "GetExtraLimitTemplates" ] def load_limits_settings(): """Load settings.""" - from django.utils.translation import ugettext as _ - from modoboa.lib import events, parameters + from modoboa.lib import events + from modoboa.parameters import tools as param_tools from .app_settings import ParametersForm - parameters.register(ParametersForm, _("Limits")) + param_tools.registry.add("global", ParametersForm, _("Limits")) events.declare(EVENTS) from . import controls from . import general_callbacks diff --git a/modoboa/limits/controls.py b/modoboa/limits/controls.py index 16921728c..8a1a9cfdb 100644 --- a/modoboa/limits/controls.py +++ b/modoboa/limits/controls.py @@ -1,18 +1,16 @@ # coding: utf-8 -""" -:mod:`controls` --- provides event handlers that check if limits are reached ----------------------------------------------------------------------------- -""" +"""Event handlers that check if limits are reached.""" -from modoboa.lib import events, parameters +from modoboa.lib import events from modoboa.lib.permissions import get_object_owner +from modoboa.parameters import tools as param_tools from . import utils def move_pool_resource(owner, user): - """Move resource from one pool to another + """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. @@ -40,7 +38,7 @@ def user_can_set_role(user, role, account=None): :param str newrole: role to check """ condition = ( - parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no" or + not param_tools.get_global_parameter("enable_admin_limits") or role != "DomainAdmins") if condition: return [True] diff --git a/modoboa/limits/general_callbacks.py b/modoboa/limits/general_callbacks.py index 3d8e5f104..71da5c367 100644 --- a/modoboa/limits/general_callbacks.py +++ b/modoboa/limits/general_callbacks.py @@ -3,7 +3,8 @@ from django.template.loader import render_to_string from django.utils.translation import ugettext as _ -from modoboa.lib import events, parameters +from modoboa.lib import events +from modoboa.parameters import tools as param_tools from . import forms from . import utils @@ -12,7 +13,7 @@ from . import utils @events.observe("ExtraAdminContent") def display_pool_usage(user, target, currentpage): condition = ( - parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no" or + not param_tools.get_global_parameter("enable_admin_limits") or target != "leftcol" or user.is_superuser) if condition: return [] @@ -40,7 +41,7 @@ def display_pool_usage(user, target, currentpage): @events.observe("ExtraAccountForm") def extra_account_form(user, account=None): - if parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no": + if not param_tools.get_global_parameter("enable_admin_limits"): return [] if user.role not in ["SuperAdmins", "Resellers"]: return [] @@ -61,7 +62,7 @@ def extra_account_form(user, account=None): @events.observe("ExtraDomainForm") def extra_domain_form(user, domain): """Include domain limits form.""" - if parameters.get_admin("ENABLE_DOMAIN_LIMITS") == "no": + if not param_tools.get_global_parameter("enable_domain_limits"): return [] if not user.has_perm("admin.change_domain"): return [] @@ -74,7 +75,7 @@ def extra_domain_form(user, domain): @events.observe("FillDomainInstances") def fill_domain_instances(user, domain, instances): """Set domain instance for resources form.""" - if parameters.get_admin("ENABLE_DOMAIN_LIMITS") == "no": + if not param_tools.get_global_parameter("enable_domain_limits"): return if not user.has_perm("admin.change_domain"): return @@ -93,7 +94,7 @@ def check_form_access(account, form): @events.observe("FillAccountInstances") def fill_account_instances(user, account, instances): condition = ( - parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no" or + not param_tools.get_global_parameter("enable_admin_limits") or (not user.is_superuser and user.role != "Resellers") ) if condition: @@ -106,7 +107,7 @@ def fill_account_instances(user, account, instances): @events.observe("GetStaticContent") def get_static_content(caller, st_type, user): condition = ( - parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no" or + not param_tools.get_global_parameter("enable_admin_limits") or caller not in ["domains", "identities"] or user.role in ["SuperAdmins", "SimpleUsers"] ) diff --git a/modoboa/limits/handlers.py b/modoboa/limits/handlers.py index d8ca5b388..b2ffad27d 100644 --- a/modoboa/limits/handlers.py +++ b/modoboa/limits/handlers.py @@ -10,7 +10,7 @@ from modoboa.admin import signals as admin_signals from modoboa.core import models as core_models from modoboa.core import signals as core_signals from modoboa.lib import signals as lib_signals -from modoboa.lib import parameters +from modoboa.parameters import tools as param_tools from . import lib from . import models @@ -21,13 +21,13 @@ from . import utils def check_object_limit(sender, context, object_type, **kwargs): """Check if user can create a new object.""" if context.__class__.__name__ == "User": - if parameters.get_admin("ENABLE_ADMIN_LIMITS") == "no": + if not param_tools.get_global_parameter("enable_admin_limits"): return if context.is_superuser: return True limit = context.userobjectlimit_set.get(name=object_type) elif context.__class__.__name__ == "Domain": - if parameters.get_admin("ENABLE_DOMAIN_LIMITS") == "no": + if not param_tools.get_global_parameter("enable_domain_limits"): return limit = context.domainobjectlimit_set.get(name=object_type) else: @@ -44,15 +44,14 @@ def create_user_limits(sender, instance, **kwargs): return request = lib_signals.get_request() creator = request.user if request else None + global_params = dict(param_tools.get_global_parameters("limits")) for name, definition in utils.get_user_limit_templates(): ct = ContentType.objects.get_by_natural_key( *definition["content_type"].split(".")) max_value = 0 # creator can be None if user was created by a factory if not creator or creator.is_superuser: - max_value = int( - parameters.get_admin( - "DEFLT_USER_{}_LIMIT".format(name.upper()))) + max_value = global_params["deflt_user_{0}_limit".format(name)] models.UserObjectLimit.objects.create( user=instance, name=name, content_type=ct, max_value=max_value) @@ -62,9 +61,9 @@ def create_domain_limits(sender, instance, **kwargs): """Create limits for new domain.""" if not kwargs.get("created"): return + global_params = dict(param_tools.get_global_parameters("limits")) for name, definition in utils.get_domain_limit_templates(): - max_value = int( - parameters.get_admin("DEFLT_DOMAIN_{}_LIMIT".format(name.upper()))) + max_value = global_params["deflt_domain_{0}_limit".format(name)] models.DomainObjectLimit.objects.create( domain=instance, name=name, max_value=max_value) @@ -72,7 +71,7 @@ def create_domain_limits(sender, instance, **kwargs): @receiver(admin_signals.extra_domain_dashboard_widgets) def display_domain_limits(sender, user, domain, **kwargs): """Display resources usage for domain.""" - if parameters.get_admin("ENABLE_DOMAIN_LIMITS") == "no": + if not param_tools.get_global_parameter("enable_domain_limits"): return [] return [{ "column": "right", @@ -87,7 +86,7 @@ def display_domain_limits(sender, user, domain, **kwargs): def display_admin_limits(sender, user, account, **kwargs): """Display resources usage for admin.""" condition = ( - parameters.get_admin("ENABLE_ADMIN_LIMITS") == "yes" and + param_tools.get_global_parameter("enable_admin_limits") and account.role in ["DomainAdmins", "Resellers"] ) if not condition: diff --git a/modoboa/limits/tests/test_api.py b/modoboa/limits/tests/test_api.py index 5d7c99a7d..687df663d 100644 --- a/modoboa/limits/tests/test_api.py +++ b/modoboa/limits/tests/test_api.py @@ -9,7 +9,6 @@ from modoboa.admin.factories import populate_database from modoboa.admin.models import Domain from modoboa.core import factories as core_factories from modoboa.core.models import User -from modoboa.lib import parameters from modoboa.lib import tests as lib_tests from .. import utils @@ -23,8 +22,9 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): """Create test data.""" super(APIAdminLimitsTestCase, cls).setUpTestData() for name, tpl in utils.get_user_limit_templates(): - parameters.save_admin( - "DEFLT_USER_{}_LIMIT".format(name.upper()), 2) + cls.localconfig.parameters.set_value( + "deflt_user_{0}_limit".format(name), 2) + cls.localconfig.save() populate_database() cls.user = User.objects.get(username="admin@test.com") cls.da_token = Token.objects.create(user=cls.user) @@ -181,10 +181,12 @@ class APIDomainLimitsTestCase(lib_tests.ModoAPITestCase): def setUpTestData(cls): """Create test data.""" super(APIDomainLimitsTestCase, cls).setUpTestData() - parameters.save_admin("ENABLE_DOMAIN_LIMITS", "yes") + cls.localconfig.parameters.set_value( + "enable_domain_limits", True) for name, tpl in utils.get_domain_limit_templates(): - parameters.save_admin( - "DEFLT_DOMAIN_{}_LIMIT".format(name.upper()), 2) + cls.localconfig.parameters.set_value( + "deflt_domain_{0}_limit".format(name), 2) + cls.localconfig.save() populate_database() def test_mailboxes_limit(self): diff --git a/modoboa/limits/tests/test_domain_limits.py b/modoboa/limits/tests/test_domain_limits.py index 3552b22fb..214145c63 100644 --- a/modoboa/limits/tests/test_domain_limits.py +++ b/modoboa/limits/tests/test_domain_limits.py @@ -7,7 +7,6 @@ from modoboa.admin import factories as admin_factories from modoboa.admin.models import Domain from modoboa.core import factories as core_factories from modoboa.core.models import User -from modoboa.lib import parameters from modoboa.lib import tests as lib_tests from .. import utils @@ -20,11 +19,14 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): def setUpTestData(cls): """Create test data.""" super(DomainLimitsTestCase, cls).setUpTestData() - parameters.save_admin("ENABLE_ADMIN_LIMITS", "no") - parameters.save_admin("ENABLE_DOMAIN_LIMITS", "yes") + cls.localconfig.parameters.set_values({ + "enable_admin_limits": False, + "enable_domain_limits": True + }) for name, tpl in utils.get_domain_limit_templates(): - parameters.save_admin( - "DEFLT_DOMAIN_{}_LIMIT".format(name.upper()), 2) + cls.localconfig.parameters.set_value( + "deflt_domain_{0}_limit".format(name), 2) + cls.localconfig.save() admin_factories.populate_database() def test_set_limits(self): diff --git a/modoboa/limits/tests/test_import.py b/modoboa/limits/tests/test_import.py index e58139a1b..dc222f426 100644 --- a/modoboa/limits/tests/test_import.py +++ b/modoboa/limits/tests/test_import.py @@ -6,7 +6,6 @@ from modoboa.admin import factories as admin_factories from modoboa.admin import models as admin_models from modoboa.core import factories as core_factories from modoboa.core import models as core_models -from modoboa.lib import parameters from modoboa.lib.tests import ModoTestCase from .. import utils @@ -135,9 +134,11 @@ class UserLimitImportTestCase(LimitImportTestCase): @classmethod def setUpTestData(cls): """Create test data.""" + localconfig = core_models.LocalConfig.objects.first() for name, tpl in utils.get_user_limit_templates(): - parameters.save_admin( - "DEFLT_USER_{}_LIMIT".format(name.upper()), 2) + localconfig.parameters.set_value( + "deflt_user_{0}_limit".format(name), 2) + localconfig.save() super(UserLimitImportTestCase, cls).setUpTestData() def test_domains_import(self): @@ -192,11 +193,15 @@ class DomainLimitImportTestCase(LimitImportTestCase): @classmethod def setUpTestData(cls): """Create test data.""" - parameters.save_admin("ENABLE_DOMAIN_LIMITS", "yes") - parameters.save_admin("ENABLE_ADMIN_LIMITS", "no") + localconfig = core_models.LocalConfig.objects.first() + localconfig.parameters.set_values({ + "enable_domain_limits": True, + "enable_admin_limits": False + }) for name, tpl in utils.get_domain_limit_templates(): - parameters.save_admin( - "DEFLT_DOMAIN_{}_LIMIT".format(name.upper()), 2) + localconfig.parameters.set_value( + "deflt_domain_{0}_limit".format(name), 2) + localconfig.save() super(DomainLimitImportTestCase, cls).setUpTestData() mb = admin_factories.MailboxFactory( user__username="user@test4.com", diff --git a/modoboa/limits/tests/test_user_limits.py b/modoboa/limits/tests/test_user_limits.py index 98ccf4e2c..494482abf 100644 --- a/modoboa/limits/tests/test_user_limits.py +++ b/modoboa/limits/tests/test_user_limits.py @@ -7,7 +7,6 @@ from modoboa.admin.factories import populate_database from modoboa.admin.models import Alias, Domain from modoboa.core.factories import UserFactory from modoboa.core.models import User -from modoboa.lib import parameters from modoboa.lib import tests as lib_tests from .. import utils @@ -48,13 +47,14 @@ class ResourceTestCase(lib_tests.ModoTestCase): def setUpTestData(cls): """Custom setUpTestData method.""" super(ResourceTestCase, cls).setUpTestData() - parameters.save_admin("ENABLE_ADMIN_LIMITS", "yes") - parameters.save_admin("ENABLE_DOMAIN_LIMITS", "no") + cls.localconfig.parameters.set_values({ + "enable_admin_limits": True, + "enable_domain_limits": False + }) for name, tpl in utils.get_user_limit_templates(): - parameters.save_admin( - "DEFLT_USER_{0}_LIMIT".format(name.upper()), 2, - app="limits" - ) + cls.localconfig.parameters.set_value( + "deflt_user_{0}_limit".format(name), 2) + cls.localconfig.save() populate_database() def _create_account(self, username, role='SimpleUsers', status=200): @@ -78,13 +78,15 @@ class ResourceTestCase(lib_tests.ModoTestCase): def _create_domain(self, name, status=200, withtpl=False): values = { - "name": name, "quota": 100, "create_dom_admin": "no", - "create_aliases": "no", "stepid": 'step3', "type": "domain" + "name": name, "quota": 100, "create_dom_admin": False, + "create_aliases": False, "stepid": "step3", "type": "domain" } if withtpl: - values['create_dom_admin'] = 'yes' - values['dom_admin_username'] = 'admin' - values['create_aliases'] = 'yes' + values.update({ + "create_dom_admin": True, + "dom_admin_username": "admin", + "create_aliases": True + }) response = self.ajax_post( reverse("admin:domain_add"), values, status ) diff --git a/modoboa/parameters/__init__.py b/modoboa/parameters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/parameters/apps.py b/modoboa/parameters/apps.py new file mode 100644 index 000000000..cbf060639 --- /dev/null +++ b/modoboa/parameters/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class ParametersConfig(AppConfig): + name = 'parameters' diff --git a/modoboa/parameters/forms.py b/modoboa/parameters/forms.py new file mode 100644 index 000000000..21243fe77 --- /dev/null +++ b/modoboa/parameters/forms.py @@ -0,0 +1,107 @@ +"""Parameters forms.""" + +from django import forms + +from modoboa.lib import events +from modoboa.lib import db_utils +from modoboa.lib import form_utils + + +class GenericParametersForm(forms.Form): + """Base class for parameter forms. + + Each extension has the possibility to define global parameters. + """ + app = None + visibility_rules = None + + def __init__(self, *args, **kwargs): + """Constructor.""" + if self.app is None: + raise NotImplementedError + + kwargs["prefix"] = self.app + load_values_from_db = kwargs.pop("load_values_from_db", True) + super(GenericParametersForm, self).__init__(*args, **kwargs) + + self.visirules = {} + if self.visibility_rules is not None: + for key, rule in self.visibility_rules.items(): + field, value = rule.split("=") + visibility = { + "field": "id_%s-%s" % (self.app, field), "value": value + } + self.visirules["%s-%s" % (self.app, key)] = visibility + + if not args and load_values_from_db: + self._load_initial_values() + + def _load_initial_values(self): + raise NotImplementedError + + @staticmethod + def has_access(**kwargs): + return True + + def save(self): + raise NotImplementedError + + +class AdminParametersForm(GenericParametersForm): + """Base form to declare admin level parameters.""" + + def __init__(self, *args, **kwargs): + """Store LocalConfig instance.""" + self.localconfig = kwargs.pop("localconfig", None) + super(AdminParametersForm, self).__init__(*args, **kwargs) + + def _load_initial_values(self): + """Load form initial values from database.""" + condition = ( + not db_utils.db_table_exists("core_localconfig") or + not self.localconfig) + if condition: + return + values = self.localconfig.parameters.get_values(app=self.app) + for key, value in values: + self.fields[key].initial = value + + def save(self): + """Save parameters to database.""" + parameters = {} + for name, value in self.cleaned_data.items(): + if type(self.fields[name]) is form_utils.SeparatorField: + continue + parameters[name] = value + self.localconfig.parameters.set_values(parameters, app=self.app) + + def to_django_settings(self): + """Inject parameters into django settings module.""" + pass + + +class UserParametersForm(GenericParametersForm): + """Base form to declare user level parameters.""" + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user", None) + super(UserParametersForm, self).__init__(*args, **kwargs) + + def _load_initial_values(self): + """Load initial values from User instance.""" + if not db_utils.db_table_exists("core_localconfig"): + return + if self.user is None: + return + parameters = self.user.parameters.get_values(self.app) + for parameter, value in parameters: + self.fields[parameter].initial = value + + def save(self): + """Save new values.""" + parameters = {} + for name, value in self.cleaned_data.items(): + if type(self.fields[name]) is form_utils.SeparatorField: + continue + parameters[name] = value + self.user.parameters.set_values(parameters, app=self.app) diff --git a/modoboa/templates/common/parameters.html b/modoboa/parameters/templates/parameters/base_form.html similarity index 76% rename from modoboa/templates/common/parameters.html rename to modoboa/parameters/templates/parameters/base_form.html index 040ec24f6..381151c24 100644 --- a/modoboa/templates/common/parameters.html +++ b/modoboa/parameters/templates/parameters/base_form.html @@ -1,7 +1,7 @@ {% load form_tags %}
{% for form in forms %} diff --git a/modoboa/parameters/tests.py b/modoboa/parameters/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/modoboa/parameters/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/modoboa/parameters/tools.py b/modoboa/parameters/tools.py new file mode 100644 index 000000000..9205e81bd --- /dev/null +++ b/modoboa/parameters/tools.py @@ -0,0 +1,203 @@ +"""Parameters management.""" + +import copy + +from modoboa.lib import exceptions +from modoboa.lib import form_utils +from modoboa.lib import signals +from modoboa.lib.sysutils import guess_extension_name + + +class NotDefined(exceptions.ModoboaException): + """Custom exception for undefined parameters.""" + + http_code = 404 + + def __init__(self, app, name=None): + self.app = app + self.name = name + + def __str__(self): + if self.name is None: + return "Application {} not registered".format(self.app) + return "Parameter {} not defined for app {}".format( + self.name, self.app) + + +class Registry(object): + """A registry for parameters.""" + + def __init__(self): + """Constructor.""" + self._registry = { + "global": {}, + "user": {} + } + + def add(self, level, formclass, label): + """Add a new class containing parameters.""" + self._registry[level][formclass.app] = { + "label": label, "formclass": formclass, "defaults": {} + } + + def _load_default_values(self, level): + """Load default values.""" + for app, data in self._registry[level].items(): + form = data["formclass"](load_values_from_db=False) + for name, field in form.fields.items(): + if type(field) is form_utils.SeparatorField: + continue + data["defaults"][name] = field.initial + + def get_forms(self, level, *args, **kwargs): + """Return form instances for all app of the given level.""" + sorted_apps = [] + first_app = kwargs.pop("first_app", "core") + if first_app in self._registry[level]: + sorted_apps.append(first_app) + sorted_apps += sorted( + [a for a in self._registry[level].keys() if a != first_app], + key=lambda a: self._registry[level][a]["label"] + ) + result = [] + for app in sorted_apps: + data = self._registry[level][app] + if not data["formclass"].has_access(**kwargs): + continue + result.append({ + "app": app, + "label": data["label"], + "form": data["formclass"](*args, **kwargs) + }) + return result + + def exists(self, level, app, parameter=None): + """Check if parameter exists.""" + parameters = self._registry[level] + result = app in parameters + if parameter: + result = result and parameter in parameters[app]["defaults"] + return result + + def get_default(self, level, app, parameter): + """Retrieve default value for parameter.""" + if app not in self._registry[level]: + raise NotDefined(app) + if parameter not in self._registry[level][app]["defaults"]: + raise NotDefined(app, parameter) + return self._registry[level][app]["defaults"][parameter] + + def get_defaults(self, level, app): + """Retrieve default values for application.""" + if app not in self._registry[level]: + raise NotDefined(app) + return self._registry[level][app]["defaults"] + +registry = Registry() + + +class Manager(object): + """A manager for parameters.""" + + def __init__(self, level, parameters): + """Constructor.""" + self._level = level + self._parameters = parameters + registry._load_default_values(level) + + def get_value(self, parameter, app=None, raise_exception=True): + """Return the value associated to the specified parameter.""" + if app is None: + app = guess_extension_name() + # Compat. with the old way... + parameter = parameter.lower() + try: + default = registry.get_default(self._level, app, parameter) + except NotDefined: + if raise_exception: + raise + return None + if app in self._parameters: + return self._parameters[app][parameter] + return default + + def get_values(self, app=None): + """Return all values for the given level/app.""" + if app is None: + app = guess_extension_name() + values = registry.get_defaults(self._level, app) + for parameter, value in values.iteritems(): + if app in self._parameters: + value = self._parameters[app].get(parameter, value) + yield (parameter, value) + + def set_value(self, parameter, value, app=None): + """Set parameter for the given app.""" + if app is None: + app = guess_extension_name() + if not registry.exists(self._level, app, parameter): + raise NotDefined(app, parameter) + if app not in self._parameters: + self._parameters[app] = copy.deepcopy( + registry.get_defaults(self._level, app)) + self._parameters[app][parameter] = value + + def set_values(self, values, app=None): + """Set/update values for the given app.""" + if app is None: + app = guess_extension_name() + if not registry.exists(self._level, app): + raise NotDefined(app) + if app not in self._parameters: + self._parameters[app] = copy.deepcopy( + registry.get_defaults(self._level, app)) + self._parameters[app].update(values) + + +def get_localconfig(): + """Retrieve current LocalConfig instance.""" + from modoboa.core import models as core_models + request = signals.get_request() + if request: + return request.localconfig + return core_models.LocalConfig.objects.first() + + +def get_global_parameter(name, app=None, **kwargs): + """Retrieve a global parameter. + + This is a shortcut to use when a LocalConfig instance is not + available (ie. when no request object is available). + + A ``NotDefined`` exception if the parameter doesn't exist. + + :param name: the parameter's name + :param app: the application owning the parameter + :return: the corresponding value + + """ + if app is None: + app = guess_extension_name() + return get_localconfig().parameters.get_value(name, app=app, **kwargs) + + +def get_global_parameters(app, **kwargs): + """Retrieve global parameters of a given app. + + This is a shortcut to use when a LocalConfig instance is not + available (ie. when no request object is available). + + A ``NotDefined`` exception if the parameter doesn't exist. + + :param name: the parameter's name + :param app: the application owning the parameter + :return: the corresponding value + + """ + return get_localconfig().parameters.get_values(app, **kwargs) + + +def apply_to_django_settings(): + """Apply global parameters to Django settings module.""" + for form in registry.get_forms("global"): + form["form"].to_django_settings() diff --git a/modoboa/relaydomains/app_settings.py b/modoboa/relaydomains/app_settings.py index cb308249f..a6cf7fb4d 100644 --- a/modoboa/relaydomains/app_settings.py +++ b/modoboa/relaydomains/app_settings.py @@ -1,16 +1,13 @@ -""" -Relay domains settings definition. -""" +"""Relay domains settings definition.""" from django import forms from django.utils.translation import ugettext_lazy -from modoboa.lib import parameters from modoboa.lib.form_utils import SeparatorField +from modoboa.parameters import forms as param_forms -class AdminParametersForm(parameters.AdminParametersForm): - +class AdminParametersForm(param_forms.AdminParametersForm): """Admin parameters definition.""" app = "relaydomains" diff --git a/modoboa/relaydomains/apps.py b/modoboa/relaydomains/apps.py index 2a8895b46..433f5d770 100644 --- a/modoboa/relaydomains/apps.py +++ b/modoboa/relaydomains/apps.py @@ -1,7 +1,7 @@ """AppConfig for relaydomains.""" from django.apps import AppConfig - +from django.utils.translation import ugettext as _ EVENTS = [ "RelayDomainCreated", @@ -16,11 +16,11 @@ EVENTS = [ def load_relaydomains_settings(): """Load application settings.""" - from django.utils.translation import ugettext as _ - from modoboa.lib import events, parameters + from modoboa.parameters import tools as param_tools + from modoboa.lib import events from .app_settings import AdminParametersForm - parameters.register(AdminParametersForm, _("Relay domains")) + param_tools.registry.add("global", AdminParametersForm, _("Relay domains")) events.declare(EVENTS) from . import general_callbacks diff --git a/modoboa/relaydomains/models.py b/modoboa/relaydomains/models.py index 8beb5e1fa..e89d528f2 100644 --- a/modoboa/relaydomains/models.py +++ b/modoboa/relaydomains/models.py @@ -7,8 +7,8 @@ from django.utils.translation import ugettext as _, ugettext_lazy from reversion import revisions as reversion from modoboa.admin import models as admin_models -from modoboa.lib import parameters from modoboa.lib.exceptions import BadRequest +from modoboa.parameters import tools as param_tools class ServiceManager(Manager): @@ -20,7 +20,7 @@ class ServiceManager(Manager): entries are saved and outdated ones (ie. present in the database but not in the file) are removed. """ - with open(parameters.get_admin('MASTER_CF_PATH')) as fp: + with open(param_tools.get_global_parameter("master_cf_path")) as fp: content = fp.read() services = [] for line in content.split('\n'): diff --git a/modoboa/relaydomains/tests.py b/modoboa/relaydomains/tests.py index 6e008491e..695e62a6c 100644 --- a/modoboa/relaydomains/tests.py +++ b/modoboa/relaydomains/tests.py @@ -6,7 +6,6 @@ from django.test import TestCase from modoboa.admin import factories as admin_factories from modoboa.admin import models as admin_models from modoboa.core.factories import UserFactory -from modoboa.lib import parameters from modoboa.lib.tests import ModoTestCase from modoboa.lib.test_utils import MapFilesTestCaseMixin from modoboa.limits import utils as limits_utils @@ -20,7 +19,7 @@ class Operations(object): def _create_relay_domain(self, name, status=200): srv, created = Service.objects.get_or_create(name='dummy') values = { - "name": name, "create_dom_admin": "no", "type": "relaydomain", + "name": name, "create_dom_admin": False, "type": "relaydomain", "target_host": "external.host.tld", "service": srv.id, "enabled": True, "stepid": "step3" } @@ -213,15 +212,18 @@ class RelayDomainsTestCase(ModoTestCase, Operations): "enabled": True } self.ajax_post(reverse("admin:alias_add"), values) - alias = admin_models.Alias.objects.get(address="alias@relaydomain.tld") + self.assertTrue( + admin_models.Alias.objects.filter( + address="alias@relaydomain.tld").exists()) values = { "address": "alias2@relaydomain.tld", "recipients": "local@relaydomain.tld", "enabled": True } self.ajax_post(reverse("admin:alias_add"), values) - alias = admin_models.Alias.objects.get( - address="alias2@relaydomain.tld") + self.assertTrue( + admin_models.Alias.objects.filter( + address="alias2@relaydomain.tld").exists()) class LimitsTestCase(ModoTestCase, Operations): @@ -230,11 +232,10 @@ class LimitsTestCase(ModoTestCase, Operations): def setUpTestData(cls): """Create test data.""" super(LimitsTestCase, cls).setUpTestData() - for name, tpl in limits_utils.get_user_limit_templates(): - parameters.save_admin( - "DEFLT_USER_{0}_LIMIT".format(name.upper()), 2, app="limits" - ) + cls.localconfig.parameters.set_value( + "deflt_user_{0}_limit".format(name), 2, app="limits") + cls.localconfig.save() cls.user = UserFactory.create( username='reseller', groups=('Resellers',) ) diff --git a/modoboa/static/js/twocols_nav.js b/modoboa/static/js/twocols_nav.js index c7dda2be5..85d164401 100644 --- a/modoboa/static/js/twocols_nav.js +++ b/modoboa/static/js/twocols_nav.js @@ -3,6 +3,9 @@ * * @constructor */ + +/* global Listing */ + var TwocolsNav = function(options) { Listing.call(this, options); }; @@ -33,6 +36,17 @@ TwocolsNav.prototype = { success_cb: $.proxy(this.save_cb, this) }); }, this)); + $(document).on('submit', '#parameters_form', function(evt) { + simple_ajax_form_post(evt, { + formid: $(this).attr('id'), + modal: false, + reload_on_success: false, + success_cb: $.proxy(this.save_cb, this), + error_cb: function(data) { + $('a[data-app="' + data.prefix + '"]').tab('show'); + } + }); + }); }, update_content: function(data) { diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py index e4ca61d1d..f0e525d0b 100644 --- a/test_project/test_project/settings.py +++ b/test_project/test_project/settings.py @@ -60,6 +60,7 @@ MODOBOA_APPS = ( 'modoboa.admin', 'modoboa.limits', 'modoboa.relaydomains', + 'modoboa.parameters', ) INSTALLED_APPS += MODOBOA_APPS @@ -75,6 +76,7 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'modoboa.core.middleware.LocalConfigMiddleware', 'modoboa.lib.middleware.AjaxLoginRedirect', 'modoboa.lib.middleware.CommonExceptionCatcher', 'modoboa.lib.middleware.RequestCatcherMiddleware',