[IMP] website_forum: search and filter tags

This commit enables user to

* Track, show, and use the forum's
  * Most commonly used tags
  * Unused tags (for the moderators)
* Search tags
* Get an improved "no results" experience with tags

Task-3349375

Part-of: odoo/odoo#123709
This commit is contained in:
Florian Charlier
2023-07-18 16:58:43 +02:00
parent 21531bd9b9
commit 3d1d5bfb09
9 changed files with 265 additions and 102 deletions
@@ -1,25 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import logging
from datetime import datetime
import lxml
import requests
import logging
import werkzeug.exceptions
import werkzeug.urls
import werkzeug.wrappers
from datetime import datetime
from odoo import http, tools, _
from odoo.exceptions import AccessError
from odoo import _, http, tools
from odoo.addons.http_routing.models.ir_http import slug
from odoo.addons.portal.controllers.portal import _build_url_w_params
from odoo.addons.website.models.ir_http import sitemap_qs2dom
from odoo.addons.website_profile.controllers.main import WebsiteProfile
from odoo.addons.portal.controllers.portal import _build_url_w_params
from odoo.exceptions import UserError
from odoo.exceptions import AccessError, UserError
from odoo.http import request
from odoo.osv import expression
_logger = logging.getLogger(__name__)
@@ -167,6 +165,9 @@ class WebsiteForum(WebsiteProfile):
values = self._prepare_user_values(forum=forum, header={'is_guidelines': True, 'is_karma': True}, **post)
return request.render("website_forum.faq_karma", values)
# Tags
# --------------------------------------------------
@http.route('/forum/get_tags', type='http', auth="public", methods=['GET'], website=True, sitemap=False)
def tag_read(self, forum_id, query='', limit=25, **post):
data = request.env['forum.tag'].search_read(
@@ -179,31 +180,63 @@ class WebsiteForum(WebsiteProfile):
headers=[("Content-Type", "application/json")]
)
@http.route(['/forum/<model("forum.forum"):forum>/tag', '/forum/<model("forum.forum"):forum>/tag/<string:tag_char>'], type='http', auth="public", website=True, sitemap=False)
def tags(self, forum, tag_char=None, **post):
# build the list of tag first char, with their value as tag_char param Ex : [('All', 'all'), ('C', 'c'), ('G', 'g'), ('Z', z)]
first_char_tag = forum.get_tags_first_char()
first_char_list = [(t, t.lower()) for t in first_char_tag if t.isalnum()]
first_char_list.insert(0, (_('All'), 'all'))
@http.route(['/forum/<model("forum.forum"):forum>/tag',
'/forum/<model("forum.forum"):forum>/tag/<string:tag_char>',
], type='http', auth="public", website=True, sitemap=False)
def tags(self, forum, tag_char='', filters='all', search='', **post):
"""Render a list of tags matching filters and search parameters.
active_char_tag = tag_char and tag_char.lower() or 'all'
:param forum: Forum
:param string tag_char: Only tags starting with a single character `tag_char`
:param filters: One of 'all'|'followed'|'most_used'|'unused'.
Can be combined with `search` and `tag_char`.
:param string search: Search query using "forum_tags_only" `search_type`
:param dict post: additional options passed to `_prepare_user_values`
"""
if not isinstance(tag_char, str) or len(tag_char) > 1 or (tag_char and not tag_char.isalpha()):
# So that further development does not miss this. Users shouldn't see it with normal usage.
raise werkzeug.exceptions.BadRequest(_('Bad "tag_char" value "%(tag_char)s"', tag_char=tag_char))
# generate domain for searched tags
domain = [('forum_id', '=', forum.id), ('posts_count', '>', 0)]
order_by = 'name'
if active_char_tag and active_char_tag != 'all':
domain.append(('name', '=ilike', tools.escape_psql(active_char_tag) + '%'))
order_by = 'posts_count DESC'
tags = request.env['forum.tag'].search(domain, limit=None, order=order_by)
# prepare values and render template
domain = [('forum_id', '=', forum.id), ('posts_count', '=' if filters == "unused" else '>', 0)]
if filters == 'followed' and not request.env.user._is_public():
domain = expression.AND([domain, [('message_is_follower', '=', True)]])
# Build tags result without using tag_char to build pager, then return tags matching it
values = self._prepare_user_values(forum=forum, searches={'tags': True}, **post)
tags = request.env["forum.tag"]
order = 'posts_count DESC' if tag_char else 'name'
if search:
values.update(search=search)
search_domain = domain if filters in ('all', 'followed') else None
__, details, __ = request.website._search_with_fuzzy(
'forum_tags_only', search, limit=None, order=order, options={'forum': forum, 'domain': search_domain},
)
tags = details[0].get('results', tags)
if filters in ('unused', 'most_used'):
filter_tags = forum.tag_most_used_ids if filters == 'most_used' else forum.tag_unused_ids
tags = tags & filter_tags if tags else filter_tags
elif filters in ('all', 'followed'):
if not search:
tags = request.env['forum.tag'].search(domain, limit=None, order=order)
else:
raise werkzeug.exceptions.BadRequest(_('Bad "filters" value "%(filters)s".', filters=filters))
first_char_tag = forum._get_tags_first_char(tags=tags)
first_char_list = [(t, t.lower()) for t in first_char_tag if t.isalnum()]
first_char_list.insert(0, (_('All'), ''))
if tag_char:
tags = tags.filtered(lambda t: t.name.startswith((tag_char.lower(), tag_char.upper())))
values.update({
'tags': tags,
'active_char_tag': tag_char.lower(),
'pager_tag_chars': first_char_list,
'active_char_tag': active_char_tag,
'search_count': len(tags) if search else None,
'tags': tags,
})
return request.render("website_forum.tag", values)
return request.render("website_forum.forum_index_tags", values)
# Questions
# --------------------------------------------------
+47 -6
View File
@@ -2,12 +2,16 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import textwrap
from collections import defaultdict
from operator import itemgetter
from markupsafe import Markup
from odoo import _, api, fields, models
from odoo.tools.translate import html_translate
from odoo.addons.http_routing.models.ir_http import slug
from odoo.tools.translate import html_translate
MOST_USED_TAGS_COUNT = 5 # Number of tags to track as "most used" to display on frontend
class Forum(models.Model):
@@ -20,7 +24,7 @@ class Forum(models.Model):
'website.multi.mixin',
'website.searchable.mixin',
]
_order = "sequence"
_order = "sequence, id"
def _get_default_welcome_message(self):
return Markup("""
@@ -136,6 +140,11 @@ class Forum(models.Model):
has_pending_post = fields.Boolean(string='Has pending post', compute='_compute_has_pending_post')
can_moderate = fields.Boolean(string="Is a moderator", compute="_compute_can_moderate")
# tags
tag_ids = fields.One2many('forum.tag', 'forum_id', string='Tags')
tag_most_used_ids = fields.One2many('forum.tag', string="Most used tags", compute='_compute_tag_ids_usage')
tag_unused_ids = fields.One2many('forum.tag', string="Unused tags", compute='_compute_tag_ids_usage')
@api.depends_context('uid')
def _compute_has_pending_post(self):
domain = [
@@ -156,6 +165,35 @@ class Forum(models.Model):
for forum in self:
forum.can_moderate = self.env.user.karma >= forum.karma_moderate
@api.depends('post_ids', 'post_ids.tag_ids', 'post_ids.tag_ids.posts_count', 'tag_ids')
def _compute_tag_ids_usage(self):
forums_without_tags = self.filtered(lambda f: not f.tag_ids)
forums_without_tags.tag_most_used_ids = forums_without_tags.tag_unused_ids = False
forums_with_tags = self - forums_without_tags
if not forums_with_tags:
return
tags_data = self.env['forum.tag'].search_read(
[('forum_id', 'in', forums_with_tags.ids)],
fields=['id', 'forum_id', 'posts_count'],
order='forum_id, posts_count DESC, name, id',
)
current_forum_id = tags_data[0]['forum_id'][0]
forum_tags = defaultdict(lambda: {'most_used_ids': [], 'unused_ids': []})
for tag_data in tags_data:
tag_id, tag_forum_id, posts_count = itemgetter('id', 'forum_id', 'posts_count')(tag_data)
if tag_forum_id[0] != current_forum_id:
current_forum_id = tag_forum_id[0]
if not posts_count: # Could be 0 or None
forum_tags[current_forum_id]['unused_ids'].append(tag_id)
elif len(forum_tags[current_forum_id]['most_used_ids']) < MOST_USED_TAGS_COUNT:
forum_tags[current_forum_id]['most_used_ids'].append(tag_id)
for forum in forums_with_tags:
forum.tag_most_used_ids = self.env['forum.tag'].browse(forum_tags[forum.id]['most_used_ids'])
forum.tag_unused_ids = self.env['forum.tag'].browse(forum_tags[forum.id]['unused_ids'])
@api.depends('description')
def _compute_teaser(self):
for forum in self:
@@ -276,10 +314,13 @@ class Forum(models.Model):
post_tags.insert(0, [6, 0, existing_keep])
return post_tags
def get_tags_first_char(self):
""" get set of first letter of forum tags """
tags = self.env['forum.tag'].search([('forum_id', '=', self.id), ('posts_count', '>', 0)])
return sorted(set([tag.name[0].upper() for tag in tags if len(tag.name)]))
def _get_tags_first_char(self, tags=None):
"""Get set of first letter of forum tags.
:param tags: tags recordset to further filter forum's tags that are also in these tags.
"""
tag_ids = self.tag_ids if tags is None else (self.tag_ids & tags)
return sorted({tag.name[0].upper() for tag in tag_ids if len(tag.name)})
# ----------------------------------------------------------------------
# WEBSITE
+40 -3
View File
@@ -2,21 +2,26 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.addons.http_routing.models.ir_http import slug, unslug
from odoo.exceptions import AccessError
class Tags(models.Model):
_name = "forum.tag"
_description = "Forum Tag"
_inherit = ['mail.thread', 'website.seo.metadata']
_inherit = [
'mail.thread',
'website.searchable.mixin',
'website.seo.metadata',
]
name = fields.Char('Name', required=True)
forum_id = fields.Many2one('forum.forum', string='Forum', required=True)
forum_id = fields.Many2one('forum.forum', string='Forum', required=True, index=True)
post_ids = fields.Many2many(
'forum.post', 'forum_tag_rel', 'forum_tag_id', 'forum_id',
string='Posts', domain=[('state', '=', 'active')])
posts_count = fields.Integer('Number of Posts', compute='_compute_posts_count', store=True)
website_url = fields.Char("Link to questions with the tag", compute='_compute_website_url')
_sql_constraints = [
('name_uniq', 'unique (name, forum_id)', "Tag name already exists!"),
]
@@ -26,6 +31,11 @@ class Tags(models.Model):
for tag in self:
tag.posts_count = len(tag.post_ids) # state filter is in field domain
@api.depends("forum_id", "forum_id.name", "name")
def _compute_website_url(self):
for tag in self:
tag.website_url = f'/forum/{slug(tag.forum_id)}/tag/{slug(tag)}/questions'
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
@@ -33,3 +43,30 @@ class Tags(models.Model):
if self.env.user.karma < forum.karma_tag_create and not self.env.is_admin():
raise AccessError(_('%d karma required to create a new Tag.', forum.karma_tag_create))
return super(Tags, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(vals_list)
# ----------------------------------------------------------------------
# WEBSITE
# ----------------------------------------------------------------------
@api.model
def _search_get_detail(self, website, order, options):
search_fields = ['name']
fetch_fields = ['id', 'name', 'website_url']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
}
base_domain = []
if forum := options.get("forum"):
forum_ids = (unslug(forum)[1],) if isinstance(forum, str) else forum.ids
search_domain = options.get("domain")
base_domain = [search_domain if search_domain is not None else [('forum_id', 'in', forum_ids)]]
return {
'model': 'forum.tag',
'base_domain': base_domain,
'search_fields': search_fields,
'fetch_fields': fetch_fields,
'mapping': mapping,
'icon': 'fa-tag',
'order': ','.join(filter(lambda f: 'is_published' not in f, order.split(','))),
}
+2
View File
@@ -38,6 +38,8 @@ class Website(models.Model):
result.append(self.env['forum.forum']._search_get_detail(self, order, options))
if search_type in ['forums', 'forum_posts_only', 'all']:
result.append(self.env['forum.post']._search_get_detail(self, order, options))
if search_type in ['forums', 'forum_tags_only', 'all']:
result.append(self.env['forum.tag']._search_get_detail(self, order, options))
return result
def _update_forum_count(self):
+12
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common
from odoo.tools import mute_logger
KARMA = {
'ask': 5, 'ans': 10,
@@ -104,3 +105,14 @@ class TestForumCommon(common.TransactionCase):
cls.website_2 = cls.env['website'].create({
'name': 'Second Website on same company',
})
@mute_logger("odoo.models.unlink")
def _activate_tags_for_counts(self):
self.env['forum.tag'].search([]).unlink()
self.tags = self.env['forum.tag'].create(
[
{'forum_id': forum_id.id, 'name': f'Test Tag {tag_idx}'}
for forum_id in self.forum | self.base_forum
for tag_idx in range(1, 8)
]
)
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.website_forum.models.forum_forum import MOST_USED_TAGS_COUNT
from odoo.addons.website_forum.tests.common import KARMA, TestForumCommon
from odoo.tests import tagged, users
@@ -107,3 +108,40 @@ class TestTags(TestForumCommon):
self.assertEqual(len(food_tags), 2, "One Food tag should have been created in each forum.")
self.assertIn(forum_1, food_tags.forum_id, "One Food tag should have been created for forum 1.")
self.assertIn(forum_2, food_tags.forum_id, "One Food tag should have been created for forum 2.")
def test_tags_usage_most_used(self):
self._activate_tags_for_counts()
posts_per_tag = [(tag_id, post_count) for tag_id, post_count in zip(self.base_forum.tag_ids, range(2, 9))]
vals_list = [
{'forum_id': self.base_forum.id, 'name': 'A post', 'content': 'A content', 'tag_ids': [tag_id.id]}
for tag_id, post_count in posts_per_tag
for __ in range(post_count)
]
self.env['forum.post'].create(vals_list)
self.env['forum.tag'].flush_model()
self.assertListEqual(
self.base_forum.tag_most_used_ids.ids,
[tag_id.id for tag_id, _ in reversed(posts_per_tag[-MOST_USED_TAGS_COUNT:])],
)
self.assertEqual(self.base_forum.tag_unused_ids, self.env['forum.tag'])
def test_tags_usage_unused(self):
self._activate_tags_for_counts()
used_tag = self.forum.tag_ids[5]
self.env['forum.post'].create(
[
{'forum_id': tag_id.forum_id.id, 'tag_ids': [tag_id.id], 'name': 'A post', 'content': 'A content'}
for tag_id in (used_tag | self.base_forum.tag_ids)
]
)
self.env['forum.tag'].flush_model()
# trigger batch compute
__ = (self.forum | self.base_forum).tag_most_used_ids
self.assertEqual(self.forum.tag_most_used_ids, used_tag)
self.assertEqual(self.forum.tag_unused_ids, self.forum.tag_ids - used_tag)
self.assertEqual(self.base_forum.tag_most_used_ids, self.base_forum.tag_ids[:MOST_USED_TAGS_COUNT])
self.assertEqual(self.base_forum.tag_unused_ids, self.env['forum.tag'])
+10 -18
View File
@@ -1,35 +1,27 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.addons.website_forum.tests.common import TestForumCommon
class TestForumTag(TestForumCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env['forum.tag'].search([]).unlink()
cls.tags = cls.env['forum.tag'].create(
[{'forum_id': cls.forum.id, 'name': f'Test Tag {tag_idx}'} for tag_idx in range(1, 3)]
)
cls.env['forum.post'].create(
[{'name': 'Posting about tag 1', 'forum_id': cls.forum.id, 'tag_ids': [[6, 0, [cls.tags[0].id]]]}]
)
def _check_tags_post_counts(self, tags, expected_counts):
self.assertEqual(tags.mapped('posts_count'), expected_counts)
def test_tag_posts_count(self):
self._check_tags_post_counts(self.tags, [1, 0])
self._activate_tags_for_counts()
test_tags = self.tags[:2]
self._check_tags_post_counts(test_tags, [0, 0])
post_tag_1 = self.env['forum.post'].create(
[{'name': 'Posting about tag 1 again', 'forum_id': self.forum.id, 'tag_ids': [[6, 0, [self.tags[0].id]]]}]
[{'name': 'Posting about tag 1', 'forum_id': self.forum.id, 'tag_ids': [Command.set([test_tags[0].id])]}]
)
self._check_tags_post_counts(self.tags, [2, 0])
self._check_tags_post_counts(test_tags, [1, 0])
post_tags = self.env['forum.post'].create(
[{'name': 'Posting about tag 2 now', 'forum_id': self.forum.id, 'tag_ids': [[6, 0, self.tags.ids]]}]
[{'name': 'Posting with both tags now', 'forum_id': self.forum.id, 'tag_ids': [Command.set(test_tags.ids)]}]
)
self._check_tags_post_counts(self.tags, [3, 1])
self._check_tags_post_counts(test_tags, [2, 1])
post_tag_1.active = False
self._check_tags_post_counts(self.tags, [2, 1])
self._check_tags_post_counts(test_tags, [1, 1])
post_tags.close(None)
self._check_tags_post_counts(self.tags, [1, 0])
self._check_tags_post_counts(test_tags, [0, 0])
@@ -43,7 +43,10 @@
<t t-set="show_author_avatar" t-value="true"/>
</t>
</table>
<t t-if="question_count == 0 or original_search" t-call="website_forum.no_results_message"/>
<t t-if="question_count == 0 or original_search" t-call="website_forum.no_results_message">
<t t-set="record_name_plural">posts</t>
<t t-set="go_back_url" t-valuef="/forum/#{ slug(forum) }/"/>
</t>
<t t-call="website.pager"/>
</t>
</template>
@@ -189,9 +192,12 @@
<t t-if="len(pager_tag_chars) &lt; 11">
<ul class="pagination overflow-auto mt0 mb0">
<t t-foreach="pager_tag_chars" t-as="tuple_char">
<t t-if="tuple_char_index &lt; 11">
<li t-attf-class="page-item #{active_char_tag == tuple_char[1] and 'active'}"><a t-attf-href="/forum/#{slug(forum)}/tag/#{quote_plus(tuple_char[1])}" class="page-link"><t t-esc="tuple_char[0]"/></a></li>
</t>
<li t-if="tuple_char_index &lt; 11" t-attf-class="page-item #{ active_char_tag == tuple_char[1] and 'active' }">
<a t-attf-href="/forum/#{ slug(forum) }/tag/#{ quote_plus(tuple_char[1] )}?#{ keep_query('filters', 'search') }"
class="page-link">
<t t-out="tuple_char[0]"/>
</a>
</li>
</t>
</ul>
</t>
@@ -200,7 +206,9 @@
<select name="filter" class="form-select w-auto" onchange="location = this.value;">
<t t-foreach="pager_tag_chars" t-as="tuple_char">
<option t-if="active_char_tag == tuple_char[1]" selected="selected" value="" t-out="tuple_char[0]"/>
<option t-else="" t-attf-value="/forum/#{slug(forum)}/tag/#{quote_plus(tuple_char[1])}" t-esc="tuple_char[0]"/>
<option t-else="" t-attf-value="/forum/#{slug(forum)}/tag/#{quote_plus(tuple_char[1]) + '?' + keep_query('filters')}"
t-out="tuple_char[0]"
/>
</t>
</select>
</div>
@@ -224,15 +232,17 @@
</div>
</div>
<t t-else="">
<t t-set="tag_filter" t-value="keep_query('filters') and keep_query('filters').split('=')[1] or ''"/>
<t t-set="tag_filter" t-value="keep_query('filters').split('=')[1] if keep_query('filters') else ''"/>
<t t-set="go_back_url" t-valuef="/forum/#{ slug(forum) }/tag"/>
<t t-set="record_name_plural">tags</t>
<t t-call="website_forum.no_results_message">
<t t-if="tag_filter and tag_filter != 'all'" t-set="results_msg">
There are no tags matching the selected filter.
<t t-if="tag_filter and tag_filter != 'all'" t-set="result_msg">
There are no tags matching the selected filter <t t-if="search">and search</t>.
</t>
<t t-if="search" t-set="results_msg">
<t t-elif="search" t-set="result_msg">
There are no tags matching the selected search.
</t>
<t t-else="" t-set="results_msg">
<t t-else="" t-set="result_msg">
There are no tags being used in this forum.
</t>
</t>
@@ -153,12 +153,16 @@
</t>
</div>
</div>
<!-- 'Search Box' [TODO] tags-->
<t t-if="question_count != 0" t-call="website.website_search_box_input">
<t t-if="_page_name == 'Tags'" t-set="search_type" t-valuef="forum_tags_only"/>
<t t-else="" t-set="search_type" t-valuef="forums"/>
<t t-if="_page_name == 'Tags'" t-set="action" t-value="'/forum/%s/tag' % (slug(forum))"/>
<t t-else="" t-set="action" t-value="'/forum/%s%s' % (slug(forum), tag and ('/tag/%s/questions' % slug(tag)) or '')"/>
<!-- 'Search Box' -->
<t t-if="question_count or tags" t-call="website.website_search_box_input">
<t t-if="_page_name == 'tags'">
<t t-set="search_type" t-value="'forum_tags_only'"/>
<t t-set="action" t-valuef="/forum/#{ slug(forum) }/tag"/>
</t>
<t t-else="">
<t t-set="search_type" t-value="'forums'"/>
<t t-set="action" t-value="'/forum/' + slug(forum)"/>
</t>
<t t-set="display_description" t-valuef="true"/>
<t t-set="display_detail" t-valuef="true"/>
<t t-set="_form_classes" t-valuef="flex-grow-1"/>
@@ -274,7 +278,7 @@
<template id="user_sidebar_body">
<div class="o_wforum_sidebar_section">
<t t-set="location" t-value="url_for('/forum') + '/' + slug(forum) + ( ('/tag/' + slug(tag) + '/questions?') if tag else '?' )"/>
<t t-set="location" t-valuef="#{ _forum_path }#{ ('/tag/' + slug(tag) + '/questions') if (tag and not tags) else '' }?"/>
<!-- All Posts -->
<a t-attf-class="nav-link my-1 py-1 #{ 'bg-primary-light text-primary disabled' if request.httprequest.path == _forum_path and no_filters and not any([my, queue_type, tags]) else 'text-reset'}" t-att-href="location">
<i t-attf-class="fa fa-list fa-fw #{ 'opacity-50' if request.httprequest.path != _forum_path or my or queue_type else '' }"/> Posts
@@ -315,19 +319,18 @@
<i t-attf-class="fa fa-window-close fa-fw #{queue_type != 'close' and 'opacity-50'}"/> Closed
</a>
</div>
<div t-if="forum.post_ids.tag_ids" class="o_wforum_sidebar_section pt-3">
<div t-if="forum.tag_most_used_ids" class="o_wforum_sidebar_section pt-3">
<div class="d-flex align-items-center px-3 pb-1 fw-bold">Tags
<a class="ms-auto px-0 fw-normal" t-att-href="_forum_path + '/tag'">
<small>View all</small>
</a>
</div>
<t t-foreach="forum.post_ids.tag_ids" t-as="tag">
<a t-attf-href="#{ _forum_path }/tag/#{ slug(tag) }/questions?#{ keep_query( 'search', 'sorting', 'my', filters='tag') }"
t-attf-class="nav-link my-1 py-1 text-reset">
<i class="fa fa-tag fa-fw small opacity-50"/>
<t t-out="tag.name"/>
</a>
</t>
<a t-foreach="forum.tag_most_used_ids" t-as="tag"
t-attf-href="#{ _forum_path }/tag/#{ slug(tag) }/questions?#{ keep_query( 'search', 'sorting', 'my', filters='tag') }"
t-attf-class="nav-link my-1 py-1 text-reset">
<i class="fa fa-tag fa-fw small opacity-50"/>
<t t-out="tag.name"/>
</a>
</div>
</template>
@@ -350,33 +353,28 @@
</template>
<template id="no_results_message">
<t t-set="_filters_str">
<t t-if="filters == 'unanswered'">&amp;nbsp;unanswered</t>
<t t-elif="filters == 'solved'">&amp;nbsp;solved</t>
<t t-elif="filters == 'unsolved'">&amp;nbsp;unsolved</t>
</t>
<t t-set="_my_str">
<t t-if="my == 'favourites'">&amp;nbsp;in your favourites</t>
<t t-elif="my == 'mine'">&amp;nbsp;in your posts</t>
</t>
<t t-set="_search_str">&amp;nbsp;<t t-if="search">matching "<em class="fw-bold text-break" t-out="original_search or search"/>"</t></t>
<t t-set="_search_and_tag_str" t-value="'&amp;nbsp;and&amp;nbsp;' if search and tag else ''"/>
<t t-set="_tag_str"><t t-if="tag">using the <span class="px-2 py-1 rounded bg-300" t-out="tag.name"/> tag</t></t>
<t t-if="queue_type">
<t t-if="queue_type">
<t t-set="_no_results_title">You've Completely Caught&amp;nbsp;Up!</t>
<t t-set="result_msg">Go enjoy a cup of coffee.</t>
</t>
<t t-else="">
<t t-set="_no_results_title">Oops!</t>
<t t-set="result_msg">Sorry, we could not find any<b>%s</b> results<b>%s</b>%s%s%s.</t>
<t t-set="result_msg" t-value="result_msg % (_filters_str.strip(), _my_str.strip(), _search_str.strip(), _search_and_tag_str.strip(), _tag_str.strip())"></t>
</t>
<div t-attf-class="#{queue_type and 'o_caught_up_alert'} row g-0 justify-content-center #{'d-none' if start_hidden else ''}">
<t t-set="record_name_plural">posts</t>
<t t-set="go_back_url" t-valuef="/forum/#{ slug(forum) }/"/>
</t>
<t t-else="" t-set="_no_results_title">Oops!</t>
<div t-attf-class="#{ queue_type and 'o_caught_up_alert' } row g-0 justify-content-center #{ 'd-none' if hide_alert else '' }">
<div class="p-3 d-flex flex-column align-items-center">
<img t-attf-src="/website_forum/static/src/img/#{'tasks' if queue_type else 'empty'}.svg" class="img img-fluid mb-3" width="150" alt="Checked boxes"/>
<h5 t-out="_no_results_title"/>
<span t-out="result_msg"/>
<span t-if="result_msg" t-out="result_msg"/>
<span t-else="">
Sorry, we could not find any <b t-if="filters in ['unanswered', 'solved', 'unsolved']" t-out="filters"/> results
<b t-if="my == 'favourites'"> in your favourites</b>
<b t-elif="my == 'mine'"> in your posts</b>
<span t-if="search">matching "<em class="fw-bold text-break" t-out="original_search or search"/>"</span>
<span t-out="'and' if search and tag else ''"/>
<t t-if="tag">
using the <span class="px-2 py-1 rounded bg-300" t-out="tag.name"/> tag
</t>.
</span>
<span t-if="original_search">Showing results for <em class="fw-bold" t-out="search"/> instead.</span>
<div t-if="question_count == 0 and forum.total_posts != 0" class="mt-3 text-start">
<p><i class="fa fa-check fa-fw me-1"/>Check your spelling and try again.</p>
@@ -386,7 +384,7 @@
<span t-elif="forum.total_posts == 0">Actually, there are no posts in this forum yet.</span>
<div class="mt-3">
<a t-if="uid and forum.total_posts == 0" role="button" type="button" class="o_forum_ask_btn btn btn-primary ms-2" t-attf-href="/forum/#{slug(forum)}/ask">Start by creating a post</a>
<a t-else="" role="button" type="button" class="btn btn-primary mt-2" t-attf-href="/forum/#{slug(forum)}/">Go back to questions list</a>
<a t-else="" role="button" type="button" class="btn btn-primary mt-2" t-att-href="go_back_url">Go back to the list of <t t-out="record_name_plural"/></a>
</div>
</div>
</div>