diff --git a/ROADMAP.md b/ROADMAP.md
index 2d5656e2..fc7acb0d 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,7 +1,7 @@
# Roadmap
-### 1.0a1 Milestone (first alpha release on May '14)
+### 1.0a1 Milestone (first alpha release on Sep '14)
1. [x] Automated deployment of the development environment
2. [x] Automated installation and upgrading
@@ -25,7 +25,7 @@
1. [ ] Initial documentation
-### 1.0b1 Milestone (first beta release on Jul '14)
+### 1.0b1 Milestone (first beta release on Nov '14)
1. [x] Resource monitoring
1. [ ] Orders
@@ -36,7 +36,7 @@
1. [ ] Full documentation
-### 1.0 Milestone (first stable release on Dec '14)
+### 1.0 Milestone (first stable release on Feb '15)
1. [ ] Stabilize data model, internal APIs and REST API
1. [ ] Integration with third-party service providers, e.g. Gandi
diff --git a/TODO.md b/TODO.md
index 90cc6e87..48c187f3 100644
--- a/TODO.md
+++ b/TODO.md
@@ -73,3 +73,6 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
* backend logs with hal logo
* Use logs for storing monitored values
* set_password orchestration method?
+
+
+* make account_link to autoreplace account on change view.
diff --git a/orchestra/apps/databases/models.py b/orchestra/apps/databases/models.py
index bf9ba555..13f9c9bc 100644
--- a/orchestra/apps/databases/models.py
+++ b/orchestra/apps/databases/models.py
@@ -15,7 +15,8 @@ class Database(models.Model):
name = models.CharField(_("name"), max_length=128,
validators=[validators.validate_name])
- users = models.ManyToManyField('databases.DatabaseUser', verbose_name=_("users"),
+ users = models.ManyToManyField('databases.DatabaseUser',
+ verbose_name=_("users"),
through='databases.Role', related_name='users')
type = models.CharField(_("type"), max_length=32,
choices=settings.DATABASES_TYPE_CHOICES,
diff --git a/orchestra/apps/mails/admin.py b/orchestra/apps/mails/admin.py
new file mode 100644
index 00000000..9817ab89
--- /dev/null
+++ b/orchestra/apps/mails/admin.py
@@ -0,0 +1,132 @@
+from django import forms
+from django.contrib import admin
+from django.contrib.auth import get_user_model
+from django.contrib.auth.admin import UserAdmin
+from django.core.urlresolvers import reverse
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext_lazy as _
+
+from orchestra.admin import ExtendedModelAdmin
+from orchestra.admin.utils import insertattr, admin_link
+from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
+from orchestra.apps.domains.forms import DomainIterator
+
+from .filters import HasMailboxListFilter, HasForwardListFilter, HasAddressListFilter
+from .models import Mailbox, Address, Autoresponse
+
+
+class AutoresponseInline(admin.StackedInline):
+ model = Autoresponse
+ verbose_name_plural = _("autoresponse")
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ if db_field.name == 'subject':
+ kwargs['widget'] = forms.TextInput(attrs={'size':'118'})
+ return super(AutoresponseInline, self).formfield_for_dbfield(db_field, **kwargs)
+
+
+class MailboxAdmin(AccountAdminMixin, ExtendedModelAdmin):
+ list_display = (
+ 'name', 'account_link', 'use_custom_filtering', 'display_addresses'
+ )
+ list_filter = ('use_custom_filtering', HasAddressListFilter)
+ add_fieldsets = (
+ (None, {
+ 'fields': ('account', 'name'),
+ }),
+ (_("Filtering"), {
+ 'fields': ('use_custom_filtering', 'custom_filtering'),
+ }),
+ )
+ fieldsets = (
+ (None, {
+ 'classes': ('wide',),
+ 'fields': ('account_link', 'name'),
+ }),
+ (_("Filtering"), {
+ 'classes': ('wide',),
+ 'fields': ('use_custom_filtering', 'custom_filtering'),
+ }),
+ (_("Addresses"), {
+ 'classes': ('wide',),
+ 'fields': ('addresses_field',)
+ }),
+ )
+ readonly_fields = ('account_link', 'display_addresses', 'addresses_field')
+
+ def display_addresses(self, mailbox):
+ addresses = []
+ for addr in mailbox.addresses.all():
+ url = reverse('admin:mails_address_change', args=(addr.pk,))
+ addresses.append('%s' % (url, addr.email))
+ return '
'.join(addresses)
+ display_addresses.short_description = _("Addresses")
+ display_addresses.allow_tags = True
+
+ def addresses_field(self, mailbox):
+ """ Address form field with "Add address" button """
+ account = mailbox.account
+ add_url = reverse('admin:mails_address_add')
+ add_url += '?account=%d&mailboxes=%s' % (account.pk, mailbox.pk)
+ img = ''
+ onclick = 'onclick="return showAddAnotherPopup(this);"'
+ add_link = '%s Add address' % (add_url, onclick, img)
+ value = '%s
' % add_link
+ for pk, name, domain in mailbox.addresses.values_list('pk', 'name', 'domain__name'):
+ url = reverse('admin:mails_address_change', args=(pk,))
+ name = '%s@%s' % (name, domain)
+ value += '
%s' % (url, name)
+ value = '' % value
+ return mark_safe('%s
' % value)
+ addresses_field.short_description = _("Addresses")
+ addresses_field.allow_tags = True
+
+
+class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
+ list_display = (
+ 'email', 'domain_link', 'display_mailboxes', 'display_forward', 'account_link'
+ )
+ list_filter = (HasMailboxListFilter, HasForwardListFilter)
+ fields = ('account_link', ('name', 'domain'), 'mailboxes', 'forward')
+ inlines = [AutoresponseInline]
+ search_fields = ('name', 'domain__name',)
+ readonly_fields = ('account_link', 'domain_link', 'email_link')
+ filter_by_account_fields = ('domain', 'mailboxes')
+ filter_horizontal = ['mailboxes']
+
+ domain_link = admin_link('domain', order='domain__name')
+
+ def email_link(self, address):
+ link = self.domain_link(address)
+ return "%s@%s" % (address.name, link)
+ email_link.short_description = _("Email")
+ email_link.allow_tags = True
+
+ def display_mailboxes(self, address):
+ boxes = []
+ for mailbox in address.mailboxes.all():
+ url = reverse('admin:mails_mailbox_change', args=(mailbox.pk,))
+ boxes.append('%s' % (url, mailbox.name))
+ return '
'.join(boxes)
+ display_mailboxes.short_description = _("Mailboxes")
+ display_mailboxes.allow_tags = True
+
+ def display_forward(self, address):
+ values = [ dest for dest in address.forward.split() ]
+ return '
'.join(values)
+ display_forward.short_description = _("Forward")
+ display_forward.allow_tags = True
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ if db_field.name == 'forward':
+ kwargs['widget'] = forms.TextInput(attrs={'size':'118'})
+ return super(AddressAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+
+ def get_queryset(self, request):
+ """ Select related for performance """
+ qs = super(AddressAdmin, self).get_queryset(request)
+ return qs.select_related('domain')
+
+
+admin.site.register(Mailbox, MailboxAdmin)
+admin.site.register(Address, AddressAdmin)
diff --git a/orchestra/apps/mails/api.py b/orchestra/apps/mails/api.py
new file mode 100644
index 00000000..24baeeca
--- /dev/null
+++ b/orchestra/apps/mails/api.py
@@ -0,0 +1,22 @@
+from rest_framework import viewsets
+
+from orchestra.api import router
+from orchestra.apps.accounts.api import AccountApiMixin
+
+from .models import Address, Mailbox
+from .serializers import AddressSerializer, MailboxSerializer
+
+
+class AddressViewSet(AccountApiMixin, viewsets.ModelViewSet):
+ model = Address
+ serializer_class = AddressSerializer
+
+
+
+class MailboxViewSet(AccountApiMixin, viewsets.ModelViewSet):
+ model = Mailbox
+ serializer_class = MailboxSerializer
+
+
+router.register(r'mailboxes', MailboxViewSet)
+router.register(r'addresses', AddressViewSet)
diff --git a/orchestra/apps/mails/backends.py b/orchestra/apps/mails/backends.py
new file mode 100644
index 00000000..c1283d22
--- /dev/null
+++ b/orchestra/apps/mails/backends.py
@@ -0,0 +1,159 @@
+import os
+
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+
+from orchestra.apps.orchestration import ServiceController
+from orchestra.apps.resources import ServiceMonitor
+
+from . import settings
+
+
+class MailSystemUserBackend(ServiceController):
+ verbose_name = _("Mail system user")
+ model = 'mail.Mailbox'
+ # TODO related_models = ('resources__content_type') ??
+
+ DEFAULT_GROUP = 'postfix'
+
+ def create_user(self, context):
+ self.append(
+ "if [[ $( id %(username)s ) ]]; then \n"
+ " usermod -p '%(password)s' %(username)s \n"
+ "else \n"
+ " useradd %(username)s --password '%(password)s' \\\n"
+ " --shell /dev/null \n"
+ "fi" % context
+ )
+ self.append("mkdir -p %(home)s" % context)
+ self.append("chown %(username)s.%(group)s %(home)s" % context)
+
+ def generate_filter(self, mailbox, context):
+ now = timezone.now().strftime("%B %d, %Y, %H:%M")
+ context['filtering'] = (
+ "# Sieve Filter\n"
+ "# Generated by Orchestra %s\n\n" % now
+ )
+ if mailbox.use_custom_filtering:
+ context['filtering'] += mailbox.custom_filtering
+ else:
+ context['filtering'] += settings.EMAILS_DEFAUL_FILTERING
+ context['filter_path'] = os.path.join(context['home'], '.orchestra.sieve')
+ self.append("echo '%(filtering)s' > %(filter_path)s" % context)
+
+ def save(self, mailbox):
+ context = self.get_context(mailbox)
+ self.create_user(context)
+ self.generate_filter(mailbox, context)
+
+ def delete(self, mailbox):
+ context = self.get_context(mailbox)
+ self.append("{ sleep 2 && killall -u %(username)s -s KILL; } &" % context)
+ self.append("killall -u %(username)s" % context)
+ self.append("userdel %(username)s" % context)
+ self.append("rm -fr %(home)s" % context)
+
+ def get_context(self, mailbox):
+ user = mailbox.user
+ context = {
+ 'username': user.username,
+ 'password': user.password if user.is_active else '*%s' % user.password,
+ 'group': self.DEFAULT_GROUP
+ }
+ context['home'] = settings.EMAILS_HOME % context
+ return context
+
+
+class PostfixAddressBackend(ServiceController):
+ verbose_name = _("Postfix address")
+ model = 'mail.Address'
+
+ def include_virtdomain(self, context):
+ self.append(
+ '[[ $(grep "^\s*%(domain)s\s*$" %(virtdomains)s) ]]'
+ ' || { echo "%(domain)s" >> %(virtdomains)s; UPDATED=1; }' % context
+ )
+
+ def exclude_virtdomain(self, context):
+ domain = context['domain']
+ if not Address.objects.filter(domain=domain).exists():
+ self.append('sed -i "s/^%(domain)s//" %(virtdomains)s' % context)
+
+ def update_virtusertable(self, context):
+ self.append(
+ 'LINE="%(email)s\t%(destination)s"\n'
+ 'if [[ ! $(grep "^%(email)s\s" %(virtusertable)s) ]]; then\n'
+ ' echo "$LINE" >> %(virtusertable)s\n'
+ ' UPDATED=1\n'
+ 'else\n'
+ ' if [[ ! $(grep "^${LINE}$" %(virtusertable)s) ]]; then\n'
+ ' sed -i "s/^%(email)s\s.*$/${LINE}/" %(virtusertable)s\n'
+ ' UPDATED=1\n'
+ ' fi\n'
+ 'fi' % context
+ )
+
+ def exclude_virtusertable(self, context):
+ self.append(
+ 'if [[ $(grep "^%(email)s\s") ]]; then\n'
+ ' sed -i "s/^%(email)s\s.*$//" %(virtusertable)s\n'
+ ' UPDATED=1\n'
+ 'fi'
+ )
+
+ def save(self, address):
+ context = self.get_context(address)
+ self.include_virtdomain(context)
+ self.update_virtusertable(context)
+
+ def delete(self, address):
+ context = self.get_context(address)
+ self.exclude_virtdomain(context)
+ self.exclude_virtusertable(context)
+
+ def commit(self):
+ context = self.get_context_files()
+ self.append('[[ $UPDATED == 1 ]] && { '
+ 'postmap %(virtdomains)s;'
+ 'postmap %(virtusertable)s;'
+ '}' % context)
+
+ def get_context_files(self):
+ return {
+ 'virtdomains': settings.EMAILS_VIRTDOMAINS_PATH,
+ 'virtusertable': settings.EMAILS_VIRTUSERTABLE_PATH,
+ }
+
+ def get_context(self, address):
+ context = self.get_context_files()
+ context.update({
+ 'domain': address.domain,
+ 'email': address.email,
+ 'destination': address.destination,
+ })
+ return context
+
+
+class AutoresponseBackend(ServiceController):
+ verbose_name = _("Mail autoresponse")
+ model = 'mail.Autoresponse'
+
+
+class MaildirDisk(ServiceMonitor):
+ model = 'email.Mailbox'
+ resource = ServiceMonitor.DISK
+ verbose_name = _("Maildir disk usage")
+
+ def monitor(self, mailbox):
+ context = self.get_context(mailbox)
+ self.append(
+ "SIZE=$(sed -n '2p' %(maildir_path)s | cut -d' ' -f1)\n"
+ "echo %(object_id)s ${SIZE:-0}" % context
+ )
+
+ def get_context(self, mailbox):
+ context = MailSystemUserBackend().get_context(site)
+ context['home'] = settings.EMAILS_HOME % context
+ context['maildir_path'] = os.path.join(context['home'], 'Maildir/maildirsize')
+ context['object_id'] = mailbox.pk
+ return context
diff --git a/orchestra/apps/mails/filters.py b/orchestra/apps/mails/filters.py
new file mode 100644
index 00000000..9ea18f81
--- /dev/null
+++ b/orchestra/apps/mails/filters.py
@@ -0,0 +1,48 @@
+from django.contrib.admin import SimpleListFilter
+from django.utils.translation import ugettext_lazy as _
+
+
+class HasMailboxListFilter(SimpleListFilter):
+ """ Filter addresses whether they have any mailbox or not """
+ title = _("Has mailbox")
+ parameter_name = 'has_mailbox'
+
+ def lookups(self, request, model_admin):
+ return (
+ ('True', _("True")),
+ ('False', _("False")),
+ )
+
+ def queryset(self, request, queryset):
+ if self.value() == 'True':
+ return queryset.filter(mailboxes__isnull=False)
+ elif self.value() == 'False':
+ return queryset.filter(mailboxes__isnull=True)
+ return queryset
+
+
+class HasForwardListFilter(HasMailboxListFilter):
+ """ Filter addresses whether they have any mailbox or not """
+ title = _("Has forward")
+ parameter_name = 'has_forward'
+
+ def queryset(self, request, queryset):
+ if self.value() == 'True':
+ return queryset.exclude(forward='')
+ elif self.value() == 'False':
+ return queryset.filter(forward='')
+ return queryset
+
+
+class HasAddressListFilter(HasMailboxListFilter):
+ """ Filter addresses whether they have any mailbox or not """
+ title = _("Has address")
+ parameter_name = 'has_address'
+
+ def queryset(self, request, queryset):
+ if self.value() == 'True':
+ return queryset.filter(addresses__isnull=False)
+ elif self.value() == 'False':
+ return queryset.filter(addresses__isnull=True)
+ return queryset
+
diff --git a/orchestra/apps/mails/models.py b/orchestra/apps/mails/models.py
index 3fcfe7c6..78637dad 100644
--- a/orchestra/apps/mails/models.py
+++ b/orchestra/apps/mails/models.py
@@ -15,62 +15,23 @@ class Mailbox(models.Model):
help_text=_("Required. 30 characters or fewer. Letters, digits and "
"@/./+/-/_ only."),
validators=[RegexValidator(r'^[\w.@+-]+$',
- _("Enter a valid username."), 'invalid')])
+ _("Enter a valid mailbox name."), 'invalid')])
+ account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
+ related_name='mailboxes')
use_custom_filtering = models.BooleanField(_("Use custom filtering"),
default=False)
custom_filtering = models.TextField(_("filtering"), blank=True,
validators=[validators.validate_sieve],
help_text=_("Arbitrary email filtering in sieve language."))
+# addresses = models.ManyToManyField('mails.Address',
+# verbose_name=_("addresses"),
+# related_name='mailboxes', blank=True)
class Meta:
verbose_name_plural = _("mailboxes")
def __unicode__(self):
- return self.user.username
-
-# def get_addresses(self):
-# regex = r'(^|\s)+%s(\s|$)+' % self.user.username
-# return Address.objects.filter(destination__regex=regex)
-#
-# def delete(self, *args, **kwargs):
-# """ Update related addresses """
-# regex = re.compile(r'(^|\s)+(\s*%s)(\s|$)+' % self.user.username)
-# super(Mailbox, self).delete(*args, **kwargs)
-# for address in self.get_addresses():
-# address.destination = regex.sub(r'\3', address.destination).strip()
-# if not address.destination:
-# address.delete()
-# else:
-# address.save()
-
-
-#class Address(models.Model):
-# name = models.CharField(_("name"), max_length=64,
-# validators=[validators.validate_emailname])
-# domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
-# verbose_name=_("domain"),
-# related_name='addresses')
-# destination = models.CharField(_("destination"), max_length=256,
-# validators=[validators.validate_destination],
-# help_text=_("Space separated mailbox names or email addresses"))
-# account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
-# related_name='addresses')
-#
-# class Meta:
-# verbose_name_plural = _("addresses")
-# unique_together = ('name', 'domain')
-#
-# def __unicode__(self):
-# return self.email
-#
-# @property
-# def email(self):
-# return "%s@%s" % (self.name, self.domain)
-#
-# def get_mailboxes(self):
-# for dest in self.destination.split():
-# if '@' not in dest:
-# yield Mailbox.objects.select_related('user').get(user__username=dest)
+ return self.name
class Address(models.Model):
@@ -79,7 +40,8 @@ class Address(models.Model):
domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
verbose_name=_("domain"),
related_name='addresses')
- mailboxes = models.ManyToManyField('mail.Mailbox', verbose_name=_("mailboxes"),
+ mailboxes = models.ManyToManyField(Mailbox,
+ verbose_name=_("mailboxes"),
related_name='addresses', blank=True)
forward = models.CharField(_("forward"), max_length=256, blank=True,
validators=[validators.validate_forward])
@@ -110,4 +72,5 @@ class Autoresponse(models.Model):
return self.address
+services.register(Mailbox)
services.register(Address)
diff --git a/orchestra/apps/mails/serializers.py b/orchestra/apps/mails/serializers.py
new file mode 100644
index 00000000..7dfe3e34
--- /dev/null
+++ b/orchestra/apps/mails/serializers.py
@@ -0,0 +1,27 @@
+from rest_framework import serializers
+
+from orchestra.apps.accounts.serializers import AccountSerializerMixin
+
+from .models import Mailbox, Address
+
+
+class MailboxSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = Mailbox
+ fields = ('url', 'name', 'use_custom_filtering', 'custom_filtering', 'addresses')
+
+
+class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = Address
+ fields = ('url', 'name', 'domain', 'mailboxes', 'forward')
+
+ def get_fields(self, *args, **kwargs):
+ fields = super(AddressSerializer, self).get_fields(*args, **kwargs)
+ account = self.context['view'].request.user.account_id
+ mailboxes = fields['mailboxes'].queryset
+ fields['mailboxes'].queryset = mailboxes.filter(account=account)
+ # TODO do it on permissions or in self.filter_by_account_field ?
+ domain = fields['domain'].queryset
+ fields['domain'].queryset = domain .filter(account=account)
+ return fields
diff --git a/orchestra/apps/mails/settings.py b/orchestra/apps/mails/settings.py
new file mode 100644
index 00000000..30473edb
--- /dev/null
+++ b/orchestra/apps/mails/settings.py
@@ -0,0 +1,29 @@
+from django.conf import settings
+
+
+EMAILS_DOMAIN_MODEL = getattr(settings, 'EMAILS_DOMAIN_MODEL', 'domains.Domain')
+
+EMAILS_HOME = getattr(settings, 'EMAILS_HOME', '/var/vmail/%(account)s/%(name)s/')
+
+EMAILS_SIEVETEST_PATH = getattr(settings, 'EMAILS_SIEVETEST_PATH', '/dev/shm')
+
+EMAILS_SIEVETEST_BIN_PATH = getattr(settings, 'EMAILS_SIEVETEST_BIN_PATH',
+ '%(orchestra_root)s/bin/sieve-test')
+
+
+EMAILS_VIRTUSERTABLE_PATH = getattr(settings, 'EMAILS_VIRTUSERTABLE_PATH',
+ '/etc/postfix/virtusertable')
+
+
+EMAILS_VIRTDOMAINS_PATH = getattr(settings, 'EMAILS_VIRTDOMAINS_PATH',
+ '/etc/postfix/virtdomains')
+
+
+EMAILS_DEFAUL_FILTERING = getattr(settings, 'EMAILS_DEFAULT_FILTERING',
+ 'require ["fileinto","regex","envelope","vacation","reject","relational","comparator-i;ascii-numeric"];\n'
+ '\n'
+ 'if header :value "ge" :comparator "i;ascii-numeric" "X-Spam-Score" "5" {\n'
+ ' fileinto "Junk";\n'
+ ' discard;\n'
+ '}'
+)
diff --git a/orchestra/apps/mails/validators.py b/orchestra/apps/mails/validators.py
new file mode 100644
index 00000000..55d241a4
--- /dev/null
+++ b/orchestra/apps/mails/validators.py
@@ -0,0 +1,63 @@
+import hashlib
+import os
+import re
+
+from django.core.validators import ValidationError, EmailValidator
+from django.utils.translation import ugettext_lazy as _
+
+from orchestra.utils import paths
+from orchestra.utils.system import run
+
+from . import settings
+
+
+def validate_emailname(value):
+ msg = _("'%s' is not a correct email name" % value)
+ if '@' in value:
+ raise ValidationError(msg)
+ value += '@localhost'
+ try:
+ EmailValidator(value)
+ except ValidationError:
+ raise ValidationError(msg)
+
+
+#def validate_destination(value):
+# """ space separated mailboxes or emails """
+# for destination in value.split():
+# msg = _("'%s' is not an existent mailbox" % destination)
+# if '@' in destination:
+# if not destination[-1].isalpha():
+# raise ValidationError(msg)
+# EmailValidator(destination)
+# else:
+# from .models import Mailbox
+# if not Mailbox.objects.filter(user__username=destination).exists():
+# raise ValidationError(msg)
+# validate_emailname(destination)
+
+
+def validate_forward(value):
+ """ space separated mailboxes or emails """
+ for destination in value.split():
+ EmailValidator(destination)
+
+
+def validate_sieve(value):
+ from .models import Mailbox
+ sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
+ path = os.path.join(settings.EMAILS_SIEVETEST_PATH, sieve_name)
+ with open(path, 'wb') as f:
+ f.write(value)
+ context = {
+ 'orchestra_root': paths.get_orchestra_root()
+ }
+ sievetest = settings.EMAILS_SIEVETEST_BIN_PATH % context
+ test = run(' '.join([sievetest, path, '/dev/null']), display=False)
+ if test.return_code:
+ errors = []
+ for line in test.stderr.splitlines():
+ error = re.match(r'^.*(line\s+[0-9]+:.*)', line)
+ if error:
+ errors += error.groups()
+ raise ValidationError(' '.join(errors))
diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py
index 940bdec3..46e2487e 100644
--- a/orchestra/apps/orders/admin.py
+++ b/orchestra/apps/orders/admin.py
@@ -91,6 +91,11 @@ class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
content_object_link = admin_link('content_object', order=False)
display_registered_on = admin_date('registered_on')
display_cancelled_on = admin_date('cancelled_on')
+
+ def get_queryset(self, request):
+ qs = super(OrderAdmin, self).get_queryset(request)
+ return qs.select_related('service').prefetch_related('content_object')
+
class MetricStorageAdmin(admin.ModelAdmin):
diff --git a/orchestra/apps/orders/filters.py b/orchestra/apps/orders/filters.py
index f82e54a6..b02f51f5 100644
--- a/orchestra/apps/orders/filters.py
+++ b/orchestra/apps/orders/filters.py
@@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _
class ActiveOrderListFilter(SimpleListFilter):
""" Filter tickets by created_by according to request.user """
- title = 'Orders'
+ title = _("Orders")
parameter_name = 'is_active'
def lookups(self, request, model_admin):
diff --git a/orchestra/apps/users/admin.py b/orchestra/apps/users/admin.py
index 7e17485b..e0737eab 100644
--- a/orchestra/apps/users/admin.py
+++ b/orchestra/apps/users/admin.py
@@ -45,6 +45,7 @@ class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
form = UserChangeForm
roles = []
ordering = ('-id',)
+ change_form_template = 'admin/users/user/change_form.html'
def display_is_main(self, instance):
return instance.is_main
diff --git a/orchestra/apps/users/models.py b/orchestra/apps/users/models.py
index a8d4a0d9..41a1b886 100644
--- a/orchestra/apps/users/models.py
+++ b/orchestra/apps/users/models.py
@@ -10,8 +10,8 @@ from orchestra.core import services
class User(auth.AbstractBaseUser):
username = models.CharField(_("username"), max_length=64, unique=True,
help_text=_("Required. 30 characters or fewer. Letters, digits and "
- "@/./+/-/_ only."),
- validators=[validators.RegexValidator(r'^[\w.@+-]+$',
+ "./-/_ only."),
+ validators=[validators.RegexValidator(r'^[\w.-]+$',
_("Enter a valid username."), 'invalid')])
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='users', null=True)
diff --git a/orchestra/apps/users/roles/__init__.py b/orchestra/apps/users/roles/__init__.py
index e69de29b..b27353d3 100644
--- a/orchestra/apps/users/roles/__init__.py
+++ b/orchestra/apps/users/roles/__init__.py
@@ -0,0 +1,29 @@
+from django.db import models
+
+from ..models import User
+
+
+class Register(object):
+ def __init__(self):
+ self._registry = {}
+
+ def __contains__(self, key):
+ return key in self._registry
+
+ def register(self, name, model):
+ if name in self._registry:
+ raise KeyError("%s already registered" % name)
+ def has_role(user):
+ try:
+ getattr(user, name)
+ except models.DoesNotExist:
+ return False
+ return True
+ setattr(User, 'has_%s' % name, has_role)
+ self._registry[name] = model
+
+ def get(self):
+ return self._registry
+
+
+roles = Register()
diff --git a/orchestra/apps/users/roles/jabber/models.py b/orchestra/apps/users/roles/jabber/models.py
index a82bc429..32850579 100644
--- a/orchestra/apps/users/roles/jabber/models.py
+++ b/orchestra/apps/users/roles/jabber/models.py
@@ -1,6 +1,8 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
+from .. import roles
+
class Jabber(models.Model):
user = models.OneToOneField('users.User', verbose_name=_("user"),
@@ -8,3 +10,6 @@ class Jabber(models.Model):
def __unicode__(self):
return str(self.user)
+
+
+roles.register('jabber', Jabber)
diff --git a/orchestra/apps/users/roles/mail/forms.py b/orchestra/apps/users/roles/mail/forms.py
index 7f1023ff..7066e5d0 100644
--- a/orchestra/apps/users/roles/mail/forms.py
+++ b/orchestra/apps/users/roles/mail/forms.py
@@ -36,7 +36,7 @@ class MailRoleAdminForm(RoleAdminBaseForm):
# value += '%s' % (url, name)
# value = '' % value
# return mark_safe('%s
' % value)
-
+
def addresses(self, mailbox):
account = mailbox.user.account
add_url = reverse('admin:mail_address_add')
diff --git a/orchestra/apps/users/roles/mail/models.py b/orchestra/apps/users/roles/mail/models.py
index 858325f9..bd03a807 100644
--- a/orchestra/apps/users/roles/mail/models.py
+++ b/orchestra/apps/users/roles/mail/models.py
@@ -7,6 +7,8 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.core import services
+from .. import roles
+
from . import validators, settings
@@ -76,7 +78,8 @@ class Address(models.Model):
domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
verbose_name=_("domain"),
related_name='addresses')
- mailboxes = models.ManyToManyField('mail.Mailbox', verbose_name=_("mailboxes"),
+ mailboxes = models.ManyToManyField('mail.Mailbox',
+ verbose_name=_("mailboxes"),
related_name='addresses', blank=True)
forward = models.CharField(_("forward"), max_length=256, blank=True,
validators=[validators.validate_forward])
@@ -108,3 +111,4 @@ class Autoresponse(models.Model):
services.register(Address)
+roles.register('mailbox', Mailbox)
diff --git a/orchestra/apps/users/roles/posix/models.py b/orchestra/apps/users/roles/posix/models.py
index 2164d4cd..a9a4590a 100644
--- a/orchestra/apps/users/roles/posix/models.py
+++ b/orchestra/apps/users/roles/posix/models.py
@@ -1,6 +1,8 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
+from .. import roles
+
from . import settings
@@ -14,3 +16,6 @@ class POSIX(models.Model):
def __unicode__(self):
return str(self.user)
+
+
+roles.register('posix', POSIX)
diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py
index a214faf4..b06f802e 100644
--- a/orchestra/conf/base_settings.py
+++ b/orchestra/conf/base_settings.py
@@ -68,9 +68,10 @@ INSTALLED_APPS = (
'orchestra.apps.orchestration',
'orchestra.apps.domains',
'orchestra.apps.users',
- 'orchestra.apps.users.roles.mail',
+# 'orchestra.apps.users.roles.mail',
'orchestra.apps.users.roles.jabber',
'orchestra.apps.users.roles.posix',
+ 'orchestra.apps.mails',
'orchestra.apps.lists',
'orchestra.apps.webapps',
'orchestra.apps.websites',
@@ -168,8 +169,9 @@ FLUENT_DASHBOARD_APP_GROUPS = (
FLUENT_DASHBOARD_APP_ICONS = {
# Services
'webs/web': 'web.png',
- 'mail/mailbox': 'email.png',
'mail/address': 'X-office-address-book.png',
+ 'mails/mailbox': 'email.png',
+ 'mails/address': 'X-office-address-book.png',
'lists/list': 'email-alter.png',
'domains/domain': 'domain.png',
'multitenance/tenant': 'apps.png',