def _get_action_plan_vars(user: user_pb2.User, now: datetime.datetime, **unused_kwargs: Any) -> dict[str, Any]: if not user.projects: raise campaign.DoNotSend('User does not have any projects.') project = user.projects[0] plan_actions = sorted( [ action for action in project.actions if (action.status == action_pb2.ACTION_CURRENT or action.status == action_pb2.ACTION_DONE) ], key=lambda action: action.expected_completion_at.ToDatetime()) if not plan_actions or not project.HasField('action_plan_started_at'): raise campaign.DoNotSend('User does not have a ready action plan.') actions_by_sections = _make_action_lists(plan_actions, user.user_id, project.project_id, now) creation_date = i18n.translate_date( project.action_plan_started_at.ToDatetime(), user.profile.locale) # TODO(Sil): Put actions and sections visibility in the same object. # TODO(cyrille) Make the variables so that we can loop on sections directly. return campaign.get_default_coaching_email_vars(user) | { 'actionPlanUrl': campaign.create_logged_url( user.user_id, f'/projet/{project.project_id}/plan-action'), 'actions': actions_by_sections, 'creationDate': creation_date, 'numActionsBySections': { section: len(actions_by_sections.get(section, [])) for section in _ACTIONS_SECTIONS } }
def _get_improve_cv_vars(user: user_pb2.User, *, now: datetime.datetime, **unused_kwargs: Any) -> dict[str, Any]: """Compute vars for the "Improve your CV" email.""" if user_profile_pb2.RESUME not in user.profile.frustrations: raise campaign.DoNotSend('User is not frustrated by its CV') if not user.projects: raise scoring.NotEnoughDataException('No project yet', {'projects.0'}) project = user.projects[0] if project.kind == project_pb2.FIND_A_FIRST_JOB: has_experience = 'False' elif project.kind in (project_pb2.FIND_A_NEW_JOB, project_pb2.FIND_ANOTHER_JOB): has_experience = 'True' else: has_experience = '' deep_link_advice_url = \ campaign.get_deep_link_advice(user.user_id, project, 'improve-resume') or \ 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) }
def _new_year_vars( user: user_pb2.User, *, now: datetime.datetime, **unused_kwargs: Any, ) -> dict[str, str]: """Compute all variables required for the New Year campaign.""" if now.month != 1 and not user.features_enabled.alpha: raise campaign.DoNotSend('Only send new-year email in January') project = next((p for p in user.projects), project_pb2.Project()) if project.passionate_level > project_pb2.PASSIONATING_JOB: goal = 'trouver un poste qui vous épanouira' elif project.kind == project_pb2.FIND_ANOTHER_JOB: goal = 'décrocher un nouveau poste' else: 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), }
def _get_galita3_short_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, Any]: if user_profile_pb2.NO_OFFER_ANSWERS not in user.profile.frustrations: raise campaign.DoNotSend( 'User is getting enough answers from recruiters.') project = user.projects[0] if (user.profile.locale or 'fr').startswith('fr'): advice_page_url = 'https://www.ionos.fr/startupguide/productivite/mail-de-relance-candidature' has_image_url = False elif user.profile.locale.startswith('en'): advice_page_url = 'https://zety.com/blog/how-to-follow-up-on-a-job-application' has_image_url = True else: logging.warning( 'No advice webpage given for campaign galita-3-short in "%s"', user.profile.locale) advice_page_url = '' has_image_url = False return campaign.get_default_coaching_email_vars(user) | { 'advicePageUrl': advice_page_url, 'hasImageUrl': has_image_url, 'weeklyApplicationsEstimate': project_pb2.NumberOfferEstimateOption.Name( project.weekly_applications_estimate) }
def _get_short_diploma_vars(user: user_pb2.User, *, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, Any]: if not user.projects: raise scoring.NotEnoughDataException('No project yet', {'projects.0'}) project = user.projects[0] if user.projects[0].diagnostic.category_id != 'missing-diploma': raise campaign.DoNotSend('The user has no missing-diploma category') scoring_project = scoring.ScoringProject(project, user, database) login_url = campaign.create_logged_url(user.user_id, f'/projet/{project.project_id}') # TODO(sil): Let's check if this is needed to have access to the method. 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'}) 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, 'ofJobName': scoring_project.populate_template('%ofJobName'), 'productUrl': f'{login_url}?utm_source=bob-emploi&utm_medium=email', }
def _get_galita1_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, str]: if user_profile_pb2.MOTIVATION not in user.profile.frustrations: raise campaign.DoNotSend('User is motivated enough.') if user.projects and user.projects[0].job_search_has_not_started: raise campaign.DoNotSend('User is not searching for a job yet.') return campaign.get_default_coaching_email_vars(user)
def _get_dwp_interview_vars(user: user_pb2.User, now: datetime.datetime, **unused_kwargs: Any) -> dict[str, Any]: if user.origin.source != 'dwp': raise campaign.DoNotSend('User does not come from DWP.') three_weeks_ago = now.replace(microsecond=0) - datetime.timedelta(21) if user.registered_at.ToDatetime( ) > three_weeks_ago and not user.features_enabled.alpha: raise campaign.DoNotSend('User registered less than 3 weeks ago.') return campaign.get_default_coaching_email_vars(user)
def _get_galita2_short_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, str]: if not user.projects: raise scoring.NotEnoughDataException( 'Project is required for galita-2-short.', fields={'user.projects.0.kind'}) project = user.projects[0] if project.kind not in {project_pb2.FIND_A_FIRST_JOB, project_pb2.REORIENTATION} and \ project.previous_job_similarity != project_pb2.NEVER_DONE: raise campaign.DoNotSend( 'User is not searching a job in a profession new to them.') return campaign.get_default_coaching_email_vars(user)
def _get_self_development_vars( user: user_pb2.User, *, now: datetime.datetime, **unused_kwargs: Any) \ -> dict[str, str]: """Computes vars for a given user for the self-development email. Returns a dict with all vars required for the template. """ if not user.projects: raise scoring.NotEnoughDataException('No project yet', {'projects.0'}) has_video = (user.profile.locale or 'fr').startswith('fr') project = user.projects[0] job_search_length = campaign.job_search_started_months_ago(project, now) if job_search_length < 0: raise campaign.DoNotSend('No info on user search duration.') if job_search_length > 12: raise campaign.DoNotSend( f'User has been searching for too long ({job_search_length:.2f}).') genderized_job_name = french.lower_first_letter( french.genderize_job(project.target_job, user.profile.gender)) age = datetime.date.today().year - user.profile.year_of_birth max_young = 30 min_old = 50 return campaign.get_default_coaching_email_vars(user) | { 'hasEnoughExperience': campaign.as_template_boolean(project.seniority > project_pb2.JUNIOR), 'hasVideo': campaign.as_template_boolean(has_video), 'isAdministrativeAssistant': campaign.as_template_boolean( project.target_job.job_group.name == 'Secrétariat'), 'isOld': campaign.as_template_boolean(age >= min_old), 'isOldNotWoman': campaign.as_template_boolean( age >= min_old and user.profile.gender != user_profile_pb2.FEMININE), 'isYoung': campaign.as_template_boolean(age <= max_young), 'isYoungNotWoman': campaign.as_template_boolean( age <= max_young and user.profile.gender != user_profile_pb2.FEMININE), 'jobName': genderized_job_name, }
def _get_jobflix_invite_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, Any]: jobflix_default_vars = jobflix.get_default_vars(user) product_url = jobflix_default_vars['productUrl'] # Keep the source sync with advisor/jobflix.tsx. source = 'dwp' if user.origin.source == 'dwp' else 'bob' return campaign.get_default_coaching_email_vars(user) | { 'senderName': jobflix_default_vars['senderFirstName'], 'sideProductName': jobflix_default_vars['productName'], 'sideProductUrl': f'{product_url}?utm_source={source}&utm_medium=email', 'statusUpdateUrl': campaign.get_status_update_link(user) }
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, }
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), }
def _get_galita3_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, str]: if user_profile_pb2.NO_OFFER_ANSWERS not in user.profile.frustrations: raise campaign.DoNotSend( 'User is getting enough answers from recruiters.') # We set a string with a blank as this is the only way to exclude a section # on Passport except to check equality or inequality with a non-empty # string. deep_link_to_follow_up_advice = ' ' if user.projects: for project in user.projects: link = campaign.get_deep_link_advice(user.user_id, project, 'follow-up') if link: deep_link_to_follow_up_advice = link return campaign.get_default_coaching_email_vars(user) | { 'deepLinkToAdvice': deep_link_to_follow_up_advice, }
def _get_prepare_your_application_vars(user: user_pb2.User, **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] deep_link_motivation_email_url = \ campaign.get_deep_link_advice(user.user_id, project, 'motivation-email') return campaign.get_default_coaching_email_vars(user) | { 'deepLinkMotivationEmailUrl': deep_link_motivation_email_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), 'loginUrl': campaign.create_logged_url(user.user_id, f'/projet/{project.project_id}'), }
def _body_language_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, str]: """Computes vars for a given user for the body language email. Returns a dict with all vars required for the template. """ worst_frustration = next( (user_profile_pb2.Frustration.Name(frustration) for frustration in (user_profile_pb2.SELF_CONFIDENCE, user_profile_pb2.INTERVIEW, user_profile_pb2.ATYPIC_PROFILE) if frustration in user.profile.frustrations), '') if not worst_frustration: raise campaign.DoNotSend( 'User has no frustration related to body language.') return campaign.get_default_coaching_email_vars(user) | { 'worstFrustration': worst_frustration, }
def _get_network_vars( user: user_pb2.User, *, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, str]: """Compute vars for a given user for the network email. Returns: a dict with all vars required for the template, or None if no email should be sent. """ if not user.projects: raise scoring.NotEnoughDataException('No project yet', {'projects.0'}) project = user.projects[0] if project.network_estimate != 1: raise campaign.DoNotSend('User has a good enough network') in_target_domain = _get_in_target_domain(project.target_job.job_group.rome_id, database) worst_frustration = next( (f for f in (user_profile_pb2.NO_OFFER_ANSWERS, user_profile_pb2.MOTIVATION) if f in user.profile.frustrations), None) is_hairdresser_or_in_marseille = \ project.target_job.job_group.rome_id.startswith('D') or \ project.city.departement_id == '13' other_job_in_city = 'coiffeur à Marseille' if is_hairdresser_or_in_marseille: other_job_in_city = 'secrétaire à Lyon' job = french.lower_first_letter(french.genderize_job( project.target_job, user.profile.gender)) in_city = french.in_city(strip_district(project.city.name)) return campaign.get_default_coaching_email_vars(user) | { 'inTargetDomain': in_target_domain, 'frustration': user_profile_pb2.Frustration.Name(worst_frustration) if worst_frustration else '', 'otherJobInCity': other_job_in_city, 'jobInCity': f'{job} {in_city}', 'emailInUrl': parse.quote(user.profile.email), }
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 send_update_confirmation(self, user_dict: dict[str, Any]) -> None: """Sends an email to the user that confirms password change.""" user_id = str(user_dict['_id']) if not user_id: return auth_link = token.create_logged_url(user_id) reset_link = self._get_reset_password_link(user_dict) if not reset_link or not auth_link: return user = proto.create_from_mongo(user_dict.copy(), user_pb2.User) template_vars = dict(campaign.get_default_coaching_email_vars(user), authLink=auth_link, resetPwdLink=reset_link) # TODO(cyrille): Create a static Campaign object and use it. result = mail_send.send_template('send-pwd-update-confirmation', user.profile, template_vars) if result.status_code != 200: logging.error('Failed to send an email with MailJet:\n %s', result.text) flask.abort(result.status_code)
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 _get_prepare_your_application_short_vars(user: user_pb2.User, **unused_kwargs: Any)\ -> dict[str, Any]: """Compute vars for the "Prepare your application short" email.""" if not user.projects: raise scoring.NotEnoughDataException('No project yet', {'projects.0'}) 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 = '' return campaign.get_default_coaching_email_vars(user) | { '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), }
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, }
if project.target_job.name: of_job_name = scoring_project.populate_template('%ofJobName') genderized_job_name = scoring_project.populate_template('%jobName') else: of_job_name = scoring_project.translate_static_string('dans votre domaine') # This variable should be included in the following sentence: # "des personnes qui travaillent comme {{var:jobNameInDepartement}} ont décroché leur poste" # That'd make a weird but ok sentence and should not happened anyway as this block relies # on job info. genderized_job_name = scoring_project.translate_static_string('vous') job_name_in_departement = f'{genderized_job_name} {in_departement}' of_job_name_in_departement = f'{of_job_name} {in_departement}' 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(
def _open_classrooms_vars(user: user_pb2.User, *, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, str]: """Template variables for open classrooms email.""" if user.registered_at.ToDatetime() < _SIX_MONTHS_AGO: raise campaign.DoNotSend('User registered less than 6 months ago.') age = datetime.date.today().year - user.profile.year_of_birth if age < 18: raise campaign.DoNotSend( 'User too young to subscribe to OpenClassrooms.') if age > 54: raise campaign.DoNotSend( 'User too old to subscribe to OpenClassrooms.') if user.profile.highest_degree > job_pb2.BAC_BACPRO: raise campaign.DoNotSend('User might have higher education.') if user.employment_status and user.employment_status[ -1].seeking != user_pb2.STILL_SEEKING: raise campaign.DoNotSend('User is no more seeking for a job.') if not (user.projects and user.projects[0]): raise scoring.NotEnoughDataException('Project is required.', fields={'user.projects.0.kind'}) project = user.projects[0] if project.kind != project_pb2.REORIENTATION and not ( project.kind == project_pb2.FIND_A_NEW_JOB and project.passionate_level == project_pb2.ALIMENTARY_JOB): raise campaign.DoNotSend( 'User is happy with their job (no reorientation and enthusiastic about their job).' ) has_children = user.profile.family_situation in { user_profile_pb2.FAMILY_WITH_KIDS, user_profile_pb2.SINGLE_PARENT_SITUATION, } job_group_info = jobs.get_group_proto(database, project.target_job.job_group.rome_id) if not job_group_info: raise scoring.NotEnoughDataException( 'Requires job group info for the difficulty of applying to this kind of job.' ) return campaign.get_default_coaching_email_vars(user) | { 'hasAtypicProfile': campaign.as_template_boolean( user_profile_pb2.ATYPIC_PROFILE in user.profile.frustrations), 'hasFamilyAndManagementIssue': campaign.as_template_boolean( has_children and user_profile_pb2.TIME_MANAGEMENT in user.profile.frustrations), 'hasSeniority': campaign.as_template_boolean( project.seniority > project_pb2.INTERMEDIARY), 'hasSimpleApplication': campaign.as_template_boolean(job_group_info.application_complexity == job_pb2.SIMPLE_APPLICATION_PROCESS), 'isReorienting': campaign.as_template_boolean( project.kind == project_pb2.REORIENTATION), 'isFrustratedOld': campaign.as_template_boolean(age >= 40 and user_profile_pb2.AGE_DISCRIMINATION in user.profile.frustrations), 'ofFirstName': french.maybe_contract_prefix('de ', "d'", user.profile.name) }
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_confidence_boost_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, Any]: return campaign.get_default_coaching_email_vars(user)
def network_plus_vars( user: user_pb2.User, *, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, str]: """Compute vars for a given user for the network email. Returns: a dict with all vars required for the template, or None if no email should be sent. """ if not user.projects: raise scoring.NotEnoughDataException('No project yet', {'projects.0'}) project = user.projects[0] if project.network_estimate < 2: raise campaign.DoNotSend('User does not have a strong network') rome_id = project.target_job.job_group.rome_id in_target_domain = _get_in_target_domain(rome_id, database) job_group_info = jobs.get_group_proto(database, rome_id) assert job_group_info application_modes = job_group_info.application_modes.values() fap_modes = [fap_modes.modes for fap_modes in application_modes if len(fap_modes.modes)] if not fap_modes: raise scoring.NotEnoughDataException( 'No information about application modes for the target job', {f'data.job_group_info.{rome_id}.application_modes'}) flat_fap_modes = [mode for modes in fap_modes for mode in modes] network_percentages = [mode.percentage for mode in flat_fap_modes if ( mode.mode == job_pb2.PERSONAL_OR_PROFESSIONAL_CONTACTS)] # We want to focus on the users for which network, # as an application mode, has a substantial importance. if not network_percentages: raise campaign.DoNotSend( 'User is not targeting a job where networking is a main application mode') scoring_project = scoring.ScoringProject(project, user, database=database) average_network_percentage = sum(network_percentages) / len(network_percentages) if average_network_percentage > 55: network_application_importance = scoring_project.translate_static_string('que la majorité') elif average_network_percentage >= 45: network_application_importance = scoring_project.translate_static_string('que la moitié') elif average_network_percentage >= 25: network_application_importance = scoring_project.translate_static_string("qu'un tiers") else: raise campaign.DoNotSend( 'User is not targeting a job where networking is a main application mode') worst_frustration = next( (f for f in (user_profile_pb2.SELF_CONFIDENCE, user_profile_pb2.MOTIVATION) if f in user.profile.frustrations), None) has_children = user.profile.family_situation in { user_profile_pb2.FAMILY_WITH_KIDS, user_profile_pb2.SINGLE_PARENT_SITUATION} age = datetime.date.today().year - user.profile.year_of_birth max_young = 35 try: in_departement = geo.get_in_a_departement_text( database, project.city.departement_id, city_hint=project.city, locale=user.profile.locale) except KeyError: raise scoring.NotEnoughDataException( 'Need departement info for phrasing', {f'data.departements.{project.city.departement_id}'}) from None job_group_name = french.lower_first_letter(project.target_job.job_group.name) if (user.profile.locale or 'fr').startswith('fr'): in_city = french.in_city(project.city.name) else: # TODO(pascal): Update the English template so that it follows the logic of "in city" and # not "city". For now it's phrased as "near {{inCity}}". in_city = project.city.name return campaign.get_default_coaching_email_vars(user) | { 'frustration': user_profile_pb2.Frustration.Name(worst_frustration) if worst_frustration else '', 'hasChildren': campaign.as_template_boolean(has_children), 'hasHighSchoolDegree': campaign.as_template_boolean( user.profile.highest_degree >= job_pb2.BAC_BACPRO), 'hasLargeNetwork': campaign.as_template_boolean(project.network_estimate >= 2), 'hasWorkedBefore': campaign.as_template_boolean( project.kind != project_pb2.FIND_A_FIRST_JOB), '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, }
def _christmas_vars(user: user_pb2.User, *, now: datetime.datetime, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, str]: """Compute all variables required for the Christmas campaign.""" if now.month != 12 and not user.features_enabled.alpha: raise campaign.DoNotSend('Only send christmas email in December') project = next((p for p in user.projects), project_pb2.Project()) job_search_started_months_ago = campaign.job_search_started_months_ago( project, now) if job_search_started_months_ago < 0: started_searching_since = '' elif job_search_started_months_ago < 2: started_searching_since = 'depuis peu' else: try: num_months = french.try_stringify_number( round(job_search_started_months_ago)) started_searching_since = f'depuis {num_months} mois' except NotImplementedError: started_searching_since = 'depuis un moment' # A city to commute to. commute_city = next( (city for a in project.advices for city in a.commute_data.cities), '') if commute_city: commute_city = french.in_city(commute_city) commute_advice_url = campaign.get_deep_link_advice(user.user_id, project, 'commute') if not commute_advice_url: commute_city = '' # A departement to relocate to. relocate_departement = next( (departement.name for a in project.advices for departement in a.relocate_data.departement_scores), '') if relocate_departement: try: departement_id = geo.get_departement_id(database, relocate_departement) relocate_departement = geo.get_in_a_departement_text( database, departement_id) except KeyError: relocate_departement = '' relocate_advice_url = campaign.get_deep_link_advice( user.user_id, project, 'relocate') if not relocate_advice_url: relocate_departement = '' # Whether the job may have freelancers. job_group_info = jobs.get_group_proto(database, project.target_job.job_group.rome_id) could_freelance = job_group_info and job_group_info.has_freelancers return campaign.get_default_coaching_email_vars(user) | { 'adviceUrlBodyLanguage': campaign.get_deep_link_advice(user.user_id, project, 'body-language'), 'adviceUrlCommute': commute_advice_url, 'adviceUrlCreateYourCompany': campaign.get_deep_link_advice(user.user_id, project, 'create-your-company'), 'adviceUrlExploreOtherJobs': campaign.get_deep_link_advice(user.user_id, project, 'explore-other-jobs'), 'adviceUrlImproveInterview': campaign.get_deep_link_advice(user.user_id, project, 'improve-interview'), 'adviceUrlRelocate': relocate_advice_url, 'adviceUrlVolunteer': campaign.get_deep_link_advice(user.user_id, project, 'volunteer'), 'couldFreelance': campaign.as_template_boolean(could_freelance), 'emailInUrl': parse.quote(user.profile.email), 'inCommuteCity': commute_city, 'inRelocateDepartement': relocate_departement, 'nextYear': str(now.year + 1), 'startedSearchingSince': started_searching_since, 'year': str(now.year), }