[FIX] web_editor, website: handle date edition in all lang and format

Before d00c0e317, `Datetime` fields would be editable in frontend but would
have unexpected results, especially in non-English languages, for when the
english lang format had been changed.
It would also crash when saving non-English strings, such as `Lundi`.
For more details, see https://github.com/odoo/odoo/pull/44484#issuecomment-586850490

Since d00c0e317, only date displayed in lang format would be editable, which
case is Event page in Odoo 12.0. Everywhere else, the format is changed for a
nicer layout, either with `widget=XXX` or `t-options=YYY`, such as:
`<time t-field="record.date" t-options='{"format": "MMM d, yyyy"}'/>`
`<time t-field="record.date" t-options="{'time_only': 'true', 'format': 'short'}"/>`

When a date parsing crashes during editor save, the problem is not only that
the date can be saved, but the whole changes of the page are lost, as they
won't be saved either.

This commit attempts to fix every languages cases, regardless of the website
lang or user lang.
To do so, we store the date in the user lang format in a data attribute of
every date field in the DOM. Once the field is clicked (to edit probably),
that value will replace the one displayed according to the widget/options.
That way, dates will always be sent to the server in the user lang format,
avoiding any possible mismatch.

This whole fix apply to `Datetime` and `Date` fields.

opw-2183055
Closes #44484
Closes #45555
Fixes #44047

closes odoo/odoo#45725

closes odoo/odoo#45997

