1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-29 10:24:20 +01:00

Merge branch 'dev' into l10n_dev

This commit is contained in:
Sylvain 2021-10-04 17:05:59 +02:00 committed by GitHub
commit ef989ec341
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 168 additions and 75 deletions

View File

@ -1,11 +1,31 @@
# Changelog Fab-manager # Changelog Fab-manager
- The upgrade script will check and report the ability to access the hub API
- Fix a bug: the upgrade script report an invalid version to upgrade to
- Fix a security issue: updated tar to 6.1.11 to fix [CVE-2021-37712](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-37712), [CVE-2021-37701](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-37701) and [CVE-2021-37713](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-37713)
- Fix a security issue: updated immer to 9.0.6 to fix [CVE-2021-3757](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3757) and [CVE-2021-23436](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-23436)
- Fix a security issue: updated url-parse to 1.5.3 to fix [CVE-2021-3664](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3664)
- Fix a security issue: updated axios to 0.21.2 to fix [CVE-2021-3749](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3749)
- Fix a security issue: updated nokogiri to 1.12.5 to fix [CVE-2021-41098](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-41098)
## v5.1.10 2021 October 04
- Fix a bug: the image of the about page is not using the image set in backoffice
- Fix a bug: updated sassc to 2.4.0 to fix ruby runtime error on some CPU architectures (#270)
- Fix a security issue: prevent HTML code edition in projects, to prevent XSS vulnerability (#293)
## v5.1.9 2021 September 21
- Add a setting for the purchase and use of a prepaid pack is only possible for the user with a valid subscription
- Fix a bug: unable to show plan name in calendar reservations
- Fix a bug: book overlapping slot setting label name
## v5.1.8 2021 September 13 ## v5.1.8 2021 September 13
- Improved stripe 3D secure payment on payment schedules - Improved stripe 3D secure payment on payment schedules
- Disable monthly payment for the subscription with interval 1 month - Disable monthly payment for the subscription with interval 1 month
- Fix a bug: unable to show statistics module in nav menu after login - Fix a bug: unable to show statistics module in nav menu after login
- Fix a bug: plans page show an error if admin dont create any plans - Fix a bug: plans page show an error if admin don't create any plans
## v5.1.7 2021 August 24 ## v5.1.7 2021 August 24
@ -14,12 +34,12 @@
## v5.1.6 2021 August 6 ## v5.1.6 2021 August 6
- Adjuste packs pricing popover position - Adjust packs pricing popover position
- Updated Norwegian language - Updated Norwegian language
- Updated addressable from 2.7.0 to 2.8.0 - Updated addressable from 2.7.0 to 2.8.0
- Updated tar from 6.1.0 to 6.1.4 - Updated tar from 6.1.0 to 6.1.4
- Fix a bug: unable to generate avoir of wallet - Fix a bug: unable to generate avoir of wallet
- Fix a bug: manager cant reserve training for user - Fix a bug: managers can't reserve trainings for users
## v5.1.5 2021 August 2 ## v5.1.5 2021 August 2
@ -60,6 +80,22 @@
- [TODO DEPLOY] `rails db:seed` - [TODO DEPLOY] `rails db:seed`
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet` - [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`
## v5.0.14 2021 September 30
- Fix a bug: unable to show plan name in calendar reservations
- Fix a bug: book overlapping slot setting labal name
## v5.0.13 2021 September 13
- Improved stripe 3D secure payment on payment schedules
- Disable monthly payment for the subscription with interval 1 month
- Fix a bug: unable to show statistics module in nav menu after login
- Fix a bug: plans page show an error if admin dont create any plans
## v5.0.12 2021 August 24
- Fix a bug: unable to show plans page
## v5.0.11 2021 August 6 ## v5.0.11 2021 August 6
- Fix a bug: unable to generate avoir of wallet - Fix a bug: unable to generate avoir of wallet

View File

@ -140,7 +140,7 @@ GEM
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
faraday (0.17.3) faraday (0.17.3)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
ffi (1.15.1) ffi (1.15.4)
foreman (0.87.0) foreman (0.87.0)
forgery (0.7.0) forgery (0.7.0)
friendly_id (5.1.0) friendly_id (5.1.0)
@ -205,7 +205,7 @@ GEM
rake rake
mini_magick (4.10.1) mini_magick (4.10.1)
mini_mime (1.1.0) mini_mime (1.1.0)
mini_portile2 (2.5.1) mini_portile2 (2.6.1)
minitest (5.14.4) minitest (5.14.4)
minitest-reporters (1.4.2) minitest-reporters (1.4.2)
ansi ansi
@ -217,8 +217,8 @@ GEM
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.1.1) multipart-post (2.1.1)
nio4r (2.5.7) nio4r (2.5.7)
nokogiri (1.11.4) nokogiri (1.12.5)
mini_portile2 (~> 2.5.0) mini_portile2 (~> 2.6.1)
racc (~> 1.4) racc (~> 1.4)
notify_with (0.0.2) notify_with (0.0.2)
jbuilder (~> 2.0) jbuilder (~> 2.0)
@ -341,7 +341,7 @@ GEM
rubyzip (>= 1.3.0) rubyzip (>= 1.3.0)
rubyzip (2.3.0) rubyzip (2.3.0)
safe_yaml (1.0.5) safe_yaml (1.0.5)
sassc (2.2.1) sassc (2.4.0)
ffi (~> 1.9) ffi (~> 1.9)
seed_dump (3.3.1) seed_dump (3.3.1)
activerecord (>= 4) activerecord (>= 4)

View File

@ -9,6 +9,8 @@ import MachineAPI from '../../api/machine';
import { Machine } from '../../models/machine'; import { Machine } from '../../models/machine';
import { User } from '../../models/user'; import { User } from '../../models/user';
import { IApplication } from '../../models/application'; import { IApplication } from '../../models/application';
import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
declare const Application: IApplication; declare const Application: IApplication;
@ -36,11 +38,17 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
const [pendingTraining, setPendingTraining] = useState<boolean>(false); const [pendingTraining, setPendingTraining] = useState<boolean>(false);
const [trainingRequired, setTrainingRequired] = useState<boolean>(false); const [trainingRequired, setTrainingRequired] = useState<boolean>(false);
const [proposePacks, setProposePacks] = useState<boolean>(false); const [proposePacks, setProposePacks] = useState<boolean>(false);
const [isPackOnlyForSubscription, setIsPackOnlyForSubscription] = useState<boolean>(true);
// handle login from an external process // handle login from an external process
useEffect(() => setUser(currentUser), [currentUser]); useEffect(() => setUser(currentUser), [currentUser]);
// check the trainings after we retrieved the machine data // check the trainings after we retrieved the machine data
useEffect(() => checkTraining(), [machine]); useEffect(() => checkTraining(), [machine]);
useEffect(() => {
SettingAPI.get(SettingName.PackOnlyForSubscription)
.then(data => setIsPackOnlyForSubscription(data.value === 'true'))
.catch(error => onError(error));
}, []);
/** /**
* Callback triggered when the user clicks on the 'reserve' button. * Callback triggered when the user clicks on the 'reserve' button.
@ -136,8 +144,9 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
*/ */
const checkPrepaidPack = (): void => { const checkPrepaidPack = (): void => {
// if the customer has already bought a pack or if there's no active packs for this machine, // if the customer has already bought a pack or if there's no active packs for this machine,
// or customer has not any subscription if admin active pack only for subscription option
// let the customer reserve // let the customer reserve
if (machine.current_user_has_packs || !machine.has_prepaid_packs_for_current_user) { if (machine.current_user_has_packs || !machine.has_prepaid_packs_for_current_user || (isPackOnlyForSubscription && !currentUser.subscribed_plan)) {
return onReserveMachine(machine); return onReserveMachine(machine);
} }

View File

@ -36,11 +36,15 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
const [userPacks, setUserPacks] = useState<Array<UserPack>>(null); const [userPacks, setUserPacks] = useState<Array<UserPack>>(null);
const [threshold, setThreshold] = useState<number>(null); const [threshold, setThreshold] = useState<number>(null);
const [packsModal, setPacksModal] = useState<boolean>(false); const [packsModal, setPacksModal] = useState<boolean>(false);
const [isPackOnlyForSubscription, setIsPackOnlyForSubscription] = useState<boolean>(true);
useEffect(() => { useEffect(() => {
SettingAPI.get(SettingName.RenewPackThreshold) SettingAPI.get(SettingName.RenewPackThreshold)
.then(data => setThreshold(parseFloat(data.value))) .then(data => setThreshold(parseFloat(data.value)))
.catch(error => onError(error)); .catch(error => onError(error));
SettingAPI.get(SettingName.PackOnlyForSubscription)
.then(data => setIsPackOnlyForSubscription(data.value === 'true'))
.catch(error => onError(error));
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -127,13 +131,32 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
if (_.isEmpty(customer)) return <div />; if (_.isEmpty(customer)) return <div />;
// prevent component rendering if ths customer have no packs and there are no packs available // prevent component rendering if ths customer have no packs and there are no packs available
if (totalHours() === 0 && packs?.length === 0) return <div/>; if (totalHours() === 0 && packs?.length === 0) return <div/>;
// render remaining hours and a warning if customer has not any subscription if admin active pack only for subscription option
if (totalHours() > 0) {
return (
<div className="packs-summary">
<h3>{t('app.logged.packs_summary.prepaid_hours')}</h3>
<div className="content">
<span className="remaining-hours">
{t('app.logged.packs_summary.remaining_HOURS', { HOURS: totalHours(), ITEM: itemType })}
{isPackOnlyForSubscription && !customer.subscribed_plan &&
<div className="alert alert-warning m-t m-b">
{t('app.logged.packs_summary.unable_to_use_pack_for_subsription_is_expired')}
</div>
}
</span>
</div>
</div>
);
}
// prevent component rendering buy pack button if customer has not any subscription if admin active pack only for subscription option
if (isPackOnlyForSubscription && !customer.subscribed_plan) return <div/>;
return ( return (
<div className="packs-summary"> <div className="packs-summary">
<h3>{t('app.logged.packs_summary.prepaid_hours')}</h3> <h3>{t('app.logged.packs_summary.prepaid_hours')}</h3>
<div className="content"> <div className="content">
<span className="remaining-hours"> <span className="remaining-hours">
{totalHours() > 0 && t('app.logged.packs_summary.remaining_HOURS', { HOURS: totalHours(), ITEM: itemType })}
{totalHours() === 0 && t('app.logged.packs_summary.no_hours', { ITEM: itemType })} {totalHours() === 0 && t('app.logged.packs_summary.no_hours', { ITEM: itemType })}
</span> </span>
{shouldDisplayButton() && <div className="button-wrapper"> {shouldDisplayButton() && <div className="button-wrapper">

View File

@ -956,14 +956,14 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui
}); });
$scope.tagsName = localizedList(tags); $scope.tagsName = localizedList(tags);
if ($scope.isOnlySubscriptions && $scope.selectedPlans.length > 0) { if ($scope.isOnlySubscriptions && $scope.selectedPlans.length > 0) {
$scope.plansName = localizedList($scope.selectedPlans); $scope.plansName = localizedList($scope.selectedPlans, 'base_name');
} }
}; };
const localizedList = function (items) { const localizedList = function (items, attr = 'name') {
if (items.length === 0) return `<span class="text-gray text-italic">${_t('app.admin.calendar.none')}</span>`; if (items.length === 0) return `<span class="text-gray text-italic">${_t('app.admin.calendar.none')}</span>`;
const names = items.map(function (i) { return $sce.trustAsHtml(`<strong>${i.name}</strong>`); }); const names = items.map(function (i) { return $sce.trustAsHtml(`<strong>${i[attr]}</strong>`); });
if (items.length > 1) return names.slice(0, -1).join(', ') + ` ${_t('app.admin.calendar.and')} ` + names[names.length - 1]; if (items.length > 1) return names.slice(0, -1).join(', ') + ` ${_t('app.admin.calendar.and')} ` + names[names.length - 1];
return names[0]; return names[0];

View File

@ -22,6 +22,7 @@
* in the various projects' admin controllers. * in the various projects' admin controllers.
* *
* Provides : * Provides :
* - $scope.summernoteOptsProject
* - $scope.totalSteps * - $scope.totalSteps
* - $scope.machines = [{Machine}] * - $scope.machines = [{Machine}]
* - $scope.components = [{Component}] * - $scope.components = [{Component}]
@ -42,7 +43,11 @@
* - $state (Ui-Router) [ 'app.public.projects_show', 'app.public.projects_list' ] * - $state (Ui-Router) [ 'app.public.projects_show', 'app.public.projects_list' ]
*/ */
class ProjectsController { class ProjectsController {
constructor ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t) { constructor ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t) {
// remove codeview from summernote editor
$scope.summernoteOptsProject = angular.copy($rootScope.summernoteOpts);
$scope.summernoteOptsProject.toolbar[6][1].splice(1, 1);
// Retrieve the list of machines from the server // Retrieve the list of machines from the server
Machine.query().$promise.then(function (data) { Machine.query().$promise.then(function (data) {
$scope.machines = data.map(function (d) { $scope.machines = data.map(function (d) {
@ -449,8 +454,8 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
/** /**
* Controller used in the project creation page * Controller used in the project creation page
*/ */
Application.Controllers.controller('NewProjectController', ['$scope', '$state', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'Diacritics', 'dialogs', 'allowedExtensions', '_t', Application.Controllers.controller('NewProjectController', ['$rootScope', '$scope', '$state', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', '$document', 'CSRF', 'Diacritics', 'dialogs', 'allowedExtensions', '_t',
function ($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, Diacritics, dialogs, allowedExtensions, _t) { function ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, CSRF, Diacritics, dialogs, allowedExtensions, _t) {
CSRF.setMetaTags(); CSRF.setMetaTags();
// API URL where the form will be posted // API URL where the form will be posted
@ -468,7 +473,7 @@ Application.Controllers.controller('NewProjectController', ['$scope', '$state',
$scope.matchingMembers = []; $scope.matchingMembers = [];
// Using the ProjectsController // Using the ProjectsController
return new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t); return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t);
} }
]); ]);
@ -509,7 +514,7 @@ Application.Controllers.controller('EditProjectController', ['$rootScope', '$sco
} }
// Using the ProjectsController // Using the ProjectsController
return new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t); return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t);
}; };
// !!! MUST BE CALLED AT THE END of the controller // !!! MUST BE CALLED AT THE END of the controller

View File

@ -166,7 +166,7 @@ Application.Filters.filter('simpleText', [function () {
}]); }]);
Application.Filters.filter('toTrusted', ['$sce', function ($sce) { Application.Filters.filter('toTrusted', ['$sce', function ($sce) {
return text => $sce.trustAsHtml(text); return text => $sce.getTrustedHtml(text);
}]); }]);
Application.Filters.filter('planIntervalFilter', [function () { Application.Filters.filter('planIntervalFilter', [function () {

View File

@ -110,6 +110,7 @@ export enum SettingName {
PayZenCurrency = 'payzen_currency', PayZenCurrency = 'payzen_currency',
PublicAgendaModule = 'public_agenda_module', PublicAgendaModule = 'public_agenda_module',
RenewPackThreshold = 'renew_pack_threshold', RenewPackThreshold = 'renew_pack_threshold',
PackOnlyForSubscription = 'pack_only_for_subscription',
} }
export type SettingValue = string|boolean|number; export type SettingValue = string|boolean|number;

View File

@ -1080,7 +1080,7 @@ angular.module('application.router', ['ui.router'])
"'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', 'trainings_module', " + "'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', 'trainings_module', " +
"'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', 'address_required', " + "'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', 'address_required', " +
"'link_name', 'home_content', 'home_css', 'phone_required', 'upcoming_events_shown', 'public_agenda_module'," + "'link_name', 'home_content', 'home_css', 'phone_required', 'upcoming_events_shown', 'public_agenda_module'," +
"'renew_pack_threshold']" "'renew_pack_threshold', 'pack_only_for_subscription']"
}).$promise; }).$promise;
}], }],
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }], privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],

View File

@ -115,7 +115,7 @@
<div class="font-sbold">{{::g.name}}</div> <div class="font-sbold">{{::g.name}}</div>
<ul class="m-n" ng-repeat="plan in g.plans"> <ul class="m-n" ng-repeat="plan in g.plans">
<li> <li>
{{::plan.name}} {{::plan.base_name}}
<span class="btn btn-warning btn-xs" ng-click="removePlan(plan)" ><i class="fa fa-times red"></i></span> <span class="btn btn-warning btn-xs" ng-click="removePlan(plan)" ><i class="fa fa-times red"></i></span>
</li> </li>
</ul> </ul>

View File

@ -149,7 +149,7 @@
<div ng-repeat="group in plansClassifiedByGroup"> <div ng-repeat="group in plansClassifiedByGroup">
<div class="text-center font-sbold">{{::group.name}}</div> <div class="text-center font-sbold">{{::group.name}}</div>
<label class="checkbox m-l-md" ng-repeat="plan in group.plans"> <label class="checkbox m-l-md" ng-repeat="plan in group.plans">
<input type="checkbox" ng-click="toggleSelectPlan(plan)" ng-model="selectedPlansBinding[plan.id]"> {{::plan.name}}</span> <input type="checkbox" ng-click="toggleSelectPlan(plan)" ng-model="selectedPlansBinding[plan.id]"> {{::plan.base_name}}</span>
</label> </label>
</div> </div>
</div> </div>

View File

@ -87,7 +87,7 @@
<h3 class="m-l m-t-lg" translate>{{ 'app.admin.settings.book_overlapping_slots_info' }}</h3> <h3 class="m-l m-t-lg" translate>{{ 'app.admin.settings.book_overlapping_slots_info' }}</h3>
<boolean-setting name="book_overlapping_slots" <boolean-setting name="book_overlapping_slots"
settings="allSettings" settings="allSettings"
label="app.admin.settings.prevent_booking" label="app.admin.settings.allow_booking"
classes="m-l"> classes="m-l">
</boolean-setting> </boolean-setting>
</div> </div>
@ -169,5 +169,14 @@
step="0.01"> step="0.01">
</number-setting> </number-setting>
</div> </div>
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.pack_only_for_subscription_info' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.pack_only_for_subscription_info_html' | translate"></p>
<boolean-setting name="pack_only_for_subscription"
settings="allSettings"
label="app.admin.settings.pack_only_for_subscription"
classes="m-l">
</boolean-setting>
</div>
</div> </div>
</div> </div>

View File

@ -49,7 +49,7 @@
required required
bs-jasny-fileinput> bs-jasny-fileinput>
</span> </span>
<a href="#" class="btn btn-danger fileinput-exists" data-dismiss="fileinput" translate>{{ 'app.shared.buttons.delete' }}</a> <button class="btn btn-danger fileinput-exists" data-dismiss="fileinput" translate>{{ 'app.shared.buttons.delete' }}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -71,7 +71,7 @@
<label for="description" class="col-sm-2 control-label">{{ 'app.shared.project.description' | translate }} *</label> <label for="description" class="col-sm-2 control-label">{{ 'app.shared.project.description' | translate }} *</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="hidden" name="project[description]" ng-value="project.description" /> <input type="hidden" name="project[description]" ng-value="project.description" />
<summernote ng-model="project.description" id="project_description" placeholder="" config="summernoteOpts" name="project[description]" required></summernote> <summernote ng-model="project.description" id="project_description" placeholder="" config="summernoteOptsProject" name="project[description]" required></summernote>
<span class="help-block" ng-show="projectForm['project[description]'].$dirty && projectForm['project[description]'].$error.required" translate>{{ 'app.shared.project.description_is_required' }}</span> <span class="help-block" ng-show="projectForm['project[description]'].$dirty && projectForm['project[description]'].$error.required" translate>{{ 'app.shared.project.description_is_required' }}</span>
</div> </div>
</div> </div>

View File

@ -37,7 +37,7 @@
<div ng-repeat="group in slot.plansGrouped"> <div ng-repeat="group in slot.plansGrouped">
<div class="font-sbold">{{::group.name}}</div> <div class="font-sbold">{{::group.name}}</div>
<ul class="m-n" ng-repeat="plan in group.plans"> <ul class="m-n" ng-repeat="plan in group.plans">
<li>{{::plan.name}}</li> <li>{{::plan.base_name}}</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -11,7 +11,7 @@
<div ng-repeat="group in slot.plansGrouped"> <div ng-repeat="group in slot.plansGrouped">
<div class="font-sbold">{{::group.name}}</div> <div class="font-sbold">{{::group.name}}</div>
<ul class="m-n" ng-repeat="plan in group.plans"> <ul class="m-n" ng-repeat="plan in group.plans">
<li>{{::plan.name}}</li> <li>{{::plan.base_name}}</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -24,7 +24,7 @@ class CartItem::Subscription < CartItem::BaseItem
end end
def name def name
@plan.name @plan.base_name
end end
def to_object def to_object

View File

@ -132,6 +132,6 @@ class Plan < ApplicationRecord
end end
def update_gateway_product def update_gateway_product
PaymentGatewayService.new.create_or_update_product(Plan.name, id) PaymentGatewayService.new.create_or_update_product(Plan.base_name, id)
end end
end end

View File

@ -119,7 +119,8 @@ class Setting < ApplicationRecord
payzen_hmac payzen_hmac
payzen_currency payzen_currency
public_agenda_module public_agenda_module
renew_pack_threshold] } renew_pack_threshold
pack_only_for_subscription] }
# WARNING: when adding a new key, you may also want to add it in app/policies/setting_policy.rb#public_whitelist # WARNING: when adding a new key, you may also want to add it in app/policies/setting_policy.rb#public_whitelist
def value def value

