Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Master website chatter jem #15405

Merged
merged 10 commits into from Jun 30, 2017
Merged

Conversation

jem-odoo
Copy link
Contributor

@jem-odoo jem-odoo commented Feb 8, 2017

Task : https://www.odoo.com/web#id=30059&view_type=form&model=project.task&action=333&active_id=809&menu_id=4720

Purpose :
In all website modules (blog, ecommerce, ...) we can post comments. But having a huge number of comments can be problemtic, since they all will be displayed on the same page (loading page time can be .... long).
The udemy comment zone is quite smart (see https://www.udemy.com/learn-html5-programming-from-scratch/ for instance). There is a pager for comments, and you can filter the rating (ecommerce support rating).

--
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr

@jem-odoo jem-odoo added the RD research & development, internal work label Feb 8, 2017

def _special_access_object(res_model, res_id, token='', token_field=''):
record = request.env[res_model].browse(res_id)
if token and record and token == getattr(record.sudo(), token_field, None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use consteq to compare tokens ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but it have to check than the token_field is filled to avoid given None as argument to consteq. So the condition looks like :
if token and record and getattr(record.sudo(), token_field, None) and consteq(getattr(record.sudo(), token_field, None), str(token)):

@@ -27,7 +33,9 @@ def _message_post_helper(res_model='', res_id=None, message='', token='', token_
"""
record = request.env[res_model].browse(res_id)
author_id = request.env.user.partner_id.id if request.env.user.partner_id else False
if token and record and token == getattr(record.sudo(), token_field, None):

access_as_sudo = _special_access_object(res_model, res_id, token=token, token_field=token_field)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we do something if the token is not correct ? If I understand well the record will not be accessed as sudo, but maybe it could just avoid doing stuff if a wrong token is given.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like raising a forbidden ? It could be cool indeed !

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so do we do it ? does it make sense or is it overkill ?

return request.redirect(url)

def _website_message_post(self, res_model, res_id, message, **kw):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of that wrapper ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a hook, to create the rating in website_rating. Otherwise, super will return the url, and don't have the message_id to associate the rating with.
Maybe _message_post_helper should be a controller method. What do you think ?

@http.route(['/website_mail/post/post'], type='http', methods=['POST'], auth='public', website=True)
def chatter_post(self, res_model='', res_id=None, message='', redirect=None, **kw):
def chatter_post(self, res_model, res_id, message, **kw):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a naming thing: chatter_post, website_message_post, message_post_helper, website_chatter_init, website_message_fetch -> there is some space for prefixing convention cleaning ;) .


@http.route('/website_mail/fetch', type='json', auth='public', website=True)
def website_message_fetch(self, res_model, res_id, domain=False, limit=False, offset=False, **kw):
if not limit:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not putting it directly in the parameters ?

# Only search into website_message_ids, so apply the same domain to perform only one search
# extract domain from the 'website_message_ids' field
field_domain = request.env[res_model]._fields['website_message_ids'].domain
domain += field_domain(request.env[res_model].browse(res_id)) if callable(field_domain) else field_domain
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I don't think we have to browse res_id. Indeed domains are only model dependent, not record dependent at the ORM level. I think we should keep the same semantic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smart guy !

domain += [('res_id', '=', res_id)]
# Check access
Message = request.env['mail.message']
access_as_sudo = _special_access_object(res_model, res_id, token=kw.get('token'), token_field=kw.get('token_field'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example here, shouldn't a wrong token send you to hell, instead of probably crashing due to access rights issues ?

Copy link
Contributor

@tde-banana-odoo tde-banana-odoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technical review. Ow.

},
fetch_message: function(domain){
var self = this;
var data = this._prepare_fetch_message_params();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary variable

_load_templates: function(){
return ajax.loadXML('/website_mail/static/src/xml/website_mail.xml', qweb);
},
fetch_message: function(domain){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use the same naming convention, and use message_fetch, message_fetch_prepare, ... ?

init: function(parent, options){
this._super.apply(this, arguments);
console.log($(parent).data());
this.options = _.defaults($(parent).data(), {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange construct, where does it come from ?

});
return messages;
},
_pager: function(page, total, step, scope){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would either remove parameters or explicitely set them to this.get(parameter) when calling the function, to lessen the number of variable processing.

var pmin = Math.max(page - parseInt(Math.floor(scope/2)), 1);
var pmax = Math.min(pmin + scope, page_count);

if(pmax - pmin < scope){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whut ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I simply transform the algo from python side. Without trying to understand ;)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, looks like the python side did the work twice because it wasn't sure it did it well ?

It looks like it's a verification of the min/max done above :p It shouldn't ever be useful

message.rating_value = mapping.get(message.id, 0.0)

def _search_rating_value(self, operator, value):
self.env.cr.execute("""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fear it will become quickly hard to perform a search. Once you have a lot of ratings, you will have a domain term like [('id', 'in', [A MAGNIFICIENT ARRAY])]. Don't you think we should store the rating value in order to search efficiently on it ?

this.change_current_page(1, domain);
},
// utils
round_to_half: function(value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a JS expert, but don't we have access to some method allowing to do it :p ?

if(this.get('rating_value')){
domain = [['rating_value', '=', this.get('rating_value')]];
}
console.log('change to ',this.get('rating_value'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover log.

@@ -1,5 +1,4 @@
import ir_http
import rating
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmmh ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normal, since I have moved this file from website_sale ;)

@@ -2,6 +2,12 @@
<odoo>
<data noupdate="1">

<record id="mail_message_rule_public" model="ir.rule">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is some mismatch in the commits. One commit add a hardcoded domain, one commit adds a void file, and this one finishes the work by removing the hardcoded domain and adding stuff in the void file. I fear you will have to do strange stuff when rebasing. Bon courage.

@jem-odoo jem-odoo force-pushed the master-website-chatter-jem branch 4 times, most recently from 5c794d7 to 839be6a Compare March 23, 2017 14:18
@jem-odoo jem-odoo force-pushed the master-website-chatter-jem branch 3 times, most recently from 217c6bf to 5fdac06 Compare May 12, 2017 13:35
@jem-odoo jem-odoo force-pushed the master-website-chatter-jem branch 5 times, most recently from 5bafd83 to e934cde Compare May 23, 2017 14:56

def _special_access_object(res_model, res_id, token='', token_field=''):
record = request.env[res_model].browse(res_id)
if token and record and getattr(record.sudo(), token_field, None) and consteq(getattr(record.sudo(), token_field, None), token):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should remove the None from the second getattr(record.sudo(), token_field, None)

We already know it exists so no default is needed, and should we remove the verification before somehow, consteq raises a TypeError with a None argument.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also do a .exists() on the record, and maybe put the .sudo() on the first line too.

@@ -27,7 +33,9 @@ def _message_post_helper(res_model='', res_id=None, message='', token='', token_
"""
record = request.env[res_model].browse(res_id)
author_id = request.env.user.partner_id.id if request.env.user.partner_id else False
if token and record and token == getattr(record.sudo(), token_field, None):

access_as_sudo = _special_access_object(res_model, res_id, token=token, token_field=token_field)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so do we do it ? does it make sense or is it overkill ?

//--------------------------------------------------------------------------

/**
* Fetch the messages nad the message count from the server for the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nad ^^

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oupssss

*
* @private
* @params {Number} page
* @returns {Object}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional parameters are set in brackets @param {number} [page]

(+ param singular and JS types don't start with a capital letters :p )

var step = this.options['pager_step'];

// Compute Pager
var page_count = parseInt(Math.ceil(parseFloat(total) / step));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the parseInt and parseFloat are necessary here, unless this.get('message_count') is a string, but I don't think it can be one.

if(domain){
d = domain;
}
this.set('domain', d); // trigger fetch message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we set this.get('domain') to a clone of itself, or why we clone the this.get('domain') if we're setting it to the domain parameter.

If it's to trigger the domain onchange, maybe we could just the method instead of cloning it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we keep the onchange triggers this way: var d = (domain ? domain : _.clone(this.get('domain'))

But imo changing the domain should change the page (or force its onchange if no page change), and changing the page should trigger the message fetching.

var pmin = Math.max(page - parseInt(Math.floor(scope/2)), 1);
var pmax = Math.min(pmin + scope, page_count);

if(pmax - pmin < scope){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, looks like the python side did the work twice because it wasn't sure it did it well ?

It looks like it's a verification of the min/max done above :p It shouldn't ever be useful

<input type='hidden' name="sha_time" t-att-value="widget.options['sha_time']" t-if="widget.options['sha_time']"/>
<input type='hidden' name="token_field" t-att-value="widget.options['token_field']" t-if="widget.options['token_field']"/>
<div class="alert alert-danger mt8 mb0 o_website_mail_composer_error" style="display:none;">
Oops! Something went wrong. Try to reload the page and to log in.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotto make a choice "and log in" or "to log in" but not "and to log in" ^^

/**
* Update the messages format
*
* @params {Array<Object>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@param

comment to propagate in this file too

* @private
* @param {MouseEvent} event
*/
_onMoveStar: function(e){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while testing the stars on the website, I found it a bit odd that after you click a certain rating, if you happen to hover on another star, your clicked rating gets lost.

Also, if you send a rating without any message, you get a refresh, but the rating doesn't go through. It'd be better if it didn't submit but had something like when you don't fill a required field on a form.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and it's done !

Copy link
Contributor Author

@jem-odoo jem-odoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first batch of answers

is_user_public = bool(request.env.user == request.website.user_id)
message_data = self.website_message_fetch(res_model, res_id, domain=domain, limit=limit, **kwargs)
display_composer = False
if kwargs.get('allow_composer'):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are very optional parameters, so not really necessary, IMHO.

}

@http.route('/website_mail/fetch', type='json', auth='public', website=True)
def website_message_fetch(self, res_model, res_id, domain=False, limit=10, offset=0, **kw):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same answer here ;)

//--------------------------------------------------------------------------

/**
* Fetch the messages nad the message count from the server for the
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oupssss

</ul>
</section>
<div id="discussion" class="hidden-print o_website_mail_thread"
t-att-data-token="token" t-att-public-user="website.env.user == request.website.user_id" t-att-data-res_model="object._name" t-att-data-res_id="object.id" t-att-data-token_field="token_field" t-att-data-pager_step="message_per_page or 10" t-att-data-allow_composer="'0' if disable_composer else '1'">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting t-att-token=="object[token_field]" will crash if token_field is not set. And since we usually use the template without the token mecanism, it should be better like this ;)

@odoo odoo deleted a comment from tde-banana-odoo Jun 29, 2017
Copy link
Contributor Author

@jem-odoo jem-odoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

second

* @param {Number} page
* @param {Array} domain
*/
_changeCurrentPage: function(page, domain){
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that for this, but when changing domain, I have to reset the current_page to trigger the messages fetch. But if I set the current_page to 1, and I'm already on the page one, replacing 1 by 1 does not trigger the fetching. So I have to use the domain, which is an object to trigger the fetching.

* @private
* @param {MouseEvent} event
*/
_onMoveStar: function(e){
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and it's done !

@odoo odoo deleted a comment from Icallhimtest Jun 29, 2017
@odoo odoo deleted a comment from Icallhimtest Jun 29, 2017
mail.thread mixin will (re)provide the field 'website_message_ids'.
Models can redifined its domain, since it can differ a bit (blog, forum,
... are not the same).
Even if the domain can differ, we still keep it on the mixin since
it will be required for the new chatter frontend (see following
commits).
Message are now fetched and display using javascript
to optimize performance.
Also the chatter is completely rewrite : add pager,
filter possiblity, ...

This commit is mainly technical: posting feature does
not change. Only the display with pager is new.
The goal is also to prepare frontend chatter for a
better integration with rating widget.

The mail.thread mixin is extended to manage the field
website_message_ids (display all messages that can be
seen on frontend by user (employee or not).

When fetching or posting message, a check is done to see
if we need to do it as sudo(). website_mail implement posting
messages with token or sha_in (using in website_quote).
The same verification is now done when fetching messages, via
'_special_access_object' method.
rating module provide now post a message with a
rating value, though `message_post` method.
'mail.message' also has now a `rating_value` field
to ease searching and filtering.

This will help the ecommerce (and other website modules)
to post a comment associate with a rating.
Extend new dynamic chatter to manage rating
when posting comment.

The model should inherit of rating.mixin and
its frontend chatter should allow rating to enjoy
star widget and rating card.

This is more generic : stats, rating, ... are manage
in one place (this module) for all document.
Slide provided a chatter where you can post message as
public user. We decided to drop that feature in order
to avoid spamming.
Like for all other document, you must at least, be portal
user to post a comment.
Reusing the standart chatter highly simplify the code.
This commit brings a new frontend chatter.
The old one displayed all message on the website
page, and that could be very slow when many many
messages are posted on a document.

The new chatter is more dynamic: we introduce
a pager, to fetch and display only a few messages.
It also still support to post a message if you are
not logged in, using a token mecanism (see
website_quote).

Moreover, a better integration with rating: in the
eshop, when posting a comment, the visitor can add
a rating value (stars).
@jem-odoo jem-odoo merged commit 66bd45a into odoo:master Jun 30, 2017
@jem-odoo jem-odoo deleted the master-website-chatter-jem branch December 12, 2017 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
RD research & development, internal work
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants