def _match_filters_for_use_case( filters: Iterable[str], use_case: use_case_pb2.UseCase) -> bool: user = use_case.user_data if not user.projects: return False scoring_project = scoring.ScoringProject( user.projects[0], user, _get_eval_db()) return scoring_project.check_filters(filters, force_exists=True)
def get_scoring_project(user_id: str, project_id: str) -> scoring.ScoringProject: """Get the scoring project or abort.""" user_proto = get_user_data(user_id) project = get_project_data(user_proto, project_id) return scoring.ScoringProject(project, user_proto, mongo.get_connections_from_env().stats_db, now=now.get())
def scoring_project( self, database: pymongo.database.Database, now: Optional[datetime.datetime] = None) -> scoring.ScoringProject: """Creates a new scoring.ScoringProject for this persona.""" return scoring.ScoringProject(project=self.project, user=self._user, database=database, now=now)
def test_string_representation(self) -> None: """A scoring project can be represented as a meaningful string.""" user = user_pb2.User() user.profile.gender = user_pb2.MASCULINE user.features_enabled.alpha = True project = project_pb2.Project(title='Developpeur web a Lyon') project_str = str(scoring.ScoringProject(project, user, None)) self.assertIn(str(user.profile), project_str) self.assertIn(str(project), project_str) self.assertIn(str(user.features_enabled), project_str)
def test_personal_string(self) -> None: """A scoring model string does not show personal identifiers.""" user = user_pb2.User() user.profile.name = 'Cyrille' user.features_enabled.alpha = True project = project_pb2.Project(project_id='secret-project') project_str = str(scoring.ScoringProject(project, user, None)) self.assertNotIn('Cyrille', project_str) self.assertNotIn('secret-project', project_str) self.assertEqual('Cyrille', user.profile.name) self.assertEqual('secret-project', project.project_id)
def _get_short_spontaneous_vars(user: user_pb2.User, *, now: datetime.datetime, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, str]: if not user.projects: raise scoring.NotEnoughDataException('No project yet', {'projects.0'}) project = user.projects[0] scoring_project = scoring.ScoringProject(project, user, database, now) job_group_info = scoring_project.job_group_info() why_specific_company = job_group_info.why_specific_company if not why_specific_company: why_specific_company = scoring_project.translate_static_string( 'vous vous reconnaissez dans leurs valeurs, leur équipe, leur service client ou ce ' "qu'elles vendent") some_companies = job_group_info.place_plural if not some_companies: some_companies = scoring_project.translate_static_string( 'des entreprises') if (user.profile.locale or 'fr').startswith('fr'): advice_page_url = 'https://labonneboite.pole-emploi.fr/comment-faire-une-candidature-spontanee' elif user.profile.locale.startswith('en'): advice_page_url = 'https://www.theguardian.com/careers/speculative-applications' else: logging.warning( 'No advice webpage given for campaign spontaneous-short in "%s"', user.profile.locale) advice_page_url = '' # If the user receives the email less than 2 months after they registered on Bob and are # searching for less than 3 months, we can be happily surprised if they found a job. is_job_found_surprising = scoring_project.get_search_length_now() < 3 and \ (scoring_project.details.created_at.ToDatetime() - now).days / 30 < 2 return campaign.get_default_coaching_email_vars(user) | { 'advicePageUrl': advice_page_url, 'atVariousCompanies': job_group_info.at_various_companies, 'isJobFoundSurprising': campaign.as_template_boolean(is_job_found_surprising), 'someCompanies': some_companies, 'whySpecificCompany': why_specific_company, }
class FilterUsingScoreTestCase(unittest.TestCase): """Unit tests for the filter_using_score function.""" dummy_project = scoring.ScoringProject(project_pb2.Project(), user_pb2.User(), mongomock.MongoClient().test) @classmethod def setUpClass(cls) -> None: """Test setup.""" super().setUpClass() scoring.SCORING_MODELS['test-zero'] = scoring.ConstantScoreModel('0') scoring.SCORING_MODELS['test-two'] = scoring.ConstantScoreModel('2') def test_filter_list_with_no_filters(self) -> None: """Filter a list with no filters to apply.""" filtered = scoring.filter_using_score(range(5), lambda a: [], self.dummy_project) self.assertEqual([0, 1, 2, 3, 4], list(filtered)) def test_filter_list_constant_scorer(self) -> None: """Filter a list returning constant scorer.""" get_scoring_func = mock.MagicMock() get_scoring_func.side_effect = [['test-zero'], ['test-two'], ['test-zero']] filtered = scoring.filter_using_score(range(3), get_scoring_func, self.dummy_project) self.assertEqual([1], list(filtered)) def test_unknown_filter(self) -> None: """Filter an item with an unknown filter.""" get_scoring_func = mock.MagicMock() get_scoring_func.return_value = ['unknown-filter'] filtered = scoring.filter_using_score([42], get_scoring_func, self.dummy_project) self.assertEqual([42], list(filtered)) def test_multiple_filters(self) -> None: """Filter an item with multiple filters.""" get_scoring_func = mock.MagicMock() get_scoring_func.return_value = ['test-two', 'test-zero'] filtered = scoring.filter_using_score([42], get_scoring_func, self.dummy_project) self.assertEqual([], list(filtered))
def _employment_vars( user: user_pb2.User, *, now: datetime.datetime, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any, ) -> dict[str, str]: """Computes vars for a given user for the employment survey. Returns a dict with all vars required for the template. """ num_months_ago = round((now - user.registered_at.ToDatetime()).days / 30.5) if num_months_ago <= 0 and not user.features_enabled.alpha: raise campaign.DoNotSend( f'User registered only recently ({user.registered_at})') scoring_project = scoring.ScoringProject(project_pb2.Project(), user, database) registered_since = scoring_project.get_several_months_text(num_months_ago) for status in user.employment_status: if status.created_at.ToDatetime() > _ONE_MONTH_AGO: raise campaign.DoNotSend( 'User has already updated their employment status less than one month ago.' ) base_params = { 'user': user.user_id, 'token': parse.quote( auth_token.create_token(user.user_id, role='employment-status')), } return campaign.get_default_vars(user) | { 'registeredSince': registered_since, 'seekingUrl': campaign.get_bob_link( '/api/employment-status', base_params | { 'seeking': user_pb2.SeekingStatus.Name(user_pb2.STILL_SEEKING), 'redirect': campaign.get_bob_link('/statut/en-recherche'), }), 'stopSeekingUrl': campaign.get_bob_link( '/api/employment-status', base_params | { 'seeking': user_pb2.SeekingStatus.Name(user_pb2.STOP_SEEKING), 'redirect': campaign.get_bob_link('/statut/ne-recherche-plus'), }), }
def _get_expanded_card_data(user_proto, project, advice_id): module = advisor.get_advice_module(advice_id, _DB) if not module or not module.trigger_scoring_model: flask.abort(404, 'Le module "{}" n\'existe pas'.format(advice_id)) model = scoring.get_scoring_model(module.trigger_scoring_model) if not model or not hasattr(model, 'get_expanded_card_data'): flask.abort( 404, 'Le module "{}" n\'a pas de données supplémentaires'.format( advice_id)) scoring_project = scoring.ScoringProject(project, user_proto.profile, user_proto.features_enabled, _DB, now=now.get()) return model.get_expanded_card_data(scoring_project)
def get_sections_for_project(user_proto: user_pb2.User) -> upskilling_pb2.Sections: """Return all the sections to browse.""" if not user_proto.projects: flask.abort(422, i18n.flask_translate("Il n'y a pas de projet à explorer.")) project = user_proto.projects[0] database = mongo.get_connections_from_env().stats_db scoring_project = scoring.ScoringProject(project, user_proto, database) result = upskilling_pb2.Sections() good_jobs = jobs.get_all_good_job_group_ids(scoring_project.database) best_salaries = { job.job_group.rome_id for job in _get_best_jobs_in_area(scoring_project).best_salaries_jobs} slots = list(_SECTION_SLOTS.get_collection(database)) are_all_jobs_hiring = _get_are_all_jobs_hiring() for section in slots: if section.is_for_alpha_only and not user_proto.features_enabled.alpha: continue generator_id = section.generator try: generator = _SECTION_GENERATORS[generator_id] except KeyError: logging.error('Unknown upskilling section generator "%s"', generator_id) continue computed_section = generator.get_jobs( scoring_project=scoring_project, allowed_job_ids=good_jobs, previous_sections={ section.id for section in result.sections if section.state.startswith(f'{generator_id}:') }) if not computed_section or len(computed_section.jobs) < 2: continue result.sections.add( id=computed_section.new_id or generator_id, state=f'{generator_id}:{computed_section.state or ""}', name=scoring_project.populate_template(scoring_project.translate_key_string( f'jobflix_sections:{computed_section.new_id or generator_id}', hint=computed_section.new_name or generator.name, context=_get_bob_deployment(), is_hint_static=True)), jobs=[ _add_perks_to_job(job, best_salaries, is_hiring=are_all_jobs_hiring) for job in computed_section.jobs], ) return result
def _send_activation_email(user: user_pb2.User, project: project_pb2.Project, database: pymongo_database.Database, base_url: str) -> None: """Send an email to the user just after we have defined their diagnosis.""" if '@' not in user.profile.email: return # Set locale. locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8') scoring_project = scoring.ScoringProject(project, user, database, now=now.get()) auth_token = parse.quote( auth.create_token(user.user_id, is_using_timestamp=True)) settings_token = parse.quote( auth.create_token(user.user_id, role='settings')) coaching_email_frequency_name = \ user_pb2.EmailFrequency.Name(user.profile.coaching_email_frequency) data = { 'changeEmailSettingsUrl': f'{base_url}/unsubscribe.html?user={user.user_id}&auth={settings_token}&' f'coachingEmailFrequency={coaching_email_frequency_name}&' f'hl={parse.quote(user.profile.locale)}', 'date': now.get().strftime('%d %B %Y'), 'firstName': user.profile.name, 'gender': user_pb2.Gender.Name(user.profile.gender), 'isCoachingEnabled': 'True' if user.profile.coaching_email_frequency and user.profile.coaching_email_frequency != user_pb2.EMAIL_NONE else '', 'loginUrl': f'{base_url}?userId={user.user_id}&authToken={auth_token}', 'ofJob': scoring_project.populate_template('%ofJobName', raise_on_missing_var=True), } # https://app.mailjet.com/template/636862/build response = mail.send_template('636862', user.profile, data) if response.status_code != 200: logging.warning('Error while sending diagnostic email: %s\n%s', response.status_code, response.text)
def _maybe_recommend_advice(user: user_pb2.User, project: project_pb2.Project, database: mongo.NoPiiMongoDatabase) -> bool: if user.features_enabled.advisor == features_pb2.CONTROL: return False scoring_project = scoring.ScoringProject(project, user, database, now=now.get()) if user.features_enabled.action_plan == features_pb2.ACTIVE and not project.actions: compute_actions_for_project(scoring_project) if project.advices: return False advices = compute_advices_for_project(scoring_project) for piece_of_advice in advices.advices: piece_of_advice.status = project_pb2.ADVICE_RECOMMENDED project.advices.extend(advices.advices[:]) return True
def test_jobgroup_info_fr_locale(self) -> None: """A scoring project can be represented as a meaningful string.""" user = user_pb2.User() user.profile.locale = '' database = mongomock.MongoClient().test database.job_group_info.insert_many([{ '_id': 'A1234', 'name': 'french' }, { '_id': 'nl:A1234', 'name': 'dutch' }]) project = project_pb2.Project(target_job=job_pb2.Job( job_group=job_pb2.JobGroup(rome_id='A1234'))) scoring_project = scoring.ScoringProject(project, user, database) job_group_info = scoring_project.job_group_info() self.assertEqual('french', job_group_info.name)
def strategize( user: user_pb2.User, project: project_pb2.Project, database: pymongo.database.Database) \ -> None: """Make strategies for the user.""" scoring_project = scoring.ScoringProject(project, user, database) advice_scores = {a.advice_id: a.num_stars for a in project.advices} category_modules = _get_strategy_modules_by_category(database).get( project.diagnostic.category_id, []) for module in category_modules: _make_strategy(scoring_project, module, advice_scores) scoring_project.details.strategies.sort(key=lambda s: -s.score) if not scoring_project.details.strategies: if category_modules: logging.warning( 'We could not find *any* strategy for a project:\n%s', scoring_project) return scoring_project.details.strategies[0].is_principal = True
def _create_mock_scoring_project() -> scoring.ScoringProject: """Create a mock scoring_project.""" _db = mongo.NoPiiMongoDatabase(mongomock.MongoClient().test) _db.job_group_info.insert_one({ '_id': 'A1234', 'requirements': { 'diplomas': [{ 'name': 'Bac+2' }], }, }) user = user_pb2.User() project = project_pb2.Project() project.target_job.job_group.rome_id = 'A1234' project.created_at.FromDatetime(datetime.datetime.now()) project.job_search_started_at.FromDatetime(datetime.datetime.now() - datetime.timedelta(days=30)) return scoring.ScoringProject(project, user, database=_db)
def get_more_jobs( user_proto: user_pb2.User, *, section_id: str, state: str) -> upskilling_pb2.Section: """Return more jobs for a given section.""" if not user_proto.projects: flask.abort(422, i18n.flask_translate("Il n'y a pas de projet à explorer.")) try: generator_id, section_state = state.split(':', 1) except ValueError: flask.abort( 422, i18n.flask_translate("Le paramètre d'état {state} n'a pas le bon format.") .format(state=state)) project = user_proto.projects[0] database = mongo.get_connections_from_env().stats_db scoring_project = scoring.ScoringProject(project, user_proto, database) try: generator = _SECTION_GENERATORS[generator_id] except KeyError: flask.abort( 404, i18n.flask_translate('Générateur de section inconnu: {generator_id}') .format(generator_id=generator_id)) try: section = generator.get_more_jobs( scoring_project=scoring_project, section_id=section_id, state=section_state) except _InvalidState: flask.abort( 422, i18n.flask_translate('Impossible de commencer à {start_from}') .format(start_from=section_state)) best_jobs_in_area = _get_best_jobs_in_area(scoring_project) are_all_jobs_hiring = _get_are_all_jobs_hiring() best_salaries = { job.job_group.rome_id for job in best_jobs_in_area.best_salaries_jobs} for job in section.jobs: _add_perks_to_job(job, best_salaries, is_hiring=are_all_jobs_hiring) return section
def _get_find_diploma_vars(user: user_pb2.User, *, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, Any]: """Compute vars for the "Prepare your application" email.""" if not user.projects: raise scoring.NotEnoughDataException('No project yet', {'projects.0'}) project = user.projects[0] scoring_project = scoring.ScoringProject(project, user, database) if not any(s.strategy_id == 'get-diploma' for s in project.opened_strategies): raise campaign.DoNotSend( 'The user has not started a strategy to get a diploma') if not project.target_job.job_group.rome_id: raise scoring.NotEnoughDataException( 'Need a job group to find trainings', # TODO(pascal): Use project_id instead of 0. {'projects.0.targetJob.jobGroup.romeId'}) trainings = scoring_project.get_trainings()[:3] deep_link_training_url = \ campaign.get_deep_link_advice(user.user_id, project, 'training') return campaign.get_default_coaching_email_vars(user) | { 'deepTrainingAdviceUrl': deep_link_training_url, 'inDepartement': scoring_project.populate_template('%inDepartement'), 'loginUrl': campaign.create_logged_url(user.user_id, f'/projet/{project.project_id}'), 'numTrainings': len(trainings), 'ofJobName': scoring_project.populate_template('%ofJobName'), 'trainings': [json_format.MessageToDict(t) for t in trainings], }
def diagnose(user, project, database): """Diagnose a project. Args: user: the user's data, mainly used for their profile and features_enabled. project: the project data. It will be modified, as its diagnostic field will be populated. database: access to the MongoDB with market data. diagnostic: a protobuf to fill, if none it will be created. Returns: the modified diagnostic protobuf. """ diagnostic = project.diagnostic scoring_project = scoring.ScoringProject(project, user.profile, user.features_enabled, database, now=now.get()) return diagnose_scoring_project(scoring_project, diagnostic)
def diagnose( user: user_pb2.User, project: project_pb2.Project, database: mongo.NoPiiMongoDatabase) \ -> diagnostic_pb2.Diagnostic: """Diagnose a project. Args: user: the user's data, mainly used for their profile and features_enabled. project: the project data. It will be modified, as its diagnostic field will be populated. database: access to the MongoDB with market data. Returns: the modified diagnostic protobuf. """ diagnostic = project.diagnostic scoring_project = scoring.ScoringProject(project, user, database, now=now.get()) return diagnose_scoring_project(scoring_project, diagnostic)
def list_all_tips( user: user_pb2.User, project: project_pb2.Project, piece_of_advice: project_pb2.Advice, database: pymongo_database.Database ) -> List[action_pb2.ActionTemplate]: """List all available tips for a piece of advice. Args: user: the full user info. project: the project to give tips for. piece_of_advice: the piece of advice to give tips for. database: access to the database to get modules and tips. Returns: An iterable of tips for this module. """ try: module = next(m for m in _advice_modules(database) if m.advice_id == piece_of_advice.advice_id) except StopIteration: logging.warning('Advice module %s does not exist anymore', piece_of_advice.advice_id) return [] # Get tip templates. all_tip_templates = _tip_templates(database) tip_templates = filter(None, (all_tip_templates.get(t) for t in module.tip_template_ids)) # Filter tips. scoring_project = scoring.ScoringProject(project, user, database, now=now.get()) filtered_tips = scoring.filter_using_score(tip_templates, lambda t: t.filters, scoring_project) return [_translate_tip(tip, scoring_project) for tip in filtered_tips]
def _get_post_covid_vars(user: user_pb2.User, *, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, str]: if not user.projects: raise scoring.NotEnoughDataException( 'Project is required.', fields={'user.projects.0.advices'}) project = user.projects[0] scoring_project = scoring.ScoringProject(project, user, database) if scoring_project.job_group_info().covid_risk != job_pb2.COVID_RISKY: raise campaign.DoNotSend("The user's project job is not covid risky.") try: network_advice_link = next( campaign.get_deep_link_advice(user.user_id, project, a.advice_id) for a in project.advices if a.advice_id.startswith('network-application')) except StopIteration: raise campaign.DoNotSend('No network-application advice found for the user.')\ from None return campaign.get_default_coaching_email_vars(user) | { 'deepLinkAdviceUrl': network_advice_link, 'ofJobName': scoring_project.populate_template('%ofJobName'), }
def diagnose( user: user_pb2.User, project: project_pb2.Project, database: pymongo.database.Database) \ -> Tuple[diagnostic_pb2.Diagnostic, Optional[List[int]]]: """Diagnose a project. Args: user: the user's data, mainly used for their profile and features_enabled. project: the project data. It will be modified, as its diagnostic field will be populated. database: access to the MongoDB with market data. diagnostic: a protobuf to fill, if none it will be created. Returns: the modified diagnostic protobuf. """ diagnostic = project.diagnostic scoring_project = scoring.ScoringProject(project, user, database, now=now.get()) return diagnose_scoring_project(scoring_project, diagnostic)
def setUp(self): super(PopulateProjectTemplateTest, self).setUp() # Pre-populate project's fields that are usualldy set. Individual tests # should not count on those values. self.project = project_pb2.Project() self.project.target_job.name = 'Boulanger / Boulangère' self.project.target_job.masculine_name = 'Boulanger' self.project.target_job.feminine_name = 'Boulangère' self.project.target_job.job_group.rome_id = 'Z9007' self.project.mobility.city.city_id = '69123' self.project.mobility.city.departement_id = '69' self.project.mobility.city.region_id = '84' self.project.mobility.city.postcodes = '69001-69002-69003-69004' self.project.mobility.city.name = 'Lyon' self.database = mongomock.MongoClient().test self.database.regions.insert_one({ '_id': '84', 'prefix': 'en ', 'name': 'Auvergne-Rhône-Alpes', }) self.scoring_project = scoring.ScoringProject( self.project, user_pb2.UserProfile(), user_pb2.Features(), self.database)
def _get_jobbing_vars( user: user_pb2.User, *, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, Any]: """Compute vars for the "Jobbing" email.""" if not user.projects: raise scoring.NotEnoughDataException('No project yet', {'projects.0'}) project = user.projects[0] if not any(s.strategy_id == 'diploma-free-job' for s in project.opened_strategies): raise campaign.DoNotSend( 'The user has not started a strategy to get a job without a diploma') scoring_project = scoring.ScoringProject(project, user, database) model = scoring.get_scoring_model('advice-reorient-jobbing') if not model: raise campaign.DoNotSend('The advice-reorient-jobbing model is not implemented') reorient_jobs = typing.cast( reorient_jobbing_pb2.JobbingReorientJobs, model.get_expanded_card_data(scoring_project), ).reorient_jobbing_jobs if not reorient_jobs: raise campaign.DoNotSend("We didn't find any jobbing jobs to reorient to for the user") if project.target_job.name: of_job_name = scoring_project.populate_template('%ofJobName') else: # This is not translated to fr@tu because the email templates are only in fr for now. of_job_name = '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], 'loginUrl': campaign.create_logged_url(user.user_id, f'/projet/{project.project_id}'), 'ofJobName': of_job_name, }
def _get_jobbing_vars(user: user_pb2.User, database: Optional[pymongo.database.Database] = None, **unused_kwargs: Any) -> Optional[Dict[str, Any]]: """Compute vars for the "Jobbing" email.""" project = user.projects[0] if not any(s.strategy_id == 'diploma-free-job' for s in project.opened_strategies): return None assert database scoring_project = scoring.ScoringProject(project, user, database) model = scoring.get_scoring_model('advice-reorient-jobbing') if not model: return None reorient_jobs = typing.cast( reorient_jobbing_pb2.JobbingReorientJobs, model.get_expanded_card_data(scoring_project), ).reorient_jobbing_jobs if not reorient_jobs: return None return dict( campaign.get_default_coaching_email_vars(user), **{ 'inDepartement': scoring_project.populate_template('%inDepartement'), 'jobs': [{ 'name': job.name } for job in reorient_jobs], 'loginUrl': campaign.create_logged_url(user.user_id, f'/projet/{project.project_id}'), 'ofJobName': scoring_project.populate_template('%ofJobName'), })
def strategize( user: user_pb2.User, project: project_pb2.Project, database: mongo.NoPiiMongoDatabase) \ -> None: """Make strategies for the user.""" scoring_project = scoring.ScoringProject(project, user, database) advice_scores = {a.advice_id: a.num_stars for a in project.advices} category_modules = _get_strategy_modules_by_category(database).get( project.diagnostic.category_id, []) for module in category_modules: _make_strategy(scoring_project, module, advice_scores) scoring_project.details.strategies.sort(key=lambda s: -s.score) if not scoring_project.details.strategies: if category_modules: logging.error( 'We could not find *any* strategy for a project:\n' 'Existing strategies: %s\n' "User's advice modules: %s\n" '%s', ', '.join(strategy.strategy_id for strategy in category_modules), ', '.join(advice.advice_id for advice in project.advices), str(scoring_project)) return scoring_project.details.strategies[0].is_principal = True
def send_focus_email_to_user( action: 'campaign.Action', user: user_pb2.User, *, dry_run_email: Optional[str] = None, database: mongo.NoPiiMongoDatabase, users_database: Optional[mongo.UsersDatabase] = None, eval_database: Optional[mongo.NoPiiMongoDatabase] = None, instant: datetime.datetime, restricted_campaigns: Optional[Set[mailjet_templates.Id]] = None) \ -> Optional[mailjet_templates.Id]: """Try to send a focus email to the user and returns the campaign ID.""" if user.profile.coaching_email_frequency == email_pb2.EMAIL_NONE: return None if not user.HasField('send_coaching_email_after'): send_coaching_email_after = _compute_next_coaching_email_date(user) if send_coaching_email_after > instant: user.send_coaching_email_after.FromDatetime(send_coaching_email_after) if user.user_id and action != 'ghost' and users_database: users_database.user.update_one( {'_id': objectid.ObjectId(user.user_id)}, {'$set': { 'sendCoachingEmailAfter': proto.datetime_to_json_string( send_coaching_email_after, ), }}) return None # Compute next send_coaching_email_after. next_send_coaching_email_after = instant + _compute_duration_to_next_coaching_email(user) focus_emails_sent: Set[mailjet_templates.Id] = set() last_focus_email_sent = None for email_sent in user.emails_sent: if email_sent.campaign_id not in _FOCUS_CAMPAIGNS: continue last_focus_email_sent = email_sent focus_emails_sent.add(typing.cast(mailjet_templates.Id, email_sent.campaign_id)) project = scoring.ScoringProject( user.projects[0] if user.projects else project_pb2.Project(), user, database, instant, ) possible_campaigns = get_possible_campaigns(database, restricted_campaigns) campaigns_scores = { campaign_id: ( project.score(possible_campaign.scoring_model) if possible_campaign.scoring_model else 2 ) for campaign_id, possible_campaign in possible_campaigns.items() } focus_emails_project_score_zero = { campaign for campaign, score in campaigns_scores.items() if not score } potential_campaigns = _shuffle( possible_campaigns.keys() - focus_emails_sent - focus_emails_project_score_zero, last_focus_email_sent, campaigns_scores, user.profile.coaching_email_frequency) for campaign_id in potential_campaigns: if _FOCUS_CAMPAIGNS[campaign_id].send_mail( user, database=database, users_database=users_database, eval_database=eval_database, action=action, dry_run_email=dry_run_email, mongo_user_update={'$set': { 'sendCoachingEmailAfter': proto.datetime_to_json_string( next_send_coaching_email_after, ), }}, now=instant): user.send_coaching_email_after.FromDatetime(next_send_coaching_email_after) return campaign_id # No focus email was supported: it seems that we have sent all the # ones we had. However maybe in the future we'll add more focus # emails so let's wait the same amount of time we have waited until # this email (this makes to wait 1 period, then 2, 4, …). last_coaching_email_sent_at = \ _compute_last_coaching_email_date(user, user.registered_at.ToDatetime()) send_coaching_email_after = instant + (instant - last_coaching_email_sent_at) user.send_coaching_email_after.FromDatetime(send_coaching_email_after) if user.user_id and action != 'ghost' and users_database: logging.debug('No more available focus email for "%s"', user.user_id) users_database.user.update_one({'_id': objectid.ObjectId(user.user_id)}, {'$set': { 'sendCoachingEmailAfter': proto.datetime_to_json_string(send_coaching_email_after), }}) return None
def _get_spontaneous_vars(user: user_pb2.User, *, now: datetime.datetime, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, str]: """Computes vars for a given user for the spontaneous email. Returns a dict with all vars required for the template. """ if not user.projects: raise scoring.NotEnoughDataException('No project yet', {'projects.0'}) project = user.projects[0] scoring_project = scoring.ScoringProject(project, user, database, now) job_search_length = scoring_project.get_search_length_now() if job_search_length < 0: raise campaign.DoNotSend('No info on user search duration') rome_id = project.target_job.job_group.rome_id if not rome_id: raise campaign.DoNotSend('User has no target job yet') job_group_info = scoring_project.job_group_info() if not job_group_info.rome_id: raise scoring.NotEnoughDataException( 'Requires job group info to check if spontaneous application is a good channel.', fields={'projects.0.targetJob.jobGroup.romeId'}) application_modes = job_group_info.application_modes if not application_modes: raise scoring.NotEnoughDataException( 'Requires application modes to check if spontaneous application is a good channel.', fields={f'data.job_group_info.{rome_id}.application_modes'}) def _should_use_spontaneous( modes: job_pb2.RecruitingModesDistribution) -> bool: return any(mode.mode == job_pb2.SPONTANEOUS_APPLICATION and mode.percentage > 20 for mode in modes.modes) if not any( _should_use_spontaneous(modes) for modes in application_modes.values()): raise campaign.DoNotSend( "Spontaneous isn't bigger than 20% of interesting channels.") contact_mode = job_group_info.preferred_application_medium if not contact_mode: raise scoring.NotEnoughDataException( 'Contact mode is required to push people to apply spontaneously', fields={ f'data.job_group_info.{rome_id}.preferred_application_medium' }) in_a_workplace = job_group_info.in_a_workplace if not in_a_workplace and contact_mode != job_pb2.APPLY_BY_EMAIL: raise scoring.NotEnoughDataException( 'To apply in person, the %inAWorkplace template is required', fields={f'data.job_group_info.{rome_id}.in_a_workplace'}) like_your_workplace = job_group_info.like_your_workplace if in_a_workplace and not like_your_workplace: raise scoring.NotEnoughDataException( 'The template %likeYourWorkplace is required', fields={f'data.job_group_info.{rome_id}.like_your_workplace'}) to_the_workplace = job_group_info.to_the_workplace if not to_the_workplace: to_the_workplace = scoring_project.translate_static_string( "à l'entreprise") some_companies = job_group_info.place_plural if not some_companies: some_companies = scoring_project.translate_static_string( 'des entreprises') what_i_love_about = scoring_project.translate_string( job_group_info.what_i_love_about, is_genderized=True) # TODO(cyrille): Drop this behaviour once phrases are translated with gender. if user.profile.gender == user_profile_pb2.FEMININE: what_i_love_about_feminine = job_group_info.what_i_love_about_feminine if what_i_love_about_feminine: what_i_love_about = what_i_love_about_feminine if not what_i_love_about and contact_mode == job_pb2.APPLY_BY_EMAIL: raise scoring.NotEnoughDataException( 'An example about "What I love about" a company is required', fields={f'data.job_group_info.{rome_id}.what_i_love_about'}) why_specific_company = job_group_info.why_specific_company if not why_specific_company: raise scoring.NotEnoughDataException( 'An example about "Why this specific company" is required', fields={f'data.job_group_info.{rome_id}.why_specific_company'}) at_various_companies = job_group_info.at_various_companies if project.weekly_applications_estimate == project_pb2.SOME: weekly_applications_count = '5' elif project.weekly_applications_estimate > project_pb2.SOME: weekly_applications_count = '15' else: weekly_applications_count = '' if project.weekly_applications_estimate: weekly_applications_option = project_pb2.NumberOfferEstimateOption.Name( project.weekly_applications_estimate) else: weekly_applications_option = '' return campaign.get_default_coaching_email_vars(user) | { 'applicationComplexity': job_pb2.ApplicationProcessComplexity.Name( job_group_info.application_complexity), 'atVariousCompanies': at_various_companies, 'contactMode': job_pb2.ApplicationMedium.Name(contact_mode).replace('APPLY_', ''), 'deepLinkLBB': f'https://labonneboite.pole-emploi.fr/entreprises/commune/{project.city.city_id}/rome/' f'{project.target_job.job_group.rome_id}?utm_medium=web&utm_source=bob&' 'utm_campaign=bob-email', 'emailInUrl': parse.quote(user.profile.email), 'experienceAsText': _EXPERIENCE_AS_TEXT.get(project.seniority, 'peu'), 'inWorkPlace': in_a_workplace, 'jobName': french.lower_first_letter( french.genderize_job(project.target_job, user.profile.gender)), 'lastName': user.profile.last_name, 'likeYourWorkplace': like_your_workplace, 'someCompanies': some_companies, 'toTheWorkplace': to_the_workplace, 'weeklyApplicationsCount': weekly_applications_count, 'weeklyApplicationsOption': weekly_applications_option, 'whatILoveAbout': what_i_love_about, 'whySpecificCompany': why_specific_company, }
def _get_imt_vars(user: user_pb2.User, database: Optional[pymongo.database.Database] = None, **unused_kwargs: Any) -> Optional[Dict[str, Any]]: """Compute vars for the "IMT" email.""" project = user.projects[0] assert database scoring_project = scoring.ScoringProject(project, user, database) genderized_job_name = french.lower_first_letter( french.genderize_job(project.target_job, user.profile.gender)) departement_id = project.city.departement_id rome_id = project.target_job.job_group.rome_id local_diagnosis = scoring_project.local_diagnosis() if not local_diagnosis.HasField('imt'): logging.info('User market has no IMT data') return None imt = local_diagnosis.imt shown_sections = [] market_stress_section = _make_market_stress_section( imt.yearly_avg_offers_per_10_candidates) if market_stress_section: shown_sections.append('marketStress') application_modes_section = _make_application_mode_section( scoring_project.get_best_application_mode(), project, user.user_id) if application_modes_section: shown_sections.append('applicationModes') departements_section = _make_departements_section( departement_id, _get_best_departements_for_job_group(rome_id, database), project.area_type, database) if departements_section: shown_sections.append('departements') employment_types_section = _make_employment_type_section( imt.employment_type_percentages) if employment_types_section: shown_sections.append('employmentTypes') months_section = _make_months_section(imt.active_months) if months_section: shown_sections.append('months') if len(shown_sections) < 3: logging.info('Only %d section(s) to be shown for user (%s).', len(shown_sections), shown_sections) return None imt_link = 'http://candidat.pole-emploi.fr/marche-du-travail/statistiques?' \ f'codeMetier={project.target_job.code_ogr}&codeZoneGeographique={departement_id}&' \ 'typeZoneGeographique=DEPARTEMENT' in_departement = geo.get_in_a_departement_text(database, departement_id) job_name_in_departement = f'{genderized_job_name} {in_departement}' return dict( 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': french.in_city(project.city.name), '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': french.maybe_contract_prefix('de ', "d'", job_name_in_departement), 'ofJobName': french.maybe_contract_prefix('de ', "d'", genderized_job_name), })
'campaign': user.origin.campaign, }, 'hasAccount': user.has_account, } def _add_scored_challenge(name: str, challenge_id: Optional[str]) -> None: if not challenge_id: return if not (score := data.get('nps_response', {}).get( 'challengeScores', {}).get(challenge_id)): return data['nps_response']['challengeScores'][name] = score last_project = _get_last_complete_project(user) if last_project: scoring_project = scoring.ScoringProject(last_project, user, stats_db) data['project'] = { 'targetJob': { 'name': last_project.target_job.name, 'job_group': { 'name': last_project.target_job.job_group.name, }, 'domain': _get_job_domain(stats_db, last_project.target_job), }, 'areaType': geo_pb2.AreaType.Name(last_project.area_type), 'city': { 'regionName': last_project.city.region_name, 'urbanScore': last_project.city.urban_score, }, 'job_search_length_months':