[MERGE] forward port of branch 8.0 up to 5ed0739
authorJeremy Kersten <jke@odoo.com>
Tue, 18 Nov 2014 09:40:59 +0000 (10:40 +0100)
committerJeremy Kersten <jke@odoo.com>
Tue, 18 Nov 2014 09:40:59 +0000 (10:40 +0100)
1  2 
addons/website_forum/controllers/main.py
addons/website_forum/data/forum_data.xml
addons/website_forum/models/forum.py
addons/website_forum/static/src/js/website.tour.forum.js
addons/website_forum/static/src/js/website_forum.js
addons/website_forum/views/website_forum.xml
doc/conf.py

@@@ -156,9 -170,18 +156,12 @@@ class WebsiteForum(http.Controller)
          return request.website.render("website_forum.faq", values)
  
      @http.route('/forum/get_tags', type='http', auth="public", methods=['GET'], website=True)
-     def tag_read(self, **post):
-         tags = request.env['forum.tag'].search_read([], ['name'])
-         data = [tag['name'] for tag in tags]
 -    def tag_read(self, q='', l=25, t='texttext', **post):
 -        data = request.registry['forum.tag'].search_read(
 -            request.cr,
 -            request.uid,
++    def tag_read(self, q='', l=25, **post):
++        data = request.env['forum.tag'].search_read(
+             domain=[('name', '=ilike', (q or '') + "%")],
+             fields=['id', 'name'],
+             limit=int(l),
 -            context=request.context
+         )
 -        if t == 'texttext':
 -            # old tag with texttext - Retro for V8 - #TODO Remove in master
 -            data = [tag['name'] for tag in data]
          return simplejson.dumps(data)
  
      @http.route(['/forum/<model("forum.forum"):forum>/tag'], type='http', auth="public", website=True)
  
      # Post
      # --------------------------------------------------
--
 -    @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/new', type='http', auth="public", methods=['POST'], website=True)
 -    def post_new(self, forum, post, **kwargs):
 +    @http.route(['/forum/<model("forum.forum"):forum>/ask'], type='http', auth="public", website=True)
 +    def forum_post(self, forum, post_type=None, **post):
          if not request.session.uid:
              return login_redirect()
 -        cr, uid, context = request.cr, request.uid, request.context
 -        user = request.registry['res.users'].browse(cr, SUPERUSER_ID, uid, context=context)
 +        user = request.env.user
 +        if not post_type in ['question', 'link', 'discussion']:  # fixme: make dynamic
 +            return werkzeug.utils.redirect('/forum/%s' % slug(forum))
          if not user.email or not tools.single_email_re.match(user.email):
 -            return werkzeug.utils.redirect("/forum/%s/user/%s/edit?email_required=1" % (slug(forum), uid))
 -        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)))
 +            return werkzeug.utils.redirect("/forum/%s/user/%s/edit?email_required=1" % (slug(forum), request.session.uid))
-         values = self._prepare_forum_values(forum=forum, searches={},  header={'ask_hide': True})
++        values = self._prepare_forum_values(forum=forum, searches={}, header={'ask_hide': True})
 +        return request.website.render("website_forum.new_%s" % post_type, values)
 +
 +    @http.route(['/forum/<model("forum.forum"):forum>/new',
 +                 '/forum/<model("forum.forum"):forum>/<model("forum.post"):post_parent>/reply'],
 +                type='http', auth="public", methods=['POST'], website=True)
 +    def post_create(self, forum, post_parent=None, post_type=None, **post):
 +        cr, uid, context = request.cr, request.uid, request.context
 +        if not request.session.uid:
 +            return login_redirect()
 +
-         post_tag_ids = []
-         Tag = request.env['forum.tag']
-         if post.get('post_tags', False) and post.get('post_tags').strip('[]'):
-             tags = post.get('post_tags').strip('[]').replace('"', '').split(",")
-             for tag in tags:
-                 tag_rec = Tag.search([('name', '=', tag)])
-                 if tag_rec:
-                     post_tag_ids.append((4, tag_rec.id))
-                 else:
-                     post_tag_ids.append((0, 0, {'name': tag, 'forum_id': forum.id}))
++        post_tag_ids = forum.tag_to_write_vals(post.get('post_tags', False))
 +        new_question = request.env['forum.post'].create({
 +            'forum_id': forum.id,
 +            'name': post.get('post_name', ''),
 +            'content': post.get('content', False),
 +            'content_link': post.get('content_link', False),
 +            'parent_id': post_parent and post_parent.id or False,
 +            'tag_ids': post_tag_ids,
 +            'post_type': post_parent and post_parent.post_type or post_type,  # tde check in selection field
 +        })
 +        return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), post_parent and slug(post_parent) or new_question.id))
  
      @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/comment', type='http', auth="public", methods=['POST'], website=True)
      def post_comment(self, forum, post, **kwargs):
  
      @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/edit', type='http', auth="user", website=True)
      def post_edit(self, forum, post, **kwargs):
