mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-01 12:24:28 +01:00
Merge branch 'ics' of git.sleede.com:projets/fab-manager into ics
This commit is contained in:
commit
5215d0643d
@ -9,11 +9,12 @@
|
||||
- Configuration of phone number in members registration forms: can be required or optional, depending on `PHONE_REQUIRED` configuration
|
||||
- Improved user experience in defining slots in the calendar management
|
||||
- Improved notification email to the member when a rolling subscription is taken
|
||||
- Calendar management: improved legend style
|
||||
- Calendar management: improved legend display and visual behavior
|
||||
- Handle Ctrl^C in upgrade scripts
|
||||
- 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)
|
||||
|
@ -88,6 +88,8 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.rout
|
||||
$rootScope.fablabWithoutInvoices = Fablab.withoutInvoices;
|
||||
// Global config: if true, the phone number is required to create an account
|
||||
$rootScope.phoneRequired = Fablab.phoneRequired;
|
||||
// Global config: if true, the events are shown in the admin calendar
|
||||
$rootScope.eventsInCalendar = Fablab.eventsInCalendar;
|
||||
|
||||
// Global function to allow the user to navigate to the previous screen (ie. $state).
|
||||
// If no previous $state were recorded, navigate to the home page
|
||||
|
@ -23,7 +23,6 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// The calendar is divided in slots of 30 minutes
|
||||
let loadingCb;
|
||||
const BASE_SLOT = '00:30:00';
|
||||
|
||||
// The bookings can be positioned every half hours
|
||||
@ -40,6 +39,9 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
||||
// currently selected availability
|
||||
$scope.availability = null;
|
||||
|
||||
// corresponding fullCalendar item in the DOM
|
||||
$scope.availabilityDom = null;
|
||||
|
||||
// bind the availabilities slots with full-Calendar events
|
||||
$scope.eventSources = [];
|
||||
$scope.eventSources.push({
|
||||
@ -62,7 +64,10 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
||||
return calendarEventClickCb(event, jsEvent, view);
|
||||
},
|
||||
eventRender (event, element, view) {
|
||||
return eventRenderCb(event, element);
|
||||
return eventRenderCb(event, element, view);
|
||||
},
|
||||
viewRender(view, element) {
|
||||
return viewRenderCb(view, element);
|
||||
},
|
||||
loading (isLoading, view) {
|
||||
return loadingCb(isLoading, view);
|
||||
@ -328,6 +333,12 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
||||
var calendarEventClickCb = function (event, jsEvent, view) {
|
||||
$scope.availability = event;
|
||||
|
||||
if ($scope.availabilityDom) {
|
||||
$scope.availabilityDom.classList.remove("fc-selected")
|
||||
}
|
||||
$scope.availabilityDom = jsEvent.target.closest('.fc-event');
|
||||
$scope.availabilityDom.classList.add("fc-selected")
|
||||
|
||||
// if the user has clicked on the delete event button, delete the event
|
||||
if ($(jsEvent.target).hasClass('remove-event')) {
|
||||
return $scope.removeSlot();
|
||||
@ -360,12 +371,23 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
||||
* Triggered when resource fetching starts/stops.
|
||||
* @see https://fullcalendar.io/docs/resource_data/loading/
|
||||
*/
|
||||
return loadingCb = function (isLoading, view) {
|
||||
const loadingCb = function (isLoading, view) {
|
||||
if (isLoading) {
|
||||
// we remove existing events when fetching starts to prevent duplicates
|
||||
return uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
|
||||
// we remove existing events when fetching starts to prevent duplicates
|
||||
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggered when the view is changed
|
||||
* @see https://fullcalendar.io/docs/v3/viewRender#v2
|
||||
*/
|
||||
const viewRenderCb = function(view, element) {
|
||||
// we unselect the current event to keep consistency
|
||||
$scope.availability = null;
|
||||
$scope.availabilityDom = null;
|
||||
};
|
||||
}
|
||||
|
||||
]);
|
||||
@ -733,3 +755,93 @@ Application.Controllers.controller('DeleteRecurrentAvailabilityController', ['$s
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Controller used in the iCalendar (ICS) imports management page
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('AdminICalendarController', ['$scope', 'iCalendars', 'ICalendar', 'growl', '_t',
|
||||
function ($scope, iCalendars, ICalendar, growl, _t) {
|
||||
// list of ICS sources
|
||||
$scope.calendars = iCalendars;
|
||||
|
||||
// configuration of a new ICS source
|
||||
$scope.newCalendar = {
|
||||
color: undefined,
|
||||
textColor: undefined,
|
||||
url: undefined,
|
||||
name: undefined,
|
||||
textHidden: 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.textColor = null;
|
||||
$scope.newCalendar.textHidden = 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.textColor,
|
||||
'width': calendar.textHidden ? '50px' : 'auto',
|
||||
'height': calendar.textHidden ? '21px' : 'auto'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given calendar from the database
|
||||
* @param calendar
|
||||
*/
|
||||
$scope.delete = function (calendar) {
|
||||
ICalendar.delete(
|
||||
{ id: calendar.id },
|
||||
function () {
|
||||
// success
|
||||
const idx = $scope.calendars.indexOf(calendar);
|
||||
$scope.calendars.splice(idx, 1);
|
||||
}, 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', 'externalsPromise',
|
||||
function ($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise, externalsPromise) {
|
||||
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,8 +38,8 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
// List of spaces
|
||||
$scope.spaces = spacesPromise.filter(t => !t.disabled);
|
||||
|
||||
// External ICS calendars
|
||||
$scope.externals = externalsPromise;
|
||||
// List of external iCalendar sources
|
||||
$scope.externals = iCalendarPromise;
|
||||
|
||||
// add availabilities source to event sources
|
||||
$scope.eventSources = [];
|
||||
@ -51,10 +51,19 @@ 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();
|
||||
$scope.externals.filter(e => e.checked).forEach(e => {
|
||||
$scope.eventSources.push({
|
||||
url: `/api/i_calendar/${e.id}/events`,
|
||||
textColor: e.textColor,
|
||||
color: e.color
|
||||
});
|
||||
});
|
||||
return uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents');
|
||||
};
|
||||
|
||||
// a variable for formation/machine/event/dispo checkbox is or not checked
|
||||
@ -62,6 +71,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
|
||||
};
|
||||
@ -88,6 +98,9 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
spaces () {
|
||||
return $scope.spaces;
|
||||
},
|
||||
externals () {
|
||||
return $scope.externals;
|
||||
},
|
||||
filter () {
|
||||
return $scope.filter;
|
||||
},
|
||||
@ -98,10 +111,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);
|
||||
@ -123,19 +137,19 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
if (event.available_type === 'machines') {
|
||||
currentMachineEvent = event;
|
||||
calendar.fullCalendar('changeView', 'agendaDay');
|
||||
return calendar.fullCalendar('gotoDate', event.start);
|
||||
calendar.fullCalendar('gotoDate', event.start);
|
||||
} else if (event.available_type === 'space') {
|
||||
calendar.fullCalendar('changeView', 'agendaDay');
|
||||
return calendar.fullCalendar('gotoDate', event.start);
|
||||
calendar.fullCalendar('gotoDate', event.start);
|
||||
} else if (event.available_type === 'event') {
|
||||
return $state.go('app.public.events_show', { id: event.event_id });
|
||||
$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 });
|
||||
$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 });
|
||||
$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 });
|
||||
$state.go('app.public.space_show', { id: event.space_id });
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -169,7 +183,7 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
};
|
||||
|
||||
const eventRenderCb = function (event, element) {
|
||||
if (event.tags.length > 0) {
|
||||
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> `;
|
||||
|
@ -638,7 +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; }],
|
||||
externalsPromise: ['Ical', function (Ical) { return Ical.get().$promise; }],
|
||||
iCalendarPromise: ['ICalendar', function (ICalendar) { return ICalendar.query().$promise; }],
|
||||
translations: ['Translations', function (Translations) { return Translations.query(['app.public.calendar']).$promise; }]
|
||||
}
|
||||
})
|
||||
@ -660,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', {
|
||||
|
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' }
|
||||
}
|
||||
}
|
||||
);
|
||||
}]);
|
@ -651,11 +651,25 @@ padding: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
.calendar-legend-block {
|
||||
text-align: right;
|
||||
padding-right: 2em;
|
||||
|
||||
.calendar-legend {
|
||||
border: 1px solid;
|
||||
border-left: 3px solid;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
padding: 2px;
|
||||
}
|
||||
h4 {
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
.legends {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.calendar-legend {
|
||||
border: 1px solid;
|
||||
border-left: 3px solid;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
padding: 2px;
|
||||
margin-left: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
@ -101,22 +101,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.display-h {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.display-v {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-self: baseline;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
|
||||
body.container{
|
||||
padding: 0;
|
||||
}
|
||||
@ -651,4 +635,4 @@ body.container{
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// medium editor placeholder
|
||||
// medium editor placeholder
|
||||
.medium-editor-placeholder {
|
||||
min-height: 30px; // fix for firefox
|
||||
}
|
||||
@ -126,6 +126,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.fc-selected {
|
||||
box-shadow: 0 6px 10px 0 rgba(0,0,0,0.14),0 1px 18px 0 rgba(0,0,0,0.12),0 3px 5px -1px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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";
|
||||
|
||||
|
19
app/assets/stylesheets/modules/icalendar.scss
Normal file
19
app/assets/stylesheets/modules/icalendar.scss
Normal file
@ -0,0 +1,19 @@
|
||||
.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;
|
||||
}
|
@ -5,22 +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 display-h" ng-class="{'p-s': !fablabWithoutSpaces}">
|
||||
<div class="display-v">
|
||||
<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-machine" translate>{{ 'admin_calendar.machines' }}</span><br>
|
||||
</div>
|
||||
<div class="display-v">
|
||||
<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-hide="fablabWithoutSpaces" translate>{{ 'admin_calendar.events' }}</span>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@ -32,6 +27,15 @@
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-9">
|
||||
<div ui-calendar="calendarConfig" ng-model="eventSources" calendar="calendar" class="wrapper-lg"></div>
|
||||
<div class="calendar-legend-block">
|
||||
<h4 translate>{{ 'admin_calendar.legend' }}</h4>
|
||||
<div class="legends">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-3">
|
||||
@ -103,7 +107,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="widget-content no-bg auto wrapper" ng-show="availability.available_type == 'event'">
|
||||
<a class="btn btn-default m-t pointer" ui-sref="app.admin.events_edit({id: availability.event_id})">
|
||||
<a class="btn btn-default pointer" ui-sref="app.admin.events_edit({id: availability.event_id})">
|
||||
<span>
|
||||
<i class="fa fa-edit"></i>
|
||||
<span class="m-l-xs" translate>{{ 'admin_calendar.edit_event' }}</span>
|
||||
@ -119,4 +123,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
|
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: 10%;"></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.textHidden ? '' : '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.textColor" class="form-control" placeholder="{{ 'icalendar.text_color' | translate}}" required/>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="hideText" class="control-label m-r" translate>{{ 'icalendar.hide_text' }}</label>
|
||||
<input bs-switch
|
||||
ng-model="newCalendar.textHidden"
|
||||
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">
|
||||
<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 class="col-md-11 col-sm-11 col-xs-11">{{::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>
|
||||
|
53
app/controllers/api/i_calendar_controller.rb
Normal file
53
app/controllers/api/i_calendar_controller.rb
Normal file
@ -0,0 +1,53 @@
|
||||
# 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
|
||||
worker = ICalendarImportWorker.new
|
||||
worker.perform([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
|
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
|
||||
|
||||
after_create :sync_events
|
||||
|
||||
private
|
||||
|
||||
def sync_events
|
||||
worker = ICalendarImportWorker.new
|
||||
worker.perform([id])
|
||||
end
|
||||
end
|
6
app/models/i_calendar_event.rb
Normal file
6
app/models/i_calendar_event.rb
Normal file
@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# iCalendar (RFC 5545) event, belonging to an ICalendar object (its source)
|
||||
class ICalendarEvent < ActiveRecord::Base
|
||||
belongs_to :i_calendar
|
||||
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
|
31
app/services/i_calendar_import_service.rb
Normal file
31
app/services/i_calendar_import_service.rb
Normal file
@ -0,0 +1,31 @@
|
||||
# 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'
|
||||
|
||||
events = []
|
||||
|
||||
i_cal = ICalendar.find(i_calendar_id)
|
||||
ics = Net::HTTP.get(URI.parse(i_cal.url))
|
||||
cals = Icalendar::Calendar.parse(ics)
|
||||
|
||||
cals.each do |cal|
|
||||
cal.events.each do |evt|
|
||||
events.push(
|
||||
uid: evt.uid,
|
||||
dtstart: evt.dtstart,
|
||||
dtend: evt.dtend,
|
||||
summary: evt.summary,
|
||||
description: evt.description,
|
||||
i_calendar_id: i_calendar_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
ICalendarEvent.create!(events)
|
||||
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
|
@ -21,6 +21,7 @@
|
||||
Fablab.withoutOnlinePayment = ('<%= Rails.application.secrets.fablab_without_online_payments %>' === 'true');
|
||||
Fablab.withoutInvoices = ('<%= Rails.application.secrets.fablab_without_invoices %>' === 'true');
|
||||
Fablab.phoneRequired = ('<%= Rails.application.secrets.phone_required %>' === 'true');
|
||||
Fablab.eventsInCalendar = ('<%= Rails.application.secrets.events_in_calendar %>' === 'true');
|
||||
Fablab.slotDuration = parseInt("<%= ApplicationHelper::SLOT_DURATION %>", 10);
|
||||
Fablab.disqusShortname = "<%= Rails.application.secrets.disqus_shortname %>";
|
||||
Fablab.defaultHost = "<%= Rails.application.secrets.default_host %>";
|
||||
|
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
|
@ -86,6 +86,7 @@ en:
|
||||
event_in_the_past: "Unable to create a slot in the past."
|
||||
edit_event: "Edit the event"
|
||||
view_reservations: "View reservations"
|
||||
legend: "legend"
|
||||
|
||||
project_elements:
|
||||
# management of the projects' components
|
||||
@ -677,6 +678,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)
|
||||
|
@ -86,6 +86,7 @@ es:
|
||||
event_in_the_past: "Unable to create a slot in the past." # translation_missing
|
||||
edit_event: "Edit the event" # translation_missing
|
||||
view_reservations: "Ver reservas" # translation_missing
|
||||
legend: "leyenda"
|
||||
|
||||
project_elements:
|
||||
# management of the projects' components
|
||||
@ -677,6 +678,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)
|
||||
|
@ -86,6 +86,26 @@ fr:
|
||||
event_in_the_past: "Impossible de créer un créneau dans le passé."
|
||||
edit_event: "Modifier l'évènement"
|
||||
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"
|
||||
|
||||
project_elements:
|
||||
# gestion des éléments constituant les projets
|
||||
@ -677,6 +697,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)
|
||||
|
@ -86,6 +86,7 @@ pt:
|
||||
event_in_the_past: "Unable to create a slot in the past." # translation_missing
|
||||
edit_event: "Edit the event" # translation_missing
|
||||
view_reservations: "Ver reservas" # translation_missing
|
||||
legend: "legenda"
|
||||
|
||||
project_elements:
|
||||
# management of the projects' components
|
||||
@ -677,6 +678,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 @@ fr:
|
||||
machines: "Machines"
|
||||
spaces: "Espaces"
|
||||
events: "Évènements"
|
||||
externals: "Autres calendriers"
|
||||
|
||||
spaces_list:
|
||||
# liste des espaces
|
||||
|
@ -112,7 +112,10 @@ Rails.application.routes.draw do
|
||||
get 'first', action: 'first', on: :collection
|
||||
end
|
||||
|
||||
get 'ical/externals' => 'ical#externals'
|
||||
resources :i_calendar, only: %i[index create destroy] do
|
||||
get 'events', on: :member
|
||||
post 'sync', on: :member
|
||||
end
|
||||
|
||||
# for admin
|
||||
resources :trainings do
|
||||
|
@ -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