1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-20 09:52:19 +01:00

Merge branch 'dev' into payzen

This commit is contained in:
Sylvain 2021-03-24 14:34:24 +01:00
commit 5b854ea831
43 changed files with 439 additions and 252 deletions

View File

@ -3,16 +3,28 @@
## Next release
- [TODO DEPLOY] `rails fablab:stripe:set_gateway`
## Next release (v4.7.6)
## v4.7.6 2021 March 24
- Ability to disable the trainings module
- Ability to set the address as a mandatory field
- The address is new requested when creating an account
- The profile completion page is less fuzzy for people landing on it without enabled SSO
- Prevent showing error message when testing for old versions during upgrade
- In the email notification, sent to admins on account creation, show the group of the user
- More explanations in the setup script
- Send pre-compressed assets to the browsers instead of the regular ones
- Links created using "medium editor" opens in new tabs
- Improved style of public plans page
- Improved the upgrade script
- Fix a bug: subscriptions tab is selected by default in statistics, even if the module is disabled
- Fix a bug: select all plans for slot restriction (through the dedicated button) also selects the disabled plans
- Fix a bug: recurring availabilities are not restricted to subscribers
- Fix a bug: accounting exports may ignore some invoices for the first and last days
- Fix a bug: accounting export caching is not working
- Fix a bug: unable to run the setup script if sudoers belong to another group than sudo
- Fix a security issue: updated elliptic to 6.5.4 to fix [CVE-2020-28498](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28498)
- [TODO DEPLOY] `\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/scripts/nginx-packs-directive.sh | bash`
- [TODO DEPLOY] `rails db:seed`
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`
## v4.7.5 2021 March 08
- Fix a bug: unable to compile the assets during the upgrade, if the env file has some whitespaces around the equal sign

View File

@ -8,7 +8,7 @@ class API::AccountingExportsController < API::ApiController
def export
authorize :accounting_export
export = Export.where(category: 'accounting', export_type: 'accounting-software', key: params[:key])
export = Export.where(category: 'accounting', export_type: params[:type], key: params[:key])
.where(extension: params[:extension], query: params[:query])
.where('created_at > ?', Invoice.maximum('updated_at'))
.last

View File

@ -49,6 +49,7 @@ class API::AuthProvidersController < API::ApiController
def active
authorize AuthProvider
@provider = AuthProvider.active
@previous = AuthProvider.previous
end

View File

@ -41,7 +41,8 @@ class ApplicationController < ActionController::Base
{
profile_attributes: %i[phone last_name first_name interest software_mastered],
invoicing_profile_attributes: [
organization_attributes: [:name, address_attributes: [:address]]
organization_attributes: [:name, address_attributes: [:address]],
address_attributes: [:address]
],
statistic_profile_attributes: %i[gender birthday]
},

View File

@ -665,7 +665,7 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
$scope.selectedPlans = [];
$scope.selectedPlansBinding = {};
if (count === 0) {
plansPromise.forEach(function (plan) {
plansPromise.filter(p => !p.disabled).forEach(function (plan) {
$scope.selectedPlans.push(plan);
$scope.selectedPlansBinding[plan.id] = true;
});

View File

@ -1334,8 +1334,8 @@ Application.Controllers.controller('AccountingExportModalController', ['$scope',
columns: $scope.exportTarget.settings.columns,
encoding: $scope.exportTarget.settings.encoding,
date_format: $scope.exportTarget.settings.dateFormat,
start_date: $scope.exportTarget.startDate,
end_date: $scope.exportTarget.endDate,
start_date: moment.utc($scope.exportTarget.startDate).startOf('day').toISOString(),
end_date: moment.utc($scope.exportTarget.endDate).endOf('day').toISOString(),
label_max_length: $scope.exportTarget.settings.labelMaxLength,
decimal_separator: $scope.exportTarget.settings.decimalSeparator,
export_invoices_at_zero: $scope.exportTarget.settings.exportInvoicesAtZero

View File

@ -650,8 +650,8 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
/**
* Controller used in the member edition page
*/
Application.Controllers.controller('EditMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t', 'walletPromise', 'transactionsPromise', 'activeProviderPromise', 'Wallet', 'phoneRequiredPromise',
function ($scope, $state, $stateParams, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t, walletPromise, transactionsPromise, activeProviderPromise, Wallet, phoneRequiredPromise) {
Application.Controllers.controller('EditMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t', 'walletPromise', 'transactionsPromise', 'activeProviderPromise', 'Wallet', 'settingsPromise',
function ($scope, $state, $stateParams, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t, walletPromise, transactionsPromise, activeProviderPromise, Wallet, settingsPromise) {
/* PUBLIC SCOPE */
// API URL where the form will be posted
@ -670,7 +670,10 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
$scope.password = { change: false };
// is the phone number required in _member_form?
$scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true');
$scope.phoneRequired = (settingsPromise.phone_required === 'true');
// is the address required in _member_form?
$scope.addressRequired = (settingsPromise.address_required === 'true');
// the user subscription
if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) {
@ -990,8 +993,8 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
/**
* Controller used in the member's creation page (admin view)
*/
Application.Controllers.controller('NewMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'Group', 'CSRF', 'phoneRequiredPromise',
function ($scope, $state, $stateParams, Member, Training, Group, CSRF, phoneRequiredPromise) {
Application.Controllers.controller('NewMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'Group', 'CSRF', 'settingsPromise',
function ($scope, $state, $stateParams, Member, Training, Group, CSRF, settingsPromise) {
CSRF.setMetaTags();
/* PUBLIC SCOPE */
@ -1006,7 +1009,10 @@ Application.Controllers.controller('NewMemberController', ['$scope', '$state', '
$scope.password = { change: false };
// is the phone number required in _member_form?
$scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true');
$scope.phoneRequired = (settingsPromise.phone_required === 'true');
// is the address required to sign-up?
$scope.addressRequired = (settingsPromise.address_required === 'true');
// Default member's profile parameters
$scope.user = {
@ -1109,8 +1115,8 @@ Application.Controllers.controller('ImportMembersResultController', ['$scope', '
/**
* Controller used in the admin creation page (admin view)
*/
Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'Admin', 'growl', '_t', 'phoneRequiredPromise',
function ($state, $scope, Admin, growl, _t, phoneRequiredPromise) {
Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'Admin', 'growl', '_t', 'settingsPromise',
function ($state, $scope, Admin, growl, _t, settingsPromise) {
// default admin profile
let getGender;
$scope.admin = {
@ -1131,7 +1137,10 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A
};
// is the phone number required in _admin_form?
$scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true');
$scope.phoneRequired = (settingsPromise.phone_required === 'true');
// is the address required in _admin_form?
$scope.addressRequired = (settingsPromise.address_required === 'true');
/**
* Shows the birthday datepicker

View File

@ -92,7 +92,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
templateUrl: '/shared/signupModal.html',
size: 'md',
resolve: {
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'recaptcha_site_key', 'confirmation_required']" }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'recaptcha_site_key', 'confirmation_required', 'address_required']" }).$promise; }]
},
controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', 'settingsPromise', 'growl', '_t', function ($scope, $uibModalInstance, Group, CustomAsset, settingsPromise, growl, _t) {
// default parameters for the date picker in the account creation modal
@ -107,6 +107,9 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
// is the phone number required to sign-up?
$scope.phoneRequired = (settingsPromise.phone_required === 'true');
// is the address required to sign-up?
$scope.addressRequired = (settingsPromise.address_required === 'true');
// reCaptcha v2 site key (or undefined)
$scope.recaptchaSiteKey = settingsPromise.recaptcha_site_key;

View File

@ -72,8 +72,8 @@ Application.Controllers.controller('MembersController', ['$scope', 'Member', 'me
/**
* Controller used when editing the current user's profile (in dashboard)
*/
Application.Controllers.controller('EditProfileController', ['$scope', '$rootScope', '$state', '$window', '$sce', '$cookies', '$injector', 'Member', 'Auth', 'Session', 'activeProviderPromise', 'phoneRequiredPromise', 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t',
function ($scope, $rootScope, $state, $window, $sce, $cookies, $injector, Member, Auth, Session, activeProviderPromise, phoneRequiredPromise, growl, dialogs, CSRF, memberPromise, groups, _t) {
Application.Controllers.controller('EditProfileController', ['$scope', '$rootScope', '$state', '$window', '$sce', '$cookies', '$injector', 'Member', 'Auth', 'Session', 'activeProviderPromise', 'settingsPromise', 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t',
function ($scope, $rootScope, $state, $window, $sce, $cookies, $injector, Member, Auth, Session, activeProviderPromise, settingsPromise, growl, dialogs, CSRF, memberPromise, groups, _t) {
/* PUBLIC SCOPE */
// API URL where the form will be posted
@ -111,7 +111,10 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
$scope.password = { change: false };
// is the phone number required in _member_form?
$scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true');
$scope.phoneRequired = (settingsPromise.phone_required === 'true');
// is the address required in _member_form?
$scope.addressRequired = (settingsPromise.address_required === 'true');
// Angular-Bootstrap datepicker configuration for birthday
$scope.datePicker = {

View File

@ -13,8 +13,8 @@
'use strict';
Application.Controllers.controller('CompleteProfileController', ['$scope', '$rootScope', '$state', '$window', '_t', 'growl', 'CSRF', 'Auth', 'Member', 'settingsPromise', 'activeProviderPromise', 'groupsPromise', 'cguFile', 'memberPromise', 'Session', 'dialogs', 'AuthProvider', 'phoneRequiredPromise',
function ($scope, $rootScope, $state, $window, _t, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise, Session, dialogs, AuthProvider, phoneRequiredPromise) {
Application.Controllers.controller('CompleteProfileController', ['$scope', '$rootScope', '$state', '$window', '_t', 'growl', 'CSRF', 'Auth', 'Member', 'settingsPromise', 'activeProviderPromise', 'groupsPromise', 'cguFile', 'memberPromise', 'Session', 'dialogs', 'AuthProvider',
function ($scope, $rootScope, $state, $window, _t, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise, Session, dialogs, AuthProvider) {
/* PUBLIC SCOPE */
// API URL where the form will be posted
@ -48,7 +48,10 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo
$scope.cgu = cguFile.custom_asset;
// is the phone number required in _member_form?
$scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true');
$scope.phoneRequired = (settingsPromise.phone_required === 'true');
// is the address required in _member_form?
$scope.addressRequired = (settingsPromise.address_required === 'true');
// Angular-Bootstrap datepicker configuration for birthday
$scope.datePicker = {
@ -200,6 +203,14 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo
});
};
/**
* Hide the new account messages.
* If hidden, the page will be used only to complete the current user's profile.
*/
$scope.hideNewAccountConfirmation = function () {
return !$scope.activeProvider.previous_provider || $scope.activeProvider.previous_provider.id === $scope.activeProvider.id;
};
/* PRIVATE SCOPE */
/**

View File

@ -98,7 +98,10 @@ export enum SettingName {
ConfirmationRequired = 'confirmation_required',
WalletModule = 'wallet_module',
StatisticsModule = 'statistics_module',
UpcomingEventsShown = 'upcoming_events_shown'
UpcomingEventsShown = 'upcoming_events_shown',
PaymentSchedulePrefix = 'payment_schedule_prefix',
TrainingsModule = 'trainings_module',
AddressRequired = 'address_required'
}
export interface Setting {

View File

@ -130,12 +130,11 @@ angular.module('application.router', ['ui.router'])
}
},
resolve: {
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['fablab_name', 'name_genre']" }).$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['fablab_name', 'name_genre', 'phone_required', 'address_required']" }).$promise; }],
activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }],
memberPromise: ['Member', 'currentUser', function (Member, currentUser) { return Member.get({ id: currentUser.id }).$promise; }],
phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }]
memberPromise: ['Member', 'currentUser', function (Member, currentUser) { return Member.get({ id: currentUser.id }).$promise; }]
}
})
@ -168,7 +167,7 @@ angular.module('application.router', ['ui.router'])
resolve: {
groups: ['Group', function (Group) { return Group.query().$promise; }],
activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }],
phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }]
}
})
.state('app.logged.dashboard.projects', {
@ -916,7 +915,7 @@ angular.module('application.router', ['ui.router'])
}
},
resolve: {
phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }]
}
})
.state('app.admin.members_import', {
@ -957,7 +956,7 @@ angular.module('application.router', ['ui.router'])
walletPromise: ['Wallet', '$stateParams', function (Wallet, $stateParams) { return Wallet.getWalletByUser({ user_id: $stateParams.id }).$promise; }],
transactionsPromise: ['Wallet', 'walletPromise', function (Wallet, walletPromise) { return Wallet.transactions({ id: walletPromise.id }).$promise; }],
tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }],
phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }]
}
})
.state('app.admin.admins_new', {
@ -969,7 +968,7 @@ angular.module('application.router', ['ui.router'])
}
},
resolve: {
phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }]
}
})
.state('app.admin.managers_new', {
@ -1062,7 +1061,7 @@ angular.module('application.router', ['ui.router'])
"'booking_cancel_delay', 'main_color', 'secondary_color', 'spaces_module', 'twitter_analytics', " +
"'fablab_name', 'name_genre', 'reminder_enable', 'plans_module', 'confirmation_required', " +
"'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', 'trainings_module', " +
"'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', " +
"'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', 'address_required', " +
"'link_name', 'home_content', 'home_css', 'phone_required', 'upcoming_events_shown']"
}).$promise;
}],

View File

@ -267,125 +267,31 @@
}
}
.pricing-panel {
border: 1px solid $border-color;
height: 391px;
&:first-child {
border-right: none;
@include border-radius(3px 0 0 3px);
.list-of-plans {
.group-title {
width: 83.33%;
border-bottom: 1px solid;
padding-bottom: 2em;
margin: auto auto 1em;
}
&:last-child {
@include border-radius(0 3px 3px 0);
}
.plan-card {
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.title {
margin: 10px 0;
font-size: rem-calc(16);
text-transform: uppercase;
color: black;
}
.content {
padding: 15px 0;
background-color: $bg-gray;
.wrap, .wrap-monthly {
width: 130px;
height: 130px;
display: inline-block;
background: white;
@include border-radius(50%, 50%, 50%, 50%);
border: 3px solid;
.price {
width: 114px;
display: flex;
flex-direction: column;
justify-content: center;
@include border-radius(50%, 50%, 50%, 50%);
}
.plans-per-group {
& {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
border: 1px solid transparent;
}
.wrap-monthly {
& > .price {
& > .amount {
padding-top: 4px;
line-height: 1.2em;
}
& > .period {
padding-top: 4px;
}
}
}
.price {
position: relative;
top: 5px;
left: 5px;
height: 114px;
background-color: black;
.amount {
padding-left: 4px;
padding-right: 4px;
font-weight: bold;
font-size: rem-calc(17);
color: white;
}
.period {
position: relative;
top: -6px;
font-size: rem-calc(14);
color: white;
}
}
}
.card-footer {
display: flex;
flex-direction: column;
justify-content: space-around;
height: 100%;
.plan-description {
max-height: 5.2em;
overflow: hidden;
& > * {
width: 50%;
}
.cta-button {
margin: 20px 0;
.subscribe-button {
@extend .btn;
@extend .rounded;
outline: 0;
font-weight: 600;
font-size: rem-calc(16);
background-color: white;
padding-left: 30px;
padding-right: 30px;
@media screen and (max-width: 992px) {
& > * {
width: 100%;
}
button.subscribe-button:focus, button.subscribe-button:hover {
outline: 0;
}
}
.info-link {
margin-top: 1em;
}
}
}

View File

@ -32,6 +32,7 @@
@import "modules/payment-schedules-list";
@import "modules/stripe-confirm";
@import "modules/payment-schedule-dashboard";
@import "modules/plan-card";
@import "modules/select-gateway-modal";
@import "app.responsive";

View File

@ -0,0 +1,102 @@
.plan-card {
display: block;
height: 100%;
width: 100%;
.title {
margin: 10px 0;
font-size: 1.6rem;
text-transform: uppercase;
}
.content {
& {
padding: 15px 0;
background-color: #f5f5f5;
}
.wrap, .wrap-monthly {
width: 130px;
height: 130px;
display: inline-block;
background: white;
@include border-radius(50%, 50%, 50%, 50%);
border: 3px solid;
.price {
width: 114px;
display: flex;
flex-direction: column;
justify-content: center;
@include border-radius(50%, 50%, 50%, 50%);
}
}
.wrap-monthly {
& > .price {
& > .amount {
padding-top: 4px;
line-height: 1.2em;
}
& > .period {
padding-top: 4px;
}
}
}
.price {
position: relative;
top: 5px;
left: 5px;
height: 114px;
background-color: black;
.amount {
padding-left: 4px;
padding-right: 4px;
font-weight: bold;
font-size: rem-calc(17);
color: white;
}
.period {
position: relative;
top: -6px;
font-size: rem-calc(14);
color: white;
}
}
}
.card-footer {
.plan-description {
& p:nth-child(2n+3), p:nth-child(2n+4) {
display: none;
}
}
.cta-button {
margin: 20px 0;
.subscribe-button {
@extend .btn;
@extend .rounded;
outline: 0;
font-weight: 600;
font-size: rem-calc(16);
background-color: white;
padding-left: 30px;
padding-right: 30px;
}
button.subscribe-button:focus, button.subscribe-button:hover {
outline: 0;
}
}
.info-link {
margin-top: 1em;
}
}
}

View File

@ -130,9 +130,13 @@
<div class="input-group m-t-md plan-description-input">
<label for="plan[description]" class="control-label m-r-md" translate>{{ 'app.shared.plan.description' }}</label>
<div class="medium-editor-input">
<div ng-model="plan.description" medium-editor options='{"placeholder": "{{ "app.shared.plan.type_a_short_description" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ]
}'>
<div ng-model="plan.description"
medium-editor
options='{
"placeholder": "{{ "app.shared.plan.type_a_short_description" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ],
"targetBlank": true,
}'>
</div>
</div>
<input type="hidden" id="plan[description]" name="plan[description]" value="{{plan.description}}" />

View File

@ -30,18 +30,27 @@
<div class="row about-fablab">
<div class="col-md-4 col-md-offset-1">
<div class="text-justify" ng-model="aboutBodySetting.value" medium-editor options='{"placeholder": "{{ "app.admin.settings.input_the_main_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]
}'>
<div class="text-justify"
ng-model="aboutBodySetting.value"
medium-editor
options='{
"placeholder": "{{ "app.admin.settings.input_the_main_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ],
"targetBlank": true
}'>
</div>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'app.admin.settings.drag_and_drop_to_insert_images' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(aboutBodySetting)" translate>{{ 'app.shared.buttons.save' }}</button>
</div>
<div class="col-md-4 col-md-offset-2">
<div ng-model="aboutContactsSetting.value" medium-editor options='{"placeholder": "{{ "app.admin.settings.input_the_fablab_contacts" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]
}'>
<div ng-model="aboutContactsSetting.value"
medium-editor
options='{
"placeholder": "{{ "app.admin.settings.input_the_fablab_contacts" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ],
"targetBlank": true
}'>
</div>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'app.admin.settings.shift_enter_to_force_carriage_return' | translate }}</span>

View File

@ -48,52 +48,76 @@
<div class="row">
<div class="col-md-3">
<h4 translate>{{ 'app.admin.settings.message_of_the_machine_booking_page' }}</h4>
<div ng-model="machineExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "app.admin.settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ]
}'>
<div ng-model="machineExplicationsAlert.value"
medium-editor
options='{
"placeholder": "{{ "app.admin.settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ],
"targetBlank": true
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(machineExplicationsAlert)" translate>{{ 'app.shared.buttons.save' }}</button>
</div>
<div class="col-md-3">
<h4 translate>{{ 'app.admin.settings.warning_message_of_the_training_booking_page'}}</h4>
<div ng-model="trainingExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "app.admin.settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ]
}'>
<div ng-model="trainingExplicationsAlert.value"
medium-editor
options='{
"placeholder": "{{ "app.admin.settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ],
"targetBlank": true
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(trainingExplicationsAlert)" translate>{{ 'app.shared.buttons.save' }}</button>
</div>
<div class="col-md-3">
<h4 translate>{{ 'app.admin.settings.information_message_of_the_training_reservation_page'}}</h4>
<div ng-model="trainingInformationMessage.value" medium-editor options='{"placeholder": "{{ "app.admin.settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ]
}'>
<div ng-model="trainingInformationMessage.value"
medium-editor
options='{
"placeholder": "{{ "app.admin.settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ],
"targetBlank": true
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(trainingInformationMessage)" translate>{{ 'app.shared.buttons.save' }}</button>
</div>
<div class="col-md-3">
<h4 translate>{{ 'app.admin.settings.message_of_the_subscriptions_page' }}</h4>
<div ng-model="subscriptionExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "app.admin.settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ]
}'>
<div ng-model="subscriptionExplicationsAlert.value"
medium-editor
options='{
"placeholder": "{{ "app.admin.settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ],
"targetBlank": true
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(subscriptionExplicationsAlert)" translate>{{ 'app.shared.buttons.save' }}</button>
</div>
<div class="col-md-3">
<h4 translate>{{ 'app.admin.settings.message_of_the_events_page' }}</h4>
<div ng-model="eventExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "app.admin.settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ]
}'>
<div ng-model="eventExplicationsAlert.value"
medium-editor
options='{
"placeholder": "{{ "app.admin.settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ],
"targetBlank": true
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(eventExplicationsAlert)" translate>{{ 'app.shared.buttons.save' }}</button>
</div>
<div class="col-md-3" ng-show="$root.modules.spaces">
<h4 translate>{{ 'app.admin.settings.message_of_the_spaces_page' }}</h4>
<div ng-model="spaceExplicationsAlert.value" medium-editor options='{"placeholder": "{{ "app.admin.settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ]
}'>
<div ng-model="spaceExplicationsAlert.value"
medium-editor
options='{
"placeholder": "{{ "app.admin.settings.type_the_message_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "unorderedlist", "header2" ],
"targetBlank": true
}'>
</div>
<button name="button" class="btn btn-warning" ng-click="save(spaceExplicationsAlert)" translate>{{ 'app.shared.buttons.save' }}</button>
</div>
@ -400,6 +424,18 @@
</boolean-setting>
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.address' }}</h3>
<p class="alert alert-warning m-h-md" translate>
{{ 'app.admin.settings.address_required_info_html' }}
</p>
<div class="col-md-10 col-md-offset-1">
<boolean-setting name="address_required"
settings="allSettings"
label="app.admin.settings.address_is_required">
</boolean-setting>
</div>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.captcha' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.captcha_info_html' | translate"></p>

View File

@ -14,8 +14,15 @@
<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 }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]}'></div>
<div ng-model="homeBlogpostSetting.value"
class="well"
medium-editor
options='{
"placeholder": "{{ "app.admin.settings.type_your_news_here" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ],
"targetBlank": true
}'>
</div>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'app.admin.settings.leave_it_empty_to_not_bring_up_any_news_on_the_home_page' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(homeBlogpostSetting)" translate>{{ 'app.shared.buttons.save' }}</button>
</div>

View File

@ -1,33 +1,25 @@
<div class="row m-t m-b padder" ng-repeat="plansGroup in plansClassifiedByGroup" ng-if="ctrl.member.group_id == plansGroup.id || !ctrl.member" ng-show="plansAreShown">
<div class="row m-t m-b padder list-of-plans" ng-repeat="plansGroup in plansClassifiedByGroup" ng-if="ctrl.member.group_id == plansGroup.id || !ctrl.member" ng-show="plansAreShown">
<div class="col-md-12 text-center">
<h2 class="text-u-c">{{ plansGroup.name }}</h2>
<h2 class="text-u-c group-title">{{ plansGroup.name }}</h2>
</div>
<div class="row row-centered padder">
<div class="col-xs-12 col-md-12 col-lg-10 col-centered no-gutter">
<div class="plans-per-group">
<div ng-repeat="(key, plan) in plansGroup.plans.filter(filterDisabledPlans) | orderBy:'interval'"
ng-class-even="'row'">
<div class="pricing-panel col-xs-12 col-md-6 col-lg-6 text-center"
ng-class="{'col-md-12 col-lg-12':(plansGroup.plans.filter(filterDisabledPlans).length % 2 == 1 && key == plansGroup.plans.filter(filterDisabledPlans).length-1)}">
<!-- ng-class directive center the last item if the list length is odd -->
<plan-card plan="plan"
user-id="ctrl.member.id"
subscribed-plan-id="ctrl.member.subscribed_plan.id"
operator="currentUser"
on-select-plan="selectPlan"
is-selected="isSelected(plan)">
</plan-card>
</div>
</div>
<a class="m-t-lg btn btn-small btn-default pull-right" href="#" ng-click="doNotSubscribePlan($event)">{{ 'app.shared.plan_subscribe.do_not_subscribe' | translate }} <i class="fa fa-long-arrow-right"></i></a>
<plan-card ng-repeat="(key, plan) in plansGroup.plans.filter(filterDisabledPlans) | orderBy:'-ui_weight'"
plan="plan"
user-id="ctrl.member.id"
subscribed-plan-id="ctrl.member.subscribed_plan.id"
operator="currentUser"
on-select-plan="selectPlan"
is-selected="isSelected(plan)">
</plan-card>
</div>
<a class="m-t-lg btn btn-small btn-default pull-right" href="#" ng-click="doNotSubscribePlan($event)">{{ 'app.shared.plan_subscribe.do_not_subscribe' | translate }} <i class="fa fa-long-arrow-right"></i></a>
<div class="row row-centered m-t-lg">
<div class="col-xs-12 col-md-12 col-lg-10 col-centered no-gutter">
<uib-alert type="warning m">

View File

@ -14,31 +14,26 @@
</section>
<div class="row no-gutter">
<div class="row no-gutter list-of-plans">
<div class="col-sm-12 col-md-9 b-r">
<div class="row m-t m-b padder" ng-repeat="plansGroup in plansClassifiedByGroup | groupFilter:ctrl.member">
<div ng-show="plansGroup.actives > 0">
<div class="col-md-12 text-center">
<h2 class="text-u-c">{{plansGroup.name}}</h2>
<h2 class="text-u-c group-title">{{plansGroup.name}}</h2>
</div>
<div class="row row-centered padder">
<div class="col-xs-12 col-md-12 col-lg-10 col-centered no-gutter">
<div class="plans-per-group">
<!-- ng-class directive center the last item if the list length is odd -->
<div class="pricing-panel col-xs-12 col-md-6 col-lg-6 text-center"
ng-class="{'col-md-12 col-lg-12 b-r':(plansGroup.plans.filter(filterDisabledPlans).length % 2 == 1 && key == plansGroup.plans.filter(filterDisabledPlans).length-1)}"
ng-repeat="(key, plan) in plansGroup.plans.filter(filterDisabledPlans) | orderBy: '-ui_weight'">
<plan-card plan="plan"
<plan-card ng-repeat="(key, plan) in plansGroup.plans.filter(filterDisabledPlans) | orderBy: '-ui_weight'"
plan="plan"
user-id="ctrl.member.id"
subscribed-plan-id="ctrl.member.subscribed_plan.id"
operator="currentUser"
on-select-plan="selectPlan"
is-selected="isSelected(plan)">
is-selected="isSelected(plan)"
</plan-card>
</div>
</div>

View File

@ -21,7 +21,7 @@
<div class="row no-gutter">
<div class="col-sm-12 col-md-12 b-r">
<div class="row">
<div class="row" ng-hide="hideNewAccountConfirmation()">
<div class="col-md-offset-2 col-md-8 m-t-md">
<section class="panel panel-default bg-light m-lg">
<div class="panel-body m-r">
@ -34,19 +34,19 @@
</section>
</div>
</div>
<div class="row col-md-2 col-md-offset-5 hidden-sm hidden-xs" ng-hide="user.merged_at">
<div class="row col-md-2 col-md-offset-5 hidden-sm hidden-xs" ng-hide="user.merged_at || hideNewAccountConfirmation()">
<p class="font-felt fleche-left text-lg upper text-center">
<img src="../../images/arrow-left.png" class="fleche-left visible-lg visible-md fleche-left-from-top" />
<span class="or" translate>{{ 'app.logged.profile_completion.or' }}</span>
<img src="../../images/arrow-left.png" class="fleche-right visible-lg visible-md fleche-right-from-top" />
</p>
</div>clear
</div>
<div class="row">
<div class="col-md-6">
<div class="m-lg panel panel-default bg-light pos-rlt" ng-hide="hasDuplicate()">
<div ng-class="{'disabling-overlay' : !!user.auth_token}">
<div class="panel-body">
<h3 translate>{{ 'app.logged.profile_completion.new_on_this_platform' }}</h3>
<h3 translate ng-hide="hideNewAccountConfirmation()">{{ 'app.logged.profile_completion.new_on_this_platform' }}</h3>
<p translate>{{ 'app.logged.profile_completion.please_fill_the_following_form'}}.</p>
<p class="text-italic">{{ 'app.logged.profile_completion.some_data_may_have_already_been_provided_by_provider_and_cannot_be_modified' | translate:{NAME:activeProvider.name} }}.<br/>
{{ 'app.logged.profile_completion.then_click_on_' | translate }} <strong translate>{{ 'app.shared.buttons.confirm_changes' }}</strong> {{ 'app.logged.profile_completion._to_start_using_the_application' | translate }}.</p>
@ -145,14 +145,14 @@
</div>
</section>
</div>
<div class="row col-xs-2 col-xs-offset-5 hidden-md hidden-lg">
<div class="row col-xs-2 col-xs-offset-5 hidden-md hidden-lg" ng-hide="hideNewAccountConfirmation()">
<p class="font-felt fleche-left text-lg upper text-center">
<span class="or" translate>{{ 'app.logged.profile_completion.or' }}</span>
</p>
</div>
<div class="col-md-6 m-t-3xl-on-md" ng-hide="user.merged_at">
<ng-include src="'/profile/_token.html'"></ng-include>
</div>
<div class="col-md-6 m-t-3xl-on-md" ng-hide="user.merged_at || hideNewAccountConfirmation()">
<ng-include src="'/profile/_token.html'"></ng-include>
</div>
</div>
</div>
</div>

View File

@ -224,7 +224,7 @@
<div class="form-group">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_invoicing' | translate }}"><i class="fa fa-map-marker"></i> </span>
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_invoicing' | translate }}"><i class="fa fa-map-marker"></i> <span class="exponent" ng-show="addressRequired"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="hidden"
name="user[invoicing_profile_attributes][address_attributes][id]"
ng-value="user.invoicing_profile.address.id" />
@ -234,7 +234,8 @@
class="form-control"
id="user_address"
ng-disabled="preventField['profile.address'] && user.invoicing_profile.address.address && !userForm['user[invoicing_profile_attributes][address_attributes][address]'].$dirty"
placeholder="{{ 'app.shared.user.address' | translate }}"/>
placeholder="{{ 'app.shared.user.address' | translate }}"
ng-required="addressRequired"/>
</div>
</div>

View File

@ -213,6 +213,24 @@
</div>
</div>
<div class="form-group required-row">
<div class="col-sm-12">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-map-marker"></i> </span>
<input type="text"
name="address"
ng-model="user.invoicing_profile_attributes.address_attributes.address"
class="form-control"
placeholder="{{ 'app.public.common.address' | translate }}"
ng-required="addressRequired"/>
</div>
<span ng-show="addressRequired" class="exponent help-cursor" title="{{ 'app.public.common.used_for_invoicing' | translate }}">
<i class="fa fa-asterisk" aria-hidden="true"></i>
</span>
<span class="help-block" ng-show="signupForm.address.$dirty && signupForm.address.$error.required" translate>{{ 'app.public.common.address_is_required' }}</span>
</div>
</div>
<div class="form-group" ng-class="{'has-error': signupForm.is_allow_contact.$dirty && signupForm.is_allow_contact.$invalid}">
<div class="col-sm-12 checkbox-group">
<input type="checkbox"

View File

@ -41,6 +41,11 @@ class AuthProvider < ApplicationRecord
end
end
## Return the previously active provider
def self.previous
find_by(status: 'previous')
end
## Get the provider matching the omniAuth strategy name
def self.from_strategy_name(strategy_name)
return SimpleAuthProvider.new if strategy_name.blank? || all.empty?

View File

@ -20,6 +20,8 @@ class InvoicingProfile < ApplicationRecord
has_many :operated_invoices, foreign_key: :operator_profile_id, class_name: 'Invoice', dependent: :nullify
has_many :operated_payment_schedules, foreign_key: :operator_profile_id, class_name: 'PaymentSchedule', dependent: :nullify
validates :address, presence: true, if: -> { Setting.get('address_required') }
def full_name
# if first_name or last_name is nil, the empty string will be used as a temporary replacement
(first_name || '').humanize.titleize + ' ' + (last_name || '').humanize.titleize

View File

@ -109,6 +109,7 @@ class Setting < ApplicationRecord
upcoming_events_shown
payment_schedule_prefix
trainings_module
address_required
payment_gateway] }
# WARNING: when adding a new key, you may also want to add it in app/policies/setting_policy.rb#public_whitelist

View File

@ -203,7 +203,8 @@ class User < ApplicationRecord
def need_completion?
statistic_profile.gender.nil? || profile.first_name.blank? || profile.last_name.blank? || username.blank? ||
email.blank? || encrypted_password.blank? || group_id.nil? || statistic_profile.birthday.blank? ||
(Setting.get('phone_required') && profile.phone.blank?)
(Setting.get('phone_required') && profile.phone.blank?) ||
(Setting.get('address_required') && invoicing_profile.address&.address&.blank?)
end
## Retrieve the requested data in the User and user's Profile tables

View File

@ -38,7 +38,7 @@ class SettingPolicy < ApplicationPolicy
fablab_name name_genre event_explications_alert space_explications_alert link_name home_content phone_required
tracking_id book_overlapping_slots slot_duration events_in_calendar spaces_module plans_module invoicing_module
recaptcha_site_key feature_tour_display disqus_shortname allowed_cad_extensions openlab_app_id openlab_default
online_payment_module stripe_public_key confirmation_required wallet_module trainings_module payment_gateway]
online_payment_module stripe_public_key confirmation_required wallet_module trainings_module address_required payment_gateway]
end
##

View File

@ -33,6 +33,7 @@ class AccountingExportService
invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC')
invoices = invoices.where('total > 0') unless export_zeros
invoices.each do |i|
puts "processing invoice #{i.id}..." unless Rails.env.test?
content << generate_rows(i)
end

View File

@ -22,7 +22,8 @@ class Availabilities::CreateAvailabilitiesService
space_ids: availability.space_ids,
tag_ids: availability.tag_ids,
nb_total_places: availability.nb_total_places,
slot_duration: availability.slot_duration
slot_duration: availability.slot_duration,
plan_ids: availability.plan_ids
).save!
end
end

View File

@ -88,7 +88,7 @@ a {
.app-generator a,
.home-events h4 a,
a.reinit-filters,
.pricing-panel a,
.plan-card a,
.calendar-url a,
.article a,
a.project-author,
@ -103,7 +103,7 @@ a.collected-infos {
.app-generator a:hover,
.home-events h4 a:hover,
a.reinit-filters:hover,
.pricing-panel a:hover,
.plan-card a:hover,
.calendar-url a:hover,
.article a:hover,
a.project-author:hover,
@ -254,19 +254,19 @@ h5:after {
color: $secondary-text-color;
}
.pricing-panel .plan-card .content .wrap,
.pricing-panel .plan-card .content .wrap-monthly {
border-color: $secondary;
.plan-card .content .wrap,
.plan-card .content .wrap-monthly {
border-color: $secondary !important;
}
.pricing-panel .plan-card .content .price {
background-color: $primary;
color: $primary-text-color;
.plan-card .content .price {
background-color: $primary !important;
color: $primary-text-color !important;
}
.pricing-panel .card-footer .cta-button .btn:hover,
.pricing-panel .card-footer .cta-button .custom-invoice .modal-body .elements li:hover,
.custom-invoice .modal-body .elements .pricing-panel .card-footer .cta-button li:hover {
.plan-card .card-footer .cta-button .btn:hover,
.plan-card .card-footer .cta-button .custom-invoice .modal-body .elements li:hover,
.custom-invoice .modal-body .elements .plan-card .card-footer .cta-button li:hover {
background-color: $secondary !important;
color: $secondary-text-color;
}
@ -306,7 +306,7 @@ section#cookies-modal div.cookies-consent .cookies-actions button.accept {
color: $secondary-text-color;
}
.pricing-panel {
.plan-card {
.card-footer {
.cta-button {
button.subscribe-button {

View File

@ -1 +1,3 @@
json.extract! auth_provider, :id, :name, :status, :providable_type, :strategy_name
# frozen_string_literal: true
json.extract! auth_provider, :id, :name, :status, :providable_type, :strategy_name

View File

@ -1,4 +1,9 @@
# frozen_string_literal: true
json.partial! 'api/auth_providers/auth_provider', auth_provider: @provider
json.previous_provider do
json.partial! 'api/auth_providers/auth_provider', auth_provider: @previous if @previous
end
json.mapping @provider.sso_fields
json.link_to_sso_profile @provider.link_to_sso_profile
if @provider.providable_type == DatabaseProvider.name

View File

@ -1081,6 +1081,7 @@ en:
machines_sort_by: "machines display order"
fab_analytics: "Fab Analytics"
phone_required: "phone required"
address_required: "address required"
tracking_id: "tracking ID"
facebook_app_id: "Facebook App ID"
twitter_analytics: "Twitter analytics account"
@ -1117,6 +1118,9 @@ en:
phone: "Phone"
phone_is_required: "Phone required"
phone_required_info: "You can define if the phone number should be required to register a new user on Fab-manager."
address: "Address"
address_required_info_html: "You can define if the address should be required to register a new user on Fab-manager.<br/><strong>Please note</strong> that, depending on your country, the regulations may requires addresses for the invoices to be valid."
address_is_required: "Address is required"
captcha: "Captcha"
captcha_info_html: "You can setup a protection against robots, to prevent them creating members accounts. This protection is using Google reCAPTCHA. Sign up for <a href='http://www.google.com/recaptcha/admin' target='_blank'>an API key pair</a> to start using the captcha."
site_key: "Site key"

View File

@ -1081,6 +1081,7 @@ fr:
machines_sort_by: "l'ordre d'affichage des machines"
fab_analytics: "Fab Analytics"
phone_required: "téléphone requis"
address_required: "adresse requise"
tracking_id: "l'ID de suivi"
facebook_app_id: "l'App ID Facebook"
twitter_analytics: "compte Twitter analytics"
@ -1117,6 +1118,9 @@ fr:
phone: "Téléphone"
phone_is_required: "Téléphone requis"
phone_required_info: "Vous pouvez définir si le numéro de téléphone doit être requis, lors de l'enregistrement d'un nouvel utilisateur sur Fab-manager."
address: "Adresse"
address_required_info_html: "Vous pouvez définir si l'adresse doit être requise, lors de l'enregistrement d'un nouvel utilisateur sur Fab-manager.<br/><strong>Veuillez noter</strong> que, selon votre pays, la réglementation peut exiger des adresses pour que les factures soient valides."
address_is_required: "Adresse requise"
captcha: "Captcha"
captcha_info_html: "Vous pouvez mettre en place une protection contre les robots, pour les empêcher de créer des comptes membre. Cette protection utilise Google reCAPTCHA. Inscrivez vous pour obtenir <a href='http://www.google.com/recaptcha/admin' target='_blank'>une paire de clefs d'API</a> afin d'utiliser le captcha."
site_key: "Clef de site"

View File

@ -84,6 +84,8 @@ en:
birth_date_is_required: "Birth date is required."
phone_number: "Phone number"
phone_number_is_required: "Phone number is required."
address: "Address"
address_is_required: "Address is required"
i_authorize_Fablab_users_registered_on_the_site_to_contact_me: "I authorize FabLab users, registered on the site, to contact me"
i_accept_to_receive_information_from_the_fablab: "I accept to receive information from the FabLab"
i_ve_read_and_i_accept_: "I've read and I accept"

View File

@ -84,6 +84,8 @@ fr:
birth_date_is_required: "La date de naissance est requise."
phone_number: "Numéro de téléphone"
phone_number_is_required: "Le numéro de téléphone est requis."
address: "Adresse"
address_is_required: "L'adresse est requise"
i_authorize_Fablab_users_registered_on_the_site_to_contact_me: "J'autorise les utilisateurs du Fab Lab inscrits sur le site à me contacter"
i_accept_to_receive_information_from_the_fablab: "J'accepte de recevoir des informations du Fab Lab"
i_ve_read_and_i_accept_: "J'ai lu et j'accepte"

View File

@ -16,7 +16,7 @@ namespace :fablab do
raise "FATAL ERROR: the provider '#{args.provider}' is already enabled" if AuthProvider.active.name == args.provider
# disable previous provider
prev_prev = AuthProvider.find_by(status: 'previous')
prev_prev = AuthProvider.previous
prev_prev&.update_attribute(:status, 'pending')
AuthProvider.active.update_attribute(:status, 'previous')

View File

@ -26,12 +26,16 @@ welcome_message()
system_requirements()
{
if [ "$(whoami)" = "root" ]; then
if is_root; then
echo "It is not recommended to run this script as root. As a normal user, elevation will be prompted if needed."
read -rp "Continue anyway? (Y/n) " confirm </dev/tty
if [[ "$confirm" = "n" ]]; then exit 1; fi
else
local _groups=("sudo" "docker")
if [ "$(has_sudo)" = 'no_sudo' ]; then
echo "You are not allowed to sudo. Please add $(whoami) to the sudoers before continuing."
exit 1
fi
local _groups=("docker")
for _group in "${_groups[@]}"; do
echo -e "detecting group $_group for current user..."
if ! groups | grep "$_group"; then
@ -53,6 +57,46 @@ system_requirements()
printf "\e[92m[ ✔ ] All requirements successfully checked.\e[39m \n\n"
}
is_root()
{
return $(id -u)
}
has_sudo()
{
local prompt
prompt=$(sudo -nv 2>&1)
if [ $? -eq 0 ]; then
echo "has_sudo__pass_set"
elif echo $prompt | grep -q '^sudo:'; then
echo "has_sudo__needs_pass"
else
echo "no_sudo"
fi
}
elevate_cmd()
{
local cmd=$@
HAS_SUDO=$(has_sudo)
case "$HAS_SUDO" in
has_sudo__pass_set)
sudo $cmd
;;
has_sudo__needs_pass)
echo "Please supply sudo password for the following command: sudo $cmd"
sudo $cmd
;;
*)
echo "Please supply root password for the following command: su -c \"$cmd\""
su -c "$cmd"
;;
esac
}
read_email()
{
local email
@ -113,8 +157,8 @@ prepare_files()
read -rp "Continue? (Y/n) " confirm </dev/tty
if [[ "$confirm" = "n" ]]; then exit 1; fi
sudo mkdir -p "$FABMANAGER_PATH/config"
sudo chown -R "$(whoami)" "$FABMANAGER_PATH"
elevate_cmd mkdir -p "$FABMANAGER_PATH/config"
elevate_cmd chown -R "$(whoami)" "$FABMANAGER_PATH"
mkdir -p "$FABMANAGER_PATH/elasticsearch/config"
@ -165,7 +209,7 @@ prepare_nginx()
echo "Removing nginx..."
yq -i eval 'del(.services.nginx)' docker-compose.yml
printf "The two following configurations are useful if you want to install Fab-manager behind a reverse proxy...\n"
read -rp "Do you want to map the Fab-manager's service to an external network? (Y/n) " confirm </dev/tty
read -rp "- Do you want to map the Fab-manager's service to an external network? (Y/n) " confirm </dev/tty
if [ "$confirm" != "n" ]; then
echo "Adding a network configuration to the docker-compose.yml file..."
yq -i eval '.networks.web.external = "true"' docker-compose.yml
@ -175,7 +219,7 @@ prepare_nginx()
yq -i eval '.services.postgres.networks += ["db"]' docker-compose.yml
yq -i eval '.services.redis.networks += ["db"]' docker-compose.yml
fi
read -rp "Do you want to rename the Fab-manager's service? (Y/n) " confirm </dev/tty
read -rp "- Do you want to rename the Fab-manager's service? (Y/n) " confirm </dev/tty
if [ "$confirm" != "n" ]; then
current="$(yq eval '.services.*.image | select(. == "sleede/fab-manager*") | path | .[-2]' docker-compose.yml)"
printf "=======================\n- \e[1mCurrent value: %s\e[21m\n- New value? (leave empty to keep the current value)\n" "$current"
@ -205,9 +249,9 @@ prepare_letsencrypt()
echo "Now downloading and configuring the certificate signing bot..."
docker pull certbot/certbot:latest
sed -i.bak "s:/apps/fabmanager:$FABMANAGER_PATH:g" "$FABMANAGER_PATH/letsencrypt/systemd/letsencrypt.service"
sudo cp "$FABMANAGER_PATH/letsencrypt/systemd/letsencrypt.service" /etc/systemd/system/letsencrypt.service
sudo cp "$FABMANAGER_PATH/letsencrypt/systemd/letsencrypt.timer" /etc/systemd/system/letsencrypt.timer
sudo systemctl daemon-reload
elevate_cmd cp "$FABMANAGER_PATH/letsencrypt/systemd/letsencrypt.service" /etc/systemd/system/letsencrypt.service
elevate_cmd cp "$FABMANAGER_PATH/letsencrypt/systemd/letsencrypt.timer" /etc/systemd/system/letsencrypt.timer
elevate_cmd systemctl daemon-reload
fi
}
@ -330,14 +374,14 @@ enable_ssl()
{
if [ "$LETSENCRYPT" != "n" ]; then
# generate certificate
sudo systemctl start letsencrypt.service
elevate_cmd systemctl start letsencrypt.service
# serve http content over ssl
mv "$FABMANAGER_PATH/config/nginx/fabmanager.conf" "$FABMANAGER_PATH/config/nginx/fabmanager.conf.nossl"
mv "$FABMANAGER_PATH/config/nginx/fabmanager.conf.ssl" "$FABMANAGER_PATH/config/nginx/fabmanager.conf"
stop
start
sudo systemctl enable letsencrypt.timer
sudo systemctl start letsencrypt.timer
elevate_cmd systemctl enable letsencrypt.timer
elevate_cmd systemctl start letsencrypt.timer
fi
}

View File

@ -87,7 +87,7 @@ add_environments()
{
for ENV in "${ENVIRONMENTS[@]}"; do
if [[ "$ENV" =~ ^[A-Z0-9_]+=.*$ ]]; then
printf "Inserting variable %s...\n" "$ENV"
printf "\e[91m::\e[0m \e[1mInserting variable %s..\e[0m.\n" "$ENV"
printf "# added on %s\n%s\n" "$(date +%Y-%m-%d\ %R)" "$ENV" >> "config/env"
else
echo "Ignoring invalid option: -e $ENV. Given value is not valid environment variable, please see https://huit.re/environment-doc"
@ -133,7 +133,7 @@ upgrade()
BRANCH='master'
if yq eval '.services.*.image | select(. == "sleede/fab-manager*")' docker-compose.yml | grep -q ':dev'; then BRANCH='dev'; fi
for SCRIPT in "${SCRIPTS[@]}"; do
printf "Running script %s from branch %s...\n" "$SCRIPT" "$BRANCH"
printf "\e[91m::\e[0m \e[1mRunning script %s from branch %s...\e[0m\n" "$SCRIPT" "$BRANCH"
if [[ "$YES_ALL" = "true" ]]; then
\curl -sSL "https://raw.githubusercontent.com/sleede/fab-manager/$BRANCH/scripts/$SCRIPT.sh" | bash -s -- -y
else
@ -143,7 +143,7 @@ upgrade()
compile_assets
docker-compose run --rm "$SERVICE" bundle exec rake db:migrate
for COMMAND in "${COMMANDS[@]}"; do
printf "Running command %s...\n" "$COMMAND"
printf "\e[91m::\e[0m \e[1mRunning command %s...\e[0m\n" "$COMMAND"
docker-compose run --rm "$SERVICE" bundle exec "$COMMAND"
done
docker-compose up -d
@ -152,7 +152,7 @@ upgrade()
clean()
{
echo "Current disk usage:"
echo -e "\e[91m::\e[0m \e[1mCurrent disk usage:\e[0m"
df -h /
[[ "$YES_ALL" = "true" ]] && confirm="y" || read -rp "Clean previous docker images? (y/N) " confirm </dev/tty
if [[ "$confirm" == "y" ]]; then

View File

@ -31,8 +31,8 @@ stylesheet_1:
.btn-warning:hover, .editable-buttons button[type=submit].btn-primary:hover, .btn-warning:focus, .editable-buttons button[type=submit].btn-primary:focus, .btn-warning.focus, .editable-buttons button.focus[type=submit].btn-primary, .btn-warning:active, .editable-buttons button[type=submit].btn-primary:active, .btn-warning.active, .editable-buttons button.active[type=submit].btn-primary, .open > .btn-warning.dropdown-toggle, .editable-buttons .open > button.dropdown-toggle[type=submit].btn-primary { background-color: #998500 !important; border-color: #998500 !important; }
.btn-warning-full { border-color: #ffdd00; background-color: #ffdd00; }
.heading .heading-btn a:hover { background-color: #ffdd00; }
.pricing-panel .content .wrap { border-color: #ffdd00; }
.pricing-panel .cta-button .btn:hover, .pricing-panel .cta-button .custom-invoice .modal-body .elements li:hover, .custom-invoice .modal-body .elements .pricing-panel .cta-button li:hover { background-color: #ffdd00 !important; }
.plan-card .content .wrap { border-color: #ffdd00; }
.plan-card .cta-button .btn:hover, .plan-card .cta-button .custom-invoice .modal-body .elements li:hover, .custom-invoice .modal-body .elements .plan-card .cta-button li:hover { background-color: #ffdd00 !important; }
a.label:hover, .form-control.form-control-ui-select .select2-choices a.select2-search-choice:hover, a.label:focus, .form-control.form-control-ui-select .select2-choices a.select2-search-choice:focus { color: #cb1117; }
.about-picture { background: linear-gradient( rgba(255,255,255,0.12), rgba(255,255,255,0.13) ), linear-gradient( rgba(203, 17, 23, 0.78), rgba(203, 17, 23, 0.82) ), url('/about-fablab.jpg') no-repeat; }
.social-icons > div:hover { background-color: #ffdd00; }
@ -46,4 +46,4 @@ stylesheet_1:
stylesheet_2:
id: 2
contents: ".home-page { }"
name: home_page
name: home_page

View File

@ -124,8 +124,8 @@ module Availabilities
'expected end_at = start_at + 4 slots of 90 minutes'
# Check the recurrence
assert_equal (availability[:start_at].to_date + 2.weeks),
availability[:end_date].to_date,
assert_equal (availability[:start_at].to_datetime + 2.weeks).to_date,
availability[:end_date].to_datetime.utc.to_date,
'expected end_date = start_at + 2 weeks'
end
end