def network_vars(user, database): """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: logging.info('User has no project') return None project = user.projects[0] registered_months_ago = campaign.get_french_months_ago( user.registered_at.ToDatetime()) if not registered_months_ago: logging.warning('User registered only recently (%s)', user.registered_at) return None job_group_info = jobs.get_group_proto(database, project.target_job.job_group.rome_id) in_target_domain = job_group_info.in_domain if not in_target_domain: logging.warning('Could not find a target domain (%s)', project.target_job.job_group) return None worst_frustration = next( (f for f in (user_pb2.NO_OFFER_ANSWERS, user_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.mobility.city.departement_id == '13' other_job_in_city = 'coiffeur à Marseille' if is_hairdresser_or_in_marseille: other_job_in_city = 'secrétaire à Lyon' return dict( campaign.get_default_vars(user), **{ 'registeredMonthsAgo': registered_months_ago, 'inTargetDomain': in_target_domain, 'frustration': user_pb2.Frustration.Name(worst_frustration) if worst_frustration else '', 'otherJobInCity': other_job_in_city, 'jobInCity': '{} {}'.format( french.lower_first_letter( french.genderize_job(project.target_job, user.profile.gender)), french.in_city(strip_district(project.mobility.city.name))), 'emailInUrl': parse.quote(user.profile.email), 'statusUpdateUrl': campaign.get_status_update_link(user.user_id, user.profile), })
def _get_network_vars(user: user_pb2.User, database: Optional[pymongo.database.Database] = None, **unused_kwargs: Any) -> Optional[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. """ project = user.projects[0] if project.network_estimate != 1: logging.info('User has a good enough network') return None assert database job_group_info = jobs.get_group_proto(database, project.target_job.job_group.rome_id) if not job_group_info: logging.warning('Could not find job group info for "%s"', project.target_job.job_group.rome_id) return None in_target_domain = job_group_info.in_domain if not in_target_domain: logging.warning('Could not find a target domain (%s)', project.target_job.job_group) return None worst_frustration = next( (f for f in (user_pb2.NO_OFFER_ANSWERS, user_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 dict( campaign.get_default_coaching_email_vars(user), **{ 'inTargetDomain': in_target_domain, 'frustration': user_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_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_self_development_vars(user: user_pb2.User, now: datetime.datetime, **unused_kwargs: Any) \ -> Optional[Dict[str, str]]: """Compute vars for a given user for the self-development email. Returns: a dict with all vars required for the template, or None if no email should be sent. """ project = user.projects[0] job_search_length = campaign.job_search_started_months_ago(project, now) if job_search_length < 0: logging.info('No info on user search duration') return None if job_search_length > 12: logging.info('User has been searching for too long (%s)', job_search_length) return None 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 dict( campaign.get_default_coaching_email_vars(user), **{ 'hasEnoughExperience': campaign.as_template_boolean( project.seniority > project_pb2.JUNIOR), '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_pb2.FEMININE), 'isYoung': campaign.as_template_boolean(age <= max_young), 'isYoungNotWoman': campaign.as_template_boolean( age <= max_young and user.profile.gender != user_pb2.FEMININE), 'jobName': genderized_job_name, 'ofJobName': french.maybe_contract_prefix('de ', "d'", genderized_job_name), })
def _get_template_variable(self, name: str) -> str: if name in self._template_variables: return self._template_variables[name] try: cache = _TEMPLATE_VARIABLES[name](self) except KeyError: # name[0] should always be %. lower_name = name[0] + french.lower_first_letter(name[1:]) if lower_name not in _TEMPLATE_VARIABLES: logging.info('Wrong case in template variable "%s", cannot replace it.', name) cache = name else: # Recursion cannot run in a loop thanks to the test just before. cache = french.upper_first_letter(self._get_template_variable(lower_name)) self._template_variables[name] = cache return cache
def _get_galita2_vars(user: user_pb2.User, **unused_kwargs: Any) -> Optional[Dict[str, str]]: if not user.projects: return None 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: return None genderized_job_name = french.lower_first_letter( french.genderize_job(project.target_job, user.profile.gender)) return dict( campaign.get_default_coaching_email_vars(user), **{ 'isReorienting': campaign.as_template_boolean( project.kind == project_pb2.REORIENTATION), 'ofJobName': french.maybe_contract_prefix('de ', "d'", genderized_job_name) })
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 _send_activation_email(user, project, database, base_url): """Send an email to the user just after we have defined their diagnosis.""" advice_modules = {a.advice_id: a for a in _advice_modules(database)} advices = [a for a in project.advices if a.advice_id in advice_modules] if not advices: logging.error( # pragma: no-cover 'Weird: the advices that just got created do not exist in DB.' ) # pragma: no-cover return # pragma: no-cover data = { 'baseUrl': base_url, 'projectId': project.project_id, 'firstName': user.profile.name, 'ofProjectTitle': french.maybe_contract_prefix( 'de ', "d'", french.lower_first_letter( french.genderize_job(project.target_job, user.profile.gender))), 'advices': [{ 'adviceId': a.advice_id, 'title': advice_modules[a.advice_id].title } for a in advices], } response = mail.send_template( # https://app.mailjet.com/template/168827/build '168827', user.profile, data, dry_run=not _EMAIL_ACTIVATION_ENABLED) if response.status_code != 200: logging.warning('Error while sending diagnostic email: %s\n%s', response.status_code, response.text)
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 network_plus_vars(user: user_pb2.User, database: Optional[pymongo.database.Database] = None, **unused_kwargs: Any) -> Optional[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. """ project = user.projects[0] if project.network_estimate < 2: logging.info('User does not have a strong network') return None assert database job_group_info = jobs.get_group_proto(database, project.target_job.job_group.rome_id) if not job_group_info: logging.warning('Could not find job group info for "%s"', project.target_job.job_group.rome_id) return None in_target_domain = job_group_info.in_domain application_modes = job_group_info.application_modes.values() if not in_target_domain: logging.warning('Could not find a target domain (%s)', project.target_job.job_group) return None fap_modes = [ fap_modes.modes for fap_modes in application_modes if len(fap_modes.modes) ] if not fap_modes: return None 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 user for which network, # as an application mode, has a substantial importance. if not network_percentages: return None average_network_percentage = sum(network_percentages) / len( network_percentages) if average_network_percentage > 55: network_application_importance = 'que la majorité' elif average_network_percentage >= 45: network_application_importance = 'que la moitié' elif average_network_percentage >= 25: network_application_importance = "qu'un tiers" else: return None worst_frustration = next( (f for f in (user_pb2.SELF_CONFIDENCE, user_pb2.MOTIVATION) if f in user.profile.frustrations), None) has_children = user.profile.family_situation in { user_pb2.FAMILY_WITH_KIDS, user_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) except KeyError: logging.warning('Could not find departement (%s)', project.city.departement_id) return None job_group_name = french.lower_first_letter( project.target_job.job_group.name) return dict( campaign.get_default_coaching_email_vars(user), **{ 'frustration': user_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': french.in_city(project.city.name), '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 network_plus_vars(user, database): """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: logging.info('User has no project') return None project = user.projects[0] registered_months_ago = campaign.get_french_months_ago( user.registered_at.ToDatetime()) if not registered_months_ago: logging.warning('User registered only recently (%s)', user.registered_at) return None job_group_info = jobs.get_group_proto(database, project.target_job.job_group.rome_id) in_target_domain = job_group_info.in_domain application_modes = job_group_info.application_modes.values() if not in_target_domain: logging.warning('Could not find a target domain (%s)', project.target_job.job_group) return None fap_modes = [ fap_modes.modes for fap_modes in application_modes if len(fap_modes.modes) ] if not fap_modes: return None 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 user for which network, # as an application mode, has a substantial importance. if not network_percentages: return None average_network_percentage = sum(network_percentages) / len( network_percentages) if average_network_percentage < 55: network_application_importance = 'que la majorité' if average_network_percentage >= 45 and average_network_percentage <= 55: network_application_importance = 'que la moitié' if average_network_percentage >= 25 and average_network_percentage < 45: network_application_importance = "qu'un tiers" else: return None worst_frustration = next( (f for f in (user_pb2.SELF_CONFIDENCE, user_pb2.MOTIVATION) if f in user.profile.frustrations), None) has_children = user.profile.family_situation in { user_pb2.FAMILY_WITH_KIDS, user_pb2.SINGLE_PARENT_SITUATION } age = datetime.date.today().year - user.profile.year_of_birth max_young = 35 return dict( campaign.get_default_vars(user), **{ 'frustration': user_pb2.Frustration.Name(worst_frustration) if worst_frustration else '', 'hasChildren': campaign.as_template_boolean(has_children), 'hasHandicap': campaign.as_template_boolean(user.profile.has_handicap), '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': french.in_city(project.mobility.city.name), 'inTargetDomain': in_target_domain, 'isYoung': campaign.as_template_boolean(age <= max_young), 'jobGroupInDepartement': '{} {}'.format( french.lower_first_letter(project.target_job.job_group.name), geo.get_in_a_departement_text( database, project.mobility.city.departement_id)), 'networkApplicationPercentage': network_application_importance, })
def quick_diagnose( user: user_pb2.User, project: project_pb2.Project, user_diff: user_pb2.User, database: mongo.NoPiiMongoDatabase) -> diagnostic_pb2.QuickDiagnostic: """Create a quick diagnostic of a project or user profile focused on the given field.""" scoring_project = scoring.ScoringProject(project or project_pb2.Project(), user, database, now=now.get()) response = diagnostic_pb2.QuickDiagnostic() has_departement_diff = user_diff.projects and user_diff.projects[ 0].city.departement_id if has_departement_diff: all_counts = get_users_counts(database) if all_counts: departement_count = all_counts.departement_counts[ project.city.departement_id] if departement_count and departement_count > 50: response.comments.add( field=diagnostic_pb2.CITY_FIELD, comment=_create_bolded_string( scoring_project.translate_static_string( 'Super, <strong>{count}</strong> personnes dans ce département ont déjà ' 'testé le diagnostic de Bob\xa0!', ).format( count=str(departement_count))), ) has_rome_id_diff = user_diff.projects and user_diff.projects[ 0].target_job.job_group.rome_id if has_rome_id_diff: all_counts = get_users_counts(database) if all_counts: job_group_count = all_counts.job_group_counts[ project.target_job.job_group.rome_id] if job_group_count and job_group_count > 50: response.comments.add( field=diagnostic_pb2.TARGET_JOB_FIELD, comment=_create_bolded_string( scoring_project.translate_static_string( "Ça tombe bien, j'ai déjà accompagné <strong>{count}</strong> personnes " 'pour ce métier\xa0!', ).format( count=str(job_group_count))), ) if user_diff.profile.year_of_birth or has_rome_id_diff or has_departement_diff: if user.profile.year_of_birth: local_diagnosis = scoring_project.local_diagnosis() is_senior = scoring_project.get_user_age() >= 35 if is_senior: salary_estimation = local_diagnosis.imt.senior_salary else: salary_estimation = local_diagnosis.imt.junior_salary if salary_estimation.short_text: response.comments.add( field=diagnostic_pb2.SALARY_FIELD, is_before_question=True, comment=diagnostic_pb2.BoldedString(string_parts=[ scoring_project.translate_static_string( 'En général les gens demandent un salaire {of_salary} par mois.', ).format(of_salary=french.lower_first_letter( salary_estimation.short_text)), ]), ) if has_rome_id_diff: required_diplomas = sorted( ( d for d in scoring_project.requirements().diplomas # Only mention real diplomas that are required in 10% or more of job offers. if d.diploma.level != job_pb2.NO_DEGREE and d.percent_required > 10), key=lambda d: d.percent_required, reverse=True, # Only take the 2 biggest ones. )[:2] if len(required_diplomas) == 2: if required_diplomas[0].percent_required >= 70: # The first one is doing more than 70% of requirements, just keep one. required_diplomas = required_diplomas[:1] else: # Sort by degree level. required_diplomas.sort(key=lambda d: d.diploma.level) if required_diplomas: diplomas = ', '.join(diploma.name for diploma in required_diplomas) response.comments.add( field=diagnostic_pb2.REQUESTED_DIPLOMA_FIELD, is_before_question=True, comment=diagnostic_pb2.BoldedString(string_parts=[ scoring_project.translate_static_string( 'Les offres demandent souvent un {diplomas} ou équivalent.', ).format(diplomas=diplomas), ])) if has_rome_id_diff or has_departement_diff: local_diagnosis = scoring_project.local_diagnosis() if local_diagnosis.imt.employment_type_percentages: main_employment_type_percentage = local_diagnosis.imt.employment_type_percentages[ 0] if main_employment_type_percentage.percentage > 98: comment = scoring_project.translate_static_string( 'La plupart des offres sont en {employment_type}.', ) else: comment = scoring_project.translate_static_string( 'Plus de {percentage}% des offres sont en {employment_type}.', ) if main_employment_type_percentage.employment_type in jobs.EMPLOYMENT_TYPES: employment_type = scoring_project.translate_static_string( jobs.EMPLOYMENT_TYPES[ main_employment_type_percentage.employment_type]) response.comments.add( field=diagnostic_pb2.EMPLOYMENT_TYPE_FIELD, is_before_question=True, comment=_create_bolded_string( comment.format( percentage=str( int(main_employment_type_percentage.percentage) ), employment_type=employment_type, )), ) return response
_an_application_mode, '%cityId': lambda scoring_project: scoring_project.details.city.city_id, # TODO(pascal): Investigate who's using that template and rename it to someting with URL in it. '%cityName': lambda scoring_project: parse.quote(scoring_project.details.city.name), '%departementId': lambda scoring_project: scoring_project.details.city.departement_id, '%eFeminine': lambda scoring_project: ('e' if scoring_project.user_profile.gender == user_pb2.FEMININE else ''), '%experienceDuration': lambda scoring_project: _EXPERIENCE_DURATION.get( scoring_project.details.seniority, ''), '%feminineJobName': lambda scoring_project: french.lower_first_letter( scoring_project.details.target_job.feminine_name), '%inAreaType': _in_area_type, '%inAWorkplace': lambda scoring_project: scoring_project.job_group_info().in_a_workplace, '%inCity': _in_city, '%inDepartement': _in_departement, '%inDomain': lambda scoring_project: scoring_project.job_group_info().in_domain, '%inRegion': _in_region, # TODO(pascal): Don't use Url as a prefix, as this makes %jobGroupName forbidden (no variable # can be the prefix of another variable). '%jobGroupNameUrl':
def test_empty(self) -> None: """Empty string.""" sentence = french.lower_first_letter('') self.assertEqual('', sentence)
def test_with_uppercase(self) -> None: """All upper case.""" sentence = french.lower_first_letter('This contains UPPERCASE') self.assertEqual('this contains UPPERCASE', sentence)
def _get_spontaneous_vars(user: user_pb2.User, now: datetime.datetime, database: Optional[pymongo.database.Database] = None, **unused_kwargs: Any) -> Optional[Dict[str, str]]: """Compute vars for a given user for the spontaneous email. Returns: a dict with all vars required for the template, or None if no email should be sent. """ project = user.projects[0] job_search_length = campaign.job_search_started_months_ago(project, now) if job_search_length < 0: logging.info('No info on user search duration') return None assert database job_group_info = jobs.get_group_proto(database, project.target_job.job_group.rome_id) if not job_group_info: logging.warning('Could not find job group info for "%s"', project.target_job.job_group.rome_id) return None 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) application_modes = job_group_info.application_modes if not any( _should_use_spontaneous(modes) for modes in application_modes.values()): return None contact_mode = job_group_info.preferred_application_medium if not contact_mode: logging.error('There is no contact mode for the job group "%s"', project.target_job.job_group.rome_id) return None in_a_workplace = job_group_info.in_a_workplace if not in_a_workplace and contact_mode != job_pb2.APPLY_BY_EMAIL: logging.error( 'There is no "in_a_workplace" field for the job group "%s".', project.target_job.job_group.rome_id) return None like_your_workplace = job_group_info.like_your_workplace if in_a_workplace and not like_your_workplace: logging.error( 'There is no "like_your_workplace" field for the job group "%s".', project.target_job.job_group.rome_id) return None to_the_workplace = job_group_info.to_the_workplace if not to_the_workplace: to_the_workplace = "à l'entreprise" some_companies = job_group_info.place_plural if not some_companies: some_companies = 'des entreprises' what_i_love_about = job_group_info.what_i_love_about if user.profile.gender == user_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: logging.error( 'There is no "What I love about" field for the job group "%s".', project.target_job.job_group.rome_id) return None why_specific_company = job_group_info.why_specific_company if not why_specific_company: logging.error( 'There is no "Why this specific company" field for the job group "%s".', project.target_job.job_group.rome_id) return None 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 dict( 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 imt_vars(user, database): """Compute vars for the "IMT" email.""" if not user.projects: logging.info('User has no project') return None project = user.projects[0] genderized_job_name = french.lower_first_letter( french.genderize_job(project.target_job, user.profile.gender)) departement_id = project.mobility.city.departement_id rome_id = project.target_job.job_group.rome_id diagnosis_key = '{}:{}'.format(departement_id, rome_id) local_diagnosis = _LOCAL_DIAGNOSIS.get_collection(database).get( diagnosis_key) if not local_diagnosis: logging.info('User market does not exist') return None imt = local_diagnosis.imt if not imt: logging.info('User market has no IMT data') return None shown_sections = 0 market_stress_section = _make_market_stress_section( imt.yearly_avg_offers_per_10_candidates) if market_stress_section: shown_sections += 1 application_modes_section = _make_application_mode_section( campaign.get_application_modes(rome_id, database), project.advices, user.user_id) if application_modes_section: shown_sections += 1 departements_section = _make_departements_section( project.mobility.city.departement_id, _get_best_departements_for_job_group(rome_id, database), project.mobility.area_type, database) if departements_section: shown_sections += 1 employment_types_section = _make_employment_type_section( sorted(imt.employment_type_percentages, key=lambda e: e.percentage)) if employment_types_section: shown_sections += 1 months_section = _make_months_section(imt.active_months) if months_section: shown_sections += 1 if shown_sections < 3: logging.info('Only %d section(s) to be shown for user.', shown_sections) return None imt_link = 'http://candidat.pole-emploi.fr/marche-du-travail/statistiques?' + \ 'codeMetier={}&codeZoneGeographique={}&typeZoneGeographique=DEPARTEMENT'.format( project.target_job.code_ogr, departement_id) job_name_in_departement = '{} {}'.format( genderized_job_name, geo.get_in_a_departement_text(database, project.mobility.city.departement_id)) return dict( campaign.get_default_vars(user), **{ 'applicationModes': _make_section(application_modes_section), 'departements': _make_section(departements_section), 'employmentType': _make_section(employment_types_section), 'imtLink': imt_link, 'inCity': french.in_city(project.mobility.city.name), 'jobNameInDepartement': job_name_in_departement, 'loginUrl': campaign.create_logged_url(user.user_id), 'marketStress': _make_section(market_stress_section), 'months': _make_section(months_section), 'ofJobNameInDepartement': french.maybe_contract_prefix('de ', "d'", job_name_in_departement), 'ofJobName': french.maybe_contract_prefix('de ', "d'", genderized_job_name), 'showPs': campaign.as_template_boolean( _can_go_to_arles_hotellerie_event(rome_id, project.mobility)), 'statusUpdateUrl': campaign.get_status_update_link(user.user_id, user.profile), })
def test_all_uppercase(self) -> None: """All upper case.""" sentence = french.lower_first_letter('THIS IS ALL UPPERCASE') self.assertEqual('THIS IS ALL UPPERCASE', sentence)
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 test_one_letter(self) -> None: """Only one letter.""" sentence = french.lower_first_letter('T') self.assertEqual('t', sentence)
def self_development_vars(user, unused_db=None): """Compute vars for a given user for the self-development email. Returns: a dict with all vars required for the template, or None if no email should be sent. """ if not user.projects: logging.info('User has no project') return None project = user.projects[0] registered_months_ago = campaign.get_french_months_ago( user.registered_at.ToDatetime()) if not registered_months_ago: logging.info('User registered only recently (%s)', user.registered_at) return None job_search_length = campaign.job_search_started_months_ago(project) if job_search_length < 0: logging.info('No info on user search duration') return None if job_search_length >= 12: logging.info('User has been searching for too long (%s)', job_search_length) return None 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 unsubscribe_token = parse.quote( auth.create_token(user.profile.email, role='unsubscribe')) max_young = 30 min_old = 50 return { 'firstName': french.cleanup_firstname(user.profile.name), 'gender': user_pb2.Gender.Name(user.profile.gender), 'hasEnoughExperience': campaign.as_template_boolean(project.seniority > project_pb2.JUNIOR), '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_pb2.FEMININE), 'isYoung': campaign.as_template_boolean(age <= max_young), 'isYoungNotWoman': campaign.as_template_boolean( age <= max_young and user.profile.gender != user_pb2.FEMININE), 'jobName': genderized_job_name, 'ofJobName': french.maybe_contract_prefix('de ', "d'", genderized_job_name), 'registeredMonthsAgo': registered_months_ago, 'unsubscribeLink': '{}/unsubscribe.html?email={}&auth={}'.format( campaign.BASE_URL, parse.quote(user.profile.email), unsubscribe_token), }
def test_acronym(self) -> None: """Starts with an acronym.""" sentence = french.lower_first_letter('SPA manager') self.assertEqual('SPA manager', sentence)
def spontaneous_vars(user, previous_email_campaign_id): """Compute vars for a given user for the spontaneous email. Returns: a dict with all vars required for the template, or None if no email should be sent. """ if not user.projects: logging.info('User has no project') return None project = user.projects[0] job_group_info = jobs.get_group_proto(_DB, project.target_job.job_group.rome_id) def _should_use_spontaneous(modes): return any(mode.mode == job_pb2.SPONTANEOUS_APPLICATION and mode.percentage > 20 for mode in modes.modes) application_modes = job_group_info.application_modes if not any( _should_use_spontaneous(modes) for modes in application_modes.values()): return None registered_months_ago = campaign.get_french_months_ago( user.registered_at.ToDatetime()) if not registered_months_ago: logging.warning('User registered only recently (%s)', user.registered_at) return None has_read_previous_email = previous_email_campaign_id and any( email.campaign_id == previous_email_campaign_id and email.status in (user_pb2.EMAIL_SENT_OPENED, user_pb2.EMAIL_SENT_CLICKED) for email in user.emails_sent) contact_mode = job_group_info.preferred_application_medium if not contact_mode: logging.error('There is no contact mode for the job group "%s"', project.target_job.job_group.rome_id) return None contact_mode = job_pb2.ApplicationMedium.Name(contact_mode).replace( 'APPLY_', '') in_a_workplace = job_group_info.in_a_workplace if not in_a_workplace and contact_mode != 'BY_EMAIL': logging.error( 'There is no "in_a_workplace" field for the job group "%s".', project.target_job.job_group.rome_id) return None like_your_workplace = job_group_info.like_your_workplace if in_a_workplace and not like_your_workplace: logging.error( 'There is no "like_your_workplace" field for the job group "%s".', project.target_job.job_group.rome_id) return None to_the_workplace = job_group_info.to_the_workplace if not to_the_workplace: to_the_workplace = "à l'entreprise" some_companies = job_group_info.place_plural if not some_companies: some_companies = 'des entreprises' what_i_love_about = job_group_info.what_i_love_about if user.profile.gender == user_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 == 'BY_EMAIL': logging.error( 'There is no "What I love about" field for the job group "%s".', project.target_job.job_group.rome_id) return None why_specific_company = job_group_info.why_specific_company if not why_specific_company: logging.error( 'There is no "Why this specific company" field for the job group "%s".', project.target_job.job_group.rome_id) return None at_various_companies = job_group_info.at_various_companies if project.weekly_applications_estimate == project_pb2.SOME: weekly_application_count = '5' elif project.weekly_applications_estimate > project_pb2.SOME: weekly_application_count = '15' else: weekly_application_count = '' survey_token = parse.quote( auth.create_token(user.user_id, role='employment-status')) unsubscribe_token = parse.quote( auth.create_token(user.profile.email, role='unsubscribe')) return { 'applicationComplexity': job_pb2.ApplicationProcessComplexity.Name( job_group_info.application_complexity), 'atVariousCompanies': at_various_companies, 'contactMode': contact_mode, 'deepLinkLBB': 'https://labonneboite.pole-emploi.fr/entreprises/commune/{}/rome/' '{}?utm_medium=web&utm_source=bob&utm_campaign=bob-email'.format( project.mobility.city.city_id, project.target_job.job_group.rome_id), 'emailInUrl': parse.quote(user.profile.email), 'experienceAsText': _EXPERIENCE_AS_TEXT.get(project.seniority, 'peu'), 'firstName': french.cleanup_firstname(user.profile.name), 'gender': user_pb2.Gender.Name(user.profile.gender), 'hasReadPreviousEmail': campaign.as_template_boolean(has_read_previous_email), '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, 'registeredMonthsAgo': registered_months_ago, 'someCompanies': some_companies, # TODO(cyrille): Use campaign.get_status_update_link 'statusUpdateUrl': '{}/statut/mise-a-jour?user={}&token={}&gender={}'.format( campaign.BASE_URL, user.user_id, survey_token, user_pb2.Gender.Name(user.profile.gender)), 'toTheWorkplace': to_the_workplace, 'unsubscribeLink': '{}/unsubscribe.html?email={}&auth={}'.format( campaign.BASE_URL, parse.quote(user.profile.email), unsubscribe_token), 'weeklyApplicationOptions': weekly_application_count, 'whatILoveAbout': what_i_love_about, 'whySpecificCompany': why_specific_company, }
def _get_imt_vars(user: user_pb2.User, database: Optional[pymongo.database.Database] = None, **unused_kwargs: Any) -> Optional[Dict[str, Any]]: """Compute vars for the "IMT" email.""" project = user.projects[0] assert database scoring_project = scoring.ScoringProject(project, user, database) genderized_job_name = french.lower_first_letter( french.genderize_job(project.target_job, user.profile.gender)) departement_id = project.city.departement_id rome_id = project.target_job.job_group.rome_id local_diagnosis = scoring_project.local_diagnosis() if not local_diagnosis.HasField('imt'): logging.info('User market has no IMT data') return None imt = local_diagnosis.imt shown_sections = [] market_stress_section = _make_market_stress_section( imt.yearly_avg_offers_per_10_candidates) if market_stress_section: shown_sections.append('marketStress') application_modes_section = _make_application_mode_section( scoring_project.get_best_application_mode(), project, user.user_id) if application_modes_section: shown_sections.append('applicationModes') departements_section = _make_departements_section( departement_id, _get_best_departements_for_job_group(rome_id, database), project.area_type, database) if departements_section: shown_sections.append('departements') employment_types_section = _make_employment_type_section( imt.employment_type_percentages) if employment_types_section: shown_sections.append('employmentTypes') months_section = _make_months_section(imt.active_months) if months_section: shown_sections.append('months') if len(shown_sections) < 3: logging.info('Only %d section(s) to be shown for user (%s).', len(shown_sections), shown_sections) return None imt_link = 'http://candidat.pole-emploi.fr/marche-du-travail/statistiques?' \ f'codeMetier={project.target_job.code_ogr}&codeZoneGeographique={departement_id}&' \ 'typeZoneGeographique=DEPARTEMENT' in_departement = geo.get_in_a_departement_text(database, departement_id) job_name_in_departement = f'{genderized_job_name} {in_departement}' return dict( campaign.get_default_coaching_email_vars(user), **{ 'applicationModes': _make_section(application_modes_section), 'departements': _make_section(departements_section), 'employmentType': _make_section(employment_types_section), 'imtLink': imt_link, 'inCity': french.in_city(project.city.name), 'jobNameInDepartement': job_name_in_departement, 'loginUrl': campaign.create_logged_url(user.user_id), 'marketStress': _make_section(market_stress_section), 'months': _make_section(months_section), 'ofJobNameInDepartement': french.maybe_contract_prefix('de ', "d'", job_name_in_departement), 'ofJobName': french.maybe_contract_prefix('de ', "d'", genderized_job_name), })
def _job_name(scoring_project): return french.lower_first_letter( french.genderize_job(scoring_project.details.target_job, scoring_project.user_profile.gender))