mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-11-13 09:04:04 +01:00
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:
@@ -250,7 +250,7 @@ For *MySQL* users, define the following map files on your server (it should work
|
||||
password = <password>
|
||||
dbname = <database>
|
||||
hosts = 127.0.0.1
|
||||
query = SELECT name FROM admin_domain WHERE name='%s' AND enabled='1'
|
||||
query = SELECT name FROM admin_domain WHERE name='%s' AND enabled=1
|
||||
|
||||
``/etc/postfix/sql-domain-aliases.cf``::
|
||||
|
||||
@@ -266,7 +266,7 @@ For *MySQL* users, define the following map files on your server (it should work
|
||||
password = <password>
|
||||
dbname = <database>
|
||||
hosts = 127.0.0.1
|
||||
query = SELECT concat(t2.name, '/', t1.path) FROM admin_mailbox AS t1, admin_domain AS t2 WHERE t1.domain_id=t2.id AND t1.address='%u' AND t2.name='%d'
|
||||
query = SELECT concat(t2.name, '/', t1.path) FROM admin_mailbox AS t1, admin_domain AS t2 WHERE t1.enabled=1 AND t1.domain_id=t2.id AND t1.address='%u' AND t2.enabled=1 AND t2.name='%d'
|
||||
|
||||
``/etc/postfix/sql-aliases.cf``::
|
||||
|
||||
@@ -274,7 +274,15 @@ For *MySQL* users, define the following map files on your server (it should work
|
||||
password = <password>
|
||||
dbname = <database>
|
||||
hosts = 127.0.0.1
|
||||
query = SELECT concat(t3.address, '@', t4.name) FROM admin_alias AS t1, admin_alias_mboxes AS t2, admin_mailbox AS t3, admin_domain AS t4 WHERE t1.enabled='1' AND t1.address='%u' AND t1.id=t2.alias_id AND t2.mailbox_id=t3.id AND t3.domain_id=t4.id AND t4.name='%d'
|
||||
query = SELECT concat(t3.address, '@', t4.name) FROM admin_alias AS t1, admin_alias_mboxes AS t2, admin_mailbox AS t3, admin_domain AS t4 WHERE t1.enabled=1 AND t1.address='%u' AND t1.id=t2.alias_id AND t2.mailbox_id=t3.id AND t3.enabled=1 AND t3.domain_id=t4.id AND t4.enabled=1 AND t4.name='%d'
|
||||
|
||||
``/etc/postfix/sql-forwards.cf``::
|
||||
|
||||
user = <user>
|
||||
password = <password>
|
||||
dbname = <database>
|
||||
hosts = 127.0.0.1
|
||||
query = SELECT t1.extmboxes FROM admin_alias AS t1, admin_domain AS t2 WHERE t1.enabled=1 AND t1.address='%u' AND t1.domain_id=t2.id AND t2.name="%d" AND t2.enabled=1
|
||||
|
||||
``/etc/postfix/sql-domain-aliases-mailboxes.cf``::
|
||||
|
||||
@@ -301,6 +309,7 @@ Then, use the following configuration in the */etc/postfix/main.cf* file
|
||||
virtual_alias_domains = mysql:/etc/postfix/sql-domain-aliases.cf
|
||||
virtual_mailbox_maps = mysql:/etc/postfix/sql-mailboxes.cf
|
||||
virtual_alias_maps = mysql:/etc/postfix/sql-aliases.cf,
|
||||
mysql:/etc/postfix/sql-forwards.cf,
|
||||
mysql:/etc/postfix/sql-domain-aliases-mailboxes.cf
|
||||
# Stuff after
|
||||
|
||||
@@ -308,6 +317,51 @@ NB: Modoboa supports both maildir and mbox formats. You can specify
|
||||
which format to use by modifying the MAILBOX_TYPE parameter available
|
||||
in the admin panel.
|
||||
|
||||
Optional : 'catchall' aliases
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Modoboa supports 'catchall' aliases creation. In the case you would
|
||||
like to use this feature, you'll need to add specific queries to
|
||||
*postfix* configuration.
|
||||
|
||||
First, create the following new maps:
|
||||
|
||||
``/etc/postfix/sql-email2email.cf``::
|
||||
|
||||
user = <user>
|
||||
password = <password>
|
||||
dbname = <database>
|
||||
hosts = 127.0.0.1
|
||||
query = SELECT concat(t1.address, '@', t2.name) FROM admin_mailbox AS t1, admin_domain AS t2 WHERE t1.domain_id=t2.id AND t1.address='%u' AND t2.name='%d'
|
||||
|
||||
``/etc/postfix/sql-catchall-aliases.cf``::
|
||||
|
||||
user = <user>
|
||||
password = <password>
|
||||
dbname = <database>
|
||||
hosts = 127.0.0.1
|
||||
query = SELECT concat(t3.address, '@', t4.name) FROM admin_alias AS t1, admin_alias_mboxes AS t2, admin_mailbox AS t3, admin_domain AS t4 WHERE t1.enabled='1' AND t1.address='*' AND t1.id=t2.alias_id AND t2.mailbox_id=t3.id AND t3.enabled='1' AND t3.domain_id=t4.id AND t4.enabled='1' AND t4.name='%d'
|
||||
|
||||
``/etc/postfix/sql-catchall-forwards.cf``::
|
||||
|
||||
user = <user>
|
||||
password = <password>
|
||||
dbname = <database>
|
||||
hosts = 127.0.0.1
|
||||
query = SELECT t1.extmboxes FROM admin_alias AS t1, admin_domain AS t2 WHERE t1.enabled='1' AND t1.address='*' AND t1.domain_id=t2.id AND t2.name="%d" AND t2.enabled='1'
|
||||
|
||||
Once done, edit the *main.cf* configuration file and add the new maps
|
||||
to the ``virtual_alias_maps`` parameter like this::
|
||||
|
||||
virtual_alias_maps = mysql:/etc/postfix/sql-aliases.cf,
|
||||
mysql:/etc/postfix/sql-forwards.cf,
|
||||
mysql:/etc/postfix/sql-domain-aliases-mailboxes.cf,
|
||||
mysql:/etc/postfix/sql-email2email.cf, # new
|
||||
mysql:/etc/postfix/sql-catchall-aliases.cf, # new
|
||||
mysql:/etc/postfix/sql-catchall-forwards.cf # new
|
||||
|
||||
Reload *postfix*.
|
||||
|
||||
.. _dovecot-label:
|
||||
|
||||
Dovecot
|
||||
@@ -529,8 +583,8 @@ images). In order to display images correctly, you need to create a
|
||||
|
||||
$ mkdir <modoboa_dir>/static/tmp
|
||||
|
||||
To be able to release messages, first take a look at this page:
|
||||
http://www.ijs.si/software/amavisd/amavisd-new-docs.html#quar-release. It
|
||||
To be able to release messages, first take a look at `this page
|
||||
<http://www.ijs.si/software/amavisd/amavisd-new-docs.html#quar-release>`_. It
|
||||
explains how to configure *amavisd-new* to listen somewhere for the
|
||||
AM.PDP protocol. This protocol is used to send release requests.
|
||||
|
||||
|
||||
+39
-20
@@ -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']
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from managesieve import ManageSieveClient, Error
|
||||
from modoboa.lib import ConnectionsManager, ConnectionError, parameters
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.connections import ConnectionsManager, ConnectionError
|
||||
|
||||
class SieveClientError(Exception):
|
||||
pass
|
||||
|
||||
@@ -15,7 +15,8 @@ import lxml
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from modoboa.lib import decode, u2u_decode, tables, \
|
||||
imap_utf7, static_url, ConnectionsManager, parameters
|
||||
imap_utf7, static_url, parameters
|
||||
from modoboa.lib.connections import ConnectionsManager
|
||||
from modoboa.lib.email_listing import MBconnector, EmailListing, Email
|
||||
|
||||
class WebmailError(Exception):
|
||||
|
||||
+18
-32
@@ -12,7 +12,6 @@ from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils import simplejson
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.auth.lib import decrypt
|
||||
|
||||
def exec_cmd(cmd, **kwargs):
|
||||
import subprocess
|
||||
@@ -33,7 +32,7 @@ def _render(request, tpl, user_context):
|
||||
def _render_to_string(request, tpl, user_context):
|
||||
"""Custom rendering function
|
||||
|
||||
Same a mrender_render.
|
||||
Same as _render.
|
||||
"""
|
||||
return render_to_string(tpl, user_context,
|
||||
context_instance=RequestContext(request))
|
||||
@@ -109,6 +108,15 @@ def ajax_response(request, status="ok", respmsg=None,
|
||||
jsonctx["norefresh"] = norefresh
|
||||
return HttpResponse(simplejson.dumps(jsonctx), mimetype="application/json")
|
||||
|
||||
def ajax_simple_response(content):
|
||||
"""Simple AJAX response
|
||||
|
||||
No extra formatting is done. The content is passed directly to simplejon.
|
||||
|
||||
:param content: the response's content (list, dict, string)
|
||||
"""
|
||||
return HttpResponse(simplejson.dumps(content), mimetype="application/json")
|
||||
|
||||
def decode(s, encodings=('utf8', 'latin1', 'windows-1252', 'ascii'), charset=None):
|
||||
if charset is not None:
|
||||
try:
|
||||
@@ -177,6 +185,14 @@ def crypt_password(password):
|
||||
return password
|
||||
|
||||
def split_mailbox(mailbox):
|
||||
"""Try to split a mailbox in two parts
|
||||
|
||||
:return: a 2-uple (local part, domain)
|
||||
"""
|
||||
try:
|
||||
mailbox.index("@")
|
||||
except ValueError:
|
||||
return mailbox, None
|
||||
parts = mailbox.split('@')
|
||||
if len(parts) == 2:
|
||||
address = parts[0]
|
||||
@@ -185,33 +201,3 @@ def split_mailbox(mailbox):
|
||||
domain = parts[-1]
|
||||
address = "@".join(parts[:-1])
|
||||
return (address, domain)
|
||||
|
||||
class ConnectionsManager(type):
|
||||
"""Singleton pattern implementation
|
||||
|
||||
This class is specialized in connection management.
|
||||
"""
|
||||
def __init__(cls, name, bases, dict):
|
||||
super(ConnectionsManager, cls).__init__(name, bases, dict)
|
||||
cls.instances = {}
|
||||
|
||||
def __call__(cls, **kwargs):
|
||||
key = None
|
||||
if kwargs.has_key("user"):
|
||||
key = kwargs["user"]
|
||||
else:
|
||||
return None
|
||||
if not cls.instances.has_key(key):
|
||||
cls.instances[key] = None
|
||||
if kwargs.has_key("password"):
|
||||
kwargs["password"] = decrypt(kwargs["password"])
|
||||
|
||||
if cls.instances[key] is None:
|
||||
cls.instances[key] = \
|
||||
super(ConnectionsManager, cls).__call__(**kwargs)
|
||||
else:
|
||||
cls.instances[key].refresh(key, kwargs["password"])
|
||||
return cls.instances[key]
|
||||
|
||||
class ConnectionError(Exception):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from modoboa.auth.lib import decrypt
|
||||
|
||||
class ConnectionsManager(type):
|
||||
"""Singleton pattern implementation
|
||||
|
||||
This class is specialized in connection management.
|
||||
"""
|
||||
def __init__(cls, name, bases, dict):
|
||||
super(ConnectionsManager, cls).__init__(name, bases, dict)
|
||||
cls.instances = {}
|
||||
|
||||
def __call__(cls, **kwargs):
|
||||
key = None
|
||||
if kwargs.has_key("user"):
|
||||
key = kwargs["user"]
|
||||
else:
|
||||
return None
|
||||
if not cls.instances.has_key(key):
|
||||
cls.instances[key] = None
|
||||
if kwargs.has_key("password"):
|
||||
kwargs["password"] = decrypt(kwargs["password"])
|
||||
|
||||
if cls.instances[key] is None:
|
||||
cls.instances[key] = \
|
||||
super(ConnectionsManager, cls).__call__(**kwargs)
|
||||
else:
|
||||
cls.instances[key].refresh(key, kwargs["password"])
|
||||
return cls.instances[key]
|
||||
|
||||
class ConnectionError(Exception):
|
||||
pass
|
||||
@@ -8,9 +8,13 @@ class Migration(DataMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
"Write your forwards methods here."
|
||||
p = orm.Parameter.objects.get(name="webmail.SECRET_KEY")
|
||||
p.name = "auth.SECRET_KEY"
|
||||
p.save()
|
||||
try:
|
||||
p = orm.Parameter.objects.get(name="webmail.SECRET_KEY")
|
||||
except orm.Parameter.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
p.name = "auth.SECRET_KEY"
|
||||
p.save()
|
||||
|
||||
def backwards(self, orm):
|
||||
"Write your backwards methods here."
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
ul.autocompleter-choices
|
||||
{
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
border: 1px solid #7c7c7c;
|
||||
border-left-color: #c3c3c3;
|
||||
border-right-color: #c3c3c3;
|
||||
border-bottom-color: #ddd;
|
||||
background-color: #fff;
|
||||
text-align: left;
|
||||
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
|
||||
z-index: 50;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
ul.autocompleter-choices li
|
||||
{
|
||||
position: relative;
|
||||
margin: -2px 0 0 0;
|
||||
padding: 0.2em 1.5em 0.2em 1em;
|
||||
display: block;
|
||||
float: none !important;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
font-size: 1em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
ul.autocompleter-choices li.autocompleter-selected
|
||||
{
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
ul.autocompleter-choices span.autocompleter-queried
|
||||
{
|
||||
display: inline;
|
||||
float: none;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.autocompleter-choices li.autocompleter-selected span.autocompleter-queried
|
||||
{
|
||||
color: #9FCFFF;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Autocompleter.Local
|
||||
*
|
||||
* http://digitarald.de/project/autocompleter/
|
||||
*
|
||||
* @version 1.1.2
|
||||
*
|
||||
* @license MIT-style license
|
||||
* @author Harald Kirschner <mail [at] digitarald.de>
|
||||
* @copyright Author
|
||||
*/
|
||||
|
||||
Autocompleter.Local = new Class({
|
||||
|
||||
Extends: Autocompleter,
|
||||
|
||||
options: {
|
||||
minLength: 0,
|
||||
delay: 200
|
||||
},
|
||||
|
||||
initialize: function(element, tokens, options) {
|
||||
this.parent(element, options);
|
||||
this.tokens = tokens;
|
||||
},
|
||||
|
||||
query: function() {
|
||||
this.update(this.filter());
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Autocompleter.Request
|
||||
*
|
||||
* http://digitarald.de/project/autocompleter/
|
||||
*
|
||||
* @version 1.1.2
|
||||
*
|
||||
* @license MIT-style license
|
||||
* @author Harald Kirschner <mail [at] digitarald.de>
|
||||
* @copyright Author
|
||||
*/
|
||||
|
||||
Autocompleter.Request = new Class({
|
||||
|
||||
Extends: Autocompleter,
|
||||
|
||||
options: {/*
|
||||
indicator: null,
|
||||
indicatorClass: null,
|
||||
onRequest: $empty,
|
||||
onComplete: $empty,*/
|
||||
postData: {},
|
||||
ajaxOptions: {},
|
||||
postVar: 'value'
|
||||
|
||||
},
|
||||
|
||||
query: function(){
|
||||
var data = $unlink(this.options.postData) || {};
|
||||
data[this.options.postVar] = this.queryValue;
|
||||
var indicator = $(this.options.indicator);
|
||||
if (indicator) indicator.setStyle('display', '');
|
||||
var cls = this.options.indicatorClass;
|
||||
if (cls) this.element.addClass(cls);
|
||||
this.fireEvent('onRequest', [this.element, this.request, data, this.queryValue]);
|
||||
this.request.send({'data': data});
|
||||
},
|
||||
|
||||
/**
|
||||
* queryResponse - abstract
|
||||
*
|
||||
* Inherated classes have to extend this function and use this.parent()
|
||||
*/
|
||||
queryResponse: function() {
|
||||
var indicator = $(this.options.indicator);
|
||||
if (indicator) indicator.setStyle('display', 'none');
|
||||
var cls = this.options.indicatorClass;
|
||||
if (cls) this.element.removeClass(cls);
|
||||
return this.fireEvent('onComplete', [this.element, this.request]);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Autocompleter.Request.JSON = new Class({
|
||||
|
||||
Extends: Autocompleter.Request,
|
||||
|
||||
initialize: function(el, url, options) {
|
||||
this.parent(el, options);
|
||||
this.request = new Request.JSON($merge({
|
||||
'url': url,
|
||||
'link': 'cancel'
|
||||
}, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this));
|
||||
},
|
||||
|
||||
queryResponse: function(response) {
|
||||
this.parent();
|
||||
this.update(response);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Autocompleter.Request.HTML = new Class({
|
||||
|
||||
Extends: Autocompleter.Request,
|
||||
|
||||
initialize: function(el, url, options) {
|
||||
this.parent(el, options);
|
||||
this.request = new Request.HTML($merge({
|
||||
'url': url,
|
||||
'link': 'cancel',
|
||||
'update': this.choices
|
||||
}, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this));
|
||||
},
|
||||
|
||||
queryResponse: function(tree, elements) {
|
||||
this.parent();
|
||||
if (!elements || !elements.length) {
|
||||
this.hideChoices();
|
||||
} else {
|
||||
this.choices.getChildren(this.options.choicesMatch).each(this.options.injectChoice || function(choice) {
|
||||
var value = choice.innerHTML;
|
||||
choice.inputValue = value;
|
||||
this.addChoiceEvents(choice.set('html', this.markQueryValue(value)));
|
||||
}, this);
|
||||
this.showChoices();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/* compatibility */
|
||||
|
||||
Autocompleter.Ajax = {
|
||||
Base: Autocompleter.Request,
|
||||
Json: Autocompleter.Request.JSON,
|
||||
Xhtml: Autocompleter.Request.HTML
|
||||
};
|
||||
@@ -0,0 +1,442 @@
|
||||
/**
|
||||
* Autocompleter
|
||||
*
|
||||
* http://digitarald.de/project/autocompleter/
|
||||
*
|
||||
* @version 1.1.2
|
||||
*
|
||||
* @license MIT-style license
|
||||
* @author Harald Kirschner <mail [at] digitarald.de>
|
||||
* @copyright Author
|
||||
*/
|
||||
|
||||
var Autocompleter = new Class({
|
||||
|
||||
Implements: [Options, Events],
|
||||
|
||||
options: {/*
|
||||
onOver: $empty,
|
||||
onSelect: $empty,
|
||||
onSelection: $empty,
|
||||
onShow: $empty,
|
||||
onHide: $empty,
|
||||
onBlur: $empty,
|
||||
onFocus: $empty,*/
|
||||
minLength: 1,
|
||||
markQuery: true,
|
||||
width: 'inherit',
|
||||
maxChoices: 10,
|
||||
injectChoice: null,
|
||||
customChoices: null,
|
||||
emptyChoices: null,
|
||||
visibleChoices: true,
|
||||
className: 'autocompleter-choices',
|
||||
zIndex: 42,
|
||||
delay: 400,
|
||||
observerOptions: {},
|
||||
fxOptions: {},
|
||||
|
||||
autoSubmit: false,
|
||||
overflow: false,
|
||||
overflowMargin: 25,
|
||||
selectFirst: false,
|
||||
filter: null,
|
||||
filterCase: false,
|
||||
filterSubset: false,
|
||||
forceSelect: false,
|
||||
selectMode: true,
|
||||
choicesMatch: null,
|
||||
|
||||
multiple: false,
|
||||
separator: ', ',
|
||||
separatorSplit: /\s*[,;]\s*/,
|
||||
autoTrim: false,
|
||||
allowDupes: false,
|
||||
|
||||
cache: true,
|
||||
relative: false
|
||||
},
|
||||
|
||||
initialize: function(element, options) {
|
||||
this.element = $(element);
|
||||
this.setOptions(options);
|
||||
this.build();
|
||||
this.observer = new Observer(this.element, this.prefetch.bind(this), $merge({
|
||||
'delay': this.options.delay
|
||||
}, this.options.observerOptions));
|
||||
this.queryValue = null;
|
||||
if (this.options.filter) this.filter = this.options.filter.bind(this);
|
||||
var mode = this.options.selectMode;
|
||||
this.typeAhead = (mode == 'type-ahead');
|
||||
this.selectMode = (mode === true) ? 'selection' : mode;
|
||||
this.cached = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* build - Initialize DOM
|
||||
*
|
||||
* Builds the html structure for choices and appends the events to the element.
|
||||
* Override this function to modify the html generation.
|
||||
*/
|
||||
build: function() {
|
||||
if ($(this.options.customChoices)) {
|
||||
this.choices = this.options.customChoices;
|
||||
} else {
|
||||
this.choices = new Element('ul', {
|
||||
'class': this.options.className,
|
||||
'styles': {
|
||||
'zIndex': this.options.zIndex
|
||||
}
|
||||
}).inject(document.body);
|
||||
this.relative = false;
|
||||
if (this.options.relative) {
|
||||
this.choices.inject(this.element, 'after');
|
||||
this.relative = this.element.getOffsetParent();
|
||||
}
|
||||
this.fix = new OverlayFix(this.choices);
|
||||
}
|
||||
if (!this.options.separator.test(this.options.separatorSplit)) {
|
||||
this.options.separatorSplit = this.options.separator;
|
||||
}
|
||||
this.fx = (!this.options.fxOptions) ? null : new Fx.Tween(this.choices, $merge({
|
||||
'property': 'opacity',
|
||||
'link': 'cancel',
|
||||
'duration': 200
|
||||
}, this.options.fxOptions)).addEvent('onStart', Chain.prototype.clearChain).set(0);
|
||||
this.element.setProperty('autocomplete', 'off')
|
||||
.addEvent((Browser.Engine.trident || Browser.Engine.webkit) ? 'keydown' : 'keypress', this.onCommand.bind(this))
|
||||
.addEvent('click', this.onCommand.bind(this, [false]))
|
||||
.addEvent('focus', this.toggleFocus.create({bind: this, arguments: true, delay: 100}))
|
||||
.addEvent('blur', this.toggleFocus.create({bind: this, arguments: false, delay: 100}));
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
if (this.fix) this.fix.destroy();
|
||||
this.choices = this.selected = this.choices.destroy();
|
||||
},
|
||||
|
||||
toggleFocus: function(state) {
|
||||
this.focussed = state;
|
||||
if (!state) this.hideChoices(true);
|
||||
this.fireEvent((state) ? 'onFocus' : 'onBlur', [this.element]);
|
||||
},
|
||||
|
||||
onCommand: function(e) {
|
||||
if (!e && this.focussed) return this.prefetch();
|
||||
if (e && e.key && !e.shift) {
|
||||
switch (e.key) {
|
||||
case 'enter':
|
||||
if (this.element.value != this.opted) return true;
|
||||
if (this.selected && this.visible) {
|
||||
this.choiceSelect(this.selected);
|
||||
return !!(this.options.autoSubmit);
|
||||
}
|
||||
break;
|
||||
case 'up': case 'down':
|
||||
if (!this.prefetch() && this.queryValue !== null) {
|
||||
var up = (e.key == 'up');
|
||||
this.choiceOver((this.selected || this.choices)[
|
||||
(this.selected) ? ((up) ? 'getPrevious' : 'getNext') : ((up) ? 'getLast' : 'getFirst')
|
||||
](this.options.choicesMatch), true);
|
||||
}
|
||||
return false;
|
||||
case 'esc': case 'tab':
|
||||
this.hideChoices(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
setSelection: function(finish) {
|
||||
var input = this.selected.inputValue, value = input;
|
||||
var start = this.queryValue.length, end = input.length;
|
||||
if (input.substr(0, start).toLowerCase() != this.queryValue.toLowerCase()) start = 0;
|
||||
if (this.options.multiple) {
|
||||
var split = this.options.separatorSplit;
|
||||
value = this.element.value;
|
||||
start += this.queryIndex;
|
||||
end += this.queryIndex;
|
||||
var old = value.substr(this.queryIndex).split(split, 1)[0];
|
||||
value = value.substr(0, this.queryIndex) + input + value.substr(this.queryIndex + old.length);
|
||||
if (finish) {
|
||||
var tokens = value.split(this.options.separatorSplit).filter(function(entry) {
|
||||
return this.test(entry);
|
||||
}, /[^\s,]+/);
|
||||
if (!this.options.allowDupes) tokens = [].combine(tokens);
|
||||
var sep = this.options.separator;
|
||||
value = tokens.join(sep) + sep;
|
||||
end = value.length;
|
||||
}
|
||||
}
|
||||
this.observer.setValue(value);
|
||||
this.opted = value;
|
||||
if (finish || this.selectMode == 'pick') start = end;
|
||||
this.element.selectRange(start, end);
|
||||
this.fireEvent('onSelection', [this.element, this.selected, value, input]);
|
||||
},
|
||||
|
||||
showChoices: function() {
|
||||
var match = this.options.choicesMatch, first = this.choices.getFirst(match);
|
||||
this.selected = this.selectedValue = null;
|
||||
if (this.fix) {
|
||||
var pos = this.element.getCoordinates(this.relative), width = this.options.width || 'auto';
|
||||
this.choices.setStyles({
|
||||
'left': pos.left,
|
||||
'top': pos.bottom,
|
||||
'width': (width === true || width == 'inherit') ? pos.width : width
|
||||
});
|
||||
}
|
||||
if (!first) return;
|
||||
if (!this.visible) {
|
||||
this.visible = true;
|
||||
this.choices.setStyle('display', '');
|
||||
if (this.fx) this.fx.start(1);
|
||||
this.fireEvent('onShow', [this.element, this.choices]);
|
||||
}
|
||||
if (this.options.selectFirst || this.typeAhead || first.inputValue == this.queryValue) this.choiceOver(first, this.typeAhead);
|
||||
var items = this.choices.getChildren(match), max = this.options.maxChoices;
|
||||
var styles = {'overflowY': 'hidden', 'height': ''};
|
||||
this.overflown = false;
|
||||
if (items.length > max) {
|
||||
var item = items[max - 1];
|
||||
styles.overflowY = 'scroll';
|
||||
styles.height = item.getCoordinates(this.choices).bottom;
|
||||
this.overflown = true;
|
||||
};
|
||||
this.choices.setStyles(styles);
|
||||
this.fix.show();
|
||||
if (this.options.visibleChoices) {
|
||||
var scroll = document.getScroll(),
|
||||
size = document.getSize(),
|
||||
coords = this.choices.getCoordinates();
|
||||
if (coords.right > scroll.x + size.x) scroll.x = coords.right - size.x;
|
||||
if (coords.bottom > scroll.y + size.y) scroll.y = coords.bottom - size.y;
|
||||
window.scrollTo(Math.min(scroll.x, coords.left), Math.min(scroll.y, coords.top));
|
||||
}
|
||||
},
|
||||
|
||||
hideChoices: function(clear) {
|
||||
if (clear) {
|
||||
var value = this.element.value;
|
||||
if (this.options.forceSelect) value = this.opted;
|
||||
if (this.options.autoTrim) {
|
||||
value = value.split(this.options.separatorSplit).filter($arguments(0)).join(this.options.separator);
|
||||
}
|
||||
this.observer.setValue(value);
|
||||
}
|
||||
if (!this.visible) return;
|
||||
this.visible = false;
|
||||
if (this.selected) this.selected.removeClass('autocompleter-selected');
|
||||
this.observer.clear();
|
||||
var hide = function(){
|
||||
this.choices.setStyle('display', 'none');
|
||||
this.fix.hide();
|
||||
}.bind(this);
|
||||
if (this.fx) this.fx.start(0).chain(hide);
|
||||
else hide();
|
||||
this.fireEvent('onHide', [this.element, this.choices]);
|
||||
},
|
||||
|
||||
prefetch: function() {
|
||||
var value = this.element.value, query = value;
|
||||
if (this.options.multiple) {
|
||||
var split = this.options.separatorSplit;
|
||||
var values = value.split(split);
|
||||
var index = this.element.getSelectedRange().start;
|
||||
var toIndex = value.substr(0, index).split(split);
|
||||
var last = toIndex.length - 1;
|
||||
index -= toIndex[last].length;
|
||||
query = values[last];
|
||||
}
|
||||
if (query.length < this.options.minLength) {
|
||||
this.hideChoices();
|
||||
} else {
|
||||
if (query === this.queryValue || (this.visible && query == this.selectedValue)) {
|
||||
if (this.visible) return false;
|
||||
this.showChoices();
|
||||
} else {
|
||||
this.queryValue = query;
|
||||
this.queryIndex = index;
|
||||
if (!this.fetchCached()) this.query();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
fetchCached: function() {
|
||||
return false;
|
||||
if (!this.options.cache
|
||||
|| !this.cached
|
||||
|| !this.cached.length
|
||||
|| this.cached.length >= this.options.maxChoices
|
||||
|| this.queryValue) return false;
|
||||
this.update(this.filter(this.cached));
|
||||
return true;
|
||||
},
|
||||
|
||||
update: function(tokens) {
|
||||
this.choices.empty();
|
||||
this.cached = tokens;
|
||||
var type = tokens && $type(tokens);
|
||||
if (!type || (type == 'array' && !tokens.length) || (type == 'hash' && !tokens.getLength())) {
|
||||
(this.options.emptyChoices || this.hideChoices).call(this);
|
||||
} else {
|
||||
if (this.options.maxChoices < tokens.length && !this.options.overflow) tokens.length = this.options.maxChoices;
|
||||
tokens.each(this.options.injectChoice || function(token){
|
||||
var choice = new Element('li', {'html': this.markQueryValue(token)});
|
||||
choice.inputValue = token;
|
||||
this.addChoiceEvents(choice).inject(this.choices);
|
||||
}, this);
|
||||
this.showChoices();
|
||||
}
|
||||
},
|
||||
|
||||
choiceOver: function(choice, selection) {
|
||||
if (!choice || choice == this.selected) return;
|
||||
if (this.selected) this.selected.removeClass('autocompleter-selected');
|
||||
this.selected = choice.addClass('autocompleter-selected');
|
||||
this.fireEvent('onSelect', [this.element, this.selected, selection]);
|
||||
if (!this.selectMode) this.opted = this.element.value;
|
||||
if (!selection) return;
|
||||
this.selectedValue = this.selected.inputValue;
|
||||
if (this.overflown) {
|
||||
var coords = this.selected.getCoordinates(this.choices), margin = this.options.overflowMargin,
|
||||
top = this.choices.scrollTop, height = this.choices.offsetHeight, bottom = top + height;
|
||||
if (coords.top - margin < top && top) this.choices.scrollTop = Math.max(coords.top - margin, 0);
|
||||
else if (coords.bottom + margin > bottom) this.choices.scrollTop = Math.min(coords.bottom - height + margin, bottom);
|
||||
}
|
||||
if (this.selectMode) this.setSelection();
|
||||
},
|
||||
|
||||
choiceSelect: function(choice) {
|
||||
if (choice) this.choiceOver(choice);
|
||||
this.setSelection(true);
|
||||
this.queryValue = false;
|
||||
this.hideChoices();
|
||||
},
|
||||
|
||||
filter: function(tokens) {
|
||||
return (tokens || this.tokens).filter(function(token) {
|
||||
return this.test(token);
|
||||
}, new RegExp(((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp(), (this.options.filterCase) ? '' : 'i'));
|
||||
},
|
||||
|
||||
/**
|
||||
* markQueryValue
|
||||
*
|
||||
* Marks the queried word in the given string with <span class="autocompleter-queried">*</span>
|
||||
* Call this i.e. from your custom parseChoices, same for addChoiceEvents
|
||||
*
|
||||
* @param {String} Text
|
||||
* @return {String} Text
|
||||
*/
|
||||
markQueryValue: function(str) {
|
||||
return (!this.options.markQuery || !this.queryValue) ? str
|
||||
: str.replace(new RegExp('(' + ((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp() + ')', (this.options.filterCase) ? '' : 'i'), '<span class="autocompleter-queried">$1</span>');
|
||||
},
|
||||
|
||||
/**
|
||||
* addChoiceEvents
|
||||
*
|
||||
* Appends the needed event handlers for a choice-entry to the given element.
|
||||
*
|
||||
* @param {Element} Choice entry
|
||||
* @return {Element} Choice entry
|
||||
*/
|
||||
addChoiceEvents: function(el) {
|
||||
return el.addEvents({
|
||||
'mouseover': this.choiceOver.bind(this, [el]),
|
||||
'click': this.choiceSelect.bind(this, [el])
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var OverlayFix = new Class({
|
||||
|
||||
initialize: function(el) {
|
||||
if (Browser.Engine.trident) {
|
||||
this.element = $(el);
|
||||
this.relative = this.element.getOffsetParent();
|
||||
this.fix = new Element('iframe', {
|
||||
'frameborder': '0',
|
||||
'scrolling': 'no',
|
||||
'src': 'javascript:false;',
|
||||
'styles': {
|
||||
'position': 'absolute',
|
||||
'border': 'none',
|
||||
'display': 'none',
|
||||
'filter': 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)'
|
||||
}
|
||||
}).inject(this.element, 'after');
|
||||
}
|
||||
},
|
||||
|
||||
show: function() {
|
||||
if (this.fix) {
|
||||
var coords = this.element.getCoordinates(this.relative);
|
||||
delete coords.right;
|
||||
delete coords.bottom;
|
||||
this.fix.setStyles($extend(coords, {
|
||||
'display': '',
|
||||
'zIndex': (this.element.getStyle('zIndex') || 1) - 1
|
||||
}));
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
if (this.fix) this.fix.setStyle('display', 'none');
|
||||
return this;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
if (this.fix) this.fix = this.fix.destroy();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Element.implement({
|
||||
|
||||
getSelectedRange: function() {
|
||||
if (!Browser.Engine.trident) return {start: this.selectionStart, end: this.selectionEnd};
|
||||
var pos = {start: 0, end: 0};
|
||||
var range = this.getDocument().selection.createRange();
|
||||
if (!range || range.parentElement() != this) return pos;
|
||||
var dup = range.duplicate();
|
||||
if (this.type == 'text') {
|
||||
pos.start = 0 - dup.moveStart('character', -100000);
|
||||
pos.end = pos.start + range.text.length;
|
||||
} else {
|
||||
var value = this.value;
|
||||
var offset = value.length - value.match(/[\n\r]*$/)[0].length;
|
||||
dup.moveToElementText(this);
|
||||
dup.setEndPoint('StartToEnd', range);
|
||||
pos.end = offset - dup.text.length;
|
||||
dup.setEndPoint('StartToStart', range);
|
||||
pos.start = offset - dup.text.length;
|
||||
}
|
||||
return pos;
|
||||
},
|
||||
|
||||
selectRange: function(start, end) {
|
||||
if (Browser.Engine.trident) {
|
||||
var diff = this.value.substr(start, end - start).replace(/\r/g, '').length;
|
||||
start = this.value.substr(0, start).replace(/\r/g, '').length;
|
||||
var range = this.createTextRange();
|
||||
range.collapse(true);
|
||||
range.moveEnd('character', start + diff);
|
||||
range.moveStart('character', start);
|
||||
range.select();
|
||||
} else {
|
||||
this.focus();
|
||||
this.setSelectionRange(start, end);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/* compatibility */
|
||||
|
||||
Autocompleter.Base = Autocompleter;
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Observer - Observe formelements for changes
|
||||
*
|
||||
* - Additional code from clientside.cnet.com
|
||||
*
|
||||
* @version 1.1
|
||||
*
|
||||
* @license MIT-style license
|
||||
* @author Harald Kirschner <mail [at] digitarald.de>
|
||||
* @copyright Author
|
||||
*/
|
||||
var Observer = new Class({
|
||||
|
||||
Implements: [Options, Events],
|
||||
|
||||
options: {
|
||||
periodical: false,
|
||||
delay: 1000
|
||||
},
|
||||
|
||||
initialize: function(el, onFired, options){
|
||||
this.element = $(el) || $$(el);
|
||||
this.addEvent('onFired', onFired);
|
||||
this.setOptions(options);
|
||||
this.bound = this.changed.bind(this);
|
||||
this.resume();
|
||||
},
|
||||
|
||||
changed: function() {
|
||||
var value = this.element.get('value');
|
||||
if ($equals(this.value, value)) return;
|
||||
this.clear();
|
||||
this.value = value;
|
||||
this.timeout = this.onFired.delay(this.options.delay, this);
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
this.element.set('value', value);
|
||||
return this.clear();
|
||||
},
|
||||
|
||||
onFired: function() {
|
||||
this.fireEvent('onFired', [this.value, this.element]);
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
$clear(this.timeout || null);
|
||||
return this;
|
||||
},
|
||||
|
||||
pause: function(){
|
||||
if (this.timer) $clear(this.timer);
|
||||
else this.element.removeEvent('keyup', this.bound);
|
||||
return this.clear();
|
||||
},
|
||||
|
||||
resume: function(){
|
||||
this.value = this.element.get('value');
|
||||
if (this.options.periodical) this.timer = this.changed.periodical(this.options.periodical, this);
|
||||
else this.element.addEvent('keyup', this.bound);
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var $equals = function(obj1, obj2) {
|
||||
return (obj1 == obj2 || JSON.encode(obj1) == JSON.encode(obj2));
|
||||
};
|
||||
@@ -118,15 +118,14 @@ var SqueezeBox = {
|
||||
return !SqueezeBox.fromElement(this, options);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
open: function(subject, options) {
|
||||
this.initialize();
|
||||
|
||||
if (this.element != null) this.trash();
|
||||
this.element = $(subject) || false;
|
||||
|
||||
|
||||
this.setOptions(Object.merge(this.presets, options || {}));
|
||||
|
||||
if (this.element && this.options.parse) {
|
||||
var obj = this.element.getProperty(this.options.parse);
|
||||
if (obj && (obj = JSON.decode(obj, this.options.parseSecure))) this.setOptions(obj);
|
||||
@@ -134,7 +133,7 @@ var SqueezeBox = {
|
||||
this.url = ((this.element) ? (this.element.get('href')) : subject) || this.options.url || '';
|
||||
|
||||
this.assignOptions();
|
||||
|
||||
|
||||
var handler = handler || this.options.handler;
|
||||
if (handler) return this.setContent(handler, this.parsers[handler].call(this, true));
|
||||
var ret = false;
|
||||
@@ -147,7 +146,7 @@ var SqueezeBox = {
|
||||
return false;
|
||||
}, this);
|
||||
},
|
||||
|
||||
|
||||
fromElement: function(from, options) {
|
||||
return this.open(from, options);
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ var Confirmation = new Class({
|
||||
id: "dlg-iconbox",
|
||||
html: "<img src='/static/pics/dialog-warning.png' alt='Warning' />"
|
||||
}).inject(content);
|
||||
|
||||
|
||||
this.textdiv = new Element("div", {
|
||||
id: "dlg-textbox",
|
||||
html: "<b>" + question + "</b>"
|
||||
@@ -42,19 +42,19 @@ var Confirmation = new Class({
|
||||
.addEvent("click", this.cancel_handler)
|
||||
.inject(buttonsdiv);
|
||||
|
||||
new Element("input", {type: "hidden", name: "result", id: "result", value: "cancel"})
|
||||
new Element("input", {type: "hidden", name: "result",
|
||||
id: "result", value: "cancel"})
|
||||
.inject(content);
|
||||
|
||||
SqueezeBox.initialize({
|
||||
size: {x: 350, y: 140},
|
||||
SqueezeBox.open(content, {
|
||||
size: {x: 350, y: 140},
|
||||
handler: "adopt",
|
||||
onClose: this.close_handler
|
||||
onClose: this.close_handler.bind(content)
|
||||
});
|
||||
SqueezeBox.open(content);
|
||||
},
|
||||
|
||||
close_handler: function(content) {
|
||||
var result = $("confirmbox").getElement("input[name=result]");
|
||||
var result = this.getElement("input[name=result]");
|
||||
if (result.get("value") != "ok") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,18 +12,6 @@ window.addEvent('domready', function() {
|
||||
});
|
||||
}
|
||||
|
||||
if ($$("a[name=deleteDom]"))
|
||||
$$("a[name=deleteDom]").addEvent('click', function(event) {
|
||||
if (!confirm(gettext("Delete this domain?")))
|
||||
event.stop();
|
||||
});
|
||||
|
||||
if ($$("a[name=deleteAlias]"))
|
||||
$$("a[name=deleteAlias]").addEvent('click', function(event) {
|
||||
if (!confirm(gettext("Delete this alias?")))
|
||||
event.stop();
|
||||
});
|
||||
|
||||
$(document.body).addEvent('click', function(event) {
|
||||
var target = event.target;
|
||||
if (target.get('tag') == 'a') {
|
||||
@@ -56,14 +44,3 @@ window.addEvent('domready', function() {
|
||||
selectable: true
|
||||
});
|
||||
});
|
||||
|
||||
loadPage = function(url, destination, callback) {
|
||||
destination = (destination == null) ? "page" : destination;
|
||||
|
||||
if (callback)
|
||||
new Request({url: url, method: "get", onSuccess: callback}).send();
|
||||
else
|
||||
new Request({url: url, method: "get", onSuccess: function(data) {
|
||||
$(destination).set('html', data);
|
||||
}}).send();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
var DynamicTextInput = new Class({
|
||||
Implements: [Options],
|
||||
|
||||
options: {
|
||||
|
||||
},
|
||||
|
||||
initialize: function(targets, options) {
|
||||
if (!$defined(targets) || !targets.length) {
|
||||
return;
|
||||
}
|
||||
for (var cpt = 0; cpt < targets.length; cpt++) {
|
||||
var target = targets[cpt];
|
||||
var btn;
|
||||
|
||||
if (cpt == 0) {
|
||||
btn = new Element("img", {
|
||||
src: parent.static_url("pics/add.png")
|
||||
});
|
||||
btn.addEvent("click", this.newinput.bind(this));
|
||||
} else {
|
||||
var btnid = "btn_" + cpt;
|
||||
btn = new Element("img", {
|
||||
src: parent.static_url("pics/remove.png"),
|
||||
id: btnid
|
||||
});
|
||||
btn.addEvent("click", this.removeinput.bind(this));
|
||||
}
|
||||
btn.setStyles({
|
||||
"float": "right",
|
||||
"margin-top": "2px"
|
||||
});
|
||||
btn.inject(target, "after");
|
||||
}
|
||||
this.nextid = targets.length;
|
||||
this.target = targets[0];
|
||||
this.setOptions(options);
|
||||
},
|
||||
|
||||
newinput: function(event) {
|
||||
if (this.target.get("value") == "") {
|
||||
return;
|
||||
}
|
||||
var coords = this.target.getCoordinates();
|
||||
var div = new Element("div").addClass("row");
|
||||
var label = new Element("label").inject(div);
|
||||
var ninput = new Element("input", {
|
||||
type: "text",
|
||||
id: this.target.get("id") + "_" + this.nextid,
|
||||
name: this.target.get("name"),
|
||||
value: this.target.get("value")
|
||||
}).inject(div);
|
||||
var delbtn = new Element("img", {
|
||||
id: "delbtn_" + this.nextid,
|
||||
src: parent.static_url("pics/remove.png")
|
||||
});
|
||||
|
||||
this.nextid += 1;
|
||||
delbtn.setStyles({
|
||||
"float" : "right",
|
||||
"margin-top": "2px"
|
||||
});
|
||||
div.inject(this.target.getParent(), "after");
|
||||
delbtn.inject(ninput, "after");
|
||||
delbtn.addEvent("click", this.removeinput.bind(this));
|
||||
this.target.set("value", "");
|
||||
},
|
||||
|
||||
removeinput: function(event) {
|
||||
var id = (event.target.get("id").split("_"))[1];
|
||||
var input = $("id_" + this.target.get("name") + "_" + id);
|
||||
|
||||
input.destroy();
|
||||
event.target.destroy();
|
||||
}
|
||||
});
|
||||
@@ -1,10 +1,4 @@
|
||||
var error_callbacks = {
|
||||
mbaliasform: function() {
|
||||
if ($("id_domain").value == 0) {
|
||||
$("id_mboxes").set("html", "");
|
||||
}
|
||||
mbaliasform_init();
|
||||
},
|
||||
permform: function() {
|
||||
if ($defined($("id_domain"))) {
|
||||
$("id_domain").options[0].setProperty("selected", "selected");
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
{% if error %}<div class="error">{{ error }}</div>{% endif %}
|
||||
<form action="{{ action }}" method="POST" id="{{ formid }}" name="{{ formid }}">
|
||||
{% include "common/genericform.html" %}
|
||||
{% block extrafields %}{% endblock %}
|
||||
<div class="spacer"></div>
|
||||
<div class="lastrow" align="center">
|
||||
<input type="submit" name="submit" value="{{ submit_label }}" />
|
||||
|
||||
@@ -12,11 +12,10 @@ window.addEvent("domready", function() {
|
||||
var sizes = $("objects").get("rel").split(" ");
|
||||
|
||||
evt.stop();
|
||||
SqueezeBox.initialize({
|
||||
size: {x: sizes[0], y: sizes[1]},
|
||||
SqueezeBox.open(rooturl + type + "/new/", {
|
||||
size: {x: parseInt(sizes[0]), y: parseInt(sizes[1])},
|
||||
handler: 'iframe'
|
||||
});
|
||||
SqueezeBox.open(rooturl + type + "/new/");
|
||||
});
|
||||
|
||||
$$("a[name=remove]").addEvent("click", function(evt) {
|
||||
@@ -28,11 +27,19 @@ window.addEvent("domready", function() {
|
||||
var selection = "";
|
||||
|
||||
objtable._selectedRows.each(function(item) {
|
||||
var options = JSON.decode(item.get("rel"));
|
||||
|
||||
if ($defined(options.ui_disabled) && options.ui_disabled == "True") {
|
||||
return;
|
||||
}
|
||||
if (selection != "") {
|
||||
selection += ",";
|
||||
}
|
||||
selection += item.get("id");
|
||||
});
|
||||
if (selection == "") {
|
||||
return;
|
||||
}
|
||||
new Confirmation(rooturl + type + "/delete/", "selection=" + selection,
|
||||
gettext("Remove this selection?"), {
|
||||
{% if deloptions %}checkboxes: { {% join deloptions %} }{% endif %}
|
||||
|
||||
@@ -2,27 +2,31 @@
|
||||
{% load i18n admin_extras %}
|
||||
{% block pagetitle %}{% trans "Mailbox aliases" %}{% endblock %}
|
||||
{% block subcontent %}
|
||||
<div id="objects" name="mbaliases" rel="310 300">
|
||||
<div id="objects" name="mbaliases" rel="320 300">
|
||||
{% if not aliases %}
|
||||
<div class="info">{% trans "No mailbox alias defined." %}</div>
|
||||
{% else %}
|
||||
<table id="objects_table">
|
||||
<tr>
|
||||
<th>{% trans "Address" %}</th>
|
||||
<th>{% trans "Mailbox" %}</th>
|
||||
<th width="25%">{% trans "Address" %}</th>
|
||||
<th width="40%">{% trans "Target(s)" %}</th>
|
||||
<th width="70px" class="center">{% trans "Enabled" %}</th>
|
||||
<th width="70px" class="center">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
{% for alias in aliases %}
|
||||
<tr class="{% cycle 'odd' 'even' %}" id="{{ alias.id }}">
|
||||
<tr class="{% cycle 'odd' 'even' %}" id="{{ alias.id }}"
|
||||
rel="{ui_disabled : '{{ alias.ui_disabled }}'}">
|
||||
<td>{{ alias.address }}@{{ alias.domain }}</td>
|
||||
<td>
|
||||
{% for mb in alias.mboxes.all %}
|
||||
{{ mb.full_address }}<br/>
|
||||
{% endfor %}
|
||||
{{ alias.repr_extmboxes|safe }}
|
||||
</td>
|
||||
<td class="center">{{ alias.enabled|yesno }}</td>
|
||||
<td class="center">{% mbalias_actions user alias %}</td>
|
||||
<td class="center">
|
||||
{% if alias.ui_disabled %}--{% else %}{% mbalias_actions user alias %}{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
{% extends "admin/adminform.html" %}
|
||||
{% load i18n %}
|
||||
{% block html_head %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/Autocompleter.css" />
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/dynamic_textinput.js"></script>
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/Observer.js"></script>
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/Autocompleter.js"></script>
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/Autocompleter.Request.js"></script>
|
||||
<script type="text/javascript">
|
||||
function init_form() {
|
||||
var dti = new DynamicTextInput($$("input[name=targets]"));
|
||||
|
||||
new Autocompleter.Request.JSON("id_targets",
|
||||
"{% url admin.views.mailboxes_search %}", {
|
||||
postVar: "search",
|
||||
minLength: 3
|
||||
});
|
||||
}
|
||||
|
||||
window.addEvent("domready", function() {
|
||||
mbaliasform_init = function() {
|
||||
$("id_domain").addEvent("change", function(event) {
|
||||
var domid = this.value;
|
||||
if (domid == "") {
|
||||
$("id_mboxes").set("html", "");
|
||||
return;
|
||||
}
|
||||
new Request({
|
||||
url: "{% url domains %}" + domid + "/rawmboxes/?target=mbaliases",
|
||||
method: "get", onSuccess: function(data) {
|
||||
$("id_mboxes").set("html", data);
|
||||
}
|
||||
}).send();
|
||||
});
|
||||
};
|
||||
mbaliasform_init();
|
||||
register_error_callback("mbaliasform", init_form);
|
||||
init_form();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block extrafields %}
|
||||
{% for t in targets %}
|
||||
<div class="row">
|
||||
<label></label>
|
||||
<input type="text" name="targets" id="id_targets_{{ forloop.counter }}" value="{{ t }}" />
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
Executable
Executable
+198
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Automatic postfixadmin to Modoboa migration
|
||||
===========================================
|
||||
|
||||
This script provides an easy way to migrate an existing postfixadmin
|
||||
database to a Modoboa one.
|
||||
|
||||
As the two products do not share the same schema, some informations
|
||||
will be lost on the new database. Here is the list:
|
||||
* Description, transport and backup MX for domains
|
||||
* Dates (creation/modification)
|
||||
* Logs
|
||||
* Fetchmail table
|
||||
|
||||
Mailboxes organization on the filesystem will be changed. PostfixAdmin
|
||||
uses the following layout::
|
||||
|
||||
<topdir>/domain.tld/user@domain.tld/
|
||||
|
||||
Whereas Modoboa uses the following::
|
||||
|
||||
<topdir>/domain.tld/user/
|
||||
|
||||
To rename directories, you'll need the appropriate permissions.
|
||||
|
||||
Domain administrators that manage more than one domain will not be
|
||||
completly restored. They will only be administrator of the domain that
|
||||
corresponds to their username.
|
||||
|
||||
Vacation messages will not be migrated by this script. Users will have
|
||||
to define new messages by using Modoboa's autoreply plugin. (if
|
||||
activated)
|
||||
|
||||
One last point about passwords. PostfixAdmin uses different hash
|
||||
algorithms to store passwords so this script does not modify
|
||||
them. They are copied "as is" is the new database. In order to use
|
||||
them, you'll have to adapt the ``PASSWORD_SCHEME`` parameter in the
|
||||
admin. panel. Currently, it appears that the ``crypt`` scheme is
|
||||
compatible with PostfixAdmin ``md5crypt`` algorithm...
|
||||
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.db.utils import ConnectionDoesNotExist
|
||||
from django.db.models import Q
|
||||
from django.contrib.auth.models import Group
|
||||
import modoboa.admin.models as md_models
|
||||
import models as pf_models
|
||||
from modoboa.lib import split_mailbox, exec_cmd
|
||||
|
||||
def migrate_domain_aliases(domain, options):
|
||||
print "\tMigrating domain aliases"
|
||||
old_domain_aliases = pf_models.AliasDomain.objects.using(options._from).filter(target_domain=domain.name)
|
||||
for old_da in old_domain_aliases:
|
||||
new_da = md_models.DomainAlias()
|
||||
new_da.name = old_da.alias_domain
|
||||
new_da.target = old_da.target_domain
|
||||
new_da.enabled = new_da.active
|
||||
new_da.save(using=options.to)
|
||||
|
||||
def migrate_mailbox_aliases(domain, options):
|
||||
print "\tMigrating mailbox aliases"
|
||||
old_aliases = pf_models.Alias.objects.using(options._from).filter(domain=domain.name)
|
||||
for old_al in old_aliases:
|
||||
if old_al.address == old_al.goto:
|
||||
continue
|
||||
new_al = md_models.Alias()
|
||||
local_part, tmp = split_mailbox(old_al.address)
|
||||
if local_part is None or not len(local_part):
|
||||
if tmp is None or not len(tmp):
|
||||
print """Warning: skipping alias %s (cannot retrieve local part).
|
||||
You will need to recreate it manually.
|
||||
""" % old_al.address
|
||||
continue
|
||||
new_al.address = "*"
|
||||
else:
|
||||
new_al.address = local_part
|
||||
new_al.domain = domain
|
||||
new_al.enabled = old_al.active
|
||||
extmboxes = []
|
||||
intmboxes = []
|
||||
for goto in old_al.goto.split(","):
|
||||
try:
|
||||
mb = md_models.Mailbox.objects.using(options.to).get(user__username=goto)
|
||||
except md_models.Mailbox.DoesNotExist:
|
||||
extmboxes += [goto]
|
||||
else:
|
||||
intmboxes += [mb]
|
||||
new_al.save(intmboxes, extmboxes, using=options.to)
|
||||
|
||||
def migrate_mailboxes(domain, options):
|
||||
print "\tMigrating mailboxes"
|
||||
old_mboxes = pf_models.Mailbox.objects.using(options._from).filter(domain=domain.name)
|
||||
for old_mb in old_mboxes:
|
||||
new_mb = md_models.Mailbox()
|
||||
new_mb.name = old_mb.name
|
||||
new_mb.address = old_mb.local_part
|
||||
new_mb.password = old_mb.password
|
||||
new_mb.domain = domain
|
||||
if old_mb.quota:
|
||||
new_mb.quota = old_mb.quota / 1024000
|
||||
|
||||
new_mb.path = "%s/" % old_mb.local_part
|
||||
if options.rename_dirs:
|
||||
oldpath = os.path.join(options.mboxes_path, domain.name, old_mb.maildir)
|
||||
newpath = os.path.join(options.mboxes_path, domain.name, new_mb.path)
|
||||
code, output = exec_cmd("mv %s %s" % (oldpath, newpath))
|
||||
if code:
|
||||
print "Error: cannot rename mailbox directory\n%s" % output
|
||||
sys.exit(1)
|
||||
|
||||
new_mb.save(using=options.to)
|
||||
|
||||
|
||||
def migrate_domain(old_dom, options):
|
||||
print "Migrating domain %s" % old_dom.domain
|
||||
newdom = md_models.Domain()
|
||||
newdom.name = old_dom.domain
|
||||
newdom.enabled = old_dom.active
|
||||
newdom.quota = old_dom.maxquota
|
||||
newdom.save(using=options.to)
|
||||
migrate_mailboxes(newdom, options)
|
||||
migrate_mailbox_aliases(newdom, options)
|
||||
migrate_domain_aliases(newdom, options)
|
||||
|
||||
def migrate_admins(options):
|
||||
print "Migrating administrators"
|
||||
|
||||
dagroup = Group.objects.using(options.to).get(name="DomainAdmins")
|
||||
for old_admin in pf_models.Admin.objects.using(options._from).all():
|
||||
local_part, domname = split_mailbox(old_admin.username)
|
||||
try:
|
||||
query = Q(username=old_admin.username) & \
|
||||
(Q(domain="ALL") | Q(domain=domname))
|
||||
creds = pf_models.DomainAdmins.objects.using(options._from).get(query)
|
||||
except pf_models.DomainAdmins.DoesNotExist:
|
||||
print "Warning: skipping useless admin %s" % (old_admin.username)
|
||||
continue
|
||||
try:
|
||||
mb = md_models.Mailbox.objects.using(options.to).get(user__username=old_admin.username)
|
||||
except md_models.Mailbox.DoesNotExist:
|
||||
try:
|
||||
domain = md_models.Domain.objects.using(options.to).get(name=domname)
|
||||
except md_models.Domain.DoesNotExist:
|
||||
print "Warning: skipping domain admin %s, domain not found" \
|
||||
% old_admin.username
|
||||
continue
|
||||
mb = md_models.Mailbox()
|
||||
mb.address = local_part
|
||||
mb.domain = domain
|
||||
mb.name = local_part
|
||||
mb.enabled = old_admin.active
|
||||
mb.password = old_admin.password
|
||||
mb.save(using=options.to)
|
||||
|
||||
if creds.domain == "ALL":
|
||||
mb.user.is_superuser = True
|
||||
else:
|
||||
mb.user.groups.add(dagroup)
|
||||
mb.save(using=options.to)
|
||||
|
||||
def do_migration(options):
|
||||
pf_domains = pf_models.Domain.objects.using(options._from).all()
|
||||
for olddom in pf_domains:
|
||||
if olddom.domain == "ALL":
|
||||
continue
|
||||
migrate_domain(olddom, options)
|
||||
migrate_admins(options)
|
||||
|
||||
if __name__ == "__main__":
|
||||
from optparse import OptionParser
|
||||
|
||||
parser = OptionParser()
|
||||
parser.add_option("-f", "--from", dest="_from", default="pfxadmin",
|
||||
help="Name of postfixadmin db connection declared in settings.py")
|
||||
parser.add_option("-t", "--to", default="modoboa",
|
||||
help="Name of the Modoboa db connection declared in settings.py")
|
||||
parser.add_option("-r", "--rename-dirs", action="store_true",
|
||||
help="Rename mailbox directories (default is no)")
|
||||
parser.add_option("-p", "--mboxes-path", default=None,
|
||||
help="Path where directories are stored on the filesystem (used only if --rename-dirs)")
|
||||
|
||||
options, args = parser.parse_args()
|
||||
|
||||
if options.rename_dirs and options.mboxes_path is None:
|
||||
print "Error: you must provide the --mboxes-path option"
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
do_migration(options)
|
||||
except ConnectionDoesNotExist, e:
|
||||
print e
|
||||
|
||||
|
||||
|
||||
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
# This is an auto-generated Django model module.
|
||||
# You'll have to do the following manually to clean this up:
|
||||
# * Rearrange models' order
|
||||
# * Make sure each model has one field with primary_key=True
|
||||
# Feel free to rename the models, but don't rename db_table values or field names.
|
||||
#
|
||||
# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'
|
||||
# into your database.
|
||||
|
||||
from django.db import models
|
||||
|
||||
class Admin(models.Model):
|
||||
username = models.CharField(max_length=765, primary_key=True)
|
||||
password = models.CharField(max_length=765)
|
||||
created = models.DateTimeField()
|
||||
modified = models.DateTimeField()
|
||||
active = models.IntegerField()
|
||||
class Meta:
|
||||
db_table = u'admin'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
class Alias(models.Model):
|
||||
address = models.CharField(max_length=765, primary_key=True)
|
||||
goto = models.TextField()
|
||||
domain = models.CharField(max_length=765)
|
||||
created = models.DateTimeField()
|
||||
modified = models.DateTimeField()
|
||||
active = models.IntegerField()
|
||||
class Meta:
|
||||
db_table = u'alias'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
class AliasDomain(models.Model):
|
||||
alias_domain = models.CharField(max_length=765, primary_key=True)
|
||||
target_domain = models.CharField(max_length=765)
|
||||
created = models.DateTimeField()
|
||||
modified = models.DateTimeField()
|
||||
active = models.IntegerField()
|
||||
class Meta:
|
||||
db_table = u'alias_domain'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
class Config(models.Model):
|
||||
id = models.IntegerField(primary_key=True)
|
||||
name = models.CharField(unique=True, max_length=60)
|
||||
value = models.CharField(max_length=60)
|
||||
class Meta:
|
||||
db_table = u'config'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
class Domain(models.Model):
|
||||
domain = models.CharField(max_length=765, primary_key=True)
|
||||
description = models.CharField(max_length=765)
|
||||
aliases = models.IntegerField()
|
||||
mailboxes = models.IntegerField()
|
||||
maxquota = models.BigIntegerField()
|
||||
quota = models.BigIntegerField()
|
||||
transport = models.CharField(max_length=765)
|
||||
backupmx = models.IntegerField()
|
||||
created = models.DateTimeField()
|
||||
modified = models.DateTimeField()
|
||||
active = models.IntegerField()
|
||||
class Meta:
|
||||
db_table = u'domain'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
class DomainAdmins(models.Model):
|
||||
username = models.CharField(max_length=765)
|
||||
domain = models.CharField(max_length=765)
|
||||
created = models.DateTimeField(primary_key=True)
|
||||
active = models.IntegerField()
|
||||
class Meta:
|
||||
db_table = u'domain_admins'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
class Fetchmail(models.Model):
|
||||
id = models.IntegerField(primary_key=True)
|
||||
mailbox = models.CharField(max_length=765)
|
||||
src_server = models.CharField(max_length=765)
|
||||
src_auth = models.CharField(max_length=33, blank=True)
|
||||
src_user = models.CharField(max_length=765)
|
||||
src_password = models.CharField(max_length=765)
|
||||
src_folder = models.CharField(max_length=765)
|
||||
poll_time = models.IntegerField()
|
||||
fetchall = models.IntegerField()
|
||||
keep = models.IntegerField()
|
||||
protocol = models.CharField(max_length=12, blank=True)
|
||||
usessl = models.IntegerField()
|
||||
extra_options = models.TextField(blank=True)
|
||||
returned_text = models.TextField(blank=True)
|
||||
mda = models.CharField(max_length=765)
|
||||
date = models.DateTimeField()
|
||||
class Meta:
|
||||
db_table = u'fetchmail'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
class Log(models.Model):
|
||||
timestamp = models.DateTimeField()
|
||||
username = models.CharField(max_length=765)
|
||||
domain = models.CharField(max_length=765)
|
||||
action = models.CharField(max_length=765)
|
||||
data = models.TextField()
|
||||
class Meta:
|
||||
db_table = u'log'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
class Mailbox(models.Model):
|
||||
username = models.CharField(max_length=765, primary_key=True)
|
||||
password = models.CharField(max_length=765)
|
||||
name = models.CharField(max_length=765)
|
||||
maildir = models.CharField(max_length=765)
|
||||
quota = models.BigIntegerField()
|
||||
local_part = models.CharField(max_length=765)
|
||||
domain = models.CharField(max_length=765)
|
||||
created = models.DateTimeField()
|
||||
modified = models.DateTimeField()
|
||||
active = models.IntegerField()
|
||||
class Meta:
|
||||
db_table = u'mailbox'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
class Quota(models.Model):
|
||||
username = models.CharField(max_length=765, primary_key=True)
|
||||
path = models.CharField(max_length=300, primary_key=True)
|
||||
current = models.BigIntegerField(null=True, blank=True)
|
||||
class Meta:
|
||||
db_table = u'quota'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
class Quota2(models.Model):
|
||||
username = models.CharField(max_length=300, primary_key=True)
|
||||
bytes = models.BigIntegerField()
|
||||
messages = models.IntegerField()
|
||||
class Meta:
|
||||
db_table = u'quota2'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
class Vacation(models.Model):
|
||||
email = models.CharField(max_length=765)
|
||||
subject = models.CharField(max_length=765)
|
||||
body = models.TextField()
|
||||
cache = models.TextField()
|
||||
domain = models.CharField(max_length=765)
|
||||
created = models.DateTimeField()
|
||||
active = models.IntegerField()
|
||||
class Meta:
|
||||
db_table = u'vacation'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
class VacationNotification(models.Model):
|
||||
on_vacation = models.ForeignKey(Vacation, db_column='on_vacation')
|
||||
notified = models.CharField(max_length=765, primary_key=True)
|
||||
notified_at = models.DateTimeField()
|
||||
class Meta:
|
||||
db_table = u'vacation_notification'
|
||||
app_label = 'pfxadmin_migrate'
|
||||
|
||||
Reference in New Issue
Block a user