-         tags = ""
-         for tag_name in post.tag_ids:
-             tags += tag_name.name + ","
 -        tag_version = kwargs.get('tag_type', 'texttext')
 -        if tag_version == "texttext":  # old version - retro v8 - #TODO Remove in master
 -            tags = ""
 -            for tag_name in post.tag_ids:
 -                tags += tag_name.name + ","
 -        elif tag_version == "select2":  # new version
 -            tags = [dict(id=tag.id, name=tag.name) for tag in post.tag_ids]
 -            tags = simplejson.dumps(tags)
++        tags = [dict(id=tag.id, name=tag.name) for tag in post.tag_ids]
++        tags = simplejson.dumps(tags)
          values = self._prepare_forum_values(forum=forum)
 -
          values.update({
              'tags': tags,
              'post': post,
          })
          return request.website.render("website_forum.edit_post", values)
  
 -    @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/edition', type='http', auth="user", website=True)
 -    def post_edit_retro(self, forum, post, **kwargs):
 -        # This function is only there for retrocompatibility between old template using texttext and template using select2
 -        # It should be removed into master  #TODO JKE: remove in master all condition with tag_type
 -        kwargs.update(tag_type="select2")
 -        return self.post_edit(forum, post, **kwargs)
 -
      @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/save', type='http', auth="user", methods=['POST'], website=True)
      def post_save(self, forum, post, **kwargs):
-         post_tags = []
-         if kwargs.get('post_tag') and kwargs.get('post_tag').strip('[]'):
-             Tag = request.env['forum.tag']
-             tags = kwargs.get('post_tag').strip('[]').replace('"', '').split(",")
-             for tag in tags:
-                 tag_rec = Tag.search([('name', '=', tag)])
-                 if tag_rec:
-                     post_tags += tag_rec.ids
-                 else:
-                     new_tag = Tag.create({'name': tag, 'forum_id': forum.id})
-                     post_tags.append(new_tag.id)
 -        cr, uid, context = request.cr, request.uid, request.context
 -        question_tags = []
 -        User = request.registry['res.users']
 -        Tag = request.registry['forum.tag']
 -        tag_version = kwargs.get('tag_type', 'texttext')
 -        if tag_version == "texttext":  # old version - retro v8 - #TODO Remove in master
 -            if kwargs.get('question_tag') and kwargs.get('question_tag').strip('[]'):
 -                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)
 -        elif tag_version == "select2":  # new version
 -            for tag in filter(None, kwargs.get('question_tag', '').split(',')):
 -                if tag.startswith('_'):  # it's a new tag
 -                # check if user have Karma needed to create need tag
 -                    user = User.browse(cr, SUPERUSER_ID, uid, context=context)
 -                    if user.exists() and user.karma >= forum.karma_retag:
 -                        # check that not arleady created meanwhile and maybe excluded by the limit on the search
 -                        tag_ids = Tag.search(cr, uid, [('name', '=', tag[1:])], context=context)
 -                        if tag_ids:
 -                            new_tag = tag_ids
 -                        else:
 -                            new_tag = Tag.create(cr, uid, {'name': tag[1:], 'forum_id': forum.id}, context=context)
 -                        question_tags.append(new_tag)
 -                else:
 -                    question_tags += [int(tag)]
 -
