Skip to content

Commit

Permalink
Merge pull request #35244 from dimagi/sk/expression-repeater-fixes
Browse files Browse the repository at this point in the history
More fixes and tweaks for expression repeaters
  • Loading branch information
snopoke authored Oct 22, 2024
2 parents f089399 + b5a8eb7 commit 2cde1b2
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
{% if attempt.message %}
- <div class="well record-attempt" style="font-family: monospace;">
+ <div class="card record-attempt" style="font-family: monospace;"> {# todo B5: css:well, inline style #}
{{ attempt.message }}
{{ attempt.message|escape|linebreaksbr }}
</div>
{% endif %}
@@ -53,7 +53,7 @@
Expand Down
32 changes: 13 additions & 19 deletions corehq/motech/repeaters/expression/repeater_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ def content_type(self):
return 'application/json'

def get_payload(self, repeat_record, payload_doc, parsed_expression):
result = parsed_expression(payload_doc, EvaluationContext(payload_doc))
result = _generate_payload(payload_doc, parsed_expression)
return json.dumps(result, cls=DjangoJSONEncoder)

def get_url(self, repeat_record, url_template, payload_doc):
if not toggles.UCR_EXPRESSION_REGISTRY.enabled(repeat_record.domain):
return ""

required_template_vars = [fn for _, fn, _, _ in Formatter().parse(url_template) if fn is not None]
context = EvaluationContext(payload_doc)
payload_doc_json = payload_doc.to_json()
context = EvaluationContext(payload_doc_json)
expressions = {
expression.name: expression.wrapped_definition(context)
for expression in UCRExpression.objects.filter(
Expand All @@ -35,37 +36,25 @@ def get_url(self, repeat_record, url_template, payload_doc):
}
return url_template.format(
**{
template_var: expressions[template_var](payload_doc) if template_var in expressions else ""
template_var: expressions[template_var](payload_doc_json) if template_var in expressions else ""
for template_var in required_template_vars
}
)


class FormExpressionPayloadGenerator(ExpressionPayloadGenerator):
def get_payload(self, repeat_record, payload_doc, parsed_expression):
result = self._parse_payload(payload_doc, parsed_expression)
return json.dumps(result, cls=DjangoJSONEncoder)

def _parse_payload(self, payload_doc, parsed_expression):
payload_doc_json = payload_doc.to_json()
return parsed_expression(payload_doc_json, EvaluationContext(payload_doc_json))


class ArcGISFormExpressionPayloadGenerator(FormExpressionPayloadGenerator):
class ArcGISFormExpressionPayloadGenerator(ExpressionPayloadGenerator):

def get_url(self, repeat_record, url_template, payload_doc):
if not (
toggles.UCR_EXPRESSION_REGISTRY.enabled(repeat_record.domain)
and toggles.ARCGIS_INTEGRATION.enabled(repeat_record.domain)
):
if not toggles.ARCGIS_INTEGRATION.enabled(repeat_record.domain):
return ""
return super().get_url(repeat_record, url_template, payload_doc)

@property
def content_type(self):
return 'application/x-www-form-urlencoded'

def get_payload(self, repeat_record, payload_doc, parsed_expression):
payload = self._parse_payload(payload_doc, parsed_expression)
payload = _generate_payload(payload_doc, parsed_expression)
conn_settings = repeat_record.repeater.connection_settings
api_token = conn_settings.plaintext_password
formatted_payload = {
Expand All @@ -74,3 +63,8 @@ def get_payload(self, repeat_record, payload_doc, parsed_expression):
'token': api_token,
}
return formatted_payload


def _generate_payload(payload_doc, parsed_expression):
payload_doc_json = payload_doc.to_json()
return parsed_expression(payload_doc_json, EvaluationContext(payload_doc_json))
22 changes: 12 additions & 10 deletions corehq/motech/repeaters/expression/repeaters.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from corehq.motech.repeaters.expression.repeater_generators import (
ArcGISFormExpressionPayloadGenerator,
ExpressionPayloadGenerator,
FormExpressionPayloadGenerator,
)
from corehq.motech.repeaters.models import (
OptionValue,
Expand Down Expand Up @@ -108,34 +107,38 @@ def get_url(self, repeat_record):
return base_url

def handle_response(self, response, repeat_record):
super().handle_response(response, repeat_record)
attempt = super().handle_response(response, repeat_record)
if self.case_action_filter_expression and is_response(response):
try:
self._process_response_as_case_update(response, repeat_record)
message = self._process_response_as_case_update(response, repeat_record)
except Exception as e:
notify_exception(None, "Error processing response from Repeater request", e)
else:
attempt.message += f"\n\n{message}"
attempt.save()

def _process_response_as_case_update(self, response, repeat_record):
domain = repeat_record.domain
context = get_evaluation_context(domain, repeat_record, self.payload_doc(repeat_record), response)
if not self.parsed_case_action_filter(context.root_doc, context):
return False
return "Response did not match filter"

self._perform_case_update(domain, context)
return True
form_id = self._perform_case_update(domain, context)
return f"Response generated a form: {form_id}"

def _perform_case_update(self, domain, context):
data = self.parsed_case_action_expression(context.root_doc, context)
if data:
data = data if isinstance(data, list) else [data]
handle_case_update(
form, _ = handle_case_update(
domain=domain,
data=data,
user=UserDuck('system', ''),
device_id=self.device_id,
is_creation=False,
xmlns=REPEATER_RESPONSE_XMLNS,
)
return form.form_id

@property
def device_id(self):
Expand All @@ -156,7 +159,7 @@ def form_class_name(self):

@memoized
def payload_doc(self, repeat_record):
return CommCareCase.objects.get_case(repeat_record.payload_id, repeat_record.domain).to_json()
return CommCareCase.objects.get_case(repeat_record.payload_id, repeat_record.domain)

def allowed_to_forward(self, payload):
allowed = super().allowed_to_forward(payload)
Expand All @@ -183,7 +186,6 @@ def allowed_to_forward(self, payload):
class FormExpressionRepeater(BaseExpressionRepeater):

friendly_name = _("Configurable Form Repeater")
payload_generator_classes = (FormExpressionPayloadGenerator,)

class Meta:
app_label = 'repeaters'
Expand Down Expand Up @@ -282,7 +284,7 @@ def get_evaluation_context(domain, repeat_record, payload_doc, response):
'success': is_success_response(response),
'payload': {
'id': repeat_record.payload_id,
'doc': payload_doc,
'doc': payload_doc.to_json(),
},
'response': {
'status_code': response.status_code,
Expand Down
110 changes: 97 additions & 13 deletions corehq/motech/repeaters/expression/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,15 @@ def test_process_response_filter(self):
"operator": "eq",
"property_value": 201,
}
self.repeater._perform_case_update = Mock()
self.repeater._perform_case_update = Mock(return_value="fake_form_id")
response = MockResponse(200)
self.assertFalse(self.repeater._process_response_as_case_update(response, repeat_record))
message = self.repeater._process_response_as_case_update(response, repeat_record)
self.assertEqual(message, "Response did not match filter")
self.repeater._perform_case_update.assert_not_called()

response = MockResponse(201)
self.assertTrue(self.repeater._process_response_as_case_update(response, repeat_record))
message = self.repeater._process_response_as_case_update(response, repeat_record)
self.assertEqual(message, "Response generated a form: fake_form_id")
self.repeater._perform_case_update.assert_called()

def test_process_response(self):
Expand Down Expand Up @@ -315,8 +317,6 @@ class FormExpressionRepeaterTest(BaseExpressionRepeaterTest):
</data>
"""

case_id = uuid.uuid4().hex

def _create_repeater(self):
self.repeater = FormExpressionRepeater(
domain=self.domain,
Expand Down Expand Up @@ -351,10 +351,9 @@ def _create_repeater(self):
)
self.repeater.save()

@property
def expected_payload(self):
def expected_payload(self, case_id):
return json.dumps({
'case_id': self.case_id,
'case_id': case_id,
'properties': {
'meta_gps_point': '1.1 2.2 3.3 4.4'
}
Expand All @@ -369,14 +368,59 @@ def test_filter_forms(self):

def test_payload(self):
instance_id = uuid.uuid4().hex
case_id = uuid.uuid4().hex
xform_xml = self.xform_xml_template.format(
self.xmlns,
instance_id,
self._create_case_block(self.case_id),
self._create_case_block(case_id),
)
submit_form_locally(xform_xml, self.domain)
repeat_record = self.repeat_records(self.domain).all()[0]
self.assertEqual(repeat_record.get_payload(), self.expected_payload)
self.assertEqual(repeat_record.get_payload(), self.expected_payload(case_id))

def test_process_response(self):
instance_id = uuid.uuid4().hex
case_id = uuid.uuid4().hex
xform_xml = self.xform_xml_template.format(
self.xmlns,
instance_id,
self._create_case_block(case_id),
)
submit_form_locally(xform_xml, self.domain)
repeat_record = self.repeat_records(self.domain).all()[0]
self.repeater.case_action_filter_expression = {
"type": "boolean_expression",
"expression": {
"type": "jsonpath",
"jsonpath": "response.status_code",
},
"operator": "eq",
"property_value": 200,
}
self.repeater.case_action_expression = {
'type': 'dict',
'properties': {
'create': False,
'case_id': {
'type': 'jsonpath',
'jsonpath': 'payload.doc.form.case.@case_id',
},
'properties': {
'type': 'dict',
'properties': {
'type': 'dict',
'prop_from_response': {
'type': 'jsonpath',
'jsonpath': 'response.body.aValue',
}
}
}
}
}
response = MockResponse(200, '{"aValue": "aResponseValue"}')
self.repeater.handle_response(response, repeat_record)
case = CommCareCase.objects.get_case(case_id, self.domain)
self.assertEqual(case.get_case_property('prop_from_response'), 'aResponseValue')


class ArcGISExpressionRepeaterTest(FormExpressionRepeaterTest):
Expand Down Expand Up @@ -456,8 +500,7 @@ def _create_repeater(self):
)
self.repeater.save()

@property
def expected_payload(self):
def expected_payload(self, case_id):
return {
'features': json.dumps([{
'attributes': {
Expand All @@ -476,10 +519,11 @@ def expected_payload(self):
}

def test_send_request_error_handling(self):
case_id = uuid.uuid4().hex
xform_xml = self.xform_xml_template.format(
self.xmlns,
uuid.uuid4().hex,
self._create_case_block(self.case_id),
self._create_case_block(case_id),
)
with patch(
'corehq.motech.repeaters.models.simple_request',
Expand Down Expand Up @@ -552,6 +596,46 @@ def test_error_response_with_empty_error(self):
self.assertEqual(resp.reason, "[No error message given by ArcGIS]")
self.assertEqual(resp.text, "")

@flag_enabled("UCR_EXPRESSION_REGISTRY")
@flag_enabled("ARCGIS_INTEGRATION")
def test_custom_url(self):
self.repeater.url_template = "/{variable1}/a_thing/delete?case_id={case_id}&{missing_variable}='foo'"

UCRExpression.objects.create(
name='variable1',
domain=self.domain,
expression_type="named_expression",
definition={
"type": "jsonpath",
"jsonpath": "form.person_name"
},
)
UCRExpression.objects.create(
name='case_id',
domain=self.domain,
expression_type="named_expression",
definition={
"type": "jsonpath",
"jsonpath": "form.case.@case_id"
},
)

case_id = uuid.uuid4().hex
xform_xml = self.xform_xml_template.format(
self.xmlns,
uuid.uuid4().hex,
self._create_case_block(case_id),
)
submit_form_locally(xform_xml, self.domain)
repeat_record = self.repeat_records(self.domain).all()[0]

expected_url = self.connection.url + f"/Timmy/a_thing/delete?case_id={case_id}&='foo'"

self.assertEqual(
self.repeater.get_url(repeat_record),
expected_url
)


def test_doctests():
import corehq.motech.repeaters.expression.repeaters as module
Expand Down
Loading

0 comments on commit 2cde1b2

Please sign in to comment.