From ecc0f4fff5f58a61511c2688b6e455624cc3ae35 Mon Sep 17 00:00:00 2001 From: Naresh Kumar D Date: Tue, 14 Jul 2026 11:24:12 +0530 Subject: [PATCH 1/6] initial docker setup done - Added notes to local development. - Need to add more details on initial development. --- .gitignore | 1 + README.md | 25 ++++++++++++++ api/core/config.py | 1 + docker-compose.local.yml | 71 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 docker-compose.local.yml diff --git a/.gitignore b/.gitignore index 4516af1..d929d58 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,4 @@ docs/tasking-mvp/tasking-mvp.postman_environment.json docs/tasking-mvp/_enrich_postman.py docs/tasking-mvp/feature-coverage.md .idea/ +data/ \ No newline at end of file diff --git a/README.md b/README.md index 7b905d3..a8867e1 100644 --- a/README.md +++ b/README.md @@ -82,3 +82,28 @@ uvx pyright --pythonpath .venv/bin/python api tests uv run black api tests && uv run isort api tests ``` +## Development with local environment + +Use the file `docker-compose.local.yml` to build and deploy local code changes. This allows you to run the entire system at once instead of +connecting to existing Databases. + +### Initial setup. +- On first launch, rails-worker will fail because migrations are not done +- Go to the `osm-rails` `/bin/sh` and execute `bundle exec rails db:migrate` +- The above script runs the migration code for osm-rails +- The rails-worker will be able to run +- The backend code will fail first time becase `workspaces-tasks-local` database is not available +- Login to postgresql container and run the following commands + `psql --username postgres` + `create database "workspaces-tasks-local";` + `psql --username postgres --dbname "workspaces-tasks-local";` + `create extension if not exists postgis;` +- Run the backend code now and it should be able to run + +### Commands to start and stop the docker compose + +`docker compose --file docker-compose.local.yml build up -d` + +`docker compose --file docker-compose.local.yml down` + +Backend code will be available at `http://localhost:3000` \ No newline at end of file diff --git a/api/core/config.py b/api/core/config.py index 04693ac..c6554c7 100644 --- a/api/core/config.py +++ b/api/core/config.py @@ -96,6 +96,7 @@ def cors_origins_list(self) -> list[str]: model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", + extra="ignore", # ignore unknown environment variables ) diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..8bfeae3 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,71 @@ +services: + database: + image: postgis/postgis:16-3.4 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: testing + POSTGRES_DB: workspaces-osm-local + ports: + - 5432:5432 + volumes: + - ./data/db:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d workspaces-osm-local"] + interval: 10s + timeout: 5s + retries: 5 + osm-cgimap: + image: opensidewalksdev.azurecr.io/workspaces-osm-cgimap:dev + environment: + CGIMAP_INSTANCES: 10 + CGIMAP_HOST: database + CGIMAP_USERNAME: postgres + CGIMAP_PASSWORD: testing + CGIMAP_DBNAME: workspaces-osm-local + CGIMAP_MAX_CHANGESET_ELEMENTS: 100000000 # max features per import or save + CGIMAP_MAX_PAYLOAD: 1000000000 # max size of dataset uploads + CGIMAP_MAP_NODES: 100000000 # max number of nodes per requeset + CGIMAP_MAP_AREA: 1 # max area per request in square degrees + ports: + - 8000 + osm-rails: + image: opensidewalksdev.azurecr.io/workspaces-osm-rails:dev + environment: + RAILS_ENV: production + SECRET_KEY_BASE: osm_secret_key + WS_OSM_DB_HOST: database + WS_OSM_DB_USER: postgres + WS_OSM_DB_PASS: testing + WS_OSM_DB_NAME: workspaces-osm-local + stdin_open: true + tty: true + ports: + - 3000 + command: ["bundle", "exec", "rails", "s", "-p", "3000", "-b", "0.0.0.0"] + + osm-rails-worker: + image: opensidewalksdev.azurecr.io/workspaces-osm-rails:dev + environment: + RAILS_ENV: production + SECRET_KEY_BASE: osm_secret_key + WS_OSM_DB_HOST: database + WS_OSM_DB_USER: postgres + WS_OSM_DB_PASS: testing + WS_OSM_DB_NAME: workspaces-osm-local + stdin_open: true + tty: true + command: ["bundle", "exec", "rake", "jobs:work"] + + backend: + build: . + container_name: workspaces-backend + volumes: + - .:/app + ports: + - 8000:8000 + environment: + TDEI_BACKEND_URL: ${TDEI_BACKEND_URL} + TDEI_OIDC_REALM: ${TDEI_OIDC_REALM} + TDEI_OIDC_URL: ${TDEI_OIDC_URL} + OSM_DATABASE_URL: postgresql+asyncpg://postgres:testing@database:5432/workspaces-osm-local + TASK_DATABASE_URL: postgresql+asyncpg://postgres:testing@database:5432/workspaces-tasks-local \ No newline at end of file From ac2700fb4f54f764dfa4df2664429816fe8ae089 Mon Sep 17 00:00:00 2001 From: Naresh Kumar D Date: Tue, 14 Jul 2026 12:19:16 +0530 Subject: [PATCH 2/6] Added custom_imagery column Migration logic yet to be written --- api/src/tasking/projects/dtos.py | 3 +++ api/src/tasking/projects/repository.py | 4 ++++ api/src/tasking/projects/schemas.py | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/api/src/tasking/projects/dtos.py b/api/src/tasking/projects/dtos.py index 488e981..709b80f 100644 --- a/api/src/tasking/projects/dtos.py +++ b/api/src/tasking/projects/dtos.py @@ -47,6 +47,7 @@ class ProjectCreateRequest(WireModel): lock_timeout_hours: int = PydField(default=8, ge=1, le=720) aoi: Optional[AoiInput] = None role_assignments: list[ProjectRoleAssignment] = PydField(default_factory=list) + custom_imagery: Optional[Any] = PydField(default=None) @field_validator("name") @classmethod @@ -68,6 +69,7 @@ class ProjectUpdateRequest(WireModel): instructions: Optional[str] = PydField(default=None, max_length=10_000) lock_timeout_hours: Optional[int] = PydField(default=None, ge=1, le=720) review_required: Optional[bool] = None + custom_imagery: Optional[Any] = PydField(default=None) class ProjectResponse(WireModel): @@ -87,6 +89,7 @@ class ProjectResponse(WireModel): created_by_name: Optional[str] = None created_at: datetime updated_at: datetime + custom_imagery: Optional[Any] = None class ProjectListItem(WireModel): diff --git a/api/src/tasking/projects/repository.py b/api/src/tasking/projects/repository.py index 44bbc42..b046436 100644 --- a/api/src/tasking/projects/repository.py +++ b/api/src/tasking/projects/repository.py @@ -243,6 +243,7 @@ def _to_response(project: TaskingProject, task_count: int = 0) -> ProjectRespons created_by_name=project.created_by_name, created_at=project.created_at, updated_at=project.updated_at, + custom_imagery=project.custom_imagery, # type: ignore[arg-type] ) async def _provision_users_from_tdei( @@ -531,6 +532,7 @@ async def create( lock_timeout_hours=body.lock_timeout_hours, created_by=current_user.user_uuid, created_by_name=current_user.user_name, + custom_imagery=body.custom_imagery, # type: ignore[arg-type] ) if body.aoi is not None: geom = _aoi_to_shapely(body.aoi) @@ -632,6 +634,8 @@ async def patch( updates["lock_timeout_hours"] = body.lock_timeout_hours if body.review_required is not None: updates["review_required"] = body.review_required + if body.custom_imagery is not None: + updates["custom_imagery"] = body.custom_imagery # type: ignore[arg-type] if updates: updates["updated_at"] = datetime.now() diff --git a/api/src/tasking/projects/schemas.py b/api/src/tasking/projects/schemas.py index 901d5f6..8150f4b 100644 --- a/api/src/tasking/projects/schemas.py +++ b/api/src/tasking/projects/schemas.py @@ -11,6 +11,7 @@ from sqlalchemy import Column from sqlalchemy import Enum as SAEnum from sqlmodel import Field, SQLModel +from sqlalchemy.dialects.postgresql import JSONB # --------------------------------------------------------------------------- # Enums (mirrors of postgres enums in the migration) @@ -94,6 +95,12 @@ class TaskingProject(SQLModel, table=True): ) deleted_at: Optional[datetime] = None + # Custom Imagery data for the project. Stored as JSONB in the database and converted to / from a Pydantic model in the repository layer. + custom_imagery: Optional[Any] = Field( + default=None, + sa_column=Column(JSONB, nullable=True,default=None) + ) + # --------------------------------------------------------------------------- # Project role enum + table From 70b14ccac6244f0526a0141a1071d94908415cae Mon Sep 17 00:00:00 2001 From: Naresh Kumar D Date: Tue, 14 Jul 2026 15:30:15 +0530 Subject: [PATCH 3/6] imagery and description added --- README.md | 2 +- ...61f527ef_custom_imagery_and_description.py | 37 +++++++++++++++++++ api/src/tasking/projects/dtos.py | 7 +++- api/src/tasking/projects/repository.py | 2 + api/src/tasking/projects/schemas.py | 7 ++-- 5 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 alembic_osm/versions/a92361f527ef_custom_imagery_and_description.py diff --git a/README.md b/README.md index a8867e1..ebba814 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ connecting to existing Databases. ### Commands to start and stop the docker compose -`docker compose --file docker-compose.local.yml build up -d` +`docker compose --file docker-compose.local.yml up --build -d` `docker compose --file docker-compose.local.yml down` diff --git a/alembic_osm/versions/a92361f527ef_custom_imagery_and_description.py b/alembic_osm/versions/a92361f527ef_custom_imagery_and_description.py new file mode 100644 index 0000000..6a4fdcc --- /dev/null +++ b/alembic_osm/versions/a92361f527ef_custom_imagery_and_description.py @@ -0,0 +1,37 @@ +"""custom_imagery_and_description + +Revision ID: a92361f527ef +Revises: f3a7b9c1d2e4 +Create Date: 2026-07-14 15:08:42.142553 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import JSONB + +# revision identifiers, used by Alembic. +revision: str = "a92361f527ef" +down_revision: Union[str, None] = "f3a7b9c1d2e4" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "tasking_projects", + sa.Column("custom_imagery", JSONB(astext_type=sa.Text()), nullable=True), + ) + op.add_column( + "tasking_projects", + sa.Column("description", sa.String(length=10000), nullable=True), + ) + pass + + +def downgrade() -> None: + op.drop_column("tasking_projects", "custom_imagery") + op.drop_column("tasking_projects", "description") + pass diff --git a/api/src/tasking/projects/dtos.py b/api/src/tasking/projects/dtos.py index 709b80f..601c09e 100644 --- a/api/src/tasking/projects/dtos.py +++ b/api/src/tasking/projects/dtos.py @@ -47,7 +47,8 @@ class ProjectCreateRequest(WireModel): lock_timeout_hours: int = PydField(default=8, ge=1, le=720) aoi: Optional[AoiInput] = None role_assignments: list[ProjectRoleAssignment] = PydField(default_factory=list) - custom_imagery: Optional[Any] = PydField(default=None) + custom_imagery: Optional[dict[str, Any]] = PydField(default=None) + description: Optional[str] = PydField(default=None, max_length=10_000) @field_validator("name") @classmethod @@ -69,7 +70,8 @@ class ProjectUpdateRequest(WireModel): instructions: Optional[str] = PydField(default=None, max_length=10_000) lock_timeout_hours: Optional[int] = PydField(default=None, ge=1, le=720) review_required: Optional[bool] = None - custom_imagery: Optional[Any] = PydField(default=None) + custom_imagery: Optional[dict[str, Any]] = PydField(default=None) + description: Optional[str] = PydField(default=None, max_length=10_000) class ProjectResponse(WireModel): @@ -90,6 +92,7 @@ class ProjectResponse(WireModel): created_at: datetime updated_at: datetime custom_imagery: Optional[Any] = None + description: Optional[str] = None class ProjectListItem(WireModel): diff --git a/api/src/tasking/projects/repository.py b/api/src/tasking/projects/repository.py index b046436..71c4eb9 100644 --- a/api/src/tasking/projects/repository.py +++ b/api/src/tasking/projects/repository.py @@ -244,6 +244,7 @@ def _to_response(project: TaskingProject, task_count: int = 0) -> ProjectRespons created_at=project.created_at, updated_at=project.updated_at, custom_imagery=project.custom_imagery, # type: ignore[arg-type] + description=project.description, # type: ignore[arg-type] ) async def _provision_users_from_tdei( @@ -533,6 +534,7 @@ async def create( created_by=current_user.user_uuid, created_by_name=current_user.user_name, custom_imagery=body.custom_imagery, # type: ignore[arg-type] + description=body.description, # type: ignore[arg-type] ) if body.aoi is not None: geom = _aoi_to_shapely(body.aoi) diff --git a/api/src/tasking/projects/schemas.py b/api/src/tasking/projects/schemas.py index 8150f4b..85fc0b2 100644 --- a/api/src/tasking/projects/schemas.py +++ b/api/src/tasking/projects/schemas.py @@ -10,8 +10,8 @@ from pydantic import Field as PydField from sqlalchemy import Column from sqlalchemy import Enum as SAEnum -from sqlmodel import Field, SQLModel from sqlalchemy.dialects.postgresql import JSONB +from sqlmodel import Field, SQLModel # --------------------------------------------------------------------------- # Enums (mirrors of postgres enums in the migration) @@ -97,10 +97,11 @@ class TaskingProject(SQLModel, table=True): # Custom Imagery data for the project. Stored as JSONB in the database and converted to / from a Pydantic model in the repository layer. custom_imagery: Optional[Any] = Field( - default=None, - sa_column=Column(JSONB, nullable=True,default=None) + default=None, sa_column=Column(JSONB, nullable=True, default=None) ) + description: Optional[str] = Field(default=None, nullable=True) + # --------------------------------------------------------------------------- # Project role enum + table From 49c11b6e1ede198768882ef346beb17d11844f4f Mon Sep 17 00:00:00 2001 From: Naresh Kumar D Date: Tue, 14 Jul 2026 16:06:52 +0530 Subject: [PATCH 4/6] Added routes for validate name Added routes for validating the project name and unit tests for the same --- api/src/tasking/projects/dtos.py | 5 ++ api/src/tasking/projects/repository.py | 16 +++++ api/src/tasking/projects/routes.py | 21 +++++++ tests/integration/test_projects_flow.py | 32 ++++++++++ tests/unit/conftest.py | 8 +++ tests/unit/test_project_routes.py | 80 +++++++++++++++++++++++++ 6 files changed, 162 insertions(+) diff --git a/api/src/tasking/projects/dtos.py b/api/src/tasking/projects/dtos.py index 601c09e..01057a8 100644 --- a/api/src/tasking/projects/dtos.py +++ b/api/src/tasking/projects/dtos.py @@ -120,6 +120,10 @@ class ProjectListResponse(WireModel): pagination: Pagination +class ProjectNameValidationResponse(WireModel): + exists: bool + + # --------------------------------------------------------------------------- # AOI response shape (canonical Feature wrapping a MultiPolygon) # --------------------------------------------------------------------------- @@ -190,6 +194,7 @@ class SelfProjectRolesResponse(WireModel): "ProjectCreateRequest", "ProjectListItem", "ProjectListResponse", + "ProjectNameValidationResponse", "ProjectResponse", "ProjectRoleAddRequest", "ProjectRoleAssignment", diff --git a/api/src/tasking/projects/repository.py b/api/src/tasking/projects/repository.py index 71c4eb9..beee8e6 100644 --- a/api/src/tasking/projects/repository.py +++ b/api/src/tasking/projects/repository.py @@ -470,6 +470,22 @@ async def list_projects( pagination=Pagination(page=page, page_size=page_size, total=total), ) + async def project_name_exists(self, workspace_id: int, name: str) -> bool: + result = await self.session.execute( + select(func.count()) + .select_from(TaskingProject) + .where( + (TaskingProject.workspace_id == workspace_id) + & (TaskingProject.name == name) + & ( + TaskingProject.deleted_at.is_( # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess] + None + ) + ) + ) + ) + return int(result.scalar() or 0) > 0 + async def create( self, workspace_id: int, diff --git a/api/src/tasking/projects/routes.py b/api/src/tasking/projects/routes.py index d4979b0..4d531d0 100644 --- a/api/src/tasking/projects/routes.py +++ b/api/src/tasking/projects/routes.py @@ -12,6 +12,7 @@ AoiFeature, ProjectCreateRequest, ProjectListResponse, + ProjectNameValidationResponse, ProjectResponse, ProjectRoleAddRequest, ProjectRoleItem, @@ -128,6 +129,26 @@ async def create_project( ) +@router.get("/validate-name", response_model=ProjectNameValidationResponse) +async def validate_project_name( + workspace_id: int, + name: str = Query(..., min_length=1, max_length=255), + current_user: UserInfo = Depends(validate_token), + workspace_repo: WorkspaceRepository = Depends(get_workspace_repo), + project_repo: TaskingProjectRepository = Depends(get_project_repo), +): + await assert_workspace_visible(workspace_id, current_user, workspace_repo) + normalized_name = name.strip() + if not normalized_name: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="name cannot be blank", + ) + return ProjectNameValidationResponse( + exists=await project_repo.project_name_exists(workspace_id, normalized_name) + ) + + @router.get("/{project_id}", response_model=ProjectResponse) async def get_project( workspace_id: int, diff --git a/tests/integration/test_projects_flow.py b/tests/integration/test_projects_flow.py index c1575de..088c614 100644 --- a/tests/integration/test_projects_flow.py +++ b/tests/integration/test_projects_flow.py @@ -206,6 +206,38 @@ async def test_outsider_404s_on_list( assert r.status_code == 404 +class TestProjectNameValidation: + async def test_validate_name_false_then_true( + self, client, as_lead, seeded_workspace_id + ): + """validate-name reports false before create and true after create for same workspace.""" + path = f"{API.format(wid=seeded_workspace_id)}/validate-name" + + r = await client.get(path, params={"name": "name-check"}) + assert r.status_code == 200, r.text + assert r.json() == {"exists": False} + + r = await client.post( + API.format(wid=seeded_workspace_id), + json={"name": "name-check"}, + ) + assert r.status_code == 201, r.text + + r = await client.get(path, params={"name": "name-check"}) + assert r.status_code == 200, r.text + assert r.json() == {"exists": True} + + async def test_validate_name_outsider_404( + self, client, as_outsider, seeded_workspace_id + ): + """Outsider receives 404 from tenancy gate on validate-name.""" + r = await client.get( + f"{API.format(wid=seeded_workspace_id)}/validate-name", + params={"name": "anything"}, + ) + assert r.status_code == 404 + + # --------------------------------------------------------------------------- # Workflow 3b — error mapping (constraint violations → precise HTTP status). # --------------------------------------------------------------------------- diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 20ac3f5..e9fc3ad 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -175,6 +175,14 @@ async def get(self, workspace_id: int, project_id: int): raise NotFoundException(f"Project {project_id} not found") return self._response(p) + async def project_name_exists(self, workspace_id: int, name: str) -> bool: + return any( + p["workspace_id"] == workspace_id + and p["name"] == name + and p["deleted_at"] is None + for p in self._projects.values() + ) + async def patch(self, workspace_id, project_id, body, current_user): p_resp = await self.get(workspace_id, project_id) p = self._projects[project_id] diff --git a/tests/unit/test_project_routes.py b/tests/unit/test_project_routes.py index 1620aba..792103a 100644 --- a/tests/unit/test_project_routes.py +++ b/tests/unit/test_project_routes.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + API = "/api/v1/workspaces/{wid}/tasking/projects" @@ -68,6 +70,17 @@ async def test_create_blank_name_422( ) assert r.status_code == 422 + @pytest.mark.parametrize("bad_value", ["not-an-object", 123, True, ["x"]]) + async def test_create_rejects_non_object_custom_imagery_422( + self, client, as_lead, seeded_workspace_id, fake_repos, bad_value + ): + """custom_imagery must be a JSON object; scalar/array values are rejected (422).""" + r = await client.post( + API.format(wid=seeded_workspace_id), + json={"name": "Pilot", "custom_imagery": bad_value}, + ) + assert r.status_code == 422 + async def test_get_404_when_missing( self, client, as_lead, seeded_workspace_id, fake_repos ): @@ -105,6 +118,24 @@ async def test_patch_name(self, client, as_lead, seeded_workspace_id, fake_repos assert r.status_code == 200 assert r.json()["name"] == "after" + @pytest.mark.parametrize("bad_value", ["not-an-object", 123, False, ["x"]]) + async def test_patch_rejects_non_object_custom_imagery_422( + self, client, as_lead, seeded_workspace_id, fake_repos, bad_value + ): + """PATCH rejects custom_imagery when it is not a JSON object (422).""" + pid = ( + await client.post( + API.format(wid=seeded_workspace_id), + json={"name": "before"}, + ) + ).json()["id"] + + r = await client.patch( + f"{API.format(wid=seeded_workspace_id)}/{pid}", + json={"custom_imagery": bad_value}, + ) + assert r.status_code == 422 + async def test_soft_delete_204_then_404( self, client, as_lead, seeded_workspace_id, fake_repos ): @@ -137,6 +168,55 @@ async def test_duplicate_name_409( assert r.status_code == 409 +class TestProjectNameValidation: + async def test_validate_name_returns_false_when_missing( + self, client, as_contributor, seeded_workspace_id, fake_repos + ): + """Validation returns exists=false when no active project uses the name.""" + r = await client.get( + f"{API.format(wid=seeded_workspace_id)}/validate-name", + params={"name": "new-project"}, + ) + assert r.status_code == 200 + assert r.json() == {"exists": False} + + async def test_validate_name_returns_true_when_exists( + self, client, as_lead, seeded_workspace_id, fake_repos + ): + """Validation returns exists=true when an active project with same name exists.""" + await client.post( + API.format(wid=seeded_workspace_id), + json={"name": "pilot-check"}, + ) + + r = await client.get( + f"{API.format(wid=seeded_workspace_id)}/validate-name", + params={"name": "pilot-check"}, + ) + assert r.status_code == 200 + assert r.json() == {"exists": True} + + async def test_validate_name_blank_rejected_422( + self, client, as_contributor, seeded_workspace_id, fake_repos + ): + """Whitespace-only name is rejected with 422.""" + r = await client.get( + f"{API.format(wid=seeded_workspace_id)}/validate-name", + params={"name": " "}, + ) + assert r.status_code == 422 + + async def test_validate_name_outsider_404( + self, client, as_outsider, seeded_workspace_id, fake_repos + ): + """Tenancy gate hides workspace existence for outsiders.""" + r = await client.get( + f"{API.format(wid=seeded_workspace_id)}/validate-name", + params={"name": "pilot-check"}, + ) + assert r.status_code == 404 + + # --------------------------------------------------------------------------- # Lifecycle gates # --------------------------------------------------------------------------- From ed1b7f45d7e9b9c27212fc4d601e6273c7f9d445 Mon Sep 17 00:00:00 2001 From: Naresh Kumar D Date: Tue, 14 Jul 2026 16:16:09 +0530 Subject: [PATCH 5/6] Update a92361f527ef_custom_imagery_and_description.py removed unnecessary pass statement --- .../versions/a92361f527ef_custom_imagery_and_description.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/alembic_osm/versions/a92361f527ef_custom_imagery_and_description.py b/alembic_osm/versions/a92361f527ef_custom_imagery_and_description.py index 6a4fdcc..6f1bc64 100644 --- a/alembic_osm/versions/a92361f527ef_custom_imagery_and_description.py +++ b/alembic_osm/versions/a92361f527ef_custom_imagery_and_description.py @@ -28,10 +28,8 @@ def upgrade() -> None: "tasking_projects", sa.Column("description", sa.String(length=10000), nullable=True), ) - pass def downgrade() -> None: op.drop_column("tasking_projects", "custom_imagery") op.drop_column("tasking_projects", "description") - pass From 1129280b57018067103b9c986b779a881c949521 Mon Sep 17 00:00:00 2001 From: Naresh Kumar D Date: Tue, 14 Jul 2026 16:26:26 +0530 Subject: [PATCH 6/6] updated required settings --- README.md | 2 +- api/src/tasking/projects/repository.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ebba814..06ba988 100644 --- a/README.md +++ b/README.md @@ -106,4 +106,4 @@ connecting to existing Databases. `docker compose --file docker-compose.local.yml down` -Backend code will be available at `http://localhost:3000` \ No newline at end of file +Backend code will be available at `http://localhost:8000` \ No newline at end of file diff --git a/api/src/tasking/projects/repository.py b/api/src/tasking/projects/repository.py index beee8e6..95875c1 100644 --- a/api/src/tasking/projects/repository.py +++ b/api/src/tasking/projects/repository.py @@ -654,6 +654,8 @@ async def patch( updates["review_required"] = body.review_required if body.custom_imagery is not None: updates["custom_imagery"] = body.custom_imagery # type: ignore[arg-type] + if body.description is not None: + updates["description"] = body.description if updates: updates["updated_at"] = datetime.now()