def unlock_task_after_mapping(mapped_task: MappedTaskDTO) -> TaskDTO: """ Unlocks the task and sets the task history appropriately """ task = MappingService.get_task_locked_by_user(mapped_task.project_id, mapped_task.task_id, mapped_task.user_id) new_state = TaskStatus[mapped_task.status.upper()] if new_state not in [ TaskStatus.MAPPED, TaskStatus.BADIMAGERY, TaskStatus.READY, ]: raise MappingServiceError( "Can only set status to MAPPED, BADIMAGERY, READY after mapping" ) # Update stats around the change of state last_state = TaskHistory.get_last_status(mapped_task.project_id, mapped_task.task_id, True) StatsService.update_stats_after_task_state_change( mapped_task.project_id, mapped_task.user_id, last_state, new_state) if mapped_task.comment: # Parses comment to see if any users have been @'d MessageService.send_message_after_comment( mapped_task.user_id, mapped_task.comment, task.id, mapped_task.project_id, ) task.unlock_task(mapped_task.user_id, new_state, mapped_task.comment) return task.as_dto_with_instructions(mapped_task.preferred_locale)
def undo_mapping(project_id: int, task_id: int, user_id: int, preferred_locale: str = "en") -> TaskDTO: """ Allows a user to Undo the task state they updated """ task = MappingService.get_task(task_id, project_id) if not MappingService._is_task_undoable(user_id, task): raise MappingServiceError("Undo not allowed for this user") current_state = TaskStatus(task.task_status) undo_state = TaskHistory.get_last_status(project_id, task_id, True) # Refer to last action for user of it. last_action = TaskHistory.get_last_action(project_id, task_id) StatsService.update_stats_after_task_state_change( project_id, last_action.user_id, current_state, undo_state, "undo") task.unlock_task( user_id, undo_state, f"Undo state from {current_state.name} to {undo_state.name}", True, ) return task.as_dto_with_instructions(preferred_locale)
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 get(self): """ Get HomePage Stats --- tags: - system produces: - application/json parameters: - in: query name: abbreviated type: boolean description: Set to false if complete details on projects including total area, campaigns, orgs are required default: True responses: 200: description: Project stats 500: description: Internal Server Error """ try: abbreviated = (strtobool(request.args.get("abbreviated")) if request.args.get("abbreviated") else True) stats = StatsService.get_homepage_stats(abbreviated) return stats.to_primitive(), 200 except Exception as e: error_msg = f"Unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to fetch summary statistics"}, 500
def test_homepage_stats_returns_results(self): # Act stats = StatsService.get_homepage_stats() # Assert self.assertGreaterEqual(stats.mappers_online, 0) self.assertGreater(stats.tasks_mapped, 0) self.assertGreater(stats.total_mappers, 0)
def get(self): """ Get stats about users registered within a period of time --- tags: - users produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token type: string required: true default: Token sessionTokenHere== - in: query name: startDate description: Initial date required: true type: string - in: query name: endDate description: Final date. type: string responses: 200: description: User statistics 400: description: Bad Request 401: description: Request is not authenticated 500: description: Internal Server Error """ try: start_date = validate_date_input(request.args.get("startDate")) end_date = validate_date_input( request.args.get("endDate", date.today())) if not (start_date): raise KeyError("Missing start date parameter") if end_date < start_date: raise ValueError("Start date must be earlier than end date") if (end_date - start_date) > timedelta(days=366 * 3): raise ValueError("Date range can not be bigger than 3 years") stats = StatsService.get_all_users_statistics(start_date, end_date) return stats.to_primitive(), 200 except (KeyError, ValueError) as e: error_msg = f"User Statistics GET - {str(e)}" current_app.logger.critical(error_msg) return {"Error": error_msg}, 400 except Exception as e: error_msg = f"User Statistics GET - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to fetch user stats"}, 500
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_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_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_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 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(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 get(self): """ Get popular projects --- tags: - projects produces: - application/json responses: 200: description: Popular Projects stats 500: description: Internal Server Error """ try: stats = StatsService.get_popular_projects() return stats.to_primitive(), 200 except Exception as e: error_msg = f"Unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": error_msg}, 500
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 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 initialise_counters(app): """ Initialise homepage counters so that users don't see 0 users on first load of application""" from backend.services.stats_service import StatsService with app.app_context(): StatsService.get_homepage_stats()
def get(self): """ Get Task Stats --- tags: - tasks produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token type: string required: true default: Token sessionTokenHere== - in: query name: startDate description: Date to filter as minimum required: true type: string - in: query name: endDate description: Date to filter as maximum. Default value is the current date. required: false type: string - in: query name: organisationName description: Organisation name to filter by required: false - in: query name: organisationId description: Organisation ID to filter by required: false - in: query name: campaign description: Campaign name to filter by required: false - in: query name: projectId description: Project IDs to filter by required: false - in: query name: country description: Country name to filter by required: false responses: 200: description: Task statistics 400: description: Bad Request 401: description: Request is not authenticated 500: description: Internal Server Error """ try: start_date = validate_date_input(request.args.get("startDate")) end_date = validate_date_input(request.args.get("endDate", date.today())) if not (start_date): raise KeyError("Missing start date parameter") if end_date < start_date: raise ValueError("Start date must be earlier than end date") if (end_date - start_date) > timedelta(days=366): raise ValueError("Date range can not be bigger than 1 year") organisation_id = request.args.get("organisationId", None, int) organisation_name = request.args.get("organisationName", None, str) campaign = request.args.get("campaign", None, str) project_id = request.args.get("projectId") if project_id: project_id = map(str, project_id.split(",")) country = request.args.get("country", None, str) task_stats = StatsService.get_task_stats( start_date, end_date, organisation_id, organisation_name, campaign, project_id, country, ) return task_stats.to_primitive(), 200 except (KeyError, ValueError) as e: error_msg = f"Task Statistics GET - {str(e)}" return {"Error": error_msg}, 400 except Exception as e: error_msg = f"Task Statistics GET - unhandled error: {str(e)}" current_app.logger.critical(error_msg) return {"Error": "Unable to fetch task statistics"}, 500
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)
def refresh_project_stats(): print("Started updating project stats...") StatsService.update_all_project_stats() print("Project stats updated")