Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add polling station intent #33

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions brooklinevoiceapp/brookline_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from mycity.intents.police_station_intent import find_closest_police_station
from mycity.intents.trash_day_intent import get_trash_pickup_info
from mycity.intents.polling_stations_intent import get_polling_location_info
from mycity.mycity_response_data_model import MyCityResponseDataModel

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -107,5 +108,9 @@ def on_intent(mycity_request):
return find_closest_police_station(mycity_request)
elif mycity_request.intent_name == "TrashDayIntent":
return get_trash_pickup_info(mycity_request)
elif mycity_request.intent_name == "PollingStationIntent":
return get_polling_location_info(mycity_request)
else:
raise ValueError("Invalid Intent")


125 changes: 113 additions & 12 deletions brooklinevoiceapp/interaction_models/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,102 @@
"when is trash picked up at {Address}",
"When is trash day at {Address}"
]
},
{
"name": "PollingStationIntent",
"slots": [
{
"name": "Address",
"type": "AMAZON.StreetAddress",
"samples": [
"The address is {Address}",
"My address is {Address}",
"{Address}"
]
},
{
"name": "number_requests",
"type": "AMAZON.NUMBER"
}
],
"samples": [
"Nearest polling stations",
"Nearest polling stations to {Address}",
"Nearest {number_requests} polling stations",
"Nearest {number_requests} polling stations to {Address}",
"Ask for the {number_requests} closest polling stations",
"Ask for the closest polling stations",
"Ask for the {number_requests} nearest polling stations",
"Ask for the polling stations",
"Say the {number_requests} closest polling stations",
"Say the closest polling stations",
"Say the nearest {number_requests} polling stations",
"Say the nearest polling stations",
"Say the polling stations",
"Tell me the {number_requests} closest polling stations",
"Tell me the closest polling stations",
"Tell me the closest polling stations {Address}",
"Tell me the nearest polling stations",
"Tell me the nearest polling stations to {Address}",
"Tell me {number_requests} polling stations",
"Tell me the polling stations",
"Request {number_requests} closest polling stations",
"Request closest polling stations",
"Request {number_requests} nearest polling stations",
"Request nearest polling stations",
"Request the polling stations",
"report {number_requests} closest polling stations",
"report closest polling stations",
"report nearest {number_requests} polling stations",
"report nearest polling stations",
"report the polling stations",
"I need the {number_requests} closest polling stations",
"I need the closest polling stations",
"I need the {number_requests} nearest polling stations",
"I need the nearest polling stations",
"I need the polling stations",
"I want the closest polling stations",
"I want polling stations",
"I want nearest polling stations",
"I want the {number_requests} nearest polling stations",
"I want the nearest polling stations",
"I want the polling stations",
"Give the {number_requests} closest polling stations",
"Give the {number_requests} nearest polling stations",
"Give the closest polling stations",
"Give the polling stations",
"Give me the polling stations",
"Give me the polling stations closest to {Address}",
"Give me the {number_requests} closest polling stations",
"Give me the {number_requests} nearest polling stations",
"Give me the nearest {number_requests} polling stations",
"Give me the nearest polling stations",
"Get me the {number_requests} closest polling stations",
"Get me the closest polling stations",
"Get me the closest polling stations to {Address}",
"Get {number_requests} nearby polling stations",
"Get the nearest polling stations",
"Get me the nearest {number_requests} polling stations",
"Get me the closest {number_requests} polling stations",
"Get me the nearest polling stations",
"Get me the polling stations",
"What are polling stations",
"What's the {number_requests} nearby polling stations",
"What's the {number_requests} polling stations near me",
"What's the closest polling stations",
"What's the nearest {number_requests} polling stations",
"What's the nearest polling stations",
"What polling stations",
"What's the polling stations",
"Give me {number_requests} polling stations",
"Give me {number_requests} polling stations at {Address}",
"{number_requests} polling stations near me",
"{number_requests} polling stations",
"Read me {number_requests} polling stations requests",
"Polling stations request"
]
}
],
"types": []
]
},
"dialog": {
"intents": [
Expand Down Expand Up @@ -109,7 +202,24 @@
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.1200741587813.991075352244"
"elicitation": "Elicit.Slot.AskAddress"
}
}
]
},
{
"name": "PollingStationIntent",
"delegationStrategy": "SKILL_RESPONSE",
"confirmationRequired": false,
"prompts": {},
"slots": [
{
"name": "Address",
"type": "AMAZON.StreetAddress",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.AskAddress"
}
}
]
Expand All @@ -126,15 +236,6 @@
"value": "What is your address?"
}
]
},
{
"id": "Elicit.Slot.1200741587813.991075352244",
"variations": [
{
"type": "PlainText",
"value": "What is your address?"
}
]
}
]
}
Expand Down
1 change: 1 addition & 0 deletions brooklinevoiceapp/mycity/intents/police_station_intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def find_closest_police_station(mycity_request):
else:
response.output_speech = _get_output_speech_for_address(current_address)
response.card_title = CARD_TITLE_POLICE_STATION
response.should_end_session = True

