[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:
Lopes Marc (loma)
2023-07-18 16:58:44 +02:00
committed by Florian Charlier
co-authored by Florian Charlier
parent 3d1d5bfb09
commit 280537b9fe
9 changed files with 39 additions and 75 deletions
+1 -1
View File
@@ -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
# --------------------------------------------------
+2 -6
View File
@@ -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 '
+10 -14
View File
@@ -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>