Skip to content
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,6 @@ venv.bak/

# mypy
.mypy_cache/

# node_modules
node_modules
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Install npm
RUN apt-get update && apt-get install -y npm

# Install Node requirements
COPY ./package.json /app/package.json
RUN npm install
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ this assessment.

To get started, fork this repo and follow the instructions below.

## Completed By

Hi, my name is [Dan Grossberg](https://github.com/dwgrossberg), and I'm grateful that you're taking the time to review
my application code challenge. Changes have been committed throughout the dev process
and there are numerous comments explaining my thought process where applicable.
Please feel free to reach out with any questions or comments and I will be happy to
answer!

## Installation

Development requires a local installation of [Docker](https://docs.docker.com/install/)
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
"description": "JavaScript development setup for the DataMade Code Challenge",
"devDependencies": {
"eslint": "^7.23.0"
},
"dependencies": {
"docker": "^1.0.0",
"docker-compose": "^0.24.8"
}
}
20 changes: 20 additions & 0 deletions parserator_web/static/css/custom.css
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
/* Add custom style overrides here. */
#error-container {
background-color: #e34d3f;
width: 100%;
padding: 7px 13px;
margin-top: 10px;
border-radius: 5px;
gap: 5px;
}

#close-button, #error-message {
font-family: inherit;
font-size: inherit;
color: white;
background-color: #e34d3f;
border: none;
}

#error-message {
margin-top: 13px;
}
113 changes: 111 additions & 2 deletions parserator_web/static/js/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,111 @@
/* TODO: Flesh this out to connect the form to the API and render results
in the #address-results div. */
// This function triggers an eslint Parsing error: The keyword 'const' is reserved,
// which I was unable to resolve in a satisfactory manner
const parseAddress = async (address) => {
/**
* Sends a user-inputted address to the api/parse API endpoint and calls a function to display
* the results or the error, if applicable.
* @param {string} address The address to parse, input by the user
*/
const url = "api/parse/?address=" + address
try {
const response = await fetch(url, {
method: "GET"
});
// Check that the response is valid
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
} else {
const data = await response.json();
// Check for input string API errors
if (data.Error) {
displayError(data.Error);
} else if (data.ParseError) {
displayError(data.ParseError);
} else if (data.RepeatedLabelError) {
displayError(data.RepeatedLabelError);
// Otherwise display the address parse results
} else {
displayAddress(data);
}
displayAddress(data);
}
// Catch any error messages -- 500 statusq
} catch (error) {
displayError(error.message);
}
};

const displayAddress = (addressData) => {
/**
* Displays the address response to the DOM if the parse was successful.
* @param {Object} addressData A dictionary object containing the input_string,
* address_components, and address_type.
*/
// Clear input value
document.getElementById("address").value = "";
// Remove error if applicable
document.getElementById("error-container").style.display = "None";
const addressResults = document.getElementById("address-results");
// Show results table
addressResults.style.display = "block";
// Display address type
document.getElementById("parse-type").innerText = addressData.address_type;
// Loop through address_components and add new rows to results table
const tableBody = document.getElementsByTagName('tbody')[0];
// Clear previous search results
while (tableBody.firstChild) {
tableBody.removeChild(tableBody.lastChild);
}
for (let [tag, part] of Object.entries(addressData.address_components)) {
let resultsRow = document.createElement("tr");
let partData = document.createElement("td");
let tagData = document.createElement("td");
partData.innerText = part;
tagData.innerText = tag;
resultsRow.appendChild(partData);
resultsRow.appendChild(tagData);
tableBody.appendChild(resultsRow);
}
}

const displayError = (error) => {
/**
* Displays any errors encountered to the user via the DOM
* @param {Error} error The error data returned from the api/parse API endpoint
*/
// Clear address results if applicable
document.getElementById("address-results").style.display = "None";
const errorContainer = document.getElementById("error-container");
const errorText = document.getElementById("error-message");
errorContainer.style.display = "flex";
if (error === "Response status: 500") {
errorText.textContent = "Unable to parse this value due to repeated labels. Our team has been notified of the error.";
} else if (error === "no_value") {
errorText.textContent = "Please provide a value to parse."
} else {
errorText.textContent = error;
}
}

