1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-21 15:54:22 +01:00

Merge branch 'custom_home' into dev

This commit is contained in:
Sylvain 2020-01-22 13:26:00 +01:00
commit d0c4dd0caf
28 changed files with 751 additions and 343 deletions

View File

@ -56,6 +56,7 @@
//= require angular-base64-upload/dist/angular-base64-upload.min
//= require summernote/dist/summernote
//= require angular-summernote/dist/angular-summernote
//= require summernote-ext-nugget
//= require jquery-minicolors/jquery.minicolors.js
//= require angular-minicolors/angular-minicolors.js
//= require angular-translate/dist/angular-translate

View File

@ -12,8 +12,8 @@
*/
'use strict';
Application.Controllers.controller('SettingsController', ['$scope', '$filter', '$uibModal', 'Setting', 'growl', 'settingsPromise', 'privacyDraftsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'profileImageFile', 'CSRF', '_t',
function ($scope, $filter, $uibModal, Setting, growl, settingsPromise, privacyDraftsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, profileImageFile, CSRF, _t) {
Application.Controllers.controller('SettingsController', ['$scope', '$rootScope', '$filter', '$uibModal', 'dialogs', 'Setting', 'growl', 'settingsPromise', 'privacyDraftsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'profileImageFile', 'CSRF', '_t',
function ($scope, $rootScope, $filter, $uibModal, dialogs, Setting, growl, settingsPromise, privacyDraftsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, profileImageFile, CSRF, _t) {
/* PUBLIC SCOPE */
// timepickers steps configuration
@ -59,6 +59,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$filter', '
$scope.privacyDpoSetting = { name: 'privacy_dpo', value: settingsPromise.privacy_dpo };
$scope.aboutContactsSetting = { name: 'about_contacts', value: settingsPromise.about_contacts };
$scope.homeBlogpostSetting = { name: 'home_blogpost', value: settingsPromise.home_blogpost };
$scope.homeContent = { name: 'home_content', value: settingsPromise.home_content };
$scope.machineExplicationsAlert = { name: 'machine_explications_alert', value: settingsPromise.machine_explications_alert };
$scope.trainingExplicationsAlert = { name: 'training_explications_alert', value: settingsPromise.training_explications_alert };
$scope.trainingInformationMessage = { name: 'training_information_message', value: settingsPromise.training_information_message };
@ -135,6 +136,21 @@ Application.Controllers.controller('SettingsController', ['$scope', '$filter', '
bodyTemp: settingsPromise.privacy_body
};
// Extend the options for summernote editor, with special tools for home page
$scope.summernoteOptsHomePage = Object.assign({}, $rootScope.summernoteOpts);
$scope.summernoteOptsHomePage.toolbar[5][1].push('nugget'); // toolbar -> insert -> nugget
$scope.summernoteOptsHomePage.nugget = {
label: '\uF12E',
tooltip: _t('app.admin.settings.home_items'),
list: [
`<div id="news">${_t('app.admin.settings.item_news')}</div>`,
`<div id="projects">${_t('app.admin.settings.item_projects')}</div>`,
`<div id="twitter">${_t('app.admin.settings.item_twitter')}</div>`,
`<div id="members">${_t('app.admin.settings.item_members')}</div>`,
`<div id="events">${_t('app.admin.settings.item_events')}</div>`
]
}
/**
* For use with 'ng-class', returns the CSS class name for the uploads previews.
* The preview may show a placeholder or the content of the file depending on the upload state.
@ -293,6 +309,29 @@ Application.Controllers.controller('SettingsController', ['$scope', '$filter', '
});
}
/**
* Reset the home page to its initial state (factory value)
*/
$scope.resetHomePage = function () {
dialogs.confirm({
resolve: {
object () {
return {
title: _t('app.admin.settings.confirmation_required'),
msg: _t('app.admin.settings.confirm_reset_home_page')
};
}
}
}
, function () { // confirmed
Setting.reset({ name: 'home_content' }, function (data) {
$scope.homeContent.value = data.value;
growl.success(_t('app.admin.settings.home_content_reset'));
})
}
)
}
/* PRIVATE SCOPE */
/**

View File

@ -1,40 +1,11 @@
/* eslint-disable
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('HomeController', ['$scope', '$stateParams', 'lastMembersPromise', 'lastProjectsPromise', 'upcomingEventsPromise', 'homeBlogpostPromise', 'twitterNamePromise',
function ($scope, $stateParams, lastMembersPromise, lastProjectsPromise, upcomingEventsPromise, homeBlogpostPromise, twitterNamePromise) {
Application.Controllers.controller('HomeController', ['$scope', '$stateParams', 'homeContentPromise',
function ($scope, $stateParams, homeContentPromise) {
/* PUBLIC SCOPE */
// The last registered members who confirmed their addresses
$scope.lastMembers = lastMembersPromise;
// The last projects published/documented on the plateform
$scope.lastProjects = lastProjectsPromise;
// The closest upcoming events
$scope.upcomingEvents = upcomingEventsPromise;
// The admin blogpost
$scope.homeBlogpost = homeBlogpostPromise.setting.value;
// Twitter username
$scope.twitterName = twitterNamePromise.setting.value;
/**
* Test if the provided event run on a single day or not
* @param event {Object} single event from the $scope.upcomingEvents array
* @returns {boolean} false if the event runs on more that 1 day
*/
$scope.isOneDayEvent = event => moment(event.start_date).isSame(event.end_date, 'day');
// Home page HTML content
$scope.homeContent = null;
/* PRIVATE SCOPE */
@ -47,6 +18,46 @@ Application.Controllers.controller('HomeController', ['$scope', '$stateParams',
if ($stateParams.reset_password_token) {
return $scope.$parent.editPassword($stateParams.reset_password_token);
}
// We set the home page content, with the directives replacing the placeholders
$scope.homeContent = insertDirectives(homeContentPromise.setting.value);
};
const insertDirectives = function (html) {
const node = document.createElement('div');
node.innerHTML = html.trim();
const newsNode = node.querySelector('div#news');
if (newsNode) {
const news = document.createElement('news');
newsNode.parentNode.replaceChild(news, newsNode);
}
const projectsNode = node.querySelector('div#projects');
if (projectsNode) {
const projects = document.createElement('projects');
projectsNode.parentNode.replaceChild(projects, projectsNode);
}
const twitterNode = node.querySelector('div#twitter');
if (twitterNode) {
const twitter = document.createElement('twitter');
twitterNode.parentNode.replaceChild(twitter, twitterNode);
}
const membersNode = node.querySelector('div#members');
if (membersNode) {
const members = document.createElement('members');
membersNode.parentNode.replaceChild(members, membersNode);
}
const eventsNode = node.querySelector('div#events');
if (eventsNode) {
const events = document.createElement('events');
eventsNode.parentNode.replaceChild(events, eventsNode);
}
return node.outerHTML;
};
// !!! MUST BE CALLED AT THE END of the controller

View File

@ -0,0 +1,21 @@
Application.Directives.directive('compile', ['$compile', function ($compile) {
return function (scope, element, attrs) {
scope.$watch(
function (scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function (value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
}]);

View File

@ -0,0 +1,31 @@
Application.Directives.directive('events', [ 'Event',
function (Event) {
return ({
restrict: 'E',
templateUrl: '<%= asset_path "home/events.html" %>',
link ($scope, element, attributes) {
// The closest upcoming events
$scope.upcomingEvents = null;
/**
* Test if the provided event run on a single day or not
* @param event {Object} single event from the $scope.upcomingEvents array
* @returns {boolean} false if the event runs on more that 1 day
*/
$scope.isOneDayEvent = function(event) {
return moment(event.start_date).isSame(event.end_date, 'day');
}
// constructor
const initialize = function () {
Event.upcoming({ limit: 3 }, function (data) {
$scope.upcomingEvents = data;
})
};
// !!! MUST BE CALLED AT THE END of the directive
return initialize();
}
});
}
]);

View File

@ -0,0 +1,22 @@
Application.Directives.directive('members', [ 'Member',
function (Member) {
return ({
restrict: 'E',
templateUrl: '<%= asset_path "home/members.html" %>',
link ($scope, element, attributes) {
// The last registered members who confirmed their addresses
$scope.lastMembers = null;
// constructor
const initialize = function () {
Member.lastSubscribed({ limit: 4 }, function (data) {
$scope.lastMembers = data;
})
};
// !!! MUST BE CALLED AT THE END of the directive
return initialize();
}
});
}
]);

View File

@ -0,0 +1,22 @@
Application.Directives.directive('news', [ 'Setting',
function (Setting) {
return ({
restrict: 'E',
templateUrl: '<%= asset_path "home/news.html" %>',
link ($scope, element, attributes) {
// The admin blogpost
$scope.homeBlogpost = null;
// constructor
const initialize = function () {
Setting.get({ name: 'home_blogpost' }, function (data) {
$scope.homeBlogpost = data.setting.value;
})
};
// !!! MUST BE CALLED AT THE END of the directive
return initialize();
}
});
}
]);

View File

@ -0,0 +1,22 @@
Application.Directives.directive('projects', [ 'Project',
function (Project) {
return ({
restrict: 'E',
templateUrl: '<%= asset_path "home/projects.html" %>',
link ($scope, element, attributes) {
// The last projects published/documented on the plateform
$scope.lastProjects = null;
// constructor
const initialize = function () {
Project.lastPublished(function (data) {
$scope.lastProjects = data;
})
};
// !!! MUST BE CALLED AT THE END of the directive
return initialize();
}
});
}
]);

View File

@ -1,19 +1,25 @@
/* global twitterFetcher */
/**
* This directive will allow show latest tweet.
* Usage: <twitter profile="{{twitterName}}"/>
* This directive will show the last tweet.
* Usage: <twitter />
*/
Application.Directives.directive('twitter', [ function () {
Application.Directives.directive('twitter', ['Setting',
function (Setting) {
return ({
restrict: 'E',
scope: {
profile: '@'
},
templateUrl: '<%= asset_path "shared/_twitter.html" %>',
templateUrl: '<%= asset_path "home/twitter.html" %>',
link ($scope, element, attributes) {
var configProfile = {
'profile': { 'screenName': $scope.profile },
// Twitter username
$scope.twitterName = null;
// constructor
const initialize = function () {
Setting.get({ name: 'twitter_name' }, function (data) {
$scope.twitterName = data.setting.value;
if ($scope.twitterName) {
const configProfile = {
'profile': { 'screenName': $scope.twitterName },
'domId': 'twitter',
'maxTweets': 1,
'enableLinks': true,
@ -24,9 +30,14 @@ Application.Directives.directive('twitter', [ function () {
'showInteraction': false,
'lang': Fablab.locale
};
if ($scope.profile) {
twitterFetcher.fetch(configProfile);
}
})
};
// !!! MUST BE CALLED AT THE END of the directive
return initialize();
}
});
}]);
}
]);

View File

@ -98,11 +98,7 @@ angular.module('application.router', ['ui.router'])
}
},
resolve: {
lastMembersPromise: ['Member', function (Member) { return Member.lastSubscribed({ limit: 4 }).$promise; }],
lastProjectsPromise: ['Project', function (Project) { return Project.lastPublished().$promise; }],
upcomingEventsPromise: ['Event', function (Event) { return Event.upcoming({ limit: 3 }).$promise; }],
homeBlogpostPromise: ['Setting', function (Setting) { return Setting.get({ name: 'home_blogpost' }).$promise; }],
twitterNamePromise: ['Setting', function (Setting) { return Setting.get({ name: 'twitter_name' }).$promise; }],
homeContentPromise: ['Setting', function (Setting) { return Setting.get({ name: 'home_content' }).$promise; }]
}
})
.state('app.public.privacy', {
@ -1006,7 +1002,7 @@ angular.module('application.router', ['ui.router'])
'fablab_name', 'name_genre', 'reminder_enable', \
'reminder_delay', 'visibility_yearly', 'visibility_others', \
'display_name_enable', 'machines_sort_by', 'fab_analytics', \
'link_name']` }).$promise;
'link_name', 'home_content']` }).$promise;
}],
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],
cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }],

View File

@ -15,6 +15,11 @@ Application.Services.factory('Setting', ['$resource', function ($resource) {
},
query: {
isArray: false
},
reset: {
url: '/api/settings/reset/:name',
params: { name: '@name' },
method: 'PUT'
}
}
);

View File

@ -636,3 +636,8 @@ body.container{
left: -4px;
}
}
.home-page {
@extend .wrapper;
}

View File

@ -0,0 +1,44 @@
.admin-settings {
.home-page-settings {
.home-page-content {
.note-editor {
.note-toolbar .note-btn-group .note-btn .nugget {
font-family: "FontAwesome";
}
.note-editing-area .note-editable {
#news {
width: 100%;
background-color: #b1b1b1;
color: white;
border: 1px dashed #8f9091;
border-radius: 5px;
text-align: center;
vertical-align: middle;
line-height: 10rem;
font-size: 2em;
}
#projects {
@extend #news;
line-height: 523px;
}
#twitter {
@extend #news;
line-height: 162px;
}
#members {
@extend #news;
line-height: 320px;
}
#events {
@extend #news;
line-height: 621px;
}
}
}
}
}
}

View File

@ -1,6 +1,17 @@
<div class="panel panel-default m-t-md">
<div class="panel panel-default m-t-md home-page-settings">
<div class="panel-body">
<div class="row">
<div class="col-md-12 home-page-content">
<h4 translate>{{ 'app.admin.settings.customize_home_page' }}</h4>
<button class="btn btn-default pull-right m-t-n-xl" ng-click="resetHomePage()" title="{{ 'app.admin.settings.reset_home_page' | translate }}"><i class="fa fa-undo"></i></button>
<summernote ng-model="homeContent.value"
id="home_content"
config="summernoteOptsHomePage">
</summernote>
<button name="button" class="btn btn-warning" ng-click="save(homeContent)" translate>{{ 'app.shared.buttons.save' }}</button>
</div>
</div>
<div class="row m-t-lg">
<div class="col-md-6">
<h4 translate>{{ 'app.admin.settings.news_of_the_home_page' }}</h4>
<div ng-model="homeBlogpostSetting.value" class="well" medium-editor options='{"placeholder": "{{ "app.admin.settings.type_your_news_here" | translate }}",

View File

@ -14,7 +14,7 @@
</div>
</section>
<section class="m-lg">
<section class="m-lg admin-settings">
<div class="row">
<div class="col-md-12">

View File

@ -1,129 +1,2 @@
<div class="alert alert-warning m-sm text-center" ng-if="(homeBlogpost != null) && (homeBlogpost != '') && (homeBlogpost != undefined)">
<span ng-bind-html="homeBlogpost"></span>
</div>
<div class="row wrapper">
<div class="col-lg-8">
<h4 class="text-sm m-t-sm" translate>{{ 'app.public.home.latest_documented_projects' }}</h4>
<uib-carousel interval="5000" disable-animation="true">
<uib-slide class="h480 cover r" ng-repeat="p in lastProjects" active="p.active" style="background-image:url({{p.project_image}});">
<!-- <img ng-src="{{p.project_image}}" style="margin:auto;"> -->
<div class="carousel-caption">
<h1 class="title"><a ui-sref="app.public.projects_show({id:p.slug})">{{p.name}}</a></h1>
<!-- <p class="description hidden-xs">{{p.description | humanize : 120}}</p> -->
</div>
</uib-slide>
</uib-carousel>
</div>
<div class="col-lg-4 m-t-lg">
<twitter profile="{{twitterName}}" ng-show="twitterName"/>
<section class="widget panel b-a" >
<div class="panel-heading small b-b">
<!-- <span class="badge inverse pull-right">110</span> -->
<h2 translate>{{ 'app.public.home.latest_registered_members' }}</h2>
</div>
<div class="row m-n">
<div class="col-md-6 b-b b-r block-link" ng-repeat="member in lastMembers" ui-sref="app.logged.members_show({id:member.slug})">
<div class="padder-v">
<span class="avatar avatar-block text-center">
<fab-user-avatar ng-model="member.profile.user_avatar" avatar-class="thumb-50"></fab-user-avatar>
<!-- <i class="on b-white bottom"></i> -->
<a ><span class="user-name m-l-sm text-black m-t-xs">{{member.name}}</span></a>
</span>
</div>
</div>
<!-- TODO EVEN <div class="col-md-6 b-b"> -->
</div>
<div class="m-t-sm m-b-sm text-center" ng-if="!isAuthenticated()">
<button href="#" ng-click="signup($event)" class="btn btn-warning-full width-70 font-sbold rounded text-sm" translate>{{ 'app.public.home.create_an_account' }}</button>
</div>
<div class="m-t-sm m-b-sm text-center" ng-if="isAuthenticated()">
<button href="#" ui-sref="app.logged.members" class="btn btn-warning-full width-70 font-sbold rounded text-sm" translate>{{ 'app.public.home.discover_members' }}</button>
</div>
</section>
</div>
<section class="home-events col-lg-12 wrapper">
<h4 class="text-sm m-t-sm">{{ 'app.public.home.fablab_s_next_events' | translate }} <a ui-sref="app.public.events_list" class="pull-right"><i class="fa fa-tags"></i> {{ 'app.public.home.every_events' | translate }}</a></h4>
<div class="row" ng-repeat="event in (upcomingEvents.length/3 | array)">
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-4" ng-repeat="event in upcomingEvents.slice(3*$index, 3*$index + 3)">
<div class="widget panel panel-default" ui-sref="app.public.events_show({id: event.id})">
<div class="panel-heading picture" style="background-image: url({{event.event_image_medium}});" >
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon" bs-holder ng-if="!event.event_image" class="img-responsive">
</div>
<div class="panel-body" style="heigth:170px;">
<div class="row">
<div class="col-xs-9">
<h1 class="m-b">{{event.title}}</h1>
</div>
<div class="col-xs-3">
<span class="v-middle badge text-xs" ng-class="'bg-{{event.category.slug}}'">{{event.category.name}}</span>
</div>
</div>
<p class="event-description" ng-bind-html="event.description | simpleText | humanize : 500 | breakFilter"></p>
<hr/>
<div class="row">
<div class="col-sm-6 row m-b-sm">
<i class="fa fa-calendar red col-xs-3 padder-icon"></i>
<h6 class="m-n col-xs-9 " ng-hide="isOneDayEvent(event)">{{ 'app.public.home.from_date_to_date' | translate:{START:(event.start_date | amDateFormat:'L'), END:(event.end_date | amDateFormat:'L')} }}</h6>
<h6 class="m-n col-xs-9 " ng-show="isOneDayEvent(event)">{{ 'app.public.home.on_the_date' | translate:{DATE:(event.start_date | amDateFormat:'L')} }}</h6>
</div>
<div class="col-sm-6 row m-b-sm">
<i class="fa fa-clock-o red col-xs-3 padder-icon"></i>
<h6 class="m-n col-xs-9">
<span ng-if="event.all_day == 'true'" translate>{{ 'app.public.home.all_day' }}</span>
<span ng-if="event.all_day == 'false'">{{ 'app.public.home.from_time_to_time' | translate:{START:(event.start_date | amDateFormat:'LT'), END:(event.end_date | amDateFormat:'LT')} }}</span>
</h6>
</div>
</div>
<div class="row">
<div class="col-sm-6 row m-b-sm">
<i class="fa fa-user red col-xs-3 padder-icon"></i>
<h6 class="m-n col-xs-9 ">
<span ng-if="event.nb_free_places > 0">{{ 'app.public.home.still_available' | translate }} {{event.nb_free_places}}</span>
<span ng-if="!event.nb_total_places" translate>{{ 'app.public.home.free_entry' }}</span>
<span ng-if="event.nb_total_places > 0 && event.nb_free_places <= 0" translate>{{ 'app.public.home.event_full' }}</span>
</h6>
</div>
<div class="col-sm-6 row m-b-sm">
<i class="fa fa-bookmark red col-xs-3 padder-icon"></i>
<h6 class="m-n col-xs-9">
<span ng-if="event.amount == 0" translate>{{ 'app.public.home.free_admission' }}</span>
<span ng-if="event.amount > 0">{{ 'app.public.home.full_price' | translate }} {{event.amount | currency}}</span>
</h6>
</div>
</div>
<div class="text-center clearfix ">
<div class="btn btn-lg btn-warning bg-white b-2x rounded m-t-sm m-b-sm upper text-sm width-70" ui-sref="app.public.events_show({id: event.id})" ><span translate>{{ 'app.shared.buttons.consult' }}</span></div>
</div>
</div>
</div>
</div>
</div>
</section>
<div class="home-page" compile="homeContent">
</div>

View File

@ -0,0 +1,65 @@
<section class="home-events">
<h4 class="text-sm m-t-sm">{{ 'app.public.home.fablab_s_next_events' | translate }} <a ui-sref="app.public.events_list" class="pull-right"><i class="fa fa-tags"></i> {{ 'app.public.home.every_events' | translate }}</a></h4>
<div class="row" ng-repeat="event in (upcomingEvents.length/3 | array)">
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-4" ng-repeat="event in upcomingEvents.slice(3*$index, 3*$index + 3)">
<div class="widget panel panel-default" ui-sref="app.public.events_show({id: event.id})">
<div class="panel-heading picture" style="background-image: url({{event.event_image_medium}});" >
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon" bs-holder ng-if="!event.event_image" class="img-responsive">
</div>
<div class="panel-body" style="heigth:170px;">
<div class="row">
<div class="col-xs-9">
<h1 class="m-b">{{event.title}}</h1>
</div>
<div class="col-xs-3">
<span class="v-middle badge text-xs" ng-class="'bg-{{event.category.slug}}'">{{event.category.name}}</span>
</div>
</div>
<p class="event-description" ng-bind-html="event.description | simpleText | humanize : 500 | breakFilter"></p>
<hr/>
<div class="row">
<div class="col-sm-6 row m-b-sm">
<i class="fa fa-calendar red col-xs-3 padder-icon"></i>
<h6 class="m-n col-xs-9 " ng-hide="isOneDayEvent(event)">{{ 'app.public.home.from_date_to_date' | translate:{START:(event.start_date | amDateFormat:'L'), END:(event.end_date | amDateFormat:'L')} }}</h6>
<h6 class="m-n col-xs-9 " ng-show="isOneDayEvent(event)">{{ 'app.public.home.on_the_date' | translate:{DATE:(event.start_date | amDateFormat:'L')} }}</h6>
</div>
<div class="col-sm-6 row m-b-sm">
<i class="fa fa-clock-o red col-xs-3 padder-icon"></i>
<h6 class="m-n col-xs-9">
<span ng-if="event.all_day == 'true'" translate>{{ 'app.public.home.all_day' }}</span>
<span ng-if="event.all_day == 'false'">{{ 'app.public.home.from_time_to_time' | translate:{START:(event.start_date | amDateFormat:'LT'), END:(event.end_date | amDateFormat:'LT')} }}</span>
</h6>
</div>
</div>
<div class="row">
<div class="col-sm-6 row m-b-sm">
<i class="fa fa-user red col-xs-3 padder-icon"></i>
<h6 class="m-n col-xs-9 ">
<span ng-if="event.nb_free_places > 0">{{ 'app.public.home.still_available' | translate }} {{event.nb_free_places}}</span>
<span ng-if="!event.nb_total_places" translate>{{ 'app.public.home.free_entry' }}</span>
<span ng-if="event.nb_total_places > 0 && event.nb_free_places <= 0" translate>{{ 'app.public.home.event_full' }}</span>
</h6>
</div>
<div class="col-sm-6 row m-b-sm">
<i class="fa fa-bookmark red col-xs-3 padder-icon"></i>
<h6 class="m-n col-xs-9">
<span ng-if="event.amount == 0" translate>{{ 'app.public.home.free_admission' }}</span>
<span ng-if="event.amount > 0">{{ 'app.public.home.full_price' | translate }} {{event.amount | currency}}</span>
</h6>
</div>
</div>
<div class="text-center clearfix ">
<div class="btn btn-lg btn-warning bg-white b-2x rounded m-t-sm m-b-sm upper text-sm width-70" ui-sref="app.public.events_show({id: event.id})" ><span translate>{{ 'app.shared.buttons.consult' }}</span></div>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,27 @@
<section class="widget panel b-a" >
<div class="panel-heading small b-b">
<h2 translate>{{ 'app.public.home.latest_registered_members' }}</h2>
</div>
<div class="row m-n">
<div class="col-md-6 b-b b-r block-link" ng-repeat="member in lastMembers" ui-sref="app.logged.members_show({id:member.slug})">
<div class="padder-v">
<span class="avatar avatar-block text-center">
<fab-user-avatar ng-model="member.profile.user_avatar" avatar-class="thumb-50"></fab-user-avatar>
<!-- <i class="on b-white bottom"></i> -->
<a ><span class="user-name m-l-sm text-black m-t-xs">{{member.name}}</span></a>
</span>
</div>
</div>
</div>
<div class="m-t-sm m-b-sm text-center" ng-if="!isAuthenticated()">
<button href="#" ng-click="signup($event)" class="btn btn-warning-full width-70 font-sbold rounded text-sm" translate>{{ 'app.public.home.create_an_account' }}</button>
</div>
<div class="m-t-sm m-b-sm text-center" ng-if="isAuthenticated()">
<button href="#" ui-sref="app.logged.members" class="btn btn-warning-full width-70 font-sbold rounded text-sm" translate>{{ 'app.public.home.discover_members' }}</button>
</div>
</section>

View File

@ -0,0 +1,3 @@
<div class="alert alert-warning text-center" ng-if="(homeBlogpost != null) && (homeBlogpost != '') && (homeBlogpost != undefined)">
<span ng-bind-html="homeBlogpost"></span>
</div>

View File

@ -0,0 +1,11 @@
<div>
<h4 class="text-sm m-t-sm" translate>{{ 'app.public.home.latest_documented_projects' }}</h4>
<uib-carousel interval="5000" disable-animation="true">
<uib-slide class="h480 cover r" ng-repeat="p in lastProjects" active="p.active" style="background-image:url({{p.project_image}});">
<div class="carousel-caption">
<h1 class="title"><a ui-sref="app.public.projects_show({id:p.slug})">{{p.name}}</a></h1>
</div>
</uib-slide>
</uib-carousel>
</div>

View File

@ -1,7 +1,7 @@
<section class="widget panel b-a m-t-sm">
<section class="widget panel b-a m-t-sm" ng-show="twitterName">
<div class="panel-heading b-b small">
<div class="pull-right text-xs align">
<a href="https://twitter.com/{{ profile }}" target="_blank">{{ 'app.public.home.follow_us' | translate }}
<a href="https://twitter.com/{{ twitterName }}" target="_blank">{{ 'app.public.home.follow_us' | translate }}
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x text-yellow"></i>
<i class="fa fa-twitter fa-stack-1x fa-inverse text-white"></i>

View File

@ -2,7 +2,7 @@
# API Controller for resources of type Setting
class API::SettingsController < API::ApiController
before_action :authenticate_user!, only: :update
before_action :authenticate_user!, only: %i[update bulk_update reset]
def index
@settings = Setting.where(name: names_as_string_to_array)
@ -36,6 +36,19 @@ class API::SettingsController < API::ApiController
@show_history = params[:history] == 'true' && current_user.admin?
end
def reset
authorize Setting
setting = Setting.find_or_create_by(name: params[:name])
first_val = setting.history_values.order(created_at: :asc).limit(1).first
new_val = HistoryValue.create!(
setting_id: setting.id,
value: first_val.value,
invoicing_profile_id: current_user.invoicing_profile.id
)
render json: new_val, status: :ok
end
private
def setting_params

View File

@ -1,3 +1,8 @@
# frozen_string_literal: true
# Setting is a configuration element of the platform. Only administrators are allowed to modify Settings
# For some settings, changing them will involve some callback actions (like rebuilding the stylesheets if the theme color Setting is changed).
# A full history of the previous values is kept in database with the date and the author of the change
class Setting < ActiveRecord::Base
has_many :history_values
validates :name, inclusion:
@ -65,7 +70,8 @@ class Setting < ActiveRecord::Base
hub_last_version
hub_public_key
fab_analytics
link_name] }
link_name
home_content] }
after_update :update_stylesheet, :notify_privacy_policy_changed if :value_changed?

