Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion addons/portal_rating/views/rating_templates.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<t t-foreach="range(0, empty_star)" t-as="num">
<i class="fa fa-star-o" role="img"></i>
</t>
<small class="text-muted ms-1">
<small t-if="not hide_rating_count" class="text-muted ms-1">
(<t t-out="rating_count"/>)
</small>
</t>
Expand Down
1 change: 1 addition & 0 deletions addons/test_website/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
],
'web.assets_frontend': [
'test_website/static/src/interactions/**/*',
'test_website/static/src/snippets/**/*.xml',
],
'website.website_builder_assets': [
'test_website/static/src/website_builder/**/*',
Expand Down
2 changes: 2 additions & 0 deletions addons/test_website/models/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ def _search_get_detail(self, website, order, options):
},
'icon': 'fa-check-square-o',
'order': 'name asc, id desc',
'template': 'test_website.search_result_item_test_model',
'group_name': self.env._("Test Models"),
}

def _get_search_highlight_handlers(self):
Expand Down
15 changes: 15 additions & 0 deletions addons/test_website/static/src/snippets/s_searchbar/000.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>

<templates xml:space="preserve">

<t t-name="test_website.search_result_item_test_model">
<t t-foreach="bucket.data" t-as="result" t-key="result_index">
<a t-att-href="result['website_url']" class="h-100 p-2 o_search_result_item">
<div class="o_search_result_item_content">
<div class="fs-6 fw-bold mb-0" t-out="result['name']"/>
</div>
</a>
</t>
</t>