// Handle form submission
const addressForm = document.getElementsByTagName("form")[0];
console.log(addressForm);
addressForm.addEventListener("submit", (e) => {
e.preventDefault();
const addressData = new FormData(e.target).get("address");
// Check for blank string or string of whitespace
if (addressData) {
parseAddress(addressData);
} else {
// Otherwise display error message
displayError("no_value")
}
})

// Handle close error message event
const errorContainer = document.getElementById("error-container");
const errorCloseButton = document.getElementById("close-button");
errorCloseButton.addEventListener("mousedown", () => {
document.getElementById("address").value = "";
errorContainer.style.display = "None";
})
5 changes: 5 additions & 0 deletions parserator_web/templates/parserator_web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ <h3 id="usaddress-parser"><i class="fa fa-fw fa-map-marker-alt"></i> U.S. addres
<form class="form" role="form">
{% csrf_token %}
<input name="address" type="text" class="form-control" id="address" placeholder="123 Main St. Suite 100 Chicago, IL">
<!-- Added container for displaying error messages. -->
<div id="error-container" style="display: None;">
<input type="button" id="close-button" value="X"/>
<p id="error-message"></p>
</div>
<button id="submit" type="submit" class="btn btn-success mt-3">Parse!</button>
</form>
</div>
Expand Down
52 changes: 47 additions & 5 deletions parserator_web/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import usaddress
from usaddress import RepeatedLabelError
from django.views.generic import TemplateView
from rest_framework.views import APIView
from rest_framework.response import Response
Expand All @@ -11,14 +12,55 @@ class Home(TemplateView):


class AddressParse(APIView):
"""
A view class that represents a parsed US address.
"""
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({})
"""
Turns a request into an API response containing a parsed US address.

Args:
request (HTTP request): Input string containing an address to be parsed.

Raises:
ParseError: Checks that query_params are valid otherwise raises ParseError.

Returns:
HTTP response: A response object containing input_string, address_components,
and address_type.
"""
# Address string passed in via request params
address = request.query_params.get('address')
# Check that request params have valid structure
if 'address' not in request.query_params:
raise ParseError
# Create the response values
try:
address_components, address_type = self.parse(address)
return Response({
'input_string': address,
'address_components': address_components,
'address_type': address_type
})
# Handle exceptions and errors
except (RepeatedLabelError, TypeError):
return Response({'RepeatedLabelError': 'Unable to parse this value due to '
'repeated labels. Our team has been notified of the error.'},
status=400)
except Exception as e:
return Response({'Error': 'Error ' + e}, status=400)

def parse(self, address):
# TODO: Implement this method to return the parsed components of a
# given address using usaddress: https://github.com/datamade/usaddress
"""
Method for parsing US addresses via the usaddress module.

Args:
address (string): The address to be parsed.

Returns:
Object: parsed address_components and address_type.
"""
address_components, address_type = usaddress.tag(address)
return address_components, address_type
54 changes: 51 additions & 3 deletions tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,63 @@
import pytest
import json
from django.urls import resolve


def test_api_address_resolves_to_view_name():
# Validate that the named view matches url path
resolver = resolve('/api/parse/')
assert resolver.view_name == 'address-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()
# Test that data returns in the correct format
test_url = '/api/parse/?address=' + address_string
data_format = {
'input_string': '123 main st chicago il',
'address_components':
{
'AddressNumber': '123',
'StreetName': 'main',
'StreetNamePostType': 'st',
'PlaceName': 'chicago',
'StateName': 'il'
},
'address_type': 'Street Address'
}
response = client.get(test_url)
data = json.loads(response.content)
assert response.status_code == 200
assert data == data_format


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()
test_url = '/api/parse/?address=' + address_string
# Check that RepeatedLabelError is raised
repeated_label_error = {
'RepeatedLabelError': 'Unable to parse this value due to repeated labels. '
'Our team has been notified of the error.'
}
response = client.get(test_url)
response_data = json.loads(response.content)
assert response.status_code == 400
assert response_data == repeated_label_error


def test_missing_address_query_term_raises_parse_error(client):
address_string = '123 main st chicago il'
test_url = '/api/parse/?=' + address_string
response = client.get(test_url)
assert response.status_code == 400


def test_api_parse_empty_string_succeeds(client):
# Test that an empty string will generate a response status_code of 200
address_string = ''
test_url = '/api/parse/?address=' + address_string
response = client.get(test_url)
assert response.status_code == 200