def test_favorites_delete(self): """ Test the deletion of a favorite. """ office = Office.query.filter(Office.siret == u'00000000000003').one() url_list = self.url_for('user.favorites_list') url_delete = self.url_for('user.favorites_delete', siret=office.siret) # An anonymous user cannot delete a favorite. rv = self.app.post(url_delete) self.assertEqual(rv.status_code, 401) with self.test_request_context: self.login(self.user) # Create a favorite for the user. UserFavoriteOffice.create(user_id=self.user.id, office_siret=office.siret) rv = self.app.get(url_list) self.assertEqual(rv.status_code, 200) self.assertTrue(office.name in rv.data.decode('utf-8')) self.assertTrue(office.city in rv.data.decode('utf-8')) rv = self.app.post(url_delete) self.assertEqual(rv.status_code, 302) self.assertEqual(rv.location, url_list) # User should be redirected to the list by default. rv = self.app.get(url_list) self.assertEqual(rv.status_code, 200) self.assertTrue(u'Aucun favori pour le moment.' in rv.data.decode('utf-8'))
def favorites_add(siret): """ Add an office to the favorites of a user. """ # Since we are not using a FlaskForm but a hidden input with the token in the # form, CSRF validation has to be done manually. # CSRF validation can be disabled globally (e.g. in unit tests), so ensure that # `WTF_CSRF_ENABLED` is enabled before. if current_app.config['WTF_CSRF_ENABLED']: csrf.validate_csrf(request.form.get('csrf_token')) office = Office.query.filter_by(siret=siret).first() if not office: abort(404) UserFavoriteOffice.get_or_create(user=current_user, office=office) message = u'"%s - %s" a été ajouté à vos favoris !' % (office.name, office.city) flash(Markup(message), 'success') next_url = request.form.get('next') if next_url and is_safe_url(next_url): return redirect(urllib.unquote(next_url)) return redirect(url_for('user.favorites_list'))
def test_favorites_list(self): """ Test favorites list. """ office = Office.query.filter(Office.siret == u'00000000000004').one() url_list = self.url_for('user.favorites_list') # An anonymous user cannot access the favorites list. rv = self.app.get(url_list) self.assertEqual(rv.status_code, 401) with self.test_request_context: self.login(self.user) rv = self.app.get(url_list) self.assertEqual(rv.status_code, 200) self.assertTrue(u'Aucun favori pour le moment.' in rv.data.decode('utf-8')) # Create a favorite for the user. UserFavoriteOffice.create(user_id=self.user.id, office_siret=office.siret) rv = self.app.get(url_list) self.assertEqual(rv.status_code, 200) self.assertTrue(office.name in rv.data.decode('utf-8')) self.assertTrue(office.city in rv.data.decode('utf-8'))
def favorites_add(siret: str, rome_code: Optional[str] = None): """ Add an office to the favorites of a user. """ # Since we are not using a FlaskForm but a hidden input with the token in the # form, CSRF validation has to be done manually. # CSRF validation can be disabled globally (e.g. in unit tests), so ensure that # `WTF_CSRF_ENABLED` is enabled before. if current_app.config['WTF_CSRF_ENABLED']: csrf.validate_csrf(request.form.get('csrf_token')) office = Office.query.filter_by(siret=siret).first() if not office: abort(404) UserFavoriteOffice.add_favorite(user=current_user, office=office, rome_code=rome_code) message = '"%s - %s" a été ajouté à vos favoris !' % (office.name, office.city) flash(Markup(message), 'success') activity.log('ajout-favori', siret=siret) return get_redirect_after_favorite_operation()
def setUp(self): """ Populate the DB with data required for these tests to work. """ super(UserAccountTest, self).setUp() self.user = User(email='*****@*****.**', gender='male', first_name='John', last_name='Doe') db_session.add(self.user) db_session.flush() self.office1 = Office( departement='57', siret='00000000000001', company_name='1', headcount='5', city_code='57070', zipcode='57070', naf='4646Z', score=90, x=6.166667, y=49.133333, ) self.office2 = Office( departement='57', siret='00000000000002', company_name='1', headcount='5', city_code='57070', zipcode='57070', naf='4646Z', score=90, x=6.166667, y=49.133333, ) db_session.add_all([self.office1, self.office2]) db_session.flush() self.user_social_auth = UserSocialAuth( provider=PEAMOpenIdConnect.name, extra_data={'id_token': 'fake'}, user_id=self.user.id, ) self.fav1 = UserFavoriteOffice(user_id=self.user.id, office_siret=self.office1.siret) self.fav2 = UserFavoriteOffice(user_id=self.user.id, office_siret=self.office2.siret) db_session.add_all([self.user_social_auth, self.fav1, self.fav2]) db_session.flush() db_session.commit() self.assertEqual(db_session.query(User).count(), 1) self.assertEqual(db_session.query(Office).count(), 2) self.assertEqual(db_session.query(UserFavoriteOffice).count(), 2) self.assertEqual(db_session.query(UserSocialAuth).count(), 1)
def test_favorites_download_list_as_pdf(self): url_favorites_download = self.url_for('user.favorites_list_as_pdf') office = Office.query.filter(Office.siret == '00000000000001').one() UserFavoriteOffice.create(user_id=self.user.id, office_siret=office.siret) with self.test_request_context(): self.login(self.user) rv = self.app.get(url_favorites_download) self.assertEqual(rv.status_code, 200) self.assertEqual('application/pdf', rv.mimetype) # Unfortunately, it's difficult to do any more testing on the content of the pdf self.assertLess(1000, len(rv.data))
def test_favorites_delete(self): """ Test the deletion of a favorite. """ office = Office.query.filter(Office.siret == '00000000000003').one() url_list = self.url_for('user.favorites_list') url_delete = self.url_for('user.favorites_delete', siret=office.siret) url_search_without_domain = '/entreprises/nancy-54100/strategie-commerciale' url_search_with_domain = 'http://labonneboite.pole-emploi.fr' + url_search_without_domain # An anonymous user cannot delete a favorite. rv = self.app.post(url_delete) self.assertEqual(rv.status_code, 401) with self.test_request_context(): self.login(self.user) # Create a favorite for the user. UserFavoriteOffice.create(user_id=self.user.id, office_siret=office.siret) rv = self.app.get(url_list) self.assertEqual(rv.status_code, 200) self.assertTrue(office.name in rv.data.decode('utf-8')) self.assertTrue(office.city in rv.data.decode('utf-8')) # Deleting favorite without next_url : # User should be redirected to the favorites list by default. rv = self.app.post(url_delete) self.assertEqual(rv.status_code, 302) self.assertEqual(rv.location, url_list) # Create again the favorite for the user. UserFavoriteOffice.create(user_id=self.user.id, office_siret=office.siret) # Deleting favorite from search results - the realistic case. # User should be redirected back to the search results. rv = self.app.post(url_delete, data={'next': url_search_without_domain}) self.assertEqual(rv.status_code, 302) self.assertEqual(rv.location, url_search_with_domain) rv = self.app.get(url_list) self.assertEqual(rv.status_code, 200) self.assertTrue( 'Aucun favori pour le moment.' in rv.data.decode('utf-8'))
def favorites_list_as_csv(): """ Download as a CSV the list of the favorited offices of a user. """ return make_csv_response( csv_text=UserFavoriteOffice.user_favs_as_csv(current_user), attachment_name='mes_favoris.csv', )
def test_favorites_download(self): """ Test download favorites list as csv. """ office = Office.query.filter(Office.siret == '00000000000004').one() url = self.url_for('user.favorites_list_as_csv') # An anonymous user cannot download the favorites list. rv = self.app.get(url) self.assertEqual(rv.status_code, 401) with self.test_request_context(): self.login(self.user) # Create a favorite for the user. UserFavoriteOffice.create(user_id=self.user.id, office_siret=office.siret) rv = self.app.get(url) self.assertEqual(rv.status_code, 200) self.assertEqual('application/csv', rv.mimetype) self.assertIn('siret', rv.data.decode('utf-8')) self.assertIn(office.siret, rv.data.decode('utf-8'))
def entreprises(): """ This view takes arguments as a query string. Expected arguments are those returned by get_parameters and expected by the selected office search form. """ fix_csrf_session() session['search_args'] = request.args location, named_location = get_location(request.args) occupation = request.args.get('occupation', '') if not occupation and 'j' in request.args: suggestion = search_util.build_job_label_suggestions(request.args['j'], size=1) occupation = suggestion[0]['occupation'] if suggestion else None rome = mapping_util.SLUGIFIED_ROME_LABELS.get(occupation) job_doesnt_exist = not rome # Build form form_kwargs = {key: val for key, val in list(request.args.items()) if val} form_kwargs['j'] = settings.ROME_DESCRIPTIONS.get(rome, occupation) if 'occupation' not in form_kwargs: form_kwargs['occupation'] = occupation if not form_kwargs.get('l') and named_location: # Override form location only if it is not available (e.g when user has # removed it from the url) form_kwargs['l'] = named_location.name if location: form_kwargs['lat'] = location.latitude form_kwargs['lon'] = location.longitude form = make_company_search_form(**form_kwargs) # Render different template if it's an ajax call template = 'search/results.html' if not request.is_xhr else 'search/results_content.html' activity_log_properties = dict( emploi=occupation, localisation={ 'nom': named_location.name if named_location else None, 'ville': named_location.city if named_location else None, 'codepostal': named_location.zipcode if named_location else None, 'latitude': location.latitude if location else None, 'longitude': location.longitude if location else None, }, ) # Stop here in case of invalid arguments if not form.validate() or job_doesnt_exist: log_search_activity(activity_log_properties) return render_template(template, job_doesnt_exist=job_doesnt_exist, form=form) # Convert request arguments to fetcher parameters parameters = get_parameters(request.args) # Fetch offices and alternatives fetcher = search_util.HiddenMarketFetcher( location, romes=[rome], distance=parameters['distance'], travel_mode=parameters['travel_mode'], duration=parameters['duration'], sort=parameters['sort'], from_number=parameters['from_number'], to_number=parameters['to_number'], public=parameters.get('public'), headcount=parameters['headcount'], naf=parameters['naf'], naf_codes=None, aggregate_by=['naf'], departments=None, ) alternative_rome_descriptions = [] naf_codes_with_descriptions = [] offices = [] office_count = 0 alternative_distances = {} # Aggregations offices, aggregations = fetcher.get_offices(add_suggestions=True) office_count = fetcher.office_count alternative_distances = fetcher.alternative_distances alternative_rome_descriptions = fetcher.get_alternative_rome_descriptions() # If a filter or more are selected, the aggregations returned by fetcher.get_offices() # will be filtered too... To avoid that, we are doing additionnal calls (one by filter activated) if aggregations: fetcher.update_aggregations(aggregations) # Generates values for the NAF filter # aggregations could be empty if errors or empty results if aggregations: for naf_aggregate in aggregations['naf']: naf_description = '%s (%s)' % (settings.NAF_CODES.get( naf_aggregate["code"]), naf_aggregate["count"]) naf_codes_with_descriptions.append( (naf_aggregate["code"], naf_description)) duration_filter_enabled = fetcher.duration is not None # Pagination. pagination_manager = pagination.PaginationManager( office_count, fetcher.from_number, fetcher.to_number, request.full_path, ) current_page = pagination_manager.get_current_page() # Anticipate future calls by pre-computing duration-related searches if location: precompute.isochrones((location.latitude, location.longitude)) form.naf.choices = [('', 'Tous les secteurs')] + sorted( naf_codes_with_descriptions, key=lambda t: t[1]) form.validate() canonical_url = get_canonical_results_url( named_location.zipcode, named_location.city, occupation) if named_location else '' context = { 'alternative_distances': alternative_distances, 'alternative_rome_descriptions': alternative_rome_descriptions, 'canonical_url': canonical_url, 'companies': list(offices), 'companies_per_page': pagination.OFFICES_PER_PAGE, 'company_count': office_count, 'distance': fetcher.distance, 'doorbell_tags': doorbell.get_tags('results'), 'form': form, 'headcount': fetcher.headcount, 'job_doesnt_exist': False, 'naf': fetcher.naf, 'location': location, 'city_name': named_location.city if named_location else '', 'location_name': named_location.name if named_location else '', 'page': current_page, 'pagination': pagination_manager, 'rome_code': rome, 'rome_description': settings.ROME_DESCRIPTIONS.get(rome, ''), 'show_favorites': True, 'sort': fetcher.sort, 'tile_server_url': settings.TILE_SERVER_URL, 'travel_mode': fetcher.travel_mode, 'travel_modes': maps_constants.TRAVEL_MODES, 'travel_modes_french': maps_constants.TRAVEL_MODES_FRENCH, 'duration_filter_enabled': duration_filter_enabled, 'user_favs_as_sirets': UserFavoriteOffice.user_favs_as_sirets(current_user), } activity_log_properties['distance'] = fetcher.distance activity_log_properties['effectif'] = fetcher.headcount activity_log_properties['tri'] = fetcher.sort activity_log_properties['naf'] = fetcher.naf activity_log_sirets = [office.siret for office in offices] activity.log_search(sirets=activity_log_sirets, count=office_count, page=current_page, **activity_log_properties) return render_template(template, **context)
def results(city, zipcode, occupation): kwargs = get_parameters(request.args) kwargs['city'] = city kwargs['zipcode'] = zipcode kwargs['occupation'] = occupation canonical = '/entreprises/%s-%s/%s' % (city, zipcode, occupation) # Remove keys with empty values. form_kwargs = {key: val for key, val in dict(**kwargs).items() if val} city = city.replace('-', ' ').capitalize() full_location = '%s (%s)' % (city, zipcode) form_kwargs['location'] = full_location # Get ROME code (Répertoire Opérationnel des Métiers et des Emplois). rome = mapping_util.SLUGIFIED_ROME_LABELS.get(occupation) # Stop here if the ROME code does not exists. if not rome: form_kwargs['job'] = occupation form = CompanySearchForm(**form_kwargs) context = {'job_doesnt_exist': True, 'form': form} return render_template('search/results.html', **context) session['search_args'] = request.args # Fetch companies and alternatives. fetcher = search_util.Fetcher(**kwargs) alternative_rome_descriptions = [] alternative_distances = {} location_error = False try: current_app.logger.debug("fetching companies and company_count") companies = fetcher.get_companies() for alternative, count in fetcher.alternative_rome_codes.iteritems(): if settings.ROME_DESCRIPTIONS.get(alternative) and count: desc = settings.ROME_DESCRIPTIONS.get(alternative) slug = slugify(desc) alternative_rome_descriptions.append( [alternative, desc, slug, count]) company_count = fetcher.company_count alternative_distances = fetcher.alternative_distances except search_util.JobException: companies = [] company_count = 0 except search_util.LocationError: companies = [] company_count = 0 location_error = True # Pagination. from_number_param = int(kwargs.get('from') or 1) to_number_param = int(kwargs.get('to') or 10) pagination_manager = PaginationManager(company_count, from_number_param, to_number_param, request.full_path) current_page = pagination_manager.get_current_page() # Get contact mode and position. for position, company in enumerate(companies, start=1): company.contact_mode = util.get_contact_mode_for_rome_and_naf( fetcher.rome, company.naf) # position is later used in labonneboite/web/static/js/results.js company.position = position # Get NAF code and their descriptions. rome_2_naf_mapper = mapping_util.Rome2NafMapper() naf_codes = rome_2_naf_mapper.map([ rome, ]) naf_codes_with_descriptions = [] for naf_code in naf_codes: naf_description = settings.NAF_CODES.get(naf_code) naf_codes_with_descriptions.append((naf_code, naf_description)) if kwargs.get('naf') in [item[0] for item in naf_codes_with_descriptions]: naf_text = settings.NAF_CODES.get(kwargs['naf']) naf_codes_with_descriptions.append((kwargs['naf'], naf_text)) form_kwargs['job'] = settings.ROME_DESCRIPTIONS[rome] form = CompanySearchForm(**form_kwargs) form.naf.choices = [('', u'Tous les secteurs')] + sorted( naf_codes_with_descriptions, key=lambda t: t[1]) form.validate() context = { 'alternative_distances': alternative_distances, 'alternative_rome_descriptions': alternative_rome_descriptions, 'canonical': canonical, 'city': city, 'companies': list(companies), 'company_count': company_count, 'distance': kwargs['distance'], 'doorbell_tags': util.get_doorbell_tags('results'), 'form': form, 'headcount': kwargs['headcount'], 'job_doesnt_exist': False, 'location': full_location, 'location_error': location_error, 'naf': kwargs['naf'], 'naf_codes': naf_codes_with_descriptions, 'page': current_page, 'pagination': pagination_manager, 'rome_code': rome, 'rome_description': settings.ROME_DESCRIPTIONS.get(fetcher.rome, ''), 'show_favorites': True, 'sort': kwargs['sort'], 'tile_server_url': settings.TILE_SERVER_URL, 'user_favs_as_sirets': UserFavoriteOffice.user_favs_as_sirets(current_user), } return render_template('search/results.html', **context)
def entreprises(): """ This view takes arguments as a query string. Expected arguments are those returned by get_parameters and expected by the selected office search form. """ fix_csrf_session() refresh_token_result = attempt_to_refresh_peam_token() if refresh_token_result["token_has_expired"]: return redirect(refresh_token_result["redirect_url"]) # filter empty url params args = { key: request.args[key] for key in request.args if request.args[key] != '' } # get structured location data from url params location, named_location, departments = get_location(args) occupation = args.get('occupation', '') if not occupation and 'j' in args: suggestion = autocomplete.build_job_label_suggestions(args['j'], size=1) occupation = suggestion[0]['occupation'] if suggestion else None rome = mapping_util.SLUGIFIED_ROME_LABELS.get(occupation) job_doesnt_exist = not rome # Related romes if settings.ENABLE_RELATED_ROMES: related_romes = None hide_suggestions = False if (named_location): with start_transaction(op='related_romes_get', name='rome_' + rome): related_area = RELATED_ROMES_AREAS.get( named_location.city_code, None) if (related_area): # Hide suggestions for places which may have suggestions hide_suggestions = True romes = RELATED_ROMES.get(related_area, []) if (rome in romes): # Case where there are suggestions for this rome and this location related_romes = romes.get(rome) # related_romes = list(map(add_nafs, related_romes)) related_romes = list( map(add_descriptions, related_romes)) # sort and limit size related_romes.sort( key=lambda rome_: rome_.get('score')) related_romes = related_romes[:settings. MAX_RELATED_ROMES] if (len(related_romes) > 0): flash( 'Nouvelle fonctionnalité : Grâce aux nouveaux filtres, élargissez votre recherche aux métiers qui recrutent !', 'info') else: related_romes = [] hide_suggestions = False # Build form form_kwargs = {key: val for key, val in list(args.items()) if val} form_kwargs['j'] = settings.ROME_DESCRIPTIONS.get(rome, occupation) if 'occupation' not in form_kwargs: form_kwargs['occupation'] = occupation if not form_kwargs.get('l') and named_location: # Override form location only if it is not available (e.g when user has # removed it from the url) form_kwargs['l'] = named_location.name if location: form_kwargs['lat'] = location.latitude form_kwargs['lon'] = location.longitude if departments: form_kwargs['departments'] = departments form = make_company_search_form(**form_kwargs) # Render different template if it's an ajax call template = 'search/results.html' if not request.is_xhr else 'search/results_content.html' activity_log_properties = dict( emploi=occupation, localisation={ 'nom': named_location.name if named_location else None, 'ville': named_location.city if named_location else None, 'codepostal': named_location.zipcode if named_location else None, 'latitude': location.latitude if location else None, 'longitude': location.longitude if location else None, 'departments': departments if departments else None, }, ) # Stop here in case of invalid arguments if not form.validate() or job_doesnt_exist: log_search_activity(activity_log_properties) return render_template(template, job_doesnt_exist=job_doesnt_exist, form=form) # Convert request arguments to fetcher parameters parameters = get_parameters(args) # Fetch offices and alternatives fetcher = HiddenMarketFetcher( location.longitude if location is not None else None, location.latitude if location is not None else None, departments=departments.split(',') if departments else None, romes=[rome], distance=parameters['distance'], travel_mode=parameters['travel_mode'], duration=parameters['duration'], sort=parameters['sort'], from_number=parameters['from_number'], to_number=parameters['to_number'], audience=parameters['audience'], headcount=parameters['headcount'], naf=parameters['naf'], naf_codes=None, aggregate_by=['naf'], ) alternative_rome_descriptions = [] naf_codes_with_descriptions = [] offices = [] office_count = 0 alternative_distances = {} # Aggregations offices, aggregations = fetcher.get_offices(add_suggestions=True) office_count = fetcher.office_count alternative_distances = fetcher.alternative_distances alternative_rome_descriptions = fetcher.get_alternative_rome_descriptions() # If a filter or more are selected, the aggregations returned by fetcher.get_offices() # will be filtered too... To avoid that, we are doing additionnal calls (one by filter activated) if aggregations: fetcher.update_aggregations(aggregations) # Generates values for the NAF filter # aggregations could be empty if errors or empty results if aggregations: for naf_aggregate in aggregations['naf']: naf_description = '%s (%s)' % (settings.NAF_CODES.get( naf_aggregate["code"]), naf_aggregate["count"]) naf_codes_with_descriptions.append( (naf_aggregate["code"], naf_description)) duration_filter_enabled = fetcher.duration is not None # Pagination. pagination_manager = pagination.PaginationManager( office_count, fetcher.from_number, fetcher.to_number, request.full_path, ) current_page = pagination_manager.get_current_page() form.naf.choices = [('', 'Tous les secteurs')] + sorted( naf_codes_with_descriptions, key=lambda t: t[1]) form.validate() canonical_url = get_canonical_results_url( named_location.zipcode, named_location.city, occupation) if named_location else '' context = { 'alternative_distances': alternative_distances, 'alternative_rome_descriptions': alternative_rome_descriptions, 'related_romes': related_romes, 'related_rome_initial': parameters.get('related_rome_initial', ''), 'hide_suggestions': hide_suggestions, 'canonical_url': canonical_url, 'companies': list(offices), 'companies_per_page': pagination.OFFICES_PER_PAGE, 'company_count': office_count, 'distance': fetcher.distance, 'form': form, 'headcount': fetcher.headcount, 'job_doesnt_exist': False, 'naf': fetcher.naf, 'location': location, 'departments': departments, 'city_name': named_location.city if named_location else '', 'location_name': named_location.name if named_location else '', 'page': current_page, 'pagination': pagination_manager, 'rome_code': rome, 'rome_description': settings.ROME_DESCRIPTIONS.get(rome, ''), 'show_favorites': True, 'sort': fetcher.sort, 'tile_server_url': settings.TILE_SERVER_URL, 'travel_mode': fetcher.travel_mode, 'travel_modes': maps_constants.TRAVEL_MODES, 'travel_modes_french': maps_constants.TRAVEL_MODES_FRENCH, 'duration_filter_enabled': duration_filter_enabled, 'user_favs_as_sirets': UserFavoriteOffice.user_favs_as_sirets(current_user), } activity_log_properties['distance'] = fetcher.distance activity_log_properties['effectif'] = fetcher.headcount activity_log_properties['tri'] = fetcher.sort activity_log_properties['naf'] = fetcher.naf activity_log_sirets = [office.siret for office in offices] activity.log_search(sirets=activity_log_sirets, count=office_count, page=current_page, **activity_log_properties) return render_template(template, **context)