def get_task_details_for_logged_in_user(user_id: int, preferred_locale: str): """ if the user is working on a task in the project return it """ tasks = Task.get_locked_tasks_details_for_user(user_id) if len(tasks) == 0: raise NotFound() # TODO put the task details in to a DTO dtos = [] for task in tasks: dtos.append(task.as_dto_with_instructions(preferred_locale)) task_dtos = TaskDTOs() task_dtos.tasks = dtos return task_dtos
def get_recommended_projects(user_name: str, preferred_locale: str): """ Gets all projects a user has mapped or validated on """ from backend.services.project_search_service import ProjectSearchService limit = 20 user = (User.query.with_entities(User.id, User.mapping_level).filter( User.username == user_name).one_or_none()) if user is None: raise NotFound() # Get all projects that the user has contributed sq = (TaskHistory.query.with_entities( TaskHistory.project_id.label("project_id")).distinct( TaskHistory.project_id).filter( TaskHistory.user_id == user.id).subquery()) # Get all campaigns for all contributed projects. campaign_tags = (Project.query.with_entities( Project.campaign.label("tag")).filter( or_(Project.author_id == user.id, Project.id == sq.c.project_id)).subquery()) # Get projects with given campaign tags but without user contributions. query = ProjectSearchService.create_search_query() projs = (query.filter(Project.campaign.any( campaign_tags.c.tag)).limit(limit).all()) # Get only user mapping level projects. len_projs = len(projs) if len_projs < limit: remaining_projs = (query.filter( Project.mapper_level == user.mapping_level).limit( limit - len_projs).all()) projs.extend(remaining_projs) dto = ProjectSearchResultsDTO() # Get all total contributions for each paginated project. contrib_counts = ProjectSearchService.get_total_contributions(projs) zip_items = zip(projs, contrib_counts) dto.results = [ ProjectSearchService.create_result_dto(p, "en", t) for p, t in zip_items ] return dto
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
def create_from_dto(cls, new_organisation_dto: NewOrganisationDTO): """ Creates a new organisation from a DTO """ new_org = cls() new_org.name = new_organisation_dto.name new_org.logo = new_organisation_dto.logo new_org.url = new_organisation_dto.url for manager in new_organisation_dto.managers: user = User.get_by_username(manager) if user is None: raise NotFound(f"User {manager} Not Found") new_org.managers.append(user) new_org.create() return new_org
def get_team_as_dto(team_id: int, user_id: int, abbreviated: bool) -> TeamDetailsDTO: team = TeamService.get_team_by_id(team_id) if team is None: raise NotFound() team_dto = TeamDetailsDTO() team_dto.team_id = team.id team_dto.name = team.name team_dto.invite_only = team.invite_only team_dto.visibility = TeamVisibility(team.visibility).name team_dto.description = team.description team_dto.logo = team.organisation.logo team_dto.organisation = team.organisation.name team_dto.organisation_id = team.organisation.id team_dto.organisation_slug = team.organisation.slug if user_id != 0: if UserService.is_user_an_admin(user_id): team_dto.is_general_admin = True if OrganisationService.is_user_an_org_manager( team.organisation.id, user_id): team_dto.is_org_admin = True else: team_dto.is_general_admin = False team_dto.is_org_admin = False if abbreviated: return team_dto team_dto.members = [ team.as_dto_team_member(member) for member in team.members ] team_projects = TeamService.get_projects_by_team_id(team.id) team_dto.team_projects = [ team.as_dto_team_project(project) for project in team_projects ] return team_dto
def get_organisation_dto(org, user_id: int, abbreviated: bool): if org is None: raise NotFound() organisation_dto = org.as_dto(abbreviated) if user_id != 0: organisation_dto.is_manager = ( OrganisationService.can_user_manage_organisation( org.id, user_id)) else: organisation_dto.is_manager = False if abbreviated: return organisation_dto organisation_dto.teams = [ team.as_dto_inside_org() for team in org.teams ] return organisation_dto
def create_from_dto(cls, new_organisation_dto: NewOrganisationDTO): """ Creates a new organisation from a DTO """ new_org = cls() new_org.name = new_organisation_dto.name new_org.slug = new_organisation_dto.slug or slugify(new_organisation_dto.name) new_org.logo = new_organisation_dto.logo new_org.description = new_organisation_dto.description new_org.url = new_organisation_dto.url new_org.type = OrganisationType[new_organisation_dto.type].value new_org.subscription_tier = new_organisation_dto.subscription_tier for manager in new_organisation_dto.managers: user = User.get_by_username(manager) if user is None: raise NotFound(f"User {manager} Not Found") new_org.managers.append(user) new_org.create() return new_org
def update(self, team_dto: TeamDTO): """ Updates Team from DTO """ if team_dto.organisation: self.organisation = Organisation().get_organisation_by_name( team_dto.organisation) for attr, value in team_dto.items(): if attr == "visibility" and value is not None: value = TeamVisibility[team_dto.visibility].value if attr in ("members", "organisation"): continue try: is_field_nullable = self.__table__.columns[attr].nullable if is_field_nullable and value is not None: setattr(self, attr, value) elif value is not None: setattr(self, attr, value) except KeyError: continue if team_dto.members != self._get_team_members() and team_dto.members: for member in self.members: db.session.delete(member) for member in team_dto.members: user = User.get_by_username(member["userName"]) if user is None: raise NotFound("User not found") new_team_member = TeamMembers() new_team_member.team = self new_team_member.member = user new_team_member.function = TeamMemberFunctions[ member["function"]].value db.session.commit()
def assert_validate_members(team_dto: TeamDTO): """ Validates that the users exist""" if len(team_dto.members) == 0: raise TeamServiceError("Must have at least one member") members = [] managers = 0 for member in team_dto.members: try: UserService.get_user_by_username(member["name"]) except NotFound: raise NotFound(f'User {member["name"]} does not exist') if member["function"] == TeamMemberFunctions.MANAGER.name: managers += 1 members.append(member) if managers == 0: raise TeamServiceError( "Must have at least one manager in team") team_dto.members = members
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 get_project_by_name(project_id: int) -> Project: project = Project.get(project_id) if project is None: raise NotFound() return project
def exists(project_id: int) -> bool: project = Project.exists(project_id) if project is None: raise NotFound() return True
def update_campaign(campaign_dto: CampaignDTO, campaign_id: int): campaign = Campaign.query.get(campaign_id) if not campaign: raise NotFound(f"Campaign id {campaign_id} not found") campaign.update(campaign_dto) return campaign
def clone(project_id: int, author_id: int): """ Clone project """ orig = Project.query.get(project_id) if orig is None: raise NotFound() # Transform into dictionary. orig_metadata = orig.__dict__.copy() # Remove unneeded data. items_to_remove = ["_sa_instance_state", "id", "allowed_users"] [orig_metadata.pop(i, None) for i in items_to_remove] # Remove clone from session so we can reinsert it as a new object orig_metadata.update({ "total_tasks": 0, "tasks_mapped": 0, "tasks_validated": 0, "tasks_bad_imagery": 0, "last_updated": timestamp(), "created": timestamp(), "author_id": author_id, "status": ProjectStatus.DRAFT.value, }) new_proj = Project(**orig_metadata) db.session.add(new_proj) proj_info = [] for info in orig.project_info.all(): info_data = info.__dict__.copy() info_data.pop("_sa_instance_state") info_data.update({ "project_id": new_proj.id, "project_id_str": str(new_proj.id) }) proj_info.append(ProjectInfo(**info_data)) new_proj.project_info = proj_info # Replace changeset comment. default_comment = current_app.config["DEFAULT_CHANGESET_COMMENT"] if default_comment is not None: orig_changeset = f"{default_comment}-{orig.id}" # Preserve space new_proj.changeset_comment = orig.changeset_comment.replace( orig_changeset, "") # Populate teams, interests and campaigns teams = [] for team in orig.teams: team_data = team.__dict__.copy() team_data.pop("_sa_instance_state") team_data.update({"project_id": new_proj.id}) teams.append(ProjectTeams(**team_data)) new_proj.teams = teams for field in ["interests", "campaign"]: value = getattr(orig, field) setattr(new_proj, field, value) new_proj.custom_editor = orig.custom_editor return new_proj
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 generate_gpx(project_id: int, task_ids_str: str, timestamp=None): """ Creates a GPX file for supplied tasks. Timestamp is for unit testing only. You can use the following URL to test locally: http://www.openstreetmap.org/edit?editor=id&#map=11/31.50362930069913/34.628906243797054&comment=CHANGSET_COMMENT&gpx=http://localhost:5000/api/v2/projects/{project_id}/tasks/queries/gpx%3Ftasks=2 """ if timestamp is None: timestamp = datetime.datetime.utcnow() root = ET.Element( "gpx", attrib=dict( xmlns="http://www.topografix.com/GPX/1/1", version="1.1", creator="HOT Tasking Manager", ), ) # Create GPX Metadata element metadata = ET.Element("metadata") link = ET.SubElement( metadata, "link", attrib=dict(href="https://github.com/hotosm/tasking-manager"), ) ET.SubElement(link, "text").text = "HOT Tasking Manager" ET.SubElement(metadata, "time").text = timestamp.isoformat() root.append(metadata) # Create trk element trk = ET.Element("trk") root.append(trk) ET.SubElement( trk, "name" ).text = f"Task for project {project_id}. Do not edit outside of this area!" # Construct trkseg elements if task_ids_str is not None: task_ids = map(int, task_ids_str.split(",")) tasks = Task.get_tasks(project_id, task_ids) if not tasks or len(tasks) == 0: raise NotFound() else: tasks = Task.get_all_tasks(project_id) if not tasks or len(tasks) == 0: raise NotFound() for task in tasks: task_geom = shape.to_shape(task.geometry) for poly in task_geom: trkseg = ET.SubElement(trk, "trkseg") for point in poly.exterior.coords: ET.SubElement( trkseg, "trkpt", attrib=dict(lon=str(point[0]), lat=str(point[1])), ) # Append wpt elements to end of doc wpt = ET.Element("wpt", attrib=dict(lon=str(point[0]), lat=str(point[1]))) root.append(wpt) xml_gpx = ET.tostring(root, encoding="utf8") return xml_gpx
def get_team_as_dto(team_id: int, user_id: int, abbreviated: bool) -> TeamDTO: team = TeamService.get_team_by_id(team_id) if team is None: raise NotFound() team_dto = TeamDetailsDTO() team_dto.team_id = team.id team_dto.name = team.name team_dto.invite_only = team.invite_only team_dto.visibility = TeamVisibility(team.visibility).name team_dto.description = team.description team_dto.logo = team.organisation.logo team_dto.organisation = team.organisation.name team_dto.organisation_id = team.organisation.id if user_id != 0: if UserService.is_user_an_admin(user_id): team_dto.is_general_admin = True if OrganisationService.is_user_an_org_manager( team.organisation.id, user_id): team_dto.is_org_admin = True else: team_dto.is_general_admin = False team_dto.is_org_admin = False if abbreviated: return team_dto team_members = TeamService._get_team_members(team_id) for member in team_members: user = UserService.get_user_by_id(member.user_id) member_dto = TeamMembersDTO() member_dto.username = user.username member_dto.pictureUrl = user.picture_url member_dto.function = TeamMemberFunctions(member.function).name member_dto.picture_url = user.picture_url member_dto.active = member.active team_dto.members.append(member_dto) team_projects = TeamService.get_projects_by_team_id(team.id) for team_project in team_projects: project_team_dto = TeamProjectDTO() project_team_dto.project_name = team_project.name project_team_dto.project_id = team_project.project_id project_team_dto.role = TeamRoles(team_project.role).name team_dto.team_projects.append(project_team_dto) org_projects = OrganisationService.get_projects_by_organisation_id( team.organisation.id) for org_project in org_projects: org_project_dto = OrganisationProjectsDTO() org_project_dto.project_id = org_project.id org_project_dto.project_name = org_project.name team_dto.organisation_projects.append(org_project_dto) return team_dto
def update(self, project_dto: ProjectDTO): """ Updates project from DTO """ self.status = ProjectStatus[project_dto.project_status].value self.priority = ProjectPriority[project_dto.project_priority].value self.default_locale = project_dto.default_locale self.enforce_random_task_selection = project_dto.enforce_random_task_selection self.private = project_dto.private self.mapper_level = MappingLevel[ project_dto.mapper_level.upper()].value self.changeset_comment = project_dto.changeset_comment self.due_date = project_dto.due_date self.imagery = project_dto.imagery self.josm_preset = project_dto.josm_preset self.id_presets = project_dto.id_presets self.last_updated = timestamp() self.license_id = project_dto.license_id if project_dto.osmcha_filter_id: # Support simple extraction of OSMCha filter id from OSMCha URL match = re.search(r"aoi=([\w-]+)", project_dto.osmcha_filter_id) self.osmcha_filter_id = (match.group(1) if match else project_dto.osmcha_filter_id) else: self.osmcha_filter_id = None if project_dto.organisation: org = Organisation.get(project_dto.organisation) if org is None: raise NotFound("Organisation does not exist") self.organisation = org # Cast MappingType strings to int array type_array = [] for mapping_type in project_dto.mapping_types: type_array.append(MappingTypes[mapping_type].value) self.mapping_types = type_array # Cast Editor strings to int array mapping_editors_array = [] for mapping_editor in project_dto.mapping_editors: mapping_editors_array.append(Editors[mapping_editor].value) self.mapping_editors = mapping_editors_array validation_editors_array = [] for validation_editor in project_dto.validation_editors: validation_editors_array.append(Editors[validation_editor].value) self.validation_editors = validation_editors_array self.country = project_dto.country_tag # Add list of allowed users, meaning the project can only be mapped by users in this list if hasattr(project_dto, "allowed_users"): self.allowed_users = [ ] # Clear existing relationships then re-insert for user in project_dto.allowed_users: self.allowed_users.append(user) # Update teams and projects relationship. self.teams = [] if hasattr(project_dto, "project_teams") and project_dto.project_teams: for team_dto in project_dto.project_teams: team = Team.get(team_dto.team_id) if team is None: raise NotFound("Team not found") role = TeamRoles[team_dto.role].value ProjectTeams(project=self, team=team, role=role) # Set Project Info for all returned locales for dto in project_dto.project_info_locales: project_info = self.project_info.filter_by( locale=dto.locale).one_or_none() if project_info is None: new_info = ProjectInfo.create_from_dto( dto) # Can't find info so must be new locale self.project_info.append(new_info) else: project_info.update_from_dto(dto) self.priority_areas = [ ] # Always clear Priority Area prior to updating if project_dto.priority_areas: for priority_area in project_dto.priority_areas: pa = PriorityArea.from_dict(priority_area) self.priority_areas.append(pa) if project_dto.custom_editor: if not self.custom_editor: new_editor = CustomEditor.create_from_dto( self.id, project_dto.custom_editor) self.custom_editor = new_editor else: self.custom_editor.update_editor(project_dto.custom_editor) else: if self.custom_editor: self.custom_editor.delete() # handle campaign update try: new_ids = [c.id for c in project_dto.campaigns] new_ids.sort() except TypeError: new_ids = [] current_ids = [c.id for c in self.campaign] current_ids.sort() if new_ids != current_ids: self.campaign = Campaign.query.filter( Campaign.id.in_(new_ids)).all() if project_dto.mapping_permission: self.mapping_permission = MappingPermission[ project_dto.mapping_permission.upper()].value if project_dto.validation_permission: self.validation_permission = ValidationPermission[ project_dto.validation_permission.upper()].value # handle interests update try: new_ids = [c.id for c in project_dto.interests] new_ids.sort() except TypeError: new_ids = [] current_ids = [c.id for c in self.interests] current_ids.sort() if new_ids != current_ids: self.interests = Interest.query.filter( Interest.id.in_(new_ids)).all() # try to update country info if that information is not present if not self.country: self.set_country_info() db.session.commit()
def get_tasks_as_geojson_feature_collection( project_id, task_ids_str: str = None, order_by: str = None, order_by_type: str = "ASC", status: int = None, ): """ Creates a geoJson.FeatureCollection object for tasks related to the supplied project ID :param project_id: Owning project ID :order_by: sorting option: available values update_date and building_area_diff :status: task status id to filter by :return: geojson.FeatureCollection """ # subquery = ( # db.session.query(func.max(TaskHistory.action_date)) # .filter( # Task.id == TaskHistory.task_id, # Task.project_id == TaskHistory.project_id, # ) # .correlate(Task) # .group_by(Task.id) # .label("update_date") # ) query = db.session.query( Task.id, Task.x, Task.y, Task.zoom, Task.is_square, Task.task_status, Task.geometry.ST_AsGeoJSON().label("geojson"), Task.locked_by, # subquery, ) filters = [Task.project_id == project_id] if task_ids_str: task_ids = map(int, task_ids_str.split(",")) tasks = Task.get_tasks(project_id, task_ids) if not tasks or len(tasks) == 0: raise NotFound() else: tasks_filters = [task.id for task in tasks] filters = [ Task.project_id == project_id, Task.id.in_(tasks_filters) ] else: tasks = Task.get_all_tasks(project_id) if not tasks or len(tasks) == 0: raise NotFound() if status: filters.append(Task.task_status == status) if order_by == "effort_prediction": query = query.outerjoin(TaskAnnotation).filter(*filters) if order_by_type == "DESC": query = query.order_by( desc( cast( cast( TaskAnnotation. properties["building_area_diff"], Text), Float, ))) else: query = query.order_by( cast( cast(TaskAnnotation.properties["building_area_diff"], Text), Float, )) # elif order_by == "last_updated": # if order_by_type == "DESC": # query = query.filter(*filters).order_by(desc("update_date")) # else: # query = query.filter(*filters).order_by("update_date") else: query = query.filter(*filters) project_tasks = query.all() tasks_features = [] for task in project_tasks: task_geometry = geojson.loads(task.geojson) task_properties = dict( taskId=task.id, taskX=task.x, taskY=task.y, taskZoom=task.zoom, taskIsSquare=task.is_square, taskStatus=TaskStatus(task.task_status).name, lockedBy=task.locked_by, ) feature = geojson.Feature(geometry=task_geometry, properties=task_properties) tasks_features.append(feature) return geojson.FeatureCollection(tasks_features)