Added dynamic help text to backend route

This commit is contained in:
Marc Aymerich 2014-11-11 12:05:47 +00:00
parent 2eb2fb5a5c
commit d422e109e9
10 changed files with 99 additions and 62 deletions

15
TODO.md
View file

@ -163,7 +163,7 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* Names: lower andupper case allow or disallow ? webapps/account.username etc * Names: lower andupper case allow or disallow ? webapps/account.username etc
* Split plans into a separate app (plans and rates / services ) ? * Split plans into a separate app (plans and rates / services ) ?
* sync() ServiceController method that synchronizes orchestra and servers (delete or import) * sync() ServiceController method that synchronizes orchestra and servers (delete or import)
@ -200,3 +200,16 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* User [Group] webapp/website option (validation) which overrides default mainsystemuser * User [Group] webapp/website option (validation) which overrides default mainsystemuser
* validate systemuser.home * validate systemuser.home
* Create plugin app
* Create options widget
* generic options fpm/fcgid/uwsgi webapps (num procs, idle io timeout)
* webapp backend option compatibility check?
* Route help text with model name when selecting backend
* Service instance name when selecting content_type
* Address.forward mailbbox validate not available on mailboxes

View file

@ -203,10 +203,11 @@ class SelectPluginAdminMixin(object):
""" Redirects to select account view if required """ """ Redirects to select account view if required """
if request.user.is_superuser: if request.user.is_superuser:
plugin_value = request.GET.get(self.plugin_field) or request.POST.get(self.plugin_field) plugin_value = request.GET.get(self.plugin_field) or request.POST.get(self.plugin_field)
if plugin_value or self.plugin.get_plugins() == 1: if plugin_value or len(self.plugin.get_plugins()) == 1:
self.plugin_value = plugin_value self.plugin_value = plugin_value
if not plugin_value: if not plugin_value:
self.plugin_value = self.plugin.get_plugins()[0] self.plugin_value = self.plugin.get_plugins()[0].get_plugin_name()
b = self.plugin_value
context = { context = {
'title': _("Add new %s") % camel_case_to_spaces(self.plugin_value), 'title': _("Add new %s") % camel_case_to_spaces(self.plugin_value),
} }

View file

@ -4,6 +4,7 @@ from functools import wraps
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.shortcuts import redirect from django.shortcuts import redirect
@ -99,7 +100,10 @@ def admin_link(*args, **kwargs):
if kwargs['field'] in ['id', 'pk', '__unicode__']: if kwargs['field'] in ['id', 'pk', '__unicode__']:
obj = instance obj = instance
else: else:
obj = get_field_value(instance, kwargs['field']) try:
obj = get_field_value(instance, kwargs['field'])
except ObjectDoesNotExist:
return '---'
if not getattr(obj, 'pk', None): if not getattr(obj, 'pk', None):
return '---' return '---'
url = change_url(obj) url = change_url(obj)

View file

@ -20,13 +20,12 @@ class BillContact(models.Model):
account = models.OneToOneField('accounts.Account', verbose_name=_("account"), account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
related_name='billcontact') related_name='billcontact')
name = models.CharField(_("name"), max_length=256, blank=True, name = models.CharField(_("name"), max_length=256, blank=True,
help_text=_("Account full name will be used when not provided")) help_text=_("Account full name will be used when left blank."))
address = models.TextField(_("address")) address = models.TextField(_("address"))
city = models.CharField(_("city"), max_length=128, city = models.CharField(_("city"), max_length=128,
default=settings.BILLS_CONTACT_DEFAULT_CITY) default=settings.BILLS_CONTACT_DEFAULT_CITY)
zipcode = models.CharField(_("zip code"), max_length=10, zipcode = models.CharField(_("zip code"), max_length=10,
validators=[RegexValidator(r'^[0-9A-Z]{3,10}$', validators=[RegexValidator(r'^[0-9A-Z]{3,10}$', _("Enter a valid zipcode."))])
_("Enter a valid zipcode."), 'invalid')])
country = models.CharField(_("country"), max_length=20, country = models.CharField(_("country"), max_length=20,
choices=settings.BILLS_CONTACT_COUNTRIES, choices=settings.BILLS_CONTACT_COUNTRIES,
default=settings.BILLS_CONTACT_DEFAULT_COUNTRY) default=settings.BILLS_CONTACT_DEFAULT_COUNTRY)

View file

@ -1,10 +1,13 @@
from django import forms
from django.contrib import admin from django.contrib import admin
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.forms.widgets import DynamicHelpTextSelect
from orchestra.admin.html import monospace_format from orchestra.admin.html import monospace_format
from orchestra.admin.utils import admin_link, admin_date, admin_colored from orchestra.admin.utils import admin_link, admin_date, admin_colored
from .backends import ServiceBackend
from .models import Server, Route, BackendLog, BackendOperation from .models import Server, Route, BackendLog, BackendOperation
@ -27,6 +30,11 @@ class RouteAdmin(admin.ModelAdmin):
list_editable = ['backend', 'host', 'match', 'is_active'] list_editable = ['backend', 'host', 'match', 'is_active']
list_filter = ['host', 'is_active', 'backend'] list_filter = ['host', 'is_active', 'backend']
BACKEND_HELP_TEXT = {
backend: "This backend operates over '%s'" % ServiceBackend.get_backend(backend).model
for backend, __ in ServiceBackend.get_plugin_choices()
}
def display_model(self, route): def display_model(self, route):
try: try:
return escape(route.backend_class().model) return escape(route.backend_class().model)
@ -42,6 +50,19 @@ class RouteAdmin(admin.ModelAdmin):
return "<span style='color: red;'>NOT AVAILABLE</span>" return "<span style='color: red;'>NOT AVAILABLE</span>"
display_actions.short_description = _("actions") display_actions.short_description = _("actions")
display_actions.allow_tags = True display_actions.allow_tags = True
def formfield_for_dbfield(self, db_field, **kwargs):
""" Provides dynamic help text on backend form field """
if db_field.name == 'backend':
kwargs['widget'] = DynamicHelpTextSelect('this.id', self.BACKEND_HELP_TEXT)
return super(RouteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
def get_form(self, request, obj=None, **kwargs):
""" Include dynamic help text for existing objects """
form = super(RouteAdmin, self).get_form(request, obj=obj, **kwargs)
if obj:
form.base_fields['backend'].help_text = self.BACKEND_HELP_TEXT[obj.backend]
return form
class BackendOperationInline(admin.TabularInline): class BackendOperationInline(admin.TabularInline):

View file

@ -6,6 +6,7 @@ from django.utils.translation import ugettext, 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 AccountAdminMixin from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.forms.widgets import DynamicHelpTextSelect
from . import settings from . import settings
from .models import WebApp, WebAppOption from .models import WebApp, WebAppOption
@ -26,21 +27,13 @@ class WebAppOptionInline(admin.TabularInline):
} }
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
if db_field.name == 'value': if db_field.name == 'value':
kwargs['widget'] = forms.TextInput(attrs={'size':'100'}) kwargs['widget'] = forms.TextInput(attrs={'size':'100'})
if db_field.name == 'name': if db_field.name == 'name':
# Help text based on select widget # Help text based on select widget
kwargs['widget'] = forms.Select(attrs={ kwargs['widget'] = DynamicHelpTextSelect(
'onChange': """ 'this.id.replace("name", "value")', self.OPTIONS_HELP_TEXT
siteoptions = %s; )
valueelement = $("#"+this.id.replace("name", "value"));
valueelement.parent().find('p').remove();
valueelement.parent().append(
"<p class='help'>" + siteoptions[this.options[this.selectedIndex].value] + "</p>"
);
""" % str(self.OPTIONS_HELP_TEXT),
})
return super(WebAppOptionInline, self).formfield_for_dbfield(db_field, **kwargs) return super(WebAppOptionInline, self).formfield_for_dbfield(db_field, **kwargs)
@ -67,7 +60,7 @@ class WebAppAdmin(AccountAdminMixin, ExtendedModelAdmin):
name = "%s on %s" % (website.name, content.path) name = "%s on %s" % (website.name, content.path)
websites.append('<a href="%s">%s</a>' % (url, name)) websites.append('<a href="%s">%s</a>' % (url, name))
add_url = reverse('admin:websites_website_add') add_url = reverse('admin:websites_website_add')
# TODO support for preselecting related we app on website # TODO support for preselecting related web app on website
add_url += '?account=%s' % webapp.account_id add_url += '?account=%s' % webapp.account_id
plus = '<strong style="color:green; font-size:12px">+</strong>' plus = '<strong style="color:green; font-size:12px">+</strong>'
websites.append('<a href="%s">%s%s</a>' % (add_url, plus, ugettext("Add website"))) websites.append('<a href="%s">%s%s</a>' % (add_url, plus, ugettext("Add website")))
@ -80,7 +73,7 @@ class WebAppAdmin(AccountAdminMixin, ExtendedModelAdmin):
if db_field.name == 'type': if db_field.name == 'type':
# Help text based on select widget # Help text based on select widget
kwargs['widget'] = forms.Select(attrs={ kwargs['widget'] = forms.Select(attrs={
'onChange': """ 'onClick': """
siteoptions = %s; siteoptions = %s;
valueelement = $("#"+this.id); valueelement = $("#"+this.id);
valueelement.parent().find('p').remove(); valueelement.parent().find('p').remove();

View file

@ -29,20 +29,20 @@ WEBAPPS_FCGID_PATH = getattr(settings, 'WEBAPPS_FCGID_PATH',
WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', { WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', {
'php5.5': { 'php5.5': {
'verbose_name': "PHP 5.5", 'verbose_name': "PHP 5.5 fpm",
# 'fpm', ('unix:/var/run/%(user)s-%(app_name)s.sock|fcgi://127.0.0.1%(app_path)s',), # 'fpm', ('unix:/var/run/%(user)s-%(app_name)s.sock|fcgi://127.0.0.1%(app_path)s',),
'directive': ('fpm', 'fcgi://{}%(app_path)s'.format(WEBAPPS_FPM_LISTEN)), 'directive': ('fpm', 'fcgi://{}%(app_path)s'.format(WEBAPPS_FPM_LISTEN)),
'help_text': _("This creates a PHP5.5 application under ~/webapps/&lt;app_name&gt;<br>" 'help_text': _("This creates a PHP5.5 application under ~/webapps/&lt;app_name&gt;<br>"
"PHP-FPM will be used to execute PHP files.") "PHP-FPM will be used to execute PHP files.")
}, },
'php5.2': { 'php5.2': {
'verbose_name': "PHP 5.2", 'verbose_name': "PHP 5.2 fcgi",
'directive': ('fcgi', WEBAPPS_FCGID_PATH), 'directive': ('fcgi', WEBAPPS_FCGID_PATH),
'help_text': _("This creates a PHP5.2 application under ~/webapps/&lt;app_name&gt;<br>" 'help_text': _("This creates a PHP5.2 application under ~/webapps/&lt;app_name&gt;<br>"
"Apache-mod-fcgid will be used to execute PHP files.") "Apache-mod-fcgid will be used to execute PHP files.")
}, },
'php4': { 'php4': {
'verbose_name': "PHP 4", 'verbose_name': "PHP 4 fcgi",
'directive': ('fcgi', WEBAPPS_FCGID_PATH,), 'directive': ('fcgi', WEBAPPS_FCGID_PATH,),
'help_text': _("This creates a PHP4 application under ~/webapps/&lt;app_name&gt;<br>" 'help_text': _("This creates a PHP4 application under ~/webapps/&lt;app_name&gt;<br>"
"Apache-mod-fcgid will be used to execute PHP files.") "Apache-mod-fcgid will be used to execute PHP files.")
@ -104,8 +104,24 @@ WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTIO
WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', { WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
# { name: ( verbose_name, [help_text], validation_regex ) } # { name: ( verbose_name, [help_text], validation_regex ) }
# Processes
'timeout': (
_("Process timeout"),
_("Maximum time in seconds allowed for a request to complete (a number between 0 and 999)."),
# FCGID FcgidIOTimeout
# FPM pm.request_terminate_timeout
# PHP max_execution_time ini
r'^[0-9]{1,3}$',
),
'processes': (
_("Number of processes"),
_("Maximum number of children that can be alive at the same time (a number between 0 and 9)."),
# FCGID MaxProcesses
# FPM pm.max_children
r'^[0-9]$',
),
# PHP # PHP
'enabled_functions': ( 'php-enabled_functions': (
_("PHP - Enabled functions"), _("PHP - Enabled functions"),
' '.join(WEBAPPS_PHP_DISABLED_FUNCTIONS), ' '.join(WEBAPPS_PHP_DISABLED_FUNCTIONS),
r'^[\w\.,-]+$' r'^[\w\.,-]+$'
@ -249,30 +265,4 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
_("PHP - zend_extension"), _("PHP - zend_extension"),
r'^[^ ]+$' r'^[^ ]+$'
), ),
# FCGID
'FcgidIdleTimeout': (
_("FCGI - Idle timeout"),
_("Number between 0 and 999."),
r'^[0-9]{1,3}$'
),
'FcgidBusyTimeout': (
_("FCGI - Busy timeout"),
_("Number between 0 and 999."),
r'^[0-9]{1,3}$'
),
'FcgidConnectTimeout': (
_("FCGI - Connection timeout"),
_("Number of seconds between 0 and 999."),
r'^[0-9]{1,3}$'
),
'FcgidIOTimeout': (
_("FCGI - IO timeout"),
_("Number of seconds between 0 and 999."),
r'^[0-9]{1,3}$'
),
'FcgidProcessLifeTime': (
_("FCGI - IO timeout"),
_("Numbe of secondsr between 0 and 9999."),
r'^[0-9]{1,4}$'
),
}) })

View file

@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import admin_link, change_url 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 orchestra.forms.widgets import DynamicHelpTextSelect
from . import settings from . import settings
from .models import Content, Website, WebsiteOption from .models import Content, Website, WebsiteOption
@ -25,21 +26,13 @@ class WebsiteOptionInline(admin.TabularInline):
# } # }
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
if db_field.name == 'value': if db_field.name == 'value':
kwargs['widget'] = forms.TextInput(attrs={'size':'100'}) kwargs['widget'] = forms.TextInput(attrs={'size':'100'})
if db_field.name == 'name': if db_field.name == 'name':
# Help text based on select widget # Help text based on select widget
kwargs['widget'] = forms.Select(attrs={ kwargs['widget'] = DynamicHelpTextSelect(
'onChange': """ 'this.id.replace("name", "value")', self.OPTIONS_HELP_TEXT
siteoptions = %s; )
valueelement = $("#"+this.id.replace("name", "value"));
valueelement.parent().find('p').remove();
valueelement.parent().append(
"<p class='help'>" + siteoptions[this.options[this.selectedIndex].value] + "</p>"
);
""" % str(self.OPTIONS_HELP_TEXT),
})
return super(WebsiteOptionInline, self).formfield_for_dbfield(db_field, **kwargs) return super(WebsiteOptionInline, self).formfield_for_dbfield(db_field, **kwargs)

View file

@ -1,4 +1,5 @@
import re import re
import textwrap
from django import forms from django import forms
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -63,3 +64,25 @@ def paddingCheckboxSelectMultiple(padding):
return mark_safe(value) return mark_safe(value)
widget.render = render widget.render = render
return widget return widget
class DynamicHelpTextSelect(forms.Select):
def __init__(self, target, help_text, *args, **kwargs):
help_text = self.get_dynamic_help_text(target, help_text)
attrs = {
'onClick': help_text,
'onChange': help_text,
}
attrs.update(kwargs.get('attrs', {}))
kwargs['attrs'] = attrs
super(DynamicHelpTextSelect, self).__init__(*args, **kwargs)
def get_dynamic_help_text(self, target, help_text):
return textwrap.dedent("""\
siteoptions = {help_text};
valueelement = $("#" + {target});
valueelement.parent().find('p').remove();
valueelement.parent().append(
"<p class='help'>" + siteoptions[this.options[this.selectedIndex].value] + "</p>"
);""".format(target=target, help_text=str(help_text))
)

View file

@ -56,6 +56,6 @@ def dict_setting_to_choices(choices):
def tuple_setting_to_choices(choices): def tuple_setting_to_choices(choices):
return sorted( return sorted(
[ (name, opt[0]) for name,opt in choices.iteritems() ], tuple((name, opt[0]) for name, opt in choices.iteritems()),
key=lambda e: e[0] key=lambda e: e[0]
) )