diff --git a/modoboa/admin/migrations/0011_domain_transport.py b/modoboa/admin/migrations/0011_domain_transport.py new file mode 100644 index 000000000..8f037fb6c --- /dev/null +++ b/modoboa/admin/migrations/0011_domain_transport.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-12-20 14:17 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('transport', '0001_initial'), + ('admin', '0010_auto_20170215_1328'), + ] + + operations = [ + migrations.AddField( + model_name='domain', + name='transport', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='transport.Transport'), + ), + ] diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index eee336dbf..04c1f3518 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -55,6 +55,9 @@ class Domain(AdminObject): help_text=ugettext_lazy("Check to enable DNS checks for this domain") ) + transport = models.OneToOneField( + "transport.Transport", null=True, on_delete=models.SET_NULL) + class Meta: permissions = ( ("view_domain", "View domain"), @@ -94,7 +97,15 @@ class Domain(AdminObject): for dt in constants.DOMAIN_TYPES: if self.type == dt[0]: label = dt[1] - return [{"name": self.type, "label": label, "type": "dom"}] + result = [{"name": self.type, "label": label, "type": "dom"}] + if self.transport: + result.append({ + "name": self.transport.service, + "label": self.transport.service, + "type": "srv", + "color": "info" + }) + return result @property def admins(self): diff --git a/modoboa/admin/static/admin/js/admin.js b/modoboa/admin/static/admin/js/admin.js index c74081545..47ce73aab 100644 --- a/modoboa/admin/static/admin/js/admin.js +++ b/modoboa/admin/static/admin/js/admin.js @@ -128,6 +128,7 @@ Domains.prototype = { this.options.navigation_params.push("domfilter", "srvfilter"); this.options.eor_message = gettext("No more domain to show"); this.register_tag_handler("dom"); + this.register_tag_handler("srv", this.srv_tag_handler); }, /** @@ -178,6 +179,22 @@ Domains.prototype = { this.init_domain_links(data); }, + srv_tag_handler: function(tag, $link) { + if (this.navobj.getparam(tag + "filter") === undefined && $link.hasClass(tag)) { + var text = $link.attr("name"); + this.navobj + .setparam("domfilter", "relaydomain") + .setparam(tag + "filter", text) + .update(); + if ($("a[name=dom]").length === 0) { + $("#taglist").append(this.make_tag("relaydomain", "dom")); + } + $("#taglist").append(this.make_tag(text, tag)); + return true; + } + return false; + }, + /** * A new page has been received, inject it. * diff --git a/modoboa/admin/templates/admin/domains.html b/modoboa/admin/templates/admin/domains.html index 2493a6caf..10f18baa6 100644 --- a/modoboa/admin/templates/admin/domains.html +++ b/modoboa/admin/templates/admin/domains.html @@ -1,5 +1,6 @@ {% extends "admin/listing.html" %} -{% load i18n admin_tags lib_tags %} + +{% load i18n admin_tags lib_tags static%} {% block pagetitle %}{% trans "Domains" %}{% endblock %} @@ -9,14 +10,15 @@ {% endblock %} {% block extra_js %} -{{ block.super }} - + {{ block.super }} + + {% extra_static_content "domains" "js" user %} {% endblock %} diff --git a/modoboa/lib/tests.py b/modoboa/lib/tests.py index aac065fcb..e8c963af7 100644 --- a/modoboa/lib/tests.py +++ b/modoboa/lib/tests.py @@ -119,7 +119,15 @@ class U2UTestCase(TestCase): "votre annonce", "Accusé de réception de votre annonce"), ("Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord", - "Sm\xf6rg\xe5sbord") + "Sm\xf6rg\xe5sbord"), + # The following case currently fails because of the way we split + # encoded words to parse them separately, which can lead to + # unexpected unicode decode errors... I think it will work fine on + # Python3 + # ("=?utf-8?B?VMOpbMOpcMOpYWdlIFZJTkNJIEF1dG9yb3V0ZXMgLSBFeHDD?=\n" + # "=?utf-8?B?qWRpdGlvbiBkZSB2b3RyZSBjb21tYW5kZSBOwrAgMjAxNzEyMDcw" + # "MDA1?=\n=?utf-8?B?MyBkdSAwNy8xMi8yMDE3IDE0OjQ5OjQx?=", + # "") ] for sample in samples: self.assertEqual(u2u_decode.u2u_decode(sample[0]), sample[1]) diff --git a/modoboa/lib/u2u_decode.py b/modoboa/lib/u2u_decode.py index e8aa6ee03..3d8e97621 100644 --- a/modoboa/lib/u2u_decode.py +++ b/modoboa/lib/u2u_decode.py @@ -1,6 +1,11 @@ # coding: utf-8 -"""Unstructured rfc2047 header to unicode.""" +""" +Unstructured rfc2047 header to unicode. + +A stupid (and not accurate) answer to https://bugs.python.org/issue1079. + +""" from __future__ import unicode_literals diff --git a/modoboa/relaydomains/forms.py b/modoboa/relaydomains/forms.py index 7a1d2b134..ca9316aac 100644 --- a/modoboa/relaydomains/forms.py +++ b/modoboa/relaydomains/forms.py @@ -2,10 +2,7 @@ from __future__ import unicode_literals -from django import forms - from modoboa.lib.form_utils import WizardStep -from modoboa.transport import backends as tr_backends from modoboa.transport import forms as tr_forms from modoboa.transport import models as tr_models @@ -19,29 +16,16 @@ class RelayDomainWizardStep(WizardStep): return wizard.steps[0].form.cleaned_data["type"] == "relaydomain" -class RelayDomainFormGeneral(tr_forms.BackendSettingsMixin, forms.ModelForm): - """A form to display transport of type relay.""" +class RelayDomainFormGeneral(tr_forms.TransportForm): + """A form to display transport.""" - class Meta(object): + class Meta: model = tr_models.Transport exclude = [ - "creation", "pattern", "service", "next_hop", "enabled", + "creation", "pattern", "next_hop", "enabled", "_settings" ] - def __init__(self, *args, **kwargs): - super(RelayDomainFormGeneral, self).__init__(*args, **kwargs) - settings = tr_backends.manager.get_backend_settings("relay") - self.inject_backend_settings("relay", settings) - - def clean(self): - """Check values.""" - cleaned_data = super(RelayDomainFormGeneral, self).clean() - if self.errors: - return cleaned_data - self.clean_backend_fields("relay") - return cleaned_data - def save(self, *args, **kwargs): """Custom save method.""" domain = kwargs.pop("domain", None) @@ -52,7 +36,7 @@ class RelayDomainFormGeneral(tr_forms.BackendSettingsMixin, forms.ModelForm): instance = super(RelayDomainFormGeneral, self).save(commit=False) instance.pattern = domain.name instance.service = "relay" - instance.enabled = domain.enabled - self.backend.serialize(instance) instance.save() + domain.transport = instance + domain.save(update_fields=["transport"]) return instance diff --git a/modoboa/relaydomains/handlers.py b/modoboa/relaydomains/handlers.py index 107ef2b91..75ab11b61 100644 --- a/modoboa/relaydomains/handlers.py +++ b/modoboa/relaydomains/handlers.py @@ -40,7 +40,7 @@ def clean_domain(sender, instance, **kwargs): tr_models.Transport.objects.filter(pattern=instance.name).delete() -@receiver(signals.pre_delete, sender=admin_models.Domain) +@receiver(signals.post_delete, sender=admin_models.Domain) def delete_transport(sender, instance, **kwargs): """Delete Transport instance if any.""" tr_models.Transport.objects.filter(pattern=instance.name).delete() @@ -65,9 +65,9 @@ def extra_domain_form(sender, user, **kwargs): if not domain or domain.type != "relaydomain": return [] return [{ - "id": "relaydomain", "title": _("Relay settings"), + "id": "relaydomain", "title": _("Transport settings"), "cls": forms.RelayDomainFormGeneral, - "formtpl": "relaydomains/relaydomain_form.html" + "formtpl": "transport/_transport_form.html" }] @@ -81,8 +81,7 @@ def fill_domain_instances(sender, user, domain, **kwargs): if condition: return {} return { - "relaydomain": ( - tr_models.Transport.objects.filter(pattern=domain.name).first()) + "relaydomain": domain.transport } @@ -90,8 +89,8 @@ def fill_domain_instances(sender, user, domain, **kwargs): def extra_wizard_step(sender, **kwargs): """Return a step to configure the relay settings.""" return [forms.RelayDomainWizardStep( - "relay", forms.RelayDomainFormGeneral, _("Relay domain"), - "relaydomains/relaydomain_form.html" + "relay", forms.RelayDomainFormGeneral, _("Transport"), + "transport/_transport_form.html" )] diff --git a/modoboa/relaydomains/lib.py b/modoboa/relaydomains/lib.py index 8ad4fdb74..e812ffb27 100644 --- a/modoboa/relaydomains/lib.py +++ b/modoboa/relaydomains/lib.py @@ -28,4 +28,4 @@ def import_relaydomain(user, row, formopts): transport = tr_models.Transport( pattern=domain.name, service="relay", _settings=settings) tr_backends.manager.get_backend("relay").serialize(transport) - transport.save(creator=user) + transport.save() diff --git a/modoboa/relaydomains/migrations/0008_auto_20171123_1653.py b/modoboa/relaydomains/migrations/0008_auto_20171123_1653.py index 662bddd34..e92b8e3af 100644 --- a/modoboa/relaydomains/migrations/0008_auto_20171123_1653.py +++ b/modoboa/relaydomains/migrations/0008_auto_20171123_1653.py @@ -10,14 +10,12 @@ def move_relaydomain_to_transport(apps, schema_editor): RelayDomain = apps.get_model("relaydomains", "RelayDomain") RecipientAccess = apps.get_model("relaydomains", "RecipientAccess") Transport = apps.get_model("transport", "Transport") - tr_to_create = [] ra_to_create = [] for rd in RelayDomain.objects.select_related("domain", "service"): next_hop = "[{}]:{}".format(rd.target_host, rd.target_port) - tr = Transport( + tr = Transport.objects.create( pattern=rd.domain.name, service="relay", - enabled=rd.domain.enabled, next_hop=next_hop, _settings={ "relay_target_host": rd.target_host, @@ -25,13 +23,13 @@ def move_relaydomain_to_transport(apps, schema_editor): "relay_verify_recipients": rd.verify_recipients } ) - tr_to_create.append(tr) + rd.domain.transport = tr + rd.domain.save(update_fields=["transport"]) if not rd.verify_recipients: continue ra_to_create.append( RecipientAccess( pattern=rd.domain.name, action="reject_unverified_recipient")) - Transport.objects.bulk_create(tr_to_create) RecipientAccess.objects.bulk_create(ra_to_create) @@ -45,6 +43,7 @@ class Migration(migrations.Migration): dependencies = [ ('relaydomains', '0007_recipientaccess'), ('transport', '0001_initial'), + ('admin', '0011_domain_transport'), ] operations = [ diff --git a/modoboa/relaydomains/serializers.py b/modoboa/relaydomains/serializers.py new file mode 100644 index 000000000..9c86efff8 --- /dev/null +++ b/modoboa/relaydomains/serializers.py @@ -0,0 +1,68 @@ +"""RelayDomain serializers.""" + +import json + +from django.utils.translation import ugettext as _ + +from rest_framework import serializers + +from modoboa.admin import models as admin_models +from modoboa.transport import backends as tr_backends +from modoboa.transport import models as tr_models + + +class TransportSerializer(serializers.ModelSerializer): + """Transport serializer.""" + + class Meta: + model = tr_models.Transport + fields = ("pk", "service", "_settings") + + def validate(self, data): + """Check fields based on backend.""" + self.backend = tr_backends.manager.get_backend(data["service"]) + if not self.backend: + raise serializers.ValidationError({ + "service": _("Unsupported service") + }) + data["_settings"] = json.loads(data["_settings"]) + errors = self.backend.clean_fields(data["_settings"]) + if errors: + raise serializers.ValidationError({ + "_settings": ",".join( + ["{}: {}".format(error[0], error[1]) for error in errors]) + }) + return data + + +class RelayDomainSerializer(serializers.ModelSerializer): + """RelayDomain serializer class.""" + + transport = TransportSerializer() + + class Meta: + model = admin_models.Domain + fields = ("pk", "name", "enabled", "transport") + + def create(self, validated_data): + """Use backend to serialize data.""" + transport = tr_models.Transport(**validated_data.pop("transport")) + transport.pattern = validated_data["name"] + transport.save() + domain = admin_models.Domain(**validated_data) + domain.type = "relaydomain" + domain.transport = transport + domain.save(creator=self.context["request"].user) + return domain + + def update(self, instance, validated_data): + """Use backend to serialize data.""" + transport_data = validated_data.pop("transport") + for key, value in validated_data.items(): + setattr(instance, key, value) + instance.save() + for key, value in transport_data.items(): + setattr(instance.transport, key, value) + instance.transport.pattern = instance.name + instance.transport.save() + return instance diff --git a/modoboa/relaydomains/templates/relaydomains/new_relaydomain_form.html b/modoboa/relaydomains/templates/relaydomains/new_relaydomain_form.html deleted file mode 100644 index 0ef3c7f89..000000000 --- a/modoboa/relaydomains/templates/relaydomains/new_relaydomain_form.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "common/generic_modal_form.html" %} - -{% block formbody %} - {% include "relaydomains/relaydomain_form.html" %} -{% endblock %} diff --git a/modoboa/relaydomains/templates/relaydomains/relaydomain_form.html b/modoboa/relaydomains/templates/relaydomains/relaydomain_form.html deleted file mode 100644 index b5336f223..000000000 --- a/modoboa/relaydomains/templates/relaydomains/relaydomain_form.html +++ /dev/null @@ -1,5 +0,0 @@ -{% load form_tags %} - -{% render_field form.relay_target_host %} -{% render_field form.relay_target_port %} -{% render_field form.relay_verify_recipients %} diff --git a/modoboa/relaydomains/templates/relaydomains/service_field.html b/modoboa/relaydomains/templates/relaydomains/service_field.html deleted file mode 100644 index 94d047a0e..000000000 --- a/modoboa/relaydomains/templates/relaydomains/service_field.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends "common/generic_field.html" %}{% load i18n %} -{% block extra_controls %} -{% trans "Scan" %} -{% endblock %} diff --git a/modoboa/relaydomains/tests.py b/modoboa/relaydomains/tests.py index 86c1e4fef..4f8ddecb7 100644 --- a/modoboa/relaydomains/tests.py +++ b/modoboa/relaydomains/tests.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals +import json + from django.core.files.base import ContentFile from django.urls import reverse from django.test import TestCase @@ -9,7 +11,7 @@ from django.test import TestCase from modoboa.admin import factories as admin_factories from modoboa.admin import models as admin_models from modoboa.core.factories import UserFactory -from modoboa.lib.tests import ModoTestCase +from modoboa.lib.tests import ModoAPITestCase, ModoTestCase from modoboa.lib.test_utils import MapFilesTestCaseMixin from modoboa.limits import utils as limits_utils from modoboa.transport import factories as tr_factories @@ -25,6 +27,7 @@ class Operations(object): "name": name, "create_dom_admin": False, "type": "relaydomain", + "service": "relay", "relay_target_host": "external.host.tld", "relay_target_port": 25, "enabled": True, @@ -42,6 +45,7 @@ class Operations(object): transport = tr_models.Transport.objects.get(pattern=domain.name) values = { "name": domain.name, + "service": "relay", "relay_target_host": transport._settings["relay_target_host"], "relay_target_port": transport._settings["relay_target_port"], "type": "relaydomain", @@ -75,15 +79,17 @@ class RelayDomainsTestCase(ModoTestCase, Operations): """Create test data.""" super(RelayDomainsTestCase, cls).setUpTestData() admin_factories.populate_database() - cls.dom = admin_factories.DomainFactory( - name="relaydomain.tld", type="relaydomain") cls.transport = tr_factories.TransportFactory( - pattern=cls.dom.name, _settings={ + pattern="relaydomain.tld", service="relay", + _settings={ "relay_target_host": "external.host.tld", "relay_target_port": "25", "relay_verify_recipients": False } ) + cls.dom = admin_factories.DomainFactory( + name="relaydomain.tld", type="relaydomain", + transport=cls.transport) admin_factories.DomainAliasFactory( name="relaydomainalias.tld", target=cls.dom) admin_factories.MailboxFactory( @@ -158,6 +164,7 @@ class RelayDomainsTestCase(ModoTestCase, Operations): """ values = { "name": "relaydomain.org", + "service": "relay", "relay_target_host": self.transport._settings["relay_target_host"], "relay_target_port": 4040, "relay_verify_recipients": True, @@ -193,6 +200,7 @@ class RelayDomainsTestCase(ModoTestCase, Operations): "quota": 0, "default_mailbox_quota": 0, "enabled": True, + "service": "relay", "relay_target_host": self.transport._settings["relay_target_host"], "relay_target_port": self.transport._settings["relay_target_port"] } @@ -200,9 +208,8 @@ class RelayDomainsTestCase(ModoTestCase, Operations): reverse("admin:domain_change", args=[domain_pk]), values) with self.assertRaises(tr_models.Transport.DoesNotExist): self.transport.refresh_from_db() - self.assertEqual( - admin_models.Domain.objects.get(name="relaydomain.tld").type, - "domain") + self.dom.refresh_from_db() + self.assertEqual(self.dom.type, "domain") values = { "name": "relaydomain.tld", "type": "relaydomain", @@ -223,6 +230,7 @@ class RelayDomainsTestCase(ModoTestCase, Operations): """ values = { "name": "relaydomain.org", + "service": "relay", "relay_target_host": self.transport._settings["relay_target_host"], "relay_target_port": self.transport._settings["relay_target_port"], "type": "relaydomain", @@ -360,3 +368,102 @@ class MapFilesTestCase(MapFilesTestCaseMixin, TestCase): "sql-relaydomains.cf", "sql-relay-recipient-verification.cf" ] + + +class DataMixin(object): + """A mixin to provide test data.""" + + @classmethod + def setUpTestData(cls): + super(DataMixin, cls).setUpTestData() + transport = tr_factories.TransportFactory( + pattern="test.com", _settings={ + "relay_target_host": "external.host.tld", + "relay_target_port": 25, + "relay_verify_recipients": False + } + ) + cls.domain1 = admin_factories.DomainFactory( + name="test.com", type="relaydomain", transport=transport) + transport = tr_factories.TransportFactory( + pattern="domain2.test", _settings={ + "relay_target_host": "external.host.tld", + "relay_target_port": 25, + "relay_verify_recipients": True + } + ) + cls.domain2 = admin_factories.DomainFactory( + name="test2.com", type="relaydomain", transport=transport) + + +class RelayDomainAPITestCase(DataMixin, ModoAPITestCase): + """API test cases.""" + + def test_list(self): + """Test list service.""" + url = reverse("api:relaydomain-list") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 2) + + def test_create(self): + """Test create service.""" + url = reverse("api:relaydomain-list") + settings = { + "relay_target_host": "1.2.3.4" + } + data = { + "name": "test3.com", + "transport": { + "service": "relay", + "_settings": json.dumps(settings) + } + } + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.data["transport"]["_settings"], + ["relay_target_port: This field is required"] + ) + + settings.update({"relay_target_port": 25}) + data["transport"]["_settings"] = json.dumps(settings) + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + domain = admin_models.Domain.objects.get(name="test3.com") + self.assertEqual( + domain.transport.next_hop, "[{}]:{}".format( + settings["relay_target_host"], settings["relay_target_port"]) + ) + + def test_update(self): + """Test update service.""" + url = reverse("api:relaydomain-detail", args=[self.domain1.pk]) + settings = self.domain1.transport._settings.copy() + settings.update({"relay_target_port": 1000}) + data = { + "name": "test3.com", + "transport": { + "service": "relay", + "_settings": json.dumps(settings) + } + } + response = self.client.put(url, data, format="json") + self.assertEqual(response.status_code, 200) + self.domain1.refresh_from_db() + self.domain1.transport.refresh_from_db() + self.assertEqual(self.domain1.name, data["name"]) + self.assertEqual( + self.domain1.transport.next_hop, "[{}]:{}".format( + settings["relay_target_host"], settings["relay_target_port"]) + ) + + def test_delete(self): + """Test delete service.""" + url = reverse("api:relaydomain-detail", args=[self.domain1.pk]) + response = self.client.delete(url) + self.assertEqual(response.status_code, 204) + with self.assertRaises(admin_models.Domain.DoesNotExist): + self.domain1.refresh_from_db() + self.assertFalse( + tr_models.Transport.objects.filter(pattern="test.com").exists()) diff --git a/modoboa/transport/urls_api.py b/modoboa/relaydomains/urls_api.py similarity index 68% rename from modoboa/transport/urls_api.py rename to modoboa/relaydomains/urls_api.py index e85766204..a7ae5cfae 100644 --- a/modoboa/transport/urls_api.py +++ b/modoboa/relaydomains/urls_api.py @@ -7,5 +7,5 @@ from . import viewsets router = routers.SimpleRouter() router.register( - r"transports", viewsets.TransportViewSet, base_name="transport") + r"relaydomains", viewsets.RelayDomainViewSet, base_name="relaydomain") urlpatterns = router.urls diff --git a/modoboa/relaydomains/viewsets.py b/modoboa/relaydomains/viewsets.py new file mode 100644 index 000000000..681f515bc --- /dev/null +++ b/modoboa/relaydomains/viewsets.py @@ -0,0 +1,26 @@ +"""RelayDomain viewsets.""" + +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions + +from modoboa.admin import models as admin_models + +from . import serializers + + +class RelayDomainViewSet(viewsets.ModelViewSet): + """RelayDomain viewset.""" + + permission_classes = [IsAuthenticated, DjangoModelPermissions, ] + serializer_class = serializers.RelayDomainSerializer + + def get_queryset(self): + """Filter queryset based on current user.""" + return ( + admin_models.Domain.objects.get_for_admin(self.request.user) + .filter(type="relaydomain") + ) + + def perform_destroy(self, instance): + """Add custom args to delete call.""" + instance.delete(self.request.user) diff --git a/modoboa/transport/admin.py b/modoboa/transport/admin.py deleted file mode 100644 index 8c38f3f3d..000000000 --- a/modoboa/transport/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/modoboa/transport/factories.py b/modoboa/transport/factories.py index 56eb88735..332bd400a 100644 --- a/modoboa/transport/factories.py +++ b/modoboa/transport/factories.py @@ -2,12 +2,10 @@ import factory -from modoboa.core.factories import PermissionFactory - from . import models -class TransportFactory(PermissionFactory): +class TransportFactory(factory.django.DjangoModelFactory): """Factory for Transport.""" class Meta: @@ -17,4 +15,3 @@ class TransportFactory(PermissionFactory): pattern = factory.Sequence(lambda n: "transport{}".format(n)) service = "relay" next_hop = "[external.host.tld]:25" - diff --git a/modoboa/transport/forms.py b/modoboa/transport/forms.py index fbd69845e..df46b5a56 100644 --- a/modoboa/transport/forms.py +++ b/modoboa/transport/forms.py @@ -19,7 +19,7 @@ class BackendSettingsMixin(object): def __init__(self, *args, **kwargs): """Constructor.""" super(BackendSettingsMixin, self).__init__(*args, **kwargs) - self.setting_fields = [] + self.setting_field_names = [] def inject_backend_settings(self, name, settings): """Inject backend settings to form.""" @@ -35,7 +35,7 @@ class BackendSettingsMixin(object): options["widget"] = setting["widget"] self.fields[fullname] = TYPE_TO_FIELD_MAP[ftype]( label=setting["label"], required=False, **options) - self.setting_fields.append(fullname) + self.setting_field_names.append(fullname) def clean_backend_fields(self, name): """Clean backend fields.""" @@ -47,7 +47,8 @@ class BackendSettingsMixin(object): """Set settings to JSON field.""" transport = super(BackendSettingsMixin, self).save(commit=False) transport._settings = { - name: self.cleaned_data[name] for name in self.setting_fields + name: self.cleaned_data[name] + for name in self.setting_field_names } if commit: transport.save() @@ -60,7 +61,7 @@ class TransportForm(BackendSettingsMixin, forms.ModelForm): service = forms.ChoiceField(choices=[]) class Meta: - fields = ("pattern", "service", "enabled") + fields = ("pattern", "service") model = models.Transport def __init__(self, *args, **kwargs): @@ -71,6 +72,10 @@ class TransportForm(BackendSettingsMixin, forms.ModelForm): for name, backend_settings in settings.items(): self.inject_backend_settings(name, backend_settings) + @property + def setting_fields(self): + return [self[name] for name in self.setting_field_names] + def _clean_fields(self): """Make backend settings required.""" backend_name = self.data.get("service") diff --git a/modoboa/transport/handlers.py b/modoboa/transport/handlers.py index aae87601a..1e69bf921 100644 --- a/modoboa/transport/handlers.py +++ b/modoboa/transport/handlers.py @@ -2,30 +2,26 @@ from __future__ import unicode_literals -from django.urls import reverse +from django.db.models import signals from django.dispatch import receiver -from django.utils.translation import ugettext as _ -from modoboa.admin import signals as admin_signals from modoboa.core import signals as core_signals +from . import backends +from . import models from . import postfix_maps -@receiver(admin_signals.extra_domain_menu_entries) -def extra_domain_menu_entries(sender, user, **kwargs): - """Add extra menu entries.""" - return [{ - "name": "transports", - "label": _("Transports"), - "url": reverse("transport:transport_list"), - "img": "fa fa-bus", - }] - - @receiver(core_signals.register_postfix_maps) def register_postfix_maps(sender, **kwargs): """Register postfix maps.""" return [ postfix_maps.TransportMap, ] + + +@receiver(signals.pre_save, sender=models.Transport) +def serialize_transport_settings(sender, instance, **kwargs): + """Call backend serialize method on transport.""" + backend = backends.manager.get_backend(instance.service) + backend.serialize(instance) diff --git a/modoboa/transport/migrations/0001_initial.py b/modoboa/transport/migrations/0001_initial.py index 1c659d5e5..771b6a1ad 100644 --- a/modoboa/transport/migrations/0001_initial.py +++ b/modoboa/transport/migrations/0001_initial.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.db import migrations, models -import django.utils.timezone + import jsonfield.fields @@ -19,12 +19,9 @@ class Migration(migrations.Migration): name='Transport', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creation', models.DateTimeField(default=django.utils.timezone.now)), - ('last_modification', models.DateTimeField(auto_now=True)), ('pattern', models.CharField(max_length=254, unique=True, verbose_name='pattern')), ('service', models.CharField(max_length=30, verbose_name='service')), ('next_hop', models.CharField(blank=True, max_length=100, verbose_name='next hop')), - ('enabled', models.BooleanField(default=True, verbose_name='enabled')), ('_settings', jsonfield.fields.JSONField(default={})), ], options={ diff --git a/modoboa/transport/models.py b/modoboa/transport/models.py index 50efdede3..e8b011008 100644 --- a/modoboa/transport/models.py +++ b/modoboa/transport/models.py @@ -9,16 +9,15 @@ from django.utils.translation import ugettext_lazy as _ import jsonfield -from modoboa.admin import models as admin_models +from . import backends -class Transport(admin_models.AdminObject): +class Transport(models.Model): """Transport table.""" pattern = models.CharField(_("pattern"), unique=True, max_length=254) service = models.CharField(_("service"), max_length=30) next_hop = models.CharField(_("next hop"), max_length=100, blank=True) - enabled = models.BooleanField("enabled", default=True) _settings = jsonfield.JSONField(default={}) @@ -28,5 +27,12 @@ class Transport(admin_models.AdminObject): def __str__(self): return self.pattern + @property + def backend(self): + """Shortcut to access backend.""" + if not self.service: + return None + return backends.manager.get_backend(self.service) + reversion.register(Transport) diff --git a/modoboa/transport/serializers.py b/modoboa/transport/serializers.py deleted file mode 100644 index 82a145e4d..000000000 --- a/modoboa/transport/serializers.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Transport serializers.""" - -import json - -from django.utils.translation import ugettext as _ - -from rest_framework import serializers - -from . import backends -from . import models - - -class TransportSerializer(serializers.ModelSerializer): - """Transport serializer class.""" - - class Meta: - model = models.Transport - fields = ("pk", "pattern", "service", "enabled", "_settings") - - def validate(self, data): - """Check fields based on backend.""" - self.backend = backends.manager.get_backend(data["service"]) - if not self.backend: - raise serializers.ValidationError({ - "service": _("Unsupported service") - }) - data["_settings"] = json.loads(data["_settings"]) - errors = self.backend.clean_fields(data["_settings"]) - if errors: - raise serializers.ValidationError({ - "_settings": ",".join( - ["{}: {}".format(error[0], error[1]) for error in errors]) - }) - return data - - def create(self, validated_data): - """Use backend to serialize data.""" - transport = models.Transport(**validated_data) - self.backend.serialize(transport) - transport.save(creator=self.context["request"].user) - return transport - - def update(self, instance, validated_data): - """Use backend to serialize data.""" - for key, value in validated_data.items(): - setattr(instance, key, value) - self.backend.serialize(instance) - instance.save() - return instance diff --git a/modoboa/transport/static/transport/js/transport.js b/modoboa/transport/static/transport/js/transport.js index 801743e6c..c6b1a695a 100644 --- a/modoboa/transport/static/transport/js/transport.js +++ b/modoboa/transport/static/transport/js/transport.js @@ -18,26 +18,8 @@ function transportFormCallback () { displayBackendSettings($(this).val()); }); displayBackendSettings($service_field.val()); - - $('.submit').one('click', $.proxy(function(e) { - simple_ajax_form_post(e, { - formid: 'transport_form', - error_cb: transportFormCallback - }); - }, this)); } $(document).ready(function () { - $('#searchquery').focus(function () { - $(this).val(''); - }).blur(function (e) { - var $this = $(this); - if ($this.val() === '') { - $this.val(gettext('Search')); - } - }); - $('a[name=deltransport]').confirm({ - question: function() { return this.$element.attr('title'); }, - method: 'DELETE' - }); + $(this).bind('domform_init', transportFormCallback); }); diff --git a/modoboa/transport/templates/transport/_transport_form.html b/modoboa/transport/templates/transport/_transport_form.html index fcb854061..291feb273 100644 --- a/modoboa/transport/templates/transport/_transport_form.html +++ b/modoboa/transport/templates/transport/_transport_form.html @@ -1,14 +1,8 @@ -{% extends "common/generic_modal_form.html" %} - {% load form_tags %} -{% block formbody %} - {% render_field form.pattern %} - {% render_field form.service %} - {% render_field form.enabled %} -
| {% trans "Pattern" %} | -{% trans "Service" %} | -{% trans "Actions" %} | -
|---|---|---|
| {{ transport.pattern }}{% if not transport.enabled %} ({% trans "disabled" %}){% endif %} | -- {{ transport.service }} - | -- - - - - - - | -
|
- {% trans "No transport defined yet." %}
- | ||