Skip to content

Commit

Permalink
Merge pull request #34929 from dimagi/ze/arcgis-form-repeater
Browse files Browse the repository at this point in the history
ArcGIS Integration Form Repeater
  • Loading branch information
zandre-eng authored Aug 8, 2024
2 parents 06fdee9 + 6f7d632 commit c47e7f7
Show file tree
Hide file tree
Showing 13 changed files with 313 additions and 18 deletions.
18 changes: 18 additions & 0 deletions corehq/motech/migrations/0014_alter_connectionsettings_password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.11 on 2024-07-30 10:18

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('motech', '0013_alter_connectionsettings_auth_type'),
]

operations = [
migrations.AlterField(
model_name='connectionsettings',
name='password',
field=models.CharField(blank=True, max_length=1023),
),
]
2 changes: 1 addition & 1 deletion corehq/motech/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class ConnectionSettings(models.Model):
choices=api_auth_settings_choices,
)
username = models.CharField(max_length=255, null=True, blank=True)
password = models.CharField(max_length=255, blank=True)
password = models.CharField(max_length=1023, blank=True)
# OAuth 2.0 Password Grant needs username, password, client_id & client_secret
client_id = models.CharField(max_length=255, null=True, blank=True)
client_secret = models.CharField(max_length=255, blank=True)
Expand Down
26 changes: 26 additions & 0 deletions corehq/motech/repeaters/expression/repeater_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,29 @@ def get_url(self, repeat_record, url_template, payload_doc):
for template_var in required_template_vars
}
)


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)
):
return ""

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

def get_payload(self, repeat_record, payload_doc, parsed_expression):
payload_doc_json = payload_doc.to_json()
payload = parsed_expression(payload_doc_json, EvaluationContext(payload_doc_json))
conn_settings = repeat_record.repeater.connection_settings
api_token = conn_settings.plaintext_password
formatted_payload = {
'features': json.dumps([payload]),
'f': 'json',
'token': api_token,
}
return formatted_payload
46 changes: 44 additions & 2 deletions corehq/motech/repeaters/expression/repeaters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
from corehq.apps.userreports.expressions.factory import ExpressionFactory
from corehq.apps.userreports.filters.factory import FilterFactory
from corehq.apps.userreports.specs import EvaluationContext, FactoryContext
from corehq.form_processor.models import CommCareCase
from corehq.form_processor.models import CommCareCase, XFormInstance
from corehq.motech.repeaters.expression.repeater_generators import (
ExpressionPayloadGenerator,
)
from corehq.motech.repeaters.models import OptionValue, Repeater
from corehq.toggles import EXPRESSION_REPEATER
from corehq.motech.repeaters.expression.repeater_generators import ArcGISFormExpressionPayloadGenerator
from corehq.toggles import EXPRESSION_REPEATER, ARCGIS_INTEGRATION


class BaseExpressionRepeater(Repeater):
Expand Down Expand Up @@ -78,3 +79,44 @@ 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()


class FormExpressionRepeater(BaseExpressionRepeater):

friendly_name = _("Configurable Form Repeater")

class Meta:
app_label = 'repeaters'
proxy = True

@property
def form_class_name(self):
return 'FormExpressionRepeater'

@memoized
def payload_doc(self, repeat_record):
return XFormInstance.objects.get_form(
repeat_record.payload_id,
repeat_record.domain,
)


class ArcGISFormExpressionRepeater(FormExpressionRepeater):

friendly_name = _("Configurable ArcGIS Form Repeater")
payload_generator_classes = (ArcGISFormExpressionPayloadGenerator,)

class Meta:
app_label = 'repeaters'
proxy = True

@property
def form_class_name(self):
return 'ArcGISFormExpressionRepeater'

@classmethod
def available_for_domain(cls, domain):
return (
super(ArcGISFormExpressionRepeater, cls).available_for_domain(domain)
and ARCGIS_INTEGRATION.enabled(domain)
)
171 changes: 157 additions & 14 deletions corehq/motech/repeaters/expression/tests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from datetime import datetime, timedelta
import uuid

