Random fixes

This commit is contained in:
Marc 2014-10-20 15:51:24 +00:00
parent 04b9ee51cb
commit 0e65c65433
17 changed files with 190 additions and 66 deletions

View File

@ -252,14 +252,8 @@ class ChangePasswordAdminMixin(object):
related.append(user.account) related.append(user.account)
else: else:
account = user account = user
# TODO plugability for rel in account.get_related_passwords():
if user._meta.model_name != 'systemuser': if not isinstance(user, type(rel)):
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:
related.append(rel) related.append(rel)
if request.method == 'POST': if request.method == 'POST':

View File

@ -1,8 +1,12 @@
import copy
import re
from django import forms from django import forms
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.contrib import admin, messages from django.contrib import admin, messages
from django.contrib.admin.util import unquote from django.contrib.admin.util import unquote
from django.contrib.auth import admin as auth from django.contrib.auth import admin as auth
from django.db.models.loading import get_model
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.six.moves.urllib.parse import parse_qsl 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.core import services, accounts
from orchestra.forms import UserChangeForm from orchestra.forms import UserChangeForm
from . import settings
from .actions import disable from .actions import disable
from .filters import HasMainUserListFilter from .filters import HasMainUserListFilter
from .forms import AccountCreationForm from .forms import AccountCreationForm
@ -84,11 +89,26 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
return super(AccountAdmin, self).change_view(request, object_id, return super(AccountAdmin, self).change_view(request, object_id,
form_url=form_url, extra_context=context) 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): def get_queryset(self, request):
""" Select related for performance """ """ Select related for performance """
qs = super(AccountAdmin, self).get_queryset(request) qs = super(AccountAdmin, self).get_queryset(request)
related = ('invoicecontact',) related = ('invoicecontact',)
return qs.select_related(*related) 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) admin.site.register(Account, AccountAdmin)
@ -162,6 +182,7 @@ class AccountAdminMixin(object):
def render(*args, **kwargs): def render(*args, **kwargs):
output = old_render(*args, **kwargs) output = old_render(*args, **kwargs)
output = output.replace('/add/"', '/add/?account=%s"' % self.account.pk) 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) return mark_safe(output)
formfield.widget.render = render formfield.widget.render = render
# Filter related object by account # Filter related object by account

View File

@ -1,21 +1,60 @@
from django import forms 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 django.utils.translation import ugettext_lazy as _
from orchestra.core.validators import validate_password
from orchestra.forms import UserCreationForm 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): AccountCreationForm = create_account_creation_form()
# 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

View File

@ -2,6 +2,7 @@ from django.contrib.auth import models as auth
from django.conf import settings as djsettings from django.conf import settings as djsettings
from django.core import validators from django.core import validators
from django.db import models from django.db import models
from django.db.models.loading import get_model
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -57,19 +58,6 @@ class Account(auth.AbstractBaseUser):
def get_main(cls): def get_main(cls):
return cls.objects.get(pk=settings.ACCOUNTS_MAIN_PK) 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): def disable(self):
self.is_active = False self.is_active = False
self.save(update_fields=['is_active']) self.save(update_fields=['is_active'])
@ -133,5 +121,21 @@ class Account(auth.AbstractBaseUser):
return True return True
return auth._user_has_module_perms(self, app_label) 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) services.register(Account, menu=False)

View File

@ -22,3 +22,32 @@ ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'en')
ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1) ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1)
ACCOUNTS_CREATE_RELATED = getattr(settings, 'ACCOUNTS_CREATE_RELATED', (
# <model>, <key field>, <kwargs>, <help_text>
('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 &lt;username&gt;.orchestra.lan or not."),
),
))

View File

@ -11,14 +11,14 @@ def validate_contact(request, bill, error=True):
'You should <a href="{url}#invoicecontact-group">provide one</a>') 'You should <a href="{url}#invoicecontact-group">provide one</a>')
valid = True valid = True
send = messages.error if error else messages.warning 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) account = force_text(bill.account)
url = reverse('admin:accounts_account_change', args=(bill.account_id,)) url = reverse('admin:accounts_account_change', args=(bill.account_id,))
message = msg.format(relation=_("Related"), account=account, url=url) message = msg.format(relation=_("Related"), account=account, url=url)
send(request, mark_safe(message)) send(request, mark_safe(message))
valid = False valid = False
main = type(bill).account.field.rel.to.get_main() main = type(bill).account.field.rel.to.get_main()
if not hasattr(main, 'invoicecontact'): if not hasattr(main, 'billcontact'):
account = force_text(main) account = force_text(main)
url = reverse('admin:accounts_account_change', args=(main.id,)) url = reverse('admin:accounts_account_change', args=(main.id,))
message = msg.format(relation=_("Main"), account=account, url=url) message = msg.format(relation=_("Main"), account=account, url=url)

