From 4b84963d7f151292ad2dce8ee111b2e3d634df84 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 6 Dec 2022 16:08:38 +0100 Subject: [PATCH] (feat) optional external id --- CHANGELOG.md | 1 + app/controllers/api/members_controller.rb | 1 + app/doc/open_api/v1/users_doc.rb | 6 + .../data-mapping-form.tsx | 2 +- .../profile-form-option.tsx | 4 +- .../components/user/user-profile-form.tsx | 26 +++-- app/frontend/src/javascript/models/setting.ts | 1 + app/frontend/src/javascript/models/user.ts | 3 + .../templates/admin/members/edit.html | 1 + app/frontend/templates/admin/members/new.html | 1 + .../templates/admin/settings/compte.html | 13 +++ .../templates/dashboard/settings.html | 2 +- app/frontend/templates/profile/complete.html | 1 + .../concerns/user_ressources_concern.rb | 44 +++++++ app/models/concerns/user_role_concern.rb | 72 ++++++++++++ app/models/setting.rb | 3 +- app/models/user.rb | 108 ++---------------- app/policies/setting_policy.rb | 3 +- app/services/members/import_service.rb | 33 +++--- app/views/api/members/_member.json.jbuilder | 2 +- .../open_api/v1/users/_user.json.jbuilder | 8 +- config/locales/app.admin.en.yml | 3 + config/locales/app.shared.en.yml | 1 + config/locales/en.yml | 1 + .../20221206100225_add_external_id_to_user.rb | 9 ++ db/schema.rb | 22 ++-- db/seeds.rb | 2 + public/example.csv | 6 +- test/fixtures/users.yml | 10 ++ test/integration/open_api/users_test.rb | 4 + 30 files changed, 245 insertions(+), 148 deletions(-) create mode 100644 app/models/concerns/user_ressources_concern.rb create mode 100644 app/models/concerns/user_role_concern.rb create mode 100644 db/migrate/20221206100225_add_external_id_to_user.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 2db5c6edd..6fd7ddca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog Fab-manager +- Optional external identifier for users - Accounting data is now built each night and saved in database - OpenAPI endpoint to fetch accounting data - Fix a bug: providing an array of attributes to filter OpenApi data, results in error diff --git a/app/controllers/api/members_controller.rb b/app/controllers/api/members_controller.rb index fa619106d..b53016202 100644 --- a/app/controllers/api/members_controller.rb +++ b/app/controllers/api/members_controller.rb @@ -232,6 +232,7 @@ class API::MembersController < API::ApiController elsif current_user.admin? || current_user.manager? params.require(:user).permit(:username, :email, :password, :password_confirmation, :is_allow_contact, :is_allow_newsletter, :group_id, + :external_id, tag_ids: [], profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job, :facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo, diff --git a/app/doc/open_api/v1/users_doc.rb b/app/doc/open_api/v1/users_doc.rb index 3f1b3c6b5..311004779 100644 --- a/app/doc/open_api/v1/users_doc.rb +++ b/app/doc/open_api/v1/users_doc.rb @@ -25,6 +25,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc "id": 1746, "email": "xxxxxxx@xxxx.com", "created_at": "2016-05-04T17:21:48.403+02:00", + "external_id": "J5821-4" "full_name": "xxxx xxxx", "group": { "id": 1, @@ -36,6 +37,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc "id": 1745, "email": "xxxxxxx@gmail.com", "created_at": "2016-05-03T15:21:13.125+02:00", + "external_id": "J5846-4" "full_name": "xxxxx xxxxx", "group": { "id": 2, @@ -47,6 +49,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc "id": 1744, "email": "xxxxxxx@gmail.com", "created_at": "2016-05-03T13:51:03.223+02:00", + "external_id": "J5900-1" "full_name": "xxxxxxx xxxx", "group": { "id": 1, @@ -58,6 +61,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc "id": 1743, "email": "xxxxxxxx@setecastronomy.eu", "created_at": "2016-05-03T12:24:38.724+02:00", + "external_id": "P4172-4" "full_name": "xxx xxxxxxx", "group": { "id": 1, @@ -75,6 +79,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc "id": 1746, "email": "xxxxxxxxxxxx", "created_at": "2016-05-04T17:21:48.403+02:00", + "external_id": "J5500-4" "full_name": "xxxx xxxxxx", "group": { "id": 1, @@ -86,6 +91,7 @@ class OpenAPI::V1::UsersDoc < OpenAPI::V1::BaseDoc "id": 1745, "email": "xxxxxxxxx@gmail.com", "created_at": "2016-05-03T15:21:13.125+02:00", + "external_id": null, "full_name": "xxxxx xxxxxx", "group": { "id": 2, diff --git a/app/frontend/src/javascript/components/authentication-provider/data-mapping-form.tsx b/app/frontend/src/javascript/components/authentication-provider/data-mapping-form.tsx index acc547701..b30f6afd3 100644 --- a/app/frontend/src/javascript/components/authentication-provider/data-mapping-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/data-mapping-form.tsx @@ -114,7 +114,7 @@ export const DataMappingForm = { - if (currentFormValues[index]?.id) { + if (currentFormValues && currentFormValues[index]?.id) { if (currentFormValues[index]._destroy) return 'destroyed-item'; return 'saved-item'; } diff --git a/app/frontend/src/javascript/components/profile-completion/profile-form-option.tsx b/app/frontend/src/javascript/components/profile-completion/profile-form-option.tsx index 261bf1cb0..9ba0aa47d 100644 --- a/app/frontend/src/javascript/components/profile-completion/profile-form-option.tsx +++ b/app/frontend/src/javascript/components/profile-completion/profile-form-option.tsx @@ -15,6 +15,7 @@ declare const Application: IApplication; interface ProfileFormOptionProps { user: User, + operator: User, activeProvider: ActiveProviderResponse, onError: (message: string) => void, onSuccess: (user: User) => void, @@ -27,7 +28,7 @@ interface ProfileFormOptionProps { * (*) This component handle the first case. * It also deals with duplicate email addresses in database */ -export const ProfileFormOption: React.FC = ({ user, activeProvider, onError, onSuccess }) => { +export const ProfileFormOption: React.FC = ({ user, operator, activeProvider, onError, onSuccess }) => { const { t } = useTranslation('logged'); const userLib = new UserLib(user); @@ -60,6 +61,7 @@ export const ProfileFormOption: React.FC = ({ user, acti void, onSuccess: (user: User) => void, @@ -51,7 +52,7 @@ interface UserProfileFormProps { /** * Form component to create or update a user */ -export const UserProfileForm: React.FC = ({ action, size, user, className, onError, onSuccess, showGroupInput, showTermsAndConditionsInput, showTrainingsInput, showTagsInput }) => { +export const UserProfileForm: React.FC = ({ action, size, user, operator, className, onError, onSuccess, showGroupInput, showTermsAndConditionsInput, showTrainingsInput, showTagsInput }) => { const { t } = useTranslation('shared'); // regular expression to validate the input fields @@ -66,7 +67,7 @@ export const UserProfileForm: React.FC = ({ action, size, const [groups, setGroups] = useState[]>([]); const [termsAndConditions, setTermsAndConditions] = useState(null); const [profileCustomFields, setProfileCustomFields] = useState([]); - const [requiredFieldsSettings, setRequiredFieldsSettings] = useState>(new Map()); + const [fieldsSettings, setFieldsSettings] = useState>(new Map()); useEffect(() => { AuthProviderAPI.active().then(data => { @@ -94,8 +95,8 @@ export const UserProfileForm: React.FC = ({ action, size, }); setValue('invoicing_profile_attributes.user_profile_custom_fields_attributes', userProfileCustomFields); }).catch(error => onError(error)); - SettingAPI.query(['phone_required', 'address_required']) - .then(settings => setRequiredFieldsSettings(settings)) + SettingAPI.query(['phone_required', 'address_required', 'external_id']) + .then(settings => setFieldsSettings(settings)) .catch(error => onError(error)); }, []); @@ -150,6 +151,10 @@ export const UserProfileForm: React.FC = ({ action, size, * Check if the given field path should be disabled */ const isDisabled = function (id: string) { + // some fields may be reserved in edition for priviledged users + if (UserFieldsReservedForPrivileged.includes(id) && !(new UserLib(operator).isPrivileged(user))) { + return true; + } // if the current provider is the local database, then all fields are enabled if (isLocalDatabaseProvider) { return false; @@ -209,7 +214,7 @@ export const UserProfileForm: React.FC = ({ action, size, value: phoneRegex, message: t('app.shared.user_profile_form.phone_number_invalid') }, - required: requiredFieldsSettings.get('phone_required') === 'true' + required: fieldsSettings.get('phone_required') === 'true' }} disabled={isDisabled} formState={formState} @@ -222,7 +227,7 @@ export const UserProfileForm: React.FC = ({ action, size, @@ -234,6 +239,11 @@ export const UserProfileForm: React.FC = ({ action, size, disabled={isDisabled} formState={formState} label={t('app.shared.user_profile_form.pseudonym')} /> + {fieldsSettings.get('external_id') === 'true' && } = (props) => { ); }; -Application.Components.component('userProfileForm', react2angular(UserProfileFormWrapper, ['action', 'size', 'user', 'className', 'onError', 'onSuccess', 'showGroupInput', 'showTermsAndConditionsInput', 'showTagsInput', 'showTrainingsInput'])); +Application.Components.component('userProfileForm', react2angular(UserProfileFormWrapper, ['action', 'size', 'user', 'operator', 'className', 'onError', 'onSuccess', 'showGroupInput', 'showTermsAndConditionsInput', 'showTagsInput', 'showTrainingsInput'])); diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index c735eebc7..ff3f7821d 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -164,6 +164,7 @@ export const accountSettings = [ 'phone_required', 'confirmation_required', 'address_required', + 'external_id', 'user_change_group', 'user_validation_required', 'user_validation_required_list' diff --git a/app/frontend/src/javascript/models/user.ts b/app/frontend/src/javascript/models/user.ts index de7066e55..d6da7591e 100644 --- a/app/frontend/src/javascript/models/user.ts +++ b/app/frontend/src/javascript/models/user.ts @@ -12,6 +12,7 @@ type ProfileAttributesSocial = { export interface User { id: number, username?: string, + external_id?: string, email: string, group_id?: number, role?: UserRole @@ -130,3 +131,5 @@ export const UserFieldMapping = Object.assign({ is_allow_newsletter: 'user.is_allow_newsletter', group_id: 'user.group_id' }, ...socialMappings); + +export const UserFieldsReservedForPrivileged = ['external_id']; diff --git a/app/frontend/templates/admin/members/edit.html b/app/frontend/templates/admin/members/edit.html index 905924ff5..0de65fcef 100644 --- a/app/frontend/templates/admin/members/edit.html +++ b/app/frontend/templates/admin/members/edit.html @@ -50,6 +50,7 @@
+
+

{{ 'app.admin.settings.external_id' }}

+

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

+
+ + +
+

{{ 'app.admin.settings.account.organization' }}

diff --git a/app/frontend/templates/dashboard/settings.html b/app/frontend/templates/dashboard/settings.html index 8d67a81d6..5d7e3da3c 100644 --- a/app/frontend/templates/dashboard/settings.html +++ b/app/frontend/templates/dashboard/settings.html @@ -108,7 +108,7 @@
- +
diff --git a/app/frontend/templates/profile/complete.html b/app/frontend/templates/profile/complete.html index f945cfb73..979a32074 100644 --- a/app/frontend/templates/profile/complete.html +++ b/app/frontend/templates/profile/complete.html @@ -41,6 +41,7 @@ diff --git a/app/models/concerns/user_ressources_concern.rb b/app/models/concerns/user_ressources_concern.rb new file mode 100644 index 000000000..55ab14b0c --- /dev/null +++ b/app/models/concerns/user_ressources_concern.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Add resources-related functionalities to the user model (eg. Reservation, Subscrtion, Project, etc.) +module UserRessourcesConcern + extend ActiveSupport::Concern + + included do + def training_machine?(machine) + return true if admin? || manager? + + trainings.map(&:machines).flatten.uniq.include?(machine) + end + + def packs?(item) + return true if admin? + + PrepaidPackService.user_packs(self, item).count.positive? + end + + def next_training_reservation_by_machine(machine) + reservations.where(reservable_type: 'Training', reservable_id: machine.trainings.map(&:id)) + .includes(:slots) + .where('slots.start_at>= ?', DateTime.current) + .order('slots.start_at': :asc) + .references(:slots) + .limit(1) + .first + end + + def subscribed_plan + return nil if subscription.nil? || subscription.expired_at < DateTime.current + + subscription.plan + end + + def subscription + subscriptions.order(:created_at).last + end + + def all_projects + my_projects.to_a.concat projects + end + end +end diff --git a/app/models/concerns/user_role_concern.rb b/app/models/concerns/user_role_concern.rb new file mode 100644 index 000000000..d9ee3fde0 --- /dev/null +++ b/app/models/concerns/user_role_concern.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# Add role-based functionalities to the user model +module UserRoleConcern + extend ActiveSupport::Concern + + included do + def admin? + has_role? :admin + end + + def member? + has_role? :member + end + + def manager? + has_role? :manager + end + + def partner? + has_role? :partner + end + + def privileged? + admin? || manager? + end + + def role + if admin? + 'admin' + elsif manager? + 'manager' + elsif member? + 'member' + else + 'other' + end + end + end + + class_methods do + def admins + User.with_role(:admin) + end + + def members + User.with_role(:member) + end + + def partners + User.with_role(:partner) + end + + def managers + User.with_role(:manager) + end + + def admins_and_managers + User.with_any_role(:admin, :manager) + end + + def online_payers + User.with_any_role(:admin, :manager, :member) + end + + def adminsys + return if Rails.application.secrets.adminsys_email.blank? + + User.find_by('lower(email) = ?', Rails.application.secrets.adminsys_email&.downcase) + end + end +end diff --git a/app/models/setting.rb b/app/models/setting.rb index de078dbf8..312ae1cee 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -157,7 +157,8 @@ class Setting < ApplicationRecord store_module store_withdrawal_instructions store_hidden - advanced_accounting] } + advanced_accounting + external_id] } # WARNING: when adding a new key, you may also want to add it in: # - config/locales/en.yml#settings # - app/frontend/src/javascript/models/setting.ts#SettingName diff --git a/app/models/user.rb b/app/models/user.rb index 7a0d8ff09..28878bc13 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -7,6 +7,8 @@ class User < ApplicationRecord include NotifyWith::NotificationAttachedObject include SingleSignOnConcern + include UserRoleConcern + include UserRessourcesConcern # Include default devise modules. Others available are: # :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, @@ -55,6 +57,7 @@ class User < ApplicationRecord email&.downcase! end + before_validation :set_external_id_nil before_create :assign_default_role after_create :init_dependencies after_update :update_invoicing_profile, if: :invoicing_data_was_modified? @@ -79,6 +82,7 @@ class User < ApplicationRecord validate :cgu_must_accept, if: :new_record? validates :username, presence: true, uniqueness: true, length: { maximum: 30 } + validates :external_id, uniqueness: true, allow_blank: true validate :password_complexity scope :active, -> { where(is_active: true) } @@ -96,104 +100,6 @@ class User < ApplicationRecord ) end - def self.admins - User.with_role(:admin) - end - - def self.members - User.with_role(:member) - end - - def self.partners - User.with_role(:partner) - end - - def self.managers - User.with_role(:manager) - end - - def self.admins_and_managers - User.with_any_role(:admin, :manager) - end - - def self.online_payers - User.with_any_role(:admin, :manager, :member) - end - - def self.adminsys - return if Rails.application.secrets.adminsys_email.blank? - - User.find_by('lower(email) = ?', Rails.application.secrets.adminsys_email&.downcase) - end - - def training_machine?(machine) - return true if admin? || manager? - - trainings.map(&:machines).flatten.uniq.include?(machine) - end - - def packs?(item) - return true if admin? - - PrepaidPackService.user_packs(self, item).count.positive? - end - - def next_training_reservation_by_machine(machine) - reservations.where(reservable_type: 'Training', reservable_id: machine.trainings.map(&:id)) - .includes(:slots) - .where('slots.start_at>= ?', DateTime.current) - .order('slots.start_at': :asc) - .references(:slots) - .limit(1) - .first - end - - def subscribed_plan - return nil if subscription.nil? || subscription.expired_at < DateTime.current - - subscription.plan - end - - def subscription - subscriptions.order(:created_at).last - end - - def admin? - has_role? :admin - end - - def member? - has_role? :member - end - - def manager? - has_role? :manager - end - - def partner? - has_role? :partner - end - - def privileged? - admin? || manager? - end - - def role - if admin? - 'admin' - elsif manager? - 'manager' - elsif member? - 'member' - else - 'other' - end - end - - def all_projects - my_projects.to_a.concat projects - end - def generate_subscription_invoice(operator_profile_id) return unless subscription @@ -267,6 +173,10 @@ class User < ApplicationRecord private + def set_external_id_nil + self.external_id = nil if external_id.blank? + end + def assign_default_role add_role(:member) if roles.blank? end @@ -353,6 +263,6 @@ class User < ApplicationRecord def password_complexity return if password.blank? || SecurePassword.is_secured?(password) - errors.add I18n.t("app.public.common.password_is_too_weak"), I18n.t("app.public.common.password_is_too_weak_explanations") + errors.add I18n.t('app.public.common.password_is_too_weak'), I18n.t('app.public.common.password_is_too_weak_explanations') end end diff --git a/app/policies/setting_policy.rb b/app/policies/setting_policy.rb index 86f76b6ee..315262047 100644 --- a/app/policies/setting_policy.rb +++ b/app/policies/setting_policy.rb @@ -42,7 +42,8 @@ class SettingPolicy < ApplicationPolicy payment_gateway payzen_endpoint payzen_public_key public_agenda_module renew_pack_threshold statistics_module pack_only_for_subscription overlapping_categories public_registrations facebook twitter viadeo linkedin instagram youtube vimeo dailymotion github echosciences pinterest lastfm flickr machines_module user_change_group - user_validation_required user_validation_required_list store_module store_withdrawal_instructions store_hidden] + user_validation_required user_validation_required_list store_module store_withdrawal_instructions store_hidden + external_id] end ## diff --git a/app/services/members/import_service.rb b/app/services/members/import_service.rb index d087cfc4b..f5ab29d1d 100644 --- a/app/services/members/import_service.rb +++ b/app/services/members/import_service.rb @@ -27,13 +27,13 @@ class Members::ImportService log << user.errors.to_hash unless user.errors.to_hash.empty? rescue StandardError => e log << e.to_s - puts e - puts e.backtrace + Rails.logger.error e + Rails.logger.debug e.backtrace end rescue ArgumentError => e log << e.to_s - puts e - puts e.backtrace + Rails.logger.error e + Rails.logger.debug e.backtrace end log end @@ -52,6 +52,7 @@ class Members::ImportService res.merge! hashify(row, 'id') res.merge! hashify(row, 'username') res.merge! hashify(row, 'email') + res.merge! hashify(row, 'external_id') res.merge! hashify(row, 'password', value: password) res.merge! hashify(row, 'password', key: :password_confirmation, value: password) res.merge! hashify(row, 'allow_contact', value: row['allow_contact'] == 'yes', key: :is_allow_contact) @@ -93,26 +94,22 @@ class Members::ImportService res.merge! hashify(row, 'softwares', key: :software_mastered) res.merge! hashify(row, 'website') res.merge! hashify(row, 'job') - res.merge! hashify(row, 'facebook') - res.merge! hashify(row, 'twitter') - res.merge! hashify(row, 'googleplus', key: :google_plus) - res.merge! hashify(row, 'viadeo') - res.merge! hashify(row, 'linkedin') - res.merge! hashify(row, 'instagram') - res.merge! hashify(row, 'youtube') - res.merge! hashify(row, 'vimeo') - res.merge! hashify(row, 'dailymotion') - res.merge! hashify(row, 'github') - res.merge! hashify(row, 'echosciences') - res.merge! hashify(row, 'pinterest') - res.merge! hashify(row, 'lastfm') - res.merge! hashify(row, 'flickr') + res.merge! social_networks(row) res[:id] = user.profile.id if user&.profile res end + def social_networks(row) + res = {} + networks = %w[facebook twitter viadeo linkedin instagram youtube vimeo dailymotion github echosciences pinterest lastfm flickr] + networks.each do |network| + res.merge! hashify(row, network) + end + res + end + def invoicing_profile(row, user) res = {} diff --git a/app/views/api/members/_member.json.jbuilder b/app/views/api/members/_member.json.jbuilder index 0476ff19a..5ae5c350e 100644 --- a/app/views/api/members/_member.json.jbuilder +++ b/app/views/api/members/_member.json.jbuilder @@ -1,6 +1,6 @@ # frozen_string_literal: true -json.extract! member, :id, :username, :email, :group_id +json.extract! member, :id, :username, :email, :group_id, :external_id json.role member.roles.first.name json.name member.profile.full_name json.need_completion member.need_completion? diff --git a/app/views/open_api/v1/users/_user.json.jbuilder b/app/views/open_api/v1/users/_user.json.jbuilder index 28e5ef69c..aa25e890e 100644 --- a/app/views/open_api/v1/users/_user.json.jbuilder +++ b/app/views/open_api/v1/users/_user.json.jbuilder @@ -1,8 +1,8 @@ -json.extract! user, :id, :email, :created_at +# frozen_string_literal: true -if user.association(:profile).loaded? - json.full_name user.profile.full_name -end +json.extract! user, :id, :email, :created_at, :external_id + +json.full_name user.profile.full_name if user.association(:profile).loaded? if user.association(:group).loaded? json.group do diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 0fd3c62c7..918150aa0 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1614,6 +1614,9 @@ en: 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" + external_id: "External identifier" + external_id_info_html: "You can set up an external identifier for your users which cannot be modified by the user himself." + enable_external_id: "Enable the external ID" 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.shared.en.yml b/config/locales/app.shared.en.yml index d2895e99c..0537178f4 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -68,6 +68,7 @@ en: declare_organization: "I declare to be an organization" declare_organization_help: "If you declare to be an organization, your invoices will be issued in the name of the organization." pseudonym: "Nickname" + external_id: "External identifier" first_name: "First name" surname: "Surname" email_address: "Email address" diff --git a/config/locales/en.yml b/config/locales/en.yml index 14058295f..bed2d5e0b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -624,3 +624,4 @@ en: store_withdrawal_instructions: "Withdrawal instructions" store_hidden: "Store hidden to the public" advanced_accounting: "Advanced accounting" + external_id: "external identifier" diff --git a/db/migrate/20221206100225_add_external_id_to_user.rb b/db/migrate/20221206100225_add_external_id_to_user.rb new file mode 100644 index 000000000..a791eaa85 --- /dev/null +++ b/db/migrate/20221206100225_add_external_id_to_user.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# From this migration users can be identified by an unique external ID +class AddExternalIdToUser < ActiveRecord::Migration[5.2] + def change + add_column :users, :external_id, :string, null: true + add_index :users, :external_id, unique: true, where: '(external_id IS NOT NULL)', name: 'unique_not_null_external_id' + end +end diff --git a/db/schema.rb b/db/schema.rb index e1ecb1d70..e08fbd013 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_11_22_123605) do +ActiveRecord::Schema.define(version: 2022_12_06_100225) do # These are extensions that must be enabled in order to support this database enable_extension "fuzzystrmatch" @@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do enable_extension "unaccent" create_table "abuses", id: :serial, force: :cascade do |t| - t.string "signaled_type" t.integer "signaled_id" + t.string "signaled_type" t.string "first_name" t.string "last_name" t.string "email" @@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do t.string "locality" t.string "country" t.string "postal_code" - t.string "placeable_type" t.integer "placeable_id" + t.string "placeable_type" t.datetime "created_at" t.datetime "updated_at" end @@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do end create_table "assets", id: :serial, force: :cascade do |t| - t.string "viewable_type" t.integer "viewable_id" + t.string "viewable_type" t.string "attachment" t.string "type" t.datetime "created_at" @@ -176,8 +176,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do end create_table "credits", id: :serial, force: :cascade do |t| - t.string "creditable_type" t.integer "creditable_id" + t.string "creditable_type" t.integer "plan_id" t.integer "hours" t.datetime "created_at" @@ -406,15 +406,15 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do create_table "notifications", id: :serial, force: :cascade do |t| t.integer "receiver_id" - t.string "attached_object_type" t.integer "attached_object_id" + t.string "attached_object_type" t.integer "notification_type_id" t.boolean "is_read", default: false t.datetime "created_at" t.datetime "updated_at" t.string "receiver_type" t.boolean "is_send", default: false - t.jsonb "meta_data", default: "{}" + t.jsonb "meta_data", default: {} t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id" t.index ["receiver_id"], name: "index_notifications_on_receiver_id" end @@ -654,8 +654,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do create_table "prices", id: :serial, force: :cascade do |t| t.integer "group_id" t.integer "plan_id" - t.string "priceable_type" t.integer "priceable_id" + t.string "priceable_type" t.integer "amount" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -855,8 +855,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do t.text "message" t.datetime "created_at" t.datetime "updated_at" - t.string "reservable_type" t.integer "reservable_id" + t.string "reservable_type" t.integer "nb_reserve_places" t.integer "statistic_profile_id" t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id" @@ -865,8 +865,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do create_table "roles", id: :serial, force: :cascade do |t| t.string "name" - t.string "resource_type" t.integer "resource_id" + t.string "resource_type" t.datetime "created_at" t.datetime "updated_at" t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" @@ -1150,9 +1150,11 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do t.inet "last_sign_in_ip" t.string "mapped_from_sso" t.datetime "validated_at" + t.string "external_id" t.index ["auth_token"], name: "index_users_on_auth_token" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true + t.index ["external_id"], name: "unique_not_null_external_id", unique: true, where: "(external_id IS NOT NULL)" t.index ["group_id"], name: "index_users_on_group_id" t.index ["provider"], name: "index_users_on_provider" t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true diff --git a/db/seeds.rb b/db/seeds.rb index 55821e783..ed8caa5a3 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -997,6 +997,8 @@ Setting.set('advanced_accounting', false) unless Setting.find_by(name: 'advanced Setting.set('accounting_VAT_code', '4457') unless Setting.find_by(name: 'accounting_VAT_code').try(:value) +Setting.set('external_id', false) unless Setting.find_by(name: 'external_id').try(:value) + if StatisticCustomAggregation.count.zero? # available reservations hours for machines machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2) diff --git a/public/example.csv b/public/example.csv index 6f6073fe2..f6f7f59f9 100644 --- a/public/example.csv +++ b/public/example.csv @@ -1,3 +1,3 @@ -id;gender;first_name;last_name;username;email;password;birthdate;address;phone;group;tags;trainings;website;job;interests;softwares;allow_contact;allow_newsletter;organization_name;organization_address;facebook;twitter;googleplus;viadeo;linkedin;instagram;youtube;vimeo;dailymotion;github;echosciences;pinterest;lastfm;flickr -;male;jean;dupont;jdupont;jean.dupont@gmail.com;;1970-01-01;12 bvd Libération - 75000 Paris;0123456789;standard;1,2;1;http://www.example.com;Charpentier;Ping-pong;AutoCAD;yes;no;;;http://www.facebook.com/jdupont;;;;;;;;;http://github.com/example;;;; -43;;;;;;newpassword +id;gender;first_name;last_name;username;email;password;external_id;birthdate;address;phone;group;tags;trainings;website;job;interests;softwares;allow_contact;allow_newsletter;organization_name;organization_address;facebook;twitter;viadeo;linkedin;instagram;youtube;vimeo;dailymotion;github;echosciences;pinterest;lastfm;flickr +;male;jean;dupont;jdupont;jean.dupont@gmail.com;;JD84401;1970-01-01;12 bvd Libération - 75000 Paris;123456789;standard;1,2;1;http://www.example.com;Charpentier;Ping-pong;AutoCAD;yes;no;;;http://www.facebook.com/jdupont;;;;;;;;http://github.com/example;;;; +43;;;;;;newP@ssword5;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index dc1a21bc7..fbb3d0cb6 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -29,6 +29,7 @@ user_1: auth_token: merged_at: is_allow_newsletter: true + external_id: J5821-4 user_2: id: 2 @@ -61,6 +62,7 @@ user_2: auth_token: merged_at: is_allow_newsletter: true + external_id: J5846-4 user_3: id: 3 @@ -93,6 +95,7 @@ user_3: auth_token: merged_at: is_allow_newsletter: false + external_id: J5900-1 user_4: id: 4 @@ -125,6 +128,7 @@ user_4: auth_token: merged_at: is_allow_newsletter: false + external_id: P4172-4 user_5: id: 5 @@ -157,6 +161,7 @@ user_5: auth_token: merged_at: is_allow_newsletter: true + external_id: J5500-4 user_6: id: 6 @@ -189,6 +194,7 @@ user_6: auth_token: merged_at: is_allow_newsletter: true + external_id: user_7: id: 7 @@ -221,6 +227,7 @@ user_7: auth_token: merged_at: is_allow_newsletter: false + external_id: user_8: id: 8 @@ -253,6 +260,7 @@ user_8: auth_token: merged_at: is_allow_newsletter: false + external_id: user_9: id: 9 @@ -285,6 +293,7 @@ user_9: auth_token: merged_at: is_allow_newsletter: true + external_id: user_10: id: 10 @@ -317,3 +326,4 @@ user_10: auth_token: merged_at: is_allow_newsletter: true + external_id: diff --git a/test/integration/open_api/users_test.rb b/test/integration/open_api/users_test.rb index 5dfdaf268..fa806fbd6 100644 --- a/test/integration/open_api/users_test.rb +++ b/test/integration/open_api/users_test.rb @@ -12,6 +12,10 @@ class OpenApi::UsersTest < ActionDispatch::IntegrationTest test 'list all users' do get '/open_api/v1/users', headers: open_api_headers(@token) assert_response :success + assert_equal Mime[:json], response.content_type + + users = json_response(response.body) + assert_not_nil(users[:users].detect { |u| u[:external_id] == 'J5821-4' }) end test 'list all users with pagination' do