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
60 changes: 40 additions & 20 deletions apps/project/custom_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,45 @@ class CustomOptionDefaults:
},
]

# NOTE: Unlike the options above, which are user-definable per project, the
# FIND, COMPARE and COMPLETENESS project types have no `custom_options`.
# Users cannot define their own; these fixed defaults are the only options these
# project types can use.
FIND_COMPARE_COMPLETENESS: list[CustomOption] = [
{
"title": "Yes",
"icon": IconEnum.CHECKMARK_OUTLINE,
"value": 1,
"description": "",
"icon_color": "#388E3C",
},
{
"title": "No",
"icon": IconEnum.CLOSE_OUTLINE,
"value": 0,
"description": "",
"icon_color": "#D32F2F",
},
{
"title": "Maybe",
"icon": IconEnum.REMOVE_OUTLINE,
"value": 2,
"description": "",
"icon_color": "#616161",
},
{
"title": "Bad Imagery",
"icon": IconEnum.ALERT_OUTLINE,
"value": 3,
"description": "",
"icon_color": "#ff9800",
},
]


def get_custom_options(project_type: ProjectTypeEnum) -> list[CustomOption]:
# User-definable: these defaults are only a starting point and can be overridden
# per project
if project_type == ProjectTypeEnum.VALIDATE:
return CustomOptionDefaults.VALIDATE
if project_type == ProjectTypeEnum.VALIDATE_IMAGE:
Expand All @@ -128,29 +165,12 @@ def get_custom_options(project_type: ProjectTypeEnum) -> list[CustomOption]:
return CustomOptionDefaults.STREET
if project_type == ProjectTypeEnum.LOCATE:
return CustomOptionDefaults.LOCATE
return []


def get_fallback_custom_options_for_export(project_type: ProjectTypeEnum) -> list[int]:
# FIXME: Should we throw error for validate, validate image and street instead?
if project_type == ProjectTypeEnum.VALIDATE:
return [item["value"] for item in CustomOptionDefaults.VALIDATE]
if project_type == ProjectTypeEnum.VALIDATE_IMAGE:
return [item["value"] for item in CustomOptionDefaults.VALIDATE_IMAGE]
if project_type == ProjectTypeEnum.STREET:
return [item["value"] for item in CustomOptionDefaults.STREET]
if project_type == ProjectTypeEnum.LOCATE:
return [item["value"] for item in CustomOptionDefaults.LOCATE]

# Fixed: these cannot be defined by the user,
# so these are the only options they can use.
if (
project_type == ProjectTypeEnum.FIND
or project_type == ProjectTypeEnum.COMPARE
or project_type == ProjectTypeEnum.COMPLETENESS
):
return [
0, # No
1, # Yes
2, # Maybe
3, # Bad Imagery
]
return CustomOptionDefaults.FIND_COMPARE_COMPLETENESS
typing.assert_never(project_type)
7 changes: 2 additions & 5 deletions apps/project/exports/exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from ulid import ULID

from apps.common.models import AssetTypeEnum, FirebasePushStatusEnum
from apps.project.custom_options import get_fallback_custom_options_for_export
from apps.project.custom_options import get_custom_options
from apps.project.exports.geojson import gzipped_csv_to_gzipped_geojson
from apps.project.exports.utils.project_progress import calculate_project_progress
from apps.project.models import Project, ProjectAsset, ProjectAssetExportTypeEnum, ProjectProgressStatusEnum
Expand Down Expand Up @@ -100,10 +100,7 @@ def _export_project_data(project: Project, tmp_directory: Path):

# Fallback if custom options is not defined
if not custom_options_raw:
custom_options_raw = [
{"value": custom_option_value}
for custom_option_value in get_fallback_custom_options_for_export(project.project_type_enum)
]
custom_options_raw = get_custom_options(project.project_type_enum)

# TODO: Cache tasks and groups as they don't change (if cached, make sure to remove Unnamed column)
tasks_df = generate_project_tasks(destination_filename=tmp_project_task_csv, project=project)
Expand Down
2 changes: 1 addition & 1 deletion apps/project/exports/mapping_results_aggregate/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from apps.project.models import Project, ProjectTypeEnum
from utils.geo.tile_functions import tile_coords_and_zoom_to_quad_key

CustomOptionType = list[dict[str, typing.Any]] # TODO: fix typing
CustomOptionType = typing.Sequence[typing.Mapping[str, typing.Any]]

# TODO: use this https://strictly-typed-pandas.readthedocs.io/en/latest/#?

Expand Down
6 changes: 5 additions & 1 deletion apps/project/exports/tasking_manager_geometries.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ def _get_row_value[T: int | float](
"task_x": task_x,
"task_y": task_y,
"task_z": task_z,
# XXX: Assuming 0->No, 1->Yes, 2->Maybe, 3->Bad
# NOTE: These values are defined canonically in
# ./apps/project/custom_options.py
# 0->No, 1->Yes, 2->Maybe, 3->Bad
# As tasking manager geometries are created for find,
# compare and completeness it's okay to hardcode these here
"no_count": _get_row_value(column_index_map, row, "0_count"),
"yes_count": _get_row_value(column_index_map, row, "1_count"),
"maybe_count": _get_row_value(column_index_map, row, "2_count"),
Expand Down
10 changes: 9 additions & 1 deletion apps/project/tests/e2e_create_project_tile_map_service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ def read_csv(

@contextmanager
def create_override():
"""E2E determinism mechanism: pin server-generated `firebase_id` to `client_id`.

The `client_id` comes from the hardcoded JSON5 fixtures, so forcing
`firebase_id == client_id` (and `tutorial_<client_id>` for tutorials) keeps the
committed expected files stable. Any ULID NOT pinned here (e.g. the AOI asset
`clientId`) stays intentionally random because nothing asserts against it.
"""

def pre_save_override(sender: typing.Any, instance: typing.Any, **kwargs): # type: ignore[reportMissingParameterType]
if sender == Tutorial:
instance.firebase_id = f"tutorial_{instance.client_id}"
Expand Down Expand Up @@ -469,7 +477,7 @@ def _test_project(self, projectKey: str, filename: str):

# Create GeoJSON Asset for AOI Geometry
aoi_asset_data = {
"clientId": str(ULID()),
"clientId": str(ULID()), # NOTE: random by design; not asserted against (see create_override)
"inputType": "AOI_GEOMETRY",
"project": project_id,
}
Expand Down
10 changes: 9 additions & 1 deletion apps/project/tests/e2e_create_street_project_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@

@contextmanager
def create_override():
"""E2E determinism mechanism: pin server-generated `firebase_id` to `client_id`.

The `client_id` comes from the hardcoded JSON5 fixtures, so forcing
`firebase_id == client_id` (and `tutorial_<client_id>` for tutorials) keeps the
committed expected files stable. Any ULID NOT pinned here (e.g. the AOI asset
`clientId`) stays intentionally random because nothing asserts against it.
"""

def pre_save_override(sender: typing.Any, instance: typing.Any, **kwargs): # type: ignore[reportMissingParameterType]
if sender == Tutorial:
instance.firebase_id = f"tutorial_{instance.client_id}"
Expand Down Expand Up @@ -392,7 +400,7 @@ def _test_project(self, filename: str):

# Create GeoJSON Asset for AOI Geometry
aoi_asset_data = {
"clientId": str(ULID()),
"clientId": str(ULID()), # NOTE: random by design; not asserted against (see create_override)
"inputType": "AOI_GEOMETRY",
"project": project_id,
}
Expand Down
10 changes: 9 additions & 1 deletion apps/project/tests/e2e_create_validate_image_project_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@

@contextmanager
def create_override():
"""E2E determinism mechanism: pin server-generated `firebase_id` to `client_id`.

The `client_id` comes from the hardcoded JSON5 fixtures, so forcing
`firebase_id == client_id` (and `tutorial_<client_id>` for tutorials) keeps the
committed expected files stable. Any ULID NOT pinned here (e.g. the AOI asset
`clientId`) stays intentionally random because nothing asserts against it.
"""

def pre_save_override(sender: typing.Any, instance: typing.Any, **kwargs): # type: ignore[reportMissingParameterType]
if sender == Tutorial:
instance.firebase_id = f"tutorial_{instance.client_id}"
Expand Down Expand Up @@ -379,7 +387,7 @@ def _test_project(self, filename: str):
coco_data = json5.load(f)
for image in iter(coco_data["images"]):
image_asset_data = {
"clientId": str(ULID()),
"clientId": str(ULID()), # NOTE: random by design; not asserted against (see create_override)
"inputType": "OBJECT_IMAGE",
"project": project_id,
"assetTypeSpecifics": {
Expand Down
10 changes: 9 additions & 1 deletion apps/project/tests/e2e_create_validate_project_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@

@contextmanager
def create_override():
"""E2E determinism mechanism: pin server-generated `firebase_id` to `client_id`.

The `client_id` comes from the hardcoded JSON5 fixtures, so forcing
`firebase_id == client_id` (and `tutorial_<client_id>` for tutorials) keeps the
committed expected files stable. Any ULID NOT pinned here (e.g. the AOI asset
`clientId`) stays intentionally random because nothing asserts against it.
"""

def pre_save_override(sender: typing.Any, instance: typing.Any, **kwargs): # type: ignore[reportMissingParameterType]
if sender == Tutorial:
instance.firebase_id = f"tutorial_{instance.client_id}"
Expand Down Expand Up @@ -408,7 +416,7 @@ def _test_project(self, filename: str):

# Create GeoJSON Asset for AOI Geometry
aoi_asset_data = {
"clientId": str(ULID()),
"clientId": str(ULID()), # NOTE: random by design; not asserted against (see create_override)
"inputType": "AOI_GEOMETRY",
"project": project_id,
}
Expand Down
Loading