diff --git a/CHANGELOG.md b/CHANGELOG.md index b7ed3d5f9..aa90e2c47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,16 +3,28 @@ ## Next release - [TODO DEPLOY] `rails fablab:stripe:set_gateway` -## Next release (v4.7.6) +## v4.7.6 2021 March 24 - Ability to disable the trainings module +- Ability to set the address as a mandatory field +- The address is new requested when creating an account +- The profile completion page is less fuzzy for people landing on it without enabled SSO - Prevent showing error message when testing for old versions during upgrade - In the email notification, sent to admins on account creation, show the group of the user - More explanations in the setup script - Send pre-compressed assets to the browsers instead of the regular ones +- Links created using "medium editor" opens in new tabs +- Improved style of public plans page +- Improved the upgrade script - Fix a bug: subscriptions tab is selected by default in statistics, even if the module is disabled +- Fix a bug: select all plans for slot restriction (through the dedicated button) also selects the disabled plans +- Fix a bug: recurring availabilities are not restricted to subscribers +- Fix a bug: accounting exports may ignore some invoices for the first and last days +- Fix a bug: accounting export caching is not working +- Fix a bug: unable to run the setup script if sudoers belong to another group than sudo - Fix a security issue: updated elliptic to 6.5.4 to fix [CVE-2020-28498](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28498) - [TODO DEPLOY] `\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/scripts/nginx-packs-directive.sh | bash` - [TODO DEPLOY] `rails db:seed` +- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet` ## v4.7.5 2021 March 08 - Fix a bug: unable to compile the assets during the upgrade, if the env file has some whitespaces around the equal sign diff --git a/app/controllers/api/accounting_exports_controller.rb b/app/controllers/api/accounting_exports_controller.rb index 24d15492a..20dd342dd 100644 --- a/app/controllers/api/accounting_exports_controller.rb +++ b/app/controllers/api/accounting_exports_controller.rb @@ -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 diff --git a/app/controllers/api/auth_providers_controller.rb b/app/controllers/api/auth_providers_controller.rb index 1efd31c44..6a4b5bea0 100644 --- a/app/controllers/api/auth_providers_controller.rb +++ b/app/controllers/api/auth_providers_controller.rb @@ -49,6 +49,7 @@ class API::AuthProvidersController < API::ApiController def active authorize AuthProvider @provider = AuthProvider.active + @previous = AuthProvider.previous end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 181777e43..10b7c224b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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] }, diff --git a/app/frontend/src/javascript/controllers/admin/calendar.js b/app/frontend/src/javascript/controllers/admin/calendar.js index 3f84b0d35..79cf9a094 100644 --- a/app/frontend/src/javascript/controllers/admin/calendar.js +++ b/app/frontend/src/javascript/controllers/admin/calendar.js @@ -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; }); diff --git a/app/frontend/src/javascript/controllers/admin/invoices.js b/app/frontend/src/javascript/controllers/admin/invoices.js index 77484dec0..16d6b79b5 100644 --- a/app/frontend/src/javascript/controllers/admin/invoices.js +++ b/app/frontend/src/javascript/controllers/admin/invoices.js @@ -1334,8 +1334,8 @@ Application.Controllers.controller('AccountingExportModalController', ['$scope', columns: $scope.exportTarget.settings.columns, encoding: $scope.exportTarget.settings.encoding, date_format: $scope.exportTarget.settings.dateFormat, - start_date: $scope.exportTarget.startDate, - end_date: $scope.exportTarget.endDate, + start_date: moment.utc($scope.exportTarget.startDate).startOf('day').toISOString(), + end_date: moment.utc($scope.exportTarget.endDate).endOf('day').toISOString(), label_max_length: $scope.exportTarget.settings.labelMaxLength, decimal_separator: $scope.exportTarget.settings.decimalSeparator, export_invoices_at_zero: $scope.exportTarget.settings.exportInvoicesAtZero diff --git a/app/frontend/src/javascript/controllers/admin/members.js b/app/frontend/src/javascript/controllers/admin/members.js index 505076714..ad5e718db 100644 --- a/app/frontend/src/javascript/controllers/admin/members.js +++ b/app/frontend/src/javascript/controllers/admin/members.js @@ -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 diff --git a/app/frontend/src/javascript/controllers/application.js.erb b/app/frontend/src/javascript/controllers/application.js.erb index c25601eb5..e3dd67d92 100644 --- a/app/frontend/src/javascript/controllers/application.js.erb +++ b/app/frontend/src/javascript/controllers/application.js.erb @@ -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; diff --git a/app/frontend/src/javascript/controllers/members.js b/app/frontend/src/javascript/controllers/members.js index 2b23102ee..05c233fe0 100644 --- a/app/frontend/src/javascript/controllers/members.js +++ b/app/frontend/src/javascript/controllers/members.js @@ -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 = { diff --git a/app/frontend/src/javascript/controllers/profile.js b/app/frontend/src/javascript/controllers/profile.js index e7fcf4788..5056c8a1a 100644 --- a/app/frontend/src/javascript/controllers/profile.js +++ b/app/frontend/src/javascript/controllers/profile.js @@ -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 */ /** diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index b42653b85..ccde19dc3 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -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 { diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index 5b2b67dbd..a0e85c3a1 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -130,12 +130,11 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['fablab_name', 'name_genre']" }).$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['fablab_name', 'name_genre', 'phone_required', 'address_required']" }).$promise; }], activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }], groupsPromise: ['Group', function (Group) { return Group.query().$promise; }], cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }], - memberPromise: ['Member', 'currentUser', function (Member, currentUser) { return Member.get({ id: currentUser.id }).$promise; }], - phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] + memberPromise: ['Member', 'currentUser', function (Member, currentUser) { return Member.get({ id: currentUser.id }).$promise; }] } }) @@ -168,7 +167,7 @@ angular.module('application.router', ['ui.router']) resolve: { groups: ['Group', function (Group) { return Group.query().$promise; }], activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }], - phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }] } }) .state('app.logged.dashboard.projects', { @@ -916,7 +915,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }] } }) .state('app.admin.members_import', { @@ -957,7 +956,7 @@ angular.module('application.router', ['ui.router']) walletPromise: ['Wallet', '$stateParams', function (Wallet, $stateParams) { return Wallet.getWalletByUser({ user_id: $stateParams.id }).$promise; }], transactionsPromise: ['Wallet', 'walletPromise', function (Wallet, walletPromise) { return Wallet.transactions({ id: walletPromise.id }).$promise; }], tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }], - phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }] } }) .state('app.admin.admins_new', { @@ -969,7 +968,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }] } }) .state('app.admin.managers_new', { @@ -1062,7 +1061,7 @@ angular.module('application.router', ['ui.router']) "'booking_cancel_delay', 'main_color', 'secondary_color', 'spaces_module', 'twitter_analytics', " + "'fablab_name', 'name_genre', 'reminder_enable', 'plans_module', 'confirmation_required', " + "'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', 'trainings_module', " + - "'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', " + + "'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', 'address_required', " + "'link_name', 'home_content', 'home_css', 'phone_required', 'upcoming_events_shown']" }).$promise; }], diff --git a/app/frontend/src/stylesheets/app.components.scss b/app/frontend/src/stylesheets/app.components.scss index 3d52a94c2..04d1e6691 100644 --- a/app/frontend/src/stylesheets/app.components.scss +++ b/app/frontend/src/stylesheets/app.components.scss @@ -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; } } } diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index c78f12976..eb8811588 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -32,6 +32,7 @@ @import "modules/payment-schedules-list"; @import "modules/stripe-confirm"; @import "modules/payment-schedule-dashboard"; +@import "modules/plan-card"; @import "modules/select-gateway-modal"; @import "app.responsive"; diff --git a/app/frontend/src/stylesheets/modules/plan-card.scss b/app/frontend/src/stylesheets/modules/plan-card.scss new file mode 100644 index 000000000..71c4fcb0b --- /dev/null +++ b/app/frontend/src/stylesheets/modules/plan-card.scss @@ -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; + } + } +} diff --git a/app/frontend/templates/admin/plans/_form.html b/app/frontend/templates/admin/plans/_form.html index 7161b264e..8cfdcbcca 100644 --- a/app/frontend/templates/admin/plans/_form.html +++ b/app/frontend/templates/admin/plans/_form.html @@ -130,9 +130,13 @@
-
+
diff --git a/app/frontend/templates/admin/settings/about.html b/app/frontend/templates/admin/settings/about.html index 109f3d330..67a0c98ba 100644 --- a/app/frontend/templates/admin/settings/about.html +++ b/app/frontend/templates/admin/settings/about.html @@ -30,18 +30,27 @@
-
+
{{ 'app.admin.settings.drag_and_drop_to_insert_images' | translate }}
-
+
{{ 'app.admin.settings.shift_enter_to_force_carriage_return' | translate }} diff --git a/app/frontend/templates/admin/settings/general.html b/app/frontend/templates/admin/settings/general.html index 611235b51..6948ffaae 100644 --- a/app/frontend/templates/admin/settings/general.html +++ b/app/frontend/templates/admin/settings/general.html @@ -48,52 +48,76 @@