from django.test import TestCase

Expand All @@ -9,15 +10,20 @@
from corehq.apps.accounting.tests.utils import DomainSubscriptionMixin
from corehq.apps.accounting.utils import clear_plan_version_cache
from corehq.apps.domain.shortcuts import create_domain
from corehq.apps.receiverwrapper.util import submit_form_locally
from corehq.apps.userreports.models import UCRExpression
from corehq.motech.models import ConnectionSettings
from corehq.motech.repeaters.expression.repeaters import CaseExpressionRepeater
from corehq.motech.repeaters.expression.repeaters import (
CaseExpressionRepeater,
ArcGISFormExpressionRepeater
)
from corehq.motech.repeaters.models import RepeatRecord
from corehq.util.test_utils import flag_enabled

from casexml.apps.case.mock import CaseBlock

@flag_enabled('EXPRESSION_REPEATER')
class CaseExpressionRepeaterTest(TestCase, DomainSubscriptionMixin):

class BaseExpressionRepeaterTest(TestCase, DomainSubscriptionMixin):
@classmethod
def setUpClass(cls):
super().setUpClass()
Expand All @@ -33,9 +39,27 @@ def setUpClass(cls):

url = 'fake-url'
cls.connection = ConnectionSettings.objects.create(domain=cls.domain, name=url, url=url)
cls.repeater = CaseExpressionRepeater(
domain=cls.domain,
connection_settings_id=cls.connection.id,

def setUp(self):
self._create_repeater()

def _create_repeater(self):
pass

@classmethod
def repeat_records(cls, domain_name):
# Enqueued repeat records have next_check set 48 hours in the future.
later = datetime.utcnow() + timedelta(hours=48 + 1)
return RepeatRecord.objects.filter(domain=domain_name, next_check__lt=later)


@flag_enabled('EXPRESSION_REPEATER')
class CaseExpressionRepeaterTest(BaseExpressionRepeaterTest):

def _create_repeater(self):
self.repeater = CaseExpressionRepeater(
domain=self.domain,
connection_settings_id=self.connection.id,
configured_filter={
"type": "or",
"filters": [
Expand Down Expand Up @@ -80,14 +104,7 @@ def setUpClass(cls):
}
}
)

cls.repeater.save()

@classmethod
def repeat_records(cls, domain_name):
# Enqueued repeat records have next_check set 48 hours in the future.
later = datetime.utcnow() + timedelta(hours=48 + 1)
return RepeatRecord.objects.filter(domain=domain_name, next_check__lt=later)
self.repeater.save()

def test_filter_cases(self):
forwardable_case = self.factory.create_case(case_type='forward-me')
Expand Down Expand Up @@ -142,3 +159,129 @@ def test_custom_url(self):
self.repeater.get_url(repeat_record),
expected_url
)


class ArcGISExpressionRepeaterTest(BaseExpressionRepeaterTest):

xmlns = 'http://foo.org/bar/123'
xform_xml_template = """<?xml version='1.0' ?>
<data xmlns:jrm="http://dev.commcarehq.org/jr/xforms" xmlns="{}">
<person_name>Timmy</person_name>
<gps_coordinate>1.1 2.2</gps_coordinate>
<meta>
<deviceID>O2XLT0WZW97W1A91E2W1Y0NJG</deviceID>
<timeStart>2011-10-01T15:25:18.404-04</timeStart>
<timeEnd>2011-10-01T15:26:29.551-04</timeEnd>
<username>admin</username>
<userID>1234</userID>
<instanceID>{}</instanceID>
</meta>
{}
</data>
"""

