def test_interpolated_name(config, client, mongo): juan = make_user('juan', 'Juan') name = 'Computes a name based on a Cow' res = client.post('/v1/execution', headers={**{ 'Content-Type': 'application/json', }, **make_auth(juan)}, data=json.dumps({ 'process_name': 'interpol', 'form_array': [{ 'ref': 'form', 'data': { 'field': 'Cow', }, }], })) # request succeeded assert res.status_code == 201 # execution has name exc = next(Execution.q().filter(status='ongoing')) assert exc.name == name # execution collection has name reg2 = next(mongo[config["EXECUTION_COLLECTION"]].find()) assert reg2['id'] == exc.id assert reg2['name'] == name # history has the name reg = next(mongo[config["POINTER_COLLECTION"]].find()) assert reg['execution']['name'] == name
def test_hidden_input(client, mocker, config, mongo): mocker.patch('pika.adapters.blocking_connection.' 'BlockingChannel.basic_publish') juan = make_user('juan', 'Juan') res = client.post('/v1/execution', headers={ **{ 'Content-Type': 'application/json', }, **make_auth(juan) }, data=json.dumps({ 'process_name': 'input-hidden', 'form_array': [{ 'ref': 'start_form', 'data': { 'data': 'yes', }, }], })) assert res.status_code == 201 exc = Execution.get_all()[0] ptr = exc.proxy.pointers.get()[0] pika.adapters.blocking_connection.BlockingChannel.\ basic_publish.assert_called_once() args = pika.adapters.blocking_connection.\ BlockingChannel.basic_publish.call_args[1] json_message = { 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': 'juan', 'input': [ Form.state_json('start_form', [ { 'label': 'data', 'type': 'text', 'value': 'yes', 'value_caption': 'yes', 'name': 'data', 'state': 'valid', 'hidden': True, }, ]) ], } assert json.loads(args['body']) == json_message
def test_hidden_input(client, mocker, config, mongo): mocker.patch('cacahuate.tasks.handle.delay') juan = make_user('juan', 'Juan') res = client.post('/v1/execution', headers={ **{ 'Content-Type': 'application/json', }, **make_auth(juan) }, data=json.dumps({ 'process_name': 'input-hidden', 'form_array': [{ 'ref': 'start_form', 'data': { 'data': 'yes', }, }], })) assert res.status_code == 201 exc = next(Execution.q().filter(status='ongoing')) ptr = next(exc.pointers.q().filter(status='ongoing')) handle.delay.assert_called_once() args = handle.delay.call_args[0][0] json_message = { 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': 'juan', 'input': [ Form.state_json('start_form', [ { 'label': 'data', 'type': 'text', 'value': 'yes', 'value_caption': 'yes', 'name': 'data', 'state': 'valid', 'hidden': True, }, ]) ], } assert json.loads(args) == json_message
def patch(self, message, channel): execution = Execution.get_or_exception(message['execution_id']) xml = Xml.load(self.config, execution.process_name, direct=True) mongo = self.get_mongo() execution_collection = mongo[self.config['EXECUTION_COLLECTION']] pointer_collection = mongo[self.config['POINTER_COLLECTION']] # set nodes with pointers as unfilled, delete pointers updates = {} for pointer in execution.proxy.pointers.q(): updates['state.items.{node}.state'.format( node=pointer.node_id, )] = 'unfilled' pointer.delete() pointer_collection.update_one({ 'id': pointer.id, }, { '$set': { 'state': 'cancelled', 'finished_at': datetime.now(), 'patch': { 'comment': message['comment'], 'inputs': message['inputs'], }, }, }) execution_collection.update_one({ 'id': execution.id, }, { '$set': updates, }) # retrieve updated state state = next(execution_collection.find({'id': execution.id})) state_updates = cascade_invalidate(xml, state, message['inputs'], message['comment']) # update state collection = mongo[self.config['EXECUTION_COLLECTION']] collection.update_one({ 'id': state['id'], }, { '$set': state_updates, }) # retrieve updated state state = next(execution_collection.find({'id': execution.id})) first_invalid_node = track_next_node(xml, state, self.get_mongo(), self.config) # wakeup and start execution from the found invalid node self.wakeup_and_notify(first_invalid_node, execution, channel, state)
def start(self, node, input, mongo, user_identifier): # the first set of values context = compact_values(input) # save the data execution = Execution( process_name=self.filename, name=self.get_name(context), name_template=self.name_template(), description=self.get_description(context), description_template=self.description_template(), started_at=datetime.now(), status='ongoing', ).save() pointer = Pointer( node_id=node.id, name=node.get_name(context), description=node.get_description(context), started_at=execution.started_at, status='ongoing', ).save() pointer.execution.set(execution) # log to mongo collection = mongo[self.config['POINTER_COLLECTION']] collection.insert_one(pointer_entry( node, pointer.name, pointer.description, execution, pointer )) collection = mongo[self.config['EXECUTION_COLLECTION']] collection.insert_one(execution_entry( execution, self.get_state(), )) # trigger rabbit from .tasks import handle handle.delay(json.dumps({ 'command': 'step', 'pointer_id': pointer.id, 'user_identifier': user_identifier, 'input': input, })) return execution
def execution_add_user(id): ''' adds the user as a candidate for solving the given node, only if the node has an active pointer. ''' # TODO possible race condition introduced here. How does this code work in # case the handler is moving the pointer? # get execution execution = Execution.get_or_exception(id) # validate the members needed validate_json(request.json, ['identifier', 'node_id']) identifier = request.json['identifier'] node_id = request.json['node_id'] # get actual pointer try: pointer = next(execution.proxy.pointers.q().filter(node_id=node_id)) except StopIteration: raise BadRequest([{ 'detail': f'{node_id} does not have a live pointer', 'code': 'validation.no_live_pointer', 'where': 'request.body.node_id', }]) # get user user = User.get_by('identifier', identifier) if user is None: raise InvalidInputError('user_id', 'request.body.identifier') # update user user.proxy.tasks.add(pointer) # update pointer collection = mongo.db[app.config['POINTER_COLLECTION']] db_pointer = collection.find_one({'id': pointer.id}) user_json = user.to_json() notified_users = db_pointer.get('notified_users', []) if user_json not in notified_users: notified_users.append(user.to_json()) collection.update_one( {'id': pointer.id}, {'$set': { 'notified_users': notified_users }}, ) return jsonify(user_json), 200
def start(self, node, input, mongo, channel, user_identifier): # save the data execution = Execution( process_name=self.filename, name=self.get_name(input), description=self.get_description(input), ).save() pointer = Pointer( node_id=node.id, name=node.name, description=node.description, ).save() pointer.proxy.execution.set(execution) # log to mongo collection = mongo[self.config['POINTER_COLLECTION']] collection.insert_one(node.pointer_entry(execution, pointer)) collection = mongo[self.config['EXECUTION_COLLECTION']] collection.insert_one({ '_type': 'execution', 'id': execution.id, 'name': execution.name, 'description': execution.description, 'status': 'ongoing', 'started_at': datetime.now(), 'finished_at': None, 'state': self.get_state(), 'values': {}, 'actors': {}, }) # trigger rabbit channel.basic_publish( exchange='', routing_key=self.config['RABBIT_QUEUE'], body=json.dumps({ 'command': 'step', 'pointer_id': pointer.id, 'user_identifier': user_identifier, 'input': input, }), properties=pika.BasicProperties( delivery_mode=2, ), ) return execution
def make_pointer( process_name, node_id, execution_status=None, pointer_status=None, ): exc = Execution( process_name=process_name, status=execution_status or 'ongoing', ).save() ptr = Pointer( node_id=node_id, status=pointer_status or 'ongoing', ).save() ptr.execution.set(exc) return ptr
def delete_process(id): execution = Execution.get_or_exception(id) channel = get_channel() channel.basic_publish( exchange='', routing_key=app.config['RABBIT_QUEUE'], body=json.dumps({ 'command': 'cancel', 'execution_id': execution.id, }), properties=pika.BasicProperties(delivery_mode=2, ), ) return jsonify({ 'data': 'accepted', }), 202
def cancel_execution(self, message): execution = Execution.get_or_exception(message['execution_id']) for pointer in execution.proxy.pointers.get(): pointer.delete() collection = self.get_mongo()[ self.config['EXECUTION_COLLECTION'] ] collection.update_one({ 'id': execution.id, }, { '$set': { 'status': 'cancelled', 'finished_at': datetime.now() } }) execution.delete()
def cancel_execution(self, message): execution = Execution.get_or_exception(message['execution_id']) if execution.status != 'ongoing': raise ModelNotFoundError( 'Specified execution never existed, and never will', ) execution.status = 'cancelled' execution.finished_at = datetime.now() execution.save() for pointer in execution.pointers.q().filter(status='ongoing'): pointer.status = 'cancelled' pointer.finished_at = datetime.now() pointer.save() self.execution_collection().update_one({ 'id': execution.id, }, { '$set': { 'status': execution.status, 'finished_at': execution.finished_at } }) self.pointer_collection().update_many( { 'execution.id': execution.id, 'state': 'ongoing', }, { '$set': { 'state': 'cancelled', 'finished_at': execution.finished_at } }) self.pointer_collection().update_many({ 'execution.id': execution.id, }, {'$set': { 'execution': execution.to_json(), }})
def cancel_execution(self, message): execution = Execution.get_or_exception(message['execution_id']) execution.status = 'cancelled' execution.finished_at = datetime.now() for pointer in execution.proxy.pointers.get(): pointer.delete() exe_collection = self.get_mongo()[self.config['EXECUTION_COLLECTION']] exe_collection.update_one({ 'id': execution.id, }, { '$set': { 'status': execution.status, 'finished_at': execution.finished_at } }) ptr_collection = self.get_mongo()[self.config['POINTER_COLLECTION']] ptr_collection.update_many( { 'execution.id': execution.id, 'state': 'ongoing', }, { '$set': { 'state': 'cancelled', 'finished_at': execution.finished_at } }) ptr_collection.update_many({ 'execution.id': execution.id, }, {'$set': { 'execution': execution.to_json(), }}) execution.delete()
def test_create_pointer(config): handler = Handler(config) xml = Xml.load(config, 'simple') xmliter = iter(xml) node = Action(next(xmliter), xmliter) exc = Execution.validate( process_name='simple.2018-02-19.xml', name='nombre', name_template='nombre', description='description', description_template='description', started_at=make_date(2020, 8, 21, 4, 5, 6), status='ongoing', ).save() pointer = handler.create_pointer(node, exc) execution = pointer.proxy.execution.get() assert pointer.node_id == 'start_node' assert execution.process_name == 'simple.2018-02-19.xml' assert execution.proxy.pointers.count() == 1
def test_call_node_render(config, mongo, mocker): mocker.patch('cacahuate.tasks.handle.delay') handler = Handler(config) user = make_user('juan', 'Juan') ptr = make_pointer('call-render.2020-04-24.xml', 'start_node') execution = ptr.execution.get() value = random_string() mongo[config["EXECUTION_COLLECTION"]].insert_one({ '_type': 'execution', 'id': execution.id, 'state': Xml.load(config, execution.process_name).get_state(), }) # teardown of first node and wakeup of call node handler.step({ 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': user.identifier, 'input': [ Form.state_json('start_form', [ { 'name': 'data', 'name': 'data', 'value': value, 'value_caption': value, 'state': 'valid', }, ]) ], }) assert Pointer.get(ptr.id).status == 'finished' ptr = next(execution.pointers.q().filter(status='ongoing')) assert ptr.node_id == 'call' new_ptr = next(Pointer.q().filter(node_id='start_node', status='ongoing')) # aditional rabbit call for new process args = handle.delay.call_args_list[0][0][0] assert json.loads(args) == { 'command': 'step', 'pointer_id': new_ptr.id, 'user_identifier': '__system__', 'input': [ Form.state_json('start_form', [ { 'label': 'Info', 'name': 'data', 'state': 'valid', 'type': 'text', 'value': value, 'value_caption': value, 'hidden': False, }, ]) ], } # normal rabbit call args = handle.delay.call_args_list[1][0][0] json_res = json.loads(args) # check execution_id exists assert json_res['input'][0]['inputs']['items']['execution'].pop('value') assert json_res['input'][0]['inputs']['items']['execution'].pop( 'value_caption') assert json_res == { 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': '__system__', 'input': [ Form.state_json('call', [ { 'name': 'execution', 'label': 'execution', 'type': 'text', 'hidden': False, 'state': 'valid', }, ]) ], } # mongo log registry created for new process reg = next(mongo[config["POINTER_COLLECTION"]].find({ 'id': new_ptr.id, })) assert reg['node']['id'] == 'start_node' # mongo execution registry created for new process reg = next(mongo[config["EXECUTION_COLLECTION"]].find({ 'id': new_ptr.execution.get().id, })) assert reg['name'] == 'Simplest process ever started with: ' + value # teardown of the call node and end of first execution handler.step({ 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': '__system__', 'input': [], }) # old execution is gone, new is here assert Execution.get(execution.id).status == 'finished' assert Pointer.get(ptr.id).status == 'finished' execution = next(Execution.q().filter(status='ongoing')) assert execution.process_name == 'simple.2018-02-19.xml'
def make_pointer(process_name, node_id): exc = Execution(process_name=process_name, ).save() ptr = Pointer(node_id=node_id, ).save() ptr.proxy.execution.set(exc) return ptr
def test_call_handler_delete_process(config, mongo): handler = Handler(config) pointer = make_pointer('simple.2018-02-19.xml', 'requester') execution = pointer.execution.get() body = '{"command":"cancel", "execution_id":"%s", "pointer_id":"%s"}'\ % (execution.id, pointer.id) mongo[config["EXECUTION_COLLECTION"]].insert_one({ 'started_at': datetime(2018, 4, 1, 21, 45), 'finished_at': None, 'status': 'ongoing', 'id': execution.id }) ptr_id_1 = 'RANDOMPOINTERNAME' ptr_id_2 = 'MORERANDOMNAME' ptr_id_3 = 'NOTSORANDOMNAME' mongo[config["POINTER_COLLECTION"]].insert_many([ { 'execution': { 'id': execution.id, }, 'state': 'finished', 'id': ptr_id_1, }, { 'execution': { 'id': execution.id, }, 'state': 'ongoing', 'id': ptr_id_2, }, { 'execution': { 'id': execution.id[::-1], }, 'state': 'ongoing', 'id': ptr_id_3, }, ]) handler(body) reg = next(mongo[config["EXECUTION_COLLECTION"]].find()) assert reg['id'] == execution.id assert reg['status'] == "cancelled" assert_near_date(reg['finished_at']) assert list(Execution.q().filter(status='ongoing')) == [] assert list(Pointer.q().filter(status='ongoing')) == [] assert mongo[config["POINTER_COLLECTION"]].find_one({ 'id': ptr_id_1, })['state'] == 'finished' assert mongo[config["POINTER_COLLECTION"]].find_one({ 'id': ptr_id_2, })['state'] == 'cancelled' assert mongo[config["POINTER_COLLECTION"]].find_one({ 'id': ptr_id_3, })['state'] == 'ongoing'
def patch(self, message): execution = Execution.get_or_exception(message['execution_id']) if execution.status != 'ongoing': raise ModelNotFoundError( 'Specified execution never existed, and never will', ) xml = Xml.load(self.config, execution.process_name, direct=True) # set nodes with pointers as unfilled, delete pointers updates = {} user = User.get_by( 'identifier', message.get('user_identifier'), ) if user is None: if message.get('user_identifier') == '__system__': user = User(identifier='__system__', fullname='System').save() else: raise InconsistentState('sent identifier of unexisten user') for pointer in execution.pointers.q().filter(status='ongoing'): updates['state.items.{node}.state'.format( node=pointer.node_id, )] = 'unfilled' pointer.status = 'cancelled' pointer.finished_at = datetime.now() pointer.save() self.pointer_collection().update_one({ 'id': pointer.id, }, { '$set': { 'state': 'cancelled', 'finished_at': pointer.finished_at, 'patch': { 'comment': message['comment'], 'inputs': message['inputs'], 'actor': user.to_json(include=[ '_type', 'fullname', 'identifier', ]), }, }, }) self.execution_collection().update_one({ 'id': execution.id, }, { '$set': updates, }) # retrieve updated state state = next(self.execution_collection().find({'id': execution.id})) state_updates, array_filters = cascade_invalidate( xml, state, message['inputs'], message['comment']) # update state self.execution_collection().update_one( {'id': state['id']}, {'$set': state_updates}, array_filters=array_filters, ) # retrieve updated state state = next(self.execution_collection().find({'id': execution.id})) first_invalid_node = track_next_node(xml, state, self.get_mongo(), self.config) # wakeup and start execution from the found invalid node self.wakeup_and_notify(first_invalid_node, execution, state)
def test_call_handler_delete_process(config, mongo): handler = Handler(config) channel = MagicMock() method = {'delivery_tag': True} properties = "" pointer = make_pointer('simple.2018-02-19.xml', 'requester') execution_id = pointer.proxy.execution.get().id body = '{"command":"cancel", "execution_id":"%s", "pointer_id":"%s"}'\ % (execution_id, pointer.id) mongo[config["EXECUTION_COLLECTION"]].insert_one({ 'started_at': datetime(2018, 4, 1, 21, 45), 'finished_at': None, 'status': 'ongoing', 'id': execution_id }) ptr_id_1 = 'RANDOMPOINTERNAME' ptr_id_2 = 'MORERANDOMNAME' ptr_id_3 = 'NOTSORANDOMNAME' mongo[config["POINTER_COLLECTION"]].insert_many([ { 'execution': { 'id': execution_id, }, 'state': 'finished', 'id': ptr_id_1, }, { 'execution': { 'id': execution_id, }, 'state': 'ongoing', 'id': ptr_id_2, }, { 'execution': { 'id': execution_id[::-1], }, 'state': 'ongoing', 'id': ptr_id_3, }, ]) handler(channel, method, properties, body) reg = next(mongo[config["EXECUTION_COLLECTION"]].find()) assert reg['id'] == execution_id assert reg['status'] == "cancelled" assert_near_date(reg['finished_at']) assert Execution.count() == 0 assert Pointer.count() == 0 assert mongo[config["POINTER_COLLECTION"]].find_one({ 'id': ptr_id_1, })['state'] == 'finished' assert mongo[config["POINTER_COLLECTION"]].find_one({ 'id': ptr_id_2, })['state'] == 'cancelled' assert mongo[config["POINTER_COLLECTION"]].find_one({ 'id': ptr_id_3, })['state'] == 'ongoing'
def continue_process(): validate_json(request.json, ['execution_id', 'node_id']) execution_id = request.json['execution_id'] node_id = request.json['node_id'] try: execution = Execution.get_or_exception(execution_id) except ModelNotFoundError: raise BadRequest([{ 'detail': 'execution_id is not valid', 'code': 'validation.invalid', 'where': 'request.body.execution_id', }]) xml = Xml.load(app.config, execution.process_name, direct=True) xmliter = iter(xml) try: continue_point = make_node( xmliter.find(lambda e: e.getAttribute('id') == node_id), xmliter) except ElementNotFound: raise BadRequest([{ 'detail': 'node_id is not a valid node', 'code': 'validation.invalid_node', 'where': 'request.body.node_id', }]) try: pointer = next(execution.proxy.pointers.q().filter(node_id=node_id)) except StopIteration: raise BadRequest([{ 'detail': 'node_id does not have a live pointer', 'code': 'validation.no_live_pointer', 'where': 'request.body.node_id', }]) # Check for authorization if pointer not in g.user.proxy.tasks: raise Forbidden([{ 'detail': 'Provided user does not have this task assigned', 'where': 'request.authorization', }]) # Validate asociated forms collected_input = continue_point.validate_input(request.json) # trigger rabbit channel = get_channel() channel.basic_publish( exchange='', routing_key=app.config['RABBIT_QUEUE'], body=json.dumps({ 'command': 'step', 'pointer_id': pointer.id, 'user_identifier': g.user.identifier, 'input': collected_input, }), properties=pika.BasicProperties(delivery_mode=2, ), ) return { 'data': 'accepted', }, 202
def test_exit_interaction(config, mongo, mocker): mocker.patch('cacahuate.tasks.handle.delay') handler = Handler(config) user = make_user('juan', 'Juan') ptr = make_pointer('exit.2018-05-03.xml', 'start_node') execution = ptr.execution.get() execution.started_at = datetime(2018, 4, 1, 21, 45) execution.save() mongo[config["EXECUTION_COLLECTION"]].insert_one({ '_type': 'execution', 'id': execution.id, 'state': Xml.load(config, execution.process_name).get_state(), 'values': [ { '_type': 'fgroup', 'ref': '_execution', 'forms': [{ 'ref': '_execution', 'fields': [ { '_type': 'field', 'name': 'name', 'value': '', 'value_caption': '', 'state': 'valid', 'actor': { 'identifier': '__system__', }, 'set_at': execution.started_at, }, { '_type': 'field', 'name': 'description', 'value': '', 'value_caption': '', 'state': 'valid', 'actor': { 'identifier': '__system__', }, 'set_at': execution.started_at, }, ], }], }, ], }) # first node handler.step({ 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': user.identifier, 'input': [], }) ptr = next(Pointer.q().filter(status='ongoing')) assert ptr.node_id args = handle.delay.call_args[0][0] assert json.loads(args) == { 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': '__system__', 'input': [], } # exit node handler.step({ 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': '__system__', 'input': [], }) assert list(Pointer.q().filter(status='ongoing')) == [] assert list(Execution.q().filter(status='ongoing')) == [] # state is coherent state = next(mongo[config["EXECUTION_COLLECTION"]].find({ 'id': execution.id, })) del state['_id'] del state['finished_at'] values = state.pop('values') assert state == { '_type': 'execution', 'id': execution.id, 'name': '', 'description': '', 'state': { '_type': ':sorted_map', 'items': { 'start_node': { '_type': 'node', 'type': 'action', 'id': 'start_node', 'state': 'valid', 'comment': '', 'actors': { '_type': ':map', 'items': { 'juan': { '_type': 'actor', 'forms': [], 'state': 'valid', 'user': { '_type': 'user', 'identifier': 'juan', 'fullname': 'Juan', 'email': None, }, }, }, }, 'milestone': False, 'name': '', 'description': '', }, 'exit': { '_type': 'node', 'type': 'exit', 'id': 'exit', 'state': 'valid', 'comment': '', 'actors': { '_type': ':map', 'items': { '__system__': { '_type': 'actor', 'forms': [], 'state': 'valid', 'user': { '_type': 'user', 'identifier': '__system__', 'fullname': 'System', 'email': None, }, }, }, }, 'milestone': False, 'name': 'Exit exit', 'description': 'Exit exit', }, 'final_node': { '_type': 'node', 'type': 'action', 'id': 'final_node', 'state': 'unfilled', 'comment': '', 'actors': { '_type': ':map', 'items': {}, }, 'milestone': False, 'name': '', 'description': '', }, }, 'item_order': ['start_node', 'exit', 'final_node'], }, 'status': 'finished', 'actors': { 'exit': '__system__', 'start_node': 'juan', }, } eval_context = make_context({'values': values}, {}) eval_actor_map = make_actor_map({'values': values}) expected_context = { '_env': [{}], '_execution': [{ 'name': '', 'get_name_display': '', 'description': '', 'get_description_display': '', }], } assert {k: list(v.all()) for k, v in eval_context.items()} == expected_context expected_actor_map = { '_execution': [{ 'name': { 'actor': '__system__', }, 'description': { 'actor': '__system__', }, }], } for frms in eval_actor_map.values(): for frm in frms: for fld in frm.values(): assert fld.pop('set_at') assert eval_actor_map == expected_actor_map
def test_call_node_render(config, mongo): handler = Handler(config) user = make_user('juan', 'Juan') ptr = make_pointer('call-render.2020-04-24.xml', 'start_node') channel = MagicMock() execution = ptr.proxy.execution.get() value = random_string() mongo[config["EXECUTION_COLLECTION"]].insert_one({ '_type': 'execution', 'id': execution.id, 'state': Xml.load(config, execution.process_name).get_state(), }) # teardown of first node and wakeup of call node handler.call( { 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': user.identifier, 'input': [ Form.state_json('start_form', [ { 'name': 'data', 'name': 'data', 'value': value, 'value_caption': value, }, ]) ], }, channel) assert Pointer.get(ptr.id) is None ptr = execution.proxy.pointers.get()[0] assert ptr.node_id == 'call' new_ptr = next(Pointer.q().filter(node_id='start_node')) # aditional rabbit call for new process args = channel.basic_publish.call_args_list[0][1] assert args['exchange'] == '' assert args['routing_key'] == config['RABBIT_QUEUE'] assert json.loads(args['body']) == { 'command': 'step', 'pointer_id': new_ptr.id, 'user_identifier': '__system__', 'input': [ Form.state_json('start_form', [ { 'label': 'Info', 'name': 'data', 'state': 'valid', 'type': 'text', 'value': value, 'value_caption': value, 'hidden': False, }, ]) ], } # normal rabbit call args = channel.basic_publish.call_args_list[1][1] assert args['exchange'] == '' assert args['routing_key'] == config['RABBIT_QUEUE'] assert json.loads(args['body']) == { 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': '__system__', 'input': [], } # mongo log registry created for new process reg = next(mongo[config["POINTER_COLLECTION"]].find({ 'id': new_ptr.id, })) assert reg['node']['id'] == 'start_node' # mongo execution registry created for new process reg = next(mongo[config["EXECUTION_COLLECTION"]].find({ 'id': new_ptr.proxy.execution.get().id, })) assert reg['name'] == 'Simplest process ever started with: ' + value # teardown of the call node and end of first execution handler.call( { 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': '__system__', 'input': [], }, channel) # old execution is gone, new is here assert Execution.get(execution.id) is None assert Pointer.get(ptr.id) is None execution = Execution.get_all()[0] assert execution.process_name == 'simple.2018-02-19.xml'
def test_ifelifelse_else(config, mongo): ''' else will be executed if preceding condition is false''' # test setup handler = Handler(config) user = make_user('juan', 'Juan') ptr = make_pointer('else.2018-07-10.xml', 'start_node') channel = MagicMock() mongo[config["EXECUTION_COLLECTION"]].insert_one({ '_type': 'execution', 'id': ptr.proxy.execution.get().id, 'state': Xml.load(config, 'else').get_state(), }) handler.call( { 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': user.identifier, 'input': [ Form.state_json('secret01', [ { 'name': 'password', 'type': 'text', 'value': 'cuca', 'value_caption': 'cuca', }, ]) ], }, channel) # pointer moved assert Pointer.get(ptr.id) is None ptr = Pointer.get_all()[0] assert ptr.node_id == 'condition01' # rabbit called channel.basic_publish.assert_called_once() args = channel.basic_publish.call_args[1] rabbit_call = { 'command': 'step', 'pointer_id': ptr.id, 'input': [ Form.state_json('condition01', [ { 'name': 'condition', 'name': 'condition', 'state': 'valid', 'type': 'bool', 'value': False, 'value_caption': 'False', }, ]) ], 'user_identifier': '__system__', } assert json.loads(args['body']) == rabbit_call channel = MagicMock() handler.call(rabbit_call, channel) # pointer moved assert Pointer.get(ptr.id) is None ptr = Pointer.get_all()[0] assert ptr.node_id == 'elif01' # rabbit called channel.basic_publish.assert_called_once() args = channel.basic_publish.call_args[1] rabbit_call = { 'command': 'step', 'pointer_id': ptr.id, 'input': [ Form.state_json('elif01', [ { 'name': 'condition', 'state': 'valid', 'type': 'bool', 'value': False, 'value_caption': 'False', }, ]) ], 'user_identifier': '__system__', } assert json.loads(args['body']) == rabbit_call channel = MagicMock() handler.call(rabbit_call, channel) # pointer moved assert Pointer.get(ptr.id) is None ptr = Pointer.get_all()[0] assert ptr.node_id == 'else01' # rabbit called channel.basic_publish.assert_called_once() args = channel.basic_publish.call_args[1] rabbit_call = { 'command': 'step', 'pointer_id': ptr.id, 'input': [ Form.state_json('else01', [ { 'name': 'condition', 'state': 'valid', 'type': 'bool', 'value': True, 'value_caption': 'True', }, ]) ], 'user_identifier': '__system__', } assert json.loads(args['body']) == rabbit_call channel = MagicMock() handler.call(rabbit_call, channel) # pointer moved assert Pointer.get(ptr.id) is None ptr = Pointer.get_all()[0] assert ptr.node_id == 'action03' # rabbit called to notify the user channel.basic_publish.assert_called_once() args = channel.basic_publish.call_args[1] assert args['exchange'] == 'charpe_notify' channel = MagicMock() handler.call( { 'command': 'step', 'pointer_id': ptr.id, 'user_identifier': user.identifier, 'input': [ Form.state_json('form01', [ { 'name': 'answer', 'value': 'answer', 'value_caption': 'answer', }, ]) ], }, channel) # execution finished assert len(Pointer.get_all()) == 0 assert len(Execution.get_all()) == 0
def execution_patch(id): execution = Execution.get_or_exception(id) collection = mongo.db[app.config['EXECUTION_COLLECTION']] execution_state = next(collection.find({'id': id})) validate_json(request.json, ['comment', 'inputs']) xml = Xml.load(app.config, execution.process_name, direct=True) dom = xml.get_dom() if type(request.json['inputs']) != list: raise RequiredListError('inputs', 'request.body.inputs') processed_inputs = [] for i, field in enumerate(request.json['inputs']): if type(field) != dict: raise RequiredDictError(str(i), 'request.body.inputs.{}'.format(i)) if 'ref' not in field: raise RequiredInputError('id', 'request.body.inputs.{}.ref'.format(i)) if type(field['ref']) != str: raise RequiredStrError('ref', 'request.body.inputs.{}.ref'.format(i)) # check down the state tree for existence of the requested ref processed_ref = [] pieces = field['ref'].split('.') try: node_id = pieces.pop(0) node_state = execution_state['state']['items'][node_id] except IndexError: raise InputError('Missing segment in ref for node_id', 'request.body.inputs.{}.ref'.format(i), 'validation.invalid') except KeyError: raise InputError('node {} not found'.format(node_id), 'request.body.inputs.{}.ref'.format(i), 'validation.invalid') if node_state['type'] != 'action': raise InputError('only action nodes may be patched', 'request.body.inputs.{}.ref'.format(i), 'validation.invalid') processed_ref.append(node_id) # node xml element node = get_element_by(dom, 'action', 'id', node_id) if len(node_state['actors']['items']) == 1: only_key = list(node_state['actors']['items'].keys())[0] actor_state = node_state['actors']['items'][only_key] else: try: actor_username = pieces.pop(0) actor_state = node_state['actors']['items'][actor_username] except IndexError: raise InputError('Missing segment in ref for actor username', 'request.body.inputs.{}.ref'.format(i), 'validation.invalid') except KeyError: raise InputError('actor {} not found'.format(actor_username), 'request.body.inputs.{}.ref'.format(i), 'validation.invalid') processed_ref.append(actor_state['user']['identifier']) try: form_ref = pieces.pop(0) except IndexError: raise InputError('Missing segment in ref for form ref', 'request.body.inputs.{}.ref'.format(i), 'validation.invalid') if re.match(r'\d+', form_ref): try: form_index = int(form_ref) form_state = actor_state['forms'][form_index] except KeyError: raise InputError('form index {} not found'.format(form_ref), 'request.body.inputs.{}.ref'.format(i), 'validation.invalid') else: matching_forms = list( map(lambda f: f['ref'] == form_ref, actor_state['forms'])) form_count = len(list(filter(lambda x: x, matching_forms))) if form_count == 1: form_index = matching_forms.index(True) form_state = actor_state['forms'][form_index] elif form_count == 0: raise InputError( 'No forms with ref {} in node'.format(form_ref), 'request.body.inputs.{}.ref'.format(i), 'validation.invalid') else: raise InputError( 'More than one form with ref {}'.format(form_ref), 'request.body.inputs.{}.ref'.format(i), 'validation.invalid') processed_ref.append(str(form_index) + ':' + form_state['ref']) # form xml element form = get_element_by(node, 'form', 'id', form_state['ref']) try: input_name = pieces.pop(0) form_state['inputs']['items'][input_name] except IndexError: raise InputError('Missing segment in ref for input name', 'request.body.inputs.{}.ref'.format(i), 'validation.invalid') except KeyError: raise InputError('input {} not found'.format(input_name), 'request.body.inputs.{}.ref'.format(i), 'validation.invalid') processed_ref.append(input_name) processed_inputs.append({ 'ref': '.'.join(processed_ref), }) # input xml element input_el = get_element_by(form, 'input', 'name', input_name) if 'value' in field: try: input_obj = make_input(input_el) value = input_obj.validate(field['value'], 0) caption = input_obj.make_caption(value) processed_inputs[-1]['value'] = value processed_inputs[-1]['value_caption'] = caption except InputError as e: raise InputError('value invalid: {}'.format(str(e)), 'request.body.inputs.{}.value'.format(i), 'validation.invalid') channel = get_channel() channel.basic_publish( exchange='', routing_key=app.config['RABBIT_QUEUE'], body=json.dumps({ 'command': 'patch', 'execution_id': execution.id, 'comment': request.json['comment'], 'inputs': processed_inputs, }), properties=pika.BasicProperties(delivery_mode=2, ), ) return jsonify({ 'data': 'accepted', }), 202