From e0b64431b383e35d0907cd63e6539437646cff3e Mon Sep 17 00:00:00 2001 From: Sushil Tiwari Date: Fri, 30 Jan 2026 15:56:43 +0545 Subject: [PATCH 1/7] feat(locate): add new project type locate features --- apps/project/custom_options.py | 2 + apps/project/exports/exports.py | 4 +- .../graphql/inputs/project_types/locate.py | 7 ++ .../graphql/types/project_types/locate.py | 7 ++ apps/project/graphql/types/types.py | 8 ++ apps/project/models.py | 5 ++ apps/project/serializers.py | 2 + firebase | 2 +- project_types/store.py | 39 ++++++++- .../tile_map_service/locate/__init__.py | 0 .../tile_map_service/locate/project.py | 82 +++++++++++++++++++ 11 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 apps/project/graphql/inputs/project_types/locate.py create mode 100644 apps/project/graphql/types/project_types/locate.py create mode 100644 project_types/tile_map_service/locate/__init__.py create mode 100644 project_types/tile_map_service/locate/project.py diff --git a/apps/project/custom_options.py b/apps/project/custom_options.py index 050176fd..ed16b21a 100644 --- a/apps/project/custom_options.py +++ b/apps/project/custom_options.py @@ -118,6 +118,8 @@ def get_fallback_custom_options_for_export(project_type: ProjectTypeEnum) -> lis project_type == ProjectTypeEnum.FIND or project_type == ProjectTypeEnum.COMPARE or project_type == ProjectTypeEnum.COMPLETENESS + # TODO(susilnem): Verify custom options for locate? setting default for now. + or project_type == ProjectTypeEnum.LOCATE ): return [ 0, # No diff --git a/apps/project/exports/exports.py b/apps/project/exports/exports.py index 351ae639..0473c6b7 100644 --- a/apps/project/exports/exports.py +++ b/apps/project/exports/exports.py @@ -19,6 +19,7 @@ from project_types.tile_map_service.compare.project import CompareProjectProperty from project_types.tile_map_service.completeness.project import CompletenessProjectProperty from project_types.tile_map_service.find.project import FindProjectProperty +from project_types.tile_map_service.locate.project import LocateProjectProperty from utils.geo.raster_tile_server.config import RasterTileServerNameEnum from .mapping_results import generate_mapping_results @@ -91,7 +92,8 @@ def _export_project_data(project: Project, tmp_directory: Path): if not isinstance( project_type_handler.project_type_specifics, # NOTE: Using negate test to throw type error if new project type is added - (CompareProjectProperty | CompletenessProjectProperty | FindProjectProperty), + # TODO(susilnem): verify Custom options for locate project type? + (CompareProjectProperty | CompletenessProjectProperty | FindProjectProperty | LocateProjectProperty), ): custom_options_raw = [ {"value": custom_option.value} diff --git a/apps/project/graphql/inputs/project_types/locate.py b/apps/project/graphql/inputs/project_types/locate.py new file mode 100644 index 00000000..d95488d8 --- /dev/null +++ b/apps/project/graphql/inputs/project_types/locate.py @@ -0,0 +1,7 @@ +import strawberry + +from project_types.tile_map_service.locate import project as locate_project + + +@strawberry.experimental.pydantic.input(model=locate_project.LocateProjectProperty, all_fields=True) +class LocateProjectPropertyInput: ... diff --git a/apps/project/graphql/types/project_types/locate.py b/apps/project/graphql/types/project_types/locate.py new file mode 100644 index 00000000..0cec0330 --- /dev/null +++ b/apps/project/graphql/types/project_types/locate.py @@ -0,0 +1,7 @@ +import strawberry + +from project_types.tile_map_service.locate import project as locate_project + + +@strawberry.experimental.pydantic.type(model=locate_project.LocateProjectProperty, all_fields=True) +class LocateProjectPropertyType: ... diff --git a/apps/project/graphql/types/types.py b/apps/project/graphql/types/types.py index 315ab2da..90e18257 100644 --- a/apps/project/graphql/types/types.py +++ b/apps/project/graphql/types/types.py @@ -18,6 +18,7 @@ from project_types.tile_map_service.compare import project as compare_project from project_types.tile_map_service.completeness import project as completeness_project from project_types.tile_map_service.find import project as find_project +from project_types.tile_map_service.locate import project as locate_project from project_types.validate import project as validate_project from project_types.validate_image import project as validate_image_project from utils.asset_types.models import AoiGeometryAssetProperty, ObjectImageAssetProperty @@ -32,6 +33,7 @@ from .project_types.compare import CompareProjectPropertyType from .project_types.completeness import CompletenessProjectPropertyType from .project_types.find import FindProjectPropertyType +from .project_types.locate import LocateProjectPropertyType from .project_types.street import StreetProjectPropertyType from .project_types.validate import ValidateProjectPropertyType from .project_types.validate_image import ValidateImageProjectPropertyType @@ -219,6 +221,7 @@ async def project_type_specifics( | ValidateImageProjectPropertyType | CompletenessProjectPropertyType | StreetProjectPropertyType + | LocateProjectPropertyType | None ): data = project.project_type_specifics @@ -245,4 +248,9 @@ async def project_type_specifics( "StreetProjectPropertyType", street_project.StreetProjectProperty.model_validate(data), ) + if project.project_type_enum == Project.Type.LOCATE: + return typing.cast( + "LocateProjectPropertyType", + locate_project.LocateProjectProperty.model_validate(data), + ) typing.assert_never(project.project_type_enum) diff --git a/apps/project/models.py b/apps/project/models.py index 44aa399c..c01312cf 100644 --- a/apps/project/models.py +++ b/apps/project/models.py @@ -114,6 +114,9 @@ class ProjectTypeEnum(models.IntegerChoices): STREET = 7, "View Streets" """ Street project type. """ + LOCATE = 9, "Locate Features" + """ Locate project type. """ + # TODO(thenav56): Confirm if we have more/less @classmethod @@ -136,6 +139,8 @@ def to_firebase(self) -> firebase_models.FbEnumProjectType: return firebase_models.FbEnumProjectType.VALIDATE_IMAGE case ProjectTypeEnum.STREET: return firebase_models.FbEnumProjectType.STREET + case ProjectTypeEnum.LOCATE: + return firebase_models.FbEnumProjectType.LOCATE # TODO(tnagorra): Reset the values later diff --git a/apps/project/serializers.py b/apps/project/serializers.py index 65e8640f..20882514 100644 --- a/apps/project/serializers.py +++ b/apps/project/serializers.py @@ -125,6 +125,8 @@ def _validate_group_size(self, attrs: dict[str, typing.Any]): group_size = 80 case Project.Type.STREET: group_size = 25 + case Project.Type.LOCATE: + group_size = 120 attrs["group_size"] = group_size diff --git a/firebase b/firebase index 8b0d6833..b4a0bf8f 160000 --- a/firebase +++ b/firebase @@ -1 +1 @@ -Subproject commit 8b0d6833579d182dab7d7ea1689e6b5c83d124b7 +Subproject commit b4a0bf8f8865c323abf0af235ef73ba124f8254b diff --git a/project_types/store.py b/project_types/store.py index f380cbe8..1d2f28d1 100644 --- a/project_types/store.py +++ b/project_types/store.py @@ -10,6 +10,8 @@ from .tile_map_service.completeness.tutorial import CompletenessTutorial, CompletenessTutorialTaskProperty from .tile_map_service.find.project import FindProject, FindProjectProperty from .tile_map_service.find.tutorial import FindTutorial, FindTutorialTaskProperty +from .tile_map_service.locate.project import LocateProject, LocateProjectProperty +from .tile_map_service.locate.tutorial import LocateTutorial, LocateTutorialTaskProperty from .validate.project import ValidateProject, ValidateProjectProperty from .validate.tutorial import ValidateTutorial, ValidateTutorialTaskProperty from .validate_image.project import ValidateImageProject, ValidateImageProjectProperty @@ -32,6 +34,8 @@ def get_tutorial_task_property(project_type: ProjectTypeEnum | None): return ("completeness", CompletenessTutorialTaskProperty) if project_type == ProjectTypeEnum.STREET: return ("street", StreetTutorialTaskProperty) + if project_type == ProjectTypeEnum.LOCATE: + return ("locate", LocateTutorialTaskProperty) typing.assert_never(project_type) @@ -51,11 +55,19 @@ def get_project_property(project_type: ProjectTypeEnum | None): return ("completeness", CompletenessProjectProperty) if project_type == ProjectTypeEnum.STREET: return ("street", StreetProjectProperty) + if project_type == ProjectTypeEnum.LOCATE: + return ("locate", LocateProjectProperty) typing.assert_never(project_type) type ProjectTypeHandlers = type[ - CompareProject | ValidateProject | ValidateImageProject | FindProject | CompletenessProject | StreetProject + CompareProject + | ValidateProject + | ValidateImageProject + | FindProject + | CompletenessProject + | StreetProject + | LocateProject ] @@ -95,6 +107,12 @@ def get_project_type_handler( ) -> type[StreetProject]: ... +@typing.overload +def get_project_type_handler( + project_type: typing.Literal[ProjectTypeEnum.LOCATE], +) -> type[LocateProject]: ... + + def get_project_type_handler(project_type: ProjectTypeEnum) -> ProjectTypeHandlers: match project_type: case ProjectTypeEnum.FIND: @@ -109,10 +127,18 @@ def get_project_type_handler(project_type: ProjectTypeEnum) -> ProjectTypeHandle return ValidateImageProject case ProjectTypeEnum.STREET: return StreetProject + case ProjectTypeEnum.LOCATE: + return LocateProject type TutorialTypeHandlers = type[ - CompareTutorial | ValidateTutorial | FindTutorial | CompletenessTutorial | ValidateImageTutorial | StreetTutorial + CompareTutorial + | ValidateTutorial + | FindTutorial + | CompletenessTutorial + | ValidateImageTutorial + | StreetTutorial + | LocateTutorial ] @@ -146,13 +172,18 @@ def get_tutorial_type_handler( ) -> type[ValidateImageTutorial]: ... -# FIXME(susilnem): Handle street @typing.overload def get_tutorial_type_handler( tutorial_type: typing.Literal[ProjectTypeEnum.STREET], ) -> type[typing.Any]: ... +@typing.overload +def get_tutorial_type_handler( + tutorial_type: typing.Literal[ProjectTypeEnum.LOCATE], +) -> type[typing.Any]: ... + + def get_tutorial_type_handler(tutorial_type: ProjectTypeEnum) -> TutorialTypeHandlers: match tutorial_type: case ProjectTypeEnum.FIND: @@ -167,3 +198,5 @@ def get_tutorial_type_handler(tutorial_type: ProjectTypeEnum) -> TutorialTypeHan return ValidateImageTutorial case ProjectTypeEnum.STREET: return StreetTutorial + case ProjectTypeEnum.LOCATE: + return LocateTutorial diff --git a/project_types/tile_map_service/locate/__init__.py b/project_types/tile_map_service/locate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project_types/tile_map_service/locate/project.py b/project_types/tile_map_service/locate/project.py new file mode 100644 index 00000000..d9285ace --- /dev/null +++ b/project_types/tile_map_service/locate/project.py @@ -0,0 +1,82 @@ +import typing + +from django.db import models +from pyfirebase_mapswipe import models as firebase_models + +from apps.project.models import Project, ProjectTypeEnum +from project_types.tile_map_service.base import project as tile_map_service_project + + +class SubGridSizeEnum(models.TextChoices): + SIZE_2X2 = "2x2", "2x2" + SIZE_4X4 = "4x4", "4x4" + SIZE_8X8 = "8x8", "8x8" + + def to_firebase(self) -> firebase_models.FBEnumSubGridSize: + match self: + case SubGridSizeEnum.SIZE_2X2: + return firebase_models.FBEnumSubGridSize.SIZE_2X2 + case SubGridSizeEnum.SIZE_4X4: + return firebase_models.FBEnumSubGridSize.SIZE_4X4 + case SubGridSizeEnum.SIZE_8X8: + return firebase_models.FBEnumSubGridSize.SIZE_8X8 + + +class LocateProjectProperty(tile_map_service_project.TileMapServiceProjectProperty): + sub_grid_size: SubGridSizeEnum + + +class LocateProjectTaskGroupProperty(tile_map_service_project.TileMapServiceProjectTaskGroupProperty): ... + + +class LocateProjectTaskProperty(tile_map_service_project.TileMapServiceProjectTaskProperty): ... + + +class LocateProject( + tile_map_service_project.TileMapServiceBaseProject[ + LocateProjectProperty, + LocateProjectTaskGroupProperty, + LocateProjectTaskProperty, + ], +): + project_property_class = LocateProjectProperty + project_task_group_property_class = LocateProjectTaskGroupProperty + project_task_property_class = LocateProjectTaskProperty + + def __init__(self, project: Project): + super().__init__(project) + if typing.TYPE_CHECKING: + assert project.project_type == ProjectTypeEnum.LOCATE, f"{type(self)} is defined for LOCATE" + + @typing.override + def get_max_time_spend_percentile(self) -> float: + return 1.4 + + @typing.override + def get_task_specifics_for_db(self, tile_x: int, tile_y: int) -> LocateProjectTaskProperty: + return self.project_task_property_class( + tile_x=tile_x, + tile_y=tile_y, + url=self.project_type_specifics.tile_server_property.generate_url( + tile_x, + tile_y, + self.project_type_specifics.zoom_level, + ), + ) + + # FIREBASE + + @typing.override + def get_project_specifics_for_firebase(self): + tsp = self.project_type_specifics.tile_server_property + return firebase_models.FbProjectLocateCreateOnlyInput( + zoomLevel=self.project_type_specifics.zoom_level, + subGridSize=self.project_type_specifics.sub_grid_size.to_firebase(), + tileServer=firebase_models.FbObjRasterTileServer( + name=tsp.name.to_firebase(), + credits=tsp.get_config()["credits"], + url=tsp.get_config()["raw_url"], + apiKey=tsp.get_config()["api_key"], + wmtsLayerName=None, + ), + ) From 091472691dd3d76a77c04a690ef4cb1fb81388ef Mon Sep 17 00:00:00 2001 From: Sushil Tiwari Date: Fri, 30 Jan 2026 15:57:43 +0545 Subject: [PATCH 2/7] feat(locate): add tutorial for locate features --- .../0011_alter_project_project_type.py | 19 ++++++++++++ .../graphql/inputs/project_types/locate.py | 7 +++++ .../graphql/types/project_types/locate.py | 7 +++++ apps/tutorial/graphql/types/types.py | 8 +++++ .../tile_map_service/locate/tutorial.py | 23 ++++++++++++++ schema.graphql | 31 ++++++++++++++++--- 6 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 apps/project/migrations/0011_alter_project_project_type.py create mode 100644 apps/tutorial/graphql/inputs/project_types/locate.py create mode 100644 apps/tutorial/graphql/types/project_types/locate.py create mode 100644 project_types/tile_map_service/locate/tutorial.py diff --git a/apps/project/migrations/0011_alter_project_project_type.py b/apps/project/migrations/0011_alter_project_project_type.py new file mode 100644 index 00000000..a6c6a94a --- /dev/null +++ b/apps/project/migrations/0011_alter_project_project_type.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.5 on 2026-01-30 10:17 + +import django_choices_field.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('project', '0010_alter_projecttask_unique_together'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='project_type', + field=django_choices_field.fields.IntegerChoicesField(choices=[(1, 'Find Features'), (2, 'Validate Footprints'), (10, 'Assess Images'), (3, 'Compare Dates'), (4, 'Check Completeness'), (7, 'View Streets'), (9, 'Locate Features')]), + ), + ] diff --git a/apps/tutorial/graphql/inputs/project_types/locate.py b/apps/tutorial/graphql/inputs/project_types/locate.py new file mode 100644 index 00000000..a24dd538 --- /dev/null +++ b/apps/tutorial/graphql/inputs/project_types/locate.py @@ -0,0 +1,7 @@ +import strawberry + +from project_types.tile_map_service.locate import tutorial as locate_tutorial + + +@strawberry.experimental.pydantic.input(model=locate_tutorial.LocateTutorialTaskProperty, all_fields=True) +class LocateTutorialTaskPropertyInput: ... diff --git a/apps/tutorial/graphql/types/project_types/locate.py b/apps/tutorial/graphql/types/project_types/locate.py new file mode 100644 index 00000000..4570ff04 --- /dev/null +++ b/apps/tutorial/graphql/types/project_types/locate.py @@ -0,0 +1,7 @@ +import strawberry + +from project_types.tile_map_service.locate import tutorial as locate_tutorial + + +@strawberry.experimental.pydantic.type(model=locate_tutorial.LocateTutorialTaskProperty, all_fields=True) +class LocateTutorialTaskPropertyType: ... diff --git a/apps/tutorial/graphql/types/types.py b/apps/tutorial/graphql/types/types.py index a0ad4615..4427594c 100644 --- a/apps/tutorial/graphql/types/types.py +++ b/apps/tutorial/graphql/types/types.py @@ -21,12 +21,14 @@ from project_types.tile_map_service.compare import tutorial as compare_tutorial from project_types.tile_map_service.completeness import tutorial as completeness_tutorial from project_types.tile_map_service.find import tutorial as find_tutorial +from project_types.tile_map_service.locate import tutorial as locate_tutorial from project_types.validate import tutorial as validate_tutorial from project_types.validate_image import tutorial as validate_image_tutorial from .project_types.compare import CompareTutorialTaskPropertyType from .project_types.completeness import CompletenessTutorialTaskPropertyType from .project_types.find import FindTutorialTaskPropertyType +from .project_types.locate import LocateTutorialTaskPropertyType from .project_types.street import StreetTutorialTaskPropertyType from .project_types.validate import ValidateTutorialTaskPropertyType from .project_types.validate_image import ValidateImageTutorialTaskPropertyType @@ -65,6 +67,7 @@ async def project_type_specifics( | ValidateImageTutorialTaskPropertyType | CompletenessTutorialTaskPropertyType | StreetTutorialTaskPropertyType + | LocateTutorialTaskPropertyType | None ): data = task.project_type_specifics @@ -99,6 +102,11 @@ async def project_type_specifics( "StreetTutorialTaskPropertyType", street_tutorial.StreetTutorialTaskProperty.model_validate(data), ) + if project_type_enum == Project.Type.LOCATE: + return typing.cast( + "LocateTutorialTaskPropertyType", + locate_tutorial.LocateTutorialTaskProperty.model_validate(data), + ) typing.assert_never(project_type_enum) diff --git a/project_types/tile_map_service/locate/tutorial.py b/project_types/tile_map_service/locate/tutorial.py new file mode 100644 index 00000000..184b3178 --- /dev/null +++ b/project_types/tile_map_service/locate/tutorial.py @@ -0,0 +1,23 @@ +from apps.tutorial.models import Tutorial +from project_types.tile_map_service.base import tutorial as tile_map_service_tutorial + +from .project import LocateProjectProperty + + +class LocateTutorialTaskProperty(tile_map_service_tutorial.TileMapServiceTutorialTaskProperty): ... + + +# TODO(susilnem): Handle tutorial for locate project type + + +class LocateTutorial( + tile_map_service_tutorial.TileMapServiceBaseTutorial[ + LocateProjectProperty, + LocateTutorialTaskProperty, + ], +): + project_property_class = LocateProjectProperty + tutorial_task_property_class = LocateTutorialTaskProperty + + def __init__(self, tutorial: Tutorial): + super().__init__(tutorial) diff --git a/schema.graphql b/schema.graphql index c66dc63d..64e9eda8 100644 --- a/schema.graphql +++ b/schema.graphql @@ -318,7 +318,7 @@ type CompareProjectPropertyType { zoomLevel: Int! } -union CompareProjectPropertyTypeFindProjectPropertyTypeValidateProjectPropertyTypeValidateImageProjectPropertyTypeCompletenessProjectPropertyTypeStreetProjectPropertyType = CompareProjectPropertyType | CompletenessProjectPropertyType | FindProjectPropertyType | StreetProjectPropertyType | ValidateImageProjectPropertyType | ValidateProjectPropertyType +union CompareProjectPropertyTypeFindProjectPropertyTypeValidateProjectPropertyTypeValidateImageProjectPropertyTypeCompletenessProjectPropertyTypeStreetProjectPropertyTypeLocateProjectPropertyType = CompareProjectPropertyType | CompletenessProjectPropertyType | FindProjectPropertyType | LocateProjectPropertyType | StreetProjectPropertyType | ValidateImageProjectPropertyType | ValidateProjectPropertyType input CompareTutorialTaskPropertyInput { tileX: Int! @@ -332,7 +332,7 @@ type CompareTutorialTaskPropertyType { tileZ: Int! } -union CompareTutorialTaskPropertyTypeFindTutorialTaskPropertyTypeValidateTutorialTaskPropertyTypeValidateImageTutorialTaskPropertyTypeCompletenessTutorialTaskPropertyTypeStreetTutorialTaskPropertyType = CompareTutorialTaskPropertyType | CompletenessTutorialTaskPropertyType | FindTutorialTaskPropertyType | StreetTutorialTaskPropertyType | ValidateImageTutorialTaskPropertyType | ValidateTutorialTaskPropertyType +union CompareTutorialTaskPropertyTypeFindTutorialTaskPropertyTypeValidateTutorialTaskPropertyTypeValidateImageTutorialTaskPropertyTypeCompletenessTutorialTaskPropertyTypeStreetTutorialTaskPropertyTypeLocateTutorialTaskPropertyType = CompareTutorialTaskPropertyType | CompletenessTutorialTaskPropertyType | FindTutorialTaskPropertyType | LocateTutorialTaskPropertyType | StreetTutorialTaskPropertyType | ValidateImageTutorialTaskPropertyType | ValidateTutorialTaskPropertyType input CompletenessProjectPropertyInput { """Numeric value as string""" @@ -935,6 +935,22 @@ input IntRangeLookup { start: Int = null } +type LocateProjectPropertyType { + """Numeric value as string""" + aoiGeometry: String! + subGridSize: SubGridSizeEnum! + tileServerProperty: ProjectRasterTileServerConfig! + + """Zoom level from 14 to 22""" + zoomLevel: Int! +} + +type LocateTutorialTaskPropertyType { + tileX: Int! + tileY: Int! + tileZ: Int! +} + type MapContributionStatsType { geojson: GenericJSON! totalContribution: Int! @@ -1826,7 +1842,7 @@ type ProjectType implements UserResourceTypeMixin & ProjectExportAssetTypeMixin projectNumber: Int! projectType: ProjectTypeEnum! projectTypeSpecificOutputAsset: ProjectAssetType - projectTypeSpecifics: CompareProjectPropertyTypeFindProjectPropertyTypeValidateProjectPropertyTypeValidateImageProjectPropertyTypeCompletenessProjectPropertyTypeStreetProjectPropertyType + projectTypeSpecifics: CompareProjectPropertyTypeFindProjectPropertyTypeValidateProjectPropertyTypeValidateImageProjectPropertyTypeCompletenessProjectPropertyTypeStreetProjectPropertyTypeLocateProjectPropertyType region: String! """Which group, institution or community is requesting this project?""" @@ -1864,6 +1880,7 @@ enum ProjectTypeEnum { COMPARE COMPLETENESS FIND + LOCATE STREET VALIDATE VALIDATE_IMAGE @@ -2191,6 +2208,12 @@ type StreetTutorialTaskPropertyType { mapillaryImageId: String! } +enum SubGridSizeEnum { + SIZE_2X2 + SIZE_4X4 + SIZE_8X8 +} + type TestValidateAoiObjectsResponse { assetId: ID error: String @@ -2559,7 +2582,7 @@ type TutorialTaskType implements UserResourceTypeMixin { id: ID! modifiedAt: DateTime! modifiedBy: UserType! - projectTypeSpecifics: CompareTutorialTaskPropertyTypeFindTutorialTaskPropertyTypeValidateTutorialTaskPropertyTypeValidateImageTutorialTaskPropertyTypeCompletenessTutorialTaskPropertyTypeStreetTutorialTaskPropertyType + projectTypeSpecifics: CompareTutorialTaskPropertyTypeFindTutorialTaskPropertyTypeValidateTutorialTaskPropertyTypeValidateImageTutorialTaskPropertyTypeCompletenessTutorialTaskPropertyTypeStreetTutorialTaskPropertyTypeLocateTutorialTaskPropertyType reference: Int! scenarioId: ID! } From 2f9524084c593de44846122335a4f0e379f9d9da Mon Sep 17 00:00:00 2001 From: Sushil Tiwari Date: Mon, 2 Feb 2026 12:18:34 +0545 Subject: [PATCH 3/7] feat(locate): add test cases for project locate features - add locate project type specifics on mutation --- apps/project/graphql/inputs/inputs.py | 2 + apps/project/tests/mutation_test.py | 301 ++++++++++++++++++++++++++ schema.graphql | 11 + 3 files changed, 314 insertions(+) diff --git a/apps/project/graphql/inputs/inputs.py b/apps/project/graphql/inputs/inputs.py index 7b99982d..c2da3f6b 100644 --- a/apps/project/graphql/inputs/inputs.py +++ b/apps/project/graphql/inputs/inputs.py @@ -18,6 +18,7 @@ from .project_types.compare import CompareProjectPropertyInput from .project_types.completeness import CompletenessProjectPropertyInput from .project_types.find import FindProjectPropertyInput +from .project_types.locate import LocateProjectPropertyInput from .project_types.street import StreetProjectPropertyInput from .project_types.validate import ValidateProjectPropertyInput from .project_types.validate_image import ValidateImageProjectPropertyInput @@ -53,6 +54,7 @@ class ProjectTypeSpecificInput: validate: ValidateProjectPropertyInput | None = strawberry.UNSET validate_image: ValidateImageProjectPropertyInput | None = strawberry.UNSET street: StreetProjectPropertyInput | None = strawberry.UNSET + locate: LocateProjectPropertyInput | None = strawberry.UNSET # NOTE: Make sure this matches with the serializers ../serializers.py diff --git a/apps/project/tests/mutation_test.py b/apps/project/tests/mutation_test.py index f406ac4a..5f28e1c2 100644 --- a/apps/project/tests/mutation_test.py +++ b/apps/project/tests/mutation_test.py @@ -27,6 +27,8 @@ from main.tests import TestCase from project_types.street import project as street_project from project_types.tile_map_service.compare import project as compare_project +from project_types.tile_map_service.locate import project as locate_project +from project_types.tile_map_service.locate.project import SubGridSizeEnum from utils.geo.raster_tile_server.config import RasterTileServerNameEnum BASE_DIR = Path(__file__).resolve().parent @@ -1692,3 +1694,302 @@ def test_project_street(self, mock_requests): # type: ignore[reportMissingParam assert resp_data["result"]["status"] == self.genum(Project.Status.READY_TO_PROCESS) mock_requests.assert_called_once() + + @patch("apps.project.serializers.process_project_task.delay") + def test_project_locate(self, mock_requests): # type: ignore[reportMissingParameterType] + self.force_login(self.user) + project_data = { + **self.project_data, + "projectType": self.genum(ProjectTypeEnum.LOCATE), + "clientId": str(ULID()), + } + content = self._create_project_mutation(project_data) + resp_data = content["data"]["createProject"] + assert resp_data["errors"] is None, content + + project_id = resp_data["result"]["id"] + project_client_id = resp_data["result"]["clientId"] + + # Creating AOI Project Asset + project_asset_data = { + "project": project_id, + "clientId": str(ULID()), + } + content = self._create_project_aoi_asset(project_asset_data) + resp_data = content["data"]["createProjectAsset"] + assert resp_data["errors"] is None, content + aoi_geometry_asset = resp_data["result"] + + # Creating Project Image Asset + project_asset_data = { + "project": project_id, + "clientId": str(ULID()), + } + content = self._create_project_image_asset(project_asset_data) + resp_data = content["data"]["createProjectAsset"] + assert resp_data["errors"] is None, content + image_asset = resp_data["result"] + + # Updating Project + project_data = { + "clientId": project_client_id, + "image": image_asset["id"], + "verificationNumber": 10, + "projectTypeSpecifics": { + "locate": { + "aoiGeometry": aoi_geometry_asset["id"], + "zoomLevel": 15, + "tileServerProperty": { + "name": self.genum(RasterTileServerNameEnum.CUSTOM), + "custom": { + "url": "https://hi-there/{x}/{y}/{z}", + "credits": "My Map", + }, + }, + "subGridSize": self.genum(SubGridSizeEnum.SIZE_2X2), + }, + }, + } + content = self._update_project_mutation(project_id, project_data) + resp_data = content["data"]["updateProject"] + assert resp_data["errors"] is None, content + + latest_project = Project.objects.get(pk=project_id) + assert latest_project.created_by_id == self.user.pk + assert latest_project.modified_by_id == self.user.pk + assert latest_project.image_id == int(image_asset["id"]) + assert latest_project.aoi_geometry_input_asset + assert latest_project.aoi_geometry_input_asset.id == int(aoi_geometry_asset["id"]) + assert latest_project.project_type_specifics == { + "aoi_geometry": aoi_geometry_asset["id"], + "zoom_level": 15, + "tile_server_property": { + "name": RasterTileServerNameEnum.CUSTOM.value, + "custom": { + "url": "https://hi-there/{x}/{y}/{z}", + "credits": "My Map", + }, + }, + "sub_grid_size": SubGridSizeEnum.SIZE_2X2.value, + } + locate_project.LocateProjectProperty.model_validate( + latest_project.project_type_specifics, + context={"project_id": latest_project.pk}, + ) + + # Updating Project: + # Test project processing + project_data = { + "clientId": project_client_id, + "status": self.genum(Project.Status.READY_TO_PROCESS), + } + content = self._update_project_status_mutation(project_id, project_data) + resp_data = content["data"]["updateProjectStatus"] + assert resp_data["errors"] is None, content + assert resp_data["result"]["status"] == self.genum(Project.Status.READY_TO_PROCESS) + latest_project.refresh_from_db() + assert latest_project.processing_status is None + + mock_requests.assert_called_once() + mock_requests.assert_has_calls([call(int(project_id))]) + + process_project_task(int(project_id)) + + latest_project.refresh_from_db() + + project_task_group_qs = ProjectTaskGroup.objects.filter(project=latest_project) + project_task_qs = ProjectTask.objects.filter(task_group__project=latest_project) + + class TaskGroupSpecificsType(typing.TypedDict): + x_max: int + x_min: int + y_max: int + y_min: int + + class TaskGroupType(typing.TypedDict): + firebase_id: str + number_of_tasks: int + required_count: int + total_area: float + project_type_specifics: TaskGroupSpecificsType + + expected_tasks_groups: list[TaskGroupType] = [ + { + "firebase_id": "g101", + "number_of_tasks": 18, + "project_type_specifics": { + "x_max": 24152, + "x_min": 24147, + "y_max": 13755, + "y_min": 13753, + }, + "required_count": 10, + "total_area": 21.010735845202447, + }, + { + "firebase_id": "g102", + "number_of_tasks": 24, + "project_type_specifics": { + "x_max": 24153, + "x_min": 24146, + "y_max": 13758, + "y_min": 13756, + }, + "required_count": 10, + "total_area": 28.02915392364502, + }, + { + "firebase_id": "g103", + "number_of_tasks": 24, + "project_type_specifics": { + "x_max": 24153, + "x_min": 24146, + "y_max": 13761, + "y_min": 13759, + }, + "required_count": 10, + "total_area": 28.043986769512177, + }, + { + "firebase_id": "g104", + "number_of_tasks": 6, + "project_type_specifics": { + "x_max": 24150, + "x_min": 24149, + "y_max": 13764, + "y_min": 13762, + }, + "required_count": 10, + "total_area": 7.014703242812157, + }, + ] + + expected_last_5_tasks = [ + { + "firebase_id": "15-24147-13753", + "project_type_specifics": { + "tile_x": 24147, + "tile_y": 13753, + "url": "https://hi-there/24147/13753/15", + }, + }, + { + "firebase_id": "15-24147-13754", + "project_type_specifics": { + "tile_x": 24147, + "tile_y": 13754, + "url": "https://hi-there/24147/13754/15", + }, + }, + { + "firebase_id": "15-24147-13755", + "project_type_specifics": { + "tile_x": 24147, + "tile_y": 13755, + "url": "https://hi-there/24147/13755/15", + }, + }, + { + "firebase_id": "15-24148-13753", + "project_type_specifics": { + "tile_x": 24148, + "tile_y": 13753, + "url": "https://hi-there/24148/13753/15", + }, + }, + { + "firebase_id": "15-24148-13754", + "project_type_specifics": { + "tile_x": 24148, + "tile_y": 13754, + "url": "https://hi-there/24148/13754/15", + }, + }, + ] + + assert { + "required_results": (18 + 24 + 24 + 6) * 10, + "tasks_groups_count": project_task_group_qs.count(), + "tasks_groups": list( + project_task_group_qs.order_by("id").values( + "firebase_id", + "number_of_tasks", + "required_count", + "total_area", + "project_type_specifics", + ), + ), + "tasks_count": project_task_qs.count(), + "tasks": list( + project_task_qs.order_by("id").values( + "firebase_id", + "project_type_specifics", + )[:5], + ), + "status": latest_project.status, + "processing_status": latest_project.processing_status, + } == { + "required_results": latest_project.required_results, + "tasks_groups_count": len(expected_tasks_groups), + "tasks_groups": expected_tasks_groups, + "tasks_count": 72, + "tasks": expected_last_5_tasks, + "status": Project.Status.PROCESSED, + "processing_status": Project.ProcessingStatus.COMPLETED, + } + + # Updating Processed Project: + # Attaching tutorial + locate_tutorial = TutorialFactory.create( + **self.user_resource_kwargs, + project=latest_project, + ) + project_data = { + "clientId": project_client_id, + "tutorial": str(locate_tutorial.id), + } + content = self._update_processed_project_mutation(project_id, project_data) + resp_data = content["data"]["updateProcessedProject"] + assert resp_data["errors"] is None, content + assert resp_data["result"]["tutorialId"] == str(locate_tutorial.id) + + # project is not sync to firebase + project_ref = self.firebase_helper.ref( + Config.FirebaseKeys.project(latest_project.firebase_id), + ) + fb_project: typing.Any = project_ref.get() + assert fb_project is None + + # Updating Processed Project: + # Publishing project + project_data = { + "clientId": project_client_id, + "status": self.genum(Project.Status.READY_TO_PUBLISH), + } + content = self._update_project_status_mutation(project_id, project_data) + resp_data = content["data"]["updateProjectStatus"] + assert resp_data["errors"] is None, content + assert resp_data["result"]["status"] == self.genum(Project.Status.READY_TO_PUBLISH) + + latest_project.refresh_from_db() + assert latest_project.processing_status == Project.ProcessingStatus.COMPLETED + + # project is sync to firebase after publish + fb_project: typing.Any = project_ref.get() + assert fb_project is not None + + # Updating Processed Project after publishing + project_data = { + "clientId": project_client_id, + "maxTasksPerUser": 1000, + } + content = self._update_processed_project_mutation(project_id, project_data) + resp_data = content["data"]["updateProcessedProject"] + assert resp_data["errors"] is None, content + assert resp_data["result"]["maxTasksPerUser"] == 1000 + project_ref = self.firebase_helper.ref( + Config.FirebaseKeys.project(latest_project.firebase_id), + ) + fb_project: typing.Any = project_ref.get() + assert fb_project is not None + assert fb_project["maxTasksPerUser"] == 1000 diff --git a/schema.graphql b/schema.graphql index 64e9eda8..a65a3d70 100644 --- a/schema.graphql +++ b/schema.graphql @@ -935,6 +935,16 @@ input IntRangeLookup { start: Int = null } +input LocateProjectPropertyInput { + """Numeric value as string""" + aoiGeometry: String! + subGridSize: SubGridSizeEnum! + tileServerProperty: ProjectRasterTileServerConfigInput! + + """Zoom level from 14 to 22""" + zoomLevel: Int! +} + type LocateProjectPropertyType { """Numeric value as string""" aoiGeometry: String! @@ -1954,6 +1964,7 @@ input ProjectTypeSpecificInput @oneOf { compare: CompareProjectPropertyInput completeness: CompletenessProjectPropertyInput find: FindProjectPropertyInput + locate: LocateProjectPropertyInput street: StreetProjectPropertyInput validate: ValidateProjectPropertyInput validateImage: ValidateImageProjectPropertyInput From 8c18aef207876abbb68541074169ceb158879df0 Mon Sep 17 00:00:00 2001 From: Sushil Tiwari Date: Mon, 2 Feb 2026 12:31:29 +0545 Subject: [PATCH 4/7] fixup! feat(locate): add test cases for project locate features --- firebase | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase b/firebase index b4a0bf8f..e72734f4 160000 --- a/firebase +++ b/firebase @@ -1 +1 @@ -Subproject commit b4a0bf8f8865c323abf0af235ef73ba124f8254b +Subproject commit e72734f45d9574480b1fd9847bc485a5fd3d79bf From 048b3d4f409cf2b581cb24d03f9e1ef4986117a7 Mon Sep 17 00:00:00 2001 From: frozenhelium Date: Wed, 4 Feb 2026 11:44:35 +0545 Subject: [PATCH 5/7] fixup! feat(locate): add new project type locate features --- main/graphql/enums.py | 2 ++ schema.graphql | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/main/graphql/enums.py b/main/graphql/enums.py index a992c7ab..75f15325 100644 --- a/main/graphql/enums.py +++ b/main/graphql/enums.py @@ -8,6 +8,7 @@ from apps.project import models as project_models from apps.tutorial import models as tutorial_models from project_types.tile_map_service.completeness.project import OverlayLayerTypeEnum +from project_types.tile_map_service.locate.project import SubGridSizeEnum from project_types.validate.project import ValidateObjectSourceTypeEnum from project_types.validate_image.project import ValidateImageSourceTypeEnum from utils.geo.raster_tile_server.config import RasterTileServerNameEnum @@ -19,6 +20,7 @@ ValidateObjectSourceTypeEnum, ValidateImageSourceTypeEnum, OverlayLayerTypeEnum, + SubGridSizeEnum, project_models.ProjectTypeEnum, project_models.ProjectProgressStatusEnum, project_models.ProjectStatusEnum, diff --git a/schema.graphql b/schema.graphql index a65a3d70..daf1dbbf 100644 --- a/schema.graphql +++ b/schema.graphql @@ -37,6 +37,7 @@ type AppEnumCollection { ProjectStatusEnum: [AppEnumCollectionProjectStatusEnum!]! ProjectTypeEnum: [AppEnumCollectionProjectTypeEnum!]! RasterTileServerNameEnum: [AppEnumCollectionRasterTileServerNameEnum!]! + SubGridSizeEnum: [AppEnumCollectionSubGridSizeEnum!]! TutorialAssetInputTypeEnum: [AppEnumCollectionTutorialAssetInputTypeEnum!]! TutorialInformationPageBlockTypeEnum: [AppEnumCollectionTutorialInformationPageBlockTypeEnum!]! TutorialStatusEnum: [AppEnumCollectionTutorialStatusEnum!]! @@ -120,6 +121,11 @@ type AppEnumCollectionRasterTileServerNameEnum { label: String! } +type AppEnumCollectionSubGridSizeEnum { + key: SubGridSizeEnum! + label: String! +} + type AppEnumCollectionTutorialAssetInputTypeEnum { key: TutorialAssetInputTypeEnum! label: String! From faaea953a2f443a9d1c2eb12e9d954004c7bb32a Mon Sep 17 00:00:00 2001 From: Sushil Tiwari Date: Tue, 10 Feb 2026 14:18:17 +0545 Subject: [PATCH 6/7] feat(locate): add new fields export meta and custom_options --- apps/project/custom_options.py | 31 +++++++++++++- apps/project/exports/exports.py | 4 +- .../graphql/inputs/project_types/locate.py | 4 ++ .../graphql/types/project_types/locate.py | 4 ++ apps/project/tests/mutation_test.py | 41 +++++++++++++++++++ firebase | 2 +- .../tile_map_service/locate/project.py | 38 +++++++++++++++++ schema.graphql | 14 +++++++ 8 files changed, 132 insertions(+), 6 deletions(-) diff --git a/apps/project/custom_options.py b/apps/project/custom_options.py index ed16b21a..39f4f826 100644 --- a/apps/project/custom_options.py +++ b/apps/project/custom_options.py @@ -94,6 +94,31 @@ class CustomOptionDefaults: }, ] + # TODO(susilnem): verify the CustomOptions? + Locate: list[CustomOption] = [ + { + "title": "Single Feature", + "icon": IconEnum.CHECKMARK_OUTLINE, + "value": 1, + "description": "the shape outlines a single feature in the image", + "icon_color": "#388E3C", + }, + { + "title": "No", + "icon": IconEnum.CLOSE_OUTLINE, + "value": 0, + "description": "the shape does not outline any feature in the image", + "icon_color": "#D32F2F", + }, + { + "title": "Multiple Features", + "icon": IconEnum.CHECKMARK_OUTLINE, + "value": 2, + "description": "the shape outlines multiple features in the image", + "icon_color": "#388E3C", + }, + ] + def get_custom_options(project_type: ProjectTypeEnum) -> list[CustomOption]: if project_type == ProjectTypeEnum.VALIDATE: @@ -102,6 +127,8 @@ def get_custom_options(project_type: ProjectTypeEnum) -> list[CustomOption]: return CustomOptionDefaults.VALIDATE_IMAGE if project_type == ProjectTypeEnum.STREET: return CustomOptionDefaults.STREET + if project_type == ProjectTypeEnum.LOCATE: + return CustomOptionDefaults.Locate return [] @@ -113,13 +140,13 @@ def get_fallback_custom_options_for_export(project_type: ProjectTypeEnum) -> lis 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] if ( project_type == ProjectTypeEnum.FIND or project_type == ProjectTypeEnum.COMPARE or project_type == ProjectTypeEnum.COMPLETENESS - # TODO(susilnem): Verify custom options for locate? setting default for now. - or project_type == ProjectTypeEnum.LOCATE ): return [ 0, # No diff --git a/apps/project/exports/exports.py b/apps/project/exports/exports.py index 0473c6b7..351ae639 100644 --- a/apps/project/exports/exports.py +++ b/apps/project/exports/exports.py @@ -19,7 +19,6 @@ from project_types.tile_map_service.compare.project import CompareProjectProperty from project_types.tile_map_service.completeness.project import CompletenessProjectProperty from project_types.tile_map_service.find.project import FindProjectProperty -from project_types.tile_map_service.locate.project import LocateProjectProperty from utils.geo.raster_tile_server.config import RasterTileServerNameEnum from .mapping_results import generate_mapping_results @@ -92,8 +91,7 @@ def _export_project_data(project: Project, tmp_directory: Path): if not isinstance( project_type_handler.project_type_specifics, # NOTE: Using negate test to throw type error if new project type is added - # TODO(susilnem): verify Custom options for locate project type? - (CompareProjectProperty | CompletenessProjectProperty | FindProjectProperty | LocateProjectProperty), + (CompareProjectProperty | CompletenessProjectProperty | FindProjectProperty), ): custom_options_raw = [ {"value": custom_option.value} diff --git a/apps/project/graphql/inputs/project_types/locate.py b/apps/project/graphql/inputs/project_types/locate.py index d95488d8..a35e6e1f 100644 --- a/apps/project/graphql/inputs/project_types/locate.py +++ b/apps/project/graphql/inputs/project_types/locate.py @@ -3,5 +3,9 @@ from project_types.tile_map_service.locate import project as locate_project +@strawberry.experimental.pydantic.input(model=locate_project.ExportMeta, all_fields=True) +class LocateExportMetaInput: ... + + @strawberry.experimental.pydantic.input(model=locate_project.LocateProjectProperty, all_fields=True) class LocateProjectPropertyInput: ... diff --git a/apps/project/graphql/types/project_types/locate.py b/apps/project/graphql/types/project_types/locate.py index 0cec0330..bedbcd94 100644 --- a/apps/project/graphql/types/project_types/locate.py +++ b/apps/project/graphql/types/project_types/locate.py @@ -3,5 +3,9 @@ from project_types.tile_map_service.locate import project as locate_project +@strawberry.experimental.pydantic.type(model=locate_project.ExportMeta, all_fields=True) +class LocateExportMeta: ... + + @strawberry.experimental.pydantic.type(model=locate_project.LocateProjectProperty, all_fields=True) class LocateProjectPropertyType: ... diff --git a/apps/project/tests/mutation_test.py b/apps/project/tests/mutation_test.py index 5f28e1c2..b81c3dde 100644 --- a/apps/project/tests/mutation_test.py +++ b/apps/project/tests/mutation_test.py @@ -1747,6 +1747,25 @@ def test_project_locate(self, mock_requests): # type: ignore[reportMissingParam }, }, "subGridSize": self.genum(SubGridSizeEnum.SIZE_2X2), + "exportMeta": { + "key": "test1", + "value": "value1", + }, + "customOptions": { + "clientId": str(ULID()), + "description": "Locate project description", + "icon": self.genum(IconEnum.ADD_OUTLINE), + "iconColor": "#FF0000", + "title": "Locate Project Title", + "value": 1, + "subOptions": [ + { + "clientId": str(ULID()), + "value": 1, + "description": "Locate sub option description", + }, + ], + }, }, }, } @@ -1760,6 +1779,7 @@ def test_project_locate(self, mock_requests): # type: ignore[reportMissingParam assert latest_project.image_id == int(image_asset["id"]) assert latest_project.aoi_geometry_input_asset assert latest_project.aoi_geometry_input_asset.id == int(aoi_geometry_asset["id"]) + sub_options = project_data["projectTypeSpecifics"]["locate"]["customOptions"]["subOptions"] # type: ignore[index] assert latest_project.project_type_specifics == { "aoi_geometry": aoi_geometry_asset["id"], "zoom_level": 15, @@ -1771,6 +1791,27 @@ def test_project_locate(self, mock_requests): # type: ignore[reportMissingParam }, }, "sub_grid_size": SubGridSizeEnum.SIZE_2X2.value, + "export_meta": { + "key": "test1", + "value": "value1", + }, + "custom_options": [ + { + "client_id": project_data["projectTypeSpecifics"]["locate"]["customOptions"]["clientId"], # type: ignore[index] + "description": "Locate project description", + "icon": IconEnum.ADD_OUTLINE.value, + "icon_color": "#FF0000", + "title": "Locate Project Title", + "value": 1, + "sub_options": [ + { + "client_id": sub_options[0]["clientId"], # type: ignore[index] + "value": 1, + "description": "Locate sub option description", + }, + ], + }, + ], } locate_project.LocateProjectProperty.model_validate( latest_project.project_type_specifics, diff --git a/firebase b/firebase index e72734f4..2ad5a6fb 160000 --- a/firebase +++ b/firebase @@ -1 +1 @@ -Subproject commit e72734f45d9574480b1fd9847bc485a5fd3d79bf +Subproject commit 2ad5a6fb9226de3f276f79c91447a28f71a26adc diff --git a/project_types/tile_map_service/locate/project.py b/project_types/tile_map_service/locate/project.py index d9285ace..d0d63e82 100644 --- a/project_types/tile_map_service/locate/project.py +++ b/project_types/tile_map_service/locate/project.py @@ -1,10 +1,12 @@ import typing from django.db import models +from pydantic import BaseModel from pyfirebase_mapswipe import models as firebase_models from apps.project.models import Project, ProjectTypeEnum from project_types.tile_map_service.base import project as tile_map_service_project +from utils.custom_options.models import CustomOption class SubGridSizeEnum(models.TextChoices): @@ -22,8 +24,21 @@ def to_firebase(self) -> firebase_models.FBEnumSubGridSize: return firebase_models.FBEnumSubGridSize.SIZE_8X8 +class ExportMeta(BaseModel): + key: str + value: str + + def to_firebase(self) -> firebase_models.FbObjExportMeta: + return firebase_models.FbObjExportMeta( + key=self.key, + value=self.value, + ) + + class LocateProjectProperty(tile_map_service_project.TileMapServiceProjectProperty): sub_grid_size: SubGridSizeEnum + custom_options: list[CustomOption] | None = None + export_meta: ExportMeta class LocateProjectTaskGroupProperty(tile_map_service_project.TileMapServiceProjectTaskGroupProperty): ... @@ -69,9 +84,11 @@ def get_task_specifics_for_db(self, tile_x: int, tile_y: int) -> LocateProjectTa @typing.override def get_project_specifics_for_firebase(self): tsp = self.project_type_specifics.tile_server_property + custom_opts = self.project_type_specifics.custom_options return firebase_models.FbProjectLocateCreateOnlyInput( zoomLevel=self.project_type_specifics.zoom_level, subGridSize=self.project_type_specifics.sub_grid_size.to_firebase(), + exportMeta=self.project_type_specifics.export_meta.to_firebase(), tileServer=firebase_models.FbObjRasterTileServer( name=tsp.name.to_firebase(), credits=tsp.get_config()["credits"], @@ -79,4 +96,25 @@ def get_project_specifics_for_firebase(self): apiKey=tsp.get_config()["api_key"], wmtsLayerName=None, ), + customOptions=[ + firebase_models.FbObjCustomOption( + title=opt.title, + description=opt.description, + value=opt.value, + icon=str(opt.icon.label), + iconColor=opt.icon_color, + subOptions=[ + firebase_models.FbBaseObjCustomSubOption( + value=sub_opt.value, + description=sub_opt.description, + ) + for sub_opt in opt.sub_options + ] + if opt.sub_options is not None + else None, + ) + for opt in custom_opts + ] + if custom_opts is not None + else None, ) diff --git a/schema.graphql b/schema.graphql index daf1dbbf..5703e7a3 100644 --- a/schema.graphql +++ b/schema.graphql @@ -941,9 +941,21 @@ input IntRangeLookup { start: Int = null } +type LocateExportMeta { + key: String! + value: String! +} + +input LocateExportMetaInput { + key: String! + value: String! +} + input LocateProjectPropertyInput { """Numeric value as string""" aoiGeometry: String! + customOptions: [CustomOptionInput!] = null + exportMeta: LocateExportMetaInput! subGridSize: SubGridSizeEnum! tileServerProperty: ProjectRasterTileServerConfigInput! @@ -954,6 +966,8 @@ input LocateProjectPropertyInput { type LocateProjectPropertyType { """Numeric value as string""" aoiGeometry: String! + customOptions: [ProjectCustomOption!] + exportMeta: LocateExportMeta! subGridSize: SubGridSizeEnum! tileServerProperty: ProjectRasterTileServerConfig! From ea714a90d74a42df4eafd4990c9148c25cd38921 Mon Sep 17 00:00:00 2001 From: Sushil Tiwari Date: Thu, 12 Feb 2026 10:03:40 +0545 Subject: [PATCH 7/7] fixup! feat(locate): add new fields export meta and custom_options --- .../graphql/inputs/project_types/locate.py | 4 ---- .../graphql/types/project_types/locate.py | 4 ---- apps/project/tests/mutation_test.py | 12 ++++-------- firebase | 2 +- .../tile_map_service/locate/project.py | 18 ++++-------------- schema.graphql | 16 ++++------------ 6 files changed, 13 insertions(+), 43 deletions(-) diff --git a/apps/project/graphql/inputs/project_types/locate.py b/apps/project/graphql/inputs/project_types/locate.py index a35e6e1f..d95488d8 100644 --- a/apps/project/graphql/inputs/project_types/locate.py +++ b/apps/project/graphql/inputs/project_types/locate.py @@ -3,9 +3,5 @@ from project_types.tile_map_service.locate import project as locate_project -@strawberry.experimental.pydantic.input(model=locate_project.ExportMeta, all_fields=True) -class LocateExportMetaInput: ... - - @strawberry.experimental.pydantic.input(model=locate_project.LocateProjectProperty, all_fields=True) class LocateProjectPropertyInput: ... diff --git a/apps/project/graphql/types/project_types/locate.py b/apps/project/graphql/types/project_types/locate.py index bedbcd94..0cec0330 100644 --- a/apps/project/graphql/types/project_types/locate.py +++ b/apps/project/graphql/types/project_types/locate.py @@ -3,9 +3,5 @@ from project_types.tile_map_service.locate import project as locate_project -@strawberry.experimental.pydantic.type(model=locate_project.ExportMeta, all_fields=True) -class LocateExportMeta: ... - - @strawberry.experimental.pydantic.type(model=locate_project.LocateProjectProperty, all_fields=True) class LocateProjectPropertyType: ... diff --git a/apps/project/tests/mutation_test.py b/apps/project/tests/mutation_test.py index b81c3dde..acd0ed05 100644 --- a/apps/project/tests/mutation_test.py +++ b/apps/project/tests/mutation_test.py @@ -1747,10 +1747,8 @@ def test_project_locate(self, mock_requests): # type: ignore[reportMissingParam }, }, "subGridSize": self.genum(SubGridSizeEnum.SIZE_2X2), - "exportMeta": { - "key": "test1", - "value": "value1", - }, + "exportMetaKey": "test1", + "exportMetaValue": "value1", "customOptions": { "clientId": str(ULID()), "description": "Locate project description", @@ -1791,10 +1789,8 @@ def test_project_locate(self, mock_requests): # type: ignore[reportMissingParam }, }, "sub_grid_size": SubGridSizeEnum.SIZE_2X2.value, - "export_meta": { - "key": "test1", - "value": "value1", - }, + "export_meta_key": "test1", + "export_meta_value": "value1", "custom_options": [ { "client_id": project_data["projectTypeSpecifics"]["locate"]["customOptions"]["clientId"], # type: ignore[index] diff --git a/firebase b/firebase index 2ad5a6fb..c91e107b 160000 --- a/firebase +++ b/firebase @@ -1 +1 @@ -Subproject commit 2ad5a6fb9226de3f276f79c91447a28f71a26adc +Subproject commit c91e107b8b0f4b5f559ec74db97c2fff921c3e12 diff --git a/project_types/tile_map_service/locate/project.py b/project_types/tile_map_service/locate/project.py index d0d63e82..fdae12b7 100644 --- a/project_types/tile_map_service/locate/project.py +++ b/project_types/tile_map_service/locate/project.py @@ -1,7 +1,6 @@ import typing from django.db import models -from pydantic import BaseModel from pyfirebase_mapswipe import models as firebase_models from apps.project.models import Project, ProjectTypeEnum @@ -24,21 +23,11 @@ def to_firebase(self) -> firebase_models.FBEnumSubGridSize: return firebase_models.FBEnumSubGridSize.SIZE_8X8 -class ExportMeta(BaseModel): - key: str - value: str - - def to_firebase(self) -> firebase_models.FbObjExportMeta: - return firebase_models.FbObjExportMeta( - key=self.key, - value=self.value, - ) - - class LocateProjectProperty(tile_map_service_project.TileMapServiceProjectProperty): sub_grid_size: SubGridSizeEnum custom_options: list[CustomOption] | None = None - export_meta: ExportMeta + export_meta_key: str + export_meta_value: str class LocateProjectTaskGroupProperty(tile_map_service_project.TileMapServiceProjectTaskGroupProperty): ... @@ -88,7 +77,8 @@ def get_project_specifics_for_firebase(self): return firebase_models.FbProjectLocateCreateOnlyInput( zoomLevel=self.project_type_specifics.zoom_level, subGridSize=self.project_type_specifics.sub_grid_size.to_firebase(), - exportMeta=self.project_type_specifics.export_meta.to_firebase(), + exportMetaKey=self.project_type_specifics.export_meta_key, + exportMetaValue=self.project_type_specifics.export_meta_value, tileServer=firebase_models.FbObjRasterTileServer( name=tsp.name.to_firebase(), credits=tsp.get_config()["credits"], diff --git a/schema.graphql b/schema.graphql index 5703e7a3..00c775b2 100644 --- a/schema.graphql +++ b/schema.graphql @@ -941,21 +941,12 @@ input IntRangeLookup { start: Int = null } -type LocateExportMeta { - key: String! - value: String! -} - -input LocateExportMetaInput { - key: String! - value: String! -} - input LocateProjectPropertyInput { """Numeric value as string""" aoiGeometry: String! customOptions: [CustomOptionInput!] = null - exportMeta: LocateExportMetaInput! + exportMetaKey: String! + exportMetaValue: String! subGridSize: SubGridSizeEnum! tileServerProperty: ProjectRasterTileServerConfigInput! @@ -967,7 +958,8 @@ type LocateProjectPropertyType { """Numeric value as string""" aoiGeometry: String! customOptions: [ProjectCustomOption!] - exportMeta: LocateExportMeta! + exportMetaKey: String! + exportMetaValue: String! subGridSize: SubGridSizeEnum! tileServerProperty: ProjectRasterTileServerConfig!