Exemple #1
0
    def get(self, project_id):
        """
        Get latest user activity on all of project task
        ---
        tags:
          - projects
        produces:
          - application/json
        parameters:
            - name: project_id
              in: path
              required: true
              type: integer
              default: 1
        responses:
            200:
                description: Project activity
            404:
                description: No activity
            500:
                description: Internal Server Error
        """
        try:
            ProjectService.get_project_by_id(project_id)
        except NotFound as e:
            current_app.logger.error(f"Error validating project: {str(e)}")
            return {"Error": "Project not found"}, 404

        try:
            activity = StatsService.get_last_activity(project_id)
            return activity.to_primitive(), 200
        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
    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
    def get(self, project_id):
        """
        Get all chat messages for a project
        ---
        tags:
          - comments
        produces:
          - application/json
        parameters:
            - name: project_id
              in: path
              description: Project ID to attach the chat message to
              required: true
              type: integer
              default: 1
            - in: query
              name: page
              description: Page of results user requested
              type: integer
              default: 1
            - in: query
              name: perPage
              description: Number of elements per page.
              type: integer
              default: 20
        responses:
            200:
                description: All messages
            404:
                description: No chat messages on project
            500:
                description: Internal Server Error
        """
        try:
            ProjectService.get_project_by_id(project_id)
        except NotFound as e:
            current_app.logger.error(f"Error validating project: {str(e)}")
            return {"Error": "Project not found"}, 404

        try:
            page = int(
                request.args.get("page")) if request.args.get("page") else 1
            per_page = int(request.args.get("perPage", 20))
            project_messages = ChatService.get_messages(
                project_id, page, per_page)
            return project_messages.to_primitive(), 200
        except NotFound:
            return {"Error": "Project not found"}, 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
    def post(self, project_id: int):
        """
        Set a project as favorite
        ---
        tags:
            - favorites
        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
        responses:
            200:
                description: New favorite created
            400:
                description: Invalid Request
            401:
                description: Unauthorized - Invalid credentials
            500:
                description: Internal Server Error
        """
        try:
            authenticated_user_id = token_auth.current_user()
            favorite_dto = ProjectFavoriteDTO()
            favorite_dto.project_id = project_id
            favorite_dto.user_id = authenticated_user_id
            favorite_dto.validate()
        except DataError as e:
            current_app.logger.error(f"Error validating request: {str(e)}")
            return str(e), 400
        try:
            ProjectService.favorite(project_id, authenticated_user_id)
        except NotFound:
            return {"Error": "Project Not Found"}, 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 {"project_id": project_id}, 200
Exemple #5
0
 def get(self):
     """
     Get featured projects
     ---
     tags:
         - projects
     produces:
         - application/json
     parameters:
         - in: header
           name: Authorization
           description: Base64 encoded session token
           required: false
           type: string
           default: Token sessionTokenHere==
     responses:
         200:
             description: Featured projects
         500:
             description: Internal Server Error
     """
     try:
         preferred_locale = request.environ.get("HTTP_ACCEPT_LANGUAGE")
         projects_dto = ProjectService.get_featured_projects(preferred_locale)
         return projects_dto.to_primitive(), 200
     except Exception as e:
         error_msg = f"FeaturedProjects GET - unhandled error: {str(e)}"
         current_app.logger.critical(error_msg)
         return {"Error": error_msg}, 500
    def update_stats_after_task_state_change(
        project_id: int,
        user_id: int,
        last_state: TaskStatus,
        new_state: TaskStatus,
        action="change",
    ):
        """ Update stats when a task has had a state change """

        if new_state in [
            TaskStatus.READY,
            TaskStatus.LOCKED_FOR_VALIDATION,
            TaskStatus.LOCKED_FOR_MAPPING,
        ]:
            return  # No stats to record for these states

        project = ProjectService.get_project_by_id(project_id)
        user = UserService.get_user_by_id(user_id)

        project, user = StatsService._update_tasks_stats(
            project, user, last_state, new_state, action
        )
        UserService.upsert_mapped_projects(user_id, project_id)
        project.last_updated = timestamp()

        # Transaction will be saved when task is saved
        return project, user
    def get_latest_activity(project_id: int, page: int) -> ProjectActivityDTO:
        """ Gets all the activity on a project """

        if not ProjectService.exists(project_id):
            raise NotFound

        results = (db.session.query(
            TaskHistory.id,
            TaskHistory.task_id,
            TaskHistory.action,
            TaskHistory.action_date,
            TaskHistory.action_text,
            User.username,
        ).join(User).filter(TaskHistory.project_id == project_id,
                            TaskHistory.action != "COMMENT").order_by(
                                TaskHistory.action_date.desc()).paginate(
                                    page, 10, True))

        activity_dto = ProjectActivityDTO()
        for item in results.items:
            history = TaskHistoryDTO()
            history.task_id = item.id
            history.task_id = item.task_id
            history.action = item.action
            history.action_text = item.action_text
            history.action_date = item.action_date
            history.action_by = item.username
            activity_dto.activity.append(history)

        activity_dto.pagination = Pagination(results)
        return activity_dto
