From b48b27fb8c248fa312a0c72fb685c073ec2a8219 Mon Sep 17 00:00:00 2001 From: memo Date: Mon, 7 Dec 2015 13:26:50 +0000 Subject: [PATCH] Remove empty tabs in freezer dashboard remove empty tab for job edition remove possibility to add job without interval if start and end date are provided show a message when a job is started/stoped Resolves bug: 1523497 Change-Id: Iff39fc9e813969672eecd48d9084fd100bcf611c --- .pylintrc | 2 +- disaster_recovery/actions/workflows/action.py | 1 - disaster_recovery/api/api.py | 3 +- disaster_recovery/jobs/tables.py | 5 +- .../jobs/templates/jobs/_actions.html | 6 + disaster_recovery/jobs/urls.py | 4 + disaster_recovery/jobs/views.py | 28 +++- disaster_recovery/jobs/workflows/create.py | 14 +- .../jobs/workflows/update_actions.py | 65 ++++++++++ .../jobs/workflows/update_job.py | 122 ++++++++++++++++++ .../freezer/js/freezer.actions.action.js | 86 ++++++------ .../freezer/js/freezer.jobs.sortable.js | 3 +- 12 files changed, 282 insertions(+), 57 deletions(-) create mode 100644 disaster_recovery/jobs/workflows/update_actions.py create mode 100644 disaster_recovery/jobs/workflows/update_job.py diff --git a/.pylintrc b/.pylintrc index 72325a1..2b78dbf 100644 --- a/.pylintrc +++ b/.pylintrc @@ -44,7 +44,7 @@ symbols=no # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=E1002,W,C,R,method-hidden +disable=E1002,W,C,R,method-hidden,import-error diff --git a/disaster_recovery/actions/workflows/action.py b/disaster_recovery/actions/workflows/action.py index 08ff273..6f797e4 100644 --- a/disaster_recovery/actions/workflows/action.py +++ b/disaster_recovery/actions/workflows/action.py @@ -208,7 +208,6 @@ class ActionConfigurationAction(workflows.Action): def populate_action_choices(self, request, context): return [ - ('', _("Select an action")), ('backup', _("Backup")), ('restore', _("Restore")), ('admin', _("Admin")), diff --git a/disaster_recovery/api/api.py b/disaster_recovery/api/api.py index 72a0a5b..36d6fc2 100644 --- a/disaster_recovery/api/api.py +++ b/disaster_recovery/api/api.py @@ -335,7 +335,8 @@ class Action(object): action['freezer_action'].get('backup_name'), action['freezer_action'].get('path_to_backup') or action['freezer_action'].get('restore_abs_path'), - action['freezer_action'].get('storage') + action['freezer_action'].get('storage'), + mode=action['freezer_action'].get('mode') ) for action in actions] def get(self, job_id, json=False): diff --git a/disaster_recovery/jobs/tables.py b/disaster_recovery/jobs/tables.py index b7051d4..2a22dbc 100644 --- a/disaster_recovery/jobs/tables.py +++ b/disaster_recovery/jobs/tables.py @@ -19,6 +19,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy from horizon import tables +from horizon import messages from horizon.utils.urlresolvers import reverse import disaster_recovery.api.api as freezer_api @@ -93,7 +94,7 @@ class EditJob(tables.LinkAction): icon = "pencil" def get_link_url(self, datum=None): - return reverse("horizon:disaster_recovery:jobs:configure", + return reverse("horizon:disaster_recovery:jobs:edit_job", kwargs={'job_id': datum.job_id}) @@ -114,6 +115,7 @@ class StartJob(tables.Action): def single(self, table, request, job_id): freezer_api.Job(request).start(job_id) + messages.success(request, _("Job has started")) return shortcuts.redirect('horizon:disaster_recovery:jobs:index') @@ -123,6 +125,7 @@ class StopJob(tables.Action): def single(self, table, request, job_id): freezer_api.Job(request).stop(job_id) + messages.success(request, _("Job has stopped")) return shortcuts.redirect('horizon:disaster_recovery:jobs:index') diff --git a/disaster_recovery/jobs/templates/jobs/_actions.html b/disaster_recovery/jobs/templates/jobs/_actions.html index 762f2cd..df68a29 100644 --- a/disaster_recovery/jobs/templates/jobs/_actions.html +++ b/disaster_recovery/jobs/templates/jobs/_actions.html @@ -10,6 +10,12 @@ } +
+
+

Drag actions in the order they will be executed on the client.

