diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 4905bd42d9b..b42e0d61f35 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -417,7 +417,7 @@ class Website(Home):
continue
field_type = field_meta.get('type')
if field_type == 'text':
- if value:
+ if value and field_meta.get('truncate', True):
value = shorten(value, max_nb_chars, placeholder='...')
if field_meta.get('match') and value and term:
pattern = '|'.join(map(re.escape, term.split()))
diff --git a/addons/website/models/website_page.py b/addons/website/models/website_page.py
index 98c198973da..dc2c8cb5a7c 100644
--- a/addons/website/models/website_page.py
+++ b/addons/website/models/website_page.py
@@ -265,7 +265,7 @@ class Page(models.Model):
fetch_fields = ['id', 'name', 'url']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
- 'website_url': {'name': 'url', 'type': 'text'},
+ 'website_url': {'name': 'url', 'type': 'text', 'truncate': False},
}
if with_description:
search_fields.append('arch_db')
diff --git a/addons/website/tests/test_fuzzy.py b/addons/website/tests/test_fuzzy.py
index 00e5aa0a1c5..bda44e95fe1 100644
--- a/addons/website/tests/test_fuzzy.py
+++ b/addons/website/tests/test_fuzzy.py
@@ -4,13 +4,16 @@
import logging
from lxml import etree
import re
+from markupsafe import Markup
-from odoo.addons.website.tools import distance
+from odoo.addons.website.controllers.main import Website
+from odoo.addons.website.tools import distance, MockRequest
import odoo.tests
from odoo.tests.common import TransactionCase
_logger = logging.getLogger(__name__)
+
@odoo.tests.tagged('-at_install', 'post_install')
class TestFuzzy(TransactionCase):
def test_01_fuzzy_names(self):
@@ -36,7 +39,7 @@ class TestFuzzy(TransactionCase):
continue
model = self.env[model_name]
if 'description' not in fields and 'description' in model:
- fields.append('description') # larger target dataset
+ fields.append('description') # larger target dataset
records = model.sudo().search_read([], fields, limit=100)
for record in records:
for field, value in record.items():
@@ -51,6 +54,7 @@ class TestFuzzy(TransactionCase):
website = self.env.ref('website.default_website')
typos = {}
+
def add_typo(expected, typo):
if typo not in words:
typos.setdefault(typo, set()).add(expected)
@@ -67,7 +71,7 @@ class TestFuzzy(TransactionCase):
add_typo(search, search[:index - 1] + '!' + search[index:])
words = list(words)
- words.sort() # guarantee results stability
+ words.sort() # guarantee results stability
mismatch_count = 0
for search, expected in typos.items():
fuzzy_guess = website._search_find_fuzzy_term({}, search, word_list=words)
@@ -97,3 +101,112 @@ class TestFuzzy(TransactionCase):
self.assertEqual(distance("", "warranty", 10), 8)
self.assertEqual(distance("warranty", "", 10), 8)
self.assertEqual(distance("", "", 10), 0)
+
+
+@odoo.tests.tagged('-at_install', 'post_install')
+class TestAutoComplete(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.website = cls.env['website'].browse(1)
+ cls.WebsiteController = Website()
+ cls.options = {
+ 'displayDescription': True,
+ }
+ cls.expectedParts = {
+ 'name': True,
+ 'description': True,
+ 'website_url': True,
+ }
+ texts = [
+ "This page only matches few",
+ "This page matches both few and many",
+ "This page only matches many",
+ "Many results contain this page",
+ "How many times does the word many appear",
+ "Will there be many results",
+ "Welcome to our many friends",
+ "This should only be approximately matched",
+ ]
+ for text in texts:
+ cls._create_page(text, text, f"/{text.lower().replace(' ', '-')}")
+
+ @classmethod
+ def _create_page(cls, name, content, url):
+ cls.env['website.page'].create({
+ 'name': name,
+ 'type': 'qweb',
+ 'arch': f'
{content}
',
+ 'url': url,
+ 'is_published': True,
+ })
+
+ def _autocomplete(self, term):
+ """ Calls the autocomplete for a given term and performs general checks """
+ with MockRequest(self.env, website=self.website):
+ suggestions = self.WebsiteController.autocomplete(
+ search_type="pages", term=term, max_nb_chars=50, options=self.options,
+ )
+ if suggestions['results_count']:
+ self.assertDictEqual(self.expectedParts, suggestions['parts'],
+ f"Parts should contain {self.expectedParts.keys()}")
+ for result in suggestions['results']:
+ self.assertEqual("fa-file-o", result['_fa'], "Expect an fa icon")
+ for field in suggestions['parts'].keys():
+ value = result[field]
+ if value:
+ self.assertTrue(
+ isinstance(value, Markup),
+ f"All fields should be wrapped in Markup: found {type(value)}: '{value}' in {field}"
+ )
+ return suggestions
+
+ def _check_highlight(self, term, value):
+ """ Verifies if a term is highlighted in a value """
+ self.assertTrue(f'{term}' in value.lower(),
+ "Term must be highlighted")
+
+ def test_01_few_results(self):
+ """ Tests an autocomplete with exact match and less than the maximum number of results """
+ suggestions = self._autocomplete("few")
+ self.assertEqual(2, suggestions['results_count'], "Text data contains two pages with 'few'")
+ self.assertEqual(2, len(suggestions['results']), "All results must be present")
+ self.assertFalse(suggestions['fuzzy_search'], "Expects an exact match")
+ for result in suggestions['results']:
+ self._check_highlight("few", result['name'])
+ self._check_highlight("few", result['description'])
+
+ def test_02_many_results(self):
+ """ Tests an autocomplete with exact match and more than the maximum number of results """
+ suggestions = self._autocomplete("many")
+ self.assertEqual(6, suggestions['results_count'], "Test data contains six pages with 'many'")
+ self.assertEqual(5, len(suggestions['results']), "Results must be limited to 5")
+ self.assertFalse(suggestions['fuzzy_search'], "Expects an exact match")
+ for result in suggestions['results']:
+ self._check_highlight("many", result['name'])
+ self._check_highlight("many", result['description'])
+
+ def test_03_no_result(self):
+ """ Tests an autocomplete without matching results """
+ suggestions = self._autocomplete("nothing")
+ self.assertEqual(0, suggestions['results_count'], "Text data contains no page with 'nothing'")
+ self.assertEqual(0, len(suggestions['results']), "No result must be present")
+
+ def test_04_fuzzy_results(self):
+ """ Tests an autocomplete with fuzzy matching results """
+ suggestions = self._autocomplete("appoximtly")
+ self.assertEqual("approximately", suggestions['fuzzy_search'], "")
+ self.assertEqual(1, suggestions['results_count'], "Text data contains one page with 'approximately'")
+ self.assertEqual(1, len(suggestions['results']), "Single result must be present")
+ for result in suggestions['results']:
+ self._check_highlight("approximately", result['name'])
+ self._check_highlight("approximately", result['description'])
+
+ def test_05_long_url(self):
+ """ Ensures that long URL do not get truncated """
+ url = "/this-url-is-so-long-it-would-be-truncated-without-the-fix"
+ self._create_page("Too long", "Way too long URL", url)
+ suggestions = self._autocomplete("long url")
+ self.assertEqual(1, suggestions['results_count'], "Text data contains one page with 'long url'")
+ self.assertEqual(1, len(suggestions['results']), "Single result must be present")
+ self.assertEqual(url, suggestions['results'][0]['website_url'], 'URL must not be truncated')
diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py
index 2794514552b..99bc7e116f0 100644
--- a/addons/website_blog/models/website_blog.py
+++ b/addons/website_blog/models/website_blog.py
@@ -99,7 +99,7 @@ class Blog(models.Model):
fetch_fields = ['id', 'name']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
- 'website_url': {'name': 'url', 'type': 'text'},
+ 'website_url': {'name': 'url', 'type': 'text', 'truncate': False},
}
if with_description:
search_fields.append('subtitle')
@@ -336,7 +336,7 @@ class BlogPost(models.Model):
fetch_fields = ['name', 'website_url']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
- 'website_url': {'name': 'website_url', 'type': 'text'},
+ 'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
}
if with_description:
search_fields.append('content')
diff --git a/addons/website_event/models/event_event.py b/addons/website_event/models/event_event.py
index 41570117f58..9cfa57d2fab 100644
--- a/addons/website_event/models/event_event.py
+++ b/addons/website_event/models/event_event.py
@@ -522,7 +522,7 @@ class Event(models.Model):
fetch_fields = ['name', 'website_url']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
- 'website_url': {'name': 'website_url', 'type': 'text'},
+ 'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
}
if with_description:
search_fields.append('subtitle')
diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py
index 5c519646883..baee75d70c5 100644
--- a/addons/website_forum/models/forum.py
+++ b/addons/website_forum/models/forum.py
@@ -275,7 +275,7 @@ class Forum(models.Model):
fetch_fields = ['id', 'name']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
- 'website_url': {'name': 'website_url', 'type': 'text'},
+ 'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
}
if with_description:
search_fields.append('description')
@@ -985,7 +985,7 @@ class Post(models.Model):
fetch_fields = ['id', 'name']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
- 'website_url': {'name': 'website_url', 'type': 'text'},
+ 'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
}
domain = website.website_domain()
diff --git a/addons/website_sale/models/product_public_category.py b/addons/website_sale/models/product_public_category.py
index 403de6e36cb..b22c141c6e6 100644
--- a/addons/website_sale/models/product_public_category.py
+++ b/addons/website_sale/models/product_public_category.py
@@ -57,7 +57,7 @@ class ProductPublicCategory(models.Model):
fetch_fields = ['id', 'name']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
- 'website_url': {'name': 'url', 'type': 'text'},
+ 'website_url': {'name': 'url', 'type': 'text', 'truncate': False},
}
if with_description:
search_fields.append('website_description')
diff --git a/addons/website_sale/models/product_template.py b/addons/website_sale/models/product_template.py
index 067d7b87c2c..5c9de19c184 100644
--- a/addons/website_sale/models/product_template.py
+++ b/addons/website_sale/models/product_template.py
@@ -366,7 +366,7 @@ class ProductTemplate(models.Model):
fetch_fields = ['id', 'name', 'website_url']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
- 'website_url': {'name': 'website_url', 'type': 'text'},
+ 'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
}
if with_image:
mapping['image_url'] = {'name': 'image_url', 'type': 'html'}
diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py
index 80d4cf6fca5..21b09fac862 100644
--- a/addons/website_slides/models/slide_channel.py
+++ b/addons/website_slides/models/slide_channel.py
@@ -873,7 +873,7 @@ class Channel(models.Model):
fetch_fields = ['name', 'website_url']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
- 'website_url': {'name': 'website_url', 'type': 'text'},
+ 'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
}
if with_description:
search_fields.append('description_short')
diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py
index a62953d6090..631945e371d 100644
--- a/addons/website_slides/models/slide_slide.py
+++ b/addons/website_slides/models/slide_slide.py
@@ -1234,9 +1234,9 @@ class Slide(models.Model):
fetch_fields = ['id', 'name']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
- 'website_url': {'name': 'url', 'type': 'text'},
+ 'website_url': {'name': 'url', 'type': 'text', 'truncate': False},
'extra_link': {'name': 'course', 'type': 'text'},
- 'extra_link_url': {'name': 'course_url', 'type': 'text'},
+ 'extra_link_url': {'name': 'course_url', 'type': 'text', 'truncate': False},
}
if with_description:
search_fields.append('description')