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_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_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 _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 _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_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 _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&amp;utm_medium=email', }
def _get_first_actions_vars(user: user_pb2.User, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, Any]: job_list = list( filter(None, (jobs.get_job_proto(database, p.target_job.code_ogr, p.target_job.job_group.rome_id) for p in user.projects))) if not job_list: raise campaign.DoNotSend('Need to have at least one job.') job_names = [job.name for job in job_list] quoted_jobs = parse.quote(' '.join(job_names)) scoring_project = scoring.ScoringProject(user.projects[0], user, database) return get_default_vars(user) | { 'departements': ','.join({p.city.departement_id for p in user.projects}), 'hasSeveralJobs': campaign.as_template_boolean(len(job_list) > 1), 'jobIds': ','.join({job.job_group.rome_id for job in job_list}), 'jobs': job_names, 'ofJobName': scoring_project.populate_template('%ofJobName'), 'optLink': 'https://www.orientation-pour-tous.fr/spip.php?' f'page=recherche&rubrique=metiers&recherche={quoted_jobs}', }
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_ffs_vars( user: user_pb2.User, *, now: datetime.datetime, **unused_kwargs: Any, ) -> dict[str, str]: user_id = user.user_id days_since_registered = (now - user.registered_at.ToDatetime()).days if user.net_promoter_score_survey_response.score: raise campaign.DoNotSend('User already answered the NPS survey') is_alpha = user.features_enabled.alpha if (days_since_registered < 6 or days_since_registered > 13) and not is_alpha: raise campaign.DoNotSend( 'User registered too long ago or too recently') main_challenge_id = user.projects[ 0].diagnostic.category_id if user.projects else '' return campaign.get_default_vars(user) | { 'buttonBackgroundColor': '#58bbfb', 'buttonTextColor': '#ffffff', 'ffsFormUrl': campaign.get_bob_link( '/api/first-followup-survey', { 'user': user_id, 'token': auth_token.create_token(user_id, 'first-followup-survey'), 'redirect': campaign.get_bob_link( '/first-followup-survey', { 'hl': user.profile.locale, 'gender': user_profile_pb2.Gender.Name( user.profile.gender), 'mainChallenge': main_challenge_id, }), }), }
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_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_first_eval_reminder_vars( user: user_pb2.User, *, now: datetime.datetime, **unused_kwargs: Any, ) -> 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'), }
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_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 _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_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 _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 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 _get_first_eval_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, Any]: if not _is_bob_jobflix_user(user): raise campaign.DoNotSend('Only interesting for Bob-Jobflix users.') return campaign.get_default_vars(user)
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_vars(user: user_pb2.User, *, now: datetime.datetime, database: mongo.NoPiiMongoDatabase, **unused_kwargs: Any) -> dict[str, str]: if not user.projects or not user.projects[0].actions: raise campaign.DoNotSend('User has no project or no actions yet') project = user.projects[0] most_recent_date = max(d.ToDatetime() for d in (user.registered_at, project.created_at, project.action_plan_started_at)) if (now - most_recent_date).days > 7: raise campaign.DoNotSend( 'User has registered a while ago, too late to send the activation') # Set locale. user_locale = user.profile.locale.split('@', 1)[0] date_format = '%d %B %Y' if user_locale == 'fr' or not user_locale: locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8') elif user_locale == 'en_UK': locale.setlocale(locale.LC_ALL, 'en_GB.UTF-8') date_format = '%B %d %Y' elif user_locale == 'en': locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') date_format = '%B %d %Y' else: logging.exception('Sending an email with an unknown locale: %s', user_locale) scoring_project = scoring.ScoringProject(project, user, database, now=now) auth_token = parse.quote( token.create_token(user.user_id, is_using_timestamp=True)) settings_token = parse.quote( token.create_token(user.user_id, role='settings')) coaching_email_frequency_name = \ email_pb2.EmailFrequency.Name(user.profile.coaching_email_frequency) # This uses tutoiement by default, because its content adressed from the user to a third party # (that we assume the user is familiar enough with), not from Bob to the user. virality_template = parse.urlencode({ 'body': scoring_project.translate_static_string( 'Salut,\n\n' "Est-ce que tu connais Bob\u00A0? C'est un site qui propose de t'aider dans ta " "recherche d'emploi en te proposant un diagnostic et des conseils personnalisés. " 'Tu verras, ça vaut le coup\u00A0: en 15 minutes, tu en sauras plus sur où tu en es, ' 'et ce que tu peux faire pour avancer plus efficacement. ' "Et en plus, c'est gratuit\u00A0!\n\n" '{invite_url}\n\n' 'En tous cas, bon courage pour la suite,\n\n' '{first_name}', ).format(invite_url=parse.urljoin( product.bob.base_url, 'invite#vm2m'), first_name=user.profile.name), 'subject': scoring_project.translate_static_string("Ça m'a fait penser à toi"), }) change_email_settings_url = parse.urljoin( product.bob.base_url, 'unsubscribe.html?' + parse.urlencode({ 'user': user.user_id, 'auth': settings_token, 'coachingEmailFrequency': coaching_email_frequency_name, 'hl': user.profile.locale, })) team_members = ( 'Tabitha', 'Paul', 'John', 'Pascal', 'Sil', 'Cyrille', 'Flo', 'Nicolas', 'Florian', 'Lillie', 'Benjamin', 'Émilie', ) # Selected and pending actions. highlighted_actions = [ action for action in project.actions if action.status == action_pb2.ACTION_CURRENT ] if len(highlighted_actions) < 2: highlighted_actions.extend( action for action in project.actions if action.status == action_pb2.ACTION_UNREAD) else: highlighted_actions = sorted( highlighted_actions, key=lambda action: action.expected_completion_at.ToDatetime()) actions = [{ 'title': action.title, 'url': parse.urljoin( product.bob.base_url, f'/projet/{project.project_id}/action/{action.action_id}' f'?userId={user.user_id}&authToken={auth_token}') } for action in highlighted_actions[:2]] data: dict[str, Any] = campaign.get_default_vars(user) data |= { 'actions': actions, 'changeEmailSettingsUrl': change_email_settings_url, 'coachingEmailFrequency': email_pb2.EmailFrequency.Name(user.profile.coaching_email_frequency) if user.profile.coaching_email_frequency and user.profile.coaching_email_frequency != email_pb2.EMAIL_NONE else '', 'date': now.strftime(date_format), 'firstTeamMember': team_members[0], 'isActionPlanCompleted': project.HasField('action_plan_started_at'), 'isCoachingEnabled': 'True' if user.profile.coaching_email_frequency and user.profile.coaching_email_frequency != email_pb2.EMAIL_NONE else '', 'loginUrl': parse.urljoin(product.bob.base_url, f'?userId={user.user_id}&authToken={auth_token}'), 'numActions': len(actions), 'numberUsers': '270\u00A0000', 'numberTeamMembers': len(team_members), 'ofJob': scoring_project.populate_template('%ofJobName', raise_on_missing_var=True) if project.target_job.name else '', 'teamMembers': ', '.join(team_members[1:]), 'viralityTemplate': f'mailto:?{virality_template}' } return data
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), }
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) }