コード例 #1
0
ファイル: evaluation.py プロジェクト: bayesimpact/bob-emploi
def _match_filters_for_use_case(
        filters: Iterable[str], use_case: use_case_pb2.UseCase) -> bool:
    user = use_case.user_data
    if not user.projects:
        return False
    scoring_project = scoring.ScoringProject(
        user.projects[0], user, _get_eval_db())
    return scoring_project.check_filters(filters, force_exists=True)
コード例 #2
0
ファイル: user.py プロジェクト: bayesimpact/bob-emploi
def get_scoring_project(user_id: str,
                        project_id: str) -> scoring.ScoringProject:
    """Get the scoring project or abort."""

    user_proto = get_user_data(user_id)
    project = get_project_data(user_proto, project_id)
    return scoring.ScoringProject(project,
                                  user_proto,
                                  mongo.get_connections_from_env().stats_db,
                                  now=now.get())
コード例 #3
0
    def scoring_project(
            self,
            database: pymongo.database.Database,
            now: Optional[datetime.datetime] = None) -> scoring.ScoringProject:
        """Creates a new scoring.ScoringProject for this persona."""

        return scoring.ScoringProject(project=self.project,
                                      user=self._user,
                                      database=database,
                                      now=now)
コード例 #4
0
    def test_string_representation(self) -> None:
        """A scoring project can be represented as a meaningful string."""

        user = user_pb2.User()
        user.profile.gender = user_pb2.MASCULINE
        user.features_enabled.alpha = True
        project = project_pb2.Project(title='Developpeur web a Lyon')
        project_str = str(scoring.ScoringProject(project, user, None))
        self.assertIn(str(user.profile), project_str)
        self.assertIn(str(project), project_str)
        self.assertIn(str(user.features_enabled), project_str)
コード例 #5
0
    def test_personal_string(self) -> None:
        """A scoring model string does not show personal identifiers."""

        user = user_pb2.User()
        user.profile.name = 'Cyrille'
        user.features_enabled.alpha = True
        project = project_pb2.Project(project_id='secret-project')
        project_str = str(scoring.ScoringProject(project, user, None))
        self.assertNotIn('Cyrille', project_str)
        self.assertNotIn('secret-project', project_str)
        self.assertEqual('Cyrille', user.profile.name)
        self.assertEqual('secret-project', project.project_id)
コード例 #6
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,
    }
コード例 #7
0
class FilterUsingScoreTestCase(unittest.TestCase):
    """Unit tests for the filter_using_score function."""

    dummy_project = scoring.ScoringProject(project_pb2.Project(),
                                           user_pb2.User(),
                                           mongomock.MongoClient().test)

    @classmethod
    def setUpClass(cls) -> None:
        """Test setup."""

        super().setUpClass()
        scoring.SCORING_MODELS['test-zero'] = scoring.ConstantScoreModel('0')
        scoring.SCORING_MODELS['test-two'] = scoring.ConstantScoreModel('2')

    def test_filter_list_with_no_filters(self) -> None:
        """Filter a list with no filters to apply."""

        filtered = scoring.filter_using_score(range(5), lambda a: [],
                                              self.dummy_project)
        self.assertEqual([0, 1, 2, 3, 4], list(filtered))

    def test_filter_list_constant_scorer(self) -> None:
        """Filter a list returning constant scorer."""

        get_scoring_func = mock.MagicMock()
        get_scoring_func.side_effect = [['test-zero'], ['test-two'],
                                        ['test-zero']]
        filtered = scoring.filter_using_score(range(3), get_scoring_func,
                                              self.dummy_project)
        self.assertEqual([1], list(filtered))

    def test_unknown_filter(self) -> None:
        """Filter an item with an unknown filter."""

        get_scoring_func = mock.MagicMock()
        get_scoring_func.return_value = ['unknown-filter']
        filtered = scoring.filter_using_score([42], get_scoring_func,
                                              self.dummy_project)
        self.assertEqual([42], list(filtered))

    def test_multiple_filters(self) -> None:
        """Filter an item with multiple filters."""

        get_scoring_func = mock.MagicMock()
        get_scoring_func.return_value = ['test-two', 'test-zero']
        filtered = scoring.filter_using_score([42], get_scoring_func,
                                              self.dummy_project)
        self.assertEqual([], list(filtered))
