1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

Merge branch 'dev' into payzen

This commit is contained in:
Sylvain 2021-03-10 15:23:56 +01:00
commit 141f4f31b1
36 changed files with 345 additions and 127 deletions

View File

@ -3,6 +3,20 @@
## Next release
- [TODO DEPLOY] `rails fablab:stripe:set_gateway`
## Next release (v4.7.6)
- Ability to disable the trainings module
- 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
- Fix a bug: subscriptions tab is selected by default in statistics, even if the module is disabled
- 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`
## 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
## v4.7.4 2021 March 08
- Show remaining training credits in the dashboard
- Allow writing short rich descriptions for each subscription plan

View File

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

View File

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

View File

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

View File

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

View File

@ -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')
@ -458,6 +459,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 +472,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 +485,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 +656,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 +671,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 +684,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',
@ -1054,7 +1061,7 @@ 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', " +
"'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', 'trainings_module', " +
"'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', " +
"'link_name', 'home_content', 'home_css', 'phone_required', 'upcoming_events_shown']"
}).$promise;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 payment_gateway]
online_payment_module stripe_public_key confirmation_required wallet_module trainings_module payment_gateway]
end
##

View File

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

View File

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

View File

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

View File

@ -1097,6 +1097,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"

View File

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

View File

@ -1097,6 +1097,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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -68,16 +68,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]}")
@ -162,6 +164,7 @@ 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
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..."

View File

@ -67,7 +67,7 @@ version_error()
version_check()
{
VERSION=$(docker-compose exec -T "$SERVICE" cat .fabmanager-version)
VERSION=$(docker-compose exec -T "$SERVICE" cat .fabmanager-version 2>/dev/null)
if [[ $? = 1 ]]; then
VERSION=$(docker-compose exec -T "$SERVICE" cat package.json | jq -r '.version')
fi
@ -95,6 +95,12 @@ add_environments()
done
}
clean_env_file()
{
# docker run --env-file does not support whitespaces in the environment variables so we must clean the file
sed -ri 's/^([A-Z0-9_]+)\s*=\s*(.*)$/\1=\2/g' ./config/env
}
compile_assets()
{
IMAGE=$(yq eval '.services.*.image | select(. == "sleede/fab-manager*")' docker-compose.yml)
@ -106,6 +112,7 @@ compile_assets()
exit 1
fi
PG_NET_ID=$(docker inspect "$PG_ID" -f "{{json .NetworkSettings.Networks }}" | jq -r '.[] .NetworkID')
clean_env_file
# shellcheck disable=SC2068
docker run --rm --env-file ./config/env ${ENV_ARGS[@]} --link "$PG_ID" --net "$PG_NET_ID" -v "${PWD}/public/new_packs:/usr/src/app/public/packs" "$IMAGE" bundle exec rake assets:precompile
docker-compose down

View File

@ -2048,10 +2048,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"
@ -2132,7 +2132,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=
@ -3411,17 +3411,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"
@ -4539,7 +4539,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=
@ -5876,7 +5876,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=