diff --git a/docker-compose.yml b/docker-compose.yml index a743cce..1c541f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,6 +41,7 @@ services: postgres: container_name: map-postgres image: postgis/postgis + platform: linux/amd64 healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s diff --git a/map/serializers.py b/map/serializers.py index 03dd912..db694a0 100644 --- a/map/serializers.py +++ b/map/serializers.py @@ -3,32 +3,32 @@ from map.models import CommunityArea, RestaurantPermit +class CommunityAreaListSerializer(serializers.ListSerializer): + def to_representation(self, data): + + result = {} + for item in data: + child_data = self.child.to_representation(item) + name = child_data["name"] + result[name] = {"area_id": child_data["area_id"], "num_permits": child_data["num_permits"]} + return [result] + + class CommunityAreaSerializer(serializers.ModelSerializer): class Meta: model = CommunityArea - fields = ["name", "num_permits"] + fields = ["name", "area_id", "num_permits"] + list_serializer_class = CommunityAreaListSerializer num_permits = serializers.SerializerMethodField() def get_num_permits(self, obj): - """ - TODO: 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: - [ - { - "ROGERS PARK": { - area_id: 17, - num_permits: 2 - }, - "BEVERLY": { - area_id: 72, - num_permits: 2 - }, - ... - } - ] - """ - - pass + area_id = obj.area_id + num_permits = RestaurantPermit.objects.filter( + community_area_id=str(area_id), + issue_date__year=self.context["year"], + ).count() + return num_permits + + + diff --git a/map/static/js/App.js b/map/static/js/App.js index 5a8ec40..22a3489 100644 --- a/map/static/js/App.js +++ b/map/static/js/App.js @@ -1,8 +1,13 @@ -import React from "react" -import { createRoot } from "react-dom/client" +import React from "react"; +import { createRoot } from "react-dom/client"; -import RestaurantPermitMap from "./RestaurantPermitMap" +import RestaurantPermitMap from "./RestaurantPermitMap"; -const container = document.getElementById("map") -const root = createRoot(container) -root.render() +const container = document.getElementById("map"); +const root = createRoot(container); +root.render( + // Would use ReactErrorBoundary library to catch errors anywhere in RestaurantPermitMap and display a fallback UI. + // + , + // +); diff --git a/map/static/js/RestaurantPermitMap.js b/map/static/js/RestaurantPermitMap.js index 57f8ea0..f7e5112 100644 --- a/map/static/js/RestaurantPermitMap.js +++ b/map/static/js/RestaurantPermitMap.js @@ -1,25 +1,19 @@ -import React, { useEffect, useState } from "react" +import React, { useEffect, useState } from "react"; -import { MapContainer, TileLayer, GeoJSON } from "react-leaflet" +import { MapContainer, TileLayer, GeoJSON } from "react-leaflet"; -import "leaflet/dist/leaflet.css" +import "leaflet/dist/leaflet.css"; -import RAW_COMMUNITY_AREAS from "../../../data/raw/community-areas.geojson" +import RAW_COMMUNITY_AREAS from "../../../data/raw/community-areas.geojson"; -function YearSelect({ setFilterVal }) { - // Filter by the permit issue year for each restaurant - const startYear = 2026 - const years = [...Array(11).keys()].map((increment) => { - return startYear - increment - }) - const options = years.map((year) => { - return ( - - {year} - - ) - }) +const START_YEAR = 2026; + +// Generates years from 2026 to 2016 +const YEAR_OPTIONS = [...Array(11).keys()].map((increment) => { + return START_YEAR - increment; +}); +function YearSelect({ setFilterVal }) { return ( <> @@ -30,80 +24,111 @@ function YearSelect({ setFilterVal }) { className="form-select form-select-lg mb-3" onChange={(e) => setFilterVal(e.target.value)} > - {options} + {YEAR_OPTIONS.map((year) => ( + + {year} + + ))} > - ) + ); } export default function RestaurantPermitMap() { - const communityAreaColors = ["#eff3ff", "#bdd7e7", "#6baed6", "#2171b5"] + const communityAreaColors = ["#eff3ff", "#bdd7e7", "#6baed6", "#2171b5"]; + const [error, setError] = useState(false); + const [currentYearData, setCurrentYearData] = useState({}); + const [year, setYear] = useState(2026); - const [currentYearData, setCurrentYearData] = useState([]) - const [year, setYear] = useState(2026) - - const yearlyDataEndpoint = `/map-data/?year=${year}` + const yearlyDataEndpoint = `/map-data/?year=${year}`; useEffect(() => { - fetch() - .then((res) => res.json()) + setError(false); + fetch(yearlyDataEndpoint) + .then((res) => { + if (!res.ok) { + throw new Error("Failed to fetch data for year: " + year); + } + // Right here,we should also check that the data structure is valid using something like Zod or do it on the backend. Otherwise, it will break the app. + return res.json(); + }) .then((data) => { - /** - * TODO: Fetch the data needed to supply to map with data - */ + setCurrentYearData(data[0]); }) - }, [yearlyDataEndpoint]) + .catch((err) => { + // report error to something like Sentry + setError(true); + }); + }, [yearlyDataEndpoint]); + const permitsIssuedThisYear = Object.values(currentYearData).reduce( + (acc, curr) => acc + curr.num_permits, + 0, + ); + + const maxNumPermitsInSingleArea = Object.values(currentYearData).reduce( + (acc, curr) => Math.max(acc, curr.num_permits), + 0, + ); function getColor(percentageOfPermits) { - /** - * TODO: Use this function in setAreaInteraction to set a community - * area's color using the communityAreaColors constant above - */ + if (percentageOfPermits < 25) { + return communityAreaColors[0]; + } else if (percentageOfPermits < 50) { + return communityAreaColors[1]; + } else if (percentageOfPermits < 75) { + return communityAreaColors[2]; + } else { + 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() - }) + const permitData = currentYearData[feature.properties.community]; + if (!permitData) return; + + const numPermits = permitData.num_permits; + const percentageOfPermits = (numPermits / permitsIssuedThisYear) * 100; + const color = getColor(percentageOfPermits); + + layer.setStyle({ fillColor: color }); + + layer.on("mouseover", () => { + layer.bindPopup( + `${numPermits} permits issued in ${feature.properties.community}${percentageOfPermits.toFixed(2)}% of total permits`, + ); + layer.openPopup(); + }); } return ( <> + {error ? ( + + There was an error loading the data: Try selecting a different year. + + ) : null} - Restaurant permits issued this year: {/* TODO: display this value */} + Restaurant permits issued this year: {permitsIssuedThisYear} Maximum number of restaurant permits in a single area: - {/* TODO: display this value */} + {maxNumPermitsInSingleArea} - + - {currentYearData.length > 0 ? ( + {Object.keys(currentYearData).length > 0 ? ( ) : null} > - ) + ); } diff --git a/tests/test_views.py b/tests/test_views.py index 24cc64e..819ea59 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -7,6 +7,12 @@ from map.models import CommunityArea, RestaurantPermit +EXPECTED_DATA = { + "Beverly": {"area_id": 1, "num_permits": 2}, + "Lincoln Park": {"area_id": 2, "num_permits": 3}, +} + + @pytest.mark.django_db def test_map_data_view(): # Create some test community areas @@ -36,6 +42,15 @@ def test_map_data_view(): client = APIClient() response = client.get(reverse("map_data", query={"year": 2021})) - # TODO: Complete the test by asserting that the /map-data/ endpoint - # returns the correct number of permits for Beverly and Lincoln - # Park in 2021 + data = response.data + + beverly_item = next((item for item in data if "Beverly" in item), None) + beverly_data = beverly_item["Beverly"] if beverly_item else None + + lincoln_park_item = next((item for item in data if "Lincoln Park" in item), None) + lincoln_park_data = lincoln_park_item["Lincoln Park"] if lincoln_park_item else None + + assert beverly_data is not None + assert lincoln_park_data is not None + assert beverly_data["num_permits"] == EXPECTED_DATA["Beverly"]["num_permits"] + assert lincoln_park_data["num_permits"] == EXPECTED_DATA["Lincoln Park"]["num_permits"]
+ There was an error loading the data: Try selecting a different year. +
- Restaurant permits issued this year: {/* TODO: display this value */} + Restaurant permits issued this year: {permitsIssuedThisYear}
Maximum number of restaurant permits in a single area: - {/* TODO: display this value */} + {maxNumPermitsInSingleArea}