[IMP] website_hr_recruitment: add pager + search and improve filters

1. Pager + search

Before this commit the jobs list had no pager, and the search parameter
was neither used for filtering nor propagated to the rendering.

After this commit the jobs list supports pager and the search parameter
is passed through all layers.

2. Filters

Before this commit, filters were displayed as a set of `nav-pills` items
in a column on the left of the main template.

After this commit, filters have been moved to the top as dropdown
elements, displaying counts inside badges, which are obtained from the
list of filtered jobs.

Filters have also been adapted to requested change: each filter content
must be based on results that would be obtained if that filter was not
applied.

Additionally, `Remote` filterable location has been included, and can
be set either by using the backend or directly with the website builder.

task-2406626

closes odoo/odoo#67913

Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
Co-authored-by: Benoit Socias <bso@odoo.com>
This commit is contained in:
Carlos Valverde
2023-05-12 19:54:08 +02:00
committed by qsm-odoo
co-authored by Benoit Socias
parent d3b7e1c127
commit 54f246f9c7
6 changed files with 507 additions and 112 deletions
+204 -41
View File
@@ -1,73 +1,189 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import warnings
from odoo import http, _
from odoo.addons.http_routing.models.ir_http import slug
from odoo.osv.expression import AND
from odoo.http import request
from werkzeug.exceptions import NotFound
from odoo.tools.misc import groupby
class WebsiteHrRecruitment(http.Controller):
_jobs_per_page = 12
def sitemap_jobs(env, rule, qs):
if not qs or qs.lower() in '/jobs':
yield {'loc': '/jobs'}
@http.route([
'/jobs',
'/jobs/country/<model("res.country"):country>',
'/jobs/department/<model("hr.department"):department>',
'/jobs/country/<model("res.country"):country>/department/<model("hr.department"):department>',
'/jobs/office/<int:office_id>',
'/jobs/country/<model("res.country"):country>/office/<int:office_id>',
'/jobs/department/<model("hr.department"):department>/office/<int:office_id>',
'/jobs/country/<model("res.country"):country>/department/<model("hr.department"):department>/office/<int:office_id>',
'/jobs/employment_type/<int:contract_type_id>',
'/jobs/country/<model("res.country"):country>/employment_type/<int:contract_type_id>',
'/jobs/department/<model("hr.department"):department>/employment_type/<int:contract_type_id>',
'/jobs/office/<int:office_id>/employment_type/<int:contract_type_id>',
'/jobs/country/<model("res.country"):country>/department/<model("hr.department"):department>/employment_type/<int:contract_type_id>',
'/jobs/country/<model("res.country"):country>/office/<int:office_id>/employment_type/<int:contract_type_id>',
'/jobs/department/<model("hr.department"):department>/office/<int:office_id>/employment_type/<int:contract_type_id>',
'/jobs/country/<model("res.country"):country>/department/<model("hr.department"):department>/office/<int:office_id>/employment_type/<int:contract_type_id>',
'/jobs/page/<int:page>',
], type='http', auth="public", website=True, sitemap=sitemap_jobs)
def jobs(self, country=None, department=None, office_id=None, contract_type_id=None, **kwargs):
def jobs(self, country_id=None, department_id=None, office_id=None, contract_type_id=None,
is_remote=False, is_other_department=False, is_untyped=None, page=1, search=None, **kwargs):
env = request.env(context=dict(request.env.context, show_address=True, no_tag_br=True))
Country = env['res.country']
Jobs = env['hr.job']
Department = env['hr.department']
# List jobs available to current UID
domain = request.website.website_domain()
job_ids = Jobs.search(domain, order="is_published desc, sequence, no_of_recruitment desc").ids
# Browse jobs as superuser, because address is restricted
jobs = Jobs.sudo().browse(job_ids)
country = Country.browse(int(country_id)) if country_id else None
department = Department.browse(int(department_id)) if department_id else None
office_id = int(office_id) if office_id else None
contract_type_id = int(contract_type_id) if contract_type_id else None
# Default search by user country
if not (country or department or office_id or contract_type_id or kwargs.get('all_countries')):
if request.geoip.country_code:
countries_ = Country.search([('code', '=', request.geoip.country_code)])
country = countries_[0] if countries_ else None
if not any(j for j in jobs if j.address_id and j.address_id.country_id == country):
country = False
if country:
country_count = Jobs.search_count(AND([
request.website.website_domain(),
[('address_id.country_id', '=', country.id)]
]))
if not country_count:
country = False
# Filter job / office for country
if country and not kwargs.get('all_countries'):
jobs = jobs.filtered(lambda j: not j.address_id or j.address_id.country_id.id == country.id)
offices = jobs.address_id.sorted(lambda a: (a.country_id.name or '', a.city or ''))
options = {
'displayDescription': True,
'allowFuzzy': not request.params.get('noFuzzy'),
'country_id': country.id if country else None,
'department_id': department.id if department else None,
'office_id': office_id,
'contract_type_id': contract_type_id,
'is_remote': is_remote,
'is_other_department': is_other_department,
'is_untyped': is_untyped,
}
total, details, fuzzy_search_term = request.website._search_with_fuzzy("jobs", search,
limit=1000, order="is_published desc, sequence, no_of_recruitment desc", options=options)
# Browse jobs as superuser, because address is restricted
jobs = details[0].get('results', Jobs).sudo()
# Deduce departments and countries offices of those jobs
departments = jobs.department_id.sorted('name')
countries = offices.country_id.sorted('name')
employment_types = jobs.contract_type_id.sorted('name')
def sort(records_list, field_name):
""" Sort records in the given collection according to the given
field name, alphabetically. None values instead of records are
placed at the end.
if department:
jobs = [j for j in jobs if j.department_id and j.department_id.id == department.id]
if office_id and office_id in [x.id for x in offices]:
jobs = [j for j in jobs if j.address_id and j.address_id.id == office_id]
:param list records_list: collection of records or None values
:param str field_name: field on which to sort
:return: sorted list
"""
return sorted(
records_list,
key=lambda item: (item is None, item and item[field_name] or ''),
)
# Countries
if country or is_remote:
cross_country_options = options.copy()
cross_country_options.update({
'allowFuzzy': False,
'country_id': None,
'is_remote': False,
})
cross_country_total, cross_country_details, _ = request.website._search_with_fuzzy("jobs",
fuzzy_search_term or search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc",
options=cross_country_options)
# Browse jobs as superuser, because address is restricted
cross_country_jobs = cross_country_details[0].get('results', Jobs).sudo()
else:
office_id = False
if contract_type_id:
jobs = [j for j in jobs if j.contract_type_id and j.contract_type_id.id == contract_type_id]
cross_country_total = total
cross_country_jobs = jobs
country_offices = set(j.address_id or None for j in cross_country_jobs)
countries = sort(set(o and o.country_id or None for o in country_offices), 'name')
count_per_country = {'all': cross_country_total}
for c, jobs_list in groupby(cross_country_jobs, lambda job: job.address_id.country_id):
count_per_country[c] = len(jobs_list)
count_remote = len(cross_country_jobs.filtered(lambda job: not job.address_id))
if count_remote:
count_per_country[None] = count_remote
# Departments
if department or is_other_department:
cross_department_options = options.copy()
cross_department_options.update({
'allowFuzzy': False,
'department_id': None,
'is_other_department': False,
})
cross_department_total, cross_department_details, _ = request.website._search_with_fuzzy("jobs",
fuzzy_search_term or search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc",
options=cross_department_options)
cross_department_jobs = cross_department_details[0].get('results', Jobs)
else:
cross_department_total = total
cross_department_jobs = jobs
departments = sort(set(j.department_id or None for j in cross_department_jobs), 'name')
count_per_department = {'all': cross_department_total}
for d, jobs_list in groupby(cross_department_jobs, lambda job: job.department_id):
count_per_department[d] = len(jobs_list)
count_other_department = len(cross_department_jobs.filtered(lambda job: not job.department_id))
if count_other_department:
count_per_department[None] = count_other_department
# Offices
if office_id or is_remote:
cross_office_options = options.copy()
cross_office_options.update({
'allowFuzzy': False,
'office_id': None,
'is_remote': False,
})
cross_office_total, cross_office_details, _ = request.website._search_with_fuzzy("jobs",
fuzzy_search_term or search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc",
options=cross_office_options)
# Browse jobs as superuser, because address is restricted
cross_office_jobs = cross_office_details[0].get('results', Jobs).sudo()
else:
cross_office_total = total
cross_office_jobs = jobs
offices = sort(set(j.address_id or None for j in cross_office_jobs), 'city')
count_per_office = {'all': cross_office_total}
for o, jobs_list in groupby(cross_office_jobs, lambda job: job.address_id):
count_per_office[o] = len(jobs_list)
count_remote = len(cross_office_jobs.filtered(lambda job: not job.address_id))
if count_remote:
count_per_office[None] = count_remote
# Employment types
if contract_type_id or is_untyped:
cross_type_options = options.copy()
cross_type_options.update({
'allowFuzzy': False,
'contract_type_id': None,
'is_untyped': False,
})
cross_type_total, cross_type_details, _ = request.website._search_with_fuzzy("jobs",
fuzzy_search_term or search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc",
options=cross_type_options)
cross_type_jobs = cross_type_details[0].get('results', Jobs)
else:
cross_type_total = total
cross_type_jobs = jobs
employment_types = sort(set(j.contract_type_id for j in jobs if j.contract_type_id), 'name')
count_per_employment_type = {'all': cross_type_total}
for t, jobs_list in groupby(cross_type_jobs, lambda job: job.contract_type_id):
count_per_employment_type[t] = len(jobs_list)
count_untyped = len(cross_type_jobs.filtered(lambda job: not job.contract_type_id))
if count_untyped:
count_per_employment_type[None] = count_untyped
pager = request.website.pager(
url=request.httprequest.path.partition('/page/')[0],
url_args=request.httprequest.args,
total=total,
page=page,
step=self._jobs_per_page,
)
offset = pager['offset']
jobs = jobs[offset:offset + self._jobs_per_page]
office = env['res.partner'].browse(int(office_id)) if office_id else None
contract_type = env['hr.contract.type'].browse(int(contract_type_id)) if contract_type_id else None
# Render page
return request.render("website_hr_recruitment.index", {
@@ -78,8 +194,19 @@ class WebsiteHrRecruitment(http.Controller):
'employment_types': employment_types,
'country_id': country,
'department_id': department,
'office_id': office_id,
'contract_type_id': contract_type_id,
'office_id': office,
'contract_type_id': contract_type,
'is_remote': is_remote,
'is_other_department': is_other_department,
'is_untyped': is_untyped,
'pager': pager,
'search': fuzzy_search_term or search,
'search_count': total,
'original_search': fuzzy_search_term and search,
'count_per_country': count_per_country,
'count_per_department': count_per_department,
'count_per_office': count_per_office,
'count_per_employment_type': count_per_employment_type,
})
@http.route('/jobs/add', type='json', auth="user", website=True)
@@ -109,3 +236,39 @@ class WebsiteHrRecruitment(http.Controller):
'error': error,
'default': default,
})
# Compatibility routes
@http.route([
'/jobs/country/<model("res.country"):country>',
'/jobs/department/<model("hr.department"):department>',
'/jobs/country/<model("res.country"):country>/department/<model("hr.department"):department>',
'/jobs/office/<int:office_id>',
'/jobs/country/<model("res.country"):country>/office/<int:office_id>',
'/jobs/department/<model("hr.department"):department>/office/<int:office_id>',
'/jobs/country/<model("res.country"):country>/department/<model("hr.department"):department>/office/<int:office_id>',
'/jobs/employment_type/<int:contract_type_id>',
'/jobs/country/<model("res.country"):country>/employment_type/<int:contract_type_id>',
'/jobs/department/<model("hr.department"):department>/employment_type/<int:contract_type_id>',
'/jobs/office/<int:office_id>/employment_type/<int:contract_type_id>',
'/jobs/country/<model("res.country"):country>/department/<model("hr.department"):department>/employment_type/<int:contract_type_id>',
'/jobs/country/<model("res.country"):country>/office/<int:office_id>/employment_type/<int:contract_type_id>',
'/jobs/department/<model("hr.department"):department>/office/<int:office_id>/employment_type/<int:contract_type_id>',
'/jobs/country/<model("res.country"):country>/department/<model("hr.department"):department>/office/<int:office_id>/employment_type/<int:contract_type_id>',
], type='http', auth="public", website=True, sitemap=False)
def jobs_compatibility(self, country=None, department=None, office_id=None, contract_type_id=None, **kwargs):
"""
Deprecated since Odoo 16.3: those routes are kept by compatibility.
They should not be used in Odoo code anymore.
"""
warnings.warn(
"This route is deprecated since Odoo 16.3: the jobs list is now available at /jobs or /jobs/page/XXX",
DeprecationWarning
)
return self.jobs(
country_id=country.id if country else None,
department_id=department.id if department else None,
office_id=office_id,
contract_type_id=contract_type_id,
**kwargs
)
+60 -1
View File
@@ -9,7 +9,12 @@ from odoo.tools.translate import html_translate
class Job(models.Model):
_name = 'hr.job'
_inherit = ['hr.job', 'website.seo.metadata', 'website.published.multi.mixin']
_inherit = [
'hr.job',
'website.seo.metadata',
'website.published.multi.mixin',
'website.searchable.mixin',
]
@mute_logger('odoo.addons.base.models.ir_qweb')
def _get_default_description(self):
@@ -65,3 +70,57 @@ class Job(models.Model):
def toggle_active(self):
self.filtered('active').website_published = False
return super().toggle_active()
@api.model
def _search_get_detail(self, website, order, options):
requires_sudo = False
with_description = options['displayDescription']
country_id = options.get('country_id')
department_id = options.get('department_id')
office_id = options.get('office_id')
contract_type_id = options.get('contract_type_id')
is_remote = options.get('is_remote')
is_other_department = options.get('is_other_department')
is_untyped = options.get('is_untyped')
domain = [website.website_domain()]
if country_id:
domain.append([('address_id.country_id', '=', int(country_id))])
requires_sudo = True
if department_id:
domain.append([('department_id', '=', int(department_id))])
elif is_other_department:
domain.append([('department_id', '=', None)])
if office_id:
domain.append([('address_id', '=', int(office_id))])
elif is_remote:
domain.append([('address_id', '=', None)])
if contract_type_id:
domain.append([('contract_type_id', '=', int(contract_type_id))])
elif is_untyped:
domain.append([('contract_type_id', '=', None)])
if requires_sudo and not self.env.user.has_group('hr_recruitment.group_hr_recruitment_user'):
# Rule must be reinforced because of sudo.
domain.append([('website_published', '=', True)])
search_fields = ['name']
fetch_fields = ['name', 'website_url']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
}
if with_description:
search_fields.append('description')
fetch_fields.append('description')
mapping['description'] = {'name': 'description', 'type': 'text', 'html': True, 'match': True}
return {
'model': 'hr.job',
'requires_sudo': requires_sudo,
'base_domain': domain,
'search_fields': search_fields,
'fetch_fields': fetch_fields,
'mapping': mapping,
'icon': 'fa-briefcase',
}
@@ -12,3 +12,9 @@ class Website(models.Model):
suggested_controllers = super(Website, self).get_suggested_controllers()
suggested_controllers.append((_('Jobs'), url_for('/jobs'), 'website_hr_recruitment'))
return suggested_controllers
def _search_get_details(self, search_type, order, options):
result = super()._search_get_details(search_type, order, options)
if search_type in ['jobs', 'all']:
result.append(self.env['hr.job']._search_get_detail(self, order, options))
return result
@@ -1,4 +1,21 @@
.o_jobs_topbar_filters {
.dropdown-toggle {
.fa {
margin-right: .4em;
}
&:after {
margin-left: 1.2em;
}
}
.dropdown-menu {
margin-top: $navbar-padding-y;
min-width: 12rem;
}
}
.o_website_hr_recruitment_jobs_list {
#jobs_grid {
.o_job_infos {
@@ -1,6 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="jobs_searchbar_input_snippet_options" inherit_id="website.searchbar_input_snippet_options" name="jobs search bar snippet options">
<xpath expr="//div[@data-js='SearchBar']/we-select[@data-name='scope_opt']" position="inside">
<we-button data-set-search-type="jobs" data-select-data-attribute="jobs" data-name="search_jobs_opt" data-form-action="/jobs">Jobs</we-button>
</xpath>
<xpath expr="//div[@data-js='SearchBar']/div[@data-dependencies='limit_opt']" position="inside">
<we-checkbox string="Description" data-dependencies="search_jobs_opt" data-select-data-attribute="true" data-attribute-name="displayDescription"
data-apply-to=".search-query"/>
</xpath>
</template>
<template id="snippet_options" inherit_id="website.snippet_options" name="Hr Recruitment Snippet Options">
<xpath expr="." position="inside">
<div data-selector="main:has(.o_website_hr_recruitment_jobs_list)" data-page-options="true" groups="website.group_website_designer" data-no-check="true" string="Jobs Page">
@@ -8,18 +19,22 @@
data-customize-website-views="website_hr_recruitment.job_filter_by_countries"
data-no-preview="true"
data-reload="/"/>
<we-checkbox string="Departments Filter"
data-customize-website-views="website_hr_recruitment.job_filter_by_departments"
data-no-preview="true"
data-reload="/"/>
<we-checkbox string="Offices Filter"
data-customize-website-views="website_hr_recruitment.job_filter_by_offices"
data-no-preview="true"
data-reload="/"/>
<we-checkbox string="Departments Filter"
data-customize-website-views="website_hr_recruitment.job_filter_by_departments"
data-no-preview="true"
data-reload="/"/>
<we-checkbox string="Employment Types Filter"
data-customize-website-views="website_hr_recruitment.job_filter_by_employment_type"
data-no-preview="true"
data-reload="/"/>
<we-checkbox string="Search Bar"
data-customize-website-views="website_hr_recruitment.job_search_bar"
data-no-preview="true"
data-reload="/"/>
<we-checkbox string="Sidebar"
data-customize-website-views="website_hr_recruitment.job_right_side_bar"
data-no-preview="true"
@@ -13,6 +13,8 @@
<div class="d-flex flex-column flex-md-row flex-md-wrap flex-lg-nowrap align-items-center justify-content-between w-100">
<!-- Title -->
<span class="navbar-brand h5 my-0 me-sm-auto">Our Job Offers</span>
<!-- Customizations -->
<t t-call="website_hr_recruitment.topbar_customizations"/>
</div>
</div>
</nav>
@@ -25,6 +27,9 @@
<div class="row flex-column">
<t t-if="not jobs">
<div class="col">
<div t-if="search" class="alert alert-warning text-center" role="alert">
No results found for '<span t-out="search"/>'.
</div>
<div class="alert alert-info text-center text-muted" groups="hr_recruitment.group_hr_recruitment_manager">
Create new job pages from the <strong>+ <i>New</i></strong> top-right button.
</div>
@@ -37,6 +42,14 @@
</div>
</div>
</t>
<t t-elif="search and original_search">
<div class="col">
<div class="alert alert-info text-center" role="alert">
No results found for '<span t-out="original_search"/>'. Showing results for '<span t-out="search"/>'.
<t t-set="search" t-value="original_search"/>
</div>
</div>
</t>
<div t-foreach="jobs" t-as="job" class="col-lg mb32">
<div t-attf-class="card #{not job.website_published and 'o_jobs_unpublished'}">
<a t-attf-href="/jobs/detail/#{ slug(job) }"
@@ -72,6 +85,12 @@
</div>
</div>
</div>
<!-- Pager -->
<div class='navbar mb-3 w-100'>
<t t-call="website.pager">
<t t-set="classname" t-valuef="mx-auto"/>
</t>
</div>
</div>
</div>
@@ -648,85 +667,201 @@
</field>
</record>
<template id="job_filter_by_countries" inherit_id="website_hr_recruitment.index" active="False" name="Filter by Countries">
<xpath expr="//div[@id='jobs_grid_left']" position="inside">
<ul class="nav nav-pills flex-column mb32">
<li class="nav-item"><a t-attf-href="/jobs#{ '/department/%s' % slug(department_id) if department_id else '' }#{ '/office/%s' % office_id if office_id else '' }#{ '/employment_type/%s' % contract_type_id if contract_type_id else '' }?all_countries=1"
t-attf-class="nav-link#{'' if country_id else ' active'}">All Countries</a></li>
<t t-foreach="countries" t-as="country">
<li class="nav-item">
<a t-attf-href="/jobs/country/#{ slug(country) }#{ '/department/%s' % slug(department_id) if department_id else '' }#{ '/office/%s' % office_id if office_id else '' }#{ '/employment_type/%s' % contract_type_id if contract_type_id else '' }"
t-attf-class="nav-link#{' active' if country_id and country_id.id == country.id else ''}"><span t-field="country.name"/></a>
</li>
</t>
</ul>
</xpath>
<xpath expr="//div[@id='jobs_grid_left']" position="attributes">
<attribute name="class">col-lg-3</attribute>
<!-- Topbar Customizations -->
<template id="topbar_customizations" name="Topbar">
<!-- Sets -->
<t t-set="current_country_param" t-value="'&amp;is_remote=1' if is_remote else ('&amp;country_id=%s' % country_id.id if country_id else '')"/>
<t t-set="current_department_param" t-value="'&amp;department_id=%s' % department_id.id if department_id else 'is_other_department=1' if is_other_department else ''"/>
<t t-set="current_office_param" t-value="'' if is_remote else '&amp;office_id=%s' % office_id.id if office_id else ''"/>
<t t-set="current_employment_type_param" t-value="'&amp;is_untyped=1' if is_untyped else ('&amp;contract_type_id=%s' % contract_type_id.id if contract_type_id else '')"/>
<t t-set="opt_jobs_filter_countries" t-value="is_view_active('website_hr_recruitment.job_filter_by_countries')"/>
<t t-set="opt_jobs_filter_departments" t-value="is_view_active('website_hr_recruitment.job_filter_by_departments')"/>
<t t-set="opt_jobs_filter_offices" t-value="is_view_active('website_hr_recruitment.job_filter_by_offices')"/>
<t t-set="opt_jobs_filter_employment_type" t-value="is_view_active('website_hr_recruitment.job_filter_by_employment_type')"/>
<t t-set="opt_jobs_search_bar" t-value="is_view_active('website_hr_recruitment.job_search_bar')"/>
<!-- Filters -->
<ul class="o_jobs_topbar_filters flex-md-nowrap nav py-2 py-md-0 ps-lg-3"/>
<!-- Search bar -->
<div t-if="opt_jobs_search_bar" class="o_jobs_topbar_search_bar d-flex align-items-center justify-content-end w-md-100 w-lg-25 me-2 me-lg-0 ps-lg-2"/>
</template>
<!-- Filter - Countries -->
<template id="job_filter_by_countries" inherit_id="website_hr_recruitment.topbar_customizations" active="False" priority="10">
<xpath expr="//ul[hasclass('o_jobs_topbar_filters')]" position="inside">
<t t-set="non_country_params" t-valuef="#{current_department_param}#{current_office_param}#{current_employment_type_param}"/>
<li t-attf-class="nav-item dropdown w-100 w-md-auto me-2 my-1 flex-fill">
<button type="button" class="btn btn-outline-primary dropdown-toggle w-100" data-bs-toggle="dropdown" id="countriesDropdown">
<i class="fa fa-map-marker"/>
<t t-if="country_id" t-out="country_id.name"/>
<t t-elif="is_remote">Remote</t>
<t t-else="">All Countries</t>
</button>
<div class="dropdown-menu w-100 w-md-auto" aria-labelledby="countriesDropdown">
<a t-attf-href="/jobs?all_countries=1#{non_country_params}"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between nav-link#{'' if country_id or is_remote else ' active'}">
All Countries
<span t-attf-class="badge rounded-pill#{' bg-primary' if country_id or is_remote else ' bg-light text-primary'} ms-2 px-2" t-out="count_per_country.get('all', '0')"/>
</a>
<t t-foreach="countries" t-as="country">
<t t-if="country">
<a t-attf-href="/jobs?country_id=#{country.id}#{non_country_params}"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between nav-link #{' active' if country_id and country_id.id == country.id else ''}">
<t t-out="country.name"/>
<span t-attf-class="badge rounded-pill#{' bg-light text-primary' if country_id and country_id.id == country.id else ' bg-primary'} ms-2 px-2" t-out="count_per_country.get(country, '0')"/>
</a>
</t>
<t t-else="">
<a t-attf-href="/jobs?is_remote=1#{current_department_param}#{current_employment_type_param}"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between nav-link#{' active' if is_remote else ''}">
Remote
<span t-attf-class="badge rounded-pill#{' bg-light text-primary' if is_remote else ' bg-primary'} ms-2 px-2" t-out="count_per_country.get(None, '0')"/>
</a>
</t>
</t>
</div>
</li>
</xpath>
</template>
<template id="job_filter_by_departments" inherit_id="website_hr_recruitment.index" active="False" name="Filter by Departments">
<xpath expr="//div[@id='jobs_grid_left']" position="inside">
<ul class="nav nav-pills flex-column mb32">
<li class="nav-item"><a t-attf-href="/jobs#{ '/country/%s' % slug(country_id) if country_id else '' }#{ '/office/%s' % office_id if office_id else ''}#{ '/employment_type/%s' % contract_type_id if contract_type_id else '' }"
t-attf-class="nav-link#{'' if department_id else ' active'}">All Departments</a></li>
<t t-foreach="departments" t-as="department">
<li class="nav-item">
<a t-attf-href="/jobs#{ '/country/%s' % slug(country_id) if country_id else '' }/department/#{ slug(department) }#{ '/office/%s' % office_id if office_id else '' }#{ '/employment_type/%s' % contract_type_id if contract_type_id else '' }"
t-attf-class="nav-link#{' active' if department_id and department_id.id == department.id else ''}"><span t-field="department.name"/></a>
</li>
</t>
</ul>
</xpath>
<xpath expr="//div[@id='jobs_grid_left']" position="attributes">
<attribute name="class">col-lg-3</attribute>
<!-- Filter - Departments -->
<template id="job_filter_by_departments" inherit_id="website_hr_recruitment.topbar_customizations" active="False" priority="30">
<xpath expr="//ul[hasclass('o_jobs_topbar_filters')]" position="inside">
<t t-set="non_department_params" t-valuef="#{current_country_param}#{current_office_param}#{current_employment_type_param}"/>
<li t-attf-class="nav-item dropdown w-100 w-md-auto me-2 my-1 flex-fill">
<button type="button" class="btn btn-outline-primary dropdown-toggle w-100" data-bs-toggle="dropdown" id="departmentsDropdown">
<i class="fa fa-sitemap"/>
<t t-if="department_id" t-out="department_id.name"/>
<t t-elif="is_other_department">Others</t>
<t t-else="">All Departments</t>
</button>
<div class="dropdown-menu w-100 w-md-auto" aria-labelledby="departmentsDropdown">
<a t-attf-href="/jobs?all_departments=1#{non_department_params}"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between nav-link#{'' if department_id or is_other_department else ' active'}">
All Departments
<span t-attf-class="badge rounded-pill#{' bg-primary' if department_id or is_other_department else ' bg-light text-primary'} ms-2 px-2" t-out="count_per_department.get('all', '0')"/>
</a>
<t t-foreach="departments" t-as="department">
<t t-if="department">
<a t-attf-href="/jobs?department_id=#{department.id}#{non_department_params}"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between nav-link #{' active' if department_id and department_id.id == department.id else ''}">
<t t-out="department.name"/>
<span t-attf-class="badge rounded-pill#{' bg-light text-primary' if department_id and department_id.id == department.id else ' bg-primary'} ms-2 px-2" t-out="count_per_department.get(department, '0')"/>
</a>
</t>
<t t-else="">
<a t-attf-href="/jobs?is_other_department=1#{non_department_params}"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between nav-link #{' active' if is_other_department else ''}">
Others
<span t-attf-class="badge rounded-pill#{' bg-light text-primary' if is_other_department else ' bg-primary'} ms-2 px-2" t-out="count_per_department.get(None, '0')"/>
</a>
</t>
</t>
</div>
</li>
</xpath>
</template>
<template id="job_filter_by_offices" inherit_id="website_hr_recruitment.index" active="False" name="Filter by Offices">
<xpath expr="//div[@id='jobs_grid_left']" position="inside">
<ul class="nav nav-pills flex-column mb32">
<li class="nav-item"><a t-attf-href="/jobs#{ '/country/%s' % slug(country_id) if country_id else '' }#{ '/department/%s' % slug(department_id) if department_id else '' }#{ '/employment_type/%s' % contract_type_id if contract_type_id else '' }"
t-attf-class="nav-link#{'' if office_id else ' active'}">All Offices</a></li>
<t t-foreach="offices" t-as="thisoffice">
<li class="nav-item">
<a t-attf-href="/jobs#{ '/country/%s' % slug(country_id) if country_id else '' }#{ '/department/%s' % slug(department_id) if department_id else '' }/office/#{ thisoffice.id }#{ '/employment_type/%s' % contract_type_id if contract_type_id else '' }"
t-attf-class="nav-link#{' active' if office_id and office_id == thisoffice.id else ''}">
<span t-field="thisoffice.city"/>
<t t-if="thisoffice.country_id">,
<span t-field="thisoffice.country_id.name"/>
</t>
<!-- Filter - Offices -->
<template id="job_filter_by_offices" inherit_id="website_hr_recruitment.topbar_customizations" active="False" priority="20">
<xpath expr="//ul[hasclass('o_jobs_topbar_filters')]" position="inside">
<t t-set="non_location_params" t-valuef="#{current_department_param}#{current_employment_type_param}"/>
<li t-attf-class="nav-item dropdown w-100 w-md-auto me-2 my-1 flex-fill">
<button type="button" class="btn btn-outline-primary dropdown-toggle w-100" data-bs-toggle="dropdown" id="officesDropdown">
<i class="fa fa-building"/>
<t t-if="office_id">
<span t-if="len(office_id.contact_address) &lt;= 5" class="fst-italic">No address specified</span>
<t t-out="office_id.city"/><t t-if="office_id.country_id">, <t t-out="office_id.country_id.name"/></t>
</t>
<t t-elif="is_remote">Remote</t>
<t t-else="">All Offices</t>
</button>
<div class="dropdown-menu w-100 w-md-auto" aria-labelledby="officesDropdown">
<a t-attf-href="/jobs?{'all_countries=1' if is_remote else current_country_path}#{non_location_params}"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between nav-link#{'' if office_id or is_remote else ' active'}">
All Offices
<span t-attf-class="badge rounded-pill#{' bg-primary' if office_id or is_remote else ' bg-light text-primary'} ms-2 px-2" t-out="count_per_office.get('all', '0')"/>
</a>
<t t-foreach="offices" t-as="thisoffice">
<t t-if="thisoffice">
<a t-attf-href="/jobs?office_id=#{thisoffice.id}#{'' if is_remote else current_country_param}#{non_location_params}"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between nav-link #{' active' if office_id and office_id.id == thisoffice.id else ''}">
<t t-if="not (thisoffice.city or thisoffice.country_id)"><span class="fst-italic">No address specified</span></t>
<t t-else="">
<t t-out="thisoffice.city"/><t t-if="thisoffice.country_id">, <t t-out="thisoffice.country_id.name"/></t>
</t>
<span t-attf-class="badge rounded-pill#{' bg-light text-primary' if office_id and office_id.id == thisoffice.id else ' bg-primary'} ms-2 px-2" t-out="count_per_office.get(thisoffice, '0')"/>
</a>
</t>
<t t-else="">
<a t-attf-href="/jobs?is_remote=1#{non_location_params}"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between nav-link#{' active' if is_remote else ''}">
Remote
<span t-attf-class="badge rounded-pill#{' bg-light text-primary' if is_remote else ' bg-primary'} ms-2 px-2" t-out="count_per_office.get(None, '0')"/>
</a>
</t>
</t>
</div>
</li>
</xpath>
</template>
<!-- Filter - Employment Type -->
<template id="job_filter_by_employment_type" inherit_id="website_hr_recruitment.topbar_customizations" active="False" priority="40">
<xpath expr="//ul[hasclass('o_jobs_topbar_filters')]" position="inside">
<t t-set="non_employment_type_params" t-valuef="#{current_country_param}#{current_department_param}#{current_office_param}"/>
<li t-attf-class="nav-item dropdown w-100 w-md-auto me-2 my-1 flex-fill">
<button type="button" class="btn btn-outline-primary dropdown-toggle w-100" data-bs-toggle="dropdown" id="officesDropdown">
<i class="fa fa-suitcase"/>
<t t-if="contract_type_id and 'name' in contract_type_id" t-out="contract_type_id.name"/>
<t t-elif="is_untyped">Unspecified type</t>
<t t-else="">All Types</t>
</button>
<div class="dropdown-menu w-100 w-md-auto" aria-labelledby="employmentDropdown">
<a t-attf-href="/jobs?all_employment_types=1#{non_employment_type_params}"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between nav-link#{'' if contract_type_id or is_untyped else ' active'}">
All Types
<span t-attf-class="badge rounded-pill#{' bg-primary' if contract_type_id else ' bg-light text-primary'} ms-2 px-2" t-out="count_per_employment_type.get('all', '0')"/>
</a>
<t t-foreach="employment_types" t-as="employment_type">
<t t-if="employment_type">
<a t-attf-href="/jobs?contract_type_id=#{employment_type.id}#{non_employment_type_params}"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between nav-link #{' active' if contract_type_id and contract_type_id == employment_type.id else ''}">
<t t-out="employment_type.name"/>
<span t-attf-class="badge rounded-pill#{' bg-light text-primary' if contract_type_id and contract_type_id == employment_type.id else ' bg-primary'} ms-2 px-2" t-out="count_per_employment_type.get(employment_type, '0')"/>
</a>
</t>
</t>
<t t-if="count_per_employment_type.get(None, 0)">
<a t-attf-href="/jobs?is_untyped=1#{non_employment_type_params}"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between nav-link#{' active' if is_untyped else ''}">
Unspecified type
<span t-attf-class="badge rounded-pill#{' bg-light text-primary' if is_remote else ' bg-primary'} ms-2 px-2" t-out="count_per_employment_type.get(None, '0')"/>
</a>
</li>
</t>
</ul>
</xpath>
<xpath expr="//div[@id='jobs_grid_left']" position="attributes">
<attribute name="class">col-lg-3</attribute>
</t>
</div>
</li>
</xpath>
</template>
<template id="job_filter_by_employment_type" inherit_id="website_hr_recruitment.index" active="False" name="Filter by Employment Type">
<xpath expr="//div[@id='jobs_grid_left']" position="inside">
<ul class="nav nav-pills flex-column mb32">
<li class="nav-item"><a t-attf-href="/jobs#{ '/country/%s' % slug(country_id) if country_id else '' }#{ '/department/%s' % slug(department_id) if department_id else '' }#{ '/office/%s' % office_id if office_id else '' }"
t-attf-class="nav-link#{'' if contract_type_id else ' active'}">All Employment Types</a></li>
<t t-foreach="employment_types" t-as="employment_type">
<li class="nav-item">
<a t-attf-href="/jobs#{ '/country/%s' % slug(country_id) if country_id else '' }#{ '/department/%s' % slug(department_id) if department_id else '' }#{ '/office/%s' % office_id if office_id else '' }/employment_type/#{ employment_type.id }"
t-attf-class="nav-link#{' active' if contract_type_id and contract_type_id == employment_type.id else ''}">
<span t-field="employment_type.name"/>
</a>
</li>
</t>
</ul>
</xpath>
<xpath expr="//div[@id='jobs_grid_left']" position="attributes">
<attribute name="class">col-lg-3</attribute>
<!-- Search Bar -->
<template id="job_search_bar" inherit_id="website_hr_recruitment.topbar_customizations" active="True">
<xpath expr="//div[hasclass('o_jobs_topbar_search_bar')]" position="inside">
<t t-call="website.website_search_box_input">
<t t-set="_form_classes" t-valuef="w-100"/>
<t t-set="_classes" t-valuef="my-1"/>
<t t-set="search_type" t-valuef="jobs"/>
<t t-set="display_description" t-valuef="true"/>
<input t-if="country_id" type="hidden" name="country_id" t-att-value="country_id.id"/>
<input t-if="office_id" type="hidden" name="office_id" t-att-value="office_id.id"/>
<input t-if="department_id" type="hidden" name="department_id" t-att-value="department_id.id"/>
<input t-if="contract_type_id" type="hidden" name="contract_type_id" t-att-value="contract_type_id.id"/>
<input t-if="is_remote" type="hidden" name="is_remote" value="1"/>
<input t-if="is_other_department" type="hidden" name="is_other_department" value="1"/>
</t>
</xpath>
</template>
<!-- Right Side Bar -->
<template id="job_right_side_bar" inherit_id="website_hr_recruitment.index" active="True" name="Right Side Bar">
<xpath expr="//div[@id='jobs_grid']" position="after">
<div class="col-lg-4 oe_structure oe_empty" id="jobs_grid_right">