{{ 'app.admin.settings.message_of_the_machine_booking_page' }}

-
+

{{ 'app.admin.settings.warning_message_of_the_training_booking_page'}}

-
+

{{ 'app.admin.settings.information_message_of_the_training_reservation_page'}}

-
+

{{ 'app.admin.settings.message_of_the_subscriptions_page' }}

-
+

{{ 'app.admin.settings.message_of_the_events_page' }}

-
+

{{ 'app.admin.settings.message_of_the_spaces_page' }}

-
+
@@ -400,6 +424,18 @@
+
+

{{ 'app.admin.settings.address' }}

+

+ {{ 'app.admin.settings.address_required_info_html' }} +

+
+ + +
+

{{ 'app.admin.settings.captcha' }}

diff --git a/app/frontend/templates/admin/settings/home_page.html b/app/frontend/templates/admin/settings/home_page.html index 40de310f7..4b4c858c1 100644 --- a/app/frontend/templates/admin/settings/home_page.html +++ b/app/frontend/templates/admin/settings/home_page.html @@ -14,8 +14,15 @@

{{ 'app.admin.settings.news_of_the_home_page' }}

-
+
+
{{ 'app.admin.settings.leave_it_empty_to_not_bring_up_any_news_on_the_home_page' | translate }}
diff --git a/app/frontend/templates/plans/_plan.html b/app/frontend/templates/plans/_plan.html index 359b55525..56a3b0f70 100644 --- a/app/frontend/templates/plans/_plan.html +++ b/app/frontend/templates/plans/_plan.html @@ -1,33 +1,25 @@ -
+
-

