[IMP] website_forum: replace bump with last activity
Purpose ======= 1. The post "activity" date shown on a forum's list of posts is not a great user experience as it is seems to be updated for writes on the original post and not, e.g., when a reply is added. 2. Remove bumping on social sharing. As we are not in 2015 anymore, sharing is not a relevant "activity" for the post. This is illustrated by the fact that `allow_bump` could not be updated with the client, and the "default" "target_type" is not found in the codebase anymore. We clean that here. Specifications ============== - Drop bump_date and use last_activity_date instead of write date to log activity - Update post last_activity_date whenever a reply or a comment is added to the original post or one of its replies - Sort posts lists by `last_activity_date desc` by default - Use luxon.js library to render the bump_date field as a `relative datetime`(e.g.: 2 days ago, 1 hour ago...) Task-3349374 Task-3316555 Part-of: odoo/odoo#123709 Co-authored-by: Florian Charlier <flch@odoo.com>
This commit is contained in:
committed by
Florian Charlier
co-authored by
Florian Charlier
parent
3d1d5bfb09
commit
280537b9fe
@@ -7,7 +7,7 @@
|
||||
'category': 'Website/Website',
|
||||
'sequence': 265,
|
||||
'summary': 'Manage a forum with FAQ and Q&A',
|
||||
'version': '1.1',
|
||||
'version': '1.2',
|
||||
'description': """
|
||||
Ask questions, get answers, no distractions
|
||||
""",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import lxml
|
||||
import requests
|
||||
@@ -281,7 +280,6 @@ class WebsiteForum(WebsiteProfile):
|
||||
'main_object': question,
|
||||
'edit_in_backend': True,
|
||||
'question': question,
|
||||
'can_bump': (question.forum_id.allow_bump and not question.child_count and (datetime.today() - question.write_date).days > 9),
|
||||
'header': {'question_data': True},
|
||||
'filters': filters,
|
||||
'reversed': reversed,
|
||||
@@ -378,11 +376,13 @@ class WebsiteForum(WebsiteProfile):
|
||||
'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("/forum/%s/%s" % (slug(forum), post_parent and slug(post_parent) or 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 if post.parent_id else post
|
||||
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'])
|
||||
@@ -390,6 +390,7 @@ class WebsiteForum(WebsiteProfile):
|
||||
body=body,
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment')
|
||||
question._update_last_activity()
|
||||
return request.redirect("/forum/%s/%s" % (slug(forum), slug(question)))
|
||||
|
||||
@http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/toggle_correct', type='json', auth="public", website=True)
|
||||
@@ -467,13 +468,6 @@ class WebsiteForum(WebsiteProfile):
|
||||
upvote = True if post.user_vote < 0 else False
|
||||
return post.vote(upvote=upvote)
|
||||
|
||||
@http.route('/forum/post/bump', type='json', auth="public", website=True)
|
||||
def post_bump(self, post_id, **kwarg):
|
||||
post = request.env['forum.post'].browse(int(post_id))
|
||||
if not post.exists() or post.parent_id:
|
||||
return False
|
||||
return post.bump()
|
||||
|
||||
# Moderation Tools
|
||||
# --------------------------------------------------
|
||||
|
||||
|
||||
@@ -76,17 +76,13 @@ class Forum(models.Model):
|
||||
sanitize_attributes=False, sanitize_form=False)
|
||||
default_order = fields.Selection([
|
||||
('create_date desc', 'Newest'),
|
||||
('write_date desc', 'Last Updated'),
|
||||
('last_activity_date desc', 'Last Updated'),
|
||||
('vote_count desc', 'Most Voted'),
|
||||
('relevancy desc', 'Relevance'),
|
||||
('child_count desc', 'Answered')],
|
||||
string='Default', required=True, default='write_date desc')
|
||||
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_bump = fields.Boolean('Allow Bump', default=True,
|
||||
help='Check this box to display a popup for posts older than 10 days '
|
||||
'without any given answer. The popup will offer to share it on social '
|
||||
'networks. When shared, a question is bumped at the top of the forum.')
|
||||
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 '
|
||||
|
||||
@@ -23,7 +23,7 @@ class Post(models.Model):
|
||||
'website.seo.metadata',
|
||||
'website.searchable.mixin',
|
||||
]
|
||||
_order = "is_correct DESC, vote_count DESC, write_date DESC"
|
||||
_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)
|
||||
@@ -48,10 +48,11 @@ class Post(models.Model):
|
||||
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)
|
||||
bump_date = fields.Datetime('Bumped on', readonly=True,
|
||||
help="Technical field allowing to bump a question. Writing on this field will trigger "
|
||||
"a write on write_date and therefore bump the post. Directly writing on write_date "
|
||||
"is currently not supported and this field is a workaround.")
|
||||
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)
|
||||
|
||||
@@ -602,15 +603,6 @@ class Post(models.Model):
|
||||
_logger.info('User %s marked as spams (in batch): %s' % (self.env.uid, spams))
|
||||
return spams.mark_as_offensive(reason_id)
|
||||
|
||||
def bump(self):
|
||||
""" Bump a question: trigger a write_date by writing on a dummy bump_date
|
||||
field. One cannot bump a question more than once every 10 days. """
|
||||
self.ensure_one()
|
||||
if self.forum_id.allow_bump and not self.child_ids and (datetime.today() - datetime.strptime(self.write_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days > 9:
|
||||
# write through super to bypass karma; sudo to allow public user to bump any post
|
||||
return self.sudo().write({'bump_date': fields.Datetime.now()})
|
||||
return False
|
||||
|
||||
def vote(self, upvote=True):
|
||||
self.ensure_one()
|
||||
Vote = self.env['forum.post.vote']
|
||||
@@ -724,6 +716,10 @@ class Post(models.Model):
|
||||
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
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
@@ -164,6 +164,12 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
|
||||
});
|
||||
});
|
||||
|
||||
this.$('.o_wforum_question, .o_wforum_answer, .o_wforum_post_comment, .o_wforum_last_activity').toArray()
|
||||
.forEach((post) => {
|
||||
const relativeDateTime = luxon.DateTime.fromSQL($(post).data('lastActivity'), { zone: 'utc'}).toRelative();
|
||||
$(post).find('.o_wforum_relative_datetime').first().text(relativeDateTime);
|
||||
});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
|
||||
@@ -34,13 +34,6 @@ var ForumShare = publicWidget.registry.socialShare.extend({
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_bindSocialEvent: function () {
|
||||
this._super.apply(this, arguments);
|
||||
$('.oe_share_bump').click($.proxy(this._postBump, this));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -59,17 +52,6 @@ var ForumShare = publicWidget.registry.socialShare.extend({
|
||||
$('#oe_social_share_modal').modal('show');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_postBump: function () {
|
||||
this._rpc({ // FIXME
|
||||
route: '/forum/post/bump',
|
||||
params: {
|
||||
post_id: this.element.data('id'),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
publicWidget.registry.websiteForumShare = publicWidget.Widget.extend({
|
||||
@@ -85,11 +67,6 @@ publicWidget.registry.websiteForumShare = publicWidget.Widget.extend({
|
||||
(new ForumShare(this, false, socialData.targetType)).attachTo($(document.body));
|
||||
sessionStorage.removeItem('social_share');
|
||||
}
|
||||
// Display an alert if post has no reply and is older than 10 days
|
||||
var $questionContainer = $('.oe_js_bump');
|
||||
if ($questionContainer.length) {
|
||||
new ForumShare(this, false, 'social-alert').attachTo($questionContainer);
|
||||
}
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -57,10 +57,10 @@
|
||||
<t t-name="website.social_alert">
|
||||
<div class="alert alert-info alert-dismissable" role="status">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
<p>Move this question to the top of the list by sharing it on social networks.</p><br/>
|
||||
<p>Share this post on social networks.</p><br/>
|
||||
<div>
|
||||
<t t-foreach="medias" t-as="media">
|
||||
<a style="cursor: pointer" t-attf-class="fa-stack fa-lg share oe_share_bump #{media}" t-attf-aria-label="Share on #{media}" t-attf-title="Share on #{media}">
|
||||
<a style="cursor: pointer" t-attf-class="fa-stack fa-lg share #{media}" t-attf-aria-label="Share on #{media}" t-attf-title="Share on #{media}">
|
||||
<span class="fa fa-square fa-stack-2x"></span>
|
||||
<span t-attf-class="oe_social_#{media} fa fa-#{media} fa-stack-1x fa-inverse"></span>
|
||||
</a>
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
</a>
|
||||
</th>
|
||||
<th class="o_wforum_table_activity small fw-normal text-center">
|
||||
<a class="text-muted" t-att-href="sorting=='write_date desc' and url_for('') + '?' + keep_query( 'search', 'filters', sorting='write_date asc') or url_for('') + '?' + keep_query( 'search', 'filters', sorting='write_date desc')">
|
||||
Activity <i t-attf-class="fa fa-caret-#{sorting=='write_date desc' and 'down' or 'up'} #{sorting == 'write_date desc' or sorting == 'write_date asc' and ' ' or 'd-none'}"></i>
|
||||
<a class="text-muted" t-attf-href="?#{ keep_query('search', 'filters', sorting='last_activity asc' if sorting == 'last_activity_date desc' else 'last_activity_date desc') }">
|
||||
Activity <i t-attf-class="fa fa-caret-#{ 'down' if sorting == 'last_activity_date desc' else 'up'} #{ not sorting.startswith('last_activity') and 'd-none ' }"></i>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
@@ -111,9 +111,8 @@
|
||||
<td class="o_wforum_view_count d-none d-lg-table-cell text-center">
|
||||
<span t-out="question.views" t-attf-class="#{question.views == 0 and 'opacity-50'}" />
|
||||
</td>
|
||||
<td class="o_wforum_last_activity d-none d-lg-table-cell order-4 text-center">
|
||||
<!-- [TODO] write_date gives last date of ANY update including favouriting. Need a "last answer" date field. -->
|
||||
<span t-field="question.write_date" t-options='{"format": "d MMM yy "}'/>
|
||||
<td class="o_wforum_last_activity d-none d-lg-table-cell order-4 text-center" t-att-data-last-activity="question.last_activity_date">
|
||||
<span class="o_wforum_relative_datetime"/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -297,13 +296,14 @@
|
||||
<t t-set="post_type" t-value="'question'"/>
|
||||
</t>
|
||||
|
||||
<div t-attf-class="o_wforum_#{post_type} row g-0 mb-2 rounded #{can_bump and 'oe_js_bump'}
|
||||
<div t-attf-class="o_wforum_#{post_type} row g-0 mb-2 rounded
|
||||
#{ 'o_wforum_answer_correct my-2 mx-n3 mx-lg-n2 mx-xl-n3 py-3 px-3 px-lg-2 px-xl-3' if post_id == answer and post_id.is_correct else '' }"
|
||||
t-att-data-type="post_type"
|
||||
t-att-data-last-update="post_id.write_date"
|
||||
t-attf-data-id="#{post_type}-#{post_id.id}"
|
||||
t-attf-id="#{post_type}-#{post_id.id}"
|
||||
t-att-data-state="post_id.state">
|
||||
t-att-data-type="post_type"
|
||||
t-att-data-last-activity="post_id.create_date"
|
||||
t-att-data-last-update="post_id.write_date"
|
||||
t-attf-data-id="#{post_type}-#{post_id.id}"
|
||||
t-attf-id="#{post_type}-#{post_id.id}"
|
||||
t-att-data-state="post_id.state">
|
||||
<div class="d-flex flex-column col-auto pe-2 pe-lg-3">
|
||||
<t t-call="website_forum.author_box">
|
||||
<t t-set="object" t-value="post_id"/>
|
||||
@@ -318,12 +318,7 @@
|
||||
<t t-set="allow_biography" t-value="True"/>
|
||||
<t t-set="show_name" t-value="True"/>
|
||||
</t>
|
||||
<!-- [TODO] To be replaced with relative date/time (ex: "2 days ago") -->
|
||||
<span class="ms-2 opacity-75 small text-muted">
|
||||
<!-- <time t-field="post_id.create_date" t-options='{"format": "d MMMM y "}'/>
|
||||
at
|
||||
<time t-field="post_id.create_date" t-options='{"format": "HH:mm "}'/> -->
|
||||
2d ago
|
||||
<span class="o_wforum_relative_datetime ms-2 opacity-75 small text-muted">
|
||||
</span>
|
||||
<t t-if="answer">
|
||||
<span t-if="_question_creator == _answer_creator" class="badge ms-2 border border-dark rounded-pill px-2 py-1 text-reset">
|
||||
@@ -532,7 +527,7 @@
|
||||
|
||||
<div t-if="object.website_message_ids" t-foreach="reversed(object.website_message_ids)" t-as="message" class="o_wforum_post_comments d-flex flex-column gap-2">
|
||||
<t t-set="_comment_author" t-value="message.create_uid.id"/>
|
||||
<div t-attf-class="o_wforum_post_comment d-flex rounded o_cc2 ps-3">
|
||||
<div t-attf-class="o_wforum_post_comment d-flex rounded o_cc2 ps-3" t-att-data-last-activity="message.create_date">
|
||||
<t t-set="allow_post_comment" t-value="(object.parent_id and object.parent_id.state != 'close' and object.parent_id.active != False)
|
||||
or (not object.parent_id and object.state != 'close' and object.active != False)"/>
|
||||
<div class="flex-grow-1 py-2 opacity-75">
|
||||
@@ -542,7 +537,7 @@
|
||||
<t t-set="compact" t-value="True"/>
|
||||
<t t-set="show_name" t-value="True"/>
|
||||
</span>
|
||||
<span class="ms-2 mb-2 small text-muted"><small>Just now</small></span>
|
||||
<span class="ms-2 mb-2 small text-muted"><small class="o_wforum_relative_datetime"/></span>
|
||||
<span t-if="_question_creator == _comment_author" class="badge ms-2 border border-dark rounded-pill px-2 py-1 text-reset">
|
||||
Author
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user