コード例 #8
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'),
            }),
    }
コード例 #9
0
def _get_expanded_card_data(user_proto, project, advice_id):
    module = advisor.get_advice_module(advice_id, _DB)
    if not module or not module.trigger_scoring_model:
        flask.abort(404, 'Le module "{}" n\'existe pas'.format(advice_id))
    model = scoring.get_scoring_model(module.trigger_scoring_model)
    if not model or not hasattr(model, 'get_expanded_card_data'):
        flask.abort(
            404, 'Le module "{}" n\'a pas de données supplémentaires'.format(
                advice_id))

    scoring_project = scoring.ScoringProject(project,
                                             user_proto.profile,
                                             user_proto.features_enabled,
                                             _DB,
                                             now=now.get())
    return model.get_expanded_card_data(scoring_project)
コード例 #10
0
def get_sections_for_project(user_proto: user_pb2.User) -> upskilling_pb2.Sections:
    """Return all the sections to browse."""

    if not user_proto.projects:
        flask.abort(422, i18n.flask_translate("Il n'y a pas de projet à explorer."))
    project = user_proto.projects[0]
    database = mongo.get_connections_from_env().stats_db
    scoring_project = scoring.ScoringProject(project, user_proto, database)

    result = upskilling_pb2.Sections()

    good_jobs = jobs.get_all_good_job_group_ids(scoring_project.database)
    best_salaries = {
        job.job_group.rome_id for job in _get_best_jobs_in_area(scoring_project).best_salaries_jobs}
    slots = list(_SECTION_SLOTS.get_collection(database))
    are_all_jobs_hiring = _get_are_all_jobs_hiring()
    for section in slots:
        if section.is_for_alpha_only and not user_proto.features_enabled.alpha:
            continue
        generator_id = section.generator
        try:
            generator = _SECTION_GENERATORS[generator_id]
        except KeyError:
            logging.error('Unknown upskilling section generator "%s"', generator_id)
            continue
        computed_section = generator.get_jobs(
            scoring_project=scoring_project, allowed_job_ids=good_jobs,
            previous_sections={
                section.id
                for section in result.sections
                if section.state.startswith(f'{generator_id}:')
            })
        if not computed_section or len(computed_section.jobs) < 2:
            continue
        result.sections.add(
            id=computed_section.new_id or generator_id,
            state=f'{generator_id}:{computed_section.state or ""}',
            name=scoring_project.populate_template(scoring_project.translate_key_string(
                f'jobflix_sections:{computed_section.new_id or generator_id}',
                hint=computed_section.new_name or generator.name,
                context=_get_bob_deployment(), is_hint_static=True)),
            jobs=[
                _add_perks_to_job(job, best_salaries, is_hiring=are_all_jobs_hiring)
                for job in computed_section.jobs],
        )

    return result
コード例 #11
0
def _send_activation_email(user: user_pb2.User, project: project_pb2.Project,
                           database: pymongo_database.Database,
                           base_url: str) -> None:
    """Send an email to the user just after we have defined their diagnosis."""

    if '@' not in user.profile.email:
        return

    # Set locale.
    locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8')

    scoring_project = scoring.ScoringProject(project,
                                             user,
                                             database,
                                             now=now.get())
    auth_token = parse.quote(
        auth.create_token(user.user_id, is_using_timestamp=True))
    settings_token = parse.quote(
        auth.create_token(user.user_id, role='settings'))
    coaching_email_frequency_name = \
        user_pb2.EmailFrequency.Name(user.profile.coaching_email_frequency)
    data = {
        'changeEmailSettingsUrl':
        f'{base_url}/unsubscribe.html?user={user.user_id}&auth={settings_token}&'
        f'coachingEmailFrequency={coaching_email_frequency_name}&'
        f'hl={parse.quote(user.profile.locale)}',
        'date':
        now.get().strftime('%d %B %Y'),
        'firstName':
        user.profile.name,
        'gender':
        user_pb2.Gender.Name(user.profile.gender),
        'isCoachingEnabled':
        'True' if user.profile.coaching_email_frequency
        and user.profile.coaching_email_frequency != user_pb2.EMAIL_NONE else
        '',
        'loginUrl':
        f'{base_url}?userId={user.user_id}&authToken={auth_token}',
        'ofJob':
        scoring_project.populate_template('%ofJobName',
                                          raise_on_missing_var=True),
    }
    # https://app.mailjet.com/template/636862/build
    response = mail.send_template('636862', user.profile, data)
    if response.status_code != 200:
        logging.warning('Error while sending diagnostic email: %s\n%s',
                        response.status_code, response.text)
