diff --git a/TODO.md b/TODO.md index 98882ae9..405971b3 100644 --- a/TODO.md +++ b/TODO.md @@ -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 -* 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) @@ -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 * 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 diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index f99d8f3a..ae847031 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -203,10 +203,11 @@ class SelectPluginAdminMixin(object): """ Redirects to select account view if required """ if request.user.is_superuser: 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 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 = { 'title': _("Add new %s") % camel_case_to_spaces(self.plugin_value), } diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py index 2988b654..b9fdf334 100644 --- a/orchestra/admin/utils.py +++ b/orchestra/admin/utils.py @@ -4,6 +4,7 @@ from functools import wraps from django.conf import settings from django.contrib import admin +from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse from django.db import models from django.shortcuts import redirect @@ -99,7 +100,10 @@ def admin_link(*args, **kwargs): if kwargs['field'] in ['id', 'pk', '__unicode__']: obj = instance 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): return '---' url = change_url(obj) diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py index f44f2e43..34f507e5 100644 --- a/orchestra/apps/bills/models.py +++ b/orchestra/apps/bills/models.py @@ -20,13 +20,12 @@ class BillContact(models.Model): account = models.OneToOneField('accounts.Account', verbose_name=_("account"), related_name='billcontact') 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")) city = models.CharField(_("city"), max_length=128, default=settings.BILLS_CONTACT_DEFAULT_CITY) zipcode = models.CharField(_("zip code"), max_length=10, - validators=[RegexValidator(r'^[0-9A-Z]{3,10}$', - _("Enter a valid zipcode."), 'invalid')]) + validators=[RegexValidator(r'^[0-9A-Z]{3,10}$', _("Enter a valid zipcode."))]) country = models.CharField(_("country"), max_length=20, choices=settings.BILLS_CONTACT_COUNTRIES, default=settings.BILLS_CONTACT_DEFAULT_COUNTRY) diff --git a/orchestra/apps/orchestration/admin.py b/orchestra/apps/orchestration/admin.py index fdbbffb6..160507f5 100644 --- a/orchestra/apps/orchestration/admin.py +++ b/orchestra/apps/orchestration/admin.py @@ -1,10 +1,13 @@ +from django import forms from django.contrib import admin from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ +from orchestra.forms.widgets import DynamicHelpTextSelect from orchestra.admin.html import monospace_format from orchestra.admin.utils import admin_link, admin_date, admin_colored +from .backends import ServiceBackend from .models import Server, Route, BackendLog, BackendOperation @@ -27,6 +30,11 @@ class RouteAdmin(admin.ModelAdmin): list_editable = ['backend', 'host', 'match', 'is_active'] 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): try: return escape(route.backend_class().model) @@ -42,6 +50,19 @@ class RouteAdmin(admin.ModelAdmin): return "NOT AVAILABLE" display_actions.short_description = _("actions") 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): diff --git a/orchestra/apps/webapps/admin.py b/orchestra/apps/webapps/admin.py index a847cdaf..b16118a4 100644 --- a/orchestra/apps/webapps/admin.py +++ b/orchestra/apps/webapps/admin.py @@ -6,6 +6,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import change_url from orchestra.apps.accounts.admin import AccountAdminMixin +from orchestra.forms.widgets import DynamicHelpTextSelect from . import settings from .models import WebApp, WebAppOption @@ -26,21 +27,13 @@ class WebAppOptionInline(admin.TabularInline): } def formfield_for_dbfield(self, db_field, **kwargs): - """ Make value input widget bigger """ if db_field.name == 'value': kwargs['widget'] = forms.TextInput(attrs={'size':'100'}) if db_field.name == 'name': # Help text based on select widget - kwargs['widget'] = forms.Select(attrs={ - 'onChange': """ - siteoptions = %s; - valueelement = $("#"+this.id.replace("name", "value")); - valueelement.parent().find('p').remove(); - valueelement.parent().append( - "

" + siteoptions[this.options[this.selectedIndex].value] + "

" - ); - """ % str(self.OPTIONS_HELP_TEXT), - }) + kwargs['widget'] = DynamicHelpTextSelect( + 'this.id.replace("name", "value")', self.OPTIONS_HELP_TEXT + ) 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) websites.append('%s' % (url, name)) 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 plus = '+' websites.append('%s%s' % (add_url, plus, ugettext("Add website"))) @@ -80,7 +73,7 @@ class WebAppAdmin(AccountAdminMixin, ExtendedModelAdmin): if db_field.name == 'type': # Help text based on select widget kwargs['widget'] = forms.Select(attrs={ - 'onChange': """ + 'onClick': """ siteoptions = %s; valueelement = $("#"+this.id); valueelement.parent().find('p').remove(); diff --git a/orchestra/apps/webapps/settings.py b/orchestra/apps/webapps/settings.py index d4300353..8665976d 100644 --- a/orchestra/apps/webapps/settings.py +++ b/orchestra/apps/webapps/settings.py @@ -29,20 +29,20 @@ WEBAPPS_FCGID_PATH = getattr(settings, 'WEBAPPS_FCGID_PATH', WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', { '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',), 'directive': ('fpm', 'fcgi://{}%(app_path)s'.format(WEBAPPS_FPM_LISTEN)), 'help_text': _("This creates a PHP5.5 application under ~/webapps/<app_name>
" "PHP-FPM will be used to execute PHP files.") }, 'php5.2': { - 'verbose_name': "PHP 5.2", + 'verbose_name': "PHP 5.2 fcgi", 'directive': ('fcgi', WEBAPPS_FCGID_PATH), 'help_text': _("This creates a PHP5.2 application under ~/webapps/<app_name>
" "Apache-mod-fcgid will be used to execute PHP files.") }, 'php4': { - 'verbose_name': "PHP 4", + 'verbose_name': "PHP 4 fcgi", 'directive': ('fcgi', WEBAPPS_FCGID_PATH,), 'help_text': _("This creates a PHP4 application under ~/webapps/<app_name>
" "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', { # { 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 - 'enabled_functions': ( + 'php-enabled_functions': ( _("PHP - Enabled functions"), ' '.join(WEBAPPS_PHP_DISABLED_FUNCTIONS), r'^[\w\.,-]+$' @@ -249,30 +265,4 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', { _("PHP - zend_extension"), 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}$' - ), }) diff --git a/orchestra/apps/websites/admin.py b/orchestra/apps/websites/admin.py index d15dde2e..5d4ea655 100644 --- a/orchestra/apps/websites/admin.py +++ b/orchestra/apps/websites/admin.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import admin_link, change_url from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin +from orchestra.forms.widgets import DynamicHelpTextSelect from . import settings from .models import Content, Website, WebsiteOption @@ -25,21 +26,13 @@ class WebsiteOptionInline(admin.TabularInline): # } def formfield_for_dbfield(self, db_field, **kwargs): - """ Make value input widget bigger """ if db_field.name == 'value': kwargs['widget'] = forms.TextInput(attrs={'size':'100'}) if db_field.name == 'name': # Help text based on select widget - kwargs['widget'] = forms.Select(attrs={ - 'onChange': """ - siteoptions = %s; - valueelement = $("#"+this.id.replace("name", "value")); - valueelement.parent().find('p').remove(); - valueelement.parent().append( - "

