diff --git a/TODO.md b/TODO.md
index 92b2d7ce..b783c23c 100644
--- a/TODO.md
+++ b/TODO.md
@@ -462,6 +462,13 @@ mkhomedir_helper or create ssh homes with bash.rc and such
# POSTFIX web traffic monitor '": uid=" from=<%(user)s>'
+# Mv .deleted make sure it works with nested destinations
+# Re-run backends (save regenerate, delete run same script) warning on confirmation page: DELETED objects will be deleted on the server if you have recreated them.
+# Automatically re-run backends until success? only timedout executions?
+
+
+
+
### Quick start
0. Install orchestra following any of these methods:
1. [PIP-only, Fast deployment setup (demo)](README.md#fast-deployment-setup)
diff --git a/orchestra/admin/decorators.py b/orchestra/admin/decorators.py
index 4c812841..4c5abcae 100644
--- a/orchestra/admin/decorators.py
+++ b/orchestra/admin/decorators.py
@@ -1,6 +1,8 @@
from functools import wraps, partial
+from django.contrib import messages
from django.contrib.admin import helpers
+from django.core.exceptions import ValidationError
from django.template.response import TemplateResponse
from django.utils.decorators import available_attrs
from django.utils.encoding import force_text
@@ -13,7 +15,7 @@ def admin_field(method):
""" Wraps a function to be used as a ModelAdmin method field """
def admin_field_wrapper(*args, **kwargs):
""" utility function for creating admin links """
- kwargs['field'] = args[0] if args else ''
+ kwargs['field'] = args[0] if args else '__str__'
kwargs['order'] = kwargs.get('order', kwargs['field'])
kwargs['popup'] = kwargs.get('popup', False)
# TODO get field verbose name
@@ -38,7 +40,7 @@ def format_display_objects(modeladmin, request, queryset):
return objects
-def action_with_confirmation(action_name=None, extra_context={},
+def action_with_confirmation(action_name=None, extra_context=None, validator=None,
template='admin/orchestra/generic_confirmation.html'):
"""
Generic pattern for actions that needs confirmation step
@@ -46,9 +48,15 @@ def action_with_confirmation(action_name=None, extra_context={},
"""
- def decorator(func, extra_context=extra_context, template=template, action_name=action_name):
+ def decorator(func, extra_context=extra_context, template=template, action_name=action_name, validatior=validator):
@wraps(func, assigned=available_attrs(func))
- def inner(modeladmin, request, queryset, action_name=action_name, extra_context=extra_context):
+ def inner(modeladmin, request, queryset, action_name=action_name, extra_context=extra_context, validator=validator):
+ if validator is not None:
+ try:
+ validator(queryset)
+ except ValidationError as e:
+ messages.error(request, '
'.join(e))
+ return
# The user has already confirmed the action.
if request.POST.get('post') == 'generic_confirmation':
stay = func(modeladmin, request, queryset)
@@ -82,7 +90,7 @@ def action_with_confirmation(action_name=None, extra_context={},
if callable(extra_context):
extra_context = extra_context(modeladmin, request, queryset)
- context.update(extra_context)
+ context.update(extra_context or {})
if 'display_objects' not in context:
# Compute it only when necessary
context['display_objects'] = format_display_objects(modeladmin, request, queryset)
diff --git a/orchestra/contrib/bills/actions.py b/orchestra/contrib/bills/actions.py
index 6d461e60..083160f6 100644
--- a/orchestra/contrib/bills/actions.py
+++ b/orchestra/contrib/bills/actions.py
@@ -4,6 +4,7 @@ from datetime import date
from django.contrib import messages
from django.contrib.admin import helpers
+from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import transaction
from django.forms.models import modelformset_factory
@@ -246,11 +247,16 @@ def copy_lines(modeladmin, request, queryset):
return move_lines(modeladmin, request, queryset)
-@action_with_confirmation()
+def validate_amend_bills(bills):
+ for bill in bills:
+ if bill.is_open:
+ raise ValidationError(_("Selected bills should be in closed state"))
+ if bill.type not in bill.AMEND_MAP:
+ raise ValidationError(_("%s can not be amended.") % bill.get_type_display())
+
+
+@action_with_confirmation(validator=validate_amend_bills)
def amend_bills(modeladmin, request, queryset):
- if queryset.filter(is_open=True).exists():
- messages.warning(request, _("Selected bills should be in closed state"))
- return
amend_ids = []
for bill in queryset:
with translation.override(bill.account.language):
diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py
index 3e1ed01b..65d0c9c8 100644
--- a/orchestra/contrib/bills/admin.py
+++ b/orchestra/contrib/bills/admin.py
@@ -321,6 +321,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
if obj:
if not obj.is_open:
exclude += ['close_bills', 'close_send_download_bills']
+ if obj.type not in obj.AMEND_MAP:
+ exclude += ['amend_bills']
return [action for action in actions if action.__name__ not in exclude]
def get_inline_instances(self, request, obj=None):
diff --git a/orchestra/contrib/mailboxes/admin.py b/orchestra/contrib/mailboxes/admin.py
index 7f77d6e5..ad405e80 100644
--- a/orchestra/contrib/mailboxes/admin.py
+++ b/orchestra/contrib/mailboxes/admin.py
@@ -219,7 +219,14 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
display_mailboxes.allow_tags = True
def display_forward(self, address):
- values = [ dest for dest in address.forward.split() ]
+ forward_mailboxes = {m.name: m for m in address.get_forward_mailboxes()}
+ values = []
+ for forward in address.forward.split():
+ mbox = forward_mailboxes.get(forward)
+ if mbox:
+ values.append(admin_link()(mbox))
+ else:
+ values.append(forward)
return '
'.join(values)
display_forward.short_description = _("Forward")
display_forward.allow_tags = True
diff --git a/orchestra/contrib/mailer/engine.py b/orchestra/contrib/mailer/engine.py
index 62e45772..fad98a46 100644
--- a/orchestra/contrib/mailer/engine.py
+++ b/orchestra/contrib/mailer/engine.py
@@ -72,5 +72,5 @@ def send_pending(bulk=settings.MAILER_BULK_MESSAGES):
except OperationLocked:
pass
finally:
- if connection.connection is not None:
+ if 'connection' in vars() and connection.connection is not None:
connection.close()
diff --git a/orchestra/contrib/orchestration/admin.py b/orchestra/contrib/orchestration/admin.py
index 4a0d7bad..000dadf9 100644
--- a/orchestra/contrib/orchestration/admin.py
+++ b/orchestra/contrib/orchestration/admin.py
@@ -134,7 +134,7 @@ class BackendLogAdmin(admin.ModelAdmin):
'display_created', 'execution_time',
)
list_display_links = ('id', 'backend')
- list_filter = ('state', 'backend', 'server')
+ list_filter = ('state', 'server', 'backend')
search_fields = ('script',)
date_hierarchy = 'created_at'
inlines = (BackendOperationInline,)
diff --git a/orchestra/contrib/orchestration/manager.py b/orchestra/contrib/orchestration/manager.py
index 44f19ed3..b5f6cfca 100644
--- a/orchestra/contrib/orchestration/manager.py
+++ b/orchestra/contrib/orchestration/manager.py
@@ -184,7 +184,7 @@ def collect(instance, action, **kwargs):
if update_fields is not None:
# TODO remove this, django does not execute post_save if update_fields=[]...
# Maybe open a ticket at Djangoproject ?
- # INITIAL INTENTION: "update_fileds=[]" is a convention for explicitly executing backend
+ # INITIAL INTENTION: "update_fields=[]" is a convention for explicitly executing backend
# i.e. account.disable()
if update_fields != []:
execute = False
diff --git a/orchestra/contrib/orchestration/methods.py b/orchestra/contrib/orchestration/methods.py
index 54a58916..8596b03c 100644
--- a/orchestra/contrib/orchestration/methods.py
+++ b/orchestra/contrib/orchestration/methods.py
@@ -123,7 +123,10 @@ def OpenSSH(backend, log, server, cmds, async=False):
exit_code = ssh.exit_code
if not log.exit_code:
log.exit_code = exit_code
- log.state = log.SUCCESS if exit_code == 0 else log.FAILURE
+ if exit_code == 255 and log.stderr.startswith('ssh: connect to host'):
+ log.state = log.TIMEOUT
+ else:
+ log.state = log.SUCCESS if exit_code == 0 else log.FAILURE
logger.debug('%s execution state on %s is %s' % (backend, server, log.state))
log.save()
except:
diff --git a/orchestra/contrib/saas/services/bscw.py b/orchestra/contrib/saas/services/bscw.py
index 6a45b759..1afd119e 100644
--- a/orchestra/contrib/saas/services/bscw.py
+++ b/orchestra/contrib/saas/services/bscw.py
@@ -22,4 +22,4 @@ class BSCWService(SoftwareService):
serializer = BSCWDataSerializer
icon = 'orchestra/icons/apps/BSCW.png'
site_domain = settings.SAAS_BSCW_DOMAIN
- change_readonly_fileds = ('email',)
+ change_readonly_fields = ('email',)
diff --git a/orchestra/contrib/saas/services/gitlab.py b/orchestra/contrib/saas/services/gitlab.py
index f62042ce..1586a77f 100644
--- a/orchestra/contrib/saas/services/gitlab.py
+++ b/orchestra/contrib/saas/services/gitlab.py
@@ -30,6 +30,6 @@ class GitLabService(SoftwareService):
change_form = GitLaChangeForm
serializer = GitLabSerializer
site_domain = settings.SAAS_GITLAB_DOMAIN
- change_readonly_fileds = ('email', 'user_id',)
+ change_readonly_fields = ('email', 'user_id',)
verbose_name = "GitLab"
icon = 'orchestra/icons/apps/gitlab.png'
diff --git a/orchestra/contrib/saas/services/options.py b/orchestra/contrib/saas/services/options.py
index 0819f75b..af2f75c5 100644
--- a/orchestra/contrib/saas/services/options.py
+++ b/orchestra/contrib/saas/services/options.py
@@ -49,8 +49,8 @@ class SoftwareService(plugins.Plugin, metaclass=plugins.PluginMount):
plugins.append(import_class(cls))
return plugins
- def get_change_readonly_fileds(cls):
- fields = super(SoftwareService, cls).get_change_readonly_fileds()
+ def get_change_readonly_fields(cls):
+ fields = super(SoftwareService, cls).get_change_readonly_fields()
return fields + ('name',)
def get_site_domain(self):
diff --git a/orchestra/contrib/saas/services/seafile.py b/orchestra/contrib/saas/services/seafile.py
index da736142..3e7ea594 100644
--- a/orchestra/contrib/saas/services/seafile.py
+++ b/orchestra/contrib/saas/services/seafile.py
@@ -28,4 +28,4 @@ class SeaFileService(SoftwareService):
serializer = SeaFileDataSerializer
icon = 'orchestra/icons/apps/seafile.png'
site_domain = settings.SAAS_SEAFILE_DOMAIN
- change_readonly_fileds = ('email',)
+ change_readonly_fields = ('email',)
diff --git a/orchestra/contrib/saas/services/wordpress.py b/orchestra/contrib/saas/services/wordpress.py
index ff8ad933..1e4d3f39 100644
--- a/orchestra/contrib/saas/services/wordpress.py
+++ b/orchestra/contrib/saas/services/wordpress.py
@@ -40,6 +40,6 @@ class WordPressService(SoftwareService):
change_form = WordPressChangeForm
serializer = WordPressDataSerializer
icon = 'orchestra/icons/apps/WordPress.png'
- change_readonly_fileds = ('email', 'blog_id')
+ change_readonly_fields = ('email', 'blog_id')
site_domain = settings.SAAS_WORDPRESS_DOMAIN
allow_custom_url = settings.SAAS_WORDPRESS_ALLOW_CUSTOM_URL
diff --git a/orchestra/contrib/webapps/backends/moodle.py b/orchestra/contrib/webapps/backends/moodle.py
index f140c6c5..9a14f7ef 100644
--- a/orchestra/contrib/webapps/backends/moodle.py
+++ b/orchestra/contrib/webapps/backends/moodle.py
@@ -67,7 +67,10 @@ class MoodleBackend(WebAppServiceMixin, ServiceController):
fi
rm %(app_path)s/.lock
chown -R %(user)s:%(group)s %(app_path)s
- su %(user)s --shell /bin/bash << 'EOF'
+ # Run install moodle cli command on the background, because it takes so long...
+ stdout=$(mktemp)
+ stderr=$(mktemp)
+ nohup su %(user)s --shell /bin/bash << 'EOF' > $stdout 2> $stderr &
php %(app_path)s/admin/cli/install_database.php \\
--fullname="%(site_name)s" \\
--shortname="%(site_name)s" \\
@@ -77,6 +80,14 @@ class MoodleBackend(WebAppServiceMixin, ServiceController):
--agree-license \\
--allow-unstable
EOF
+ pid=$!
+ sleep 2
+ if ! ps -p $pid > /dev/null; then
+ cat $stdout
+ cat $stderr >&2
+ exit_code=$(wait $pid)
+ fi
+ rm $stdout $stderr
""") % context
)
diff --git a/orchestra/contrib/webapps/types/cms.py b/orchestra/contrib/webapps/types/cms.py
index d3000354..80822cc2 100644
--- a/orchestra/contrib/webapps/types/cms.py
+++ b/orchestra/contrib/webapps/types/cms.py
@@ -51,12 +51,13 @@ class CMSApp(PHPApp):
""" Abstract AppType with common CMS functionality """
serializer = CMSAppSerializer
change_form = CMSAppForm
- change_readonly_fileds = ('db_name', 'db_user', 'password',)
+ change_readonly_fields = ('db_name', 'db_user', 'password',)
db_type = Database.MYSQL
abstract = True
+ db_prefix = 'cms_'
def get_db_name(self):
- db_name = 'wp_%s_%s' % (self.instance.name, self.instance.account)
+ db_name = '%s%s_%s' % (self.db_prefix, self.instance.name, self.instance.account)
# Limit for mysql database names
return db_name[:65]
diff --git a/orchestra/contrib/webapps/types/misc.py b/orchestra/contrib/webapps/types/misc.py
index f94b879a..81224e71 100644
--- a/orchestra/contrib/webapps/types/misc.py
+++ b/orchestra/contrib/webapps/types/misc.py
@@ -52,4 +52,4 @@ class SymbolicLinkApp(PHPApp):
form = SymbolicLinkForm
serializer = SymbolicLinkSerializer
icon = 'orchestra/icons/apps/SymbolicLink.png'
- change_readonly_fileds = ('path',)
+ change_readonly_fields = ('path',)
diff --git a/orchestra/contrib/webapps/types/moodle.py b/orchestra/contrib/webapps/types/moodle.py
index 9d61450a..54921dd6 100644
--- a/orchestra/contrib/webapps/types/moodle.py
+++ b/orchestra/contrib/webapps/types/moodle.py
@@ -13,6 +13,7 @@ class MoodleApp(CMSApp):
"The password will be visible in the 'password' field after the installer has finished."
)
icon = 'orchestra/icons/apps/Moodle.png'
+ db_prefix = 'modl_'
def get_detail(self):
return self.instance.data.get('php_version', '')
diff --git a/orchestra/contrib/webapps/types/wordpress.py b/orchestra/contrib/webapps/types/wordpress.py
index b8606246..0d02ff8f 100644
--- a/orchestra/contrib/webapps/types/wordpress.py
+++ b/orchestra/contrib/webapps/types/wordpress.py
@@ -13,6 +13,7 @@ class WordPressApp(CMSApp):
"The password will be visible in the 'password' field after the installer has finished."
)
icon = 'orchestra/icons/apps/WordPress.png'
+ db_prefix = 'wp_'
def get_detail(self):
return self.instance.data.get('php_version', '')
diff --git a/orchestra/contrib/websites/serializers.py b/orchestra/contrib/websites/serializers.py
index e08c12c4..89fd6ce8 100644
--- a/orchestra/contrib/websites/serializers.py
+++ b/orchestra/contrib/websites/serializers.py
@@ -54,7 +54,7 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
class Meta:
model = Website
fields = ('url', 'id', 'name', 'protocol', 'domains', 'is_active', 'contents', 'directives')
- postonly_fileds = ('name',)
+ postonly_fields = ('name',)
def validate(self, data):
""" Prevent multiples domains on the same protocol """
diff --git a/orchestra/forms/options.py b/orchestra/forms/options.py
index 36719af8..8d4875bf 100644
--- a/orchestra/forms/options.py
+++ b/orchestra/forms/options.py
@@ -82,7 +82,7 @@ class ReadOnlyFormMixin(object):
"""
Mixin class for ModelForm or Form that provides support for SpanField on readonly fields
Meta:
- readonly_fileds = (ro_field1, ro_field2)
+ readonly_fields = (ro_field1, ro_field2)
"""
def __init__(self, *args, **kwargs):
super(ReadOnlyFormMixin, self).__init__(*args, **kwargs)
diff --git a/orchestra/plugins/forms.py b/orchestra/plugins/forms.py
index a657d748..fcb50421 100644
--- a/orchestra/plugins/forms.py
+++ b/orchestra/plugins/forms.py
@@ -27,7 +27,7 @@ class PluginDataForm(forms.ModelForm):
self._meta.help_texts = {
self.plugin_field: plugin_help_text or model_help_text
}
- for field in self.plugin.get_change_readonly_fileds():
+ for field in self.plugin.get_change_readonly_fields():
value = getattr(self.instance, field, None) or self.instance.data.get(field)
display = value
foo_display = getattr(self.instance, 'get_%s_display' % field, None)
diff --git a/orchestra/plugins/options.py b/orchestra/plugins/options.py
index 7d0c5ca2..0ff197aa 100644
--- a/orchestra/plugins/options.py
+++ b/orchestra/plugins/options.py
@@ -9,7 +9,7 @@ class Plugin(object):
change_form = None
form = None
serializer = None
- change_readonly_fileds = ()
+ change_readonly_fields = ()
plugin_field = None
def __init__(self, instance=None):
@@ -52,8 +52,8 @@ class Plugin(object):
return sorted(choices, key=lambda e: e[1])
@classmethod
- def get_change_readonly_fileds(cls):
- return cls.change_readonly_fileds
+ def get_change_readonly_fields(cls):
+ return cls.change_readonly_fields
@classmethod
def get_class_path(cls):