コード例 #1
0
ファイル: advisor_test.py プロジェクト: b3rday/bob-emploi
    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])
コード例 #2
0
    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:])
コード例 #3
0
    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])
コード例 #4
0
    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])
コード例 #5
0
    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)
コード例 #6
0
    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)
コード例 #7
0
    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()
コード例 #8
0
ファイル: advisor_test.py プロジェクト: b3rday/bob-emploi
    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()
コード例 #9
0
ファイル: advisor_test.py プロジェクト: b3rday/bob-emploi
    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()))
コード例 #10
0
ファイル: advisor_test.py プロジェクト: b3rday/bob-emploi
    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()))
コード例 #11
0
    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)
コード例 #12
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])
コード例 #13
0
    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))
コード例 #14
0
ファイル: advisor_test.py プロジェクト: b3rday/bob-emploi
    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)
コード例 #15
0
    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)
コード例 #16
0
    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)
コード例 #17
0
    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'])
コード例 #18
0
    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)
コード例 #19
0
    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)
コード例 #20
0
    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()
コード例 #21
0
ファイル: advisor_test.py プロジェクト: b3rday/bob-emploi
    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:])
コード例 #22
0
ファイル: user.py プロジェクト: bayesimpact/bob-emploi
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
コード例 #23
0
    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()
コード例 #24
0
    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()
コード例 #25
0
ファイル: advisor_test.py プロジェクト: b3rday/bob-emploi
    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)
コード例 #26
0
    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()
コード例 #27
0
    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)
コード例 #28
0
ファイル: advisor_test.py プロジェクト: b3rday/bob-emploi
    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'])
コード例 #29
0
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
コード例 #30
0
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