From d583ecac6644de7b3655e256d4452ecb967e4c6c Mon Sep 17 00:00:00 2001 From: FEE1DE4D Date: Thu, 26 Jun 2014 03:55:17 +0200 Subject: [PATCH 01/22] Few spellchecks --- README.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index add7e40b2..d342343bf 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ and simplified Web User Interface. It provides useful components such as an administration panel or a webmail. Modoboa integrates with well known software such as `Postfix -`_ or `Dovecot `_. A SQL +`_ or `Dovecot `_. A SQL database (`MySQL `_, `PostgreSQL `_ or `SQLite `_) is used as a central point of communication between all components. @@ -18,8 +18,8 @@ Modoboa is developed with modularity in mind, expanding it is really easy. Actually, all current features are extensions. It is written in Python and uses the `Django -`_, `jQuery `_ and -`Bootstap, from Twitter `_ +`_, `jQuery `_ and +`Bootstrap, from Twitter `_ frameworks. ************* @@ -37,7 +37,7 @@ Main features Documentation ************* -A detailled `documentation `_ will help you +A detailed `documentation `_ will help you to install, use or extend Modoboa. ************* @@ -47,19 +47,19 @@ External code The following external libraries are provided with Modoboa: * `jQuery version 1.9.1 `_ -* `jQuery-UI 1.10+ `_ +* `jQuery-UI 1.10+ `_ * `Bootstrap, from Twitter version 2.3.2 `_ * `Bootstrap datetimepicker `_ -.. |latest-version| image:: https://pypip.in/v/modoboa/badge.png +.. |latest-version| image:: https://www.pypip.in/v/modoboa/badge.png :alt: Latest version on Pypi - :target: https://crate.io/packages/modoboa/ -.. |downloads| image:: https://pypip.in/d/modoboa/badge.png + :target: https://www.crate.io/packages/modoboa/ +.. |downloads| image:: https://www.pypip.in/d/modoboa/badge.png :alt: Downloads from Pypi - :target: https://crate.io/packages/modoboa/ -.. |travis| image:: https://travis-ci.org/tonioo/modoboa.png?branch=master - :target: https://travis-ci.org/tonioo/modoboa + :target: https://www.crate.io/packages/modoboa/ +.. |travis| image:: https://www.travis-ci.org/tonioo/modoboa.png?branch=master + :target: https://www.travis-ci.org/tonioo/modoboa .. image:: https://d2weczhvl823v0.cloudfront.net/tonioo/modoboa/trend.png :alt: Bitdeli badge - :target: https://bitdeli.com/free + :target: https://www.bitdeli.com/free From 3992898c50475388a941ff7b7cb22b506030676b Mon Sep 17 00:00:00 2001 From: FEE1DE4D Date: Thu, 26 Jun 2014 03:58:25 +0200 Subject: [PATCH 02/22] Few spellchecks --- README.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index d342343bf..b576d1a0f 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ and simplified Web User Interface. It provides useful components such as an administration panel or a webmail. Modoboa integrates with well known software such as `Postfix -`_ or `Dovecot `_. A SQL +`_ or `Dovecot `_. A SQL database (`MySQL `_, `PostgreSQL `_ or `SQLite `_) is used as a central point of communication between all components. @@ -18,7 +18,7 @@ Modoboa is developed with modularity in mind, expanding it is really easy. Actually, all current features are extensions. It is written in Python and uses the `Django -`_, `jQuery `_ and +`_, `jQuery `_ and `Bootstrap, from Twitter `_ frameworks. @@ -47,19 +47,19 @@ External code The following external libraries are provided with Modoboa: * `jQuery version 1.9.1 `_ -* `jQuery-UI 1.10+ `_ +* `jQuery-UI 1.10+ `_ * `Bootstrap, from Twitter version 2.3.2 `_ * `Bootstrap datetimepicker `_ -.. |latest-version| image:: https://www.pypip.in/v/modoboa/badge.png +.. |latest-version| image:: https://pypip.in/v/modoboa/badge.png :alt: Latest version on Pypi - :target: https://www.crate.io/packages/modoboa/ -.. |downloads| image:: https://www.pypip.in/d/modoboa/badge.png + :target: https://crate.io/packages/modoboa/ +.. |downloads| image:: https://pypip.in/d/modoboa/badge.png :alt: Downloads from Pypi - :target: https://www.crate.io/packages/modoboa/ -.. |travis| image:: https://www.travis-ci.org/tonioo/modoboa.png?branch=master - :target: https://www.travis-ci.org/tonioo/modoboa + :target: https://crate.io/packages/modoboa/ +.. |travis| image:: https://travis-ci.org/tonioo/modoboa.png?branch=master + :target: https://travis-ci.org/tonioo/modoboa .. image:: https://d2weczhvl823v0.cloudfront.net/tonioo/modoboa/trend.png :alt: Bitdeli badge - :target: https://www.bitdeli.com/free + :target: https://bitdeli.com/free From 6c9fb7cccf0b423817a1c78bfc0537a7dde60929 Mon Sep 17 00:00:00 2001 From: Jonas Forsberg Date: Fri, 27 Jun 2014 13:02:55 +0200 Subject: [PATCH 03/22] check if email is empty, iterate_query dovecot --- doc/integration/imap_and_smtp.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index 520909faf..cbeee2c60 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -155,7 +155,7 @@ MySQL users user_query = SELECT '/%d/%n' AS home, as uid, as gid, concat('*:bytes=', mb.quota, 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%n' AND dom.name='%d' - iterate_query = SELECT email AS username FROM core_user + iterate_query = SELECT email AS username FROM core_user WHERE email<>'' .. _dovecot_pg_queries: @@ -174,7 +174,7 @@ PostgreSQL users user_query = SELECT '/%d/%n' AS home, as uid, as gid, '*:bytes=' || mb.quota || 'M' AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%n' AND dom.name='%d' - iterate_query = SELECT email AS username FROM core_user + iterate_query = SELECT email AS username FROM core_user WHERE email<>'' SQLite users ------------ @@ -191,7 +191,7 @@ SQLite users user_query = SELECT '/%d/%n' AS home, as uid, as gid, ('*:bytes=' || mb.quota || 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%n' AND dom.name='%d' - iterate_query = SELECT email AS username FROM core_user + iterate_query = SELECT email AS username FROM core_user WHERE email<>'' .. note:: From fadc609a646e19db75d29ecd664e51c71d1da29f Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 27 Jun 2014 15:28:31 +0200 Subject: [PATCH 04/22] Don't display quota if unlimited. fix #566 --- modoboa/extensions/webmail/templates/webmail/index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modoboa/extensions/webmail/templates/webmail/index.html b/modoboa/extensions/webmail/templates/webmail/index.html index de45b6271..af6e3ee3d 100644 --- a/modoboa/extensions/webmail/templates/webmail/index.html +++ b/modoboa/extensions/webmail/templates/webmail/index.html @@ -37,6 +37,7 @@ $(document).ready(function() {
{% endblock %} {% block footerleft %} +{% if quota != -1 %}
@@ -44,5 +45,6 @@ $(document).ready(function() {
+{% endif %} {% endblock %} From bf368f656a361203ef54f02f85ed57f1318fc7fd Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 27 Jun 2014 15:40:01 +0200 Subject: [PATCH 05/22] Check if dates are too old only when the reply is activated. fix #570 --- modoboa/extensions/postfix_autoreply/forms.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modoboa/extensions/postfix_autoreply/forms.py b/modoboa/extensions/postfix_autoreply/forms.py index cf9cd5ad4..f87b4dea6 100644 --- a/modoboa/extensions/postfix_autoreply/forms.py +++ b/modoboa/extensions/postfix_autoreply/forms.py @@ -1,5 +1,4 @@ # coding: utf-8 -import datetime from django import forms from django.utils.translation import ugettext as _, ugettext_lazy from django.utils import timezone @@ -42,20 +41,25 @@ class ARmessageForm(forms.ModelForm): """Custom fields validaton. We want to be sure that fromdate < untildate and that they are - both in the future. + both in the future ONLY IF the autoreply is beeing activated. + """ super(ARmessageForm, self).clean() if self._errors: raise forms.ValidationError(self._errors) + if not self.cleaned_data["enabled"]: + return self.cleaned_data if self.cleaned_data["fromdate"] is not None: if self.cleaned_data["fromdate"] < timezone.now(): - self._errors["fromdate"] = self.error_class([_("This date is over")]) + self._errors["fromdate"] = self.error_class( + [_("This date is over")]) del self.cleaned_data['fromdate'] else: self.cleaned_data['fromdate'] = timezone.now() if self.cleaned_data["untildate"] is not None: if self.cleaned_data["untildate"] < timezone.now(): - self._errors["untildate"] = self.error_class([_("This date is over")]) + self._errors["untildate"] = self.error_class( + [_("This date is over")]) del self.cleaned_data['untildate'] elif 'fromdate' in self.cleaned_data and \ self.cleaned_data['untildate'] < self.cleaned_data['fromdate']: From 20606e13f2333595c20b8ff08eb9b4e0da949f3c Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 27 Jun 2014 17:02:45 +0200 Subject: [PATCH 06/22] Define the foreign key on mailbox's side. fix #543 --- doc/integration/imap_and_smtp.rst | 65 +-------- ...ilbox_quota_value__del_field_quota_mbox.py | 135 ++++++++++++++++++ .../0039_set_mailbox_quota_value.py | 130 +++++++++++++++++ ...040_auto__chg_field_mailbox_quota_value.py | 125 ++++++++++++++++ modoboa/extensions/admin/models/mailbox.py | 46 +++--- 5 files changed, 422 insertions(+), 79 deletions(-) create mode 100644 modoboa/extensions/admin/migrations/0038_auto__add_field_mailbox_quota_value__del_field_quota_mbox.py create mode 100644 modoboa/extensions/admin/migrations/0039_set_mailbox_quota_value.py create mode 100644 modoboa/extensions/admin/migrations/0040_auto__chg_field_mailbox_quota_value.py diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index 520909faf..4412a851e 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -330,69 +330,16 @@ Trigger As indicated on `Dovecot's wiki `_, you need a trigger to -properly update the quota. Unfortunately, the provided example won't -work for Modoboa. You should use the following one instead: +properly update the quota. -.. sourcecode:: sql +A working copy of this trigger is available on `Modoboa's website +`_. - CREATE OR REPLACE FUNCTION merge_quota() RETURNS TRIGGER AS $$ - BEGIN - IF NEW.messages < 0 OR NEW.messages IS NULL THEN - -- ugly kludge: we came here from this function, really do try to insert - IF NEW.messages IS NULL THEN - NEW.messages = 0; - ELSE - NEW.messages = -NEW.messages; - END IF; - return NEW; - END IF; - - LOOP - UPDATE admin_quota SET bytes = bytes + NEW.bytes, - messages = messages + NEW.messages - WHERE username = NEW.username; - IF found THEN - RETURN NULL; - END IF; - - BEGIN - IF NEW.messages = 0 THEN - RETURN NEW; - ELSE - NEW.messages = - NEW.messages; - return NEW; - END IF; - EXCEPTION WHEN unique_violation THEN - -- someone just inserted the record, update it - END; - END LOOP; - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION set_mboxid() RETURNS TRIGGER AS $$ - DECLARE - mboxid INTEGER; - BEGIN - SELECT admin_mailbox.id INTO STRICT mboxid FROM admin_mailbox INNER JOIN core_user ON admin_mailbox.user_id=core_user.id WHERE core_user.username=NEW.username; - UPDATE admin_quota SET mbox_id = mboxid - WHERE username = NEW.username; - RETURN NULL; - END; - $$ LANGUAGE plpgsql; - - DROP TRIGGER IF EXISTS mergequota ON admin_quota; - CREATE TRIGGER mergequota BEFORE INSERT ON admin_quota - FOR EACH ROW EXECUTE PROCEDURE merge_quota(); - - DROP TRIGGER IF EXISTS setmboxid ON admin_quota; - CREATE TRIGGER setmboxid AFTER INSERT ON admin_quota - FOR EACH ROW EXECUTE PROCEDURE set_mboxid(); - -Copy this example into a file (for example: :file:`quota-trigger.sql`) on -server running postgres and execute the following commands:: +Download this file and copy it on the server running postgres. Then, +execute the following commands:: $ su - postgres - $ psql [modoboa database] < /path/to/quota-trigger.sql + $ psql [modoboa database] < /path/to/modoboa_postgres_trigger.sql $ exit Replace ``[modoboa database]`` by the appropriate value. diff --git a/modoboa/extensions/admin/migrations/0038_auto__add_field_mailbox_quota_value__del_field_quota_mbox.py b/modoboa/extensions/admin/migrations/0038_auto__add_field_mailbox_quota_value__del_field_quota_mbox.py new file mode 100644 index 000000000..aacb7383a --- /dev/null +++ b/modoboa/extensions/admin/migrations/0038_auto__add_field_mailbox_quota_value__del_field_quota_mbox.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Mailbox.quota_value' + db.add_column(u'admin_mailbox', 'quota_value', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['admin.Quota'], null=True), + keep_default=False) + + # Deleting field 'Quota.mbox' + db.delete_column(u'admin_quota', 'mbox_id') + + + def backwards(self, orm): + # Deleting field 'Mailbox.quota_value' + db.delete_column(u'admin_mailbox', 'quota_value_id') + + # Adding field 'Quota.mbox' + db.add_column(u'admin_quota', 'mbox', + self.gf('django.db.models.fields.related.OneToOneField')(related_name='quota_value', unique=True, null=True, to=orm['admin.Mailbox']), + keep_default=False) + + + models = { + 'admin.alias': { + 'Meta': {'ordering': "['domain__name', 'address']", 'unique_together': "(('address', 'domain'),)", 'object_name': 'Alias'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'aliases': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Alias']", 'symmetrical': 'False'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + 'admin.domain': { + 'Meta': {'ordering': "['name']", 'object_name': 'Domain'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}) + }, + 'admin.domainalias': { + 'Meta': {'object_name': 'DomainAlias'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}) + }, + 'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '252'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'quota': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'quota_value': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Quota']", 'null': 'True'}), + 'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) + }, + 'admin.mailboxoperation': { + 'Meta': {'object_name': 'MailboxOperation'}, + 'argument': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Mailbox']", 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, + 'admin.objectdates': { + 'Meta': {'object_name': 'ObjectDates'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'admin.quota': { + 'Meta': {'object_name': 'Quota'}, + 'bytes': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'messages': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'username': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'primary_key': 'True'}) + }, + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'core.objectaccess': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) + }, + u'core.user': { + 'Meta': {'ordering': "['username']", 'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_local': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'}) + } + } + + complete_apps = ['admin'] \ No newline at end of file diff --git a/modoboa/extensions/admin/migrations/0039_set_mailbox_quota_value.py b/modoboa/extensions/admin/migrations/0039_set_mailbox_quota_value.py new file mode 100644 index 000000000..bd42e28ee --- /dev/null +++ b/modoboa/extensions/admin/migrations/0039_set_mailbox_quota_value.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + for mailbox in orm.Mailbox.objects.all(): + quota, created = orm.Quota.objects.get_or_create( + pk="%s@%s" % (mailbox.address, mailbox.domain.name) + ) + mailbox.quota_value = quota + mailbox.save() + + def backwards(self, orm): + "Write your backwards methods here." + for mailbox in orm.Mailbox.objects.all(): + mailbox.quota_value = None + mailbox.save() + + models = { + 'admin.alias': { + 'Meta': {'ordering': "['domain__name', 'address']", 'unique_together': "(('address', 'domain'),)", 'object_name': 'Alias'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'aliases': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Alias']", 'symmetrical': 'False'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + 'admin.domain': { + 'Meta': {'ordering': "['name']", 'object_name': 'Domain'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}) + }, + 'admin.domainalias': { + 'Meta': {'object_name': 'DomainAlias'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}) + }, + 'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '252'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'quota': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'quota_value': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Quota']", 'null': 'True'}), + 'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) + }, + 'admin.mailboxoperation': { + 'Meta': {'object_name': 'MailboxOperation'}, + 'argument': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Mailbox']", 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, + 'admin.objectdates': { + 'Meta': {'object_name': 'ObjectDates'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'admin.quota': { + 'Meta': {'object_name': 'Quota'}, + 'bytes': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'messages': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'username': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'primary_key': 'True'}) + }, + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'core.objectaccess': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) + }, + u'core.user': { + 'Meta': {'ordering': "['username']", 'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_local': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'}) + } + } + + complete_apps = ['admin'] + symmetrical = True diff --git a/modoboa/extensions/admin/migrations/0040_auto__chg_field_mailbox_quota_value.py b/modoboa/extensions/admin/migrations/0040_auto__chg_field_mailbox_quota_value.py new file mode 100644 index 000000000..7aebef23d --- /dev/null +++ b/modoboa/extensions/admin/migrations/0040_auto__chg_field_mailbox_quota_value.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'Mailbox.quota_value' + db.alter_column(u'admin_mailbox', 'quota_value_id', self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['admin.Quota'])) + + def backwards(self, orm): + + # Changing field 'Mailbox.quota_value' + db.alter_column(u'admin_mailbox', 'quota_value_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['admin.Quota'], null=True)) + + models = { + 'admin.alias': { + 'Meta': {'ordering': "['domain__name', 'address']", 'unique_together': "(('address', 'domain'),)", 'object_name': 'Alias'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'aliases': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Alias']", 'symmetrical': 'False'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + 'admin.domain': { + 'Meta': {'ordering': "['name']", 'object_name': 'Domain'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}) + }, + 'admin.domainalias': { + 'Meta': {'object_name': 'DomainAlias'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}) + }, + 'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '252'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'quota': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'quota_value': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Quota']"}), + 'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) + }, + 'admin.mailboxoperation': { + 'Meta': {'object_name': 'MailboxOperation'}, + 'argument': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Mailbox']", 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, + 'admin.objectdates': { + 'Meta': {'object_name': 'ObjectDates'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'admin.quota': { + 'Meta': {'object_name': 'Quota'}, + 'bytes': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'messages': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'username': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'primary_key': 'True'}) + }, + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'core.objectaccess': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) + }, + u'core.user': { + 'Meta': {'ordering': "['username']", 'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_local': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'}) + } + } + + complete_apps = ['admin'] \ No newline at end of file diff --git a/modoboa/extensions/admin/models/mailbox.py b/modoboa/extensions/admin/models/mailbox.py index 82abbc13a..233fde8e1 100644 --- a/modoboa/extensions/admin/models/mailbox.py +++ b/modoboa/extensions/admin/models/mailbox.py @@ -16,6 +16,17 @@ from .base import AdminObject from .domain import Domain +class Quota(models.Model): + """Mailbox current quota usage. + """ + username = models.EmailField(primary_key=True, max_length=254) + bytes = models.BigIntegerField(default=0) + messages = models.IntegerField(default=0) + + class Meta: + app_label = 'admin' + + class MailboxManager(Manager): def get_for_admin(self, admin, squery=None): @@ -43,7 +54,7 @@ class MailboxManager(Manager): qf = Q(pk__in=ids) & qf else: qf = Q(pk__in=ids) - return self.get_query_set().filter(qf) + return self.get_query_set().select_related().filter(qf) class Mailbox(AdminObject): @@ -53,6 +64,7 @@ class Mailbox(AdminObject): ) quota = models.PositiveIntegerField() use_domain_quota = models.BooleanField(default=False) + quota_value = models.ForeignKey(Quota, to_field="username") domain = models.ForeignKey(Domain) user = models.ForeignKey(User) @@ -95,7 +107,7 @@ class Mailbox(AdminObject): We ask dovecot to give us this information because there are several patterns to understand and we don't want to implement - them. + them. """ hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) if hm is None or hm == "no": @@ -179,11 +191,10 @@ class Mailbox(AdminObject): def get_quota(self): """Get quota limit. - + :rtype: int """ - q = Quota.objects.get(username=self.full_address) - return int(q.bytes / 1048576) + return int(self.quota_value.bytes / 1048576) def get_quota_in_percent(self): """Get current quota usage. @@ -192,8 +203,9 @@ class Mailbox(AdminObject): """ if not self.quota: return 0 - q = Quota.objects.get(username=self.full_address) - return int(q.bytes / float(self.quota * 1048576) * 100) + return int( + self.quota_value.bytes / float(self.quota * 1048576) * 100 + ) def post_create(self, creator): from modoboa.lib.permissions import grant_access_to_object @@ -209,11 +221,16 @@ class Mailbox(AdminObject): grant_access_to_object(admin, self.user) def save(self, *args, **kwargs): - super(Mailbox, self).save(*args, **kwargs) + """Custom save. + + We just make sure a quota record is defined for this mailbox. + """ try: q = self.quota_value except Quota.DoesNotExist: - Quota.objects.create(mbox=self, username=self.full_address) + self.quota_value, created = Quota.objects.get_or_create( + username=self.full_address) + super(Mailbox, self).save(*args, **kwargs) def delete(self, keepdir=False): """Custom delete method @@ -257,17 +274,6 @@ def mailbox_deleted_handler(sender, **kwargs): alias.delete() -class Quota(models.Model): - username = models.EmailField(primary_key=True, max_length=254) - bytes = models.BigIntegerField(default=0) - messages = models.IntegerField(default=0) - - mbox = models.OneToOneField(Mailbox, related_name="quota_value", null=True) - - class Meta: - app_label = 'admin' - - class MailboxOperation(models.Model): mailbox = models.ForeignKey(Mailbox, blank=True, null=True) type = models.CharField( From 19cf06eecb6b06d068903a569c2795caa3d9e10e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 27 Jun 2014 19:28:35 +0200 Subject: [PATCH 07/22] Update the quota record when a mailbox is renamed. see #543 --- modoboa/extensions/admin/forms/domain.py | 13 +- ...a_value__add_unique_mailbox_quota_value.py | 131 ++++++++++++++++++ modoboa/extensions/admin/models/mailbox.py | 17 ++- modoboa/extensions/admin/tests/account.py | 1 + 4 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 modoboa/extensions/admin/migrations/0041_auto__chg_field_mailbox_quota_value__add_unique_mailbox_quota_value.py diff --git a/modoboa/extensions/admin/forms/domain.py b/modoboa/extensions/admin/forms/domain.py index 9bdfe81c5..99d955422 100644 --- a/modoboa/extensions/admin/forms/domain.py +++ b/modoboa/extensions/admin/forms/domain.py @@ -102,7 +102,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): return cleaned_data def update_mailbox_quotas(self, domain): - """Update all quota records associated to this domain + """Update all quota records associated to this domain. This method must be called only when a domain gets renamed. As the primary key used for a quota is an email address, rename a @@ -116,9 +116,14 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): perfomant at all as it will generate one query per quota record to update. """ - for q in Quota.objects.filter(username__contains="@%s" % self.oldname).values('username'): - username = q['username'].replace('@%s' % self.oldname, '@%s' % domain.name) - Quota.objects.filter(username=q['username']).update(username=username) + for q in Quota.objects.filter(username__contains="@%s" % self.oldname): + username = q.username.replace( + '@%s' % self.oldname, '@%s' % domain.name) + newq = Quota.objects.create( + username=username, bytes=q.bytes, messages=q.messages) + q.mailbox.quota_value = newq + q.mailbox.save() + q.delete() def save(self, user, commit=True, domalias_post_create=False): """Custom save method diff --git a/modoboa/extensions/admin/migrations/0041_auto__chg_field_mailbox_quota_value__add_unique_mailbox_quota_value.py b/modoboa/extensions/admin/migrations/0041_auto__chg_field_mailbox_quota_value__add_unique_mailbox_quota_value.py new file mode 100644 index 000000000..9bc6a8f19 --- /dev/null +++ b/modoboa/extensions/admin/migrations/0041_auto__chg_field_mailbox_quota_value__add_unique_mailbox_quota_value.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'Mailbox.quota_value' + db.alter_column(u'admin_mailbox', 'quota_value_id', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['admin.Quota'], unique=True)) + # Adding unique constraint on 'Mailbox', fields ['quota_value'] + db.create_unique(u'admin_mailbox', ['quota_value_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'Mailbox', fields ['quota_value'] + db.delete_unique(u'admin_mailbox', ['quota_value_id']) + + + # Changing field 'Mailbox.quota_value' + db.alter_column(u'admin_mailbox', 'quota_value_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['admin.Quota'])) + + models = { + 'admin.alias': { + 'Meta': {'ordering': "['domain__name', 'address']", 'unique_together': "(('address', 'domain'),)", 'object_name': 'Alias'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'aliases': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Alias']", 'symmetrical': 'False'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + 'admin.domain': { + 'Meta': {'ordering': "['name']", 'object_name': 'Domain'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}) + }, + 'admin.domainalias': { + 'Meta': {'object_name': 'DomainAlias'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}) + }, + 'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '252'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'quota': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'quota_value': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['admin.Quota']", 'unique': 'True'}), + 'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) + }, + 'admin.mailboxoperation': { + 'Meta': {'object_name': 'MailboxOperation'}, + 'argument': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Mailbox']", 'null': 'True', 'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, + 'admin.objectdates': { + 'Meta': {'object_name': 'ObjectDates'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'admin.quota': { + 'Meta': {'object_name': 'Quota'}, + 'bytes': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'messages': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'username': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'primary_key': 'True'}) + }, + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'core.objectaccess': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) + }, + u'core.user': { + 'Meta': {'ordering': "['username']", 'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_local': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'}) + } + } + + complete_apps = ['admin'] \ No newline at end of file diff --git a/modoboa/extensions/admin/models/mailbox.py b/modoboa/extensions/admin/models/mailbox.py index 233fde8e1..6b137358c 100644 --- a/modoboa/extensions/admin/models/mailbox.py +++ b/modoboa/extensions/admin/models/mailbox.py @@ -64,7 +64,7 @@ class Mailbox(AdminObject): ) quota = models.PositiveIntegerField() use_domain_quota = models.BooleanField(default=False) - quota_value = models.ForeignKey(Quota, to_field="username") + quota_value = models.OneToOneField(Quota, to_field="username") domain = models.ForeignKey(Domain) user = models.ForeignKey(User) @@ -146,16 +146,25 @@ class Mailbox(AdminObject): ) def rename(self, address, domain): - """Rename the mailbox + """Rename the mailbox. + + To update the associated Quota record, we must create a new + one first, update the foreign key and then we can delete the + original record! :param string address: the new mailbox's address (local part) :param Domain domain: the new mailbox's domain + """ old_mail_home = self.mail_home - qs = Quota.objects.filter(username=self.full_address) self.address = address self.domain = domain - qs.update(username=self.full_address) + old_qvalue = self.quota_value + self.quota_value = Quota.objects.create( + username=self.full_address, bytes=old_qvalue.bytes, + messages=old_qvalue.messages + ) + old_qvalue.delete() self.rename_dir(old_mail_home) def delete_dir(self): diff --git a/modoboa/extensions/admin/tests/account.py b/modoboa/extensions/admin/tests/account.py index 5ad4d556f..178f8c814 100644 --- a/modoboa/extensions/admin/tests/account.py +++ b/modoboa/extensions/admin/tests/account.py @@ -46,6 +46,7 @@ class AccountTestCase(ModoTestCase): ) mb = Mailbox.objects.get(pk=mb.id) self.assertEqual(mb.full_address, "pouet@test.com") + self.assertEqual(mb.quota_value.username, "pouet@test.com") self.ajax_post( reverse("modoboa.extensions.admin.views.identity.delaccount", args=[account.id]), From ec18e7b342e9ee39c9ec99403f2a49437ad4227e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 28 Jun 2014 09:04:11 +0200 Subject: [PATCH 08/22] Specific virtualenv instructions added. fix #563 --- doc/getting_started/configuration.rst | 6 ++++++ doc/getting_started/plugins.rst | 11 ++++++++++- doc/getting_started/upgrade.rst | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/doc/getting_started/configuration.rst b/doc/getting_started/configuration.rst index c56271a05..59e3aa58c 100644 --- a/doc/getting_started/configuration.rst +++ b/doc/getting_started/configuration.rst @@ -297,6 +297,9 @@ online panel. To use it, you can setup a cron job to run every night:: 0 0 * * * /manage.py cleanlogs + # + # Or like this if you use a virtual environment: + # 0 0 * * * /manage.py cleanlogs Cleaning the session table ========================== @@ -311,3 +314,6 @@ cleanup``. That script deletes any session in the session table whose For example, you could setup a cron job to run this script every night:: 0 0 * * * /manage.py cleanup + # + # Or like this if you use a virtual environment: + # 0 0 * * * /manage.py cleanup diff --git a/doc/getting_started/plugins.rst b/doc/getting_started/plugins.rst index 61556029e..9a395055c 100644 --- a/doc/getting_started/plugins.rst +++ b/doc/getting_started/plugins.rst @@ -140,6 +140,9 @@ purge the quarantine database. To use it, add the following line inside root's crontab:: 0 0 * * * /manage.py qcleanup + # + # Or like this if you use a virtual environment: + # 0 0 * * * /manage.py qcleanup Replace ``modoboa_site`` with the path of your Modoboa instance. @@ -206,6 +209,9 @@ administrators or domain administrators. To use it, add the following example line to root's crontab:: 0 12 * * * /manage.py amnotify --baseurl='' + # + # Or like this if you use a virtual environment: + # 0 12 * * * /manage.py amnotify --baseurl='' You are free to change the frequency. @@ -279,7 +285,7 @@ files (see `rrdtool `_) and then generates graphics in PNG format. To use it, go to the online parameters panel and adapt the following -ones to your environnement: +ones to your environment: +--------------------+--------------------+--------------------------+ |Name |Description |Default value | @@ -307,6 +313,9 @@ To finish, you need to collect information periodically in order to feed the RRD files. Add the following line into root's crontab:: */5 * * * * /manage.py logparser &> /dev/null + # + # Or like this if you use a virtual environment: + # 0/5 * * * * /manage.py logparser &> /dev/null Replace ```` with the path of your Modoboa instance. diff --git a/doc/getting_started/upgrade.rst b/doc/getting_started/upgrade.rst index fde360a62..fb1643141 100644 --- a/doc/getting_started/upgrade.rst +++ b/doc/getting_started/upgrade.rst @@ -560,7 +560,7 @@ requires a migration. $ python manage.py migrate admin -#. Update SQL queries used in your environnement (see +#. Update SQL queries used in your environment (see :ref:`postfix` or :ref:`dovecot`). #. Update Postfix configuration so that it can handle domain aliases From 21a2c4622ff37167ea9a2706da23714771598ea3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 28 Jun 2014 09:23:50 +0200 Subject: [PATCH 09/22] External links added. fix #565 --- doc/getting_started/configuration.rst | 8 ++++++++ doc/getting_started/install.rst | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/getting_started/configuration.rst b/doc/getting_started/configuration.rst index 59e3aa58c..d7050cc34 100644 --- a/doc/getting_started/configuration.rst +++ b/doc/getting_started/configuration.rst @@ -66,6 +66,14 @@ The *admin* application exposes several parameters, they are presented below: | |is specified | | +--------------------+--------------------+--------------------+ +.. note:: + + If you are not familiar with virtual domain hosting, you should + take a look at `postfix's documentation + `_. This `How to + `_ + also contains useful information. + .. note:: A random secret key will be generated each time the *Parameters* diff --git a/doc/getting_started/install.rst b/doc/getting_started/install.rst index 8a5afff43..0d7c6bce6 100644 --- a/doc/getting_started/install.rst +++ b/doc/getting_started/install.rst @@ -155,8 +155,9 @@ server:: You should be able to access Modoboa at http://localhost:8000/. -For a fully working interface using the embedded HTTP server, you need to set the ``DEBUG`` -parameter in settings.py to ``True``. +For a fully working interface using the embedded HTTP server, you need +to set the ``DEBUG`` parameter in settings.py to ``True``. For a production environment, we recommend using a stable webserver -like :ref:`apache2` or :ref:`nginx-label`. Don't forget to set ``DEBUG`` back to ``False``. +like :ref:`apache2` or :ref:`nginx-label`. Don't forget to set +``DEBUG`` back to ``False``. From a24883aa0126eeec8085794994183a1b27aa4e4a Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 29 Jun 2014 09:37:37 +0200 Subject: [PATCH 10/22] Amavis configuration instructions added. fix #567 --- doc/getting_started/plugins.rst | 55 +++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/doc/getting_started/plugins.rst b/doc/getting_started/plugins.rst index 9a395055c..ad1ce99c4 100644 --- a/doc/getting_started/plugins.rst +++ b/doc/getting_started/plugins.rst @@ -99,8 +99,46 @@ This plugin provides a simple management frontend for `amavisd-new you're planning to use the :ref:`selfservice`, you'll need version **2.8.0**. -Database -======== +Quick Amavis setup +================== + +By default, amavis doesn't use a database. To configure this +behaviour, you first need to create a dedicated database. This step is a +bit manual since no *ready-to-use* SQL schema is provided by +amavis. The information is located inside README files, one for `MySQL +`_ and one for `PostgreSQL +`_. + +Then, you must tell amavis to use this database for lookups and +quarantined messages storing. Here is a working configuration sample:: + + @lookup_sql_dsn = + (['DBI::database=;host=;port=', '', ']']); + + @storage_sql_dsn = + (['DBI::database=;host=;port=', '', ']']); + + $virus_quarantine_method = 'sql:'; + $spam_quarantine_method = 'sql:'; + $banned_files_quarantine_method = 'sql:'; + $bad_header_quarantine_method = 'sql:'; + + $virus_quarantine_to = 'virus-quarantine'; + $banned_quarantine_to = 'banned-quarantine'; + $bad_header_quarantine_to = 'bad-header-quarantine'; + $spam_quarantine_to = 'spam-quarantine'; + +Replace values between ``<>`` by yours. To know how to configure +amavis to allow quarantined messages release, read this :ref:`section +`. + +.. note:: + + Amavis configuration allows for separate lookup and storage + databases but Modoboa doesn't support it yet. + +Connect Modoboa and Amavis +========================== You must tell to Modoboa where it can find the amavis database. Inside :file:`settings.py`, add a new connection to the @@ -120,17 +158,6 @@ database. Inside :file:`settings.py`, add a new connection to the Replace values between ``<>`` with yours. -.. note:: - - Modoboa doesn't create amavis tables. You need to install them - following the `official documentation - `_. - -.. note:: - - Amavis configuration allows for separate lookup and storage - databases but Modoboa doesn't support it yet. - Cleanup ------- @@ -150,6 +177,8 @@ By default, messages older than 14 days are automatically purged. You can modify this value by changing the ``MAX_MESSAGES_AGE`` parameter in the online panel. +.. _amavis_release: + Release messages ================ From 28159289a64eee34e244312d2437e68719df777d Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 29 Jun 2014 10:35:12 +0200 Subject: [PATCH 11/22] mod_wsgi daemon mode instructions added. fix #568 --- doc/integration/web_servers.rst | 44 ++++++--------------------------- 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/doc/integration/web_servers.rst b/doc/integration/web_servers.rst index eef79b526..c4a0b93cc 100644 --- a/doc/integration/web_servers.rst +++ b/doc/integration/web_servers.rst @@ -48,45 +48,15 @@ following content inside:: This is just one possible configuration. -.. note:: - Django 1.3 users, please consult this `page `_, - it contains an example :file:`wsgi.py` file. +To use mod_wsgi daemon mode, add the two following directives just +under ``WSGIScriptAlias``:: -.. note:: - You will certainly need more configuration in order to launch - Apache. + WSGIDaemonProcess example.com python-path=:/lib/python2.7/site-packages + WSGIProcessGroup example.com -mod_python -========== - -First, make sure that mod_python is installed on your server. - -Create a new virtualhost in your Apache configuration and put the -following content inside:: - - - ServerName - DocumentRoot - - - SetHandler mod_python - PythonHandler django.core.handlers.modpython - PythonPath "[''] + sys.path" - SetEnv DJANGO_SETTINGS_MODULE .settings - - - Alias "/sitestatic" "/sitestatic" - - SetHandler None - - - Alias "/media" "/media" - - SetHandler None - - - -This is just one possible configuration. +Replace values between ``<>`` with yours. If you don't use a +`virtualenv `_, just +remove the last part of the ``WSGIDaemonProcess`` directive. .. note:: You will certainly need more configuration in order to launch From 8bb16e1e0f3f16d1b94a34c447765899227d1d5a Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 2 Jul 2014 21:49:07 +0200 Subject: [PATCH 12/22] Make sure notifications are always visible. fix #549 --- modoboa/static/js/notify.js | 4 ++-- modoboa/static/js/twocols_nav.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modoboa/static/js/notify.js b/modoboa/static/js/notify.js index de7e9af97..cdf94874b 100644 --- a/modoboa/static/js/notify.js +++ b/modoboa/static/js/notify.js @@ -15,7 +15,7 @@ id: bid, click: $.proxy(this.destroy_box, this) }).css({ - position: "absolute" + position: "fixed" }); return div; @@ -101,4 +101,4 @@ top_position: 50 }; -})(jQuery); \ No newline at end of file +})(jQuery); diff --git a/modoboa/static/js/twocols_nav.js b/modoboa/static/js/twocols_nav.js index c6fddc606..2c2642957 100644 --- a/modoboa/static/js/twocols_nav.js +++ b/modoboa/static/js/twocols_nav.js @@ -130,4 +130,4 @@ TwocolsNav.prototype = { } }; -TwocolsNav.prototype = $.extend({}, Listing.prototype, TwocolsNav.prototype); \ No newline at end of file +TwocolsNav.prototype = $.extend({}, Listing.prototype, TwocolsNav.prototype); From f83df1c7070fac030bedafdb304ca62b510aeb0f Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 4 Jul 2014 10:56:39 +0200 Subject: [PATCH 13/22] postfix_maps command silent mode. fix #536 --- modoboa/core/commands/postfix_maps.py | 57 +++++++++++++++++++------ modoboa/core/tests.py | 61 +++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/modoboa/core/commands/postfix_maps.py b/modoboa/core/commands/postfix_maps.py index 5ac75ce8a..5ba7c23f3 100644 --- a/modoboa/core/commands/postfix_maps.py +++ b/modoboa/core/commands/postfix_maps.py @@ -10,19 +10,29 @@ from . import Command class MapFilesGenerator(object): - def __init__(self): + def __init__(self, dbinfo=None): + """Constructor. + """ self.template = Template("""user = {{ dbuser }} password = {{ dbpass }} dbname = {{ dbname }} hosts = {{ dbhost }} query = {{ query|safe }} """) - self.tpl_context = { - 'dbhost': raw_input('Database host: '), - 'dbname': raw_input('Database name: '), - 'dbuser': raw_input('Username: '), - 'dbpass': getpass.getpass('Password: ') - } + if dbinfo is None: + self.tpl_context = { + 'dbhost': raw_input('Database host: '), + 'dbname': raw_input('Database name: '), + 'dbuser': raw_input('Username: '), + 'dbpass': getpass.getpass('Password: ') + } + else: + self.tpl_context = { + 'dbhost': dbinfo['HOST'], + 'dbname': dbinfo['NAME'], + 'dbuser': dbinfo['USER'], + 'dbpass': dbinfo['PASSWORD'] + } if not self.tpl_context['dbhost']: self.tpl_context['dbhost'] = '127.0.0.1' @@ -77,13 +87,18 @@ query = {{ query|safe }} class SQLiteMapFilesGenerator(MapFilesGenerator): - def __init__(self): + def __init__(self, dbinfo=None): self.template = Template("""dbpath = {{ dbpath }} query = {{ query|safe }} """) - self.tpl_context = { - 'dbpath': raw_input('Database path: ') - } + if dbinfo is None: + self.tpl_context = { + 'dbpath': raw_input('Database path: ') + } + else: + self.tpl_context = { + 'dbpath': dbinfo['NAME'] + } class MapFile(object): @@ -194,14 +209,30 @@ class PostfixMapsCommand(Command): default='mysql', help='Used database type' ) + self._parser.add_argument( + '--dburl', type=str, nargs=1, default=None, + help="url of Modoboa's database" + ) self._parser.add_argument( '--categories', type=str, nargs='*', default=['std'], help='Map file categories to generate (choices: std, autoreply)' ) def handle(self, parsed_args): + """Command entry point. + """ + dbinfo = None + if parsed_args.dburl: + import dj_database_url + dbinfo = dj_database_url.config(default=parsed_args.dburl[0]) + if 'sqlite' in dbinfo['ENGINE']: + parsed_args.dbtype = 'sqlite' + elif 'psycopg2' in dbinfo['ENGINE']: + parsed_args.dbtype = 'postgres' + else: + parsed_args.dbtype = 'mysql' if parsed_args.dbtype == 'sqlite': - g = SQLiteMapFilesGenerator() + g = SQLiteMapFilesGenerator(dbinfo) else: - g = MapFilesGenerator() + g = MapFilesGenerator(dbinfo) g.render(parsed_args) diff --git a/modoboa/core/tests.py b/modoboa/core/tests.py index 80de9db01..f21c4b34c 100644 --- a/modoboa/core/tests.py +++ b/modoboa/core/tests.py @@ -1,4 +1,10 @@ +import os +import tempfile + from django.core.urlresolvers import reverse +from django.test import TestCase + +from modoboa.lib.sysutils import exec_cmd from modoboa.lib.tests import ModoTestCase from . import factories @@ -40,3 +46,58 @@ class ProfileTestCase(ModoTestCase): self.assertTrue( self.clt.login(username="user@test.com", password="tutu") ) + + +class ModoboaAdminCommandTestCase(TestCase): + MAP_FILES = { + "std": [ + "sql-domains.cf", "sql-domain-aliases.cf", "sql-aliases.cf", + "sql-domain-aliases-mailboxes.cf", "sql-catchall-aliases.cf", + "sql-maintain.cf" + ], + "autoreply": ["sql-autoreplies-transport.cf", "sql-autoreplies.cf"], + "relaydomains": [ + "sql-relaydomains.cf", "sql-relaydomains-transport.cf", + "sql-relaydomain-aliases-transport.cf", + "sql-relay-recipient-verification.cf" + ] + } + + def setUp(self): + self.workdir = tempfile.mkdtemp() + + def tearDown(self): + exec_cmd("rm -rf {}".format(self.workdir)) + + def _test_maps_generation(self, engine, categories=None): + if categories is None: + categories = ["std", "autoreply", "relaydomains"] + dburl = "{}://user:password@localhost/testdb".format(engine) + code, output = exec_cmd( + "modoboa-admin.py postfix_maps --categories {} --dburl {} {}".format( + " ".join(categories), dburl, self.workdir + ) + ) + self.assertEqual(code, 0) + for category in categories: + for mapfile in self.MAP_FILES[category]: + path = "{}/{}".format(self.workdir, mapfile) + self.assertTrue(os.path.exists(path)) + with open(path) as fpo: + content = fpo.read() + if engine != "sqlite": + self.assertIn("user = user", content) + self.assertIn("password = password", content) + self.assertIn("dbname = testdb", content) + self.assertIn("hosts = localhost", content) + else: + self.assertIn("dbpath = testdb", content) + + def test_postgres_maps(self): + self._test_maps_generation("postgres") + + def test_mysql_maps(self): + self._test_maps_generation("mysql") + + def test_sqlite_maps(self): + self._test_maps_generation("sqlite") From e6f23161c2d58a4a5af611549109b5ad93a92b6b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 20 Jul 2014 15:31:51 +0200 Subject: [PATCH 14/22] Convert signature to unicode. fix #582 --- modoboa/extensions/webmail/lib/signature.py | 8 ++++---- modoboa/extensions/webmail/views.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modoboa/extensions/webmail/lib/signature.py b/modoboa/extensions/webmail/lib/signature.py index 359801a1f..c90a00fa5 100644 --- a/modoboa/extensions/webmail/lib/signature.py +++ b/modoboa/extensions/webmail/lib/signature.py @@ -8,22 +8,22 @@ class EmailSignature(object): :param user: User object """ def __init__(self, user): - self._sig = "" + self._sig = u"" dformat = parameters.get_user(user, "EDITOR") content = parameters.get_user(user, "SIGNATURE") if len(content): getattr(self, "_format_sig_%s" % dformat)(content) def _format_sig_plain(self, content): - self._sig = """ + self._sig = u""" --- %s""" % content def _format_sig_html(self, content): content = re.sub("\n", "
", content) - self._sig = """
+ self._sig = u"""
---
%s""" % content - def __repr__(self): + def __unicode__(self): return self._sig diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index 5f5088ac3..f39fa3576 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -344,14 +344,14 @@ def listmailbox(request, defmailbox="INBOX", update_session=True): def render_compose(request, form, posturl, email=None, insert_signature=False): editor = parameters.get_user(request.user, "EDITOR") if email is None: - body = "" - textheader = "" + body = u"" + textheader = u"" else: body = email.body textheader = email.textheader if insert_signature: signature = EmailSignature(request.user) - body += str(signature) + body += unicode(signature) randid = None if not "id" in request.GET: if "compose_mail" in request.session: From 300ba51fb02b0d89ee40a86668d17f65ecfc11d3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 20 Jul 2014 16:57:07 +0200 Subject: [PATCH 15/22] Explicit labels. see #492 --- modoboa/extensions/stats/graphics.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modoboa/extensions/stats/graphics.py b/modoboa/extensions/stats/graphics.py index bc3ebb195..b711b938d 100644 --- a/modoboa/extensions/stats/graphics.py +++ b/modoboa/extensions/stats/graphics.py @@ -95,8 +95,7 @@ class Graphic(object): class AverageTraffic(Graphic): """Average traffic. """ - title = ugettext_lazy('Average traffic') - vertlabel = ugettext_lazy('msgs/min') + title = ugettext_lazy('Average traffic (msgs/min)') # Curve definitions sent = Curve("sent", "lawngreen", ugettext_lazy("sent messages")) @@ -110,8 +109,7 @@ class AverageTraffic(Graphic): class AverageTrafficSize(Graphic): """Average traffic size. """ - title = ugettext_lazy('Average normal traffic size') - vertlabel = ugettext_lazy('bytes/min') + title = ugettext_lazy('Average normal traffic size (bytes/min)') # Curve definitions size_recv = Curve("size_recv", "orange", ugettext_lazy("received size")) From a5acec792339b0bc3feb4488b499ffe5535c0b44 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 20 Jul 2014 17:08:23 +0200 Subject: [PATCH 16/22] A brief note about media permissions. fix #569 --- doc/getting_started/configuration.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/getting_started/configuration.rst b/doc/getting_started/configuration.rst index d7050cc34..ff035892f 100644 --- a/doc/getting_started/configuration.rst +++ b/doc/getting_started/configuration.rst @@ -84,6 +84,20 @@ The *admin* application exposes several parameters, they are presented below: Specific LDAP parameters are also available, see :ref:`LDAP authentication `. +*********** +Media files +*********** + +Modoboa uses a specific directory to upload files (ie. when the +webmail is in use) or to create ones (ex: graphical statistics). This +directory is named ``media`` and is located inside modoboa's +installation directory (called ``modoboa_site`` in this +documentation). + +To work properly, the system user which runs modoboa (``www-data``, +``apache``, whatever) must have write access to this directory. + + ************* Customization ************* From dafb45aabfac1a9c4bc990f6f9ff75485c23ad32 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 20 Jul 2014 17:17:00 +0200 Subject: [PATCH 17/22] ValueError can be raised by the datetime module too... fix #556 --- modoboa/extensions/webmail/lib/imapheader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modoboa/extensions/webmail/lib/imapheader.py b/modoboa/extensions/webmail/lib/imapheader.py index 2106ef8e3..ffc8cafb6 100644 --- a/modoboa/extensions/webmail/lib/imapheader.py +++ b/modoboa/extensions/webmail/lib/imapheader.py @@ -85,9 +85,9 @@ def parse_date(value, **kwargs): tmp = email.utils.parsedate_tz(value) if not tmp: return value - ndate = datetime.datetime(*(tmp)[:7]) - now = datetime.datetime.now() try: + ndate = datetime.datetime(*(tmp)[:7]) + now = datetime.datetime.now() if now - ndate > datetime.timedelta(7): return ndate.strftime("%d.%m.%Y %H:%M") return ndate.strftime("%a %H:%M") From d44b69c63bf2321213430d9c43c6a12cf34c7e59 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 20 Jul 2014 17:28:22 +0200 Subject: [PATCH 18/22] Version 1.1.5 --- doc/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 75b698213..62cd369dc 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -50,7 +50,7 @@ copyright = u'2014, Antoine Nguyen' # The short X.Y version. version = '1.1' # The full version, including alpha/beta/rc tags. -release = '1.1.4' +release = '1.1.5' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 97c97c2ec..0110a7bde 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ def read(fname): setup( name="modoboa", - version='1.1.4', + version='1.1.5', url='http://modoboa.org/', license='MIT', description="Mail hosting made simple", From bf58b2363bc98f8bfe9a161209deef54af965113 Mon Sep 17 00:00:00 2001 From: rdpse Date: Tue, 22 Jul 2014 12:19:43 +0100 Subject: [PATCH 19/22] Update install.rst Minor typo --- doc/getting_started/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/getting_started/install.rst b/doc/getting_started/install.rst index 0d7c6bce6..6f33726fe 100644 --- a/doc/getting_started/install.rst +++ b/doc/getting_started/install.rst @@ -91,7 +91,7 @@ sure your database is using UTF8 as a default charset. Deployment ********** -`modoboa-admin.py`, a command line tool, let you deploy a +`modoboa-admin.py`, a command line tool, lets you deploy a *ready-to-use* Modoboa site using only one instruction:: $ modoboa-admin.py deploy modoboa_example --syncdb --collectstatic [--with-amavis] [--dburl database-url] [--amavis_dburl database-url] From 9713e1a147b8ccf2030ba4037ec04bf54fc241cf Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 25 Jul 2014 10:18:49 +0200 Subject: [PATCH 20/22] Handle empty subjects properly. fix #586 --- modoboa/extensions/webmail/lib/imapemail.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/modoboa/extensions/webmail/lib/imapemail.py b/modoboa/extensions/webmail/lib/imapemail.py index e07f92f71..b087e4f6e 100644 --- a/modoboa/extensions/webmail/lib/imapemail.py +++ b/modoboa/extensions/webmail/lib/imapemail.py @@ -218,6 +218,11 @@ class Modifier(ImapEmail): def _modify_html(self): pass + @property + def subject(self): + """Just a shortcut to return a subject in any case.""" + return self.Subject if hasattr(self, "Subject") else "" + class ReplyModifier(Modifier): headernames = ImapEmail.headernames + \ @@ -246,11 +251,11 @@ class ReplyModifier(Modifier): if self.form.fields["cc"].initial != "": self.form.fields["cc"].initial += ", " self.form.fields["cc"].initial += tmp.fulladdress - m = re.match(r"re\s*:\s*.+", self.Subject.lower()) + m = re.match(r"re\s*:\s*.+", self.subject.lower()) if m: - self.form.fields["subject"].initial = self.Subject + self.form.fields["subject"].initial = self.subject else: - self.form.fields["subject"].initial = "Re: %s" % self.Subject + self.form.fields["subject"].initial = "Re: %s" % self.subject def _modify_plain(self): super(ReplyModifier, self)._modify_plain() @@ -264,10 +269,15 @@ class ReplyModifier(Modifier): class ForwardModifier(Modifier): + + """ + Modify a message so it can be forwarded. + """ + def __init__(self, *args, **kwargs): super(ForwardModifier, self).__init__(*args, **kwargs) self._header() - self.form.fields["subject"].initial = "Fwd: %s" % self.Subject + self.form.fields["subject"].initial = "Fwd: %s" % self.subject def __getfunc(self, name): return getattr(self, "%s_%s" % (name, self.dformat)) @@ -275,7 +285,7 @@ class ForwardModifier(Modifier): def _header(self): self.textheader = self.__getfunc("_header_begin")() + "\n" self.textheader += \ - self.__getfunc("_header_line")(_("Subject"), self.Subject) + self.__getfunc("_header_line")(_("Subject"), self.subject) self.textheader += \ self.__getfunc("_header_line")(_("Date"), self.Date) for hdr in ["From", "To", "Reply-To"]: From 1d4bba0779e8065771a48290531721cb59fb4918 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 27 Jul 2014 22:46:37 +0200 Subject: [PATCH 21/22] Fixing demo fixtures. --- modoboa/extensions/admin/fixtures/demo.json | 305 +++++++++++++++++++- 1 file changed, 304 insertions(+), 1 deletion(-) diff --git a/modoboa/extensions/admin/fixtures/demo.json b/modoboa/extensions/admin/fixtures/demo.json index f1971a053..36ac1cf2f 100644 --- a/modoboa/extensions/admin/fixtures/demo.json +++ b/modoboa/extensions/admin/fixtures/demo.json @@ -1 +1,304 @@ -[{"pk": 1, "model": "core.user", "fields": {"username": "admin", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": false, "last_login": "2012-05-03T16:58:51+01:00", "groups": [], "user_permissions": [], "password": "{CRYPT}dTTsGDkA5ZHKg", "email": "", "date_joined": "2012-01-26T15:19:25+01:00"}}, {"pk": 2, "model": "core.user", "fields": {"username": "admin@demo.local", "first_name": "Demo", "last_name": "Administrator", "is_active": true, "is_superuser": false, "is_staff": false, "last_login": "2012-05-24T05:00:12+01:00", "groups": [1], "user_permissions": [], "password": "{CRYPT}SJ2bQWwabfH/6", "email": "admin@demo.local", "date_joined": "2012-05-03T16:50:15+01:00"}}, {"pk": 4, "model": "core.user", "fields": {"username": "user@demo.local", "first_name": "Demo", "last_name": "User", "is_active": true, "is_superuser": false, "is_staff": false, "last_login": "2012-05-03T16:56:50+01:00", "groups": [2], "user_permissions": [], "password": "{CRYPT}2iAFXtxUA6.7Y", "email": "user@demo.local", "date_joined": "2012-05-03T16:56:50+01:00"}}, {"pk": 2, "model": "core.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["admin", "domain"], "object_id": 1}}, {"pk": 3, "model": "core.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["core", "user"], "object_id": 2}}, {"pk": 4, "model": "core.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["admin", "mailbox"], "object_id": 1}}, {"pk": 5, "model": "core.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["admin", "alias"], "object_id": 1}}, {"pk": 6, "model": "core.objectaccess", "fields": {"is_owner": false, "user": 2, "content_type": ["admin", "domain"], "object_id": 1}}, {"pk": 7, "model": "core.objectaccess", "fields": {"is_owner": false, "user": 2, "content_type": ["admin", "mailbox"], "object_id": 1}}, {"pk": 8, "model": "core.objectaccess", "fields": {"is_owner": false, "user": 2, "content_type": ["core", "user"], "object_id": 2}}, {"pk": 9, "model": "core.objectaccess", "fields": {"is_owner": false, "user": 2, "content_type": ["admin", "alias"], "object_id": 1}}, {"pk": 13, "model": "core.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["core", "user"], "object_id": 4}}, {"pk": 14, "model": "core.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["admin", "mailbox"], "object_id": 3}}, {"pk": 15, "model": "core.objectaccess", "fields": {"is_owner": false, "user": 2, "content_type": ["admin", "mailbox"], "object_id": 3}}, {"pk": 16, "model": "core.objectaccess", "fields": {"is_owner": false, "user": 2, "content_type": ["core", "user"], "object_id": 4}}, {"pk": 1, "model": "admin.objectdates", "fields": {"creation": "2012-05-03T16:49:40+01:00", "last_modification": "2012-05-03T16:49:40+01:00"}}, {"pk": 2, "model": "admin.objectdates", "fields": {"creation": "2012-05-03T16:50:15+01:00", "last_modification": "2012-05-03T16:50:36+01:00"}}, {"pk": 3, "model": "admin.objectdates", "fields": {"creation": "2012-05-03T16:50:15+01:00", "last_modification": "2012-05-03T16:50:15+01:00"}}, {"pk": 4, "model": "admin.objectdates", "fields": {"creation": "2012-05-03T16:51:04+01:00", "last_modification": "2012-05-03T16:51:04+01:00"}}, {"pk": 5, "model": "admin.objectdates", "fields": {"creation": "2012-05-03T16:56:50+01:00", "last_modification": "2012-05-03T16:56:50+01:00"}}, {"pk": 1, "model": "admin.domain", "fields": {"dates": 1, "enabled": true, "name": "demo.local", "quota": 10}}, {"pk": 1, "model": "admin.mailbox", "fields": {"dates": 2, "quota": 10, "user": 2, "domain": 1, "address": "admin"}}, {"pk": 3, "model": "admin.mailbox", "fields": {"dates": 5, "quota": 10, "user": 4, "domain": 1, "address": "user"}}, {"pk": 1, "model": "admin.alias", "fields": {"dates": 3, "enabled": true, "domain": 1, "mboxes": [1], "address": "postmaster", "extmboxes": ""}}, {"pk": 1, "model": "core.extension", "fields": {"enabled": true, "name": "limits"}}, {"pk": 2, "model": "core.extension", "fields": {"enabled": true, "name": "postfix_autoreply"}}, {"pk": 3, "model": "core.extension", "fields": {"enabled": true, "name": "stats"}}, {"pk": 4, "model": "core.extension", "fields": {"enabled": true, "name": "amavis"}}, {"pk": 5, "model": "core.extension", "fields": {"enabled": true, "name": "webmail"}}, {"pk": 6, "model": "core.extension", "fields": {"enabled": true, "name": "sievefilters"}}, {"pk": 7, "model": "core.extension", "fields": {"enabled": true, "name": "postfix_relay_domains"}}, {"pk": "admin@demo.local", "model": "admin.quota", "fields": {"mbox": 1, "username": "admin@demo.local", "bytes": 0, "messages": 0}}, {"pk": "user@demo.local", "model": "admin.quota", "fields": {"mbox": 3, "username": "user@demo.local", "bytes": 0, "messages": 0}}] +[ + { + "pk": 1, + "model": "core.user", + "fields": { + "username": "admin", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": true, + "is_staff": false, + "last_login": "2012-05-03T16:58:51+01:00", + "groups": [], + "user_permissions": [], + "password": "{CRYPT}dTTsGDkA5ZHKg", + "email": "", + "date_joined": "2012-01-26T15:19:25+01:00" + } + }, + { + "pk": 2, + "model": "core.user", + "fields": { + "username": "admin@demo.local", + "first_name": "Demo", + "last_name": "Administrator", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2012-05-24T05:00:12+01:00", + "groups": [1], + "user_permissions": [], + "password": "{CRYPT}SJ2bQWwabfH/6", + "email": "admin@demo.local", + "date_joined": "2012-05-03T16:50:15+01:00" + } + }, + { + "pk": 4, + "model": "core.user", + "fields": { + "username": "user@demo.local", + "first_name": "Demo", "last_name": "User", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2012-05-03T16:56:50+01:00", + "groups": [2], + "user_permissions": [], + "password": "{CRYPT}2iAFXtxUA6.7Y", + "email": "user@demo.local", + "date_joined": "2012-05-03T16:56:50+01:00" + } + }, + { + "pk": 2, + "model": "core.objectaccess", + "fields": { + "is_owner": true, + "user": 1, + "content_type": ["admin", "domain"], + "object_id": 1 + } + }, + { + "pk": 3, + "model": "core.objectaccess", + "fields": { + "is_owner": true, + "user": 1, + "content_type": ["core", "user"], + "object_id": 2 + } + }, + { + "pk": 4, + "model": "core.objectaccess", + "fields": { + "is_owner": true, + "user": 1, + "content_type": ["admin", "mailbox"], + "object_id": 1 + } + }, + { + "pk": 5, + "model": "core.objectaccess", + "fields": { + "is_owner": true, + "user": 1, + "content_type": ["admin", "alias"], + "object_id": 1 + } + }, + { + "pk": 6, + "model": "core.objectaccess", + "fields": { + "is_owner": false, + "user": 2, + "content_type": ["admin", "domain"], + "object_id": 1 + } + }, + { + "pk": 7, + "model": "core.objectaccess", + "fields": { + "is_owner": false, + "user": 2, + "content_type": ["admin", "mailbox"], + "object_id": 1 + } + }, + { + "pk": 8, + "model": "core.objectaccess", + "fields": { + "is_owner": false, + "user": 2, + "content_type": ["core", "user"], + "object_id": 2 + } + }, + { + "pk": 9, + "model": "core.objectaccess", + "fields": { + "is_owner": false, + "user": 2, + "content_type": ["admin", + "alias"], + "object_id": 1}}, + { + "pk": 13, + "model": "core.objectaccess", + "fields": { + "is_owner": true, + "user": 1, + "content_type": ["core", + "user"], + "object_id": 4}}, + { + "pk": 14, + "model": "core.objectaccess", + "fields": { + "is_owner": true, + "user": 1, + "content_type": ["admin", + "mailbox"], + "object_id": 3}}, + { + "pk": 15, + "model": "core.objectaccess", + "fields": { + "is_owner": false, + "user": 2, + "content_type": ["admin", + "mailbox"], + "object_id": 3}}, + { + "pk": 16, + "model": "core.objectaccess", + "fields": { + "is_owner": false, + "user": 2, + "content_type": ["core", + "user"], + "object_id": 4}}, + { + "pk": 1, + "model": "admin.objectdates", + "fields": { + "creation": "2012-05-03T16:49:40+01:00", + "last_modification": "2012-05-03T16:49:40+01:00"}}, + { + "pk": 2, + "model": "admin.objectdates", + "fields": { + "creation": "2012-05-03T16:50:15+01:00", + "last_modification": "2012-05-03T16:50:36+01:00"}}, + { + "pk": 3, + "model": "admin.objectdates", + "fields": { + "creation": "2012-05-03T16:50:15+01:00", + "last_modification": "2012-05-03T16:50:15+01:00"}}, + { + "pk": 4, + "model": "admin.objectdates", + "fields": { + "creation": "2012-05-03T16:51:04+01:00", + "last_modification": "2012-05-03T16:51:04+01:00"}}, + { + "pk": 5, + "model": "admin.objectdates", + "fields": { + "creation": "2012-05-03T16:56:50+01:00", + "last_modification": "2012-05-03T16:56:50+01:00"}}, + { + "pk": 1, + "model": "admin.domain", + "fields": { + "dates": 1, + "enabled": true, + "name": "demo.local", + "quota": 10}}, + { + "pk": 1, + "model": "admin.mailbox", + "fields": { + "dates": 2, + "quota": 10, + "user": 2, + "domain": 1, + "address": "admin", + "quota_value": "admin@demo.local" + } + }, + { + "pk": 3, + "model": "admin.mailbox", + "fields": { + "dates": 5, + "quota": 10, + "user": 4, + "domain": 1, + "address": "user", + "quota_value": "user@demo.local" + } + }, + { + "pk": 1, + "model": "admin.alias", + "fields": { + "dates": 3, + "enabled": true, + "domain": 1, + "mboxes": [1], + "address": "postmaster", + "extmboxes": ""}}, + { + "pk": 1, + "model": "core.extension", + "fields": { + "enabled": true, + "name": "limits"}}, + { + "pk": 2, + "model": "core.extension", + "fields": { + "enabled": true, + "name": "postfix_autoreply"}}, + { + "pk": 3, + "model": "core.extension", + "fields": { + "enabled": true, + "name": "stats"}}, + { + "pk": 4, + "model": "core.extension", + "fields": { + "enabled": true, + "name": "amavis"}}, + { + "pk": 5, + "model": "core.extension", + "fields": { + "enabled": true, + "name": "webmail"}}, + { + "pk": 6, + "model": "core.extension", + "fields": { + "enabled": true, + "name": "sievefilters"}}, + { + "pk": 7, + "model": "core.extension", + "fields": { + "enabled": true, + "name": "postfix_relay_domains" + } + }, + { + "pk": "admin@demo.local", + "model": "admin.quota", + "fields": { + "username": "admin@demo.local", + "bytes": 0, + "messages": 0 + } + }, + { + "pk": "user@demo.local", + "model": "admin.quota", + "fields": { + "username": "user@demo.local", + "bytes": 0, + "messages": 0 + } + } +] From 1a80545f27a815d1ca7ff7cccf08aa92f21fbe79 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 28 Jul 2014 20:12:49 +0200 Subject: [PATCH 22/22] Do not override the default history object. fix #588 --- .../sievefilters/static/sievefilters/js/lib.js | 14 +++++++------- .../sievefilters/templates/sievefilters/index.html | 2 +- modoboa/static/js/global.js | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js b/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js index 700a4e423..9edd81f62 100644 --- a/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js +++ b/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js @@ -20,7 +20,7 @@ function removefilter(evt) { url: $this.attr("href"), dataType: 'json' }).done(function(response) { - history.update(true); + histomanager.update(true); $("body").notify('success', response, 2000); }); } @@ -30,7 +30,7 @@ function removefilter(evt) { */ function changefs(evt) { evt.preventDefault(); - history.baseurl($(this).val()).update(); + histomanager.baseurl($(this).val()).update(); } /* @@ -48,7 +48,7 @@ function init_filters_list() { * It updates the middle DIV and initializes the user interface. */ function loadfs(response) { - curscript = history.getbaseurl(); + curscript = histomanager.getbaseurl(); $("#curfset").val(curscript); $("#set_content").html(response.content); @@ -78,7 +78,7 @@ function send_command(cmd, extracb) { extracb(response); } if (!response.norefresh) { - history.update(true); + histomanager.update(true); } $("body").notify('success', response.respmsg, 2000); }); @@ -133,7 +133,7 @@ function removefs(evt) { var $curfset = $("#curfset"); $curfset.find("option[value='" + curscript + "']").remove(); - history.baseurl(response.newfs, 1); + histomanager.baseurl(response.newfs, 1); }); } @@ -206,7 +206,7 @@ function filterset_created(data) { } }); } - history.baseurl(data.url, 1).update(1); + histomanager.baseurl(data.url, 1).update(1); } function filtersetform_cb() { @@ -240,4 +240,4 @@ function fetch_templates(url, container) { result = data; }); return result; -} \ No newline at end of file +} diff --git a/modoboa/extensions/sievefilters/templates/sievefilters/index.html b/modoboa/extensions/sievefilters/templates/sievefilters/index.html index 63ac35146..884040aa4 100644 --- a/modoboa/extensions/sievefilters/templates/sievefilters/index.html +++ b/modoboa/extensions/sievefilters/templates/sievefilters/index.html @@ -7,7 +7,7 @@