View File

@ -2,7 +2,7 @@
# Check the access policies for API::SettingsController
class SettingPolicy < ApplicationPolicy
%w[update bulk_update].each do |action|
%w[update bulk_update reset].each do |action|
define_method "#{action}?" do
user.admin?
end

View File

@ -928,6 +928,18 @@ fr:
space_explications_alert: "l'explication sur la page de réservation d'un espace"
main_color: "la couleur principale"
secondary_color: "la couleur secondaire"
customize_home_page: "Personnaliser la page d'accueil"
reset_home_page: "Remettre la page d'accueil dans son état initial"
confirmation_required: "Confirmation requise"
confirm_reset_home_page: "Voulez-vous vraiment remettre la page d'accueil à sa valeur d'usine ?"
home_items: "Éléments de la page d'accueil"
item_news: "Brève"
item_projects: "Derniers projets"
item_twitter: "Dernier tweet"
item_members: "Derniers membres"
item_events: "Prochains événements"
home_content: "la page d'accueil"
home_content_reset: "La page d'accueil a bien été restaurée dans sa configuration initiale."
home_blogpost: "la brève de la page d'accueil"
twitter_name: "nom du flux Twitter"
link_name: "l'intitulé du lien vers la page \"À propos\""

View File

@ -47,6 +47,7 @@ Rails.application.routes.draw do
resources :admins, only: %i[index create destroy]
resources :settings, only: %i[show update index], param: :name do
patch '/bulk_update', action: 'bulk_update', on: :collection
put '/reset/:name', action: 'reset', on: :collection
end
resources :users, only: %i[index create]
resources :members, only: %i[index show create update destroy] do

