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