[FIX] website_forum: fixed issue with user vote badly taken into account when upvotin...
[odoo/odoo.git] / addons / website_forum / models / forum.py
1 # -*- coding: utf-8 -*-
2
3 from datetime import datetime
4
5 import openerp
6 from openerp import tools
7 from openerp import SUPERUSER_ID
8 from openerp.addons.website.models.website import slug
9 from openerp.osv import osv, fields
10 from openerp.tools import html2plaintext
11 from openerp.tools.translate import _
12
13
14 class KarmaError(ValueError):
15     """ Karma-related error, used for forum and posts. """
16     pass
17
18
19 class Forum(osv.Model):
20     """TDE TODO: set karma values for actions dynamic for a given forum"""
21     _name = 'forum.forum'
22     _description = 'Forums'
23     _inherit = ['mail.thread', 'website.seo.metadata']
24
25     _columns = {
26         'name': fields.char('Name', required=True, translate=True),
27         'faq': fields.html('Guidelines'),
28         'description': fields.html('Description'),
29         # karma generation
30         'karma_gen_question_new': fields.integer('Karma earned for new questions'),
31         'karma_gen_question_upvote': fields.integer('Karma earned for upvoting a question'),
32         'karma_gen_question_downvote': fields.integer('Karma earned for downvoting a question'),
33         'karma_gen_answer_upvote': fields.integer('Karma earned for upvoting an answer'),
34         'karma_gen_answer_downvote': fields.integer('Karma earned for downvoting an answer'),
35         'karma_gen_answer_accept': fields.integer('Karma earned for accepting an anwer'),
36         'karma_gen_answer_accepted': fields.integer('Karma earned for having an answer accepted'),
37         'karma_gen_answer_flagged': fields.integer('Karma earned for having an answer flagged'),
38         # karma-based actions
39         'karma_ask': fields.integer('Karma to ask a new question'),
40         'karma_answer': fields.integer('Karma to answer a question'),
41         'karma_edit_own': fields.integer('Karma to edit its own posts'),
42         'karma_edit_all': fields.integer('Karma to edit all posts'),
43         'karma_close_own': fields.integer('Karma to close its own posts'),
44         'karma_close_all': fields.integer('Karma to close all posts'),
45         'karma_unlink_own': fields.integer('Karma to delete its own posts'),
46         'karma_unlink_all': fields.integer('Karma to delete all posts'),
47         'karma_upvote': fields.integer('Karma to upvote'),
48         'karma_downvote': fields.integer('Karma to downvote'),
49         'karma_answer_accept_own': fields.integer('Karma to accept an answer on its own questions'),
50         'karma_answer_accept_all': fields.integer('Karma to accept an answers to all questions'),
51         'karma_editor_link_files': fields.integer('Karma for linking files (Editor)'),
52         'karma_editor_clickable_link': fields.integer('Karma for clickable links (Editor)'),
53         'karma_comment_own': fields.integer('Karma to comment its own posts'),
54         'karma_comment_all': fields.integer('Karma to comment all posts'),
55         'karma_comment_convert_own': fields.integer('Karma to convert its own answers to comments and vice versa'),
56         'karma_comment_convert_all': fields.integer('Karma to convert all answers to answers and vice versa'),
57         'karma_comment_unlink_own': fields.integer('Karma to unlink its own comments'),
58         'karma_comment_unlink_all': fields.integer('Karma to unlinnk all comments'),
59         'karma_retag': fields.integer('Karma to change question tags'),
60         'karma_flag': fields.integer('Karma to flag a post as offensive'),
61     }
62
63     def _get_default_faq(self, cr, uid, context=None):
64         fname = openerp.modules.get_module_resource('website_forum', 'data', 'forum_default_faq.html')
65         with open(fname, 'r') as f:
66             return f.read()
67         return False
68
69     _defaults = {
70         'description': 'This community is for professionals and enthusiasts of our products and services.',
71         'faq': _get_default_faq,
72         'karma_gen_question_new': 2,
73         'karma_gen_question_upvote': 5,
74         'karma_gen_question_downvote': -2,
75         'karma_gen_answer_upvote': 10,
76         'karma_gen_answer_downvote': -2,
77         'karma_gen_answer_accept': 2,
78         'karma_gen_answer_accepted': 15,
79         'karma_gen_answer_flagged': -100,
80         'karma_ask': 0,
81         'karma_answer': 0,
82         'karma_edit_own': 1,
83         'karma_edit_all': 300,
84         'karma_close_own': 100,
85         'karma_close_all': 500,
86         'karma_unlink_own': 500,
87         'karma_unlink_all': 1000,
88         'karma_upvote': 5,
89         'karma_downvote': 50,
90         'karma_answer_accept_own': 20,
91         'karma_answer_accept_all': 500,
92         'karma_editor_link_files': 20,
93         'karma_editor_clickable_link': 20,
94         'karma_comment_own': 1,
95         'karma_comment_all': 1,
96         'karma_comment_convert_own': 50,
97         'karma_comment_convert_all': 500,
98         'karma_comment_unlink_own': 50,
99         'karma_comment_unlink_all': 500,
100         'karma_retag': 75,
101         'karma_flag': 500,
102     }
103
104     def create(self, cr, uid, values, context=None):
105         if context is None:
106             context = {}
107         create_context = dict(context, mail_create_nolog=True)
108         return super(Forum, self).create(cr, uid, values, context=create_context)
109
110
111 class Post(osv.Model):
112     _name = 'forum.post'
113     _description = 'Forum Post'
114     _inherit = ['mail.thread', 'website.seo.metadata']
115     _order = "is_correct DESC, vote_count DESC"
116
117     def _get_user_vote(self, cr, uid, ids, field_name, arg, context):
118         res = dict.fromkeys(ids, 0)
119         vote_ids = self.pool['forum.post.vote'].search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context)
120         for vote in self.pool['forum.post.vote'].browse(cr, uid, vote_ids, context=context):
121             res[vote.post_id.id] = vote.vote
122         return res
123
124     def _get_vote_count(self, cr, uid, ids, field_name, arg, context):
125         res = dict.fromkeys(ids, 0)
126         for post in self.browse(cr, uid, ids, context=context):
127             for vote in post.vote_ids:
128                 res[post.id] += int(vote.vote)
129         return res
130
131     def _get_post_from_vote(self, cr, uid, ids, context=None):
132         result = {}
133         for vote in self.pool['forum.post.vote'].browse(cr, uid, ids, context=context):
134             result[vote.post_id.id] = True
135         return result.keys()
136
137     def _get_user_favourite(self, cr, uid, ids, field_name, arg, context):
138         res = dict.fromkeys(ids, False)
139         for post in self.browse(cr, uid, ids, context=context):
140             if uid in [f.id for f in post.favourite_ids]:
141                 res[post.id] = True
142         return res
143
144     def _get_favorite_count(self, cr, uid, ids, field_name, arg, context):
145         res = dict.fromkeys(ids, 0)
146         for post in self.browse(cr, uid, ids, context=context):
147             res[post.id] += len(post.favourite_ids)
148         return res
149
150     def _get_post_from_hierarchy(self, cr, uid, ids, context=None):
151         post_ids = set(ids)
152         for post in self.browse(cr, SUPERUSER_ID, ids, context=context):
153             if post.parent_id:
154                 post_ids.add(post.parent_id.id)
155         return list(post_ids)
156
157     def _get_child_count(self, cr, uid, ids, field_name=False, arg={}, context=None):
158         res = dict.fromkeys(ids, 0)
159         for post in self.browse(cr, uid, ids, context=context):
160             if post.parent_id:
161                 res[post.parent_id.id] = len(post.parent_id.child_ids)
162             else:
163                 res[post.id] = len(post.child_ids)
164         return res
165
166     def _get_uid_answered(self, cr, uid, ids, field_name, arg, context=None):
167         res = dict.fromkeys(ids, False)
168         for post in self.browse(cr, uid, ids, context=context):
169             res[post.id] = any(answer.create_uid.id == uid for answer in post.child_ids)
170         return res
171
172     def _get_has_validated_answer(self, cr, uid, ids, field_name, arg, context=None):
173         res = dict.fromkeys(ids, False)
174         ans_ids = self.search(cr, uid, [('parent_id', 'in', ids), ('is_correct', '=', True)], context=context)
175         for answer in self.browse(cr, uid, ans_ids, context=context):
176             res[answer.parent_id.id] = True
177         return res
178
179     def _is_self_reply(self, cr, uid, ids, field_name, arg, context=None):
180         res = dict.fromkeys(ids, False)
181         for post in self.browse(cr, uid, ids, context=context):
182             res[post.id] = post.parent_id and post.parent_id.create_uid == post.create_uid or False
183         return res
184
185     def _get_post_karma_rights(self, cr, uid, ids, field_name, arg, context=None):
186         user = self.pool['res.users'].browse(cr, uid, uid, context=context)
187         res = dict.fromkeys(ids, False)
188         for post in self.browse(cr, uid, ids, context=context):
189             res[post.id] = {
190                 'karma_ask': post.forum_id.karma_ask,
191                 'karma_answer': post.forum_id.karma_answer,
192                 '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,
193                 'karma_edit': post.create_uid.id == uid and post.forum_id.karma_edit_own or post.forum_id.karma_edit_all,
194                 'karma_close': post.create_uid.id == uid and post.forum_id.karma_close_own or post.forum_id.karma_close_all,
195                 'karma_unlink': post.create_uid.id == uid and post.forum_id.karma_unlink_own or post.forum_id.karma_unlink_all,
196                 'karma_upvote': post.forum_id.karma_upvote,
197                 'karma_downvote': post.forum_id.karma_downvote,
198                 'karma_comment': post.create_uid.id == uid and post.forum_id.karma_comment_own or post.forum_id.karma_comment_all,
199                 'karma_comment_convert': post.create_uid.id == uid and post.forum_id.karma_comment_convert_own or post.forum_id.karma_comment_convert_all,
200             }
201             res[post.id].update({
202                 'can_ask': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_ask'],
203                 'can_answer': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_answer'],
204                 'can_accept': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_accept'],
205                 'can_edit': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_edit'],
206                 'can_close': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_close'],
207                 'can_unlink': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_unlink'],
208                 'can_upvote': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_upvote'],
209                 'can_downvote': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_downvote'],
210                 'can_comment': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_comment'],
211                 'can_comment_convert': uid == SUPERUSER_ID or user.karma >= res[post.id]['karma_comment_convert'],
212             })
213         return res
214
215     _columns = {
216         'name': fields.char('Title'),
217         'forum_id': fields.many2one('forum.forum', 'Forum', required=True),
218         'content': fields.html('Content'),
219         'tag_ids': fields.many2many('forum.tag', 'forum_tag_rel', 'forum_id', 'forum_tag_id', 'Tags'),
220         'state': fields.selection([('active', 'Active'), ('close', 'Close'), ('offensive', 'Offensive')], 'Status'),
221         'views': fields.integer('Number of Views'),
222         'active': fields.boolean('Active'),
223         'is_correct': fields.boolean('Valid Answer', help='Correct Answer or Answer on this question accepted.'),
224         'website_message_ids': fields.one2many(
225             'mail.message', 'res_id',
226             domain=lambda self: [
227                 '&', ('model', '=', self._name), ('type', 'in', ['email', 'comment'])
228             ],
229             string='Post Messages', help="Comments on forum post",
230         ),
231         # history
232         'create_date': fields.datetime('Asked on', select=True, readonly=True),
233         'create_uid': fields.many2one('res.users', 'Created by', select=True, readonly=True),
234         'write_date': fields.datetime('Update on', select=True, readonly=True),
235         'write_uid': fields.many2one('res.users', 'Updated by', select=True, readonly=True),
236         # vote fields
237         'vote_ids': fields.one2many('forum.post.vote', 'post_id', 'Votes'),
238         'user_vote': fields.function(_get_user_vote, string='My Vote', type='integer'),
239         'vote_count': fields.function(
240             _get_vote_count, string="Votes", type='integer',
241             store={
242                 'forum.post': (lambda self, cr, uid, ids, c={}: ids, ['vote_ids'], 10),
243                 'forum.post.vote': (_get_post_from_vote, [], 10),
244             }),
245         # favorite fields
246         'favourite_ids': fields.many2many('res.users', string='Favourite'),
247         'user_favourite': fields.function(_get_user_favourite, string="My Favourite", type='boolean'),
248         'favourite_count': fields.function(
249             _get_favorite_count, string='Favorite Count', type='integer',
250             store={
251                 'forum.post': (lambda self, cr, uid, ids, c={}: ids, ['favourite_ids'], 10),
252             }),
253         # hierarchy
254         'parent_id': fields.many2one('forum.post', 'Question', ondelete='cascade'),
255         'self_reply': fields.function(
256             _is_self_reply, 'Reply to own question', type='boolean',
257             store={
258                 'forum.post': (lambda self, cr, uid, ids, c={}: ids, ['parent_id', 'create_uid'], 10),
259             }),
260         'child_ids': fields.one2many('forum.post', 'parent_id', 'Answers'),
261         'child_count': fields.function(
262             _get_child_count, string="Answers", type='integer',
263             store={
264                 'forum.post': (_get_post_from_hierarchy, ['parent_id', 'child_ids'], 10),
265             }),
266         'uid_has_answered': fields.function(
267             _get_uid_answered, string='Has Answered', type='boolean',
268         ),
269         'has_validated_answer': fields.function(
270             _get_has_validated_answer, string='Has a Validated Answered', type='boolean',
271             store={
272                 'forum.post': (_get_post_from_hierarchy, ['parent_id', 'child_ids', 'is_correct'], 10),
273             }
274         ),
275         # closing
276         'closed_reason_id': fields.many2one('forum.post.reason', 'Reason'),
277         'closed_uid': fields.many2one('res.users', 'Closed by', select=1),
278         'closed_date': fields.datetime('Closed on', readonly=True),
279         # karma
280         'karma_ask': fields.function(_get_post_karma_rights, string='Karma to ask', type='integer', multi='_get_post_karma_rights'),
281         'karma_answer': fields.function(_get_post_karma_rights, string='Karma to answer', type='integer', multi='_get_post_karma_rights'),
282         'karma_accept': fields.function(_get_post_karma_rights, string='Karma to accept this answer', type='integer', multi='_get_post_karma_rights'),
283         'karma_edit': fields.function(_get_post_karma_rights, string='Karma to edit', type='integer', multi='_get_post_karma_rights'),
284         'karma_close': fields.function(_get_post_karma_rights, string='Karma to close', type='integer', multi='_get_post_karma_rights'),
285         'karma_unlink': fields.function(_get_post_karma_rights, string='Karma to unlink', type='integer', multi='_get_post_karma_rights'),
286         'karma_upvote': fields.function(_get_post_karma_rights, string='Karma to upvote', type='integer', multi='_get_post_karma_rights'),
287         'karma_downvote': fields.function(_get_post_karma_rights, string='Karma to downvote', type='integer', multi='_get_post_karma_rights'),
288         'karma_comment': fields.function(_get_post_karma_rights, string='Karma to comment', type='integer', multi='_get_post_karma_rights'),
289         'karma_comment_convert': fields.function(_get_post_karma_rights, string='karma to convert as a comment', type='integer', multi='_get_post_karma_rights'),
290         # access rights
291         'can_ask': fields.function(_get_post_karma_rights, string='Can Ask', type='boolean', multi='_get_post_karma_rights'),
292         'can_answer': fields.function(_get_post_karma_rights, string='Can Answer', type='boolean', multi='_get_post_karma_rights'),
293         'can_accept': fields.function(_get_post_karma_rights, string='Can Accept', type='boolean', multi='_get_post_karma_rights'),
294         'can_edit': fields.function(_get_post_karma_rights, string='Can Edit', type='boolean', multi='_get_post_karma_rights'),
295         'can_close': fields.function(_get_post_karma_rights, string='Can Close', type='boolean', multi='_get_post_karma_rights'),
296         'can_unlink': fields.function(_get_post_karma_rights, string='Can Unlink', type='boolean', multi='_get_post_karma_rights'),
297         'can_upvote': fields.function(_get_post_karma_rights, string='Can Upvote', type='boolean', multi='_get_post_karma_rights'),
298         'can_downvote': fields.function(_get_post_karma_rights, string='Can Downvote', type='boolean', multi='_get_post_karma_rights'),
299         'can_comment': fields.function(_get_post_karma_rights, string='Can Comment', type='boolean', multi='_get_post_karma_rights'),
300         'can_comment_convert': fields.function(_get_post_karma_rights, string='Can Convert to Comment', type='boolean', multi='_get_post_karma_rights'),
301     }
302
303     _defaults = {
304         'state': 'active',
305         'views': 0,
306         'active': True,
307         'vote_ids': list(),
308         'favourite_ids': list(),
309         'child_ids': list(),
310     }
311
312     def create(self, cr, uid, vals, context=None):
313         if context is None:
314             context = {}
315         create_context = dict(context, mail_create_nolog=True)
316         post_id = super(Post, self).create(cr, uid, vals, context=create_context)
317         post = self.browse(cr, SUPERUSER_ID, post_id, context=context)  # SUPERUSER_ID to avoid read access rights issues when creating
318         # karma-based access
319         if post.parent_id and not post.can_ask:
320             raise KarmaError('Not enough karma to create a new question')
321         elif not post.parent_id and not post.can_answer:
322             raise KarmaError('Not enough karma to answer to a question')
323         # messaging and chatter
324         base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
325         if post.parent_id:
326             body = _(
327                 '<p>A new answer for <i>%s</i> has been posted. <a href="%s/forum/%s/question/%s">Click here to access the post.</a></p>' %
328                 (post.parent_id.name, base_url, slug(post.parent_id.forum_id), slug(post.parent_id))
329             )
330             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)
331         else:
332             body = _(
333                 '<p>A new question <i>%s</i> has been asked on %s. <a href="%s/forum/%s/question/%s">Click here to access the question.</a></p>' %
334                 (post.name, post.forum_id.name, base_url, slug(post.forum_id), slug(post))
335             )
336             self.message_post(cr, uid, post_id, subject=post.name, body=body, subtype='website_forum.mt_question_new', context=context)
337             self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], post.forum_id.karma_gen_question_new, context=context)
338         return post_id
339
340     def write(self, cr, uid, ids, vals, context=None):
341         posts = self.browse(cr, uid, ids, context=context)
342         if 'state' in vals:
343             if vals['state'] in ['active', 'close'] and any(not post.can_close for post in posts):
344                 raise KarmaError('Not enough karma to close or reopen a post.')
345         if 'active' in vals:
346             if any(not post.can_unlink for post in posts):
347                 raise KarmaError('Not enough karma to delete or reactivate a post')
348         if 'is_correct' in vals:
349             if any(not post.can_accept for post in posts):
350                 raise KarmaError('Not enough karma to accept or refuse an answer')
351             # update karma except for self-acceptance
352             mult = 1 if vals['is_correct'] else -1
353             for post in self.browse(cr, uid, ids, context=context):
354                 if vals['is_correct'] != post.is_correct and post.create_uid.id != uid:
355                     self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], post.forum_id.karma_gen_answer_accepted * mult, context=context)
356                     self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], post.forum_id.karma_gen_answer_accept * mult, context=context)
357         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):
358             raise KarmaError('Not enough karma to edit a post.')
359
360         res = super(Post, self).write(cr, uid, ids, vals, context=context)
361         # if post content modify, notify followers
362         if 'content' in vals or 'name' in vals:
363             for post in posts:
364                 if post.parent_id:
365                     body, subtype = _('Answer Edited'), 'website_forum.mt_answer_edit'
366                     obj_id = post.parent_id.id
367                 else:
368                     body, subtype = _('Question Edited'), 'website_forum.mt_question_edit'
369                     obj_id = post.id
370                 self.message_post(cr, uid, obj_id, body=body, subtype=subtype, context=context)
371         return res
372
373     def close(self, cr, uid, ids, reason_id, context=None):
374         if any(post.parent_id for post in self.browse(cr, uid, ids, context=context)):
375             return False
376         return self.pool['forum.post'].write(cr, uid, ids, {
377             'state': 'close',
378             'closed_uid': uid,
379             'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT),
380             'closed_reason_id': reason_id,
381         }, context=context)
382
383     def unlink(self, cr, uid, ids, context=None):
384         posts = self.browse(cr, uid, ids, context=context)
385         if any(not post.can_unlink for post in posts):
386             raise KarmaError('Not enough karma to unlink a post')
387         # if unlinking an answer with accepted answer: remove provided karma
388         for post in posts:
389             if post.is_correct:
390                 self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], post.forum_id.karma_gen_answer_accepted * -1, context=context)
391                 self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], post.forum_id.karma_gen_answer_accept * -1, context=context)
392         return super(Post, self).unlink(cr, uid, ids, context=context)
393
394     def vote(self, cr, uid, ids, upvote=True, context=None):
395         posts = self.browse(cr, uid, ids, context=context)
396
397         if upvote and any(not post.can_upvote for post in posts):
398             raise KarmaError('Not enough karma to upvote.')
399         elif not upvote and any(not post.can_downvote for post in posts):
400             raise KarmaError('Not enough karma to downvote.')
401
402         Vote = self.pool['forum.post.vote']
403         vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], limit=1, context=context)
404         new_vote = 0
405         if vote_ids:
406             for vote in Vote.browse(cr, uid, vote_ids, context=context):
407                 if upvote:
408                     new_vote = '0' if vote.vote == '-1' else '1'
409                 else:
410                     new_vote = '0' if vote.vote == '1' else '-1'
411                 Vote.write(cr, uid, vote_ids, {'vote': new_vote}, context=context)
412         else:
413             for post_id in ids:
414                 new_vote = '1' if upvote else '-1'
415                 Vote.create(cr, uid, {'post_id': post_id, 'vote': new_vote}, context=context)
416         return {'vote_count': self._get_vote_count(cr, uid, ids, None, None, context=context)[ids[0]], 'user_vote': new_vote}
417
418     def convert_answer_to_comment(self, cr, uid, id, context=None):
419         """ Tools to convert an answer (forum.post) to a comment (mail.message).
420         The original post is unlinked and a new comment is posted on the question
421         using the post create_uid as the comment's author. """
422         post = self.browse(cr, uid, id, context=context)
423         if not post.parent_id:
424             return False
425
426         # karma-based action check: use the post field that computed own/all value
427         if not post.can_comment_convert:
428             raise KarmaError('Not enough karma to convert an answer to a comment')
429
430         # post the message
431         question = post.parent_id
432         values = {
433             'author_id': post.create_uid.partner_id.id,
434             'body': html2plaintext(post.content),
435             'type': 'comment',
436             'subtype': 'mail.mt_comment',
437             'date': post.create_date,
438         }
439         message_id = self.pool['forum.post'].message_post(
440             cr, uid, question.id,
441             context=dict(context, mail_create_nosubcribe=True),
442             **values)
443
444         # unlink the original answer, using SUPERUSER_ID to avoid karma issues
445         self.pool['forum.post'].unlink(cr, SUPERUSER_ID, [post.id], context=context)
446
447         return message_id
448
449     def convert_comment_to_answer(self, cr, uid, message_id, default=None, context=None):
450         """ Tool to convert a comment (mail.message) into an answer (forum.post).
451         The original comment is unlinked and a new answer from the comment's author
452         is created. Nothing is done if the comment's author already answered the
453         question. """
454         comment = self.pool['mail.message'].browse(cr, SUPERUSER_ID, message_id, context=context)
455         post = self.pool['forum.post'].browse(cr, uid, comment.res_id, context=context)
456         user = self.pool['res.users'].browse(cr, uid, uid, context=context)
457         if not comment.author_id or not comment.author_id.user_ids:  # only comment posted by users can be converted
458             return False
459
460         # karma-based action check: must check the message's author to know if own / all
461         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
462         can_convert = uid == SUPERUSER_ID or user.karma >= karma_convert
463         if not can_convert:
464             raise KarmaError('Not enough karma to convert a comment to an answer')
465
466         # check the message's author has not already an answer
467         question = post.parent_id if post.parent_id else post
468         post_create_uid = comment.author_id.user_ids[0]
469         if any(answer.create_uid.id == post_create_uid.id for answer in question.child_ids):
470             return False
471
472         # create the new post
473         post_values = {
474             'forum_id': question.forum_id.id,
475             'content': comment.body,
476             'parent_id': question.id,
477         }
478         # done with the author user to have create_uid correctly set
479         new_post_id = self.pool['forum.post'].create(cr, post_create_uid.id, post_values, context=context)
480
481         # delete comment
482         self.pool['mail.message'].unlink(cr, SUPERUSER_ID, [comment.id], context=context)
483
484         return new_post_id
485
486     def unlink_comment(self, cr, uid, id, message_id, context=None):
487         comment = self.pool['mail.message'].browse(cr, SUPERUSER_ID, message_id, context=context)
488         post = self.pool['forum.post'].browse(cr, uid, id, context=context)
489         user = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context)
490         if not comment.model == 'forum.post' or not comment.res_id == id:
491             return False
492
493         # karma-based action check: must check the message's author to know if own or all
494         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
495         can_unlink = uid == SUPERUSER_ID or user.karma >= karma_unlink
496         if not can_unlink:
497             raise KarmaError('Not enough karma to unlink a comment')
498
499         return self.pool['mail.message'].unlink(cr, SUPERUSER_ID, [message_id], context=context)
500
501     def set_viewed(self, cr, uid, ids, context=None):
502         cr.execute("""UPDATE forum_post SET views = views+1 WHERE id IN %s""", (tuple(ids),))
503         return True
504
505     def _get_access_link(self, cr, uid, mail, partner, context=None):
506         post = self.pool['forum.post'].browse(cr, uid, mail.res_id, context=context)
507         res_id = post.parent_id and "%s#answer-%s" % (post.parent_id.id, post.id) or post.id
508         return "/forum/%s/question/%s" % (post.forum_id.id, res_id)
509
510
511 class PostReason(osv.Model):
512     _name = "forum.post.reason"
513     _description = "Post Closing Reason"
514     _order = 'name'
515     _columns = {
516         'name': fields.char('Post Reason', required=True, translate=True),
517     }
518
519
520 class Vote(osv.Model):
521     _name = 'forum.post.vote'
522     _description = 'Vote'
523     _columns = {
524         'post_id': fields.many2one('forum.post', 'Post', ondelete='cascade', required=True),
525         'user_id': fields.many2one('res.users', 'User', required=True),
526         'vote': fields.selection([('1', '1'), ('-1', '-1'), ('0', '0')], 'Vote', required=True),
527         'create_date': fields.datetime('Create Date', select=True, readonly=True),
528     }
529     _defaults = {
530         'user_id': lambda self, cr, uid, ctx: uid,
531         'vote': lambda *args: '1',
532     }
533
534     def _get_karma_value(self, old_vote, new_vote, up_karma, down_karma):
535         _karma_upd = {
536             '-1': {'-1': 0, '0': -1 * down_karma, '1': -1 * down_karma + up_karma},
537             '0': {'-1': 1 * down_karma, '0': 0, '1': up_karma},
538             '1': {'-1': -1 * up_karma + down_karma, '0': -1 * up_karma, '1': 0}
539         }
540         return _karma_upd[old_vote][new_vote]
541
542     def create(self, cr, uid, vals, context=None):
543         vote_id = super(Vote, self).create(cr, uid, vals, context=context)
544         vote = self.browse(cr, uid, vote_id, context=context)
545         if vote.post_id.parent_id:
546             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)
547         else:
548             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)
549         self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [vote.post_id.create_uid.id], karma_value, context=context)
550         return vote_id
551
552     def write(self, cr, uid, ids, values, context=None):
553         if 'vote' in values:
554             for vote in self.browse(cr, uid, ids, context=context):
555                 if vote.post_id.parent_id:
556                     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)
557                 else:
558                     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)
559                 self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [vote.post_id.create_uid.id], karma_value, context=context)
560         res = super(Vote, self).write(cr, uid, ids, values, context=context)
561         return res
562
563
564 class Tags(osv.Model):
565     _name = "forum.tag"
566     _description = "Tag"
567     _inherit = ['website.seo.metadata']
568
569     def _get_posts_count(self, cr, uid, ids, field_name, arg, context=None):
570         return dict((tag_id, self.pool['forum.post'].search_count(cr, uid, [('tag_ids', 'in', tag_id)], context=context)) for tag_id in ids)
571
572     def _get_tag_from_post(self, cr, uid, ids, context=None):
573         return list(set(
574             [tag.id for post in self.pool['forum.post'].browse(cr, SUPERUSER_ID, ids, context=context) for tag in post.tag_ids]
575         ))
576
577     _columns = {
578         'name': fields.char('Name', required=True),
579         'forum_id': fields.many2one('forum.forum', 'Forum', required=True),
580         'post_ids': fields.many2many('forum.post', 'forum_tag_rel', 'tag_id', 'post_id', 'Posts'),
581         'posts_count': fields.function(
582             _get_posts_count, type='integer', string="Number of Posts",
583             store={
584                 'forum.post': (_get_tag_from_post, ['tag_ids'], 10),
585             }
586         ),
587         'create_uid': fields.many2one('res.users', 'Created by', readonly=True),
588     }