Merge branch 'dev' into chore/log-changes-661

This commit is contained in:
mike cullerton 2022-03-09 11:27:13 -05:00
commit 6ec01a0a85
13 changed files with 192 additions and 59 deletions

View File

@ -74,7 +74,6 @@ def update_datastore(id, body):
def add_datastore(body): def add_datastore(body):
""" add a new datastore item """ """ add a new datastore item """
print(body)
if body.get(id, None): if body.get(id, None):
raise ApiError('id_specified', 'You may not specify an id for a new datastore item') raise ApiError('id_specified', 'You may not specify an id for a new datastore item')

View File

@ -33,8 +33,6 @@ pet_label = enum_label(task='task_pet_form',field='pet',value='1') // might r
# get the field information for the provided task_name (NOT the current task) # get the field information for the provided task_name (NOT the current task)
workflow_model = db.session.query(WorkflowModel).filter(WorkflowModel.id == workflow_id).first() workflow_model = db.session.query(WorkflowModel).filter(WorkflowModel.id == workflow_id).first()
field = self.find_field(task_name, field_name, spiff_task.workflow) field = self.find_field(task_name, field_name, spiff_task.workflow)
print(field)
if field.type == Task.FIELD_TYPE_AUTO_COMPLETE: if field.type == Task.FIELD_TYPE_AUTO_COMPLETE:
return self.lookup_label(workflow_model, task_name, field, value) return self.lookup_label(workflow_model, task_name, field, value)
elif field.has_property(Task.FIELD_PROP_SPREADSHEET_NAME): elif field.has_property(Task.FIELD_PROP_SPREADSHEET_NAME):

View File

@ -24,7 +24,6 @@ class FactService(Script):
self.do_task(task, study_id, workflow_id, **kwargs) self.do_task(task, study_id, workflow_id, **kwargs)
def do_task(self, task, study_id, workflow_id, **kwargs): def do_task(self, task, study_id, workflow_id, **kwargs):
print(task.data)
if "type" not in task.data: if "type" not in task.data:
raise Exception("No Fact Provided.") raise Exception("No Fact Provided.")
@ -41,6 +40,4 @@ class FactService(Script):
details = "unknown fact type." details = "unknown fact type."
#self.add_data_to_task(task, details) #self.add_data_to_task(task, details)
print(details)
return details return details

View File

@ -66,7 +66,6 @@ class GitService(object):
repo = self.setup_repo(remote_path, directory) repo = self.setup_repo(remote_path, directory)
except Exception as e: except Exception as e:
print(e)
app.logger.error(e) app.logger.error(e)
raise ApiError(code='unknown_exception', raise ApiError(code='unknown_exception',
message=f'There was an unknown exception. Original message is: {e}') message=f'There was an unknown exception. Original message is: {e}')
@ -118,11 +117,11 @@ class GitService(object):
try: try:
repo.remotes.origin.pull() repo.remotes.origin.pull()
except GitCommandError as ce: except GitCommandError as ce:
print(ce) raise ApiError(code='git_command_error',
message='Error Running Git Command:' + str(ce))
else: else:
raise ApiError(code='dirty_repo', raise ApiError(code='dirty_repo',
message='You have modified or untracked files. Please fix this before attempting to pull.') message='You have modified or untracked files. Please fix this before attempting to pull.')
print(repo)
return repo return repo
def merge_with_branch(self, branch): def merge_with_branch(self, branch):

View File