def _create_repeater(self):
self.repeater = ArcGISFormExpressionRepeater(
domain=self.domain,
connection_settings_id=self.connection.id,
configured_filter={
"type": "boolean_expression",
"expression": {
"type": "property_name",
"property_name": "xmlns",
},
"operator": "eq",
"property_value": self.xmlns,
},
configured_expression={
"type": "dict",
"properties": {
"attributes": {
"type": "dict",
"properties": {
"name": {
"type": "property_path",
"property_path": ["form", "person_name"],
},
},
},
"geometry": {
"type": "dict",
"properties": {
"x": {
"datatype": "decimal",
"type": "split_string",
"string_expression": {
"type": "property_path",
"property_path": ["form", "gps_coordinate"],
},
"index_expression": {"type": "constant", "constant": 1},
},
"y": {
"datatype": "decimal",
"type": "split_string",
"string_expression": {
"type": "property_path",
"property_path": ["form", "gps_coordinate"],
},
"index_expression": {"type": "constant", "constant": 0},
},
"spatialReference": {
"type": "dict",
"properties": {
"wkid": {"type": "constant", "constant": 4326}
},
},
},
},
},
},
)
self.repeater.save()

def _create_case_block(self):
return CaseBlock(
create=True,
case_id=uuid.uuid4().hex,
case_type='person',
case_name=uuid.uuid4().hex,
).as_text()

def _create_case(self, xmlns):
return self.factory.post_case_blocks(
[self._create_case_block()],
xmlns=xmlns,
)[0]

def test_filter_forms(self):
forwardable_form = self._create_case(self.xmlns)
self._create_case(xmlns='http://do-not.org/forward')
self.assertEqual(RepeatRecord.objects.filter(domain=self.domain).count(), 1)
repeat_records = self.repeat_records(self.domain).all()
self.assertEqual(repeat_records[0].payload_id, forwardable_form.form_id)

def test_payload(self):
instance_id = uuid.uuid4().hex
xform_xml = self.xform_xml_template.format(
self.xmlns,
instance_id,
self._create_case_block(),
)
submit_form_locally(xform_xml, self.domain)
repeat_record = self.repeat_records(self.domain).all()[0]
self.assertEqual(repeat_record.get_payload(), {
'features': json.dumps([{
'attributes': {
'name': 'Timmy',
},
'geometry': {
'x': '2.2',
'y': '1.1',
'spatialReference': {
'wkid': 4326
}
}
}]),
'f': 'json',
'token': ''
})
9 changes: 9 additions & 0 deletions corehq/motech/repeaters/expression/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,12 @@ def set_repeater_attr(self, repeater, cleaned_data):
class EditCaseExpressionRepeaterView(EditRepeaterView, AddCaseExpressionRepeaterView):
urlname = 'edit_case_expression_repeater'
page_title = _("Edit Case Repeater")


class AddArcGISFormExpressionRepeaterView(AddCaseExpressionRepeaterView):
urlname = 'add_arcgis_form_expression_repeater'


class EditArcGISFormExpressionRepeaterView(EditRepeaterView, AddCaseExpressionRepeaterView):
urlname = 'edit_arcgis_form_expression_repeater'
page_title = _("Edit ArcGIS Form Repeater")
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.11 on 2024-07-24 13:53

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('repeaters', '0011_remove_obsolete_entities'),
]

operations = [
migrations.CreateModel(
name='FormExpressionRepeater',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('repeaters.baseexpressionrepeater',),
),
migrations.CreateModel(
name='ArcGISFormExpressionRepeater',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('repeaters.formexpressionrepeater',),
),
]
2 changes: 1 addition & 1 deletion corehq/motech/repeaters/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ def register(self, payload, fire_synchronously=False):

def allowed_to_forward(self, payload):
"""
Return True/False depending on whether the payload meets forawrding criteria or not
Return True/False depending on whether the payload meets forwarding criteria or not
"""
return True

Expand Down
2 changes: 2 additions & 0 deletions corehq/motech/repeaters/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@

def create_form_repeat_records(sender, xform, **kwargs):
from corehq.motech.repeaters.models import FormRepeater
from corehq.motech.repeaters.expression.repeaters import ArcGISFormExpressionRepeater
if not xform.is_duplicate:
create_repeat_records(FormRepeater, xform)
create_repeat_records(ArcGISFormExpressionRepeater, xform)


def create_case_repeat_records(sender, case, **kwargs):
Expand Down
Loading

0 comments on commit c47e7f7

Please sign in to comment.