++        post_tags = forum.tag_to_write_vals(kwargs.get('post_tag', ''))
          vals = {
-             'tag_ids': [(6, 0, post_tags)],
 -            'tag_ids': [(6, 0, question_tags)],
 -            'name': kwargs.get('question_name'),
++            'tag_ids': post_tags,
 +            'name': kwargs.get('post_name'),
              'content': kwargs.get('content'),
          }
 -        request.registry['forum.post'].write(cr, uid, [post.id], vals, context=context)
 +        post.write(vals)
          question = post.parent_id if post.parent_id else post
          return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
  
              elif rec['vote'] == '-1':
                  down_votes = rec['vote_count']
  
 -        #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)
 +        # Votes which given by users on others questions and answers.
 +        vote_ids = Vote.search([('user_id', '=', user.id)])
  
 -        #activity by user.
 -        model, comment = Data.get_object_reference(cr, uid, 'mail', 'mt_comment')
 -        activity_ids = Activity.search(cr, uid, [('res_id', 'in', user_question_ids+user_answer_ids), ('model', '=', 'forum.post'), ('subtype_id', '!=', comment)], order='date DESC', limit=100, context=context)
 -        activities = Activity.browse(cr, uid, activity_ids, context=context)
 +        # activity by user.
 +        model, comment = Data.get_object_reference('mail', 'mt_comment')
-         activities = Activity.search([('res_id', 'in', (user_question_ids+user_answer_ids).ids), ('model', '=', 'forum.post'), ('subtype_id', '!=', comment)], order='date DESC', limit=100)
++        activities = Activity.search([('res_id', 'in', (user_question_ids + user_answer_ids).ids), ('model', '=', 'forum.post'), ('subtype_id', '!=', comment)],
++                                     order='date DESC', limit=100)
  
          posts = {}
          for act in activities:
@@@ -19,7 -19,8 +19,7 @@@
          <record id="action_open_forum" model="ir.actions.act_url">
              <field name="name">Forum</field>
              <field name="target">self</field>
-             <field name="url" eval="'/forum/'+str(ref('website_forum.forum_help'))+'/#tutorial.forum=true'"/>
 -            <!-- TODO in master : remove the tutorial or create one tutorial forum -->
 -            <field name="url" eval="'/forum/'+str(ref('website_forum.forum_help'))+'/#tutorial.forum=true'"/>
++            <field name="url" eval="'/forum/'+str(ref('website_forum.forum_help'))"/>
          </record>
          <record id="base.open_menu" model="ir.actions.todo">
              <field name="action_id" ref="action_open_forum"/>
@@@ -16,6 -14,9 +16,7 @@@ from openerp.exceptions import Warnin
  
  _logger = logging.getLogger(__name__)
  
++
  class KarmaError(Forbidden):
      """ Karma-related error, used for forum and posts. """
      pass