return response

Expand Down
109 changes: 109 additions & 0 deletions brooklinevoiceapp/mycity/intents/polling_stations_intent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
""" Intent for responding to polling station requests """
import logging
from mycity.mycity_response_data_model import MyCityResponseDataModel
from mycity.intents import intent_constants
from mycity.utils.address_utils import set_address_in_session
from mycity.utils.brookline_arcgis_api_utils import get_polling_locations_json


LOGGER = logging.getLogger(__name__)

# User facing strings
CARD_TITLE_POLLING = "Polling Locations"
OUTPUT_SPEECH_TEMPLATE = "The {} polling station in {} is located at {}. "

# Strings used in parsing json data returned by server
FEATURES_PATH = "features"
ATTRIBUTES_PATH = "attributes"
NAME_PATH = "NAME"
PRECINCT_PATH = "PRECINCT"
ADDR_PATH = "FULLADD"

# Request data model strings
NUMBER_LOCATIONS_SLOT_NAME = "number_requests"
MAX_LOCATIONS = 10
DEFAULT_LOCATIONS = 3

def get_polling_location_info(mycity_request):
"""
Generates a response to a polling location request

:param mycity_request: MyCityRequestDataModel containing the user request
:return: MyCityResponseDataModel containing the speech to return to the user
"""
LOGGER.debug('Getting polling location information')

response = MyCityResponseDataModel()
set_address_in_session(mycity_request)
current_address = \
mycity_request.session_attributes.get(intent_constants.CURRENT_ADDRESS_KEY)
if current_address is None:
# Delegate to the Alexa interaction model for getting the user address
LOGGER.debug('Requesting user address.')
response.dialog_directive = "Delegate"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could alternatively use auto delegation

else:
response.output_speech = _get_output_speech_for_address(current_address, mycity_request)
response.card_title = CARD_TITLE_POLLING
response.should_end_session = True

return response


def _get_output_speech_for_address(address, mycity_request):
"""
Creates output speech for polling locations near the provided address

:param address: Current address
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing :param mycity_request

:return: Output speech string
"""
number_locations = _number_of_locations(mycity_request)
response = get_polling_locations_json(address)
try:
results = response[FEATURES_PATH]
output_speech = ""
for result in results[:number_locations]:
output_speech += _build_speech_from_result(result)
except (IndexError, KeyError):
LOGGER.error("Error extracting polling station response.")
return intent_constants.NO_RESULTS_RESPONSE

if not output_speech:
return intent_constants.NO_RESULTS_RESPONSE

return output_speech


def _number_of_locations(mycity_request):
"""
Returns number of locations from the request if available or a default value
:param mycity_request: MyCityRequestDataModel object
:return: Number of polling station requests to return from this intent
"""
if NUMBER_LOCATIONS_SLOT_NAME in \
mycity_request.intent_variables and \
"value" in mycity_request.intent_variables[
NUMBER_LOCATIONS_SLOT_NAME]:
return min(
int(mycity_request.intent_variables[NUMBER_LOCATIONS_SLOT_NAME]["value"]),
MAX_LOCATIONS)

return DEFAULT_LOCATIONS


def _build_speech_from_result(result):
"""
Builds a speech string from a given polling location result
:param result: JSON object of a single polling location result
:return: Speech string representing this result
"""

try:
attributes = result[ATTRIBUTES_PATH]
name = attributes[NAME_PATH]
precinct = attributes[PRECINCT_PATH]
address = attributes[ADDR_PATH]
except KeyError:
LOGGER.error("Polling station response json did not contain the expected attributes.")
raise KeyError

