From 113124e14b7977ea45d76d36aef3394bc25f96aa Mon Sep 17 00:00:00 2001 From: Amelia Cordwell Date: Mon, 24 Apr 2017 17:16:39 +1200 Subject: [PATCH] Task List Panel Main page allows viewing of all active, approved, completed and canceled tasks can click through to view full task details. Un-approved tasks can be resubmitted with new data, or approved, tasks can be cancelled. Change-Id: I35306fdf8e774d39b34c8c6fa50af5c70e010420 --- MANIFEST.in | 1 + stacktask_ui/api/stacktask.py | 95 +++++++++ stacktask_ui/content/tasks/__init__.py | 0 stacktask_ui/content/tasks/forms.py | 55 ++++++ stacktask_ui/content/tasks/panel.py | 23 +++ stacktask_ui/content/tasks/tables.py | 184 ++++++++++++++++++ stacktask_ui/content/tasks/tabs.py | 134 +++++++++++++ .../templates/tasks/_task_detail_actions.html | 17 ++ .../templates/tasks/_task_detail_notes.html | 15 ++ .../tasks/_task_detail_overview.html | 45 +++++ .../tasks/templates/tasks/_update.html | 8 + .../content/tasks/templates/tasks/index.html | 11 ++ .../content/tasks/templates/tasks/update.html | 6 + .../content/tasks/templatetags/__init__.py | 0 .../tasks/templatetags/task_filters.py | 18 ++ stacktask_ui/content/tasks/urls.py | 29 +++ stacktask_ui/content/tasks/views.py | 108 ++++++++++ .../enabled/_6090_management_task_list.py | 13 ++ 18 files changed, 762 insertions(+) create mode 100644 stacktask_ui/content/tasks/__init__.py create mode 100644 stacktask_ui/content/tasks/forms.py create mode 100644 stacktask_ui/content/tasks/panel.py create mode 100644 stacktask_ui/content/tasks/tables.py create mode 100644 stacktask_ui/content/tasks/tabs.py create mode 100644 stacktask_ui/content/tasks/templates/tasks/_task_detail_actions.html create mode 100644 stacktask_ui/content/tasks/templates/tasks/_task_detail_notes.html create mode 100644 stacktask_ui/content/tasks/templates/tasks/_task_detail_overview.html create mode 100644 stacktask_ui/content/tasks/templates/tasks/_update.html create mode 100644 stacktask_ui/content/tasks/templates/tasks/index.html create mode 100644 stacktask_ui/content/tasks/templates/tasks/update.html create mode 100644 stacktask_ui/content/tasks/templatetags/__init__.py create mode 100644 stacktask_ui/content/tasks/templatetags/task_filters.py create mode 100644 stacktask_ui/content/tasks/urls.py create mode 100644 stacktask_ui/content/tasks/views.py create mode 100644 stacktask_ui/enabled/_6090_management_task_list.py diff --git a/MANIFEST.in b/MANIFEST.in index 44bc8a0..4ee60e5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,5 +3,6 @@ include setup.py recursive-include stacktask_ui/content/default/templates * recursive-include stacktask_ui/content/forgotpassword/templates * recursive-include stacktask_ui/content/project_users/templates * +recursive-include stacktask_ui/content/tasks/templates * recursive-include stacktask_ui/content/token/templates * recursive-include stacktask_ui/static * diff --git a/stacktask_ui/api/stacktask.py b/stacktask_ui/api/stacktask.py index 4bf1c3c..468dbf3 100644 --- a/stacktask_ui/api/stacktask.py +++ b/stacktask_ui/api/stacktask.py @@ -20,6 +20,8 @@ from six.moves.urllib.parse import urljoin from django.conf import settings +from horizon.utils import functions as utils + from openstack_dashboard.api import base LOG = logging.getLogger(__name__) @@ -29,6 +31,12 @@ USER = collections.namedtuple('User', TOKEN = collections.namedtuple('Token', ['action']) +TASK = collections.namedtuple('Task', + ['id', 'task_type', 'valid', + 'request_by', 'request_project', + 'created_on', 'approved_on', 'page', + 'completed_on', 'actions', 'status']) + def _get_endpoint_url(request): # If the request is made by an anonymous user, this endpoint request fails. @@ -230,3 +238,90 @@ def forgotpassword_submit(request, data): except Exception as e: LOG.error(e) raise + + +def task_list(request, filters={}, page=1): + tasks_per_page = utils.get_page_size(request) + tasklist = [] + prev = more = False + try: + headers = {"Content-Type": "application/json", + 'X-Auth-Token': request.user.token.id} + params = { + "filters": json.dumps(filters), + "page": page, + "tasks_per_page": tasks_per_page + } + resp = get(request, "tasks", params=params, data=json.dumps({}), + headers=headers).json() + prev = resp['has_prev'] + more = resp['has_more'] + for task in resp['tasks']: + tasklist.append(task_obj_get(request, task=task, page=page)) + except Exception as e: + LOG.error(e) + raise + + return tasklist, prev, more + + +def task_get(request, task_id): + # Get a single task + headers = {"Content-Type": "application/json", + 'X-Auth-Token': request.user.token.id} + + return get(request, "tasks/%s" % task_id, + headers=headers) + + +def task_obj_get(request, task_id=None, task=None, page=0): + if not task: + task = task_get(request, task_id) + + status = "Awaiting Approval" + if task['cancelled']: + status = "Cancelled" + elif task['completed_on']: + status = "Completed" + elif task['approved_on']: + status = "Approved; Incomplete" + + valid = False not in [action['valid'] for + action in task['actions']] + return TASK( + id=task['uuid'], + task_type=task['task_type'], + valid=valid, + request_by=task['keystone_user'].get('username'), + request_project=task['keystone_user'].get('project_name'), + status=status, + created_on=task['created_on'], + approved_on=task['approved_on'], + completed_on=task['completed_on'], + actions=task['actions'], + page=page + ) + + +def task_cancel(request, task_id): + headers = {"Content-Type": "application/json", + 'X-Auth-Token': request.user.token.id} + + return delete(request, "tasks/%s" % task_id, + headers=headers) + + +def task_approve(request, task_id): + headers = {"Content-Type": "application/json", + 'X-Auth-Token': request.user.token.id} + + return post(request, "tasks/%s" % task_id, + data=json.dumps({"approved": True}), headers=headers) + + +def task_update(request, task_id, new_data): + headers = {"Content-Type": "application/json", + 'X-Auth-Token': request.user.token.id} + + return put(request, "tasks/%s" % task_id, + data=new_data, headers=headers) diff --git a/stacktask_ui/content/tasks/__init__.py b/stacktask_ui/content/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/content/tasks/forms.py b/stacktask_ui/content/tasks/forms.py new file mode 100644 index 0000000..4b1abbd --- /dev/null +++ b/stacktask_ui/content/tasks/forms.py @@ -0,0 +1,55 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.utils.translation import ugettext_lazy as _ + +from horizon import forms +from horizon import messages + +import json + +from stacktask_ui.api import stacktask + + +class UpdateTaskForm(forms.SelfHandlingForm): + task_id = forms.CharField(widget=forms.HiddenInput()) + task_type = forms.CharField(widget=forms.TextInput( + attrs={'readonly': True})) + task_data = forms.CharField(widget=forms.Textarea) + + def clean_task_data(self): + taskdata = self.cleaned_data['task_data'] + try: + json.loads(taskdata) + except ValueError: + raise forms.ValidationError( + "Invalid non-JSON data in Task Data field") + + return taskdata + + def handle(self, request, data): + task_id = self.cleaned_data.pop('task_id') + try: + response = stacktask.task_update( + request, task_id, data['task_data']) + if response.status_code == 200: + messages.success(request, _('Updated task successfully.')) + elif response.status_code == 400: + messages.error(request, _(response.text)) + else: + messages.error(request, _('Failed to update task.')) + return True + except Exception: + messages.error(request, _('Failed to update task.')) + return False diff --git a/stacktask_ui/content/tasks/panel.py b/stacktask_ui/content/tasks/panel.py new file mode 100644 index 0000000..abbfeb1 --- /dev/null +++ b/stacktask_ui/content/tasks/panel.py @@ -0,0 +1,23 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class TaskList(horizon.Panel): + name = _('Tasks') + slug = 'tasks' + policy_rules = (("identity", "role:admin"),) diff --git a/stacktask_ui/content/tasks/tables.py b/stacktask_ui/content/tasks/tables.py new file mode 100644 index 0000000..1263bbf --- /dev/null +++ b/stacktask_ui/content/tasks/tables.py @@ -0,0 +1,184 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import exceptions +from horizon import tables + +from stacktask_ui.api import stacktask + + +class CancelTask(tables.DeleteAction): + help_text = _("This will cancel all selected tasks.") + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Cancel Task", + u"Cancel Tasks", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Cancelled Task", + u"Cancelled Tasks", + count + ) + + def delete(self, request, obj_id): + result = stacktask.task_cancel(request, obj_id) + if not result or result.status_code != 200: + exception = exceptions.NotAvailable() + exception._safe_message = False + raise exception + + def allowed(self, request, task=None): + if task: + return not( + task.status == "Completed" or task.status == "Cancelled") + return True + + +class ApproveTask(tables.BatchAction): + name = "approve" + help_text = _("This will approve all of the selected tasks.") + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Approve Task", + u"Approve Tasks", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Approved Task", + u"Approved Tasks", + count + ) + + def action(self, request, obj_id): + result = stacktask.task_approve(request, obj_id) + if not result or result.status_code != 200: + exception = exceptions.NotAvailable() + exception._safe_message = False + raise exception + + def allowed(self, request, task=None): + if task: + return not( + task.status == "Completed" or task.status == "Cancelled") + return True + + +class ReapproveTask(ApproveTask): + name = "approve" + help_text = _("This will approve all of the selected tasks.") + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Reapprove Task", + u"Repprove Tasks", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Reapproved Task", + u"Reapproved Tasks", + count + ) + + +class UpdateTask(tables.LinkAction): + name = "update" + verbose_name = _("Update Task") + url = "horizon:management:tasks:update" + classes = ("ajax-modal",) + + def allowed(self, request, task=None): + if task: + return task.status == 'Awaiting Approval' + return True + + +def TaskTypeDisplayFilter(task_type): + return task_type.replace("_", " ").title() + + +class TaskTable(tables.DataTable): + uuid = tables.Column('id', verbose_name=_('Task ID'), + hidden=True) + task_type = tables.Column('task_type', verbose_name=_('Task Type'), + filters=[TaskTypeDisplayFilter], + link="horizon:management:tasks:detail") + status = tables.Column('status', verbose_name=_('Status')) + request_by = tables.Column('request_by', verbose_name=_('Requestee')) + request_project = tables.Column('request_project', + verbose_name=_('Request Project')) + valid = tables.Column('valid', verbose_name=_("Actions Valid")) + created_on = tables.Column('created_on', + verbose_name=_('Request Date')) + page = tables.Column('page', hidden=True) + + class Meta(object): + name = 'task_table' + verbose_name = _('Tasks') + table_actions = (CancelTask, ApproveTask) + row_actions = (ApproveTask, UpdateTask, CancelTask) + prev_pagination_param = pagination_param = 'task_page' + + def get_prev_marker(self): + return str(int(self.data[0].page) - 1) if self.data else '' + + def get_marker(self): + return str(int(self.data[0].page) + 1) if self.data else '' + + def get_object_display(self, obj): + task_type = obj.task_type.replace("_", " ").title() + return "%s (%s)" % (task_type, obj.id) + + +class ApprovedTaskTable(TaskTable): + + class Meta(object): + name = 'approved_table' + verbose_name = _('Tasks') + table_actions = (CancelTask, ReapproveTask) + row_actions = (CancelTask, ReapproveTask) + prev_pagination_param = pagination_param = 'approved_page' + + +class CompletedTaskTable(TaskTable): + + class Meta(object): + name = 'completed_table' + verbose_name = _('Tasks') + prev_pagination_param = pagination_param = 'completed_page' + + +class CancelledTaskTable(TaskTable): + + class Meta(object): + name = 'cancelled_table' + verbose_name = _('Tasks') + prev_pagination_param = pagination_param = 'cancelled_page' diff --git a/stacktask_ui/content/tasks/tabs.py b/stacktask_ui/content/tasks/tabs.py new file mode 100644 index 0000000..332a997 --- /dev/null +++ b/stacktask_ui/content/tasks/tabs.py @@ -0,0 +1,134 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tabs + +from stacktask_ui.content.tasks import tables as task_tables +from stacktask_ui.api import stacktask + + +class ActiveTaskListTab(tabs.TableTab): + table_classes = (task_tables.TaskTable,) + template_name = 'horizon/common/_detail_table.html' + page_title = _("Active Tasks") + name = _('Active') + slug = 'active' + filters = {'cancelled': {'exact': False}, + 'approved': {'exact': False}} + _prev = False + _more = False + + def get_task_table_data(self): + tasks = [] + marker = self.request.GET.get( + self.table_classes[0]._meta.pagination_param, 1) + try: + tasks, self._prev, self._more = stacktask.task_list( + self.request, filters=self.filters, page=marker) + except Exception: + exceptions.handle(self.request, _('Failed to list tasks.')) + return tasks + + def has_prev_data(self, table): + return self._prev + + def has_more_data(self, table): + return self._more + + +class ApprovedTaskListTab(ActiveTaskListTab): + table_classes = (task_tables.ApprovedTaskTable,) + page_title = _("Approved Tasks") + name = _('Approved') + slug = 'approved' + filters = {'cancelled': {'exact': False}, + 'approved': {'exact': True}, + 'completed': {'exact': False}} + + def get_approved_table_data(self): + return super(ApprovedTaskListTab, self).get_task_table_data() + + +class CompletedTaskListTab(ActiveTaskListTab): + table_classes = (task_tables.CompletedTaskTable,) + page_title = _("Completed Tasks") + name = _('Completed') + slug = 'completed' + filters = {'completed': {'exact': True}} + + def get_completed_table_data(self): + return super(CompletedTaskListTab, self).get_task_table_data() + + +class CancelledTaskListTab(ActiveTaskListTab): + table_classes = (task_tables.CancelledTaskTable,) + name = _('Cancelled') + slug = 'cancelled' + filters = {'cancelled': {'exact': True}} + + def get_cancelled_table_data(self): + return super(CancelledTaskListTab, self).get_task_table_data() + + +class TaskTabs(tabs.TabGroup): + slug = "tasks" + tabs = (ActiveTaskListTab, ApprovedTaskListTab, CompletedTaskListTab, + CancelledTaskListTab) + sticky = True + + def get_selected_tab(self): + super(TaskTabs, self).get_selected_tab() + if not self._selected: + for tab in self.tabs: + param = tab.table_classes[0]._meta.pagination_param + if self.request.GET.get(param): + self._selected = self.get_tab(tab.slug) + return self._selected + + +class TaskOverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = 'management/tasks/_task_detail_overview.html' + + def get_context_data(self, request): + print self.tab_group + print self.tab_group.kwargs + return {"task": self.tab_group.kwargs['task']} + + +class TaskActionsTab(tabs.Tab): + name = _("Actions") + slug = "actions" + template_name = 'management/tasks/_task_detail_actions.html' + + def get_context_data(self, request): + return {"task": self.tab_group.kwargs['task']} + + +class TaskNotesTab(tabs.Tab): + name = _("Action Notes") + slug = "notes" + template_name = 'management/tasks/_task_detail_notes.html' + + def get_context_data(self, request): + return {"task": self.tab_group.kwargs['task']} + + +class TaskDetailTabs(tabs.DetailTabsGroup): + slug = "task_details" + tabs = (TaskOverviewTab, TaskActionsTab, TaskNotesTab) diff --git a/stacktask_ui/content/tasks/templates/tasks/_task_detail_actions.html b/stacktask_ui/content/tasks/templates/tasks/_task_detail_actions.html new file mode 100644 index 0000000..96779ce --- /dev/null +++ b/stacktask_ui/content/tasks/templates/tasks/_task_detail_actions.html @@ -0,0 +1,17 @@ +{% load i18n sizeformat %} +{% load task_filters %} + +
+

{% trans "Actions" %}

+
+ {% for action in task.actions %} +
{{ action.action_name }}
+
+
+
{% trans "Valid" %}
+
{{ action.valid }}
+
{% trans "Data" %}
+
{{ action.data|pretty_json }}
+
+ {% endfor %} +
diff --git a/stacktask_ui/content/tasks/templates/tasks/_task_detail_notes.html b/stacktask_ui/content/tasks/templates/tasks/_task_detail_notes.html new file mode 100644 index 0000000..7bff222 --- /dev/null +++ b/stacktask_ui/content/tasks/templates/tasks/_task_detail_notes.html @@ -0,0 +1,15 @@ +{% load i18n sizeformat %} +{% load task_filters %} + +
+

{% trans "Action Notes" %}

+
+
+ {% for action, notes in task.action_notes.iteritems %} +
{{ action }}
+ {% for note in notes %} +
{{ note }}
+ {% endfor %} + {% endfor %} +
+
diff --git a/stacktask_ui/content/tasks/templates/tasks/_task_detail_overview.html b/stacktask_ui/content/tasks/templates/tasks/_task_detail_overview.html new file mode 100644 index 0000000..a98b50b --- /dev/null +++ b/stacktask_ui/content/tasks/templates/tasks/_task_detail_overview.html @@ -0,0 +1,45 @@ +{% load i18n sizeformat %} +{% load task_filters %} + +
+

{% trans "Task Details" %}

+
+
+
{% trans "Task Type" %}
+
{{ task.task_type }}
+
{% trans "ID" %}
+
{{ task.uuid }}
+
{% trans "Cancelled" %}
+
{{ task.cancelled }}
+
{% trans "Approved" %}
+
{{ task.approved }}
+
{% trans "Completed" %}
+
{{ task.completed }}
+ {% if task.approved_by %} +
{% trans "Approved By" %}
+
{{ task.approved_by.username }}
+ {% endif %} +
{% trans "Requested on" %}
+
{{ task.created_on }}
+
{% trans "Approved on" %}
+
{% if task.approved_on %}{{ task.approved_on}}{% else %}-{% endif %}
+
{% trans "Completed on" %}
+
{% if task.completed_on %}{{ task.completed_on}}{% else %}-{% endif %}
+
+ + + {% if task.keystone_user %} +

{% trans "Requestee" %}

+
+
+
{% trans "Username" %}
+
{{ task.keystone_user.username }}
+
{% trans "Project" %}
+
{{ task.keystone_user.project_name }}
+
{% trans "Roles" %}
+
{{ task.keystone_user.roles|pretty_list }}
+
{% trans "IP Address" %}
+
{{ task.ip_address }}
+
+ {% endif %} +
diff --git a/stacktask_ui/content/tasks/templates/tasks/_update.html b/stacktask_ui/content/tasks/templates/tasks/_update.html new file mode 100644 index 0000000..0093577 --- /dev/null +++ b/stacktask_ui/content/tasks/templates/tasks/_update.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal_id %}update_user_form{% endblock %} + +{% block modal-body %} + {% include "horizon/common/_form_fields.html" %} +{% endblock %} diff --git a/stacktask_ui/content/tasks/templates/tasks/index.html b/stacktask_ui/content/tasks/templates/tasks/index.html new file mode 100644 index 0000000..fc3f191 --- /dev/null +++ b/stacktask_ui/content/tasks/templates/tasks/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Tasks" %}{% endblock %} + +{% block main %} +
+
+ {{ tab_group.render }} +
+
+{% endblock %} diff --git a/stacktask_ui/content/tasks/templates/tasks/update.html b/stacktask_ui/content/tasks/templates/tasks/update.html new file mode 100644 index 0000000..1a28f1d --- /dev/null +++ b/stacktask_ui/content/tasks/templates/tasks/update.html @@ -0,0 +1,6 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update User" %}{% endblock %} +{% block main %} + {% include 'management/tasks/_update.html' %} +{% endblock %} diff --git a/stacktask_ui/content/tasks/templatetags/__init__.py b/stacktask_ui/content/tasks/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/content/tasks/templatetags/task_filters.py b/stacktask_ui/content/tasks/templatetags/task_filters.py new file mode 100644 index 0000000..68df58c --- /dev/null +++ b/stacktask_ui/content/tasks/templatetags/task_filters.py @@ -0,0 +1,18 @@ +from django import template +import json + +register = template.Library() + + +@register.simple_tag +def pretty_json(value): + return json.dumps(value, indent=4) + + +@register.simple_tag +def pretty_list(value): + return ', '.join(value) + + +register.filter('pretty_json', pretty_json) +register.filter('pretty_list', pretty_list) diff --git a/stacktask_ui/content/tasks/urls.py b/stacktask_ui/content/tasks/urls.py new file mode 100644 index 0000000..4e3b488 --- /dev/null +++ b/stacktask_ui/content/tasks/urls.py @@ -0,0 +1,29 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.conf.urls import patterns +from django.conf.urls import url + +from stacktask_ui.content.tasks import views + + +urlpatterns = patterns( + '', + url(r'^(?P[^/]+)/$', + views.TaskDetailView.as_view(), + name='detail'), + url(r'^(?P[^/]+)/update/$', + views.UpdateTaskView.as_view(), name='update'), + url(r'^$', views.IndexView.as_view(), name='index'), +) diff --git a/stacktask_ui/content/tasks/views.py b/stacktask_ui/content/tasks/views.py new file mode 100644 index 0000000..aa6d49f --- /dev/null +++ b/stacktask_ui/content/tasks/views.py @@ -0,0 +1,108 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import tabs + +from horizon.utils import memoized + +from stacktask_ui.content.tasks import tables as task_tables +from stacktask_ui.content.tasks import forms as task_forms +from stacktask_ui.content.tasks import tabs as task_tabs +from stacktask_ui.api import stacktask + +import json + + +class IndexView(tabs.TabbedTableView): + tab_group_class = task_tabs.TaskTabs + template_name = 'management/tasks/index.html' + redirect_url = 'horizon:management:tasks:index' + page_title = _("Tasks") + + +class TaskDetailView(tabs.TabView): + tab_group_class = task_tabs.TaskDetailTabs + template_name = 'horizon/common/_detail.html' + redirect_url = 'horizon:management:tasks:index' + page_title = "{{ task.task_type }}" + + def get_context_data(self, **kwargs): + context = super(TaskDetailView, self).get_context_data(**kwargs) + task = self.get_data() + context["task"] = task + context["url"] = reverse(self.redirect_url) + context["actions"] = self._get_actions( + stacktask.task_obj_get(self.request, task=task)) + return context + + def _get_actions(self, task): + table = task_tables.TaskTable(self.request) + return table.render_row_actions(task) + + @memoized.memoized_method + def get_data(self): + return stacktask.task_get(self.request, self.kwargs['task_id']).json() + + def get_tabs(self, request, *args, **kwargs): + task = self.get_data() + return self.tab_group_class(request, task=task, **kwargs) + + +class UpdateTaskView(forms.ModalFormView): + form_class = task_forms.UpdateTaskForm + form_id = "update_user_form" + modal_header = _("Update Task") + submit_label = _("Update") + submit_url = 'horizon:management:tasks:update' + template_name = 'management/tasks/update.html' + context_object_name = 'project_users' + success_url = "horizon:management:tasks:detail" + page_title = _("Update Task") + + def get_success_url(self): + return reverse(self.success_url, + args=(self.kwargs['task_id'],)) + + @memoized.memoized_method + def get_object(self): + try: + return stacktask.task_get(self.request, + self.kwargs['task_id']).json() + except Exception: + msg = _('Unable to retrieve user.') + url = reverse('horizon:management:tasks:index') + exceptions.handle(self.request, msg, redirect=url) + + def get_context_data(self, **kwargs): + context = super(UpdateTaskView, self).get_context_data(**kwargs) + context['task'] = self.get_object() + args = (self.kwargs['task_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + task = self.get_object() + task_data = {} + for data in [action['data'] for action in task['actions']]: + task_data.update(data) + data = {'task_id': self.kwargs['task_id'], + 'task_type': task['task_type'], + 'task_data': json.dumps(task_data, indent=4), + } + return data diff --git a/stacktask_ui/enabled/_6090_management_task_list.py b/stacktask_ui/enabled/_6090_management_task_list.py new file mode 100644 index 0000000..dd01064 --- /dev/null +++ b/stacktask_ui/enabled/_6090_management_task_list.py @@ -0,0 +1,13 @@ +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'tasks' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'management' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'default' + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'stacktask_ui.content.tasks.panel.TaskList' + +ADD_INSTALLED_APPS = [ + 'stacktask_ui.content.tasks' +]