[IMP] [TEST] website_forum: security fixes + tests

- fixed voting, karma check could be avoided
- fixed posting comments, now correctly checking karma (not for
notifications)
- fixed bootstraping of users, now not allowed to ask questions by default;
added validation email that gives the first karma points required to
participate
- added tests
This commit is contained in:
ssh-odoo 2014-09-19 12:55:19 +05:30 committed by Thibault Delavallée
parent bf3251fd0a
commit ef8099424d
11 changed files with 556 additions and 97 deletions

View File

@ -2,3 +2,4 @@
import controllers import controllers
import models import models
import tests

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import datetime
import werkzeug.urls import werkzeug.urls
import werkzeug.wrappers import werkzeug.wrappers
import re
import simplejson import simplejson
from openerp import tools 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.web.http import request
from openerp.addons.website.controllers.main import Website as controllers from openerp.addons.website.controllers.main import Website as controllers
from openerp.addons.website.models.website import slug from openerp.addons.website.models.website import slug
from openerp.tools.translate import _
controllers = controllers() controllers = controllers()
@ -35,12 +32,15 @@ class WebsiteForum(http.Controller):
def _prepare_forum_values(self, forum=None, **kwargs): def _prepare_forum_values(self, forum=None, **kwargs):
user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, context=request.context) user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, context=request.context)
values = {'user': user, values = {
'is_public_user': user.id == request.website.user_id.id, 'user': user,
'notifications': self._get_notifications(), 'is_public_user': user.id == request.website.user_id.id,
'header': kwargs.get('header', dict()), 'notifications': self._get_notifications(),
'searches': kwargs.get('searches', dict()), '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: if forum:
values['forum'] = forum values['forum'] = forum
elif kwargs.get('forum_id'): elif kwargs.get('forum_id'):
@ -48,6 +48,34 @@ class WebsiteForum(http.Controller):
values.update(kwargs) values.update(kwargs)
return values 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 # Forum
# -------------------------------------------------- # --------------------------------------------------
@ -298,10 +326,12 @@ class WebsiteForum(http.Controller):
cr, uid, context = request.cr, request.uid, request.context cr, uid, context = request.cr, request.uid, request.context
if kwargs.get('comment') and post.forum_id.id == forum.id: 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 # TDE FIXME: check that post_id is the question or one of its answers
request.registry['forum.post']._post_comment( request.registry['forum.post'].message_post(
cr, uid, post, cr, uid, post.id,
body=kwargs.get('comment'), 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))) return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/toggle_correct', type='json', auth="public", website=True) @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/toggle_correct', type='json', auth="public", website=True)

View File

@ -100,5 +100,36 @@
<field name="name">too localized</field> <field name="name">too localized</field>
</record> </record>
<!-- Email template for email validation (for karma purpose) -->
<record id="validation_email" model="email.template">
<field name="name">Email Verification</field>
<field name="model_id" ref="base.model_res_users"/>
<field name="email_from"><![CDATA[${object.company_id.name} <${(object.company_id.email or user.email)|safe}>]]></field>
<field name="email_to">${object.email|safe}</field>
<field name="subject"><![CDATA[${object.company_id.name} Forums validation]]></field>
<field name="body_html"><![CDATA[
<p>
Hello ${object.name},
</p>
<p>
You have been invited to validate your email in order to get access to "${object.company_id.name}" Q/A Forums.
</p>
<p>
To validate your email, please click on the following link:
</p>
<ul>
<li><a href="${ctx.get('token_url')}">Validate my account for "${object.company_id.name}" Q/A Forums</a></li>
</ul>
<p>
Thanks,
</p>
<pre>
--
${object.company_id.name or ''}
${object.company_id.email or ''}
${object.company_id.phone or ''}
</pre>]]></field>
</record>
</data> </data>
</openerp> </openerp>

View File

