website_forum/models/forum_forum.py

363 lines
18 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import textwrap
from collections import defaultdict
from operator import itemgetter
from markupsafe import Markup
from odoo import _, api, fields, models
from odoo.addons.http_routing.models.ir_http import slug
from odoo.tools.translate import html_translate
MOST_USED_TAGS_COUNT = 5 # Number of tags to track as "most used" to display on frontend
class Forum(models.Model):
_name = 'forum.forum'
_description = 'Forum'
_inherit = [
'mail.thread',
'image.mixin',
'website.seo.metadata',
'website.multi.mixin',
'website.searchable.mixin',
]
_order = "sequence, id"
@api.model
def _get_default_welcome_message(self):
return Markup("""
<h2 class="display-3-fs" style="text-align: center;clear-both;font-weight: bold;">%(message_intro)s</h2>
<div class="text-white">
<p class="lead o_default_snippet_text" style="text-align: center;">%(message_post)s</p>
<p style="text-align: center;">
<a class="btn btn-primary forum_register_url" href="/web/login">%(register_text)s</a>
<button type="button" class="btn btn-light js_close_intro" aria-label="Dismiss message">
%(hide_text)s
</button>
</p>
</div>
""") % {
'message_intro': _("Welcome!"),
'message_post': _(
"Share and discuss the best content and new marketing ideas, build your professional profile and become"
" a better marketer together."
),
'hide_text': _('Dismiss'),
'register_text': _('Sign up'),
}
# description and use
name = fields.Char('Forum Name', required=True, translate=True)
sequence = fields.Integer('Sequence', default=1)
mode = fields.Selection([
('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'),
('connected', 'Signed In'),
('private', 'Some users')],
help="Public: Forum is public\nSigned In: Forum is visible for signed in users\nSome users: Forum and their content are hidden for non members of selected group",
default='public')
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', translate=html_translate,
sanitize=True, sanitize_overridable=True)
description = fields.Text('Description', translate=True)
teaser = fields.Text('Teaser', compute='_compute_teaser', store=True)
welcome_message = fields.Html(
'Welcome Message', translate=html_translate,
default=_get_default_welcome_message,
sanitize_attributes=False, sanitize_form=False)
default_order = fields.Selection([
('create_date desc', 'Newest'),
('last_activity_date desc', 'Last Updated'),
('vote_count desc', 'Most Voted'),
('relevancy desc', 'Relevance'),
('child_count desc', 'Answered')],
string='Default', required=True, default='last_activity_date desc')
relevancy_post_vote = fields.Float('First Relevance Parameter', default=0.8, help="This formula is used in order to sort by relevance. The variable 'votes' represents number of votes for a post, and 'days' is number of days since the post creation")
relevancy_time_decay = fields.Float('Second Relevance Parameter', default=1.8)
allow_share = fields.Boolean('Sharing Options', default=True,
help='After posting the user will be proposed to share its question '
'or answer on social networks, enabling social network propagation '
'of the forum content.')
# posts statistics
post_ids = fields.One2many('forum.post', 'forum_id', string='Posts')
last_post_id = fields.Many2one('forum.post', compute='_compute_last_post_id')
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
karma_gen_question_new = fields.Integer(string='Asking a question', default=2)
karma_gen_question_upvote = fields.Integer(string='Question upvoted', default=5)
karma_gen_question_downvote = fields.Integer(string='Question downvoted', default=-2)
karma_gen_answer_upvote = fields.Integer(string='Answer upvoted', default=10)
karma_gen_answer_downvote = fields.Integer(string='Answer downvoted', default=-2)
karma_gen_answer_accept = fields.Integer(string='Accepting an answer', default=2)
karma_gen_answer_accepted = fields.Integer(string='Answer accepted', default=15)
karma_gen_answer_flagged = fields.Integer(string='Answer flagged', default=-100)
# karma-based actions
karma_ask = fields.Integer(string='Ask questions', default=3)
karma_answer = fields.Integer(string='Answer questions', default=3)
karma_edit_own = fields.Integer(string='Edit own posts', default=1)
karma_edit_all = fields.Integer(string='Edit all posts', default=300)
karma_edit_retag = fields.Integer(string='Change question tags', default=75)
karma_close_own = fields.Integer(string='Close own posts', default=100)
karma_close_all = fields.Integer(string='Close all posts', default=500)
karma_unlink_own = fields.Integer(string='Delete own posts', default=500)
karma_unlink_all = fields.Integer(string='Delete all posts', default=1000)
karma_tag_create = fields.Integer(string='Create new tags', default=30)
karma_upvote = fields.Integer(string='Upvote', default=5)
karma_downvote = fields.Integer(string='Downvote', default=50)
karma_answer_accept_own = fields.Integer(string='Accept an answer on own questions', default=20)
karma_answer_accept_all = fields.Integer(string='Accept an answer to all questions', default=500)
karma_comment_own = fields.Integer(string='Comment own posts', default=1)
karma_comment_all = fields.Integer(string='Comment all posts', default=1)
karma_comment_convert_own = fields.Integer(string='Convert own answers to comments and vice versa', default=50)
karma_comment_convert_all = fields.Integer(string='Convert all answers to comments and vice versa', default=500)
karma_comment_unlink_own = fields.Integer(string='Unlink own comments', default=50)
karma_comment_unlink_all = fields.Integer(string='Unlink all comments', default=500)
karma_flag = fields.Integer(string='Flag a post as offensive', default=500)
karma_dofollow = fields.Integer(string='Nofollow links', help='If the author has not enough karma, a nofollow attribute is added to links', default=500)
karma_editor = fields.Integer(string='Editor Features: image and links',
default=30)
karma_user_bio = fields.Integer(string='Display detailed user biography', default=750)
karma_post = fields.Integer(string='Ask questions without validation', default=100)
karma_moderate = fields.Integer(string='Moderate posts', default=1000)
has_pending_post = fields.Boolean(string='Has pending post', compute='_compute_has_pending_post')
can_moderate = fields.Boolean(string="Is a moderator", compute="_compute_can_moderate")
# tags
tag_ids = fields.One2many('forum.tag', 'forum_id', string='Tags')
tag_most_used_ids = fields.One2many('forum.tag', string="Most used tags", compute='_compute_tag_ids_usage')
tag_unused_ids = fields.One2many('forum.tag', string="Unused tags", compute='_compute_tag_ids_usage')
@api.depends_context('uid')
def _compute_has_pending_post(self):
domain = [
('create_uid', '=', self.env.user.id),
('state', '=', 'pending'),
('parent_id', '=', False),
]
pending_forums = self.env['forum.forum'].search([
('id', 'in', self.ids),
('post_ids', 'any', domain),
])
pending_forums.has_pending_post = True
(self - pending_forums).has_pending_post = False
@api.depends_context('uid')
@api.depends('karma_moderate')
def _compute_can_moderate(self):
for forum in self:
forum.can_moderate = self.env.user.karma >= forum.karma_moderate
@api.depends('post_ids', 'post_ids.tag_ids', 'post_ids.tag_ids.posts_count', 'tag_ids')
def _compute_tag_ids_usage(self):
forums_without_tags = self.filtered(lambda f: not f.tag_ids)
forums_without_tags.tag_most_used_ids = forums_without_tags.tag_unused_ids = False
forums_with_tags = self - forums_without_tags
if not forums_with_tags:
return
tags_data = self.env['forum.tag'].search_read(
[('forum_id', 'in', forums_with_tags.ids)],
fields=['id', 'forum_id', 'posts_count'],
order='forum_id, posts_count DESC, name, id',
)
current_forum_id = tags_data[0]['forum_id'][0]
forum_tags = defaultdict(lambda: {'most_used_ids': [], 'unused_ids': []})
for tag_data in tags_data:
tag_id, tag_forum_id, posts_count = itemgetter('id', 'forum_id', 'posts_count')(tag_data)
if tag_forum_id[0] != current_forum_id:
current_forum_id = tag_forum_id[0]
if not posts_count: # Could be 0 or None
forum_tags[current_forum_id]['unused_ids'].append(tag_id)
elif len(forum_tags[current_forum_id]['most_used_ids']) < MOST_USED_TAGS_COUNT:
forum_tags[current_forum_id]['most_used_ids'].append(tag_id)
for forum in forums_with_tags:
forum.tag_most_used_ids = self.env['forum.tag'].browse(forum_tags[forum.id]['most_used_ids'])
forum.tag_unused_ids = self.env['forum.tag'].browse(forum_tags[forum.id]['unused_ids'])
@api.depends('description')
def _compute_teaser(self):
for forum in self:
forum.teaser = textwrap.shorten(forum.description, width=180, placeholder='...') if forum.description else ""
@api.depends('post_ids')
def _compute_last_post_id(self):
last_forums_posts = self.env['forum.post']._read_group(
[('forum_id', 'in', self.ids), ('parent_id', '=', False), ('state', '=', 'active')],
groupby=['forum_id'], aggregates=['id:max'],
)
forum_to_last_post_id = {forum.id: last_post_id for forum, last_post_id in last_forums_posts}
for forum in self:
forum.last_post_id = forum_to_last_post_id.get(forum.id, False)
@api.depends('post_ids.state', 'post_ids.views', 'post_ids.child_count', 'post_ids.favourite_count')
def _compute_forum_statistics(self):
default_stats = {'total_posts': 0, 'total_views': 0, 'total_answers': 0, 'total_favorites': 0}
if not self.ids:
self.update(default_stats)
return
result = {cid: dict(default_stats) for cid in self.ids}
read_group_res = self.env['forum.post']._read_group(
[('forum_id', 'in', self.ids), ('state', 'in', ('active', 'close')), ('parent_id', '=', False)],
['forum_id'],
['__count', 'views:sum', 'child_count:sum', 'favourite_count:sum'])
for forum, count, views_sum, child_count_sum, favourite_count_sum in read_group_res:
stat_forum = result[forum.id]
stat_forum['total_posts'] += count
stat_forum['total_views'] += views_sum
stat_forum['total_answers'] += child_count_sum
stat_forum['total_favorites'] += 1 if favourite_count_sum else 0
for record in self:
record.update(result[record.id])
def _compute_count_posts_waiting_validation(self):
for forum in self:
domain = [('forum_id', '=', forum.id), ('state', '=', 'pending')]
forum.count_posts_waiting_validation = self.env['forum.post'].search_count(domain)
def _compute_count_flagged_posts(self):
for forum in self:
domain = [('forum_id', '=', forum.id), ('state', '=', 'flagged')]
forum.count_flagged_posts = self.env['forum.post'].search_count(domain)
# EXTENDS WEBSITE.MULTI.MIXIN
def _compute_website_url(self):
if not self.id:
return False
return f'/forum/{slug(self)}'
# ----------------------------------------------------------------------
# CRUD
# ----------------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
forums = super(
Forum,
self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)
).create(vals_list)
self.env['website'].sudo()._update_forum_count()
forums._set_default_faq()
return forums
def unlink(self):
self.env['website'].sudo()._update_forum_count()
return super().unlink()
def write(self, vals):
if 'privacy' in vals:
if not vals['privacy']:
# The forum is neither public, neither private, remove menu to avoid conflict
self.menu_id.unlink()
elif vals['privacy'] == 'public':
# The forum is public, the menu must be also public
vals['authorized_group_id'] = False
elif vals['privacy'] == 'connected':
vals['authorized_group_id'] = False
res = super().write(vals)
if 'active' in vals:
# archiving/unarchiving a forum does it on its posts, too
self.env['forum.post'].with_context(active_test=False).search([('forum_id', 'in', self.ids)]).write({'active': vals['active']})
if 'active' in vals or 'website_id' in vals:
self.env['website'].sudo()._update_forum_count()
return res
def _set_default_faq(self):
for forum in self:
forum.faq = self.env['ir.ui.view']._render_template('website_forum.faq_accordion', {"forum": forum})
# ----------------------------------------------------------------------
# TOOLS
# ----------------------------------------------------------------------
def _tag_to_write_vals(self, tags=''):
Tag = self.env['forum.tag']
post_tags = []
existing_keep = []
user = self.env.user
for tag_id_or_new_name in (tag.strip() for tag in tags.split(',') if tag and tag.strip()):
if tag_id_or_new_name.startswith('_'): # it's a new tag
tag_name = tag_id_or_new_name[1:]
# check that not already created meanwhile or maybe excluded by the limit on the search
tag_ids = Tag.search([('name', '=', tag_name), ('forum_id', '=', self.id)], limit=1)
if tag_ids:
existing_keep.append(tag_ids.id)
else:
# check if user have Karma needed to create need tag
if user.exists() and user.karma >= self.karma_tag_create and tag_name:
post_tags.append((0, 0, {'name': tag_name, 'forum_id': self.id}))
else:
existing_keep.append(int(tag_id_or_new_name))
post_tags.insert(0, [6, 0, existing_keep])
return post_tags
def _get_tags_first_char(self, tags=None):
"""Get set of first letter of forum tags.
:param tags: tags recordset to further filter forum's tags that are also in these tags.
"""
tag_ids = self.tag_ids if tags is None else (self.tag_ids & tags)
return sorted({tag.name[0].upper() for tag in tag_ids if len(tag.name)})
# ----------------------------------------------------------------------
# WEBSITE
# ----------------------------------------------------------------------
def go_to_website(self):
self.ensure_one()
website_url = self._compute_website_url()
if not website_url:
return False
return self.env['website'].get_client_action(self._compute_website_url())
@api.model
def _search_get_detail(self, website, order, options):
with_description = options['displayDescription']
search_fields = ['name']
fetch_fields = ['id', 'name']
mapping = {
'name': {'name': 'name', 'type': 'text', 'match': True},
'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
}
if with_description:
search_fields.append('description')
fetch_fields.append('description')
mapping['description'] = {'name': 'description', 'type': 'text', 'match': True}
return {
'model': 'forum.forum',
'base_domain': [website.website_domain()],
'search_fields': search_fields,
'fetch_fields': fetch_fields,
'mapping': mapping,
'icon': 'fa-comments-o',
'order': 'name desc, id desc' if 'name desc' in order else 'name asc, id desc',
}
def _search_render_results(self, fetch_fields, mapping, icon, limit):
results_data = super()._search_render_results(fetch_fields, mapping, icon, limit)
for forum, data in zip(self, results_data):
data['website_url'] = forum._compute_website_url()
return results_data