@@@ -41,70 -78,49 +42,90 @@@ class Forum(models.Model)
              return f.read()
          return False
  
 -    _defaults = {
 -        'description': 'This community is for professionals and enthusiasts of our products and services.',
 -        'faq': _get_default_faq,
 -        'karma_gen_question_new': 0,  # set to null for anti spam protection
 -        'karma_gen_question_upvote': 5,
 -        'karma_gen_question_downvote': -2,
 -        'karma_gen_answer_upvote': 10,
 -        'karma_gen_answer_downvote': -2,
 -        'karma_gen_answer_accept': 2,
 -        'karma_gen_answer_accepted': 15,
 -        'karma_gen_answer_flagged': -100,
 -        'karma_ask': 3,  # set to not null for anti spam protection
 -        'karma_answer': 3,  # set to not null for anti spam protection
 -        'karma_edit_own': 1,
 -        'karma_edit_all': 300,
 -        'karma_close_own': 100,
 -        'karma_close_all': 500,
 -        'karma_unlink_own': 500,
 -        'karma_unlink_all': 1000,
 -        'karma_upvote': 5,
 -        'karma_downvote': 50,
 -        'karma_answer_accept_own': 20,
 -        'karma_answer_accept_all': 500,
 -        'karma_editor_link_files': 20,
 -        'karma_editor_clickable_link': 20,
 -        'karma_comment_own': 3,
 -        'karma_comment_all': 5,
 -        'karma_comment_convert_own': 50,
 -        'karma_comment_convert_all': 500,
 -        'karma_comment_unlink_own': 50,
 -        'karma_comment_unlink_all': 500,
 -        'karma_retag': 75,
 -        'karma_flag': 500,
 -    }
 -
 -    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):
 +    # description and use
 +    name = fields.Char('Forum Name', required=True, translate=True)
 +    faq = fields.Html('Guidelines', default=_get_default_faq, translate=True)
 +    description = fields.Html(
 +        'Description',
 +        default='<p> This community is for professionals and enthusiasts of our products and services.'
 +                'Share and discuss the best content and new marketing ideas,'
 +                'build your professional profile and become a better marketer together.</p>')
 +    default_order = fields.Selection([
 +        ('create_date desc', 'Newest'),
 +        ('write_date desc', 'Last Updated'),
 +        ('vote_count desc', 'Most Voted'),
 +        ('relevancy desc', 'Relevancy'),
 +        ('child_count desc', 'Answered')],
 +        string='Default Order', required=True, default='write_date desc')
 +    relevancy_post_vote = fields.Float('First Relevancy Parameter', default=0.8)
 +    relevancy_time_decay = fields.Float('Second Relevancy Parameter', default=1.8)
 +    default_post_type = fields.Selection([
 +        ('question', 'Question'),
 +        ('discussion', 'Discussion'),
 +        ('link', 'Link')],
 +        string='Default Post', required=True, default='question')
 +    allow_question = fields.Boolean('Questions', help="Users can answer only once per question. Contributors can edit answers and mark the right ones.", default=True)
 +    allow_discussion = fields.Boolean('Discussions', default=False)
 +    allow_link = fields.Boolean('Links', help="When clicking on the post, it redirects to an external link", default=False)
 +    # karma generation
 +    karma_gen_question_new = fields.Integer(string='Asking a question', default=2)
 +    karma_gen_question_upvote = fields.Integer(string='Question upvoted', default=5)
 +    karma_gen_question_downvote = fields.Integer(string='Question downvoted', default=-2)
 +    karma_gen_answer_upvote = fields.Integer(string='Answer upvoted', default=10)
 +    karma_gen_answer_downvote = fields.Integer(string='Answer downvoted', default=-2)
 +    karma_gen_answer_accept = fields.Integer(string='Accepting an answer', default=2)
 +    karma_gen_answer_accepted = fields.Integer(string='Answer accepted', default=15)
 +    karma_gen_answer_flagged = fields.Integer(string='Answer flagged', default=-100)
 +    # karma-based actions
 +    karma_ask = fields.Integer(string='Ask a new question', default=3)
 +    karma_answer = fields.Integer(string='Answer a question', default=3)
 +    karma_edit_own = fields.Integer(string='Edit its own posts', default=1)
 +    karma_edit_all = fields.Integer(string='Edit all posts', default=300)
 +    karma_close_own = fields.Integer(string='Close its own posts', default=100)
 +    karma_close_all = fields.Integer(string='Close all posts', default=500)
 +    karma_unlink_own = fields.Integer(string='Delete its own posts', default=500)
 +    karma_unlink_all = fields.Integer(string='Delete all posts', default=1000)
 +    karma_upvote = fields.Integer(string='Upvote', default=5)
 +    karma_downvote = fields.Integer(string='Downvote', default=50)
 +    karma_answer_accept_own = fields.Integer(string='Accept an answer on its own questions', default=20)
 +    karma_answer_accept_all = fields.Integer(string='Accept an answers to all questions', default=500)
 +    karma_editor_link_files = fields.Integer(string='Linking files (Editor)', default=20)
 +    karma_editor_clickable_link = fields.Integer(string='Add clickable links (Editor)', default=20)
 +    karma_comment_own = fields.Integer(string='Comment its own posts', default=1)
 +    karma_comment_all = fields.Integer(string='Comment all posts', default=1)
 +    karma_comment_convert_own = fields.Integer(string='Convert its own answers to comments and vice versa', default=50)
 +    karma_comment_convert_all = fields.Integer(string='Convert all answers to answers and vice versa', default=500)
 +    karma_comment_unlink_own = fields.Integer(string='Unlink its own comments', default=50)
 +    karma_comment_unlink_all = fields.Integer(string='Unlinnk all comments', default=500)
 +    karma_retag = fields.Integer(string='Change question tags', default=75)
 +    karma_flag = fields.Integer(string='Flag a post as offensive', default=500)
 +
 +    @api.model
 +    def create(self, values):
 +        return super(Forum, self.with_context(mail_create_nolog=True)).create(values)
 +
++    @api.model
++    def tag_to_write_vals(self, tags=''):
++        User = self.env['res.users']
++        Tag = self.env['forum.tag']
++        post_tags = []
++        for tag in filter(None, tags.split(',')):
++            if tag.startswith('_'):  # it's a new tag
++                # check that not arleady created meanwhile or maybe excluded by the limit on the search
++                tag_ids = Tag.search([('name', '=', tag[1:])])
++                if tag_ids:
++                    post_tags.append((4, int(tag_ids[0])))
++                else:
++                    # check if user have Karma needed to create need tag
++                    user = User.sudo().browse(self._uid)
++                    if user.exists() and user.karma >= self.karma_retag:
++                            post_tags.append((0, 0, {'name': tag[1:], 'forum_id': self.id}))
++            else:
++                post_tags.append((4, int(tag)))
++        return post_tags
++
 +
 +class Post(models.Model):
      _name = 'forum.post'
      _description = 'Forum Post'
      _inherit = ['mail.thread', 'website.seo.metadata']
