Odoo18-Base/extra-addons/website_knowledge/controllers/main.py

260 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug
from odoo import http, _
from odoo.http import request
from odoo.addons.knowledge.controllers.main import KnowledgeController
from odoo.exceptions import AccessError
from odoo.osv import expression
class KnowledgeWebsiteController(KnowledgeController):
_KNOWLEDGE_TREE_ARTICLES_LIMIT = 50
# ------------------------
# Article Access Routes
# ------------------------
@http.route('/knowledge/home', type='http', auth='public', website=True, sitemap=False)
def access_knowledge_home(self):
if request.env.user._is_public():
article = request.env["knowledge.article"]._get_first_accessible_article()
if not article:
raise werkzeug.exceptions.NotFound()
return request.redirect("/knowledge/article/%s" % article.id)
return super().access_knowledge_home()
# Override routes to display articles to public users
@http.route('/knowledge/article/<int:article_id>', type='http', auth='public', website=True, sitemap=False)
def redirect_to_article(self, **kwargs):
if request.env.user._is_public():
article = request.env['knowledge.article'].sudo().browse(kwargs['article_id'])
if not article.exists():
raise werkzeug.exceptions.NotFound()
if article.website_published:
return self._redirect_to_public_view(article, kwargs.get('no_sidebar', False))
# public users can't access articles that are not published, let them login first
return request.redirect('/web/login?redirect=/knowledge/article/%s' % kwargs['article_id'])
return super().redirect_to_article(**kwargs)
def _redirect_to_public_view(self, article, no_sidebar=False):
# The sidebar is hidden if no_sidebar is True or if there is no article
# to show in the sidebar (i.e. only one article in the tree is published).
return request.render('website_knowledge.article_view_public', {
'article_data': self._prepare_public_article(article),
'main_object': article,
'res_id': article.id,
'show_sidebar': bool(
not no_sidebar
and (
article.parent_id.website_published
or article.child_ids.filtered('website_published')
)
),
})
# ------------------------
# Articles tree generation
# ------------------------
@http.route('/knowledge/public_sidebar', type='json', auth='public')
def get_public_sidebar(self, active_article_id=False, unfolded_articles_ids=False, search_term=False):
""" Public access for the sidebar.
If a search_term is given, show the articles matching this search_term in the sidebar.
In that case, unfolded_articles_ids is ignored (the children of the matching articles
are not shown).
"""
if search_term:
public_sidebar_values = self._prepare_public_sidebar_search_values(search_term, active_article_id)
else:
public_sidebar_values = self._prepare_public_sidebar_values(active_article_id, unfolded_articles_ids)
return request.env['ir.qweb']._render('website_knowledge.public_sidebar', public_sidebar_values)
@http.route('/knowledge/public_sidebar/load_more', type='json', auth='public')
def public_sidebar_load_more(self, limit, offset, active_article_id=False, parent_id=False):
"""" Route called when loading more articles in a particular sub-tree.
Fetching is done based either on a parent, either on root articles when no parent is
given.
"limit" and "offset" allow controlling the returned result size.
In addition, if we receive an 'active_article_id', it is forcefully displayed even if not
in the first 50 articles of its own subtree.
(Subsequently, all his parents are also forcefully displayed).
That allows the end-user to always see where he is situated within the articles hierarchy.
See 'articles_template' template docstring for details. """
if parent_id:
parent_id = int(parent_id)
articles_domain = [('parent_id', '=', parent_id), ('website_published', '=', True)]
else:
# root articles
articles_domain = self._prepare_public_root_articles_domain()
offset = int(offset)
limit = int(limit)
articles = request.env['knowledge.article'].search(
articles_domain,
limit=limit + 1,
offset=offset,
order='sequence, id',
)
if len(articles) < limit:
articles_left_count = len(articles)
else:
articles_left_count = request.env['knowledge.article'].search_count(articles_domain) - offset
active_article_ancestor_ids = []
unfolded_articles_ids = []
force_show_active_article = False
if articles and active_article_id and active_article_id not in articles.ids:
active_article_with_ancestors = request.env['knowledge.article'].search(
[('id', 'parent_of', active_article_id)]
)
active_article = active_article_with_ancestors.filtered(
lambda article: article.id == active_article_id)
active_article_ancestors = active_article_with_ancestors - active_article
unfolded_articles_ids = active_article_ancestors.ids
# we only care about articles our current hierarchy (base domain)
# and that are "next" (based on sequence of last article retrieved)
force_show_domain = expression.AND([
articles_domain,
[('sequence', '>', articles[-1].sequence)]
])
force_show_active_article = active_article.filtered_domain(force_show_domain)
active_article_ancestors = active_article_ancestors.filtered_domain(force_show_domain)
active_article_ancestor_ids = active_article_ancestors.ids
if active_article_ancestors and not any(
ancestor_id in articles.ids for ancestor_id in active_article_ancestors.ids):
articles |= active_article_ancestors
return request.env['ir.qweb']._render('website_knowledge.articles_template', {
"active_article_id": active_article_id,
"active_article_ancestor_ids": active_article_ancestor_ids,
"articles": articles,
"articles_count": articles_left_count,
"articles_displayed_limit": self._KNOWLEDGE_TREE_ARTICLES_LIMIT,
"articles_displayed_offset": offset,
"has_parent": bool(parent_id),
"force_show_active_article": force_show_active_article,
"unfolded_articles_ids": unfolded_articles_ids,
})
@http.route('/knowledge/public_sidebar/children', type='json', auth='public')
def get_public_sidebar_children(self, parent_id):
parent = request.env['knowledge.article'].search([('id', '=', parent_id)])
if not parent:
raise AccessError(_("This Article cannot be unfolded. Either you lost access to it or it has been deleted."))
articles = parent.child_ids.filtered(
lambda a: not a.is_article_item
).sorted("sequence") if parent.has_article_children else request.env['knowledge.article']
return request.env['ir.qweb']._render('website_knowledge.articles_template', {
'articles': articles,
"articles_displayed_limit": self._KNOWLEDGE_TREE_ARTICLES_LIMIT,
"articles_displayed_offset": 0,
"has_parent": True,
})
# --------------------
# Articles tree utils
# --------------------
def _prepare_public_root_articles_domain(self, active_article=False):
""" Public root articles are root articles that are published and in the workspace.
These root articles are now possibly different from the regular roots as we are creating sub-sites
with limited hierarchy.
These subsites are composed of all the accessible articles that are descendants of the closet accessible
root from the article.
An accessible root article is the highest ancestor of the current article that is accessible, meaning
that their parent either is false or isn't accessible.
"""
if not active_article:
return [("parent_id", "=", False), ("category", "=", "workspace"), ("website_published", "=", True)]
return [("id", "=", active_article._get_accessible_root_ancestors()[-1].id)]
def _prepare_public_sidebar_values(self, active_article_id, unfolded_articles_ids):
""" Prepares all the info needed to render the sidebar in public
:param int active_article_id: used to highlight the given article_id in the template;
:param unfolded_articles_ids: List of IDs used to display the children
of the given article ids. Unfolded articles are saved into local storage.
When reloading/opening the article page, previously unfolded articles
nodes must be opened;
"""
active_article_ancestor_ids = []
unfolded_ids = unfolded_articles_ids or []
# Add active article and its parents in list of unfolded articles
active_article = request.env['knowledge.article'].browse(active_article_id)
# Sudo to speed up the search, as permissions will be computed anyways
# when getting the visible articles
root_articles = request.env['knowledge.article'].sudo().search(
self._prepare_public_root_articles_domain(active_article), order="sequence, id"
)
if active_article and active_article.parent_id:
active_article_ancestor_ids = active_article._get_ancestor_ids()
unfolded_ids += active_article_ancestor_ids
all_visible_articles = request.env['knowledge.article'].get_visible_articles(root_articles.ids, unfolded_ids)
return {
"active_article_id": active_article_id,
"active_article_ancestor_ids": active_article_ancestor_ids,
"articles_displayed_limit": self._KNOWLEDGE_TREE_ARTICLES_LIMIT,
"articles_displayed_offset": 0,
"all_visible_articles": all_visible_articles,
"root_articles": all_visible_articles.filtered(lambda article: article in root_articles),
"unfolded_articles_ids": unfolded_ids,
}
def _prepare_public_sidebar_search_values(self, search_term, active_article_id=False):
""" Prepares all the info needed to render the sidebar given the search_term in public.
The tree is completely flattened (no sections nor child articles) to avoid noise
(unnecessary parents display when children are matching) and redondancy (duplicated articles
because of the favorite tree).
:param int active_article_id: used to highlight the given article_id in the template;
:param string search_term: user search term to filter the articles on;
"""
# Get all the visible articles based on the search term
all_visible_articles = request.env['knowledge.article'].search(
expression.AND([
[('is_article_item', '=', False)],
[('name', 'ilike', search_term)],
expression.OR([
[('id', 'child_of', active_article_id)],
[('id', 'parent_of', active_article_id)],
])
]),
order='name',
limit=self._KNOWLEDGE_TREE_ARTICLES_LIMIT,
)
return {
"search_tree": True, # Display the flatenned tree instead of the basic tree with sections
"active_article_id": active_article_id,
"articles_displayed_limit": self._KNOWLEDGE_TREE_ARTICLES_LIMIT,
'articles': all_visible_articles,
}
def _prepare_public_article(self, article):
public_fields = {'body', 'cover_image_position', 'cover_image_url', 'display_name',
'full_width', 'icon', 'name'}
public_article = {field: article[field] for field in public_fields}
public_article['cover_image_id'] = article.cover_image_id.id
return public_article