Пример #1
0
    def delete(self, project_id, campaign_id):
        """
        Delete a campaign for a project
        ---
        tags:
          - campaigns
        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
            - name: campaign_id
              in: path
              description: Unique campaign ID
              required: true
              type: integer
              default: 1
        responses:
            200:
                description: Campaign assigned successfully
            400:
                description: Client Error - Invalid Request
            401:
                description: Unauthorized - Invalid credentials
            403:
                description: Forbidden
            500:
                description: Internal Server Error
        """
        try:
            ProjectAdminService.is_user_action_permitted_on_project(
                tm.authenticated_user_id, project_id)
        except ValueError as e:
            error_msg = f"ProjectsCampaignsAPI DELETE: {str(e)}"
            return {"Error": error_msg}, 403

        try:
            CampaignService.delete_project_campaign(project_id, campaign_id)
            return {"Success": "Campaigns Deleted"}, 200
        except NotFound:
            return {"Error": "Campaign Not Found"}, 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
Пример #2
0
    def delete(self, project_id):
        """
        Deletes 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
        responses:
            200:
                description: Project deleted
            401:
                description: Unauthorized - Invalid credentials
            403:
                description: Forbidden
            404:
                description: Project not found
            500:
                description: Internal Server Error
        """
        try:
            authenticated_user_id = token_auth.current_user()
            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

        try:
            ProjectAdminService.delete_project(project_id, authenticated_user_id)
            return {"Success": "Project deleted"}, 200
        except ProjectAdminServiceError:
            return {"Error": "Project has some mapping"}, 403
        except NotFound:
            return {"Error": "Project Not Found"}, 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
Пример #3
0
    def post(self, project_id):
        """
        Set a project as featured
        ---
        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
        responses:
            200:
                description: Featured projects
            400:
                description: Bad request
            403:
                description: Forbidden
            404:
                description: Project not found
            500:
                description: Internal Server Error
        """
        try:
            authenticated_user_id = token_auth.current_user()
            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

        try:
            ProjectService.set_project_as_featured(project_id)
            return {"Success": True}, 200
        except NotFound:
            return {"Error": "Project Not Found"}, 404
        except ValueError as e:
            error_msg = f"FeaturedProjects POST: {str(e)}"
            return {"Error": error_msg}, 400
        except Exception as e:
            error_msg = f"FeaturedProjects POST - unhandled error: {str(e)}"
            current_app.logger.critical(error_msg)
            return {"Error": error_msg}, 500
Пример #4
0
    def post(self, project_id):
        """
        Set all bad imagery tasks as ready for mapping
        ---
        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: Unique project ID
              required: true
              type: integer
              default: 1
        responses:
            200:
                description: All bad imagery tasks marked ready for mapping
            401:
                description: Unauthorized - Invalid credentials
            403:
                description: Forbidden
            500:
                description: Internal Server Error
        """
        try:
            authenticated_user_id = token_auth.current_user()
            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

        try:
            MappingService.reset_all_badimagery(project_id,
                                                authenticated_user_id)
            return {
                "Success": "All bad imagery tasks marked ready for mapping"
            }, 200
        except Exception as e:
            error_msg = (
                f"TasksActionsResetBadImageryAllAPI POST - unhandled error: {str(e)}"
            )
            current_app.logger.critical(error_msg)
            return {"Error": "Unable to reset tasks"}, 500
Пример #5
0
    def get(self, project_id):
        """
        Retrieves 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
        responses:
            200:
                description: Project found
            401:
                description: Unauthorized - Invalid credentials
            403:
                description: Forbidden
            404:
                description: Project not found
            500:
                description: Internal Server Error
        """
        try:
            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

        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
        except Exception as e:
            error_msg = f"Project GET - unhandled error: {str(e)}"
            current_app.logger.critical(error_msg)
            return {"Error": error_msg}, 500
