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 = 'Add Another' + 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',