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)
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':

View File

@ -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,12 +89,27 @@ 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

View File

@ -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)
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
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)
AccountCreationForm = create_account_creation_form()

View File

@ -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)

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_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>')
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)

View File

@ -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 = '<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):
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 = '<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):
useradmin = UserAdmin(DatabaseUser, self.admin_site)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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')

View File

@ -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)

View File

@ -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])