Skip to content

Commit 856aa08

Browse files
Merge pull request #7207 from hotosm/staging
v5.5.2 release
2 parents 4a20b5c + d062126 commit 856aa08

24 files changed

Lines changed: 453 additions & 92 deletions

File tree

.github/workflows/backend-build-deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ env:
2424

2525
jobs:
2626
image-build-and-push:
27-
uses: hotosm/gh-workflows/.github/workflows/image_build.yml@1.5.1
27+
uses: hotosm/gh-workflows/.github/workflows/image_build.yml@3.6.0
2828
with:
2929
image_name: ghcr.io/${{ github.repository }}/backend
3030
build_target: prod

.github/workflows/pr_test_frontend.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ jobs:
7474

7575
frontend-dev-image-build:
7676
name: Build dev image for testing
77-
uses: hotosm/gh-workflows/.github/workflows/image_build.yml@1.5.1
77+
uses: hotosm/gh-workflows/.github/workflows/image_build.yml@3.6.0
7878
with:
7979
image_name: ghcr.io/${{ github.repository }}/frontend
8080
build_target: debug

README.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ This is Free and Open Source Software. You are welcome to use the code and set u
2020

2121
## Product Roadmap
2222

23-
[Discussion](https://github.com/hotosm/tasking-manager/discussions/6688) | [Roadmap Kanban](https://github.com/orgs/hotosm/projects/41/views/1)
24-
2523

2624
✅ Completed: Finished, available on [production instance](https://tasks.hotosm.org)
2725

@@ -34,15 +32,45 @@ This is Free and Open Source Software. You are welcome to use the code and set u
3432
Status | Feature | Release
3533
-------|---------|---------
3634
✅ | Up-to-date OSM Statistics: Integrated with [ohsome Now](https://stats.now.ohsome.org/) for real-time data insights.| Released in [v4.6.2](https://github.com/hotosm/tasking-manager/releases/tag/v4.6.2).
35+
36+
37+
### 2024
38+
39+
Status | Feature | Release
40+
-------|---------|---------
3741
✅ | Downloadable OSM Exports: Export data directly from each project. | Available in[ v4.7.0](https://github.com/hotosm/tasking-manager/releases/tag/v4.7.0).
3842
✅ | Rapid Editor Upgrade: Enhanced mapping experience with the latest rapid editor updates.| Last updated in [v4.8.2](https://github.com/hotosm/tasking-manager/releases/tag/v4.8.2)
3943
✅ | Public-Facing Partner Pages: Create and display dedicated pages for partners running remote mapathons.| [v4.8.2](https://github.com/hotosm/tasking-manager/releases/tag/v4.8.2)
4044
✅ | Downloadable Project List View: Allow users to explore projects via a downloadable list. [View issue](https://github.com/hotosm/tasking-manager/issues/3394).| [v4.8.2](https://github.com/hotosm/tasking-manager/releases/tag/v4.8.2)
4145
✅ | MapSwipe Stats Integration: Display MapSwipe statistics on Partner Pages.|[v4.8.2](https://github.com/hotosm/tasking-manager/releases/tag/v4.8.2)
42-
✅ | iD Editor Latest Features: Integrate the newest features of the iD editor.|[v5.0.5](https://github.com/hotosm/tasking-manager/releases/tag/v5.0.5)
46+
47+
48+
### 2025
49+
50+
Status | Feature | Release
51+
-------|---------|---------
4352
✅ | FastAPI Migration: Improve performance and scalability of Tasking Manager to handle large scale validation and mapping efforts.| [v5 launch 🎉](https://github.com/hotosm/tasking-manager/releases/tag/v5.0.0)
53+
✅ | iD Editor Latest Features: Integrate the newest features of the iD editor.|[v5.0.5](https://github.com/hotosm/tasking-manager/releases/tag/v5.0.5)
4454
✅ | Super Mapper: Redefine Mapper Level Milestones | [v5.2.0](https://github.com/hotosm/tasking-manager/releases/tag/v5.2.0)
45-
🔄 | OSM Practice Projects: Enable users to engage in OSM practice projects within Tasking Manager workflow.
55+
✅ | Ability to unlink projects and subsequent team deletion | [v5.3.1](https://github.com/hotosm/tasking-manager/releases/tag/v5.3.1)
56+
✅ | User account deletion (self-service + admin initiated) | [v5.4.0](https://github.com/hotosm/tasking-manager/releases/tag/v5.4.0)
57+
58+
### 2026
59+
60+
Status | Feature | Release
61+
-------|---------|---------
62+
✅ | Markdown support in Project Q&A | [v5.4.1](https://github.com/hotosm/tasking-manager/releases/tag/v5.4.1)
63+
✅ | Improved panel arrangement in task contribution section | [v5.4.1](https://github.com/hotosm/tasking-manager/releases/tag/v5.4.1)
64+
✅ | OSM Practice Projects (sandbox): Enable users to engage in OSM practice projects within Tasking Manager workflow. |[v5.5](https://github.com/hotosm/tasking-manager/releases/tag/v5.5)
65+
✅ | Complete migration to MapLibre libraries | [v5.5](https://github.com/hotosm/tasking-manager/releases/tag/v5.5)
66+
✅ | [Digital Public Goods](https://www.digitalpublicgoods.net/registry) badge display | [v5.5](https://github.com/hotosm/tasking-manager/releases/tag/v5.5)
67+
✅ | Filter by imagery type using API | [v5.5](https://github.com/hotosm/tasking-manager/releases/tag/v5.5)
68+
✅ | Backend support for messaging all Campaign Contributors | [v5.5](https://github.com/hotosm/tasking-manager/releases/tag/v5.5)
69+
🔄 | Allow data downloads for sandbox projects through frontend |
70+
🔄 | Custom data reference layer for sandbox projects |
71+
🔄 | Choropleth layer to highlight most invalidated tasks |
72+
🔄 | Dependency & Framework health check |
73+
🔄 | Additional imagery filter under explore projects section |
4674
📅 | Expanding Project Types beyond basemap features
4775
📅 | AI Integration: task assignment, difficulty estimation, and validation
4876
📅 | External tools Integration: MapSwipe, uMap, Maproulette

backend/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ def assemble_db_connection(
280280
"OHSOME_STATS_API_URL", "https://stats.now.ohsome.org/api"
281281
)
282282
OHSOME_STATS_TOPICS: str = os.getenv("OHSOME_STATS_TOPICS", None)
283+
OSM_USER_AGENT: str = os.getenv(
284+
"OSM_USER_AGENT",
285+
"HOT-TaskingManager-API/5.0 (https://tasking-manager-production-api.hotosm.org)",
286+
)
283287

284288

285289
class TestEnvironmentConfig(Settings):
@@ -297,6 +301,10 @@ class TestEnvironmentConfig(Settings):
297301
)
298302

299303
LOG_LEVEL: str = "DEBUG"
304+
OSM_USER_AGENT: str = os.getenv(
305+
"OSM_USER_AGENT",
306+
"HOT-TaskingManager-API/5.0 (https://tasking-manager-production-api.hotosm.org)",
307+
)
300308

301309

302310
@lru_cache

backend/models/dtos/user_dto.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ class UserDTO(BaseModel):
5454
)
5555
projects_notifications: bool = Field(None, alias="projectsNotifications")
5656
tasks_notifications: bool = Field(None, alias="tasksNotifications")
57+
task_validation_notification: bool = Field(None, alias="taskValidationNotification")
58+
task_invalidation_notification: bool = Field(
59+
None, alias="taskInvalidationNotification"
60+
)
5761
tasks_comments_notifications: bool = Field(None, alias="taskCommentsNotifications")
5862
teams_announcement_notifications: bool = Field(
5963
None, alias="teamsAnnouncementNotifications"

backend/models/postgis/project_info.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import List
22

3+
from backend.models.postgis.utils import sanitize_markdown
34
from databases import Database
45
from sqlalchemy import (
56
Column,
@@ -57,11 +58,11 @@ async def create_from_dto(cls, dto: ProjectInfoDTO, project_id: int, db: Databas
5758
self.project_id = project_id
5859
self.project_id_str = str(project_id) # Allows project_id to be searched
5960

60-
# Note project info not bleached on basis that admins are trusted users and shouldn't be doing anything bad
61-
self.short_description = dto.short_description
62-
self.description = dto.description
63-
self.instructions = dto.instructions
64-
self.per_task_instructions = dto.per_task_instructions
61+
self.short_description = sanitize_markdown(dto.short_description)
62+
self.description = sanitize_markdown(dto.description)
63+
self.instructions = sanitize_markdown(dto.instructions)
64+
self.per_task_instructions = sanitize_markdown(dto.per_task_instructions)
65+
6566
columns = {
6667
c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs
6768
}
@@ -75,11 +76,11 @@ async def update_from_dto(self, dto: ProjectInfoDTO, db: Database):
7576
self.name = dto.name
7677
self.project_id_str = str(self.project_id) # Allows project_id to be searched
7778

78-
# Note project info not bleached on basis that admins are trusted users and shouldn't be doing anything bad
79-
self.short_description = dto.short_description
80-
self.description = dto.description
81-
self.instructions = dto.instructions
82-
self.per_task_instructions = dto.per_task_instructions
79+
self.short_description = sanitize_markdown(dto.short_description)
80+
self.description = sanitize_markdown(dto.description)
81+
self.instructions = sanitize_markdown(dto.instructions)
82+
self.per_task_instructions = sanitize_markdown(dto.per_task_instructions)
83+
8384
columns = {
8485
c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs
8586
}

backend/models/postgis/user.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ class User(Base):
7777
projects_comments_notifications = Column(Boolean, default=False, nullable=False)
7878
projects_notifications = Column(Boolean, default=True, nullable=False)
7979
tasks_notifications = Column(Boolean, default=True, nullable=False)
80+
task_validation_notification = Column(Boolean, default=True, nullable=False)
81+
task_invalidation_notification = Column(Boolean, default=True, nullable=False)
8082
tasks_comments_notifications = Column(Boolean, default=False, nullable=False)
8183
teams_announcement_notifications = Column(Boolean, default=True, nullable=False)
8284
date_registered = Column(DateTime, default=timestamp)
@@ -503,6 +505,8 @@ async def as_dto(self, logged_in_username: str, db: Database) -> UserDTO:
503505
user_dto.projects_notifications = self.projects_notifications
504506
user_dto.projects_comments_notifications = self.projects_comments_notifications
505507
user_dto.tasks_notifications = self.tasks_notifications
508+
user_dto.task_validation_notification = self.task_validation_notification
509+
user_dto.task_invalidation_notification = self.task_invalidation_notification
506510
user_dto.tasks_comments_notifications = self.tasks_comments_notifications
507511
user_dto.teams_announcement_notifications = (
508512
self.teams_announcement_notifications

backend/models/postgis/utils.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import datetime
22
import json
3-
import re
43
from geoalchemy2 import Geometry
54
from geoalchemy2.functions import GenericFunction
65
from loguru import logger
6+
import re
7+
from markdown import markdown
8+
import bleach
79

810

911
class NotFound(Exception):
@@ -156,6 +158,100 @@ def parse_duration(time_str):
156158
return datetime.timedelta(**time_params)
157159

158160

161+
def sanitize_markdown(text: str | None) -> str | None:
162+
"""Convert markdown to sanitized HTML. Returns None for empty input."""
163+
if not text:
164+
return None
165+
166+
# Extended allowed tags to support markdown features
167+
allowed_tags = [
168+
"a",
169+
"abbr",
170+
"acronym",
171+
"b",
172+
"blockquote",
173+
"br",
174+
"code",
175+
"em",
176+
"h1",
177+
"h2",
178+
"h3",
179+
"h4",
180+
"h5",
181+
"h6",
182+
"img",
183+
"i",
184+
"li",
185+
"ol",
186+
"p",
187+
"pre",
188+
"strong",
189+
"ul",
190+
"div",
191+
"table",
192+
"thead",
193+
"tbody",
194+
"tfoot",
195+
"tr",
196+
"td",
197+
"th",
198+
"iframe",
199+
"input",
200+
"hr",
201+
"del",
202+
"s",
203+
"strike",
204+
"span",
205+
"caption",
206+
"col",
207+
"colgroup",
208+
]
209+
210+
allowed_attributes = {
211+
"a": ["href", "rel", "target", "title"],
212+
"img": ["src", "alt", "title", "width", "height"],
213+
"iframe": [
214+
"width",
215+
"height",
216+
"src",
217+
"title",
218+
"frameborder",
219+
"allow",
220+
"referrerpolicy",
221+
"allowfullscreen",
222+
],
223+
"input": ["type", "checked", "disabled"],
224+
"th": ["align", "scope"],
225+
"td": ["align", "colspan", "rowspan"],
226+
"table": ["class"],
227+
"code": ["class"],
228+
"pre": ["class"],
229+
}
230+
231+
# Support markdown ~~strike~~ -> <del>
232+
text = re.sub(r"~~(.*?)~~", r"<del>\1</del>", text)
233+
234+
html_content = markdown(
235+
text,
236+
extensions=[
237+
"markdown.extensions.tables",
238+
"markdown.extensions.fenced_code",
239+
"markdown.extensions.nl2br",
240+
"markdown.extensions.sane_lists",
241+
"markdown.extensions.codehilite",
242+
],
243+
)
244+
245+
clean_message = bleach.clean(
246+
html_content, tags=allowed_tags, attributes=allowed_attributes, strip=False
247+
)
248+
249+
# Turn URLs into links and parse emails
250+
clean_message = bleach.linkify(clean_message, parse_email=True)
251+
252+
return clean_message
253+
254+
159255
class DateTimeEncoder(json.JSONEncoder):
160256
"""
161257
Custom JSON Encoder that handles Python date/times

backend/services/messaging/message_service.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,16 @@ async def _push_messages(messages: list, db: Database):
206206
and obj.message_type == MessageType.TASK_COMMENT_NOTIFICATION.value
207207
):
208208
continue
209+
if (
210+
user.task_validation_notification is False
211+
and obj.message_type == MessageType.VALIDATION_NOTIFICATION.value
212+
):
213+
continue
209214

210-
if user.tasks_notifications is False and obj.message_type in (
211-
MessageType.VALIDATION_NOTIFICATION.value,
212-
MessageType.INVALIDATION_NOTIFICATION.value,
215+
if (
216+
user.task_invalidation_notification is False
217+
and obj.message_type == MessageType.INVALIDATION_NOTIFICATION.value
213218
):
214-
messages_objs.append(obj)
215219
continue
216220
# If the notification is enabled, send an email
217221
messages_objs.append(obj)
@@ -726,6 +730,38 @@ async def resend_email_validation(user_id: int, db: Database):
726730
raise ValueError("EmailNotSet- User does not have an email address")
727731
await SMTPService.send_verification_email(user.email_address, user.username)
728732

733+
@staticmethod
734+
async def get_all_tasks_mappers(
735+
project_id: int, task_id: int, db: Database
736+
) -> List[str]:
737+
query = """
738+
SELECT DISTINCT u.username
739+
FROM task_history th
740+
JOIN users u ON th.user_id = u.id
741+
WHERE th.project_id = :project_id
742+
AND th.task_id = :task_id
743+
AND th.action = 'STATE_CHANGE'
744+
AND th.action_text = 'MAPPED'
745+
"""
746+
rows = await db.fetch_all(
747+
query,
748+
{"project_id": project_id, "task_id": task_id},
749+
)
750+
return [r["username"] for r in rows]
751+
752+
@staticmethod
753+
async def get_all_project_mappers(project_id: int, db: Database) -> List[str]:
754+
query = """
755+
SELECT DISTINCT u.username
756+
FROM task_history th
757+
JOIN users u ON th.user_id = u.id
758+
WHERE th.project_id = :project_id
759+
AND th.action = 'STATE_CHANGE'
760+
AND th.action_text = 'MAPPED'
761+
"""
762+
rows = await db.fetch_all(query, {"project_id": project_id})
763+
return [r["username"] for r in rows]
764+
729765
@staticmethod
730766
async def _parse_message_for_bulk_mentions(
731767
message: str, project_id: int, task_id: int = None, db: Database = None
@@ -796,6 +832,16 @@ async def _parse_message_for_bulk_mentions(
796832
project_id, task_id, db
797833
)
798834
usernames.extend(contributors)
835+
836+
if "mappers" in parsed:
837+
if task_id:
838+
mappers = await MessageService.get_all_tasks_mappers(
839+
project_id, task_id, db
840+
)
841+
else:
842+
mappers = await MessageService.get_all_project_mappers(project_id, db)
843+
usernames.extend(mappers)
844+
799845
return list(set(usernames))
800846

801847
@staticmethod

0 commit comments

Comments
 (0)