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

Parserator challenge #34

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
#
# Read more on Dockerfile best practices at the source:
# https://docs.docker.com/develop/develop-images/dockerfile_best-practices
RUN apt-get update && apt-get install -y --no-install-recommends postgresql-client nodejs
RUN apt-get update && apt-get install -y --no-install-recommends postgresql-client nodejs npm

# Inside the container, create an app directory and switch into it
RUN mkdir /app
Expand Down
99 changes: 97 additions & 2 deletions parserator_web/static/js/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,97 @@
/* TODO: Flesh this out to connect the form to the API and render results
in the #address-results div. */
const $form = $(".form");
const $addressInput = $("#address");
const $addressResults = $("#address-results");
const $parseType = $("#parse-type");
const $resultsTableBody = $("tbody");
const $errorMessage = $("<p>")
.attr("id", "error-message")
.addClass("text-danger d-inline-block mx-4");
const DEFAULT_ERROR_MESSAGE = "Could not parse address";
const API_URL = "/api/parse/"

$form.on("submit", (evt) => {
evt.preventDefault();
handleSubmit();
});

/** handleSubmit:
* Handles user form input;
* calls functions to fetch parsed data and display results/errors
*/
async function handleSubmit() {
const input = $addressInput.val();

try {
const parsedAddressData = await fetchParsedAddressData(input);
renderResults(
parsedAddressData.address_components,
parsedAddressData.address_type
);
} catch (e) {
renderErrorMessage(e.message);
}
}

/** fetchParsedAddressData:
* fetches parsed address data from API
* @param {String} address
* @returns {Object} parsed address data, from JSON response
* @throws Error on bad request
*/
async function fetchParsedAddressData(address) {
const params = new URLSearchParams({ address });
const response = await fetch(`${API_URL}?${params}`);
const parsedAddressData = await response.json();

if (response.status != 200) {
throw new Error(parsedAddressData.error || DEFAULT_ERROR_MESSAGE);
}

return parsedAddressData;
}

/** renderResults:
* Displays parsed address components and information in UI
* @param {Object} addressComponents tagged address components
* @param {String} addressType
*/
function renderResults(addressComponents, addressType) {
clearResultsDisplay();

$parseType.text(addressType);
fillResultsTableBody(addressComponents);
}

/** renderErrorMessage:
* Displays an error message in UI
* @param {String} message error message
*/
function renderErrorMessage(message) {
$errorMessage.text(message);
$form.append($errorMessage);
}

/** fillResultsTableBody:
* builds displayed table of address components in UI
* @param {Object} addressComponents like {component: tag, ...}
*/
function fillResultsTableBody(addressComponents) {
for (const addressPart in addressComponents) {
const $row = $("<tr>");
const tag = addressComponents[addressPart];
$row.append(
$("<td>").text(addressPart),
$("<td>").text(tag)
);
$resultsTableBody.append($row);
}
}

/** clearResultsDisplay:
* resets UI, in order to display new results
*/
function clearResultsDisplay() {
$errorMessage.remove();
$resultsTableBody.empty();
$addressResults.removeClass("d-none");
}
10 changes: 9 additions & 1 deletion parserator_web/templates/parserator_web/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,15 @@ <h3 class="mb-1">
/>
</a>
<br>
... and you!
... and
<a
href="https://github.com/noahappelbaum"
target="_blank"
rel="noopener noreferref"
class="link-info"
>
Noah Appelbaum!
</a>
</p>
</div>
</footer>
Expand Down
3 changes: 1 addition & 2 deletions parserator_web/templates/parserator_web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ <h3 id="usaddress-parser"><i class="fa fa-fw fa-map-marker-alt"></i> U.S. addres
<button id="submit" type="submit" class="btn btn-success mt-3">Parse!</button>
</form>
</div>
<!-- TODO: Display parsed address components here. -->
<div id="address-results" style="display:none">
<div id="address-results" class="d-none">
<h4>Parsing results</h4>
<p>Address type: <strong><span id="parse-type"></span></strong></p>
<table class="table table-bordered">
Expand Down
45 changes: 40 additions & 5 deletions parserator_web/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import usaddress
from usaddress import RepeatedLabelError
from django.views.generic import TemplateView
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer
from rest_framework.exceptions import ParseError
from rest_framework import status


class Home(TemplateView):
Expand All @@ -14,11 +16,44 @@ class AddressParse(APIView):
renderer_classes = [JSONRenderer]

def get(self, request):
# TODO: Flesh out this method to parse an address string using the
# parse() method and return the parsed components to the frontend.
return Response({})
"""
API endpoint to parse a U.S. address
expects a string "address" query
responds with JSON, like:
{
"input_string": "123 main street"
"address_components": {"AddressNumber": "123", ...},
"addres_type": "Street Address"
}
"""
form_data = request.query_params
if ("address" not in form_data):
raise ParseError

input_string = form_data["address"]

try:
(address_components, address_type) = self.parse(input_string)
response_object = {}
response_object["input_string"] = input_string
response_object["address_components"] = address_components
response_object["address_type"] = address_type

return Response(response_object)

except RepeatedLabelError:
error_message = "Repeated Label Error; check that address is correct"
return Response(
{"error": error_message}, status=status.HTTP_400_BAD_REQUEST
)
# TODO: Add except clauses for any other errors raised by usaddress

def parse(self, address):
# TODO: Implement this method to return the parsed components of a
# given address using usaddress: https://github.com/datamade/usaddress
"""
Parses a United States address string into its component parts,
using usaddress: https://github.com/datamade/usaddress.

-> address_components: OrderedDict, address_type: str
"""
(address_components, address_type) = usaddress.tag(address)
return address_components, address_type
42 changes: 35 additions & 7 deletions tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
import pytest
import json

parse_url = "/api/parse/"


def test_api_parse_succeeds(client):
# TODO: Finish this test. Send a request to the API and confirm that the
# data comes back in the appropriate format.
address_string = '123 main st chicago il'
pytest.fail()
response = client.get(parse_url, {"address": address_string})

assert response.status_code == 200

parsed_data = json.loads(response.content)
assert parsed_data == {
"input_string": "123 main st chicago il",
"address_components": {
"AddressNumber": "123",
"StreetName": "main",
"StreetNamePostType": "st",
"PlaceName": "chicago",
"StateName": "il"
},
"address_type": "Street Address"
}


def test_api_parse_raises_error(client):
# TODO: Finish this test. The address_string below will raise a
# RepeatedLabelError, so ParseAddress.parse() will not be able to parse it.
address_string = '123 main st chicago il 123 main st'
pytest.fail()
response = client.get(parse_url, {"address": address_string})

assert response.status_code == 400


def test_api_parse_raises_error_on_missing_address_string(client):
response = client.get(parse_url, {})

assert response.status_code == 400


def test_api_parse_succeeds_on_empty_string(client):
address_string = ""
response = client.get(parse_url, {"address": address_string})

assert response.status_code == 200