[IMP] groupby domains & unformated dates

* ensure groupby domains are normalised, that's significantly more
  convenient for subsequent manipulations and combinations (e.g. OR-ing
  domains of multiple groups)
* split off "raw" groupby which provides ISO-8601 date and datetime
  ranges[0] (half-open) as values for grouped date/datetime fields

  for instance when grouping weekwise on a date field, the group
  including May 11th, 1930 would have the following value for
  the (grouped) date field: `("1930-05-05/1930-05-12", "W19 1930")`[1]

  this means callers can access the programmatic date range value
* fix website_mail_channel and website_blog to *not* go and randomly dig
  into the domains assuming a bunch of unspecified stuff about its
  formatting (such as it not being normalized)

[0] https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
[1] actual week number turns out to be locale-dependent...
This commit is contained in:
Xavier Morel
2016-03-29 10:34:25 +02:00
parent 2fea143b06
commit bc4d10f48d
3 changed files with 115 additions and 45 deletions
+19 -9
View File
@@ -50,18 +50,28 @@ class WebsiteBlog(http.Controller):
_post_comment_per_page = 10
def nav_list(self, blog=None):
blog_post_obj = request.registry['blog.post']
domain = blog and [('blog_id', '=', blog.id)] or []
groups = blog_post_obj.read_group(
request.cr, request.uid, domain, ['name', 'create_date'],
groupby="create_date", orderby="create_date desc", context=request.context)
blog_post_obj = request.env['blog.post']
groups = blog_post_obj._read_group_raw(
blog and [('blog_id', '=', blog.id)] or [],
['name', 'create_date'],
groupby=["create_date"], orderby="create_date desc")
for group in groups:
begin_date = datetime.datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
end_date = datetime.datetime.strptime(group['__domain'][1][2], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
group['date_begin'] = '%s' % datetime.date.strftime(begin_date, tools.DEFAULT_SERVER_DATE_FORMAT)
group['date_end'] = '%s' % datetime.date.strftime(end_date, tools.DEFAULT_SERVER_DATE_FORMAT)
(r, label) = group['create_date']
start, end = r.split('/')
group['create_date'] = label
group['date_begin'] = self._to_date(start)
group['date_end'] = self._to_date(end)
return groups
def _to_date(self, dt):
""" create_date is a datetime so start and end are datetime strings,
but we just want date strings
"""
return (datetime.datetime
.strptime(dt, tools.DEFAULT_SERVER_DATETIME_FORMAT)
.date() # may be unnecessary?
.strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
@http.route([
'/blog',
'/blog/page/<int:page>',
@@ -14,17 +14,27 @@ class MailGroup(http.Controller):
_replies_per_page = 10
def _get_archives(self, group_id):
MailMessage = request.registry['mail.message']
groups = MailMessage.read_group(
request.cr, request.uid, [('model', '=', 'mail.channel'), ('res_id', '=', group_id)], ['subject', 'date'],
groupby="date", orderby="date desc", context=request.context)
MailMessage = request.env['mail.message']
groups = MailMessage._read_group_raw(
[('model', '=', 'mail.channel'), ('res_id', '=', group_id)], ['subject', 'date'],
groupby=["date"], orderby="date desc")
for group in groups:
begin_date = datetime.datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
end_date = datetime.datetime.strptime(group['__domain'][1][2], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
group['date_begin'] = '%s' % datetime.date.strftime(begin_date, tools.DEFAULT_SERVER_DATE_FORMAT)
group['date_end'] = '%s' % datetime.date.strftime(end_date, tools.DEFAULT_SERVER_DATE_FORMAT)
(r, label) = group['date']
start, end = r.split('/')
group['date'] = label
group['date_begin'] = self._to_date(start)
group['date_end'] = self._to_date(end)
return groups
def _to_date(self, dt):
""" date is (of course) a datetime so start and end are datetime
strings, but we just want date strings
"""
return (datetime.datetime
.strptime(dt, tools.DEFAULT_SERVER_DATETIME_FORMAT)
.date() # may be unnecessary?
.strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
@http.route("/groups", type='http', auth="public", website=True)
def view(self, **post):
cr, uid, context = request.cr, request.uid, request.context
+78 -28
View File
@@ -2058,37 +2058,68 @@ class BaseModel(object):
value = pytz.timezone(context['tz']).localize(value)
return value
def _read_group_get_domain(self, groupby, value):
"""
Helper method to construct the domain corresponding to a groupby and
a given value. This is mostly relevant for date/datetime.
"""
if groupby['type'] in ('date', 'datetime') and value:
dt_format = DEFAULT_SERVER_DATETIME_FORMAT if groupby['type'] == 'datetime' else DEFAULT_SERVER_DATE_FORMAT
domain_dt_begin = value
domain_dt_end = value + groupby['interval']
if groupby['tz_convert']:
domain_dt_begin = domain_dt_begin.astimezone(pytz.utc)
domain_dt_end = domain_dt_end.astimezone(pytz.utc)
return [(groupby['field'], '>=', domain_dt_begin.strftime(dt_format)),
(groupby['field'], '<', domain_dt_end.strftime(dt_format))]
if groupby['type'] == 'many2one' and value:
value = value[0]
return [(groupby['field'], '=', value)]
def _read_group_format_result(self, data, annotated_groupbys, groupby, groupby_dict, domain, context):
def _read_group_format_result(self, data, annotated_groupbys, groupby, domain, context):
"""
Helper method to format the data contained in the dictionary data by
adding the domain corresponding to its values, the groupbys in the
context and by properly formatting the date/datetime values.
"""
domain_group = [dom for gb in annotated_groupbys for dom in self._read_group_get_domain(gb, data[gb['groupby']])]
for k,v in data.iteritems():
gb = groupby_dict.get(k)
if gb and gb['type'] in ('date', 'datetime') and v:
data[k] = babel.dates.format_date(v, format=gb['display_format'], locale=context.get('lang', 'en_US'))
context and by properly formatting the date/datetime values.
data['__domain'] = domain_group + domain
:param data: a single group
:param annotated_groupbys: expanded grouping metainformation
:param groupby: original grouping metainformation
:param domain: original domain for read_group
"""
sections = []
for gb in annotated_groupbys:
ftype = gb['type']
value = data[gb['groupby']]
# full domain for this groupby spec
d = None
if value:
if ftype == 'many2one':
value = value[0]
elif ftype in ('date', 'datetime'):
locale = context.get('lang', 'en_US')
fmt = DEFAULT_SERVER_DATETIME_FORMAT if ftype == 'datetime' else DEFAULT_SERVER_DATE_FORMAT
tzinfo = None
range_start = value
range_end = value + gb['interval']
# value from postgres is in local tz (so range is
# considered in local tz e.g. "day" is [00:00, 00:00[
# local rather than UTC which could be [11:00, 11:00]
# local) but domain and raw value should be in UTC
if gb['tz_convert']:
tzinfo = range_start.tzinfo
range_start = range_start.astimezone(pytz.utc)
range_end = range_end.astimezone(pytz.utc)
range_start = range_start.strftime(fmt)
range_end = range_end.strftime(fmt)
if ftype == 'datetime':
label = babel.dates.format_datetime(
value, format=gb['display_format'],
tzinfo=tzinfo, locale=locale
)
else:
label = babel.dates.format_date(
value, format=gb['display_format'],
locale=locale
)
data[gb['groupby']] = ('%s/%s' % (range_start, range_end), label)
d = [
'&',
(gb['field'], '>=', range_start),
(gb['field'], '<', range_end),
]
if d is None:
d = [(gb['field'], '=', value)]
sections.append(d)
sections.append(domain)
data['__domain'] = expression.AND(sections)
if len(groupby) - len(annotated_groupbys) >= 1:
data['__context'] = { 'group_by': groupby[len(annotated_groupbys):]}
del data['id']
@@ -2126,6 +2157,25 @@ class BaseModel(object):
:raise AccessError: * if user has no read rights on the requested object
* if user tries to bypass access rules for read on the requested object
"""
if context is None:
context = context
result = self._read_group_raw(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy)
groupby = [groupby] if isinstance(groupby, basestring) else groupby
dt = [
f for f in groupby
if self._fields[f.split(':')[0]].type in ('date', 'datetime')
]
# iterate on all results and replace the "full" date/datetime value
# (range, label) by just the formatted label, in-place
for group in result:
for df in dt:
if df in group:
group[df] = group[df][1]
return result
def _read_group_raw(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
if context is None:
context = {}
self.check_access_rights(cr, uid, 'read')
@@ -2215,7 +2265,7 @@ class BaseModel(object):
d.update(data_dict[d['id']])
data = map(lambda r: {k: self._read_group_prepare_data(k,v, groupby_dict, context) for k,v in r.iteritems()}, fetched_data)
result = [self._read_group_format_result(d, annotated_groupbys, groupby, groupby_dict, domain, context) for d in data]
result = [self._read_group_format_result(d, annotated_groupbys, groupby, domain, context) for d in data]
if lazy and groupby_fields[0] in self._group_by_full:
# Right now, read_group only fill results in lazy mode (by default).
# If you need to have the empty groups in 'eager' mode, then the