コード例 #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_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)
コード例 #3
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)
コード例 #4
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,
    }
コード例 #5
0
def _employment_vars(
    user: user_pb2.User,
    *,
    now: datetime.datetime,
    database: mongo.NoPiiMongoDatabase,
    **unused_kwargs: Any,
) -> dict[str, str]:
    """Computes vars for a given user for the employment survey.

    Returns a dict with all vars required for the template.
    """

    num_months_ago = round((now - user.registered_at.ToDatetime()).days / 30.5)
    if num_months_ago <= 0 and not user.features_enabled.alpha:
        raise campaign.DoNotSend(
            f'User registered only recently ({user.registered_at})')

    scoring_project = scoring.ScoringProject(project_pb2.Project(), user,
                                             database)
    registered_since = scoring_project.get_several_months_text(num_months_ago)

    for status in user.employment_status:
        if status.created_at.ToDatetime() > _ONE_MONTH_AGO:
            raise campaign.DoNotSend(
                'User has already updated their employment status less than one month ago.'
            )
    base_params = {
        'user':
        user.user_id,
        'token':
        parse.quote(
            auth_token.create_token(user.user_id, role='employment-status')),
    }
    return campaign.get_default_vars(user) | {
        'registeredSince':
        registered_since,
        'seekingUrl':
        campaign.get_bob_link(
            '/api/employment-status', base_params | {
                'seeking': user_pb2.SeekingStatus.Name(user_pb2.STILL_SEEKING),
                'redirect': campaign.get_bob_link('/statut/en-recherche'),
            }),
        'stopSeekingUrl':
        campaign.get_bob_link(
            '/api/employment-status', base_params | {
                'seeking': user_pb2.SeekingStatus.Name(user_pb2.STOP_SEEKING),
                'redirect': campaign.get_bob_link('/statut/ne-recherche-plus'),
            }),
    }
コード例 #6
0
ファイル: holiday.py プロジェクト: bayesimpact/bob-emploi
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),
    }
コード例 #7
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)
    }
コード例 #8
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)
    }
コード例 #9
0
ファイル: training.py プロジェクト: bayesimpact/bob-emploi
def _get_short_diploma_vars(user: user_pb2.User, *,
                            database: mongo.NoPiiMongoDatabase,
                            **unused_kwargs: Any) -> dict[str, Any]:

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

    project = user.projects[0]

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

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

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

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

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

    return campaign.get_default_coaching_email_vars(user) | {
        'deepTrainingAdviceUrl':
        deep_link_training_url,
        'ofJobName':
        scoring_project.populate_template('%ofJobName'),
        'productUrl':
        f'{login_url}?utm_source=bob-emploi&amp;amp;utm_medium=email',
    }
コード例 #10
0
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}',
    }
コード例 #11
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),
    }
コード例 #12
0
def _get_ffs_vars(
    user: user_pb2.User,
    *,
    now: datetime.datetime,
    **unused_kwargs: Any,
) -> dict[str, str]:
    user_id = user.user_id
    days_since_registered = (now - user.registered_at.ToDatetime()).days

    if user.net_promoter_score_survey_response.score:
        raise campaign.DoNotSend('User already answered the NPS survey')

    is_alpha = user.features_enabled.alpha
    if (days_since_registered < 6
            or days_since_registered > 13) and not is_alpha:
        raise campaign.DoNotSend(
            'User registered too long ago or too recently')

    main_challenge_id = user.projects[
        0].diagnostic.category_id if user.projects else ''
    return campaign.get_default_vars(user) | {
        'buttonBackgroundColor':
        '#58bbfb',
        'buttonTextColor':
        '#ffffff',
        'ffsFormUrl':
        campaign.get_bob_link(
            '/api/first-followup-survey', {
                'user':
                user_id,
                'token':
                auth_token.create_token(user_id, 'first-followup-survey'),
                'redirect':
                campaign.get_bob_link(
                    '/first-followup-survey', {
                        'hl': user.profile.locale,
                        'gender': user_profile_pb2.Gender.Name(
                            user.profile.gender),
                        'mainChallenge': main_challenge_id,
                    }),
            }),
    }
コード例 #13
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)
コード例 #14
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'),
    }
コード例 #15
0
def _get_first_eval_reminder_vars(
    user: user_pb2.User,
    *,
    now: datetime.datetime,
    **unused_kwargs: Any,
) -> dict[str, Any]:
    if not any(email.campaign_id == 'jobflix-first-eval'
               for email in user.emails_sent):
        raise campaign.DoNotSend(
            'Only useful for user that have received the first campaign')
    next_week = now + datetime.timedelta(days=7)
    return campaign.get_default_vars(user) | {
        'closingDate': next_week.strftime('%A %d %B'),
    }
コード例 #16
0
ファイル: jobbing.py プロジェクト: bayesimpact/bob-emploi
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,
    }
コード例 #17
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,
    }
コード例 #18
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,
    }
コード例 #19
0
ファイル: training.py プロジェクト: bayesimpact/bob-emploi
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],
    }
コード例 #20
0
ファイル: network.py プロジェクト: bayesimpact/bob-emploi
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),
    }
コード例 #21
0
ファイル: network.py プロジェクト: bayesimpact/bob-emploi
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,
    }
コード例 #22
0
def _get_first_eval_vars(user: user_pb2.User,
                         **unused_kwargs: Any) -> dict[str, Any]:
    if not _is_bob_jobflix_user(user):
        raise campaign.DoNotSend('Only interesting for Bob-Jobflix users.')
    return campaign.get_default_vars(user)
