diff --git a/apps/project/slack_messages.py b/apps/project/slack_messages.py index 7f99b10c..0768195e 100644 --- a/apps/project/slack_messages.py +++ b/apps/project/slack_messages.py @@ -78,7 +78,6 @@ def get_project_information_block( ) -> dict: # type: ignore[reportMissingTypeArgument] progress_bar = SlackMessage.format_progress_bar(project.progress) username = SlackMessage.format_user_link(project.created_by) - project_text = SlackMessage.format_project_link(project) tutorial_text = SlackMessage.format_tutorial_link(project.tutorial) status = SlackMessage.format_project_status(project.status_enum) project_type = SlackMessage.format_project_type(project.project_type_enum) @@ -89,7 +88,6 @@ def get_project_information_block( "text": { "type": "mrkdwn", "text": ( - f"Project: {project_text}\n" f"Tutorial: {tutorial_text}\n" f"Project Type: {project_type}\n" f"Requesting Organization: {project.requesting_organization.name}\n" @@ -122,7 +120,7 @@ def get_message_for_project_progress( website_url = Config.WebsiteKeys.project(firebase_id=project.firebase_id) if progress >= 100: - text = f"Project '{SlackMessage.format_project_name(project)}' reached 100%" + text = "Project reached 100%" blocks = [ { "type": "header", @@ -135,21 +133,20 @@ def get_message_for_project_progress( "type": "section", "text": { "type": "mrkdwn", - "text": "Congratulations! This project is now complete. Great work!", + "text": f"Congratulations! {SlackMessage.format_project_link(project)} is now complete. Great work!", }, }, { "type": "section", "text": { "type": "mrkdwn", - "text": f"You can now *finish* this <{project_url}|project> and create another one. :mapswipe:", + "text": f"You can now *finish* this <{project_url}|project> and create another one.", }, "accessory": { "type": "button", "text": { "type": "plain_text", "text": "Visit Website", - "emoji": True, }, "value": "website-link", "url": website_url, @@ -163,7 +160,7 @@ def get_message_for_project_progress( } if 90 <= progress < 100: - text = f"Project '{SlackMessage.format_project_name(project)}' reached 90%" + text = "Project reached 90%" blocks = [ { "type": "header", @@ -176,7 +173,10 @@ def get_message_for_project_progress( "type": "section", "text": { "type": "mrkdwn", - "text": "Almost there! Get your next projects ready.", + "text": ( + f"Almost there! {SlackMessage.format_project_link(project)} is nearing completion.\n\n" + "Get your next projects ready." + ), }, }, ] @@ -185,12 +185,19 @@ def get_message_for_project_progress( "blocks": blocks, } - text = f"Project '{SlackMessage.format_project_name(project)}' reached {project.progress}%" + text = f"Project reached {project.progress}%" blocks = [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": f"Project reached {project.progress}%", + }, + }, { "type": "section", "text": { - "type": "mrkdwn", + "type": "plain_text", "text": "We are getting there! One swipe at a time.", }, }, @@ -206,6 +213,7 @@ def get_message_for_project_publish( project: Project, ) -> MapswipeSlack.MapswipeSlackMessageArgumentType: website_url = Config.WebsiteKeys.project(firebase_id=project.firebase_id) + project_url = Config.ManagerDashboardUrls.project_url(project_id=project.pk) text = "Project published! :raised_hands:" blocks = [ @@ -221,20 +229,34 @@ def get_message_for_project_publish( { "type": "section", "text": { - "type": "mrkdwn", + "type": "plain_text", "text": "Happy Swiping! :slightly_smiling_face: :mapswipe:", }, - "accessory": { - "type": "button", - "text": { - "type": "plain_text", - "text": "Visit Website", - "emoji": True, + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Manage", + }, + "value": project.pk, + "url": project_url, + "action_id": "open-manager-dashboard-link", }, - "value": "website-link", - "url": website_url, - "action_id": "button-action", - }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Visit Website", + }, + "value": project.firebase_id, + "url": website_url, + "action_id": "open-website-link", + }, + ], }, ] return { diff --git a/assets b/assets index 210d8f66..f05f8de3 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 210d8f66670ac6fbc44b5b133beee26db9729784 +Subproject commit f05f8de3f2da02d594030e48836418f1cfa2c9c7 diff --git a/project_types/base/tutorial.py b/project_types/base/tutorial.py index b92ae9f6..93eebc2d 100644 --- a/project_types/base/tutorial.py +++ b/project_types/base/tutorial.py @@ -75,7 +75,7 @@ def __init_subclass__(cls, **kwargs): # type: ignore[reportMissingParameterType cls._inheritance_checks() @abstractmethod - def get_task_specifics_for_firebase(self, task: TutorialTask, index: int) -> BaseModel: ... + def get_task_specifics_for_firebase(self, task: TutorialTask, index: int, screen: int) -> BaseModel: ... @abstractmethod def get_group_specifics_for_firebase(self) -> BaseModel: ... @@ -121,10 +121,21 @@ def create_tasks_on_firebase(self, task_ref: FbReference): *self.get_task_sort_keys(["scenario__scenario_page_number"]), ) + # NOTE: We want to use the index of the scenario instead of the scenario page + scenario_index_map: dict[int, int] = {} + # FIXME(tnagorra): We only need to read scenario.pk + scenarios = self.tutorial.scenarios.order_by("scenario_page_number").all() + for scenario_index, scenario in enumerate(scenarios): + scenario_index_map[scenario.pk] = scenario_index + 1 + fb_tasks: list[dict[str, typing.Any]] = [] index = 1 for task in tasks.iterator(): - task_tutorial_specific_data = self.get_task_specifics_for_firebase(task, index) + task_tutorial_specific_data = self.get_task_specifics_for_firebase( + task, + index, + scenario_index_map[task.scenario.pk], + ) fb_tasks.append(firebase_utils.serialize(task_tutorial_specific_data)) index += 1 @@ -177,8 +188,8 @@ def create_tutorial_on_firebase(self, tutorial_ref: FbReference): self.create_tasks_on_firebase(task_ref) self.create_groups_on_firebase(group_ref) - scenarios = self.tutorial.scenarios.all() - informationPages = self.tutorial.information_pages.all() + scenarios = self.tutorial.scenarios.order_by("scenario_page_number").all() + information_pages = self.tutorial.information_pages.order_by("page_number").all() tutorial_data = firebase_models.FbBaseTutorial( exampleImage1=None, @@ -186,19 +197,19 @@ def create_tutorial_on_firebase(self, tutorial_ref: FbReference): contributorCount=0, informationPages=[ firebase_models.FbInformationPage( - title=informationPage.title, - pageNumber=informationPage.page_number, + title=info_page.title, + pageNumber=info_page_index + 1, blocks=[ firebase_models.FbInformationPageBlock( - blockNumber=block.block_number, + blockNumber=block_index + 1, blockType=TutorialInformationPageBlockTypeEnum(block.block_type).to_firebase(), textDescription=block.text, image=get_absolute_uri(block.image.file if block.image else None), ) - for block in informationPage.blocks.all() + for block_index, block in enumerate(info_page.blocks.order_by("block_number").all()) ], ) - for informationPage in informationPages + for info_page_index, info_page in enumerate(information_pages) ], lookFor=self.tutorial.project.look_for, name=self.tutorial.name, @@ -258,8 +269,8 @@ def update_tutorial_on_firebase(self, tutorial_ref: FbReference, fb_tutorial: fi self.create_tasks_on_firebase(task_ref) self.create_groups_on_firebase(group_ref) - scenarios = self.tutorial.scenarios.all() - informationPages = self.tutorial.information_pages.all() + scenarios = self.tutorial.scenarios.order_by("scenario_page_number").all() + information_pages = self.tutorial.information_pages.order_by("page_number").all() tutorial_data = firebase_models.FbBaseTutorial( exampleImage1=None, @@ -267,19 +278,19 @@ def update_tutorial_on_firebase(self, tutorial_ref: FbReference, fb_tutorial: fi contributorCount=0, informationPages=[ firebase_models.FbInformationPage( - title=informationPage.title, - pageNumber=informationPage.page_number, + title=info_page.title, + pageNumber=info_page_index + 1, blocks=[ firebase_models.FbInformationPageBlock( - blockNumber=block.block_number, + blockNumber=block_index + 1, blockType=TutorialInformationPageBlockTypeEnum(block.block_type).to_firebase(), textDescription=block.text, image=get_absolute_uri(block.image.file if block.image else None), ) - for block in informationPage.blocks.all() + for block_index, block in enumerate(info_page.blocks.order_by("block_number").all()) ], ) - for informationPage in informationPages + for info_page_index, info_page in enumerate(information_pages) ], lookFor=self.tutorial.project.look_for, name=self.tutorial.name, diff --git a/project_types/street/tutorial.py b/project_types/street/tutorial.py index 231f26f9..cb72a87e 100644 --- a/project_types/street/tutorial.py +++ b/project_types/street/tutorial.py @@ -34,7 +34,7 @@ def compress_tasks_on_firebase(self) -> bool: return True @typing.override - def get_task_specifics_for_firebase(self, task: TutorialTask, index: int): + def get_task_specifics_for_firebase(self, task: TutorialTask, index: int, screen: int): task_specifics = self.tutorial_task_property_class.model_validate(task.project_type_specifics) return firebase_models.FbStreetTutorialTask( projectId=self.tutorial.firebase_id, @@ -42,7 +42,7 @@ def get_task_specifics_for_firebase(self, task: TutorialTask, index: int): taskId=task_specifics.mapillary_image_id, geometry="", referenceAnswer=task.reference, - screen=task.scenario.scenario_page_number, + screen=screen, ) @typing.override diff --git a/project_types/tile_map_service/base/tutorial.py b/project_types/tile_map_service/base/tutorial.py index 2e3a99db..1b49d603 100644 --- a/project_types/tile_map_service/base/tutorial.py +++ b/project_types/tile_map_service/base/tutorial.py @@ -39,14 +39,13 @@ def get_task_specifics_for_firebase( self, task: TutorialTask, index: int, + screen: int, ) -> firebase_models.FbTileMapServiceTutorialTask: task_specifics = self.tutorial_task_property_class.model_validate(task.project_type_specifics) - # FIXME(tnagorra): Add validation that scenario_page_number should start from 1 - i = index % 6 - task_x = 100 + (2 * task.scenario.scenario_page_number - 1) + task_x = 100 + (2 * screen - 1) if i < 3: task_x += 0 else: @@ -65,7 +64,7 @@ def get_task_specifics_for_firebase( groupId=self.get_tutorial_group_key(), projectId=self.tutorial.firebase_id, referenceAnswer=task.reference, - screen=task.scenario.scenario_page_number, + screen=screen, taskId_real=f"{task_specifics.tile_z}-{task_specifics.tile_x}-{task_specifics.tile_y}", taskX=task_x, taskY=task_y, diff --git a/project_types/tile_map_service/compare/tutorial.py b/project_types/tile_map_service/compare/tutorial.py index 182be016..768e84be 100644 --- a/project_types/tile_map_service/compare/tutorial.py +++ b/project_types/tile_map_service/compare/tutorial.py @@ -26,13 +26,13 @@ def __init__(self, tutorial: Tutorial): super().__init__(tutorial) @typing.override - def get_task_specifics_for_firebase(self, task: TutorialTask, index: int): + def get_task_specifics_for_firebase(self, task: TutorialTask, index: int, screen: int): tsp = self.project_type_specifics.tile_server_property tsp_b = self.project_type_specifics.tile_server_b_property task_specifics = self.tutorial_task_property_class.model_validate(task.project_type_specifics) - resp = super().get_task_specifics_for_firebase(task, index) + resp = super().get_task_specifics_for_firebase(task, index, screen) return firebase_ext_models.FbCompareTutorialTaskComplete( geometry=resp.geometry, diff --git a/project_types/tile_map_service/completeness/tutorial.py b/project_types/tile_map_service/completeness/tutorial.py index 56f0297b..f10f5e9b 100644 --- a/project_types/tile_map_service/completeness/tutorial.py +++ b/project_types/tile_map_service/completeness/tutorial.py @@ -26,13 +26,13 @@ def __init__(self, tutorial: Tutorial): super().__init__(tutorial) @typing.override - def get_task_specifics_for_firebase(self, task: TutorialTask, index: int): + def get_task_specifics_for_firebase(self, task: TutorialTask, index: int, screen: int): tsp = self.project_type_specifics.tile_server_property tsp_overlay = self.project_type_specifics.overlay_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) + resp = super().get_task_specifics_for_firebase(task, index, screen) return firebase_ext_models.FbCompletenessTutorialTaskComplete( geometry=resp.geometry, diff --git a/project_types/tile_map_service/find/tutorial.py b/project_types/tile_map_service/find/tutorial.py index eb61ef98..857cb766 100644 --- a/project_types/tile_map_service/find/tutorial.py +++ b/project_types/tile_map_service/find/tutorial.py @@ -26,12 +26,12 @@ def __init__(self, tutorial: Tutorial): super().__init__(tutorial) @typing.override - def get_task_specifics_for_firebase(self, task: TutorialTask, index: int): + 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) + resp = super().get_task_specifics_for_firebase(task, index, screen) return firebase_ext_models.FbFindTutorialTaskComplete( geometry=resp.geometry, diff --git a/project_types/validate/tutorial.py b/project_types/validate/tutorial.py index f17194cd..9eefb8d6 100644 --- a/project_types/validate/tutorial.py +++ b/project_types/validate/tutorial.py @@ -40,7 +40,7 @@ def compress_tasks_on_firebase(self) -> bool: return True @typing.override - def get_task_specifics_for_firebase(self, task: TutorialTask, index: int): + def get_task_specifics_for_firebase(self, task: TutorialTask, index: int, screen: int): task_specifics = self.tutorial_task_property_class.model_validate(task.project_type_specifics) geojson = json.loads(task_specifics.object_geometry) @@ -52,7 +52,7 @@ def get_task_specifics_for_firebase(self, task: TutorialTask, index: int): properties=firebase_models.FbValidateTutorialTaskProperties( id=task_specifics.identifier, reference=task.reference, - screen=task.scenario.scenario_page_number, + screen=screen, ), geometry=geometry_wkt, ) diff --git a/project_types/validate_image/tutorial.py b/project_types/validate_image/tutorial.py index b24ad03e..8d3e7923 100644 --- a/project_types/validate_image/tutorial.py +++ b/project_types/validate_image/tutorial.py @@ -37,7 +37,7 @@ def __init__(self, tutorial: Tutorial): super().__init__(tutorial) @typing.override - def get_task_specifics_for_firebase(self, task: TutorialTask, index: int): + def get_task_specifics_for_firebase(self, task: TutorialTask, index: int, screen: int): task_specifics = self.tutorial_task_property_class.model_validate(task.project_type_specifics) return firebase_models.FbValidateImageTutorialTask( @@ -45,7 +45,7 @@ def get_task_specifics_for_firebase(self, task: TutorialTask, index: int): groupId=self.get_tutorial_group_key(), projectId=self.tutorial.firebase_id, referenceAnswer=task.reference, - screen=task.scenario.scenario_page_number, + screen=screen, taskId=task_specifics.image_id or str(index), url=task_specifics.url, fileName=task_specifics.file_name, diff --git a/utils/geo/raster_tile_server/config.py b/utils/geo/raster_tile_server/config.py index 5119d376..436ec709 100644 --- a/utils/geo/raster_tile_server/config.py +++ b/utils/geo/raster_tile_server/config.py @@ -61,18 +61,24 @@ def get_config(name: RasterTileServerNameEnumWithoutCustom) -> RasterTileServerN match name: case RasterTileServerNameEnum.BING: api_key = settings.MAP_IMAGE_BING_API_KEY - url = "https://ecn.t0.tiles.virtualearth.net/tiles/a{{quad_key}}.jpeg?g=7505&mkt=en-US&token={apiKey}" + # FIXME(tnagorra): + # How do we keep the `g` value up-to-date? + # Mapswipe App has a hardcoded value of g=1 + # https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial?key={key} responds with g=15384 + g = 15384 + url = "https://ecn.t0.tiles.virtualearth.net/tiles/a{{quad_key}}.jpeg?g={g}&mkt=en-US&token={apiKey}" return { - "url": url.format(apiKey=api_key), - "raw_url": url.format(apiKey="{apiKey}"), + "url": url.format(apiKey=api_key, g=g), + "raw_url": url.format(apiKey="{apiKey}", g=g), "api_key": api_key, "credits": "© 2019 Microsoft Corporation, Earthstar Geographics SIO", - "min_zoom": 0, + "min_zoom": 1, "max_zoom": 21, } case RasterTileServerNameEnum.MAPBOX: api_key = settings.MAP_IMAGE_MAPBOX_API_KEY url = "https://d.tiles.mapbox.com/v4/mapbox.satellite/{{z}}/{{x}}/{{y}}.jpg?access_token={apiKey}" + # Documentation at https://docs.mapbox.com/data/tilesets/reference/mapbox-satellite return { "url": url.format(apiKey=api_key), "raw_url": url.format(apiKey="{apiKey}"), @@ -83,6 +89,7 @@ def get_config(name: RasterTileServerNameEnumWithoutCustom) -> RasterTileServerN } case RasterTileServerNameEnum.MAXAR_PREMIUM: api_key = settings.MAP_IMAGE_MAXAR_PREMIUM_API_KEY + # Deprecated url = ( "https://services.digitalglobe.com" "/earthservice/tmsaccess/tms/1.0.0/DigitalGlobe%3AImageryTileService@EPSG%3A3857@jpg" @@ -99,6 +106,7 @@ def get_config(name: RasterTileServerNameEnumWithoutCustom) -> RasterTileServerN } case RasterTileServerNameEnum.MAXAR_STANDARD: api_key = settings.MAP_IMAGE_MAXAR_STANDARD_API_KEY + # Deprecated url = ( "https://services.digitalglobe.com" "/earthservice/tmsaccess/tms/1.0.0/DigitalGlobe%3AImageryTileService@EPSG%3A3857@jpg" @@ -115,6 +123,7 @@ def get_config(name: RasterTileServerNameEnumWithoutCustom) -> RasterTileServerN } case RasterTileServerNameEnum.ESRI: url = "https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}" + # Documentation at https://doc.arcgis.com/en/data-appliance/latest/maps/world-imagery.htm return { "url": url, "raw_url": url, @@ -125,6 +134,7 @@ def get_config(name: RasterTileServerNameEnumWithoutCustom) -> RasterTileServerN } case RasterTileServerNameEnum.ESRI_BETA: url = "https://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}" + # Documentation at https://doc.arcgis.com/en/data-appliance/latest/maps/world-imagery.htm return { "url": url, "raw_url": url, diff --git a/utils/geo/tile_functions.py b/utils/geo/tile_functions.py index 14fdaa40..34db7d8b 100644 --- a/utils/geo/tile_functions.py +++ b/utils/geo/tile_functions.py @@ -83,13 +83,6 @@ def tile_coords_and_zoom_to_quad_key(tile_x: int, tile_y: int, zoom: int) -> str return quad_key -# FIXME(tnagorra): This is not used. -def quad_key_to_bing_url(quad_key: str, api_key: str): - """Create a tile image URL linking to a Bing tile server.""" - # FIXME(tnagorra): We should not hardcode the urls - return f"https://ecn.t0.tiles.virtualearth.net/tiles/a{quad_key}.jpeg?g=7505&mkt=en-US&token={api_key}" - - # FIXME(tnagorra): Add typings for osgeo def geometry_from_tile_coords(tile_x: float, tile_y: float, zoom: int, *, skip_flatten: bool = False) -> str: """Compute the polygon geometry of a tile map service tile."""