diff --git a/admin/mailu/admin/__init__.py b/admin/mailu/admin/__init__.py
index fc909cf0..77df2052 100644
--- a/admin/mailu/admin/__init__.py
+++ b/admin/mailu/admin/__init__.py
@@ -28,4 +28,5 @@ from mailu.admin.views import \
aliases, \
users, \
domains, \
+ alternatives, \
fetches
diff --git a/admin/mailu/admin/forms.py b/admin/mailu/admin/forms.py
index ec3c7164..41b924bf 100644
--- a/admin/mailu/admin/forms.py
+++ b/admin/mailu/admin/forms.py
@@ -51,6 +51,11 @@ class DomainForm(flask_wtf.FlaskForm):
submit = fields.SubmitField(_('Create'))
+class AlternativeForm(flask_wtf.FlaskForm):
+ name = fields.StringField(_('Alternative name'), [validators.DataRequired()])
+ submit = fields.SubmitField(_('Create'))
+
+
class UserForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('E-mail'), [validators.DataRequired()])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
diff --git a/admin/mailu/admin/models.py b/admin/mailu/admin/models.py
index a1828a84..80a81ce9 100644
--- a/admin/mailu/admin/models.py
+++ b/admin/mailu/admin/models.py
@@ -100,6 +100,22 @@ class Domain(Base):
return False
+class Alternative(Base):
+ """ Alternative name for a served domain.
+ The name "domain alias" was avoided to prevent some confusion.
+ """
+
+ __tablename__ = "alternative"
+
+ name = db.Column(db.String(80), primary_key=True, nullable=False)
+ domain_name = db.Column(db.String(80), db.ForeignKey(Domain.name))
+ domain = db.relationship(Domain,
+ backref=db.backref('alternatives', cascade='all, delete-orphan'))
+
+ def __str__(self):
+ return self.name
+
+
class Email(object):
""" Abstraction for an email address (localpart and domain).
"""
diff --git a/admin/mailu/admin/templates/alternative/create.html b/admin/mailu/admin/templates/alternative/create.html
new file mode 100644
index 00000000..75461c67
--- /dev/null
+++ b/admin/mailu/admin/templates/alternative/create.html
@@ -0,0 +1,9 @@
+{% extends "form.html" %}
+
+{% block title %}
+{% trans %}Create alternative domain{% endtrans %}
+{% endblock %}
+
+{% block subtitle %}
+{{ domain }}
+{% endblock %}
diff --git a/admin/mailu/admin/templates/alternative/list.html b/admin/mailu/admin/templates/alternative/list.html
new file mode 100644
index 00000000..547d2ecb
--- /dev/null
+++ b/admin/mailu/admin/templates/alternative/list.html
@@ -0,0 +1,34 @@
+{% extends "base.html" %}
+
+{% block title %}
+{% trans %}Alternative domain list{% endtrans %}
+{% endblock %}
+
+{% block subtitle %}
+{{ domain.name }}
+{% endblock %}
+
+{% block main_action %}
+{% trans %}Add alternative{% endtrans %}
+{% endblock %}
+
+{% block box %}
+
+
+
+ {% trans %}Actions{% endtrans %} |
+ {% trans %}Name{% endtrans %} |
+ {% trans %}Created{% endtrans %} |
+
+ {% for alternative in domain.alternatives %}
+
+
+
+ |
+ {{ alternative }} |
+ {{ alternative.created_at }} |
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/admin/mailu/admin/templates/domain/list.html b/admin/mailu/admin/templates/domain/list.html
index ea81e4c3..46a5d723 100644
--- a/admin/mailu/admin/templates/domain/list.html
+++ b/admin/mailu/admin/templates/domain/list.html
@@ -36,6 +36,9 @@
+ {% if current_user.global_admin %}
+
+ {% endif %}
{{ domain.name }} |
{{ domain.users | count }} / {{ domain.max_users or '∞' }} |
diff --git a/admin/mailu/admin/views/alternatives.py b/admin/mailu/admin/views/alternatives.py
new file mode 100644
index 00000000..b3b4ab3b
--- /dev/null
+++ b/admin/mailu/admin/views/alternatives.py
@@ -0,0 +1,46 @@
+from mailu.admin import app, db, models, forms, access
+
+import flask
+import wtforms_components
+
+
+@app.route('/alternative/list/', methods=['GET'])
+@access.global_admin
+def alternative_list(domain_name):
+ domain = models.Domain.query.get(domain_name) or flask.abort(404)
+ return flask.render_template('alternative/list.html', domain=domain)
+
+
+@app.route('/alternative/create/', methods=['GET', 'POST'])
+@access.global_admin
+def alternative_create(domain_name):
+ domain = models.Domain.query.get(domain_name) or flask.abort(404)
+ form = forms.AlternativeForm()
+ if form.validate_on_submit():
+ conflicting_domain = models.Domain.query.get(form.name.data)
+ conflicting_alternative = models.Alternative.query.get(form.name.data)
+ if conflicting_domain or conflicting_alternative:
+ flask.flash('Domain %s is already used' % form.name.data, 'error')
+ else:
+ alternative = models.Alternative(domain=domain)
+ form.populate_obj(alternative)
+ db.session.add(alternative)
+ db.session.commit()
+ flask.flash('Alternative domain %s created' % alternative)
+ return flask.redirect(
+ flask.url_for('.alternative_list', domain_name=domain.name))
+ return flask.render_template('alternative/create.html',
+ domain=domain, form=form)
+
+
+@app.route('/alternative/delete/', methods=['GET', 'POST'])
+@access.global_admin
+@access.confirmation_required("delete {alternative}")
+def alternative_delete(alternative):
+ alternative = models.Alternative.query.get(alternative) or flask.abort(404)
+ domain = alternative.domain
+ db.session.delete(alternative)
+ db.session.commit()
+ flask.flash('Alternative %s deleted' % alternative)
+ return flask.redirect(
+ flask.url_for('.alternative_list', domain_name=domain.name))
diff --git a/admin/mailu/admin/views/domains.py b/admin/mailu/admin/views/domains.py
index 9bd26641..371b46f9 100644
--- a/admin/mailu/admin/views/domains.py
+++ b/admin/mailu/admin/views/domains.py
@@ -16,7 +16,9 @@ def domain_list():
def domain_create():
form = forms.DomainForm()
if form.validate_on_submit():
- if models.Domain.query.get(form.name.data):
+ conflicting_domain = models.Domain.query.get(form.name.data)
+ conflicting_alternative = models.Alternative.query.get(form.name.data)
+ if conflicting_domain or conflicting_alternative:
flask.flash('Domain %s is already used' % form.name.data, 'error')
else:
domain = models.Domain()
diff --git a/admin/migrations/versions/c9a0b4e653cf_.py b/admin/migrations/versions/c9a0b4e653cf_.py
new file mode 100644
index 00000000..8882d079
--- /dev/null
+++ b/admin/migrations/versions/c9a0b4e653cf_.py
@@ -0,0 +1,30 @@
+""" Add alternative domains
+
+Revision ID: c9a0b4e653cf
+Revises: 73e56bad5ec5
+Create Date: 2017-09-03 18:23:36.356527
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'c9a0b4e653cf'
+down_revision = '73e56bad5ec5'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.create_table('alternative',
+ sa.Column('created_at', sa.Date(), nullable=False),
+ sa.Column('updated_at', sa.Date(), nullable=True),
+ sa.Column('comment', sa.String(length=255), nullable=True),
+ sa.Column('name', sa.String(length=80), nullable=False),
+ sa.Column('domain_name', sa.String(length=80), nullable=True),
+ sa.ForeignKeyConstraint(['domain_name'], ['domain.name'], ),
+ sa.PrimaryKeyConstraint('name')
+ )
+
+
+def downgrade():
+ op.drop_table('alternative')