mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-28 09:24:24 +01:00
(feat) optional external id
This commit is contained in:
parent
e246480049
commit
4b84963d7f
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -114,7 +114,7 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
|
||||
* Return a className based on the current mapping-item status
|
||||
*/
|
||||
const itemStatus = (index: number): string => {
|
||||
if (currentFormValues[index]?.id) {
|
||||
if (currentFormValues && currentFormValues[index]?.id) {
|
||||
if (currentFormValues[index]._destroy) return 'destroyed-item';
|
||||
return 'saved-item';
|
||||
}
|
||||
|
@ -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<ProfileFormOptionProps> = ({ user, activeProvider, onError, onSuccess }) => {
|
||||
export const ProfileFormOption: React.FC<ProfileFormOptionProps> = ({ user, operator, activeProvider, onError, onSuccess }) => {
|
||||
const { t } = useTranslation('logged');
|
||||
|
||||
const userLib = new UserLib(user);
|
||||
@ -60,6 +61,7 @@ export const ProfileFormOption: React.FC<ProfileFormOptionProps> = ({ user, acti
|
||||
<UserProfileForm onError={onError}
|
||||
action="update"
|
||||
user={user}
|
||||
operator={operator}
|
||||
onSuccess={onSuccess}
|
||||
size="small"
|
||||
showGroupInput
|
||||
|
@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { useForm, useWatch, ValidateResult } from 'react-hook-form';
|
||||
import { isNil as _isNil } from 'lodash';
|
||||
import { User, UserFieldMapping } from '../../models/user';
|
||||
import { User, UserFieldMapping, UserFieldsReservedForPrivileged } from '../../models/user';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { Loader } from '../base/loader';
|
||||
import { FormInput } from '../form/form-input';
|
||||
@ -39,6 +39,7 @@ interface UserProfileFormProps {
|
||||
action: 'create' | 'update',
|
||||
size?: 'small' | 'large',
|
||||
user: User,
|
||||
operator: User,
|
||||
className?: string,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (user: User) => void,
|
||||
@ -51,7 +52,7 @@ interface UserProfileFormProps {
|
||||
/**
|
||||
* Form component to create or update a user
|
||||
*/
|
||||
export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size, user, className, onError, onSuccess, showGroupInput, showTermsAndConditionsInput, showTrainingsInput, showTagsInput }) => {
|
||||
export const UserProfileForm: React.FC<UserProfileFormProps> = ({ 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<UserProfileFormProps> = ({ action, size,
|
||||
const [groups, setGroups] = useState<SelectOption<number>[]>([]);
|
||||
const [termsAndConditions, setTermsAndConditions] = useState<CustomAsset>(null);
|
||||
const [profileCustomFields, setProfileCustomFields] = useState<ProfileCustomField[]>([]);
|
||||
const [requiredFieldsSettings, setRequiredFieldsSettings] = useState<Map<SettingName, string>>(new Map());
|
||||
const [fieldsSettings, setFieldsSettings] = useState<Map<SettingName, string>>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
AuthProviderAPI.active().then(data => {
|
||||
@ -94,8 +95,8 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ 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<UserProfileFormProps> = ({ 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<UserProfileFormProps> = ({ 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<UserProfileFormProps> = ({ action, size,
|
||||
<FormInput id="invoicing_profile_attributes.address_attributes.address"
|
||||
register={register}
|
||||
disabled={isDisabled}
|
||||
rules={{ required: requiredFieldsSettings.get('address_required') === 'true' }}
|
||||
rules={{ required: fieldsSettings.get('address_required') === 'true' }}
|
||||
label={t('app.shared.user_profile_form.address')} />
|
||||
</div>
|
||||
</div>
|
||||
@ -234,6 +239,11 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
disabled={isDisabled}
|
||||
formState={formState}
|
||||
label={t('app.shared.user_profile_form.pseudonym')} />
|
||||
{fieldsSettings.get('external_id') === 'true' && <FormInput id="external_id"
|
||||
register={register}
|
||||
disabled={isDisabled}
|
||||
formState={formState}
|
||||
label={t('app.shared.user_profile_form.external_id')} />}
|
||||
<FormInput id="email"
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
@ -398,4 +408,4 @@ const UserProfileFormWrapper: React.FC<UserProfileFormProps> = (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']));
|
||||
|
@ -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'
|
||||
|
@ -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'];
|
||||
|
@ -50,6 +50,7 @@
|
||||
<section class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
<user-profile-form user="user"
|
||||
operator="currentUser"
|
||||
action="'update'"
|
||||
on-error="onError"
|
||||
on-success="onUserSuccess"
|
||||
|
@ -35,6 +35,7 @@
|
||||
<div class="panel-body m-r">
|
||||
|
||||
<user-profile-form user="user"
|
||||
operator="currentUser"
|
||||
action="'create'"
|
||||
on-error="onError"
|
||||
on-success="onUserSuccess"
|
||||
|
@ -126,6 +126,19 @@
|
||||
</boolean-setting>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.external_id' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" translate>
|
||||
{{ 'app.admin.settings.external_id_info_html' }}
|
||||
</p>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<boolean-setting name="'external_id'"
|
||||
label="'app.admin.settings.enable_external_id' | translate"
|
||||
on-success="onSuccess"
|
||||
on-error="onError">
|
||||
</boolean-setting>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.account.organization' }}</h3>
|
||||
|
@ -108,7 +108,7 @@
|
||||
</section>
|
||||
<section class="panel panel-default bg-light m">
|
||||
<div class="panel-body m-r">
|
||||
<user-profile-form user="user" action="'update'" on-error="onError" on-success="onSuccess" />
|
||||
<user-profile-form user="user" operator="user" action="'update'" on-error="onError" on-success="onSuccess" />
|
||||
</div> <!-- ./panel-body -->
|
||||
</section>
|
||||
</div>
|
||||
|
@ -41,6 +41,7 @@
|
||||
<profile-form-option on-error="onError"
|
||||
on-success="onSuccess"
|
||||
user="user"
|
||||
operator="currentUser"
|
||||
active-provider="activeProvider" />
|
||||
</div>
|
||||
</div>
|
||||
|
44
app/models/concerns/user_ressources_concern.rb
Normal file
44
app/models/concerns/user_ressources_concern.rb
Normal file
@ -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
|
72
app/models/concerns/user_role_concern.rb
Normal file
72
app/models/concerns/user_role_concern.rb
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
##
|
||||
|
@ -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 = {}
|
||||
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
@ -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.<br/><strong>Please note</strong> 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 <a href='http://www.google.com/recaptcha/admin' target='_blank'>an API key pair</a> to start using the captcha."
|
||||
site_key: "Site key"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
9
db/migrate/20221206100225_add_external_id_to_user.rb
Normal file
9
db/migrate/20221206100225_add_external_id_to_user.rb
Normal file
@ -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
|
22
db/schema.rb
22
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
|
||||
|
@ -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)
|
||||
|
@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
10
test/fixtures/users.yml
vendored
10
test/fixtures/users.yml
vendored
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user