Improved admin UI performance

This commit is contained in:
root 2014-11-02 14:33:55 +00:00
parent c4e8c07311
commit d116360212
16 changed files with 61 additions and 33 deletions

21
TODO.md
View File

@ -1,5 +1,4 @@
TODO
====
TODO ====
* scape strings before executing scripts in order to prevent exploits: django templates automatically scapes things. Most important is to ensuer that all escape ' to &quot
* Don't store passwords and other service parameters that can be changed by the services i.e. mailman, vps etc. Find an execution mechanism that trigger `change_password()`
@ -163,3 +162,21 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* Domain validation has to be done with injected records and subdomains
* Names: lower andupper case allow or disallow ? webapps/account.username etc
* Split plans into a separate app (plans and rates / services ) ?
* reconsider binding webapps to systemusers (pangea multiple users wordpress-ftp, moodle-pangea, etc)
* sync() ServiceController method that synchronizes orchestra and servers (delete or import)
* validate address.forward: if mailbox in account.mailboxes then: _("Please use mailboxes field") or consider removing mailbox support on forward (user@pangea.org instead)
* reespell systemuser to system_user
* remove order in account admin and others
* create admin prefetch_related on ExtendedModelAdmin
* Databases.User add reverse M2M databases widget (like mailbox.addresses)
* One domain zone validation for each save, not one per subdomain, maybe on modeladmin.save_related? prevent save on model_related, and save it on save_related()

View File

@ -154,7 +154,13 @@ class ChangeAddFieldsMixin(object):
class ExtendedModelAdmin(ChangeViewActionsMixin, ChangeAddFieldsMixin, admin.ModelAdmin):
pass
prefetch_related = None
def get_queryset(self, request):
qs = super(ExtendedModelAdmin, self).get_queryset(request)
if self.prefetch_related:
qs = qs.prefetch_related(*self.prefetch_related)
return qs
class SelectPluginAdminMixin(object):

View File

@ -58,7 +58,7 @@ BILLS_ORDER_MODEL = getattr(settings, 'BILLS_ORDER_MODEL', 'orders.Order')
BILLS_CONTACT_DEFAULT_CITY = getattr(settings, 'BILLS_CONTACT_DEFAULT_CITY', 'Barcelona')
BILLS_CONTACT_COUNTRIES = getattr(settings, 'BILLS_CONTACT_COUNTRIES', data.COUNTRIES)
BILLS_CONTACT_COUNTRIES = getattr(settings, 'BILLS_CONTACT_COUNTRIES', ((k,v) for k,v in data.COUNTRIES.iteritems()))
BILLS_CONTACT_DEFAULT_COUNTRY = getattr(settings, 'BILLS_CONTACT_DEFAULT_COUNTRY', 'ES')

View File

@ -52,7 +52,8 @@ class Contact(models.Model):
validators=[RegexValidator(r'^[0-9,A-Z]{3,10}$',
_("Enter a valid zipcode."), 'invalid')])
country = models.CharField(_("country"), max_length=20, blank=True,
choices=settings.CONTACTS_COUNTRIES)
choices=settings.CONTACTS_COUNTRIES,
default=settings.CONTACTS_DEFAULT_COUNTRY)
def __unicode__(self):
return self.short_name

View File

@ -10,7 +10,7 @@ CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES
CONTACTS_DEFAULT_CITY = getattr(settings, 'CONTACTS_DEFAULT_CITY', 'Barcelona')
CONTACTS_COUNTRIES = getattr(settings, 'CONTACTS_COUNTRIES', data.COUNTRIES)
CONTACTS_COUNTRIES = getattr(settings, 'CONTACTS_COUNTRIES', ((k,v) for k,v in data.COUNTRIES.iteritems()))
CONTACTS_DEFAULT_COUNTRY = getattr(settings, 'CONTACTS_DEFAULT_COUNTRY', 'ES')

View File

