Skip to content

Commit

Permalink
SIT-1185 Request a Lead Test Kit | Form 1.1 (#2852)
Browse files Browse the repository at this point in the history
* SIT-1185 work in progress: splitting out address into separate street, city, state, zip fields; not quite done

* SIT-1185 work in progress, updates to Address Verifier to split out city/state/zip fields and improve functionality and usability.

* SIT-1185 lots of refactoring work on widget, now calls intersects api to get unincorporated city name. Still need to update the full address and display address if unincorporated.

* SIT-1185 better city handling for address verifier, get city from zipcode if unincorporated location in location widget

* SIT-1185 hide address part fields

* SIT-1185 incremental commit to switch branches; most water form upates complete, location picker now splits out street address parts, Smartsheet handler supports sub-elements. Just need to figure out how to make Address Verifier fields not required if the element is hidden using conditional logic.

* SIT-1185 make city/state/zip fields not required, can be set in element config

* SIT-1185 remove computed elements used to get subelement values

* SIT-1185 also make location_address subfield not required in code, conditionally required in form

* SIT-1185 re-added computed state, since location widget doesn't return it because it's always OR

* SIT-1185 fixed issue with setting city when unincorporated

* SIT-1185 make city name uppercase

* SIT-1185 incremental update for backup purposes, trying new approach of adding a secondary query option instead of using location widget

* SIT-1185 initial development done: converted form to use modified address verifier widget instead of location picker

* SIT-1185 added find_unincorporated to address verifier config

* SIT-1185 updates to handler, add comma before unit, misc tweaks

* SIT-1185 fixes to verifier logic (resetverified wasn't working), added loading indicator.

* SIT-1185 added Portland Ajax Indicator widget, final tweaks to Lead Test Kit form

* SIT-1185 put custom Portland elements in their own category

* SIT-1185 update handler to put new Smartsheet entries at the top of the sheet

* SIT-1185 remove computed state (no longer needed), add change trigger to address field

* SIT-1185 renamed spinner image, deleted unneeded files

* SIT-1185 removed old spinner image from address verifier module

* SIT-1185 fixed filename for spinner

* SIT-1185 make spinner smaller and modal, add gray background

* SIT-1185 remove debugging comment

* SIT-1185 changed to use City instead of Jurisdiction, put Jurisdiction into separate sub-element, fixed issue with capturing values from custom query when multiple instances of widget, add new rows to top of smartsheet

* SIT-1185 trying to figure how out to allow unvalidated addresses

* SIT-1185 final form tweaks, use verified status to determine whether rest of form is displayed.

* SIT-1185 manually reverted changes to location picker since it's no longer used in this form

* SIT-1185 fixed conditional logic issues

* SIT-1185 made ajax spinner accessible

* SIT-1185 added newlines to end of files

* SIT-1185 simplify ajax indicator module by attaching library to the element, + more styling

* SIT-1185 update composite element subroutine in Smartsheet handler

This code is inspired by WebformExcludedElements.php in the webform module and should be a more reliable/standard way of getting the composite key.

Additionally, make all the select elements select2 so they are easily searchable when there are a bunch of subelements.

* SIT-1185 Fix error when feature array was empty on secondary query

e.g with a Canby address, an error would be thrown since no features were found in the water provider layer. Fix this by moving the obj undefined check after the array check.

* SIT-1185 fix verification resetting when using control keys in the address element

For example, if you did a Ctrl+C or used arrow keys to navigate in the text field, it would reset the verification even though the text hasn't changed. By using the "input" event, we can react only when the value of the field is changed..

* SIT-1185 compute water provider value on client-side

Instead of using a computed element to show the water provider value, add a small script to replace the value when the location_capture_field element has changed. This saves on AJAX requests back to the server and therefore better performance after the user enters an address.

* SIT-1185 fix composite separator usage that I missed

* SIT-1185 remove image [skip ci]

---------

Co-authored-by: Oden <[email protected]>
  • Loading branch information
jookyg and odensc authored Sep 30, 2024
1 parent 71cadf1 commit 4b0eee6
Show file tree
Hide file tree
Showing 20 changed files with 470 additions and 269 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,13 @@ Default value: 0
**find_unincorporated**
Some addresses are technically outside of incorporated areas but are related by zipcode to a nearby city. If this property is true, an additional call is made to the Intersects API to retrieve the zipcode city and use that instead of "UNINCORPORATED."
Allowed values: 1|0
Default value: 0
Default value: 0

**secondary_query_url**
When populated, a second API call is made to the specified API URL with the x/y coordinates passed in the geometry parameter. All 3 properties (secondary_query_url, secondary_query_capture_property, and secondary_query_capture_field) must be set for this to work.

**secondary_query_capture_property**
The path of the property to capture from the JSON returned by the secondary_query_url. All 3 properties (secondary_query_url, secondary_query_capture_property, and secondary_query_capture_field) must be set for this to work.

**secondary_query_capture_field**
The ID of the form field into which the captured value should be stored. All 3 properties (secondary_query_url, secondary_query_capture_property, and secondary_query_capture_field) must be set for this to work.
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ AddressVerifierModel.locationItem = function (data, $element = null, isSingleton
var arrAddress = data.address.split(', ');
data.address = arrAddress[0];
}
this.fullAddress = AddressVerifierModel.buildFullAddress(data.address, data.unit, data.attributes.jurisdiction, data.attributes.zip_code).toUpperCase();
this.fullAddress = AddressVerifierModel.buildFullAddress(data.address, data.attributes.city, data.attributes.state, data.attributes.zip_code, data.unit).toUpperCase();
this.displayAddress = data.address.toUpperCase();// + ', ' + data.attributes.jurisdiction.toUpperCase();
this.street = data.address.toUpperCase();
this.streetNumber = data.attributes.address_number;
this.streetQuadrant = data.attributes.street_direction;
this.streetDirectionSuffix = data.attributes.street_direction_suffix ? data.attributes.street_direction_suffix.trim() : "";
this.streetName = data.attributes.street_name + " " + data.attributes.street_type;
this.streetName = data.attributes.street_name;
this.streetType = data.attributes.street_type;
if (this.streetDirectionSuffix) {
this.streetName += " " + this.streetDirectionSuffix;
this.streetType += " " + this.streetDirectionSuffix;
}
this.city = data.attributes.jurisdiction.toUpperCase();
this.state = data.attributes.state.toUpperCase();
this.city = data.attributes.city;
this.jurisdiction = data.attributes.jurisdiction;
this.state = data.attributes.state ? data.attributes.state.toUpperCase() : "";
this.zipCode = data.attributes.zip_code;
this.lat = data.attributes.lat;
this.lon = data.attributes.lon;
Expand All @@ -43,20 +45,20 @@ AddressVerifierModel.prototype.fetchAutocompleteItems = function (addrSearch, $e
return this.$.ajax({
url: apiUrl,
method: 'GET'
}).then(function(response) {
}).then(function (response) {
if (response && response.candidates && Array.isArray(response.candidates)) {
// KLUDGE: There's an issue with the PortlandMaps suggests API where the data is
// formatted differently when there is only a single candidate returned, as opposed
// to multiple candidates. The locationItem object constructor avoids this issue
// by always assembling the address from its component parts if we send the isSingleton flag.

if (response.candidates.length > 1) {
return response.candidates.map(function(candidate) {
return response.candidates.map(function (candidate) {
var retItem = new AddressVerifierModel.locationItem(candidate, $element);
return retItem;
});
} else if (response.candidates.length == 1) {
return response.candidates.map(function(candidate) {
return response.candidates.map(function (candidate) {
var retItem = new AddressVerifierModel.locationItem(candidate, $element, true);
return retItem;
});
Expand All @@ -72,22 +74,22 @@ AddressVerifierModel.prototype.fetchAutocompleteItems = function (addrSearch, $e
};

AddressVerifierModel.prototype._getSphericalMercCoords = function (lat, lon) {
// Radius of the Earth in meters
const R = 6378137;
// Convert the longitude from degrees to radians
const x = lon * (Math.PI / 180) * R;
// Convert the latitude from degrees to radians
const latRad = lat * (Math.PI / 180);
// Calculate the y value using the Mercator projection formula
const y = R * Math.log(Math.tan((Math.PI / 4) + (latRad / 2)));
return { x, y };
// Radius of the Earth in meters
const R = 6378137;

// Convert the longitude from degrees to radians
const x = lon * (Math.PI / 180) * R;

// Convert the latitude from degrees to radians
const latRad = lat * (Math.PI / 180);

// Calculate the y value using the Mercator projection formula
const y = R * Math.log(Math.tan((Math.PI / 4) + (latRad / 2)));

return { x, y };
}

AddressVerifierModel.locationItem.prototype.parseStreetData = function(street) {
AddressVerifierModel.locationItem.prototype.parseStreetData = function (street) {
// Assuming street is in the format "1234 NW Main St"
const streetParts = street.split(' ');
this.streetNumber = streetParts.shift();
Expand All @@ -99,8 +101,8 @@ AddressVerifierModel.locationItem.prototype.parseStreetData = function(street) {

AddressVerifierModel.buildFullAddress = function (address, city, state, zip, unit = null) {
var fullAddress = address;
fullAddress += unit ? " " + unit : "";
fullAddress += ", " + city + ", " + state + " " + zip;
fullAddress += unit ? ", " + unit : "";
fullAddress += ", " + city + ", " + state + " " + zip;
return fullAddress;
}

Expand Down Expand Up @@ -131,7 +133,7 @@ AddressVerifierModel.buildMailingLabel = function (item, $element, useHtml = fal
return label;
}

AddressVerifierModel.prototype.updateLocationFromIntersects = function(lat, lon, item, callback, view) {
AddressVerifierModel.prototype.updateLocationFromIntersects = function (lat, lon, item, callback, view) {
var xy = this._getSphericalMercCoords(lat, lon);
url = REVERSE_GEOCODE_URL;
url = url.replace('${x}', xy.x).replace('${y}', xy.y).replace('${apiKey}', this.apiKey);
Expand All @@ -154,3 +156,41 @@ AddressVerifierModel.prototype.updateLocationFromIntersects = function(lat, lon,
});
}

AddressVerifierModel.prototype.callSecondaryQuery = function (url, x, y, callback, view, capturePath, captureField, $) {
url = url + "&geometry=" + x + "," + y;
this.$.ajax({
url: url, success: function (response) {
callback(response, view, capturePath, captureField, $);
},
error: function (e) {
// if the PortlandMaps API is down, this is where we'll get stuck.
// any way to fail the location lookup gracefull and still let folks submit?
// at least display an error message.
console.error(e);
}
});
}

AddressVerifierModel.getPropertyByPath = function (jsonObject, path) {
const keys = path.split('.');

return keys.reduce((obj, key) => {
// Automatically use the first element if the current object is an array
if (Array.isArray(obj)) {
obj = obj[0];
}

if (!obj) return undefined;
// Check if the key includes an array index, like 'features[0]'
const arrayIndexMatch = key.match(/(.+)\[(\d+)\]$/);

if (arrayIndexMatch) {
const arrayKey = arrayIndexMatch[1];
const index = parseInt(arrayIndexMatch[2], 10);
return obj[arrayKey] && obj[arrayKey][index] !== undefined ? obj[arrayKey][index] : undefined;
} else {
return obj[key] !== undefined ? obj[key] : undefined;
}
}, jsonObject);
};

Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ var $element;
var $input;
var $suggestModal;
var $statusModal;
const NOT_VERIFIED_MESSAGE = "We're unable to verify this address.";
const NOT_VERIFIED_REASONS = "This sometimes happens with new addresses, PO boxes, and multi-family buildings with unit numbers.";
const IF_CERTAIN_MESSAGE = "If you are certain this is the full, correct address, you may use it without verification.";
const MUST_PROVIDE_ADDRESS_MESSAGE = "You must enter an address or partial address to verify.";
const UNVERIFIED_WARNING_MESSAGE = "We're unable to verify this address. If you're certain this is the full, correct address, you may proceed without verification."
const VERIFIED_MESSAGE = "Address is verified!";
Expand Down Expand Up @@ -84,9 +81,9 @@ AddressVerifierView.prototype._setUpUnitNumberField = function () {
var item = JSON.parse(json);
item.unit = unit;
item.fullAddress = AddressVerifierModel.buildFullAddress(item.street, item.city, item.state, item.zipCode, unit);
item.displayAddress = item.street + (unit ? " " + unit : "") + ", " + item.city;
item.displayAddress = item.street + (unit ? ", " + unit : "") + ", " + item.city;
$locationData.val(JSON.stringify(item));
// self.$element.find('#location_address_label').val(AddressVerifierModel.buildMailingLabel(item, self.$element));
self.$element.find('#location_full_address').val(item.fullAddress);
// self.$element.find('#mailing_label').html(AddressVerifierModel.buildMailingLabel(item, self.$element, true));
}
});
Expand All @@ -104,8 +101,8 @@ AddressVerifierView.prototype._setUpInputFieldAndAutocomplete = function () {
}
});

this.$input.on('keyup', function () {
if (self.$(this).val().length > 3 && self.isVerified) {
this.$input.on('input', function () {
if (self.isVerified) {
self._resetVerified(self.$checkmark, self.$button);
}
});
Expand Down Expand Up @@ -163,6 +160,20 @@ AddressVerifierView.prototype._selectAddress = function (item) {
} else {
self._setVerified(item);
}

// if configured, run secondary query // url, x, y, callback, view
if (this.settings.secondary_query_url && this.settings.secondary_query_capture_property && this.settings.secondary_query_capture_field) {
this.model.callSecondaryQuery(this.settings.secondary_query_url, item.x, item.y, self._processSecondaryResults, self, this.settings.secondary_query_capture_property, this.settings.secondary_query_capture_field, this.$);
}

// blur address field after setting
self.$element.find('#location_address').blur();
}

AddressVerifierView.prototype._processSecondaryResults = function (results, view = this, capturePath, captureField, $) {
// get property value from results as indicated by path (can we access this.settings here?)
var propertyValue = AddressVerifierModel.getPropertyByPath(results, capturePath);
view.$element.find('#' + captureField).val(propertyValue).trigger('change');
}

// this is hte method that handles the location item once its data is complete.
Expand All @@ -186,14 +197,17 @@ AddressVerifierView.prototype._setVerified = function (item, view = this) {
// TODO: if configured, set the taxlot id

// populate hidden sub-elements for address data
view.$element.find('#location_address').val(item.street);
view.$element.find('#location_address').val(item.street).trigger('change');
//$element.find('#location_street').val(item.street);
view.$element.find('#location_full_address').val(item.fullAddress);
view.$element.find('#location_address_street_number').val(item.streetNumber);
view.$element.find('#location_address_street_quadrant').val(item.streetQuadrant);
view.$element.find('#location_address_street_name').val(item.streetName);
view.$element.find('#location_city').val(item.city);
view.$element.find('#location_address_street_type').val(item.streetType);
view.$element.find('#location_city').val(item.city).trigger('change');
view.$element.find('#location_jurisdiction').val(item.jurisdiction);
view._setStateByLabel(view, item.state);
view.$element.find('#location_zip').val(item.zipCode);
view.$element.find('#location_zip').val(item.zipCode).trigger('change');
view.$element.find('#location_lat').val(item.lat);
view.$element.find('#location_lon').val(item.lon);
view.$element.find('#location_x').val(item.x);
Expand All @@ -202,7 +216,7 @@ AddressVerifierView.prototype._setVerified = function (item, view = this) {
// view.$element.find('#location_address_label').val(AddressVerifierModel.buildMailingLabel(item, view.$element));
// view.$element.find('#mailing_label').html(AddressVerifierModel.buildMailingLabel(item, view.$element, true));
// view.$element.find('#location_address_label_markup').removeClass('d-none');
view.$element.find('#location_verification_status').val("Verified");
view.$element.find('#location_verification_status').val("Verified").trigger('change');
view.$element.find('#location_data').val(JSON.stringify(item));
view.isVerified = true;
console.log(JSON.stringify(item));
Expand Down Expand Up @@ -291,7 +305,7 @@ AddressVerifierView.prototype._resetSuggestModal = function () {
AddressVerifierView.prototype._showNotFoundModal = function () {
var self = this;
var inputVal = self.$input.val().trim();
this.$notFoundModal.html(`<p><strong>${NOT_VERIFIED_MESSAGE}</strong> ${NOT_VERIFIED_REASONS}</p><p>${IF_CERTAIN_MESSAGE}</p></p>`);
this.$notFoundModal.html(`<p><strong>${this.settings.not_verified_heading}</strong> ${this.settings.not_verified_reasons}</p><p>${this.settings.not_verified_remedy}</p></p>`);
Drupal.dialog(this.$notFoundModal, {
width: '600px',
buttons: [{
Expand Down Expand Up @@ -334,23 +348,26 @@ AddressVerifierView.prototype._resetVerified = function ($checkmark, $button) {

// clear hidden sub-elements for address data
var $element = this.$(this.element);
$element.find('#location_address').val("");
$element.find('#location_address_street_number').val("");
$element.find('#location_address_street_quadrant').val("");
$element.find('#location_address_street_name').val("");
$element.find('#location_city').val("");
$element.find('#location_state').val("");
$element.find('#location_zip').val("");
$element.find('#location_lat').val("");
$element.find('#location_lon').val("");
$element.find('#location_x').val("");
$element.find('#location_y').val("");
$element.find('#location_taxlot_id').val("");
$element.find('#location_is_unincorporated').val("");
//this.$element.find('#location_address').val("");
this.$element.find('#location_full_address').val("");
this.$element.find('#location_address_street_number').val("");
this.$element.find('#location_address_street_quadrant').val("");
this.$element.find('#location_address_street_name').val("");
this.$element.find('#location_address_street_type').val("");
this.$element.find('#location_city').val("").trigger('change');
this.$element.find('#location_jurisdiction').val("");
this.$element.find('#location_state').val("").trigger('change');
this.$element.find('#location_zip').val("").trigger('change');
this.$element.find('#location_lat').val("");
this.$element.find('#location_lon').val("");
this.$element.find('#location_x').val("");
this.$element.find('#location_y').val("");
this.$element.find('#location_taxlot_id').val("");
this.$element.find('#location_is_unincorporated').val("");
// $element.find('#address_label').val("");
// $element.find('#location_address_label_markup').addClass('d-none');
$element.find('#location_verification_status').val("");
$element.find('#location_data').val("");
this.$element.find('#location_verification_status').val("").trigger('change');
this.$element.find('#location_data').val("");
// this.$element.find('#container_unit').removeClass('d-none');
// this.$element.find('#location_address_label_markup').removeClass('d-none');
this.isVerified = false;
Expand Down Expand Up @@ -387,4 +404,4 @@ AddressVerifierView.prototype._useUnverified = function () {
// AddressVerifierView.prototype.handleTaxlotId = function (taxlotId, $element) {
// $element.find('#location_taxlot_id').val(taxlotId);
// // alert($element.find('#location_taxlot_id').val());
// }
// }
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public static function getCompositeElements(array $element) {
'#description_display' => 'before',
'#required_error' => 'Please enter an address and verify it.',
];
$element['location_full_address'] = [
'#type' => 'hidden',
'#title' => t('Full Address'),
'#attributes' => ['id' => 'location_full_address']
];
$element['location_address_street_number'] = [
'#type' => 'hidden',
'#title' => t('Street Number'),
Expand All @@ -74,6 +79,11 @@ public static function getCompositeElements(array $element) {
'#title' => t('Street Name'),
'#attributes' => ['id' => 'location_address_street_name']
];
$element['location_address_street_type'] = [
'#type' => 'hidden',
'#title' => t('Street Type'),
'#attributes' => ['id' => 'location_address_street_type']
];
$element['unit_number'] = [
'#type' => 'textfield',
'#title' => t('Unit Number'),
Expand Down Expand Up @@ -104,6 +114,11 @@ public static function getCompositeElements(array $element) {
'#attributes' => ['class' => ['webform-zip']],
'#wrapper_attributes' => ['class' => ['webform-zip']],
];
$element['location_jurisdiction'] = [
'#type' => 'hidden',
'#title' => t('Jurisdiction'),
'#id' => 'location_jurisdiction',
];
$element['suggestions_modal'] = [
'#type' => 'markup',
'#title' => 'Suggestions',
Expand Down Expand Up @@ -163,6 +178,11 @@ public static function getCompositeElements(array $element) {
'#attributes' => [ 'id' => 'location_verification_status'],
'#required_error' => 'Please verify the address before continuing.'
];
$element['location_capture_field'] = [
'#type' => 'hidden',
'#title' => t('Capture Field'),
'#attributes' => [ 'id' => 'location_capture_field'],
];
$element['location_data'] = [
'#type' => 'hidden',
'#title' => t('Location Data'),
Expand Down
Loading

0 comments on commit 4b0eee6

Please sign in to comment.