View File

@ -57,7 +57,7 @@ class Subscription < ApplicationRecord
operator_profile_id: operator_profile_id, operator_profile_id: operator_profile_id,
total: 0 total: 0
) )
invoice.invoice_items.push InvoiceItem.new(amount: 0, description: plan.name, object: od) invoice.invoice_items.push InvoiceItem.new(amount: 0, description: plan.base_name, object: od)
invoice.save invoice.save
if save if save

View File

@ -39,7 +39,8 @@ class SettingPolicy < ApplicationPolicy
tracking_id book_overlapping_slots slot_duration events_in_calendar spaces_module plans_module invoicing_module 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 recaptcha_site_key feature_tour_display disqus_shortname allowed_cad_extensions openlab_app_id openlab_default
online_payment_module stripe_public_key confirmation_required wallet_module trainings_module address_required online_payment_module stripe_public_key confirmation_required wallet_module trainings_module address_required
payment_gateway payzen_endpoint payzen_public_key public_agenda_module renew_pack_threshold statistics_module] payment_gateway payzen_endpoint payzen_public_key public_agenda_module renew_pack_threshold statistics_module
pack_only_for_subscription]
end end
## ##

View File

@ -175,7 +175,7 @@ class InvoicesService
invoice.invoice_items.push InvoiceItem.new( invoice.invoice_items.push InvoiceItem.new(
amount: payment_details[:elements][:plan], amount: payment_details[:elements][:plan],
description: subscription.plan.name, description: subscription.plan.base_name,
object: subscription, object: subscription,
main: main main: main
) )

