def is_user_permitted_to_map(project_id: int, user_id: int): """ Check if the user is allowed to map the on the project in scope """ if UserService.is_user_blocked(user_id): return False, MappingNotAllowed.USER_NOT_ON_ALLOWED_LIST project = ProjectService.get_project_by_id(project_id) if ProjectStatus(project.status) != ProjectStatus.PUBLISHED and not UserService.is_user_a_project_manager(user_id): return False, MappingNotAllowed.PROJECT_NOT_PUBLISHED tasks = project.get_locked_tasks_for_user(user_id) if len(tasks) > 0: return False, MappingNotAllowed.USER_ALREADY_HAS_TASK_LOCKED if project.enforce_mapper_level: if not ProjectService._is_user_mapping_level_at_or_above_level_requests(MappingLevel(project.mapper_level), user_id): return False, MappingNotAllowed.USER_NOT_CORRECT_MAPPING_LEVEL if project.license_id: if not UserService.has_user_accepted_license(user_id, project.license_id): return False, MappingNotAllowed.USER_NOT_ACCEPTED_LICENSE if project.private: # Check user is in allowed users try: next(user for user in project.allowed_users if user.id == user_id) except StopIteration: return False, MappingNotAllowed.USER_NOT_ON_ALLOWED_LIST return True, 'User allowed to map'
def update_stats_after_task_state_change(project_id: int, user_id: int, new_state: TaskStatus, task_id: int): """ 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) if new_state == TaskStatus.MAPPED: StatsService._set_counters_after_mapping(project, user) elif new_state == TaskStatus.INVALIDATED: StatsService._set_counters_after_invalidated( task_id, project, user) elif new_state == TaskStatus.VALIDATED: StatsService._set_counters_after_validated(project, user) elif new_state == TaskStatus.BADIMAGERY: StatsService._set_counters_after_bad_imagery(project) UserService.upsert_mapped_projects(user_id, project_id) project.last_updated = timestamp() # Transaction will be saved when task is saved return project, user
def is_user_permitted_to_validate(project_id, user_id): """ Check if the user is allowed to validate on the project in scope """ if UserService.is_user_blocked(user_id): return False, ValidatingNotAllowed.USER_NOT_ON_ALLOWED_LIST project = ProjectService.get_project_by_id(project_id) if ProjectStatus( project.status ) != ProjectStatus.PUBLISHED and not UserService.is_user_a_project_manager( user_id): return False, ValidatingNotAllowed.PROJECT_NOT_PUBLISHED if project.enforce_validator_role and not UserService.is_user_validator( user_id): return False, ValidatingNotAllowed.USER_NOT_VALIDATOR if project.license_id: if not UserService.has_user_accepted_license( user_id, project.license_id): return False, ValidatingNotAllowed.USER_NOT_ACCEPTED_LICENSE if project.private: # Check user is in allowed users try: next(user for user in project.allowed_users if user.id == user_id) except StopIteration: return False, ValidatingNotAllowed.USER_NOT_ON_ALLOWED_LIST return True, 'User allowed to validate'
def put(self, username): """ Creates user --- tags: - admin - users produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string - in: body name: body required: true description: JSON object for creating a new user schema: properties: password: type: string default: password role: type: string default: user - in: path name: username description: the unique user required: true type: string default: dmisuser responses: 201: description: User Created 400: description: Invalid request 401: description: Unauthorized, credentials are invalid 403: description: Forbidden, username already exists 500: description: Internal Server Error """ try: user_dto = UserDTO(request.get_json()) user_dto.username = username user_dto.validate() except DataError as e: current_app.logger.error(f'error validating request: {str(e)}') return str(e), 400 try: UserService.create_user(user_dto) except UserExistsError as e: return {"Error": str(e)}, 403 except Exception as e: error_msg = f'User Create - Unhandled error: {str(e)}' current_app.logger.critical(error_msg) return {"Error": error_msg}, 500
def login_user(osm_user_details, redirect_to, user_element='user') -> str: """ Generates authentication details for user, creating in DB if user is unknown to us :param osm_user_details: XML response from OSM :param redirect_to: Route to redirect user to, from callback url :param user_element: Exists for unit testing :raises AuthServiceError :returns Authorized URL with authentication details in query string """ osm_user = osm_user_details.find(user_element) if osm_user is None: raise AuthServiceError('User element not found in OSM response') osm_id = int(osm_user.attrib['id']) username = osm_user.attrib['display_name'] try: UserService.get_user_by_id(osm_id) except NotFound: # User not found, so must be new user changesets = osm_user.find('changesets') changeset_count = int(changesets.attrib['count']) new_user = UserService.register_user(osm_id, username, changeset_count) MessageService.send_welcome_message(new_user) session_token = AuthenticationService.generate_session_token_for_user( osm_id) authorized_url = AuthenticationService.generate_authorized_url( username, session_token, redirect_to) return authorized_url
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 test_user_service_raise_error_if_user_element_not_found(self): # Arrange osm_response = get_canned_simplified_osm_user_details() # Act / Assert with self.assertRaises(UserServiceError): UserService._parse_osm_user_details_response( osm_response, 'wont-find')
def test_pm_not_allowed_to_add_admin_role_when_setting_role( self, mock_admin): # Arrange admin = User() admin.role = UserRole.PROJECT_MANAGER.value mock_admin.return_value = admin # Act with self.assertRaises(UserServiceError): UserService.add_role_to_user(1, 'test', 'ADMIN')
def test_get_user_by_id_returns_not_found_exception(self): """ Get a canned user from the DB """ if self.skip_tests: return # Arrange test_user_id = 9999999999999999999 # Act with self.assertRaises(NotFound): UserService.get_user_by_id(test_user_id)
def test_get_user_by_username_returns_not_found_exception(self): """ Check that NotFound exception returned if username not found """ if self.skip_tests: return # Arrange test_username = '******' # Act with self.assertRaises(NotFound): UserService.get_user_by_username(test_username)
def test_upsert_inserts_project_if_not_exists(self): if self.skip_tests: return # Arrange UserService.upsert_mapped_projects(self.test_user.id, self.test_project.id) # Act projects = UserService.get_mapped_projects(self.test_user.username, 'en') # Assert mapped_project = projects.mapped_projects[0] self.assertEqual(mapped_project.project_id, self.test_project.id) # We should find we've mapped the test project
def post(self, username, role): """ Allows PMs to set the users role --- tags: - user produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - name: username in: path description: The users username required: true type: string default: Thinkwhere - name: role in: path description: The role to add required: true type: string default: ADMIN responses: 200: description: Role set 401: description: Unauthorized - Invalid credentials 403: description: Forbidden 404: description: User not found 500: description: Internal Server Error """ try: UserService.add_role_to_user(tm.authenticated_user_id, username, role) return {"Success": "Role Added"}, 200 except UserServiceError: return {"Error": "Not allowed"}, 403 except NotFound: return {"Error": "User or mapping not found"}, 404 except Exception as e: error_msg = f'User GET - unhandled error: {str(e)}' current_app.logger.critical(error_msg) return {"error": error_msg}, 500
def post(self, username, level): """ Allows PMs to set a users mapping level --- tags: - user produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - name: username in: path description: The users username required: true type: string default: Thinkwhere - name: level in: path description: The mapping level that should be set required: true type: string default: ADVANCED responses: 200: description: Level set 400: description: Bad Request - Client Error 401: description: Unauthorized - Invalid credentials 404: description: User not found 500: description: Internal Server Error """ try: UserService.set_user_mapping_level(username, level) return {"Success": "Level set"}, 200 except UserServiceError: return {"Error": "Not allowed"}, 400 except NotFound: return {"Error": "User or mapping not found"}, 404 except Exception as e: error_msg = f'User GET - unhandled error: {str(e)}' current_app.logger.critical(error_msg) return {"error": error_msg}, 500
def test_mapper_level_updates_correctly(self, mock_user, mock_osm, mock_save): # Arrange test_user = User() test_user.mapping_level = MappingLevel.BEGINNER.value mock_user.return_value = test_user test_osm = UserOSMDTO() test_osm.changeset_count = 350 mock_osm.return_value = test_osm # Act UserService.check_and_update_mapper_level(123) #Assert self.assertTrue(test_user.mapping_level, MappingLevel.INTERMEDIATE.value)
def send_message_to_all_contributors(project_id: int, message_dto: MessageDTO): """ Sends supplied message to all contributors on specified project. Message all contributors can take over a minute to run, so this method is expected to be called on its own thread """ app = create_app( ) # Because message-all run on background thread it needs it's own app context with app.app_context(): contributors = Message.get_all_contributors(project_id) project_link = MessageService.get_project_link(project_id) message_dto.message = f'{project_link}<br/><br/>' + message_dto.message # Append project link to end of message msg_count = 0 for contributor in contributors: message = Message.from_dto(contributor[0], message_dto) message.save() user = UserService.get_user_by_id(contributor[0]) SMTPService.send_email_alert(user.email_address, user.username) msg_count += 1 if msg_count == 10: time.sleep( 0.5 ) # Sleep for 0.5 seconds to avoid hitting AWS rate limits every 10 messages msg_count = 0
def get_project_user_stats(project_id: int, username: str) -> ProjectUserStatsDTO: """ Gets the user stats for a specific project """ print(project_id) project = ProjectService.get_project_by_id(project_id) user = UserService.get_user_by_username(username) return project.get_project_user_stats(user.id)
def post(self): """ Updates user info --- tags: - user produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string default: Token sessionTokenHere== - in: body name: body required: true description: JSON object for creating draft project schema: properties: emailAddress: type: string default: [email protected] twitterId: type: string default: tweeter facebookId: type: string default: fbme linkedinId: type: string default: linkme responses: 200: description: Details saved 400: description: Client Error - Invalid Request 401: description: Unauthorized - Invalid credentials 500: description: Internal Server Error """ try: user_dto = UserDTO(request.get_json()) user_dto.validate() except DataError as e: current_app.logger.error(f'error validating request: {str(e)}') return str(e), 400 try: verification_sent = UserService.update_user_details( tm.authenticated_user_id, user_dto) return verification_sent, 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": error_msg}, 500
def is_valid_token(token, token_expiry): """ Validates if the supplied token is valid, and hasn't expired. :param token: Token to check :param token_expiry: When the token expires :return: True if token is valid """ serializer = URLSafeTimedSerializer(current_app.secret_key) try: tokenised_user_id = serializer.loads(token, max_age=token_expiry) dmis.authenticated_user_id = tokenised_user_id except SignatureExpired: current_app.logger.debug('Token has expired') return False except BadSignature: current_app.logger.debug('Bad Token Signature') return False # Ensure user is Admin priv to access admin only endpoints if dmis.is_admin_only_resource: user = UserService.get_user_by_id(tokenised_user_id) if UserRole(user.role) != UserRole.ADMIN: current_app.logger.debug(f'User {tokenised_user_id} is not an Admin {request.base_url}') return False return True
def get(self): """ Gets a list of all users --- tags: - admin - users produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string responses: 200: description: Users found 401: description: Unauthorized, credentials are invalid 500: description: Internal Server Error """ try: user_dto = UserService.get_all_users() return user_dto.to_primitive(), 200 except Exception as e: error_msg = f'User list GET - unhandled error: {str(e)}' current_app.logger.critical(error_msg) return {"error": error_msg}, 500
def send_message_after_comment(comment_from: int, comment: str, task_id: int, project_id: int): """ Will send a canned message to anyone @'d in a comment """ usernames = MessageService._parse_message_for_username(comment) if len(usernames) == 0: return # Nobody @'d so return task_link = MessageService.get_task_link(project_id, task_id) project_title = ProjectService.get_project_title(project_id) for username in usernames: try: user = UserService.get_user_by_username(username) except NotFound: current_app.logger.error(f'Username {username} not found') continue # If we can't find the user, keep going no need to fail message = Message() message.from_user_id = comment_from message.to_user_id = user.id message.subject = f'You were mentioned in a comment in Project {project_id} on {task_link}' message.message = comment message.add_message() SMTPService.send_email_alert(user.email_address, user.username)
def send_message_after_chat(chat_from: int, chat: str, project_id: int): """ Send alert to user if they were @'d in a chat message """ current_app.logger.debug('Sending Message After Chat') usernames = MessageService._parse_message_for_username(chat) if len(usernames) == 0: return # Nobody @'d so return link = MessageService.get_project_link(project_id) for username in usernames: current_app.logger.debug(f'Searching for {username}') try: user = UserService.get_user_by_username(username) except NotFound: current_app.logger.error(f'Username {username} not found') continue # If we can't find the user, keep going no need to fail message = Message() message.from_user_id = chat_from message.to_user_id = user.id message.subject = f'You were mentioned in Project Chat on {link}' message.message = chat message.add_message() SMTPService.send_email_alert(user.email_address, user.username)
def send_message_after_validation(status: int, validated_by: int, mapped_by: int, task_id: int, project_id: int): """ Sends mapper a notification after their task has been marked valid or invalid """ if validated_by == mapped_by: return # No need to send a message to yourself user = UserService.get_user_by_id(mapped_by) if user.validation_message == False: return # No need to send validation message text_template = get_template('invalidation_message_en.txt' if status == TaskStatus.INVALIDATED \ else 'validation_message_en.txt') status_text = 'marked invalid' if status == TaskStatus.INVALIDATED else 'validated' task_link = MessageService.get_task_link(project_id, task_id) text_template = text_template.replace('[USERNAME]', user.username) text_template = text_template.replace('[TASK_LINK]', task_link) validation_message = Message() validation_message.from_user_id = validated_by validation_message.to_user_id = mapped_by validation_message.subject = f'Your mapping in Project {project_id} on {task_link} has just been {status_text}' validation_message.message = text_template validation_message.add_message() SMTPService.send_email_alert(user.email_address, user.username)
def get(self, username): """ Get detailed stats about user --- tags: - user produces: - application/json parameters: - name: username in: path description: The users 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 = UserService.get_detailed_stats(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": error_msg}, 500
def verify_token(token): """ Verify the supplied token and check user role is correct for the requested resource""" if not token: current_app.logger.debug(f'Token not supplied {request.base_url}') return False try: decoded_token = base64.b64decode(token).decode('utf-8') except UnicodeDecodeError: current_app.logger.debug(f'Unable to decode token {request.base_url}') return False # Can't decode token, so fail login valid_token, user_id = AuthenticationService.is_valid_token( decoded_token, 604800) if not valid_token: current_app.logger.debug(f'Token not valid {request.base_url}') return False if tm.is_pm_only_resource: if not UserService.is_user_a_project_manager(user_id): current_app.logger.debug( f'User {user_id} is not a PM {request.base_url}') return False current_app.logger.debug( f'Validated user {user_id} for {request.base_url}') tm.authenticated_user_id = user_id # Set the user ID on the decorator as a convenience return True # All tests passed token is good for the requested resource
def set_counters_after_undo(project_id: int, user_id: int, current_state: TaskStatus, undo_state: TaskStatus): """ Resets counters after a user undoes their task""" project = ProjectService.get_project_by_id(project_id) user = UserService.get_user_by_id(user_id) # This is best endeavours to reset the stats and may have missed some edge cases, hopefully majority of # cases will be Mapped to Ready if current_state == TaskStatus.MAPPED and undo_state == TaskStatus.READY: project.tasks_mapped -= 1 user.tasks_mapped -= 1 if current_state == TaskStatus.MAPPED and undo_state == TaskStatus.INVALIDATED: user.tasks_mapped -= 1 project.tasks_mapped -= 1 elif current_state == TaskStatus.BADIMAGERY and undo_state == TaskStatus.READY: project.tasks_bad_imagery -= 1 elif current_state == TaskStatus.BADIMAGERY and undo_state == TaskStatus.MAPPED: project.tasks_mapped += 1 project.tasks_bad_imagery -= 1 elif current_state == TaskStatus.BADIMAGERY and undo_state == TaskStatus.INVALIDATED: project.tasks_bad_imagery -= 1 elif current_state == TaskStatus.INVALIDATED and undo_state == TaskStatus.MAPPED: user.tasks_invalidated -= 1 project.tasks_mapped += 1 elif current_state == TaskStatus.INVALIDATED and undo_state == TaskStatus.VALIDATED: user.tasks_invalidated -= 1 project.tasks_validated += 1 elif current_state == TaskStatus.VALIDATED and undo_state == TaskStatus.MAPPED: user.tasks_validated -= 1 project.tasks_validated -= 1 elif current_state == TaskStatus.VALIDATED and undo_state == TaskStatus.BADIMAGERY: user.tasks_validated -= 1 project.tasks_validated -= 1
def test_user_can_register_with_correct_mapping_level(self, mock_user): # Act test_user = UserService().register_user(12, 'Thinkwhere', 300) # Assert self.assertEqual(test_user.mapping_level, MappingLevel.INTERMEDIATE.value)
def test_admin_role_is_recognized_as_a_validator(self, mock_user): # Arrange stub_user = User() stub_user.role = UserRole.ADMIN.value mock_user.return_value = stub_user # Act / Assert self.assertTrue(UserService.is_user_validator(123))
def test_mapper_role_is_not_recognized_as_a_validator(self, mock_user): # Arrange stub_user = User() stub_user.role = UserRole.MAPPER.value mock_user.return_value = stub_user # Act / Assert self.assertFalse(UserService.is_user_validator(123))
def post(self, username): """ Updates a user --- tags: - admin - users produces: - application/json parameters: - in: header name: Authorization description: Base64 encoded session token required: true type: string - in: body name: body required: true description: JSON object for creating a new user schema: properties: role: type: string default: user - in: path name: username description: the unique user required: true type: string default: dmisuser responses: 200: description: User details updated 400: description: Invalid request 401: description: Unauthorized, credentials are invalid 404: description: Not found 500: description: Internal Server Error """ try: user_update_dto = UserUpdateDTO(request.get_json()) user_update_dto.username = username user_update_dto.validate() except DataError as e: current_app.logger.error(f'error validating request: {str(e)}') return str(e), 400 try: updated_user = UserService.update_user(user_update_dto) return updated_user.to_primitive(), 200 except NotFound: return {"Error": "User not found"}, 404 except Exception as e: error_msg = f'User update - Unhandled error: {str(e)}' current_app.logger.critical(error_msg) return {"Error": error_msg}, 500
def put(self, project_id): """ Add a message to project chat --- tags: - messaging 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: The ID of the project to attach the chat message to required: true type: integer default: 1 - in: body name: body required: true description: JSON object for creating a new mapping license schema: properties: message: type: string default: This is an awesome project responses: 201: description: Message posted successfully 400: description: Invalid Request 500: description: Internal Server Error """ if UserService.is_user_blocked(tm.authenticated_user_id): return 'User is on read only mode', 403 try: chat_dto = ChatMessageDTO(request.get_json()) chat_dto.user_id = tm.authenticated_user_id chat_dto.project_id = project_id chat_dto.validate() except DataError as e: current_app.logger.error(f'Error validating request: {str(e)}') return str(e), 400 try: project_messages = ChatService.post_message(chat_dto) return project_messages.to_primitive(), 201 except Exception as e: error_msg = f'Chat PUT - unhandled error: {str(e)}' current_app.logger.critical(error_msg) return {"error": error_msg}, 500