diff --git a/CHANGELOG b/CHANGELOG index f714bdc9af..139fa3dc51 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add calendar view * Move info-bar to the top * Add noeval to PYSONDecoder * Add support for pyson_order on action window diff --git a/bower.json b/bower.json index 00bead29d5..103ee34734 100644 --- a/bower.json +++ b/bower.json @@ -21,7 +21,8 @@ "eonasdan-bootstrap-datetimepicker": "~4.17.42", "gettext.js": "~0.5.0", "d3": "~3.5.0", - "c3": "~0.4.10" + "c3": "~0.4.10", + "fullcalendar": "~3.0.0" }, "devDependencies": { "qunit": "~1.18.0" diff --git a/index.html b/index.html index 17c2724416..ac4cd7bcae 100644 --- a/index.html +++ b/index.html @@ -17,10 +17,13 @@ this repository contains the full copyright notices and license terms. --> + + + diff --git a/src/sao.less b/src/sao.less index 99d7491d23..1ed2e54ceb 100644 --- a/src/sao.less +++ b/src/sao.less @@ -216,6 +216,11 @@ button { text-align: right; } +.calendar { + width: 100%; + height: 100%; +} + .progress { margin-bottom: 0px; } diff --git a/src/screen.js b/src/screen.js index c127f34e3d..4ea6c92a0a 100644 --- a/src/screen.js +++ b/src/screen.js @@ -864,6 +864,15 @@ } else { domain = this.attributes.domain || []; } + if (this.current_view && + this.current_view.view_type == 'calendar') { + if (!jQuery.isEmptyObject(domain)) { + domain = ['AND', domain, + this.current_view.current_domain()]; + } else { + domain = this.current_view.current_domain(); + } + } return domain; }, count_tab_domain: function() { @@ -1036,10 +1045,15 @@ } }, new_: function(default_, rec_name) { + var previous_view = this.current_view; if (default_ === undefined) { default_ = true; } var prm = jQuery.when(); + if (this.current_view.view_type == 'calendar') { + var selected_date = this.current_view.date; + prm = this.switch_view('form'); + } if (this.current_view && ((this.current_view.view_type == 'tree' && !this.current_view.editable) || @@ -1056,6 +1070,9 @@ var record = group.new_(default_, undefined, rec_name); group.add(record, this.new_model_position()); this.set_current_record(record); + if (previous_view.view_type == 'calendar') { + previous_view.set_default_date(record, selected_date); + } this.display().done(function() { this.set_cursor(true, true); }.bind(this)); diff --git a/src/view/calendar.js b/src/view/calendar.js index 77c125713b..25f9820e5c 100644 --- a/src/view/calendar.js +++ b/src/view/calendar.js @@ -4,20 +4,253 @@ 'use strict'; Sao.View.Calendar = Sao.class_(Sao.View, { + /* Fullcalendar works with utc date, the default week start day depends on + the user language, the events dates are handled by moment object. */ init: function(screen, xml) { - Sao.View.Graph._super.init.call(this, screen, xml); + Sao.View.Calendar._super.init.call(this, screen, xml); + // Used at display to reload the events when they are loaded. + this.prm = null; this.view_type = 'calendar'; this.el = jQuery('
', { 'class': 'calendar' }); - // TODO - Sao.common.warning.run( - Sao.i18n.gettext('Calendar view not yet implemented'), - Sao.i18n.gettext('Warning')); + var lang = Sao.i18n.getlang(); + lang = lang.slice(0, 2); + var defaultview = 'month'; + if (this.attributes.mode == 'week') { + defaultview = this.get_week_view(); + } + this.el.fullCalendar({ + defaultView: defaultview, + header: { + left: 'prev,next', + center: 'title', + right: ' month,' + this.get_week_view() + }, + timeFormat: 'H:mm', + events: this.get_events.bind(this), + locale: lang, + buttonIcons: false, + eventRender: this.event_render.bind(this), + eventResize: this.event_resize.bind(this), + eventDrop: this.event_drop.bind(this), + eventClick: this.event_click.bind(this), + dayClick: this.day_click.bind(this), + }); + this.fields = []; + xml.find('calendar').children().each(function(pos, child){ + if (child.tagName == 'field') { + this.fields.push(child.attributes.name.value); + } + }.bind(this)); + this.el.fullCalendar('changeView', defaultview); }, display: function() { - return jQuery.when(); - } + this.el.fullCalendar('render'); + if (this.prm) { + this.prm.done(function() { + return this.el.fullCalendar('refetchEvents'); + }.bind(this)); + } + }, + insert_event: function(record) { + var title = this.screen.model.fields[this.fields[0]].get_client( + record); + var date_start = record.field_get_client(this.attributes.dtstart); + var date_end = null; + if (this.attributes.dtend) { + date_end = record.field_get_client(this.attributes.dtend); + } + var allDay = true; + var description = []; + for (var i = 1; i < this.fields.length; i++) { + description.push( + this.screen.model.fields[this.fields[i]].get_client( + record)); + } + description = description.join('\n'); + if (date_start) { + if (date_end && date_end.isDateTime) { + allDay = false; + } else if (date_end && !date_end.isSame(date_start) && + this.screen.current_view.view_type == "calendar") { + // Add one day to allday event that last more than one day. + // http://github.com/fullcalendar/fullcalendar/issues/2909 + date_end.add(1, 'day'); + } + // Skip invalid event + if (date_end && date_start > date_end) { + return; + } + var values = { + title: title, + start: date_start, + end: date_end, + allDay: allDay, + editable: true, + color: 'lightblue', + textColor: 'black', + record: record, + description: description + }; + this.events.push(values); + } + }, + get_events: function(start, end, timezone, callback) { + this.start = Sao.DateTime(start.utc()); + this.end = Sao.DateTime(end.utc()); + var prm = jQuery.when(); + if (this.screen.current_view.view_type != 'form') { + var search_string = this.screen.screen_container.get_text(); + prm = this.screen.search_filter(search_string); + } + this.events = []; + var promisses = []; + this.prm = prm.then(function() { + this.screen.group.forEach(function(record) { + var record_promisses = []; + this.fields.forEach(function(name) { + record_promisses.push(record.load(name)); + }); + var prm = jQuery.when.apply(jQuery, record_promisses).then( + function(){ + this.insert_event(record); + }.bind(this)); + promisses.push(prm); + }.bind(this)); + jQuery.when.apply(jQuery, promisses).then(function() { + callback(this.events); + }.bind(this)); + }.bind(this)); + }, + get_week_view: function() { + if (this.screen.model.fields[this.attributes.dtstart] + .description.type == "datetime") { + return 'agendaWeek'; + } else { + return 'basicWeek'; + } + }, + event_click: function(calEvent, jsEvent, view) { + // Prevent opening the wrong event while the calendar event clicked + // when loading + if (!this.clicked_event) { + this.clicked_event = true; + this.screen.set_current_record(calEvent.record); + this.screen.switch_view().always(function(){ + this.clicked_event = false; + }.bind(this)); + } + }, + event_drop: function(event, delta, revertFunc, jsEvent, ui, view) { + var dtstart = this.attributes.dtstart; + var dtend = this.attributes.dtend; + var record = event.record; + var group = record.group; + var previous_start = record.field_get(dtstart); + var previous_end = previous_start; + if (dtend) { + previous_end = record.field_get(dtend); + } + var new_start = event.start; + var new_end = event.end; + if (new_end == previous_start || !new_end) { + new_end = new_start; + } + if (previous_start.isDateTime) { + new_end = Sao.DateTime(new_end.format()).utc(); + new_start = Sao.DateTime(new_start.format()).utc(); + } else if (!previous_start.isSame(previous_end)) { + // Remove the day that was added at the event end. + new_end.subtract(1, 'day'); + this.el.fullCalendar('refetchEvents'); + } + if (previous_start <= new_start) { + if (dtend) { + record.field_set_client(dtend, new_end); + } + record.field_set_client(dtstart, new_start); + } else { + record.field_set_client(dtstart, new_start); + if (dtend) { + record.field_set_client(dtend, new_end); + } + } + record.save(); + }, + event_resize: function(event, delta, revertFunc, jsEvent, ui, view) { + var dtend = this.attributes.dtend; + var record = event.record; + var group = record.group; + var previous_end = record.field_get(dtend); + var new_end = event.end; + if (previous_end.isDateTime === true) { + new_end = Sao.DateTime(new_end.format()).utc(); + } else { + // Remove the day that was added at the event end. + new_end.subtract(1, 'day'); + this.el.fullCalendar('refetchEvents'); + } + if (new_end == previous_end || !new_end) { + new_end = previous_end; + } + record.field_set_client(dtend, new_end); + record.save(); + }, + event_render: function(event, element, view) { + // The description field is added in the calendar events and the + // event time is not shown in week view. + if (this.screen.model.fields.date && + this.screen.view_name == 'calendar') { + element.find('.fc-time').remove(); + } + element.append(event.description); + element.css('white-space', 'pre'); + var model_access = Sao.common.MODELACCESS.get( + this.screen.model_name); + if (!model_access.write) { + event.editable = false; + } + }, + day_click: function(date, jsEvent, view){ + var model_access = Sao.common.MODELACCESS.get( + this.screen.model_name); + if (model_access.create) { + this.date = date; + this.screen.set_current_record(null); + this.screen.new_(); + } + }, + current_domain: function() { + if (!this.start && !this.end) { + return [['id', '=', -1]]; + } + var first_datetime = Sao.DateTime(this.start); + var last_datetime = Sao.DateTime(this.end); + var dtstart = this.attributes.dtstart; + var dtend = this.attributes.dtend || dtstart; + return ['OR', + ['AND', [dtstart, '>=', first_datetime], + [dtstart, '<', last_datetime]], + ['AND', [dtend, '>=', first_datetime], + [dtend, '<', last_datetime]], + ['AND', [dtstart, '<', first_datetime], + [dtend, '>', last_datetime]]]; + }, + get_displayed_period: function(){ + var DatesPeriod = []; + if (this.start && this.end) { + DatesPeriod.push(this.start, this.end); + } + return DatesPeriod; + }, + set_default_date: function(record, selected_date){ + var dtstart = this.attributes.dtstart; + if (record.model.fields[dtstart].description.type == 'datetime') { + selected_date = Sao.DateTime(selected_date.format()).utc(); + } + record.field_set_client(dtstart, selected_date); + }, }); }());