Skip to content

Commit

Permalink
Merge pull request #33426 from dimagi/ze/add-geoproperty-validation
Browse files Browse the repository at this point in the history
Add geospatial case property validation
zandre-eng authored Sep 8, 2023
2 parents fbde9e2 + 07d6a99 commit 37502e1
Showing 9 changed files with 157 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -23,14 +23,15 @@ hqDefine("data_dictionary/js/data_dictionary", [
DOMPurify,
toggles
) {
var caseType = function (name, fhirResourceType, deprecated, moduleCount) {
var caseType = function (name, fhirResourceType, deprecated, moduleCount, geoCaseProp) {
var self = {};
self.name = name || gettext("No Name");
self.deprecated = deprecated;
self.appCount = moduleCount; // The number of application modules using this case type
self.url = "#" + name;
self.fhirResourceType = ko.observable(fhirResourceType);
self.groups = ko.observableArray();
self.geoCaseProp = geoCaseProp;

self.init = function (groupData, changeSaveButton) {
for (let group of groupData) {
@@ -40,9 +41,10 @@ hqDefine("data_dictionary/js/data_dictionary", [
groupObj.toBeDeprecated.subscribe(changeSaveButton);

for (let prop of group.properties) {
const isGeoCaseProp = (self.geoCaseProp === prop.name);
var propObj = propertyListItem(prop.name, prop.label, false, prop.group, self.name, prop.data_type,
prop.description, prop.allowed_values, prop.fhir_resource_prop_path, prop.deprecated,
prop.removeFHIRResourcePropertyPath);
prop.removeFHIRResourcePropertyPath, isGeoCaseProp);
propObj.description.subscribe(changeSaveButton);
propObj.label.subscribe(changeSaveButton);
propObj.fhirResourcePropPath.subscribe(changeSaveButton);
@@ -87,7 +89,7 @@ hqDefine("data_dictionary/js/data_dictionary", [
};

var propertyListItem = function (name, label, isGroup, groupName, caseType, dataType, description, allowedValues,
fhirResourcePropPath, deprecated, removeFHIRResourcePropertyPath) {
fhirResourcePropPath, deprecated, removeFHIRResourcePropertyPath, isGeoCaseProp) {
var self = {};
self.name = name;
self.label = ko.observable(label);
@@ -100,6 +102,7 @@ hqDefine("data_dictionary/js/data_dictionary", [
self.fhirResourcePropPath = ko.observable(fhirResourcePropPath);
self.originalResourcePropPath = fhirResourcePropPath;
self.deprecated = ko.observable(deprecated || false);
self.isGeoCaseProp = isGeoCaseProp;
self.removeFHIRResourcePropertyPath = ko.observable(removeFHIRResourcePropertyPath || false);
let subTitle;
if (toggles.toggleEnabled("CASE_IMPORT_DATA_DICTIONARY_VALIDATION")) {
@@ -125,7 +128,19 @@ hqDefine("data_dictionary/js/data_dictionary", [
};

self.deprecateProperty = function () {
self.deprecated(true);
if (toggles.toggleEnabled('GEOSPATIAL') && self.isGeoCaseProp) {
self.confirmGeospatialDeprecation();
} else {
self.deprecated(true);
}
};

self.confirmGeospatialDeprecation = function () {
const $modal = $("#deprecate-geospatial-prop-modal").modal('show');
$("#deprecate-geospatial-prop-btn").off('click').on('click', function () {
self.deprecated(true);
$modal.modal('hide');
});
};

self.restoreProperty = function () {
@@ -234,6 +249,7 @@ hqDefine("data_dictionary/js/data_dictionary", [
caseTypeData.fhir_resource_type,
caseTypeData.is_deprecated,
caseTypeData.module_count,
data.geo_case_property
);
caseTypeObj.init(caseTypeData.groups, changeSaveButton);
self.caseTypes.push(caseTypeObj);
@@ -295,14 +311,14 @@ hqDefine("data_dictionary/js/data_dictionary", [
url: initialPageData.reverse('deprecate_or_restore_case_type', activeCaseType.name),
method: 'POST',
data: {
'is_deprecated': shouldDeprecate
'is_deprecated': shouldDeprecate,
},
success: function () {
window.location.reload(true);
},
error: function (error) {
error: function () {
$("#deprecate-case-type-error").show();
}
},
});
};

@@ -320,7 +336,7 @@ hqDefine("data_dictionary/js/data_dictionary", [
self.saveButton.setState('saved');
};

self.newPropertyNameUnique = ko.computed(function() {
self.newPropertyNameUnique = ko.computed(function () {
if (!self.newPropertyName()) {
return true;
}
@@ -335,7 +351,7 @@ hqDefine("data_dictionary/js/data_dictionary", [
return true;
});

self.newGroupNameUnique = ko.computed(function() {
self.newGroupNameUnique = ko.computed(function () {
if (!self.newGroupName()) {
return true;
}
@@ -406,7 +422,7 @@ hqDefine("data_dictionary/js/data_dictionary", [

// CREATE workflow
self.name = ko.observable("").extend({
rateLimit: { method: "notifyWhenChangesStop", timeout: 400, }
rateLimit: { method: "notifyWhenChangesStop", timeout: 400 },
});

self.nameValid = ko.observable(false);
48 changes: 48 additions & 0 deletions corehq/apps/data_dictionary/templates/data_dictionary/base.html
Original file line number Diff line number Diff line change
@@ -116,6 +116,7 @@ <h4 class="modal-title">{% trans "Create a new Case Type" %}</h4>
{% initial_page_data 'typeChoices' question_types %}
{% initial_page_data 'fhirResourceTypes' fhir_resource_types %}
{% initial_page_data 'read_only_mode' request.is_view_only %}
{% url 'geospatial_settings' domain as geospatial_settings_url %}
<div id="deprecate-case-type-error" class="alert alert-danger hidden">
<p>
{% blocktrans %}
@@ -408,4 +409,51 @@ <h4 class="modal-title">
</div>
</div>
</script>

<div id="deprecate-geospatial-prop-modal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">
{% trans "Deprecate GPS property" %}
</h4>
</div>
<div class="modal-body">
<p>
{% blocktrans %}
This GPS case property is currently being used to store the geolocation for cases.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
Deprecating this case property may result in unintended behaviour, and so
it is advised to first change the selected custom case property in
<a href="{{ geospatial_settings_url }}">
geospatial settings
</a>
before deprecating this case property.
{% endblocktrans %}
</p>

<p>
{% blocktrans %}
Would you like to proceed with deprecating this case property?
{% endblocktrans %}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
{% trans 'Cancel' %}
</button>
<button id="deprecate-geospatial-prop-btn" type="button" class="btn btn-primary">
{% trans 'Confirm' %}
</button>
</div>
</div>
</div>

</div>
{% endblock %}
14 changes: 8 additions & 6 deletions corehq/apps/data_dictionary/tests/test_view.py
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@

from corehq.apps.data_dictionary.models import CaseProperty, CasePropertyGroup, CasePropertyAllowedValue, CaseType
from corehq.apps.domain.shortcuts import create_domain
from corehq.apps.geospatial.const import GEO_POINT_CASE_PROPERTY
from corehq.apps.users.models import WebUser, HqPermissions
from corehq.apps.users.models_role import UserRole

@@ -269,18 +270,18 @@ def setUpClass(cls):
CaseProperty(case_type=cls.case_type_obj, name='property').save()
CasePropertyGroup(case_type=cls.case_type_obj, name='group').save()

def setUp(self):
self.endpoint = reverse(self.urlname, args=(self.domain, self.case_type_obj.name))
self.client = Client()
self.client.login(username='test', password='foobar')

@classmethod
def tearDownClass(cls):
cls.case_type_obj.delete()
cls.admin_webuser.delete(cls.domain, None)
cls.domain_obj.delete()
return super().tearDownClass()

def setUp(self):
self.endpoint = reverse(self.urlname, args=(self.domain, self.case_type_obj.name))
self.client = Client()
self.client.login(username='test', password='foobar')

def _update_deprecate_state(self, is_deprecated):
case_type_obj = CaseType.objects.get(name=self.case_type_name)
case_type_obj.is_deprecated = is_deprecated
@@ -388,6 +389,7 @@ def test_get_json_success(self, *args):
"module_count": 0,
"properties": [],
}
]
],
"geo_case_property": GEO_POINT_CASE_PROPERTY,
}
self.assertEqual(response.json(), expected_response)
6 changes: 5 additions & 1 deletion corehq/apps/data_dictionary/views.py
Original file line number Diff line number Diff line change
@@ -43,6 +43,7 @@
from corehq.apps.accounting.decorators import requires_privilege_with_fallback, requires_privilege
from corehq import privileges
from corehq.apps.app_manager.dbaccessors import get_case_type_app_module_count
from corehq.apps.geospatial.utils import get_geo_case_property

from .bulk import (
process_bulk_upload,
@@ -123,7 +124,10 @@ def data_dictionary_json(request, domain, case_type_name=None):
"properties": grouped_properties.get(None, [])
})
props.append(p)
return JsonResponse({'case_types': props})
return JsonResponse({
'case_types': props,
'geo_case_property': get_geo_case_property(domain),
})


@login_and_domain_required
30 changes: 29 additions & 1 deletion corehq/apps/geospatial/forms.py
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ class Meta:
)
case_location_property_name = forms.CharField(
label=_("Fetch case location data from property"),
widget=forms.widgets.Select(choices=[]),
required=True,
help_text=_("The name of the case property storing the geo-location data of your cases."),
)
@@ -45,7 +46,34 @@ def __init__(self, *args, **kwargs):
'user_location_property_name',
data_bind="value: customUserFieldName"
),
crispy.Field('case_location_property_name', data_bind="value: geoCasePropertyName"),
crispy.Field(
'case_location_property_name',
data_bind="options: geoCasePropOptions, "
"value: geoCasePropertyName, "
"event: {change: onGeoCasePropChange}"
),
crispy.Div(
crispy.HTML('%s' % _(
'The currently used "{{ config.case_location_property_name }}" case property '
'has been deprecated in the Data Dictionary. Please consider switching this '
'to another property.')
),
css_class='alert alert-warning',
data_bind="visible: isCasePropDeprecated"
),
crispy.Div(
crispy.HTML('%s' % _(
'The currently used "{{ config.case_location_property_name }}" case '
'property may be associated with cases. Selecting a new case '
'property will have the following effects:'
'<ul><li>All cases using the old case property will no longer appear on maps.</li>'
'<li>If the old case property is being used in an application, a new version would '
'need to be released with the new case property for all users that capture '
'location data.</li></ul>')
),
css_class='alert alert-warning',
data_bind="visible: hasGeoCasePropChanged"
),
),
hqcrispy.FormActions(
StrictButton(
22 changes: 17 additions & 5 deletions corehq/apps/geospatial/static/geospatial/js/geo_config.js
Original file line number Diff line number Diff line change
@@ -5,20 +5,32 @@ hqDefine("geospatial/js/geo_config", [
], function (
$,
ko,
initialPageData,
initialPageData
) {
const CUSTOM_USER_PROP = "custom_user_property";

var geoConfigViewModel = function (configData) {
'use strict';
var self = {};

var data = configData.get('config')
const gpsCaseProps = configData.get('gps_case_props_deprecated_state');
self.geoCasePropOptions = ko.observableArray(Object.keys(gpsCaseProps));

var data = configData.get('config');
self.customUserFieldName = ko.observable(data.user_location_property_name);
self.geoCasePropertyName = ko.observable(data.case_location_property_name);
self.isCasePropDeprecated = ko.observable(gpsCaseProps[self.geoCasePropertyName()]);
self.savedGeoCasePropName = ko.observable(data.case_location_property_name);
self.hasGeoCasePropChanged = ko.observable(false);

self.onGeoCasePropChange = function () {
if (self.geoCasePropertyName() !== self.savedGeoCasePropName()) {
self.hasGeoCasePropChanged(true);
} else {
self.hasGeoCasePropChanged(false);
}
};

return self;
}
};

$(function () {
$('#geospatial-config-form').koApplyBindings(geoConfigViewModel(initialPageData));
1 change: 1 addition & 0 deletions corehq/apps/geospatial/templates/geospatial/settings.html
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@

{% block page_content %}
{% initial_page_data 'config' config %}
{% initial_page_data 'gps_case_props_deprecated_state' gps_case_props_deprecated_state %}

<form id="geospatial-config-form" class="form-horizontal disable-on-submit" method="post">
{% crispy form %}
18 changes: 14 additions & 4 deletions corehq/apps/geospatial/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.test import TestCase
from django.urls import reverse

from corehq.apps.data_dictionary.models import CaseType, CaseProperty
from corehq.apps.domain.shortcuts import create_domain
from corehq.apps.users.models import WebUser
from corehq.apps.geospatial.views import GeospatialConfigPage
@@ -28,11 +29,20 @@ def setUpClass(cls):
)
cls.webuser.save()

cls.case_type = CaseType(domain=cls.domain, name='case_type')
cls.case_type.save()
cls.gps_case_prop_name = 'gps_prop'
CaseProperty(
case_type=cls.case_type,
name=cls.gps_case_prop_name,
data_type=CaseProperty.DataType.GPS,
).save()

@classmethod
def tearDownClass(cls):
cls.case_type.delete()
cls.webuser.delete(None, None)
cls.domain_obj.delete()

super().tearDownClass()

def _make_post(self, data):
@@ -58,21 +68,21 @@ def test_new_config_create(self):
self._make_post(
self.construct_data(
user_property='some_user_field',
case_property='some_case_prop',
case_property=self.gps_case_prop_name,
)
)
config = GeoConfig.objects.get(domain=self.domain)

self.assertTrue(config.location_data_source == GeoConfig.CUSTOM_USER_PROPERTY)
self.assertEqual(config.user_location_property_name, 'some_user_field')
self.assertEqual(config.case_location_property_name, 'some_case_prop')
self.assertEqual(config.case_location_property_name, self.gps_case_prop_name)

@flag_enabled('GEOSPATIAL')
def test_config_update(self):
self._make_post(
self.construct_data(
user_property='some_user_field',
case_property='some_case_prop',
case_property=self.gps_case_prop_name,
)
)
config = GeoConfig.objects.get(domain=self.domain)
10 changes: 9 additions & 1 deletion corehq/apps/geospatial/views.py
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@
from dimagi.utils.web import json_response

from corehq import toggles
from corehq.apps.data_dictionary.models import CaseProperty
from corehq.apps.domain.views.base import BaseDomainView
from corehq.apps.geospatial.reports import CaseManagementMap
from corehq.apps.geospatial.forms import GeospatialConfigForm
@@ -129,12 +130,19 @@ def page_url(self):

@property
def page_context(self):
gps_case_props = CaseProperty.objects.filter(
case_type__domain=self.domain,
data_type=CaseProperty.DataType.GPS,
)
return {
'form': self.settings_form,
'config': model_to_dict(
self.config,
fields=GeospatialConfigForm.Meta.fields
)
),
'gps_case_props_deprecated_state': {
prop.name: prop.deprecated for prop in gps_case_props
}
}

@property

0 comments on commit 37502e1

Please sign in to comment.