'advicePageUrl': advice_page_url, 'hasInterviewFrustration': campaign.as_template_boolean(user_profile_pb2.INTERVIEW in user.profile.frustrations), 'hasSelfConfidenceFrustration': campaign.as_template_boolean(user_profile_pb2.SELF_CONFIDENCE in user.profile.frustrations), } _CAMPAIGNS = [ campaign.Campaign( campaign_id='prepare-your-application', mongo_filters={ 'projects': {'$elemMatch': { 'isIncomplete': {'$ne': True}, }}, }, get_vars=_get_prepare_your_application_vars, sender_name=i18n.make_translatable_string("Joanna et l'équipe de {{var:productName}}"), sender_email='*****@*****.**', is_coaching=True, is_big_focus=True, ), campaign.Campaign( campaign_id='prepare-your-application-short', mongo_filters={ 'projects': {'$elemMatch': { 'isIncomplete': {'$ne': True}, }}, }, get_vars=_get_prepare_your_application_short_vars, sender_name=i18n.make_translatable_string("Pascal et l'équipe de {{var:productName}}"),
'de definir votre projet professionnel') return campaign.get_default_coaching_email_vars(user) | { 'inDepartement': scoring_project.populate_template('%inDepartement'), 'jobs': [{'name': job.name} for job in reorient_jobs], 'ofJobName': of_job_name, } campaign.register_campaign(campaign.Campaign( campaign_id='jobbing', mongo_filters={ 'projects': {'$elemMatch': { 'isIncomplete': {'$ne': True}, 'openedStrategies.strategyId': 'diploma-free-job', }}, }, get_vars=_get_jobbing_vars, sender_name=i18n.make_translatable_string("Joanna et l'équipe de {{var:productName}}"), sender_email='*****@*****.**', is_coaching=True, is_big_focus=False, )) campaign.register_campaign(campaign.Campaign( campaign_id='jobbing-short', mongo_filters={ 'projects': {'$elemMatch': { 'isIncomplete': {'$ne': True}, 'openedStrategies.strategyId': 'diploma-free-job', }}, }, get_vars=_get_jobbing_short_vars,
) -> dict[str, Any]: if not any(email.campaign_id == 'jobflix-first-eval' for email in user.emails_sent): raise campaign.DoNotSend( 'Only useful for user that have received the first campaign') next_week = now + datetime.timedelta(days=7) return campaign.get_default_vars(user) | { 'closingDate': next_week.strftime('%A %d %B'), } campaign.register_campaign( campaign.Campaign(campaign_id='jobflix-first-eval', mongo_filters={'projects.isIncomplete': { '$ne': True }}, get_vars=_get_first_eval_vars, sender_name=i18n.make_translatable_string( "{0} et l'équipe de {1}").format('Tabitha', 'Bob'), sender_email='*****@*****.**')) campaign.register_campaign( campaign.Campaign( campaign_id='jobflix-first-eval-reminder', mongo_filters={'emailsSent.campaignId': 'jobflix-first-eval'}, get_vars=_get_first_eval_reminder_vars, sender_name=i18n.make_translatable_string( "{0} et l'équipe de {1}").format('Tabitha', 'Bob'), sender_email='*****@*****.**')) campaign.register_campaign( campaign.Campaign(campaign_id='jobflix-welcome',
import pymongo import requests from bob_emploi.common.python.test import nowmock from bob_emploi.frontend.api import email_pb2 from bob_emploi.frontend.server import mongo from bob_emploi.frontend.server import product from bob_emploi.frontend.server.mail import campaign from bob_emploi.frontend.server.mail import mail_blast from bob_emploi.frontend.server.mail.templates import mailjet_templates from bob_emploi.frontend.server.test import mailjetmock _FAKE_CAMPAIGNS = { 'fake-user-campaign': campaign.Campaign(typing.cast(mailjet_templates.Id, 'fake-user-campaign'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**') } class EmailPolicyTestCase(unittest.TestCase): """Tests for the EmailPolicy class.""" def _make_email( self, campaign_id: str, days_ago: int = 0, hours_ago: int = 0, status: 'email_pb2.EmailSentStatus.V' = email_pb2.EMAIL_SENT_SENT, status_updated_days_after: Optional[int] = 8 ) -> email_pb2.EmailSent: email = email_pb2.EmailSent(campaign_id=campaign_id, status=status)
why_specific_company, } campaign.register_campaign( campaign.Campaign( campaign_id='focus-spontaneous', mongo_filters={ 'projects': { '$elemMatch': { 'jobSearchHasNotStarted': { '$ne': True }, 'isIncomplete': { '$ne': True }, } }, }, get_vars=_get_spontaneous_vars, sender_name=i18n.make_translatable_string( "Joanna et l'équipe de {{var:productName}}"), sender_email='*****@*****.**', is_coaching=True, is_big_focus=True, )) campaign.register_campaign( campaign.Campaign( campaign_id='spontaneous-short', mongo_filters={ 'projects': {
} _NPS_CAMPAIGN = campaign.Campaign( # See https://app.mailjet.com/template/100819/build campaign_id=_CAMPAIGN_ID, mongo_filters={ 'emailsSent': { '$not': { '$elemMatch': { 'campaignId': _CAMPAIGN_ID } } }, 'projects': { '$exists': True }, 'projects.isIncomplete': { '$ne': True }, 'registeredAt': { '$gt': '2018-01-01' }, }, get_vars=_get_nps_vars, sender_name=product.bob.name, sender_email='*****@*****.**', ) campaign.register_campaign(_NPS_CAMPAIGN)
class SendFocusEmailTest(unittest.TestCase): """Unit tests for the main function.""" def setUp(self) -> None: super().setUp() patcher = mock.patch(focus.mongo.__name__ + '.get_connections_from_env') mock_mongo = patcher.start() self.addCleanup(patcher.stop) self._db = mongomock.MongoClient() mock_mongo.return_value = (focus.mongo.NoPiiMongoDatabase( self._db.test), focus.mongo.UsersDatabase.from_database( self._db.user_test), focus.mongo.NoPiiMongoDatabase( self._db.eval_test)) patcher = nowmock.patch() self.mock_now = patcher.start() self.addCleanup(patcher.stop) self.mock_now.return_value = datetime.datetime(2018, 5, 31, 12, 38) self._db.user_test.user.insert_one({ 'profile': { 'coachingEmailFrequency': 'EMAIL_MAXIMUM', 'email': '*****@*****.**', 'frustrations': ['SELF_CONFIDENCE', 'INTERVIEW'], }, 'projects': [{ 'kind': 'FIND_A_FIRST_JOB', 'network_estimate': 1, 'jobSearchStartedAt': '2017-10-01T09:34:00Z', 'targetJob': { 'jobGroup': { 'romeId': 'A1234' } }, }], 'registeredAt': '2018-01-15T15:24:34Z', }) self._db.test.focus_emails.insert_many([{ 'campaignId': c } for c in _GOLDEN_FOCUS_CAMPAIGNS]) self._db.test.job_group_info.insert_one({ '_id': 'A1234', 'inDomain': 'dans le domaine', }) @mock.patch('logging.info') def test_list_campaigns(self, mock_logging: mock.MagicMock) -> None: """List existing focus email campaigns.""" self._db.user_test.user.drop() focus.main(['list']) mock_logging.assert_called() for logging_call in mock_logging.call_args_list: if logging_call[0][0].startswith('Potential focus emails:'): self.assertEqual( list(_GOLDEN_FOCUS_CAMPAIGNS), logging_call[0][1], msg= "Update the golden focus campaigns as it's used in other tests." ) break else: # pragma: no-cover self.fail('No logging call about potential focus emails.') @mock.patch('logging.info') def test_list_emails(self, mock_logging: mock.MagicMock) -> None: """List uses logging extensively but does not send any email.""" focus.main(['list']) mock_logging.assert_called() self.assertFalse(mailjetmock.get_all_sent_messages()) def test_send_first(self) -> None: """Sending a first focus email.""" focus.main(['send', '--disable-sentry']) self.assertEqual(['*****@*****.**'], [ m.recipient['Email'] for m in mailjetmock.get_all_sent_messages() ]) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual(1, len(user_data.get('emailsSent'))) self.assertIn(user_data['emailsSent'][0]['campaignId'], _GOLDEN_FOCUS_CAMPAIGNS) def test_send_first_too_early(self) -> None: """Sending a first focus email too soon after a registration.""" self._db.user_test.user.update_one( {}, {'$set': { 'registeredAt': '2018-05-30T14:22:00Z' }}) focus.main(['send', '--disable-sentry']) self.assertFalse(mailjetmock.get_all_sent_messages()) user_data = self._db.user_test.user.find_one() assert user_data self.assertFalse(user_data.get('emailsSent')) self.assertEqual('2018-06-02T14:22:00Z', user_data.get('sendCoachingEmailAfter')) def test_send_shortly_after_another(self) -> None: """Sending a second focus email shortly after the first one.""" focus.main(['send', '--disable-sentry']) self.mock_now.return_value += datetime.timedelta(hours=1) mailjetmock.clear_sent_messages() focus.main(['send', '--disable-sentry']) self.assertFalse(mailjetmock.get_all_sent_messages()) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual(1, len(user_data.get('emailsSent'))) def test_send_a_week_after_another(self) -> None: """Sending a second focus email a week after the first one.""" focus.main(['send', '--disable-sentry']) self.mock_now.return_value += datetime.timedelta(days=9) mailjetmock.clear_sent_messages() focus.main(['send', '--disable-sentry']) self.assertEqual(['*****@*****.**'], [ m.recipient['Email'] for m in mailjetmock.get_all_sent_messages() ]) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual(2, len(user_data.get('emailsSent'))) self.assertIn(user_data['emailsSent'][1]['campaignId'], _GOLDEN_FOCUS_CAMPAIGNS) def test_send_only_once_a_month(self) -> None: """Sending focus emails to user on "once-a-month" frequency.""" self._db.user_test.user.update_one( {}, {'$set': { 'profile.coachingEmailFrequency': 'EMAIL_ONCE_A_MONTH' }}) focus.main(['send', '--disable-sentry']) self.mock_now.return_value += datetime.timedelta(days=15) mailjetmock.clear_sent_messages() focus.main(['send', '--disable-sentry']) # No email sent, even 15 days later. self.assertFalse(mailjetmock.get_all_sent_messages()) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual(1, len(user_data.get('emailsSent'))) self.mock_now.return_value += datetime.timedelta(days=30) mailjetmock.clear_sent_messages() focus.main(['send', '--disable-sentry']) self.assertEqual(['*****@*****.**'], [ m.recipient['Email'] for m in mailjetmock.get_all_sent_messages() ]) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual(2, len(user_data.get('emailsSent'))) def test_send_after_not_focus(self) -> None: """Sending a second focus email shortly after another random email.""" self._db.user_test.user.update_one({}, { '$push': { 'emailsSent': { 'campaignId': 'not-a-focus', 'sentAt': '2018-05-30T23:12:00Z', } } }) focus.main(['send', '--disable-sentry']) self.assertEqual(['*****@*****.**'], [ m.recipient['Email'] for m in mailjetmock.get_all_sent_messages() ]) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual(2, len(user_data.get('emailsSent'))) self.assertIn(user_data['emailsSent'][1]['campaignId'], _GOLDEN_FOCUS_CAMPAIGNS) @mock.patch('logging.info') def test_send_all_focus_emails( self, unused_mock_logging: mock.MagicMock) -> None: """Sending all focus emails in 6 months.""" days_without_email = 0 sent_emails_count = 0 # Try sending emails until there has been a month without any email sent. while days_without_email < 30 and sent_emails_count <= len( _GOLDEN_FOCUS_CAMPAIGNS): focus.main(['send', '--disable-sentry']) emails_sent = mailjetmock.get_all_sent_messages() if len(emails_sent) > sent_emails_count: sent_emails_count = len(emails_sent) days_without_email = 0 else: days_without_email += 1 self.mock_now.return_value += datetime.timedelta(days=1) emails_sent = mailjetmock.get_all_sent_messages() self.assertEqual({'*****@*****.**'}, {m.recipient['Email'] for m in emails_sent}) self.assertLessEqual(len(emails_sent), len(_GOLDEN_FOCUS_CAMPAIGNS)) user_data = self._db.user_test.user.find_one() assert user_data campaigns_sent = [e.get('campaignId') for e in user_data['emailsSent']] self.assertCountEqual(set(campaigns_sent), campaigns_sent, msg='No duplicates') self.assertLessEqual(set(campaigns_sent), set(_GOLDEN_FOCUS_CAMPAIGNS)) # Try sending emails until the next check. next_date = datetime.datetime.fromisoformat( user_data['sendCoachingEmailAfter'][:-1]) while next_date >= self.mock_now.return_value: focus.main(['send', '--disable-sentry']) self.mock_now.return_value += datetime.timedelta(days=1) self.assertEqual( len(emails_sent), len(mailjetmock.get_all_sent_messages()), msg='No new messages.' ' There probably is an issue with time sensitive conditions on some emails' ) user_data = self._db.user_test.user.find_one() # Next check should be at least a month from now. self.assertLessEqual( self.mock_now.return_value + datetime.timedelta(days=30), datetime.datetime.fromisoformat( user_data['sendCoachingEmailAfter'][:-1])) @mock.patch('logging.info') @mock.patch('random.random', new=lambda: 0.5) def test_change_setting(self, unused_mock_logging: mock.MagicMock) -> None: """Changing the settings after the first email has been sent.""" focus.main(['send', '--disable-sentry']) mailjetmock.clear_sent_messages() # Change the email frequency setting right after the first email. self._db.user_test.user.update_one({}, { '$set': { 'profile.coachingEmailFrequency': 'EMAIL_ONCE_A_MONTH' }, '$unset': { 'sendCoachingEmailAfter': 1 }, }) # A week later, there should be no email. for unused_day in range(7): self.mock_now.return_value += datetime.timedelta(days=1) focus.main(['send', '--disable-sentry']) self.assertFalse(mailjetmock.get_all_sent_messages()) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual(1, len(user_data.get('emailsSent', []))) # A month later, there should be another email. for unused_data in range(30): self.mock_now.return_value += datetime.timedelta(days=1) focus.main(['send', '--disable-sentry']) self.assertEqual(['*****@*****.**'], [ m.recipient['Email'] for m in mailjetmock.get_all_sent_messages() ]) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual(2, len(user_data.get('emailsSent', []))) self.assertIn(user_data['emailsSent'][1]['campaignId'], _GOLDEN_FOCUS_CAMPAIGNS) def test_dont_send_to_deleted(self) -> None: """Do not send focus emails to deleted users.""" self._db.user_test.user.update_one({}, { '$set': { 'profile.email': 'REDACTED', 'deletedAt': '2018-06-01T15:24:34Z', } }) focus.main(['send', '--disable-sentry']) self.assertFalse(mailjetmock.get_all_sent_messages()) def test_dont_send_to_mistyped_emails(self) -> None: """Do not send focus emails to users with an incorrect email address.""" self._db.user_test.user.update_one( {}, {'$set': { 'profile.email': 'pascal@ corpet.net', }}) focus.main(['send', '--disable-sentry']) self.assertFalse(mailjetmock.get_all_sent_messages()) self._db.user_test.user.update_one( {}, {'$set': { 'profile.email': 'pascal@corpet', }}) focus.main(['send', '--disable-sentry']) self.assertFalse(mailjetmock.get_all_sent_messages()) @mock.patch('logging.warning') def test_dont_send_to_example(self, mock_warning: mock.MagicMock) -> None: """Do not send focus emails to users with an example email address.""" self._db.user_test.user.update_one( {}, {'$set': { 'profile.email': '*****@*****.**', }}) focus.main(['send', '--disable-sentry']) self.assertFalse(mailjetmock.get_all_sent_messages()) mock_warning.assert_not_called() def test_restrict_campaign(self) -> None: """Restrict to only one campaign.""" self._db.test.focus_emails.drop() self._db.test.focus_emails.insert_one({'campaignId': 'galita-2'}) focus.main(['send', '--disable-sentry']) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual('galita-2', user_data['emailsSent'][0]['campaignId']) @mock.patch('bob_emploi.frontend.server.mail.mail_send.send_template') @mock.patch('logging.warning') def test_error_while_sending(self, mock_warning: mock.MagicMock, mock_send_template: mock.MagicMock) -> None: """Error when sending a focus email get caught and logged as warning.""" mock_send_template( ).raise_for_status.side_effect = requests.exceptions.HTTPError focus.main(['send', '--disable-sentry']) self.assertFalse(mailjetmock.get_all_sent_messages()) user_data = self._db.user_test.user.find_one() assert user_data self.assertFalse(user_data.get('emailsSent')) mock_warning.assert_called_once() self.assertEqual('Error while sending an email: %s', mock_warning.call_args[0][0]) @mock.patch('bob_emploi.frontend.server.mail.mail_send.send_template') def test_error_while_dry_run(self, mock_send_template: mock.MagicMock) -> None: """Error when sending a focus email in dry run mode.""" mock_send_template( ).raise_for_status.side_effect = requests.exceptions.HTTPError with self.assertRaises(requests.exceptions.HTTPError): focus.main(['dry-run', '--disable-sentry']) self.assertFalse(mailjetmock.get_all_sent_messages()) user_data = self._db.user_test.user.find_one() assert user_data self.assertFalse(user_data.get('emailsSent')) def test_error_no_secret_salt(self) -> None: """Error when trying to send without a secret salt.""" with mock.patch(focus.auth_token.__name__ + '.SECRET_SALT', new=focus.auth_token.FAKE_SECRET_SALT): with self.assertRaises(ValueError): focus.main(['send', '--disable-sentry']) @mock.patch(focus.report.__name__ + '._setup_sentry_logging') @mock.patch.dict(os.environ, {'SENTRY_DSN': 'fake-sentry'}) def test_setup_report(self, mock_setup_sentry: mock.MagicMock) -> None: """Make sure the report is setup.""" focus.main(['send']) mock_setup_sentry.assert_called_once_with('fake-sentry') self.assertTrue(mailjetmock.get_all_sent_messages()) @mock.patch('logging.error') def test_failed_setup_report(self, mock_error: mock.MagicMock) -> None: """Warn if the report is not correctly setup.""" focus.main(['send']) mock_error.assert_called_once_with( 'Please set SENTRY_DSN to enable logging to Sentry, or use --disable-sentry option' ) self.assertFalse(mailjetmock.get_all_sent_messages()) def test_ghost_mode(self) -> None: """Test the ghost mode.""" user = user_pb2.User() user.profile.coaching_email_frequency = email_pb2.EMAIL_ONCE_A_MONTH user.profile.frustrations.append(user_profile_pb2.SELF_CONFIDENCE) user.projects.add() campaign_id = focus.send_focus_email_to_user( 'ghost', user, database=focus.mongo.NoPiiMongoDatabase(self._db.test), instant=datetime.datetime.now()) self.assertIn(campaign_id, _GOLDEN_FOCUS_CAMPAIGNS) self.assertEqual([campaign_id], [e.campaign_id for e in user.emails_sent]) self.assertGreater(user.send_coaching_email_after.ToDatetime(), datetime.datetime.now()) self.assertFalse(mailjetmock.get_all_sent_messages()) def test_ghost_email_none(self) -> None: """Test the ghost mode for users that don't want any emails.""" user = user_pb2.User() user.profile.coaching_email_frequency = email_pb2.EMAIL_NONE user.profile.frustrations.append(user_profile_pb2.SELF_CONFIDENCE) user.projects.add() campaign_id = focus.send_focus_email_to_user( 'ghost', user, database=focus.mongo.NoPiiMongoDatabase(self._db.test), instant=datetime.datetime.now()) self.assertFalse(campaign_id) self.assertFalse(mailjetmock.get_all_sent_messages()) @mock.patch( focus.__name__ + '._FOCUS_CAMPAIGNS', { 'coaching-campaign': campaign.Campaign( typing.cast(mailjet_templates.Id, 'coaching-campaign'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_coaching=True, ), }) @mock.patch.dict(mailjet_templates.MAP, { 'coaching-campaign': { 'mailjetTemplate': 0 }, }) @mock.patch(campaign.__name__ + '.get_campaign_subject', lambda campaign_id: { 'coaching-campaign': 'Campagne de coaching', }[campaign_id]) def test_focus_with_project_score_zero(self) -> None: """Test no email sent if project score is 0""" self._db.test.focus_emails.drop() self._db.test.focus_emails.insert_many([ { 'campaignId': 'post-covid', 'scoringModel': 'constant(0)' }, ]) mailjetmock.clear_sent_messages() user = user_pb2.User() user.profile.coaching_email_frequency = email_pb2.EMAIL_ONCE_A_MONTH user.profile.frustrations.append(user_profile_pb2.SELF_CONFIDENCE) user.projects.add() focus.main(['send', '--disable-sentry']) self.assertFalse(mailjetmock.get_all_sent_messages()) @mock.patch( focus.__name__ + '._FOCUS_CAMPAIGNS', { 'post-covid': campaign.Campaign( typing.cast(mailjet_templates.Id, 'post-covid'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_big_focus=False, ), }) def test_focus_with_no_scoring_model(self) -> None: """Test email sent if there is no scoring model""" self._db.test.focus_emails.drop() self._db.test.focus_emails.insert_many([ { 'campaignId': 'post-covid' }, ]) mailjetmock.clear_sent_messages() user = user_pb2.User() user.profile.coaching_email_frequency = email_pb2.EMAIL_ONCE_A_MONTH user.profile.frustrations.append(user_profile_pb2.SELF_CONFIDENCE) user.projects.add() focus.main(['send', '--disable-sentry']) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual(1, len(user_data.get('emailsSent'))) self.assertEqual(user_data['emailsSent'][0]['campaignId'], 'post-covid') @mock.patch( focus.__name__ + '._FOCUS_CAMPAIGNS', { 'big-important': campaign.Campaign( typing.cast(mailjet_templates.Id, 'big-important'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_big_focus=True, ), 'small-very-important': campaign.Campaign( typing.cast(mailjet_templates.Id, 'small-very-important'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_big_focus=False, ), 'big-no-priority': campaign.Campaign( typing.cast(mailjet_templates.Id, 'big-no-priority'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_big_focus=True, ), 'big-less-important': campaign.Campaign( typing.cast(mailjet_templates.Id, 'big-less-important'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_big_focus=True, ), }) @mock.patch.dict( mailjet_templates.MAP, { 'big-important': { 'mailjetTemplate': 0 }, 'small-very-important': { 'mailjetTemplate': 1 }, 'big-no-priority': { 'mailjetTemplate': 2 }, 'big-less-important': { 'mailjetTemplate': 3 }, }) @mock.patch( campaign.__name__ + '.get_campaign_subject', lambda campaign_id: { 'big-important': 'Un mail gros et important', 'small-very-important': 'Un mail petit et très important', 'big-no-priority': 'Un mail gros et pas important', 'big-less-important': 'Un mail gros et moins important', }[campaign_id]) @mock.patch('random.random') def test_send_priority(self, mock_random_random: mock.MagicMock) -> None: """Send priority focus emails first.""" # Avoid random influence in the order calculation mock_random_random.return_value = 0 self._db.test.focus_emails.drop() self._db.test.focus_emails.insert_many([ { 'campaignId': 'big-important', 'scoringModel': 'constant(2.5)' }, { 'campaignId': 'small-very-important', 'scoringModel': 'constant(3)' }, { 'campaignId': 'big-no-priority' }, { 'campaignId': 'big-less-important', 'scoringModel': 'constant(1)' }, ]) focus.main([ 'send', '--disable-sentry', '--restrict-campaigns', 'big-important', 'small-very-important', 'big-no-priority', 'big-less-important', ]) self.assertEqual(['*****@*****.**'], [ m.recipient['Email'] for m in mailjetmock.get_all_sent_messages() ]) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual(1, len(user_data.get('emailsSent'))) self.assertEqual('big-important', user_data['emailsSent'][0]['campaignId']) @mock.patch( focus.__name__ + '._FOCUS_CAMPAIGNS', { 'first-one': campaign.Campaign( typing.cast(mailjet_templates.Id, 'first-one'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_coaching=True, ), 'second-one': campaign.Campaign( typing.cast(mailjet_templates.Id, 'second-one'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_coaching=True, ), 'third-one': campaign.Campaign( typing.cast(mailjet_templates.Id, 'third-one'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_coaching=True, ), }) @mock.patch.dict( mailjet_templates.MAP, { 'first-one': { 'mailjetTemplate': 0 }, 'second-one': { 'mailjetTemplate': 0 }, 'third-one': { 'mailjetTemplate': 0 }, }) @mock.patch( campaign.__name__ + '.get_campaign_subject', lambda campaign_id: { 'first-one': 'Un premier email', 'second-one': 'Un deuxième email', 'third-one': 'Un troisième email', }[campaign_id]) @mock.patch(focus.report.__name__ + '.notify_slack') def test_slack(self, mock_notify_slack: mock.MagicMock) -> None: """Send message to slack.""" self._db.test.focus_emails.drop() self._db.test.focus_emails.insert_many([ { 'campaignId': 'first-one', 'scoringModel': 'constant(3)' }, { 'campaignId': 'third-one', 'scoringModel': 'constant(.1)' }, ]) # Note that random will not be flaky: # the diff score is (3 - .1) / 3 * _SCORES_WEIGHT = 5 * 2.9 / 3 # the max random diff is _RANDOM_WEIGHTS = 4 # So the difference in score will always be bigger than the random diff and thus first-one # will always be selected. focus.main([ 'send', '--disable-sentry', ]) self.assertEqual(['*****@*****.**'], [ m.recipient['Email'] for m in mailjetmock.get_all_sent_messages() ]) mock_notify_slack.assert_called_once_with( textwrap.dedent('''\ Focus emails sent today: • *first-one*: 1 email • *third-one*: 0 email''')) @mock.patch( focus.__name__ + '._FOCUS_CAMPAIGNS', { 'just-big': campaign.Campaign( typing.cast(mailjet_templates.Id, 'just-big'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_big_focus=True, ), 'small-very-important': campaign.Campaign( typing.cast(mailjet_templates.Id, 'small-very-important'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_big_focus=False, ), }) @mock.patch.dict( mailjet_templates.MAP, { 'just-big': { 'mailjetTemplate': 0 }, 'small-very-important': { 'mailjetTemplate': 1 }, }) @mock.patch( campaign.__name__ + '.get_campaign_subject', lambda campaign_id: { 'just-big': 'Un mail gros', 'small-very-important': 'Un mail petit et très important', }[campaign_id]) @mock.patch('random.random') def test_send_shuffle_random(self, mock_random_random: mock.MagicMock) -> None: """Test random in shuffle.""" mock_random_random.side_effect = real_random self._db.test.focus_emails.drop() self._db.test.focus_emails.insert_many([ { 'campaignId': 'just-big', 'scoringModel': 'constant(0.5)' }, { 'campaignId': 'small-very-important', 'scoringModel': 'constant(3)' }, ]) focus.main([ 'send', '--disable-sentry', '--restrict-campaigns', 'just-big', 'small-very-important', ]) mock_random_random.assert_called() self.assertEqual(['*****@*****.**'], [ m.recipient['Email'] for m in mailjetmock.get_all_sent_messages() ]) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual(1, len(user_data.get('emailsSent'))) self.assertIn(user_data['emailsSent'][0]['campaignId'], {'just-big', 'small-very-important'}) @mock.patch( focus.__name__ + '._FOCUS_CAMPAIGNS', { 'just-big': campaign.Campaign( typing.cast(mailjet_templates.Id, 'just-big'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_big_focus=True, ), 'small-very-important': campaign.Campaign( typing.cast(mailjet_templates.Id, 'small-very-important'), get_vars=lambda user, **unused_kwargs: {'key': 'value'}, sender_name='Sender', sender_email='*****@*****.**', is_big_focus=False, ), }) @mock.patch.dict( mailjet_templates.MAP, { 'just-big': { 'mailjetTemplate': 0 }, 'small-very-important': { 'mailjetTemplate': 1 }, }) @mock.patch( campaign.__name__ + '.get_campaign_subject', lambda campaign_id: { 'big-important': 'Un mail gros', 'small-very-important': 'Un mail petit et très important', }[campaign_id]) @mock.patch('random.random') def test_send_shuffle(self, mock_random_random: mock.MagicMock) -> None: """Send the mail with a better score first.""" mock_random_random.return_value = 0 self._db.test.focus_emails.drop() self._db.test.focus_emails.insert_many([ { 'campaignId': 'just-big', 'scoringModel': 'constant(0.5)' }, { 'campaignId': 'small-very-important', 'scoringModel': 'constant(3)' }, ]) focus.main([ 'send', '--disable-sentry', '--restrict-campaigns', 'just-big', 'small-very-important', ]) self.assertEqual(['*****@*****.**'], [ m.recipient['Email'] for m in mailjetmock.get_all_sent_messages() ]) user_data = self._db.user_test.user.find_one() assert user_data self.assertEqual(1, len(user_data.get('emailsSent'))) self.assertEqual('small-very-important', user_data['emailsSent'][0]['campaignId'])
campaign.get_deep_link_advice(user.user_id, project, 'fresh-resume') return campaign.get_default_coaching_email_vars(user) | { 'deepLinkAdviceUrl': deep_link_advice_url, 'hasExperience': has_experience, 'isSeptember': campaign.as_template_boolean(now.month == 9), 'loginUrl': campaign.create_logged_url(user.user_id) } campaign.register_campaign( campaign.Campaign( campaign_id='improve-cv', mongo_filters={ 'profile.frustrations': 'RESUME', 'projects': { '$elemMatch': { 'isIncomplete': { '$ne': True }, } }, }, get_vars=_get_improve_cv_vars, sender_name=i18n.make_translatable_string( "Joanna et l'équipe de {{var:productName}}"), sender_email='*****@*****.**', is_coaching=True, is_big_focus=True, ))
'productUrl': f'{login_url}?utm_source=bob-emploi&amp;utm_medium=email', } campaign.register_campaign( campaign.Campaign( campaign_id='get-diploma', mongo_filters={ 'projects': { '$elemMatch': { 'isIncomplete': { '$ne': True }, 'openedStrategies.strategyId': 'get-diploma', } }, }, get_vars=_get_find_diploma_vars, sender_name=i18n.make_translatable_string( "Joanna et l'équipe de {{var:productName}}"), sender_email='*****@*****.**', is_coaching=True, is_big_focus=True, )) campaign.register_campaign( campaign.Campaign( campaign_id='get-diploma-short', mongo_filters={}, get_vars=_get_short_diploma_vars,
'inCity': in_city, 'inTargetDomain': in_target_domain, 'isAbleBodied': campaign.as_template_boolean(not user.profile.has_handicap), 'isYoung': campaign.as_template_boolean(age <= max_young), 'jobGroupInDepartement': f'{job_group_name} {in_departement}', 'networkApplicationPercentage': network_application_importance, } campaign.register_campaign(campaign.Campaign( campaign_id='focus-network', mongo_filters={ 'projects': {'$elemMatch': { 'networkEstimate': 1, 'isIncomplete': {'$ne': True}, }}, }, get_vars=_get_network_vars, sender_name=i18n.make_translatable_string("Joanna et l'équipe de {{var:productName}}"), sender_email='*****@*****.**', is_coaching=True, is_big_focus=True, )) campaign.register_campaign(campaign.Campaign( campaign_id='network-plus', mongo_filters={ 'projects': {'$elemMatch': { 'networkEstimate': {'$gte': 2}, 'isIncomplete': {'$ne': True}, }}, },
goal = 'décrocher votre prochain emploi' return campaign.get_default_coaching_email_vars(user) | { 'goal': goal, 'numberUsers': '250\u00A0000', 'lastYear': str(now.year - 1), 'year': str(now.year), } campaign.register_campaign( campaign.Campaign( campaign_id='christmas', mongo_filters={}, get_vars=_christmas_vars, is_coaching=True, is_big_focus=True, sender_name=i18n.make_translatable_string( 'Joanna de {{var:productName}}'), sender_email='*****@*****.**', )) campaign.register_campaign( campaign.Campaign( campaign_id='new-year', mongo_filters={}, get_vars=_new_year_vars, sender_name=i18n.make_translatable_string( 'Joanna de {{var:productName}}'), sender_email='*****@*****.**', ))
""" import json import os import typing from typing import Any from bob_emploi.frontend.server import i18n from bob_emploi.frontend.server.mail import campaign def _get_mongo_filters() -> dict[str, Any]: """Get the mongo filters from env vars.""" filters_as_string = os.getenv('RESEARCH_TARGET_USERS', '') if not filters_as_string: return {} return typing.cast(dict[str, Any], json.loads(filters_as_string)) campaign.register_campaign( campaign.Campaign( campaign_id='bob-research-recruit', get_mongo_filters=_get_mongo_filters, get_vars=campaign.get_default_vars, sender_name=i18n.make_translatable_string( "Tabitha et l'équipe de {{var:productName}}"), sender_email='*****@*****.**', ))
return campaign.get_default_coaching_email_vars(user) # TODO(cyrille): Modularize. _CAMPAIGNS = [ campaign.Campaign( campaign_id='focus-self-develop', mongo_filters={ 'projects': { '$elemMatch': { 'jobSearchHasNotStarted': { '$ne': True }, 'isIncomplete': { '$ne': True }, } } }, get_vars=_get_self_development_vars, sender_name=i18n.make_translatable_string( "Joanna et l'équipe de {{var:productName}}"), sender_email='*****@*****.**', is_coaching=True, is_big_focus=True, ), campaign.Campaign( campaign_id='focus-body-language', mongo_filters={ 'projects': { '$elemMatch': {
def _get_switch_vars(user: user_pb2.User, *, now: datetime.datetime, **unused_kwargs: Any) -> dict[str, str]: """Compute all variables required for the Switch campaign.""" if now.year - user.profile.year_of_birth < 22: raise campaign.DoNotSend('User is too young') project = next((p for p in user.projects), project_pb2.Project()) if project.seniority <= project_pb2.INTERMEDIARY: raise campaign.DoNotSend("User doesn't have enough experience") return campaign.get_default_coaching_email_vars(user) | { 'isConverting': campaign.as_template_boolean( project.kind == project_pb2.REORIENTATION), } campaign.register_campaign( campaign.Campaign( campaign_id='switch-grant', mongo_filters={}, is_coaching=True, get_vars=_get_switch_vars, sender_name=i18n.make_translatable_string( 'Joanna de {{var:productName}}'), sender_email='*****@*****.**', ))
'gender': user_profile_pb2.Gender.Name( user.profile.gender), 'mainChallenge': main_challenge_id, }), }), } _FFS_CAMPAIGN = campaign.Campaign( campaign_id=_CAMPAIGN_ID, mongo_filters={ 'emailsSent': { '$not': { '$elemMatch': { 'campaignId': _CAMPAIGN_ID } } }, # Don't bug very old users, they wouldn't pass the date check anyway. 'registeredAt': { '$gt': '2022-02-01' }, }, get_vars=_get_ffs_vars, sender_name=i18n.make_translatable_string( 'Tabitha de {{var:productName}}'), sender_email='*****@*****.**', ) campaign.register_campaign(_FFS_CAMPAIGN)
campaign.register_campaign( campaign.Campaign( campaign_id=_CAMPAIGN_ID, mongo_filters={ 'projects': { '$elemMatch': { 'jobSearchHasNotStarted': { '$ne': True }, 'isIncomplete': { '$ne': True }, 'actions.0': { '$exists': True }, } }, 'emailsSent': { '$not': { '$elemMatch': { 'campaignId': _CAMPAIGN_ID } } }, }, get_vars=_get_vars, sender_name=i18n.make_translatable_string( "{{var:firstTeamMember}} et l'équipe de {{var:productName}}"), sender_email='*****@*****.**', ))
from bob_emploi.frontend.server import proto from bob_emploi.frontend.server.mail import campaign from bob_emploi.frontend.server.mail import mail_send _TWO_YEARS_AGO_STRING = \ proto.datetime_to_json_string(datetime.datetime.now() - datetime.timedelta(730)) def _account_deletion_notice_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, str]: return dict( campaign.get_default_vars(user), loginUrl=campaign.create_logged_url(user.user_id)) campaign.register_campaign(campaign.Campaign( campaign_id='account-deletion-notice', mongo_filters={ # User hasn't been on Bob for two years. 'registeredAt': {'$lt': _TWO_YEARS_AGO_STRING}, 'requestedByUserAtDate': {'$not': {'$gt': _TWO_YEARS_AGO_STRING}}, # User hasn't read any email we sent to them in the last two years. 'emailsSent': {'$not': {'$elemMatch': { 'sentAt': {'$gt': _TWO_YEARS_AGO_STRING}, 'status': {'$in': list(mail_send.READ_EMAIL_STATUS_STRINGS)}, }}}, }, get_vars=_account_deletion_notice_vars, sender_name=i18n.make_translatable_string("Joanna et l'équipe de {{var:productName}}"), sender_email='*****@*****.**', ))
return campaign.get_default_coaching_email_vars(user) | { 'applicationModes': _make_section(application_modes_section), 'departements': _make_section(departements_section), 'employmentType': _make_section(employment_types_section), 'imtLink': imt_link, 'inCity': scoring_project.populate_template('%inCity'), 'jobNameInDepartement': job_name_in_departement, 'loginUrl': campaign.create_logged_url(user.user_id), 'marketStress': _make_section(market_stress_section), 'months': _make_section(months_section), 'ofJobNameInDepartement': of_job_name_in_departement, 'ofJobName': of_job_name, } campaign.register_campaign(campaign.Campaign( campaign_id='imt', mongo_filters={ 'projects': { '$elemMatch': { 'isIncomplete': {'$ne': True}, }, }, }, get_vars=_get_imt_vars, sender_name=i18n.make_translatable_string("Pascal et l'équipe de {{var:productName}}"), sender_email='*****@*****.**', is_coaching=True, is_big_focus=True, ))