[ADD] added functionality to make question favourite, and improved some code
[odoo/odoo.git] / addons / website_forum / models / forum.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import re
23
24 import openerp
25 from openerp import tools
26 from openerp import SUPERUSER_ID
27 from openerp.osv import osv, fields
28 from openerp.tools.translate import _
29 from datetime import date, datetime
30
31 from openerp.addons.website.models.website import slug
32
33 #TODO: Do we need a forum object like blog object ? Need to check with BE team 
34 class Forum(osv.Model):
35     _name = 'website.forum'
36     _description = 'Forums'
37     _inherit = ['mail.thread', 'website.seo.metadata']
38
39     def _get_right_column(self, cr, uid, ids, field_name, arg, context):
40         res = {}
41         for forum in self.browse(cr, uid, ids, context=context):
42             res[forum.id] = """<div class="panel panel-default">
43                 <div class="panel-heading">
44                     <h3 class="panel-title">About This Forum</h3>
45                 </div>
46                 <div class="panel-body">
47                     This community is for professionals and enthusiasts of our 
48                     products and services.<br/>
49                     <a href="/forum/%s/faq" class="fa fa-arrow-right"> Read Guidelines</a>
50                 </div>
51             </div>""" % slug(forum)
52         return res
53
54     _columns = {
55         'name': fields.char('Name', required=True, translate=True),
56         'faq': fields.html('Guidelines'),
57         'right_column':fields.function(_get_right_column, string="Right Column", type='html', store=True),
58     }
59     def _get_default_faq(self, cr, uid, context={}):
60         fname = openerp.modules.get_module_resource('website_forum', 'data', 'forum_default_faq.html')
61         with open(fname, 'r') as f:
62             return f.read()
63         return False
64
65     _defaults = {
66         'faq': _get_default_faq,
67     }
68
69 class Post(osv.Model):
70     _name = 'website.forum.post'
71     _description = "Question"
72     _inherit = ['mail.thread', 'website.seo.metadata']
73
74     def _get_vote_count(self, cr, uid, ids, field_name, arg, context):
75         res = dict.fromkeys(ids, 0)
76         for post in self.browse(cr, uid, ids, context=context):
77             if post.vote_ids:
78                 for vote in post.vote_ids:
79                     res[post.id] += int(vote.vote)
80         return res
81
82     def _get_vote(self, cr, uid, ids, context=None):
83         result = {}
84         for vote in self.pool['website.forum.post.vote'].browse(cr, uid, ids, context=context):
85             result[vote.post_id.id] = True
86         return result.keys()
87
88     def _get_child_count(self, cr, uid, ids, field_name=False, arg={}, context=None):
89         res = dict.fromkeys(ids, 0)
90         for post in self.browse(cr, uid, ids, context=context):
91             if post.parent_id:
92                 res[post.parent_id.id] = len(post.parent_id.child_ids)
93             else:
94                 res[post.id] = len(post.child_ids)
95         return res
96
97     def _get_child(self, cr, uid, ids, context=None):
98         return ids
99
100     def _get_view_count(self, cr, uid, ids, field_name=False, arg={}, context=None):
101         res = dict.fromkeys(ids, 0)
102         for post in self.browse(cr, uid, ids, context=context):
103             res[post.id] = post.views + 1
104         return res
105
106     def _set_view_count(self, cr, uid, ids, name, value, arg, context=None):
107         if isinstance(ids, (int, long)):
108             ids = [ids]
109         for rec in self.browse(cr, uid, ids, context=context):
110             views = rec.views + value
111             cr.execute('UPDATE website_forum_post SET views=%s WHERE id=%s;',(views, rec.id))
112         return True
113
114     def _get_views(self, cr, uid, ids, context=None):
115         result = {}
116         for statistic in self.pool['website.forum.post.statistics'].browse(cr, uid, ids, context=context):
117             result[statistic.post_id.id] = True
118         return result.keys()
119
120     def _get_user_vote(self, cr, uid, ids, field_name, arg, context):
121         res = dict.fromkeys(ids, 0)
122         # Note: read_group is not returning all fields which we passed in list.when it will work uncomment this code and remove remaining code 
123         #Vote = self.pool['website.forum.post.vote']
124         #data = Vote.read_group(cr, uid, [('post_id','in', ids), ('user_id', '=', uid)], [ "post_id", "vote"], groupby=["post_id"], context=context)
125         #for rec in data:
126         #    res[rec[post_id][0]] = rec['vote']
127         for post in self.browse(cr, uid, ids, context=context):
128             for vote in post.vote_ids:
129                 if vote.user_id.id == uid:
130                     if vote.vote == '1':
131                         res[post.id] = 1
132                     elif vote.vote == '-1':
133                         res[post.id] = -1
134         return res
135
136     def _get_user_favourite(self, cr, uid, ids, field_name, arg, context):
137         res = dict.fromkeys(ids, False)
138         user = self.pool['res.users'].browse(cr, uid, uid, context=context)
139         for post in self.browse(cr, uid, ids, context=context):
140             if user in post.favourite_ids:
141                 res[post.id] = True
142         return res
143
144     _columns = {
145         'name': fields.char('Title', size=128),
146         'forum_id': fields.many2one('website.forum', 'Forum', required=True),
147         'content': fields.text('Content'),
148         'create_date': fields.datetime('Asked on', select=True, readonly=True),
149         'user_id': fields.many2one('res.users', 'Asked by', select=True, readonly=True ),
150         'write_date': fields.datetime('Update on', select=True, readonly=True ),
151         'write_uid': fields.many2one('res.users', 'Update by', select=True, readonly=True),
152
153         'tags': fields.many2many('website.forum.tag', 'forum_tag_rel', 'forum_id', 'forum_tag_id', 'Tag'),
154         'vote_ids':fields.one2many('website.forum.post.vote', 'post_id', 'Votes'),
155         'user_vote':fields.function(_get_user_vote, string="My Vote", type='integer'),
156
157         'favourite_ids': fields.many2many('res.users', 'forum_favourite_rel', 'forum_id', 'user_id', 'Favourite'),
158         'user_favourite':fields.function(_get_user_favourite, string="My Favourite", type='boolean'),
159
160         'state': fields.selection([('active', 'Active'), ('close', 'Close'),('offensive', 'Offensive')], 'Status'),
161         'active': fields.boolean('Active'),
162
163         'views_ids': fields.one2many('website.forum.post.statistics', 'post_id', 'Views'),
164         'views':fields.function(_get_view_count, string="Views", type='integer',
165             store={
166                 'website.forum.post.statistics': (_get_views, [], 10),
167             }
168         ),
169
170         'parent_id': fields.many2one('website.forum.post', 'Question', ondelete='cascade'),
171         'child_ids': fields.one2many('website.forum.post', 'parent_id', 'Answers'),
172         'child_count':fields.function(_get_child_count, string="Answers", type='integer',
173             store={
174                 'website.forum.post': (_get_child, [], 10),
175             }
176         ),
177
178         'history_ids': fields.one2many('blog.post.history', 'post_id', 'History', help='Last post modifications'),
179         # TODO FIXME: when website_mail/mail_thread.py inheritance work -> this field won't be necessary
180         'website_message_ids': fields.one2many(
181             'mail.message', 'res_id',
182             domain=lambda self: [
183                 '&', ('model', '=', self._name), ('type', '=', 'comment')
184             ],
185             string='Post Messages',
186             help="Comments on forum post",
187         ),
188
189         'vote_count':fields.function(_get_vote_count, string="Votes", type='integer',
190             store={
191                 'website.forum.post': (lambda self, cr, uid, ids, c={}: ids, ['vote_ids'], 10),
192                 'website.forum.post.vote': (_get_vote, [], 10),
193             }
194         ),
195
196         'correct': fields.boolean('Correct Answer/ Answer on this question accepted.'),
197         'reason_id': fields.many2one('website.forum.post.reason', 'Reason'),
198         'closed_by': fields.many2one('res.users', 'Closed by'),
199         'closed_date': fields.datetime('Closed on', readonly=True),
200     }
201     _defaults = {
202         'state': 'active',
203         'vote_count': 0,
204         'active': True,
205     }
206
207     def create_history(self, cr, uid, ids, context=None):
208         hist_obj = self.pool['website.forum.post.history']
209         for post in self.browse(cr, uid, ids, context=context):
210             history_count = hist_obj.search(cr, uid, [('post_id','=', post.id)], count=True, context=context)
211             date = datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
212             name = "No.%s Revision" %(history_count+1) if history_count else "initial version"
213             hist_obj.create(cr, uid, {
214                 'post_id': post.id,
215                 'content': post.content,
216                 'name': '%s - (%s) %s' % (history_count+1, date, name),
217                 'post_name': post.name,
218                 'tags': [(6,0, [x.id for x in post.tags])],
219                 'user_id': post.write_uid and post.write_uid.id or post.user_id.id,
220             }, context=context)
221
222     def create(self, cr, uid, vals, context=None):
223         if context is None:
224             context = {}
225         create_context = dict(context, mail_create_nolog=True)
226         body, subtype = "Asked a question", "website_forum.mt_question_create"
227         if vals.get("parent_id"):
228             body, subtype = "Answered a question", "website_forum.mt_answer_create"
229             #Note: because of no name it gives error on slug so set name of question in answer
230             question = self.browse(cr, uid, vals.get("parent_id"), context=context)
231             vals['name'] = question.name
232             #add 2 karma to user when asks question.
233             self.pool['res.users'].write(cr, SUPERUSER_ID, [vals.get('user_id')], {'karma': 2}, context=context)
234         post_id = super(Post, self).create(cr, uid, vals, context=create_context)
235         self.create_history(cr, uid, [post_id], context=context)
236         self.message_post(cr, uid, [post_id], body=_(body), subtype=subtype, context=context)
237         return post_id
238
239     def write(self, cr, uid, ids, vals, context=None):
240         super(Post, self).write(cr, uid, ids, vals, context=context)
241         #NOTE: to avoid message post on write of last comment time
242         if not vals.get('message_last_post'):
243             user = self.pool['res.users'].browse(cr, uid ,uid, context=context)
244             self.create_history(cr, uid, ids, context=context)
245             for post in self.browse(cr, uid, ids, context=context):
246                 body, subtype = "Edited question", "website_forum.mt_question_edit"
247                 if post.parent_id:
248                     body, subtype = "Edited answer", "website_forum.mt_answer_edit"
249                 self.message_post(cr, uid, [post.id], body=_(body), subtype=subtype, context=context)
250
251                 #update karma of related user when any answer accepted.
252                 value = 0
253                 if vals.get('correct'):
254                     value = 15
255                 elif vals.get('correct') == False:
256                     value = -15 
257                 self.pool['res.users'].write(cr, SUPERUSER_ID, [post.user_id.id], {'karma': value}, context=context)
258         return True
259
260 class PostStatistics(osv.Model):
261     _name = "website.forum.post.statistics"
262     _description = "Post Statistics"
263     _columns = {
264         'name': fields.char('Post Reason'),
265         'post_id': fields.many2one('website.forum.post', 'Post', ondelete='cascade'),
266         'user_id': fields.many2one('res.users', 'Created by'),
267     }
268
269 class PostReason(osv.Model):
270     _name = "website.forum.post.reason"
271     _description = "Post Reason"
272     _columns = {
273         'name': fields.char('Post Reason'),
274     }
275
276 class Users(osv.Model):
277     _inherit = 'res.users'
278
279     def _get_user_badge_level(self, cr, uid, ids, name, args, context=None):
280         """Return total badge per level of users"""
281         result = dict.fromkeys(ids, False)
282         badge_user_obj = self.pool['gamification.badge.user']
283         for id in ids:
284             result[id] = {
285                 'gold_badge': badge_user_obj.search(cr, uid, [('badge_id.level', '=', 'gold'), ('user_id', '=', id)], context=context, count=True),
286                 'silver_badge': badge_user_obj.search(cr, uid, [('badge_id.level', '=', 'silver'), ('user_id', '=', id)], context=context, count=True),
287                 'bronze_badge': badge_user_obj.search(cr, uid, [('badge_id.level', '=', 'bronze'), ('user_id', '=', id)], context=context, count=True),
288             }
289         return result
290
291     def _set_user_karma(self, cr, uid, ids, name, value, arg, context=None):
292         if isinstance(ids, (int, long)):
293             ids = [ids]
294         for rec in self.browse(cr, uid, ids, context=context):
295             karma = rec.karma + value
296             cr.execute('UPDATE res_users SET karma=%s WHERE id=%s;',(karma, rec.id))
297         return True
298
299     def _get_user_karma(self, cr, uid, ids, field_name, arg, context=None):
300         result = {}
301         for user in self.browse(cr, uid, ids, context=context):
302             result[user.id] = user.karma
303         return result
304
305     _columns = {
306         'create_date': fields.datetime('Create Date', select=True, readonly=True),
307         'forum': fields.boolean('Is Forum Member'),
308         'karma': fields.function(_get_user_karma, fnct_inv=_set_user_karma, type='integer', string="Karma", store=True),
309
310         'badges': fields.one2many('gamification.badge.user', 'user_id', 'Badges'),
311         'gold_badge':fields.function(_get_user_badge_level, string="Number of gold badges", type='integer', multi='badge_level'),
312         'silver_badge':fields.function(_get_user_badge_level, string="Number of silver badges", type='integer', multi='badge_level'),
313         'bronze_badge':fields.function(_get_user_badge_level, string="Number of bronze badges", type='integer', multi='badge_level'),
314     }
315     _defaults = {
316         'forum': False,
317         'karma': 1,
318     }
319
320 class PostHistory(osv.Model):
321     _name = 'website.forum.post.history'
322     _description = 'Post History'
323     _inherit = ['website.seo.metadata']
324     _columns = {
325         'name': fields.char('History Title'),
326         'post_id': fields.many2one('website.forum.post', 'Post', ondelete='cascade'),
327         'post_name': fields.char('Post Name'),
328         'content': fields.text('Content'),
329         'user_id': fields.many2one('res.users', 'Created by', select=True, readonly=True),
330         'tags': fields.many2many('website.forum.tag', 'post_tag_rel', 'post_id', 'post_tag_id', 'Tag'),
331     }
332
333 class Vote(osv.Model):
334     _name = 'website.forum.post.vote'
335     _description = 'Vote'
336     _columns = {
337         'post_id': fields.many2one('website.forum.post', 'Post', ondelete='cascade', required=True),
338         'user_id': fields.many2one('res.users', 'User'),
339         'vote': fields.selection([('1', '1'),('-1', '-1'),('0','0')], 'Vote'),
340         'create_date': fields.datetime('Create Date', select=True, readonly=True),
341     }
342     _defaults = {
343         'user_id': lambda self, cr, uid, ctx: uid,
344         'vote': lambda *args: 1
345     }
346
347     def create(self, cr, uid, vals, context=None):
348         vote_id = super(Vote, self).create(cr, uid, vals, context=context)
349         Post = self.pool["website.forum.post"]
350         record = Post.browse(cr, uid, vals.get('post_id'), context=context)
351         #Add 10 karma when user get up vote and subtract 10 karma when gets down vote.
352         value = 10 if vals.get('vote') == '1' else -10
353         self.pool['res.users'].write(cr, SUPERUSER_ID, [record.user_id.id], {'karma': value}, context=context)
354         body = "voted %s %s" % ('answer' if record.parent_id else 'question','up' if vals.get('vote')==1 else 'down')
355         Post.message_post(cr, uid, [record.id], body=_(body), context=context)
356         return vote_id
357
358     def vote(self, cr, uid, post_id, vote, context=None):
359         assert int(vote) in (1, -1, 0), "vote can be -1 or 1, nothing else"
360         #user can not vote on own post.
361         post = self.pool['website.forum.post'].browse(cr, uid, post_id, context=context)
362         if post.user_id.id == uid:
363             return {'error': 'own_post'}
364         user = self.pool['res.users'].browse(cr, uid, uid, context=None)
365         if (vote == '-1') and (user.karma <= 10):
366             return {'error': 'lessthen_10_karma'}
367         vote_ids = self.search(cr, uid, [('post_id', '=', post_id), ('user_id','=',uid)], context=context)
368         if vote_ids:
369             #when user will click again on vote it should set it 0.
370             record = self.browse(cr,uid, vote_ids[0], context=context)
371             new_vote = '0' if record.vote in ['1','-1'] else vote
372             #update karma when user changed vote.
373             if record.vote == '1' or new_vote == '-1':
374                 value = -10
375             elif record.vote == '-1' or new_vote == '1':
376                 value = 10
377             self.pool['res.users'].write(cr, SUPERUSER_ID, [record.post_id.user_id.id], {'karma': value}, context=context)
378             self.write(cr, uid, vote_ids, {
379                 'vote': new_vote
380             }, context=context)
381         else:
382             self.create(cr, uid, {
383                 'post_id': post_id,
384                 'vote': vote,
385             }, context=context)
386         post.refresh()
387         return {'vote_count': post.vote_count}
388
389 class MailMessage(osv.Model):
390     _inherit = 'mail.message'
391     _columns = {
392         'create_uid': fields.many2one('res.users', 'Created by', readonly=True ),
393     }
394
395 class Tags(osv.Model):
396     _name = "website.forum.tag"
397     _description = "Tag"
398     _inherit = ['website.seo.metadata']
399
400     def _get_posts_count(self, cr, uid, ids, field_name, arg, context=None):
401         result = {}
402         Post = self.pool['website.forum.post']
403         for tag in ids:
404             result[tag] = Post.search_count(cr, uid , [('tags', '=', tag)], context=context)
405         return result
406
407     def _get_post(self, cr, uid, ids, context=None):
408         result = {}
409         for post in self.pool['website.forum.post'].browse(cr, uid, ids, context=context):
410             for tag in post.tags:
411                 result[tag.id] = True
412         return result.keys()
413
414     _columns = {
415         'name': fields.char('Name', size=64, required=True),
416         'forum_id': fields.many2one('website.forum', 'Forum', required=True),
417         'posts_count': fields.function(_get_posts_count, type='integer', string="# of Posts",
418             store={
419                 'website.forum.post': (_get_post, ['tags'], 10),
420             }
421         ),
422    }