コード例 #12
0
ファイル: advisor.py プロジェクト: bayesimpact/bob-emploi
def _maybe_recommend_advice(user: user_pb2.User, project: project_pb2.Project,
                            database: mongo.NoPiiMongoDatabase) -> bool:
    if user.features_enabled.advisor == features_pb2.CONTROL:
        return False
    scoring_project = scoring.ScoringProject(project,
                                             user,
                                             database,
                                             now=now.get())
    if user.features_enabled.action_plan == features_pb2.ACTIVE and not project.actions:
        compute_actions_for_project(scoring_project)
    if project.advices:
        return False
    advices = compute_advices_for_project(scoring_project)
    for piece_of_advice in advices.advices:
        piece_of_advice.status = project_pb2.ADVICE_RECOMMENDED
    project.advices.extend(advices.advices[:])
    return True
コード例 #13
0
    def test_jobgroup_info_fr_locale(self) -> None:
        """A scoring project can be represented as a meaningful string."""

        user = user_pb2.User()
        user.profile.locale = ''
        database = mongomock.MongoClient().test
        database.job_group_info.insert_many([{
            '_id': 'A1234',
            'name': 'french'
        }, {
            '_id': 'nl:A1234',
            'name': 'dutch'
        }])
        project = project_pb2.Project(target_job=job_pb2.Job(
            job_group=job_pb2.JobGroup(rome_id='A1234')))
        scoring_project = scoring.ScoringProject(project, user, database)
        job_group_info = scoring_project.job_group_info()
        self.assertEqual('french', job_group_info.name)
コード例 #14
0
def strategize(
        user: user_pb2.User, project: project_pb2.Project, database: pymongo.database.Database) \
        -> None:
    """Make strategies for the user."""

    scoring_project = scoring.ScoringProject(project, user, database)
    advice_scores = {a.advice_id: a.num_stars for a in project.advices}
    category_modules = _get_strategy_modules_by_category(database).get(
        project.diagnostic.category_id, [])
    for module in category_modules:
        _make_strategy(scoring_project, module, advice_scores)
    scoring_project.details.strategies.sort(key=lambda s: -s.score)
    if not scoring_project.details.strategies:
        if category_modules:
            logging.warning(
                'We could not find *any* strategy for a project:\n%s',
                scoring_project)
        return
    scoring_project.details.strategies[0].is_principal = True
コード例 #15
0
def _create_mock_scoring_project() -> scoring.ScoringProject:
    """Create a mock scoring_project."""

    _db = mongo.NoPiiMongoDatabase(mongomock.MongoClient().test)
    _db.job_group_info.insert_one({
        '_id': 'A1234',
        'requirements': {
            'diplomas': [{
                'name': 'Bac+2'
            }],
        },
    })
    user = user_pb2.User()
    project = project_pb2.Project()
    project.target_job.job_group.rome_id = 'A1234'
    project.created_at.FromDatetime(datetime.datetime.now())
    project.job_search_started_at.FromDatetime(datetime.datetime.now() -
                                               datetime.timedelta(days=30))
    return scoring.ScoringProject(project, user, database=_db)
