diff --git a/frontend/src/Components/Charts/ChartSections/ShareList.js b/frontend/src/Components/Charts/ChartSections/ShareList.js index f6a88b5d..1bdec373 100644 --- a/frontend/src/Components/Charts/ChartSections/ShareList.js +++ b/frontend/src/Components/Charts/ChartSections/ShareList.js @@ -5,11 +5,15 @@ import * as S from './ShareList.styled'; import TwitterLogo from '../../../img/x-logo-black.png'; import FacebookLogo from '../../../img/meta_logo_primary.svg'; -function ShareList({ shareUrl, twitterTitle, onPressHandler }) { +function ShareList({ shareUrl, twitterTitle, onPressHandler, graphAnchor = null }) { + let shareURL = shareUrl; + if (graphAnchor) { + shareURL = `${shareURL}%23${graphAnchor}`; + } return ( { + if (window.location.hash) { + document.querySelector(`${window.location.hash}`).scrollIntoView(); + } + }, []); + const [year, setYear] = useState(YEARS_DEFAULT); const renderMetaTags = useMetaTags(); const [renderTableModal] = useTableModal(); - const [contrabandData, setContrabandData] = useState({ + + const initContrabandData = { labels: [], datasets: [], isModalOpen: false, tableData: [], csvData: [], loading: true, - }); - const [contrabandYear, setContrabandYear] = useState(YEARS_DEFAULT); - const [contrabandTypesData, setContrabandTypesData] = useState({ + }; + const [contrabandData, setContrabandData] = useState(initContrabandData); + + const initContrabandTypesData = { labels: [], datasets: [], isModalOpen: false, tableData: [], csvData: [], loading: true, - }); + }; + const [contrabandTypesData, setContrabandTypesData] = useState(initContrabandTypesData); - const [contrabandTypesYear, setContrabandTypesYear] = useState(YEARS_DEFAULT); - const [contrabandStopPurposeData, setContrabandStopPurposeData] = useState({ + const initContrabandStopPurposeData = { labels: [], datasets: [], loading: true, - }); + }; + const [contrabandStopPurposeData, setContrabandStopPurposeData] = useState( + initContrabandStopPurposeData + ); const [contrabandStopPurposeModalData, setContrabandStopPurposeModalData] = useState({ modalData: {}, isOpen: false, @@ -83,9 +94,7 @@ function Contraband(props) { loading: true, }); - const [contrabandStopPurposeYear, setContrabandStopPurposeYear] = useState(YEARS_DEFAULT); - - const initialContrabandGroupedData = [ + const initContrabandGroupedStopPurposeData = [ { labels: [], datasets: [], @@ -103,7 +112,7 @@ function Contraband(props) { }, ]; const [contrabandGroupedStopPurposeData, setContrabandGroupedStopPurposeData] = useState( - initialContrabandGroupedData + initContrabandGroupedStopPurposeData ); const [shouldRedrawContrabandGraphs, setShouldReDrawContrabandGraphs] = useState(true); const [contrabandTypes, setContrabandTypes] = useState(() => @@ -143,29 +152,18 @@ function Contraband(props) { const handleYearSelect = (y) => { if (y === year) return; setYear(y); - setContrabandGroupedStopPurposeData(initialContrabandGroupedData); + setContrabandData(initContrabandData); + setContrabandTypesData(initContrabandTypesData); + setContrabandStopPurposeData(initContrabandStopPurposeData); + setContrabandGroupedStopPurposeData(initContrabandGroupedStopPurposeData); fetchHitRateByStopPurpose(y); }; - const handleContrabandYearSelect = (y) => { - if (y === contrabandYear) return; - setContrabandYear(y); - }; - const handleContrabandTypesYearSelect = (y) => { - if (y === contrabandTypesYear) return; - setContrabandTypesYear(y); - }; - - const handleGroupedContrabandYearSelect = (y) => { - if (y === contrabandStopPurposeYear) return; - setContrabandStopPurposeYear(y); - }; - // Build New Contraband Data useEffect(() => { const params = []; - if (contrabandYear && contrabandYear !== 'All') { - params.push({ param: 'year', val: contrabandYear }); + if (year && year !== 'All') { + params.push({ param: 'year', val: year }); } if (officerId) { params.push({ param: 'officer', val: officerId }); @@ -217,12 +215,12 @@ function Contraband(props) { setContrabandData(data); }) .catch((err) => console.log(err)); - }, [contrabandYear]); + }, [year]); useEffect(() => { const params = []; - if (contrabandTypesYear && contrabandTypesYear !== 'All') { - params.push({ param: 'year', val: contrabandTypesYear }); + if (year && year !== 'All') { + params.push({ param: 'year', val: year }); } if (officerId) { params.push({ param: 'officer', val: officerId }); @@ -273,12 +271,12 @@ function Contraband(props) { setContrabandTypesData(data); }) .catch((err) => console.log(err)); - }, [contrabandTypesYear]); + }, [year]); useEffect(() => { const params = []; - if (contrabandStopPurposeYear && contrabandStopPurposeYear !== 'All') { - params.push({ param: 'year', val: contrabandStopPurposeYear }); + if (year && year !== 'All') { + params.push({ param: 'year', val: year }); } if (officerId) { params.push({ param: 'officer', val: officerId }); @@ -314,7 +312,7 @@ function Contraband(props) { }); }) .catch((err) => console.log(err)); - }, [contrabandStopPurposeYear]); + }, [year]); useEffect(() => { fetchHitRateByStopPurpose('All'); @@ -511,14 +509,14 @@ function Contraband(props) { const getBarChartModalSubHeading = (title) => `${title} ${subjectObserving()}.`; - const getBarChartModalHeading = (title, yearSelected) => { + const getBarChartModalHeading = (title) => { let subject = chartState.data[AGENCY_DETAILS].name; if (officerId) { subject = `Officer ${officerId}`; } let fromYear = ` since ${chartState.yearRange[chartState.yearRange.length - 1]}`; - if (yearSelected && yearSelected !== 'All') { - fromYear = ` in ${yearSelected}`; + if (year && year !== 'All') { + fromYear = ` in ${year}`; } return `${title} by ${subject}${fromYear}`; }; @@ -557,6 +555,15 @@ function Contraband(props) { a tiny fraction of the illegal substance +
+ +
- - - - +

@@ -653,27 +654,20 @@ function Contraband(props) { ), agencyName: chartState.data[AGENCY_DETAILS].name, chartTitle: getBarChartModalHeading( - 'Contraband "Hit Rate" Grouped By Stop Purpose', - contrabandStopPurposeYear + 'Contraband "Hit Rate" Grouped By Stop Purpose' ), }} /> - - - - + setContrabandTypesData((state) => ({ ...state, isOpen: true }))} + shareProps={{ + graphAnchor: 'hit_rate_by_type', + }} />

Shows what percentage of searches discovered specific types of illegal items.

@@ -702,30 +696,22 @@ function Contraband(props) { 'Shows what number of searches discovered specific types of illegal items' ), agencyName: chartState.data[AGENCY_DETAILS].name, - chartTitle: getBarChartModalHeading( - 'Contraband "Hit Rate" by type', - contrabandTypesYear - ), + chartTitle: getBarChartModalHeading('Contraband "Hit Rate" by type'), }} /> - - - +
- + setGroupedContrabandStopPurposeModalData((state) => ({ ...state, isOpen: true })) } + shareProps={{ + graphAnchor: 'hit_rate_by_type_and_stop_purpose', + }} />

@@ -777,13 +763,6 @@ function Contraband(props) { ))} - @@ -828,8 +806,7 @@ function Contraband(props) { ), agencyName: chartState.data[AGENCY_DETAILS].name, chartTitle: getBarChartModalHeading( - 'Contraband "Hit Rate" by Type grouped by Regulatory/Equipment', - year + 'Contraband "Hit Rate" by Type grouped by Regulatory/Equipment' ), }} /> @@ -855,8 +832,7 @@ function Contraband(props) { ), agencyName: chartState.data[AGENCY_DETAILS].name, chartTitle: getBarChartModalHeading( - 'Contraband "Hit Rate" by Type grouped by Other', - year + 'Contraband "Hit Rate" by Type grouped by Other' ), }} /> diff --git a/frontend/src/Components/Charts/Overview/Overview.js b/frontend/src/Components/Charts/Overview/Overview.js index 37209b6a..301d49d8 100644 --- a/frontend/src/Components/Charts/Overview/Overview.js +++ b/frontend/src/Components/Charts/Overview/Overview.js @@ -32,12 +32,14 @@ import useOfficerId from '../../../Hooks/useOfficerId'; import DataSubsetPicker from '../ChartSections/DataSubsetPicker/DataSubsetPicker'; import PieChart from '../../NewCharts/PieChart'; import { pieChartConfig, pieChartLabels } from '../../../util/setChartColors'; +import useYearSet from '../../../Hooks/useYearSet'; function Overview(props) { const { agencyId } = props; const history = useHistory(); const match = useRouteMatch(); const officerId = useOfficerId(); + const [yearRange] = useYearSet(); useDataset(agencyId, STOPS); useDataset(agencyId, SEARCHES); @@ -203,7 +205,7 @@ function Overview(props) { label="Year" value={year} onChange={handleYearSelect} - options={[YEARS_DEFAULT].concat(chartState.yearRange)} + options={[YEARS_DEFAULT].concat(yearRange)} dropDown /> diff --git a/frontend/src/Components/Charts/SearchRate/SearchRate.js b/frontend/src/Components/Charts/SearchRate/SearchRate.js index 28772c84..00c203e3 100644 --- a/frontend/src/Components/Charts/SearchRate/SearchRate.js +++ b/frontend/src/Components/Charts/SearchRate/SearchRate.js @@ -27,12 +27,15 @@ function SearchRate(props) { const [chartState] = useDataset(agencyId, LIKELIHOOD_OF_SEARCH); const [year, setYear] = useState(YEARS_DEFAULT); - const [searchRateData, setSearchRateData] = useState({ labels: [], datasets: [], loading: true }); + + const initData = { labels: [], datasets: [], loading: true }; + const [searchRateData, setSearchRateData] = useState(initData); const renderMetaTags = useMetaTags(); const [renderTableModal, { openModal }] = useTableModal(); useEffect(() => { + setSearchRateData(initData); const params = []; if (year && year !== 'All') { params.push({ param: 'year', val: year }); diff --git a/frontend/src/Components/Charts/Searches/Searches.js b/frontend/src/Components/Charts/Searches/Searches.js index 4219629c..877bf397 100644 --- a/frontend/src/Components/Charts/Searches/Searches.js +++ b/frontend/src/Components/Charts/Searches/Searches.js @@ -38,6 +38,12 @@ function Searches(props) { useDataset(agencyId, SEARCHES); const [chartState] = useDataset(agencyId, SEARCHES_BY_TYPE); + useEffect(() => { + if (window.location.hash) { + document.querySelector(`${window.location.hash}`).scrollIntoView(); + } + }, []); + const [searchType, setSearchType] = useState(SEARCH_TYPE_DEFAULT); const [searchPercentageData, setSearchPercentageData] = useState({ @@ -46,11 +52,12 @@ function Searches(props) { loading: true, }); - const [searchCountData, setSearchCountData] = useState({ + const initCountData = { labels: [], datasets: [], loading: true, - }); + }; + const [searchCountData, setSearchCountData] = useState(initCountData); const [searchCountType, setSearchCountType] = useState(0); const renderMetaTags = useMetaTags(); const [renderTableModal, { openModal }] = useTableModal(); @@ -75,6 +82,7 @@ function Searches(props) { // Build Searches By Count useEffect(() => { + setSearchCountData(initCountData); const params = []; if (searchType !== 0) { params.push({ param: 'search_type', val: searchCountType }); @@ -161,10 +169,13 @@ function Searches(props) { {/* Search Rate */} {renderMetaTags()} {renderTableModal()} - +

Shows the percent of stops that led to searches, broken down by race/ethnicity.

@@ -192,8 +203,14 @@ function Searches(props) {
{/* Searches by Count */} - - + +

Shows the number of searches performed {subjectObserving()}, broken down by search type and race / ethnicity. diff --git a/frontend/src/Components/Charts/TrafficStops/TrafficStops.js b/frontend/src/Components/Charts/TrafficStops/TrafficStops.js index 0380ea9b..69e57578 100644 --- a/frontend/src/Components/Charts/TrafficStops/TrafficStops.js +++ b/frontend/src/Components/Charts/TrafficStops/TrafficStops.js @@ -60,6 +60,12 @@ function TrafficStops(props) { // TODO: Remove this when moving table modal data to new modal component useDataset(agencyId, STOPS_BY_REASON); + useEffect(() => { + if (window.location.hash) { + document.querySelector(`${window.location.hash}`).scrollIntoView(); + } + }, []); + const [pickerActive, setPickerActive] = useState(null); const [year, setYear] = useState(YEARS_DEFAULT); @@ -107,6 +113,26 @@ function TrafficStops(props) { datasets: [], loading: true, }); + const [groupedStopYear, setGroupedStopYear] = useState(YEARS_DEFAULT); + + const purposeGroupedPieLabels = ['Safety Violation', 'Regulatory and Equipment', 'Other']; + const purposeGroupedPieColors = ['#5F0F40', '#E36414', '#0F4C5C']; + const purposeGroupedPieConfig = { + backgroundColor: purposeGroupedPieColors, + borderColor: purposeGroupedPieColors, + hoverBackgroundColor: purposeGroupedPieColors, + borderWidth: 1, + }; + const [stopPurposeGroupedPieData, setStopPurposeGroupedPieData] = useState({ + labels: purposeGroupedPieLabels, + datasets: [ + { + data: [], + ...purposeGroupedPieConfig, + }, + ], + }); + const [stopsGroupedByPurposeData, setStopsGroupedByPurpose] = useState({ labels: [], safety: { labels: [], datasets: [], loading: true }, @@ -197,17 +223,19 @@ function TrafficStops(props) { null, null, ]); - const [trafficStopsByCount, setTrafficStopsByCount] = useState({ + const initStopsByCount = { labels: [], datasets: [], loading: true, - }); + }; + const [trafficStopsByCount, setTrafficStopsByCount] = useState(initStopsByCount); const createDateForRange = (yr) => Number.isInteger(yr) ? new Date(`${yr}-01-01`) : new Date(yr); // Build Stops By Count useEffect(() => { + setTrafficStopsByCount(initStopsByCount); const params = []; if (trafficStopsByCountRange !== null) { const _from = `${trafficStopsByCountRange.from.year}-${trafficStopsByCountRange.from.month @@ -255,12 +283,13 @@ function TrafficStops(props) { .get(url) .then((res) => { setStopPurposeGroups(res.data); + buildStopPurposeGroupedPieData(res.data); }) .catch((err) => console.log(err)); }, []); - const buildPercentages = (data, ds) => { - if (!data.length) return [0, 0, 0, 0, 0, 0]; + const buildEthnicPercentages = (data, ds) => { + if (!data.hasOwnProperty(ds)) return [0, 0, 0, 0, 0, 0]; const dsTotal = data[ds].datasets .map((s) => s.data.reduce((a, b) => a + b, 0)) .reduce((a, b) => a + b, 0); @@ -280,9 +309,9 @@ function TrafficStops(props) { .then((res) => { setStopsGroupedByPurpose(res.data); updateStoppedPurposePieChart( - buildPercentages(res.data, 'safety'), - buildPercentages(res.data, 'regulatory'), - buildPercentages(res.data, 'other') + buildEthnicPercentages(res.data, 'safety'), + buildEthnicPercentages(res.data, 'regulatory'), + buildEthnicPercentages(res.data, 'other') ); }) .catch((err) => console.log(err)); @@ -340,6 +369,52 @@ function TrafficStops(props) { setYear(y); }; + const buildStopPurposeGroupedPieData = (ds, stopPurposeYear = null) => { + const getValues = (arr) => { + if (!stopPurposeYear) { + return arr.reduce((a, b) => a + b, 0); + } + // Reverse to match dropdown descending years + return arr.toReversed()[stopPurposeYear - 1] || 0; + }; + + const data = []; + if (ds) { + const safety = getValues(ds.datasets[0].data); + const regulatory = getValues(ds.datasets[1].data); + const other = getValues(ds.datasets[2].data); + const total = safety + regulatory + other; + + const normalize = (n) => ((n / total) * 100).toFixed(4); + + if (total > 0) { + data.push(normalize(safety)); + data.push(normalize(regulatory)); + data.push(normalize(other)); + } + } + setStopPurposeGroupedPieData({ + labels: purposeGroupedPieLabels, + datasets: [ + { + data, + ...purposeGroupedPieConfig, + }, + ], + }); + }; + + const handleGroupedStopPurposeYearSelect = (y, i) => { + if (y === groupedStopYear) return; + + setGroupedStopYear(y); + if (y === YEARS_DEFAULT) { + // eslint-disable-next-line no-param-reassign + i = null; + } + buildStopPurposeGroupedPieData(stopPurposeGroupsData, i); + }; + // Handle stop purpose dropdown state const handleStopPurposeSelect = (p, i) => { if (p === purpose) return; @@ -469,7 +544,7 @@ function TrafficStops(props) { setChecked(nextChecked); }; - const buildPercentagesForYear = (data, ds, idx = null) => { + const buildEthnicPercentagesForYear = (data, ds, idx = null) => { const dsTotal = data[ds].datasets.map((s) => s.data[idx]).reduce((a, b) => a + b, 0); return data[ds].datasets.map((s) => ((s.data[idx] / dsTotal) * 100 || 0).toFixed(2)); }; @@ -480,14 +555,14 @@ function TrafficStops(props) { const idxForYear = stopsGroupedByPurposeData.labels.length - idx; updateStoppedPurposePieChart( selectedYear === YEARS_DEFAULT - ? buildPercentages(stopsGroupedByPurposeData, 'safety') - : buildPercentagesForYear(stopsGroupedByPurposeData, 'safety', idxForYear), + ? buildEthnicPercentages(stopsGroupedByPurposeData, 'safety') + : buildEthnicPercentagesForYear(stopsGroupedByPurposeData, 'safety', idxForYear), selectedYear === YEARS_DEFAULT - ? buildPercentages(stopsGroupedByPurposeData, 'regulatory') - : buildPercentagesForYear(stopsGroupedByPurposeData, 'regulatory', idxForYear), + ? buildEthnicPercentages(stopsGroupedByPurposeData, 'regulatory') + : buildEthnicPercentagesForYear(stopsGroupedByPurposeData, 'regulatory', idxForYear), selectedYear === YEARS_DEFAULT - ? buildPercentages(stopsGroupedByPurposeData, 'other') - : buildPercentagesForYear(stopsGroupedByPurposeData, 'other', idxForYear) + ? buildEthnicPercentages(stopsGroupedByPurposeData, 'other') + : buildEthnicPercentagesForYear(stopsGroupedByPurposeData, 'other', idxForYear) ); }; @@ -545,15 +620,27 @@ function TrafficStops(props) { subject = `Officer ${officerId}`; } return `Traffic Stops By Percentage for ${subject} ${ - year === YEARS_DEFAULT ? `since ${stopsChartState.yearRange.reverse()[0]}` : `in ${year}` + year === YEARS_DEFAULT ? `since ${stopsChartState.yearRange.toReversed()[0]}` : `in ${year}` }`; }; - const getPieChartModalSubHeading = (title) => { - const yearSelected = year && year !== 'All' ? ` in ${year}` : ''; + const getPieChartModalSubHeading = (title, yr = null) => { + const yearSelected = yr && yr !== 'All' ? ` in ${yr}` : ''; return `${title} ${subjectObserving()}${yearSelected}.`; }; + const stopPurposeGroupPieChartTitle = () => { + let subject = stopsChartState.data[AGENCY_DETAILS].name; + if (officerId) { + subject = `Officer ${officerId}`; + } + return `Traffic Stops By Stop Purpose for ${subject} ${ + groupedStopYear === YEARS_DEFAULT + ? `since ${stopsGroupedByPurposeData.labels[0]}` + : `in ${groupedStopYear}` + }`; + }; + const getPieChartModalHeading = (stopPurpose) => { let subject = stopsChartState.data[AGENCY_DETAILS].name; if (officerId) { @@ -602,6 +689,14 @@ function TrafficStops(props) { return `Traffic Stops by Percentage for ${subject} since ${stopsByPercentageData.labels[0]}`; }; + const stopPurposeGroupedPieYears = () => { + if (stopPurposeGroupsData.labels) { + const years = [...stopPurposeGroupsData.labels].toReversed(); + return [YEARS_DEFAULT].concat(years); + } + return [YEARS_DEFAULT]; + }; + return ( {/* Traffic Stops by Percentage */} @@ -644,7 +739,8 @@ function TrafficStops(props) { modalConfig={{ tableHeader: 'Traffic Stops By Percentage', tableSubheader: getPieChartModalSubHeading( - 'Shows the race/ethnic composition of drivers stopped' + 'Shows the race/ethnic composition of drivers stopped', + year ), agencyName: stopsChartState.data[AGENCY_DETAILS].name, chartTitle: pieChartTitle(), @@ -656,15 +752,21 @@ function TrafficStops(props) { label="Year" value={year} onChange={handleYearSelect} - options={[YEARS_DEFAULT].concat(stopsChartState.yearRange)} + options={[YEARS_DEFAULT].concat(stopsByPercentageData.labels.toReversed())} /> {/* Traffic Stops by Count */} - - + +

Shows the number of traffics stops broken down by purpose and race / ethnicity.

@@ -706,10 +808,13 @@ function TrafficStops(props) {
- +

Shows the number of traffics stops broken down by purpose and race / ethnicity.

setStopPurposeModalData((state) => ({ ...state, isOpen: false }))} /> - - + + - - + + + + + + + + + +
- +

Shows the number of traffics stops broken down by purpose and race / ethnicity.

diff --git a/nc/data/importer.py b/nc/data/importer.py index fd461aa0..2e86d44e 100755 --- a/nc/data/importer.py +++ b/nc/data/importer.py @@ -26,9 +26,7 @@ MAGIC_NC_FTP_URL = "ftp://nc.us/" -def run( - url, destination=None, zip_path=None, min_stop_id=None, max_stop_id=None, prime_cache=False -): +def run(url, destination=None, zip_path=None, min_stop_id=None, max_stop_id=None, prime_cache=True): """ Download NC data, extract, convert to CSV, and load into PostgreSQL diff --git a/nc/management/commands/prime_cache.py b/nc/management/commands/prime_cache.py index 152ca83a..a479ea64 100755 --- a/nc/management/commands/prime_cache.py +++ b/nc/management/commands/prime_cache.py @@ -14,7 +14,7 @@ def add_arguments(self, parser): ) parser.add_argument("--clear-cache", "-c", action="store_true", default=False) parser.add_argument("--skip-agencies", action="store_true", default=False) - parser.add_argument("--skip-officers", action="store_true", default=False) + parser.add_argument("--skip-officers", action="store_true", default=True) parser.add_argument( "--officer-cutoff-count", type=int, diff --git a/nc/prime_cache.py b/nc/prime_cache.py index 9b0c4f74..06008bd6 100755 --- a/nc/prime_cache.py +++ b/nc/prime_cache.py @@ -123,7 +123,7 @@ def get_endpoints(self): def prime(self): logger.info(f"{self} starting") - self.count = self.get_queryset().count() + self.count = len(self.get_queryset()) logger.info(f"{self} priming {self.count:,} objects") for endpoints in self.get_endpoints(): for endpoint in endpoints: @@ -140,13 +140,24 @@ def __repr__(self): class AgencyStopsPrimer(CachePrimer): def get_queryset(self): - return ( + qs = list( Stop.objects.no_cache() .annotate(agency_name=F("agency_description")) .values("agency_name", "agency_id") .annotate(num_stops=Count("stop_id")) .order_by("-num_stops") ) + # Manually insert the statewide to force the caching since a + # stop instance won't directly be associated with the statewide agency id. + qs.insert( + 0, + { + "agency_name": "North Carolina State", + "agency_id": -1, + "num_stops": Stop.objects.count(), + }, + ) + return qs def get_urls(self, row): urls = [] @@ -177,7 +188,7 @@ def run( cutoff_duration_secs=None, clear_cache=False, skip_agencies=False, - skip_officers=False, + skip_officers=True, officer_cutoff_count=None, ): """ @@ -216,6 +227,6 @@ def run( if not skip_officers: OfficerStopsPrimer( cutoff_secs=0, cutoff_count=officer_cutoff_count - ).prime() # cache all officer endpoins for now + ).prime() # cache all officer endpoints for now logger.info("Complete") diff --git a/nc/tests/test_prime_cache.py b/nc/tests/test_prime_cache.py index 5ba04a2d..0d1aec8f 100755 --- a/nc/tests/test_prime_cache.py +++ b/nc/tests/test_prime_cache.py @@ -1,5 +1,6 @@ from django.test import TestCase +from nc.models import Agency from nc.prime_cache import run from nc.tests import factories @@ -13,6 +14,8 @@ class PrimeCacheTests(TestCase): databases = "__all__" def test_prime_cache(self): + factories.AgencyFactory(id=-1) # Statewide data + factories.ContrabandFactory() factories.ContrabandFactory() factories.ContrabandFactory() diff --git a/nc/views.py b/nc/views.py index 153dad09..50c929e6 100644 --- a/nc/views.py +++ b/nc/views.py @@ -4,6 +4,7 @@ from functools import reduce from operator import concat +import numpy import pandas as pd from dateutil import relativedelta @@ -12,7 +13,7 @@ from django.db.models import Case, Count, F, Q, Sum, Value, When from django.db.models.functions import ExtractYear from django.utils.decorators import method_decorator -from django.views.decorators.cache import never_cache +from django.views.decorators.cache import cache_page, never_cache from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets from rest_framework.decorators import action @@ -89,6 +90,9 @@ class QueryKeyConstructor(DefaultObjectKeyConstructor): query_cache_key_func = QueryKeyConstructor() +CACHE_TIMEOUT = settings.CACHE_COUNT_TIMEOUT + + def get_date_range(request): # Only filter is from and to values are found and are valid date_precision = "year" @@ -434,6 +438,7 @@ def get_values(race): ], } + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): stop_qs = StopSummary.objects.all().annotate(year=ExtractYear("date")) @@ -531,6 +536,7 @@ def get_values(race): ], } + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): date_precision, date_range = get_date_range(request) @@ -576,6 +582,7 @@ def get_values(self, df, stop_purpose, years_len): else: return [0] * years_len + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): qs = StopSummary.objects.all() agency_id = int(agency_id) @@ -677,6 +684,7 @@ def get_values(col): ], } + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): qs = StopSummary.objects.all() agency_id = int(agency_id) @@ -740,6 +748,7 @@ def get(self, request, agency_id): class AgencyContrabandView(APIView): + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): year = request.GET.get("year", None) @@ -822,6 +831,7 @@ def get(self, request, agency_id): class AgencyContrabandTypesView(APIView): + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): year = request.GET.get("year", None) @@ -943,6 +953,7 @@ def get_values(col): ], } + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): year = request.GET.get("year", None) @@ -1094,6 +1105,7 @@ def create_dataset(self, contraband_df, searches_df, stop_purpose): data.append(group) return data + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): year = request.GET.get("year", None) @@ -1162,6 +1174,7 @@ def get(self, request, agency_id): class AgencyContrabandStopGroupByPurposeModalView(APIView): + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): grouped_stop_purpose = request.GET.get("grouped_stop_purpose") contraband_type = request.GET.get("contraband_type") @@ -1271,6 +1284,7 @@ def get_values(race): ], } + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): stop_qs = StopSummary.objects.all().annotate(year=ExtractYear("date")) @@ -1383,6 +1397,7 @@ def get_values(race): ], } + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): date_precision, date_range = get_date_range(request) @@ -1425,7 +1440,11 @@ def build_response(self, df, labels): def get_values(race): if race in df: values = [float(df[race][label]) if label in df[race] else 0 for label in labels] - values.insert(0, sum(values) / len(values)) + try: + average = sum(values) / len(values) + except ZeroDivisionError: + average = 0 + values.insert(0, average) return values return [0] * (len(labels) + 1) @@ -1466,6 +1485,7 @@ def get_values(race): ], } + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): stop_qs = StopSummary.objects.all().annotate(year=ExtractYear("date")) search_qs = StopSummary.objects.filter(search_type__isnull=False).annotate( @@ -1514,8 +1534,9 @@ def get(self, request, agency_id): def get_val(df, column, purpose): if column in df and purpose in df[column]: - return df[column][purpose] - return 0 + val = df[column][purpose] + return float(0) if numpy.isnan(val) else float(val) + return float(0) for col in columns: for k, v in purpose_choices.items(): @@ -1592,6 +1613,7 @@ def get_values(race): ], } + @method_decorator(cache_page(CACHE_TIMEOUT)) def get(self, request, agency_id): qs = StopSummary.objects.filter(search_type__isnull=False, engage_force="t").annotate( year=ExtractYear("date")