From 2a4ec367a9a3b19a75061f14b436ee125fcd815e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 1 Jul 2014 09:19:22 +0200 Subject: [PATCH] [IMP] [FIX] website_forum: cleaned and improved the management of available actions based on the user's karma. The karma levels are now stored on the forum itself, allowing customization. The various check of karma are done at model level and not at controller level anymore, leading to a more secure and easier to inherit behavior. --- addons/website_forum/controllers/main.py | 90 +---- addons/website_forum/data/forum_data.xml | 4 +- addons/website_forum/models/forum.py | 348 +++++++++++++++--- .../static/src/css/website_forum.css | 8 + .../static/src/css/website_forum.sass | 7 + .../static/src/js/website_forum.js | 36 +- addons/website_forum/views/forum.xml | 21 ++ addons/website_forum/views/website_forum.xml | 82 +++-- 8 files changed, 411 insertions(+), 185 deletions(-) diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index 0d27f70e486..2d2e5f24606 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -12,7 +12,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 import html2plaintext controllers = controllers() @@ -33,20 +32,12 @@ class WebsiteForum(http.Controller): return msg def _prepare_forum_values(self, forum=None, **kwargs): - Forum = request.registry['forum.forum'] 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()), - 'can_edit_own': True, - 'can_edit_all': user.karma > Forum._karma_modo_edit_all, - 'can_close_own': user.karma > Forum._karma_modo_close_own, - 'can_close_all': user.karma > Forum._karma_modo_close_all, - 'can_unlink_own': user.karma > Forum._karma_modo_unlink_own, - 'can_unlink_all': user.karma > Forum._karma_modo_unlink_all, - 'can_unlink_comment': user.karma > Forum._karma_modo_unlink_comment, } if forum: values['forum'] = forum @@ -55,14 +46,6 @@ class WebsiteForum(http.Controller): values.update(kwargs) return values - def _has_enough_karma(self, karma_name, uid=None): - Forum = request.registry['forum.forum'] - karma = hasattr(Forum, karma_name) and getattr(Forum, karma_name) or 0 - user = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, uid or request.uid, context=request.context) - if user.karma < karma: - return False, {'error': 'not_enough_karma', 'karma': karma} - return True, {} - # Forum # -------------------------------------------------- @@ -244,10 +227,6 @@ class WebsiteForum(http.Controller): @http.route('/forum//question//ask_for_close', type='http', auth="user", methods=['POST'], website=True) def question_ask_for_close(self, forum, question, **post): - check_res = self._has_enough_karma(question.create_uid.id == request.uid and '_karma_modo_close_own' or '_karma_modo_close_all') - if not check_res[0]: - return werkzeug.utils.redirect("/forum/%s" % slug(forum)) - cr, uid, context = request.cr, request.uid, request.context Reason = request.registry['forum.post.reason'] reason_ids = Reason.search(cr, uid, [], context=context) @@ -272,42 +251,21 @@ class WebsiteForum(http.Controller): @http.route('/forum//question//close', type='http', auth="user", methods=['POST'], website=True) def question_close(self, forum, question, **post): - check_res = self._has_enough_karma(question.create_uid.id == request.uid and '_karma_modo_close_own' or '_karma_modo_close_all') - if not check_res[0]: - return werkzeug.utils.redirect("/forum/%s" % slug(forum)) - - request.registry['forum.post'].write(request.cr, request.uid, [question.id], { - 'state': 'close', - 'closed_uid': request.uid, - 'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), - 'closed_reason_id': int(post.get('reason_id', False)), - }, context=request.context) + request.registry['forum.post'].close(request.cr, request.uid, [question.id], reason_id=int(post.get('reason_id', False)), context=request.context) return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) @http.route('/forum//question//reopen', type='http', auth="user", methods=['POST'], website=True) def question_reopen(self, forum, question, **kwarg): - check_res = self._has_enough_karma(question.create_uid.id == request.uid and '_karma_modo_close_own' or '_karma_modo_close_all') - if not check_res[0]: - return werkzeug.utils.redirect("/forum/%s" % slug(forum)) - request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'state': 'active'}, context=request.context) return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) @http.route('/forum//question//delete', type='http', auth="user", methods=['POST'], website=True) def question_delete(self, forum, question, **kwarg): - check_res = self._has_enough_karma(question.create_uid.id == request.uid and '_karma_modo_unlink_own' or '_karma_modo_unlink_all') - if not check_res[0]: - return werkzeug.utils.redirect("/forum/%s" % slug(forum)) - request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'active': False}, context=request.context) return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) @http.route('/forum//question//undelete', type='http', auth="user", methods=['POST'], website=True) def question_undelete(self, forum, question, **kwarg): - check_res = self._has_enough_karma(question.create_uid.id == request.uid and '_karma_modo_unlink_own' or '_karma_modo_unlink_all') - if not check_res[0]: - return werkzeug.utils.redirect("/forum/%s" % slug(forum)) - request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'active': True}, context=request.context) return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) @@ -349,11 +307,6 @@ class WebsiteForum(http.Controller): return request.redirect('/') if not request.session.uid: return {'error': 'anonymous_user'} - user = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, request.uid, context=request.context) - if post.parent_id.create_uid.id != uid and user.karma < request.registry['forum.forum']._karma_answer_accept_all: - return {'error': 'not_enough_karma', 'karma': request.registry['forum.forum']._karma_answer_accept_all} - if post.create_uid.id == user.id and user.karma < request.registry['forum.forum']._karma_answer_accept_own: - return {'error': 'not_enough_karma', 'karma': request.registry['forum.forum']._karma_answer_accept_own} # set all answers to False, only one can be accepted request.registry['forum.post'].write(cr, uid, [c.id for c in post.parent_id.child_ids], {'is_correct': False}, context=context) @@ -362,10 +315,6 @@ class WebsiteForum(http.Controller): @http.route('/forum//post//delete', type='http', auth="user", methods=['POST'], website=True) def post_delete(self, forum, post, **kwargs): - check_res = self._has_enough_karma(post.create_uid.id == request.uid and '_karma_modo_unlink_own' or '_karma_modo_unlink_all') - if not check_res[0]: - return werkzeug.utils.redirect("/forum/%s" % slug(forum)) - question = post.parent_id request.registry['forum.post'].unlink(request.cr, request.uid, [post.id], context=request.context) if question: @@ -374,10 +323,6 @@ class WebsiteForum(http.Controller): @http.route('/forum//post//edit', type='http', auth="user", website=True) def post_edit(self, forum, post, **kwargs): - check_res = self._has_enough_karma(post.create_uid.id == request.uid and '_karma_modo_edit_own' or '_karma_modo_edit_all') - if not check_res[0]: - return werkzeug.utils.redirect("/forum/%s" % slug(forum)) - tags = "" for tag_name in post.tag_ids: tags += tag_name.name + "," @@ -419,9 +364,6 @@ class WebsiteForum(http.Controller): return {'error': 'anonymous_user'} if request.uid == post.create_uid.id: return {'error': 'own_post'} - check_res = self._has_enough_karma('_karma_upvote') - if not check_res[0]: - return check_res[1] upvote = True if not post.user_vote > 0 else False return request.registry['forum.post'].vote(request.cr, request.uid, [post.id], upvote=upvote, context=request.context) @@ -431,9 +373,6 @@ class WebsiteForum(http.Controller): return {'error': 'anonymous_user'} if request.uid == post.create_uid.id: return {'error': 'own_post'} - check_res = self._has_enough_karma('_karma_downvote') - if not check_res[0]: - return check_res[1] upvote = True if post.user_vote < 0 else False return request.registry['forum.post'].vote(request.cr, request.uid, [post.id], upvote=upvote, context=request.context) @@ -625,26 +564,25 @@ class WebsiteForum(http.Controller): # Messaging # -------------------------------------------------- - @http.route('/forum//post//comment//convert_to_answer', type='http', auth="public", methods=['POST'], website=True) + @http.route('/forum//post//comment//convert_to_answer', type='http', auth="user", methods=['POST'], website=True) def convert_comment_to_answer(self, forum, post, comment, **kwarg): - body = comment.body - request.registry['mail.message'].unlink(request.cr, request.uid, [comment.id], context=request.context) + new_post_id = request.registry['forum.post'].convert_comment_to_answer(request.cr, request.uid, comment.id, context=request.context) + if not new_post_id: + return werkzeug.utils.redirect("/forum/%s" % slug(forum)) + post = request.registry['forum.post'].browse(request.cr, request.uid, new_post_id, context=request.context) question = post.parent_id if post.parent_id else post - for answer in question.child_ids: - if answer.create_uid.id == request.uid: - return self.post_comment(forum, answer, comment=html2plaintext(body)) - return self.post_new(forum, question, content=body) + return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) @http.route('/forum//post//convert_to_comment', type='http', auth="user", methods=['POST'], website=True) def convert_answer_to_comment(self, forum, post, **kwarg): - values = { - 'comment': html2plaintext(post.content), - } question = post.parent_id - request.registry['forum.post'].unlink(request.cr, SUPERUSER_ID, [post.id], context=request.context) - return self.post_comment(forum, question, **values) + new_msg_id = request.registry['forum.post'].convert_answer_to_comment(request.cr, request.uid, post.id, context=request.context) + if not new_msg_id: + return werkzeug.utils.redirect("/forum/%s" % slug(forum)) + return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) @http.route('/forum//post//comment//delete', type='json', auth="user", website=True) def delete_comment(self, forum, post, comment, **kwarg): - request.registry['mail.message'].unlink(request.cr, SUPERUSER_ID, [comment.id], context=request.context) - return True + if not request.session.uid: + return {'error': 'anonymous_user'} + return request.registry['forum.post'].unlink(request.cr, request.uid, post.id, comment.id, context=request.context) diff --git a/addons/website_forum/data/forum_data.xml b/addons/website_forum/data/forum_data.xml index a3c9b470537..61bb7ee0fb4 100644 --- a/addons/website_forum/data/forum_data.xml +++ b/addons/website_forum/data/forum_data.xml @@ -44,7 +44,7 @@ New Question forum.post - + New Question @@ -57,7 +57,7 @@ New Answer forum.forum - + forum_id diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 01512705be8..900eb4e0fe3 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -1,50 +1,63 @@ # -*- coding: utf-8 -*- +from datetime import datetime + import openerp +from openerp import tools from openerp import SUPERUSER_ID from openerp.addons.website.models.website import slug from openerp.osv import osv, fields +from openerp.tools import html2plaintext from openerp.tools.translate import _ +class KarmaError(ValueError): + """ Karma-related error, used for forum and posts. """ + pass + + class Forum(osv.Model): """TDE TODO: set karma values for actions dynamic for a given forum""" _name = 'forum.forum' _description = 'Forums' _inherit = ['mail.thread', 'website.seo.metadata'] - # karma values - _karma_upvote = 5 # done - _karma_downvote = 50 # done - _karma_answer_accept_own = 20 # done - _karma_answer_accept_own_now = 50 - _karma_answer_accept_all = 500 - _karma_editor_link_files = 30 # done - _karma_editor_clickable_link = 50 - _karma_comment = 1 - _karma_modo_retag = 75 - _karma_modo_flag = 100 - _karma_modo_flag_see_all = 300 - _karma_modo_unlink_comment = 750 - _karma_modo_edit_own = 1 # done - _karma_modo_edit_all = 300 # done - _karma_modo_close_own = 100 # done - _karma_modo_close_all = 900 # done - _karma_modo_unlink_own = 500 # done - _karma_modo_unlink_all = 1000 # done - # karma generation - _karma_gen_quest_new = 2 # done - _karma_gen_upvote_quest = 5 # done - _karma_gen_downvote_quest = -2 # done - _karma_gen_upvote_ans = 10 # done - _karma_gen_downvote_ans = -2 # done - _karma_gen_ans_accept = 2 # done - _karma_gen_ans_accepted = 15 # done - _karma_gen_ans_flagged = -100 _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-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'), } def _get_default_faq(self, cr, uid, context=None): @@ -56,6 +69,36 @@ 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_upvote': 5, + 'karma_gen_question_downvote': -2, + 'karma_gen_answer_upvote': 10, + 'karma_gen_answer_downvote': -2, + 'karma_gen_answer_accept': 2, + 'karma_gen_answer_accepted': 15, + 'karma_gen_answer_flagged': -100, + 'karma_ask': 0, + 'karma_answer': 0, + 'karma_edit_own': 1, + 'karma_edit_all': 300, + 'karma_close_own': 100, + 'karma_close_all': 500, + 'karma_unlink_own': 500, + 'karma_unlink_all': 1000, + 'karma_upvote': 5, + 'karma_downvote': 50, + 'karma_answer_accept_own': 20, + '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_convert_own': 50, + 'karma_comment_convert_all': 500, + 'karma_comment_unlink_own': 50, + 'karma_comment_unlink_all': 500, + 'karma_retag': 75, + 'karma_flag': 500, } def create(self, cr, uid, values, context=None): @@ -139,6 +182,36 @@ class Post(osv.Model): res[post.id] = post.parent_id and post.parent_id.create_uid == post.create_uid or False return res + def _get_post_karma_rights(self, cr, uid, ids, field_name, arg, context=None): + user = self.pool['res.users'].browse(cr, uid, uid, context=context) + res = dict.fromkeys(ids, False) + for post in self.browse(cr, uid, ids, context=context): + res[post.id] = { + 'karma_ask': post.forum_id.karma_ask, + 'karma_answer': post.forum_id.karma_answer, + 'karma_accept': post.parent_id and post.parent_id.create_uid.id == uid and post.forum_id.karma_answer_accept_own or post.forum_id.karma_answer_accept_all, + 'karma_edit': post.create_uid.id == uid and post.forum_id.karma_edit_own or post.forum_id.karma_edit_all, + 'karma_close': post.create_uid.id == uid and post.forum_id.karma_close_own or post.forum_id.karma_close_all, + 'karma_unlink': post.create_uid.id == uid and post.forum_id.karma_unlink_own or post.forum_id.karma_unlink_all, + 'karma_upvote': post.forum_id.karma_upvote, + 'karma_downvote': post.forum_id.karma_downvote, + 'karma_comment': post.create_uid.id == uid and post.forum_id.karma_comment_own or post.forum_id.karma_comment_all, + 'karma_comment_convert': post.create_uid.id == uid and post.forum_id.karma_comment_convert_own or post.forum_id.karma_comment_convert_all, + } + res[post.id].update({ + 'can_ask': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_ask'], + 'can_answer': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_answer'], + 'can_accept': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_accept'], + 'can_edit': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_edit'], + 'can_close': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_close'], + 'can_unlink': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_unlink'], + 'can_upvote': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_upvote'], + 'can_downvote': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_downvote'], + 'can_comment': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_comment'], + 'can_comment_convert': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_comment_convert'], + }) + return res + _columns = { 'name': fields.char('Title'), 'forum_id': fields.many2one('forum.forum', 'Forum', required=True), @@ -151,7 +224,7 @@ class Post(osv.Model): 'website_message_ids': fields.one2many( 'mail.message', 'res_id', domain=lambda self: [ - '&', ('model', '=', self._name), ('type', '=', 'comment') + '&', ('model', '=', self._name), ('type', 'in', ['email', 'comment']) ], string='Post Messages', help="Comments on forum post", ), @@ -203,6 +276,26 @@ class Post(osv.Model): 'closed_reason_id': fields.many2one('forum.post.reason', 'Reason'), 'closed_uid': fields.many2one('res.users', 'Closed by', select=1), 'closed_date': fields.datetime('Closed on', readonly=True), + # karma + 'karma_ask': fields.function(_get_post_karma_rights, string='Karma to ask', type='integer', multi='_get_post_karma_rights'), + 'karma_accept': fields.function(_get_post_karma_rights, string='Karma to accept this answer', type='integer', multi='_get_post_karma_rights'), + 'karma_edit': fields.function(_get_post_karma_rights, string='Karma to edit', type='integer', multi='_get_post_karma_rights'), + 'karma_close': fields.function(_get_post_karma_rights, string='Karma to close', type='integer', multi='_get_post_karma_rights'), + 'karma_unlink': fields.function(_get_post_karma_rights, string='Karma to unlink', type='integer', multi='_get_post_karma_rights'), + 'karma_upvote': fields.function(_get_post_karma_rights, string='Karma to upvote', type='integer', multi='_get_post_karma_rights'), + 'karma_downvote': fields.function(_get_post_karma_rights, string='Karma to downvote', type='integer', multi='_get_post_karma_rights'), + 'karma_comment': fields.function(_get_post_karma_rights, string='Karma to comment', type='integer', multi='_get_post_karma_rights'), + 'karma_comment_convert': fields.function(_get_post_karma_rights, string='karma to convert as a comment', type='integer', multi='_get_post_karma_rights'), + # access rights + 'can_ask': fields.function(_get_post_karma_rights, string='Can Ask', type='boolean', multi='_get_post_karma_rights'), + 'can_accept': fields.function(_get_post_karma_rights, string='Can Accept', type='boolean', multi='_get_post_karma_rights'), + 'can_edit': fields.function(_get_post_karma_rights, string='Can Edit', type='boolean', multi='_get_post_karma_rights'), + 'can_close': fields.function(_get_post_karma_rights, string='Can Close', type='boolean', multi='_get_post_karma_rights'), + 'can_unlink': fields.function(_get_post_karma_rights, string='Can Unlink', type='boolean', multi='_get_post_karma_rights'), + 'can_upvote': fields.function(_get_post_karma_rights, string='Can Upvote', type='boolean', multi='_get_post_karma_rights'), + 'can_downvote': fields.function(_get_post_karma_rights, string='Can Downvote', type='boolean', multi='_get_post_karma_rights'), + 'can_comment': fields.function(_get_post_karma_rights, string='Can Comment', type='boolean', multi='_get_post_karma_rights'), + 'can_comment_convert': fields.function(_get_post_karma_rights, string='Can Convert to Comment', type='boolean', multi='_get_post_karma_rights'), } _defaults = { @@ -219,41 +312,93 @@ class Post(osv.Model): context = {} create_context = dict(context, mail_create_nolog=True) post_id = super(Post, self).create(cr, uid, vals, context=create_context) - # post message + subtype depending on parent_id - if vals.get("parent_id"): - parent = self.browse(cr, SUPERUSER_ID, vals['parent_id'], context=context) - body = _('

