Skip to content

Commit cb85897

Browse files
committed
test(project): add tests for street project, aggregate, geo functions
1 parent c337f0d commit cb85897

File tree

9 files changed

+679
-7
lines changed

9 files changed

+679
-7
lines changed

apps/project/exports/mapping_results_aggregate/tests/__init__.py

Whitespace-only changes.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import pandas as pd
2+
3+
from apps.project.exports.mapping_results_aggregate.task import (
4+
_add_missing_result_columns, # type: ignore[reportPrivateUsage]
5+
_calc_agreement, # type: ignore[reportPrivateUsage]
6+
_calc_count, # type: ignore[reportPrivateUsage]
7+
_calc_parent_option_count, # type: ignore[reportPrivateUsage]
8+
_calc_share, # type: ignore[reportPrivateUsage]
9+
_get_custom_options, # type: ignore[reportPrivateUsage]
10+
)
11+
from main.tests import TestCase
12+
13+
14+
class TestProjectStats(TestCase):
15+
def test_calc_agreement(self):
16+
ds = pd.Series(
17+
data=[40, 15, 5, 17, 3],
18+
index=["total_count", "1_count", "2_count", "3_count", "4_count"],
19+
)
20+
agg2 = _calc_agreement(ds)
21+
assert agg2 == 0.32564102564102565
22+
23+
def test_calc_count(self):
24+
df = pd.DataFrame(
25+
data=[[1, 15, 5, 20], [1, 234, 45, 6]],
26+
columns=["taskId", "1_count", "2_count", "3_count"],
27+
)
28+
result = _calc_count(df)
29+
assert result[0] == 40
30+
31+
def test_calc_share(self):
32+
df = pd.DataFrame(
33+
data=[[1, 40, 15, 5, 20], [1, 285, 234, 45, 6]],
34+
columns=["taskId", "total_count", "1_count", "2_count", "3_count"],
35+
)
36+
share = _calc_share(df)
37+
assert share.filter(like="share").iloc[0].tolist() == [0.375, 0.125, 0.5]
38+
39+
def test_get_custom_options(self):
40+
for raw_custom_options, excepted_values in [
41+
(
42+
[{"value": 0}, {"value": 1}, {"value": 2}, {"value": 3}],
43+
{0: set(), 1: set(), 2: set(), 3: set()},
44+
),
45+
(
46+
[
47+
{
48+
"value": 0,
49+
"subOptions": [{"value": 4}, {"value": 5}],
50+
},
51+
{"value": 1},
52+
{"value": 2},
53+
{"value": 3},
54+
],
55+
{0: {4, 5}, 1: set(), 2: set(), 3: set()},
56+
),
57+
(
58+
[
59+
{
60+
"value": 0,
61+
"subOptions": [{"value": 4}, {"value": 5}],
62+
},
63+
{"value": 1},
64+
{"value": 2},
65+
{
66+
"value": 3,
67+
"subOptions": [{"value": 10}, {"value": 12}],
68+
},
69+
],
70+
{0: {4, 5}, 1: set(), 2: set(), 3: {10, 12}},
71+
),
72+
]:
73+
parsed_custom_options = _get_custom_options(raw_custom_options)
74+
assert parsed_custom_options == excepted_values
75+
76+
def test_add_missing_result_columns(self):
77+
df = pd.DataFrame(
78+
data=[
79+
["project-1-group-1-task-1", 1],
80+
["project-1-group-1-task-1", 5],
81+
["project-1-group-2-task-1", 1],
82+
["project-1-group-2-task-1", 1],
83+
["project-1-group-2-task-1", 1],
84+
["project-2-group-3-task-1", 2],
85+
["project-2-group-1-task-1", 3],
86+
],
87+
columns=[
88+
"task_id",
89+
"result",
90+
],
91+
)
92+
df = df.groupby(["task_id", "result"]).size().unstack(fill_value=0)
93+
updated_df = _add_missing_result_columns(
94+
df,
95+
{
96+
1: {4, 5},
97+
2: {6},
98+
3: set(),
99+
},
100+
)
101+
# Existing columns
102+
assert list(df.columns) == [1, 2, 3, 5]
103+
# New columns
104+
assert list(updated_df.columns) == [1, 2, 3, 4, 5, 6]
105+
# Existing data
106+
assert df.to_csv() == (
107+
"task_id,1,2,3,5\n"
108+
"project-1-group-1-task-1,1,0,0,1\n"
109+
"project-1-group-2-task-1,3,0,0,0\n"
110+
"project-2-group-1-task-1,0,0,1,0\n"
111+
"project-2-group-3-task-1,0,1,0,0\n"
112+
)
113+
# New data
114+
assert updated_df.to_csv() == (
115+
"task_id,1,2,3,4,5,6\n"
116+
"project-1-group-1-task-1,1,0,0,0,1,0\n"
117+
"project-1-group-2-task-1,3,0,0,0,0,0\n"
118+
"project-2-group-1-task-1,0,0,1,0,0,0\n"
119+
"project-2-group-3-task-1,0,1,0,0,0,0\n"
120+
)
121+
122+
def test_calc_parent_option_count(self):
123+
df = pd.DataFrame(
124+
data=[
125+
[1, 40, 1, 0, 20, 0, 1, 0],
126+
[2, 41, 0, 5, 20, 0, 0, 0],
127+
[3, 42, 10, 10, 20, 0, 0, 1],
128+
[4, 281, 0, 1, 0, 1, 1, 4],
129+
[5, 282, 15, 10, 0, 1, 2, 4],
130+
[1, 283, 2, 20, 0, 1, 0, 0],
131+
],
132+
columns=[
133+
"taskId",
134+
"total_count",
135+
"1_count",
136+
"2_count",
137+
"3_count",
138+
"4_count", # Child of 1
139+
"5_count", # Child of 1
140+
"6_count", # Child of 2
141+
],
142+
)
143+
updated_df = _calc_parent_option_count(
144+
df,
145+
{
146+
1: {4, 5},
147+
2: {6},
148+
3: set(),
149+
},
150+
)
151+
# Columns without child shouldn't change
152+
for column in [
153+
"taskId",
154+
"total_count",
155+
"3_count",
156+
"4_count",
157+
"5_count",
158+
"6_count",
159+
]:
160+
assert df[column].compare(updated_df[column]).size == 0
161+
# Columns with child should change
162+
for column, updated_index, updated_value in [
163+
("1_count", [0, 3, 4, 5], [2, 2, 18, 3]),
164+
("2_count", [2, 3, 4], [11, 5, 14]),
165+
]:
166+
compared = df[column].compare(updated_df[column])
167+
assert list(compared["other"].index) == updated_index
168+
assert list(compared["other"]) == updated_value