@ -31,17 +31,36 @@ Please Introduce yourself.
Hi Dan, This is a jinja template too! Hi Dan, This is a jinja template too!
Cool Right? Cool Right?
""" """
template_pattern = re.compile('{ ?% ?include\s*[\'\"](\w+)[\'\"]\s*?-?%}', re.DOTALL)
@staticmethod @staticmethod
def get_content(input_template, data): def get_content(input_template, data):
templates = data templates = {}
references = JinjaService.template_references(input_template)
for ref in references:
if ref in data.keys():
templates[ref] = data[ref]
else:
raise ApiError("missing_template", f"Your documentation imports a template that doest not exist: {ref}")
templates['main_template'] = input_template templates['main_template'] = input_template
jinja2_env = Environment(loader=DictLoader(templates)) jinja2_env = Environment(loader=DictLoader(templates))
# We just make a call here and let any errors percolate up to the calling method # We just make a call here and let any errors percolate up to the calling method
template = jinja2_env.get_template('main_template') template = jinja2_env.get_template('main_template')
return template.render(**data) try:
result = template.render(**data)
except AttributeError as ae:
if str(ae) == '\'NoneType\' object has no attribute \'splitlines\'':
raise ApiError("template_error", "Error processing template. You may be using a wordwrap "
"with a field that has no value.")
return result
@staticmethod
def template_references(input_template):
"""Using regex, determine what other templates are included, and return a list of those names."""
matches = JinjaService.template_pattern.findall(input_template)
return matches
# #
# The rest of this is for using Word documents as Jinja templates # The rest of this is for using Word documents as Jinja templates
# #

View File

@ -84,7 +84,6 @@ class LookupService(object):
# to rebuild. # to rebuild.
workflow_spec = WorkflowSpecService().get_spec(workflow.workflow_spec_id) workflow_spec = WorkflowSpecService().get_spec(workflow.workflow_spec_id)
timestamp = SpecFileService.timestamp(workflow_spec, lookup_model.file_name) timestamp = SpecFileService.timestamp(workflow_spec, lookup_model.file_name)
print(f"*** Comparing {timestamp} and {lookup_model.file_timestamp}")
# Assures we have the same timestamp, as storage in the database might create slight variations in # Assures we have the same timestamp, as storage in the database might create slight variations in
# the floating point values, just assure they values match to within a second. # the floating point values, just assure they values match to within a second.
is_current = int(timestamp - lookup_model.file_timestamp) == 0 is_current = int(timestamp - lookup_model.file_timestamp) == 0

View File

@ -31,7 +31,6 @@ SPEC_SCHEMA = WorkflowSpecModelSchema()
def remove_all_json_files(path): def remove_all_json_files(path):
for json_file in pathlib.Path(path).glob('*.json'): for json_file in pathlib.Path(path).glob('*.json'):
print("removing ", json_file)
os.remove(json_file) os.remove(json_file)
@ -41,7 +40,6 @@ def update_workflows_for_category(path, schemas, category_id):
new_path = os.path.join(path, schema.id) new_path = os.path.join(path, schema.id)
if (os.path.exists(orig_path)): if (os.path.exists(orig_path)):
os.rename(orig_path, new_path) os.rename(orig_path, new_path)
print(new_path)
update_spec(new_path, schema, category_id) update_spec(new_path, schema, category_id)

View File

