Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
fbc7750
Sort users dynamically based on sortBy value
suzit-10 Aug 12, 2025
97ae9f8
Add sorting header with options to sort by `mapped`, `validated` and …
suzit-10 Aug 12, 2025
961609f
Adjust the test cases after sorting the contribution list by `mapped`…
suzit-10 Aug 18, 2025
32b0288
Refactor code
suzit-10 Sep 1, 2025
221be29
Remove density layer from base layer options list
suzit-10 Sep 5, 2025
f833d7d
prevent accessing unauthorized pages through url
suzit-10 Sep 4, 2025
df2b88d
Allow same team to assign as different role
suzit-10 Sep 15, 2025
95a758e
Restrict save until the team is assigned as a explicit role(mapper/va…
suzit-10 Sep 15, 2025
1badd44
Prevent assigning the same team multiple times to a same role
suzit-10 Sep 16, 2025
dffd95c
Merge pull request #7020 from hotosm/feat/order-project-contribution
ramyaragupathy Sep 24, 2025
d0acb60
Merge pull request #7028 from hotosm/fix/6755-remove-density-layer
ramyaragupathy Sep 24, 2025
3c5721f
Merge pull request #7034 from hotosm/fix/6906-pages-access-through-url
ramyaragupathy Sep 24, 2025
eb366e7
Unlink team from a project for allowing team deletion
prabinoid Jul 25, 2025
59b88be
Add CustomMenu component with active item highlighting
suzit-10 Jul 29, 2025
12cf5a1
Add TeamLinkedProjectCard component with placeholder support
suzit-10 Jul 29, 2025
efb4128
Create `TeamLinkedProjects` to display a team's linked projects group…
suzit-10 Jul 29, 2025
feb1ebf
Replaced the generic `Projects` component with the new `TeamLinkedPro…
suzit-10 Jul 29, 2025
ec108f4
Add source strings of new component `TeamLinkedProjects` on messages.js
suzit-10 Jul 29, 2025
300582b
refactor: Minor code cleanup and accessibility improvements
suzit-10 Jul 29, 2025
01bddff
Check team permissions and eligibility to unlink from projects and bu…
prabinoid Aug 22, 2025
b0720e9
Unlink a team from all the project after permission validation
prabinoid Aug 27, 2025
1efd8fe
Update individual project unlink api endpoint
suzit-10 Aug 25, 2025
89848e6
add i18n messages for Unlink buttons
suzit-10 Sep 2, 2025
e7dcedd
Add project selection checkbox on project list and visible to users h…
suzit-10 Sep 2, 2025
8146dbf
Enable unlink project by project selection/inidividually or in bulk(A…
suzit-10 Sep 2, 2025
6d8b879
Team unlink check modified with updated mapping and validation permis…
prabinoid Sep 8, 2025
d9e4a94
Show inline error message for individual project unlink failure
suzit-10 Sep 8, 2025
46616ea
Display persistent success/error message insted of toast on project u…
suzit-10 Sep 8, 2025
d3a14c1
Add confirmation dialog for bulk actions(unlink all/unlink selected) …
suzit-10 Sep 8, 2025
bb27c41
Add i18n messages for confirmation dialog for bulk project unlink
suzit-10 Sep 8, 2025
5e48531
Add project manager permission check for team unlinking
prabinoid Sep 16, 2025
8865849
Update error message for bulk unlinking failure by restricted permiss…
suzit-10 Oct 6, 2025
a0ef91f
Comment density text check on test case
suzit-10 Oct 9, 2025
b3568d5
Subcode refactored on team deletion permission checks
prabinoid Oct 9, 2025
bc2d577
infra: update ecs scale down bounds
nischalstha9 Oct 15, 2025
331e706
Merge pull request #7051 from hotosm/infra/scaling-tweaks
dakotabenjamin Oct 16, 2025
4382b5b
private project mapping and validation permission checks team members…
prabinoid Oct 15, 2025
ef55990
ci: rename label.yml --> pr_label.yml
spwoodcock Oct 20, 2025
8ee05dc
ci: label each issue with `repo:tm` as they are made
spwoodcock Oct 20, 2025
ed047e5
Change project and task geom columns wkt to ewkt to make it projectio…
prabinoid Aug 27, 2025
8282fa7
Change default contact address
prabinoid Oct 28, 2025
38e2490
Merge pull request #7035 from hotosm/fix/7032-project-save-validation
ramyaragupathy Oct 28, 2025
b623b97
Merge pull request #6953 from hotosm/feat/team-deletion
ramyaragupathy Oct 28, 2025
699a5dd
Combine all base layers source and layer on single object
suzit-10 Oct 7, 2025
54cbdf0
Toggle base layer visibility insted of updating overall style which p…
suzit-10 Oct 7, 2025
c07247a
Add only basic styles on initialization and add baselayer on `on.load…
suzit-10 Oct 7, 2025
13ded51
refactor: update foreach loop to for of and add Object as a proptype …
suzit-10 Oct 29, 2025
59270d7
Level ordering in user detail and level upgrade template updated.
prabinoid Oct 29, 2025
7ec8f62
Update project unlink failure error messages based on permission type…
suzit-10 Oct 29, 2025
845434e
Use user level order instead of fixed `advanced` level for enabling o…
suzit-10 Oct 29, 2025
4da31c3
add a function that adds the mapper suffix to the mapper level
suzit-10 Oct 29, 2025
6703b1d
add testid identifier to popup trigger and popup content
suzit-10 Sep 24, 2025
48ba01d
Remove unused variable function `getAllByRole`
suzit-10 Sep 24, 2025
11a0455
move redundant code to setup section and make test case async
suzit-10 Oct 16, 2025
4c34651
Introduce test retries for better handling of intermittent failures
suzit-10 Oct 16, 2025
f048c8d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 31, 2025
9c6fc54
Refactored partners stats to be non blocking operation, handle csrf a…
prabinoid Oct 30, 2025
0fe6332
fix blank `swipes by project type` chart
suzit-10 Oct 30, 2025
cde5637
Display ID column and navigate to project details page on table row c…
suzit-10 Nov 3, 2025
85f7c81
re-order the map layers
suzit-10 Nov 3, 2025
6448fa5
Merge pull request #7063 from hotosm/fix/follow-up-team-unlink-error-…
ramyaragupathy Nov 4, 2025
4edbf08
Merge pull request #7045 from hotosm/fix/7030-changing-basemap-kills-…
ramyaragupathy Nov 4, 2025
8624a78
Merge pull request #7065 from hotosm/fix/7005-show-project-ids
ramyaragupathy Nov 4, 2025
120f87f
Merge pull request #7062 from hotosm/fix/mapping-level-naming
ramyaragupathy Nov 4, 2025
b972cb0
Merge pull request #7058 from hotosm/fix/geom-wkt
ramyaragupathy Nov 4, 2025
71641e0
Mapswipe url made env variable
prabinoid Nov 5, 2025
c102666
Merge pull request #7057 from hotosm/fix-contact-address
ramyaragupathy Nov 7, 2025
9ca2617
Merge pull request #7061 from hotosm/fix/mapswipe-stats
ramyaragupathy Nov 7, 2025
10983f2
Move super mapper to done
ramyaragupathy Nov 7, 2025
b28e262
Merge pull request #7071 from hotosm/update/roadmap
ramyaragupathy Nov 7, 2025
61cb20c
Merge pull request #7042 from hotosm/fix/test-case-flikyness
nischalstha9 Nov 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/issue_label.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# We add a label `repo:repo-name` to each new issue,
# for easier tracking in external systems

name: 🏷️ Issue Label

on:
issues:
types:
- opened

jobs:
issue-label:
runs-on: ubuntu-latest
permissions:
issues: write

steps:
- run: gh issue edit "$NUMBER" --add-label "$LABELS"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
LABELS: repo:tm
File renamed without changes.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Status | Feature | Release
✅ | MapSwipe Stats Integration: Display MapSwipe statistics on Partner Pages.|[v4.8.2](https://github.com/hotosm/tasking-manager/releases/tag/v4.8.2)
✅ | 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)
✅ | 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)
🔄 | Super Mapper: Redefine Mapper Level Milestones
| Super Mapper: Redefine Mapper Level Milestones | [v5.2.0](https://github.com/hotosm/tasking-manager/releases/tag/v5.2.0)
🔄 | OSM Practice Projects: Enable users to engage in OSM practice projects within Tasking Manager workflow.
📅 | Expanding Project Types beyond basemap features
📅 | AI Integration: task assignment, difficulty estimation, and validation
Expand Down
29 changes: 18 additions & 11 deletions backend/api/partners/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,15 @@ async def get_filtered_statistics(
sub_code=MAPSWIPE_GROUP_EMPTY_SUBCODE,
message=MAPSWIPE_GROUP_EMPTY_MESSAGE,
)

mapswipe = MapswipeService()
return mapswipe.fetch_filtered_partner_stats(
partner.id, partner.mapswipe_group_id, from_date, to_date
)
try:
result = await mapswipe.fetch_filtered_partner_stats(
partner.id, partner.mapswipe_group_id, from_date, to_date
)
finally:
await mapswipe.aclose()

return result


@router.get("/{permalink:str}/general-statistics/")
Expand Down Expand Up @@ -176,13 +180,16 @@ async def get_general_statistics(
)

mapswipe = MapswipeService()
group_dto = mapswipe.fetch_grouped_partner_stats(
partner.id,
partner.mapswipe_group_id,
limit,
offset,
download_as_csv,
)
try:
group_dto = await mapswipe.fetch_grouped_partner_stats(
partner.id,
partner.mapswipe_group_id,
limit,
offset,
download_as_csv,
)
finally:
await mapswipe.aclose()

if download_as_csv:
csv_content = group_dto.to_csv()
Expand Down
212 changes: 212 additions & 0 deletions backend/api/teams/actions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List
from backend.models.dtos.team_dto import ProjectTeamPairDTOList
from databases import Database
from fastapi import APIRouter, BackgroundTasks, Body, Depends, Request
from fastapi.responses import JSONResponse
Expand Down Expand Up @@ -407,3 +409,213 @@ async def message_team(
)
except ValueError as e:
return JSONResponse(content={"Error": str(e)}, status_code=400)


@router.delete("/projects/{project_id}/teams/{team_id}/")
async def remove_team_from_project(
project_id: int,
team_id: int,
request: Request,
user: AuthUserDTO = Depends(login_required),
db: Database = Depends(get_db),
):
"""
Unlink a Team from a Project.
"""
permitted = await TeamService.is_user_team_manager(team_id, user.id, db)
if not permitted:
return JSONResponse(
{
"Error": "User is not a manager of the team",
"SubCode": "UserPermissionError",
},
status_code=403,
)

deny_resp = await TeamService.ensure_unlink_allowed(project_id, team_id, db)
if deny_resp:
return deny_resp

try:
deleted = await TeamService.unlink_team(project_id, team_id, db)
if not deleted:
return JSONResponse(
{"Error": "No such team linked to project", "SubCode": "NotFoundError"},
status_code=404,
)
return JSONResponse({"Success": True}, status_code=200)
except Exception as e:
return JSONResponse(
{"Error": "Internal server error", "Details": str(e)},
status_code=500,
)


@router.delete("/projects/teams/{team_id}/unlink")
async def remove_team_from_all_projects(
team_id: int,
request: Request,
user: AuthUserDTO = Depends(login_required),
db: Database = Depends(get_db),
):
"""
Unlink the given team from all projects it is assigned to.

Steps:
- ensure caller is a manager of the team
- fetch all project_ids for the team from project_teams
- run ensure_unlink_allowed(project_id, team_id, db) for every project
- if all checks pass, unlink each (inside one DB transaction)
"""
permitted = await TeamService.is_user_team_manager(team_id, user.id, db)
if not permitted:
return JSONResponse(
{
"Error": (
f"Cannot unlink team with team id-{team_id}: "
f"user {user.id} is not a manager of the team"
),
"SubCode": "UserPermissionError",
},
status_code=403,
)

rows = await db.fetch_all(
"SELECT project_id FROM project_teams WHERE team_id = :tid",
{"tid": team_id},
)
project_ids: List[int] = [r["project_id"] for r in rows] if rows else []

if not project_ids:
return JSONResponse(
{
"Error": (
f"Cannot unlink team with team id-{team_id}: "
"team is not linked to any projects"
),
"SubCode": "NotFoundError",
},
status_code=404,
)

for pid in project_ids:
deny_resp = await TeamService.ensure_unlink_allowed(pid, team_id, db)
if deny_resp:
return deny_resp

try:
async with db.transaction():
for pid in project_ids:
deleted = await TeamService.unlink_team(pid, team_id, db)
if not deleted:
raise RuntimeError(f"NOT_FOUND:{pid}:{team_id}")

projects_str = ", ".join(str(p) for p in project_ids)
return JSONResponse(
{
"Success": True,
"Message": (
f"Team id-{team_id} unlinked from projects: {projects_str}"
),
},
status_code=200,
)

except Exception as e:
return JSONResponse(
{
"Error": (
f"Cannot unlink team with team id-{team_id}: internal server error - {str(e)}"
),
"SubCode": "InternalServerError",
},
status_code=500,
)


@router.delete("/projects/unlink")
async def remove_teams_from_projects(
payload: ProjectTeamPairDTOList,
request: Request,
user: AuthUserDTO = Depends(login_required),
db: Database = Depends(get_db),
):
"""
Bulk unlink teams from projects.

Body:
{
"items": [
{"project_id": 1442, "team_id": 43},
{"project_id": 2000, "team_id": 55}
]
}

First: run all checks for all items (manager check + ensure_unlink_allowed).
If any check fails, return error immediately and do NOT modify DB.
If all checks pass, perform unlink operations inside a single DB transaction.
"""
items = payload.items or []
if not items:
return JSONResponse(
{"Error": "No project/team pairs provided", "SubCode": "InvalidRequest"},
status_code=400,
)

seen = set()
pairs = []
for it in items:
key = (it.project_id, it.team_id)
if key in seen:
continue
seen.add(key)
pairs.append(it)

for it in pairs:
pid = it.project_id
tid = it.team_id

permitted = await TeamService.is_user_team_manager(tid, user.id, db)
if not permitted:
return JSONResponse(
{
"Error": (
f"Cannot unlink team with team id-{tid}: user {user.id} is not a manager of the team"
),
"SubCode": "UserPermissionError",
},
status_code=403,
)

deny_resp = await TeamService.ensure_unlink_allowed(pid, tid, db)
if deny_resp:
return deny_resp

try:
async with db.transaction():
for it in pairs:
pid = it.project_id
tid = it.team_id

deleted = await TeamService.unlink_team(pid, tid, db)
if not deleted:
raise RuntimeError(f"NOT_FOUND:{pid}:{tid}")

pairs_str = ", ".join(
[f"(project {p.project_id}, team {p.team_id})" for p in pairs]
)
return JSONResponse(
{
"Success": True,
"Message": f"Unlinked teams: {pairs_str}",
},
status_code=200,
)
except Exception as e:
return JSONResponse(
{
"Error": f"Cannot unlink teams: internal server error - {str(e)}",
"SubCode": "InternalServerError",
},
status_code=500,
)
7 changes: 6 additions & 1 deletion backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class Config:

# The address to use as the receiver in contact form.
EMAIL_CONTACT_ADDRESS: str = os.getenv(
"TM_EMAIL_CONTACT_ADDRESS", "sysadmin@hotosm.org"
"TM_EMAIL_CONTACT_ADDRESS", "admin@yourorganisation.com"
)

# A freely definable secret key for connecting the front end with the back end
Expand Down Expand Up @@ -269,6 +269,11 @@ def assemble_db_connection(
# Sentry backend DSN
SENTRY_BACKEND_DSN: Optional[str] = os.getenv("TM_SENTRY_BACKEND_DSN", None)

# Mapswipe backend url
MAPSWIPE_API_URL: str = os.getenv(
"MAPSWIPE_API_URL", "https://backend.mapswipe.org/graphql/"
)

# Ohsome Stats Token
OHSOME_STATS_TOKEN: str = os.getenv("OHSOME_STATS_TOKEN", None)
OHSOME_STATS_API_URL: str = os.getenv(
Expand Down
9 changes: 9 additions & 0 deletions backend/models/dtos/team_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,12 @@ class TeamSearchDTO(BaseModel):

class Config:
populate_by_name = True


class ProjectTeamPairDTO(BaseModel):
project_id: int
team_id: int


class ProjectTeamPairDTOList(BaseModel):
items: List[ProjectTeamPairDTO]
1 change: 1 addition & 0 deletions backend/models/dtos/user_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class UserDTO(BaseModel):
username: Optional[str] = None
role: Optional[str] = None
mapping_level: Optional[str] = Field(None, alias="mappingLevel")
level_ordering: Optional[int] = None
projects_mapped: Optional[int] = Field(None, alias="projectsMapped")
email_address: Optional[str] = Field(None, alias="emailAddress")
is_email_verified: Optional[bool] = Field(
Expand Down
5 changes: 2 additions & 3 deletions backend/models/postgis/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ async def set_project_aoi(self, draft_project_dto: DraftProjectDTO, db: Database
valid_geojson = geojson.dumps(aoi_geometry)

query = """
SELECT ST_AsText(
SELECT ST_AsEWKT(
ST_SetSRID(
ST_GeomFromGeoJSON(:geojson), 4326
)
Expand All @@ -295,9 +295,8 @@ async def set_project_aoi(self, draft_project_dto: DraftProjectDTO, db: Database
# Execute the query with the GeoJSON value passed in as a parameter
result = await db.fetch_one(query=query, values={"geojson": valid_geojson})
self.geometry = result["geometry_wkt"] if result else None

query = """
SELECT ST_AsText(ST_Centroid(ST_SetSRID(ST_GeomFromGeoJSON(:geometry), 4326))) AS centroid
SELECT ST_AsEWKT(ST_Centroid(ST_SetSRID(ST_GeomFromGeoJSON(:geometry), 4326))) AS centroid
"""

# Execute the query and pass the GeoJSON as a parameter
Expand Down
4 changes: 3 additions & 1 deletion backend/models/postgis/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,9 @@ def from_geojson_feature(cls, task_id, task_feature):
task.y = task_feature.properties["y"]
task.zoom = task_feature.properties["zoom"]
task.is_square = task_feature.properties["isSquare"]
task.geometry = shape(task_feature.geometry).wkt
wkt = shape(task_feature.geometry).wkt
ewkt = f"SRID=4326;{wkt}"
task.geometry = ewkt
except KeyError as e:
raise InvalidData(
f"PropertyNotFound: Expected property not found: {str(e)}"
Expand Down
4 changes: 4 additions & 0 deletions backend/models/postgis/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,9 +476,13 @@ async def as_dto(self, logged_in_username: str, db: Database) -> UserDTO:
user_dto.id = self.id
user_dto.username = self.username
user_dto.role = UserRole(self.role).name

user_dto.mapping_level = (
await MappingLevel.get_by_id(self.mapping_level, db)
).name
user_dto.level_ordering = (
await MappingLevel.get_by_id(self.mapping_level, db)
).ordering
user_dto.projects_mapped = (
len(self.projects_mapped) if self.projects_mapped else None
)
Expand Down
Loading