diff --git a/addons/website_forum/__init__.py b/addons/website_forum/__init__.py
index bde83af3aea..dc8bc3228a8 100644
--- a/addons/website_forum/__init__.py
+++ b/addons/website_forum/__init__.py
@@ -2,3 +2,4 @@
import controllers
import models
+import tests
diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py
index 08e03d9d6bf..a17dc6bdbc1 100644
--- a/addons/website_forum/controllers/main.py
+++ b/addons/website_forum/controllers/main.py
@@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
-from datetime import datetime
import werkzeug.urls
import werkzeug.wrappers
-import re
import simplejson
from openerp import tools
@@ -13,7 +11,6 @@ from openerp.addons.web.controllers.main import login_redirect
from openerp.addons.web.http import request
from openerp.addons.website.controllers.main import Website as controllers
from openerp.addons.website.models.website import slug
-from openerp.tools.translate import _
controllers = controllers()
@@ -35,12 +32,15 @@ class WebsiteForum(http.Controller):
def _prepare_forum_values(self, forum=None, **kwargs):
user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, context=request.context)
- values = {'user': user,
- 'is_public_user': user.id == request.website.user_id.id,
- 'notifications': self._get_notifications(),
- 'header': kwargs.get('header', dict()),
- 'searches': kwargs.get('searches', dict()),
- }
+ values = {
+ 'user': user,
+ 'is_public_user': user.id == request.website.user_id.id,
+ 'notifications': self._get_notifications(),
+ '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 forum:
values['forum'] = forum
elif kwargs.get('forum_id'):
@@ -48,6 +48,34 @@ class WebsiteForum(http.Controller):
values.update(kwargs)
return values
+ # User and validation
+ # --------------------------------------------------
+
+ @http.route('/forum/send_validation_email', type='json', auth='user', website=True)
+ def send_validation_email(self, forum_id=None, **kwargs):
+ request.registry['res.users'].send_forum_validation_email(request.cr, request.uid, request.uid, forum_id=forum_id, context=request.context)
+ request.session['validation_email_sent'] = True
+ return True
+
+ @http.route('/forum/validate_email', type='http', auth='public', website=True)
+ 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.registry['res.users'].process_forum_validation_token(request.cr, request.uid, token, int(id), email, forum_id=forum_id, context=request.context)
+ if done:
+ request.session['validation_email_done'] = True
+ if forum_id:
+ return request.redirect("/forum/%s" % int(forum_id))
+ return request.redirect('/forum')
+
+ @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
+
# Forum
# --------------------------------------------------
@@ -298,10 +326,12 @@ class WebsiteForum(http.Controller):
cr, uid, context = request.cr, request.uid, request.context
if kwargs.get('comment') and post.forum_id.id == forum.id:
# TDE FIXME: check that post_id is the question or one of its answers
- request.registry['forum.post']._post_comment(
- cr, uid, post,
+ request.registry['forum.post'].message_post(
+ cr, uid, post.id,
body=kwargs.get('comment'),
- context=context)
+ type='comment',
+ subtype='mt_comment',
+ context=dict(context, mail_create_nosubcribe=True))
return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
@http.route('/forum/
+ 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, +
++-- +${object.company_id.name or ''} +${object.company_id.email or ''} +${object.company_id.phone or ''} +]]> + + diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 1bc9b69b37a..a98b3ef63ca 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -1,16 +1,18 @@ # -*- coding: utf-8 -*- from datetime import datetime +import uuid +from werkzeug.exceptions import Forbidden import openerp -from openerp import tools +from openerp import api, tools from openerp import SUPERUSER_ID from openerp.addons.website.models.website import slug +from openerp.exceptions import Warning from openerp.osv import osv, fields from openerp.tools import html2plaintext from openerp.tools.translate import _ -from werkzeug.exceptions import Forbidden class KarmaError(Forbidden): """ Karma-related error, used for forum and posts. """ @@ -23,42 +25,48 @@ class Forum(osv.Model): _description = 'Forums' _inherit = ['mail.thread', 'website.seo.metadata'] + def init(self, cr): + """ Add forum uuid for user email validation. """ + forum_uuids = self.pool['ir.config_parameter'].search(cr, SUPERUSER_ID, [('key', '=', 'website_forum.uuid')]) + if not forum_uuids: + self.pool['ir.config_parameter'].set_param(cr, SUPERUSER_ID, 'website_forum.uuid', str(uuid.uuid4()), ['base.group_system']) + _columns = { 'name': fields.char('Name', required=True, translate=True), 'faq': fields.html('Guidelines'), 'description': fields.html('Description'), # karma generation - 'karma_gen_question_new': fields.integer('Karma earned for new questions'), - 'karma_gen_question_upvote': fields.integer('Karma earned for upvoting a question'), - 'karma_gen_question_downvote': fields.integer('Karma earned for downvoting a question'), - 'karma_gen_answer_upvote': fields.integer('Karma earned for upvoting an answer'), - 'karma_gen_answer_downvote': fields.integer('Karma earned for downvoting an answer'), - 'karma_gen_answer_accept': fields.integer('Karma earned for accepting an anwer'), - 'karma_gen_answer_accepted': fields.integer('Karma earned for having an answer accepted'), - 'karma_gen_answer_flagged': fields.integer('Karma earned for having an answer flagged'), + 'karma_gen_question_new': fields.integer('Asking a question'), + 'karma_gen_question_upvote': fields.integer('Question upvoted'), + 'karma_gen_question_downvote': fields.integer('Question downvoted'), + 'karma_gen_answer_upvote': fields.integer('Answer upvoted'), + 'karma_gen_answer_downvote': fields.integer('Answer downvoted'), + 'karma_gen_answer_accept': fields.integer('Accepting an answer'), + 'karma_gen_answer_accepted': fields.integer('Answer accepted'), + 'karma_gen_answer_flagged': fields.integer('Answer flagged'), # karma-based actions - 'karma_ask': fields.integer('Karma to ask a new question'), - 'karma_answer': fields.integer('Karma to answer a question'), - 'karma_edit_own': fields.integer('Karma to edit its own posts'), - 'karma_edit_all': fields.integer('Karma to edit all posts'), - 'karma_close_own': fields.integer('Karma to close its own posts'), - 'karma_close_all': fields.integer('Karma to close all posts'), - 'karma_unlink_own': fields.integer('Karma to delete its own posts'), - 'karma_unlink_all': fields.integer('Karma to delete all posts'), - 'karma_upvote': fields.integer('Karma to upvote'), - 'karma_downvote': fields.integer('Karma to downvote'), - 'karma_answer_accept_own': fields.integer('Karma to accept an answer on its own questions'), - 'karma_answer_accept_all': fields.integer('Karma to accept an answers to all questions'), - 'karma_editor_link_files': fields.integer('Karma for linking files (Editor)'), - 'karma_editor_clickable_link': fields.integer('Karma for clickable links (Editor)'), - 'karma_comment_own': fields.integer('Karma to comment its own posts'), - 'karma_comment_all': fields.integer('Karma to comment all posts'), - 'karma_comment_convert_own': fields.integer('Karma to convert its own answers to comments and vice versa'), - 'karma_comment_convert_all': fields.integer('Karma to convert all answers to answers and vice versa'), - 'karma_comment_unlink_own': fields.integer('Karma to unlink its own comments'), - 'karma_comment_unlink_all': fields.integer('Karma to unlinnk all comments'), - 'karma_retag': fields.integer('Karma to change question tags'), - 'karma_flag': fields.integer('Karma to flag a post as offensive'), + 'karma_ask': fields.integer('Ask a question'), + 'karma_answer': fields.integer('Answer a question'), + 'karma_edit_own': fields.integer('Edit its own posts'), + 'karma_edit_all': fields.integer('Edit all posts'), + 'karma_close_own': fields.integer('Close its own posts'), + 'karma_close_all': fields.integer('Close all posts'), + 'karma_unlink_own': fields.integer('Delete its own posts'), + 'karma_unlink_all': fields.integer('Delete all posts'), + 'karma_upvote': fields.integer('Upvote'), + 'karma_downvote': fields.integer('Downvote'), + 'karma_answer_accept_own': fields.integer('Accept an answer on its own questions'), + 'karma_answer_accept_all': fields.integer('Accept an answer to all questions'), + 'karma_editor_link_files': fields.integer('Linking files (Editor)'), + 'karma_editor_clickable_link': fields.integer('Clickable links (Editor)'), + 'karma_comment_own': fields.integer('Comment its own posts'), + 'karma_comment_all': fields.integer('Comment all posts'), + 'karma_comment_convert_own': fields.integer('Convert its own answers to comments and vice versa'), + 'karma_comment_convert_all': fields.integer('Convert all answers to comments and vice versa'), + 'karma_comment_unlink_own': fields.integer('Unlink its own comments'), + 'karma_comment_unlink_all': fields.integer('Unlink all comments'), + 'karma_retag': fields.integer('Change question tags'), + 'karma_flag': fields.integer('Flag a post as offensive'), } def _get_default_faq(self, cr, uid, context=None): @@ -70,7 +78,7 @@ class Forum(osv.Model): _defaults = { 'description': 'This community is for professionals and enthusiasts of our products and services.', 'faq': _get_default_faq, - 'karma_gen_question_new': 2, + 'karma_gen_question_new': 0, # set to null for anti spam protection 'karma_gen_question_upvote': 5, 'karma_gen_question_downvote': -2, 'karma_gen_answer_upvote': 10, @@ -78,8 +86,8 @@ class Forum(osv.Model): 'karma_gen_answer_accept': 2, 'karma_gen_answer_accepted': 15, 'karma_gen_answer_flagged': -100, - 'karma_ask': 0, - 'karma_answer': 0, + 'karma_ask': 3, # set to not null for anti spam protection + 'karma_answer': 3, # set to not null for anti spam protection 'karma_edit_own': 1, 'karma_edit_all': 300, 'karma_close_own': 100, @@ -92,8 +100,8 @@ class Forum(osv.Model): 'karma_answer_accept_all': 500, 'karma_editor_link_files': 20, 'karma_editor_clickable_link': 20, - 'karma_comment_own': 1, - 'karma_comment_all': 1, + 'karma_comment_own': 3, + 'karma_comment_all': 5, 'karma_comment_convert_own': 50, 'karma_comment_convert_all': 500, 'karma_comment_unlink_own': 50, @@ -393,13 +401,6 @@ class Post(osv.Model): return super(Post, self).unlink(cr, uid, ids, context=context) def vote(self, cr, uid, ids, upvote=True, context=None): - posts = self.browse(cr, uid, ids, context=context) - - if upvote and any(not post.can_upvote for post in posts): - raise KarmaError('Not enough karma to upvote.') - elif not upvote and any(not post.can_downvote for post in posts): - raise KarmaError('Not enough karma to downvote.') - Vote = self.pool['forum.post.vote'] vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context) new_vote = '1' if upvote else '-1' @@ -509,15 +510,18 @@ class Post(osv.Model): res_id = post.parent_id and "%s#answer-%s" % (post.parent_id.id, post.id) or post.id return "/forum/%s/question/%s" % (post.forum_id.id, res_id) - def _post_comment(self, cr, uid, post, body, context=None): - context = dict(context or {}, mail_create_nosubcribe=True) - if not post.can_comment: - raise KarmaError('Not enough karma to comment') - return self.message_post(cr, uid, post.id, - body=body, - type='comment', - subtype='mt_comment', - context=context) + @api.cr_uid_ids_context + def message_post(self, cr, uid, thread_id, type='notification', subtype=None, context=None, **kwargs): + if thread_id and type == 'comment': # user comments have a restriction on karma + if isinstance(thread_id, (list, tuple)): + post_id = thread_id[0] + else: + post_id = thread_id + post = self.browse(cr, uid, post_id, context=context) + if not post.can_comment: + raise KarmaError('Not enough karma to comment') + return super(Post, self).message_post(cr, uid, thread_id, type=type, subtype=subtype, context=context, **kwargs) + class PostReason(osv.Model): _name = "forum.post.reason" @@ -557,6 +561,17 @@ class Vote(osv.Model): def create(self, cr, uid, vals, context=None): vote_id = super(Vote, self).create(cr, uid, vals, context=context) vote = self.browse(cr, uid, vote_id, context=context) + + # own post check + if vote.user_id.id == vote.post_id.create_uid.id: + raise Warning('Not allowed to vote for its own post') + # karma check + if vote.vote == '1' and not vote.post_id.can_upvote: + raise KarmaError('Not enough karma to upvote.') + elif vote.vote == '-1' and not vote.post_id.can_downvote: + raise KarmaError('Not enough karma to downvote.') + + # karma update if vote.post_id.parent_id: karma_value = self._get_karma_value('0', vote.vote, vote.forum_id.karma_gen_answer_upvote, vote.forum_id.karma_gen_answer_downvote) else: @@ -567,6 +582,16 @@ class Vote(osv.Model): def write(self, cr, uid, ids, values, context=None): if 'vote' in values: for vote in self.browse(cr, uid, ids, context=context): + # own post check + if vote.user_id.id == vote.post_id.create_uid.id: + raise Warning('Not allowed to vote for its own post') + # karma check + if (values['vote'] == '1' or vote.vote == '-1' and values['vote'] == '0') and not vote.post_id.can_upvote: + raise KarmaError('Not enough karma to upvote.') + elif (values['vote'] == '-1' or vote.vote == '1' and values['vote'] == '0') and not vote.post_id.can_downvote: + raise KarmaError('Not enough karma to downvote.') + + # karma update if vote.post_id.parent_id: karma_value = self._get_karma_value(vote.vote, values['vote'], vote.forum_id.karma_gen_answer_upvote, vote.forum_id.karma_gen_answer_downvote) else: diff --git a/addons/website_forum/models/res_users.py b/addons/website_forum/models/res_users.py index fa59b8155d7..651be986c89 100644 --- a/addons/website_forum/models/res_users.py +++ b/addons/website_forum/models/res_users.py @@ -1,14 +1,22 @@ # -*- coding: utf-8 -*- +from datetime import datetime +from urllib import urlencode + +import hashlib + +from openerp import SUPERUSER_ID from openerp.osv import osv, fields + class Users(osv.Model): _inherit = 'res.users' def __init__(self, pool, cr): - init_res = super(Users, self).__init__(pool, cr) - self.SELF_WRITEABLE_FIELDS = list(set( - self.SELF_WRITEABLE_FIELDS + \ + init_res = super(Users, self).__init__(pool, cr) + self.SELF_WRITEABLE_FIELDS = list( + set( + self.SELF_WRITEABLE_FIELDS + ['country_id', 'city', 'website', 'website_description', 'website_published'])) return init_res @@ -37,6 +45,50 @@ class Users(osv.Model): 'karma': 0, } + def _generate_forum_token(self, cr, uid, 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.pool.get('ir.config_parameter').get_param(cr, SUPERUSER_ID, 'website_forum.uuid') + return hashlib.sha256('%s-%s-%s-%s' % ( + datetime.now().replace(hour=0, minute=0, second=0, microsecond=0), + forum_uuid, + user_id, + email)).hexdigest() + + def send_forum_validation_email(self, cr, uid, user_id, forum_id=None, context=None): + user = self.pool['res.users'].browse(cr, uid, user_id, context=context) + token = self._generate_forum_token(cr, uid, user_id, user.email) + activation_template_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'website_forum.validation_email') + if activation_template_id: + params = { + 'token': token, + 'id': user_id, + 'email': user.email} + if forum_id: + params['forum_id'] = forum_id + base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') + token_url = base_url + '/forum/validate_email?%s' % urlencode(params) + tpl_ctx = dict(context, token_url=token_url) + self.pool['email.template'].send_mail(cr, SUPERUSER_ID, activation_template_id, user_id, force_send=True, context=tpl_ctx) + return True + + def process_forum_validation_token(self, cr, uid, token, user_id, email, forum_id=None, context=None): + validation_token = self.pool['res.users']._generate_forum_token(cr, uid, user_id, email) + user = self.pool['res.users'].browse(cr, SUPERUSER_ID, user_id, context=context) + if token == validation_token and user.karma == 0: + karma = 3 + if not forum_id: + forum_ids = self.pool['forum.forum'].search(cr, uid, [], limit=1, context=context) + if forum_ids: + forum_id = forum_ids[0] + if forum_id: + forum = self.pool['forum.forum'].browse(cr, uid, forum_id, context=context) + # karma gained: karma to ask a question and have 2 downvotes + karma = forum.karma_ask + (-2 * forum.karma_gen_question_downvote) + return user.write({'karma': karma}) + return False + def add_karma(self, cr, uid, ids, karma, context=None): for user in self.browse(cr, uid, ids, context=context): self.write(cr, uid, [user.id], {'karma': user.karma + karma}, context=context) diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js index 9a00eb7de23..75cf37c05a7 100644 --- a/addons/website_forum/static/src/js/website_forum.js +++ b/addons/website_forum/static/src/js/website_forum.js @@ -6,8 +6,8 @@ $(document).ready(function () { ev.preventDefault(); var $warning = $('
+ It appears your email has not been verified. + Click here to send a verification email allowing you to participate to the forum. +
+Congratulations! Your email has just been validated. You may now participate to our forums.
+