1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-03-21 12:29:03 +01:00

Merge branch 'dev' for release 4.7.6

This commit is contained in:
Sylvain 2021-03-24 11:19:16 +01:00
commit 4de184fb19
71 changed files with 774 additions and 380 deletions

View File

@ -1,5 +1,28 @@
# Changelog Fab-manager
## 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

@ -51,6 +51,9 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
// active tab will be set here
$scope.selectedIndex = null;
// ui-bootstrap active tab index
$scope.selectedTab = 0;
// for palmares graphs, filters values are stored here
$scope.ranking = {
sortCriterion: 'ca',
@ -101,9 +104,11 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
* Callback called when the active tab is changed.
* Recover the current tab and store its value in $scope.selectedIndex
* @param tab {Object} elasticsearch statistic structure
* @param index {number} index of the tab in the $scope.statistics array
*/
$scope.setActiveTab = function (tab) {
$scope.setActiveTab = function (tab, index) {
$scope.selectedIndex = tab;
$scope.selectedTab = index;
$scope.ranking.groupCriterion = 'subType';
if (tab.ca) {
$scope.ranking.sortCriterion = 'ca';
@ -113,6 +118,18 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
return refreshChart();
};
/**
* Returns true if the provided tab must be hidden due to some global or local configuration
* @param tab {Object} elasticsearch statistic structure (from statistic_indices table)
*/
$scope.hiddenTab = function (tab) {
if (tab.graph) {
return !((tab.es_type_key === 'subscription' && !$rootScope.modules.plans) ||
(tab.es_type_key === 'training' && !$rootScope.modules.trainings));
}
return false;
};
/**
* Callback to close the date-picking popup and refresh the results
*/
@ -137,11 +154,20 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
$scope.$watch(scope => scope.ranking.groupCriterion
, (newValue, oldValue) => refreshChart());
return refreshChart();
// set the default tab to "machines" if "subscriptions" are disabled
if (!$rootScope.modules.plans) {
const idx = $scope.statistics.findIndex(s => s.es_type_key === 'machine');
$scope.setActiveTab($scope.statistics[idx], idx);
} else {
const idx = $scope.statistics.findIndex(s => s.es_type_key === 'subscription');
$scope.setActiveTab($scope.statistics[idx], idx);
}
});
// workaround for angular-bootstrap::tabs behavior: on tab deletion, another tab will be selected
// which will cause every tabs to reload, one by one, when the view is closed
return $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
if ((fromState.name === 'app.admin.stats_graphs') && (Object.keys(fromParams).length === 0)) {
return $scope.preventRefresh = true;
}

View File

@ -1309,8 +1309,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
@ -1370,7 +1370,6 @@ Application.Controllers.controller('StripeKeysModalController', ['$scope', '$uib
$scope.publicKeyStatus = false;
return;
}
const today = new Date();
$http({
method: 'POST',
url: 'https://api.stripe.com/v1/tokens',

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

@ -111,7 +111,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
* @returns {float}
*/
$scope.findTrainingsPricing = function (trainingsPricings, trainingId, groupId) {
for (let trainingsPricing of Array.from(trainingsPricings)) {
for (const trainingsPricing of Array.from(trainingsPricings)) {
if ((trainingsPricing.training_id === trainingId) && (trainingsPricing.group_id === groupId)) {
return trainingsPricing;
}
@ -138,7 +138,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
* @returns {Object} Plan, inherits from $resource
*/
$scope.getPlanFromId = function (id) {
for (let plan of Array.from($scope.plans)) {
for (const plan of Array.from($scope.plans)) {
if (plan.id === parseInt(id)) {
return plan;
}
@ -151,7 +151,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
* @returns {Object} Group, inherits from $resource
*/
$scope.getGroupFromId = function (groups, id) {
for (let group of Array.from(groups)) {
for (const group of Array.from(groups)) {
if (group.id === parseInt(id)) {
return group;
}
@ -313,7 +313,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
* @param [id] {number} credit id for edition, create a new credit object if not provided
*/
$scope.saveMachineCredit = function (data, id) {
for (let mc of Array.from($scope.machineCredits)) {
for (const mc of Array.from($scope.machineCredits)) {
if ((mc.plan_id === data.plan_id) && (mc.creditable_id === data.creditable_id) && ((id === null) || (mc.id !== id))) {
growl.error(_t('app.admin.pricing.error_a_credit_linking_this_machine_with_that_subscription_already_exists'));
if (!id) {
@ -383,7 +383,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
* @param [id] {number} credit id for edition, create a new credit object if not provided
*/
$scope.saveSpaceCredit = function (data, id) {
for (let sc of Array.from($scope.spaceCredits)) {
for (const sc of Array.from($scope.spaceCredits)) {
if ((sc.plan_id === data.plan_id) && (sc.creditable_id === data.creditable_id) && ((id === null) || (sc.id !== id))) {
growl.error(_t('app.admin.pricing.error_a_credit_linking_this_space_with_that_subscription_already_exists'));
if (!id) {
@ -459,7 +459,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
* Retrieve a price from prices array by a machineId and a groupId
*/
$scope.findPriceBy = function (prices, machineId, groupId) {
for (let price of Array.from(prices)) {
for (const price of Array.from(prices)) {
if ((price.priceable_id === machineId) && (price.group_id === groupId)) {
return price;
}
@ -603,7 +603,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
/**
* Load the next 10 coupons
*/
$scope.loadMore = function() {
$scope.loadMore = function () {
$scope.couponsPage++;
Coupon.query({ page: $scope.couponsPage, filter: $scope.filter.coupon }, function (data) {
$scope.coupons = $scope.coupons.concat(data);
@ -613,19 +613,19 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
/**
* Reset the list of coupons according to the newly selected filter
*/
$scope.updateCouponFilter = function() {
$scope.updateCouponFilter = function () {
$scope.couponsPage = 1;
Coupon.query({ page: $scope.couponsPage, filter: $scope.filter.coupon }, function (data) {
$scope.coupons = data;
});
}
};
/**
* Return the exemple price based on the configuration of the default slot duration.
* @param type {string} 'hourly_rate' | *
* @returns {number} price for "SLOT_DURATION" minutes.
*/
$scope.examplePrice = function(type) {
$scope.examplePrice = function (type) {
const hourlyRate = 10;
if (type === 'hourly_rate') {
@ -634,7 +634,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
const price = (hourlyRate / 60) * $scope.slotDuration;
return $filter('currency')(price);
}
};
/**
* Setup the feature-tour for the admin/pricing page.
@ -660,14 +660,16 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
content: _t('app.admin.tour.pricing.new_plan.content'),
placement: 'bottom'
});
uitour.createStep({
selector: '.plans-pricing .trainings-tab',
stepId: 'trainings',
order: 2,
title: _t('app.admin.tour.pricing.trainings.title'),
content: _t('app.admin.tour.pricing.trainings.content'),
placement: 'bottom'
});
if ($scope.$root.modules.trainings) {
uitour.createStep({
selector: '.plans-pricing .trainings-tab',
stepId: 'trainings',
order: 2,
title: _t('app.admin.tour.pricing.trainings.title'),
content: _t('app.admin.tour.pricing.trainings.content'),
placement: 'bottom'
});
}
uitour.createStep({
selector: '.plans-pricing .machines-tab',
stepId: 'machines',
@ -733,7 +735,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('pricing') < 0) {
uitour.start();
}
}
};
/* PRIVATE SCOPE */
@ -746,7 +748,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
// adds empty array for plan which hasn't any credits yet
return (function () {
const result = [];
for (let plan of Array.from($scope.plans)) {
for (const plan of Array.from($scope.plans)) {
if ($scope.trainingCreditsGroups[plan.id] == null) {
result.push($scope.trainingCreditsGroups[plan.id] = []);
} else {
@ -763,7 +765,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
* @param id {number}
* @returns {number} item index in the provided array
*/
var findItemIdxById = function (items, id) {
const findItemIdxById = function (items, id) {
return (items.map(function (item) { return item.id; })).indexOf(id);
};
@ -771,7 +773,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
* Group the given credits array into a map associating the plan ID with its associated trainings/machines
* @return {Object} the association map
*/
var groupCreditsByPlan = function (credits) {
const groupCreditsByPlan = function (credits) {
const creditsMap = {};
angular.forEach(credits, function (c) {
if (!creditsMap[c.plan_id]) {
@ -787,11 +789,11 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
* @param trainingId {number|string} training ID
* @param planId {number|string} plan ID
*/
var findTrainingCredit = function (trainingId, planId) {
const findTrainingCredit = function (trainingId, planId) {
trainingId = parseInt(trainingId);
planId = parseInt(planId);
for (let credit of Array.from($scope.trainingCredits)) {
for (const credit of Array.from($scope.trainingCredits)) {
if ((credit.plan_id === planId) && (credit.creditable_id === trainingId)) {
return credit;
}
@ -803,8 +805,8 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
* @param id {number} training ID
* @returns {Object} Training inherited from $resource
*/
var getTrainingFromId = function (id) {
for (let training of Array.from($scope.trainings)) {
const getTrainingFromId = function (id) {
for (const training of Array.from($scope.trainings)) {
if (training.id === parseInt(id)) {
return training;
}

View File

@ -76,6 +76,9 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
// active tab will be set here
$scope.selectedIndex = null;
// ui-bootstrap active tab index
$scope.selectedTab = 0;
// type filter binding
$scope.type = {
selected: null,
@ -135,7 +138,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
*/
$scope.customFieldName = function (field) {
return _t(`app.admin.statistics.${field}`);
}
};
/**
* Callback to open the datepicker (interval start)
@ -159,9 +162,11 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
* Callback called when the active tab is changed.
* recover the current tab and store its value in $scope.selectedIndex
* @param tab {Object} elasticsearch statistic structure (from statistic_indices table)
* @param index {number} index of the tab in the $scope.statistics array
*/
$scope.setActiveTab = function (tab) {
$scope.setActiveTab = function (tab, index) {
$scope.selectedIndex = tab;
$scope.selectedTab = index;
$scope.type.selected = tab.types[0];
$scope.type.active = $scope.type.selected;
$scope.customFilter.criterion = {};
@ -179,9 +184,10 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
*/
$scope.hiddenTab = function (tab) {
if (tab.table) {
if ((tab.es_type_key === 'subscription') && !$rootScope.modules.plans) {
return true;
} else return (tab.es_type_key === 'space') && !$rootScope.modules.spaces;
return ((tab.es_type_key === 'subscription' && !$rootScope.modules.plans) ||
(tab.es_type_key === 'training' && !$rootScope.modules.trainings) ||
(tab.es_type_key === 'space' && !$rootScope.modules.spaces)
);
} else {
return true;
}
@ -292,8 +298,8 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
return refreshStats();
} else {
return es.scroll({
'scroll': ES_SCROLL_TIME + 'm',
'body': { scrollId: $scope.scrollId }
scroll: ES_SCROLL_TIME + 'm',
body: { scrollId: $scope.scrollId }
}
, function (error, response) {
if (error) {
@ -335,7 +341,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
};
return $uibModal.open(options)
.result['finally'](null).then(function (info) { console.log(info); });
.result.finally(null).then(function (info) { console.log(info); });
};
/**
@ -391,7 +397,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('statistics') < 0) {
uitour.start();
}
}
};
/* PRIVATE SCOPE */
@ -406,6 +412,15 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
return $scope.preventRefresh = true;
}
});
// set the default tab to "machines" if "subscriptions" are disabled
if (!$rootScope.modules.plans) {
const idx = $scope.statistics.findIndex(s => s.es_type_key === 'machine');
$scope.setActiveTab($scope.statistics[idx], idx);
} else {
const idx = $scope.statistics.findIndex(s => s.es_type_key === 'subscription');
$scope.setActiveTab($scope.statistics[idx], idx);
}
};
/**
@ -471,15 +486,15 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
// run query
return es.search({
'index': 'stats',
'type': index,
'size': RESULTS_PER_PAGE,
'scroll': ES_SCROLL_TIME + 'm',
index: 'stats',
type: index,
size: RESULTS_PER_PAGE,
scroll: ES_SCROLL_TIME + 'm',
'stat-type': type,
'custom-query': custom ? JSON.stringify(Object.assign({ exclude: custom.exclude }, buildElasticCustomCriterion(custom))) : '',
'start-date': moment($scope.datePickerStart.selected).format(),
'end-date': moment($scope.datePickerEnd.selected).format(),
'body': buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
body: buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
}
, function (error, response) {
if (error) {
@ -503,19 +518,19 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
*/
const buildElasticDataQuery = function (type, custom, ageMin, ageMax, intervalBegin, intervalEnd, sortings) {
const q = {
'query': {
'bool': {
'must': [
query: {
bool: {
must: [
{
'term': {
'type': type
term: {
type: type
}
},
{
'range': {
'date': {
'gte': intervalBegin.format(),
'lte': intervalEnd.format()
range: {
date: {
gte: intervalBegin.format(),
lte: intervalEnd.format()
}
}
}
@ -526,10 +541,10 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
// optional date range
if ((typeof ageMin === 'number') && (typeof ageMax === 'number')) {
q.query.bool.must.push({
'range': {
'age': {
'gte': ageMin,
'lte': ageMax
range: {
age: {
gte: ageMin,
lte: ageMax
}
}
});
@ -539,7 +554,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
const criterion = buildElasticCustomCriterion(custom);
if (custom.exclude) {
q.query.bool.must_not = [
{ 'term': criterion.match }
{ term: criterion.match }
];
} else {
q.query.bool.must.push(criterion);
@ -547,24 +562,24 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
}
if (sortings) {
q['sort'] = buildElasticSortCriteria(sortings);
q.sort = buildElasticSortCriteria(sortings);
}
// aggregations (avg age & CA sum)
q['aggs'] = {
'total_ca': {
'sum': {
'field': 'ca'
q.aggs = {
total_ca: {
sum: {
field: 'ca'
}
},
'average_age': {
'avg': {
'field': 'age'
average_age: {
avg: {
field: 'age'
}
},
'total_stat': {
'sum': {
'field': 'stat'
total_stat: {
sum: {
field: 'stat'
}
}
};
@ -579,7 +594,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
const buildElasticCustomCriterion = function (custom) {
if (custom) {
const criterion = {
'match': {}
match: {}
};
switch ($scope.getCustomValueInputType($scope.customFilter.criterion)) {
case 'input_date': criterion.match[custom.key] = moment(custom.value).format('YYYY-MM-DD'); break;
@ -602,7 +617,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
angular.forEach(criteria, function (value, key) {
if ((typeof value !== 'undefined') && (value !== null) && (value !== 'none')) {
const c = {};
c[key] = { 'order': value };
c[key] = { order: value };
return crits.push(c);
}
});
@ -624,8 +639,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
// if no plans were created, there's no types for statisticIndex=subscriptions
if ($scope.type.active) {
$scope.filters.splice(4, 0, { key: 'subType', label: _t('app.admin.statistics.type'), values: $scope.type.active.subtypes })
$scope.filters.splice(4, 0, { key: 'subType', label: _t('app.admin.statistics.type'), values: $scope.type.active.subtypes });
if (!$scope.type.active.simple) {
const f = { key: 'stat', label: $scope.type.active.label, values: ['input_number'] };
@ -670,7 +684,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
]);
Application.Controllers.controller('ExportStatisticsController', [ '$scope', '$uibModalInstance', 'Export', 'dates', 'query', 'index', 'type', 'CSRF', 'growl', '_t',
Application.Controllers.controller('ExportStatisticsController', ['$scope', '$uibModalInstance', 'Export', 'dates', 'query', 'index', 'type', 'CSRF', 'growl', '_t',
function ($scope, $uibModalInstance, Export, dates, query, index, type, CSRF, growl, _t) {
// Retrieve Anti-CSRF tokens from cookies
CSRF.setMetaTags();
@ -739,14 +753,14 @@ Application.Controllers.controller('ExportStatisticsController', [ '$scope', '$u
if ($scope.export.type === 'global') {
$scope.actionUrl = '/stats/global/export';
return $scope.query = JSON.stringify({
'query': {
'bool': {
'must': [
query: {
bool: {
must: [
{
'range': {
'date': {
'gte': moment($scope.dates.start).format(),
'lte': moment($scope.dates.end).format()
range: {
date: {
gte: moment($scope.dates.start).format(),
lte: moment($scope.dates.end).format()
}
}
}
@ -766,8 +780,8 @@ Application.Controllers.controller('ExportStatisticsController', [ '$scope', '$u
$scope.exportData = function () {
const statusQry = { category: 'statistics', type: $scope.export.type, query: $scope.query };
if ($scope.export.type !== 'global') {
statusQry['type'] = index.key;
statusQry['key'] = type.key;
statusQry.type = index.key;
statusQry.key = type.key;
}
Export.status(statusQry).then(function (res) {

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

@ -35,12 +35,6 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc
linkIcon: 'cogs',
class: 'reserve-machine-link'
},
{
state: 'app.public.trainings_list',
linkText: 'app.public.common.trainings_registrations',
linkIcon: 'graduation-cap',
class: 'reserve-training-link'
},
{
state: 'app.public.events_list',
linkText: 'app.public.common.events_registrations',
@ -67,6 +61,15 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc
});
}
if ($scope.$root.modules.trainings) {
$scope.navLinks.splice(4, 0, {
state: 'app.public.trainings_list',
linkText: 'app.public.common.trainings_registrations',
linkIcon: 'graduation-cap',
class: 'reserve-training-link'
});
}
if ($scope.$root.modules.spaces) {
$scope.navLinks.splice(4, 0, {
state: 'app.public.spaces_list',
@ -90,12 +93,6 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc
linkIcon: 'cogs',
authorizedRoles: ['admin', 'manager']
},
{
state: 'app.admin.trainings',
linkText: 'app.public.common.trainings_monitoring',
linkIcon: 'graduation-cap',
authorizedRoles: ['admin', 'manager']
},
{
state: 'app.admin.events',
linkText: 'app.public.common.manage_the_events',
@ -147,6 +144,15 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc
$scope.adminNavLinks = adminNavLinks;
if ($scope.$root.modules.trainings) {
$scope.adminNavLinks.splice(3, 0, {
state: 'app.admin.trainings',
linkText: 'app.public.common.trainings_monitoring',
linkIcon: 'graduation-cap',
authorizedRoles: ['admin', 'manager']
});
}
if ($scope.$root.modules.spaces) {
$scope.adminNavLinks.splice(3, 0, {
state: 'app.public.spaces_list',

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

@ -37,7 +37,7 @@ angular.module('application.router', ['ui.router'])
logoFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-file' }).$promise; }],
logoBlackFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-black-file' }).$promise; }],
sharedTranslations: ['Translations', function (Translations) { return Translations.query(['app.shared', 'app.public.common']).$promise; }],
modulesPromise: ['Setting', function (Setting) { return Setting.query({ names: "['spaces_module', 'plans_module', 'invoicing_module', 'wallet_module', 'statistics_module']" }).$promise; }]
modulesPromise: ['Setting', function (Setting) { return Setting.query({ names: "['spaces_module', 'plans_module', 'invoicing_module', 'wallet_module', 'statistics_module', 'trainings_module']" }).$promise; }]
},
onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'modulesPromise', 'CSRF', function ($rootScope, logoFile, logoBlackFile, modulesPromise, CSRF) {
// Retrieve Anti-CSRF tokens from cookies
@ -48,6 +48,7 @@ angular.module('application.router', ['ui.router'])
$rootScope.modules = {
spaces: (modulesPromise.spaces_module === 'true'),
plans: (modulesPromise.plans_module === 'true'),
trainings: (modulesPromise.trainings_module === 'true'),
invoicing: (modulesPromise.invoicing_module === 'true'),
wallet: (modulesPromise.wallet_module === 'true'),
statistics: (modulesPromise.statistics_module === 'true')
@ -129,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; }]
}
})
@ -167,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', {
@ -458,6 +458,7 @@ angular.module('application.router', ['ui.router'])
// trainings
.state('app.public.trainings_list', {
url: '/trainings',
abstract: !Fablab.trainingsModule,
views: {
'main@': {
templateUrl: '/trainings/index.html',
@ -470,6 +471,7 @@ angular.module('application.router', ['ui.router'])
})
.state('app.public.training_show', {
url: '/trainings/:id',
abstract: !Fablab.trainingsModule,
views: {
'main@': {
templateUrl: '/trainings/show.html',
@ -482,6 +484,7 @@ angular.module('application.router', ['ui.router'])
})
.state('app.logged.trainings_reserve', {
url: '/trainings/:id/reserve',
abstract: !Fablab.trainingsModule,
views: {
'main@': {
templateUrl: '/trainings/reserve.html',
@ -652,6 +655,7 @@ angular.module('application.router', ['ui.router'])
// trainings
.state('app.admin.trainings', {
url: '/admin/trainings',
abstract: !Fablab.trainingsModule,
views: {
'main@': {
templateUrl: '/admin/trainings/index.html',
@ -666,6 +670,7 @@ angular.module('application.router', ['ui.router'])
})
.state('app.admin.trainings_new', {
url: '/admin/trainings/new',
abstract: !Fablab.trainingsModule,
views: {
'main@': {
templateUrl: '/admin/trainings/new.html',
@ -678,6 +683,7 @@ angular.module('application.router', ['ui.router'])
})
.state('app.admin.trainings_edit', {
url: '/admin/trainings/:id/edit',
abstract: !Fablab.trainingsModule,
views: {
'main@': {
templateUrl: '/admin/trainings/edit.html',
@ -909,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', {
@ -950,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', {
@ -962,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', {
@ -1054,8 +1060,8 @@ angular.module('application.router', ['ui.router'])
"'booking_move_enable', 'booking_move_delay', 'booking_cancel_enable', 'feature_tour_display', " +
"'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', " +
"'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', " +
"'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', 'trainings_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,5 +32,6 @@
@import "modules/payment-schedules-list";
@import "modules/stripe-confirm";
@import "modules/payment-schedule-dashboard";
@import "modules/plan-card";
@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

@ -6,7 +6,7 @@
<div class="modal-body" ng-show="step === 1">
<label class="m-t-sm" translate>{{ 'app.admin.calendar.what_kind_of_slot_do_you_want_to_create' }}</label>
<div class="form-group">
<div class="radio">
<div class="radio" ng-show="$root.modules.trainings">
<label>
<input type="radio" id="training" name="available_type" value="training" ng-model="availability.available_type">
<span translate>{{ 'app.admin.calendar.training' }}</span>

View File

@ -98,7 +98,7 @@
</section>
</uib-tab>
<uib-tab heading="{{ 'app.admin.members_edit.trainings' | translate }}">
<uib-tab heading="{{ 'app.admin.members_edit.trainings' | translate }}" ng-show="$root.modules.trainings">
<div class="col-md-4">
<div class="widget panel b-a m-t-lg">
<div class="panel-heading b-b ">

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

@ -31,7 +31,7 @@
<ng-include src="'/admin/pricing/subscriptions.html'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.pricing.trainings' | translate }}" index="1" class="trainings-tab">
<uib-tab heading="{{ 'app.admin.pricing.trainings' | translate }}" ng-show="$root.modules.trainings" index="1" class="trainings-tab">
<ng-include src="'/admin/pricing/trainings.html'"></ng-include>
</uib-tab>

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>
@ -457,6 +493,14 @@
label="app.admin.settings.enable_plans"
classes="m-l"></boolean-setting>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.trainings' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.trainings_info_html' | translate"></p>
<boolean-setting name="trainings_module"
settings="allSettings"
label="app.admin.settings.enable_trainings"
classes="m-l"></boolean-setting>
</div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.invoicing' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.invoicing_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

@ -102,7 +102,7 @@
</form>
<uib-tabset justified="true">
<uib-tab ng-repeat="stat in statistics" heading="{{stat.label}}" select="setActiveTab(stat)" ng-if="stat.graph && !(stat.es_type_key == 'subscription' && modules.plans)" class="row">
<uib-tab ng-repeat="(index, stat) in statistics" heading="{{stat.label}}" select="setActiveTab(stat, index)" ng-if="hiddenTab(stat)" index="index" class="row">
<div ng-if="stat.graph.chart_type == 'discreteBarChart'">
<div id="rankingFilters">

View File

@ -31,8 +31,8 @@
<div class="row">
<div class="col-md-12">
<uib-tabset justified="true">
<uib-tab ng-repeat="stat in statistics" heading="{{stat.label}}" select="setActiveTab(stat)" ng-hide="hiddenTab(stat)">
<uib-tabset justified="true" active="selectedTab">
<uib-tab ng-repeat="(index, stat) in statistics" heading="{{stat.label}}" select="setActiveTab(stat, index)" index="index" ng-hide="hiddenTab(stat)">
<form id="filters_form" name="filters_form" class="form-inline m-t-md m-b-lg" novalidate="novalidate">
<div id="agePickerPane" class="form-group datepicker-container" style="z-index:102;">
<button id="agePickerExpand" class="btn btn-default" type="button" ng-click="agePicker.show = !agePicker.show">

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

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

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]
online_payment_module stripe_public_key confirmation_required wallet_module trainings_module address_required]
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

@ -25,6 +25,7 @@
Fablab.plansModule = ('<%= Setting.get('plans_module') %>' === 'true');
Fablab.spacesModule = ('<%= Setting.get('spaces_module') %>' === 'true');
Fablab.trainingsModule = ('<%= Setting.get('trainings_module') %>' === 'true');
Fablab.walletModule = ('<%= Setting.get('wallet_module') %>' === 'true');
Fablab.statisticsModule = ('<%= Setting.get('statistics_module') %>' === 'true');
Fablab.defaultHost = "<%= Rails.application.secrets.default_host %>";

View File

@ -1,6 +1,13 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<p><%= t('.body.new_account_created') %> "<%= @attached_object&.profile&.full_name || t('api.notifications.deleted_user') %> &lt;<%= @attached_object.email%>&gt;"</p>
<p>
<%= t('.body.new_account_created') %>
"<%= @attached_object&.profile&.full_name || t('api.notifications.deleted_user') %> &lt;<%= @attached_object.email%>&gt;"
</p>
<p>
<%= t('.body.user_of_group_html', { GROUP: @attached_object&.group&.name }) %>
</p>
<% if @attached_object.invoicing_profile.organization %>
<p><%= t('.body.account_for_organization') %> <%= @attached_object.invoicing_profile.organization.name %></p>

View File

@ -1097,6 +1097,10 @@ de:
plans_info_html: "<p>Abonnements bieten eine Möglichkeit, Ihre Preise zu segmentieren und Vorteile für reguläre Benutzer zu bieten.</p><p><strong>Warnung:</strong> Es wird nicht empfohlen, die Abonnements zu deaktivieren, wenn mindestens ein Abonnement auf dem System aktiv ist.</p>"
enable_plans: "Pläne aktivieren"
plans_module: "Plan-Modul"
trainings: "Trainings"
trainings_info_html: "<p>Trainings are fully integrated into the Fab-manger's agenda. If enabled, your members will be able to book and pay trainings.</p><p>Trainings provides a way to prevent members to book some machines, if they do have not taken the prerequisite course.</p>"
enable_trainings: "Enable the trainings"
trainings_module: "trainings module"
invoicing: "Rechnungsstellung"
invoicing_info_html: "<p>Sie können das Rechnungsmodul komplett deaktivieren.</p><p>Das ist nützlich, wenn Sie über Ihr eigenes Rechnungssystem verfügen und nicht wollen, dass Fab-Manager Rechnungen generiert und an Mitglieder sendet.</p><p><strong>Warnung:</strong> Auch wenn Sie das Rechnungsmodul deaktivieren, müssen Sie die Mehrwertsteuer konfigurieren, um Fehler in Rechnungslegung und Preisen zu vermeiden. Die Konfiguration erfolgt in der Sektion « Rechnungen > Einstellungen ».</p>"
enable_invoicing: "Rechnungsstellung aktivieren"

View File

@ -1073,6 +1073,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"
@ -1097,6 +1098,10 @@ en:
plans_info_html: "<p>Subscriptions provide a way to segment your prices and provide benefits to regular users.</p><p><strong>Warning:</strong> It is not recommended to disable plans if at least one subscription is active on the system.</p>"
enable_plans: "Enable the plans"
plans_module: "plans module"
trainings: "Trainings"
trainings_info_html: "<p>Trainings are fully integrated into the Fab-manger's agenda. If enabled, your members will be able to book and pay trainings.</p><p>Trainings provides a way to prevent members to book some machines, if they do have not taken the prerequisite course.</p>"
enable_trainings: "Enable the trainings"
trainings_module: "trainings module"
invoicing: "Invoicing"
invoicing_info_html: "<p>You can fully disable the invoicing module.</p><p>This is useful if you have your own invoicing system, and you don't want Fab-manager generates and sends invoices to the members.</p><p><strong>Warning:</strong> even if you disable the invoicing module, you must to configure the VAT to prevent errors in accounting and prices. Do it from the « Invoices > Invoicing settings » section.</p>"
enable_invoicing: "Enable invoicing"
@ -1105,6 +1110,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

@ -1097,6 +1097,10 @@ es:
plans_info_html: "<p>Subscriptions provide a way to segment your prices and provide benefits to regular users.</p><p><strong>Warning:</strong> It is not recommended to disable plans if at least one subscription is active on the system.</p>"
enable_plans: "Enable the plans"
plans_module: "plans module"
trainings: "Trainings"
trainings_info_html: "<p>Trainings are fully integrated into the Fab-manger's agenda. If enabled, your members will be able to book and pay trainings.</p><p>Trainings provides a way to prevent members to book some machines, if they do have not taken the prerequisite course.</p>"
enable_trainings: "Enable the trainings"
trainings_module: "trainings module"
invoicing: "Invoicing"
invoicing_info_html: "<p>You can fully disable the invoicing module.</p><p>This is useful if you have your own invoicing system, and you don't want Fab-manager generates and sends invoices to the members.</p><p><strong>Warning:</strong> even if you disable the invoicing module, you must to configure the VAT to prevent errors in accounting and prices. Do it from the « Invoices > Invoicing settings » section.</p>"
enable_invoicing: "Enable invoicing"

View File

@ -1073,6 +1073,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"
@ -1097,6 +1098,10 @@ fr:
plans_info_html: "<p>Les abonnements offrent un moyen de segmenter vos tarifs et d'accorder des avantages aux utilisateurs réguliers.</p><p><strong>Attention :</strong> Il n'est pas recommandé de désactiver les abonnements si au moins un abonnement est en cours.</p>"
enable_plans: "Activer les abonnements"
plans_module: "module abonnements"
trainings: "Formations"
trainings_info_html: "<p>Les formations sont entièrement intégrées dans l'agenda de Fab-manger. Si elles sont activées, vos membres pourrons réserver et payer des formations.</p><p>Les formations fournissent une solution pour éviter que des membres ne réservent des machines, sans avoir suivi la formation préalable.</p>"
enable_trainings: "Activer les formations"
trainings_module: "module formations"
invoicing: "Facturation"
invoicing_info_html: "<p>Vous pouvez complètement désactiver le module de facturation.</p><p>Cela est utile si vous possédez votre propre système de facturation, et que vous ne souhaitez pas que Fab-manager génère et envoie des factures aux membres.</p><p><strong>Attention :</strong> même si vous désactivez le module de facturation, vous devez configurer la TVA pour éviter des erreurs de prix et de comptabilité. Faites le depuis la section « Factures > Paramètres de facturation ».</p>"
enable_invoicing: "Activer la facturation"
@ -1105,6 +1110,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

@ -1097,6 +1097,10 @@ pt:
plans_info_html: "<p>As assinaturas fornecem uma maneira de segmentar seus preços e proporcionar benefícios aos usuários normais.</p><p><strong>Aviso:</strong> não é recomendável desativar os planos se pelo menos uma assinatura estiver ativa no sistema.</p>"
enable_plans: "Ativar os planos"
plans_module: "módulo de planos"
trainings: "Trainings"
trainings_info_html: "<p>Trainings are fully integrated into the Fab-manger's agenda. If enabled, your members will be able to book and pay trainings.</p><p>Trainings provides a way to prevent members to book some machines, if they do have not taken the prerequisite course.</p>"
enable_trainings: "Enable the trainings"
trainings_module: "trainings module"
invoicing: "Faturamento"
invoicing_info_html: "<p>Você pode desativar completamente o módulo de faturamento.</p><p>Isso é útil se você tiver o seu próprio sistema de faturação, e não quer que o Fab-manager gere e envie faturas para os membros.</p><p><strong>Aviso:</strong> mesmo se você desativar o módulo de faturação, você deve configurar o IVA para evitar erros na contabilidade e nos preços. Faça isso na seção « Faturas > Configurações de faturação ».</p>"
enable_invoicing: "Habilitar faturamento"

View File

@ -1097,6 +1097,10 @@ zu:
plans_info_html: "crwdns20664:0crwdne20664:0"
enable_plans: "crwdns20666:0crwdne20666:0"
plans_module: "crwdns20668:0crwdne20668:0"
trainings: "crwdns21436:0crwdne21436:0"
trainings_info_html: "crwdns21438:0crwdne21438:0"
enable_trainings: "crwdns21440:0crwdne21440:0"
trainings_module: "crwdns21442:0crwdne21442:0"
invoicing: "crwdns20670:0crwdne20670:0"
invoicing_info_html: "crwdns20672:0crwdne20672:0"
enable_invoicing: "crwdns20674:0crwdne20674:0"

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

@ -100,6 +100,7 @@ de:
subject: "Ein Benutzerkonto wurde erstellt"
body:
new_account_created: "Ein neues Benutzerkonto wurde auf der Website erstellt:"
user_of_group_html: "The user has registered in the group <strong>%{GROUP}</strong>"
account_for_organization: "Dieses Konto verwaltet eine Organisation:"
notify_admin_subscribed_plan:
subject: "Ein Abonnement wurde gekauft"

View File

@ -100,6 +100,7 @@ en:
subject: "A user account has been created"
body:
new_account_created: "A new user account has been created on the website:"
user_of_group_html: "The user has registered in the group <strong>%{GROUP}</strong>"
account_for_organization: "This account manage an organization:"
notify_admin_subscribed_plan:
subject: "A subscription has been purchased"

View File

@ -100,6 +100,7 @@ es:
subject: "Se ha creado una nueva cuenta"
body:
new_account_created: "Se ha creado un nuevo usuario en la web:"
user_of_group_html: "The user has registered in the group <strong>%{GROUP}</strong>"
account_for_organization: "Esta cuenta gestiona una organización :"
notify_admin_subscribed_plan:
subject: "Se ha adquirido un plan de suscripción"

View File

@ -100,6 +100,7 @@ fr:
subject: "Un compte utilisateur a été créé"
body:
new_account_created: "Un nouveau compte utilisateur vient d'être créé sur la plateforme :"
user_of_group_html: "L'utilisateur s'est inscrit dans le groupe <strong>%{GROUP}</strong>"
account_for_organization: "Ce compte gère une structure :"
notify_admin_subscribed_plan:
subject: "Un abonnement a été souscrit"

View File

@ -100,6 +100,7 @@ pt:
subject: "A conta de usuário foi criada"
body:
new_account_created: "Uma nova conta de usuário foi criada no site:"
user_of_group_html: "The user has registered in the group <strong>%{GROUP}</strong>"
account_for_organization: "Esta conta gerencia uma organização:"
notify_admin_subscribed_plan:
subject: "Uma assinatura foi comprada"

View File

@ -100,6 +100,7 @@ zu:
subject: "crwdns3983:0crwdne3983:0"
body:
new_account_created: "crwdns3985:0crwdne3985:0"
user_of_group_html: "crwdns21434:0%{GROUP}crwdne21434:0"
account_for_organization: "crwdns3987:0crwdne3987:0"
notify_admin_subscribed_plan:
subject: "crwdns3989:0crwdne3989:0"

View File

@ -893,6 +893,8 @@ Setting.set('statistics_module', true) unless Setting.find_by(name: 'statistics_
Setting.set('upcoming_events_shown', 'until_start') unless Setting.find_by(name: 'upcoming_events_shown').try(:value)
Setting.set('trainings_module', true) unless Setting.find_by(name: 'trainings_module').try(:value)
if StatisticCustomAggregation.count.zero?
# available reservations hours for machines
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2)

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

@ -1,6 +1,6 @@
{
"name": "fab-manager",
"version": "4.7.5",
"version": "4.7.6",
"description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.",
"keywords": [
"fablab",

View File

@ -0,0 +1,90 @@
#!/usr/bin/env bash
yq() {
docker run --rm -i -v "${NGINX_PATH}:/workdir" mikefarah/yq:4 "$@"
}
config()
{
if [ "$(whoami)" = "root" ]
then
echo "It is not recommended to run this script as root. As a normal user, elevation will be prompted if needed."
[[ "$YES_ALL" = "true" ]] && confirm="y" || read -rp "Continue anyway? (Y/n) " confirm </dev/tty
if [[ "$confirm" = "n" ]]; then exit 1; fi
else
if ! groups | grep docker; then
echo "Please add your current user to the docker group."
echo "You can run the following as root: \"usermod -aG docker $(whoami)\", then logout and login again"
echo "current user is not allowed to use docker, exiting..."
exit 1
fi
fi
NGINX_PATH=$(pwd)
TYPE="NOT-FOUND"
[[ "$YES_ALL" = "true" ]] && confirm="y" || read -rp "Is Fab-manager installed at \"$NGINX_PATH\"? (y/N) " confirm </dev/tty
if [ "$confirm" = "y" ]; then
test_docker_compose
while [[ "$TYPE" = "NOT-FOUND" ]]
do
echo "nginx was not found at the current path, please specify the nginx installation path..."
read -e -rp "> " nginxpath </dev/tty
NGINX_PATH="${nginxpath}"
test_docker_compose
done
else
echo "Please run this script from the Fab-manager's installation folder"
exit 1
fi
SERVICE="$(yq eval '.services.*.image | select(. == "nginx*") | path | .[-2]' docker-compose.yml)"
}
test_docker_compose()
{
if [[ -f "$NGINX_PATH/docker-compose.yml" ]]
then
docker-compose -f "$NGINX_PATH/docker-compose.yml" ps | grep nginx
if [[ $? = 0 ]]
then
printf "nginx found at %s\n" "$NGINX_PATH"
TYPE="DOCKER-COMPOSE"
fi
fi
}
proceed_upgrade()
{
files=()
while IFS= read -r -d $'\0'; do
files+=("$REPLY")
done < <(find "$NGINX_PATH" -name "*.conf" -print0 2>/dev/null)
for file in "${files[@]}"; do
read -rp "Process \"$file\" (y/N)? " confirm </dev/tty
if [[ "$confirm" = "y" ]]; then
sed -i.bak -e 's:location ^~ /assets/ {:location ^~ /packs/ {:g' "$file"
echo "$file was successfully upgraded"
fi
done
}
docker_restart()
{
docker-compose -f "$NGINX_PATH/docker-compose.yml" restart "$SERVICE"
}
function trap_ctrlc()
{
echo "Ctrl^C, exiting..."
exit 2
}
upgrade_directive()
{
trap "trap_ctrlc" 2 # SIGINT
config
proceed_upgrade
docker_restart
printf "upgrade complete\n"
}
upgrade_directive "$@"

View File

@ -9,7 +9,7 @@ SLOT_DURATION=60
FEATURE_TOUR_DISPLAY=once
DEFAULT_HOST=demo.fab-manager.com
DEFAULT_PROTOCOL=http
DEFAULT_PROTOCOL=https
DELIVERY_METHOD=smtp
SMTP_ADDRESS=smtp.sendgrid.net

View File

@ -7,7 +7,7 @@ server {
server_name MAIN_DOMAIN;
root /usr/src/app/public;
location ^~ /assets/ {
location ^~ /packs/ {
gzip_static on;
expires max;
add_header Cache-Control public;
@ -43,7 +43,7 @@ server {
root /usr/src/app/public/;
rewrite ^(.*)$ /maintenance.html break;
}
location /.well-known/acme-challenge {
root /etc/letsencrypt/webrootauth;
default_type "text/plain";

View File

@ -28,7 +28,7 @@ server {
ssl_stapling_verify on;
location ^~ /assets/ {
location ^~ /packs/ {
gzip_static on;
expires max;
add_header Cache-Control public;

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
@ -68,16 +112,18 @@ config()
{
SERVICE="fabmanager"
echo 'We recommend nginx to serve the application over the network (internet). You can use your own solution or let this script install and configure nginx for Fab-manager.'
printf 'If you want to setup install Fab-manager behind a reverse proxy, you may not need to install the integrated nginx.\n'
read -rp 'Do you want install nginx? (Y/n) ' NGINX </dev/tty
if [ "$NGINX" != "n" ]; then
# if the user doesn't want nginx, let him use its own solution for HTTPS
printf "\n\nWe recommend let's encrypt to secure the application with HTTPS. You can use your own certificate or let this script install and configure let's encrypt for Fab-manager.\n"
printf "\n\nWe highly recommend to secure the application with HTTPS. You can use your own certificate or let this script install and configure let's encrypt for Fab-manager."
printf "\nIf this server is publicly available on the internet, you can use Let's encrypt to automatically generate and renew a valid SSL certificate for free.\n"
read -rp "Do you want install let's encrypt? (Y/n) " LETSENCRYPT </dev/tty
if [ "$LETSENCRYPT" != "n" ]; then
printf "\n\nLet's encrypt requires an email address to receive notifications about certificate expiration.\n"
read_email
fi
# if the user doesn't want nginx, let him configure his own solution
# if the user wants to install nginx, configure the domains
printf "\n\nWhat's the domain name where the instance will be active (eg. fab-manager.com)?\n"
read_domain
MAIN_DOMAIN=("${DOMAINS[0]}")
@ -111,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"
@ -162,7 +208,8 @@ prepare_nginx()
# if nginx is not installed, remove its associated block from docker-compose.yml
echo "Removing nginx..."
yq -i eval 'del(.services.nginx)' docker-compose.yml
read -rp "Do you want to map the Fab-manager's service to an external network? (Y/n) " confirm </dev/tty
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
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
@ -172,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"
@ -202,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
}
@ -327,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

@ -67,7 +67,7 @@ version_error()
version_check()
{
VERSION=$(docker-compose exec -T "$SERVICE" cat .fabmanager-version)
VERSION=$(docker-compose exec -T "$SERVICE" cat .fabmanager-version 2>/dev/null)
if [[ $? = 1 ]]; then
VERSION=$(docker-compose exec -T "$SERVICE" cat package.json | jq -r '.version')
fi
@ -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

View File

@ -2041,10 +2041,10 @@ bluebird@^3.5.5:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
version "4.11.9"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
bn.js@^5.1.1:
version "5.1.3"
@ -2125,7 +2125,7 @@ braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
brorand@^1.0.1:
brorand@^1.0.1, brorand@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
@ -3404,17 +3404,17 @@ electron-to-chromium@^1.3.562:
integrity sha512-WhRe6liQ2q/w1MZc8mD8INkenHivuHdrr4r5EQHNomy3NJux+incP6M6lDMd0paShP3MD0WGe5R1TWmEClf+Bg==
elliptic@^6.5.3:
version "6.5.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
dependencies:
bn.js "^4.4.0"
brorand "^1.0.1"
bn.js "^4.11.9"
brorand "^1.1.0"
hash.js "^1.0.0"
hmac-drbg "^1.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
hmac-drbg "^1.0.1"
inherits "^2.0.4"
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
emoji-regex@^7.0.1:
version "7.0.3"
@ -4527,7 +4527,7 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
hmac-drbg@^1.0.0:
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
@ -5864,7 +5864,7 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=