def test_unlock_of_not_locked_for_mapping_raises_error(self, mock_task): # Arrange mock_task.return_value = self.task_stub # Act / Assert with self.assertRaises(MappingServiceError): MappingService.unlock_task_after_mapping(MagicMock())
def test_cant_unlock_a_task_you_dont_own(self, mock_task): # Arrange self.task_stub.task_status = TaskStatus.LOCKED_FOR_MAPPING.value self.task_stub.locked_by = 12 mock_task.return_value = self.task_stub # Act / Assert with self.assertRaises(MappingServiceError): MappingService.unlock_task_after_mapping(self.mapped_task_dto)
def test_lock_task_for_mapping_raises_error_if_task_in_invalid_state( self, mock_task): # Arrange self.task_stub.task_status = TaskStatus.MAPPED.value mock_task.return_value = self.task_stub # Act / Assert with self.assertRaises(MappingServiceError): MappingService.lock_task_for_mapping(self.lock_task_dto)
def test_lock_task_for_mapping_raises_error_if_user_has_not_accepted_license( self, mock_task, mock_project): # Arrange mock_task.return_value = self.task_stub mock_project.return_value = False, MappingNotAllowed.USER_NOT_ACCEPTED_LICENSE # Act / Assert with self.assertRaises(UserLicenseError): MappingService.lock_task_for_mapping(self.lock_task_dto)
def test_map_all_sets_counters_correctly(self): if self.skip_tests: return # Act MappingService.map_all_tasks(self.test_project.id, self.test_user.id) # Assert self.assertEqual(self.test_project.tasks_mapped, self.test_project.total_tasks)
def test_mapped_by_is_set_after_mapping_all(self): if self.skip_tests: return # Act MappingService.map_all_tasks(self.test_project.id, self.test_user.id) # Assert for task in self.test_project.tasks: self.assertIsNotNone(task.mapped_by)
def test_if_new_state_not_acceptable_raise_error(self, mock_task): # Arrange self.task_stub.task_status = TaskStatus.LOCKED_FOR_MAPPING.value mock_task.return_value = self.task_stub self.mapped_task_dto.status = TaskStatus.LOCKED_FOR_VALIDATION.name # Act / Assert with self.assertRaises(MappingServiceError): MappingService.unlock_task_after_mapping(self.mapped_task_dto)
def test_lock_task_for_mapping_raises_error_if_user_already_has_locked_task( self, mock_task, mock_project): # Arrange mock_task.return_value = self.task_stub mock_project.return_value = ( False, MappingNotAllowed.USER_ALREADY_HAS_TASK_LOCKED, ) # Act / Assert with self.assertRaises(MappingServiceError): MappingService.lock_task_for_mapping(self.lock_task_dto)
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
def get(self, project_id, task_id): """ Get a task's metadata --- tags: - tasks produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: false type: string default: Token sessionTokenHere== - in: header name: Accept-Language description: Language user is requesting type: string required: true default: en - name: project_id in: path description: Project ID the task is associated with required: true type: integer default: 1 - name: task_id in: path description: Unique task ID required: true type: integer default: 1 responses: 200: description: Task found 404: description: Task not found 500: description: Internal Server Error """ try: preferred_locale = request.environ.get("HTTP_ACCEPT_LANGUAGE") token = request.environ.get("HTTP_AUTHORIZATION") # Login isn't required here, but if we have a token we can find out if the user can undo the task if token: verify_token(token[6:]) user_id = tm.authenticated_user_id task = MappingService.get_task_as_dto(task_id, project_id, preferred_locale, user_id) return task.to_primitive(), 200 except NotFound: return {"Error": "Task Not Found"}, 404 except Exception as e: error_msg = f"TasksRestAPI - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to fetch task"}, 500
def post(self, project_id): """ Map all tasks on a project --- 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 mapped 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"TasksActionsMapAllAPI POST: {str(e)}" return {"Error": error_msg}, 403 try: MappingService.map_all_tasks(project_id, tm.authenticated_user_id) return {"Success": "All tasks mapped"}, 200 except Exception as e: error_msg = f"TasksActionsMapAllAPI POST - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to mapall tasks"}, 500
def post(self, project_id, task_id): """ Undo a task's mapping status --- tags: - tasks 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 - name: project_id in: path description: Project ID the task is associated with required: true type: integer default: 1 - name: task_id in: path description: Unique task ID required: true type: integer default: 1 responses: 200: description: Task found 403: description: Forbidden 404: description: Task not found 500: description: Internal Server Error """ try: preferred_locale = request.environ.get("HTTP_ACCEPT_LANGUAGE") task = MappingService.undo_mapping(project_id, task_id, token_auth.current_user(), preferred_locale) return task.to_primitive(), 200 except NotFound: return {"Error": "Task Not Found"}, 404 except MappingServiceError: return {"Error": "User not permitted to undo task"}, 403 except Exception as e: error_msg = f"Task GET API - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to lock task"}, 500
def get(self, project_id, task_id): """ Get a task's metadata --- tags: - tasks 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: Project ID the task is associated with required: true type: integer default: 1 - name: task_id in: path description: Unique task ID required: true type: integer default: 1 responses: 200: description: Task found 404: description: Task not found 500: description: Internal Server Error """ try: preferred_locale = request.environ.get("HTTP_ACCEPT_LANGUAGE") task = MappingService.get_task_as_dto(task_id, project_id, preferred_locale) return task.to_primitive(), 200 except NotFound: return {"Error": "Task Not Found"}, 404 except Exception as e: error_msg = f"TasksRestAPI - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to fetch task"}, 500
def test_task_is_not_undoable_if_last_change_not_made_by_you( self, last_action, mock_project): # Arrange task_history = TaskHistory(1, 1, 1) task_history.user_id = 2 last_action.return_value = task_history task = Task() task.task_status = TaskStatus.MAPPED.value task.mapped_by = 1 # Act mock_project.return_value = False is_undoable = MappingService._is_task_undoable(1, task) # Assert self.assertFalse(is_undoable)
def test_osm_xml_file_generated_correctly_all_tasks(self, mock_task): if self.skip_tests: return # Arrange task = Task.get(1, self.test_project.id) mock_task.return_value = [task] # Act osm_xml = MappingService.generate_osm_xml(1, None) # Convert XML into a hash that should be identical every time osm_xml_str = osm_xml.decode("utf-8") osm_hash = hashlib.md5(osm_xml_str.encode("utf-8")).hexdigest() # Assert self.assertEqual(osm_hash, "eafd0760a0d372e2ab139e25a2d300f1")
def test_gpx_xml_file_generated_correctly_all_tasks(self, mock_task): if self.skip_tests: return # Arrange task = Task.get(1, self.test_project.id) mock_task.return_value = [task] timestamp = datetime.date(2017, 4, 13) # Act gpx_xml = MappingService.generate_gpx(1, None, timestamp) # Convert XML into a hash that should be identical every time gpx_xml_str = gpx_xml.decode("utf-8") gpx_hash = hashlib.md5(gpx_xml_str.encode("utf-8")).hexdigest() # Assert self.assertEqual(gpx_hash, "b91f7361cc1d6d9433cf393609103272")
def test_unlock_with_status_change_sets_history( self, mock_task, mock_history, mock_update, mock_stats, mock_instructions, mock_state, ): # Arrange self.task_stub.task_status = TaskStatus.LOCKED_FOR_MAPPING.value mock_task.return_value = self.task_stub mock_state.return_value = TaskStatus.LOCKED_FOR_MAPPING # Act test_task = MappingService.unlock_task_after_mapping(self.mapped_task_dto) # Assert self.assertEqual(TaskAction.STATE_CHANGE.name, test_task.task_history[0].action) self.assertEqual(test_task.task_history[0].action_text, TaskStatus.MAPPED.name) self.assertEqual(TaskStatus.MAPPED.name, test_task.task_status)
def test_unlock_with_comment_sets_history( self, mock_task, mock_history, mock_update, mock_stats, mock_instructions, mock_state, ): # Arrange self.task_stub.task_status = TaskStatus.LOCKED_FOR_MAPPING.value self.mapped_task_dto.comment = "Test comment" mock_task.return_value = self.task_stub mock_state.return_value = TaskStatus.LOCKED_FOR_MAPPING # Act test_task = MappingService.unlock_task_after_mapping(self.mapped_task_dto) # Assert self.assertEqual(TaskAction.COMMENT.name, test_task.task_history[0].action) self.assertEqual(test_task.task_history[0].action_text, "Test comment")
def post(self, project_id, task_id): """ Set a task as mapped --- 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: Project ID the task is associated with required: true type: integer default: 1 - name: task_id in: path description: Unique task ID required: true type: integer default: 1 - in: body name: body required: true description: JSON object for unlocking a task schema: id: TaskUpdateUnlock required: - status properties: status: type: string description: The new status for the task default: MAPPED comment: type: string description: Optional user comment about the task default: Comment about the mapping responses: 200: description: Task unlocked 400: description: Client Error 401: description: Unauthorized - Invalid credentials 403: description: Forbidden 404: description: Task not found 500: description: Internal Server Error """ try: authenticated_user_id = token_auth.current_user() mapped_task = MappedTaskDTO(request.get_json()) mapped_task.user_id = authenticated_user_id mapped_task.task_id = task_id mapped_task.project_id = project_id mapped_task.validate() except DataError as e: current_app.logger.error(f"Error validating request: {str(e)}") return {"Error": "Task unlock failed"}, 400 try: task = MappingService.unlock_task_after_mapping(mapped_task) return task.to_primitive(), 200 except NotFound: return {"Error": "Task Not Found"}, 404 except MappingServiceError: return {"Error": "Task unlock failed"}, 403 except Exception as e: error_msg = f"Task Lock API - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Task unlock failed"}, 500 finally: # Refresh mapper level after mapping UserService.check_and_update_mapper_level(authenticated_user_id)
def post(self, project_id, task_id): """ Adds a comment to the task outside of mapping/validation --- tags: - comments 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: Project ID the task is associated with required: true type: integer default: 1 - name: task_id in: path description: Unique task ID required: true type: integer default: 1 - in: body name: body required: true description: JSON object representing the comment schema: id: TaskComment required: - comment properties: comment: type: string description: user comment about the task responses: 200: description: Comment added 400: description: Client Error 401: description: Unauthorized - Invalid credentials 403: description: Forbidden 404: description: Task not found 500: description: Internal Server Error """ try: task_comment = TaskCommentDTO(request.get_json()) task_comment.user_id = tm.authenticated_user_id task_comment.task_id = task_id task_comment.project_id = project_id task_comment.validate() except DataError as e: current_app.logger.error(f"Error validating request: {str(e)}") return {"Error": "Unable to add comment"}, 400 try: task = MappingService.add_task_comment(task_comment) return task.to_primitive(), 201 except NotFound: return {"Error": "Task Not Found"}, 404 except MappingServiceError: return {"Error": "Task update failed"}, 403 except Exception as e: error_msg = f"Task Comment API - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Task update failed"}, 500
def get(self, project_id): """ Get all tasks for a project as GPX --- tags: - tasks produces: - application/xml parameters: - name: project_id in: path description: Project ID the task is associated with required: true type: integer default: 1 - in: query name: tasks type: string description: List of tasks; leave blank for all default: 1,2 - in: query name: as_file type: boolean description: Set to true if file download preferred default: False responses: 200: description: GPX XML 400: description: Client error 404: description: No mapped tasks 500: description: Internal Server Error """ try: current_app.logger.debug("GPX Called") tasks = request.args.get("tasks") as_file = ( strtobool(request.args.get("as_file")) if request.args.get("as_file") else False ) xml = MappingService.generate_gpx(project_id, tasks) if as_file: return send_file( io.BytesIO(xml), mimetype="text.xml", as_attachment=True, attachment_filename=f"HOT-project-{project_id}.gpx", ) return Response(xml, mimetype="text/xml", status=200) except NotFound: return ( {"Error": "Not found; please check the project and task numbers."}, 404, ) except Exception as e: error_msg = f"TasksQueriesGpxAPI - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to fetch task GPX"}, 500
def post(self, project_id, task_id): """ Unlock a task that is locked for mapping resetting it to its last status --- tags: - tasks 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 - name: project_id in: path description: Project ID the task is associated with required: true type: integer default: 1 - name: task_id in: path description: Unique task ID required: true type: integer default: 1 - in: body name: body required: true description: JSON object for unlocking a task schema: id: TaskUpdateStop properties: comment: type: string description: Optional user comment about the task default: Comment about mapping done before stop responses: 200: description: Task unlocked 400: description: Client Error 401: description: Unauthorized - Invalid credentials 403: description: Forbidden 404: description: Task not found 500: description: Internal Server Error """ try: stop_task = StopMappingTaskDTO(request.get_json()) stop_task.user_id = token_auth.current_user() stop_task.task_id = task_id stop_task.project_id = project_id stop_task.preferred_locale = request.environ.get( "HTTP_ACCEPT_LANGUAGE") stop_task.validate() except DataError as e: current_app.logger.error(f"Error validating request: {str(e)}") return {"Error": "Task unlock failed"}, 400 try: task = MappingService.stop_mapping_task(stop_task) return task.to_primitive(), 200 except NotFound: return {"Error": "Task Not Found"}, 404 except MappingServiceError: return {"Error": "Task unlock failed"}, 403 except Exception as e: error_msg = f"Task Lock API - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Task unlock failed"}, 500
def test_get_task_raises_error_if_task_not_found(self, mock_task): mock_task.return_value = None with self.assertRaises(NotFound): MappingService.get_task(12, 12)
def post(self, project_id, task_id): """ Locks a task for mapping --- tags: - tasks 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 - name: project_id in: path description: Project ID the task is associated with required: true type: integer default: 1 - name: task_id in: path description: Unique task ID required: true type: integer default: 1 responses: 200: description: Task locked 400: description: Client Error 401: description: Unauthorized - Invalid credentials 403: description: Forbidden 404: description: Task not found 409: description: User has not accepted license terms of project 500: description: Internal Server Error """ try: lock_task_dto = LockTaskDTO() lock_task_dto.user_id = token_auth.current_user() lock_task_dto.project_id = project_id lock_task_dto.task_id = task_id lock_task_dto.preferred_locale = request.environ.get( "HTTP_ACCEPT_LANGUAGE") lock_task_dto.validate() except DataError as e: current_app.logger.error(f"Error validating request: {str(e)}") return {"Error": "Unable to lock task"}, 400 try: task = MappingService.lock_task_for_mapping(lock_task_dto) return task.to_primitive(), 200 except NotFound: return {"Error": "Task Not Found"}, 404 except MappingServiceError as e: return {"Error": str(e)}, 403 except UserLicenseError: return {"Error": "User not accepted license terms"}, 409 except Exception as e: error_msg = f"Task Lock API - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to lock task"}, 500