Пример #6
0
    def get_project_dto_for_mapper(
        project_id, current_user_id, locale="en", abbrev=False
    ) -> ProjectDTO:
        """
        Get the project DTO for mappers
        :param project_id: ID of the Project mapper has requested
        :param locale: Locale the mapper has requested
        :raises ProjectServiceError, NotFound
        """
        project = ProjectService.get_project_by_id(project_id)
        # if project is public and is not draft, we don't need to check permissions
        if not project.private and not project.status == ProjectStatus.DRAFT.value:
            return project.as_dto_for_mapping(current_user_id, locale, abbrev)

        is_allowed_user = True
        is_team_member = None
        is_manager_permission = False

        if current_user_id:
            is_manager_permission = ProjectAdminService.is_user_action_permitted_on_project(
                current_user_id, project_id
            )
        # Draft Projects - admins, authors, org admins & team managers permitted
        if project.status == ProjectStatus.DRAFT.value:
            if not is_manager_permission:
                is_allowed_user = False
                raise ProjectServiceError("Unable to fetch project")

        # Private Projects - allowed_users, admins, org admins &
        # assigned teams (mappers, validators, project managers), authors permitted

        if project.private and not is_manager_permission:
            is_allowed_user = False
            if current_user_id:
                is_allowed_user = (
                    len(
                        [
                            user
                            for user in project.allowed_users
                            if user.id == current_user_id
                        ]
                    )
                    > 0
                )

        if not (is_allowed_user or is_manager_permission):
            if current_user_id:
                allowed_roles = [
                    TeamRoles.MAPPER.value,
                    TeamRoles.VALIDATOR.value,
                    TeamRoles.PROJECT_MANAGER.value,
                ]
                is_team_member = TeamService.check_team_membership(
                    project_id, allowed_roles, current_user_id
                )

        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")
Пример #7
0
    def post(self, project_id):
        """
        Reset all tasks on project back to ready, preserving history
        ---
        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: Unique project ID
              required: true
              type: integer
              default: 1
        responses:
            200:
                description: All tasks reset
            401:
                description: Unauthorized - Invalid credentials
            403:
                description: Forbidden
            500:
                description: Internal Server Error
        """
        try:
            ProjectAdminService.is_user_action_permitted_on_project(
                tm.authenticated_user_id, project_id)
        except ValueError as e:
            error_msg = f"TasksActionsResetAllAPI POST: {str(e)}"
            return {"Error": error_msg}, 403

        try:
            ProjectAdminService.reset_all_tasks(project_id,
                                                tm.authenticated_user_id)
            return {"Success": "All tasks reset"}, 200
        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
Пример #8
0
    def is_user_permitted_to_validate(project_id, user_id):
        """ 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

        project = ProjectService.get_project_by_id(project_id)
        if project.license_id:
            if not UserService.has_user_accepted_license(user_id, project.license_id):
                return False, ValidatingNotAllowed.USER_NOT_ACCEPTED_LICENSE
        validation_permission = project.validation_permission

        # is_admin or is_author or is_org_manager or is_manager_team
        is_manager_permission = False
        if ProjectAdminService.is_user_action_permitted_on_project(user_id, project_id):
            is_manager_permission = True

        # Draft (public/private) accessible only for is_manager_permission
        if (
            ProjectStatus(project.status) == ProjectStatus.DRAFT
            and not is_manager_permission
        ):
            return False, ValidatingNotAllowed.PROJECT_NOT_PUBLISHED

        is_restriction = None
        if not is_manager_permission and validation_permission:
            is_restriction = ProjectService.evaluate_validation_permission(
                project_id, user_id, validation_permission
            )

        tasks = Task.get_locked_tasks_for_user(user_id)
        if len(tasks.locked_tasks) > 0:
            return False, ValidatingNotAllowed.USER_ALREADY_HAS_TASK_LOCKED

        is_allowed_user = None
        if project.private and not is_manager_permission:
            # Check if user is in allowed user list
            is_allowed_user = ProjectService.is_user_in_the_allowed_list(
                project.allowed_users, user_id
            )

            if is_allowed_user:
                return True, "User allowed to validate"

        if not is_manager_permission and is_restriction:
            return is_restriction
        elif project.private and not (
            is_manager_permission or is_allowed_user or not is_restriction
        ):
            return False, ValidatingNotAllowed.USER_NOT_ON_ALLOWED_LIST

        return True, "User allowed to validate"
Пример #9
0
    def is_user_permitted_to_validate(project_id, user_id):
        """ 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

        project = ProjectService.get_project_by_id(project_id)
        if project.license_id:
            if not UserService.has_user_accepted_license(user_id, project.license_id):
                return False, ValidatingNotAllowed.USER_NOT_ACCEPTED_LICENSE

        validation_permission = project.validation_permission

        is_manager_permission = False
        # is_admin or is_author or is_org_manager or is_manager_team
        if ProjectAdminService.is_user_action_permitted_on_project(user_id, project_id):
            is_manager_permission = True

        if (
            ProjectStatus(project.status) != ProjectStatus.PUBLISHED
            and not is_manager_permission
        ):
            return False, ValidatingNotAllowed.PROJECT_NOT_PUBLISHED

        tasks = Task.get_locked_tasks_for_user(user_id)

        if len(tasks.locked_tasks) > 0:
            return False, ValidatingNotAllowed.USER_ALREADY_HAS_TASK_LOCKED

        if project.private and not is_manager_permission:
            # Check user is in allowed users
            try:
                next(user for user in project.allowed_users if user.id == user_id)
            except StopIteration:
                return False, ValidatingNotAllowed.USER_NOT_ON_ALLOWED_LIST

            is_restriction = ProjectService.evaluate_validation_permission(
                project_id, user_id, validation_permission
            )
            if is_restriction:
                return is_restriction

        if project.validation_permission and not is_manager_permission:
            is_restriction = ProjectService.evaluate_validation_permission(
                project_id, user_id, validation_permission
            )
            if is_restriction:
                return is_restriction

        return True, "User allowed to validate"