project_types/street/api_calls.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import mercantile # type: ignore[reportMissingTypeStubs]
99
import pandas as pd
1010
import requests
11+
from geojson_pydantic import Feature as PydanticFeature
1112
from geojson_pydantic import FeatureCollection as PydanticFeatureCollection
1213
from geojson_pydantic.geometries import MultiPolygon as PydanticMultiPolygon
1314
from geojson_pydantic.geometries import Polygon as PydanticPolygon
@@ -73,17 +74,33 @@ def create_tiles(
7374

7475
# FIXME: move this to utils
7576
def geojson_to_polygon(geojson_data: dict[str, Any]):
77+
fc: PydanticFeatureCollection[Any] | None
78+
try:
79+
feature = PydanticFeature(**geojson_data)
80+
fc = PydanticFeatureCollection(
81+
type="FeatureCollection",
82+
features=[feature],
83+
)
84+
except ValidationError:
85+
fc = None
86+
7687
# NOTE: We might not need this, as we already check this
7788
try:
78-
fc = PydanticFeatureCollection(**geojson_data)
89+
if not fc:
90+
fc = PydanticFeatureCollection(**geojson_data)
7991
except ValidationError as e:
8092
raise ValidationException("Invalid GeoJSON FeatureCollection") from e
8193

8294
polygon_types = (PydanticPolygon, PydanticMultiPolygon)
83-
geometries = [shape(feature.geometry) for feature in fc.features if isinstance(feature.geometry, polygon_types)]
8495

85-
if not geometries:
86-
raise ValidationException("No valid Polygon or MultiPolygon found in the GeoJSON FeatureCollection")
96+
has_invalid_geometries = any(not isinstance(feature.geometry, polygon_types) for feature in fc.features)
97+
if has_invalid_geometries:
98+
raise ValidationException("Non-polygon geometries cannot be combined into a MultiPolygon.")
99+
100+
geometries = [shape(feature.geometry) for feature in fc.features]
101+
102+
# if not geometries:
103+
# raise ValidationException("No valid Polygon or MultiPolygon found in the GeoJSON FeatureCollection")
87104

88105
return unary_union(geometries)
89106

@@ -140,7 +157,7 @@ def parallelized_processing(
140157

141158
def download_and_process_tile(
142159
*,
143-
row: dict[Hashable, Any],
160+
row: dict[Hashable, Any] | pd.Series,
144161
polygon: ShapelyBaseGeometry,
145162
kwargs: dict[str, Any],
146163
attempt_limit: int = 3,
@@ -219,7 +236,7 @@ def filter_results(
219236
results_df: pd.DataFrame,
220237
creator_id: int | None = None,
221238
is_pano: bool | None = None,
222-
organization_id: str | None = None,
239+
organization_id: int | None = None,
223240
start_time: str | None = None,
224241
end_time: str | None = None,
225242
):

project_types/street/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)