@ -41,13 +41,15 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
add_form = DatabaseCreationForm
readonly_fields = ('account_link', 'display_users',)
filter_horizontal = ['users']
filter_by_account_fields = ('users',)
prefetch_related = ('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)
return '<br>'.join(links)
display_users.short_description = _("Users")
display_users.allow_tags = True
display_users.admin_order_field = 'users__username'
@ -87,13 +89,15 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten
}),
)
readonly_fields = ('account_link', 'display_databases',)
filter_by_account_fields = ('databases',)
prefetch_related = ('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)
return '<br>'.join(links)
display_databases.short_description = _("Databases")
display_databases.allow_tags = True
display_databases.admin_order_field = 'databases__name'

View File

@ -59,6 +59,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
change_readonly_fields = ('name',)
add_form = MailboxCreationForm
form = MailboxChangeForm
prefetch_related = ('addresses__domain',)
def display_addresses(self, mailbox):
addresses = []
@ -104,6 +105,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
filter_by_account_fields = ('domain', 'mailboxes')
filter_horizontal = ['mailboxes']
form = AddressForm
prefetch_related = ('mailboxes', 'domain')
domain_link = admin_link('domain', order='domain__name')
@ -133,11 +135,6 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
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')
def get_fields(self, request, obj=None):
""" Remove mailboxes field when creating address from a popup i.e. from mailbox add form """
fields = super(AddressAdmin, self).get_fields(request, obj=obj)

View File

@ -13,7 +13,7 @@ class MailboxForm(forms.ModelForm):
""" hacky form for adding reverse M2M form field for Mailbox.addresses """
# TODO keep track of this ticket for future reimplementation
# https://code.djangoproject.com/ticket/897
addresses = forms.ModelMultipleChoiceField(queryset=Address.objects, required=False,
addresses = forms.ModelMultipleChoiceField(queryset=Address.objects.select_related('domain'), required=False,
widget=widgets.FilteredSelectMultiple(verbose_name=_('addresses'), is_stacked=False))
def __init__(self, *args, **kwargs):

View File

@ -76,8 +76,9 @@ class Mailbox(models.Model):
class Address(models.Model):
name = models.CharField(_("name"), max_length=64,
validators=[validators.validate_emailname])
name = models.CharField(_("name"), max_length=64, blank=True,
validators=[validators.validate_emailname],
help_text=_("Address name, left blank for a <i>catch-all</i> address"))
domain = models.ForeignKey(settings.MAILBOXES_DOMAIN_MODEL,
verbose_name=_("domain"),
related_name='addresses')

View File

@ -26,7 +26,11 @@ def validate_emailname(value):
def validate_forward(value):
""" space separated mailboxes or emails """
from .models import Mailbox
destinations = []
for destination in value.split():
if destination in destinations:
raise ValidationError(_("'%s' is already present.") % destination)
destinations.append(destination)
msg = _("'%s' is not an existent mailbox" % destination)
if '@' in destination:
if not destination[-1].isalpha():

View File

@ -20,7 +20,6 @@ transports = {}
def BashSSH(backend, log, server, cmds):
from .models import BackendLog
# TODO save remote file into a root read only directory to avoid users sniffing passwords and stuff
script = '\n'.join(['set -e', 'set -o pipefail'] + cmds + ['exit 0'])
script = script.replace('\r', '')
@ -36,8 +35,8 @@ def BashSSH(backend, log, server, cmds):
logger.debug('%s is going to be executed on %s' % (backend, server))
# Avoid "Argument list too long" on large scripts by genereting a file
# and scping it to the remote server
with open(path, 'w') as script_file:
script_file.write(script)
with os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT, 0600), 'w') as handle:
handle.write(script)
# ssh connection
ssh = paramiko.SSHClient()

View File

@ -87,7 +87,8 @@ class ResourceDataAdmin(ExtendedModelAdmin):
actions = (run_monitor,)
change_view_actions = actions
ordering = ('-updated_at',)
prefetch_related = ('content_object',)
resource_link = admin_link('resource')
content_object_link = admin_link('content_object')
display_updated = admin_date('updated_at', short_description=_("Updated"))
@ -96,10 +97,6 @@ class ResourceDataAdmin(ExtendedModelAdmin):
return data.unit
display_unit.short_description = _("Unit")
display_unit.admin_order_field = 'resource__unit'
def get_queryset(self, request):
queryset = super(ResourceDataAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_object')
class MonitorDataAdmin(ExtendedModelAdmin):

View File

@ -34,15 +34,17 @@ class WebAppAdmin(AccountAdminMixin, ExtendedModelAdmin):
inlines = [WebAppOptionInline]
readonly_fields = ('account_link',)
change_readonly_fields = ('name', 'type')
prefetch_related = ('content_set__website',)
def display_websites(self, webapp):
websites = []
for content in webapp.content_set.all().select_related('website'):
for content in webapp.content_set.all():
website = content.website
url = change_url(website)
name = "%s on %s" % (website.name, content.path)
websites.append('<a href="%s">%s</a>' % (url, name))
add_url = reverse('admin:websites_website_add')
# TODO support for preselecting related we app on website
add_url += '?account=%s' % webapp.account_id
plus = '<strong style="color:green; font-size:12px">+</strong>'
websites.append('<a href="%s">%s%s</a>' % (add_url, plus, ugettext("Add website")))

View File

@ -217,6 +217,10 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
_("FCGI - IO timeout"),
r'^[0-9]{1,3}$'
),
'FcgidProcessLifeTime': (
_("FCGI - IO timeout"),
r'^[0-9]{1,4}$'
),
})

View File

@ -55,6 +55,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
}),
)
filter_by_account_fields = ['domains']
prefetch_related = ('domains', 'content_set__webapp')
def display_domains(self, website):
domains = []
@ -67,7 +68,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
def display_webapps(self, website):
webapps = []
for content in website.content_set.all().select_related('webapp'):
for content in website.content_set.all():
webapp = content.webapp
url = change_url(webapp)
name = "%s on %s" % (webapp.get_type_display(), content.path)
@ -80,11 +81,6 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
if db_field.name == 'root':
kwargs['widget'] = forms.TextInput(attrs={'size':'100'})
return super(WebsiteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
def get_queryset(self, request):
""" Select related for performance """
qs = super(WebsiteAdmin, self).get_queryset(request)
return qs.prefetch_related('domains')
admin.site.register(Website, WebsiteAdmin)

View File

@ -44,8 +44,8 @@ def validate_name(value):
"""
A single non-empty line of free-form text with no whitespace.
"""
validators.RegexValidator('^[\.\w\-]+$',
_("Enter a valid name (text without whitspaces)."), 'invalid')(value)
validators.RegexValidator('^[\.\_\-0-9a-z]+$',
_("Enter a valid name (spaceless lowercase text including _.-)."), 'invalid')(value)
def validate_ascii(value):