def test_dynamic_sticky_steps(self): """Check that dynamic fields of sticky steps are populated.""" new_action = action_pb2.Action() database = mongomock.MongoClient().test database.sticky_action_steps.insert_one({ '_id': 'step1', 'title': 'Trouver un job de %masculineJobName', 'content': 'Regarder sur le [ROME](http://go/rome/%romeId).', 'link': 'http://lbb.fr/city/%cityId/rome/%romeId', 'linkName': 'Les bonnes boites de %cityName', 'finishCheckboxCaption': "J'ai trouvé un job de %masculineJobName", }) action.instantiate( new_action, user_pb2.User(), project_pb2.Project( mobility=geo_pb2.Location(city=geo_pb2.FrenchCity( city_id='45123', name='Orléans', departement_id='45', region_id='84')), target_job=job_pb2.Job( masculine_name='Pompier', code_ogr='78910', job_group=job_pb2.JobGroup(rome_id='A1101', name='Combattants')), ), action_pb2.ActionTemplate( action_template_id='my-sticky', step_ids=['step1']), set(), database, None) step = new_action.steps[0] self.assertEqual('http://lbb.fr/city/45123/rome/A1101', step.link) self.assertEqual('Regarder sur le [ROME](http://go/rome/A1101).', step.content)
def test_advice_job_boards_extra_data(self): """Test that the advisor computes extra data for the "Find a Job Board" advice.""" project = project_pb2.Project( target_job=job_pb2.Job(job_group=job_pb2.JobGroup( rome_id='A1234')), mobility=geo_pb2.Location(city=geo_pb2.FrenchCity( departement_id='14')), job_search_length_months=7, weekly_applications_estimate=project_pb2.A_LOT, total_interview_count=1, ) self.database.jobboards.insert_one({ 'title': 'Indeed', 'filters': ['for-departement(14)'] }) self.database.advice_modules.insert_one({ 'adviceId': 'job-boards', 'triggerScoringModel': 'advice-job-boards', 'extraDataFieldName': 'job_boards_data', 'isReadyForProd': True, }) advisor.clear_cache() advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'job-boards') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual('Indeed', advice.job_boards_data.job_board_title) self.assertFalse(advice.job_boards_data.is_specific_to_job_group) self.assertTrue(advice.job_boards_data.is_specific_to_region)
def test_advice_spontaneous_application_extra_data(self, mock_get_lbb_companies): """Test that the advisor computes extra data for the "Spontaneous Application" advice.""" project = project_pb2.Project( target_job=job_pb2.Job(job_group=job_pb2.JobGroup(rome_id='A1234')), mobility=geo_pb2.Location(city=geo_pb2.FrenchCity(departement_id='14')), job_search_length_months=7, weekly_applications_estimate=project_pb2.A_LOT, total_interview_count=1, ) self.database.local_diagnosis.insert_one({ '_id': '14:A1234', 'imt': {'applicationModes': {'Foo': {'first': 'SPONTANEOUS_APPLICATION'}}}, }) self.database.advice_modules.insert_one({ 'adviceId': 'my-advice', 'triggerScoringModel': 'chantier-spontaneous-application', 'extraDataFieldName': 'spontaneous_application_data', 'isReadyForProd': True, }) mock_get_lbb_companies.return_value = iter([ {'name': 'EX NIHILO'}, {'name': 'M.F.P MULTIMEDIA FRANCE PRODUCTIONS'}, ]) advisor.clear_cache() advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'my-advice') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual( ['EX NIHILO', 'M.F.P MULTIMEDIA FRANCE PRODUCTIONS'], [c.name for c in advice.spontaneous_application_data.companies])
def _find_closest_events( self, project: scoring_base.ScoringProject) -> event_pb2.CloseByEvents: """Find the events nearest to the project's location.""" now_date = project.now.strftime('%Y-%m-%d') event_collection = [ event for event in self.event_db.get_collection(project.database) if not event.start_date or event.start_date >= now_date ] city_name = self.find_closest_city_with_events(project) if not city_name: all_events = list(event_collection) target_city = geo.get_city_location( project.database, project.details.city.city_id) or geo_pb2.FrenchCity() if not target_city.latitude: return event_pb2.CloseByEvents( events=random.sample(all_events, min(5, len(all_events)))) sorted_events = sorted( all_events, key=lambda event: _compute_square_degree_distance( event, target_city)) return event_pb2.CloseByEvents(events=sorted_events[:5]) return event_pb2.CloseByEvents( city=city_name, events=[ event for event in event_collection if event.city_name == city_name ], )
def get_city_proto(city_id: str) -> Optional[geo_pb2.FrenchCity]: """Compute a full FrenchCity proto from a simple city_id.""" if not city_id: return None try: algolia_city = _get_algolia_index().get_object(city_id) except exceptions.AlgoliaException as err: logging.warning('Error in algolia: %s for city ID %s', err, city_id) return None if not algolia_city: return None # Keep in sync with frontend/src/components/suggestions.jsx if 'urban' in algolia_city: urban = algolia_city.pop('urban') algolia_city['urbanScore'] = urban if urban else -1 if 'transport' in algolia_city: algolia_city['publicTransportationScore'] = algolia_city.pop( 'transport') if 'zipCode' in algolia_city: algolia_city['postcodes'] = algolia_city.pop('zipCode') city = geo_pb2.FrenchCity(city_id=city_id) try: json_format.ParseDict(algolia_city, city, ignore_unknown_fields=True) except json_format.ParseError as error: logging.warning( 'Error %s while parsing a city proto from Algolia:\n%s', error, algolia_city) return None return city
def test_advice_spontaneous_application_extra_data(self, mock_get_lbb_companies): """Test that the advisor computes extra data for the "Spontaneous Application" advice.""" project = project_pb2.Project( target_job=job_pb2.Job(job_group=job_pb2.JobGroup( rome_id='A1234')), mobility=geo_pb2.Location(city=geo_pb2.FrenchCity( departement_id='14')), job_search_length_months=7, weekly_applications_estimate=project_pb2.A_LOT, total_interview_count=1, ) self.database.job_group_info.insert_one({ '_id': 'A1234', 'applicationModes': { 'R4Z92': { 'modes': [{ 'percentage': 36.38, 'mode': 'SPONTANEOUS_APPLICATION' }, { 'percentage': 29.46, 'mode': 'PERSONAL_OR_PROFESSIONAL_CONTACTS' }, { 'percentage': 18.38, 'mode': 'PLACEMENT_AGENCY' }, { 'percentage': 15.78, 'mode': 'UNDEFINED_APPLICATION_MODE' }], } }, }) self.database.advice_modules.insert_one({ 'adviceId': 'my-advice', 'categories': ['first'], 'triggerScoringModel': 'advice-spontaneous-application', 'extraDataFieldName': 'spontaneous_application_data', 'isReadyForProd': True, }) mock_get_lbb_companies.return_value = iter([ { 'name': 'EX NIHILO' }, { 'name': 'M.F.P MULTIMEDIA FRANCE PRODUCTIONS' }, ]) advisor.clear_cache() advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'my-advice') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual( ['EX NIHILO', 'M.F.P MULTIMEDIA FRANCE PRODUCTIONS'], [c.name for c in advice.spontaneous_application_data.companies])
def test_advice_better_job_in_group_extra_data(self): """Test that the advisor computes extra data for the "Better Job in Group" advice.""" project = project_pb2.Project( target_job=job_pb2.Job( code_ogr='1234', job_group=job_pb2.JobGroup(rome_id='A1234')), mobility=geo_pb2.Location(city=geo_pb2.FrenchCity( departement_id='14')), job_search_length_months=7, weekly_applications_estimate=project_pb2.A_LOT, total_interview_count=1, ) self.database.job_group_info.insert_one({ '_id': 'A1234', 'jobs': [ { 'codeOgr': '1234', 'name': 'Pilote' }, { 'codeOgr': '5678', 'name': 'Pompier' }, { 'codeOgr': '9012', 'name': 'Facteur' }, ], 'requirements': { 'specificJobs': [ { 'codeOgr': '5678', 'percentSuggested': 55, }, { 'codeOgr': '1234', 'percentSuggested': 45, }, ], }, }) self.database.advice_modules.insert_one({ 'adviceId': 'my-advice', 'triggerScoringModel': 'advice-better-job-in-group', 'extraDataFieldName': 'better_job_in_group_data', 'isReadyForProd': True, }) advisor.clear_cache() advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'my-advice') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual('Pompier', advice.better_job_in_group_data.better_job.name) self.assertEqual(1, advice.better_job_in_group_data.num_better_jobs)
def test_get_in_a_departement_text_city_hint(self) -> None: """Test get_in_a_departement_text func with a city hing.""" self.assertEqual( 'in Illinois', geo.get_in_a_departement_text(self._db, '19', city_hint=geo_pb2.FrenchCity( departement_id='19', departement_name='Illinois', departement_prefix='in ', )))
def test_advice_improve_success_rate_extra_data(self): """Test that the advisor computes extra data for the "Improve Success Rate" advice.""" project = project_pb2.Project( target_job=job_pb2.Job(job_group=job_pb2.JobGroup( rome_id='A1234')), mobility=geo_pb2.Location(city=geo_pb2.FrenchCity( departement_id='14')), job_search_length_months=6, weekly_applications_estimate=project_pb2.A_LOT, total_interview_count=1, ) self.database.local_diagnosis.insert_one({ '_id': '14:A1234', 'imt': { 'yearlyAvgOffersDenominator': 10, 'yearlyAvgOffersPer10Candidates': 2, }, }) self.database.job_group_info.insert_one({ '_id': 'A1234', 'requirements': { 'skills': [{ 'name': 'Humour' }, { 'name': 'Empathie' }], 'skillsShortText': '**Humour** et **empathie**', }, }) self.database.advice_modules.insert_one({ 'adviceId': 'improve-success', 'triggerScoringModel': 'advice-improve-resume', 'extraDataFieldName': 'improve_success_rate_data', 'isReadyForProd': True, }) advisor.clear_cache() advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'improve-success') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertGreater( advice.improve_success_rate_data.num_interviews_increase, 50) self.assertFalse(advice.improve_success_rate_data.requirements.skills) self.assertEqual( '**Humour** et **empathie**', advice.improve_success_rate_data.requirements.skills_short_text)
def test_advice_events_extra_data(self, mock_now): """Test that the advisor computes extra data for the "Events" advice.""" mock_now.return_value = datetime.datetime(2017, 8, 15) project = project_pb2.Project( target_job=job_pb2.Job( code_ogr='1234', job_group=job_pb2.JobGroup(rome_id='A1234')), mobility=geo_pb2.Location(city=geo_pb2.FrenchCity( departement_id='75')), job_search_length_months=7, weekly_applications_estimate=project_pb2.A_LOT, total_interview_count=1, ) self.database.advice_modules.insert_one({ 'adviceId': 'my-advice', 'triggerScoringModel': 'advice-event', 'extraDataFieldName': 'events_data', 'isReadyForProd': True, }) self.database.events.insert_many([ { 'title': 'AP HEROS CANDIDATS MADIRCOM - BORDEAUX', 'link': 'https://www.workuper.com/events/ap-heros-candidats-madircom-bordeaux', 'organiser': 'MADIRCOM', 'startDate': '2017-08-29', }, { 'title': 'Le Salon du Travail et de la Mobilité Professionnelle', 'link': 'https://www.workuper.com/events/le-salon-du-travail-et-de-la-mobilite-' 'professionnelle', 'organiser': 'Altice Media Events', 'startDate': '2018-01-19', }, ]) advisor.clear_cache() self.user.features_enabled.alpha = True advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'my-advice') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual('AP HEROS CANDIDATS MADIRCOM - BORDEAUX', advice.events_data.event_name)
def test_arrondissements( self, mock_requests: requests_mock.Mocker, mock_get_city_proto: mock.MagicMock) -> None: """Test that arrondissements IDs from PE are converted to city IDs.""" mock_get_city_proto.side_effect = lambda c: geo_pb2.FrenchCity(name='City', city_id=c) # Sanity check. self.assertEqual('31555', self._test_new_user_in_city(mock_requests, '31555')) # Lyon. self.assertEqual('69123', self._test_new_user_in_city(mock_requests, '69381')) self.assertEqual('69123', self._test_new_user_in_city(mock_requests, '69386')) # Marseille. self.assertEqual('13055', self._test_new_user_in_city(mock_requests, '13201')) self.assertEqual('13055', self._test_new_user_in_city(mock_requests, '13212')) # Paris. self.assertEqual('75056', self._test_new_user_in_city(mock_requests, '75101')) self.assertEqual('75056', self._test_new_user_in_city(mock_requests, '75113'))
def list_nearby_cities(self, project): """Compute and store all interesting cities that are not too close and not too far. Those cities will be used by the Commute advice. """ job_group = project.details.target_job.job_group.rome_id all_cities = commute_pb2.HiringCities() proto.parse_from_mongo( project.database.hiring_cities.find_one({'_id': job_group}), all_cities) interesting_cities_for_rome = all_cities.hiring_cities if not interesting_cities_for_rome: return [] target_city = geo_pb2.FrenchCity() mongo_city = project.database.cities.find_one( {'_id': project.details.mobility.city.city_id}) if not mongo_city: return [] proto.parse_from_mongo(mongo_city, target_city) commuting_cities = list( _get_commuting_cities(interesting_cities_for_rome, target_city)) obvious_cities = [ city for city in commuting_cities if city.distance_km < _MIN_CITY_DISTANCE ] interesting_cities = [ city for city in commuting_cities if city.distance_km >= _MIN_CITY_DISTANCE ] # If there is only one city nearby and no obvious city, the nearby city becomes obvious, so # we do not recommend it. if len(interesting_cities) == 1 and not obvious_cities: return [] return interesting_cities
def get_city_proto(city_id: str) -> Optional[geo_pb2.FrenchCity]: """Compute a full FrenchCity proto from a simple city_id.""" if not city_id: return None if not _ALGOLIA_INDEX: _ALGOLIA_INDEX.append( search_client.SearchClient.create( os.getenv('ALGOLIA_APP_ID', 'K6ACI9BKKT'), os.getenv( 'ALGOLIA_API_KEY', 'da4db0bf437e37d6d49cefcb8768c67a')).init_index('cities')) try: algolia_city = _ALGOLIA_INDEX[0].get_object(city_id) except exceptions.AlgoliaException as err: logging.warning('Error in algolia: %s for city ID %s', err, city_id) return None if not algolia_city: return None # Keep in sync with frontend/src/components/suggestions.jsx if 'urban' in algolia_city: urban = algolia_city.pop('urban') algolia_city['urbanScore'] = urban if urban else -1 if 'transport' in algolia_city: algolia_city['publicTransportationScore'] = algolia_city.pop( 'transport') if 'zipCode' in algolia_city: algolia_city['postcodes'] = algolia_city.pop('zipCode') city = geo_pb2.FrenchCity(city_id=city_id) try: json_format.ParseDict(algolia_city, city, ignore_unknown_fields=True) except json_format.ParseError as error: logging.warning( 'Error %s while parsing a city proto from Algolia:\n%s', error, algolia_city) return None return city
def test_advice_volunteer_extra_data(self): """Test that the advisor computes extra data for the "Volunteer" advice.""" project = project_pb2.Project( target_job=job_pb2.Job( code_ogr='1234', job_group=job_pb2.JobGroup(rome_id='A1234')), mobility=geo_pb2.Location(city=geo_pb2.FrenchCity( departement_id='75')), job_search_length_months=7, weekly_applications_estimate=project_pb2.A_LOT, total_interview_count=1, ) self.database.volunteering_missions.insert_one({ '_id': '75', 'missions': [ { 'associationName': 'BackUp Rural' }, { 'associationName': 'Construisons Ensemble Comment Faire' }, ], }) self.database.advice_modules.insert_one({ 'adviceId': 'my-advice', 'triggerScoringModel': 'advice-volunteer', 'extraDataFieldName': 'volunteer_data', 'isReadyForProd': True, }) advisor.clear_cache() advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'my-advice') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual( ['BackUp Rural', 'Construisons Ensemble Comment Faire'], sorted(advice.volunteer_data.association_names))
def test_advice_association_help_extra_data(self): """Test that the advisor computes extra data for the "Find an association" advice.""" project = project_pb2.Project( target_job=job_pb2.Job( code_ogr='1234', job_group=job_pb2.JobGroup(rome_id='A1234')), mobility=geo_pb2.Location(city=geo_pb2.FrenchCity( departement_id='14')), job_search_length_months=7, weekly_applications_estimate=project_pb2.A_LOT, total_interview_count=1, ) self.database.associations.insert_many([ { 'name': 'Pôle emploi' }, { 'name': 'SNC', 'filters': ['for-departement(14,15,16)'] }, { 'name': 'Ressort', 'filters': ['for-departement(69)'] }, ]) self.database.advice_modules.insert_one({ 'adviceId': 'my-advice', 'triggerScoringModel': 'advice-association-help', 'extraDataFieldName': 'associations_data', 'isReadyForProd': True, }) advisor.clear_cache() advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'my-advice') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual('SNC', advice.associations_data.association_name)
def get_expanded_card_data( self, project: scoring_base.ScoringProject) -> geo_pb2.FrenchCity: """Retrieve data for the expanded card.""" return geo.get_city_location(project.database, project.details.city.city_id) or \ geo_pb2.FrenchCity()
def _find_state(state_name: str) -> geo_pb2.FrenchCity: return geo_pb2.FrenchCity(region_id=STATE_MAP[state_name], region_name=state_name)
def test_new_user( self, mock_requests: requests_mock.Mocker, mock_get_job_proto: mock.MagicMock, mock_get_city_proto: mock.MagicMock) -> None: """Auth request with PE Connect for a new user.""" def _match_correct_code(request: 'requests_mock._RequestObjectProxy') -> bool: return 'code=correct-code' in (request.text or '') mock_requests.post( 'https://authentification-candidat.pole-emploi.fr/connexion/oauth2/access_token?' 'realm=/individu', json={ 'access_token': '123456', 'nonce': 'correct-nonce', 'scope': 'api_peconnect-individuv1 openid profile email api_peconnect-coordonneesv1 ' 'coordonnees competences', 'token_type': 'Bearer', }, additional_matcher=_match_correct_code) mock_requests.get( 'https://api.emploi-store.fr/partenaire/peconnect-competences/v1/competences', headers={'Authorization': 'Bearer 123456'}, json=[ { 'codeAppellation': '86420', 'codeRome': 'A1234', }, { 'codeAppellation': '86421', 'codeRome': 'A1235', }, ], ) mock_requests.get( 'https://api.emploi-store.fr/partenaire/peconnect-individu/v1/userinfo', headers={'Authorization': 'Bearer 123456'}, json={ 'email': '*****@*****.**', 'family_name': 'CORPET', 'gender': 'male', 'given_name': 'PASCAL', 'sub': 'pe-connect-user-id-1', }) mock_requests.get( 'https://api.emploi-store.fr/partenaire/peconnect-coordonnees/v1/coordonnees', headers={'Authorization': 'Bearer 123456'}, json={ 'codeINSEE': '69386', 'address1': '55 rue du lac', }) mock_get_city_proto.return_value = geo_pb2.FrenchCity(name='Lyon', city_id='69123') mock_get_job_proto.return_value = job_pb2.Job(name='Plombier') response = self.app.post( '/api/user/authenticate', data='{"peConnectCode": "correct-code", "peConnectNonce": "correct-nonce"}', content_type='application/json') auth_response = self.json_from_response(response) self.assertTrue(auth_response['isNewUser']) user = auth_response['authenticatedUser'] self.assertEqual('pe-connect-user-id-1', user.get('peConnectId')) self.assertTrue(user.get('hasAccount')) self.assertEqual('Pascal', user.get('profile', {}).get('name')) self.assertEqual('Corpet', user.get('profile', {}).get('lastName')) self.assertEqual('MASCULINE', user.get('profile', {}).get('gender')) self.assertEqual([True], [p.get('isIncomplete') for p in user.get('projects', [])]) self.assertEqual('Lyon', user['projects'][0].get('city', {}).get('name')) self.assertEqual('69123', user['projects'][0].get('city', {}).get('cityId')) self.assertEqual('Plombier', user['projects'][0].get('targetJob', {}).get('name')) user_id = user['userId'] self.assertEqual([user_id], [str(u['_id']) for u in self._user_db.user.find()]) mock_get_city_proto.assert_called_once_with('69123') mock_get_job_proto.assert_called_once() self.assertEqual(('86420', 'A1234'), mock_get_job_proto.call_args[0][1:])