View File

@ -249,7 +249,7 @@ class PaymentScheduleService
invoice.invoice_items.push InvoiceItem.new( invoice.invoice_items.push InvoiceItem.new(
amount: payment_details[:subscription], amount: payment_details[:subscription],
description: subscription.plan.name, description: subscription.plan.base_name,
object: subscription, object: subscription,
main: main main: main
) )

View File

@ -61,6 +61,11 @@ class PrepaidPackService
## Total number of prepaid minutes available ## Total number of prepaid minutes available
def minutes_available(user, priceable) def minutes_available(user, priceable)
is_pack_only_for_subscription = Setting.find_by(name: "pack_only_for_subscription")&.value
if is_pack_only_for_subscription == 'true' && !user.subscribed_plan
return 0
end
user_packs = user_packs(user, priceable) user_packs = user_packs(user, priceable)
total_available = user_packs.map { |up| up.prepaid_pack.minutes }.reduce(:+) || 0 total_available = user_packs.map { |up| up.prepaid_pack.minutes }.reduce(:+) || 0
total_used = user_packs.map(&:minutes_used).reduce(:+) || 0 total_used = user_packs.map(&:minutes_used).reduce(:+) || 0

View File

@ -279,9 +279,6 @@ a.label:focus,
} }
.about-picture { .about-picture {
background: linear-gradient( rgba(255,255,255,0.12), rgba(255,255,255,0.13) ),
linear-gradient(<%=Stylesheet.primary_with_alpha(0.78)%>, <%=Stylesheet.primary_with_alpha(0.82)%>),
url('/about-fablab.jpg') no-repeat;
} }
.social-icons > div:hover { .social-icons > div:hover {
@ -289,7 +286,7 @@ a.label:focus,
color: $secondary-text-color; color: $secondary-text-color;
} }
.profile-top { .profile-top, .about-picture {
background: linear-gradient( rgba(255,255,255,0.12), rgba(255,255,255,0.13) ), background: linear-gradient( rgba(255,255,255,0.12), rgba(255,255,255,0.13) ),
linear-gradient(<%=Stylesheet.primary_with_alpha(0.78)%>, <%=Stylesheet.primary_with_alpha(0.82)%>), linear-gradient(<%=Stylesheet.primary_with_alpha(0.78)%>, <%=Stylesheet.primary_with_alpha(0.82)%>),
url("<%=CustomAsset.get_url('profile-image-file') || '/about-fablab.jpg'%>") no-repeat; url("<%=CustomAsset.get_url('profile-image-file') || '/about-fablab.jpg'%>") no-repeat;

View File

@ -57,6 +57,7 @@ json.array!(@members) do |member|
json.plan do json.plan do
json.id member.subscription.plan.id json.id member.subscription.plan.id
json.name member.subscription.plan.name json.name member.subscription.plan.name
json.base_name member.subscription.plan.base_name
json.interval member.subscription.plan.interval json.interval member.subscription.plan.interval
json.amount member.subscription.plan.amount ? (member.subscription.plan.amount / 100.0) : 0 json.amount member.subscription.plan.amount ? (member.subscription.plan.amount / 100.0) : 0
end end

View File

@ -1,5 +1,4 @@
json.title notification.notification_type json.title notification.notification_type
json.description t('.subscription_PLAN_has_been_subscribed_by_USER_html', json.description t('.subscription_PLAN_has_been_subscribed_by_USER_html',
PLAN: notification.attached_object.plan.name, PLAN: notification.attached_object.plan.base_name,
USER: notification.attached_object.user&.profile&.full_name || t('api.notifications.deleted_user')) USER: notification.attached_object.user&.profile&.full_name || t('api.notifications.deleted_user'))

View File

@ -1,9 +1,8 @@
json.title notification.notification_type json.title notification.notification_type
json.description _t('.subscription_PLAN_of_the_member_USER_has_been_extended_FREE_until_DATE_html', json.description _t('.subscription_PLAN_of_the_member_USER_has_been_extended_FREE_until_DATE_html',
{ {
PLAN: notification.attached_object.plan.name, PLAN: notification.attached_object.plan.base_name,
USER: notification.attached_object.user&.profile&.full_name || t('api.notifications.deleted_user'), USER: notification.attached_object.user&.profile&.full_name || t('api.notifications.deleted_user'),
FREE: notification.get_meta_data(:free_days).to_s, FREE: notification.get_meta_data(:free_days).to_s,
DATE: I18n.l(notification.attached_object.expired_at.to_date) DATE: I18n.l(notification.attached_object.expired_at.to_date)
}) # messageFormat }) # messageFormat

View File

@ -1,4 +1,3 @@
json.title notification.notification_type json.title notification.notification_type
json.description t('.you_have_subscribed_to_PLAN_html', json.description t('.you_have_subscribed_to_PLAN_html',
PLAN: notification.attached_object.plan.name) PLAN: notification.attached_object.plan.base_name)

View File

@ -1,4 +1,3 @@
json.title notification.notification_type json.title notification.notification_type
json.description t('.your_subscription_PLAN_was_successfully_cancelled_html', json.description t('.your_subscription_PLAN_was_successfully_cancelled_html',
PLAN: notification.attached_object.plan.name) PLAN: notification.attached_object.plan.base_name)

View File

@ -1,8 +1,7 @@
json.title notification.notification_type json.title notification.notification_type
json.description _t('.your_subscription_PLAN_has_been_extended_FREE_until_DATE_html', json.description _t('.your_subscription_PLAN_has_been_extended_FREE_until_DATE_html',
{ {
PLAN: notification.attached_object.plan.name, PLAN: notification.attached_object.plan.base_name,
FREE: notification.get_meta_data(:free_days).to_s, FREE: notification.get_meta_data(:free_days).to_s,
DATE: I18n.l(notification.attached_object.expired_at.to_date) DATE: I18n.l(notification.attached_object.expired_at.to_date)
}) # messageFormat }) # messageFormat

View File

@ -1,7 +1,6 @@
json.title notification.notification_type json.title notification.notification_type
json.description t('.subscription_partner_PLAN_has_been_subscribed_by_USER_html', json.description t('.subscription_partner_PLAN_has_been_subscribed_by_USER_html',
{ {
PLAN: notification.attached_object.plan.name, PLAN: notification.attached_object.plan.base_name,
USER: notification.attached_object.user&.profile&.full_name || t('api.notifications.deleted_user') USER: notification.attached_object.user&.profile&.full_name || t('api.notifications.deleted_user')
}) # messageFormat }) # messageFormat

View File

@ -1154,7 +1154,7 @@ en:
error_SETTING_locked: "Unable to update the setting: {SETTING} is locked. Please contact your system administrator." error_SETTING_locked: "Unable to update the setting: {SETTING} is locked. Please contact your system administrator."
an_error_occurred_saving_the_setting: "An error occurred while saving the setting. Please try again later." an_error_occurred_saving_the_setting: "An error occurred while saving the setting. Please try again later."
book_overlapping_slots_info: "Allow / prevent the reservation of overlapping slots" book_overlapping_slots_info: "Allow / prevent the reservation of overlapping slots"
prevent_booking: "Prevent booking" allow_booking: "Allow booking"
default_slot_duration: "Default duration for slots" default_slot_duration: "Default duration for slots"
duration_minutes: "Duration (in minutes)" duration_minutes: "Duration (in minutes)"
default_slot_duration_info: "Machine and space availabilities are divided in multiple slots of this duration. This value can be overridden per availability." default_slot_duration_info: "Machine and space availabilities are divided in multiple slots of this duration. This value can be overridden per availability."
@ -1208,6 +1208,9 @@ en:
display_invite_to_renew_pack: "Display the invite to renew prepaid-packs" display_invite_to_renew_pack: "Display the invite to renew prepaid-packs"
packs_threshold_info_html: "You can define under how many hours the user will be invited to buy a new prepaid-pack, if his stock of prepaid hours is under this threshold.<br/>You can set a <strong>number of hours</strong> (<em>eg. 5</em>) or a <strong>percentage</strong> of his current pack pack (<em>eg. 0.05 means 5%</em>)." packs_threshold_info_html: "You can define under how many hours the user will be invited to buy a new prepaid-pack, if his stock of prepaid hours is under this threshold.<br/>You can set a <strong>number of hours</strong> (<em>eg. 5</em>) or a <strong>percentage</strong> of his current pack pack (<em>eg. 0.05 means 5%</em>)."
renew_pack_threshold: "threshold for packs renewal" renew_pack_threshold: "threshold for packs renewal"
pack_only_for_subscription_info_html: "If this option is activated, the purchase and use of a prepaid pack is only possible for the user with a valid subscription."
pack_only_for_subscription: "Subscription valid for purchase and use of a prepaid pack"
pack_only_for_subscription_info: "Make subscription mandatory for prepaid packs"
general: general:
general: "General" general: "General"
title: "Title" title: "Title"

View File

@ -196,6 +196,7 @@ en:
remaining_HOURS: "You have {HOURS} prepaid hours remaining for this {ITEM, select, Machine{machine} Space{space} other{}}." remaining_HOURS: "You have {HOURS} prepaid hours remaining for this {ITEM, select, Machine{machine} Space{space} other{}}."
no_hours: "You don't have any prepaid hours for this {ITEM, select, Machine{machine} Space{space} other{}}." no_hours: "You don't have any prepaid hours for this {ITEM, select, Machine{machine} Space{space} other{}}."
buy_a_new_pack: "Buy a new pack" buy_a_new_pack: "Buy a new pack"
unable_to_use_pack_for_subsription_is_expired: "You must have a valid subscription to use your remaining hours."
#book a training #book a training
trainings_reserve: trainings_reserve:
trainings_planning: "Trainings planning" trainings_planning: "Trainings planning"

View File

@ -899,6 +899,8 @@ Setting.set('public_agenda_module', true) unless Setting.find_by(name: 'public_a
Setting.set('renew_pack_threshold', 0.2) unless Setting.find_by(name: 'renew_pack_threshold').try(:value) Setting.set('renew_pack_threshold', 0.2) unless Setting.find_by(name: 'renew_pack_threshold').try(:value)
Setting.set('pack_only_for_subscription', true) unless Setting.find_by(name: 'pack_only_for_subscription').try(:value)
if StatisticCustomAggregation.count.zero? if StatisticCustomAggregation.count.zero?
# available reservations hours for machines # available reservations hours for machines
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2) machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2)

View File

@ -1,6 +1,6 @@
{ {
"name": "fab-manager", "name": "fab-manager",
"version": "5.1.8", "version": "5.1.10",
"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.", "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": [ "keywords": [
"fablab", "fablab",
@ -83,7 +83,7 @@
"angular-ui-tour": "https://github.com/sleede/angular-ui-tour.git#master", "angular-ui-tour": "https://github.com/sleede/angular-ui-tour.git#master",
"angular-unsavedchanges": "0.2", "angular-unsavedchanges": "0.2",
"angular-xeditable": "0.10", "angular-xeditable": "0.10",
"axios": "^0.21.1", "axios": "^0.21.2",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24", "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"bootstrap-sass": "3.4.1", "bootstrap-sass": "3.4.1",
"checklist-model": "0.2", "checklist-model": "0.2",
@ -95,7 +95,7 @@
"i18next": "^19.8.3", "i18next": "^19.8.3",
"i18next-http-backend": "^1.0.21", "i18next-http-backend": "^1.0.21",
"i18next-icu": "^1.4.2", "i18next-icu": "^1.4.2",
"immer": "^9.0.1", "immer": "^9.0.6",
"jasny-bootstrap": "3.1", "jasny-bootstrap": "3.1",
"jquery": ">=3.5.0", "jquery": ">=3.5.0",
"jquery-ujs": "^1.2.2", "jquery-ujs": "^1.2.2",

View File

@ -89,6 +89,11 @@ target_version()
if [[ "$TAG" =~ ^:release-v[\.0-9]+$ ]]; then if [[ "$TAG" =~ ^:release-v[\.0-9]+$ ]]; then
TARGET=$(echo "$TAG" | grep -Eo '[\.0-9]{5}') TARGET=$(echo "$TAG" | grep -Eo '[\.0-9]{5}')
elif [ "$TAG" = ":latest" ] || [ "$TAG" = "" ]; then elif [ "$TAG" = ":latest" ] || [ "$TAG" = "" ]; then
HTTP_CODE=$(curl -I -s -w "%{http_code}\n" -o /dev/null https://hub.fab-manager.com/api/versions/latest)
if [ "$HTTP_CODE" != 200 ]; then
printf "\n\n\e[91m[ ❌ ] Unable to retrieve the last version of Fab-manager. Please check your internet connection or restart this script providing the \e[1m-t\e[0m\e[91m option\n\e[39m"
exit 3
fi
TARGET=$(\curl -sSL "https://hub.fab-manager.com/api/versions/latest" | jq -r '.semver') TARGET=$(\curl -sSL "https://hub.fab-manager.com/api/versions/latest" | jq -r '.semver')
else else
TARGET='custom' TARGET='custom'
@ -120,8 +125,8 @@ version_check()
version_error "v4.0.4 first" version_error "v4.0.4 first"
elif verlt "$VERSION" 4.4.6 && verlt 4.4.6 "$TARGET"; then elif verlt "$VERSION" 4.4.6 && verlt 4.4.6 "$TARGET"; then
version_error "v4.4.6 first" version_error "v4.4.6 first"
elif verlt "$VERSION" 4.7.13 && verlt 4.7.13 "$TARGET"; then elif verlt "$VERSION" 4.7.14 && verlt 4.7.14 "$TARGET"; then
version_error "v4.7.13 first" version_error "v4.7.14 first"
elif verlt "$TARGET" "$VERSION"; then elif verlt "$TARGET" "$VERSION"; then
version_error "a version > $VERSION" version_error "a version > $VERSION"
fi fi

View File

@ -1924,12 +1924,12 @@ autoprefixer@^9.6.1:
postcss "^7.0.32" postcss "^7.0.32"
postcss-value-parser "^4.1.0" postcss-value-parser "^4.1.0"
axios@^0.21.1: axios@^0.21.2:
version "0.21.1" version "0.21.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017"
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==
dependencies: dependencies:
follow-redirects "^1.10.0" follow-redirects "^1.14.0"
babel-loader@^8.2.2: babel-loader@^8.2.2:
version "8.2.2" version "8.2.2"
@ -3962,10 +3962,10 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3" inherits "^2.0.3"
readable-stream "^2.3.6" readable-stream "^2.3.6"
follow-redirects@^1.0.0, follow-redirects@^1.10.0: follow-redirects@^1.0.0, follow-redirects@^1.14.0:
version "1.14.1" version "1.14.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e"
integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg== integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==
for-in@^1.0.2: for-in@^1.0.2:
version "1.0.2" version "1.0.2"
@ -4507,10 +4507,10 @@ ignore@^5.1.1, ignore@^5.1.4:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
immer@^9.0.1: immer@^9.0.6:
version "9.0.3" version "9.0.6"
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.3.tgz#146e2ba8b84d4b1b15378143c2345559915097f4" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
integrity sha512-mONgeNSMuyjIe0lkQPa9YhdmTv8P19IeHV0biYhcXhbd5dhdB9HSK93zBpyKjp6wersSUgT5QyU0skmejUVP2A== integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==
import-cwd@^2.0.0: import-cwd@^2.0.0:
version "2.1.0" version "2.1.0"
@ -8197,9 +8197,9 @@ tapable@^1.0.0, tapable@^1.1.3:
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
tar@^6.0.2: tar@^6.0.2:
version "6.1.4" version "6.1.11"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.4.tgz#9f0722b772a5e00dba7d52e1923b37a7ec3799b3" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
integrity sha512-kcPWrO8S5ABjuZ/v1xQHP8xCEvj1dQ1d9iAb6Qs4jLYzaAIYWwST2IQpz7Ud8VNYRI+fGhFjrnzRKmRggKWg3g== integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
dependencies: dependencies:
chownr "^2.0.0" chownr "^2.0.0"
fs-minipass "^2.0.0" fs-minipass "^2.0.0"
@ -8530,9 +8530,9 @@ urix@^0.1.0:
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
url-parse@^1.4.3, url-parse@^1.5.1: url-parse@^1.4.3, url-parse@^1.5.1:
version "1.5.1" version "1.5.3"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862"
integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==
dependencies: dependencies:
querystringify "^2.1.1" querystringify "^2.1.1"
requires-port "^1.0.0" requires-port "^1.0.0"