diff --git a/addons/gamification/data/badge.xml b/addons/gamification/data/badge.xml index 5a390be9deb..33d02bf016e 100644 --- a/addons/gamification/data/badge.xml +++ b/addons/gamification/data/badge.xml @@ -23,7 +23,6 @@ - Brilliant With your brilliant ideas, you are an inspiration to others. @@ -32,6 +31,16 @@ 2 + + + + Badge Granted + res.users + + + Badge Granted + + diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py index eb97b55ab07..d8a3ef18840 100644 --- a/addons/gamification/models/badge.py +++ b/addons/gamification/models/badge.py @@ -35,6 +35,7 @@ class gamification_badge_user(osv.Model): _name = 'gamification.badge.user' _description = 'Gamification user badge' _order = "create_date desc" + _rec_name = "badge_name" _columns = { 'user_id': fields.many2one('res.users', string="User", required=True), @@ -62,7 +63,12 @@ class gamification_badge_user(osv.Model): template_id = self.pool['ir.model.data'].get_object(cr, uid, 'gamification', 'email_template_badge_received', context) for badge_user in self.browse(cr, uid, ids, context=context): body_html = temp_obj.render_template(cr, uid, template_id.body_html, 'gamification.badge.user', badge_user.id, context=context) - res = user_obj.message_post(cr, uid, badge_user.user_id.id, body=body_html, context=context) + res = user_obj.message_post( + cr, uid, badge_user.user_id.id, + body=body_html, + subtype='gamification.mt_badge_granted', + partner_ids=[badge_user.user_id.partner_id.id], + context=context) return res def create(self, cr, uid, vals, context=None): diff --git a/addons/portal_gamification/security/ir.model.access.csv b/addons/portal_gamification/security/ir.model.access.csv index 1a75ab2c0ad..448e483d7cf 100644 --- a/addons/portal_gamification/security/ir.model.access.csv +++ b/addons/portal_gamification/security/ir.model.access.csv @@ -4,4 +4,6 @@ goal_definition_portal,"Goal Definition Portal",gamification.model_gamification_ challenge_portal,"Goal Challenge Portal",gamification.model_gamification_challenge,base.group_portal,1,0,0,0 challenge_line_portal,"Challenge Line Portal",gamification.model_gamification_challenge_line,base.group_portal,1,0,0,0 badge_portal,"Badge Portal",gamification.model_gamification_badge,base.group_portal,1,0,0,0 +badge_public,"Badge Public",gamification.model_gamification_badge,base.group_public,1,0,0,0 badge_user_portal,"Badge-user Portal",gamification.model_gamification_badge_user,base.group_portal,1,1,1,0 +badge_user_public,"Badge-user Public",gamification.model_gamification_badge_user,base.group_public,1,0,0,0 diff --git a/addons/website_forum/__init__.py b/addons/website_forum/__init__.py new file mode 100644 index 00000000000..bde83af3aea --- /dev/null +++ b/addons/website_forum/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +import controllers +import models diff --git a/addons/website_forum/__openerp__.py b/addons/website_forum/__openerp__.py new file mode 100644 index 00000000000..0fce7b6fe05 --- /dev/null +++ b/addons/website_forum/__openerp__.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2014-Today OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{ + 'name': 'Forum', + 'category': 'Website', + 'summary': 'Forum, FAQ, Q&A', + 'version': '1.0', + 'description': """ +Ask questions, get answers, no distractions + """, + 'author': 'OpenERP SA', + 'depends': [ + 'auth_signup', + 'gamification', + 'website_mail', + 'website_partner' + ], + 'data': [ + 'data/forum_data.xml', + 'views/forum.xml', + 'views/res_users.xml', + 'views/website_forum.xml', + 'views/ir_qweb.xml', + 'security/ir.model.access.csv', + 'data/badges_question.xml', + 'data/badges_answer.xml', + 'data/badges_participation.xml', + 'data/badges_moderation.xml', + ], + 'qweb': [ + 'static/src/xml/*.xml' + ], + 'demo': [ + 'data/forum_demo.xml', + ], + 'css': ['static/src/css/website_forum.css'], + 'installable': True, + 'application': True, +} diff --git a/addons/website_forum/controllers/__init__.py b/addons/website_forum/controllers/__init__.py new file mode 100644 index 00000000000..bbd183e955b --- /dev/null +++ b/addons/website_forum/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +import main diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py new file mode 100644 index 00000000000..90cbf0ba722 --- /dev/null +++ b/addons/website_forum/controllers/main.py @@ -0,0 +1,550 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime +import werkzeug.urls +import simplejson + +from openerp import tools +from openerp import SUPERUSER_ID +from openerp.addons.web import http +from openerp.addons.web.controllers.main import login_redirect +from openerp.addons.web.http import request +from openerp.addons.website.controllers.main import Website as controllers +from openerp.addons.website.models.website import slug +from openerp.tools import html2plaintext + +controllers = controllers() + + +class WebsiteForum(http.Controller): + _post_per_page = 10 + _user_per_page = 30 + + def _get_notifications(self): + cr, uid, context = request.cr, request.uid, request.context + Message = request.registry['mail.message'] + badge_st_id = request.registry['ir.model.data'].xmlid_to_res_id(cr, uid, 'gamification.mt_badge_granted') + if badge_st_id: + msg_ids = Message.search(cr, uid, [('subtype_id', '=', badge_st_id), ('to_read', '=', True)], context=context) + msg = Message.browse(cr, uid, msg_ids, context=context) + else: + msg = list() + return msg + + def _prepare_forum_values(self, forum=None, **kwargs): + user = request.registry['res.users'].browse(request.cr, request.uid, request.uid, context=request.context) + public_uid = request.registry['website'].get_public_user(request.cr, request.uid, request.context) + values = {'user': user, 'is_public_user': user.id == public_uid, + 'notifications': self._get_notifications(), + 'header': kwargs.get('header', dict()), + 'searches': kwargs.get('searches', dict())} + if forum: + values['forum'] = forum + elif kwargs.get('forum_id'): + values['forum'] = request.registry['forum.forum'].browse(request.cr, request.uid, kwargs.pop('forum_id'), context=request.context) + values.update(kwargs) + return values + + # Forum + # -------------------------------------------------- + + @http.route(['/forum'], type='http', auth="public", website=True, multilang=True) + def forum(self, **kwargs): + cr, uid, context = request.cr, request.uid, request.context + Forum = request.registry['forum.forum'] + obj_ids = Forum.search(cr, uid, [], context=context) + forums = Forum.browse(cr, uid, obj_ids, context=context) + return request.website.render("website_forum.forum_all", {'forums': forums}) + + @http.route('/forum/new', type='http', auth="user", multilang=True, website=True) + def forum_create(self, forum_name="New Forum", **kwargs): + forum_id = request.registry['forum.forum'].create(request.cr, request.uid, { + 'name': forum_name, + }, context=request.context) + return request.redirect("/forum/%s" % slug(forum_id)) + + @http.route('/forum/notification_read', type='json', auth="user", multilang=True, methods=['POST'], website=True) + def notification_read(self, **kwargs): + request.registry['mail.message'].set_message_read(request.cr, request.uid, [int(kwargs.get('notification_id'))], read=True, context=request.context) + return True + + @http.route(['/forum/', + '/forum//page/', + '/forum//tag//questions' + ], type='http', auth="public", website=True, multilang=True) + def questions(self, forum, tag=None, page=1, filters='all', sorting='date', search='', **post): + cr, uid, context = request.cr, request.uid, request.context + Post = request.registry['forum.post'] + user = request.registry['res.users'].browse(cr, uid, uid, context=context) + + domain = [('forum_id', '=', forum.id), ('parent_id', '=', False)] + if search: + domain += ['|', ('name', 'ilike', search), ('content', 'ilike', search)] + if tag: + domain += [('tag_ids', 'in', tag.id)] + if filters == 'unanswered': + domain += [('child_ids', '=', False)] + elif filters == 'followed': + domain += [('message_follower_ids', '=', user.partner_id.id)] + else: + filters = 'all' + + if sorting == 'answered': + order = 'child_count desc' + elif sorting == 'vote': + order = 'vote_count desc' + else: + sorting = 'date' + order = 'write_date desc' + + question_count = Post.search(cr, uid, domain, count=True, context=context) + pager = request.website.pager(url="/forum/%s" % slug(forum), total=question_count, page=page, step=self._post_per_page, scope=self._post_per_page) + + obj_ids = Post.search(cr, uid, domain, limit=self._post_per_page, offset=pager['offset'], order=order, context=context) + question_ids = Post.browse(cr, uid, obj_ids, context=context) + + values = self._prepare_forum_values(forum=forum, searches=post) + values.update({ + 'question_ids': question_ids, + 'pager': pager, + 'tag': tag, + 'filters': filters, + 'sorting': sorting, + 'search': search, + }) + return request.website.render("website_forum.forum_index", values) + + @http.route(['/forum//faq'], type='http', auth="public", website=True, multilang=True) + def forum_faq(self, forum, **post): + values = self._prepare_forum_values(forum=forum, searches=dict(), **post) + return request.website.render("website_forum.faq", values) + + @http.route('/forum/get_tags', type='http', auth="public", multilang=True, methods=['GET'], website=True) + def tag_read(self, **post): + tags = request.registry['forum.tag'].search_read(request.cr, request.uid, [], ['name'], context=request.context) + data = [tag['name'] for tag in tags] + return simplejson.dumps(data) + + @http.route(['/forum//tag'], type='http', auth="public", website=True, multilang=True) + def tags(self, forum, page=1, **post): + cr, uid, context = request.cr, request.uid, request.context + Tag = request.registry['forum.tag'] + obj_ids = Tag.search(cr, uid, [('forum_id', '=', forum.id)], limit=None, context=context) + tags = Tag.browse(cr, uid, obj_ids, context=context) + values = self._prepare_forum_values(forum=forum, searches={'tags': True}, **post) + values.update({ + 'tags': tags, + }) + return request.website.render("website_forum.tag", values) + + # Questions + # -------------------------------------------------- + + @http.route(['/forum//ask'], type='http', auth="public", website=True, multilang=True) + def question_ask(self, forum, **post): + if not request.session.uid: + return login_redirect() + values = self._prepare_forum_values(forum=forum, searches={}, header={'ask_hide': True}) + return request.website.render("website_forum.ask_question", values) + + @http.route('/forum//question/new', type='http', auth="user", multilang=True, methods=['POST'], website=True) + def question_create(self, forum, **post): + cr, uid, context = request.cr, request.uid, request.context + Tag = request.registry['forum.tag'] + question_tag_ids = [] + if post.get('question_tags').strip('[]'): + tags = post.get('question_tags').strip('[]').replace('"', '').split(",") + for tag in tags: + tag_ids = Tag.search(cr, uid, [('name', '=', tag)], context=context) + if tag_ids: + question_tag_ids.append((4, tag_ids[0])) + else: + question_tag_ids.append((0, 0, {'name': tag, 'forum_id': forum.id})) + + new_question_id = request.registry['forum.post'].create( + request.cr, request.uid, { + 'forum_id': forum.id, + 'name': post.get('question_name'), + 'content': post.get('content'), + 'tag_ids': question_tag_ids, + }, context=context) + return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), new_question_id)) + + @http.route(['/forum//question/'], type='http', auth="public", website=True, multilang=True) + def question(self, forum, question, **post): + cr, uid, context = request.cr, request.uid, request.context + # increment view counter + request.registry['forum.post'].set_viewed(cr, SUPERUSER_ID, [question.id], context=context) + + filters = 'question' + values = self._prepare_forum_values(forum=forum, searches=post) + values.update({ + 'question': question, + 'header': {'question_data': True}, + 'filters': filters, + 'reversed': reversed, + }) + return request.website.render("website_forum.post_description_full", values) + + @http.route('/forum//question//toggle_favourite', type='json', auth="user", multilang=True, methods=['POST'], website=True) + def question_toggle_favorite(self, forum, question, **post): + if not request.session.uid: + return {'error': 'anonymous_user'} + # TDE: add check for not public + favourite = False if question.user_favourite else True + if favourite: + favourite_ids = [(4, request.uid)] + else: + favourite_ids = [(3, request.uid)] + request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'favourite_ids': favourite_ids}, context=request.context) + return favourite + + @http.route('/forum//question//ask_for_close', type='http', auth="user", multilang=True, website=True) + def question_ask_for_close(self, forum, question, **post): + cr, uid, context = request.cr, request.uid, request.context + Reason = request.registry['forum.post.reason'] + reason_ids = Reason.search(cr, uid, [], context=context) + reasons = Reason.browse(cr, uid, reason_ids, context) + + values = self._prepare_forum_values(**post) + values.update({ + 'post': question, + 'forum': forum, + 'reasons': reasons, + }) + return request.website.render("website_forum.close_question", values) + + @http.route('/forum//question//edit_answer', type='http', auth="user", website=True, multilang=True) + def question_edit_answer(self, forum, question, **kwargs): + for record in question.child_ids: + if record.create_uid.id == request.uid: + answer = record + break + return werkzeug.utils.redirect("/forum/%s/post/%s/edit" % (slug(forum), slug(answer))) + + @http.route('/forum//question//close', type='http', auth="user", multilang=True, methods=['POST'], website=True) + def question_close(self, forum, question, **post): + request.registry['forum.post'].write(request.cr, request.uid, [question.id], { + 'state': 'close', + 'closed_uid': request.uid, + 'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), + 'closed_reason_id': post.get('reason_id', False), + }, context=request.context) + return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) + + @http.route('/forum//question//reopen', type='http', auth="user", multilang=True, website=True) + def question_reopen(self, forum, question, **kwarg): + request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'state': 'active'}, context=request.context) + return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) + + @http.route('/forum//question//delete', type='http', auth="user", multilang=True, website=True) + def question_delete(self, forum, question, **kwarg): + #instead of unlink record just change 'active' to false so user can undelete it. + request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'active': False}, context=request.context) + return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) + + @http.route('/forum//question//undelete', type='http', auth="user", multilang=True, website=True) + def question_undelete(self, forum, question, **kwarg): + request.registry['forum.post'].write(request.cr, request.uid, [question.id], {'active': True}, context=request.context) + return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) + + # Post + # -------------------------------------------------- + + @http.route('/forum//post//new', type='http', auth="public", multilang=True, methods=['POST'], website=True) + def post_new(self, forum, post, **kwargs): + if not request.session.uid: + return login_redirect() + request.registry['forum.post'].create( + request.cr, request.uid, { + 'forum_id': forum.id, + 'parent_id': post.id, + 'content': kwargs.get('content'), + }, context=request.context) + return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(post))) + + @http.route('/forum//post//comment', type='http', auth="public", methods=['POST'], website=True) + def post_comment(self, forum, post, **kwargs): + if not request.session.uid: + return login_redirect() + question = post.parent_id if post.parent_id else post + cr, uid, context = request.cr, request.uid, request.context + if kwargs.get('comment') and post.forum_id.id == forum.id: + # TDE FIXME: check that post_id is the question or one of its answers + if request.registry['res.users'].has_group(cr, uid, 'website_mail.group_comment'): + request.registry['forum.post'].message_post( + cr, uid, post.id, + body=kwargs.get('comment'), + type='comment', + subtype='mt_comment', + context=dict(context, mail_create_nosubcribe=True)) + return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) + + @http.route('/forum//post//toggle_correct', type='json', auth="public", website=True) + def post_toggle_correct(self, forum, post, **kwargs): + cr, uid, context = request.cr, request.uid, request.context + if not request.session.uid: + return {'error': 'anonymous_user'} + # if user have not access to accept answer then reise warning + if post.parent_id is False or post.parent_id.create_uid.id != uid: + return {'error': 'own_post'} + + # set all answers to False, only one can be accepted + request.registry['forum.post'].write(cr, uid, [c.id for c in post.parent_id.child_ids], {'is_correct': False}, context=context) + request.registry['forum.post'].write(cr, uid, [post.id, post.parent_id.id], {'is_correct': not post.is_correct}, context=context) + return not post.is_correct + + @http.route('/forum//post//delete', type='http', auth="user", multilang=True, website=True) + def post_delete(self, forum, post, **kwargs): + question = post.parent_id + request.registry['forum.post'].unlink(request.cr, request.uid, [post.id], context=request.context) + if question: + werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) + return werkzeug.utils.redirect("/forum/%s" % slug(forum)) + + @http.route('/forum//post//edit', type='http', auth="user", website=True, multilang=True) + def post_edit(self, forum, post, **kwargs): + tags = "" + for tag_name in post.tag_ids: + tags += tag_name.name + "," + values = self._prepare_forum_values(forum=forum) + values.update({ + 'tags': tags, + 'post': post, + 'is_answer': bool(post.parent_id), + 'searches': kwargs + }) + return request.website.render("website_forum.edit_post", values) + + @http.route('/forum//post//save', type='http', auth="user", multilang=True, methods=['POST'], website=True) + def post_save(self, forum, post, **kwargs): + cr, uid, context = request.cr, request.uid, request.context + question_tags = [] + if kwargs.get('question_tag') and kwargs.get('question_tag').strip('[]'): + Tag = request.registry['forum.tag'] + tags = kwargs.get('question_tag').strip('[]').replace('"', '').split(",") + for tag in tags: + tag_ids = Tag.search(cr, uid, [('name', '=', tag)], context=context) + if tag_ids: + question_tags += tag_ids + else: + new_tag = Tag.create(cr, uid, {'name': tag, 'forum_id': forum.id}, context=context) + question_tags.append(new_tag) + vals = { + 'tag_ids': [(6, 0, question_tags)], + 'name': kwargs.get('question_name'), + 'content': kwargs.get('content'), + } + request.registry['forum.post'].write(cr, uid, [post.id], vals, context=context) + question = post.parent_id if post.parent_id else post + return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question))) + + @http.route('/forum//post//upvote', type='json', auth="public", multilang=True, website=True) + def post_upvote(self, forum, post, **kwargs): + # check for karma and not self vote + if not request.session.uid: + return {'error': 'anonymous_user'} + if request.uid == post.create_uid.id: + return {'error': 'own_post'} + user = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, request.uid, context=request.context) + if user.karma <= 5: + return {'error': 'not_enough_karma', 'karma': 5} + return request.registry['forum.post'].vote(request.cr, request.uid, [post.id], upvote=True, context=request.context) + + @http.route('/forum//post//downvote', type='json', auth="public", multilang=True, website=True) + def post_downvote(self, forum, post, **kwargs): + if not request.session.uid: + return {'error': 'anonymous_user'} + if request.uid == post.create_uid.id: + return {'error': 'own_post'} + user = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, request.uid, context=request.context) + if user.karma <= 50: + return {'error': 'not_enough_karma', 'karma': 50} + return request.registry['forum.post'].vote(request.cr, request.uid, [post.id], upvote=False, context=request.context) + + # User + # -------------------------------------------------- + + @http.route('/forum//users', type='http', auth="public", website=True, multilang=True) + def users(self, forum, page=1, **searches): + cr, uid, context = request.cr, request.uid, request.context + User = request.registry['res.users'] + + step = 30 + tag_count = User.search(cr, SUPERUSER_ID, [('karma', '>', 1)], count=True, context=context) + pager = request.website.pager(url="/forum/users", total=tag_count, page=page, step=step, scope=30) + + obj_ids = User.search(cr, SUPERUSER_ID, [('karma', '>', 1)], limit=step, offset=pager['offset'], context=context) + users = User.browse(cr, SUPERUSER_ID, obj_ids, context=context) + searches['users'] = 'True' + + values = self._prepare_forum_values(forum=forum, searches=searches) + values .update({ + 'users': users, + 'notifications': self._get_notifications(), + 'pager': pager, + }) + + return request.website.render("website_forum.users", values) + + @http.route(['/forum//user/'], type='http', auth="public", website=True, multilang=True) + def open_user(self, forum, user_id=0, **post): + cr, uid, context = request.cr, request.uid, request.context + User = request.registry['res.users'] + Post = request.registry['forum.post'] + Vote = request.registry['forum.post.vote'] + Activity = request.registry['mail.message'] + Followers = request.registry['mail.followers'] + Data = request.registry["ir.model.data"] + + user_id = User.search(cr, SUPERUSER_ID, [('id', '=', user_id), ('karma', '>', '1')], context=context) + if not user_id: + return werkzeug.utils.redirect("/forum/%s" % slug(forum)) + user = User.browse(cr, SUPERUSER_ID, user_id[0], context=context) + + # questions and answers by user + user_questions, user_answers = [], [] + user_post_ids = Post.search( + cr, uid, [ + ('forum_id', '=', forum.id), ('create_uid', '=', user.id), + '|', ('active', '=', False), ('active', '=', True)], context=context) + user_posts = Post.browse(cr, uid, user_post_ids, context=context) + for record in user_posts: + if record.parent_id: + user_answers.append(record) + else: + user_questions.append(record) + + # showing questions which user following + obj_ids = Followers.search(cr, SUPERUSER_ID, [('res_model', '=', 'forum.post'), ('partner_id', '=', user.partner_id.id)], context=context) + post_ids = [follower.res_id for follower in Followers.browse(cr, SUPERUSER_ID, obj_ids, context=context)] + que_ids = Post.search(cr, uid, [('id', 'in', post_ids), ('forum_id', '=', forum.id), ('parent_id', '=', False)], context=context) + followed = Post.browse(cr, uid, que_ids, context=context) + + #showing Favourite questions of user. + fav_que_ids = Post.search(cr, uid, [('favourite_ids', '=', user.id), ('forum_id', '=', forum.id), ('parent_id', '=', False)], context=context) + favourite = Post.browse(cr, uid, fav_que_ids, context=context) + + #votes which given on users questions and answers. + data = Vote.read_group(cr, uid, [('post_id.forum_id', '=', forum.id), ('post_id.create_uid', '=', user.id)], ["vote"], groupby=["vote"], context=context) + up_votes, down_votes = 0, 0 + for rec in data: + if rec['vote'] == '1': + up_votes = rec['vote_count'] + elif rec['vote'] == '-1': + down_votes = rec['vote_count'] + total_votes = up_votes + down_votes + + #Votes which given by users on others questions and answers. + post_votes = Vote.search(cr, uid, [('user_id', '=', user.id)], context=context) + vote_ids = Vote.browse(cr, uid, post_votes, context=context) + + #activity by user. + model, comment = Data.get_object_reference(cr, uid, 'mail', 'mt_comment') + activity_ids = Activity.search(cr, uid, [('res_id', 'in', user_post_ids), ('model', '=', 'forum.post'), ('subtype_id', '!=', comment)], context=context) + activities = Activity.browse(cr, uid, activity_ids, context=context) + + posts = {} + for act in activities: + posts[act.res_id] = True + posts_ids = Post.browse(cr, uid, posts.keys(), context=context) + posts = dict(map(lambda x: (x.id, (x.parent_id or x, x.parent_id and x or False)), posts_ids)) + + post['users'] = 'True' + + values = self._prepare_forum_values(**post) + values.update({ + 'uid': uid, + 'user': user, + 'main_object': user, + 'searches': post, + 'forum': forum, + 'questions': user_questions, + 'answers': user_answers, + 'followed': followed, + 'favourite': favourite, + 'total_votes': total_votes, + 'up_votes': up_votes, + 'down_votes': down_votes, + 'activities': activities, + 'posts': posts, + 'vote_post': vote_ids, + }) + return request.website.render("website_forum.user_detail_full", values) + + @http.route('/forum//user//edit', type='http', auth="user", multilang=True, website=True) + def edit_profile(self, forum, user, **kwargs): + country = request.registry['res.country'] + country_ids = country.search(request.cr, SUPERUSER_ID, [], context=request.context) + countries = country.browse(request.cr, SUPERUSER_ID, country_ids, context=request.context) + values = self._prepare_forum_values(forum=forum, searches=kwargs) + values.update({ + 'countries': countries, + 'notifications': self._get_notifications(), + }) + return request.website.render("website_forum.edit_profile", values) + + @http.route('/forum//user//save', type='http', auth="user", multilang=True, website=True) + def save_edited_profile(self, forum, user, **kwargs): + request.registry['res.users'].write(request.cr, request.uid, [user.id], { + 'name': kwargs.get('name'), + 'website': kwargs.get('website'), + 'email': kwargs.get('email'), + 'city': kwargs.get('city'), + 'country_id': kwargs.get('country'), + 'website_description': kwargs.get('description'), + }, context=request.context) + return werkzeug.utils.redirect("/forum/%s/user/%d" % (slug(forum), user.id)) + + # Badges + # -------------------------------------------------- + + @http.route('/forum//badge', type='http', auth="public", website=True, multilang=True) + def badges(self, forum, **searches): + cr, uid, context = request.cr, request.uid, request.context + Badge = request.registry['gamification.badge'] + badge_ids = Badge.search(cr, SUPERUSER_ID, [('challenge_ids.category', '=', 'forum')], context=context) + badges = Badge.browse(cr, uid, badge_ids, context=context) + values = self._prepare_forum_values(forum=forum, searches={'badges': True}) + values.update({ + 'badges': badges, + }) + return request.website.render("website_forum.badge", values) + + @http.route(['/forum//badge/'], type='http', auth="public", website=True, multilang=True) + def badge_users(self, forum, badge, **kwargs): + user_ids = [badge_user.user_id.id for badge_user in badge.owner_ids] + users = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, user_ids, context=request.context) + + values = self._prepare_forum_values(forum=forum, searches={'badges': True}) + values.update({ + 'badge': badge, + 'users': users, + }) + return request.website.render("website_forum.badge_user", values) + + # Messaging + # -------------------------------------------------- + + @http.route('/forum//post//comment//convert_to_answer', type='http', auth="public", multilang=True, website=True) + def convert_comment_to_answer(self, forum, post, comment, **kwarg): + values = { + 'content': comment.body, + } + request.registry['mail.message'].unlink(request.cr, request.uid, [comment.id], context=request.context) + question = post.parent_id if post.parent_id else post + return self.post_new(forum, question, **values) + + @http.route('/forum//post//convert_to_comment', type='http', auth="user", multilang=True, website=True) + def convert_answer_to_comment(self, forum, post, **kwarg): + values = { + 'comment': html2plaintext(post.content), + } + question = post.parent_id + request.registry['forum.post'].unlink(request.cr, SUPERUSER_ID, [post.id], context=request.context) + return self.post_comment(forum, question, **values) + + @http.route('/forum//post//comment//delete', type='json', auth="user", multilang=True, website=True) + def delete_comment(self, forum, post, comment, **kwarg): + request.registry['mail.message'].unlink(request.cr, SUPERUSER_ID, [comment.id], context=request.context) + return True diff --git a/addons/website_forum/data/badges_answer.xml b/addons/website_forum/data/badges_answer.xml new file mode 100644 index 00000000000..8b56a5ac02e --- /dev/null +++ b/addons/website_forum/data/badges_answer.xml @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/website_forum/data/badges_moderation.xml b/addons/website_forum/data/badges_moderation.xml new file mode 100644 index 00000000000..c169e5bc668 --- /dev/null +++ b/addons/website_forum/data/badges_moderation.xml @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/website_forum/data/badges_participation.xml b/addons/website_forum/data/badges_participation.xml new file mode 100644 index 00000000000..9d0c2139abe --- /dev/null +++ b/addons/website_forum/data/badges_participation.xml @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/website_forum/data/badges_question.xml b/addons/website_forum/data/badges_question.xml new file mode 100644 index 00000000000..02e9a9f306e --- /dev/null +++ b/addons/website_forum/data/badges_question.xml @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/website_forum/data/forum_data.xml b/addons/website_forum/data/forum_data.xml new file mode 100644 index 00000000000..245169c23e9 --- /dev/null +++ b/addons/website_forum/data/forum_data.xml @@ -0,0 +1,93 @@ + + + + + + Help + + + + Forum + + + 35 + + + + auth_signup.allow_uninvited + + + + + + Forum + self + + + + + open + + + + + New Answer + forum.post + + + New Answer + + + Answer Edited + forum.post + + Answer Edited + + + + New Question + forum.post + + New Question + + + Question Edited + forum.post + + Question Edited + + + + + + + + duplicate question + + + off-topic or not relevant + + + too subjective and argumentative + + + not a real question + + + already answered and an answer was accepted + + + not relevant or out dated + + + contains offensive or malicious remarks + + + spam or advertising + + + too localized + + + + diff --git a/addons/website_forum/data/forum_default_faq.html b/addons/website_forum/data/forum_default_faq.html new file mode 100644 index 00000000000..8c6d32e43ed --- /dev/null +++ b/addons/website_forum/data/forum_default_faq.html @@ -0,0 +1,147 @@ +

Guidelines

+

What kinds of questions can I ask here?

+

+ This community is for professional and enthusiast users, + partners and programmers. You can ask questions about: +

+
    +
  • how to install OpenERP on a specific infrastructure,
  • +
  • how to configure or customize OpenERP to specific business needs,
  • +
  • what's the best way to use OpenERP for a specific business need,
  • +
  • how to develop modules for your own need,
  • +
  • specific questions about OpenERP service offers, etc.
  • +
+

+ Before you ask - please make sure to search for a similar question. You can + search questions by their title or tags. It’s also OK to + answer your own question. +

+ Please avoid asking questions that are too subjective + and argumentative or not relevant to this community. +

+

What should I avoid in my questions?

+

+ You should only ask practical, answerable questions based + on actual problems that you face. Chatty, open-ended + questions diminish the usefulness of this site and push + other questions off the front page. +

+ To prevent your question from being flagged and possibly removed, avoid asking + subjective questions where … +

+
    +
  • every answer is equally valid: “What’s your favorite ______?”
  • +
  • your answer is provided along with the question, and you expect more answers: “I use ______ for ______, what do you use?”
  • +
  • there is no actual problem to be solved: “I’m curious if other people feel like I do.”
  • +
  • we are being asked an open-ended, hypothetical question: “What if ______ happened?”
  • +
  • it is a rant disguised as a question: “______ sucks, am I right?”
  • +
+

+ If you fit in one of these example or if your motivation for asking the + question is “I would like to participate in a discussion about ______”, then + you should not be asking here but on our mailing lists. + However, if your motivation is “I would like others to explain ______ to me”, + then you are probably OK. +

+ (The above section was adapted from Stackoverflow’s FAQ.) +

+ More over: +

+
    +
  • Answers should not add or expand questions. Instead either edit the question edit or add a question comment.
  • +
  • Answers should not comment other answers. Instead add a comment on the other answers.
  • +
  • Answers shouldn't just point to other Questions. Instead add a question comment indication "Possible duplicate of...". However, it's ok to include links to other questions or answers providing relevant additional information.
  • +
  • Answers shouldn't just provide a link a solution. Instead provide the solution description text in your answer, even if it's just a copy/paste. Links are welcome, but should complementary to answer, referring sources or additional reading.
  • +
+ +

What should I avoid in my answers?

+

+ Answers should not add or expand questions. Instead + either edit the question or add a comment. +

+ Answers should not comment other answers. Instead + add a comment on the other answers. +

+ Answers shouldn't just point to other questions. + Instead add a comment indicating "Possible duplicate + of...". However, it's fine to include links to other + questions or answers providing relevant additional + information.

Answers shouldn't just provide a + link a solution. Instead provide the solution + description text in your answer, even if it's just a + copy/paste. Links are welcome, but should complementary to + answer, referring sources or additional reading. +

+ Answers should not start debates + This community Q&A is not a discussion group. Please avoid holding debates in + your answers as they tend to dilute the essence of questions and answers. For + brief discussions please use commenting facility. +

+ When a question or answer is upvoted, the user who posted them will gain some + points, which are called "karma points". These points serve as a rough + measure of the community trust to him/her. Various moderation tasks are + gradually assigned to the users based on those points. +

+ For example, if you ask an interesting question or give a helpful answer, your + input will be upvoted. On the other hand if the answer is misleading - it will + be downvoted. Each vote in favor will generate 10 points, each vote against + will subtract 10 points. There is a limit of 200 points that can be accumulated + for a question or answer per day. The table below explains reputation point + requirements for each type of moderation task. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1upvote, add comments
10downvote
30insert text link, upload files
50insert clickable link, answer own question immediately
75retag, edit wiki questions and answers
100flag offensive, close own questions
200answer/comment by email
300edit any post, view offensive flags
500accept any answer (after 1 week)
750delete any comment
900lock posts
1000delete any question or answer
+ +

Why can other people edit my questions/answers?

+

+ The goal of this site is create a relevant knowledge base that would answer + questions related to OpenERP. +

+ Therefore questions and answers can be edited like wiki pages by experienced users of + this site in order to improve the overall quality of the knowledge base content. + Such privileges are granted based on user karma level: you will be able to do the same + once your karma gets high enough. +

+ If this approach is not for you, please respect the community and use Google+ + communities instead. +

diff --git a/addons/website_forum/data/forum_demo.xml b/addons/website_forum/data/forum_demo.xml new file mode 100644 index 00000000000..85a8a8512bf --- /dev/null +++ b/addons/website_forum/data/forum_demo.xml @@ -0,0 +1,74 @@ + + + + + + + Contract + + + + Action + + + + ecommerce + + + + + + How to configure alerts for employee contract expiration + + 3 + + + + CMS replacement for ERP and eCommerce + 8 + + I use Wordpress as a CMS and eCommerce platform. The developing in Wordpress is quite easy and solid but it missing ERP feature (there is single plugin to integrate with Frontaccounting) so I wonder: + +Can I use OpenERP as a replacement CMS of Wordpress + eCommerce plugin? + +In simple words does OpenERP became CMS+ERP platform?

]]>
+ +
+ + + + + Just for posterity so other can see. Here are the steps to set automatic alerts on any contract.. i.e. HR Employee, or Fleet for example. I will use fleet as an example.