View File

@ -214,7 +214,7 @@ if Machine.count.zero?
"\r\nVitesse d'analyse (scannage): 4-15 mm/sec\r\n \r\n \r\nLogiciel utilisé pour le fraisage: Roland Modela player" \
" 4 \r\nLogiciel utilisé pour l'usinage de circuits imprimés: Cad.py (linux)\r\nFormats acceptés: STL,PNG 3D\r\n" \
"Format d'exportation des données scannées: DXF, VRML, STL, 3DMF, IGES, Grayscale, Point Group et BMP\r\n",
slug: 'petite-fraiseuse' },
slug: 'petite-fraiseuse' }
])
Price.all.each do |p|
@ -294,13 +294,18 @@ end
unless Setting.find_by(name: 'subscription_explications_alert').try(:value)
setting = Setting.find_or_initialize_by(name: 'subscription_explications_alert')
setting.value = '<p><b>Règle sur la date de début des abonnements</b><br></p><ul><li>' \
' <span style=\"font-size: 1.6rem; line-height: 2.4rem;\">Si vous êtes un nouvel utilisateur - i.e aucune ' \
" formation d'enregistrée sur le site - votre abonnement débutera à la date de réservation de votre première " \
' formation.</span><br></li><li><span style=\"font-size: 1.6rem; line-height: 2.4rem;\">Si vous avez déjà une ' \
" formation ou plus de validée, votre abonnement débutera à la date de votre achat d'abonnement.</span></li>" \
" </ul><p>Merci de bien prendre ses informations en compte, et merci de votre compréhension. L'équipe du Fab Lab.<br>" \
' </p><p></p>'
setting.value = <<~HTML
<p><b>Règle sur la date de début des abonnements</b></p>
<ul>
<li><span style=\"font-size: 1.6rem; line-height: 2.4rem;\">Si vous êtes un nouvel utilisateur - i.e aucune
formation d'enregistrée sur le site - votre abonnement débutera à la date de réservation de votre première
formation.</span></li>
<li><span style="font-size: 1.6rem; line-height: 2.4rem;">Si vous avez déjà une formation ou plus de validée,
votre abonnement débutera à la date de votre achat d'abonnement.</span></li>
</ul>
<p>Merci de bien prendre ses informations en compte, et merci de votre compréhension. L'équipe du Fab Lab.<br>
</p>
HTML
setting.save
end
@ -495,7 +500,8 @@ end
unless Setting.find_by(name: 'privacy_draft').try(:value)
setting = Setting.find_or_initialize_by(name: 'privacy_draft')
setting.value = "<p>La présente politique de confidentialité définit et vous informe de la manière dont _________ utilise et protège les
setting.value = <<~HTML
<p>La présente politique de confidentialité définit et vous informe de la manière dont _________ utilise et protège les
informations que vous nous transmettez, le cas échéant, lorsque vous utilisez le présent site accessible à partir de lURL suivante :
_________ (ci-après le « Site »).</p><p>Veuillez noter que cette politique de confidentialité est susceptible dêtre modifiée ou
complétée à tout moment par _________, notamment en vue de se conformer à toute évolution législative, réglementaire, jurisprudentielle
@ -534,7 +540,7 @@ unless Setting.find_by(name: 'privacy_draft').try(:value)
sera en droit, le cas échéant, de sopposer aux demandes manifestement abusives (de par leur nombre, leur caractère répétitif ou
systématique).</p><p>Pour vous aider dans votre démarche, notamment si vous désirez exercer votre droit daccès par le biais dune
demande écrite à ladresse postale mentionnée au point 1, vous trouverez en cliquant sur le <a
href=\"https://www.cnil.fr/fr/modele/courrier/exercer-son-droit-dacces\">lien</a> suivant un modèle de courrier élaboré par la Commission
href="https://www.cnil.fr/fr/modele/courrier/exercer-son-droit-dacces">lien</a> suivant un modèle de courrier élaboré par la Commission
Nationale de lInformatique et des Libertés (la « CNIL »).</p><p><b>o Droit de rectification des données</b></p><p>Au titre de ce droit,
la législation vous habilite à demander la rectification, la mise à jour, le verrouillage ou encore leffacement des données vous
concernant qui peuvent savérer le cas échéant inexactes, erronées, incomplètes ou obsolètes.</p><p>Egalement, vous pouvez définir des
@ -542,13 +548,13 @@ unless Setting.find_by(name: 'privacy_draft').try(:value)
dune personne décédée peuvent exiger de prendre en considération le décès de leur proche et/ou de procéder aux mises à jour nécessaires.
</p><p>Pour vous aider dans votre démarche, notamment si vous désirez exercer, pour votre propre compte ou pour le compte de lun de vos
proches défunt, votre droit de rectification par le biais dune demande écrite à ladresse postale mentionnée au point 1, vous trouverez
en cliquant sur le <a href=\"https://www.cnil.fr/fr/modele/courrier/rectifier-des-donnees-inexactes-obsoletes-ou-perimees\">lien</a>
en cliquant sur le <a href="https://www.cnil.fr/fr/modele/courrier/rectifier-des-donnees-inexactes-obsoletes-ou-perimees">lien</a>
suivant un modèle de courrier élaboré par la CNIL.</p><p><b>o Droit dopposition</b></p><p>Lexercice de ce droit nest possible que dans
lune des deux situations suivantes :</p><p>Lorsque lexercice de ce droit est fondé sur des motifs légitimes ; ou</p><p>Lorsque
lexercice de ce droit vise à faire obstacle à ce que les données recueillies soient utilisées à des fins de prospection commerciale.</p>
<p>Pour vous aider dans votre démarche, notamment si vous désirez exercer votre droit dopposition par le biais dune demande écrite
adressée à ladresse postale indiquée au point 1, vous trouverez en cliquant sur le <a
href=\"https://www.cnil.fr/fr/modele/courrier/supprimer-des-informations-vous-concernant-dun-site-internet\">lien</a> suivant un modèle de
href="https://www.cnil.fr/fr/modele/courrier/supprimer-des-informations-vous-concernant-dun-site-internet">lien</a> suivant un modèle de
courrier élaboré par la CNIL.</p><h4>6. Délais de réponse</h4><p> _________ sengage à répondre à votre demande daccès, de rectification
ou dopposition ou toute autre demande complémentaire dinformations dans un délai raisonnable qui ne saurait dépasser 1 mois à compter
de la réception de votre demande.</p><h4>7. Prestataires habilités et transfert vers un pays tiers de lUnion Européenne</h4><p>_________
@ -561,7 +567,7 @@ unless Setting.find_by(name: 'privacy_draft').try(:value)
en (année d'approbation)&nbsp;_________.</p><h4>8. Plainte auprès de lautorité compétente</h4><p>Si vous considérez que _________ ne
respecte pas ses obligations au regard de vos Informations Personnelles, vous pouvez adresser une plainte ou une demande auprès de
lautorité compétente. En France, lautorité compétente est la CNIL à laquelle vous pouvez adresser une demande par voie électronique en
cliquant sur le lien suivant : <a href=\"https://www.cnil.fr/fr/plaintes/internet\">https://www.cnil.fr/fr/plaintes/internet</a>.</p>
cliquant sur le lien suivant : <a href="https://www.cnil.fr/fr/plaintes/internet">https://www.cnil.fr/fr/plaintes/internet</a>.</p>
<h3>II. POLITIQUE RELATIVE AUX COOKIES</h3><p>Lors de votre première connexion sur le site web de _________, vous êtes avertis par un
bandeau en bas de votre écran que des informations relatives à votre navigation sont susceptibles dêtre enregistrées dans des fichiers
dénommés « cookies ». Notre politique dutilisation des cookies vous permet de mieux comprendre les dispositions que nous mettons en œuvre
@ -590,8 +596,8 @@ unless Setting.find_by(name: 'privacy_draft').try(:value)
par voie électronique. Il sagit des cookies suivants :</p><p><b>o Identifiant de session</b> et&nbsp;<b>authentification</b> sur l'API.
Ces cookies sont intégralement soumis à la présente politique dans la mesure ils sont émis et gérés par _________.</p><p>
<b>o Stripe</b>, permettant de gérer les paiements par carte bancaire et dont la politique de confidentialité est accessible sur ce
<a href=\"https://stripe.com/fr/privacy\">lien</a>.</p><p><b>o Disqus</b>, permettant de poster des commentaires sur les fiches projet et
dont la politique de confidentialité est accessible sur ce <a href=\"https://help.disqus.com/articles/1717103-disqus-privacy-policy\">lien
<a href="https://stripe.com/fr/privacy">lien</a>.</p><p><b>o Disqus</b>, permettant de poster des commentaires sur les fiches projet et
dont la politique de confidentialité est accessible sur ce <a href="https://help.disqus.com/articles/1717103-disqus-privacy-policy">lien
</a>.</p><h4>b. Les cookies nécessitant le recueil préalable de votre consentement</h4><p>Cette
exigence concerne les cookies émis par des tiers et qui sont qualifiés de « persistants » dans la mesure ils demeurent dans votre
terminal jusquà leur effacement ou leur date dexpiration.</p><p>De tels cookies étant émis par des tiers, leur utilisation et leur dépôt
@ -600,7 +606,7 @@ unless Setting.find_by(name: 'privacy_draft').try(:value)
fréquentation et lutilisation de divers éléments du site web (comme les contenus/pages que vous avez visité).
Ces données participent à lamélioration de lergonomie du site web de _________. Un outil de mesure daudience est utilisé sur le
présent site internet :</p><p><b>o Google Analytics</b> pour gérer les statistiques de visites dont la politique de
confidentialité est disponible (uniquement en anglais) à partir du <a href=\"https://policies.google.com/privacy?hl=fr&amp;gl=ZZ\">lien
confidentialité est disponible (uniquement en anglais) à partir du <a href="https://policies.google.com/privacy?hl=fr&amp;gl=ZZ">lien
</a> suivant. </p><h4>c. Vous disposez de divers outils de paramétrage des cookies</h4><p>La plupart
des navigateurs Internet sont configurés par défaut de façon à ce que le dépôt de cookies soit autorisé. Votre navigateur vous offre
lopportunité de modifier ces paramètres standards de manière à ce que lensemble des cookies soit rejeté systématiquement ou bien à ce
@ -618,15 +624,16 @@ unless Setting.find_by(name: 'privacy_draft').try(:value)
cookies. Pour savoir de quelle manière modifier vos préférences en matière de cookies, vous trouverez ci-dessous les liens vers laide
nécessaire pour accéder au menu de votre navigateur prévu à cet effet :</p>
<ul>
<li><a href=\"https://support.google.com/chrome/answer/95647?hl=fr\">Chrome</a></li>
<li><a href=\"https://support.mozilla.org/fr/kb/activer-desactiver-cookies\">Firefox</a></li>
<li><a href=\"https://support.microsoft.com/fr-fr/help/17442/windows-internet-explorer-delete-manage-cookies#ie=ie-11\">Internet
<li><a href="https://support.google.com/chrome/answer/95647?hl=fr">Chrome</a></li>
<li><a href="https://support.mozilla.org/fr/kb/activer-desactiver-cookies">Firefox</a></li>
<li><a href="https://support.microsoft.com/fr-fr/help/17442/windows-internet-explorer-delete-manage-cookies#ie=ie-11">Internet
Explorer</a></li>
<li><a href=\"http://help.opera.com/Windows/10.20/fr/cookies.html\">Opera</a></li>
<li><a href=\"https://support.apple.com/kb/PH21411?viewlocale=fr_FR&amp;locale=fr_FR\">Safari</a></li>
<li><a href="http://help.opera.com/Windows/10.20/fr/cookies.html">Opera</a></li>
<li><a href="https://support.apple.com/kb/PH21411?viewlocale=fr_FR&amp;locale=fr_FR">Safari</a></li>
</ul>
<p>Pour de plus amples informations concernant les outils de maîtrise des cookies, vous pouvez consulter le
<a href=\"https://www.cnil.fr/fr/cookies-les-outils-pour-les-maitriser\">site internet</a> de la CNIL.</p>"
<a href="https://www.cnil.fr/fr/cookies-les-outils-pour-les-maitriser">site internet</a> de la CNIL.</p>
HTML
setting.save
end
@ -644,6 +651,30 @@ unless Setting.find_by(name: 'link_name').try(:value)
setting.save
end
unless Setting.find_by(name: 'home_content').try(:value)
setting = Setting.find_or_initialize_by(name: 'home_content')
setting.value = <<~HTML
<div class="m-sm">
<div id="news">#{I18n.t('app.admin.settings.item_news')}</div>
</div>
<div class="row wrapper">
<div class="col-lg-8">
<div id="projects">#{I18n.t('app.admin.settings.item_projects')}</div>
</div>
<div class="col-lg-4 m-t-lg">
<div id="twitter">#{I18n.t('app.admin.settings.item_twitter')}</div>
<div id="members">#{I18n.t('app.admin.settings.item_members')}</div>
</div>
</div>
<div class="row wrapper m-t-sm">
<div class="col-lg-12">
<div id="events">#{I18n.t('app.admin.settings.item_events')}</div>
</div>
</div>
HTML
setting.save
end
if StatisticCustomAggregation.count.zero?
# available reservations hours for machines
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2)

