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_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_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 _make_months_section( months: Iterable['job_pb2.Month.V'], month_names_as_string: str) -> Optional[dict[str, str]]: month_names_map = _get_months_map(month_names_as_string) active_months = [ month_names_map[month] for month in months if month in month_names_map] if not active_months: return None return { 'activeMonths': ' - '.join(active_months), 'onlyOneMonth': campaign.as_template_boolean(len(active_months) == 1), }
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_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_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_galita2_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, str]: if not user.projects: raise scoring.NotEnoughDataException( 'Project is required for galita-2.', 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) | { 'isReorienting': campaign.as_template_boolean(project.kind == project_pb2.REORIENTATION) }
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 _make_departements_section( user_departement_id: str, best_departements: list[str], area_type: 'geo_pb2.AreaType.V', database: mongo.NoPiiMongoDatabase, scoring_project: scoring.ScoringProject) -> Optional[dict[str, str]]: if area_type < geo_pb2.COUNTRY or not best_departements: return None best_departements_title = '<br />'.join( geo.get_departement_name(database, dep) for dep in best_departements) try: best_departements.remove(user_departement_id) is_best_departement = True except ValueError: is_best_departement = False best_departements_sentence = scoring_project.translate_static_string(' et ').join( geo.get_in_a_departement_text(database, dep) for dep in best_departements) return { 'count': str(len(best_departements)), 'isInBest': campaign.as_template_boolean(is_best_departement), 'title': best_departements_title, 'sentence': best_departements_sentence, }
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 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 _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 _make_section(values: Optional[dict[str, str]]) -> dict[str, str]: return {'showSection': campaign.as_template_boolean(bool(values))} | (values or {})