def create_result_dto(project, preferred_locale, total_contributors): project_info_dto = ProjectInfo.get_dto_for_locale( project.id, preferred_locale, project.default_locale) list_dto = ListSearchResultDTO() list_dto.project_id = project.id list_dto.locale = project_info_dto.locale list_dto.name = project_info_dto.name list_dto.priority = ProjectPriority(project.priority).name list_dto.mapper_level = MappingLevel(project.mapper_level).name list_dto.short_description = project_info_dto.short_description list_dto.last_updated = project.last_updated list_dto.due_date = project.due_date list_dto.percent_mapped = Project.calculate_tasks_percent( "mapped", project.total_tasks, project.tasks_mapped, project.tasks_validated, project.tasks_bad_imagery, ) list_dto.percent_validated = Project.calculate_tasks_percent( "validated", project.total_tasks, project.tasks_mapped, project.tasks_validated, project.tasks_bad_imagery, ) list_dto.status = ProjectStatus(project.status).name list_dto.active_mappers = Project.get_active_mappers(project.id) list_dto.total_contributors = total_contributors list_dto.country = project.country list_dto.organisation_name = project.organisation_name list_dto.organisation_logo = project.organisation_logo list_dto.campaigns = Project.get_project_campaigns(project.id) return list_dto
def create_canned_project() -> Tuple[Project, User]: """ Generates a canned project in the DB to help with integration tests """ test_aoi_geojson = geojson.loads(json.dumps(get_canned_json("test_aoi.json"))) task_feature = geojson.loads(json.dumps(get_canned_json("splittable_task.json"))) task_non_square_feature = geojson.loads( json.dumps(get_canned_json("non_square_task.json")) ) test_user = create_canned_user() test_project_dto = DraftProjectDTO() test_project_dto.project_name = "Test" test_project_dto.user_id = test_user.id test_project_dto.area_of_interest = test_aoi_geojson test_project = Project() test_project.create_draft_project(test_project_dto) test_project.set_project_aoi(test_project_dto) test_project.total_tasks = 2 # Setup test task test_task = Task.from_geojson_feature(1, task_feature) test_task.task_status = TaskStatus.MAPPED.value test_task.mapped_by = test_user.id test_task.is_square = True test_task2 = Task.from_geojson_feature(2, task_non_square_feature) test_task2.task_status = TaskStatus.READY.value test_task2.is_square = False test_project.tasks.append(test_task) test_project.tasks.append(test_task2) test_project.create() return test_project, test_user
def test_update_after_flagging_bad_imagery(self): # Arrange test_project = Project() test_project.tasks_bad_imagery = 0 test_user = User() test_user.tasks_invalidated = 0 # Act test_project, test_user = StatsService._update_tasks_stats( test_project, test_user, TaskStatus.READY, TaskStatus.BADIMAGERY) # Assert self.assertEqual(test_project.tasks_bad_imagery, 1)
def test_update_after_mapping_increments_counter(self): # Arrange test_project = Project() test_project.tasks_mapped = 0 test_user = User() test_user.tasks_mapped = 0 # Act test_project, test_user = StatsService._update_tasks_stats( test_project, test_user, TaskStatus.READY, TaskStatus.MAPPED) # Assert self.assertEqual(test_project.tasks_mapped, 1) self.assertEqual(test_user.tasks_mapped, 1)
def test_same_state_keeps_counter(self): # Arrange test_project = Project() test_project.tasks_mapped = 0 test_user = User() test_user.tasks_mapped = 0 # Act test_project, test_user = StatsService._update_tasks_stats( test_project, test_user, TaskStatus.MAPPED, TaskStatus.MAPPED) # Assert self.assertEqual(test_project.tasks_mapped, 0) self.assertEqual(test_user.tasks_mapped, 0)
def test_update_after_invalidating_bad_imagery_task_sets_counters_correctly(self): # Arrange test_project = Project() test_project.tasks_bad_imagery = 1 test_user = User() test_user.tasks_invalidated = 0 # Act test_project, test_user = StatsService._update_tasks_stats( test_project, test_user, TaskStatus.BADIMAGERY, TaskStatus.INVALIDATED ) # Assert self.assertEqual(test_project.tasks_bad_imagery, 0) self.assertEqual(test_user.tasks_invalidated, 1)
def search_projects(search_dto: ProjectSearchDTO, user) -> ProjectSearchResultsDTO: """ Searches all projects for matches to the criteria provided by the user """ all_results, paginated_results = ProjectSearchService._filter_projects( search_dto, user) if paginated_results.total == 0: raise NotFound() dto = ProjectSearchResultsDTO() dto.results = [ ProjectSearchService.create_result_dto( p, search_dto.preferred_locale, Project.get_project_total_contributions(p[0]), ) for p in paginated_results.items ] dto.pagination = Pagination(paginated_results) if search_dto.omit_map_results: return dto features = [] for project in all_results: # This loop creates a geojson feature collection so you can see all active projects on the map properties = { "projectId": project.id, "priority": ProjectPriority(project.priority).name, } # centroid = project.centroid feature = geojson.Feature(geometry=geojson.loads(project.centroid), properties=properties) features.append(feature) feature_collection = geojson.FeatureCollection(features) dto.map_results = feature_collection return dto
def _attach_tasks_to_project(draft_project: Project, tasks_geojson): """ Validates then iterates over the array of tasks and attach them to the draft project :param draft_project: Draft project in scope :param tasks_geojson: GeoJSON feature collection of mapping tasks :raises InvalidGeoJson, InvalidData """ tasks = geojson.loads(json.dumps(tasks_geojson)) if type(tasks) is not geojson.FeatureCollection: raise InvalidGeoJson( "Tasks: Invalid GeoJson must be FeatureCollection") is_valid_geojson = geojson.is_valid(tasks) if is_valid_geojson["valid"] == "no": raise InvalidGeoJson( f"Tasks: Invalid FeatureCollection - {is_valid_geojson['message']}" ) task_count = 1 for feature in tasks["features"]: try: task = Task.from_geojson_feature(task_count, feature) except (InvalidData, InvalidGeoJson) as e: raise e draft_project.tasks.append(task) task_count += 1 task_count -= 1 # Remove last increment before falling out loop draft_project.total_tasks = task_count
def is_user_action_permitted_on_project(authenticated_user_id: int, project_id: int) -> bool: """ Is user action permitted on project""" project = Project.get(project_id) author_id = project.author_id allowed_roles = [TeamRoles.PROJECT_MANAGER.value] is_admin = UserService.is_user_an_admin(authenticated_user_id) is_author = UserService.is_user_the_project_author( authenticated_user_id, author_id) is_org_manager = False if hasattr(project, "organisation_id") and project.organisation_id: org_id = project.organisation_id org = OrganisationService.get_organisation_by_id_as_dto( org_id, authenticated_user_id) if org.is_manager: is_org_manager = True is_manager_team = None if hasattr(project, "project_teams") and project.project_teams: teams_dto = TeamService.get_project_teams_as_dto(project_id) if teams_dto.teams: teams_allowed = [ team_dto for team_dto in teams_dto.teams if team_dto.role in allowed_roles ] user_membership = [ team_dto.team_id for team_dto in teams_allowed if TeamService.is_user_member_of_team( team_dto.team_id, authenticated_user_id) ] if user_membership: is_manager_team = True return is_admin or is_author or is_org_manager or is_manager_team
def test_update_after_invalidating_mapped_task_sets_counters_correctly( self): # Arrange test_project = Project() test_project.tasks_mapped = 1 test_user = User() test_user.tasks_invalidated = 0 # Act test_project, test_user = StatsService._update_tasks_stats( test_project, test_user, TaskStatus.MAPPED, TaskStatus.INVALIDATED) # Assert self.assertEqual(test_project.tasks_mapped, 0) self.assertEqual(test_user.tasks_invalidated, 1)
def _get_project_by_id(project_id: int) -> Project: project = Project.get(project_id) if project is None: raise NotFound() return project
def test_project_can_be_cloned(self): self.test_project, self.test_user = create_canned_project() # Arrange self.update_project_with_info() # Act original_id = copy.copy(self.test_project.id) cloned_project = Project.clone(original_id, self.test_user.id) self.assertTrue(cloned_project) self.assertEqual(cloned_project.project_info[0].name, "Thinkwhere Test") # Tidy Up cloned_project.delete() original_project = Project.get( original_id ) # SQLAlchemy is hanging on to a ref to the old project original_project.delete()
def test_get_intersecting_projects( self, get_dto_for_locale, _get_intersecting_projects, get_user_by_username, validate_bbox_area, _make_4326_polygon_from_bbox, ): if self.skip_tests: return # arrange _make_4326_polygon_from_bbox mock _make_4326_polygon_from_bbox.return_value = Polygon([ (34.68826225820438, -12.59912449955007), (34.68826225820438, -11.57858317689196), (32.50198296132938, -11.57858317689196), (32.50198296132938, -12.59912449955007), (34.68826225820438, -12.59912449955007), ]) # arrange validate_bbox_area mock validate_bbox_area.return_value = True # arrange get_user_by_username mock get_user_by_username.return_value = User(id=3488526) # arrange _get_intersecting_projects mock polygon = json.dumps(get_canned_json("search_bbox_feature.json")) project = Project(id=2274, status=0, default_locale="en", geometry=polygon) projects = [project] _get_intersecting_projects.return_value = projects # arrange get_dto_for_locale mock get_dto_for_locale.return_value = ProjectInfo( name="PEPFAR Kenya: Homa Bay") # arrange dto dto = ProjectSearchBBoxDTO() dto.bbox = map(float, "34.404,-1.034, 34.717,-0.624".split(",")) dto.preferred_locale = "en" dto.input_srid = 4326 dto.project_author = 3488526 dto.validate() # arrange expected result expected = json.dumps(get_canned_json("search_bbox_result.json")) # act # result = ProjectSearchService.get_projects_geojson(dto) # assert self.assertEqual(str(expected), str(expected))
def test_split_task_helper( self, mock_task_get, mock_task_get_max_task_id_for_project, mock_task_create, mock_task_delete, mock_project_get, mock_project_save, mock_project_tasks, mock_instructions, ): if self.skip_tests: return # arrange task_stub = Task() task_stub.id = 1 task_stub.project_id = 1 task_stub.task_status = 1 task_stub.locked_by = 1234 task_stub.lock_holder = 1234 task_stub.is_square = True task_stub.x = 16856 task_stub.y = 17050 task_stub.zoom = 15 task_stub.geometry = shape.from_shape( Polygon( [ (5.1855468740711421, 7.2970875628719796), (5.1855468740711421, 7.3079847788619219), (5.1965332021941588, 7.3079847788619219), (5.1965332021941588, 7.2970875628719796), (5.1855468740711421, 7.2970875628719796), ] ) ) mock_task_get.return_value = task_stub mock_task_get_max_task_id_for_project.return_value = 1 mock_project_get.return_value = Project() mock_project_tasks.return_value = [task_stub] splitTaskDTO = SplitTaskDTO() splitTaskDTO.user_id = 1234 splitTaskDTO.project_id = 1 splitTaskDTO.task_id = 1 # act result = SplitService.split_task(splitTaskDTO) # assert self.assertEqual(4, len(result.tasks))
def delete_tasks(project_id: int, tasks_ids): # Validate project exists. project = Project.get(project_id) if project is None: raise NotFound({"project": project_id}) tasks = [{"id": i, "obj": Task.get(i, project_id)} for i in tasks_ids] # In case a task is not found. not_found = [t["id"] for t in tasks if t["obj"] is None] if len(not_found) > 0: raise NotFound({"tasks": not_found}) # Delete task one by one. [t["obj"].delete() for t in tasks]
def transfer_project_to(project_id: int, transfering_user_id: int, username: str): """ Transfers project from old owner (transfering_user_id) to new owner (username) """ project = Project.get(project_id) # Check permissions for the user (transferring_user_id) who initiatied the action if not ProjectAdminService.is_user_action_permitted_on_project( transfering_user_id, project_id): raise ValueError("User action not permitted") new_owner = UserService.get_user_by_username(username) # Check permissions for the new owner - must be an admin or project's org manager or a PM team member if not ProjectAdminService.is_user_action_permitted_on_project( new_owner.id, project_id): raise ValueError("User action not permitted") else: project.save()
def is_user_action_permitted_on_project(authenticated_user_id: int, project_id: int) -> bool: """ Is user action permitted on project""" project = Project.get(project_id) author_id = project.author_id allowed_roles = [TeamRoles.PROJECT_MANAGER.value] is_admin = UserService.is_user_an_admin(authenticated_user_id) is_author = UserService.is_user_the_project_author( authenticated_user_id, author_id) is_org_manager = False is_manager_team = False if not (is_admin or is_author): if hasattr(project, "organisation_id") and project.organisation_id: org_id = project.organisation_id is_org_manager = OrganisationService.is_user_an_org_manager( org_id, authenticated_user_id) if not is_org_manager: is_manager_team = TeamService.check_team_membership( project_id, allowed_roles, authenticated_user_id) return is_admin or is_author or is_org_manager or is_manager_team
def create_draft_project(draft_project_dto: DraftProjectDTO) -> int: """ Validates and then persists draft projects in the DB :param draft_project_dto: Draft Project DTO with data from API :raises InvalidGeoJson :returns ID of new draft project """ user_id = draft_project_dto.user_id is_admin = UserService.is_user_an_admin(user_id) user_orgs = OrganisationService.get_organisations_managed_by_user_as_dto( user_id) is_org_manager = len(user_orgs.organisations) > 0 # First things first, we need to validate that the author_id is a PM. issue #1715 if not (is_admin or is_org_manager): user = UserService.get_user_by_id(user_id) raise (ProjectAdminServiceError( f"User {user.username} is not permitted to create project")) # If we're cloning we'll copy all the project details from the clone, otherwise create brand new project if draft_project_dto.cloneFromProjectId: draft_project = Project.clone(draft_project_dto.cloneFromProjectId, user_id) else: draft_project = Project() draft_project.create_draft_project(draft_project_dto) draft_project.set_project_aoi(draft_project_dto) # if arbitrary_tasks requested, create tasks from aoi otherwise use tasks in DTO if draft_project_dto.has_arbitrary_tasks: tasks = GridService.tasks_from_aoi_features( draft_project_dto.area_of_interest) draft_project.task_creation_mode = TaskCreationMode.ARBITRARY.value else: tasks = draft_project_dto.tasks ProjectAdminService._attach_tasks_to_project(draft_project, tasks) if draft_project_dto.cloneFromProjectId: draft_project.save() # Update the clone else: draft_project.create() # Create the new project draft_project.set_default_changeset_comment() draft_project.set_country_info() return draft_project.id
def get_projects_for_admin(admin_id: int, preferred_locale: str, search_dto: ProjectSearchDTO): """ Get all projects for provided admin """ ProjectSearchService.create_search_query() return Project.get_projects_for_admin(admin_id, preferred_locale, search_dto)
def split_task(split_task_dto: SplitTaskDTO) -> TaskDTOs: """ Replaces a task square with 4 smaller tasks at the next OSM tile grid zoom level Validates that task is: - locked for mapping by current user :param split_task_dto: :return: new tasks in a DTO """ # get the task to be split original_task = Task.get(split_task_dto.task_id, split_task_dto.project_id) if original_task is None: raise NotFound() original_geometry = shape.to_shape(original_task.geometry) # check its locked for mapping by the current user if TaskStatus( original_task.task_status) != TaskStatus.LOCKED_FOR_MAPPING: raise SplitServiceError( "Status must be LOCKED_FOR_MAPPING to split") if original_task.locked_by != split_task_dto.user_id: raise SplitServiceError( "Attempting to split a task owned by another user") # create new geometries from the task geometry try: new_tasks_geojson = SplitService._create_split_tasks( original_task.x, original_task.y, original_task.zoom, original_task) except Exception as e: raise SplitServiceError(f"Error splitting task{str(e)}") # create new tasks from the new geojson i = Task.get_max_task_id_for_project(split_task_dto.project_id) new_tasks = [] new_tasks_dto = [] for new_task_geojson in new_tasks_geojson: # Sanity check: ensure the new task geometry intersects the original task geometry new_geometry = shapely_shape(new_task_geojson.geometry) if not new_geometry.intersects(original_geometry): raise InvalidGeoJson( "New split task does not intersect original task") # insert new tasks into database i = i + 1 new_task = Task.from_geojson_feature(i, new_task_geojson) new_task.project_id = split_task_dto.project_id new_task.task_status = TaskStatus.READY.value new_task.create() new_task.task_history.extend(original_task.copy_task_history()) if new_task.task_history: new_task.clear_task_lock() # since we just copied the lock new_task.set_task_history(TaskAction.STATE_CHANGE, split_task_dto.user_id, None, TaskStatus.SPLIT) new_task.set_task_history(TaskAction.STATE_CHANGE, split_task_dto.user_id, None, TaskStatus.READY) new_task.task_status = TaskStatus.READY.value new_tasks.append(new_task) new_task.update() new_tasks_dto.append( new_task.as_dto_with_instructions( split_task_dto.preferred_locale)) # delete original task from the database try: original_task.delete() except Exception: db.session.rollback() # Ensure the new tasks are cleaned up for new_task in new_tasks: new_task.delete() db.session.commit() raise # update project task counts project = Project.get(split_task_dto.project_id) project.total_tasks = project.tasks.count() # update bad imagery because we may have split a bad imagery tile project.tasks_bad_imagery = project.tasks.filter( Task.task_status == TaskStatus.BADIMAGERY.value).count() project.save() # return the new tasks in a DTO task_dtos = TaskDTOs() task_dtos.tasks = new_tasks_dto return task_dtos
def get_all_campaign_tags(preferred_locale): """ Get all campaign tags""" return Project.get_all_campaign_tag(preferred_locale=preferred_locale)
def get_projects_for_admin(admin_id: int, preferred_locale: str, search_dto: ProjectSearchDTO): """ Get all projects for provided admin """ return Project.get_projects_for_admin(admin_id, preferred_locale, search_dto)
def _set_default_changeset_comment(draft_project: Project): """ Sets the default changesset comment when project created """ default_comment = current_app.config["DEFAULT_CHANGESET_COMMENT"] draft_project.changeset_comment = f"{default_comment}-{draft_project.id}" draft_project.save()
def get_all_countries(): """ Get all countries""" return Project.get_all_countries()
def get_all_organisation_tags(preferred_locale): """ Get all org tags""" return Project.get_all_organisations_tag( preferred_locale=preferred_locale)
def exists(project_id: int) -> bool: project = Project.exists(project_id) if project is None: raise NotFound() return True
def test_tasks_state_representation(self): # Arrange test_project = Project() test_project.tasks_mapped = 0 test_project.tasks_validated = 0 test_project.tasks_bad_imagery = 0 test_mapper = User() test_mapper.tasks_mapped = 0 test_mapper.tasks_validated = 0 test_mapper.tasks_invalidated = 0 test_validator = User() test_validator.tasks_mapped = 0 test_validator.tasks_validated = 0 test_validator.tasks_invalidated = 0 test_admin = User() test_admin.tasks_mapped = 0 test_admin.tasks_validated = 0 test_admin.tasks_invalidated = 0 # Mapper marks task as mapped test_project, test_mapper = StatsService._update_tasks_stats( test_project, test_mapper, TaskStatus.READY, TaskStatus.MAPPED) # Validator marks task as bad imagery test_project, test_validator = StatsService._update_tasks_stats( test_project, test_validator, TaskStatus.MAPPED, TaskStatus.BADIMAGERY) # Admin undos marking task as bad imagery test_project, test_admin = StatsService._update_tasks_stats( test_project, test_admin, TaskStatus.BADIMAGERY, TaskStatus.MAPPED, "undo") # Validator marks task as invalid test_project, test_validator = StatsService._update_tasks_stats( test_project, test_validator, TaskStatus.MAPPED, TaskStatus.INVALIDATED) # Mapper marks task as mapped test_project, test_mapper = StatsService._update_tasks_stats( test_project, test_mapper, TaskStatus.INVALIDATED, TaskStatus.MAPPED) # Admin undos marking task as mapped (test_mapper is given to the function though, as the author of the # last_change - compare with MappingServer.undo_mapping() method) test_project, test_mapper = StatsService._update_tasks_stats( test_project, test_mapper, TaskStatus.MAPPED, TaskStatus.INVALIDATED, "undo") # Mapper marks task as mapped test_project, test_mapper = StatsService._update_tasks_stats( test_project, test_mapper, TaskStatus.INVALIDATED, TaskStatus.MAPPED) # Validator marks task as valid test_project, test_validator = StatsService._update_tasks_stats( test_project, test_validator, TaskStatus.MAPPED, TaskStatus.VALIDATED) # Assert self.assertEqual(test_project.tasks_mapped, 0) self.assertEqual(test_project.tasks_validated, 1) self.assertEqual(test_project.tasks_bad_imagery, 0) self.assertEqual(test_mapper.tasks_mapped, 2) self.assertEqual(test_mapper.tasks_validated, 0) self.assertEqual(test_mapper.tasks_invalidated, 0) self.assertEqual(test_validator.tasks_mapped, 0) self.assertEqual(test_validator.tasks_validated, 1) self.assertEqual(test_validator.tasks_invalidated, 1) self.assertEqual(test_admin.tasks_mapped, 0) self.assertEqual(test_admin.tasks_validated, 0) self.assertEqual(test_admin.tasks_invalidated, 0)