diff --git a/TODO.md b/TODO.md index 83418b15..58888824 100644 --- a/TODO.md +++ b/TODO.md @@ -414,3 +414,5 @@ mkhomedir_helper or create ssh homes with bash.rc and such # warnings if some plugins are disabled, like make routes red # replace show emails by https://docs.python.org/3/library/email.contentmanager.html#module-email.contentmanager + +# SElect contact list breadcrumbs diff --git a/orchestra/contrib/orchestration/admin.py b/orchestra/contrib/orchestration/admin.py index acaef8c2..7afc245e 100644 --- a/orchestra/contrib/orchestration/admin.py +++ b/orchestra/contrib/orchestration/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, admin_date, admin_colored, display_mono, display_code +from orchestra.plugins.admin import display_plugin_field from . import settings, helpers from .backends import ServiceBackend @@ -27,7 +28,8 @@ STATE_COLORS = { class RouteAdmin(ExtendedModelAdmin): list_display = ( - 'backend', 'host', 'match', 'display_model', 'display_actions', 'async', 'is_active' + 'display_backend', 'host', 'match', 'display_model', 'display_actions', 'async', + 'is_active' ) list_editable = ('host', 'match', 'async', 'is_active') list_filter = ('host', 'is_active', 'async', 'backend') @@ -41,6 +43,8 @@ class RouteAdmin(ExtendedModelAdmin): backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends() } + display_backend = display_plugin_field('backend') + def display_model(self, route): try: return escape(route.backend_class.model) @@ -60,7 +64,8 @@ class RouteAdmin(ExtendedModelAdmin): def formfield_for_dbfield(self, db_field, **kwargs): """ Provides dynamic help text on backend form field """ if db_field.name == 'backend': - kwargs['widget'] = RouteBackendSelect('this.id', self.BACKEND_HELP_TEXT, self.DEFAULT_MATCH) + kwargs['widget'] = RouteBackendSelect( + 'this.id', self.BACKEND_HELP_TEXT, self.DEFAULT_MATCH) field = super(RouteAdmin, self).formfield_for_dbfield(db_field, **kwargs) if db_field.name == 'host': # Cache host choices @@ -70,7 +75,6 @@ class RouteAdmin(ExtendedModelAdmin): request._host_choices_cache = choices = list(field.choices) field.choices = choices return field - def get_form(self, request, obj=None, **kwargs): """ Include dynamic help text for existing objects """ diff --git a/orchestra/contrib/orchestration/forms.py b/orchestra/contrib/orchestration/forms.py index a6f253c2..bd3f96ad 100644 --- a/orchestra/contrib/orchestration/forms.py +++ b/orchestra/contrib/orchestration/forms.py @@ -1,14 +1,20 @@ from django import forms -from orchestra.forms.widgets import SpanWidget -from orchestra.forms.widgets import paddingCheckboxSelectMultiple + +from orchestra.forms.widgets import SpanWidget, paddingCheckboxSelectMultiple class RouteForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(RouteForm, self).__init__(*args, **kwargs) if self.instance: - self.fields['backend'].widget = SpanWidget() self.fields['backend'].required = False - self.fields['async_actions'].widget = paddingCheckboxSelectMultiple(45) - actions = self.instance.backend_class.actions - self.fields['async_actions'].choices = ((action, action) for action in actions) + try: + backend_class = self.instance.backend_class + except KeyError: + self.fields['backend'].widget = SpanWidget( + display='%s NOT AVAILABLE' % self.instance.backend) + else: + self.fields['backend'].widget = SpanWidget() + actions = backend_class.actions + self.fields['async_actions'].widget = paddingCheckboxSelectMultiple(45) + self.fields['async_actions'].choices = ((action, action) for action in actions) diff --git a/orchestra/contrib/orchestration/models.py b/orchestra/contrib/orchestration/models.py index e3b439e8..8ccc1347 100644 --- a/orchestra/contrib/orchestration/models.py +++ b/orchestra/contrib/orchestration/models.py @@ -1,3 +1,4 @@ +import logging import socket from django.contrib.contenttypes.fields import GenericForeignKey @@ -10,12 +11,14 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.core.validators import validate_ip_address, ValidationError from orchestra.models.fields import NullableCharField, MultiSelectField -#from orchestra.utils.apps import autodiscover from . import settings from .backends import ServiceBackend +logger = logging.getLogger(__name__) + + class Server(models.Model): """ Machine runing daemons (services) """ name = models.CharField(_("name"), max_length=256, unique=True) @@ -147,12 +150,17 @@ class RouteQuerySet(models.QuerySet): cache = kwargs.get('cache', {}) if not cache: for route in self.filter(is_active=True).select_related('host'): - for action in route.backend_class.get_actions(): - key = (route.backend, action) - try: - cache[key].append(route) - except KeyError: - cache[key] = [route] + try: + backend_class = route.backend_class + except KeyError: + logger.warning("Backed '%s' not installed." % route.backend) + else: + for action in backend_class.get_actions(): + key = (route.backend, action) + try: + cache[key].append(route) + except KeyError: + cache[key] = [route] routes = [] backend_cls = operation.backend key = (backend_cls.get_name(), operation.action) @@ -202,7 +210,13 @@ class Route(models.Model): if not self.match: self.match = 'True' if self.backend: - backend_model = self.backend_class.model_class() + try: + backend_class = self.backend_class + except KeyError: + raise ValidationError({ + 'backend': _("Backend '%s' is not installed.") % self.backend + }) + backend_model = backend_class.model_class() try: obj = backend_model.objects.all()[0] except IndexError: diff --git a/orchestra/contrib/saas/backends/moodle.py b/orchestra/contrib/saas/backends/moodle.py index dc230664..fa7dbe88 100644 --- a/orchestra/contrib/saas/backends/moodle.py +++ b/orchestra/contrib/saas/backends/moodle.py @@ -14,12 +14,12 @@ class MoodleMuBackend(ServiceController): Creates a Moodle site on a Moodle multisite installation // config.php + // map custom domains to sites $site_map = array( // "" => ["", ""], ); $site = getenv("SITE"); - $wwwroot = "https://{$site}-courses.pangea.org"; if ( $site == '' ) { $http_host = $_SERVER['HTTP_HOST']; if (array_key_exists($http_host, $site_map)) { @@ -32,7 +32,16 @@ class MoodleMuBackend(ServiceController): $site = array_shift((explode(".", $http_host))); $wwwroot = "https://{$site}-courses.pangea.org"; } + } else { + $wwwroot = "https://{$site}-courses.pangea.org"; + foreach ($site_map as $key => $value) { + if ($value[0] == $site) { + $wwwroot = $value[1]; + break; + } + } } + $prefix = str_replace('-', '_', $site); $CFG->prefix = "${prefix}_"; $CFG->wwwroot = $wwwroot; diff --git a/orchestra/contrib/webapps/admin.py b/orchestra/contrib/webapps/admin.py index bdfb1c55..59b5dd5b 100644 --- a/orchestra/contrib/webapps/admin.py +++ b/orchestra/contrib/webapps/admin.py @@ -9,7 +9,7 @@ from orchestra.admin.utils import change_url, get_modeladmin from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdminMixin from orchestra.forms.widgets import DynamicHelpTextSelect -from orchestra.plugins.admin import SelectPluginAdminMixin +from orchestra.plugins.admin import SelectPluginAdminMixin, display_plugin_field from orchestra.utils.html import get_on_site_link from .filters import HasWebsiteListFilter, PHPVersionListFilter @@ -50,7 +50,7 @@ class WebAppOptionInline(admin.TabularInline): class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin): - list_display = ('name', 'type', 'display_detail', 'display_websites', 'account_link') + list_display = ('name', 'display_type', 'display_detail', 'display_websites', 'account_link') list_filter = ('type', HasWebsiteListFilter, PHPVersionListFilter) inlines = [WebAppOptionInline] readonly_fields = ('account_link', ) @@ -62,6 +62,8 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) plugin_title = _("Web application type") actions = (list_accounts,) + display_type = display_plugin_field('type') + def display_websites(self, webapp): websites = [] for content in webapp.content_set.all(): @@ -81,8 +83,12 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) display_websites.allow_tags = True def display_detail(self, webapp): - return webapp.type_instance.get_detail() + try: + return webapp.type_instance.get_detail() + except KeyError: + return "Not available" display_detail.short_description = _("detail") + display_detail.allow_tags = True # def get_form(self, request, obj=None, **kwargs): # form = super(WebAppAdmin, self).get_form(request, obj, **kwargs) diff --git a/orchestra/plugins/admin.py b/orchestra/plugins/admin.py index 64341716..dac9d5b3 100644 --- a/orchestra/plugins/admin.py +++ b/orchestra/plugins/admin.py @@ -15,8 +15,13 @@ class SelectPluginAdminMixin(object): def get_form(self, request, obj=None, **kwargs): if obj: - plugin = getattr(obj, '%s_instance' % self.plugin_field) - self.form = getattr(plugin, 'get_change_form', plugin.get_form)() + try: + plugin = getattr(obj, '%s_instance' % self.plugin_field) + except KeyError: + plugin_name = getattr(obj, self.plugin_field) + raise KeyError(_("Plugin '%s' is not available.") % plugin_name) + else: + self.form = getattr(plugin, 'get_change_form', plugin.get_form)() else: plugin = self.plugin.get(self.plugin_value)() self.form = plugin.get_form() @@ -90,9 +95,12 @@ class SelectPluginAdminMixin(object): def change_view(self, request, object_id, form_url='', extra_context=None): obj = self.get_object(request, unquote(object_id)) - plugin = getattr(obj, '%s_class' % self.plugin_field) + try: + verbose = getattr(obj, '%s_class' % self.plugin_field).verbose_name + except KeyError: + raise KeyError(_("Plugin '%s' is not available.") % getattr(obj, self.plugin_field)) context = { - 'title': _("Change %s") % plugin.verbose_name, + 'title': _("Change %s") % verbose, } context.update(extra_context or {}) return super(SelectPluginAdminMixin, self).change_view( @@ -102,3 +110,17 @@ class SelectPluginAdminMixin(object): if not change: setattr(obj, self.plugin_field, self.plugin_value) obj.save() + + +def display_plugin_field(field_name): + def inner(modeladmin, obj, field_name=field_name): + try: + plugin_class = getattr(obj, '%s_class' % field_name) + except KeyError: + value = getattr(obj, field_name) + return "%s" % value + return getattr(obj, 'get_%s_display' % field_name)() + inner.short_description = field_name + inner.admin_order_field = field_name + inner.allow_tags = True + return inner diff --git a/orchestra/utils/apps.py b/orchestra/utils/apps.py index c6ecaa69..c66189f6 100644 --- a/orchestra/utils/apps.py +++ b/orchestra/utils/apps.py @@ -1,22 +1,3 @@ -#from django.utils.importlib import import_module -#from django.utils.module_loading import module_has_submodule - - -#def autodiscover(module): -# """ Auto-discover INSTALLED_APPS module.py """ -# from django.conf import settings -# for app in settings.INSTALLED_APPS: -# mod = import_module(app) -# try: -# import_module('%s.%s' % (app, module)) -# except ImportError: -# # Decide whether to bubble up this error. If the app just -# # doesn't have the module, we can ignore the error -# # attempting to import it, otherwise we want it to bubble up. -# if module_has_submodule(mod, module): -# print '%s module caused this error:' % module -# raise - def isinstalled(app): """ returns True if app is installed """ from django.conf import settings diff --git a/orchestra/views.py b/orchestra/views.py index cbf56238..31067678 100644 --- a/orchestra/views.py +++ b/orchestra/views.py @@ -1,5 +1,5 @@ from django.http import Http404 -from django.contrib.admin.util import unquote +from django.contrib.admin.utils import unquote from django.core.exceptions import PermissionDenied from django.db.models import get_model from django.shortcuts import get_object_or_404 diff --git a/requirements.txt b/requirements.txt index d1d79229..86e13d7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -django==1.8.4 +django==1.8.5 django-fluent-dashboard==0.5.3 django-admin-tools==0.6.0 -django-extensions==1.5.2 +django-extensions==1.5.7 django-celery==3.1.16 celery==3.1.16 kombu==3.0.23