diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index 78da86e1..eb95f376 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -252,14 +252,8 @@ class ChangePasswordAdminMixin(object): related.append(user.account) else: account = user - # TODO plugability - if user._meta.model_name != 'systemuser': - rel = account.systemusers.filter(username=username).first() - if rel: - related.append(rel) - if user._meta.model_name != 'mailbox': - rel = account.mailboxes.filter(name=username).first() - if rel: + for rel in account.get_related_passwords(): + if not isinstance(user, type(rel)): related.append(rel) if request.method == 'POST': diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py index 75aecb0b..a3f261bb 100644 --- a/orchestra/apps/accounts/admin.py +++ b/orchestra/apps/accounts/admin.py @@ -1,8 +1,12 @@ +import copy +import re + from django import forms from django.conf.urls import patterns, url from django.contrib import admin, messages from django.contrib.admin.util import unquote from django.contrib.auth import admin as auth +from django.db.models.loading import get_model from django.http import HttpResponseRedirect from django.utils.safestring import mark_safe from django.utils.six.moves.urllib.parse import parse_qsl @@ -13,6 +17,7 @@ from orchestra.admin.utils import wrap_admin_view, admin_link, set_url_query, ch from orchestra.core import services, accounts from orchestra.forms import UserChangeForm +from . import settings from .actions import disable from .filters import HasMainUserListFilter from .forms import AccountCreationForm @@ -84,11 +89,26 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) return super(AccountAdmin, self).change_view(request, object_id, form_url=form_url, extra_context=context) + def get_fieldsets(self, request, obj=None): + fieldsets = super(AccountAdmin, self).get_fieldsets(request, obj=obj) + if not obj: + fields = AccountCreationForm.create_related_fields + if fields: + fieldsets = copy.deepcopy(fieldsets) + fieldsets = list(fieldsets) + fieldsets.insert(1, (_("Related services"), {'fields': fields})) + return fieldsets + def get_queryset(self, request): """ Select related for performance """ qs = super(AccountAdmin, self).get_queryset(request) related = ('invoicecontact',) return qs.select_related(*related) + + def save_model(self, request, obj, form, change): + super(AccountAdmin, self).save_model(request, obj, form, change) + if not change: + form.save_related(obj) admin.site.register(Account, AccountAdmin) @@ -162,6 +182,7 @@ class AccountAdminMixin(object): def render(*args, **kwargs): output = old_render(*args, **kwargs) output = output.replace('/add/"', '/add/?account=%s"' % self.account.pk) + output = re.sub(r'/add/\?([^".]*)"', r'/add/?\1&account=%s"' % self.account.pk, output) return mark_safe(output) formfield.widget.render = render # Filter related object by account diff --git a/orchestra/apps/accounts/forms.py b/orchestra/apps/accounts/forms.py index 0a012e19..076d68eb 100644 --- a/orchestra/apps/accounts/forms.py +++ b/orchestra/apps/accounts/forms.py @@ -1,21 +1,60 @@ from django import forms -from django.contrib import auth +from django.db.models.loading import get_model from django.utils.translation import ugettext_lazy as _ -from orchestra.core.validators import validate_password from orchestra.forms import UserCreationForm -from orchestra.forms.widgets import ReadOnlyWidget + +from . import settings +from .models import Account +def create_account_creation_form(): + fields = {} + for model, key, kwargs, help_text in settings.ACCOUNTS_CREATE_RELATED: + model = get_model(model) + field_name = 'create_%s' % model._meta.model_name + label = _("Create related %s") % model._meta.verbose_name + fields[field_name] = forms.BooleanField(initial=True, required=False, label=label, + help_text=help_text) + + def clean(self): + """ unique usernames between accounts and system users """ + cleaned_data = UserCreationForm.clean(self) + try: + account = Account( + username=cleaned_data['username'], + password=cleaned_data['password1'] + ) + except KeyError: + # Previous validation error + return + for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED: + model = get_model(model) + kwargs = { + key: eval(related_kwargs[key], {'account': account}) + } + if model.objects.filter(**kwargs).exists(): + verbose_name = model._meta.verbose_name + raise forms.ValidationError( + _("A %s with this name already exists") % verbose_name + ) + + def save_related(self, account): + for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED: + model = get_model(model) + field_name = 'create_%s' % model._meta.model_name + if self.cleaned_data[field_name]: + for key, value in related_kwargs.iteritems(): + related_kwargs[key] = eval(value, {'account': account}) + model.objects.create(account=account, **related_kwargs) + + fields.update({ + 'create_related_fields': fields.keys(), + 'clean': clean, + 'save_related': save_related, + }) + + return type('AccountCreationForm', (UserCreationForm,), fields) -class AccountCreationForm(UserCreationForm): - def clean_username(self): - # Since model.clean() will check this, this is redundant, - # but it sets a nicer error message than the ORM and avoids conflicts with contrib.auth - username = self.cleaned_data["username"] - account_model = self._meta.model - if hasattr(account_model, 'systemusers'): - systemuser_model = account_model.systemusers.related.model - if systemuser_model.objects.filter(username=username).exists(): - raise forms.ValidationError(self.error_messages['duplicate_username']) - return username + +AccountCreationForm = create_account_creation_form() diff --git a/orchestra/apps/accounts/models.py b/orchestra/apps/accounts/models.py index 446135aa..ba9abbec 100644 --- a/orchestra/apps/accounts/models.py +++ b/orchestra/apps/accounts/models.py @@ -2,6 +2,7 @@ from django.contrib.auth import models as auth from django.conf import settings as djsettings from django.core import validators from django.db import models +from django.db.models.loading import get_model from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -57,19 +58,6 @@ class Account(auth.AbstractBaseUser): def get_main(cls): return cls.objects.get(pk=settings.ACCOUNTS_MAIN_PK) - def clean(self): - """ unique usernames between accounts and system users """ - if not self.pk and hasattr(self, 'systemusers'): - if self.systemusers.model.objects.filter(username=self.username).exists(): - raise validators.ValidationError(_("A user with this name already exists")) - - def save(self, *args, **kwargs): - created = not self.pk - super(Account, self).save(*args, **kwargs) - if created and hasattr(self, 'systemusers'): - self.systemusers.create(username=self.username, account=self, - password=self.password, is_main=True) - def disable(self): self.is_active = False self.save(update_fields=['is_active']) @@ -133,5 +121,21 @@ class Account(auth.AbstractBaseUser): return True return auth._user_has_module_perms(self, app_label) + def get_related_passwords(self): + related = [] + for model, key, kwargs, __ in settings.ACCOUNTS_CREATE_RELATED: + if 'password' not in kwargs: + continue + model = get_model(model) + kwargs = { + key: eval(kwargs[key], {'account': self}) + } + try: + rel = model.objects.get(account=self, **kwargs) + except model.DoesNotExist: + continue + related.append(rel) + return related + services.register(Account, menu=False) diff --git a/orchestra/apps/accounts/settings.py b/orchestra/apps/accounts/settings.py index 9e52f3c9..aff8f945 100644 --- a/orchestra/apps/accounts/settings.py +++ b/orchestra/apps/accounts/settings.py @@ -22,3 +22,32 @@ ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'en') ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1) + + +ACCOUNTS_CREATE_RELATED = getattr(settings, 'ACCOUNTS_CREATE_RELATED', ( + # , , , + ('systemusers.SystemUser', + 'username', + { + 'username': 'account.username', + 'password': 'account.password', + 'is_main': 'True', + }, + _("Designates whether to creates a related system users with the same username and password or not."), + ), + ('mailboxes.Mailbox', + 'name', + { + 'name': 'account.username', + 'password': 'account.password', + }, + _("Designates whether to creates a related mailbox with the same name and password or not."), + ), + ('domains.Domain', + 'name', + { + 'name': '"%s.orchestra.lan" % account.username' + }, + _("Designates whether to creates a related subdomain <username>.orchestra.lan or not."), + ), +)) diff --git a/orchestra/apps/bills/helpers.py b/orchestra/apps/bills/helpers.py index cc0619fb..cd802605 100644 --- a/orchestra/apps/bills/helpers.py +++ b/orchestra/apps/bills/helpers.py @@ -11,14 +11,14 @@ def validate_contact(request, bill, error=True): 'You should provide one') valid = True send = messages.error if error else messages.warning - if not hasattr(bill.account, 'invoicecontact'): + if not hasattr(bill.account, 'billcontact'): account = force_text(bill.account) url = reverse('admin:accounts_account_change', args=(bill.account_id,)) message = msg.format(relation=_("Related"), account=account, url=url) send(request, mark_safe(message)) valid = False main = type(bill).account.field.rel.to.get_main() - if not hasattr(main, 'invoicecontact'): + if not hasattr(main, 'billcontact'): account = force_text(main) url = reverse('admin:accounts_account_change', args=(main.id,)) message = msg.format(relation=_("Main"), account=account, url=url) diff --git a/orchestra/apps/databases/admin.py b/orchestra/apps/databases/admin.py index bfcc81a3..65044492 100644 --- a/orchestra/apps/databases/admin.py +++ b/orchestra/apps/databases/admin.py @@ -5,7 +5,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin -from orchestra.admin.utils import admin_link +from orchestra.admin.utils import admin_link, change_url from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin from .forms import DatabaseCreationForm, DatabaseUserChangeForm, DatabaseUserCreationForm @@ -13,7 +13,7 @@ from .models import Database, DatabaseUser class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): - list_display = ('name', 'type', 'account_link') + list_display = ('name', 'type', 'display_users', 'account_link') list_filter = ('type',) search_fields = ['name'] change_readonly_fields = ('name', 'type') @@ -21,7 +21,7 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): fieldsets = ( (None, { 'classes': ('extrapretty',), - 'fields': ('account_link', 'name', 'type', 'users'), + 'fields': ('account_link', 'name', 'type', 'users', 'display_users'), }), ) add_fieldsets = ( @@ -39,6 +39,18 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): }), ) add_form = DatabaseCreationForm + readonly_fields = ('account_link', 'display_users',) + filter_horizontal = ['users'] + + def display_users(self, db): + links = [] + for user in db.users.all(): + link = '%s' % (change_url(user), user.username) + links.append(link) + return ', '.join(links) + display_users.short_description = _("Users") + display_users.allow_tags = True + display_users.admin_order_field = 'users__username' def save_model(self, request, obj, form, change): super(DatabaseAdmin, self).save_model(request, obj, form, change) @@ -56,7 +68,7 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, ExtendedModelAdmin): - list_display = ('username', 'type', 'account_link') + list_display = ('username', 'type', 'display_databases', 'account_link') list_filter = ('type',) search_fields = ['username'] form = DatabaseUserChangeForm @@ -65,7 +77,7 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten fieldsets = ( (None, { 'classes': ('extrapretty',), - 'fields': ('account_link', 'username', 'password', 'type') + 'fields': ('account_link', 'username', 'password', 'type', 'display_databases') }), ) add_fieldsets = ( @@ -74,6 +86,17 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten 'fields': ('account_link', 'username', 'password1', 'password2', 'type') }), ) + readonly_fields = ('account_link', 'display_databases',) + + def display_databases(self, user): + links = [] + for db in user.databases.all(): + link = '%s' % (change_url(db), db.name) + links.append(link) + return ', '.join(links) + display_databases.short_description = _("Databases") + display_databases.allow_tags = True + display_databases.admin_order_field = 'databases__name' def get_urls(self): useradmin = UserAdmin(DatabaseUser, self.admin_site) diff --git a/orchestra/apps/domains/backends.py b/orchestra/apps/domains/backends.py index acd1eb42..282953a1 100644 --- a/orchestra/apps/domains/backends.py +++ b/orchestra/apps/domains/backends.py @@ -39,7 +39,7 @@ class Bind9MasterDomainBackend(ServiceController): def update_conf(self, context): self.append(textwrap.dedent("""\ sed '/zone "%(name)s".*/,/^\s*};\s*$/!d' %(conf_path)s | diff -B -I"^\s*//" - <(echo '%(conf)s') || { - sed -i -e '/zone "%(name)s".*/,/^\s*};/d' \\ + sed -i -e '/zone\s\s*"%(name)s".*/,/^\s*};/d' \\ -e 'N; /^\\n$/d; P; D' %(conf_path)s echo '%(conf)s' >> %(conf_path)s UPDATED=1 @@ -47,7 +47,7 @@ class Bind9MasterDomainBackend(ServiceController): )) # Delete ex-top-domains that are now subdomains self.append(textwrap.dedent("""\ - sed -i -e '/zone ".*\.%(name)s".*/,/^\s*};\s*$/d' \\ + sed -i -e '/zone\s\s*".*\.%(name)s".*/,/^\s*};\s*$/d' \\ -e 'N; /^\\n$/d; P; D' %(conf_path)s""" % context )) if 'zone_path' in context: @@ -64,7 +64,7 @@ class Bind9MasterDomainBackend(ServiceController): # These can never be top level domains return self.append(textwrap.dedent("""\ - sed -e '/zone ".*\.%(name)s".*/,/^\s*};\s*$/d' \\ + sed -e '/zone\s\s*"%(name)s".*/,/^\s*};\s*$/d' \\ -e 'N; /^\\n$/d; P; D' %(conf_path)s > %(conf_path)s.tmp""" % context )) self.append('diff -B -I"^\s*//" %(conf_path)s.tmp %(conf_path)s || UPDATED=1' % context) diff --git a/orchestra/apps/domains/models.py b/orchestra/apps/domains/models.py index 4e1cebca..01171103 100644 --- a/orchestra/apps/domains/models.py +++ b/orchestra/apps/domains/models.py @@ -21,6 +21,17 @@ class Domain(models.Model): def __unicode__(self): return self.name + @classmethod + def get_top_domain(cls, name): + split = name.split('.') + top = None + for i in range(1, len(split)-1): + name = '.'.join(split[i:]) + domain = Domain.objects.filter(name=name) + if domain: + top = domain.get() + return top + @property def origin(self): return self.top or self @@ -39,14 +50,7 @@ class Domain(models.Model): return self.origin.subdomains.all() def get_top(self): - split = self.name.split('.') - top = None - for i in range(1, len(split)-1): - name = '.'.join(split[i:]) - domain = Domain.objects.filter(name=name) - if domain: - top = domain.get() - return top + return type(self).get_top_domain(self.name) def render_zone(self): origin = self.origin diff --git a/orchestra/apps/domains/serializers.py b/orchestra/apps/domains/serializers.py index 740882bf..c1fd69f8 100644 --- a/orchestra/apps/domains/serializers.py +++ b/orchestra/apps/domains/serializers.py @@ -27,6 +27,14 @@ class DomainSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): fields = ('url', 'name', 'records') postonly_fields = ('name',) + def clean_name(self, attrs, source): + """ prevent users creating subdomains of other users domains """ + name = attrs[source] + top = Domain.get_top_domain(name) + if top and top.account != self.account: + raise ValidationError(_("Can not create subdomains of other users domains")) + return attrs + def full_clean(self, instance): """ Checks if everything is consistent """ instance = super(DomainSerializer, self).full_clean(instance) diff --git a/orchestra/apps/lists/backends.py b/orchestra/apps/lists/backends.py index b8f490d4..3c607f64 100644 --- a/orchestra/apps/lists/backends.py +++ b/orchestra/apps/lists/backends.py @@ -124,7 +124,7 @@ class MailmanBackend(ServiceController): 'name': mail_list.name, 'password': mail_list.password, 'domain': mail_list.address_domain or settings.LISTS_DEFAULT_DOMAIN, - 'address_name': mail_list.address_name, + 'address_name': mail_list.get_address_name, 'address_domain': mail_list.address_domain, 'admin': mail_list.admin_email, 'mailman_root': settings.LISTS_MAILMAN_ROOT_PATH, diff --git a/orchestra/apps/lists/forms.py b/orchestra/apps/lists/forms.py index a32457a4..dd37bd47 100644 --- a/orchestra/apps/lists/forms.py +++ b/orchestra/apps/lists/forms.py @@ -12,9 +12,6 @@ class CleanAddressMixin(object): if name and not domain: msg = _("Domain should be selected for provided address name") raise forms.ValidationError(msg) - elif not name and domain: - msg = _("Address name should be provided for this selected domain") - raise forms.ValidationError(msg) return domain diff --git a/orchestra/apps/lists/models.py b/orchestra/apps/lists/models.py index 7956bcab..1c0b69e4 100644 --- a/orchestra/apps/lists/models.py +++ b/orchestra/apps/lists/models.py @@ -35,6 +35,9 @@ class List(models.Model): return "%s@%s" % (self.address_name, self.address_domain) return '' + def get_address_name(self): + return self.address_name or self.name + def get_username(self): return self.name diff --git a/orchestra/apps/lists/serializers.py b/orchestra/apps/lists/serializers.py index 85e0e877..1fd7a143 100644 --- a/orchestra/apps/lists/serializers.py +++ b/orchestra/apps/lists/serializers.py @@ -40,15 +40,15 @@ class ListSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): raise serializers.ValidationError(_("Password required")) return attrs - def validate(self, attrs): - address_domain = attrs.get('address_domain') - address_name = attrs.get('address_name', ) + def validate_address_domain(self, attrs, source): + address_domain = attrs.get(source) + address_name = attrs.get('address_name') if self.object: address_domain = address_domain or self.object.address_domain address_name = address_name or self.object.address_name - if bool(address_domain) != bool(address_name): + if address_name and not address_domain: raise serializers.ValidationError( - _("address_name and address_domain should go in tandem")) + _("address_domains should should be provided when providing an addres_name")) return attrs def save_object(self, obj, **kwargs): diff --git a/orchestra/apps/webapps/admin.py b/orchestra/apps/webapps/admin.py index d08ab230..8eaca4af 100644 --- a/orchestra/apps/webapps/admin.py +++ b/orchestra/apps/webapps/admin.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import change_url -from orchestra.apps.accounts.admin import SelectAccountAdminMixin +from orchestra.apps.accounts.admin import AccountAdminMixin from .models import WebApp, WebAppOption @@ -25,10 +25,11 @@ class WebAppOptionInline(admin.TabularInline): return super(WebAppOptionInline, self).formfield_for_dbfield(db_field, **kwargs) -class WebAppAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): - fields = ('account_link', 'name', 'type') +class WebAppAdmin(AccountAdminMixin, ExtendedModelAdmin): list_display = ('name', 'type', 'display_websites', 'account_link') list_filter = ('type',) + add_fields = ('account', 'name', 'type') + fields = ('account_link', 'name', 'type') inlines = [WebAppOptionInline] readonly_fields = ('account_link',) change_readonly_fields = ('name', 'type') diff --git a/orchestra/core/validators.py b/orchestra/core/validators.py index aef8fc66..5dc363a9 100644 --- a/orchestra/core/validators.py +++ b/orchestra/core/validators.py @@ -41,7 +41,7 @@ def validate_name(value): """ A single non-empty line of free-form text with no whitespace. """ - validators.RegexValidator('^[\.\w]+$', + validators.RegexValidator('^[\.\w\-]+$', _("Enter a valid name (text without whitspaces)."), 'invalid')(value) diff --git a/orchestra/forms/options.py b/orchestra/forms/options.py index acfe64d7..b509763a 100644 --- a/orchestra/forms/options.py +++ b/orchestra/forms/options.py @@ -38,6 +38,7 @@ class UserCreationForm(forms.ModelForm): """ error_messages = { 'password_mismatch': _("The two password fields didn't match."), + 'duplicate_username': _("A user with that username already exists."), } password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput, validators=[validate_password])