backend/services/validator_service.py (288 lines of code) (raw):

from flask import current_app from sqlalchemy import text from backend.models.dtos.mapping_dto import TaskDTOs from backend.models.dtos.stats_dto import Pagination from backend.models.dtos.validator_dto import ( LockForValidationDTO, UnlockAfterValidationDTO, MappedTasks, StopValidationDTO, InvalidatedTask, InvalidatedTasks, ) from backend.models.postgis.statuses import ValidatingNotAllowed from backend.models.postgis.task import ( Task, TaskStatus, TaskHistory, TaskInvalidationHistory, TaskMappingIssue, ) from backend.models.postgis.utils import NotFound, UserLicenseError, timestamp from backend.models.postgis.project_info import ProjectInfo from backend.services.messaging.message_service import MessageService from backend.services.project_service import ProjectService from backend.services.stats_service import StatsService from backend.services.users.user_service import UserService class ValidatorServiceError(Exception): """ Custom exception to notify callers that error has occurred """ def __init__(self, message): if current_app: current_app.logger.debug(message) class ValidatorService: @staticmethod 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 @staticmethod def _user_can_validate_task(user_id: int, mapped_by: int) -> bool: """ check whether a user is able to validate a task. Users cannot validate their own tasks unless they are a PM (admin counts as project manager too) :param user_id: id of user attempting to validate :param mapped_by: id of user who mapped the task :return: Boolean """ is_admin = UserService.is_user_an_admin(user_id) if is_admin: return True else: mapped_by_me = mapped_by == user_id if not mapped_by_me: return True return False @staticmethod def unlock_tasks_after_validation( validated_dto: UnlockAfterValidationDTO, ) -> TaskDTOs: """ Unlocks supplied tasks after validation :raises ValidatorServiceError """ validated_tasks = validated_dto.validated_tasks project_id = validated_dto.project_id user_id = validated_dto.user_id tasks_to_unlock = ValidatorService.get_tasks_locked_by_user( project_id, validated_tasks, user_id ) # Unlock all tasks dtos = [] message_sent_to = [] for task_to_unlock in tasks_to_unlock: task = task_to_unlock["task"] if task_to_unlock["comment"]: # Parses comment to see if any users have been @'d MessageService.send_message_after_comment( validated_dto.user_id, task_to_unlock["comment"], task.id, validated_dto.project_id, ) if ( task_to_unlock["new_state"] == TaskStatus.VALIDATED or task_to_unlock["new_state"] == TaskStatus.INVALIDATED ): # All mappers get a notification if their task has been validated or invalidated. # Only once if multiple tasks mapped if task.mapped_by not in message_sent_to: MessageService.send_message_after_validation( task_to_unlock["new_state"], validated_dto.user_id, task.mapped_by, task.id, validated_dto.project_id, ) message_sent_to.append(task.mapped_by) if task_to_unlock["new_state"] == TaskStatus.VALIDATED: # Set last_validation_date for the mapper to current date task.mapper.last_validation_date = timestamp() # Update stats if user setting task to a different state from previous state prev_status = TaskHistory.get_last_status(project_id, task.id) if prev_status != task_to_unlock["new_state"]: StatsService.update_stats_after_task_state_change( validated_dto.project_id, validated_dto.user_id, prev_status, task_to_unlock["new_state"], ) task_mapping_issues = ValidatorService.get_task_mapping_issues( task_to_unlock ) task.unlock_task( validated_dto.user_id, task_to_unlock["new_state"], task_to_unlock["comment"], issues=task_mapping_issues, ) dtos.append(task.as_dto_with_instructions(validated_dto.preferred_locale)) task_dtos = TaskDTOs() task_dtos.tasks = dtos return task_dtos @staticmethod def stop_validating_tasks(stop_validating_dto: StopValidationDTO) -> TaskDTOs: """ Unlocks supplied tasks after validation :raises ValidatorServiceError """ reset_tasks = stop_validating_dto.reset_tasks project_id = stop_validating_dto.project_id user_id = stop_validating_dto.user_id tasks_to_unlock = ValidatorService.get_tasks_locked_by_user( project_id, reset_tasks, user_id ) dtos = [] for task_to_unlock in tasks_to_unlock: task = task_to_unlock["task"] if task_to_unlock["comment"]: # Parses comment to see if any users have been @'d MessageService.send_message_after_comment( user_id, task_to_unlock["comment"], task.id, project_id ) task.reset_lock(user_id, task_to_unlock["comment"]) dtos.append( task.as_dto_with_instructions(stop_validating_dto.preferred_locale) ) task_dtos = TaskDTOs() task_dtos.tasks = dtos return task_dtos @staticmethod def get_tasks_locked_by_user(project_id: int, unlock_tasks, user_id: int): """ Returns tasks specified by project id and unlock_tasks list if found and locked for validation by user, otherwise raises ValidatorServiceError, NotFound :param project_id: :param unlock_tasks: List of tasks to be unlocked :param user_id: :return: List of Tasks :raises ValidatorServiceError :raises NotFound """ tasks_to_unlock = [] # Loop supplied tasks to check they can all be unlocked for unlock_task in unlock_tasks: task = Task.get(unlock_task.task_id, project_id) if task is None: raise NotFound(f"Task {unlock_task.task_id} not found") current_state = TaskStatus(task.task_status) if current_state != TaskStatus.LOCKED_FOR_VALIDATION: raise ValidatorServiceError( f"Task {unlock_task.task_id} is not LOCKED_FOR_VALIDATION" ) if task.locked_by != user_id: raise ValidatorServiceError( "Attempting to unlock a task owned by another user" ) if hasattr(unlock_task, "status"): # we know what status we ate going to be setting to on unlock new_status = TaskStatus[unlock_task.status] else: new_status = None tasks_to_unlock.append( dict( task=task, new_state=new_status, comment=unlock_task.comment, issues=unlock_task.issues, ) ) return tasks_to_unlock @staticmethod def get_mapped_tasks_by_user(project_id: int) -> MappedTasks: """ Get all mapped tasks on the project grouped by user""" mapped_tasks = Task.get_mapped_tasks_by_user(project_id) return mapped_tasks @staticmethod def get_user_invalidated_tasks( as_validator, username: str, preferred_locale: str, closed=None, project_id=None, page=1, page_size=10, sort_by="updated_date", sort_direction="desc", ) -> InvalidatedTasks: """ Get invalidated tasks either mapped or invalidated by the user """ user = UserService.get_user_by_username(username) query = ( TaskInvalidationHistory.query.filter_by(invalidator_id=user.id) if as_validator else TaskInvalidationHistory.query.filter_by(mapper_id=user.id) ) if closed is not None: query = query.filter_by(is_closed=closed) if project_id is not None: query = query.filter_by(project_id=project_id) results = query.order_by(text(sort_by + " " + sort_direction)).paginate( page, page_size, True ) project_names = {} invalidated_tasks_dto = InvalidatedTasks() for entry in results.items: dto = InvalidatedTask() dto.task_id = entry.task_id dto.project_id = entry.project_id dto.history_id = entry.invalidation_history_id dto.closed = entry.is_closed dto.updated_date = entry.updated_date if dto.project_id not in project_names: project_names[dto.project_id] = ProjectInfo.get_dto_for_locale( dto.project_id, preferred_locale ).name dto.project_name = project_names[dto.project_id] invalidated_tasks_dto.invalidated_tasks.append(dto) invalidated_tasks_dto.pagination = Pagination(results) return invalidated_tasks_dto @staticmethod def invalidate_all_tasks(project_id: int, user_id: int): """ Invalidates all mapped tasks on a project""" mapped_tasks = Task.query.filter( Task.project_id == project_id, ~Task.task_status.in_( [TaskStatus.READY.value, TaskStatus.BADIMAGERY.value] ), ).all() for task in mapped_tasks: 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_validating(user_id) task.unlock_task(user_id, new_state=TaskStatus.INVALIDATED) # Reset counters project = ProjectService.get_project_by_id(project_id) project.tasks_mapped = 0 project.tasks_validated = 0 project.save() @staticmethod def validate_all_tasks(project_id: int, user_id: int): """ Validates all mapped tasks on a project""" tasks_to_validate = Task.query.filter( Task.project_id == project_id, Task.task_status != TaskStatus.BADIMAGERY.value, ).all() for task in tasks_to_validate: task.mapped_by = task.mapped_by or user_id # Ensure we set mapped by value 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_validating(user_id) task.unlock_task(user_id, new_state=TaskStatus.VALIDATED) # Set counters to fully mapped and validated project = ProjectService.get_project_by_id(project_id) project.tasks_mapped = project.total_tasks - project.tasks_bad_imagery project.tasks_validated = project.total_tasks project.save() @staticmethod def get_task_mapping_issues(task_to_unlock: dict): if task_to_unlock["issues"] is None: return None # map ValidationMappingIssue DTOs to TaskMappingIssue instances for any issues # that have count above zero. return list( map( lambda issue_dto: TaskMappingIssue( issue=issue_dto.issue, count=issue_dto.count, mapping_issue_category_id=issue_dto.mapping_issue_category_id, ), filter(lambda issue_dto: issue_dto.count > 0, task_to_unlock["issues"]), ) )