diff --git a/MANIFEST.in b/MANIFEST.in index 38f115085..02a707cf3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include README.rst COPYING requirements.txt recursive-include modoboa *.html *.js *.css *.png *.po *.mo *.json *.gif recursive-include modoboa/core/templates *.tpl -recursive-include doc * \ No newline at end of file +recursive-include modoboa/core/commands/templates *.tpl +recursive-include doc * diff --git a/doc/getting_started/install.rst b/doc/getting_started/install.rst index 63dd59bfb..8a5afff43 100644 --- a/doc/getting_started/install.rst +++ b/doc/getting_started/install.rst @@ -94,7 +94,7 @@ Deployment `modoboa-admin.py`, a command line tool, let you deploy a *ready-to-use* Modoboa site using only one instruction:: - $ modoboa-admin.py deploy modoboa_example --syncdb --collectstatic [--with-amavis] + $ modoboa-admin.py deploy modoboa_example --syncdb --collectstatic [--with-amavis] [--dburl database-url] [--amavis_dburl database-url] Just answer the few questions and you're done. You can now go to the :ref:`first_use` section. @@ -104,6 +104,31 @@ Just answer the few questions and you're done. You can now go to the The `--with-amavis` option must be set only if you intend to use the :ref:`amavis_frontend`. + +In case you need a **silent installation**, e.g. if you're using Salt-Stack. +It's possible to supply the database credentials as commandline arguments. + +.. note:: + + `--dburl database-url` for the modoboa database credentials + `--amavis_dburl database-url` for the amavis database credentials + + Your database-url should meet the following syntax: + ``scheme://[user:pass@][host:port]/dbname`` + + **or** + + ``sqlite:////full/path/to/your/database/file.sqlite`` + + Available `schemes` are: + * postgres + * postgresql + * postgis + * mysql + * mysql2 + * sqlite + + .. note:: If you plan to serve Modoboa using a URL prefix, you must change the diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index 2c90a9fe5..0eef997d5 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -25,6 +25,8 @@ variable:: Then, edit the ``inbox`` namespace and add the following lines:: + inbox = yes + mailbox Drafts { auto = create special_use = \Drafts @@ -111,7 +113,7 @@ content inside:: # Path for SQL configuration file, see example-config/dovecot-sql.conf.ext args = /etc/dovecot/dovecot-sql.conf.ext } - + userdb sql { driver = sql args = /etc/dovecot/dovecot-sql.conf.ext @@ -149,7 +151,7 @@ PostgreSQL users :: - driver = postgresql + driver = pgsql connect = host= dbname= user= password= @@ -213,7 +215,7 @@ proper permissions:: unix_listener dict { mode = 0600 user = - #group = + #group = } } @@ -392,12 +394,12 @@ Finally, configure the ``sieve`` plugin by editing the :file:`conf.d/90-sieve.conf` file. Put the follwing caontent inside:: plugin { - # Location of the active script. When ManageSieve is used this is actually + # Location of the active script. When ManageSieve is used this is actually # a symlink pointing to the active script in the sieve storage directory. sieve = ~/.dovecot.sieve # - # The path to the directory where the personal Sieve scripts are stored. For + # The path to the directory where the personal Sieve scripts are stored. For # ManageSieve this is where the uploaded scripts are stored. sieve_dir = ~/sieve } @@ -440,12 +442,12 @@ Use the following configuration in the :file:`/etc/postfix/main.cf` file virtual_transport = dovecot dovecot_destination_recipient_limit = 1 - virtual_minimum_uid = + virtual_minimum_uid = virtual_gid_maps = static: virtual_uid_maps = static: virtual_mailbox_base = /var/vmail - relay_domains = + relay_domains = virtual_mailbox_domains = :/etc/postfix/sql-domains.cf virtual_alias_domains = :/etc/postfix/sql-domain-aliases.cf virtual_mailbox_maps = :/etc/postfix/sql-mailboxes.cf @@ -455,7 +457,7 @@ Use the following configuration in the :file:`/etc/postfix/main.cf` file smtpd_recipient_restrictions = ... - check_recipient_access :/etc/postfix/maps/sql-maintain.cf + check_recipient_access :/etc/postfix/sql-maintain.cf permit_mynetworks ... diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index 751e84e47..5e010631a 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -60,4 +60,4 @@ def unset_default_topredirection(extension): """ topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION") if topredirection == extension.name: - parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs") + parameters.save_admin("DEFAULT_TOP_REDIRECTION", "core") diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 9acfafae5..edb9bfe48 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -17,12 +17,16 @@ def enabled_applications(): :return: a list """ from modoboa.core.models import Extension + from modoboa.core.extensions import exts_pool from modoboa.lib.dbutils import db_table_exists - result = [("core", "core")] + result = [("user", "user")] if db_table_exists("core_extension"): exts = Extension.objects.filter(enabled=True) - result += [(ext.name, ext.name) for ext in exts] + for ext in exts: + extclass = exts_pool.get_extension(ext.name) + if extclass.available_for_topredirection: + result.append((ext.name, ext.name)) return sorted(result, key=lambda e: e[0]) @@ -98,7 +102,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): label=ugettext_lazy("Bind password"), initial='', help_text=ugettext_lazy("The password to use when binding to the LDAP server (with 'Bind DN')"), - widget=forms.PasswordInput, + widget=forms.PasswordInput(render_value=True), required=False ) @@ -152,7 +156,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): default_top_redirection = forms.ChoiceField( label=ugettext_lazy("Default top redirection"), choices=[], - initial="core", + initial="user", help_text=ugettext_lazy("The default redirection used when no application is specified") ) @@ -176,6 +180,13 @@ class GeneralParametersForm(parameters.AdminParametersForm): super(GeneralParametersForm, self).__init__(*args, **kwargs) self.fields["default_top_redirection"].choices = enabled_applications() + def clean_secret_key(self): + if len(self.cleaned_data["secret_key"]) not in [16, 24, 32]: + raise forms.ValidationError( + _("Key must be either 16, 24, or 32 bytes long") + ) + return self.cleaned_data["secret_key"] + def clean_ldap_user_dn_template(self): tpl = self.cleaned_data["ldap_user_dn_template"] try: diff --git a/modoboa/core/commands/__init__.py b/modoboa/core/commands/__init__.py index e7ef1ae9e..9b2b1c9f2 100644 --- a/modoboa/core/commands/__init__.py +++ b/modoboa/core/commands/__init__.py @@ -19,7 +19,7 @@ class Command(object): self._verbose = verbose if not settings.configured: settings.configure() - self._templates_dir = "%s/commands/templates" % os.path.dirname(__file__) + self._templates_dir = "%s/templates" % os.path.dirname(__file__) def _render_template(self, tplfile, env): fp = open(tplfile) diff --git a/modoboa/core/commands/deploy.py b/modoboa/core/commands/deploy.py index 91c90341e..5d10ad69b 100644 --- a/modoboa/core/commands/deploy.py +++ b/modoboa/core/commands/deploy.py @@ -4,19 +4,21 @@ import os import shutil import getpass import subprocess +import dj_database_url + from django.core import management from django.template import Context, Template from modoboa.lib.sysutils import exec_cmd -from . import Command +from modoboa.core.commands import Command dbconn_tpl = """ '{{ conn_name }}': { - 'ENGINE': 'django.db.backends.{{ dbtype }}', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': '{{ dbname }}', # Or path to database file if using sqlite3. - 'USER': '{{ username }}', # Not used with sqlite3. - 'PASSWORD': '{{ password }}', # Not used with sqlite3. - 'HOST': '{{ dbhost }}', # Set to empty string for localhost. Not used with sqlite3. - 'PORT': '', # Set to empty string for default. Not used with sqlite3.{% if dbtype == 'mysql' %} + 'ENGINE': '{{ ENGINE }}', + 'NAME': '{{ NAME }}', # Or path to database file if using sqlite3. + 'USER': '{% if USER %}{{ USER }}{% endif %}', # Not used with sqlite3. + 'PASSWORD': '{% if PASSWORD %}{{ PASSWORD }}{% endif %}', # Not used with sqlite3. + 'HOST': '{% if HOST %}{{ HOST }}{% endif %}', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '{% if PORT %}{{ PORT }}{% endif %}', # Set to empty string for default. Not used with sqlite3.{% if ENGINE == 'django.db.backends.mysql' %} 'OPTIONS' : { "init_command" : 'SET foreign_key_checks = 0;', },{% endif %} @@ -43,6 +45,16 @@ class DeployCommand(Command): '--collectstatic', action='store_true', default=False, help='Run django collectstatic command' ) + self._parser.add_argument( + '--dburl', type=str, nargs=1, default=None, + help='The database-url for your modoboa instance') + self._parser.add_argument( + '--amavis_dburl', type=str, nargs=1, default=None, + help='The database-url for your amavis instance') + self._parser.add_argument( + '--domain', type=str, nargs=1, default=None, + help='The domain under which you want to deploy modoboa') + def _exec_django_command(self, name, cwd, *args): """Run a django command for the freshly created project @@ -76,22 +88,29 @@ class DeployCommand(Command): """ print "Configuring database connection: %s" % name info = {'conn_name': name, - 'dbtype': raw_input('Database type (mysql, postgres or sqlite3): ')} - if info['dbtype'] not in ['mysql', 'postgres', 'sqlite3']: - info['dbtype'] = 'mysql' - if info['dbtype'] == 'postgres': - info['dbtype'] = 'postgresql_psycopg2' - if info['dbtype'] == 'sqlite3': - info['dbname'] = '%s.db' % name + 'ENGINE': raw_input('Database type (mysql, postgres or sqlite3): ')} + if info['ENGINE'] not in ['mysql', 'postgres', 'sqlite3']: + raise RuntimeError('Unsupported database engine') + + if info['ENGINE'] == 'sqlite3': + info['ENGINE'] = 'django.db.backends.sqlite3' + info['NAME'] = '%s.db' % name + return info + if info['ENGINE'] == 'postgres': + info['ENGINE'] = 'django.db.backends.postgresql_psycopg2' + default_port = 5432 else: - default_host = 'localhost' if info['dbtype'] == 'mysql' else '' - info['dbhost'] = \ - raw_input("Database host (default: '%s'): " % default_host) - if info['dbhost'] == '': - info['dbhost'] = default_host - info['dbname'] = raw_input('Database name: ') - info['username'] = raw_input('Username: ') - info['password'] = getpass.getpass('Password: ') + info['ENGINE'] = 'django.db.backends.mysql' + default_port = 3306 + info['HOST'] = raw_input("Database host (default: 'localhost'): ") + info['PORT'] = raw_input("Database port (default: '%s'): " % default_port) + # leave port setting empty, if default value is supplied and + # leave it to django + if info['PORT'] == default_port: + info['PORT'] = '' + info['NAME'] = raw_input('Database name: ') + info['USER'] = raw_input('Username: ') + info['PASSWORD'] = getpass.getpass('Password: ') return info def handle(self, parsed_args): @@ -109,13 +128,41 @@ class DeployCommand(Command): django14 = False t = Template(dbconn_tpl) - default_conn = t.render(Context(self.ask_db_info())) - amavis_conn = t.render(Context(self.ask_db_info('amavis'))) \ - if parsed_args.with_amavis else None - allowed_host = raw_input( - 'Under which domain do you want to deploy modoboa? ' - ) + if parsed_args.dburl: + info = dj_database_url.config(default=parsed_args.dburl[0]) + # In case the user fails to supply a valid database url, fallback to manual mode + if not info: + print "There was a problem with your database-url. \n" + info = self.ask_db_info() + #If we set this earlier, our fallback method will never be triggered + info['conn_name'] = 'default' + else: + info = self.ask_db_info() + + default_conn = t.render(Context(info)) + + if parsed_args.with_amavis: + if parsed_args.amavis_dburl: + amavis_info = dj_database_url.config(default=parsed_args.amavis_dburl[0]) + # In case the user fails to supply a valid database url, fallback to manual mode + if not amavis_info: + amavis_info = self.ask_db_info('amavis') + #If we set this earlier, our fallback method will never be triggered + amavis_info['conn_name'] = 'amavis' + else: + amavis_info = self.ask_db_info('amavis') + + amavis_conn = t.render(Context(amavis_info)) + else: + amavis_conn = None + + if parsed_args.domain: + allowed_host = parsed_args.domain[0] + else: + allowed_host = raw_input( + 'Under which domain do you want to deploy modoboa? ' + ) mod = __import__(parsed_args.name, globals(), locals(), ['settings']) tpl = self._render_template( @@ -126,15 +173,15 @@ class DeployCommand(Command): 'allowed_host': allowed_host } ) - fp = open("%s/settings.py" % path, "w") - fp.write(tpl) - fp.close() + with open("%s/settings.py" % path, "w") as fp: + fp.write(tpl) shutil.copyfile( "%s/urls.py.tpl" % self._templates_dir, "%s/urls.py" % path ) os.mkdir("%s/media" % path) if parsed_args.syncdb: + os.unlink("%s/settings.pyc" % path) self._exec_django_command( "syncdb", parsed_args.name, '--noinput' ) diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index c46ddd6f7..f86ad1e39 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -159,7 +159,6 @@ MODOBOA_APPS = ( 'modoboa.extensions.webmail', 'modoboa.extensions.stats', 'modoboa.extensions.sievefilters', - 'modoboa.extensions.stats', {% if not amavis_conn %}#{% endif %}'modoboa.extensions.amavis', ) @@ -167,6 +166,8 @@ INSTALLED_APPS += MODOBOA_APPS AUTH_USER_MODEL = 'core.User' +SOUTH_TESTS_MIGRATE = False + #MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png") AUTHENTICATION_BACKENDS = ( diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py index b1f81e9d8..fe11435c7 100644 --- a/modoboa/core/extensions.py +++ b/modoboa/core/extensions.py @@ -15,6 +15,7 @@ class ModoExtension(object): url = None needs_media = False always_active = False + available_for_topredirection = False def infos(self): return dict( diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index f4d132d9e..67eb3f973 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -35,7 +35,7 @@ def dologin(request): nextlocation = request.POST.get("next", None) if nextlocation is None or nextlocation == "None": - if user.group != "SimpleUsers": + if user.group == "SimpleUsers": nextlocation = reverse("modoboa.lib.webutils.topredirection") else: nextlocation = reverse("domains") diff --git a/modoboa/extensions/amavis/__init__.py b/modoboa/extensions/amavis/__init__.py index 1327288ae..4378ae222 100644 --- a/modoboa/extensions/amavis/__init__.py +++ b/modoboa/extensions/amavis/__init__.py @@ -20,6 +20,7 @@ class Amavis(ModoExtension): version = "1.0" description = ugettext_lazy("Simple amavis management frontend") url = "quarantine" + available_for_topredirection = True def init(self): """Init function diff --git a/modoboa/extensions/sievefilters/__init__.py b/modoboa/extensions/sievefilters/__init__.py index 76cade1f7..50ef1ebe9 100644 --- a/modoboa/extensions/sievefilters/__init__.py +++ b/modoboa/extensions/sievefilters/__init__.py @@ -11,6 +11,7 @@ class SieveFilters(ModoExtension): version = "1.0" description = ugettext_lazy("Plugin to easily create server-side filters") url = "sfilters" + available_for_topredirection = True def load(self): from .app_settings import ParametersForm, UserSettings diff --git a/modoboa/extensions/webmail/__init__.py b/modoboa/extensions/webmail/__init__.py index 597f85375..b57e93b88 100644 --- a/modoboa/extensions/webmail/__init__.py +++ b/modoboa/extensions/webmail/__init__.py @@ -12,6 +12,7 @@ class Webmail(ModoExtension): version = "1.0" description = ugettext_lazy("Simple IMAP webmail") needs_media = True + available_for_topredirection = True def load(self): from .app_settings import ParametersForm, UserSettings diff --git a/modoboa/lib/fixtures/demo.json b/modoboa/lib/fixtures/demo.json index d155e29d7..046dd26f0 100644 --- a/modoboa/lib/fixtures/demo.json +++ b/modoboa/lib/fixtures/demo.json @@ -20,7 +20,7 @@ "model": "lib.parameter", "fields": { "name": "general.DEFAULT_TOP_REDIRECTION", - "value": "userprefs" + "value": "user" } }, { diff --git a/modoboa/lib/webutils.py b/modoboa/lib/webutils.py index 8723b50c6..b2da344a8 100644 --- a/modoboa/lib/webutils.py +++ b/modoboa/lib/webutils.py @@ -138,27 +138,21 @@ def size2integer(value): @login_required def topredirection(request): - """Simple view to redirect the request when no application is specified + """Simple view to redirect the request when no application is specified. The default "top redirection" can be specified in the *Admin > Settings* panel. It is the application that will be launched by default. Users that are not allowed to access this application will be redirected to the "User preferences" application. + This feature only applies to simple users. + :param request: a Request object """ from modoboa.lib import parameters from modoboa.core.extensions import exts_pool topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="core") - if not topredir in ["core"]: - infos = exts_pool.get_extension_infos(topredir) - path = infos["url"] if infos["url"] else infos["name"] - else: - path = "admin" # topredir - - if topredir in ["core", "stats"] and \ - request.user.belongs_to_group('SimpleUsers'): - path = "userprefs" - + infos = exts_pool.get_extension_infos(topredir) + path = infos["url"] if infos["url"] else infos["name"] return HttpResponseRedirect(path) diff --git a/modoboa/static/js/global.js b/modoboa/static/js/global.js index 6aaa76e11..7be4d4c63 100644 --- a/modoboa/static/js/global.js +++ b/modoboa/static/js/global.js @@ -42,7 +42,8 @@ function modalbox(e, css, defhref, defcb, defclosecb) { $div.modal({show: false}); $div.one('shown', function() { $(".help").popover({ - container: "#modalbox" + container: "#modalbox", + trigger: "hover" }).click(function(e) {e.preventDefault();}); if (modalcb != undefined) { if (typeof modalcb === "function") modalcb(); else eval(modalcb + '()'); @@ -308,5 +309,5 @@ $(document).ready(function() { $(document).on('click', 'a[data-toggle="ajaxmodal"]', modalbox); $(document).on('click', 'a[data-toggle="ajaxmodal-autowidth"]', modalbox_autowidth); $(document).on('click', '.activator', activate_widget); - $(".help").popover().click(function(e) {e.preventDefault();}); + $(".help").popover({trigger: 'hover'}).click(function(e) {e.preventDefault();}); }); \ No newline at end of file diff --git a/modoboa/static/js/twocols_nav.js b/modoboa/static/js/twocols_nav.js index 8c955c2f4..8d98e272d 100644 --- a/modoboa/static/js/twocols_nav.js +++ b/modoboa/static/js/twocols_nav.js @@ -45,7 +45,7 @@ TwocolsNav.prototype = { $('#' + this.options.divid + ' input[type=radio]').click($.proxy(this.radio_clicked, this)); $(".help").popover({ placement: 'bottom', - trigger: 'click' + trigger: 'hover' }).click(function(e) {e.preventDefault();}); $('#' + this.options.divid + ' select').change(); $('#' + this.options.divid + ' input[type=radio]:checked').click(); diff --git a/requirements.txt b/requirements.txt index 6b74c0566..d7ba96b5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ django django-reversion +dj-database-url south lxml pycrypto diff --git a/tests/install_from_scratch.py b/tests/install_from_scratch.py index 4d6800cec..50e03947c 100644 --- a/tests/install_from_scratch.py +++ b/tests/install_from_scratch.py @@ -1,19 +1,11 @@ # coding: utf-8 import os -import sys -import subprocess import tempfile import pexpect import shutil import unittest - - -def runcmd(cmd, **kwargs): - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - shell=True, **kwargs) - output = p.communicate()[0] - return p.returncode, output +from modoboa.lib.sysutils import exec_cmd class DeployTest(unittest.TestCase): @@ -25,19 +17,23 @@ class DeployTest(unittest.TestCase): def setUp(self): cmd = "mysqladmin -u %s -p%s create %s" % (self.dbuser, self.dbpassword, self.projname) - code, out = runcmd(cmd) + code, out = exec_cmd(cmd) self.assertEqual(code, 0) self.workdir = tempfile.mkdtemp() def tearDown(self): + path = os.path.join(self.workdir, self.projname) + code, output = exec_cmd("python manage.py test admin", cwd=path) + self.assertEqual(code, 0) + if hasattr(self, "workdir"): - shutil.rmtree(self.workdir) + shutil.rmtree(self.workdir) child = pexpect.spawn("mysqladmin -u root -p%s drop %s" % (self.dbpassword, self.projname)) child.expect("\[y/N\]") child.sendline("y") child.expect('Database "%s" dropped' % self.projname) - def test(self): + def test_standard(self): timeout = 2 cmd = "modoboa-admin.py deploy --syncdb --collectstatic %s" % self.projname child = pexpect.spawn(cmd, cwd=self.workdir) @@ -47,6 +43,8 @@ class DeployTest(unittest.TestCase): child.sendline(self.dbtype) child.expect("Database host \(default: 'localhost'\):", timeout=timeout) child.sendline(self.dbhost) + child.expect("Database port \(default: '3306'\):", timeout=timeout) + child.sendline('3306') child.expect("Database name:", timeout=timeout) child.sendline(self.projname) child.expect("Username:", timeout=timeout) @@ -59,8 +57,13 @@ class DeployTest(unittest.TestCase): fout.close() self.assertEqual(child.exitstatus, 0) - path = os.path.join(self.workdir, self.projname) - code, output = runcmd("python manage.py test admin", cwd=path) + def test_silent(self): + dburl = "%s://%s:%s@%s/%s" \ + % (self.dbtype, self.dbuser, self.dbpassword, + self.dbhost, self.projname) + cmd = "modoboa-admin.py deploy --syncdb --collectstatic --dburl %s --domain %s %s" \ + % (dburl, 'localhost', self.projname) + code, output = exec_cmd(cmd, cwd=self.workdir) self.assertEqual(code, 0)