diff --git a/addons/gamification/models/gamification_karma_rank.py b/addons/gamification/models/gamification_karma_rank.py index a324b323773..8e987076bfe 100644 --- a/addons/gamification/models/gamification_karma_rank.py +++ b/addons/gamification/models/gamification_karma_rank.py @@ -3,6 +3,7 @@ from odoo import api, fields, models from odoo.tools.translate import html_translate + class KarmaRank(models.Model): _name = 'gamification.karma.rank' _description = 'Rank based on karma' @@ -14,14 +15,23 @@ class KarmaRank(models.Model): description_motivational = fields.Html( string='Motivational', translate=html_translate, sanitize_attributes=False, help="Motivational phrase to reach this rank") - karma_min = fields.Integer(string='Required Karma', required=True, default=1, + karma_min = fields.Integer( + string='Required Karma', required=True, default=1, help='Minimum karma needed to reach this rank') user_ids = fields.One2many('res.users', 'rank_id', string='Users', help="Users having this rank") + rank_users_count = fields.Integer("# Users", compute="_compute_rank_users_count") _sql_constraints = [ ('karma_min_check', "CHECK( karma_min > 0 )", 'The required karma has to be above 0.') ] + @api.depends('user_ids') + def _compute_rank_users_count(self): + requests_data = self.env['res.users'].read_group([('rank_id', '!=', False)], ['rank_id'], ['rank_id']) + requests_mapped_data = dict((data['rank_id'][0], data['rank_id_count']) for data in requests_data) + for rank in self: + rank.rank_users_count = requests_mapped_data.get(rank.id, 0) + @api.model_create_multi def create(self, values_list): res = super(KarmaRank, self).create(values_list) diff --git a/addons/gamification/views/gamification_karma_rank_views.xml b/addons/gamification/views/gamification_karma_rank_views.xml index fc0205794b5..bee0f28f40e 100644 --- a/addons/gamification/views/gamification_karma_rank_views.xml +++ b/addons/gamification/views/gamification_karma_rank_views.xml @@ -1,6 +1,14 @@ + + + Users + res.users + tree,form + [('rank_id', '=', active_id)] + + Ranks @@ -36,6 +44,7 @@ + @@ -46,7 +55,16 @@
-
+
+ +
- - - - - + + + + + + + +
diff --git a/addons/website_forum/__manifest__.py b/addons/website_forum/__manifest__.py index e7e3423035c..19e72d59585 100644 --- a/addons/website_forum/__manifest__.py +++ b/addons/website_forum/__manifest__.py @@ -17,6 +17,7 @@ Ask questions, get answers, no distractions 'website_profile', ], 'data': [ + 'data/forum_default_faq.xml', 'data/forum_data.xml', 'views/forum.xml', 'views/res_users_views.xml', diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index b309a644be0..51a4c00b0c1 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -45,7 +45,10 @@ class WebsiteForum(WebsiteProfile): forums = request.env['forum.forum'].search(domain) if len(forums) == 1: return werkzeug.utils.redirect('/forum/%s' % slug(forums[0]), code=302) - return request.render("website_forum.forum_all", {'forums': forums}) + + return request.render("website_forum.forum_all", { + 'forums': forums + }) @http.route('/forum/new', type='json', auth="user", methods=['POST'], website=True) def forum_create(self, forum_name="New Forum", forum_mode="questions", forum_privacy="public", forum_privacy_group=False, add_menu=False): @@ -164,6 +167,11 @@ class WebsiteForum(WebsiteProfile): values = self._prepare_user_values(forum=forum, searches=dict(), header={'is_guidelines': True}, **post) return request.render("website_forum.faq", values) + @http.route(['/forum//faq/karma'], type='http', auth="public", website=True, sitemap=False) + def forum_faq_karma(self, forum, **post): + values = self._prepare_user_values(forum=forum, header={'is_guidelines': True, 'is_karma': True}, **post) + return request.render("website_forum.faq_karma", values) + @http.route('/forum/get_tags', type='http', auth="public", methods=['GET'], website=True, sitemap=False) def tag_read(self, query='', limit=25, **post): data = request.env['forum.tag'].search_read( diff --git a/addons/website_forum/data/forum_data.xml b/addons/website_forum/data/forum_data.xml index bda7eb83e21..3000a08e72e 100644 --- a/addons/website_forum/data/forum_data.xml +++ b/addons/website_forum/data/forum_data.xml @@ -77,55 +77,55 @@ - duplicate post + Duplicate post basic - off-topic or not relevant + Off-topic or not relevant basic - too subjective and argumentative + Too subjective and argumentative basic - not a real post + Not a real post basic - not relevant or out dated + Not relevant or out dated basic - contains offensive or malicious remarks + Contains offensive or malicious remarks basic - spam or advertising + Spam or advertising basic - too localized + Too localized basic - insulting and offensive language + Insulting and offensive language offensive - violent language + Violent language offensive - inappropriate and unacceptable statements + Inappropriate and unacceptable statements offensive - threatening language + Threatening language offensive - racist and hate speech + Racist and hate speech offensive diff --git a/addons/website_forum/data/forum_default_faq.html b/addons/website_forum/data/forum_default_faq.html deleted file mode 100644 index 43f4489c67b..00000000000 --- a/addons/website_forum/data/forum_default_faq.html +++ /dev/null @@ -1,63 +0,0 @@ -
-
- -
-
-

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

-
    -
  • how to install Odoo on a specific infrastructure,
  • -
  • how to configure or customize Odoo to specific business needs,
  • -
  • what's the best way to use Odoo for a specific business need,
  • -
  • how to develop modules for your own need,
  • -
  • specific questions about Odoo 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.

-
-
-
-
- -
-
-

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 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 be complementary to answer, referring sources or additional reading.
  • -
-
-
-
-
- -
-
-

Answers should not add or expand questions. Insteadeither 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 be 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 given at the end explains reputation point requirements for each type of moderation task.

-
-
-
diff --git a/addons/website_forum/data/forum_default_faq.xml b/addons/website_forum/data/forum_default_faq.xml new file mode 100644 index 00000000000..71406580d45 --- /dev/null +++ b/addons/website_forum/data/forum_default_faq.xml @@ -0,0 +1,92 @@ + + + + Faq Accordion + qweb + website_forum.faq_accordion + +
+
+
+
+ +
+
+

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

+
    +
  • how to install Odoo on a specific infrastructure,
  • +
  • how to configure or customize Odoo to specific business needs,
  • +
  • what's the best way to use Odoo for a specific business need,
  • +
  • how to develop modules for your own need,
  • +
  • specific questions about Odoo 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.

+
+
+
+
+ +
+
+

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 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 be complementary to answer, referring sources or additional reading.
  • +
+
+
+
+
+ +
+
+

Answers should not add or expand questions. Insteadeither 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 be 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 given at the end explains reputation point requirements for each type of moderation task.

+
+
+
+
+ +
+
+

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

+

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.

+ Here a table with the privileges and the karma level +
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/addons/website_forum/data/forum_demo.xml b/addons/website_forum/data/forum_demo.xml index 2884cab5c76..041ef4c8831 100644 --- a/addons/website_forum/data/forum_demo.xml +++ b/addons/website_forum/data/forum_demo.xml @@ -48,6 +48,7 @@ In simple words does Odoo became CMS+ERP platform?

]]> + Re: How to configure alerts for employee contract expiration 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. @@ -59,6 +60,7 @@ In simple words does Odoo became CMS+ERP platform?

    ]]> + Re: CMS replacement for ERP and eCommerce Odoo v8 provides a web module and an e-commerce module: www.odoo.com/page/website-builder The CMS editor in Odoo web is nice but I prefer Drupal for customization and there is a Drupal module for Odoo. I think WP is better than Odoo web too. diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 55137df5148..d77270c55a9 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -11,6 +11,7 @@ from odoo import api, fields, models, tools, SUPERUSER_ID, _ from odoo.exceptions import UserError, ValidationError, AccessError from odoo.tools import misc from odoo.tools.translate import html_translate +from odoo.addons.http_routing.models.ir_http import slug _logger = logging.getLogger(__name__) @@ -21,18 +22,13 @@ class Forum(models.Model): _inherit = ['mail.thread', 'image.mixin', 'website.seo.metadata', 'website.multi.mixin'] _order = "sequence" - @api.model - def _get_default_faq(self): - with misc.file_open('website_forum/data/forum_default_faq.html', 'r') as f: - return f.read() - # description and use name = fields.Char('Forum Name', required=True, translate=True) sequence = fields.Integer('Sequence', default=1) mode = fields.Selection([ - ('questions', 'Questions'), - ('discussions', 'Discussions')], - string='Forum Mode', required=True, default='questions', + ('questions', 'Questions (1 answer)'), + ('discussions', 'Discussions (multiple answers)')], + string='Mode', required=True, default='questions', help='Questions mode: only one answer allowed\n Discussions mode: multiple answers allowed') privacy = fields.Selection([ ('public', 'Public'), @@ -43,8 +39,9 @@ class Forum(models.Model): authorized_group_id = fields.Many2one('res.groups', 'Authorized Group') menu_id = fields.Many2one('website.menu', 'Menu', copy=False) active = fields.Boolean(default=True) - faq = fields.Html('Guidelines', default=_get_default_faq, translate=html_translate, sanitize=False) + faq = fields.Html('Guidelines', translate=html_translate, sanitize=False) description = fields.Text('Description', translate=True) + teaser = fields.Text('Teaser', compute='_compute_teaser', store=True) welcome_message = fields.Html( 'Welcome Message', translate=True, @@ -84,10 +81,11 @@ class Forum(models.Model): 'of the forum content.') # posts statistics post_ids = fields.One2many('forum.post', 'forum_id', string='Posts') - total_posts = fields.Integer('Post Count', compute='_compute_forum_statistics') - total_views = fields.Integer('Views Count', compute='_compute_forum_statistics') - total_answers = fields.Integer('Answers Count', compute='_compute_forum_statistics') - total_favorites = fields.Integer('Favorites Count', compute='_compute_forum_statistics') + last_post_id = fields.Many2one('forum.post', compute='_compute_last_post') + total_posts = fields.Integer('# Posts', compute='_compute_forum_statistics') + total_views = fields.Integer('# Views', compute='_compute_forum_statistics') + total_answers = fields.Integer('# Answers', compute='_compute_forum_statistics') + total_favorites = fields.Integer('# Favorites', compute='_compute_forum_statistics') count_posts_waiting_validation = fields.Integer(string="Number of posts waiting for validation", compute='_compute_count_posts_waiting_validation') count_flagged_posts = fields.Integer(string='Number of flagged posts', compute='_compute_count_flagged_posts') # karma generation @@ -128,11 +126,28 @@ class Forum(models.Model): karma_post = fields.Integer(string='Ask questions without validation', default=100) karma_moderate = fields.Integer(string='Moderate posts', default=1000) + @api.depends('post_ids') + def _compute_last_post(self): + for forum in self: + forum.last_post_id = forum.post_ids.search([('forum_id', '=', forum.id), ('parent_id', '=', False), ('state', '=', 'active')], order='create_date desc', limit=1) + + @api.depends('description') + def _compute_teaser(self): + for forum in self: + if forum.description: + desc = forum.description.replace('\n', ' ') + if len(forum.description) > 180: + forum.teaser = desc[:180] + '...' + else: + forum.teaser = forum.description + else: + forum.teaser = "" + @api.depends('post_ids.state', 'post_ids.views', 'post_ids.child_count', 'post_ids.favourite_count') def _compute_forum_statistics(self): result = dict((cid, dict(total_posts=0, total_views=0, total_answers=0, total_favorites=0)) for cid in self.ids) read_group_res = self.env['forum.post'].read_group( - [('forum_id', 'in', self.ids), ('state', 'in', ('active', 'close'))], + [('forum_id', 'in', self.ids), ('state', 'in', ('active', 'close')), ('parent_id', '=', False)], ['forum_id', 'views', 'child_count', 'favourite_count'], groupby=['forum_id'], lazy=False) @@ -141,7 +156,7 @@ class Forum(models.Model): result[cid]['total_posts'] += res_group.get('__count', 0) result[cid]['total_views'] += res_group.get('views', 0) result[cid]['total_answers'] += res_group.get('child_count', 0) - result[cid]['total_favorites'] += res_group.get('favourite_count', 0) + result[cid]['total_favorites'] += 1 if res_group.get('favourite_count', 0) else 0 for record in self: record.update(result[record.id]) @@ -156,9 +171,14 @@ class Forum(models.Model): domain = [('forum_id', '=', forum.id), ('state', '=', 'flagged')] forum.count_flagged_posts = self.env['forum.post'].search_count(domain) + def _set_default_faq(self): + self.faq = self.env['ir.ui.view']._render_template('website_forum.faq_accordion', {"forum": self}).decode('utf-8') + @api.model def create(self, values): - return super(Forum, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(values) + res = super(Forum, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(values) + res._set_default_faq() + return res def write(self, vals): if 'privacy' in vals: @@ -202,11 +222,21 @@ class Forum(models.Model): post_tags.insert(0, [6, 0, existing_keep]) return post_tags + def _compute_website_url(self): + return '/forum/%s' % (slug(self)) + def get_tags_first_char(self): """ get set of first letter of forum tags """ tags = self.env['forum.tag'].search([('forum_id', '=', self.id), ('posts_count', '>', 0)]) return sorted(set([tag.name[0].upper() for tag in tags if len(tag.name)])) + def go_to_website(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_url', + 'target': 'self', + 'url': self._compute_website_url(), + } class Post(models.Model): @@ -220,8 +250,8 @@ class Post(models.Model): content = fields.Html('Content', strip_style=True) plain_content = fields.Text('Plain Content', compute='_get_plain_content', store=True) tag_ids = fields.Many2many('forum.tag', 'forum_tag_rel', 'forum_id', 'forum_tag_id', string='Tags') - state = fields.Selection([('active', 'Active'), ('pending', 'Waiting Validation'), ('close', 'Close'), ('offensive', 'Offensive'), ('flagged', 'Flagged')], string='Status', default='active') - views = fields.Integer('Number of Views', default=0) + state = fields.Selection([('active', 'Active'), ('pending', 'Waiting Validation'), ('close', 'Closed'), ('offensive', 'Offensive'), ('flagged', 'Flagged')], string='Status', default='active') + views = fields.Integer('Views', default=0, readonly=True) active = fields.Boolean('Active', default=True) website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])]) website_id = fields.Many2one(related='forum_id.website_id', readonly=True) @@ -229,7 +259,7 @@ class Post(models.Model): # history create_date = fields.Datetime('Asked on', index=True, readonly=True) create_uid = fields.Many2one('res.users', string='Created by', index=True, readonly=True) - write_date = fields.Datetime('Update on', index=True, readonly=True) + write_date = fields.Datetime('Updated on', index=True, readonly=True) bump_date = fields.Datetime('Bumped on', readonly=True, help="Technical field allowing to bump a question. Writing on this field will trigger " "a write on write_date and therefore bump the post. Directly writing on write_date " @@ -245,14 +275,14 @@ class Post(models.Model): # favorite favourite_ids = fields.Many2many('res.users', string='Favourite') user_favourite = fields.Boolean('Is Favourite', compute='_get_user_favourite') - favourite_count = fields.Integer('Favorite Count', compute='_get_favorite_count', store=True) + favourite_count = fields.Integer('Favorite', compute='_get_favorite_count', store=True) # hierarchy is_correct = fields.Boolean('Correct', help='Correct answer or answer accepted') - parent_id = fields.Many2one('forum.post', string='Question', ondelete='cascade') + parent_id = fields.Many2one('forum.post', string='Question', ondelete='cascade', readonly=True, index=True) self_reply = fields.Boolean('Reply to own question', compute='_is_self_reply', store=True) - child_ids = fields.One2many('forum.post', 'parent_id', string='Answers') - child_count = fields.Integer('Number of answers', compute='_get_child_count', store=True) + child_ids = fields.One2many('forum.post', 'parent_id', string='Post Answers') + child_count = fields.Integer('Answers', compute='_get_child_count', store=True) uid_has_answered = fields.Boolean('Has Answered', compute='_get_uid_has_answered') has_validated_answer = fields.Boolean('Is answered', compute='_get_has_validated_answer', store=True) @@ -262,7 +292,7 @@ class Post(models.Model): # closing closed_reason_id = fields.Many2one('forum.post.reason', string='Reason') - closed_uid = fields.Many2one('res.users', string='Closed by', index=True) + closed_uid = fields.Many2one('res.users', string='Closed by', index=True, readonly=True) closed_date = fields.Datetime('Closed on', readonly=True) # karma calculation and access @@ -364,17 +394,10 @@ class Post(models.Model): for post in self: post.self_reply = post.parent_id.create_uid.id == post._uid - @api.depends('child_ids.create_uid', 'website_message_ids') + @api.depends('child_ids') def _get_child_count(self): for post in self: - children = self.search([('id', 'child_of', post.id)]) - children_count = len(children) - message_count = self.env['mail.message'].search_count([ - ('model', '=', post._name), - ('message_type', 'in', ['email', 'comment']), - ('res_id', 'in', children.ids + [post.id]), - ]) - post.child_count = children_count + message_count + post.child_count = len(post.child_ids) def _get_uid_has_answered(self): for post in self: @@ -407,8 +430,8 @@ class Post(models.Model): post.can_edit = is_admin or user.karma >= post.karma_edit post.can_close = is_admin or user.karma >= post.karma_close post.can_unlink = is_admin or user.karma >= post.karma_unlink - post.can_upvote = is_admin or user.karma >= post.forum_id.karma_upvote - post.can_downvote = is_admin or user.karma >= post.forum_id.karma_downvote + post.can_upvote = is_admin or user.karma >= post.forum_id.karma_upvote or post.user_vote == -1 + post.can_downvote = is_admin or user.karma >= post.forum_id.karma_downvote or post.user_vote == 1 post.can_comment = is_admin or user.karma >= post.karma_comment post.can_comment_convert = is_admin or user.karma >= post.karma_comment_convert post.can_view = is_admin or user.karma >= post.karma_close or (post_sudo.create_uid.karma > 0 and (post_sudo.active or post_sudo.create_uid == user)) @@ -869,6 +892,21 @@ class Post(models.Model): return return super(Post, self)._notify_record_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs) + def _compute_website_url(self): + return '/forum/{forum}/question/{post}{anchor}'.format( + forum=slug(self.forum_id), + post=slug(self), + anchor=self.parent_id and '#answer_%d' % self.id or '' + ) + + def go_to_website(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_url', + 'target': 'self', + 'url': self._compute_website_url(), + } + class PostReason(models.Model): _name = "forum.post.reason" @@ -876,7 +914,7 @@ class PostReason(models.Model): _order = 'name' name = fields.Char(string='Closing Reason', required=True, translate=True) - reason_type = fields.Char(string='Reason Type') + reason_type = fields.Selection([('basic', 'Basic'), ('offensive', 'Offensive')], string='Reason Type', default='basic') class Vote(models.Model): diff --git a/addons/website_forum/static/src/js/tours/website_forum.js b/addons/website_forum/static/src/js/tours/website_forum.js index d3d182701d2..55f57457ae6 100644 --- a/addons/website_forum/static/src/js/tours/website_forum.js +++ b/addons/website_forum/static/src/js/tours/website_forum.js @@ -11,11 +11,11 @@ odoo.define("website_forum.tour_forum", function (require) { }, [{ trigger: ".o_forum_ask_btn", position: "left", - content: _t("Ask the question in this forum by clicking on the button."), + content: _t("Create a new post in this forum by clicking on the button."), }, { trigger: "input[name=post_name]", position: "top", - content: _t("Give your question title."), + content: _t("Give your post title."), }, { trigger: ".note-editable p", extra_trigger: "input[name=post_name]:not(:propValue(\"\"))", @@ -31,7 +31,7 @@ odoo.define("website_forum.tour_forum", function (require) { actions.auto("input[id=s2id_autogen2]"); }, }, { - trigger: "button:contains(\"Post Your Question\")", + trigger: "button:contains(\"Post\")", extra_trigger: "input[id=s2id_autogen2]:not(:propValue(\"Tags\"))", content: _t("Click to post your question."), position: "bottom", @@ -39,7 +39,13 @@ odoo.define("website_forum.tour_forum", function (require) { extra_trigger: 'div.modal.modal_shown', trigger: ".modal-header button.close", auto: true, - }, { + }, + { + trigger: "a:contains(\"Answer\").collapsed", + content: _t("Click to answer."), + position: "bottom", + }, + { trigger: ".note-editable p", content: _t("Put your answer here."), position: "bottom", diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js index 5b22fa03c01..dcbca2e364a 100644 --- a/addons/website_forum/static/src/js/website_forum.js +++ b/addons/website_forum/static/src/js/website_forum.js @@ -27,6 +27,7 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({ 'click .o_wforum_favourite_toggle': '_onFavoriteQuestionClick', 'click .comment_delete': '_onDeleteCommentClick', 'click .js_close_intro': '_onCloseIntroClick', + 'submit .js_wforum_submit_form:has(:not(.karma_required).o_wforum_submit_post)': '_onSubmitForm', }, /** @@ -164,6 +165,12 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({ }); }); + this.$('#post_reply').on('shown.bs.collapse', function (e) { + $('html').animate({ + scrollTop: $('#post_reply').offset().top - ($(window).innerHeight() - $('#post_reply').innerHeight()) + }, 500); + }); + return this._super.apply(this, arguments); }, @@ -171,6 +178,59 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({ // Handlers //-------------------------------------------------------------------------- + /** + * + * @override + * @param {Event} ev + */ + _onSubmitForm: function (ev) { + let validForm = true; + + let $form = $(ev.currentTarget); + let $title = $form.find('input[name=post_name]'); + let $textarea = $form.find('textarea[name=content]'); + // It's not really in the textarea that the user write at first + let textareaContent = $form.find('.o_wysiwyg_wrapper .note-editable.panel-body').text().trim(); + + if ($title.length && $title[0].required) { + if ($title.val()) { + $title.removeClass('is-invalid'); + } else { + $title.addClass('is-invalid'); + validForm = false; + } + } + + // Because the textarea is hidden, we add the red or green border to its container + if ($textarea[0].required) { + let $textareaContainer = $form.find('.o_wysiwyg_wrapper .note-editor.panel.panel-default'); + if (!textareaContent.length) { + $textareaContainer.addClass('border border-danger rounded-top'); + validForm = false; + } else { + $textareaContainer.removeClass('border border-danger rounded-top'); + } + } + + if (validForm) { + // Stores social share data to display modal on next page. + if ($form.has('.oe_social_share_call').length) { + sessionStorage.setItem('social_share', JSON.stringify({ + targetType: $(ev.currentTarget).find('.o_wforum_submit_post').data('social-target-type'), + })); + } + } else { + ev.preventDefault(); + setTimeout(function() { + var $buttons = $(ev.currentTarget).find('button[type="submit"], a.a-submit'); + _.each($buttons, function (btn) { + let $btn = $(btn); + $btn.find('i').remove(); + $btn.prop('disabled', false); + }); + }, 0); + } + }, /** * @private * @param {Event} ev @@ -317,10 +377,20 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({ if (userVote === 1) { $voteUp.addClass('text-success'); $voteCount.addClass('text-success'); + $voteDown.removeClass('karma_required'); } if (userVote === -1) { $voteDown.addClass('text-danger'); $voteCount.addClass('text-danger'); + $voteUp.removeClass('karma_required'); + } + if (userVote === 0) { + if (!$voteDown.data('can-downvote')) { + $voteDown.addClass('karma_required'); + } + if (!$voteUp.data('can-upvote')) { + $voteUp.addClass('karma_required'); + } } $voteCount.html(data['vote_count']).addClass('o_forum_vote_animate'); } diff --git a/addons/website_forum/static/src/js/website_forum.share.js b/addons/website_forum/static/src/js/website_forum.share.js index 0545d775a81..68a2b8f3b72 100644 --- a/addons/website_forum/static/src/js/website_forum.share.js +++ b/addons/website_forum/static/src/js/website_forum.share.js @@ -76,9 +76,6 @@ var ForumShare = publicWidget.registry.socialShare.extend({ publicWidget.registry.websiteForumShare = publicWidget.Widget.extend({ selector: '.website_forum', - events: { - 'click :not(.karma_required).oe_social_share_call': '_onShareCallClick', - }, /** * @override @@ -98,20 +95,5 @@ publicWidget.registry.websiteForumShare = publicWidget.Widget.extend({ return this._super.apply(this, arguments); }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Stores social share data to display modal on next page. - * - * @private - */ - _onShareCallClick: function (ev) { - sessionStorage.setItem('social_share', JSON.stringify({ - targetType: $(ev.currentTarget).data('social-target-type'), - })); - }, }); }); diff --git a/addons/website_forum/static/tests/tours/website_forum_question.js b/addons/website_forum/static/tests/tours/website_forum_question.js index 8457d58640f..be292067db6 100644 --- a/addons/website_forum/static/tests/tours/website_forum_question.js +++ b/addons/website_forum/static/tests/tours/website_forum_question.js @@ -26,7 +26,7 @@ odoo.define('website_forum.tour_forum_question', function (require) { }, { content: "Click to post your question.", extra_trigger: "#wrap:not(:has(input[id=s2id_autogen2]:propValue('')))", - trigger: 'button:contains("Post Your Question")', + trigger: 'button:contains("Post")', }, { content: "This page contain new created question.", trigger: '#wrap:has(".fa-star")', @@ -35,7 +35,13 @@ odoo.define('website_forum.tour_forum_question', function (require) { content: "Close modal once modal animation is done.", extra_trigger: 'div.modal.modal_shown', trigger: ".modal-header button.close", - }, { + }, + { + trigger: "a:contains(\"Answer\").collapsed", + content: "Click to answer.", + position: "bottom", + }, + { content: "Put your answer here.", trigger: '.note-editable p', run: 'text First Answer', diff --git a/addons/website_forum/views/forum.xml b/addons/website_forum/views/forum.xml index c01640354c9..138c24e9e27 100644 --- a/addons/website_forum/views/forum.xml +++ b/addons/website_forum/views/forum.xml @@ -1,6 +1,21 @@ + + + Users favorite posts + forum.post + tree,form + [('forum_id', '=', active_id), ('favourite_count', '>', 0), ('state', 'in', ('active', 'close'))] + + + + Posts + forum.post + tree,form + [('forum_id', '=', active_id), ('parent_id', '=', False), ('state', 'in', ('active', 'close'))] + + @@ -32,6 +47,29 @@
    +
    + + + +
    @@ -149,10 +187,11 @@ - - - + + + + @@ -163,6 +202,13 @@ +
    + +