1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
|
# -*- 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.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')
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 comments to answers', default=50)
karma_comment_convert_all = fields.Integer(string='Convert all comments to answers', default=500)
karma_comment_unlink_own = fields.Integer(string='Delete own comments', default=50)
karma_comment_unlink_all = fields.Integer(string='Delete 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/{self.env["ir.http"]._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 vals['privacy'] in ('public', '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
|