From 9fe23ffb413c66c50ac794444a8eb451bb4aaceb Mon Sep 17 00:00:00 2001 From: Taratip Jaikaeo Date: Tue, 21 Apr 2020 20:41:07 -0400 Subject: [PATCH] Implemented school district intent #15 --- brooklinevoiceapp/brookline_controller.py | 3 + .../interaction_models/en_US.json | 39 +++++++++++ .../mycity/intents/school_district_intent.py | 65 +++++++++++++++++++ .../test_school_district_intent.py | 50 ++++++++++++++ .../mycity/test/test_constants.py | 41 ++++++++++++ .../utils/brookline_arcgis_api_utils.py | 26 ++++++++ 6 files changed, 224 insertions(+) create mode 100644 brooklinevoiceapp/mycity/intents/school_district_intent.py create mode 100644 brooklinevoiceapp/mycity/test/integration_tests/test_school_district_intent.py diff --git a/brooklinevoiceapp/brookline_controller.py b/brooklinevoiceapp/brookline_controller.py index 8828138..dcf69b4 100644 --- a/brooklinevoiceapp/brookline_controller.py +++ b/brooklinevoiceapp/brookline_controller.py @@ -7,6 +7,7 @@ from mycity.intents.police_station_intent import find_closest_police_station from mycity.intents.library_intent import find_closest_library from mycity.intents.trash_day_intent import get_trash_pickup_info +from mycity.intents.school_district_intent import find_closest_school_district from mycity.mycity_response_data_model import MyCityResponseDataModel from mycity.utils.exceptions import BaseOutputSpeechError @@ -112,6 +113,8 @@ def on_intent(mycity_request): return get_trash_pickup_info(mycity_request) elif mycity_request.intent_name == "LibraryIntent": return find_closest_library(mycity_request) + elif mycity_request.intent_name == "SchoolDistrictIntent": + return find_closest_school_district(mycity_request) else: raise ValueError("Invalid Intent") except BaseOutputSpeechError as e: diff --git a/brooklinevoiceapp/interaction_models/en_US.json b/brooklinevoiceapp/interaction_models/en_US.json index 448d179..0e92764 100644 --- a/brooklinevoiceapp/interaction_models/en_US.json +++ b/brooklinevoiceapp/interaction_models/en_US.json @@ -65,6 +65,29 @@ "Where is the library" ] }, + { + "name": "SchoolDistrictIntent", + "slots": [ + { + "name": "Address", + "type": "AMAZON.StreetAddress", + "samples": [ + "I live at {Address}", + "I am at {Address}", + "{Address}" + ] + } + ], + "samples": [ + "closest school district", + "school district near me", + "school district", + "where is the closest school district", + "closest school district to {Address}", + "school district near {Address}", + "where is the closest school district to {Address}" + ] + }, { "name": "TrashDayIntent", "slots": [ @@ -130,6 +153,22 @@ } ] }, + { + "name": "SchoolDistrictIntent", + "confirmationRequired": false, + "prompts": {}, + "slots": [ + { + "name": "Address", + "type": "AMAZON.StreetAddress", + "confirmationRequired": false, + "elicitationRequired": true, + "prompts": { + "elicitation": "Elicit.Slot.AskAddress" + } + } + ] + }, { "name": "TrashDayIntent", "delegationStrategy": "SKILL_RESPONSE", diff --git a/brooklinevoiceapp/mycity/intents/school_district_intent.py b/brooklinevoiceapp/mycity/intents/school_district_intent.py new file mode 100644 index 0000000..fd78abb --- /dev/null +++ b/brooklinevoiceapp/mycity/intents/school_district_intent.py @@ -0,0 +1,65 @@ +"""Alexa intent used to find school district associated with address""" + +import logging + +from mycity.intents import intent_constants +from mycity.mycity_response_data_model import MyCityResponseDataModel +from mycity.utils.address_utils import set_address_in_session +from mycity.utils.brookline_arcgis_api_utils import get_sorted_school_district_json + +logger = logging.getLogger(__name__) + +CARD_TITLE_SCHOOL_DISTRICT = "Nearest School District" + +OUTPUT_SPEECH_TEMPLATE = \ + "The nearest school district to you is {}." + +FEATURES_PATH = "features" +ATTRIBUTES_PATH = "attributes" +NAME_PATH = "NAME" + +def find_closest_school_district(mycity_request): + """ + Finds the closest school district in Brookline + to a given address + + :param mycity_request: MyCityRequestDataModel object + :return: MyCityResponseDataModel object + """ + logger.debug('Finding closest school district') + + response = MyCityResponseDataModel() + set_address_in_session(mycity_request) + current_address = mycity_request \ + .session_attributes \ + .get(intent_constants.CURRENT_ADDRESS_KEY) + logger.debug(current_address) + if current_address is None: + response.dialog_directive = "Delegate" + else: + response.output_speech = _get_output_speech_for_address(current_address) + response.card_title = CARD_TITLE_SCHOOL_DISTRICT + + return response + + +def _get_output_speech_for_address(address): + """ + Gets the API response and builds an output speech string + + :param address: Current address + :return: Output speech string + """ + + logger.debug("Getting response for address " + str(address)) + features = get_sorted_school_district_json(address) + logger.debug("school district response:", features) + + try: + first_feature = features[0] + logger.debug(first_feature) + facility_name = first_feature[ATTRIBUTES_PATH][NAME_PATH] + except IndexError: + return intent_constants.NO_RESULTS_RESPONSE + + return OUTPUT_SPEECH_TEMPLATE.format(facility_name) diff --git a/brooklinevoiceapp/mycity/test/integration_tests/test_school_district_intent.py b/brooklinevoiceapp/mycity/test/integration_tests/test_school_district_intent.py new file mode 100644 index 0000000..c1a8320 --- /dev/null +++ b/brooklinevoiceapp/mycity/test/integration_tests/test_school_district_intent.py @@ -0,0 +1,50 @@ +import copy +import logging + +from mycity.intents import ( + intent_constants, + school_district_intent as ps_intent, +) +from mycity.test import test_constants +from mycity.test.integration_tests import ( + intent_base_case as base_case, + intent_test_mixins as mix_ins, +) + +############################################ +# TestCase class for school_district_intent # +############################################ + +MOCK_RESPONSE = test_constants.GET_SCHOOL_DISTRICT_API_MOCK + +NO_RESULTS_RESPONSE = intent_constants.NO_RESULTS_RESPONSE + +FEATURES = ps_intent.FEATURES_PATH +ATTRIBUTES = ps_intent.ATTRIBUTES_PATH +NAME = ps_intent.NAME_PATH + +class SchoolDistrictTestCase(mix_ins.RepromptTextTestMixIn, + mix_ins.CardTitleTestMixIn, + base_case.IntentBaseCase): + intent_to_test = "SchoolDistrictIntent" + expected_title = ps_intent.CARD_TITLE_SCHOOL_DISTRICT + returns_reprompt_text = False + + def setUp(self): + """ + Patching out the functions in SchoolDistrictIntent that use requests.get + """ + super().setUp() + self.mock_requests(get_geocode_data=copy.deepcopy(test_constants.GET_ADDRESS_CANDIDATES_API_MOCK), + get_data=copy.deepcopy(test_constants.GET_SCHOOL_DISTRICT_API_MOCK)) + + def testResponseContainsName(self): + response = self.controller.on_intent(self.request) + for feature in MOCK_RESPONSE[FEATURES]: + self.assertIn(feature[ATTRIBUTES][NAME], response.output_speech) + + def testNoFeatureResults(self): + self.mock_requests(get_geocode_data=copy.deepcopy(test_constants.GET_ADDRESS_CANDIDATES_API_MOCK), + get_data=copy.deepcopy(test_constants.NO_RESULTS_GET_SCHOOL_DISTRICT_API_MOCK)) + response = self.controller.on_intent(self.request) + self.assertEqual(response.output_speech, NO_RESULTS_RESPONSE) diff --git a/brooklinevoiceapp/mycity/test/test_constants.py b/brooklinevoiceapp/mycity/test/test_constants.py index c7a285f..d04cbfe 100644 --- a/brooklinevoiceapp/mycity/test/test_constants.py +++ b/brooklinevoiceapp/mycity/test/test_constants.py @@ -338,3 +338,44 @@ } ] } + +GET_SCHOOL_DISTRICT_API_MOCK = { + "displayFieldName": "NAME", + "fieldAliases": { + "OBJECTID": "OBJECTID", + "NAME": "School Name", + "DISTRCTNAME": "School District Name", + "SCHOOLAREA": "Area in Square Miles", + "LASTUPDATE": "Last Update Date", + "LASTEDITOR": "Last Editor" + }, + "features": [ + { + "attributes": { + "OBJECTID": 1, + "NAME": "Brookline School", + "DISTRCTNAME": "Brookline", + "SCHOOLAREA": "1", + "LASTUPDATE": None, + "LASTEDITOR": None + }, + "geometry": { + "x": -7920615.96685251, + "y": 5205180.75934551 + } + } + ] +} + +NO_RESULTS_GET_SCHOOL_DISTRICT_API_MOCK = { + "displayFieldName": "NAME", + "fieldAliases": { + "OBJECTID": "OBJECTID", + "NAME": "School Name", + "DISTRCTNAME": "School District Name", + "SCHOOLAREA": "Area in Square Miles", + "LASTUPDATE": "Last Update Date", + "LASTEDITOR": "Last Editor" + }, + "features": [] +} diff --git a/brooklinevoiceapp/mycity/utils/brookline_arcgis_api_utils.py b/brooklinevoiceapp/mycity/utils/brookline_arcgis_api_utils.py index bc58bcc..e948be9 100644 --- a/brooklinevoiceapp/mycity/utils/brookline_arcgis_api_utils.py +++ b/brooklinevoiceapp/mycity/utils/brookline_arcgis_api_utils.py @@ -37,6 +37,7 @@ class MapFeatureID(Enum): TRASH_DAY = 12 LIBRARY = 9 POLLING_LOCATION = 21 + SCHOOL_DISTRICT = 16 class NonSortedFeatures(Enum): """Brookline GIS feature types that shouldn't be sorted""" @@ -183,6 +184,31 @@ def get_sorted_library_json(address: str, return _get_sorted_features_json(address, MapFeatureID.LIBRARY, geometry_params, home_address) + +def get_sorted_school_district_json(address: str, + _get_sorted_features_json: callable = get_sorted_features_json, + _geocode_address: callable = geocode_address) -> object: + """ + Queries the Brookline arcgis server for the nearest school district + + :param address: Address string to query + :return: Json data object response + """ + logger.debug('Finding closest school district for address: ' + str(address)) + home_address = _geocode_address(address) + coordinates = '[{},{}]'.format(home_address['x'], home_address['y']) + + if 'z' in home_address: + del home_address['z'] + + geometry_params = { + SPATIAL_REL_PARAM: "esriSpatialRelIntersects", + GEOMETRY_TYPE_PARAM: "esriGeometryPoint", + GEOMETRY_PARAM: coordinates, + } + + return _get_sorted_features_json(address, MapFeatureID.SCHOOL_DISTRICT, geometry_params, home_address) + def get_polling_locations(address: str, _get_sorted_features_json: callable = get_sorted_features_json, _geocode_address: callable = geocode_address) -> object: