diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e24dfa63..06a66c74 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,9 +59,11 @@ repos: - id: ruff types_or: [python, pyi, jupyter, toml] args: [--fix, --exit-non-zero-on-fix] + exclude: ^.*uv\.lock$ # Formatter - id: ruff-format types_or: [python, pyi, jupyter, toml] + exclude: ^.*uv\.lock$ - repo: https://github.com/RobertCraigie/pyright-python rev: v1.1.398 diff --git a/apps/project/custom_options.py b/apps/project/custom_options.py index 0b8d00e8..c35528df 100644 --- a/apps/project/custom_options.py +++ b/apps/project/custom_options.py @@ -107,14 +107,14 @@ class CustomOptionDefaults: "icon": IconEnum.CLOSE_OUTLINE, "value": 0, "description": "the shape does not outline any feature in the image", - "icon_color": "#D32F2F", + "icon_color": "transparent", }, { "title": "Multiple Features", "icon": IconEnum.CHECKMARK_OUTLINE, "value": 2, "description": "the shape outlines multiple features in the image", - "icon_color": "#388E3C", + "icon_color": "#ffff00", }, ] diff --git a/apps/tutorial/factories.py b/apps/tutorial/factories.py index a7fa9ee8..6a770409 100644 --- a/apps/tutorial/factories.py +++ b/apps/tutorial/factories.py @@ -46,6 +46,7 @@ class Meta: # type: ignore[reportIncompatibleVariableOverride] client_id = factory.LazyFunction(lambda: str(ULID())) reference = 1 + task_partition_index = factory.Sequence(lambda n: n) project_type_specifics = factory.LazyAttribute(lambda _: {}) diff --git a/apps/tutorial/graphql/inputs/inputs.py b/apps/tutorial/graphql/inputs/inputs.py index 2d0c9b39..a5d041ec 100644 --- a/apps/tutorial/graphql/inputs/inputs.py +++ b/apps/tutorial/graphql/inputs/inputs.py @@ -23,6 +23,7 @@ from .project_types.compare import CompareTutorialTaskPropertyInput from .project_types.completeness import CompletenessTutorialTaskPropertyInput from .project_types.find import FindTutorialTaskPropertyInput +from .project_types.locate import LocateTutorialTaskPropertyInput from .project_types.street import StreetTutorialTaskPropertyInput from .project_types.validate import ValidateTutorialTaskPropertyInput from .project_types.validate_image import ValidateImageTutorialTaskPropertyInput @@ -36,12 +37,14 @@ class TutorialTaskProjectTypeSpecificInput: validate_image: ValidateImageTutorialTaskPropertyInput | None = strawberry.UNSET completeness: CompletenessTutorialTaskPropertyInput | None = strawberry.UNSET street: StreetTutorialTaskPropertyInput | None = strawberry.UNSET + locate: LocateTutorialTaskPropertyInput | None = strawberry.UNSET @strawberry_django.input(TutorialTask) class TutorialTaskCreateInput(UserResourceCreateInputMixin): # NOTE: scenario_id will be referenced from parent reference: strawberry.auto + task_partition_index: strawberry.auto project_type_specifics: TutorialTaskProjectTypeSpecificInput @@ -49,6 +52,7 @@ class TutorialTaskCreateInput(UserResourceCreateInputMixin): class TutorialTaskUpdateInput(UserResourceUpdateInputMixin): # NOTE: scenario_id will be referenced from parent reference: strawberry.auto + task_partition_index: strawberry.auto project_type_specifics: TutorialTaskProjectTypeSpecificInput diff --git a/apps/tutorial/graphql/types/types.py b/apps/tutorial/graphql/types/types.py index 4427594c..06de1152 100644 --- a/apps/tutorial/graphql/types/types.py +++ b/apps/tutorial/graphql/types/types.py @@ -50,6 +50,7 @@ class TutorialTaskType(UserResourceTypeMixin): id: strawberry.ID scenario_id: strawberry.ID reference: strawberry.auto + task_partition_index: strawberry.auto @strawberry_django.field( only=["project_type_specifics"], diff --git a/apps/tutorial/migrations/0003_tutorialtask_task_partition_index.py b/apps/tutorial/migrations/0003_tutorialtask_task_partition_index.py new file mode 100644 index 00000000..2c3642a8 --- /dev/null +++ b/apps/tutorial/migrations/0003_tutorialtask_task_partition_index.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.5 on 2026-02-26 10:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tutorial', '0002_initial'), + ] + + operations = [ + migrations.AddField( + model_name='tutorialtask', + name='task_partition_index', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + ] diff --git a/apps/tutorial/models.py b/apps/tutorial/models.py index 354d4297..1380978f 100644 --- a/apps/tutorial/models.py +++ b/apps/tutorial/models.py @@ -218,6 +218,8 @@ class TutorialTask(UserResource): reference = models.PositiveSmallIntegerField[int, int]() + task_partition_index = models.PositiveSmallIntegerField[int, int](null=True, blank=True) + project_type_specifics = models.JSONField() # Type hints diff --git a/apps/tutorial/serializers.py b/apps/tutorial/serializers.py index 5a095670..402cd519 100644 --- a/apps/tutorial/serializers.py +++ b/apps/tutorial/serializers.py @@ -41,6 +41,7 @@ class Meta: # type: ignore[reportIncompatibleVariableOverride] fields = ( "id", "reference", + "task_partition_index", "project_type_specifics", ) @@ -94,9 +95,52 @@ def _validate_project_type_specifics(self, attrs: dict[str, typing.Any]): attrs["project_type_specifics"] = project_type_specifics + def _validate_task_partition_index(self, attrs: dict[str, typing.Any]) -> None: + tutorial = self.context.get("tutorial") + if tutorial is None: + return + + scenario = self.context.get("scenario") + task_partition_index = attrs.get( + "task_partition_index", + getattr(self.instance, "task_partition_index", None), + ) + + # NOTE: task_partition_index can be None, but if provided + # it should not be the same as any other task in the same scenario. + if task_partition_index is None: + return + + project_type = tutorial.project.project_type + if project_type is None: + raise serializers.ValidationError( + { + "project_type": gettext("Project type is required."), + }, + ) + + # Only enforce uniqueness for LOCATE projects + if project_type != ProjectTypeEnum.LOCATE: + return + + task_qs = TutorialTask.objects.filter(scenario=scenario) + + if self.instance is not None: + task_qs = task_qs.exclude(pk=self.instance.pk) + + if task_qs.filter(task_partition_index=task_partition_index).exists(): + raise serializers.ValidationError( + { + "task_partition_index": gettext( + "Task partition index should be unique among tasks of the same scenario.", + ), + }, + ) + @typing.override def validate(self, attrs: dict[str, typing.Any]): self._validate_project_type_specifics(attrs) + self._validate_task_partition_index(attrs) return super().validate(attrs) @typing.override diff --git a/apps/tutorial/tests/mutation_test.py b/apps/tutorial/tests/mutation_test.py index 78e93662..66406570 100644 --- a/apps/tutorial/tests/mutation_test.py +++ b/apps/tutorial/tests/mutation_test.py @@ -5,6 +5,7 @@ from django.core.files.temp import NamedTemporaryFile from PIL import Image +from apps.common.models import IconEnum from apps.project.factories import OrganizationFactory, ProjectFactory from apps.project.models import ( ProjectTypeEnum, @@ -23,6 +24,7 @@ from apps.user.factories import UserFactory from main.config import Config from main.tests import TestCase +from project_types.tile_map_service.locate.project import SubGridSizeEnum from utils.common import format_object_keys, to_camel_case from utils.geo.raster_tile_server.config import RasterTileServerNameEnum @@ -124,6 +126,11 @@ class Mutation: tileY tileZ } + ... on LocateTutorialTaskPropertyType { + tileX + tileY + tileZ + } ... on CompletenessTutorialTaskPropertyType { tileX tileY @@ -188,6 +195,7 @@ class Mutation: id clientId reference + taskPartitionIndex projectTypeSpecifics { ... on CompareTutorialTaskPropertyType { tileX @@ -199,6 +207,11 @@ class Mutation: tileY tileZ } + ... on LocateTutorialTaskPropertyType { + tileX + tileY + tileZ + } ... on CompletenessTutorialTaskPropertyType { tileX tileY @@ -721,6 +734,7 @@ def get_update_for_task(tut: dict): # type: ignore[reportMissingTypeArgument] "id": self.gID(y.pk), "clientId": y.client_id, "reference": y.reference, + "taskPartitionIndex": y.task_partition_index, "projectTypeSpecifics": format_object_keys(y.project_type_specifics, to_camel_case), } for y in x.tasks.all() @@ -846,3 +860,342 @@ def test_tutorial_state_transitions(self): assert "Tutorial status cannot be changed" in resp_data["errors"][0]["messages"], response tutorial.refresh_from_db() assert tutorial.status == old_status + + def test_create_locate_tutorial(self): + project_type_specifics = { + "zoom_level": 15, + "aoi_geometry": "1", + "sub_grid_size": SubGridSizeEnum.SIZE_2X2.value, + "export_meta_key": "test1", + "export_meta_value": "value1", + "tile_server_property": { + "name": RasterTileServerNameEnum.CUSTOM.value, + "custom": { + "credits": "My Map", + "url": "https://hi-there/{x}/{y}/{z}", + }, + }, + } + locate_project = ProjectFactory.create( + **self.user_resource_kwargs, + project_type=ProjectTypeEnum.LOCATE, + topic="Locate Project", + region="Locate Region", + project_number=2, + requesting_organization=self.organization, + look_for="", + additional_info_url="https://hi-there/locate-about.html", + description="The new **locate project** from hi-there.", + project_type_specifics=project_type_specifics, + ) + + tutorial_data = { + "clientId": "01K748SHD9W5BTQA8EB9RCZGB8", + "name": "Locate Tutorial", + "project": locate_project.pk, + } + + # Creating Locate Tutorial: With Authentication + self.force_login(self.user) + content = self._create_tutorial_mutation(tutorial_data) + resp_data = content["data"]["createTutorial"] + assert resp_data["errors"] is None, content + + latest_tutorial = Tutorial.objects.get(pk=resp_data["result"]["id"]) + assert latest_tutorial.status == TutorialStatusEnum.DRAFT + + # Create page block image + + # Creating Project Image Asset + tutorial_asset_data = { + "tutorial": latest_tutorial.pk, + "clientId": "01K748TDZPSDKPX1QVC5J5H558", + } + content = self._create_tutorial_image_asset(tutorial_asset_data) + resp_data = content["data"]["createTutorialAsset"] + assert resp_data["errors"] is None, content + image_asset = resp_data["result"] + + # Update Tutorial + tutorial_data.pop("project") + + tutorial_data = { + **tutorial_data, + "scenarios": [ + { + "create": { + "clientId": "01K748TDZPADVS0XX5FV59Q8JK", + "scenarioPageNumber": 1, + "instructionsDescription": "Anything that is not naturally occurring", + "instructionsIcon": self.genum(IconEnum.STAR_OUTLINE), + "instructionsTitle": "Identify man-made structures", + "hintDescription": "They have sharp boundaries", + "hintIcon": self.genum(IconEnum.INFORMATION_OUTLINE), + "hintTitle": "Look closer!", + "successDescription": "You identified all man-made structures", + "successIcon": self.genum(IconEnum.CHECK), + "successTitle": "Well done!", + "tasks": [ + { + "clientId": "01K748TDZQAX1242QFT3R8V3H1", + "projectTypeSpecifics": {"locate": {"tileZ": 18, "tileX": 193196, "tileY": 110087}}, + "reference": 0, + "taskPartitionIndex": 0, + }, + { + "clientId": "01K748TDZQDAHN2RAEJ6KKQZBS", + "projectTypeSpecifics": {"locate": {"tileZ": 18, "tileX": 193196, "tileY": 110088}}, + "reference": 1, + "taskPartitionIndex": 1, + }, + { + "clientId": "01K748TDZQN9KR7PPAWY282B8V", + "projectTypeSpecifics": {"locate": {"tileZ": 18, "tileX": 193196, "tileY": 110089}}, + "reference": 0, + "taskPartitionIndex": 2, + }, + { + "clientId": "01K748TDZQCYF1Q88MVW3B5J8A", + "projectTypeSpecifics": {"locate": {"tileZ": 18, "tileX": 193197, "tileY": 110087}}, + "reference": 0, + "taskPartitionIndex": 3, + }, + { + "clientId": "01K748TDZR861B76GJAYS3WCY6", + "projectTypeSpecifics": {"locate": {"tileZ": 18, "tileX": 193197, "tileY": 110088}}, + "reference": 1, + "taskPartitionIndex": 4, + }, + { + "clientId": "01K748TDZRT35XEZFT0SRR1EF0", + "projectTypeSpecifics": {"locate": {"tileZ": 18, "tileX": 193197, "tileY": 110089}}, + "reference": 0, + "taskPartitionIndex": 5, + }, + ], + }, + }, + ], + "informationPages": [ + { + "create": { + "clientId": "01K748TDZTPW7Y19MEMPH0332S", + "title": "Man-made structures", + "pageNumber": 1, + "blocks": [ + { + "clientId": "01K748TDZTPVP7HF5VX03HTW9Z", + "blockNumber": 1, + "blockType": "TEXT", + "text": "Man-made structures are physical constructions created by humans, typically " + "using tools, materials, and engineering principles.", + }, + { + "clientId": "01K748TDZT5T9D33D78VV5NFKS", + "blockNumber": 2, + "blockType": "TEXT", + "text": "These structures are built to serve specific purposes, such as housing, " + "transportation, defense, communication, or recreation.", + }, + { + "clientId": "01K748TDZT7PV91ZX9W5H4BS47", + "blockNumber": 3, + "blockType": "IMAGE", + "image": image_asset["id"], + }, + ], + }, + }, + { + "create": { + "clientId": "01K748TDZTZ2M1FJWEKHGDVSV9", + "title": "Natural structures", + "pageNumber": 2, + "blocks": [ + { + "clientId": "01K748TDZTS0MK857GPZHKYDWW", + "blockNumber": 1, + "blockType": "TEXT", + "text": "Natural structures are physical formations that are created by nature " + "without human intervention", + }, + ], + }, + }, + ], + } + + # Updating Tutorial: With Authentication and correct data + content = self._update_tutorial_mutation(str(latest_tutorial.pk), tutorial_data) + resp_data = content["data"]["updateTutorial"] + assert resp_data["errors"] is None, content + + latest_tutorial.refresh_from_db() + + # Update tutorial status to publish + status_data = { + "clientId": latest_tutorial.client_id, + "status": self.genum(TutorialStatusEnum.READY_TO_PUBLISH), + } + self._update_tutorial_status_mutation(latest_tutorial.pk, status_data) + latest_tutorial.refresh_from_db() + + # Updating Tutorial: Nested Updates + def get_update_for_task(tut: dict[str, typing.Any]): + return {"update": {**tut, "projectTypeSpecifics": {"locate": tut.get("projectTypeSpecifics")}}} + + tutorial_from_res = resp_data["result"] + tutorial_from_res.pop("projectId") + tutorial_from_res.pop("id") + tutorial_from_res.pop("status") + + tutorial_data = { + **tutorial_from_res, + "scenarios": [ + { + "update": { + **tutorial_from_res["scenarios"][0], + "tasks": [ + get_update_for_task(tutorial_from_res["scenarios"][0]["tasks"][0]), + get_update_for_task(tutorial_from_res["scenarios"][0]["tasks"][1]), + get_update_for_task(tutorial_from_res["scenarios"][0]["tasks"][2]), + get_update_for_task(tutorial_from_res["scenarios"][0]["tasks"][3]), + get_update_for_task(tutorial_from_res["scenarios"][0]["tasks"][4]), + get_update_for_task(tutorial_from_res["scenarios"][0]["tasks"][5]), + ], + }, + }, + ], + "informationPages": [ + { + "update": { + **tutorial_from_res["informationPages"][0], + "blocks": [ + { + "update": { + **tutorial_from_res["informationPages"][0]["blocks"][0], + }, + }, + { + "delete": { + "id": tutorial_from_res["informationPages"][0]["blocks"][1]["id"], + }, + }, + { + "create": { + "clientId": "01K748TDZTRDF6Z3X2VTNKJEGF", + # NOTE: blockNumber 2 is previously deleted so should not error on unique constraint + "blockNumber": 2, + "blockType": "TEXT", + "text": "These structures are built to serve various purposes", + }, + }, + ], + }, + }, + { + "update": { + **tutorial_from_res["informationPages"][1], + "blocks": [ + { + "update": { + **tutorial_from_res["informationPages"][1]["blocks"][0], + }, + }, + ], + }, + }, + ], + } + + # Updating Tutorial: With Authentication and correct data + content = self._update_tutorial_mutation(str(latest_tutorial.pk), tutorial_data) + resp_data = content["data"]["updateTutorial"] + assert resp_data["errors"] is None, content + latest_tutorial.refresh_from_db() + + assert resp_data == self.g_mutation_response( + ok=True, + result=dict( + clientId=latest_tutorial.client_id, + id=self.gID(latest_tutorial.pk), + status=self.genum(TutorialStatusEnum.PUBLISHED), + projectId=self.gID(latest_tutorial.project_id), + scenarios=[ + { + "id": self.gID(x.pk), + "clientId": x.client_id, + "scenarioPageNumber": x.scenario_page_number, + "hintDescription": x.hint_description, + "hintIcon": self.genum(x.hint_icon), # type: ignore[reportArgumentType] + "hintTitle": x.hint_title, + "instructionsDescription": x.instructions_description, + "instructionsIcon": self.genum(x.instructions_icon), # type: ignore[reportArgumentType] + "instructionsTitle": x.instructions_title, + "successDescription": x.success_description, + "successIcon": self.genum(x.success_icon), # type: ignore[reportArgumentType] + "successTitle": x.success_title, + "tasks": [ + { + "id": self.gID(y.pk), + "clientId": y.client_id, + "reference": y.reference, + "taskPartitionIndex": y.task_partition_index, + "projectTypeSpecifics": format_object_keys(y.project_type_specifics, to_camel_case), + } + for y in x.tasks.all() + ], + } + for x in latest_tutorial.scenarios.all() + ], + informationPages=[ + { + "id": self.gID(x.pk), + "clientId": x.client_id, + "pageNumber": x.page_number, + "title": x.title, + "blocks": [ + { + "id": self.gID(y.pk), + "clientId": y.client_id, + "blockNumber": y.block_number, + "blockType": self.genum(y.block_type), # type: ignore[reportArgumentType] + "text": y.text, + } + for y in x.blocks.all() + ], + } + for x in latest_tutorial.information_pages.all() + ], + ), + ), content + + # Publishing tutorial: + data = { + "clientId": latest_tutorial.client_id, + "status": self.genum(TutorialStatusEnum.PUBLISHED), + } + response = self._update_tutorial_status_mutation(str(latest_tutorial.pk), data) + resp_data = response["data"]["updateTutorialStatus"] + assert resp_data["errors"] is None, response + + # Tutorial data + tutorial_ref = self.firebase_helper.ref( + Config.FirebaseKeys.tutorial(latest_tutorial.firebase_id), + ) + fb_tutorial: typing.Any = tutorial_ref.get() + assert fb_tutorial is not None + + # Tutorial group data + tutorial_group_ref = self.firebase_helper.ref( + Config.FirebaseKeys.tutorial_groups(latest_tutorial.firebase_id), + ) + fb_tutorial_group: typing.Any = tutorial_group_ref.get() + assert fb_tutorial_group is not None + + # Tutorial Task data + tutorial_task_ref = self.firebase_helper.ref( + Config.FirebaseKeys.tutorial_tasks(latest_tutorial.firebase_id), + ) + fb_tutorial_task: typing.Any = tutorial_task_ref.get() + assert fb_tutorial_task is not None diff --git a/firebase b/firebase index 3d079b60..87e6e8bc 160000 --- a/firebase +++ b/firebase @@ -1 +1 @@ -Subproject commit 3d079b609ee5b1814519d54f51985c5278f77ff5 +Subproject commit 87e6e8bc88fed9fef031e6c8e298002633fb4413 diff --git a/project_types/tile_map_service/base/tutorial.py b/project_types/tile_map_service/base/tutorial.py index 1b49d603..2302b3d6 100644 --- a/project_types/tile_map_service/base/tutorial.py +++ b/project_types/tile_map_service/base/tutorial.py @@ -64,6 +64,7 @@ def get_task_specifics_for_firebase( groupId=self.get_tutorial_group_key(), projectId=self.tutorial.firebase_id, referenceAnswer=task.reference, + taskPartitionIndex=task.task_partition_index, screen=screen, taskId_real=f"{task_specifics.tile_z}-{task_specifics.tile_x}-{task_specifics.tile_y}", taskX=task_x, diff --git a/project_types/tile_map_service/locate/tutorial.py b/project_types/tile_map_service/locate/tutorial.py index 184b3178..4382bedd 100644 --- a/project_types/tile_map_service/locate/tutorial.py +++ b/project_types/tile_map_service/locate/tutorial.py @@ -1,4 +1,10 @@ -from apps.tutorial.models import Tutorial +import typing + +from pyfirebase_mapswipe import extended_models as firebase_ext_models +from pyfirebase_mapswipe import models as firebase_models + +from apps.project.models import ProjectTypeEnum +from apps.tutorial.models import Tutorial, TutorialTask from project_types.tile_map_service.base import tutorial as tile_map_service_tutorial from .project import LocateProjectProperty @@ -7,9 +13,6 @@ class LocateTutorialTaskProperty(tile_map_service_tutorial.TileMapServiceTutorialTaskProperty): ... -# TODO(susilnem): Handle tutorial for locate project type - - class LocateTutorial( tile_map_service_tutorial.TileMapServiceBaseTutorial[ LocateProjectProperty, @@ -21,3 +24,49 @@ class LocateTutorial( def __init__(self, tutorial: Tutorial): super().__init__(tutorial) + + @typing.override + def get_task_specifics_for_firebase(self, task: TutorialTask, index: int, screen: int): + tsp = self.project_type_specifics.tile_server_property + + task_specifics = self.tutorial_task_property_class.model_validate(task.project_type_specifics) + + resp = super().get_task_specifics_for_firebase(task, index, screen) + + return firebase_ext_models.FbLocateTutorialTaskComplete( + geometry=resp.geometry, + groupId=resp.groupId, + projectId=resp.projectId, + referenceAnswer=resp.referenceAnswer, + taskPartitionIndex=resp.taskPartitionIndex, + screen=resp.screen, + taskId_real=resp.taskId_real, + taskX=resp.taskX, + taskY=resp.taskY, + taskId=resp.taskId, + url=tsp.generate_url( + task_specifics.tile_x, + task_specifics.tile_y, + task_specifics.tile_z, + ), + ) + + @typing.override + def get_tutorial_specifics_for_firebase(self): + tsp = self.project_type_specifics.tile_server_property + + projectType = ProjectTypeEnum.LOCATE.value + assert projectType == 9, "Project Locate should be 9" + + return firebase_models.FbLocateTutorial( + zoomLevel=self.project_type_specifics.zoom_level, + projectType=projectType, + 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, + ), + ) diff --git a/schema.graphql b/schema.graphql index 00c775b2..f90109ac 100644 --- a/schema.graphql +++ b/schema.graphql @@ -736,7 +736,7 @@ input CustomOptionInput { description: String! icon: IconEnum! - """Hex color string like '#fff' or '#ffffff'""" + """Hex color string like '#fff' or '#ffffff' or 'transparent'""" iconColor: String! subOptions: [CustomSubOptionInput!] title: String! @@ -967,6 +967,12 @@ type LocateProjectPropertyType { zoomLevel: Int! } +input LocateTutorialTaskPropertyInput { + tileX: Int! + tileY: Int! + tileZ: Int! +} + type LocateTutorialTaskPropertyType { tileX: Int! tileY: Int! @@ -1469,7 +1475,7 @@ type ProjectCustomOption { description: String! icon: IconEnum! - """Hex color string like '#fff' or '#ffffff'""" + """Hex color string like '#fff' or '#ffffff' or 'transparent'""" iconColor: String! subOptions: [ProjectCustomSubOption!] title: String! @@ -1567,7 +1573,7 @@ input ProjectOverlayTileServerConfigInput { } type ProjectOverlayVectorTileServerConfig { - """Hex color string like '#fff' or '#ffffff'""" + """Hex color string like '#fff' or '#ffffff' or 'transparent'""" circleColor: String! """Float value from 0.0 to 1.0""" @@ -1576,13 +1582,13 @@ type ProjectOverlayVectorTileServerConfig { """Positive float value""" circleRadius: Float! - """Hex color string like '#fff' or '#ffffff'""" + """Hex color string like '#fff' or '#ffffff' or 'transparent'""" fillColor: String! """Float value from 0.0 to 1.0""" fillOpacity: Float! - """Hex color string like '#fff' or '#ffffff'""" + """Hex color string like '#fff' or '#ffffff' or 'transparent'""" lineColor: String! lineDasharray: [Int!]! @@ -1595,7 +1601,7 @@ type ProjectOverlayVectorTileServerConfig { } input ProjectOverlayVectorTileServerConfigInput { - """Hex color string like '#fff' or '#ffffff'""" + """Hex color string like '#fff' or '#ffffff' or 'transparent'""" circleColor: String! """Float value from 0.0 to 1.0""" @@ -1604,13 +1610,13 @@ input ProjectOverlayVectorTileServerConfigInput { """Positive float value""" circleRadius: Float! - """Hex color string like '#fff' or '#ffffff'""" + """Hex color string like '#fff' or '#ffffff' or 'transparent'""" fillColor: String! """Float value from 0.0 to 1.0""" fillOpacity: Float! - """Hex color string like '#fff' or '#ffffff'""" + """Hex color string like '#fff' or '#ffffff' or 'transparent'""" lineColor: String! lineDasharray: [Int!]! @@ -2580,6 +2586,7 @@ input TutorialTaskCreateInput { clientId: String! projectTypeSpecifics: TutorialTaskProjectTypeSpecificInput! reference: Int! + taskPartitionIndex: Int } input TutorialTaskInput { @@ -2592,6 +2599,7 @@ input TutorialTaskProjectTypeSpecificInput @oneOf { compare: CompareTutorialTaskPropertyInput completeness: CompletenessTutorialTaskPropertyInput find: FindTutorialTaskPropertyInput + locate: LocateTutorialTaskPropertyInput street: StreetTutorialTaskPropertyInput validate: ValidateTutorialTaskPropertyInput validateImage: ValidateImageTutorialTaskPropertyInput @@ -2608,6 +2616,7 @@ type TutorialTaskType implements UserResourceTypeMixin { projectTypeSpecifics: CompareTutorialTaskPropertyTypeFindTutorialTaskPropertyTypeValidateTutorialTaskPropertyTypeValidateImageTutorialTaskPropertyTypeCompletenessTutorialTaskPropertyTypeStreetTutorialTaskPropertyTypeLocateTutorialTaskPropertyType reference: Int! scenarioId: ID! + taskPartitionIndex: Int } """Model representing a individual task in the scenario.""" @@ -2616,6 +2625,7 @@ input TutorialTaskUpdateInput { id: ID! projectTypeSpecifics: TutorialTaskProjectTypeSpecificInput! reference: Int + taskPartitionIndex: Int } """ diff --git a/utils/fields.py b/utils/fields.py index a25a5384..74a527c5 100644 --- a/utils/fields.py +++ b/utils/fields.py @@ -87,8 +87,8 @@ def _validate_vector_tile_url(value: typing.Any): PydanticHexColor = typing.Annotated[ str, Field( - pattern=r"^#(?:[0-9a-fA-F]{3}){1,2}$", - description="Hex color string like '#fff' or '#ffffff'", + pattern=r"^(#(?:[0-9a-fA-F]{3}){1,2}|transparent)$", + description="Hex color string like '#fff' or '#ffffff' or 'transparent'", ), ]