@ -1,16 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import datetime from datetime import datetime
import uuid
from werkzeug.exceptions import Forbidden
import openerp import openerp
from openerp import tools from openerp import api, tools
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
from openerp.addons.website.models.website import slug from openerp.addons.website.models.website import slug
from openerp.exceptions import Warning
from openerp.osv import osv, fields from openerp.osv import osv, fields
from openerp.tools import html2plaintext from openerp.tools import html2plaintext
from openerp.tools.translate import _ from openerp.tools.translate import _
from werkzeug.exceptions import Forbidden
class KarmaError(Forbidden): class KarmaError(Forbidden):
""" Karma-related error, used for forum and posts. """ """ Karma-related error, used for forum and posts. """
@ -23,42 +25,48 @@ class Forum(osv.Model):
_description = 'Forums' _description = 'Forums'
_inherit = ['mail.thread', 'website.seo.metadata'] _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 = { _columns = {
'name': fields.char('Name', required=True, translate=True), 'name': fields.char('Name', required=True, translate=True),
'faq': fields.html('Guidelines'), 'faq': fields.html('Guidelines'),
'description': fields.html('Description'), 'description': fields.html('Description'),
# karma generation # karma generation
'karma_gen_question_new': fields.integer('Karma earned for new questions'), 'karma_gen_question_new': fields.integer('Asking a question'),
'karma_gen_question_upvote': fields.integer('Karma earned for upvoting a question'), 'karma_gen_question_upvote': fields.integer('Question upvoted'),
'karma_gen_question_downvote': fields.integer('Karma earned for downvoting a question'), 'karma_gen_question_downvote': fields.integer('Question downvoted'),
'karma_gen_answer_upvote': fields.integer('Karma earned for upvoting an answer'), 'karma_gen_answer_upvote': fields.integer('Answer upvoted'),
'karma_gen_answer_downvote': fields.integer('Karma earned for downvoting an answer'), 'karma_gen_answer_downvote': fields.integer('Answer downvoted'),
'karma_gen_answer_accept': fields.integer('Karma earned for accepting an anwer'), 'karma_gen_answer_accept': fields.integer('Accepting an answer'),
'karma_gen_answer_accepted': fields.integer('Karma earned for having an answer accepted'), 'karma_gen_answer_accepted': fields.integer('Answer accepted'),
'karma_gen_answer_flagged': fields.integer('Karma earned for having an answer flagged'), 'karma_gen_answer_flagged': fields.integer('Answer flagged'),
# karma-based actions # karma-based actions
'karma_ask': fields.integer('Karma to ask a new question'), 'karma_ask': fields.integer('Ask a question'),
'karma_answer': fields.integer('Karma to answer a question'), 'karma_answer': fields.integer('Answer a question'),
'karma_edit_own': fields.integer('Karma to edit its own posts'), 'karma_edit_own': fields.integer('Edit its own posts'),
'karma_edit_all': fields.integer('Karma to edit all posts'), 'karma_edit_all': fields.integer('Edit all posts'),
'karma_close_own': fields.integer('Karma to close its own posts'), 'karma_close_own': fields.integer('Close its own posts'),
'karma_close_all': fields.integer('Karma to close all posts'), 'karma_close_all': fields.integer('Close all posts'),
'karma_unlink_own': fields.integer('Karma to delete its own posts'), 'karma_unlink_own': fields.integer('Delete its own posts'),
'karma_unlink_all': fields.integer('Karma to delete all posts'), 'karma_unlink_all': fields.integer('Delete all posts'),
'karma_upvote': fields.integer('Karma to upvote'), 'karma_upvote': fields.integer('Upvote'),
'karma_downvote': fields.integer('Karma to downvote'), 'karma_downvote': fields.integer('Downvote'),
'karma_answer_accept_own': fields.integer('Karma to accept an answer on its own questions'), 'karma_answer_accept_own': fields.integer('Accept an answer on its own questions'),
'karma_answer_accept_all': fields.integer('Karma to accept an answers to all questions'), 'karma_answer_accept_all': fields.integer('Accept an answer to all questions'),
'karma_editor_link_files': fields.integer('Karma for linking files (Editor)'), 'karma_editor_link_files': fields.integer('Linking files (Editor)'),
'karma_editor_clickable_link': fields.integer('Karma for clickable links (Editor)'), 'karma_editor_clickable_link': fields.integer('Clickable links (Editor)'),
'karma_comment_own': fields.integer('Karma to comment its own posts'), 'karma_comment_own': fields.integer('Comment its own posts'),
'karma_comment_all': fields.integer('Karma to comment all posts'), 'karma_comment_all': fields.integer('Comment all posts'),
'karma_comment_convert_own': fields.integer('Karma to convert its own answers to comments and vice versa'), 'karma_comment_convert_own': fields.integer('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_convert_all': fields.integer('Convert all answers to comments and vice versa'),
'karma_comment_unlink_own': fields.integer('Karma to unlink its own comments'), 'karma_comment_unlink_own': fields.integer('Unlink its own comments'),
'karma_comment_unlink_all': fields.integer('Karma to unlinnk all comments'), 'karma_comment_unlink_all': fields.integer('Unlink all comments'),
'karma_retag': fields.integer('Karma to change question tags'), 'karma_retag': fields.integer('Change question tags'),
'karma_flag': fields.integer('Karma to flag a post as offensive'), 'karma_flag': fields.integer('Flag a post as offensive'),
} }
def _get_default_faq(self, cr, uid, context=None): def _get_default_faq(self, cr, uid, context=None):
@ -70,7 +78,7 @@ class Forum(osv.Model):
_defaults = { _defaults = {
'description': 'This community is for professionals and enthusiasts of our products and services.', 'description': 'This community is for professionals and enthusiasts of our products and services.',
'faq': _get_default_faq, '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_upvote': 5,
'karma_gen_question_downvote': -2, 'karma_gen_question_downvote': -2,
'karma_gen_answer_upvote': 10, 'karma_gen_answer_upvote': 10,
@ -78,8 +86,8 @@ class Forum(osv.Model):
'karma_gen_answer_accept': 2, 'karma_gen_answer_accept': 2,
'karma_gen_answer_accepted': 15, 'karma_gen_answer_accepted': 15,
'karma_gen_answer_flagged': -100, 'karma_gen_answer_flagged': -100,
'karma_ask': 0, 'karma_ask': 3, # set to not null for anti spam protection
'karma_answer': 0, 'karma_answer': 3, # set to not null for anti spam protection
'karma_edit_own': 1, 'karma_edit_own': 1,
'karma_edit_all': 300, 'karma_edit_all': 300,
'karma_close_own': 100, 'karma_close_own': 100,
@ -92,8 +100,8 @@ class Forum(osv.Model):
'karma_answer_accept_all': 500, 'karma_answer_accept_all': 500,
'karma_editor_link_files': 20, 'karma_editor_link_files': 20,
'karma_editor_clickable_link': 20, 'karma_editor_clickable_link': 20,
'karma_comment_own': 1, 'karma_comment_own': 3,
'karma_comment_all': 1, 'karma_comment_all': 5,
'karma_comment_convert_own': 50, 'karma_comment_convert_own': 50,
'karma_comment_convert_all': 500, 'karma_comment_convert_all': 500,
'karma_comment_unlink_own': 50, 'karma_comment_unlink_own': 50,
@ -393,13 +401,6 @@ class Post(osv.Model):
return super(Post, self).unlink(cr, uid, ids, context=context) return super(Post, self).unlink(cr, uid, ids, context=context)
def vote(self, cr, uid, ids, upvote=True, context=None): 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 = self.pool['forum.post.vote']
vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context) vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context)
new_vote = '1' if upvote else '-1' 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 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) return "/forum/%s/question/%s" % (post.forum_id.id, res_id)
def _post_comment(self, cr, uid, post, body, context=None): @api.cr_uid_ids_context
context = dict(context or {}, mail_create_nosubcribe=True) def message_post(self, cr, uid, thread_id, type='notification', subtype=None, context=None, **kwargs):
if not post.can_comment: if thread_id and type == 'comment': # user comments have a restriction on karma
raise KarmaError('Not enough karma to comment') if isinstance(thread_id, (list, tuple)):
return self.message_post(cr, uid, post.id, post_id = thread_id[0]
body=body, else:
type='comment', post_id = thread_id
subtype='mt_comment', post = self.browse(cr, uid, post_id, context=context)
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): class PostReason(osv.Model):
_name = "forum.post.reason" _name = "forum.post.reason"
@ -557,6 +561,17 @@ class Vote(osv.Model):
def create(self, cr, uid, vals, context=None): def create(self, cr, uid, vals, context=None):
vote_id = super(Vote, self).create(cr, uid, vals, context=context) vote_id = super(Vote, self).create(cr, uid, vals, context=context)
vote = self.browse(cr, uid, vote_id, 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: 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) karma_value = self._get_karma_value('0', vote.vote, vote.forum_id.karma_gen_answer_upvote, vote.forum_id.karma_gen_answer_downvote)
else: else:
@ -567,6 +582,16 @@ class Vote(osv.Model):
def write(self, cr, uid, ids, values, context=None): def write(self, cr, uid, ids, values, context=None):
if 'vote' in values: if 'vote' in values:
for vote in self.browse(cr, uid, ids, context=context): 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: 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) 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: else:

View File

@ -1,14 +1,22 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import datetime
from urllib import urlencode
import hashlib
from openerp import SUPERUSER_ID
from openerp.osv import osv, fields from openerp.osv import osv, fields
class Users(osv.Model): class Users(osv.Model):
_inherit = 'res.users' _inherit = 'res.users'
def __init__(self, pool, cr): def __init__(self, pool, cr):
init_res = super(Users, self).__init__(pool, cr) init_res = super(Users, self).__init__(pool, cr)
self.SELF_WRITEABLE_FIELDS = list(set( self.SELF_WRITEABLE_FIELDS = list(
self.SELF_WRITEABLE_FIELDS + \ set(
self.SELF_WRITEABLE_FIELDS +
['country_id', 'city', 'website', 'website_description', 'website_published'])) ['country_id', 'city', 'website', 'website_description', 'website_published']))
return init_res return init_res
@ -37,6 +45,50 @@ class Users(osv.Model):
'karma': 0, '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): def add_karma(self, cr, uid, ids, karma, context=None):
for user in self.browse(cr, uid, ids, context=context): for user in self.browse(cr, uid, ids, context=context):
self.write(cr, uid, [user.id], {'karma': user.karma + karma}, context=context) self.write(cr, uid, [user.id], {'karma': user.karma + karma}, context=context)

