From 3e352e0b3b4f1e0c3aa10fdfd1743edff4cfd90c Mon Sep 17 00:00:00 2001 From: Georg Ehrke Date: Fri, 30 Sep 2016 11:39:36 +0200 Subject: [PATCH] refactor event objects --- js/app/factory/icalFactory.js | 28 + js/app/models/calendarmodel.js | 3 + js/app/models/fcEventModel.js | 194 ++++++ js/app/models/fceventmodel.js | 170 ----- ...impleeventmodel.js => simpleEventModel.js} | 451 ++++++------- js/app/models/veventModel.js | 289 ++++++++ js/app/models/veventmodel.js | 290 -------- js/app/models/webcalModel.js | 2 +- js/app/service/veventService.js | 4 +- tests/js/unit/factory/icalFactorySpec.js | 27 +- tests/js/unit/models/fcEventModelSpec.js | 51 ++ tests/js/unit/models/simpleEventModelSpec.js | 171 +++++ tests/js/unit/models/veventModelSpec.js | 623 ++++++++++++++++++ tests/js/unit/services/veventServiceSpec.js | 22 +- 14 files changed, 1618 insertions(+), 707 deletions(-) create mode 100644 js/app/models/fcEventModel.js delete mode 100644 js/app/models/fceventmodel.js rename js/app/models/{simpleeventmodel.js => simpleEventModel.js} (52%) create mode 100644 js/app/models/veventModel.js delete mode 100644 js/app/models/veventmodel.js create mode 100644 tests/js/unit/models/fcEventModelSpec.js create mode 100644 tests/js/unit/models/simpleEventModelSpec.js create mode 100644 tests/js/unit/models/veventModelSpec.js diff --git a/js/app/factory/icalFactory.js b/js/app/factory/icalFactory.js index b5d770c9f5..a16225b1a6 100644 --- a/js/app/factory/icalFactory.js +++ b/js/app/factory/icalFactory.js @@ -24,6 +24,12 @@ app.service('ICalFactory', function() { 'use strict'; + const self = this; + + /** + * create a new ICAL calendar object + * @returns {ICAL.Component} + */ this.new = function() { const root = new ICAL.Component(['vcalendar', [], []]); @@ -35,4 +41,26 @@ app.service('ICalFactory', function() { return root; }; + + /** + * create a new ICAL calendar object that contains a calendar + * @param uid + * @returns ICAL.Component + */ + this.newEvent = function(uid) { + const comp = self.new(); + + const event = new ICAL.Component('vevent'); + comp.addSubcomponent(event); + + event.updatePropertyWithValue('created', ICAL.Time.now()); + event.updatePropertyWithValue('dtstamp', ICAL.Time.now()); + event.updatePropertyWithValue('last-modified', ICAL.Time.now()); + event.updatePropertyWithValue('uid', uid); + + //add a dummy dtstart, so it's a valid ics + event.updatePropertyWithValue('dtstart', ICAL.Time.now()); + + return comp; + }; }); diff --git a/js/app/models/calendarmodel.js b/js/app/models/calendarmodel.js index cdea963bd6..e224db6d2d 100644 --- a/js/app/models/calendarmodel.js +++ b/js/app/models/calendarmodel.js @@ -33,6 +33,9 @@ app.factory('Calendar', function($window, Hook, VEventService, TimezoneService, context.fcEventSource.isRendering = true; iface.emit(Calendar.hookFinishedRendering); + start = moment(start.stripZone().format()); + end = moment(end.stripZone().format()); + const TimezoneServicePromise = TimezoneService.get(timezone); const VEventServicePromise = VEventService.getAll(iface, start, end); Promise.all([TimezoneServicePromise, VEventServicePromise]).then(function(results) { diff --git a/js/app/models/fcEventModel.js b/js/app/models/fcEventModel.js new file mode 100644 index 0000000000..93b55d202f --- /dev/null +++ b/js/app/models/fcEventModel.js @@ -0,0 +1,194 @@ +/** + * Calendar App + * + * @author Raghu Nayyar + * @author Georg Ehrke + * @copyright 2016 Raghu Nayyar + * @copyright 2016 Georg Ehrke + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see . + * + */ + +app.factory('FcEvent', function(SimpleEvent) { + 'use strict'; + + /** + * @param {VEvent} vevent + * @param {ICAL.Component} event + * @param {ICAL.Time} start + * @param {ICAL.Time} end + */ + function FcEvent(vevent, event, start, end) { + const context = {vevent, event, start, end}; + context.iCalEvent = new ICAL.Event(event); + + const iface = { + _isAFcEventObject: true + }; + + Object.defineProperties(iface, { + vevent: { + get: function() { + return context.vevent; + }, + enumerable: true + }, + event: { + get: function() { + return context.event; + }, + enumerable: true + }, + calendar: { + get: () => context.vevent.calendar, + enumerable: true + }, + id: { + get: function() { + let id = context.vevent.uri; + if (event.hasProperty('recurrence-id')) { + id += context.event.getFirstPropertyValue('recurrence-id').toICALString(); + } + + return id; + }, + enumerable: true + }, + allDay: { + get: function() { + return (context.start.icaltype === 'date' && + context.end.icaltype === 'date'); + }, + enumerable: true + }, + start: { + get: function() { + return context.start.toJSDate(); + }, + enumerable: true + }, + end: { + get: function() { + return context.end.toJSDate(); + }, + enumerable: true + }, + repeating: { + get: function() { + return context.iCalEvent.isRecurring(); + }, + enumerable: true + }, + backgroundColor: { + get: function() { + return context.vevent.calendar.color; + }, + enumerable: true + }, + borderColor: { + get: function() { + return context.vevent.calendar.color; + }, + enumerable: true + }, + className: { + get: function() { + return ['fcCalendar-id-' + context.vevent.calendar.tmpId]; + }, + enumerable: true + }, + editable: { + get: function() { + return context.vevent.calendar.isWritable(); + }, + enumerable: true + }, + textColor: { + get: function() { + return context.vevent.calendar.textColor; + }, + enumerable: true + }, + title: { + get: function() { + return context.event.getFirstPropertyValue('summary'); + }, + enumerable: true + } + }); + + /** + * get SimpleEvent for current fcEvent + * @returns {SimpleEvent} + */ + iface.getSimpleEvent = function () { + return SimpleEvent(context.event); + }; + + /** + * moves the event to a different position + * @param {Duration} delta + */ + iface.drop = function (delta) { + delta = new ICAL.Duration().fromSeconds(delta.asSeconds()); + + if (context.event.hasProperty('dtstart')) { + const dtstart = context.event.getFirstPropertyValue('dtstart'); + dtstart.addDuration(delta); + context.event.updatePropertyWithValue('dtstart', dtstart); + } + + if (context.event.hasProperty('dtend')) { + const dtend = context.event.getFirstPropertyValue('dtend'); + dtend.addDuration(delta); + context.event.updatePropertyWithValue('dtend', dtend); + } + + context.vevent.touch(); + }; + + /** + * resizes the event + * @param {moment.duration} delta + */ + iface.resize = function (delta) { + delta = new ICAL.Duration().fromSeconds(delta.asSeconds()); + + if (context.event.hasProperty('duration')) { + const duration = context.event.getFirstPropertyValue('duration'); + duration.fromSeconds((delta.toSeconds() + duration.toSeconds())); + context.event.updatePropertyWithValue('duration', duration); + } else if (context.event.hasProperty('dtend')) { + const dtend = context.event.getFirstPropertyValue('dtend'); + dtend.addDuration(delta); + context.event.updatePropertyWithValue('dtend', dtend); + } else if (context.event.hasProperty('dtstart')) { + const dtstart = event.getFirstPropertyValue('dtstart').clone(); + dtstart.addDuration(delta); + context.event.addPropertyWithValue('dtend', dtstart); + } + + context.vevent.touch(); + }; + + return iface; + } + + FcEvent.isFcEvent = function(obj) { + return (typeof obj === 'object' && obj !== null && obj._isAFcEventObject === true); + }; + + return FcEvent; +}); diff --git a/js/app/models/fceventmodel.js b/js/app/models/fceventmodel.js deleted file mode 100644 index a28a4eed9e..0000000000 --- a/js/app/models/fceventmodel.js +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Calendar App - * - * @author Raghu Nayyar - * @author Georg Ehrke - * @copyright 2016 Raghu Nayyar - * @copyright 2016 Georg Ehrke - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU AFFERO GENERAL PUBLIC LICENSE for more details. - * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see . - * - */ - -app.factory('FcEvent', function(SimpleEvent) { - 'use strict'; - - /** - * check if dtstart and dtend are both of type date - * @param dtstart - * @param dtend - * @returns {boolean} - */ - function isEventAllDay (dtstart, dtend) { - return (dtstart.icaltype === 'date' && dtend.icaltype === 'date'); - } - - /** - * get recurrence id from event - * @param {Component} event - * @returns {string} - */ - function getRecurrenceIdFromEvent (event) { - return event.hasProperty('recurrence-id') ? - event.getFirstPropertyValue('recurrence-id').toICALString() : - null; - } - - /** - * get calendar related information about event - * @param vevent - * @returns {{calendar: *, editable: *, backgroundColor: *, borderColor: *, textColor: *, className: *[]}} - */ - function getCalendarRelatedProps (vevent) { - return { - calendar: vevent.calendar, - editable: vevent.calendar.isWritable(), - className: ['fcCalendar-id-' + vevent.calendar.tmpId] - }; - } - - /** - * get event related information about event - * @param {Component} event - * @returns {{title: string}} - */ - function getEventRelatedProps (event) { - return { - title: event.getFirstPropertyValue('summary') - }; - } - - /** - * get unique id for fullcalendar - * @param {VEvent} vevent - * @param {Component} event - * @returns {string} - */ - function getFcEventId (vevent, event) { - var id = vevent.uri; - var recurrenceId = getRecurrenceIdFromEvent(event); - if (recurrenceId) { - id += recurrenceId; - } - - return id; - } - - /** - * @constructor - * @param {VEvent} vevent - * @param {Component} event - * @param {icaltime} start - * @param {icaltime} end - */ - function FcEvent (vevent, event, start, end) { - var iCalEvent = new ICAL.Event(event); - - angular.extend(this, { - vevent: vevent, - event: event, - id: getFcEventId(vevent, event), - allDay: isEventAllDay(start, end), - start: start.toJSDate(), - end: end.toJSDate(), - repeating: iCalEvent.isRecurring() - }, getCalendarRelatedProps(vevent), getEventRelatedProps(event)); - } - - FcEvent.prototype = { - get backgroundColor() { - return this.vevent.calendar.color; - }, - get borderColor() { - return this.vevent.calendar.color; - - }, - get textColor() { - return this.vevent.calendar.textColor; - }, - /** - * get SimpleEvent for current fcEvent - * @returns {SimpleEvent} - */ - getSimpleEvent: function () { - return new SimpleEvent(this.event); - }, - /** - * moves the event to a different position - * @param {Duration} delta - */ - drop: function (delta) { - delta = new ICAL.Duration().fromSeconds(delta.asSeconds()); - - if (this.event.hasProperty('dtstart')) { - var dtstart = this.event.getFirstPropertyValue('dtstart'); - dtstart.addDuration(delta); - this.event.updatePropertyWithValue('dtstart', dtstart); - } - - if (this.event.hasProperty('dtend')) { - var dtend = this.event.getFirstPropertyValue('dtend'); - dtend.addDuration(delta); - this.event.updatePropertyWithValue('dtend', dtend); - } - }, - /** - * resizes the event - * @param {moment.duration} delta - */ - resize: function (delta) { - delta = new ICAL.Duration().fromSeconds(delta.asSeconds()); - - if (this.event.hasProperty('duration')) { - var duration = this.event.getFirstPropertyValue('duration'); - duration.fromSeconds((delta.toSeconds() + duration.toSeconds())); - this.event.updatePropertyWithValue('duration', duration); - } else if (this.event.hasProperty('dtend')) { - var dtend = this.event.getFirstPropertyValue('dtend'); - dtend.addDuration(delta); - this.event.updatePropertyWithValue('dtend', dtend); - } else if (this.event.hasProperty('dtstart')) { - var dtstart = event.getFirstPropertyValue('dtstart').clone(); - dtstart.addDuration(delta); - this.event.addPropertyWithValue('dtend', dtstart); - } - } - }; - - return FcEvent; -}); diff --git a/js/app/models/simpleeventmodel.js b/js/app/models/simpleEventModel.js similarity index 52% rename from js/app/models/simpleeventmodel.js rename to js/app/models/simpleEventModel.js index cae84fff1e..6f55821b85 100644 --- a/js/app/models/simpleeventmodel.js +++ b/js/app/models/simpleEventModel.js @@ -21,17 +21,12 @@ * */ -app.factory('SimpleEvent', function() { +app.factory('SimpleEvent', function () { 'use strict'; - /** - * structure of simple data - */ - var defaults = { + const defaults = { 'summary': null, 'location': null, - //'created': null, - //'last-modified': null, 'organizer': null, 'class': null, 'description': null, @@ -49,7 +44,7 @@ app.factory('SimpleEvent', function() { 'exdate': null }; - var attendeeParameters = [ + const attendeeParameters = [ 'role', 'rvsp', 'partstat', @@ -59,54 +54,49 @@ app.factory('SimpleEvent', function() { 'delegated-to' ]; - var organizerParameters = [ + const organizerParameters = [ 'cn' ]; /** * parsers of supported properties */ - var simpleParser = { - date: function(data, vevent, key, parameters) { + const simpleParser = { + date: function (data, vevent, key, parameters) { parameters = (parameters || []).concat(['tzid']); - simpleParser._parseSingle(data, vevent, key, parameters, function(p) { - return (p.type === 'duration') ? - p.getFirstValue().toSeconds(): - moment(p.getFirstValue().toJSDate()); + simpleParser._parseSingle(data, vevent, key, parameters, function (p) { + const first = p.getFirstValue(); + return (p.type === 'duration') ? first.toSeconds() : moment(first.toJSDate()); }); }, - dates: function(data, vevent, key, parameters) { + dates: function (data, vevent, key, parameters) { parameters = (parameters || []).concat(['tzid']); - simpleParser._parseMultiple(data, vevent, key, parameters, function(p) { - var values = p.getValues(), - usableValues = []; - for (var vKey in values) { - if (!values.hasOwnProperty(vKey)) { - continue; - } + simpleParser._parseMultiple(data, vevent, key, parameters, function (p) { + const values = p.getValues(), usableValues = []; - usableValues.push( - (p.type === 'duration') ? - values[vKey].toSeconds(): - moment(values[vKey].toJSDate()) - ); - } + values.forEach(function (value) { + if (p.type === 'duration') { + usableValues.push(value.toSeconds()); + } else { + usableValues.push(moment(value.toJSDate())); + } + }); return usableValues; }); }, - string: function(data, vevent, key, parameters) { - simpleParser._parseSingle(data, vevent, key, parameters, function(p) { + string: function (data, vevent, key, parameters) { + simpleParser._parseSingle(data, vevent, key, parameters, function (p) { return p.isMultiValue ? p.getValues() : p.getFirstValue(); }); }, - strings: function(data, vevent, key, parameters) { - simpleParser._parseMultiple(data, vevent, key, parameters, function(p) { + strings: function (data, vevent, key, parameters) { + simpleParser._parseMultiple(data, vevent, key, parameters, function (p) { return p.isMultiValue ? p.getValues() : p.getFirstValue(); }); }, - _parseSingle: function(data, vevent, key, parameters, valueParser) { - var prop = vevent.getFirstProperty(key); + _parseSingle: function (data, vevent, key, parameters, valueParser) { + const prop = vevent.getFirstProperty(key); if (!prop) { return; } @@ -117,131 +107,116 @@ app.factory('SimpleEvent', function() { }; if (prop.isMultiValue) { - angular.extend(data[key], { - values: valueParser(prop) - }); + data[key].values = valueParser(prop); } else { - angular.extend(data[key], { - value: valueParser(prop) - }); + data[key].value = valueParser(prop); } }, - _parseMultiple: function(data, vevent, key, parameters, valueParser) { + _parseMultiple: function (data, vevent, key, parameters, valueParser) { data[key] = data[key] || []; - var properties = vevent.getAllProperties(key), - group = 0; - - for (var pKey in properties) { - if (!properties.hasOwnProperty(pKey)) { - continue; - } + const properties = vevent.getAllProperties(key); + let group = 0; - var currentElement = { + properties.forEach(function (property) { + const currentElement = { group: group, - parameters: simpleParser._parseParameters(properties[pKey], parameters), - type: properties[pKey].type + parameters: simpleParser._parseParameters(property, parameters), + type: property.type }; - if (properties[pKey].isMultiValue) { - angular.extend(currentElement, { - values: valueParser(properties[pKey]) - }); + if (property.isMultiValue) { + currentElement.values = valueParser(property); } else { - angular.extend(currentElement, { - value: valueParser(properties[pKey]) - }); + currentElement.value = valueParser(property); } data[key].push(currentElement); - properties[pKey].setParameter('x-oc-group-id', group.toString()); + property.setParameter('x-oc-group-id', group.toString()); group++; - } + }); }, - _parseParameters: function(prop, para) { - var parameters = {}; + _parseParameters: function (prop, para) { + const parameters = {}; if (!para) { return parameters; } - for (var i=0,l=para.length; i < l; i++) { - parameters[para[i]] = prop.getParameter(para[i]); - } + para.forEach(function (p) { + parameters[p] = prop.getParameter(p); + }); return parameters; } }; - var simpleReader = { - date: function(vevent, oldSimpleData, newSimpleData, key, parameters) { + const simpleReader = { + date: function (vevent, oldSimpleData, newSimpleData, key, parameters) { parameters = (parameters || []).concat(['tzid']); - simpleReader._readSingle(vevent, oldSimpleData, newSimpleData, key, parameters, function(v, isMultiValue) { - if (v.type === 'duration') { - return ICAL.Duration.fromSeconds(v.value); - } else { - return ICAL.Time.fromJSDate(v.value.toDate()); - } + simpleReader._readSingle(vevent, oldSimpleData, newSimpleData, key, parameters, function (v, isMultiValue) { + return (v.type === 'duration') ? ICAL.Duration.fromSeconds(v.value) : ICAL.Time.fromJSDate(v.value.toDate()); }); }, - dates: function(vevent, oldSimpleData, newSimpleData, key, parameters) { + dates: function (vevent, oldSimpleData, newSimpleData, key, parameters) { parameters = (parameters || []).concat(['tzid']); - simpleReader._readMultiple(vevent, oldSimpleData, newSimpleData, key, parameters, function(v, isMultiValue) { - var values = []; + simpleReader._readMultiple(vevent, oldSimpleData, newSimpleData, key, parameters, function (v, isMultiValue) { + const values = []; - for (var i=0, length=v.values.length; i < length; i++) { + v.values.forEach(function (value) { if (v.type === 'duration') { - values.push(ICAL.Duration.fromSeconds(v.values[i])); + values.push(ICAL.Duration.fromSeconds(value)); } else { - values.push(ICAL.Time.fromJSDate(v.values[i].toDate())); + values.push(ICAL.Time.fromJSDate(value.toDate())); } - } + }); return values; }); }, - string: function(vevent, oldSimpleData, newSimpleData, key, parameters) { - simpleReader._readSingle(vevent, oldSimpleData, newSimpleData, key, parameters, function(v, isMultiValue) { + string: function (vevent, oldSimpleData, newSimpleData, key, parameters) { + simpleReader._readSingle(vevent, oldSimpleData, newSimpleData, key, parameters, function (v, isMultiValue) { return isMultiValue ? v.values : v.value; }); }, - strings: function(vevent, oldSimpleData, newSimpleData, key, parameters) { - simpleReader._readMultiple(vevent, oldSimpleData, newSimpleData, key, parameters, function(v, isMultiValue) { + strings: function (vevent, oldSimpleData, newSimpleData, key, parameters) { + simpleReader._readMultiple(vevent, oldSimpleData, newSimpleData, key, parameters, function (v, isMultiValue) { return isMultiValue ? v.values : v.value; }); }, - _readSingle: function(vevent, oldSimpleData, newSimpleData, key, parameters, valueReader) { + _readSingle: function (vevent, oldSimpleData, newSimpleData, key, parameters, valueReader) { if (!newSimpleData[key]) { return; } if (!newSimpleData[key].hasOwnProperty('value') && !newSimpleData[key].hasOwnProperty('values')) { return; } - var isMultiValue = newSimpleData[key].hasOwnProperty('values'); + const isMultiValue = newSimpleData[key].hasOwnProperty('values'); - var prop = vevent.updatePropertyWithValue(key, valueReader(newSimpleData[key], isMultiValue)); + const prop = vevent.updatePropertyWithValue(key, valueReader(newSimpleData[key], isMultiValue)); simpleReader._readParameters(prop, newSimpleData[key], parameters); }, - _readMultiple: function(vevent, oldSimpleData, newSimpleData, key, parameters, valueReader) { - var oldGroups=[], properties=null, pKey=null, groupId; + _readMultiple: function (vevent, oldSimpleData, newSimpleData, key, parameters, valueReader) { + const oldGroups = []; + let properties, pKey, groupId; oldSimpleData[key] = oldSimpleData[key] || []; - for (var i=0, oldLength=oldSimpleData[key].length; i < oldLength; i++) { - oldGroups.push(oldSimpleData[key][i].group); - } + oldSimpleData[key].forEach(function (e) { + oldGroups.push(e.group); + }); newSimpleData[key] = newSimpleData[key] || []; - for (var j=0, newLength=newSimpleData[key].length; j < newLength; j++) { - var isMultiValue = newSimpleData[key][j].hasOwnProperty('values'); - var value = valueReader(newSimpleData[key][j], isMultiValue); + newSimpleData[key].forEach(function (e) { + const isMultiValue = e.hasOwnProperty('values'); + const value = valueReader(e, isMultiValue); - if (oldGroups.indexOf(newSimpleData[key][j].group) === -1) { - var property = new ICAL.Property(key); + if (oldGroups.indexOf(e.group) === -1) { + const property = new ICAL.Property(key); simpleReader._setProperty(property, value, isMultiValue); - simpleReader._readParameters(property, newSimpleData[key][j], parameters); + simpleReader._readParameters(property, e, parameters); vevent.addProperty(property); } else { - oldGroups.splice(oldGroups.indexOf(newSimpleData[key][j].group), 1); + oldGroups.splice(oldGroups.indexOf(e.group), 1); properties = vevent.getAllProperties(key); for (pKey in properties) { @@ -253,27 +228,23 @@ app.factory('SimpleEvent', function() { if (groupId === null) { continue; } - if (parseInt(groupId) === newSimpleData[key][j].group) { + if (parseInt(groupId) === e.group) { simpleReader._setProperty(properties[pKey], value, isMultiValue); - simpleReader._readParameters(properties[pKey], newSimpleData[key][j], parameters); + simpleReader._readParameters(properties[pKey], e, parameters); } } } - } + }); properties = vevent.getAllProperties(key); - for (pKey in properties) { - if (!properties.hasOwnProperty(pKey)) { - continue; - } - - groupId = properties[pKey].getParameter('x-oc-group-id'); + properties.forEach(function (property) { + groupId = property.getParameter('x-oc-group-id'); if (oldGroups.indexOf(parseInt(groupId)) !== -1) { - vevent.removeProperty(properties[pKey]); + vevent.removeProperty(property); } - } + }); }, - _readParameters: function(prop, simple, para) { + _readParameters: function (prop, simple, para) { if (!para) { return; } @@ -281,15 +252,15 @@ app.factory('SimpleEvent', function() { return; } - for (var i=0,l=para.length; i < l; i++) { - if (simple.parameters[para[i]]) { - prop.setParameter(para[i], simple.parameters[para[i]]); + para.forEach(function (p) { + if (simple.parameters[p]) { + prop.setParameter(p, simple.parameters[p]); } else { - prop.removeParameter(simple.parameters[para[i]]); + prop.removeParameter(simple.parameters[p]); } - } + }); }, - _setProperty: function(prop, value, isMultiValue) { + _setProperty: function (prop, value, isMultiValue) { if (isMultiValue) { prop.setValues(value); } else { @@ -301,20 +272,29 @@ app.factory('SimpleEvent', function() { /** * properties supported by event editor */ - var simpleProperties = { + const simpleProperties = { //General 'summary': {parser: simpleParser.string, reader: simpleReader.string}, 'location': {parser: simpleParser.string, reader: simpleReader.string}, - //'created': {parser: simpleParser.date, reader: simpleReader.date}, - //'last-modified': {parser: simpleParser.date, reader: simpleReader.date}, //'categories': {parser: simpleParser.strings, reader: simpleReader.strings}, //attendees - 'attendee': {parser: simpleParser.strings, reader: simpleReader.strings, parameters: attendeeParameters}, - 'organizer': {parser: simpleParser.string, reader: simpleReader.string, parameters: organizerParameters}, + 'attendee': { + parser: simpleParser.strings, + reader: simpleReader.strings, + parameters: attendeeParameters + }, + 'organizer': { + parser: simpleParser.string, + reader: simpleReader.string, + parameters: organizerParameters + }, //sharing 'class': {parser: simpleParser.string, reader: simpleReader.string}, //other - 'description': {parser: simpleParser.string, reader: simpleReader.string}, + 'description': { + parser: simpleParser.string, + reader: simpleReader.string + }, //'url': {parser: simpleParser.string, reader: simpleReader.string}, 'status': {parser: simpleParser.string, reader: simpleReader.string} //'resources': {parser: simpleParser.strings, reader: simpleReader.strings} @@ -323,19 +303,14 @@ app.factory('SimpleEvent', function() { /** * specific parsers that check only one property */ - var specificParser = { - alarm: function(data, vevent) { + const specificParser = { + alarm: function (data, vevent) { data.alarm = data.alarm || []; - var alarms = vevent.getAllSubcomponents('valarm'), - group = 0; - for (var key in alarms) { - if (!alarms.hasOwnProperty(key)) { - continue; - } - - var alarm = alarms[key]; - var alarmData = { + const alarms = vevent.getAllSubcomponents('valarm'); + let group = 0; + alarms.forEach(function (alarm) { + const alarmData = { group: group, action: {}, trigger: {}, @@ -351,8 +326,8 @@ app.factory('SimpleEvent', function() { //simpleParser.strings(alarmData, alarm, 'attendee', attendeeParameters); if (alarmData.trigger.type === 'duration' && alarm.hasProperty('trigger')) { - var trigger = alarm.getFirstProperty('trigger'); - var related = trigger.getParameter('related'); + const trigger = alarm.getFirstProperty('trigger'); + const related = trigger.getParameter('related'); if (related) { alarmData.trigger.related = related; } else { @@ -365,10 +340,12 @@ app.factory('SimpleEvent', function() { alarm.getFirstProperty('action') .setParameter('x-oc-group-id', group.toString()); group++; - } + }); }, - date: function(data, vevent) { - var dtstart = vevent.getFirstPropertyValue('dtstart'), dtend; + date: function (data, vevent) { + const dtstart = vevent.getFirstPropertyValue('dtstart'); + let dtend; + if (vevent.hasProperty('dtend')) { dtend = vevent.getFirstPropertyValue('dtend'); } else if (vevent.hasProperty('duration')) { @@ -383,25 +360,37 @@ app.factory('SimpleEvent', function() { zone: dtstart.zone.toString() }, type: dtstart.icaltype, - value: moment({years: dtstart.year, months: dtstart.month - 1, date: dtstart.day, - hours: dtstart.hour, minutes: dtstart.minute, seconds: dtstart.seconds}) + value: moment({ + years: dtstart.year, + months: dtstart.month - 1, + date: dtstart.day, + hours: dtstart.hour, + minutes: dtstart.minute, + seconds: dtstart.seconds + }) }; data.dtend = { parameters: { zone: dtend.zone.toString() }, type: dtend.icaltype, - value: moment({years: dtend.year, months: dtend.month - 1, date: dtend.day, - hours: dtend.hour, minutes: dtend.minute, seconds: dtend.seconds}) + value: moment({ + years: dtend.year, + months: dtend.month - 1, + date: dtend.day, + hours: dtend.hour, + minutes: dtend.minute, + seconds: dtend.seconds + }) }; data.allDay = (dtstart.icaltype === 'date' && dtend.icaltype === 'date'); }, - repeating: function(data, vevent) { - var iCalEvent = new ICAL.Event(vevent); + repeating: function (data, vevent) { + const iCalEvent = new ICAL.Event(vevent); data.repeating = iCalEvent.isRecurring(); - var rrule = vevent.getFirstPropertyValue('rrule'); + const rrule = vevent.getFirstPropertyValue('rrule'); if (rrule) { data.rrule = { count: rrule.count, @@ -422,34 +411,34 @@ app.factory('SimpleEvent', function() { } }; - var specificReader = { - alarm: function(vevent, oldSimpleData, newSimpleData) { - var oldGroups, newGroups, valarm, removedAlarms, components={}, key='alarm'; + const specificReader = { + alarm: function (vevent, oldSimpleData, newSimpleData) { + const components = {}, key = 'alarm'; - function getAlarmGroup(alarmData) { + function getAlarmGroup (alarmData) { return alarmData.group; } oldSimpleData[key] = oldSimpleData[key] || []; - oldGroups = oldSimpleData[key].map(getAlarmGroup); - + const oldGroups = oldSimpleData[key].map(getAlarmGroup); + newSimpleData[key] = newSimpleData[key] || []; - newGroups = newSimpleData[key].map(getAlarmGroup); - + const newGroups = newSimpleData[key].map(getAlarmGroup); + //check for any alarms that are in the old data, //but have been removed from the new data - removedAlarms = oldGroups.filter(function(group) { + const removedAlarms = oldGroups.filter(function (group) { return (newGroups.indexOf(group) === -1); }); //get all of the valarms and save them in an object keyed by their groupId - angular.forEach(vevent.getAllSubcomponents('valarm'), function(component) { - var group = component.getFirstProperty('action').getParameter('x-oc-group-id'); - components[group] = component; + vevent.getAllSubcomponents('valarm').forEach(function (alarm) { + const group = alarm.getFirstProperty('action').getParameter('x-oc-group-id'); + components[group] = alarm; }); //remove any valarm subcomponents have a groupId that matches one of the removedAlarms - angular.forEach(removedAlarms, function(group){ + removedAlarms.forEach(function (group) { if (components[group]) { vevent.removeSubcomponent(components[group]); delete components[group]; @@ -457,14 +446,16 @@ app.factory('SimpleEvent', function() { }); //update and create valarms using the new alarm data - angular.forEach(newSimpleData[key], function(alarmData) { + newSimpleData[key].forEach(function (alarmData) { + let valarm; + if (oldGroups.indexOf(alarmData.group) === -1) { valarm = new ICAL.Component('VALARM'); vevent.addSubcomponent(valarm); } else { valarm = components[alarmData.group]; } - + simpleReader.string(valarm, {}, alarmData, 'action', []); simpleReader.date(valarm, {}, alarmData, 'trigger', []); simpleReader.string(valarm, {}, alarmData, 'repeat', []); @@ -472,7 +463,7 @@ app.factory('SimpleEvent', function() { simpleReader.strings(valarm, {}, alarmData, 'attendee', attendeeParameters); }); }, - date: function(vevent, oldSimpleData, newSimpleData) { + date: function (vevent, oldSimpleData, newSimpleData) { vevent.removeAllProperties('dtstart'); vevent.removeAllProperties('dtend'); vevent.removeAllProperties('duration'); @@ -480,37 +471,35 @@ app.factory('SimpleEvent', function() { newSimpleData.dtstart.parameters.zone = newSimpleData.dtstart.parameters.zone || 'floating'; newSimpleData.dtend.parameters.zone = newSimpleData.dtend.parameters.zone || 'floating'; - if (newSimpleData.dtstart.parameters.zone !== 'floating' && - !ICAL.TimezoneService.has(newSimpleData.dtstart.parameters.zone)) { + if (newSimpleData.dtstart.parameters.zone !== 'floating' && !ICAL.TimezoneService.has(newSimpleData.dtstart.parameters.zone)) { throw { kind: 'timezone_missing', missing_timezone: newSimpleData.dtstart.parameters.zone }; } - if (newSimpleData.dtend.parameters.zone !== 'floating' && - !ICAL.TimezoneService.has(newSimpleData.dtend.parameters.zone)) { + if (newSimpleData.dtend.parameters.zone !== 'floating' && !ICAL.TimezoneService.has(newSimpleData.dtend.parameters.zone)) { throw { kind: 'timezone_missing', missing_timezone: newSimpleData.dtend.parameters.zone }; } - var start = ICAL.Time.fromJSDate(newSimpleData.dtstart.value.toDate(), false); + const start = ICAL.Time.fromJSDate(newSimpleData.dtstart.value.toDate(), false); start.isDate = newSimpleData.allDay; - var end = ICAL.Time.fromJSDate(newSimpleData.dtend.value.toDate(), false); + const end = ICAL.Time.fromJSDate(newSimpleData.dtend.value.toDate(), false); end.isDate = newSimpleData.allDay; - var availableTimezones = []; - var vtimezones = vevent.parent.getAllSubcomponents('vtimezone'); - angular.forEach(vtimezones, function(vtimezone) { + const availableTimezones = []; + const vtimezones = vevent.parent.getAllSubcomponents('vtimezone'); + vtimezones.forEach(function (vtimezone) { availableTimezones.push(vtimezone.getFirstPropertyValue('tzid')); }); - var dtstart = new ICAL.Property('dtstart', vevent); + const dtstart = new ICAL.Property('dtstart', vevent); dtstart.setValue(start); if (newSimpleData.dtstart.parameters.zone !== 'floating') { dtstart.setParameter('tzid', newSimpleData.dtstart.parameters.zone); - var startTz = ICAL.TimezoneService.get(newSimpleData.dtstart.parameters.zone); + const startTz = ICAL.TimezoneService.get(newSimpleData.dtstart.parameters.zone); start.zone = startTz; if (availableTimezones.indexOf(newSimpleData.dtstart.parameters.zone) === -1) { vevent.parent.addSubcomponent(startTz.component); @@ -518,11 +507,11 @@ app.factory('SimpleEvent', function() { } } - var dtend = new ICAL.Property('dtend', vevent); + const dtend = new ICAL.Property('dtend', vevent); dtend.setValue(end); if (newSimpleData.dtend.parameters.zone !== 'floating') { dtend.setParameter('tzid', newSimpleData.dtend.parameters.zone); - var endTz = ICAL.TimezoneService.get(newSimpleData.dtend.parameters.zone); + const endTz = ICAL.TimezoneService.get(newSimpleData.dtend.parameters.zone); end.zone = endTz; if (availableTimezones.indexOf(newSimpleData.dtend.parameters.zone) === -1) { vevent.parent.addSubcomponent(endTz.component); @@ -532,7 +521,7 @@ app.factory('SimpleEvent', function() { vevent.addProperty(dtstart); vevent.addProperty(dtend); }, - repeating: function(vevent, oldSimpleData, newSimpleData) { + repeating: function (vevent, oldSimpleData, newSimpleData) { // We won't support exrule, because it's deprecated and barely used in the wild if (newSimpleData.rrule === null || newSimpleData.rrule.freq === 'NONE') { vevent.removeAllProperties('rdate'); @@ -546,7 +535,7 @@ app.factory('SimpleEvent', function() { return; } - var params = { + const params = { interval: newSimpleData.rrule.interval, freq: newSimpleData.rrule.freq }; @@ -555,81 +544,75 @@ app.factory('SimpleEvent', function() { params.count = newSimpleData.rrule.count; } - var rrule = new ICAL.Recur(params); + const rrule = new ICAL.Recur(params); vevent.updatePropertyWithValue('rrule', rrule); } }; - function SimpleEvent(event) { - this._event = event; - angular.extend(this, defaults); - - var parser, parameters; - for (var key in simpleProperties) { - if (!simpleProperties.hasOwnProperty(key)) { - continue; - } - - parser = simpleProperties[key].parser; - parameters = simpleProperties[key].parameters; - if (this._event.hasProperty(key)) { - parser(this, this._event, key, parameters); - } - } - - for (parser in specificParser) { - if (!specificParser.hasOwnProperty(parser)) { - continue; - } - - specificParser[parser](this, this._event); - } - - this._generateOldProperties(); - } + function SimpleEvent (event) { + const context = { + event, + oldProperties: {} + }; - SimpleEvent.prototype = { - _generateOldProperties: function() { - this._oldProperties = {}; + const iface = { + _isASimpleEventObject: true, + }; + angular.extend(iface, defaults); - for (var def in defaults) { - if (!defaults.hasOwnProperty(def)) { - continue; - } + context.generateOldProperties = function () { + context.oldProperties = {}; - this._oldProperties[def] = angular.copy(this[def]); + for (let key in defaults) { + context.oldProperties[key] = angular.copy(iface[key]); } - }, - patch: function() { - var key, reader, parameters; + }; - for (key in simpleProperties) { - if (!simpleProperties.hasOwnProperty(key)) { - continue; - } + iface.patch = function () { + for (let simpleKey in simpleProperties) { + const simpleProperty = simpleProperties[simpleKey]; - reader = simpleProperties[key].reader; - parameters = simpleProperties[key].parameters; - if (this._oldProperties[key] !== this[key]) { - if (this[key] === null) { - this._event.removeAllProperties(key); + const reader = simpleProperty.reader; + const parameters = simpleProperty.parameters; + if (context.oldProperties[simpleKey] !== iface[simpleKey]) { + if (iface[simpleKey] === null) { + context.event.removeAllProperties(simpleKey); } else { - reader(this._event, this._oldProperties, this, key, parameters); + reader(context.event, context.oldProperties, iface, simpleKey, parameters); } } } - for (key in specificReader) { - if (!specificReader.hasOwnProperty(key)) { - continue; - } + for (let specificKey in specificReader) { + const reader = specificReader[specificKey]; + reader(context.event, context.oldProperties, iface); + } + + context.generateOldProperties(); + }; - reader = specificReader[key]; - reader(this._event, this._oldProperties, this); + for (let simpleKey in simpleProperties) { + const simpleProperty = simpleProperties[simpleKey]; + + const parser = simpleProperty.parser; + const parameters = simpleProperty.parameters; + if (context.event.hasProperty(simpleKey)) { + parser(iface, context.event, simpleKey, parameters); } + } - this._generateOldProperties(); + for (let specificKey in specificParser) { + const parser = specificParser[specificKey]; + parser(iface, context.event); } + + context.generateOldProperties(); + + return iface; + } + + SimpleEvent.isSimpleEvent = function (obj) { + return (typeof obj === 'object' && obj !== null && obj._isASimpleEventObject === true); }; return SimpleEvent; diff --git a/js/app/models/veventModel.js b/js/app/models/veventModel.js new file mode 100644 index 0000000000..1ee110559c --- /dev/null +++ b/js/app/models/veventModel.js @@ -0,0 +1,289 @@ +/** + * Calendar App + * + * @author Raghu Nayyar + * @author Georg Ehrke + * @copyright 2016 Raghu Nayyar + * @copyright 2016 Georg Ehrke + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with this library. If not, see . + * + */ + +app.factory('VEvent', function(FcEvent, SimpleEvent, ICalFactory, StringUtility) { + 'use strict'; + + /** + * get a VEvent object + * @param {Calendar} calendar + * @param {ICAL.Component} comp + * @param {string} uri + * @param {string} etag + */ + function VEvent(calendar, comp, uri, etag='') { + const context = {calendar, comp, uri, etag}; + const iface = { + _isAVEventObject: true + }; + + if (!context.comp || !context.comp.jCal || context.comp.jCal.length === 0) { + throw new TypeError('Given comp is not a valid calendar'); + } + + // read all timezones in the comp and register them + const vtimezones = comp.getAllSubcomponents('vtimezone'); + vtimezones.forEach(function(vtimezone) { + const timezone = new ICAL.Timezone(vtimezone); + ICAL.TimezoneService.register(timezone.tzid, timezone); + }); + + if (!uri) { + const vevent = context.comp.getFirstSubcomponent('vevent'); + context.uri = vevent.getFirstPropertyValue('uid'); + } + + /** + * get DTEND from vevent + * @param {ICAL.Component} vevent + * @returns {ICAL.Time} + */ + context.calculateDTEnd = function(vevent) { + if (vevent.hasProperty('dtend')) { + return vevent.getFirstPropertyValue('dtend'); + } else if (vevent.hasProperty('duration')) { + const dtstart = vevent.getFirstPropertyValue('dtstart').clone(); + dtstart.addDuration(vevent.getFirstPropertyValue('duration')); + + return dtstart; + } else { + return vevent.getFirstPropertyValue('dtstart').clone(); + } + }; + + /** + * convert a dt's timezone if necessary + * @param {ICAL.Time} dt + * @param {ICAL.Component} timezone + * @returns {ICAL.Time} + */ + context.convertTz = function(dt, timezone) { + if (context.needsTzConversion(dt) && timezone) { + dt = dt.convertToZone(timezone); + } + + return dt; + }; + + /** + * check if we need to convert the timezone of either dtstart or dtend + * @param {ICAL.Time} dt + * @returns {boolean} + */ + context.needsTzConversion = function(dt) { + return (dt.icaltype !== 'date' && + dt.zone !== ICAL.Timezone.utcTimezone && + dt.zone !== ICAL.Timezone.localTimezone); + }; + + Object.defineProperties(iface, { + calendar: { + get: function() { + return context.calendar; + }, + set: function(calendar) { + context.calendar = calendar; + } + }, + comp: { + get: function() { + return context.comp; + } + }, + data: { + get: function() { + return context.comp.toString(); + } + }, + etag: { + get: function() { + return context.etag; + }, + set: function(etag) { + context.etag = etag; + } + }, + uri: { + get: function() { + return context.uri; + } + } + }); + + /** + * get fullcalendar event in a defined time-range + * @param {moment} start + * @param {moment} end + * @param {Timezone} timezone + * @returns {Array} + */ + iface.getFcEvent = function(start, end, timezone) { + const iCalStart = ICAL.Time.fromJSDate(start.toDate()); + const iCalEnd = ICAL.Time.fromJSDate(end.toDate()); + const fcEvents = []; + + const vevents = context.comp.getAllSubcomponents('vevent'); + vevents.forEach(function(vevent) { + const iCalEvent = new ICAL.Event(vevent); + + if (!vevent.hasProperty('dtstart')) { + return; + } + + const rawDtstart = vevent.getFirstPropertyValue('dtstart'); + const rawDtend = context.calculateDTEnd(vevent); + + if (iCalEvent.isRecurring()) { + const duration = rawDtend.subtractDate(rawDtstart); + const iterator = new ICAL.RecurExpansion({ + component: vevent, + dtstart: rawDtstart + }); + + let next; + while ((next = iterator.next())) { + if (next.compare(iCalStart) < 0) { + continue; + } + if (next.compare(iCalEnd) > 0) { + break; + } + + const singleDtStart = next.clone(); + const singleDtEnd = next.clone(); + singleDtEnd.addDuration(duration); + + const dtstart = context.convertTz(singleDtStart, timezone.jCal); + const dtend = context.convertTz(singleDtEnd, timezone.jCal); + const fcEvent = FcEvent(iface, vevent, dtstart, dtend); + + fcEvents.push(fcEvent); + } + } else { + const dtstart = context.convertTz(rawDtstart, timezone.jCal); + const dtend = context.convertTz(rawDtend, timezone.jCal); + const fcEvent = FcEvent(iface, vevent, dtstart, dtend); + + fcEvents.push(fcEvent); + } + }); + + return fcEvents; + }; + + /** + * + * @param searchedRecurrenceId + * @returns {SimpleEvent} + */ + iface.getSimpleEvent = function(searchedRecurrenceId) { + const vevents = context.comp.getAllSubcomponents('vevent'); + + const veventsLength = vevents.length; + for (let i=0; i < veventsLength; i++) { + const vevent = vevents[i]; + const hasRecurrenceId = vevent.hasProperty('recurrence-id'); + const recurrenceId = event.getFirstPropertyValue('recurrence-id'); + + if (!hasRecurrenceId && !searchedRecurrenceId || + hasRecurrenceId && searchedRecurrenceId === recurrenceId) { + return SimpleEvent(vevent); + } + } + + throw new Error('Event not found'); + }; + + /** + * update events last-modified property to now + */ + iface.touch = function() { + const vevent = context.comp.getFirstSubcomponent('vevent'); + vevent.updatePropertyWithValue('last-modified', ICAL.Time.now()); + }; + + return iface; + } + + VEvent.isVEvent = function(obj) { + return (typeof obj === 'object' && obj !== null && obj._isAVEventObject === true); + }; + + /** + * create a VEvent object from raw ics data + * @param {Calendar} calendar + * @param {string} ics + * @param {string} uri + * @param {string} etag + * @returns {VEvent} + */ + VEvent.fromRawICS = function(calendar, ics, uri, etag='') { + let comp; + try { + const jCal = ICAL.parse(ics); + comp = new ICAL.Component(jCal); + } catch (e) { + console.log(e); + throw new TypeError('given ics data was not valid'); + } + + return VEvent(calendar, comp, uri, etag); + }; + + + /** + * generates a new VEvent based on start and end + * @param start + * @param end + * @param timezone + * @returns {VEvent} + */ + VEvent.fromStartEnd = function(start, end, timezone) { + const uid = StringUtility.uid(); + const comp = ICalFactory.newEvent(uid); + const uri = StringUtility.uid('Nextcloud', 'ics'); + const vevent = VEvent(null, comp, uri); + const simple = vevent.getSimpleEvent(); + + simple.allDay = !start.hasTime() && !end.hasTime(); + simple.dtstart = { + type: start.hasTime() ? 'datetime' : 'date', + value: start, + parameters: { + zone: timezone + } + }; + simple.dtend = { + type: end.hasTime() ? 'datetime' : 'date', + value: end, + parameters: { + zone: timezone + } + }; + simple.patch(); + + return vevent; + }; + + return VEvent; +}); diff --git a/js/app/models/veventmodel.js b/js/app/models/veventmodel.js deleted file mode 100644 index e6c9ce7038..0000000000 --- a/js/app/models/veventmodel.js +++ /dev/null @@ -1,290 +0,0 @@ -/** - * Calendar App - * - * @author Raghu Nayyar - * @author Georg Ehrke - * @copyright 2016 Raghu Nayyar - * @copyright 2016 Georg Ehrke - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE - * License as published by the Free Software Foundation; either - * version 3 of the License, or any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU AFFERO GENERAL PUBLIC LICENSE for more details. - * - * You should have received a copy of the GNU Affero General Public - * License along with this library. If not, see . - * - */ - -app.factory('VEvent', function(FcEvent, SimpleEvent, ICalFactory, RandomStringService) { - 'use strict'; - - /** - * get DTEND from vevent - * @param {ICAL.Component} vevent - * @returns {ICAL.Time} - */ - function calculateDTEnd(vevent) { - if (vevent.hasProperty('dtend')) { - return vevent.getFirstPropertyValue('dtend'); - } else if (vevent.hasProperty('duration')) { - var dtstart = vevent.getFirstPropertyValue('dtstart').clone(); - dtstart.addDuration(vevent.getFirstPropertyValue('duration')); - return dtstart; - } else { - return vevent.getFirstPropertyValue('dtstart').clone(); - } - } - - /** - * check if we need to convert the timezone of either dtstart or dtend - * @param {ICAL.Time} dt - * @returns {boolean} - */ - function isTimezoneConversionNecessary(dt) { - return (dt.icaltype !== 'date' && - dt.zone !== ICAL.Timezone.utcTimezone && - dt.zone !== ICAL.Timezone.localTimezone); - } - - /** - * convert a dt's timezone if necessary - * @param {ICAL.Time} dt - * @param {ICAL.Component} timezone - * @returns {ICAL.Time} - */ - function convertTimezoneIfNecessary(dt, timezone) { - if (isTimezoneConversionNecessary(dt) && timezone) { - dt = dt.convertToZone(timezone); - } - - return dt; - } - - /** - * parse an recurring event - * @param vevent - * @param event - * @param start - * @param end - * @param timezone - * @return [] - */ - function getTimeForRecurring(vevent, event, start, end, timezone) { - var dtstart = event.getFirstPropertyValue('dtstart'); - var dtend = calculateDTEnd(event); - var duration = dtend.subtractDate(dtstart); - var fcEvents = []; - - var iterator = new ICAL.RecurExpansion({ - component: event, - dtstart: dtstart - }); - - var next; - while ((next = iterator.next())) { - if (next.compare(start) < 0) { - continue; - } - if (next.compare(end) > 0) { - break; - } - - var singleDtStart = next.clone(); - var singleDtEnd = next.clone(); - singleDtEnd.addDuration(duration); - - fcEvents.push(new FcEvent(vevent, event, - convertTimezoneIfNecessary(singleDtStart, timezone), - convertTimezoneIfNecessary(singleDtEnd, timezone))); - } - - return fcEvents; - } - - /** - * parse a single event - * @param vevent - * @param event - * @param timezone - * @returns {FcEvent} - */ - function getTime(vevent, event, timezone) { - var dtstart = event.getFirstPropertyValue('dtstart'); - var dtend = calculateDTEnd(event); - - return new FcEvent(vevent, event, - convertTimezoneIfNecessary(dtstart, timezone), - convertTimezoneIfNecessary(dtend, timezone)); - } - - /** - * register timezones from ical response - * @param {ICAL.Component} components - */ - function registerTimezones(components) { - var vtimezones = components.getAllSubcomponents('vtimezone'); - angular.forEach(vtimezones, function (vtimezone) { - var timezone = new ICAL.Timezone(vtimezone); - ICAL.TimezoneService.register(timezone.tzid, timezone); - }); - } - - /** - * @constructor - * @param {Calendar} calendar - * @param {string|ICAL.Component} ical - * @param {string|null} etag - * @param {string|null} uri - * @constructor - */ - function VEvent(calendar, ical, etag, uri) { - if (typeof ical === 'string') { - try { - var jcal = ICAL.parse(ical); - this.comp = new ICAL.Component(jcal); - } catch (e) { - console.log(e); - throw VEvent.INVALID; - } - } else if (Object.getPrototypeOf(ical) === ICAL.Component.prototype) { - this.comp = ical; - } - - if (!this.comp || this.comp.jCal.length === 0) { - throw VEvent.INVALID; - } - - registerTimezones(this.comp); - - etag = etag || ''; - if (typeof uri === 'undefined') { - const vevent = this.comp.getFirstSubcomponent('vevent'); - uri = vevent.getFirstPropertyValue('uid'); - } - - angular.extend(this, { - calendar: calendar, - etag: etag, - uri: uri - }); - } - - VEvent.prototype = { - /** - * serialize jsical object to actual ical data - * @returns {String} - */ - get data() { - return this.comp.toString(); - }, - /** - * - * @param start - * @param end - * @param timezone - * @returns {Array} - */ - getFcEvent: function(start, end, timezone) { - var iCalStart = ICAL.Time.fromJSDate(start.toDate()); - var iCalEnd = ICAL.Time.fromJSDate(end.toDate()); - var renderedEvents = [], self = this; - - var vevents = this.comp.getAllSubcomponents('vevent'); - angular.forEach(vevents, function (event) { - var iCalEvent = new ICAL.Event(event); - - if (!event.hasProperty('dtstart')) { - return; - } - - if (iCalEvent.isRecurring()) { - angular.extend(renderedEvents, - getTimeForRecurring(self, event, iCalStart, iCalEnd, timezone.jCal)); - } else { - renderedEvents.push(getTime(self, event, timezone.jCal)); - } - }); - - return renderedEvents; - }, - /** - * - * @param recurrenceId - * @returns {SimpleEvent} - */ - getSimpleEvent: function(recurrenceId) { - var vevents = this.comp.getAllSubcomponents('vevent'), simpleEvent = null; - - angular.forEach(vevents, function (event) { - var hasRecurrenceId = event.hasProperty('recurrence-id'); - if ((!hasRecurrenceId && recurrenceId === null) || - (hasRecurrenceId && recurrenceId === event.getFirstPropertyValue('recurrence-id'))) { - simpleEvent = new SimpleEvent(event); - } - }); - - return simpleEvent; - } - }; - - /** - * - * @param start - * @param end - * @param timezone - * @returns {VEvent} - */ - VEvent.fromStartEnd = function(start, end, timezone) { - var comp = ICalFactory.new(); - - var iCalEvent = new ICAL.Component('vevent'); - comp.addSubcomponent(iCalEvent); - iCalEvent.updatePropertyWithValue('created', ICAL.Time.now()); - iCalEvent.updatePropertyWithValue('dtstamp', ICAL.Time.now()); - iCalEvent.updatePropertyWithValue('last-modified', ICAL.Time.now()); - iCalEvent.updatePropertyWithValue('uid', RandomStringService.generate()); - // add a dummy dtstart to make the ical valid before we create SimpleEvent - iCalEvent.updatePropertyWithValue('dtstart', ICAL.Time.now()); - - var uri = RandomStringService.generate(); - uri += RandomStringService.generate(); - uri += '.ics'; - - var vevent = new VEvent(null, comp, null, uri); - var simple = new SimpleEvent(iCalEvent); - angular.extend(simple, { - allDay: !start.hasTime() && !end.hasTime(), - dtstart: { - type: start.hasTime() ? 'datetime' : 'date', - value: start, - parameters: { - zone: timezone - } - }, - dtend: { - type: end.hasTime() ? 'datetime' : 'date', - value: end, - parameters: { - zone: timezone - } - } - }); - simple.patch(); - - return vevent; - }; - - /** - * - * @type {string} - */ - VEvent.INVALID = 'INVALID_EVENT'; - - return VEvent; -}); diff --git a/js/app/models/webcalModel.js b/js/app/models/webcalModel.js index dd96e90f4b..21ceb4d815 100644 --- a/js/app/models/webcalModel.js +++ b/js/app/models/webcalModel.js @@ -59,7 +59,7 @@ app.factory('WebCal', function($http, Calendar, VEvent, TimezoneService, WebCalS response.vevents.forEach(function(ical) { try { - const vevent = new VEvent(iface, ical); + const vevent = VEvent.fromRawICS(iface, ical); const events = vevent.getFcEvent(start, end, tz); vevents = vevents.concat(events); } catch (err) { diff --git a/js/app/service/veventService.js b/js/app/service/veventService.js index 0c5250e8c0..660cd781a2 100644 --- a/js/app/service/veventService.js +++ b/js/app/service/veventService.js @@ -115,7 +115,7 @@ app.service('VEventService', function(DavClient, StringUtility, XMLUtility, VEve const uri = obj.href.substr(obj.href.lastIndexOf('/') + 1); try { - const vevent = new VEvent(calendar, calendarData, etag, uri); + const vevent = VEvent.fromRawICS(calendar, calendarData, uri, etag); vevents.push(vevent); } catch (e) { console.log(e); @@ -147,7 +147,7 @@ app.service('VEventService', function(DavClient, StringUtility, XMLUtility, VEve const etag = response.xhr.getResponseHeader('ETag'); try { - return new VEvent(calendar, calendarData, etag, uri); + return VEvent.fromRawICS(calendar, calendarData, uri, etag); } catch (e) { console.log(e); return Promise.reject(e); diff --git a/tests/js/unit/factory/icalFactorySpec.js b/tests/js/unit/factory/icalFactorySpec.js index 1518a190dc..5880f1f534 100644 --- a/tests/js/unit/factory/icalFactorySpec.js +++ b/tests/js/unit/factory/icalFactorySpec.js @@ -30,4 +30,29 @@ describe('ICalFactory tests', function () { expect(ical.getFirstPropertyValue('calscale')).toEqual('GREGORIAN'); expect(ical.getFirstPropertyValue('prodid')).toEqual('-//Nextcloud calendar v42.2.4'); }); -}); \ No newline at end of file + + it ('should return an ICAL object with an event in it', function() { + const baseTime = new Date(2016, 0, 1); + jasmine.clock().mockDate(baseTime); + + const uid = 'foobar'; + + const ical = ICalFactory.newEvent(uid); + expect(ical.getFirstPropertyValue('version')).toEqual('2.0'); + expect(ical.getFirstPropertyValue('calscale')).toEqual('GREGORIAN'); + expect(ical.getFirstPropertyValue('prodid')).toEqual('-//Nextcloud calendar v42.2.4'); + + const components = ical.getAllSubcomponents(); + expect(components.length).toEqual(1); + + expect(components[0].name).toEqual('vevent'); + expect(components[0].getAllProperties().length).toEqual(5); + + expect(components[0].getFirstPropertyValue('created').toString()).toEqual('2016-01-01T00:00:00'); + expect(components[0].getFirstPropertyValue('dtstamp').toString()).toEqual('2016-01-01T00:00:00'); + expect(components[0].getFirstPropertyValue('last-modified').toString()).toEqual('2016-01-01T00:00:00'); + expect(components[0].getFirstPropertyValue('uid')).toEqual('foobar'); + expect(components[0].getFirstPropertyValue('dtstart').toString()).toEqual('2016-01-01T00:00:00'); + + }); +}); diff --git a/tests/js/unit/models/fcEventModelSpec.js b/tests/js/unit/models/fcEventModelSpec.js new file mode 100644 index 0000000000..ca80de59ef --- /dev/null +++ b/tests/js/unit/models/fcEventModelSpec.js @@ -0,0 +1,51 @@ +describe('The FullCalendar Event factory', function () { + 'use strict'; + + let FcEvent, SimpleEvent; + + beforeEach(module('Calendar', function ($provide) { + SimpleEvent = jasmine.createSpy(); + + $provide.value('SimpleEvent', SimpleEvent); + })); + + beforeEach(inject(function (_FcEvent_) { + FcEvent = _FcEvent_; + })); + + it ('should initialize correctly', function() { + + }); + + it ('should check if it\'s an FcEvent object', function() { + + }); + + it ('should return a simpleEvent object', function() { + + }); + + it ('should provide a defined set of properties', function() { + + }); + + it ('should drop an event correctly - with DTEND', function() { + + }); + + it ('should drop an event correctly - without DTEND', function() { + + }); + + it ('should resize an event correctly - with DURATION', function() { + + }); + + it ('should resize an event correctly - with DTEND', function() { + + }); + + it ('should resize an event correctly - with neither DURATION nor DTEND', function() { + + }); +}); diff --git a/tests/js/unit/models/simpleEventModelSpec.js b/tests/js/unit/models/simpleEventModelSpec.js new file mode 100644 index 0000000000..e966b6ff28 --- /dev/null +++ b/tests/js/unit/models/simpleEventModelSpec.js @@ -0,0 +1,171 @@ +describe('The SimpleEvent factory', function () { + 'use strict'; + + let SimpleEvent; + + beforeEach(module('Calendar')); + + beforeEach(inject(function (_SimpleEvent_) { + SimpleEvent = _SimpleEvent_; + })); + + it ('should initialize correctly', function() { + + }); + + it ('should check if it\'s an SimpleEvent object', function() { + + }); + + it ('should patch an object once', function() { + + }); + + it ('should patch an object twice', function() { + + }); + + it ('should add a summary', function() { + + }); + + it ('should modify the summary', function() { + + }); + + it ('should delete the summary', function() { + + }); + + it ('should add a location', function() { + + }); + + it ('should modify the location', function() { + + }); + + it ('should delete the location', function() { + + }); + + it ('should add a attendee', function() { + + }); + + it ('should add multiple attendee', function() { + + }); + + it ('should modify an attendee', function() { + + }); + + it ('should modify an attendee without altering the other ones', function() { + + }); + + it ('should delete an attendee', function() { + + }); + + it ('should delete an attendee without altering/deleting the other ones', function() { + + }); + + it ('should add an organizer', function() { + + }); + + it ('should modify the organizer', function() { + + }); + + it ('should delete the organizer', function() { + + }); + + it ('should add a class', function() { + + }); + + it ('should modify the class', function() { + + }); + + it ('should delete the class', function() { + + }); + + it ('should add a description', function() { + + }); + + it ('should modify the description', function() { + + }); + + it ('should delete the description', function() { + + }); + + it ('should add a status', function() { + + }); + + it ('should modify the status', function() { + + }); + + it ('should delete the status', function() { + + }); + + it ('should add an alarm', function() { + + }); + + it ('should add multiple alarm', function() { + + }); + + it ('should modify an alarm', function() { + + }); + + it ('should modify an alarm without altering the others', function() { + + }); + + it ('should delete an alarm', function() { + + }); + + it ('should delete an alarm without altering the others', function() { + + }); + + it ('should add the general date-time information', function() { + + }); + + it ('should modify the general date-time information', function() { + + }); + + it ('should delete the general date-time information', function() { + + }); + + it ('should add the repeating information', function() { + + }); + + it ('should modify the repeating information', function() { + + }); + + it ('should delete the repeating information', function() { + + }); +}); diff --git a/tests/js/unit/models/veventModelSpec.js b/tests/js/unit/models/veventModelSpec.js new file mode 100644 index 0000000000..cbb20e4e61 --- /dev/null +++ b/tests/js/unit/models/veventModelSpec.js @@ -0,0 +1,623 @@ +describe('The VEvent factory', function () { + 'use strict'; + + let VEvent, FcEvent, SimpleEvent, ICalFactory, StringUtility; + + const ics1 = `BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR`; + + const ics2 = `BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Tests// +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:America/New_York +BEGIN:DAYLIGHT +TZNAME:EDT +RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4 +DTSTART:20000402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZNAME:EDT +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +DTSTART:20070311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10 +DTSTART:20001029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:STANDARD +TZNAME:EST +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +DTSTART:20071104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR`; + + const ics3 = `BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:America/New_York +X-LIC-LOCATION:America/New_York +BEGIN:DAYLIGHT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:EDT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:EST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20161002T105542Z +UID:DF8A5F8D-9037-4FA3-84CC-97FB6D5D0DA9 +DTEND;TZID=America/New_York:20161004T113000 +TRANSP:OPAQUE +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +SUMMARY:Event 1 +DTSTART;TZID=America/New_York:20161004T090000 +DTSTAMP:20161002T105552Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR`; + + const ics4 = `BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:America/New_York +X-LIC-LOCATION:America/New_York +BEGIN:DAYLIGHT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:EDT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:EST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20161002T105555Z +UID:C8E094B8-A7E6-4CF3-9E59-58608B9B61C5 +TRANSP:OPAQUE +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +SUMMARY:Event 2 +DTSTART;TZID=America/New_York:20160925T000000 +DURATION:P15DT5H0M20S +DTSTAMP:20161002T105633Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR`; + + const ics5 = `BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:America/New_York +X-LIC-LOCATION:America/New_York +BEGIN:DAYLIGHT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:EDT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:EST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20161002T105635Z +UID:9D0C33D1-334E-4B46-9E0E-D62C11E60700 +TRANSP:OPAQUE +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +SUMMARY:Event 3 +DTSTART;TZID=America/New_York:20161105T235900 +DTSTAMP:20161002T105648Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR`; + + const timezone_nyc = { + jCal: new ICAL.Timezone(new ICAL.Component(ICAL.parse(`BEGIN:VTIMEZONE +TZID:America/New_York +X-LIC-LOCATION:America/New_York +BEGIN:DAYLIGHT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:EDT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:EST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +END:STANDARD +END:VTIMEZONE`))), + name: 'America/New_York' + }; + const timezone_berlin = { + jCal: new ICAL.Timezone(new ICAL.Component(ICAL.parse(`BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +END:VTIMEZONE +`))), + name: 'Europe/Berlin' + }; + const timezone_utc = { + jCal: ICAL.TimezoneService.get('UTC'), + name: 'UTC' + }; + + beforeEach(module('Calendar', function ($provide) { + FcEvent = jasmine.createSpy().and.callFake(function() { + return Array.from(arguments); + }); + SimpleEvent = jasmine.createSpy().and.callFake(function() { + return Array.from(arguments); + }); + + ICalFactory = {}; + ICalFactory.newEvent = jasmine.createSpy().and.callFake(function(uid) { + + }); + + StringUtility = {}; + StringUtility.uid = jasmine.createSpy(); + + spyOn(ICAL.TimezoneService, 'register'); + + $provide.value('FcEvent', FcEvent); + $provide.value('SimpleEvent', SimpleEvent); + $provide.value('ICalFactory', ICalFactory); + $provide.value('StringUtility', StringUtility); + })); + + beforeEach(inject(function (_VEvent_) { + VEvent = _VEvent_; + })); + + it ('should initialize correctly', function() { + const calendar = {this_is_a_fancy_calendar: true}; + const comp = new ICAL.Component(ICAL.parse(ics1)); + const uri = 'foobar123'; + const etag = 'etag2.7182'; + + expect(() => VEvent(calendar, comp, uri, etag)).not.toThrow(); + const vevent = VEvent(calendar, comp, uri, etag); + expect(vevent.calendar).toEqual(calendar); + expect(vevent.comp).toEqual(comp); + expect(vevent.uri).toEqual(uri); + expect(vevent.etag).toEqual(etag); + }); + + it ('should set etag to empty string when not set', function() { + const calendar = {this_is_a_fancy_calendar: true}; + const comp = new ICAL.Component(ICAL.parse(ics1)); + const uri = 'foobar123'; + + expect(() => VEvent(calendar, comp, uri)).not.toThrow(); + const vevent = VEvent(calendar, comp, uri); + expect(vevent.calendar).toEqual(calendar); + expect(vevent.comp).toEqual(comp); + expect(vevent.uri).toEqual(uri); + expect(vevent.etag).toEqual(''); + }); + + it ('should set the uri to the events uid when not set', function() { + const calendar = {this_is_a_fancy_calendar: true}; + const comp = new ICAL.Component(ICAL.parse(ics1)); + + expect(() => VEvent(calendar, comp)).not.toThrow(); + const vevent = VEvent(calendar, comp); + expect(vevent.calendar).toEqual(calendar); + expect(vevent.comp).toEqual(comp); + expect(vevent.uri).toEqual('0AD16F58-01B3-463B-A215-FD09FC729A02'); + expect(vevent.etag).toEqual(''); + }); + + it ('should throw a typeerror when second parameter is not a comp', function() { + const calendar = {this_is_a_fancy_calendar: true}; + + expect(() => VEvent(calendar, 'foobar')).toThrowError(TypeError, 'Given comp is not a valid calendar'); + }); + + it ('should register timezones in the given comp', function() { + const calendar = {this_is_a_fancy_calendar: true}; + const comp = new ICAL.Component(ICAL.parse(ics2)); + + const vevent = VEvent(calendar, comp); + expect(vevent.calendar).toEqual(calendar); + + expect(ICAL.TimezoneService.register.calls.count()).toEqual(2); + expect(ICAL.TimezoneService.register.calls.argsFor(0).length).toEqual(2); + expect(ICAL.TimezoneService.register.calls.argsFor(0)[0]).toEqual('Europe/Berlin'); + expect(ICAL.TimezoneService.register.calls.argsFor(0)[1].component.toString()).toEqual(`BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE`.split("\n").join("\r\n")); + expect(ICAL.TimezoneService.register.calls.argsFor(1).length).toEqual(2); + expect(ICAL.TimezoneService.register.calls.argsFor(1)[0]).toEqual('America/New_York'); + expect(ICAL.TimezoneService.register.calls.argsFor(1)[1].component.toString()).toEqual(`BEGIN:VTIMEZONE +TZID:America/New_York +BEGIN:DAYLIGHT +TZNAME:EDT +RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4 +DTSTART:20000402T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:DAYLIGHT +TZNAME:EDT +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +DTSTART:20070311T020000 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:EST +RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10 +DTSTART:20001029T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:STANDARD +TZNAME:EST +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +DTSTART:20071104T020000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +END:VTIMEZONE`.split("\n").join("\r\n")); + }); + + it ('should have a writable calendar property', function() { + const calendar1 = {this_is_a_fancy_calendar: true}; + const calendar2 = {this_is_another_fancy_calendar: true}; + const comp = new ICAL.Component(ICAL.parse(ics1)); + + const vevent = VEvent(calendar1, comp); + expect(vevent.calendar).toEqual(calendar1); + + vevent.calendar = calendar2; + expect(vevent.calendar).toEqual(calendar2); + }); + + it ('should have a read-only comp property', function() { + const calendar = {this_is_a_fancy_calendar: true}; + const comp1 = new ICAL.Component(ICAL.parse(ics1)); + const comp2 = new ICAL.Component(ICAL.parse(ics2)); + + const vevent = VEvent(calendar, comp1); + expect(vevent.comp).toEqual(comp1); + + expect(() => vevent.comp = comp2).toThrowError(TypeError); + }); + + it ('should have a read-only data property', function() { + const calendar = {this_is_a_fancy_calendar: true}; + const comp1 = new ICAL.Component(ICAL.parse(ics1)); + + const vevent = VEvent(calendar, comp1); + expect(vevent.data).toEqual(ics1.split("\n").join("\r\n")); + + expect(() => vevent.data = 'foobar').toThrowError(TypeError); + }); + + it ('should have a writable etag property', function() { + const calendar = {this_is_a_fancy_calendar: true}; + const comp = new ICAL.Component(ICAL.parse(ics1)); + const etag1 = '123'; + const etag2 = '456'; + + const vevent = VEvent(calendar, comp, '', etag1); + expect(vevent.etag).toEqual(etag1); + + vevent.etag = etag2; + + expect(vevent.etag = etag2); + }); + + it ('should have a read-only uri property', function() { + const calendar = {this_is_a_fancy_calendar: true}; + const comp = new ICAL.Component(ICAL.parse(ics1)); + const uri1 = 'foobar'; + const uri2 = 'barfoo'; + + const vevent = VEvent(calendar, comp, uri1); + expect(vevent.uri).toEqual(uri1); + + expect(() => vevent.uri = uri2).toThrowError(TypeError); + }); + + it ('should generate FcEvents for a dedicated time-range - single event with DTEND', function() { + const calendar = {this_is_a_fancy_calendar: true}; + const comp = new ICAL.Component(ICAL.parse(ics3)); + + const vevent = VEvent(calendar, comp); + const start = moment('2016-09-25'); + const end = moment('2016-11-06'); + + const fcEvents = vevent.getFcEvent(start, end, timezone_nyc); + expect(fcEvents.length).toEqual(1); + expect(fcEvents[0].length).toEqual(4); + expect(fcEvents[0][0]).toEqual(vevent); + expect(fcEvents[0][1].toString()).toEqual(`BEGIN:VEVENT +CREATED:20161002T105542Z +UID:DF8A5F8D-9037-4FA3-84CC-97FB6D5D0DA9 +DTEND;TZID=America/New_York:20161004T113000 +TRANSP:OPAQUE +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +SUMMARY:Event 1 +DTSTART;TZID=America/New_York:20161004T090000 +DTSTAMP:20161002T105552Z +SEQUENCE:0 +END:VEVENT`.split("\n").join("\r\n")); + expect(ICAL.Component.prototype.isPrototypeOf(fcEvents[0][1])).toBe(true); + expect(fcEvents[0][2].toString()).toEqual('2016-10-04T09:00:00'); + expect(ICAL.Time.prototype.isPrototypeOf(fcEvents[0][2])).toBe(true); + expect(fcEvents[0][3].toString()).toEqual('2016-10-04T11:30:00'); + expect(ICAL.Time.prototype.isPrototypeOf(fcEvents[0][3])).toBe(true); + }); + + it ('should generate FcEvents for a dedicated time-range - single event with DURATION', function() { + const calendar = {this_is_a_fancy_calendar: true}; + const comp = new ICAL.Component(ICAL.parse(ics4)); + + const vevent = VEvent(calendar, comp); + const start = moment('2016-09-25'); + const end = moment('2016-11-06'); + + const fcEvents = vevent.getFcEvent(start, end, timezone_nyc); + expect(fcEvents.length).toEqual(1); + expect(fcEvents[0].length).toEqual(4); + expect(fcEvents[0][0]).toEqual(vevent); + expect(fcEvents[0][1].toString()).toEqual(`BEGIN:VEVENT +CREATED:20161002T105555Z +UID:C8E094B8-A7E6-4CF3-9E59-58608B9B61C5 +TRANSP:OPAQUE +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +SUMMARY:Event 2 +DTSTART;TZID=America/New_York:20160925T000000 +DURATION:P15DT5H0M20S +DTSTAMP:20161002T105633Z +SEQUENCE:0 +END:VEVENT`.split("\n").join("\r\n")); + expect(ICAL.Component.prototype.isPrototypeOf(fcEvents[0][1])).toBe(true); + expect(fcEvents[0][2].toString()).toEqual('2016-09-25T00:00:00'); + expect(ICAL.Time.prototype.isPrototypeOf(fcEvents[0][2])).toBe(true); + expect(fcEvents[0][3].toString()).toEqual('2016-10-10T05:00:20'); + expect(ICAL.Time.prototype.isPrototypeOf(fcEvents[0][3])).toBe(true); + }); + + it ('should generate FcEvents for a dedicated time-range - single event with neither DTEND nor DURATION', function() { + const calendar = {this_is_a_fancy_calendar: true}; + const comp = new ICAL.Component(ICAL.parse(ics5)); + + const vevent = VEvent(calendar, comp); + const start = moment('2016-09-25'); + const end = moment('2016-11-06'); + + const fcEvents = vevent.getFcEvent(start, end, timezone_nyc); + expect(fcEvents.length).toEqual(1); + expect(fcEvents[0].length).toEqual(4); + expect(fcEvents[0][0]).toEqual(vevent); + expect(fcEvents[0][1].toString()).toEqual(`BEGIN:VEVENT +CREATED:20161002T105635Z +UID:9D0C33D1-334E-4B46-9E0E-D62C11E60700 +TRANSP:OPAQUE +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +SUMMARY:Event 3 +DTSTART;TZID=America/New_York:20161105T235900 +DTSTAMP:20161002T105648Z +SEQUENCE:0 +END:VEVENT`.split("\n").join("\r\n")); + expect(ICAL.Component.prototype.isPrototypeOf(fcEvents[0][1])).toBe(true); + expect(fcEvents[0][2].toString()).toEqual('2016-11-05T23:59:00'); + expect(ICAL.Time.prototype.isPrototypeOf(fcEvents[0][2])).toBe(true); + expect(fcEvents[0][3].toString()).toEqual('2016-11-05T23:59:00'); + expect(ICAL.Time.prototype.isPrototypeOf(fcEvents[0][3])).toBe(true); + }); + + it ('should generate FcEvents for a dedicated time-range - single event /w timezone conversion', function() { + const calendar = {this_is_a_fancy_calendar: true}; + const comp = new ICAL.Component(ICAL.parse(ics3)); + console.log(comp.getFirstSubcomponent('vevent').getFirstPropertyValue('dtstart').toString()); + + const vevent = VEvent(calendar, comp); + const start = moment('2016-09-25'); + const end = moment('2016-11-06'); + + const fcEvents = vevent.getFcEvent(start, end, timezone_berlin); + expect(fcEvents.length).toEqual(1); + expect(fcEvents[0].length).toEqual(4); + expect(fcEvents[0][0]).toEqual(vevent); + expect(fcEvents[0][1].toString()).toEqual(`BEGIN:VEVENT +CREATED:20161002T105542Z +UID:DF8A5F8D-9037-4FA3-84CC-97FB6D5D0DA9 +DTEND;TZID=America/New_York:20161004T113000 +TRANSP:OPAQUE +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +SUMMARY:Event 1 +DTSTART;TZID=America/New_York:20161004T090000 +DTSTAMP:20161002T105552Z +SEQUENCE:0 +END:VEVENT`.split("\n").join("\r\n")); + expect(ICAL.Component.prototype.isPrototypeOf(fcEvents[0][1])).toBe(true); + + console.log(fcEvents[0][2]); + console.log(fcEvents[0][2].icaltype); + console.log(fcEvents[0][2].zone === ICAL.Timezone.utcTimezone); + console.log(fcEvents[0][2].zone === ICAL.Timezone.localTimezone); + console.log(fcEvents[0][2].zone); + + expect(fcEvents[0][2].toString()).toEqual('2016-10-04T09:00:00'); + expect(ICAL.Time.prototype.isPrototypeOf(fcEvents[0][2])).toBe(true); + expect(fcEvents[0][3].toString()).toEqual('2016-10-04T11:30:00'); + expect(ICAL.Time.prototype.isPrototypeOf(fcEvents[0][3])).toBe(true); + }); + + it ('should generate FcEvents for a dedicated time-range - skip an event when it doesn\'t contain a DTSTART', function() { + + }); + + it ('should generate FcEvents for a dedicated time-range - recurring events', function() { + + }); + + it ('should generate FcEvents for a dedicated time-range - recurring events ending before requested time-frame', function() { + + }); + + it ('should generate FcEvents for a dedicated time-range - recurring events starting after requested time-frame', function() { + + }); + + it ('should generate FcEvents for a dedicated time-range - recurring events with recurrence Exceptions', function() { + + }); + + it ('should get a simple event without recurrenceId', function() { + + }); + + it ('should get a simple event with recurrenceId', function() { + + }); + + it ('should throw an error when event for simple event was not found', function() { + + }); + + it ('should provide a touch method', function() { + + }); + + it ('should check if an object is an VEvent', function() { + + }); + + it ('should provide a constructor from raw ics1 data', function() { + + }); + + it ('should provide a constructor from a start and end', function() { + + }); +}); diff --git a/tests/js/unit/services/veventServiceSpec.js b/tests/js/unit/services/veventServiceSpec.js index e048f74572..f7f5f932b7 100644 --- a/tests/js/unit/services/veventServiceSpec.js +++ b/tests/js/unit/services/veventServiceSpec.js @@ -17,10 +17,14 @@ describe('VEventService', function () { XMLUtility.getRootSkeleton = jasmine.createSpy(); XMLUtility.serialize = jasmine.createSpy(); - VEvent = jasmine.createSpy().and.callFake(function() { - this.uri = arguments[3]; + VEvent = {}; + VEvent.fromRawICS = jasmine.createSpy().and.callFake(function() { + return { + uri: arguments[2] + }; }); + OC.requestToken = 'requestToken42'; $provide.value('DavClient', DavClient); @@ -127,9 +131,9 @@ describe('VEventService', function () { expect(result.length).toEqual(2); expect(result[0].uri).toEqual('Nextcloud-m9qnwt85rkqpi5f4x8j2lnmil5llj7p1fbj3fsrmvtg74x6r.ics'); expect(result[1].uri).toEqual('Nextcloud-g123jhg13hgghasdgjhasjdghjgsdjasgd123gjjahsgdash.ics'); - expect(VEvent.calls.count()).toEqual(2); - expect(VEvent.calls.argsFor(0)).toEqual([{url: 'calendar-url-123'}, 'fancy-ical-data-1', '"223c4ded836176fff47a23b820f63930"', 'Nextcloud-m9qnwt85rkqpi5f4x8j2lnmil5llj7p1fbj3fsrmvtg74x6r.ics']); - expect(VEvent.calls.argsFor(1)).toEqual([{url: 'calendar-url-123'}, 'fancy-ical-data-2', '"8769876sdfbsdf876asdasdas78d6987"', 'Nextcloud-g123jhg13hgghasdgjhasjdghjgsdjasgd123gjjahsgdash.ics']); + expect(VEvent.fromRawICS.calls.count()).toEqual(2); + expect(VEvent.fromRawICS.calls.argsFor(0)).toEqual([{url: 'calendar-url-123'}, 'fancy-ical-data-1', 'Nextcloud-m9qnwt85rkqpi5f4x8j2lnmil5llj7p1fbj3fsrmvtg74x6r.ics', '"223c4ded836176fff47a23b820f63930"']); + expect(VEvent.fromRawICS.calls.argsFor(1)).toEqual([{url: 'calendar-url-123'}, 'fancy-ical-data-2', 'Nextcloud-g123jhg13hgghasdgjhasjdghjgsdjasgd123gjjahsgdash.ics', '"8769876sdfbsdf876asdasdas78d6987"']); called = true; }); getAllRequest.catch(function() { @@ -246,8 +250,8 @@ describe('VEventService', function () { let called = false; getRequest.then(function(result) { expect(result.uri).toEqual('Nextcloud-m9qnwt85rkqpi5f4x8j2lnmil5llj7p1fbj3fsrmvtg74x6r.ics'); - expect(VEvent.calls.count()).toEqual(1); - expect(VEvent.calls.argsFor(0)).toEqual([{url: 'calendar-url-123/'}, 'fancy-ical-data', '"223c4ded836176fff47a23b820f63930"', 'Nextcloud-m9qnwt85rkqpi5f4x8j2lnmil5llj7p1fbj3fsrmvtg74x6r.ics']); + expect(VEvent.fromRawICS.calls.count()).toEqual(1); + expect(VEvent.fromRawICS.calls.argsFor(0)).toEqual([{url: 'calendar-url-123/'}, 'fancy-ical-data', 'Nextcloud-m9qnwt85rkqpi5f4x8j2lnmil5llj7p1fbj3fsrmvtg74x6r.ics', '"223c4ded836176fff47a23b820f63930"']); called = true; }); @@ -381,8 +385,8 @@ describe('VEventService', function () { let called = false; createRequest.then(function(result) { expect(result.uri).toEqual('awesome-uid'); - expect(VEvent.calls.count()).toEqual(1); - expect(VEvent.calls.argsFor(0)).toEqual([{url: 'calendar-url-123/'}, 'fancy-ical-data', '"223c4ded836176fff47a23b820f63930"', 'awesome-uid']); + expect(VEvent.fromRawICS.calls.count()).toEqual(1); + expect(VEvent.fromRawICS.calls.argsFor(0)).toEqual([{url: 'calendar-url-123/'}, 'fancy-ical-data', 'awesome-uid', '"223c4ded836176fff47a23b820f63930"']); called = true; });