@ -1,10 +1,9 @@
import copy import copy
import json import json
import sys
import time
import traceback
import random import random
import string import string
import sys
import traceback
from datetime import datetime from datetime import datetime
from typing import List from typing import List
@ -19,7 +18,7 @@ from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask
from SpiffWorkflow.exceptions import WorkflowTaskExecException from SpiffWorkflow.exceptions import WorkflowTaskExecException
from SpiffWorkflow.specs import CancelTask, StartTask from SpiffWorkflow.specs import CancelTask, StartTask
from SpiffWorkflow.util.deep_merge import DeepMerge from SpiffWorkflow.util.deep_merge import DeepMerge
from SpiffWorkflow.util.metrics import timeit, firsttime, sincetime from sentry_sdk import capture_message, push_scope
from sqlalchemy.exc import InvalidRequestError from sqlalchemy.exc import InvalidRequestError
from crc import db, app, session from crc import db, app, session
@ -32,7 +31,6 @@ from crc.models.task_event import TaskEventModel
from crc.models.user import UserModel from crc.models.user import UserModel
from crc.models.workflow import WorkflowModel, WorkflowStatus from crc.models.workflow import WorkflowModel, WorkflowStatus
from crc.services.data_store_service import DataStoreBase from crc.services.data_store_service import DataStoreBase
from crc.services.document_service import DocumentService from crc.services.document_service import DocumentService
from crc.services.jinja_service import JinjaService from crc.services.jinja_service import JinjaService
from crc.services.lookup_service import LookupService from crc.services.lookup_service import LookupService
@ -42,8 +40,6 @@ from crc.services.user_service import UserService
from crc.services.workflow_processor import WorkflowProcessor from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_spec_service import WorkflowSpecService from crc.services.workflow_spec_service import WorkflowSpecService
from sentry_sdk import capture_message, push_scope
class WorkflowService(object): class WorkflowService(object):
TASK_ACTION_COMPLETE = "COMPLETE" TASK_ACTION_COMPLETE = "COMPLETE"
@ -203,12 +199,19 @@ class WorkflowService(object):
task, task,
add_docs_and_forms=True) # Assure we try to process the documentation, and raise those errors. add_docs_and_forms=True) # Assure we try to process the documentation, and raise those errors.
# make sure forms have a form key # make sure forms have a form key
if hasattr(task_api, 'form') and task_api.form is not None and task_api.form.key == '': if hasattr(task_api, 'form') and task_api.form is not None:
if task_api.form.key == '':
raise ApiError(code='missing_form_key', raise ApiError(code='missing_form_key',
message='Forms must include a Form Key.', message='Forms must include a Form Key.',
task_id=task.id, task_id=task.id,
task_name=task.get_name()) task_name=task.get_name())
WorkflowService.populate_form_with_random_data(task, task_api, required_only) WorkflowService.populate_form_with_random_data(task, task_api, required_only)
if not WorkflowService.validate_form(task, task_api):
# In the process of completing the form, it is possible for fields to become required
# based on later fields. If the form has incomplete, but required fields (validate_form)
# then try to populate the form again, with this new information.
WorkflowService.populate_form_with_random_data(task, task_api, required_only)
processor.complete_task(task) processor.complete_task(task)
if test_until == task.task_spec.name: if test_until == task.task_spec.name:
raise ApiError.from_task( raise ApiError.from_task(
@ -238,6 +241,26 @@ class WorkflowService(object):
WorkflowService.delete_test_data(workflow_model) WorkflowService.delete_test_data(workflow_model)
return processor.bpmn_workflow.last_task.data return processor.bpmn_workflow.last_task.data
@staticmethod
def validate_form(task, task_api):
for field in task_api.form.fields:
if WorkflowService.is_required_field(field, task):
if not field.id in task.data or task.data[field.id] is None:
return False
return True
@staticmethod
def is_required_field(field, task):
# Get Required State
is_required = False
if (field.has_validation(Task.FIELD_CONSTRAINT_REQUIRED) and
field.get_validation(Task.FIELD_CONSTRAINT_REQUIRED)):
is_required = True
if (field.has_property(Task.FIELD_PROP_REQUIRED_EXPRESSION) and
WorkflowService.evaluate_property(Task.FIELD_PROP_REQUIRED_EXPRESSION, field, task)):
is_required = True
return is_required
@staticmethod @staticmethod
def populate_form_with_random_data(task, task_api, required_only): def populate_form_with_random_data(task, task_api, required_only):
"""populates a task with random data - useful for testing a spec.""" """populates a task with random data - useful for testing a spec."""
@ -256,6 +279,8 @@ class WorkflowService(object):
form_data[field.id] = None form_data[field.id] = None
for field in task_api.form.fields: for field in task_api.form.fields:
is_required = WorkflowService.is_required_field(field, task)
# Assure we have a field type # Assure we have a field type
if field.type is None: if field.type is None:
raise ApiError(code='invalid_form_data', raise ApiError(code='invalid_form_data',
@ -284,19 +309,13 @@ class WorkflowService(object):
task=task) task=task)
# If a field is hidden and required, it must have a default value # If a field is hidden and required, it must have a default value
if field.has_property(Task.FIELD_PROP_HIDE_EXPRESSION) and field.has_validation( # if field.has_property(Task.FIELD_PROP_HIDE_EXPRESSION) and field.has_validation(
Task.FIELD_CONSTRAINT_REQUIRED): # Task.FIELD_CONSTRAINT_REQUIRED):
if field.default_value is None: # if field.default_value is None:
raise ApiError(code='hidden and required field missing default', # raise ApiError(code='hidden and required field missing default',
message=f'Field "{field.id}" is required but can be hidden. It must have a default value.', # message=f'Field "{field.id}" is required but can be hidden. It must have a def1ault value.',
task_id='task.id', # task_id='task.id',
task_name=task.get_name()) # task_name=task.get_name())
# If the field is hidden and not required, it should not produce a value.
if field.has_property(Task.FIELD_PROP_HIDE_EXPRESSION) and not field.has_validation(
Task.FIELD_CONSTRAINT_REQUIRED):
if WorkflowService.evaluate_property(Task.FIELD_PROP_HIDE_EXPRESSION, field, task):
continue
# If we have a default_value, try to set the default # If we have a default_value, try to set the default
if field.default_value: if field.default_value:
@ -306,21 +325,21 @@ class WorkflowService(object):
raise ApiError.from_task("bad default value", f'The default value "{field.default_value}" in field {field.id} ' raise ApiError.from_task("bad default value", f'The default value "{field.default_value}" in field {field.id} '
f'could not be understood or evaluated. ', f'could not be understood or evaluated. ',
task=task) task=task)
if not field.has_property(Task.FIELD_PROP_REPEAT): # If we have a good default value, and we aren't dealing with a repeat, we can stop here.
if form_data[field.id] is not None and not field.has_property(Task.FIELD_PROP_REPEAT):
continue continue
else: else:
form_data[field.id] = None form_data[field.id] = None
# If the field is hidden we can leave it as none.
if field.has_property(Task.FIELD_PROP_HIDE_EXPRESSION):
if WorkflowService.evaluate_property(Task.FIELD_PROP_HIDE_EXPRESSION, field, task):
continue
# If we are only populating required fields, and this isn't required. stop here. # If we are only populating required fields, and this isn't required. stop here.
if required_only: if required_only:
if (not field.has_validation(Task.FIELD_CONSTRAINT_REQUIRED) or if not is_required:
field.get_validation(Task.FIELD_CONSTRAINT_REQUIRED).lower().strip() != "true"):
continue # Don't include any fields that aren't specifically marked as required. continue # Don't include any fields that aren't specifically marked as required.
if field.has_property(Task.FIELD_PROP_REQUIRED_EXPRESSION):
result = WorkflowService.evaluate_property(Task.FIELD_PROP_REQUIRED_EXPRESSION, field, task)
if not result and required_only:
continue # Don't complete fields that are not required.
# If it is read only, stop here. # If it is read only, stop here.
if field.has_property("read_only") and field.get_property( if field.has_property("read_only") and field.get_property(
@ -1083,14 +1102,6 @@ class WorkflowService(object):
db.session.commit() db.session.commit()
return workflow_model return workflow_model
@staticmethod
def get_standalone_workflow_specs():
return spec_service.standalone.values()
@staticmethod
def get_library_workflow_specs():
return spec_service.libraries.values()
@staticmethod @staticmethod
def delete_workflow_spec_task_events(spec_id): def delete_workflow_spec_task_events(spec_id):
session.query(TaskEventModel).filter(TaskEventModel.workflow_spec_id == spec_id).delete() session.query(TaskEventModel).filter(TaskEventModel.workflow_spec_id == spec_id).delete()

View File

@ -1,6 +0,0 @@
from crc.api.file import get_document_directory
def render_files(study_id,irb_codes):
files = get_document_directory(study_id)
print(files)

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1v1rp1q" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.10.0">
<bpmn:process id="Required" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0lvudp8</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_0lvudp8" sourceRef="StartEvent_1" targetRef="Task_Required_Fields" />
<bpmn:endEvent id="EndEvent_0q4qzl9">
<bpmn:incoming>Flow_0payrur</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_02vev7n" sourceRef="Task_Required_Fields" targetRef="Activity_0kbvgue" />
<bpmn:userTask id="Task_Required_Fields" name="Required fields" camunda:formKey="RequiredForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="required_if_true" label="&#39;String&#39;" type="string" defaultValue="&#39;some string&#39;">
<camunda:properties>
<camunda:property id="required_expression" value="boolean_field" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="boolean_field" label="&#39;My Boolean&#39;" type="boolean" defaultValue="True" />
<camunda:formField id="required_if_false" label="&#39;some label&#39;" type="string">
<camunda:properties>
<camunda:property id="required_expression" value="not boolean_field" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="always_set" type="string" defaultValue="&#34;always&#34;">
<camunda:properties>
<camunda:property id="hide_expression" value="True" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0lvudp8</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_02vev7n</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0payrur" sourceRef="Activity_0kbvgue" targetRef="EndEvent_0q4qzl9" />
<bpmn:scriptTask id="Activity_0kbvgue" name="Verify Script">
<bpmn:incoming>SequenceFlow_02vev7n</bpmn:incoming>
<bpmn:outgoing>Flow_0payrur</bpmn:outgoing>
<bpmn:script># By directly referencing the variables
# we can assure that whatever happens in
# validation, we won't have an error.
if boolean_field:
result = required_if_true
else:
result = required_if_false
# Note that hidden fields with a default
# value should always exist.
if not always_set == "always":
should_never_get_here
</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Required">
<bpmndi:BPMNEdge id="SequenceFlow_02vev7n_di" bpmnElement="SequenceFlow_02vev7n">
<di:waypoint x="370" y="117" />
<di:waypoint x="440" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0lvudp8_di" bpmnElement="SequenceFlow_0lvudp8">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0payrur_di" bpmnElement="Flow_0payrur">
<di:waypoint x="540" y="117" />
<di:waypoint x="592" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_18ly1yq_di" bpmnElement="Task_Required_Fields">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0q4qzl9_di" bpmnElement="EndEvent_0q4qzl9">
<dc:Bounds x="592" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0ks06ox_di" bpmnElement="Activity_0kbvgue">
<dc:Bounds x="440" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -76,7 +76,26 @@ class TestJinjaService(BaseTest):
self.assertEquals("Word Document creation error : unexpected '%'", ae.exception.message) self.assertEquals("Word Document creation error : unexpected '%'", ae.exception.message)
self.assertEquals(14, ae.exception.line_number) self.assertEquals(14, ae.exception.line_number)
def test_find_template_references(self):
test_string = """
{ % include 'template_1' %}
{ % include
'template_2' %}
{ % include 'template_3' -%}
{% include 'template_4'%}
"""
self.assertEqual(['template_1', 'template_2', 'template_3', 'template_4'], JinjaService().template_references(test_string))
def test_better_error_message_for_wordwrap(self):
data = {"my_val": None}
my_tempate = "{{my_val | wordwrap(70)}}"
with self.assertRaises(ApiError) as e:
result = JinjaService().get_content(my_tempate, data)
self.assertEqual(e.exception.message, 'Error processing template. You may be using a wordwrap '
'with a field that has no value.')
def test_jinja_service_properties(self): def test_jinja_service_properties(self):
pass pass

View File

@ -1,9 +1,12 @@
from unittest import skip
from tests.base_test import BaseTest from tests.base_test import BaseTest
import json import json
class TestWorkflowHiddenRequiredField(BaseTest): class TestWorkflowHiddenRequiredField(BaseTest):
@skip("Maybe we don't need to require a default for required hidden fields after all.")
def test_require_default(self): def test_require_default(self):
# We have a field that can be hidden and required. # We have a field that can be hidden and required.
# Validation should fail if we don't have a default value. # Validation should fail if we don't have a default value.

View File

@ -173,3 +173,17 @@ class TestWorkflowSpecValidation(BaseTest):
self.create_reference_document() self.create_reference_document()
errors = self.validate_workflow("date_value_expression") errors = self.validate_workflow("date_value_expression")
self.assertEqual(0, len(errors)) self.assertEqual(0, len(errors))
def test_fields_required_based_on_later_fields_correctly_populates(self):
"""Say you have a form, where the first field is required only if the
SECOND field is checked true. This assures such a case will validate and
that the variables that should exist (because they are required) do exist.
As a bonus test, we also assert that a field with a default value is always present
regardless of it's hidden status.
"""
self.load_test_spec('empty_workflow', master_spec=True)
self.create_reference_document()
errors = self.validate_workflow("required_expressions")
self.assertEqual(0, len(errors))