View File

@ -6,8 +6,8 @@ $(document).ready(function () {
ev.preventDefault(); ev.preventDefault();
var $warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="karma_alert">'+ var $warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="karma_alert">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+ '<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
karma + ' karma is required to perform this action. You can earn karma by answering questions or having '+ karma + ' karma is required to perform this action. You can earn karma by having '+
'your answers upvoted by the community.</div>'); 'your answers upvoted by the community.</div>');
var vote_alert = $(ev.currentTarget).parent().find("#vote_alert"); var vote_alert = $(ev.currentTarget).parent().find("#vote_alert");
if (vote_alert.length == 0) { if (vote_alert.length == 0) {
$(ev.currentTarget).parent().append($warning); $(ev.currentTarget).parent().append($warning);
@ -50,7 +50,6 @@ $(document).ready(function () {
} }
} }
}); });
return true;
}); });
$('.accept_answer').not('.karma_required').on('click', function (ev) { $('.accept_answer').not('.karma_required').on('click', function (ev) {
@ -76,7 +75,6 @@ $(document).ready(function () {
} }
} }
}); });
return true;
}); });
$('.favourite_question').on('click', function (ev) { $('.favourite_question').on('click', function (ev) {
@ -89,7 +87,6 @@ $(document).ready(function () {
$link.removeClass("forum_favourite_question") $link.removeClass("forum_favourite_question")
} }
}); });
return true;
}); });
$('.comment_delete').on('click', function (ev) { $('.comment_delete').on('click', function (ev) {
@ -98,15 +95,29 @@ $(document).ready(function () {
openerp.jsonRpc($link.data('href'), 'call', {}).then(function (data) { openerp.jsonRpc($link.data('href'), 'call', {}).then(function (data) {
$link.parents('.comment').first().remove(); $link.parents('.comment').first().remove();
}); });
return true;
}); });
$('.notification_close').on('click', function (ev) { $('.notification_close').on('click', function (ev) {
ev.preventDefault(); ev.preventDefault();
var $link = $(ev.currentTarget); var $link = $(ev.currentTarget);
openerp.jsonRpc("/forum/notification_read", 'call', { openerp.jsonRpc("/forum/notification_read", 'call', {
'notification_id': $link.attr("id")}) 'notification_id': $link.attr("id")});
return true; });
$('.send_validation_email').on('click', function (ev) {
ev.preventDefault();
var $link = $(ev.currentTarget);
openerp.jsonRpc("/forum/send_validation_email", 'call', {
'forum_id': $link.attr('forum-id'),
}).then(function (data) {
if (data) {
$('button.validation_email_close').click();
}
});
});
$('.validated_email_close').on('click', function (ev) {
openerp.jsonRpc("/forum/validate_email/close", 'call', {});
}); });
if($('input.load_tags').length){ if($('input.load_tags').length){

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
import common
import test_forum

View File

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
from openerp.tests import common
KARMA = {
'ask': 5, 'ans': 10,
'com_own': 5, 'com_all': 10,
'com_conv_all': 50,
'upv': 5, 'dwv': 10,
'edit_own': 10, 'edit_all': 20,
'close_own': 10, 'close_all': 20,
'unlink_own': 10, 'unlink_all': 20,
'gen_que_new': 1, 'gen_que_upv': 5, 'gen_que_dwv': -10,
'gen_ans_upv': 10, 'gen_ans_dwv': -20,
}
class TestForumCommon(common.TransactionCase):
def setUp(self):
super(TestForumCommon, self).setUp()
Forum = self.env['forum.forum']
Post = self.env['forum.post']
# Test users
TestUsersEnv = self.env['res.users'].with_context({'no_reset_password': True})
group_employee_id = self.ref('base.group_user')
group_portal_id = self.ref('base.group_portal')
group_public_id = self.ref('base.group_public')
self.user_employee = TestUsersEnv.create({
'name': 'Armande Employee',
'login': 'Armande',
'alias_name': 'armande',
'email': 'armande.employee@example.com',
'karma': 0,
'groups_id': [(6, 0, [group_employee_id])]
})
self.user_portal = TestUsersEnv.create({
'name': 'Beatrice Portal',
'login': 'Beatrice',
'alias_name': 'beatrice',
'email': 'beatrice.employee@example.com',
'karma': 0,
'groups_id': [(6, 0, [group_portal_id])]
})
self.user_public = TestUsersEnv.create({
'name': 'Cedric Public',
'login': 'Cedric',
'alias_name': 'cedric',
'email': 'cedric.employee@example.com',
'karma': 0,
'groups_id': [(6, 0, [group_public_id])]
})
# Test forum
self.forum = Forum.create({
'name': 'TestForum',
'karma_ask': KARMA['ask'],
'karma_answer': KARMA['ans'],
'karma_comment_own': KARMA['com_own'],
'karma_comment_all': KARMA['com_all'],
'karma_answer_accept_own': 9999,
'karma_answer_accept_all': 9999,
'karma_upvote': KARMA['upv'],
'karma_downvote': KARMA['dwv'],
'karma_edit_own': KARMA['edit_own'],
'karma_edit_all': KARMA['edit_all'],
'karma_close_own': KARMA['close_own'],
'karma_close_all': KARMA['close_all'],
'karma_unlink_own': KARMA['unlink_own'],
'karma_unlink_all': KARMA['unlink_all'],
'karma_comment_convert_all': KARMA['com_conv_all'],
'karma_gen_question_new': KARMA['gen_que_new'],
'karma_gen_question_upvote': KARMA['gen_que_upv'],
'karma_gen_question_downvote': KARMA['gen_que_dwv'],
'karma_gen_answer_upvote': KARMA['gen_ans_upv'],
'karma_gen_answer_downvote': KARMA['gen_ans_dwv'],
'karma_gen_answer_accept': 9999,
'karma_gen_answer_accepted': 9999,
})
self.post = Post.create({
'name': 'TestQuestion',
'content': 'I am not a bird.',
'forum_id': self.forum.id,
'tag_ids': [(0, 0, {'name': 'Tag0', 'forum_id': self.forum.id})]
})
self.answer = Post.create({
'name': 'TestAnswer',
'content': 'I am an anteater.',
'forum_id': self.forum.id,
'parent_id': self.post.id,
})

View File

@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
from openerp.addons.website_forum.tests.common import KARMA, TestForumCommon
from openerp.addons.website_forum.models.forum import KarmaError
from openerp.exceptions import Warning, AccessError
from openerp.tools import mute_logger
class TestForum(TestForumCommon):
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.models')
def test_ask(self):
Post = self.env['forum.post']
# Public user asks a question: not allowed
with self.assertRaises(AccessError):
Post.sudo(self.user_public).create({
'name': " Question ?",
'forum_id': self.forum.id,
})
# Portal user asks a question with tags: not allowed, unsufficient karma
with self.assertRaises(KarmaError):
Post.sudo(self.user_portal).create({
'name': " Q_0",
'forum_id': self.forum.id,
'tag_ids': [(0, 0, {'name': 'Tag0', 'forum_id': self.forum.id})]
})
# Portal user asks a question with tags: ok if enough karma
self.user_portal.karma = KARMA['ask']
Post.sudo(self.user_portal).create({
'name': " Q0",
'forum_id': self.forum.id,
'tag_ids': [(0, 0, {'name': 'Tag0', 'forum_id': self.forum.id})]
})
self.assertEqual(self.user_portal.karma, KARMA['ask'] + KARMA['gen_que_new'], 'website_forum: wrong karma generation when asking question')
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.models')
def test_answer(self):
Post = self.env['forum.post']
# Answers its own question: not allowed, unsufficient karma
with self.assertRaises(KarmaError):
Post.sudo(self.user_employee).create({
'name': " A0",
'forum_id': self.forum.id,
'parent_id': self.post.id,
})
# Answers on question: ok if enough karma
self.user_employee.karma = KARMA['ans']
Post.sudo(self.user_employee).create({
'name': " A0",
'forum_id': self.forum.id,
'parent_id': self.post.id,
})
self.assertEqual(self.user_employee.karma, KARMA['ans'], 'website_forum: wrong karma generation when answering question')
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.models')
def test_vote_crash(self):
Post = self.env['forum.post']
self.user_employee.karma = KARMA['ans']
emp_answer = Post.sudo(self.user_employee).create({
'name': 'TestAnswer',
'forum_id': self.forum.id,
'parent_id': self.post.id})
# upvote its own post
with self.assertRaises(Warning):
emp_answer.vote(upvote=True)
# not enough karma
with self.assertRaises(KarmaError):
self.post.sudo(self.user_portal).vote(upvote=True)
def test_vote(self):
self.post.create_uid.karma = KARMA['ask']
self.user_portal.karma = KARMA['upv']
self.post.sudo(self.user_portal).vote(upvote=True)
self.assertEqual(self.post.create_uid.karma, KARMA['ask'] + KARMA['gen_que_upv'], 'website_forum: wrong karma generation of upvoted question author')
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.models')
def test_downvote_crash(self):
Post = self.env['forum.post']
self.user_employee.karma = KARMA['ans']
emp_answer = Post.sudo(self.user_employee).create({
'name': 'TestAnswer',
'forum_id': self.forum.id,
'parent_id': self.post.id})
# downvote its own post
with self.assertRaises(Warning):
emp_answer.vote(upvote=False)
# not enough karma
with self.assertRaises(KarmaError):
self.post.sudo(self.user_portal).vote(upvote=False)
def test_downvote(self):
self.post.create_uid.karma = 50
self.user_portal.karma = KARMA['dwv']
self.post.sudo(self.user_portal).vote(upvote=False)
self.assertEqual(self.post.create_uid.karma, 50 + KARMA['gen_que_dwv'], 'website_forum: wrong karma generation of downvoted question author')
def test_comment_crash(self):
with self.assertRaises(KarmaError):
self.post.sudo(self.user_portal).message_post(body='Should crash', type='comment')
def test_comment(self):
self.post.sudo(self.user_employee).message_post(body='Test0', type='notification')
self.user_employee.karma = KARMA['com_all']
self.post.sudo(self.user_employee).message_post(body='Test1', type='comment')
self.assertEqual(len(self.post.message_ids), 4, 'website_forum: wrong behavior of message_post')
def test_convert_answer_to_comment_crash(self):
Post = self.env['forum.post']
# converting a question does nothing
msg_ids = self.post.sudo(self.user_portal).convert_answer_to_comment()
self.assertEqual(msg_ids[0], False, 'website_forum: question to comment conversion failed')
self.assertEqual(Post.search([('name', '=', 'TestQuestion')])[0].forum_id.name, 'TestForum', 'website_forum: question to comment conversion failed')
with self.assertRaises(KarmaError):
self.answer.sudo(self.user_portal).convert_answer_to_comment()
def test_convert_answer_to_comment(self):
self.user_portal.karma = KARMA['com_conv_all']
post_author = self.answer.create_uid.partner_id
msg_ids = self.answer.sudo(self.user_portal).convert_answer_to_comment()
self.assertEqual(len(msg_ids), 1, 'website_forum: wrong answer to comment conversion')
msg = self.env['mail.message'].browse(msg_ids[0])
self.assertEqual(msg.author_id, post_author, 'website_forum: wrong answer to comment conversion')
self.assertIn('I am an anteater', msg.body, 'website_forum: wrong answer to comment conversion')
def test_edit_post_crash(self):
with self.assertRaises(KarmaError):
self.post.sudo(self.user_portal).write({'name': 'I am not your father.'})
def test_edit_post(self):
self.post.create_uid.karma = KARMA['edit_own']
self.post.write({'name': 'Actually I am your dog.'})
self.user_portal.karma = KARMA['edit_all']
self.post.sudo(self.user_portal).write({'name': 'Actually I am your cat.'})
def test_close_post_crash(self):
with self.assertRaises(KarmaError):
self.post.sudo(self.user_portal).close(None)
def test_close_post_own(self):
self.post.create_uid.karma = KARMA['close_own']
self.post.close(None)
def test_close_post_all(self):
self.user_portal.karma = KARMA['close_all']
self.post.sudo(self.user_portal).close(None)
def test_deactivate_post_crash(self):
with self.assertRaises(KarmaError):
self.post.sudo(self.user_portal).write({'active': False})
def test_deactivate_post_own(self):
self.post.create_uid.karma = KARMA['unlink_own']
self.post.write({'active': False})
def test_deactivate_post_all(self):
self.user_portal.karma = KARMA['unlink_all']
self.post.sudo(self.user_portal).write({'active': False})
def test_unlink_post_crash(self):
with self.assertRaises(KarmaError):
self.post.sudo(self.user_portal).unlink()
def test_unlink_post_own(self):
self.post.create_uid.karma = KARMA['unlink_own']
self.post.unlink()
def test_unlink_post_all(self):
self.user_portal.karma = KARMA['unlink_all']
self.post.sudo(self.user_portal).unlink()

View File

@ -24,28 +24,45 @@
<sheet> <sheet>
<group> <group>
<field name="name"/> <field name="name"/>
<field name="karma_ask"/>
<field name="karma_edit_own"/>
<field name="karma_edit_all"/>
<field name="karma_close_own"/>
<field name="karma_close_all"/>
<field name="karma_unlink_own"/>
<field name="karma_unlink_all"/>
</group>
<group>
<field name="karma_upvote"/>
<field name="karma_downvote"/>
<field name="karma_answer_accept_own"/>
<field name="karma_answer_accept_all"/>
<field name="karma_editor_link_files"/>
<field name="karma_editor_clickable_link"/>
<field name="karma_comment_own"/>
<field name="karma_comment_all"/>
<field name="karma_comment_convert_own"/>
<field name="karma_comment_convert_all"/>
<field name="karma_comment_unlink_own"/>
<field name="karma_comment_unlink_all"/>
</group> </group>
<notebook>
<page string='Karma Gains'>
<group>
<field name="karma_gen_question_new"/>
<field name="karma_gen_question_upvote"/>
<field name="karma_gen_question_downvote"/>
<field name="karma_gen_answer_upvote"/>
<field name="karma_gen_answer_downvote"/>
<field name="karma_gen_answer_accept"/>
<field name="karma_gen_answer_accepted"/>
</group>
</page>
<page string='Karma Requirements'>
<group>
<group>
<field name="karma_ask"/>
<field name="karma_upvote"/>
<field name="karma_downvote"/>
<field name="karma_edit_own"/>
<field name="karma_edit_all"/>
<field name="karma_close_own"/>
<field name="karma_close_all"/>
<field name="karma_unlink_own"/>
<field name="karma_unlink_all"/>
</group>
<group>
<field name="karma_answer_accept_own"/>
<field name="karma_answer_accept_all"/>
<field name="karma_comment_own"/>
<field name="karma_comment_all"/>
<field name="karma_comment_convert_own"/>
<field name="karma_comment_convert_all"/>
<field name="karma_comment_unlink_own"/>
<field name="karma_comment_unlink_all"/>
</group>
</group>
</page>
</notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/> <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>

View File

@ -96,6 +96,21 @@
<div t-field="notification.body"/> <div t-field="notification.body"/>
<a t-attf-href="/forum/#{ slug(forum) }/user/#{ user.id }#badges" class="fa fa-arrow-right">View Your Badges</a> <a t-attf-href="/forum/#{ slug(forum) }/user/#{ user.id }#badges" class="fa fa-arrow-right">View Your Badges</a>
</div> </div>
<div t-if="not validation_email_sent and not is_public_user and user.karma == 0" class="alert alert-danger alert-dismissable">
<button type="button" class="close validation_email_close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<div>
<p>
It appears your email has not been verified.
<a class="send_validation_email" href="#" t-att-forum-id="forum.id">Click here to send a verification email allowing you to participate to the forum.</a>
</p>
</div>
</div>
<div t-if="validation_email_done" class="alert alert-success alert-dismissable">
<button type="button" class="close validated_email_close" data-dismiss="alert" aria-hidden="true">&amp;times;</button>
<div>
<p>Congratulations! Your email has just been validated. You may now participate to our forums.</p>
</div>
</div>
<t t-raw="0"/> <t t-raw="0"/>
</div> </div>
<div class="col-sm-3" id="right-column"> <div class="col-sm-3" id="right-column">