def update_stats_after_task_state_change(project_id: int, user_id: int, new_state: TaskStatus, task_id: int): """ 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) if new_state == TaskStatus.MAPPED: StatsService._set_counters_after_mapping(project, user) elif new_state == TaskStatus.INVALIDATED: StatsService._set_counters_after_invalidated(task_id, project, user) elif new_state == TaskStatus.VALIDATED: StatsService._set_counters_after_validated(project, user) elif new_state == TaskStatus.BADIMAGERY: StatsService._set_counters_after_bad_imagery(project) UserService.upsert_mapped_projects(user_id, project_id) project.last_updated = timestamp() # Transaction will be saved when task is saved return project, user
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: int): """ Gets project summary --- tags: - mapping produces: - application/json parameters: - in: header name: Accept-Language description: Language user is requesting type: string required: true default: en - name: project_id in: path description: The ID of the project required: true type: integer default: 1 responses: 200: description: Project Summary 404: description: Project not found 500: description: Internal Server Error """ try: preferred_locale = request.environ.get('HTTP_ACCEPT_LANGUAGE') summary = ProjectService.get_project_summary(project_id, preferred_locale) return summary.to_primitive(), 200 except NotFound: return {"Error": "Project not found"}, 404 except Exception as e: error_msg = f'Project Summary GET - 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: - user produces: - application/json parameters: - name: project_id in: path required: true type: integer default: 1 - name: username in: path description: The users 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": error_msg}, 500
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()
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) 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 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()
def send_message_after_comment(comment_from: int, comment: str, task_id: int, project_id: int): """ Will send a canned message to anyone @'d in a comment """ usernames = MessageService._parse_message_for_username(comment) if len(usernames) == 0: return # Nobody @'d so return task_link = MessageService.get_task_link(project_id, task_id) project_title = ProjectService.get_project_title(project_id) for username in usernames: try: user = UserService.get_user_by_username(username) except NotFound: continue # If we can't find the user, keep going no need to fail message = Message() message.from_user_id = comment_from message.to_user_id = user.id message.subject = f'You were mentioned in a comment in Project {project_id} on {task_link}' message.message = comment message.add_message() SMTPService.send_email_alert(user.email_address, user.username)
def get(self, project_id): """ Get HOT Project for mapping --- tags: - mapping produces: - application/json parameters: - in: header name: Accept-Language description: Language user is requesting type: string required: true default: en - name: project_id in: path description: The 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: 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, request.environ.get('HTTP_ACCEPT_LANGUAGE'), abbreviated) 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 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": error_msg}, 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 test_project_service_raises_error_if_project_not_found( self, mock_project): mock_project.return_value = None with self.assertRaises(NotFound): ProjectService.get_project_by_id(123)
def post(self, project_id: int, annotation_type: str): """ Store new task annotations for tasks of a project --- tags: - task annotations produces: - application/json parameters: - in: header name: Content-Type description: Content type for post body required: true type: string default: application/json - name: project_id in: path description: The unique project ID required: true type: integer - name: annotation_type in: path description: Annotation type required: true type: string - name: Application-Token in: header description: Application token registered with TM required: true type: string - in: body name: body required: true description: JSON object for creating draft project schema: projectId: type: integer required: true annotationType: type: string required: true tasks: type: array required: true items: schema: taskId: type: integer required: true annotationSource: type: string annotationMarkdown: type: string properties: description: JSON object with properties responses: 200: description: Project updated 400: description: Client Error - Invalid Request 404: description: Project or task not found 500: description: Internal Server Error """ if 'Application-Token' in request.headers: application_token = request.headers['Application-Token'] try: is_valid_token = ApplicationService.check_token( application_token) except NotFound as e: current_app.logger.error(f'Invalid token') return {"error": "Invalid token"}, 500 else: current_app.logger.error(f'No token supplied') return {"error": "No token supplied"}, 500 try: annotations = request.get_json() or {} except DataError as e: current_app.logger.error(f'Error validating request: {str(e)}') try: project = ProjectService.get_project_by_id(project_id) except NotFound as e: current_app.logger.error(f'Error validating project: {str(e)}') task_ids = [t['taskId'] for t in annotations['tasks']] # check if task ids are valid tasks = Task.get_tasks(project_id, task_ids) tasks_ids_db = [t.id for t in tasks] if (len(task_ids) != len(tasks_ids_db)): return {"error": 'Invalid task id'}, 500 for annotation in annotations['tasks']: try: TaskAnnotationsService.add_or_update_annotation( annotation, project_id, annotation_type) except DataError as e: current_app.logger.error( f'Error creating annotations: {str(e)}') return {"Error": "Error creating annotations"}, 500 return project_id, 200
def test_user_is_allowed_to_map_if_level_enforced(self, mock_level): # Arrange mock_level.return_value = MappingLevel.ADVANCED # Act / Assert self.assertTrue(ProjectService._is_user_mapping_level_at_or_above_level_requests(MappingLevel.ADVANCED, 1))
def test_user_not_allowed_to_map_if_level_enforced(self, mock_level): # Arrange mock_level.return_value = MappingLevel.BEGINNER # Act / Assert self.assertFalse(ProjectService._is_user_mapping_level_at_or_above_level_requests(MappingLevel.INTERMEDIATE, 1))