{{ plansGroup.name }}

+

{{ plansGroup.name }}

-
+ + {{ 'app.shared.plan_subscribe.do_not_subscribe' | translate }} +
diff --git a/app/frontend/templates/plans/index.html b/app/frontend/templates/plans/index.html index 0706c9c6e..a5d81fbfa 100644 --- a/app/frontend/templates/plans/index.html +++ b/app/frontend/templates/plans/index.html @@ -14,31 +14,26 @@ -
+
-

{{plansGroup.name}}

+

{{plansGroup.name}}

-
+
- -
- - + is-selected="isSelected(plan)" -
diff --git a/app/frontend/templates/profile/complete.html b/app/frontend/templates/profile/complete.html index 5af46af3e..88a2d20cd 100644 --- a/app/frontend/templates/profile/complete.html +++ b/app/frontend/templates/profile/complete.html @@ -21,7 +21,7 @@
-
+
@@ -34,19 +34,19 @@
-
-

{{ 'app.logged.profile_completion.new_on_this_platform' }}

+

{{ 'app.logged.profile_completion.new_on_this_platform' }}

{{ 'app.logged.profile_completion.please_fill_the_following_form'}}.

{{ 'app.logged.profile_completion.some_data_may_have_already_been_provided_by_provider_and_cannot_be_modified' | translate:{NAME:activeProvider.name} }}.
{{ 'app.logged.profile_completion.then_click_on_' | translate }} {{ 'app.shared.buttons.confirm_changes' }} {{ 'app.logged.profile_completion._to_start_using_the_application' | translate }}.

@@ -145,14 +145,14 @@
-
diff --git a/app/frontend/templates/shared/_member_form.html b/app/frontend/templates/shared/_member_form.html index f277940d4..fbf336c10 100644 --- a/app/frontend/templates/shared/_member_form.html +++ b/app/frontend/templates/shared/_member_form.html @@ -224,7 +224,7 @@
- + @@ -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"/>
diff --git a/app/frontend/templates/shared/signupModal.html b/app/frontend/templates/shared/signupModal.html index b9f2639a8..a91c05f7d 100644 --- a/app/frontend/templates/shared/signupModal.html +++ b/app/frontend/templates/shared/signupModal.html @@ -213,6 +213,24 @@
+
+
+
+ + +
+ + + + {{ 'app.public.common.address_is_required' }} +
+
+
{ 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 diff --git a/app/models/setting.rb b/app/models/setting.rb index d221fb50c..7c04ec9fe 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -109,6 +109,7 @@ class Setting < ApplicationRecord upcoming_events_shown payment_schedule_prefix trainings_module + address_required payment_gateway] } # WARNING: when adding a new key, you may also want to add it in app/policies/setting_policy.rb#public_whitelist diff --git a/app/models/user.rb b/app/models/user.rb index 23372aa39..01307ab03 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/policies/setting_policy.rb b/app/policies/setting_policy.rb index 9baa6000d..01006b7e1 100644 --- a/app/policies/setting_policy.rb +++ b/app/policies/setting_policy.rb @@ -38,7 +38,7 @@ class SettingPolicy < ApplicationPolicy fablab_name name_genre event_explications_alert space_explications_alert link_name home_content phone_required tracking_id book_overlapping_slots slot_duration events_in_calendar spaces_module plans_module invoicing_module recaptcha_site_key feature_tour_display disqus_shortname allowed_cad_extensions openlab_app_id openlab_default - online_payment_module stripe_public_key confirmation_required wallet_module trainings_module payment_gateway] + online_payment_module stripe_public_key confirmation_required wallet_module trainings_module address_required payment_gateway] end ## diff --git a/app/services/accounting_export_service.rb b/app/services/accounting_export_service.rb index f3986f75a..81be782a7 100644 --- a/app/services/accounting_export_service.rb +++ b/app/services/accounting_export_service.rb @@ -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 diff --git a/app/services/availabilities/create_availabilities_service.rb b/app/services/availabilities/create_availabilities_service.rb index da8891642..fb5f54474 100644 --- a/app/services/availabilities/create_availabilities_service.rb +++ b/app/services/availabilities/create_availabilities_service.rb @@ -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 diff --git a/app/themes/casemate/style.scss.erb b/app/themes/casemate/style.scss.erb index 28e1e9b6a..0caae8bc3 100644 --- a/app/themes/casemate/style.scss.erb +++ b/app/themes/casemate/style.scss.erb @@ -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 { diff --git a/app/views/api/auth_providers/_auth_provider.json.jbuilder b/app/views/api/auth_providers/_auth_provider.json.jbuilder index 70eb85290..325094b72 100644 --- a/app/views/api/auth_providers/_auth_provider.json.jbuilder +++ b/app/views/api/auth_providers/_auth_provider.json.jbuilder @@ -1 +1,3 @@ -json.extract! auth_provider, :id, :name, :status, :providable_type, :strategy_name \ No newline at end of file +# frozen_string_literal: true + +json.extract! auth_provider, :id, :name, :status, :providable_type, :strategy_name diff --git a/app/views/api/auth_providers/active.json.jbuilder b/app/views/api/auth_providers/active.json.jbuilder index c9bade12f..0c9104c7a 100644 --- a/app/views/api/auth_providers/active.json.jbuilder +++ b/app/views/api/auth_providers/active.json.jbuilder @@ -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 diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 972363d40..5a9b3bb81 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1081,6 +1081,7 @@ en: machines_sort_by: "machines display order" fab_analytics: "Fab Analytics" phone_required: "phone required" + address_required: "address required" tracking_id: "tracking ID" facebook_app_id: "Facebook App ID" twitter_analytics: "Twitter analytics account" @@ -1117,6 +1118,9 @@ en: phone: "Phone" phone_is_required: "Phone required" phone_required_info: "You can define if the phone number should be required to register a new user on Fab-manager." + address: "Address" + address_required_info_html: "You can define if the address should be required to register a new user on Fab-manager.
Please note 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 an API key pair to start using the captcha." site_key: "Site key" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 2c4cd6b99..f7798b2bc 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -1081,6 +1081,7 @@ fr: machines_sort_by: "l'ordre d'affichage des machines" fab_analytics: "Fab Analytics" phone_required: "téléphone requis" + address_required: "adresse requise" tracking_id: "l'ID de suivi" facebook_app_id: "l'App ID Facebook" twitter_analytics: "compte Twitter analytics" @@ -1117,6 +1118,9 @@ fr: phone: "Téléphone" phone_is_required: "Téléphone requis" phone_required_info: "Vous pouvez définir si le numéro de téléphone doit être requis, lors de l'enregistrement d'un nouvel utilisateur sur Fab-manager." + address: "Adresse" + address_required_info_html: "Vous pouvez définir si l'adresse doit être requise, lors de l'enregistrement d'un nouvel utilisateur sur Fab-manager.
Veuillez noter 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 une paire de clefs d'API afin d'utiliser le captcha." site_key: "Clef de site" diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index ff55a8efd..f3c8b1244 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -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" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index ab65878a2..0a6e258d9 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -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" diff --git a/lib/tasks/fablab/auth.rake b/lib/tasks/fablab/auth.rake index a555900ad..7d2c1d033 100644 --- a/lib/tasks/fablab/auth.rake +++ b/lib/tasks/fablab/auth.rake @@ -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') diff --git a/setup/setup.sh b/setup/setup.sh index 17aad2a7c..093e062af 100755 --- a/setup/setup.sh +++ b/setup/setup.sh @@ -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 &1) + if [ $? -eq 0 ]; then + echo "has_sudo__pass_set" + elif echo $prompt | grep -q '^sudo:'; then + echo "has_sudo__needs_pass" + else + echo "no_sudo" + fi +} + +elevate_cmd() +{ + local cmd=$@ + + HAS_SUDO=$(has_sudo) + + case "$HAS_SUDO" in + has_sudo__pass_set) + sudo $cmd + ;; + has_sudo__needs_pass) + echo "Please supply sudo password for the following command: sudo $cmd" + sudo $cmd + ;; + *) + echo "Please supply root password for the following command: su -c \"$cmd\"" + su -c "$cmd" + ;; + esac +} + read_email() { local email @@ -113,8 +157,8 @@ prepare_files() read -rp "Continue? (Y/n) " confirm > "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 .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 \ No newline at end of file + name: home_page diff --git a/test/integration/availabilities/as_admin_test.rb b/test/integration/availabilities/as_admin_test.rb index 41df807fe..37b378768 100644 --- a/test/integration/availabilities/as_admin_test.rb +++ b/test/integration/availabilities/as_admin_test.rb @@ -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