diff --git a/attract/static/js/tasks.js b/attract/static/js/tasks.js index 1f4db6f..2d048d7 100644 --- a/attract/static/js/tasks.js +++ b/attract/static/js/tasks.js @@ -20,6 +20,26 @@ return this; }; + /** + * Fades out the element, then erases its contents and shows the now-empty element again. + */ + $.fn.fadeOutAndClear = function(fade_speed) { + var target = this; + this + .fadeOut(fade_speed, function() { + target + .html('') + .show(); + }); + } + + $.fn.hideAndRemove = function(hide_speed, finished) { + this.slideUp(hide_speed, function() { + this.remove(); + if (typeof finished !== 'undefined') finished(); + }); + } + $.fn.removeClassPrefix = function(prefix) { this.each(function(i, el) { var classes = el.className.split(" ").filter(function(c) { @@ -31,6 +51,20 @@ }; }(jQuery)); +/** + * Removes the task from the task list and shot list, and show the 'task-add-link' + * when this was the last task in its category. + */ +function _remove_task_from_list(task_id) { + var $task_link = $('#task-' + task_id) + var $task_link_parent = $task_link.parent(); + $task_link.hideAndRemove(300, function() { + if ($task_link_parent.children('.task-link').length == 0) { + $task_link_parent.find('.task-add-link').removeClass('hidden'); + } + }); +} + /** * Shows a task in the #task-details div. */ @@ -220,6 +254,36 @@ function shot_save(shot_id, shot_url) { }); } +function task_delete(task_id, task_etag, task_delete_url) { + if (task_id === undefined || task_etag === undefined || task_delete_url === undefined) { + throw new ReferenceError("task_delete(" + task_id + ", " + task_etag + ", " + task_delete_url + ") called."); + } + + $.ajax({ + type: 'DELETE', + url: task_delete_url, + data: {'etag': task_etag} + }) + .done(function(e) { + if (console) console.log('Task', task_id, 'was deleted.'); + $('#task-details').fadeOutAndClear(); + _remove_task_from_list(task_id); + }) + .fail(function(xhr) { + $('#status-bar').text('Unable to delete task, code ' + xhr.status); + if (xhr.status == 412) { + alert('Someone else edited this task before you deleted it; refresh to try again.'); + // TODO: implement something nice here. Just make sure we don't throw + // away the user's edits. It's up to the user to handle this. + // TODO: refresh activity feed and point user to it. + } else { + // TODO: find a better place to put this error message, without overwriting the + // task the user is looking at in-place. + $('#task-view-feed').html(xhr.responseText); + } + }); +} + $(function() { $("a.shot-link[data-shot-id]").click(function(e) { e.preventDefault(); diff --git a/attract/tasks/__init__.py b/attract/tasks/__init__.py index 348a837..e9e69a4 100644 --- a/attract/tasks/__init__.py +++ b/attract/tasks/__init__.py @@ -85,6 +85,13 @@ class TaskManager(object): task.update(api=api) return task + def delete_task(self, task_id, etag): + api = pillar_api() + + self._log.info('Deleting task %s', task_id) + task = pillarsdk.Node({'_id': task_id, '_etag': etag}) + task.delete(api=api) + def setup_app(app): from . import eve_hooks diff --git a/attract/tasks/routes.py b/attract/tasks/routes.py index 75049d4..cef152b 100644 --- a/attract/tasks/routes.py +++ b/attract/tasks/routes.py @@ -22,6 +22,16 @@ def index(): return render_template('attract/tasks/index.html') +@blueprint.route('/', methods=['DELETE']) +def delete(task_id): + log.info('Deleting task %s', task_id) + + etag = request.form['etag'] + current_attract.task_manager.delete_task(task_id, etag) + + return '', 204 + + @perproject_blueprint.route('/', endpoint='index') @attract_project_view() def for_project(project, task_id=None): diff --git a/src/templates/attract/shots/for_project.jade b/src/templates/attract/shots/for_project.jade index 8cbf17e..3da8cba 100644 --- a/src/templates/attract/shots/for_project.jade +++ b/src/templates/attract/shots/for_project.jade @@ -48,11 +48,10 @@ href="{{ url_for('attract.shots.perproject.with_task', project_url=project.url, task_id=task._id) }}", class="status-{{ task.properties.status }} task-link") | {% endfor %} - | {% if not tasks_for_shots[shot._id][task_type] %} a.task-add( + class="task-add-link {% if tasks_for_shots[shot._id][task_type] %}hidden{% endif %}" href="javascript:task_create('{{ shot._id }}', '{{ project.url }}', '{{ task_type }}');") | + Task - | {% endif %} | {% endfor %} | {% endfor %} diff --git a/src/templates/attract/tasks/view_task_embed.jade b/src/templates/attract/tasks/view_task_embed.jade index 630054c..a576023 100644 --- a/src/templates/attract/tasks/view_task_embed.jade +++ b/src/templates/attract/tasks/view_task_embed.jade @@ -48,8 +48,11 @@ | {% endfor %} .input-transparent-group + | {% if 'DELETE' in task.allowed_methods %} + button.btn.btn-danger(type='button',onclick="task_delete('{{ task._id }}', '{{ task._etag }}', '{{ url_for('attract.tasks.delete', task_id=task._id, _method='DELETE') }}')") Delete + | {% endif %} | {% if 'PUT' in task.allowed_methods %} - button.btn.btn-default.btn-block(type='submit') Save Changes + button.btn.btn-default(type='submit') Save Changes | {% endif %} diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 89ff4bf..266af5f 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -102,3 +102,32 @@ class TaskWorkflowTest(AbstractAttractTest): json=remove_private_keys(json_task), auth_token='token', headers={'If-Match': json_task['_etag']}) + + @responses.activate + def test_delete_task(self): + task = self.create_task() + task_id = task['_id'] + + self.create_valid_auth_token(ctd.EXAMPLE_PROJECT_OWNER_ID, 'token') + node_url = '/api/nodes/%s' % task_id + self.get(node_url, auth_token='token') + + with self.app.test_request_context(): + # Log in as project admin user + pillar.auth.login_user(ctd.EXAMPLE_PROJECT_OWNER_ID) + + self.mock_blenderid_validate_happy() + self.assertRaises(sdk_exceptions.PreconditionFailed, + self.mngr.delete_task, + task._id, + 'jemoeder') + self.mngr.delete_task(task._id, task._etag) + + # Test directly with MongoDB + with self.app.test_request_context(): + nodes_coll = self.app.data.driver.db['nodes'] + found = nodes_coll.find_one(ObjectId(task_id)) + self.assertTrue(found['_deleted']) + + # Test with Eve + self.get(node_url, auth_token='token', expected_status=404)