Exemple #8
0
    def map_all_tasks(project_id: int, user_id: int):
        """ Marks all tasks on a project as mapped """
        tasks_to_map = Task.query.filter(
            Task.project_id == project_id,
            Task.task_status.notin_([
                TaskStatus.BADIMAGERY.value,
                TaskStatus.MAPPED.value,
                TaskStatus.VALIDATED.value,
            ]),
        ).all()

        for task in tasks_to_map:
            if TaskStatus(task.task_status) not in [
                    TaskStatus.LOCKED_FOR_MAPPING,
                    TaskStatus.LOCKED_FOR_VALIDATION,
            ]:
                # Only lock tasks that are not already locked to avoid double lock issue
                task.lock_task_for_mapping(user_id)

            task.unlock_task(user_id, new_state=TaskStatus.MAPPED)

        # Set counters to fully mapped
        project = ProjectService.get_project_by_id(project_id)
        project.tasks_mapped = project.total_tasks - project.tasks_bad_imagery
        project.save()
Exemple #9
0
    def lock_task_for_mapping(lock_task_dto: LockTaskDTO) -> TaskDTO:
        """
        Sets the task_locked status to locked so no other user can work on it
        :param lock_task_dto: DTO with data needed to lock the task
        :raises TaskServiceError
        :return: Updated task, or None if not found
        """
        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")

        user_can_map, error_reason = ProjectService.is_user_permitted_to_map(
            lock_task_dto.project_id, lock_task_dto.user_id)
        if not user_can_map:
            if error_reason == MappingNotAllowed.USER_NOT_ACCEPTED_LICENSE:
                raise UserLicenseError(
                    "User must accept license to map this task")
            else:
                raise MappingServiceError(
                    f"Mapping not allowed because: {error_reason.name}")

        task.lock_task_for_mapping(lock_task_dto.user_id)
        return task.as_dto_with_instructions(lock_task_dto.preferred_locale)
 def get(self, project_id):
     """
     Get contributions by day for a project
     ---
     tags:
       - projects
     produces:
       - application/json
     parameters:
         - name: project_id
           in: path
           description: Unique project ID
           required: true
           type: integer
           default: 1
     responses:
         200:
             description: Project contributions by day
         404:
             description: Not found
         500:
             description: Internal Server Error
     """
     try:
         contribs = ProjectService.get_contribs_by_day(project_id)
         return contribs.to_primitive(), 200
     except NotFound:
         return {"Error": "Project not found"}, 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
Exemple #11
0
 def get(self):
     """
     Gets any locked task on the project for the logged in user
     ---
     tags:
         - mapping
     produces:
         - application/json
     parameters:
         - in: header
           name: Authorization
           description: Base64 encoded session token
           required: true
           type: string
           default: Token sessionTokenHere==
     responses:
         200:
             description: Task user is working on
         401:
             description: Unauthorized - Invalid credentials
         404:
             description: User is not working on any tasks
         500:
             description: Internal Server Error
     """
     try:
         locked_tasks = ProjectService.get_task_for_logged_in_user(
             token_auth.current_user())
         return locked_tasks.to_primitive(), 200
     except Exception as e:
         error_msg = f"UsersQueriesOwnLockedAPI - unhandled error: {str(e)}"
         current_app.logger.critical(error_msg)
         return {"Error": error_msg}, 500
Exemple #12
0
    def get(self, project_id: int, annotation_type: str = None):
        """
        Get all task annotations for a project
        ---
        tags:
            - annotations
        produces:
            - application/json
        parameters:
            - name: project_id
              in: path
              description: The ID of the project
              required: true
              type: integer
            - name: annotation_type
              in: path
              description: The type of annotation to fetch
              required: false
              type: integer
        responses:
            200:
                description: Project Annotations
            404:
                description: Project or annotations not found
            500:
                description: Internal Server Error
        """
        try:
            ProjectService.exists(project_id)
        except NotFound as e:
            current_app.logger.error(f"Error validating project: {str(e)}")
            return {"Error": "Project not found"}, 404

        try:
            if annotation_type:
                annotations = TaskAnnotation.get_task_annotations_by_project_id_type(
                    project_id, annotation_type
                )
            else:
                annotations = TaskAnnotation.get_task_annotations_by_project_id(
                    project_id
                )
            return annotations.to_primitive(), 200
        except NotFound:
            return {"Error": "Annotations not found"}, 404
Exemple #13
0
    def test_user_not_permitted_to_map_if_already_locked_tasks(
        self, mock_project, mock_user_tasks
    ):
        # Arrange
        mock_project.return_value = Project()
        mock_user_tasks.return_value = LockedTasksForUser()

        # Act / Assert
        self.assertFalse(ProjectService.get_task_for_logged_in_user(1).locked_tasks)
Exemple #14
0
    def create_or_update_project_interests(project_id, interests):
        project = ProjectService.get_project_by_id(project_id)
        project.create_or_update_interests(interests)

        # Return DTO.
        dto = InterestsListDTO()
        dto.interests = [i.as_dto() for i in project.interests]

        return dto
Exemple #15
0
    def get(self, project_id):
        """
        Get AOI of Project
        ---
        tags:
            - projects
        produces:
            - application/json
        parameters:
            - 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 false if file download not preferred
              default: True
        responses:
            200:
                description: Project found
            403:
                description: Forbidden
            404:
                description: Project not found
            500:
                description: Internal Server Error
        """
        try:
            as_file = (
                strtobool(request.args.get("as_file"))
                if request.args.get("as_file")
                else True
            )

            project_aoi = ProjectService.get_project_aoi(project_id)

            if as_file:
                return send_file(
                    io.BytesIO(geojson.dumps(project_aoi).encode("utf-8")),
                    mimetype="application/json",
                    as_attachment=True,
                    attachment_filename=f"{str(project_id)}.geojson",
                )

            return project_aoi, 200
        except NotFound:
            return {"Error": "Project Not Found"}, 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
    def get(self, project_id):
        """
        Get all user activity on a project
        ---
        tags:
          - projects
        produces:
          - application/json
        parameters:
            - name: project_id
              in: path
              description: Unique project ID
              required: true
              type: integer
              default: 1
            - in: query
              name: page
              description: Page of results user requested
              type: integer
        responses:
            200:
                description: Project activity
            404:
                description: No activity
            500:
                description: Internal Server Error
        """
        try:
            ProjectService.exists(project_id)
        except NotFound as e:
            current_app.logger.error(f"Error validating project: {str(e)}")
            return {"Error": "Project not found"}, 404

        try:
            page = int(
                request.args.get("page")) if request.args.get("page") else 1
            activity = StatsService.get_latest_activity(project_id, page)
            return activity.to_primitive(), 200
        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
    def lock_tasks_for_validation(
            validation_dto: LockForValidationDTO) -> TaskDTOs:
        """
        Lock supplied tasks for validation
        :raises ValidatorServiceError
        """
        # Loop supplied tasks to check they can all be locked for validation
        tasks_to_lock = []
        for task_id in validation_dto.task_ids:
            task = Task.get(task_id, validation_dto.project_id)

            if task is None:
                raise NotFound(f"Task {task_id} not found")

            if TaskStatus(task.task_status) not in [
                    TaskStatus.MAPPED,
                    TaskStatus.INVALIDATED,
                    TaskStatus.BADIMAGERY,
            ]:
                raise ValidatorServiceError(
                    f"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"
                )

            tasks_to_lock.append(task)

        user_can_validate, error_reason = ProjectService.is_user_permitted_to_validate(
            validation_dto.project_id, validation_dto.user_id)

        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_ALREADY_HAS_TASK_LOCKED:
                raise ValidatorServiceError("User already has a task locked")
            else:
                raise ValidatorServiceError(
                    f"Validation not allowed because: {error_reason}")

        # Lock all tasks for validation
        dtos = []
        for task in tasks_to_lock:
            task.lock_task_for_validating(validation_dto.user_id)
            dtos.append(
                task.as_dto_with_instructions(validation_dto.preferred_locale))

        task_dtos = TaskDTOs()
        task_dtos.tasks = dtos

        return task_dtos
    def test_user_not_permitted_to_map_if_already_locked_tasks(
            self, mock_project, mock_user_tasks):
        # Arrange
        mock_project.return_value = Project()
        mock_user_tasks.return_value = LockedTasksForUser()
        mock_user_tasks.return_value.lockedTasks = [1]

        # Act
        allowed, reason = ProjectService.is_user_permitted_to_map(1, 1)

        # Assert
        self.assertFalse(allowed)
    def update_project_stats(project_id: int):
        project = ProjectService.get_project_by_id(project_id)
        tasks = Task.query.filter(Task.project_id == project_id)

        project.total_tasks = tasks.count()
        project.tasks_mapped = tasks.filter(
            Task.task_status == TaskStatus.MAPPED.value).count()
        project.tasks_validated = tasks.filter(
            Task.task_status == TaskStatus.VALIDATED.value).count()
        project.tasks_bad_imagery = tasks.filter(
            Task.task_status == TaskStatus.BADIMAGERY.value).count()
        project.save()
    def test_user_not_permitted_to_map_if_user_is_blocked(
            self, mock_project, mock_user_blocked):

        # Arrange
        mock_project.return_value = Project()
        mock_user_blocked.return_value = True

        # Act
        allowed, reason = ProjectService.is_user_permitted_to_map(1, 1)

        # Assert
        self.assertFalse(allowed)
        self.assertEqual(reason, MappingNotAllowed.USER_NOT_ON_ALLOWED_LIST)
Exemple #21
0
    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).all()

        for task in badimagery_tasks:
            task.lock_task_for_mapping(user_id)
            task.unlock_task(user_id, new_state=TaskStatus.READY)

        # Reset bad imagery counter
        project = ProjectService.get_project_by_id(project_id)
        project.tasks_bad_imagery = 0
        project.save()
    def get(self, project_id):
        """
        Get all user contributions on a project
        ---
        tags:
          - projects
        produces:
          - application/json
        parameters:
            - name: project_id
              in: path
              description: Unique project ID
              required: true
              type: integer
              default: 1
        responses:
            200:
                description: User contributions
            404:
                description: No contributions
            500:
                description: Internal Server Error
        """
        try:
            ProjectService.get_project_by_id(project_id)
        except NotFound as e:
            current_app.logger.error(f"Error validating project: {str(e)}")
            return {"Error": "Project not found"}, 404

        try:
            contributions = StatsService.get_user_contributions(project_id)
            return contributions.to_primitive(), 200
        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
Exemple #23
0
    def test_user_cant_map_if_project_not_published(
        self, mock_project, mock_user_blocked, mock_user_pm_status
    ):
        # Arrange
        stub_project = Project()
        stub_project.status = ProjectStatus.DRAFT.value
        mock_project.return_value = stub_project

        mock_user_pm_status.return_value = False
        mock_user_blocked.return_value = False

        # Act
        allowed, reason = ProjectService.is_user_permitted_to_map(1, 1)

        # Assert
        self.assertFalse(allowed)
        self.assertEqual(reason, MappingNotAllowed.PROJECT_NOT_PUBLISHED)
Exemple #24
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")
Exemple #25
0
    def test_featured_projects_service(self):
        self.test_project, self.test_user = create_canned_project()

        # Featured a not created project.
        with self.assertRaises(NotFound):
            ProjectService.set_project_as_featured(project_id=100)

        # Feature an already created project.
        ProjectService.set_project_as_featured(project_id=self.test_project.id)

        # List all featured projects.
        featured_projects = ProjectService.get_featured_projects(None)
        self.assertEqual(len(featured_projects.results), 1)

        # Unfeature project.
        ProjectService.unset_project_as_featured(
            project_id=self.test_project.id)
        # List all featured projects.
        featured_projects = ProjectService.get_featured_projects(None)
        self.assertEqual(len(featured_projects.results), 0)
Exemple #26
0
    def test_user_not_permitted_to_map_if_user_has_not_accepted_license(
        self, mock_project, mock_user_tasks, mock_user_blocked, mock_user_service
    ):
        # Arrange
        stub_project = Project()
        stub_project.status = ProjectStatus.PUBLISHED.value
        stub_project.license_id = 11

        mock_project.return_value = stub_project
        mock_user_tasks.return_value = LockedTasksForUser()
        mock_user_tasks.return_value.locked_tasks = []
        mock_user_service.return_value = False
        mock_user_blocked.return_value = False

        # Act
        allowed, reason = ProjectService.is_user_permitted_to_map(1, 1)

        # Assert
        self.assertFalse(allowed)
Exemple #27
0
    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 """
        # 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,
                TaskStatus.LOCKED_FOR_VALIDATION,
                TaskStatus.READY,
        ]:

            last_action = TaskHistory.get_last_action(task.project_id, task.id)

            # User requesting task made the last change, so they are allowed to undo it.
            if last_action.user_id == int(
                    logged_in_user_id
            ) or ProjectService.is_user_permitted_to_validate(
                    task.project_id, logged_in_user_id):
                return True

        return False
    def get(self, project_id: int):
        """
        Validate that project is favorited
        ---
        tags:
            - favorites
        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
        responses:
            200:
                description: Project favorite
            400:
                description: Invalid Request
            401:
                description: Unauthorized - Invalid credentials
            500:
                description: Internal Server Error
        """
        try:
            user_id = token_auth.current_user()
            favorited = ProjectService.is_favorited(project_id, user_id)
            if favorited is True:
                return {"favorited": True}, 200

            return {"favorited": False}, 200
        except NotFound:
            return {"Error": "Project Not Found"}, 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
Exemple #29
0
 def get(self):
     """
     Gets details of any locked task for the logged in user
     ---
     tags:
         - mapping
     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
           required: true
           default: en
     responses:
         200:
             description: Task user is working on
         401:
             description: Unauthorized - Invalid credentials
         404:
             description: User is not working on any tasks
         500:
             description: Internal Server Error
     """
     try:
         preferred_locale = request.environ.get("HTTP_ACCEPT_LANGUAGE")
         locked_tasks = ProjectService.get_task_details_for_logged_in_user(
             token_auth.current_user(), preferred_locale
         )
         return locked_tasks.to_primitive(), 200
     except NotFound:
         return {"Error": "User has no locked tasks"}, 404
     except Exception as e:
         error_msg = f"UsersQueriesOwnLockedDetailsAPI - unhandled error: {str(e)}"
         current_app.logger.critical(error_msg)
         return {"Error": error_msg}, 500
 def get(self, project_id, username):
     """
     Get detailed stats about user
     ---
     tags:
       - projects
     produces:
       - application/json
     parameters:
         - name: project_id
           in: path
           description: Unique project ID
           required: true
           type: integer
           default: 1
         - name: username
           in: path
           description: Mapper's OpenStreetMap username
           required: true
           type: string
           default: Thinkwhere
     responses:
         200:
             description: User found
         404:
             description: User not found
         500:
             description: Internal Server Error
     """
     try:
         stats_dto = ProjectService.get_project_user_stats(
             project_id, username)
         return stats_dto.to_primitive(), 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 fetch user statistics for project"
         }, 500