def test_is_for_alpha_only(self) -> None: """Test that the advisor marks modules not ready for prod as alpha only.""" project = project_pb2.Project( project_id='1234', target_job=job_pb2.Job( name='Steward/ Hôtesse', feminine_name='Hôtesse', masculine_name='Steward', ), ) self.user.features_enabled.alpha = True self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, }, { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(1)', }, ]) advisor.maybe_advise(self.user, project, self.database, 'http://base.example.com') self.assertEqual(['spontaneous-application', 'other-work-env'], [a.advice_id for a in project.advices]) self.assertEqual([False, True], [a.is_for_alpha_only for a in project.advices])
def test_module_crashes(self, mock_logger: mock.MagicMock) -> None: """Test that the advisor does not crash if one module does.""" constant_1_model = typing.cast(_ScoringModelMock, scoring.SCORING_MODELS['constant(1)']) constant_1_model.score_and_explain.return_value = scoring.ExplainedScore(1, []) constant_1_model.get_advice_override.return_value = None crash_me_model = typing.cast(_ScoringModelMock, advisor.scoring.SCORING_MODELS['crash-me']) crash_me_model.score_and_explain.side_effect = ValueError('ouch') crash_me_model.get_advice_override.return_value = None project = project_pb2.Project() self.database.advice_modules.insert_many([ { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'crash-me', 'isReadyForProd': True, }, { 'adviceId': 'network', 'categories': ['first'], 'triggerScoringModel': 'constant(1)', 'isReadyForProd': True, }, ]) self.user.profile.locale = 'fr' advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['network'], [a.advice_id for a in project.advices]) mock_logger.assert_called_once() self.assertIn('REDACTED', mock_logger.call_args[0][0] % mock_logger.call_args[0][1:])
def test_recommend_all_modules(self) -> None: """Test that all advice are recommended when all_modules is true even if incompatible.""" project = project_pb2.Project() self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, 'airtableId': 'abc', 'incompatibleAdviceIds': ['def'], }, { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, 'airtableId': 'def', }, { 'adviceId': 'new-advice', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, 'airtableId': 'def', 'incompatibleAdviceIds': ['abc'], }, ]) self.user.features_enabled.all_modules = True advisor.maybe_advise(self.user, project, self.database) self.assertEqual( ['spontaneous-application', 'other-work-env', 'new-advice'], [a.advice_id for a in project.advices])
def test_all_action_modules(self) -> None: """A user with all_modules enabled should get all actions.""" self.database.action_templates.insert_many([ { 'actionTemplateId': 'not-for-you', 'triggerScoringModel': 'constant(0)', }, { 'actionTemplateId': 'review-me', 'triggerScoringModel': 'constant(1)', 'tags': ['chrome-tool'], 'duration': 'FIFTEEN_TO_30_MIN', }, { 'actionTemplateId': 'finish-the-sprint', 'triggerScoringModel': 'constant(2)', }, ]) self.user.features_enabled.action_plan = features_pb2.ACTIVE self.user.features_enabled.all_modules = True project = project_pb2.Project( project_id='1234', target_job=job_pb2.Job( name='Steward/ Hôtesse', feminine_name='Hôtesse', masculine_name='Steward', ), ) advisor.maybe_advise(self.user, project, self.database) self.assertEqual( ['review-me', 'not-for-you', 'finish-the-sprint'], [a.action_id for a in project.actions])
def test_redacted_email_address(self) -> None: """Test that we do not send any email if the email address is already redacted.""" project = project_pb2.Project( project_id='1234', target_job=job_pb2.Job( name='Steward/ Hôtesse', feminine_name='Hôtesse', masculine_name='Steward', ), ) self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, }, { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, }, ]) self.user.profile.email = 'REDACTED' advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['spontaneous-application'], [a.advice_id for a in project.advices]) self.assertEqual(project_pb2.ADVICE_RECOMMENDED, project.advices[0].status)
def test_advice_other_work_env_extra_data(self): """Test that the advisor computes extra data for the work environment advice.""" project = project_pb2.Project(target_job=job_pb2.Job( job_group=job_pb2.JobGroup(rome_id='A1234')), ) self.user.features_enabled.alpha = True self.database.job_group_info.insert_one({ '_id': 'A1234', 'workEnvironmentKeywords': { 'structures': ['A', 'B'], 'sectors': ['sector Toise', 'sector Gal'], }, }) self.database.advice_modules.insert_one({ 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'advice-other-work-env', 'extraDataFieldName': 'other_work_env_advice_data', 'isReadyForProd': True, }) advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'other-work-env') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual(['A', 'B'], advice.other_work_env_advice_data. work_environment_keywords.structures) self.assertEqual([ 'sector Toise', 'sector Gal' ], advice.other_work_env_advice_data.work_environment_keywords.sectors)
def test_explained_advice(self, mock_scoring_models, mock_send_template): """Test that the advisor gives explanations for the advices.""" mock_send_template().status_code = 200 mock_send_template.reset_mock() mock_scoring_models['constant(1)'] = mock.MagicMock( spec=['score_and_explain']) mock_scoring_models['constant(1)'].score_and_explain.return_value = \ scoring.ExplainedScore(1, ['voilà pourquoi', 'explication genré%eFeminine']) project = project_pb2.Project() self.database.advice_modules.insert_one({ 'adviceId': 'network', 'categories': ['first'], 'triggerScoringModel': 'constant(1)', 'isReadyForProd': True, }) self.user.profile.gender = user_pb2.FEMININE advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['network'], [a.advice_id for a in project.advices]) self.assertEqual(['voilà pourquoi', 'explication genrée'], project.advices[0].explanations) mock_send_template.assert_called_once()
def test_recommend_advice_none(self, mock_logger: mock.MagicMock) -> None: """Test that the advisor does not recommend anyting if all modules score 0.""" project = project_pb2.Project() self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, }, { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, }, ]) advisor.maybe_advise(self.user, project, self.database) self.assertFalse(project.advices) self.assertFalse(mailjetmock.get_all_sent_messages()) mock_logger.assert_called_once()
def test_explained_advice(self, mock_scoring_models: Dict[str, Any]) -> None: """Test that the advisor gives explanations for the advices.""" mock_scoring_models['constant(1)'] = mock.MagicMock( spec=['get_advice_override', 'score_and_explain']) mock_scoring_models['constant(1)'].score_and_explain.return_value = \ scoring.ExplainedScore(1, ['voilà pourquoi', 'explication genré%eFeminine']) mock_scoring_models[ 'constant(1)'].get_advice_override.return_value = None project = project_pb2.Project() self.database.advice_modules.insert_one({ 'adviceId': 'network', 'categories': ['first'], 'triggerScoringModel': 'constant(1)', 'isReadyForProd': True, }) self.user.profile.gender = user_pb2.FEMININE advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['network'], [a.advice_id for a in project.advices]) self.assertEqual(['voilà pourquoi', 'explication genrée'], project.advices[0].explanations) self.assertEqual(1, len(mailjetmock.get_all_sent_messages()))
def test_incompatible_advice_modules(self) -> None: """Test that the advisor discard incompatible advice modules.""" project = project_pb2.Project() self.database.advice_modules.insert_many([ { 'adviceId': 'other-work-env', 'airtableId': 'abc', 'categories': ['first'], 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, 'incompatibleAdviceIds': ['def'], }, { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'airtableId': 'def', 'triggerScoringModel': 'constant(3)', 'isReadyForProd': True, 'incompatibleAdviceIds': ['abc'], }, { 'adviceId': 'final-one', 'categories': ['first'], 'airtableId': 'ghi', 'triggerScoringModel': 'constant(1)', 'isReadyForProd': True, }, ]) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['spontaneous-application', 'final-one'], [a.advice_id for a in project.advices]) self.assertEqual(1, len(mailjetmock.get_all_sent_messages()))
def test_no_advice_if_project_incomplete(self) -> None: """Test that the advice do not get populated when the project is marked as incomplete.""" project = project_pb2.Project(is_incomplete=True) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(len(project.advices), 0)
def test_advice_spontaneous_application_extra_data(self, mock_get_lbb_companies): """Test that the advisor computes extra data for the "Spontaneous Application" advice.""" project = project_pb2.Project( target_job=job_pb2.Job(job_group=job_pb2.JobGroup( rome_id='A1234')), mobility=geo_pb2.Location(city=geo_pb2.FrenchCity( departement_id='14')), job_search_length_months=7, weekly_applications_estimate=project_pb2.A_LOT, total_interview_count=1, ) self.database.job_group_info.insert_one({ '_id': 'A1234', 'applicationModes': { 'R4Z92': { 'modes': [{ 'percentage': 36.38, 'mode': 'SPONTANEOUS_APPLICATION' }, { 'percentage': 29.46, 'mode': 'PERSONAL_OR_PROFESSIONAL_CONTACTS' }, { 'percentage': 18.38, 'mode': 'PLACEMENT_AGENCY' }, { 'percentage': 15.78, 'mode': 'UNDEFINED_APPLICATION_MODE' }], } }, }) self.database.advice_modules.insert_one({ 'adviceId': 'my-advice', 'categories': ['first'], 'triggerScoringModel': 'advice-spontaneous-application', 'extraDataFieldName': 'spontaneous_application_data', 'isReadyForProd': True, }) mock_get_lbb_companies.return_value = iter([ { 'name': 'EX NIHILO' }, { 'name': 'M.F.P MULTIMEDIA FRANCE PRODUCTIONS' }, ]) advisor.clear_cache() advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'my-advice') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual( ['EX NIHILO', 'M.F.P MULTIMEDIA FRANCE PRODUCTIONS'], [c.name for c in advice.spontaneous_application_data.companies])
def test_missing_module(self) -> None: """Test that the advisor does not crash when a module is missing.""" project = project_pb2.Project(advices=[project_pb2.Advice( advice_id='does-not-exist', status=project_pb2.ADVICE_ACCEPTED)]) project_before = str(project) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(project_before, str(project))
def test_advice_specific_to_job_override(self) -> None: """Test that the advisor overrides some advice data with the "Specific to Job" module.""" project = project_pb2.Project(target_job=job_pb2.Job( job_group=job_pb2.JobGroup(rome_id='D1102')), ) self.database.advice_modules.insert_one({ 'adviceId': 'custom-advice-id', 'categories': ['first'], 'triggerScoringModel': 'advice-specific-to-job', 'isReadyForProd': True, }) self.database.specific_to_job_advice.insert_one({ 'title': 'Présentez-vous au chef boulanger dès son arrivée tôt le matin', 'shortTitle': 'Astuces de boulanger', 'goal': 'impressionner le patron', 'diagnosticTopics': [ diagnostic_pb2.MARKET_DIAGNOSTIC, diagnostic_pb2.PROJECT_DIAGNOSTIC ], 'filters': ['for-job-group(D1102)', 'not-for-job(12006)'], 'cardText': 'Allez à la boulangerie la veille pour savoir à quelle ' 'heure arrive le chef boulanger.', 'expandedCardHeader': "Voilà ce qu'il faut faire", 'expandedCardItems': [ 'Se présenter aux boulangers entre 4h et 7h du matin.', 'Demander au vendeur / à la vendeuse à quelle heure arrive le chef le matin', 'Contacter les fournisseurs de farine locaux : ils connaissent ' 'tous les boulangers du coin et sauront où il y a des ' 'embauches.', ], }) advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'custom-advice-id') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual( 'Présentez-vous au chef boulanger dès son arrivée tôt le matin', advice.title) self.assertEqual("Voilà ce qu'il faut faire", advice.expanded_card_header) self.assertTrue(advice.card_text) self.assertTrue(advice.expanded_card_items) self.assertEqual('Astuces de boulanger', advice.short_title) self.assertEqual([ diagnostic_pb2.MARKET_DIAGNOSTIC, diagnostic_pb2.PROJECT_DIAGNOSTIC ], advice.diagnostic_topics) self.assertEqual('impressionner le patron', advice.goal)
def test_generate_actions(self) -> None: """Test that the advisor generates actions if the action_plan feature is active.""" self.database.action_templates.insert_many([ { 'actionTemplateId': 'review-me', 'triggerScoringModel': 'constant(1)', 'tags': ['chrome-tool'], 'duration': 'FIFTEEN_TO_30_MIN', 'resourceUrl': 'https://bar', }, { 'actionTemplateId': 'not-for-you', 'triggerScoringModel': 'constant(0)', }, { 'actionTemplateId': 'finish-the-sprint', 'triggerScoringModel': 'constant(2)', }, ]) self.database.translations.insert_many([ {'string': 'actionTemplates:review-me:title', 'en': 'Review me!'}, {'string': 'actionTemplates:review-me:short_description', 'en': 'Please review me now'}, { 'string': 'actionTemplates:review-me:short_description_FEMININE', 'en': 'Please review me now, Madam', }, {'string': 'actionTemplates:tags:chrome-tool', 'en': 'Chrome Tool'}, {'string': 'actionTemplates:review-me:resource_url_usa', 'en': 'https://foo'}, ]) self.user.features_enabled.action_plan = features_pb2.ACTIVE self.user.profile.locale = 'en' project = project_pb2.Project( project_id='1234', target_job=job_pb2.Job( name='Steward/ Hôtesse', feminine_name='Hôtesse', masculine_name='Steward', ), ) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['finish-the-sprint', 'review-me'], [a.action_id for a in project.actions]) review_me = project.actions[1] self.assertEqual('Review me!', review_me.title) self.assertEqual('Please review me now, Madam', review_me.short_description) self.assertEqual(['Chrome Tool'], review_me.tags) self.assertEqual(action_pb2.FIFTEEN_TO_30_MIN, review_me.duration) self.assertEqual('https://foo', review_me.resource_url)
def test_find_all_pieces_of_advice(self, mock_now: mock.MagicMock) -> None: """Test that the advisor scores all advice modules.""" mock_now.return_value = datetime.datetime(2018, 6, 10) project = project_pb2.Project( project_id='1234', target_job=job_pb2.Job( name='Steward/ Hôtesse', feminine_name='Hôtesse', masculine_name='Steward', ), actions=[ action_pb2.Action( action_id='be-happy', title='Be happy', status=action_pb2.ACTION_UNREAD, ), action_pb2.Action( action_id='dont-worry', title="Don't worry", status=action_pb2.ACTION_CURRENT, ), action_pb2.Action( action_id='sing-along', title='Sing along', status=action_pb2.ACTION_UNREAD, ), ], ) self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, }, { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, }, ]) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['spontaneous-application'], [a.advice_id for a in project.advices]) self.assertEqual(project_pb2.ADVICE_RECOMMENDED, project.advices[0].status)
def test_find_all_pieces_of_advice(self, mock_send_template): """Test that the advisor scores all advice modules.""" mock_send_template().status_code = 200 mock_send_template.reset_mock() project = project_pb2.Project( project_id='1234', target_job=job_pb2.Job( name='Steward/ Hôtesse', feminine_name='Hôtesse', masculine_name='Steward', ), ) self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, }, { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, }, ]) advisor.maybe_advise(self.user, project, self.database, 'http://base.example.com') self.assertEqual(['spontaneous-application'], [a.advice_id for a in project.advices]) self.assertEqual(project_pb2.ADVICE_RECOMMENDED, project.advices[0].status) mock_send_template.assert_called_once() data = mock_send_template.call_args[0][2] self.assertEqual( ['advices', 'baseUrl', 'firstName', 'ofProjectTitle', 'projectId'], sorted(data.keys())) self.assertEqual('http://base.example.com', data['baseUrl']) self.assertEqual('Margaux', data['firstName']) self.assertEqual("d'hôtesse", data['ofProjectTitle']) self.assertEqual('1234', data['projectId'])
def test_module_has_missing_data(self) -> None: """A module fails because of missing data.""" mock_scoring_model = typing.cast(_ScoringModelMock, scoring.SCORING_MODELS['missing-data']) mock_scoring_model.get_advice_override.return_value = None mock_scoring_model.score_and_explain.side_effect = advisor.scoring.NotEnoughDataException( 'Please, feed me data', fields={'field1', 'field2'}, reasons=['missing-diploma']) self.database.advice_modules.insert_one({ 'advice_id': 'get-a-diploma', 'triggerScoringModel': 'missing-data', 'isReadyForProd': True, }) project = project_pb2.Project() advisor.maybe_advise(self.user, project, self.database) self.assertEqual(1, len(project.advices), msg=project.advices) advice = project.advices[0] self.assertIn('missing-diploma', advice.explanations) self.assertEqual('get-a-diploma', advice.advice_id) self.assertAlmostEqual(.1, advice.num_stars)
def test_advice_specific_to_job_override_i18n(self) -> None: """Test that the advisor translate overrides with the "Specific to Job" module.""" self.user.profile.locale = 'en' self.database.translations.insert_many([ {'string': 'specificToJobAdvice:baker:title', 'en': 'Get there early'}, {'string': 'Astuces de boulanger', 'en': 'Baker tips'}, ]) project = project_pb2.Project( target_job=job_pb2.Job(job_group=job_pb2.JobGroup(rome_id='D1102')), ) self.database.advice_modules.insert_one({ 'adviceId': 'custom-advice-id', 'categories': ['first'], 'triggerScoringModel': 'advice-specific-to-job', 'isReadyForProd': True, }) self.database.specific_to_job_advice.insert_one({ 'id': 'baker', 'title': 'Présentez-vous au chef boulanger dès son arrivée tôt le matin', 'shortTitle': 'Astuces de boulanger', 'goal': 'impressionner le patron', 'filters': ['for-job-group(D1102)', 'not-for-job(12006)'], 'cardText': 'Allez à la boulangerie la veille pour savoir à quelle ' 'heure arrive le chef boulanger.', 'expandedCardHeader': "Voilà ce qu'il faut faire", 'expandedCardItems': [ 'Se présenter aux boulangers entre 4h et 7h du matin.', 'Demander au vendeur / à la vendeuse à quelle heure arrive le chef le matin', 'Contacter les fournisseurs de farine locaux : ils connaissent ' 'tous les boulangers du coin et sauront où il y a des ' 'embauches.', ], }) advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'custom-advice-id') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual('Get there early', advice.title) self.assertEqual('Baker tips', advice.short_title)
def test_module_crashes(self, mock_logger, mock_scoring_models): """Test that the advisor does not crash if one module does while getting extra data.""" mock_scoring_models['constant(1)'] = mock.MagicMock( spec=['score_and_explain', 'compute_extra_data']) mock_scoring_models['constant(1)'].score_and_explain.return_value = \ scoring.ExplainedScore(1, []) mock_scoring_models['constant(1)'].compute_extra_data.return_value = \ project_pb2.CommuteData(cities=['Lyon', 'Paris']) mock_scoring_models['crash-me'] = mock.MagicMock( spec=['score_and_explain', 'compute_extra_data']) mock_scoring_models['crash-me'].score_and_explain.return_value = \ scoring.ExplainedScore(2, []) mock_scoring_models[ 'crash-me'].compute_extra_data.side_effect = ValueError('ouch') project = project_pb2.Project() self.database.advice_modules.insert_many([ { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(1)', 'extraDataFieldName': 'commute_data', 'isReadyForProd': True, }, { 'adviceId': 'network', 'categories': ['first'], 'triggerScoringModel': 'crash-me', 'extraDataFieldName': 'commute_data', 'isReadyForProd': True, }, ]) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['network', 'other-work-env'], [a.advice_id for a in project.advices]) self.assertFalse(project.advices[0].WhichOneof('extra_data')) self.assertEqual(['Lyon', 'Paris'], project.advices[1].commute_data.cities) mock_logger.assert_called_once()
def test_module_crashes(self, mock_logger: mock.MagicMock, mock_scoring_models: Dict[str, Any]) -> None: """Test that the advisor does not crash if one module does.""" mock_scoring_models['constant(1)'] = mock.MagicMock( spec=['get_advice_override', 'score_and_explain']) mock_scoring_models['constant(1)'].score_and_explain.return_value = \ scoring.ExplainedScore(1, []) mock_scoring_models[ 'constant(1)'].get_advice_override.return_value = None mock_scoring_models['crash-me'] = mock.MagicMock( spec=['get_advice_override', 'score_and_explain']) mock_scoring_models[ 'crash-me'].score_and_explain.side_effect = ValueError('ouch') mock_scoring_models['crash-me'].get_advice_override.return_value = None project = project_pb2.Project() self.database.advice_modules.insert_many([ { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'crash-me', 'isReadyForProd': True, }, { 'adviceId': 'network', 'categories': ['first'], 'triggerScoringModel': 'constant(1)', 'isReadyForProd': True, }, ]) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['network'], [a.advice_id for a in project.advices]) self.assertEqual(1, len(mailjetmock.get_all_sent_messages())) mock_logger.assert_called_once() self.assertIn( 'REDACTED', mock_logger.call_args[0][0] % mock_logger.call_args[0][1:])
def _save_project(project: project_pb2.Project, previous_project: project_pb2.Project, user_data: user_pb2.User) -> project_pb2.Project: # TODO(cyrille): Check for completeness here, rather than in client. if project.is_incomplete: return project tick.tick('Process project start') rome_id = project.target_job.job_group.rome_id departement_id = project.city.departement_id if not project.project_id: # Add ID, timestamp and stats to new projects project.project_id = _create_new_project_id(user_data) common_proto.set_date_now(project.created_at) database = mongo.get_connections_from_env().stats_db tick.tick('Populate local stats') if previous_project.city.departement_id != departement_id or \ previous_project.target_job.job_group.rome_id != rome_id: project.ClearField('local_stats') if not project.HasField('local_stats'): project.local_stats.CopyFrom( jobs.get_local_stats(database, departement_id, rome_id)) tick.tick('Diagnostic') diagnostic.maybe_diagnose(user_data, project, database) tick.tick('Advisor') advisor.maybe_advise(user_data, project, database) tick.tick('Strategies') strategist.maybe_strategize(user_data, project, database) tick.tick('New feedback') if project.feedback.text and not previous_project.feedback.text: give_project_feedback(user_data.user_id, '@' in user_data.profile.email, project) tick.tick('Process project end') return project
def test_incompatible_advice_modules(self, mock_send_template): """Test that the advisor discard incompatible advice modules.""" mock_send_template().status_code = 200 mock_send_template.reset_mock() project = project_pb2.Project() self.database.advice_modules.insert_many([ { 'adviceId': 'other-work-env', 'airtableId': 'abc', 'categories': ['first'], 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, 'incompatibleAdviceIds': ['def'], }, { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'airtableId': 'def', 'triggerScoringModel': 'constant(3)', 'isReadyForProd': True, 'incompatibleAdviceIds': ['abc'], }, { 'adviceId': 'final-one', 'categories': ['first'], 'airtableId': 'ghi', 'triggerScoringModel': 'constant(1)', 'isReadyForProd': True, }, ]) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['spontaneous-application', 'final-one'], [a.advice_id for a in project.advices]) mock_send_template.assert_called_once()
def test_module_crashes(self, mock_logger, mock_scoring_models, mock_send_template): """Test that the advisor does not crash if one module does.""" mock_send_template().status_code = 200 mock_send_template.reset_mock() mock_scoring_models['constant(1)'] = mock.MagicMock( spec=['score_and_explain']) mock_scoring_models['constant(1)'].score_and_explain.return_value = \ scoring.ExplainedScore(1, []) mock_scoring_models['crash-me'] = mock.MagicMock( spec=['score_and_explain']) mock_scoring_models[ 'crash-me'].score_and_explain.side_effect = ValueError('ouch') project = project_pb2.Project() self.database.advice_modules.insert_many([ { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'crash-me', 'isReadyForProd': True, }, { 'adviceId': 'network', 'categories': ['first'], 'triggerScoringModel': 'constant(1)', 'isReadyForProd': True, }, ]) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['network'], [a.advice_id for a in project.advices]) mock_send_template.assert_called_once() mock_logger.assert_called_once()
def test_missing_email_address(self) -> None: """Test that we do not send any email if we are missing the address.""" project = project_pb2.Project( project_id='1234', target_job=job_pb2.Job( name='Steward/ Hôtesse', feminine_name='Hôtesse', masculine_name='Steward', ), ) self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, }, { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, }, ]) self.user.profile.email = '' advisor.maybe_advise(self.user, project, self.database, 'http://base.example.com') self.assertEqual(['spontaneous-application'], [a.advice_id for a in project.advices]) self.assertEqual(project_pb2.ADVICE_RECOMMENDED, project.advices[0].status) mails_sent = mailjetmock.get_all_sent_messages() self.assertFalse(mails_sent)
def test_recommend_advice_none(self, mock_send_template): """Test that the advisor does not recommend anyting if all modules score 0.""" project = project_pb2.Project() self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, }, { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, }, ]) advisor.maybe_advise(self.user, project, self.database) self.assertFalse(project.advices) mock_send_template.assert_not_called()
def test_explained_advice(self) -> None: """Test that the advisor gives explanations for the advices.""" mock_scoring_model = typing.cast(_ScoringModelMock, scoring.SCORING_MODELS['constant(1)']) mock_scoring_model.score_and_explain.return_value = \ scoring.ExplainedScore(1, ['voilà pourquoi', 'explication genré%eFeminine']) mock_scoring_model.get_advice_override.return_value = None project = project_pb2.Project() self.database.advice_modules.insert_one({ 'adviceId': 'network', 'categories': ['first'], 'triggerScoringModel': 'constant(1)', 'isReadyForProd': True, }) self.user.profile.gender = user_profile_pb2.FEMININE advisor.maybe_advise(self.user, project, self.database) self.assertEqual( ['network'], [a.advice_id for a in project.advices]) self.assertEqual( ['voilà pourquoi', 'explication genrée'], project.advices[0].explanations)
def test_find_all_pieces_of_advice(self, mock_now: mock.MagicMock) -> None: """Test that the advisor scores all advice modules.""" mock_now.return_value = datetime.datetime(2018, 6, 10) project = project_pb2.Project( project_id='1234', target_job=job_pb2.Job( name='Steward/ Hôtesse', feminine_name='Hôtesse', masculine_name='Steward', ), ) self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, }, { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, }, ]) advisor.maybe_advise(self.user, project, self.database, 'http://base.example.com') self.assertEqual(['spontaneous-application'], [a.advice_id for a in project.advices]) self.assertEqual(project_pb2.ADVICE_RECOMMENDED, project.advices[0].status) mails_sent = mailjetmock.get_all_sent_messages() self.assertEqual(1, len(mails_sent), msg=mails_sent) data = mails_sent[0].properties['Variables'] with open(path.join(_TEMPLATE_PATH, 'activation-email', 'vars.txt'), 'r') as vars_file: template_vars = {v.strip() for v in vars_file} self.assertEqual(template_vars, set(data.keys())) self.assertEqual('10 juin 2018', data['date']) self.assertEqual('Margaux', data['firstName']) self.assertEqual('FEMININE', data['gender']) self.assertEqual("d'hôtesse", data['ofJob']) login_url = data.pop('loginUrl') base_url = re.escape( f'http://base.example.com?userId={self.user.user_id}') self.assertRegex(login_url, rf'^{base_url}&authToken=\d+\.[a-f0-9]+$') email_settings_url = data.pop('changeEmailSettingsUrl') base_url = re.escape( f'http://base.example.com/unsubscribe.html?user={self.user.user_id}' ) self.assertRegex( email_settings_url, rf'^{base_url}&auth=\d+\.[a-f0-9]+&coachingEmailFrequency=UNKNOWN_EMAIL_FREQUENCY&' r'hl=fr%40tu$') self.assertEqual('', data['isCoachingEnabled'])
def _save_project(project: project_pb2.Project, previous_project: project_pb2.Project, user_data: user_pb2.User) -> project_pb2.Project: # TODO(cyrille): Check for completeness here, rather than in client. if project.is_incomplete: return project _tick('Process project start') rome_id = project.target_job.job_group.rome_id departement_id = project.city.departement_id if not project.project_id: # Add ID, timestamp and stats to new projects project.project_id = _create_new_project_id(user_data) project.created_at.FromDatetime(now.get()) project.created_at.nanos = 0 if not project.WhichOneof( 'job_search_length') and project.job_search_length_months: if project.job_search_length_months < 0: project.job_search_has_not_started = True else: job_search_length_days = 30.5 * project.job_search_length_months job_search_length_duration = datetime.timedelta( days=job_search_length_days) project.job_search_started_at.FromDatetime( project.created_at.ToDatetime() - job_search_length_duration) project.job_search_started_at.nanos = 0 database = flask.current_app.config['DATABASE'] _tick('Populate local stats') if previous_project.city.departement_id != departement_id or \ previous_project.target_job.job_group.rome_id != rome_id: project.ClearField('local_stats') if not project.HasField('local_stats'): project.local_stats.CopyFrom( jobs.get_local_stats(database, departement_id, rome_id)) _tick('Diagnostic') diagnostic.maybe_diagnose(user_data, project, database) _tick('Advisor') advisor.maybe_advise(user_data, project, database, parse.urljoin(flask.request.base_url, '/')[:-1]) _tick('Strategies') strategist.maybe_strategize(user_data, project, database) _tick('New feedback') if project.feedback.text and not previous_project.feedback.text: stars = ':star:' * project.feedback.score user_url = parse.urljoin(flask.request.base_url, f'/eval?userId={user_data.user_id}') feedback = '\n> '.join(project.feedback.text.split('\n')) slack_text = f'[{stars}] <{user_url}|{user_data.user_id}>\n> {feedback}' give_feedback(feedback_pb2.Feedback( user_id=str(user_data.user_id), project_id=str(project.project_id), feedback=project.feedback.text, source=feedback_pb2.PROJECT_FEEDBACK, score=project.feedback.score), slack_text=slack_text) _tick('Process project end') return project
def _save_user(user_data, is_new_user): _tick('Save user start') if is_new_user: previous_user_data = user_data else: _tick('Load old user data') previous_user_data = _get_user_data(user_data.user_id) if user_data.revision and previous_user_data.revision > user_data.revision: # Do not overwrite newer data that was saved already: just return it. return previous_user_data if not previous_user_data.registered_at.seconds: user_data.registered_at.FromDatetime(now.get()) # No need to pollute our DB with super precise timestamps. user_data.registered_at.nanos = 0 # Enable Advisor for new users. if not ADVISOR_DISABLED_FOR_TESTING: user_data.features_enabled.advisor = user_pb2.ACTIVE user_data.features_enabled.advisor_email = user_pb2.ACTIVE user_data.profile.email_days.extend( [user_pb2.MONDAY, user_pb2.WEDNESDAY, user_pb2.FRIDAY]) elif not _is_test_user(previous_user_data): user_data.registered_at.CopyFrom(previous_user_data.registered_at) user_data.features_enabled.advisor = previous_user_data.features_enabled.advisor _tick('Unverified data zone check start') # TODO(guillaume): Check out how we could not recompute that every time gracefully. if _is_in_unverified_data_zone(user_data.profile, user_data.projects): user_data.app_not_available = True _tick('Unverified data zone check end') _populate_feature_flags(user_data) for project in user_data.projects: if project.is_incomplete: continue _tick('Process project start') rome_id = project.target_job.job_group.rome_id if not project.project_id: # Add ID, timestamp and stats to new projects project.project_id = _create_new_project_id(user_data) project.created_at.FromDatetime(now.get()) previous_project = next((p for p in previous_user_data.projects if p.project_id == project.project_id), project_pb2.Project()) if project.job_search_length_months != previous_project.job_search_length_months: if project.job_search_length_months < 0: project.job_search_has_not_started = True else: job_search_length_days = 30.5 * project.job_search_length_months job_search_length_duration = datetime.timedelta( days=job_search_length_days) project.job_search_started_at.FromDatetime( project.created_at.ToDatetime() - job_search_length_duration) project.job_search_started_at.nanos = 0 _tick('Populate local stats') if not project.HasField('local_stats'): _populate_job_stats_dict({rome_id: project.local_stats}, project.mobility.city) _tick('Diagnostic') advisor.maybe_diagnose(user_data, project, _DB) _tick('Advisor') advisor.maybe_advise(user_data, project, _DB, parse.urljoin(flask.request.base_url, '/')[:-1]) _tick('Categorizer') advisor.maybe_categorize_advice(user_data, project, _DB) _tick('New feedback') if not is_new_user and (project.feedback.text or project.feedback.score): if project.feedback.text and not previous_project.feedback.text: _give_feedback( feedback_pb2.Feedback( user_id=str(user_data.user_id), project_id=str(project.project_id), feedback=project.feedback.text, source=feedback_pb2.PROJECT_FEEDBACK)) _tick('Process project end') if not is_new_user: _assert_no_credentials_change(previous_user_data, user_data) _copy_unmodifiable_fields(previous_user_data, user_data) _populate_feature_flags(user_data) user_data.revision += 1 _tick('Save user') _save_low_level(user_data, is_new_user=is_new_user) _tick('Return user proto') return user_data