コード例 #16
0
def get_more_jobs(
        user_proto: user_pb2.User, *, section_id: str, state: str) -> upskilling_pb2.Section:
    """Return more jobs for a given section."""

    if not user_proto.projects:
        flask.abort(422, i18n.flask_translate("Il n'y a pas de projet à explorer."))

    try:
        generator_id, section_state = state.split(':', 1)
    except ValueError:
        flask.abort(
            422,
            i18n.flask_translate("Le paramètre d'état {state} n'a pas le bon format.")
            .format(state=state))

    project = user_proto.projects[0]
    database = mongo.get_connections_from_env().stats_db
    scoring_project = scoring.ScoringProject(project, user_proto, database)

    try:
        generator = _SECTION_GENERATORS[generator_id]
    except KeyError:
        flask.abort(
            404,
            i18n.flask_translate('Générateur de section inconnu: {generator_id}')
            .format(generator_id=generator_id))

    try:
        section = generator.get_more_jobs(
            scoring_project=scoring_project, section_id=section_id, state=section_state)
    except _InvalidState:
        flask.abort(
            422,
            i18n.flask_translate('Impossible de commencer à {start_from}')
            .format(start_from=section_state))
    best_jobs_in_area = _get_best_jobs_in_area(scoring_project)
    are_all_jobs_hiring = _get_are_all_jobs_hiring()
    best_salaries = {
        job.job_group.rome_id for job in best_jobs_in_area.best_salaries_jobs}
    for job in section.jobs:
        _add_perks_to_job(job, best_salaries, is_hiring=are_all_jobs_hiring)
    return section
コード例 #17
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],
    }
コード例 #18
0
def diagnose(user, project, database):
    """Diagnose a project.

    Args:
        user: the user's data, mainly used for their profile and features_enabled.
        project: the project data. It will be modified, as its diagnostic field
            will be populated.
        database: access to the MongoDB with market data.
        diagnostic: a protobuf to fill, if none it will be created.
    Returns:
        the modified diagnostic protobuf.
    """

    diagnostic = project.diagnostic

    scoring_project = scoring.ScoringProject(project,
                                             user.profile,
                                             user.features_enabled,
                                             database,
                                             now=now.get())
    return diagnose_scoring_project(scoring_project, diagnostic)
コード例 #19
0
def diagnose(
        user: user_pb2.User, project: project_pb2.Project, database: mongo.NoPiiMongoDatabase) \
        -> diagnostic_pb2.Diagnostic:
    """Diagnose a project.

    Args:
        user: the user's data, mainly used for their profile and features_enabled.
        project: the project data. It will be modified, as its diagnostic field
            will be populated.
        database: access to the MongoDB with market data.
    Returns:
        the modified diagnostic protobuf.
    """

    diagnostic = project.diagnostic

    scoring_project = scoring.ScoringProject(project,
                                             user,
                                             database,
                                             now=now.get())
    return diagnose_scoring_project(scoring_project, diagnostic)
コード例 #20
0
def list_all_tips(
        user: user_pb2.User, project: project_pb2.Project,
        piece_of_advice: project_pb2.Advice,
        database: pymongo_database.Database
) -> List[action_pb2.ActionTemplate]:
    """List all available tips for a piece of advice.

    Args:
        user: the full user info.
        project: the project to give tips for.
        piece_of_advice: the piece of advice to give tips for.
        database: access to the database to get modules and tips.
    Returns:
        An iterable of tips for this module.
    """

    try:
        module = next(m for m in _advice_modules(database)
                      if m.advice_id == piece_of_advice.advice_id)
    except StopIteration:
        logging.warning('Advice module %s does not exist anymore',
                        piece_of_advice.advice_id)
        return []

    # Get tip templates.
    all_tip_templates = _tip_templates(database)
    tip_templates = filter(None, (all_tip_templates.get(t)
                                  for t in module.tip_template_ids))

    # Filter tips.
    scoring_project = scoring.ScoringProject(project,
                                             user,
                                             database,
                                             now=now.get())
    filtered_tips = scoring.filter_using_score(tip_templates,
                                               lambda t: t.filters,
                                               scoring_project)

    return [_translate_tip(tip, scoring_project) for tip in filtered_tips]
コード例 #21
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'),
    }
コード例 #22
0
def diagnose(
        user: user_pb2.User, project: project_pb2.Project, database: pymongo.database.Database) \
        -> Tuple[diagnostic_pb2.Diagnostic, Optional[List[int]]]:
    """Diagnose a project.

    Args:
        user: the user's data, mainly used for their profile and features_enabled.
        project: the project data. It will be modified, as its diagnostic field
            will be populated.
        database: access to the MongoDB with market data.
        diagnostic: a protobuf to fill, if none it will be created.
    Returns:
        the modified diagnostic protobuf.
    """

    diagnostic = project.diagnostic

    scoring_project = scoring.ScoringProject(project,
                                             user,
                                             database,
                                             now=now.get())
    return diagnose_scoring_project(scoring_project, diagnostic)
