def clone(project_id: int, author_id: int): """ Clone project """ orig = Project.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, "geometry": None, "centroid": None, }) 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, "") # Copy array relationships. for field in ["interests", "campaign", "teams"]: value = getattr(orig, field) setattr(new_proj, field, value) new_proj.custom_editor = orig.custom_editor db.session.commit() return new_proj
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 add_or_update_annotation(annotation, project_id, annotation_type): """ Takes a json of tasks and create annotations in the db """ task_id = annotation["taskId"] source = annotation.get("annotationSource", None) markdown = annotation.get("annotationMarkdown", None) task_annotation = TaskAnnotation( task_id, project_id, annotation_type, annotation["properties"], source, markdown, ) # check if the task has this annotation_type existing_annotation = TaskAnnotation.get_task_annotation( task_id, project_id, annotation_type) if existing_annotation: # update this annotation existing_annotation.properties = task_annotation.properties existing_annotation.updated_timestamp = timestamp() existing_annotation.update() else: # add this annotation task_annotation.create()
def create_draft_project(self, draft_project_dto: DraftProjectDTO): """ Creates a draft project :param draft_project_dto: DTO containing draft project details :param aoi: Area of Interest for the project (eg boundary of project) """ self.project_info.append( ProjectInfo.create_from_name(draft_project_dto.project_name)) self.status = ProjectStatus.DRAFT.value self.author_id = draft_project_dto.user_id self.last_updated = timestamp()
def record_validation(project_id, task_id, validator_id, history): entry = TaskInvalidationHistory.get_open_for_task(project_id, task_id) # If no open invalidation to update, then nothing to do if entry is None: return last_mapped = TaskHistory.get_last_mapped_action(project_id, task_id) entry.mapper_id = last_mapped.user_id entry.mapped_date = last_mapped.action_date entry.validator_id = validator_id entry.validated_date = history.action_date entry.is_closed = True entry.updated_date = timestamp()
def record_invalidation(project_id, task_id, invalidator_id, history): # Invalidation always kicks off a new entry for a task, so close any existing ones. TaskInvalidationHistory.close_all_for_task(project_id, task_id) last_mapped = TaskHistory.get_last_mapped_action(project_id, task_id) if last_mapped is None: return entry = TaskInvalidationHistory(project_id, task_id) entry.invalidation_history_id = history.id entry.mapper_id = last_mapped.user_id entry.mapped_date = last_mapped.action_date entry.invalidator_id = invalidator_id entry.invalidated_date = history.action_date entry.updated_date = timestamp() db.session.add(entry)
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 unlock_tasks_after_validation( validated_dto: UnlockAfterValidationDTO, ) -> TaskDTOs: """ Unlocks supplied tasks after validation :raises ValidatorServiceError """ validated_tasks = validated_dto.validated_tasks project_id = validated_dto.project_id user_id = validated_dto.user_id tasks_to_unlock = ValidatorService.get_tasks_locked_by_user( project_id, validated_tasks, user_id) # Unlock all tasks dtos = [] message_sent_to = [] for task_to_unlock in tasks_to_unlock: task = task_to_unlock["task"] if task_to_unlock["comment"]: # Parses comment to see if any users have been @'d MessageService.send_message_after_comment( validated_dto.user_id, task_to_unlock["comment"], task.id, validated_dto.project_id, ) if (task_to_unlock["new_state"] == TaskStatus.VALIDATED or task_to_unlock["new_state"] == TaskStatus.INVALIDATED): # All mappers get a notification if their task has been validated or invalidated. # Only once if multiple tasks mapped if task.mapped_by not in message_sent_to: MessageService.send_message_after_validation( task_to_unlock["new_state"], validated_dto.user_id, task.mapped_by, task.id, validated_dto.project_id, ) message_sent_to.append(task.mapped_by) if task_to_unlock["new_state"] == TaskStatus.VALIDATED: # Set last_validation_date for the mapper to current date task.mapper.last_validation_date = timestamp() # Update stats if user setting task to a different state from previous state prev_status = TaskHistory.get_last_status(project_id, task.id) if prev_status != task_to_unlock["new_state"]: StatsService.update_stats_after_task_state_change( validated_dto.project_id, validated_dto.user_id, prev_status, task_to_unlock["new_state"], ) task_mapping_issues = ValidatorService.get_task_mapping_issues( task_to_unlock) task.unlock_task( validated_dto.user_id, task_to_unlock["new_state"], task_to_unlock["comment"], issues=task_mapping_issues, ) dtos.append( task.as_dto_with_instructions(validated_dto.preferred_locale)) task_dtos = TaskDTOs() task_dtos.tasks = dtos return task_dtos
def update(self): self.date = timestamp() db.session.commit()
def clone(project_id: int, author_id: int): """ Clone project """ cloned_project = Project.get(project_id) # Remove clone from session so we can reinsert it as a new object db.session.expunge(cloned_project) make_transient(cloned_project) # Re-initialise counters and meta-data cloned_project.total_tasks = 0 cloned_project.tasks_mapped = 0 cloned_project.tasks_validated = 0 cloned_project.tasks_bad_imagery = 0 cloned_project.last_updated = timestamp() cloned_project.created = timestamp() cloned_project.author_id = author_id cloned_project.status = ProjectStatus.DRAFT.value cloned_project.id = None # Reset ID so we get a new ID when inserted cloned_project.geometry = None cloned_project.centroid = None db.session.add(cloned_project) db.session.commit() # Now add the project info, we have to do it in a two stage commit because we need to know the new project id original_project = Project.get(project_id) for info in original_project.project_info: db.session.expunge(info) make_transient( info ) # Must remove the object from the session or it will be updated rather than inserted info.id = None info.project_id_str = str(cloned_project.id) cloned_project.project_info.append(info) # Now add allowed users now we know new project id, if there are any for user in original_project.allowed_users: cloned_project.allowed_users.append(user) # Add other project metadata cloned_project.priority = original_project.priority cloned_project.default_locale = original_project.default_locale cloned_project.mapper_level = original_project.mapper_level cloned_project.mapping_permission = original_project.mapping_permission cloned_project.validation_permission = original_project.validation_permission cloned_project.enforce_random_task_selection = ( original_project.enforce_random_task_selection) cloned_project.private = original_project.private cloned_project.entities_to_map = original_project.entities_to_map cloned_project.due_date = original_project.due_date cloned_project.imagery = original_project.imagery cloned_project.josm_preset = original_project.josm_preset cloned_project.license_id = original_project.license_id cloned_project.mapping_types = original_project.mapping_types # We try to remove the changeset comment referencing the old project. This # assumes the default changeset comment has not changed between the old # project and the cloned. This is a best effort basis. default_comment = current_app.config["DEFAULT_CHANGESET_COMMENT"] changeset_comments = [] if original_project.changeset_comment is not None: changeset_comments = original_project.changeset_comment.split(" ") if f"{default_comment}-{original_project.id}" in changeset_comments: changeset_comments.remove( f"{default_comment}-{original_project.id}") cloned_project.changeset_comment = " ".join(changeset_comments) db.session.add(cloned_project) db.session.commit() return cloned_project