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:
commit
4de184fb19
23
CHANGELOG.md
23
CHANGELOG.md
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -49,6 +49,7 @@ class API::AuthProvidersController < API::ApiController
|
||||
def active
|
||||
authorize AuthProvider
|
||||
@provider = AuthProvider.active
|
||||
@previous = AuthProvider.previous
|
||||
end
|
||||
|
||||
|
||||
|
@ -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]
|
||||
},
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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 = {
|
||||
|
@ -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 */
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}],
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
102
app/frontend/src/stylesheets/modules/plan-card.scss
Normal file
102
app/frontend/src/stylesheets/modules/plan-card.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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 ">
|
||||
|
@ -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}}" />
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
##
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 %>";
|
||||
|
@ -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') %> <<%= @attached_object.email%>>"</p>
|
||||
<p>
|
||||
<%= t('.body.new_account_created') %>
|
||||
"<%= @attached_object&.profile&.full_name || t('api.notifications.deleted_user') %> <<%= @attached_object.email%>>"
|
||||
</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>
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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",
|
||||
|
90
scripts/nginx-packs-directive.sh
Normal file
90
scripts/nginx-packs-directive.sh
Normal 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 "$@"
|
@ -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
|
||||
|
@ -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";
|
||||
|
@ -28,7 +28,7 @@ server {
|
||||
ssl_stapling_verify on;
|
||||
|
||||
|
||||
location ^~ /assets/ {
|
||||
location ^~ /packs/ {
|
||||
gzip_static on;
|
||||
expires max;
|
||||
add_header Cache-Control public;
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
6
test/fixtures/stylesheets.yml
vendored
6
test/fixtures/stylesheets.yml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
32
yarn.lock
32
yarn.lock
@ -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=
|
||||
|
Loading…
x
Reference in New Issue
Block a user