From 9fbfc18788bb86ed8e385d1516737d2ba7f5ee20 Mon Sep 17 00:00:00 2001 From: Wille Marcel Date: Fri, 18 Jun 2021 17:28:36 -0300 Subject: [PATCH 01/29] Add embedded RapiD editor --- .github/dependabot.yml | 64 +++++++++- backend/models/dtos/project_dto.py | 9 +- backend/models/postgis/project.py | 6 + backend/models/postgis/statuses.py | 1 + example.env | 1 + frontend/.env.expand | 1 + frontend/package.json | 5 +- frontend/src/assets/styles/_extra.scss | 29 +++++ frontend/src/components/editor.js | 2 +- frontend/src/components/formInputs.js | 6 +- .../src/components/projectEdit/messages.js | 8 ++ .../components/projectEdit/settingsForm.js | 23 ++++ frontend/src/components/rapidEditor.js | 119 ++++++++++++++++++ .../src/components/taskSelection/action.js | 36 ++++-- .../src/components/taskSelection/footer.js | 10 +- frontend/src/config/index.js | 2 + frontend/src/utils/editorsList.js | 9 +- frontend/src/utils/openEditor.js | 6 +- frontend/src/utils/tests/editorsList.test.js | 5 + frontend/src/views/projectEdit.js | 1 + frontend/yarn.lock | 62 ++++++++- migrations/versions/8a6419f289aa_.py | 40 ++++++ 22 files changed, 416 insertions(+), 29 deletions(-) create mode 100644 frontend/src/components/rapidEditor.js create mode 100644 migrations/versions/8a6419f289aa_.py 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/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..334fcbe648 100644 --- a/backend/models/postgis/project.py +++ b/backend/models/postgis/project.py @@ -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) @@ -174,6 +175,7 @@ class Project(db.Model): Editors.ID.value, Editors.JOSM.value, Editors.CUSTOM.value, + Editors.RAPID.value, ], index=True, nullable=False, @@ -184,6 +186,7 @@ class Project(db.Model): Editors.ID.value, Editors.JOSM.value, Editors.CUSTOM.value, + Editors.RAPID.value, ], index=True, nullable=False, @@ -375,6 +378,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 @@ -841,6 +845,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 @@ -1004,6 +1009,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 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/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..04da898669 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.4", "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/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) => (
{label && labelPosition !== 'right' && {label}}
@@ -31,8 +31,8 @@ export const SwitchToggle = ({ label, isChecked, onChange, labelPosition }: Obje checked={isChecked} onChange={onChange} /> -
-
+
+
{label && labelPosition === 'right' && {label}} diff --git a/frontend/src/components/projectEdit/messages.js b/frontend/src/components/projectEdit/messages.js index 30da13633b..dd62744bd0 100644 --- a/frontend/src/components/projectEdit/messages.js +++ b/frontend/src/components/projectEdit/messages.js @@ -347,6 +347,14 @@ export default defineMessages({ defaultMessage: 'If checked, users must edit tasks at random for the initial editing stage (managers and admins are exempt).', }, + rapidPowerUser: { + id: 'projects.formInputs.rapid_power_user', + defaultMessage: 'Enable RapiD Power User Features', + }, + rapidPowerUserDescription: { + id: 'projects.formInputs.rapid_power_user.description', + defaultMessage: 'If checked, RapiD will load with the power user dialog enabled.', + }, imagery: { id: 'projects.formInputs.imagery', defaultMessage: 'Imagery', diff --git a/frontend/src/components/projectEdit/settingsForm.js b/frontend/src/components/projectEdit/settingsForm.js index ad6ea60366..20933ab42c 100644 --- a/frontend/src/components/projectEdit/settingsForm.js +++ b/frontend/src/components/projectEdit/settingsForm.js @@ -127,6 +127,29 @@ export const SettingsForm = ({ languages, defaultLocale }) => {

+ {(projectInfo.mappingEditors.includes('RAPID') || projectInfo.validationEditors.includes('RAPID')) && ( +
+
); }; diff --git a/frontend/src/components/rapidEditor.js b/frontend/src/components/rapidEditor.js new file mode 100644 index 0000000000..3e55d04068 --- /dev/null +++ b/frontend/src/components/rapidEditor.js @@ -0,0 +1,119 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import * as RapiD from 'RapiD'; +import 'RapiD/dist/iD.css'; + +import { OSM_CONSUMER_KEY, OSM_CONSUMER_SECRET, OSM_SERVER_URL } from '../config'; + +export default function RapidEditor({ setDisable, comment, presets, imagery, gpxUrl, powerUser = false }) { + const dispatch = useDispatch(); + const session = useSelector((state) => state.auth.get('session')); + const iDContext = useSelector((state) => state.editor.context); + const locale = useSelector((state) => state.preferences.locale); + const [customImageryIsSet, setCustomImageryIsSet] = useState(false); + const windowInit = typeof window !== undefined; + const customSource = + iDContext && iDContext.background() && iDContext.background().findSource('custom'); + + useEffect(() => { + if (!customImageryIsSet && imagery && customSource) { + if (imagery.startsWith('http')) { + iDContext.background().baseLayerSource(customSource.template(imagery)); + setCustomImageryIsSet(true); + // this line is needed to update the value on the custom background dialog + window.iD.prefs('background-custom-template', imagery); + } else { + const imagerySource = iDContext.background().findSource(imagery); + if (imagerySource) { + iDContext.background().baseLayerSource(imagerySource); + } + } + } + }, [customImageryIsSet, imagery, iDContext, customSource]); + + useEffect(() => { + return () => { + dispatch({ type: 'SET_VISIBILITY', isVisible: true }); + }; + // eslint-disable-next-line + }, []); + + useEffect(() => { + if (windowInit) { + dispatch({ type: 'SET_VISIBILITY', isVisible: false }); + if (iDContext === null) { + // we need to keep iD context on redux store because iD works better if + // the context is not restarted while running in the same browser session + dispatch({ type: 'SET_EDITOR', context: window.iD.coreContext()}) + // } else{ + // if (RapiDContext === null) { + // dispatch({ type: 'SET_EDITOR', context: iDContext, rapidContext: window.iD.coreRapidContext(())}) + // } + // } + } + } + }, [windowInit, iDContext, dispatch]); + + useEffect(() => { + if (iDContext && comment) { + iDContext.defaultChangesetComment(comment); + } + }, [comment, iDContext]); + + useEffect(() => { + if (session && locale && RapiD && iDContext) { + // if presets is not a populated list we need to set it as null + try { + if (presets.length) { + window.iD.presetManager.addablePresetIDs(presets); + } else { + window.iD.presetManager.addablePresetIDs(null); + } + } catch (e) { + window.iD.presetManager.addablePresetIDs(null); + } + // setup the context + iDContext + .embed(true) + .assetPath('/static/rapid/') + .locale(locale) + .setsDocumentTitle(false) + .containerNode(document.getElementById('id-container')); + // init the ui or restart if it was loaded previously + if (iDContext.ui() !== undefined) { + iDContext.reset(); + iDContext.ui().restart(); + } else { + iDContext.init(); + } + if (gpxUrl) { + iDContext.layers().layer('data').url(gpxUrl, '.gpx'); + } + + iDContext.rapidContext().showPowerUser = powerUser; + + let osm = iDContext.connection(); + const auth = { + urlroot: OSM_SERVER_URL, + oauth_consumer_key: OSM_CONSUMER_KEY, + oauth_secret: OSM_CONSUMER_SECRET, + oauth_token: session.osm_oauth_token, + oauth_token_secret: session.osm_oauth_token_secret, + }; + osm.switch(auth); + + const thereAreChanges = (changes) => + changes.modified.length || changes.created.length || changes.deleted.length; + + iDContext.history().on('change', () => { + if (thereAreChanges(iDContext.history().changes())) { + setDisable(true); + } else { + setDisable(false); + } + }); + } + }, [session, iDContext, setDisable, presets, locale, gpxUrl, powerUser]); + + return
; +} diff --git a/frontend/src/components/taskSelection/action.js b/frontend/src/components/taskSelection/action.js index 6ad8e11130..10e56a4e68 100644 --- a/frontend/src/components/taskSelection/action.js +++ b/frontend/src/components/taskSelection/action.js @@ -33,6 +33,7 @@ import { ResourcesTab } from './resourcesTab'; import { ActionTabsNav } from './actionTabsNav'; const Editor = React.lazy(() => import('../editor')); +const RapiDEditor = React.lazy(() => import('../rapidEditor')); export function TaskMapAction({ project, projectIsReady, tasks, activeTasks, action, editor }) { useSetProjectPageTitleTag(project); @@ -119,6 +120,7 @@ export function TaskMapAction({ project, projectIsReady, tasks, activeTasks, act [window.innerWidth, window.innerHeight], null, ); + if (url) { navigate(`./${url}`); } else { @@ -142,13 +144,14 @@ export function TaskMapAction({ project, projectIsReady, tasks, activeTasks, act } else { navigate(`./?editor=${arr[0].value}`); } + window.location.reload(); }; return (
- {editor === 'ID' ? ( + {['ID', 'RAPID'].includes(editor) ? ( @@ -161,13 +164,24 @@ export function TaskMapAction({ project, projectIsReady, tasks, activeTasks, act
} > - + {editor === 'ID' ? ( + + ) : ( + + )} ) : ( 0} > - {activeEditor === 'ID' && } + {(activeEditor === 'ID' || activeEditor === 'RAPID') && ( + + )} - {editor === 'ID' && ( + {(editor === 'ID' || editor === 'RAPID') && ( ( diff --git a/frontend/src/components/taskSelection/footer.js b/frontend/src/components/taskSelection/footer.js index 18cb0fce99..9840c2f732 100644 --- a/frontend/src/components/taskSelection/footer.js +++ b/frontend/src/components/taskSelection/footer.js @@ -42,7 +42,7 @@ const TaskSelectionFooter = ({ defaultUserEditor, project, tasks, taskAction, se const lockFailed = (windowObjectReference, message) => { // JOSM and iD don't open a new window - if (!['JOSM', 'ID'].includes(editor)) { + if (!['JOSM', 'ID', 'RAPID'].includes(editor)) { windowObjectReference.close(); } fetchLockedTasks(); @@ -71,8 +71,11 @@ const TaskSelectionFooter = ({ defaultUserEditor, project, tasks, taskAction, se } } let windowObjectReference; - if (!['JOSM', 'ID'].includes(editor)) { - windowObjectReference = window.open('', `TM-${project.projectId}-${selectedTasks}`); + if (!['JOSM', 'ID', 'RAPID'].includes(editor)) { + windowObjectReference = window.open( + '', + `TM-${project.projectId}-${selectedTasks}`, + ); } if (['validateSelectedTask', 'validateAnotherTask', 'validateATask'].includes(taskAction)) { const mappedTasks = selectedTasks.filter( @@ -105,6 +108,7 @@ const TaskSelectionFooter = ({ defaultUserEditor, project, tasks, taskAction, se ) .then((res) => { lockSuccess('LOCKED_FOR_MAPPING', 'map', windowObjectReference); + window.location.reload(); }) .catch((e) => lockFailed(windowObjectReference, e.message)); } diff --git a/frontend/src/config/index.js b/frontend/src/config/index.js index fb451b9dfd..1a035935bc 100644 --- a/frontend/src/config/index.js +++ b/frontend/src/config/index.js @@ -57,6 +57,8 @@ export const ID_EDITOR_URL = export const POTLATCH2_EDITOR_URL = process.env.REACT_APP_POTLATCH2_EDITOR_URL || 'https://www.openstreetmap.org/edit?editor=potlatch2'; +export const RAPID_EDITOR_URL = + process.env.REACT_APP_RAPID_EDITOR_URL || 'https://mapwith.ai/rapid'; export const TASK_COLOURS = { READY: '#fff', diff --git a/frontend/src/utils/editorsList.js b/frontend/src/utils/editorsList.js index d3410127e3..73a7a719f0 100644 --- a/frontend/src/utils/editorsList.js +++ b/frontend/src/utils/editorsList.js @@ -1,7 +1,12 @@ -import { ID_EDITOR_URL, POTLATCH2_EDITOR_URL } from '../config'; +import { ID_EDITOR_URL, POTLATCH2_EDITOR_URL, RAPID_EDITOR_URL } from '../config'; export function getEditors(filterList, customEditor) { let editors = [ + { + label: 'RapiD', + value: 'RAPID', + url: RAPID_EDITOR_URL, + }, { label: 'iD Editor', value: 'ID', @@ -24,7 +29,7 @@ export function getEditors(filterList, customEditor) { }, ]; if (filterList) { - editors = editors.filter(i => filterList.includes(i.value)); + editors = editors.filter((i) => filterList.includes(i.value)); } if (customEditor && filterList.includes('CUSTOM')) { editors.push({ label: customEditor.name, value: 'CUSTOM', url: customEditor.url }); diff --git a/frontend/src/utils/openEditor.js b/frontend/src/utils/openEditor.js index 5c8aa7a599..e490658a81 100644 --- a/frontend/src/utils/openEditor.js +++ b/frontend/src/utils/openEditor.js @@ -14,8 +14,8 @@ export function openEditor( return '?editor=JOSM'; } const { center, zoom } = getCentroidAndZoomFromSelectedTasks(tasks, selectedTasks, windowSize); - if (editor === 'ID') { - return getIdUrl(project, center, zoom, selectedTasks, '?editor=ID'); + if (['ID', 'RAPID'].includes(editor)) { + return getIdUrl(project, center, zoom, selectedTasks, ('?editor=' + editor)); } if (windowObjectReference == null || windowObjectReference.closed) { windowObjectReference = window.open('', `iD-${project}-${selectedTasks}`); @@ -76,7 +76,7 @@ export function getIdUrl(project, centroid, zoomLevel, selectedTasks, customUrl) const base = customUrl ? formatCustomUrl(customUrl) : `${ID_EDITOR_URL}`; let url = base + '#map=' + [zoomLevel, centroid[1], centroid[0]].join('/'); // the other URL params are only needed by external iD editors - if (customUrl !== '?editor=ID') { + if (!['?editor=ID','?editor=RAPID'].includes(customUrl)) { if (project.changesetComment) { url += '&comment=' + encodeURIComponent(project.changesetComment); } diff --git a/frontend/src/utils/tests/editorsList.test.js b/frontend/src/utils/tests/editorsList.test.js index 3e50076299..edc12e9024 100644 --- a/frontend/src/utils/tests/editorsList.test.js +++ b/frontend/src/utils/tests/editorsList.test.js @@ -3,6 +3,11 @@ import { getEditors } from '../editorsList'; describe('test getEditors', () => { it('without filterList and without customEditor', () => { expect(getEditors()).toStrictEqual([ + { + label: 'RapiD', + value: 'RAPID', + url: 'https://mapwith.ai/rapid', + }, { label: 'iD Editor', value: 'ID', diff --git a/frontend/src/views/projectEdit.js b/frontend/src/views/projectEdit.js index 57bca563b6..8a86ff0891 100644 --- a/frontend/src/views/projectEdit.js +++ b/frontend/src/views/projectEdit.js @@ -75,6 +75,7 @@ export default function ProjectEdit({ id }) { perTaskInstructions: '', }, ], + rapidPowerUser: false, }); const [userCanEditProject] = useEditProjectAllowed(projectInfo); const supportedLanguages = diff --git a/frontend/yarn.lock b/frontend/yarn.lock index f3a2ea9511..0c4efe8fc3 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2433,6 +2433,18 @@ geojson-precision "^1.0.0" polygon-clipping "~0.15.3" +"@ideditor/location-conflation@~0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@ideditor/location-conflation/-/location-conflation-0.8.0.tgz#a3eb656be1ffca87d247c145105387b48c4f11a8" + integrity sha512-5hwNffKTf/0Hi8QrJ5Xj6qDhQEHCu4bgCGCD0GgRaRtgaA4HPCqcID9XFxuEVlcBwi7rgwECrkG7oxLhKeBKQQ== + dependencies: + "@aitodotai/json-stringify-pretty-compact" "^1.3.0" + "@ideditor/country-coder" "^4.0.0" + "@mapbox/geojson-area" "^0.2.2" + circle-to-polygon "^2.0.2" + geojson-precision "^1.0.0" + polygon-clipping "~0.15.1" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -4007,6 +4019,38 @@ JSONStream@0.8.0: jsonparse "0.0.5" through "~2.2.7" +RapiD@facebookincubator/rapid#rapid-v1.1.4: + version "1.1.4" + resolved "https://codeload.github.com/facebookincubator/rapid/tar.gz/452fc17b37e7efd517f426eea4cc36d956aff3cd" + dependencies: + "@ideditor/country-coder" "^4.0.0" + "@ideditor/location-conflation" "~0.8.0" + "@mapbox/geojson-area" "^0.2.2" + "@mapbox/geojson-rewind" "^0.5.0" + "@mapbox/sexagesimal" "1.2.0" + "@mapbox/togeojson" "0.16.0" + "@mapbox/vector-tile" "^1.3.1" + "@turf/bbox-clip" "^6.0.0" + abortcontroller-polyfill "^1.4.0" + aes-js "^3.1.2" + alif-toolkit "^1.2.9" + core-js "^3.6.5" + diacritics "1.3.0" + fast-deep-equal "~3.1.1" + fast-json-stable-stringify "2.1.0" + lodash-es "~4.17.15" + marked "~2.0.0" + node-diff3 "~2.1.1" + osm-auth "1.1.0" + pannellum "2.5.6" + polygon-clipping "~0.15.1" + prop-types "^15.7.2" + rbush "3.0.1" + react "^17.0.1" + react-dom "^17.0.1" + whatwg-fetch "^3.4.1" + which-polygon "2.2.0" + abab@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -9896,6 +9940,15 @@ marked@~2.0.0: resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.7.tgz#bc5b857a09071b48ce82a1f7304913a993d4b7d1" integrity sha512-BJXxkuIfJchcXOJWTT2DOL+yFWifFv2yGYOUzvXg8Qz610QKw+sHCvTMYwA+qWGhlA2uivBezChZ/pBy1tWdkQ== +martinez-polygon-clipping@0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/martinez-polygon-clipping/-/martinez-polygon-clipping-0.7.0.tgz#5ae979d4ba32c6425c8cdd422f14d54276350ab7" + integrity sha512-EBxKjlUqrVjzT1HRwJARaSwj66JZqEUl+JnqnrzHZLU4hd4XrCQWqShZx40264NR/pm5wIHRlNEaIrev44wvKA== + dependencies: + robust-predicates "^2.0.4" + splaytree "^0.1.4" + tinyqueue "^1.2.0" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -10360,6 +10413,11 @@ node-diff3@2.1.0: resolved "https://registry.yarnpkg.com/node-diff3/-/node-diff3-2.1.0.tgz#6ab59613943db919e6e4a65a077bcd8c40239caf" integrity sha512-m04x9YCoimejmtDeG33RyEiSY+o1oMqavIQC27xprayVxcdKRZyv0m5CY3yVrkhnv0Hg/vTw6AWtJ/0xmI2CLg== +node-diff3@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/node-diff3/-/node-diff3-2.1.2.tgz#72f6f899b608894920ccaacddcf33cc38e459bf8" + integrity sha512-0BsMH8yqNG72kqPranIv0i9B4nXyzQ22HWcea6MBJ2/jBnLSLc9+NrqzS0UFV3TRIRtUfcPR0fUbA/AwrWIzBg== + node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" @@ -12307,7 +12365,7 @@ react-dom@^16.9.0: prop-types "^15.6.2" scheduler "^0.19.1" -react-dom@^17.0.2: +react-dom@^17.0.1, react-dom@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== @@ -12557,7 +12615,7 @@ react@^16.9.0: object-assign "^4.1.1" prop-types "^15.6.2" -react@^17.0.2: +react@^17.0.1, react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== diff --git a/migrations/versions/8a6419f289aa_.py b/migrations/versions/8a6419f289aa_.py new file mode 100644 index 0000000000..ee4ba37750 --- /dev/null +++ b/migrations/versions/8a6419f289aa_.py @@ -0,0 +1,40 @@ +"""empty message + +Revision ID: 8a6419f289aa +Revises: 2ee4bca188a9 +Create Date: 2021-06-23 17:11:22.666814 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "8a6419f289aa" +down_revision = "2ee4bca188a9" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "projects", sa.Column("rapid_power_user", sa.Boolean(), nullable=True) + ) + op.drop_index( + "idx_task_validation_validator_status_composite", + table_name="task_invalidation_history", + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_index( + "idx_task_validation_validator_status_composite", + "task_invalidation_history", + ["invalidator_id", "is_closed"], + unique=False, + ) + op.drop_column("projects", "rapid_power_user") + # ### end Alembic commands ### From 03c8473e8441ec78b827a4c78f75af6be40347f8 Mon Sep 17 00:00:00 2001 From: Zack LaVergne Date: Mon, 20 Dec 2021 17:30:45 -0700 Subject: [PATCH 02/29] Upgrade RapiD to v.1.1.8 --- frontend/package.json | 2 +- frontend/src/components/rapidEditor.js | 2 +- frontend/yarn.lock | 387 ++++++++++++++++++++++--- 3 files changed, 349 insertions(+), 42 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 04da898669..5abee321bf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,7 +36,7 @@ "final-form": "^4.20.2", "fromentries": "^1.3.2", "humanize-duration": "^3.27.0", - "RapiD": "facebookincubator/rapid#rapid-v1.1.4", + "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", diff --git a/frontend/src/components/rapidEditor.js b/frontend/src/components/rapidEditor.js index 3e55d04068..fcb7e3e9a0 100644 --- a/frontend/src/components/rapidEditor.js +++ b/frontend/src/components/rapidEditor.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import * as RapiD from 'RapiD'; +import * as RapiD from 'RapiD/dist/iD.legacy'; import 'RapiD/dist/iD.css'; import { OSM_CONSUMER_KEY, OSM_CONSUMER_SECRET, OSM_SERVER_URL } from '../config'; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 0c4efe8fc3..fda7c2f8ca 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2414,6 +2414,77 @@ dependencies: prettier "^2.0.5" +"@id-sdk/extent@^3.0.0-pre.10": + version "3.0.0-pre.10" + resolved "https://registry.yarnpkg.com/@id-sdk/extent/-/extent-3.0.0-pre.10.tgz#f48b72dc987f145842ca42a69b0df066d379df11" + integrity sha512-6UEUwQfoyE57REUrg7mNGiT76ySYgUMXl1jDEMvrs7hDq4e9VyhgFITU5zZVi6m/Mqu4EViYdnVK5a83r3vyWw== + dependencies: + "@id-sdk/geo" "^3.0.0-pre.10" + "@id-sdk/vector" "^3.0.0-pre.10" + +"@id-sdk/geo@^3.0.0-pre.10": + version "3.0.0-pre.10" + resolved "https://registry.yarnpkg.com/@id-sdk/geo/-/geo-3.0.0-pre.10.tgz#6a8aa4d870d43bed5d778fd2971c7700d538887c" + integrity sha512-JWVOpGKGUDObDZ+BxLfpdZD5WdPe5XUwkkP+2GYUJMKSP03k6yJ740VbwApUS2js/5J1OleR9K8E+7EiYXdJ5g== + dependencies: + "@id-sdk/vector" "^3.0.0-pre.10" + +"@id-sdk/geom@^3.0.0-pre.10": + version "3.0.0-pre.10" + resolved "https://registry.yarnpkg.com/@id-sdk/geom/-/geom-3.0.0-pre.10.tgz#9c13242944bdcf6d54735ca80861b2b4856c5aea" + integrity sha512-4xj/cQu6c14ErWQ/edF64FAWlQeChTkMi1lCZRzqrucuUymi0BrBhYGbeQgz6co90aD8d/QX6ZJepbEgPIS1KQ== + dependencies: + "@id-sdk/extent" "^3.0.0-pre.10" + "@id-sdk/vector" "^3.0.0-pre.10" + "@types/d3-polygon" "^3.0.0" + d3-polygon "^3.0.1" + +"@id-sdk/math@~3.0.0-pre.10": + version "3.0.0-pre.10" + resolved "https://registry.yarnpkg.com/@id-sdk/math/-/math-3.0.0-pre.10.tgz#22c7d9e017ff2c02539a6be1bc9a9a793fd84eef" + integrity sha512-7bFv6D3/fkKD/+IhTZXdeg7Dl3zAlS/32jB8cUvsSl24RZC0TG4iCin4e6DtBg/92+N3x5glLjJw60l63Adl5w== + dependencies: + "@id-sdk/extent" "^3.0.0-pre.10" + "@id-sdk/geo" "^3.0.0-pre.10" + "@id-sdk/geom" "^3.0.0-pre.10" + "@id-sdk/projection" "^3.0.0-pre.10" + "@id-sdk/tiler" "^3.0.0-pre.10" + "@id-sdk/vector" "^3.0.0-pre.10" + +"@id-sdk/projection@^3.0.0-pre.10": + version "3.0.0-pre.10" + resolved "https://registry.yarnpkg.com/@id-sdk/projection/-/projection-3.0.0-pre.10.tgz#a4fb8b59678dbe178e886e87d2dd481af456d7df" + integrity sha512-xidjW7roaVezW411RvQTux+8wqTMD/5xlgClMRo5U1VIpLbGXu+ufEiSipMIbUq+n2o0s4UH2r1Ht6wgRnU46w== + dependencies: + "@id-sdk/vector" "^3.0.0-pre.10" + "@types/d3-geo" "^3.0.2" + "@types/d3-zoom" "^3.0.1" + d3-geo "^3.0.1" + d3-zoom "^3.0.0" + +"@id-sdk/tiler@^3.0.0-pre.10": + version "3.0.0-pre.10" + resolved "https://registry.yarnpkg.com/@id-sdk/tiler/-/tiler-3.0.0-pre.10.tgz#c5891278caf759fb1c5d552b4651629eb9d395fc" + integrity sha512-Mr41pkmFVutLH1ozg7DEOYE+ROaOh6OpSUFbUmRWa3H0QkXf5QyxMgN/PaGsUR5uasZHtA6CZ/vV8bwtZpWjOQ== + dependencies: + "@id-sdk/extent" "^3.0.0-pre.10" + "@id-sdk/geo" "^3.0.0-pre.10" + "@id-sdk/projection" "^3.0.0-pre.10" + "@id-sdk/vector" "^3.0.0-pre.10" + +"@id-sdk/util@~3.0.0-pre.10": + version "3.0.0-pre.10" + resolved "https://registry.yarnpkg.com/@id-sdk/util/-/util-3.0.0-pre.10.tgz#c9f9056f6de1cadd12320f6571b292a7a56189fa" + integrity sha512-jO5mqVwqM9DwnhhFLqSITLYoBnGAQ1YG/L33dRE2VD1MnlupkS9idcZvIqLbvJ+ts6e48g52bVwHqeBq8JLNcA== + dependencies: + aes-js "^3.1.2" + diacritics "^1.3.0" + +"@id-sdk/vector@^3.0.0-pre.10": + version "3.0.0-pre.10" + resolved "https://registry.yarnpkg.com/@id-sdk/vector/-/vector-3.0.0-pre.10.tgz#81801bc2288ad75587be22d41afbbf5982b7f0ea" + integrity sha512-qU3jOo/48EgYH8DaOjnWzYwiRAU9sbALbQAIBjJFqJEwXnpOhkezXxGXHqlpmGG9BDLO7Ecps2FAwRTXqzUtBw== + "@ideditor/country-coder@^5.0.3", "@ideditor/country-coder@~5.0.3": version "5.0.3" resolved "https://registry.yarnpkg.com/@ideditor/country-coder/-/country-coder-5.0.3.tgz#79e14880cb87273f0c50891d5c6427bccc4a2de2" @@ -2433,18 +2504,6 @@ geojson-precision "^1.0.0" polygon-clipping "~0.15.3" -"@ideditor/location-conflation@~0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@ideditor/location-conflation/-/location-conflation-0.8.0.tgz#a3eb656be1ffca87d247c145105387b48c4f11a8" - integrity sha512-5hwNffKTf/0Hi8QrJ5Xj6qDhQEHCu4bgCGCD0GgRaRtgaA4HPCqcID9XFxuEVlcBwi7rgwECrkG7oxLhKeBKQQ== - dependencies: - "@aitodotai/json-stringify-pretty-compact" "^1.3.0" - "@ideditor/country-coder" "^4.0.0" - "@mapbox/geojson-area" "^0.2.2" - circle-to-polygon "^2.0.2" - geojson-precision "^1.0.0" - polygon-clipping "~0.15.1" - "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -3443,6 +3502,43 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== +"@types/d3-color@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.0.2.tgz#53f2d6325f66ee79afd707c05ac849e8ae0edbb0" + integrity sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ== + +"@types/d3-geo@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.0.2.tgz#e7ec5f484c159b2c404c42d260e6d99d99f45d9a" + integrity sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ== + dependencies: + "@types/geojson" "*" + +"@types/d3-interpolate@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc" + integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw== + dependencies: + "@types/d3-color" "*" + +"@types/d3-polygon@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.0.tgz#5200a3fa793d7736fa104285fa19b0dbc2424b93" + integrity sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw== + +"@types/d3-selection@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.2.tgz#23e48a285b24063630bbe312cc0cfe2276de4a59" + integrity sha512-d29EDd0iUBrRoKhPndhDY6U/PYxOWqgIZwKTooy2UkBfU7TNZNpRho0yLWPxlatQrFWk2mnTu71IZQ4+LRgKlQ== + +"@types/d3-zoom@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.1.tgz#4bfc7e29625c4f79df38e2c36de52ec3e9faf826" + integrity sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + "@types/eslint@^7.2.6": version "7.2.6" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.6.tgz#5e9aff555a975596c03a98b59ecd103decc70c3c" @@ -3471,6 +3567,11 @@ resolved "https://registry.yarnpkg.com/@types/format-util/-/format-util-1.0.1.tgz#6fd9a41194776e2f33e2846ae955da8dbc8af4d1" integrity sha512-uqU+S8ZeHlEslQgMzD6fpYhZlCqMfWR8MqoIMHy4uEy6VIkFqtZC25GKhl9n7fGmL0BZ51xrB5Ak8Wkb9oKw3A== +"@types/geojson@*": + version "7946.0.8" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca" + integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA== + "@types/geojson@^1.0.2": version "1.0.6" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-1.0.6.tgz#3e02972728c69248c2af08d60a48cbb8680fffdf" @@ -4019,30 +4120,30 @@ JSONStream@0.8.0: jsonparse "0.0.5" through "~2.2.7" -RapiD@facebookincubator/rapid#rapid-v1.1.4: - version "1.1.4" - resolved "https://codeload.github.com/facebookincubator/rapid/tar.gz/452fc17b37e7efd517f426eea4cc36d956aff3cd" +RapiD@facebookincubator/rapid#rapid-v1.1.8-TM: + version "1.1.8" + resolved "https://codeload.github.com/facebookincubator/rapid/tar.gz/47897f881279d46779ce89545cb42811e5ab857e" dependencies: - "@ideditor/country-coder" "^4.0.0" - "@ideditor/location-conflation" "~0.8.0" + "@id-sdk/math" "~3.0.0-pre.10" + "@id-sdk/util" "~3.0.0-pre.10" + "@ideditor/country-coder" "~5.0.3" + "@ideditor/location-conflation" "~1.0.2" "@mapbox/geojson-area" "^0.2.2" - "@mapbox/geojson-rewind" "^0.5.0" "@mapbox/sexagesimal" "1.2.0" - "@mapbox/togeojson" "0.16.0" "@mapbox/vector-tile" "^1.3.1" + "@tmcw/togeojson" "^4.5.0" "@turf/bbox-clip" "^6.0.0" abortcontroller-polyfill "^1.4.0" - aes-js "^3.1.2" alif-toolkit "^1.2.9" - core-js "^3.6.5" - diacritics "1.3.0" + esbuild "^0.13.9" fast-deep-equal "~3.1.1" fast-json-stable-stringify "2.1.0" lodash-es "~4.17.15" - marked "~2.0.0" - node-diff3 "~2.1.1" - osm-auth "1.1.0" + marked "~3.0.8" + node-diff3 "~3.1.0" + osm-auth "1.1.1" pannellum "2.5.6" + pbf "^3.2.1" polygon-clipping "~0.15.1" prop-types "^15.7.2" rbush "3.0.1" @@ -6063,6 +6164,87 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= +"d3-array@2.5.0 - 3": + version "3.1.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.1.1.tgz#7797eb53ead6b9083c75a45a681e93fc41bc468c" + integrity sha512-33qQ+ZoZlli19IFiQx4QEpf2CBEayMRzhlisJHSCsSUbDXv6ZishqS1x7uFVClKG4Wr7rZVHvaAttoLow6GqdQ== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a" + integrity sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw== + +"d3-dispatch@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + +"d3-drag@2 - 3": + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + +"d3-ease@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +d3-geo@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.0.1.tgz#4f92362fd8685d93e3b1fae0fd97dc8980b1ed7e" + integrity sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA== + dependencies: + d3-array "2.5.0 - 3" + +"d3-interpolate@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-polygon@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398" + integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== + +"d3-selection@2 - 3", d3-selection@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + +"d3-timer@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +"d3-transition@2 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + +d3-zoom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" @@ -6287,7 +6469,7 @@ detect-port-alt@1.1.6: address "^1.0.1" debug "^2.6.0" -diacritics@1.3.0: +diacritics@1.3.0, diacritics@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" integrity sha1-PvqHMj67hj5mls67AILUj/PW96E= @@ -6724,6 +6906,114 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.3: d "^1.0.1" ext "^1.1.2" +esbuild-android-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz#3fc3ff0bab76fe35dd237476b5d2b32bb20a3d44" + integrity sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg== + +esbuild-darwin-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz#8e9169c16baf444eacec60d09b24d11b255a8e72" + integrity sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ== + +esbuild-darwin-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz#1b07f893b632114f805e188ddfca41b2b778229a" + integrity sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ== + +esbuild-freebsd-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz#0b8b7eca1690c8ec94c75680c38c07269c1f4a85" + integrity sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA== + +esbuild-freebsd-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz#2e1a6c696bfdcd20a99578b76350b41db1934e52" + integrity sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ== + +esbuild-linux-32@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz#6fd39f36fc66dd45b6b5f515728c7bbebc342a69" + integrity sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g== + +esbuild-linux-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz#9cb8e4bcd7574e67946e4ee5f1f1e12386bb6dd3" + integrity sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA== + +esbuild-linux-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz#3891aa3704ec579a1b92d2a586122e5b6a2bfba1" + integrity sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA== + +esbuild-linux-arm@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz#8a00e99e6a0c6c9a6b7f334841364d8a2b4aecfe" + integrity sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA== + +esbuild-linux-mips64le@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz#36b07cc47c3d21e48db3bb1f4d9ef8f46aead4f7" + integrity sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg== + +esbuild-linux-ppc64le@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz#f7e6bba40b9a11eb9dcae5b01550ea04670edad2" + integrity sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ== + +esbuild-netbsd-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz#a2fedc549c2b629d580a732d840712b08d440038" + integrity sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w== + +esbuild-openbsd-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz#b22c0e5806d3a1fbf0325872037f885306b05cd7" + integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g== + +esbuild-sunos-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz#d0b6454a88375ee8d3964daeff55c85c91c7cef4" + integrity sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw== + +esbuild-windows-32@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz#c96d0b9bbb52f3303322582ef8e4847c5ad375a7" + integrity sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw== + +esbuild-windows-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz#1f79cb9b1e1bb02fb25cd414cb90d4ea2892c294" + integrity sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ== + +esbuild-windows-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz#482173070810df22a752c686509c370c3be3b3c3" + integrity sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA== + +esbuild@^0.13.9: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.15.tgz#db56a88166ee373f87dbb2d8798ff449e0450cdf" + integrity sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw== + optionalDependencies: + esbuild-android-arm64 "0.13.15" + esbuild-darwin-64 "0.13.15" + esbuild-darwin-arm64 "0.13.15" + esbuild-freebsd-64 "0.13.15" + esbuild-freebsd-arm64 "0.13.15" + esbuild-linux-32 "0.13.15" + esbuild-linux-64 "0.13.15" + esbuild-linux-arm "0.13.15" + esbuild-linux-arm64 "0.13.15" + esbuild-linux-mips64le "0.13.15" + esbuild-linux-ppc64le "0.13.15" + esbuild-netbsd-64 "0.13.15" + esbuild-openbsd-64 "0.13.15" + esbuild-sunos-64 "0.13.15" + esbuild-windows-32 "0.13.15" + esbuild-windows-64 "0.13.15" + esbuild-windows-arm64 "0.13.15" + escalade@^3.0.2, escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -8411,6 +8701,11 @@ internal-slot@^1.0.2: has "^1.0.3" side-channel "^1.0.2" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + intl-messageformat-parser@^3.6.4: version "3.6.4" resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-3.6.4.tgz#5199d106d816c3dda26ee0694362a9cf823978fb" @@ -9438,7 +9733,7 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= -jshashes@~1.0.0: +jshashes@~1.0.0, jshashes@~1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/jshashes/-/jshashes-1.0.8.tgz#f60d837428383abf73ab022e1542e6614bd75514" integrity sha512-btmQZ/w1rj8Lb6nEwvhjM7nBYoj54yaEFo2PWh3RkxZ8qNwuvOxvQYN/JxVuwoMmdIluL+XwYVJ+pEEZoSYybQ== @@ -9940,14 +10235,10 @@ marked@~2.0.0: resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.7.tgz#bc5b857a09071b48ce82a1f7304913a993d4b7d1" integrity sha512-BJXxkuIfJchcXOJWTT2DOL+yFWifFv2yGYOUzvXg8Qz610QKw+sHCvTMYwA+qWGhlA2uivBezChZ/pBy1tWdkQ== -martinez-polygon-clipping@0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/martinez-polygon-clipping/-/martinez-polygon-clipping-0.7.0.tgz#5ae979d4ba32c6425c8cdd422f14d54276350ab7" - integrity sha512-EBxKjlUqrVjzT1HRwJARaSwj66JZqEUl+JnqnrzHZLU4hd4XrCQWqShZx40264NR/pm5wIHRlNEaIrev44wvKA== - dependencies: - robust-predicates "^2.0.4" - splaytree "^0.1.4" - tinyqueue "^1.2.0" +marked@~3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/marked/-/marked-3.0.8.tgz#2785f0dc79cbdc6034be4bb4f0f0a396bd3f8aeb" + integrity sha512-0gVrAjo5m0VZSJb4rpL59K1unJAMb/hm8HRXqasD8VeC8m91ytDPMritgFSlKonfdt+rRYYpP/JfLxgIX8yoSw== md5.js@^1.3.4: version "1.3.5" @@ -10413,10 +10704,10 @@ node-diff3@2.1.0: resolved "https://registry.yarnpkg.com/node-diff3/-/node-diff3-2.1.0.tgz#6ab59613943db919e6e4a65a077bcd8c40239caf" integrity sha512-m04x9YCoimejmtDeG33RyEiSY+o1oMqavIQC27xprayVxcdKRZyv0m5CY3yVrkhnv0Hg/vTw6AWtJ/0xmI2CLg== -node-diff3@~2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/node-diff3/-/node-diff3-2.1.2.tgz#72f6f899b608894920ccaacddcf33cc38e459bf8" - integrity sha512-0BsMH8yqNG72kqPranIv0i9B4nXyzQ22HWcea6MBJ2/jBnLSLc9+NrqzS0UFV3TRIRtUfcPR0fUbA/AwrWIzBg== +node-diff3@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/node-diff3/-/node-diff3-3.1.0.tgz#df2c949447d3314e9d263bd6106768bd8cfcb722" + integrity sha512-Po+8MaiOAJkT3YJZPiBEszjpxs29mzIQtk2Idi+Xl0x8WIhkg2Rcinhy/wi8r2YMyPTd6IH86seUQAfj1USPiA== node-fetch@^2.6.1: version "2.6.1" @@ -10710,6 +11001,13 @@ ohauth@~1.0.0: jshashes "~1.0.0" xtend "~4.0.0" +ohauth@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ohauth/-/ohauth-1.0.1.tgz#a5d4ab8e5390bb1cad68a58cc9c58630173c02f2" + integrity sha512-R9ZUN3+FVCwzeOOHCJpzA9jw/byRxp5O9X06mTL6Sp/LIQn/rLrMv6cwYctX+hoIKzRUsalGJXZ1kG5wBmSskQ== + dependencies: + jshashes "~1.0.8" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -10832,6 +11130,15 @@ osm-auth@1.1.0: store "~2.0.4" xtend "~4.0.0" +osm-auth@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/osm-auth/-/osm-auth-1.1.1.tgz#2a6be41fb45998d9276b879154f8b86628de17d7" + integrity sha512-U2Q7wvjcLJSi7P1a4JKIgTSvWlVq2rqCsjYLHX/cMeIuiMDM8y0B/LQCQyvM7KKJa1juk3Do35dhlwPGxtxc/Q== + dependencies: + ohauth "~1.0.1" + resolve-url "~0.2.1" + store "~2.0.12" + osm-polygon-features@^0.9.1: version "0.9.2" resolved "https://registry.yarnpkg.com/osm-polygon-features/-/osm-polygon-features-0.9.2.tgz#20ae41130c486e49a3b2a3c2b58a1419c4986778" @@ -13870,7 +14177,7 @@ stealthy-require@^1.1.1: resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= -store@~2.0.4: +store@~2.0.12, store@~2.0.4: version "2.0.12" resolved "https://registry.yarnpkg.com/store/-/store-2.0.12.tgz#8c534e2a0b831f72b75fc5f1119857c44ef5d593" integrity sha1-jFNOKguDH3K3X8XxEZhXxE711ZM= From 257316bbdf192c3e2f63118162ee846b65ff192c Mon Sep 17 00:00:00 2001 From: Dakota Benjamin Date: Fri, 11 Feb 2022 14:05:56 -0500 Subject: [PATCH 03/29] Clean up unnecessary comments --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b91fab08e2..f0994c9032 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: From ebc403a0c4309237799bf3e1e89fa490764cb89e Mon Sep 17 00:00:00 2001 From: Zack LaVergne Date: Mon, 28 Feb 2022 09:25:51 -0700 Subject: [PATCH 04/29] Make sure reset all bad imagery only resets for tasks in the specified project. --- backend/services/mapping_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/services/mapping_service.py b/backend/services/mapping_service.py index 11c97e2e42..4535e594f7 100644 --- a/backend/services/mapping_service.py +++ b/backend/services/mapping_service.py @@ -369,7 +369,8 @@ def map_all_tasks(project_id: int, user_id: int): def reset_all_badimagery(project_id: int, user_id: int): """ 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: From fc7a25d3ca4589cfc3f31785a44aa6f3fc017b50 Mon Sep 17 00:00:00 2001 From: Zack LaVergne Date: Mon, 28 Feb 2022 09:26:11 -0700 Subject: [PATCH 05/29] Add test for reset_all_bad_imagery and fix a few other tests --- tests/backend/helpers/test_helpers.py | 9 ++++++++- .../integration/models/postgis/test_project.py | 2 +- .../integration/services/test_mapping_service.py | 15 +++++++++++++++ .../services/messaging/test_template_service.py | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/tests/backend/helpers/test_helpers.py b/tests/backend/helpers/test_helpers.py index e67da6cb8f..9e3e769bf7 100644 --- a/tests/backend/helpers/test_helpers.py +++ b/tests/backend/helpers/test_helpers.py @@ -89,6 +89,7 @@ def create_canned_project() -> Tuple[Project, User]: task_non_square_feature = geojson.loads( json.dumps(get_canned_json("non_square_task.json")) ) + task_arbitrary_feature = geojson.loads(json.dumps(get_canned_json("splittable_task.json"))) test_user = get_canned_user("Thinkwhere TEST") if test_user is None: test_user = create_canned_user() @@ -100,7 +101,7 @@ def create_canned_project() -> Tuple[Project, User]: test_project = Project() test_project.create_draft_project(test_project_dto) test_project.set_project_aoi(test_project_dto) - test_project.total_tasks = 2 + test_project.total_tasks = 3 # Setup test task test_task = Task.from_geojson_feature(1, task_feature) @@ -112,8 +113,14 @@ def create_canned_project() -> Tuple[Project, User]: test_task2.task_status = TaskStatus.READY.value test_task2.is_square = False + test_task3 = Task.from_geojson_feature(3, task_arbitrary_feature) + test_task3.task_status = TaskStatus.BADIMAGERY.value + test_task3.mapped_by = test_user.id + test_task3.is_square = True + test_project.tasks.append(test_task) test_project.tasks.append(test_task2) + test_project.tasks.append(test_task3) test_project.create() return test_project, test_user diff --git a/tests/backend/integration/models/postgis/test_project.py b/tests/backend/integration/models/postgis/test_project.py index 6248726dc3..36e7981de4 100644 --- a/tests/backend/integration/models/postgis/test_project.py +++ b/tests/backend/integration/models/postgis/test_project.py @@ -34,7 +34,7 @@ def test_task_can_generate_valid_feature_collection(self): self.test_project.id, None ) self.assertIsInstance(feature_collection, geojson.FeatureCollection) - self.assertEqual(2, len(feature_collection.features)) + self.assertEqual(3, len(feature_collection.features)) def test_project_can_be_generated_as_dto(self): self.test_project, self.test_user = create_canned_project() diff --git a/tests/backend/integration/services/test_mapping_service.py b/tests/backend/integration/services/test_mapping_service.py index b09ede0ee1..63d0caee1b 100644 --- a/tests/backend/integration/services/test_mapping_service.py +++ b/tests/backend/integration/services/test_mapping_service.py @@ -2,6 +2,7 @@ import hashlib from unittest.mock import patch from backend.services.mapping_service import MappingService, Task +from backend.models.postgis.task import TaskStatus from tests.backend.base import BaseTestCase from tests.backend.helpers.test_helpers import create_canned_project @@ -114,3 +115,17 @@ def test_mapped_by_is_set_after_mapping_all(self): # Assert for task in self.test_project.tasks: self.assertIsNotNone(task.mapped_by) + + + def test_reset_all_bad_imagery( + self, + ): + if self.skip_tests: + return + + # Act + MappingService.reset_all_badimagery(self.test_project.id, self.test_user.id) + + # Assert + for task in self.test_project.tasks: + self.assertNotEqual(task.task_status, TaskStatus.BADIMAGERY.value) diff --git a/tests/backend/unit/services/messaging/test_template_service.py b/tests/backend/unit/services/messaging/test_template_service.py index af9815d27c..4d825a3367 100644 --- a/tests/backend/unit/services/messaging/test_template_service.py +++ b/tests/backend/unit/services/messaging/test_template_service.py @@ -42,7 +42,7 @@ def test_format_username_link(self): base_url = current_app.config["APP_BASE_URL"] self.assertEqual( format_username_link("try @[yo] @[us2]! [t](http://a.c)"), - f'try @yo @us2! [t](http://a.c)', + f'try @yo @us2! [t](http://a.c)', ) self.assertEqual( format_username_link( From 5b1b9240060a064909034e7b151c1bf9adb199e3 Mon Sep 17 00:00:00 2001 From: Zack LaVergne Date: Mon, 28 Feb 2022 09:37:36 -0700 Subject: [PATCH 06/29] formatting --- backend/services/mapping_service.py | 2 +- tests/backend/helpers/test_helpers.py | 4 +++- tests/backend/integration/services/test_mapping_service.py | 1 - .../backend/unit/services/messaging/test_template_service.py | 5 ++++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/services/mapping_service.py b/backend/services/mapping_service.py index 4535e594f7..b607ce99a2 100644 --- a/backend/services/mapping_service.py +++ b/backend/services/mapping_service.py @@ -370,7 +370,7 @@ def reset_all_badimagery(project_id: int, user_id: int): """ Marks all bad imagery tasks ready for mapping """ badimagery_tasks = Task.query.filter( Task.task_status == TaskStatus.BADIMAGERY.value, - Task.project_id == project_id + Task.project_id == project_id, ).all() for task in badimagery_tasks: diff --git a/tests/backend/helpers/test_helpers.py b/tests/backend/helpers/test_helpers.py index 9e3e769bf7..0d49d19c4a 100644 --- a/tests/backend/helpers/test_helpers.py +++ b/tests/backend/helpers/test_helpers.py @@ -89,7 +89,9 @@ def create_canned_project() -> Tuple[Project, User]: task_non_square_feature = geojson.loads( json.dumps(get_canned_json("non_square_task.json")) ) - task_arbitrary_feature = geojson.loads(json.dumps(get_canned_json("splittable_task.json"))) + task_arbitrary_feature = geojson.loads( + json.dumps(get_canned_json("splittable_task.json")) + ) test_user = get_canned_user("Thinkwhere TEST") if test_user is None: test_user = create_canned_user() diff --git a/tests/backend/integration/services/test_mapping_service.py b/tests/backend/integration/services/test_mapping_service.py index 63d0caee1b..799a1e3458 100644 --- a/tests/backend/integration/services/test_mapping_service.py +++ b/tests/backend/integration/services/test_mapping_service.py @@ -116,7 +116,6 @@ def test_mapped_by_is_set_after_mapping_all(self): for task in self.test_project.tasks: self.assertIsNotNone(task.mapped_by) - def test_reset_all_bad_imagery( self, ): diff --git a/tests/backend/unit/services/messaging/test_template_service.py b/tests/backend/unit/services/messaging/test_template_service.py index 4d825a3367..4562bc3f89 100644 --- a/tests/backend/unit/services/messaging/test_template_service.py +++ b/tests/backend/unit/services/messaging/test_template_service.py @@ -42,7 +42,10 @@ def test_format_username_link(self): base_url = current_app.config["APP_BASE_URL"] self.assertEqual( format_username_link("try @[yo] @[us2]! [t](http://a.c)"), - f'try @yo @us2! [t](http://a.c)', + ( + f'try @yo' + f' @us2! [t](http://a.c)' + ), ) self.assertEqual( format_username_link( From 877b42f04d1022676b2c3c62c6051a4ec55ee4be Mon Sep 17 00:00:00 2001 From: Zack LaVergne Date: Mon, 28 Feb 2022 17:32:23 -0700 Subject: [PATCH 07/29] update RapiD tag --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 5abee321bf..48887df0ce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,7 +36,7 @@ "final-form": "^4.20.2", "fromentries": "^1.3.2", "humanize-duration": "^3.27.0", - "RapiD": "facebookincubator/rapid#rapid-v1.1.8-TM", + "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", From 602828cf270e9244fa37415027fb9d03aab93680 Mon Sep 17 00:00:00 2001 From: petya-kangalova <98902727+petya-kangalova@users.noreply.github.com> Date: Thu, 3 Mar 2022 10:23:20 +0000 Subject: [PATCH 08/29] Update issue templates Suggesting to add two Issue templates that will help user in providing relevant information. --- .github/ISSUE_TEMPLATE/bug_report.md | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..022203aef7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +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] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. From 0dd890cb365ad3d67c2add55de9a0135d2c0d985 Mon Sep 17 00:00:00 2001 From: petya-kangalova <98902727+petya-kangalova@users.noreply.github.com> Date: Tue, 8 Mar 2022 14:11:06 +0000 Subject: [PATCH 09/29] Update issue templates- Feature I added a PR for the BUG template (https://github.com/hotosm/tasking-manager/pull/5026) and now adding for a FEATURE. --- .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md 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. From 5b99230ee26f788680ced008bb90f5ea8da80228 Mon Sep 17 00:00:00 2001 From: petya-kangalova <98902727+petya-kangalova@users.noreply.github.com> Date: Tue, 8 Mar 2022 15:08:49 +0000 Subject: [PATCH 10/29] Update bug_report.md @dakotabenjamin thanks for the comment on the smartphone. I have removed now. --- .github/ISSUE_TEMPLATE/bug_report.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 022203aef7..a90b3f8050 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -28,11 +28,5 @@ If applicable, add screenshots to help explain your problem. - Browser [e.g. chrome, safari] - Version [e.g. 22] -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - **Additional context** Add any other context about the problem here. From 6b56c5a478c02e363323e984213cb863a0d37852 Mon Sep 17 00:00:00 2001 From: petya-kangalova <98902727+petya-kangalova@users.noreply.github.com> Date: Wed, 9 Mar 2022 10:55:41 +0000 Subject: [PATCH 11/29] Updating contributing guidelines I have now simplified the labels list. As discussed, I have updated the contributing guidelines to reflect the changes and explain the different ways in which people can contribute. The plan is to keep this page up to date with any changes/updates as we continue the collaboration with the collective. --- docs/contributing.md | 51 +++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 20 deletions(-) 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)** From 140bbd95c9133ddc7b9206463a9fb73c2a67fe54 Mon Sep 17 00:00:00 2001 From: petya-kangalova <98902727+petya-kangalova@users.noreply.github.com> Date: Wed, 9 Mar 2022 14:03:19 +0000 Subject: [PATCH 12/29] Update READ.ME Simplified and updated the text. Removed the page referencing Working Group meetings @dakotabenjamin kept the developer section as is and we can update later on. On the instances, we can also update later on if there are other instances missing. --- README.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d3742ded9f..029c2cf13a 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,16 @@ [![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. - -[](./docs/assets/project-view.gif) +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. This is Free and Open Source Software. You are welcome to use the code and set up your own instance. The Tasking Manager has been initially designed and built by and for the [Humanitarian OpenStreetMap Team](https://www.hotosm.org/), and is nowadays used by many communities and organizations. ## 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 opportinity to meet other Tasking Manager contributors- details coming shortly! + ## Developers From 181b9bcc009809a820a611d879160bc22cd49995 Mon Sep 17 00:00:00 2001 From: Aadesh-Baral Date: Tue, 15 Mar 2022 11:58:15 +0545 Subject: [PATCH 13/29] fix typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 029c2cf13a..fedf16a3e2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This is Free and Open Source Software. You are welcome to use the code and set u * 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 opportinity to meet other Tasking Manager contributors- details coming shortly! +* Join the Tasking Manager Collective Meet up- an opportunity to meet other Tasking Manager contributors- details coming shortly! ## Developers From afaa382adb29c43e43852706e9c25ccb1cb5abfd Mon Sep 17 00:00:00 2001 From: Aadesh Baral <67958673+Aadesh-Baral@users.noreply.github.com> Date: Tue, 15 Mar 2022 16:32:35 +0545 Subject: [PATCH 14/29] Re-add images on Readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index fedf16a3e2..de59481154 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,12 @@ [![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) +

+ TM-screenshot.jpg +        + TM-project-view.gif +

+ 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. This is Free and Open Source Software. You are welcome to use the code and set up your own instance. The Tasking Manager has been initially designed and built by and for the [Humanitarian OpenStreetMap Team](https://www.hotosm.org/), and is nowadays used by many communities and organizations. From 99fad57debcf145e3cdd724dfae841eaa46188bc Mon Sep 17 00:00:00 2001 From: Aadesh Baral <67958673+Aadesh-Baral@users.noreply.github.com> Date: Tue, 15 Mar 2022 16:41:24 +0545 Subject: [PATCH 15/29] Revert "Update READ.ME" --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index de59481154..d3742ded9f 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,22 @@ [![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) -

- TM-screenshot.jpg -        - TM-project-view.gif -

+The most popular tool for teams to coordinate mapping on OpenStreetMap. -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. +[](./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. + +[](./docs/assets/project-view.gif) This is Free and Open Source Software. You are welcome to use the code and set up your own instance. The Tasking Manager has been initially designed and built by and for the [Humanitarian OpenStreetMap Team](https://www.hotosm.org/), and is nowadays used by many communities and organizations. ## Get involved! -* 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! - +* 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) ## Developers From 8cffe3420563cdb59170442e7ede14d08ebe993b Mon Sep 17 00:00:00 2001 From: Aadesh Baral <67958673+Aadesh-Baral@users.noreply.github.com> Date: Tue, 15 Mar 2022 17:52:58 +0545 Subject: [PATCH 16/29] Update Readme.md --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 From b3cf8776e177b883442ff75776f664e3d2d539c3 Mon Sep 17 00:00:00 2001 From: Dakota Benjamin Date: Fri, 18 Mar 2022 11:45:15 -0400 Subject: [PATCH 17/29] Move Codedeploy config to end of init script --- .circleci/config.yml | 4 ++-- scripts/aws/cloudformation/tasking-manager.template.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b91fab08e2..2d616a7101 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -252,7 +252,7 @@ workflows: filters: branches: only: - - fix/sentry-environments + - fix/cfn-init-codedeploy requires: - build stack_name: "test" @@ -263,7 +263,7 @@ workflows: filters: branches: only: - - fix/sentry-environments + - fix/cfn-init-codedeploy requires: - build context: tasking-manager-staging diff --git a/scripts/aws/cloudformation/tasking-manager.template.js b/scripts/aws/cloudformation/tasking-manager.template.js index cddc56a39c..10ce10e031 100644 --- a/scripts/aws/cloudformation/tasking-manager.template.js +++ b/scripts/aws/cloudformation/tasking-manager.template.js @@ -366,11 +366,6 @@ const Resources = { 'sudo apt-get -y install git', 'sudo apt-get -y install awscli', 'sudo apt-get -y install ruby', - 'pushd /home/ubuntu', - 'wget https://aws-codedeploy-us-east-1.s3.us-east-1.amazonaws.com/latest/install', - 'chmod +x ./install && sudo ./install auto', - 'sudo systemctl start codedeploy-agent', - 'popd', 'git clone --recursive https://github.com/hotosm/tasking-manager.git', 'cd tasking-manager/', cf.sub('git reset --hard ${GitSha}'), @@ -413,6 +408,11 @@ const Resources = { cf.if('DatabaseDumpFileGiven', cf.sub('aws s3 cp ${DatabaseDump} dump.sql; sudo -u postgres psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_ENDPOINT/$POSTGRES_DB" < dump.sql'), ''), './venv/bin/python3 manage.py db upgrade', 'echo "------------------------------------------------------------"', + 'pushd /home/ubuntu', + 'wget https://aws-codedeploy-us-east-1.s3.us-east-1.amazonaws.com/latest/install', + 'chmod +x ./install && sudo ./install auto', + 'sudo systemctl start codedeploy-agent', + 'popd', cf.sub('export NEW_RELIC_LICENSE_KEY="${NewRelicLicense}"'), cf.sub('export TM_SENTRY_BACKEND_DSN="${SentryBackendDSN}"'), 'export NEW_RELIC_ENVIRONMENT=$TM_ENVIRONMENT', From aa832fcc01fc5802eb1a7cc56197c473e0d3d67b Mon Sep 17 00:00:00 2001 From: Dakota Benjamin Date: Fri, 18 Mar 2022 12:45:46 -0400 Subject: [PATCH 18/29] Add wait time to init script --- scripts/aws/cloudformation/tasking-manager.template.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/aws/cloudformation/tasking-manager.template.js b/scripts/aws/cloudformation/tasking-manager.template.js index 10ce10e031..03b83569e7 100644 --- a/scripts/aws/cloudformation/tasking-manager.template.js +++ b/scripts/aws/cloudformation/tasking-manager.template.js @@ -340,6 +340,7 @@ const Resources = { UserData: cf.userData([ '#!/bin/bash', 'set -x', + 'sleep 60', 'export DEBIAN_FRONTEND=noninteractive', 'export LC_ALL="en_US.UTF-8"', 'export LC_CTYPE="en_US.UTF-8"', From 73cfb935a638c28a094155a4267afdeb66cd5b16 Mon Sep 17 00:00:00 2001 From: Hel Nershing Thapa Date: Tue, 15 Mar 2022 14:01:35 +0545 Subject: [PATCH 19/29] Add 'blocked' option to dropdown --- frontend/src/components/user/list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/user/list.js b/frontend/src/components/user/list.js index 1f6cddc965..4b05182d37 100644 --- a/frontend/src/components/user/list.js +++ b/frontend/src/components/user/list.js @@ -63,7 +63,7 @@ const UserFilter = ({ filters, setFilters, updateFilters, intl }) => { }; const RoleFilter = ({ filters, setFilters, updateFilters }) => { - const roles = ['ALL', 'MAPPER', 'ADMIN']; + const roles = ['ALL', 'MAPPER', 'ADMIN', 'READ_ONLY']; const options = roles.map((role) => { return { value: role, label: }; From dca6c7d85660c727ac192d7f425848832367739d Mon Sep 17 00:00:00 2001 From: Aadesh-Baral Date: Wed, 16 Mar 2022 14:36:17 +0545 Subject: [PATCH 20/29] set-default-permission-for-validation-to-intermediate-and-advanced --- backend/models/postgis/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/models/postgis/project.py b/backend/models/postgis/project.py index 334fcbe648..b3e3cbf638 100644 --- a/backend/models/postgis/project.py +++ b/backend/models/postgis/project.py @@ -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 From 66d69961dfeac635d00f1bc52a87a5d000be82d0 Mon Sep 17 00:00:00 2001 From: DK Benjamin Date: Thu, 24 Mar 2022 17:23:49 -0400 Subject: [PATCH 21/29] Enable OpsGenie webhook by fixing yaml syntax --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 805a4d7195..bb69055235 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -333,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 From 5b58ada85354176522b7dfc9e71d91ac2f7364b7 Mon Sep 17 00:00:00 2001 From: Hel Nershing Thapa Date: Mon, 21 Mar 2022 10:09:19 +0545 Subject: [PATCH 22/29] Add cheatsheet under manuals --- frontend/src/locales/en.json | 9 +++++ frontend/src/views/learn.js | 73 ++++++++++++++++++---------------- frontend/src/views/messages.js | 8 ++++ 3 files changed, 56 insertions(+), 34 deletions(-) diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index e7e3b00ab2..3bc995a957 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -4,6 +4,10 @@ "banner.button.agree": "I agree", "banner.privacyPolicy": "privacy policy", "banner.text": "We use cookies and similar technologies to recognize and analyze your visits, and measure traffic usage and activity. You can learn about how we use the data about your visit or information you provide reading our {link}. By clicking \"I Agree\", you consent to the use of cookies.", + "banner.button.close": "Close", + "banner.button.learnmore": "Learn More", + "banner.title.notification": "Important notification for users", + "banner.text.archivalNotification": "Attention: as part of the ongoing maintenance of the Tasking Manager, projects created before 2020 will be archived.", "comment.input.imageUpload.error": "The image upload failed.", "comment.input.imageUpload.progress": "Uploading file...", "comment.input.sending.progress": "Sending message...", @@ -406,6 +410,8 @@ "projects.formInputs.random_task_selection": "Enforce random task selection", "projects.formInputs.random_task_selection.mapping": "Enforce random task selection on mapping", "projects.formInputs.random_task_selection.description": "If checked, users must edit tasks at random for the initial editing stage (managers and admins are exempt).", + "projects.formInputs.rapid_power_user": "Enable RapiD Power User Features", + "projects.formInputs.rapid_power_user.description": "If checked, RapiD will load with the power user dialog enabled.", "projects.formInputs.imagery": "Imagery", "projects.formInputs.imagery.note": "Follow this format for TMS URLs: {exampleUrl}", "projects.formInputs.priority_areas.options.polygon": "Draw polygon", @@ -531,6 +537,7 @@ "project.stats.totalEdits": "Total map edits", "project.stats.changesets": "Changesets", "project.stats.edits": "Edits", + "project.stats.edits.info": "These stats are retrieved using the default changeset comment of the project", "project.tasks.unsaved_map_changes.title": "You have some unsaved map changes", "project.tasks.unsaved_map_changes.split": "Save or undo it to be able to split the task", "project.tasks.unsaved_map_changes.unlock": "Save or undo it to be able to select another task", @@ -988,6 +995,8 @@ "pages.learn.tutorials.tm_manual.description": "Learn how to find a project and task that interests you, how to lock a task to edit and select the suitable editing software.", "pages.learn.tutorials.osm_step_by_step.title": "Learn OpenStreetMap Step by Step", "pages.learn.tutorials.osm_step_by_step.description": "Beginner’s Guide to mapping on OpenStreetMap", + "pages.learn.tutorials.cheatsheet.title": "Tasking Manager Cheatsheet", + "pages.learn.tutorials.cheatsheet.description": "TM's concise set of notes used for quick reference", "pages.learn.tutorials.learnosm.title": "Administration Guide", "pages.learn.tutorials.learnosm.description": "Manual on how to create and manage projects in the Tasking Manager", "pages.learn.map.steps.project.title": "Select a project", diff --git a/frontend/src/views/learn.js b/frontend/src/views/learn.js index 3ea9f50b4d..08dcc07038 100644 --- a/frontend/src/views/learn.js +++ b/frontend/src/views/learn.js @@ -88,43 +88,43 @@ const Steps = ({ items }) => ( ); const Manuals = ({ contents }) => ( -
-

- -

-
- {contents.map((content, i) => ( - -); + ); const Videos = ({ contents }) => { const [activeVideo, setActiveVideo] = useState(null); @@ -348,6 +348,11 @@ const LearnToMap = ({ section }) => { url: 'https://learnosm.org/en/beginner/', img: LearnOSMLogo, }, + { + message: 'learnTMCheatsheet', + url: 'https://drive.google.com/file/d/19pckU4Cru-cSz_aclsLsBk-45SQ1Qyy_/view?usp=sharing', + img: QuickstartLogo, + }, ]; const videos = [ diff --git a/frontend/src/views/messages.js b/frontend/src/views/messages.js index 9539407488..f54352fb4b 100644 --- a/frontend/src/views/messages.js +++ b/frontend/src/views/messages.js @@ -296,6 +296,14 @@ export default defineMessages({ id: 'pages.learn.tutorials.osm_step_by_step.description', defaultMessage: 'Beginner’s Guide to mapping on OpenStreetMap', }, + learnTMCheatsheetTitle: { + id: 'pages.learn.tutorials.cheatsheet.title', + defaultMessage: 'Tasking Manager Cheatsheet', + }, + learnTMCheatsheetDescription: { + id: 'pages.learn.tutorials.cheatsheet.description', + defaultMessage: "TM's concise set of notes used for quick reference", + }, learnOSMTutorialTitle: { id: 'pages.learn.tutorials.learnosm.title', defaultMessage: 'Administration Guide', From 155207f18ed89fcb5906d35a6318ebfc6e2c36e7 Mon Sep 17 00:00:00 2001 From: ramyaragupathy Date: Wed, 2 Mar 2022 21:11:16 +0530 Subject: [PATCH 23/29] #5017 Support link added --- frontend/src/components/header/index.js | 13 ++++++++++++- frontend/src/components/header/messages.js | 4 ++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/header/index.js b/frontend/src/components/header/index.js index 05001054fa..caf50cc4f0 100644 --- a/frontend/src/components/header/index.js +++ b/frontend/src/components/header/index.js @@ -5,7 +5,7 @@ import Popup from 'reactjs-popup'; import { FormattedMessage } from 'react-intl'; import messages from './messages'; -import { ORG_URL, ORG_NAME, ORG_LOGO } from '../../config'; +import { ORG_URL, ORG_NAME, ORG_LOGO, SERVICE_DESK } from '../../config'; import logo from '../../assets/img/main-logo.svg'; import { ExternalLinkIcon } from '../svgIcons'; import { Dropdown } from '../dropdown'; @@ -33,6 +33,7 @@ function getMenuItensForUser(userDetails, organisations) { { label: messages.manage, link: 'manage', authenticated: true, manager: true }, { label: messages.learn, link: 'learn', showAlways: true }, { label: messages.about, link: 'about', showAlways: true }, + { label: messages.support, link: SERVICE_DESK, showAlways: true, serviceDesk: true }, ]; let filteredMenuItems; if (userDetails.username) { @@ -114,6 +115,16 @@ const PopupItems = (props) => {

))} + + {props.menuItems + .filter((item) => item.serviceDesk === true) + .map((item, n) => ( +

+ + + +

+ ))} {/* user links */} {props.userDetails.username && ( <> diff --git a/frontend/src/components/header/messages.js b/frontend/src/components/header/messages.js index ccff938fe0..ee8e69a43a 100644 --- a/frontend/src/components/header/messages.js +++ b/frontend/src/components/header/messages.js @@ -15,6 +15,10 @@ export default defineMessages({ id: 'header.nav.aboutLink', defaultMessage: 'About', }, + support: { + id: 'header.nav.support', + defaultMessage: 'Support', + }, myContributions: { id: 'header.nav.my_contributions', defaultMessage: 'My contributions', From 0a8816bae9a8af7f468ba2728390d523e08efe51 Mon Sep 17 00:00:00 2001 From: Hel Nershing Thapa Date: Mon, 21 Mar 2022 11:46:38 +0545 Subject: [PATCH 24/29] Use anchor tag for external support link --- frontend/src/components/header/index.js | 47 +++++++++++++++++-------- frontend/src/locales/en.json | 1 + 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/header/index.js b/frontend/src/components/header/index.js index caf50cc4f0..59b3164a22 100644 --- a/frontend/src/components/header/index.js +++ b/frontend/src/components/header/index.js @@ -98,9 +98,21 @@ const PopupItems = (props) => { .filter((item) => item.authenticated === false || item.showAlways) .map((item, n) => (

- - - + {!item.serviceDesk ? ( + + + + ) : ( + + + + + )}

))}

@@ -116,15 +128,6 @@ const PopupItems = (props) => {

))} - {props.menuItems - .filter((item) => item.serviceDesk === true) - .map((item, n) => ( -

- - - -

- ))} {/* user links */} {props.userDetails.username && ( <> @@ -174,9 +177,23 @@ class Header extends React.Component { return (
{filteredMenuItems.map((item, n) => ( - - - + <> + {!item.serviceDesk ? ( + + + + ) : ( + + + + + )} + ))}
); diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 3bc995a957..088b06dbf4 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -59,6 +59,7 @@ "header.nav.projects": "Explore projects", "header.nav.learn": "Learn", "header.nav.aboutLink": "About", + "header.nav.support": "Support", "header.nav.my_contributions": "My contributions", "header.nav.manage": "Manage", "header.buttons.logIn": "Log in", From ba95a475b291692f8098c906ccc5080fcc4a7069 Mon Sep 17 00:00:00 2001 From: Hel Nershing Thapa Date: Mon, 21 Mar 2022 11:57:56 +0545 Subject: [PATCH 25/29] Update menu items test case to include support portal link --- frontend/src/components/header/tests/menuItens.test.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/header/tests/menuItens.test.js b/frontend/src/components/header/tests/menuItens.test.js index a0d6a359a6..3f7288c677 100644 --- a/frontend/src/components/header/tests/menuItens.test.js +++ b/frontend/src/components/header/tests/menuItens.test.js @@ -1,25 +1,26 @@ import { getMenuItensForUser } from '../index'; +import { SERVICE_DESK } from '../../../config'; it('test menuItems for unlogged user', () => { const userDetails = {}; const menuItems = getMenuItensForUser(userDetails, []).map((i) => i.link); - expect(menuItems).toEqual(['explore', 'learn', 'about']); + expect(menuItems).toEqual(['explore', 'learn', 'about', SERVICE_DESK]); }); it('test menuItems for logged non admin user', () => { const userDetails = { username: 'test', role: 'MAPPER' }; const menuItems = getMenuItensForUser(userDetails, []).map((i) => i.link); - expect(menuItems).toEqual(['explore', 'contributions', 'learn', 'about']); + expect(menuItems).toEqual(['explore', 'contributions', 'learn', 'about', SERVICE_DESK]); }); it('test menuItems for logged non admin user, but org manager', () => { const userDetails = { username: 'test', role: 'MAPPER' }; const menuItems = getMenuItensForUser(userDetails, [1, 3, 4]).map((i) => i.link); - expect(menuItems).toEqual(['explore', 'contributions', 'manage', 'learn', 'about']); + expect(menuItems).toEqual(['explore', 'contributions', 'manage', 'learn', 'about', SERVICE_DESK]); }); it('test menuItems for logged admin user', () => { const userDetails = { username: 'test', role: 'ADMIN' }; const menuItems = getMenuItensForUser(userDetails, []).map((i) => i.link); - expect(menuItems).toEqual(['explore', 'contributions', 'manage', 'learn', 'about']); + expect(menuItems).toEqual(['explore', 'contributions', 'manage', 'learn', 'about', SERVICE_DESK]); }); From 88d5b4a7425576dd7aa520a23c5ead28a58e74c0 Mon Sep 17 00:00:00 2001 From: Aadesh-Baral Date: Tue, 29 Mar 2022 10:21:07 +0545 Subject: [PATCH 26/29] Change default editors while creating project --- backend/models/postgis/project.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/models/postgis/project.py b/backend/models/postgis/project.py index b3e3cbf638..5d8a53a82f 100644 --- a/backend/models/postgis/project.py +++ b/backend/models/postgis/project.py @@ -175,7 +175,6 @@ class Project(db.Model): Editors.ID.value, Editors.JOSM.value, Editors.CUSTOM.value, - Editors.RAPID.value, ], index=True, nullable=False, @@ -186,7 +185,6 @@ class Project(db.Model): Editors.ID.value, Editors.JOSM.value, Editors.CUSTOM.value, - Editors.RAPID.value, ], index=True, nullable=False, From 532aa94ad205e7db21165f68ef50775d14084cd0 Mon Sep 17 00:00:00 2001 From: Hel Nershing Thapa <51614993+HelNershingThapa@users.noreply.github.com> Date: Thu, 31 Mar 2022 10:16:12 +0545 Subject: [PATCH 27/29] Improve task lock error messages (#5039) * Add error message for 403 status code * Update test cases * Update translation source * add sub-codes in error response * add sub-codes on error response for tasks API * fix formatting * fix formatting * fix formatting * add subcodes on projects response * Include error messages for lock error subcodes * add subcode on error response * fix formatting * fix formatting Co-authored-by: Aadesh-Baral --- backend/api/annotations/resources.py | 15 +- backend/api/campaigns/resources.py | 31 ++-- backend/api/comments/resources.py | 42 +++-- backend/api/countries/resources.py | 2 +- backend/api/interests/resources.py | 29 +-- backend/api/issues/resources.py | 50 ++++- backend/api/licenses/actions.py | 7 +- backend/api/licenses/resources.py | 40 ++-- backend/api/notifications/actions.py | 5 +- backend/api/notifications/resources.py | 37 ++-- backend/api/organisations/campaigns.py | 25 ++- backend/api/organisations/resources.py | 57 +++--- backend/api/projects/actions.py | 70 ++++--- backend/api/projects/activities.py | 14 +- backend/api/projects/campaigns.py | 28 +-- backend/api/projects/contributions.py | 14 +- backend/api/projects/favorites.py | 16 +- backend/api/projects/resources.py | 164 +++++++++++------ backend/api/projects/statistics.py | 16 +- backend/api/projects/teams.py | 21 ++- backend/api/system/applications.py | 22 ++- backend/api/system/authentication.py | 11 +- backend/api/system/general.py | 10 +- backend/api/system/image_upload.py | 18 +- backend/api/system/statistics.py | 5 +- backend/api/tasks/actions.py | 174 ++++++++++++------ backend/api/tasks/resources.py | 80 ++++++-- backend/api/tasks/statistics.py | 13 +- backend/api/teams/actions.py | 51 +++-- backend/api/teams/resources.py | 51 ++--- backend/api/users/actions.py | 57 ++++-- backend/api/users/openstreetmap.py | 7 +- backend/api/users/resources.py | 46 +++-- backend/api/users/statistics.py | 30 +-- backend/api/users/tasks.py | 4 +- backend/models/postgis/project.py | 54 +++--- backend/models/postgis/task.py | 56 +++--- backend/services/campaign_service.py | 16 +- backend/services/grid/grid_service.py | 12 +- backend/services/grid/split_service.py | 16 +- backend/services/mapping_service.py | 50 +++-- backend/services/messaging/chat_service.py | 8 +- backend/services/messaging/message_service.py | 35 ++-- backend/services/organisation_service.py | 26 +-- backend/services/project_admin_service.py | 51 ++--- backend/services/project_search_service.py | 20 +- backend/services/project_service.py | 30 +-- backend/services/team_service.py | 21 ++- backend/services/users/user_service.py | 44 +++-- backend/services/validator_service.py | 31 +++- .../src/components/taskSelection/footer.js | 2 +- .../components/taskSelection/lockedTasks.js | 64 ++----- .../src/components/taskSelection/messages.js | 57 +++++- .../taskSelection/tests/lockedTasks.test.js | 41 ++++- frontend/src/locales/en.json | 12 ++ frontend/src/utils/promise.js | 19 +- 56 files changed, 1230 insertions(+), 697 deletions(-) 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/postgis/project.py b/backend/models/postgis/project.py index 5d8a53a82f..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" @@ -235,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) @@ -245,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}" @@ -255,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) @@ -272,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: @@ -360,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: @@ -499,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() @@ -524,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() @@ -554,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 @@ -637,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" @@ -813,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 @@ -948,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( @@ -969,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( @@ -986,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 @@ -1099,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( @@ -1124,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 ) @@ -1147,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( @@ -1163,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/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 b607ce99a2..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,7 +383,7 @@ 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.project_id == project_id, 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/frontend/src/components/taskSelection/footer.js b/frontend/src/components/taskSelection/footer.js index 9840c2f732..1eecba1723 100644 --- a/frontend/src/components/taskSelection/footer.js +++ b/frontend/src/components/taskSelection/footer.js @@ -85,7 +85,7 @@ const TaskSelectionFooter = ({ defaultUserEditor, project, tasks, taskAction, se ).length, ); if (!mappedTasks.length) { - setLockError('No mapped tasks selected'); + setLockError('noMappedTasksSelected'); } else { setIsPending(true); pushToLocalJSONAPI( diff --git a/frontend/src/components/taskSelection/lockedTasks.js b/frontend/src/components/taskSelection/lockedTasks.js index 441f27e59e..0a10865d28 100644 --- a/frontend/src/components/taskSelection/lockedTasks.js +++ b/frontend/src/components/taskSelection/lockedTasks.js @@ -1,8 +1,7 @@ import React, { useEffect, useState } from 'react'; -import { Link } from '@reach/router'; +import { Link, navigate } from '@reach/router'; import { fetchLocalJSONAPI, pushToLocalJSONAPI } from '../../network/genericJSONRequest'; import { useSelector } from 'react-redux'; -import { navigate } from '@reach/router'; import { FormattedMessage } from 'react-intl'; import messages from './messages'; @@ -117,45 +116,22 @@ export const LicenseError = ({ id, close, lockTasks }) => { ); }; -export function LockError() { +export function LockError({ error, close }) { return ( <>

- + {messages[`${error}Error`] ? ( + + ) : ( + + )}

- -
- - ); -} - -export function JosmError({ close }: Object) { - return ( - <> -

- -

-
- -
-
- -
- - ); -} - -export function NoMappedTasksError({ close }: Object) { - return ( - <> -

- -

-
- + {messages[`${error}ErrorDescription`] ? ( + + ) : ( + + )}
@@ -88,43 +95,43 @@ const Steps = ({ items }) => ( ); const Manuals = ({ contents }) => ( -
-

- -

-
- {contents.map((content, i) => ( - +); const Videos = ({ contents }) => { const [activeVideo, setActiveVideo] = useState(null); @@ -210,6 +217,28 @@ const LearnToManage = ({ section }) => { const messagesObjs = { intro: 'learnManageIntro', description: 'learnManageDescription', + values: { + organizationsListLink: ( + + + + ), + createNewOrganizationFormLink: ( + + + + ), + }, }; const items = [ diff --git a/frontend/src/views/messages.js b/frontend/src/views/messages.js index f54352fb4b..531ecda33b 100644 --- a/frontend/src/views/messages.js +++ b/frontend/src/views/messages.js @@ -450,7 +450,7 @@ export default defineMessages({ learnManageDescription: { id: 'pages.learn.manage.description', defaultMessage: - 'You can use the Tasking Manager to set up your own projects. Be sure to be responsible by making sure your skill level matches your ambition. It is good to reach out to the administrators of the Tasking Manager and learn more about what is needed to obtain the permissions to create and manage projects.', + 'You can use the Tasking Manager to set up your own projects. Be sure to be responsible by making sure your skill level matches your ambition. If you are interested in creating projects please check whether your organization already exists on Tasking Manager by viewing this {organizationsListLink}. If your organization already exists on Tasking Manager please contact one of your listed organization managers. If your organization does not exist, feel free to register your interest in creating projects by completing this {createNewOrganizationFormLink}.', }, learnManageStepJoinTitle: { id: 'pages.learn.manage.steps.join.title', @@ -461,6 +461,14 @@ export default defineMessages({ defaultMessage: 'The Tasking Manager allows you to create projects as part of a community or organization. Either get in touch with one you know, or request the admins to add your group to the Tasking Manager.', }, + list: { + id: 'pages.learn.manage.list', + defaultMessage: 'list', + }, + form: { + id: 'pages.learn.manage.form', + defaultMessage: 'form', + }, learnManageStepCreateTitle: { id: 'pages.learn.manage.steps.create.title', defaultMessage: 'Create a project and be loud about it', From d12c9fc6a39caa8f0a31e92dd8f48b8eca92d629 Mon Sep 17 00:00:00 2001 From: Hel Nershing Thapa Date: Tue, 5 Apr 2022 11:57:52 +0545 Subject: [PATCH 29/29] Fix support link in footer --- frontend/src/components/footer.js | 38 +++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) 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) {
{getMenuItensForUser(userDetails).map((item, n) => ( - - - + <> + {!item.serviceDesk ? ( + + + + ) : ( + + + + + )} + ))}

{socialNetworks