diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py index f556dcc514a..6a188f2003a 100644 --- a/addons/gamification/models/res_users.py +++ b/addons/gamification/models/res_users.py @@ -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): diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index f31b59e430f..ed2cb30df2e 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -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//user/'], 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)) diff --git a/addons/website_forum/data/forum_data.xml b/addons/website_forum/data/forum_data.xml index fe5fb2b8ea2..4196804495a 100644 --- a/addons/website_forum/data/forum_data.xml +++ b/addons/website_forum/data/forum_data.xml @@ -129,107 +129,5 @@ offensive - - - Forum: Email Verification - - ${object.company_id.name} Forums validation - ${('<%s>' % (object.env.user.company_id.email or user.email)) | safe} - ${object.email|safe} - - - - -
- - - - - - - - - - - - - - - -
- - - - - - - - -
- Your Forum
- - ${object.company_id.name} Forum validation - -
- ${user.company_id.name} -
-
-
-
- - - - - - - -
-

- Hello ${object.name},

- 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: -

- Thanks for your participation! -

-
-
-
-
- - - - - -
- ${user.company_id.name} - - ${user.company_id.phone} - % if user.company_id.email: - | - ${user.company_id.email} - - % endif - % if user.company_id.website: - | - ${user.company_id.website} - - % endif -
-
-
- - -
- Powered by Odoo -
-
-
-
- diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 68d48b8534f..03b45d6bf17 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -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) diff --git a/addons/website_forum/models/res_users.py b/addons/website_forum/models/res_users.py index 43791f3538c..67326175ac9 100644 --- a/addons/website_forum/models/res_users.py +++ b/addons/website_forum/models/res_users.py @@ -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): diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js index 613163a045a..cfca0be8454 100644 --- a/addons/website_forum/static/src/js/website_forum.js +++ b/addons/website_forum/static/src/js/website_forum.js @@ -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({ }); }, }); + }); diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml index 482a7d9541b..1c4755f7092 100644 --- a/addons/website_forum/views/website_forum.xml +++ b/addons/website_forum/views/website_forum.xml @@ -52,22 +52,12 @@
-
- -
- -
-

Congratulations! Your email has just been validated. You may now participate to our forums.

-
-
+
+ + + + +
diff --git a/addons/website_profile/__manifest__.py b/addons/website_profile/__manifest__.py index 1214d3f03e8..fbb57d126fd 100644 --- a/addons/website_profile/__manifest__.py +++ b/addons/website_profile/__manifest__.py @@ -12,6 +12,7 @@ 'gamification' ], 'data': [ + 'data/profile_data.xml', 'views/gamification_badge_views.xml', 'views/website_profile.xml', 'security/ir.model.access.csv', diff --git a/addons/website_profile/controllers/main.py b/addons/website_profile/controllers/main.py index 4d0e58efbe8..de56318e7c6 100644 --- a/addons/website_profile/controllers/main.py +++ b/addons/website_profile/controllers/main.py @@ -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 diff --git a/addons/website_profile/data/profile_data.xml b/addons/website_profile/data/profile_data.xml new file mode 100644 index 00000000000..5331fa23e8e --- /dev/null +++ b/addons/website_profile/data/profile_data.xml @@ -0,0 +1,106 @@ + + + + + + + Profile: Email Verification + + ${object.company_id.name} Profile validation + ${('<%s>' % (object.env.user.company_id.email or user.email)) | safe} + ${object.email|safe} + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + +
+ + ${object.company_id.name} Profile validation + + + ${user.company_id.name} +
+
+
+
+ + + + + + + +
+

+ Hello ${object.name},

+ 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: +

+ Thanks for your participation! +

+
+
+
+
+ + + + + +
+ ${user.company_id.name} + + ${user.company_id.phone} + % if user.company_id.email: + | + ${user.company_id.email} + + % endif + % if user.company_id.website: + | + ${user.company_id.website} + + % endif +
+
+
+ + +
+ Powered by Odoo +
+
+
+
+
+
diff --git a/addons/website_profile/models/__init__.py b/addons/website_profile/models/__init__.py index c26e78d1255..e5d082ad113 100644 --- a/addons/website_profile/models/__init__.py +++ b/addons/website_profile/models/__init__.py @@ -3,3 +3,4 @@ from . import gamification_badge from . import website +from . import res_users diff --git a/addons/website_profile/models/res_users.py b/addons/website_profile/models/res_users.py new file mode 100644 index 00000000000..5bf49be0a78 --- /dev/null +++ b/addons/website_profile/models/res_users.py @@ -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 diff --git a/addons/website_profile/static/src/js/website_profile.js b/addons/website_profile/static/src/js/website_profile.js new file mode 100644 index 00000000000..a3cc18f53f5 --- /dev/null +++ b/addons/website_profile/static/src/js/website_profile.js @@ -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; + +}); diff --git a/addons/website_profile/static/src/scss/website_profile.scss b/addons/website_profile/static/src/scss/website_profile.scss index 224b46b1c84..a20b1ea2eef 100644 --- a/addons/website_profile/static/src/scss/website_profile.scss +++ b/addons/website_profile/static/src/scss/website_profile.scss @@ -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 { diff --git a/addons/website_profile/views/website_profile.xml b/addons/website_profile/views/website_profile.xml index 2a108ef8906..b115d553e8a 100644 --- a/addons/website_profile/views/website_profile.xml +++ b/addons/website_profile/views/website_profile.xml @@ -4,6 +4,9 @@ + +