+
+
+
diff --git a/disaster_recovery/jobs/urls.py b/disaster_recovery/jobs/urls.py index 2c1491b..4d465f8 100644 --- a/disaster_recovery/jobs/urls.py +++ b/disaster_recovery/jobs/urls.py @@ -33,6 +33,10 @@ urlpatterns = patterns( views.JobWorkflowView.as_view(), name='configure'), + url(r'^edit/(?P[^/]+)?$', + views.EditJobWorkflowView.as_view(), + name='edit_job'), + url(r'^edit_actions/(?P[^/]+)?$', views.ActionsInJobView.as_view(), name='edit_action'), diff --git a/disaster_recovery/jobs/views.py b/disaster_recovery/jobs/views.py index 2685a83..5c4a2d3 100644 --- a/disaster_recovery/jobs/views.py +++ b/disaster_recovery/jobs/views.py @@ -16,7 +16,8 @@ from horizon import browsers from horizon import workflows import workflows.create as configure_workflow -import workflows.actions as actions_workflow +import workflows.update_job as update_job_workflow +import workflows.update_actions as update_actions_workflow import disaster_recovery.api.api as freezer_api import disaster_recovery.jobs.browsers as project_browsers @@ -65,8 +66,31 @@ class JobWorkflowView(workflows.WorkflowView): return initial +class EditJobWorkflowView(workflows.WorkflowView): + workflow_class = update_job_workflow.UpdateJob + + @shield("Unable to get job", redirect="jobs:index") + def get_object(self): + return freezer_api.Job(self.request).get(self.kwargs['job_id']) + + def is_update(self): + return 'job_id' in self.kwargs and bool(self.kwargs['job_id']) + + @shield("Unable to get job", redirect="jobs:index") + def get_initial(self): + initial = super(EditJobWorkflowView, self).get_initial() + if self.is_update(): + initial.update({'job_id': None}) + job = freezer_api.Job(self.request).get(self.kwargs['job_id'], + json=True) + initial.update(**job) + initial.update(**job['job_schedule']) + + return initial + + class ActionsInJobView(workflows.WorkflowView): - workflow_class = actions_workflow.ConfigureActions + workflow_class = update_actions_workflow.UpdateActions @shield("Unable to get job", redirect="jobs:index") def get_object(self): diff --git a/disaster_recovery/jobs/workflows/create.py b/disaster_recovery/jobs/workflows/create.py index eda1e59..74c9e1b 100644 --- a/disaster_recovery/jobs/workflows/create.py +++ b/disaster_recovery/jobs/workflows/create.py @@ -141,12 +141,24 @@ class InfoConfigurationAction(workflows.Action): return False def _check_start_datetime(self, cleaned_data): - if cleaned_data.get('start_datetime') and not \ + if cleaned_data.get('schedule_start_date') and not \ self._validate_iso_format( cleaned_data.get('schedule_start_date')): msg = _("Start date time is not in ISO format.") self._errors['schedule_start_date'] = self.error_class([msg]) + if (cleaned_data.get('schedule_start_date') and + cleaned_data.get('schedule_end_date')) and\ + not cleaned_data.get('schedule_interval'): + msg = _("Please provide this value.") + self._errors['schedule_interval'] = self.error_class([msg]) + + if (cleaned_data.get('schedule_end_date') and + not cleaned_data.get('schedule_start_date')) and\ + not cleaned_data.get('schedule_interval'): + msg = _("Please provide this value.") + self._errors['schedule_start_date'] = self.error_class([msg]) + def _check_end_datetime(self, cleaned_data): if cleaned_data.get('end_datetime') and not \ self._validate_iso_format( diff --git a/disaster_recovery/jobs/workflows/update_actions.py b/disaster_recovery/jobs/workflows/update_actions.py new file mode 100644 index 0000000..8541f5c --- /dev/null +++ b/disaster_recovery/jobs/workflows/update_actions.py @@ -0,0 +1,65 @@ +# (c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from django import shortcuts +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import workflows + +import disaster_recovery.api.api as freezer_api + + +LOG = logging.getLogger(__name__) + + +class ActionsConfigurationAction(workflows.Action): + actions = forms.CharField( + required=False) + + job_id = forms.CharField( + required=False) + + class Meta(object): + name = _("Actions") + slug = "actions" + help_text_template = "disaster_recovery/jobs" \ + "/_actions.html" + + +class ActionsConfiguration(workflows.Step): + action_class = ActionsConfigurationAction + contributes = ('actions', 'job_id') + + +class UpdateActions(workflows.Workflow): + slug = "update_actions" + name = _("Update Actions") + finalize_button_name = _("Save") + success_message = _('Actions updated correctly.') + failure_message = _('Unable to update actions.') + success_url = "horizon:disaster_recovery:jobs:index" + default_steps = (ActionsConfiguration,) + + def handle(self, request, context): + try: + if context['job_id'] != '': + freezer_api.Job(request).update(context['job_id'], context) + return shortcuts.redirect('horizon:disaster_recovery:jobs:index') + except Exception: + exceptions.handle(request) + return False diff --git a/disaster_recovery/jobs/workflows/update_job.py b/disaster_recovery/jobs/workflows/update_job.py new file mode 100644 index 0000000..28d86b5 --- /dev/null +++ b/disaster_recovery/jobs/workflows/update_job.py @@ -0,0 +1,122 @@ +# (c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +import datetime + +from django import shortcuts +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import workflows + +import disaster_recovery.api.api as freezer_api + + +LOG = logging.getLogger(__name__) + + +class InfoConfigurationAction(workflows.Action): + + description = forms.CharField( + label=_("Job Name"), + help_text=_("Set a name for this job"), + required=True) + + job_id = forms.CharField( + widget=forms.HiddenInput(), + required=False) + + schedule_start_date = forms.CharField( + label=_("Start Date and Time"), + required=False) + + schedule_interval = forms.CharField( + label=_("Interval"), + required=False, + help_text=_("Repeat this configuration in a minutes interval.")) + + schedule_end_date = forms.CharField( + label=_("End Date and Time"), + required=False) + + def __init__(self, request, context, *args, **kwargs): + self.request = request + self.context = context + super(InfoConfigurationAction, self).__init__( + request, context, *args, **kwargs) + + def clean(self): + cleaned_data = super(InfoConfigurationAction, self).clean() + self._check_start_datetime(cleaned_data) + self._check_end_datetime(cleaned_data) + return cleaned_data + + def _validate_iso_format(self, start_date): + try: + return datetime.datetime.strptime( + start_date, "%Y-%m-%dT%H:%M:%S") + except ValueError: + return False + + def _check_start_datetime(self, cleaned_data): + if cleaned_data.get('start_datetime') and not \ + self._validate_iso_format( + cleaned_data.get('schedule_start_date')): + msg = _("Start date time is not in ISO format.") + self._errors['schedule_start_date'] = self.error_class([msg]) + + def _check_end_datetime(self, cleaned_data): + if cleaned_data.get('end_datetime') and not \ + self._validate_iso_format( + cleaned_data.get('schedule_end_date')): + msg = _("End date time is not in ISO format.") + self._errors['schedule_end_date'] = self.error_class([msg]) + + class Meta(object): + name = _("Job Info") + slug = "info" + help_text_template = "disaster_recovery/jobs" \ + "/_scheduling.html" + + +class InfoConfiguration(workflows.Step): + action_class = InfoConfigurationAction + contributes = ('description', + 'job_id', + 'actions', + 'schedule_start_date', + 'schedule_interval', + 'schedule_end_date') + + +class UpdateJob(workflows.Workflow): + slug = "update_job" + name = _("Update Job") + finalize_button_name = _("Save") + success_message = _('Job created correctly.') + failure_message = _('Unable to created job.') + success_url = "horizon:disaster_recovery:jobs:index" + default_steps = (InfoConfiguration,) + + def handle(self, request, context): + try: + if context['job_id'] != '': + freezer_api.Job(request).update(context['job_id'], context) + return shortcuts.redirect('horizon:disaster_recovery:jobs:index') + except Exception: + exceptions.handle(request) + return False diff --git a/disaster_recovery/static/freezer/js/freezer.actions.action.js b/disaster_recovery/static/freezer/js/freezer.actions.action.js index d5a96d7..b641939 100644 --- a/disaster_recovery/static/freezer/js/freezer.actions.action.js +++ b/disaster_recovery/static/freezer/js/freezer.actions.action.js @@ -75,31 +75,33 @@ function showRestoreOptions() { $("#id_storage").closest(".form-group").show(); } -function showNovaOptions() { - $("#id_mode").closest(".form-group").show(); - $("#id_nova_inst_id").closest(".form-group").show(); - $("#id_backup_name").closest(".form-group").show(); - $("#id_container").closest(".form-group").show(); -} - -function showCinderOptions() { - $("#id_mode").closest(".form-group").show(); - $("#id_cinder_vol_id").closest(".form-group").show(); - $("#id_backup_name").closest(".form-group").show(); - $("#id_container").closest(".form-group").show(); -} - function showSSHOptions() { $("#id_ssh_key").closest(".form-group").show(); $("#id_ssh_username").closest(".form-group").show(); $("#id_ssh_host").closest(".form-group").show(); } -hideEverything(); +function triggerChanges() { + $("#id_action").trigger('change'); + $("#id_mode").trigger('change'); + $("#id_storage").trigger('change'); +} + +function hideModeOptions() { + $("#id_cinder_vol_id").closest(".form-group").hide(); + $("#id_nova_inst_id").closest(".form-group").hide(); + $("#id_mysql_conf").closest(".form-group").hide(); + $("#id_sql_server_conf").closest(".form-group").hide(); +} + +function hideSSHOptions() { + $("#id_ssh_key").closest(".form-group").hide(); + $("#id_ssh_username").closest(".form-group").hide(); + $("#id_ssh_host").closest(".form-group").hide(); +} $("#id_action").change(function () { // Update the inputs according freezer action - if ($("#id_action").val() === 'backup') { hideEverything(); showBackupOptions(); @@ -117,22 +119,18 @@ $("#id_action").change(function () { $("#id_storage").change(function () { // Update the inputs according freezer storage backend - if ($("#id_storage").val() === 'swift') { - hideEverything(); + //hideEverything(); showBackupOptions(); - ("#id_mode").closest(".form-group").show(); + hideSSHOptions(); } else if ($("#id_storage").val() === 'ssh') { - hideEverything(); + //hideEverything(); showBackupOptions(); - $("#id_mode").closest(".form-group").show(); showSSHOptions(); } else if ($("#id_storage").val() === 'local') { - hideEverything(); + //hideEverything(); showBackupOptions(); - $("#id_mode").closest(".form-group").show(); - } else { - hideEverything(); + hideSSHOptions(); } }); @@ -141,37 +139,27 @@ $("#id_mode").change(function () { // Update the inputs according freezer mode if ($("#id_action").val() === 'backup') { if ($("#id_mode").val() === 'fs') { - hideEverything(); - showBackupOptions(); - $("#id_advanced_configuration").closest(".form-group").show(); + hideModeOptions(); } else if ($("#id_mode").val() === 'mysql') { - hideEverything(); - showBackupOptions(); + hideModeOptions(); $("#id_mysql_conf").closest(".form-group").show(); - $("#id_sql_server_conf").closest(".form-group").hide(); - $("#id_advanced_configuration").closest(".form-group").show(); } else if ($("#id_mode").val() === 'mssql') { - hideEverything(); - showBackupOptions(); + hideModeOptions(); $("#id_sql_server_conf").closest(".form-group").show(); - $("#id_mysql_conf").closest(".form-group").hide(); - $("#id_advanced_configuration").closest(".form-group").show(); } else if ($("#id_mode").val() === 'mongo') { - hideEverything(); - showBackupOptions(); - $("#id_sql_server_conf").closest(".form-group").hide(); - $("#id_mysql_conf").closest(".form-group").hide(); - $("#id_advanced_configuration").closest(".form-group").show(); + hideModeOptions(); } else if ($("#id_mode").val() === 'cinder') { - hideEverything(); - showCinderOptions(); - $("#id_cinder_vol_id").closest(".form-group").show().addClass("required"); - $("#id_advanced_configuration").closest(".form-group").show(); + hideModeOptions(); + $("#id_cinder_vol_id").closest(".form-group").show(); } else if ($("#id_mode").val() === 'nova') { - hideEverything(); - showNovaOptions(); - $("#id_nova_inst_id").closest(".form-group").show().addClass("required"); - $("#id_advanced_configuration").closest(".form-group").show(); + hideModeOptions(); + $("#id_nova_inst_id").closest(".form-group").show(); } } }); + + +$(function () { + hideEverything(); + triggerChanges(); +}); \ No newline at end of file diff --git a/disaster_recovery/static/freezer/js/freezer.jobs.sortable.js b/disaster_recovery/static/freezer/js/freezer.jobs.sortable.js index 33919a8..8f2f1da 100644 --- a/disaster_recovery/static/freezer/js/freezer.jobs.sortable.js +++ b/disaster_recovery/static/freezer/js/freezer.jobs.sortable.js @@ -14,7 +14,7 @@ var parent = $(".sortable_lists").parent(); parent.removeClass("col-sm-6"); parent.addClass("col-sm-12"); var siblings = parent.siblings(); -siblings.remove(); +siblings.hide(); $("form").submit(function (event) { @@ -29,6 +29,7 @@ $("form").submit(function (event) { var job_id = $('#id_job_id').val(); + function get_url() { var url = $(location).attr("origin"); url += '/disaster_recovery/api/actions/job/';