Reintroduced relay domain concept. (#1334)

* Reintroduced relay domain concept.

* Removed wrong url include.

* Removed deprecated tests.

* Updated requirement.
This commit is contained in:
Antoine Nguyen
2017-12-21 11:15:02 +01:00
committed by GitHub
parent bc1832fff1
commit 36a434f346
35 changed files with 342 additions and 596 deletions
@@ -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'),
),
]
+12 -1
View File
@@ -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):
+17
View File
@@ -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.
*
+11 -9
View File
@@ -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 }}
<script type="text/javascript">
$(document).ready(function() {
admin = new Domains({
load_page_url: "{% url 'admin:_domain_list' %}"
});
});
</script>
{{ block.super }}
<script src="{% static 'transport/js/transport.js' %}" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
admin = new Domains({
load_page_url: "{% url 'admin:_domain_list' %}"
});
});
</script>
{% extra_static_content "domains" "js" user %}
{% endblock %}
+9 -1
View File
@@ -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])
+6 -1
View File
@@ -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
+6 -22
View File
@@ -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
+6 -7
View File
@@ -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"
)]
+1 -1
View File
@@ -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()
@@ -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 = [
+68
View File
@@ -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
@@ -1,5 +0,0 @@
{% extends "common/generic_modal_form.html" %}
{% block formbody %}
{% include "relaydomains/relaydomain_form.html" %}
{% endblock %}
@@ -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 %}
@@ -1,4 +0,0 @@
{% extends "common/generic_field.html" %}{% load i18n %}
{% block extra_controls %}
<a id="btnscan" class="btn btn-success" style="margin-top: 2px" href="{% url 'relaydomains:service_scan' %}">{% trans "Scan" %}</a>
{% endblock %}
+114 -7
View File
@@ -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())
@@ -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
+26
View File
@@ -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)
-3
View File
@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.
+1 -4
View File
@@ -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"
+9 -4
View File
@@ -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")
+10 -14
View File
@@ -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)
+1 -4
View File
@@ -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={
+9 -3
View File
@@ -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)
-49
View File
@@ -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
@@ -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);
});
@@ -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 %}
<div id="backend_settings">
{% for field in setting_fields %}
{% render_field field hidden=True %}
{% endfor %}
</div>
{% endblock %}
{% render_field form.service %}
<div id="backend_settings">
{% for field in form.setting_fields %}
{% render_field field hidden=True %}
{% endfor %}
</div>
@@ -1,5 +0,0 @@
{% load i18n %}
<th id="name" class="sortable" data-sort_order="name" width="">{% trans "Name" %}</th>
<th id="tags" class="sortable" data-sort_order="tags" width="">{% trans "Tags" %}</th>
<th id="actions" width="">{% trans "Actions" %}</th>
@@ -1,66 +0,0 @@
{% extends "twocols.html" %}
{% load i18n admin_tags lib_tags static %}
{% block pagetitle %}{% trans "Transports" %}{% endblock %}
{% block leftcol %}
<ul class="nav nav-sidebar">
{% domains_menu "transports" user False %}
</ul>
{% endblock %}
{% block apparea %}
<div class="row">
<div class="col-sm-5">
{% include "admin/searchform.html" %}
</div>
<div class="col-sm-3">
{% if perms.transport.add_transport %}
<a href="{% url 'transport:transport_create' %}" class="btn btn-primary" data-toggle="ajaxmodal" modalcb="transportFormCallback">
<span class="fa fa-plus"></span> {% trans "Add" %}
</a>
{% endif %}
</div>
<div id="taglist" class="col-sm-4"> </div>
</div>
<table class="table table-hover">
<thead>
<tr>
<th id="pattern" class="sortable" data-sort_order="pattern" width="">{% trans "Pattern" %}</th>
<th id="service" class="sortable" data-sort_order="service" width="">{% trans "Service" %}</th>
<th id="actions" width="">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for transport in object_list %}
<tr class="{% if not transport.enabled %}text-muted{% endif %}">
<td>{{ transport.pattern }}{% if not transport.enabled %} ({% trans "disabled" %}){% endif %}</td>
<td><span class="label label-default">
<a href="#" class="filter transport">{{ transport.service }}</a>
</span></td>
<td name="actions">
<a href="{% url 'transport:transport_update' transport.pk %}"
data-toggle="ajaxmodal" modalcb="transportFormCallback">
<span class="fa fa-edit"></span>
</a>
<a href="{% url 'transport:transport_delete' transport.pk %}"
name="deltransport" title="{% blocktrans with pattern=transport.pattern %}Delete {{ pattern }}? {% endblocktrans %}">
<span class="fa fa-trash"></span>
</a>
</td>
</tr>
{% empty %}
<tr><td colspan="3">
<div class="alert alert-info">{% trans "No transport defined yet." %}</div>
</td></tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<script type="text/javascript" src="{% static 'transport/js/transport.js' %}"></script>
{% endblock %}
-163
View File
@@ -1,163 +0,0 @@
"""Transport tests."""
import json
from django.urls import reverse
from modoboa.lib.tests import ModoAPITestCase, ModoTestCase
from . import factories
from . import models
class DataMixin(object):
"""A mixin to provide test data."""
@classmethod
def setUpTestData(cls):
super(DataMixin, cls).setUpTestData()
cls.transport1 = factories.TransportFactory(
pattern="domain1.test", _settings={
"relay_target_host": "external.host.tld",
"relay_target_port": 25,
"relay_verify_recipients": False
}
)
cls.transport2 = factories.TransportFactory(
pattern="domain2.test", _settings={
"relay_target_host": "external.host.tld",
"relay_target_port": 25,
"relay_verify_recipients": True
}
)
class TransportViewsTestCase(DataMixin, ModoTestCase):
"""Test transport views."""
def test_transport_list(self):
"""Test list view."""
url = reverse("transport:transport_list")
response = self.client.get(url)
self.assertContains(response, self.transport1.pattern)
self.assertContains(response, self.transport2.pattern)
url = "{}?searchquery=domain1".format(url)
response = self.client.get(url)
self.assertContains(response, self.transport1.pattern)
self.assertNotContains(response, self.transport2.pattern)
def test_create_transport(self):
"""Test create view."""
url = reverse("transport:transport_create")
response = self.client.get(url)
self.assertContains(response, "New transport")
data = {
"pattern": "domain3.test",
"service": "relay",
"relay_target_host": "1.2.3.4",
}
response = self.ajax_post(url, data, 400)
data.update({"relay_target_port": 25})
self.ajax_post(url, data)
transport = models.Transport.objects.get(pattern="domain3.test")
self.assertEqual(
transport.next_hop, "[{}]:{}".format(
data["relay_target_host"], data["relay_target_port"])
)
def test_update_transport(self):
"""Test update view."""
url = reverse("transport:transport_update", args=[self.transport1.pk])
response = self.client.get(url)
self.assertContains(response, "Edit transport")
data = {
"pattern": self.transport1.pattern,
"service": self.transport1.service,
"relay_target_host": "1.2.3.5",
"relay_target_port": 234,
"relay_verify_recipients": True
}
self.ajax_post(url, data)
self.transport1.refresh_from_db()
self.assertTrue(
self.transport1._settings["relay_verify_recipients"])
self.assertEqual(
self.transport1.next_hop, "[{}]:{}".format(
data["relay_target_host"], data["relay_target_port"])
)
def test_delete_transport(self):
"""Test delete view."""
url = reverse("transport:transport_delete", args=[self.transport1.pk])
response = self.client.delete(url)
self.assertEqual(response.status_code, 302)
with self.assertRaises(models.Transport.DoesNotExist):
self.transport1.refresh_from_db()
class TransportAPITestCase(DataMixin, ModoAPITestCase):
"""API test cases."""
def test_list(self):
"""Test list service."""
url = reverse("api:transport-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:transport-list")
settings = {
"relay_target_host": "1.2.3.4"
}
data = {
"pattern": "domain3.test",
"service": "relay",
"_settings": json.dumps(settings)
}
response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.data["_settings"],
["relay_target_port: This field is required"]
)
settings.update({"relay_target_port": 25})
data["_settings"] = json.dumps(settings)
response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, 201)
transport = models.Transport.objects.get(pattern="domain3.test")
self.assertEqual(
transport.next_hop, "[{}]:{}".format(
settings["relay_target_host"], settings["relay_target_port"])
)
def test_update(self):
"""Test update service."""
url = reverse("api:transport-detail", args=[self.transport1.pk])
settings = self.transport1._settings.copy()
settings.update({"relay_target_port": 1000})
data = {
"pattern": "domain3.test",
"service": "relay",
"_settings": json.dumps(settings)
}
response = self.client.put(url, data, format="json")
self.assertEqual(response.status_code, 200)
self.transport1.refresh_from_db()
self.assertEqual(self.transport1.pattern, data["pattern"])
self.assertEqual(
self.transport1.next_hop, "[{}]:{}".format(
settings["relay_target_host"], settings["relay_target_port"])
)
def test_delete(self):
"""Test delete service."""
url = reverse("api:transport-detail", args=[self.transport1.pk])
response = self.client.delete(url)
self.assertEqual(response.status_code, 204)
with self.assertRaises(models.Transport.DoesNotExist):
self.transport1.refresh_from_db()
-17
View File
@@ -1,17 +0,0 @@
"""Transport urls."""
from django.conf.urls import url
from . import views
urlpatterns = [
url(r"^$", views.TransportListView.as_view(),
name="transport_list"),
url(r"^create/$", views.TransportCreateView.as_view(),
name="transport_create"),
url(r"^update/(?P<pk>\d+)/$", views.TransportUpdateView.as_view(),
name="transport_update"),
url(r"^delete/(?P<pk>\d+)/$", views.TransportDeleteView.as_view(),
name="transport_delete"),
]
-142
View File
@@ -1,142 +0,0 @@
"""Transport views."""
from django import http
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext as _, ugettext_lazy
from django.views import generic
from django.contrib.auth import mixins as auth_mixins
from django.contrib import messages
from . import backends
from . import forms
from . import models
class TransportListView(
auth_mixins.PermissionRequiredMixin, generic.ListView):
"""ListView for Transport."""
model = models.Transport
permission_required = "transport.add_transport"
template_name = "transport/transport_list.html"
def get_context_data(self, **kwargs):
"""Inject search query."""
context = super(TransportListView, self).get_context_data(**kwargs)
context.update({
"selection": "domains",
"squery": self.request.GET.get("searchquery")
})
return context
def get_queryset(self):
"""Filter."""
qs = super(TransportListView, self).get_queryset()
searchquery = self.request.GET.get("searchquery")
if searchquery:
qs = qs.filter(pattern__icontains=searchquery)
return qs
class TransportMixin(object):
"""Mixin for Transport creation/update."""
form_class = forms.TransportForm
model = models.Transport
success_message = ugettext_lazy("Transport created")
success_url = reverse_lazy("transport:transport_list")
template_name = "transport/_transport_form.html"
def form_invalid(self, form):
"""Return JSON response."""
return http.JsonResponse({
"form_errors": form.errors,
"prefix": form.prefix
}, status=400)
def form_valid(self, form):
"""Save transport."""
transport = form.save(commit=False)
backend = backends.manager.get_backend(transport.service)
backend.serialize(transport)
options = {}
if not transport.pk:
options.update({"creator": self.request.user})
transport.save(**options)
messages.success(self.request, self.success_message)
return http.JsonResponse({})
def get_context_data(self, **kwargs):
"""Include extra data."""
context = super(TransportMixin, self).get_context_data(
**kwargs)
setting_fields = []
for fname in context["form"].setting_fields:
setting_fields.append(context["form"][fname])
context.update({
"action_classes": "submit",
"formid": "transport_form",
"setting_fields": setting_fields
})
return context
class TransportCreateView(
auth_mixins.PermissionRequiredMixin,
TransportMixin,
generic.CreateView):
"""Transport create view."""
permission_required = "transport.add_transport"
def get_context_data(self, **kwargs):
"""Include extra data."""
context = super(TransportCreateView, self).get_context_data(
**kwargs)
context.update({
"action": reverse("transport:transport_create"),
"action_label": _("Create"),
"title": _("New transport"),
})
return context
class TransportUpdateView(
auth_mixins.PermissionRequiredMixin,
TransportMixin,
generic.UpdateView):
"""Transport update view."""
permission_required = "transport.change_transport"
success_message = ugettext_lazy("Transport updated")
def get_context_data(self, **kwargs):
"""Include extra data."""
context = super(TransportUpdateView, self).get_context_data(
**kwargs)
context.update({
"action": reverse(
"transport:transport_update", args=[self.object.pk]),
"action_label": _("Update"),
"title": _("Edit transport"),
})
return context
class TransportDeleteView(
auth_mixins.PermissionRequiredMixin,
TransportMixin,
generic.DeleteView):
"""Transport delete view."""
model = models.Transport
permission_required = "transport.del_transport"
success_url = reverse_lazy("transport:transport_list")
def delete(self, request, *args, **kwargs):
"""Add message."""
response = super(TransportDeleteView, self).delete(
request, *args, **kwargs)
messages.success(request, _("Transport deleted"))
return response
-18
View File
@@ -1,18 +0,0 @@
"""Transport viewsets."""
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
from . import models
from . import serializers
class TransportViewSet(viewsets.ModelViewSet):
"""Transport viewset."""
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
serializer_class = serializers.TransportSerializer
def get_queryset(self):
"""Filter queryset based on current user."""
return models.Transport.objects.get_for_admin(self.request.user)
-2
View File
@@ -28,8 +28,6 @@ urlpatterns = [
url('', include('modoboa.core.urls', namespace="core")),
url('^user/forward/', forward, name="user_forward"),
url('admin/', include('modoboa.admin.urls', namespace="admin")),
url(r'^transports/',
include("modoboa.transport.urls", namespace="transport")),
# No namespace
url(r'^accounts/password_reset/$', core_views.password_reset,
name="password_reset"),
+1 -1
View File
@@ -10,6 +10,6 @@ from modoboa.core.extensions import exts_pool
urlpatterns = [
url("", include("modoboa.admin.urls_api")),
url("", include("modoboa.limits.urls_api")),
url("", include("modoboa.transport.urls_api")),
url("", include("modoboa.relaydomains.urls_api")),
]
urlpatterns += exts_pool.get_urls(category="api")
+1 -1
View File
@@ -4,7 +4,7 @@ django-ckeditor==5.2.2
django-reversion==2.0.12
django-subcommand2==0.1.1
django-versionfield2>=0.5.0
django-xforwardedfor-middleware
django-xforwardedfor-middleware==2.0
dj-database-url