New Answer Posted

' % (slug(parent.forum_id), slug(parent))) - self.message_post(cr, uid, parent.id, subject=_('Re: %s') % parent.name, body=body, subtype='website_forum.mt_answer_new', context=context) + post = self.browse(cr, SUPERUSER_ID, post_id, context=context) # SUPERUSER_ID to avoid read access rights issues when creating + # karma-based access + if post.parent_id and not post.can_ask: + raise KarmaError('Not enough karma to create a new question') + elif not post.parent_id and not post.can_answer: + raise KarmaError('Not enough karma to answer to a question') + # messaging and chatter + base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') + if post.parent_id: + body = _( + '

A new answer for %s has been posted. Click here to access the post.

' % + (post.parent_id.name, base_url, slug(post.parent_id.forum_id), slug(post.parent_id)) + ) + self.message_post(cr, uid, post.parent_id.id, subject=_('Re: %s') % post.parent_id.name, body=body, subtype='website_forum.mt_answer_new', context=context) else: - self.message_post(cr, uid, post_id, subject=vals.get('name', ''), body=_('New Question Created'), subtype='website_forum.mt_question_new', context=context) - self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], self.pool['forum.forum']._karma_gen_quest_new, context=context) + body = _( + '

A new question %s has been asked on %s. Click here to access the question.

' % + (post.name, post.forum_id.name, base_url, slug(post.forum_id), slug(post)) + ) + self.message_post(cr, uid, post_id, subject=post.name, body=body, subtype='website_forum.mt_question_new', context=context) + self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], post.forum_id.karma_gen_question_new, context=context) return post_id def write(self, cr, uid, ids, vals, context=None): - Forum = self.pool['forum.forum'] - # update karma when accepting/rejecting answers + posts = self.browse(cr, uid, ids, context=context) + if 'state' in vals: + if vals['state'] in ['active', 'close'] and any(not post.can_close for post in posts): + raise KarmaError('Not enough karma to close or reopen a post.') + if 'active' in vals: + if any(not post.can_unlink for post in posts): + raise KarmaError('Not enough karma to delete or reactivate a post') if 'is_correct' in vals: + if any(not post.can_accept for post in posts): + raise KarmaError('Not enough karma to accept or refuse an answer') + # update karma except for self-acceptance mult = 1 if vals['is_correct'] else -1 for post in self.browse(cr, uid, ids, context=context): - if vals['is_correct'] != post.is_correct: - self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], Forum._karma_gen_ans_accepted * mult, context=context) - self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], Forum._karma_gen_ans_accept * mult, context=context) + if vals['is_correct'] != post.is_correct and post.create_uid.id != uid: + self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], post.forum_id.karma_gen_answer_accepted * mult, context=context) + self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], post.forum_id.karma_gen_answer_accept * mult, context=context) + if any(key not in ['state', 'active', 'is_correct', 'closed_uid', 'closed_date', 'closed_reason_id'] for key in vals.keys()) and any(not post.can_edit for post in posts): + raise KarmaError('Not enough karma to edit a post.') + res = super(Post, self).write(cr, uid, ids, vals, context=context) # if post content modify, notify followers if 'content' in vals or 'name' in vals: - for post in self.browse(cr, uid, ids, context=context): + for post in posts: if post.parent_id: body, subtype = _('Answer Edited'), 'website_forum.mt_answer_edit' obj_id = post.parent_id.id else: body, subtype = _('Question Edited'), 'website_forum.mt_question_edit' obj_id = post.id - self.message_post(cr, uid, obj_id, body=_(body), subtype=subtype, context=context) + self.message_post(cr, uid, obj_id, body=body, subtype=subtype, context=context) return res + def close(self, cr, uid, ids, reason_id, context=None): + if any(post.parent_id for post in self.browse(cr, uid, ids, context=context)): + return False + return self.pool['forum.post'].write(cr, uid, ids, { + 'state': 'close', + 'closed_uid': uid, + 'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), + 'closed_reason_id': reason_id, + }, context=context) + + def unlink(self, cr, uid, ids, context=None): + posts = self.browse(cr, uid, ids, context=context) + if any(not post.can_unlink for post in posts): + raise KarmaError('Not enough karma to unlink a post') + # if unlinking an answer with accepted answer: remove provided karma + for post in posts: + if post.is_correct: + self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], post.forum_id.karma_gen_answer_accepted * -1, context=context) + self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], post.forum_id.karma_gen_answer_accept * -1, context=context) + 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) + vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], limit=1, context=context) if vote_ids: for vote in Vote.browse(cr, uid, vote_ids, context=context): if upvote: @@ -267,6 +412,89 @@ class Post(osv.Model): Vote.create(cr, uid, {'post_id': post_id, 'vote': new_vote}, context=context) return {'vote_count': self._get_vote_count(cr, uid, ids, None, None, context=context)[ids[0]]} + def convert_answer_to_comment(self, cr, uid, id, context=None): + """ Tools to convert an answer (forum.post) to a comment (mail.message). + The original post is unlinked and a new comment is posted on the question + using the post create_uid as the comment's author. """ + post = self.browse(cr, uid, id, context=context) + if not post.parent_id: + return False + + # karma-based action check: use the post field that computed own/all value + if not post.can_comment_convert: + raise KarmaError('Not enough karma to convert an answer to a comment') + + # post the message + question = post.parent_id + values = { + 'author_id': post.create_uid.partner_id.id, + 'body': html2plaintext(post.content), + 'type': 'comment', + 'subtype': 'mail.mt_comment', + 'date': post.create_date, + } + message_id = self.pool['forum.post'].message_post( + cr, uid, question.id, + context=dict(context, mail_create_nosubcribe=True), + **values) + + # unlink the original answer, using SUPERUSER_ID to avoid karma issues + self.pool['forum.post'].unlink(cr, SUPERUSER_ID, [post.id], context=context) + + return message_id + + def convert_comment_to_answer(self, cr, uid, message_id, default=None, context=None): + """ Tool to convert a comment (mail.message) into an answer (forum.post). + The original comment is unlinked and a new answer from the comment's author + is created. Nothing is done if the comment's author already answered the + question. """ + comment = self.pool['mail.message'].browse(cr, SUPERUSER_ID, message_id, context=context) + post = self.pool['forum.post'].browse(cr, uid, comment.res_id, context=context) + user = self.pool['res.users'].browse(cr, uid, uid, context=context) + if not comment.author_id or not comment.author_id.user_ids: # only comment posted by users can be converted + return False + + # karma-based action check: must check the message's author to know if own / all + karma_convert = comment.author_id.id == user.partner_id.id and post.forum_id.karma_comment_convert_own or post.forum_id.karma_comment_convert_all + can_convert = uid == SUPERUSER_ID or user.karma >= karma_convert + if not can_convert: + raise KarmaError('Not enough karma to convert a comment to an answer') + + # check the message's author has not already an answer + question = post.parent_id if post.parent_id else post + post_create_uid = comment.author_id.user_ids[0] + if any(answer.create_uid.id == post_create_uid.id for answer in question.child_ids): + return False + + # create the new post + post_values = { + 'forum_id': question.forum_id.id, + 'content': comment.body, + 'parent_id': question.id, + } + # done with the author user to have create_uid correctly set + new_post_id = self.pool['forum.post'].create(cr, post_create_uid.id, post_values, context=context) + + # delete comment + self.pool['mail.message'].unlink(cr, SUPERUSER_ID, [comment.id], context=context) + + return new_post_id + + def unlink_comment(self, cr, uid, id, message_id, context=None): + comment = self.pool['mail.message'].browse(cr, SUPERUSER_ID, message_id, context=context) + post = self.pool['forum.post'].browse(cr, uid, id, context=context) + user = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context) + if not comment.model == 'forum.post' or not comment.res_id == id: + return False + + # karma-based action check: must check the message's author to know if own or all + karma_unlink = comment.author_id.id == user.partner_id.id and post.forum_id.karma_comment_unlink_own or post.forum_id.karma_comment_unlink_all + can_unlink = uid == SUPERUSER_ID or user.karma >= karma_unlink + if not can_unlink: + raise KarmaError('Not enough karma to unlink a comment') + + return self.pool['mail.message'].unlink(cr, SUPERUSER_ID, [message_id], context=context) + def set_viewed(self, cr, uid, ids, context=None): cr.execute("""UPDATE forum_post SET views = views+1 WHERE id IN %s""", (tuple(ids),)) return True @@ -300,31 +528,31 @@ class Vote(osv.Model): 'vote': lambda *args: '1', } + def _get_karma_value(self, old_vote, new_vote, up_karma, down_karma): + _karma_upd = { + '-1': {'-1': 0, '0': -1 * down_karma, '1': -1 * down_karma + up_karma}, + '0': {'-1': 1 * down_karma, '0': 0, '1': up_karma}, + '1': {'-1': -1 * up_karma + down_karma, '0': -1 * up_karma, '1': 0} + } + return _karma_upd[old_vote][new_vote] + def create(self, cr, uid, vals, context=None): vote_id = super(Vote, self).create(cr, uid, vals, context=context) - if vals.get('vote', '1') == '1': - karma = self.pool['forum.forum']._karma_upvote - elif vals.get('vote', '1') == '-1': - karma = self.pool['forum.forum']._karma_downvote - post = self.pool['forum.post'].browse(cr, uid, vals['post_id'], context=context) - self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], karma, context=context) + vote = self.browse(cr, uid, vote_id, context=context) + if vote.post_id.parent_id: + karma_value = self._get_karma_value('0', vote.vote, vote.post_id.forum_id.karma_gen_answer_upvote, vote.post_id.forum_id.karma_gen_answer_downvote) + else: + karma_value = self._get_karma_value('0', vote.vote, vote.post_id.forum_id.karma_gen_question_upvote, vote.post_id.forum_id.karma_gen_question_downvote) + self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [vote.post_id.create_uid.id], karma_value, context=context) return vote_id def write(self, cr, uid, ids, values, context=None): - def _get_karma_value(old_vote, new_vote, up_karma, down_karma): - _karma_upd = { - '-1': {'-1': 0, '0': -1 * down_karma, '1': -1 * down_karma + up_karma}, - '0': {'-1': 1 * down_karma, '0': 0, '1': up_karma}, - '1': {'-1': -1 * up_karma + down_karma, '0': -1 * up_karma, '1': 0} - } - return _karma_upd[old_vote][new_vote] if 'vote' in values: - Forum = self.pool['forum.forum'] for vote in self.browse(cr, uid, ids, context=context): if vote.post_id.parent_id: - karma_value = _get_karma_value(vote.vote, values['vote'], Forum._karma_gen_upvote_ans, Forum._karma_gen_downvote_ans) + karma_value = self._get_karma_value(vote.vote, values['vote'], vote.post_id.forum_id.karma_gen_answer_upvote, vote.post_id.forum_id.karma_gen_answer_downvote) else: - karma_value = _get_karma_value(vote.vote, values['vote'], Forum._karma_gen_upvote_quest, Forum._karma_gen_downvote_quest) + karma_value = self._get_karma_value(vote.vote, values['vote'], vote.post_id.forum_id.karma_gen_question_upvote, vote.post_id.forum_id.karma_gen_question_downvote) self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [vote.post_id.create_uid.id], karma_value, context=context) res = super(Vote, self).write(cr, uid, ids, values, context=context) return res diff --git a/addons/website_forum/static/src/css/website_forum.css b/addons/website_forum/static/src/css/website_forum.css index 54a7f47380f..d3af4309b24 100644 --- a/addons/website_forum/static/src/css/website_forum.css +++ b/addons/website_forum/static/src/css/website_forum.css @@ -104,6 +104,14 @@ a.no-decoration { height: 1.2em !important; } +.oe_forum_alert { + position: absolute; + margin-top: -30px; + margin-left: 90px; + width: 300px; + z-index: 9999; +} + button.btn-link.text-muted { color: #999999; } diff --git a/addons/website_forum/static/src/css/website_forum.sass b/addons/website_forum/static/src/css/website_forum.sass index 4a16f48e77e..c14e91e2893 100644 --- a/addons/website_forum/static/src/css/website_forum.sass +++ b/addons/website_forum/static/src/css/website_forum.sass @@ -84,5 +84,12 @@ a.no-decoration font: 1.2em "Helvetica Neue", Helvetica, Arial, sans-serif !important height: 1.2em !important +.oe_forum_alert + position: absolute + margin-top: -30px + margin-left: 90px + width: 300px + z-index: 9999 + button.btn-link.text-muted color: #999 diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js index 5db211ab9aa..aac2ba8f5bb 100644 --- a/addons/website_forum/static/src/js/website_forum.js +++ b/addons/website_forum/static/src/js/website_forum.js @@ -1,28 +1,37 @@ $(document).ready(function () { - $('.vote_up ,.vote_down').on('click', function (ev) { + $('.karma_required').on('click', function (ev) { + var karma = $(ev.currentTarget).data('karma'); + if (karma) { + ev.preventDefault(); + var $warning = $('
'+ + ''+ + karma + ' karma is required to perform this action. You can earn karma by answering questions or having '+ + 'your answers upvoted by the community.
'); + var vote_alert = $(ev.currentTarget).parent().find("#vote_alert"); + if (vote_alert.length == 0) { + $(ev.currentTarget).parent().append($warning); + } + } + }); + + $('.vote_up,.vote_down').not('.karma_required').on('click', function (ev) { ev.preventDefault(); var $link = $(ev.currentTarget); openerp.jsonRpc($link.data('href'), 'call', {}) .then(function (data) { if (data['error']){ if (data['error'] == 'own_post'){ - var $warning = $('
'+ + var $warning = $('
'+ ''+ 'Sorry, you cannot vote for your own posts'+ '
'); } else if (data['error'] == 'anonymous_user'){ - var $warning = $('
'+ + var $warning = $('
'+ ''+ 'Sorry you must be logged to vote'+ '
'); } - else if (data['error'] == 'not_enough_karma') { - var $warning = $('
'+ - ''+ - 'Sorry, at least ' + data['karma'] + ' karma is required to vote. You can gain karma by answering questions and receiving votes.'+ - '
'); - } vote_alert = $link.parent().find("#vote_alert"); if (vote_alert.length == 0) { $link.parent().append($warning); @@ -44,21 +53,16 @@ $(document).ready(function () { return true; }); - $('.accept_answer').on('click', function (ev) { + $('.accept_answer').not('.karma_required').on('click', function (ev) { ev.preventDefault(); var $link = $(ev.currentTarget); openerp.jsonRpc($link.data('href'), 'call', {}).then(function (data) { if (data['error']) { - if (data['error'] == 'anonymous_user'){ + if (data['error'] == 'anonymous_user') { var $warning = $('
'+ ''+ 'Sorry, anonymous users cannot choose correct answer.'+ '
'); - } else if (data['error'] == 'not_enough_karma') { - var $warning = $('
'+ - ''+ - 'Sorry, at least ' + data['karma'] + ' karma is required to accept this answer. You can gain karma by answering questions and receiving votes.'+ - '
'); } correct_answer_alert = $link.parent().find("#correct_answer_alert"); if (correct_answer_alert.length == 0) { diff --git a/addons/website_forum/views/forum.xml b/addons/website_forum/views/forum.xml index d6a2484bfd1..a2d7c093a7f 100644 --- a/addons/website_forum/views/forum.xml +++ b/addons/website_forum/views/forum.xml @@ -24,6 +24,27 @@ + + + + + + + + + + + + + + + + + + + + +
diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml index e4f9e0c04b9..73fcbef0fd6 100644 --- a/addons/website_forum/views/website_forum.xml +++ b/addons/website_forum/views/website_forum.xml @@ -25,7 +25,7 @@ @@ -163,7 +163,7 @@
Answer
-