コード例 #23
0
 def setUp(self):
     super(PopulateProjectTemplateTest, self).setUp()
     # Pre-populate project's fields that are usualldy set. Individual tests
     # should not count on those values.
     self.project = project_pb2.Project()
     self.project.target_job.name = 'Boulanger / Boulangère'
     self.project.target_job.masculine_name = 'Boulanger'
     self.project.target_job.feminine_name = 'Boulangère'
     self.project.target_job.job_group.rome_id = 'Z9007'
     self.project.mobility.city.city_id = '69123'
     self.project.mobility.city.departement_id = '69'
     self.project.mobility.city.region_id = '84'
     self.project.mobility.city.postcodes = '69001-69002-69003-69004'
     self.project.mobility.city.name = 'Lyon'
     self.database = mongomock.MongoClient().test
     self.database.regions.insert_one({
         '_id': '84',
         'prefix': 'en ',
         'name': 'Auvergne-Rhône-Alpes',
     })
     self.scoring_project = scoring.ScoringProject(
         self.project, user_pb2.UserProfile(), user_pb2.Features(), self.database)
コード例 #24
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,
    }
コード例 #25
0
ファイル: jobbing.py プロジェクト: b3rday/bob-emploi
def _get_jobbing_vars(user: user_pb2.User,
                      database: Optional[pymongo.database.Database] = None,
                      **unused_kwargs: Any) -> Optional[Dict[str, Any]]:
    """Compute vars for the "Jobbing" email."""

    project = user.projects[0]

    if not any(s.strategy_id == 'diploma-free-job'
               for s in project.opened_strategies):
        return None

    assert database
    scoring_project = scoring.ScoringProject(project, user, database)
    model = scoring.get_scoring_model('advice-reorient-jobbing')
    if not model:
        return None
    reorient_jobs = typing.cast(
        reorient_jobbing_pb2.JobbingReorientJobs,
        model.get_expanded_card_data(scoring_project),
    ).reorient_jobbing_jobs
    if not reorient_jobs:
        return None

    return dict(
        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':
            scoring_project.populate_template('%ofJobName'),
        })
コード例 #26
0
def strategize(
        user: user_pb2.User, project: project_pb2.Project, database: mongo.NoPiiMongoDatabase) \
        -> None:
    """Make strategies for the user."""

    scoring_project = scoring.ScoringProject(project, user, database)
    advice_scores = {a.advice_id: a.num_stars for a in project.advices}
    category_modules = _get_strategy_modules_by_category(database).get(
        project.diagnostic.category_id, [])
    for module in category_modules:
        _make_strategy(scoring_project, module, advice_scores)
    scoring_project.details.strategies.sort(key=lambda s: -s.score)
    if not scoring_project.details.strategies:
        if category_modules:
            logging.error(
                'We could not find *any* strategy for a project:\n'
                'Existing strategies: %s\n'
                "User's advice modules: %s\n"
                '%s',
                ', '.join(strategy.strategy_id for strategy in category_modules),
                ', '.join(advice.advice_id for advice in project.advices),
                str(scoring_project))
        return
    scoring_project.details.strategies[0].is_principal = True
