**Q:** Do I need to create an account to use the FMTM Web App?
@@ -14,6 +15,7 @@ humanitarian organizations.
**A:** No, you can use the FMTM Web App without creating an account, but
creating an account allows you to contribute to mapping projects and
access additional features.
+
**Q:** How do I browse and select mapping projects on the FMTM Web App?
@@ -22,6 +24,7 @@ access additional features.
clicking on the "Projects" tab and selecting a project of
interest. You can view project details, tasks, and mapping data on the
project page.
+
**Q:** How do I contribute to a mapping project on the FMTM Web App?
@@ -30,6 +33,7 @@ project page.
create an account, select a project of interest, and choose a task to
work on. You can then use the mapping tools provided to complete the
task.
+
**Q:** Can I work on multiple mapping tasks at the same time on the FMTM Web App?
@@ -37,6 +41,7 @@ task.
**A:** Yes, you can work on multiple mapping tasks at the same time on the
FMTM Web App, as long as you can commit the necessary time and effort
to each task.
+
**Q:** How do I know if my mapping work on the FMTM Web App is accurate?
@@ -44,6 +49,7 @@ to each task.
**A:** The FMTM Web App has a validation process where other contributors
review and validate the mapping work. This helps to ensure the
accuracy of the mapping data.
+
**Q:** Can I provide feedback on a mapping project on the FMTM Web App?
@@ -51,6 +57,7 @@ accuracy of the mapping data.
**A:** Yes, you can provide feedback on a mapping project on the FMTM Web
App by leaving a comment on the project page or contacting the project
manager.
+
**Q:** How do I download mapping data from a project on the FMTM Web App?
@@ -58,6 +65,7 @@ manager.
**A:** To download mapping data from a project on the FMTM Web App, you
can select the project of interest and click on the "Export" button on
the project page.
+
**Q:** Can I use the mapping data from the FMTM Web App for my own research or projects?
@@ -66,6 +74,7 @@ the project page.
available for use, but it is important to check the specific project
requirements and licenses before using the data for your own research
or projects.
+
### For Contributors
@@ -75,12 +84,14 @@ or projects.
**A:** The Field Mapping Tasking Manager (FMTM) is an online platform that
allows contributors to participate in mapping projects related to
humanitarian and development work.
+
**Q:** How can I become a contributor to the FMTM?
**A:** To become a contributor to the FMTM, you can create an account on
the platform and join a mapping project.
+
**Q:** Who can contribute to FMTM?
@@ -88,6 +99,7 @@ the platform and join a mapping project.
**A:** Anyone can contribute to FMTM. It is an open-source project, and
contributions from developers, designers, and other contributors are
always welcome.
+
**Q:** What kind of contributions can I make to FMTM?
@@ -113,6 +125,7 @@ improvements.
Translation: If you are fluent in a language other than English, you
can contribute by translating the application or its documentation
into your language.
+
**Q:** What technologies are used in FMTM?
@@ -120,6 +133,7 @@ into your language.
**A:** FMTM is built using several technologies, including Django,
Postgres, Redis, Celery, and Vue.js. The codebase is written in
Python, HTML, CSS, and JavaScript.
+
**Q:** How do I set up FMTM locally?
@@ -127,6 +141,7 @@ Python, HTML, CSS, and JavaScript.
**A:** To set up FMTM locally, you need to have Python, Postgres, Redis,
and Node.js installed on your system. You can follow the instructions
in the README file of the FMTM repository to set up the project.
+
**Q:** How can I report a bug or suggest a new feature for FMTM?
@@ -135,6 +150,7 @@ in the README file of the FMTM repository to set up the project.
the FMTM repository on GitHub. Be sure to provide as much detail as
possible, including steps to reproduce the bug, screenshots, or
mockups for new features.
+
**Q:** How can I contribute to FMTM if I'm new to open source or web development?
@@ -146,6 +162,7 @@ documentation, asking questions in the community, and contributing to
issues labeled as "good first issue." Additionally, you can join the
FMTM community on Slack to connect with other contributors and get
help with your contributions.
+
**Q:** What are the benefits of contributing to the FMTM?
@@ -153,6 +170,7 @@ help with your contributions.
**A:** Contributing to the FMTM allows you to help with important
humanitarian and development work, while also developing your mapping
skills and knowledge.
+
**Q:** Do I need to have prior mapping experience to contribute to the FMTM?
@@ -160,6 +178,7 @@ skills and knowledge.
**A:** No, prior mapping experience is not required to contribute to the
FMTM. The platform provides training and resources to help new
contributors get started.
+
**Q:** How do I know which mapping project to join?
@@ -168,6 +187,7 @@ contributors get started.
location, the organization sponsoring the project, and the mapping
goals. Review the project information and choose a project that
interests you.
+
**Q:** Can I work on multiple mapping projects at the same time?
@@ -175,6 +195,7 @@ interests you.
**A:** Yes, you can work on multiple mapping projects at the same
time. However, it is important to ensure that you can commit the
necessary time and effort to each project.
+
**Q:** How do I get feedback on my mapping work?
@@ -182,6 +203,7 @@ necessary time and effort to each project.
**A:** The FMTM provides a validation process where other contributors
review and provide feedback on mapping work. You can also contact
project managers or experienced contributors for additional feedback.
+
**Q:** How can I improve my mapping skills?
@@ -189,6 +211,7 @@ project managers or experienced contributors for additional feedback.
**A:** The FMTM provides training and resources to help you improve your
mapping skills. You can also join mapping communities and forums to
connect with other contributors and learn from their experiences.
+
**Q:** Can I use the mapping data for my own research or projects?
@@ -196,8 +219,3 @@ connect with other contributors and learn from their experiences.
**A:** The mapping data on the FMTM is generally open and available for
use, but it is important to check the specific project requirements
and licenses before using the data for your own research or projects.
-
-
-
-
-
diff --git a/docs/Guide-On-Improving-Documentation.md b/docs/Guide-On-Improving-Documentation.md
index 185d1443c1..c228cf0bef 100644
--- a/docs/Guide-On-Improving-Documentation.md
+++ b/docs/Guide-On-Improving-Documentation.md
@@ -9,6 +9,7 @@ documentation needed by **HOTOSM**. It also includes specific tips for
improving already existing documentation.
## Goals
+
1. Highlight the definitions of each type of documentation that are
commonly used on Github.
2. Provide a foundation to lay information on top of, in order to
@@ -27,7 +28,7 @@ documentation may include some Process documentation under the Usage
section, demonstrating how to use that product. These 4 types in full,
are as follows:
-- **Product Documentation**
+- **Product Documentation**
- **Project Documentation**
- **System Documentation**
- **Process Documentation**
@@ -35,11 +36,12 @@ are as follows:
### 1. Structure for Product Documentation
Product documentation is the process of recording key information
-(almost everything you need to know) about a product, including how
+(almost everything you need to know) about a product, including how
to use it. Product documentation may have form of **Process
documentation** within it (this will be further explained later on). A
flexible and reusable structure of essential components of product
documentation is as follows:
+
- Overview
- Introduction: What the product is, what it does, the target audience, etc.
- Features: A breakdown of each integral part of the product, their
@@ -61,6 +63,7 @@ Project documentation is the process of recording the key project
details that are needed to implement a project. It’s like a roadmap of
what the project is and all necessary information about what it
entails. Main structural components are in the following order:
+
- Overview
- Vision
- Aim / Mission
@@ -71,7 +74,7 @@ entails. Main structural components are in the following order:
- Access
- Resources
- Support / Guidelines
-
+
### 3. Structure for System Documentation
System documentation is an all-encompassing record of details of a
@@ -90,6 +93,7 @@ step. Process documentation is very useful in other documentation
forms. As mentioned earlier, it can be used within **System**,
**Product** or **Project** documentation, to explain a process. The
format is usually:
+
- Overview
- Introduction
- Explanation steps (breakdown of the task)
@@ -116,20 +120,20 @@ added to and built upon.
documentation more user friendly and much easier to understand,
since users come from all different backgrounds. For example:
- “ODK incorporates a new functionality” can become “ODK has brought in a new feature”.
+ “ODK incorporates a new functionality” can become “ODK has brought in a new feature”.
- “Field Mappers select (or are allocated) individual tasks within a
- project’s AOI” could be changed to “Field Mappers choose or are
- given tasks that are part of a project’s Areas Of Interest.”
+ “Field Mappers select (or are allocated) individual tasks within a
+ project’s AOI” could be changed to “Field Mappers choose or are
+ given tasks that are part of a project’s Areas Of Interest.”
4. **Avoid long paragraphs**. Short paragraphs that pass a clear
- message are less clumsy and flustering for readers. Breaking down
- topics into little, easy to understand chunks, is more user
- friendly.
+ message are less clumsy and flustering for readers. Breaking down
+ topics into little, easy to understand chunks, is more user
+ friendly.
5. **Maintain a positive tone in the writing.**. Keep the text
positive and informative. Avoid words like ‘obviously’ and
- ‘basically’, that may be interpreted as demeaning or
+ ‘basically’, that may be interpreted as demeaning or
condescending. Do not expect readers to have a certain amount of
knowledge on specific aspects, break down everything that needs to
be broken down.
@@ -143,9 +147,9 @@ added to and built upon.
documentation.This consistency includes but is not limited to
elements like numbering, font styles, heading sizes, and spacing.
- Using the same font for all headings and subheadings can help
- readers quickly identify important sections of the
- document. Similarly, using consistent spacing between paragraphs
- and sections can make the document more visually appealing and
- easier to follow. This helps to create a documentation that is
- clear, effective, and easy to use.
+ Using the same font for all headings and subheadings can help
+ readers quickly identify important sections of the
+ document. Similarly, using consistent spacing between paragraphs
+ and sections can make the document more visually appealing and
+ easier to follow. This helps to create a documentation that is
+ clear, effective, and easy to use.
diff --git a/docs/User-Manual-For-Project-Managers.md b/docs/User-Manual-For-Project-Managers.md
index 5e91836ffa..4071ffd2ba 100644
--- a/docs/User-Manual-For-Project-Managers.md
+++ b/docs/User-Manual-For-Project-Managers.md
@@ -1,4 +1,5 @@
# User Manual for FMTM
+
This manual is a step by step guide for the project managers on how to get started with the Field Mapping Tasking Manager.
@@ -9,7 +10,7 @@ This manual is a step by step guide for the project managers on how to get start
- [Steps to create a project in FMTM](#steps-to-create-a-project-in-fmtm)
- [Steps to start access your project and Start mapping](#steps-to-start-access-your-project-and-start-mapping-or-a-mapping-campaign)
- [Help and Support](#help-and-support)
-- [Thank you note](#thank-you)
+- [Thank you note](#thank-you)
## Introduction
@@ -74,54 +75,35 @@ reliability of the data collected by volunteers, **FMTM** helps to
provide critical information that can be used to support decision-making
and improve the effectiveness of humanitarian efforts.
-
## Prerequisites
+
- Stable Internet connection
- Knowledge on field mapping . If you are new to mapping we suggest you to read [this](https://tasks.hotosm.org/learn/map) .
- Account on ODK Central Server. [Here are the instructions for setting up an ODK Central server on Digital Ocean](https://docs.getodk.org/central-install-digital-ocean/) (it's very similar on AWS or whatever)
## Video Tutorial
+
-https://github.com/hotosm/fmtm/assets/97789856/6ad200e7-3af9-418b-bb6e-6666bbab9a15
-
-
-
-https://github.com/hotosm/fmtm/assets/97789856/62646dd8-6130-4612-99fe-4df29ae432d9
-
-
-
-https://github.com/hotosm/fmtm/assets/97789856/8677062c-981c-4ea3-964f-3348c4953f82
-
-
-
-
-https://github.com/hotosm/fmtm/assets/97789856/02355809-2f40-470c-856f-afe56250883f
-
-
-
-https://github.com/hotosm/fmtm/assets/97789856/084ce707-95ba-4d51-a650-132be84fbe68
-
+
+
-https://github.com/hotosm/fmtm/assets/97789856/6711badb-c93e-4109-9090-0ad1f1554699
+
+
+
-https://github.com/hotosm/fmtm/assets/97789856/b2af3c7d-5392-4e10-bf83-853b2f517c9a
-
-
-
-
-https://github.com/hotosm/fmtm/assets/97789856/d8b2bf72-e8e0-41bc-a568-77854f45efa6
-
+
+
## Steps to create a project in FMTM
1. Go to [fmtm](https://fmtm.hotosm.org/) .
2. In the header, you'll find three tabs: Explore Projects, Manage Organization, and Manage Categories.
-
+
![WhatsApp Image 2023-06-23 at 1 23 07 PM](https://github.com/hotosm/fmtm/assets/97789856/c0d272f0-c69c-483f-9e9d-83dd75b9e748)
3. Start by exploring the projects listed by different nations and world communities for field mapping exercises.
@@ -138,33 +120,27 @@ https://github.com/hotosm/fmtm/assets/97789856/d8b2bf72-e8e0-41bc-a568-77854f45e
10. If your organization's name is not listed, you can add it through the "Manage Organization" tab.
11. Provide the necessary credentials for the ODK (Open Data Kit) central setup, including URL, username, and password.
12. Proceed to the next step, which is uploading the area for field mapping. Choose the file option and select the AOI (Area of Interest) file in GEOJSON file format.
-Review the displayed map that corresponds to your selected area and click on "Next".
+ Review the displayed map that corresponds to your selected area and click on "Next".
![3](https://github.com/hotosm/fmtm/assets/97789856/680eb831-790a-48f1-8997-c20b5213909d)
13. Define the tasks of the project.
-![WhatsApp Image 2023-06-23 at 1 38 18 PM](https://github.com/hotosm/fmtm/assets/97789856/177d8258-900e-447f-906a-28aeb1fd6b03)
+ ![WhatsApp Image 2023-06-23 at 1 38 18 PM](https://github.com/hotosm/fmtm/assets/97789856/177d8258-900e-447f-906a-28aeb1fd6b03)
If you choose "Divide on Square," specify the dimensions of the square tasks.
Click on "Next" to proceed.
-
![WhatsApp Image 2023-06-23 at 1 17 37 PM](https://github.com/hotosm/fmtm/assets/97789856/f53d76b4-e6cc-44a4-8c7c-00082eb72693)
-
14. Select Form . Select the form category you want to use for the field mapping, such as "Data Extract" or any other relevant category.
-Choose a specific form from the existing forms or upload a custom form if needed.
-Click on "Submit" to proceed.
+ Choose a specific form from the existing forms or upload a custom form if needed.
+ Click on "Submit" to proceed.
![WhatsApp Image 2023-06-23 at 1 37 19 PM](https://github.com/hotosm/fmtm/assets/97789856/f9a4bed7-d1a9-44dd-b2d4-b55f428f9416)
-
15. Wait for the system to generate QR codes for each task, which will be used later in the field mapping process.
16. After the QR codes are generated, you can find your project in the project dashboard.
-
-
-
## Steps to start access your project and Start mapping or a mapping campaign
@@ -174,29 +150,30 @@ Click on "Submit" to proceed.
![WhatsApp Image 2023-06-23 at 1 26 39 PM](https://github.com/hotosm/fmtm/assets/97789856/162af2e0-dbfa-4787-8037-f03e71417df8)
3. If a task is already locked by another user, choose a different task that is available for mapping.If a task is already locked by another user, choose a different task that is available for mapping.
- - The drop down icon beside **LEGEND** displays a color code. This
- color code lets you know the status of each task on the map.
- - **READY** means that task is available to be mapped
- - **LOCKED FOR MAPPING** means that task is already being mapped by another volunteer and therefore unavailable for mapping
- - **MAPPED** or **READY FOR VALIDATION** means that task has been completely mapped and ready to be validated.
- - **LOCKED FOR VALIDATION** means that task has been mapped and being validated.
- - **VALIDATED** means that task has successfully been validated and completely mapped with no errors
- - **INVALIDATED** or **MORE MAPPING NEEDED** means that task did not pass the validation process and needs more mapping
- - **BAD** means that task is not clear and cannot be mapped
+- The drop down icon beside **LEGEND** displays a color code. This
+ color code lets you know the status of each task on the map.
+
+ - **READY** means that task is available to be mapped
+ - **LOCKED FOR MAPPING** means that task is already being mapped by another volunteer and therefore unavailable for mapping
+ - **MAPPED** or **READY FOR VALIDATION** means that task has been completely mapped and ready to be validated.
+ - **LOCKED FOR VALIDATION** means that task has been mapped and being validated.
+ - **VALIDATED** means that task has successfully been validated and completely mapped with no errors
+ - **INVALIDATED** or **MORE MAPPING NEEDED** means that task did not pass the validation process and needs more mapping
+ - **BAD** means that task is not clear and cannot be mapped
> Note: 'task' refers to each section of the map enclosed in the dotted
> lines and each task has a corresponding number tag.
- ![WhatsApp Image 2023-06-23 at 1 29 10 PM](https://github.com/hotosm/fmtm/assets/97789856/2c0397b0-1829-420a-982e-3d971b514f2c)
- - To begin mapping, click on a task closest to you that has the color
- code associated with **READY** and change it's status from **READY**
- to **LOCKED FOR MAPPING**. Remember to take note of the number tag.
- - Scroll to the bottom of the page. The **ACTIVITIES** tab shows the
- tasks either **LOCKED FOR MAPPING**, **BAD** or **LOCKED FOR
- VALIDATION**. You can search for tasks with the status mentioned
- using the number tag associated with each task.
+![WhatsApp Image 2023-06-23 at 1 29 10 PM](https://github.com/hotosm/fmtm/assets/97789856/2c0397b0-1829-420a-982e-3d971b514f2c)
+- To begin mapping, click on a task closest to you that has the color
+ code associated with **READY** and change it's status from **READY**
+ to **LOCKED FOR MAPPING**. Remember to take note of the number tag.
+- Scroll to the bottom of the page. The **ACTIVITIES** tab shows the
+ tasks either **LOCKED FOR MAPPING**, **BAD** or **LOCKED FOR
+ VALIDATION**. You can search for tasks with the status mentioned
+ using the number tag associated with each task.
4. Use the QR code to start mapping the selected task using the ODK Collect app on your mobile phone.
5. Install and open the ODK Collect app on your phone.
@@ -207,11 +184,14 @@ Click on "Submit" to proceed.
10. After completing the assigned task, go back to the project platform on FMTM and mark it as fully mapped.
## Help and Support
+
If you encounter any issues or need assistance while using FMTM, you can access the following resources:
+
- Check the [FAQs](https://github.com/hotosm/fmtm/wiki/FAQ) .
- Ask your doubts in the [Slack channel: #fmtm-field-pilots](https://hotosm.slack.com/archives/C04PCBFDEGN)
-## Thank you
+## Thank you
+
We are excited to have you join our community of passionate mappers and volunteers. FMTM is a powerful platform developed by the Humanitarian OpenStreetMap Team (HOT) to facilitate mapping projects for disaster response, humanitarian efforts, and community development.
With FMTM, you have the opportunity to make a real impact by mapping areas that are in need of support. Your contributions help create detailed and up-to-date maps that aid organizations and communities in their efforts to respond to crises, plan infrastructure, and improve the lives of people around the world.
diff --git a/docs/dev/Frontend.md b/docs/dev/Frontend.md
index 6a2d5e4359..b2c7dbb1a8 100644
--- a/docs/dev/Frontend.md
+++ b/docs/dev/Frontend.md
@@ -32,7 +32,7 @@ Install the dependencies by running the following command: `npm install`
Run the frontend with hot-reloading: `npm run start:live`
-The frontend should now be accessible at: <<<<<<>>>>>>>.
+The frontend should now be accessible at: <<<<<<<>>>>>>>>.
## Frontend Tips
diff --git a/scripts/postgis_snippets/import_geojson_as_postgis_with_jsonb.md b/scripts/postgis_snippets/import_geojson_as_postgis_with_jsonb.md
index 505efa693a..625e33e564 100644
--- a/scripts/postgis_snippets/import_geojson_as_postgis_with_jsonb.md
+++ b/scripts/postgis_snippets/import_geojson_as_postgis_with_jsonb.md
@@ -1,6 +1,6 @@
# Deprecated; now using pg_dump
-Import the GeoJSON file to PostGIS. To function in the same way as a layer directly imported from OSM using osm2psql, the ```tags``` column needs to be jsonb type.
+Import the GeoJSON file to PostGIS. To function in the same way as a layer directly imported from OSM using osm2psql, the `tags` column needs to be jsonb type.
There probably is a simple way to combine changing the column type and casting the json string to the actual jsonb type, but I don't know how to do it. So here's the workaround:
@@ -25,7 +25,7 @@ update "Islington_AOI_polygons"
set tags = tagsvarchar::jsonb
```
-- Nuke the renamed column with the varchar, leaving only the ```tags``` column with the jsonb type
+- Nuke the renamed column with the varchar, leaving only the `tags` column with the jsonb type
```
alter table "Islington_AOI_polygons"
diff --git a/scripts/postgis_snippets/postgis_resources.md b/scripts/postgis_snippets/postgis_resources.md
index 9a63ea3118..fd309cbe46 100644
--- a/scripts/postgis_snippets/postgis_resources.md
+++ b/scripts/postgis_snippets/postgis_resources.md
@@ -1,11 +1,16 @@
# PostGIS resources
+
## Paul Ramsey or CleverElephant
-Paul Ramsey is a Canadian open source geographical analyst and developer. [His blog](https://blog.cleverelephant.ca/) (which is often *very* technical) is a gold mine of information on many subjects, notably PostGIS.
+
+Paul Ramsey is a Canadian open source geographical analyst and developer. [His blog](https://blog.cleverelephant.ca/) (which is often _very_ technical) is a gold mine of information on many subjects, notably PostGIS.
+
- [Overlays of polygons](https://blog.cleverelephant.ca/2019/07/postgis-overlays.html)
## Matt Forest's Spatial SQL Cookbook
-- Intended for someone who's already an experienced GIS user and wants to transfer their knowledge to SQL. [Lots of clear, useful recipes](https://forrest.nyc/spatial-sql-cookbook/).
+
+- Intended for someone who's already an experienced GIS user and wants to transfer their knowledge to SQL. [Lots of clear, useful recipes](https://forrest.nyc/spatial-sql-cookbook/).
## Random
+
- [A nice Stack Exchange](https://gis.stackexchange.com/questions/172198/constructing-voronoi-diagram-in-postgis/174219#174219) on Voronoi Polygons
- [Best answer ever](https://stackoverflow.com/questions/49531535/pass-fields-when-applying-st-voronoipolygons-and-clip-output) on how to subdivide existing areas with Voronoi polygons
diff --git a/scripts/postgis_snippets/task_splitting/task_splitting_readme.md b/scripts/postgis_snippets/task_splitting/task_splitting_readme.md
index ecdfe35999..7e7f29649b 100644
--- a/scripts/postgis_snippets/task_splitting/task_splitting_readme.md
+++ b/scripts/postgis_snippets/task_splitting/task_splitting_readme.md
@@ -1,41 +1,45 @@
# Task Splitting
-The file ```task_splitting_optimized.sql``` is a spatial Structured Query Language (SQL) script to split an area of interest for field mapping into small "task" areas.
+The file `task_splitting_optimized.sql` is a spatial Structured Query Language (SQL) script to split an area of interest for field mapping into small "task" areas.
-It operates within a Postgresql database with the the spatial extension PostGIS enabled. It requires write access to the database for performance reasons (there is another version without the suffice "_optimized" that doesn't require write access, but it's not likely to ever work well enough for production.
+It operates within a Postgresql database with the the spatial extension PostGIS enabled. It requires write access to the database for performance reasons (there is another version without the suffice "\_optimized" that doesn't require write access, but it's not likely to ever work well enough for production.
It takes into account roads, waterways, and railways to avoid forcing mappers to cross such features during mapping.
It uses a clustering algorithm to divide the area into discrete polygons containing an average number of tasks.
## Inputs (tables/layers)
+
This script takes 4 inputs, all of which are Postgresql/PostGIS tables/layers.
-- ```project-aoi```, a PostGIS polygon layer containing a single feature: a polygon containing the Area of Interest.
-- ```ways_line```, a PostGIS line layer containing all OpenStreetMap "open ways" (the OSM term for linestrings) in the Area of Interest.
-- ```ways_poly```, a Postgis polygon layer containing all OpenSTreetMap "closed ways" (the OSM term for polygons) in the AOI.
-- ```project-config```, a Postgresql table containing settings (for example, the average number of features desired per task). _This isn't yet implemented; these settings are hard-coded for the moment. The script runs without a ```project-config``` table, but the number of features per task needs to be tweaked within the code._
-OSM data (```ways-line``` and ```ways_poly```) can be loaded into a PostGIS database using the [Underpass](https://github.com/hotosm/underpass) configuration file [raw.lua](https://github.com/hotosm/underpass/blob/master/utils/raw.lua). If these two layers are present in the same database and schema as the ```project-aoi``` layer, the script will make use of them automatically (non-desctructively; it doesn't modify any tables other than the ones it creates unless you're unlucky enough to have tables matching the very specific names I'm using, which I'll later change to names that should avoid all realistically possible collisions).
+- `project-aoi`, a PostGIS polygon layer containing a single feature: a polygon containing the Area of Interest.
+- `ways_line`, a PostGIS line layer containing all OpenStreetMap "open ways" (the OSM term for linestrings) in the Area of Interest.
+- `ways_poly`, a Postgis polygon layer containing all OpenSTreetMap "closed ways" (the OSM term for polygons) in the AOI.
+- `project-config`, a Postgresql table containing settings (for example, the average number of features desired per task). _This isn't yet implemented; these settings are hard-coded for the moment. The script runs without a `project-config` table, but the number of features per task needs to be tweaked within the code._
+
+OSM data (`ways-line` and `ways_poly`) can be loaded into a PostGIS database using the [Underpass](https://github.com/hotosm/underpass) configuration file [raw.lua](https://github.com/hotosm/underpass/blob/master/utils/raw.lua). If these two layers are present in the same database and schema as the `project-aoi` layer, the script will make use of them automatically (non-desctructively; it doesn't modify any tables other than the ones it creates unless you're unlucky enough to have tables matching the very specific names I'm using, which I'll later change to names that should avoid all realistically possible collisions).
## Running the script
+
You need a Postgresql database with PostGIS extension enabled. If both Postgresql and PostGIS are installed and you have permissions set up properly (doing both of those things is way beyond scope here), this should do the trick (choose whatever database name you want):
```
createdb [databasename] -O [username]
```
+
```
psql -U [username] -d [databasename] -c 'CREATE EXTENSION POSTGIS'
```
-Now you need to get some OSM data in there. You can get OSM data from the GeoFabrik download tool or the HOT export tool in ```.pbf``` format.
+Now you need to get some OSM data in there. You can get OSM data from the GeoFabrik download tool or the HOT export tool in `.pbf` format.
-If you have your own way of getting the OSM data into the database, as long as it'll create the ```ways_line``` and ```ways_poly``` layers, go for it. Here's ho I'm doing it:
+If you have your own way of getting the OSM data into the database, as long as it'll create the `ways_line` and `ways_poly` layers, go for it. Here's ho I'm doing it:
```
-osm2pgsql --create -H localhost -U [username] -P 5432 -d [database name] -W --extra-attributes --output=flex --style /path/to/git/underpass/utils/raw.lua /path/to/my_extract.osm.pbf
+osm2pgsql --create -H localhost -U [username] -P 5432 -d [database name] -W --extra-attributes --output=flex --style /path/to/git/underpass/utils/raw.lua /path/to/my_extract.osm.pbf
```
-Now you need an AOI. I'm using QGIS connect to the database using the Database Manager, then creating a polygon layer (make a "Temporary scratch layer' with polygon geometry, draw an AOI, and import that layer into the database using the Database Manager). If you don't want to use QGIS, you can get a GeoJSON polygon some other way ([geojson.io](geojson.io) comes to mind) and shove it into the database using ogr2ogr or some other tool. Whatever. Just ensure it's a polygon layer in EPSG:4326 and it's called ```project-aoi```.
+Now you need an AOI. I'm using QGIS connect to the database using the Database Manager, then creating a polygon layer (make a "Temporary scratch layer' with polygon geometry, draw an AOI, and import that layer into the database using the Database Manager). If you don't want to use QGIS, you can get a GeoJSON polygon some other way ([geojson.io](geojson.io) comes to mind) and shove it into the database using ogr2ogr or some other tool. Whatever. Just ensure it's a polygon layer in EPSG:4326 and it's called `project-aoi`.
```
psql -U [username] -d [database name] -f path/to/fmtm/scripts/postgis_snippets/task_splitting/task_splitting_optimized.sql
@@ -44,25 +48,27 @@ psql -U [username] -d [database name] -f path/to/fmtm/scripts/postgis_snippets/t
If all is set up correctly, that'll run and spit out some console output. It's moderately likely to include some warning messages due to messy OSM data, and will very likely complain that some tables do not exist (that's because I clobber any tables with colliding names before creating my own tables; don't run this script on random production databases until I collision-proof the names, and probably not even then).
## Outputs
+
You should now have the following useful layers in your Postgresql/PostGIS database:
+
- clusteredbuildings
- taskpolygons
As well as the following non-useful layers (well, they're useful for debugging, but not for end users' purposes):
+
- buildings
- dumpedpoints
- lowfeaturecountpolygons
- splitpolygons
- voronois
-The ```taskpolygons``` layer can be exported as GeoJSON and used as a task to upload to the FMTM. This works in at least some cases; I'm not sure if there are cases where whatever was in the AOI and OSM layers causes outputs that break somehow (there are definitely some cases where building footprints in OSM are sufficiently messed up that they create weird task geometries, but so far these haven't actually broken anything).
+The `taskpolygons` layer can be exported as GeoJSON and used as a task to upload to the FMTM. This works in at least some cases; I'm not sure if there are cases where whatever was in the AOI and OSM layers causes outputs that break somehow (there are definitely some cases where building footprints in OSM are sufficiently messed up that they create weird task geometries, but so far these haven't actually broken anything).
## Next steps
It's working OK now, but needs more work.
+
- Still simply discards polygon delineated by roads/waterways/railways rather than merging them into neighbors, which causes the task polygons to not tile the full AOI. This isn't necessarily always a problem, but it would be better to have the option to merge rather than discard those areas.
- Task polygon edges can be rough, often jagged, occasionally poking into buildings from adjacent polygons (though never, I think to the centroid). Working on simplifying/smoothing these, but there are some complications...
- Task polygon edges can contain closed-off loops unconnected to their main bodies. May need to increase density of segmentation of buildings in some places.
- Clustering is really pretty good, but not very strict at keeping similar numbers of features per cluster; you get a bit of a range of task sizes (though much, much better than anything we've had previously). I think it's possible to tweak this, though I think it might be expensive in terms of performance.
-
-
diff --git a/src/backend/app/auth/auth_routes.py b/src/backend/app/auth/auth_routes.py
index 75a756a5b6..dbf7bcae04 100644
--- a/src/backend/app/auth/auth_routes.py
+++ b/src/backend/app/auth/auth_routes.py
@@ -18,13 +18,12 @@
"""Auth routes, using OSM OAuth2 endpoints."""
-from loguru import logger as log
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse
+from loguru import logger as log
from sqlalchemy.orm import Session
-from ..config import settings
-
+from ..config import settings
from ..db import database
from ..db.db_models import DbUser
from ..users import user_crud
@@ -39,8 +38,7 @@
@router.get("/osm_login/")
def login_url(request: Request, osm_auth=Depends(init_osm_auth)):
- """
- Generate a login URL for authentication using OAuth2 Application registered with OpenStreetMap.
+ """Generate a login URL for authentication using OAuth2 Application registered with OpenStreetMap.
Args:
request (Request): The request object.
@@ -56,8 +54,7 @@ def login_url(request: Request, osm_auth=Depends(init_osm_auth)):
@router.get("/callback/")
def callback(request: Request, osm_auth=Depends(init_osm_auth)):
- """
- Perform token exchange between OpenStreetMap and Export tool API.
+ """Perform token exchange between OpenStreetMap and Export tool API.
Args:
request (Request): The request object.
@@ -67,8 +64,10 @@ def callback(request: Request, osm_auth=Depends(init_osm_auth)):
A JSONResponse with the access token.
"""
print("Call back api requested", request.url)
-
- access_token = osm_auth.callback(str(request.url).replace('http',settings.URL_SCHEME))
+
+ access_token = osm_auth.callback(
+ str(request.url).replace("http", settings.URL_SCHEME)
+ )
log.debug(f"Access token returned: {access_token}")
return JSONResponse(content={"access_token": access_token}, status_code=200)
diff --git a/src/backend/app/auth/osm.py b/src/backend/app/auth/osm.py
index 594741a659..aa4f0417b6 100644
--- a/src/backend/app/auth/osm.py
+++ b/src/backend/app/auth/osm.py
@@ -13,22 +13,21 @@
class AuthUser(BaseModel):
- """
- A Pydantic model representing an authenticated user.
+ """A Pydantic model representing an authenticated user.
Attributes:
id (int): The ID of the user.
username (str): The username of the user.
img_url (Union[str, None]): The URL of the user's profile image, or None if not provided.
"""
+
id: int
username: str
img_url: Union[str, None]
def init_osm_auth():
- """
- Initialize an instance of the Auth class from the osm_login_python.core module.
+ """Initialize an instance of the Auth class from the osm_login_python.core module.
Returns:
An instance of the Auth class.
@@ -44,8 +43,7 @@ def init_osm_auth():
def login_required(access_token: str = Header(...)):
- """
- A dependency that deserializes an access token from the request header.
+ """A dependency that deserializes an access token from the request header.
Args:
access_token (str, optional): The access token from the request header. Injected by FastAPI.
diff --git a/src/backend/app/central/central_crud.py b/src/backend/app/central/central_crud.py
index 92f8d43ccb..8e094c5e8a 100644
--- a/src/backend/app/central/central_crud.py
+++ b/src/backend/app/central/central_crud.py
@@ -15,8 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with FMTM. If not, see .
#
-from loguru import logger as log
-
import base64
import json
import os
@@ -26,13 +24,13 @@
# import osm_fieldwork
# Qr code imports
import segno
-import xmltodict
from fastapi import HTTPException
+from fastapi.responses import JSONResponse
+from loguru import logger as log
from osm_fieldwork.CSVDump import CSVDump
from osm_fieldwork.OdkCentral import OdkAppUser, OdkForm, OdkProject
from pyxform.xls2xform import xls2xform_convert
from sqlalchemy.orm import Session
-from fastapi.responses import JSONResponse
from ..config import settings
from ..db import db_models
@@ -40,8 +38,7 @@
def get_odk_project(odk_central: project_schemas.ODKCentral = None):
- """
- Get an instance of the OdkProject class with the provided credentials.
+ """Get an instance of the OdkProject class with the provided credentials.
Args:
odk_central (project_schemas.ODKCentral, optional): The ODK Central credentials. Defaults to None.
@@ -76,8 +73,7 @@ def get_odk_project(odk_central: project_schemas.ODKCentral = None):
def get_odk_form(odk_central: project_schemas.ODKCentral = None):
- """
- Get an instance of the OdkForm class with the provided credentials.
+ """Get an instance of the OdkForm class with the provided credentials.
Args:
odk_central (project_schemas.ODKCentral, optional): The ODK Central credentials. Defaults to None.
@@ -113,8 +109,7 @@ def get_odk_form(odk_central: project_schemas.ODKCentral = None):
def get_odk_app_user(odk_central: project_schemas.ODKCentral = None):
- """
- Get an instance of the OdkAppUser class with the provided credentials.
+ """Get an instance of the OdkAppUser class with the provided credentials.
Args:
odk_central (project_schemas.ODKCentral, optional): The ODK Central credentials. Defaults to None.
@@ -149,8 +144,7 @@ def get_odk_app_user(odk_central: project_schemas.ODKCentral = None):
def list_odk_projects(odk_central: project_schemas.ODKCentral = None):
- """
- List all projects on a remote ODK Server.
+ """List all projects on a remote ODK Server.
Args:
odk_central (project_schemas.ODKCentral, optional): The ODK Central credentials. Defaults to None.
@@ -163,8 +157,7 @@ def list_odk_projects(odk_central: project_schemas.ODKCentral = None):
def create_odk_project(name: str, odk_central: project_schemas.ODKCentral = None):
- """
- Create a project on a remote ODK Server.
+ """Create a project on a remote ODK Server.
Args:
name (str): The name of the project to create.
@@ -201,8 +194,7 @@ def create_odk_project(name: str, odk_central: project_schemas.ODKCentral = None
def delete_odk_project(project_id: int, odk_central: project_schemas.ODKCentral = None):
- """
- Delete a project from a remote ODK Server.
+ """Delete a project from a remote ODK Server.
Args:
project_id (int): The ID of the project to delete.
@@ -216,9 +208,7 @@ def delete_odk_project(project_id: int, odk_central: project_schemas.ODKCentral
try:
project = get_odk_project(odk_central)
result = project.deleteProject(project_id)
- log.info(
- f"Project {project_id} has been deleted from the ODK Central server."
- )
+ log.info(f"Project {project_id} has been deleted from the ODK Central server.")
return result
except Exception:
return "Could not delete project from central odk"
@@ -251,8 +241,7 @@ def create_appuser(
def delete_app_user(
project_id: int, name: str, odk_central: project_schemas.ODKCentral = None
):
- """
- Delete an app-user from a remote ODK Server.
+ """Delete an app-user from a remote ODK Server.
Args:
project_id (int): The ID of the project to delete an app-user from.
@@ -306,8 +295,7 @@ def create_odk_xform(
upload_media=True,
convert_to_draft_when_publishing=True,
):
- """
- Create an XForm on a remote ODK Central server.
+ """Create an XForm on a remote ODK Central server.
Args:
project_id (int): The ID of the project to create an XForm for.
@@ -362,8 +350,7 @@ def delete_odk_xform(
filespec: str,
odk_central: project_schemas.ODKCentral = None,
):
- """
- Delete an XForm from a remote ODK Central server.
+ """Delete an XForm from a remote ODK Central server.
Args:
project_id (int): The ID of the project to delete an XForm from.
@@ -381,7 +368,11 @@ def delete_odk_xform(
# def list_odk_xforms(project_id: int, odk_central: project_schemas.ODKCentral = None):
-def list_odk_xforms(project_id: int, odk_central: project_schemas.ODKCentral = None, metadata:bool = False):
+def list_odk_xforms(
+ project_id: int,
+ odk_central: project_schemas.ODKCentral = None,
+ metadata: bool = False,
+):
"""List all XForms in an ODK Central project."""
project = get_odk_project(odk_central)
xforms = project.listForms(project_id, metadata)
@@ -414,8 +405,7 @@ def list_task_submissions(
def list_submissions(project_id: int, odk_central: project_schemas.ODKCentral = None):
- """
- List all submissions from a remote ODK server.
+ """List all submissions from a remote ODK server.
Args:
project_id (int): The ID of the project to list submissions for.
@@ -435,8 +425,7 @@ def list_submissions(project_id: int, odk_central: project_schemas.ODKCentral =
def get_form_list(db: Session, skip: int, limit: int):
- """
- Get a list of IDs and titles of XForms from the database.
+ """Get a list of IDs and titles of XForms from the database.
Args:
db (Session): The database session.
@@ -450,21 +439,21 @@ def get_form_list(db: Session, skip: int, limit: int):
HTTPException: If there is an error querying the database.
"""
try:
- forms = (
+ forms = (
db.query(db_models.DbXForm.id, db_models.DbXForm.title)
.offset(skip)
.limit(limit)
.all()
)
-
+
result_dict = []
for form in forms:
form_dict = {
- 'id': form[0], # Assuming the first element is the ID
- 'title': form[1] # Assuming the second element is the title
+ "id": form[0], # Assuming the first element is the ID
+ "title": form[1], # Assuming the second element is the title
}
result_dict.append(form_dict)
-
+
return result_dict
except Exception as e:
@@ -479,8 +468,7 @@ def download_submissions(
get_json: bool = True,
odk_central: project_schemas.ODKCentral = None,
):
- """
- Download submissions from a remote ODK server.
+ """Download submissions from a remote ODK server.
Args:
project_id (int): The ID of the project to download submissions for.
@@ -491,7 +479,7 @@ def download_submissions(
Returns:
A list of downloaded submissions from the remote ODK server for the specified parameters.
- """
+ """
xform = get_odk_form(odk_central)
# FIXME: should probably filter by timestamps or status value
data = xform.getSubmissions(project_id, xform_id, submission_id, True, get_json)
@@ -515,7 +503,10 @@ async def test_form_validity(xform_content: str, form_type: str):
xls2xform_convert(xlsform_path=xlsform_path, xform_path=outfile, validate=False)
return {"message": "Your form is valid"}
except Exception as e:
- return JSONResponse(content={"message":"Your form is invalid", "possible_reason":str(e)}, status_code=400)
+ return JSONResponse(
+ content={"message": "Your form is invalid", "possible_reason": str(e)},
+ status_code=400,
+ )
def generate_updated_xform(
@@ -523,8 +514,7 @@ def generate_updated_xform(
xform: str,
form_type: str,
):
- """
- Update the version in an XForm so it's unique.
+ """Update the version in an XForm so it's unique.
Args:
xlsform (str): The path to the XLSForm file used to create the XForm.
@@ -604,12 +594,11 @@ def generate_updated_xform(
import xml.etree.ElementTree as ET
root = ET.fromstring(data)
- head = root.find("h:head",namespaces)
- model = head.find("xforms:model",namespaces)
- instances = model.findall("xforms:instance",namespaces)
+ head = root.find("h:head", namespaces)
+ model = head.find("xforms:model", namespaces)
+ instances = model.findall("xforms:instance", namespaces)
index = 0
- data_tag_present = False
for inst in instances:
try:
if "src" in inst.attrib:
@@ -621,8 +610,7 @@ def generate_updated_xform(
if data_tags:
for dt in data_tags:
dt.attrib["id"] = id
- data_tag_present = True
- except Exception as e:
+ except Exception:
continue
index += 1
@@ -685,8 +673,7 @@ def upload_media(
filespec: str,
odk_central: project_schemas.ODKCentral = None,
):
- """
- Upload a data file to ODK Central.
+ """Upload a data file to ODK Central.
Args:
project_id (int): The ID of the project to upload a data file for.
@@ -704,8 +691,7 @@ def download_media(
filespec: str,
odk_central: project_schemas.ODKCentral = None,
):
- """
- Download a data file from ODK Central.
+ """Download a data file from ODK Central.
Args:
project_id (int): The ID of the project to download a data file for.
@@ -722,8 +708,7 @@ def convert_csv(
filespec: str,
data: bytes,
):
- """
- Convert an ODK CSV file to OSM XML and GeoJson.
+ """Convert an ODK CSV file to OSM XML and GeoJson.
Args:
filespec (str): The path to the CSV file to convert.
diff --git a/src/backend/app/central/central_routes.py b/src/backend/app/central/central_routes.py
index b7f92ec601..190965c455 100644
--- a/src/backend/app/central/central_routes.py
+++ b/src/backend/app/central/central_routes.py
@@ -15,12 +15,11 @@
# You should have received a copy of the GNU General Public License
# along with FMTM. If not, see .
#
-from loguru import logger as log
-
import json
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
+from loguru import logger as log
from sqlalchemy import (
column,
select,
@@ -43,8 +42,7 @@
@router.get("/projects")
async def list_projects():
- """
- List projects in ODK Central.
+ """List projects in ODK Central.
Returns:
A dictionary containing a list of projects in ODK Central.
@@ -62,8 +60,7 @@ async def create_appuser(
name: str,
db: Session = Depends(database.get_db),
):
- """
- Create an app-user in ODK Central.
+ """Create an app-user in ODK Central.
Args:
project_id (int): The ID of the project to create an app-user for.
@@ -92,8 +89,7 @@ async def get_form_lists(
skip: int = 0,
limit: int = 100
):
- """
- Retrieve a list of XForms from a database.
+ """Retrieve a list of XForms from a database.
Args:
db (Session, optional): The database session. Injected by FastAPI.
@@ -112,8 +108,7 @@ async def download_submissions(
project_id: int,
db: Session = Depends(database.get_db),
):
- """
- Download submissions data from ODK Central.
+ """Download submissions data from ODK Central.
Args:
project_id (int): The ID of the project to download submissions data for.
@@ -157,8 +152,7 @@ async def list_submissions(
xml_form_id: str = None,
db: Session = Depends(database.get_db),
):
- """
- List submissions data from ODK Central.
+ """List submissions data from ODK Central.
Args:
project_id (int): The ID of the project to list submissions data for.
@@ -215,8 +209,7 @@ async def get_submission(
submission_id: str=None,
db: Session = Depends(database.get_db),
):
- """
- Retrieve submission data from ODK Central.
+ """Retrieve submission data from ODK Central.
Args:
project_id (int): The ID of the project to retrieve submission data for.
diff --git a/src/backend/app/central/central_schemas.py b/src/backend/app/central/central_schemas.py
index eb4adc3510..001e794b53 100644
--- a/src/backend/app/central/central_schemas.py
+++ b/src/backend/app/central/central_schemas.py
@@ -15,30 +15,29 @@
# You should have received a copy of the GNU General Public License
# along with FMTM. If not, see .
#
-from loguru import logger as log
-
from enum import Enum
+from loguru import logger as log
from pydantic import BaseModel
class CentralBase(BaseModel):
- """
- A Pydantic model representing the base information for interacting with an ODK Central server.
+ """A Pydantic model representing the base information for interacting with an ODK Central server.
Attributes:
central_url (str): The URL of the ODK Central server.
"""
+
central_url: str
class Central(CentralBase):
- """
- A Pydantic model representing additional information for interacting with an ODK Central server.
+ """A Pydantic model representing additional information for interacting with an ODK Central server.
Attributes:
geometry_geojson (str): The geometry of a GeoJSON file.
"""
+
geometry_geojson: str
# qr_code_binary: bytes
@@ -48,12 +47,12 @@ class CentralOut(CentralBase):
class CentralFileType(BaseModel):
- """
- A Pydantic model representing an enumeration of file types that can be used with ODK Central.
+ """A Pydantic model representing an enumeration of file types that can be used with ODK Central.
Attributes:
filetype (Enum): An enumeration of file types.
"""
+
filetype: Enum("FileType", ["xform", "extract", "zip", "xlsform", "all"])
log.debug("Hello World!")
diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py
index 880eed5ed3..30d5188a83 100644
--- a/src/backend/app/db/db_models.py
+++ b/src/backend/app/db/db_models.py
@@ -60,8 +60,7 @@
class DbUser(Base):
- """
- A SQLAlchemy model representing a user.
+ """A SQLAlchemy model representing a user.
Attributes:
id (BigInteger): The ID of the user.
@@ -134,8 +133,7 @@ class DbUser(Base):
class DbOrganisation(Base):
- """
- A SQLAlchemy model representing an organisation.
+ """A SQLAlchemy model representing an organisation.
Attributes:
id (Integer): The ID of the organisation.
@@ -168,8 +166,7 @@ class DbOrganisation(Base):
class DbTeam(Base):
- """
- A SQLAlchemy model representing a team.
+ """A SQLAlchemy model representing a team.
Attributes:
id (Integer): The ID of the team.
@@ -177,7 +174,7 @@ class DbTeam(Base):
name (String): The name of the team.
logo (String): The URL of the team's logo.
description (String): A description of the team.
- invite_only (Boolean): Whether this team is invite-only or not.
+ invite_only (Boolean): Whether this team is invite-only or not.
"""
__tablename__ = "teams"
@@ -209,8 +206,7 @@ class DbTeam(Base):
class DbProjectTeams(Base):
- """
- A SQLAlchemy model representing the relationship between a project and its teams.
+ """A SQLAlchemy model representing the relationship between a project and its teams.
Attributes:
team_id (Integer): The ID of the team.
@@ -219,6 +215,7 @@ class DbProjectTeams(Base):
project (relationship): A relationship to the project that this team is associated with.
team (relationship): A relationship to the team that is associated with this project.
"""
+
__tablename__ = "project_teams"
team_id = Column(Integer, ForeignKey("teams.id"), primary_key=True)
project_id = Column(Integer, ForeignKey("projects.id"), primary_key=True)
@@ -233,8 +230,7 @@ class DbProjectTeams(Base):
class DbProjectInfo(Base):
- """
- A SQLAlchemy model representing localized information for a project.
+ """A SQLAlchemy model representing localized information for a project.
Attributes:
project_id (Integer): The ID of the project.
@@ -265,8 +261,7 @@ class DbProjectInfo(Base):
class DbProjectChat(Base):
- """
- A SQLAlchemy model representing chat messages for a project.
+ """A SQLAlchemy model representing chat messages for a project.
Attributes:
id (BigInteger): The ID of the chat message.
@@ -289,8 +284,7 @@ class DbProjectChat(Base):
class DbXForm(Base):
- """
- A SQLAlchemy model representing an XForm template or custom upload.
+ """A SQLAlchemy model representing an XForm template or custom upload.
Attributes:
id (Integer): The ID of the XForm.
@@ -313,8 +307,7 @@ class DbXForm(Base):
class DbTaskInvalidationHistory(Base):
- """
- A SQLAlchemy model representing the most recent history of task invalidation and subsequent validation.
+ """A SQLAlchemy model representing the most recent history of task invalidation and subsequent validation.
Attributes:
id (Integer): The ID of this invalidation history record.
@@ -327,7 +320,7 @@ class DbTaskInvalidationHistory(Base):
invalidated_date (DateTime): The date and time when this task was invalidated by the invalidator user.
invalidation_history_id (Integer): The ID of a previous invalidation history record for this task, if any.
validator_id (BigInteger): The ID of the user who validated this task after it was invalidated, if any.
- """
+ """
__tablename__ = "task_invalidation_history"
id = Column(Integer, primary_key=True)
@@ -361,8 +354,7 @@ class DbTaskInvalidationHistory(Base):
class DbTaskMappingIssue(Base):
- """
- A SQLAlchemy model representing an issue with a task mapping that contributed to invalidation of the task.
+ """A SQLAlchemy model representing an issue with a task mapping that contributed to invalidation of the task.
Attributes:
id (Integer): The ID of the task mapping issue.
@@ -387,8 +379,7 @@ class DbTaskMappingIssue(Base):
class DbMappingIssueCategory(Base):
- """
- A SQLAlchemy model representing a category of task mapping issues identified during validation.
+ """A SQLAlchemy model representing a category of task mapping issues identified during validation.
Attributes:
id (Integer): The ID of the mapping issue category.
@@ -405,8 +396,7 @@ class DbMappingIssueCategory(Base):
class DbTaskHistory(Base):
- """
- A SQLAlchemy model representing the history associated with a task.
+ """A SQLAlchemy model representing the history associated with a task.
Attributes:
id (Integer): The ID of this task history record.
@@ -418,7 +408,7 @@ class DbTaskHistory(Base):
user_id (BigInteger): The ID of the user who performed this action on this task.
invalidation_history (relationship): A relationship to a list of invalidation history records for this task, if any.
actioned_by (relationship): A relationship to the user who performed this action on this task.
- task_mapping_issues (relationship): A relationship to a list of mapping issues for this task, if any.
+ task_mapping_issues (relationship): A relationship to a list of mapping issues for this task, if any.
"""
__tablename__ = "task_history"
@@ -453,13 +443,12 @@ class DbTaskHistory(Base):
class DbQrCode(Base):
- """
- A SQLAlchemy model representing a QR Code.
+ """A SQLAlchemy model representing a QR Code.
Attributes:
id (Integer): The ID of the QR Code.
filename (String): The filename of the QR Code image file.
- image (LargeBinary): The binary data for the QR Code image file.
+ image (LargeBinary): The binary data for the QR Code image file.
"""
__tablename__ = "qr_code"
@@ -470,8 +459,7 @@ class DbQrCode(Base):
class DbTask(Base):
- """
- A SQLAlchemy model representing an individual mapping Task.
+ """A SQLAlchemy model representing an individual mapping Task.
Attributes:
id (Integer): The ID of the Task.
@@ -479,7 +467,7 @@ class DbTask(Base):
project_task_index (Integer): The index of this Task within its project.
project_task_name (String): The name of this Task within its project.
outline (Geometry("POLYGON", srid=4326)): The outline geometry for this Task in WGS84 coordinates.
- """
+ """
__tablename__ = "tasks"
@@ -666,12 +654,13 @@ def tasks_bad(self):
# Count of tasks where osm extracts is completed, used for progress bar.
extract_completed_count = Column(Integer, default=0)
- form_xls = Column(LargeBinary) # XLSForm file if custom xls is uploaded
- form_config_file = Column(LargeBinary) # Yaml config file if custom xls is uploaded
+ form_xls = Column(LargeBinary) # XLSForm file if custom xls is uploaded
+ form_config_file = Column(LargeBinary) # Yaml config file if custom xls is uploaded
+
+ data_extract_type = Column(String) # Type of data extract (Polygon or Centroid)
+ task_split_type = Column(String) # Type of split (Grid or Feature)
+ hashtags = Column(ARRAY(String)) # Project hashtag
- data_extract_type = Column(String) # Type of data extract (Polygon or Centroid)
- task_split_type = Column(String) # Type of split (Grid or Feature)
- hashtags = Column(ARRAY(String)) # Project hashtag
# TODO: Add index on project geometry, tried to add in __table args__
# Index("idx_geometry", DbProject.geometry, postgresql_using="gist")
@@ -726,8 +715,7 @@ class DbFeatures(Base):
class BackgroundTasks(Base):
- """
- A SQLAlchemy model representing a background task.
+ """A SQLAlchemy model representing a background task.
Attributes:
id (String): The ID of the background task.
@@ -735,6 +723,7 @@ class BackgroundTasks(Base):
status (Enum): The status of the background task.
message (String): A message associated with the background task.
"""
+
__tablename__ = "background_tasks"
id = Column(String, primary_key=True)
@@ -745,8 +734,7 @@ class BackgroundTasks(Base):
class DbUserRoles(Base):
- """
- A SQLAlchemy model representing the roles of a user in various contexts.
+ """A SQLAlchemy model representing the roles of a user in various contexts.
Attributes:
user_id (BigInteger): The ID of the user.
@@ -755,8 +743,9 @@ class DbUserRoles(Base):
organization (relationship): A relationship to the organization that this role is associated with, if any.
project_id (Integer): The ID of the project that this role is associated with, if any.
project (relationship): A relationship to the project that this role is associated with, if any.
- role (Enum): The role of the user in the specified context.
+ role (Enum): The role of the user in the specified context.
"""
+
__tablename__ = "user_roles"
user_id = Column(BigInteger, ForeignKey("users.id"), primary_key=True)
@@ -769,17 +758,17 @@ class DbUserRoles(Base):
class DbProjectAOI(Base):
- """
- A SQLAlchemy model representing an Area of Interest for a project.
+ """A SQLAlchemy model representing an Area of Interest for a project.
Attributes:
id (Integer): The ID of the Area of Interest.
project_id (String): The ID of the project that this Area of Interest is associated with.
- geom (Geometry(geometry_type="GEOMETRY", srid=4326)): The geometry of this Area of Interest in WGS84 coordinates.
- tags (JSONB): A JSON object containing tags for this Area of Interest.
+ geom (Geometry(geometry_type="GEOMETRY", srid=4326)): The geometry of this Area of Interest in WGS84 coordinates.
+ tags (JSONB): A JSON object containing tags for this Area of Interest.
"""
+
__tablename__ = "project_aoi"
-
+
id = Column(Integer, primary_key=True)
project_id = Column(String)
geom = Column(Geometry(geometry_type="GEOMETRY", srid=4326))
@@ -787,17 +776,17 @@ class DbProjectAOI(Base):
class DbOsmLines(Base):
- """
- A SQLAlchemy model representing OSM lines for a project.
+ """A SQLAlchemy model representing OSM lines for a project.
Attributes:
id (Integer): The ID of the OSM line.
project_id (String): The ID of the project that this OSM line is associated with.
- geom (Geometry(geometry_type="GEOMETRY", srid=4326)): The geometry of this OSM line in WGS84 coordinates.
- tags (JSONB): A JSON object containing tags for this OSM line.
+ geom (Geometry(geometry_type="GEOMETRY", srid=4326)): The geometry of this OSM line in WGS84 coordinates.
+ tags (JSONB): A JSON object containing tags for this OSM line.
"""
+
__tablename__ = "ways_line"
-
+
id = Column(Integer, primary_key=True)
project_id = Column(String)
geom = Column(Geometry(geometry_type="GEOMETRY", srid=4326))
@@ -805,15 +794,15 @@ class DbOsmLines(Base):
class DbBuildings(Base):
- """
- A SQLAlchemy model representing buildings for a project.
+ """A SQLAlchemy model representing buildings for a project.
Attributes:
id (Integer): The ID of the building.
project_id (String): The ID of the project that this building is associated with.
osm_id (String): The OSM ID of this building, if any.
geom (Geometry(geometry_type="GEOMETRY", srid=4326)): The geometry of this building in WGS84 coordinates.
- """
+ """
+
__tablename__ = "ways_poly"
id = Column(Integer, primary_key=True)
@@ -824,8 +813,7 @@ class DbBuildings(Base):
class DbTilesPath(Base):
- """
- A SQLAlchemy model representing the path to an MBTiles file for a project.
+ """A SQLAlchemy model representing the path to an MBTiles file for a project.
Attributes:
id (Integer): The ID of the MBTiles path.
@@ -834,10 +822,9 @@ class DbTilesPath(Base):
path (String): The path to the MBTiles file.
tile_source (String): The source of the tiles used to generate the MBTiles file.
background_task_id (String): The ID of the background task associated with this MBTiles path.
- created_at (DateTime): The date and time when this MBTiles path was created.
+ created_at (DateTime): The date and time when this MBTiles path was created.
"""
-
__tablename__ = "mbtiles_path"
id = Column(Integer, primary_key=True)
diff --git a/src/backend/app/db/postgis_utils.py b/src/backend/app/db/postgis_utils.py
index ed62fa12f2..a8657a1ba3 100644
--- a/src/backend/app/db/postgis_utils.py
+++ b/src/backend/app/db/postgis_utils.py
@@ -25,8 +25,7 @@
def timestamp():
- """
- Get the current UTC timestamp.
+ """Get the current UTC timestamp.
Returns:
The current UTC timestamp as a datetime object.
@@ -35,8 +34,7 @@ def timestamp():
def geometry_to_geojson(geometry: Geometry, properties: str = {}, id: int = None):
- """
- Convert a geometry object to a GeoJSON Feature.
+ """Convert a geometry object to a GeoJSON Feature.
Args:
geometry (Geometry): The geometry object to convert.
@@ -53,14 +51,13 @@ def geometry_to_geojson(geometry: Geometry, properties: str = {}, id: int = None
"geometry": mapping(shape),
"properties": properties,
"id": id,
- "bbox":shape.bounds
+ "bbox": shape.bounds,
}
return Feature(**geojson)
def get_centroid(geometry: Geometry, properties: str = {}):
- """
- Get the centroid of a geometry object as a GeoJSON Feature.
+ """Get the centroid of a geometry object as a GeoJSON Feature.
Args:
geometry (Geometry): The geometry object to get the centroid of.
diff --git a/src/backend/app/models/enums.py b/src/backend/app/models/enums.py
index 1f3f01e036..e7784c170b 100644
--- a/src/backend/app/models/enums.py
+++ b/src/backend/app/models/enums.py
@@ -20,12 +20,14 @@
class StrEnum(str, Enum):
- """Designed to work with string-based enumerations"""
+ """Designed to work with string-based enumerations."""
+
pass
class IntEnum(int, Enum):
- """It is intended for integer-based enumerations"""
+ """It is intended for integer-based enumerations."""
+
pass
@@ -121,8 +123,7 @@ class TaskStatus(IntEnum, Enum):
def verify_valid_status_update(old_status: TaskStatus, new_status: TaskStatus):
- """
- Verify if the transition from the old status to the new status is valid.
+ """Verify if the transition from the old status to the new status is valid.
Args:
old_status (TaskStatus): The previous status of the task.
@@ -181,8 +182,7 @@ class TaskAction(IntEnum, Enum):
def is_status_change_action(task_action):
- """
- Check if a given task action is related to changing the status of a task.
+ """Check if a given task action is related to changing the status of a task.
Args:
task_action: The task action to check.
@@ -203,8 +203,7 @@ def is_status_change_action(task_action):
def get_action_for_status_change(task_status: TaskStatus):
- """
- Check if a given task action is related to changing the status of a task.
+ """Check if a given task action is related to changing the status of a task.
Args:
task_action: The task action to check.
@@ -231,18 +230,16 @@ def get_action_for_status_change(task_status: TaskStatus):
class TaskType(IntEnum, Enum):
- """
- Enum describing different types of tasks.
- """
+ """Enum describing different types of tasks."""
+
BUILDINGS = 0
AMENITIES = 1
OTHER = 2
class ProjectSplitStrategy(IntEnum, Enum):
- """
- Enum describing different strategies for splitting projects.
- """
+ """Enum describing different strategies for splitting projects."""
+
GRID = 0
OSM_VECTORS = 1
OTHER = 2
diff --git a/src/backend/app/models/languages_and_countries.py b/src/backend/app/models/languages_and_countries.py
index 7c5104bdde..cc8d5060d4 100644
--- a/src/backend/app/models/languages_and_countries.py
+++ b/src/backend/app/models/languages_and_countries.py
@@ -1,6 +1,6 @@
# see https://gist.github.com/alexanderjulo/4073388
-"""List of languages and countries"""
+"""List of languages and countries."""
languages = [
("aa", "Afar"),
diff --git a/src/backend/app/organization/organization_crud.py b/src/backend/app/organization/organization_crud.py
index c04ce48319..77cec0a9d9 100644
--- a/src/backend/app/organization/organization_crud.py
+++ b/src/backend/app/organization/organization_crud.py
@@ -15,15 +15,16 @@
# You should have received a copy of the GNU General Public License
# along with FMTM. If not, see .
#
-from loguru import logger as log
-
import os
import random
-import string
-from fastapi import HTTPException, File,UploadFile
import re
+import string
+
+from fastapi import HTTPException, UploadFile
+from loguru import logger as log
from sqlalchemy import func
from sqlalchemy.orm import Session
+
from ..db import db_models
IMAGEDIR = "app/images/"
@@ -32,8 +33,7 @@
def get_organisations(
db: Session,
):
- """
- Retrieve a list of organisations from the database.
+ """Retrieve a list of organisations from the database.
Args:
db (Session): SQLAlchemy database session.
@@ -44,9 +44,9 @@ def get_organisations(
db_organisation = db.query(db_models.DbOrganisation).all()
return db_organisation
+
def generate_slug(text: str) -> str:
- """
- Generate a slug from the given text.
+ """Generate a slug from the given text.
This function removes special characters, replaces spaces with hyphens, and ensures a clean slug format.
@@ -57,9 +57,9 @@ def generate_slug(text: str) -> str:
str: The generated slug.
"""
# Remove special characters and replace spaces with hyphens
- slug = re.sub(r'[^\w\s-]', '', text).strip().lower().replace(' ', '-')
+ slug = re.sub(r"[^\w\s-]", "", text).strip().lower().replace(" ", "-")
# Remove consecutive hyphens
- slug = re.sub(r'[-\s]+', '-', slug)
+ slug = re.sub(r"[-\s]+", "-", slug)
return slug
@@ -67,15 +67,14 @@ async def get_organisation_by_name(db: Session, name: str):
# Use SQLAlchemy's query-building capabilities
db_organisation = (
db.query(db_models.DbOrganisation)
- .filter(func.lower(db_models.DbOrganisation.name).like(func.lower(f'%{name}%')))
+ .filter(func.lower(db_models.DbOrganisation.name).like(func.lower(f"%{name}%")))
.first()
)
return db_organisation
async def upload_image(db: Session, file: UploadFile(None)):
- """
- Upload an image file.
+ """Upload an image file.
This function saves an uploaded image file to the specified directory and returns the filename.
@@ -90,9 +89,8 @@ async def upload_image(db: Session, file: UploadFile(None)):
filename = file.filename
file_path = f"{IMAGEDIR}{filename}"
while os.path.exists(file_path):
-
# Generate a random character
- random_char = ''.join(random.choices(string.ascii_letters + string.digits, k=3))
+ random_char = "".join(random.choices(string.ascii_letters + string.digits, k=3))
# Add the random character to the filename
logo_name, extension = os.path.splitext(filename)
@@ -109,9 +107,10 @@ async def upload_image(db: Session, file: UploadFile(None)):
return filename
-async def create_organization(db: Session, name: str, description: str, url: str, logo: UploadFile(None)):
- """
- Creates a new organization with the given name, description, url, type, and logo.
+async def create_organization(
+ db: Session, name: str, description: str, url: str, logo: UploadFile(None)
+):
+ """Creates a new organization with the given name, description, url, type, and logo.
Saves the logo file to the app/images folder.
Args:
@@ -125,7 +124,6 @@ async def create_organization(db: Session, name: str, description: str, url: str
Returns:
bool: True if organization was created successfully
"""
-
# create new organization
try:
logo_name = await upload_image(db, logo) if logo else None
@@ -135,7 +133,7 @@ async def create_organization(db: Session, name: str, description: str, url: str
slug=generate_slug(name),
description=description,
url=url,
- logo=logo_name
+ logo=logo_name,
)
db.add(db_organization)
@@ -151,8 +149,7 @@ async def create_organization(db: Session, name: str, description: str, url: str
async def get_organisation_by_id(db: Session, id: int):
- """
- Get an organization by its id.
+ """Get an organization by its id.
Args:
db (Session): database session
@@ -162,22 +159,25 @@ async def get_organisation_by_id(db: Session, id: int):
DbOrganisation: organization with the given id
"""
db_organization = (
- db.query(db_models.DbOrganisation).filter(db_models.DbOrganisation.id == id).first()
+ db.query(db_models.DbOrganisation)
+ .filter(db_models.DbOrganisation.id == id)
+ .first()
)
return db_organization
async def update_organization_info(
- db: Session,
- organization_id, name: str,
+ db: Session,
+ organization_id,
+ name: str,
description: str,
url: str,
- logo: UploadFile
- ):
+ logo: UploadFile,
+):
organization = await get_organisation_by_id(db, organization_id)
if not organization:
- raise HTTPException(status_code=404, detail='Organization not found')
-
+ raise HTTPException(status_code=404, detail="Organization not found")
+
if name:
organization.name = name
if description:
diff --git a/src/backend/app/organization/organization_routes.py b/src/backend/app/organization/organization_routes.py
index ab03a857a2..c0abfa3ff8 100644
--- a/src/backend/app/organization/organization_routes.py
+++ b/src/backend/app/organization/organization_routes.py
@@ -16,7 +16,6 @@
# along with FMTM. If not, see .
#
-from typing import Union,Optional
from fastapi import (
APIRouter,
@@ -26,7 +25,6 @@
HTTPException,
UploadFile,
)
-from loguru import logger as log
from sqlalchemy.orm import Session
from ..db import database
@@ -43,10 +41,8 @@
@router.get("/")
def get_organisations(
db: Session = Depends(database.get_db),
-
):
- """
- Get the list of organizations.
+ """Get the list of organizations.
Args:
db (Session): SQLAlchemy database session.
@@ -54,17 +50,15 @@ def get_organisations(
Returns:
List[DbOrganisation]: A list of organization records from the database.
"""
-
organizations = organization_crud.get_organisations(db)
return organizations
@router.get("/{organization_id}")
async def get_organization_detail(
- organization_id: int,
- db: Session = Depends(database.get_db)
+ organization_id: int, db: Session = Depends(database.get_db)
):
- """Get API for fetching detail about a organiation based on id"""
+ """Get API for fetching detail about a organiation based on id."""
organization = await organization_crud.get_organisation_by_id(db, organization_id)
if not organization:
raise HTTPException(status_code=404, detail="Organization not found")
@@ -80,8 +74,7 @@ async def create_organization(
logo: UploadFile = File(None), # Optional field for organization logo
db: Session = Depends(database.get_db), # Dependency for database session
):
- """
- Create an organization with the given details.
+ """Create an organization with the given details.
Args:
name (str): The name of the organization. Required.
@@ -95,7 +88,9 @@ async def create_organization(
"""
# Check if the organization with the same already exists
if await organization_crud.get_organisation_by_name(db, name=name):
- raise HTTPException(status_code=400, detail=f"Organization already exists with the name {name}")
+ raise HTTPException(
+ status_code=400, detail=f"Organization already exists with the name {name}"
+ )
await organization_crud.create_organization(db, name, description, url, logo)
@@ -104,28 +99,28 @@ async def create_organization(
@router.patch("/{organization_id}/")
async def update_organization(
- organization_id: int,
+ organization_id: int,
name: str = Form(None),
description: str = Form(None),
url: str = Form(None),
logo: UploadFile = File(None),
- db: Session = Depends(database.get_db)
+ db: Session = Depends(database.get_db),
):
- """PUT API to update the details of an organization"""
+ """PUT API to update the details of an organization."""
try:
- organization = await organization_crud.update_organization_info(db, organization_id, name, description, url, logo)
+ organization = await organization_crud.update_organization_info(
+ db, organization_id, name, description, url, logo
+ )
return organization
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error updating organization: {e}")
-
@router.delete("/{organization_id}")
async def delete_organisations(
organization_id: int, db: Session = Depends(database.get_db)
- ):
- """
- Upload an image file.
+):
+ """Upload an image file.
This function saves an uploaded image file to the specified directory and returns the filename.
@@ -136,7 +131,6 @@ async def delete_organisations(
Returns:
str: The filename of the uploaded image.
"""
-
organization = await organization_crud.get_organisation_by_id(db, organization_id)
if not organization:
diff --git a/src/backend/app/organization/organization_schemas.py b/src/backend/app/organization/organization_schemas.py
index a0f27b4f5b..67b0f658ac 100644
--- a/src/backend/app/organization/organization_schemas.py
+++ b/src/backend/app/organization/organization_schemas.py
@@ -18,10 +18,8 @@
from pydantic import BaseModel
-
class Organisation(BaseModel):
- """
- Represents an organization.
+ """Represents an organization.
Attributes:
slug (str): The unique identifier (slug) for the organization.
diff --git a/src/backend/app/pagination/pagination.py b/src/backend/app/pagination/pagination.py
index b4cc8c0277..65e3181209 100644
--- a/src/backend/app/pagination/pagination.py
+++ b/src/backend/app/pagination/pagination.py
@@ -1,9 +1,9 @@
import math
from typing import List
+
def get_pages_nav(total_pages: int, current_page: int) -> tuple[int, int]:
- """
- Generate navigation links for pagination.
+ """Generate navigation links for pagination.
Args:
total_pages (int): Total number of pages.
@@ -20,9 +20,11 @@ def get_pages_nav(total_pages: int, current_page: int) -> tuple[int, int]:
prev_page = current_page - 1
return next_page, prev_page
-def paginate_data(data: List[dict], page_no: int, page_size: int, total_content: int) -> dict[str, any]:
- """
- Paginate a list of data.
+
+def paginate_data(
+ data: List[dict], page_no: int, page_size: int, total_content: int
+) -> dict[str, any]:
+ """Paginate a list of data.
Args:
data (List[dict]): The list of data to be paginated.
@@ -42,4 +44,3 @@ def paginate_data(data: List[dict], page_no: int, page_size: int, total_content:
"prev_page": prev_page,
"results": data,
}
-
diff --git a/src/backend/app/projects/project_crud.py b/src/backend/app/projects/project_crud.py
index 3ca7cde367..06970788ca 100644
--- a/src/backend/app/projects/project_crud.py
+++ b/src/backend/app/projects/project_crud.py
@@ -15,8 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with FMTM. If not, see .
#
-from loguru import logger as log
-
import base64
import io
import json
@@ -33,39 +31,35 @@
import geoalchemy2
import geojson
import numpy as np
+import pkg_resources
import requests
import segno
import shapely.wkb as wkblib
import sqlalchemy
-import pkg_resources
from fastapi import File, HTTPException, UploadFile
from geoalchemy2.shape import from_shape
from geojson import dump
+from loguru import logger as log
from osm_fieldwork import basemapper
+from osm_fieldwork.json2osm import json2osm
from osm_fieldwork.make_data_extract import PostgresClient
from osm_fieldwork.OdkCentral import OdkAppUser
from osm_fieldwork.xlsforms import xlsforms_path
-from osm_fieldwork.json2osm import json2osm
from shapely import wkt
from shapely.geometry import MultiPolygon, Polygon, mapping, shape
-from sqlalchemy import and_, column, func, inspect, select, table
+from sqlalchemy import and_, column, func, inspect, select, table, text
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.orm import Session
-from sqlalchemy import text
from sqlalchemy.sql import text
-from cpuinfo import get_cpu_info
-from ..db import database
-import concurrent.futures
from ..central import central_crud
from ..config import settings
-from ..db import db_models
+from ..db import database, db_models
from ..db.postgis_utils import geometry_to_geojson, timestamp
from ..tasks import tasks_crud
from ..users import user_crud
from . import project_schemas
-
QR_CODES_DIR = "QR_codes/"
TASK_GEOJSON_DIR = "geojson/"
TILESDIR = "/opt/tiles"
@@ -79,8 +73,7 @@ def get_projects(
db_objects: bool = False,
hashtags: List[str] = None,
):
- """
- Gets a list of projects.
+ """Gets a list of projects.
Args:
db (Session): A database session.
@@ -146,8 +139,7 @@ def get_project_summaries(
def get_project_by_id_w_all_tasks(db: Session, project_id: int):
- """
- Gets a project by its ID and includes all tasks.
+ """Gets a project by its ID and includes all tasks.
Args:
db (Session): A database session.
@@ -166,8 +158,7 @@ def get_project_by_id_w_all_tasks(db: Session, project_id: int):
def get_project(db: Session, project_id: int):
- """
- Gets a project by its ID.
+ """Gets a project by its ID.
Args:
db (Session): A database session.
@@ -205,8 +196,7 @@ def get_project_info_by_id(db: Session, project_id: int):
def delete_project_by_id(db: Session, project_id: int):
- """
- Deletes a project by its ID.
+ """Deletes a project by its ID.
Args:
db (Session): A database session.
@@ -264,8 +254,7 @@ def partial_update_project_info(
def update_project_info(
db: Session, project_metadata: project_schemas.BETAProjectUpload, project_id
):
- """
- Updates a project's information.
+ """Updates a project's information.
Args:
db (Session): A database session.
@@ -322,8 +311,7 @@ def update_project_info(
def create_project_with_project_info(
db: Session, project_metadata: project_schemas.BETAProjectUpload, project_id
):
- """
- Creates a new project with the specified information.
+ """Creates a new project with the specified information.
Args:
db (Session): A database session.
@@ -424,8 +412,7 @@ def upload_xlsform(
name: str,
category: str,
):
- """
- Uploads an XLSForm to the database.
+ """Uploads an XLSForm to the database.
Args:
db (Session): A database session.
@@ -462,8 +449,7 @@ def update_multi_polygon_project_boundary(
project_id: int,
boundary: str,
):
- """
- Updates a project's boundary with multiple polygons.
+ """Updates a project's boundary with multiple polygons.
Args:
db (Session): A database session.
@@ -523,12 +509,12 @@ def remove_z_dimension(coord):
)
db.commit()
-
-
# Generate project outline from tasks
- query = text(f"""SELECT ST_AsText(ST_ConvexHull(ST_Collect(outline)))
+ query = text(
+ f"""SELECT ST_AsText(ST_ConvexHull(ST_Collect(outline)))
FROM tasks
- WHERE project_id={project_id};""")
+ WHERE project_id={project_id};"""
+ )
log.debug("Generating project outline from tasks")
result = db.execute(query)
@@ -547,8 +533,7 @@ def remove_z_dimension(coord):
async def preview_tasks(boundary: str, dimension: int):
- """
- Previews tasks by returning a list of task objects.
+ """Previews tasks by returning a list of task objects.
Args:
boundary (str): A GeoJSON string representing the boundary of the tasks to preview.
@@ -654,8 +639,7 @@ def remove_z_dimension(coord):
def get_osm_extracts(boundary: str):
- """
- Gets OSM extracts for a specified boundary.
+ """Gets OSM extracts for a specified boundary.
Args:
boundary (str): A GeoJSON string representing the boundary to get OSM extracts for.
@@ -719,15 +703,17 @@ def get_osm_extracts(boundary: str):
return data
+
async def split_into_tasks(
- db: Session, boundary: str, no_of_buildings: int, has_data_extracts:bool
+ db: Session, boundary: str, no_of_buildings: int, has_data_extracts: bool
):
- """
- Splits a project into tasks.
+ """Splits a project into tasks.
+
Args:
db (Session): A database session.
boundary (str): A GeoJSON string representing the boundary of the project to split into tasks.
no_of_buildings (int): The number of buildings to include in each task.
+
Returns:
Any: A GeoJSON object containing the tasks for the specified project.
"""
@@ -736,32 +722,36 @@ async def split_into_tasks(
all_results = []
boundary_data = []
result = []
- if outline['type'] == "FeatureCollection":
+ if outline["type"] == "FeatureCollection":
boundary_data.extend(feature["geometry"] for feature in outline["features"])
result.extend(
process_polygon(db, project_id, data, no_of_buildings, has_data_extracts)
for data in boundary_data
- )
+ )
for inner_list in result:
all_results.extend(iter(inner_list))
- elif outline['type'] == "GeometryCollection":
+ elif outline["type"] == "GeometryCollection":
geometries = outline["geometries"]
boundary_data.extend(iter(geometries))
result.extend(
process_polygon(db, project_id, data, no_of_buildings, has_data_extracts)
for data in boundary_data
- )
+ )
for inner_list in result:
all_results.extend(iter(inner_list))
- elif outline['type'] == "Feature":
+ elif outline["type"] == "Feature":
boundary_data = outline["geometry"]
- result = process_polygon(db, project_id, boundary_data, no_of_buildings, has_data_extracts)
+ result = process_polygon(
+ db, project_id, boundary_data, no_of_buildings, has_data_extracts
+ )
all_results.extend(iter(result))
else:
boundary_data = outline
- result = process_polygon(db, project_id, boundary_data, no_of_buildings, has_data_extracts)
+ result = process_polygon(
+ db, project_id, boundary_data, no_of_buildings, has_data_extracts
+ )
all_results.extend(result)
return {
"type": "FeatureCollection",
@@ -769,7 +759,13 @@ async def split_into_tasks(
}
-def process_polygon(db:Session, project_id:uuid.UUID, boundary_data:str, no_of_buildings:int, has_data_extracts: bool):
+def process_polygon(
+ db: Session,
+ project_id: uuid.UUID,
+ boundary_data: str,
+ no_of_buildings: int,
+ has_data_extracts: bool,
+):
outline = shape(boundary_data)
db_task = db_models.DbProjectAOI(
project_id=project_id,
@@ -783,43 +779,40 @@ def process_polygon(db:Session, project_id:uuid.UUID, boundary_data:str, no_of_b
if not data:
return None
for feature in data["features"]:
- feature_shape = shape(feature['geometry'])
+ feature_shape = shape(feature["geometry"])
wkb_element = from_shape(feature_shape, srid=4326)
- if feature['properties'].get('building') == 'yes':
+ if feature["properties"].get("building") == "yes":
db_feature = db_models.DbBuildings(
- project_id=project_id,
- geom=wkb_element,
- tags=feature["properties"]
+ project_id=project_id, geom=wkb_element, tags=feature["properties"]
)
db.add(db_feature)
- elif 'highway' in feature['properties']:
+ elif "highway" in feature["properties"]:
db_feature = db_models.DbOsmLines(
- project_id=project_id,
- geom=wkb_element,
- tags=feature["properties"]
+ project_id=project_id, geom=wkb_element, tags=feature["properties"]
)
db.add(db_feature)
db.commit()
else:
# Remove the polygons outside of the project AOI using a parameterized query
- query = text(f"""
+ query = text(
+ f"""
DELETE FROM ways_poly
WHERE NOT ST_Within(ST_Centroid(ways_poly.geom), (SELECT geom FROM project_aoi WHERE project_id = '{project_id}'));
- """)
+ """
+ )
result = db.execute(query)
db.commit()
- with open('app/db/split_algorithm.sql', 'r') as sql_file:
+ with open("app/db/split_algorithm.sql", "r") as sql_file:
query = sql_file.read()
- result = db.execute(text(query), params={'num_buildings': no_of_buildings})
+ result = db.execute(text(query), params={"num_buildings": no_of_buildings})
result = result.fetchall()
db.query(db_models.DbBuildings).delete()
db.query(db_models.DbOsmLines).delete()
db.query(db_models.DbProjectAOI).delete()
db.commit()
- return result[0][0]['features']
-
+ return result[0][0]["features"]
# def update_project_boundary(
@@ -897,8 +890,7 @@ def process_polygon(db:Session, project_id:uuid.UUID, boundary_data:str, no_of_b
def update_project_boundary(
db: Session, project_id: int, boundary: str, dimension: int
):
- """
- Updates a project's boundary.
+ """Updates a project's boundary.
Args:
db (Session): A database session.
@@ -909,7 +901,6 @@ def update_project_boundary(
Returns:
bool: True if the project's boundary was successfully updated, False otherwise.
"""
-
db_project = get_project_by_id(db, project_id)
if not db_project:
log.error(f"Project {project_id} doesn't exist!")
@@ -1002,8 +993,7 @@ def update_project_with_zip(
task_type_prefix: str,
uploaded_zip: UploadFile,
):
- """
- Updates a project with data from a ZIP file.
+ """Updates a project with data from a ZIP file.
Args:
db (Session): A database session.
@@ -1170,8 +1160,7 @@ def read_xlsforms(
db: Session,
directory: str,
):
- """
- Reads a list of XLSForms from a directory.
+ """Reads a list of XLSForms from a directory.
Args:
db (Session): A database session.
@@ -1186,7 +1175,7 @@ def read_xlsforms(
if xls.endswith(".xls") or xls.endswith(".xlsx"):
file_name = xls.split(".")[0]
yaml_file_name = f"data_models/{file_name}.yaml"
- if pkg_resources.resource_exists(package_name,yaml_file_name):
+ if pkg_resources.resource_exists(package_name, yaml_file_name):
xlsforms.append(xls)
else:
continue
@@ -1219,8 +1208,7 @@ def read_xlsforms(
def get_odk_id_for_project(db: Session, project_id: int):
- """
- Gets the ODK project ID for a specified project.
+ """Gets the ODK project ID for a specified project.
Args:
db (Session): A database session.
@@ -1331,7 +1319,7 @@ def generate_task_files(
# If app user could not be created, raise an exception.
if not appuser:
- project_log.error(f"Couldn't create appuser for project")
+ project_log.error("Couldn't create appuser for project")
return False
# prefix should be sent instead of name
@@ -1359,7 +1347,8 @@ def generate_task_files(
# Get the features for this task.
# Postgis query to filter task inside this task outline and of this project
# Update those features and set task_id
- query = text(f"""UPDATE features
+ query = text(
+ f"""UPDATE features
SET task_id={task_id}
WHERE id IN (
SELECT id
@@ -1368,12 +1357,14 @@ def generate_task_files(
AND ST_IsValid(geometry)
AND ST_IsValid('{task.outline}'::Geometry)
AND ST_Contains('{task.outline}'::Geometry, ST_Centroid(geometry))
- )""")
+ )"""
+ )
result = db.execute(query)
# Get the geojson of those features for this task.
- query = text(f"""SELECT jsonb_build_object(
+ query = text(
+ f"""SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', jsonb_agg(feature)
)
@@ -1386,13 +1377,14 @@ def generate_task_files(
) AS feature
FROM features
WHERE project_id={project_id} and task_id={task_id}
- ) features;""")
+ ) features;"""
+ )
result = db.execute(query)
features = result.fetchone()[0]
- upload_media = False if features['features'] is None else True
+ upload_media = False if features["features"] is None else True
# Update outfile containing osm extracts with the new geojson contents containing title in the properties.
with open(extracts, "w") as jsonfile:
@@ -1402,10 +1394,11 @@ def generate_task_files(
project_log.info(f"Generating xform for task {task_id}")
outfile = central_crud.generate_updated_xform(xlsform, xform, form_type)
-
# Create an odk xform
project_log.info(f"Uploading media in {task_id}")
- result = central_crud.create_odk_xform(odk_id, task_id, outfile, odk_credentials, False, upload_media)
+ result = central_crud.create_odk_xform(
+ odk_id, task_id, outfile, odk_credentials, False, upload_media
+ )
# result = central_crud.create_odk_xform(odk_id, task_id, outfile, odk_credentials)
project_log.info(f"Updating role for app user in task {task_id}")
@@ -1439,7 +1432,6 @@ def generate_task_files(
return True
-
def generate_task_files_wrapper(project_id, task, xlsform, form_type, odk_credentials):
for db in database.get_db():
generate_task_files(db, project_id, task, xlsform, form_type, odk_credentials)
@@ -1469,7 +1461,7 @@ def generate_appuser_files(
"""
try:
project_log = log.bind(task="create_project", project_id=project_id)
-
+
project_log.info(f"Starting generate_appuser_files for project {project_id}")
# Get the project table contents.
@@ -1532,11 +1524,11 @@ def generate_appuser_files(
# Data Extracts
if extracts_contents is not None:
- project_log.info(f"Uploading data extracts")
+ project_log.info("Uploading data extracts")
upload_custom_data_extracts(db, project_id, extracts_contents)
else:
- project_log.info(f"Extracting Data from OSM")
+ project_log.info("Extracting Data from OSM")
# OSM Extracts for whole project
pg = PostgresClient(settings.UNDERPASS_API_URL, "underpass")
@@ -1597,7 +1589,12 @@ def generate_appuser_files(
for task in tasks_list:
try:
generate_task_files(
- db, project_id, task, xlsform, form_type, odk_credentials,
+ db,
+ project_id,
+ task,
+ xlsform,
+ form_type,
+ odk_credentials,
)
except Exception as e:
log.warning(str(e))
@@ -1623,8 +1620,7 @@ def create_qrcode(
project_name: str,
odk_central_url: str = None,
):
- """
- Creates a QR code for an app user.
+ """Creates a QR code for an app user.
Args:
db (Session): A database session.
@@ -1704,10 +1700,10 @@ def get_task_geometry(db: Session, project_id: int):
return json.dumps(feature_collection)
-async def get_project_features_geojson(db:Session, project_id:int):
-
+async def get_project_features_geojson(db: Session, project_id: int):
# Get the geojson of those features for this task.
- query = text(f"""SELECT jsonb_build_object(
+ query = text(
+ f"""SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', jsonb_agg(feature)
)
@@ -1721,7 +1717,8 @@ async def get_project_features_geojson(db:Session, project_id:int):
FROM features
WHERE project_id={project_id}
) features;
- """)
+ """
+ )
result = db.execute(query)
features = result.fetchone()[0]
@@ -1729,8 +1726,7 @@ async def get_project_features_geojson(db:Session, project_id:int):
def create_task_grid(db: Session, project_id: int, delta: int):
- """
- Creates a grid of tasks for a specified project.
+ """Creates a grid of tasks for a specified project.
Args:
db (Session): A database session.
@@ -1837,8 +1833,7 @@ def create_task_grid(db: Session, project_id: int, delta: int):
def get_json_from_zip(zip, filename: str, error_detail: str):
- """
- Gets a JSON object from a file in a ZIP archive.
+ """Gets a JSON object from a file in a ZIP archive.
Args:
zip (ZipFile): The ZIP archive to read from.
@@ -1859,8 +1854,7 @@ def get_json_from_zip(zip, filename: str, error_detail: str):
def get_outline_from_geojson_file_in_zip(
zip, filename: str, error_detail: str, feature_index: int = 0
):
- """
- Gets an outline from a GeoJSON file in a ZIP archive.
+ """Gets an outline from a GeoJSON file in a ZIP archive.
Args:
zip (ZipFile): The ZIP archive to read from.
@@ -1889,8 +1883,7 @@ def get_outline_from_geojson_file_in_zip(
def get_shape_from_json_str(feature: str, error_detail: str):
- """
- Gets a shape object from a JSON string representing a feature.
+ """Gets a shape object from a JSON string representing a feature.
Args:
feature (str): A JSON string representing a feature.
@@ -1911,8 +1904,7 @@ def get_shape_from_json_str(feature: str, error_detail: str):
def get_dbqrcode_from_file(zip, qr_filename: str, error_detail: str):
- """
- Gets a database QR code object from a file in a ZIP archive.
+ """Gets a database QR code object from a file in a ZIP archive.
Args:
zip (ZipFile): The ZIP archive to read from.
@@ -1949,8 +1941,7 @@ def get_dbqrcode_from_file(zip, qr_filename: str, error_detail: str):
def convert_to_app_project(db_project: db_models.DbProject):
- """
- Converts a database project object to an app project object.
+ """Converts a database project object to an app project object.
Args:
db_project (db_models.DbProject): The database project object to convert.
@@ -1977,8 +1968,7 @@ def convert_to_app_project(db_project: db_models.DbProject):
def convert_to_app_project_info(db_project_info: db_models.DbProjectInfo):
- """
- Converts a database project info object to an app project info object.
+ """Converts a database project info object to an app project info object.
Args:
db_project_info (db_models.DbProjectInfo): The database project info object to convert.
@@ -1994,8 +1984,7 @@ def convert_to_app_project_info(db_project_info: db_models.DbProjectInfo):
def convert_to_app_projects(db_projects: List[db_models.DbProject]):
- """
- Converts a list of database project objects to a list of app project objects.
+ """Converts a list of database project objects to a list of app project objects.
Args:
db_projects (List[db_models.DbProject]): The list of database project objects to convert.
@@ -2015,8 +2004,7 @@ def convert_to_app_projects(db_projects: List[db_models.DbProject]):
def convert_to_project_summary(db_project: db_models.DbProject):
- """
- Converts a database project object to a project summary object.
+ """Converts a database project object to a project summary object.
Args:
db_project (db_models.DbProject): The database project object to convert.
@@ -2039,7 +2027,9 @@ def convert_to_project_summary(db_project: db_models.DbProject):
summary.num_contributors = (
db_project.tasks_mapped + db_project.tasks_validated
) # TODO: get real number of contributors
- summary.organisation_logo = db_project.organisation.logo if db_project.organisation else None
+ summary.organisation_logo = (
+ db_project.organisation.logo if db_project.organisation else None
+ )
return summary
else:
@@ -2047,8 +2037,7 @@ def convert_to_project_summary(db_project: db_models.DbProject):
def convert_to_project_summaries(db_projects: List[db_models.DbProject]):
- """
- Converts a list of database project objects to a list of project summary objects.
+ """Converts a list of database project objects to a list of project summary objects.
Args:
db_projects (List[db_models.DbProject]): The list of database project objects to convert.
@@ -2068,8 +2057,7 @@ def convert_to_project_summaries(db_projects: List[db_models.DbProject]):
def convert_to_project_feature(db_project_feature: db_models.DbFeatures):
- """
- Converts a database feature object to a project feature object.
+ """Converts a database feature object to a project feature object.
Args:
db_project_feature (db_models.DbFeatures): The database feature object to convert.
@@ -2093,8 +2081,7 @@ def convert_to_project_feature(db_project_feature: db_models.DbFeatures):
def convert_to_project_features(db_project_features: List[db_models.DbFeatures]):
- """
- Converts a list of database feature objects to a list of project feature objects.
+ """Converts a list of database feature objects to a list of project feature objects.
Args:
db_project_features (List[db_models.DbFeatures]): The list of database feature objects to convert.
@@ -2113,8 +2100,7 @@ def convert_to_project_features(db_project_features: List[db_models.DbFeatures])
def get_project_features(db: Session, project_id: int, task_id: int = None):
- """
- Gets the features for a specified project and task.
+ """Gets the features for a specified project and task.
Args:
db (Session): A database session.
@@ -2141,8 +2127,7 @@ def get_project_features(db: Session, project_id: int, task_id: int = None):
async def get_extract_completion_count(project_id: int, db: Session):
- """
- Gets the extract completion count for a specified project.
+ """Gets the extract completion count for a specified project.
Args:
db (Session): A database session.
@@ -2160,8 +2145,7 @@ async def get_extract_completion_count(project_id: int, db: Session):
async def get_background_task_status(task_id: uuid.UUID, db: Session):
- """
- Gets the status of a background task.
+ """Gets the status of a background task.
Args:
task_id (uuid.UUID): The ID of the background task.
@@ -2181,8 +2165,7 @@ async def get_background_task_status(task_id: uuid.UUID, db: Session):
async def insert_background_task_into_database(
db: Session, task_id: uuid.UUID, name: str = None, project_id=None
):
- """
- Inserts a new background task into the database.
+ """Inserts a new background task into the database.
Args:
db (Session): A database session.
@@ -2206,8 +2189,7 @@ async def insert_background_task_into_database(
def update_background_task_status_in_database(
db: Session, task_id: uuid.UUID, status: int, message: str = None
):
- """
- Updates the status of a background task in the database.
+ """Updates the status of a background task in the database.
Args:
db (Session): A database session.
@@ -2237,8 +2219,7 @@ def add_features_into_database(
background_task_id: uuid.UUID,
feature_type: str,
):
- """
- Adds features into the database for a specified project.
+ """Adds features into the database for a specified project.
Args:
db (Session): A database session.
@@ -2411,7 +2392,8 @@ async def update_project_form(
# Get the features for this task.
# Postgis query to filter task inside this task outline and of this project
# Update those features and set task_id
- query = text(f"""UPDATE features
+ query = text(
+ f"""UPDATE features
SET task_id={task}
WHERE id in (
@@ -2419,12 +2401,14 @@ async def update_project_form(
FROM features
WHERE project_id={project_id} and ST_Intersects(geometry, '{task_obj.outline}'::Geometry)
- )""")
+ )"""
+ )
result = db.execute(query)
# Get the geojson of those features for this task.
- query = text(f"""SELECT jsonb_build_object(
+ query = text(
+ f"""SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', jsonb_agg(feature)
)
@@ -2437,7 +2421,8 @@ async def update_project_form(
) AS feature
FROM features
WHERE project_id={project_id} and task_id={task}
- ) features;""")
+ ) features;"""
+ )
result = db.execute(query)
features = result.fetchone()[0]
@@ -2477,7 +2462,8 @@ async def update_odk_credentials(
async def get_extracted_data_from_db(db: Session, project_id: int, outfile: str):
"""Get the geojson of those features for this project."""
- query = text(f"""SELECT jsonb_build_object(
+ query = text(
+ f"""SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', jsonb_agg(feature)
)
@@ -2490,7 +2476,8 @@ async def get_extracted_data_from_db(db: Session, project_id: int, outfile: str)
) AS feature
FROM features
WHERE project_id={project_id}
- ) features;""")
+ ) features;"""
+ )
result = db.execute(query)
features = result.fetchone()[0]
@@ -2531,7 +2518,8 @@ def get_project_tiles(
db.commit()
# Project Outline
- query = text(f"""SELECT jsonb_build_object(
+ query = text(
+ f"""SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', jsonb_agg(feature)
)
@@ -2543,7 +2531,8 @@ def get_project_tiles(
) AS feature
FROM projects
WHERE id={project_id}
- ) features;""")
+ ) features;"""
+ )
result = db.execute(query)
features = result.fetchone()[0]
@@ -2593,8 +2582,7 @@ def get_project_tiles(
async def get_mbtiles_list(db: Session, project_id: int):
- """
- Gets a list of MBTiles for a specified project.
+ """Gets a list of MBTiles for a specified project.
Args:
db (Session): A database session.
diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py
index f466f58cd6..fc2f5f5453 100644
--- a/src/backend/app/projects/project_routes.py
+++ b/src/backend/app/projects/project_routes.py
@@ -15,8 +15,6 @@
# You should have received a copy of the GNU General Public License
# along with FMTM. If not, see .
#
-from loguru import logger as log
-
import json
import os
import uuid
@@ -34,6 +32,7 @@
UploadFile,
)
from fastapi.responses import FileResponse
+from loguru import logger as log
from osm_fieldwork.make_data_extract import getChoices
from osm_fieldwork.xlsforms import xlsforms_path
from sqlalchemy.orm import Session
@@ -44,7 +43,6 @@
from ..tasks import tasks_crud
from . import project_crud, project_schemas, utils
-
router = APIRouter(
prefix="/projects",
tags=["projects"],
@@ -60,8 +58,7 @@ async def read_projects(
limit: int = 100,
db: Session = Depends(database.get_db),
):
- """
- Get a list of projects.
+ """Get a list of projects.
Args:
user_id (int, optional): The ID of the user to filter projects by. Defaults to None.
@@ -97,7 +94,9 @@ async def get_projet_details(project_id: int, db: Session = Depends(database.get
odk_central_password=project.odk_central_password,
)
- odk_details = await central_crud.get_project_full_details(project.odkid, odk_credentials)
+ odk_details = await central_crud.get_project_full_details(
+ project.odkid, odk_credentials
+ )
# Features count
query = f"select count(*) from features where project_id={project_id} and task_id is not null"
@@ -105,19 +104,18 @@ async def get_projet_details(project_id: int, db: Session = Depends(database.get
features = result.fetchone()[0]
return {
- 'id':project_id,
- 'name':odk_details['name'],
- 'createdAt':odk_details['createdAt'],
- 'tasks':odk_details['forms'],
- 'lastSubmission':odk_details['lastSubmission'],
- 'total_features':features
+ "id": project_id,
+ "name": odk_details["name"],
+ "createdAt": odk_details["createdAt"],
+ "tasks": odk_details["forms"],
+ "lastSubmission": odk_details["lastSubmission"],
+ "total_features": features,
}
@router.post("/near_me", response_model=project_schemas.ProjectSummary)
def get_task(lat: float, long: float, user_id: int = None):
- """
- Get a task near the specified location.
+ """Get a task near the specified location.
Args:
lat (float): The latitude of the location.
@@ -138,8 +136,7 @@ async def read_project_summaries(
limit: int = 100,
db: Session = Depends(database.get_db),
):
- """
- Get a list of project summaries.
+ """Get a list of project summaries.
Args:
user_id (int, optional): The ID of the user to filter projects by. Defaults to None.
@@ -163,8 +160,7 @@ async def read_project_summaries(
@router.get("/{project_id}", response_model=project_schemas.ProjectOut)
async def read_project(project_id: int, db: Session = Depends(database.get_db)):
- """
- Get a project by its ID.
+ """Get a project by its ID.
Args:
project_id (int): The ID of the project.
@@ -175,7 +171,7 @@ async def read_project(project_id: int, db: Session = Depends(database.get_db)):
Raises:
HTTPException: If the project is not found.
-
+
"""
project = project_crud.get_project_by_id(db, project_id)
if project:
@@ -186,8 +182,7 @@ async def read_project(project_id: int, db: Session = Depends(database.get_db)):
@router.delete("/delete/{project_id}")
async def delete_project(project_id: int, db: Session = Depends(database.get_db)):
- """
- Delete a project by its ID.
+ """Delete a project by its ID.
Args:
project_id (int): The ID of the project.
@@ -309,8 +304,7 @@ async def update_project(
project_info: project_schemas.BETAProjectUpload,
db: Session = Depends(database.get_db),
):
- """
- Update an existing project by its ID.
+ """Update an existing project by its ID.
Args:
id (int): The ID of the project to update.
@@ -322,7 +316,7 @@ async def update_project(
Raises:
HTTPException: If the project is not found.
-
+
"""
project = project_crud.update_project_info(db, project_info, id)
if project:
@@ -337,8 +331,7 @@ async def project_partial_update(
project_info: project_schemas.ProjectUpdate,
db: Session = Depends(database.get_db),
):
- """
- Partially update an existing project by its ID.
+ """Partially update an existing project by its ID.
Args:
id (int): The ID of the project to update.
@@ -350,7 +343,7 @@ async def project_partial_update(
Raises:
HTTPException: If the project is not found.
-
+
"""
# Update project informations
project = project_crud.partial_update_project_info(db, project_info, id)
@@ -369,8 +362,7 @@ async def upload_project_boundary_with_zip(
upload: UploadFile,
db: Session = Depends(database.get_db),
):
- """
- Upload a ZIP file with task geojson polygons and QR codes for an existing project.
+ """Upload a ZIP file with task geojson polygons and QR codes for an existing project.
Args:
project_id (int): The ID of the project to upload to.
@@ -384,7 +376,7 @@ async def upload_project_boundary_with_zip(
Raises:
HTTPException: If there is a connection error to ODK Central.
-
+
"""
r"""Upload a ZIP with task geojson polygons and QR codes for an existing project.
@@ -412,8 +404,7 @@ async def upload_custom_xls(
category: str = Form(...),
db: Session = Depends(database.get_db),
):
- """
- Upload a custom XLSForm to the database.
+ """Upload a custom XLSForm to the database.
Args:
upload (UploadFile): The XLSForm file to upload.
@@ -437,8 +428,7 @@ async def upload_multi_project_boundary(
upload: UploadFile = File(...),
db: Session = Depends(database.get_db),
):
- """
- Upload a multi-polygon project boundary in JSON format for a specified project ID.
+ """Upload a multi-polygon project boundary in JSON format for a specified project ID.
Args:
project_id (int): The ID of the project to which the boundary is being uploaded.
@@ -450,7 +440,7 @@ async def upload_multi_project_boundary(
Raises:
HTTPException: If the project ID does not exist in the database.
-
+
"""
log.debug(
"Uploading project boundary multipolygon for " f"project ID: {project_id}"
@@ -478,10 +468,9 @@ async def task_split(
upload: UploadFile = File(...),
no_of_buildings: int = Form(50),
has_data_extracts: bool = Form(False),
- db: Session = Depends(database.get_db)
- ):
- """
- Split a task into subtasks.
+ db: Session = Depends(database.get_db),
+):
+ """Split a task into subtasks.
Args:
upload (UploadFile): The file to split.
@@ -490,14 +479,15 @@ async def task_split(
Returns:
The result of splitting the task into subtasks.
-
- """
+ """
# read entire file
await upload.seek(0)
content = await upload.read()
- result = await project_crud.split_into_tasks(db, content, no_of_buildings, has_data_extracts)
+ result = await project_crud.split_into_tasks(
+ db, content, no_of_buildings, has_data_extracts
+ )
return result
@@ -522,7 +512,7 @@ async def upload_project_boundary(
Raises:
HTTPException: If the provided file is not valid or if the project ID does not exist in the database.
-
+
"""
# Validating for .geojson File.
file_name = os.path.splitext(upload.filename)
@@ -619,8 +609,7 @@ async def generate_files(
data_extracts: Optional[UploadFile] = File(None),
db: Session = Depends(database.get_db),
):
- """
- Generate required media files for tasks in the project based on the provided parameters.
+ """Generate required media files for tasks in the project based on the provided parameters.
Args:
background_tasks (BackgroundTasks): The background tasks object.
@@ -635,7 +624,7 @@ async def generate_files(
Raises:
HTTPException: If the project ID does not exist in the database or if an invalid file is provided.
-
+
"""
log.debug(f"Generating media files tasks for project: {project_id}")
contents = None
@@ -666,13 +655,15 @@ async def generate_files(
if config_file:
config_file_name = os.path.splitext(config_file.filename)
- config_file_ext = config_file_name[1]
+ config_file_ext = config_file_name[1]
if not config_file_ext == ".yaml":
- raise HTTPException(status_code=400, detail="Provide a valid .yaml config file")
+ raise HTTPException(
+ status_code=400, detail="Provide a valid .yaml config file"
+ )
await config_file.seek(0)
config_file_contents = await config_file.read()
project.form_config_file = config_file_contents
-
+
db.commit()
if data_extracts:
@@ -742,8 +733,7 @@ def get_project_features(
task_id: int = None,
db: Session = Depends(database.get_db),
):
- """
- Get all the features of a project.
+ """Get all the features of a project.
Args:
project_id (int): The project's ID.
@@ -761,9 +751,7 @@ def get_project_features(
async def generate_log(
project_id: int, uuid: uuid.UUID, db: Session = Depends(database.get_db)
):
-
- """
- Get the contents of a log file in a log format.
+ """Get the contents of a log file in a log format.
Args:
project_id (int): The project's ID.
@@ -794,8 +782,13 @@ async def generate_log(
with open("/opt/logs/create_project.json", "r") as log_file:
logs = [json.loads(line) for line in log_file]
-
- filtered_logs = [log.get("record",{}).get("message",None) for log in logs if log.get("record", {}).get("extra", {}).get("project_id") == project_id]
+
+ filtered_logs = [
+ log.get("record", {}).get("message", None)
+ for log in logs
+ if log.get("record", {}).get("extra", {}).get("project_id")
+ == project_id
+ ]
last_50_logs = filtered_logs[-50:]
logs = "\n".join(last_50_logs)
@@ -812,12 +805,11 @@ async def generate_log(
@router.get("/categories/")
async def get_categories():
- """
- Get all the categories from osm_fieldwork.
+ """Get all the categories from osm_fieldwork.
Returns:
A list of categories and their respective forms.
-
+
"""
categories = (
getChoices()
@@ -827,8 +819,7 @@ async def get_categories():
@router.post("/preview_tasks/")
async def preview_tasks(upload: UploadFile = File(...), dimension: int = Form(500)):
- """
- Preview tasks for a project.
+ """Preview tasks for a project.
Args:
upload (UploadFile): The boundary file to preview tasks for.
@@ -839,7 +830,7 @@ async def preview_tasks(upload: UploadFile = File(...), dimension: int = Form(50
Raises:
HTTPException: If an invalid file is provided.
-
+
"""
# Validating for .geojson File.
file_name = os.path.splitext(upload.filename)
@@ -861,11 +852,12 @@ async def preview_tasks(upload: UploadFile = File(...), dimension: int = Form(50
async def add_features(
background_tasks: BackgroundTasks,
upload: UploadFile = File(...),
- feature_type: str = Query(..., description="Select feature type ", enum=["buildings","lines"]),
+ feature_type: str = Query(
+ ..., description="Select feature type ", enum=["buildings", "lines"]
+ ),
db: Session = Depends(database.get_db),
):
- """
- Add features to a project.
+ """Add features to a project.
This endpoint allows you to add features to a project.
@@ -900,7 +892,7 @@ async def add_features(
db,
features,
background_task_id,
- feature_type
+ feature_type,
)
return True
@@ -972,8 +964,7 @@ async def update_project_category(
@router.get("/download_template/")
async def download_template(category: str, db: Session = Depends(database.get_db)):
- """
- Download a template based on the provided category.
+ """Download a template based on the provided category.
Args:
category (str): The category of the template.
@@ -1035,19 +1026,15 @@ async def download_task_boundaries(
@router.get("/features/download/")
-async def download_features(
- project_id: int,
- db: Session = Depends(database.get_db)
-):
+async def download_features(project_id: int, db: Session = Depends(database.get_db)):
"""Downloads the features of a project as a GeoJSON file.
-
- Args:
- project_id (int): The id of the project.
-
- Returns:
- Response: The HTTP response object containing the downloaded file.
- """
+ Args:
+ project_id (int): The id of the project.
+
+ Returns:
+ Response: The HTTP response object containing the downloaded file.
+ """
out = await project_crud.get_project_features_geojson(db, project_id)
headers = {
@@ -1144,15 +1131,16 @@ async def download_task_boundary_osm(
response = Response(content=content, media_type="application/xml")
return response
+
from sqlalchemy.sql import text
+
@router.get("/centroid/")
async def project_centroid(
- project_id:int = None,
- db: Session = Depends(database.get_db),
- ):
- """
- Get a centroid of each projects.
+ project_id: int = None,
+ db: Session = Depends(database.get_db),
+):
+ """Get a centroid of each projects.
Parameters:
project_id (int): The ID of the project.
@@ -1160,11 +1148,12 @@ async def project_centroid(
Returns:
List[Tuple[int, str]]: A list of tuples containing the task ID and the centroid as a string.
"""
-
- query = text(f"""SELECT id, ARRAY_AGG(ARRAY[ST_X(ST_Centroid(outline)), ST_Y(ST_Centroid(outline))]) AS centroid
+ query = text(
+ f"""SELECT id, ARRAY_AGG(ARRAY[ST_X(ST_Centroid(outline)), ST_Y(ST_Centroid(outline))]) AS centroid
FROM projects
WHERE {f"id={project_id}" if project_id else "1=1"}
- GROUP BY id;""")
+ GROUP BY id;"""
+ )
result = db.execute(query)
result_dict_list = [{"id": row[0], "centroid": row[1]} for row in result.fetchall()]
diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py
index f8578cb6df..7efb7f8454 100644
--- a/src/backend/app/projects/project_schemas.py
+++ b/src/backend/app/projects/project_schemas.py
@@ -16,7 +16,7 @@
# along with FMTM. If not, see .
#
-from typing import List, Union, Optional
+from typing import List, Optional, Union
from geojson_pydantic import Feature as GeojsonFeature
from pydantic import BaseModel
@@ -27,8 +27,7 @@
class ODKCentral(BaseModel):
- """
- Represents the configuration details for an ODK Central instance.
+ """Represents the configuration details for an ODK Central instance.
Attributes:
odk_central_url (str): The URL of the ODK Central instance.
@@ -36,14 +35,14 @@ class ODKCentral(BaseModel):
odk_central_password (str): The password for ODK Central.
"""
+
odk_central_url: str
odk_central_user: str
odk_central_password: str
class ProjectInfo(BaseModel):
- """
- Basic information about a project.
+ """Basic information about a project.
Attributes:
name (str): The project's name.
@@ -51,6 +50,7 @@ class ProjectInfo(BaseModel):
description (str): The full description of the project.
"""
+
name: str
short_description: str
description: str
@@ -63,8 +63,7 @@ class ProjectUpdate(BaseModel):
class BETAProjectUpload(BaseModel):
- """
- Data needed to upload a new project.
+ """Data needed to upload a new project.
Attributes:
author (User): The author of the project.
@@ -74,6 +73,7 @@ class BETAProjectUpload(BaseModel):
hashtags (Union[List[str], None]): List of hashtags for the project.
"""
+
author: User
project_info: ProjectInfo
xform_title: Union[str, None]
@@ -92,8 +92,7 @@ class Feature(BaseModel):
class ProjectSummary(BaseModel):
- """
- Summary view of project details.
+ """Summary view of project details.
Attributes:
id (int): The project's ID.
@@ -110,6 +109,7 @@ class ProjectSummary(BaseModel):
hashtags (List[str]): List of project hashtags.
"""
+
id: int = -1
priority: ProjectPriority = ProjectPriority.MEDIUM
priority_str: str = priority.name
@@ -127,8 +127,7 @@ class ProjectSummary(BaseModel):
class ProjectBase(BaseModel):
- """
- Base structure of a project.
+ """Base structure of a project.
Attributes:
id (int): The project's ID.
@@ -142,6 +141,7 @@ class ProjectBase(BaseModel):
hashtags (List[str]): List of project hashtags.
"""
+
id: int
odkid: int
author: User
@@ -156,13 +156,10 @@ class ProjectBase(BaseModel):
class ProjectOut(ProjectBase):
- """
- Detailed project information.
+ """Detailed project information.
Inherits from ProjectBase and provides additional information.
"""
- pass
-
-
+ pass
diff --git a/src/backend/app/projects/utils.py b/src/backend/app/projects/utils.py
index 7d43a47127..d89b61a6d1 100644
--- a/src/backend/app/projects/utils.py
+++ b/src/backend/app/projects/utils.py
@@ -9,12 +9,12 @@
HTTPException,
UploadFile,
)
-from loguru import logger as log
from sqlalchemy.orm import Session
from ..db import database
from . import project_crud
+
async def generate_files(
background_tasks: BackgroundTasks,
project_id: int,
@@ -22,8 +22,7 @@ async def generate_files(
upload: Optional[UploadFile] = File(None),
db: Session = Depends(database.get_db),
):
- """
- Generate required media files and tasks for a project based on the provided parameters.
+ """Generate required media files and tasks for a project based on the provided parameters.
This function accepts a project ID, category, custom form flag, and an uploaded file as inputs.
The generated files are associated with the project ID and stored in the database.
@@ -43,7 +42,6 @@ async def generate_files(
dict: A dictionary containing a success message and the associated task ID.
"""
-
contents = None
xform_title = None
@@ -53,11 +51,11 @@ async def generate_files(
status_code=428, detail=f"Project with id {project_id} does not exist"
)
- project.data_extract_type = 'polygon' if extract_polygon else 'centroid'
+ project.data_extract_type = "polygon" if extract_polygon else "centroid"
db.commit()
if upload:
- file_ext = 'xls'
+ file_ext = "xls"
contents = upload
# generate a unique task ID using uuid
@@ -76,7 +74,7 @@ async def generate_files(
contents,
None,
xform_title,
- file_ext if upload else 'xls',
+ file_ext if upload else "xls",
background_task_id,
)
diff --git a/src/backend/app/submission/submission_crud.py b/src/backend/app/submission/submission_crud.py
index 2a95ae1e8a..47a5a5985c 100644
--- a/src/backend/app/submission/submission_crud.py
+++ b/src/backend/app/submission/submission_crud.py
@@ -15,33 +15,29 @@
# You should have received a copy of the GNU General Public License
# along with FMTM. If not, see .
#
-from loguru import logger as log
-
import asyncio
-import os
-import zipfile
import concurrent.futures
-import threading
import csv
import io
+import json
import os
+import threading
import zipfile
-import json
-from datetime import datetime
+from pathlib import Path
+
from fastapi import HTTPException, Response
from fastapi.responses import FileResponse
+from loguru import logger as log
+from osm_fieldwork.json2osm import JsonDump
from sqlalchemy.orm import Session
from ..central.central_crud import get_odk_form, get_odk_project
-from ..tasks import tasks_crud
from ..projects import project_crud, project_schemas
-from osm_fieldwork.json2osm import JsonDump
-from pathlib import Path
+from ..tasks import tasks_crud
def get_submission_of_project(db: Session, project_id: int, task_id: int = None):
- """
- Gets the submission of project.
+ """Gets the submission of project.
Args:
db (Session): A database session.
@@ -89,7 +85,6 @@ def get_submission_of_project(db: Session, project_id: int, task_id: int = None)
data = []
for id in task_list:
-
# XML Form Id is a combination or project_name, category and task_id
xml_form_id = f"{project_name}_{form_category}_{id}".split("_")[2]
submission_list = xform.listSubmissions(odkid, xml_form_id)
@@ -110,8 +105,7 @@ def get_submission_of_project(db: Session, project_id: int, task_id: int = None)
def get_forms_of_project(db: Session, project_id: int):
- """
- Gets the forms of a project.
+ """Gets the forms of a project.
Args:
db (Session): A database session.
@@ -134,8 +128,7 @@ def get_forms_of_project(db: Session, project_id: int):
def list_app_users_or_project(db: Session, project_id: int):
- """
- Lists the app users of a project.
+ """Lists the app users of a project.
Args:
db (Session): A database session.
@@ -157,8 +150,7 @@ def list_app_users_or_project(db: Session, project_id: int):
def create_zip_file(files, output_file_path):
- """
- Creates a zip file containing the specified files.
+ """Creates a zip file containing the specified files.
Args:
files (list): A list of file paths to include in the zip file.
@@ -208,10 +200,8 @@ def create_zip_file(files, output_file_path):
# return osmoutfile
-
async def convert_json_to_osm_xml(file_path):
- """
- Converts a JSON file to an OSM XML file.
+ """Converts a JSON file to an OSM XML file.
Args:
file_path (str): The path to the JSON file to be converted.
@@ -219,7 +209,6 @@ async def convert_json_to_osm_xml(file_path):
Returns:
str: The path to the created OSM XML file.
"""
-
# TODO refactor to simply use json2osm(file_path)
jsonin = JsonDump()
infile = Path(file_path)
@@ -237,12 +226,12 @@ async def process_entry_async(entry):
return None
if len(feature) > 0:
if "lat" not in feature["attrs"]:
- if 'geometry' in feature['tags']:
- if type(feature['tags']['geometry']) == str:
- coords = list(feature['tags']['geometry'])
+ if "geometry" in feature["tags"]:
+ if type(feature["tags"]["geometry"]) == str:
+ coords = list(feature["tags"]["geometry"])
else:
- coords = feature['tags']['geometry']['coordinates']
- feature['attrs'] = {'lat': coords[1], 'lon': coords[0]}
+ coords = feature["tags"]["geometry"]["coordinates"]
+ feature["attrs"] = {"lat": coords[1], "lon": coords[0]}
else:
log.warning("Bad record! %r" % feature)
return None
@@ -264,8 +253,7 @@ async def write_osm_async(features):
async def convert_json_to_osm(file_path):
- """
- Converts a JSON file to an OSM XML file and a GeoJSON file.
+ """Converts a JSON file to an OSM XML file and a GeoJSON file.
Args:
file_path (str): The path to the JSON file to be converted.
@@ -273,7 +261,6 @@ async def convert_json_to_osm(file_path):
Returns:
Tuple[str, str]: A tuple containing the paths to the created OSM XML and GeoJSON files.
"""
-
# TODO refactor to simply use json2osm(file_path)
jsonin = JsonDump()
infile = Path(file_path)
@@ -295,14 +282,14 @@ async def convert_json_to_osm(file_path):
continue
if len(feature) > 0:
if "lat" not in feature["attrs"]:
- if 'geometry' in feature['tags']:
- if type(feature['tags']['geometry']) == str:
- coords = list(feature['tags']['geometry'])
+ if "geometry" in feature["tags"]:
+ if type(feature["tags"]["geometry"]) == str:
+ coords = list(feature["tags"]["geometry"])
# del feature['tags']['geometry']
else:
- coords = feature['tags']['geometry']['coordinates']
+ coords = feature["tags"]["geometry"]["coordinates"]
# del feature['tags']['geometry']
- feature['attrs'] = {'lat': coords[1], 'lon': coords[0]}
+ feature["attrs"] = {"lat": coords[1], "lon": coords[0]}
else:
log.warning("Bad record! %r" % feature)
continue
@@ -317,8 +304,7 @@ async def convert_json_to_osm(file_path):
async def convert_to_osm_for_task(odk_id: int, form_id: int, xform: any):
- """
- Converts submission data from ODK Central to OSM XML and GeoJSON files.
+ """Converts submission data from ODK Central to OSM XML and GeoJSON files.
Args:
odk_id (int): The ID of the ODK project.
@@ -328,7 +314,6 @@ async def convert_to_osm_for_task(odk_id: int, form_id: int, xform: any):
Returns:
Tuple[str, str]: A tuple containing the paths to the created OSM XML and GeoJSON files.
"""
-
# This file stores the submission data.
file_path = f"/tmp/{odk_id}_{form_id}.json"
@@ -346,8 +331,7 @@ async def convert_to_osm_for_task(odk_id: int, form_id: int, xform: any):
async def convert_to_osm(db: Session, project_id: int, task_id: int):
- """
- Converts submission data from a project to OSM XML and GeoJSON files and returns a ZIP file containing the converted files.
+ """Converts submission data from a project to OSM XML and GeoJSON files and returns a ZIP file containing the converted files.
Args:
db (Session): A database session.
@@ -357,7 +341,6 @@ async def convert_to_osm(db: Session, project_id: int, task_id: int):
Returns:
FileResponse: A FileResponse object containing the ZIP file with the converted OSM XML and GeoJSON files.
"""
-
project_info = project_crud.get_project(db, project_id)
# Return exception if project is not found
@@ -388,7 +371,7 @@ async def convert_to_osm(db: Session, project_id: int, task_id: int):
# Submission JSON
if task_id:
submission = xform.getSubmissions(odkid, task_id, None, False, True)
- submission = (json.loads(submission))['value']
+ submission = (json.loads(submission))["value"]
else:
submission = await get_all_submissions(db, project_id)
@@ -399,29 +382,30 @@ async def convert_to_osm(db: Session, project_id: int, task_id: int):
jsoninfile = "/tmp/json_infile.json"
# Write the submission to a file
- with open(jsoninfile, 'w') as f:
+ with open(jsoninfile, "w") as f:
f.write(json.dumps(submission))
# Convert the submission to osm xml format
osmoutfile, jsonoutfile = await convert_json_to_osm(jsoninfile)
if osmoutfile and jsonoutfile:
-
- #FIXME: Need to fix this when generating osm file
+ # FIXME: Need to fix this when generating osm file
# Remove the extra closing tag from the end of the file
- with open(osmoutfile, 'r') as f:
+ with open(osmoutfile, "r") as f:
osmoutfile_data = f.read()
# Find the last index of the closing tag
- last_osm_index = osmoutfile_data.rfind('')
+ last_osm_index = osmoutfile_data.rfind("")
# Remove the extra closing tag from the end
- processed_xml_string = osmoutfile_data[:last_osm_index] + osmoutfile_data[last_osm_index + len(''):]
+ processed_xml_string = (
+ osmoutfile_data[:last_osm_index]
+ + osmoutfile_data[last_osm_index + len("") :]
+ )
# Write the modified XML data back to the file
- with open(osmoutfile, 'w') as f:
+ with open(osmoutfile, "w") as f:
f.write(processed_xml_string)
-
# Add the files to the ZIP file
with zipfile.ZipFile(final_zip_file_path, mode="a") as final_zip_file:
final_zip_file.write(osmoutfile)
@@ -430,10 +414,8 @@ async def convert_to_osm(db: Session, project_id: int, task_id: int):
return FileResponse(final_zip_file_path)
-
def download_submission_for_project(db, project_id):
- """
- Downloads submission data for a project.
+ """Downloads submission data for a project.
Args:
db (Session): A database session.
@@ -442,7 +424,7 @@ def download_submission_for_project(db, project_id):
Returns:
None
"""
- print('Download submission for a project')
+ print("Download submission for a project")
project_info = project_crud.get_project(db, project_id)
@@ -466,7 +448,9 @@ def download_submission_for_project(db, project_id):
xform = get_odk_form(odk_credentials)
def download_submission_for_task(task_id):
- log.info(f"Thread {threading.current_thread().name} - Downloading submission for Task ID {task_id}")
+ log.info(
+ f"Thread {threading.current_thread().name} - Downloading submission for Task ID {task_id}"
+ )
xml_form_id = f"{project_name}_{form_category}_{task_id}".split("_")[2]
file = xform.getSubmissionMedia(odkid, xml_form_id)
file_path = f"{project_name}_{form_category}_submission_{task_id}.zip"
@@ -475,18 +459,22 @@ def download_submission_for_task(task_id):
return file_path
def extract_files(zip_file_path):
- log.info(f"Thread {threading.current_thread().name} - Extracting files from {zip_file_path}")
+ log.info(
+ f"Thread {threading.current_thread().name} - Extracting files from {zip_file_path}"
+ )
with zipfile.ZipFile(zip_file_path, "r") as zip_file:
extract_dir = os.path.splitext(zip_file_path)[0]
zip_file.extractall(extract_dir)
return [os.path.join(extract_dir, f) for f in zip_file.namelist()]
-
with concurrent.futures.ThreadPoolExecutor() as executor:
task_list = [x.id for x in project_tasks]
# Download submissions using thread pool
- futures = {executor.submit(download_submission_for_task, task_id): task_id for task_id in task_list}
+ futures = {
+ executor.submit(download_submission_for_task, task_id): task_id
+ for task_id in task_list
+ }
files = []
for future in concurrent.futures.as_completed(futures):
@@ -494,20 +482,30 @@ def extract_files(zip_file_path):
try:
file_path = future.result()
files.append(file_path)
- log.info(f"Thread {threading.current_thread().name} - Task {task_id} - Download completed.")
+ log.info(
+ f"Thread {threading.current_thread().name} - Task {task_id} - Download completed."
+ )
except Exception as e:
- log.error(f"Thread {threading.current_thread().name} - Error occurred while downloading submission for task {task_id}: {e}")
+ log.error(
+ f"Thread {threading.current_thread().name} - Error occurred while downloading submission for task {task_id}: {e}"
+ )
# Extract files using thread pool
extracted_files = []
- futures = {executor.submit(extract_files, file_path): file_path for file_path in files}
+ futures = {
+ executor.submit(extract_files, file_path): file_path for file_path in files
+ }
for future in concurrent.futures.as_completed(futures):
file_path = futures[future]
try:
extracted_files.extend(future.result())
- log.info(f"Thread {threading.current_thread().name} - Extracted files from {file_path}")
+ log.info(
+ f"Thread {threading.current_thread().name} - Extracted files from {file_path}"
+ )
except Exception as e:
- log.error(f"Thread {threading.current_thread().name} - Error occurred while extracting files from {file_path}: {e}")
+ log.error(
+ f"Thread {threading.current_thread().name} - Error occurred while extracting files from {file_path}: {e}"
+ )
# Create a new ZIP file for the extracted files
final_zip_file_path = f"{project_name}_{form_category}_submissions_final.zip"
@@ -536,8 +534,7 @@ async def get_all_submissions(db: Session, project_id):
def get_project_submission(db: Session, project_id: int):
- """
- Gets submission data for a project.
+ """Gets submission data for a project.
Args:
db (Session): A database session.
@@ -571,15 +568,13 @@ def get_project_submission(db: Session, project_id: int):
task_list = [x.id for x in project_tasks]
for id in task_list:
- xml_form_id = f"{project_name}_{form_category}_{id}".split("_")[
- 2]
- file = xform.getSubmissions(
- odkid, xml_form_id, None, False, True)
+ xml_form_id = f"{project_name}_{form_category}_{id}".split("_")[2]
+ file = xform.getSubmissions(odkid, xml_form_id, None, False, True)
if not file:
json_data = None
else:
json_data = json.loads(file)
- json_data_value = json_data.get('value')
+ json_data_value = json_data.get("value")
if json_data_value:
submissions.extend(json_data_value)
@@ -587,8 +582,7 @@ def get_project_submission(db: Session, project_id: int):
def download_submission(db: Session, project_id: int, task_id: int, export_json: bool):
- """
- Downloads submission data for a project.
+ """Downloads submission data for a project.
Args:
db (Session): A database session.
@@ -599,7 +593,6 @@ def download_submission(db: Session, project_id: int, task_id: int, export_json:
Returns:
Union[FileResponse, Response]: A FileResponse object containing the downloaded submission data as a ZIP or JSON file.
"""
-
project_info = project_crud.get_project(db, project_id)
# Return empty list if project is not found
@@ -633,11 +626,9 @@ def download_submission(db: Session, project_id: int, task_id: int, export_json:
files = []
for id in task_list:
-
# XML Form Id is a combination or project_name, category and task_id
# FIXME: fix xml_form_id
- xml_form_id = f"{project_name}_{form_category}_{id}".split("_")[
- 2]
+ xml_form_id = f"{project_name}_{form_category}_{id}".split("_")[2]
file = xform.getSubmissionMedia(odkid, xml_form_id)
# Create a new output file for each submission
@@ -661,15 +652,16 @@ def download_submission(db: Session, project_id: int, task_id: int, export_json:
] # Add the extracted file paths to the list of extracted files
# Create a new ZIP file for the extracted files
- final_zip_file_path = f"{project_name}_{form_category}_submissions_final.zip"
+ final_zip_file_path = (
+ f"{project_name}_{form_category}_submissions_final.zip"
+ )
with zipfile.ZipFile(final_zip_file_path, mode="w") as final_zip_file:
for file_path in extracted_files:
final_zip_file.write(file_path)
return FileResponse(final_zip_file_path)
else:
- xml_form_id = f"{project_name}_{form_category}_{task_id}".split("_")[
- 2]
+ xml_form_id = f"{project_name}_{form_category}_{task_id}".split("_")[2]
file = xform.getSubmissionMedia(odkid, xml_form_id)
with open(file_path, "wb") as f:
f.write(file.content)
@@ -685,32 +677,29 @@ def download_submission(db: Session, project_id: int, task_id: int, export_json:
if task_id is None:
task_list = [x.id for x in project_tasks]
for id in task_list:
- xml_form_id = f"{project_name}_{form_category}_{id}".split("_")[
- 2]
- file = xform.getSubmissions(
- odkid, xml_form_id, None, False, True)
+ xml_form_id = f"{project_name}_{form_category}_{id}".split("_")[2]
+ file = xform.getSubmissions(odkid, xml_form_id, None, False, True)
if not file:
json_data = None
else:
json_data = json.loads(file)
- json_data_value = json_data.get('value')
+ json_data_value = json_data.get("value")
if json_data_value:
files.extend(json_data_value)
else:
- xml_form_id = f"{project_name}_{form_category}_{task_id}".split("_")[
- 2]
+ xml_form_id = f"{project_name}_{form_category}_{task_id}".split("_")[2]
file = xform.getSubmissions(odkid, xml_form_id, None, False, True)
json_data = json.loads(file)
response_content = json.dumps(
- files if task_id is None else json_data, indent=4).encode()
+ files if task_id is None else json_data, indent=4
+ ).encode()
return Response(content=response_content, headers=headers)
def get_submission_points(db: Session, project_id: int, task_id: int = None):
- """
- Gets the submission points of a project.
+ """Gets the submission points of a project.
Args:
db (Session): A database session.
@@ -752,8 +741,7 @@ def get_submission_points(db: Session, project_id: int, task_id: int = None):
# Open the zipfile
with zipfile.ZipFile(response_file_obj, "r") as zip_ref:
# Find the CSV file in the zipfile (assuming it has a .csv extension)
- csv_filename = [
- f for f in zip_ref.namelist() if f.endswith(".csv")][0]
+ csv_filename = [f for f in zip_ref.namelist() if f.endswith(".csv")][0]
# Open the CSV file
with zip_ref.open(csv_filename) as csv_file:
# Read the CSV data
@@ -763,8 +751,7 @@ def get_submission_points(db: Session, project_id: int, task_id: int = None):
# Check if the row contains the 'warmup-Latitude' and 'warmup-Longitude' columns
# FIXME: fix the column names (they might not be same warmup-Latitude and warmup-Longitude)
if "warmup-Latitude" in row and "warmup-Longitude" in row:
- point = (row["warmup-Latitude"],
- row["warmup-Longitude"])
+ point = (row["warmup-Latitude"], row["warmup-Longitude"])
# Create a GeoJSON Feature object
geometry.append(
@@ -782,10 +769,8 @@ def get_submission_points(db: Session, project_id: int, task_id: int = None):
return None
-async def get_submission_count_of_a_project(db:Session,
- project_id: int):
- """
- Gets the submission count for a project.
+async def get_submission_count_of_a_project(db: Session, project_id: int):
+ """Gets the submission count for a project.
Args:
db (Session): A database session.
@@ -794,7 +779,6 @@ async def get_submission_count_of_a_project(db:Session,
Returns:
int: The submission count for the specified project.
"""
-
project_info = project_crud.get_project(db, project_id)
# Return empty list if project is not found
@@ -820,15 +804,13 @@ async def get_submission_count_of_a_project(db:Session,
task_list = [x.id for x in project_tasks]
for id in task_list:
- xml_form_id = f"{project_name}_{form_category}_{id}".split("_")[
- 2]
- file = xform.getSubmissions(
- odkid, xml_form_id, None, False, True)
+ xml_form_id = f"{project_name}_{form_category}_{id}".split("_")[2]
+ file = xform.getSubmissions(odkid, xml_form_id, None, False, True)
if not file:
json_data = None
else:
json_data = json.loads(file)
- json_data_value = json_data.get('value')
+ json_data_value = json_data.get("value")
if json_data_value:
files.extend(json_data_value)
diff --git a/src/backend/app/submission/submission_routes.py b/src/backend/app/submission/submission_routes.py
index ba19285d3a..35f8bc70bc 100644
--- a/src/backend/app/submission/submission_routes.py
+++ b/src/backend/app/submission/submission_routes.py
@@ -15,17 +15,17 @@
# You should have received a copy of the GNU General Public License
# along with FMTM. If not, see .
#
-import os
import json
-from fastapi import APIRouter, Depends, HTTPException, Response
-from ..projects import project_crud, project_schemas
-from loguru import logger as log
-from sqlalchemy.orm import Session
+import os
+
+from fastapi import APIRouter, Depends, Response
from fastapi.responses import FileResponse
from osm_fieldwork.odk_merge import OdkMerge
from osm_fieldwork.osmfile import OsmFile
-from ..projects import project_crud
+from sqlalchemy.orm import Session
+
from ..db import database
+from ..projects import project_crud
from . import submission_crud
router = APIRouter(
@@ -42,8 +42,7 @@ async def read_submissions(
task_id: int = None,
db: Session = Depends(database.get_db),
):
- """
- Returns the submission made in the project.
+ """Returns the submission made in the project.
Args:
project_id (int): The ID of the project. This endpoint returns the submission made in this project.
@@ -61,8 +60,7 @@ async def list_forms(
project_id: int,
db: Session = Depends(database.get_db),
):
- """
- Returns the list of forms in the odk central.
+ """Returns the list of forms in the odk central.
Args:
project_id (int): The ID of the project. This endpoint returns the list of forms in this project.
@@ -79,8 +77,7 @@ async def list_app_users(
project_id: int,
db: Session = Depends(database.get_db),
):
- """
- Returns the list of app users in a project.
+ """Returns the list of app users in a project.
Args:
project_id (int): The ID of the project. This endpoint returns the list of app users in this project.
@@ -99,8 +96,7 @@ async def download_submission(
export_json: bool = True,
db: Session = Depends(database.get_db),
):
- """
- Downloads the submission made in a project.
+ """Downloads the submission made in a project.
Args:
project_id (int): The ID of the project. This endpoint returns the submission made in this project.
@@ -124,8 +120,7 @@ async def submission_points(
task_id: int = None,
db: Session = Depends(database.get_db),
):
- """
- Returns the submission points of a project.
+ """Returns the submission points of a project.
Args:
project_id (int): The ID of the project. This endpoint returns the submission points of this project.
@@ -144,9 +139,7 @@ async def convert_to_osm(
task_id: int = None,
db: Session = Depends(database.get_db),
):
-
- """
- Converts submission data to OSM format.
+ """Converts submission data to OSM format.
Args:
project_id (int): The ID of the project. This endpoint converts submission data for this project.
@@ -163,9 +156,8 @@ async def convert_to_osm(
async def get_submission_count(
project_id: int,
db: Session = Depends(database.get_db),
- ):
- """
- Returns the submission count for a project.
+):
+ """Returns the submission count for a project.
Args:
project_id (int): The ID of the project. This endpoint returns the submission count for this project.
@@ -181,9 +173,8 @@ async def get_submission_count(
async def conflate_osm_date(
project_id: int,
db: Session = Depends(database.get_db),
- ):
- """
- Conflates OSM data for a project.
+):
+ """Conflates OSM data for a project.
Args:
project_id (int): The ID of the project. This endpoint conflates OSM data for this project.
@@ -192,7 +183,6 @@ async def conflate_osm_date(
Returns:
Any: The conflated OSM data for the specified project.
"""
-
# Submission JSON
submission = submission_crud.get_all_submissions(db, project_id)
@@ -213,28 +203,31 @@ async def conflate_osm_date(
os.remove(jsoninfile)
# Write the submission to a file
- with open(jsoninfile, 'w') as f:
+ with open(jsoninfile, "w") as f:
f.write(json.dumps(submission))
# Convert the submission to osm xml format
osmoutfile, jsonoutfile = await submission_crud.convert_json_to_osm(jsoninfile)
# Remove the extra closing tag from the end of the file
- with open(osmoutfile, 'r') as f:
+ with open(osmoutfile, "r") as f:
osmoutfile_data = f.read()
# Find the last index of the closing tag
- last_osm_index = osmoutfile_data.rfind('')
+ last_osm_index = osmoutfile_data.rfind("")
# Remove the extra closing tag from the end
- processed_xml_string = osmoutfile_data[:last_osm_index] + osmoutfile_data[last_osm_index + len(''):]
-
+ processed_xml_string = (
+ osmoutfile_data[:last_osm_index]
+ + osmoutfile_data[last_osm_index + len("") :]
+ )
+
# Write the modified XML data back to the file
- with open(osmoutfile, 'w') as f:
+ with open(osmoutfile, "w") as f:
f.write(processed_xml_string)
odkf = OsmFile(outfile)
osm = odkf.loadFile(osmoutfile)
if osm:
- odk_merge = OdkMerge(data_extracts_file,None)
+ odk_merge = OdkMerge(data_extracts_file, None)
data = odk_merge.conflateData(osm)
return data
return []
@@ -244,7 +237,7 @@ async def conflate_osm_date(
async def get_osm_xml(
project_id: int,
db: Session = Depends(database.get_db),
- ):
+):
# JSON FILE PATH
jsoninfile = f"/tmp/{project_id}_json_infile.json"
@@ -256,22 +249,25 @@ async def get_osm_xml(
submission = await submission_crud.get_all_submissions(db, project_id)
# Write the submission to a file
- with open(jsoninfile, 'w') as f:
+ with open(jsoninfile, "w") as f:
f.write(json.dumps(submission))
# Convert the submission to osm xml format
osmoutfile = await submission_crud.convert_json_to_osm_xml(jsoninfile)
# Remove the extra closing tag from the end of the file
- with open(osmoutfile, 'r') as f:
+ with open(osmoutfile, "r") as f:
osmoutfile_data = f.read()
# Find the last index of the closing tag
- last_osm_index = osmoutfile_data.rfind('')
+ last_osm_index = osmoutfile_data.rfind("")
# Remove the extra closing tag from the end
- processed_xml_string = osmoutfile_data[:last_osm_index] + osmoutfile_data[last_osm_index + len(''):]
+ processed_xml_string = (
+ osmoutfile_data[:last_osm_index]
+ + osmoutfile_data[last_osm_index + len("") :]
+ )
# Write the modified XML data back to the file
- with open(osmoutfile, 'w') as f:
+ with open(osmoutfile, "w") as f:
f.write(processed_xml_string)
# Create a plain XML response
diff --git a/src/backend/app/tasks/tasks_crud.py b/src/backend/app/tasks/tasks_crud.py
index c82d088f4f..90d9a92481 100644
--- a/src/backend/app/tasks/tasks_crud.py
+++ b/src/backend/app/tasks/tasks_crud.py
@@ -15,20 +15,21 @@
# You should have received a copy of the GNU General Public License
# along with FMTM. If not, see .
#
-from loguru import logger as log
-
import base64
from typing import List
from fastapi import HTTPException
from geoalchemy2.shape import from_shape
from geojson import dump
+from loguru import logger as log
from osm_fieldwork.make_data_extract import PostgresClient
from shapely.geometry import shape
from sqlalchemy import column, select, table
from sqlalchemy.orm import Session
from sqlalchemy.sql import text
+from app.config import settings
+
from ..central import central_crud
from ..db import db_models
from ..db.postgis_utils import geometry_to_geojson, get_centroid
@@ -40,7 +41,6 @@
from ..projects import project_crud
from ..tasks import tasks_schemas
from ..users import user_crud
-from app.config import settings
async def get_task_count_in_project(db: Session, project_id: int):
@@ -51,11 +51,13 @@ async def get_task_count_in_project(db: Session, project_id: int):
def get_task_lists(db: Session, project_id: int):
"""Get a list of tasks for a project."""
- query = text("""
+ query = text(
+ """
SELECT id
FROM tasks
WHERE project_id = :project_id
- """)
+ """
+ )
# Then execute the query with the desired parameter
result = db.execute(query, {"project_id": project_id})
@@ -68,8 +70,7 @@ def get_task_lists(db: Session, project_id: int):
def get_tasks(
db: Session, project_id: int, user_id: int, skip: int = 0, limit: int = 1000
):
- """
- Get a list of tasks for a project or user.
+ """Get a list of tasks for a project or user.
Args:
db (Session): Database session.
@@ -110,8 +111,7 @@ def get_task(db: Session, task_id: int, db_obj: bool = False):
def update_task_status(db: Session, user_id: int, task_id: int, new_status: TaskStatus):
- """
- Update the status of a task.
+ """Update the status of a task.
Args:
db (Session): Database session.
@@ -125,7 +125,6 @@ def update_task_status(db: Session, user_id: int, task_id: int, new_status: Task
Returns:
Task: Updated Task object.
"""
-
if not user_id:
raise HTTPException(status_code=400, detail="User id required.")
@@ -138,10 +137,11 @@ def update_task_status(db: Session, user_id: int, task_id: int, new_status: Task
db_task = get_task(db, task_id, db_obj=True)
if db_task:
- if db_task.task_status in [TaskStatus.LOCKED_FOR_MAPPING, TaskStatus.LOCKED_FOR_VALIDATION]:
- if not (
- user_id is not db_task.locked_by
- ):
+ if db_task.task_status in [
+ TaskStatus.LOCKED_FOR_MAPPING,
+ TaskStatus.LOCKED_FOR_VALIDATION,
+ ]:
+ if not (user_id is not db_task.locked_by):
raise HTTPException(
status_code=401,
detail=f"User {user_id} with username {db_user.username} has not locked this task.",
@@ -194,8 +194,7 @@ def update_qrcode(
qr_id: int,
project_id: int,
):
- """
- Update the QR code for a task.
+ """Update the QR code for a task.
Args:
db (Session): Database session.
@@ -227,8 +226,7 @@ def update_qrcode(
def create_task_history_for_status_change(
db_task: db_models.DbTask, new_status: TaskStatus, db_user: db_models.DbUser
):
- """
- Create a task history entry for a status change.
+ """Create a task history entry for a status change.
Args:
db_task (db_models.DbTask): Database task object.
@@ -268,8 +266,7 @@ def create_task_history_for_status_change(
def convert_to_app_history(db_histories: List[db_models.DbTaskHistory]):
- """
- Convert a list of database task history entries to application task history entries.
+ """Convert a list of database task history entries to application task history entries.
Args:
db_histories (List[db_models.DbTaskHistory]): List of database task history entries.
@@ -292,8 +289,7 @@ def convert_to_app_history(db_histories: List[db_models.DbTaskHistory]):
def convert_to_app_task(db_task: db_models.DbTask):
- """
- Convert a database task object to an application task object.
+ """Convert a database task object to an application task object.
Args:
db_task (db_models.DbTask): Database task object.
@@ -332,9 +328,7 @@ def convert_to_app_task(db_task: db_models.DbTask):
log.debug("Task currently locked by user " f"{app_task.locked_by_username}")
if db_task.qr_code:
- log.debug(
- f"QR code found for task ID {db_task.id}. Converting to base64"
- )
+ log.debug(f"QR code found for task ID {db_task.id}. Converting to base64")
app_task.qr_code_base64 = base64.b64encode(db_task.qr_code.image)
else:
log.warning(f"No QR code found for task ID {db_task.id}")
@@ -369,8 +363,7 @@ def get_qr_codes_for_task(
db: Session,
task_id: int,
):
- """
- Get the QR code for a task.
+ """Get the QR code for a task.
Args:
db (Session): Database session.
diff --git a/src/backend/app/tasks/tasks_routes.py b/src/backend/app/tasks/tasks_routes.py
index 02fa5e8d06..477d411321 100644
--- a/src/backend/app/tasks/tasks_routes.py
+++ b/src/backend/app/tasks/tasks_routes.py
@@ -17,20 +17,18 @@
#
import json
-import asyncio
from typing import List
-from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
+from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from sqlalchemy.orm import Session
+from sqlalchemy.sql import text
+from ..central import central_crud
from ..db import database
from ..models.enums import TaskStatus
+from ..projects import project_crud, project_schemas
from ..users import user_schemas
from . import tasks_crud, tasks_schemas
-from ..projects import project_crud, project_schemas
-from ..central import central_crud
-from sqlalchemy.sql import text
-
router = APIRouter(
prefix="/tasks",
@@ -45,9 +43,8 @@ async def read_task_list(
project_id: int,
limit: int = 1000,
db: Session = Depends(database.get_db),
- ):
- """
- Get a list of tasks for a project.
+):
+ """Get a list of tasks for a project.
Args:
project_id (int): Project ID.
@@ -60,13 +57,12 @@ async def read_task_list(
Returns:
List[TaskOut]: List of TaskOut objects.
"""
-
tasks = tasks_crud.get_tasks(db, project_id, limit)
if tasks:
return tasks
else:
raise HTTPException(status_code=404, detail="Tasks not found")
-
+
@router.get("/", response_model=List[tasks_schemas.TaskOut])
async def read_tasks(
@@ -76,8 +72,7 @@ async def read_tasks(
limit: int = 1000,
db: Session = Depends(database.get_db),
):
- """
- Get a list of tasks for a project or user.
+ """Get a list of tasks for a project or user.
Args:
project_id (int): Project ID.
@@ -106,13 +101,8 @@ async def read_tasks(
@router.get("/point_on_surface")
-async def get_point_on_surface(
- project_id:int,
- db: Session = Depends(database.get_db)
- ):
-
- """
- Get a point on the surface of the geometry for each task of the project.
+async def get_point_on_surface(project_id: int, db: Session = Depends(database.get_db)):
+ """Get a point on the surface of the geometry for each task of the project.
Parameters:
project_id (int): The ID of the project.
@@ -120,12 +110,13 @@ async def get_point_on_surface(
Returns:
List[Tuple[int, str]]: A list of tuples containing the task ID and the centroid as a string.
"""
-
- query = text(f"""
+ query = text(
+ f"""
SELECT id, ARRAY_AGG(ARRAY[ST_X(ST_PointOnSurface(outline)), ST_Y(ST_PointOnSurface(outline))]) AS point
FROM tasks
WHERE project_id = {project_id}
- GROUP BY id; """)
+ GROUP BY id; """
+ )
result = db.execute(query)
result_dict_list = [{"id": row[0], "point": row[1]} for row in result.fetchall()]
@@ -134,8 +125,7 @@ async def get_point_on_surface(
@router.post("/near_me", response_model=tasks_schemas.TaskOut)
def get_task(lat: float, long: float, project_id: int = None, user_id: int = None):
- """
- Get tasks near the requesting user.
+ """Get tasks near the requesting user.
Args:
lat (float): Latitude of the user's location.
@@ -151,8 +141,7 @@ def get_task(lat: float, long: float, project_id: int = None, user_id: int = Non
@router.get("/{task_id}", response_model=tasks_schemas.TaskOut)
async def read_tasks(task_id: int, db: Session = Depends(database.get_db)):
- """
- Get a task by its ID.
+ """Get a task by its ID.
Args:
task_id (int): Task ID.
@@ -178,8 +167,7 @@ async def update_task_status(
new_status: tasks_schemas.TaskStatusOption,
db: Session = Depends(database.get_db),
):
- """
- Update the status of a task.
+ """Update the status of a task.
Args:
user (user_schemas.User): User object for the logged in user.
@@ -210,8 +198,7 @@ async def get_qr_code_list(
task_id: int,
db: Session = Depends(database.get_db),
):
- """
- Get the QR code for a task.
+ """Get the QR code for a task.
Args:
task_id (int): Task ID.
@@ -228,9 +215,8 @@ async def edit_task_boundary(
task_id: int,
boundary: UploadFile = File(...),
db: Session = Depends(database.get_db),
- ):
- """
- Edit the boundary of a task.
+):
+ """Edit the boundary of a task.
Args:
task_id (int): Task ID.
@@ -240,12 +226,11 @@ async def edit_task_boundary(
Returns:
bool or dict or list or str or NoneType or Response or JSONResponse or HTMLResponse or RedirectResponse or StreamingResponse or FileResponse or UJSONResponse or ORJSONResponse or MsgpackResponse: Result of the boundary edit.
"""
-
# read entire file
content = await boundary.read()
boundary_json = json.loads(content)
- edit_boundary = await tasks_crud.edit_task_boundary(db, task_id, boundary_json)
+ edit_boundary = await tasks_crud.edit_task_boundary(db, task_id, boundary_json)
return edit_boundary
@@ -254,9 +239,8 @@ async def edit_task_boundary(
async def task_features_count(
project_id: int,
db: Session = Depends(database.get_db),
- ):
- """
- Get the feature count for tasks in a project.
+):
+ """Get the feature count for tasks in a project.
Args:
project_id (int): Project ID.
@@ -265,33 +249,36 @@ async def task_features_count(
Returns:
dict or list or str or NoneType or Response or JSONResponse or HTMLResponse or RedirectResponse or StreamingResponse or FileResponse or UJSONResponse or ORJSONResponse or MsgpackResponse: Feature count for tasks in the project.
"""
-
# Get the project object.
project = project_crud.get_project(db, project_id)
# ODK Credentials
odk_credentials = project_schemas.ODKCentral(
- odk_central_url = project.odk_central_url,
- odk_central_user = project.odk_central_user,
- odk_central_password = project.odk_central_password,
- )
+ odk_central_url=project.odk_central_url,
+ odk_central_user=project.odk_central_user,
+ odk_central_password=project.odk_central_password,
+ )
odk_details = central_crud.list_odk_xforms(project.odkid, odk_credentials, True)
# Assemble the final data list
data = []
for x in odk_details:
- feature_count_query = text(f"""
+ feature_count_query = text(
+ f"""
select count(*) from features where project_id = {project_id} and task_id = {x['xmlFormId']}
- """)
+ """
+ )
result = db.execute(feature_count_query)
feature_count = result.fetchone()
- data.append({
- 'task_id': x['xmlFormId'],
- 'submission_count': x['submissions'],
- 'last_submission': x['lastSubmission'],
- 'feature_count': feature_count[0]
- })
+ data.append(
+ {
+ "task_id": x["xmlFormId"],
+ "submission_count": x["submissions"],
+ "last_submission": x["lastSubmission"],
+ "feature_count": feature_count[0],
+ }
+ )
- return data
\ No newline at end of file
+ return data
diff --git a/src/backend/app/tasks/tasks_schemas.py b/src/backend/app/tasks/tasks_schemas.py
index 49c835b8c1..dd36cd55fc 100644
--- a/src/backend/app/tasks/tasks_schemas.py
+++ b/src/backend/app/tasks/tasks_schemas.py
@@ -28,8 +28,7 @@
def get_task_status_strings():
- """
- Get the task status strings.
+ """Get the task status strings.
Returns:
Enum: Enum containing the task status strings.
@@ -44,29 +43,27 @@ def get_task_status_strings():
class TaskHistoryBase(BaseModel):
- """
- Base model for a task history entry.
+ """Base model for a task history entry.
Attributes:
id (int): Task history ID.
action_text (str): Text describing the action taken.
action_date (datetime): Date and time of the action.
"""
+
id: int
action_text: str
action_date: datetime
class TaskHistoryOut(TaskHistoryBase):
- """
- Output model for a task history entry.
- """
+ """Output model for a task history entry."""
+
pass
class TaskBasicInfo(BaseModel):
- """
- Basic information about a task.
+ """Basic information about a task.
Attributes:
id (int): Task ID.
@@ -77,6 +74,7 @@ class TaskBasicInfo(BaseModel):
locked_by_username (str, optional): Username of the user who has locked the task. Defaults to None.
task_history (List[TaskHistoryBase]): List of task history entries for the task.
"""
+
id: int
project_id: int
project_task_index: int
@@ -87,8 +85,7 @@ class TaskBasicInfo(BaseModel):
class TaskBase(BaseModel):
- """
- Base model for a task.
+ """Base model for a task.
Attributes:
id (int): Task ID.
@@ -102,6 +99,7 @@ class TaskBase(BaseModel):
locked_by_username (str, optional): Username of the user who has locked the task. Defaults to None.
task_history (List[TaskHistoryBase]): List of task history entries for the task.
"""
+
id: int
project_id: int
project_task_index: int
@@ -116,13 +114,13 @@ class TaskBase(BaseModel):
class Task(TaskBase):
- """
- Model for a task.
+ """Model for a task.
Attributes:
qr_code_base64 (str): Base64 representation of the QR code for the task.
task_status_str (TaskStatusOption): String representation of the task status.
"""
+
# geometry_geojson: str
qr_code_base64: str
task_status_str: TaskStatusOption
@@ -130,18 +128,17 @@ class Task(TaskBase):
class TaskOut(TaskBase):
- """
- Output model for a task.
+ """Output model for a task.
Attributes:
task_status_str (TaskStatusOption): String representation of the task status.
"""
+
task_status_str: TaskStatusOption
pass
class TaskDetails(TaskBase):
- """
- Detailed information about a task.
- """
+ """Detailed information about a task."""
+
pass
diff --git a/src/backend/app/users/user_crud.py b/src/backend/app/users/user_crud.py
index bb0920062c..a807e7d29f 100644
--- a/src/backend/app/users/user_crud.py
+++ b/src/backend/app/users/user_crud.py
@@ -28,8 +28,7 @@
def get_users(db: Session, skip: int = 0, limit: int = 100):
- """
- Get a list of users from the database.
+ """Get a list of users from the database.
Args:
db (Session): The database session.
@@ -44,8 +43,7 @@ def get_users(db: Session, skip: int = 0, limit: int = 100):
def get_user(db: Session, user_id: int, db_obj: bool = False):
- """
- Get a user from the database by their ID.
+ """Get a user from the database by their ID.
Args:
db (Session): The database session.
@@ -55,7 +53,6 @@ def get_user(db: Session, user_id: int, db_obj: bool = False):
Returns:
user_schemas.User: The user with the given ID.
"""
-
db_user = db.query(db_models.DbUser).filter(db_models.DbUser.id == user_id).first()
if db_obj:
return db_user
@@ -63,8 +60,7 @@ def get_user(db: Session, user_id: int, db_obj: bool = False):
def get_user_by_username(db: Session, username: str):
- """
- Get a user from the database by their username.
+ """Get a user from the database by their username.
Args:
db (Session): The database session.
@@ -86,8 +82,7 @@ def get_user_by_username(db: Session, username: str):
# TODO: write tests for these
def convert_to_app_user(db_user: db_models.DbUser):
- """
- Convert a database user object to an app user object.
+ """Convert a database user object to an app user object.
Args:
db_user (db_models.DbUser): The database user object.
@@ -103,8 +98,7 @@ def convert_to_app_user(db_user: db_models.DbUser):
def convert_to_app_users(db_users: List[db_models.DbUser]):
- """
- Convert a list of database user objects to a list of app user objects.
+ """Convert a list of database user objects to a list of app user objects.
Args:
db_users (List[db_models.DbUser]): The list of database user objects.
@@ -124,8 +118,7 @@ def convert_to_app_users(db_users: List[db_models.DbUser]):
def get_user_role_by_user_id(db: Session, user_id: int):
- """
- Get the role of a user from the database by their ID.
+ """Get the role of a user from the database by their ID.
Args:
db (Session): The database session.
@@ -134,7 +127,6 @@ def get_user_role_by_user_id(db: Session, user_id: int):
Returns:
str: The role of the user with the given ID.
"""
-
db_user_role = (
db.query(db_models.DbUserRoles)
.filter(db_models.DbUserRoles.user_id == user_id)
@@ -170,8 +162,7 @@ async def create_user_roles(user_role: user_schemas.UserRoles, db: Session):
def get_user_by_id(db: Session, user_id: int):
- """
- Get a user from the database by their ID.
+ """Get a user from the database by their ID.
Args:
db (Session): The database session.
diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py
index 0f37cddbe8..04602b1e22 100644
--- a/src/backend/app/users/user_routes.py
+++ b/src/backend/app/users/user_routes.py
@@ -40,8 +40,7 @@ def get_users(
limit: int = 100,
db: Session = Depends(database.get_db),
):
- """
- Get a list of users from the database.
+ """Get a list of users from the database.
Args:
username (str, optional): Filter users by username. Defaults to "".
@@ -59,8 +58,7 @@ def get_users(
@router.get("/{id}", response_model=user_schemas.UserOut)
async def get_user_by_id(id: int, db: Session = Depends(database.get_db)):
- """
- Get a user from the database by their ID.
+ """Get a user from the database by their ID.
Args:
id (int): The ID of the user to retrieve.
@@ -97,7 +95,6 @@ async def create_user_role(
Status Code 200 (OK): If the role is successfully created
Status Code 400 (Bad Request): If the user is already assigned a role
"""
-
existing_user_role = user_crud.get_user_role_by_user_id(
db, user_id=user_role.user_id
)
@@ -115,8 +112,7 @@ async def create_user_role(
@router.get("/user-role-options/")
async def get_user_roles():
- """
- Get a list of available roles for users.
+ """Get a list of available roles for users.
Returns:
dict[str,str]: A dictionary containing all available roles and their values.
diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py
index 1acdbf05a6..bf2e8dc730 100644
--- a/src/backend/app/users/user_schemas.py
+++ b/src/backend/app/users/user_schemas.py
@@ -22,44 +22,40 @@
class UserBase(BaseModel):
- """
- A base model for user data.
- """
+ """A base model for user data."""
+
username: str
class User(UserBase):
- """
- A model for user data in the database.
- """
+ """A model for user data in the database."""
+
id: int
class UserOut(UserBase):
- """
- A model for user data when retrieving a user from the database.
- """
+ """A model for user data when retrieving a user from the database."""
+
id: int
role: str
class UserRole(BaseModel):
- """
- A model for a user's role.
- """
+ """A model for a user's role."""
+
role: str
class UserRoles(BaseModel):
- """
- A model for assigning a role to a user.
-
+ """A model for assigning a role to a user.
+
Attributes:
user_id (int): The ID of the user to assign the role to.
organization_id (Optional[int]): The ID of the organization to assign the role for. Defaults to None.
project_id (Optional[int]): The ID of the project to assign the role for. Defaults to None.
role (str): The role to assign to the user.
"""
+
user_id: int
organization_id: Optional[int] = None
project_id: Optional[int] = None
diff --git a/src/frontend/.babelrc b/src/frontend/.babelrc
index 88e14d6a9c..3d0837b2e3 100755
--- a/src/frontend/.babelrc
+++ b/src/frontend/.babelrc
@@ -1,6 +1,8 @@
{
- "presets": ["@babel/preset-typescript", "@babel/preset-react", "@babel/preset-env"],
- "plugins": [
- ["@babel/transform-runtime"]
- ]
+ "presets": [
+ "@babel/preset-typescript",
+ "@babel/preset-react",
+ "@babel/preset-env"
+ ],
+ "plugins": [["@babel/transform-runtime"]]
}
diff --git a/src/frontend/main/.prettierrc b/src/frontend/main/.prettierrc
index 5d33d3bae7..d4a0fc192a 100755
--- a/src/frontend/main/.prettierrc
+++ b/src/frontend/main/.prettierrc
@@ -1,7 +1,7 @@
{
- "semi": true,
- "trailingComma": "all",
- "singleQuote": true,
- "printWidth": 120,
- "endOfLine": "auto"
-}
\ No newline at end of file
+ "semi": true,
+ "trailingComma": "all",
+ "singleQuote": true,
+ "printWidth": 120,
+ "endOfLine": "auto"
+}
diff --git a/src/frontend/main/public/manifest.json b/src/frontend/main/public/manifest.json
index e2bf572d18..380b5a429e 100644
--- a/src/frontend/main/public/manifest.json
+++ b/src/frontend/main/public/manifest.json
@@ -1,30 +1,30 @@
{
- "name": "Field Mapping Tasking Manager",
- "short_name": "FMTM",
- "start_url": ".",
- "display": "fullscreen",
- "background_color": "#fff",
- "description": "A project to provide tools for Open Mapping campaigns.",
- "icons": [
- {
- "src": "/icon-192x192.png",
- "sizes": "192x192",
- "type": "image/png"
- },
- {
- "src": "/icon-256x256.png",
- "sizes": "256x256",
- "type": "image/png"
- },
- {
- "src": "/icon-384x384.png",
- "sizes": "384x384",
- "type": "image/png"
- },
- {
- "src": "/icon-512x512.png",
- "sizes": "512x512",
- "type": "image/png"
- }
- ]
+ "name": "Field Mapping Tasking Manager",
+ "short_name": "FMTM",
+ "start_url": ".",
+ "display": "fullscreen",
+ "background_color": "#fff",
+ "description": "A project to provide tools for Open Mapping campaigns.",
+ "icons": [
+ {
+ "src": "/icon-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon-256x256.png",
+ "sizes": "256x256",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon-384x384.png",
+ "sizes": "384x384",
+ "type": "image/png"
+ },
+ {
+ "src": "/icon-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ]
}
diff --git a/src/frontend/main/src/api/OrganizationService.ts b/src/frontend/main/src/api/OrganizationService.ts
index a62f1da5d1..1aba3c4fe6 100644
--- a/src/frontend/main/src/api/OrganizationService.ts
+++ b/src/frontend/main/src/api/OrganizationService.ts
@@ -4,108 +4,97 @@ import { GetOrganizationDataModel, OrganizationModal } from '../models/organizat
import { CommonActions } from '../store/slices/CommonSlice';
import { OrganizationAction } from '../store/slices/organizationSlice';
-
function appendObjectToFormData(formData, object) {
- for (const [key, value] of Object.entries(object)) {
- // if (key === 'logo') {
- // formData.append(key, value[0])
- // }
- formData.append(key, value);
- }
+ for (const [key, value] of Object.entries(object)) {
+ // if (key === 'logo') {
+ // formData.append(key, value[0])
+ // }
+ formData.append(key, value);
+ }
}
export const OrganizationService: Function = (url: string, payload: OrganizationModal) => {
+ return async (dispatch) => {
+ dispatch(CommonActions.PostOrganizationLoading(true));
+
+ const postOrganization = async (url, payload) => {
+ try {
+ const generateApiFormData = new FormData();
+ appendObjectToFormData(generateApiFormData, payload);
+ await axios.post(url, generateApiFormData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+ // const resp: HomeProjectCardModel = postOrganizationData.data;
+ // dispatch(CommonActions.SetOrganizationDetail(resp))
+ dispatch(CommonActions.PostOrganizationLoading(false));
+ } catch (error) {
+ dispatch(CommonActions.PostOrganizationLoading(false));
+ }
+ };
- return async (dispatch) => {
- dispatch(CommonActions.PostOrganizationLoading(true))
-
- const postOrganization = async (url, payload) => {
-
- try {
- const generateApiFormData = new FormData();
- appendObjectToFormData(generateApiFormData, payload);
- await axios.post(url, generateApiFormData,
- {
- headers: {
- "Content-Type": "multipart/form-data",
- }
- });
- // const resp: HomeProjectCardModel = postOrganizationData.data;
- // dispatch(CommonActions.SetOrganizationDetail(resp))
- dispatch(CommonActions.PostOrganizationLoading(false))
- } catch (error) {
- dispatch(CommonActions.PostOrganizationLoading(false))
- }
- }
-
- await postOrganization(url, payload);
-
- }
-
-}
+ await postOrganization(url, payload);
+ };
+};
export const OrganizationDataService: Function = (url: string) => {
- return async (dispatch) => {
- dispatch(OrganizationAction.GetOrganizationDataLoading(true))
- const getOrganizationData = async (url) => {
- try {
- const getOrganizationDataResponse = await axios.get(url);
- const response: GetOrganizationDataModel = getOrganizationDataResponse.data;
- dispatch(OrganizationAction.GetOrganizationsData(response))
- } catch (error) {
- dispatch(OrganizationAction.GetOrganizationDataLoading(false))
- }
- }
- await getOrganizationData(url);
- }
-}
-
-export const PostOrganizationDataService:Function = (url: string, payload: any) => {
- return async (dispatch) => {
- dispatch(OrganizationAction.PostOrganizationDataLoading(true));
-
- const postOrganizationData = async (url, payload) => {
- dispatch(OrganizationAction.SetOrganizationFormData(payload))
-
- try {
- const generateApiFormData = new FormData();
- appendObjectToFormData(generateApiFormData, payload);
-
- const postOrganizationData = await axios.post(
- url,
- payload,
- {
- headers: {
- 'Content-Type': 'multipart/form-data',
- },
- }
- );
-
- const resp: HomeProjectCardModel = postOrganizationData.data;
- dispatch(OrganizationAction.PostOrganizationDataLoading(false))
- dispatch(OrganizationAction.postOrganizationData(resp))
- dispatch(
- CommonActions.SetSnackBar({
- open: true,
- message: 'Organization Successfully Created.',
- variant: "success",
- duration: 2000,
- })
- );
- } catch (error:any) {
- dispatch(
- CommonActions.SetSnackBar({
- open: true,
- message: error.response.data.detail,
- variant: "error",
- duration: 2000,
- })
- );
- dispatch(OrganizationAction.PostOrganizationDataLoading(false))
-
- }
- };
-
- await postOrganizationData(url, payload);
+ return async (dispatch) => {
+ dispatch(OrganizationAction.GetOrganizationDataLoading(true));
+ const getOrganizationData = async (url) => {
+ try {
+ const getOrganizationDataResponse = await axios.get(url);
+ const response: GetOrganizationDataModel = getOrganizationDataResponse.data;
+ dispatch(OrganizationAction.GetOrganizationsData(response));
+ } catch (error) {
+ dispatch(OrganizationAction.GetOrganizationDataLoading(false));
+ }
};
-};
\ No newline at end of file
+ await getOrganizationData(url);
+ };
+};
+
+export const PostOrganizationDataService: Function = (url: string, payload: any) => {
+ return async (dispatch) => {
+ dispatch(OrganizationAction.PostOrganizationDataLoading(true));
+
+ const postOrganizationData = async (url, payload) => {
+ dispatch(OrganizationAction.SetOrganizationFormData(payload));
+
+ try {
+ const generateApiFormData = new FormData();
+ appendObjectToFormData(generateApiFormData, payload);
+
+ const postOrganizationData = await axios.post(url, payload, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+
+ const resp: HomeProjectCardModel = postOrganizationData.data;
+ dispatch(OrganizationAction.PostOrganizationDataLoading(false));
+ dispatch(OrganizationAction.postOrganizationData(resp));
+ dispatch(
+ CommonActions.SetSnackBar({
+ open: true,
+ message: 'Organization Successfully Created.',
+ variant: 'success',
+ duration: 2000,
+ }),
+ );
+ } catch (error: any) {
+ dispatch(
+ CommonActions.SetSnackBar({
+ open: true,
+ message: error.response.data.detail,
+ variant: 'error',
+ duration: 2000,
+ }),
+ );
+ dispatch(OrganizationAction.PostOrganizationDataLoading(false));
+ }
+ };
+
+ await postOrganizationData(url, payload);
+ };
+};
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/index.js b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/index.js
index 23dab94e14..dec28db24b 100644
--- a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/index.js
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/LayerSwitcher/index.js
@@ -6,7 +6,7 @@ import LayerGroup from 'ol/layer/Group';
import LayerTile from 'ol/layer/Tile';
import SourceOSM from 'ol/source/OSM';
import LayerSwitcher from 'ol-layerswitcher';
-import React,{ useEffect } from 'react';
+import React, { useEffect } from 'react';
import { XYZ } from 'ol/source';
@@ -126,7 +126,6 @@ const monochromeMidNight = (visible = false) =>
}),
});
-
const LayerSwitcherControl = ({ map, visible = 'osm' }) => {
useEffect(() => {
if (!map) return;
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/VectorTileLayer.js b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/VectorTileLayer.js
index bd4afdeaa4..c7cd0d7f45 100644
--- a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/VectorTileLayer.js
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Layers/VectorTileLayer.js
@@ -1,4 +1,4 @@
-import React,{ useEffect, useMemo } from 'react';
+import React, { useEffect, useMemo } from 'react';
// import * as olExtent from 'ol/extent';
import VectorTile from 'ol/layer/VectorTile';
import MVT from 'ol/format/MVT';
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.css b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.css
index bdd5110d88..6a5112f811 100644
--- a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.css
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.css
@@ -16,7 +16,7 @@
.ol-popup:before {
top: 100%;
border: solid transparent;
- content: " ";
+ content: ' ';
height: 0;
width: 0;
position: absolute;
@@ -45,5 +45,5 @@
}
.ol-popup-closer:after {
- content: "✖";
-}/*# sourceMappingURL=popup.css.map */
\ No newline at end of file
+ content: '✖';
+} /*# sourceMappingURL=popup.css.map */
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.scss b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.scss
index 2890e1b352..1e8ca3f0d8 100644
--- a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.scss
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/Popup/popup.scss
@@ -4,7 +4,7 @@
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
// padding: 15px;
border-radius: 20px;
- border: 1px solid #BDBDBD;
+ border: 1px solid #bdbdbd;
min-height: fit-content;
// top: 6px;
margin-top: 12px;
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.css b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.css
index d84aeb99db..a1fcd582f9 100644
--- a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.css
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.css
@@ -86,12 +86,12 @@
border-radius: 5px !important;
}
.layer-switcher button::before {
- font-family: "Material Icons";
- content: "layers";
+ font-family: 'Material Icons';
+ content: 'layers';
position: relative;
top: -3px;
}
.ol-attribution {
display: none !important;
-}/*# sourceMappingURL=map.css.map */
\ No newline at end of file
+} /*# sourceMappingURL=map.css.map */
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.scss b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.scss
index 4e6b38ebf0..8f38f9f611 100644
--- a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.scss
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/map.scss
@@ -1,16 +1,15 @@
-
@keyframes blink {
- 0% {
- opacity: 1;
- }
- 50% {
- opacity: 0;
- }
- 100% {
- opacity: 1;
- }
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0;
}
-
+ 100% {
+ opacity: 1;
+ }
+}
+
.blink {
- animation: blink 1s infinite;
-}
\ No newline at end of file
+ animation: blink 1s infinite;
+}
diff --git a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/useOLMap/index.js b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/useOLMap/index.js
index 487a5b80c8..8881769ee8 100644
--- a/src/frontend/main/src/components/MapComponent/OpenLayersComponent/useOLMap/index.js
+++ b/src/frontend/main/src/components/MapComponent/OpenLayersComponent/useOLMap/index.js
@@ -1,5 +1,5 @@
/* eslint-disable consistent-return */
-import React,{ useRef, useState, useEffect } from 'react';
+import React, { useRef, useState, useEffect } from 'react';
import Map from 'ol/Map';
import { View } from 'ol';
import * as olExtent from 'ol/extent';
diff --git a/src/frontend/main/src/components/MapLegends.jsx b/src/frontend/main/src/components/MapLegends.jsx
index f269ff2fd6..8777d7fc53 100755
--- a/src/frontend/main/src/components/MapLegends.jsx
+++ b/src/frontend/main/src/components/MapLegends.jsx
@@ -1,68 +1,68 @@
-import React from "react";
+import React from 'react';
import CoreModules from 'fmtm/CoreModules';
import AssetModules from 'fmtm/AssetModules';
const MapLegends = ({ direction, spacing, iconBtnProps, defaultTheme, valueStatus }) => {
-
- const MapDetails = [
- {
- value: 'Ready',
- color: defaultTheme.palette.mapFeatureColors.ready,
- status: 'none'
- },
- {
- value: 'Locked For Mapping',
- color: defaultTheme.palette.mapFeatureColors.locked_for_mapping,
- status: 'lock'
- },
- {
- value: 'Locked For Validation',
- color: defaultTheme.palette.mapFeatureColors.locked_for_validation,
- status: 'lock'
- },
- {
- value: 'Ready For Validation',
- color: defaultTheme.palette.mapFeatureColors.mapped,
- status: 'none'
- },
- {
- value: 'Validated',
- color: defaultTheme.palette.mapFeatureColors.validated,
- status: 'none'
- },
- {
- value: 'Bad',
- color: defaultTheme.palette.mapFeatureColors.bad,
- status: 'none'
- },
- {
- value: 'More mapping needed',
- color: defaultTheme.palette.mapFeatureColors.invalidated,
- status: 'none'
- }
- ]
- return (
-
- {
- MapDetails.map((data, index) => {
- return (
-
-
-
-
- {
- valueStatus &&
-
- {data.value}
-
-
- }
-
- )
- })
- }
-
- )
-}
+ const MapDetails = [
+ {
+ value: 'Ready',
+ color: defaultTheme.palette.mapFeatureColors.ready,
+ status: 'none',
+ },
+ {
+ value: 'Locked For Mapping',
+ color: defaultTheme.palette.mapFeatureColors.locked_for_mapping,
+ status: 'lock',
+ },
+ {
+ value: 'Locked For Validation',
+ color: defaultTheme.palette.mapFeatureColors.locked_for_validation,
+ status: 'lock',
+ },
+ {
+ value: 'Ready For Validation',
+ color: defaultTheme.palette.mapFeatureColors.mapped,
+ status: 'none',
+ },
+ {
+ value: 'Validated',
+ color: defaultTheme.palette.mapFeatureColors.validated,
+ status: 'none',
+ },
+ {
+ value: 'Bad',
+ color: defaultTheme.palette.mapFeatureColors.bad,
+ status: 'none',
+ },
+ {
+ value: 'More mapping needed',
+ color: defaultTheme.palette.mapFeatureColors.invalidated,
+ status: 'none',
+ },
+ ];
+ return (
+
+ {MapDetails.map((data, index) => {
+ return (
+
+
+
+
+ {valueStatus && (
+
+ {data.value}
+
+ )}
+
+ );
+ })}
+
+ );
+};
export default MapLegends;
diff --git a/src/frontend/main/src/components/ProjectMap/ProjectMap.jsx b/src/frontend/main/src/components/ProjectMap/ProjectMap.jsx
index 77585d180a..cf6ae604b7 100644
--- a/src/frontend/main/src/components/ProjectMap/ProjectMap.jsx
+++ b/src/frontend/main/src/components/ProjectMap/ProjectMap.jsx
@@ -1,30 +1,24 @@
-import React, { useState } from "react";
-import CoreModules from "fmtm/CoreModules";
-import { useOLMap } from "../MapComponent/OpenLayersComponent";
-import { MapContainer as MapComponent } from "../MapComponent/OpenLayersComponent";
-import LayerSwitcherControl from "../MapComponent/OpenLayersComponent/LayerSwitcher/index.js";
-import { VectorLayer } from "../MapComponent/OpenLayersComponent/Layers";
+import React, { useState } from 'react';
+import CoreModules from 'fmtm/CoreModules';
+import { useOLMap } from '../MapComponent/OpenLayersComponent';
+import { MapContainer as MapComponent } from '../MapComponent/OpenLayersComponent';
+import LayerSwitcherControl from '../MapComponent/OpenLayersComponent/LayerSwitcher/index.js';
+import { VectorLayer } from '../MapComponent/OpenLayersComponent/Layers';
const basicGeojsonTemplate = {
- type: "FeatureCollection",
+ type: 'FeatureCollection',
features: [],
};
const ProjectMap = ({}) => {
- const defaultTheme = CoreModules.useAppSelector(
- (state) => state.theme.hotTheme
- );
+ const defaultTheme = CoreModules.useAppSelector((state) => state.theme.hotTheme);
const { mapRef, map } = useOLMap({
// center: fromLonLat([85.3, 27.7]),
center: [0, 0],
zoom: 4,
maxZoom: 25,
});
- const projectTaskBoundries = CoreModules.useAppSelector(
- (state) => state.project.projectTaskBoundries
- );
- const projectBuildingGeojson = CoreModules.useAppSelector(
- (state) => state.project.projectBuildingGeojson
- );
+ const projectTaskBoundries = CoreModules.useAppSelector((state) => state.project.projectTaskBoundries);
+ const projectBuildingGeojson = CoreModules.useAppSelector((state) => state.project.projectBuildingGeojson);
const [projectBoundaries, setProjectBoundaries] = useState(null);
const [buildingBoundaries, setBuildingBoundaries] = useState(null);
@@ -55,20 +49,20 @@ const ProjectMap = ({}) => {
setBuildingBoundaries(buildingGeojsonFeatureCollection);
}
return (
-
+
-