" + siteoptions[this.options[this.selectedIndex].value] + "

" - ); - """ % str(self.OPTIONS_HELP_TEXT), - }) + kwargs['widget'] = DynamicHelpTextSelect( + 'this.id.replace("name", "value")', self.OPTIONS_HELP_TEXT + ) return super(WebsiteOptionInline, self).formfield_for_dbfield(db_field, **kwargs) diff --git a/orchestra/forms/widgets.py b/orchestra/forms/widgets.py index 0cc2728a..b4673b2a 100644 --- a/orchestra/forms/widgets.py +++ b/orchestra/forms/widgets.py @@ -1,4 +1,5 @@ import re +import textwrap from django import forms from django.utils.safestring import mark_safe @@ -63,3 +64,25 @@ def paddingCheckboxSelectMultiple(padding): return mark_safe(value) widget.render = render 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( + "

" + siteoptions[this.options[this.selectedIndex].value] + "

" + );""".format(target=target, help_text=str(help_text)) + ) diff --git a/orchestra/utils/options.py b/orchestra/utils/options.py index 0e52ad1b..a6961bd1 100644 --- a/orchestra/utils/options.py +++ b/orchestra/utils/options.py @@ -56,6 +56,6 @@ def dict_setting_to_choices(choices): def tuple_setting_to_choices(choices): 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] )