コード例 #23
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,
    }
コード例 #24
0
ファイル: activation.py プロジェクト: bayesimpact/bob-emploi
def _get_vars(user: user_pb2.User, *, now: datetime.datetime,
              database: mongo.NoPiiMongoDatabase,
              **unused_kwargs: Any) -> dict[str, str]:

    if not user.projects or not user.projects[0].actions:
        raise campaign.DoNotSend('User has no project or no actions yet')

    project = user.projects[0]

    most_recent_date = max(d.ToDatetime()
                           for d in (user.registered_at, project.created_at,
                                     project.action_plan_started_at))
    if (now - most_recent_date).days > 7:
        raise campaign.DoNotSend(
            'User has registered a while ago, too late to send the activation')

    # Set locale.
    user_locale = user.profile.locale.split('@', 1)[0]
    date_format = '%d %B %Y'
    if user_locale == 'fr' or not user_locale:
        locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8')
    elif user_locale == 'en_UK':
        locale.setlocale(locale.LC_ALL, 'en_GB.UTF-8')
        date_format = '%B %d %Y'
    elif user_locale == 'en':
        locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
        date_format = '%B %d %Y'
    else:
        logging.exception('Sending an email with an unknown locale: %s',
                          user_locale)

    scoring_project = scoring.ScoringProject(project, user, database, now=now)
    auth_token = parse.quote(
        token.create_token(user.user_id, is_using_timestamp=True))
    settings_token = parse.quote(
        token.create_token(user.user_id, role='settings'))
    coaching_email_frequency_name = \
        email_pb2.EmailFrequency.Name(user.profile.coaching_email_frequency)
    # This uses tutoiement by default, because its content adressed from the user to a third party
    # (that we assume the user is familiar enough with), not from Bob to the user.
    virality_template = parse.urlencode({
        'body':
        scoring_project.translate_static_string(
            'Salut,\n\n'
            "Est-ce que tu connais Bob\u00A0? C'est un site qui propose de t'aider dans ta "
            "recherche d'emploi en te proposant un diagnostic et des conseils personnalisés. "
            'Tu verras, ça vaut le coup\u00A0: en 15 minutes, tu en sauras plus sur où tu en es, '
            'et ce que tu peux faire pour avancer plus efficacement. '
            "Et en plus, c'est gratuit\u00A0!\n\n"
            '{invite_url}\n\n'
            'En tous cas, bon courage pour la suite,\n\n'
            '{first_name}', ).format(invite_url=parse.urljoin(
                product.bob.base_url, 'invite#vm2m'),
                                     first_name=user.profile.name),
        'subject':
        scoring_project.translate_static_string("Ça m'a fait penser à toi"),
    })
    change_email_settings_url = parse.urljoin(
        product.bob.base_url,
        'unsubscribe.html?' + parse.urlencode({
            'user': user.user_id,
            'auth': settings_token,
            'coachingEmailFrequency': coaching_email_frequency_name,
            'hl': user.profile.locale,
        }))
    team_members = (
        'Tabitha',
        'Paul',
        'John',
        'Pascal',
        'Sil',
        'Cyrille',
        'Flo',
        'Nicolas',
        'Florian',
        'Lillie',
        'Benjamin',
        'Émilie',
    )

    # Selected and pending actions.
    highlighted_actions = [
        action for action in project.actions
        if action.status == action_pb2.ACTION_CURRENT
    ]
    if len(highlighted_actions) < 2:
        highlighted_actions.extend(
            action for action in project.actions
            if action.status == action_pb2.ACTION_UNREAD)
    else:
        highlighted_actions = sorted(
            highlighted_actions,
            key=lambda action: action.expected_completion_at.ToDatetime())
    actions = [{
        'title':
        action.title,
        'url':
        parse.urljoin(
            product.bob.base_url,
            f'/projet/{project.project_id}/action/{action.action_id}'
            f'?userId={user.user_id}&authToken={auth_token}')
    } for action in highlighted_actions[:2]]

    data: dict[str, Any] = campaign.get_default_vars(user)
    data |= {
        'actions':
        actions,
        'changeEmailSettingsUrl':
        change_email_settings_url,
        'coachingEmailFrequency':
        email_pb2.EmailFrequency.Name(user.profile.coaching_email_frequency)
        if user.profile.coaching_email_frequency and
        user.profile.coaching_email_frequency != email_pb2.EMAIL_NONE else '',
        'date':
        now.strftime(date_format),
        'firstTeamMember':
        team_members[0],
        'isActionPlanCompleted':
        project.HasField('action_plan_started_at'),
        'isCoachingEnabled':
        'True' if user.profile.coaching_email_frequency
        and user.profile.coaching_email_frequency != email_pb2.EMAIL_NONE else
        '',
        'loginUrl':
        parse.urljoin(product.bob.base_url,
                      f'?userId={user.user_id}&authToken={auth_token}'),
        'numActions':
        len(actions),
        'numberUsers':
        '270\u00A0000',
        'numberTeamMembers':
        len(team_members),
        'ofJob':
        scoring_project.populate_template('%ofJobName',
                                          raise_on_missing_var=True)
        if project.target_job.name else '',
        'teamMembers':
        ', '.join(team_members[1:]),
        'viralityTemplate':
        f'mailto:?{virality_template}'
    }
    return data
コード例 #25
0
ファイル: holiday.py プロジェクト: bayesimpact/bob-emploi
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),
    }
コード例 #26
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)
    }