def get(self, project_id, task_id): """ Get comments for a task --- tags: - comments produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - name: project_id in: path description: Project ID the task is associated with required: true type: integer default: 1 - name: task_id in: path description: Unique task ID required: true type: integer default: 1 - in: body name: body required: true description: JSON object representing the comment schema: id: TaskComment required: - comment properties: comment: type: string description: user comment about the task responses: 200: description: Comment retrieved 400: description: Client Error 401: description: Unauthorized - Invalid credentials 403: description: Forbidden 404: description: Task not found 500: description: Internal Server Error """ try: task_comment = TaskCommentDTO(request.get_json()) task_comment.user_id = token_auth.current_user() task_comment.task_id = task_id task_comment.project_id = project_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 try: # NEW FUNCTION HAS TO BE ADDED # task = MappingService.add_task_comment(task_comment) # return task.to_primitive(), 200 return except NotFound: return {"Error": "Task Not Found"}, 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
def post(self): """ Creates a tasking-manager project --- tags: - projects produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - in: body name: body required: true description: JSON object for creating draft project schema: properties: cloneFromProjectId: type: int default: 1 description: Specify this value if you want to clone a project, otherwise avoid information projectName: type: string default: HOT Project areaOfInterest: schema: properties: type: type: string default: FeatureCollection features: type: array items: schema: $ref: "#/definitions/GeoJsonFeature" tasks: schema: properties: type: type: string default: FeatureCollection features: type: array items: schema: $ref: "#/definitions/GeoJsonFeature" arbitraryTasks: type: boolean default: false responses: 201: description: Draft project created successfully 400: description: Client Error - Invalid Request 401: description: Unauthorized - Invalid credentials 403: description: Forbidden 500: description: Internal Server Error """ try: draft_project_dto = DraftProjectDTO(request.get_json()) draft_project_dto.user_id = token_auth.current_user() 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 try: draft_project_id = ProjectAdminService.create_draft_project( draft_project_dto) return {"projectId": draft_project_id}, 201 except ProjectAdminServiceError as e: return {"Error": str(e)}, 403 except (InvalidGeoJson, InvalidData): return {"Error": "Invalid GeoJson"}, 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
def get(self): """ List and search projects by bounding box --- tags: - projects produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - in: header name: Accept-Language description: Language user is requesting type: string default: en - in: query name: bbox description: comma separated list xmin, ymin, xmax, ymax type: string required: true default: 34.404,-1.034, 34.717,-0.624 - in: query name: srid description: srid of bbox coords type: integer default: 4326 - in: query name: createdByMe description: limit to projects created by authenticated user type: boolean required: true default: false responses: 200: description: ok 400: description: Client Error - Invalid Request 403: description: Forbidden 500: description: Internal Server Error """ try: authenticated_user_id = token_auth.current_user() orgs_dto = OrganisationService.get_organisations_managed_by_user_as_dto( authenticated_user_id) if len(orgs_dto.organisations) < 1: raise ValueError("User not a project manager") except ValueError as e: error_msg = f"ProjectsQueriesBboxAPI GET: {str(e)}" return {"Error": error_msg}, 403 try: search_dto = ProjectSearchBBoxDTO() search_dto.bbox = map(float, request.args.get("bbox").split(",")) search_dto.input_srid = request.args.get("srid") search_dto.preferred_locale = request.environ.get( "HTTP_ACCEPT_LANGUAGE") created_by_me = (strtobool(request.args.get("createdByMe")) if request.args.get("createdByMe") else False) if created_by_me: search_dto.project_author = authenticated_user_id search_dto.validate() except Exception as e: current_app.logger.error(f"Error validating request: {str(e)}") return {"Error": "Unable to fetch projects"}, 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 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
def post(self, project_id): """ Add a message to project chat --- tags: - comments produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - name: project_id in: path description: Project ID to attach the chat message to required: true type: integer default: 1 - in: body name: body required: true description: JSON object for creating a new mapping license schema: properties: message: type: string default: This is an awesome project responses: 201: description: Message posted successfully 400: description: Invalid Request 500: description: Internal Server Error """ authenticated_user_id = token_auth.current_user() if UserService.is_user_blocked(authenticated_user_id): return {"Error": "User is on read only mode."}, 403 try: chat_dto = ChatMessageDTO(request.get_json()) chat_dto.user_id = authenticated_user_id chat_dto.project_id = 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 try: project_messages = ChatService.post_message( chat_dto, project_id, authenticated_user_id ) return project_messages.to_primitive(), 201 except ValueError as e: error_msg = f"CommentsProjectsRestAPI POST: {str(e)}" return {"Error": error_msg}, 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
def get(self, project_id): """ Get a specified project including it's area --- tags: - projects produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: false type: string default: Token sessionTokenHere== - in: header name: Accept-Language description: Language user is requesting type: string required: true default: en - name: project_id in: path description: Unique project ID required: true type: integer default: 1 - in: query name: as_file type: boolean description: Set to true if file download is preferred default: False - in: query name: abbreviated type: boolean description: Set to true if only state information is desired default: False responses: 200: description: Project found 403: description: Forbidden 404: description: Project not found 500: description: Internal Server Error """ try: authenticated_user_id = token_auth.current_user() as_file = (strtobool(request.args.get("as_file")) if request.args.get("as_file") else False) abbreviated = (strtobool(request.args.get("abbreviated")) if request.args.get("abbreviated") else False) project_dto = ProjectService.get_project_dto_for_mapper( project_id, authenticated_user_id, request.environ.get("HTTP_ACCEPT_LANGUAGE"), abbreviated, ) if project_dto: project_dto = project_dto.to_primitive() if as_file: return send_file( io.BytesIO(geojson.dumps(project_dto).encode("utf-8")), mimetype="application/json", as_attachment=True, attachment_filename=f"project_{str(project_id)}.json", ) return project_dto, 200 else: return {"Error": "Private Project"}, 403 except NotFound: return {"Error": "Project Not Found"}, 404 except ProjectServiceError as e: return {"Error": str(e)}, 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 finally: # this will try to unlock tasks that have been locked too long try: ProjectService.auto_unlock_tasks(project_id) except Exception as e: current_app.logger.critical(str(e))
def get(self): """ List and search for projects --- tags: - projects produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token type: string default: Token sessionTokenHere== - in: header name: Accept-Language description: Language user is requesting type: string required: true default: en - in: query name: mapperLevel type: string - in: query name: orderBy type: string default: priority enum: [id,mapper_level,priority,status,last_updated,due_date] - in: query name: orderByType type: string default: ASC enum: [ASC, DESC] - in: query name: mappingTypes type: string - in: query name: mappingTypesExact type: boolean default: false description: if true, limits projects to match the exact mapping types requested - in: query name: organisationName description: Organisation name to search for type: string - in: query name: organisationId description: Organisation ID to search for type: integer - in: query name: campaign description: Campaign name to search for type: string - in: query name: page description: Page of results user requested type: integer default: 1 - in: query name: textSearch description: Text to search type: string - in: query name: country description: Project country type: string - in: query name: action description: Filter projects by possible actions enum: [map, validate, any] type: string - in: query name: projectStatuses description: Authenticated PMs can search for archived or draft statuses type: string - in: query name: lastUpdatedFrom description: Filter projects whose last update date is equal or greater than a date type: string - in: query name: lastUpdatedTo description: Filter projects whose last update date is equal or lower than a date type: string - in: query name: createdFrom description: Filter projects whose creation date is equal or greater than a date type: string - in: query name: createdTo description: Filter projects whose creation date is equal or lower than a date type: string - in: query name: interests type: string description: Filter by interest on project default: null - in: query name: createdByMe description: Limit to projects created by the authenticated user type: boolean default: false - in: query name: mappedByMe description: Limit to projects mapped/validated by the authenticated user type: boolean default: false - in: query name: favoritedByMe description: Limit to projects favorited by the authenticated user type: boolean default: false - in: query name: managedByMe description: Limit to projects that can be managed by the authenticated user, excluding the ones created by them type: boolean default: false - in: query name: teamId type: string description: Filter by team on project default: null name: omitMapResults type: boolean description: If true, it will not return the project centroid's geometries. default: false responses: 200: description: Projects found 404: description: No projects found 500: description: Internal Server Error """ try: user = None user_id = token_auth.current_user() if user_id: user = UserService.get_user_by_id(user_id) search_dto = self.setup_search_dto() results_dto = ProjectSearchService.search_projects( search_dto, user) return results_dto.to_primitive(), 200 except NotFound: return {"mapResults": {}, "results": []}, 200 except (KeyError, ValueError) as e: error_msg = f"Projects GET - {str(e)}" return {"Error": error_msg}, 400 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
def get(self): """ List all organisations --- tags: - organisations produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token type: string default: Token sessionTokenHere== - name: manager_user_id in: query description: Filter projects on managers with this user_id required: false type: integer - in: query name: omitManagerList type: boolean description: Set it to true if you don't want the managers list on the response. default: False responses: 200: description: Organisations found 400: description: Client Error - Invalid Request 401: description: Unauthorized - Invalid credentials 403: description: Unauthorized - Not allowed 404: description: Organisations not found 500: description: Internal Server Error """ # Restrict some of the parameters to some permissions authenticated_user_id = token_auth.current_user() try: manager_user_id = int(request.args.get("manager_user_id")) except Exception: manager_user_id = None 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" }, 403, ) # Validate abbreviated. omit_managers = strtobool(request.args.get("omitManagerList", "false")) # Obtain organisations try: results_dto = OrganisationService.get_organisations_as_dto( manager_user_id, authenticated_user_id, omit_managers ) return results_dto.to_primitive(), 200 except NotFound: return {"Error": "No organisations found"}, 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
def patch(self, project_id): """ Updates a Tasking-Manager project --- tags: - projects produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - name: project_id in: path description: Unique project ID required: true type: integer default: 1 - in: body name: body required: true description: JSON object for updating an existing project schema: properties: projectStatus: type: string default: DRAFT projectPriority: type: string default: MEDIUM defaultLocale: type: string default: en mapperLevel: type: string default: BEGINNER validation_permission: type: string default: ANY mapping_permission: type: string default: ANY private: type: boolean default: false changesetComment: type: string default: hotosm-project-1 dueDate: type: date default: "2017-04-11T12:38:49" imagery: type: string default: http//www.bing.com/maps/ josmPreset: type: string default: josm preset goes here mappingTypes: type: array items: type: string default: [BUILDINGS, ROADS] mappingEditors: type: array items: type: string default: [ID, JOSM, POTLATCH_2, FIELD_PAPERS] validationEditors: type: array items: type: string default: [ID, JOSM, POTLATCH_2, FIELD_PAPERS] campaign: type: string default: malaria organisation: type: integer default: 1 countryTag: type: array items: type: string default: [] licenseId: type: integer default: 1 description: Id of imagery license associated with the project allowedUsernames: type: array items: type: string default: ["Iain Hunter", LindaA1] priorityAreas: type: array items: schema: $ref: "#/definitions/GeoJsonPolygon" projectInfoLocales: type: array items: schema: $ref: "#/definitions/ProjectInfo" taskCreationMode: type: integer default: GRID responses: 200: description: Project updated 400: description: Client Error - Invalid Request 401: description: Unauthorized - Invalid credentials 403: description: Forbidden 404: description: Project not found 500: description: Internal Server Error """ authenticated_user_id = token_auth.current_user() try: 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 try: project_dto = ProjectDTO(request.get_json()) project_dto.project_id = 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 try: ProjectAdminService.update_project(project_dto, authenticated_user_id) return {"Status": "Updated"}, 200 except InvalidGeoJson as e: return {"Invalid GeoJson": str(e)}, 400 except NotFound as e: return {"Error": str(e) or "Project Not Found"}, 404 except ProjectAdminServiceError as e: return {"Error": str(e)}, 400 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
def post(self): """ Creates a new organisation --- tags: - organisations produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - in: body name: body required: true description: JSON object for creating organisation schema: properties: name: type: string default: HOT logo: type: string default: https://cdn.hotosm.org/tasking-manager/uploads/1588741335578_hot-logo.png url: type: string default: https://hotosm.org managers: type: array items: type: string default: [ user_1, user_2 ] responses: 201: description: Organisation created successfully 400: description: Client Error - Invalid Request 401: description: Unauthorized - Invalid credentials 403: description: Forbidden 402: description: Duplicate Name - Organisation name already exists 500: description: Internal Server Error """ request_user = User.get_by_id(token_auth.current_user()) if request_user.role != 1: return {"Error": "Only admin users can create organisations."}, 403 try: organisation_dto = NewOrganisationDTO(request.get_json()) if request_user.username not in organisation_dto.managers: organisation_dto.managers.append(request_user.username) organisation_dto.validate() except DataError as e: current_app.logger.error(f"error validating request: {str(e)}") return str(e), 400 try: org_id = OrganisationService.create_organisation(organisation_dto) return {"organisationId": org_id}, 201 except OrganisationServiceError as e: return str(e), 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
def patch(self, organisation_id): """ Updates an organisation --- tags: - organisations produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - name: organisation_id in: path description: The unique organisation ID required: true type: integer default: 1 - in: body name: body required: true description: JSON object for updating an organisation schema: properties: name: type: string default: HOT logo: type: string default: https://tasks.hotosm.org/assets/img/hot-tm-logo.svg url: type: string default: https://hotosm.org managers: type: array items: type: string default: [ user_1, user_2 ] responses: 201: description: Organisation updated successfully 400: description: Client Error - Invalid Request 401: description: Unauthorized - Invalid credentials 403: description: Forbidden 500: description: Internal Server Error """ if not OrganisationService.can_user_manage_organisation( organisation_id, token_auth.current_user() ): return {"Error": "User is not an admin for the org"}, 403 try: organisation_dto = UpdateOrganisationDTO(request.get_json()) organisation_dto.organisation_id = organisation_id # Don't update organisation type if user is not admin if User.get_by_id(token_auth.current_user()).role != 1: org_type = OrganisationService.get_organisation_by_id( organisation_id ).type organisation_dto.type = OrganisationType(org_type).name organisation_dto.validate() except DataError as e: current_app.logger.error(f"error validating request: {str(e)}") return str(e), 400 try: OrganisationService.update_organisation(organisation_dto) return {"Status": "Updated"}, 200 except NotFound as e: return {"Error": str(e)}, 404 except OrganisationServiceError as e: return str(e), 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
def patch(self, campaign_id): """ Updates an existing campaign --- tags: - campaigns produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token type: string required: true default: Token sessionTokenHere== - in: header name: Accept-Language description: Language user is requesting type: string required: true default: en - name: campaign_id in: path description: Campaign ID required: true type: integer default: 1 - in: body name: body required: true description: JSON object for updating a Campaign schema: properties: name: type: string example: HOT Campaign logo: type: string example: https://tasks.hotosm.org/assets/img/hot-tm-logo.svg url: type: string example: https://hotosm.org organisations: type: array items: type: integer default: [ 1 ] responses: 200: description: Campaign updated successfully 401: description: Unauthorized - Invalid credentials 403: description: Forbidden 500: description: Internal Server Error """ try: orgs_dto = OrganisationService.get_organisations_managed_by_user_as_dto( token_auth.current_user()) if len(orgs_dto.organisations) < 1: raise ValueError("User not a Org Manager") except ValueError as e: error_msg = f"CampaignsRestAPI PATCH: {str(e)}" return {"Error": error_msg}, 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 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 except Exception as e: error_msg = f"Campaign PUT - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": error_msg}, 500
def patch(self, interest_id): """ Update an existing interest --- tags: - interests produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - name: interest_id in: path description: Interest ID required: true type: integer default: 1 - in: body name: body required: true description: JSON object for creating a new interest schema: properties: name: type: string default: Public Domain responses: 200: description: Interest updated 400: description: Invalid Request 401: description: Unauthorized - Invalid credentials 403: description: Forbidden 500: description: Internal Server Error """ try: orgs_dto = OrganisationService.get_organisations_managed_by_user_as_dto( token_auth.current_user()) if len(orgs_dto.organisations) < 1: raise ValueError("User not a Org Manager") except ValueError as e: error_msg = f"InterestsAllAPI PATCH: {str(e)}" return {"Error": error_msg}, 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 try: update_interest = InterestService.update(interest_id, interest_dto) return update_interest.to_primitive(), 200 except Exception as e: error_msg = f"Interest PUT - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": error_msg}, 500
def patch(self): """ Updates user info --- tags: - users produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - in: body name: body required: true description: JSON object to update a user schema: properties: name: type: string example: Your Name city: type: string example: Your City country: type: string example: Your Country emailAddress: type: string example: [email protected] twitterId: type: string example: twitter handle without @ facebookId: type: string example: facebook username linkedinId: type: string example: linkedin username gender: type: string description: gender selfDescriptionGender: type: string description: gender self-description responses: 200: description: Details saved 400: description: Client Error - Invalid Request 401: description: Unauthorized - Invalid credentials 500: description: Internal Server Error """ try: user_dto = UserDTO(request.get_json()) if user_dto.email_address == "": user_dto.email_address = ( None # Replace empty string with None so validation doesn't break ) user_dto.validate() authenticated_user_id = token_auth.current_user() if authenticated_user_id != user_dto.id: return {"Error": "Unable to authenticate"}, 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 try: verification_sent = UserService.update_user_details( authenticated_user_id, user_dto) return verification_sent, 200 except NotFound: return {"Error": "User not found"}, 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
def get(self): """ List all organisations --- tags: - organisations produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token type: string default: Token sessionTokenHere== - name: manager_user_id in: query description: Filter projects on managers with this user_id required: false type: integer responses: 200: description: Organisations found 400: description: Client Error - Invalid Request 401: description: Unauthorized - Invalid credentials 403: description: Unauthorized - Not allowed 404: description: Organisations not found 500: description: Internal Server Error """ # Restrict some of the parameters to some permissions authenticated_user_id = token_auth.current_user() try: manager_user_id = int(request.args.get("manager_user_id")) except Exception: manager_user_id = None if manager_user_id is not None: try: # Check whether user is admin (can do any query) or user is checking for own projects if (not UserService.is_user_an_admin(authenticated_user_id) and authenticated_user_id != manager_user_id): raise ValueError except Exception: return {"Error": "Unauthorized - Not allowed"}, 403 # Obtain organisations try: results_dto = OrganisationService.get_organisations_as_dto( manager_user_id, authenticated_user_id) return results_dto.to_primitive(), 200 except NotFound: return {"Error": "No projects found"}, 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
def delete(self, project_id): """ Delete a list of tasks from a project --- tags: - tasks produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - name: project_id in: path description: Project ID the task is associated with required: true type: integer default: 1 - in: body name: body required: true description: JSON object with a list of tasks to delete schema: properties: tasks: type: array items: type: integer default: [ 1, 2 ] responses: 200: description: Task(s) deleted 400: description: Bad request 403: description: Forbidden 404: description: Project or Task Not Found 500: description: Internal Server Error """ 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 tasks_ids = request.get_json().get("tasks") if tasks_ids is None: return {"Error": "Tasks ids not provided"}, 400 if type(tasks_ids) != list: return {"Error": "Tasks were not provided as a list"}, 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 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