From c67e8532d715bff67719977ecc96dfe1be660a4b Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Mon, 27 Apr 2015 16:36:48 +0200 Subject: [PATCH] Finalyse POC.... --- __openerp__.py | 5 +- project_view.xml | 28 + static/src/css/web_timeline.css | 17 + static/src/js/web_timeline.js | 1369 +++++-------------------------- static/src/xml/web_timeline.xml | 75 +- views/web_timeline.xml | 2 +- 6 files changed, 266 insertions(+), 1230 deletions(-) create mode 100644 project_view.xml diff --git a/__openerp__.py b/__openerp__.py index b6e921ba..bf205575 100644 --- a/__openerp__.py +++ b/__openerp__.py @@ -7,9 +7,10 @@ "author": "ACSONE SA/NV", "category": "Acsone", "website": "http://acsone.eu", - 'depends': ['web'], - 'qweb': ['static/src/xml/timeline.xml'], + 'depends': ['web', 'project'], + 'qweb': ['static/src/xml/web_timeline.xml'], 'data': [ 'views/web_timeline.xml', + 'project_view.xml', ], } diff --git a/project_view.xml b/project_view.xml new file mode 100644 index 00000000..7416f322 --- /dev/null +++ b/project_view.xml @@ -0,0 +1,28 @@ + + + + + + project.task.timeline + project.task + timeline + + + + + + + + + Tasks + project.task + kanban,tree,form,calendar,gantt,timeline,graph + + + + + diff --git a/static/src/css/web_timeline.css b/static/src/css/web_timeline.css index e69de29b..95cb7de8 100644 --- a/static/src/css/web_timeline.css +++ b/static/src/css/web_timeline.css @@ -0,0 +1,17 @@ +/* Button style */ +.openerp .oe_view_manager .oe_view_manager_switch .oe_vm_switch_timeline:after { + content: "N"; +} +.timeline-navigation-zoom-in .ui-icon{ + background: none !important; +} + +.timeline-navigation-zoom-out .ui-icon{ + background: none !important; +} +.timeline-navigation-move-left .ui-icon{ + background: none !important; +} +.timeline-navigation-move-right .ui-icon{ + background: none !important; +} \ No newline at end of file diff --git a/static/src/js/web_timeline.js b/static/src/js/web_timeline.js index 46f8ee1b..10ddc8d8 100644 --- a/static/src/js/web_timeline.js +++ b/static/src/js/web_timeline.js @@ -1,5 +1,5 @@ /*--------------------------------------------------------- - * OpenERP web_calendar + * OpenERP web_timeline *---------------------------------------------------------*/ _.str.toBoolElse = function (str, elseValues, trueValues, falseValues) { @@ -10,70 +10,21 @@ _.str.toBoolElse = function (str, elseValues, trueValues, falseValues) { return ret; }; -openerp.web_calendar = function(instance) { +openerp.web_timeline = function(instance) { var _t = instance.web._t, _lt = instance.web._lt, QWeb = instance.web.qweb; - function get_class(name) { - return new instance.web.Registry({'tmp' : name}).get_object("tmp"); - } - - function get_fc_defaultOptions() { - shortTimeformat = Date.CultureInfo.formatPatterns.shortTime; - var dateFormat = Date.normalizeFormat(instance.web.strip_raw_chars(_t.database.parameters.date_format)); - return { - weekNumberTitle: _t("W"), - allDayText: _t("All day"), - buttonText : { - today: _t("Today"), - month: _t("Month"), - week: _t("Week"), - day: _t("Day") - }, - monthNames: Date.CultureInfo.monthNames, - monthNamesShort: Date.CultureInfo.abbreviatedMonthNames, - dayNames: Date.CultureInfo.dayNames, - dayNamesShort: Date.CultureInfo.abbreviatedDayNames, - firstDay: Date.CultureInfo.firstDayOfWeek, - weekNumbers: true, - axisFormat : shortTimeformat.replace(/:mm/,'(:mm)'), - timeFormat : { - // for agendaWeek and agendaDay - agenda: shortTimeformat + '{ - ' + shortTimeformat + '}', // 5:00 - 6:30 - // for all other views - '': shortTimeformat.replace(/:mm/,'(:mm)') // 7pm - }, - titleFormat: { - month: 'MMMM yyyy', - week: dateFormat + "{ '—'"+ dateFormat, - day: dateFormat, - }, - columnFormat: { - month: 'ddd', - week: 'ddd ' + dateFormat, - day: 'dddd ' + dateFormat, - }, - weekMode : 'liquid', - aspectRatio: 1.8, - snapMinutes: 15, - }; - } - - function is_virtual_id(id) { - return typeof id === "string" && id.indexOf('-') >= 0; - } - function isNullOrUndef(value) { return _.isUndefined(value) || _.isNull(value); } - instance.web.views.add('calendar', 'instance.web_calendar.CalendarView'); + instance.web.views.add('timeline', 'instance.web_timeline.TimelineView'); - instance.web_calendar.CalendarView = instance.web.View.extend({ - template: "CalendarView", - display_name: _lt('Calendar'), - quick_create_instance: 'instance.web_calendar.QuickCreate', + instance.web_timeline.TimelineView = instance.web.View.extend({ + template: "TimelineView", + display_name: _lt('Timeline'), + quick_create_instance: 'instance.web_timeline.QuickCreate', init: function (parent, dataset, view_id, options) { this._super(parent); @@ -83,11 +34,12 @@ openerp.web_calendar = function(instance) { this.model = dataset.model; this.fields_view = {}; this.view_id = view_id; - this.view_type = 'calendar'; + this.view_type = 'timeline'; this.color_map = {}; this.range_start = null; this.range_stop = null; this.selected_filters = []; + this.group_by_name = {}; }, set_default_options: function(options) { @@ -97,38 +49,28 @@ openerp.web_calendar = function(instance) { }); }, - destroy: function() { - this.$calendar.fullCalendar('destroy'); - if (this.$small_calendar) { - this.$small_calendar.datepicker('destroy'); + parse_colors: function(){ + if(this.fields_view.arch.attrs.colors) { + this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain().compact().map(function(color_pair) { + var pair = color_pair.split(':'), color = pair[0], expr = pair[1]; + var temp = py.parse(py.tokenize(expr)); + return {'color': color, 'field': temp.expressions[0].value, 'opt': temp.operators[0], 'value': temp.expressions[1].value}; + }).value(); } - this._super.apply(this, arguments); }, view_loading: function (fv) { - /* xml view calendar options */ + /* xml view timeline options */ var attrs = fv.arch.attrs, self = this; this.fields_view = fv; - this.$calendar = this.$el.find(".oe_calendar_widget"); + this.parse_colors(); + this.$timeline = this.$el.find(".oe_timeline_widget"); this.info_fields = []; - /* buttons */ - this.$buttons = $(QWeb.render("CalendarView.buttons", {'widget': this})); - if (this.options.$buttons) { - this.$buttons.appendTo(this.options.$buttons); - } else { - this.$el.find('.oe_calendar_buttons').replaceWith(this.$buttons); - } - - this.$buttons.on('click', 'button.oe_calendar_button_new', function () { - self.dataset.index = null; - self.do_switch_view('form'); - }); - if (!attrs.date_start) { - throw new Error(_t("Calendar view has not defined 'date_start' attribute.")); + throw new Error(_t("Timeline view has not defined 'date_start' attribute.")); } this.$el.addClass(attrs['class']); @@ -137,333 +79,112 @@ openerp.web_calendar = function(instance) { this.view_id = fv.view_id; this.mode = attrs.mode; // one of month, week or day - this.date_start = attrs.date_start; // Field name of starting date field + this.date_start = attrs.date_start; // Field name of starting + // date field this.date_delay = attrs.date_delay; // duration this.date_stop = attrs.date_stop; - this.all_day = attrs.all_day; - this.how_display_event = ''; - this.attendee_people = attrs.attendee; - + if (!isNullOrUndef(attrs.quick_create_instance)) { self.quick_create_instance = 'instance.' + attrs.quick_create_instance; } - //if quick_add = False, we don't allow quick_add - //if quick_add = not specified in view, we use the default quick_create_instance - //if quick_add = is NOT False and IS specified in view, we this one for quick_create_instance' - - this.quick_add_pop = (isNullOrUndef(attrs.quick_add) || _.str.toBoolElse(attrs.quick_add, true)); - if (this.quick_add_pop && !isNullOrUndef(attrs.quick_add)) { - self.quick_create_instance = 'instance.' + attrs.quick_add; - } - // The display format which will be used to display the event where fields are between "[" and "]" - if (!isNullOrUndef(attrs.display)) { - this.how_display_event = attrs.display; // String with [FIELD] - } - - // If this field is set ot true, we don't open the event in form view, but in a popup with the view_id passed by this parameter + // If this field is set ot true, we don't open the event in form + // view, but in a popup with the view_id passed by this parameter if (isNullOrUndef(attrs.event_open_popup) || !_.str.toBoolElse(attrs.event_open_popup, true)) { this.open_popup_action = false; } else { this.open_popup_action = attrs.event_open_popup; } - // If this field is set to true, we will use the calendar_friends model as filter and not the color field. - this.useContacts = (!isNullOrUndef(attrs.use_contacts) && _.str.toBool(attrs.use_contacts)) && (!isNullOrUndef(self.options.$sidebar)); - - // If this field is set ot true, we don't add itself as an attendee when we use attendee_people to add each attendee icon on an event - // The color is the color of the attendee, so don't need to show again that it will be present - this.colorIsAttendee = (!(isNullOrUndef(attrs.color_is_attendee) || !_.str.toBoolElse(attrs.color_is_attendee, true))) && (!isNullOrUndef(self.options.$sidebar)); - - // if we have not sidebar, (eg: Dashboard), we don't use the filter "coworkers" - if (isNullOrUndef(self.options.$sidebar)) { - this.useContacts = false; - this.colorIsAttendee = false; - this.attendee_people = undefined; - } - -/* - Will be more logic to do it in futur, but see below to stay Retro-compatible - if (isNull(attrs.avatar_model)) { - this.avatar_model = 'res.partner'; - } - else { - if (attrs.avatar_model == 'False') { - this.avatar_model = null; - } - else { - this.avatar_model = attrs.avatar_model; - } - } -*/ - if (isNullOrUndef(attrs.avatar_model)) { - this.avatar_model = null; - } else { - this.avatar_model = attrs.avatar_model; - } - - if (isNullOrUndef(attrs.avatar_title)) { - this.avatar_title = this.avatar_model; - } else { - this.avatar_title = attrs.avatar_title; - } - - if (isNullOrUndef(attrs.avatar_filter)) { - this.avatar_filter = this.avatar_model; - } else { - this.avatar_filter = attrs.avatar_filter; - } - - this.color_field = attrs.color; - - if (this.color_field && this.selected_filters.length === 0) { - var default_filter; - if ((default_filter = this.dataset.context['calendar_default_' + this.color_field])) { - this.selected_filters.push(default_filter + ''); - } - } - this.fields = fv.fields; for (var fld = 0; fld < fv.arch.children.length; fld++) { this.info_fields.push(fv.arch.children[fld].attrs.name); } + var fields_get = new instance.web.Model(this.dataset.model) + .call('fields_get') + .then(function (fields) { + self.fields = fields; + }); + var unlink_check = new instance.web.Model(this.dataset.model) + .call("check_access_rights", ["unlink", false]) + .then(function (unlink_right) { + self.unlink_right = unlink_right; + }); var edit_check = new instance.web.Model(this.dataset.model) .call("check_access_rights", ["write", false]) .then(function (write_right) { self.write_right = write_right; + }); var init = new instance.web.Model(this.dataset.model) .call("check_access_rights", ["create", false]) .then(function (create_right) { self.create_right = create_right; - self.init_calendar().then(function() { + self.init_timeline().then(function() { $(window).trigger('resize'); - self.trigger('calendar_view_loaded', fv); + self.trigger('timeline_view_loaded', fv); self.ready.resolve(); }); }); - return $.when(edit_check, init); - }, - - get_fc_init_options: function () { - //Documentation here : http://arshaw.com/fullcalendar/docs/ - var self = this; - return $.extend({}, get_fc_defaultOptions(), { - - defaultView: (this.mode == "month")?"month": - (this.mode == "week"?"agendaWeek": - (this.mode == "day"?"agendaDay":"month")), - header: { - left: 'prev,next today', - center: 'title', - right: 'month,agendaWeek,agendaDay' - }, - selectable: !this.options.read_only_mode && this.create_right, - selectHelper: true, - editable: !this.options.read_only_mode, - droppable: true, - - // callbacks - - eventDrop: function (event, _day_delta, _minute_delta, _all_day, _revertFunc) { - var data = self.get_event_data(event); - self.proxy('update_record')(event._id, data); // we don't revert the event, but update it. - }, - eventResize: function (event, _day_delta, _minute_delta, _revertFunc) { - var data = self.get_event_data(event); - self.proxy('update_record')(event._id, data); - }, - eventRender: function (event, element, view) { - element.find('.fc-event-title').html(event.title); - }, - eventAfterRender: function (event, element, view) { - if ((view.name !== 'month') && (((event.end-event.start)/60000)<=30)) { - //if duration is too small, we see the html code of img - var current_title = $(element.find('.fc-event-time')).text(); - var new_title = current_title.substr(0,current_title.indexOf("0?current_title.indexOf("= curView.start) { - context.$calendar.fullCalendar('changeView','agendaDay'); - } - } - else if (curView.name != "agendaDay" || (curView.name == "agendaDay" && curDate.compareTo(curView.start)===0)) { - context.$calendar.fullCalendar('changeView','agendaWeek'); - } - context.$calendar.fullCalendar('gotoDate', obj.currentYear , obj.currentMonth, obj.currentDay); - }; - }, - - init_calendar: function() { + init_timeline: function() { var self = this; - - if (!this.sidebar && this.options.$sidebar) { - translate = get_fc_defaultOptions(); - this.sidebar = new instance.web_calendar.Sidebar(this); - this.sidebar.appendTo(this.$el.find('.oe_calendar_sidebar_container')); - - this.$small_calendar = self.$el.find(".oe_calendar_mini"); - this.$small_calendar.datepicker({ - onSelect: self.calendarMiniChanged(self), - dayNamesMin : translate.dayNamesShort, - monthNames: translate.monthNamesShort, - firstDay: translate.firstDay, - }); - - this.extraSideBar(); - } - self.$calendar.fullCalendar(self.get_fc_init_options()); - + var options = { + "editable": self.write_right, + "groupsChangeable": self.write_right, + "timeChangeable": self.write_right, + "showNavigation": true, + "start": new Date(), + }; + self.timeline = new links.Timeline(self.$timeline.get(0)); + self.timeline.setOptions(options); + self.register_events(); return $.when(); }, - extraSideBar: function() { - }, - open_quick_create: function(data_template) { - if (!isNullOrUndef(this.quick)) { - return this.quick.trigger('close'); - } - var QuickCreate = get_class(this.quick_create_instance); - - this.options.disable_quick_create = this.options.disable_quick_create || !this.quick_add_pop; - - this.quick = new QuickCreate(this, this.dataset, true, this.options, data_template); - this.quick.on('added', this, this.quick_created) - .on('slowadded', this, this.slow_created) - .on('close', this, function() { - this.quick.destroy(); - delete this.quick; - this.$calendar.fullCalendar('unselect'); - }); - this.quick.replace(this.$el.find('.oe_calendar_qc_placeholder')); - this.quick.focus(); - - }, - - /** - * Refresh one fullcalendar event identified by it's 'id' by reading OpenERP record state. - * If event was not existent in fullcalendar, it'll be created. - */ - refresh_event: function(id) { + register_events: function(){ var self = this; - if (is_virtual_id(id)) { - // Should avoid "refreshing" a virtual ID because it can't - // really be modified so it should never be refreshed. As upon - // edition, a NEW event with a non-virtual id will be created. - console.warn("Unwise use of refresh_event on a virtual ID."); - } - this.dataset.read_ids([id], _.keys(this.fields)).done(function (incomplete_records) { - self.perform_necessary_name_gets(incomplete_records).then(function(records) { - // Event boundaries were already changed by fullcalendar, but we need to reload them: - var new_event = self.event_data_transform(records[0]); - // fetch event_obj - var event_objs = self.$calendar.fullCalendar('clientEvents', id); - if (event_objs.length == 1) { // Already existing obj to update - var event_obj = event_objs[0]; - // update event_obj - _(new_event).each(function (value, key) { - event_obj[key] = value; - }); - self.$calendar.fullCalendar('updateEvent', event_obj); - } else { // New event object to create - self.$calendar.fullCalendar('renderEvent', new_event); - // By forcing attribution of this event to this source, we - // make sure that the event will be removed when the source - // will be removed (which occurs at each do_search) - self.$calendar.fullCalendar('clientEvents', id)[0].source = self.event_source; - } - }); + links.events.addListener(self.timeline, 'edit', function() { + var sel = self.timeline.getSelection(); + if (sel.length) { + if (sel[0].row != undefined) { + var row = sel[0].row; + self.open_event(row); + } + } }); - }, - - get_color: function(key) { - if (this.color_map[key]) { - return this.color_map[key]; - } - var index = (((_.keys(this.color_map).length + 1) * 5) % 24) + 1; - this.color_map[key] = index; - return index; - }, - - - /** - * In o2m case, records from dataset won't have names attached to their *2o values. - * We should make sure this is the case. - */ - perform_necessary_name_gets: function(evts) { - var def = $.Deferred(); - var self = this; - var to_get = {}; - _(this.info_fields).each(function (fieldname) { - if (!_(["many2one", "one2one"]).contains( - self.fields[fieldname].type)) - return; - to_get[fieldname] = []; - _(evts).each(function (evt) { - var value = evt[fieldname]; - if (value === false || (value instanceof Array)) { - return; - } - to_get[fieldname].push(value); - }); - if (to_get[fieldname].length === 0) { - delete to_get[fieldname]; + links.events.addListener(self.timeline, 'delete', function() { + if(! self.unlink_right){ + self.timeline.cancelDelete(); + alert(_t("You are not allowed to delete this event ?")); } + var sel = self.timeline.getSelection(); + if (sel.length) { + if (sel[0].row != undefined) { + var row = sel[0].row; + self.remove_event(self.timeline.getItem(row), undefined); + } + } }); - var defs = _(to_get).map(function (ids, fieldname) { - return (new instance.web.Model(self.fields[fieldname].relation)) - .call('name_get', ids).then(function (vals) { - return [fieldname, vals]; - }); + links.events.addListener(self.timeline, 'changed', function() { + var sel = self.timeline.getSelection(); + if (sel.length) { + if (sel[0].row != undefined) { + var row = sel[0].row; + self.on_item_changed(self.timeline.getItem(row)); + } + } }); - - $.when.apply(this, defs).then(function() { - var values = arguments; - _(values).each(function(value) { - var fieldname = value[0]; - var name_gets = value[1]; - _(name_gets).each(function(name_get) { - _(evts).chain() - .filter(function (e) {return e[fieldname] == name_get[0];}) - .each(function(evt) { - evt[fieldname] = name_get; - }); - }); - }); - def.resolve(evts); - }); - return def; }, - + + /** - * Transform OpenERP event object to fullcalendar event object - */ + * Transform OpenERP event object to fulltimeline event object + */ event_data_transform: function(evt) { var self = this; @@ -479,105 +200,32 @@ openerp.web_calendar = function(instance) { } else { date_start = instance.web.auto_str_to_date(evt[this.date_start].split(' ')[0],'start'); - date_stop = this.date_stop ? instance.web.auto_str_to_date(evt[this.date_stop].split(' ')[0],'start') : null; //.addSeconds(-1) : null; - } - - if (this.info_fields) { - var temp_ret = {}; - res_computed_text = this.how_display_event; - - _.each(this.info_fields, function (fieldname) { - var value = evt[fieldname]; - if (_.contains(["many2one", "one2one"], self.fields[fieldname].type)) { - if (value === false) { - temp_ret[fieldname] = null; - } - else if (value instanceof Array) { - temp_ret[fieldname] = value[1]; // no name_get to make - } - else { - throw new Error("Incomplete data received from dataset for record " + evt.id); - } - } - else if (_.contains(["one2many","many2many"], self.fields[fieldname].type)) { - if (value === false) { - temp_ret[fieldname] = null; - } - else if (value instanceof Array) { - temp_ret[fieldname] = value; // if x2many, keep all id ! - } - else { - throw new Error("Incomplete data received from dataset for record " + evt.id); - } - } - else { - temp_ret[fieldname] = value; - } - res_computed_text = res_computed_text.replace("["+fieldname+"]",temp_ret[fieldname]); - }); - - - if (res_computed_text.length) { - the_title = res_computed_text; - } - else { - var res_text= []; - _.each(temp_ret, function(val,key) { - if( typeof(val) == 'boolean' && val == false ) { } - else { res_text.push(val) }; - }); - the_title = res_text.join(', '); - } - the_title = _.escape(the_title); - - - the_title_avatar = ''; - - if (! _.isUndefined(this.attendee_people)) { - var MAX_ATTENDEES = 3; - var attendee_showed = 0; - var attendee_other = ''; - - _.each(temp_ret[this.attendee_people], - function (the_attendee_people) { - attendees.push(the_attendee_people); - attendee_showed += 1; - if (attendee_showed<= MAX_ATTENDEES) { - if (self.avatar_model !== null) { - the_title_avatar += ''; - } - else { - if (!self.colorIsAttendee || the_attendee_people != temp_ret[self.color_field]) { - tempColor = (self.all_filters[the_attendee_people] !== undefined) - ? self.all_filters[the_attendee_people].color - : (self.all_filters[-1] ? self.all_filters[-1].color : 1); - the_title_avatar += ''; - }//else don't add myself - } - } - else { - attendee_other += self.all_attendees[the_attendee_people] +", "; - } - } - ); - if (attendee_other.length>2) { - the_title_avatar += '+'; - } - the_title = the_title_avatar + the_title; - } + date_stop = this.date_stop ? instance.web.auto_str_to_date(evt[this.date_stop].split(' ')[0],'stop') : null; } - if (!date_stop && date_delay) { + if (!date_start){ + date_start = new Date(); + } + if(!date_stop) { date_stop = date_start.clone().addHours(date_delay); } + var group = 'Unassigned'; + + if (evt.user_id){ + var info = evt.user_id.slice(0, 2); + var id = info[0]; + var name = info[1]; + self.group_by_name[name] = id; + group = name; + } var r = { - 'start': date_start.toString('yyyy-MM-dd HH:mm:ss'), - 'end': date_stop.toString('yyyy-MM-dd HH:mm:ss'), - 'title': the_title, - 'allDay': (this.fields[this.date_start].type == 'date' || (this.all_day && evt[this.all_day]) || false), + 'start': date_start, + 'end': date_stop, + 'content': evt.__name, 'id': evt.id, - 'attendees':attendees + 'group': group, + 'evt': evt, + }; if (!self.useContacts || self.all_filters[evt[this.color_field]] !== undefined) { if (this.color_field && evt[this.color_field]) { @@ -585,38 +233,39 @@ openerp.web_calendar = function(instance) { if (typeof color_key === "object") { color_key = color_key[0]; } - r.className = 'cal_opacity calendar_color_'+ this.get_color(color_key); + r.className = 'cal_opacity timeline_color_'+ this.get_color(color_key); } } else { // if form all, get color -1 - r.className = 'cal_opacity calendar_color_'+ self.all_filters[-1].color; + r.className = 'cal_opacity timeline_color_'+ self.all_filters[-1].color; } return r; }, /** - * Transform fullcalendar event object to OpenERP Data object - */ + * Transform fulltimeline event object to OpenERP Data object + */ get_event_data: function(event) { - // Normalize event_end without changing fullcalendars event. + // Normalize event_end without changing fulltimelines event. var data = { name: event.title }; var event_end = event.end; - //Bug when we move an all_day event from week or day view, we don't have a dateend or duration... + // Bug when we move an all_day event from week or day view, we don't + // have a dateend or duration... if (event_end == null) { event_end = new Date(event.start).addHours(2); } if (event.allDay) { - // Sometimes fullcalendar doesn't give any event.end. + // Sometimes fulltimeline doesn't give any event.end. if (event_end == null || _.isUndefined(event_end)) { event_end = new Date(event.start); } if (this.all_day) { - //event_end = (new Date(event_end.getTime())).addDays(1); + // event_end = (new Date(event_end.getTime())).addDays(1); date_start_day = new Date(Date.UTC(event.start.getFullYear(),event.start.getMonth(),event.start.getDate())); date_stop_day = new Date(Date.UTC(event_end.getFullYear(),event_end.getMonth(),event_end.getDate())); } @@ -650,174 +299,78 @@ openerp.web_calendar = function(instance) { return data; }, - do_search: function(domain, context, _group_by) { + do_search: function (domains, contexts, group_bys) { + var self = this; + self.last_domains = domains; + self.last_contexts = contexts; + self.last_group_bys = group_bys; + // self.reload_gantt(); + // select the group by + var n_group_bys = []; + if (this.fields_view.arch.attrs.default_group_by) { + n_group_bys = this.fields_view.arch.attrs.default_group_by.split(','); + } + if (group_bys.length) { + n_group_bys = group_bys; + } + // gather the fields to get + var fields = _.compact(_.map(["date_start", "date_delay", "date_stop", "progress"], function(key) { + return self.fields_view.arch.attrs[key] || ''; + })); + fields = _.uniq(fields.concat(_.pluck(this.colors, "field").concat(n_group_bys))); + + return $.when(this.has_been_loaded).then(function() { + return self.dataset.read_slice(fields, { + domain: domains, + context: contexts + }).then(function(data) { + return self.on_data_loaded(data, n_group_bys); + }); + }); + }, + reload: function() { + var self = this; + if (this.last_domains !== undefined) + return this.do_search(this.last_domains, this.last_contexts, this.last_group_bys); + }, + on_data_loaded: function(tasks, group_bys) { + var self = this; + var ids = _.pluck(tasks, "id"); + return this.dataset.name_get(ids).then(function(names) { + var ntasks = _.map(tasks, function(task) { + return _.extend({__name: _.detect(names, function(name) { return name[0] == task.id; })[1]}, task); + }); + return self.on_data_loaded_2(ntasks, group_bys); + }); + }, + + on_data_loaded_2: function(tasks, group_bys) { var self = this; - if (! self.all_filters) { - self.all_filters = {} - } - - if (! _.isUndefined(this.event_source)) { - this.$calendar.fullCalendar('removeEventSource', this.event_source); - } - this.event_source = { - events: function(start, end, callback) { - var current_event_source = self.event_source; - self.dataset.read_slice(_.keys(self.fields), { - offset: 0, - domain: self.get_range_domain(domain, start, end), - context: context, - }).done(function(events) { - if (self.dataset.index === null) { - if (events.length) { - self.dataset.index = 0; - } - } else if (self.dataset.index >= events.length) { - self.dataset.index = events.length ? 0 : null; - } - - if (self.event_source !== current_event_source) { - console.log("Consecutive ``do_search`` called. Cancelling."); - return; - } - - if (!self.useContacts) { // If we use all peoples displayed in the current month as filter in sidebars - var filter_item; - - self.now_filter_ids = []; - - var color_field = self.fields[self.color_field]; - _.each(events, function (e) { - var key,val = null; - if (self.color_field.type == "selection") { - key = e[self.color_field]; - val = _.find( self.color_field.selection, function(name){ return name[0] === key;}); - } - else { - key = e[self.color_field][0]; - val = e[self.color_field]; - } - if (!self.all_filters[key]) { - filter_item = { - value: key, - label: val[1], - color: self.get_color(key), - avatar_model: (_.str.toBoolElse(self.avatar_filter, true) ? self.avatar_filter : false ), - is_checked: true - }; - self.all_filters[key] = filter_item; - } - if (! _.contains(self.now_filter_ids, key)) { - self.now_filter_ids.push(key); - } - }); - - if (self.sidebar) { - self.sidebar.filter.events_loaded(); - self.sidebar.filter.set_filters(); - - events = $.map(events, function (e) { - var key = self.color_field.type == "selection" ? e[self.color_field] : e[self.color_field][0]; - if (_.contains(self.now_filter_ids, key) && self.all_filters[key].is_checked) { - return e; - } - return null; - }); - } - - } - else { //WE USE CONTACT - if (self.attendee_people !== undefined) { - //if we don't filter on 'Everybody's Calendar - if (!self.all_filters[-1] || !self.all_filters[-1].is_checked) { - var checked_filter = $.map(self.all_filters, function(o) { if (o.is_checked) { return o.value; }}); - // If we filter on contacts... we keep only events from coworkers - events = $.map(events, function (e) { - if (_.intersection(checked_filter,e[self.attendee_people]).length) { - return e; - } - return null; - }); - } - } - } - var all_attendees = $.map(events, function (e) { return e[self.attendee_people]; }); - all_attendees = _.chain(all_attendees).flatten().uniq().value(); - - self.all_attendees = {}; - if (self.avatar_title !== null) { - new instance.web.Model(self.avatar_title).query(["name"]).filter([["id", "in", all_attendees]]).all().then(function(result) { - _.each(result, function(item) { - self.all_attendees[item.id] = item.name; - }); - }).done(function() { - return self.perform_necessary_name_gets(events).then(callback); - }); - } - else { - _.each(all_attendees,function(item){ - self.all_attendees[item] = ''; - }); - return self.perform_necessary_name_gets(events).then(callback); - } - }); - }, - eventDataTransform: function (event) { - return self.event_data_transform(event); - }, - }; - this.$calendar.fullCalendar('addEventSource', this.event_source); + var data = []; + self.group_by_name = {}; + _.each(tasks, function(event) { + data.push(self.event_data_transform(event)); + }); + this.timeline.draw(data); + this.timeline.setVisibleChartRange(new Date(), null); + this.timeline.zoom(0.5, new Date()); }, - /** - * Build OpenERP Domain to filter object by this.date_start field - * between given start, end dates. - */ - get_range_domain: function(domain, start, end) { - var format = instance.web.date_to_str; - extend_domain = [[this.date_start, '>=', format(start.clone())], - [this.date_start, '<=', format(end.clone())]]; - - if (this.date_stop) { - //add at start - extend_domain.splice(0,0,'|','|','&'); - //add at end - extend_domain.push( - '&', - [this.date_start, '<=', format(start.clone())], - [this.date_stop, '>=', format(start.clone())], - '&', - [this.date_start, '<=', format(end.clone())], - [this.date_stop, '>=', format(start.clone())] - ); - //final -> (A & B) | (C & D) | (E & F) -> | | & A B & C D & E F - } - return new instance.web.CompoundDomain(domain, extend_domain); - }, - - /** - * Updates record identified by ``id`` with values in object ``data`` - */ - update_record: function(id, data) { + set_records: function(events){ var self = this; - delete(data.name); // Cannot modify actual name yet - var index = this.dataset.get_id_index(id); - if (index !== null) { - event_id = this.dataset.ids[index]; - this.dataset.write(event_id, data, {}).done(function() { - if (is_virtual_id(event_id)) { - // this is a virtual ID and so this will create a new event - // with an unknown id for us. - self.$calendar.fullCalendar('refetchEvents'); - } else { - // classical event that we can refresh - self.refresh_event(event_id); - } - }); - } - return false; + var data = []; + _.each(events, function(event) { + this.push(this.event_data_transform(event)); + }); + this.timeline.draw(data); }, - open_event: function(id, title) { + + open_event: function(index) { var self = this; + var item = self.timeline.getItem(index); + var id = item.evt.id; + var title = item.evt.__name; + var index = index; if (! this.open_popup_action) { var index = this.dataset.get_id_index(id); this.dataset.index = index; @@ -849,7 +402,7 @@ openerp.web_calendar = function(instance) { $('.delme').click( function() { $('.oe_form_button_cancel').trigger('click'); - self.remove_event(id); + self.remove_event(item, index); } ); $('.editme').click( @@ -865,18 +418,10 @@ openerp.web_calendar = function(instance) { }, do_show: function() { - if (this.$buttons) { - this.$buttons.show(); - } this.do_push_state({}); return this._super(); }, - do_hide: function () { - if (this.$buttons) { - this.$buttons.hide(); - } - return this._super(); - }, + is_action_enabled: function(action) { if (action === 'create' && !this.options.creatable) { return false; @@ -884,39 +429,50 @@ openerp.web_calendar = function(instance) { return this._super(action); }, + on_item_changed: function(item) { + var self = this; + var start = item.start; + var end = item.end; + var group = false; + if (item.group in self.group_by_name) { + group = self.group_by_name[item.group]; + } + var data = {}; + data[self.fields_view.arch.attrs.date_start] = + instance.web.auto_date_to_str(start, self.fields[self.fields_view.arch.attrs.date_start].type); + data[self.fields_view.arch.attrs.date_stop] = + instance.web.auto_date_to_str(end, self.fields[self.fields_view.arch.attrs.date_stop].type); + data[self.fields_view.arch.attrs.default_group_by] = group; + var id = item.evt.id; + this.dataset.write(id, data); + }, + /** - * Handles a newly created record - * - * @param {id} id of the newly created record - */ + * Handles a newly created record + * + * @param {id} id of the newly created record + */ quick_created: function (id) { - /** Note: - * it's of the most utter importance NOT to use inplace - * modification on this.dataset.ids as reference to this - * data is spread out everywhere in the various widget. - * Some of these reference includes values that should - * trigger action upon modification. - */ + /** + * Note: it's of the most utter importance NOT to use inplace + * modification on this.dataset.ids as reference to this data is + * spread out everywhere in the various widget. Some of these + * reference includes values that should trigger action upon + * modification. + */ this.dataset.ids = this.dataset.ids.concat([id]); this.dataset.trigger("dataset_changed", id); this.refresh_event(id); }, - slow_created: function () { - // refresh all view, because maybe some recurrents item - var self = this; - if (self.sidebar) { - // force filter refresh - self.sidebar.filter.is_loaded = false; - } - self.$calendar.fullCalendar('refetchEvents'); - }, - remove_event: function(id) { + remove_event: function(item, index) { var self = this; function do_it() { - return $.when(self.dataset.unlink([id])).then(function() { - self.$calendar.fullCalendar('removeEvents', id); + return $.when(self.dataset.unlink([item.evt.id])).then(function() { + if(! isNullOrUndef(index)){ + self.timeline.deleteItem(index); + } }); } if (this.options.confirm_on_delete) { @@ -927,503 +483,4 @@ openerp.web_calendar = function(instance) { return do_it(); }, }); - - - /** - * Quick creation view. - * - * Triggers a single event "added" with a single parameter "name", which is the - * name entered by the user - * - * @class - * @type {*} - */ - instance.web_calendar.QuickCreate = instance.web.Widget.extend({ - template: 'CalendarView.quick_create', - - init: function(parent, dataset, buttons, options, data_template) { - this._super(parent); - this.dataset = dataset; - this._buttons = buttons || false; - this.options = options; - - // Can hold data pre-set from where you clicked on agenda - this.data_template = data_template || {}; - }, - get_title: function () { - var parent = this.getParent(); - if (_.isUndefined(parent)) { - return _t("Create"); - } - var title = (_.isUndefined(parent.field_widget)) ? - (parent.string || parent.name) : - parent.field_widget.string || parent.field_widget.name || ''; - return _t("Create: ") + title; - }, - start: function () { - var self = this; - - if (this.options.disable_quick_create) { - this.$el.hide(); - this.slow_create(); - return; - } - - self.$input = this.$el.find('input'); - self.$input.keyup(function enterHandler (event) { - if(event.keyCode == 13){ - self.$input.off('keyup', enterHandler); - if (!self.quick_add()){ - self.$input.on('keyup', enterHandler); - } - } - }); - - var submit = this.$el.find(".oe_calendar_quick_create_add"); - submit.click(function clickHandler() { - submit.off('click', clickHandler); - if (!self.quick_add()){ - submit.on('click', clickHandler); } - self.focus(); - }); - this.$el.find(".oe_calendar_quick_create_edit").click(function () { - self.slow_add(); - self.focus(); - }); - this.$el.find(".oe_calendar_quick_create_close").click(function (ev) { - ev.preventDefault(); - self.trigger('close'); - }); - self.$input.keyup(function enterHandler (e) { - if (e.keyCode == 27 && self._buttons) { - self.trigger('close'); - } - }); - self.$el.dialog({ title: this.get_title()}); - self.on('added', self, function() { - self.trigger('close'); - }); - - self.$el.on('dialogclose', self, function() { - self.trigger('close'); - }); - - }, - focus: function() { - this.$el.find('input').focus(); - }, - - /** - * Gathers data from the quick create dialog a launch quick_create(data) method - */ - quick_add: function() { - var val = this.$input.val(); - if (/^\s*$/.test(val)) { - return false; - } - return this.quick_create({'name': val}).always(function() { return true; }); - }, - - slow_add: function() { - var val = this.$input.val(); - this.slow_create({'name': val}); - }, - - /** - * Handles saving data coming from quick create box - */ - quick_create: function(data, options) { - var self = this; - return this.dataset.create($.extend({}, this.data_template, data), options) - .then(function(id) { - self.trigger('added', id); - self.$input.val(""); - }).fail(function(r, event) { - event.preventDefault(); - // This will occurs if there are some more fields required - self.slow_create(data); - }); - }, - - /** - * Show full form popup - */ - get_form_popup_infos: function() { - var parent = this.getParent(); - var infos = { - view_id: false, - title: this.name, - }; - if (!_.isUndefined(parent) && !(_.isUndefined(parent.ViewManager))) { - infos.view_id = parent.ViewManager.get_view_id('form'); - } - return infos; - }, - slow_create: function(data) { - //if all day, we could reset time to display 00:00:00 - - var self = this; - var def = $.Deferred(); - var defaults = {}; - var created = false; - - _.each($.extend({}, this.data_template, data), function(val, field_name) { - defaults['default_' + field_name] = val; - }); - - var pop_infos = self.get_form_popup_infos(); - var pop = new instance.web.form.FormOpenPopup(this); - var context = new instance.web.CompoundContext(this.dataset.context, defaults); - pop.show_element(this.dataset.model, null, this.dataset.get_context(defaults), { - title: this.get_title(), - disable_multiple_selection: true, - view_id: pop_infos.view_id, - // Ensuring we use ``self.dataset`` and DO NOT create a new one. - create_function: function(data, options) { - return self.dataset.create(data, options).done(function(r) { - }).fail(function (r, event) { - if (!r.data.message) { //else manage by openerp - throw new Error(r); - } - }); - }, - read_function: function(id, fields, options) { - return self.dataset.read_ids.apply(self.dataset, arguments).done(function() { - }).fail(function (r, event) { - if (!r.data.message) { //else manage by openerp - throw new Error(r); - } - }); - }, - }); - pop.on('closed', self, function() { - // ``self.trigger('close')`` would itself destroy all child element including - // the slow create popup, which would then re-trigger recursively the 'closed' signal. - // Thus, here, we use a deferred and its state to cut the endless recurrence. - if (def.state() === "pending") { - def.resolve(); - } - }); - pop.on('create_completed', self, function(id) { - created = true; - self.trigger('slowadded'); - }); - def.then(function() { - if (created) { - var parent = self.getParent(); - parent.$calendar.fullCalendar('refetchEvents'); - } - self.trigger('close'); - }); - return def; - }, - }); - - - /** - * Form widgets - */ - - function widget_calendar_lazy_init() { - if (instance.web.form.Many2ManyCalendarView) { - return; - } - - instance.web_calendar.FieldCalendarView = instance.web_calendar.CalendarView.extend({ - - init: function (parent) { - this._super.apply(this, arguments); - // Warning: this means only a field_widget should instanciate this Class - this.field_widget = parent; - }, - - view_loading: function (fv) { - var self = this; - return $.when(this._super.apply(this, arguments)).then(function() { - self.on('event_rendered', this, function (event, element, view) { - - }); - }); - }, - - // In forms, we could be hidden in a notebook. Thus we couldn't - // render correctly fullcalendar so we try to detect when we are - // not visible to wait for when we will be visible. - init_calendar: function() { - if (this.$calendar.width() !== 0) { // visible - return this._super(); - } - // find all parents tabs. - var def = $.Deferred(); - var self = this; - this.$calendar.parents(".ui-tabs").on('tabsactivate', this, function() { - if (self.$calendar.width() !== 0) { // visible - self.$calendar.fullCalendar(self.get_fc_init_options()); - def.resolve(); - } - }); - return def; - }, - }); - } - - instance.web_calendar.BufferedDataSet = instance.web.BufferedDataSet.extend({ - - /** - * Adds verification on possible missing fields for the sole purpose of - * O2M dataset being compatible with the ``slow_create`` detection of - * missing fields... which is as simple to try to write and upon failure - * go to ``slow_create``. Current BufferedDataSet would'nt fail because - * they do not send data to the server at create time. - */ - create: function (data, options) { - var def = $.Deferred(); - var self = this; - var create = this._super; - if (_.isUndefined(this.required_fields)) { - this.required_fields = (new instance.web.Model(this.model)) - .call('fields_get').then(function (fields_def) { - return _(fields_def).chain() - // equiv to .pairs() - .map(function (value, key) { return [key, value]; }) - // equiv to .omit(self.field_widget.field.relation_field) - .filter(function (pair) { return pair[0] !== self.field_widget.field.relation_field; }) - .filter(function (pair) { return pair[1].required; }) - .map(function (pair) { return pair[0]; }) - .value(); - }); - } - $.when(this.required_fields).then(function (required_fields) { - var missing_fields = _(required_fields).filter(function (v) { - return _.isUndefined(data[v]); - }); - var default_get = (missing_fields.length !== 0) ? - self.default_get(missing_fields) : []; - $.when(default_get).then(function (defaults) { - - // Remove all fields that have a default from the missing fields. - missing_fields = _(missing_fields).filter(function (f) { - return _.isUndefined(defaults[f]); - }); - if (missing_fields.length !== 0) { - def.reject( - _.str.sprintf( - _t("Missing required fields %s"), missing_fields.join(", ")), - $.Event()); - return; - } - create.apply(self, [data, options]).then(function (result) { - def.resolve(result); - }); - }); - }); - return def; - }, - }); - - instance.web_calendar.fields_dataset = new instance.web.Registry({ - 'many2many': 'instance.web.DataSetStatic', - 'one2many': 'instance.web_calendar.BufferedDataSet', - }); - - - function get_field_dataset_class(type) { - var obj = instance.web_calendar.fields_dataset.get_any([type]); - if (!obj) { - throw new Error(_.str.sprintf(_t("Dataset for type '%s' is not defined."), type)); - } - - // Override definition of legacy datasets to add field_widget context - return obj.extend({ - init: function (parent) { - this._super.apply(this, arguments); - this.field_widget = parent; - }, - get_context: function() { - this.context = this.field_widget.build_context(); - return this.context; - } - }); - } - - /** - * Common part to manage any field using calendar view - */ - instance.web_calendar.FieldCalendar = instance.web.form.AbstractField.extend({ - - disable_utility_classes: true, - calendar_view_class: 'instance.web_calendar.FieldCalendarView', - - init: function(field_manager, node) { - this._super(field_manager, node); - widget_calendar_lazy_init(); - this.is_loaded = $.Deferred(); - this.initial_is_loaded = this.is_loaded; - - var self = this; - - // This dataset will use current widget to '.build_context()'. - var field_type = field_manager.fields_view.fields[node.attrs.name].type; - this.dataset = new (get_field_dataset_class(field_type))( - this, this.field.relation); - - this.dataset.on('unlink', this, function(_ids) { - this.dataset.trigger('dataset_changed'); - }); - - // quick_create widget instance will be attached when spawned - this.quick_create = null; - - this.no_rerender = true; - - }, - - start: function() { - this._super.apply(this, arguments); - - var self = this; - - self.load_view(); - self.on("change:effective_readonly", self, function() { - self.is_loaded = self.is_loaded.then(function() { - self.calendar_view.destroy(); - return $.when(self.load_view()).done(function() { - self.render_value(); - }); - }); - }); - }, - - load_view: function() { - var self = this; - var calendar_view_class = get_class(this.calendar_view_class); - this.calendar_view = new calendar_view_class(this, this.dataset, false, $.extend({ - 'create_text': _t("Add"), - 'creatable': self.get("effective_readonly") ? false : true, - 'quick_creatable': self.get("effective_readonly") ? false : true, - 'read_only_mode': self.get("effective_readonly") ? true : false, - 'confirm_on_delete': false, - }, this.options)); - var embedded = (this.field.views || {}).calendar; - if (embedded) { - this.calendar_view.set_embedded_view(embedded); - } - var loaded = $.Deferred(); - this.calendar_view.on("calendar_view_loaded", self, function() { - self.initial_is_loaded.resolve(); - loaded.resolve(); - }); - this.calendar_view.on('switch_mode', this, this.open_popup); - $.async_when().done(function () { - self.calendar_view.appendTo(self.$el); - }); - return loaded; - }, - - render_value: function() { - var self = this; - this.dataset.set_ids(this.get("value")); - this.is_loaded = this.is_loaded.then(function() { - return self.calendar_view.do_search(self.build_domain(), self.dataset.get_context(), []); - }); - }, - - open_popup: function(type, unused) { - if (type !== "form") { return; } - if (this.dataset.index == null) { - if (typeof this.open_popup_add === "function") { - this.open_popup_add(); - } - } else { - if (typeof this.open_popup_edit === "function") { - this.open_popup_edit(); - } - } - }, - - open_popup_add: function() { - throw new Error("Not Implemented"); - }, - - open_popup_edit: function() { - var id = this.dataset.ids[this.dataset.index]; - var self = this; - var pop = (new instance.web.form.FormOpenPopup(this)); - pop.show_element(this.field.relation, id, this.build_context(), { - title: _t("Open: ") + this.string, - write_function: function(id, data, _options) { - return self.dataset.write(id, data, {}).done(function() { - // Note that dataset will trigger itself the - // ``dataset_changed`` signal - self.calendar_view.refresh_event(id); - }); - }, - read_function: function(id, fields, options) { - return self.dataset.read_ids.apply(self.dataset, arguments).done(function() { - }).fail(function (r, event) { - throw new Error(r); - }); - }, - - alternative_form_view: this.field.views ? this.field.views.form : undefined, - parent_view: this.view, - child_name: this.name, - readonly: this.get("effective_readonly") - }); - } - }); - - instance.web_calendar.Sidebar = instance.web.Widget.extend({ - template: 'CalendarView.sidebar', - - start: function() { - this._super(); - this.filter = new instance.web_calendar.SidebarFilter(this, this.getParent()); - this.filter.appendTo(this.$el.find('.oe_calendar_filter')); - } - }); - instance.web_calendar.SidebarFilter = instance.web.Widget.extend({ - events: { - 'change input:checkbox': 'filter_click', - 'click span.color_filter': 'select_previous', - - }, - init: function(parent, view) { - this._super(parent); - this.view = view; - }, - set_filters: function() { - var self = this; - _.forEach(self.view.all_filters, function(o) { - if (_.contains(self.view.now_filter_ids, o.value)) { - self.$('div.oe_calendar_responsible input[value=' + o.value + ']').prop('checked',o.is_checked); - } - }); - }, - events_loaded: function(filters) { - var self = this; - if (filters == null) { - filters = []; - _.forEach(self.view.all_filters, function(o) { - if (_.contains(self.view.now_filter_ids, o.value)) { - filters.push(o); - } - }); - } - this.$el.html(QWeb.render('CalendarView.sidebar.responsible', { filters: filters })); - }, - filter_click: function(e) { - var self = this; - if (self.view.all_filters[0] && e.target.value == self.view.all_filters[0].value) { - self.view.all_filters[0].is_checked = e.target.checked; - } else { - self.view.all_filters[e.target.value].is_checked = e.target.checked; - } - self.view.$calendar.fullCalendar('refetchEvents'); - }, - select_previous: function(e) { - $(e.target).siblings('input').trigger('click'); - } - }); - }; diff --git a/static/src/xml/web_timeline.xml b/static/src/xml/web_timeline.xml index 06815009..5af3cbf8 100644 --- a/static/src/xml/web_timeline.xml +++ b/static/src/xml/web_timeline.xml @@ -1,74 +1,7 @@ diff --git a/views/web_timeline.xml b/views/web_timeline.xml index e72d66c3..1047de47 100644 --- a/views/web_timeline.xml +++ b/views/web_timeline.xml @@ -6,7 +6,7 @@