Skip to content

Commit

Permalink
Implemented school district intent codeforboston#15
Browse files Browse the repository at this point in the history
  • Loading branch information
taratip committed Apr 22, 2020
1 parent bf7a5c0 commit 9fe23ff
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 0 deletions.
3 changes: 3 additions & 0 deletions brooklinevoiceapp/brookline_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
39 changes: 39 additions & 0 deletions brooklinevoiceapp/interaction_models/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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",
Expand Down
65 changes: 65 additions & 0 deletions brooklinevoiceapp/mycity/intents/school_district_intent.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
41 changes: 41 additions & 0 deletions brooklinevoiceapp/mycity/test/test_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
}
26 changes: 26 additions & 0 deletions brooklinevoiceapp/mycity/utils/brookline_arcgis_api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 9fe23ff

Please sign in to comment.