+
    +
  • Step 1. As a user who has access rights to Technical Features, go to Settings --> Automated Actions. Create A new Automated Action. For the Related Document Model choose.. Contract information on a vehicle (you can also type in the actual model name.. fleet.vehicle.log.contract ) . Set the trigger date to ... Contract Expiration Date. The Next Field (Delay After Trigger Date) is a bit ridiculous. Who wants to be reminded of a contract expiration AFTER the fact? The field should say Days Before Date to Fire Action and the number should be converted to a negative. IMHO. Any way... to get a workable solution you must enter in the number in the negative. So for instance like me if you want to be warned 35 days BEFORE the expiration... put in Delay After Trigger Date.. the number -35 But the sake of testing, right now just put in -1 for 1 day before. Save the Action. +
  • Step 2. Go to Server Actions and create new Action. Call it Fleet Contract Expiration Warning. The Object will be the same as above .. Contract information on a vehicle. The Action Type is Email. For email address I just put my email. Under subject put in... [[object.name]]. This will tell you the name of the car. Message you can put any text message you like. Now save the Server Action.
  • +
  • Step 3. Now go back to the Automated Action you created and go to the Action tab next to the conditions tab. Click Add and add the server action you created . In this case Fleet Contract Expiration Warning. Then Save.
  • +
  • Step 4. To test, set a contract to expire tomorrow under one of your fleets vehicles. Then Save it.
  • +
  • Step 5. Go to Scheduled Actions.. Set interval number to 1. Interval Unit to Minutes. Then Set the Next Execution date to 2 minutes from now. If your SMTP is configured correctly you will start to get a mail every minute with the reminder.
]]>
+ +
+ + + OpenERP v8 (next release) provides a web module and an e-commerce module: www.openerp.com/website_cms + The CMS editor in OpernERP web is nice but I prefer drupal for customization and there is a drupal module for OpenERP. I think WP is better than OpenERP web too. + + + + + + + + + 1 + + + + + 1 + + + + + +
+
diff --git a/addons/website_forum/models/__init__.py b/addons/website_forum/models/__init__.py new file mode 100644 index 00000000000..9dc113bc87e --- /dev/null +++ b/addons/website_forum/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +import gamification +import res_users +import forum diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py new file mode 100644 index 00000000000..74f4d9e5393 --- /dev/null +++ b/addons/website_forum/models/forum.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- + +import openerp + +from openerp import SUPERUSER_ID +from openerp.addons.website.models.website import slug +from openerp.osv import osv, fields +from openerp.tools.translate import _ + + +class Forum(osv.Model): + _name = 'forum.forum' + _description = 'Forums' + _inherit = ['website.seo.metadata'] + + _columns = { + 'name': fields.char('Name', required=True, translate=True), + 'faq': fields.html('Guidelines'), + 'description': fields.html('Description'), + } + + def _get_default_faq(self, cr, uid, context=None): + fname = openerp.modules.get_module_resource('website_forum', 'data', 'forum_default_faq.html') + with open(fname, 'r') as f: + return f.read() + return False + + _defaults = { + 'description': 'This community is for professionals and enthusiasts of our products and services.', + 'faq': _get_default_faq, + } + + def create(self, cr, uid, values, context=None): + if context is None: + context = {} + create_context = dict(context, mail_create_nolog=True) + return super(Forum, self).create(cr, uid, values, context=create_context) + + +class Post(osv.Model): + _name = 'forum.post' + _description = 'Forum Post' + _inherit = ['mail.thread', 'website.seo.metadata'] + + def _get_user_vote(self, cr, uid, ids, field_name, arg, context): + res = dict.fromkeys(ids, 0) + vote_ids = self.pool['forum.post.vote'].search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context) + for vote in self.pool['forum.post.vote'].browse(cr, uid, vote_ids, context=context): + res[vote.post_id.id] = vote.vote + return res + + def _get_vote_count(self, cr, uid, ids, field_name, arg, context): + res = dict.fromkeys(ids, 0) + for post in self.browse(cr, uid, ids, context=context): + for vote in post.vote_ids: + res[post.id] += int(vote.vote) + return res + + def _get_post_from_vote(self, cr, uid, ids, context=None): + result = {} + for vote in self.pool['forum.post.vote'].browse(cr, uid, ids, context=context): + result[vote.post_id.id] = True + return result.keys() + + def _get_user_favourite(self, cr, uid, ids, field_name, arg, context): + res = dict.fromkeys(ids, False) + for post in self.browse(cr, uid, ids, context=context): + if uid in [f.id for f in post.favourite_ids]: + res[post.id] = True + return res + + def _get_favorite_count(self, cr, uid, ids, field_name, arg, context): + res = dict.fromkeys(ids, 0) + for post in self.browse(cr, uid, ids, context=context): + res[post.id] += len(post.favourite_ids) + return res + + def _get_post_from_hierarchy(self, cr, uid, ids, context=None): + post_ids = set(ids) + for post in self.browse(cr, SUPERUSER_ID, ids, context=context): + if post.parent_id: + post_ids.add(post.parent_id.id) + return list(post_ids) + + def _get_child_count(self, cr, uid, ids, field_name=False, arg={}, context=None): + res = dict.fromkeys(ids, 0) + for post in self.browse(cr, uid, ids, context=context): + if post.parent_id: + res[post.parent_id.id] = len(post.parent_id.child_ids) + else: + res[post.id] = len(post.child_ids) + return res + + def _get_uid_answered(self, cr, uid, ids, field_name, arg, context=None): + res = dict.fromkeys(ids, False) + for post in self.browse(cr, uid, ids, context=context): + res[post.id] = any(answer.create_uid.id == uid for answer in post.child_ids) + return res + + _columns = { + 'name': fields.char('Title', size=128), + 'forum_id': fields.many2one('forum.forum', 'Forum', required=True), + 'content': fields.html('Content'), + 'tag_ids': fields.many2many('forum.tag', 'forum_tag_rel', 'forum_id', 'forum_tag_id', 'Tags'), + 'state': fields.selection([('active', 'Active'), ('close', 'Close'), ('offensive', 'Offensive')], 'Status'), + 'views': fields.integer('Number of Views'), + 'active': fields.boolean('Active'), + 'is_correct': fields.boolean('Valid Answer', help='Correct Answer/ Answer on this question accepted.'), + 'website_message_ids': fields.one2many( + 'mail.message', 'res_id', + domain=lambda self: [ + '&', ('model', '=', self._name), ('type', '=', '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', 'Created by', select=True, readonly=True), + 'write_date': fields.datetime('Update on', select=True, readonly=True), + 'write_uid': fields.many2one('res.users', 'Updated by', select=True, readonly=True), + # vote fields + 'vote_ids': fields.one2many('forum.post.vote', 'post_id', 'Votes'), + 'user_vote': fields.function(_get_user_vote, string='My Vote', type='integer'), + 'vote_count': fields.function( + _get_vote_count, string="Votes", type='integer', + store={ + 'forum.post': (lambda self, cr, uid, ids, c={}: ids, ['vote_ids'], 10), + 'forum.post.vote': (_get_post_from_vote, [], 10), + }), + # favorite fields + 'favourite_ids': fields.many2many('res.users', string='Favourite'), + 'user_favourite': fields.function(_get_user_favourite, string="My Favourite", type='boolean'), + 'favourite_count': fields.function( + _get_favorite_count, string='Favorite Count', type='integer', + store={ + 'forum.post': (lambda self, cr, uid, ids, c={}: ids, ['favourite_ids'], 10), + }), + # hierarchy + 'parent_id': fields.many2one('forum.post', 'Question', ondelete='cascade'), + 'child_ids': fields.one2many('forum.post', 'parent_id', 'Answers'), + 'child_count': fields.function( + _get_child_count, string="Answers", type='integer', + store={ + 'forum.post': (_get_post_from_hierarchy, ['parent_id', 'child_ids'], 10), + }), + 'uid_has_answered': fields.function( + _get_uid_answered, string='Has Answered', type='boolean', + ), + # closing + 'closed_reason_id': fields.many2one('forum.post.reason', 'Reason'), + 'closed_uid': fields.many2one('res.users', 'Closed by', select=1), + 'closed_date': fields.datetime('Closed on', readonly=True), + } + + _defaults = { + 'state': 'active', + 'views': 0, + 'active': True, + 'vote_ids': list(), + 'favourite_ids': list(), + 'child_ids': list(), + } + + def create(self, cr, uid, vals, context=None): + if context is None: + context = {} + create_context = dict(context, mail_create_nolog=True) + post_id = super(Post, self).create(cr, uid, vals, context=create_context) + post = self.browse(cr, uid, post_id, context=context) + # post message + subtype depending on parent_id + if vals.get("parent_id"): + parent = self.browse(cr, SUPERUSER_ID, vals['parent_id'], context=context) + body = _('

New Answer Posted

' % (slug(parent.forum_id), slug(parent))) + self.message_post(cr, uid, parent.id, subject=_('Re: %s') % parent.name, body=body, subtype='website_forum.mt_answer_new', context=context) + else: + self.message_post(cr, uid, post.id, subject=post.name, body=_('New Question Created'), subtype='website_forum.mt_question_new', context=context) + self.pool['res.users'].write(cr, SUPERUSER_ID, [post.create_uid.id], {'karma': 2}, context=context) + return post_id + + def write(self, cr, uid, ids, vals, context=None): + res = super(Post, self).write(cr, uid, ids, vals, context=context) + # if post content modify, notify followers + if 'content' in vals or 'name' in vals: + for post in self.browse(cr, uid, ids, context=context): + if post.parent_id: + body, subtype = _('Answer Edited'), 'website_forum.mt_answer_edit' + obj_id = post.parent_id.id + else: + body, subtype = _('Question Edited'), 'website_forum.mt_question_edit' + obj_id = post.id + self.message_post(cr, uid, obj_id, body=_(body), subtype=subtype, context=context) + # update karma of related user when any answer accepted + if 'correct' in vals: + for post in self.browse(cr, uid, ids, context=context): + karma_value = 15 if vals.get('correct') else -15 + self.pool['res.users'].write(cr, SUPERUSER_ID, [post.create_uid.id], {'karma': karma_value}, context=context) + return res + + def vote(self, cr, uid, ids, upvote=True, context=None): + Vote = self.pool['forum.post.vote'] + vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context) + if vote_ids: + for vote in Vote.browse(cr, uid, vote_ids, context=context): + if upvote: + new_vote = '0' if vote.vote == '-1' else '1' + else: + new_vote = '0' if vote.vote == '1' else '-1' + Vote.write(cr, uid, vote_ids, {'vote': new_vote}, context=context) + else: + for post_id in ids: + new_vote = '1' if upvote else '-1' + Vote.create(cr, uid, {'post_id': post_id, 'vote': new_vote}, context=context) + return {'vote_count': self._get_vote_count(cr, uid, ids, None, None, context=context)[ids[0]]} + + def set_viewed(self, cr, uid, ids, context=None): + for post in self.browse(cr, uid, ids, context=context): + self.write(cr, uid, [post.id], {'views': post.views + 1}, context=context) + return True + + +class PostReason(osv.Model): + _name = "forum.post.reason" + _description = "Post Closing Reason" + _columns = { + 'name': fields.char('Post Reason', required=True), + } + + +class Vote(osv.Model): + _name = 'forum.post.vote' + _description = 'Vote' + _columns = { + 'post_id': fields.many2one('forum.post', 'Post', ondelete='cascade', required=True), + 'user_id': fields.many2one('res.users', 'User'), + 'vote': fields.selection([('1', '1'), ('-1', '-1'), ('0', '0')], 'Vote', required=True), + 'create_date': fields.datetime('Create Date', select=True, readonly=True), + } + _defaults = { + 'user_id': lambda self, cr, uid, ctx: uid, + 'vote': lambda *args: '1', + } + + def update_karma(self, cr, uid, ids, new_vote='0', old_vote='0', context=None): + karma_value = (int(new_vote) - int(old_vote)) * 10 + if karma_value: + for vote in self.browse(cr, uid, ids, context=context): + self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [vote.post_id.create_uid.id], karma_value, context=context) + return True + + def create(self, cr, uid, vals, context=None): + vote_id = super(Vote, self).create(cr, uid, vals, context=context) + self.update_karma(cr, uid, [vote_id], new_vote=vals.get('vote', '1'), context=context) + return vote_id + + def write(self, cr, uid, ids, values, context=None): + res = super(Vote, self).write(cr, uid, ids, values, context=context) + if 'vote' in values: + for vote in self.browse(cr, uid, ids, context=context): + self.update_karma(cr, uid, ids, new_vote=values['vote'], old_vote=vote.vote, context=context) + return res + + +class Tags(osv.Model): + _name = "forum.tag" + _description = "Tag" + _inherit = ['website.seo.metadata'] + + def _get_posts_count(self, cr, uid, ids, field_name, arg, context=None): + return dict((tag_id, self.pool['forum.post'].search_count(cr, uid, [('tag_ids', 'in', tag_id)], context=context)) for tag_id in ids) + + def _get_tag_from_post(self, cr, uid, ids, context=None): + return list(set( + [tag.id for post in self.pool['forum.post'].browse(cr, SUPERUSER_ID, ids, context=context) for tag in post.tag_ids] + )) + + _columns = { + 'name': fields.char('Name', required=True), + 'forum_id': fields.many2one('forum.forum', 'Forum', required=True), + 'post_ids': fields.many2many('forum.post', 'forum_tag_rel', 'tag_id', 'post_id', 'Posts'), + 'posts_count': fields.function( + _get_posts_count, type='integer', string="Number of Posts", + store={ + 'forum.post': (_get_tag_from_post, ['tag_ids'], 10), + } + ), + } diff --git a/addons/website_forum/models/gamification.py b/addons/website_forum/models/gamification.py new file mode 100644 index 00000000000..cffe52c4848 --- /dev/null +++ b/addons/website_forum/models/gamification.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +from openerp.osv import osv, fields + + +class gamification_challenge(osv.Model): + _inherit = 'gamification.challenge' + + def _get_categories(self, cr, uid, context=None): + res = super(gamification_challenge, self)._get_categories(cr, uid, context=context) + res.append(('forum', 'Website / Forum')) + return res + + +class Badge(osv.Model): + _inherit = 'gamification.badge' + _columns = { + 'level': fields.selection([('bronze', 'bronze'), ('silver', 'silver'), ('gold', 'gold')], 'Forum Badge Level'), + } diff --git a/addons/website_forum/models/res_users.py b/addons/website_forum/models/res_users.py new file mode 100644 index 00000000000..89a23cc2901 --- /dev/null +++ b/addons/website_forum/models/res_users.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +from openerp.osv import osv, fields + + +class Users(osv.Model): + _inherit = 'res.users' + + def _get_user_badge_level(self, cr, uid, ids, name, args, context=None): + """Return total badge per level of users""" + result = dict.fromkeys(ids, False) + badge_user_obj = self.pool['gamification.badge.user'] + for id in ids: + result[id] = { + 'gold_badge': badge_user_obj.search(cr, uid, [('badge_id.level', '=', 'gold'), ('user_id', '=', id)], context=context, count=True), + 'silver_badge': badge_user_obj.search(cr, uid, [('badge_id.level', '=', 'silver'), ('user_id', '=', id)], context=context, count=True), + 'bronze_badge': badge_user_obj.search(cr, uid, [('badge_id.level', '=', 'bronze'), ('user_id', '=', id)], context=context, count=True), + } + return result + + _columns = { + 'create_date': fields.datetime('Create Date', select=True, readonly=True), + 'karma': fields.integer('Karma'), + 'badge_ids': fields.one2many('gamification.badge.user', 'user_id', 'Badges'), + 'gold_badge': fields.function(_get_user_badge_level, string="Number of gold badges", type='integer', multi='badge_level'), + 'silver_badge': fields.function(_get_user_badge_level, string="Number of silver badges", type='integer', multi='badge_level'), + 'bronze_badge': fields.function(_get_user_badge_level, string="Number of bronze badges", type='integer', multi='badge_level'), + } + + _defaults = { + 'karma': 0, + } + + def add_karma(self, cr, uid, ids, karma, context=None): + for user in self.browse(cr, uid, ids, context=context): + self.write(cr, uid, [user.id], {'karma': user.karma + karma}, context=context) + return True diff --git a/addons/website_forum/security/ir.model.access.csv b/addons/website_forum/security/ir.model.access.csv new file mode 100644 index 00000000000..f7a0e666ad7 --- /dev/null +++ b/addons/website_forum/security/ir.model.access.csv @@ -0,0 +1,19 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_forum_forum,forum.forum,model_forum_forum,,1,0,0,0 +access_forum_forum_manager,forum.forum.maanger,model_forum_forum,base.group_erp_manager,1,1,1,1 + +access_forum_post_public,forum.post.public,model_forum_post,base.group_public,1,0,0,0 +access_forum_post_portal,forum.post.portal,model_forum_post,base.group_portal,1,1,1,0 +access_forum_post_user,forum.post.user,model_forum_post,base.group_user,1,1,1,1 + +access_forum_post_vote_public,forum.post.vote.public,model_forum_post_vote,base.group_public,1,0,0,0 +access_forum_post_vote_portal,orum.post.vote.portal,model_forum_post_vote,base.group_portal,1,1,1,0 +access_forum_post_vote_user,forum.post.vote.user,model_forum_post_vote,base.group_user,1,1,1,1 + +access_forum_post_reason_public,forum.post.reason.public,model_forum_post_reason,base.group_public,1,0,0,0 +access_forum_post_reason_portal,forum.post.reason.portal,model_forum_post_reason,base.group_portal,1,0,0,0 +access_forum_post_reason_user,forum.post.reason.user,model_forum_post_reason,base.group_user,1,1,1,1 + +access_forum_tag_public,forum.tag.public,model_forum_tag,base.group_public,1,0,1,0 +access_forum_tag_portal,forum.tag.portal,model_forum_tag,base.group_portal,1,0,1,0 +access_forum_tag_user,forum.tag.user,model_forum_tag,base.group_user,1,1,1,1 diff --git a/addons/website_forum/static/description/icon.png b/addons/website_forum/static/description/icon.png new file mode 100644 index 00000000000..63fae243f1d Binary files /dev/null and b/addons/website_forum/static/description/icon.png differ diff --git a/addons/website_forum/static/src/css/Makefile b/addons/website_forum/static/src/css/Makefile new file mode 100644 index 00000000000..ceee4ae3f7a --- /dev/null +++ b/addons/website_forum/static/src/css/Makefile @@ -0,0 +1,5 @@ +all: website_forum.css +%.css: %.sass + sass -t expanded --compass --unix-newlines $< $@ +watch: + sass -t expanded --compass --unix-newlines --watch .:. diff --git a/addons/website_forum/static/src/css/website_forum.css b/addons/website_forum/static/src/css/website_forum.css new file mode 100644 index 00000000000..1155ded81aa --- /dev/null +++ b/addons/website_forum/static/src/css/website_forum.css @@ -0,0 +1,82 @@ +.box { + padding-left: 8px; + padding-right: 8px; + margin-left: 4px; + margin-right: 4px; + border-radius: 4px; + min-width: 80px; +} +.box span { + font-size: 200%; + font-weight: bold; +} + +.question .question-name { + font-size: 150%; +} + +.oe_grey { + background-color: #eeeeee; +} + +.img-avatar { + max-width: 50px; + margin-right: 10px; +} + +.badge-gold { + color: #ffcc00; +} + +.badge-silver { + color: #cccccc; +} + +.badge-bronze { + color: #eea91e; +} + +.oe_answer_true { + color: #428bca; +} + +.oe_answer_false { + color: #999999; +} + +.forum_favourite_question { + color: #ffcc00 !important; +} + +.load_tags { + width: 845px !important; +} + +a.no-decoration { + cursor: pointer; + text-decoration: none !important; +} + +.faq-question:hover { + text-decoration: none !important; + color: #428bca; +} + +.oe_comment_grey { + background-color: whitesmoke; + padding: 4px; +} + +.country_flag { + max-height: 16px; + display: inline-block; + margin-left: 2px; +} + +.tag_text .text-core .text-wrap textarea, .tag_text .text-core .text-wrap input, .tag_text .text-core .text-wrap .text-dropdown, .tag_text .text-core .text-wrap .text-prompt { + font: 1.2em "Helvetica Neue", Helvetica, Arial, sans-serif !important; +} +.tag_text .text-core .text-wrap .text-tags .text-tag .text-button { + font: 1.2em "Helvetica Neue", Helvetica, Arial, sans-serif !important; + height: 1.2em !important; +} diff --git a/addons/website_forum/static/src/css/website_forum.sass b/addons/website_forum/static/src/css/website_forum.sass new file mode 100644 index 00000000000..f5bfb667b74 --- /dev/null +++ b/addons/website_forum/static/src/css/website_forum.sass @@ -0,0 +1,66 @@ +.box + padding-left: 8px + padding-right: 8px + margin-left: 4px + margin-right: 4px + border-radius: 4px + min-width: 80px + span + font-size: 200% + font-weight: bold + +.question + .question-name + font-size: 150% + +.oe_grey + background-color: #eeeeee + +.img-avatar + max-width: 50px + margin-right: 10px + +.badge-gold + color: #ffcc00 + +.badge-silver + color: #cccccc + +.badge-bronze + color: #eea91e + +.oe_answer_true + color: #428bca + +.oe_answer_false + color: #999999 + +.forum_favourite_question + color: #ffcc00 !important + +.load_tags + width: 845px !important + +a.no-decoration + cursor: pointer + text-decoration: none !important + +.faq-question:hover + text-decoration: none !important + color: #428bca + +.oe_comment_grey + background-color: #f5f5f5 + padding: 4px + +.country_flag + max-height: 16px + display: inline-block + margin-left: 2px + +.tag_text .text-core .text-wrap + textarea, input, .text-dropdown, .text-prompt + font: 1.2em "Helvetica Neue", Helvetica, Arial, sans-serif !important + .text-tags .text-tag .text-button + font: 1.2em "Helvetica Neue", Helvetica, Arial, sans-serif !important + height: 1.2em !important \ No newline at end of file diff --git a/addons/website_forum/static/src/js/website.tour.forum.js b/addons/website_forum/static/src/js/website.tour.forum.js new file mode 100644 index 00000000000..b4ca841463d --- /dev/null +++ b/addons/website_forum/static/src/js/website.tour.forum.js @@ -0,0 +1,31 @@ +(function () { + 'use strict'; + + var website = openerp.website; + var _t = openerp._t; + + // website.EditorBar.include({ + // start: function () { + // this.registerTour(new website.Tour.Forum(this)); + // return this._super(); + // }, + // }); + + // website.Tour.Forum = website.Tour.extend({ + // id: 'question', + // name: "Create a question", + // testPath: '/forum(/[0-9]+/register)?', + // init: function (editor) { + // var self = this; + // self.steps = [ + // { + // title: _t("Create a question"), + // content: _t("Let's go through the first steps to create a new question."), + // popover: { next: _("Start Tutorial"), end: _("Skip It") }, + // }, + // ]; + // return this._super(); + // } + // }); + +}()); diff --git a/addons/website_forum/static/src/js/website_forum.editor.js b/addons/website_forum/static/src/js/website_forum.editor.js new file mode 100644 index 00000000000..0c2adf1a4bc --- /dev/null +++ b/addons/website_forum/static/src/js/website_forum.editor.js @@ -0,0 +1,31 @@ +(function() { + "use strict"; + + var website = openerp.website; + var _t = openerp._t; + website.add_template_file('/website_forum/static/src/xml/website_forum.xml'); + + website.is_editable = true; + website.EditorBar.include({ + start: function() { + website.is_editable_button = website.is_editable_button || !!$("#wrap").size(); + var res = this._super(); + this.$(".dropdown:has(.oe_content_menu)").removeClass("hidden"); + return res; + }, + events: _.extend({}, website.EditorBar.prototype.events, { + 'click a[data-action=new_forum]': function (ev) { + ev.preventDefault(); + website.prompt({ + id: "editor_new_forum", + window_title: _t("New Forum"), + input: "Forum Name", + }).then(function (forum_name) { + website.form('/forum/new', 'POST', { + forum_name: forum_name + }); + }); + } + }), + }); +})(); diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js new file mode 100644 index 00000000000..2c428740776 --- /dev/null +++ b/addons/website_forum/static/src/js/website_forum.js @@ -0,0 +1,156 @@ +$(document).ready(function () { + + $('.vote_up ,.vote_down').on('click', function (ev) { + ev.preventDefault(); + var $link = $(ev.currentTarget); + openerp.jsonRpc($link.attr('href'), 'call', {}) + .then(function (data) { + if (data['error']){ + if (data['error'] == 'own_post'){ + var $warning = $('
'+ + ''+ + 'Sorry, you cannot vote for your own posts'+ + '
'); + } else if (data['error'] == 'anonymous_user'){ + var $warning = $('
'+ + ''+ + 'Sorry you must be logged to vote'+ + '
'); + } + else if (data['error'] == 'not_enough_karma') { + var $warning = $('
'+ + ''+ + 'Sorry, at least ' + data['karma'] + ' karma is required to vote'+ + '
'); + } + vote_alert = $link.parent().find("#vote_alert"); + if (vote_alert.length == 0) { + $link.parent().append($warning); + } + } else { + $link.parent().find("#vote_count").html(data['vote_count']); + if (data['vote_count'] == 0) { + $link.parent().find(".text-success").removeClass("text-success"); + $link.parent().find(".text-warning").removeClass("text-warning"); + } else { + if (data['vote_count'] == 1) { + $link.addClass("text-success"); + } else { + $link.addClass("text-warning"); + } + } + } + }); + return true; + }); + + $('.accept_answer').on('click', function (ev) { + ev.preventDefault(); + var $link = $(ev.currentTarget); + openerp.jsonRpc($link.attr('href'), 'call', {}).then(function (data) { + if (data['error']) { + if (data['error'] == 'anonymous_user'){ + var $warning = $('
'+ + ''+ + 'Sorry, anonymous users cannot choose correct answer.'+ + '
'); + } else if (data['error'] == 'own_post'){ + var $warning = $('
'+ + ''+ + 'Sorry, the user who asked this question can only accept the answer as correct.'+ + '
'); + } + correct_answer_alert = $link.parent().find("#correct_answer_alert"); + if (correct_answer_alert.length == 0) { + $link.parent().append($warning); + } + } else { + $link.parents().find(".oe_answer_true").removeClass("oe_answer_true alert alert-info").addClass('oe_answer_false'); + $link.parents().find(".answer_correct").removeClass("answer_correct alert alert-info") + if (data) { + $link.removeClass("oe_answer_false").addClass('oe_answer_true'); + $link.parents('.forum_answer').first().addClass("answer_correct alert alert-info"); + } + } + }); + return true; + }); + + $('.favourite_question').on('click', function (ev) { + ev.preventDefault(); + var $link = $(ev.currentTarget); + openerp.jsonRpc($link.attr('href'), 'call', {}).then(function (data) { + if (data) { + $link.addClass("forum_favourite_question") + } else { + $link.removeClass("forum_favourite_question") + } + }); + return true; + }); + + $('.comment_delete').on('click', function (ev) { + ev.preventDefault(); + var $link = $(ev.currentTarget); + openerp.jsonRpc($link.attr('href'), 'call', {}).then(function (data) { + $link.parents('.comment').first().remove(); + }); + return true; + }); + + $('.notification_close').on('click', function (ev) { + ev.preventDefault(); + var $link = $(ev.currentTarget); + openerp.jsonRpc("/forum/notification_read", 'call', { + 'notification_id': $link.attr("id")}) + return true; + }); + + if($('input.load_tags').length){ + var tags = $("input.load_tags").val(); + $("input.load_tags").val(""); + set_tags(tags); + }; + + function set_tags(tags) { + $("input.load_tags").textext({ + plugins: 'tags focus autocomplete ajax', + tagsItems: tags.split(","), + //Note: The following list of keyboard keys is added. All entries are default except {32 : 'whitespace!'}. + keys: {8: 'backspace', 9: 'tab', 13: 'enter!', 27: 'escape!', 37: 'left', 38: 'up!', 39: 'right', + 40: 'down!', 46: 'delete', 108: 'numpadEnter', 32: 'whitespace!'}, + ajax: { + url: '/forum/get_tags', + dataType: 'json', + cacheResults: true + } + }); + // Note: Adding event handler of whitespaceKeyDown event. + $("input.load_tags").bind("whitespaceKeyDown",function () { + $(this).textext()[0].tags().addTags([ $(this).val() ]); + $(this).val(""); + }); + } + + if ($('textarea.load_editor').length) { + var editor = CKEDITOR.instances['content']; + editor.on('instanceReady', CKEDITORLoadComplete); + } +}); + +function IsKarmaValid(eventNumber,minKarma){ + "use strict"; + if(parseInt($("#karma").val()) >= minKarma){ + CKEDITOR.tools.callFunction(eventNumber,this); + return false; + } else { + alert("Sorry you need more than 30 Karma."); + } +} + +function CKEDITORLoadComplete(){ + "use strict"; + $('.cke_button__link').attr('onclick','IsKarmaValid(33,30)'); + $('.cke_button__unlink').attr('onclick','IsKarmaValid(37,30)'); + $('.cke_button__image').attr('onclick','IsKarmaValid(41,30)'); +} diff --git a/addons/website_forum/static/src/xml/website_forum.xml b/addons/website_forum/static/src/xml/website_forum.xml new file mode 100644 index 00000000000..f2298b80f44 --- /dev/null +++ b/addons/website_forum/static/src/xml/website_forum.xml @@ -0,0 +1,7 @@ + + + +
  • New Forum
  • +
    +
    +
    \ No newline at end of file diff --git a/addons/website_forum/views/forum.xml b/addons/website_forum/views/forum.xml new file mode 100644 index 00000000000..7c6048ec250 --- /dev/null +++ b/addons/website_forum/views/forum.xml @@ -0,0 +1,113 @@ + + + + + + + + + forum.forum.list + forum.forum + + + + + + + + + forum.forum.form + forum.forum + +
    + + + + + + +
    +
    +
    + + + Forums + forum.forum + form + tree,form + + + + + + + forum.post.list + forum.post + + + + + + + + + + forum.post.form + forum.post + +
    + +

    + + + + + + + + + + + + + + +
    +
    + + +
    +
    +
    +
    + + + forum.post.search + forum.post + + + + + + + + + + + + + + + Forum Posts + forum.post + form + tree,form + + + + + + +
    +
    diff --git a/addons/website_forum/views/ir_qweb.xml b/addons/website_forum/views/ir_qweb.xml new file mode 100644 index 00000000000..04188b54234 --- /dev/null +++ b/addons/website_forum/views/ir_qweb.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/addons/website_forum/views/res_users.xml b/addons/website_forum/views/res_users.xml new file mode 100644 index 00000000000..40aa30ecefc --- /dev/null +++ b/addons/website_forum/views/res_users.xml @@ -0,0 +1,18 @@ + + + + + + + res.users.form.forum + res.users + + + + + + + + + + diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml new file mode 100644 index 00000000000..2d1787a48ab --- /dev/null +++ b/addons/website_forum/views/website_forum.xml @@ -0,0 +1,906 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +