Holger Brunn
7 years ago
No known key found for this signature in database
GPG Key ID: 1C9760FECA3AE18
12 changed files with 356 additions and 0 deletions
-
1web_widget_date_interval/DESCRIPTION.rst
-
28web_widget_date_interval/DEVELOP.rst
-
1web_widget_date_interval/README.rst
-
4web_widget_date_interval/ROADMAP.rst
-
3web_widget_date_interval/__init__.py
-
23web_widget_date_interval/__manifest__.py
-
13web_widget_date_interval/demo/res_users.xml
-
BINweb_widget_date_interval/static/description/icon.png
-
8web_widget_date_interval/static/src/css/web_widget_date_interval.css
-
245web_widget_date_interval/static/src/js/web_widget_date_interval.js
-
21web_widget_date_interval/static/src/xml/web_widget_date_interval.xml
-
9web_widget_date_interval/views/templates.xml
@ -0,0 +1 @@ |
|||||
|
This module was written to add support for a date interval widget. |
@ -0,0 +1,28 @@ |
|||||
|
Search views |
||||
|
============ |
||||
|
|
||||
|
On search views, this addon allows developers to show a predefined range of intervals. Use ``widget="date_interval"`` and an options dictionary to configure the addon, at least a type of interval. Currently, the only supported interval type is `iso_week`: ``options="{'type': 'iso_week'}"``. |
||||
|
|
||||
|
Configuration options |
||||
|
--------------------- |
||||
|
|
||||
|
type |
||||
|
Possible values are ``iso_week`` (shows iso week numbers) |
||||
|
|
||||
|
date |
||||
|
The reference date from which to calculate intervals if different from `now` |
||||
|
|
||||
|
lookahead |
||||
|
The amount of intervals after `date` to offer to the user |
||||
|
|
||||
|
lookbehind |
||||
|
The amount of intervals before `date` to offer to the user |
||||
|
|
||||
|
exclusive |
||||
|
If truthy, selecting one interval will unselect all already selected intervals. If not set (default), selecting multiple intervals will yield a domain for all selected intervals |
||||
|
|
||||
|
cycle |
||||
|
If truthy, the currently selected interval will be used for setting the reference date. The effect is that you can cycle through intervals rapidly in combination with `exclusive = 1` and `dropdown = 1` |
||||
|
|
||||
|
dropdown |
||||
|
If truthy (default), the intervals to choose are shown as a dropdown menu |
@ -0,0 +1 @@ |
|||||
|
/ |
@ -0,0 +1,4 @@ |
|||||
|
- migrate the work done in https://github.com/OCA/web/pull/575 to 10 for the form view part |
||||
|
- support tree and kanban views |
||||
|
- support much more interval types (days, months, quarters...) |
||||
|
- invent some textual way to fill in some interval (-1w+3w for now - one week to now + three weeks) |
@ -0,0 +1,3 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2018 Therp BV <https://therp.nl> |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
@ -0,0 +1,23 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2018 Therp BV <https://therp.nl> |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
{ |
||||
|
"name": "Date interval widget", |
||||
|
"version": "10.0.1.0.0", |
||||
|
"author": "Therp BV,Odoo Community Association (OCA)", |
||||
|
"license": "AGPL-3", |
||||
|
"category": "Hidden/Dependency", |
||||
|
"summary": "Widget for date intervals", |
||||
|
"depends": [ |
||||
|
'web', |
||||
|
], |
||||
|
"demo": [ |
||||
|
"demo/res_users.xml", |
||||
|
], |
||||
|
"data": [ |
||||
|
'views/templates.xml', |
||||
|
], |
||||
|
"qweb": [ |
||||
|
'static/src/xml/web_widget_date_interval.xml', |
||||
|
], |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<odoo> |
||||
|
<record id="view_users_search" model="ir.ui.view"> |
||||
|
<field name="model">res.users</field> |
||||
|
<field name="inherit_id" ref="base.view_users_search" /> |
||||
|
<field name="arch" type="xml"> |
||||
|
<xpath expr="." position="inside"> |
||||
|
<field name="login_date" widget="date_interval" options="{'type': 'iso_week', 'dropdown': 0, 'exclusive': 1, 'cycle': 1}" /> |
||||
|
<field name="create_date" widget="date_interval" options="{'type': 'iso_week', 'lookbehind': 104, 'lookahead': 1}" /> |
||||
|
</xpath> |
||||
|
</field> |
||||
|
</record> |
||||
|
</odoo> |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
@ -0,0 +1,8 @@ |
|||||
|
div.o_date_interval > button > a |
||||
|
{ |
||||
|
color: #4c4c4c; |
||||
|
} |
||||
|
div.o_date_interval > button > a.selected |
||||
|
{ |
||||
|
font-weight: bold; |
||||
|
} |
@ -0,0 +1,245 @@ |
|||||
|
//-*- coding: utf-8 -*-
|
||||
|
//Copyright 2018 Therp BV <https://therp.nl>
|
||||
|
//License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
|
||||
|
odoo.define('web_widget_date_interval', function(require) { |
||||
|
var core = require('web.core'), |
||||
|
pyeval = require('web.pyeval'), |
||||
|
Widget = require('web.Widget'), |
||||
|
SearchView = require('web.SearchView'); |
||||
|
|
||||
|
var SearchWidgetDateInterval = Widget.extend({ |
||||
|
template: 'SearchView.DateInterval', |
||||
|
events: { |
||||
|
'click [data-id]': 'on_click_interval', |
||||
|
}, |
||||
|
_option_defaults: { |
||||
|
iso_week: { |
||||
|
lookbehind: 1, |
||||
|
lookahead: 10, |
||||
|
exclusive: false, |
||||
|
cycle: false, |
||||
|
}, |
||||
|
}, |
||||
|
options: { |
||||
|
type: 'iso_week', |
||||
|
dropdown: true, |
||||
|
}, |
||||
|
init: function(parent, field_name, string, options) { |
||||
|
this.searchview = parent; |
||||
|
this.string = string; |
||||
|
this.options = _.extend( |
||||
|
{}, this.options, this._option_defaults[options.type] || {}, |
||||
|
options |
||||
|
); |
||||
|
this.field = field_name; |
||||
|
return this._super.apply(this, arguments); |
||||
|
}, |
||||
|
start: function() { |
||||
|
this.searchview.query.on( |
||||
|
'add change remove reset', this.proxy('on_searchview_change') |
||||
|
); |
||||
|
return this._super.apply(this, arguments); |
||||
|
}, |
||||
|
get_intervals: function() { |
||||
|
var interval_function = _.str.sprintf( |
||||
|
'get_intervals_%s', this.options.type |
||||
|
); |
||||
|
if(!this[interval_function]) { |
||||
|
throw new Error('Unknown interval type given'); |
||||
|
} |
||||
|
return this[interval_function](); |
||||
|
}, |
||||
|
get_intervals_iso_week: function() { |
||||
|
var existing = this.searchview.query.findWhere({ |
||||
|
category: this.string, |
||||
|
}), |
||||
|
reference_date = moment( |
||||
|
!existing || !this.options.cycle |
||||
|
? this.options.date |
||||
|
: existing.get('field').get_domain().slice(-2, -1)[0][2] |
||||
|
), |
||||
|
start_date = reference_date.clone().startOf('isoWeek') |
||||
|
.subtract(this.options.lookbehind, 'weeks'), |
||||
|
stop_date = reference_date.clone().endOf('isoWeek') |
||||
|
.add(this.options.lookahead, 'weeks'), |
||||
|
current_date = start_date.clone(), |
||||
|
result = []; |
||||
|
|
||||
|
while(current_date.isBefore(stop_date)) { |
||||
|
result.push({ |
||||
|
name: current_date.year() === moment().year() |
||||
|
? _.str.sprintf(core._t('%s'), current_date.isoWeek()) |
||||
|
: _.str.sprintf( |
||||
|
core._t('%s / %s'), current_date.isoWeek(), |
||||
|
current_date.isoWeekYear() |
||||
|
), |
||||
|
_id: _.str.sprintf( |
||||
|
'%s-%s', this.field, current_date.format('YYYY-MM-DD') |
||||
|
), |
||||
|
start: current_date.format('YYYY-MM-DD'), |
||||
|
stop: current_date.add(1, 'weeks').format('YYYY-MM-DD'), |
||||
|
}); |
||||
|
} |
||||
|
return result; |
||||
|
}, |
||||
|
on_click_interval: function(e) { |
||||
|
var $this = jQuery(e.currentTarget); |
||||
|
return this._update_searchview( |
||||
|
$this.data('start'), $this.data('stop'), $this.text().trim() |
||||
|
); |
||||
|
}, |
||||
|
_create_facet: function(date_start, date_stop, label) { |
||||
|
var self = this; |
||||
|
|
||||
|
return { |
||||
|
field: { |
||||
|
get_domain: function() { |
||||
|
return [ |
||||
|
'&', |
||||
|
[self.field, '>=', date_start], |
||||
|
[self.field, '<', date_stop], |
||||
|
]; |
||||
|
}, |
||||
|
// eslint-disable-next-line no-empty-function
|
||||
|
get_context: function() {}, |
||||
|
// eslint-disable-next-line no-empty-function
|
||||
|
get_groupby: function() {}, |
||||
|
}, |
||||
|
category: this.string, |
||||
|
icon: 'fa-calendar', |
||||
|
values: [ |
||||
|
{ |
||||
|
label: _.str.sprintf( |
||||
|
'%s: %s', this.string, label |
||||
|
), |
||||
|
value: null, |
||||
|
}, |
||||
|
], |
||||
|
_id: _.str.sprintf( |
||||
|
'%s-%s', self.field, date_start |
||||
|
), |
||||
|
}; |
||||
|
}, |
||||
|
_update_searchview: function(date_start, date_stop, label, options) { |
||||
|
var facet = this._create_facet(date_start, date_stop, label), |
||||
|
existing = this.searchview.query.findWhere({ |
||||
|
category: this.string, |
||||
|
}); |
||||
|
if(existing) { |
||||
|
var is_removal = existing.get('_id').includes(facet._id); |
||||
|
|
||||
|
this.searchview.query.remove(existing, {silent: !is_removal}); |
||||
|
|
||||
|
if(!this.options.exclusive) { |
||||
|
// concatenate existing facet with ours
|
||||
|
var domain = [].concat( |
||||
|
['|'], facet.field.get_domain(), |
||||
|
existing.get('field').get_domain() |
||||
|
); |
||||
|
facet._id = _.str.sprintf( |
||||
|
'%s %s', facet._id, existing.get('_id') |
||||
|
); |
||||
|
facet.values = facet.values.concat(existing.get('values')); |
||||
|
facet.field.get_domain = function() { |
||||
|
return domain; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
if(is_removal) { |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
this.searchview.query.add(facet, options); |
||||
|
}, |
||||
|
on_searchview_change: function() { |
||||
|
var self = this; |
||||
|
this.$('[data-id]').removeClass('selected'); |
||||
|
if(this.options.cycle) { |
||||
|
this.renderElement(); |
||||
|
} |
||||
|
this.searchview.query.each(function(facet) { |
||||
|
if(facet.get('category') === self.string) { |
||||
|
self.$( |
||||
|
_.map( |
||||
|
facet.get('_id').split(' '), function(x) { |
||||
|
return _.str.sprintf('[data-id="%s"]', x); |
||||
|
} |
||||
|
).join(',') |
||||
|
).addClass('selected'); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
facet_for_defaults: function(values) { |
||||
|
var self = this, |
||||
|
default_value = values[_.str.sprintf( |
||||
|
'date_interval_%s', this.field |
||||
|
)], |
||||
|
facet = false; |
||||
|
|
||||
|
if(!default_value) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
default_value = moment(default_value); |
||||
|
|
||||
|
_(this.get_intervals()).each(function(interval) { |
||||
|
if( |
||||
|
!moment(interval.start).isBefore(default_value) || |
||||
|
!moment(interval.stop).isAfter(default_value) |
||||
|
) { |
||||
|
return; |
||||
|
} |
||||
|
facet = self._create_facet( |
||||
|
interval.start, interval.stop, interval.name |
||||
|
); |
||||
|
}); |
||||
|
return facet; |
||||
|
}, |
||||
|
visible: function() { |
||||
|
return false; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
SearchView.include({ |
||||
|
init: function() { |
||||
|
this._super.apply(this, arguments); |
||||
|
this.date_intervals = []; |
||||
|
}, |
||||
|
start: function() { |
||||
|
var self = this, |
||||
|
deferreds = [this._super.apply(this, arguments)]; |
||||
|
|
||||
|
if(this.$buttons && !this.options.disable_date_interval) { |
||||
|
_(this.date_intervals).each(function(widget) { |
||||
|
deferreds.push(widget.appendTo(self.$buttons)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return jQuery.when.apply(jQuery, deferreds); |
||||
|
}, |
||||
|
prepare_search_inputs: function() { |
||||
|
this._super.apply(this, arguments); |
||||
|
|
||||
|
var self = this; |
||||
|
|
||||
|
_.chain(this.fields_view.arch.children) |
||||
|
.filter(function(x) { |
||||
|
return x.tag === 'field' && x.attrs.widget === 'date_interval'; |
||||
|
}) |
||||
|
.each(function(x) { |
||||
|
var widget = new SearchWidgetDateInterval( |
||||
|
self, x.attrs.name, core._t(x.attrs.string) || |
||||
|
self.ViewManager.search_fields_view.fields[x.attrs.name] |
||||
|
.string, pyeval.py_eval(x.attrs.options || '{}') |
||||
|
); |
||||
|
self.date_intervals.push(widget); |
||||
|
self.search_fields.push(widget); |
||||
|
}); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
return { |
||||
|
search_widget_date_interval: SearchWidgetDateInterval, |
||||
|
}; |
||||
|
}); |
@ -0,0 +1,21 @@ |
|||||
|
<templates> |
||||
|
<div t-name="SearchView.DateInterval" class="btn-group o_date_interval o_dropdown"> |
||||
|
<button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" t-att-data-toggle="widget.options.dropdown and 'dropdown' or ''"> |
||||
|
<span class="fa fa-calendar"></span> |
||||
|
<t t-esc="widget.string" /> |
||||
|
<span t-if="widget.options.dropdown" class="caret" /> |
||||
|
<t t-else="" t-att-data-date_interval="widget.field"> |
||||
|
<t t-foreach="widget.get_intervals()" t-as="interval"> |
||||
|
<a t-att-data-id="interval._id" t-att-data-start="interval.start" t-att-data-stop="interval.stop" href="#"><t t-esc="interval.name" /></a> |
||||
|
</t> |
||||
|
</t> |
||||
|
</button> |
||||
|
<ul t-if="widget.options.dropdown" class="dropdown-menu o_filters_menu" role="menu" t-att-data-date_interval="widget.field"> |
||||
|
<t t-foreach="widget.get_intervals()" t-as="interval"> |
||||
|
<li t-att-data-id="interval._id" t-att-data-start="interval.start" t-att-data-stop="interval.stop"> |
||||
|
<a href="#"><t t-esc="interval.name" /></a> |
||||
|
</li> |
||||
|
</t> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</templates> |
@ -0,0 +1,9 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<odoo> |
||||
|
<template id="assets_backend" name="web_widget_date_interval assets" inherit_id="web.assets_backend"> |
||||
|
<xpath expr="." position="inside"> |
||||
|
<script type="text/javascript" src="/web_widget_date_interval/static/src/js/web_widget_date_interval.js"></script> |
||||
|
<link rel="stylesheet" href="/web_widget_date_interval/static/src/css/web_widget_date_interval.css"/> |
||||
|
</xpath> |
||||
|
</template> |
||||
|
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue