260 lines
12 KiB
Python
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
|