Files
odoo_source/addons/knowledge/controllers/main.py
T
Fabio Barbero 5d71064d03 [FIX] knowledge: allow portal users to click on articles
Purpose
=======
Properly compute portal_readonly_mode to also take into account portal
users, so the template is properly rendered for them.

closes odoo/odoo#92850

Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com>
2022-06-03 12:02:39 +02:00

351 lines
16 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug
from odoo import http, tools, _
from odoo.exceptions import AccessError, ValidationError
from odoo.http import request
class KnowledgeController(http.Controller):
# ------------------------
# Article Access Routes
# ------------------------
@http.route('/knowledge/home', type='http', auth='user')
def access_knowledge_home(self):
""" This route will redirect internal users to the backend view of the
article and the share users to the frontend view instead. """
article = request.env["knowledge.article"]._get_first_accessible_article()
if request.env.user.has_group('base.group_user') and not article:
return self._redirect_to_backend_view(article)
if not article:
return self._redirect_to_portal_view(False, hide_side_bar=True)
return request.redirect("/knowledge/article/%s" % article.id)
@http.route('/knowledge/article/<int:article_id>', type='http', auth='user')
def redirect_to_article(self, article_id):
""" This route will redirect internal users to the backend view of the
article and the share users to the frontend view instead."""
article = self._fetch_article(article_id)
if request.env.user.has_group('base.group_user'):
if not article:
return werkzeug.exceptions.Forbidden()
return self._redirect_to_backend_view(article)
return self._redirect_to_portal_view(article)
@http.route('/knowledge/article/invite/<int:member_id>/<string:invitation_hash>', type='http', auth='public')
def article_invite(self, member_id, invitation_hash):
""" This route will check if the given parameter allows the client to access the article via the invite token.
Then, if the partner has not registered yet, we will redirect the client to the signup page to finally redirect
them to the article.
If the partner already has registrered, we redirect them directly to the article.
"""
member = request.env['knowledge.article.member'].sudo().browse(member_id).exists()
correct_token = member._get_invitation_hash() if member else False
if not correct_token or not tools.consteq(correct_token, invitation_hash):
raise werkzeug.exceptions.NotFound()
partner = member.partner_id
article = member.article_id
if not partner.user_ids:
# Force the signup even if not enabled (as we explicitly invited the member).
# They should still be able to create a user.
signup_allowed = request.env['res.users']._get_signup_invitation_scope() == 'b2c'
if not signup_allowed:
partner.signup_prepare()
partner.signup_get_auth_param()
signup_url = partner._get_signup_url_for_action(url='/knowledge/article/%s' % article.id)[partner.id]
return request.redirect(signup_url)
return request.redirect('/web/login?redirect=/knowledge/article/%s' % article.id)
def _fetch_article(self, article_id):
""" Check the access of the given article for the current user. """
article = request.env['knowledge.article'].browse(int(article_id)).exists()
if not article:
return request.env['knowledge.article']
try:
article.check_access_rights('read')
article.check_access_rule('read')
except AccessError:
return request.env['knowledge.article']
return article
def _redirect_to_backend_view(self, article):
return request.redirect("/web#id=%s&model=knowledge.article&action=%s&menu_id=%s" % (
article.id if article else '',
request.env.ref("knowledge.knowledge_article_action_form").id,
request.env.ref('knowledge.knowledge_menu_root').id
))
def _redirect_to_portal_view(self, article, hide_side_bar=False):
return request.render('knowledge.knowledge_article_view_frontend', {
'article': article,
'portal_readonly_mode': True, # used to bypass access check (to speed up loading)
'show_sidebar': not hide_side_bar and bool(self._get_root_articles(limit=1))
})
# ------------------------
# Articles tree generation
# ------------------------
def _get_root_articles(self, limit=None):
""" Meant to be overriden by website_knowledge to search in sudo with adapted domain."""
_order = 'sequence' if limit is not None else None
return request.env["knowledge.article"].search([("parent_id", "=", False)], limit=limit, order=_order)
def _prepare_articles_tree_html(self, template, active_article, unfolded_articles=False):
""" Prepares all the info needed to render the article tree view side panel
and returns the rendered given template with those values.
:param <knowledge.article> active_article: used to highlight the given
article_id in the template;
:param list unfolded_articles: 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;
"""
unfolded_articles = set() if not unfolded_articles else set(unfolded_articles)
if active_article:
# root articles = starting point of the tree view : unfold only if root_article in (accessible) parents
ancestors = active_article._get_readable_ancetors()
if active_article.root_article_id in ancestors:
unfolded_articles |= set(ancestors.ids)
root_articles = self._get_root_articles()
values = {
"active_article": active_article,
"workspace_articles": root_articles.filtered(lambda article: article.category == 'workspace'),
"shared_articles": root_articles.filtered(lambda article: article.category == 'shared'),
"private_articles": root_articles.filtered(
lambda article: article.category == "private" and article.user_has_write_access),
"unfolded_articles": unfolded_articles,
'portal_readonly_mode': not request.env.user.has_group('base.group_user'),
}
favorites = request.env['knowledge.article.favorite']
if not request.env.user._is_public():
favorites = request.env['knowledge.article.favorite'].search([
("user_id", "=", request.env.user.id), ('article_id.active', '=', True)
])
values["favorites"] = favorites
return request.env['ir.qweb']._render(template, values)
@http.route('/knowledge/tree_panel', type='json', auth='user')
def get_tree_panel_all(self, active_article_id=False, unfolded_articles=False):
return self._prepare_articles_tree_html(
'knowledge.knowledge_article_tree',
self._fetch_article(active_article_id),
unfolded_articles=unfolded_articles
)
@http.route('/knowledge/tree_panel/portal', type='json', auth='public')
def get_tree_panel_portal(self, active_article_id=False, unfolded_articles=False):
""" Frontend access for left panel. """
return self._prepare_articles_tree_html(
'knowledge.knowledge_article_tree_frontend',
self._fetch_article(active_article_id),
unfolded_articles=unfolded_articles
)
@http.route('/knowledge/tree_panel/children', type='json', auth='user')
def get_tree_panel_children(self, parent_id):
parent = self._fetch_article(parent_id)
if not parent:
return werkzeug.exceptions.NotFound()
return request.env['ir.qweb']._render('knowledge.articles_template', {
'articles': parent.child_ids,
'portal_readonly_mode': not request.env.user.has_group('base.group_user'), # used to bypass access check (to speed up loading)
})
@http.route('/knowledge/tree_panel/favorites', type='json', auth='user')
def get_tree_panel_favorites(self, active_article_id=False):
favorite_articles = request.env['knowledge.article.favorite'].search([
("user_id", "=", request.env.user.id), ('article_id.active', '=', True)
])
return request.env['ir.qweb']._render('knowledge.knowledge_article_tree_favorites', {
'favorites': favorite_articles,
"active_article": request.env['knowledge.article'].browse(active_article_id),
})
# ------------------------
# Article permission panel
# ------------------------
@http.route('/knowledge/get_article_permission_panel_data', type='json', auth='user')
def get_article_permission_panel_data(self, article_id):
"""
Returns a dictionnary containing all values required to render the permission panel.
:param article_id: (int) article id
"""
article = self._fetch_article(article_id)
if not article:
return werkzeug.exceptions.Forbidden()
is_sync = not article.is_desynchronized
# Get member permission info
members_values = []
members_permission = article._get_article_member_permissions(additional_fields={
'res.partner': [
('name', 'partner_name'),
('email', 'partner_email'),
('partner_share', 'partner_share'),
],
'knowledge.article': [
('icon', 'based_on_icon'),
('name', 'based_on_name'),
],
})[article.id]
for partner_id, member in members_permission.items():
# empty member added by '_get_article_member_permissions', don't show it in the panel
if not member['member_id']:
continue
# if share partner and permission = none, don't show it in the permission panel.
if member['permission'] == 'none' and member['partner_share']:
continue
# if article is desyncronized, don't show members based on parent articles.
if not is_sync and member['based_on']:
continue
member_values = {
'id': member['member_id'],
'partner_id': partner_id,
'partner_name': member['partner_name'],
'partner_email': member['partner_email'],
'permission': member['permission'],
'based_on': f'{member["based_on_icon"] or "📄"} {member["based_on_name"]}' if member['based_on_name'] else False,
'based_on_id': member['based_on'],
'partner_share': member['partner_share'],
'is_unique_writer': member['permission'] == "write" and article.inherited_permission != "write" and not any(
other_member['permission'] == 'write'
for partner_id, other_member in members_permission.items()
if other_member['member_id'] != member['member_id']
),
}
members_values.append(member_values)
internal_permission_field = request.env['knowledge.article']._fields['internal_permission']
permission_field = request.env['knowledge.article.member']._fields['permission']
user_is_admin = request.env.user._is_admin()
return {
'internal_permission_options': internal_permission_field.get_description(request.env).get('selection', []),
'internal_permission': article.inherited_permission,
'parent_permission': article.parent_id.inherited_permission,
'based_on': article.inherited_permission_parent_id.display_name,
'based_on_id': article.inherited_permission_parent_id.id,
'members_options': permission_field.get_description(request.env).get('selection', []),
'members': members_values,
'is_sync': is_sync,
'parent_id': article.parent_id.id,
'parent_name': article.parent_id.display_name,
'user_is_admin': user_is_admin,
'show_admin_tip': user_is_admin and article.user_permission != 'write',
}
@http.route('/knowledge/article/set_member_permission', type='json', auth='user')
def article_set_member_permission(self, article_id, permission, member_id=False, inherited_member_id=False):
""" Sets the permission of the given member for the given article.
The returned result can also include a `reload_tree` entry that tells the
caller that the aside block listing all articles should be reloaded. This
happens when the article moves from one section to another.
**Note**: The user needs "write" permission to change the permission of a user.
:param int article_id: target article id;
:param string permission: permission to set on member, one of 'none',
'read' or 'write';
:param int member_id: id of a member of the given article;
:param int inherited_member_id: id of a member from one of the article's
parent (indicates rights are inherited from parents);
"""
article = self._fetch_article(article_id)
if not article:
return werkzeug.exceptions.Forbidden()
member = request.env['knowledge.article.member'].browse(member_id or inherited_member_id).exists()
if not member:
return {'error': _("The selected member does not exists or has been already deleted.")}
previous_category = article.category
try:
article._set_member_permission(member, permission, bool(inherited_member_id))
except (AccessError, ValidationError):
return {'error': _("You cannot change the permission if this member.")}
if article.category != previous_category:
return {'reload_tree': True}
return {}
@http.route('/knowledge/article/remove_member', type='json', auth='user')
def article_remove_member(self, article_id, member_id=False, inherited_member_id=False):
""" Removes the given member from the given article.
The returned result can also include a `reload_tree` entry that tells the
caller that the aside block listing all articles should be reloaded. This
happens when the article moves from one section to another.
**Note**: The user needs "write" permission to remove another member from
the list. The user can always remove themselves from the list.
:param int article_id: target article id;
:param int member_id: id of a member of the given article;
:param int inherited_member_id: id of a member from one of the article's
parent (indicates rights are inherited from parents);
"""
article = self._fetch_article(article_id)
if not article:
return werkzeug.exceptions.Forbidden()
member = request.env['knowledge.article.member'].browse(member_id or inherited_member_id).exists()
if not member:
return {'error': _("The selected member does not exists or has been already deleted.")}
previous_category = article.category
try:
article._remove_member(member)
except (AccessError, ValidationError) as e:
return {'error': e}
if article.category != previous_category:
return {'reload_tree': True}
return {}
@http.route('/knowledge/article/set_internal_permission', type='json', auth='user')
def article_set_internal_permission(self, article_id, permission):
""" Sets the internal permission of the given article.
The returned result can also include a `reload_tree` entry that tells the
caller that the aside block listing all articles should be reloaded. This
happens when the article moves from one section to another.
**Note**: The user needs "write" permission to update the internal permission
of the article.
:param int article_id: target article id;
:param string permission: permission to set on member, one of 'none',
'read' or 'write';
"""
article = self._fetch_article(article_id)
if not article:
return werkzeug.exceptions.Forbidden()
previous_category = article.category
try:
article._set_internal_permission(permission)
except (AccessError, ValidationError):
return {'error': _("You cannot change the internal permission of this article.")}
if article.category != previous_category:
return {'reload_tree': True}
return {}