コード例 #27
0
ファイル: focus.py プロジェクト: bayesimpact/bob-emploi
def send_focus_email_to_user(
        action: 'campaign.Action', user: user_pb2.User, *, dry_run_email: Optional[str] = None,
        database: mongo.NoPiiMongoDatabase,
        users_database: Optional[mongo.UsersDatabase] = None,
        eval_database: Optional[mongo.NoPiiMongoDatabase] = None,
        instant: datetime.datetime,
        restricted_campaigns: Optional[Set[mailjet_templates.Id]] = None) \
        -> Optional[mailjet_templates.Id]:
    """Try to send a focus email to the user and returns the campaign ID."""

    if user.profile.coaching_email_frequency == email_pb2.EMAIL_NONE:
        return None
    if not user.HasField('send_coaching_email_after'):
        send_coaching_email_after = _compute_next_coaching_email_date(user)
        if send_coaching_email_after > instant:
            user.send_coaching_email_after.FromDatetime(send_coaching_email_after)
            if user.user_id and action != 'ghost' and users_database:
                users_database.user.update_one(
                    {'_id': objectid.ObjectId(user.user_id)}, {'$set': {
                        'sendCoachingEmailAfter': proto.datetime_to_json_string(
                            send_coaching_email_after,
                        ),
                    }})
            return None

    # Compute next send_coaching_email_after.
    next_send_coaching_email_after = instant + _compute_duration_to_next_coaching_email(user)

    focus_emails_sent: Set[mailjet_templates.Id] = set()
    last_focus_email_sent = None
    for email_sent in user.emails_sent:
        if email_sent.campaign_id not in _FOCUS_CAMPAIGNS:
            continue
        last_focus_email_sent = email_sent
        focus_emails_sent.add(typing.cast(mailjet_templates.Id, email_sent.campaign_id))

    project = scoring.ScoringProject(
        user.projects[0] if user.projects else project_pb2.Project(),
        user, database, instant,
    )

    possible_campaigns = get_possible_campaigns(database, restricted_campaigns)
    campaigns_scores = {
        campaign_id: (
            project.score(possible_campaign.scoring_model)
            if possible_campaign.scoring_model else 2
        )
        for campaign_id, possible_campaign in possible_campaigns.items()
    }

    focus_emails_project_score_zero = {
        campaign for campaign, score in campaigns_scores.items() if not score
    }

    potential_campaigns = _shuffle(
        possible_campaigns.keys() - focus_emails_sent - focus_emails_project_score_zero,
        last_focus_email_sent, campaigns_scores,
        user.profile.coaching_email_frequency)

    for campaign_id in potential_campaigns:
        if _FOCUS_CAMPAIGNS[campaign_id].send_mail(
                user, database=database, users_database=users_database, eval_database=eval_database,
                action=action, dry_run_email=dry_run_email,
                mongo_user_update={'$set': {
                    'sendCoachingEmailAfter': proto.datetime_to_json_string(
                        next_send_coaching_email_after,
                    ),
                }}, now=instant):
            user.send_coaching_email_after.FromDatetime(next_send_coaching_email_after)
            return campaign_id

    # No focus email was supported: it seems that we have sent all the
    # ones we had. However maybe in the future we'll add more focus
    # emails so let's wait the same amount of time we have waited until
    # this email (this makes to wait 1 period, then 2, 4, …).
    last_coaching_email_sent_at = \
        _compute_last_coaching_email_date(user, user.registered_at.ToDatetime())
    send_coaching_email_after = instant + (instant - last_coaching_email_sent_at)
    user.send_coaching_email_after.FromDatetime(send_coaching_email_after)
    if user.user_id and action != 'ghost' and users_database:
        logging.debug('No more available focus email for "%s"', user.user_id)
        users_database.user.update_one({'_id': objectid.ObjectId(user.user_id)}, {'$set': {
            'sendCoachingEmailAfter': proto.datetime_to_json_string(send_coaching_email_after),
        }})
    return None
コード例 #28
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,
    }
コード例 #29
0
ファイル: imt.py プロジェクト: b3rday/bob-emploi
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),
        })
コード例 #30
0
            'campaign': user.origin.campaign,
        },
        'hasAccount': user.has_account,
    }

    def _add_scored_challenge(name: str, challenge_id: Optional[str]) -> None:
        if not challenge_id:
            return
        if not (score := data.get('nps_response', {}).get(
                'challengeScores', {}).get(challenge_id)):
            return
        data['nps_response']['challengeScores'][name] = score

    last_project = _get_last_complete_project(user)
    if last_project:
        scoring_project = scoring.ScoringProject(last_project, user, stats_db)
        data['project'] = {
            'targetJob': {
                'name': last_project.target_job.name,
                'job_group': {
                    'name': last_project.target_job.job_group.name,
                },
                'domain': _get_job_domain(stats_db, last_project.target_job),
            },
            'areaType':
            geo_pb2.AreaType.Name(last_project.area_type),
            'city': {
                'regionName': last_project.city.region_name,
                'urbanScore': last_project.city.urban_score,
            },
            'job_search_length_months':