[MERGE][IMP] website_slides, website_*: email validation, comment restrictions with karma and search user profile

This merge is improving various points linked to eLearning.
Impacted modules: website_profile, website_forum, website_rating and website_slides

- Move validation email from website_forum to website_profile to use the feature in both Forum and elearning. The purpose is to give first karma points to allow users to comment, vote, ... on slides.
- Add search bar in 'all users' page. To do so, we needed to introduced the `karma_position` field.
- Restrict comment, review and vote based on user's karma in eLearning (minimum karam required to review/vote/comment, like in forum). This feature required to add 3 stored fields, with the benediction of chs !
- Fix channel completion computation and linked karma gain. Also provide some test on karma gain.

See sub commits for more details.

Task-1943788

closes odoo/odoo#31321

Signed-off-by: Jérome Maes (jem) <jem@openerp.com>
This commit is contained in:
Robot Odoo
2019-03-15 20:00:40 +01:00
committed by GitHub
29 changed files with 702 additions and 325 deletions
+24
View File
@@ -8,6 +8,7 @@ class Users(models.Model):
_inherit = 'res.users'
karma = fields.Integer('Karma', default=0)
karma_position = fields.Integer('Karma position', compute="_compute_karma_position", store=False)
badge_ids = fields.One2many('gamification.badge.user', 'user_id', string='Badges', copy=False)
gold_badge = fields.Integer('Gold badges count', compute="_get_user_badge_level")
silver_badge = fields.Integer('Silver badges count', compute="_get_user_badge_level")
@@ -15,6 +16,29 @@ class Users(models.Model):
rank_id = fields.Many2one('gamification.karma.rank', 'Rank', index=False)
next_rank_id = fields.Many2one('gamification.karma.rank', 'Next Rank', index=False)
@api.depends('karma')
def _compute_karma_position(self):
where_query = self._where_calc([])
self._apply_ir_rules(where_query, 'read')
from_clause, where_clause, where_clause_params = where_query.get_sql()
query = """
SELECT sub.id, sub.karma_position
FROM (
SELECT id, row_number() OVER (ORDER BY res_users.karma DESC) AS karma_position
FROM {from_clause}
WHERE {where_clause}
) sub
WHERE sub.id IN %s
""".format(from_clause=from_clause, where_clause=where_clause)
self.env.cr.execute(query, where_clause_params + [tuple(self.ids)])
position_map = {item['id']: item['karma_position'] for item in self.env.cr.dictfetchall()}
for user in self:
user.karma_position = position_map.get(user.id, 0)
@api.multi
@api.depends('badge_ids')
def _get_user_badge_level(self):
+14 -23
View File
@@ -3,6 +3,7 @@
import json
import lxml
import requests
import logging
import werkzeug.exceptions
import werkzeug.urls
import werkzeug.wrappers
@@ -15,6 +16,8 @@ from odoo.addons.website.models.ir_http import sitemap_qs2dom
from odoo.addons.website_profile.controllers.main import WebsiteProfile
from odoo.http import request
_logger = logging.getLogger(__name__)
class WebsiteForum(WebsiteProfile):
_post_per_page = 10
@@ -26,8 +29,6 @@ class WebsiteForum(WebsiteProfile):
values.update({
'header': kwargs.get('header', dict()),
'searches': kwargs.get('searches', dict()),
'validation_email_sent': request.session.get('validation_email_sent', False),
'validation_email_done': request.session.get('validation_email_done', False),
})
if kwargs.get('forum'):
values['forum'] = kwargs.get('forum')
@@ -37,32 +38,21 @@ class WebsiteForum(WebsiteProfile):
# User and validation
# --------------------------------------------------
# TODO DBE : Those 3 are deprecated. To remove in v13.
@http.route('/forum/send_validation_email', type='json', auth='user', website=True)
def send_validation_email(self, forum_id=None, **kwargs):
if request.env.uid != request.website.user_id.id:
request.env.user._send_forum_validation_email(forum_id=forum_id)
request.session['validation_email_sent'] = True
return True
def send_forum_validation_email(self, forum_id=None, **kwargs):
_logger.warning("The method send_forum_validation_email() is deprecated and should be removed in v13.")
return self.send_forum_validation_email(forum_id=forum_id, **kwargs)
@http.route('/forum/validate_email', type='http', auth='public', website=True, sitemap=False)
def validate_email(self, token, id, email, forum_id=None, **kwargs):
if forum_id:
try:
forum_id = int(forum_id)
except ValueError:
forum_id = None
done = request.env['res.users'].sudo().browse(int(id)).process_forum_validation_token(token, email, forum_id=forum_id)[0]
if done:
request.session['validation_email_done'] = True
if forum_id:
return request.redirect("/forum/%s" % int(forum_id))
return request.redirect('/forum')
def forum_validate_email(self, token, user_id, email, forum_id=None, **kwargs):
_logger.warning("The method forum_validate_email() is deprecated and should be removed in v13.")
return self.forum_validate_email(token=token, user_id=user_id, email=email, forum_id=forum_id, **kwargs)
@http.route('/forum/validate_email/close', type='json', auth='public', website=True)
def validate_email_done(self):
request.session['validation_email_done'] = False
return True
def forum_validate_email_done(self, **kwargs):
_logger.warning("The method forum_validate_email_done() is deprecated and should be removed in v13.")
return self.validate_email_done(**kwargs)
# Forum
# --------------------------------------------------
@@ -550,6 +540,7 @@ class WebsiteForum(WebsiteProfile):
# Profile
# -----------------------------------
@http.route(['/forum/<model("forum.forum"):forum>/user/<int:user_id>'], type='http', auth="public", website=True)
def view_user_forum_profile(self, forum, user_id, **post):
return werkzeug.utils.redirect('/profile/user/' + str(user_id) + '?forum_id=' + str(forum.id))
-102
View File
@@ -129,107 +129,5 @@
<field name="reason_type">offensive</field>
</record>
<!-- Email template for email validation (for karma purpose) -->
<record id="validation_email" model="mail.template">
<field name="name">Forum: Email Verification</field>
<field name="model_id" ref="base.model_res_users"/>
<field name="subject">${object.company_id.name} Forums validation</field>
<field name="email_from">${('&lt;%s&gt;' % (object.env.user.company_id.email or user.email)) | safe}</field>
<field name="email_to">${object.email|safe}</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr>
<td valign="middle">
<span style="font-size: 10px;">Your Forum</span><br/>
<span style="font-size: 20px; font-weight: bold;">
${object.company_id.name} Forum validation
</span>
</td>
<td valign="middle" align="right">
<img src="/logo.png?company=${user.company_id.id}" style="padding: 0px; margin: 0px; height: auto; width: 80px;" alt="${user.company_id.name}"/>
</td>
</tr>
<tr>
<td colspan="2" style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin:16px 0px 16px 0px;"/>
</td>
</tr>
</table>
</td>
</tr>
<!-- CONTENT -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr>
<td valign="top" style="font-size: 13px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Hello ${object.name},<br /><br />
You have been invited to validate your email in order to get access to "${object.company_id.name}" Q/A Forums.
To validate your email, please click on the following link:
<div style="margin: 16px 0px 16px 0px;">
<a href="${ctx.get('token_url')}"
style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
Validate my account
</a>
</div>
Thanks for your participation!
</p>
</td>
</tr>
<tr>
<td style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td>
</tr>
</table>
</td>
</tr>
<!-- FOOTER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style=" font-family: 'Verdana Regular'; color: #454748; min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr>
<td valign="middle" align="left">
${user.company_id.name}
</td>
<td valign="middle" align="right" style="opacity: 0.7;">
${user.company_id.phone}
% if user.company_id.email:
| <a href="'mailto:%s' % ${user.company_id.email}" style="text-decoration:none; color: #454748;">
${user.company_id.email}
</a>
% endif
% if user.company_id.website:
| <a href="${user.company_id.website}" style="text-decoration:none; color: #454748;">
${user.company_id.website}
</a>
% endif
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td></tr>
<!-- POWERED BY -->
<tr><td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
<tr><td style="text-align: center; font-size: 13px;">
Powered by <a target="_blank" href="https://www.odoo.com?utm_source=db&amp;utm_medium=forum" style="color: #875A7B;">Odoo</a>
</td></tr>
</table>
</td></tr>
</table>
</field>
</record>
</data>
</odoo>
+2 -12
View File
@@ -4,7 +4,6 @@
import logging
import math
import re
import uuid
from datetime import datetime
from odoo.addons.gamification.models.gamification_karma_rank import KarmaError
@@ -21,15 +20,6 @@ class Forum(models.Model):
_description = 'Forum'
_inherit = ['mail.thread', 'website.seo.metadata', 'website.multi.mixin']
@api.model_cr
def init(self):
""" Add forum uuid for user email validation.
TDE TODO: move me somewhere else, auto_init ? """
forum_uuids = self.env['ir.config_parameter'].search([('key', '=', 'website_forum.uuid')])
if not forum_uuids:
forum_uuids.set_param('website_forum.uuid', str(uuid.uuid4()))
@api.model
def _get_default_faq(self):
with misc.file_open('website_forum/data/forum_default_faq.html', 'r') as f:
@@ -590,7 +580,7 @@ class Post(models.Model):
@api.one
def refuse(self):
if not self.can_moderate:
raise KarmaError('Not enough karma to refuse a post')
raise KarmaError(_('Not enough karma to refuse a post'))
self.moderator_id = self.env.user
return True
@@ -809,7 +799,7 @@ class Post(models.Model):
self.ensure_one()
if not self.can_comment:
raise KarmaError('Not enough karma to comment')
raise KarmaError(_('Not enough karma to comment'))
if not kwargs.get('record_name') and self.parent_id:
kwargs['record_name'] = self.parent_id.name
return super(Post, self).message_post(message_type=message_type, **kwargs)
-51
View File
@@ -32,57 +32,6 @@ class Users(models.Model):
domain = [('parent_id', '=', False), ('state', '=', 'pending'), ('create_uid', '=', user.id)]
user.forum_waiting_posts_count = Post.search_count(domain)
@api.model
def _generate_forum_token(self, user_id, email):
"""Return a token for email validation. This token is valid for the day
and is a hash based on a (secret) uuid generated by the forum module,
the user_id, the email and currently the day (to be updated if necessary). """
forum_uuid = self.env['ir.config_parameter'].sudo().get_param('website_forum.uuid')
return hashlib.sha256((u'%s-%s-%s-%s' % (
datetime.now().replace(hour=0, minute=0, second=0, microsecond=0),
forum_uuid,
user_id,
email
)).encode('utf-8')).hexdigest()
@api.one
def _send_forum_validation_email(self, forum_id=None):
if not self.email:
return False
token = self._generate_forum_token(self.id, self.email)
activation_template = self.env.ref('website_forum.validation_email')
if activation_template:
params = {
'token': token,
'id': self.id,
'email': self.email}
if forum_id:
params['forum_id'] = forum_id
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
token_url = base_url + '/forum/validate_email?%s' % urls.url_encode(params)
with self._cr.savepoint():
activation_template.sudo().with_context(token_url=token_url).send_mail(
self.id, force_send=True, raise_exception=True)
return True
@api.one
def process_forum_validation_token(self, token, email, forum_id=None, context=None):
validation_token = self._generate_forum_token(self.id, email)
if token == validation_token and self.karma == 0:
karma = 3
forum = None
if forum_id:
forum = self.env['forum.forum'].browse(forum_id)
else:
forum_ids = self.env['forum.forum'].search([], limit=1)
if forum_ids:
forum = forum_ids[0]
if forum:
# karma gained: karma to ask a question and have 2 downvotes
karma = forum.karma_ask + (-2 * forum.karma_gen_question_downvote)
return self.write({'karma': karma})
return False
# Wrapper for call_kw with inherits
@api.multi
def open_website_url(self):
@@ -6,6 +6,7 @@ var Wysiwyg = require('web_editor.wysiwyg.root');
var sAnimations = require('website.content.snippets.animation');
var session = require('web.session');
var qweb = core.qweb;
var WebsiteProfile = require('website_profile.website_profile');
var _t = core._t;
@@ -29,8 +30,6 @@ sAnimations.registry.websiteForum = sAnimations.Class.extend({
'click .favourite_question': '_onFavoriteQuestionClick',
'click .comment_delete': '_onDeleteCommentClick',
'click .notification_close': '_onCloseNotificationClick',
'click .send_validation_email': '_onSendValidationEmailClick',
'click .validated_email_close': '_onCloseValidatedEmailClick',
'click .js_close_intro': '_onCloseIntroClick',
},
@@ -465,32 +464,6 @@ sAnimations.registry.websiteForum = sAnimations.Class.extend({
});
}
},
/**
* @private
* @param {Event} ev
*/
_onSendValidationEmailClick: function (ev) {
ev.preventDefault();
var $link = $(ev.currentTarget);
this._rpc({
route: '/forum/send_validation_email',
params: {
forum_id: $link.attr('forum-id'),
},
}).then(function (data) {
if (data) {
$('button.validation_email_close').click();
}
});
},
/**
* @private
*/
_onCloseValidatedEmailClick: function () {
this._rpc({
route: '/forum/validate_email/close',
});
},
/**
* @private
* @param {Event} ev
@@ -579,4 +552,5 @@ sAnimations.registry.websiteForumSpam = sAnimations.Class.extend({
});
},
});
});
+6 -16
View File
@@ -52,22 +52,12 @@
<div id="wrap" t-attf-class="container #{website_forum_action}">
<div class="row">
<div class="col o_left_forum_container mb16 mt16">
<div t-if="not validation_email_sent and not is_public_user and user.karma == 0" class="alert alert-danger alert-dismissable" role="alert">
<button type="button" class="close validation_email_close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
<div>
<p>
It appears your email has not been verified.
<a class="send_validation_email" href="#" t-att-forum-id="forum.id">Click here to send a verification email allowing you to participate to the forum.</a>
</p>
</div>
</div>
<div t-if="validation_email_done" class="alert alert-success alert-dismissable" role="status">
<button type="button" class="close validated_email_close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
<div>
<p>Congratulations! Your email has just been validated. You may now participate to our forums.</p>
</div>
</div>
<div class="o_wprofile_email_validation_container mb16 mt16">
<t t-call="website_profile.email_validation_banner">
<t t-set="redirect_url" t-value="'/forum/%s' % forum.id"/>
<t t-set="send_validation_email_message" t-value="'Click here to send a verification email allowing you to participate to the forum.'"/>
<t t-set="additional_validated_email_message" t-value="' You may now participate to our forums.'"/>
</t>
<t t-raw="0"/>
</div>
<t t-if="not is_profile_page">
+1
View File
@@ -12,6 +12,7 @@
'gamification'
],
'data': [
'data/profile_data.xml',
'views/gamification_badge_views.xml',
'views/website_profile.xml',
'security/ir.model.access.csv',
+45 -20
View File
@@ -6,6 +6,7 @@ import werkzeug
import werkzeug.exceptions
import werkzeug.urls
import werkzeug.wrappers
import math
from odoo import http, modules, tools
from odoo.http import request
@@ -13,8 +14,9 @@ from odoo.osv import expression
class WebsiteProfile(http.Controller):
PAGER_MAX_PAGE = 5
_users_per_page = 30
_pager_max_pages = 5
# Profile
# ---------------------------------------------------
@@ -57,6 +59,8 @@ class WebsiteProfile(http.Controller):
values = {
'user': request.env.user,
'is_public_user': request.website.is_public_user(),
'validation_email_sent': request.session.get('validation_email_sent', False),
'validation_email_done': request.session.get('validation_email_done', False),
}
values.update(kwargs)
return values
@@ -111,8 +115,9 @@ class WebsiteProfile(http.Controller):
user = self._check_user_profile_access(user_id)
if not user:
return request.render("website_profile.private_profile")
values = self._prepare_user_values(**post)
params = self._prepare_user_profile_parameters(**post)
values = self._prepare_user_profile_values(user, **params)
values.update(self._prepare_user_profile_values(user, **params))
return request.render("website_profile.user_profile_main", values)
# Edit Profile
@@ -195,11 +200,12 @@ class WebsiteProfile(http.Controller):
# All Users Page
# ---------------------------------------------------
def _prepare_all_users_values(self, user, position):
def _prepare_all_users_values(self, user):
return {
'position': position,
'position': user.karma_position,
'id': user.id,
'name': user.name,
'company_name': user.company_id.name,
'rank': user.rank_id.name,
'karma': user.karma,
'badge_count': len(user.badge_ids),
@@ -211,26 +217,45 @@ class WebsiteProfile(http.Controller):
User = request.env['res.users']
dom = [('karma', '>', 1), ('website_published', '=', True)]
# Get the Top 3 users
top3_users = User.sudo().search(dom, limit=3, order='karma DESC')
top3_user_values = [self._prepare_all_users_values(user, position+1) for position, user in enumerate(top3_users)]
# Searches
search_term = searches.get('search')
if search_term:
dom = expression.AND([['|', ('name', 'ilike', search_term), ('company_id.name', 'ilike', search_term)], dom])
# Get the other users
if top3_users:
dom += [('id', 'not in', top3_users.ids)]
step = 30
user_count = User.sudo().search_count(dom)
pager = request.website.pager(url="/profile/users", total=user_count, page=page, step=step, scope=step)
page_count = math.ceil(user_count / self._users_per_page)
pager = request.website.pager(url="/profile/users", total=user_count, page=page, step=self._users_per_page,
scope=page_count if page_count < self._pager_max_pages else self._pager_max_pages)
if searches.get('user'):
dom += [('name', 'ilike', searches.get('user'))]
users = User.sudo().search(dom, limit=self._users_per_page, offset=pager['offset'], order='karma DESC')
user_values = [self._prepare_all_users_values(user) for user in users]
users = User.sudo().search(dom, limit=step, offset=pager['offset'], order='karma DESC')
user_values = [self._prepare_all_users_values(user, position + 4 + ((page-1) * step)) for position, user in enumerate(users)]
values = {
'top3_users': top3_user_values,
'users': user_values,
'top3_users': user_values[:3] if not search_term and page == 1 else None,
'users': user_values[3:] if not search_term and page == 1 else user_values,
'pager': pager
}
return request.render("website_profile.users_page_main", values)
# User and validation
# --------------------------------------------------
@http.route('/profile/send_validation_email', type='json', auth='user', website=True)
def send_validation_email(self, **kwargs):
if request.env.uid != request.website.user_id.id:
request.env.user._send_profile_validation_email(**kwargs)
request.session['validation_email_sent'] = True
return True
@http.route('/profile/validate_email', type='http', auth='public', website=True, sitemap=False)
def validate_email(self, token, user_id, email, **kwargs):
done = request.env['res.users'].sudo().browse(int(user_id))._process_profile_validation_token(token, email)
if done:
request.session['validation_email_done'] = True
url = kwargs.get('redirect_url', '/')
return request.redirect(url)
@http.route('/profile/validate_email/close', type='json', auth='public', website=True)
def validate_email_done(self, **kwargs):
request.session['validation_email_done'] = False
return True
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Email template for email validation (for karma purpose) -->
<record id="validation_email" model="mail.template">
<field name="name">Profile: Email Verification</field>
<field name="model_id" ref="base.model_res_users"/>
<field name="subject">${object.company_id.name} Profile validation</field>
<field name="email_from">${('&lt;%s&gt;' % (object.env.user.company_id.email or user.email)) | safe}</field>
<field name="email_to">${object.email|safe}</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr>
<td valign="middle">
<span style="font-size: 20px; font-weight: bold;">
${object.company_id.name} Profile validation
</span>
</td>
<td valign="middle" align="right">
<img src="/logo.png?company=${user.company_id.id}" style="padding: 0px; margin: 0px; height: auto; width: 80px;" alt="${user.company_id.name}"/>
</td>
</tr>
<tr>
<td colspan="2" style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin:16px 0px 16px 0px;"/>
</td>
</tr>
</table>
</td>
</tr>
<!-- CONTENT -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr>
<td valign="top" style="font-size: 13px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Hello ${object.name},<br /><br />
You have been invited to validate your email in order to get access to "${object.company_id.name}" website.
To validate your email, please click on the following link:
<div style="margin: 16px 0px 16px 0px;">
<a href="${ctx.get('token_url')}"
style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
Validate my account
</a>
</div>
Thanks for your participation!
</p>
</td>
</tr>
<tr>
<td style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td>
</tr>
</table>
</td>
</tr>
<!-- FOOTER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style=" font-family: 'Verdana Regular'; color: #454748; min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr>
<td valign="middle" align="left">
${user.company_id.name}
</td>
<td valign="middle" align="right" style="opacity: 0.7;">
${user.company_id.phone}
% if user.company_id.email:
| <a href="'mailto:%s' % ${user.company_id.email}" style="text-decoration:none; color: #454748;">
${user.company_id.email}
</a>
% endif
% if user.company_id.website:
| <a href="${user.company_id.website}" style="text-decoration:none; color: #454748;">
${user.company_id.website}
</a>
% endif
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td></tr>
<!-- POWERED BY -->
<tr><td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
<tr><td style="text-align: center; font-size: 13px;">
Powered by <a target="_blank" href="https://www.odoo.com?utm_source=db&amp;utm_medium=forum" style="color: #875A7B;">Odoo</a>
</td></tr>
</table>
</td></tr>
</table>
</field>
</record>
</data>
</odoo>
@@ -3,3 +3,4 @@
from . import gamification_badge
from . import website
from . import res_users
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import hashlib
import uuid
from datetime import datetime
from werkzeug import urls
from odoo import api, models
VALIDATION_KARMA_GAIN = 3
class Users(models.Model):
_inherit = 'res.users'
@api.model
def _generate_profile_token(self, user_id, email):
"""Return a token for email validation. This token is valid for the day
and is a hash based on a (secret) uuid generated by the forum module,
the user_id, the email and currently the day (to be updated if necessary). """
profile_uuid = self.env['ir.config_parameter'].sudo().get_param('website_profile.uuid')
if not profile_uuid:
profile_uuid = str(uuid.uuid4())
self.env['ir.config_parameter'].sudo().set_param('website_profile.uuid', profile_uuid)
return hashlib.sha256((u'%s-%s-%s-%s' % (
datetime.now().replace(hour=0, minute=0, second=0, microsecond=0),
profile_uuid,
user_id,
email
)).encode('utf-8')).hexdigest()
def _send_profile_validation_email(self, **kwargs):
if not self.email:
return False
token = self._generate_profile_token(self.id, self.email)
activation_template = self.env.ref('website_profile.validation_email')
if activation_template:
params = {
'token': token,
'user_id': self.id,
'email': self.email
}
params.update(kwargs)
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
token_url = base_url + '/profile/validate_email?%s' % urls.url_encode(params)
with self._cr.savepoint():
activation_template.sudo().with_context(token_url=token_url).send_mail(
self.id, force_send=True, raise_exception=True)
return True
@api.multi
def _process_profile_validation_token(self, token, email):
self.ensure_one()
validation_token = self._generate_profile_token(self.id, email)
if token == validation_token and self.karma == 0:
return self.write({'karma': VALIDATION_KARMA_GAIN})
return False
@@ -0,0 +1,47 @@
odoo.define('website_profile.website_profile', function (require) {
'use strict';
var sAnimations = require('website.content.snippets.animation');
//TODO DBE : to change into publicWidget = require('web.public.widget'); after forward port in master
sAnimations.registry.websiteProfile = sAnimations.Class.extend({
selector: '.o_wprofile_email_validation_container',
read_events: {
'click .send_validation_email': '_onSendValidationEmailClick',
'click .validated_email_close': '_onCloseValidatedEmailClick',
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Event} ev
*/
_onSendValidationEmailClick: function (ev) {
ev.preventDefault();
var self = this;
var $element = $(ev.currentTarget);
this._rpc({
route: '/profile/send_validation_email',
params: {'redirect_url': $element.data('redirect_url')},
}).then(function (data) {
if (data) {
self.$('button.validation_email_close').click();
}
});
},
/**
* @private
*/
_onCloseValidatedEmailClick: function () {
this._rpc({
route: '/profile/validate_email/close',
});
},
});
return sAnimations.registry.websiteProfile;
});
@@ -118,11 +118,54 @@ $owprofile-tabs-height: 37px;
}
// All Users Page
.o_wprofile_all_users_nav {
margin: auto;
background: linear-gradient(90deg, #875A7B 20%, #62495B 80%) !important;
a {
color: #FFFFFF;
}
nav {
.breadcrumb-item::before {
font-weight: bold;
color: #FFFFFF;
opacity: 0.8;
}
.breadcrumb-item {
a {
font-weight: bold;
color: #FFFFFF;
opacity: 0.8;
transition: opacity linear .3s;
}
&.active {
a {
opacity: 1.0;
}
}
}
ol {
background-color: transparent;
}
}
}
.o_wprofile_all_users_header {
@include media-breakpoint-up(sm) {
min-height: 200px;
margin-bottom: -60px;
}
margin: auto;
background: linear-gradient(90deg, #875A7B 20%, #62495B 80%) !important;
> div:first-child {
margin: auto;
width: 100%;
max-width: 1140px;
padding: 32px 32px 0px 32px;
color: white;
top: 48px;
padding-top: 0px;
height: 250px;
}
}
.o_wprofile_top3_card_footer div {
@@ -4,6 +4,9 @@
<xpath expr="link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/website_profile/static/src/scss/website_profile.scss"/>
</xpath>
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/website_profile/static/src/js/website_profile.js"/>
</xpath>
</template>
@@ -130,7 +133,7 @@
<div class="col-12 col-md-4 col-lg-3">
<div t-attf-class="d-flex align-items-start #{'justify-content-between' if (request.env.user == user) else 'justify-content-around' }">
<div class="o_wprofile_pict d-inline-block mb-3 mb-md-0" t-attf-style="background-image:url(/web/image/res.users/#{user.id}/image)"/>
<a class="btn btn-primary d-inline-block d-md-none" t-if="request.env.user == user" t-attf-href="/profile/edit?url_param=#{edit_button_url_param}">
<a class="btn btn-primary d-inline-block d-md-none" t-if="request.env.user == user and validation_email_done" t-attf-href="/profile/edit?url_param=#{edit_button_url_param}">
<i class="fa fa-pencil mr-1"/>EDIT
</a>
</div>
@@ -139,8 +142,10 @@
<!-- ==== Header Right ==== -->
<div class="col-12 col-md-8 col-lg-9 d-flex flex-column">
<div class="d-flex justify-content-between align-items-start">
<h1 class="o_card_people_name" t-field="user.name"/>
<a class="btn btn-primary d-none d-md-inline-block" t-if="request.env.user == user" t-attf-href="/profile/edit?url_param=#{edit_button_url_param}">
<h1 class="o_card_people_name">
<span t-field="user.name"/><small t-if="not validation_email_done"> (not verified)</small>
</h1>
<a class="btn btn-primary d-none d-md-inline-block" t-if="request.env.user == user and validation_email_done" t-attf-href="/profile/edit?url_param=#{edit_button_url_param}">
<i class="fa fa-pencil mr-2"/>EDIT PROFILE
</a>
</div>
@@ -212,6 +217,12 @@
</ul>
<div class="tab-content py-4 o_wprofile_tabs_content mb-4" id="profile_extra_info_tabcontent">
<div role="tabpanel" class="tab-pane active" id="profile_tab_content_about">
<div class="o_wprofile_email_validation_container mb16 mt16">
<t t-call="website_profile.email_validation_banner">
<t t-set="redirect_url" t-value="'/profile/user/%s' % user.id"/>
<t t-set="send_validation_email_message" t-value="'Click here to send a verification email.'"/>
</t>
</div>
<div id="profile_about_badge" class="mb32">
<h5 class="border-bottom pb-1">Badges</h5>
<t t-call="website_profile.user_badges"></t>
@@ -381,6 +392,40 @@
</template>
<template id="users_page_header" name="Users Page Header">
<div class="o_wprofile_all_users_nav">
<div class="container">
<div class="row align-items-center justify-content-between">
<!-- nav -->
<nav aria-label="breadcrumb" class="col-md-8 col-xs-12">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a t-att-href="home_url or '/'">Home</a>
</li>
<li t-att-class="'breadcrumb-item active'">
<a href="#">Users</a>
</li>
</ol>
</nav>
<div class="col-md-4 col-xs-12 d-flex flex-row align-items-center">
<!-- karma / profile -->
<a t-if="not is_public_user" t-att-href="'/profile/user/%s' % user_id.id" class="font-weight-bold mr-3">
<i class="fa fa-diamond" /> <t t-esc="user_id.karma"/>
</a>
<!-- search -->
<form t-attf-action="/profile/users" role="search" method="get">
<div class="input-group">
<input type="text" class="form-control" name="search" placeholder="Search in Users" t-att-value="search"/>
<span class="input-group-append">
<button class="btn btn-primary" type="submit" aria-label="Search" title="Search">
<i class="fa fa-search"></i>
</button>
</span>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="o_wprofile_all_users_header o_wprofile_gradient text-white">
<div class="container">
<h1 class="py-5">All Users</h1>
@@ -463,4 +508,26 @@
</ul>
</template>
<template id="email_validation_banner">
<div t-if="not validation_email_sent and not is_public_user and user.karma == 0" class="alert alert-danger alert-dismissable" role="alert">
<button type="button" class="close validation_email_close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
<div>
<p>
It appears your email has not been verified.
<a class="send_validation_email" href="#" t-att-data-redirect_url="redirect_url">
<span t-esc="send_validation_email_message"/></a>
</p>
</div>
</div>
<div t-if="validation_email_done" class="alert alert-success alert-dismissable" role="status">
<button type="button" class="close validated_email_close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
<div>
<p>
<span id="email_validated_message">Congratulations! Your email has just been validated.</span>
<span t-esc="additional_validated_email_message"/>
</p>
</div>
</div>
</template>
</data></odoo>
@@ -301,7 +301,7 @@ odoo.define('website_rating.thread', function(require) {
'res_model': false,
'res_id': false,
'pid': 0,
'display_composer': !session.is_website_user,
'display_composer': options['disable_composer'] ? false : !session.is_website_user,
'display_rating': true,
'csrf_token': odoo.csrf_token,
'user_id': session.user_id,
@@ -61,7 +61,8 @@
t-att-data-default_message="default_message"
t-att-data-default_message_id="default_message_id"
t-att-data-default_rating_value="default_rating_value"
t-att-data-force_submit_url="force_submit_url">
t-att-data-force_submit_url="force_submit_url"
t-att-data-disable_composer="disable_composer">
</div>
</template>
+37 -24
View File
@@ -20,9 +20,9 @@ _logger = logging.getLogger(__name__)
class WebsiteSlides(WebsiteProfile):
SLIDES_PER_PAGE = 12
SLIDES_PER_ASIDE = 20
SLIDES_PER_CATEGORY = 4
_slides_per_page = 12
_slides_per_aside = 20
_slides_per_category = 4
_channel_order_by_criterion = {
'vote': 'total_votes desc',
'view': 'total_views desc',
@@ -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}
@@ -73,8 +70,8 @@ class WebsiteSlides(WebsiteProfile):
if slide.channel_id.channel_type == 'documentation':
related_domain = expression.AND([base_domain, [('category_id', '=', slide.category_id.id)]])
most_viewed_slides = request.env['slide.slide'].search(base_domain, limit=self.SLIDES_PER_ASIDE, order='total_views desc')
related_slides = request.env['slide.slide'].search(related_domain, limit=self.SLIDES_PER_ASIDE)
most_viewed_slides = request.env['slide.slide'].search(base_domain, limit=self._slides_per_aside, order='total_views desc')
related_slides = request.env['slide.slide'].search(related_domain, limit=self._slides_per_aside)
category_data = []
uncategorized_slides = request.env['slide.slide']
else:
@@ -242,9 +239,8 @@ class WebsiteSlides(WebsiteProfile):
else:
users = None
return request.render('website_slides.courses_home', {
'user': request.env.user,
'is_public_user': request.website.is_public_user(),
values = self._prepare_user_values(**post)
values.update({
'channels_my': channels_my,
'channels_popular': channels_popular,
'channels_newest': channels_newest,
@@ -254,6 +250,8 @@ class WebsiteSlides(WebsiteProfile):
'challenges_done': challenges_done,
})
return request.render('website_slides.courses_home', values)
@http.route('/slides/all', type='http', auth="public", website=True)
def slides_channel_all(self, slide_type=None, my=False, **post):
""" Home page displaying a list of courses displayed according to some
@@ -282,9 +280,8 @@ class WebsiteSlides(WebsiteProfile):
tag_groups = request.env['slide.channel.tag.group'].search(['&', ('tag_ids', '!=', False), ('website_published', '=', True)])
search_tags = self._extract_channel_tag_search(**post)
return request.render('website_slides.courses_all', {
'user': request.env.user,
'is_public_user': request.website.is_public_user(),
values = self._prepare_user_values(**post)
values.update({
'channels': channels,
'tag_groups': tag_groups,
'search_term': post.get('search'),
@@ -294,6 +291,8 @@ class WebsiteSlides(WebsiteProfile):
'search_channel_tag_id': post.get('channel_tag_id'),
})
return request.render('website_slides.courses_all', values)
def _prepare_additional_channel_values(self, values, **kwargs):
return values
@@ -346,10 +345,10 @@ class WebsiteSlides(WebsiteProfile):
pager_args['sorting'] = actual_sorting
slide_count = request.env['slide.slide'].sudo().search_count(domain)
page_count = math.ceil(slide_count / self.SLIDES_PER_PAGE)
page_count = math.ceil(slide_count / self._slides_per_page)
pager = request.website.pager(url=pager_url, total=slide_count, page=page,
step=self.SLIDES_PER_PAGE, url_args=pager_args,
scope=page_count if page_count < self.PAGER_MAX_PAGE else self.PAGER_MAX_PAGE)
step=self._slides_per_page, url_args=pager_args,
scope=page_count if page_count < self._pager_max_pages else self._pager_max_pages)
values = {
'channel': channel,
@@ -368,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([
@@ -380,12 +378,15 @@ class WebsiteSlides(WebsiteProfile):
], order='write_date DESC', limit=1).read(['body', 'rating_value'])
last_message_data = last_message_values[0] if last_message_values else {}
values.update({
'message_post_hash': channel._sign_token(request.env.user.partner_id.id),
'message_post_pid': request.env.user.partner_id.id,
'last_message_id': last_message_data.get('id'),
'last_message': tools.html2plaintext(last_message_data.get('body', '')),
'last_rating_value': last_message_data.get('rating_value'),
})
if channel.can_review:
values.update({
'message_post_hash': channel._sign_token(request.env.user.partner_id.id),
'message_post_pid': request.env.user.partner_id.id,
})
# fetch slides and handle uncategorized slides; done as sudo because we want to display all
# of them but unreachable ones won't be clickable (+ slide controller will crash anyway)
@@ -395,12 +396,13 @@ class WebsiteSlides(WebsiteProfile):
values['category_data'] = channel._get_categorized_slides(
domain, order,
force_void=True,
limit=self.SLIDES_PER_CATEGORY if channel.channel_type == 'documentation' else False,
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
@@ -489,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''',
@@ -571,7 +575,16 @@ 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:
return {'error': 'missing_karma'}
if upvote:
slide.action_like()
else:
+32 -12
View File
@@ -6,6 +6,7 @@ import uuid
from odoo import api, fields, models, tools, _
from odoo.addons.http_routing.models.ir_http import slug
from odoo.addons.gamification.models.gamification_karma_rank import KarmaError
from odoo.exceptions import UserError
from odoo.osv import expression
@@ -46,15 +47,22 @@ class ChannelUsersRelation(models.Model):
record.completion = math.ceil(100.0 * slide_done / slide_total)
def _write(self, values):
partner_karma = False
if 'completion' in values and values['completion'] >= 100:
values['completed'] = True
result = super(ChannelUsersRelation, self)._write(values)
partner_has_completed = {channel_partner.partner_id.id: channel_partner.channel_id for channel_partner in self}
users = self.env['res.users'].sudo().search([('partner_id', 'in', list(partner_has_completed.keys()))])
incomplete_channel_partners = self.filtered(lambda cp: not cp.completed)
partner_karma = dict.fromkeys(incomplete_channel_partners.mapped('partner_id').ids, 0)
for channel_partner in incomplete_channel_partners:
partner_karma[channel_partner.partner_id.id] += channel_partner.channel_id.karma_gen_channel_finish
partner_karma = {partner_id: karma_to_add
for partner_id, karma_to_add in partner_karma.items() if karma_to_add > 0}
result = super(ChannelUsersRelation, self)._write(values)
if partner_karma:
users = self.env['res.users'].sudo().search([('partner_id', 'in', list(partner_karma.keys()))])
for user in users:
users.add_karma(partner_has_completed[user.partner_id.id].karma_gen_channel_finish)
else:
result = super(ChannelUsersRelation, self)._write(values)
users.add_karma(partner_karma[user.partner_id.id])
return result
@@ -107,7 +115,7 @@ class Channel(models.Model):
total_time = fields.Float('# Hours', compute='_compute_slides_statistics', digits=(10, 4), store=True)
# configuration
allow_comment = fields.Boolean(
"Allow comment on Content", default=False,
"Allow rating on Course", default=False,
help="If checked it allows members to either:\n"
" * like content and post comments on documentation course;\n"
" * post comment and review on training course;")
@@ -146,10 +154,13 @@ class Channel(models.Model):
can_upload = fields.Boolean('Can Upload', compute='_compute_can_upload')
# karma generation
karma_gen_slide_vote = fields.Integer(string='Lesson voted', default=1)
karma_gen_channel_share = fields.Integer(string='Course shared', default=2)
karma_gen_channel_rank = fields.Integer(string='Course ranked', default=5)
karma_gen_channel_finish = fields.Integer(string='Course finished', default=10)
# TODO DBE : Add karma based action rules (like in forum)
# Karma based actions
karma_review = fields.Integer('Add a review', default=10, help="Karma needed to add a review on the course")
karma_slide_comment = fields.Integer('Add a comment', default=3, help="Karma needed to add a comment on a slide of this course")
karma_slide_vote = fields.Integer('Vote on slide', default=3, help="Karma needed to like/dislike a slide of this course.")
can_review = fields.Boolean('Can Review', compute='_compute_karma_rights')
@api.depends('slide_ids.is_published')
def _compute_slide_last_update(self):
@@ -257,6 +268,11 @@ class Channel(models.Model):
if channel.id: # avoid to perform a slug on a not yet saved record in case of an onchange.
channel.website_url = '%s/slides/%s' % (base_url, slug(channel))
@api.multi
def _compute_karma_rights(self):
for channel in self:
channel.can_review = self.env.user.karma >= channel.karma_review
# ---------------------------------------------------------
# ORM Overrides
# ---------------------------------------------------------
@@ -312,6 +328,8 @@ class Channel(models.Model):
through the 'Presentation Published' email, it should be considered as a
note as we don't want all channel followers to be notified of this answer. """
self.ensure_one()
if kwargs.get('message_type') == 'comment' and not self.can_review:
raise KarmaError(_('Not enough karma to review'))
if parent_id:
parent_message = self.env['mail.message'].sudo().browse(parent_id)
if parent_message.subtype_id and parent_message.subtype_id == self.env.ref('website_slides.mt_channel_slide_published'):
@@ -435,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
+49 -3
View File
@@ -13,7 +13,8 @@ from werkzeug import urls
from odoo import api, fields, models, _
from odoo.addons.http_routing.models.ir_http import slug
from odoo.exceptions import Warning, UserError
from odoo.addons.gamification.models.gamification_karma_rank import KarmaError
from odoo.exceptions import Warning, UserError, AccessError
from odoo.http import request
from odoo.addons.http_routing.models.ir_http import url_for
@@ -155,6 +156,9 @@ class Slide(models.Model):
slide_views = fields.Integer('# of Website Views', store=True, compute="_compute_slide_views")
public_views = fields.Integer('# of Public Views')
total_views = fields.Integer("Total # Views", default="0", compute='_compute_total', store=True)
# Karma Based action
can_comment = fields.Boolean('Can Comment', compute='_compute_karma_rights')
can_vote = fields.Boolean('Can Vote', compute='_compute_karma_rights')
_sql_constraints = [
('exclusion_html_content_and_url', "CHECK(html_content IS NULL OR url IS NULL)", "A slide is either filled with a document url or HTML content. Not both.")
@@ -267,6 +271,13 @@ class Slide(models.Model):
def _get_can_publish_error_message(self):
return _("Publishing is restricted to the responsible of training courses or members of the publisher group for documentation courses")
@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
# ---------------------------------------------------------
# ORM Overrides
# ---------------------------------------------------------
@@ -313,6 +324,14 @@ class Slide(models.Model):
# Mail/Rating
# ---------------------------------------------------------
@api.multi
@api.returns('mail.message', lambda value: value.id)
def message_post(self, message_type='notification', **kwargs):
self.ensure_one()
if message_type == 'comment' and not self.can_comment: # user comments have a restriction on karma
raise KarmaError(_('Not enough karma to comment'))
return super(Slide, self).message_post(message_type=message_type, **kwargs)
@api.multi
def get_access_action(self, access_uid=None):
""" Instead of the classic form view, redirect to website if it is published. """
@@ -338,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
# ---------------------------------------------------------
@@ -395,7 +436,9 @@ class Slide(models.Model):
('slide_id', 'in', self.ids),
('partner_id', '=', self.env.user.partner_id.id)
])
new_slides = self_sudo - slide_partners.mapped('slide_id')
slide_id = slide_partners.mapped('slide_id')
new_slides = self_sudo - slide_id
channel = slide_id.channel_id
for slide_partner in slide_partners:
if upvote:
@@ -409,7 +452,10 @@ class Slide(models.Model):
new_slide.write({
'slide_partner_ids': [(0, 0, {'vote': new_vote, 'partner_id': self.env.user.partner_id.id})]
})
self.env.user.add_karma(new_slide.channel_id.karma_gen_slide_vote)
if new_vote != 0:
self.env.user.add_karma(channel.karma_gen_slide_vote)
else:
self.env.user.add_karma(-channel.karma_gen_slide_vote)
def action_set_viewed(self, quiz_attempts_inc=False):
if not all(slide.channel_id.is_member for slide in self):
@@ -56,9 +56,15 @@ 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') {
self._popoverAlert(self.$el, _t('You don\'t have enough karma to vote'));
} else {
self._popoverAlert(self.$el, _t('Unknown error'));
}
@@ -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 {
+1
View File
@@ -4,3 +4,4 @@
from . import common
from . import test_security
from . import test_statistics
from . import test_karma
+93
View File
@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.website_slides.tests import common
from odoo.tests import tagged
from odoo.tools import mute_logger
@tagged('functional')
class TestKarmaGain(common.SlidesCase):
def setUp(self):
super(TestKarmaGain, self).setUp()
self.channel_2 = self.env['slide.channel'].sudo(self.user_publisher).create({
'name': 'Test Channel 2',
'channel_type': 'training',
'promote_strategy': 'most_voted',
'enroll': 'public',
'visibility': 'public',
'website_published': True,
'karma_gen_channel_finish': 100,
})
self.slide_2 = self.env['slide.slide'].sudo(self.user_publisher).create({
'name': 'How to travel through space and time',
'channel_id': self.channel_2.id,
'slide_type': 'presentation',
'website_published': True,
'completion_time': 2.0,
})
self.channel_3 = self.env['slide.channel'].sudo(self.user_publisher).create({
'name': 'Test Channel 3',
'channel_type': 'training',
'promote_strategy': 'most_voted',
'enroll': 'public',
'visibility': 'public',
'website_published': True,
'karma_gen_channel_finish': 50,
})
self.slide_3 = self.env['slide.slide'].sudo(self.user_publisher).create({
'name': 'How to duplicate yourself',
'channel_id': self.channel_3.id,
'slide_type': 'presentation',
'website_published': True,
'completion_time': 2.0,
})
def karma_gain_test(self, user):
# Add the user to the course
self.channel.sudo()._action_add_members(user.partner_id)
# Init user env
channel = self.channel.sudo(user)
slide = self.slide.sudo(user)
self.assertEqual(user.karma, 0)
# Finish the Course
karma = channel.karma_gen_channel_finish
slide.action_set_completed()
self.assertEqual(user.karma, karma)
# Vote for a slide
karma = karma + channel.karma_gen_slide_vote
slide.action_like()
self.assertEqual(user.karma, karma)
slide.action_dislike()
self.assertEqual(user.karma, karma - channel.karma_gen_slide_vote)
slide.action_dislike()
self.assertEqual(user.karma, karma)
# Finish two course at the same time (should not ever happen but hey, we never know)
self.channel_2.sudo()._action_add_members(user.partner_id)
self.channel_3.sudo()._action_add_members(user.partner_id)
karma = karma + self.channel_2.karma_gen_channel_finish + self.channel_3.karma_gen_channel_finish
slides = self.slide_2.sudo(user) | self.slide_3.sudo(user)
slides.action_set_completed()
self.assertEqual(user.karma, karma)
@mute_logger('odoo.models')
def test_users_karma_gain(self):
self.karma_gain_test(self.user_emp)
@mute_logger('odoo.models')
def test_user_publisher_karma_gain(self):
self.karma_gain_test(self.user_publisher)
@mute_logger('odoo.models')
def test_user_portal_karma_gain(self):
self.karma_gain_test(self.user_portal)
@@ -111,6 +111,20 @@
<field name="allow_comment"/>
</group>
</page>
<page string="Karma Rules" name="karma_rules">
<group>
<group string="Karma generation">
<field name="karma_gen_slide_vote"/>
<field name="karma_gen_channel_rank"/>
<field name="karma_gen_channel_finish"/>
</group>
<group string="Karma based actions">
<field name="karma_review"/>
<field name="karma_slide_comment"/>
<field name="karma_slide_vote"/>
</group>
</group>
</page>
<page string="Statistics" groups="base.group_no_one">
<group>
<group>
@@ -104,6 +104,7 @@
<t t-set="default_message" t-value="last_message"/>
<t t-set="default_rating_value" t-value="last_rating_value"/>
<t t-set="force_submit_url" t-value="'/slides/mail/update_comment' if last_message_id else False"/>
<t t-set="disable_composer" t-value="not channel.allow_comment or not channel.can_review"/>
</t>
</div>
</div>
@@ -115,7 +116,7 @@
<i class="fa fa-home"/> Course
</a>
</li>
<li class="nav-item">
<li class="nav-item" t-if="channel.allow_comment">
<a class="nav-link" id="review-tab" data-toggle="pill" href="#review" role="tab" aria-controls="review" aria-selected="false">Review</a>
</li>
</ul>
@@ -386,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="">
@@ -419,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>
@@ -437,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">
@@ -468,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>
@@ -41,7 +41,15 @@
</div>
</div>
</nav>
<div class="o_wprofile_email_validation_container mb16 mt16">
<t t-call="website_profile.email_validation_banner">
<t t-set="redirect_url" t-value="'/slides'"/>
<t t-set="send_validation_email_message" t-value="'Click here to send a verification email allowing you to participate to the eLearning.'"/>
<t t-set="additional_validated_email_message" t-value="' You may now participate to our eLearning.'"/>
</t>
</div>
</div>
<div class="container o_wslides_home_main">
<div class="row">
<div t-if="is_public_user" class="col-lg-3 order-3 order-lg-2">
@@ -206,6 +214,13 @@
</form>
</div>
</nav>
<div class="o_wprofile_email_validation_container mb16 mt16">
<t t-call="website_profile.email_validation_banner">
<t t-set="redirect_url" t-value="'/slides'"/>
<t t-set="send_validation_email_message" t-value="'Click here to send a verification email allowing you to participate to the eLearning.'"/>
<t t-set="additional_validated_email_message" t-value="' You may now participate to our eLearning.'"/>
</t>
</div>
</div>
<div class="container o_wslides_home_main pb-5">
<div t-if="search_term and not channels" class="alert alert-info mb-5">
@@ -383,5 +398,4 @@
t-att-src="'/profile/avatar/%s?field=image_medium' % user.id"
t-att-alt="user.name"/>
</template>
</data></odoo>
@@ -237,19 +237,21 @@
</t>
</div>
<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">
<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">
<i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"></i>
<span t-esc="slide.dislikes"/>
</span>
<t t-if="slide.channel_id.allow_comment and slide.channel_id.channel_type == 'documentation'">
<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 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 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>
</div>
</div>
</div>
</t>
</div>
</div>
<div class="row mb-5">
@@ -287,8 +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="hash" t-value="message_post_hash"/>
<t t-set="pid" t-value="message_post_pid"/>
<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>
@@ -60,8 +60,8 @@ class WebsiteSlides(WebsiteSlides):
# All Users Page
# ---------------------------------------------------
def _prepare_all_users_values(self, user, position):
result = super(WebsiteSlides, self)._prepare_all_users_values(user, position)
def _prepare_all_users_values(self, user):
result = super(WebsiteSlides, self)._prepare_all_users_values(user)
result.update({
'certification_count': len(self._get_user_certificates(user))
})