Пример #1
0
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
        }
    }
Пример #2
0
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)
    }
Пример #3
0
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),
    }
Пример #4
0
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)
    }
Пример #5
0
def _get_short_diploma_vars(user: user_pb2.User, *,
                            database: mongo.NoPiiMongoDatabase,
                            **unused_kwargs: Any) -> dict[str, Any]:

    if not user.projects:
        raise scoring.NotEnoughDataException('No project yet', {'projects.0'})

    project = user.projects[0]

    if user.projects[0].diagnostic.category_id != 'missing-diploma':
        raise campaign.DoNotSend('The user has no missing-diploma category')

    scoring_project = scoring.ScoringProject(project, user, database)

    login_url = campaign.create_logged_url(user.user_id,
                                           f'/projet/{project.project_id}')

    # TODO(sil): Let's check if this is needed to have access to the method.
    if not project.target_job.job_group.rome_id:
        raise scoring.NotEnoughDataException(
            'Need a job group to find trainings',
            # TODO(pascal): Use project_id instead of 0.
            {'projects.0.targetJob.jobGroup.romeId'})

    deep_link_training_url = \
        campaign.get_deep_link_advice(user.user_id, project, 'training')

    return campaign.get_default_coaching_email_vars(user) | {
        'deepTrainingAdviceUrl':
        deep_link_training_url,
        'ofJobName':
        scoring_project.populate_template('%ofJobName'),
        'productUrl':
        f'{login_url}?utm_source=bob-emploi&utm_medium=email',
    }
Пример #6
0
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)
Пример #7
0
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)
Пример #8
0
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)
Пример #9
0
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,
    }
Пример #10
0
def _get_jobflix_invite_vars(user: user_pb2.User,
                             **unused_kwargs: Any) -> dict[str, Any]:
    jobflix_default_vars = jobflix.get_default_vars(user)
    product_url = jobflix_default_vars['productUrl']
    # Keep the source sync with advisor/jobflix.tsx.
    source = 'dwp' if user.origin.source == 'dwp' else 'bob'
    return campaign.get_default_coaching_email_vars(user) | {
        'senderName': jobflix_default_vars['senderFirstName'],
        'sideProductName': jobflix_default_vars['productName'],
        'sideProductUrl':
        f'{product_url}?utm_source={source}&utm_medium=email',
        'statusUpdateUrl': campaign.get_status_update_link(user)
    }
Пример #11
0
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,
    }
Пример #12
0
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),
    }
Пример #13
0
def _get_galita3_vars(user: user_pb2.User,
                      **unused_kwargs: Any) -> dict[str, str]:
    if user_profile_pb2.NO_OFFER_ANSWERS not in user.profile.frustrations:
        raise campaign.DoNotSend(
            'User is getting enough answers from recruiters.')
    # We set a string with a blank as this is the only way to exclude a section
    # on Passport except to check equality or inequality with a non-empty
    # string.
    deep_link_to_follow_up_advice = ' '
    if user.projects:
        for project in user.projects:
            link = campaign.get_deep_link_advice(user.user_id, project,
                                                 'follow-up')
            if link:
                deep_link_to_follow_up_advice = link
    return campaign.get_default_coaching_email_vars(user) | {
        'deepLinkToAdvice': deep_link_to_follow_up_advice,
    }
def _get_prepare_your_application_vars(user: user_pb2.User, **unused_kwargs: Any) -> dict[str, Any]:
    """Compute vars for the "Prepare your application" email."""

    if not user.projects:
        raise scoring.NotEnoughDataException('No project yet', {'projects.0'})

    project = user.projects[0]

    deep_link_motivation_email_url = \
        campaign.get_deep_link_advice(user.user_id, project, 'motivation-email')

    return campaign.get_default_coaching_email_vars(user) | {
        'deepLinkMotivationEmailUrl': deep_link_motivation_email_url,
        'hasInterviewFrustration':
        campaign.as_template_boolean(user_profile_pb2.INTERVIEW in user.profile.frustrations),
        'hasSelfConfidenceFrustration':
        campaign.as_template_boolean(user_profile_pb2.SELF_CONFIDENCE in user.profile.frustrations),
        'loginUrl': campaign.create_logged_url(user.user_id, f'/projet/{project.project_id}'),
    }
Пример #15
0
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,
    }
Пример #16
0
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),
    }
Пример #17
0
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],
    }
Пример #18
0
    def send_update_confirmation(self, user_dict: dict[str, Any]) -> None:
        """Sends an email to the user that confirms password change."""

        user_id = str(user_dict['_id'])
        if not user_id:
            return
        auth_link = token.create_logged_url(user_id)
        reset_link = self._get_reset_password_link(user_dict)
        if not reset_link or not auth_link:
            return
        user = proto.create_from_mongo(user_dict.copy(), user_pb2.User)
        template_vars = dict(campaign.get_default_coaching_email_vars(user),
                             authLink=auth_link,
                             resetPwdLink=reset_link)
        # TODO(cyrille): Create a static Campaign object and use it.
        result = mail_send.send_template('send-pwd-update-confirmation',
                                         user.profile, template_vars)
        if result.status_code != 200:
            logging.error('Failed to send an email with MailJet:\n %s',
                          result.text)
            flask.abort(result.status_code)
Пример #19
0
def _get_post_covid_vars(user: user_pb2.User, *,
                         database: mongo.NoPiiMongoDatabase,
                         **unused_kwargs: Any) -> dict[str, str]:
    if not user.projects:
        raise scoring.NotEnoughDataException(
            'Project is required.', fields={'user.projects.0.advices'})
    project = user.projects[0]
    scoring_project = scoring.ScoringProject(project, user, database)
    if scoring_project.job_group_info().covid_risk != job_pb2.COVID_RISKY:
        raise campaign.DoNotSend("The user's project job is not covid risky.")
    try:
        network_advice_link = next(
            campaign.get_deep_link_advice(user.user_id, project, a.advice_id)
            for a in project.advices
            if a.advice_id.startswith('network-application'))
    except StopIteration:
        raise campaign.DoNotSend('No network-application advice found for the user.')\
            from None
    return campaign.get_default_coaching_email_vars(user) | {
        'deepLinkAdviceUrl': network_advice_link,
        'ofJobName': scoring_project.populate_template('%ofJobName'),
    }
def _get_prepare_your_application_short_vars(user: user_pb2.User, **unused_kwargs: Any)\
        -> dict[str, Any]:
    """Compute vars for the "Prepare your application short" email."""

    if not user.projects:
        raise scoring.NotEnoughDataException('No project yet', {'projects.0'})

    if (user.profile.locale or 'fr').startswith('fr'):
        advice_page_url = 'https://labonneboite.pole-emploi.fr/comment-faire-une-candidature-spontanee'
    elif user.profile.locale.startswith('en'):
        advice_page_url = 'https://www.theguardian.com/careers/speculative-applications'
    else:
        logging.warning(
            'No advice webpage given for campaign spontaneous-short in "%s"', user.profile.locale)
        advice_page_url = ''

    return campaign.get_default_coaching_email_vars(user) | {
        'advicePageUrl': advice_page_url,
        'hasInterviewFrustration':
        campaign.as_template_boolean(user_profile_pb2.INTERVIEW in user.profile.frustrations),
        'hasSelfConfidenceFrustration':
        campaign.as_template_boolean(user_profile_pb2.SELF_CONFIDENCE in user.profile.frustrations),
    }
Пример #21
0
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,
    }
Пример #22
0
    if project.target_job.name:
        of_job_name = scoring_project.populate_template('%ofJobName')
        genderized_job_name = scoring_project.populate_template('%jobName')
    else:
        of_job_name = scoring_project.translate_static_string('dans votre domaine')

        # This variable should be included in the following sentence:
        # "des personnes qui travaillent comme {{var:jobNameInDepartement}} ont décroché leur poste"
        # That'd make a weird but ok sentence and should not happened anyway as this block relies
        # on job info.
        genderized_job_name = scoring_project.translate_static_string('vous')

    job_name_in_departement = f'{genderized_job_name} {in_departement}'
    of_job_name_in_departement = f'{of_job_name} {in_departement}'

    return campaign.get_default_coaching_email_vars(user) | {
        'applicationModes': _make_section(application_modes_section),
        'departements': _make_section(departements_section),
        'employmentType': _make_section(employment_types_section),
        'imtLink': imt_link,
        'inCity': scoring_project.populate_template('%inCity'),
        'jobNameInDepartement': job_name_in_departement,
        'loginUrl': campaign.create_logged_url(user.user_id),
        'marketStress': _make_section(market_stress_section),
        'months': _make_section(months_section),
        'ofJobNameInDepartement': of_job_name_in_departement,
        'ofJobName': of_job_name,
    }


campaign.register_campaign(campaign.Campaign(
Пример #23
0
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)
    }
Пример #24
0
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,
    }
Пример #25
0
def _get_confidence_boost_vars(user: user_pb2.User,
                               **unused_kwargs: Any) -> dict[str, Any]:
    return campaign.get_default_coaching_email_vars(user)
Пример #26
0
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,
    }
Пример #27
0
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),
    }