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

(feat) remove admins group: allow admins to reserve

This commit is contained in:
Sylvain 2022-10-25 11:57:26 +02:00
parent abea5006b2
commit 744b811b62
35 changed files with 94 additions and 167 deletions

View File

@ -34,3 +34,7 @@ Style/AndOr:
EnforcedStyle: conditionals
Style/FormatString:
EnforcedStyle: sprintf
Rails/RedundantPresenceValidationOnBelongsTo:
Enabled: false
Rails/UnknownEnv:
Environments: development, test, staging, production

View File

@ -35,7 +35,7 @@ class API::AdminsController < API::ApiController
def admin_params
params.require(:admin).permit(
:username, :email,
:username, :email, :group_id,
profile_attributes: %i[first_name last_name phone],
invoicing_profile_attributes: [address_attributes: [:address]],
statistic_profile_attributes: %i[gender birthday]

View File

@ -6,7 +6,7 @@ class API::GroupsController < API::ApiController
before_action :authenticate_user!, except: :index
def index
@groups = GroupService.list(current_user, params)
@groups = GroupService.list(params)
end
def create

View File

@ -43,7 +43,7 @@ export const ChangeGroup: React.FC<ChangeGroupProps> = ({ user, onSuccess, onErr
const { handleSubmit, control } = useForm();
useEffect(() => {
GroupAPI.index({ disabled: false, admins: user?.role === 'admin' }).then(setGroups).catch(onError);
GroupAPI.index({ disabled: false }).then(setGroups).catch(onError);
MemberAPI.current().then(setOperator).catch(onError);
SettingAPI.get('user_change_group').then((setting) => {
setAllowedUserChangeGoup(setting.value === 'true');

View File

@ -39,7 +39,7 @@ export const PlansFilter: React.FC<PlansFilterProps> = ({ user, groups, onGroupS
* Convert all groups to the react-select format
*/
const buildGroupOptions = (): Array<selectOption> => {
return groups.filter(g => !g.disabled && g.slug !== 'admins').map(g => {
return groups.filter(g => !g.disabled).map(g => {
return { value: g.id, label: g.name };
});
};

View File

@ -41,7 +41,7 @@ export const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuc
MachineAPI.index({ disabled: false })
.then(data => setMachines(data))
.catch(error => onError(error));
GroupAPI.index({ disabled: false, admins: false })
GroupAPI.index({ disabled: false })
.then(data => setGroups(data))
.catch(error => onError(error));
PriceAPI.index({ priceable_type: 'Machine', plan_id: null })

View File

@ -38,7 +38,7 @@ export const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess
SpaceAPI.index()
.then(data => setSpaces(data))
.catch(error => onError(error));
GroupAPI.index({ disabled: false, admins: false })
GroupAPI.index({ disabled: false })
.then(data => setGroups(data))
.catch(error => onError(error));
PriceAPI.index({ priceable_type: 'Space', plan_id: null })

View File

@ -45,7 +45,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
// get groups
useEffect(() => {
GroupAPI.index({ disabled: false, admins: false }).then(data => {
GroupAPI.index({ disabled: false }).then(data => {
setGroups(data);
ProofOfIdentityTypeAPI.index().then(pData => {
setSupportingDocumentsTypes(pData);

View File

@ -45,7 +45,7 @@ export const ChangeRoleModal: React.FC<ChangeRoleModalProps> = ({ isOpen, toggle
const [selectedRole, setSelectedRole] = useState<UserRole>(user.role);
useEffect(() => {
GroupAPI.index({ disabled: false, admins: false }).then(setGroups).catch(onError);
GroupAPI.index({ disabled: false }).then(setGroups).catch(onError);
}, []);
/**

View File

@ -77,7 +77,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
setIsLocalDatabaseProvider(data.providable_type === 'DatabaseProvider');
}).catch(error => onError(error));
if (showGroupInput) {
GroupAPI.index({ disabled: false, admins: user.role === 'admin' }).then(data => {
GroupAPI.index({ disabled: false }).then(data => {
setGroups(buildOptions(data));
}).catch(error => onError(error));
}
@ -155,11 +155,6 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
* Check if the given field path should be disabled
*/
const isDisabled = function (id: string) {
// never allows admins to change their group
if (id === 'group_id' && user.role === 'admin') {
return true;
}
// if the current provider is the local database, then all fields are enabled
if (isLocalDatabaseProvider) {
return false;

View File

@ -38,7 +38,7 @@
class MembersController {
constructor ($scope, $state, Group, Training) {
// Retrieve the profiles groups (e.g. students ...)
Group.query(function (groups) { $scope.groups = groups.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; }); });
Group.query(function (groups) { $scope.groups = groups.filter(function (g) { return !g.disabled; }); });
// Retrieve the list of available trainings
Training.query().$promise.then(function (data) {
@ -1118,8 +1118,8 @@ Application.Controllers.controller('ImportMembersResultController', ['$scope', '
/**
* Controller used in the admin creation page (admin view)
*/
Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'Admin', 'growl', '_t', 'settingsPromise',
function ($state, $scope, Admin, growl, _t, settingsPromise) {
Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'Admin', 'growl', '_t', 'settingsPromise', 'groupsPromise',
function ($state, $scope, Admin, growl, _t, settingsPromise, groupsPromise) {
// default admin profile
let getGender;
$scope.admin = {
@ -1145,6 +1145,9 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A
// is the address required in _admin_form?
$scope.addressRequired = (settingsPromise.address_required === 'true');
// all available groups
$scope.groups = groupsPromise;
/**
* Shows the birthday datepicker
*/
@ -1208,7 +1211,7 @@ Application.Controllers.controller('NewManagerController', ['$state', '$scope',
};
// list of all groups
$scope.groups = groupsPromise.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; });
$scope.groups = groupsPromise.filter(function (g) { return !g.disabled; });
// list of all tags
$scope.tags = tagsPromise;

View File

@ -27,7 +27,7 @@ class PlanController {
// groups list
$scope.groups = groups
.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; })
.filter(function (g) { return !g.disabled; })
.map(e => Object.assign({}, e, { category: 'app.shared.plan.groups', id: `${e.id}` }));
$scope.groups.push({ id: 'all', name: 'app.shared.plan.transversal_all_groups', category: 'app.shared.plan.all' });

View File

@ -30,8 +30,8 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
$scope.enabledPlans = plans.filter(function (p) { return !p.disabled; });
// List of groups (eg. normal, student ...)
$scope.groups = groups.filter(function (g) { return g.slug !== 'admins'; });
$scope.enabledGroups = groups.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; });
$scope.groups = groups;
$scope.enabledGroups = groups.filter(function (g) { return !g.disabled; });
// List of all plan-categories
$scope.planCategories = planCategories;

View File

@ -117,9 +117,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
// retrieve the groups (standard, student ...)
Group.query(function (groups) {
$scope.groups = groups;
$scope.enabledGroups = groups.filter(function (g) {
return (g.slug !== 'admins') && !g.disabled;
});
$scope.enabledGroups = groups.filter(g => !g.disabled);
});
// retrieve the CGU

View File

@ -161,7 +161,7 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
* Check if it is allowed the change the group of the current user
*/
$scope.isAllowedChangingGroup = function () {
return !$scope.user.subscribed_plan?.name && $scope.user.role !== 'admin';
return !$scope.user.subscribed_plan?.name;
};
/**

View File

@ -1,8 +1,7 @@
import { ApiFilter } from './api';
export interface GroupIndexFilter extends ApiFilter {
disabled?: boolean,
admins?: boolean,
disabled?: boolean
}
export interface Group {

View File

@ -1001,7 +1001,8 @@ angular.module('application.router', ['ui.router'])
}
},
resolve: {
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query({ disabled: false }).$promise; }]
}
})
.state('app.admin.managers_new', {

View File

@ -37,7 +37,7 @@
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-hide="rowform.$visible || group.slug === 'admins'">
<div class="buttons" ng-hide="rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'app.shared.buttons.edit' }}</span>
</button>

View File

@ -119,6 +119,19 @@
ng-required="phoneRequired">
</div>
</div>
<div class="form-group" ng-class="{'has-error': adminForm['admin[group_id]'].$dirty && adminForm['admin[group_id]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-group"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<select ng-model="admin.group_id"
name="admin[group_id]"
class="form-control"
id="group_id"
required
ng-options="g.id as g.name for g in groups">
</select>
</div>
</div>
</div>
</div>

View File

@ -2,17 +2,15 @@
# Group is way to bind users with prices. Different prices can be defined for each plan/reservable, for each group
class Group < ApplicationRecord
has_many :plans
has_many :users
has_many :statistic_profiles
has_many :plans, dependent: :destroy
has_many :users, dependent: :nullify
has_many :statistic_profiles, dependent: :nullify
has_many :trainings_pricings, dependent: :destroy
has_many :machines_prices, -> { where(priceable_type: 'Machine') }, class_name: 'Price', dependent: :destroy
has_many :spaces_prices, -> { where(priceable_type: 'Space') }, class_name: 'Price', dependent: :destroy
has_many :machines_prices, -> { where(priceable_type: 'Machine') }, class_name: 'Price', dependent: :destroy, inverse_of: :group
has_many :spaces_prices, -> { where(priceable_type: 'Space') }, class_name: 'Price', dependent: :destroy, inverse_of: :group
has_many :proof_of_identity_types_groups, dependent: :destroy
has_many :proof_of_identity_types, through: :proof_of_identity_types_groups
scope :all_except_admins, -> { where.not(slug: 'admins') }
extend FriendlyId
friendly_id :name, use: :slugged
@ -41,19 +39,19 @@ class Group < ApplicationRecord
end
def create_trainings_pricings
Training.all.each do |training|
Training.find_each do |training|
TrainingsPricing.create(group: self, training: training, amount: 0)
end
end
def create_machines_prices
Machine.all.each do |machine|
Machine.find_each do |machine|
Price.create(priceable: machine, group: self, amount: 0)
end
end
def create_spaces_prices
Space.all.each do |space|
Space.find_each do |space|
Price.create(priceable: space, group: self, amount: 0)
end
end
@ -74,7 +72,7 @@ class Group < ApplicationRecord
def disable_plans
plans.each do |plan|
plan.update_attributes(disabled: disabled)
plan.update(disabled: disabled)
end
end
end

View File

@ -7,13 +7,13 @@ class Plan < ApplicationRecord
belongs_to :plan_category
has_many :credits, dependent: :destroy
has_many :training_credits, -> { where(creditable_type: 'Training') }, class_name: 'Credit'
has_many :machine_credits, -> { where(creditable_type: 'Machine') }, class_name: 'Credit'
has_many :space_credits, -> { where(creditable_type: 'Space') }, class_name: 'Credit'
has_many :subscriptions
has_many :training_credits, -> { where(creditable_type: 'Training') }, class_name: 'Credit', dependent: :destroy, inverse_of: :plan
has_many :machine_credits, -> { where(creditable_type: 'Machine') }, class_name: 'Credit', dependent: :destroy, inverse_of: :plan
has_many :space_credits, -> { where(creditable_type: 'Space') }, class_name: 'Credit', dependent: :destroy, inverse_of: :plan
has_many :subscriptions, dependent: :nullify
has_one :plan_file, as: :viewable, dependent: :destroy
has_many :prices, dependent: :destroy
has_one :payment_gateway_object, as: :item
has_one :payment_gateway_object, as: :item, dependent: :destroy
extend FriendlyId
friendly_id :base_name, use: :slugged
@ -37,7 +37,7 @@ class Plan < ApplicationRecord
def self.create_for_all_groups(plan_params)
plans = []
Group.all_except_admins.each do |group|
Group.find_each do |group|
plan = if plan_params[:type] == 'PartnerPlan'
PartnerPlan.new(plan_params.except(:group_id, :type))
else
@ -59,14 +59,14 @@ class Plan < ApplicationRecord
end
def create_machines_prices
Machine.all.each do |machine|
Machine.all.find_each do |machine|
default_price = Price.find_by(priceable: machine, plan: nil, group_id: group_id)&.amount || 0
Price.create(priceable: machine, plan: self, group_id: group_id, amount: default_price)
end
end
def create_spaces_prices
Space.all.each do |space|
Space.all.find_each do |space|
default_price = Price.find_by(priceable: space, plan: nil, group_id: group_id)&.amount || 0
Price.create(priceable: space, plan: self, group_id: group_id, amount: default_price)
end
@ -128,7 +128,7 @@ class Plan < ApplicationRecord
end
def set_name
update_columns(name: human_readable_name)
update_columns(name: human_readable_name) # rubocop:disable Rails/SkipsModelValidations
end
def update_gateway_product

View File

@ -8,11 +8,11 @@ class Subscription < ApplicationRecord
belongs_to :statistic_profile
has_one :payment_schedule_object, as: :object, dependent: :destroy
has_one :payment_gateway_object, as: :item
has_one :payment_gateway_object, as: :item, dependent: :destroy
has_many :invoice_items, as: :object, dependent: :destroy
has_many :offer_days, dependent: :destroy
validates_presence_of :plan_id
validates :plan_id, presence: true
validates_with SubscriptionGroupValidator
# creation
@ -21,18 +21,20 @@ class Subscription < ApplicationRecord
after_save :notify_admin_subscribed_plan
after_save :notify_partner_subscribed_plan, if: :of_partner_plan?
delegate :user, to: :statistic_profile
def generate_and_save_invoice(operator_profile_id)
generate_invoice(operator_profile_id).save
end
def expire(time)
if !expired?
update_columns(expiration_date: time, canceled_at: time)
if expired?
false
else
update_columns(expiration_date: time, canceled_at: time) # rubocop:disable Rails/SkipsModelValidations
notify_admin_subscription_canceled
notify_member_subscription_canceled
true
else
false
end
end
@ -47,10 +49,6 @@ class Subscription < ApplicationRecord
expiration_date
end
def user
statistic_profile.user
end
def original_payment_schedule
payment_schedule_object&.payment_schedule
end

View File

@ -2,20 +2,14 @@
# Provides methods for Groups
class GroupService
def self.list(operator, filters = {})
groups = if operator&.admin?
Group.where(nil)
else
Group.where.not(slug: 'admins')
end
def self.list(filters = {})
groups = Group.where(nil)
if filters[:disabled].present?
state = filters[:disabled] == 'false' ? [nil, false] : true
groups = groups.where(disabled: state)
end
groups = groups.where.not(slug: 'admins') if filters[:admins] == 'false'
groups
end
end

View File

@ -15,12 +15,6 @@ class Members::MembersService
return false
end
if admin_group_change?(params)
# an admin cannot change his group
@member.errors.add(:group_id, I18n.t('members.admins_cant_change_group'))
return false
end
group_changed = user_group_change?(params)
ex_group = @member.group
@ -130,9 +124,7 @@ class Members::MembersService
@member.remove_role ex_role
@member.add_role new_role
# if the new role is 'admin', then change the group to the admins group, otherwise to change to the provided group
group_id = new_role == 'admin' ? Group.find_by(slug: 'admins').id : new_group_id
@member.update(group_id: group_id)
@member.update(group_id: new_group_id)
# notify
NotificationCenter.call type: 'notify_user_role_update',
@ -176,10 +168,6 @@ class Members::MembersService
params[:group_id] && @member.group_id != params[:group_id].to_i && !@member.subscribed_plan.nil?
end
def admin_group_change?(params)
params[:group_id] && params[:group_id].to_i != Group.find_by(slug: 'admins').id && @member.admin?
end
def user_group_change?(params)
@member.group_id && params[:group_id] && @member.group_id != params[:group_id].to_i
end

View File

@ -36,9 +36,6 @@ class UserService
admin = User.new(params.merge(password: generated_password))
admin.send :set_slug
# we associate the admin group to prevent linking any other 'normal' group (which won't be deletable afterwards)
admin.group = Group.find_by(slug: 'admins')
# if the authentication is made through an SSO, generate a migration token
admin.generate_auth_migration_token unless AuthProvider.active.providable_type == DatabaseProvider.name

View File

@ -2,18 +2,16 @@
ActiveRecord::Base.class_eval do
def dump_fixture
fixture_file = "#{Rails.root}/test/fixtures/#{self.class.table_name}.yml"
fixture_file = Rails.root.join("/test/fixtures/#{self.class.table_name}.yml")
File.open(fixture_file, 'a') do |f|
f.puts({ "#{self.class.table_name.singularize}_#{id}" => attributes }.
to_yaml.sub!(/---\s?/, "\n"))
f.puts({ "#{self.class.table_name.singularize}_#{id}" => attributes }.to_yaml.sub!(/---\s?/, "\n"))
end
end
def self.dump_fixtures
fixture_file = "#{Rails.root}/test/fixtures/#{table_name}.yml"
fixture_file = Rails.root.join("/test/fixtures/#{table_name}.yml")
mode = (File.exist?(fixture_file) ? 'a' : 'w')
File.open(fixture_file, mode) do |f|
if attribute_names.include?('id')
all.each do |instance|
f.puts({ "#{table_name.singularize}_#{instance.id}" => instance.attributes }.to_yaml.sub!(/---\s?/, "\n"))

View File

@ -1,8 +0,0 @@
# Be sure to restart your server when you modify this file.
# ActiveSupport::Reloader.to_prepare do
# ApplicationController.renderer.defaults.merge!(
# http_host: 'example.org',
# https: false
# )
# end

View File

@ -1,39 +0,0 @@
# # frozen_string_literal: true
#
# # Be sure to restart your server when you modify this file.
#
# # Version of your assets, change this if you want to expire all your assets.
# Rails.application.config.assets.version = '1.0'
#
# # allow use rails helpers in angular templates
# Rails.application.config.assets.configure do |env|
# env.context_class.class_eval do
# include ActionView::Helpers
# include Rails.application.routes.url_helpers
# end
# end
#
# # Add additional assets to the asset load path.
# # Rails.application.config.assets.paths << Emoji.images_path
# # Add Yarn node_modules folder to the asset load path.
# Rails.application.config.assets.paths << Rails.root.join('node_modules')
#
# # Precompile additional assets.
# # application.js, application.css, and all non-JS/CSS in the app/assets
# # folder are already added.
# # Rails.application.config.assets.precompile += %w( admin.js admin.css )
#
# Rails.application.config.assets.precompile += %w[
# fontawesome-webfont.eot
# fontawesome-webfont.woff
# fontawesome-webfont.svg
# fontawesome-webfont.ttf
# ]
# Rails.application.config.assets.precompile += %w[app.printer.css]
#
# Rails.application.config.assets.precompile += %w[
# angular-i18n/angular-locale_*.js
# moment/locale/*.js
# summernote/lang/*.js
# fullcalendar/dist/lang/*.js
# ]

View File

@ -1,7 +0,0 @@
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
# Rails.backtrace_cleaner.remove_silencers!

View File

@ -8,16 +8,6 @@
Rails.application.config.content_security_policy do |policy| # # If you are using webpack-dev-server then specify webpack-dev-server host
policy.connect_src :self, :https, :wss, 'http://localhost:3035', 'ws://localhost:3035' if Rails.env.development?
# policy.default_src :self, :https
# policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
# policy.style_src :self, :https
# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
end
# If you are using UJS then enable automatic nonce generation

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# FriendlyId Global Configuration
#
# Use this to set up shared configuration options for your entire application.
@ -16,8 +18,7 @@ FriendlyId.defaults do |config|
# undesirable to allow as slugs. Edit this list as needed for your app.
config.use :reserved
config.reserved_words = %w(new edit index session login logout users
stylesheets assets javascripts images)
config.reserved_words = %w[new edit index session login logout users stylesheets assets javascripts images]
# ## Friendly Finders
#

View File

@ -2,14 +2,15 @@
# Be sure to restart your server when you modify this file.
redis_host = ENV['REDIS_HOST'] || 'localhost'
redis_host = ENV.fetch('REDIS_HOST', 'localhost')
Rails.application.config.session_store :redis_session_store,
redis: {
expire_after: 14.days, # cookie expiration
ttl: 14.days, # Redis expiration, defaults to 'expire_after'
key_prefix: 'fabmanager:session:',
url: "redis://#{redis_host}:6379",
url: "redis://#{redis_host}:6379"
},
key: '_Fab-manager_session',
secure: (Rails.env.production? || Rails.env.staging?) && !Rails.application.secrets.allow_insecure_http
secure: (Rails.env.production? || Rails.env.staging?) &&
!Rails.application.secrets.allow_insecure_http

View File

@ -5,7 +5,7 @@ require 'sidekiq-scheduler'
require 'sidekiq/middleware/i18n'
require 'sidekiq/server_locale'
redis_host = ENV['REDIS_HOST'] || 'localhost'
redis_host = ENV.fetch('REDIS_HOST', 'localhost')
redis_url = "redis://#{redis_host}:6379"
Sidekiq.configure_server do |config|

View File

@ -57,7 +57,6 @@ en:
#members management
members:
unable_to_change_the_group_while_a_subscription_is_running: "Unable to change the group while a subscription is running"
admins_cant_change_group: "Unable to remove an administrator from his dedicated group"
please_input_the_authentication_code_sent_to_the_address: "Please input the authentication code sent to the e-mail address %{EMAIL}"
your_authentication_code_is_not_valid: "Your authentication code is not valid."
current_authentication_method_no_code: "The current authentication method does not require any migration code"

View File

@ -12,7 +12,9 @@ namespace :fablab do
desc 'add missing VAT rate to history'
task :add_vat_rate, %i[rate date] => :environment do |_task, args|
raise 'Missing argument. Usage exemple: rails fablab:setup:add_vat_rate[20,2014-01-01]. Use 0 to disable' unless args.rate && args.date
unless args.rate && args.date
raise 'Missing argument. Usage exemple: rails fablab:setup:add_vat_rate[20,2014-01-01]. Use 0 to disable'
end
if args.rate == '0'
setting = Setting.find_by(name: 'invoice_VAT-active')
@ -116,6 +118,8 @@ namespace :fablab do
admin.update(group_id: select_group(groups))
PaymentGatewayService.new.create_user(admin.id)
end
print "\e[91m::\e[0m \e[1mRemoving the 'admins' group...\e[0m\n"
Group.find_by(slug: 'admins').destroy
print "\e[32m✅\e[0m \e[1mDone\e[0m\n"
end