[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:
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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">${('<%s>' % (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&utm_medium=forum" style="color: #875A7B;">Odoo</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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">&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">&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">
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
'gamification'
|
||||
],
|
||||
'data': [
|
||||
'data/profile_data.xml',
|
||||
'views/gamification_badge_views.xml',
|
||||
'views/website_profile.xml',
|
||||
'security/ir.model.access.csv',
|
||||
|
||||
@@ -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">${('<%s>' % (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&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">&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">&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>
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import common
|
||||
from . import test_security
|
||||
from . import test_statistics
|
||||
from . import test_karma
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user