diff --git a/.circleci/config.yml b/.circleci/config.yml
index b91fab08e2..bb69055235 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -39,7 +39,6 @@ jobs:
command: |
cd ${CIRCLE_WORKING_DIRECTORY}
mkdir ${CIRCLE_WORKING_DIRECTORY}/tests/backend/lint
- # . ${CIRCLE_WORKING_DIRECTORY}/env/bin/activate
black --check manage.py backend tests migrations
flake8 manage.py backend tests migrations
- run:
@@ -252,7 +251,7 @@ workflows:
filters:
branches:
only:
- - fix/sentry-environments
+ - fix/cfn-init-codedeploy
requires:
- build
stack_name: "test"
@@ -263,7 +262,7 @@ workflows:
filters:
branches:
only:
- - fix/sentry-environments
+ - fix/cfn-init-codedeploy
requires:
- build
context: tasking-manager-staging
@@ -334,4 +333,4 @@ workflows:
context: tasking-manager-tm4-production
notify:
webhooks:
- -url: https://api.opsgenie.com/v1/json/circleci?apiKey=$OPSGENIE_API
+ - url: https://api.opsgenie.com/v1/json/circleci?apiKey=$OPSGENIE_API
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000000..a90b3f8050
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,32 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: "[BUG]"
+labels: 'Type: Bug'
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000000..bbcbbe7d61
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 8a723f6b50..2fde9a67fc 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,7 +4,7 @@ updates:
directory: "/"
schedule:
interval: daily
- time: "11:00"
+ time: "13:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: python-dotenv
@@ -30,7 +30,7 @@ updates:
directory: "/frontend"
schedule:
interval: daily
- time: "11:00"
+ time: "13:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: "@testing-library/user-event"
@@ -141,6 +141,16 @@ updates:
- 2.4.21
- 2.4.22
- 2.4.23
+
+ - dependency-name: react-datepicker
+ versions:
+ - 3.7.0
+ - dependency-name: reactjs-popup
+ versions:
+ - 2.0.4
+ - dependency-name: ini
+ versions:
+ - 1.3.8
- dependency-name: "@formatjs/intl-relativetimeformat"
versions:
- 8.0.4
@@ -260,3 +270,53 @@ updates:
- dependency-name: react-placeholder
versions:
- 4.1.0
+- package-ecosystem: pip
+ directory: "/"
+ schedule:
+ interval: daily
+ time: "13:00"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: python-dotenv
+ versions:
+ - 0.15.0
+ - 0.16.0
+ - 0.17.0
+ - dependency-name: alembic
+ versions:
+ - 1.5.5
+ - 1.5.6
+ - 1.5.7
+ - dependency-name: sentry-sdk[flask]
+ versions:
+ - 0.20.3
+ - dependency-name: greenlet
+ versions:
+ - 1.0.0
+ - dependency-name: gevent
+ versions:
+ - 21.1.2
+ - dependency-name: oauthlib
+ versions:
+ - 3.1.0
+ - dependency-name: bleach
+ versions:
+ - 3.3.0
+ - dependency-name: flask-oauthlib
+ versions:
+ - 0.9.6
+ - dependency-name: flask-cors
+ versions:
+ - 3.0.10
+ - dependency-name: attrs
+ versions:
+ - 20.3.0
+ - dependency-name: werkzeug
+ versions:
+ - 1.0.1
+ - dependency-name: black
+ versions:
+ - 20.8b1
+ - dependency-name: geojson
+ versions:
+ - 2.5.0
diff --git a/README.md b/README.md
index d3742ded9f..e5597b1955 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,9 @@
[![TM Backend on Quay](https://quay.io/repository/hotosm/tasking-manager/status "Tasking Manager Backend Build")](https://quay.io/repository/hotosm/tasking-manager)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=hotosm_tasking-manager&metric=alert_status)](https://sonarcloud.io/dashboard?id=hotosm_tasking-manager)
-The most popular tool for teams to coordinate mapping on OpenStreetMap.
-
[](./screenshot.jpg)
-With this web application an area of interest can be defined and divided up into smaller tasks that can be completed rapidly. It shows which areas need to be mapped and which areas need a review for quality assurance. You can see the tool in action: log into the widely used [HOT Tasking Manager](https://tasks.hotosm.org/) and start mapping.
+The most popular tool for teams to coordinate mapping on OpenStreetMap. With this web application an area of interest can be defined and divided up into smaller tasks that can be completed rapidly. It shows which areas need to be mapped and which areas need a review for quality assurance. You can see the tool in action: log into the widely used [HOT Tasking Manager](https://tasks.hotosm.org/) and start mapping.
[
](./docs/assets/project-view.gif)
@@ -15,10 +13,10 @@ This is Free and Open Source Software. You are welcome to use the code and set u
## Get involved!
-* Check our [Code of conduct](./docs/code_of_conduct.md)
-* Get familiar with our [contributor guidelines](./docs/contributing.md)
-* Join the [working groups](./docs/working-groups.md)
-* Help us to [translate the user interface](./docs/contributing-translation.md)
+* Start by reading our [Code of conduct](./docs/code_of_conduct.md)
+* Get familiar with our [contributor guidelines](./docs/contributing.md) explaining the different ways in which you can support this project! We need your help!
+* Join the Tasking Manager Collective Meet up- an opportunity to meet other Tasking Manager contributors- details coming shortly!
+
## Developers
diff --git a/backend/api/annotations/resources.py b/backend/api/annotations/resources.py
index 7d0af9fed4..3bcb6dc806 100644
--- a/backend/api/annotations/resources.py
+++ b/backend/api/annotations/resources.py
@@ -39,7 +39,7 @@ def get(self, project_id: int, annotation_type: str = None):
ProjectService.exists(project_id)
except NotFound as e:
current_app.logger.error(f"Error validating project: {str(e)}")
- return {"Error": "Project not found"}, 404
+ return {"Error": "Project not found", "SubCode": "NotFound"}, 404
try:
if annotation_type:
@@ -52,7 +52,7 @@ def get(self, project_id: int, annotation_type: str = None):
)
return annotations.to_primitive(), 200
except NotFound:
- return {"Error": "Annotations not found"}, 404
+ return {"Error": "Annotations not found", "SubCode": "NotFound"}, 404
def post(self, project_id: int, annotation_type: str):
"""
@@ -128,10 +128,10 @@ def post(self, project_id: int, annotation_type: str):
)
except NotFound:
current_app.logger.error("Invalid token")
- return {"Error": "Invalid token"}, 500
+ return {"Error": "Invalid token", "SubCode": "NotFound"}, 500
else:
current_app.logger.error("No token supplied")
- return {"Error": "No token supplied"}, 500
+ return {"Error": "No token supplied", "SubCode": "NotFound"}, 500
try:
annotations = request.get_json() or {}
@@ -142,7 +142,7 @@ def post(self, project_id: int, annotation_type: str):
ProjectService.exists(project_id)
except NotFound as e:
current_app.logger.error(f"Error validating project: {str(e)}")
- return {"Error": "Project not found"}, 404
+ return {"Error": "Project not found", "SubCode": "NotFound"}, 404
task_ids = [t["taskId"] for t in annotations["tasks"]]
@@ -159,7 +159,10 @@ def post(self, project_id: int, annotation_type: str):
)
except DataError as e:
current_app.logger.error(f"Error creating annotations: {str(e)}")
- return {"Error": "Error creating annotations"}, 500
+ return {
+ "Error": "Error creating annotations",
+ "SubCode": "InvalidData",
+ }, 400
return project_id, 200
diff --git a/backend/api/campaigns/resources.py b/backend/api/campaigns/resources.py
index f688bd3095..47bd9af1a9 100644
--- a/backend/api/campaigns/resources.py
+++ b/backend/api/campaigns/resources.py
@@ -53,11 +53,11 @@ def get(self, campaign_id):
campaign = CampaignService.get_campaign_as_dto(campaign_id, 0)
return campaign.to_primitive(), 200
except NotFound:
- return {"Error": "No campaign found"}, 404
+ return {"Error": "No campaign found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Campaign GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def patch(self, campaign_id):
@@ -129,27 +129,27 @@ def patch(self, campaign_id):
raise ValueError("User not a Org Manager")
except ValueError as e:
error_msg = f"CampaignsRestAPI PATCH: {str(e)}"
- return {"Error": error_msg}, 403
+ return {"Error": error_msg, "SubCode": "UserNotPermitted"}, 403
try:
campaign_dto = CampaignDTO(request.get_json())
campaign_dto.validate()
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
campaign = CampaignService.update_campaign(campaign_dto, campaign_id)
return {"Success": "Campaign {} updated".format(campaign.id)}, 200
except NotFound:
- return {"Error": "Campaign not found"}, 404
+ return {"Error": "Campaign not found", "SubCode": "NotFound"}, 404
except ValueError:
error_msg = "Campaign PATCH - name already exists"
- return {"Error": error_msg}, 409
+ return {"Error": error_msg, "SubCode": "NameExists"}, 409
except Exception as e:
error_msg = f"Campaign PATCH - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def delete(self, campaign_id):
@@ -197,18 +197,18 @@ def delete(self, campaign_id):
raise ValueError("User not a Org Manager")
except ValueError as e:
error_msg = f"CampaignsRestAPI DELETE: {str(e)}"
- return {"Error": error_msg}, 403
+ return {"Error": error_msg, "SubCode": "UserNotPermitted"}, 403
try:
campaign = CampaignService.get_campaign(campaign_id)
CampaignService.delete_campaign(campaign.id)
return {"Success": "Campaign deleted"}, 200
except NotFound:
- return {"Error": "Campaign not found"}, 404
+ return {"Error": "Campaign not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Campaign DELETE - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class CampaignsAllAPI(Resource):
@@ -232,7 +232,7 @@ def get(self):
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def post(self):
@@ -298,22 +298,21 @@ def post(self):
raise ValueError("User not a Org Manager")
except ValueError as e:
error_msg = f"CampaignsAllAPI POST: {str(e)}"
- return {"Error": error_msg}, 403
+ return {"Error": error_msg, "SubCode": "UserNotPermitted"}, 403
try:
campaign_dto = NewCampaignDTO(request.get_json())
campaign_dto.validate()
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
campaign = CampaignService.create_campaign(campaign_dto)
return {"campaignId": campaign.id}, 200
except ValueError as e:
- error_msg = f"Campaign POST - {str(e)}"
- return {"Error": error_msg}, 409
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 409
except Exception as e:
error_msg = f"Campaign POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/comments/resources.py b/backend/api/comments/resources.py
index 2401b9c5b8..72c21149b4 100644
--- a/backend/api/comments/resources.py
+++ b/backend/api/comments/resources.py
@@ -54,7 +54,7 @@ def post(self, project_id):
"""
authenticated_user_id = token_auth.current_user()
if UserService.is_user_blocked(authenticated_user_id):
- return {"Error": "User is on read only mode."}, 403
+ return {"Error": "User is on read only mode.", "SubCode": "ReadOnly"}, 403
try:
chat_dto = ChatMessageDTO(request.get_json())
@@ -63,7 +63,10 @@ def post(self, project_id):
chat_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to add chat message"}, 400
+ return {
+ "Error": "Unable to add chat message",
+ "SubCode": "InvalidData",
+ }, 400
try:
project_messages = ChatService.post_message(
@@ -71,12 +74,14 @@ def post(self, project_id):
)
return project_messages.to_primitive(), 201
except ValueError as e:
- error_msg = f"CommentsProjectsRestAPI POST: {str(e)}"
- return {"Error": error_msg}, 403
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"Chat POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to add chat message"}, 500
+ return {
+ "Error": "Unable to add chat message",
+ "SubCode": "InternalServerError",
+ }, 500
def get(self, project_id):
"""
@@ -115,7 +120,7 @@ def get(self, project_id):
ProjectService.exists(project_id)
except NotFound as e:
current_app.logger.error(f"Error validating project: {str(e)}")
- return {"Error": "Project not found"}, 404
+ return {"Error": "Project not found", "SubCode": "NotFound"}, 404
try:
page = int(request.args.get("page")) if request.args.get("page") else 1
@@ -123,11 +128,14 @@ def get(self, project_id):
project_messages = ChatService.get_messages(project_id, page, per_page)
return project_messages.to_primitive(), 200
except NotFound:
- return {"Error": "Project not found"}, 404
+ return {"Error": "Project not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Chat GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch chat messages"}, 500
+ return {
+ "Error": "Unable to fetch chat messages",
+ "SubCode": "InternalServerError",
+ }, 500
class CommentsTasksRestAPI(Resource):
@@ -194,19 +202,22 @@ def post(self, project_id, task_id):
task_comment.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to add comment"}, 400
+ return {"Error": "Unable to add comment", "SubCode": "InvalidData"}, 400
try:
task = MappingService.add_task_comment(task_comment)
return task.to_primitive(), 201
except NotFound:
- return {"Error": "Task Not Found"}, 404
+ return {"Error": "Task Not Found", "SubCode": "NotFound"}, 404
except MappingServiceError:
return {"Error": "Task update failed"}, 403
except Exception as e:
error_msg = f"Task Comment API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Task update failed"}, 500
+ return {
+ "Error": "Task update failed",
+ "SubCode": "InternalServerError",
+ }, 500
def get(self, project_id, task_id):
"""
@@ -269,7 +280,10 @@ def get(self, project_id, task_id):
task_comment.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to fetch task comments"}, 400
+ return {
+ "Error": "Unable to fetch task comments",
+ "SubCode": "InvalidData",
+ }, 400
try:
# NEW FUNCTION HAS TO BE ADDED
@@ -277,10 +291,10 @@ def get(self, project_id, task_id):
# return task.to_primitive(), 200
return
except NotFound:
- return {"Error": "Task Not Found"}, 404
+ return {"Error": "Task Not Found", "SubCode": "NotFound"}, 404
except MappingServiceError as e:
return {"Error": str(e)}, 403
except Exception as e:
error_msg = f"Task Comment API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/countries/resources.py b/backend/api/countries/resources.py
index aa20944a54..d224f3e5bb 100644
--- a/backend/api/countries/resources.py
+++ b/backend/api/countries/resources.py
@@ -23,4 +23,4 @@ def get(self):
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/interests/resources.py b/backend/api/interests/resources.py
index 8a86b8f804..e733385a93 100644
--- a/backend/api/interests/resources.py
+++ b/backend/api/interests/resources.py
@@ -56,27 +56,30 @@ def post(self):
raise ValueError("User not a Org Manager")
except ValueError as e:
error_msg = f"InterestsAllAPI POST: {str(e)}"
- return {"Error": error_msg}, 403
+ return {"Error": error_msg, "SubCode": "UserNotPermitted"}, 403
try:
interest_dto = InterestDTO(request.get_json())
interest_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
new_interest = InterestService.create(interest_dto.name)
return new_interest.to_primitive(), 200
except IntegrityError:
return (
- {"error": "Value '{0}' already exists".format(interest_dto.name)},
+ {
+ "error": "Value '{0}' already exists".format(interest_dto.name),
+ "SubCode": "NameExists",
+ },
400,
)
except Exception as e:
error_msg = f"Interest POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
def get(self):
"""
@@ -98,7 +101,7 @@ def get(self):
except Exception as e:
error_msg = f"Interest GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class InterestsRestAPI(Resource):
@@ -144,7 +147,7 @@ def get(self, interest_id):
raise ValueError("User not a Org Manager")
except ValueError as e:
error_msg = f"InterestsRestAPI GET: {str(e)}"
- return {"Error": error_msg}, 403
+ return {"Error": error_msg, "SubCode": "UserNotPermitted"}, 403
try:
interest = InterestService.get(interest_id)
@@ -152,7 +155,7 @@ def get(self, interest_id):
except Exception as e:
error_msg = f"Interest GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def patch(self, interest_id):
@@ -205,14 +208,14 @@ def patch(self, interest_id):
raise ValueError("User not a Org Manager")
except ValueError as e:
error_msg = f"InterestsAllAPI PATCH: {str(e)}"
- return {"Error": error_msg}, 403
+ return {"Error": error_msg, "SubCode": "UserNotPermitted"}, 403
try:
interest_dto = InterestDTO(request.get_json())
interest_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
update_interest = InterestService.update(interest_id, interest_dto)
@@ -220,7 +223,7 @@ def patch(self, interest_id):
except Exception as e:
error_msg = f"Interest PUT - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def delete(self, interest_id):
@@ -264,14 +267,14 @@ def delete(self, interest_id):
raise ValueError("User not a Org Manager")
except ValueError as e:
error_msg = f"InterestsAllAPI DELETE: {str(e)}"
- return {"Error": error_msg}, 403
+ return {"Error": error_msg, "SubCode": "UserNotPermitted"}, 403
try:
InterestService.delete(interest_id)
return {"Success": "Interest deleted"}, 200
except NotFound:
- return {"Error": "Interest Not Found"}, 404
+ return {"Error": "Interest Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"License DELETE - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/issues/resources.py b/backend/api/issues/resources.py
index 0e8ab1c638..8a535f0c29 100644
--- a/backend/api/issues/resources.py
+++ b/backend/api/issues/resources.py
@@ -35,11 +35,17 @@ def get(self, category_id):
category_dto = MappingIssueCategoryService.get_category_as_dto(category_id)
return category_dto.to_primitive(), 200
except NotFound:
- return {"Error": "Mapping-issue category Not Found"}, 404
+ return {
+ "Error": "Mapping-issue category Not Found",
+ "SubCode": "NotFound",
+ }, 404
except Exception as e:
error_msg = f"Mapping-issue category PUT - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch mapping issue category"}, 500
+ return {
+ "Error": "Unable to fetch mapping issue category",
+ "SubCode": "InternalServerError",
+ }, 500
@tm.pm_only()
@token_auth.login_required
@@ -90,7 +96,10 @@ def patch(self, category_id):
category_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to update mapping issue category"}, 400
+ return {
+ "Error": "Unable to update mapping issue category",
+ "SubCode": "InvalidData",
+ }, 400
try:
updated_category = (
@@ -98,11 +107,17 @@ def patch(self, category_id):
)
return updated_category.to_primitive(), 200
except NotFound:
- return {"Error": "Mapping-issue category Not Found"}, 404
+ return {
+ "Error": "Mapping-issue category Not Found",
+ "SubCode": "NotFound",
+ }, 404
except Exception as e:
error_msg = f"Mapping-issue category PUT - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to update mapping issue category"}, 500
+ return {
+ "Error": "Unable to update mapping issue category",
+ "SubCode": "InternalServerError",
+ }, 500
@tm.pm_only()
@token_auth.login_required
@@ -144,11 +159,17 @@ def delete(self, category_id):
MappingIssueCategoryService.delete_mapping_issue_category(category_id)
return {"Success": "Mapping-issue category deleted"}, 200
except NotFound:
- return {"Error": "Mapping-issue category Not Found"}, 404
+ return {
+ "Error": "Mapping-issue category Not Found",
+ "SubCode": "NotFound",
+ }, 404
except Exception as e:
error_msg = f"Mapping-issue category DELETE - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to delete mapping issue category"}, 500
+ return {
+ "Error": "Unable to delete mapping issue category",
+ "SubCode": "InternalServerError",
+ }, 500
class IssuesAllAPI(Resource):
@@ -181,7 +202,10 @@ def get(self):
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch mapping issue categories"}, 500
+ return {
+ "Error": "Unable to fetch mapping issue categories",
+ "SubCode": "InternalServerError",
+ }, 500
@tm.pm_only()
@token_auth.login_required
@@ -226,7 +250,10 @@ def post(self):
category_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to create a new mapping issue category"}, 400
+ return {
+ "Error": "Unable to create a new mapping issue category",
+ "SubCode": "InvalidData",
+ }, 400
try:
new_category_id = MappingIssueCategoryService.create_mapping_issue_category(
@@ -236,4 +263,7 @@ def post(self):
except Exception as e:
error_msg = f"Mapping-issue category POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to create a new mapping issue category"}, 500
+ return {
+ "Error": "Unable to create a new mapping issue category",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/licenses/actions.py b/backend/api/licenses/actions.py
index d8c0f18772..21a7134055 100644
--- a/backend/api/licenses/actions.py
+++ b/backend/api/licenses/actions.py
@@ -41,8 +41,11 @@ def post(self, license_id):
UserService.accept_license_terms(token_auth.current_user(), license_id)
return {"Success": "Terms Accepted"}, 200
except NotFound:
- return {"Error": "User or mapping not found"}, 404
+ return {"Error": "User or mapping not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to update license terms"}, 500
+ return {
+ "Error": "Unable to update license terms",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/licenses/resources.py b/backend/api/licenses/resources.py
index 7fc68b6527..cb0a34ed07 100644
--- a/backend/api/licenses/resources.py
+++ b/backend/api/licenses/resources.py
@@ -55,7 +55,10 @@ def post(self):
license_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to create new mapping license"}, 400
+ return {
+ "Error": "Unable to create new mapping license",
+ "SubCode": "InvalidData",
+ }, 400
try:
new_license_id = LicenseService.create_licence(license_dto)
@@ -63,7 +66,10 @@ def post(self):
except Exception as e:
error_msg = f"License PUT - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to create new mapping license"}, 500
+ return {
+ "Error": "Unable to create new mapping license",
+ "SubCode": "InternalServerError",
+ }, 500
def get(self, license_id):
"""
@@ -92,11 +98,14 @@ def get(self, license_id):
license_dto = LicenseService.get_license_as_dto(license_id)
return license_dto.to_primitive(), 200
except NotFound:
- return {"Error": "License Not Found"}, 404
+ return {"Error": "License Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"License PUT - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch license"}, 500
+ return {
+ "Error": "Unable to fetch license",
+ "SubCode": "InternalServerError",
+ }, 500
@tm.pm_only()
@token_auth.login_required
@@ -152,17 +161,20 @@ def patch(self, license_id):
license_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
updated_license = LicenseService.update_licence(license_dto)
return updated_license.to_primitive(), 200
except NotFound:
- return {"Error": "License Not Found"}, 404
+ return {"Error": "License Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"License POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to update license"}, 500
+ return {
+ "Error": "Unable to update license",
+ "SubCode": "InternalServerError",
+ }, 500
@tm.pm_only()
@token_auth.login_required
@@ -201,11 +213,14 @@ def delete(self, license_id):
LicenseService.delete_license(license_id)
return {"Success": "License deleted"}, 200
except NotFound:
- return {"Error": "License Not Found"}, 404
+ return {"Error": "License Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"License DELETE - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to delete license"}, 500
+ return {
+ "Error": "Unable to delete license",
+ "SubCode": "InternalServerError",
+ }, 500
class LicensesAllAPI(Resource):
@@ -229,8 +244,11 @@ def get(self):
licenses_dto = LicenseService.get_all_licenses()
return licenses_dto.to_primitive(), 200
except NotFound:
- return {"Error": "License Not Found"}, 404
+ return {"Error": "License Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"License PUT - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch all licenses"}, 500
+ return {
+ "Error": "Unable to fetch all licenses",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/notifications/actions.py b/backend/api/notifications/actions.py
index 17f18d9001..2aa4937e10 100644
--- a/backend/api/notifications/actions.py
+++ b/backend/api/notifications/actions.py
@@ -49,4 +49,7 @@ def delete(self):
except Exception as e:
error_msg = f"DeleteMultipleMessages - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to delete messages"}, 500
+ return {
+ "Error": "Unable to delete messages",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/notifications/resources.py b/backend/api/notifications/resources.py
index 5aa5a01def..2b6f35c139 100644
--- a/backend/api/notifications/resources.py
+++ b/backend/api/notifications/resources.py
@@ -47,14 +47,17 @@ def get(self, message_id):
message_id, token_auth.current_user()
)
return user_message.to_primitive(), 200
- except MessageServiceError:
- return {"Error": "Unable to fetch message"}, 403
+ except MessageServiceError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except NotFound:
- return {"Error": "No messages found"}, 404
+ return {"Error": "No messages found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Messages GET all - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch message"}, 500
+ return {
+ "Error": "Unable to fetch message",
+ "SubCode": "InternalServerError",
+ }, 500
@tm.pm_only(False)
@token_auth.login_required
@@ -92,14 +95,17 @@ def delete(self, message_id):
try:
MessageService.delete_message(message_id, token_auth.current_user())
return {"Success": "Message deleted"}, 200
- except MessageServiceError:
- return {"Error": "Unable to delete message"}, 403
+ except MessageServiceError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except NotFound:
- return {"Error": "No messages found"}, 404
+ return {"Error": "No messages found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Messages GET all - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to delete message"}, 500
+ return {
+ "Error": "Unable to delete message",
+ "SubCode": "InternalServerError",
+ }, 500
class NotificationsAllAPI(Resource):
@@ -193,7 +199,10 @@ def get(self):
except Exception as e:
error_msg = f"Messages GET all - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch messages"}, 500
+ return {
+ "Error": "Unable to fetch messages",
+ "SubCode": "InternalServerError",
+ }, 500
class NotificationsQueriesCountUnreadAPI(Resource):
@@ -228,7 +237,10 @@ def get(self):
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch messages count"}, 500
+ return {
+ "Error": "Unable to fetch messages count",
+ "SubCode": "InternalServerError",
+ }, 500
class NotificationsQueriesPostUnreadAPI(Resource):
@@ -264,4 +276,7 @@ def post(self):
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch messages count"}, 500
+ return {
+ "Error": "Unable to fetch messages count",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/organisations/campaigns.py b/backend/api/organisations/campaigns.py
index 6867dfe007..f7c9fae50b 100644
--- a/backend/api/organisations/campaigns.py
+++ b/backend/api/organisations/campaigns.py
@@ -59,7 +59,7 @@ def post(self, organisation_id, campaign_id):
campaign_id, organisation_id
)
)
- return {"Error": message}, 400
+ return {"Error": message, "SubCode": "CampaignAlreadyAssigned"}, 400
CampaignService.create_campaign_organisation(
organisation_id, campaign_id
@@ -71,11 +71,14 @@ def post(self, organisation_id, campaign_id):
)
return {"Success": message}, 200
else:
- return {"Error": "User is not a manager of the organisation"}, 403
+ return {
+ "Error": "User is not a manager of the organisation",
+ "SubCode": "UserNotPermitted",
+ }, 403
except Exception as e:
error_msg = f"Campaign Organisation POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
def get(self, organisation_id):
"""
@@ -112,11 +115,11 @@ def get(self, organisation_id):
)
return campaigns.to_primitive(), 200
except NotFound:
- return {"Error": "No campaign found"}, 404
+ return {"Error": "No campaign found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Organisation Campaigns GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def delete(self, organisation_id, campaign_id):
@@ -170,10 +173,16 @@ def delete(self, organisation_id, campaign_id):
200,
)
else:
- return {"Error": "User is not a manager of the organisation"}, 403
+ return {
+ "Error": "User is not a manager of the organisation",
+ "SubCode": "UserNotPermitted",
+ }, 403
except NotFound:
- return {"Error": "Organisation Campaign Not Found"}, 404
+ return {
+ "Error": "Organisation Campaign Not Found",
+ "SubCode": "NotFound",
+ }, 404
except Exception as e:
error_msg = f"Organisation Campaigns DELETE - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/organisations/resources.py b/backend/api/organisations/resources.py
index 46e5d36a88..fcd8818d19 100644
--- a/backend/api/organisations/resources.py
+++ b/backend/api/organisations/resources.py
@@ -65,11 +65,11 @@ def get(self, slug):
)
return organisation_dto.to_primitive(), 200
except NotFound:
- return {"Error": "Organisation Not Found"}, 404
+ return {"Error": "Organisation Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Organisation GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class OrganisationsRestAPI(Resource):
@@ -131,7 +131,10 @@ def post(self):
"""
request_user = User.get_by_id(token_auth.current_user())
if request_user.role != 1:
- return {"Error": "Only admin users can create organisations."}, 403
+ return {
+ "Error": "Only admin users can create organisations.",
+ "SubCode": "OnlyAdminAccess",
+ }, 403
try:
organisation_dto = NewOrganisationDTO(request.get_json())
@@ -140,17 +143,17 @@ def post(self):
organisation_dto.validate()
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
org_id = OrganisationService.create_organisation(organisation_dto)
return {"organisationId": org_id}, 201
except OrganisationServiceError as e:
- return str(e), 400
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 400
except Exception as e:
error_msg = f"Organisation PUT - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def delete(self, organisation_id):
@@ -189,18 +192,24 @@ def delete(self, organisation_id):
if not OrganisationService.can_user_manage_organisation(
organisation_id, token_auth.current_user()
):
- return {"Error": "User is not an admin for the org"}, 403
+ return {
+ "Error": "User is not an admin for the org",
+ "SubCode": "UserNotOrgAdmin",
+ }, 403
try:
OrganisationService.delete_organisation(organisation_id)
return {"Success": "Organisation deleted"}, 200
except OrganisationServiceError:
- return {"Error": "Organisation has some projects"}, 403
+ return {
+ "Error": "Organisation has some projects",
+ "SubCode": "OrgHasProjects",
+ }, 403
except NotFound:
- return {"Error": "Organisation Not Found"}, 404
+ return {"Error": "Organisation Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Organisation DELETE - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
def get(self, organisation_id):
"""
@@ -250,11 +259,11 @@ def get(self, organisation_id):
)
return organisation_dto.to_primitive(), 200
except NotFound:
- return {"Error": "Organisation Not Found"}, 404
+ return {"Error": "Organisation Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Organisation GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def patch(self, organisation_id):
@@ -319,7 +328,10 @@ def patch(self, organisation_id):
if not OrganisationService.can_user_manage_organisation(
organisation_id, token_auth.current_user()
):
- return {"Error": "User is not an admin for the org"}, 403
+ return {
+ "Error": "User is not an admin for the org",
+ "SubCode": "UserNotOrgAdmin",
+ }, 403
try:
organisation_dto = UpdateOrganisationDTO(request.get_json())
organisation_dto.organisation_id = organisation_id
@@ -331,19 +343,19 @@ def patch(self, organisation_id):
organisation_dto.validate()
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
OrganisationService.update_organisation(organisation_dto)
return {"Status": "Updated"}, 200
except NotFound as e:
- return {"Error": str(e)}, 404
+ return {"Error": str(e), "SubCode": "NotFound"}, 404
except OrganisationServiceError as e:
- return str(e), 402
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 402
except Exception as e:
error_msg = f"Organisation PATCH - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class OrganisationsStatsAPI(Resource):
@@ -377,11 +389,11 @@ def get(self, organisation_id):
)
return organisation_dto.to_primitive(), 200
except NotFound:
- return {"Error": "Organisation Not Found"}, 404
+ return {"Error": "Organisation Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Organisation GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class OrganisationsAllAPI(Resource):
@@ -435,7 +447,8 @@ def get(self):
if manager_user_id is not None and not authenticated_user_id:
return (
{
- "Error": "Unauthorized - Filter by manager_user_id is not allowed to unauthenticated requests"
+ "Error": "Unauthorized - Filter by manager_user_id is not allowed to unauthenticated requests",
+ "SubCode": "LoginToFilterManager",
},
403,
)
@@ -449,8 +462,8 @@ def get(self):
)
return results_dto.to_primitive(), 200
except NotFound:
- return {"Error": "No organisations found"}, 404
+ return {"Error": "No organisations found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Organisations GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/projects/actions.py b/backend/api/projects/actions.py
index 8a72b96954..dfbd54a2aa 100644
--- a/backend/api/projects/actions.py
+++ b/backend/api/projects/actions.py
@@ -63,11 +63,14 @@ def post(self, project_id):
)
return {"Success": "Project Transferred"}, 200
except ValueError as e:
- return {"Error": str(e)}, 403
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"ProjectsActionsTransferAPI POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to transfer project"}, 500
+ return {
+ "Error": "Unable to transfer project",
+ "SubCode": "InternalServerError",
+ }, 500
class ProjectsActionsMessageContributorsAPI(Resource):
@@ -124,7 +127,10 @@ def post(self, project_id):
message_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to send message to mappers"}, 400
+ return {
+ "Error": "Unable to send message to mappers",
+ "SubCode": "InvalidData",
+ }, 400
try:
ProjectAdminService.is_user_action_permitted_on_project(
@@ -136,12 +142,18 @@ def post(self, project_id):
).start()
return {"Success": "Messages started"}, 200
- except ValueError as e:
- return {"Error": str(e)}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
except Exception as e:
error_msg = f"Send message all - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to send messages to mappers"}, 500
+ return {
+ "Error": "Unable to send messages to mappers",
+ "SubCode": "InternalServerError",
+ }, 500
class ProjectsActionsFeatureAPI(Resource):
@@ -184,22 +196,23 @@ def post(self, project_id):
ProjectAdminService.is_user_action_permitted_on_project(
authenticated_user_id, project_id
)
- except ValueError as e:
- error_msg = f"FeaturedProjects POST: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
ProjectService.set_project_as_featured(project_id)
return {"Success": True}, 200
except NotFound:
- return {"Error": "Project Not Found"}, 404
+ return {"Error": "Project Not Found", "SubCode": "NotFound"}, 404
except ValueError as e:
- error_msg = f"FeaturedProjects POST: {str(e)}"
- return {"Error": error_msg}, 400
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"FeaturedProjects POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class ProjectsActionsUnFeatureAPI(Resource):
@@ -241,22 +254,23 @@ def post(self, project_id):
ProjectAdminService.is_user_action_permitted_on_project(
token_auth.current_user(), project_id
)
- except ValueError as e:
- error_msg = f"FeaturedProjects POST: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
ProjectService.unset_project_as_featured(project_id)
return {"Success": True}, 200
except NotFound:
- return {"Error": "Project Not Found"}, 404
+ return {"Error": "Project Not Found", "SubCode": "NotFound"}, 404
except ValueError as e:
- error_msg = f"FeaturedProjects DELETE: {str(e)}"
- return {"Error": error_msg}, 400
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"FeaturedProjects DELETE - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class ProjectsActionsSetInterestsAPI(Resource):
@@ -308,9 +322,11 @@ def post(self, project_id):
ProjectAdminService.is_user_action_permitted_on_project(
token_auth.current_user(), project_id
)
- except ValueError as e:
- error_msg = f"ProjectsActionsSetInterestsAPI POST: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
data = request.get_json()
@@ -319,13 +335,13 @@ def post(self, project_id):
)
return project_interests.to_primitive(), 200
except NotFound:
- return {"Error": "Project not Found"}, 404
+ return {"Error": "Project not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = (
f"ProjectsActionsSetInterestsAPI POST - unhandled error: {str(e)}"
)
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class ProjectActionsIntersectingTilesAPI(Resource):
@@ -390,7 +406,7 @@ def post(self):
grid_dto.validate()
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
grid = GridService.trim_grid_to_aoi(grid_dto)
@@ -400,4 +416,4 @@ def post(self):
except Exception as e:
error_msg = f"IntersectingTiles GET API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"error": error_msg}, 500
+ return {"error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/projects/activities.py b/backend/api/projects/activities.py
index 8bdd8814b6..9cf4f2097c 100644
--- a/backend/api/projects/activities.py
+++ b/backend/api/projects/activities.py
@@ -36,7 +36,7 @@ def get(self, project_id):
ProjectService.exists(project_id)
except NotFound as e:
current_app.logger.error(f"Error validating project: {str(e)}")
- return {"Error": "Project not found"}, 404
+ return {"Error": "Project not found", "SubCode": "NotFound"}, 404
try:
page = int(request.args.get("page")) if request.args.get("page") else 1
@@ -45,7 +45,10 @@ def get(self, project_id):
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch user activity"}, 500
+ return {
+ "Error": "Unable to fetch user activity",
+ "SubCode": "InternalServerError",
+ }, 500
class ProjectsLastActivitiesAPI(Resource):
@@ -75,7 +78,7 @@ def get(self, project_id):
ProjectService.exists(project_id)
except NotFound as e:
current_app.logger.error(f"Error validating project: {str(e)}")
- return {"Error": "Project not found"}, 404
+ return {"Error": "Project not found", "SubCode": "NotFound"}, 404
try:
activity = StatsService.get_last_activity(project_id)
@@ -83,4 +86,7 @@ def get(self, project_id):
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch user activity"}, 500
+ return {
+ "Error": "Unable to fetch user activity",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/projects/campaigns.py b/backend/api/projects/campaigns.py
index 1dd69bcde0..cc0e6a07b7 100644
--- a/backend/api/projects/campaigns.py
+++ b/backend/api/projects/campaigns.py
@@ -53,9 +53,11 @@ def post(self, project_id, campaign_id):
ProjectAdminService.is_user_action_permitted_on_project(
token_auth.current_user(), project_id
)
- except ValueError as e:
- error_msg = f"ProjectsCampaignsAPI POST: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
campaign_project_dto = CampaignProjectDTO()
@@ -64,7 +66,7 @@ def post(self, project_id, campaign_id):
campaign_project_dto.validate()
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
CampaignService.create_campaign_project(campaign_project_dto)
@@ -75,7 +77,7 @@ def post(self, project_id, campaign_id):
except Exception as e:
error_msg = f"ProjectsCampaignsAPI POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
def get(self, project_id):
"""
@@ -106,11 +108,11 @@ def get(self, project_id):
campaigns = CampaignService.get_project_campaigns_as_dto(project_id)
return campaigns.to_primitive(), 200
except NotFound:
- return {"Error": "No campaign found"}, 404
+ return {"Error": "No campaign found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Messages GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def delete(self, project_id, campaign_id):
@@ -156,16 +158,18 @@ def delete(self, project_id, campaign_id):
ProjectAdminService.is_user_action_permitted_on_project(
token_auth.current_user(), project_id
)
- except ValueError as e:
- error_msg = f"ProjectsCampaignsAPI DELETE: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
CampaignService.delete_project_campaign(project_id, campaign_id)
return {"Success": "Campaigns Deleted"}, 200
except NotFound:
- return {"Error": "Campaign Not Found"}, 404
+ return {"Error": "Campaign Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"ProjectsCampaignsAPI DELETE - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/projects/contributions.py b/backend/api/projects/contributions.py
index 7d4f857f93..e8da1a58e1 100644
--- a/backend/api/projects/contributions.py
+++ b/backend/api/projects/contributions.py
@@ -32,7 +32,7 @@ def get(self, project_id):
ProjectService.exists(project_id)
except NotFound as e:
current_app.logger.error(f"Error validating project: {str(e)}")
- return {"Error": "Project not found"}, 404
+ return {"Error": "Project not found", "SubCode": "NotFound"}, 404
try:
contributions = StatsService.get_user_contributions(project_id)
@@ -40,7 +40,10 @@ def get(self, project_id):
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch user contributions"}, 500
+ return {
+ "Error": "Unable to fetch user contributions",
+ "SubCode": "InternalServerError",
+ }, 500
class ProjectsContributionsQueriesDayAPI(Resource):
@@ -71,8 +74,11 @@ def get(self, project_id):
contribs = ProjectService.get_contribs_by_day(project_id)
return contribs.to_primitive(), 200
except NotFound:
- return {"Error": "Project not found"}, 404
+ return {"Error": "Project not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Project contributions GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch per day user contribution"}, 500
+ return {
+ "Error": "Unable to fetch per day user contribution",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/projects/favorites.py b/backend/api/projects/favorites.py
index 6c0334de9f..485c402418 100644
--- a/backend/api/projects/favorites.py
+++ b/backend/api/projects/favorites.py
@@ -47,11 +47,11 @@ def get(self, project_id: int):
return {"favorited": False}, 200
except NotFound:
- return {"Error": "Project Not Found"}, 404
+ return {"Error": "Project Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Favorite GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def post(self, project_id: int):
@@ -92,17 +92,17 @@ def post(self, project_id: int):
favorite_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
ProjectService.favorite(project_id, authenticated_user_id)
except NotFound:
- return {"Error": "Project Not Found"}, 404
+ return {"Error": "Project Not Found", "SubCode": "NotFound"}, 404
except ValueError as e:
return {"Error": str(e)}, 400
except Exception as e:
error_msg = f"Favorite PUT - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
return {"project_id": project_id}, 200
@@ -140,12 +140,12 @@ def delete(self, project_id: int):
try:
ProjectService.unfavorite(project_id, token_auth.current_user())
except NotFound:
- return {"Error": "Project Not Found"}, 404
+ return {"Error": "Project Not Found", "SubCode": "NotFound"}, 404
except ValueError as e:
- return {"Error": str(e)}, 400
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"Favorite PUT - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
return {"project_id": project_id}, 200
diff --git a/backend/api/projects/resources.py b/backend/api/projects/resources.py
index 1911aa32ee..bc7b39f837 100644
--- a/backend/api/projects/resources.py
+++ b/backend/api/projects/resources.py
@@ -112,15 +112,18 @@ def get(self, project_id):
return project_dto, 200
else:
- return {"Error": "Private Project"}, 403
+ return {"Error": "Private Project", "SubCode": "PrivateProject"}, 403
except NotFound:
- return {"Error": "Project Not Found"}, 404
+ return {"Error": "Project Not Found", "SubCode": "NotFound"}, 404
except ProjectServiceError as e:
- return {"Error": str(e)}, 403
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"Project GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch project"}, 500
+ return {
+ "Error": "Unable to fetch project",
+ "SubCode": "InternalServerError",
+ }, 500
finally:
# this will try to unlock tasks that have been locked too long
try:
@@ -200,7 +203,7 @@ def post(self):
draft_project_dto.validate()
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return {"Error": "Unable to create project"}, 400
+ return {"Error": "Unable to create project", "SubCode": "InvalidData"}, 400
try:
draft_project_id = ProjectAdminService.create_draft_project(
@@ -208,13 +211,16 @@ def post(self):
)
return {"projectId": draft_project_id}, 201
except ProjectAdminServiceError as e:
- return {"Error": str(e)}, 403
- except (InvalidGeoJson, InvalidData):
- return {"Error": "Invalid GeoJson"}, 400
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
+ except (InvalidGeoJson, InvalidData) as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 400
except Exception as e:
error_msg = f"Project PUT - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to create project"}, 500
+ return {
+ "Error": "Unable to create project",
+ "SubCode": "InternalServerError",
+ }, 500
@token_auth.login_required
def head(self, project_id):
@@ -254,19 +260,24 @@ def head(self, project_id):
ProjectAdminService.is_user_action_permitted_on_project(
token_auth.current_user(), project_id
)
- except ValueError as e:
- error_msg = f"ProjectsRestAPI HEAD: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
project_dto = ProjectAdminService.get_project_dto_for_admin(project_id)
return project_dto.to_primitive(), 200
except NotFound:
- return {"Error": "Project Not Found"}, 404
+ return {"Error": "Project Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"ProjectsRestAPI HEAD - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch project"}, 500
+ return {
+ "Error": "Unable to fetch project",
+ "SubCode": "InternalServerError",
+ }, 500
@token_auth.login_required
def patch(self, project_id):
@@ -396,9 +407,11 @@ def patch(self, project_id):
ProjectAdminService.is_user_action_permitted_on_project(
authenticated_user_id, project_id
)
- except ValueError as e:
- error_msg = f"ProjectsRestAPI PATCH: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
project_dto = ProjectDTO(request.get_json())
@@ -406,7 +419,7 @@ def patch(self, project_id):
project_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to update project"}, 400
+ return {"Error": "Unable to update project", "SubCode": "InvalidData"}, 400
try:
ProjectAdminService.update_project(project_dto, authenticated_user_id)
@@ -414,13 +427,16 @@ def patch(self, project_id):
except InvalidGeoJson as e:
return {"Invalid GeoJson": str(e)}, 400
except NotFound as e:
- return {"Error": str(e) or "Project Not Found"}, 404
+ return {"Error": str(e) or "Project Not Found", "SubCode": "NotFound"}, 404
except ProjectAdminServiceError as e:
- return {"Error": str(e)}, 400
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"ProjectsRestAPI PATCH - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to update project"}, 500
+ return {
+ "Error": "Unable to update project",
+ "SubCode": "InternalServerError",
+ }, 500
@token_auth.login_required
def delete(self, project_id):
@@ -461,21 +477,26 @@ def delete(self, project_id):
ProjectAdminService.is_user_action_permitted_on_project(
authenticated_user_id, project_id
)
- except ValueError as e:
- error_msg = f"ProjectsRestAPI DELETE: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
ProjectAdminService.delete_project(project_id, authenticated_user_id)
return {"Success": "Project deleted"}, 200
- except ProjectAdminServiceError:
- return {"Error": "Project has some mapping"}, 403
+ except ProjectAdminServiceError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except NotFound:
- return {"Error": "Project Not Found"}, 404
+ return {"Error": "Project Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"ProjectsRestAPI DELETE - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to delete project"}, 500
+ return {
+ "Error": "Unable to delete project",
+ "SubCode": "InternalServerError",
+ }, 500
class ProjectSearchBase(Resource):
@@ -694,7 +715,10 @@ def get(self):
except Exception as e:
error_msg = f"Projects GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch projects"}, 500
+ return {
+ "Error": "Unable to fetch projects",
+ "SubCode": "InternalServerError",
+ }, 500
class ProjectsQueriesBboxAPI(Resource):
@@ -753,7 +777,7 @@ def get(self):
authenticated_user_id
)
if len(orgs_dto.organisations) < 1:
- raise ValueError("User not a project manager")
+ raise ValueError("UserPermissionError- User not a project manager")
except ValueError as e:
error_msg = f"ProjectsQueriesBboxAPI GET: {str(e)}"
return {"Error": error_msg}, 403
@@ -773,18 +797,24 @@ def get(self):
search_dto.validate()
except Exception as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to fetch projects"}, 400
+ return {
+ "Error": "Unable to fetch projects",
+ "SubCode": "InternalServerError",
+ }, 400
try:
geojson = ProjectSearchService.get_projects_geojson(search_dto)
return geojson, 200
- except BBoxTooBigError:
- return {"Error": "Bounding Box too large"}, 403
- except ProjectSearchServiceError:
- return {"Error": "Unable to fetch projects"}, 400
+ except BBoxTooBigError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
+ except ProjectSearchServiceError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"ProjectsQueriesBboxAPI GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch projects"}, 500
+ return {
+ "Error": "Unable to fetch projects",
+ "SubCode": "InternalServerError",
+ }, 500
class ProjectsQueriesOwnerAPI(ProjectSearchBase):
@@ -828,10 +858,9 @@ def get(self):
authenticated_user_id
)
if len(orgs_dto.organisations) < 1:
- raise ValueError("User not a project manager")
+ raise ValueError("UserPermissionError- User not a project manager")
except ValueError as e:
- error_msg = f"ProjectsQueriesOwnerAPI GET: {str(e)}"
- return {"Error": error_msg}, 403
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
try:
search_dto = self.setup_search_dto()
@@ -842,11 +871,11 @@ def get(self):
)
return admin_projects.to_primitive(), 200
except NotFound:
- return {"Error": "No comments found"}, 404
+ return {"Error": "No comments found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Project GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class ProjectsQueriesTouchedAPI(Resource):
@@ -888,11 +917,14 @@ def get(self, username):
user_dto = UserService.get_mapped_projects(username, locale)
return user_dto.to_primitive(), 200
except NotFound:
- return {"Error": "User not found"}, 404
+ return {"Error": "User not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch projects"}, 500
+ return {
+ "Error": "Unable to fetch projects",
+ "SubCode": "InternalServerError",
+ }, 500
class ProjectsQueriesSummaryAPI(Resource):
@@ -930,11 +962,14 @@ def get(self, project_id: int):
summary = ProjectService.get_project_summary(project_id, preferred_locale)
return summary.to_primitive(), 200
except NotFound:
- return {"Error": "Project not found"}, 404
+ return {"Error": "Project not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Project Summary GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch project summary"}, 500
+ return {
+ "Error": "Unable to fetch project summary",
+ "SubCode": "InternalServerError",
+ }, 500
class ProjectsQueriesNoGeometriesAPI(Resource):
@@ -996,13 +1031,16 @@ def get(self, project_id):
return project_dto, 200
except NotFound:
- return {"Error": "Project Not Found"}, 404
- except ProjectServiceError:
- return {"Error": "Unable to fetch project"}, 403
+ return {"Error": "Project Not Found", "SubCode": "NotFound"}, 404
+ except ProjectServiceError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"Project GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch project"}, 500
+ return {
+ "Error": "Unable to fetch project",
+ "SubCode": "InternalServerError",
+ }, 500
finally:
# this will try to unlock tasks that have been locked too long
try:
@@ -1050,19 +1088,21 @@ def get(self, project_id):
ProjectAdminService.is_user_action_permitted_on_project(
token_auth.current_user(), project_id
)
- except ValueError as e:
- error_msg = f"ProjectsQueriesNoTasksAPI GET: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
project_dto = ProjectAdminService.get_project_dto_for_admin(project_id)
return project_dto.to_primitive(), 200
except NotFound:
- return {"Error": "Project Not Found"}, 404
+ return {"Error": "Project Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Project GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class ProjectsQueriesAoiAPI(Resource):
@@ -1115,13 +1155,16 @@ def get(self, project_id):
return project_aoi, 200
except NotFound:
- return {"Error": "Project Not Found"}, 404
+ return {"Error": "Project Not Found", "SubCode": "NotFound"}, 404
except ProjectServiceError:
return {"Error": "Unable to fetch project"}, 403
except Exception as e:
error_msg = f"Project GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch project"}, 500
+ return {
+ "Error": "Unable to fetch project",
+ "SubCode": "InternalServerError",
+ }, 500
class ProjectsQueriesPriorityAreasAPI(Resource):
@@ -1154,13 +1197,16 @@ def get(self, project_id):
priority_areas = ProjectService.get_project_priority_areas(project_id)
return priority_areas, 200
except NotFound:
- return {"Error": "Project Not Found"}, 404
+ return {"Error": "Project Not Found", "SubCode": "NotFound"}, 404
except ProjectServiceError:
return {"Error": "Unable to fetch project"}, 403
except Exception as e:
error_msg = f"Project GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch project"}, 500
+ return {
+ "Error": "Unable to fetch project",
+ "SubCode": "InternalServerError",
+ }, 500
class ProjectsQueriesFeaturedAPI(Resource):
@@ -1192,4 +1238,4 @@ def get(self):
except Exception as e:
error_msg = f"FeaturedProjects GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/projects/statistics.py b/backend/api/projects/statistics.py
index 6c24b2e238..566291d050 100644
--- a/backend/api/projects/statistics.py
+++ b/backend/api/projects/statistics.py
@@ -24,7 +24,7 @@ def get(self):
except Exception as e:
error_msg = f"Unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class ProjectsStatisticsAPI(Resource):
@@ -62,11 +62,14 @@ def get(self, project_id):
summary = ProjectService.get_project_stats(project_id)
return summary.to_primitive(), 200
except NotFound:
- return {"Error": "Project not found"}, 404
+ return {"Error": "Project not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Project Summary GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch project statistics"}, 500
+ return {
+ "Error": "Unable to fetch project statistics",
+ "SubCode": "InternalServerError",
+ }, 500
class ProjectsStatisticsQueriesUsernameAPI(Resource):
@@ -103,8 +106,11 @@ def get(self, project_id, username):
stats_dto = ProjectService.get_project_user_stats(project_id, username)
return stats_dto.to_primitive(), 200
except NotFound:
- return {"Error": "User not found"}, 404
+ return {"Error": "User not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch user statistics for project"}, 500
+ return {
+ "Error": "Unable to fetch user statistics for project",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/projects/teams.py b/backend/api/projects/teams.py
index 9da20c1cf9..e2685c9e89 100644
--- a/backend/api/projects/teams.py
+++ b/backend/api/projects/teams.py
@@ -43,7 +43,7 @@ def get(self, project_id):
except Exception as e:
error_msg = f"Team GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def post(self, team_id, project_id):
@@ -93,13 +93,16 @@ def post(self, team_id, project_id):
description: Internal Server Error
"""
if not TeamService.is_user_team_manager(team_id, token_auth.current_user()):
- return {"Error": "User is not an admin or a manager for the team"}, 401
+ return {
+ "Error": "User is not an admin or a manager for the team",
+ "SubCode": "UserPermissionError",
+ }, 401
try:
role = request.get_json(force=True)["role"]
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
TeamService.add_team_project(team_id, project_id, role)
@@ -114,7 +117,7 @@ def post(self, team_id, project_id):
except Exception as e:
error_msg = f"Project Team POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def patch(self, team_id, project_id):
@@ -169,19 +172,19 @@ def patch(self, team_id, project_id):
role = request.get_json(force=True)["role"]
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
TeamService.change_team_role(team_id, project_id, role)
return {"Status": "Team role updated successfully."}, 200
except NotFound as e:
- return {"Error": str(e)}, 404
+ return {"Error": str(e), "SubCode": "NotFound"}, 404
except TeamServiceError as e:
return str(e), 402
except Exception as e:
error_msg = f"Team-Project PATCH - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def delete(self, team_id, project_id):
@@ -223,8 +226,8 @@ def delete(self, team_id, project_id):
TeamService.delete_team_project(team_id, project_id)
return {"Success": True}, 200
except NotFound:
- return {"Error": "No team found"}, 404
+ return {"Error": "No team found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"TeamMembers DELETE - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/system/applications.py b/backend/api/system/applications.py
index f2ad6e6a8b..bc563a42e9 100644
--- a/backend/api/system/applications.py
+++ b/backend/api/system/applications.py
@@ -39,7 +39,10 @@ def get(self):
except Exception as e:
error_msg = f"Application GET API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch application keys"}, 500
+ return {
+ "Error": "Unable to fetch application keys",
+ "SubCode": "InternalServerError",
+ }, 500
@token_auth.login_required
def post(self):
@@ -71,7 +74,10 @@ def post(self):
except Exception as e:
error_msg = f"Application POST API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to create application keys"}, 500
+ return {
+ "Error": "Unable to create application keys",
+ "SubCode": "InternalServerError",
+ }, 500
def patch(self, application_key):
"""
@@ -105,7 +111,10 @@ def patch(self, application_key):
except Exception as e:
error_msg = f"Application PUT API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to check application key"}, 500
+ return {
+ "Error": "Unable to check application key",
+ "SubCode": "InternalServerError",
+ }, 500
@token_auth.login_required
def delete(self, application_key):
@@ -147,8 +156,11 @@ def delete(self, application_key):
else:
return 302
except NotFound:
- return {"Error": "Key does not exist for user"}, 404
+ return {"Error": "Key does not exist for user", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Application DELETE API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to delete application key"}, 500
+ return {
+ "Error": "Unable to delete application key",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/system/authentication.py b/backend/api/system/authentication.py
index 7d75c916a2..6d01f22aef 100644
--- a/backend/api/system/authentication.py
+++ b/backend/api/system/authentication.py
@@ -10,7 +10,7 @@
@osm.tokengetter
def get_oauth_token():
- """ Required by Flask-OAuthlib. Pulls oauth token from the session so we can make authenticated requests"""
+ """Required by Flask-OAuthlib. Pulls oauth token from the session so we can make authenticated requests"""
if "osm_oauth" in session:
resp = session["osm_oauth"]
return resp["oauth_token"], resp["oauth_token_secret"]
@@ -92,7 +92,7 @@ def get(self):
user_params["session"] = osm_resp
return user_params, 200
except AuthServiceError:
- return {"Error": "Unable to authenticate"}, 500
+ return {"Error": "Unable to authenticate", "SubCode": "AuthError"}, 500
class SystemAuthenticationEmailAPI(Resource):
@@ -126,8 +126,11 @@ def get(self):
return {"Status": "OK"}, 200
except AuthServiceError:
- return {"Error": "Unable to authenticate"}, 403
+ return {"Error": "Unable to authenticate", "SubCode": "AuthError"}, 403
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to authenticate"}, 500
+ return {
+ "Error": "Unable to authenticate",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/system/general.py b/backend/api/system/general.py
index 5e2a0dcaca..f7c32e3db2 100644
--- a/backend/api/system/general.py
+++ b/backend/api/system/general.py
@@ -177,7 +177,10 @@ def get(self):
except Exception as e:
error_msg = f"Languages GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch supported languages"}, 500
+ return {
+ "Error": "Unable to fetch supported languages",
+ "SubCode": "InternalServerError",
+ }, 500
class SystemContactAdminRestAPI(Resource):
@@ -220,4 +223,7 @@ def post(self):
except Exception as e:
error_msg = f"Application GET API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch application keys"}, 500
+ return {
+ "Error": "Unable to fetch application keys",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/system/image_upload.py b/backend/api/system/image_upload.py
index 4a8978c389..8aa7dbb808 100644
--- a/backend/api/system/image_upload.py
+++ b/backend/api/system/image_upload.py
@@ -52,12 +52,18 @@ def post(self):
current_app.config["IMAGE_UPLOAD_API_URL"] is None
or current_app.config["IMAGE_UPLOAD_API_KEY"] is None
):
- return {"Error": "Image upload service not defined"}, 500
+ return {
+ "Error": "Image upload service not defined",
+ "SubCode": "UndefinedImageService",
+ }, 500
try:
data = request.get_json()
if data.get("filename") is None:
- return {"Error": "Missing filename parameter"}, 400
+ return {
+ "Error": "Missing filename parameter",
+ "SubCode": "MissingFilename",
+ }, 400
if data.get("mime") in [
"image/png",
"image/jpeg",
@@ -81,11 +87,15 @@ def post(self):
else:
return (
{
- "Error": "Mimetype is not allowed. The supported formats are: png, jpeg, webp and gif."
+ "Error": "Mimetype is not allowed. The supported formats are: png, jpeg, webp and gif.",
+ "SubCode": "UnsupportedFile",
},
400,
)
except Exception as e:
error_msg = f"Image upload POST API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to upload image"}, 500
+ return {
+ "Error": "Unable to upload image",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/system/statistics.py b/backend/api/system/statistics.py
index 4e87bd8d4f..e3984c6ae4 100644
--- a/backend/api/system/statistics.py
+++ b/backend/api/system/statistics.py
@@ -37,4 +37,7 @@ def get(self):
except Exception as e:
error_msg = f"Unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch summary statistics"}, 500
+ return {
+ "Error": "Unable to fetch summary statistics",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/tasks/actions.py b/backend/api/tasks/actions.py
index 7cee2d4b8a..f6bc5aeabf 100644
--- a/backend/api/tasks/actions.py
+++ b/backend/api/tasks/actions.py
@@ -85,21 +85,27 @@ def post(self, project_id, task_id):
lock_task_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to lock task"}, 400
+ return {"Error": "Unable to lock task", "SubCode": "InvalidData"}, 400
try:
task = MappingService.lock_task_for_mapping(lock_task_dto)
return task.to_primitive(), 200
except NotFound:
- return {"Error": "Task Not Found"}, 404
+ return {"Error": "Task Not Found", "SubCode": "NotFound"}, 404
except MappingServiceError as e:
- return {"Error": str(e)}, 403
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except UserLicenseError:
- return {"Error": "User not accepted license terms"}, 409
+ return {
+ "Error": "User not accepted license terms",
+ "SubCode": "UserLicenseError",
+ }, 409
except Exception as e:
error_msg = f"Task Lock API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to lock task"}, 500
+ return {
+ "Error": "Unable to lock task",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksActionsMappingStopAPI(Resource):
@@ -171,19 +177,22 @@ def post(self, project_id, task_id):
stop_task.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Task unlock failed"}, 400
+ return {"Error": "Task unlock failed", "SubCode": "InvalidData"}, 400
try:
task = MappingService.stop_mapping_task(stop_task)
return task.to_primitive(), 200
except NotFound:
- return {"Error": "Task Not Found"}, 404
- except MappingServiceError:
- return {"Error": "Task unlock failed"}, 403
+ return {"Error": "Task Not Found", "SubCode": "NotFound"}, 404
+ except MappingServiceError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"Task Lock API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Task unlock failed"}, 500
+ return {
+ "Error": "Task unlock failed",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksActionsMappingUnlockAPI(Resource):
@@ -255,19 +264,22 @@ def post(self, project_id, task_id):
mapped_task.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Task unlock failed"}, 400
+ return {"Error": "Task unlock failed", "SubCode": "InvalidData"}, 400
try:
task = MappingService.unlock_task_after_mapping(mapped_task)
return task.to_primitive(), 200
except NotFound:
- return {"Error": "Task Not Found"}, 404
- except MappingServiceError:
- return {"Error": "Task unlock failed"}, 403
+ return {"Error": "Task Not Found", "SubCode": "NotFound"}, 404
+ except MappingServiceError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"Task Lock API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Task unlock failed"}, 500
+ return {
+ "Error": "Task unlock failed",
+ "SubCode": "InternalServerError",
+ }, 500
finally:
# Refresh mapper level after mapping
UserService.check_and_update_mapper_level(authenticated_user_id)
@@ -325,13 +337,16 @@ def post(self, project_id, task_id):
)
return task.to_primitive(), 200
except NotFound:
- return {"Error": "Task Not Found"}, 404
- except MappingServiceError:
- return {"Error": "User not permitted to undo task"}, 403
+ return {"Error": "Task Not Found", "SubCode": "NotFound"}, 404
+ except MappingServiceError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"Task GET API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to lock task"}, 500
+ return {
+ "Error": "Unable to lock task",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksActionsValidationLockAPI(Resource):
@@ -399,22 +414,27 @@ def post(self, project_id):
validator_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to lock task"}, 400
+ return {"Error": "Unable to lock task", "SubCode": "InvalidData"}, 400
try:
tasks = ValidatorService.lock_tasks_for_validation(validator_dto)
return tasks.to_primitive(), 200
except ValidatorServiceError as e:
- error_msg = f"Validator Lock API - {str(e)}"
- return {"Error": error_msg}, 403
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except NotFound:
- return {"Error": "Task not found"}, 404
+ return {"Error": "Task not found", "SubCode": "NotFound"}, 404
except UserLicenseError:
- return {"Error": "User not accepted license terms"}, 409
+ return {
+ "Error": "User not accepted license terms",
+ "SubCode": "UserLicenseError",
+ }, 409
except Exception as e:
error_msg = f"Validator Lock API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to lock task"}, 500
+ return {
+ "Error": "Unable to lock task",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksActionsValidationStopAPI(Resource):
@@ -480,19 +500,22 @@ def post(self, project_id):
validated_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Task unlock failed"}, 400
+ return {"Error": "Task unlock failed", "SubCode": "InvalidData"}, 400
try:
tasks = ValidatorService.stop_validating_tasks(validated_dto)
return tasks.to_primitive(), 200
- except ValidatorServiceError:
- return {"Error": "Task unlock failed"}, 403
+ except ValidatorServiceError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except NotFound:
- return {"Error": "Task unlock failed"}, 404
+ return {"Error": "Task unlock failed", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Stop Validating API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Task unlock failed"}, 500
+ return {
+ "Error": "Task unlock failed",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksActionsValidationUnlockAPI(Resource):
@@ -557,19 +580,22 @@ def post(self, project_id):
validated_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Task unlock failed"}, 400
+ return {"Error": "Task unlock failed", "SubCode": "InvalidData"}, 400
try:
tasks = ValidatorService.unlock_tasks_after_validation(validated_dto)
return tasks.to_primitive(), 200
- except ValidatorServiceError:
- return {"Error": "Task unlock failed"}, 403
+ except ValidatorServiceError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except NotFound:
- return {"Error": "Task unlock failed"}, 404
+ return {"Error": "Task unlock failed", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Validator Lock API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Task unlock failed"}, 500
+ return {
+ "Error": "Task unlock failed",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksActionsMapAllAPI(Resource):
@@ -610,9 +636,11 @@ def post(self, project_id):
ProjectAdminService.is_user_action_permitted_on_project(
authenticated_user_id, project_id
)
- except ValueError as e:
- error_msg = f"TasksActionsMapAllAPI POST: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
MappingService.map_all_tasks(project_id, authenticated_user_id)
@@ -620,7 +648,10 @@ def post(self, project_id):
except Exception as e:
error_msg = f"TasksActionsMapAllAPI POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to map all the tasks"}, 500
+ return {
+ "Error": "Unable to map all the tasks",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksActionsValidateAllAPI(Resource):
@@ -661,9 +692,11 @@ def post(self, project_id):
ProjectAdminService.is_user_action_permitted_on_project(
authenticated_user_id, project_id
)
- except ValueError as e:
- error_msg = f"TasksActionsValidateAllAPI POST: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
ValidatorService.validate_all_tasks(project_id, authenticated_user_id)
@@ -671,7 +704,10 @@ def post(self, project_id):
except Exception as e:
error_msg = f"TasksActionsValidateAllAPI POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to validate all tasks"}, 500
+ return {
+ "Error": "Unable to validate all tasks",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksActionsInvalidateAllAPI(Resource):
@@ -712,9 +748,11 @@ def post(self, project_id):
ProjectAdminService.is_user_action_permitted_on_project(
authenticated_user_id, project_id
)
- except ValueError as e:
- error_msg = f"TasksActionsInvalidateAllAPI POST: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
ValidatorService.invalidate_all_tasks(project_id, authenticated_user_id)
@@ -722,7 +760,10 @@ def post(self, project_id):
except Exception as e:
error_msg = f"TasksActionsInvalidateAllAPI POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to invalidate all tasks"}, 500
+ return {
+ "Error": "Unable to invalidate all tasks",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksActionsResetBadImageryAllAPI(Resource):
@@ -763,9 +804,11 @@ def post(self, project_id):
ProjectAdminService.is_user_action_permitted_on_project(
authenticated_user_id, project_id
)
- except ValueError as e:
- error_msg = f"TasksActionsResetBadImageryAllAPI POST: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
MappingService.reset_all_badimagery(project_id, authenticated_user_id)
@@ -775,7 +818,10 @@ def post(self, project_id):
f"TasksActionsResetBadImageryAllAPI POST - unhandled error: {str(e)}"
)
current_app.logger.critical(error_msg)
- return {"Error": "Unable to reset tasks"}, 500
+ return {
+ "Error": "Unable to reset tasks",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksActionsResetAllAPI(Resource):
@@ -816,9 +862,11 @@ def post(self, project_id):
ProjectAdminService.is_user_action_permitted_on_project(
authenticated_user_id, project_id
)
- except ValueError as e:
- error_msg = f"TasksActionsResetAllAPI POST: {str(e)}"
- return {"Error": error_msg}, 403
+ except ValueError:
+ return {
+ "Error": "User is not a manager of the project",
+ "SubCode": "UserPermissionError",
+ }, 403
try:
ProjectAdminService.reset_all_tasks(project_id, authenticated_user_id)
@@ -826,7 +874,10 @@ def post(self, project_id):
except Exception as e:
error_msg = f"TasksActionsResetAllAPI POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to reset tasks"}, 500
+ return {
+ "Error": "Unable to reset tasks",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksActionsSplitAPI(Resource):
@@ -889,17 +940,20 @@ def post(self, project_id, task_id):
split_task_dto.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to split task"}, 400
+ return {"Error": "Unable to split task", "SubCode": "InvalidData"}, 400
try:
tasks = SplitService.split_task(split_task_dto)
return tasks.to_primitive(), 200
except NotFound:
- return {"Error": "Task Not Found"}, 404
+ return {"Error": "Task Not Found", "SubCode": "NotFound"}, 404
except SplitServiceError as e:
- return {"Error": str(e)}, 403
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except InvalidGeoJson as e:
- return {"Error": str(e)}, 500
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"TasksActionsSplitAPI POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to split task"}, 500
+ return {
+ "Error": "Unable to split task",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/tasks/resources.py b/backend/api/tasks/resources.py
index d187cf218f..5f884333bb 100644
--- a/backend/api/tasks/resources.py
+++ b/backend/api/tasks/resources.py
@@ -60,11 +60,14 @@ def get(self, project_id, task_id):
task = MappingService.get_task_as_dto(task_id, project_id, preferred_locale)
return task.to_primitive(), 200
except NotFound:
- return {"Error": "Task Not Found"}, 404
+ return {"Error": "Task Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"TasksRestAPI - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch task"}, 500
+ return {
+ "Error": "Unable to fetch task",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksQueriesJsonAPI(Resource):
@@ -124,12 +127,15 @@ def get(self, project_id):
return tasks_json, 200
except NotFound:
- return {"Error": "Project or Task Not Found"}, 404
+ return {"Error": "Project or Task Not Found", "SubCode": "NotFound"}, 404
except ProjectServiceError as e:
return {"Error": str(e)}, 403
except Exception as e:
current_app.logger.critical(e)
- return {"Error": "Unable to fetch task JSON"}, 500
+ return {
+ "Error": "Unable to fetch task JSON",
+ "SubCode": "InternalServerError",
+ }, 500
@token_auth.login_required
def delete(self, project_id):
@@ -179,24 +185,36 @@ def delete(self, project_id):
user_id = token_auth.current_user()
user = UserService.get_user_by_id(user_id)
if user.role != UserRole.ADMIN.value:
- return {"Error": "This endpoint action is restricted to ADMIN users."}, 403
+ return {
+ "Error": "This endpoint action is restricted to ADMIN users.",
+ "SubCode": "OnlyAdminAccess",
+ }, 403
tasks_ids = request.get_json().get("tasks")
if tasks_ids is None:
- return {"Error": "Tasks ids not provided"}, 400
+ return {"Error": "Tasks ids not provided", "SubCode": "InvalidData"}, 400
if type(tasks_ids) != list:
- return {"Error": "Tasks were not provided as a list"}, 400
+ return {
+ "Error": "Tasks were not provided as a list",
+ "SubCode": "InvalidData",
+ }, 400
try:
ProjectService.delete_tasks(project_id, tasks_ids)
return {"Success": "Task(s) deleted"}, 200
except NotFound as e:
- return {"Error": f"Project or Task Not Found: {e}"}, 404
+ return {
+ "Error": f"Project or Task Not Found: {e}",
+ "SubCode": "NotFound",
+ }, 404
except ProjectServiceError as e:
return {"Error": str(e)}, 403
except Exception as e:
current_app.logger.critical(e)
- return {"Error": "Unable to delete tasks"}, 500
+ return {
+ "Error": "Unable to delete tasks",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksQueriesXmlAPI(Resource):
@@ -256,13 +274,19 @@ def get(self, project_id):
return Response(xml, mimetype="text/xml", status=200)
except NotFound:
return (
- {"Error": "Not found; please check the project and task numbers."},
+ {
+ "Error": "Not found; please check the project and task numbers.",
+ "SubCode": "NotFound",
+ },
404,
)
except Exception as e:
error_msg = f"TasksQueriesXmlAPI - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch task XML"}, 500
+ return {
+ "Error": "Unable to fetch task XML",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksQueriesGpxAPI(Resource):
@@ -323,13 +347,19 @@ def get(self, project_id):
return Response(xml, mimetype="text/xml", status=200)
except NotFound:
return (
- {"Error": "Not found; please check the project and task numbers."},
+ {
+ "Error": "Not found; please check the project and task numbers.",
+ "SubCode": "NotFound",
+ },
404,
)
except Exception as e:
error_msg = f"TasksQueriesGpxAPI - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch task GPX"}, 500
+ return {
+ "Error": "Unable to fetch task GPX",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksQueriesAoiAPI(Resource):
@@ -394,17 +424,23 @@ def put(self):
grid_dto.validate()
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return {"Error": "Unable to fetch tiles interesecting AOI"}, 400
+ return {
+ "Error": "Unable to fetch tiles interesecting AOI",
+ "SubCode": "InvalidData",
+ }, 400
try:
grid = GridService.trim_grid_to_aoi(grid_dto)
return grid, 200
except InvalidGeoJson as e:
- return {"Error": f"{str(e)}"}, 400
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 400
except Exception as e:
error_msg = f"TasksQueriesAoiAPI - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch tiles intersecting AOI"}, 500
+ return {
+ "Error": "Unable to fetch tiles intersecting AOI",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksQueriesMappedAPI(Resource):
@@ -435,7 +471,10 @@ def get(self, project_id):
except Exception as e:
error_msg = f"Task Lock API - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch mapped tasks"}, 500
+ return {
+ "Error": "Unable to fetch mapped tasks",
+ "SubCode": "InternalServerError",
+ }, 500
class TasksQueriesOwnInvalidatedAPI(Resource):
@@ -536,8 +575,11 @@ def get(self, username):
)
return invalidated_tasks.to_primitive(), 200
except NotFound:
- return {"Error": "No invalidated tasks"}, 404
+ return {"Error": "No invalidated tasks", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"TasksQueriesMappedAPI - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch invalidated tasks for user"}, 500
+ return {
+ "Error": "Unable to fetch invalidated tasks for user",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/tasks/statistics.py b/backend/api/tasks/statistics.py
index 469dae13e4..1485a3e2ed 100644
--- a/backend/api/tasks/statistics.py
+++ b/backend/api/tasks/statistics.py
@@ -67,11 +67,15 @@ def get(self):
start_date = validate_date_input(request.args.get("startDate"))
end_date = validate_date_input(request.args.get("endDate", date.today()))
if not (start_date):
- raise KeyError("Missing start date parameter")
+ raise KeyError("MissingDate- Missing start date parameter")
if end_date < start_date:
- raise ValueError("Start date must be earlier than end date")
+ raise ValueError(
+ "InvalidStartDate- Start date must be earlier than end date"
+ )
if (end_date - start_date) > timedelta(days=366):
- raise ValueError("Date range can not be bigger than 1 year")
+ raise ValueError(
+ "InvalidDateRange- Date range can not be bigger than 1 year"
+ )
organisation_id = request.args.get("organisationId", None, int)
organisation_name = request.args.get("organisationName", None, str)
campaign = request.args.get("campaign", None, str)
@@ -90,8 +94,7 @@ def get(self):
)
return task_stats.to_primitive(), 200
except (KeyError, ValueError) as e:
- error_msg = f"Task Statistics GET - {str(e)}"
- return {"Error": error_msg}, 400
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 400
except Exception as e:
error_msg = f"Task Statistics GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
diff --git a/backend/api/teams/actions.py b/backend/api/teams/actions.py
index edeae5c263..f193d9d4f6 100644
--- a/backend/api/teams/actions.py
+++ b/backend/api/teams/actions.py
@@ -60,7 +60,10 @@ def post(self, team_id):
role = post_data.get("role", None)
except (DataError, KeyError) as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return str(e), 400
+ return {
+ "Error": str(e),
+ "SubCode": "InvalidData",
+ }, 400
try:
authenticated_user_id = token_auth.current_user()
@@ -70,11 +73,11 @@ def post(self, team_id):
else:
return {"Success": "Request to join the team sent successfully."}, 200
except TeamJoinNotAllowed as e:
- return {"Error": str(e)}, 403
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except Exception as e:
error_msg = f"User POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@tm.pm_only(False)
@token_auth.login_required
@@ -138,7 +141,10 @@ def patch(self, team_id):
role = json_data.get("role", "member")
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return str(e), 400
+ return {
+ "Error": str(e),
+ "SubCode": "InvalidData",
+ }, 400
try:
authenticated_user_id = token_auth.current_user()
@@ -151,7 +157,8 @@ def patch(self, team_id):
else:
return (
{
- "Error": "You don't have permissions to approve this join team request"
+ "Error": "You don't have permissions to approve this join team request",
+ "SubCode": "ApproveJoinError",
},
403,
)
@@ -163,7 +170,10 @@ def patch(self, team_id):
except Exception as e:
error_msg = f"Team Join PUT - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {
+ "Error": error_msg,
+ "SubCode": "InternalServerError",
+ }, 500
class TeamsActionsLeaveAPI(Resource):
@@ -225,16 +235,20 @@ def post(self, team_id):
{
"Error": "You don't have permissions to remove {} from this team.".format(
username
- )
+ ),
+ "SubCode": "RemoveUserError",
},
403,
)
except NotFound:
- return {"Error": "No team member found"}, 404
+ return {"Error": "No team member found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"TeamMembers DELETE - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {
+ "Error": error_msg,
+ "SubCode": "InternalServerError",
+ }, 500
class TeamsActionsMessageMembersAPI(Resource):
@@ -301,12 +315,20 @@ def post(self, team_id):
message_dto.from_user_id = authenticated_user_id
message_dto.validate()
if not message_dto.message.strip() or not message_dto.subject.strip():
- raise DataError({"Validation": "Empty message not allowed"})
+ raise DataError(
+ {"Error": "Empty message not allowed", "SubCode": "EmptyMessage"}
+ )
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Request payload did not match validation"}, 400
+ return {
+ "Error": "Request payload did not match validation",
+ "SubCode": "InvalidData",
+ }, 400
except ValueError:
- return {"Error": "Unauthorised to send message to team members"}, 403
+ return {
+ "Error": "Unauthorised to send message to team members",
+ "SubCode": "UserNotPermitted",
+ }, 403
try:
threading.Thread(
@@ -320,4 +342,7 @@ def post(self, team_id):
except Exception as e:
error_msg = f"Send message all - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to send messages to team members"}, 500
+ return {
+ "Error": "Unable to send messages to team members",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/teams/resources.py b/backend/api/teams/resources.py
index 721b0bd422..9f0a57a7d4 100644
--- a/backend/api/teams/resources.py
+++ b/backend/api/teams/resources.py
@@ -86,22 +86,25 @@ def post(self, team_id):
) and not OrganisationService.can_user_manage_organisation(
org.id, authenticated_user_id
):
- return {"Error": "User is not a admin or a manager for the team"}, 401
+ return {
+ "Error": "User is not a admin or a manager for the team",
+ "SubCode": "UserNotTeamManager",
+ }, 401
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
TeamService.update_team(team_dto)
return {"Status": "Updated"}, 200
except NotFound as e:
- return {"Error": str(e)}, 404
+ return {"Error": str(e), "SubCode": "NotFound"}, 404
except TeamServiceError as e:
return str(e), 402
except Exception as e:
error_msg = f"Team POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def patch(self, team_id):
@@ -173,22 +176,25 @@ def patch(self, team_id):
) and not OrganisationService.can_user_manage_organisation(
team.organisation_id, authenticated_user_id
):
- return {"Error": "User is not a admin or a manager for the team"}, 401
+ return {
+ "Error": "User is not a admin or a manager for the team",
+ "SubCode": "UserNotTeamManager",
+ }, 401
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
TeamService.update_team(team_dto)
return {"Status": "Updated"}, 200
except NotFound as e:
- return {"Error": str(e)}, 404
+ return {"Error": str(e), "SubCode": "NotFound"}, 404
except TeamServiceError as e:
return str(e), 402
except Exception as e:
error_msg = f"Team PATCH - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
def get(self, team_id):
"""
@@ -230,11 +236,11 @@ def get(self, team_id):
team_dto = TeamService.get_team_as_dto(team_id, user_id, omit_members)
return team_dto.to_primitive(), 200
except NotFound:
- return {"Error": "Team Not Found"}, 404
+ return {"Error": "Team Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Team GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
# TODO: Add delete API then do front end services and ui work
@@ -273,16 +279,19 @@ def delete(self, team_id):
description: Internal Server Error
"""
if not TeamService.is_user_team_manager(team_id, token_auth.current_user()):
- return {"Error": "User is not a manager for the team"}, 401
+ return {
+ "Error": "User is not a manager for the team",
+ "SubCode": "UserNotTeamManager",
+ }, 401
try:
TeamService.delete_team(team_id)
return {"Success": "Team deleted"}, 200
except NotFound:
- return {"Error": "Team Not Found"}, 404
+ return {"Error": "Team Not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Team DELETE - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class TeamsAllAPI(Resource):
@@ -352,7 +361,7 @@ def get(self):
except Exception as e:
error_msg = f"Teams GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
filters = {}
@@ -387,7 +396,7 @@ def get(self):
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
@token_auth.login_required
def post(self):
@@ -445,7 +454,7 @@ def post(self):
team_dto.validate()
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
organisation_id = team_dto.organisation_id
@@ -458,16 +467,14 @@ def post(self):
team_id = TeamService.create_team(team_dto)
return {"teamId": team_id}, 201
else:
- error_msg = (
- "Team POST - User not permitted to create team for the Organisation"
- )
- return {"Error": error_msg}, 403
+ error_msg = "User not permitted to create team for the Organisation"
+ return {"Error": error_msg, "SubCode": "CreateTeamNotPermitted"}, 403
except TeamServiceError as e:
return str(e), 400
except NotFound:
error_msg = "Team POST - Organisation does not exist"
- return {"Error": error_msg}, 400
+ return {"Error": error_msg, "SubCode": "NotFound"}, 400
except Exception as e:
error_msg = f"Team POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/users/actions.py b/backend/api/users/actions.py
index 5b4303c178..45a108d68a 100644
--- a/backend/api/users/actions.py
+++ b/backend/api/users/actions.py
@@ -79,12 +79,18 @@ def patch(self):
user_dto.validate()
authenticated_user_id = token_auth.current_user()
if authenticated_user_id != user_dto.id:
- return {"Error": "Unable to authenticate"}, 401
+ return {
+ "Error": "Unable to authenticate",
+ "SubCode": "UnableToAuth",
+ }, 401
except ValueError as e:
return {"Error": str(e)}, 400
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return {"Error": "Unable to update user details"}, 400
+ return {
+ "Error": "Unable to update user details",
+ "SubCode": "InvalidData",
+ }, 400
try:
verification_sent = UserService.update_user_details(
@@ -92,11 +98,14 @@ def patch(self):
)
return verification_sent, 200
except NotFound:
- return {"Error": "User not found"}, 404
+ return {"Error": "User not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to update user details"}, 500
+ return {
+ "Error": "Unable to update user details",
+ "SubCode": "InternalServerError",
+ }, 500
class UsersActionsSetLevelAPI(Resource):
@@ -144,14 +153,17 @@ def patch(self, username, level):
try:
UserService.set_user_mapping_level(username, level)
return {"Success": "Level set"}, 200
- except UserServiceError:
- return {"Error": "Not allowed"}, 400
+ except UserServiceError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 400
except NotFound:
- return {"Error": "User or mapping not found"}, 404
+ return {"Error": "User or mapping not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to update mapping level"}, 500
+ return {
+ "Error": "Unable to update mapping level",
+ "SubCode": "InternalServerError",
+ }, 500
class UsersActionsSetRoleAPI(Resource):
@@ -199,14 +211,17 @@ def patch(self, username, role):
try:
UserService.add_role_to_user(token_auth.current_user(), username, role)
return {"Success": "Role Added"}, 200
- except UserServiceError:
- return {"Error": "Not allowed"}, 403
+ except UserServiceError as e:
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 403
except NotFound:
- return {"Error": "User or mapping not found"}, 404
+ return {"Error": "User or mapping not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to update user role"}, 500
+ return {
+ "Error": "Unable to update user role",
+ "SubCode": "InternalServerError",
+ }, 500
class UsersActionsSetExpertModeAPI(Resource):
@@ -252,11 +267,14 @@ def patch(self, is_expert):
except UserServiceError:
return {"Error": "Not allowed"}, 400
except NotFound:
- return {"Error": "User not found"}, 404
+ return {"Error": "User not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"UserSetExpert POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to update expert mode"}, 500
+ return {
+ "Error": "Unable to update expert mode",
+ "SubCode": "InternalServerError",
+ }, 500
class UsersActionsVerifyEmailAPI(Resource):
@@ -289,7 +307,10 @@ def patch(self):
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to send verification email"}, 500
+ return {
+ "Error": "Unable to send verification email",
+ "SubCode": "InternalServerError",
+ }, 500
class UsersActionsRegisterEmailAPI(Resource):
@@ -324,7 +345,7 @@ def post(self):
user_dto.validate()
except DataError as e:
current_app.logger.error(f"error validating request: {str(e)}")
- return str(e), 400
+ return {"Error": str(e), "SubCode": "InvalidData"}, 400
try:
user = UserService.register_user_with_email(user_dto)
@@ -395,8 +416,8 @@ def post(self):
except ValueError as e:
return {"Error": str(e)}, 400
except NotFound:
- return {"Error": "Interest not Found"}, 404
+ return {"Error": "Interest not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"User relationship POST - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/users/openstreetmap.py b/backend/api/users/openstreetmap.py
index ee56c5e8f6..58a114d916 100644
--- a/backend/api/users/openstreetmap.py
+++ b/backend/api/users/openstreetmap.py
@@ -41,10 +41,13 @@ def get(self, username):
osm_dto = UserService.get_osm_details_for_user(username)
return osm_dto.to_primitive(), 200
except NotFound:
- return {"Error": "User not found"}, 404
+ return {"Error": "User not found", "SubCode": "NotFound"}, 404
except UserServiceError as e:
return {"Error": str(e)}, 502
except Exception as e:
error_msg = f"User OSM GET - unhandled error: {str(e)}"
current_app.logger.error(error_msg)
- return {"Error": "Unable to fetch OpenStreetMap details"}, 500
+ return {
+ "Error": "Unable to fetch OpenStreetMap details",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/users/resources.py b/backend/api/users/resources.py
index 86789bccf3..fd72867917 100644
--- a/backend/api/users/resources.py
+++ b/backend/api/users/resources.py
@@ -46,11 +46,14 @@ def get(self, user_id):
)
return user_dto.to_primitive(), 200
except NotFound:
- return {"Error": "User not found"}, 404
+ return {"Error": "User not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Userid GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch user details"}, 500
+ return {
+ "Error": "Unable to fetch user details",
+ "SubCode": "InternalServerError",
+ }, 500
class UsersAllAPI(Resource):
@@ -105,7 +108,7 @@ def get(self):
query.validate()
except DataError as e:
current_app.logger.error(f"Error validating request: {str(e)}")
- return {"Error": "Unable to fetch user list"}, 400
+ return {"Error": "Unable to fetch user list", "SubCode": "InvalidData"}, 400
try:
users_dto = UserService.get_all_users(query)
@@ -113,7 +116,10 @@ def get(self):
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch user list"}, 500
+ return {
+ "Error": "Unable to fetch user list",
+ "SubCode": "InternalServerError",
+ }, 500
class UsersQueriesUsernameAPI(Resource):
@@ -155,11 +161,14 @@ def get(self, username):
)
return user_dto.to_primitive(), 200
except NotFound:
- return {"Error": "User not found"}, 404
+ return {"Error": "User not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch user details"}, 500
+ return {
+ "Error": "Unable to fetch user details",
+ "SubCode": "InternalServerError",
+ }, 500
class UsersQueriesUsernameFilterAPI(Resource):
@@ -208,11 +217,14 @@ def get(self, username):
users_dto = UserService.filter_users(username, project_id, page)
return users_dto.to_primitive(), 200
except NotFound:
- return {"Error": "User not found"}, 404
+ return {"Error": "User not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch matching users"}, 500
+ return {
+ "Error": "Unable to fetch matching users",
+ "SubCode": "InternalServerError",
+ }, 500
class UsersQueriesOwnLockedAPI(Resource):
@@ -250,7 +262,7 @@ def get(self):
except Exception as e:
error_msg = f"UsersQueriesOwnLockedAPI - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class UsersQueriesOwnLockedDetailsAPI(Resource):
@@ -293,11 +305,11 @@ def get(self):
)
return locked_tasks.to_primitive(), 200
except NotFound:
- return {"Error": "User has no locked tasks"}, 404
+ return {"Error": "User has no locked tasks", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"UsersQueriesOwnLockedDetailsAPI - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class UsersQueriesFavoritesAPI(Resource):
@@ -329,11 +341,11 @@ def get(self):
favs_dto = UserService.get_projects_favorited(token_auth.current_user())
return favs_dto.to_primitive(), 200
except NotFound:
- return {"Error": "User not found"}, 404
+ return {"Error": "User not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"UserFavorites GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class UsersQueriesInterestsAPI(Resource):
@@ -371,11 +383,11 @@ def get(self, username):
interests_dto = UserService.get_interests(user)
return interests_dto.to_primitive(), 200
except NotFound:
- return {"Error": "User not found"}, 404
+ return {"Error": "User not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"UserInterests GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class UsersRecommendedProjectsAPI(Resource):
@@ -428,8 +440,8 @@ def get(self, username):
user_dto = UserService.get_recommended_projects(username, locale)
return user_dto.to_primitive(), 200
except NotFound:
- return {"Error": "User or mapping not found"}, 404
+ return {"Error": "User or mapping not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/api/users/statistics.py b/backend/api/users/statistics.py
index f958447140..05a7dedb3a 100644
--- a/backend/api/users/statistics.py
+++ b/backend/api/users/statistics.py
@@ -46,11 +46,14 @@ def get(self, username):
stats_dto = UserService.get_detailed_stats(username)
return stats_dto.to_primitive(), 200
except NotFound:
- return {"Error": "User not found"}, 404
+ return {"Error": "User not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch user statistics"}, 500
+ return {
+ "Error": "Unable to fetch user statistics",
+ "SubCode": "InternalServerError",
+ }, 500
class UsersStatisticsInterestsAPI(Resource):
@@ -87,11 +90,11 @@ def get(self, user_id):
rate = InterestService.compute_contributions_rate(user_id)
return rate.to_primitive(), 200
except NotFound:
- return {"Error": "User not Found"}, 404
+ return {"Error": "User not Found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"Interest GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 500
+ return {"Error": error_msg, "SubCode": "InternalServerError"}, 500
class UsersStatisticsAllAPI(Resource):
@@ -134,19 +137,24 @@ def get(self):
start_date = validate_date_input(request.args.get("startDate"))
end_date = validate_date_input(request.args.get("endDate", date.today()))
if not (start_date):
- raise KeyError("Missing start date parameter")
+ raise KeyError("MissingDate- Missing start date parameter")
if end_date < start_date:
- raise ValueError("Start date must be earlier than end date")
+ raise ValueError(
+ "InvalidStartDate- Start date must be earlier than end date"
+ )
if (end_date - start_date) > timedelta(days=366 * 3):
- raise ValueError("Date range can not be bigger than 3 years")
+ raise ValueError(
+ "DateRangeGreaterThan3- Date range can not be bigger than 3 years"
+ )
stats = StatsService.get_all_users_statistics(start_date, end_date)
return stats.to_primitive(), 200
except (KeyError, ValueError) as e:
- error_msg = f"User Statistics GET - {str(e)}"
- current_app.logger.critical(error_msg)
- return {"Error": error_msg}, 400
+ return {"Error": str(e).split("-")[1], "SubCode": str(e).split("-")[0]}, 400
except Exception as e:
error_msg = f"User Statistics GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"Error": "Unable to fetch user stats"}, 500
+ return {
+ "Error": "Unable to fetch user stats",
+ "SubCode": "InternalServerError",
+ }, 500
diff --git a/backend/api/users/tasks.py b/backend/api/users/tasks.py
index 8f13d51e48..64af438feb 100644
--- a/backend/api/users/tasks.py
+++ b/backend/api/users/tasks.py
@@ -112,8 +112,8 @@ def get(self, user_id):
except ValueError:
return {"tasks": [], "pagination": {"total": 0}}, 200
except NotFound:
- return {"Error": "User or tasks not found"}, 404
+ return {"Error": "User or tasks not found", "SubCode": "NotFound"}, 404
except Exception as e:
error_msg = f"User GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
- return {"error": error_msg}, 500
+ return {"error": error_msg, "SubCode": "InternalServerError"}, 500
diff --git a/backend/models/dtos/project_dto.py b/backend/models/dtos/project_dto.py
index 0c88cf9091..ca22f5ddd2 100644
--- a/backend/models/dtos/project_dto.py
+++ b/backend/models/dtos/project_dto.py
@@ -79,7 +79,8 @@ def is_known_editor(value):
raise ValidationError(
f"Unknown editor: {value} Valid values are {Editors.ID.name}, "
f"{Editors.JOSM.name}, {Editors.POTLATCH_2.name}, "
- f"{Editors.FIELD_PAPERS.name}"
+ f"{Editors.FIELD_PAPERS.name}, "
+ f"{Editors.RAPID.name} "
)
@@ -206,6 +207,9 @@ class ProjectDTO(Model):
imagery = StringType()
josm_preset = StringType(serialized_name="josmPreset", serialize_when_none=False)
id_presets = ListType(StringType, serialized_name="idPresets", default=[])
+ rapid_power_user = BooleanType(
+ serialized_name="rapidPowerUser", default=False, required=False
+ )
mapping_types = ListType(
StringType,
serialized_name="mappingTypes",
@@ -500,6 +504,9 @@ class ProjectSummary(Model):
imagery = StringType()
license_id = IntType(serialized_name="licenseId")
id_presets = ListType(StringType, serialized_name="idPresets", default=[])
+ rapid_power_user = BooleanType(
+ serialized_name="rapidPowerUser", default=False, required=False
+ )
mapping_editors = ListType(
StringType,
min_size=1,
diff --git a/backend/models/postgis/project.py b/backend/models/postgis/project.py
index 77acf89611..ba2f25448b 100644
--- a/backend/models/postgis/project.py
+++ b/backend/models/postgis/project.py
@@ -93,16 +93,16 @@ class ProjectTeams(db.Model):
)
def create(self):
- """ Creates and saves the current model to the DB """
+ """Creates and saves the current model to the DB"""
db.session.add(self)
db.session.commit()
def save(self):
- """ Save changes to db"""
+ """Save changes to db"""
db.session.commit()
def delete(self):
- """ Deletes the current model from the DB """
+ """Deletes the current model from the DB"""
db.session.delete(self)
db.session.commit()
@@ -112,7 +112,7 @@ def delete(self):
class Project(db.Model):
- """ Describes a HOT Mapping Project """
+ """Describes a HOT Mapping Project"""
__tablename__ = "projects"
@@ -132,7 +132,7 @@ class Project(db.Model):
) # Mapper level project is suitable for
mapping_permission = db.Column(db.Integer, default=MappingPermission.ANY.value)
validation_permission = db.Column(
- db.Integer, default=ValidationPermission.ANY.value
+ db.Integer, default=ValidationPermission.LEVEL.value
) # Means only users with validator role can validate
enforce_random_task_selection = db.Column(
db.Boolean, default=False
@@ -149,6 +149,7 @@ class Project(db.Model):
imagery = db.Column(db.String)
josm_preset = db.Column(db.String)
id_presets = db.Column(ARRAY(db.String))
+ rapid_power_user = db.Column(db.Boolean, default=False)
last_updated = db.Column(db.DateTime, default=timestamp)
license_id = db.Column(db.Integer, db.ForeignKey("licenses.id", name="fk_licenses"))
geometry = db.Column(Geometry("MULTIPOLYGON", srid=4326), nullable=False)
@@ -234,7 +235,7 @@ def create_draft_project(self, draft_project_dto: DraftProjectDTO):
self.last_updated = timestamp()
def set_project_aoi(self, draft_project_dto: DraftProjectDTO):
- """ Sets the AOI for the supplied project """
+ """Sets the AOI for the supplied project"""
aoi_geojson = geojson.loads(json.dumps(draft_project_dto.area_of_interest))
aoi_geometry = GridService.merge_to_multi_polygon(aoi_geojson, dissolve=True)
@@ -244,7 +245,7 @@ def set_project_aoi(self, draft_project_dto: DraftProjectDTO):
self.centroid = ST_Centroid(self.geometry)
def set_default_changeset_comment(self):
- """ Sets the default changeset comment"""
+ """Sets the default changeset comment"""
default_comment = current_app.config["DEFAULT_CHANGESET_COMMENT"]
self.changeset_comment = (
f"{default_comment}-{self.id} {self.changeset_comment}"
@@ -254,7 +255,7 @@ def set_default_changeset_comment(self):
self.save()
def set_country_info(self):
- """ Sets the default country based on centroid"""
+ """Sets the default country based on centroid"""
centroid = to_shape(self.centroid)
lat, lng = (centroid.y, centroid.x)
@@ -271,17 +272,17 @@ def set_country_info(self):
self.save()
def create(self):
- """ Creates and saves the current model to the DB """
+ """Creates and saves the current model to the DB"""
db.session.add(self)
db.session.commit()
def save(self):
- """ Save changes to db"""
+ """Save changes to db"""
db.session.commit()
@staticmethod
def clone(project_id: int, author_id: int):
- """ Clone project """
+ """Clone project"""
orig = Project.query.get(project_id)
if orig is None:
@@ -359,7 +360,7 @@ def get(project_id: int):
).get(project_id)
def update(self, project_dto: ProjectDTO):
- """ Updates project from DTO """
+ """Updates project from DTO"""
self.status = ProjectStatus[project_dto.project_status].value
self.priority = ProjectPriority[project_dto.project_priority].value
if self.default_locale != project_dto.default_locale:
@@ -375,6 +376,7 @@ def update(self, project_dto: ProjectDTO):
self.imagery = project_dto.imagery
self.josm_preset = project_dto.josm_preset
self.id_presets = project_dto.id_presets
+ self.rapid_power_user = project_dto.rapid_power_user
self.last_updated = timestamp()
self.license_id = project_dto.license_id
@@ -497,7 +499,7 @@ def update(self, project_dto: ProjectDTO):
db.session.commit()
def delete(self):
- """ Deletes the current model from the DB """
+ """Deletes the current model from the DB"""
db.session.delete(self)
db.session.commit()
@@ -522,24 +524,24 @@ def favorite(self, user_id: int):
def unfavorite(self, user_id: int):
user = User.query.get(user_id)
if user not in self.favorited:
- raise ValueError("Project not been favorited by user")
+ raise ValueError("NotFeatured- Project not been favorited by user")
self.favorited.remove(user)
db.session.commit()
def set_as_featured(self):
if self.featured is True:
- raise ValueError("Project is already featured")
+ raise ValueError("AlreadyFeatured- Project is already featured")
self.featured = True
db.session.commit()
def unset_as_featured(self):
if self.featured is False:
- raise ValueError("Project is not featured")
+ raise ValueError("NotFeatured- Project is not featured")
self.featured = False
db.session.commit()
def can_be_deleted(self) -> bool:
- """ Projects can be deleted if they have no mapped work """
+ """Projects can be deleted if they have no mapped work"""
task_count = self.tasks.filter(
Task.task_status != TaskStatus.READY.value
).count()
@@ -552,7 +554,7 @@ def can_be_deleted(self) -> bool:
def get_projects_for_admin(
admin_id: int, preferred_locale: str, search_dto: ProjectSearchDTO
) -> PMDashboardDTO:
- """ Get projects for admin """
+ """Get projects for admin"""
query = Project.query.filter(Project.author_id == admin_id)
# Do Filtering Here
@@ -635,7 +637,7 @@ def get_project_user_stats(self, user_id: int) -> ProjectUserStatsDTO:
return stats_dto
def get_project_stats(self) -> ProjectStatsDTO:
- """ Create Project Stats model for postgis project object"""
+ """Create Project Stats model for postgis project object"""
project_stats = ProjectStatsDTO()
project_stats.project_id = self.id
project_area_sql = "select ST_Area(geometry, true)/1000000 as area from public.projects where id = :id"
@@ -811,7 +813,7 @@ def get_project_stats(self) -> ProjectStatsDTO:
return project_stats
def get_project_summary(self, preferred_locale) -> ProjectSummary:
- """ Create Project Summary model for postgis project object"""
+ """Create Project Summary model for postgis project object"""
summary = ProjectSummary()
summary.project_id = self.id
priority = self.priority
@@ -841,6 +843,7 @@ def get_project_summary(self, preferred_locale) -> ProjectSummary:
summary.license_id = self.license_id
summary.status = ProjectStatus(self.status).name
summary.id_presets = self.id_presets
+ summary.rapid_power_user = self.rapid_power_user
summary.imagery = self.imagery
if self.organisation_id:
summary.organisation = self.organisation_id
@@ -945,12 +948,12 @@ def get_project_total_contributions(project_id: int) -> int:
return project_contributors_count
def get_aoi_geometry_as_geojson(self):
- """ Helper which returns the AOI geometry as a geojson object """
+ """Helper which returns the AOI geometry as a geojson object"""
aoi_geojson = db.engine.execute(self.geometry.ST_AsGeoJSON()).scalar()
return geojson.loads(aoi_geojson)
def get_project_teams(self):
- """ Helper to return teams with members so we can handle permissions """
+ """Helper to return teams with members so we can handle permissions"""
project_teams = []
for t in self.teams:
project_teams.append(
@@ -966,7 +969,7 @@ def get_project_teams(self):
@staticmethod
@cached(active_mappers_cache)
def get_active_mappers(project_id) -> int:
- """ Get count of Locked tasks as a proxy for users who are currently active on the project """
+ """Get count of Locked tasks as a proxy for users who are currently active on the project"""
return (
Task.query.filter(
@@ -983,7 +986,7 @@ def get_active_mappers(project_id) -> int:
)
def _get_project_and_base_dto(self):
- """ Populates a project DTO with properties common to all roles """
+ """Populates a project DTO with properties common to all roles"""
base_dto = ProjectDTO()
base_dto.project_id = self.id
base_dto.project_status = ProjectStatus(self.status).name
@@ -1004,6 +1007,7 @@ def _get_project_and_base_dto(self):
base_dto.imagery = self.imagery
base_dto.josm_preset = self.josm_preset
base_dto.id_presets = self.id_presets
+ base_dto.rapid_power_user = self.rapid_power_user
base_dto.country_tag = self.country
base_dto.organisation_id = self.organisation_id
base_dto.license_id = self.license_id
@@ -1095,7 +1099,7 @@ def _get_project_and_base_dto(self):
def as_dto_for_mapping(
self, authenticated_user_id: int = None, locale: str = "en", abbrev: bool = True
) -> Optional[ProjectDTO]:
- """ Creates a Project DTO suitable for transmitting to mapper users """
+ """Creates a Project DTO suitable for transmitting to mapper users"""
project, project_dto = self._get_project_and_base_dto()
if abbrev is False:
project_dto.tasks = Task.get_tasks_as_geojson_feature_collection(
@@ -1120,7 +1124,7 @@ def as_dto_for_mapping(
def tasks_as_geojson(
self, task_ids_str: str, order_by=None, order_by_type="ASC", status=None
):
- """ Creates a geojson of all areas """
+ """Creates a geojson of all areas"""
project_tasks = Task.get_tasks_as_geojson_feature_collection(
self.id, task_ids_str, order_by, order_by_type, status
)
@@ -1143,7 +1147,7 @@ def get_all_countries():
def calculate_tasks_percent(
target, total_tasks, tasks_mapped, tasks_validated, tasks_bad_imagery
):
- """ Calculates percentages of contributions """
+ """Calculates percentages of contributions"""
try:
if target == "mapped":
return int(
@@ -1159,7 +1163,7 @@ def calculate_tasks_percent(
return 0
def as_dto_for_admin(self, project_id):
- """ Creates a Project DTO suitable for transmitting to project admins """
+ """Creates a Project DTO suitable for transmitting to project admins"""
project, project_dto = self._get_project_and_base_dto()
if project is None:
diff --git a/backend/models/postgis/statuses.py b/backend/models/postgis/statuses.py
index 0fc9cc8073..acd3c9f101 100644
--- a/backend/models/postgis/statuses.py
+++ b/backend/models/postgis/statuses.py
@@ -108,6 +108,7 @@ class Editors(Enum):
POTLATCH_2 = 2
FIELD_PAPERS = 3
CUSTOM = 4
+ RAPID = 5
class TeamVisibility(Enum):
diff --git a/backend/models/postgis/task.py b/backend/models/postgis/task.py
index 5ba62eb463..ca33fc16ad 100644
--- a/backend/models/postgis/task.py
+++ b/backend/models/postgis/task.py
@@ -34,7 +34,7 @@
class TaskAction(Enum):
- """ Describes the possible actions that can happen to to a task, that we'll record history for """
+ """Describes the possible actions that can happen to to a task, that we'll record history for"""
LOCKED_FOR_MAPPING = 1
LOCKED_FOR_VALIDATION = 2
@@ -45,7 +45,7 @@ class TaskAction(Enum):
class TaskInvalidationHistory(db.Model):
- """ Describes the most recent history of task invalidation and subsequent validation """
+ """Describes the most recent history of task invalidation and subsequent validation"""
__tablename__ = "task_invalidation_history"
id = db.Column(db.Integer, primary_key=True)
@@ -87,7 +87,7 @@ def __init__(self, project_id, task_id):
self.is_closed = False
def delete(self):
- """ Deletes the current model from the DB """
+ """Deletes the current model from the DB"""
db.session.delete(self)
db.session.commit()
@@ -162,7 +162,7 @@ def __init__(self, issue, count, mapping_issue_category_id, task_history_id=None
self.mapping_issue_category_id = mapping_issue_category_id
def delete(self):
- """ Deletes the current model from the DB """
+ """Deletes the current model from the DB"""
db.session.delete(self)
db.session.commit()
@@ -178,7 +178,7 @@ def __repr__(self):
class TaskHistory(db.Model):
- """ Describes the history associated with a task """
+ """Describes the history associated with a task"""
__tablename__ = "task_history"
@@ -239,7 +239,7 @@ def set_auto_unlock_action(self, task_action: TaskAction):
self.action = task_action.name
def delete(self):
- """ Deletes the current model from the DB """
+ """Deletes the current model from the DB"""
db.session.delete(self)
db.session.commit()
@@ -351,7 +351,7 @@ def update_expired_and_locked_actions(
@staticmethod
def get_all_comments(project_id: int) -> ProjectCommentsDTO:
- """ Gets all comments for the supplied project_id"""
+ """Gets all comments for the supplied project_id"""
comments = (
db.session.query(
@@ -381,7 +381,7 @@ def get_all_comments(project_id: int) -> ProjectCommentsDTO:
@staticmethod
def get_last_status(project_id: int, task_id: int, for_undo: bool = False):
- """ Get the status the task was set to the last time the task had a STATUS_CHANGE"""
+ """Get the status the task was set to the last time the task had a STATUS_CHANGE"""
result = (
db.session.query(TaskHistory.action_text)
.filter(
@@ -480,7 +480,7 @@ def get_last_mapped_action(project_id: int, task_id: int):
class Task(db.Model):
- """ Describes an individual mapping Task """
+ """Describes an individual mapping Task"""
__tablename__ = "tasks"
@@ -516,16 +516,16 @@ class Task(db.Model):
mapper = db.relationship(User, foreign_keys=[mapped_by])
def create(self):
- """ Creates and saves the current model to the DB """
+ """Creates and saves the current model to the DB"""
db.session.add(self)
db.session.commit()
def update(self):
- """ Updates the DB with the current state of the Task """
+ """Updates the DB with the current state of the Task"""
db.session.commit()
def delete(self):
- """ Deletes the current model from the DB """
+ """Deletes the current model from the DB"""
db.session.delete(self)
db.session.commit()
@@ -538,18 +538,16 @@ def from_geojson_feature(cls, task_id, task_feature):
:raises InvalidGeoJson, InvalidData
"""
if type(task_feature) is not geojson.Feature:
- raise InvalidGeoJson("Task: Invalid GeoJson should be a feature")
+ raise InvalidGeoJson("MustBeFeature- Invalid GeoJson should be a feature")
task_geometry = task_feature.geometry
if type(task_geometry) is not geojson.MultiPolygon:
- raise InvalidGeoJson("Task: Geometry must be a MultiPolygon")
+ raise InvalidGeoJson("MustBeMultiPloygon- Geometry must be a MultiPolygon")
is_valid_geojson = geojson.is_valid(task_geometry)
if is_valid_geojson["valid"] == "no":
- raise InvalidGeoJson(
- f"Task: Invalid MultiPolygon - {is_valid_geojson['message']}"
- )
+ raise InvalidGeoJson(f"InvalidMultiPolygon- {is_valid_geojson['message']}")
task = cls()
try:
@@ -558,7 +556,9 @@ def from_geojson_feature(cls, task_id, task_feature):
task.zoom = task_feature.properties["zoom"]
task.is_square = task_feature.properties["isSquare"]
except KeyError as e:
- raise InvalidData(f"Task: Expected property not found: {str(e)}")
+ raise InvalidData(
+ f"PropertyNotFound: Expected property not found: {str(e)}"
+ )
if "extra_properties" in task_feature.properties:
task.extra_properties = json.dumps(
@@ -585,14 +585,14 @@ def get(task_id: int, project_id: int):
@staticmethod
def get_tasks(project_id: int, task_ids: List[int]):
- """ Get all tasks that match supplied list """
+ """Get all tasks that match supplied list"""
return Task.query.filter(
Task.project_id == project_id, Task.id.in_(task_ids)
).all()
@staticmethod
def get_all_tasks(project_id: int):
- """ Get all tasks for a given project """
+ """Get all tasks for a given project"""
return Task.query.filter(Task.project_id == project_id).all()
@staticmethod
@@ -643,7 +643,7 @@ def auto_unlock_expired_tasks(self, expiry_date, lock_duration):
self.clear_lock()
def is_mappable(self):
- """ Determines if task in scope is in suitable state for mapping """
+ """Determines if task in scope is in suitable state for mapping"""
if TaskStatus(self.task_status) not in [
TaskStatus.READY,
TaskStatus.INVALIDATED,
@@ -744,7 +744,7 @@ def record_auto_unlock(self, lock_duration):
def unlock_task(
self, user_id, new_state=None, comment=None, undo=False, issues=None
):
- """ Unlock task and ensure duration task locked is saved in History """
+ """Unlock task and ensure duration task locked is saved in History"""
if comment:
self.set_task_history(
action=TaskAction.COMMENT,
@@ -803,7 +803,7 @@ def reset_lock(self, user_id, comment=None):
self.clear_lock()
def clear_lock(self):
- """ Resets to last status and removes current lock from a task """
+ """Resets to last status and removes current lock from a task"""
self.task_status = TaskHistory.get_last_status(self.project_id, self.id).value
self.locked_by = None
self.update()
@@ -944,7 +944,7 @@ def get_tasks_as_geojson_feature_collection_no_geom(project_id):
@staticmethod
def get_mapped_tasks_by_user(project_id: int):
- """ Gets all mapped tasks for supplied project grouped by user"""
+ """Gets all mapped tasks for supplied project grouped by user"""
results = (
db.session.query(
User.username,
@@ -1062,7 +1062,7 @@ def get_per_task_annotations(self):
return result
def get_per_task_instructions(self, search_locale: str) -> str:
- """ Gets any per task instructions attached to the project """
+ """Gets any per task instructions attached to the project"""
project_info = self.projects.project_info.all()
for info in project_info:
@@ -1070,7 +1070,7 @@ def get_per_task_instructions(self, search_locale: str) -> str:
return self.format_per_task_instructions(info.per_task_instructions)
def format_per_task_instructions(self, instructions) -> str:
- """ Format instructions by looking for X, Y, Z tokens and replacing them with the task values """
+ """Format instructions by looking for X, Y, Z tokens and replacing them with the task values"""
if not instructions:
return "" # No instructions so return empty string
@@ -1104,7 +1104,7 @@ def copy_task_history(self) -> list:
return copies
def get_locked_tasks_for_user(user_id: int):
- """ Gets tasks on project owned by specified user id"""
+ """Gets tasks on project owned by specified user id"""
tasks = Task.query.filter_by(locked_by=user_id)
tasks_dto = LockedTasksForUser()
for task in tasks:
@@ -1115,7 +1115,7 @@ def get_locked_tasks_for_user(user_id: int):
return tasks_dto
def get_locked_tasks_details_for_user(user_id: int):
- """ Gets tasks on project owned by specified user id"""
+ """Gets tasks on project owned by specified user id"""
tasks = Task.query.filter_by(locked_by=user_id)
locked_tasks = [task for task in tasks]
diff --git a/backend/services/campaign_service.py b/backend/services/campaign_service.py
index 28dbe460cf..df66fbb0c7 100644
--- a/backend/services/campaign_service.py
+++ b/backend/services/campaign_service.py
@@ -73,7 +73,7 @@ def get_project_campaigns_as_dto(project_id: int) -> CampaignListDTO:
@staticmethod
def delete_project_campaign(project_id: int, campaign_id: int):
- """ Delete campaign for a project"""
+ """Delete campaign for a project"""
campaign = Campaign.query.get(campaign_id)
project = Project.query.get(project_id)
project.campaign.remove(campaign)
@@ -83,14 +83,14 @@ def delete_project_campaign(project_id: int, campaign_id: int):
@staticmethod
def get_all_campaigns() -> CampaignListDTO:
- """ Returns a list of all campaigns """
+ """Returns a list of all campaigns"""
query = Campaign.query.order_by(Campaign.name).distinct()
return Campaign.campaign_list_as_dto(query)
@staticmethod
def create_campaign(campaign_dto: NewCampaignDTO):
- """ Creates a new campaign """
+ """Creates a new campaign"""
campaign = Campaign.from_dto(campaign_dto)
try:
campaign.create()
@@ -102,9 +102,9 @@ def create_campaign(campaign_dto: NewCampaignDTO):
except IntegrityError as e:
current_app.logger.info("Integrity error: {}".format(e.args[0]))
if isinstance(e.orig, UniqueViolation):
- raise ValueError("Campaign name already exists") from e
+ raise ValueError("NameExists- Campaign name already exists") from e
if isinstance(e.orig, NotNullViolation):
- raise ValueError("Campaign name cannot be null") from e
+ raise ValueError("NullName- Campaign name cannot be null") from e
return campaign
@staticmethod
@@ -120,7 +120,7 @@ def create_campaign_project(dto: CampaignProjectDTO):
@staticmethod
def create_campaign_organisation(organisation_id: int, campaign_id: int):
- """ Creates new campaign from DTO """
+ """Creates new campaign from DTO"""
statement = campaign_organisations.insert().values(
campaign_id=campaign_id, organisation_id=organisation_id
)
@@ -133,7 +133,7 @@ def create_campaign_organisation(organisation_id: int, campaign_id: int):
@staticmethod
def get_organisation_campaigns_as_dto(organisation_id: int) -> CampaignListDTO:
- """ Gets all the campaigns for a specified project """
+ """Gets all the campaigns for a specified project"""
query = (
Campaign.query.join(campaign_organisations)
.filter(campaign_organisations.c.organisation_id == organisation_id)
@@ -154,7 +154,7 @@ def campaign_organisation_exists(campaign_id: int, org_id: int):
@staticmethod
def delete_organisation_campaign(organisation_id: int, campaign_id: int):
- """ Delete campaign for a organisation"""
+ """Delete campaign for a organisation"""
campaign = Campaign.query.get(campaign_id)
org = Organisation.query.get(organisation_id)
try:
diff --git a/backend/services/grid/grid_service.py b/backend/services/grid/grid_service.py
index bef62c1c44..e7880f19e2 100644
--- a/backend/services/grid/grid_service.py
+++ b/backend/services/grid/grid_service.py
@@ -9,7 +9,7 @@
class GridServiceError(Exception):
- """ Custom Exception to notify callers an error occurred when handling projects """
+ """Custom Exception to notify callers an error occurred when handling projects"""
def __init__(self, message):
if current_app:
@@ -115,12 +115,14 @@ def merge_to_multi_polygon(
# validate the geometry
if type(aoi_multi_polygon_geojson) is not geojson.MultiPolygon:
- raise InvalidGeoJson("Area Of Interest: geometry must be a MultiPolygon")
+ raise InvalidGeoJson(
+ "MustBeMultiPloygon- Area Of Interest: geometry must be a MultiPolygon"
+ )
is_valid_geojson = geojson.is_valid(aoi_multi_polygon_geojson)
if is_valid_geojson["valid"] == "no":
raise InvalidGeoJson(
- f"Area of Interest: Invalid MultiPolygon - {is_valid_geojson['message']}"
+ f"InvalidMultipolygon- Area of Interest: Invalid MultiPolygon - {is_valid_geojson['message']}"
)
return aoi_multi_polygon_geojson
@@ -154,7 +156,9 @@ def _to_shapely_geometries(input: str) -> list:
collection = geojson.loads(input, object_hook=geojson.GeoJSON.to_instance)
if not hasattr(collection, "features") or len(collection.features) < 1:
- raise InvalidGeoJson("Geojson does not contain any features")
+ raise InvalidGeoJson(
+ "MustHaveFeatures- Geojson does not contain any features"
+ )
shapely_features = list(
(
diff --git a/backend/services/grid/split_service.py b/backend/services/grid/split_service.py
index dac6346e0a..fa2557a74d 100644
--- a/backend/services/grid/split_service.py
+++ b/backend/services/grid/split_service.py
@@ -13,7 +13,7 @@
class SplitServiceError(Exception):
- """ Custom Exception to notify callers an error occurred when handling splitting tasks """
+ """Custom Exception to notify callers an error occurred when handling splitting tasks"""
def __init__(self, message):
if current_app:
@@ -179,14 +179,18 @@ def split_task(split_task_dto: SplitTaskDTO) -> TaskDTOs:
if (
original_task.zoom and original_task.zoom >= 18
) or original_task_area_m < 25000:
- raise SplitServiceError("Task is too small to be split")
+ raise SplitServiceError("SmallToSplit- Task is too small to be split")
# check its locked for mapping by the current user
if TaskStatus(original_task.task_status) != TaskStatus.LOCKED_FOR_MAPPING:
- raise SplitServiceError("Status must be LOCKED_FOR_MAPPING to split")
+ raise SplitServiceError(
+ "LockToSplit- Status must be LOCKED_FOR_MAPPING to split"
+ )
if original_task.locked_by != split_task_dto.user_id:
- raise SplitServiceError("Attempting to split a task owned by another user")
+ raise SplitServiceError(
+ "SplitOtherUserTask- Attempting to split a task owned by another user"
+ )
# create new geometries from the task geometry
try:
@@ -204,7 +208,9 @@ def split_task(split_task_dto: SplitTaskDTO) -> TaskDTOs:
# Sanity check: ensure the new task geometry intersects the original task geometry
new_geometry = shapely_shape(new_task_geojson.geometry)
if not new_geometry.intersects(original_geometry):
- raise InvalidGeoJson("New split task does not intersect original task")
+ raise InvalidGeoJson(
+ "SplitGeoJsonError- New split task does not intersect original task"
+ )
# insert new tasks into database
i = i + 1
diff --git a/backend/services/mapping_service.py b/backend/services/mapping_service.py
index 11c97e2e42..f15099ac0d 100644
--- a/backend/services/mapping_service.py
+++ b/backend/services/mapping_service.py
@@ -20,7 +20,7 @@
class MappingServiceError(Exception):
- """ Custom Exception to notify callers an error occurred when handling mapping """
+ """Custom Exception to notify callers an error occurred when handling mapping"""
def __init__(self, message):
if current_app:
@@ -47,14 +47,14 @@ def get_task_as_dto(
project_id: int,
preferred_local: str = "en",
) -> TaskDTO:
- """ Get task as DTO for transmission over API """
+ """Get task as DTO for transmission over API"""
task = MappingService.get_task(task_id, project_id)
task_dto = task.as_dto_with_instructions(preferred_local)
return task_dto
@staticmethod
def _is_task_undoable(logged_in_user_id: int, task: Task) -> bool:
- """ Determines if the current task status can be undone by the logged in user """
+ """Determines if the current task status can be undone by the logged in user"""
# Test to see if user can undo status on this task
if logged_in_user_id and TaskStatus(task.task_status) not in [
TaskStatus.LOCKED_FOR_MAPPING,
@@ -85,7 +85,9 @@ def lock_task_for_mapping(lock_task_dto: LockTaskDTO) -> TaskDTO:
task = MappingService.get_task(lock_task_dto.task_id, lock_task_dto.project_id)
if not task.is_mappable():
- raise MappingServiceError("Task in invalid state for mapping")
+ raise MappingServiceError(
+ "InvalidTaskState- Task in invalid state for mapping"
+ )
user_can_map, error_reason = ProjectService.is_user_permitted_to_map(
lock_task_dto.project_id, lock_task_dto.user_id
@@ -93,9 +95,19 @@ def lock_task_for_mapping(lock_task_dto: LockTaskDTO) -> TaskDTO:
if not user_can_map:
if error_reason == MappingNotAllowed.USER_NOT_ACCEPTED_LICENSE:
raise UserLicenseError("User must accept license to map this task")
+ elif error_reason == MappingNotAllowed.USER_NOT_ON_ALLOWED_LIST:
+ raise MappingServiceError("UserNotAllowed- User not on allowed list")
+ elif error_reason == MappingNotAllowed.PROJECT_NOT_PUBLISHED:
+ raise MappingServiceError(
+ "ProjectNotPublished- Project is not published"
+ )
+ elif error_reason == MappingNotAllowed.USER_ALREADY_HAS_TASK_LOCKED:
+ raise MappingServiceError(
+ "UserAlreadyHasTaskLocked- User already has task locked"
+ )
else:
raise MappingServiceError(
- f"Mapping not allowed because: {error_reason}"
+ f"{error_reason}- Mapping not allowed because: {error_reason}"
)
task.lock_task_for_mapping(lock_task_dto.user_id)
@@ -103,7 +115,7 @@ def lock_task_for_mapping(lock_task_dto: LockTaskDTO) -> TaskDTO:
@staticmethod
def unlock_task_after_mapping(mapped_task: MappedTaskDTO) -> TaskDTO:
- """ Unlocks the task and sets the task history appropriately """
+ """Unlocks the task and sets the task history appropriately"""
task = MappingService.get_task_locked_by_user(
mapped_task.project_id, mapped_task.task_id, mapped_task.user_id
)
@@ -116,7 +128,7 @@ def unlock_task_after_mapping(mapped_task: MappedTaskDTO) -> TaskDTO:
TaskStatus.READY,
]:
raise MappingServiceError(
- "Can only set status to MAPPED, BADIMAGERY, READY after mapping"
+ "InvalidUnlockState- Can only set status to MAPPED, BADIMAGERY, READY after mapping"
)
# Update stats around the change of state
@@ -142,7 +154,7 @@ def unlock_task_after_mapping(mapped_task: MappedTaskDTO) -> TaskDTO:
@staticmethod
def stop_mapping_task(stop_task: StopMappingTaskDTO) -> TaskDTO:
- """ Unlocks the task and revert the task status to the last one """
+ """Unlocks the task and revert the task status to the last one"""
task = MappingService.get_task_locked_by_user(
stop_task.project_id, stop_task.task_id, stop_task.user_id
)
@@ -164,22 +176,24 @@ def get_task_locked_by_user(project_id: int, task_id: int, user_id: int) -> Task
"""
task = MappingService.get_task(task_id, project_id)
if task is None:
- raise MappingServiceError(f"Task {task_id} not found")
+ raise NotFound(f"Task {task_id} not found")
current_state = TaskStatus(task.task_status)
if current_state != TaskStatus.LOCKED_FOR_MAPPING:
- raise MappingServiceError("Status must be LOCKED_FOR_MAPPING to unlock")
+ raise MappingServiceError(
+ "LockBeforeUnlocking- Status must be LOCKED_FOR_MAPPING to unlock"
+ )
if task.locked_by != user_id:
raise MappingServiceError(
- "Attempting to unlock a task owned by another user"
+ "TaskNotOwned- Attempting to unlock a task owned by another user"
)
return task
@staticmethod
def add_task_comment(task_comment: TaskCommentDTO) -> TaskDTO:
- """ Adds the comment to the task history """
+ """Adds the comment to the task history"""
task = Task.get(task_comment.task_id, task_comment.project_id)
if task is None:
- raise MappingServiceError(f"Task {task_comment.task_id} not found")
+ raise NotFound(f"Task {task_comment.task_id} not found")
task.set_task_history(
TaskAction.COMMENT, task_comment.user_id, task_comment.comment
@@ -311,11 +325,13 @@ def generate_osm_xml(project_id: int, task_ids_str: str) -> str:
def undo_mapping(
project_id: int, task_id: int, user_id: int, preferred_locale: str = "en"
) -> TaskDTO:
- """ Allows a user to Undo the task state they updated """
+ """Allows a user to Undo the task state they updated"""
task = MappingService.get_task(task_id, project_id)
if not MappingService._is_task_undoable(user_id, task):
- raise MappingServiceError("Undo not allowed for this user")
+ raise MappingServiceError(
+ "UndoPermissionError- Undo not allowed for this user"
+ )
current_state = TaskStatus(task.task_status)
undo_state = TaskHistory.get_last_status(project_id, task_id, True)
@@ -338,7 +354,7 @@ def undo_mapping(
@staticmethod
def map_all_tasks(project_id: int, user_id: int):
- """ Marks all tasks on a project as mapped """
+ """Marks all tasks on a project as mapped"""
tasks_to_map = Task.query.filter(
Task.project_id == project_id,
Task.task_status.notin_(
@@ -367,9 +383,10 @@ def map_all_tasks(project_id: int, user_id: int):
@staticmethod
def reset_all_badimagery(project_id: int, user_id: int):
- """ Marks all bad imagery tasks ready for mapping """
+ """Marks all bad imagery tasks ready for mapping"""
badimagery_tasks = Task.query.filter(
- Task.task_status == TaskStatus.BADIMAGERY.value
+ Task.task_status == TaskStatus.BADIMAGERY.value,
+ Task.project_id == project_id,
).all()
for task in badimagery_tasks:
diff --git a/backend/services/messaging/chat_service.py b/backend/services/messaging/chat_service.py
index 1d50dcf230..ac99a45020 100644
--- a/backend/services/messaging/chat_service.py
+++ b/backend/services/messaging/chat_service.py
@@ -18,7 +18,7 @@ class ChatService:
def post_message(
chat_dto: ChatMessageDTO, project_id: int, authenticated_user_id: int
) -> ProjectChatDTO:
- """ Save message to DB and return latest chat"""
+ """Save message to DB and return latest chat"""
current_app.logger.debug("Posting Chat Message")
project = ProjectService.get_project_by_id(project_id)
@@ -33,7 +33,7 @@ def post_message(
ProjectStatus(project.status) == ProjectStatus.DRAFT
and not is_manager_permission
):
- raise ValueError("User not permitted to post Comment")
+ raise ValueError("UserNotPermitted- User not permitted to post Comment")
if project.private:
is_allowed_user = False
@@ -68,9 +68,9 @@ def post_message(
# Ensure we return latest messages after post
return ProjectChat.get_messages(chat_dto.project_id, 1, 5)
else:
- raise ValueError("User not permitted to post Comment")
+ raise ValueError("UserNotPermitted- User not permitted to post Comment")
@staticmethod
def get_messages(project_id: int, page: int, per_page: int) -> ProjectChatDTO:
- """ Get all messages attached to a project """
+ """Get all messages attached to a project"""
return ProjectChat.get_messages(project_id, page, per_page)
diff --git a/backend/services/messaging/message_service.py b/backend/services/messaging/message_service.py
index 465a4dd0b9..1df242c5cf 100644
--- a/backend/services/messaging/message_service.py
+++ b/backend/services/messaging/message_service.py
@@ -29,7 +29,7 @@
class MessageServiceError(Exception):
- """ Custom Exception to notify callers an error occurred when handling mapping """
+ """Custom Exception to notify callers an error occurred when handling mapping"""
def __init__(self, message):
if current_app:
@@ -65,7 +65,7 @@ def send_welcome_message(user: User):
def send_message_after_validation(
status: int, validated_by: int, mapped_by: int, task_id: int, project_id: int
):
- """ Sends mapper a notification after their task has been marked valid or invalid """
+ """Sends mapper a notification after their task has been marked valid or invalid"""
if validated_by == mapped_by:
return # No need to send a notification if you've verified your own task
@@ -202,7 +202,7 @@ def _push_messages(messages):
def send_message_after_comment(
comment_from: int, comment: str, task_id: int, project_id: int
):
- """ Will send a canned message to anyone @'d in a comment """
+ """Will send a canned message to anyone @'d in a comment"""
usernames = MessageService._parse_message_for_username(comment, project_id)
if len(usernames) != 0:
task_link = MessageService.get_task_link(project_id, task_id)
@@ -376,7 +376,7 @@ def send_invite_to_join_team(
@staticmethod
def send_message_after_chat(chat_from: int, chat: str, project_id: int):
- """ Send alert to user if they were @'d in a chat message """
+ """Send alert to user if they were @'d in a chat message"""
# Because message-all run on background thread it needs it's own app context
app = create_app()
with app.app_context():
@@ -506,7 +506,7 @@ def send_favorite_project_activities(user_id: int):
@staticmethod
def resend_email_validation(user_id: int):
- """ Resends the email validation email to the logged in user """
+ """Resends the email validation email to the logged in user"""
user = UserService.get_user_by_id(user_id)
SMTPService.send_verification_email(user.email_address, user.username)
@@ -540,7 +540,7 @@ def _get_managers(message: str, project_id: int) -> List[str]:
@staticmethod
def _parse_message_for_username(message: str, project_id: int) -> List[str]:
- """ Extracts all usernames from a comment looks for format @[user name] """
+ """Extracts all usernames from a comment looks for format @[user name]"""
parser = re.compile(r"((?<=@)\w+|\[.+?\])")
@@ -559,7 +559,7 @@ def _parse_message_for_username(message: str, project_id: int) -> List[str]:
@staticmethod
@cached(message_cache)
def has_user_new_messages(user_id: int) -> dict:
- """ Determines if the user has any unread messages """
+ """Determines if the user has any unread messages"""
count = Notification.get_unread_message_count(user_id)
new_messages = False
@@ -582,7 +582,7 @@ def get_all_messages(
task_id=None,
status=None,
):
- """ Get all messages for user """
+ """Get all messages for user"""
sort_column = Message.__table__.columns.get(sort_by)
if sort_column is None:
sort_column = Message.date
@@ -634,7 +634,7 @@ def get_all_messages(
@staticmethod
def get_message(message_id: int, user_id: int) -> Message:
- """ Gets the specified message """
+ """Gets the specified message"""
message = Message.query.get(message_id)
if message is None:
@@ -642,32 +642,33 @@ def get_message(message_id: int, user_id: int) -> Message:
if message.to_user_id != int(user_id):
raise MessageServiceError(
- f"User {user_id} attempting to access another users message {message_id}"
+ "AccessOtherUserMessage- "
+ + f"User {user_id} attempting to access another users message {message_id}"
)
return message
@staticmethod
def get_message_as_dto(message_id: int, user_id: int):
- """ Gets the selected message and marks it as read """
+ """Gets the selected message and marks it as read"""
message = MessageService.get_message(message_id, user_id)
message.mark_as_read()
return message.as_dto()
@staticmethod
def delete_message(message_id: int, user_id: int):
- """ Deletes the specified message """
+ """Deletes the specified message"""
message = MessageService.get_message(message_id, user_id)
message.delete()
@staticmethod
def delete_multiple_messages(message_ids: list, user_id: int):
- """ Deletes the specified messages to the user """
+ """Deletes the specified messages to the user"""
Message.delete_multiple_messages(message_ids, user_id)
@staticmethod
def get_task_link(project_id: int, task_id: int, base_url=None) -> str:
- """ Helper method that generates a link to the task """
+ """Helper method that generates a link to the task"""
if not base_url:
base_url = current_app.config["APP_BASE_URL"]
@@ -677,7 +678,7 @@ def get_task_link(project_id: int, task_id: int, base_url=None) -> str:
def get_project_link(
project_id: int, base_url=None, include_chat_section=False
) -> str:
- """ Helper method to generate a link to project chat"""
+ """Helper method to generate a link to project chat"""
if not base_url:
base_url = current_app.config["APP_BASE_URL"]
if include_chat_section:
@@ -689,7 +690,7 @@ def get_project_link(
@staticmethod
def get_user_profile_link(user_name: str, base_url=None) -> str:
- """ Helper method to generate a link to a user profile"""
+ """Helper method to generate a link to a user profile"""
if not base_url:
base_url = current_app.config["APP_BASE_URL"]
@@ -697,7 +698,7 @@ def get_user_profile_link(user_name: str, base_url=None) -> str:
@staticmethod
def get_user_settings_link(section=None, base_url=None) -> str:
- """ Helper method to generate a link to a user profile"""
+ """Helper method to generate a link to a user profile"""
if not base_url:
base_url = current_app.config["APP_BASE_URL"]
diff --git a/backend/services/organisation_service.py b/backend/services/organisation_service.py
index e67a36c20b..7acbf80815 100644
--- a/backend/services/organisation_service.py
+++ b/backend/services/organisation_service.py
@@ -23,7 +23,7 @@
class OrganisationServiceError(Exception):
- """ Custom Exception to notify callers an error occurred when handling organisations """
+ """Custom Exception to notify callers an error occurred when handling organisations"""
def __init__(self, message):
if current_app:
@@ -97,7 +97,7 @@ def create_organisation(new_organisation_dto: NewOrganisationDTO) -> int:
return org.id
except IntegrityError:
raise OrganisationServiceError(
- f"Organisation name already exists: {new_organisation_dto.name}"
+ f"NameExists- Organisation name already exists: {new_organisation_dto.name}"
)
@staticmethod
@@ -117,7 +117,7 @@ def update_organisation(organisation_dto: UpdateOrganisationDTO) -> Organisation
@staticmethod
def delete_organisation(organisation_id: int):
- """ Deletes an organisation if it has no projects """
+ """Deletes an organisation if it has no projects"""
org = OrganisationService.get_organisation_by_id(organisation_id)
if org.can_be_deleted():
@@ -130,7 +130,7 @@ def delete_organisation(organisation_id: int):
@staticmethod
def get_organisations(manager_user_id: int):
if manager_user_id is None:
- """ Get all organisations """
+ """Get all organisations"""
return Organisation.get_all_organisations()
else:
return Organisation.get_organisations_managed_by_user(manager_user_id)
@@ -151,7 +151,7 @@ def get_organisations_as_dto(
@staticmethod
def get_organisations_managed_by_user(user_id: int):
- """ Get all organisations a user manages """
+ """Get all organisations a user manages"""
if UserService.is_user_an_admin(user_id):
return Organisation.get_all_organisations()
@@ -232,15 +232,19 @@ def get_organisation_stats(organisation_id: int) -> OrganizationStatsDTO:
@staticmethod
def assert_validate_name(org: Organisation, name: str):
- """ Validates that the organisation name doesn't exist """
+ """Validates that the organisation name doesn't exist"""
if org.name != name and Organisation.get_organisation_by_name(name) is not None:
- raise OrganisationServiceError(f"Organisation name already exists: {name}")
+ raise OrganisationServiceError(
+ f"NameExists- Organisation name already exists: {name}"
+ )
@staticmethod
def assert_validate_users(organisation_dto: OrganisationDTO):
- """ Validates that the users exist"""
+ """Validates that the users exist"""
if organisation_dto.managers and len(organisation_dto.managers) == 0:
- raise OrganisationServiceError("Must have at least one admin")
+ raise OrganisationServiceError(
+ "MustHaveAdmin- Must have at least one admin"
+ )
if organisation_dto.managers and len(organisation_dto.managers) > 0:
managers = []
@@ -256,7 +260,7 @@ def assert_validate_users(organisation_dto: OrganisationDTO):
@staticmethod
def can_user_manage_organisation(organisation_id: int, user_id: int):
- """ Check that the user is an admin for the org or a global admin"""
+ """Check that the user is an admin for the org or a global admin"""
if UserService.is_user_an_admin(user_id):
return True
else:
@@ -264,7 +268,7 @@ def can_user_manage_organisation(organisation_id: int, user_id: int):
@staticmethod
def is_user_an_org_manager(organisation_id: int, user_id: int):
- """ Check that the user is an manager for the org """
+ """Check that the user is an manager for the org"""
org = Organisation.get(organisation_id)
diff --git a/backend/services/project_admin_service.py b/backend/services/project_admin_service.py
index f4bc7dfecf..497bd2be2e 100644
--- a/backend/services/project_admin_service.py
+++ b/backend/services/project_admin_service.py
@@ -21,7 +21,7 @@
class ProjectAdminServiceError(Exception):
- """ Custom Exception to notify callers an error occurred when validating a Project """
+ """Custom Exception to notify callers an error occurred when validating a Project"""
def __init__(self, message):
if current_app:
@@ -29,7 +29,7 @@ def __init__(self, message):
class ProjectStoreError(Exception):
- """ Custom Exception to notify callers an error occurred with database CRUD operations """
+ """Custom Exception to notify callers an error occurred with database CRUD operations"""
def __init__(self, message):
if current_app:
@@ -57,7 +57,7 @@ def create_draft_project(draft_project_dto: DraftProjectDTO) -> int:
user = UserService.get_user_by_id(user_id)
raise (
ProjectAdminServiceError(
- f"User {user.username} is not permitted to create project"
+ f"NotPermittedToCreate- User {user.username} is not permitted to create project"
)
)
@@ -97,7 +97,7 @@ def create_draft_project(draft_project_dto: DraftProjectDTO) -> int:
@staticmethod
def _set_default_changeset_comment(draft_project: Project):
- """ Sets the default changesset comment when project created """
+ """Sets the default changesset comment when project created"""
default_comment = current_app.config["DEFAULT_CHANGESET_COMMENT"]
draft_project.changeset_comment = f"{default_comment}-{draft_project.id}"
draft_project.save()
@@ -113,7 +113,7 @@ def _get_project_by_id(project_id: int) -> Project:
@staticmethod
def get_project_dto_for_admin(project_id: int) -> ProjectDTO:
- """ Get the project as DTO for project managers """
+ """Get the project as DTO for project managers"""
project = ProjectAdminService._get_project_by_id(project_id)
return project.as_dto_for_admin(project_id)
@@ -129,6 +129,7 @@ def update_project(project_dto: ProjectDTO, authenticated_user_id: int):
if project_dto.license_id:
ProjectAdminService._validate_imagery_licence(project_dto.license_id)
+ # To be handled before reaching this function
if ProjectAdminService.is_user_action_permitted_on_project(
authenticated_user_id, project_id
):
@@ -144,15 +145,17 @@ def update_project(project_dto: ProjectDTO, authenticated_user_id: int):
@staticmethod
def _validate_imagery_licence(license_id: int):
- """ Ensures that the suppliced license Id actually exists """
+ """Ensures that the suppliced license Id actually exists"""
try:
LicenseService.get_license_as_dto(license_id)
except NotFound:
- raise ProjectAdminServiceError(f"LicenseId {license_id} not found")
+ raise ProjectAdminServiceError(
+ f"RequireLicenseId- LicenseId {license_id} not found"
+ )
@staticmethod
def delete_project(project_id: int, authenticated_user_id: int):
- """ Deletes project if it has no completed tasks """
+ """Deletes project if it has no completed tasks"""
project = ProjectAdminService._get_project_by_id(project_id)
is_admin = UserService.is_user_an_admin(authenticated_user_id)
@@ -166,16 +169,16 @@ def delete_project(project_id: int, authenticated_user_id: int):
project.delete()
else:
raise ProjectAdminServiceError(
- "Project has mapped tasks, cannot be deleted"
+ "HasMappedTasks- Project has mapped tasks, cannot be deleted"
)
else:
raise ProjectAdminServiceError(
- "User does not have permissions to delete project"
+ "DeletePermissionError- User does not have permissions to delete project"
)
@staticmethod
def reset_all_tasks(project_id: int, user_id: int):
- """ Resets all tasks on project, preserving history"""
+ """Resets all tasks on project, preserving history"""
tasks_to_reset = Task.query.filter(Task.project_id == project_id).all()
for task in tasks_to_reset:
@@ -193,7 +196,7 @@ def reset_all_tasks(project_id: int, user_id: int):
@staticmethod
def get_all_comments(project_id: int) -> ProjectCommentsDTO:
- """ Gets all comments mappers, validators have added to tasks associated with project """
+ """Gets all comments mappers, validators have added to tasks associated with project"""
comments = TaskHistory.get_all_comments(project_id)
if len(comments.comments) == 0:
@@ -212,12 +215,14 @@ def _attach_tasks_to_project(draft_project: Project, tasks_geojson):
tasks = geojson.loads(json.dumps(tasks_geojson))
if type(tasks) is not geojson.FeatureCollection:
- raise InvalidGeoJson("Tasks: Invalid GeoJson must be FeatureCollection")
+ raise InvalidGeoJson(
+ "MustBeFeatureCollection- Invalid: GeoJson must be FeatureCollection"
+ )
is_valid_geojson = geojson.is_valid(tasks)
if is_valid_geojson["valid"] == "no":
raise InvalidGeoJson(
- f"Tasks: Invalid FeatureCollection - {is_valid_geojson['message']}"
+ f"InvalidFeatureCollection- {is_valid_geojson['message']}"
)
task_count = 1
@@ -250,7 +255,7 @@ def _validate_default_locale(default_locale, project_info_locales):
if default_info is None:
raise ProjectAdminServiceError(
- "Project Info for Default Locale not provided"
+ "InfoForLocaleRequired- Project Info for Default Locale not provided"
)
for attr, value in default_info.items():
@@ -259,7 +264,9 @@ def _validate_default_locale(default_locale, project_info_locales):
if not value:
raise (
- ProjectAdminServiceError(f"{attr} not provided for Default Locale")
+ ProjectAdminServiceError(
+ f"MissingRequiredAttribute- {attr} not provided for Default Locale"
+ )
)
return True # Indicates valid default locale for unit testing
@@ -268,26 +275,28 @@ def _validate_default_locale(default_locale, project_info_locales):
def get_projects_for_admin(
admin_id: int, preferred_locale: str, search_dto: ProjectSearchDTO
):
- """ Get all projects for provided admin """
+ """Get all projects for provided admin"""
return Project.get_projects_for_admin(admin_id, preferred_locale, search_dto)
@staticmethod
def transfer_project_to(project_id: int, transfering_user_id: int, username: str):
- """ Transfers project from old owner (transfering_user_id) to new owner (username) """
+ """Transfers project from old owner (transfering_user_id) to new owner (username)"""
project = Project.get(project_id)
# Check permissions for the user (transferring_user_id) who initiatied the action
if not ProjectAdminService.is_user_action_permitted_on_project(
transfering_user_id, project_id
):
- raise ValueError("User action not permitted")
+ raise ValueError("UserNotPermitted- User action not permitted")
new_owner = UserService.get_user_by_username(username)
# Check permissions for the new owner - must be an admin or project's org manager or a PM team member
if not ProjectAdminService.is_user_action_permitted_on_project(
new_owner.id, project_id
):
- raise ValueError("User action not permitted")
+ raise ValueError(
+ "InvalidNewOwner- New owner must be an admin or project's org manager or a PM team member"
+ )
else:
project.save()
@@ -295,7 +304,7 @@ def transfer_project_to(project_id: int, transfering_user_id: int, username: str
def is_user_action_permitted_on_project(
authenticated_user_id: int, project_id: int
) -> bool:
- """ Is user action permitted on project"""
+ """Is user action permitted on project"""
project = Project.get(project_id)
author_id = project.author_id
allowed_roles = [TeamRoles.PROJECT_MANAGER.value]
diff --git a/backend/services/project_search_service.py b/backend/services/project_search_service.py
index 445d44712e..839854b4be 100644
--- a/backend/services/project_search_service.py
+++ b/backend/services/project_search_service.py
@@ -48,7 +48,7 @@
class ProjectSearchServiceError(Exception):
- """ Custom Exception to notify callers an error occurred when handling mapping """
+ """Custom Exception to notify callers an error occurred when handling mapping"""
def __init__(self, message):
if current_app:
@@ -56,7 +56,7 @@ def __init__(self, message):
class BBoxTooBigError(Exception):
- """ Custom Exception to notify callers an error occurred when handling mapping """
+ """Custom Exception to notify callers an error occurred when handling mapping"""
def __init__(self, message):
if current_app:
@@ -178,7 +178,7 @@ def get_total_contributions(paginated_results):
@staticmethod
@cached(search_cache)
def search_projects(search_dto: ProjectSearchDTO, user) -> ProjectSearchResultsDTO:
- """ Searches all projects for matches to the criteria provided by the user """
+ """Searches all projects for matches to the criteria provided by the user"""
all_results, paginated_results = ProjectSearchService._filter_projects(
search_dto, user
)
@@ -217,7 +217,7 @@ def search_projects(search_dto: ProjectSearchDTO, user) -> ProjectSearchResultsD
@staticmethod
def _filter_projects(search_dto: ProjectSearchDTO, user):
- """ Filters all projects based on criteria provided by user"""
+ """Filters all projects based on criteria provided by user"""
query = ProjectSearchService.create_search_query(user)
@@ -472,7 +472,9 @@ def get_projects_geojson(
# validate the bbox area is less than or equal to the max area allowed to prevent
# abuse of the api or performance issues from large requests
if not ProjectSearchService.validate_bbox_area(polygon):
- raise BBoxTooBigError("Requested bounding box is too large")
+ raise BBoxTooBigError(
+ "BBoxTooBigError- Requested bounding box is too large"
+ )
# get projects intersecting the polygon for created by the author_id
intersecting_projects = ProjectSearchService._get_intersecting_projects(
@@ -504,7 +506,7 @@ def get_projects_geojson(
@staticmethod
def _get_intersecting_projects(search_polygon: Polygon, author_id: int):
- """Executes a database query to get the intersecting projects created by the author if provided """
+ """Executes a database query to get the intersecting projects created by the author if provided"""
query = db.session.query(
Project.id,
@@ -531,7 +533,7 @@ def _get_intersecting_projects(search_polygon: Polygon, author_id: int):
@staticmethod
def _make_4326_polygon_from_bbox(bbox: list, srid: int) -> Polygon:
- """ make a shapely Polygon in SRID 4326 from bbox and srid"""
+ """make a shapely Polygon in SRID 4326 from bbox and srid"""
try:
polygon = box(bbox[0], bbox[1], bbox[2], bbox[3])
if not srid == 4326:
@@ -544,13 +546,13 @@ def _make_4326_polygon_from_bbox(bbox: list, srid: int) -> Polygon:
@staticmethod
def _get_area_sqm(polygon: Polygon) -> float:
- """ get the area of the polygon in square metres """
+ """get the area of the polygon in square metres"""
return db.engine.execute(
ST_Area(ST_Transform(shape.from_shape(polygon, 4326), 3857))
).scalar()
@staticmethod
def validate_bbox_area(polygon: Polygon) -> bool:
- """ check polygon does not exceed maximim allowed area"""
+ """check polygon does not exceed maximim allowed area"""
area = ProjectSearchService._get_area_sqm(polygon)
return area <= MAX_AREA
diff --git a/backend/services/project_service.py b/backend/services/project_service.py
index 1fed8b4f5f..b6e53d93d2 100644
--- a/backend/services/project_service.py
+++ b/backend/services/project_service.py
@@ -33,7 +33,7 @@
class ProjectServiceError(Exception):
- """ Custom Exception to notify callers an error occurred when handling projects """
+ """Custom Exception to notify callers an error occurred when handling projects"""
def __init__(self, message):
if current_app:
@@ -214,7 +214,7 @@ def get_project_dto_for_mapper(
if project.status == ProjectStatus.DRAFT.value:
if not is_manager_permission:
is_allowed_user = False
- raise ProjectServiceError("Unable to fetch project")
+ raise ProjectServiceError("ProjectNotFetched- Unable to fetch project")
# Private Projects - allowed_users, admins, org admins &
# assigned teams (mappers, validators, project managers), authors permitted
@@ -247,7 +247,7 @@ def get_project_dto_for_mapper(
if is_allowed_user or is_manager_permission or is_team_member:
return project.as_dto_for_mapping(current_user_id, locale, abbrev)
else:
- raise ProjectServiceError("Unable to fetch project")
+ raise ProjectServiceError("ProjectNotFetched- Unable to fetch project")
@staticmethod
def get_project_tasks(
@@ -275,7 +275,7 @@ def get_project_priority_areas(project_id):
@staticmethod
def get_task_for_logged_in_user(user_id: int):
- """ if the user is working on a task in the project return it """
+ """if the user is working on a task in the project return it"""
tasks = Task.get_locked_tasks_for_user(user_id)
tasks_dto = tasks
@@ -283,7 +283,7 @@ def get_task_for_logged_in_user(user_id: int):
@staticmethod
def get_task_details_for_logged_in_user(user_id: int, preferred_locale: str):
- """ if the user is working on a task in the project return it """
+ """if the user is working on a task in the project return it"""
tasks = Task.get_locked_tasks_details_for_user(user_id)
if len(tasks) == 0:
@@ -336,7 +336,7 @@ def evaluate_mapping_permission(
@staticmethod
def is_user_permitted_to_map(project_id: int, user_id: int):
- """ Check if the user is allowed to map the on the project in scope """
+ """Check if the user is allowed to map the on the project in scope"""
if UserService.is_user_blocked(user_id):
return False, MappingNotAllowed.USER_NOT_ON_ALLOWED_LIST
@@ -389,7 +389,7 @@ def is_user_permitted_to_map(project_id: int, user_id: int):
@staticmethod
def _is_user_intermediate_or_advanced(user_id):
- """ Helper method to determine if user level is not beginner """
+ """Helper method to determine if user level is not beginner"""
user_mapping_level = UserService.get_mapping_level(user_id)
if user_mapping_level not in [MappingLevel.INTERMEDIATE, MappingLevel.ADVANCED]:
return False
@@ -421,7 +421,7 @@ def evaluate_validation_permission(
@staticmethod
def is_user_permitted_to_validate(project_id, user_id):
- """ Check if the user is allowed to validate on the project in scope """
+ """Check if the user is allowed to validate on the project in scope"""
if UserService.is_user_blocked(user_id):
return False, ValidatingNotAllowed.USER_NOT_ON_ALLOWED_LIST
@@ -477,25 +477,25 @@ def is_user_permitted_to_validate(project_id, user_id):
def get_project_summary(
project_id: int, preferred_locale: str = "en"
) -> ProjectSummary:
- """ Gets the project summary DTO """
+ """Gets the project summary DTO"""
project = ProjectService.get_project_by_id(project_id)
return project.get_project_summary(preferred_locale)
@staticmethod
def set_project_as_featured(project_id: int):
- """ Sets project as featured """
+ """Sets project as featured"""
project = ProjectService.get_project_by_id(project_id)
project.set_as_featured()
@staticmethod
def unset_project_as_featured(project_id: int):
- """ Sets project as featured """
+ """Sets project as featured"""
project = ProjectService.get_project_by_id(project_id)
project.unset_as_featured()
@staticmethod
def get_featured_projects(preferred_locale):
- """ Sets project as featured """
+ """Sets project as featured"""
query = ProjectSearchService.create_search_query()
projects = query.filter(Project.featured == true()).group_by(Project.id).all()
@@ -529,20 +529,20 @@ def unfavorite(project_id: int, user_id: int):
@staticmethod
def get_project_title(project_id: int, preferred_locale: str = "en") -> str:
- """ Gets the project title DTO """
+ """Gets the project title DTO"""
project = ProjectService.get_project_by_id(project_id)
return project.get_project_title(preferred_locale)
@staticmethod
@cached(TTLCache(maxsize=1024, ttl=600))
def get_project_stats(project_id: int) -> ProjectStatsDTO:
- """ Gets the project stats DTO """
+ """Gets the project stats DTO"""
project = ProjectService.get_project_by_id(project_id)
return project.get_project_stats()
@staticmethod
def get_project_user_stats(project_id: int, username: str) -> ProjectUserStatsDTO:
- """ Gets the user stats for a specific project """
+ """Gets the user stats for a specific project"""
project = ProjectService.get_project_by_id(project_id)
user = UserService.get_user_by_username(username)
return project.get_project_user_stats(user.id)
diff --git a/backend/services/team_service.py b/backend/services/team_service.py
index 1a9ba51061..671e83baa0 100644
--- a/backend/services/team_service.py
+++ b/backend/services/team_service.py
@@ -28,7 +28,7 @@
class TeamServiceError(Exception):
- """ Custom Exception to notify callers an error occurred when handling teams """
+ """Custom Exception to notify callers an error occurred when handling teams"""
def __init__(self, message):
if current_app:
@@ -36,7 +36,7 @@ def __init__(self, message):
class TeamJoinNotAllowed(Exception):
- """ Custom Exception to notify bad user level on joining team """
+ """Custom Exception to notify bad user level on joining team"""
def __init__(self, message):
if current_app:
@@ -52,7 +52,8 @@ def join_team(team_id: int, requesting_user: int, username: str, role: str = Non
if TeamService.is_user_team_member(team.id, user.id):
raise TeamJoinNotAllowed(
- "User is already a member of this team or has already requested to join"
+ "UserAlreadyInList- "
+ + "User is already a member of this team or has already requested to join"
)
if is_manager:
@@ -67,7 +68,9 @@ def join_team(team_id: int, requesting_user: int, username: str, role: str = Non
TeamService.add_team_member(team_id, user.id, role, True)
else:
if user.id != requesting_user:
- raise TeamJoinNotAllowed("User not allowed to join team")
+ raise TeamJoinNotAllowed(
+ "UserJoinDisallowed- User not allowed to join team"
+ )
role = TeamMemberFunctions.MEMBER.value
@@ -348,7 +351,7 @@ def get_projects_by_team_id(team_id: int):
@staticmethod
def get_project_teams_as_dto(project_id: int) -> TeamsListDTO:
- """ Gets all the teams for a specified project """
+ """Gets all the teams for a specified project"""
project_teams = ProjectTeams.query.filter(
ProjectTeams.project_id == project_id
).all()
@@ -423,7 +426,7 @@ def update_team(team_dto: TeamDTO) -> Team:
@staticmethod
def assert_validate_organisation(org_id: int):
- """ Makes sure an organisation exists """
+ """Makes sure an organisation exists"""
try:
OrganisationService.get_organisation_by_id(org_id)
except NotFound:
@@ -431,7 +434,7 @@ def assert_validate_organisation(org_id: int):
@staticmethod
def assert_validate_members(team_dto: TeamDTO):
- """ Validates that the users exist"""
+ """Validates that the users exist"""
if len(team_dto.members) == 0:
raise TeamServiceError("Must have at least one member")
@@ -516,7 +519,7 @@ def is_user_team_manager(team_id: int, user_id: int):
@staticmethod
def delete_team(team_id: int):
- """ Deletes a team """
+ """Deletes a team"""
team = TeamService.get_team_by_id(team_id)
if team.can_be_deleted():
@@ -526,7 +529,7 @@ def delete_team(team_id: int):
@staticmethod
def check_team_membership(project_id: int, allowed_roles: list, user_id: int):
- """ Given a project and permitted team roles, check user's membership in the team list """
+ """Given a project and permitted team roles, check user's membership in the team list"""
teams_dto = TeamService.get_project_teams_as_dto(project_id)
teams_allowed = [
team_dto for team_dto in teams_dto.teams if team_dto.role in allowed_roles
diff --git a/backend/services/users/user_service.py b/backend/services/users/user_service.py
index 0b0faa2d94..bc47026ba4 100644
--- a/backend/services/users/user_service.py
+++ b/backend/services/users/user_service.py
@@ -39,7 +39,7 @@
class UserServiceError(Exception):
- """ Custom Exception to notify callers an error occurred when in the User Service """
+ """Custom Exception to notify callers an error occurred when in the User Service"""
def __init__(self, message):
if current_app:
@@ -173,7 +173,7 @@ def register_user(osm_id, username, changeset_count, picture_url, email):
def get_user_dto_by_username(
requested_username: str, logged_in_user_id: int
) -> UserDTO:
- """Gets user DTO for supplied username """
+ """Gets user DTO for supplied username"""
requested_user = UserService.get_user_by_username(requested_username)
logged_in_user = UserService.get_user_by_id(logged_in_user_id)
UserService.check_and_update_mapper_level(requested_user.id)
@@ -182,7 +182,7 @@ def get_user_dto_by_username(
@staticmethod
def get_user_dto_by_id(user: int, request_user: int) -> UserDTO:
- """Gets user DTO for supplied user id """
+ """Gets user DTO for supplied user id"""
user = UserService.get_user_by_id(user)
if request_user:
request_username = UserService.get_user_by_id(request_user).username
@@ -479,18 +479,18 @@ def update_user_details(user_id: int, user_dto: UserDTO) -> dict:
@staticmethod
def get_all_users(query: UserSearchQuery) -> UserSearchDTO:
- """ Gets paginated list of users """
+ """Gets paginated list of users"""
return User.get_all_users(query)
@staticmethod
@cached(user_filter_cache)
def filter_users(username: str, project_id: int, page: int) -> UserFilterDTO:
- """ Gets paginated list of users, filtered by username, for autocomplete """
+ """Gets paginated list of users, filtered by username, for autocomplete"""
return User.filter_users(username, project_id, page)
@staticmethod
def is_user_an_admin(user_id: int) -> bool:
- """ Is the user an admin """
+ """Is the user an admin"""
user = UserService.get_user_by_id(user_id)
if UserRole(user.role) == UserRole.ADMIN:
return True
@@ -499,19 +499,19 @@ def is_user_an_admin(user_id: int) -> bool:
@staticmethod
def is_user_the_project_author(user_id: int, author_id: int) -> bool:
- """ Is user the author of the project """
+ """Is user the author of the project"""
return user_id == author_id
@staticmethod
def get_mapping_level(user_id: int):
- """ Gets mapping level user is at"""
+ """Gets mapping level user is at"""
user = UserService.get_user_by_id(user_id)
return MappingLevel(user.mapping_level)
@staticmethod
def is_user_validator(user_id: int) -> bool:
- """ Determines if user is a validator """
+ """Determines if user is a validator"""
user = UserService.get_user_by_id(user_id)
if UserRole(user.role) in [
@@ -523,7 +523,7 @@ def is_user_validator(user_id: int) -> bool:
@staticmethod
def is_user_blocked(user_id: int) -> bool:
- """ Determines if a user is blocked """
+ """Determines if a user is blocked"""
user = UserService.get_user_by_id(user_id)
if UserRole(user.role) == UserRole.READ_ONLY:
@@ -590,18 +590,18 @@ def get_countries_contributed(user_id: int):
@staticmethod
def upsert_mapped_projects(user_id: int, project_id: int):
- """ Add project to mapped projects if it doesn't exist, otherwise return """
+ """Add project to mapped projects if it doesn't exist, otherwise return"""
User.upsert_mapped_projects(user_id, project_id)
@staticmethod
def get_mapped_projects(user_name: str, preferred_locale: str):
- """ Gets all projects a user has mapped or validated on """
+ """Gets all projects a user has mapped or validated on"""
user = UserService.get_user_by_username(user_name)
return User.get_mapped_projects(user.id, preferred_locale)
@staticmethod
def get_recommended_projects(user_name: str, preferred_locale: str):
- """ Gets all projects a user has mapped or validated on """
+ """Gets all projects a user has mapped or validated on"""
from backend.services.project_search_service import ProjectSearchService
limit = 20
@@ -668,14 +668,17 @@ def add_role_to_user(admin_user_id: int, username: str, role: str):
requested_role = UserRole[role.upper()]
except KeyError:
raise UserServiceError(
- f"Unknown role {role} accepted values are ADMIN, PROJECT_MANAGER, VALIDATOR"
+ "UnknownAddRole- "
+ + f"Unknown role {role} accepted values are ADMIN, PROJECT_MANAGER, VALIDATOR"
)
admin = UserService.get_user_by_id(admin_user_id)
admin_role = UserRole(admin.role)
if admin_role != UserRole.ADMIN and requested_role == UserRole.ADMIN:
- raise UserServiceError("You must be an Admin to assign Admin role")
+ raise UserServiceError(
+ "NeedAdminRole- You must be an Admin to assign Admin role"
+ )
user = UserService.get_user_by_username(username)
user.set_user_role(requested_role)
@@ -690,7 +693,8 @@ def set_user_mapping_level(username: str, level: str) -> User:
requested_level = MappingLevel[level.upper()]
except KeyError:
raise UserServiceError(
- f"Unknown role {level} accepted values are BEGINNER, INTERMEDIATE, ADVANCED"
+ "UnknownUserRole- "
+ + f"Unknown role {level} accepted values are BEGINNER, INTERMEDIATE, ADVANCED"
)
user = UserService.get_user_by_username(username)
@@ -711,13 +715,13 @@ def set_user_is_expert(user_id: int, is_expert: bool) -> User:
@staticmethod
def accept_license_terms(user_id: int, license_id: int):
- """ Saves the fact user has accepted license terms """
+ """Saves the fact user has accepted license terms"""
user = UserService.get_user_by_id(user_id)
user.accept_license_terms(license_id)
@staticmethod
def has_user_accepted_license(user_id: int, license_id: int):
- """ Checks if user has accepted specified license """
+ """Checks if user has accepted specified license"""
user = UserService.get_user_by_id(user_id)
return user.has_user_accepted_licence(license_id)
@@ -734,7 +738,7 @@ def get_osm_details_for_user(username: str) -> UserOSMDTO:
@staticmethod
def check_and_update_mapper_level(user_id: int):
- """ Check users mapping level and update if they have crossed threshold """
+ """Check users mapping level and update if they have crossed threshold"""
user = UserService.get_user_by_id(user_id)
user_level = MappingLevel(user.mapping_level)
@@ -783,7 +787,7 @@ def notify_level_upgrade(user_id: int, username: str, level: str):
@staticmethod
def refresh_mapper_level() -> int:
- """ Helper function to run thru all users in the DB and update their mapper level """
+ """Helper function to run thru all users in the DB and update their mapper level"""
users = User.get_all_users_not_paginated()
users_updated = 1
total_users = len(users)
diff --git a/backend/services/validator_service.py b/backend/services/validator_service.py
index 47702f0870..eef75c56e3 100644
--- a/backend/services/validator_service.py
+++ b/backend/services/validator_service.py
@@ -28,7 +28,7 @@
class ValidatorServiceError(Exception):
- """ Custom exception to notify callers that error has occurred """
+ """Custom exception to notify callers that error has occurred"""
def __init__(self, message):
if current_app:
@@ -56,14 +56,15 @@ def lock_tasks_for_validation(validation_dto: LockForValidationDTO) -> TaskDTOs:
TaskStatus.BADIMAGERY,
]:
raise ValidatorServiceError(
- f"Task {task_id} is not MAPPED, BADIMAGERY or INVALIDATED"
+ f"NotReadyForValidation- Task {task_id} is not MAPPED, BADIMAGERY or INVALIDATED"
)
user_can_validate = ValidatorService._user_can_validate_task(
validation_dto.user_id, task.mapped_by
)
if not user_can_validate:
raise ValidatorServiceError(
- "Tasks cannot be validated by the same user who marked task as mapped or badimagery"
+ "CannotValidateMappedTask-"
+ + "Tasks cannot be validated by the same user who marked task as mapped or badimagery"
)
tasks_to_lock.append(task)
@@ -75,8 +76,18 @@ def lock_tasks_for_validation(validation_dto: LockForValidationDTO) -> TaskDTOs:
if not user_can_validate:
if error_reason == ValidatingNotAllowed.USER_NOT_ACCEPTED_LICENSE:
raise UserLicenseError("User must accept license to map this task")
+ elif error_reason == ValidatingNotAllowed.USER_NOT_ON_ALLOWED_LIST:
+ raise ValidatorServiceError(
+ "UserNotAllowed- Validation not allowed because: User not on allowed list"
+ )
+ elif error_reason == ValidatingNotAllowed.PROJECT_NOT_PUBLISHED:
+ raise ValidatorServiceError(
+ "ProjectNotPublished- Validation not allowed because: Project not published"
+ )
elif error_reason == ValidatingNotAllowed.USER_ALREADY_HAS_TASK_LOCKED:
- raise ValidatorServiceError("User already has a task locked")
+ raise ValidatorServiceError(
+ "UserAlreadyHasTaskLocked- User already has a task locked"
+ )
else:
raise ValidatorServiceError(
f"Validation not allowed because: {error_reason}"
@@ -241,12 +252,12 @@ def get_tasks_locked_by_user(project_id: int, unlock_tasks, user_id: int):
current_state = TaskStatus(task.task_status)
if current_state != TaskStatus.LOCKED_FOR_VALIDATION:
raise ValidatorServiceError(
- f"Task {unlock_task.task_id} is not LOCKED_FOR_VALIDATION"
+ f"NotLockedForValidation- Task {unlock_task.task_id} is not LOCKED_FOR_VALIDATION"
)
if task.locked_by != user_id:
raise ValidatorServiceError(
- "Attempting to unlock a task owned by another user"
+ "TaskNotOwned- Attempting to unlock a task owned by another user"
)
if hasattr(unlock_task, "status"):
@@ -268,7 +279,7 @@ def get_tasks_locked_by_user(project_id: int, unlock_tasks, user_id: int):
@staticmethod
def get_mapped_tasks_by_user(project_id: int) -> MappedTasks:
- """ Get all mapped tasks on the project grouped by user"""
+ """Get all mapped tasks on the project grouped by user"""
mapped_tasks = Task.get_mapped_tasks_by_user(project_id)
return mapped_tasks
@@ -284,7 +295,7 @@ def get_user_invalidated_tasks(
sort_by="updated_date",
sort_direction="desc",
) -> InvalidatedTasks:
- """ Get invalidated tasks either mapped or invalidated by the user """
+ """Get invalidated tasks either mapped or invalidated by the user"""
user = UserService.get_user_by_username(username)
query = (
TaskInvalidationHistory.query.filter_by(invalidator_id=user.id)
@@ -324,7 +335,7 @@ def get_user_invalidated_tasks(
@staticmethod
def invalidate_all_tasks(project_id: int, user_id: int):
- """ Invalidates all mapped tasks on a project"""
+ """Invalidates all mapped tasks on a project"""
mapped_tasks = Task.query.filter(
Task.project_id == project_id,
~Task.task_status.in_(
@@ -348,7 +359,7 @@ def invalidate_all_tasks(project_id: int, user_id: int):
@staticmethod
def validate_all_tasks(project_id: int, user_id: int):
- """ Validates all mapped tasks on a project"""
+ """Validates all mapped tasks on a project"""
tasks_to_validate = Task.query.filter(
Task.project_id == project_id,
Task.task_status != TaskStatus.BADIMAGERY.value,
diff --git a/docs/contributing.md b/docs/contributing.md
index 7392e9a47b..0934bedaa5 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -1,36 +1,47 @@
+
# Contributing to the Tasking Manager
## Welcome
-Thank you very much for considering contributing to this project. Humanitarian OpenStreetMap Team (HOT) is a volunteer driven non-profit and we really rely on the community for our success. We welcome and encourage contributors of all skill levels to be a part of our development community and we are committed to making sure your participation in our community is enjoyable and rewarding. If you have never contributed to an open source project before, we are the perfect place to start and will make sure you are supported every step of the way. If you have **any** questions, please ask!
+:+1::tada: First off, I'm really glad you're reading this, because we need volunteer developers to help improve the Tasking Manager.! :tada::+1:
+We welcome and encourage contributors of all skill levels and we are committed to making sure your participation in our tech collective is inclusive, enjoyable and rewarding. If you have never contributed to an open source project before, we are a good place to start and will make sure you are supported every step of the way. If you have **any** questions, please ask!
+
+We are collaborating with Kathmandu Living Labs on the maintenance of the Tasking Manager - expect to hear a lot from all of us on Github :)
+
+There are many ways to contribute to the Tasking Manager Project:
+
+## Report bugs and suggest improvements:
+
+The [issue queue](https://github.com/hotosm/tasking-manager/issues) is the best way to get started. There are issue templates for BUGs and FEATURES that you can use, or you can create your own. Once you have submitted an issue, it will be assigned one label out of the following [label categories](https://github.com/hotosm/tasking-manager/labels):
+
+- **Backlog**: Backlog=triage will first be assigned to any new issues
+- **Component**
+
+On a monthly basis, we will collaboratively triage issues from the *backlog=triage* and assign one of the below labels:
+- **Assigned**: once reviewed the issue will be assigned either to hot_tech OR tm_collective. Issues assigned to tm_collective is where we really need your help!
+- **Type**: specifying whether the issue is a bug or feature/enhancement
+- **Priority**: specifying the priority level for each issue. We want to collaboratively agree the criteria for prioritisation.
+- **Status**: specifying whether the issue is in progress or done.
+- **Experience**: we have added a beginner label for good first issues. We will work with the community to update the labels in this category and make them suitable.
-There are many ways to contribute to a project, below are some examples:
+Note: Issues older than 6 months from the point of raising the issue with no engagement will be labelled as *archived*.
-1. Report bugs, offer ideas, and/or request features by creating “Issues” in the project repository.
-2. Fork the code and play with it, whether you later choose to make a pull request or not.
-3. Create pull requests for changes that you think are needed. From typos and wording changes, to significant new features or major design flaws, you will find lots of opportunities to contribute improvement.
-4. Review or submit language translations.
+## Testing
-## Code of Conduct
+Test a bug fix or new feature. Once an issue has been addressed and Pull Request (PR) change deployed to the [Tasking Manager Staging site](https://tasks-stage.hotosm.org/), you will be able to view and test the change on the staging site. A PR would then be made from develop to master branch, which would require two reviews. If you notice any issues while testing, please comment on the PR directly.
-All of HOT's projects fall under the general [HOT Community Code of Conduct](https://www.hotosm.org/hot_code_of_conduct), which is in part based on the well known [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/).
-The short version is:
+## Code contributions
-* Use welcoming and inclusive language
-* Be respectful of differing viewpoints and experiences
-* Gracefully accept constructive criticism
-* Focus on what is best for the community and the project
-* Show empathy toward other community members
+Create pull requests (PRs) for changes that you think are needed. We would really appreciate your help! We ask that you follow our [coding contribution guidelines](https://github.com/hotosm/tasking-manager/blob/develop/docs/contributing-code.md).
+## Translating
+Review or submit [language translations]( https://github.com/hotosm/tasking-manager/blob/develop/docs/contributing-translation.md)
-## The issue queue and the repository
+## Thank you!
+Thank you very much in advance for your contributions!! Please ensure you refer to our [Code of Conduct](https://github.com/hotosm/tasking-manager/blob/develop/docs/code_of_conduct.md) when you contribute!
-The main tool used to manage the Tasking Manager software is its [repository on Github](https://github.com/hotosm/tasking-manager). In this repository, there is an [issue queue](https://github.com/hotosm/tasking-manager/issues), where most communication, planning and reporting of errors happens. Feel free to take part in these conversations.
+If you've read the guidelines, but you are still not sure how to contribute on Github, please reach out to us via our [ HOT Tech Support page](https://hotosm.atlassian.net/servicedesk/customer/portal/4) and we will be happy to help!
-## Contribute now!
-All kind of contributions are welcome and the [issue queue](https://github.com/hotosm/tasking-manager/issues) is the best way to get started. We have specific guides for contributing to the Tasking Manager:
-* **[translation](./contributing-translation.md)**
-* **[code](./contributing-code.md)**
diff --git a/example.env b/example.env
index 94c6d8d791..f62e54a05a 100644
--- a/example.env
+++ b/example.env
@@ -41,6 +41,7 @@ OSM_REGISTER_URL=https://www.openstreetmap.org/user/new
# You only need to modify it in case you want to direct users to map on a different OSM instance.
# ID_EDITOR_URL=https://www.openstreetmap.org/edit?editor=id&
# POTLATCH2_EDITOR_URL=https://www.openstreetmap.org/edit?editor=potlatch2
+# RAPID_EDITOR_URL=https://mapwith.ai/rapid
# Matomo configuration. Optional, configure it in case you have a Matomo instance.
# TM_MATOMO_ID="site_id"
diff --git a/frontend/.env.expand b/frontend/.env.expand
index 6e6225cb5f..3247d44106 100644
--- a/frontend/.env.expand
+++ b/frontend/.env.expand
@@ -41,3 +41,4 @@ REACT_APP_POTLATCH2_EDITOR_URL=$POTLATCH2_EDITOR_URL
REACT_APP_SENTRY_FRONTEND_DSN=$TM_SENTRY_FRONTEND_DSN
REACT_APP_ENVIRONMENT=$TM_ENVIRONMENT
REACT_APP_TM_DEFAULT_CHANGESET_COMMENT=$TM_DEFAULT_CHANGESET_COMMENT
+REACT_APP_RAPID_EDITOR_URL=$RAPID_EDITOR_URL
diff --git a/frontend/package.json b/frontend/package.json
index 459c2d0efd..48887df0ce 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -36,6 +36,7 @@
"final-form": "^4.20.2",
"fromentries": "^1.3.2",
"humanize-duration": "^3.27.0",
+ "RapiD": "facebookincubator/rapid#rapid-v1.1.8_tm",
"immutable": "^4.0.0-rc.12",
"mapbox-gl": "^1.13.1",
"mapbox-gl-draw-rectangle-mode": "^1.0.4",
@@ -77,8 +78,8 @@
},
"scripts": {
"build-locales": "combine-messages -i './src/**/messages.js' -o './src/locales/en.json'",
- "copy-static": "bash -c \"if ! (test -a public/static/index.js); then cp -R node_modules/@hotosm/id/dist/* public/static/; fi\"",
- "update-static": "bash -c \"cp -R node_modules/@hotosm/id/dist/* public/static/;\"",
+ "copy-static": "bash -c \"mkdir -p public/static/id; mkdir -p public/static/rapid; if ! (test -a public/static/id/index.js); then cp -R node_modules/@hotosm/id/dist/* public/static/id; elif ! (test -a public/static/rapid/index.js); then cp -R node_modules/RapiD/dist/* public/static/rapid; fi\"",
+ "update-static": "bash -c \"mkdir -p public/static/id; mkdir -p public/static/rapid; cp -R node_modules/@hotosm/id/dist/* public/static/id; cp -R node_modules/RapiD/dist/* public/static/rapid;\"",
"preparation": "bash -c \"if (test -a ../tasking-manager.env); then grep -hs ^ ../tasking-manager.env .env.expand > .env; else cp .env.expand .env; fi\"",
"start": "npm run preparation && npm run copy-static && react-scripts start",
"build": "npm run preparation && npm run update-static && react-scripts build",
diff --git a/frontend/src/assets/styles/_extra.scss b/frontend/src/assets/styles/_extra.scss
index 24a2fed1d7..54322d8d73 100644
--- a/frontend/src/assets/styles/_extra.scss
+++ b/frontend/src/assets/styles/_extra.scss
@@ -129,6 +129,12 @@ a:active {
background-color: $white; // @extend .bg-moon-gray;
border-color: $red;
}
+
+ .checkbox-toggle-sm {
+ left: 1rem; // @extend .left-1;
+ background-color: $white; // @extend .bg-moon-gray;
+ border-color: $red;
+ }
}
.checkbox-toggle:hover {
border: dashed red !important;
@@ -289,3 +295,26 @@ div.messageBodyLinks {
top: 2.5rem;
}
}
+
+.rapid-beta {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: bold;
+ color: #eee;
+ margin: 0 10px;
+ width: 1.8em;
+ height: 1.8em;
+ border: 1px solid #909;
+ border-radius: 5px;
+ background: rgb(203,16,237);
+ background: -webkit-gradient(linear, left bottom, left top, color-stop(6%, rgba(108,1,167,1)), color-stop(50%, rgba(203,16,237,1)), color-stop(90%, rgb(229, 140, 253)), to(rgb(201, 42, 251)));
+ background: -o-linear-gradient(bottom, rgba(108,1,167,1) 6%, rgba(203,16,237,1) 50%, rgb(229, 140, 253) 90%, rgb(201, 42, 251) 100%);
+ background: linear-gradient(0deg, rgba(108,1,167,1) 6%, rgba(203,16,237,1) 50%, rgb(229, 140, 253) 90%, rgb(201, 42, 251) 100%);
+
+ &:before {
+ content: '\03b2';
+ font-size: 1.2em;
+ vertical-align: middle;
+ }
+}
diff --git a/frontend/src/components/editor.js b/frontend/src/components/editor.js
index 298f2a83b8..82606344c5 100644
--- a/frontend/src/components/editor.js
+++ b/frontend/src/components/editor.js
@@ -70,7 +70,7 @@ export default function Editor({ setDisable, comment, presets, imagery, gpxUrl }
// setup the context
iDContext
.embed(true)
- .assetPath('/static/')
+ .assetPath('/static/id/')
.locale(locale)
.setsDocumentTitle(false)
.containerNode(document.getElementById('id-container'));
diff --git a/frontend/src/components/footer.js b/frontend/src/components/footer.js
index 1429cca24a..fa12786e0e 100644
--- a/frontend/src/components/footer.js
+++ b/frontend/src/components/footer.js
@@ -2,8 +2,14 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { Link } from '@reach/router';
import { FormattedMessage } from 'react-intl';
-
-import { TwitterIcon, FacebookIcon, YoutubeIcon, GithubIcon, InstagramIcon } from './svgIcons';
+import {
+ TwitterIcon,
+ FacebookIcon,
+ YoutubeIcon,
+ GithubIcon,
+ InstagramIcon,
+ ExternalLinkIcon,
+} from './svgIcons';
import messages from './messages';
import { getMenuItensForUser } from './header';
import {
@@ -43,13 +49,27 @@ export function Footer({ location }: Object) {
{socialNetworks diff --git a/frontend/src/components/formInputs.js b/frontend/src/components/formInputs.js index f3ea5d645e..67aa72cf79 100644 --- a/frontend/src/components/formInputs.js +++ b/frontend/src/components/formInputs.js @@ -21,7 +21,7 @@ export const RadioField = ({ name, value, className }: Object) => ( /> ); -export const SwitchToggle = ({ label, isChecked, onChange, labelPosition }: Object) => ( +export const SwitchToggle = ({ label, isChecked, onChange, labelPosition, small = false }: Object) => (
-
-