Skip to content

feat: task assignment UI for project managers (#3095) - #3097

Open
kartikey54 wants to merge 3 commits into
hotosm:devfrom
kartikey54:feat/task-assignment-ui
Open

kartikey54 wants to merge 3 commits into
hotosm:devfrom
kartikey54:feat/task-assignment-ui

Conversation

@kartikey54

@kartikey54 kartikey54 commented Jun 14, 2026

Copy link
Copy Markdown

What

A first, "assign in advance" task assignment UI for project managers, addressing #3095. Managers get a panel on the project details page that shows task areas on a map, lets them select tasks and apply an assignee + group label (with one-click quadrant auto-grouping for the NE/SW/SE/NW bulk case), and persists assignments as feature properties on the existing task_areas_geojson.

This is opened as a draft for discussion — it implements the durable "edit in the browser, confirm to save" half of the issue. The field-device sync (QFieldCloud geopackage / ODK entity push) and the CoMaps .kmz export (#3064) are deliberately deferred to follow-up PRs (see Deferred below). I'd value a steer on the open questions before going further.

How it fits the existing architecture

No SPA, no build step, no new dependencies, no schema change — entirely server-rendered LiteStar + HTMX + Jinja2 with a flat static JS file, per AGENTS.md and the existing decisions.

Two commits:

  1. fix(backend): stabilize task_id across save and export paths — a small prerequisite refactor. Task ids were assigned positionally and divergently in the QField (_build_tasks_geojson) and ODK (_build_task_entities) export paths, so the two ecosystems could disagree on task identity. This stamps a persistent, collision-safe integer task_id into task_areas_geojson features on save and makes both export builders read the stored id. This is the keystone the assignment feature keys on, and is independently useful — happy to split it into its own PR if you'd prefer.

  2. feat(htmx): task assignment map panel for project managers — the panel itself:

    • New app/htmx/assignment/ module with three project_manager-gated routes: a lazy-loaded panel fragment, a JSON assignments/geojson endpoint (tasks merged with assigned_to / assigned_group), and a POST save.
    • save_task_assignments service alongside save_task_areas: validates the group label (^[a-zA-Z0-9_-]*$), caps/sanitises the assignee, rejects unknown task ids, merges into feature properties under a FOR UPDATE row lock, and never writes status — task state stays owned by the field apps.
    • task_assignment.js (flat file, reuses the global Leaflet): click / shift-click selection shared between map and table, quadrant auto-grouping, draft edits staged in sessionStorage (matching the existing ftm-draft-aoi-outline precedent) and replayed over fresh server data, all user values rendered via textContent only.
    • Confirm warnings on the data-extract / re-split steps when stored assignments would be discarded.

Screenshots

Assignment panel — task areas on a map, quadrant-coloured by group, with the summary table below:

Assignment panel

Selecting tasks — click / shift-click on the map and table, then apply an assignee + group to the selection:

Selecting tasks

After saving — assignments persisted to feature properties and reflected in the table:

After saving

Tests

34 new tests (tests/htmx/test_assignment_routes.py, plus task-id stability coverage in test_finalize_odk.py / test_qfield_routes.py): route registration and auth, geojson shape and defaults, the save happy path (incl. HX-Trigger payload and that status is never written), XSS escaping, validation rejects (bad group label, overlong assignee, unknown task id, missing payload), the empty/{} no-split sentinel, and cross-builder task-id agreement. Local backend suite: 305 passed, 0 failed (the two ODK-Central / QGIS-container suites were not run locally).

Deliberately deferred (happy to sequence as follow-ups)

  • ODK entity sync — push assigned_to / assigned_group to the tasks dataset entities (most machinery already exists in central_crud).
  • QFieldCloud sync — pull tasks.gpkg, merge assignment fields by task_id, push back; plus stopping _add_plugin_task_fields from clobbering assignments on regeneration.
  • CoMaps / Organic Maps .kmz export (Mapping app: add CoMaps #3064).
  • Field status read-back (showing live task progress on the manager's map) — depends on the sync work above.

Open questions

  1. Storage shape: assignments as task_areas_geojson feature properties (this PR) vs a relational task_assignments table (audit trail, row-level locking). I went properties-first to keep this PR schema-free; would you want the table from the start?
  2. Permissions: nothing in the codebase appears to write user_roles, so project_manager only resolves for global admins in deployments today. Should project creators get a project-admin role at creation?
  3. Assignee identity: free-text assigned_to (covers account-less mappers / shared QFC accounts) with opportunistic QFC-collaborator suggestions — acceptable, or should assignment drive per-mapper QFC user provisioning?
  4. Groups: is the free-text assigned_group label the right model for the quadrant use case, or did you envision groups of users?
  5. Single-task projects (task_areas_geojson == {}): this PR shows a "split first" callout rather than materialising a synthetic single task — OK?

Refs #3095. Related #3064.

Stamp a persistent task_id into task_areas_geojson features on save,
and make the ODK entity builder honor stored ids instead of
re-deriving them positionally, so QField and ODK exports agree on
task identity across re-finalization.

Signed-off-by: kartikey54 <kartikey54@users.noreply.github.com>
Add an assignment panel to the project details page: a Leaflet task
selection map with quadrant auto-grouping, per-task assignee and
group labels staged in sessionStorage, saved as feature properties
on task_areas_geojson. Assignment fields only; task status stays
owned by the field apps. Part of hotosm#3095.

Signed-off-by: kartikey54 <kartikey54@users.noreply.github.com>
@github-actions github-actions Bot added enhancement New feature or request backend Related to backend code tests Related to automated code tests labels Jun 14, 2026
@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Contributor Signature Required

Thank you for your contribution!

Before we can accept your pull request, you need to sign our Contribution Policy.

Why do I need to do this?

  • Ensures you have read and will abide by our contribution guidelines.
  • Adds a small barrier for bot / AI account contribution, which we do not allow.
  • We care deeply about our community at HOT, and want everyone to be on the same page.

How to sign

To sign the agreement, please comment on this PR with:

I have read the CONTRIBUTING.md document and I hereby sign and agree with the guidelines

⚠️ Note: You only need to sign once. Future contributions to this repository will not require re-signing.


I have read the CONTRIBUTING.md document and I hereby sign and agree with the guidelines


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@kartikey54

Copy link
Copy Markdown
Author

I have read the CONTRIBUTING.md document and I hereby sign and agree with the guidelines

@kartikey54
kartikey54 marked this pull request as ready for review June 14, 2026 06:45
@spwoodcock

Copy link
Copy Markdown
Member

Cool! Thanks @kartikey54!

Sorry for the slow review, I have been on holiday. I will check this ASAP and give feedback =)

@caiotheodoro

Copy link
Copy Markdown

Read through the task_id stability fix, save_task_assignments, and assignment_routes.py - the FOR UPDATE locking is correct (psycopg's async connections default to autocommit=False, so the lock holds until the later commit()), and the <option value="{{ name }}"> QFC-suggestion rendering is safe since JinjaTemplateEngine sets autoescape=True. No blocking issues in the core files.

On open question #2: confirmed by grep, user_roles has no writer anywhere in the codebase (only read in roles.py/user_crud.py). project_manager requires either db_user.is_admin or a PROJECT_ADMIN row in that table, so as things stand a project's own creator can't reach this panel (or any other project_manager-gated route) unless someone is manually inserting rows. That's not new to this PR, but worth flagging loudly since it means the feature can't actually be exercised by its target user in a normal deployment yet - seems like the real blocker to sequence before or alongside this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Related to backend code enhancement New feature or request tests Related to automated code tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants