Work in progress for #140 and #143

Aliases to external addresses are now supported. Alias creation/modification forms have been rewritten. (autocompletion of addresses, dynamic number of input fields)

see #143

First version of the postfixadmin to modoboa migration script. It has not been tested deeply but I think it should work pretty well.

see #140

Some javascript errors (SqueezeBox related) have been fixed.
This commit is contained in:
Antoine Nguyen
2011-06-06 11:55:13 +02:00
parent 1d068ef404
commit a2ec92daa7
31 changed files with 1663 additions and 165 deletions
+39 -20
View File
@@ -4,7 +4,7 @@ from modoboa.admin.models import *
from django.utils.translation import ugettext as _
from django.contrib.auth.models import User
from modoboa.admin.templatetags.admin_extras import gender
from modoboa.lib import tables
from modoboa.lib import tables, split_mailbox
class DomainForm(forms.ModelForm):
class Meta:
@@ -87,51 +87,70 @@ class MailboxForm(ProxyForm):
return m
class AliasForm(ProxyForm):
domain = forms.ModelChoiceField(queryset=Domain.objects.all(), label=_("Domain"),
required=True,
empty_label=_("Select a domain"),
help_text=_("Select a domain in the list"))
targets = forms.CharField(label=_("Target(s)"), required=False,
help_text=_("Mailbox(es) this alias will point to"))
def __init__(self, *args, **kwargs):
super(AliasForm, self).__init__(*args, **kwargs)
self.fields['address'].widget.attrs['size'] = 14
self.fields['mboxes'].widget.attrs['size'] = 5
# This part is a little bit tricky. As 'domain' is not a
# member of Alias, we need to fill it manually. if
# self._domain exists, it means a DomainAdmin is going to
# create a new alias, we know how to fill the fields. Else,
# leave them empty (super admin mode).
# create a new alias, we know how to fill the fields.
if self._domain is not None:
self.fields["domain"].queryset = Domain.objects.filter(pk=self._domain.id)
elif len(args) < 2 or args[1]["domain"] == '':
self.fields['mboxes'].queryset = Mailbox.objects.none()
# if 'instance' exists, it means we are modifying an existing
# record, we need to select the right value for
# 'domain'. Finally, if super user is doing the job, we need
# to fill 'mboxes' because it's currently empty (see upper).
if kwargs.has_key("instance") or self._domain is not None:
domain = self._domain is None and kwargs["instance"].domain or self._domain
# record, we need to select the right value for 'domain' and
# to fill the first "targets" input (because it is created by the
# Form object, not by us).
domain = None
if self._domain:
domain = self._domain
if kwargs.has_key("instance"):
if self._domain is None:
domain = kwargs["instance"].domain
self.fields["targets"].initial = kwargs["instance"].first_target()
if domain is not None:
self.fields["domain"].initial = domain
self.fields['mboxes'].queryset = Mailbox.objects.filter(domain=domain)
self.fields.keyOrder = ['domain', 'address', 'mboxes', 'enabled']
self.fields.keyOrder = ['address', 'domain', 'enabled', 'targets']
class Meta:
model = Alias
fields = ('address', 'mboxes', 'enabled')
fields = ('address', 'domain', 'enabled')
def clean_address(self):
if self.cleaned_data["address"].find('@') != -1:
return self.cleaned_data["address"].rsplit("@", 1)[0]
return self.cleaned_data["address"]
def set_targets(self, user, values):
self.ext_targets = []
self.int_targets = []
for addr in values:
if addr == "":
continue
local_part, domain = split_mailbox(addr)
if domain is None:
raise AdminError("%s %s" % (_("Invalid mailbox"), addr))
try:
mb = Mailbox.objects.get(address=local_part, domain__name=domain)
except Mailbox.DoesNotExist:
self.ext_targets += [addr]
else:
if not user.is_superuser:
usermb = Mailbox.objects.get(user=user.id)
if usermb.domain.id != mb.domain.id:
raise AdminError("%s %s" % (_("Permission denied on"), addr))
self.int_targets += [mb]
def save(self, force_insert=False, force_update=False, commit=True):
a = super(AliasForm, self).save(commit=False)
if commit:
domain = self.cleaned_data["domain"].name
a.save()
a.save(self.int_targets, self.ext_targets)
self.save_m2m()
return a
class SuperAdminForm(forms.Form):
@@ -0,0 +1,103 @@
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Alias.extmboxes'
db.add_column('admin_alias', 'extmboxes', self.gf('django.db.models.fields.TextField')(default='', blank=True), keep_default=False)
db.add_column('admin_alias', 'domain', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['admin.Domain'], default=-1))
def backwards(self, orm):
# Deleting field 'Alias.extmboxes'
db.delete_column('admin_alias', 'extmboxes')
db.delete_column('admin_alias', 'domain')
models = {
'admin.alias': {
'Meta': {'object_name': 'Alias'},
'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'})
},
'admin.domain': {
'Meta': {'object_name': 'Domain'},
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'quota': ('django.db.models.fields.IntegerField', [], {})
},
'admin.domainalias': {
'Meta': {'object_name': 'DomainAlias'},
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"})
},
'admin.extension': {
'Meta': {'object_name': 'Extension'},
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '150'})
},
'admin.mailbox': {
'Meta': {'object_name': 'Mailbox'},
'address': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}),
'gid': ('django.db.models.fields.IntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['admin']
+101
View File
@@ -0,0 +1,101 @@
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
class Migration(DataMigration):
def forwards(self, orm):
"Write your forwards methods here."
for alias in orm.Alias.objects.all():
print alias.address
alias.domain = alias.mboxes.all()[0].domain
alias.save()
def backwards(self, orm):
"Write your backwards methods here."
models = {
'admin.alias': {
'Meta': {'object_name': 'Alias'},
'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'})
},
'admin.domain': {
'Meta': {'object_name': 'Domain'},
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'quota': ('django.db.models.fields.IntegerField', [], {})
},
'admin.domainalias': {
'Meta': {'object_name': 'DomainAlias'},
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"})
},
'admin.extension': {
'Meta': {'object_name': 'Extension'},
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '150'})
},
'admin.mailbox': {
'Meta': {'object_name': 'Mailbox'},
'address': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}),
'gid': ('django.db.models.fields.IntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['admin']
+46 -4
View File
@@ -215,9 +215,11 @@ class Mailbox(models.Model):
class Alias(models.Model):
address = models.CharField(_('address'), max_length=254,
help_text=_("The alias address (without the domain part)"))
help_text=_("The alias address (without the domain part). For a 'catch-all' address, just enter an * character."))
domain = models.ForeignKey(Domain)
mboxes = models.ManyToManyField(Mailbox, verbose_name=_('mailboxes'),
help_text=_("The mailboxes this alias points to"))
extmboxes = models.TextField(blank=True)
enabled = models.BooleanField(_('enabled'),
help_text=_("Check to activate this alias"))
@@ -226,10 +228,50 @@ class Alias(models.Model):
("view_aliases", "View aliases"),
)
def __domain(self):
return self.mboxes.all()[0].domain
def save(self, int_targets, ext_targets, *args, **kwargs):
if len(ext_targets):
self.extmboxes = ",".join(ext_targets)
super(Alias, self).save(*args, **kwargs)
curmboxes = self.mboxes.all()
for t in int_targets:
if not t in curmboxes:
self.mboxes.add(t)
for t in curmboxes:
if not t in int_targets:
self.mboxes.remove(t)
domain = property(__domain)
def first_target(self):
"""Return the first target of this alias
It is taken from internal addresses, or external addresses.
"""
if len(self.mboxes.all()):
return self.mboxes.all()[0]
if len(self.extmboxes):
return self.extmboxes.split(",")[0]
return None
def get_targets(self):
"""Return a list containing targeted mailboxes
Internal and external addresses are mixed into a single list.
"""
result = []
if len(self.mboxes.all()):
for mb in self.mboxes.all()[1:]:
result += [mb.full_address]
if len(self.extmboxes):
result += self.extmboxes.split(",")
else:
result = self.extmboxes.split(",")[1:]
return result
def repr_extmboxes(self):
"""Return external targets as a string
A ready to print representation of external targets.
"""
return "<br/>".join(self.extmboxes.split(","))
class Extension(models.Model):
name = models.CharField(max_length=150)
+1 -1
View File
@@ -206,7 +206,7 @@ def mbalias_actions(user, alias):
"img" : static_url("pics/edit.png"),
"title" : _("Edit mailbox alias"),
"class" : "boxed",
"rel" : "{handler:'iframe',size:{x:300,y:300}}"}
"rel" : "{handler:'iframe',size:{x:320,y:300}}"}
]
return render_actions(actions)
+1
View File
@@ -16,6 +16,7 @@ urlpatterns = patterns('modoboa.admin.views',
(r'^mailboxes/new/$', 'newmailbox'),
(r'^mailboxes/edit/(?P<mbox_id>\d+)/$', 'editmailbox'),
(r'^mailboxes/delete/$', 'delmailbox'),
(r'^mailboxes/search/$', 'mailboxes_search'),
(r'^mbaliases/$', 'mbaliases'),
(r'^mbaliases/new/$', 'newmbalias'),
+63 -33
View File
@@ -6,6 +6,7 @@ from django.utils.http import urlquote
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators \
import login_required, permission_required, user_passes_test
from django.db.models import Q
from django.contrib.auth.models import User, Group
from django.contrib import messages
from django.db import transaction, IntegrityError
@@ -14,7 +15,8 @@ from modoboa import admin, userprefs
from models import *
from admin.permissions import *
from modoboa.lib import crypt_password
from modoboa.lib import _render, ajax_response, getctx, events, parameters
from modoboa.lib import _render, ajax_response, ajax_simple_response, \
getctx, events, parameters, split_mailbox
from modoboa.lib.models import Parameter
import copy
@@ -60,7 +62,7 @@ def domains(request):
domains = Domain.objects.all()
for dom in domains:
dom.mbalias_counter = len(Alias.objects.filter(mboxes__domain__pk=dom.id))
dom.mbalias_counter = len(Alias.objects.filter(domain=dom.id))
deloptions = {"keepdir" : _("Do not delete domain directory")}
return render_domains_page(request, "domains",
domains=domains,
@@ -266,6 +268,22 @@ def mailboxes_raw(request, dom_id=None):
"target" : target
})
@login_required
@good_domain
@permission_required("admin.view_mailboxes")
def mailboxes_search(request):
if request.method != "POST":
return
local_part, domain = split_mailbox(request.POST["search"])
query = Q(address__startswith=local_part)
if domain is not None and domain != "":
query &= Q(domain__name__startswith=domain)
if request.GET.has_key("domid"):
query = Q(domain=request.GET["domid"]) & query
mboxes = Mailbox.objects.filter(query)
result = map(lambda mb: mb.full_address, mboxes)
return ajax_simple_response(result)
@login_required
@good_domain
@permission_required("admin.add_mailbox")
@@ -361,13 +379,50 @@ def delmailbox(request):
@permission_required("admin.view_aliases")
def mbaliases(request):
if request.GET.has_key("domid"):
aliases = Alias.objects.filter(mboxes__domain__pk=request.GET["domid"]).distinct()
aliases = Alias.objects.filter(domain=request.GET["domid"])
elif request.GET.has_key("mbid"):
aliases = Alias.objects.filter(mboxes__id=request.GET["mbid"]).distinct()
else:
aliases = Alias.objects.all()
if not request.user.is_superuser:
usermb = Mailbox.objects.get(user=request.user.id)
for al in aliases:
al.ui_disabled = False
for mb in al.mboxes.all():
if mb.domain.id != usermb.domain.id:
al.ui_disabled = True
break
return render_domains_page(request, "mbaliases", aliases=aliases)
def _validate_mbalias(request, form, successmsg, tplname, commonctx):
"""Mailbox alias validation
Common function shared between creation and modification actions.
"""
error = None
if form.is_valid():
try:
if not request.POST.has_key("targets"):
raise AdminError(_("No target defined"))
form.set_targets(request.user, request.POST.getlist("targets"))
try:
alias = form.save()
except IntegrityError:
raise AdminError(_("Alias with this name already exists"))
messages.info(request, successmsg, fail_silently=True)
return ajax_response(request, url=reverse(admin.views.mbaliases))
except AdminError, e:
error = str(e)
if request.POST.has_key("targets"):
targets = request.POST.getlist("targets")
commonctx["targets"] = targets[:-1]
commonctx["form"] = form
commonctx["error"] = error
return ajax_response(request, status="ko", template=tplname, **commonctx)
@login_required
@good_domain
@permission_required("admin.add_alias")
@@ -378,24 +433,10 @@ def newmbalias(request, tplname="admin/mbaliasform.html"):
"formid" : "mbaliasform"}
if request.method == "POST":
form = AliasForm(request.user, request.POST)
error = None
if form.is_valid():
try:
alias = form.save()
except IntegrityError:
error = _("Alias with this name already exists")
else:
form.save_m2m()
messages.info(request, _("Mailbox alias created"), fail_silently=True)
return ajax_response(request, url=reverse(admin.views.mbaliases))
commonctx["form"] = form
commonctx["error"] = error
return ajax_response(request, status="ko", template=tplname, **commonctx)
return _validate_mbalias(request, form, _("Mailbox alias created"),
tplname, commonctx)
form = AliasForm(request.user)
if request.GET.has_key("mbid"):
form.fields["mboxes"].initial = request.GET["mbid"]
commonctx["form"] = form
commonctx["noerrors"] = True
return _render(request, tplname, commonctx)
@@ -411,23 +452,12 @@ def editmbalias(request, alias_id, tplname="admin/mbaliasform.html"):
"formid" : "mbaliasform"}
if request.method == "POST":
form = AliasForm(request.user, request.POST, instance=alias)
error = None
if form.is_valid():
try:
alias = form.save()
except IntegrityError:
error = _("Alias with this name already exists")
else:
form.save_m2m()
messages.info(request, _("Mailbox alias modified"),
fail_silently=True)
return ajax_response(request, url=reverse(admin.views.mbaliases))
commonctx["form"] = form
commonctx["error"] = error
return ajax_response(request, status="ko", template=tplname, **commonctx)
return _validate_mbalias(request, form, _("Mailbox alias modified"),
tplname, commonctx)
form = AliasForm(request.user, instance=alias)
commonctx["form"] = form
commonctx["targets"] = alias.get_targets()
return _render(request, tplname, commonctx)
@login_required