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

Merge branch 'statisticprofile' into dev

This commit is contained in:
Sylvain 2019-06-05 16:10:22 +02:00
commit 971102ec1d
79 changed files with 653 additions and 373 deletions

View File

@ -1,3 +1,3 @@
web: bundle exec rails server puma -p $PORT -b0.0.0.0
#web: bundle exec rails server puma -p $PORT -b0.0.0.0
worker: bundle exec sidekiq -C ./config/sidekiq.yml
mail: bundle exec mailcatcher --foreground

View File

@ -264,8 +264,8 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
* @return {string} 'male' or 'female'
*/
var getGender = function (user) {
if (user.profile) {
if (user.profile.gender === 'true') { return 'male'; } else { return 'female'; }
if (user.statistic_profile) {
if (user.statistic_profile.gender === 'true') { return 'male'; } else { return 'female'; }
} else { return 'other'; }
};

View File

@ -225,8 +225,11 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
* Callback when the search field content changes: reload the search results
*/
$scope.updateTextSearch = function () {
resetSearchMember();
return memberSearch();
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(function() {
resetSearchMember();
memberSearch();
}, 300);
};
/**
@ -252,6 +255,11 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
}
};
/**
* Will temporize the search query to prevent overloading the API
*/
var searchTimeout = null;
/**
* Iterate through the provided array and return the index of the requested admin
* @param admins {Array} full list of users with role 'admin'
@ -267,13 +275,13 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
*/
var resetSearchMember = function () {
$scope.member.noMore = false;
return $scope.member.page = 1;
$scope.member.page = 1;
};
/**
* Run a search query with the current parameters set ($scope.member[searchText,order,page])
* and affect or append the result in $scope.members, depending on the concat parameter
* @param concat {boolean} if true, the result will be append to $scope.members instead of being affected
* @param [concat] {boolean} if true, the result will be append to $scope.members instead of being affected
*/
var memberSearch = function (concat) {
Member.list({
@ -536,7 +544,7 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
CSRF.setMetaTags();
// init the birth date to JS object
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate();
$scope.user.statistic_profile.birthday = moment($scope.user.statistic_profile.birthday).toDate();
// the user subscription
if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) {
@ -579,7 +587,11 @@ Application.Controllers.controller('NewMemberController', ['$scope', '$state', '
$scope.password = { change: false };
// Default member's profile parameters
$scope.user = { plan_interval: '' };
$scope.user = {
plan_interval: '',
invoicing_profile: {},
statistic_profile: {}
};
// Callback when the admin check/uncheck the box telling that the new user is an organization.
// Disable or enable the organization fields in the form, accordingly
@ -604,9 +616,10 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A
// default admin profile
let getGender;
$scope.admin = {
profile_attributes: {
statistic_profile_attributes: {
gender: true
},
profile_attributes: {},
invoicing_profile_attributes: {}
};
@ -650,8 +663,8 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A
* @return {string} 'male' or 'female'
*/
return getGender = function (user) {
if (user.profile_attributes) {
if (user.profile_attributes.gender) { return 'male'; } else { return 'female'; }
if (user.statistic_profile_attributes) {
if (user.statistic_profile_attributes.gender) { return 'male'; } else { return 'female'; }
} else { return 'other'; }
};
}

View File

@ -86,7 +86,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
return $uibModal.open({
templateUrl: '<%= asset_path "shared/signupModal.html" %>',
size: 'md',
controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', function ($scope, $uibModalInstance, Group, CustomAsset) {
controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', 'growl', '_t', function ($scope, $uibModalInstance, Group, CustomAsset, growl, _t) {
// default parameters for the date picker in the account creation modal
$scope.datePicker = {
format: Fablab.uibDateFormat,
@ -134,8 +134,13 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
delete $scope.user.organization;
// register on server
return Auth.register($scope.user).then(function (user) {
// creation successful
$uibModalInstance.close(user);
if (user.id) {
// creation successful
$uibModalInstance.close(user);
} else {
// the user was not saved in database, something wrong occurred
growl.error(_t('unexpected_error_occurred'));
}
}, function (error) {
// creation failed...
// restore organization param

View File

@ -258,7 +258,7 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
CSRF.setMetaTags();
// init the birth date to JS object
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate();
$scope.user.statistic_profile.birthday = moment($scope.user.statistic_profile.birthday).toDate();
if ($scope.activeProvider.providable_type !== 'DatabaseProvider') {
$scope.preventPassword = true;

View File

@ -150,8 +150,8 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
* @return {string} 'male' or 'female'
*/
$scope.getGender = function (user) {
if (user && user.profile) {
if (user.profile.gender === 'true') { return 'male'; } else { return 'female'; }
if (user && user.statistic_profile) {
if (user.statistic_profile.gender === 'true') { return 'male'; } else { return 'female'; }
} else { return 'other'; }
};

View File

@ -206,7 +206,7 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo
CSRF.setMetaTags();
// init the birth date to JS object
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate();
$scope.user.statistic_profile.birthday = moment($scope.user.statistic_profile.birthday).toDate();
// bind fields protection with sso fields
angular.forEach(activeProviderPromise.mapping, function (map) { $scope.preventField[map] = true; });

View File

@ -1,18 +1,18 @@
<div class="row m-t">
<div class="col-sm-offset-3 col-sm-6">
<div class="form-group" ng-class="{'has-error': adminForm['admin[profile_attributes][gender]'].$dirty && adminForm['admin[profile_attributes][gender]'].$invalid}">
<div class="form-group" ng-class="{'has-error': adminForm['admin[statistic_profile_attributes][gender]'].$dirty && adminForm['admin[statistic_profile_attributes][gender]'].$invalid}">
<label class="checkbox-inline btn btn-default">
<input type="radio"
name="admin[profile_attributes][gender]"
ng-model="admin.profile_attributes.gender"
name="admin[statistic_profile_attributes][gender]"
ng-model="admin.statistic_profile_attributes.gender"
ng-value="true"
required/>
<i class="fa fa-male m-l-sm"></i> {{ 'man' | translate }}
</label>
<label class="checkbox-inline btn btn-default">
<input type="radio"
name="admin[profile_attributes][gender]"
ng-model="admin.profile_attributes.gender"
name="admin[statistic_profile_attributes][gender]"
ng-model="admin.statistic_profile_attributes.gender"
ng-value="false"/>
<i class="fa fa-female m-l-sm"></i> {{ 'woman' | translate }}
</label>
@ -73,13 +73,13 @@
<span class="help-block" ng-show="adminForm['admin[email]'].$dirty && adminForm['admin[email]'].$error.required" translate>{{ 'email_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': adminForm['admin[profile_attributes][birthday]'].$dirty && adminForm['admin[profile_attributes][birthday]'].$invalid}">
<div class="form-group" ng-class="{'has-error': adminForm['admin[statistic_profile_attributes][birthday]'].$dirty && adminForm['admin[statistic_profile_attributes][birthday]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar-o"></i> </span>
<input type="text"
id="user_birthday"
class="form-control"
ng-model="admin.profile_attributes.birthday"
ng-model="admin.statistic_profile_attributes.birthday"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
@ -87,8 +87,8 @@
ng-click="openDatePicker($event)"
/>
<input type="hidden"
name="admin[profile_attributes][birthday]"
value="{{admin.profile_attributes.birthday | toIsoDate}}" />
name="admin[statistic_profile_attributes][birthday]"
value="{{admin.statistic_profile_attributes.birthday | toIsoDate}}" />
</div>
</div>

View File

@ -3,6 +3,7 @@
<input name="_method" type="hidden" ng-value="method">
<input name="user[profile_attributes][id]" type="hidden" ng-value="user.profile.id">
<input name="user[invoicing_profile_attributes][id]" type="hidden" ng-value="user.invoicing_profile.id">
<input name="user[statistic_profile_attributes][id]" type="hidden" ng-value="user.statistic_profile.id">
<div class="row m-t">
<div class="col-sm-3 col-sm-offset-1">
@ -38,27 +39,27 @@
</div>
<div class="col-sm-offset-1 col-sm-6">
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][gender]'].$dirty && userForm['user[profile_attributes][gender]'].$invalid}">
<div class="form-group" ng-class="{'has-error': userForm['user[statistic_profile_attributes][gender]'].$dirty && userForm['user[statistic_profile_attributes][gender]'].$invalid}">
<label class="checkbox-inline btn btn-default">
<input type="radio"
name="user[profile_attributes][gender]"
ng-model="user.profile.gender"
name="user[statistic_profile_attributes][gender]"
ng-model="user.statistic_profile.gender"
value="true"
ng-disabled="preventField['profile.gender'] && user.profile.gender && !userForm['user[profile_attributes][gender]'].$dirty"
ng-disabled="preventField['profile.gender'] && user.statistic_profile.gender && !userForm['user[statistic_profile_attributes][gender]'].$dirty"
required/>
<i class="fa fa-male m-l-sm"></i> {{ 'man' | translate }}
</label>
<label class="checkbox-inline btn btn-default">
<input type="radio"
name="user[profile_attributes][gender]"
ng-model="user.profile.gender"
name="user[statistic_profile_attributes][gender]"
ng-model="user.statistic_profile.gender"
value="false"
ng-disabled="preventField['profile.gender'] && user.profile.gender && !userForm['user[profile_attributes][gender]'].$dirty"/>
ng-disabled="preventField['profile.gender'] && user.statistic_profile.gender && !userForm['user[statistic_profile_attributes][gender]'].$dirty"/>
<i class="fa fa-female m-l-sm"></i> {{ 'woman' | translate }}
</label>
<span class="exponent m-l-xs"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="userForm['user[profile_attributes][gender]'].$dirty && userForm['user[profile_attributes][gender]'].$error.required" translate>{{ 'gender_is_required' }}</span>
<span class="help-block" ng-show="userForm['user[statistic_profile_attributes][gender]'].$dirty && userForm['user[statistic_profile_attributes][gender]'].$error.required" translate>{{ 'gender_is_required' }}</span>
</div>
@ -200,25 +201,25 @@
<span class="help-block" ng-show="userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$error.required" translate>{{ 'organization_address_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][birthday]'].$dirty && userForm['user[profile_attributes][birthday]'].$invalid}">
<div class="form-group" ng-class="{'has-error': userForm['user[statistic_profile_attributes][birthday]'].$dirty && userForm['user[statistic_profile_attributes][birthday]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar-o"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
id="user_birthday"
class="form-control"
ng-model="user.profile.birthday"
ng-model="user.statistic_profile.birthday"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
placeholder="{{ 'date_of_birth' | translate }}"
ng-click="openDatePicker($event)"
ng-disabled="preventField['profile.birthday'] && user.profile.birthday && !userForm['user[profile_attributes][birthday]'].$dirty"
ng-disabled="preventField['profile.birthday'] && user.statistic_profile.birthday && !userForm['user[statistic_profile_attributes][birthday]'].$dirty"
required/>
<input type="hidden"
name="user[profile_attributes][birthday]"
value="{{user.profile.birthday | toIsoDate}}" />
name="user[statistic_profile_attributes][birthday]"
value="{{user.statistic_profile.birthday | toIsoDate}}" />
</div>
<span class="help-block" ng-show="userForm['user[profile_attributes][birthday]'].$dirty && userForm['user[profile_attributes][birthday]'].$error.required" translate>{{ 'date_of_birth_is_required' }}</span>
<span class="help-block" ng-show="userForm['user[statistic_profile_attributes][birthday]'].$dirty && userForm['user[statistic_profile_attributes][birthday]'].$error.required" translate>{{ 'date_of_birth_is_required' }}</span>
</div>
<div class="form-group">

View File

@ -12,14 +12,14 @@
<label class="checkbox-inline">
<input type="radio"
name="gender"
ng-model="user.profile_attributes.gender"
ng-model="user.statistic_profile_attributes.gender"
value="true"
required/> {{ 'man' | translate }}
</label>
<label class="checkbox-inline">
<input type="radio"
name="gender"
ng-model="user.profile_attributes.gender"
ng-model="user.statistic_profile_attributes.gender"
value="false"/> {{ 'woman' | translate }}
</label>
<span class="exponent m-l-xs"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
@ -182,7 +182,7 @@
<input type="text"
class="form-control"
name="birthday"
ng-model="user.profile_attributes.birthday"
ng-model="user.statistic_profile_attributes.birthday"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"

View File

@ -11,24 +11,13 @@ class API::AdminsController < API::ApiController
def create
authorize :admin
generated_password = Devise.friendly_token.first(8)
@admin = User.new(admin_params.merge(password: generated_password))
@admin.send :set_slug
res = UserService.create_admin(admin_params)
# we associate the admin group to prevent linking any other 'normal' group (which won't be deletable afterwards)
@admin.group = Group.find_by(slug: 'admins')
# if the authentication is made through an SSO, generate a migration token
@admin.generate_auth_migration_token unless AuthProvider.active.providable_type == DatabaseProvider.name
if @admin.save(validate: false)
@admin.send_confirmation_instructions
@admin.add_role(:admin)
@admin.remove_role(:member)
UsersMailer.delay.notify_user_account_created(@admin, generated_password)
if res[:saved]
@admin = res[:user]
render :create, status: :created
else
render json: @admin.errors.full_messages, status: :unprocessable_entity
render json: res[:user].errors.full_messages, status: :unprocessable_entity
end
end
@ -47,8 +36,9 @@ class API::AdminsController < API::ApiController
def admin_params
params.require(:admin).permit(
:username, :email,
profile_attributes: %i[first_name last_name gender birthday phone],
invoicing_profile_attributes: [address_attributes: [:address]]
profile_attributes: %i[first_name last_name phone],
invoicing_profile_attributes: [address_attributes: [:address]],
statistic_profile_attributes: %i[gender birthday]
)
end
end

View File

@ -189,32 +189,31 @@ class API::MembersController < API::ApiController
def user_params
if current_user.id == params[:id].to_i
params.require(:user).permit(:username, :email, :password, :password_confirmation, :group_id, :is_allow_contact,
:is_allow_newsletter,
profile_attributes: [:id, :first_name, :last_name, :gender, :birthday, :phone, :interest,
:software_mastered, :website, :job, :facebook, :twitter,
:google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy]],
invoicing_profile_attributes: [
address_attributes: %i[id address],
organization_attributes: [:id, :name, address_attributes: %i[id address]]
])
elsif current_user.admin?
params.require(:user).permit(:username, :email, :password, :password_confirmation,
:is_allow_contact, :is_allow_newsletter, :group_id,
training_ids: [], tag_ids: [],
profile_attributes: [:id, :first_name, :last_name, :gender, :birthday, :phone, :interest,
:software_mastered, :website, :job, :facebook, :twitter,
:google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
params.require(:user).permit(:username, :email, :password, :password_confirmation, :group_id, :is_allow_contact, :is_allow_newsletter,
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy]],
invoicing_profile_attributes: [
:id,
address_attributes: %i[id address],
organization_attributes: [:id, :name, address_attributes: %i[id address]]
])
],
statistic_profile_attributes: %i[id gender birthday])
elsif current_user.admin?
params.require(:user).permit(:username, :email, :password, :password_confirmation, :is_allow_contact, :is_allow_newsletter, :group_id,
training_ids: [], tag_ids: [],
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy]],
invoicing_profile_attributes: [
:id,
address_attributes: %i[id address],
organization_attributes: [:id, :name, address_attributes: %i[id address]]
],
statistic_profile_attributes: %i[id gender birthday])
end
end

View File

@ -23,7 +23,7 @@ class API::ReservationsController < API::ApiController
def create
method = current_user.admin? ? :local : :stripe
user_id = current_user.admin? ? reservation_params[:user_id] : current_user.id
user_id = current_user.admin? ? params[:reservation][:user_id] : current_user.id
@reservation = Reservation.new(reservation_params)
is_reserve = Reservations::Reserve.new(user_id, current_user.id)
@ -56,8 +56,7 @@ class API::ReservationsController < API::ApiController
end
def reservation_params
params.require(:reservation).permit(:user_id, :message, :reservable_id, :reservable_type, :card_token, :plan_id,
:nb_reserve_places,
params.require(:reservation).permit(:message, :reservable_id, :reservable_type, :card_token, :plan_id, :nb_reserve_places,
tickets_attributes: %i[event_price_category_id booked],
slots_attributes: %i[id start_at end_at availability_id offered])
end

View File

@ -16,10 +16,10 @@ class API::SubscriptionsController < API::ApiController
head 403
else
method = current_user.admin? ? :local : :stripe
user_id = current_user.admin? ? subscription_params[:user_id] : current_user.id
user_id = current_user.admin? ? params[:subscription][:user_id] : current_user.id
@subscription = Subscription.new(subscription_params)
is_subscribe = Subscriptions::Subscribe.new(user_id, current_user.id)
is_subscribe = Subscriptions::Subscribe.new(current_user.id, user_id)
.pay_and_save(@subscription, method, coupon_params[:coupon_code], true)
if is_subscribe
@ -35,7 +35,7 @@ class API::SubscriptionsController < API::ApiController
free_days = params[:subscription][:free] || false
res = Subscriptions::Subscribe.new(@subscription.user_id, current_user.id)
res = Subscriptions::Subscribe.new(current_user.id)
.extend_subscription(@subscription, subscription_update_params[:expired_at], free_days)
if res.is_a?(Subscription)
@subscription = res
@ -56,7 +56,7 @@ class API::SubscriptionsController < API::ApiController
# Never trust parameters from the scary internet, only allow the white list through.
def subscription_params
params.require(:subscription).permit(:plan_id, :user_id, :card_token)
params.require(:subscription).permit(:plan_id, :card_token)
end
def coupon_params

View File

@ -14,24 +14,13 @@ class API::UsersController < API::ApiController
def create
if current_user.admin?
generated_password = Devise.friendly_token.first(8)
@user = User.new(email: partner_params[:email],
username: "#{partner_params[:first_name]}#{partner_params[:last_name]}",
password: generated_password,
password_confirmation: generated_password,
group_id: Group.first.id)
@user.build_profile(first_name: partner_params[:first_name],
last_name: partner_params[:last_name],
gender: true,
birthday: Time.now,
phone: '0000000000')
res = UserService.create_partner(partner_params)
if @user.save
@user.remove_role :member
@user.add_role :partner
if res[:saved]
@user = res[:user]
render status: :created
else
render json: @user.errors.full_messages, status: :unprocessable_entity
render json: res[:user].errors.full_messages, status: :unprocessable_entity
end
else
head 403

View File

@ -33,10 +33,11 @@ class ApplicationController < ActionController::Base
devise_parameter_sanitizer.permit(:sign_up,
keys: [
{
profile_attributes: %i[phone last_name first_name gender birthday interest software_mastered],
profile_attributes: %i[phone last_name first_name interest software_mastered],
invoicing_profile_attributes: [
organization_attributes: [:name, address_attributes: [:address]]
]
],
statistic_profile_attributes: %i[gender birthday]
},
:username, :is_allow_contact, :is_allow_newsletter, :cgu, :group_id
])

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
# Handling a new user registration through the sign-up modal
class RegistrationsController < Devise::RegistrationsController
# POST /resource
# POST /users.json
def create
build_resource(sign_up_params)
@ -24,5 +27,4 @@ class RegistrationsController < Devise::RegistrationsController
respond_with resource
end
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Devise controller for handling client sessions
class SessionsController < Devise::SessionsController
#before_action :set_csrf_headers, only: [:create, :destroy]
def new
active_provider = AuthProvider.active
@ -9,9 +11,4 @@ class SessionsController < Devise::SessionsController
super
end
end
protected
def set_csrf_headers
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end
end

View File

@ -0,0 +1,3 @@
# Raised when an expected profile (statistic, invoicing or normal) was not found on an user
class NoProfileError < StandardError
end

View File

@ -1,6 +1,7 @@
class Group < ActiveRecord::Base
has_many :plans
has_many :users
has_many :statistic_profiles
has_many :trainings_pricings, dependent: :destroy
has_many :machines_prices, -> { where(priceable_type: 'Machine') }, class_name: 'Price', dependent: :destroy
has_many :spaces_prices, -> { where(priceable_type: 'Space') }, class_name: 'Price', dependent: :destroy

View File

@ -1,3 +1,8 @@
# frozen_string_literal: true
# This table will save the user's profile data needed for legal accounting (invoices, wallet, etc.)
# Legal accounting must be kept for 10 years but GDPR requires that an user can delete his account at any time.
# The data will be kept even if the user is deleted, but it will be unlinked from the user's account.
class InvoicingProfile < ActiveRecord::Base
belongs_to :user
has_one :address, as: :placeable, dependent: :destroy
@ -11,17 +16,8 @@ class InvoicingProfile < ActiveRecord::Base
has_many :history_values, dependent: :nullify
after_create :create_a_wallet
def full_name
# if first_name or last_name is nil, the empty string will be used as a temporary replacement
(first_name || '').humanize.titleize + ' ' + (last_name || '').humanize.titleize
end
private
def create_a_wallet
create_wallet
end
end

View File

@ -10,11 +10,9 @@ class Profile < ActiveRecord::Base
validates :first_name, presence: true, length: { maximum: 30 }
validates :last_name, presence: true, length: { maximum: 30 }
validates :gender, inclusion: { in: [true, false] }
validates :birthday, presence: true
validates_numericality_of :phone, only_integer: true, allow_blank: false
after_save :update_invoicing_profile
after_commit :update_invoicing_profile, if: :invoicing_data_was_modified?, on: [:update]
def full_name
# if first_name or last_name is nil, the empty string will be used as a temporary replacement
@ -25,19 +23,6 @@ class Profile < ActiveRecord::Base
full_name
end
def age
if birthday.present?
now = Time.now.utc.to_date
(now - birthday).to_f / 365.2425
else
''
end
end
def str_gender
gender ? 'male' : 'female'
end
def self.mapping
# we protect some fields as they are designed to be managed by the system and must not be updated externally
blacklist = %w[id user_id created_at updated_at]
@ -51,19 +36,17 @@ class Profile < ActiveRecord::Base
private
def invoicing_data_was_modified?
first_name_changed? or last_name_changed? or new_record?
end
def update_invoicing_profile
if user.invoicing_profile.nil?
InvoicingProfile.create!(
user: user,
first_name: first_name,
last_name: last_name
)
else
user.invoicing_profile.update_attributes(
first_name: first_name,
last_name: last_name
)
end
raise NoProfileError if user.invoicing_profile.nil?
user.invoicing_profile.update_attributes(
first_name: first_name,
last_name: last_name
)
end
end

View File

@ -1,7 +1,7 @@
class Reservation < ActiveRecord::Base
include NotifyWith::NotificationAttachedObject
belongs_to :user
belongs_to :statistic_profile
has_many :slots_reservations, dependent: :destroy
has_many :slots, through: :slots_reservations
@ -240,8 +240,8 @@ class Reservation < ActiveRecord::Base
# TODO: refactoring
customer = Stripe::Customer.retrieve(user.stp_customer_id)
if plan_id
self.subscription = Subscription.find_or_initialize_by(user_id: user.id)
subscription.attributes = { plan_id: plan_id, user_id: user.id, card_token: card_token, expiration_date: nil }
self.subscription = Subscription.find_or_initialize_by(statistic_profile_id: statistic_profile_id)
subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, card_token: card_token, expiration_date: nil }
if subscription.save_with_payment(operator_id, false)
self.stp_invoice_id = invoice_items.first.refresh.invoice
invoice.stp_invoice_id = invoice_items.first.refresh.invoice
@ -338,8 +338,8 @@ class Reservation < ActiveRecord::Base
end
# check reservation amount total and strip invoice total to pay is equal
# @params stp_invoice[Stripe::Invoice]
# @params coupon_code[String]
# @param stp_invoice[Stripe::Invoice]
# @param coupon_code[String]
# return Boolean
def is_equal_reservation_total_and_stp_invoice_total(stp_invoice, coupon_code = nil)
compute_amount_total_to_pay(coupon_code) == stp_invoice.total
@ -375,8 +375,8 @@ class Reservation < ActiveRecord::Base
return false unless valid?
if plan_id
self.subscription = Subscription.find_or_initialize_by(user_id: user.id)
subscription.attributes = { plan_id: plan_id, user_id: user.id, expiration_date: nil }
self.subscription = Subscription.find_or_initialize_by(statistic_profile_id: statistic_profile_id)
subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, expiration_date: nil }
if subscription.save_with_local_payment(operator_id, false)
invoice.invoice_items.push InvoiceItem.new(
amount: subscription.plan.amount,
@ -405,6 +405,10 @@ class Reservation < ActiveRecord::Base
total
end
def user
statistic_profile.user
end
private
def machine_not_already_reserved

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
# This table will save the user's profile data needed for statistical purposes.
# GDPR requires that an user can delete his account at any time but we need to keep the statistics original data to being able to
# rebuild them at any time.
# The data will be kept even if the user is deleted, but it will be unlinked from the user's account (ie. anonymized)
class StatisticProfile < ActiveRecord::Base
belongs_to :user
belongs_to :group
# relations to reservations, trainings, subscriptions
has_many :subscriptions, dependent: :destroy
accepts_nested_attributes_for :subscriptions, allow_destroy: false
has_many :reservations, dependent: :destroy
accepts_nested_attributes_for :reservations, allow_destroy: false
def str_gender
gender ? 'male' : 'female'
end
def age
if birthday.present?
now = Time.now.utc.to_date
(now - birthday).to_f / 365.2425
else
''
end
end
end

View File

@ -2,7 +2,7 @@ class Subscription < ActiveRecord::Base
include NotifyWith::NotificationAttachedObject
belongs_to :plan
belongs_to :user
belongs_to :statistic_profile
has_many :invoices, as: :invoiced, dependent: :destroy
has_many :offer_days, dependent: :destroy
@ -223,6 +223,10 @@ class Subscription < ActiveRecord::Base
false
end
def user
statistic_profile.user
end
private
def notify_member_subscribed_plan

View File

@ -24,22 +24,19 @@ class User < ActiveRecord::Base
has_one :invoicing_profile, dependent: :nullify
accepts_nested_attributes_for :invoicing_profile
has_one :statistic_profile, dependent: :nullify
accepts_nested_attributes_for :statistic_profile
has_many :my_projects, foreign_key: :author_id, class_name: 'Project', dependent: :destroy
has_many :project_users, dependent: :destroy
has_many :projects, through: :project_users
has_many :reservations, dependent: :destroy
accepts_nested_attributes_for :reservations, allow_destroy: true
# Trainings that were already passed
has_many :user_trainings, dependent: :destroy
has_many :trainings, through: :user_trainings
belongs_to :group
has_many :subscriptions, dependent: :destroy
accepts_nested_attributes_for :subscriptions, allow_destroy: true
has_many :users_credits, dependent: :destroy
has_many :credits, through: :users_credits
@ -62,20 +59,27 @@ class User < ActiveRecord::Base
before_create :assign_default_role
after_commit :create_stripe_customer, on: [:create]
after_commit :notify_admin_when_user_is_created, on: :create
after_create :init_dependencies
after_update :notify_group_changed, if: :group_id_changed?
after_save :update_invoicing_profile
after_update :update_invoicing_profile, if: :invoicing_data_was_modified?
after_update :update_statistic_profile, if: :statistic_data_was_modified?
attr_accessor :cgu
delegate :first_name, to: :profile
delegate :last_name, to: :profile
delegate :subscriptions, to: :statistic_profile
delegate :reservations, to: :statistic_profile
delegate :wallet, to: :invoicing_profile
delegate :wallet_transactions, to: :invoicing_profile
delegate :invoices, to: :invoicing_profile
validate :cgu_must_accept, if: :new_record?
validates :username, presence: true, uniqueness: true, length: { maximum: 30 }
scope :active, -> { where(is_active: true) }
scope :without_subscription, -> { includes(:subscriptions).where(subscriptions: { user_id: nil }) }
scope :with_subscription, -> { joins(:subscriptions) }
scope :without_subscription, -> { includes(statistic_profile: [:subscriptions]).where(subscriptions: { statistic_profile_id: nil }) }
scope :with_subscription, -> { joins(statistic_profile: [:subscriptions]) }
def to_json(*)
ApplicationController.new.view_context.render(
@ -128,18 +132,6 @@ class User < ActiveRecord::Base
my_projects.to_a.concat projects
end
def invoices
invoicing_profile.invoices
end
def wallet
invoicing_profile.wallet
end
def wallet_transactions
invoicing_profile.wallet_transactions
end
def generate_subscription_invoice(operator_id)
return unless subscription
@ -166,9 +158,7 @@ class User < ActiveRecord::Base
def self.from_omniauth(auth)
active_provider = AuthProvider.active
if active_provider.strategy_name != auth.provider
raise SecurityError, 'The identity provider does not match the activated one'
end
raise SecurityError, 'The identity provider does not match the activated one' if active_provider.strategy_name != auth.provider
where(provider: auth.provider, uid: auth.uid).first_or_create.tap do |user|
# execute this regardless of whether record exists or not (-> User#tap)
@ -182,8 +172,8 @@ class User < ActiveRecord::Base
end
def need_completion?
profile.gender.nil? || profile.first_name.blank? || profile.last_name.blank? || username.blank? ||
email.blank? || encrypted_password.blank? || group_id.nil? || profile.birthday.blank? || profile.phone.blank?
statistic_profile.gender.nil? || profile.first_name.blank? || profile.last_name.blank? || username.blank? ||
email.blank? || encrypted_password.blank? || group_id.nil? || statistic_profile.birthday.blank? || profile.phone.blank?
end
## Retrieve the requested data in the User and user's Profile tables
@ -244,9 +234,7 @@ class User < ActiveRecord::Base
## and remove the auth_token to mark his account as "migrated"
def link_with_omniauth_provider(auth)
active_provider = AuthProvider.active
if active_provider.strategy_name != auth.provider
raise SecurityError, 'The identity provider does not match the activated one'
end
raise SecurityError, 'The identity provider does not match the activated one' if active_provider.strategy_name != auth.provider
if User.where(provider: auth.provider, uid: auth.uid).size.positive?
raise DuplicateIndexError, "This #{active_provider.name} account is already linked to an existing user"
@ -362,16 +350,52 @@ class User < ActiveRecord::Base
attached_object: self
end
def update_invoicing_profile
def invoicing_data_was_modified?
email_changed?
end
def statistic_data_was_modified?
group_id_changed?
end
def init_dependencies
if invoicing_profile.nil?
InvoicingProfile.create!(
user: user,
email: email
)
else
invoicing_profile.update_attributes(
email: email
ip = InvoicingProfile.create!(
user: self,
email: email,
first_name: first_name,
last_name: last_name
)
end
if wallet.nil?
ip ||= invoicing_profile
Wallet.create!(
invoicing_profile: ip
)
end
if statistic_profile.nil?
StatisticProfile.create!(
user: self,
group_id: group_id
)
else
update_statistic_profile
end
end
def update_invoicing_profile
raise NoProfileError if invoicing_profile.nil?
invoicing_profile.update_attributes(
email: email
)
end
def update_statistic_profile
raise NoProfileError if statistic_profile.nil?
statistic_profile.update_attributes(
group_id: group_id
)
end
end

View File

@ -2,7 +2,7 @@ class UserPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin?
scope.includes(:group, :training_credits, :machine_credits, subscriptions: [plan: [:credits]], profile: [:user_avatar])
scope.includes(:group, :training_credits, :machine_credits, statistic_profile: [subscriptions: [plan: [:credits]]], profile: [:user_avatar])
.joins(:roles).where("users.is_active = 'true' AND roles.name = 'member'").order('users.created_at desc')
else
scope.includes(profile: [:user_avatar]).joins(:roles).where("users.is_active = 'true' AND roles.name = 'member'")

View File

@ -93,7 +93,7 @@ class Availabilities::AvailabilitiesService
def reservations(reservable)
Reservation.where('reservable_type = ? and reservable_id = ?', reservable.class.name, reservable.id)
.includes(:slots, user: [:profile])
.includes(:slots, statistic_profile: [user: [:profile]])
.references(:slots, :user)
.where('slots.start_at > ?', Time.now)
end

View File

@ -4,16 +4,25 @@
class Members::ListService
class << self
def list(params)
@query = User.includes(:profile, :group, :subscriptions)
@query = User.includes(:profile, :group, :statistic_profile)
.joins(:profile,
:statistic_profile,
:group,
:roles,
'LEFT JOIN "subscriptions" ON "subscriptions"."user_id" = "users"."id" ' \
'LEFT JOIN (
SELECT *
FROM "subscriptions" AS s1
INNER JOIN (
SELECT MAX("created_at") AS "s2_created_at", "statistic_profile_id" AS "s2_statistic_profile_id"
FROM "subscriptions"
GROUP BY "statistic_profile_id"
) As s2
ON "s1"."statistic_profile_id" = "s2"."s2_statistic_profile_id"
WHERE "s1"."expiration_date" > now()::date
) AS "subscriptions" ON "subscriptions"."statistic_profile_id" = "statistic_profiles"."id" ' \
'LEFT JOIN "plans" ON "plans"."id" = "subscriptions"."plan_id"')
.where("users.is_active = 'true' AND roles.name = 'member'")
.order(list_order(params))
.page(params[:page])
.per(params[:size])
# ILIKE => PostgreSQL case-insensitive LIKE
if params[:search].size.positive?
@ -31,12 +40,13 @@ class Members::ListService
def search(current_user, query, subscription)
members = User.includes(:profile)
.joins(:profile,
:statistic_profile,
:roles,
'LEFT JOIN "subscriptions" ON "subscriptions"."user_id" = "users"."id" AND ' \
'LEFT JOIN "subscriptions" ON "subscriptions"."statistic_profile_id" = "statistic_profiles"."id" AND ' \
'"subscriptions"."created_at" = ( ' \
'SELECT max("created_at") ' \
'FROM "subscriptions" ' \
'WHERE "user_id" = "users"."id")')
'WHERE "statistic_profile_id" = "statistic_profiles"."id")')
.where("users.is_active = 'true' AND roles.name = 'member'")
.limit(50)
query.downcase.split(' ').each do |word|
@ -64,6 +74,8 @@ class Members::ListService
def list_order(params)
direction = (params[:order_by][0] == '-' ? 'DESC' : 'ASC')
order_key = (params[:order_by][0] == '-' ? params[:order_by][1, params[:order_by].size] : params[:order_by])
limit = params[:size]
offset = (params[:page]&.to_i || 1) - 1
order_key = case order_key
when 'last_name'
@ -82,7 +94,7 @@ class Members::ListService
'users.id'
end
"#{order_key} #{direction}"
"#{order_key} #{direction} LIMIT #{limit} OFFSET #{offset}"
end
end
end

View File

@ -10,7 +10,7 @@ class Reservations::Reserve
end
def pay_and_save(reservation, payment_method, coupon)
reservation.user_id = user_id
reservation.statistic_profile_id = User.find(user_id).statistic_profile.id
if payment_method == :local
reservation.save_with_local_payment(operator_id, coupon)
elsif payment_method == :stripe

View File

@ -356,8 +356,8 @@ class StatisticService
def user_info(user)
{
user_id: user.id,
gender: user.profile.str_gender,
age: user.profile.age,
gender: user.statistic_profile.str_gender,
age: user.statistic_profile.age,
group: user.group ? user.group.slug : nil,
email: user.email
}

View File

@ -4,13 +4,15 @@
class Subscriptions::Subscribe
attr_accessor :user_id, :operator_id
def initialize(user_id, operator_id)
def initialize(operator_id, user_id = nil)
@user_id = user_id
@operator_id = operator_id
end
def pay_and_save(subscription, payment_method, coupon, invoice)
subscription.user_id = user_id
return false if user_id.nil?
subscription.statistic_profile_id = User.find(user_id).statistic_profile.id
if payment_method == :local
subscription.save_with_local_payment(operator_id, invoice, coupon)
elsif payment_method == :stripe
@ -23,7 +25,7 @@ class Subscriptions::Subscribe
new_sub = Subscription.create(
plan_id: subscription.plan_id,
user_id: subscription.user_id,
statistic_profile_id: subscription.statistic_profile_id,
expiration_date: new_expiration_date
)
if new_sub.save
@ -32,4 +34,4 @@ class Subscriptions::Subscribe
end
false
end
end
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
# helpers for managing users with special roles
class UserService
def self.create_partner(params)
generated_password = Devise.friendly_token.first(8)
group_id = Group.first.id
user = User.new(
email: params[:email],
username: "#{params[:first_name]}#{params[:last_name]}".parameterize,
password: generated_password,
password_confirmation: generated_password,
group_id: group_id
)
user.build_profile(
first_name: params[:first_name],
last_name: params[:last_name],
phone: '0000000000'
)
user.build_statistic_profile(
gender: true,
birthday: Time.now
)
saved = user.save
if saved
user.remove_role :member
user.add_role :partner
end
{ saved: saved, user: user }
end
def self.create_admin(params)
generated_password = Devise.friendly_token.first(8)
admin = User.new(params.merge(password: generated_password))
admin.send :set_slug
# we associate the admin group to prevent linking any other 'normal' group (which won't be deletable afterwards)
admin.group = Group.find_by(slug: 'admins')
# if the authentication is made through an SSO, generate a migration token
admin.generate_auth_migration_token unless AuthProvider.active.providable_type == DatabaseProvider.name
saved = admin.save(validate: false)
if saved
admin.send_confirmation_instructions
admin.add_role(:admin)
admin.remove_role(:member)
UsersMailer.delay.notify_user_account_created(admin, generated_password)
end
{ saved: saved, user: admin }
end
end

View File

@ -1,7 +1,7 @@
class SubscriptionGroupValidator < ActiveModel::Validator
def validate(record)
if record.user.group != record.plan.group
record.errors[:plan_id] << "This plan is not compatible with the current user's group"
end
return if record.statistic_profile.group_id == record.plan.group_id
record.errors[:plan_id] << "This plan is not compatible with the current user's group"
end
end
end

View File

@ -3,8 +3,8 @@ json.profile_attributes do
json.id admin.profile.id
json.first_name admin.profile.first_name
json.last_name admin.profile.last_name
json.gender admin.profile.gender
json.birthday admin.profile.birthday if admin.profile.birthday
json.gender admin.statistic_profile.gender
json.birthday admin.statistic_profile.birthday if admin.statistic_profile.birthday
json.phone admin.profile.phone
if admin.profile.user_avatar
json.user_avatar do

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
json.extract! member, :id, :username, :email, :group_id
json.role member.roles.first.name
json.name member.profile.full_name
@ -13,8 +15,6 @@ json.profile do
end
json.first_name member.profile.first_name
json.last_name member.profile.last_name
json.gender member.profile.gender.to_s
json.birthday member.profile.birthday.to_date.iso8601 if member.profile.birthday
json.interest member.profile.interest
json.software_mastered member.profile.software_mastered
json.phone member.profile.phone
@ -46,6 +46,12 @@ json.invoicing_profile do
end
end
json.statistic_profile do
json.id member.statistic_profile.id
json.gender member.statistic_profile.gender.to_s
json.birthday member.statistic_profile&.birthday&.to_date&.iso8601
end
if member.subscribed_plan
json.subscribed_plan do
json.partial! 'api/shared/plan', plan: member.subscribed_plan

View File

@ -1,4 +1,6 @@
user_is_admin = (current_user and current_user.admin?)
# frozen_string_literal: true
user_is_admin = current_user&.admin?
max_members = @query.except(:offset, :limit, :order).count
json.array!(@members) do |member|
@ -10,55 +12,77 @@ json.array!(@members) do |member|
json.email member.email if current_user
json.first_name member.profile.first_name
json.last_name member.profile.last_name
json.profile do
json.user_avatar do
json.id member.profile.user_avatar.id
json.attachment_url member.profile.user_avatar.attachment_url
end if member.profile.user_avatar
json.first_name member.profile.first_name
json.last_name member.profile.last_name
json.gender member.profile.gender.to_s
if user_is_admin
json.phone member.profile.phone
json.birthday member.profile.birthday.iso8601 if member.profile.birthday
end
end if attribute_requested?(@requested_attributes, 'profile')
json.need_completion member.need_completion?
json.group_id member.group_id
json.group do
json.id member.group.id
json.name member.group.name
end if attribute_requested?(@requested_attributes, 'group') and member.group
if user_is_admin
json.subscribed_plan do
json.partial! 'api/shared/plan', plan: member.subscribed_plan
end if member.subscribed_plan
json.subscription do
json.id member.subscription.id
json.expired_at member.subscription.expired_at.iso8601
json.canceled_at member.subscription.canceled_at.iso8601 if member.subscription.canceled_at
json.stripe member.subscription.stp_subscription_id.present?
json.plan do
json.id member.subscription.plan.id
json.name member.subscription.plan.name
json.interval member.subscription.plan.interval
json.amount member.subscription.plan.amount ? (member.subscription.plan.amount / 100.0) : 0
if attribute_requested?(@requested_attributes, 'profile')
json.profile do
if member.profile.user_avatar
json.user_avatar do
json.id member.profile.user_avatar.id
json.attachment_url member.profile.user_avatar.attachment_url
end
end
end if member.subscription
end if attribute_requested?(@requested_attributes, 'subscription')
json.first_name member.profile.first_name
json.last_name member.profile.last_name
json.phone member.profile.phone
end
if user_is_admin
json.statistic_profile do
json.gender member.statistic_profile.gender.to_s
json.birthday member.statistic_profile&.birthday&.iso8601
end
end
end
json.training_credits member.training_credits do |tc|
json.training_id tc.creditable_id
end if attribute_requested?(@requested_attributes, 'credits') or attribute_requested?(@requested_attributes, 'training_credits')
if attribute_requested?(@requested_attributes, 'group') && member.group
json.group do
json.id member.group.id
json.name member.group.name
end
end
json.machine_credits member.machine_credits do |mc|
json.machine_id mc.creditable_id
json.hours_used mc.users_credits.find_by(user_id: member.id).hours_used
end if attribute_requested?(@requested_attributes, 'credits') or attribute_requested?(@requested_attributes, 'machine_credits')
if attribute_requested?(@requested_attributes, 'subscription')
if user_is_admin
if member.subscribed_plan
json.subscribed_plan do
json.partial! 'api/shared/plan', plan: member.subscribed_plan
end
end
if member.subscription
json.subscription do
json.id member.subscription.id
json.expired_at member.subscription.expired_at.iso8601
json.canceled_at member.subscription&.canceled_at&.iso8601
json.stripe member.subscription.stp_subscription_id.present?
json.plan do
json.id member.subscription.plan.id
json.name member.subscription.plan.name
json.interval member.subscription.plan.interval
json.amount member.subscription.plan.amount ? (member.subscription.plan.amount / 100.0) : 0
end
end
end
end
end
json.tags member.tags do |t|
json.id t.id
json.name t.name
end if attribute_requested?(@requested_attributes, 'tags')
if attribute_requested?(@requested_attributes, 'credits') || attribute_requested?(@requested_attributes, 'training_credits')
json.training_credits member.training_credits do |tc|
json.training_id tc.creditable_id
end
end
if attribute_requested?(@requested_attributes, 'credits') || attribute_requested?(@requested_attributes, 'machine_credits')
json.machine_credits member.machine_credits do |mc|
json.machine_id mc.creditable_id
json.hours_used mc.users_credits.find_by(user_id: member.id).hours_used
end
end
if attribute_requested?(@requested_attributes, 'tags')
json.tags member.tags do |t|
json.id t.id
json.name t.name
end
end
end

View File

@ -39,13 +39,15 @@ json.all_projects @member.all_projects do |project|
json.first_name pu.user.profile.first_name
json.last_name pu.user.profile.last_name
json.full_name pu.user.profile.full_name
json.user_avatar do
json.id pu.user.profile.user_avatar.id
json.attachment_url pu.user.profile.user_avatar.attachment_url
end if pu.user.profile.user_avatar
if pu.user.profile.user_avatar
json.user_avatar do
json.id pu.user.profile.user_avatar.id
json.attachment_url pu.user.profile.user_avatar.attachment_url
end
end
json.username pu.user.username
json.is_valid pu.is_valid
json.valid_token pu.valid_token if !pu.is_valid and @member == pu.user
json.valid_token pu.valid_token if !pu.is_valid && @member == pu.user
end
end
end

View File

@ -2,6 +2,6 @@ json.title notification.notification_type
json.description _t('.user_NAME_changed_his_group_html',
{
NAME: notification.attached_object.profile.full_name,
GENDER: bool_to_sym(notification.attached_object.profile.gender)
GENDER: bool_to_sym(notification.attached_object.statistic_profile.gender)
}) # messageFormat
json.url notification_url(notification, format: :json)

View File

@ -2,7 +2,7 @@ json.title notification.notification_type
json.description _t('.user_NAME_has_merged_his_account_with_the_one_imported_from_PROVIDER_UID_html',
{
NAME: notification.attached_object.profile.full_name,
GENDER: bool_to_sym(notification.attached_object.profile.gender),
GENDER: bool_to_sym(notification.attached_object.statistic_profile.gender),
PROVIDER: notification.attached_object.provider,
UID: notification.attached_object.uid
}) # messageFormat

View File

@ -1,5 +1,5 @@
json.id reservation.id
json.user_id reservation.user_id
json.user_id reservation.statistic_profile.user_id
json.user_full_name reservation.user.profile.full_name
json.message reservation.message
json.slots reservation.slots do |s|

View File

@ -1,16 +1,18 @@
json.id @reservation.id
json.user_id @reservation.user_id
json.user_id @reservation.statistic_profile.user_id
json.user do
json.id @reservation.user.id
json.subscribed_plan do
json.partial! 'api/shared/plan', plan: @reservation.user.subscribed_plan
end if @reservation.user.subscribed_plan
if @reservation.user.subscribed_plan
json.subscribed_plan do
json.partial! 'api/shared/plan', plan: @reservation.user.subscribed_plan
end
end
json.training_credits @reservation.user.training_credits do |tc|
json.training_id tc.creditable_id
end
json.machine_credits @reservation.user.machine_credits do |mc|
json.machine_id mc.creditable_id
json.hours_used mc.users_credits.find_by(user_id: @reservation.user_id).hours_used
json.hours_used mc.users_credits.find_by(user_id: @reservation.statistic_profile.user_id).hours_used
end
end
json.message @reservation.message

View File

@ -1,2 +1,4 @@
json.extract! @user, :id, :email, :first_name, :last_name
json.name "#{@user.first_name} #{@user.last_name}"
# frozen_string_literal: true
json.extract! @user, :id, :email, :first_name, :last_name
json.name @user.profile.full_name

View File

@ -1,4 +1,4 @@
json.users @users do |user|
json.extract! user, :id, :email, :first_name, :last_name
json.name "#{user.first_name} #{user.last_name}"
json.name user.profile.full_name
end

View File

@ -14,10 +14,14 @@ json.invoices do
end
end
json.user do
json.extract! invoice[:invoice].user, :id, :email, :created_at
json.profile do
json.extract! invoice[:invoice].user.profile, :id, :first_name, :last_name, :birthday, :phone
json.gender invoice[:invoice].user.profile.gender ? 'male' : 'female'
json.extract! invoice[:invoice].invoicing_profile, :user_id, :email, :first_name, :last_name
json.address invoice[:invoice].invoicing_profile&.address&.address
json.invoicing_profile_id invoice[:invoice].invoicing_profile.id
if invoice[:invoice].invoicing_profile.organization
json.organization do
json.extract! invoice[:invoice].invoicing_profile.organization, :name, :id
json.address invoice[:invoice].invoicing_profile.organization&.address&.address
end
end
end
json.invoice_items invoice[:invoice].invoice_items do |item|

View File

@ -42,7 +42,7 @@ wb.add_worksheet(name: t('export_members.members')) do |sheet|
member.profile.first_name,
member.email,
member.is_allow_newsletter,
member.profile.gender ? t('export_members.man') : t('export_members.woman'),
member.statistic_profile.gender ? t('export_members.man') : t('export_members.woman'),
member.profile.age,
member.invoicing_profile.address ? member.invoicing_profile.address.address : '',
member.profile.phone,

View File

@ -86,6 +86,7 @@ en:
i_ve_read_and_i_accept_: "I've read and I accept"
_the_fablab_policy: "the FabLab policy"
field_required: "Field required"
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
# password modification modal
change_your_password: "Change your password"

View File

@ -84,7 +84,8 @@ es:
i_accept_to_receive_information_from_the_fablab: "Acepto recibir información del FabLab"
i_ve_read_and_i_accept_: "He leido y acepto"
_the_fablab_policy: "la política de FabLab"
field_required: "Field required" #translation_missing
field_required: "Field required" # translation missing
unexpected_error_occurred: "An unexpected error occurred. Please try again later." # translation missing
# password modification modal
change_your_password: "Cambiar contraseña"

View File

@ -86,6 +86,7 @@ fr:
i_ve_read_and_i_accept_: "J'ai lu et j'accepte"
_the_fablab_policy: "la charte d'utilisation du Fab Lab"
field_required: "Champ requis"
unexpected_error_occurred: "Une erreur inattendue s'est produite. Veuillez réessayer ultérieurement."
# fenêtre de changement de mot de passe
change_your_password: "Modifier votre mot de passe"

View File

@ -85,7 +85,8 @@ pt:
i_accept_to_receive_information_from_the_fablab: "Eu aceito receber informações do FabLab"
i_ve_read_and_i_accept_: "Eu li e aceito"
_the_fablab_policy: "a política do FabLab"
field_required: "Field required" #translation_missing
field_required: "Field required" # translation missing
unexpected_error_occurred: "An unexpected error occurred. Please try again later." # translation missing
# password modification modal
change_your_password: "Mudar sua senha"

View File

@ -7,12 +7,14 @@ Rails.application.routes.draw do
if AuthProvider.active.providable_type == DatabaseProvider.name
# with local authentification we do not use omniAuth so we must differentiate the config
devise_for :users, controllers: {registrations: 'registrations', sessions: 'sessions',
confirmations: 'confirmations', passwords: 'passwords'}
devise_for :users, controllers: {
registrations: 'registrations', sessions: 'sessions', confirmations: 'confirmations', passwords: 'passwords'
}
else
devise_for :users, controllers: {registrations: 'registrations', sessions: 'sessions',
confirmations: 'confirmations', passwords: 'passwords',
:omniauth_callbacks => 'users/omniauth_callbacks'}
devise_for :users, controllers: {
registrations: 'registrations', sessions: 'sessions', confirmations: 'confirmations', passwords: 'passwords',
omniauth_callbacks: 'users/omniauth_callbacks'
}
end

View File

@ -11,5 +11,8 @@ class CreateInvoicingProfiles < ActiveRecord::Migration
add_reference :organizations, :invoicing_profile, index: true, foreign_key: true
add_reference :invoices, :invoicing_profile, index: true, foreign_key: true
add_reference :wallets, :invoicing_profile, index: true, foreign_key: true
add_reference :wallet_transactions, :invoicing_profile, index: true, foreign_key: true
add_reference :history_values, :invoicing_profile, index: true, foreign_key: true
end
end

View File

@ -0,0 +1,9 @@
class RemoveUserIdColumns < ActiveRecord::Migration
def change
remove_column :invoices, :user_id, :integer
remove_reference :organizations, :profile, index: true, foreign_key: true
remove_reference :wallets, :user, index: true, foreign_key: true
remove_reference :wallet_transactions, :user, index: true, foreign_key: true
remove_reference :history_values, :user, index: true, foreign_key: true
end
end

View File

@ -1,5 +0,0 @@
class RemoveUserIdFromInvoice < ActiveRecord::Migration
def change
remove_column :invoices, :user_id, :integer
end
end

View File

@ -1,5 +0,0 @@
class RemoveProfileFromOrganization < ActiveRecord::Migration
def change
remove_reference :organizations, :profile, index: true, foreign_key: true
end
end

View File

@ -1,6 +0,0 @@
class AddInvoicingProfileToWallet < ActiveRecord::Migration
def change
add_reference :wallets, :invoicing_profile, index: true, foreign_key: true
add_reference :wallet_transactions, :invoicing_profile, index: true, foreign_key: true
end
end

View File

@ -1,6 +0,0 @@
class RemoveUserIdFromWallet < ActiveRecord::Migration
def change
remove_reference :wallets, :user, index: true, foreign_key: true
remove_reference :wallet_transactions, :user, index: true, foreign_key: true
end
end

View File

@ -1,5 +0,0 @@
class AddInvoicingProfileToHistoryValue < ActiveRecord::Migration
def change
add_reference :history_values, :invoicing_profile, index: true, foreign_key: true
end
end

View File

@ -1,5 +0,0 @@
class RemoveUserIdFromHistoryValue < ActiveRecord::Migration
def change
remove_reference :history_values, :user, index: true, foreign_key: true
end
end

View File

@ -0,0 +1,13 @@
class CreateStatisticProfile < ActiveRecord::Migration
def change
create_table :statistic_profiles do |t|
t.boolean :gender
t.date :birthday
t.belongs_to :group, index: true, foreign_key: true
t.belongs_to :user, index: true, foreign_key: true
end
add_reference :reservations, :statistic_profile, index: true, foreign_key: true
add_reference :subscriptions, :statistic_profile, index: true, foreign_key: true
end
end

View File

@ -0,0 +1,27 @@
class MigrateProfileToStatisticProfile < ActiveRecord::Migration
def up
User.all.each do |u|
p = u.profile
puts "WARNING: User #{u.id} has no profile" and next unless p
StatisticProfile.create!(
user: u,
group: u.group,
gender: p.gender,
birthday: p.birthday
)
end
end
def down
StatisticProfile.all.each do |sp|
p = sp.user.profile
puts "WARNING: User #{sp.user_id} has no profile" and next unless p
p.update_attributes(
gender: sp.gender,
birthday: sp.birthday
)
end
end
end

View File

@ -0,0 +1,19 @@
class MigrateReservationToStatisticProfile < ActiveRecord::Migration
def up
Reservation.all.each do |r|
user = User.find(r.user_id)
r.update_column(
'statistic_profile_id', user.statistic_profile.id
)
end
end
def down
Reservation.all.each do |r|
statistic_profile = User.find(r.statistic_profile_id)
r.update_column(
'user_id', statistic_profile.user_id
)
end
end
end

View File

@ -0,0 +1,19 @@
class MigrateSubscriptionToStatisticProfile < ActiveRecord::Migration
def up
Subscription.all.each do |s|
user = User.find(s.user_id)
s.update_column(
'statistic_profile_id', user.statistic_profile.id
)
end
end
def down
Subscription.all.each do |s|
statistic_profile = User.find(s.statistic_profile_id)
s.update_column(
'user_id', statistic_profile.user_id
)
end
end
end

View File

@ -0,0 +1,8 @@
class RemoveStatisticColumns < ActiveRecord::Migration
def change
remove_column :profiles, :gender, :boolean
remove_column :profiles, :birthday, :date
remove_column :reservations, :user_id
remove_column :subscriptions, :user_id
end
end

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20190603141109) do
ActiveRecord::Schema.define(version: 20190604075717) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -447,8 +447,6 @@ ActiveRecord::Schema.define(version: 20190603141109) do
t.integer "user_id"
t.string "first_name", limit: 255
t.string "last_name", limit: 255
t.boolean "gender"
t.date "birthday"
t.string "phone", limit: 255
t.text "interest"
t.text "software_mastered"
@ -545,19 +543,19 @@ ActiveRecord::Schema.define(version: 20190603141109) do
add_index "projects_themes", ["theme_id"], name: "index_projects_themes_on_theme_id", using: :btree
create_table "reservations", force: :cascade do |t|
t.integer "user_id"
t.text "message"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "reservable_id"
t.string "reservable_type", limit: 255
t.string "stp_invoice_id", limit: 255
t.string "reservable_type", limit: 255
t.string "stp_invoice_id", limit: 255
t.integer "nb_reserve_places"
t.integer "statistic_profile_id"
end
add_index "reservations", ["reservable_id", "reservable_type"], name: "index_reservations_on_reservable_id_and_reservable_type", using: :btree
add_index "reservations", ["statistic_profile_id"], name: "index_reservations_on_statistic_profile_id", using: :btree
add_index "reservations", ["stp_invoice_id"], name: "index_reservations_on_stp_invoice_id", using: :btree
add_index "reservations", ["user_id"], name: "index_reservations_on_user_id", using: :btree
create_table "roles", force: :cascade do |t|
t.string "name", limit: 255
@ -664,6 +662,16 @@ ActiveRecord::Schema.define(version: 20190603141109) do
t.boolean "ca", default: true
end
create_table "statistic_profiles", force: :cascade do |t|
t.boolean "gender"
t.date "birthday"
t.integer "group_id"
t.integer "user_id"
end
add_index "statistic_profiles", ["group_id"], name: "index_statistic_profiles_on_group_id", using: :btree
add_index "statistic_profiles", ["user_id"], name: "index_statistic_profiles_on_user_id", using: :btree
create_table "statistic_sub_types", force: :cascade do |t|
t.string "key", limit: 255
t.string "label", limit: 255
@ -701,16 +709,16 @@ ActiveRecord::Schema.define(version: 20190603141109) do
create_table "subscriptions", force: :cascade do |t|
t.integer "plan_id"
t.integer "user_id"
t.string "stp_subscription_id", limit: 255
t.string "stp_subscription_id", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
t.datetime "expiration_date"
t.datetime "canceled_at"
t.integer "statistic_profile_id"
end
add_index "subscriptions", ["plan_id"], name: "index_subscriptions_on_plan_id", using: :btree
add_index "subscriptions", ["user_id"], name: "index_subscriptions_on_user_id", using: :btree
add_index "subscriptions", ["statistic_profile_id"], name: "index_subscriptions_on_statistic_profile_id", using: :btree
create_table "tags", force: :cascade do |t|
t.string "name"
@ -906,11 +914,15 @@ ActiveRecord::Schema.define(version: 20190603141109) do
add_foreign_key "prices", "plans"
add_foreign_key "projects_spaces", "projects"
add_foreign_key "projects_spaces", "spaces"
add_foreign_key "reservations", "statistic_profiles"
add_foreign_key "slots_reservations", "reservations"
add_foreign_key "slots_reservations", "slots"
add_foreign_key "spaces_availabilities", "availabilities"
add_foreign_key "spaces_availabilities", "spaces"
add_foreign_key "statistic_custom_aggregations", "statistic_types"
add_foreign_key "statistic_profiles", "groups"
add_foreign_key "statistic_profiles", "users"
add_foreign_key "subscriptions", "statistic_profiles"
add_foreign_key "tickets", "event_price_categories"
add_foreign_key "tickets", "reservations"
add_foreign_key "user_tags", "tags"

View File

@ -89,7 +89,8 @@ Group.create! name: I18n.t('group.admins'), slug: 'admins' unless Group.find_by(
if Role.where(name: 'admin').joins(:users).count.zero?
admin = User.new(username: 'admin', email: ENV['ADMIN_EMAIL'], password: ENV['ADMIN_PASSWORD'],
password_confirmation: Rails.application.secrets.admin_password, group_id: Group.find_by(slug: 'admins').id,
profile_attributes: { first_name: 'admin', last_name: 'admin', gender: true, phone: '0123456789', birthday: Time.now })
profile_attributes: { first_name: 'admin', last_name: 'admin', phone: '0123456789' },
statistic_profile_attributes: { gender: true, birthday: Time.now })
admin.add_role 'admin'
admin.save!
end

View File

@ -42,12 +42,12 @@ This can be achieved doing the following:
## Using another DBMS
Some users may want to use another DBMS than PostgreSQL.
This is currently not supported, because of some PostgreSQL specific instructions that cannot be efficiently handled with the ActiveRecord ORM:
- `app/controllers/api/members_controllers.rb@list` is using `ILIKE`
- `app/controllers/api/invoices_controllers.rb@list` is using `ILIKE` and `date_trunc()`
- `app/services/members/list_service.rb@list` is using `ILIKE`, `now()::date` and `OFFSET`.
- `app/services/invoices_service.rb@list` is using `ILIKE` and `date_trunc()`
- `db/migrate/20160613093842_create_unaccent_function.rb` is using [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html) modules and defines a PL/pgSQL function (`f_unaccent()`)
- `app/controllers/api/members_controllers.rb@search` is using `f_unaccent()` (see above) and `regexp_replace()`
- `db/migrate/20150604131525_add_meta_data_to_notifications.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
- `db/migrate/20160915105234_add_transformation_to_o_auth2_mapping.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
- `db/migrate/20181217103441_migrate_settings_value_to_history_values.rb` is using `SELECT DISTINCT ON`.
- `db/migrate/20190107111749_protect_accounting_periods.rb` is using `CREATE RULE` and `DROP RULE`.
- `db/migrate/20190522115230_migrate_user_to_invoicing_profile.rb` is using `CREATE RULE` and `DROP RULE`.
- `db/migrate/20190522115230_migrate_user_to_invoicing_profile.rb` is using `CREATE RULE` and `DROP RULE`.

View File

@ -4,8 +4,6 @@ profile_1:
user_id: 1
first_name: admin
last_name: admin
gender: true
birthday: 2016-04-04
phone: 0123456789
interest:
software_mastered:
@ -17,8 +15,6 @@ profile_2:
user_id: 2
first_name: Jean
last_name: Dupont
gender: true
birthday: 1975-06-07
phone: '0214321420'
interest: 3D printers
software_mastered: autocad
@ -30,8 +26,6 @@ profile_4:
user_id: 4
first_name: Kevin
last_name: Dumas
gender: true
birthday: 1998-06-05
phone: 0446124793
interest: ''
software_mastered: solidworks
@ -43,8 +37,6 @@ profile_5:
user_id: 5
first_name: Vanessa
last_name: Lonchamp
gender: false
birthday: 1994-05-03
phone: 0233412482
interest: vélo, natation
software_mastered: ''
@ -56,8 +48,6 @@ profile_6:
user_id: 6
first_name: Gilbert
last_name: Partenaire
gender: true
birthday: 2016-04-04
phone: '0000000000'
interest:
software_mastered:
@ -69,8 +59,6 @@ profile_3:
user_id: 3
first_name: Paulette
last_name: Durand
gender: false
birthday: 1949-06-18
phone: '0474264261'
interest: ''
software_mastered: ''
@ -82,8 +70,6 @@ profile_7:
user_id: 7
first_name: Lucile
last_name: Seguin
gender: false
birthday: 1969-02-03
phone: '0241853679'
interest:
software_mastered:

View File

@ -1,7 +1,7 @@
reservation_1:
id: 1
user_id: 7
statistic_profile_id: 7
message:
created_at: 2012-03-12 11:03:31.651441000 Z
updated_at: 2012-03-12 11:03:31.651441000 Z
@ -12,7 +12,7 @@ reservation_1:
reservation_2:
id: 2
user_id: 3
statistic_profile_id: 3
message:
created_at: 2015-06-10 11:20:01.341130000 Z
updated_at: 2015-06-10 11:20:01.341130000 Z

48
test/fixtures/statistic_profiles.yml vendored Normal file
View File

@ -0,0 +1,48 @@
admin:
id: 1
user_id: 1
gender: true
birthday: 2016-04-04
group_id: 3
jdupont:
id: 2
user_id: 2
gender: true
birthday: 1975-06-07
group_id: 1
kdumas:
id: 4
user_id: 4
gender: true
birthday: 1998-06-05
group_id: 2
vlonchamp:
id: 5
user_id: 5
gender: false
birthday: 1994-05-03
group_id: 2
gpartenaire:
id: 6
user_id: 6
gender: true
birthday: 2016-04-04
group_id: 1
pdurand:
id: 3
user_id: 3
gender: false
birthday: 1949-06-18
group_id: 1
lseguin:
id: 7
user_id: 7
gender: false
birthday: 1969-02-03
group_id: 1

View File

@ -2,7 +2,7 @@
subscription_1:
id: 1
plan_id: 2
user_id: 3
statistic_profile_id: 3
stp_subscription_id: sub_8DGB4ErIc2asOv
created_at: <%= 10.days.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %>
updated_at: <%= 10.days.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %>
@ -12,7 +12,7 @@ subscription_1:
subscription_2:
id: 2
plan_id: 3
user_id: 4
statistic_profile_id: 4
stp_subscription_id:
created_at: <%= 10.days.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %>
updated_at: <%= 10.days.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %>
@ -23,7 +23,7 @@ subscription_2:
subscription_3:
id: 3
plan_id: 1
user_id: 7
statistic_profile_id: 7
stp_subscription_id:
created_at: 2012-03-12 11:03:31.651441000 Z
updated_at: 2012-03-12 11:03:31.651441000 Z

View File

@ -23,14 +23,16 @@ class AdminsTest < ActionDispatch::IntegrationTest
profile_attributes: {
first_name: 'Gérard',
last_name: 'Lepower',
gender: true,
birthday: '1999-09-19',
phone: '0547124852',
phone: '0547124852'
},
invoicing_profile_attributes: {
address_attributes: {
address: '6 Avenue Henri de Bournazel, 19000 Tulle'
}
},
statistic_profile_attributes: {
gender: true,
birthday: '1999-09-19'
}
}
}.to_json,

View File

@ -20,16 +20,18 @@ class MembersTest < ActionDispatch::IntegrationTest
email: email,
group_id: group_id,
profile_attributes: {
gender: true,
last_name: 'Dubois',
first_name: 'Robert',
birthday: '2018-02-08',
phone: '0485232145'
},
invoicing_profile_attributes: {
address_attributes: {
address: '21 grand rue, 73110 Bourget-en-Huile'
}
},
statistic_profile_attributes: {
gender: true,
birthday: '2018-02-08'
}
} }.to_json, default_headers
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Subscriptions
class CreateAsAdminTest < ActionDispatch::IntegrationTest
@ -7,11 +9,11 @@ module Subscriptions
login_as(@admin, scope: :user)
end
test "admin successfully takes a subscription for a user" do
test 'admin successfully takes a subscription for a user' do
user = User.find_by(username: 'jdupond')
plan = Plan.find_by(group_id: user.group.id, type: 'Plan', base_name: 'Mensuel')
VCR.use_cassette("subscriptions_admin_create_success") do
VCR.use_cassette('subscriptions_admin_create_success') do
post '/api/subscriptions',
{
subscription: {

View File

@ -1,9 +1,15 @@
# frozen_string_literal: true
require 'test_helper'
class UserTest < ActiveSupport::TestCase
test "must create a wallet after create user" do
test 'must create wallet and profiles after create user' do
u = User.create(username: 'user', email: 'userwallet@fabmanager.com', password: 'testpassword', password_confirmation: 'testpassword',
profile_attributes: {first_name: 'user', last_name: 'wallet', gender: true, birthday: 18.years.ago, phone: '0123456789'} )
profile_attributes: { first_name: 'user', last_name: 'wallet', phone: '0123456789' },
statistic_profile_attributes: { gender: true, birthday: 18.years.ago })
assert u.wallet.present?
assert u.profile.present?
assert u.invoicing_profile.present?
assert u.statistic_profile.present?
end
end

View File

@ -8,14 +8,14 @@ class SubscriptionExtensionAfterReservationTest < ActiveSupport::TestCase
@plan = Plan.find(3)
@plan.update!(is_rolling: true)
@user = User.joins(:subscriptions).find_by(subscriptions: { plan: @plan })
@user = User.joins(statistic_profile: [:subscriptions]).find_by(subscriptions: { plan_id: @plan.id })
@user.reservations.destroy_all # ensure no reservations
@availability = @machine.availabilities.first
slot = Slot.new(start_at: @availability.start_at, end_at: @availability.end_at, availability_id: @availability.id)
@reservation_machine = Reservation.new(user: @user, reservable: @machine, slots: [slot])
@reservation_training = Reservation.new(user: @user, reservable: @training, slots: [slot])
@reservation_machine = Reservation.new(statistic_profile: @user.statistic_profile, reservable: @machine, slots: [slot])
@reservation_training = Reservation.new(statistic_profile: @user.statistic_profile, reservable: @training, slots: [slot])
@reservation_training.save!
end

View File

@ -5,11 +5,11 @@ class UsersCreditsManagerTest < ActiveSupport::TestCase
@machine = Machine.find(6)
@training = Training.find(2)
@plan = Plan.find(3)
@user = User.joins(:subscriptions).find_by(subscriptions: { plan: @plan })
@user = User.joins(statistic_profile: [:subscriptions]).find_by(subscriptions: { plan_id: @plan.id })
@user.users_credits.destroy_all
@availability = @machine.availabilities.first
@reservation_machine = Reservation.new(user: @user, reservable: @machine)
@reservation_training = Reservation.new(user: @user, reservable: @training)
@reservation_machine = Reservation.new(statistic_profile: @user.statistic_profile, reservable: @machine)
@reservation_training = Reservation.new(statistic_profile: @user.statistic_profile, reservable: @training)
end
## context machine reservation