View File

@ -0,0 +1,125 @@
// Inspired by: https://github.com/pHAlkaline/summernote-plugins/tree/master/plugins/nugget
(function (factory) {
/* global define */
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node/CommonJS
module.exports = factory(require('jquery'));
} else {
// Browser globals
factory(window.jQuery);
}
}(function ($) {
$.extend($.summernote.options, {
nugget: {
list: []
}
});
$.extend(true, $.summernote, {
// add localization texts
lang: {
'en-US': {
nugget: {
Nugget: 'Nugget',
Insert_nugget: 'Insert Nugget'
}
},
'en-GB': {
nugget: {
Nugget: 'Nugget',
Insert_nugget: 'Insert Nugget'
}
},
'pt-PT': {
nugget: {
Nugget: 'Pepita',
Insert_nugget: 'Inserir pepita'
}
},
'it-IT': {
nugget: {
Nugget: 'Pepite',
Insert_nugget: 'Pepite Inserto'
}
}
}
});
// Extends plugins for adding nuggets.
// - plugin is external module for customizing.
$.extend($.summernote.plugins, {
/**
* @param {Object} context - context object has status of editor.
*/
'nugget': function (context) {
// ui has renders to build ui elements.
// - you can create a button with `ui.button`
const ui = $.summernote.ui;
const options = context.options.nugget;
const context_options = context.options;
const lang = context_options.langInfo;
const defaultOptions = {
label: lang.nugget.Nugget,
tooltip: lang.nugget.Insert_nugget
};
// Assign default values if not supplied
for (const propertyName in defaultOptions) {
if (options.hasOwnProperty(propertyName) === false) {
options[propertyName] = defaultOptions[propertyName];
}
}
// add hello button
context.memo('button.nugget', function () {
// create button
const button = ui.buttonGroup([
ui.button({
className: 'dropdown-toggle',
contents: '<span class="nugget">' + options.label + ' </span><span class="note-icon-caret"></span>',
tooltip: options.tooltip,
data: {
toggle: 'dropdown'
}
}),
ui.dropdown({
className: 'dropdown-nugget',
contents: options.list.map((i) => {
const li = document.createElement('li');
const a = document.createElement('a');
a.innerHTML = i.trim();
a.setAttribute('href', '#');
li.appendChild(a);
return li.outerHTML;
}),
click: function (event) {
event.preventDefault();
const $button = $(event.target);
const value = $button[0].outerHTML;
const node = document.createElement('div');
node.innerHTML = value.trim();
context.invoke('editor.insertNode', node.firstChild);
}
})
]);
// create jQuery object from button instance.
return button.render();
});
}
});
}));