X-original-commit: c0db9bb8027ff926c619aebe6e0a5cdd05808232
Original-signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
Co-authored-by: Romain Derie <rde@odoo.com>
Co-authored-by: Jeremy Kersten <jke@odoo.com>
This commit is contained in:
Romain Derie
2020-02-21 18:57:59 +00:00
co-authored by Jeremy Kersten
parent 4a6d3ae934
commit 4eafd78847
3 changed files with 55 additions and 8 deletions
+37 -6
View File
@@ -9,6 +9,7 @@ Also, adds methods to convert values back to Odoo models.
"""
import ast
import babel
import base64
import io
import itertools
@@ -21,6 +22,7 @@ from datetime import datetime
import pytz
import requests
from datetime import datetime
from lxml import etree, html
from PIL import Image as I
from werkzeug import urls
@@ -28,7 +30,7 @@ from werkzeug import urls
import odoo.modules
from odoo import api, models, fields
from odoo.tools import ustr, pycompat
from odoo.tools import ustr, posix_to_ldml, pycompat
from odoo.tools import html_escape as escape
from odoo.addons.base.models import ir_qweb
@@ -203,6 +205,21 @@ class Date(models.AbstractModel):
attrs = super(Date, self).attributes(record, field_name, options, values)
if options.get('inherit_branding'):
attrs['data-oe-original'] = record[field_name]
if record._fields[field_name].type == 'datetime':
attrs = self.env['ir.qweb.field.datetime'].attributes(record, field_name, options, values)
attrs['data-oe-type'] = 'datetime'
return attrs
lg = self.env['res.lang']._lang_get(self.env.user.lang)
locale = babel.Locale.parse(lg.code)
babel_format = value_format = posix_to_ldml(lg.date_format, locale=locale)
if record[field_name]:
date = fields.Date.from_string(record[field_name])
value_format = pycompat.to_text(babel.dates.format_date(date, format=babel_format, locale=locale))
attrs['data-oe-original-with-format'] = value_format
return attrs
@api.model
@@ -211,7 +228,9 @@ class Date(models.AbstractModel):
if not value:
return False
return value
lg = self.env['res.lang']._lang_get(self.env.user.lang)
date = datetime.strptime(value, lg.date_format)
return fields.Date.to_string(date)
class DateTime(models.AbstractModel):
@@ -222,15 +241,27 @@ class DateTime(models.AbstractModel):
@api.model
def attributes(self, record, field_name, options, values):
attrs = super(DateTime, self).attributes(record, field_name, options, values)
if options.get('inherit_branding'):
value = record[field_name]
lg = self.env['res.lang']._lang_get(self.env.user.lang)
locale = babel.Locale.parse(lg.code)
babel_format = value_format = posix_to_ldml('%s %s' % (lg.date_format, lg.time_format), locale=locale)
tz = record.env.context.get('tz') or self.env.user.tz
if isinstance(value, str):
value = fields.Datetime.from_string(value)
if value:
# convert from UTC (server timezone) to user timezone
value = fields.Datetime.context_timestamp(self, timestamp=value)
value = fields.Datetime.context_timestamp(self.with_context(tz=tz), timestamp=value)
value_format = pycompat.to_text(babel.dates.format_datetime(value, format=babel_format, locale=locale))
value = fields.Datetime.to_string(value)
attrs['data-oe-original'] = value
attrs['data-oe-original-with-format'] = value_format
attrs['data-oe-original-tz'] = tz
return attrs
@api.model
@@ -240,11 +271,11 @@ class DateTime(models.AbstractModel):
return False
# parse from string to datetime
date_format = self.env['res.lang']._lang_get(self.env.user.lang).date_format + ' %H:%M'
dt = datetime.strptime(value, date_format)
lg = self.env['res.lang']._lang_get(self.env.user.lang)
dt = datetime.strptime(value, '%s %s' % (lg.date_format, lg.time_format))
# convert back from user's timezone to UTC
tz_name = self.env.context.get('tz') or self.env.user.tz
tz_name = element.attrib.get('data-oe-original-tz') or self.env.context.get('tz') or self.env.user.tz
if tz_name:
try:
user_tz = pytz.timezone(tz_name)
+16 -1
View File
@@ -482,7 +482,7 @@ var RTEWidget = Widget.extend({
$('.o_editable')
.destroy()
.removeClass('o_editable o_is_inline_editable');
.removeClass('o_editable o_is_inline_editable o_editable_date_field_linked o_editable_date_field_format_changed');
var $dirty = $('.o_dirty');
$dirty
@@ -550,6 +550,17 @@ var RTEWidget = Widget.extend({
* @param {jQuery} $editable
*/
_enableEditableArea: function ($editable) {
if ($editable.data('oe-type') === "datetime" || $editable.data('oe-type') === "date") {
var selector = '[data-oe-id="' + $editable.data('oe-id') + '"]';
selector += '[data-oe-field="' + $editable.data('oe-field') + '"]';
selector += '[data-oe-model="' + $editable.data('oe-model') + '"]';
var $linkedFieldNodes = this.editable().find(selector).addBack(selector);
$linkedFieldNodes.not($editable).addClass('o_editable_date_field_linked');
if (!$editable.hasClass('o_editable_date_field_format_changed')) {
$linkedFieldNodes.html($editable.data('oe-original-with-format'));
$linkedFieldNodes.addClass('o_editable_date_field_format_changed');
}
}
if ($editable.data('oe-type') === "monetary") {
$editable.attr('contenteditable', false);
$editable.find('.oe_currency_value').attr('contenteditable', true);
@@ -664,6 +675,9 @@ var RTEWidget = Widget.extend({
var $target = $(ev.target);
var $editable = $target.closest('.o_editable');
if (this && this.$last && this.$last.length && this.$last[0] !== $target[0]) {
$('.o_editable_date_field_linked').removeClass('o_editable_date_field_linked');
}
if (!$editable.length || $.summernote.core.dom.isContentEditableFalse($target)) {
return;
}
@@ -716,6 +730,7 @@ var RTEWidget = Widget.extend({
clearTimeout(lastTimerId);
}
}
if ($editable.length && (!this.$last || this.$last[0] !== $editable[0])) {
$editable.summernote(this._getConfig($editable));
@@ -20,7 +20,8 @@ $-editor-messages-margin-x: 2%;
.o_editable {
&:not(:empty), &[data-oe-type] {
&:not([data-oe-model="ir.ui.view"]):not([data-oe-type="html"]):not(.o_editable_no_shadow):hover {
&:not([data-oe-model="ir.ui.view"]):not([data-oe-type="html"]):not(.o_editable_no_shadow):hover,
&.o_editable_date_field_linked {
box-shadow: $o-brand-odoo 0 0 5px 2px inset;
}
}