View File

@ -5,7 +5,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin 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 orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
from .forms import DatabaseCreationForm, DatabaseUserChangeForm, DatabaseUserCreationForm from .forms import DatabaseCreationForm, DatabaseUserChangeForm, DatabaseUserCreationForm
@ -13,7 +13,7 @@ from .models import Database, DatabaseUser
class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = ('name', 'type', 'account_link') list_display = ('name', 'type', 'display_users', 'account_link')
list_filter = ('type',) list_filter = ('type',)
search_fields = ['name'] search_fields = ['name']
change_readonly_fields = ('name', 'type') change_readonly_fields = ('name', 'type')
@ -21,7 +21,7 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
fieldsets = ( fieldsets = (
(None, { (None, {
'classes': ('extrapretty',), 'classes': ('extrapretty',),
'fields': ('account_link', 'name', 'type', 'users'), 'fields': ('account_link', 'name', 'type', 'users', 'display_users'),
}), }),
) )
add_fieldsets = ( add_fieldsets = (
@ -39,6 +39,18 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
}), }),
) )
add_form = DatabaseCreationForm 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 = '<a href="%s">%s</a>' % (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): def save_model(self, request, obj, form, change):
super(DatabaseAdmin, self).save_model(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): class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, ExtendedModelAdmin):
list_display = ('username', 'type', 'account_link') list_display = ('username', 'type', 'display_databases', 'account_link')
list_filter = ('type',) list_filter = ('type',)
search_fields = ['username'] search_fields = ['username']
form = DatabaseUserChangeForm form = DatabaseUserChangeForm
@ -65,7 +77,7 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten
fieldsets = ( fieldsets = (
(None, { (None, {
'classes': ('extrapretty',), 'classes': ('extrapretty',),
'fields': ('account_link', 'username', 'password', 'type') 'fields': ('account_link', 'username', 'password', 'type', 'display_databases')
}), }),
) )
add_fieldsets = ( add_fieldsets = (
@ -74,6 +86,17 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten
'fields': ('account_link', 'username', 'password1', 'password2', 'type') '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 = '<a href="%s">%s</a>' % (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): def get_urls(self):
useradmin = UserAdmin(DatabaseUser, self.admin_site) useradmin = UserAdmin(DatabaseUser, self.admin_site)

View File

@ -39,7 +39,7 @@ class Bind9MasterDomainBackend(ServiceController):
def update_conf(self, context): def update_conf(self, context):
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
sed '/zone "%(name)s".*/,/^\s*};\s*$/!d' %(conf_path)s | diff -B -I"^\s*//" - <(echo '%(conf)s') || { 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 -e 'N; /^\\n$/d; P; D' %(conf_path)s
echo '%(conf)s' >> %(conf_path)s echo '%(conf)s' >> %(conf_path)s
UPDATED=1 UPDATED=1
@ -47,7 +47,7 @@ class Bind9MasterDomainBackend(ServiceController):
)) ))
# Delete ex-top-domains that are now subdomains # Delete ex-top-domains that are now subdomains
self.append(textwrap.dedent("""\ 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 -e 'N; /^\\n$/d; P; D' %(conf_path)s""" % context
)) ))
if 'zone_path' in context: if 'zone_path' in context:
@ -64,7 +64,7 @@ class Bind9MasterDomainBackend(ServiceController):
# These can never be top level domains # These can never be top level domains
return return
self.append(textwrap.dedent("""\ 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 -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) self.append('diff -B -I"^\s*//" %(conf_path)s.tmp %(conf_path)s || UPDATED=1' % context)

View File

@ -21,6 +21,17 @@ class Domain(models.Model):
def __unicode__(self): def __unicode__(self):
return self.name 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 @property
def origin(self): def origin(self):
return self.top or self return self.top or self
@ -39,14 +50,7 @@ class Domain(models.Model):
return self.origin.subdomains.all() return self.origin.subdomains.all()
def get_top(self): def get_top(self):
split = self.name.split('.') return type(self).get_top_domain(self.name)
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
def render_zone(self): def render_zone(self):
origin = self.origin origin = self.origin

View File

@ -27,6 +27,14 @@ class DomainSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
fields = ('url', 'name', 'records') fields = ('url', 'name', 'records')
postonly_fields = ('name',) 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): def full_clean(self, instance):
""" Checks if everything is consistent """ """ Checks if everything is consistent """
instance = super(DomainSerializer, self).full_clean(instance) instance = super(DomainSerializer, self).full_clean(instance)

View File

@ -124,7 +124,7 @@ class MailmanBackend(ServiceController):
'name': mail_list.name, 'name': mail_list.name,
'password': mail_list.password, 'password': mail_list.password,
'domain': mail_list.address_domain or settings.LISTS_DEFAULT_DOMAIN, '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, 'address_domain': mail_list.address_domain,
'admin': mail_list.admin_email, 'admin': mail_list.admin_email,
'mailman_root': settings.LISTS_MAILMAN_ROOT_PATH, 'mailman_root': settings.LISTS_MAILMAN_ROOT_PATH,

View File

@ -12,9 +12,6 @@ class CleanAddressMixin(object):
if name and not domain: if name and not domain:
msg = _("Domain should be selected for provided address name") msg = _("Domain should be selected for provided address name")
raise forms.ValidationError(msg) 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 return domain

View File

@ -35,6 +35,9 @@ class List(models.Model):
return "%s@%s" % (self.address_name, self.address_domain) return "%s@%s" % (self.address_name, self.address_domain)
return '' return ''
def get_address_name(self):
return self.address_name or self.name
def get_username(self): def get_username(self):
return self.name return self.name

View File

@ -40,15 +40,15 @@ class ListSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
raise serializers.ValidationError(_("Password required")) raise serializers.ValidationError(_("Password required"))
return attrs return attrs
def validate(self, attrs): def validate_address_domain(self, attrs, source):
address_domain = attrs.get('address_domain') address_domain = attrs.get(source)
address_name = attrs.get('address_name', ) address_name = attrs.get('address_name')
if self.object: if self.object:
address_domain = address_domain or self.object.address_domain address_domain = address_domain or self.object.address_domain
address_name = address_name or self.object.address_name 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( 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 return attrs
def save_object(self, obj, **kwargs): def save_object(self, obj, **kwargs):

View File

@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import change_url 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 from .models import WebApp, WebAppOption
@ -25,10 +25,11 @@ class WebAppOptionInline(admin.TabularInline):
return super(WebAppOptionInline, self).formfield_for_dbfield(db_field, **kwargs) return super(WebAppOptionInline, self).formfield_for_dbfield(db_field, **kwargs)
class WebAppAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): class WebAppAdmin(AccountAdminMixin, ExtendedModelAdmin):
fields = ('account_link', 'name', 'type')
list_display = ('name', 'type', 'display_websites', 'account_link') list_display = ('name', 'type', 'display_websites', 'account_link')
list_filter = ('type',) list_filter = ('type',)
add_fields = ('account', 'name', 'type')
fields = ('account_link', 'name', 'type')
inlines = [WebAppOptionInline] inlines = [WebAppOptionInline]
readonly_fields = ('account_link',) readonly_fields = ('account_link',)
change_readonly_fields = ('name', 'type') change_readonly_fields = ('name', 'type')

View File

@ -41,7 +41,7 @@ def validate_name(value):
""" """
A single non-empty line of free-form text with no whitespace. 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) _("Enter a valid name (text without whitspaces)."), 'invalid')(value)

View File

@ -38,6 +38,7 @@ class UserCreationForm(forms.ModelForm):
""" """
error_messages = { error_messages = {
'password_mismatch': _("The two password fields didn't match."), 'password_mismatch': _("The two password fields didn't match."),
'duplicate_username': _("A user with that username already exists."),
} }
password1 = forms.CharField(label=_("Password"), password1 = forms.CharField(label=_("Password"),
widget=forms.PasswordInput, validators=[validate_password]) widget=forms.PasswordInput, validators=[validate_password])