[IMP] website_forum: back2basics

Improve ranking display
Improve backend view
Fix share popup displayed on invalid post
Fix ranking search
Fix Upvote allow when already downvote
Allow to edit FAQ (karma out of accordion)
Add list view mode on forum index
Add last_post / Number of post on forum index
...

task-2201708

closes odoo/odoo#46634

Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
This commit is contained in:
Odoo's Mergebot
2020-05-15 13:05:58 +02:00
committed by GitHub
20 changed files with 617 additions and 242 deletions
@@ -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)
@@ -1,6 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- ACTIONS -->
<record id="action_current_rank_users" model="ir.actions.act_window">
<field name="name">Users</field>
<field name="res_model">res.users</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('rank_id', '=', active_id)]</field>
</record>
<!-- Ranks views -->
<record id="gamification_karma_ranks_action" model="ir.actions.act_window">
<field name="name">Ranks</field>
@@ -36,6 +44,7 @@
<tree string="Ranks List">
<field name="name"/>
<field name="karma_min"/>
<field name="rank_users_count"/>
</tree>
</field>
</record>
@@ -46,7 +55,16 @@
<field name="arch" type="xml">
<form string="Rank">
<sheet>
<div class="oe_button_box" name="button_box"/>
<div class="oe_button_box" name="button_box">
<button type="action" class="oe_stat_button" icon="fa-users" name="%(action_current_rank_users)d">
<div class="o_form_field o_stat_info">
<span class="o_stat_value">
<field name="rank_users_count" />
</span>
<span class="o_stat_text">Users</span>
</div>
</button>
</div>
<field name="image_1920" widget="image" class="oe_avatar" options="{'preview_image': 'image_128'}"/>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
@@ -56,13 +74,16 @@
</div>
<group>
<field name="karma_min"/>
<field name="description" placeholder="e.g. A Master Chief knows quite everything on the forum! You cannot beat him!"/>
<field name="description_motivational" placeholder="e.g. Reach this rank to gain a free mug !"/>
<field name="create_date" invisible="1"/>
</group>
<group string="Users to that rank" attrs="{'invisible': [('create_date','=',False)]}">
<field name="user_ids" readonly="1" />
</group>
<notebook>
<page string="Description" name="description">
<field name="description" placeholder="e.g. A Master Chief knows quite everything on the forum! You cannot beat him!"/>
</page>
<page string="Motivational" name="motivational">
<field name="description_motivational" placeholder="e.g. Reach this rank to gain a free mug !"/>
</page>
</notebook>
</sheet>
</form>
</field>
+1
View File
@@ -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',
+9 -1
View File
@@ -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/<model("forum.forum"):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(
+13 -13
View File
@@ -77,55 +77,55 @@
<!-- Reasons for closing Post -->
<record id="reason_1" model="forum.post.reason">
<field name="name">duplicate post</field>
<field name="name">Duplicate post</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_2" model="forum.post.reason">
<field name="name">off-topic or not relevant</field>
<field name="name">Off-topic or not relevant</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_3" model="forum.post.reason">
<field name="name">too subjective and argumentative</field>
<field name="name">Too subjective and argumentative</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_4" model="forum.post.reason">
<field name="name">not a real post</field>
<field name="name">Not a real post</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_6" model="forum.post.reason">
<field name="name">not relevant or out dated</field>
<field name="name">Not relevant or out dated</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_7" model="forum.post.reason">
<field name="name">contains offensive or malicious remarks</field>
<field name="name">Contains offensive or malicious remarks</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_8" model="forum.post.reason">
<field name="name">spam or advertising</field>
<field name="name">Spam or advertising</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_9" model="forum.post.reason">
<field name="name">too localized</field>
<field name="name">Too localized</field>
<field name="reason_type">basic</field>
</record>
<record id="reason_11" model="forum.post.reason">
<field name="name">insulting and offensive language</field>
<field name="name">Insulting and offensive language</field>
<field name="reason_type">offensive</field>
</record>
<record id="reason_12" model="forum.post.reason">
<field name="name">violent language</field>
<field name="name">Violent language</field>
<field name="reason_type">offensive</field>
</record>
<record id="reason_13" model="forum.post.reason">
<field name="name">inappropriate and unacceptable statements</field>
<field name="name">Inappropriate and unacceptable statements</field>
<field name="reason_type">offensive</field>
</record>
<record id="reason_14" model="forum.post.reason">
<field name="name">threatening language</field>
<field name="name">Threatening language</field>
<field name="reason_type">offensive</field>
</record>
<record id="reason_15" model="forum.post.reason">
<field name="name">racist and hate speech</field>
<field name="name">Racist and hate speech</field>
<field name="reason_type">offensive</field>
</record>
@@ -1,63 +0,0 @@
<div id="accordion">
<div class="card mb-2 bg-100">
<a href="#collapseOne" class="card-header" id="headingOne" data-toggle="collapse" aria-expanded="false" aria-controls="collapseOne">
<b>What kinds of questions can I ask here?</b> <i class="fa fa-caret-down float-right"/>
</a>
<div id="collapseOne" class="collapse" aria-labelledby="headingOne" data-parent="#accordion">
<div class="card-body">
<p>This community is for professional and enthusiast users, partners and programmers. You can ask questions about:</p>
<ul>
<li>how to install Odoo on a specific infrastructure,</li>
<li>how to configure or customize Odoo to specific business needs,</li>
<li>what's the best way to use Odoo for a specific business need,</li>
<li>how to develop modules for your own need,</li>
<li>specific questions about Odoo service offers, etc.</li>
</ul>
<p><b>Before you ask - please make sure to search for a similar question.</b> You can search questions by their title or tags. It’s also OK to answer your own question.</p>
<p><b>Please avoid asking questions that are too subjective and argumentative</b> or not relevant to this community.</p>
</div>
</div>
</div>
<div class="card mb-2 bg-100">
<a href="#collapseTwo" class="card-header" id="headingTwo" data-toggle="collapse" aria-expanded="false" aria-controls="collapseTwo">
<b>What should I avoid in my questions?</b> <i class="fa fa-caret-down float-right"/>
</a>
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordion">
<div class="card-body">
<p>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.</p>
<p>To prevent your question from being flagged and possibly removed, avoid asking subjective questions where …</p>
<ul>
<li>every answer is equally valid: “What’s your favorite ______?”</li>
<li>your answer is provided along with the question, and you expect more answers: “I use ______ for ______, what do you use?”</li>
<li>there is no actual problem to be solved: “I’m curious if other people feel like I do.”</li>
<li>we are being asked an open-ended, hypothetical question: “What if ______ happened?”</li>
<li>it is a rant disguised as a question: “______ sucks, am I right?”</li>
</ul>
<p>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.</p>
<p>(The above section was adapted from Stackoverflow’s FAQ.)</p>
<p>More over:</p>
<ul>
<li><b>Answers should not add or expand questions</b>. Instead either edit the question or add a question comment.</li>
<li><b>Answers should not comment other answers</b>. Instead add a comment on the other answers.</li>
<li><b>Answers shouldn't just point to other Questions</b>. 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.</li>
<li><b>Answers shouldn't just provide a link a solution</b>. 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.</li>
</ul>
</div>
</div>
</div>
<div class="card mb-2 bg-100">
<a href="#collapseThree" class="card-header" id="headingThree" data-toggle="collapse" aria-expanded="false" aria-controls="collapseThree">
<b>What should I avoid in my answers?</b> <i class="fa fa-caret-down float-right"/>
</a>
<div id="collapseThree" class="collapse" aria-labelledby="headingThree" data-parent="#accordion">
<div class="card-body">
<p><b>Answers should not add or expand questions</b>. Insteadeither edit the question or add a comment.</p>
<p><b>Answers should not comment other answers</b>. Instead add a comment on the other answers.</p>
<p><b>Answers shouldn't just point to other questions</b>.Instead add a comment indicating <i>"Possible duplicate of..."</i>. However, it's fine to include links to other questions or answers providing relevant additional information.</p>
<p> <b>Answers shouldn't just provide a link a solution</b>. 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.</p>
<p><b>Answers should not start debates</b> This community Q&amp;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.</p>
<p>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.</p>
<p>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.</p>
</div>
</div>
</div>
@@ -0,0 +1,92 @@
<odoo>
<data>
<record id="default_faq" model="ir.ui.view">
<field name="name">Faq Accordion</field>
<field name="type">qweb</field>
<field name="key">website_forum.faq_accordion</field>
<field name="arch" type="xml">
<section class="s_faq_collapse pt32 pb32">
<div class="container">
<div id="myCollapse" class="accordion" role="tablist">
<div class="card bg-white" data-name="Item">
<a href="#" role="tab" data-toggle="collapse" aria-expanded="false" class="collapsed card-header" data-target="#collapse1">
<b>What kinds of questions can I ask here?</b>
</a>
<div id="collapse1" class="collapse" role="tabpanel">
<div class="card-body">
<p>This community is for professional and enthusiast users, partners and programmers. You can ask questions about:</p>
<ul>
<li>how to install Odoo on a specific infrastructure,</li>
<li>how to configure or customize Odoo to specific business needs,</li>
<li>what's the best way to use Odoo for a specific business need,</li>
<li>how to develop modules for your own need,</li>
<li>specific questions about Odoo service offers, etc.</li>
</ul>
<p><b>Before you ask - please make sure to search for a similar question.</b> You can search questions by their title or tags. It’s also OK to answer your own question.</p>
<p><b>Please avoid asking questions that are too subjective and argumentative</b> or not relevant to this community.</p>
</div>
</div>
</div>
<div class="card bg-white" data-name="Item">
<a href="#" role="tab" data-toggle="collapse" aria-expanded="false" class="collapsed card-header" data-target="#collapse2">
<b>What should I avoid in my questions?</b>
</a>
<div id="collapse2" class="collapse" role="tabpanel">
<div class="card-body">
<p>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.</p>
<p>To prevent your question from being flagged and possibly removed, avoid asking subjective questions where …</p>
<ul>
<li>every answer is equally valid: “What’s your favorite ______?”</li>
<li>your answer is provided along with the question, and you expect more answers: “I use ______ for ______, what do you use?”</li>
<li>there is no actual problem to be solved: “I’m curious if other people feel like I do.”</li>
<li>we are being asked an open-ended, hypothetical question: “What if ______ happened?”</li>
<li>it is a rant disguised as a question: “______ sucks, am I right?”</li>
</ul>
<p>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.</p>
<p>(The above section was adapted from Stackoverflow’s FAQ.)</p>
<p>More over:</p>
<ul>
<li><b>Answers should not add or expand questions</b>. Instead either edit the question or add a question comment.</li>
<li><b>Answers should not comment other answers</b>. Instead add a comment on the other answers.</li>
<li><b>Answers shouldn't just point to other Questions</b>. 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.</li>
<li><b>Answers shouldn't just provide a link a solution</b>. 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.</li>
</ul>
</div>
</div>
</div>
<div class="card bg-white" data-name="Item">
<a href="#" role="tab" data-toggle="collapse" aria-expanded="false" class="collapsed card-header" data-target="#collapse3">
<b>What should I avoid in my answers?</b>
</a>
<div id="collapse3" class="collapse" role="tabpanel">
<div class="card-body">
<p><b>Answers should not add or expand questions</b>. Insteadeither edit the question or add a comment.</p>
<p><b>Answers should not comment other answers</b>. Instead add a comment on the other answers.</p>
<p><b>Answers shouldn't just point to other questions</b>.Instead add a comment indicating <i>"Possible duplicate of..."</i>. However, it's fine to include links to other questions or answers providing relevant additional information.</p>
<p> <b>Answers shouldn't just provide a link a solution</b>. 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.</p>
<p><b>Answers should not start debates</b> This community Q&amp;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.</p>
<p>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.</p>
<p>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.</p>
</div>
</div>
</div>
<div class="card bg-white" data-name="Item">
<a href="#" role="tab" data-toggle="collapse" aria-expanded="false" class="collapsed card-header" data-target="#collapse4">
<b>Why can other people edit my questions/answers?</b>
</a>
<div id="collapse4" class="collapse" role="tabpanel">
<div class="card-body">
<p>The goal of this site is create a relevant knowledge base that would answer questions related to Odoo.</p>
<p>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.</p>
<p>If this approach is not for you, please respect the community.</p>
<a t-attf-href="/forum/#{slug(forum)}/faq/karma">Here a table with the privileges and the karma level</a>
</div>
</div>
</div>
</div>
</div>
</section>
</field>
</record>
</data>
</odoo>
+2
View File
@@ -48,6 +48,7 @@ In simple words does Odoo became CMS+ERP platform?</p>]]></field>
<!-- Answer -->
<record id="answer_0" model="forum.post">
<field name="forum_id" ref="website_forum.forum_help"/>
<field name="name">Re: How to configure alerts for employee contract expiration</field>
<field name="content"><![CDATA[<p>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.</p>
<ul>
<li>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?</p>]]></field>
</record>
<record id="answer_1" model="forum.post">
<field name="forum_id" ref="website_forum.forum_help"/>
<field name="name">Re: CMS replacement for ERP and eCommerce</field>
<field name="content"><![CDATA[
<p>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.
+74 -36
View File
@@ -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):
@@ -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",
@@ -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');
}
@@ -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'),
}));
},
});
});
@@ -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',
+77 -9
View File
@@ -1,6 +1,21 @@
<?xml version="1.0"?>
<odoo>
<data>
<!-- FORUM ACTIONS -->
<record id="action_forum_favorites" model="ir.actions.act_window">
<field name="name">Users favorite posts</field>
<field name="res_model">forum.post</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('forum_id', '=', active_id), ('favourite_count', '>', 0), ('state', 'in', ('active', 'close'))]</field>
</record>
<record id="action_forum_posts" model="ir.actions.act_window">
<field name="name">Posts</field>
<field name="res_model">forum.post</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('forum_id', '=', active_id), ('parent_id', '=', False), ('state', 'in', ('active', 'close'))]</field>
</record>
<!-- MAIN FORUM MENU -->
<menuitem name="Forum" id="menu_website_forum"
parent="website.menu_website_configuration" sequence="50" groups="website.group_website_designer"/>
@@ -32,6 +47,29 @@
<field name="arch" type="xml">
<form string="Forum">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="%(action_forum_posts)d" type="action" class="oe_stat_button" icon="fa-comments">
<div class="o_form_field o_stat_info">
<span class="o_stat_value">
<field name="total_posts" />
</span>
<span class="o_stat_text">Posts</span>
</div>
</button>
<button name="%(action_forum_favorites)d" class="oe_stat_button" icon="fa-star" type="action">
<div class="o_form_field o_stat_info">
<span class="o_stat_value">
<field name="total_favorites" />
</span>
<span class="o_stat_text">Favorites</span>
</div>
</button>
<button type="object" class="oe_stat_button" icon="fa-globe" name="go_to_website">
<div class="o_form_field o_stat_info">
<span class="o_stat_text">Go to <br/>Website</span>
</div>
</button>
</div>
<field name="active" invisible="1"/>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<field name="image_1920" widget="image" options="{'preview_image': 'image_128'}" class="oe_avatar"/>
@@ -149,10 +187,11 @@
<field name="name"/>
<field name="active" invisible="1"/>
<field name="forum_id"/>
<field name="views"/>
<field name="child_count"/>
<field name="favourite_count"/>
<field name="views" sum="Total Views"/>
<field name="child_count" sum="Total Answers"/>
<field name="favourite_count" sum="Total Favorites"/>
<field name="website_id" groups="website.group_multi_website"/>
<field name="state"/>
</tree>
</field>
</record>
@@ -163,6 +202,13 @@
<field name="arch" type="xml">
<form string="Forum Post">
<sheet>
<div class="oe_button_box" name="button_box">
<button type="object" class="oe_stat_button" icon="fa-globe" name="go_to_website">
<div class="o_form_field o_stat_info">
<span class="o_stat_text">Go to <br/>Website</span>
</div>
</button>
</div>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<label for="name" class="oe_edit_only"/>
<h1>
@@ -225,10 +271,10 @@
<field name="create_uid"/>
<field name="forum_id"/>
<field name="tag_ids" string="Tag"/>
<filter string="Questions" name="questions" domain="[('parent_id', '=', False)]" />
<filter string="Posts" name="posts" domain="[('parent_id', '=', False)]" />
<filter string="Answers" name="answers" domain="[('parent_id', '!=', False)]" />
<filter string="Accepted Answer" name="accepted_answer" domain="[('is_correct' , '!=', False), ('parent_id', '!=', False)]" />
<filter string="Answered Questions" name="answered_questions" domain="[('child_count', '!=', 0), ('parent_id', '=', False)]" />
<filter string="Answered Posts" name="answered_posts" domain="[('child_count', '!=', 0), ('parent_id', '=', False)]" />
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group expand="0" string="Group By">
@@ -257,7 +303,7 @@
<field name="view_mode">tree,form,graph</field>
<field name="view_id" ref="view_forum_post_list"/>
<field name="search_view_id" ref="view_forum_post_search"/>
<field name="context">{'search_default_questions':1}</field>
<field name="context">{'search_default_posts':1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new forum post
@@ -272,9 +318,9 @@
<field name="name">forum.tag.list</field>
<field name="model">forum.tag</field>
<field name="arch" type="xml">
<tree string="Tags">
<tree string="Tags" editable="bottom">
<field name="name"/>
<field name="forum_id"/>
<field name="forum_id" options="{'no_create_edit': True}"/>
</tree>
</field>
</record>
@@ -287,7 +333,7 @@
<sheet>
<group>
<field name="name"/>
<field name="forum_id"/>
<field name="forum_id" options="{'no_create_edit': True}"/>
</group>
</sheet>
<div class="oe_chatter">
@@ -310,6 +356,28 @@
</record>
<menuitem id="menu_forum_tag_global" parent="menu_website_forum_global" name="Tags" action="forum_tag_action" sequence="30"/>
<!-- POST REASON VIEWS -->
<record id="forum_post_reason_view_list" model="ir.ui.view">
<field name="name">forum.post.reason.list</field>
<field name="model">forum.post.reason</field>
<field name="arch" type="xml">
<tree string="Reasons" editable="bottom">
<field name="name"/>
<field name="reason_type"/>
</tree>
</field>
</record>
<record id="forum_post_reasons_action" model="ir.actions.act_window">
<field name="name">Post Close Reasons</field>
<field name="res_model">forum.post.reason</field>
<field name="view_mode">tree</field>
</record>
<menuitem id="menu_forum_rank_global" parent="menu_website_forum_global" name="Ranks" action="gamification.gamification_karma_ranks_action" sequence="5"/>
<menuitem id="menu_forum_badges" parent="menu_website_forum_global" name="Badges" action="gamification.badge_list_action" sequence="40"/>
<menuitem id="menu_forum_post_reasons" parent="menu_website_forum_global" name="Close Reasons" action="forum_post_reasons_action" sequence="50"/>
</data>
</odoo>
+157 -68
View File
@@ -83,14 +83,22 @@
<li class="breadcrumb-item">
<a t-attf-href="/forum/#{ slug(forum) }" t-esc="forum.name"/>
</li>
<li t-if="header.get('is_guidelines')" class="breadcrumb-item">Guidelines</li>
<t t-if="header.get('is_guidelines')">
<li class="breadcrumb-item">
<a t-if="header.get('is_karma')" t-attf-href="/forum/#{ slug(forum) }/faq">Guidelines</a>
<t t-else="">
Guidelines
</t>
</li>
<li t-if="header.get('is_karma')" class="breadcrumb-item">Karma</li>
</t>
<li t-if="queue_type" class="breadcrumb-item">Moderation</li>
<li t-if="queue_type == 'validation'" class="breadcrumb-item">To Validate</li>
<li t-if="queue_type == 'flagged'" class="breadcrumb-item">Flagged</li>
<li t-if="queue_type == 'offensive'" class="breadcrumb-item">Offensive</li>
<li t-if="reasons and offensive" class="breadcrumb-item">Offensive Post</li>
<li t-if="reasons and not offensive" class="breadcrumb-item">Close post</li>
<li t-if="new_question" class="breadcrumb-item">Ask a Question</li>
<li t-if="reasons and not offensive" class="breadcrumb-item">Close Post</li>
<li t-if="new_question" class="breadcrumb-item">New Post</li>
<t t-if="is_edit">
<t t-set="target" t-value="post.parent_id if is_answer else post"/>
<li class="breadcrumb-item text-truncate" style="max-width:150px">
@@ -149,7 +157,8 @@
<a t-attf-href="/forum/#{ slug(forum) }/tag" t-attf-class="nav-link #{searches.get('tags') and 'active'}">Tags</a>
</li>
<li class="nav-item">
<a t-attf-href="/profile/ranks_badges?badge_category=forum" t-attf-class="nav-link #{searches.get('badges') and 'active'}">Badges</a>
<a t-attf-href="/profile/ranks_badges?badge_category=forum&amp;url_origin=#{request.httprequest.path}&amp;name_origin=#{forum.name}"
t-attf-class="nav-link #{searches.get('badges') and 'active'}">Badges</a>
</li>
<li class="nav-item">
<a t-attf-href="/forum/#{ slug(forum) }/faq" t-attf-class="nav-link #{header.get('is_guidelines') and 'active'}">About</a>
@@ -166,7 +175,7 @@
<t t-elif="searches.get('badges')">Badges</t>
<t t-elif="header.get('is_guidelines')">About</t>
<t t-elif="uid and my == 'favourites'">Favourites</t>
<t t-elif="uid and my == 'mine'">My questions</t>
<t t-elif="uid and my == 'mine'">My Posts</t>
<t t-elif="uid and my == 'followed'">Following</t>
<t t-elif="question">Question</t>
<t t-else="">All Topics</t>
@@ -175,13 +184,13 @@
<a t-if="searches or my or question" t-attf-href="/forum/#{ slug(forum) }" class="dropdown-item">All Topics</a>
<a t-if="not searches.get('users')" t-attf-href="/profile/users?forum_origin=#{request.httprequest.path}" class="dropdown-item">People</a>
<a t-if="not searches.get('tags')" t-attf-href="/forum/#{slug(forum)}/tag" class="dropdown-item">Tags</a>
<a t-if="not searches.get('badges')" t-attf-href="/profile/ranks_badges?badge_category=forum" class="dropdown-item">Badges</a>
<a t-if="not searches.get('badges')" t-attf-href="/profile/ranks_badges?badge_category=forum&amp;url_origin=#{request.httprequest.path}&amp;name_origin=#{forum.name}" class="dropdown-item">Badges</a>
<a t-if="not header.get('is_guidelines')" t-attf-href="/forum/#{ slug(forum) }/faq" class="dropdown-item">About</a>
<t t-if="uid">
<div class="dropdown-divider"/>
<a t-att-href="'/forum/%s/user/%s?forum_origin=%s' % (slug(forum), uid, request.httprequest.path)"
class="dropdown-item">My profile</a>
<a t-if="my != 'mine'" t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', 'filters', my='mine')" class="dropdown-item">My Questions</a>
<a t-if="my != 'mine'" t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', 'filters', my='mine')" class="dropdown-item">My Posts</a>
<a t-if="my != 'favourites'" t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', 'filters', my='favourites')" class="dropdown-item">My Favourites</a>
<a t-if="my != 'followed'" t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', 'filters', my='followed')" class="dropdown-item">I'm Following</a>
<a t-if="my != 'tagged'" t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', 'filters', my='tagged')" class="dropdown-item">Tags I Follow</a>
@@ -362,15 +371,21 @@
<t t-call="website_forum.header">
<div t-field="forum.description" class="mb-4"/>
<div t-field="forum.faq"/>
<div class="card mb-2">
<a href="#collapseFour" class="card-header" id="headingFour" data-toggle="collapse" aria-expanded="false" aria-controls="collapseFour">
<b>Why can other people edit my questions/answers?</b> <i class="fa fa-caret-down float-right"/>
</a>
<div id="collapseFour" class="collapse" aria-labelledby="headingFour" data-parent="#accordion">
<div class="card-body">
</t>
</template>
<!-- FAQ Karma Layout -->
<template id="faq_karma" name="Karma">
<t t-call="website_forum.header">
<div class="card bg-white" data-name="Item">
<div role="tab" class="card-header">
<b>Why can other people edit my questions/answers?</b>
</div>
<div role="tabpanel">
<div class="card-body">
<p>The goal of this site is create a relevant knowledge base that would answer questions related to Odoo.</p>
<p>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.</p>
<p>If this approach is not for you, please respect the community and use Google+ communities instead.</p>
<p>If this approach is not for you, please respect the community.</p>
<table class="table table-striped mt-4 bg-white">
<tbody>
<tr>
@@ -417,6 +432,13 @@
<!-- All Forums Layout -->
<template id="forum_all" name="Forum Navigation">
<t t-set="col_class" t-valuef="mb-3 col-sm-6"/>
<t t-set="img_class" t-valuef="col-md-4"/>
<t t-set="content_class" t-valuef="col-md-8"/>
<t t-set="last_post_class" t-valuef="col-md-12 pr-md-3 pt-3"/>
<t t-set="nb_post_class" t-valuef="col-md-12 pr-md-3 pt-2"/>
<t t-call="website.layout">
<t t-set="pageName" t-value="'website_forum'"/>
<div id="wrap">
@@ -444,27 +466,70 @@
<!-- First, list all forums (those with descriptions first) -->
<t t-foreach="sorted_forums" t-as="forum">
<t t-set="has_desc" t-value="forum.description"/>
<div t-attf-class="mb-3 col-sm-6 #{not has_desc and 'col-md-3'}">
<div t-attf-class="#{has_desc and 'row' or 'd-flex flex-column h-100'} #{no_description_exist and has_desc and 'py-4 bg-200'}">
<div t-attf-class="#{has_desc and 'col-md-4 pr-md-0' or 'flex-lg-grow-1'}">
<div t-attf-class="#{col_class}">
<div class="row py-4 bg-200 mx-1 h-100 o_forum_row">
<div t-attf-class="o_forum_image_container pr-md-0 h-100 #{img_class}">
<a t-attf-href="/forum/#{slug(forum)}">
<div t-attf-class="oe_img_bg rounded shadow-sm py-4 px-5 o_wforum_forum_card_bg flex-shrink-0 h-100 o_not_editable {not has_desc and 'h-100'}"
t-attf-style="#{forum.image_256 and ('background-image: url(%s);' % website.image_url(forum, 'image_256'))} background-position: center;"/>
<div t-attf-class="h-100 w-100 #{not forum.image_1920 and 'rounded o_wforum_forum_card_bg shadow-sm flex-shrink-0'}">
<div t-if="forum.image_1920 or editable" t-attf-class="h-100"
t-field="forum.image_1920" t-options="{'widget': 'image', 'preview_image': 'image_256', 'class': 'w-100 h-100 o_object_fit_cover rounded'}" />
</div>
</a>
</div>
<div t-attf-class="mt-2 #{has_desc and 'col-md-8 mt-md-0'}">
<div t-attf-class="#{content_class} mt-2 mt-md-0 d-flex flex-column h-100">
<a t-attf-href="/forum/#{slug(forum)}" class="text-reset" t-att-title="forum.name">
<h3 t-attf-class="#{has_desc and 'h4' or 'h5 m-0'}" t-field="forum.name"/>
<h3 class="h4" t-field="forum.name"/>
</a>
<p t-attf-class="m-0 #{has_desc and not no_description_exist and 'text-muted' or not has_desc and 'css_non_editable_mode_hidden'}"
<p class="m-0 flex-grow-1"
placeholder="Description"
t-field="forum.description"/>
t-field="forum.teaser"/>
<t t-if="is_view_active('website_forum.opt_post_count') or is_view_active('website_forum.opt_last_post')" t-call="website_forum.forum_post_options"/>
</div>
</div>
</div>
</t>
</template>
<template id="forum_post_options">
<div class="row">
<div t-attf-class="#{last_post_class}">
<div t-if="is_view_active('website_forum.opt_last_post') and forum.post_ids" class="text-truncate">Last Post: <a t-attf-href="/forum/#{slug(forum)}/question/#{slug(forum.last_post_id)}"><t t-esc="forum.last_post_id.name"/></a></div>
</div>
<div t-attf-class="#{nb_post_class}">
<div t-if="is_view_active('website_forum.opt_post_count')">Posts: <strong><t t-esc="forum.total_posts"/></strong></div>
</div>
</div>
</template>
<!-- (Options) Forum : List View
Display forums as a list -->
<template name="List View" id="website_forum.opt_list_view" inherit_id="website_forum.forum_all" active="False" customize_show="True">
<xpath expr="//t[@t-set='col_class']" position="attributes">
<attribute name="t-valuef">mb-3 col-sm-12</attribute>
</xpath>
<xpath expr="//t[@t-set='img_class']" position="attributes">
<attribute name="t-valuef">col-md-3</attribute>
</xpath>
<xpath expr="//t[@t-set='content_class']" position="attributes">
<attribute name="t-valuef">col-md-9</attribute>
</xpath>
<xpath expr="//t[@t-set='last_post_class']" position="attributes">
<attribute name="t-valuef">col-md-10 pr-md-0 pt-3</attribute>
</xpath>
<xpath expr="//t[@t-set='nb_post_class']" position="attributes">
<attribute name="t-valuef">col-md-2 pr-md-3 pt-3</attribute>
</xpath>
</template>
<!-- (Options) Forum : Show Post Count
Show the number of post a forum has -->
<template name="Show Post Count" id="website_forum.opt_post_count" inherit_id="website_forum.forum_all" active="False" customize_show="True"/>
<!-- (Options) Forum : Show Last Post
Show the title of the latest post in each forum -->
<template name="Show Last Post" id="website_forum.opt_last_post" inherit_id="website_forum.forum_all" active="False" customize_show="True"/>
<!-- Default content for the "All Forums Layout" header above -->
<!-- (simulate an oe_structure edition) -->
<template id="forum_all_oe_structure_forum_all_top" inherit_id="website_forum.forum_all" name="Forum Navigation (oe_structure_forum_all_top)">
@@ -503,13 +568,13 @@
<t t-set="location" t-value="url_for('/forum/') + slug(forum) + ( ('/tag/' + slug(tag) + '/questions?') if tag else '?' )"/>
<!-- My Questions -->
<!-- My Posts -->
<span t-if="my == 'mine'" class="nav-link rounded-pill mb-2 active font-weight-bold">
<i class="fa fa-question-circle-o fa-fw"/> My Questions
<i class="fa fa-question-circle-o fa-fw"/> My Posts
<a class="text-reset pull-right no-decoration" t-att-href="location + keep_query('search', 'filters', 'sorting')">&#215;</a>
</span>
<a t-else="" class="nav-link rounded-pill mb-2 text-reset" t-att-href="location + keep_query('search', 'filters', 'sorting', my='mine')">
<i class="fa fa-question-circle-o fa-fw"/> My Questions
<i class="fa fa-question-circle-o fa-fw"/> My Posts
</a>
<!-- My Favourites -->
@@ -581,7 +646,7 @@
<img t-if="uid" class="o_forum_avatar rounded-circle mr-1" t-att-src="website.image_url(user, 'image_128', '16x16')" alt="Avatar"/>
<span t-if="my == 'favourites'"> My <b>Favourites</b></span>
<span t-elif="my == 'followed'"> I'm <b>Following</b></span>
<span t-elif="my == 'mine'"> My <b>Questions</b></span>
<span t-elif="my == 'mine'"> My <b>Posts</b></span>
<span t-elif="my == 'tagged'"> <b>Tags</b> I Follow</span>
</div>
<a t-att-href="url_for('') + '?' + keep_query( 'search', 'filters', 'sorting')" class="btn py-1">&#215;</a>
@@ -597,9 +662,9 @@
<div t-if="uid and request.env.user.forum_waiting_posts_count"
title="You already have a pending post"
data-toggle="popover" data-trigger="hover" data-content="Please wait for a moderator to validate your previous post before continuing.">
<a class="disabled btn btn-secondary btn-block mb-3 mb-md-0" t-attf-href="/forum/#{slug(forum)}/ask">Ask a Question</a>
<a class="disabled btn btn-secondary btn-block mb-3 mb-md-0" t-attf-href="/forum/#{slug(forum)}/ask">New Post</a>
</div>
<a t-else="" role="button" type="button" class="btn btn-primary btn-block o_forum_ask_btn mb-3 mb-md-0" t-att-href="uid and '/forum/' + slug(forum) + '/ask' or '/web/login'">Ask a Question</a>
<a t-else="" role="button" type="button" class="btn btn-primary btn-block o_forum_ask_btn mb-3 mb-md-0" t-att-href="uid and '/forum/' + slug(forum) + '/ask' or '/web/login'">New Post</a>
</div>
<t t-set="no_filters" t-value="not filters in ('solved', 'unsolved', 'unanswered')"/>
@@ -620,12 +685,14 @@
</a>
<div class="dropdown-divider"/>
<a t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', filters='solved')"
class="dropdown-item">Solved
</a>
<a t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', filters='unsolved')"
class="dropdown-item">Unsolved
</a>
<t t-if="forum.mode == 'questions'">
<a t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', filters='solved')"
class="dropdown-item">Solved
</a>
<a t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', filters='unsolved')"
class="dropdown-item">Unsolved
</a>
</t>
<a t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', filters='unanswered')"
class="dropdown-item">Unanswered
</a>
@@ -640,13 +707,15 @@
t-attf-class="nav-link py-1 rounded-pill #{no_filters and 'active font-weight-bold' or 'pl-0 pr-2'}">
All
</a>
<span class="mx-1 text-400 d-none d-lg-block">|</span>
<a t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', 'my', filters='solved')"
t-attf-class="nav-link py-1 rounded-pill #{filters == 'solved' and 'active font-weight-bold' or 'px-2'}">Solved
</a>
<a t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', 'my', filters='unsolved')"
t-attf-class="d-none d-lg-block nav-link py-1 rounded-pill #{filters == 'unsolved' and 'active font-weight-bold' or 'px-2'}">Unsolved
</a>
<t t-if="forum.mode == 'questions'">
<span class="mx-1 text-400 d-none d-lg-block">|</span>
<a t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', 'my', filters='solved')"
t-attf-class="nav-link py-1 rounded-pill #{filters == 'solved' and 'active font-weight-bold' or 'px-2'}">Solved
</a>
<a t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', 'my', filters='unsolved')"
t-attf-class="d-none d-lg-block nav-link py-1 rounded-pill #{filters == 'unsolved' and 'active font-weight-bold' or 'px-2'}">Unsolved
</a>
</t>
<span class="mx-1 text-400 d-none d-lg-block">|</span>
<a t-att-href="url_for('') + '?' + keep_query( 'search', 'sorting', 'my', filters='unanswered')"
t-attf-class="nav-link py-1 rounded-pill #{filters == 'unanswered' and 'active font-weight-bold' or 'px-2'}">Unanswered
@@ -689,7 +758,7 @@
</t>
</div>
<!-- No questions or search/filters result -->
<!-- No posts or search/filters result -->
<div t-if="question_count == 0" class="col">
<div t-if="search or tag or (not no_filters)" class="alert alert-info">
<t t-set="_filters_str">
@@ -700,7 +769,7 @@
<t t-set="_my_str">
<t t-if="my == 'favourites'">in your favourites</t>
<t t-if="my == 'followed'">in your followed list</t>
<t t-if="my == 'mine'">in your questions</t>
<t t-if="my == 'mine'">in your posts</t>
</t>
<t t-set="_search_str"><t t-if="search">matching "<em class="font-weight-bold" t-esc="search"/>"</t></t>
<t t-set="_search_and_tag_str"><t t-if="search and tag">&amp;nbsp;and&amp;nbsp;</t></t>
@@ -721,7 +790,7 @@
No favourite questions in this forum (yet).<br/>
<a t-attf-href="/forum/#{slug(forum)}/">Browse All</a>
</div>
<div t-elif="my == 'mine'" class="alert border">You have no questions in this forum (yet).</div>
<div t-elif="my == 'mine'" class="alert border">You have no posts in this forum (yet).</div>
</t>
<div t-elif="filters == 'unanswered'" class="alert alert-info">Amazing! There are no unanswered questions left!</div>
@@ -765,12 +834,12 @@
<a t-attf-href="/forum/#{ slug(forum) }" title="All Topics"><i class="fa fa-chevron-left mr-2"/>Back to All Topics</a>
</div>
<form t-else="" t-attf-action="/forum/#{slug(forum)}/new" method="post" role="form" class="tag_text js_website_submit_form o_wforum_readable">
<form t-else="" t-attf-action="/forum/#{slug(forum)}/new" method="post" role="form" class="tag_text js_website_submit_form js_wforum_submit_form o_wforum_readable">
<div class="form-group">
<label for="content">Title</label>
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="text" name="post_name" required="required" pattern=".*\S.*" t-attf-value="#{post_name}"
class="form-control form-control-lg" placeholder="A clear, explicit and concise question title" title="Title must not be empty"/>
class="form-control form-control-lg" placeholder="A clear, explicit and concise title" title="Title must not be empty"/>
<input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
<div class="form-text small text-muted">
<a data-toggle="collapse" href="#newQuestionExample" role="button" aria-expanded="false" aria-controls="newQuestionExample">
@@ -782,7 +851,7 @@
<i class="fa fa-check"/> How to configure TPS and TVQ's canadian taxes?
</div>
<div class="text-danger">
<i class="fa fa-times"/> Good morning to all! Please, can someone help solve solve my tax computation problem in Canada? Thanks!
<i class="fa fa-times"/> Good morning to all! Please, can someone help solve my tax computation problem in Canada? Thanks!
</div>
</div>
</div>
@@ -801,9 +870,9 @@
<input type="hidden" name="post_tags" placeholder="Tags" class="form-control js_select2"/>
</div>
<div class="mb-5">
<button type="submit" t-attf-class="btn btn-primary #{forum.allow_share and 'oe_social_share_call'} #{(user.karma &lt; forum.karma_ask) and 'karma_required'}"
t-att-data-karma="forum.karma_ask"
data-hashtags="#question" data-social-target-type="question">Post Your Question</button>
<button type="submit" t-attf-class="btn btn-primary o_wforum_submit_post #{forum.allow_share and 'oe_social_share_call'} #{(user.karma &lt; forum.karma_ask) and 'karma_required'}"
t-att-data-karma="forum.karma_ask"
data-hashtags="#question" data-social-target-type="question">Post Your Question</button>
<a class="btn btn-secondary" title="Back to Question" t-attf-href="/forum/#{ slug(forum) }"> Discard</a>
</div>
</form>
@@ -818,7 +887,7 @@
<h5 class="mb-1 text-muted" t-esc="post.parent_id.name"/>
<div t-field="post.parent_id.content" class="o_wforum_post_content text-muted oe_no_empty"/>
</article>
<form t-attf-action="/forum/#{slug(forum)}/post/#{slug(post)}/save" method="post" role="form" class="tag_text js_website_submit_form o_wforum_readable">
<form t-attf-action="/forum/#{slug(forum)}/post/#{slug(post)}/save" method="post" role="form" class="tag_text js_website_submit_form js_wforum_submit_form o_wforum_readable">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div t-if="not is_answer" class="form-group">
<label for="post_name">Title</label>
@@ -830,12 +899,12 @@
<i class="fa fa-question-circle"/>
</a>
<div class="collapse" id="newQuestionExample">
<div class="my-2">Use a clear, explicit and concise question title</div>
<div class="my-2">Use a clear, explicit and concise title</div>
<div class="text-success">
<i class="fa fa-check"/> How to configure TPS and TVQ's canadian taxes?
</div>
<div class="text-danger">
<i class="fa fa-times"/> Good morning to all! Please, can someone help solve solve my tax computation problem in Canada? Thanks!
<i class="fa fa-times"/> Good morning to all! Please, can someone help solve my tax computation problem in Canada? Thanks!
</div>
</div>
</div>
@@ -860,7 +929,7 @@
t-att-title="edit_tags_karma_fail and edit_tags_karma_error_message"/>
</div>
<div class="mb-4">
<button type="submit" class="btn btn-primary">Save changes</button>
<button type="submit" class="btn btn-primary o_wforum_submit_post">Save Changes</button>
<a class="btn btn-secondary" title="Back to Question" t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(post)}">
Discard
</a>
@@ -912,13 +981,13 @@
<!-- Edition: post a reply -->
<template id="post_reply">
<div class="css_editable_mode_hidden">
<form class="collapse js_website_submit_form"
<form class="collapse js_website_submit_form js_wforum_submit_form"
t-attf-action="/forum/#{slug(forum)}/#{slug(object)}/reply" method="post" role="form">
<h3 class="mt8">Your Reply</h3>
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
<textarea name="content" class="form-control o_wysiwyg_loader" required="required" t-att-data-karma="forum.karma_editor"/>
<button type="submit" class="btn btn-primary mb16">Post Answer</button>
<textarea name="content" class="form-control o_wysiwyg_loader" required="required" minlength="50" t-att-data-karma="forum.karma_editor"/>
<button type="submit" class="btn btn-primary mb16 o_wforum_submit_post">Post Answer</button>
</form>
</div>
</template>
@@ -932,19 +1001,24 @@
<t t-if="request.params.get('nocontent')">
<p class="alert alert-danger" role="alert">You cannot post an empty answer</p>
</t>
<form t-attf-action="/forum/#{ slug(forum) }/#{slug(question)}/reply" method="post" class="js_website_submit_form" role="form">
<form t-attf-action="/forum/#{ slug(forum) }/#{slug(question)}/reply" method="post" class="js_website_submit_form js_wforum_submit_form" role="form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
<textarea name="content" t-attf-id="content-#{str(question.id)}" class="form-control o_wysiwyg_loader" required="required" t-att-data-karma="forum.karma_editor"/>
<textarea name="content" t-attf-id="content-#{str(question.id)}" class="form-control o_wysiwyg_loader" required="required" minlength="50" t-att-data-karma="forum.karma_editor"/>
<p class="small mt-2 mb-1">
<b>Please try to give a substantial answer.</b> If you wanted to comment on the question or answer, just
<b>use the commenting tool.</b> Please remember that you can always <b>revise your answers</b>
- no need to answer the same question twice. Also, please <b>don't forget to vote</b>
- it really helps to select the best questions and answers!
</p>
<button type="submit" t-attf-class="btn btn-primary #{forum.allow_share and 'oe_social_share_call'} my-3 #{not question.can_answer and 'karma_required'}"
<button type="submit" t-attf-class="btn btn-primary o_wforum_submit_post #{forum.allow_share and 'oe_social_share_call'} my-3 #{not question.can_answer and 'karma_required'}"
t-att-data-karma="question.forum_id.karma_answer"
data-social-target-type="answer" data-hashtags="#answer">Post Answer</button>
<a href="#"
class="btn btn-secondary"
data-toggle="collapse"
data-target=".answer_collapse"
aria-expanded="false">Discard</a>
</form>
</template>
@@ -958,6 +1032,7 @@
t-attf-class="btn btn-link vote_up fa fa-caret-up pl-0 #{vertical and 'px-md-0 pt-md-0' or 'pr-2'} #{own_vote == 1 and 'text-success' or 'text-muted'} #{not can_upvote and 'karma_required'}"
t-att-disabled="own_vote == 1 and 'disabled'"
t-att-data-karma="forum.karma_upvote"
t-att-data-can-upvote="can_upvote"
aria-label="Positive vote" title="Positive vote"/>
<b t-attf-class="vote_count #{own_vote == 1 and 'text-success' or (own_vote == -1 and 'text-danger' or 'text-muted')}"
t-esc="post.vote_count"/>
@@ -965,6 +1040,7 @@
t-attf-class="btn btn-link vote_down fa fa-caret-down #{vertical and 'px-md-0' or 'px-2'} #{own_vote == -1 and 'text-danger' or 'text-muted'} #{not can_downvote and 'karma_required'}"
t-att-disabled="own_vote == -1 and 'disabled'"
t-att-data-karma="forum.karma_downvote"
t-att-data-can-downvote="can_downvote"
aria-label="Negative vote" title="Negative vote"/>
<t t-raw="0"/>
</div>
@@ -1074,15 +1150,17 @@
</div>
</div>
<div t-field="question.content" class="o_wforum_post_content o_wforum_readable oe_no_empty"/>
<div t-field="question.content" class="o_wforum_post_content o_wforum_readable oe_no_empty o_not_editable"/>
<t t-set="_question_comment_collapse_uid" t-value="'comment_%s_%s' % (question._name.replace('.', '_'), question.id)"/>
<div t-if="question.state == 'active' and question.active != False"
class="btn-toolbar mb-3" role="toolbar">
<div t-if="not question.uid_has_answered and question.can_answer" class="btn-group btn-group-sm mr-2">
<a t-attf-class="btn btn-primary #{not question.can_answer and 'karma_required text-muted'}"
<a t-attf-class="btn btn-primary collapsed #{not question.can_answer and 'karma_required text-muted'}"
t-att-data-karma="question.forum_id.karma_answer"
href="#post_reply">
data-toggle="collapse"
data-target=".answer_collapse"
href="#">
<i class="fa fa-reply mr-1"/>Answer
</a>
</div>
@@ -1119,8 +1197,19 @@
</div>
</t>
</section>
<div t-if="not question.uid_has_answered and question.can_answer and question.child_ids" class="btn-group btn-group-sm mr-2">
<a t-attf-class="btn btn-primary answer_collapse show collapsed collapse #{not question.can_answer and 'karma_required text-muted'}"
t-att-data-karma="question.forum_id.karma_answer"
data-toggle="collapse"
data-target=".answer_collapse"
href="#">
<i class="fa fa-reply mr-1"/>Answer
</a>
</div>
<t t-if="question.state != 'close' and question.active != False and question.can_answer and not request.env.user.forum_waiting_posts_count">
<div id="post_reply" t-if="(not question.uid_has_answered or question.forum_id.mode == 'discussions')">
<div id="post_reply" class="collapse answer_collapse" t-if="(not question.uid_has_answered or question.forum_id.mode == 'discussions')">
<t t-call="website_forum.post_answer"/>
</div>
<div t-elif="uid and request.env.user.forum_waiting_posts_count" class="alert alert-gamma text-center">
@@ -1237,7 +1326,7 @@
</span>
</div>
<div t-field="answer.content" class="mb-2 o_wforum_readable oe_no_empty"/>
<div t-field="answer.content" class="mb-2 o_wforum_readable oe_no_empty o_not_editable"/>
<t t-set="_answer_comment_collapse_uid" t-value="'comment_%s_%s' % (answer._name.replace('.', '_'), answer.id)"/>
<div class="btn-toolbar mb-3">
@@ -1371,7 +1460,7 @@
</div>
</div>
<div class="css_editable_mode_hidden o_wforum_readable">
<form t-att-id="_collapse_uid" class="collapse p-1 oe_comment_grey js_website_submit_form"
<form t-att-id="_collapse_uid" class="collapse p-1 oe_comment_grey js_website_submit_form js_wforum_submit_form"
t-attf-action="/forum/#{slug(forum)}/post/#{slug(object)}/comment" method="POST">
<div class="shadow bg-white rounded px-3 pt-3 pb-4 mb-4">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
@@ -1381,7 +1470,7 @@
<div class="w-100">
<textarea name="comment" class="form-control mb-2" placeholder="Comment this post..."/>
<div>
<button type="submit" class="btn btn-primary">Post comment</button>
<button type="submit" class="btn btn-primary o_wforum_submit_post">Post Comment</button>
<a t-attf-href="##{_collapse_uid}" data-toggle="collapse" class="btn border">Discard</a>
</div>
</div>
+25 -8
View File
@@ -233,9 +233,11 @@ class WebsiteProfile(http.Controller):
'group_by': group_by or 'all',
}
if search_term:
dom = expression.AND([['|', ('name', 'ilike', search_term), ('company_id.name', 'ilike', search_term)], dom])
dom = expression.AND([['|', ('name', 'ilike', search_term), ('partner_id.commercial_company_name', 'ilike', search_term)], dom])
user_count = User.sudo().search_count(dom)
my_user = request.env.user
current_user_values = False
if user_count:
page_count = math.ceil(user_count / self._users_per_page)
pager = request.website.pager(url="/profile/users", total=user_count, page=page, step=self._users_per_page,
@@ -246,11 +248,7 @@ class WebsiteProfile(http.Controller):
# Get karma position for users (only website_published)
position_domain = [('karma', '>', 1), ('website_published', '=', True)]
if group_by:
position_map = self._get_user_tracking_karma_gain_position(position_domain, users.ids, group_by)
else:
position_results = request.env['res.users'].browse(users.ids)._get_karma_position(position_domain)
position_map = dict((user_data['user_id'], dict(user_data)) for user_data in position_results)
position_map = self._get_position_map(position_domain, users, group_by)
max_position = max([user_data['karma_position'] for user_data in position_map.values()], default=1)
for user in user_values:
@@ -259,16 +257,35 @@ class WebsiteProfile(http.Controller):
user['karma_gain'] = user_data.get('karma_gain_total', 0)
user_values.sort(key=itemgetter('position'))
if my_user.website_published and my_user.karma and my_user.id not in users.ids:
# Need to keep the dom to search only for users that appear in the ranking page
current_user = User.sudo().search(expression.AND([[('id', '=', my_user.id)], dom]))
if current_user:
current_user_values = self._prepare_all_users_values(current_user)[0]
user_data = self._get_position_map(position_domain, current_user, group_by).get(current_user.id, {})
current_user_values['position'] = user_data.get('karma_position', 0)
current_user_values['karma_gain'] = user_data.get('karma_gain_total', 0)
else:
user_values = []
pager = {'page_count': 0}
render_values.update({
'top3_users': user_values[:3] if not search_term and page == 1 else [],
'users': user_values,
'pager': pager})
'my_user': current_user_values,
'pager': pager,
})
return request.render("website_profile.users_page_main", render_values)
def _get_position_map(self, position_domain, users, group_by):
if group_by:
position_map = self._get_user_tracking_karma_gain_position(position_domain, users.ids, group_by)
else:
position_results = users._get_karma_position(position_domain)
position_map = dict((user_data['user_id'], dict(user_data)) for user_data in position_results)
return position_map
def _get_user_tracking_karma_gain_position(self, domain, user_ids, group_by):
""" Helper method computing boundaries to give to _get_tracking_karma_gain_position.
See that method for more details. """
@@ -249,3 +249,8 @@ $owprofile-color-bg: mix($body-bg, #efeff4);
.o_wprofile_pointer {
cursor: pointer;
}
// Own profile border
.o_wprofile_border_focus {
border-left: 4px solid theme-color('secondary');
}
@@ -252,7 +252,7 @@
<div t-if="user.rank_id" class="d-flex align-items-center">
<small class="font-weight-bold mr-2">Current rank:</small>
<img t-att-src="website.image_url(user.rank_id, 'image_128')" width="16" height="16" alt="" class="o_object_fit_cover mr-1"/>
<a href="/profile/ranks_badges" t-field="user.rank_id"/>
<a t-attf-href="/profile/ranks_badges?url_origin=#{request.httprequest.path}&amp;name_origin=#{user.name}" t-field="user.rank_id"/>
</div>
<button class="btn btn-sm d-md-none bg-white border" type="button" data-toggle="collapse" data-target="#o_wprofile_sidebar_collapse" aria-expanded="false" aria-controls="o_wprofile_sidebar_collapse">More info</button>
</div>
@@ -391,8 +391,10 @@
<p class="text-muted">No badges yet!</p>
</div>
<div class="text-right d-inline-block pull-right">
<a t-if="not user.badge_ids and badge_category" t-attf-href="/profile/ranks_badges?badge_category=#{badge_category}" class="btn btn-link btn-sm"><i class="fa fa-arrow-right"/> All Badges</a>
<a t-else="" href="/profile/ranks_badges" class="btn btn-link btn-sm"><i class="fa fa-arrow-right mr-1"/>All Badges</a>
<a t-if="not user.badge_ids and badge_category" t-attf-href="/profile/ranks_badges?badge_category=#{badge_category}&amp;url_origin=#{request.httprequest.path}&amp;name_origin=#{user.name}"
class="btn btn-link btn-sm"><i class="fa fa-arrow-right"/> All Badges</a>
<a t-else="" t-attf-href="/profile/ranks_badges?url_origin=#{request.httprequest.path}&amp;name_origin=#{user.name}"
class="btn btn-link btn-sm"><i class="fa fa-arrow-right mr-1"/>All Badges</a>
</div>
</template>
@@ -400,6 +402,14 @@
<template id="rank_badge_main" name="Ranks Page">
<t t-call="website.layout">
<div class="container mb32 mt48">
<nav t-if="request.params.get('url_origin') and request.params.get('name_origin')" aria-label="breadcrumb">
<ol class="breadcrumb p-0 bg-white">
<li class="breadcrumb-item">
<a t-att-href="request.website._get_http_domain() + request.params.get('url_origin')" t-esc="request.params.get('name_origin')"/>
</li>
<li class="breadcrumb-item">Badges</li>
</ol>
</nav>
<div class="row justify-content-between" t-if="ranks">
<div class="col-12 col-md-6 col-lg-5">
<h1>Ranks</h1>
@@ -523,9 +533,22 @@
</div>
</div>
<table class="table table-sm" t-if='users'>
<tr t-foreach="users" t-as="user" t-attf-onclick="location.href='/profile/user/#{user['id']}';" class="o_wprofile_pointer bg-white">
<t t-call="website_profile.all_user_card"/>
</tr>
<t t-foreach="users" t-as="user">
<tr t-attf-onclick="location.href='/profile/user/#{user['id']}';" t-attf-class="o_wprofile_pointer bg-white #{user['id'] == user_id.id and 'o_wprofile_border_focus'}">
<t t-call="website_profile.all_user_card"/>
</tr>
</t>
<t t-if="my_user">
<!-- keep same table to avoid missaligment -->
<tr>
<td colspan="7"></td>
</tr>
<t t-set='user' t-value='my_user'/>
<tr t-attf-onclick="location.href='/profile/user/#{user['id']}';" t-attf-class="o_wprofile_pointer bg-white o_wprofile_border_focus">
<t t-call="website_profile.all_user_card">
</t>
</tr>
</t>
</table>
<t t-if='search and not users'>
<div class='alert alert-warning'>No user found for <strong><t t-esc="search"/></strong>. Try another search.</div>
@@ -581,8 +604,8 @@
</span>
</t>
</td>
<td t-if="not user['website_published']" class="align-middle font-weight-bold text-right text-nowrap">
<span class="badge badge-danger font-weight-normal px-2 py-1 m-1">Unpublished</span>
<td class="align-middle font-weight-bold text-right text-nowrap">
<span t-if="not user['website_published']" class="badge badge-danger font-weight-normal px-2 py-1 m-1">Unpublished</span>
</td>
<td class="align-middle font-weight-bold text-right text-nowrap">
<b t-esc="user['karma']"/> <span class="text-muted small font-weight-bold">XP</span>
@@ -40,9 +40,9 @@
<div class="row align-items-start">
<div class="col-12 col-sm-4">
<a t-attf-href="/forum/#{slug(forum)}">
<div class="oe_img_bg rounded shadow-sm py-4 px-5 o_wforum_forum_card_bg flex-shrink-0"
t-attf-style="#{forum.image_256 and ('background-image: url(%s);' % website.image_url(forum, 'image_256'))} background-position: center;">
<div class="p-5"/>
<div t-attf-class="h-100 w-100 o_editable #{not forum.image_1920 and 'rounded o_wforum_forum_card_bg shadow-sm flex-shrink-0'}">
<div t-if="forum.image_1920 or editable" t-attf-class="h-100"
t-field="forum.image_1920" t-options="{'widget': 'image', 'preview_image': 'image_256', 'class': 'w-100 h-100 o_object_fit_cover rounded'}" />
</div>
</a>
</div>
@@ -52,7 +52,7 @@
</a>
<p t-attf-class="m-0 lead #{not forum.description and 'css_non_editable_mode_hidden'}"
placeholder="Description"
t-field="forum.description"/>
t-field="forum.teaser"/>
</div>
</div>
</div>
@@ -20,7 +20,7 @@
<template id="slide_fullscreen" inherit_id="website_slides.slide_fullscreen">
<xpath expr="//a[hasclass('o_wslides_fs_review')]" position="after">
<a t-if="slide.channel_id.forum_id" id="fullscreen_forum_button" class="d-flex align-items-center px-3" t-attf-href="/forum/#{slug(slide.channel_id.forum_id)}" target="new">
<i class="fa fa-comments"/><span class="ml-1 d-none d-md-inline-block">Ask a question</span>
<i class="fa fa-comments"/><span class="ml-1 d-none d-md-inline-block">New Post</span>
</a>
</xpath>
</template>