[IMP] website_slides : improve get slide access computation

As slides are returned in sudo to the template, the user rights were
checked via a can_access variable set in the template directly.
In order to disable the vote for user that cannot get access to the course
all the checks are now done in backend (also to avoid to get access to slides
in sudo mode with rpc calls). We add now, for each slide on the course view,
the access and vote rights to the template : can_access and can_vote.
Those two are computed in backend, using the user's access right instead of admin's.
But, as if the user is not member of the channel, he cannot get access to the
course slides (expect for Free Peview ones), so the check on can_vote
will crash. This is why we check the access rights before checking can_vote,
for each slides (as free preview slides are still accessible,
even if the user is not in channel members).

The vote widget is disabled if user's can_access or can_vote = False, enabled otherwise.
If user have access but have not enough karma, the controller will handle this
case and return an error to the user telling him he has not enough karma to vote.
If user can vote but cannot access, the controller will handle this
case and return an error to the user telling him he has no access to the lesson.

Task ID: 1943788
PR #31321
This commit is contained in:
David Beguin
2019-03-15 15:05:54 +00:00
parent d4db21a9cc
commit 5798ca28bf
7 changed files with 59 additions and 25 deletions
+11 -7
View File
@@ -45,10 +45,7 @@ class WebsiteSlides(WebsiteProfile):
slide = request.env['slide.slide'].browse(int(slide_id)).exists()
if not slide:
return {'error': 'slide_wrong'}
try:
slide.check_access_rights('read')
slide.check_access_rule('read')
except:
if not slide._check_read_access():
return {'error': 'slide_access'}
return {'slide': slide}
@@ -370,7 +367,6 @@ class WebsiteSlides(WebsiteProfile):
'user': request.env.user,
'pager': pager,
'is_public_user': request.website.is_public_user(),
'is_slides_publisher': request.env.user.has_group('website.group_website_publisher'),
}
if not request.env.user._is_public():
last_message_values = request.env['mail.message'].search([
@@ -402,10 +398,11 @@ class WebsiteSlides(WebsiteProfile):
force_void=True,
limit=self._slides_per_category if channel.channel_type == 'documentation' else False,
offset=pager['offset'])
values['slide_promoted_access'] = values['slide_promoted'].sudo(request.env.user)._get_slide_action_access()[values['slide_promoted'].id]
values['channel_progress'] = self._get_channel_progress(channel, include_quiz=True)
values = self._prepare_additional_channel_values(values, **kw)
return request.render('website_slides.course_main', values)
# SLIDE.CHANNEL UTILS
@@ -494,6 +491,8 @@ class WebsiteSlides(WebsiteProfile):
if 'fullscreen' in kwargs:
return request.render("website_slides.slide_fullscreen", values)
else:
values['slide_access'] = slide.sudo(request.env.user)._get_slide_action_access()[slide.id]
return request.render("website_slides.slide_main", values)
@http.route('''/slides/slide/<model("slide.slide"):slide>/pdf_content''',
@@ -576,7 +575,12 @@ class WebsiteSlides(WebsiteProfile):
])
if (upvote and slide_partners.vote == 1) or (not upvote and slide_partners.vote == -1):
return {'error': 'vote_done'}
slide = request.env['slide.slide'].browse(int(slide_id))
# check slide access
fetch_res = self._fetch_slide(slide_id)
if fetch_res.get('error'):
return fetch_res
# check slide operation
slide = fetch_res['slide']
if not slide.channel_id.allow_comment:
return {'error': 'comment_disabled'}
if not slide.can_vote:
@@ -453,22 +453,24 @@ class Channel(models.Model):
if uncategorized_slides or force_void:
category_data.append({
'category': False, 'id': False,
'name': _('Uncategorized'), 'slug_name': _('Uncategorized'),
'name': _('Uncategorized'), 'slug_name': _('Uncategorized'),
'total_slides': len(uncategorized_slides),
'slides': uncategorized_slides[(offset or 0):(limit or len(uncategorized_slides))],
'slides_access': uncategorized_slides.sudo(self.env.user)._get_slide_action_access()
})
# Then all categories by natural order
for category in all_categories:
category_slides = all_slides.filtered(lambda slide: slide.category_id == category)
if not category_slides and not force_void:
continue
slides = category_slides[(offset or 0):(limit or len(category_slides))]
category_data.append({
'category': category, 'id': category.id,
'name': category.name, 'slug_name': slug(category),
'total_slides': len(category_slides),
'slides': category_slides[(offset or 0):(limit or len(category_slides))],
'slides': slides,
'slides_access': slides.sudo(self.env.user)._get_slide_action_access()
})
return category_data
+24 -1
View File
@@ -14,7 +14,7 @@ from werkzeug import urls
from odoo import api, fields, models, _
from odoo.addons.http_routing.models.ir_http import slug
from odoo.addons.gamification.models.gamification_karma_rank import KarmaError
from odoo.exceptions import Warning, UserError
from odoo.exceptions import Warning, UserError, AccessError
from odoo.http import request
from odoo.addons.http_routing.models.ir_http import url_for
@@ -273,6 +273,7 @@ class Slide(models.Model):
@api.multi
def _compute_karma_rights(self):
""" This method supposed current user have access to the slide. If not, I will crash, meaning he can not execute the action """
for slide in self:
slide.can_comment = self.env.user.karma >= slide.channel_id.karma_slide_comment
slide.can_vote = self.env.user.karma >= slide.channel_id.karma_slide_vote
@@ -356,6 +357,28 @@ class Slide(models.Model):
return groups
# ---------------------------------------------------------
# Access Rights Methods
# ---------------------------------------------------------
def _check_read_access(self):
try:
self.check_access_rights('read')
self.check_access_rule('read')
except AccessError:
return False
return True
def _get_slide_action_access(self):
result = dict((slide_id, dict(can_access=False, can_vote=False, can_comment=False)) for slide_id in self.ids)
for slide in self:
can_access = slide._check_read_access()
if can_access and (slide.channel_id.is_member or slide.is_preview or slide.can_publish):
result[slide.id]['can_access'] = True
result[slide.id]['can_vote'] = slide.can_vote
result[slide.id]['can_comment'] = slide.can_comment
return result
# ---------------------------------------------------------
# Business Methods
# ---------------------------------------------------------
@@ -56,9 +56,11 @@ var SlideLikeWidget = Widget.extend({
self.$el.find('span.o_wslides_js_slide_like_down span').text(data.dislikes);
} else {
if (data.error === 'public_user') {
self._popoverAlert(self.$el, _.str.sprintf(_t('Please <a href="/web/login?redirect=%s">login</a> to vote this slide'), (document.URL)));
self._popoverAlert(self.$el, _.str.sprintf(_t('Please <a href="/web/login?redirect=%s">login</a> to vote this lesson'), (document.URL)));
} else if (data.error === 'vote_done') {
self._popoverAlert(self.$el, _t('You have already voted for this slide'));
self._popoverAlert(self.$el, _t('You have already voted for this lesson'));
} else if (data.error === 'slide_access') {
self._popoverAlert(self.$el, _t('You don\'t have access to this lesson'));
} else if (data.error === 'comment_disabled') {
self._popoverAlert(self.$el, _t('Votes and comments are disabled for this channel'));
} else if (data.error === 'missing_karma') {
@@ -32,8 +32,10 @@ $o-enterprise-radient-color: #62495B !default;
.o_wslides_js_slide_like_up,
.o_wslides_js_slide_like_down {
cursor: pointer;
color: $link-color;
&:not(.disabled) {
cursor: pointer;
color: $link-color;
}
}
.o_wslides_js_lesson_quiz_question {
@@ -387,7 +387,7 @@
</div>
<div class="col-8">
<h3 class="w-100" t-att-title="slide_promoted.name">
<t t-if="slide_promoted.is_preview or channel.is_member or is_slides_publisher">
<t t-if="slide_promoted_access['can_access']">
<a t-attf-href="/slides/slide/#{slug(slide_promoted)}" class="font-weight-bold text-muted" t-field="slide_promoted.name"/>
</t>
<t t-else="">
@@ -420,7 +420,9 @@
</div>
<div class="row mx-n2 mt8">
<t t-foreach="category['slides']" t-as="slide">
<div class="col-md-3 col-sm-6 px-2 col-xs-12 d-flex flex-grow-1" t-call="website_slides.lesson_card"/>
<div class="col-md-3 col-sm-6 px-2 col-xs-12 d-flex flex-grow-1" t-call="website_slides.lesson_card">
<t t-set="slide_access" t-value="category['slides_access'][slide.id]"/>
</div>
</t>
</div>
</div>
@@ -438,15 +440,14 @@
<template id='lesson_card' name="Lesson Card">
<div class="card w-100 o_wslides_lesson_card mb-4">
<t t-set="lesson_image" t-value="'/slides/slide/%s/get_image?field=image_large' % slide.id"/>
<t t-set="can_access" t-value="slide.is_preview or channel.is_member or is_slides_publisher"/>
<a t-if="can_access" t-attf-href="/slides/slide/#{slug(slide)}" t-title="slide.name">
<a t-if="slide_access['can_access']" t-attf-href="/slides/slide/#{slug(slide)}" t-title="slide.name">
<div class="card-img-top" t-attf-style="padding-top: 50%; background-image: url(#{lesson_image}); background-size: cover; background-position:center"/>
</a>
<t t-else="">
<div class="card-img-top" t-attf-style="padding-top: 50%; background-image: url(#{lesson_image}); background-size: cover; background-position:center"/>
</t>
<div class="card-body">
<a t-if="can_access" class="card-title h5" t-attf-href="/slides/slide/#{slug(slide)}" t-esc="slide.name"/>
<a t-if="slide_access['can_access']" class="card-title h5" t-attf-href="/slides/slide/#{slug(slide)}" t-esc="slide.name"/>
<span t-else="" class="card-title h5 text-muted" t-esc="slide.name"/>
<div class="card-subtitle mb-2 text-muted">
<t t-if="slide.is_preview">
@@ -469,11 +470,11 @@
<div class="d-flex align-items-center small">
<span class="font-weight-bold mr-auto" t-field="slide.completion_time" t-options='{"widget": "float_time"}'/>
<div class="o_wslides_js_slide_like mr-2">
<span class="o_wslides_js_slide_like_up" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
<span t-att-class="('o_wslides_js_slide_like_up %s') % ('disabled' if not (slide_access['can_access'] and slide_access['can_vote']) else '')" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
<i class="fa fa-thumbs-up fa-1x" role="img" aria-label="Likes" title="Likes"></i>
<span t-esc="slide.likes"/>
</span>
<span class="o_wslides_js_slide_like_down" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
<span t-att-class="('o_wslides_js_slide_like_down %s') % ('disabled' if not (slide_access['can_access'] and slide_access['can_vote']) else '')" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
<i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"></i>
<span t-esc="slide.dislikes"/>
</span>
@@ -241,11 +241,11 @@
<span class="text-muted font-weight-bold mr-3">Rating</span>
<div class="text-muted border-left pl-3">
<div class="o_wslides_js_slide_like mr-2">
<span class="o_wslides_js_slide_like_up" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
<span t-att-class="('o_wslides_js_slide_like_up %s') % ('disabled' if not (slide_access['can_access'] and slide_access['can_vote']) else '')" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
<i class="fa fa-thumbs-up fa-1x" role="img" aria-label="Likes" title="Likes"></i>
<span t-esc="slide.likes"/>
</span>
<span class="o_wslides_js_slide_like_down ml-3" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
<span t-att-class="('o_wslides_js_slide_like_down ml-3 %s') % ('disabled' if not (slide_access['can_access'] and slide_access['can_vote']) else '')" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
<i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"></i>
<span t-esc="slide.dislikes"/>
</span>
@@ -289,7 +289,7 @@
<div role="tabpanel" t-att-class="comments and 'tab-pane fade in show active' or 'tab-pane fade'" id="discuss">
<t t-call="portal.message_thread">
<t t-set="object" t-value="slide"/>
<t t-set="disable_composer" t-value="not (slide.can_comment and slide.channel_id.allow_comment and slide.channel_id.channel_type == 'training')"/>
<t t-set="disable_composer" t-value="not (slide_access['can_comment'] and slide.channel_id.allow_comment and slide.channel_id.channel_type == 'training')"/>
<t t-set="display_rating" t-value="False"/>
</t>
</div>