From 6184cb787680f8f1edc96ecb12c4a5fd85aeb39d Mon Sep 17 00:00:00 2001 From: Cesar Nunez Date: Fri, 13 Mar 2026 02:15:30 -0500 Subject: [PATCH 01/11] feat(make): Add new make commands for docker usage --- Makefile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Makefile b/Makefile index c3926e2..02f7691 100644 --- a/Makefile +++ b/Makefile @@ -18,3 +18,12 @@ data/raw/chicago-restaurants.csv: data/raw/community-areas.geojson: curl https://data.cityofchicago.org/resource/igwz-8jzy.geojson -o $@ + +build: + docker compose build + +up: build + docker compose up + +down: + docker compose down --rmi 'all' \ No newline at end of file From ce7de04cda4bbaac9cdfe9ddf178c9409fcfdc03 Mon Sep 17 00:00:00 2001 From: Cesar Nunez Date: Fri, 13 Mar 2026 02:27:25 -0500 Subject: [PATCH 02/11] fix(migration): ensure that the data is available when running the docker container --- docker-entrypoint.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index a88d164..fc7750f 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -19,4 +19,6 @@ if [ "$DJANGO_MANAGEPY_MIGRATE" = 'on' ]; then python manage.py migrate --noinput fi +make all + exec "$@" From 5671d5947bcbc25f8057aa192a8bf8c2d1d8b829 Mon Sep 17 00:00:00 2001 From: Cesar Nunez Date: Fri, 13 Mar 2026 02:32:50 -0500 Subject: [PATCH 03/11] feat(test): Implemented test for map_data_view --- map/serializers.py | 19 ++++++++++++++----- tests/docker-compose.yml | 1 + tests/test_views.py | 13 ++++++++++--- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/map/serializers.py b/map/serializers.py index 03dd912..d57814c 100644 --- a/map/serializers.py +++ b/map/serializers.py @@ -6,11 +6,9 @@ class CommunityAreaSerializer(serializers.ModelSerializer): class Meta: model = CommunityArea - fields = ["name", "num_permits"] + fields = [] - num_permits = serializers.SerializerMethodField() - - def get_num_permits(self, obj): + def to_representation(self, obj: CommunityArea): """ TODO: supplement each community area object with the number of permits issued in the given year. @@ -30,5 +28,16 @@ def get_num_permits(self, obj): } ] """ + year = self.context.get("year") + + qs = RestaurantPermit.objects.filter( + community_area_id=obj.area_id + ) - pass + if year: + qs = qs.filter(issue_date__year=year) + + return {obj.name : { + 'area_id' : obj.area_id, + 'num_permits' : qs.count() + }} \ No newline at end of file diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index ebb1387..1c2e5d4 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -7,4 +7,5 @@ services: # Disable manifest storage for testing DJANGO_STATICFILES_STORAGE: django.contrib.staticfiles.storage.StaticFilesStorage DJANGO_SETTINGS_MODULE: map.settings + entrypoint: [] command: pytest -sv diff --git a/tests/test_views.py b/tests/test_views.py index 24cc64e..7cb07c8 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -35,7 +35,14 @@ def test_map_data_view(): # Query the map data endpoint client = APIClient() response = client.get(reverse("map_data", query={"year": 2021})) + + resp1, resp2 = tuple(response.data) - # TODO: Complete the test by asserting that the /map-data/ endpoint - # returns the correct number of permits for Beverly and Lincoln - # Park in 2021 + assert area1.name in resp1.keys() + assert area2.name in resp2.keys() + + assert area1.area_id == str(resp1[area1.name]["area_id"]) + assert area2.area_id == str(resp2[area2.name]["area_id"]) + + assert resp1[area1.name]["num_permits"] == 2 + assert resp2[area2.name]["num_permits"] == 3 \ No newline at end of file From 440f2da683588b634cd17d25ccfd806a89955d4f Mon Sep 17 00:00:00 2001 From: Cesar Nunez Date: Fri, 13 Mar 2026 02:33:44 -0500 Subject: [PATCH 04/11] feat(map-data-view): Add views and serializers for map-data --- map/serializers.py | 2 +- map/static/js/RestaurantPermitMap.js | 145 +++++++++++++++++---------- 2 files changed, 91 insertions(+), 56 deletions(-) diff --git a/map/serializers.py b/map/serializers.py index d57814c..bf3b7bf 100644 --- a/map/serializers.py +++ b/map/serializers.py @@ -10,7 +10,7 @@ class Meta: def to_representation(self, obj: CommunityArea): """ - TODO: supplement each community area object with the number + Supplement each community area object with the number of permits issued in the given year. e.g. The endpoint /map-data/?year=2017 should return something like: diff --git a/map/static/js/RestaurantPermitMap.js b/map/static/js/RestaurantPermitMap.js index 57f8ea0..b5dc4f1 100644 --- a/map/static/js/RestaurantPermitMap.js +++ b/map/static/js/RestaurantPermitMap.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react" +import React, { useEffect, useState, useMemo } from "react" import { MapContainer, TileLayer, GeoJSON } from "react-leaflet" @@ -6,31 +6,26 @@ import "leaflet/dist/leaflet.css" import RAW_COMMUNITY_AREAS from "../../../data/raw/community-areas.geojson" -function YearSelect({ setFilterVal }) { - // Filter by the permit issue year for each restaurant +function YearSelect({ filterVal, setFilterVal }) { const startYear = 2026 - const years = [...Array(11).keys()].map((increment) => { - return startYear - increment - }) - const options = years.map((year) => { - return ( - - ) - }) + const years = [...Array(11).keys()].map((increment) => startYear - increment) return ( <> ) @@ -45,65 +40,105 @@ export default function RestaurantPermitMap() { const yearlyDataEndpoint = `/map-data/?year=${year}` useEffect(() => { - fetch() - .then((res) => res.json()) + fetch(yearlyDataEndpoint) + .then((res) => { + if (!res.ok) { + throw new Error(`Request failed: ${res.status}`) + } + return res.json() + }) .then((data) => { - /** - * TODO: Fetch the data needed to supply to map with data - */ + console.log("API data:", data) + setCurrentYearData(Array.isArray(data) ? data : []) + }) + .catch((err) => { + console.error("Error fetching map data:", err) + setCurrentYearData([]) }) }, [yearlyDataEndpoint]) + const areaPermitMap = useMemo(() => { + const mapped = {} + + if (!Array.isArray(currentYearData)) return mapped + + currentYearData.forEach((item) => { + if (!item || typeof item !== "object") return + + const keys = Object.keys(item) + if (keys.length === 0) return + + const areaName = keys[0] + mapped[areaName] = item[areaName] + }) + + console.log("areaPermitMap:", mapped) + return mapped + }, [currentYearData]) + + const totalNumPermits = useMemo(() => { + return Object.values(areaPermitMap).reduce( + (sum, area) => sum + (area?.num_permits ?? 0), + 0 + ) + }, [areaPermitMap]) + + const maxNumPermits = useMemo(() => { + const permitCounts = Object.values(areaPermitMap).map( + (area) => area?.num_permits ?? 0 + ) + return permitCounts.length > 0 ? Math.max(...permitCounts) : 0 + }, [areaPermitMap]) function getColor(percentageOfPermits) { - /** - * TODO: Use this function in setAreaInteraction to set a community - * area's color using the communityAreaColors constant above - */ + if (percentageOfPermits <= 0) return communityAreaColors[0] + if (percentageOfPermits <= 0.33) return communityAreaColors[1] + if (percentageOfPermits <= 0.66) return communityAreaColors[2] + return communityAreaColors[3] } function setAreaInteraction(feature, layer) { - /** - * TODO: Use the methods below to: - * 1) Shade each community area according to what percentage of - * permits were issued there in the selected year - * 2) On hover, display a popup with the community area's raw - * permit count for the year - */ - layer.setStyle() - layer.on("", () => { - layer.bindPopup("") - layer.openPopup() + console.log("GeoJSON feature:", feature) + + const areaName = feature?.properties?.community ?? "Unknown" + const areaData = areaPermitMap[areaName] + const numPermits = areaData?.num_permits ?? 0 + + const percentageOfPermits = + maxNumPermits > 0 ? numPermits / maxNumPermits : 0 + + layer.setStyle({ + fillColor: getColor(percentageOfPermits), + fillOpacity: 0.7, + color: "#333", + weight: 1, }) - } + layer.bindPopup(`${areaName}
Permits: ${numPermits}`) + + layer.on("mouseover", () => layer.openPopup()) + layer.on("mouseout", () => layer.closePopup()) + } + return ( <> +

Restaurant permits issued this year: {totalNumPermits}

- Restaurant permits issued this year: {/* TODO: display this value */} + Maximum number of restaurant permits in a single area: {maxNumPermits}

-

- Maximum number of restaurant permits in a single area: - {/* TODO: display this value */} -

- + + - {currentYearData.length > 0 ? ( - - ) : null} + ) -} +} \ No newline at end of file From cdc07d47be08939ccff2c2d1128ad0a055d95f42 Mon Sep 17 00:00:00 2001 From: Cesar Nunez Date: Fri, 13 Mar 2026 13:56:15 -0500 Subject: [PATCH 05/11] feat(map): editions to map hover. --- map/static/js/RestaurantPermitMap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/map/static/js/RestaurantPermitMap.js b/map/static/js/RestaurantPermitMap.js index b5dc4f1..ec47872 100644 --- a/map/static/js/RestaurantPermitMap.js +++ b/map/static/js/RestaurantPermitMap.js @@ -114,7 +114,7 @@ export default function RestaurantPermitMap() { weight: 1, }) - layer.bindPopup(`${areaName}
Permits: ${numPermits}`) + layer.bindPopup(`${areaName}
Year: ${year}
Restaurant permits: ${numPermits}`) layer.on("mouseover", () => layer.openPopup()) layer.on("mouseout", () => layer.closePopup()) From a92e897a8d38a8c21e81a42aac1e5b0f95c19a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20N=C3=BA=C3=B1ez?= <75745356+cesarnunezh@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:59:23 -0500 Subject: [PATCH 06/11] Add data loading command to 'up' target Add command to load data fixtures before starting the app. --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 02f7691..57e5499 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,9 @@ build: docker compose build up: build + docker compose run --rm app python manage.py loaddata map/fixtures/restaurant_permits.json map/fixtures/community_areas.json docker compose up down: - docker compose down --rmi 'all' \ No newline at end of file + docker compose down --rmi 'all' + From 8ddf9d5f8bdbb4038f32c0da6c84194e48c72dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20N=C3=BA=C3=B1ez?= <75745356+cesarnunezh@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:59:41 -0500 Subject: [PATCH 07/11] Remove 'make all' from docker-entrypoint.sh Removed 'make all' command from entrypoint script. --- docker-entrypoint.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index fc7750f..a88d164 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -19,6 +19,4 @@ if [ "$DJANGO_MANAGEPY_MIGRATE" = 'on' ]; then python manage.py migrate --noinput fi -make all - exec "$@" From 696e3a6e50e43cc82d306ca4977dec077ebc27e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20N=C3=BA=C3=B1ez?= <75745356+cesarnunezh@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:01:50 -0500 Subject: [PATCH 08/11] Update Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 57e5499..6a471cd 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ build: up: build docker compose run --rm app python manage.py loaddata map/fixtures/restaurant_permits.json map/fixtures/community_areas.json - docker compose up + docker compose up -d down: docker compose down --rmi 'all' From ef865da7e9ab0c614e77318ac46c8dc3209846d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20N=C3=BA=C3=B1ez?= <75745356+cesarnunezh@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:03:19 -0500 Subject: [PATCH 09/11] Revise Docker instructions in README Updated instructions to use 'make up' instead of 'docker compose'. Removed data loading instructions. --- README.md | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4f1a3b7..90fa1a9 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,7 @@ Development requires a local installation of [Docker](https://docs.docker.com/ge Once you have Docker and Docker Compose installed, build the application containers from the project's root directory: ```bash -docker compose build -``` - -Load in the data: - -```bash -docker compose run --rm app python manage.py loaddata map/fixtures/restaurant_permits.json map/fixtures/community_areas.json -``` - -And finally, run the app: - -```bash -docker compose up +make up ``` The app will log to the console, and you should be able to visit it at http://localhost:8000 @@ -89,4 +77,4 @@ _Note: If you would prefer to keep your code challenge private, please share acc | Xavier | https://github.com/xmedr | | Hayley | https://github.com/haowens | -Keep in mind that you cannot create a private fork of a public repository on GitHub, so you’ll need to [follow these instructions](https://gist.github.com/0xjac/85097472043b697ab57ba1b1c7530274) to create a private copy of the repo. \ No newline at end of file +Keep in mind that you cannot create a private fork of a public repository on GitHub, so you’ll need to [follow these instructions](https://gist.github.com/0xjac/85097472043b697ab57ba1b1c7530274) to create a private copy of the repo. From f2d59d65eaa4dfa4b7170872df74edaca571f96b Mon Sep 17 00:00:00 2001 From: Cesar Nunez Date: Fri, 13 Mar 2026 14:17:51 -0500 Subject: [PATCH 10/11] feat(map): Add exception for serializer --- map/serializers.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/map/serializers.py b/map/serializers.py index bf3b7bf..efd320c 100644 --- a/map/serializers.py +++ b/map/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers +from rest_framework.exceptions import ValidationError from map.models import CommunityArea, RestaurantPermit @@ -30,14 +31,14 @@ def to_representation(self, obj: CommunityArea): """ year = self.context.get("year") - qs = RestaurantPermit.objects.filter( + if year: + query = RestaurantPermit.objects.filter( community_area_id=obj.area_id ) - - if year: - qs = qs.filter(issue_date__year=year) - - return {obj.name : { - 'area_id' : obj.area_id, - 'num_permits' : qs.count() - }} \ No newline at end of file + + return {obj.name : { + 'area_id' : obj.area_id, + 'num_permits' : query.count() + }} + else: + raise(ValidationError('Year not specified.')) \ No newline at end of file From 8ea70e15433d161bb1cc7d585f5ea0cc11682f0e Mon Sep 17 00:00:00 2001 From: Cesar Nunez Date: Fri, 13 Mar 2026 14:31:13 -0500 Subject: [PATCH 11/11] remove test entrypoint --- tests/docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 1c2e5d4..ebb1387 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -7,5 +7,4 @@ services: # Disable manifest storage for testing DJANGO_STATICFILES_STORAGE: django.contrib.staticfiles.storage.StaticFilesStorage DJANGO_SETTINGS_MODULE: map.settings - entrypoint: [] command: pytest -sv