[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:
@@ -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
|
||||
# --------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(','))),
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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) < 11">
|
||||
<ul class="pagination overflow-auto mt0 mb0">
|
||||
<t t-foreach="pager_tag_chars" t-as="tuple_char">
|
||||
<t t-if="tuple_char_index < 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 < 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'">&nbsp;unanswered</t>
|
||||
<t t-elif="filters == 'solved'">&nbsp;solved</t>
|
||||
<t t-elif="filters == 'unsolved'">&nbsp;unsolved</t>
|
||||
</t>
|
||||
<t t-set="_my_str">
|
||||
<t t-if="my == 'favourites'">&nbsp;in your favourites</t>
|
||||
<t t-elif="my == 'mine'">&nbsp;in your posts</t>
|
||||
</t>
|
||||
<t t-set="_search_str">&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="'&nbsp;and&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&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>
|
||||
|
||||
Reference in New Issue
Block a user