Пример #10
0
    def post_message(chat_dto: ChatMessageDTO, project_id: int,
                     authenticated_user_id: int) -> ProjectChatDTO:
        """ Save message to DB and return latest chat"""
        current_app.logger.debug("Posting Chat Message")

        if UserService.is_user_blocked(authenticated_user_id):
            raise ValueError("User is on read only mode")

        project = ProjectService.get_project_by_id(project_id)
        is_allowed_user = True
        is_manager_permission = ProjectAdminService.is_user_action_permitted_on_project(
            authenticated_user_id, project_id)
        is_team_member = False

        # Draft (public/private) accessible only for is_manager_permission
        if (ProjectStatus(project.status) == ProjectStatus.DRAFT
                and not is_manager_permission):
            raise ValueError("User not permitted to post Comment")

        if project.private:
            is_allowed_user = False
            if not is_manager_permission:
                allowed_roles = [
                    TeamRoles.PROJECT_MANAGER.value,
                    TeamRoles.VALIDATOR.value,
                    TeamRoles.MAPPER.value,
                ]
                is_team_member = TeamService.check_team_membership(
                    project_id, allowed_roles, authenticated_user_id)
                if not is_team_member:
                    is_allowed_user = (len([
                        user for user in project.allowed_users
                        if user.id == authenticated_user_id
                    ]) > 0)

        if is_manager_permission or is_team_member or is_allowed_user:
            chat_message = ProjectChat.create_from_dto(chat_dto)
            MessageService.send_message_after_chat(chat_dto.user_id,
                                                   chat_message.message,
                                                   chat_dto.project_id)
            db.session.commit()
            # Ensure we return latest messages after post
            return ProjectChat.get_messages(chat_dto.project_id, 1)
        else:
            raise ValueError("User not permitted to post Comment")
    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 """
        if UserService.is_user_blocked(user_id):
            return False, MappingNotAllowed.USER_NOT_ON_ALLOWED_LIST

        project = ProjectService.get_project_by_id(project_id)
        mapping_permission = project.mapping_permission

        if ProjectStatus(
                project.status
        ) != ProjectStatus.PUBLISHED and not ProjectAdminService.is_user_action_permitted_on_project(
                user_id, project_id):
            return False, MappingNotAllowed.PROJECT_NOT_PUBLISHED
        tasks = Task.get_locked_tasks_for_user(user_id)
        if len(tasks.locked_tasks) > 0:
            return False, MappingNotAllowed.USER_ALREADY_HAS_TASK_LOCKED
        if project.private:
            # Check user is in allowed users
            try:
                next(user for user in project.allowed_users
                     if user.id == user_id)
            except StopIteration:
                return False, MappingNotAllowed.USER_NOT_ON_ALLOWED_LIST
            is_restriction = ProjectService.evaluate_mapping_permission(
                project_id, user_id, mapping_permission)
            if is_restriction:
                return is_restriction

        if project.mapping_permission:
            is_restriction = ProjectService.evaluate_mapping_permission(
                project_id, user_id, mapping_permission)
            if is_restriction:
                return is_restriction

        if project.license_id:
            if not UserService.has_user_accepted_license(
                    user_id, project.license_id):
                return False, MappingNotAllowed.USER_NOT_ACCEPTED_LICENSE

        return True, "User allowed to map"
Пример #12
0
    def post(self, project_id, campaign_id):
        """
        Assign a campaign for a project
        ---
        tags:
          - campaigns
        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
            - name: campaign_id
              in: path
              description: Unique campaign ID
              required: true
              type: integer
              default: 1
        responses:
            201:
                description: Campaign assigned successfully
            400:
                description: Client Error - Invalid Request
            401:
                description: Unauthorized - Invalid credentials
            403:
                description: Forbidden
            500:
                description: Internal Server Error
        """
        try:
            ProjectAdminService.is_user_action_permitted_on_project(
                tm.authenticated_user_id, project_id)
        except ValueError as e:
            error_msg = f"ProjectsCampaignsAPI POST: {str(e)}"
            return {"Error": error_msg}, 403

        try:
            campaign_project_dto = CampaignProjectDTO()
            campaign_project_dto.campaign_id = campaign_id
            campaign_project_dto.project_id = project_id
            campaign_project_dto.validate()
        except DataError as e:
            current_app.logger.error(f"error validating request: {str(e)}")
            return str(e), 400

        try:
            CampaignService.create_campaign_project(campaign_project_dto)
            message = "campaign with id {} assigned successfully for project with id {}".format(
                campaign_id, project_id)
            return ({"Success": message}, 200)
        except Exception as e:
            error_msg = f"ProjectsCampaignsAPI POST - unhandled error: {str(e)}"
            current_app.logger.critical(error_msg)
            return {"Error": error_msg}, 500
Пример #13
0
    def post(self, project_id):
        """
        Send message to all contributors of a 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 creating draft project
              schema:
                properties:
                    subject:
                        type: string
                        default: Thanks
                        required: true
                    message:
                        type: string
                        default: Thanks for your contribution
                        required: true
        responses:
            200:
                description: Message sent successfully
            401:
                description: Unauthorized - Invalid credentials
            403:
                description: Forbidden
            500:
                description: Internal Server Error
        """
        try:
            authenticated_user_id = token_auth.current_user()
            message_dto = MessageDTO(request.get_json())
            message_dto.from_user_id = authenticated_user_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

        try:
            ProjectAdminService.is_user_action_permitted_on_project(
                authenticated_user_id, project_id)
            threading.Thread(
                target=MessageService.send_message_to_all_contributors,
                args=(project_id, message_dto),
            ).start()

            return {"Success": "Messages started"}, 200
        except ValueError as e:
            return {"Error": str(e)}, 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
Пример #14
0
    def post(self, project_id):
        """
        Creates a relationship between project and interests
        ---
        tags:
            - interests
        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 creating/updating project and interests relationships
              schema:
                  properties:
                      interests:
                          type: array
                          items:
                            type: integer
        responses:
            200:
                description: New project interest relationship created
            400:
                description: Invalid Request
            401:
                description: Unauthorized - Invalid credentials
            403:
                description: Forbidden
            500:
                description: Internal Server Error
        """
        try:
            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

        try:
            data = request.get_json()
            project_interests = InterestService.create_or_update_project_interests(
                project_id, data["interests"])
            return project_interests.to_primitive(), 200
        except NotFound:
            return {"Error": "Project not Found"}, 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
Пример #15
0
    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 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