Начальное наполнение
This commit is contained in:
parent
be57b68e14
commit
875e1038b3
5
__init__.py
Normal file
5
__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import populate
|
82
__manifest__.py
Normal file
82
__manifest__.py
Normal file
@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
{
|
||||
'name': 'Forum',
|
||||
'category': 'Website/Website',
|
||||
'sequence': 265,
|
||||
'summary': 'Manage a forum with FAQ and Q&A',
|
||||
'version': '1.2',
|
||||
'description': """
|
||||
Ask questions, get answers, no distractions
|
||||
""",
|
||||
'website': 'https://www.odoo.com/app/forum',
|
||||
'depends': [
|
||||
'auth_signup',
|
||||
'website_mail',
|
||||
'website_profile',
|
||||
],
|
||||
'data': [
|
||||
'data/ir_config_parameter_data.xml',
|
||||
'data/forum_forum_template_faq.xml',
|
||||
'data/forum_forum_data.xml',
|
||||
'data/forum_post_reason_data.xml',
|
||||
'data/ir_actions_data.xml',
|
||||
'data/mail_message_subtype_data.xml',
|
||||
'data/mail_templates.xml',
|
||||
'data/website_menu_data.xml',
|
||||
|
||||
'views/forum_post_views.xml',
|
||||
'views/forum_post_reason_views.xml',
|
||||
'views/forum_tag_views.xml',
|
||||
'views/forum_forum_views.xml',
|
||||
'views/res_users_views.xml',
|
||||
'views/gamification_karma_tracking_views.xml',
|
||||
'views/forum_menus.xml',
|
||||
|
||||
'views/base_contact_templates.xml',
|
||||
'views/forum_forum_templates.xml',
|
||||
'views/forum_forum_templates_forum_all.xml',
|
||||
'views/forum_forum_templates_layout.xml',
|
||||
'views/forum_forum_templates_moderation.xml',
|
||||
'views/forum_forum_templates_post.xml',
|
||||
'views/forum_forum_templates_tools.xml',
|
||||
'views/forum_templates_mail.xml',
|
||||
'views/website_profile_templates.xml',
|
||||
'views/snippets/snippets.xml',
|
||||
|
||||
'security/ir.model.access.csv',
|
||||
'security/ir_rule_data.xml',
|
||||
|
||||
'data/gamification_badge_data_question.xml',
|
||||
'data/gamification_badge_data_answer.xml',
|
||||
'data/gamification_badge_data_participation.xml',
|
||||
'data/gamification_badge_data_moderation.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/forum_tag_demo.xml',
|
||||
'data/forum_post_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'assets': {
|
||||
'website.assets_editor': [
|
||||
'website_forum/static/src/js/systray_items/*.js',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'website_forum/static/tests/**/*',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
'website_forum/static/src/js/tours/website_forum.js',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
'website_forum/static/src/js/tours/website_forum.js',
|
||||
'website_forum/static/src/scss/website_forum.scss',
|
||||
'website_forum/static/src/js/website_forum.js',
|
||||
'website_forum/static/src/js/website_forum.share.js',
|
||||
'website_forum/static/src/xml/public_templates.xml',
|
||||
'website_forum/static/src/components/flag_mark_as_offensive/**/*',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
}
|
3
controllers/__init__.py
Normal file
3
controllers/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import website_forum
|
776
controllers/website_forum.py
Normal file
776
controllers/website_forum.py
Normal file
@ -0,0 +1,776 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import json
|
||||
import logging
|
||||
|
||||
import lxml
|
||||
import requests
|
||||
import werkzeug.exceptions
|
||||
import werkzeug.urls
|
||||
import werkzeug.wrappers
|
||||
|
||||
from odoo import _, http, tools
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
from odoo.addons.portal.controllers.portal import _build_url_w_params
|
||||
from odoo.addons.website.models.ir_http import sitemap_qs2dom
|
||||
from odoo.addons.website_profile.controllers.main import WebsiteProfile
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.http import request
|
||||
from odoo.osv import expression
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebsiteForum(WebsiteProfile):
|
||||
_post_per_page = 10
|
||||
_user_per_page = 30
|
||||
|
||||
def _prepare_user_values(self, **kwargs):
|
||||
values = super(WebsiteForum, self)._prepare_user_values(**kwargs)
|
||||
values['forum_welcome_message'] = request.httprequest.cookies.get('forum_welcome_message', False)
|
||||
values.update({
|
||||
'header': kwargs.get('header', dict()),
|
||||
'searches': kwargs.get('searches', dict()),
|
||||
})
|
||||
if kwargs.get('forum'):
|
||||
values['forum'] = kwargs.get('forum')
|
||||
elif kwargs.get('forum_id'):
|
||||
values['forum'] = request.env['forum.forum'].browse(int(kwargs.pop('forum_id')))
|
||||
forum = values.get('forum')
|
||||
if forum and forum is not True and not request.env.user._is_public():
|
||||
def _get_my_other_forums():
|
||||
post_domain = expression.OR(
|
||||
[[('create_uid', '=', request.uid)],
|
||||
[('favourite_ids', '=', request.uid)]]
|
||||
)
|
||||
return request.env['forum.forum'].search(expression.AND([
|
||||
request.website.website_domain(),
|
||||
[('id', '!=', forum.id)],
|
||||
[('post_ids', 'any', post_domain)]
|
||||
]))
|
||||
values['my_other_forums'] = tools.lazy(_get_my_other_forums)
|
||||
else:
|
||||
values['my_other_forums'] = request.env['forum.forum']
|
||||
return values
|
||||
|
||||
def _prepare_mark_as_offensive_values(self, post, **kwargs):
|
||||
offensive_reasons = request.env['forum.post.reason'].search([('reason_type', '=', 'offensive')])
|
||||
|
||||
values = self._prepare_user_values(**kwargs)
|
||||
values.update({
|
||||
'question': post,
|
||||
'forum': post.forum_id,
|
||||
'reasons': offensive_reasons,
|
||||
'offensive': True,
|
||||
})
|
||||
return values
|
||||
|
||||
# Forum
|
||||
# --------------------------------------------------
|
||||
|
||||
@http.route(['/forum'], type='http', auth="public", website=True, sitemap=True)
|
||||
def forum(self, **kwargs):
|
||||
domain = request.website.website_domain()
|
||||
forums = request.env['forum.forum'].search(domain)
|
||||
if len(forums) == 1:
|
||||
return request.redirect('/forum/%s' % slug(forums[0]), code=302)
|
||||
|
||||
return request.render("website_forum.forum_all", {
|
||||
'forums': forums
|
||||
})
|
||||
|
||||
def sitemap_forum(env, rule, qs):
|
||||
Forum = env['forum.forum']
|
||||
dom = sitemap_qs2dom(qs, '/forum', Forum._rec_name)
|
||||
dom += env['website'].get_current_website().website_domain()
|
||||
for f in Forum.search(dom):
|
||||
loc = '/forum/%s' % slug(f)
|
||||
if not qs or qs.lower() in loc:
|
||||
yield {'loc': loc}
|
||||
|
||||
def _get_forum_post_search_options(self, forum=None, tag=None, filters=None, my=None, create_uid=False, include_answers=False, **post):
|
||||
return {
|
||||
'allowFuzzy': not post.get('noFuzzy'),
|
||||
'create_uid': create_uid,
|
||||
'displayDescription': False,
|
||||
'displayDetail': False,
|
||||
'displayExtraDetail': False,
|
||||
'displayExtraLink': False,
|
||||
'displayImage': False,
|
||||
'filters': filters,
|
||||
'forum': str(forum.id) if forum else None,
|
||||
'include_answers': include_answers,
|
||||
'my': my,
|
||||
'tag': str(tag.id) if tag else None,
|
||||
}
|
||||
|
||||
@http.route(['/forum/all',
|
||||
'/forum/all/page/<int:page>',
|
||||
'/forum/<model("forum.forum"):forum>',
|
||||
'/forum/<model("forum.forum"):forum>/page/<int:page>',
|
||||
'''/forum/<model("forum.forum"):forum>/tag/<model("forum.tag"):tag>/questions''',
|
||||
'''/forum/<model("forum.forum"):forum>/tag/<model("forum.tag"):tag>/questions/page/<int:page>''',
|
||||
], type='http', auth="public", website=True, sitemap=sitemap_forum)
|
||||
def questions(self, forum=None, tag=None, page=1, filters='all', my=None, sorting=None, search='', create_uid=False, include_answers=False, **post):
|
||||
Post = request.env['forum.post']
|
||||
|
||||
author = request.env['res.users'].browse(int(create_uid))
|
||||
|
||||
if author == request.env.user:
|
||||
my = 'mine'
|
||||
if sorting:
|
||||
# check that sorting is valid
|
||||
# retro-compatibility for V8 and google links
|
||||
try:
|
||||
sorting = werkzeug.urls.url_unquote_plus(sorting)
|
||||
Post._order_to_sql(sorting, None)
|
||||
except (UserError, ValueError):
|
||||
sorting = False
|
||||
|
||||
if not sorting:
|
||||
sorting = forum.default_order if forum else 'last_activity_date desc'
|
||||
|
||||
options = self._get_forum_post_search_options(
|
||||
forum=forum,
|
||||
tag=tag,
|
||||
filters=filters,
|
||||
my=my,
|
||||
create_uid=author.id,
|
||||
include_answers=include_answers,
|
||||
my_profile=request.env.user == author,
|
||||
**post
|
||||
)
|
||||
question_count, details, fuzzy_search_term = request.website._search_with_fuzzy(
|
||||
"forum_posts_only", search, limit=page * self._post_per_page, order=sorting, options=options)
|
||||
question_ids = details[0].get('results', Post)
|
||||
question_ids = question_ids[(page - 1) * self._post_per_page:page * self._post_per_page]
|
||||
|
||||
if not forum:
|
||||
url = '/forum/all'
|
||||
else:
|
||||
url = f"/forum/{slug(forum)}{f'/tag/{slug(tag)}/questions' if tag else ''}"
|
||||
|
||||
url_args = {'sorting': sorting}
|
||||
|
||||
for name, value in zip(['filters', 'search', 'my'], [filters, search, my]):
|
||||
if value:
|
||||
url_args[name] = value
|
||||
|
||||
pager = tools.lazy(lambda: request.website.pager(
|
||||
url=url, total=question_count, page=page, step=self._post_per_page,
|
||||
scope=self._post_per_page, url_args=url_args))
|
||||
|
||||
values = self._prepare_user_values(forum=forum, searches=post)
|
||||
values.update({
|
||||
'author': author,
|
||||
'edit_in_backend': True,
|
||||
'question_ids': question_ids,
|
||||
'question_count': question_count,
|
||||
'search_count': question_count,
|
||||
'pager': pager,
|
||||
'tag': tag,
|
||||
'filters': filters,
|
||||
'my': my,
|
||||
'sorting': sorting,
|
||||
'search': fuzzy_search_term or search,
|
||||
'original_search': fuzzy_search_term and search,
|
||||
})
|
||||
|
||||
if forum or tag:
|
||||
values['main_object'] = tag or forum
|
||||
|
||||
return request.render("website_forum.forum_index", values)
|
||||
|
||||
@http.route(['''/forum/<model("forum.forum"):forum>/faq'''], type='http', auth="public", website=True, sitemap=True)
|
||||
def forum_faq(self, forum, **post):
|
||||
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)
|
||||
|
||||
# Tags
|
||||
# --------------------------------------------------
|
||||
|
||||
@http.route('/forum/get_tags', type='http', auth="public", methods=['GET'], website=True, sitemap=False)
|
||||
def tag_read(self, forum_id, query='', limit=25, **post):
|
||||
data = request.env['forum.tag'].search_read(
|
||||
domain=[('forum_id', '=', int(forum_id)), ('name', '=ilike', (query or '') + "%")],
|
||||
fields=['id', 'name'],
|
||||
limit=int(limit),
|
||||
)
|
||||
return request.make_response(
|
||||
json.dumps(data),
|
||||
headers=[("Content-Type", "application/json")]
|
||||
)
|
||||
|
||||
@http.route(['/forum/<model("forum.forum"):forum>/tag',
|
||||
'/forum/<model("forum.forum"):forum>/tag/<string:tag_char>',
|
||||
], type='http', auth="public", website=True, sitemap=False)
|
||||
def tags(self, forum, tag_char='', filters='all', search='', **post):
|
||||
"""Render a list of tags matching filters and search parameters.
|
||||
|
||||
:param forum: Forum
|
||||
:param string tag_char: Only tags starting with a single character `tag_char`
|
||||
:param filters: One of 'all'|'followed'|'most_used'|'unused'.
|
||||
Can be combined with `search` and `tag_char`.
|
||||
:param string search: Search query using "forum_tags_only" `search_type`
|
||||
:param dict post: additional options passed to `_prepare_user_values`
|
||||
"""
|
||||
if not isinstance(tag_char, str) or len(tag_char) > 1 or (tag_char and not tag_char.isalpha()):
|
||||
# So that further development does not miss this. Users shouldn't see it with normal usage.
|
||||
raise werkzeug.exceptions.BadRequest(_('Bad "tag_char" value "%(tag_char)s"', tag_char=tag_char))
|
||||
|
||||
domain = [('forum_id', '=', forum.id), ('posts_count', '=' if filters == "unused" else '>', 0)]
|
||||
if filters == 'followed' and not request.env.user._is_public():
|
||||
domain = expression.AND([domain, [('message_is_follower', '=', True)]])
|
||||
|
||||
# Build tags result without using tag_char to build pager, then return tags matching it
|
||||
values = self._prepare_user_values(forum=forum, searches={'tags': True}, **post)
|
||||
tags = request.env["forum.tag"]
|
||||
|
||||
order = 'posts_count DESC' if tag_char else 'name'
|
||||
|
||||
if search:
|
||||
values.update(search=search)
|
||||
search_domain = domain if filters in ('all', 'followed') else None
|
||||
__, details, __ = request.website._search_with_fuzzy(
|
||||
'forum_tags_only', search, limit=None, order=order, options={'forum': forum, 'domain': search_domain},
|
||||
)
|
||||
tags = details[0].get('results', tags)
|
||||
|
||||
if filters in ('unused', 'most_used'):
|
||||
filter_tags = forum.tag_most_used_ids if filters == 'most_used' else forum.tag_unused_ids
|
||||
tags = tags & filter_tags if tags else filter_tags
|
||||
elif filters in ('all', 'followed'):
|
||||
if not search:
|
||||
tags = request.env['forum.tag'].search(domain, limit=None, order=order)
|
||||
else:
|
||||
raise werkzeug.exceptions.BadRequest(_('Bad "filters" value "%(filters)s".', filters=filters))
|
||||
|
||||
first_char_tag = forum._get_tags_first_char(tags=tags)
|
||||
first_char_list = [(t, t.lower()) for t in first_char_tag if t.isalnum()]
|
||||
first_char_list.insert(0, (_('All'), ''))
|
||||
if tag_char:
|
||||
tags = tags.filtered(lambda t: t.name.startswith((tag_char.lower(), tag_char.upper())))
|
||||
|
||||
values.update({
|
||||
'active_char_tag': tag_char.lower(),
|
||||
'pager_tag_chars': first_char_list,
|
||||
'search_count': len(tags) if search else None,
|
||||
'tags': tags,
|
||||
})
|
||||
return request.render("website_forum.forum_index_tags", values)
|
||||
|
||||
# Questions
|
||||
# --------------------------------------------------
|
||||
|
||||
@http.route('/forum/get_url_title', type='json', auth="user", methods=['POST'], website=True)
|
||||
def get_url_title(self, **kwargs):
|
||||
try:
|
||||
req = requests.get(kwargs.get('url'))
|
||||
req.raise_for_status()
|
||||
arch = lxml.html.fromstring(req.content)
|
||||
return arch.find(".//title").text
|
||||
except IOError:
|
||||
return False
|
||||
|
||||
@http.route(['''/forum/<model("forum.forum"):forum>/question/<model("forum.post", "[('forum_id','=',forum.id),('parent_id','=',False),('can_view', '=', True)]"):question>'''],
|
||||
type='http', auth="public", website=True, sitemap=False)
|
||||
def old_question(self, forum, question, **post):
|
||||
# Compatibility pre-v14
|
||||
return request.redirect(_build_url_w_params("/forum/%s/%s" % (slug(forum), slug(question)), request.params), code=301)
|
||||
|
||||
@http.route(['''/forum/<model("forum.forum"):forum>/<model("forum.post", "[('forum_id','=',forum.id),('parent_id','=',False),('can_view', '=', True)]"):question>'''],
|
||||
type='http', auth="public", website=True, sitemap=True)
|
||||
def question(self, forum, question, **post):
|
||||
if not forum.active:
|
||||
return request.render("website_forum.header", {'forum': forum})
|
||||
|
||||
# Hide posts from abusers (negative karma), except for moderators
|
||||
if not question.can_view:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
# Hide pending posts from non-moderators and non-creator
|
||||
user = request.env.user
|
||||
if question.state == 'pending' and user.karma < forum.karma_post and question.create_uid != user:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
if question.parent_id:
|
||||
redirect_url = "/forum/%s/%s" % (slug(forum), slug(question.parent_id))
|
||||
return request.redirect(redirect_url, 301)
|
||||
filters = 'question'
|
||||
values = self._prepare_user_values(forum=forum, searches=post)
|
||||
values.update({
|
||||
'main_object': question,
|
||||
'edit_in_backend': True,
|
||||
'question': question,
|
||||
'header': {'question_data': True},
|
||||
'filters': filters,
|
||||
'reversed': reversed,
|
||||
})
|
||||
if (request.httprequest.referrer or "").startswith(request.httprequest.url_root):
|
||||
values['has_back_button_url'] = True
|
||||
|
||||
# increment view counter
|
||||
question.sudo()._set_viewed()
|
||||
|
||||
return request.render("website_forum.post_description_full", values)
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/toggle_favourite', type='json', auth="user", methods=['POST'], website=True)
|
||||
def question_toggle_favorite(self, forum, question, **post):
|
||||
if not request.session.uid:
|
||||
return {'error': 'anonymous_user'}
|
||||
favourite = not question.user_favourite
|
||||
question.sudo().favourite_ids = [(favourite and 4 or 3, request.uid)]
|
||||
if favourite:
|
||||
# Automatically add the user as follower of the posts that he
|
||||
# favorites (on unfavorite we chose to keep him as a follower until
|
||||
# he decides to not follow anymore).
|
||||
question.sudo().message_subscribe(request.env.user.partner_id.ids)
|
||||
return favourite
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/ask_for_close', type='http', auth="user", methods=['POST'], website=True)
|
||||
def question_ask_for_close(self, forum, question, **post):
|
||||
reasons = request.env['forum.post.reason'].search([('reason_type', '=', 'basic')])
|
||||
|
||||
values = self._prepare_user_values(**post)
|
||||
values.update({
|
||||
'question': question,
|
||||
'forum': forum,
|
||||
'reasons': reasons,
|
||||
})
|
||||
return request.render("website_forum.close_post", values)
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/edit_answer', type='http', auth="user", website=True)
|
||||
def question_edit_answer(self, forum, question, **kwargs):
|
||||
for record in question.child_ids:
|
||||
if record.create_uid.id == request.uid:
|
||||
answer = record
|
||||
break
|
||||
else:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
return request.redirect(f'/forum/{slug(forum)}/post/{slug(answer)}/edit')
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/close', type='http', auth="user", methods=['POST'], website=True)
|
||||
def question_close(self, forum, question, **post):
|
||||
question.close(reason_id=int(post.get('reason_id', False)))
|
||||
return request.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/reopen', type='http', auth="user", methods=['POST'], website=True)
|
||||
def question_reopen(self, forum, question, **kwarg):
|
||||
question.reopen()
|
||||
return request.redirect("/forum/%s/%s" % (slug(forum), slug(question)))
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/delete', type='http', auth="user", methods=['POST'], website=True)
|
||||
def question_delete(self, forum, question, **kwarg):
|
||||
question.active = False
|
||||
return request.redirect("/forum/%s" % slug(forum))
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/undelete', type='http', auth="user", methods=['POST'], website=True)
|
||||
def question_undelete(self, forum, question, **kwarg):
|
||||
question.active = True
|
||||
return request.redirect("/forum/%s/%s" % (slug(forum), slug(question)))
|
||||
|
||||
# Post
|
||||
# --------------------------------------------------
|
||||
@http.route(['/forum/<model("forum.forum"):forum>/ask'], type='http', auth="user", website=True)
|
||||
def forum_post(self, forum, **post):
|
||||
user = request.env.user
|
||||
if not user.email or not tools.single_email_re.match(user.email):
|
||||
return request.redirect("/forum/%s/user/%s/edit?email_required=1" % (slug(forum), request.session.uid))
|
||||
values = self._prepare_user_values(forum=forum, searches={}, new_question=True)
|
||||
return request.render("website_forum.new_question", values)
|
||||
|
||||
@http.route(['/forum/<model("forum.forum"):forum>/new',
|
||||
'/forum/<model("forum.forum"):forum>/<model("forum.post"):post_parent>/reply'],
|
||||
type='http', auth="user", methods=['POST'], website=True)
|
||||
def post_create(self, forum, post_parent=None, **post):
|
||||
if post.get('content', '') == '<p><br></p>':
|
||||
return request.render('http_routing.http_error', {
|
||||
'status_code': _('Bad Request'),
|
||||
'status_message': post_parent and _('Reply should not be empty.') or _('Question should not be empty.')
|
||||
})
|
||||
|
||||
post_tag_ids = forum._tag_to_write_vals(post.get('post_tags', ''))
|
||||
if forum.has_pending_post:
|
||||
return request.redirect("/forum/%s/ask" % slug(forum))
|
||||
|
||||
new_question = request.env['forum.post'].create({
|
||||
'forum_id': forum.id,
|
||||
'name': post.get('post_name') or (post_parent and 'Re: %s' % (post_parent.name or '')) or '',
|
||||
'content': post.get('content', False),
|
||||
'parent_id': post_parent and post_parent.id or False,
|
||||
'tag_ids': post_tag_ids
|
||||
})
|
||||
if post_parent:
|
||||
post_parent._update_last_activity()
|
||||
return request.redirect(f'/forum/{slug(forum)}/{slug(post_parent) if post_parent else new_question.id}')
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/comment', type='http', auth="user", methods=['POST'], website=True)
|
||||
def post_comment(self, forum, post, **kwargs):
|
||||
question = post.parent_id or post
|
||||
if kwargs.get('comment') and post.forum_id.id == forum.id:
|
||||
# TDE FIXME: check that post_id is the question or one of its answers
|
||||
body = tools.mail.plaintext2html(kwargs['comment'])
|
||||
post.with_context(mail_create_nosubscribe=True).message_post(
|
||||
body=body,
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment')
|
||||
question._update_last_activity()
|
||||
return request.redirect(f'/forum/{slug(forum)}/{slug(question)}')
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/toggle_correct', type='json', auth="public", website=True)
|
||||
def post_toggle_correct(self, forum, post, **kwargs):
|
||||
if post.parent_id is False:
|
||||
return request.redirect('/')
|
||||
if request.uid == post.create_uid.id:
|
||||
return {'error': 'own_post'}
|
||||
if not request.session.uid:
|
||||
return {'error': 'anonymous_user'}
|
||||
|
||||
# set all answers to False, only one can be accepted
|
||||
(post.parent_id.child_ids - post).write(dict(is_correct=False))
|
||||
post.is_correct = not post.is_correct
|
||||
return post.is_correct
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/delete', type='http', auth="user", methods=['POST'], website=True)
|
||||
def post_delete(self, forum, post, **kwargs):
|
||||
question = post.parent_id
|
||||
post.unlink()
|
||||
if question:
|
||||
request.redirect("/forum/%s/%s" % (slug(forum), slug(question)))
|
||||
return request.redirect("/forum/%s" % slug(forum))
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/edit', type='http', auth="user", website=True)
|
||||
def post_edit(self, forum, post, **kwargs):
|
||||
tags = [dict(id=tag.id, name=tag.name) for tag in post.tag_ids]
|
||||
tags = json.dumps(tags)
|
||||
values = self._prepare_user_values(forum=forum)
|
||||
values.update({
|
||||
'tags': tags,
|
||||
'post': post,
|
||||
'is_edit': True,
|
||||
'is_answer': bool(post.parent_id),
|
||||
'searches': kwargs,
|
||||
'content': post.name,
|
||||
})
|
||||
return request.render("website_forum.edit_post", values)
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/save', type='http', auth="user", methods=['POST'], website=True)
|
||||
def post_save(self, forum, post, **kwargs):
|
||||
vals = {
|
||||
'content': kwargs.get('content'),
|
||||
}
|
||||
|
||||
if 'post_name' in kwargs:
|
||||
if not kwargs.get('post_name').strip():
|
||||
return request.render('http_routing.http_error', {
|
||||
'status_code': _('Bad Request'),
|
||||
'status_message': _('Title should not be empty.')
|
||||
})
|
||||
|
||||
vals['name'] = kwargs.get('post_name')
|
||||
vals['tag_ids'] = forum._tag_to_write_vals(kwargs.get('post_tags', ''))
|
||||
post.write(vals)
|
||||
question = post.parent_id if post.parent_id else post
|
||||
return request.redirect("/forum/%s/%s" % (slug(forum), slug(question)))
|
||||
|
||||
# JSON utilities
|
||||
# --------------------------------------------------
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/upvote', type='json', auth="public", website=True)
|
||||
def post_upvote(self, forum, post, **kwargs):
|
||||
if not request.session.uid:
|
||||
return {'error': 'anonymous_user'}
|
||||
if request.uid == post.create_uid.id:
|
||||
return {'error': 'own_post'}
|
||||
upvote = True if not post.user_vote > 0 else False
|
||||
return post.vote(upvote=upvote)
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/downvote', type='json', auth="public", website=True)
|
||||
def post_downvote(self, forum, post, **kwargs):
|
||||
if not request.session.uid:
|
||||
return {'error': 'anonymous_user'}
|
||||
if request.uid == post.create_uid.id:
|
||||
return {'error': 'own_post'}
|
||||
upvote = True if post.user_vote < 0 else False
|
||||
return post.vote(upvote=upvote)
|
||||
|
||||
# Moderation Tools
|
||||
# --------------------------------------------------
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/validation_queue', type='http', auth="user", website=True)
|
||||
def validation_queue(self, forum, **kwargs):
|
||||
user = request.env.user
|
||||
if user.karma < forum.karma_moderate:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
Post = request.env['forum.post']
|
||||
domain = [('forum_id', '=', forum.id), ('state', '=', 'pending')]
|
||||
posts_to_validate_ids = Post.search(domain)
|
||||
|
||||
values = self._prepare_user_values(forum=forum)
|
||||
values.update({
|
||||
'posts_ids': posts_to_validate_ids.sudo(),
|
||||
'queue_type': 'validation',
|
||||
})
|
||||
|
||||
return request.render("website_forum.moderation_queue", values)
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/flagged_queue', type='http', auth="user", website=True)
|
||||
def flagged_queue(self, forum, **kwargs):
|
||||
user = request.env.user
|
||||
if user.karma < forum.karma_moderate:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
Post = request.env['forum.post']
|
||||
domain = [('forum_id', '=', forum.id), ('state', '=', 'flagged')]
|
||||
if kwargs.get('spam_post'):
|
||||
domain += [('name', 'ilike', kwargs.get('spam_post'))]
|
||||
flagged_posts_ids = Post.search(domain, order='write_date DESC')
|
||||
|
||||
values = self._prepare_user_values(forum=forum)
|
||||
values.update({
|
||||
'posts_ids': flagged_posts_ids.sudo(),
|
||||
'queue_type': 'flagged',
|
||||
'flagged_queue_active': 1,
|
||||
})
|
||||
|
||||
return request.render("website_forum.moderation_queue", values)
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/offensive_posts', type='http', auth="user", website=True)
|
||||
def offensive_posts(self, forum, **kwargs):
|
||||
user = request.env.user
|
||||
if user.karma < forum.karma_moderate:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
Post = request.env['forum.post']
|
||||
domain = [('forum_id', '=', forum.id), ('state', '=', 'offensive'), ('active', '=', False)]
|
||||
offensive_posts_ids = Post.search(domain, order='write_date DESC')
|
||||
|
||||
values = self._prepare_user_values(forum=forum)
|
||||
values.update({
|
||||
'posts_ids': offensive_posts_ids.sudo(),
|
||||
'queue_type': 'offensive',
|
||||
})
|
||||
|
||||
return request.render("website_forum.moderation_queue", values)
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/closed_posts', type='http', auth="user", website=True)
|
||||
def closed_posts(self, forum, **kwargs):
|
||||
if request.env.user.karma < forum.karma_moderate:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
closed_posts_ids = request.env['forum.post'].search(
|
||||
[('forum_id', '=', forum.id), ('state', '=', 'close')],
|
||||
order='write_date DESC, id DESC',
|
||||
)
|
||||
values = self._prepare_user_values(forum=forum)
|
||||
values.update({
|
||||
'posts_ids': closed_posts_ids,
|
||||
'queue_type': 'close',
|
||||
})
|
||||
|
||||
return request.render("website_forum.moderation_queue", values)
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/validate', type='http', auth="user", website=True)
|
||||
def post_accept(self, forum, post, **kwargs):
|
||||
if post.state == 'flagged':
|
||||
url = f'/forum/{slug(forum)}/flagged_queue'
|
||||
elif post.state == 'offensive':
|
||||
url = f'/forum/{slug(forum)}/offensive_posts'
|
||||
elif post.state == 'close':
|
||||
url = f'/forum/{slug(forum)}/closed_posts'
|
||||
else:
|
||||
url = f'/forum/{slug(forum)}/validation_queue'
|
||||
post.validate()
|
||||
return request.redirect(url)
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/refuse', type='http', auth="user", website=True)
|
||||
def post_refuse(self, forum, post, **kwargs):
|
||||
post.refuse()
|
||||
return self.question_ask_for_close(forum, post)
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/flag', type='json', auth="public", website=True)
|
||||
def post_flag(self, forum, post, **kwargs):
|
||||
if not request.session.uid:
|
||||
return {'error': 'anonymous_user'}
|
||||
return post.flag()[0]
|
||||
|
||||
@http.route('/forum/<model("forum.post"):post>/ask_for_mark_as_offensive', type='json', auth="user", website=True)
|
||||
def post_json_ask_for_mark_as_offensive(self, post, **kwargs):
|
||||
if not post.can_moderate:
|
||||
raise AccessError(_('%d karma required to mark a post as offensive.', post.forum_id.karma_moderate))
|
||||
values = self._prepare_mark_as_offensive_values(post, **kwargs)
|
||||
return request.env['ir.ui.view']._render_template('website_forum.mark_as_offensive', values)
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/ask_for_mark_as_offensive', type='http', auth="user", methods=['GET'], website=True)
|
||||
def post_http_ask_for_mark_as_offensive(self, forum, post, **kwargs):
|
||||
if not post.can_moderate:
|
||||
raise AccessError(_('%d karma required to mark a post as offensive.', forum.karma_moderate))
|
||||
values = self._prepare_mark_as_offensive_values(post, **kwargs)
|
||||
return request.render("website_forum.close_post", values)
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/mark_as_offensive', type='http', auth="user", methods=["POST"], website=True)
|
||||
def post_mark_as_offensive(self, forum, post, **kwargs):
|
||||
post.mark_as_offensive(reason_id=int(kwargs.get('reason_id', False)))
|
||||
if post.parent_id:
|
||||
url = f'/forum/{slug(forum)}/{post.parent_id.id}/#answer-{post.id}'
|
||||
else:
|
||||
url = f'/forum/{slug(forum)}/{slug(post)}'
|
||||
return request.redirect(url)
|
||||
|
||||
# User
|
||||
# --------------------------------------------------
|
||||
@http.route(['/forum/<model("forum.forum"):forum>/partner/<int:partner_id>'], type='http', auth="public", website=True)
|
||||
def open_partner(self, forum, partner_id=0, **post):
|
||||
if partner_id:
|
||||
partner = request.env['res.partner'].sudo().search([('id', '=', partner_id)])
|
||||
if partner and partner.user_ids:
|
||||
return request.redirect(f'/forum/{slug(forum)}/user/{partner.user_ids[0].id}')
|
||||
return request.redirect('/forum/' + slug(forum))
|
||||
|
||||
# Profile
|
||||
# -----------------------------------
|
||||
|
||||
@http.route(['/forum/user/<int:user_id>'], type='http', auth="public", website=True)
|
||||
def view_user_forum_profile(self, user_id, forum_id='', forum_origin='/forum', **post):
|
||||
forum_origin_query = f'?forum_origin={forum_origin}&forum_id={forum_id}' if forum_id else ''
|
||||
return request.redirect(f'/profile/user/{user_id}{forum_origin_query}')
|
||||
|
||||
def _prepare_user_profile_values(self, user, **post):
|
||||
values = super(WebsiteForum, self)._prepare_user_profile_values(user, **post)
|
||||
if not post.get('no_forum'):
|
||||
if post.get('forum'):
|
||||
forums = post['forum']
|
||||
elif post.get('forum_id'):
|
||||
forums = request.env['forum.forum'].browse(int(post['forum_id']))
|
||||
values.update({
|
||||
'edit_button_url_param': 'forum_id=%s' % str(post['forum_id']),
|
||||
'forum_filtered': forums.name,
|
||||
})
|
||||
else:
|
||||
forums = request.env['forum.forum'].search([])
|
||||
|
||||
values.update(self._prepare_user_values(forum=forums[0] if len(forums) == 1 else True, **post))
|
||||
if forums:
|
||||
values.update(self._prepare_open_forum_user(user, forums))
|
||||
return values
|
||||
|
||||
def _prepare_open_forum_user(self, user, forums, **kwargs):
|
||||
Post = request.env['forum.post']
|
||||
Vote = request.env['forum.post.vote']
|
||||
Activity = request.env['mail.message']
|
||||
Followers = request.env['mail.followers']
|
||||
Data = request.env["ir.model.data"]
|
||||
|
||||
# questions and answers by user
|
||||
user_question_ids = Post.search([
|
||||
('parent_id', '=', False),
|
||||
('forum_id', 'in', forums.ids), ('create_uid', '=', user.id)],
|
||||
order='create_date desc')
|
||||
count_user_questions = len(user_question_ids)
|
||||
min_karma_unlink = min(forums.mapped('karma_unlink_all'))
|
||||
|
||||
# limit length of visible posts by default for performance reasons, except for the high
|
||||
# karma users (not many of them, and they need it to properly moderate the forum)
|
||||
post_display_limit = None
|
||||
if request.env.user.karma < min_karma_unlink:
|
||||
post_display_limit = 20
|
||||
|
||||
user_questions = user_question_ids[:post_display_limit]
|
||||
user_answer_ids = Post.search([
|
||||
('parent_id', '!=', False),
|
||||
('forum_id', 'in', forums.ids), ('create_uid', '=', user.id)],
|
||||
order='create_date desc')
|
||||
count_user_answers = len(user_answer_ids)
|
||||
user_answers = user_answer_ids[:post_display_limit]
|
||||
|
||||
# showing questions which user following
|
||||
post_ids = [follower.res_id for follower in Followers.sudo().search(
|
||||
[('res_model', '=', 'forum.post'), ('partner_id', '=', user.partner_id.id)])]
|
||||
followed = Post.search([('id', 'in', post_ids), ('forum_id', 'in', forums.ids), ('parent_id', '=', False)])
|
||||
|
||||
# showing Favourite questions of user.
|
||||
favourite = Post.search(
|
||||
[('favourite_ids', '=', user.id), ('forum_id', 'in', forums.ids), ('parent_id', '=', False)])
|
||||
|
||||
# votes which given on users questions and answers.
|
||||
data = Vote._read_group(
|
||||
[('forum_id', 'in', forums.ids), ('recipient_id', '=', user.id)], ['vote'], aggregates=['__count']
|
||||
)
|
||||
up_votes, down_votes = 0, 0
|
||||
for vote, count in data:
|
||||
if vote == '1':
|
||||
up_votes = count
|
||||
elif vote == '-1':
|
||||
down_votes = count
|
||||
|
||||
# Votes which given by users on others questions and answers.
|
||||
vote_ids = Vote.search([('user_id', '=', user.id), ('forum_id', 'in', forums.ids)])
|
||||
|
||||
# activity by user.
|
||||
comment = Data._xmlid_lookup('mail.mt_comment')[1]
|
||||
activities = Activity.search(
|
||||
[('res_id', 'in', (user_question_ids + user_answer_ids).ids), ('model', '=', 'forum.post'),
|
||||
('subtype_id', '!=', comment)],
|
||||
order='date DESC', limit=100)
|
||||
|
||||
posts = {}
|
||||
for act in activities:
|
||||
posts[act.res_id] = True
|
||||
posts_ids = Post.search([('id', 'in', list(posts))])
|
||||
posts = {x.id: (x.parent_id or x, x.parent_id and x or False) for x in posts_ids}
|
||||
|
||||
if user != request.env.user:
|
||||
kwargs['users'] = True
|
||||
|
||||
values = {
|
||||
'uid': request.env.user.id,
|
||||
'user': user,
|
||||
'main_object': user,
|
||||
'searches': kwargs,
|
||||
'questions': user_questions,
|
||||
'count_questions': count_user_questions,
|
||||
'answers': user_answers,
|
||||
'count_answers': count_user_answers,
|
||||
'followed': followed,
|
||||
'favourite': favourite,
|
||||
'up_votes': up_votes,
|
||||
'down_votes': down_votes,
|
||||
'activities': activities,
|
||||
'posts': posts,
|
||||
'vote_post': vote_ids,
|
||||
'is_profile_page': True,
|
||||
'badge_category': 'forum',
|
||||
}
|
||||
|
||||
return values
|
||||
|
||||
# Messaging
|
||||
# --------------------------------------------------
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/comment/<model("mail.message"):comment>/convert_to_answer', type='http', auth="user", methods=['POST'], website=True)
|
||||
def convert_comment_to_answer(self, forum, post, comment, **kwarg):
|
||||
post = request.env['forum.post'].convert_comment_to_answer(comment.id)
|
||||
if not post:
|
||||
return request.redirect("/forum/%s" % slug(forum))
|
||||
question = post.parent_id if post.parent_id else post
|
||||
return request.redirect("/forum/%s/%s" % (slug(forum), slug(question)))
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/convert_to_comment', type='http', auth="user", methods=['POST'], website=True)
|
||||
def convert_answer_to_comment(self, forum, post, **kwarg):
|
||||
question = post.parent_id
|
||||
new_msg = post.convert_answer_to_comment()
|
||||
if not new_msg:
|
||||
return request.redirect("/forum/%s" % slug(forum))
|
||||
return request.redirect("/forum/%s/%s" % (slug(forum), slug(question)))
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/comment/<model("mail.message"):comment>/delete', type='json', auth="user", website=True)
|
||||
def delete_comment(self, forum, post, comment, **kwarg):
|
||||
if not request.session.uid:
|
||||
return {'error': 'anonymous_user'}
|
||||
return post.unlink_comment(comment.id)[0]
|
10
data/forum_forum_data.xml
Normal file
10
data/forum_forum_data.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
|
||||
<record id="forum_help" model="forum.forum">
|
||||
<field name="name">Help</field>
|
||||
<field name="image_1920" type="base64" file="website_forum/static/src/img/help.jpg"/>
|
||||
<field name="description">This community is for professionals and enthusiasts of our products and services. Share and discuss the best content and new marketing ideas, build your professional profile and become a better marketer together.</field>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
100
data/forum_forum_template_faq.xml
Normal file
100
data/forum_forum_template_faq.xml
Normal file
@ -0,0 +1,100 @@
|
||||
<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 mb-5">
|
||||
<div class="container">
|
||||
<div id="myCollapse" class="accordion rounded border" role="tablist">
|
||||
<div class="accordion-item" data-name="Item">
|
||||
<h2 class="accordion-header" id="headingOne">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapse1">
|
||||
What kind of questions can I ask here?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse1" class="accordion-collapse collapse show" data-bs-parent="#myCollapse" aria-labelledby="headingOne">
|
||||
<div class="accordion-body bg-light">
|
||||
<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="accordion-item" data-name="Item">
|
||||
<h2 class="accordion-header" id="headingTwo">
|
||||
<button class="collapsed accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapse2">
|
||||
What should I avoid in my questions?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse2" class="collapse" data-bs-parent="#myCollapse" aria-labelledby="headingTwo">
|
||||
<div class="accordion-body bg-light">
|
||||
<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="accordion-item" data-name="Item">
|
||||
<h2 class="accordion-header" id="headingThree">
|
||||
<button class="collapsed accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapse3">
|
||||
What should I avoid in my answers?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse3" class="collapse" data-bs-parent="#myCollapse" aria-labelledby="headingThree">
|
||||
<div class="accordion-body bg-light">
|
||||
<p><b>Answers should not add or expand questions</b>. Instead, either 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&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 2 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="accordion-item" data-name="Item">
|
||||
<h2 class="accordion-header" id="headingFour">
|
||||
<button class="collapsed accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapse4">
|
||||
Why can other people edit my questions/answers?
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse4" class="collapse" data-bs-parent="#myCollapse" aria-labelledby="headingFour">
|
||||
<div class="accordion-body bg-light">
|
||||
<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>
|
72
data/forum_post_demo.xml
Normal file
72
data/forum_post_demo.xml
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="question_0" model="forum.post">
|
||||
<field name="name">How to configure alerts for employee contract expiration</field>
|
||||
<field name="forum_id" ref="website_forum.forum_help"/>
|
||||
<field name="views">3</field>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="write_uid" ref="base.user_admin"/>
|
||||
<field name="tag_ids" eval="[(4,ref('website_forum.tags_0')), (4,ref('website_forum.tags_1'))]"/>
|
||||
</record>
|
||||
<record id="question_1" model="forum.post">
|
||||
<field name="name">CMS replacement for ERP and eCommerce</field>
|
||||
<field name="views">8</field>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="write_uid" ref="base.user_admin"/>
|
||||
<field name="forum_id" ref="website_forum.forum_help"/>
|
||||
<field name="content"><![CDATA[<p>I use Wordpress as a CMS and eCommerce platform. The developing in Wordpress is quite easy and solid but it missing ERP feature (there is single plugin to integrate with Frontaccounting) so I wonder:
|
||||
|
||||
Can I use Odoo as a replacement CMS of Wordpress + eCommerce plugin?
|
||||
|
||||
In simple words does Odoo became CMS+ERP platform?</p>]]></field>
|
||||
<field name="tag_ids" eval="[(4,ref('website_forum.tags_2'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Answer -->
|
||||
<record id="answer_0" model="forum.post">
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<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 --> Automation Rules. Create A new Automation Rule. 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 Rule 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 Rule.
|
||||
<li>Step 2. Go to Server Actions and create new Action. Call it Fleet Contract Expiration Warning. The Object will be the same as above .. Contract information on a vehicle. The Action Type is Email. For email address I just put my email. Under subject put in... [[object.name]]. This will tell you the name of the car. Message you can put any text message you like. Now save the Server Action.</li>
|
||||
<li>Step 3. Now go back to the Automation Rule you created and go to the Rule tab next to the conditions tab. Click Add and add the server action you created . In this case Fleet Contract Expiration Warning. Then Save.</li>
|
||||
<li>Step 4. To test, set a contract to expire tomorrow under one of your fleets vehicles. Then Save it.</li>
|
||||
<li>Step 5. Go to Scheduled Actions.. Set interval number to 1. Interval Unit to Minutes. Then Set the Next Execution date to 2 minutes from now. If your SMTP is configured correctly you will start to get a mail every minute with the reminder.</li></ul>]]></field>
|
||||
<field name="parent_id" ref="question_0" />
|
||||
</record>
|
||||
<record id="answer_1" model="forum.post">
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<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 provides a web module and an e-commerce module: www.odoo.com/app/website
|
||||
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.
|
||||
</p>]]></field>
|
||||
<field name="parent_id" ref="question_1"/>
|
||||
</record>
|
||||
|
||||
<!-- Post Vote -->
|
||||
<record id="post_vote_0" model="forum.post.vote">
|
||||
<field name="post_id" ref="question_0"/>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="vote">1</field>
|
||||
</record>
|
||||
<record id="post_vote_1" model="forum.post.vote">
|
||||
<field name="post_id" ref="answer_0"/>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="vote">1</field>
|
||||
</record>
|
||||
|
||||
<!-- Run Scheduler -->
|
||||
<function model="gamification.challenge" name="_cron_update">
|
||||
<value eval="False"/>
|
||||
<value eval="False"/>
|
||||
</function>
|
||||
|
||||
</data>
|
||||
</odoo>
|
59
data/forum_post_reason_data.xml
Normal file
59
data/forum_post_reason_data.xml
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Reasons for closing Post -->
|
||||
<record id="reason_1" model="forum.post.reason">
|
||||
<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="reason_type">basic</field>
|
||||
</record>
|
||||
<record id="reason_3" model="forum.post.reason">
|
||||
<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="reason_type">basic</field>
|
||||
</record>
|
||||
<record id="reason_6" model="forum.post.reason">
|
||||
<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="reason_type">basic</field>
|
||||
</record>
|
||||
<record id="reason_8" model="forum.post.reason">
|
||||
<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="reason_type">basic</field>
|
||||
</record>
|
||||
<record id="reason_11" model="forum.post.reason">
|
||||
<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="reason_type">offensive</field>
|
||||
</record>
|
||||
<record id="reason_13" model="forum.post.reason">
|
||||
<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="reason_type">offensive</field>
|
||||
</record>
|
||||
<record id="reason_15" model="forum.post.reason">
|
||||
<field name="name">Racist and hate speech</field>
|
||||
<field name="reason_type">offensive</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
20
data/forum_tag_demo.xml
Normal file
20
data/forum_tag_demo.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data>
|
||||
<!-- Tag -->
|
||||
<record id="tags_0" model="forum.tag">
|
||||
<field name="name">Contract</field>
|
||||
<field name="forum_id" ref="website_forum.forum_help"/>
|
||||
</record>
|
||||
<record id="tags_1" model="forum.tag">
|
||||
<field name="name">Action</field>
|
||||
<field name="forum_id" ref="website_forum.forum_help"/>
|
||||
</record>
|
||||
<record id="tags_2" model="forum.tag">
|
||||
<field name="name">ecommerce</field>
|
||||
<field name="forum_id" ref="website_forum.forum_help"/>
|
||||
</record>
|
||||
<record id="tags_3" model="forum.tag">
|
||||
<field name="name">Development</field>
|
||||
<field name="forum_id" ref="website_forum.forum_help"/>
|
||||
</record>
|
||||
</data></odoo>
|
256
data/gamification_badge_data_answer.xml
Normal file
256
data/gamification_badge_data_answer.xml
Normal file
@ -0,0 +1,256 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- QUALITY (VOTES) -->
|
||||
<!-- Teacher: at least 3 upvotes -->
|
||||
<record id="badge_a_1" model="gamification.badge">
|
||||
<field name="name">Teacher</field>
|
||||
<field name="description">Received at least 3 upvote for an answer for the first time</field>
|
||||
<field name="level">bronze</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_teacher">
|
||||
<field name="name">Teacher</field>
|
||||
<field name="description">Received at least 3 upvote for an answer for the first time</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[('parent_id', '!=', False), ('vote_count', '>=', 3)]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_teacher">
|
||||
<field name="name">Teacher</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_a_1"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_teacher">
|
||||
<field name="definition_id" ref="definition_teacher"/>
|
||||
<field name="challenge_id" ref="challenge_teacher"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
<!-- Nice: at least 4 upvotes -->
|
||||
<record id="badge_a_2" model="gamification.badge">
|
||||
<field name="name">Nice Answer</field>
|
||||
<field name="description">Answer voted up 4 times</field>
|
||||
<field name="level">bronze</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_nice_answer">
|
||||
<field name="name">Nice Answer (4)</field>
|
||||
<field name="description">Answer voted up 4 times</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[('parent_id', '!=', False), ('vote_count', '>=', 4)]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_nice_answer">
|
||||
<field name="name">Nice Answer</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_a_2"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_nice_answer">
|
||||
<field name="definition_id" ref="definition_nice_answer"/>
|
||||
<field name="challenge_id" ref="challenge_nice_answer"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
<!-- Good: at least 6 upvotes -->
|
||||
<record id="badge_a_3" model="gamification.badge">
|
||||
<field name="name">Good Answer</field>
|
||||
<field name="description">Answer voted up 6 times</field>
|
||||
<field name="level">silver</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_good_answer">
|
||||
<field name="name">Good Answer (6)</field>
|
||||
<field name="description">Answer voted up 6 times</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[('parent_id', '!=', False), ('vote_count', '>=', 6)]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_good_answer">
|
||||
<field name="name">Good Answer</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_a_3"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_good_answer">
|
||||
<field name="definition_id" ref="definition_good_answer"/>
|
||||
<field name="challenge_id" ref="challenge_good_answer"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
<!-- Great: at least 15 upvotes -->
|
||||
<record id="badge_a_4" model="gamification.badge">
|
||||
<field name="name">Great Answer</field>
|
||||
<field name="description">Answer voted up 15 times</field>
|
||||
<field name="level">gold</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_great_answer">
|
||||
<field name="name">Great Answer (15)</field>
|
||||
<field name="description">Answer voted up 15 times</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[('parent_id', '!=', False), ('vote_count', '>=', 15)]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_great_answer">
|
||||
<field name="name">Great Answer</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_a_4"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_great_answer">
|
||||
<field name="definition_id" ref="definition_great_answer"/>
|
||||
<field name="challenge_id" ref="challenge_great_answer"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
|
||||
<!-- ACCEPTANCE -->
|
||||
<!-- Enlightened: at least 3 upvotes for an accepted answer -->
|
||||
<record id="badge_a_5" model="gamification.badge">
|
||||
<field name="name">Enlightened</field>
|
||||
<field name="description">Answer was accepted with 3 or more votes</field>
|
||||
<field name="level">silver</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_enlightened">
|
||||
<field name="name">Enlightened</field>
|
||||
<field name="description">Answer was accepted with 3 or more votes</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[('parent_id', '!=', False), ('vote_count', '>=', 3), ('is_correct', '=', True)]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_enlightened">
|
||||
<field name="name">Enlightened</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_a_5"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_enlightened">
|
||||
<field name="definition_id" ref="definition_enlightened"/>
|
||||
<field name="challenge_id" ref="challenge_enlightened"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
<!-- Guru: at least 15 upvotes for an accepted answer -->
|
||||
<record id="badge_a_6" model="gamification.badge">
|
||||
<field name="name">Guru</field>
|
||||
<field name="description">Answer accepted with 15 or more votes</field>
|
||||
<field name="level">silver</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_guru">
|
||||
<field name="name">Guru (15)</field>
|
||||
<field name="description">Answer accepted with 15 or more votes</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[('parent_id', '!=', False), ('vote_count', '>=', 15), ('is_correct', '=', True)]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_guru">
|
||||
<field name="name">Guru</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_a_6"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_guru">
|
||||
<field name="definition_id" ref="definition_guru"/>
|
||||
<field name="target_goal">1</field>
|
||||
<field name="challenge_id" ref="challenge_guru"/>
|
||||
</record>
|
||||
|
||||
<!-- Sealf Leaner: own question, 3+ upvotes -->
|
||||
<record id="badge_a_8" model="gamification.badge">
|
||||
<field name="name">Self-Learner</field>
|
||||
<field name="description">Answered own question with at least 4 up votes</field>
|
||||
<field name="level">gold</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_self_learner">
|
||||
<field name="name">Self-Learner</field>
|
||||
<field name="description">Answer own question with at least 4 up votes</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[('self_reply', '=', True), ('vote_count', '>=', 4)]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_self_learner">
|
||||
<field name="name">Self-Learner</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_a_8"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_self_learner">
|
||||
<field name="definition_id" ref="definition_self_learner"/>
|
||||
<field name="target_goal">1</field>
|
||||
<field name="challenge_id" ref="challenge_self_learner"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
193
data/gamification_badge_data_moderation.xml
Normal file
193
data/gamification_badge_data_moderation.xml
Normal file
@ -0,0 +1,193 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Cleanup: answer or question edition -->
|
||||
<!-- Not rollback feature in forum -->
|
||||
<!-- <record id="badge_3" model="gamification.badge">
|
||||
<field name="name">Cleanup</field>
|
||||
<field name="description">First rollback</field>
|
||||
<field name="level">gold</field>
|
||||
</record> -->
|
||||
|
||||
<!-- Critic: downvote based -->
|
||||
<record id="badge_5" model="gamification.badge">
|
||||
<field name="name">Critic</field>
|
||||
<field name="description">First downvote</field>
|
||||
<field name="level">bronze</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_critic">
|
||||
<field name="name">Critic</field>
|
||||
<field name="description">First downvote</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post_vote"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[('vote', '=', '-1')]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post_vote__user_id"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_critic">
|
||||
<field name="name">Critic</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_5"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_critic">
|
||||
<field name="definition_id" ref="definition_critic"/>
|
||||
<field name="challenge_id" ref="challenge_critic"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
|
||||
<!-- Disciplined: delete own post with >=3 upvotes -->
|
||||
<record id="badge_6" model="gamification.badge">
|
||||
<field name="name">Disciplined</field>
|
||||
<field name="description">Deleted own post with 3 or more upvotes</field>
|
||||
<field name="level">bronze</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_disciplined">
|
||||
<field name="name">Disciplined</field>
|
||||
<field name="description">Delete own post with 3 or more upvotes</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[('vote_count', '>=', 3), ('active', '=', False)]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_disciplined">
|
||||
<field name="name">Disciplined</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_6"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_disciplined">
|
||||
<field name="definition_id" ref="definition_disciplined"/>
|
||||
<field name="challenge_id" ref="challenge_disciplined"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
|
||||
<!-- Editor: first edit -->
|
||||
<record id="badge_7" model="gamification.badge">
|
||||
<field name="name">Editor</field>
|
||||
<field name="description">First edit</field>
|
||||
<field name="level">gold</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_editor">
|
||||
<field name="name">Editor</field>
|
||||
<field name="description">First edit of answer or question</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="mail.model_mail_message"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain" eval="[('model', '=', 'forum.post'), ('subtype_id', 'in', [ref('website_forum.mt_answer_edit'), ref('website_forum.mt_question_edit')])]"/>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="mail.field_mail_message__author_id"/>
|
||||
<field name="batch_user_expression">user.partner_id.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_editor">
|
||||
<field name="name">Editor</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_7"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_editor">
|
||||
<field name="definition_id" ref="definition_editor"/>
|
||||
<field name="challenge_id" ref="challenge_editor"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
|
||||
<record id="badge_31" model="gamification.badge">
|
||||
<field name="name">Supporter</field>
|
||||
<field name="description">First upvote</field>
|
||||
<field name="level">gold</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_supporter">
|
||||
<field name="name">Supporter</field>
|
||||
<field name="description">First upvote</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post_vote"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[('vote', '=', '1')]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post_vote__user_id"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_supporter">
|
||||
<field name="name">Supporter</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_31"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_supporter">
|
||||
<field name="definition_id" ref="definition_supporter"/>
|
||||
<field name="target_goal">1</field>
|
||||
<field name="challenge_id" ref="challenge_supporter"/>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="badge_23" model="gamification.badge">
|
||||
<field name="name">Peer Pressure</field>
|
||||
<field name="description">Deleted own post with 3 or more downvotes</field>
|
||||
<field name="level">gold</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_peer_pressure">
|
||||
<field name="name">Peer Pressure</field>
|
||||
<field name="description">Delete own post with 3 or more down votes</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[('vote_count', '<=', -3), ('active', '=', False)]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_peer_pressure">
|
||||
<field name="name">Peer Pressure</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_23"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_peer_pressure">
|
||||
<field name="definition_id" ref="definition_peer_pressure"/>
|
||||
<field name="target_goal">1</field>
|
||||
<field name="challenge_id" ref="challenge_peer_pressure"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
176
data/gamification_badge_data_participation.xml
Normal file
176
data/gamification_badge_data_participation.xml
Normal file
@ -0,0 +1,176 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Biography: complet your profile -->
|
||||
<record id="badge_p_1" model="gamification.badge">
|
||||
<field name="name">Autobiographer</field>
|
||||
<field name="description">Completed own biography</field>
|
||||
<field name="level">bronze</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_configure_profile">
|
||||
<field name="name">Completed own biography</field>
|
||||
<field name="description">Write some information about yourself</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[
|
||||
('partner_id.country_id', '!=', False),
|
||||
('partner_id.city', '!=', False),
|
||||
('partner_id.email', '!=', False)
|
||||
]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="base.field_res_users__id"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_configure_profile">
|
||||
<field name="name">Complete own biography</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_p_1"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_configure_profile">
|
||||
<field name="definition_id" ref="definition_configure_profile"/>
|
||||
<field name="challenge_id" ref="challenge_configure_profile"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
|
||||
<!-- Commentator: at least 10 comments posted on posts -->
|
||||
<record id="badge_p_2" model="gamification.badge">
|
||||
<field name="name">Commentator</field>
|
||||
<field name="description">Posted 10 comments</field>
|
||||
<field name="level">bronze</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_commentator">
|
||||
<field name="name">Commentator</field>
|
||||
<field name="description">Comment an answer or a question</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="mail.model_mail_message"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain" eval="[('message_type', '=', 'comment'), ('subtype_id', '=', ref('mail.mt_comment')), ('model', '=', 'forum.post')]"/>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="mail.field_mail_message__author_id"/>
|
||||
<field name="batch_user_expression">user.partner_id.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_commentator">
|
||||
<field name="name">Commentator</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_p_2"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_commentator">
|
||||
<field name="definition_id" ref="definition_commentator"/>
|
||||
<field name="challenge_id" ref="challenge_commentator"/>
|
||||
<field name="target_goal">10</field>
|
||||
</record>
|
||||
|
||||
<!-- Pundit: 10 answers with at least score of 10 -->
|
||||
<record id="badge_25" model="gamification.badge">
|
||||
<field name="name">Pundit</field>
|
||||
<field name="description">Left 10 answers with score of 10 or more</field>
|
||||
<field name="level">silver</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_pundit">
|
||||
<field name="name">Pundit</field>
|
||||
<field name="description">Post 10 answers with score of 10 or more</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="condition">higher</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="domain" eval="[('parent', '!=', False), ('vote_count' '>=', 10)]"/>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_pundit">
|
||||
<field name="name">Pundit</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_25"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_pundit">
|
||||
<field name="definition_id" ref="definition_pundit"/>
|
||||
<field name="target_goal">10</field>
|
||||
<field name="challenge_id" ref="challenge_pundit"/>
|
||||
</record>
|
||||
|
||||
<!-- Chief Commentator: 100 comments -->
|
||||
<record id="badge_p_4" model="gamification.badge">
|
||||
<field name="name">Chief Commentator</field>
|
||||
<field name="description">Posted 100 comments</field>
|
||||
<field name="level">silver</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_chief_commentator">
|
||||
<field name="name">Chief Commentator</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_p_4"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_chief_commentator">
|
||||
<field name="definition_id" ref="definition_commentator"/>
|
||||
<field name="challenge_id" ref="challenge_chief_commentator"/>
|
||||
<field name="target_goal">100</field>
|
||||
</record>
|
||||
|
||||
<record id="badge_32" model="gamification.badge">
|
||||
<field name="name">Taxonomist</field>
|
||||
<field name="description">Created a tag used by 15 questions</field>
|
||||
<field name="level">silver</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_taxonomist">
|
||||
<field name="name">Taxonomist</field>
|
||||
<field name="description">Create a tag which can used in minimum 15 questions</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_tag"/>
|
||||
<field name="condition">higher</field>
|
||||
<field name="domain">[('posts_count', '>=', 15)]</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_tag__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_taxonomist">
|
||||
<field name="name">Taxonomist</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_32"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_taxonomist">
|
||||
<field name="definition_id" ref="definition_taxonomist"/>
|
||||
<field name="challenge_id" ref="challenge_taxonomist"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
399
data/gamification_badge_data_question.xml
Normal file
399
data/gamification_badge_data_question.xml
Normal file
@ -0,0 +1,399 @@
|
||||
<!-- <?xml version="1.0" encoding="utf-8"?> -->
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- POPULARITY (VIEWS) -->
|
||||
<!-- Popular: 150 views -->
|
||||
<record id="badge_q_1" model="gamification.badge">
|
||||
<field name="name">Popular Question</field>
|
||||
<field name="description">Asked a question with at least 150 views</field>
|
||||
<field name="level">bronze</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_popular_question">
|
||||
<field name="name">Popular Question (150)</field>
|
||||
<field name="description">Asked a question with at least 150 views</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="domain">[('parent_id', '=', False), ('views', '>=', 150)]</field>
|
||||
<field name="condition">higher</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_popular_question">
|
||||
<field name="name">Popular Question</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_q_1"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_popular_question">
|
||||
<field name="definition_id" ref="definition_popular_question"/>
|
||||
<field name="challenge_id" ref="challenge_popular_question"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
|
||||
<!-- Notable: 250 views -->
|
||||
<record id="badge_q_2" model="gamification.badge">
|
||||
<field name="name">Notable Question</field>
|
||||
<field name="description">Asked a question with at least 250 views</field>
|
||||
<field name="level">silver</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_notable_question">
|
||||
<field name="name">Popular Question (250)</field>
|
||||
<field name="description">Asked a question with at least 250 views</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="domain">[('parent_id', '=', False), ('views', '>=', 250)]</field>
|
||||
<field name="condition">higher</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_notable_question">
|
||||
<field name="name">Notable Question</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_q_2"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_notable_question">
|
||||
<field name="definition_id" ref="definition_notable_question"/>
|
||||
<field name="challenge_id" ref="challenge_notable_question"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
<!-- Famous: 500 views -->
|
||||
<record id="badge_q_3" model="gamification.badge">
|
||||
<field name="name">Famous Question</field>
|
||||
<field name="description">Asked a question with at least 500 views</field>
|
||||
<field name="level">gold</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_famous_question">
|
||||
<field name="name">Popular Question (500)</field>
|
||||
<field name="description">Asked a question with at least 500 views</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="domain">[('parent_id', '=', False), ('views', '>=', 500)]</field>
|
||||
<field name="condition">higher</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_famous_question">
|
||||
<field name="name">Famous Question</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_q_3"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_famous_question">
|
||||
<field name="definition_id" ref="definition_famous_question"/>
|
||||
<field name="challenge_id" ref="challenge_famous_question"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
|
||||
<!-- FAVORITE -->
|
||||
<!-- Credible: at least 1 user have it in favorite -->
|
||||
<record id="badge_q_4" model="gamification.badge">
|
||||
<field name="name">Credible Question</field>
|
||||
<field name="description">Question set as favorite by 1 user</field>
|
||||
<field name="level">bronze</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_favorite_question_1">
|
||||
<field name="name">Favourite Question (1)</field>
|
||||
<field name="description">Question set as favorite by 1 user</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="domain">[('parent_id', '=', False), ('favourite_count', '>=', 1)]</field>
|
||||
<field name="condition">higher</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_favorite_question_1">
|
||||
<field name="name">Credible Question</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_q_4"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_favorite_question_1">
|
||||
<field name="definition_id" ref="definition_favorite_question_1"/>
|
||||
<field name="challenge_id" ref="challenge_favorite_question_1"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
<!-- Favorite: at least 5 users have it in favorite -->
|
||||
<record id="badge_q_5" model="gamification.badge">
|
||||
<field name="name">Favorite Question</field>
|
||||
<field name="description">Question set as favorite by 5 users</field>
|
||||
<field name="level">silver</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_favorite_question_5">
|
||||
<field name="name">Favourite Question (5)</field>
|
||||
<field name="description">Question set as favorite by 5 user</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="domain">[('parent_id', '=', False), ('favourite_count', '>=', 5)]</field>
|
||||
<field name="condition">higher</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_favorite_question_5">
|
||||
<field name="name">Favorite Question</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_q_5"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_favorite_question_5">
|
||||
<field name="definition_id" ref="definition_favorite_question_5"/>
|
||||
<field name="challenge_id" ref="challenge_favorite_question_5"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
<!-- Stellar: at least 25 users have it in favorite -->
|
||||
<record id="badge_q_6" model="gamification.badge">
|
||||
<field name="name">Stellar Question</field>
|
||||
<field name="description">Question set as favorite by 25 users</field>
|
||||
<field name="level">bronze</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_stellar_question_25">
|
||||
<field name="name">Favourite Question (25)</field>
|
||||
<field name="description">Question set as favorite by 25 user</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="domain">[('parent_id', '=', False), ('favourite_count', '>=', 25)]</field>
|
||||
<field name="condition">higher</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_stellar_question_25">
|
||||
<field name="name">Stellar Question</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_q_6"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_stellar_question_25">
|
||||
<field name="definition_id" ref="definition_stellar_question_25"/>
|
||||
<field name="challenge_id" ref="challenge_stellar_question_25"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
|
||||
<!-- QUALITY (VOTES) -->
|
||||
<!-- Student: at least 1 upvote -->
|
||||
<record id="badge_q_7" model="gamification.badge">
|
||||
<field name="name">Student</field>
|
||||
<field name="description">Asked first question with at least one up vote</field>
|
||||
<field name="level">gold</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_student">
|
||||
<field name="name">Upvoted question (1)</field>
|
||||
<field name="description">Asked first question with at least one up vote</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="domain">[('parent_id', '=', False), ('vote_count', '>=', 1)]</field>
|
||||
<field name="condition">higher</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_student">
|
||||
<field name="name">Student</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_q_7"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_student">
|
||||
<field name="definition_id" ref="definition_student"/>
|
||||
<field name="challenge_id" ref="challenge_student"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
<!-- Nice: at least 4 upvotes -->
|
||||
<record id="badge_q_8" model="gamification.badge">
|
||||
<field name="name">Nice Question</field>
|
||||
<field name="description">Question voted up 4 times</field>
|
||||
<field name="level">bronze</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_nice_question">
|
||||
<field name="name">Upvoted question (4)</field>
|
||||
<field name="description">Asked first question with at least 4 up votes</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="domain">[('parent_id', '=', False), ('vote_count', '>=', 4)]</field>
|
||||
<field name="condition">higher</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_nice_question">
|
||||
<field name="name">Nice Question</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_q_8"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_nice_question">
|
||||
<field name="definition_id" ref="definition_nice_question"/>
|
||||
<field name="challenge_id" ref="challenge_nice_question"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
<!-- Good: at least 6 upvotes -->
|
||||
<record id="badge_q_9" model="gamification.badge">
|
||||
<field name="name">Good Question</field>
|
||||
<field name="description">Question voted up 6 times</field>
|
||||
<field name="level">silver</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_good_question">
|
||||
<field name="name">Upvoted question (6)</field>
|
||||
<field name="description">Asked first question with at least 6 up votes</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="domain">[('parent_id', '=', False), ('vote_count', '>=', 6)]</field>
|
||||
<field name="condition">higher</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_good_question">
|
||||
<field name="name">Good Question</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_q_9"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_good_question">
|
||||
<field name="definition_id" ref="definition_good_question"/>
|
||||
<field name="challenge_id" ref="challenge_good_question"/>
|
||||
<field name="target_goal">1</field>
|
||||
</record>
|
||||
<!-- Great: at least 15 upvotes -->
|
||||
<record id="badge_q_10" model="gamification.badge">
|
||||
<field name="name">Great Question</field>
|
||||
<field name="description">Question voted up 15 times</field>
|
||||
<field name="level">gold</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_great_question">
|
||||
<field name="name">Upvoted question (15)</field>
|
||||
<field name="description">Asked first question with at least 15 up votes</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="domain">[('parent_id', '=', False), ('vote_count', '>=', 15)]</field>
|
||||
<field name="condition">higher</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_great_question">
|
||||
<field name="name">Great Question</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_q_10"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_great_question">
|
||||
<field name="definition_id" ref="definition_great_question"/>
|
||||
<field name="target_goal">1</field>
|
||||
<field name="challenge_id" ref="challenge_great_question"/>
|
||||
</record>
|
||||
|
||||
<!-- Question + Answer -->
|
||||
<record id="badge_26" model="gamification.badge">
|
||||
<field name="name">Scholar</field>
|
||||
<field name="description">Asked a question and accepted an answer</field>
|
||||
<field name="level">gold</field>
|
||||
<field name="rule_auth">nobody</field>
|
||||
</record>
|
||||
<record model="gamification.goal.definition" id="definition_scholar">
|
||||
<field name="name">Scholar</field>
|
||||
<field name="description">Ask a question and accept an answer</field>
|
||||
<field name="computation_mode">count</field>
|
||||
<field name="display_mode">boolean</field>
|
||||
<field name="model_id" ref="website_forum.model_forum_post"/>
|
||||
<field name="domain">[('parent_id', '=', False), ('has_validated_answer', '=', True)]</field>
|
||||
<field name="condition">higher</field>
|
||||
<field name="batch_mode">True</field>
|
||||
<field name="batch_distinctive_field" ref="website_forum.field_forum_post__create_uid"/>
|
||||
<field name="batch_user_expression">user.id</field>
|
||||
</record>
|
||||
<record model="gamification.challenge" id="challenge_scholar">
|
||||
<field name="name">Scholar</field>
|
||||
<field name="period">once</field>
|
||||
<field name="visibility_mode">personal</field>
|
||||
<field name="report_message_frequency">never</field>
|
||||
<field name="reward_id" ref="badge_26"/>
|
||||
<field name="reward_realtime">True</field>
|
||||
<field name="user_domain">[('karma', '>', 0)]</field>
|
||||
<field name="state">inprogress</field>
|
||||
<field name="challenge_category">forum</field>
|
||||
</record>
|
||||
<record model="gamification.challenge.line" id="line_scholar">
|
||||
<field name="definition_id" ref="definition_scholar"/>
|
||||
<field name="target_goal">1</field>
|
||||
<field name="challenge_id" ref="challenge_scholar"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
14
data/ir_actions_data.xml
Normal file
14
data/ir_actions_data.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
<!-- JUMP TO FORUM AT INSTALL -->
|
||||
<record id="action_open_forum" model="ir.actions.act_url">
|
||||
<field name="name">Forum</field>
|
||||
<field name="target">self</field>
|
||||
<field name="url" eval="'/forum/'+str(ref('website_forum.forum_help'))"/>
|
||||
</record>
|
||||
|
||||
<record id="base.open_menu" model="ir.actions.todo">
|
||||
<field name="action_id" ref="action_open_forum"/>
|
||||
<field name="state">open</field>
|
||||
</record>
|
||||
</data></odoo>
|
4
data/ir_config_parameter_data.xml
Normal file
4
data/ir_config_parameter_data.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
<function model="ir.config_parameter" name="set_param" eval="('auth_signup.invitation_scope', 'b2c')"/>
|
||||
</data></odoo>
|
49
data/mail_message_subtype_data.xml
Normal file
49
data/mail_message_subtype_data.xml
Normal file
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Answers subtypes -->
|
||||
<record id="mt_answer_new" model="mail.message.subtype">
|
||||
<field name="name">New Answer</field>
|
||||
<field name="res_model">forum.post</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="hidden" eval="False"/>
|
||||
<field name="description">New Answer</field>
|
||||
</record>
|
||||
<record id="mt_answer_edit" model="mail.message.subtype">
|
||||
<field name="name">Answer Edited</field>
|
||||
<field name="res_model">forum.post</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Answer Edited</field>
|
||||
</record>
|
||||
<!-- Questions subtypes -->
|
||||
<record id="mt_question_new" model="mail.message.subtype">
|
||||
<field name="name">New Question</field>
|
||||
<field name="res_model">forum.post</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="description">New Question</field>
|
||||
</record>
|
||||
<record id="mt_question_edit" model="mail.message.subtype">
|
||||
<field name="name">Question Edited</field>
|
||||
<field name="res_model">forum.post</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Question Edited</field>
|
||||
</record>
|
||||
<!-- Forum subtypes, to follow all answers or questions -->
|
||||
<record id="mt_forum_answer_new" model="mail.message.subtype">
|
||||
<field name="name">New Answer</field>
|
||||
<field name="res_model">forum.forum</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="hidden" eval="False"/>
|
||||
<field name="parent_id" ref="mt_answer_new"/>
|
||||
<field name="relation_field">forum_id</field>
|
||||
</record>
|
||||
<record id="mt_forum_question_new" model="mail.message.subtype">
|
||||
<field name="name">New Question</field>
|
||||
<field name="res_model">forum.forum</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="hidden" eval="False"/>
|
||||
<field name="parent_id" ref="mt_question_new"/>
|
||||
<field name="relation_field">forum_id</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
32
data/mail_templates.xml
Normal file
32
data/mail_templates.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
<template id="forum_post_template_new_answer">
|
||||
<p>A new answer on <t t-esc="object.name"/> has been posted. Click here to access the post :</p>
|
||||
<p style="margin-left: 30px; margin-top: 10 px; margin-bottom: 10px;">
|
||||
<a t-attf-href="/forum/#{slug(object.forum_id)}/#{slug(object)}"
|
||||
style="padding: 5px 10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: 0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer; background-color: #875A7B; border: 1px solid #875A7B; border-radius:3px">
|
||||
See post
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template id="forum_post_template_new_question">
|
||||
<p>A new question <b t-esc="object.name" /> on <t t-esc="object.forum_id.name"/> has been posted. Click here to access the question :</p>
|
||||
<p style="margin-left: 30px; margin-top: 10 px; margin-bottom: 10px;">
|
||||
<a t-attf-href="/forum/#{slug(object.forum_id)}/#{slug(object)}"
|
||||
style="padding: 5px 10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: 0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer;background-color: #875A7B; border: 1px solid #875A7B; border-radius:3px">
|
||||
See question
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template id="forum_post_template_validation">
|
||||
<p>A new question <b t-esc="object.name" /> on <t t-esc="object.forum_id.name"/> has been posted and require your validation. Click here to access the question :</p>
|
||||
<p style="margin-left: 30px; margin-top: 10 px; margin-bottom: 10px;">
|
||||
<a t-attf-href="/forum/#{slug(object.forum_id)}/#{slug(object)}"
|
||||
style="padding: 5px 10px; font-size: 12px; line-height: 18px; color: #FFFFFF; border-color:#875A7B; text-decoration: none; display: inline-block; margin-bottom: 0px; font-weight: 400; text-align: center; vertical-align: middle; cursor: pointer;background-color: #875A7B; border: 1px solid #875A7B; border-radius:3px">
|
||||
Validate question
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
</data></odoo>
|
9
data/website_menu_data.xml
Normal file
9
data/website_menu_data.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
<record id="menu_website_forums" model="website.menu">
|
||||
<field name="name">Forum</field>
|
||||
<field name="url">/forum</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">35</field>
|
||||
</record>
|
||||
</data></odoo>
|
3983
i18n/af.po
Normal file
3983
i18n/af.po
Normal file
File diff suppressed because it is too large
Load Diff
4222
i18n/ar.po
Normal file
4222
i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
3984
i18n/az.po
Normal file
3984
i18n/az.po
Normal file
File diff suppressed because it is too large
Load Diff
4141
i18n/bg.po
Normal file
4141
i18n/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
3985
i18n/bs.po
Normal file
3985
i18n/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
4228
i18n/ca.po
Normal file
4228
i18n/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
4187
i18n/cs.po
Normal file
4187
i18n/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
4189
i18n/da.po
Normal file
4189
i18n/da.po
Normal file
File diff suppressed because it is too large
Load Diff
4286
i18n/de.po
Normal file
4286
i18n/de.po
Normal file
File diff suppressed because it is too large
Load Diff
3989
i18n/el.po
Normal file
3989
i18n/el.po
Normal file
File diff suppressed because it is too large
Load Diff
3982
i18n/en_GB.po
Normal file
3982
i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load Diff
4263
i18n/es.po
Normal file
4263
i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
4264
i18n/es_419.po
Normal file
4264
i18n/es_419.po
Normal file
File diff suppressed because it is too large
Load Diff
3992
i18n/es_CO.po
Normal file
3992
i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load Diff
3981
i18n/es_DO.po
Normal file
3981
i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load Diff
3997
i18n/es_EC.po
Normal file
3997
i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load Diff
3982
i18n/es_PE.po
Normal file
3982
i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load Diff
4199
i18n/et.po
Normal file
4199
i18n/et.po
Normal file
File diff suppressed because it is too large
Load Diff
4195
i18n/fa.po
Normal file
4195
i18n/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
4233
i18n/fi.po
Normal file
4233
i18n/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
4266
i18n/fr.po
Normal file
4266
i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
3987
i18n/gu.po
Normal file
3987
i18n/gu.po
Normal file
File diff suppressed because it is too large
Load Diff
4146
i18n/he.po
Normal file
4146
i18n/he.po
Normal file
File diff suppressed because it is too large
Load Diff
4003
i18n/hr.po
Normal file
4003
i18n/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
4147
i18n/hu.po
Normal file
4147
i18n/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
4248
i18n/id.po
Normal file
4248
i18n/id.po
Normal file
File diff suppressed because it is too large
Load Diff
3979
i18n/is.po
Normal file
3979
i18n/is.po
Normal file
File diff suppressed because it is too large
Load Diff
4252
i18n/it.po
Normal file
4252
i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
4164
i18n/ja.po
Normal file
4164
i18n/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
3981
i18n/ka.po
Normal file
3981
i18n/ka.po
Normal file
File diff suppressed because it is too large
Load Diff
3981
i18n/kab.po
Normal file
3981
i18n/kab.po
Normal file
File diff suppressed because it is too large
Load Diff
3986
i18n/km.po
Normal file
3986
i18n/km.po
Normal file
File diff suppressed because it is too large
Load Diff
4170
i18n/ko.po
Normal file
4170
i18n/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
3979
i18n/lb.po
Normal file
3979
i18n/lb.po
Normal file
File diff suppressed because it is too large
Load Diff
4144
i18n/lt.po
Normal file
4144
i18n/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
4093
i18n/lv.po
Normal file
4093
i18n/lv.po
Normal file
File diff suppressed because it is too large
Load Diff
3991
i18n/mk.po
Normal file
3991
i18n/mk.po
Normal file
File diff suppressed because it is too large
Load Diff
4019
i18n/mn.po
Normal file
4019
i18n/mn.po
Normal file
File diff suppressed because it is too large
Load Diff
3995
i18n/nb.po
Normal file
3995
i18n/nb.po
Normal file
File diff suppressed because it is too large
Load Diff
4255
i18n/nl.po
Normal file
4255
i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
4205
i18n/pl.po
Normal file
4205
i18n/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
4090
i18n/pt.po
Normal file
4090
i18n/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
4260
i18n/pt_BR.po
Normal file
4260
i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load Diff
4026
i18n/ro.po
Normal file
4026
i18n/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
4264
i18n/ru.po
Normal file
4264
i18n/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
4123
i18n/sk.po
Normal file
4123
i18n/sk.po
Normal file
File diff suppressed because it is too large
Load Diff
4096
i18n/sl.po
Normal file
4096
i18n/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
4200
i18n/sr.po
Normal file
4200
i18n/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
3985
i18n/sr@latin.po
Normal file
3985
i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load Diff
4107
i18n/sv.po
Normal file
4107
i18n/sv.po
Normal file
File diff suppressed because it is too large
Load Diff
4225
i18n/th.po
Normal file
4225
i18n/th.po
Normal file
File diff suppressed because it is too large
Load Diff
4199
i18n/tr.po
Normal file
4199
i18n/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
4194
i18n/uk.po
Normal file
4194
i18n/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
4188
i18n/vi.po
Normal file
4188
i18n/vi.po
Normal file
File diff suppressed because it is too large
Load Diff
4082
i18n/website_forum.pot
Normal file
4082
i18n/website_forum.pot
Normal file
File diff suppressed because it is too large
Load Diff
4155
i18n/zh_CN.po
Normal file
4155
i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
4142
i18n/zh_TW.po
Normal file
4142
i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load Diff
12
models/__init__.py
Normal file
12
models/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import forum_forum
|
||||
from . import forum_post
|
||||
from . import forum_post_reason
|
||||
from . import forum_post_vote
|
||||
from . import forum_tag
|
||||
from . import gamification_challenge
|
||||
from . import gamification_karma_tracking
|
||||
from . import ir_attachment
|
||||
from . import res_users
|
||||
from . import website
|
362
models/forum_forum.py
Normal file
362
models/forum_forum.py
Normal file
@ -0,0 +1,362 @@
|
||||
# -*- 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
|
870
models/forum_post.py
Normal file
870
models/forum_post.py
Normal file
@ -0,0 +1,870 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.addons.http_routing.models.ir_http import slug, unslug
|
||||
from odoo.exceptions import UserError, ValidationError, AccessError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import sql
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Post(models.Model):
|
||||
_name = 'forum.post'
|
||||
_description = 'Forum Post'
|
||||
_inherit = [
|
||||
'mail.thread',
|
||||
'website.seo.metadata',
|
||||
'website.searchable.mixin',
|
||||
]
|
||||
_order = "is_correct DESC, vote_count DESC, last_activity_date DESC"
|
||||
|
||||
name = fields.Char('Title')
|
||||
forum_id = fields.Many2one('forum.forum', string='Forum', required=True)
|
||||
content = fields.Html('Content', strip_style=True)
|
||||
plain_content = fields.Text(
|
||||
'Plain Content',
|
||||
compute='_compute_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', 'Closed'), ('offensive', 'Offensive'),
|
||||
('flagged', 'Flagged'),
|
||||
], string='Status', default='active')
|
||||
views = fields.Integer('Views', default=0, readonly=True, copy=False)
|
||||
active = fields.Boolean('Active', default=True)
|
||||
website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment', 'email_outgoing'])])
|
||||
website_url = fields.Char('Website URL', compute='_compute_website_url')
|
||||
website_id = fields.Many2one(related='forum_id.website_id', readonly=True)
|
||||
|
||||
# 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('Updated on', index=True, readonly=True)
|
||||
last_activity_date = fields.Datetime(
|
||||
'Last activity on', readonly=True, required=True, default=fields.Datetime.now,
|
||||
help="Field to keep track of a post's last activity. Updated whenever it is replied to, "
|
||||
"or when a comment is added on the post or one of its replies."
|
||||
)
|
||||
write_uid = fields.Many2one('res.users', string='Updated by', index=True, readonly=True)
|
||||
relevancy = fields.Float('Relevance', compute="_compute_relevancy", store=True)
|
||||
|
||||
# vote
|
||||
vote_ids = fields.One2many('forum.post.vote', 'post_id', string='Votes')
|
||||
user_vote = fields.Integer('My Vote', compute='_compute_user_vote')
|
||||
vote_count = fields.Integer('Total Votes', compute='_compute_vote_count', store=True)
|
||||
|
||||
# favorite
|
||||
favourite_ids = fields.Many2many('res.users', string='Favourite')
|
||||
user_favourite = fields.Boolean('Is Favourite', compute='_compute_user_favourite')
|
||||
favourite_count = fields.Integer('Favorite', compute='_compute_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', readonly=True, index=True)
|
||||
self_reply = fields.Boolean('Reply to own question', compute='_compute_self_reply', store=True)
|
||||
child_ids = fields.One2many(
|
||||
'forum.post', 'parent_id', string='Post Answers',
|
||||
domain="[('forum_id', '=', forum_id)]")
|
||||
child_count = fields.Integer('Answers', compute='_compute_child_count', store=True)
|
||||
uid_has_answered = fields.Boolean('Has Answered', compute='_compute_uid_has_answered')
|
||||
has_validated_answer = fields.Boolean(
|
||||
'Is answered',
|
||||
compute='_compute_has_validated_answer', store=True)
|
||||
|
||||
# offensive moderation tools
|
||||
flag_user_id = fields.Many2one('res.users', string='Flagged by')
|
||||
moderator_id = fields.Many2one('res.users', string='Reviewed by', readonly=True)
|
||||
|
||||
# closing
|
||||
closed_reason_id = fields.Many2one('forum.post.reason', string='Reason', copy=False)
|
||||
closed_uid = fields.Many2one('res.users', string='Closed by', readonly=True, copy=False)
|
||||
closed_date = fields.Datetime('Closed on', readonly=True, copy=False)
|
||||
|
||||
# karma calculation and access
|
||||
karma_accept = fields.Integer(
|
||||
'Convert comment to answer',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
karma_edit = fields.Integer(
|
||||
'Karma to edit',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
karma_close = fields.Integer(
|
||||
'Karma to close',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
karma_unlink = fields.Integer(
|
||||
'Karma to unlink',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
karma_comment = fields.Integer(
|
||||
'Karma to comment',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
karma_comment_convert = fields.Integer(
|
||||
'Karma to convert comment to answer',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
karma_flag = fields.Integer(
|
||||
'Flag a post as offensive',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_ask = fields.Boolean(
|
||||
'Can Ask',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_answer = fields.Boolean(
|
||||
'Can Answer',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_accept = fields.Boolean(
|
||||
'Can Accept',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_edit = fields.Boolean(
|
||||
'Can Edit',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_close = fields.Boolean(
|
||||
'Can Close',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_unlink = fields.Boolean(
|
||||
'Can Unlink',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_upvote = fields.Boolean(
|
||||
'Can Upvote',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_downvote = fields.Boolean(
|
||||
'Can Downvote',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_comment = fields.Boolean(
|
||||
'Can Comment',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_comment_convert = fields.Boolean(
|
||||
'Can Convert to Comment',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_view = fields.Boolean(
|
||||
'Can View',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False, search='_search_can_view')
|
||||
can_display_biography = fields.Boolean(
|
||||
"Is the author's biography visible from his post",
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_post = fields.Boolean(
|
||||
'Can Automatically be Validated',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_flag = fields.Boolean(
|
||||
'Can Flag',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_moderate = fields.Boolean(
|
||||
'Can Moderate',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
can_use_full_editor = fields.Boolean( # Editor Features: image and links
|
||||
'Can Use Full Editor',
|
||||
compute='_compute_post_karma_rights', compute_sudo=False)
|
||||
|
||||
@api.constrains('parent_id')
|
||||
def _check_parent_id(self):
|
||||
if not self._check_recursion():
|
||||
raise ValidationError(_('You cannot create recursive forum posts.'))
|
||||
|
||||
@api.depends('content')
|
||||
def _compute_plain_content(self):
|
||||
for post in self:
|
||||
post.plain_content = tools.html2plaintext(post.content)[0:500] if post.content else False
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_website_url(self):
|
||||
self.website_url = False
|
||||
for post in self.filtered(lambda post: post.id):
|
||||
anchor = f'#answer_{post.id}' if post.parent_id else ''
|
||||
post.website_url = f'/forum/{slug(post.forum_id)}/{slug(post)}{anchor}'
|
||||
|
||||
@api.depends('vote_count', 'forum_id.relevancy_post_vote', 'forum_id.relevancy_time_decay')
|
||||
def _compute_relevancy(self):
|
||||
for post in self:
|
||||
if post.create_date:
|
||||
days = (datetime.today() - post.create_date).days
|
||||
post.relevancy = math.copysign(1, post.vote_count) * (abs(post.vote_count - 1) ** post.forum_id.relevancy_post_vote / (days + 2) ** post.forum_id.relevancy_time_decay)
|
||||
else:
|
||||
post.relevancy = 0
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_user_vote(self):
|
||||
votes = self.env['forum.post.vote'].search_read([('post_id', 'in', self._ids), ('user_id', '=', self._uid)], ['vote', 'post_id'])
|
||||
mapped_vote = dict([(v['post_id'][0], v['vote']) for v in votes])
|
||||
for vote in self:
|
||||
vote.user_vote = mapped_vote.get(vote.id, 0)
|
||||
|
||||
@api.depends('vote_ids.vote')
|
||||
def _compute_vote_count(self):
|
||||
read_group_res = self.env['forum.post.vote']._read_group([('post_id', 'in', self._ids)], ['post_id', 'vote'], ['__count'])
|
||||
result = dict.fromkeys(self._ids, 0)
|
||||
for post, vote, count in read_group_res:
|
||||
result[post.id] += count * int(vote)
|
||||
for post in self:
|
||||
post.vote_count = result[post.id]
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_user_favourite(self):
|
||||
for post in self:
|
||||
post.user_favourite = post._uid in post.favourite_ids.ids
|
||||
|
||||
@api.depends('favourite_ids')
|
||||
def _compute_favorite_count(self):
|
||||
for post in self:
|
||||
post.favourite_count = len(post.favourite_ids)
|
||||
|
||||
@api.depends('create_uid', 'parent_id')
|
||||
def _compute_self_reply(self):
|
||||
for post in self:
|
||||
post.self_reply = post.parent_id.create_uid == post.create_uid
|
||||
|
||||
@api.depends('child_ids')
|
||||
def _compute_child_count(self):
|
||||
for post in self:
|
||||
post.child_count = len(post.child_ids)
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_uid_has_answered(self):
|
||||
for post in self:
|
||||
post.uid_has_answered = post._uid in post.child_ids.create_uid.ids
|
||||
|
||||
@api.depends('child_ids.is_correct')
|
||||
def _compute_has_validated_answer(self):
|
||||
for post in self:
|
||||
post.has_validated_answer = any(answer.is_correct for answer in post.child_ids)
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_post_karma_rights(self):
|
||||
user = self.env.user
|
||||
is_admin = self.env.is_admin()
|
||||
# sudoed recordset instead of individual posts so values can be
|
||||
# prefetched in bulk
|
||||
for post, post_sudo in zip(self, self.sudo()):
|
||||
is_creator = post.create_uid == user
|
||||
|
||||
post.karma_accept = post.forum_id.karma_answer_accept_own if post.parent_id.create_uid == user else post.forum_id.karma_answer_accept_all
|
||||
post.karma_edit = post.forum_id.karma_edit_own if is_creator else post.forum_id.karma_edit_all
|
||||
post.karma_close = post.forum_id.karma_close_own if is_creator else post.forum_id.karma_close_all
|
||||
post.karma_unlink = post.forum_id.karma_unlink_own if is_creator else post.forum_id.karma_unlink_all
|
||||
post.karma_comment = post.forum_id.karma_comment_own if is_creator else post.forum_id.karma_comment_all
|
||||
post.karma_comment_convert = post.forum_id.karma_comment_convert_own if is_creator else post.forum_id.karma_comment_convert_all
|
||||
post.karma_flag = post.forum_id.karma_flag
|
||||
|
||||
post.can_ask = is_admin or user.karma >= post.forum_id.karma_ask
|
||||
post.can_answer = is_admin or user.karma >= post.forum_id.karma_answer
|
||||
post.can_accept = is_admin or user.karma >= post.karma_accept
|
||||
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 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 = post.can_close or post_sudo.active and (post_sudo.create_uid.karma > 0 or post_sudo.create_uid == user)
|
||||
post.can_display_biography = is_admin or (post_sudo.create_uid.karma >= post.forum_id.karma_user_bio and post_sudo.create_uid.website_published)
|
||||
post.can_post = is_admin or user.karma >= post.forum_id.karma_post
|
||||
post.can_flag = is_admin or user.karma >= post.forum_id.karma_flag
|
||||
post.can_moderate = is_admin or user.karma >= post.forum_id.karma_moderate
|
||||
post.can_use_full_editor = is_admin or user.karma >= post.forum_id.karma_editor
|
||||
|
||||
def _search_can_view(self, operator, value):
|
||||
if operator not in ('=', '!=', '<>'):
|
||||
raise ValueError('Invalid operator: %s' % (operator,))
|
||||
|
||||
if not value:
|
||||
operator = '!=' if operator == '=' else '='
|
||||
|
||||
user = self.env.user
|
||||
# Won't impact sitemap, search() in converter is forced as public user
|
||||
if self.env.is_admin():
|
||||
return [(1, '=', 1)]
|
||||
|
||||
req = """
|
||||
SELECT p.id
|
||||
FROM forum_post p
|
||||
LEFT JOIN res_users u ON p.create_uid = u.id
|
||||
LEFT JOIN forum_forum f ON p.forum_id = f.id
|
||||
WHERE
|
||||
(p.create_uid = %s and f.karma_close_own <= %s)
|
||||
or (p.create_uid != %s and f.karma_close_all <= %s)
|
||||
or (
|
||||
u.karma > 0
|
||||
and (p.active or p.create_uid = %s)
|
||||
)
|
||||
"""
|
||||
|
||||
op = 'inselect' if operator == '=' else "not inselect"
|
||||
|
||||
# don't use param named because orm will add other param (test_active, ...)
|
||||
return [('id', op, (req, (user.id, user.karma, user.id, user.karma, user.id)))]
|
||||
|
||||
# EXTENDS WEBSITE.SEO.METADATA
|
||||
|
||||
def _default_website_meta(self):
|
||||
res = super(Post, self)._default_website_meta()
|
||||
res['default_opengraph']['og:title'] = res['default_twitter']['twitter:title'] = self.name
|
||||
res['default_opengraph']['og:description'] = res['default_twitter']['twitter:description'] = self.plain_content
|
||||
res['default_opengraph']['og:image'] = res['default_twitter']['twitter:image'] = self.env['website'].image_url(self.create_uid, 'image_1024')
|
||||
res['default_twitter']['twitter:card'] = 'summary'
|
||||
res['default_meta_description'] = self.plain_content
|
||||
return res
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# CRUD
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if 'content' in vals and vals.get('forum_id'):
|
||||
vals['content'] = self._update_content(vals['content'], vals['forum_id'])
|
||||
|
||||
posts = super(Post, self.with_context(mail_create_nolog=True)).create(vals_list)
|
||||
|
||||
for post in posts:
|
||||
# deleted or closed questions
|
||||
if post.parent_id and (post.parent_id.state == 'close' or post.parent_id.active is False):
|
||||
raise UserError(_('Posting answer on a [Deleted] or [Closed] question is not possible.'))
|
||||
# karma-based access
|
||||
if not post.parent_id and not post.can_ask:
|
||||
raise AccessError(_('%d karma required to create a new question.', post.forum_id.karma_ask))
|
||||
elif post.parent_id and not post.can_answer:
|
||||
raise AccessError(_('%d karma required to answer a question.', post.forum_id.karma_answer))
|
||||
if not post.parent_id and not post.can_post:
|
||||
post.sudo().state = 'pending'
|
||||
|
||||
# add karma for posting new questions
|
||||
if not post.parent_id and post.state == 'active':
|
||||
post.create_uid.sudo()._add_karma(post.forum_id.karma_gen_question_new, post, _('Ask a new question'))
|
||||
posts.post_notification()
|
||||
return posts
|
||||
|
||||
def unlink(self):
|
||||
# if unlinking an answer with accepted answer: remove provided karma
|
||||
for post in self:
|
||||
if post.is_correct:
|
||||
post.create_uid.sudo()._add_karma(post.forum_id.karma_gen_answer_accepted * -1, post, _('The accepted answer is deleted'))
|
||||
self.env.user.sudo()._add_karma(post.forum_id.karma_gen_answer_accepted * -1, post, _('Delete the accepted answer'))
|
||||
return super(Post, self).unlink()
|
||||
|
||||
def write(self, vals):
|
||||
trusted_keys = ['active', 'is_correct', 'tag_ids'] # fields where security is checked manually
|
||||
if 'content' in vals:
|
||||
vals['content'] = self._update_content(vals['content'], self.forum_id.id)
|
||||
|
||||
tag_ids = False
|
||||
if 'tag_ids' in vals:
|
||||
tag_ids = set(self.new({'tag_ids': vals['tag_ids']}).tag_ids.ids)
|
||||
|
||||
for post in self:
|
||||
if 'state' in vals:
|
||||
if vals['state'] in ['active', 'close']:
|
||||
if not post.can_close:
|
||||
raise AccessError(_('%d karma required to close or reopen a post.', post.karma_close))
|
||||
trusted_keys += ['state', 'closed_uid', 'closed_date', 'closed_reason_id']
|
||||
elif vals['state'] == 'flagged':
|
||||
if not post.can_flag:
|
||||
raise AccessError(_('%d karma required to flag a post.', post.forum_id.karma_flag))
|
||||
trusted_keys += ['state', 'flag_user_id']
|
||||
if 'active' in vals:
|
||||
if not post.can_unlink:
|
||||
raise AccessError(_('%d karma required to delete or reactivate a post.', post.karma_unlink))
|
||||
if 'is_correct' in vals:
|
||||
if not post.can_accept:
|
||||
raise AccessError(_('%d karma required to accept or refuse an answer.', post.karma_accept))
|
||||
# update karma except for self-acceptance
|
||||
mult = 1 if vals['is_correct'] else -1
|
||||
if vals['is_correct'] != post.is_correct and post.create_uid.id != self._uid:
|
||||
post.create_uid.sudo()._add_karma(post.forum_id.karma_gen_answer_accepted * mult, post,
|
||||
_('User answer accepted') if mult > 0 else _('Accepted answer removed'))
|
||||
self.env.user.sudo()._add_karma(post.forum_id.karma_gen_answer_accept * mult, post,
|
||||
_('Validate an answer') if mult > 0 else _('Remove validated answer'))
|
||||
if tag_ids:
|
||||
if set(post.tag_ids.ids) != tag_ids and self.env.user.karma < post.forum_id.karma_edit_retag:
|
||||
raise AccessError(_('%d karma required to retag.', post.forum_id.karma_edit_retag))
|
||||
if any(key not in trusted_keys for key in vals) and not post.can_edit:
|
||||
raise AccessError(_('%d karma required to edit a post.', post.karma_edit))
|
||||
|
||||
res = super(Post, self).write(vals)
|
||||
|
||||
# if post content modify, notify followers
|
||||
if 'content' in vals or 'name' in vals:
|
||||
for post in self:
|
||||
if post.parent_id:
|
||||
body, subtype_xmlid = _('Answer Edited'), 'website_forum.mt_answer_edit'
|
||||
obj_id = post.parent_id
|
||||
else:
|
||||
body, subtype_xmlid = _('Question Edited'), 'website_forum.mt_question_edit'
|
||||
obj_id = post
|
||||
obj_id.message_post(body=body, subtype_xmlid=subtype_xmlid)
|
||||
if 'active' in vals:
|
||||
answers = self.env['forum.post'].with_context(active_test=False).search([('parent_id', 'in', self.ids)])
|
||||
if answers:
|
||||
answers.write({'active': vals['active']})
|
||||
return res
|
||||
|
||||
def _get_access_action(self, access_uid=None, force_website=False):
|
||||
""" Instead of the classic form view, redirect to the post on the website directly """
|
||||
self.ensure_one()
|
||||
if not force_website and not self.state == 'active':
|
||||
return super(Post, self)._get_access_action(access_uid=access_uid, force_website=force_website)
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/forum/%s/%s' % (self.forum_id.id, self.id),
|
||||
'target': 'self',
|
||||
'target_type': 'public',
|
||||
'res_id': self.id,
|
||||
}
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_if_enough_karma(self):
|
||||
for post in self:
|
||||
if not post.can_unlink:
|
||||
raise AccessError(_('%d karma required to unlink a post.', post.karma_unlink))
|
||||
|
||||
def _update_content(self, content, forum_id):
|
||||
forum = self.env['forum.forum'].browse(forum_id)
|
||||
if content and self.env.user.karma < forum.karma_dofollow:
|
||||
for match in re.findall(r'<a\s.*href=".*?">', content):
|
||||
escaped_match = re.escape(match) # replace parenthesis or special char in regex
|
||||
url_match = re.match(r'^.*href="(.*)".*', match) # extracting the link allows to rebuild a clean link tag
|
||||
url = url_match.group(1)
|
||||
content = re.sub(escaped_match, f'<a rel="nofollow" href="{url}">', content)
|
||||
|
||||
if self.env.user.karma < forum.karma_editor:
|
||||
filter_regexp = r'(<img.*?>)|(<a[^>]*?href[^>]*?>)|(<[a-z|A-Z]+[^>]*style\s*=\s*[\'"][^\'"]*\s*background[^:]*:[^url;]*url)'
|
||||
content_match = re.search(filter_regexp, content, re.I)
|
||||
if content_match:
|
||||
raise AccessError(_('%d karma required to post an image or link.', forum.karma_editor))
|
||||
return content
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# BUSINESS
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
def post_notification(self):
|
||||
for post in self:
|
||||
tag_partners = post.tag_ids.sudo().mapped('message_partner_ids')
|
||||
|
||||
if post.state == 'active' and post.parent_id:
|
||||
post.parent_id.message_post_with_source(
|
||||
'website_forum.forum_post_template_new_answer',
|
||||
subject=_('Re: %s', post.parent_id.name),
|
||||
partner_ids=tag_partners.ids,
|
||||
subtype_xmlid='website_forum.mt_answer_new',
|
||||
)
|
||||
elif post.state == 'active' and not post.parent_id:
|
||||
post.message_post_with_source(
|
||||
'website_forum.forum_post_template_new_question',
|
||||
subject=post.name,
|
||||
partner_ids=tag_partners.ids,
|
||||
subtype_xmlid='website_forum.mt_question_new',
|
||||
)
|
||||
elif post.state == 'pending' and not post.parent_id:
|
||||
# TDE FIXME: in master, you should probably use a subtype;
|
||||
# however here we remove subtype but set partner_ids
|
||||
partners = post.sudo().message_partner_ids | tag_partners
|
||||
partners = partners.filtered(lambda partner: partner.user_ids and any(user.karma >= post.forum_id.karma_moderate for user in partner.user_ids))
|
||||
|
||||
post.message_post_with_source(
|
||||
'website_forum.forum_post_template_validation',
|
||||
subject=post.name,
|
||||
partner_ids=partners.ids,
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return True
|
||||
|
||||
def reopen(self):
|
||||
if any(post.parent_id or post.state != 'close' for post in self):
|
||||
return False
|
||||
|
||||
reason_offensive = self.env.ref('website_forum.reason_7')
|
||||
reason_spam = self.env.ref('website_forum.reason_8')
|
||||
for post in self:
|
||||
if post.closed_reason_id in (reason_offensive, reason_spam):
|
||||
_logger.info('Upvoting user <%s>, reopening spam/offensive question',
|
||||
post.create_uid)
|
||||
|
||||
karma = post.forum_id.karma_gen_answer_flagged
|
||||
if post.closed_reason_id == reason_spam:
|
||||
# If first post, increase the karma to add
|
||||
count_post = post.search_count([('parent_id', '=', False), ('forum_id', '=', post.forum_id.id), ('create_uid', '=', post.create_uid.id)])
|
||||
if count_post == 1:
|
||||
karma *= 10
|
||||
post.create_uid.sudo()._add_karma(karma * -1, post, _('Reopen a banned question'))
|
||||
|
||||
self.sudo().write({'state': 'active'})
|
||||
|
||||
def close(self, reason_id):
|
||||
if any(post.parent_id for post in self):
|
||||
return False
|
||||
|
||||
reason_offensive = self.env.ref('website_forum.reason_7').id
|
||||
reason_spam = self.env.ref('website_forum.reason_8').id
|
||||
if reason_id in (reason_offensive, reason_spam):
|
||||
for post in self:
|
||||
_logger.info('Downvoting user <%s> for posting spam/offensive contents',
|
||||
post.create_uid)
|
||||
karma = post.forum_id.karma_gen_answer_flagged
|
||||
if reason_id == reason_spam:
|
||||
# If first post, increase the karma to remove
|
||||
count_post = post.search_count([('parent_id', '=', False), ('forum_id', '=', post.forum_id.id), ('create_uid', '=', post.create_uid.id)])
|
||||
if count_post == 1:
|
||||
karma *= 10
|
||||
message = (
|
||||
_('Post is closed and marked as spam')
|
||||
if reason_id == reason_spam else
|
||||
_('Post is closed and marked as offensive content')
|
||||
)
|
||||
post.create_uid.sudo()._add_karma(karma, post, message)
|
||||
|
||||
self.write({
|
||||
'state': 'close',
|
||||
'closed_uid': self._uid,
|
||||
'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
'closed_reason_id': reason_id,
|
||||
})
|
||||
return True
|
||||
|
||||
def validate(self):
|
||||
for post in self:
|
||||
if not post.can_moderate:
|
||||
raise AccessError(_('%d karma required to validate a post.', post.forum_id.karma_moderate))
|
||||
# if state == pending, no karma previously added for the new question
|
||||
if post.state == 'pending':
|
||||
post.create_uid.sudo()._add_karma(
|
||||
post.forum_id.karma_gen_question_new,
|
||||
post,
|
||||
_('Ask a question'),
|
||||
)
|
||||
post.write({
|
||||
'state': 'active',
|
||||
'active': True,
|
||||
'moderator_id': self.env.user.id,
|
||||
})
|
||||
post.post_notification()
|
||||
return True
|
||||
|
||||
def refuse(self):
|
||||
for post in self:
|
||||
if not post.can_moderate:
|
||||
raise AccessError(_('%d karma required to refuse a post.', post.forum_id.karma_moderate))
|
||||
post.moderator_id = self.env.user
|
||||
return True
|
||||
|
||||
def flag(self):
|
||||
res = []
|
||||
for post in self:
|
||||
if not post.can_flag:
|
||||
raise AccessError(_('%d karma required to flag a post.', post.forum_id.karma_flag))
|
||||
if post.state == 'flagged':
|
||||
res.append({'error': 'post_already_flagged'})
|
||||
elif post.state == 'active':
|
||||
# TODO: potential performance bottleneck, can be batched
|
||||
post.write({
|
||||
'state': 'flagged',
|
||||
'flag_user_id': self.env.user.id,
|
||||
})
|
||||
res.append(
|
||||
post.can_moderate and
|
||||
{'success': 'post_flagged_moderator'} or
|
||||
{'success': 'post_flagged_non_moderator'}
|
||||
)
|
||||
else:
|
||||
res.append({'error': 'post_non_flaggable'})
|
||||
return res
|
||||
|
||||
def mark_as_offensive(self, reason_id):
|
||||
for post in self:
|
||||
if not post.can_moderate:
|
||||
raise AccessError(_('%d karma required to mark a post as offensive.', post.forum_id.karma_moderate))
|
||||
# remove some karma
|
||||
_logger.info('Downvoting user <%s> for posting spam/offensive contents', post.create_uid)
|
||||
post.create_uid.sudo()._add_karma(post.forum_id.karma_gen_answer_flagged, post, _('Downvote for posting offensive contents'))
|
||||
# TODO: potential bottleneck, could be done in batch
|
||||
post.write({
|
||||
'state': 'offensive',
|
||||
'moderator_id': self.env.user.id,
|
||||
'closed_date': fields.Datetime.now(),
|
||||
'closed_reason_id': reason_id,
|
||||
'active': False,
|
||||
})
|
||||
return True
|
||||
|
||||
def mark_as_offensive_batch(self, key, values):
|
||||
spams = self.browse()
|
||||
if key == 'create_uid':
|
||||
spams = self.filtered(lambda x: x.create_uid.id in values)
|
||||
elif key == 'country_id':
|
||||
spams = self.filtered(lambda x: x.create_uid.country_id.id in values)
|
||||
elif key == 'post_id':
|
||||
spams = self.filtered(lambda x: x.id in values)
|
||||
|
||||
reason_id = self.env.ref('website_forum.reason_8').id
|
||||
_logger.info('User %s marked as spams (in batch): %s' % (self.env.uid, spams))
|
||||
return spams.mark_as_offensive(reason_id)
|
||||
|
||||
def vote(self, upvote=True):
|
||||
self.ensure_one()
|
||||
Vote = self.env['forum.post.vote']
|
||||
existing_vote = Vote.search([('post_id', '=', self.id), ('user_id', '=', self._uid)])
|
||||
new_vote_value = '1' if upvote else '-1'
|
||||
if existing_vote:
|
||||
if upvote:
|
||||
new_vote_value = '0' if existing_vote.vote == '-1' else '1'
|
||||
else:
|
||||
new_vote_value = '0' if existing_vote.vote == '1' else '-1'
|
||||
existing_vote.vote = new_vote_value
|
||||
else:
|
||||
Vote.create({'post_id': self.id, 'vote': new_vote_value})
|
||||
return {'vote_count': self.vote_count, 'user_vote': new_vote_value}
|
||||
|
||||
def convert_answer_to_comment(self):
|
||||
""" Tools to convert an answer (forum.post) to a comment (mail.message).
|
||||
The original post is unlinked and a new comment is posted on the question
|
||||
using the post create_uid as the comment's author. """
|
||||
self.ensure_one()
|
||||
if not self.parent_id:
|
||||
return self.env['mail.message']
|
||||
|
||||
# karma-based action check: use the post field that computed own/all value
|
||||
if not self.can_comment_convert:
|
||||
raise AccessError(_('%d karma required to convert an answer to a comment.', self.karma_comment_convert))
|
||||
|
||||
# post the message
|
||||
question = self.parent_id
|
||||
self_sudo = self.sudo()
|
||||
values = {
|
||||
'author_id': self_sudo.create_uid.partner_id.id, # use sudo here because of access to res.users model
|
||||
'email_from': self_sudo.create_uid.email_formatted, # use sudo here because of access to res.users model
|
||||
'body': tools.html_sanitize(self.content, sanitize_attributes=True, strip_style=True, strip_classes=True),
|
||||
'message_type': 'comment',
|
||||
'subtype_xmlid': 'mail.mt_comment',
|
||||
'date': self.create_date,
|
||||
}
|
||||
# done with the author user to have create_uid correctly set
|
||||
new_message = question.with_user(self_sudo.create_uid.id).with_context(mail_create_nosubscribe=True).sudo().message_post(**values).sudo(False)
|
||||
|
||||
# unlink the original answer, using SUPERUSER_ID to avoid karma issues
|
||||
self.sudo().unlink()
|
||||
|
||||
return new_message
|
||||
|
||||
@api.model
|
||||
def convert_comment_to_answer(self, message_id):
|
||||
""" Tool to convert a comment (mail.message) into an answer (forum.post).
|
||||
The original comment is unlinked and a new answer from the comment's author
|
||||
is created. Nothing is done if the comment's author already answered the
|
||||
question. """
|
||||
comment_sudo = self.env['mail.message'].sudo().browse(message_id)
|
||||
post = self.browse(comment_sudo.res_id)
|
||||
if not comment_sudo.author_id or not comment_sudo.author_id.user_ids: # only comment posted by users can be converted
|
||||
return False
|
||||
|
||||
# karma-based action check: must check the message's author to know if own / all
|
||||
is_author = comment_sudo.author_id.id == self.env.user.partner_id.id
|
||||
karma_own = post.forum_id.karma_comment_convert_own
|
||||
karma_all = post.forum_id.karma_comment_convert_all
|
||||
karma_convert = is_author and karma_own or karma_all
|
||||
can_convert = self.env.user.karma >= karma_convert
|
||||
if not can_convert:
|
||||
if is_author and karma_own < karma_all:
|
||||
raise AccessError(_('%d karma required to convert your comment to an answer.', karma_own))
|
||||
else:
|
||||
raise AccessError(_('%d karma required to convert a comment to an answer.', karma_all))
|
||||
|
||||
# check the message's author has not already an answer
|
||||
question = post.parent_id if post.parent_id else post
|
||||
post_create_uid = comment_sudo.author_id.user_ids[0]
|
||||
if any(answer.create_uid.id == post_create_uid.id for answer in question.child_ids):
|
||||
return False
|
||||
|
||||
# create the new post
|
||||
post_values = {
|
||||
'forum_id': question.forum_id.id,
|
||||
'content': comment_sudo.body,
|
||||
'parent_id': question.id,
|
||||
'name': _('Re: %s', question.name or ''),
|
||||
}
|
||||
# done with the author user to have create_uid correctly set
|
||||
new_post = self.with_user(post_create_uid).sudo().create(post_values).sudo(False)
|
||||
|
||||
# delete comment
|
||||
comment_sudo.unlink()
|
||||
|
||||
return new_post
|
||||
|
||||
def unlink_comment(self, message_id):
|
||||
comment_sudo = self.env['mail.message'].sudo().browse(message_id)
|
||||
if comment_sudo.model != 'forum.post':
|
||||
return [False] * len(self)
|
||||
|
||||
user_karma = self.env.user.karma
|
||||
result = []
|
||||
for post in self:
|
||||
if comment_sudo.res_id != post.id:
|
||||
result.append(False)
|
||||
continue
|
||||
# karma-based action check: must check the message's author to know if own or all
|
||||
karma_required = (
|
||||
post.forum_id.karma_comment_unlink_own
|
||||
if comment_sudo.author_id.id == self.env.user.partner_id.id
|
||||
else post.forum_id.karma_comment_unlink_all
|
||||
)
|
||||
if user_karma < karma_required:
|
||||
raise AccessError(_('%d karma required to delete a comment.', karma_required))
|
||||
result.append(comment_sudo.unlink())
|
||||
return result
|
||||
|
||||
def _set_viewed(self):
|
||||
self.ensure_one()
|
||||
return sql.increment_fields_skiplock(self, 'views')
|
||||
|
||||
def _update_last_activity(self):
|
||||
self.ensure_one()
|
||||
return self.sudo().write({'last_activity_date': fields.Datetime.now()})
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# MESSAGING
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _get_mail_message_access(self, res_ids, operation, model_name=None):
|
||||
# XDO FIXME: to be correctly fixed with new _get_mail_message_access and filter access rule
|
||||
if operation in ('write', 'unlink') and (not model_name or model_name == 'forum.post'):
|
||||
# Make sure only author or moderator can edit/delete messages
|
||||
for post in self.browse(res_ids):
|
||||
if not post.can_edit:
|
||||
raise AccessError(_('%d karma required to edit a post.', post.karma_edit))
|
||||
return super(Post, self)._get_mail_message_access(res_ids, operation, model_name=model_name)
|
||||
|
||||
def _notify_get_recipients_groups(self, message, model_description, msg_vals=None):
|
||||
""" Add access button to everyone if the document is active. """
|
||||
groups = super()._notify_get_recipients_groups(
|
||||
message, model_description, msg_vals=msg_vals
|
||||
)
|
||||
if not self:
|
||||
return groups
|
||||
|
||||
self.ensure_one()
|
||||
if self.state == 'active':
|
||||
for _group_name, _group_method, group_data in groups:
|
||||
group_data['has_button_access'] = True
|
||||
|
||||
return groups
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, *, message_type='notification', **kwargs):
|
||||
if self.ids and message_type == 'comment': # user comments have a restriction on karma
|
||||
# add followers of comments on the parent post
|
||||
if self.parent_id:
|
||||
partner_ids = kwargs.get('partner_ids', [])
|
||||
comment_subtype = self.sudo().env.ref('mail.mt_comment')
|
||||
question_followers = self.env['mail.followers'].sudo().search([
|
||||
('res_model', '=', self._name),
|
||||
('res_id', '=', self.parent_id.id),
|
||||
('partner_id', '!=', False),
|
||||
]).filtered(lambda fol: comment_subtype in fol.subtype_ids).mapped('partner_id')
|
||||
partner_ids += question_followers.ids
|
||||
kwargs['partner_ids'] = partner_ids
|
||||
|
||||
self.ensure_one()
|
||||
if not self.can_comment:
|
||||
raise AccessError(_('%d karma required to comment.', self.karma_comment))
|
||||
if not kwargs.get('record_name') and self.parent_id:
|
||||
kwargs['record_name'] = self.parent_id.name
|
||||
return super(Post, self).message_post(message_type=message_type, **kwargs)
|
||||
|
||||
def _notify_thread_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
|
||||
""" Override to avoid keeping all notified recipients of a comment.
|
||||
We avoid tracking needaction on post comments. Only emails should be
|
||||
sufficient. """
|
||||
if msg_vals is None:
|
||||
msg_vals = {}
|
||||
if msg_vals.get('message_type', message.message_type) == 'comment':
|
||||
return
|
||||
return super(Post, self)._notify_thread_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# WEBSITE
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
def go_to_website(self):
|
||||
self.ensure_one()
|
||||
if not self.website_url:
|
||||
return False
|
||||
return self.env['website'].get_client_action(self.website_url)
|
||||
|
||||
@api.model
|
||||
def _search_get_detail(self, website, order, options):
|
||||
with_description = options['displayDescription']
|
||||
with_date = options['displayDetail']
|
||||
search_fields = ['name']
|
||||
fetch_fields = ['id', 'name', 'website_url']
|
||||
mapping = {
|
||||
'name': {'name': 'name', 'type': 'text', 'match': True},
|
||||
'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
|
||||
}
|
||||
|
||||
domain = website.website_domain()
|
||||
domain = expression.AND([domain, [('state', '=', 'active'), ('can_view', '=', True)]])
|
||||
include_answers = options.get('include_answers', False)
|
||||
if not include_answers:
|
||||
domain = expression.AND([domain, [('parent_id', '=', False)]])
|
||||
forum = options.get('forum')
|
||||
if forum:
|
||||
domain = expression.AND([domain, [('forum_id', '=', unslug(forum)[1])]])
|
||||
tags = options.get('tag')
|
||||
if tags:
|
||||
domain = expression.AND([domain, [('tag_ids', 'in', [unslug(tag)[1] for tag in tags.split(',')])]])
|
||||
filters = options.get('filters')
|
||||
if filters == 'unanswered':
|
||||
domain = expression.AND([domain, [('child_ids', '=', False)]])
|
||||
elif filters == 'solved':
|
||||
domain = expression.AND([domain, [('has_validated_answer', '=', True)]])
|
||||
elif filters == 'unsolved':
|
||||
domain = expression.AND([domain, [('has_validated_answer', '=', False)]])
|
||||
user = self.env.user
|
||||
my = options.get('my')
|
||||
create_uid = user.id if my == 'mine' else options.get('create_uid')
|
||||
if create_uid:
|
||||
domain = expression.AND([domain, [('create_uid', '=', create_uid)]])
|
||||
if my == 'followed':
|
||||
domain = expression.AND([domain, [('message_partner_ids', '=', user.partner_id.id)]])
|
||||
elif my == 'tagged':
|
||||
domain = expression.AND([domain, [('tag_ids.message_partner_ids', '=', user.partner_id.id)]])
|
||||
elif my == 'favourites':
|
||||
domain = expression.AND([domain, [('favourite_ids', '=', user.id)]])
|
||||
elif my == 'upvoted':
|
||||
domain = expression.AND([domain, [('vote_ids.user_id', '=', user.id)]])
|
||||
|
||||
# 'sorting' from the form's "Order by" overrides order during auto-completion
|
||||
order = options.get('sorting', order)
|
||||
if 'is_published' in order:
|
||||
parts = [part for part in order.split(',') if 'is_published' not in part]
|
||||
order = ','.join(parts)
|
||||
|
||||
if with_description:
|
||||
search_fields.append('content')
|
||||
fetch_fields.append('content')
|
||||
mapping['description'] = {'name': 'content', 'type': 'text', 'html': True, 'match': True}
|
||||
if with_date:
|
||||
fetch_fields.append('write_date')
|
||||
mapping['detail'] = {'name': 'date', 'type': 'html'}
|
||||
return {
|
||||
'model': 'forum.post',
|
||||
'base_domain': [domain],
|
||||
'search_fields': search_fields,
|
||||
'fetch_fields': fetch_fields,
|
||||
'mapping': mapping,
|
||||
'icon': 'fa-comment-o',
|
||||
'order': order,
|
||||
}
|
||||
|
||||
def _search_render_results(self, fetch_fields, mapping, icon, limit):
|
||||
with_date = 'detail' in mapping
|
||||
results_data = super()._search_render_results(fetch_fields, mapping, icon, limit)
|
||||
for post, data in zip(self, results_data):
|
||||
if with_date:
|
||||
data['date'] = self.env['ir.qweb.field.date'].record_to_html(post, 'write_date', {})
|
||||
return results_data
|
13
models/forum_post_reason.py
Normal file
13
models/forum_post_reason.py
Normal file
@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class PostReason(models.Model):
|
||||
_name = "forum.post.reason"
|
||||
_description = "Post Closing Reason"
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(string='Closing Reason', required=True, translate=True)
|
||||
reason_type = fields.Selection([('basic', 'Basic'), ('offensive', 'Offensive')], string='Reason Type', default='basic')
|
113
models/forum_post_vote.py
Normal file
113
models/forum_post_vote.py
Normal file
@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, AccessError
|
||||
|
||||
|
||||
class Vote(models.Model):
|
||||
_name = 'forum.post.vote'
|
||||
_description = 'Post Vote'
|
||||
_order = 'create_date desc, id desc'
|
||||
|
||||
post_id = fields.Many2one('forum.post', string='Post', ondelete='cascade', required=True)
|
||||
user_id = fields.Many2one('res.users', string='User', required=True, default=lambda self: self._uid, ondelete='cascade')
|
||||
vote = fields.Selection([('1', '1'), ('-1', '-1'), ('0', '0')], string='Vote', required=True, default='1')
|
||||
create_date = fields.Datetime('Create Date', index=True, readonly=True)
|
||||
forum_id = fields.Many2one('forum.forum', string='Forum', related="post_id.forum_id", store=True, readonly=False)
|
||||
recipient_id = fields.Many2one('res.users', string='To', related="post_id.create_uid", store=True, readonly=False)
|
||||
|
||||
_sql_constraints = [
|
||||
('vote_uniq', 'unique (post_id, user_id)', "Vote already exists!"),
|
||||
]
|
||||
|
||||
def _get_karma_value(self, old_vote, new_vote, up_karma, down_karma):
|
||||
"""Return the karma to add / remove based on the old vote and on the new vote."""
|
||||
karma_values = {'-1': down_karma, '0': 0, '1': up_karma}
|
||||
karma = karma_values[new_vote] - karma_values[old_vote]
|
||||
|
||||
if old_vote == new_vote:
|
||||
reason = _('no changes')
|
||||
elif new_vote == '1':
|
||||
reason = _('upvoted')
|
||||
elif new_vote == '-1':
|
||||
reason = _('downvoted')
|
||||
elif old_vote == '1':
|
||||
reason = _('no more upvoted')
|
||||
else:
|
||||
reason = _('no more downvoted')
|
||||
|
||||
return karma, reason
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
# can't modify owner of a vote
|
||||
if not self.env.is_admin():
|
||||
for vals in vals_list:
|
||||
vals.pop('user_id', None)
|
||||
|
||||
votes = super(Vote, self).create(vals_list)
|
||||
|
||||
for vote in votes:
|
||||
vote._check_general_rights()
|
||||
vote._check_karma_rights(vote.vote == '1')
|
||||
|
||||
# karma update
|
||||
vote._vote_update_karma('0', vote.vote)
|
||||
return votes
|
||||
|
||||
def write(self, values):
|
||||
# can't modify owner of a vote
|
||||
if not self.env.is_admin():
|
||||
values.pop('user_id', None)
|
||||
|
||||
for vote in self:
|
||||
vote._check_general_rights(values)
|
||||
vote_value = values.get('vote')
|
||||
if vote_value is not None:
|
||||
upvote = vote.vote == '-1' if vote_value == '0' else vote_value == '1'
|
||||
vote._check_karma_rights(upvote)
|
||||
|
||||
# karma update
|
||||
vote._vote_update_karma(vote.vote, vote_value)
|
||||
|
||||
res = super(Vote, self).write(values)
|
||||
return res
|
||||
|
||||
def _check_general_rights(self, vals=None):
|
||||
if vals is None:
|
||||
vals = {}
|
||||
post = self.post_id
|
||||
if vals.get('post_id'):
|
||||
post = self.env['forum.post'].browse(vals.get('post_id'))
|
||||
if not self.env.is_admin():
|
||||
# own post check
|
||||
if self._uid == post.create_uid.id:
|
||||
raise UserError(_('It is not allowed to vote for its own post.'))
|
||||
# own vote check
|
||||
if self._uid != self.user_id.id:
|
||||
raise UserError(_('It is not allowed to modify someone else\'s vote.'))
|
||||
|
||||
def _check_karma_rights(self, upvote=False):
|
||||
# karma check
|
||||
if upvote and not self.post_id.can_upvote:
|
||||
raise AccessError(_('%d karma required to upvote.', self.post_id.forum_id.karma_upvote))
|
||||
elif not upvote and not self.post_id.can_downvote:
|
||||
raise AccessError(_('%d karma required to downvote.', self.post_id.forum_id.karma_downvote))
|
||||
|
||||
def _vote_update_karma(self, old_vote, new_vote):
|
||||
if self.post_id.parent_id:
|
||||
karma, reason = self._get_karma_value(
|
||||
old_vote,
|
||||
new_vote,
|
||||
self.forum_id.karma_gen_answer_upvote,
|
||||
self.forum_id.karma_gen_answer_downvote)
|
||||
source = _('Answer %s', reason)
|
||||
else:
|
||||
karma, reason = self._get_karma_value(
|
||||
old_vote,
|
||||
new_vote,
|
||||
self.forum_id.karma_gen_question_upvote,
|
||||
self.forum_id.karma_gen_question_downvote)
|
||||
source = _('Question %s', reason)
|
||||
self.recipient_id.sudo()._add_karma(karma, self.post_id, source)
|
72
models/forum_tag.py
Normal file
72
models/forum_tag.py
Normal file
@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.http_routing.models.ir_http import slug, unslug
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class Tags(models.Model):
|
||||
_name = "forum.tag"
|
||||
_description = "Forum Tag"
|
||||
_inherit = [
|
||||
'mail.thread',
|
||||
'website.searchable.mixin',
|
||||
'website.seo.metadata',
|
||||
]
|
||||
|
||||
name = fields.Char('Name', required=True)
|
||||
forum_id = fields.Many2one('forum.forum', string='Forum', required=True, index=True)
|
||||
post_ids = fields.Many2many(
|
||||
'forum.post', 'forum_tag_rel', 'forum_tag_id', 'forum_id',
|
||||
string='Posts', domain=[('state', '=', 'active')])
|
||||
posts_count = fields.Integer('Number of Posts', compute='_compute_posts_count', store=True)
|
||||
website_url = fields.Char("Link to questions with the tag", compute='_compute_website_url')
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name, forum_id)', "Tag name already exists!"),
|
||||
]
|
||||
|
||||
@api.depends("post_ids", "post_ids.tag_ids", "post_ids.state", "post_ids.active")
|
||||
def _compute_posts_count(self):
|
||||
for tag in self:
|
||||
tag.posts_count = len(tag.post_ids) # state filter is in field domain
|
||||
|
||||
@api.depends("forum_id", "forum_id.name", "name")
|
||||
def _compute_website_url(self):
|
||||
for tag in self:
|
||||
tag.website_url = f'/forum/{slug(tag.forum_id)}/tag/{slug(tag)}/questions'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
forum = self.env['forum.forum'].browse(vals.get('forum_id'))
|
||||
if self.env.user.karma < forum.karma_tag_create and not self.env.is_admin():
|
||||
raise AccessError(_('%d karma required to create a new Tag.', forum.karma_tag_create))
|
||||
return super(Tags, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(vals_list)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# WEBSITE
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _search_get_detail(self, website, order, options):
|
||||
search_fields = ['name']
|
||||
fetch_fields = ['id', 'name', 'website_url']
|
||||
mapping = {
|
||||
'name': {'name': 'name', 'type': 'text', 'match': True},
|
||||
'website_url': {'name': 'website_url', 'type': 'text', 'truncate': False},
|
||||
}
|
||||
base_domain = []
|
||||
if forum := options.get("forum"):
|
||||
forum_ids = (unslug(forum)[1],) if isinstance(forum, str) else forum.ids
|
||||
search_domain = options.get("domain")
|
||||
base_domain = [search_domain if search_domain is not None else [('forum_id', 'in', forum_ids)]]
|
||||
return {
|
||||
'model': 'forum.tag',
|
||||
'base_domain': base_domain,
|
||||
'search_fields': search_fields,
|
||||
'fetch_fields': fetch_fields,
|
||||
'mapping': mapping,
|
||||
'icon': 'fa-tag',
|
||||
'order': ','.join(filter(lambda f: 'is_published' not in f, order.split(','))),
|
||||
}
|
12
models/gamification_challenge.py
Normal file
12
models/gamification_challenge.py
Normal file
@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class Challenge(models.Model):
|
||||
_inherit = 'gamification.challenge'
|
||||
|
||||
challenge_category = fields.Selection(selection_add=[
|
||||
('forum', 'Website / Forum')
|
||||
], ondelete={'forum': 'set default'})
|
11
models/gamification_karma_tracking.py
Normal file
11
models/gamification_karma_tracking.py
Normal file
@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class KarmaTracking(models.Model):
|
||||
_inherit = 'gamification.karma.tracking'
|
||||
|
||||
def _get_origin_selection_values(self):
|
||||
return super()._get_origin_selection_values() + [('forum.post', self.env['ir.model']._get('forum.post').display_name)]
|
23
models/ir_attachment.py
Normal file
23
models/ir_attachment.py
Normal file
@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class Attachment(models.Model):
|
||||
|
||||
_inherit = "ir.attachment"
|
||||
|
||||
def _can_bypass_rights_on_media_dialog(self, **attachment_data):
|
||||
# Bypass the attachment create ACL and let the user create the image
|
||||
# attachment if they have write access to the model (the image attachment
|
||||
# will be bound to this model's record).
|
||||
res_model = attachment_data['res_model']
|
||||
res_id = attachment_data.get('res_id')
|
||||
if (
|
||||
res_model == 'forum.post' and res_id
|
||||
and self.env['forum.post'].browse(res_id).can_use_full_editor
|
||||
):
|
||||
return True
|
||||
|
||||
return super()._can_bypass_rights_on_media_dialog(**attachment_data)
|
22
models/res_users.py
Normal file
22
models/res_users.py
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, fields, models
|
||||
|
||||
|
||||
class Users(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
create_date = fields.Datetime('Create Date', readonly=True, index=True)
|
||||
|
||||
# Wrapper for call_kw with inherits
|
||||
def open_website_url(self):
|
||||
return self.mapped('partner_id').open_website_url()
|
||||
|
||||
def get_gamification_redirection_data(self):
|
||||
res = super().get_gamification_redirection_data()
|
||||
res.append({
|
||||
'label': _('See our Forum'),
|
||||
'url': '/forum',
|
||||
})
|
||||
return res
|
57
models/website.py
Normal file
57
models/website.py
Normal file
@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.addons.http_routing.models.ir_http import url_for
|
||||
|
||||
|
||||
class Website(models.Model):
|
||||
_inherit = 'website'
|
||||
|
||||
forum_count = fields.Integer(readonly=True, default=0)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
websites = super().create(vals_list)
|
||||
websites._update_forum_count()
|
||||
return websites
|
||||
|
||||
def get_suggested_controllers(self):
|
||||
suggested_controllers = super(Website, self).get_suggested_controllers()
|
||||
suggested_controllers.append((_('Forum'), url_for('/forum'), 'website_forum'))
|
||||
return suggested_controllers
|
||||
|
||||
def configurator_get_footer_links(self):
|
||||
links = super().configurator_get_footer_links()
|
||||
links.append({'text': _("Forum"), 'href': '/forum'})
|
||||
return links
|
||||
|
||||
def configurator_set_menu_links(self, menu_company, module_data):
|
||||
# Forum menu should only be a footer link, not a menu
|
||||
forum_menu = self.env['website.menu'].search([('url', '=', '/forum'), ('website_id', '=', self.id)])
|
||||
forum_menu.unlink()
|
||||
super().configurator_set_menu_links(menu_company, module_data)
|
||||
|
||||
def _search_get_details(self, search_type, order, options):
|
||||
result = super()._search_get_details(search_type, order, options)
|
||||
if search_type in ['forums', 'forums_only', 'all']:
|
||||
result.append(self.env['forum.forum']._search_get_detail(self, order, options))
|
||||
if search_type in ['forums', 'forum_posts_only', 'all']:
|
||||
result.append(self.env['forum.post']._search_get_detail(self, order, options))
|
||||
if search_type in ['forums', 'forum_tags_only', 'all']:
|
||||
result.append(self.env['forum.tag']._search_get_detail(self, order, options))
|
||||
return result
|
||||
|
||||
def _update_forum_count(self):
|
||||
""" Update count of forum linked to some websites. This has to be
|
||||
done manually as website_id=False on forum model means a shared forum.
|
||||
There is therefore no straightforward relationship to be used between
|
||||
forum and website.
|
||||
|
||||
This method either runs on self (if not void), either on all existing
|
||||
websites (to update globally counters, notably when a new forum is
|
||||
created). """
|
||||
websites = self if self else self.search([])
|
||||
forums_all = self.env['forum.forum'].search([])
|
||||
for website in websites:
|
||||
website.forum_count = len(forums_all.filtered_domain(website.website_domain()))
|
6
populate/__init__.py
Normal file
6
populate/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import forum_forum
|
||||
from . import forum_post
|
||||
from . import mail_message
|
15
populate/forum_forum.py
Normal file
15
populate/forum_forum.py
Normal file
@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.tools import populate
|
||||
|
||||
class Forum(models.Model):
|
||||
_inherit = 'forum.forum'
|
||||
_populate_sizes = {'small': 1, 'medium': 3, 'large': 10}
|
||||
|
||||
def _populate_factories(self):
|
||||
return [
|
||||
('name', populate.constant('Forum_{counter}')),
|
||||
('description', populate.constant('This is forum number {counter}'))
|
||||
]
|
84
populate/forum_post.py
Normal file
84
populate/forum_post.py
Normal file
@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
from odoo import fields, models
|
||||
from odoo.tools import populate
|
||||
|
||||
QA_WEIGHTS = {0: 25, 1: 35, 2: 20, 3: 10, 4: 4, 5: 3, 6: 2, 7: 1}
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class Post(models.Model):
|
||||
_inherit = 'forum.post'
|
||||
# Include an additional average of 2 post answers for each given size
|
||||
# e.g.: 100 posts as populated_model_records = ~300 actual forum.posts records
|
||||
_populate_sizes = {'small': 100, 'medium': 1000, 'large': 90000}
|
||||
_populate_dependencies = ['forum.forum', 'res.users']
|
||||
|
||||
def _populate_factories(self):
|
||||
forum_ids = self.env.registry.populated_models['forum.forum']
|
||||
hours = [_ for _ in range(1, 49)]
|
||||
random = populate.Random('forum_posts')
|
||||
|
||||
def create_answers(values=None, **kwargs):
|
||||
"""Create random number of answers
|
||||
We set the `last_activity_date` to convert it to `create_date` in `_populate`
|
||||
as the ORM prevents setting these.
|
||||
"""
|
||||
return [
|
||||
fields.Command.create({
|
||||
'name': f"reply to {values['name']}",
|
||||
'forum_id': values['forum_id'],
|
||||
'content': 'Answer content',
|
||||
'last_activity_date': fields.Datetime.add(values['last_activity_date'], hours=random.choice(hours)),
|
||||
})
|
||||
for _ in range(random.choices(*zip(*QA_WEIGHTS.items()))[0])
|
||||
]
|
||||
|
||||
def get_last_activity_date(iterator, *args):
|
||||
days = [_ for _ in range(3, 93)]
|
||||
now = fields.Datetime.now()
|
||||
|
||||
for values in iterator:
|
||||
values.update(last_activity_date=fields.Datetime.subtract(now, days=random.choice(days)))
|
||||
yield values
|
||||
|
||||
return [
|
||||
('forum_id', populate.randomize(forum_ids)),
|
||||
('name', populate.constant('post_{counter}')),
|
||||
('last_activity_date', get_last_activity_date), # Must be before call to 'create_answers'
|
||||
('child_ids', populate.compute(create_answers)),
|
||||
]
|
||||
|
||||
def _populate(self, size):
|
||||
records = super()._populate(size)
|
||||
user_ids = self.env.registry.populated_models['res.users']
|
||||
|
||||
# Overwrite auto-fields: use last_activity_date to update create date
|
||||
_logger.info('forum.post: update create date and uid')
|
||||
question_ids = tuple(records.ids)
|
||||
query = """
|
||||
SELECT setseed(0.5);
|
||||
UPDATE forum_post
|
||||
SET create_date = last_activity_date,
|
||||
create_uid = floor(random() * (%(max_value)s - %(min_value)s + 1) + %(min_value)s)
|
||||
WHERE id in %(question_ids)s or parent_id in %(question_ids)s
|
||||
"""
|
||||
self.env.cr.execute(query, {'question_ids': question_ids, 'min_value': user_ids[0], 'max_value': user_ids[-1]})
|
||||
|
||||
_logger.info('forum.post: update last_activity_date of questions with answers')
|
||||
query = """
|
||||
WITH latest_answer AS(
|
||||
SELECT parent_id, max(last_activity_date) as answer_date
|
||||
FROM forum_post
|
||||
WHERE parent_id in %(question_ids)s
|
||||
GROUP BY parent_id
|
||||
)
|
||||
UPDATE forum_post fp
|
||||
SET last_activity_date = latest_answer.answer_date
|
||||
FROM latest_answer
|
||||
WHERE fp.id = latest_answer.parent_id
|
||||
"""
|
||||
self.env.cr.execute(query, {'question_ids': question_ids})
|
||||
|
||||
return records
|
82
populate/mail_message.py
Normal file
82
populate/mail_message.py
Normal file
@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
from odoo import fields, models
|
||||
from odoo.tools import populate
|
||||
|
||||
CP_WEIGHTS = {1: 35, 2: 30, 3: 25, 4: 10}
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class Message(models.Model):
|
||||
_inherit = 'mail.message'
|
||||
|
||||
@property
|
||||
def _populate_dependencies(self):
|
||||
return super()._populate_dependencies + ["res.users", "forum.post"]
|
||||
|
||||
def _populate(self, size):
|
||||
"""Randomly assign messages to some populated posts and answers.
|
||||
This makes sure these questions and answers get one to four comments.
|
||||
Also define a date that is more recent than the post/answer's create_date
|
||||
"""
|
||||
records = super()._populate(size)
|
||||
comment_subtype = self.env.ref('mail.mt_comment')
|
||||
hours = [_ for _ in range(1, 24)]
|
||||
random = populate.Random('comments_on_forum_posts')
|
||||
users = self.env["res.users"].browse(self.env.registry.populated_models['res.users'])
|
||||
vals_list = []
|
||||
question_ids = self.env.registry.populated_models['forum.post']
|
||||
posts = self.env['forum.post'].search(['|', ('id', 'in', question_ids), ('parent_id', 'in', question_ids)])
|
||||
for post in random.sample(posts, int(len(question_ids) * 0.7)):
|
||||
nb_comments = random.choices(*zip(*CP_WEIGHTS.items()))[0]
|
||||
for counter in range(nb_comments):
|
||||
vals_list.append({
|
||||
"author_id": random.choice(users.partner_id.ids),
|
||||
"body": f"message_body_{counter}",
|
||||
"date": fields.Datetime.add(post.create_date, hours=random.choice(hours)),
|
||||
"message_type": "comment",
|
||||
"model": "forum.post",
|
||||
"res_id": post.id,
|
||||
"subtype_id": comment_subtype.id,
|
||||
})
|
||||
messages = self.env["mail.message"].create(vals_list)
|
||||
_logger.info('mail.message: update comments create date and uid')
|
||||
_logger.info('forum.post: update last_activity_date for posts with comments and/or commented answers')
|
||||
query = """
|
||||
WITH comment_author AS(
|
||||
SELECT mm.id, mm.author_id, ru.id as user_id, ru.partner_id
|
||||
FROM mail_message mm
|
||||
JOIN res_users ru
|
||||
ON mm.author_id = ru.partner_id
|
||||
WHERE mm.id in %(comment_ids)s
|
||||
),
|
||||
updated_comments as (
|
||||
UPDATE mail_message mm
|
||||
SET create_date = date,
|
||||
create_uid = ca.user_id
|
||||
FROM comment_author ca
|
||||
WHERE mm.id = ca.id
|
||||
RETURNING res_id as post_id, create_date as comment_date
|
||||
),
|
||||
max_comment_dates AS (
|
||||
SELECT post_id, max(comment_date) as last_comment_date
|
||||
FROM updated_comments
|
||||
GROUP BY post_id
|
||||
),
|
||||
updated_posts AS (
|
||||
UPDATE forum_post fp
|
||||
SET last_activity_date = CASE --on questions, answer could be more recent
|
||||
WHEN fp.parent_id IS NOT NULL THEN greatest(last_activity_date, last_comment_date)
|
||||
ELSE last_comment_date END
|
||||
FROM max_comment_dates
|
||||
WHERE max_comment_dates.post_id = fp.id
|
||||
RETURNING fp.id as post_id, fp.last_activity_date as last_activity_date, fp.parent_id as parent_id
|
||||
)
|
||||
UPDATE forum_post fp
|
||||
SET last_activity_date = greatest(fp.last_activity_date, up.last_activity_date)
|
||||
FROM updated_posts up
|
||||
WHERE up.parent_id = fp.id
|
||||
"""
|
||||
self.env.cr.execute(query, {'comment_ids': tuple(messages.ids)})
|
||||
return records + messages
|
17
security/ir.model.access.csv
Normal file
17
security/ir.model.access.csv
Normal file
@ -0,0 +1,17 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_forum_forum_public,forum.forum,model_forum_forum,base.group_public,1,0,0,0
|
||||
access_forum_forum_portal,forum.forum,model_forum_forum,base.group_portal,1,0,0,0
|
||||
access_forum_forum_employee,forum.forum,model_forum_forum,base.group_user,1,0,0,0
|
||||
access_forum_forum_manager,forum.forum.maanger,model_forum_forum,base.group_erp_manager,1,1,1,1
|
||||
access_forum_post_public,forum.post.public,model_forum_post,base.group_public,1,0,0,0
|
||||
access_forum_post_portal,forum.post.portal,model_forum_post,base.group_portal,1,1,1,1
|
||||
access_forum_post_user,forum.post.user,model_forum_post,base.group_user,1,1,1,1
|
||||
access_forum_post_vote_public,forum.post.vote.public,model_forum_post_vote,base.group_public,1,0,0,0
|
||||
access_forum_post_vote_portal,orum.post.vote.portal,model_forum_post_vote,base.group_portal,1,1,1,0
|
||||
access_forum_post_vote_user,forum.post.vote.user,model_forum_post_vote,base.group_user,1,1,1,1
|
||||
access_forum_post_reason_public,forum.post.reason.public,model_forum_post_reason,base.group_public,1,0,0,0
|
||||
access_forum_post_reason_portal,forum.post.reason.portal,model_forum_post_reason,base.group_portal,1,0,0,0
|
||||
access_forum_post_reason_user,forum.post.reason.user,model_forum_post_reason,base.group_user,1,1,1,1
|
||||
access_forum_tag_public,forum.tag.public,model_forum_tag,base.group_public,1,0,1,0
|
||||
access_forum_tag_portal,forum.tag.portal,model_forum_tag,base.group_portal,1,0,1,0
|
||||
access_forum_tag_user,forum.tag.user,model_forum_tag,base.group_user,1,1,1,1
|
|
74
security/ir_rule_data.xml
Normal file
74
security/ir_rule_data.xml
Normal file
@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="website_forum_public" model="ir.rule">
|
||||
<field name="name">Website forum: Public user can only access to public forum</field>
|
||||
<field name="model_id" ref="model_forum_forum"/>
|
||||
<field name="domain_force">[('privacy', '=', 'public')]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_public'))]"/>
|
||||
</record>
|
||||
<record id="website_forum_connected" model="ir.rule">
|
||||
<field name="name">Website forum: User can only access to public (or authorized) forum</field>
|
||||
<field name="model_id" ref="model_forum_forum"/>
|
||||
<field name="domain_force">[
|
||||
'|',
|
||||
('privacy', 'in', ['public', 'connected']),
|
||||
'&',
|
||||
('privacy', '=', 'private'),
|
||||
('authorized_group_id', 'in', user.groups_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
<record id="website_forum_create_website_designer" model="ir.rule">
|
||||
<field name="name">Website forum: Website designer can create private forum</field>
|
||||
<field name="model_id" ref="model_forum_forum"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('website.group_website_designer'))]"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
<field name="perm_write" eval="0"/>
|
||||
<field name="perm_read" eval="0"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
</record>
|
||||
<record id="website_forum_private" model="ir.rule">
|
||||
<field name="name">Website forum: All access for manager</field>
|
||||
<field name="model_id" ref="model_forum_forum"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_erp_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="website_forum_public_post" model="ir.rule">
|
||||
<field name="name">Website forum post: Public user can only access to public post</field>
|
||||
<field name="model_id" ref="model_forum_post"/>
|
||||
<field name="domain_force">[('forum_id.privacy', '=', 'public')]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_public'))]"/>
|
||||
</record>
|
||||
<record id="website_forum_connected_post" model="ir.rule">
|
||||
<field name="name">Website forum post: User can only access to public (or authorized) post</field>
|
||||
<field name="model_id" ref="model_forum_post"/>
|
||||
<field name="domain_force">['|', ('forum_id.privacy', 'in', ['public', 'connected']), '&', ('forum_id.privacy', '=', 'private'), ('forum_id.authorized_group_id', 'in', user.groups_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
<record id="website_forum_private_post" model="ir.rule">
|
||||
<field name="name">Website forum post : All access for manager</field>
|
||||
<field name="model_id" ref="model_forum_post"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_erp_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="website_forum_public_tag" model="ir.rule">
|
||||
<field name="name">Website forum tag: Public user can only access to tag linked to public forum</field>
|
||||
<field name="model_id" ref="model_forum_tag"/>
|
||||
<field name="domain_force">[('forum_id.privacy', '=', 'public')]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_public'))]"/>
|
||||
</record>
|
||||
<record id="website_forum_connected_tag" model="ir.rule">
|
||||
<field name="name">Website forum tag: User can only access to tag linked to public (or authorized) forum</field>
|
||||
<field name="model_id" ref="model_forum_tag"/>
|
||||
<field name="domain_force">['|', ('forum_id.privacy', 'in', ['public', 'connected']), '&', ('forum_id.privacy', '=', 'private'), ('forum_id.authorized_group_id', 'in', user.groups_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
<record id="website_forum_private_tag" model="ir.rule">
|
||||
<field name="name">Website forum tag : Manager user can access to all tags</field>
|
||||
<field name="model_id" ref="model_forum_tag"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_erp_manager'))]"/>
|
||||
</record>
|
||||
</odoo>
|
BIN
static/description/icon.png
Normal file
BIN
static/description/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
1
static/description/icon.svg
Normal file
1
static/description/icon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#o_icon_website_forum__a)"><path d="M26.001 34.5s-.83-2-.997-2.167c20.502-9.84 23.518-3.54 23.94-.955.068.418.005.843-.124 1.247A24.857 24.857 0 0 1 47 36.89c-1.5 1.5-10 1.61-10 1.61l-11-4Z" fill="#962B48"/><path d="m21 14 4.004 3.667c-20.502 9.84-23.518 3.54-23.94.955-.068-.418-.005-.843.124-1.247a24.84 24.84 0 0 1 1.428-3.514C6.713 5.644 14.501 11 14.501 11l6.5 3Z" fill="#1A6F66"/><path d="M1.187 32.625c-.129-.404-.192-.829-.124-1.247.422-2.586 3.438-8.886 23.94.955 14.197 6.815 20.01 5.89 22.382 3.818C43.286 44.36 34.803 50 25.003 50 13.855 50 4.411 42.703 1.187 32.625Z" fill="#FC868B"/><path d="M48.819 17.375c.129.404.192.829.124 1.247-.422 2.586-3.438 8.886-23.94-.955-14.197-6.815-20.01-5.89-22.382-3.818C6.72 5.64 15.203 0 25.003 0 36.15 0 45.594 7.297 48.819 17.375Z" fill="#1AD3BB"/></g><defs><clipPath id="o_icon_website_forum__a"><path fill="#fff" d="M0 0h50v50H0z"/></clipPath></defs></svg>
|
After Width: | Height: | Size: 1012 B |
@ -0,0 +1,31 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useEffect } from "@odoo/owl";
|
||||
import { useChildRef } from "@web/core/utils/hooks";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
|
||||
export class FlagMarkAsOffensiveDialog extends Component {
|
||||
static template = "website_forum.FlagMarkAsOffensiveDialog";
|
||||
static components = { Dialog };
|
||||
|
||||
setup() {
|
||||
this.modalRef = useChildRef();
|
||||
|
||||
const onClickDiscard = (ev) => {
|
||||
ev.preventDefault();
|
||||
this.props.close();
|
||||
};
|
||||
|
||||
useEffect(
|
||||
(discardButton) => {
|
||||
if (discardButton) {
|
||||
discardButton.addEventListener("click", onClickDiscard);
|
||||
return () => {
|
||||
discardButton.removeEventListener("click", onClickDiscard);
|
||||
};
|
||||
}
|
||||
},
|
||||
() => [this.modalRef.el?.querySelector(".btn-link")]
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="website_forum.FlagMarkAsOffensiveDialog">
|
||||
<Dialog size="'md'" title="props.title" footer="false" modalRef="modalRef" >
|
||||
<t t-out="props.body"/>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
1
static/src/img/empty.svg
Normal file
1
static/src/img/empty.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 203.0829 243.7154"><g id="c"><g id="d"><polygon points="101.5414 238.2685 20.0382 190.7423 20.0382 101.8237 101.5414 148.9972 101.5414 238.2685" style="fill:#c1dbf6;"/><polygon points="101.5414 238.2685 183.044 191.095 183.044 101.8237 101.5414 148.9972 101.5414 238.2685" style="fill:#fff;"/><polygon points="183.044 101.8237 183.044 158.6371 111.3156 202.3055 101.5414 148.9972 183.044 101.8237" style="fill:#c1dbf6;"/><polygon points="20.8288 101.9859 101.5414 148.9972 101.5414 148.6905 101.5414 54.625 20.8288 101.9859" style="fill:#c1dbf6;"/><polygon points="101.5414 54.625 101.5414 148.9972 183.044 101.8237 101.5414 54.625" style="fill:#374874;"/><path d="m155.6613,118.1373l-23.2882-13.2862c-2.5378-1.4928-4.1799-4.1799-4.1799-7.1656h0c0-3.2842,3.5828-5.3742,6.4192-3.7321l24.6317,14.1819c2.09,1.1943,3.4335,3.5828,3.4335,5.9713h0c.1493,3.5828-3.8814,5.822-7.0163,4.0306Z" style="fill:#fff;"/><path d="m101.5414,54.625l81.5026,47.1988,14.3455,43.1314-14.3455,8.2813v37.8586l-81.5025,47.1734-81.5032-47.5262v-32.0983l-.0114-.007v-5.4006l-14.3455-8.2813,14.3455-43.1314,81.5146-47.1987m0-5.4541L17.6841,97.7379l-1.5591.894-.5672,1.7053L1.2123,143.4687l-1.2123,3.6449,3.3267,1.9204,11.9903,6.9217v5.3231l.0114.007v32.1621l2.3372,1.3629,83.8758,48.9045,83.8617-48.5441,2.3506-1.3604v-37.8551l11.9903-6.9217,3.3388-1.9273-1.2296-3.6538-14.4874-43.0493-.5615-1.6686-1.5184-.8911-83.7445-48.673h0Z" style="fill:#374874;"/><polygon points="183.044 101.8237 101.5414 54.625 20.2749 101.9673 20.0269 101.8237 5.6814 144.9551 87.3993 192.1287 101.5414 148.9972 24.9857 104.6936 101.5414 60.2454 178.3204 104.5575 101.5414 148.9972 115.6715 192.1287 197.3895 144.9551 183.044 101.8237" style="fill:#fff;"/><path d="m49.8818,64.403c-.6002,0-1.2004-.2277-1.6592-.6842l-24.5309-24.3699c-.9233-.9164-.9279-2.4078-.0115-3.33.9176-.9222,2.4101-.9268,3.33-.0115l24.5309,24.3699c.9233.9164.9279,2.4078.0115,3.33-.4599.4634-1.0659.6957-1.6707.6957Z" style="fill:#374874;"/><path d="m150.4164,64.403c-.6048,0-1.2108-.2323-1.6707-.6957-.9164-.9222-.9118-2.4135.0115-3.33l24.5309-24.3699c.9199-.9176,2.4124-.913,3.33.0115.9164.9222.9118,2.4135-.0115,3.33l-24.5309,24.3699c-.4588.4565-1.059.6842-1.6592.6842Z" style="fill:#374874;"/><path d="m101.5353,41.5659c-1.3005,0-2.3549-1.0544-2.3549-2.3549V2.3549c0-1.3005,1.0544-2.3549,2.3549-2.3549s2.3549,1.0544,2.3549,2.3549v36.8561c0,1.3005-1.0544,2.3549-2.3549,2.3549Z" style="fill:#374874;"/></g></g></svg>
|
After Width: | Height: | Size: 2.5 KiB |
BIN
static/src/img/help.jpg
Normal file
BIN
static/src/img/help.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
5
static/src/img/tasks.svg
Normal file
5
static/src/img/tasks.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 10 KiB |
20
static/src/js/systray_items/forum_forum_add_form.js
Normal file
20
static/src/js/systray_items/forum_forum_add_form.js
Normal file
@ -0,0 +1,20 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {NewContentFormController, NewContentFormView} from '@website/js/new_content_form';
|
||||
import {registry} from "@web/core/registry";
|
||||
|
||||
export class AddForumFormController extends NewContentFormController {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
computePath() {
|
||||
return `/forum/${encodeURIComponent(this.model.root.resId)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const AddForumFormView = {
|
||||
...NewContentFormView,
|
||||
Controller: AddForumFormController,
|
||||
};
|
||||
|
||||
registry.category("views").add("website_forum_add_form", AddForumFormView);
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user