From 205ce9865665e1bb02073243e67d8f9dcb6a9550 Mon Sep 17 00:00:00 2001 From: Julien Louette Date: Sat, 5 Sep 2015 14:44:06 +0200 Subject: [PATCH] [IMP] website_forum: moderation & anti spam --- addons/website_forum/controllers/main.py | 109 +++++++- addons/website_forum/data/forum_data.xml | 28 ++ addons/website_forum/models/forum.py | 192 ++++++++++---- .../static/src/js/website_forum.js | 46 +++- addons/website_forum/tests/common.py | 2 + addons/website_forum/tests/test_forum.py | 101 ++++++- addons/website_forum/views/forum.xml | 7 +- addons/website_forum/views/website_forum.xml | 246 ++++++++++++++---- 8 files changed, 626 insertions(+), 105 deletions(-) diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index 192c69ec77e..b004215e1ff 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -228,6 +228,11 @@ class WebsiteForum(http.Controller): if not question.can_view: raise werkzeug.exceptions.NotFound() + # Hide pending posts from non-moderators and non-creator + user = request.env.user + if question.state == 'pending' and user.karma < forum.karma_post and question.create_uid != user: + raise werkzeug.exceptions.NotFound() + # increment view counter question.sudo().set_viewed() if question.parent_id: @@ -260,11 +265,10 @@ 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): - reasons = request.env['forum.post.reason'].search([]) + reasons = request.env['forum.post.reason'].search([('reason_type', '=', 'basic')]) values = self._prepare_forum_values(**post) values.update({ - 'question': question, 'question': question, 'forum': forum, 'reasons': reasons, @@ -419,6 +423,107 @@ class WebsiteForum(http.Controller): return False return post.bump() + # Moderation Tools + # -------------------------------------------------- + + @http.route('/forum//validation_queue', type='http', auth="user", website=True) + def validation_queue(self, forum): + user = request.env.user + if user.karma < forum.karma_moderate: + raise werkzeug.exceptions.NotFound() + + Post = request.env['forum.post'] + domain = [('forum_id', '=', forum.id), ('state', '=', 'pending')] + posts_to_validate_ids = Post.search(domain) + + values = self._prepare_forum_values(forum=forum) + values.update({ + 'posts_ids': posts_to_validate_ids, + 'queue_type': 'validation', + }) + + return request.website.render("website_forum.moderation_queue", values) + + @http.route('/forum//flagged_queue', type='http', auth="user", website=True) + def flagged_queue(self, forum): + user = request.env.user + if user.karma < forum.karma_moderate: + raise werkzeug.exceptions.NotFound() + + Post = request.env['forum.post'] + domain = [('forum_id', '=', forum.id), ('state', '=', 'flagged')] + flagged_posts_ids = Post.search(domain, order='write_date DESC') + + values = self._prepare_forum_values(forum=forum) + values.update({ + 'posts_ids': flagged_posts_ids, + 'queue_type': 'flagged', + }) + + return request.website.render("website_forum.moderation_queue", values) + + @http.route('/forum//offensive_posts', type='http', auth="user", website=True) + def offensive_posts(self, forum): + user = request.env.user + if user.karma < forum.karma_moderate: + raise werkzeug.exceptions.NotFound() + + Post = request.env['forum.post'] + domain = [('forum_id', '=', forum.id), ('state', '=', 'offensive'), ('active', '=', False)] + offensive_posts_ids = Post.search(domain, order='write_date DESC') + + values = self._prepare_forum_values(forum=forum) + values.update({ + 'posts_ids': offensive_posts_ids, + 'queue_type': 'offensive', + }) + + return request.website.render("website_forum.moderation_queue", values) + + @http.route('/forum//post//validate', type='http', auth="user", website=True) + def post_accept(self, forum, post): + url = "/forum/%s/validation_queue" % (slug(forum)) + if post.state == 'flagged': + url = "/forum/%s/flagged_queue" % (slug(forum)) + elif post.state == 'offensive': + url = "/forum/%s/offensive_posts" % (slug(forum)) + post.validate() + return werkzeug.utils.redirect(url) + + @http.route('/forum//post//refuse', type='http', auth="user", website=True) + def post_refuse(self, forum, post): + post.refuse() + return self.question_ask_for_close(forum, post) + + @http.route('/forum//post//flag', type='json', auth="public", website=True) + def post_flag(self, forum, post, **kwargs): + if not request.session.uid: + return {'error': 'anonymous_user'} + return post.flag()[0] + + @http.route('/forum//post//ask_for_mark_as_offensive', type='http', auth="user", methods=['GET'], website=True) + def post_ask_for_mark_as_offensive(self, forum, post): + offensive_reasons = request.env['forum.post.reason'].search([('reason_type', '=', 'offensive')]) + + values = self._prepare_forum_values(forum=forum) + values.update({ + 'question': post, + 'forum': forum, + 'reasons': offensive_reasons, + 'offensive': True, + }) + return request.website.render("website_forum.close_post", values) + + @http.route('/forum//post//mark_as_offensive', type='http', auth="user", methods=["POST"], website=True) + def post_mark_as_offensive(self, forum, post, **kwargs): + post.mark_as_offensive(reason_id=int(kwargs.get('reason_id', False))) + url = '' + if post.parent_id: + url = "/forum/%s/question/%s/#answer-%s" % (slug(forum), post.parent_id.id, post.id) + else: + url = "/forum/%s/question/%s" % (slug(forum), slug(post)) + return werkzeug.utils.redirect(url) + # User # -------------------------------------------------- diff --git a/addons/website_forum/data/forum_data.xml b/addons/website_forum/data/forum_data.xml index e804525be18..9c4a0ff48ad 100644 --- a/addons/website_forum/data/forum_data.xml +++ b/addons/website_forum/data/forum_data.xml @@ -83,27 +83,55 @@ duplicate post + basic off-topic or not relevant + basic too subjective and argumentative + basic not a real post + basic not relevant or out dated + basic contains offensive or malicious remarks + basic spam or advertising + basic too localized + basic + + + insulting and offensive language + offensive + + + violent language + offensive + + + inappropriate and unacceptable statements + offensive + + + threatening language + offensive + + + racist and hate speech + offensive diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 7da1e16acf1..8487854b8ca 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -63,7 +63,7 @@ class Forum(models.Model):

This community is for professionals and enthusiasts of our products and services. Share and discuss the best content and new marketing ideas, build your professional profile and become a better marketer together.

-      Hide Intro    Register 
+ Hide Intro Register """) @@ -92,6 +92,8 @@ class Forum(models.Model): help='After posting the user will be proposed to share its question' 'or answer on social networks, enabling social network propagation' 'of the forum content.') + count_posts_waiting_validation = fields.Integer(string="Number of posts waiting for validation", compute='_compute_count_posts_waiting_validation') + count_flagged_posts = fields.Integer(string='Number of flagged posts', compute='_compute_count_flagged_posts') # karma generation karma_gen_question_new = fields.Integer(string='Asking a question', default=2) karma_gen_question_upvote = fields.Integer(string='Question upvoted', default=5) @@ -126,6 +128,8 @@ class Forum(models.Model): karma_editor = fields.Integer(string='Editor Features: image and links', default=30, oldname='karma_editor_link_files') karma_user_bio = fields.Integer(string='Display detailed user biography', default=750) + karma_post = fields.Integer(string='Ask questions without validation', default=100) + karma_moderate = fields.Integer(string='Moderate posts', default=1000) @api.one @api.constrains('allow_question', 'allow_discussion', 'allow_link', 'default_post_type') @@ -141,6 +145,15 @@ class Forum(models.Model): if self.default_post_type == 'link' and not self.allow_link or self.default_post_type == 'question' and not self.allow_question or self.default_post_type == 'discussion' and not self.allow_discussion: raise Warning(_('Post type in "Default post" must be activated')) + @api.one + def _compute_count_posts_waiting_validation(self): + domain = [('forum_id', '=', self.id), ('state', '=', 'pending')] + self.count_posts_waiting_validation = self.env['forum.post'].search_count(domain) + + @api.one + def _compute_count_flagged_posts(self): + domain = [('forum_id', '=', self.id), ('state', '=', 'flagged')] + self.count_flagged_posts = self.env['forum.post'].search_count(domain) @api.model def create(self, values): @@ -185,15 +198,9 @@ class Post(models.Model): forum_id = fields.Many2one('forum.forum', string='Forum', required=True) content = fields.Html('Content', strip_style=True) plain_content = fields.Text('Plain Content', compute='_get_plain_content', store=True) - - @api.one - @api.depends('content') - def _get_plain_content(self): - self.plain_content = tools.html2plaintext(self.content)[0:500] if self.content else False - content_link = fields.Char('URL', help="URL of Link Articles") tag_ids = fields.Many2many('forum.tag', 'forum_tag_rel', 'forum_id', 'forum_tag_id', string='Tags') - state = fields.Selection([('active', 'Active'), ('close', 'Close'), ('offensive', 'Offensive')], string='Status', default='active') + state = fields.Selection([('active', 'Active'), ('pending', 'Waiting Validation'), ('close', 'Close'), ('offensive', 'Offensive'), ('flagged', 'Flagged')], string='Status', default='active') views = fields.Integer('Number of Views', default=0) active = fields.Boolean('Active', default=True) post_type = fields.Selection([ @@ -206,6 +213,7 @@ class Post(models.Model): domain=lambda self: ['&', ('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])], string='Post Messages', help="Comments on forum post", ) + # history create_date = fields.Datetime('Asked on', select=True, readonly=True) create_uid = fields.Many2one('res.users', string='Created by', select=True, readonly=True) @@ -217,6 +225,63 @@ class Post(models.Model): write_uid = fields.Many2one('res.users', string='Updated by', select=True, readonly=True) relevancy = fields.Float('Relevance', compute="_compute_relevancy", store=True) + # vote + vote_ids = fields.One2many('forum.post.vote', 'post_id', string='Votes') + user_vote = fields.Integer('My Vote', compute='_get_user_vote') + vote_count = fields.Integer('Votes', compute='_get_vote_count', store=True) + + # favorite + favourite_ids = fields.Many2many('res.users', string='Favourite') + user_favourite = fields.Boolean('Is Favourite', compute='_get_user_favourite') + favourite_count = fields.Integer('Favorite Count', compute='_get_favorite_count', store=True) + + # hierarchy + is_correct = fields.Boolean('Correct', help='Correct answer or answer accepted') + parent_id = fields.Many2one('forum.post', string='Question', ondelete='cascade') + self_reply = fields.Boolean('Reply to own question', compute='_is_self_reply', store=True) + child_ids = fields.One2many('forum.post', 'parent_id', string='Answers') + child_count = fields.Integer('Number of answers', compute='_get_child_count', store=True) + uid_has_answered = fields.Boolean('Has Answered', compute='_get_uid_has_answered') + has_validated_answer = fields.Boolean('Is answered', compute='_get_has_validated_answer', store=True) + + # offensive moderation tools + flag_user_id = fields.Many2one('res.users', string='Flagged by') + moderator_id = fields.Many2one('res.users', string='Reviewed by', readonly=True) + + # closing + closed_reason_id = fields.Many2one('forum.post.reason', string='Reason') + closed_uid = fields.Many2one('res.users', string='Closed by', select=1) + closed_date = fields.Datetime('Closed on', readonly=True) + + # karma calculation and access + karma_accept = fields.Integer('Convert comment to answer', compute='_get_post_karma_rights') + karma_edit = fields.Integer('Karma to edit', compute='_get_post_karma_rights') + karma_close = fields.Integer('Karma to close', compute='_get_post_karma_rights') + karma_unlink = fields.Integer('Karma to unlink', compute='_get_post_karma_rights') + karma_comment = fields.Integer('Karma to comment', compute='_get_post_karma_rights') + karma_comment_convert = fields.Integer('Karma to convert comment to answer', compute='_get_post_karma_rights') + karma_flag = fields.Integer('Flag a post as offensive', compute='_get_post_karma_rights') + can_ask = fields.Boolean('Can Ask', compute='_get_post_karma_rights') + can_answer = fields.Boolean('Can Answer', compute='_get_post_karma_rights') + can_accept = fields.Boolean('Can Accept', compute='_get_post_karma_rights') + can_edit = fields.Boolean('Can Edit', compute='_get_post_karma_rights') + can_close = fields.Boolean('Can Close', compute='_get_post_karma_rights') + can_unlink = fields.Boolean('Can Unlink', compute='_get_post_karma_rights') + can_upvote = fields.Boolean('Can Upvote', compute='_get_post_karma_rights') + can_downvote = fields.Boolean('Can Downvote', compute='_get_post_karma_rights') + can_comment = fields.Boolean('Can Comment', compute='_get_post_karma_rights') + can_comment_convert = fields.Boolean('Can Convert to Comment', compute='_get_post_karma_rights') + can_view = fields.Boolean('Can View', compute='_get_post_karma_rights') + can_display_biography = fields.Boolean('Can userbiography of the author be viewed', compute='_get_post_karma_rights') + can_post = fields.Boolean('Can Automatically be Validated', compute='_get_post_karma_rights') + can_flag = fields.Boolean('Can Flag', compute='_get_post_karma_rights') + can_moderate = fields.Boolean('Can Moderate', compute='_get_post_karma_rights') + + @api.one + @api.depends('content') + def _get_plain_content(self): + self.plain_content = tools.html2plaintext(self.content)[0:500] if self.content else False + @api.one @api.depends('vote_count', 'forum_id.relevancy_post_vote', 'forum_id.relevancy_time_decay') def _compute_relevancy(self): @@ -226,11 +291,6 @@ class Post(models.Model): else: self.relevancy = 0 - # vote - vote_ids = fields.One2many('forum.post.vote', 'post_id', string='Votes') - user_vote = fields.Integer('My Vote', compute='_get_user_vote') - vote_count = fields.Integer('Votes', compute='_get_vote_count', store=True) - @api.multi def _get_user_vote(self): votes = self.env['forum.post.vote'].search_read([('post_id', 'in', self._ids), ('user_id', '=', self._uid)], ['vote', 'post_id']) @@ -248,11 +308,6 @@ class Post(models.Model): for post in self: post.vote_count = result[post.id] - # favorite - favourite_ids = fields.Many2many('res.users', string='Favourite') - user_favourite = fields.Boolean('Is Favourite', compute='_get_user_favourite') - favourite_count = fields.Integer('Favorite Count', compute='_get_favorite_count', store=True) - @api.one def _get_user_favourite(self): self.user_favourite = self._uid in self.favourite_ids.ids @@ -262,15 +317,6 @@ class Post(models.Model): def _get_favorite_count(self): self.favourite_count = len(self.favourite_ids) - # hierarchy - is_correct = fields.Boolean('Correct', help='Correct answer or answer accepted') - parent_id = fields.Many2one('forum.post', string='Question', ondelete='cascade') - self_reply = fields.Boolean('Reply to own question', compute='_is_self_reply', store=True) - child_ids = fields.One2many('forum.post', 'parent_id', string='Answers') - child_count = fields.Integer('Number of answers', compute='_get_child_count', store=True) - uid_has_answered = fields.Boolean('Has Answered', compute='_get_uid_has_answered') - has_validated_answer = fields.Boolean('Is answered', compute='_get_has_validated_answer', store=True) - @api.one @api.depends('create_uid', 'parent_id') def _is_self_reply(self): @@ -295,29 +341,6 @@ class Post(models.Model): def _get_has_validated_answer(self): self.has_validated_answer = any(answer.is_correct for answer in self.child_ids) - # closing - closed_reason_id = fields.Many2one('forum.post.reason', string='Reason') - closed_uid = fields.Many2one('res.users', string='Closed by', select=1) - closed_date = fields.Datetime('Closed on', readonly=True) - # karma calculation and access - karma_accept = fields.Integer('Convert comment to answer', compute='_get_post_karma_rights') - karma_edit = fields.Integer('Karma to edit', compute='_get_post_karma_rights') - karma_close = fields.Integer('Karma to close', compute='_get_post_karma_rights') - karma_unlink = fields.Integer('Karma to unlink', compute='_get_post_karma_rights') - karma_comment = fields.Integer('Karma to comment', compute='_get_post_karma_rights') - karma_comment_convert = fields.Integer('Karma to convert comment to answer', compute='_get_post_karma_rights') - can_ask = fields.Boolean('Can Ask', compute='_get_post_karma_rights') - can_answer = fields.Boolean('Can Answer', compute='_get_post_karma_rights') - can_accept = fields.Boolean('Can Accept', compute='_get_post_karma_rights') - can_edit = fields.Boolean('Can Edit', compute='_get_post_karma_rights') - can_close = fields.Boolean('Can Close', compute='_get_post_karma_rights') - can_unlink = fields.Boolean('Can Unlink', compute='_get_post_karma_rights') - can_upvote = fields.Boolean('Can Upvote', compute='_get_post_karma_rights') - can_downvote = fields.Boolean('Can Downvote', compute='_get_post_karma_rights') - can_comment = fields.Boolean('Can Comment', compute='_get_post_karma_rights') - can_comment_convert = fields.Boolean('Can Convert to Comment', compute='_get_post_karma_rights') - can_view = fields.Boolean('Can View', compute='_get_post_karma_rights') - can_display_biography = fields.Boolean('Can userbiography of the author be viewed', compute='_get_post_karma_rights') @api.multi def _get_post_karma_rights(self): @@ -347,6 +370,9 @@ class Post(models.Model): post.can_comment_convert = is_admin or user.karma >= post.karma_comment_convert post.can_view = is_admin or user.karma >= post.karma_close or post_sudo.create_uid.karma > 0 post.can_display_biography = is_admin or post_sudo.create_uid.karma >= post.forum_id.karma_user_bio + post.can_post = is_admin or user.karma >= post.forum_id.karma_post + post.can_flag = is_admin or user.karma >= post.forum_id.karma_flag + post.can_moderate = is_admin or user.karma >= post.forum_id.karma_moderate @api.one @api.constrains('post_type', 'forum_id') @@ -383,6 +409,8 @@ class Post(models.Model): raise KarmaError('Not enough karma to create a new question') elif post.parent_id and not post.can_answer: raise KarmaError('Not enough karma to answer to a question') + if not post.parent_id and not post.can_post: + post.state = 'pending' # messaging and chatter base_url = self.env['ir.config_parameter'].get_param('web.base.url') if post.parent_id: @@ -397,7 +425,8 @@ class Post(models.Model): (post.name, post.forum_id.name, base_url, slug(post.forum_id), slug(post)) ) post.message_post(subject=post.name, body=body, subtype='website_forum.mt_question_new') - self.env.user.sudo().add_karma(post.forum_id.karma_gen_question_new) + if post.state == 'active': + self.env.user.sudo().add_karma(post.forum_id.karma_gen_question_new) return post @api.model @@ -492,6 +521,66 @@ class Post(models.Model): }) return True + @api.one + def validate(self): + if not self.can_moderate: + raise KarmaError('Not enough karma to validate a post') + + # if state == pending, no karma previously added for the new question + if self.state == 'pending': + self.create_uid.sudo().add_karma(self.forum_id.karma_gen_question_new) + + self.moderator_id = self.env.user + + self.write({ + 'state': 'active', + 'active': True, + }) + return True + + @api.one + def refuse(self): + if not self.can_moderate: + raise KarmaError('Not enough karma to refuse a post') + + self.moderator_id = self.env.user + return True + + @api.one + def flag(self): + if not self.can_flag: + raise KarmaError('Not enough karma to flag a post') + + if(self.state == 'flagged'): + return {'error': 'post_already_flagged'} + elif(self.state == 'active'): + self.write({ + 'state': 'flagged', + 'flag_user_id': self.env.user.id, + }) + return self.can_moderate and {'success': 'post_flagged_moderator'} or {'success': 'post_flagged_non_moderator'} + else: + return {'error': 'post_non_flaggable'} + + @api.one + def mark_as_offensive(self, reason_id): + if not self.can_moderate: + raise KarmaError('Not enough karma to mark a post as offensive') + + # remove some karma + # TODO: consider making this a tunable karma parameter (cfr close) + _logger.info('Downvoting user <%s> for posting spam/offensive contents', self.create_uid) + self.create_uid.sudo().add_karma(self.forum_id.karma_gen_question_downvote * 5) + + self.write({ + 'state': 'offensive', + 'moderator_id': self.env.user.id, + 'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), + 'closed_reason_id': reason_id, + 'active': False, + }) + return True + @api.multi def unlink(self): if any(not post.can_unlink for post in self): @@ -661,6 +750,7 @@ class PostReason(models.Model): _order = 'name' name = fields.Char(string='Closing Reason', required=True, translate=True) + reason_type = fields.Char(string='Reason Type') class Vote(models.Model): diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js index 563ef1e0177..b8afff01be8 100644 --- a/addons/website_forum/static/src/js/website_forum.js +++ b/addons/website_forum/static/src/js/website_forum.js @@ -45,6 +45,50 @@ if(!$('.website_forum').length) { } ); + $('.flag').not('.karma_required').on('click', function (ev) { + ev.preventDefault(); + var $link = $(ev.currentTarget); + ajax.jsonRpc($link.data('href'), 'call', {}) + .then(function (data) { + if(data.error) { + var $warning; + if(data.error == 'anonymous_user') { + $warning = $('
'+ + ''+ + _t('Sorry you must be logged to flag a post') + + '
'); + } else if(data.error == 'post_already_flagged') { + $warning = $('
'+ + ''+ + _t('This post is already flagged') + + '
'); + } else if(data.error == 'post_non_flaggable') { + $warning = $('
'+ + ''+ + _t('This post can not be flagged') + + '
'); + } + var flag_alert = $link.parent().find("#flag_alert"); + if (flag_alert.length === 0) { + $link.parent().append($warning); + } + } else if(data.success) { + var elem = $link; + if(data.success == 'post_flagged_moderator') { + elem.html(' Flagged'); + var c = parseInt($('#count_flagged_posts').html(), 10); + c++; + $('#count_flagged_posts').html(c); + } else if(data.success == 'post_flagged_non_moderator') { + elem.html(' Flagged'); + var forum_answer = elem.closest('.forum_answer'); + forum_answer.fadeIn(1000); + forum_answer.slideUp(1000); + } + } + }); + }); + $('.vote_up,.vote_down').not('.karma_required').on('click', function (ev) { ev.preventDefault(); var $link = $(ev.currentTarget); @@ -278,4 +322,4 @@ if(!$('.website_forum').length) { }); }); -}); +}); \ No newline at end of file diff --git a/addons/website_forum/tests/common.py b/addons/website_forum/tests/common.py index 8b28d7fa9b5..e0f143ac293 100644 --- a/addons/website_forum/tests/common.py +++ b/addons/website_forum/tests/common.py @@ -10,6 +10,7 @@ KARMA = { 'edit_own': 10, 'edit_all': 20, 'close_own': 10, 'close_all': 20, 'unlink_own': 10, 'unlink_all': 20, + 'post': 100, 'flag': 500, 'moderate': 1000, 'gen_que_new': 1, 'gen_que_upv': 5, 'gen_que_dwv': -10, 'gen_ans_upv': 10, 'gen_ans_dwv': -20, } @@ -71,6 +72,7 @@ class TestForumCommon(common.SavepointCase): 'karma_close_all': KARMA['close_all'], 'karma_unlink_own': KARMA['unlink_own'], 'karma_unlink_all': KARMA['unlink_all'], + 'karma_post': KARMA['post'], 'karma_comment_convert_all': KARMA['com_conv_all'], 'karma_gen_question_new': KARMA['gen_que_new'], 'karma_gen_question_upvote': KARMA['gen_que_upv'], diff --git a/addons/website_forum/tests/test_forum.py b/addons/website_forum/tests/test_forum.py index f9a34bad27e..fbe6dc4f6d5 100644 --- a/addons/website_forum/tests/test_forum.py +++ b/addons/website_forum/tests/test_forum.py @@ -34,7 +34,15 @@ class TestForum(TestForumCommon): 'forum_id': self.forum.id, 'tag_ids': [(0, 0, {'name': 'Tag1', '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') + self.assertEqual(self.user_portal.karma, KARMA['ask'], 'website_forum: wrong karma generation when asking question') + + self.user_portal.karma = KARMA['post'] + Post.sudo(self.user_portal).create({ + 'name': " Q0", + 'forum_id': self.forum.id, + 'tag_ids': [(0, 0, {'name': 'Tag42', 'forum_id': self.forum.id})] + }) + self.assertEqual(self.user_portal.karma, KARMA['post'] + 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): @@ -113,6 +121,97 @@ class TestForum(TestForumCommon): self.post.sudo(self.user_employee).message_post(body='Test1', message_type='comment') self.assertEqual(len(self.post.message_ids), 4, 'website_forum: wrong behavior of message_post') + def test_flag_a_post(self): + Post = self.env['forum.post'] + self.user_portal.karma = KARMA['ask'] + post = Post.sudo(self.user_portal).create({ + 'name': "Q0", + 'forum_id': self.forum.id, + }) + + # portal user flags a post: not allowed, unsufficient karma + with self.assertRaises(KarmaError): + post.sudo(self.user_portal).flag() + + # portal user flags a post: ok if enough karma + self.user_portal.karma = KARMA['flag'] + post.state = 'active' + post.sudo(self.user_portal).flag() + self.assertEqual(post.state, 'flagged', 'website_forum: wrong state when flagging a post') + + def test_validate_a_post(self): + Post = self.env['forum.post'] + self.user_portal.karma = KARMA['ask'] + post = Post.sudo(self.user_portal).create({ + 'name': "Q0", + 'forum_id': self.forum.id, + }) + + # portal user validate a post: not allowed, unsufficient karma + with self.assertRaises(KarmaError): + post.sudo(self.user_portal).validate() + + # portal user validate a pending post + self.user_portal.karma = KARMA['moderate'] + post.state = 'pending' + init_karma = post.create_uid.karma + post.sudo(self.user_portal).validate() + self.assertEqual(post.state, 'active', 'website_forum: wrong state when validate a post after pending') + self.assertEqual(post.create_uid.karma, init_karma + KARMA['gen_que_new'], 'website_forum: wrong karma when validate a post after pending') + + # portal user validate a flagged post: ok if enough karma + self.user_portal.karma = KARMA['moderate'] + post.state = 'flagged' + post.sudo(self.user_portal).validate() + self.assertEqual(post.state, 'active', 'website_forum: wrong state when validate a post after flagged') + + # portal user validate an offensive post: ok if enough karma + self.user_portal.karma = KARMA['moderate'] + post.state = 'offensive' + init_karma = post.create_uid.karma + post.sudo(self.user_portal).validate() + self.assertEqual(post.state, 'active', 'website_forum: wrong state when validate a post after offensive') + + def test_refuse_a_post(self): + Post = self.env['forum.post'] + self.user_portal.karma = KARMA['ask'] + post = Post.sudo(self.user_portal).create({ + 'name': "Q0", + 'forum_id': self.forum.id, + }) + + # portal user validate a post: not allowed, unsufficient karma + with self.assertRaises(KarmaError): + post.sudo(self.user_portal).refuse() + + # portal user validate a pending post + self.user_portal.karma = KARMA['moderate'] + post.state = 'pending' + init_karma = post.create_uid.karma + post.sudo(self.user_portal).refuse() + self.assertEqual(post.moderator_id, self.user_portal, 'website_forum: wrong moderator_id when refusing') + self.assertEqual(post.create_uid.karma, init_karma, 'website_forum: wrong karma when refusing a post') + + def test_mark_a_post_as_offensive(self): + Post = self.env['forum.post'] + self.user_portal.karma = KARMA['ask'] + post = Post.sudo(self.user_portal).create({ + 'name': "Q0", + 'forum_id': self.forum.id, + }) + + # portal user mark a post as offensive: not allowed, unsufficient karma + with self.assertRaises(KarmaError): + post.sudo(self.user_portal).mark_as_offensive(12) + + # portal user mark a post as offensive + self.user_portal.karma = KARMA['moderate'] + post.state = 'flagged' + init_karma = post.create_uid.karma + post.sudo(self.user_portal).mark_as_offensive(12) + self.assertEqual(post.state, 'offensive', 'website_forum: wrong state when marking a post as offensive') + self.assertEqual(post.create_uid.karma, init_karma + 5 * KARMA['gen_que_dwv'], 'website_forum: wrong karma when marking a post as offensive') + def test_convert_answer_to_comment_crash(self): Post = self.env['forum.post'] diff --git a/addons/website_forum/views/forum.xml b/addons/website_forum/views/forum.xml index 41569412842..a8b45e64c04 100644 --- a/addons/website_forum/views/forum.xml +++ b/addons/website_forum/views/forum.xml @@ -72,10 +72,10 @@ - - + + @@ -83,6 +83,9 @@ + + + diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml index ecb3765b2fe..7f261ce41d2 100644 --- a/addons/website_forum/views/website_forum.xml +++ b/addons/website_forum/views/website_forum.xml @@ -166,6 +166,16 @@ Back to +
+
+

Moderation Tools

+
+ +
@@ -207,6 +217,53 @@ + + @@ -272,6 +292,37 @@
+ + + + +