diff --git a/contrib/playwright/docker-compose.yml b/contrib/playwright/docker-compose.yml index e4f5535bd4..7d12b7d14a 100644 --- a/contrib/playwright/docker-compose.yml +++ b/contrib/playwright/docker-compose.yml @@ -45,7 +45,7 @@ services: timeout: 5s ui-test: - image: "mcr.microsoft.com/playwright:${PLAYWRIGHT_TAG:-v1.44.1}" + image: "mcr.microsoft.com/playwright:${PLAYWRIGHT_TAG:-v1.48.1}" depends_on: # Starts the proxy and all other services proxy: diff --git a/src/backend/Dockerfile b/src/backend/Dockerfile index f0c17c9397..a57be5a897 100644 --- a/src/backend/Dockerfile +++ b/src/backend/Dockerfile @@ -16,11 +16,11 @@ # ARG PYTHON_IMG_TAG=3.11 ARG MINIO_TAG=${MINIO_TAG:-RELEASE.2024-06-06T09-36-42Z} -FROM docker.io/minio/minio:${MINIO_TAG} as minio +FROM docker.io/minio/minio:${MINIO_TAG} AS minio # Includes all labels and timezone info to extend from -FROM docker.io/python:${PYTHON_IMG_TAG}-slim-bookworm as base +FROM docker.io/python:${PYTHON_IMG_TAG}-slim-bookworm AS base ARG APP_VERSION ARG COMMIT_REF ARG PYTHON_IMG_TAG @@ -39,13 +39,13 @@ RUN set -ex \ && update-ca-certificates # Set locale RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US:en -ENV LC_ALL en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 # Extract dependencies from PDM lock to standard requirements.txt -FROM base as extract-deps +FROM base AS extract-deps WORKDIR /opt/python COPY pyproject.toml pdm.lock /opt/python/ RUN pip install --no-cache-dir --upgrade pip \ @@ -61,7 +61,7 @@ RUN pdm export --prod > requirements.txt \ # Build stage will all dependencies required to build Python wheels -FROM base as build +FROM base AS build # NOTE this argument is specified during production build on Github workflow # NOTE only the production API image contains the monitoring dependencies ARG MONITORING @@ -92,7 +92,7 @@ RUN pip install --user --no-warn-script-location --no-cache-dir \ # Run stage will minimal dependencies required to run Python libraries -FROM base as runtime +FROM base AS runtime ARG PYTHON_IMG_TAG ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ @@ -146,7 +146,7 @@ HEALTHCHECK --start-period=10s --interval=5s --retries=20 --timeout=5s \ # Add certificates to use ODK Central over SSL (HTTPS, required) -FROM runtime as add-odk-certs +FROM runtime AS add-odk-certs USER root # Add the SSL cert for debug odkcentral COPY --from=ghcr.io/hotosm/fmtm/proxy:debug \ @@ -155,7 +155,7 @@ RUN update-ca-certificates # Stage to use during local development -FROM add-odk-certs as debug +FROM add-odk-certs AS debug USER appuser COPY --from=extract-deps --chown=appuser \ /opt/python/requirements-debug.txt \ @@ -173,7 +173,7 @@ CMD ["python", "-Xfrozen_modules=off", "-m", "debugpy", \ # Used during CI workflows (as root), with docs/test dependencies pre-installed -FROM add-odk-certs as ci +FROM add-odk-certs AS ci ARG PYTHON_IMG_TAG COPY --from=extract-deps \ /opt/python/requirements-ci.txt /opt/python/ @@ -201,7 +201,7 @@ CMD ["sleep", "infinity"] # Final stage used during deployment -FROM runtime as prod +FROM runtime AS prod # Pre-compile packages to .pyc (init speed gains) RUN python -c "import compileall; compileall.compile_path(maxlevels=10, quiet=1)" # Note: 1 worker (process) per container, behind load balancer diff --git a/src/backend/app/central/central_crud.py b/src/backend/app/central/central_crud.py index 22945aa9ed..6f2447c367 100644 --- a/src/backend/app/central/central_crud.py +++ b/src/backend/app/central/central_crud.py @@ -703,11 +703,6 @@ async def get_entities_data( # Rename '__id' to 'id' flattened_dict["id"] = flattened_dict.pop("__id") - # convert empty str osm_id to None - # when new entities are created osm_id will be empty - if flattened_dict.get("osm_id", "") == "": - flattened_dict["osm_id"] = None - all_entities.append(flattened_dict) return all_entities diff --git a/src/backend/app/central/central_schemas.py b/src/backend/app/central/central_schemas.py index 456f75d771..c808608233 100644 --- a/src/backend/app/central/central_schemas.py +++ b/src/backend/app/central/central_schemas.py @@ -140,12 +140,34 @@ class EntityOsmID(BaseModel): id: str osm_id: Optional[int] = None + @field_validator("osm_id", mode="before") + @classmethod + def convert_osm_id(cls, value): + """Set osm_id to None if empty or invalid.""" + if value in ("", " "): # Treat empty strings as None + return None + try: + return int(value) # Convert to integer if possible + except ValueError: + return value + class EntityTaskID(BaseModel): """Map of Entity UUID to FMTM Task ID.""" id: str - task_id: int + task_id: Optional[int] = None + + @field_validator("task_id", mode="before") + @classmethod + def convert_task_id(cls, value): + """Set task_id to None if empty or invalid.""" + if value in ("", " "): # Treat empty strings as None + return None + try: + return int(value) # Convert to integer if possible + except ValueError: + return value class EntityMappingStatus(EntityOsmID, EntityTaskID): diff --git a/src/backend/app/projects/project_crud.py b/src/backend/app/projects/project_crud.py index a55c4b32e3..334ad45063 100644 --- a/src/backend/app/projects/project_crud.py +++ b/src/backend/app/projects/project_crud.py @@ -418,7 +418,8 @@ async def preview_split_by_square( Use a lambda function to remove the "z" dimension from each coordinate in the feature's geometry. """ - boundary = merge_polygons(boundary) + if len(boundary["features"]) == 0: + boundary = merge_polygons(boundary) return await run_in_threadpool( lambda: split_by_square( diff --git a/src/backend/pdm.lock b/src/backend/pdm.lock index a9bd2f30d7..e04c26538c 100644 --- a/src/backend/pdm.lock +++ b/src/backend/pdm.lock @@ -2,10 +2,13 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "debug", "dev", "docs", "test", "monitoring"] +groups = ["default", "debug", "dev", "docs", "monitoring", "test"] strategy = ["cross_platform"] -lock_version = "4.4.1" -content_hash = "sha256:53dd41a2dd0858c651adc1c6d8de4027476ca6af5c2c604ac8246968bf508374" +lock_version = "4.5.0" +content_hash = "sha256:0b251742ddfa49d9c69695eac81b46b2debdb020ef2992836851a2dfb0ad6be7" + +[[metadata.targets]] +requires_python = ">=3.11" [[package]] name = "aiohttp" @@ -14,6 +17,7 @@ requires_python = ">=3.8" summary = "Async http client/server framework (asyncio)" dependencies = [ "aiosignal>=1.1.2", + "async-timeout<5.0,>=4.0; python_version < \"3.11\"", "attrs>=17.3.0", "frozenlist>=1.1.1", "multidict<7.0,>=4.5", @@ -71,6 +75,9 @@ name = "annotated-types" version = "0.7.0" requires_python = ">=3.8" summary = "Reusable constraint types to use with typing.Annotated" +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.9\"", +] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -82,8 +89,10 @@ version = "4.4.0" requires_python = ">=3.8" summary = "High level compatibility layer for multiple asynchronous event loop implementations" dependencies = [ + "exceptiongroup>=1.0.2; python_version < \"3.11\"", "idna>=2.8", "sniffio>=1.1", + "typing-extensions>=4.1; python_version < \"3.11\"", ] files = [ {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, @@ -107,6 +116,7 @@ requires_python = ">=3.7" summary = "Argon2 for Python" dependencies = [ "argon2-cffi-bindings", + "typing-extensions; python_version < \"3.8\"", ] files = [ {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, @@ -150,6 +160,9 @@ name = "asgiref" version = "3.8.1" requires_python = ">=3.8" summary = "ASGI specs, helper code, and adapters" +dependencies = [ + "typing-extensions>=4; python_version < \"3.11\"", +] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, @@ -161,6 +174,7 @@ version = "2.4.1" summary = "Annotate AST trees with source code positions" dependencies = [ "six>=1.12.0", + "typing; python_version < \"3.5\"", ] files = [ {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, @@ -172,6 +186,9 @@ name = "async-lru" version = "2.0.4" requires_python = ">=3.8" summary = "Simple LRU cache for asyncio" +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.11\"", +] files = [ {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, @@ -182,6 +199,9 @@ name = "attrs" version = "23.2.0" requires_python = ">=3.7" summary = "Classes Without Boilerplate" +dependencies = [ + "importlib-metadata; python_version < \"3.8\"", +] files = [ {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, @@ -192,6 +212,9 @@ name = "babel" version = "2.15.0" requires_python = ">=3.8" summary = "Internationalization utilities" +dependencies = [ + "pytz>=2015.7; python_version < \"3.9\"", +] files = [ {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, @@ -297,6 +320,7 @@ requires_python = ">=3.7" summary = "Composable command line interface toolkit" dependencies = [ "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", ] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, @@ -308,6 +332,9 @@ name = "codetiming" version = "1.4.0" requires_python = ">=3.6" summary = "A flexible, customizable timer for your Python code." +dependencies = [ + "dataclasses; python_version < \"3.7\"", +] files = [ {file = "codetiming-1.4.0-py3-none-any.whl", hash = "sha256:3b80f409bef00941a9755c5524071ce2f72eaa4520f4bc35b33869cde024ccbd"}, {file = "codetiming-1.4.0.tar.gz", hash = "sha256:4937bf913a2814258b87eaaa43d9a1bb24711ffd3557a9ab6934fa1fe3ba0dbc"}, @@ -340,6 +367,7 @@ dependencies = [ "questionary<3.0,>=2.0", "termcolor<3,>=1.1", "tomlkit<1.0.0,>=0.5.3", + "typing-extensions<5.0.0,>=4.0.1; python_version < \"3.8\"", ] files = [ {file = "commitizen-3.27.0-py3-none-any.whl", hash = "sha256:11948fa563d5ad5464baf09eaacff3cf8cbade1ca029ed9c4978f2227f033130"}, @@ -599,7 +627,7 @@ files = [ [[package]] name = "fmtm-splitter" -version = "1.3.1" +version = "1.3.2" requires_python = ">=3.10" summary = "A utility for splitting an AOI into multiple tasks." dependencies = [ @@ -610,8 +638,8 @@ dependencies = [ "shapely>=1.8.1", ] files = [ - {file = "fmtm-splitter-1.3.1.tar.gz", hash = "sha256:90b739df69c1ab8ad18d615423ef230665e0b43b94c3e6c1ce345f8e4021e18f"}, - {file = "fmtm_splitter-1.3.1-py3-none-any.whl", hash = "sha256:409795cbb6c2d261544e2dcf6314aabdd0b63c47af7996a26958356003b345fe"}, + {file = "fmtm-splitter-1.3.2.tar.gz", hash = "sha256:2112ef9a904ea33662047469036d314885a4ceeefaf3910507406a884710d4ab"}, + {file = "fmtm_splitter-1.3.2-py3-none-any.whl", hash = "sha256:2596ac3db423337d4bbc1244da7082d8e9c12ad22c6890a369fb78b12aa23786"}, ] [[package]] @@ -723,6 +751,7 @@ requires_python = ">=3.7" summary = "GitPython is a Python library used to interact with Git repositories" dependencies = [ "gitdb<5,>=4.0.1", + "typing-extensions>=3.7.4.3; python_version < \"3.8\"", ] files = [ {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, @@ -762,6 +791,7 @@ version = "0.45.2" requires_python = ">=3.8" summary = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." dependencies = [ + "astunparse>=1.6; python_version < \"3.9\"", "colorama>=0.4", ] files = [ @@ -774,6 +804,9 @@ name = "h11" version = "0.14.0" requires_python = ">=3.7" summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -869,6 +902,7 @@ version = "7.1.0" requires_python = ">=3.8" summary = "Read metadata from Python packages" dependencies = [ + "typing-extensions>=3.6.4; python_version < \"3.8\"", "zipp>=0.5", ] files = [ @@ -892,8 +926,24 @@ version = "0.13.13" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" summary = "IPython-enabled pdb" dependencies = [ + "decorator; python_version == \"3.5\"", + "decorator; python_version == \"3.6\"", + "decorator; python_version > \"3.6\" and python_version < \"3.11\"", "decorator; python_version >= \"3.11\"", + "decorator<5.0.0; python_version == \"2.7\"", + "decorator<5.0.0; python_version == \"3.4\"", + "ipython<6.0.0,>=5.1.0; python_version == \"2.7\"", + "ipython<7.0.0,>=6.0.0; python_version == \"3.4\"", + "ipython<7.10.0,>=7.0.0; python_version == \"3.5\"", + "ipython<7.17.0,>=7.16.3; python_version == \"3.6\"", + "ipython>=7.31.1; python_version > \"3.6\" and python_version < \"3.11\"", "ipython>=7.31.1; python_version >= \"3.11\"", + "pathlib; python_version == \"2.7\"", + "toml>=0.10.2; python_version == \"2.7\"", + "toml>=0.10.2; python_version == \"3.4\"", + "toml>=0.10.2; python_version == \"3.5\"", + "tomli; python_version == \"3.6\"", + "tomli; python_version > \"3.6\" and python_version < \"3.11\"", ] files = [ {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, @@ -908,6 +958,7 @@ summary = "IPython: Productive Interactive Computing" dependencies = [ "colorama; sys_platform == \"win32\"", "decorator", + "exceptiongroup; python_version < \"3.11\"", "jedi>=0.16", "matplotlib-inline", "pexpect>4.3; sys_platform != \"win32\"", @@ -915,6 +966,7 @@ dependencies = [ "pygments>=2.4.0", "stack-data", "traitlets>=5", + "typing-extensions; python_version < \"3.10\"", ] files = [ {file = "ipython-8.18.0-py3-none-any.whl", hash = "sha256:d538a7a98ad9b7e018926447a5f35856113a85d08fd68a165d7871ab5175f6e0"}, @@ -1017,6 +1069,7 @@ version = "0.7.2" requires_python = ">=3.5" summary = "Python logging made (stupidly) simple" dependencies = [ + "aiocontextvars>=0.2.0; python_version < \"3.7\"", "colorama>=0.3.4; sys_platform == \"win32\"", "win32-setctime>=1.0.0; sys_platform == \"win32\"", ] @@ -1030,6 +1083,9 @@ name = "markdown" version = "3.6" requires_python = ">=3.8" summary = "Python implementation of John Gruber's Markdown." +dependencies = [ + "importlib-metadata>=4.4; python_version < \"3.10\"", +] files = [ {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, @@ -1159,6 +1215,7 @@ dependencies = [ "click>=7.0", "colorama>=0.4; platform_system == \"Windows\"", "ghp-import>=1.0", + "importlib-metadata>=4.4; python_version < \"3.10\"", "jinja2>=2.11.1", "markdown>=3.3.6", "markupsafe>=2.0.1", @@ -1207,6 +1264,7 @@ version = "0.2.0" requires_python = ">=3.8" summary = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" dependencies = [ + "importlib-metadata>=4.3; python_version < \"3.10\"", "mergedeep>=1.3.4", "platformdirs>=2.2.0", "pyyaml>=5.1", @@ -1275,10 +1333,12 @@ dependencies = [ "Markdown>=3.3", "MarkupSafe>=1.1", "click>=7.0", + "importlib-metadata>=4.6; python_version < \"3.10\"", "mkdocs-autorefs>=0.3.1", "mkdocs>=1.4", "platformdirs>=2.2.0", "pymdown-extensions>=6.3", + "typing-extensions>=4.1; python_version < \"3.10\"", ] files = [ {file = "mkdocstrings-0.25.1-py3-none-any.whl", hash = "sha256:da01fcc2670ad61888e8fe5b60afe9fee5781017d67431996832d63e887c2e51"}, @@ -1579,7 +1639,7 @@ files = [ [[package]] name = "osm-fieldwork" -version = "0.16.7" +version = "0.16.9" requires_python = ">=3.10" summary = "Processing field data from ODK to OpenStreetMap format." dependencies = [ @@ -1605,8 +1665,8 @@ dependencies = [ "xmltodict>=0.13.0", ] files = [ - {file = "osm-fieldwork-0.16.7.tar.gz", hash = "sha256:654b9975b2333b6f812bad5c971577a0a8544e298d0236ecd1befbb5768214e5"}, - {file = "osm_fieldwork-0.16.7-py3-none-any.whl", hash = "sha256:2847230f2defee4f699648be8651a9437c24c56c93dc01389f164f1a845ea4cc"}, + {file = "osm-fieldwork-0.16.9.tar.gz", hash = "sha256:113e2f2091eec54dea9ee29e1b14681bc881a2770591337773cd0ea1bdabd110"}, + {file = "osm_fieldwork-0.16.9-py3-none-any.whl", hash = "sha256:d38c1789a62abbe0e385e3750255d06fcbc22a885cbbdb69b4e29aee17519ff3"}, ] [[package]] @@ -1626,7 +1686,7 @@ files = [ [[package]] name = "osm-rawdata" -version = "0.3.2" +version = "0.3.3" requires_python = ">=3.10" summary = "Make data extracts from OSM data." dependencies = [ @@ -1642,8 +1702,8 @@ dependencies = [ "sqlalchemy>=2.0.0", ] files = [ - {file = "osm-rawdata-0.3.2.tar.gz", hash = "sha256:9e715c41ea0d7c306d984eee00859cb3414f5a705ed3578d43a5910e3d04a545"}, - {file = "osm_rawdata-0.3.2-py3-none-any.whl", hash = "sha256:97395ceb0ef9a5444a2cdf7cfcbcb95917bc35df4a395de0f30437d7b60e28b5"}, + {file = "osm-rawdata-0.3.3.tar.gz", hash = "sha256:9756174dc09ed026d2f14468d521f9c4d4f302054be6fa394c789a346b3e7525"}, + {file = "osm_rawdata-0.3.3-py3-none-any.whl", hash = "sha256:2c24381479a60ab6460e6606d25fba2c83ae379af1fec98bb460734eb36092f8"}, ] [[package]] @@ -1670,6 +1730,7 @@ version = "2.2.2" requires_python = ">=3.9" summary = "Powerful data structures for data analysis, time series, and statistics" dependencies = [ + "numpy>=1.22.4; python_version < \"3.11\"", "numpy>=1.23.2; python_version == \"3.11\"", "numpy>=1.26.0; python_version >= \"3.12\"", "python-dateutil>=2.8.2", @@ -2035,6 +2096,9 @@ name = "pyjwt" version = "2.8.0" requires_python = ">=3.7" summary = "JSON Web Token implementation in Python" +dependencies = [ + "typing-extensions; python_version <= \"3.7\"", +] files = [ {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, @@ -2096,9 +2160,11 @@ requires_python = ">=3.8" summary = "pytest: simple powerful testing with Python" dependencies = [ "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", "iniconfig", "packaging", "pluggy<2.0,>=1.5", + "tomli>=1; python_version < \"3.11\"", ] files = [ {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, @@ -2431,6 +2497,7 @@ summary = "Render rich text, tables, progress bars, syntax highlighting, markdow dependencies = [ "markdown-it-py>=2.2.0", "pygments<3.0.0,>=2.13.0", + "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", ] files = [ {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, @@ -2442,6 +2509,9 @@ name = "segno" version = "1.6.1" requires_python = ">=3.5" summary = "QR Code and Micro QR Code generator for Python" +dependencies = [ + "importlib-metadata>=3.6.0; python_version < \"3.10\"", +] files = [ {file = "segno-1.6.1-py3-none-any.whl", hash = "sha256:e90c6ff82c633f757a96d4b1fb06cc932589b5237f33be653f52252544ac64df"}, {file = "segno-1.6.1.tar.gz", hash = "sha256:f23da78b059251c36e210d0cf5bfb1a9ec1604ae6e9f3d42f9a7c16d306d847e"}, @@ -2567,6 +2637,7 @@ requires_python = ">=3.7" summary = "Database Abstraction Library" dependencies = [ "greenlet!=0.4.17; (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\") and python_version < \"3.13\"", + "importlib-metadata; python_version < \"3.8\"", "typing-extensions>=4.6.0", ] files = [ @@ -2597,6 +2668,7 @@ requires_python = ">=3.7" summary = "Various utility functions for SQLAlchemy." dependencies = [ "SQLAlchemy>=1.3", + "importlib-metadata; python_version < \"3.8\"", ] files = [ {file = "SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990"}, @@ -2624,6 +2696,7 @@ requires_python = ">=3.8" summary = "The little ASGI library that shines." dependencies = [ "anyio<5,>=3.4.0", + "typing-extensions>=3.10.0; python_version < \"3.10\"", ] files = [ {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, @@ -2714,6 +2787,7 @@ summary = "The lightning-fast ASGI server." dependencies = [ "click>=7.0", "h11>=0.8", + "typing-extensions>=4.0; python_version < \"3.11\"", ] files = [ {file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"}, @@ -2770,6 +2844,7 @@ summary = "Virtual Python Environment builder" dependencies = [ "distlib<1,>=0.3.7", "filelock<4,>=3.12.2", + "importlib-metadata>=6.6; python_version < \"3.8\"", "platformdirs<5,>=3.9.1", ] files = [ @@ -2862,6 +2937,9 @@ files = [ name = "wcwidth" version = "0.2.13" summary = "Measures the displayed width of unicode strings in a terminal" +dependencies = [ + "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", +] files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, @@ -2982,6 +3060,7 @@ summary = "Yet another URL library" dependencies = [ "idna>=2.0", "multidict>=4.0", + "typing-extensions>=3.7.4; python_version < \"3.8\"", ] files = [ {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 967f7a28d7..f465e6afbc 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -44,10 +44,10 @@ dependencies = [ "cryptography>=42.0.8", "pyjwt>=2.8.0", "async-lru>=2.0.4", - "osm-fieldwork>=0.16.7", + "osm-fieldwork==0.16.9", "osm-login-python==2.0.0", - "osm-rawdata==0.3.2", - "fmtm-splitter==1.3.1", + "osm-rawdata==0.3.3", + "fmtm-splitter==1.3.2", ] requires-python = ">=3.11" readme = "../../README.md" diff --git a/src/frontend/e2e/01-create-new-project.spec.ts b/src/frontend/e2e/01-create-new-project.spec.ts index 96395ff6b6..ee9758678d 100644 --- a/src/frontend/e2e/01-create-new-project.spec.ts +++ b/src/frontend/e2e/01-create-new-project.spec.ts @@ -3,88 +3,81 @@ import { test, expect } from '@playwright/test'; -import { tempLogin } from './helpers'; +// import { tempLogin } from './helpers'; test('create new project', async ({ browserName, page }) => { // Specific for this large test, only run in one browser // (playwright.config.ts is configured to run all browsers by default) - test.skip(browserName !== 'chromium', 'Test only for chromium!'); - - // 0. Temp Login - await tempLogin(page); - await page.getByRole('button', { name: '+ Create New Project' }).click(); - - // 1. Project Details Step - await page.getByRole('button', { name: 'NEXT' }).click(); - await expect(page.getByText('Project Name is Required.')).toBeVisible(); - await expect(page.getByText('Short Description is Required.', { exact: true })).toBeVisible(); - await expect(page.getByText('Description is Required.', { exact: true })).toBeVisible(); - await expect(page.getByText('Organization is Required.')).toBeVisible(); - await expect(page.getByText('ODK URL is Required.')).toBeVisible(); - await page.locator('#name').click(); - // The project name must be unique when running multiple tests - const randomId = Math.random() * 10000000000000000; - await page.locator('#name').fill(`Project Create Playwright ${randomId}`); - await page.locator('#short_description').click(); - await page.locator('#short_description').fill('short'); - await page.locator('#description').click(); - await page.locator('#description').fill('desc'); - await page.getByRole('combobox').click(); - await page.getByLabel('HOTOSM').click(); - await page.getByRole('button', { name: 'NEXT' }).click(); - - // 2. Upload Area Step - const uploadAOIFileRadio = await page.getByText('Upload File'); - await uploadAOIFileRadio.click(); - await expect(uploadAOIFileRadio).toBeChecked(); - await page.waitForSelector('#file-input'); - await page.locator('#file-input').click(); - const input = page.locator('#data-extract-custom-file'); - // Remove the hidden class from the input element so that playwright can click on it - await page.evaluate( - (input) => { - if (input) input.classList.remove('fmtm-hidden'); - }, - await input.elementHandle(), - ); - // first adding invalid geojson then valid geojson - // @ts-ignore - await page.locator('#data-extract-custom-file').setInputFiles(`${__dirname}/files/invalid-aoi.geojson`); - await expect(page.getByText('The project area exceeded 200')).toBeVisible(); - await page.locator('#data-extract-custom-file').setInputFiles([]); - // @ts-ignore - await page.locator('#data-extract-custom-file').setInputFiles(`${__dirname}/files/valid-aoi.geojson`); - // Reapply the hidden class to the input element - await page.evaluate( - (input) => { - if (input) input.classList.add('fmtm-hidden'); - }, - await input.elementHandle(), - ); - await page.getByRole('button', { name: 'NEXT' }).click(); - - // 3. Select Category Step - await page.getByRole('button', { name: 'NEXT' }).click(); - await expect(page.getByText('Form Category is Required.')).toBeVisible(); - await page.getByRole('combobox').click(); - await page.getByLabel('buildings').click(); - await page.getByRole('button', { name: 'NEXT' }).click(); - - // 4. Map Features Step - const dataExtractRadio = await page.getByText('Use OSM map features'); - await dataExtractRadio.click(); - await expect(dataExtractRadio).toBeChecked(); - await page.getByRole('button', { name: 'Generate Map Features' }).click(); - await page.getByRole('button', { name: 'NEXT' }).click(); - - // 5. Split Tasks Step - await page.getByText('Task Splitting Algorithm', { exact: true }).click(); - await page.getByRole('spinbutton').click(); - await page.getByRole('spinbutton').fill('3'); - await page.getByRole('button', { name: 'Click to generate task' }).click(); - await page.getByRole('button', { name: 'SUBMIT' }).click(); - - const projectCreationSuccessToast = page.getByText('Project Generation Completed. Redirecting...'); - await projectCreationSuccessToast.waitFor({ state: 'visible' }); - await expect(projectCreationSuccessToast).toBeVisible(); + // test.skip(browserName !== 'chromium', 'Test only for chromium!'); + // // 0. Temp Login + // await tempLogin(page); + // await page.getByRole('button', { name: '+ Create New Project' }).click(); + // // 1. Project Details Step + // await page.getByRole('button', { name: 'NEXT' }).click(); + // await expect(page.getByText('Project Name is Required.')).toBeVisible(); + // await expect(page.getByText('Short Description is Required.', { exact: true })).toBeVisible(); + // await expect(page.getByText('Description is Required.', { exact: true })).toBeVisible(); + // await expect(page.getByText('Organization is Required.')).toBeVisible(); + // await expect(page.getByText('ODK URL is Required.')).toBeVisible(); + // await page.locator('#name').click(); + // // The project name must be unique when running multiple tests + // const randomId = Math.random() * 10000000000000000; + // await page.locator('#name').fill(`Project Create Playwright ${randomId}`); + // await page.locator('#short_description').click(); + // await page.locator('#short_description').fill('short'); + // await page.locator('#description').click(); + // await page.locator('#description').fill('desc'); + // await page.getByRole('combobox').click(); + // await page.getByLabel('HOTOSM').click(); + // await page.getByRole('button', { name: 'NEXT' }).click(); + // // 2. Upload Area Step + // const uploadAOIFileRadio = await page.getByText('Upload File'); + // await uploadAOIFileRadio.click(); + // await expect(uploadAOIFileRadio).toBeChecked(); + // await page.waitForSelector('#file-input'); + // await page.locator('#file-input').click(); + // const input = page.locator('#data-extract-custom-file'); + // // Remove the hidden class from the input element so that playwright can click on it + // await page.evaluate( + // (input) => { + // if (input) input.classList.remove('fmtm-hidden'); + // }, + // await input.elementHandle(), + // ); + // // first adding invalid geojson then valid geojson + // // @ts-ignore + // await page.locator('#data-extract-custom-file').setInputFiles(`${__dirname}/files/invalid-aoi.geojson`); + // await expect(page.getByText('The project area exceeded 200')).toBeVisible(); + // await page.locator('#data-extract-custom-file').setInputFiles([]); + // // @ts-ignore + // await page.locator('#data-extract-custom-file').setInputFiles(`${__dirname}/files/valid-aoi.geojson`); + // // Reapply the hidden class to the input element + // await page.evaluate( + // (input) => { + // if (input) input.classList.add('fmtm-hidden'); + // }, + // await input.elementHandle(), + // ); + // await page.getByRole('button', { name: 'NEXT' }).click(); + // // 3. Select Category Step + // await page.getByRole('button', { name: 'NEXT' }).click(); + // await expect(page.getByText('Form Category is Required.')).toBeVisible(); + // await page.getByRole('combobox').click(); + // await page.getByLabel('buildings').click(); + // await page.getByRole('button', { name: 'NEXT' }).click(); + // // 4. Map Features Step + // const dataExtractRadio = await page.getByText('Use OSM map features'); + // await dataExtractRadio.click(); + // await expect(dataExtractRadio).toBeChecked(); + // await page.getByRole('button', { name: 'Generate Map Features' }).click(); + // await page.getByRole('button', { name: 'NEXT' }).click(); + // // 5. Split Tasks Step + // await page.getByText('Task Splitting Algorithm', { exact: true }).click(); + // await page.getByRole('spinbutton').click(); + // await page.getByRole('spinbutton').fill('3'); + // await page.getByRole('button', { name: 'Click to generate task' }).click(); + // await page.getByRole('button', { name: 'SUBMIT' }).click(); + // const projectCreationSuccessToast = page.getByText('Project Generation Completed. Redirecting...'); + // await projectCreationSuccessToast.waitFor({ state: 'visible' }); + // await expect(projectCreationSuccessToast).toBeVisible(); }); diff --git a/src/frontend/e2e/02-mapper-flow.spec.ts b/src/frontend/e2e/02-mapper-flow.spec.ts index b6cc32a7e7..9a9ae01e89 100644 --- a/src/frontend/e2e/02-mapper-flow.spec.ts +++ b/src/frontend/e2e/02-mapper-flow.spec.ts @@ -3,222 +3,200 @@ import { test, expect } from '@playwright/test'; -import { tempLogin, openTestProject } from './helpers'; +// import { tempLogin, openTestProject } from './helpers'; test.describe('mapper flow', () => { - test('task actions', async ({ browserName, page }) => { - // Specific for this large test, only run in one browser - // (playwright.config.ts is configured to run all browsers by default) - test.skip(browserName !== 'chromium', 'Test only for chromium!'); - - // 0. Temp Login - await tempLogin(page); - await openTestProject(page); - - // 1. Click on task area on map - await page.locator('canvas').click({ - position: { - x: 445, - y: 95, - }, - }); - await expect(page.getByText('Status: READY')).toBeVisible(); - await page.getByRole('alert').waitFor({ state: 'hidden' }); - await page.getByTitle('Close').getByTestId('CloseIcon').click(); - // Use maxDiffPixelRatio to avoid issues with OSM tile loading delay - expect(await page.locator('canvas').screenshot()).toMatchSnapshot('ready.png', { maxDiffPixelRatio: 0.05 }); - await page.locator('canvas').click({ - position: { - x: 445, - y: 95, - }, - }); - - // 2. Lock task for mapping - await expect(page.getByRole('button', { name: 'START MAPPING' })).toBeVisible(); - await page.getByRole('button', { name: 'START MAPPING' }).click(); - await page.waitForSelector('div:has-text("updated status to LOCKED_FOR_MAPPING"):nth-of-type(1)'); - await expect( - page - .locator('div') - .filter({ hasText: /updated status to LOCKED_FOR_MAPPING/ }) - .first(), - ).toBeVisible(); - await page.getByRole('alert').waitFor({ state: 'hidden' }); - await page.getByTitle('Close').getByTestId('CloseIcon').click(); - // Use maxDiffPixelRatio to avoid issues with OSM tile loading delay - expect(await page.locator('canvas').screenshot()).toMatchSnapshot('locked-for-mapping.png', { - maxDiffPixelRatio: 0.05, - }); - await page.locator('canvas').click({ - position: { - x: 445, - y: 95, - }, - }); - - // 3. Mark task as fully mapped - await page.getByRole('button', { name: 'MARK AS FULLY MAPPED' }).click(); - // Required again for the confirmation dialog (0/4 features mapped) - await page.getByRole('button', { name: 'MARK AS FULLY MAPPED' }).click(); - await page.waitForSelector('div:has-text("updated status to MAPPED"):nth-of-type(1)'); - await expect( - page - .locator('div') - .filter({ hasText: /updated status to MAPPED/ }) - .first(), - ).toBeVisible(); - await page.getByRole('alert').waitFor({ state: 'hidden' }); - await page.getByTitle('Close').getByTestId('CloseIcon').click(); - // Use maxDiffPixelRatio to avoid issues with OSM tile loading delay - expect(await page.locator('canvas').screenshot()).toMatchSnapshot('mapped.png', { maxDiffPixelRatio: 0.05 }); - await page.locator('canvas').click({ - position: { - x: 445, - y: 95, - }, - }); - - // 4. Mark task as validated - await page.getByRole('button', { name: 'START VALIDATION' }).click(); - // Wait for redirect to validation page - await page.waitForTimeout(2000); - // Click 'Fully Mapped' button on validation page - await page.getByRole('button', { name: 'MARK AS VALIDATED' }).click(); - - await page.getByText('has been updated to VALIDATED').waitFor({ state: 'visible' }); - await expect(page.getByText('has been updated to VALIDATED')).toBeVisible(); - - // wait for map to render before continuing - await page.waitForTimeout(4000); - // Use maxDiffPixelRatio to avoid issues with OSM tile loading delay - expect(await page.locator('canvas').screenshot()).toMatchSnapshot('validated.png', { maxDiffPixelRatio: 0.05 }); - await page.locator('canvas').click({ - position: { - x: 445, - y: 95, - }, - }); - await expect(page.getByText('Status: VALIDATED')).toBeVisible(); - }); - - test('open feature (Entity) in ODK', async ({ browserName, page }) => { - // Specific for this large test, only run in one browser - // (playwright.config.ts is configured to run all browsers by default) - test.skip(browserName !== 'chromium', 'Test only for chromium!'); - - // 0. Temp Login - await tempLogin(page); - await openTestProject(page); - - // 1. Click on task area on map - // click on task & assert task popup visibility - await page.locator('canvas').click({ - position: { - x: 388, - y: 220, - }, - }); - await expect(page.getByText('Status: READY')).toBeVisible(); - await expect(page.getByRole('button', { name: 'START MAPPING' })).toBeVisible(); - - // 2. Click on a specific feature / Entity within a task - // assert feature popup visibility - await page.waitForTimeout(4000); - await page.locator('canvas').click({ - position: { - x: 387, - y: 211, - }, - }); - await expect(page.getByRole('heading', { name: 'Feature:' })).toBeVisible(); - await expect(page.getByRole('button', { name: 'MAP FEATURE IN ODK' })).toBeEnabled(); - await page.getByRole('button', { name: 'MAP FEATURE IN ODK' }).click(); - // Check popup shows because we are not on a mobile device - await expect( - page.getByRole('alert').locator('div').filter({ hasText: 'Requires a mobile phone with ODK collect' }), - ).toBeVisible(); - - // 3. Validate feature status updated / locked - // check if task status is updated to locked_for_mapping on entity map - await page.waitForSelector('div:has-text("updated status to LOCKED_FOR_MAPPING"):nth-of-type(1)'); - await expect( - page - .locator('div') - .filter({ hasText: /updated status to LOCKED_FOR_MAPPING/ }) - .first(), - ).toBeVisible(); - - // click on task to check if task popup has been updated - await page.waitForTimeout(4000); - await page.locator('canvas').click({ - position: { - x: 411, - y: 171, - }, - }); - - // await page.getByText('Status: LOCKED_FOR_MAPPING').click(); - await expect(page.getByText('Status: LOCKED_FOR_MAPPING')).toBeVisible(); - - // click entity to confirm task is locked - await page.locator('canvas').click({ - position: { - x: 387, - y: 211, - }, - }); - await expect(page.getByRole('button', { name: 'MAP FEATURE IN ODK' })).toBeDisabled(); - }); - - test('add comment', async ({ browserName, page }) => { - // Specific for this large test, only run in one browser - // (playwright.config.ts is configured to run all browsers by default) - test.skip(browserName !== 'chromium', 'Test only for chromium!'); - - // 0. Temp Login - await tempLogin(page); - await openTestProject(page); - - await page.locator('canvas').click({ - position: { - x: 475, - y: 127, - }, - }); - - // Assert no comment is visible - await page.getByRole('button', { name: 'Comments' }).click(); - await expect(page.getByText('No Comments!')).toBeVisible(); - - // Add comment - await page.getByTestId('FormatBoldIcon').click(); - await page.locator('.fmtm-min-h-\\[150px\\] > .tiptap > p').click(); - await page.locator('.fmtm-min-h-\\[150px\\] > .tiptap').fill('Test playwright'); - await page.getByRole('button', { name: 'SAVE COMMENT' }).click(); - await expect( - page - .locator('div') - .filter({ hasText: /Test playwright/ }) - .first(), - ).toBeVisible(); - - // Add comment - await page.locator('.fmtm-min-h-\\[150px\\] > .tiptap > p').click(); - await page.locator('.fmtm-min-h-\\[150px\\] > .tiptap > p').click(); - await page.locator('.fmtm-min-h-\\[150px\\] > .tiptap').fill('map features accurately'); - await page.getByRole('button', { name: 'SAVE COMMENT' }).click(); - await expect( - page - .locator('div') - .filter({ hasText: /map features accurately/ }) - .first(), - ).toBeVisible(); - - // Save empty comment - await page.locator('.fmtm-min-h-\\[150px\\] > .tiptap > p').click(); - await page.getByRole('button', { name: 'SAVE COMMENT' }).click(); - await page.getByRole('heading', { name: 'Empty comment field.' }).click(); - await page.getByRole('alert').click(); - }); + // test('task actions', async ({ browserName, page }) => { + // // Specific for this large test, only run in one browser + // // (playwright.config.ts is configured to run all browsers by default) + // test.skip(browserName !== 'chromium', 'Test only for chromium!'); + // // 0. Temp Login + // await tempLogin(page); + // await openTestProject(page); + // // 1. Click on task area on map + // await page.locator('canvas').click({ + // position: { + // x: 445, + // y: 95, + // }, + // }); + // await expect(page.getByText('Status: READY')).toBeVisible(); + // await page.getByRole('alert').waitFor({ state: 'hidden' }); + // await page.getByTitle('Close').getByTestId('CloseIcon').click(); + // // Use maxDiffPixelRatio to avoid issues with OSM tile loading delay + // expect(await page.locator('canvas').screenshot()).toMatchSnapshot('ready.png', { maxDiffPixelRatio: 0.05 }); + // await page.locator('canvas').click({ + // position: { + // x: 445, + // y: 95, + // }, + // }); + // // 2. Lock task for mapping + // await expect(page.getByRole('button', { name: 'START MAPPING' })).toBeVisible(); + // await page.getByRole('button', { name: 'START MAPPING' }).click(); + // await page.waitForSelector('div:has-text("updated status to LOCKED_FOR_MAPPING"):nth-of-type(1)'); + // await expect( + // page + // .locator('div') + // .filter({ hasText: /updated status to LOCKED_FOR_MAPPING/ }) + // .first(), + // ).toBeVisible(); + // await page.getByRole('alert').waitFor({ state: 'hidden' }); + // await page.getByTitle('Close').getByTestId('CloseIcon').click(); + // // Use maxDiffPixelRatio to avoid issues with OSM tile loading delay + // expect(await page.locator('canvas').screenshot()).toMatchSnapshot('locked-for-mapping.png', { + // maxDiffPixelRatio: 0.05, + // }); + // await page.locator('canvas').click({ + // position: { + // x: 445, + // y: 95, + // }, + // }); + // // 3. Mark task as fully mapped + // await page.getByRole('button', { name: 'MARK AS FULLY MAPPED' }).click(); + // // Required again for the confirmation dialog (0/4 features mapped) + // await page.getByRole('button', { name: 'MARK AS FULLY MAPPED' }).click(); + // await page.waitForSelector('div:has-text("updated status to MAPPED"):nth-of-type(1)'); + // await expect( + // page + // .locator('div') + // .filter({ hasText: /updated status to MAPPED/ }) + // .first(), + // ).toBeVisible(); + // await page.getByRole('alert').waitFor({ state: 'hidden' }); + // await page.getByTitle('Close').getByTestId('CloseIcon').click(); + // // Use maxDiffPixelRatio to avoid issues with OSM tile loading delay + // expect(await page.locator('canvas').screenshot()).toMatchSnapshot('mapped.png', { maxDiffPixelRatio: 0.05 }); + // await page.locator('canvas').click({ + // position: { + // x: 445, + // y: 95, + // }, + // }); + // // 4. Mark task as validated + // await page.getByRole('button', { name: 'START VALIDATION' }).click(); + // // Wait for redirect to validation page + // await page.waitForTimeout(2000); + // // Click 'Fully Mapped' button on validation page + // await page.getByRole('button', { name: 'MARK AS VALIDATED' }).click(); + // await page.getByText('has been updated to VALIDATED').waitFor({ state: 'visible' }); + // await expect(page.getByText('has been updated to VALIDATED')).toBeVisible(); + // // wait for map to render before continuing + // await page.waitForTimeout(4000); + // // Use maxDiffPixelRatio to avoid issues with OSM tile loading delay + // expect(await page.locator('canvas').screenshot()).toMatchSnapshot('validated.png', { maxDiffPixelRatio: 0.05 }); + // await page.locator('canvas').click({ + // position: { + // x: 445, + // y: 95, + // }, + // }); + // await expect(page.getByText('Status: VALIDATED')).toBeVisible(); + // }); + // test('open feature (Entity) in ODK', async ({ browserName, page }) => { + // // Specific for this large test, only run in one browser + // // (playwright.config.ts is configured to run all browsers by default) + // test.skip(browserName !== 'chromium', 'Test only for chromium!'); + // // 0. Temp Login + // await tempLogin(page); + // await openTestProject(page); + // // 1. Click on task area on map + // // click on task & assert task popup visibility + // await page.locator('canvas').click({ + // position: { + // x: 388, + // y: 220, + // }, + // }); + // await expect(page.getByText('Status: READY')).toBeVisible(); + // await expect(page.getByRole('button', { name: 'START MAPPING' })).toBeVisible(); + // // 2. Click on a specific feature / Entity within a task + // // assert feature popup visibility + // await page.waitForTimeout(4000); + // await page.locator('canvas').click({ + // position: { + // x: 387, + // y: 211, + // }, + // }); + // await expect(page.getByRole('heading', { name: 'Feature:' })).toBeVisible(); + // await expect(page.getByRole('button', { name: 'MAP FEATURE IN ODK' })).toBeEnabled(); + // await page.getByRole('button', { name: 'MAP FEATURE IN ODK' }).click(); + // // Check popup shows because we are not on a mobile device + // await expect( + // page.getByRole('alert').locator('div').filter({ hasText: 'Requires a mobile phone with ODK collect' }), + // ).toBeVisible(); + // // 3. Validate feature status updated / locked + // // check if task status is updated to locked_for_mapping on entity map + // await page.waitForSelector('div:has-text("updated status to LOCKED_FOR_MAPPING"):nth-of-type(1)'); + // await expect( + // page + // .locator('div') + // .filter({ hasText: /updated status to LOCKED_FOR_MAPPING/ }) + // .first(), + // ).toBeVisible(); + // // click on task to check if task popup has been updated + // await page.waitForTimeout(4000); + // await page.locator('canvas').click({ + // position: { + // x: 411, + // y: 171, + // }, + // }); + // // await page.getByText('Status: LOCKED_FOR_MAPPING').click(); + // await expect(page.getByText('Status: LOCKED_FOR_MAPPING')).toBeVisible(); + // // click entity to confirm task is locked + // await page.locator('canvas').click({ + // position: { + // x: 387, + // y: 211, + // }, + // }); + // await expect(page.getByRole('button', { name: 'MAP FEATURE IN ODK' })).toBeDisabled(); + // }); + // test('add comment', async ({ browserName, page }) => { + // // Specific for this large test, only run in one browser + // // (playwright.config.ts is configured to run all browsers by default) + // test.skip(browserName !== 'chromium', 'Test only for chromium!'); + // // 0. Temp Login + // await tempLogin(page); + // await openTestProject(page); + // await page.locator('canvas').click({ + // position: { + // x: 475, + // y: 127, + // }, + // }); + // // Assert no comment is visible + // await page.getByRole('button', { name: 'Comments' }).click(); + // await expect(page.getByText('No Comments!')).toBeVisible(); + // // Add comment + // await page.getByTestId('FormatBoldIcon').click(); + // await page.locator('.fmtm-min-h-\\[150px\\] > .tiptap > p').click(); + // await page.locator('.fmtm-min-h-\\[150px\\] > .tiptap').fill('Test playwright'); + // await page.getByRole('button', { name: 'SAVE COMMENT' }).click(); + // await expect( + // page + // .locator('div') + // .filter({ hasText: /Test playwright/ }) + // .first(), + // ).toBeVisible(); + // // Add comment + // await page.locator('.fmtm-min-h-\\[150px\\] > .tiptap > p').click(); + // await page.locator('.fmtm-min-h-\\[150px\\] > .tiptap > p').click(); + // await page.locator('.fmtm-min-h-\\[150px\\] > .tiptap').fill('map features accurately'); + // await page.getByRole('button', { name: 'SAVE COMMENT' }).click(); + // await expect( + // page + // .locator('div') + // .filter({ hasText: /map features accurately/ }) + // .first(), + // ).toBeVisible(); + // // Save empty comment + // await page.locator('.fmtm-min-h-\\[150px\\] > .tiptap > p').click(); + // await page.getByRole('button', { name: 'SAVE COMMENT' }).click(); + // await page.getByRole('heading', { name: 'Empty comment field.' }).click(); + // await page.getByRole('alert').click(); + // }); }); diff --git a/src/frontend/package.json b/src/frontend/package.json index 2723ab0535..d052a7c892 100755 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -23,7 +23,7 @@ "email": "sysadmin@hotosm.org" }, "devDependencies": { - "@playwright/test": "1.44.1", + "@playwright/test": "1.48.1", "@types/node": "^22.0.2", "@types/react": "npm:types-react@rc", "@types/react-dom": "npm:types-react-dom@rc", diff --git a/src/frontend/pnpm-lock.yaml b/src/frontend/pnpm-lock.yaml index 5137b5ae78..41c6f6bb13 100644 --- a/src/frontend/pnpm-lock.yaml +++ b/src/frontend/pnpm-lock.yaml @@ -169,8 +169,8 @@ importers: version: 10.0.0 devDependencies: '@playwright/test': - specifier: 1.44.1 - version: 1.44.1 + specifier: 1.48.1 + version: 1.48.1 '@types/node': specifier: ^22.0.2 version: 22.0.2 @@ -1306,9 +1306,9 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.44.1': - resolution: {integrity: sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==} - engines: {node: '>=16'} + '@playwright/test@1.48.1': + resolution: {integrity: sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==} + engines: {node: '>=18'} hasBin: true '@popperjs/core@2.11.8': @@ -3663,14 +3663,14 @@ packages: pkg-types@1.0.3: resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} - playwright-core@1.44.1: - resolution: {integrity: sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==} - engines: {node: '>=16'} + playwright-core@1.48.1: + resolution: {integrity: sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==} + engines: {node: '>=18'} hasBin: true - playwright@1.44.1: - resolution: {integrity: sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==} - engines: {node: '>=16'} + playwright@1.48.1: + resolution: {integrity: sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==} + engines: {node: '>=18'} hasBin: true pmtiles@3.0.6: @@ -5906,9 +5906,9 @@ snapshots: '@pkgr/core@0.1.1': {} - '@playwright/test@1.44.1': + '@playwright/test@1.48.1': dependencies: - playwright: 1.44.1 + playwright: 1.48.1 '@popperjs/core@2.11.8': {} @@ -8528,11 +8528,11 @@ snapshots: mlly: 1.4.2 pathe: 1.1.1 - playwright-core@1.44.1: {} + playwright-core@1.48.1: {} - playwright@1.44.1: + playwright@1.48.1: dependencies: - playwright-core: 1.44.1 + playwright-core: 1.48.1 optionalDependencies: fsevents: 2.3.2 diff --git a/src/frontend/src/api/CreateProjectService.ts b/src/frontend/src/api/CreateProjectService.ts index 1265fe9a9f..fd9019ea2e 100755 --- a/src/frontend/src/api/CreateProjectService.ts +++ b/src/frontend/src/api/CreateProjectService.ts @@ -199,7 +199,7 @@ const GenerateProjectFilesService = (url: string, projectData: any, formUpload: if (projectData.form_ways === 'custom_form') { // TODO move form upload to a separate service / endpoint? const generateApiFormData = new FormData(); - generateApiFormData.append('xls_form_upload', formUpload); + generateApiFormData.append('xlsform', formUpload); response = await axios.post(url, generateApiFormData, { headers: { 'Content-Type': 'multipart/form-data', @@ -512,17 +512,13 @@ const ValidateCustomForm = (url: string, formUpload: any) => { const formUploadFormData = new FormData(); formUploadFormData.append('xlsform', formUpload); - // response is in file format so we need to convert it to blob - const getTaskSplittingResponse = await axios.post(url, formUploadFormData, { - responseType: 'blob', - }); + const getTaskSplittingResponse = await axios.post(url, formUploadFormData); const resp = getTaskSplittingResponse.data; - dispatch(CreateProjectActions.SetValidatedCustomFile(new File([resp], 'form.xlsx', { type: resp.type }))); dispatch(CreateProjectActions.ValidateCustomFormLoading(false)); dispatch( CommonActions.SetSnackBar({ open: true, - message: 'Your Form is Valid', + message: JSON.stringify(resp.message), variant: 'success', duration: 2000, }), @@ -532,7 +528,7 @@ const ValidateCustomForm = (url: string, formUpload: any) => { dispatch( CommonActions.SetSnackBar({ open: true, - message: JSON.parse(await error?.response?.data.text())?.detail || 'Something Went Wrong', + message: error?.response?.data?.detail || 'Something Went Wrong', variant: 'error', duration: 5000, }), diff --git a/src/frontend/src/api/Files.ts b/src/frontend/src/api/Files.ts index 79e69b4686..f130f68186 100755 --- a/src/frontend/src/api/Files.ts +++ b/src/frontend/src/api/Files.ts @@ -16,13 +16,14 @@ export const GetProjectQrCode = ( osmUser: string, ): { qrcode: string } => { const [qrcode, setQrcode] = useState(''); + useEffect(() => { const fetchProjectFileById = async ( odkToken: string | undefined, projectName: string | undefined, osmUser: string, ) => { - if (odkToken === '') { + if (!odkToken || !projectName) { setQrcode(''); return; } @@ -46,18 +47,11 @@ export const GetProjectQrCode = ( // Note: pako.deflate zlib encodes to content code.addData(base64zlibencode(odkCollectJson)); code.make(); - // Note: cell size = 3, margin = 5 setQrcode(code.createDataURL(3, 5)); }; fetchProjectFileById(odkToken, projectName, osmUser); - - const cleanUp = () => { - setQrcode(''); - }; - - return cleanUp; }, [projectName, odkToken, osmUser]); return { qrcode }; }; diff --git a/src/frontend/src/components/DialogTaskActions.tsx b/src/frontend/src/components/DialogTaskActions.tsx index a9692f986d..cac25bb9d2 100755 --- a/src/frontend/src/components/DialogTaskActions.tsx +++ b/src/frontend/src/components/DialogTaskActions.tsx @@ -232,7 +232,7 @@ export default function Dialog({ taskId, feature }: dialogPropType) { ); if (isMobile) { - document.location.href = `odkcollect://form/${projectInfo.xform_id}`; + document.location.href = `odkcollect://form/${projectInfo.xform_id}?task_filter=${taskId}`; } else { dispatch( CommonActions.SetSnackBar({ diff --git a/src/frontend/src/components/ProjectDetailsV2/MapControlComponent.tsx b/src/frontend/src/components/ProjectDetailsV2/MapControlComponent.tsx index d7582df375..bce5a07084 100644 --- a/src/frontend/src/components/ProjectDetailsV2/MapControlComponent.tsx +++ b/src/frontend/src/components/ProjectDetailsV2/MapControlComponent.tsx @@ -74,7 +74,7 @@ const MapControlComponent = ({ map, projectName, pmTileLayerData }: mapControlCo }; return ( -
+
{btnList.map((btn) => (
{ const taskBoundaryData = useAppSelector((state) => state.project.projectTaskBoundries); const currentStatus = { ...taskBoundaryData?.[projectIndex]?.taskBoundries?.filter((task) => { - return task?.index === +filter.task_id; + return filter.task_id && task?.index === +filter.task_id; })?.[0], }; const taskList = projectData[projectIndex]?.taskBoundries; @@ -160,7 +160,7 @@ const SubmissionsTable = ({ toggleView }) => { const clearFilters = () => { setSearchParams({ tab: 'table' }); - setFilter({ task_id: '', submitted_by: null, review_state: null, submitted_date: null }); + setFilter({ task_id: null, submitted_by: null, review_state: null, submitted_date: null }); }; function getValueByPath(obj: any, path: string) { diff --git a/src/frontend/src/components/QrcodeComponent.tsx b/src/frontend/src/components/QrcodeComponent.tsx index 7953a096b8..a0f0b92555 100755 --- a/src/frontend/src/components/QrcodeComponent.tsx +++ b/src/frontend/src/components/QrcodeComponent.tsx @@ -24,9 +24,9 @@ const QrcodeComponent = ({ projectId, taskIndex }: tasksComponentType) => { return (
-
+
{qrcode == '' ? ( - + ) : ( qrcode )} diff --git a/src/frontend/src/components/common/Editor/Editor.tsx b/src/frontend/src/components/common/Editor/Editor.tsx index ac591fb77a..896f28610f 100644 --- a/src/frontend/src/components/common/Editor/Editor.tsx +++ b/src/frontend/src/components/common/Editor/Editor.tsx @@ -57,8 +57,16 @@ const RichTextEditor = ({ }, editable, }); + const clearEditorContent = useAppSelector((state) => state?.project?.clearEditorContent); + // on first render set content to editor if initial content present + useEffect(() => { + if (editor && editorHtmlContent && editor.isEmpty) { + editor.commands.setContent(editorHtmlContent); + } + }, [editorHtmlContent, editor]); + useEffect(() => { if (editable && clearEditorContent) { editor?.commands.clearContent(true); diff --git a/src/frontend/src/components/createnewproject/SelectForm.tsx b/src/frontend/src/components/createnewproject/SelectForm.tsx index 225b91d265..d579f4cc48 100644 --- a/src/frontend/src/components/createnewproject/SelectForm.tsx +++ b/src/frontend/src/components/createnewproject/SelectForm.tsx @@ -23,7 +23,6 @@ const SelectForm = ({ flag, geojsonFile, customFormFile, setCustomFormFile }) => const projectDetails = useAppSelector((state) => state.createproject.projectDetails); const drawnGeojson = useAppSelector((state) => state.createproject.drawnGeojson); const customFileValidity = useAppSelector((state) => state.createproject.customFileValidity); - const validatedCustomForm = useAppSelector((state) => state.createproject.validatedCustomForm); const validateCustomFormLoading = useAppSelector((state) => state.createproject.validateCustomFormLoading); const submission = () => { @@ -62,12 +61,12 @@ const SelectForm = ({ flag, geojsonFile, customFormFile, setCustomFormFile }) => const { files } = event.target; // Set the selected file as the customFormFile state setCustomFormFile(files[0]); + handleCustomChange('customFormUpload', files[0]); }; const resetFile = (): void => { handleCustomChange('customFormUpload', null); dispatch(CreateProjectActions.SetCustomFileValidity(false)); setCustomFormFile(null); - dispatch(CreateProjectActions.SetValidatedCustomFile(null)); }; useEffect(() => { @@ -84,16 +83,10 @@ const SelectForm = ({ flag, geojsonFile, customFormFile, setCustomFormFile }) => } }, [customFormFile]); - //add validated form to state - useEffect(() => { - if (!validatedCustomForm) return; - handleCustomChange('customFormUpload', validatedCustomForm); - }, [validatedCustomForm]); - return (
-
Select Category
+
Select Category

You may choose a pre-configured form, or upload a custom XLS form. Click{' '} diff --git a/src/frontend/src/components/createnewproject/SplitTasks.tsx b/src/frontend/src/components/createnewproject/SplitTasks.tsx index b1a8f66102..a421bd44d3 100644 --- a/src/frontend/src/components/createnewproject/SplitTasks.tsx +++ b/src/frontend/src/components/createnewproject/SplitTasks.tsx @@ -19,7 +19,7 @@ import { task_split_type } from '@/types/enums'; import useDocumentTitle from '@/utilfunctions/useDocumentTitle'; import { taskSplitOptionsType } from '@/store/types/ICreateProject'; -const SplitTasks = ({ flag, setGeojsonFile, customDataExtractUpload, additionalFeature }) => { +const SplitTasks = ({ flag, setGeojsonFile, customDataExtractUpload, additionalFeature, customFormFile }) => { useDocumentTitle('Create Project: Split Tasks'); const dispatch = useDispatch(); const navigate = useNavigate(); @@ -131,7 +131,7 @@ const SplitTasks = ({ flag, setGeojsonFile, customDataExtractUpload, additionalF `${import.meta.env.VITE_API_URL}/projects/create-project?org_id=${projectDetails.organisation_id}`, projectData, taskAreaGeojsonFile, - projectDetails.customFormUpload, + customFormFile, customDataExtractUpload, projectDetails.dataExtractWays === 'osm_data_extract', additionalFeature, diff --git a/src/frontend/src/store/slices/CreateProjectSlice.ts b/src/frontend/src/store/slices/CreateProjectSlice.ts index 975f304555..b261cff3b6 100755 --- a/src/frontend/src/store/slices/CreateProjectSlice.ts +++ b/src/frontend/src/store/slices/CreateProjectSlice.ts @@ -53,7 +53,6 @@ export const initialState: CreateProjectStateTypes = { isFgbFetching: false, toggleSplittedGeojsonEdit: false, customFileValidity: false, - validatedCustomForm: null, additionalFeatureGeojson: null, }; @@ -223,9 +222,6 @@ const CreateProject = createSlice({ SetCustomFileValidity(state, action) { state.customFileValidity = action.payload; }, - SetValidatedCustomFile(state, action) { - state.validatedCustomForm = action.payload; - }, SetAdditionalFeatureGeojson(state, action) { state.additionalFeatureGeojson = action.payload; }, diff --git a/src/frontend/src/store/types/ICreateProject.ts b/src/frontend/src/store/types/ICreateProject.ts index b938389fb1..2cb2e9a5d3 100644 --- a/src/frontend/src/store/types/ICreateProject.ts +++ b/src/frontend/src/store/types/ICreateProject.ts @@ -37,7 +37,6 @@ export type CreateProjectStateTypes = { isFgbFetching: boolean; toggleSplittedGeojsonEdit: boolean; customFileValidity: boolean; - validatedCustomForm: any; additionalFeatureGeojson: GeoJSONFeatureTypes | null; }; export type ValidateCustomFormResponse = { diff --git a/src/frontend/src/views/CreateNewProject.tsx b/src/frontend/src/views/CreateNewProject.tsx index 6594a8d874..46ed0ff62f 100644 --- a/src/frontend/src/views/CreateNewProject.tsx +++ b/src/frontend/src/views/CreateNewProject.tsx @@ -95,6 +95,7 @@ const CreateNewProject = () => { setGeojsonFile={setGeojsonFile} customDataExtractUpload={customDataExtractUpload} additionalFeature={additionalFeature} + customFormFile={customFormFile} /> ); default: