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"]),
)
)