mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-19 05:58:34 +02:00
@@ -71,6 +71,15 @@ class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
)
|
||||
)
|
||||
|
||||
default_mailbox_quota = forms.IntegerField(
|
||||
label=ugettext_lazy("Default mailbox quota"),
|
||||
initial=0,
|
||||
help_text=ugettext_lazy(
|
||||
"Default mailbox quota (in MB) applied to freshly created domains "
|
||||
"with no value specified. A value of 0 means no quota."
|
||||
)
|
||||
)
|
||||
|
||||
auto_account_removal = YesNoField(
|
||||
label=ugettext_lazy("Automatic account removal"),
|
||||
initial=False,
|
||||
@@ -96,7 +105,8 @@ class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AdminParametersForm, self).__init__(*args, **kwargs)
|
||||
self.field_widths = {
|
||||
"default_domain_quota": 2
|
||||
"default_domain_quota": 2,
|
||||
"default_mailbox_quota": 2
|
||||
}
|
||||
hide_fields = False
|
||||
dpath = None
|
||||
@@ -127,11 +137,19 @@ class AdminParametersForm(param_forms.AdminParametersForm):
|
||||
|
||||
def clean_default_domain_quota(self):
|
||||
"""Ensure quota is a positive integer."""
|
||||
if self.cleaned_data['default_domain_quota'] < 0:
|
||||
if self.cleaned_data["default_domain_quota"] < 0:
|
||||
raise forms.ValidationError(
|
||||
ugettext_lazy('Must be a positive integer')
|
||||
ugettext_lazy("Must be a positive integer")
|
||||
)
|
||||
return self.cleaned_data['default_domain_quota']
|
||||
return self.cleaned_data["default_domain_quota"]
|
||||
|
||||
def clean_default_mailbox_quota(self):
|
||||
"""Ensure quota is a positive integer."""
|
||||
if self.cleaned_data["default_mailbox_quota"] < 0:
|
||||
raise forms.ValidationError(
|
||||
ugettext_lazy("Must be a positive integer")
|
||||
)
|
||||
return self.cleaned_data["default_mailbox_quota"]
|
||||
|
||||
|
||||
def load_admin_settings():
|
||||
|
||||
@@ -16,6 +16,7 @@ class DomainFactory(PermissionFactory):
|
||||
django_get_or_create = ("name", )
|
||||
|
||||
type = "domain"
|
||||
quota = 0
|
||||
default_mailbox_quota = 10
|
||||
enabled = True
|
||||
|
||||
@@ -64,7 +65,7 @@ def populate_database():
|
||||
|
||||
2 domains, 1 domain admin, 2 users
|
||||
"""
|
||||
dom = DomainFactory(name="test.com")
|
||||
dom = DomainFactory(name="test.com", quota=50)
|
||||
admin = UserFactory(
|
||||
username="admin@test.com", groups=('DomainAdmins', ),
|
||||
password='{PLAIN}toto'
|
||||
|
||||
@@ -187,7 +187,6 @@ class AccountFormGeneral(forms.ModelForm):
|
||||
|
||||
|
||||
class AccountFormMail(forms.Form, DynamicForm):
|
||||
|
||||
"""Form to handle mail part."""
|
||||
|
||||
email = lib_fields.UTF8EmailField(
|
||||
@@ -198,8 +197,7 @@ class AccountFormMail(forms.Form, DynamicForm):
|
||||
help_text=_("Quota in MB for this mailbox. Define a custom value or "
|
||||
"use domain's default one. Leave empty to define an "
|
||||
"unlimited value (not allowed for domain "
|
||||
"administrators)."),
|
||||
widget=forms.widgets.TextInput(attrs={"class": "form-control"})
|
||||
"administrators).")
|
||||
)
|
||||
quota_act = forms.BooleanField(required=False)
|
||||
aliases = lib_fields.UTF8AndEmptyUserEmailField(
|
||||
@@ -280,8 +278,11 @@ class AccountFormMail(forms.Form, DynamicForm):
|
||||
Check if quota is >= 0 only when the domain value is not used.
|
||||
"""
|
||||
cleaned_data = super(AccountFormMail, self).clean()
|
||||
if not cleaned_data["email"]:
|
||||
return cleaned_data
|
||||
use_default_domain_quota = cleaned_data["quota_act"]
|
||||
condition = (
|
||||
not cleaned_data["quota_act"] and
|
||||
not use_default_domain_quota and
|
||||
cleaned_data["quota"] is not None and
|
||||
cleaned_data["quota"] < 0)
|
||||
if condition:
|
||||
|
||||
@@ -33,14 +33,6 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
type = forms.ChoiceField(
|
||||
label=ugettext_lazy("Type"),
|
||||
)
|
||||
default_mailbox_quota = forms.IntegerField(
|
||||
label=ugettext_lazy("Default mailbox quota"),
|
||||
required=False,
|
||||
help_text=ugettext_lazy(
|
||||
"Default quota in MB applied to mailboxes. Leave empty to use the "
|
||||
"default value."
|
||||
)
|
||||
)
|
||||
aliases = DomainNameField(
|
||||
label=ugettext_lazy("Alias(es)"),
|
||||
required=False,
|
||||
@@ -53,15 +45,18 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = (
|
||||
"name", "type", "default_mailbox_quota", "aliases", "enabled",
|
||||
"enable_dns_checks")
|
||||
"name", "type", "quota", "default_mailbox_quota", "aliases",
|
||||
"enabled", "enable_dns_checks")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.oldname = None
|
||||
if "instance" in kwargs:
|
||||
self.oldname = kwargs["instance"].name
|
||||
super(DomainFormGeneral, self).__init__(*args, **kwargs)
|
||||
|
||||
params = dict(param_tools.get_global_parameters("admin"))
|
||||
self.fields["quota"].initial = params["default_domain_quota"]
|
||||
self.fields["default_mailbox_quota"].initial = (
|
||||
params["default_mailbox_quota"])
|
||||
extra_domain_types = reduce(
|
||||
lambda a, b: a + b,
|
||||
[result[1] for result in signals.extra_domain_types.send(
|
||||
@@ -69,6 +64,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
)
|
||||
self.fields["type"].choices = DOMAIN_TYPES + extra_domain_types
|
||||
self.field_widths = {
|
||||
"quota": 3,
|
||||
"default_mailbox_quota": 3
|
||||
}
|
||||
|
||||
@@ -80,14 +76,6 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
name = "aliases_%d" % (pos + 1)
|
||||
self._create_field(forms.CharField, name, dalias.name, 3)
|
||||
|
||||
def clean_default_mailbox_quota(self):
|
||||
"""Check quota value."""
|
||||
if self.cleaned_data["default_mailbox_quota"] is None:
|
||||
return param_tools.get_global_parameter("default_domain_quota")
|
||||
if self.cleaned_data["default_mailbox_quota"] < 0:
|
||||
raise forms.ValidationError(_("Must be a positive integer"))
|
||||
return self.cleaned_data["default_mailbox_quota"]
|
||||
|
||||
def clean(self):
|
||||
"""Custom fields validation.
|
||||
|
||||
@@ -165,7 +153,6 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
|
||||
|
||||
class DomainFormOptions(forms.Form):
|
||||
|
||||
"""A form containing options for domain creation."""
|
||||
|
||||
create_dom_admin = YesNoField(
|
||||
@@ -272,7 +259,6 @@ class DomainFormOptions(forms.Form):
|
||||
|
||||
|
||||
class DomainForm(TabForms):
|
||||
|
||||
"""Domain edition form."""
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
@@ -342,7 +328,6 @@ class DomainForm(TabForms):
|
||||
|
||||
|
||||
class DomainWizard(WizardForm):
|
||||
|
||||
"""Domain creation wizard."""
|
||||
|
||||
def __init__(self, request):
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2017-02-15 12:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '0009_auto_20170215_0948'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='domain',
|
||||
name='quota',
|
||||
field=models.PositiveIntegerField(default=0, help_text='Quota in MB shared between mailboxes. A value of 0 means no quota.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='domain',
|
||||
name='default_mailbox_quota',
|
||||
field=models.PositiveIntegerField(default=0, help_text='Default quota in MB applied to mailboxes. A value of 0 means no quota.', verbose_name='Default mailbox quota'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mailbox',
|
||||
name='quota',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -42,8 +42,21 @@ class Domain(AdminObject):
|
||||
|
||||
name = models.CharField(ugettext_lazy('name'), max_length=100, unique=True,
|
||||
help_text=ugettext_lazy("The domain name"))
|
||||
# quota = models.IntegerField()
|
||||
default_mailbox_quota = models.IntegerField()
|
||||
quota = models.PositiveIntegerField(
|
||||
default=0,
|
||||
help_text=ugettext_lazy(
|
||||
"Quota in MB shared between mailboxes. A value of 0 means "
|
||||
"no quota."
|
||||
)
|
||||
)
|
||||
default_mailbox_quota = models.PositiveIntegerField(
|
||||
verbose_name=ugettext_lazy("Default mailbox quota"),
|
||||
default=0,
|
||||
help_text=ugettext_lazy(
|
||||
"Default quota in MB applied to mailboxes. A value of 0 means "
|
||||
"no quota."
|
||||
)
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
ugettext_lazy('enabled'),
|
||||
help_text=ugettext_lazy("Check to activate this domain"),
|
||||
@@ -144,6 +157,16 @@ class Domain(AdminObject):
|
||||
else:
|
||||
return "success"
|
||||
|
||||
@cached_property
|
||||
def quota_usage(self):
|
||||
"""Return current quota usage."""
|
||||
if not self.quota:
|
||||
return 0
|
||||
if not self.mailbox_set.exists():
|
||||
return 0
|
||||
return self.mailbox_set.aggregate(
|
||||
total=models.Sum("quota"))["total"]
|
||||
|
||||
def add_admin(self, account):
|
||||
"""Add a new administrator to this domain.
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class Mailbox(AdminObject):
|
||||
help_text=ugettext_lazy(
|
||||
"Mailbox address (without the @domain.tld part)")
|
||||
)
|
||||
quota = models.PositiveIntegerField()
|
||||
quota = models.PositiveIntegerField(default=0)
|
||||
use_domain_quota = models.BooleanField(default=False)
|
||||
domain = models.ForeignKey(Domain)
|
||||
user = models.OneToOneField(User)
|
||||
@@ -218,7 +218,7 @@ class Mailbox(AdminObject):
|
||||
MailboxOperation.objects.create(type="delete", argument=self.mail_home)
|
||||
|
||||
def set_quota(self, value=None, override_rules=False):
|
||||
"""Set or update quota's value for this mailbox.
|
||||
"""Set or update quota value for this mailbox.
|
||||
|
||||
A value equal to 0 means the mailbox won't have any quota. The
|
||||
following cases allow people to define such behaviour:
|
||||
@@ -228,21 +228,23 @@ class Mailbox(AdminObject):
|
||||
:param integer value: the quota's value
|
||||
:param bool override_rules: allow to override defined quota rules
|
||||
"""
|
||||
default_mailbox_quota = self.domain.default_mailbox_quota
|
||||
old_quota = self.quota
|
||||
if value is None:
|
||||
if self.use_domain_quota:
|
||||
self.quota = default_mailbox_quota
|
||||
self.quota = self.domain.default_mailbox_quota
|
||||
else:
|
||||
self.quota = 0
|
||||
elif int(value) > default_mailbox_quota and not override_rules:
|
||||
raise lib_exceptions.BadRequest(
|
||||
_("Quota is greater than the allowed domain's limit (%dM)") %
|
||||
default_mailbox_quota
|
||||
)
|
||||
else:
|
||||
self.quota = value
|
||||
if not self.quota and default_mailbox_quota and not override_rules:
|
||||
raise lib_exceptions.BadRequest(_("A quota is required"))
|
||||
if self.quota == 0:
|
||||
if self.domain.quota and not override_rules:
|
||||
raise lib_exceptions.BadRequest(_("A quota is required"))
|
||||
elif self.domain.quota:
|
||||
quota_usage = self.domain.quota_usage
|
||||
if old_quota:
|
||||
quota_usage -= old_quota
|
||||
if quota_usage + self.quota > self.domain.quota:
|
||||
raise lib_exceptions.BadRequest(_("Domain quota exceeded"))
|
||||
|
||||
def get_quota(self):
|
||||
"""Get quota limit.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% load i18n form_tags %}
|
||||
{% render_field form.name %}
|
||||
{% render_field form.type %}
|
||||
{% render_field form.quota appended_text=_("MB") %}
|
||||
{% render_field form.default_mailbox_quota appended_text=_("MB") %}
|
||||
{% render_fields_group form "aliases" %}
|
||||
{% render_field form.enabled %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% blocktrans trimmed %}
|
||||
Provide a CSV file where lines respect one of the following formats:
|
||||
<ul>
|
||||
<li><em>domain; name; quota; enabled</em></li>
|
||||
<li><em>domain; name; default mailbox quota; enabled</em></li>
|
||||
<li><em>domainalias; name; targeted domain; enabled</em></li>
|
||||
<li><em>relaydomain; name; target host; target port; service; enabled; verify recipients</em></li>
|
||||
</ul>
|
||||
|
||||
@@ -165,17 +165,34 @@ class AccountTestCase(ModoTestCase):
|
||||
self._set_quota("user@test.com", 0)
|
||||
|
||||
def test_set_nul_quota_as_domainadmin(self):
|
||||
"""Check cases where a domain admin set unlimited quota."""
|
||||
self.client.logout()
|
||||
self.assertTrue(
|
||||
self.client.login(username="admin@test.com", password="toto")
|
||||
)
|
||||
# Fails because domain has a quota
|
||||
self._set_quota("user@test.com", 0, 400)
|
||||
self.client.logout()
|
||||
self.assertTrue(
|
||||
self.client.login(username="admin@test2.com", password="toto")
|
||||
)
|
||||
# Ok because domain has no quota
|
||||
self._set_quota("user@test2.com", 0)
|
||||
|
||||
def test_domain_quota(self):
|
||||
"""Check domain quota."""
|
||||
dom = models.Domain.objects.get(name="test.com")
|
||||
dom.quota = 100
|
||||
dom.save(update_fields=["quota"])
|
||||
# 2 x 10MB
|
||||
self.assertEqual(dom.quota_usage, 20)
|
||||
self._set_quota("user@test.com", 80)
|
||||
del dom.quota_usage
|
||||
# 10 + 80 < 100 => ok
|
||||
self.assertEqual(dom.quota_usage, 90)
|
||||
# 30 + 80 > 100 => failure
|
||||
self._set_quota("admin@test.com", 30, 400)
|
||||
|
||||
def test_master_user(self):
|
||||
"""Validate the master user mode."""
|
||||
values = {
|
||||
@@ -250,11 +267,15 @@ class PermissionsTestCase(ModoTestCase):
|
||||
cls.reseller = core_factories.UserFactory(username="reseller")
|
||||
cls.reseller.role = "Resellers"
|
||||
cls.user = User.objects.get(username="user@test.com")
|
||||
cls.values = dict(
|
||||
username=cls.user.username, role="DomainAdmins",
|
||||
is_active=cls.user.is_active, email="user@test.com",
|
||||
quota_act=True
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
"""Initiate test context."""
|
||||
super(PermissionsTestCase, self).setUp()
|
||||
self.values = {
|
||||
"username": self.user.username, "role": "DomainAdmins",
|
||||
"is_active": self.user.is_active, "email": "user@test.com",
|
||||
"quota_act": True
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
self.client.logout()
|
||||
|
||||
@@ -25,12 +25,13 @@ class DomainTestCase(ModoTestCase):
|
||||
def test_create(self):
|
||||
"""Test the creation of a domain."""
|
||||
values = {
|
||||
"name": "pouet.com", "default_mailbox_quota": 100,
|
||||
"name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100,
|
||||
"create_dom_admin": False, "type": "domain", "stepid": "step3"
|
||||
}
|
||||
self.ajax_post(reverse("admin:domain_add"), values)
|
||||
dom = Domain.objects.get(name="pouet.com")
|
||||
self.assertEqual(dom.name, "pouet.com")
|
||||
self.assertEqual(dom.quota, 1000)
|
||||
self.assertEqual(dom.default_mailbox_quota, 100)
|
||||
self.assertEqual(dom.enabled, False)
|
||||
self.assertFalse(dom.admins)
|
||||
@@ -38,12 +39,13 @@ class DomainTestCase(ModoTestCase):
|
||||
def test_create_utf8(self):
|
||||
"""Test the creation of a domain with non-ASCII characters."""
|
||||
values = {
|
||||
"name": "pouét.com", "default_mailbox_quota": 100,
|
||||
"name": "pouét.com", "quota": 1000, "default_mailbox_quota": 100,
|
||||
"create_dom_admin": False, "type": "domain", "stepid": "step3"
|
||||
}
|
||||
self.ajax_post(reverse("admin:domain_add"), values)
|
||||
dom = Domain.objects.get(name=u"pouét.com")
|
||||
self.assertEqual(dom.name, u"pouét.com")
|
||||
self.assertEqual(dom.quota, 1000)
|
||||
self.assertEqual(dom.default_mailbox_quota, 100)
|
||||
self.assertEqual(dom.enabled, False)
|
||||
self.assertFalse(dom.admins)
|
||||
@@ -51,7 +53,7 @@ class DomainTestCase(ModoTestCase):
|
||||
def test_create_with_template(self):
|
||||
"""Test the creation of a domain with a template."""
|
||||
values = {
|
||||
"name": "pouet.com", "default_mailbox_quota": 100,
|
||||
"name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100,
|
||||
"create_dom_admin": True, "dom_admin_username": "toto",
|
||||
"create_aliases": True, "type": "domain", "stepid": 'step3'
|
||||
}
|
||||
@@ -67,7 +69,7 @@ class DomainTestCase(ModoTestCase):
|
||||
self.assertTrue(da.can_access(al))
|
||||
|
||||
values = {
|
||||
"name": "pouet2.com", "default_mailbox_quota": 100,
|
||||
"name": "pouet2.com", "quota": 1000, "default_mailbox_quota": 100,
|
||||
"create_dom_admin": True, "dom_admin_username": "postmaster",
|
||||
"create_aliases": True, "type": "domain", "stepid": "step3"
|
||||
}
|
||||
@@ -81,7 +83,7 @@ class DomainTestCase(ModoTestCase):
|
||||
def test_create_with_template_and_empty_quota(self):
|
||||
"""Test the creation of a domain with a template and no quota"""
|
||||
values = {
|
||||
"name": "pouet.com", "default_mailbox_quota": 0,
|
||||
"name": "pouet.com", "quota": 0, "default_mailbox_quota": 0,
|
||||
"create_dom_admin": True, "dom_admin_username": "toto",
|
||||
"create_aliases": True, "type": "domain", "stepid": 'step3'
|
||||
}
|
||||
@@ -104,7 +106,7 @@ class DomainTestCase(ModoTestCase):
|
||||
password = "Toto1000"
|
||||
self.set_global_parameter("default_password", password, app="core")
|
||||
values = {
|
||||
"name": "pouet.com", "default_mailbox_quota": 0,
|
||||
"name": "pouet.com", "quota": 0, "default_mailbox_quota": 0,
|
||||
"create_dom_admin": True, "dom_admin_username": "toto",
|
||||
"create_aliases": True, "type": "domain", "stepid": 'step3'
|
||||
}
|
||||
@@ -125,20 +127,12 @@ class DomainTestCase(ModoTestCase):
|
||||
self.client.login(username="toto@pouet.com", password=password))
|
||||
|
||||
def test_create_using_default_quota(self):
|
||||
self.set_global_parameter("default_domain_quota", 50)
|
||||
values = {
|
||||
"name": "pouet.com", "create_dom_admin": True,
|
||||
"dom_admin_username": "toto", "create_aliases": True,
|
||||
"type": "domain", "stepid": 'step3'
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("admin:domain_add"),
|
||||
values
|
||||
)
|
||||
dom = Domain.objects.get(name="pouet.com")
|
||||
self.assertEqual(dom.default_mailbox_quota, 50)
|
||||
da = User.objects.get(username="toto@pouet.com")
|
||||
self.assertEqual(da.mailbox.quota, 50)
|
||||
"""Check that default value is used for creation."""
|
||||
self.set_global_parameter("default_domain_quota", 500)
|
||||
self.set_global_parameter("default_mailbox_quota", 50)
|
||||
response = self.client.get(reverse("admin:domain_add"))
|
||||
self.assertContains(response, "value=\"500\"")
|
||||
self.assertContains(response, "value=\"50\"")
|
||||
|
||||
def test_modify(self):
|
||||
"""Test the modification of a domain
|
||||
@@ -146,7 +140,7 @@ class DomainTestCase(ModoTestCase):
|
||||
Rename 'test.com' domain to 'pouet.com'
|
||||
"""
|
||||
values = {
|
||||
"name": "pouet.com", "default_mailbox_quota": 100,
|
||||
"name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100,
|
||||
"type": "domain", "enabled": True
|
||||
}
|
||||
dom = Domain.objects.get(name="test.com")
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2017-02-15 09:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def rename_settings(apps, schema_editor):
|
||||
"""Rename settings."""
|
||||
LocalConfig = apps.get_model("core", "LocalConfig")
|
||||
lc = LocalConfig.objects.first()
|
||||
condition = (
|
||||
lc and
|
||||
"admin" in lc._parameters and
|
||||
"default_domain_quota" in lc._parameters["admin"]
|
||||
)
|
||||
if condition:
|
||||
lc._parameters["admin"]["default_mailbox_quota"] = (
|
||||
lc._parameters["admin"]["default_domain_quota"])
|
||||
del lc._parameters["admin"]["default_domain_quota"]
|
||||
lc.save(update_fields=["_parameters"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0011_auto_20161104_2217'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(rename_settings)
|
||||
]
|
||||
@@ -21,7 +21,6 @@ from modoboa.lib.web_utils import render_to_json_response
|
||||
|
||||
|
||||
class WizardStep(object):
|
||||
|
||||
"""A wizard step."""
|
||||
|
||||
def __init__(self, uid, formclass, title, formtpl=None, new_args=None):
|
||||
@@ -66,7 +65,6 @@ class WizardStep(object):
|
||||
|
||||
|
||||
class WizardForm(object):
|
||||
|
||||
"""Custom wizard."""
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
Reference in New Issue
Block a user