</templates>
8 changes: 2 additions & 6 deletions addons/test_website/tests/test_fuzzy.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ def _autocomplete(self, term, expected_count, expected_fuzzy_term, search_type="

def _autocomplete_page(self, term, expected_count, expected_fuzzy_term):
self._autocomplete(term, expected_count, expected_fuzzy_term, search_type="pages", options={
'displayDescription': False, 'displayDetail': False,
'displayExtraDetail': False, 'displayExtraLink': False,
'displayImage': False, 'allowFuzzy': True
'allowFuzzy': True
})

def test_01_many_records(self):
Expand Down Expand Up @@ -98,9 +96,7 @@ def test_02_pages_search(self):
# ORM level, not in SQL, due to how `inherits` works.
self.env['website'].browse(1)._search_with_fuzzy(
'pages', 'test', limit=5, order='name asc, website_id desc, id', options={
'displayDescription': False, 'displayDetail': False,
'displayExtraDetail': False, 'displayExtraLink': False,
'displayImage': False, 'allowFuzzy': True
'allowFuzzy': True
}
)

Expand Down
123 changes: 57 additions & 66 deletions addons/website/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,14 +587,15 @@ def _get_search_order(self, order):
return 'is_published desc, %s, id desc' % order

@http.route('/website/snippet/autocomplete', type='jsonrpc', auth='public', website=True, readonly=True)
def autocomplete(self, search_type=None, term=None, order=None, limit=5, max_nb_chars=999, options=None):
def autocomplete(self, search_type=None, term=None, order=None, limit=6, offset=0, max_nb_chars=999, options=None):
"""
Returns list of results according to the term and options

:param str search_type: indicates what to search within, 'all' matches all available types
:param str term: search term written by the user
:param str order:
:param int limit: number of results to consider, defaults to 5
:param int offset: number of results to skip, defaults to 0
:param int max_nb_chars: max number of characters for text fields
:param dict options: options map containing
allowFuzzy: enables the fuzzy matching when truthy
Expand All @@ -611,55 +612,66 @@ def autocomplete(self, search_type=None, term=None, order=None, limit=5, max_nb_
"""
order = self._get_search_order(order)
options = options or {}
results_count, search_results, fuzzy_term = request.website._search_with_fuzzy(search_type, term, limit, order, options)
results_count, search_results, fuzzy_term = request.website._search_with_fuzzy(search_type, term, limit, offset, order, options)
# Sort result based in sequence for ordered results.
search_results.sort(key=lambda d: d.get('sequence', float('inf')))
if not results_count:
return {
'results': [],
'results': {},
'results_count': 0,
'parts': {},
}
term = fuzzy_term or term
search_results = request.website._search_render_results(search_results, limit)
search_results = request.website.sudo()._search_render_results(search_results, limit)

mappings = []
results_data = []
result = {}
for search_result in search_results:
for result in search_result['results_data']:
result['model'] = search_result['model']
results_data.append(result)
if not len(search_result['results_data']):
continue
search_result['results_data'].sort(key=lambda r: r.get('name', ''), reverse='name desc' in order)
mappings.append(search_result['mapping'])
if search_type == 'all':
# Only supported order for 'all' is on name
results_data.sort(key=lambda r: r.get('name', ''), reverse='name desc' in order)
results_data = results_data[:limit]
result = []
for record in results_data:
mapping = record['_mapping']
model = request.env[record['model']]
mapped = {
'_fa': record.get('_fa'),
}
for mapped_name, field_meta in mapping.items():
value = record.get(field_meta.get('name'))
if not value:
mapped[mapped_name] = ''
continue
field_type = field_meta.get('type')
if field_type == 'text' and field_meta.get('truncate', True):
value = self._shorten_around_match(value, term, max_nb_chars)

if field_meta.get('match'):
skip_field, value, field_type = model._search_highlight_field(field_meta, value, term)
if skip_field:
group_name = search_result.get("group_name")
group_key = '_'.join(group_name.lower().split())
result_data = []
for record in search_result['results_data']:
model = request.env[search_result['model']]
mapping = record['_mapping']
mapped = {
'_fa': record.get('_fa'),
}
model = request.env[search_result['model']]
for mapped_name, field_meta in mapping.items():
value = record.get(field_meta.get('name'))
if not value:
mapped[mapped_name] = ''
continue

if field_type not in ('image', 'binary') and ('ir.qweb.field.%s' % field_type) in request.env:
opt = {}
if field_type == 'monetary':
opt['display_currency'] = options['display_currency']
value = request.env[('ir.qweb.field.%s' % field_type)].value_to_html(value, opt)
mapped[mapped_name] = escape(value)
result.append(mapped)
field_type = field_meta.get('type')
if field_type == 'text' and field_meta.get('truncate', True):
value = self._shorten_around_match(value, term, max_nb_chars)

if field_meta.get('match'):
skip_field, value, field_type = model._search_highlight_field(field_meta, value, term)
if skip_field:
continue

if field_type not in ('image', 'binary') and ('ir.qweb.field.%s' % field_type) in request.env:
opt = {}
if field_type == 'monetary':
opt['display_currency'] = options['display_currency']
elif field_type == 'float':
opt['precision'] = field_meta.get('precision', 2)
value = request.env[('ir.qweb.field.%s' % field_type)].value_to_html(value, opt)
mapped[mapped_name] = escape(value)
result_data.append(mapped)

result[group_key] = {
"groupName": group_name,
"templateKey": search_result.get("template_key"),
"searchCount": search_result.get('count'),
"searchType": search_result.get("model").replace('.', '_'),
"data": result_data,
}

return {
'results': result,
Expand All @@ -670,11 +682,6 @@ def autocomplete(self, search_type=None, term=None, order=None, limit=5, max_nb_

def _get_page_search_options(self, **post):
return {
'displayDescription': False,
'displayDetail': False,
'displayExtraDetail': False,
'displayExtraLink': False,
'displayImage': False,
'allowFuzzy': not post.get('noFuzzy'),
}

Expand Down Expand Up @@ -708,47 +715,31 @@ def pages_list(self, page=1, search='', **kw):

def _get_hybrid_search_options(self, **post):
return {
'displayDescription': True,
'displayDetail': True,
'displayExtraDetail': True,
'displayExtraLink': True,
'displayImage': True,
'allowFuzzy': not post.get('noFuzzy'),
}

@http.route([
'/website/search',
'/website/search/page/<int:page>',
'/website/search/<string:search_type>',
'/website/search/<string:search_type>/page/<int:page>',
], type='http', auth="public", website=True, sitemap=False, readonly=True)
def hybrid_list(self, page=1, search='', search_type='all', **kw):
def hybrid_list(self, search='', search_type='all', **kw):
if not search:
return request.render("website.list_hybrid")

options = self._get_hybrid_search_options(**kw)
data = self.autocomplete(search_type=search_type, term=search, order='name asc', limit=500, max_nb_chars=200, options=options)
limit = 24
data = self.autocomplete(search_type=search_type, term=search, order='name asc', limit=limit, offset=0, max_nb_chars=200, options=options)

results = data.get('results', [])
search_count = len(results)
search_count = data.get('results_count', 0)
parts = data.get('parts', {})

step = 50
pager = portal_pager(
url="/website/search/%s" % search_type,
url_args={'search': search},
total=search_count,
page=page,
step=step
)

results = results[(page - 1) * step:page * step]

# TODO: Implement pagination or load more button for more records
values = {
'pager': pager,
'results': results,
'parts': parts,
'search': search,
'limit': limit,
'fuzzy_search': data.get('fuzzy_search'),
'search_count': search_count,
}
Expand Down
15 changes: 13 additions & 2 deletions addons/website/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ def _get_background(self, height=None, width=None):
img = img[:-1] + suffix + ')'
return img

def _get_image_url(self):
self.ensure_one()
properties = json_safe.loads(self.cover_properties)
img = properties.get('background-image', None)
if not img:
return None
match = re.search(r"url\(\s*(['\"]?)(.*?)\1\s*\)", img)
return match.group(2) if match else img

def write(self, vals):
if 'cover_properties' not in vals:
return super().write(vals)
Expand Down Expand Up @@ -680,21 +689,23 @@ def _search_get_detail(self, website, order, options):
raise NotImplementedError()

@api.model
def _search_fetch(self, search_detail, search, limit, order):
def _search_fetch(self, search_detail, search, limit, offset, order):
fields = search_detail['search_fields']
base_domain = search_detail['base_domain']
domain = self._search_build_domain(base_domain, search, fields, search_detail.get('search_extra'))
model = self.sudo() if search_detail.get('requires_sudo') else self
results = model.search(
domain,
limit=limit,
offset=offset,
order=search_detail.get('order', order)
)
count = model.search_count(domain) if limit and limit == len(results) else len(results)
return results, count

def _search_render_results(self, fetch_fields, mapping, icon, limit):
results_data = self.read(fetch_fields)[:limit]
# Some fields are not avaiable in public user group - require sudo to complete result
results_data = self.sudo().read(fetch_fields)[:limit]
for result in results_data:
result['_fa'] = icon
result['_mapping'] = mapping
Expand Down
15 changes: 8 additions & 7 deletions addons/website/models/website.py
Original file line number Diff line number Diff line change
Expand Up @@ -2052,17 +2052,18 @@ def _search_get_details(self, search_type, order, options):
:return: list of search details obtained from the `website.searchable.mixin`'s `_search_get_detail()`
"""
result = []
if search_type in ['pages', 'all']:
if search_type in ['website_page', 'all']:
result.append(self.env['website.page']._search_get_detail(self, order, options))
return result

def _search_with_fuzzy(self, search_type, search, limit, order, options):
def _search_with_fuzzy(self, search_type, search, limit, offset, order, options):
"""
Performs a search with a search text or with a resembling word

:param search_type: indicates what to search within, 'all' matches all available types
:param search: text against which to match results
:param limit: maximum number of results per model type involved in the result
:param offset: number of results to skip per model type involved in the result
:param order: order on which to sort results within a model type
:param options: search options from the submitted form containing:
- allowFuzzy: boolean indicating whether the fuzzy matching must be done
Expand All @@ -2079,16 +2080,16 @@ def _search_with_fuzzy(self, search_type, search, limit, order, options):
if search and options.get('allowFuzzy', True):
fuzzy_term = self._search_find_fuzzy_term(search_details, search)
if fuzzy_term:
count, results = self._search_exact(search_details, fuzzy_term, limit, order)
count, results = self._search_exact(search_details, fuzzy_term, limit, offset, order)
if fuzzy_term.lower() == search.lower():
fuzzy_term = False
else:
count, results = self._search_exact(search_details, search, limit, order)
count, results = self._search_exact(search_details, search, limit, offset, order)
else:
count, results = self._search_exact(search_details, search, limit, order)
count, results = self._search_exact(search_details, search, limit, offset, order)
return count, results, fuzzy_term

def _search_exact(self, search_details, search, limit, order):
def _search_exact(self, search_details, search, limit, offset, order):
"""
Performs a search with a search text

Expand All @@ -2108,7 +2109,7 @@ def _search_exact(self, search_details, search, limit, order):
total_count = 0
for search_detail in search_details:
model = self.env[search_detail['model']]
results, count = model._search_fetch(search_detail, search, limit, order)
results, count = model._search_fetch(search_detail, search, limit, offset, order)
search_detail['results'] = results
total_count += count
search_detail['count'] = count
Expand Down
15 changes: 7 additions & 8 deletions addons/website/models/website_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ def get_website_meta(self):

@api.model
def _search_get_detail(self, website, order, options):
with_description = options['displayDescription']
# Read access on website.page requires sudo.
requires_sudo = True
domain = [website.website_domain()]
Expand All @@ -224,16 +223,13 @@ def _search_get_detail(self, website, order, options):
[('group_ids', '=', False)], [('group_ids', 'in', self.env.user.group_ids.ids)]
]))

search_fields = ['name', 'url']
fetch_fields = ['id', 'name', 'url']
search_fields = ['name', 'url', 'arch_db']
fetch_fields = ['id', 'name', 'url', 'arch']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
'website_url': {'name': 'url', 'type': 'text', 'truncate': False},
'description': {'name': 'arch', 'type': 'text', 'html': True, 'match': True}
}
if with_description:
search_fields.append('arch_db')
fetch_fields.append('arch')
mapping['description'] = {'name': 'arch', 'type': 'text', 'html': True, 'match': True}
return {
'model': 'website.page',
'base_domain': domain,
Expand All @@ -242,10 +238,13 @@ def _search_get_detail(self, website, order, options):
'fetch_fields': fetch_fields,
'mapping': mapping,
'icon': 'fa-file-o',
'template_key': 'website.search_items_page',
'group_name': self.env._("Pages"),
'sequence': 10,
}

@api.model
def _search_fetch(self, search_detail, search, limit, order):
def _search_fetch(self, search_detail, search, limit, offset, order):
with_description = 'description' in search_detail['mapping']
# Cannot rely on the super's _search_fetch because the search must be
# performed among the most specific pages only.
Expand Down
Loading