diff --cc addons/website_forum/static/src/js/website.tour.forum.js
index b4ca841,009b9a5..0000000
deleted file mode 100644,100644
+++ /dev/null
@@@ -1,31 -1,13 +1,0 @@@
--(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();
-     //     }
-     // });
- }());
 -    openerp.Tour.register({
 -        id: 'forum',
 -        name: '',
 -        steps : [{}],
 -    });
 -
 -}());
@@@ -1,5 -1,5 +1,7 @@@
  $(document).ready(function () {
      if ($('.website_forum').length){
++        $("[data-toggle='popover']").popover();
++        
          $('.karma_required').on('click', function (ev) {
              var karma = $(ev.currentTarget).data('karma');
              if (karma) {
              openerp.jsonRpc("/forum/validate_email/close", 'call', {});
          });
  
 +        $('.js_close_intro').on('click', function (ev) {
 +            ev.preventDefault();
 +            document.cookie = "no_introduction_message = false";
 +            return true;
 +        });
 +
 +        $('.link_url').on('change', function (ev) {
 +            ev.preventDefault();
 +            var $link = $(ev.currentTarget);
 +            if ($link.attr("value").search("^http(s?)://.*")) {
 +                var $warning = $('<div class="alert alert-danger alert-dismissable" style="position:absolute; margin-top: -180px; margin-left: 90px;">'+
 +                    '<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
 +                    'Please enter valid URl.'+
 +                    '</div>');
 +                $link.parent().append($warning);
 +                $link.parent().find("button#btn_post_your_article")[0].disabled = true;
 +                $link.parent().find("input[name='content']")[0].value = '';
 +            } else {
 +                openerp.jsonRpc("/forum/get_url_title", 'call', {'url': $link.attr("value")}).then(function (data) {
 +                    $link.parent().find("input[name='content']")[0].value = data;
 +                    $('button').prop('disabled', false);
 +                    $('input').prop('readonly', false);
 +                });
 +            }
 +        });
 +
-         if($('input.load_tags').length){
-             var tags = $("input.load_tags").val();
-             $("input.load_tags").val("");
-             set_tags(tags);
-         };
-         function htmlEntities(str) {
-             return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
-         }
-         function set_tags(tags) {
-             $("input.load_tags").textext({
-                 plugins: 'tags focus autocomplete ajax',
-                 ext: {
-                     autocomplete: {
-                         onSetSuggestions : function(e, data) {
-                             var self        = this,
-                                 val         = self.val(),
-                                 suggestions = self._suggestions = data.result;
-                             if(data.showHideDropdown !== false)
-                                 self.trigger(suggestions === null || suggestions.length === 0 && val.length === 0 ? "hideDropdown" : "showDropdown");
-                         },
-                         renderSuggestions: function(suggestions) {
-                             var self = this,
-                                 val  = self.val();
-                             self.clearItems();
-                             $.each(suggestions || [], function(index, item) {
-                                 self.addSuggestion(htmlEntities(item));
-                             });
-                             var lowerCasesuggestions = $.map(suggestions, function(n,i){return n.toLowerCase();});
-                             if(jQuery.inArray(val.toLowerCase(), lowerCasesuggestions) ==-1) {
-                                 self.addSuggestion("Create '" + htmlEntities(val) + "'");
-                             }
-                         },
-                     },
-                     tags: {
-                         onEnterKeyPress: function(e) {
-                             var self = this,
-                                 val  = self.val(),
-                                 tag  = self.itemManager().stringToItem(val);
-                             if(self.isTagAllowed(tag)) {
-                                 tag = tag.replace(/Create\ '|\'|'/g,'');
-                                 self.addTags([ tag ]);
-                                 // refocus the textarea just in case it lost the focus
-                                 self.core().focusInput();
-                             }
-                         },
+         $('input.js_select2').select2({
+             tags: true,
+             tokenSeparators: [",", " ", "_"],
+             maximumInputLength: 35,
+             minimumInputLength: 2,
+             maximumSelectionSize: 5,
+             lastsearch: [],
+             createSearchChoice: function (term) {
+                 if ($(lastsearch).filter(function () { return this.text.localeCompare(term) === 0;}).length === 0) {
+                     //check Karma
+                     if (parseInt($("#karma").val()) >= parseInt($("#karma_retag").val())) {
+                         return {
+                             id: "_" + $.trim(term),
+                             text: $.trim(term) + ' *',
+                             isNew: true,
+                         };
                      }
+                     
+                 }
+             },
+             formatResult: function(term) {
+                 if (term.isNew) {
+                     return '<span class="label label-primary">New</span> ' + _.escape(term.text);
+                 }
+                 else {
+                     return _.escape(term.text);
+                 }
+             },
+             ajax: {
+                 url: '/forum/get_tags',
+                 dataType: 'json',
+                 data: function(term, page) {
+                     return {
+                         q: term,
 -                        t: 'select2',
+                         l: 50
+                     };
                  },
-                 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
+                 results: function(data, page) {
+                     var ret = [];
+                     _.each(data, function(x) {
+                         ret.push({ id: x.id, text: x.name, isNew: false });
+                     });
+                     lastsearch = ret;
+                     return { results: ret };
                  }
-             });
+             },
  
-             $("input.load_tags").on('isTagAllowed', function(e, data) {
-                 if (_.indexOf($(this).textext()[0].tags()._formData, data.tag) != -1) {
-                     data.result = false;
-                 }
-             });
-         }
+             // Take default tags from the input value
+             initSelection: function (element, callback) {
+                 var data = [];
+                 _.each(JSON.parse(element.val()), function(x) {
+                     data.push({ id: x.id, text: x.name, isNew: false });
+                 });
+                 element.val('');
+                 callback(data);
+             },
+         });
  
 -        //TODO Remove in master
 -        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
 -                }
 -            });
 -            // Adds: create tags on space + blur
 -            $("input.load_tags").on('whitespaceKeyDown blur', function () {
 -                $(this).textext()[0].tags().addTags([ $(this).val() ]);
 -                $(this).val("");
 -            });
 -            $("input.load_tags").on('isTagAllowed', function(e, data) {
 -                if (_.indexOf($(this).textext()[0].tags()._formData, data.tag) != -1) {
 -                    data.result = false;
 +        if ($('textarea.load_editor').length) {
 +            $('textarea.load_editor').each(function () {
 +                if (this['id']) {
 +                    CKEDITOR.replace(this['id']).on('instanceReady', CKEDITORLoadComplete);
                  }
              });
          }
      }
  });
  
--
 -
  function IsKarmaValid(eventNumber,minKarma){
      "use strict";
      if(parseInt($("#karma").val()) >= minKarma){
@@@ -2,33 -2,6 +2,32 @@@
  <openerp>
      <data>
  
 +<!-- Editor custom -->
 +<template id="assets_editor" inherit_id="website.assets_editor" name="Forum Editor Assets" groups="base.group_user">
 +    <xpath expr="." position="inside">
-         <script type="text/javascript" src="/website_forum/static/src/js/website.tour.forum.js"/>
 +        <script type="text/javascript" src="/website_forum/static/src/js/website_forum.editor.js"/>
 +    </xpath>
 +</template>
 +
 +<!-- Front-end custom css / js + ckeditor lib and customization -->
 +<template id="assets_frontend" inherit_id="website.assets_frontend" name="Forum Assets">
 +    <xpath expr="." position="inside">
 +        <link rel='stylesheet' href="/web/static/lib/jquery.textext/jquery.textext.css"/>
 +        <link rel='stylesheet' href='/website_forum/static/src/css/website_forum.css'/>
 +        <script type="text/javascript" src="/website_forum/static/src/js/website_forum.js"/>
 +        <script type="text/javascript" src="/web/static/lib/jquery.textext/jquery.textext.js"/>
 +        <script type="text/javascript">
 +            var CKEDITOR_BASEPATH = '/web/static/lib/ckeditor/';
 +        </script>
 +        <script type="text/javascript" src="/web/static/lib/ckeditor/ckeditor.js"></script>
 +         <script type="text/javascript">
 +                CKEDITOR.config.toolbar = [['Bold','Italic','Underline','Strike'],['NumberedList','BulletedList', 'Blockquote']
 +                ,['Outdent','Indent','Link','Unlink','Image'],] ;
 +        </script>
 +
 +    </xpath>
 +</template>
 +
  <!-- Layout add nav and footer -->
  <template id="header_footer_custom" inherit_id="website.footer_default"
      name="Footer Questions Link">
      </t>
  </template>
  
 +<!-- Edition: Post Article -->
 +<template id="new_link">
 +    <t t-call="website_forum.header">
 +        <h1 class="mt0">Submit a Link</h1>
 +        <p class="mb32">
 +            Share an awesome link. Your post will appear in the 'Newest' top-menu.
 +            If the community vote on your post, it will get traction by being promoted
 +            in the homepage.
 +        </p><p>
 +            We keep a high level of quality in showcased posts, only 20% of the submited
 +            posts will be featured.
 +        </p>
 +        <form t-attf-action="/forum/#{ slug(forum) }/new?post_type=link" method="post" role="form" class="tag_text form-horizontal">
 +            <input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
 +            <div class="form-group">
 +                <label class="col-sm-2 control-label" for="content_link">URL to Share</label>
 +                <div class="col-sm-8">
 +                    <input type="text" name="post_name" required="True" t-attf-value="#{post_name}"
 +                        class="form-control mb16 link_url" placeholder="e.g. https://www.odoo.com"/>
 +                </div>
 +            </div>
 +            <div class="form-group">
 +                <label class="col-sm-2 control-label" for="post_name">Post Title</label>
 +                <div class="col-sm-8">
 +                    <input type="text" name="content" readonly="True" required="True" t-attf-value="#{content}"
 +                        class="form-control"/>
 +                </div>
 +            </div>
 +            <div class="form-group">
 +                <label class="col-sm-2 control-label" for="post_tags">Tags</label>
 +                <div class="col-sm-8">
 +                    <input type="text" name="post_tags" readonly="True" class="form-control load_tags"/>
 +                </div>
 +            </div>
 +            <div class="form-group">
 +                <div class="col-sm-offset-2 col-sm-8">
 +                    <button class="btn btn-primary" disabled="True" id="btn_post_your_article">Post Your Article</button>
 +                </div>
 +            </div>
 +        </form>
 +    </t>
 +</template>
 +
 +<!-- Edition: Post your Discussion Topic -->
 +<template id="new_discussion">
 +    <t t-call="website_forum.header">
 +        <h1 class="mt0">Post Your Discussion Topic</h1>
 +        <p>
 +            <b>Share</b> Start Something Awesome
 +        </p>
 +        <form t-attf-action="/forum/#{slug(forum)}/new?post_type=discussion" method="post" role="form" class="tag_text">
 +            <input type="text" name="post_name" required="True" t-attf-value="#{post_name}"
 +                class="form-control mb16" placeholder="Your Discussion Title..."/>
 +            <input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
 +            <textarea name="content" id="content" required="True" class="form-control load_editor">
 +                <t t-esc="question_content"/>
 +            </textarea>
 +            <br/>
 +            <input type="text" name="post_tags" placeholder="Tags" class="form-control load_tags"/>
-             <br/>
++            <br/><br/>
 +            <button class="btn btn-primary">Post Your Topic</button>
 +        </form>
 +    </t>
 +</template>
 +
  <!-- Edition: ask your question -->
 -<template id="ask_question">
 +<template id="new_question">
      <t t-call="website_forum.header">
-         <t t-set="head">
-             <script type="text/javascript">
-                 window.onload = function() {
-                     $("[data-toggle='popover']").popover();
-                 };
-             </script>
-         </t>
 -        <h1 class="mt0">Ask your Question</h1>
 +        <h1 class="mt0">Ask Your Question</h1>
 +        <p>
 +            To improve your chance getting an answer:
 +        </p>
          <ul>
 -            <li> please, try to make your question interesting to others </li>
 -            <li> provide enough details and, if possible, give an example </li>
 -            <li> be clear and concise, avoid unnecessary introductions (Hi, ... Thanks...) </li>
 +            <li>Set a clear, explicit and concise question title
 +                (check
 +                <a href="#" data-placement="top" data-toggle="popover" data-content="Inventory Date Problem, Task remaining hours, Can you help solve solve my tax computation problem in Canada?" title="Click to get bad question samples">bad examples</a>
 +                and
 +                <a href="#" data-placement="bottom" data-toggle="popover" data-content="How to create a physical inventory at an anterior date?, How is the 'remaining hours' field computed on tasks?, How to configure TPS and TVQ's canadian taxes?" title="Click to get good question titles">good examples</a>
 +                ),
 +            </li>
 +            <li>Avoid unnecessary introductions (Hi,... Please... Thanks...),</li>
 +            <li>Provide enough details and, if possible, give an example.</li>
          </ul>
 -        <form t-attf-action="/forum/#{ slug(forum) }/question/new" method="post" role="form" class="tag_text">
 -            <input type="text" name="question_name" required="True" t-attf-value="#{question_name}"
 -                class="form-control" placeholder="Enter your Question"/>
 -            <h5 class="mt20">Please enter a descriptive question (should finish with a '?')</h5>
 +        <form t-attf-action="/forum/#{slug(forum)}/new?post_type=question" method="post" role="form" class="tag_text">
 +            <input type="text" name="post_name" required="True" t-attf-value="#{post_name}"
 +                class="form-control mb16" placeholder="Your Question Title..."/>
              <input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
 -            <input type="hidden" name="karma_retag" t-attf-value="#{forum.karma_retag}" id="karma_retag"/>
 -            <textarea name="content" required="True" class="form-control load_editor">
++
 +            <textarea name="content" required="True" id="content" class="form-control load_editor">
                  <t t-esc="question_content"/>
              </textarea>
              <br/>
-             <input type="text" name="post_tags" placeholder="Tags" class="form-control load_tags"/>
-             <br/>
 -            <input type="hidden" name="tag_type" value="select2"/>
 -            <input type="hidden" name="question_tags" placeholder="Tags" class="form-control js_select2"/>
 -            <br/>
++            <input type="hidden" name="karma_retag" t-attf-value="#{forum.karma_retag}" id="karma_retag"/>
++            <input type="hidden" name="post_tags" placeholder="Tags" class="form-control js_select2"/>
++            <br/><br/>
              <button t-attf-class="btn btn-primary #{(user.karma &lt; forum.karma_ask) and 'karma_required' or ''}"
 -                id="btn_ask_your_question" t-att-data-karma="forum.karma_ask">Post Your Question</button>
 +                t-att-data-karma="forum.karma_ask">Post Your Question</button>
          </form>
 -        <script type="text/javascript">
 -            CKEDITOR.replace("content");
 -        </script>
      </t>
  </template>
  
  <!-- Edition: edit a post -->
  <template id="edit_post">
      <t t-call="website_forum.header">
 -        <h3 t-if="not is_answer">Edit question</h3>
 -        <h3 t-if="is_answer">Edit answer</h3>
 +        <h3 t-if="not is_answer">Edit <span t-field="post.post_type"/></h3>
 +        <h3 t-if="is_answer">Edit reply</h3>
          <form t-attf-action="/forum/#{slug(forum)}/post/#{slug(post)}/save" method="post" role="form" class="tag_text">
              <div t-if="not is_answer">
 -                <input type="text" name="question_name" id="question_name" required="True"
 -                    t-attf-value="#{post.name}" class="form-control" placeholder="Edit your Question"/>
 -                <h5 class="mt20">Please enter a descriptive question (should finish by a '?')</h5>
 +                <input type="text" name="post_name" required="True"
 +                    t-attf-value="#{post.name}" class="form-control mb8" placeholder="Edit your Post"/>
 +                <h5 t-if="post.post_type == 'question'" class="mt20">Please enter a descriptive question (should finish by a '?')</h5>
              </div>
              <input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
+             <input type="hidden" name="karma_retag" t-attf-value="#{forum.karma_retag}" id="karma_retag"/>
 -            <textarea name="content" required="True" class="form-control load_editor">
 +            <textarea name="content" id="content" required="True" class="form-control load_editor">
                  <t t-esc="post.content"/>
              </textarea>
              <div t-if="not is_answer">
                  <br/>
-                 <input type="text" name="post_tag" class="form-control col-md-9 load_tags" placeholder="Tags" t-attf-value="#{tags}"/>
 -                <input type="hidden" name="tag_type" value="select2"/>
 -                <input type="hidden" name="question_tag" class="form-control col-md-9 js_select2" placeholder="Tags" t-attf-value="#{tags}"/>
++                <input type="text" name="post_tag" class="form-control col-md-9 js_select2" placeholder="Tags" t-attf-value="#{tags}"/>
                  <br/>
              </div>
              <button class="btn btn-primary btn-lg">Save</button>
diff --cc doc/conf.py
Simple merge