implement ical splitter

This commit is contained in:
Georg Ehrke 2016-01-06 20:22:27 +01:00
parent 222360ab4a
commit 2bbd80a7bf
19 changed files with 887 additions and 165 deletions

View File

@ -50,8 +50,10 @@ class Application extends App {
});
$container->registerService('ViewController', function(IAppContainer $c) {
$request = $c->query('Request');
$userSession = $c->getServer()->getUserSession();
$config = $c->getServer()->getConfig();
return new Controller\ViewController($c->getAppName(), $request);
return new Controller\ViewController($c->getAppName(), $request, $userSession, $config);
});
}

View File

@ -29,17 +29,51 @@ use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\IRequest;
use OCP\IUserSession;
class ViewController extends Controller {
/**
* @var IConfig
*/
private $config;
/**
* @var IUserSession
*/
private $userSession;
/**
* @param string $appName
* @param IRequest $request an instance of the request
* @param IConfig $config
* @param IUserSession $userSession
*/
public function __construct($appName, IRequest $request,
IUserSession $userSession, IConfig $config) {
parent::__construct($appName, $request);
$this->config = $config;
$this->userSession = $userSession;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @return TemplateResponse
*/
public function index(){
return new TemplateResponse('calendar', 'main');
public function index() {
$userId = $this->userSession->getUser()->getUID();
$appVersion = $this->config->getAppValue($this->appName, 'installed_version');
$defaultView = $this->config->getUserValue($userId, $this->appName, 'currentView', 'month');
return new TemplateResponse('calendar', 'main', [
'appVersion' => $appVersion,
'defaultView' => $defaultView,
]);
}
/**

View File

@ -77,12 +77,7 @@
-ms-transition: 0.05s ease-out 0.05s;
transition: 0.05s ease-out 0.05s;
}
.dialog .table .name {
padding-left: 20px;
}
.dialog .table .dialog-select {
width: 100%;
}
.dialog .table:hover {
background: #ddd;
-webkit-transition: 0.05s ease-out 0.05s;
@ -102,3 +97,44 @@
#importdialog .loading.disabled {
background-color: #6c8fc0;
}
#importdialog table,
#importdialog tbody tr,
#importdialog tbody tr:hover,
#importdialog tbody tr:focus {
background: transparent !important;
}
.progress-bar {
margin: 0;
float: left;
width: 0;
height: 100%;
font-size: 12px;
line-height: 20px;
background-color: #5cb85c;
}
#importdialog table {
width: 100%;
}
#importdialog td.name {
width: 40%;
}
#importdialog td.calendartype {
width: 50%;
}
#importdialog td.calendartype select {
width: 90%;
}
#importdialog td.buttongroup {
width: 10%;
}
#importdialog tr {
height: 38px;
}

View File

@ -147,6 +147,7 @@ app.controller('CalController', ['$scope', '$rootScope', '$window', 'CalendarSer
header: false,
firstDay: moment().startOf('week').format('d'),
select: $scope.newEvent,
eventLimit: true,
eventClick: function(fcEvent, jsEvent, view) {
var simpleData = fcEvent.event.getSimpleData(fcEvent);
$rootScope.$broadcast('initializeEventEditor', {
@ -253,6 +254,13 @@ app.controller('CalController', ['$scope', '$rootScope', '$window', 'CalendarSer
delete $scope.eventSource[deletedObject];
});
$rootScope.$on('refetchEvents', function (event, calendar) {
if (switcher.indexOf(calendar.url) !== -1) {
uiCalendarConfig.calendars.calendar.fullCalendar('removeEventSource', $scope.eventSource[calendar.url]);
uiCalendarConfig.calendars.calendar.fullCalendar('addEventSource', $scope.eventSource[calendar.url]);
}
});
/**
* After a calendar's visibility was changed:
* - add event source to fullcalendar if enabled is true
@ -263,6 +271,7 @@ app.controller('CalController', ['$scope', '$rootScope', '$window', 'CalendarSer
uiCalendarConfig.calendars.calendar.fullCalendar('addEventSource', $scope.eventSource[calendar.url]);
} else {
uiCalendarConfig.calendars.calendar.fullCalendar('removeEventSource', $scope.eventSource[calendar.url]);
calendar.list.loading = false;
}
});

View File

@ -36,7 +36,8 @@ app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'Ca
$scope.create = function (name, color) {
CalendarService.create(name, color).then(function(calendar) {
$scope.calendars.push(calendar);
$scope.$apply();
$rootScope.$broadcast('createdCalendar', calendar);
$rootScope.$broadcast('reloadCalendarList');
});
$scope.newCalendarInputVal = '';
@ -64,8 +65,11 @@ app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'Ca
$scope.performUpdate = function (calendar) {
CalendarService.update(calendar).then(function() {
calendar.dropPreviousState();
calendar.list.edit = false;
$scope.$apply();
console.log(calendar);
$rootScope.$broadcast('updatedCalendar', calendar);
$rootScope.$broadcast('reloadCalendarList');
});
};
@ -75,6 +79,7 @@ app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'Ca
CalendarService.update(calendar).then(function() {
$rootScope.$broadcast('updatedCalendarsVisibility', calendar);
$rootScope.$broadcast('reloadCalendarList');
});
};
@ -84,22 +89,15 @@ app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'Ca
$scope.calendars = $scope.calendars.filter(function (element) {
return element.url !== calendar.url;
});
$scope.$apply();
$rootScope.$broadcast('removedCalendar', calendar);
$rootScope.$broadcast('reloadCalendarList');
});
};
//We need to reload the refresh the calendar-list,
//if the user added a subscription
$rootScope.$on('createdSubscription', function() {
// TO BE REIMPLEMENTED, BUT IN A DIFFERENT PR
});
$rootScope.$on('finishedLoadingEvents', function(event, calendarId) {
//var calendar = CalendarModel.get(calendarId);
//calendar.list.loading = false;
//CalendarModel.update(calendar);
//$scope.calendars = CalendarModel.getAll();
$rootScope.$on('reloadCalendarList', function() {
if(!$scope.$$phase) {
$scope.$apply();
}
});
}
]);

View File

@ -26,8 +26,8 @@
* Description: Takes care of the Calendar Settings.
*/
app.controller('SettingsController', ['$scope', '$rootScope', 'CalendarService', 'VEventService', 'DialogModel',
function ($scope, $rootScope, CalendarService, VEventService, DialogModel) {
app.controller('SettingsController', ['$scope', '$rootScope', '$filter', 'CalendarService', 'VEventService', 'DialogModel', 'SplitterService',
function ($scope, $rootScope, $filter, CalendarService, VEventService, DialogModel, SplitterService) {
'use strict';
$scope.settingsCalDavLink = OC.linkToRemote('caldav') + '/';
@ -35,44 +35,130 @@ app.controller('SettingsController', ['$scope', '$rootScope', 'CalendarService',
// have to use the native HTML call for filereader to work efficiently
var reader = new FileReader();
$('#import').on('change', function () {
$scope.calendarAdded(this);
$scope.analyzeFiles(this.files);
});
$scope.calendarAdded = function (elem) {
$scope.files = elem.files;
$scope.analyzeFiles = function (files) {
$scope.files = files;
console.log($scope.calendars);
angular.forEach($scope.files, function(file) {
var reader = new FileReader();
reader.onload = function(event) {
var splitter = SplitterService.split(event.target.result);
angular.extend(reader.linkedFile, {
split: splitter.split,
newCalendarColor: splitter.color,
newCalendarName: splitter.name,
//state: analyzed
state: 1
});
$scope.preselectCalendar(reader.linkedFile);
$scope.$apply();
};
angular.extend(file, {
//state: analyzing
state: 0,
errors: 0,
progress: 0,
progressToReach: 0
});
reader.linkedFile = file;
reader.readAsText(file);
});
$scope.$apply();
DialogModel.initsmall('#importdialog');
DialogModel.open('#importdialog');
};
$scope.import = function (file) {
var reader = new FileReader();
file.isImporting = true;
file.progressToReach = file.split.vevent.length +
file.split.vjournal.length +
file.split.vtodo.length;
//state: import scheduled
file.state = 2;
reader.onload = function() {
/*Restangular.one('calendars', file.importToCalendar).withHttpConfig({transformRequest: angular.identity}).customPOST(
reader.result,
'import',
undefined,
{
'Content-Type': 'text/calendar'
}
).then( function () {
file.done = true;
}, function (response) {
OC.Notification.show(t('calendar', response.data.message));
});*/
var importCalendar = function(calendar) {
var componentNames = ['vevent', 'vjournal', 'vtodo'];
angular.forEach(componentNames, function (componentName) {
angular.forEach(file.split[componentName], function(object) {
VEventService.create(calendar, object, false).then(function(response) {
//state: importing
file.state = 3;
file.progress++;
$scope.$apply();
if (!response) {
file.errors++;
}
calendar.list.loading = true;
if (file.progress === file.progressToReach) {
//state: done
file.state = 4;
$scope.$apply();
$rootScope.$broadcast('refetchEvents', calendar);
}
});
});
});
};
reader.readAsText(file);
if (file.calendar === 'new') {
var name = file.newCalendarName || file.name;
var color = file.newCalendarColor || '#1d2d44';
var components = [];
if (file.split.vevent.length > 0) {
components.push('vevent');
}
if (file.split.vjournal.length > 0) {
components.push('vjournal');
}
if (file.split.vtodo.length > 0) {
components.push('vtodo');
}
CalendarService.create(name, color, components).then(function(calendar) {
if (calendar.components.vevent) {
$scope.calendars.push(calendar);
$rootScope.$broadcast('createdCalendar', calendar);
$rootScope.$broadcast('reloadCalendarList');
}
importCalendar(calendar);
});
} else {
var calendar = $scope.calendars.filter(function (element) {
return element.url === file.calendar;
})[0];
importCalendar(calendar);
}
};
//to send a patch to add a hidden event again
$scope.enableCalendar = function (id) {
//Restangular.one('calendars', id).patch({ 'components' : {'vevent' : true }});
$scope.preselectCalendar = function(file) {
var possibleCalendars = $filter('importCalendarFilter')($scope.calendars, file);
if (possibleCalendars.length === 0) {
file.calendar = 'new';
} else {
file.calendar = possibleCalendars[0];
}
};
$scope.changeCalendar = function(file) {
if (file.calendar === 'new') {
file.incompatibleObjectsWarning = false;
} else {
var possibleCalendars = $filter('importCalendarFilter')($scope.calendars, file);
file.incompatibleObjectsWarning = (possibleCalendars.indexOf(file.calendar) === -1);
}
};
}
]);

View File

@ -38,9 +38,9 @@ app.filter('datepickerFilter',
case 'month':
return moment(item).week() === 1 ?
moment(item).add(1, 'week').format('MMMM GGGG'):
moment(item).add(1, 'week').format('MMMM GGGG') :
moment(item).format('MMMM GGGG');
}
}
};
}
);

View File

@ -0,0 +1,51 @@
/**
* ownCloud - Calendar App
*
* @author Raghu Nayyar
* @author Georg Ehrke
* @copyright 2016 Raghu Nayyar <beingminimal@gmail.com>
* @copyright 2016 Georg Ehrke <oc.list@georgehrke.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
app.filter('importCalendarFilter',
function () {
'use strict';
return function (calendars, file) {
var possibleCalendars = [];
if (typeof file.split === 'undefined') {
return possibleCalendars;
}
angular.forEach(calendars, function(calendar) {
if (file.split.vevent.length !== 0 && !calendar.components.vevent) {
return;
}
if (file.split.vjournal.length !== 0 && !calendar.components.vjournal) {
return;
}
if (file.split.vtodo.length !== 0 && !calendar.components.vtodo) {
return;
}
possibleCalendars.push(calendar);
});
return possibleCalendars;
};
}
);

View File

@ -0,0 +1,41 @@
/**
* ownCloud - Calendar App
*
* @author Raghu Nayyar
* @author Georg Ehrke
* @copyright 2016 Raghu Nayyar <beingminimal@gmail.com>
* @copyright 2016 Georg Ehrke <oc.list@georgehrke.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
app.filter('importErrorFilter',
function () {
'use strict';
return function (file) {
if (file.errors === 0) {
return t('calendar', 'Successfully imported');
} else {
if (file.errors === 1) {
return t('calendar', 'Partially imported, 1 failure');
} else {
return t('calendar', 'Partially imported, {n} failures', {
n: file.errors
});
}
}
};
}
);

View File

@ -1,4 +1,4 @@
app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function($filter, VEventService, TimezoneService) {
app.factory('Calendar', ['$rootScope', '$filter', 'VEventService', 'TimezoneService', function($rootScope, $filter, VEventService, TimezoneService) {
'use strict';
function Calendar(url, props) {
@ -12,17 +12,17 @@ app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function
displayname: props['{DAV:}displayname'] || 'Unnamed',
color: props['{http://apple.com/ns/ical/}calendar-color'] || '#1d2d44',
order: parseInt(props['{http://apple.com/ns/ical/}calendar-order']) || 0,
components: {
vevent: false,
vjournal: false,
vtodo: false
},
cruds: {
create: props.canWrite,
read: true,
update: props.canWrite,
delete: props.canWrite,
share: props.canWrite
},
list: {
edit: false,
loading: true,
locked: false
}
},
_updatedProperties: []
@ -31,8 +31,10 @@ app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function
angular.extend(this, {
fcEventSource: {
events: function (start, end, timezone, callback) {
console.log('querying events ...');
TimezoneService.get(timezone).then(function(tz) {
_this._properties.list.loading = true;
_this.list.loading = true;
$rootScope.$broadcast('reloadCalendarList');
VEventService.getAll(_this, start, end).then(function(events) {
var vevents = [];
@ -42,15 +44,29 @@ app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function
callback(vevents);
_this._properties.list.loading = false;
_this.list.loading = false;
$rootScope.$broadcast('reloadCalendarList');
});
});
},
color: this._properties.color,
editable: this._properties.cruds.update,
calendar: this
},
list: {
edit: false,
loading: this.enabled,
locked: false
}
});
var components = props['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'];
for (var i=0; i < components.length; i++) {
var name = components[i].attributes.getNamedItem('name').textContent.toLowerCase();
if (this._properties.components.hasOwnProperty(name)) {
this._properties.components[name] = true;
}
}
}
Calendar.prototype = {
@ -60,6 +76,9 @@ app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function
get enabled() {
return this._properties.enabled;
},
get components() {
return this._properties.components;
},
set enabled(enabled) {
this._properties.enabled = enabled;
this._setUpdated('enabled');
@ -88,12 +107,6 @@ app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function
get cruds() {
return this._properties.cruds;
},
get list() {
return this._properties.list;
},
set list(list) {
this._properties.list = list;
},
_setUpdated: function(propName) {
if (this._updatedProperties.indexOf(propName) === -1) {
this._updatedProperties.push(propName);
@ -106,12 +119,15 @@ app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function
this._updatedProperties = [];
},
prepareUpdate: function() {
this._properties.list.edit = true;
this.list.edit = true;
this._propertiesBackup = angular.copy(this._properties);
},
resetToPreviousState: function() {
this._properties = angular.copy(this._propertiesBackup);
this._properties.list.edit = false;
this.list.edit = false;
this._propertiesBackup = {};
},
dropPreviousState: function() {
this._propertiesBackup = {};
}
};

View File

@ -154,13 +154,17 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal
});
};
this.create = function(name, color) {
this.create = function(name, color, components) {
if (this._CALENDAR_HOME === null) {
return discoverHome(function() {
return _this.create(name, color);
});
}
if (typeof components === 'undefined') {
components = ['vevent'];
}
var xmlDoc = document.implementation.createDocument('', '', null);
var cMkcalendar = xmlDoc.createElement('c:mkcalendar');
cMkcalendar.setAttribute('xmlns:c', 'urn:ietf:params:xml:ns:caldav');
@ -178,7 +182,7 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal
dProp.appendChild(this._createXMLForProperty(xmlDoc, 'displayname', name));
dProp.appendChild(this._createXMLForProperty(xmlDoc, 'enabled', true));
dProp.appendChild(this._createXMLForProperty(xmlDoc, 'color', color));
dProp.appendChild(this._createXMLForProperty(xmlDoc, 'components', {vevent: true}));
dProp.appendChild(this._createXMLForProperty(xmlDoc, 'components', components));
var body = cMkcalendar.outerHTML;
@ -191,7 +195,10 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal
return DavClient.request('MKCALENDAR', url, headers, body).then(function(response) {
if (response.status === 201) {
_this._takenUrls.push(url);
return _this.get(url);
return _this.get(url).then(function(calendar) {
calendar.enabled = true;
return _this.update(calendar);
});
}
});
};
@ -230,6 +237,7 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal
return DavClient.request('PROPPATCH', url, headers, body).then(function(response) {
var responseBody = DavClient.parseMultiStatus(response.body);
console.log(responseBody);
return calendar;
});
};
@ -268,12 +276,10 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal
case 'components':
var cComponents = xmlDoc.createElement('c:supported-calendar-component-set');
for (var component in value) {
if (value.hasOwnProperty(component) && value[component]) {
var cComp = xmlDoc.createElement('c:comp');
cComp.setAttribute('name', component.toUpperCase());
cComponents.appendChild(cComp);
}
for (var i=0; i < value.length; i++) {
var cComp = xmlDoc.createElement('c:comp');
cComp.setAttribute('name', value[i].toUpperCase());
cComponents.appendChild(cComp);
}
return cComponents;
}

View File

@ -0,0 +1,40 @@
/**
* ownCloud - Calendar App
*
* @author Raghu Nayyar
* @author Georg Ehrke
* @copyright 2016 Raghu Nayyar <beingminimal@gmail.com>
* @copyright 2016 Georg Ehrke <oc.list@georgehrke.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
app.service('ICalFactory', [
function() {
'use strict';
// creates a new ICAL root element with a product id property
return {
new: function() {
var root = new ICAL.Component(['vcalendar', [], []]);
var version = angular.element('#fullcalendar').attr('data-appVersion');
root.updatePropertyWithValue('prodid', '-//ownCloud calendar v' + version);
return root;
}
};
}
]);

View File

@ -0,0 +1,77 @@
/**
* ownCloud - Calendar App
*
* @author Raghu Nayyar
* @author Georg Ehrke
* @copyright 2016 Raghu Nayyar <beingminimal@gmail.com>
* @copyright 2016 Georg Ehrke <oc.list@georgehrke.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
app.service('SplitterService', ['ICalFactory',
function(ICalFactory) {
'use strict';
// provides function to split big ics blobs into an array of little ics blobs
return {
split: function(iCalString) {
var timezones = [];
var allObjects = {};
var jcal = ICAL.parse(iCalString);
var components = new ICAL.Component(jcal);
var vtimezones = components.getAllSubcomponents('vtimezone');
angular.forEach(vtimezones, function (vtimezone) {
timezones.push(vtimezone);
});
var componentNames = ['vevent', 'vjournal', 'vtodo'];
angular.forEach(componentNames, function (componentName) {
var vobjects = components.getAllSubcomponents(componentName);
allObjects[componentName] = {};
angular.forEach(vobjects, function (vobject) {
var uid = vobject.getFirstPropertyValue('uid');
allObjects[componentName][uid] = allObjects[componentName][uid] || [];
allObjects[componentName][uid].push(vobject);
});
});
var split = [];
angular.forEach(componentNames, function (componentName) {
split[componentName] = [];
angular.forEach(allObjects[componentName], function (objects) {
var component = ICalFactory.new();
angular.forEach(timezones, function (timezone) {
component.addSubcomponent(timezone);
});
angular.forEach(objects, function (object) {
component.addSubcomponent(object);
});
split[componentName].push(component.toString());
});
});
return {
name: components.getFirstPropertyValue('x-wr-calname'),
color: components.getFirstPropertyValue('x-apple-calendar-color'),
split: split
};
}
};
}
]);

View File

@ -100,7 +100,11 @@ app.service('VEventService', ['DavClient', 'VEvent', function(DavClient, VEvent)
});
};
this.create = function(calendar, data) {
this.create = function(calendar, data, returnEvent) {
if (typeof returnEvent === 'undefined') {
returnEvent = true;
}
var headers = {
'Content-Type': 'text/calendar; charset=utf-8'
};
@ -109,10 +113,14 @@ app.service('VEventService', ['DavClient', 'VEvent', function(DavClient, VEvent)
return DavClient.request('PUT', url, headers, data).then(function(response) {
if (!DavClient.wasRequestSuccessful(response.status)) {
console.log(response);
return false;
// TODO - something went wrong, do smth about it
}
return _this.get(calendar, uri);
return returnEvent ?
_this.get(calendar, uri) :
true;
});
};

View File

@ -167,6 +167,7 @@ app.controller('CalController', ['$scope', '$rootScope', '$window', 'CalendarSer
header: false,
firstDay: moment().startOf('week').format('d'),
select: $scope.newEvent,
eventLimit: true,
eventClick: function(fcEvent, jsEvent, view) {
var simpleData = fcEvent.event.getSimpleData(fcEvent);
$rootScope.$broadcast('initializeEventEditor', {
@ -273,6 +274,13 @@ app.controller('CalController', ['$scope', '$rootScope', '$window', 'CalendarSer
delete $scope.eventSource[deletedObject];
});
$rootScope.$on('refetchEvents', function (event, calendar) {
if (switcher.indexOf(calendar.url) !== -1) {
uiCalendarConfig.calendars.calendar.fullCalendar('removeEventSource', $scope.eventSource[calendar.url]);
uiCalendarConfig.calendars.calendar.fullCalendar('addEventSource', $scope.eventSource[calendar.url]);
}
});
/**
* After a calendar's visibility was changed:
* - add event source to fullcalendar if enabled is true
@ -283,6 +291,7 @@ app.controller('CalController', ['$scope', '$rootScope', '$window', 'CalendarSer
uiCalendarConfig.calendars.calendar.fullCalendar('addEventSource', $scope.eventSource[calendar.url]);
} else {
uiCalendarConfig.calendars.calendar.fullCalendar('removeEventSource', $scope.eventSource[calendar.url]);
calendar.list.loading = false;
}
});
@ -304,7 +313,8 @@ app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'Ca
$scope.create = function (name, color) {
CalendarService.create(name, color).then(function(calendar) {
$scope.calendars.push(calendar);
$scope.$apply();
$rootScope.$broadcast('createdCalendar', calendar);
$rootScope.$broadcast('reloadCalendarList');
});
$scope.newCalendarInputVal = '';
@ -332,8 +342,11 @@ app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'Ca
$scope.performUpdate = function (calendar) {
CalendarService.update(calendar).then(function() {
calendar.dropPreviousState();
calendar.list.edit = false;
$scope.$apply();
console.log(calendar);
$rootScope.$broadcast('updatedCalendar', calendar);
$rootScope.$broadcast('reloadCalendarList');
});
};
@ -343,6 +356,7 @@ app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'Ca
CalendarService.update(calendar).then(function() {
$rootScope.$broadcast('updatedCalendarsVisibility', calendar);
$rootScope.$broadcast('reloadCalendarList');
});
};
@ -352,22 +366,15 @@ app.controller('CalendarListController', ['$scope', '$rootScope', '$window', 'Ca
$scope.calendars = $scope.calendars.filter(function (element) {
return element.url !== calendar.url;
});
$scope.$apply();
$rootScope.$broadcast('removedCalendar', calendar);
$rootScope.$broadcast('reloadCalendarList');
});
};
//We need to reload the refresh the calendar-list,
//if the user added a subscription
$rootScope.$on('createdSubscription', function() {
// TO BE REIMPLEMENTED, BUT IN A DIFFERENT PR
});
$rootScope.$on('finishedLoadingEvents', function(event, calendarId) {
//var calendar = CalendarModel.get(calendarId);
//calendar.list.loading = false;
//CalendarModel.update(calendar);
//$scope.calendars = CalendarModel.getAll();
$rootScope.$on('reloadCalendarList', function() {
if(!$scope.$$phase) {
$scope.$apply();
}
});
}
]);
@ -885,8 +892,8 @@ app.controller('EventsModalController', ['$scope', '$rootScope', '$routeParams',
* Description: Takes care of the Calendar Settings.
*/
app.controller('SettingsController', ['$scope', '$rootScope', 'CalendarService', 'VEventService', 'DialogModel',
function ($scope, $rootScope, CalendarService, VEventService, DialogModel) {
app.controller('SettingsController', ['$scope', '$rootScope', '$filter', 'CalendarService', 'VEventService', 'DialogModel', 'SplitterService',
function ($scope, $rootScope, $filter, CalendarService, VEventService, DialogModel, SplitterService) {
'use strict';
$scope.settingsCalDavLink = OC.linkToRemote('caldav') + '/';
@ -894,44 +901,130 @@ app.controller('SettingsController', ['$scope', '$rootScope', 'CalendarService',
// have to use the native HTML call for filereader to work efficiently
var reader = new FileReader();
$('#import').on('change', function () {
$scope.calendarAdded(this);
$scope.analyzeFiles(this.files);
});
$scope.calendarAdded = function (elem) {
$scope.files = elem.files;
$scope.analyzeFiles = function (files) {
$scope.files = files;
console.log($scope.calendars);
angular.forEach($scope.files, function(file) {
var reader = new FileReader();
reader.onload = function(event) {
var splitter = SplitterService.split(event.target.result);
angular.extend(reader.linkedFile, {
split: splitter.split,
newCalendarColor: splitter.color,
newCalendarName: splitter.name,
//state: analyzed
state: 1
});
$scope.preselectCalendar(reader.linkedFile);
$scope.$apply();
};
angular.extend(file, {
//state: analyzing
state: 0,
errors: 0,
progress: 0,
progressToReach: 0
});
reader.linkedFile = file;
reader.readAsText(file);
});
$scope.$apply();
DialogModel.initsmall('#importdialog');
DialogModel.open('#importdialog');
};
$scope.import = function (file) {
var reader = new FileReader();
file.isImporting = true;
file.progressToReach = file.split.vevent.length +
file.split.vjournal.length +
file.split.vtodo.length;
//state: import scheduled
file.state = 2;
reader.onload = function() {
/*Restangular.one('calendars', file.importToCalendar).withHttpConfig({transformRequest: angular.identity}).customPOST(
reader.result,
'import',
undefined,
{
'Content-Type': 'text/calendar'
}
).then( function () {
file.done = true;
}, function (response) {
OC.Notification.show(t('calendar', response.data.message));
});*/
var importCalendar = function(calendar) {
var componentNames = ['vevent', 'vjournal', 'vtodo'];
angular.forEach(componentNames, function (componentName) {
angular.forEach(file.split[componentName], function(object) {
VEventService.create(calendar, object, false).then(function(response) {
//state: importing
file.state = 3;
file.progress++;
$scope.$apply();
if (!response) {
file.errors++;
}
calendar.list.loading = true;
if (file.progress === file.progressToReach) {
//state: done
file.state = 4;
$scope.$apply();
$rootScope.$broadcast('refetchEvents', calendar);
}
});
});
});
};
reader.readAsText(file);
if (file.calendar === 'new') {
var name = file.newCalendarName || file.name;
var color = file.newCalendarColor || '#1d2d44';
var components = [];
if (file.split.vevent.length > 0) {
components.push('vevent');
}
if (file.split.vjournal.length > 0) {
components.push('vjournal');
}
if (file.split.vtodo.length > 0) {
components.push('vtodo');
}
CalendarService.create(name, color, components).then(function(calendar) {
if (calendar.components.vevent) {
$scope.calendars.push(calendar);
$rootScope.$broadcast('createdCalendar', calendar);
$rootScope.$broadcast('reloadCalendarList');
}
importCalendar(calendar);
});
} else {
var calendar = $scope.calendars.filter(function (element) {
return element.url === file.calendar;
})[0];
importCalendar(calendar);
}
};
//to send a patch to add a hidden event again
$scope.enableCalendar = function (id) {
//Restangular.one('calendars', id).patch({ 'components' : {'vevent' : true }});
$scope.preselectCalendar = function(file) {
var possibleCalendars = $filter('importCalendarFilter')($scope.calendars, file);
if (possibleCalendars.length === 0) {
file.calendar = 'new';
} else {
file.calendar = possibleCalendars[0];
}
};
$scope.changeCalendar = function(file) {
if (file.calendar === 'new') {
file.incompatibleObjectsWarning = false;
} else {
var possibleCalendars = $filter('importCalendarFilter')($scope.calendars, file);
file.incompatibleObjectsWarning = (possibleCalendars.indexOf(file.calendar) === -1);
}
};
}
]);
@ -1113,10 +1206,60 @@ app.filter('datepickerFilter',
case 'month':
return moment(item).week() === 1 ?
moment(item).add(1, 'week').format('MMMM GGGG'):
moment(item).add(1, 'week').format('MMMM GGGG') :
moment(item).format('MMMM GGGG');
}
}
};
}
);
app.filter('importCalendarFilter',
function () {
'use strict';
return function (calendars, file) {
var possibleCalendars = [];
if (typeof file.split === 'undefined') {
return possibleCalendars;
}
angular.forEach(calendars, function(calendar) {
if (file.split.vevent.length !== 0 && !calendar.components.vevent) {
return;
}
if (file.split.vjournal.length !== 0 && !calendar.components.vjournal) {
return;
}
if (file.split.vtodo.length !== 0 && !calendar.components.vtodo) {
return;
}
possibleCalendars.push(calendar);
});
return possibleCalendars;
};
}
);
app.filter('importErrorFilter',
function () {
'use strict';
return function (file) {
if (file.errors === 0) {
return t('calendar', 'Successfully imported');
} else {
if (file.errors === 1) {
return t('calendar', 'Partially imported, 1 failure');
} else {
return t('calendar', 'Partially imported, {n} failures', {
n: file.errors
});
}
}
};
}
);
@ -1187,7 +1330,7 @@ app.filter('subscriptionFilter',
}
]);
app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function($filter, VEventService, TimezoneService) {
app.factory('Calendar', ['$rootScope', '$filter', 'VEventService', 'TimezoneService', function($rootScope, $filter, VEventService, TimezoneService) {
'use strict';
function Calendar(url, props) {
@ -1201,17 +1344,17 @@ app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function
displayname: props['{DAV:}displayname'] || 'Unnamed',
color: props['{http://apple.com/ns/ical/}calendar-color'] || '#1d2d44',
order: parseInt(props['{http://apple.com/ns/ical/}calendar-order']) || 0,
components: {
vevent: false,
vjournal: false,
vtodo: false
},
cruds: {
create: props.canWrite,
read: true,
update: props.canWrite,
delete: props.canWrite,
share: props.canWrite
},
list: {
edit: false,
loading: true,
locked: false
}
},
_updatedProperties: []
@ -1220,8 +1363,10 @@ app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function
angular.extend(this, {
fcEventSource: {
events: function (start, end, timezone, callback) {
console.log('querying events ...');
TimezoneService.get(timezone).then(function(tz) {
_this._properties.list.loading = true;
_this.list.loading = true;
$rootScope.$broadcast('reloadCalendarList');
VEventService.getAll(_this, start, end).then(function(events) {
var vevents = [];
@ -1231,15 +1376,29 @@ app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function
callback(vevents);
_this._properties.list.loading = false;
_this.list.loading = false;
$rootScope.$broadcast('reloadCalendarList');
});
});
},
color: this._properties.color,
editable: this._properties.cruds.update,
calendar: this
},
list: {
edit: false,
loading: this.enabled,
locked: false
}
});
var components = props['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'];
for (var i=0; i < components.length; i++) {
var name = components[i].attributes.getNamedItem('name').textContent.toLowerCase();
if (this._properties.components.hasOwnProperty(name)) {
this._properties.components[name] = true;
}
}
}
Calendar.prototype = {
@ -1249,6 +1408,9 @@ app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function
get enabled() {
return this._properties.enabled;
},
get components() {
return this._properties.components;
},
set enabled(enabled) {
this._properties.enabled = enabled;
this._setUpdated('enabled');
@ -1277,12 +1439,6 @@ app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function
get cruds() {
return this._properties.cruds;
},
get list() {
return this._properties.list;
},
set list(list) {
this._properties.list = list;
},
_setUpdated: function(propName) {
if (this._updatedProperties.indexOf(propName) === -1) {
this._updatedProperties.push(propName);
@ -1295,12 +1451,15 @@ app.factory('Calendar', ['$filter', 'VEventService', 'TimezoneService', function
this._updatedProperties = [];
},
prepareUpdate: function() {
this._properties.list.edit = true;
this.list.edit = true;
this._propertiesBackup = angular.copy(this._properties);
},
resetToPreviousState: function() {
this._properties = angular.copy(this._propertiesBackup);
this._properties.list.edit = false;
this.list.edit = false;
this._propertiesBackup = {};
},
dropPreviousState: function() {
this._propertiesBackup = {};
}
};
@ -1521,13 +1680,17 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal
});
};
this.create = function(name, color) {
this.create = function(name, color, components) {
if (this._CALENDAR_HOME === null) {
return discoverHome(function() {
return _this.create(name, color);
});
}
if (typeof components === 'undefined') {
components = ['vevent'];
}
var xmlDoc = document.implementation.createDocument('', '', null);
var cMkcalendar = xmlDoc.createElement('c:mkcalendar');
cMkcalendar.setAttribute('xmlns:c', 'urn:ietf:params:xml:ns:caldav');
@ -1545,7 +1708,7 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal
dProp.appendChild(this._createXMLForProperty(xmlDoc, 'displayname', name));
dProp.appendChild(this._createXMLForProperty(xmlDoc, 'enabled', true));
dProp.appendChild(this._createXMLForProperty(xmlDoc, 'color', color));
dProp.appendChild(this._createXMLForProperty(xmlDoc, 'components', {vevent: true}));
dProp.appendChild(this._createXMLForProperty(xmlDoc, 'components', components));
var body = cMkcalendar.outerHTML;
@ -1558,7 +1721,10 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal
return DavClient.request('MKCALENDAR', url, headers, body).then(function(response) {
if (response.status === 201) {
_this._takenUrls.push(url);
return _this.get(url);
return _this.get(url).then(function(calendar) {
calendar.enabled = true;
return _this.update(calendar);
});
}
});
};
@ -1597,6 +1763,7 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal
return DavClient.request('PROPPATCH', url, headers, body).then(function(response) {
var responseBody = DavClient.parseMultiStatus(response.body);
console.log(responseBody);
return calendar;
});
};
@ -1635,12 +1802,10 @@ app.service('CalendarService', ['DavClient', 'Calendar', function(DavClient, Cal
case 'components':
var cComponents = xmlDoc.createElement('c:supported-calendar-component-set');
for (var component in value) {
if (value.hasOwnProperty(component) && value[component]) {
var cComp = xmlDoc.createElement('c:comp');
cComp.setAttribute('name', component.toUpperCase());
cComponents.appendChild(cComp);
}
for (var i=0; i < value.length; i++) {
var cComp = xmlDoc.createElement('c:comp');
cComp.setAttribute('name', value[i].toUpperCase());
cComponents.appendChild(cComp);
}
return cComponents;
}
@ -2281,6 +2446,23 @@ app.factory('fcHelper', function () {
};
});
app.service('ICalFactory', [
function() {
'use strict';
// creates a new ICAL root element with a product id property
return {
new: function() {
var root = new ICAL.Component(['vcalendar', [], []]);
var version = angular.element('#fullcalendar').attr('data-appVersion');
root.updatePropertyWithValue('prodid', '-//ownCloud calendar v' + version);
return root;
}
};
}
]);
app.factory('is', function () {
'use strict';
@ -2912,6 +3094,60 @@ app.service('SettingsService', ['$rootScope', '$http', function($rootScope, $htt
};
}]);
app.service('SplitterService', ['ICalFactory',
function(ICalFactory) {
'use strict';
// provides function to split big ics blobs into an array of little ics blobs
return {
split: function(iCalString) {
var timezones = [];
var allObjects = {};
var jcal = ICAL.parse(iCalString);
var components = new ICAL.Component(jcal);
var vtimezones = components.getAllSubcomponents('vtimezone');
angular.forEach(vtimezones, function (vtimezone) {
timezones.push(vtimezone);
});
var componentNames = ['vevent', 'vjournal', 'vtodo'];
angular.forEach(componentNames, function (componentName) {
var vobjects = components.getAllSubcomponents(componentName);
allObjects[componentName] = {};
angular.forEach(vobjects, function (vobject) {
var uid = vobject.getFirstPropertyValue('uid');
allObjects[componentName][uid] = allObjects[componentName][uid] || [];
allObjects[componentName][uid].push(vobject);
});
});
var split = [];
angular.forEach(componentNames, function (componentName) {
split[componentName] = [];
angular.forEach(allObjects[componentName], function (objects) {
var component = ICalFactory.new();
angular.forEach(timezones, function (timezone) {
component.addSubcomponent(timezone);
});
angular.forEach(objects, function (object) {
component.addSubcomponent(object);
});
split[componentName].push(component.toString());
});
});
return {
name: components.getFirstPropertyValue('x-wr-calname'),
color: components.getFirstPropertyValue('x-apple-calendar-color'),
split: split
};
}
};
}
]);
app.service('TimezoneService', ['$rootScope', '$http', 'Timezone',
function($rootScope, $http, Timezone) {
'use strict';
@ -3053,7 +3289,11 @@ app.service('VEventService', ['DavClient', 'VEvent', function(DavClient, VEvent)
});
};
this.create = function(calendar, data) {
this.create = function(calendar, data, returnEvent) {
if (typeof returnEvent === 'undefined') {
returnEvent = true;
}
var headers = {
'Content-Type': 'text/calendar; charset=utf-8'
};
@ -3062,10 +3302,14 @@ app.service('VEventService', ['DavClient', 'VEvent', function(DavClient, VEvent)
return DavClient.request('PUT', url, headers, data).then(function(response) {
if (!DavClient.wasRequestSuccessful(response.status)) {
console.log(response);
return false;
// TODO - something went wrong, do smth about it
}
return _this.get(calendar, uri);
return returnEvent ?
_this.get(calendar, uri) :
true;
});
};

View File

@ -22,8 +22,8 @@
*
*/
?>
<span class="calendarCheckbox" ng-show="!calendar.loading && !calendar.list.edit" ng-style="{ background : calendar.enabled == true ? '{{ calendar.color }}' : 'transparent' }"></span>
<span class="loading pull-left" ng-show="calendar.loading && !calendar.list.edit">
<span class="calendarCheckbox" ng-show="!calendar.list.loading && !calendar.list.edit" ng-style="{ background : calendar.enabled == true ? '{{ calendar.color }}' : 'transparent' }"></span>
<span class="loading pull-left" ng-show="calendar.list.loading && !calendar.list.edit">
<i class="fa fa-spinner fa-spin"></i>
</span>
<a href="#/" ng-click="triggerEnable(calendar)" data-id="{{ calendar.id }}" ng-show="!calendar.list.edit">

View File

@ -27,5 +27,6 @@
id="fullcalendar" class="calendar"
calendar="calendar"
ng-model="eventSources"
data-defaultView="<?php p(OCP\Config::getUserValue(OCP\User::getUser(), 'calendar', 'currentView', 'month')); ?>">
data-appVersion="<?php p($_['appVersion']); ?>"
data-defaultView="<?php p($_['defaultView']); ?>">
</div>

View File

@ -62,25 +62,61 @@
<span>{{ file.name }}</span>
</td>
<td class="calendartype">
<select
class="settings-select"
ng-init="file.importToCalendar = (calendars | calendarFilter)[0].id"
ng-model="file.importToCalendar"
ng-options="calendar.id as calendar.displayname for calendar in calendars | calendarFilter | orderBy:['order']"
ng-disabled="file.isImporting">
</select>
<span
ng-show="file.state === 0">
<?php p($l->t('Analyzing calendar')); ?>
</span>
<div
ng-show="file.state === 1">
<span
class="svg icon-error"
ng-show="file.incompatibleObjectsWarning"
title="<?php p($l->t('The file contains objects incompatible with the selected calendar')); ?>">
&nbsp;&nbsp;
</span>
<select
class="settings-select"
ng-change="changeCalendar(file)"
ng-model="file.calendar"
ng-show="file.state === 1">
<option
ng-repeat="calendar in calendars | calendarFilter | orderBy:['order']"
value="{{ calendar.url }}">
{{ calendar.displayname }}
</option>
<option
value="new">
<?php p($l->t('New calendar')); ?>
</option>
</select>
</div>
<span
ng-show="file.state === 2">
<?php p($l->t('Import scheduled')); ?>
</span>
<uib-progressbar
ng-show="file.state === 3"
animate="false"
value="file.progress"
max="file.progressToReach">
&nbsp;
</uib-progressbar>
<div
ng-show="file.state === 4">
<span>
{{ file | importErrorFilter }}
</span>
</div>
</td>
<td class="buttongroup">
<div class="pull-right">
<div class="pull-right" ng-show="file.state === 1">
<button
class="primary btn icon-checkmark-white"
ng-click="import(file, $index)"
ng-disabled="file.isImporting" ng-class="{ loading: file.isImporting, disabled: file.isImporting }">
ng-click="import(file)">
</button>
<button
class="btn icon-close"
ng-click="file.done = true"
ng-disabled="file.isImporting">
ng-click="file.done = true">
</button>
</div>
</td>

View File

@ -56,6 +56,10 @@ class ViewControllerTest extends \PHPUnit_Framework_TestCase {
private $appName;
private $request;
private $config;
private $userSession;
private $dummyUser;
private $controller;
@ -64,14 +68,47 @@ class ViewControllerTest extends \PHPUnit_Framework_TestCase {
$this->request = $this->getMockBuilder('\OCP\IRequest')
->disableOriginalConstructor()
->getMock();
$this->config = $this->getMockBuilder('\OCP\IConfig')
->disableOriginalConstructor()
->getMock();
$this->userSession = $this->getMockBuilder('\OCP\IUserSession')
->disableOriginalConstructor()
->getMock();
$this->controller = new ViewController($this->appName, $this->request);
$this->dummyUser = $this->getMockBuilder('OCP\IUser')
->disableOriginalConstructor()
->getMock();
$this->controller = new ViewController($this->appName, $this->request,
$this->userSession, $this->config);
}
public function testIndex() {
$this->userSession->expects($this->once())
->method('getUser')
->will($this->returnValue($this->dummyUser));
$this->dummyUser->expects($this->once())
->method('getUID')
->will($this->returnValue('user123'));
$this->config->expects($this->once())
->method('getAppValue')
->with($this->appName, 'installed_version')
->will($this->returnValue('42.13.37'));
$this->config->expects($this->once())
->method('getUserValue')
->with('user123', $this->appName, 'currentView', 'month')
->will($this->returnValue('someView'));
$actual = $this->controller->index();
$this->assertInstanceOf('OCP\AppFramework\Http\TemplateResponse', $actual);
$this->assertEquals([
'appVersion' => '42.13.37',
'defaultView' => 'someView',
], $actual->getParams());
$this->assertEquals('main', $actual->getTemplateName());
}