diff --git a/apps/project/custom_options.py b/apps/project/custom_options.py index c35528df..3845f129 100644 --- a/apps/project/custom_options.py +++ b/apps/project/custom_options.py @@ -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: @@ -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) diff --git a/apps/project/exports/exports.py b/apps/project/exports/exports.py index 1eb3865d..d2e9aea9 100644 --- a/apps/project/exports/exports.py +++ b/apps/project/exports/exports.py @@ -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 @@ -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) diff --git a/apps/project/exports/mapping_results_aggregate/task.py b/apps/project/exports/mapping_results_aggregate/task.py index cd69a6bf..bfad7df9 100644 --- a/apps/project/exports/mapping_results_aggregate/task.py +++ b/apps/project/exports/mapping_results_aggregate/task.py @@ -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/#? diff --git a/apps/project/exports/tasking_manager_geometries.py b/apps/project/exports/tasking_manager_geometries.py index 07057eab..917ff0e8 100644 --- a/apps/project/exports/tasking_manager_geometries.py +++ b/apps/project/exports/tasking_manager_geometries.py @@ -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"), diff --git a/apps/project/tests/e2e_create_project_tile_map_service_test.py b/apps/project/tests/e2e_create_project_tile_map_service_test.py index 85e9273d..d22d63da 100644 --- a/apps/project/tests/e2e_create_project_tile_map_service_test.py +++ b/apps/project/tests/e2e_create_project_tile_map_service_test.py @@ -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_` 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}" @@ -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, } diff --git a/apps/project/tests/e2e_create_street_project_test.py b/apps/project/tests/e2e_create_street_project_test.py index 062df685..f8e267b8 100644 --- a/apps/project/tests/e2e_create_street_project_test.py +++ b/apps/project/tests/e2e_create_street_project_test.py @@ -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_` 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}" @@ -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, } diff --git a/apps/project/tests/e2e_create_validate_image_project_test.py b/apps/project/tests/e2e_create_validate_image_project_test.py index bb4aa269..b01b2564 100644 --- a/apps/project/tests/e2e_create_validate_image_project_test.py +++ b/apps/project/tests/e2e_create_validate_image_project_test.py @@ -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_` 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}" @@ -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": { diff --git a/apps/project/tests/e2e_create_validate_project_test.py b/apps/project/tests/e2e_create_validate_project_test.py index d3d0fb02..573341ca 100644 --- a/apps/project/tests/e2e_create_validate_project_test.py +++ b/apps/project/tests/e2e_create_validate_project_test.py @@ -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_` 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}" @@ -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, }