Random fixes
This commit is contained in:
parent
523592dcad
commit
cbdac257a0
7
TODO.md
7
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>'
|
# 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
|
### Quick start
|
||||||
0. Install orchestra following any of these methods:
|
0. Install orchestra following any of these methods:
|
||||||
1. [PIP-only, Fast deployment setup (demo)](README.md#fast-deployment-setup)
|
1. [PIP-only, Fast deployment setup (demo)](README.md#fast-deployment-setup)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from functools import wraps, partial
|
from functools import wraps, partial
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import available_attrs
|
from django.utils.decorators import available_attrs
|
||||||
from django.utils.encoding import force_text
|
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 """
|
""" Wraps a function to be used as a ModelAdmin method field """
|
||||||
def admin_field_wrapper(*args, **kwargs):
|
def admin_field_wrapper(*args, **kwargs):
|
||||||
""" utility function for creating admin links """
|
""" 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['order'] = kwargs.get('order', kwargs['field'])
|
||||||
kwargs['popup'] = kwargs.get('popup', False)
|
kwargs['popup'] = kwargs.get('popup', False)
|
||||||
# TODO get field verbose name
|
# TODO get field verbose name
|
||||||
|
@ -38,7 +40,7 @@ def format_display_objects(modeladmin, request, queryset):
|
||||||
return objects
|
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'):
|
template='admin/orchestra/generic_confirmation.html'):
|
||||||
"""
|
"""
|
||||||
Generic pattern for actions that needs confirmation step
|
Generic pattern for actions that needs confirmation step
|
||||||
|
@ -46,9 +48,15 @@ def action_with_confirmation(action_name=None, extra_context={},
|
||||||
<input type="hidden" name="post" value="generic_confirmation" />
|
<input type="hidden" name="post" value="generic_confirmation" />
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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))
|
@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, '<br>'.join(e))
|
||||||
|
return
|
||||||
# The user has already confirmed the action.
|
# The user has already confirmed the action.
|
||||||
if request.POST.get('post') == 'generic_confirmation':
|
if request.POST.get('post') == 'generic_confirmation':
|
||||||
stay = func(modeladmin, request, queryset)
|
stay = func(modeladmin, request, queryset)
|
||||||
|
@ -82,7 +90,7 @@ def action_with_confirmation(action_name=None, extra_context={},
|
||||||
|
|
||||||
if callable(extra_context):
|
if callable(extra_context):
|
||||||
extra_context = extra_context(modeladmin, request, queryset)
|
extra_context = extra_context(modeladmin, request, queryset)
|
||||||
context.update(extra_context)
|
context.update(extra_context or {})
|
||||||
if 'display_objects' not in context:
|
if 'display_objects' not in context:
|
||||||
# Compute it only when necessary
|
# Compute it only when necessary
|
||||||
context['display_objects'] = format_display_objects(modeladmin, request, queryset)
|
context['display_objects'] = format_display_objects(modeladmin, request, queryset)
|
||||||
|
|
|
@ -4,6 +4,7 @@ from datetime import date
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.forms.models import modelformset_factory
|
from django.forms.models import modelformset_factory
|
||||||
|
@ -246,11 +247,16 @@ def copy_lines(modeladmin, request, queryset):
|
||||||
return move_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):
|
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 = []
|
amend_ids = []
|
||||||
for bill in queryset:
|
for bill in queryset:
|
||||||
with translation.override(bill.account.language):
|
with translation.override(bill.account.language):
|
||||||
|
|
|
@ -321,6 +321,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
if obj:
|
if obj:
|
||||||
if not obj.is_open:
|
if not obj.is_open:
|
||||||
exclude += ['close_bills', 'close_send_download_bills']
|
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]
|
return [action for action in actions if action.__name__ not in exclude]
|
||||||
|
|
||||||
def get_inline_instances(self, request, obj=None):
|
def get_inline_instances(self, request, obj=None):
|
||||||
|
|
|
@ -219,7 +219,14 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
display_mailboxes.allow_tags = True
|
display_mailboxes.allow_tags = True
|
||||||
|
|
||||||
def display_forward(self, address):
|
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 '<br>'.join(values)
|
return '<br>'.join(values)
|
||||||
display_forward.short_description = _("Forward")
|
display_forward.short_description = _("Forward")
|
||||||
display_forward.allow_tags = True
|
display_forward.allow_tags = True
|
||||||
|
|
|
@ -72,5 +72,5 @@ def send_pending(bulk=settings.MAILER_BULK_MESSAGES):
|
||||||
except OperationLocked:
|
except OperationLocked:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
if connection.connection is not None:
|
if 'connection' in vars() and connection.connection is not None:
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
|
@ -134,7 +134,7 @@ class BackendLogAdmin(admin.ModelAdmin):
|
||||||
'display_created', 'execution_time',
|
'display_created', 'execution_time',
|
||||||
)
|
)
|
||||||
list_display_links = ('id', 'backend')
|
list_display_links = ('id', 'backend')
|
||||||
list_filter = ('state', 'backend', 'server')
|
list_filter = ('state', 'server', 'backend')
|
||||||
search_fields = ('script',)
|
search_fields = ('script',)
|
||||||
date_hierarchy = 'created_at'
|
date_hierarchy = 'created_at'
|
||||||
inlines = (BackendOperationInline,)
|
inlines = (BackendOperationInline,)
|
||||||
|
|
|
@ -184,7 +184,7 @@ def collect(instance, action, **kwargs):
|
||||||
if update_fields is not None:
|
if update_fields is not None:
|
||||||
# TODO remove this, django does not execute post_save if update_fields=[]...
|
# TODO remove this, django does not execute post_save if update_fields=[]...
|
||||||
# Maybe open a ticket at Djangoproject ?
|
# 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()
|
# i.e. account.disable()
|
||||||
if update_fields != []:
|
if update_fields != []:
|
||||||
execute = False
|
execute = False
|
||||||
|
|
|
@ -123,7 +123,10 @@ def OpenSSH(backend, log, server, cmds, async=False):
|
||||||
exit_code = ssh.exit_code
|
exit_code = ssh.exit_code
|
||||||
if not log.exit_code:
|
if not log.exit_code:
|
||||||
log.exit_code = 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))
|
logger.debug('%s execution state on %s is %s' % (backend, server, log.state))
|
||||||
log.save()
|
log.save()
|
||||||
except:
|
except:
|
||||||
|
|
|
@ -22,4 +22,4 @@ class BSCWService(SoftwareService):
|
||||||
serializer = BSCWDataSerializer
|
serializer = BSCWDataSerializer
|
||||||
icon = 'orchestra/icons/apps/BSCW.png'
|
icon = 'orchestra/icons/apps/BSCW.png'
|
||||||
site_domain = settings.SAAS_BSCW_DOMAIN
|
site_domain = settings.SAAS_BSCW_DOMAIN
|
||||||
change_readonly_fileds = ('email',)
|
change_readonly_fields = ('email',)
|
||||||
|
|
|
@ -30,6 +30,6 @@ class GitLabService(SoftwareService):
|
||||||
change_form = GitLaChangeForm
|
change_form = GitLaChangeForm
|
||||||
serializer = GitLabSerializer
|
serializer = GitLabSerializer
|
||||||
site_domain = settings.SAAS_GITLAB_DOMAIN
|
site_domain = settings.SAAS_GITLAB_DOMAIN
|
||||||
change_readonly_fileds = ('email', 'user_id',)
|
change_readonly_fields = ('email', 'user_id',)
|
||||||
verbose_name = "GitLab"
|
verbose_name = "GitLab"
|
||||||
icon = 'orchestra/icons/apps/gitlab.png'
|
icon = 'orchestra/icons/apps/gitlab.png'
|
||||||
|
|
|
@ -49,8 +49,8 @@ class SoftwareService(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
plugins.append(import_class(cls))
|
plugins.append(import_class(cls))
|
||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
def get_change_readonly_fileds(cls):
|
def get_change_readonly_fields(cls):
|
||||||
fields = super(SoftwareService, cls).get_change_readonly_fileds()
|
fields = super(SoftwareService, cls).get_change_readonly_fields()
|
||||||
return fields + ('name',)
|
return fields + ('name',)
|
||||||
|
|
||||||
def get_site_domain(self):
|
def get_site_domain(self):
|
||||||
|
|
|
@ -28,4 +28,4 @@ class SeaFileService(SoftwareService):
|
||||||
serializer = SeaFileDataSerializer
|
serializer = SeaFileDataSerializer
|
||||||
icon = 'orchestra/icons/apps/seafile.png'
|
icon = 'orchestra/icons/apps/seafile.png'
|
||||||
site_domain = settings.SAAS_SEAFILE_DOMAIN
|
site_domain = settings.SAAS_SEAFILE_DOMAIN
|
||||||
change_readonly_fileds = ('email',)
|
change_readonly_fields = ('email',)
|
||||||
|
|
|
@ -40,6 +40,6 @@ class WordPressService(SoftwareService):
|
||||||
change_form = WordPressChangeForm
|
change_form = WordPressChangeForm
|
||||||
serializer = WordPressDataSerializer
|
serializer = WordPressDataSerializer
|
||||||
icon = 'orchestra/icons/apps/WordPress.png'
|
icon = 'orchestra/icons/apps/WordPress.png'
|
||||||
change_readonly_fileds = ('email', 'blog_id')
|
change_readonly_fields = ('email', 'blog_id')
|
||||||
site_domain = settings.SAAS_WORDPRESS_DOMAIN
|
site_domain = settings.SAAS_WORDPRESS_DOMAIN
|
||||||
allow_custom_url = settings.SAAS_WORDPRESS_ALLOW_CUSTOM_URL
|
allow_custom_url = settings.SAAS_WORDPRESS_ALLOW_CUSTOM_URL
|
||||||
|
|
|
@ -67,7 +67,10 @@ class MoodleBackend(WebAppServiceMixin, ServiceController):
|
||||||
fi
|
fi
|
||||||
rm %(app_path)s/.lock
|
rm %(app_path)s/.lock
|
||||||
chown -R %(user)s:%(group)s %(app_path)s
|
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 \\
|
php %(app_path)s/admin/cli/install_database.php \\
|
||||||
--fullname="%(site_name)s" \\
|
--fullname="%(site_name)s" \\
|
||||||
--shortname="%(site_name)s" \\
|
--shortname="%(site_name)s" \\
|
||||||
|
@ -77,6 +80,14 @@ class MoodleBackend(WebAppServiceMixin, ServiceController):
|
||||||
--agree-license \\
|
--agree-license \\
|
||||||
--allow-unstable
|
--allow-unstable
|
||||||
EOF
|
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
|
""") % context
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -51,12 +51,13 @@ class CMSApp(PHPApp):
|
||||||
""" Abstract AppType with common CMS functionality """
|
""" Abstract AppType with common CMS functionality """
|
||||||
serializer = CMSAppSerializer
|
serializer = CMSAppSerializer
|
||||||
change_form = CMSAppForm
|
change_form = CMSAppForm
|
||||||
change_readonly_fileds = ('db_name', 'db_user', 'password',)
|
change_readonly_fields = ('db_name', 'db_user', 'password',)
|
||||||
db_type = Database.MYSQL
|
db_type = Database.MYSQL
|
||||||
abstract = True
|
abstract = True
|
||||||
|
db_prefix = 'cms_'
|
||||||
|
|
||||||
def get_db_name(self):
|
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
|
# Limit for mysql database names
|
||||||
return db_name[:65]
|
return db_name[:65]
|
||||||
|
|
||||||
|
|
|
@ -52,4 +52,4 @@ class SymbolicLinkApp(PHPApp):
|
||||||
form = SymbolicLinkForm
|
form = SymbolicLinkForm
|
||||||
serializer = SymbolicLinkSerializer
|
serializer = SymbolicLinkSerializer
|
||||||
icon = 'orchestra/icons/apps/SymbolicLink.png'
|
icon = 'orchestra/icons/apps/SymbolicLink.png'
|
||||||
change_readonly_fileds = ('path',)
|
change_readonly_fields = ('path',)
|
||||||
|
|
|
@ -13,6 +13,7 @@ class MoodleApp(CMSApp):
|
||||||
"The password will be visible in the 'password' field after the installer has finished."
|
"The password will be visible in the 'password' field after the installer has finished."
|
||||||
)
|
)
|
||||||
icon = 'orchestra/icons/apps/Moodle.png'
|
icon = 'orchestra/icons/apps/Moodle.png'
|
||||||
|
db_prefix = 'modl_'
|
||||||
|
|
||||||
def get_detail(self):
|
def get_detail(self):
|
||||||
return self.instance.data.get('php_version', '')
|
return self.instance.data.get('php_version', '')
|
||||||
|
|
|
@ -13,6 +13,7 @@ class WordPressApp(CMSApp):
|
||||||
"The password will be visible in the 'password' field after the installer has finished."
|
"The password will be visible in the 'password' field after the installer has finished."
|
||||||
)
|
)
|
||||||
icon = 'orchestra/icons/apps/WordPress.png'
|
icon = 'orchestra/icons/apps/WordPress.png'
|
||||||
|
db_prefix = 'wp_'
|
||||||
|
|
||||||
def get_detail(self):
|
def get_detail(self):
|
||||||
return self.instance.data.get('php_version', '')
|
return self.instance.data.get('php_version', '')
|
||||||
|
|
|
@ -54,7 +54,7 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Website
|
model = Website
|
||||||
fields = ('url', 'id', 'name', 'protocol', 'domains', 'is_active', 'contents', 'directives')
|
fields = ('url', 'id', 'name', 'protocol', 'domains', 'is_active', 'contents', 'directives')
|
||||||
postonly_fileds = ('name',)
|
postonly_fields = ('name',)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
""" Prevent multiples domains on the same protocol """
|
""" Prevent multiples domains on the same protocol """
|
||||||
|
|
|
@ -82,7 +82,7 @@ class ReadOnlyFormMixin(object):
|
||||||
"""
|
"""
|
||||||
Mixin class for ModelForm or Form that provides support for SpanField on readonly fields
|
Mixin class for ModelForm or Form that provides support for SpanField on readonly fields
|
||||||
Meta:
|
Meta:
|
||||||
readonly_fileds = (ro_field1, ro_field2)
|
readonly_fields = (ro_field1, ro_field2)
|
||||||
"""
|
"""
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(ReadOnlyFormMixin, self).__init__(*args, **kwargs)
|
super(ReadOnlyFormMixin, self).__init__(*args, **kwargs)
|
||||||
|
|
|
@ -27,7 +27,7 @@ class PluginDataForm(forms.ModelForm):
|
||||||
self._meta.help_texts = {
|
self._meta.help_texts = {
|
||||||
self.plugin_field: plugin_help_text or model_help_text
|
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)
|
value = getattr(self.instance, field, None) or self.instance.data.get(field)
|
||||||
display = value
|
display = value
|
||||||
foo_display = getattr(self.instance, 'get_%s_display' % field, None)
|
foo_display = getattr(self.instance, 'get_%s_display' % field, None)
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Plugin(object):
|
||||||
change_form = None
|
change_form = None
|
||||||
form = None
|
form = None
|
||||||
serializer = None
|
serializer = None
|
||||||
change_readonly_fileds = ()
|
change_readonly_fields = ()
|
||||||
plugin_field = None
|
plugin_field = None
|
||||||
|
|
||||||
def __init__(self, instance=None):
|
def __init__(self, instance=None):
|
||||||
|
@ -52,8 +52,8 @@ class Plugin(object):
|
||||||
return sorted(choices, key=lambda e: e[1])
|
return sorted(choices, key=lambda e: e[1])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_change_readonly_fileds(cls):
|
def get_change_readonly_fields(cls):
|
||||||
return cls.change_readonly_fileds
|
return cls.change_readonly_fields
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_class_path(cls):
|
def get_class_path(cls):
|
||||||
|
|
Loading…
Reference in New Issue