[IMP] website_forum: moderation & anti spam

This commit is contained in:
Julien Louette
2015-09-05 14:44:06 +02:00
committed by Jeremy Kersten
parent e301e0817e
commit 205ce98656
8 changed files with 626 additions and 105 deletions
+107 -2
View File
@@ -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/<model("forum.forum"):forum>/question/<model("forum.post"):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/<model("forum.forum"):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/<model("forum.forum"):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/<model("forum.forum"):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/<model("forum.forum"):forum>/post/<model("forum.post"):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/<model("forum.forum"):forum>/post/<model("forum.post"):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/<model("forum.forum"):forum>/post/<model("forum.post"):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/<model("forum.forum"):forum>/post/<model("forum.post"):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/<model("forum.forum"):forum>/post/<model("forum.post"):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
# --------------------------------------------------
+28
View File
@@ -83,27 +83,55 @@
<!-- Reasons for closing Post -->
<record id="reason_1" model="forum.post.reason">
<field name="name">duplicate post</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_2" model="forum.post.reason">
<field name="name">off-topic or not relevant</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_3" model="forum.post.reason">
<field name="name">too subjective and argumentative</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_4" model="forum.post.reason">
<field name="name">not a real post</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_6" model="forum.post.reason">
<field name="name">not relevant or out dated</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_7" model="forum.post.reason">
<field name="name">contains offensive or malicious remarks</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_8" model="forum.post.reason">
<field name="name">spam or advertising</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_9" model="forum.post.reason">
<field name="name">too localized</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_11" model="forum.post.reason">
<field name="name">insulting and offensive language</field>
<field name="reason_type">offensive</field>
</record>
<record id="reason_12" model="forum.post.reason">
<field name="name">violent language</field>
<field name="reason_type">offensive</field>
</record>
<record id="reason_13" model="forum.post.reason">
<field name="name">inappropriate and unacceptable statements</field>
<field name="reason_type">offensive</field>
</record>
<record id="reason_14" model="forum.post.reason">
<field name="name">threatening language</field>
<field name="reason_type">offensive</field>
</record>
<record id="reason_15" model="forum.post.reason">
<field name="name">racist and hate speech</field>
<field name="reason_type">offensive</field>
</record>
<!-- Email template for email validation (for karma purpose) -->
+141 -51
View File
@@ -63,7 +63,7 @@ class Forum(models.Model):
<p class="text-muted text-center" style="text-align: left;">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.</p>
</div>
<div class="col-md-12">
     <a href="#" class="js_close_intro">Hide Intro</a>    <a class="btn btn-primary forum_register_url" href="/web/login">Register</a> </div>
<a href="#" class="js_close_intro">Hide Intro</a> <a class="btn btn-primary forum_register_url" href="/web/login">Register</a> </div>
</div>
</div>
</section>""")
@@ -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):
@@ -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 = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="flag_alert">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
_t('Sorry you must be logged to flag a post') +
'</div>');
} else if(data.error == 'post_already_flagged') {
$warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="flag_alert">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
_t('This post is already flagged') +
'</div>');
} else if(data.error == 'post_non_flaggable') {
$warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="flag_alert">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
_t('This post can not be flagged') +
'</div>');
}
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) {
});
});
});
});
+2
View File
@@ -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'],
+100 -1
View File
@@ -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']
+5 -2
View File
@@ -72,10 +72,10 @@
<field name="karma_unlink_own"/>
<field name="karma_unlink_all"/>
<field name="karma_dofollow"/>
</group>
<group>
<field name="karma_answer_accept_own"/>
<field name="karma_answer_accept_all"/>
</group>
<group>
<field name="karma_editor"/>
<field name="karma_comment_own"/>
<field name="karma_comment_all"/>
@@ -83,6 +83,9 @@
<field name="karma_comment_convert_all"/>
<field name="karma_comment_unlink_own"/>
<field name="karma_comment_unlink_all"/>
<field name="karma_post"/>
<field name="karma_flag"/>
<field name="karma_moderate"/>
</group>
</group>
</page>
+198 -48
View File
@@ -166,6 +166,16 @@
<a t-if="header.get('is_guidelines')" t-attf-href="/forum/#{slug(forum)}" class="fa fa-arrow-right"> Back to <span t-field="forum.name"/></a>
</div>
</div>
<div class="panel panel-default" id="moderation_tools" t-if="user.karma>=forum.karma_moderate">
<div class="panel-heading">
<h3 class="panel-title">Moderation Tools</h3>
</div>
<div class="panel-body text-center">
<a t-attf-href="/forum/#{slug(forum)}/validation_queue"><strong><t t-esc="forum.count_posts_waiting_validation"/></strong> waiting for validation</a>
<hr style="margin-top:4px;margin-bottom:0;padding-bottom:4px;" />
<a t-attf-href="/forum/#{slug(forum)}/flagged_queue"><strong id="count_flagged_posts"><t t-esc="forum.count_flagged_posts"/></strong> post<t t-if="forum.count_flagged_posts&gt;1">s</t> flagged</a>
</div>
</div>
<div t-if="header.get('question_data')">
<div class="panel panel-default">
<div class="panel-heading text-center">
@@ -207,6 +217,53 @@
</template>
<!-- Display a post -->
<template id="display_post_question_block">
<div class="question-block">
<div class="question-name">
<a t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }">
<span t-field="question.name"/>
</a>
<span t-if="not question.active and question.state!='offensive'"><b> [Deleted]</b></span>
<span t-if="not question.active and question.state=='offensive'"><b> [Offensive]</b></span>
<span t-if="not question.active and question.state=='offensive' and question.closed_reason_id"><b> [<t t-esc="question.closed_reason_id.name[0].upper() + question.closed_reason_id.name[1:]"/>]</b></span>
<span t-if="question.state == 'close'"><b> [Closed]</b></span>
<t t-if="question.post_type == 'link'" >
<a title="Redirect to external link" t-att-href="question.content_link" class="text-muted"><i class="fa fa-external-link forum_link"/></a>
</t>
<span t-if="question.has_validated_answer" title="Question has accepted answer"><i class="fa fa-check-circle oe_answer_true"/></span>
</div>
<div><t t-raw="post_content"/></div>
<t t-foreach="question.tag_ids" t-as="question_tag">
<a t-attf-href="/forum/#{ slug(forum) }/tag/#{slug(question_tag)}/questions">
<span t-attf-class="pull-right label #{tag and tag.name == question_tag.name and 'label-primary' or 'label-default'}" t-field="question_tag.name"
style="margin-right: 4px;"/>
</a>
</t>
<small class="text-muted">
<i t-if="question.post_type == 'question'" t-attf-class="fa fa-question-circle" t-att-title="question.post_type.capitalize()" />
<i t-if="question.post_type == 'discussion'" t-attf-class="fa fa-comments-o" t-att-title="question.post_type.capitalize()" />
<i t-if="question.post_type == 'link'" t-attf-class="fa fa-link" t-att-title="question.post_type.capitalize()" />
<span>By </span><span t-field="question.create_uid" t-field-options='{"widget": "contact", "country_image": true, "fields": ["name", "country_id"]}' style="display: inline-block;"/>
<span> • </span><span t-field="question.write_date" t-field-options='{"format":"short"}'/>
<span> • </span><span t-field="question.views" /> View<t t-if="question.views&gt;1">s</t>
<span> • </span><a t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }">
<t t-esc="question.child_count"/>
<t t-if="question.post_type == 'question'">
Answer<t t-if="question.child_count&gt;1">s</t>
</t>
<t t-if="question.post_type != 'question'">
Comment<t t-if="question.child_count&gt;1">s</t>
</t>
</a>
<t t-if="question.state == 'flagged'">
<span> • </span>
Flagged
</t>
</small>
</div>
</template>
<template id="display_post">
<div class="question">
<div class="pull-left">
@@ -214,44 +271,7 @@
<t t-set="post" t-value="question"/>
</t>
</div>
<div class="question-block">
<div class="question-name">
<a t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }">
<span t-field="question.name"/>
</a>
<span t-if="not question.active"><b> [Deleted]</b></span>
<span t-if="question.state == 'close'"><b> [Closed]</b></span>
<t t-if="question.post_type == 'link'" >
<a title="Redirect to external link" t-att-href="question.content_link" class="text-muted"><i class="fa fa-external-link forum_link"/></a>
</t>
<span t-if="question.has_validated_answer" title="Question has accepted answer"><i class="fa fa-check-circle oe_answer_true"/></span>
</div>
<t t-foreach="question.tag_ids" t-as="question_tag">
<a t-attf-href="/forum/#{ slug(forum) }/tag/#{slug(question_tag)}/questions">
<span t-attf-class="pull-right label #{tag and tag.name == question_tag.name and 'label-primary' or 'label-default'}" t-field="question_tag.name"
style="margin-right: 4px;"/>
</a>
</t>
<small class="text-muted">
<i t-if="question.post_type == 'question'" t-attf-class="fa fa-question-circle" t-att-title="question.post_type.capitalize()" />
<i t-if="question.post_type == 'discussion'" t-attf-class="fa fa-comments-o" t-att-title="question.post_type.capitalize()" />
<i t-if="question.post_type == 'link'" t-attf-class="fa fa-link" t-att-title="question.post_type.capitalize()" />
<span>By </span><span t-field="question.create_uid" t-field-options='{"widget": "contact", "country_image": true, "fields": ["name", "country_id"]}' style="display: inline-block;"/>
<span> • </span><span t-field="question.write_date" t-field-options='{"format":"short"}'/>
<span> • </span><span t-field="question.views" /> View<t t-if="question.views&gt;1">s</t>
<span> • </span><a t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }">
<t t-esc="question.child_count"/>
<t t-if="question.post_type == 'question'">
Answer<t t-if="question.child_count&gt;1">s</t>
</t>
<t t-if="question.post_type != 'question'">
Comment<t t-if="question.child_count&gt;1">s</t>
</t>
</a>
</small>
</div>
<t t-call="website_forum.display_post_question_block"/>
</div>
</template>
@@ -272,6 +292,37 @@
</div>
</template>
<!-- Moderation tools -->
<template id="moderation_display_post_question_block">
<t t-call="website_forum.display_post_question_block">
<t t-set="post_content">
<div class="clearfix"><span t-field="question.content" class="oe_no_empty"/></div>
</t>
</t>
</template>
<template id="moderation_display_post_answer">
<div class="clearfix">
<div class="question-name">
<a style="font-size: 15px;" t-attf-href="/forum/#{ slug(forum) }/question/#{ answer.parent_id.id }/#answer-#{ answer.id }" t-esc="answer.parent_id.name"/>
<b>[Answer]</b>
<span t-if="not answer.active and answer.state=='offensive'"><b> [Offensive]</b></span>
<span t-if="not answer.active and answer.state=='offensive' and answer.closed_reason_id"><b> [<t t-esc="answer.closed_reason_id.name[0].upper() + answer.closed_reason_id.name[1:]"/>]</b></span>
<t t-if="answer.state == 'flagged'">
<small class="text-muted">
Flagged
</small>
</t>
<t t-if="len(answer.website_message_ids)&gt;0">
(<t t-esc="len(answer.website_message_ids)"/>
<t t-if="len(answer.website_message_ids)&gt;1"> Comments</t>
<t t-if="len(answer.website_message_ids)&lt;=1"> Comment</t>)
</t>
</div>
<div class="clearfix"><span t-field="answer.content" class="oe_no_empty"/></div>
</div>
</template>
<!-- FAQ Layout -->
<template id="faq">
<t t-call="website_forum.header">
@@ -557,18 +608,28 @@
<!-- Moderation: close a post -->
<template id="close_post">
<t t-call="website_forum.header">
<h1 class="mt0">Close Post</h1>
<p class="text-muted">
<h1 class="mt0"><t t-esc="offensive and 'Offensive' or 'Close'"/> Post</h1>
<p class="text-muted" t-if="not offensive">
If you close this post, it will be hidden for most users. Only
users having a high karma can see closed posts to moderate
them.
</p>
<form t-attf-action="/forum/#{ slug(forum) }/question/#{slug(question)}/close" method="post" role="form" class="form-horizontal mt32 mb64 js_website_submit_form">
<p class="text-muted" t-if="offensive">
If you mark this post as offensive, it will be hidden for most users. Only
users having a high karma can see offensive posts to moderate
them.
</p>
<form t-attf-action="/forum/#{ slug(forum) }/#{offensive and 'post' or 'question'}/#{slug(question)}/#{offensive and 'mark_as_offensive' or 'close'}" method="post" role="form" class="form-horizontal mt32 mb64 js_website_submit_form">
<input name="post_id" t-att-value="question.id" type="hidden"/>
<div class="form-group">
<label class="col-md-3 control-label" for="reason">Post:</label>
<div class="col-md-8 mt8">
<span t-field="question.name"/>
<t t-if="not question.parent_id">
<span t-field="question.name"/>
</t>
<t t-if="question.parent_id">
<span t-field="question.parent_id.name"/>
</t>
</div>
</div>
<div class="form-group">
@@ -583,7 +644,7 @@
</div>
<div class="form-group">
<div class="col-md-offset-3 col-md-8">
<button type="submit" class="btn btn-primary">Close post</button>
<button type="submit" class="btn btn-primary"><t t-esc="offensive and 'Mark as offensive' or 'Close post'"/></button>
<span class="text-muted">or</span>
<a class="btn btn-link" t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }">back to post</a>
</div>
@@ -648,6 +709,13 @@
<!-- Specific Post Layout -->
<template id="post_description_full" name="Question Navigation">
<t t-call="website_forum.header">
<div class="alert alert-info text-center" role="alert" t-if="forum and question.state == 'pending' and user.karma>=forum.karma_moderate and question.active">
<a t-attf-href="/forum/#{slug(forum)}/post/#{slug(question)}/validate" type="button" class="btn btn-success">Accept <i class="fa fa-check"/></a>
<a t-attf-href="/forum/#{slug(forum)}/post/#{slug(question)}/refuse" type="button" class="btn btn-danger">Reject <i class="fa fa-times"/></a>
</div>
<div class="alert alert-info text-center" role="alert" t-if="question.state == 'pending' and user.karma &lt; forum.karma_moderate">
Waiting for validation
</div>
<div t-attf-class="question #{can_bump and 'oe_js_bump' or ''}" t-att-data-type="question.post_type" t-att-data-last-update="question.write_date" t-att-data-id="question.id">
<div class="pull-left">
<t t-call="website_forum.vote">
@@ -670,7 +738,8 @@
<t t-esc="question.name"/>
</a>
</t>
<span t-if="not question.active"><b> [Deleted]</b></span>
<span t-if="not question.active and question.state!='offensive'"><b> [Deleted]</b></span>
<span t-if="not question.active and question.state=='offensive'"><b> [Offensive]</b></span>
<span t-if="question.state == 'close'"><b> [Closed]</b></span>
</h1>
<div class="pull-right">
@@ -749,7 +818,7 @@
<t t-set="karma" t-value="not question.can_unlink and question.karma_unlink or 0"/>
</t>
</li>
<li t-if="not question.active">
<li t-if="not question.active and question.state != 'offensive'">
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) +'/question/' + slug(question) + '/undelete'"/>
<t t-set="label" t-value="'Reactivate'"/>
@@ -757,6 +826,23 @@
<t t-set="karma" t-value="not question.can_unlink and question.karma_unlink or 0"/>
</t>
</li>
<li t-if="not question.active and question.state == 'offensive'">
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) +'/post/' + slug(question) + '/validate'"/>
<t t-set="label" t-value="'Reactivate'"/>
<t t-set="classes" t-value="'fa-trash-o'"/>
<t t-set="karma" t-value="not question.can_moderate and question.forum_id.karma_moderate or 0"/>
</t>
</li>
<li t-if="question.active">
<a t-attf-style="cursor: pointer;#{not question.can_flag and 'color:#999;' or ''}"
t-attf-class="flag fa fa-flag #{not question.can_flag and 'karma_required' or ''}"
t-attf-data-karma="#{not question.can_flag and question.forum_id.karma_flag or 0}"
t-attf-data-href="/forum/#{slug(forum)}/post/#{slug(question)}/flag"
>
<t class="flag_flagged_label" t-esc="question.state == 'flagged' and 'Flagged' or 'Flag'"/>
</a>
</li>
</ul>
<ul class="list-inline mb0">
<li t-if="(question.post_type != 'question' or question.post_type == 'question' and not question.uid_has_answered) and question.state != 'close' and question.active != False">
@@ -783,9 +869,11 @@
</div>
</div>
<div t-foreach="question.child_ids" t-as="post_answer" class="mt16 mb32">
<hr class="mb4 mt4"/>
<t t-call="website_forum.post_answers">
<t t-set="answer" t-value="post_answer"/>
<t t-if="post_answer.state != 'flagged' or (post_answer.state == 'flagged' and post_answer.can_moderate)">
<hr class="mb4 mt4"/>
<t t-call="website_forum.post_answers">
<t t-set="answer" t-value="post_answer"/>
</t>
</t>
</div>
<div id="post_reply"
@@ -848,6 +936,15 @@
<t t-set="karma" t-value="not answer.can_unlink and answer.karma_unlink or 0"/>
</t>
</li>
<li>
<a t-attf-style="cursor: pointer;#{not answer.can_flag and 'color:#999;' or ''}"
t-attf-class="flag fa fa-flag #{not answer.can_flag and 'karma_required' or ''}"
t-attf-data-karma="#{not answer.can_flag and answer.forum_id.karma_flag or 0}"
t-attf-data-href="/forum/#{slug(forum)}/post/#{slug(answer)}/flag"
>
<t class="flag_flagged_label" t-esc="answer.state == 'flagged' and 'Flagged' or 'Flag'"/>
</a>
</li>
<li t-if="question.post_type == 'question'">
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/convert_to_comment'"/>
@@ -1047,6 +1144,59 @@
</t>
</template>
<template id="moderation_queue">
<t t-call="website_forum.header">
<h2 class="page-header mt0">
<t t-set="count_posts" t-value="len(posts_ids)"/>
<t t-esc="count_posts"/>
<span class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<t t-if="queue_type == 'validation'"> Pending</t>
<t t-if="queue_type == 'flagged'"> Flagged</t>
<t t-if="queue_type == 'offensive'"> Offensive</t>
<b class="caret"/>
</a>
<ul class="dropdown-menu">
<li t-att-class="queue_type == 'validation' and 'active' or '' ">
<a t-attf-href="/forum/#{slug(forum)}/validation_queue">Pending</a>
</li>
<li t-att-class="queue_type == 'flagged' and 'active' or '' ">
<a t-attf-href="/forum/#{slug(forum)}/flagged_queue">Flagged</a>
</li>
<li t-if="uid" t-att-class="queue_type == 'offensive' and 'active' or '' ">
<a t-attf-href="/forum/#{slug(forum)}/offensive_posts">Offensive</a>
</li>
</ul>
</span>
</h2>
<div class="text-center" t-if="count_posts == 0">
<t t-if="queue_type == 'validation'">The validation queue is empty.</t>
<t t-if="queue_type == 'flagged'">The flagged queue is empty.</t>
<t t-if="queue_type == 'offensive'">The offensive queue is empty.</t>
</div>
<div t-foreach="posts_ids" t-as="question" class="mb16 row" style="padding-bottom:16px;border-bottom:1px solid #e7e7e7;">
<div class="col-sm-2">
<div class="panel panel-default text-center" style="background-color:#eee;margin:0;padding:5px 12px 5px 12px;">
<a t-attf-href="/forum/#{slug(forum)}/post/#{slug(question)}/validate" style="padding-right:8px;"><i class="fa fa-check fa-2x"/></a>
<a t-if="queue_type == 'validation'" t-attf-href="/forum/#{slug(forum)}/post/#{slug(question)}/refuse"><i class="fa fa-times fa-2x"/></a>
<a t-if="queue_type == 'flagged'" t-attf-href="/forum/#{slug(forum)}/post/#{slug(question)}/ask_for_mark_as_offensive"><i class="fa fa-times fa-2x"/></a>
<a href="#" t-if="queue_type == 'offensive'" disabled="True"><i class="fa fa-times fa-2x" style="color:rgb(179, 179, 179);"/></a>
</div>
</div>
<div class="col-sm-10">
<t t-if="question.parent_id">
<div t-foreach="question" t-as="answer" style="display: inline-block;">
<t t-call="website_forum.moderation_display_post_answer"/>
</div>
</t>
<t t-if="not question.parent_id" style="display: inline-block;margin-left:0;">
<t t-call="website_forum.moderation_display_post_question_block"/>
</t>
</div>
</div>
</t>
</template>
<template id="users">
<t t-call="website_forum.header">
<div t-foreach="users" t-as="row_users" class="row mt16">