def publish(self, request, pk=None): if not pk: raise Http404 logging.debug("Entering the publish channel endpoint") channel = self.get_edit_object() if (not channel.main_tree.get_descendants(include_self=True).filter( changed=True).exists()): raise ValidationError("Cannot publish an unchanged channel") channel.main_tree.publishing = True channel.main_tree.save() version_notes = request.data.get("version_notes") task_args = { "user_id": request.user.pk, "channel_id": channel.id, "version_notes": version_notes, "language": get_language(), } create_async_task("export-channel", request.user, **task_args) return Response("")
def publish_channel(request): logging.debug("Entering the publish_channel endpoint") if request.method != 'POST': return HttpResponseBadRequest( "Only POST requests are allowed on this endpoint.") data = json.loads(request.body) try: channel_id = data["channel_id"] request.user.can_edit(channel_id) task_info = { 'user': request.user, 'metadata': { 'affects': { 'channels': [channel_id] } } } task_args = { 'user_id': request.user.pk, 'channel_id': channel_id, 'version_notes': data.get('version_notes') } task, task_info = create_async_task('export-channel', task_info, task_args) return HttpResponse(JSONRenderer().render( TaskSerializer(task_info).data)) except KeyError: raise ObjectDoesNotExist( "Missing attribute from data: {}".format(data))
def sync(self, request, pk=None): if not pk: raise Http404 logging.debug("Entering the sync channel endpoint") channel = self.get_edit_object() if ( not channel.main_tree.get_descendants() .filter( Q(original_node__isnull=False) | Q( original_channel_id__isnull=False, original_source_node_id__isnull=False, ) ) .exists() ): raise ValidationError("Cannot sync a channel with no imported content") data = request.data task_args = { "user_id": request.user.pk, "channel_id": channel.id, "sync_attributes": data.get("attributes"), "sync_tags": data.get("tags"), "sync_files": data.get("files"), "sync_assessment_items": data.get("assessment_items"), } _, task_info = create_async_task("sync-channel", request.user, **task_args) return Response({ 'changes': [self.create_task_event(task_info)] })
def move(self, pk, target=None, position=None): try: contentnode = self.get_edit_queryset().get(pk=pk) except ContentNode.DoesNotExist: error = ValidationError("Specified node does not exist") return str(error), None try: target, position = self.validate_targeting_args(target, position) channel_id = target.channel_id task_args = { "user_id": self.request.user.id, "channel_id": channel_id, "node_id": contentnode.id, "target_id": target.id, "position": position, } task, task_info = create_async_task("move-nodes", self.request.user, **task_args) return ( None, None, ) except ValidationError as e: return str(e), None
def delete_from_changes(self, changes): errors = [] changes_to_return = [] queryset = self.get_edit_queryset().order_by() for change in changes: try: instance = queryset.get( **dict(self.values_from_key(change["key"]))) task_args = { "user_id": self.request.user.id, "channel_id": instance.channel_id, "node_id": instance.id, } task, task_info = create_async_task("delete-node", self.request.user, **task_args) except ContentNode.DoesNotExist: # If the object already doesn't exist, as far as the user is concerned # job done! pass except Exception as e: log_sync_exception(e) change["errors"] = [str(e)] errors.append(change) return errors, changes_to_return
def test_asynctask_reports_error(self): """ Tests that if a task fails with an error, that the error information is stored in the Task object for later retrieval and analysis. """ metadata = {'test': True} task_options = { 'user_id': self.user.pk, 'task_type': 'asynctask', 'metadata': metadata } task, task_info = create_async_task('error-test', task_options) task = Task.objects.get(task_id=task.id) self.assertEqual(task.status, 'FAILURE') self.assertTrue('error' in task.metadata) error = task.metadata['error'] self.assertItemsEqual(list(error.keys()), ['task_args', 'task_kwargs', 'traceback']) self.assertEqual(len(error['task_args']), 0) self.assertEqual(len(error['task_kwargs']), 0) traceback_string = '\n'.join(error['traceback']) self.assertTrue("Exception" in traceback_string) self.assertTrue( "I'm sorry Dave, I'm afraid I can't do that." in traceback_string)
def test_duplicate_nodes_task(self): ids = [] node_ids = [] for i in range(3, 6): node_id = "0000000000000000000000000000000" + str(i) node_ids.append(node_id) node = ContentNode.objects.get(node_id=node_id) ids.append(node.pk) parent_node = ContentNode.objects.get( node_id="00000000000000000000000000000002") tasks = [] for source_id in ids: task_args = { "user_id": self.user.pk, "channel_id": self.channel.pk, "source_id": source_id, "target_id": parent_node.pk, "pk": uuid.uuid4().hex, } task, task_info = create_async_task("duplicate-nodes", self.user, apply_async=False, **task_args) tasks.append((task_args, task_info)) for task_args, task_info in tasks: # progress is retrieved dynamically upon calls to get the task info, so # use an API call rather than checking the db directly for progress. url = reverse("task-detail", kwargs={"task_id": task_info.task_id}) response = self.get(url) assert (response.data["status"] == states.SUCCESS ), "Task failed, exception: {}".format( response.data["metadata"]["error"]["traceback"]) self.assertEqual(response.data["status"], states.SUCCESS) self.assertEqual(response.data["task_type"], "duplicate-nodes") result = response.data["metadata"]["result"] node_id = ContentNode.objects.get(pk=task_args["pk"]).node_id self.assertEqual( result["changes"][0], generate_update_event(task_args["pk"], CONTENTNODE, { COPYING_FLAG: False, "node_id": node_id }), ) parent_node.refresh_from_db() children = parent_node.get_children() for child in children: # make sure the copies are in the results if child.original_source_node_id and child.source_node_id: assert child.original_source_node_id in node_ids assert child.source_node_id in node_ids
def test_duplicate_nodes_task(self): metadata = {'test': True} task_options = {'user_id': self.user.pk, 'metadata': metadata} ids = [] node_ids = [] for i in range(3, 6): node_id = '0000000000000000000000000000000' + str(i) node_ids.append(node_id) node = ContentNode.objects.get(node_id=node_id) ids.append(node.pk) parent_node = ContentNode.objects.get( node_id='00000000000000000000000000000002') task_args = { 'user_id': self.user.pk, 'channel_id': self.channel.pk, 'node_ids': ids, 'target_parent': parent_node.pk } task, task_info = create_async_task('duplicate-nodes', task_options, task_args) # progress is retrieved dynamically upon calls to get the task info, so # use an API call rather than checking the db directly for progress. url = '{}/{}'.format(self.task_url, task_info.id) response = self.get(url) assert response.data[ 'status'] == 'SUCCESS', "Task failed, exception: {}".format( response.data['metadata']['error']['traceback']) self.assertEqual(response.data['status'], 'SUCCESS') self.assertEqual(response.data['task_type'], 'duplicate-nodes') self.assertEqual(response.data['metadata']['progress'], 100) result = response.data['metadata']['result'] self.assertTrue(isinstance(result, list)) parent_node.refresh_from_db() children = parent_node.get_children() child_ids = [] for child in children: child_ids.append(child.source_node_id) # make sure the changes were actually made to the DB for node_id in node_ids: assert node_id in child_ids # make sure the copies are in the results for item in result: assert item['original_source_node_id'] in node_ids
def generate_node_diff(request, updated_id, original_id): try: # Get queryset to test permissions nodes = ContentNode.objects.filter( Q(pk=updated_id) | Q(pk=original_id)) request.user.can_view_nodes(nodes) except PermissionDenied: return Response('Diff is not available', status=status.HTTP_403_FORBIDDEN) # See if there's already a staging task in progress is_generating = Task.objects.filter( task_type="get-node-diff", metadata__args__updated_id=updated_id, metadata__args__original_id=original_id, ).exclude(Q(status='FAILURE') | Q(status='SUCCESS')).exists() if not is_generating: create_async_task("get-node-diff", request.user, updated_id=updated_id, original_id=original_id) return Response('Diff is being generated')
def move_nodes(request): logging.debug("Entering the move_nodes endpoint") data = request.data try: nodes = data["nodes"] target_parent = ContentNode.objects.get(pk=data["target_parent"]) channel_id = data["channel_id"] min_order = data.get("min_order") or 0 max_order = data.get("max_order") or min_order + len(nodes) channel = target_parent.get_channel() try: request.user.can_edit(channel and channel.pk) request.user.can_edit_nodes( ContentNode.objects.filter(id__in=list(n["id"] for n in nodes))) except PermissionDenied: return HttpResponseNotFound("Resources not found") task_info = { 'user': request.user, 'metadata': { 'affects': { 'channels': [channel_id], 'nodes': nodes, } } } task_args = { 'user_id': request.user.pk, 'channel_id': channel_id, 'node_ids': nodes, 'target_parent': data["target_parent"], 'min_order': min_order, 'max_order': max_order } task, task_info = create_async_task('move-nodes', task_info, task_args) return HttpResponse(JSONRenderer().render( TaskSerializer(task_info).data)) except KeyError: raise ObjectDoesNotExist( "Missing attribute from data: {}".format(data))
def copy(self, pk, from_key=None, target=None, position=None, mods=None, excluded_descendants=None, **kwargs): try: target, position = self.validate_targeting_args(target, position) except ValidationError as e: return str(e), None try: source = self.get_queryset().get(pk=from_key) except ContentNode.DoesNotExist: error = ValidationError("Copy source node does not exist") return str(error), [generate_delete_event(pk, CONTENTNODE)] # Affected channel for the copy is the target's channel channel_id = target.channel_id if ContentNode.objects.filter(pk=pk).exists(): error = ValidationError("Copy pk already exists") return str(error), None task_args = { "user_id": self.request.user.id, "channel_id": channel_id, "source_id": source.id, "target_id": target.id, "pk": pk, "mods": mods, "excluded_descendants": excluded_descendants, "position": position, } task, task_info = create_async_task("duplicate-nodes", self.request.user, **task_args) return ( None, [ generate_update_event(pk, CONTENTNODE, {TASK_ID: task_info.task_id}) ], )
def test_asynctask_reports_success(self): """ Tests that when an async task is created and completed, the Task object has a status of 'SUCCESS' and contains the return value of the task. """ metadata = {'test': True} task_options = {'user_id': self.user.pk, 'metadata': metadata} task, task_info = create_async_task('test', task_options) self.assertTrue(Task.objects.filter(metadata__test=True).count() == 1) self.assertEqual(task_info.user, self.user) self.assertEqual(task_info.task_type, 'test') self.assertEqual(task_info.is_progress_tracking, False) result = task.get() self.assertEqual(result, 42) self.assertEqual( Task.objects.get(task_id=task.id).metadata['result'], 42) self.assertEqual(Task.objects.get(task_id=task.id).status, 'SUCCESS')
def test_asynctask_reports_success(self): """ Tests that when an async task is created and completed, the Task object has a status of 'SUCCESS' and contains the return value of the task. """ task, task_info = create_async_task("test", self.user, apply_async=False) self.assertEqual(task_info.user, self.user) self.assertEqual(task_info.task_type, "test") result = task.get() self.assertEqual(result, 42) self.assertEqual(task.status, states.SUCCESS) self.assertEqual( Task.objects.get(task_id=task.id).metadata["result"], 42) self.assertEqual( Task.objects.get(task_id=task.id).status, states.SUCCESS)
def test_asynctask_reports_error(self): """ Tests that if a task fails with an error, that the error information is stored in the Task object for later retrieval and analysis. """ celery_task, task_info = create_async_task("error-test", self.user, apply_async=False) task_info.refresh_from_db() self.assertEqual(task_info.status, states.FAILURE) self.assertTrue("error" in task_info.metadata) error = task_info.metadata["error"] self.assertEqual(list(error.keys()), ["message", "traceback"]) traceback_string = "\n".join(error["traceback"]) self.assertTrue("Exception" in traceback_string) self.assertTrue( "I'm sorry Dave, I'm afraid I can't do that." in traceback_string)
def test_asynctask_reports_success(self): """ Tests that when an async task is created and completed, the Task object has a status of 'SUCCESS' and contains the return value of the task. """ metadata = {'test': True} task_options = { 'user_id': self.user.pk, 'task_type': 'asynctask', 'metadata': metadata } task, task_info = create_async_task('test', task_options) self.assertTrue(Task.objects.filter(metadata__test=True).count()==1) self.assertEqual(task_info.user, self.user) self.assertEqual(task_info.task_type, 'test') self.assertEqual(task_info.is_progress_tracking, False) result = task.get() self.assertEqual(Task.objects.get(task_id=task.id).metadata['result'], 42) self.assertEqual(Task.objects.get(task_id=task.id).status, 'SUCCESS')
def duplicate_node_inline(request): logging.debug("Entering the dupllicate_node_inline endpoint") if request.method != 'POST': return HttpResponseBadRequest( "Only POST requests are allowed on this endpoint.") data = request.data try: node_id = data["node_id"] channel_id = data["channel_id"] target_parent = ContentNode.objects.get(pk=data["target_parent"]) channel = target_parent.get_channel() try: request.user.can_edit(channel and channel.pk) except PermissionDenied: return HttpResponseNotFound("No channel matching: {}".format( channel and channel.pk)) task_info = { 'user': request.user, 'metadata': { 'affects': { 'channels': [channel_id], 'nodes': [node_id], } } } task_args = { 'user_id': request.user.pk, 'channel_id': channel_id, 'target_parent': target_parent.pk, 'node_id': node_id, } task, task_info = create_async_task('duplicate-node-inline', task_info, task_args) return Response(TaskSerializer(task_info).data) except KeyError: raise ObjectDoesNotExist( "Missing attribute from data: {}".format(data))
def duplicate_nodes(request): logging.debug("Entering the copy_node endpoint") data = request.data try: node_ids = data["node_ids"] sort_order = data.get("sort_order") or 1 channel_id = data["channel_id"] target_parent = ContentNode.objects.get(pk=data["target_parent"]) channel = target_parent.get_channel() try: request.user.can_edit(channel and channel.pk) except PermissionDenied: return HttpResponseNotFound("No channel matching: {}".format( channel and channel.pk)) task_info = { 'user': request.user, 'metadata': { 'affects': { 'channels': [channel_id], 'nodes': node_ids, } } } task_args = { 'user_id': request.user.pk, 'channel_id': channel_id, 'target_parent': target_parent.pk, 'node_ids': node_ids, 'sort_order': sort_order } task, task_info = create_async_task('duplicate-nodes', task_info, task_args) return HttpResponse(JSONRenderer().render( TaskSerializer(task_info).data)) except KeyError: raise ObjectDoesNotExist( "Missing attribute from data: {}".format(data))
def sync_nodes(request): logging.debug("Entering the sync_nodes endpoint") data = request.data try: nodes = data["nodes"] channel_id = data['channel_id'] try: request.user.can_edit(channel_id) request.user.can_edit_nodes( ContentNode.objects.filter(id__in=list(n["id"] for n in nodes))) except PermissionDenied: return HttpResponseNotFound("Resources not found") task_info = { 'user': request.user, 'metadata': { 'affects': { 'channels': [channel_id], 'nodes': nodes, } } } task_args = { 'user_id': request.user.pk, 'channel_id': channel_id, 'node_ids': nodes, 'sync_attributes': True, 'sync_tags': True, 'sync_files': True, 'sync_assessment_items': True, } task, task_info = create_async_task('sync-nodes', task_info, task_args) return HttpResponse(JSONRenderer().render( TaskSerializer(task_info).data)) except KeyError: raise ObjectDoesNotExist( "Missing attribute from data: {}".format(data))
def test_asynctask_reports_progress(self): """ Test that we can retrieve task progress via the Task API. """ metadata = {'test': True} task_options = {'user_id': self.user.pk, 'metadata': metadata} task, task_info = create_async_task('progress-test', task_options) self.assertTrue(Task.objects.filter(metadata__test=True).count() == 1) result = task.get() self.assertEqual(result, 42) self.assertEqual(Task.objects.get(task_id=task.id).status, 'SUCCESS') # progress is retrieved dynamically upon calls to get the task info, so # use an API call rather than checking the db directly for progress. url = '{}/{}'.format(self.task_url, task_info.id) response = self.get(url) self.assertEqual(response.data['status'], 'SUCCESS') self.assertEqual(response.data['task_type'], 'progress-test') self.assertEqual(response.data['metadata']['progress'], 100) self.assertEqual(response.data['metadata']['result'], 42)
def test_asynctask_filters_by_channel(self): """ Test that we can filter tasks by channel ID. """ self.channel.editors.add(self.user) self.channel.save() metadata = {'affects': {'channels': [self.channel.id]}} task_options = { 'user_id': self.user.pk, 'metadata': metadata } task, task_info = create_async_task('progress-test', task_options) self.assertTrue(Task.objects.filter(metadata__affects__channels__contains=[self.channel.id]).count() == 1) result = task.get() self.assertEqual(result, 42) self.assertEqual(Task.objects.get(task_id=task.id).status, 'SUCCESS') # since tasks run sync in tests, we can't test it in an actual running state # so simulate the running state in the task object. db_task = Task.objects.get(task_id=task.id) db_task.status = 'STARTED' db_task.save() url = '{}?channel_id={}'.format(self.task_url, self.channel.id) response = self.get(url) self.assertEqual(len(response.data), 1) self.assertEqual(response.data[0]['status'], 'STARTED') self.assertEqual(response.data[0]['task_type'], 'progress-test') self.assertEqual(response.data[0]['metadata']['progress'], 100) self.assertEqual(response.data[0]['metadata']['result'], 42) # once the task is completed, it should be removed from the list of channel tasks. db_task.status = 'SUCCESS' db_task.save() response = self.get(url) self.assertEqual(len(response.data), 0) url = '{}?channel_id={}'.format(self.task_url, task_info.id, "nope") response = self.get(url) self.assertEqual(len(response.data), 0)
def sync_channel_endpoint(request): logging.debug("Entering the sync_nodes endpoint") data = request.data try: channel_id = data['channel_id'] try: request.user.can_edit(channel_id) except PermissionDenied: return HttpResponseNotFound( "No channel matching: {}".format(channel_id)) task_info = { 'user': request.user, 'metadata': { 'affects': { 'channels': [channel_id], } } } task_args = { 'user_id': request.user.pk, 'channel_id': channel_id, 'sync_attributes': data.get('attributes'), 'sync_tags': data.get('tags'), 'sync_files': data.get('files'), 'sync_assessment_items': data.get('assessment_items'), 'sync_sort_order': data.get('sort'), } task, task_info = create_async_task('sync-channel', task_info, task_args) return HttpResponse(JSONRenderer().render( TaskSerializer(task_info).data)) except KeyError: raise ObjectDoesNotExist( "Missing attribute from data: {}".format(data))
def test_asynctask_reports_error(self): """ Tests that if a task fails with an error, that the error information is stored in the Task object for later retrieval and analysis. """ metadata = {'test': True} task_options = { 'user_id': self.user.pk, 'task_type': 'asynctask', 'metadata': metadata } task, task_info = create_async_task('error-test', task_options) task = Task.objects.get(task_id=task.id) self.assertEqual(task.status, 'FAILURE') self.assertTrue('error' in task.metadata) error = task.metadata['error'] self.assertItemsEqual(list(error.keys()), ['task_args', 'task_kwargs', 'traceback']) self.assertEqual(len(error['task_args']), 0) self.assertEqual(len(error['task_kwargs']), 0) traceback_string = '\n'.join(error['traceback']) self.assertTrue("Exception" in traceback_string) self.assertTrue("I'm sorry Dave, I'm afraid I can't do that." in traceback_string)
def test_asynctask_reports_progress(self): """ Test that we can retrieve task progress via the Task API. """ metadata = {'test': True} task_options = { 'user_id': self.user.pk, 'task_type': 'asynctask', 'metadata': metadata } task, task_info = create_async_task('progress-test', task_options) self.assertTrue(Task.objects.filter(metadata__test=True).count()==1) result = task.get() self.assertEqual(result, 42) self.assertEqual(Task.objects.get(task_id=task.id).status, 'SUCCESS') # progress is retrieved dynamically upon calls to get the task info, so # use an API call rather than checking the db directly for progress. url = '{}/{}'.format(self.task_url, task_info.id) response = self.get(url) self.assertEqual(response.data['status'], 'SUCCESS') self.assertEqual(response.data['task_type'], 'progress-test') self.assertEqual(response.data['metadata']['progress'], 100) self.assertEqual(response.data['metadata']['result'], 42)
def api_commit_channel(request): """ Commit the channel chef_tree to staging tree to the main tree. This view backs the endpoint `/api/internal/finish_channel` called by ricecooker. """ data = json.loads(request.body) try: channel_id = data['channel_id'] request.user.can_edit(channel_id) obj = Channel.objects.get(pk=channel_id) # Need to rebuild MPTT tree pointers since we used `disable_mptt_updates` ContentNode.objects.partial_rebuild(obj.chef_tree.tree_id) # set original_channel_id and source_channel_id to self since chef tree obj.chef_tree.get_descendants(include_self=True).update(original_channel_id=channel_id, source_channel_id=channel_id) # replace staging_tree with chef_tree old_staging = obj.staging_tree obj.staging_tree = obj.chef_tree obj.chef_tree = None obj.save() # Prepare change event indicating a new staging_tree is available event = generate_update_event(channel_id, CHANNEL, { "root_id": obj.main_tree.id, "staging_root_id": obj.staging_tree.id, }) # Mark old staging tree for garbage collection if old_staging and old_staging != obj.main_tree: # IMPORTANT: Do not remove this block, MPTT updating the deleted chefs block could hang the server with ContentNode.objects.disable_mptt_updates(): garbage_node = get_deleted_chefs_root() old_staging.parent = garbage_node old_staging.title = "Old staging tree for channel {}".format(obj.pk) old_staging.save() # Send event (new staging tree or new main tree) to all channel editors for editor in obj.editors.all(): add_event_for_user(editor.id, event) _, task = create_async_task( "get-node-diff", request.user, updated_id=obj.staging_tree.id, original_id=obj.main_tree.id, ) # Send response back to the content integration script return Response({ "success": True, "new_channel": obj.pk, "diff_task_id": task.pk, }) except (Channel.DoesNotExist, PermissionDenied): return HttpResponseNotFound("No channel matching: {}".format(channel_id)) except KeyError: return HttpResponseBadRequest("Required attribute missing from data: {}".format(data)) except Exception as e: handle_server_error(request) return HttpResponseServerError(content=str(e), reason=str(e))