mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-28 09:24:24 +01:00
Merge branch 'ics' into dev
This commit is contained in:
commit
11e74c6859
@ -14,6 +14,7 @@
|
||||
- Updated moment-timezone
|
||||
- Added freeCAD files as default allowed extensions
|
||||
- Fix a bug: unable to remove the picture from a training
|
||||
- Fix a bug: report errors on admin creation
|
||||
- Fix a security issue: updated loofah to fix [CVE-2019-15587](https://github.com/advisories/GHSA-c3gv-9cxf-6f57)
|
||||
- Fix a security issue: updated angular to 1.7.9 to fix [CVE-2019-10768](https://github.com/advisories/GHSA-89mq-4x47-5v83)
|
||||
- [TODO DEPLOY] add the `SLOT_DURATION` environment variable (see [doc/environment.md](doc/environment.md#SLOT_DURATION) for configuration details)
|
||||
|
@ -191,6 +191,9 @@ GEM
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
icalendar (2.5.3)
|
||||
ice_cube (~> 0.16)
|
||||
ice_cube (0.16.3)
|
||||
ice_nine (0.11.2)
|
||||
jaro_winkler (1.5.1)
|
||||
jbuilder (2.5.0)
|
||||
@ -497,6 +500,7 @@ DEPENDENCIES
|
||||
forgery
|
||||
friendly_id (~> 5.1.0)
|
||||
has_secure_token
|
||||
icalendar
|
||||
jbuilder (~> 2.5)
|
||||
jbuilder_cache_multi
|
||||
jquery-rails
|
||||
|
@ -755,3 +755,109 @@ Application.Controllers.controller('DeleteRecurrentAvailabilityController', ['$s
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Controller used in the iCalendar (ICS) imports management page
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('AdminICalendarController', ['$scope', 'iCalendars', 'ICalendar', 'dialogs', 'growl', '_t',
|
||||
function ($scope, iCalendars, ICalendar, dialogs, growl, _t) {
|
||||
// list of ICS sources
|
||||
$scope.calendars = iCalendars;
|
||||
|
||||
// configuration of a new ICS source
|
||||
$scope.newCalendar = {
|
||||
color: undefined,
|
||||
text_color: undefined,
|
||||
url: undefined,
|
||||
name: undefined,
|
||||
text_hidden: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the new iCalendar in database
|
||||
*/
|
||||
$scope.save = function () {
|
||||
ICalendar.save({}, { i_calendar: $scope.newCalendar }, function (data) {
|
||||
// success
|
||||
$scope.calendars.push(data);
|
||||
$scope.newCalendar.url = undefined;
|
||||
$scope.newCalendar.name = undefined;
|
||||
$scope.newCalendar.color = null;
|
||||
$scope.newCalendar.text_color = null;
|
||||
$scope.newCalendar.text_hidden = false;
|
||||
}, function (error) {
|
||||
// failed
|
||||
growl.error(_t('icalendar.create_error'));
|
||||
console.error(error);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a CSS-like style of the given calendar configuration
|
||||
* @param calendar
|
||||
*/
|
||||
$scope.calendarStyle = function (calendar) {
|
||||
return {
|
||||
'border-color': calendar.color,
|
||||
'color': calendar.text_color,
|
||||
'width': calendar.text_hidden ? '50px' : 'auto',
|
||||
'height': calendar.text_hidden ? '21px' : 'auto'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given calendar from the database
|
||||
* @param calendar
|
||||
*/
|
||||
$scope.delete = function (calendar) {
|
||||
dialogs.confirm(
|
||||
{
|
||||
resolve: {
|
||||
object () {
|
||||
return {
|
||||
title: _t('icalendar.confirmation_required'),
|
||||
msg: _t('icalendar.confirm_delete_import')
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
function () {
|
||||
ICalendar.delete(
|
||||
{ id: calendar.id },
|
||||
function () {
|
||||
// success
|
||||
const idx = $scope.calendars.indexOf(calendar);
|
||||
$scope.calendars.splice(idx, 1);
|
||||
growl.info(_t('icalendar.delete_success'));
|
||||
}, function (error) {
|
||||
// failed
|
||||
growl.error(_t('icalendar.delete_failed'));
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously re-fetches the events from the given calendar
|
||||
* @param calendar
|
||||
*/
|
||||
$scope.sync = function (calendar) {
|
||||
ICalendar.sync(
|
||||
{ id: calendar.id },
|
||||
function () {
|
||||
// success
|
||||
growl.info(_t('icalendar.refresh'));
|
||||
}, function (error) {
|
||||
// failed
|
||||
growl.error(_t('icalendar.sync_failed'));
|
||||
console.error(error);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
@ -754,7 +754,8 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A
|
||||
return $state.go('app.admin.members');
|
||||
}
|
||||
, function (error) {
|
||||
console.log(error);
|
||||
growl.error(_t('failed_to_create_admin') + JSON.stringify(error.data ? error.data : error));
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -16,8 +16,8 @@
|
||||
* Controller used in the public calendar global
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('CalendarController', ['$scope', '$state', '$aside', 'moment', 'Availability', 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise',
|
||||
function ($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise) {
|
||||
Application.Controllers.controller('CalendarController', ['$scope', '$state', '$aside', 'moment', 'Availability', 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise', 'iCalendarPromise',
|
||||
function ($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise, iCalendarPromise) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
let currentMachineEvent = null;
|
||||
machinesPromise.forEach(m => m.checked = true);
|
||||
@ -38,6 +38,9 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
// List of spaces
|
||||
$scope.spaces = spacesPromise.filter(t => !t.disabled);
|
||||
|
||||
// List of external iCalendar sources
|
||||
$scope.externals = iCalendarPromise.map(e => Object.assign(e, { checked: true }));
|
||||
|
||||
// add availabilities source to event sources
|
||||
$scope.eventSources = [];
|
||||
|
||||
@ -48,10 +51,41 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
trainings: isSelectAll('trainings', scope),
|
||||
machines: isSelectAll('machines', scope),
|
||||
spaces: isSelectAll('spaces', scope),
|
||||
externals: isSelectAll('externals', scope),
|
||||
evt: filter.evt,
|
||||
dispo: filter.dispo
|
||||
});
|
||||
return $scope.calendarConfig.events = availabilitySourceUrl();
|
||||
$scope.calendarConfig.events = availabilitySourceUrl();
|
||||
// external iCalendar events sources
|
||||
$scope.externals.forEach(e => {
|
||||
if (e.checked) {
|
||||
if (!$scope.eventSources.some(evt => evt.id === e.id)) {
|
||||
$scope.eventSources.push({
|
||||
id: e.id,
|
||||
url: `/api/i_calendar/${e.id}/events`,
|
||||
textColor: e.text_color || '#000',
|
||||
color: e.color
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if ($scope.eventSources.some(evt => evt.id === e.id)) {
|
||||
const idx = $scope.eventSources.findIndex(evt => evt.id === e.id);
|
||||
$scope.eventSources.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
uiCalendarConfig.calendars.calendar.fullCalendar('refetchEventSources');
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a CSS-like style of the given calendar configuration
|
||||
* @param calendar
|
||||
*/
|
||||
$scope.calendarStyle = function (calendar) {
|
||||
return {
|
||||
'border-color': calendar.color,
|
||||
'color': calendar.text_color
|
||||
};
|
||||
};
|
||||
|
||||
// a variable for formation/machine/event/dispo checkbox is or not checked
|
||||
@ -59,6 +93,7 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
trainings: isSelectAll('trainings', $scope),
|
||||
machines: isSelectAll('machines', $scope),
|
||||
spaces: isSelectAll('spaces', $scope),
|
||||
externals: isSelectAll('externals', $scope),
|
||||
evt: true,
|
||||
dispo: true
|
||||
};
|
||||
@ -85,6 +120,9 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
spaces () {
|
||||
return $scope.spaces;
|
||||
},
|
||||
externals () {
|
||||
return $scope.externals;
|
||||
},
|
||||
filter () {
|
||||
return $scope.filter;
|
||||
},
|
||||
@ -95,10 +133,11 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
return $scope.filterAvailabilities;
|
||||
}
|
||||
},
|
||||
controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'spaces', 'filter', 'toggleFilter', 'filterAvailabilities', function ($scope, $uibModalInstance, trainings, machines, spaces, filter, toggleFilter, filterAvailabilities) {
|
||||
controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'spaces', 'externals', 'filter', 'toggleFilter', 'filterAvailabilities', function ($scope, $uibModalInstance, trainings, machines, spaces, externals, filter, toggleFilter, filterAvailabilities) {
|
||||
$scope.trainings = trainings;
|
||||
$scope.machines = machines;
|
||||
$scope.spaces = spaces;
|
||||
$scope.externals = externals;
|
||||
$scope.filter = filter;
|
||||
|
||||
$scope.toggleFilter = (type, filter) => toggleFilter(type, filter);
|
||||
@ -114,78 +153,11 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
const calendarEventClickCb = function (event, jsEvent, view) {
|
||||
// current calendar object
|
||||
const { calendar } = uiCalendarConfig.calendars;
|
||||
if (event.available_type === 'machines') {
|
||||
currentMachineEvent = event;
|
||||
calendar.fullCalendar('changeView', 'agendaDay');
|
||||
return calendar.fullCalendar('gotoDate', event.start);
|
||||
} else if (event.available_type === 'space') {
|
||||
calendar.fullCalendar('changeView', 'agendaDay');
|
||||
return calendar.fullCalendar('gotoDate', event.start);
|
||||
} else if (event.available_type === 'event') {
|
||||
return $state.go('app.public.events_show', { id: event.event_id });
|
||||
} else if (event.available_type === 'training') {
|
||||
return $state.go('app.public.training_show', { id: event.training_id });
|
||||
} else {
|
||||
if (event.machine_id) {
|
||||
return $state.go('app.public.machines_show', { id: event.machine_id });
|
||||
} else if (event.space_id) {
|
||||
return $state.go('app.public.space_show', { id: event.space_id });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// agendaDay view: disable slotEventOverlap
|
||||
// agendaWeek view: enable slotEventOverlap
|
||||
const toggleSlotEventOverlap = function (view) {
|
||||
// set defaultView, because when we change slotEventOverlap
|
||||
// ui-calendar will trigger rerender calendar
|
||||
$scope.calendarConfig.defaultView = view.type;
|
||||
const today = currentMachineEvent ? currentMachineEvent.start : moment().utc().startOf('day');
|
||||
if ((today > view.intervalStart) && (today < view.intervalEnd) && (today !== view.intervalStart)) {
|
||||
$scope.calendarConfig.defaultDate = today;
|
||||
} else {
|
||||
$scope.calendarConfig.defaultDate = view.intervalStart;
|
||||
}
|
||||
if (view.type === 'agendaDay') {
|
||||
return $scope.calendarConfig.slotEventOverlap = false;
|
||||
} else {
|
||||
return $scope.calendarConfig.slotEventOverlap = true;
|
||||
}
|
||||
};
|
||||
|
||||
// function is called when calendar view is rendered or changed
|
||||
const viewRenderCb = function (view, element) {
|
||||
toggleSlotEventOverlap(view);
|
||||
if (view.type === 'agendaDay') {
|
||||
// get availabilties by 1 day for show machine slots
|
||||
return uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents');
|
||||
}
|
||||
};
|
||||
|
||||
const eventRenderCb = function (event, element) {
|
||||
if (event.tags.length > 0) {
|
||||
let html = '';
|
||||
for (let tag of Array.from(event.tags)) {
|
||||
html += `<span class='label label-success text-white'>${tag.name}</span> `;
|
||||
}
|
||||
element.find('.fc-title').append(`<br/>${html}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getFilter = function () {
|
||||
const t = $scope.trainings.filter(t => t.checked).map(t => t.id);
|
||||
const m = $scope.machines.filter(m => m.checked).map(m => m.id);
|
||||
const s = $scope.spaces.filter(s => s.checked).map(s => s.id);
|
||||
return { t, m, s, evt: $scope.filter.evt, dispo: $scope.filter.dispo };
|
||||
};
|
||||
|
||||
var availabilitySourceUrl = () => `/api/availabilities/public?${$.param(getFilter())}`;
|
||||
|
||||
const initialize = () =>
|
||||
// fullCalendar (v2) configuration
|
||||
/**
|
||||
* Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
*/
|
||||
const initialize = () => {
|
||||
// fullCalendar (v2) configuration
|
||||
$scope.calendarConfig = CalendarConfig({
|
||||
events: availabilitySourceUrl(),
|
||||
slotEventOverlap: true,
|
||||
@ -207,6 +179,97 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
return eventRenderCb(event, element);
|
||||
}
|
||||
});
|
||||
$scope.externals.forEach(e => {
|
||||
if (e.checked) {
|
||||
$scope.eventSources.push({
|
||||
id: e.id,
|
||||
url: `/api/i_calendar/${e.id}/events`,
|
||||
textColor: e.text_color || '#000',
|
||||
color: e.color
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when an event object is clicked in the fullCalendar view
|
||||
*/
|
||||
const calendarEventClickCb = function (event, jsEvent, view) {
|
||||
// current calendar object
|
||||
const { calendar } = uiCalendarConfig.calendars;
|
||||
if (event.available_type === 'machines') {
|
||||
currentMachineEvent = event;
|
||||
calendar.fullCalendar('changeView', 'agendaDay');
|
||||
calendar.fullCalendar('gotoDate', event.start);
|
||||
} else if (event.available_type === 'space') {
|
||||
calendar.fullCalendar('changeView', 'agendaDay');
|
||||
calendar.fullCalendar('gotoDate', event.start);
|
||||
} else if (event.available_type === 'event') {
|
||||
$state.go('app.public.events_show', { id: event.event_id });
|
||||
} else if (event.available_type === 'training') {
|
||||
$state.go('app.public.training_show', { id: event.training_id });
|
||||
} else {
|
||||
if (event.machine_id) {
|
||||
$state.go('app.public.machines_show', { id: event.machine_id });
|
||||
} else if (event.space_id) {
|
||||
$state.go('app.public.space_show', { id: event.space_id });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// agendaDay view: disable slotEventOverlap
|
||||
// agendaWeek view: enable slotEventOverlap
|
||||
const toggleSlotEventOverlap = function (view) {
|
||||
// set defaultView, because when we change slotEventOverlap
|
||||
// ui-calendar will trigger rerender calendar
|
||||
$scope.calendarConfig.defaultView = view.type;
|
||||
const today = currentMachineEvent ? currentMachineEvent.start : moment().utc().startOf('day');
|
||||
if ((today > view.intervalStart) && (today < view.intervalEnd) && (today !== view.intervalStart)) {
|
||||
$scope.calendarConfig.defaultDate = today;
|
||||
} else {
|
||||
$scope.calendarConfig.defaultDate = view.intervalStart;
|
||||
}
|
||||
if (view.type === 'agendaDay') {
|
||||
return $scope.calendarConfig.slotEventOverlap = false;
|
||||
} else {
|
||||
return $scope.calendarConfig.slotEventOverlap = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is called when calendar view is rendered or changed
|
||||
* @see https://fullcalendar.io/docs/v3/viewRender#v2
|
||||
*/
|
||||
const viewRenderCb = function (view, element) {
|
||||
toggleSlotEventOverlap(view);
|
||||
if (view.type === 'agendaDay') {
|
||||
// get availabilties by 1 day for show machine slots
|
||||
return uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered by fullCalendar when it is about to render an event.
|
||||
* @see https://fullcalendar.io/docs/v3/eventRender#v2
|
||||
*/
|
||||
const eventRenderCb = function (event, element) {
|
||||
if (event.tags && event.tags.length > 0) {
|
||||
let html = '';
|
||||
for (let tag of Array.from(event.tags)) {
|
||||
html += `<span class='label label-success text-white'>${tag.name}</span> `;
|
||||
}
|
||||
element.find('.fc-title').append(`<br/>${html}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getFilter = function () {
|
||||
const t = $scope.trainings.filter(t => t.checked).map(t => t.id);
|
||||
const m = $scope.machines.filter(m => m.checked).map(m => m.id);
|
||||
const s = $scope.spaces.filter(s => s.checked).map(s => s.id);
|
||||
return { t, m, s, evt: $scope.filter.evt, dispo: $scope.filter.dispo };
|
||||
};
|
||||
|
||||
var availabilitySourceUrl = () => `/api/availabilities/public?${$.param(getFilter())}`;
|
||||
|
||||
// !!! MUST BE CALLED AT THE END of the controller
|
||||
return initialize();
|
||||
|
@ -638,6 +638,7 @@ angular.module('application.router', ['ui.router'])
|
||||
trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }],
|
||||
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
|
||||
spacesPromise: ['Space', function (Space) { return Space.query().$promise; }],
|
||||
iCalendarPromise: ['ICalendar', function (ICalendar) { return ICalendar.query().$promise; }],
|
||||
translations: ['Translations', function (Translations) { return Translations.query(['app.public.calendar']).$promise; }]
|
||||
}
|
||||
})
|
||||
@ -659,6 +660,19 @@ angular.module('application.router', ['ui.router'])
|
||||
translations: ['Translations', function (Translations) { return Translations.query('app.admin.calendar').$promise; }]
|
||||
}
|
||||
})
|
||||
.state('app.admin.calendar.icalendar', {
|
||||
url: '/admin/calendar/icalendar',
|
||||
views: {
|
||||
'main@': {
|
||||
templateUrl: '<%= asset_path "admin/calendar/icalendar.html" %>',
|
||||
controller: 'AdminICalendarController'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
iCalendars: ['ICalendar', function (ICalendar) { return ICalendar.query().$promise; }],
|
||||
translations: ['Translations', function (Translations) { return Translations.query('app.admin.icalendar').$promise; }]
|
||||
}
|
||||
})
|
||||
|
||||
// project's elements
|
||||
.state('app.admin.project_elements', {
|
||||
|
5
app/assets/javascripts/services/ical.js
Normal file
5
app/assets/javascripts/services/ical.js
Normal file
@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
Application.Services.factory('Ical', ['$resource', function ($resource) {
|
||||
return $resource('/api/ical/externals');
|
||||
}]);
|
17
app/assets/javascripts/services/icalendar.js
Normal file
17
app/assets/javascripts/services/icalendar.js
Normal file
@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
Application.Services.factory('ICalendar', ['$resource', function ($resource) {
|
||||
return $resource('/api/i_calendar/:id',
|
||||
{ id: '@id' }, {
|
||||
events: {
|
||||
method: 'GET',
|
||||
url: '/api/i_calendar/:id/events'
|
||||
},
|
||||
sync: {
|
||||
method: 'POST',
|
||||
url: '/api/i_calendar/:id/sync',
|
||||
params: { id: '@id' }
|
||||
}
|
||||
}
|
||||
);
|
||||
}]);
|
@ -670,5 +670,6 @@ padding: 10px;
|
||||
font-size: 10px;
|
||||
padding: 2px;
|
||||
margin-left: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
@ -102,6 +102,7 @@ p, .widget p {
|
||||
|
||||
.text-italic { font-style: italic; }
|
||||
|
||||
.text-left { text-align: left !important; }
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
|
@ -32,11 +32,7 @@
|
||||
@import "app.buttons";
|
||||
@import "app.components";
|
||||
@import "app.plugins";
|
||||
@import "modules/invoice";
|
||||
@import "modules/signup";
|
||||
@import "modules/abuses";
|
||||
@import "modules/cookies";
|
||||
@import "modules/stripe";
|
||||
@import "modules/*";
|
||||
|
||||
@import "app.responsive";
|
||||
|
||||
|
24
app/assets/stylesheets/modules/icalendar.scss
Normal file
24
app/assets/stylesheets/modules/icalendar.scss
Normal file
@ -0,0 +1,24 @@
|
||||
.calendar-form {
|
||||
margin : 2em;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
padding: 1em;
|
||||
|
||||
& > .input-group, & > .minicolors {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-name {
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.calendar-url {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.external-calendar-legend {
|
||||
border-left: 3px solid;
|
||||
border-radius: 3px;
|
||||
}
|
@ -5,15 +5,17 @@
|
||||
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
<div class="col-xs-10 b-l b-r-md">
|
||||
<section class="heading-title">
|
||||
<h1 translate>{{ 'admin_calendar.calendar_management' }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
|
||||
<section class="heading-actions wrapper" ng-class="{'p-s': !fablabWithoutSpaces}">
|
||||
|
||||
<div class="col-xs-1">
|
||||
<section class="heading-actions wrapper">
|
||||
<a role="button" ui-sref="app.admin.calendar.icalendar" class="btn btn-default b-2x rounded pointer m-t-sm">
|
||||
<i class="fa fa-exchange" aria-hidden="true"></i>
|
||||
</a>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -28,7 +30,7 @@
|
||||
<div class="calendar-legend-block">
|
||||
<h4 translate>{{ 'admin_calendar.legend' }}</h4>
|
||||
<div class="legends">
|
||||
<span class="calendar-legend text-sm border-formation" ng-class="{'m-t-sm': fablabWithoutSpaces}" translate>{{ 'admin_calendar.trainings' }}</span><br>
|
||||
<span class="calendar-legend text-sm border-formation" translate>{{ 'admin_calendar.trainings' }}</span><br>
|
||||
<span class="calendar-legend text-sm border-machine" translate>{{ 'admin_calendar.machines' }}</span><br>
|
||||
<span class="calendar-legend text-sm border-space" ng-hide="fablabWithoutSpaces" translate>{{ 'admin_calendar.spaces' }}</span>
|
||||
<span class="calendar-legend text-sm border-event" ng-show="eventsInCalendar" translate>{{ 'admin_calendar.events' }}</span>
|
||||
|
96
app/assets/templates/admin/calendar/icalendar.html
Normal file
96
app/assets/templates/admin/calendar/icalendar.html
Normal file
@ -0,0 +1,96 @@
|
||||
<section class="heading b-b">
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a role="button" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-md-8 b-l b-r-md">
|
||||
<section class="heading-title">
|
||||
<h1 translate>{{ 'icalendar.icalendar_import' }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<section class="heading-actions wrapper">
|
||||
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="row no-gutter">
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-9">
|
||||
<div class="alert alert-info m-lg" translate>
|
||||
{{ 'icalendar.intro' }}
|
||||
</div>
|
||||
<div class="wrapper-lg">
|
||||
<table class="table" ng-show="calendars.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 35%;" translate>{{ 'icalendar.name' }}</th>
|
||||
<th style="width: 35%;" translate>{{ 'icalendar.url' }}</th>
|
||||
<th translate>{{ 'icalendar.display' }}</th>
|
||||
<th style="width: 20%;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="calendar in calendars">
|
||||
<td class="calendar-name">{{calendar.name}}</td>
|
||||
<td class="calendar-url"><a href="{{calendar.url}}" target="_blank">{{calendar.url}}</a></td>
|
||||
<td class="calendar-legend-block text-left"><span class="calendar-legend" ng-style="calendarStyle(calendar)" translate> {{ calendar.text_hidden ? '' : 'icalendar.example' }}</span>
|
||||
<td class="calendar-actions">
|
||||
<button class="btn btn-info" ng-click="sync(calendar)"><i class="fa fa-refresh"></i></button>
|
||||
<button class="btn btn-danger" ng-click="delete(calendar)"><i class="fa fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<form class="calendar-form" name="newImportForm">
|
||||
<h4 translate>{{ 'icalendar.new_import' }}</h4>
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">
|
||||
<i class="fa fa-font"></i>
|
||||
</div>
|
||||
<input type="text" ng-model="newCalendar.name" class="form-control" placeholder="{{ 'icalendar.name' | translate }}" required>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">
|
||||
<i class="fa fa-link"></i>
|
||||
</div>
|
||||
<input type="url" ng-model="newCalendar.url" class="form-control" placeholder="{{ 'icalendar.url' | translate }}" required>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
</div>
|
||||
<input type="text" minicolors ng-model="newCalendar.color" class="form-control" placeholder="{{ 'icalendar.color' | translate}}" required/>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
</div>
|
||||
<input type="text" minicolors ng-model="newCalendar.text_color" class="form-control" placeholder="{{ 'icalendar.text_color' | translate}}" ng-required="!newCalendar.text_hidden"/>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="hideText" class="control-label m-r" translate>{{ 'icalendar.hide_text' }}</label>
|
||||
<input bs-switch
|
||||
ng-model="newCalendar.text_hidden"
|
||||
id="hideText"
|
||||
type="checkbox"
|
||||
class="form-control"
|
||||
switch-on-text="{{ 'icalendar.hidden' | translate }}"
|
||||
switch-off-text="{{ 'icalendar.shown' | translate }}"
|
||||
switch-animate="true"/>
|
||||
</div>
|
||||
<div class="m-t text-right">
|
||||
<button role="button" class="btn btn-warning" ng-click="save()" ng-disabled="newImportForm.$invalid" translate>
|
||||
{{ 'confirm' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
@ -341,7 +341,9 @@
|
||||
</select>
|
||||
<button name="button" class="btn btn-warning m-t" ng-click="save(machinesSortBy)" translate>{{ 'save' }}</button>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-4">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,7 +41,7 @@
|
||||
<h3 translate>{{ 'calendar.filter_calendar' }}</h3>
|
||||
</div>
|
||||
<div class="widget-content no-bg auto wrapper calendar-filter">
|
||||
<ng-include src="'<%= asset_path 'calendar/filter.html' %>'"></ng-include>
|
||||
<ng-include src="'<%= asset_path "calendar/filter.html" %>'"></ng-include>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -56,7 +56,7 @@
|
||||
<h1 class="modal-title" translate>{{ 'calendar.filter_calendar' }}</h1>
|
||||
</div>
|
||||
<div class="modal-body widget-content calendar-filter calendar-filter-aside">
|
||||
<ng-include src="'<%= asset_path 'calendar/filter.html' %>'"></ng-include>
|
||||
<ng-include src="'<%= asset_path "calendar/filter.html" %>'"></ng-include>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
@ -36,3 +36,14 @@
|
||||
<h3 class="col-md-11 col-sm-11 col-xs-11 text-black" translate>{{ 'calendar.show_unavailables' }}</h3>
|
||||
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.dispo" ng-change="filterAvailabilities(filter)">
|
||||
</div>
|
||||
<div class="m-t" ng-hide="externals.length == 0">
|
||||
<div class="row">
|
||||
<h3 class="col-md-11 col-sm-11 col-xs-11 text-black" translate>{{ 'calendar.externals' }}</h3>
|
||||
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.externals" ng-change="toggleFilter('externals', filter)">
|
||||
</div>
|
||||
|
||||
<div class="row" ng-repeat="e in externals">
|
||||
<span ng-style="calendarStyle(e)" class="col-md-11 col-sm-11 col-xs-11 external-calendar-legend">{{::e.name}}</span>
|
||||
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="e.checked" ng-change="filterAvailabilities(filter)">
|
||||
</div>
|
||||
</div>
|
||||
|
52
app/controllers/api/i_calendar_controller.rb
Normal file
52
app/controllers/api/i_calendar_controller.rb
Normal file
@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# API Controller for resources of type iCalendar
|
||||
class API::ICalendarController < API::ApiController
|
||||
before_action :authenticate_user!, except: %i[index events]
|
||||
before_action :set_i_cal, only: [:destroy]
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@i_cals = ICalendar.all
|
||||
end
|
||||
|
||||
def create
|
||||
authorize ICalendar
|
||||
@i_cal = ICalendar.new(i_calendar_params)
|
||||
if @i_cal.save
|
||||
render :show, status: :created, location: @i_cal
|
||||
else
|
||||
render json: @i_cal.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize ICalendar
|
||||
@i_cal.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def events
|
||||
start_date = ActiveSupport::TimeZone[params[:timezone]]&.parse(params[:start])
|
||||
end_date = ActiveSupport::TimeZone[params[:timezone]]&.parse(params[:end])&.end_of_day
|
||||
|
||||
@events = ICalendarEvent.where(i_calendar_id: params[:id])
|
||||
.where('dtstart >= ? AND dtend <= ?', start_date, end_date)
|
||||
.joins(:i_calendar)
|
||||
end
|
||||
|
||||
def sync
|
||||
ICalendarImportWorker.perform_async([params[:id]])
|
||||
render json: { processing: [params[:id]] }, status: :created
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_i_cal
|
||||
@i_cal = ICalendar.find(params[:id])
|
||||
end
|
||||
|
||||
def i_calendar_params
|
||||
params.require(:i_calendar).permit(:name, :url, :color, :text_color, :text_hidden)
|
||||
end
|
||||
end
|
19
app/controllers/api/ical_controller.rb
Normal file
19
app/controllers/api/ical_controller.rb
Normal file
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# API Controller for resources of type iCalendar
|
||||
class API::IcalController < API::ApiController
|
||||
respond_to :json
|
||||
|
||||
def externals
|
||||
require 'net/http'
|
||||
require 'uri'
|
||||
|
||||
ics = Net::HTTP.get(URI.parse('https://calendar.google.com/calendar/ical/sylvain%40sleede.com/public/basic.ics'))
|
||||
|
||||
require 'icalendar'
|
||||
require 'icalendar/tzinfo'
|
||||
|
||||
cals = Icalendar::Calendar.parse(ics)
|
||||
@events = cals.first.events
|
||||
end
|
||||
end
|
15
app/models/i_calendar.rb
Normal file
15
app/models/i_calendar.rb
Normal file
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# iCalendar (RFC 5545) files, stored by URL and kept with their display configuration
|
||||
class ICalendar < ActiveRecord::Base
|
||||
has_many :i_calendar_events, dependent: :destroy
|
||||
|
||||
after_create :sync_events
|
||||
|
||||
private
|
||||
|
||||
def sync_events
|
||||
worker = ICalendarImportWorker.new
|
||||
worker.perform([id])
|
||||
end
|
||||
end
|
12
app/models/i_calendar_event.rb
Normal file
12
app/models/i_calendar_event.rb
Normal file
@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# iCalendar (RFC 5545) event, belonging to an ICalendar object (its source)
|
||||
class ICalendarEvent < ActiveRecord::Base
|
||||
belongs_to :i_calendar
|
||||
|
||||
def self.update_or_create_by(args, attributes)
|
||||
obj = find_or_create_by(args)
|
||||
obj.update(attributes)
|
||||
obj
|
||||
end
|
||||
end
|
12
app/policies/i_calendar_policy.rb
Normal file
12
app/policies/i_calendar_policy.rb
Normal file
@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Check the access policies for API::ICalendarController
|
||||
class ICalendarPolicy < ApplicationPolicy
|
||||
def create?
|
||||
user.admin?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
user.admin?
|
||||
end
|
||||
end
|
35
app/services/i_calendar_import_service.rb
Normal file
35
app/services/i_calendar_import_service.rb
Normal file
@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Import all events from a given remote RFC 5545 iCalendar
|
||||
class ICalendarImportService
|
||||
def import(i_calendar_id)
|
||||
require 'net/http'
|
||||
require 'uri'
|
||||
require 'icalendar'
|
||||
|
||||
uids = []
|
||||
|
||||
i_cal = ICalendar.find(i_calendar_id)
|
||||
ics = Net::HTTP.get(URI.parse(i_cal.url))
|
||||
cals = Icalendar::Calendar.parse(ics)
|
||||
|
||||
# create new events and update existings
|
||||
cals.each do |cal|
|
||||
cal.events.each do |evt|
|
||||
uids.push(evt.uid.to_s)
|
||||
ICalendarEvent.update_or_create_by(
|
||||
{ uid: evt.uid.to_s },
|
||||
{
|
||||
dtstart: evt.dtstart,
|
||||
dtend: evt.dtend,
|
||||
summary: evt.summary,
|
||||
description: evt.description,
|
||||
i_calendar_id: i_calendar_id
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
# remove deleted events
|
||||
ICalendarEvent.where(i_calendar_id: i_calendar_id).where.not(uid: uids).destroy_all
|
||||
end
|
||||
end
|
@ -41,7 +41,7 @@ class UserService
|
||||
# if the authentication is made through an SSO, generate a migration token
|
||||
admin.generate_auth_migration_token unless AuthProvider.active.providable_type == DatabaseProvider.name
|
||||
|
||||
saved = admin.save(validate: false)
|
||||
saved = admin.save
|
||||
if saved
|
||||
admin.send_confirmation_instructions
|
||||
admin.add_role(:admin)
|
||||
|
3
app/views/api/i_calendar/_i_calendar.json.jbuilder
Normal file
3
app/views/api/i_calendar/_i_calendar.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! i_cal, :id, :name, :url, :color, :text_color, :text_hidden
|
9
app/views/api/i_calendar/events.json.jbuilder
Normal file
9
app/views/api/i_calendar/events.json.jbuilder
Normal file
@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.array!(@events) do |event|
|
||||
json.id event.uid
|
||||
json.title event.i_calendar.text_hidden ? '' : event.summary
|
||||
json.start event.dtstart.iso8601
|
||||
json.end event.dtend.iso8601
|
||||
json.backgroundColor 'white'
|
||||
end
|
5
app/views/api/i_calendar/index.json.jbuilder
Normal file
5
app/views/api/i_calendar/index.json.jbuilder
Normal file
@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.array!(@i_cals) do |i_cal|
|
||||
json.partial! 'api/i_calendar/i_calendar', i_cal: i_cal
|
||||
end
|
3
app/views/api/i_calendar/show.json.jbuilder
Normal file
3
app/views/api/i_calendar/show.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'api/i_calendar/i_calendar', i_cal: @i_cal
|
8
app/views/api/ical/externals.json.jbuilder
Normal file
8
app/views/api/ical/externals.json.jbuilder
Normal file
@ -0,0 +1,8 @@
|
||||
json.array!(@events) do |event|
|
||||
json.id event.uid
|
||||
json.title event.summary
|
||||
json.start event.dtstart.iso8601
|
||||
json.end event.dtend.iso8601
|
||||
json.backgroundColor 'white'
|
||||
json.borderColor '#214712'
|
||||
end
|
14
app/workers/i_calendar_import_worker.rb
Normal file
14
app/workers/i_calendar_import_worker.rb
Normal file
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Periodically import the iCalendar RFC 5545 events from the configured source
|
||||
class ICalendarImportWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(calendar_ids = ICalendar.all.map(&:id))
|
||||
service = ICalendarImportService.new
|
||||
|
||||
calendar_ids.each do |id|
|
||||
service.import(id)
|
||||
end
|
||||
end
|
||||
end
|
@ -88,6 +88,28 @@ en:
|
||||
view_reservations: "View reservations"
|
||||
legend: "legend"
|
||||
|
||||
icalendar:
|
||||
icalendar:
|
||||
icalendar_import: "iCalendar import"
|
||||
intro: "Fab-manager allows to automatically import calendar events, at RFC 5545 iCalendar format, from external URL. These URL are synchronized every nights and the events are shown in the public calendar."
|
||||
new_import: "New ICS import"
|
||||
color: "Colour"
|
||||
text_color: "Text colour"
|
||||
url: "URL"
|
||||
name: "Name"
|
||||
example: "Example"
|
||||
display: "Display"
|
||||
hide_text: "Hide the text"
|
||||
hidden: "Hidden"
|
||||
shown: "Shown"
|
||||
create_error: "Unable to create iCalendar import. Please try again later"
|
||||
delete_failed: "Unable to delete the iCalendar import. Please try again later"
|
||||
refresh: "Updating..."
|
||||
sync_failed: "Unable to synchronize the URL. Please try again later"
|
||||
confirmation_required: "Confirmation required"
|
||||
confirm_delete_import: "Do you really want to delete this iCalendar import?"
|
||||
delete_success: "iCalendar import successfully deleted"
|
||||
|
||||
project_elements:
|
||||
# management of the projects' components
|
||||
project_elements:
|
||||
@ -678,6 +700,7 @@ en:
|
||||
# add a new administrator to the platform
|
||||
add_an_administrator: "Add an administrator"
|
||||
administrator_successfully_created_he_will_receive_his_connection_directives_by_email: "Administrator successfully created. {GENDER, select, female{She} other{He}} receive {GENDER, select, female{her} other{his}} connection directives by e-mail." # messageFormat interpolation
|
||||
failed_to_create_admin: "Unable to create the administrator:"
|
||||
|
||||
authentication_new:
|
||||
# add a new authentication provider (SSO)
|
||||
|
@ -88,6 +88,28 @@ es:
|
||||
view_reservations: "Ver reservas" # translation_missing
|
||||
legend: "leyenda"
|
||||
|
||||
icalendar:
|
||||
icalendar:
|
||||
icalendar_import: "iCalendar import" # translation_missing
|
||||
intro: "Fab-manager allows to automatically import calendar events, at RFC 5545 iCalendar format, from external URL. These URL are synchronized every nights and the events are shown in the public calendar." # translation_missing
|
||||
new_import: "New ICS import" # translation_missing
|
||||
color: "Colour" # translation_missing
|
||||
text_color: "Text colour" # translation_missing
|
||||
url: "URL" # translation_missing
|
||||
name: "Name" # translation_missing
|
||||
example: "Example" # translation_missing
|
||||
display: "Display" # translation_missing
|
||||
hide_text: "Hide the text" # translation_missing
|
||||
hidden: "Hidden" # translation_missing
|
||||
shown: "Shown" # translation_missing
|
||||
create_error: "Unable to create iCalendar import. Please try again later" # translation_missing
|
||||
delete_failed: "Unable to delete the iCalendar import. Please try again later" # translation_missing
|
||||
refresh: "Updating..." # translation_missing
|
||||
sync_failed: "Unable to synchronize the URL. Please try again later" # translation_missing
|
||||
confirmation_required: "Confirmation required" # translation_missing
|
||||
confirm_delete_import: "Do you really want to delete this iCalendar import?" # translation_missing
|
||||
delete_success: "iCalendar import successfully deleted" # translation_missing
|
||||
|
||||
project_elements:
|
||||
# management of the projects' components
|
||||
project_elements:
|
||||
@ -678,6 +700,7 @@ es:
|
||||
# add a new administrator to the platform
|
||||
add_an_administrator: "Agregar un administrador"
|
||||
administrator_successfully_created_he_will_receive_his_connection_directives_by_email: "administrador creado correctamente. {GENDER, select, female{She} other{He}} receive {GENDER, select, female{her} other{his}} directivas de conexión por e-mail." # messageFormat interpolation
|
||||
failed_to_create_admin: "No se puede crear el administrador :"
|
||||
|
||||
authentication_new:
|
||||
# add a new authentication provider (SSO)
|
||||
|
@ -88,6 +88,28 @@ fr:
|
||||
view_reservations: "Voir les réservations"
|
||||
legend: "Légende"
|
||||
|
||||
icalendar:
|
||||
icalendar:
|
||||
icalendar_import: "Import iCalendar"
|
||||
intro: "Fab-manager vous permet d'importer automatiquement des évènements de calendrier, au format iCalendar RFC 5545, depuis des URL externes. Ces URL seront synchronisée toutes les nuits et les évènements seront affichés dans le calendrier publique."
|
||||
new_import: "Nouvel import ICS"
|
||||
color: "Couleur"
|
||||
text_color: "Couleur du texte"
|
||||
url: "URL"
|
||||
name: "Nom"
|
||||
example: "Exemple"
|
||||
display: "Affichage"
|
||||
hide_text: "Cacher le texte"
|
||||
hidden: "Caché"
|
||||
shown: "Affiché"
|
||||
create_error: "Impossible de créer l'import iCalendar. Veuillez réessayer ultérieurement"
|
||||
delete_failed: "Impossible de supprimer l'import iCalendar. Veuillez réessayer ultérieurement"
|
||||
refresh: "Mise à jour en cours..."
|
||||
sync_failed: "Impossible de synchroniser l'URL. Veuillez réessayer ultérieurement"
|
||||
confirmation_required: "Confirmation requise"
|
||||
confirm_delete_import: "Êtes-vous sur de vouloir supprimer cet import iCalendar ?"
|
||||
delete_success: "L'import iCalendar a bien été supprimé"
|
||||
|
||||
project_elements:
|
||||
# gestion des éléments constituant les projets
|
||||
project_elements:
|
||||
@ -678,6 +700,7 @@ fr:
|
||||
# ajouter un nouvel administrateur à la plate-forme
|
||||
add_an_administrator: "Ajouter un administrateur"
|
||||
administrator_successfully_created_he_will_receive_his_connection_directives_by_email: "L'administrateur a bien été créé. {GENDER, select, female{Elle} other{Il}} recevra ses instructions de connexion par email." # messageFormat interpolation
|
||||
failed_to_create_admin: "Impossible de créer l'administrateur :"
|
||||
|
||||
authentication_new:
|
||||
# ajouter un nouveau fournisseur d'authentification (SSO)
|
||||
|
@ -88,6 +88,28 @@ pt:
|
||||
view_reservations: "Ver reservas" # translation_missing
|
||||
legend: "legenda"
|
||||
|
||||
icalendar:
|
||||
icalendar:
|
||||
icalendar_import: "iCalendar import" # translation_missing
|
||||
intro: "Fab-manager allows to automatically import calendar events, at RFC 5545 iCalendar format, from external URL. These URL are synchronized every nights and the events are shown in the public calendar." # translation_missing
|
||||
new_import: "New ICS import" # translation_missing
|
||||
color: "Colour" # translation_missing
|
||||
text_color: "Text colour" # translation_missing
|
||||
url: "URL" # translation_missing
|
||||
name: "Name" # translation_missing
|
||||
example: "Example" # translation_missing
|
||||
display: "Display" # translation_missing
|
||||
hide_text: "Hide the text" # translation_missing
|
||||
hidden: "Hidden" # translation_missing
|
||||
shown: "Shown" # translation_missing
|
||||
create_error: "Unable to create iCalendar import. Please try again later" # translation_missing
|
||||
delete_failed: "Unable to delete the iCalendar import. Please try again later" # translation_missing
|
||||
refresh: "Updating..." # translation_missing
|
||||
sync_failed: "Unable to synchronize the URL. Please try again later" # translation_missing
|
||||
confirmation_required: "Confirmation required" # translation_missing
|
||||
confirm_delete_import: "Do you really want to delete this iCalendar import?" # translation_missing
|
||||
delete_success: "iCalendar import successfully deleted" # translation_missing
|
||||
|
||||
project_elements:
|
||||
# management of the projects' components
|
||||
project_elements:
|
||||
@ -678,6 +700,7 @@ pt:
|
||||
# add a new administrator to the platform
|
||||
add_an_administrator: "Adicionar administrador"
|
||||
administrator_successfully_created_he_will_receive_his_connection_directives_by_email: "Administrator criado com sucesso. {GENDER, select, female{Ela} other{Ele}} receberá {GENDER, select, female{sua} other{seu}} diretivas de conexão por e-mail." # messageFormat interpolation
|
||||
failed_to_create_admin: "Não é possível criar administrador:"
|
||||
|
||||
authentication_new:
|
||||
# add a new authentication provider (SSO)
|
||||
|
@ -306,6 +306,7 @@ en:
|
||||
machines: "Machines"
|
||||
spaces: "Spaces"
|
||||
events: "Events"
|
||||
externals: "Other calendars"
|
||||
|
||||
spaces_list:
|
||||
# list of spaces
|
||||
|
@ -306,6 +306,7 @@ es:
|
||||
machines: "Máquinas"
|
||||
spaces: "Espacios"
|
||||
events: "Eventos"
|
||||
externals: "Otros calendarios"
|
||||
|
||||
spaces_list:
|
||||
# list of spaces
|
||||
|
@ -306,6 +306,7 @@ fr:
|
||||
machines: "Machines"
|
||||
spaces: "Espaces"
|
||||
events: "Évènements"
|
||||
externals: "Autres calendriers"
|
||||
|
||||
spaces_list:
|
||||
# liste des espaces
|
||||
|
@ -306,6 +306,7 @@ pt:
|
||||
machines: "Máquinas"
|
||||
spaces: "Espaços"
|
||||
events: "Eventos"
|
||||
externals: "Outras agendas"
|
||||
|
||||
spaces_list:
|
||||
# list of spaces
|
||||
|
@ -112,6 +112,11 @@ Rails.application.routes.draw do
|
||||
get 'first', action: 'first', on: :collection
|
||||
end
|
||||
|
||||
resources :i_calendar, only: %i[index create destroy] do
|
||||
get 'events', on: :member
|
||||
post 'sync', on: :member
|
||||
end
|
||||
|
||||
# for admin
|
||||
resources :trainings do
|
||||
get :availabilities, on: :member
|
||||
|
@ -15,6 +15,11 @@ generate_statistic:
|
||||
class: "StatisticWorker"
|
||||
queue: default
|
||||
|
||||
i_calendar_import:
|
||||
cron: "0 2 * * *"
|
||||
class: "ICalendarImportWorker"
|
||||
queue: default
|
||||
|
||||
open_api_trace_calls_count:
|
||||
cron: "0 4 * * 0" # every sunday at 4am
|
||||
class: "OpenAPITraceCallsCountWorker"
|
||||
|
17
db/migrate/20191127153729_create_i_calendars.rb
Normal file
17
db/migrate/20191127153729_create_i_calendars.rb
Normal file
@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# From this migration, we store URL to iCalendar files and a piece of configuration about them.
|
||||
# This allows to display the events of these external calendars in fab-manager
|
||||
class CreateICalendars < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :i_calendars do |t|
|
||||
t.string :url
|
||||
t.string :name
|
||||
t.string :color
|
||||
t.string :text_color
|
||||
t.boolean :text_hidden
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
end
|
15
db/migrate/20191202135507_create_i_calendar_events.rb
Normal file
15
db/migrate/20191202135507_create_i_calendar_events.rb
Normal file
@ -0,0 +1,15 @@
|
||||
class CreateICalendarEvents < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :i_calendar_events do |t|
|
||||
t.string :uid
|
||||
t.datetime :dtstart
|
||||
t.datetime :dtend
|
||||
t.string :summary
|
||||
t.string :description
|
||||
t.string :attendee
|
||||
t.belongs_to :i_calendar, index: true, foreign_key: true
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
end
|
27
db/schema.rb
27
db/schema.rb
@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20191113103352) do
|
||||
ActiveRecord::Schema.define(version: 20191202135507) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@ -251,6 +251,30 @@ ActiveRecord::Schema.define(version: 20191113103352) do
|
||||
add_index "history_values", ["invoicing_profile_id"], name: "index_history_values_on_invoicing_profile_id", using: :btree
|
||||
add_index "history_values", ["setting_id"], name: "index_history_values_on_setting_id", using: :btree
|
||||
|
||||
create_table "i_calendar_events", force: :cascade do |t|
|
||||
t.string "uid"
|
||||
t.datetime "dtstart"
|
||||
t.datetime "dtend"
|
||||
t.string "summary"
|
||||
t.string "description"
|
||||
t.string "attendee"
|
||||
t.integer "i_calendar_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
add_index "i_calendar_events", ["i_calendar_id"], name: "index_i_calendar_events_on_i_calendar_id", using: :btree
|
||||
|
||||
create_table "i_calendars", force: :cascade do |t|
|
||||
t.string "url"
|
||||
t.string "name"
|
||||
t.string "color"
|
||||
t.string "text_color"
|
||||
t.boolean "text_hidden"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "imports", force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.string "attachment"
|
||||
@ -923,6 +947,7 @@ ActiveRecord::Schema.define(version: 20191113103352) do
|
||||
add_foreign_key "exports", "users"
|
||||
add_foreign_key "history_values", "invoicing_profiles"
|
||||
add_foreign_key "history_values", "settings"
|
||||
add_foreign_key "i_calendar_events", "i_calendars"
|
||||
add_foreign_key "invoices", "coupons"
|
||||
add_foreign_key "invoices", "invoicing_profiles"
|
||||
add_foreign_key "invoices", "invoicing_profiles", column: "operator_profile_id"
|
||||
|
19
test/fixtures/i_calendar_events.yml
vendored
Normal file
19
test/fixtures/i_calendar_events.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
uid: MyString
|
||||
dtstart: 2019-12-02 14:55:07
|
||||
dtend: 2019-12-02 14:55:07
|
||||
summary: MyString
|
||||
description: MyString
|
||||
attendee: MyString
|
||||
i_calendar_id:
|
||||
|
||||
two:
|
||||
uid: MyString
|
||||
dtstart: 2019-12-02 14:55:07
|
||||
dtend: 2019-12-02 14:55:07
|
||||
summary: MyString
|
||||
description: MyString
|
||||
attendee: MyString
|
||||
i_calendar_id:
|
13
test/fixtures/i_calendars.yml
vendored
Normal file
13
test/fixtures/i_calendars.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
url: MyString
|
||||
color: MyString
|
||||
text_color: MyString
|
||||
text_hidden: false
|
||||
|
||||
two:
|
||||
url: MyString
|
||||
color: MyString
|
||||
text_color: MyString
|
||||
text_hidden: false
|
Loading…
Reference in New Issue
Block a user