return OUTPUT_SPEECH_TEMPLATE.format(name, precinct, address)
1 change: 1 addition & 0 deletions brooklinevoiceapp/mycity/intents/trash_day_intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def get_trash_pickup_info(mycity_request):
else:
response.output_speech = _get_output_speech_for_address(current_address)
response.card_title = CARD_TITLE_TRASH_DAY
response.should_end_session = True

return response

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
""" Integration tests for PollingStationIntent """
import mycity.test.test_constants as test_constants
import mycity.test.integration_tests.intent_base_case as base_case
import mycity.test.integration_tests.intent_test_mixins as mix_ins
import mycity.intents.polling_stations_intent as polling_intent
import mycity.intents.intent_constants as intent_constants
import copy

FEATURES=polling_intent.FEATURES_PATH
ATTRIBUTES=polling_intent.ATTRIBUTES_PATH
NAME=polling_intent.NAME_PATH

class PollingStationsTestCase(mix_ins.RepromptTextTestMixIn,
mix_ins.CardTitleTestMixIn,
base_case.IntentBaseCase):

intent_to_test = "PollingStationIntent"
expected_title = polling_intent.CARD_TITLE_POLLING
returns_reprompt_text = False

def setUp(self):
super().setUp()

# Patch requests.get in PollingStationIntent
self.mock_requests(get_data=copy.deepcopy(test_constants.GET_ADDRESS_CANDIDATES_API_MOCK),
post_data=copy.deepcopy(test_constants.GET_POLLING_LOCATIONS_API_MOCK))

def test_response_contains_polling_first_station_name(self):
first_station_name = test_constants.GET_POLLING_LOCATIONS_API_MOCK[FEATURES][0][ATTRIBUTES][NAME]
response = self.controller.on_intent(self.request)
self.assertTrue(first_station_name in response.output_speech)

def test_no_feature_results(self):
self.mock_requests(get_data=copy.deepcopy(test_constants.GET_ADDRESS_CANDIDATES_API_MOCK),
post_data=copy.deepcopy(test_constants.NO_RESPONSE_POLLING_LOCATIONS_API_MOCK))
response = self.controller.on_intent(self.request)
self.assertEqual(response.output_speech, intent_constants.NO_RESULTS_RESPONSE)
20 changes: 20 additions & 0 deletions brooklinevoiceapp/mycity/test/test_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,25 @@
}

GET_POLLING_LOCATIONS_API_MOCK = {
"displayFieldName": "NAME",
"fieldAliases": {
"OBJECTID": "OBJECTID",
"NAME": "Polling Location",
"POLLINGID": "Precinct",
"FULLADD": "Address",
"CITY": "City",
"STATE": "State",
"OPERHOURS": "Polling Hours",
"HANDICAP": "Handicap Accessible",
"NEXTELECT": "Next Election Date",
"REGDATE": "Voter Registration Deadline",
"CONTACT": "Contact Name",
"PHONE": "Phone",
"EMAIL": "Email",
"LASTUPDATE": "Last Update Date",
"LASTEDITOR": "Last Editor",
"PRECINCT": "PRECINCT"
},
"features": [
{
"attributes": {
Expand Down Expand Up @@ -263,3 +282,4 @@
}
]
}

6 changes: 3 additions & 3 deletions brooklinevoiceapp/mycity/test/unit_tests/test_gis_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def test_get_trash_day_json(self):
def test_get_polling_locations_json(self):
mock_nearest_feature_call = mock.MagicMock(return_value=test_constants.GET_POLLING_LOCATIONS_API_MOCK)
mock_geocode_call = mock.MagicMock(return_value=test_constants.LOCATION_MOCK)
result = utils.get_nearest_police_station_json(self.test_address,
_get_nearest_feature_json=mock_nearest_feature_call,
_geocode_address=mock_geocode_call)
result = utils.get_polling_locations_json(self.test_address,
_get_nearest_feature_json=mock_nearest_feature_call,
_geocode_address=mock_geocode_call)
self.assertEqual(result, test_constants.GET_POLLING_LOCATIONS_API_MOCK)
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,4 @@ def get_trash_day_json(address: str,
"""
logger.debug('Finding trash day for address: ' + str(address))
return _get_nearest_feature_json(address, MapFeatureID.TRASH_DAY)