1
0
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:
Sylvain 2022-12-06 16:08:38 +01:00
parent e246480049
commit 4b84963d7f
30 changed files with 245 additions and 148 deletions

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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';
}

View File

@ -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

View File

@ -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']));

View File

@ -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'

View File

@ -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'];

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -41,6 +41,7 @@
<profile-form-option on-error="onError"
on-success="onSuccess"
user="user"
operator="currentUser"
active-provider="activeProvider" />
</div>
</div>

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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
##

View File

@ -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 = {}

View File

@ -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?

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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"

View 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

View File

@ -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

View File

@ -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)

View File

@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;

1 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 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
2 ;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;;;; 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
3 43;;;;;;newpassword 43 newP@ssword5

View File

@ -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:

View File

@ -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