2019-01-14 12:57:31 +01:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
# User is a physical or moral person with its authentication parameters
|
|
|
|
# It is linked to the Profile model with hold informations about this person (like address, name, etc.)
|
2015-05-05 03:10:25 +02:00
|
|
|
class User < ActiveRecord::Base
|
|
|
|
include NotifyWith::NotificationReceiver
|
|
|
|
include NotifyWith::NotificationAttachedObject
|
|
|
|
# Include default devise modules. Others available are:
|
2019-01-14 12:57:31 +01:00
|
|
|
# :lockable, :timeoutable and :omniauthable
|
2018-12-03 15:10:04 +01:00
|
|
|
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable,
|
2019-03-25 14:57:48 +01:00
|
|
|
:confirmable
|
2015-05-05 03:10:25 +02:00
|
|
|
rolify
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
# enable OmniAuth authentication only if needed
|
2018-12-03 15:10:04 +01:00
|
|
|
devise :omniauthable, omniauth_providers: [AuthProvider.active.strategy_name.to_sym] unless
|
|
|
|
AuthProvider.active.providable_type == DatabaseProvider.name
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2015-05-05 03:10:25 +02:00
|
|
|
extend FriendlyId
|
|
|
|
friendly_id :username, use: :slugged
|
|
|
|
|
|
|
|
has_one :profile, dependent: :destroy
|
|
|
|
accepts_nested_attributes_for :profile
|
|
|
|
|
2019-05-22 17:49:22 +02:00
|
|
|
has_one :invoicing_profile, dependent: :nullify
|
2019-05-29 14:28:14 +02:00
|
|
|
accepts_nested_attributes_for :invoicing_profile
|
2019-05-22 17:49:22 +02:00
|
|
|
|
2019-06-03 17:25:04 +02:00
|
|
|
has_one :statistic_profile, dependent: :nullify
|
|
|
|
accepts_nested_attributes_for :statistic_profile
|
|
|
|
|
2015-05-05 03:10:25 +02:00
|
|
|
has_many :project_users, dependent: :destroy
|
|
|
|
has_many :projects, through: :project_users
|
|
|
|
|
|
|
|
belongs_to :group
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
has_many :users_credits, dependent: :destroy
|
|
|
|
has_many :credits, through: :users_credits
|
|
|
|
|
|
|
|
has_many :training_credits, through: :users_credits, source: :training_credit
|
|
|
|
has_many :machine_credits, through: :users_credits, source: :machine_credit
|
|
|
|
|
|
|
|
has_many :user_tags, dependent: :destroy
|
|
|
|
has_many :tags, through: :user_tags
|
|
|
|
accepts_nested_attributes_for :tags, allow_destroy: true
|
|
|
|
|
2016-07-27 11:28:54 +02:00
|
|
|
has_many :exports, dependent: :destroy
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
# fix for create admin user
|
|
|
|
before_save do
|
2018-12-03 15:10:04 +01:00
|
|
|
email&.downcase!
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
2015-05-05 03:10:25 +02:00
|
|
|
before_create :assign_default_role
|
2016-03-23 18:39:41 +01:00
|
|
|
after_commit :create_stripe_customer, on: [:create]
|
|
|
|
after_commit :notify_admin_when_user_is_created, on: :create
|
2019-06-04 16:50:23 +02:00
|
|
|
after_create :init_dependencies
|
2016-03-23 18:39:41 +01:00
|
|
|
after_update :notify_group_changed, if: :group_id_changed?
|
2019-06-05 12:11:51 +02:00
|
|
|
after_update :update_invoicing_profile, if: :invoicing_data_was_modified?
|
|
|
|
after_update :update_statistic_profile, if: :statistic_data_was_modified?
|
2015-05-05 03:10:25 +02:00
|
|
|
|
|
|
|
attr_accessor :cgu
|
2016-03-23 18:39:41 +01:00
|
|
|
delegate :first_name, to: :profile
|
|
|
|
delegate :last_name, to: :profile
|
2019-06-04 16:50:23 +02:00
|
|
|
delegate :subscriptions, to: :statistic_profile
|
|
|
|
delegate :reservations, to: :statistic_profile
|
2019-06-06 12:00:21 +02:00
|
|
|
delegate :trainings, to: :statistic_profile
|
2019-06-06 16:34:53 +02:00
|
|
|
delegate :my_projects, to: :statistic_profile
|
2019-06-04 16:50:23 +02:00
|
|
|
delegate :wallet, to: :invoicing_profile
|
|
|
|
delegate :wallet_transactions, to: :invoicing_profile
|
|
|
|
delegate :invoices, to: :invoicing_profile
|
2015-05-05 03:10:25 +02:00
|
|
|
|
|
|
|
validate :cgu_must_accept, if: :new_record?
|
|
|
|
|
|
|
|
validates :username, presence: true, uniqueness: true, length: { maximum: 30 }
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
scope :active, -> { where(is_active: true) }
|
2019-06-04 16:50:23 +02:00
|
|
|
scope :without_subscription, -> { includes(statistic_profile: [:subscriptions]).where(subscriptions: { statistic_profile_id: nil }) }
|
|
|
|
scope :with_subscription, -> { joins(statistic_profile: [:subscriptions]) }
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-01-14 12:57:31 +01:00
|
|
|
def to_json(*)
|
2018-07-04 14:05:44 +02:00
|
|
|
ApplicationController.new.view_context.render(
|
2018-12-03 15:10:04 +01:00
|
|
|
partial: 'api/members/member',
|
|
|
|
locals: { member: self },
|
|
|
|
formats: [:json],
|
|
|
|
handlers: [:jbuilder]
|
2018-07-04 14:05:44 +02:00
|
|
|
)
|
2015-05-05 03:10:25 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.admins
|
|
|
|
User.with_role(:admin)
|
|
|
|
end
|
|
|
|
|
2019-03-20 16:49:38 +01:00
|
|
|
def self.superadmin
|
|
|
|
return unless Rails.application.secrets.superadmin_email.present?
|
|
|
|
|
|
|
|
User.find_by(email: Rails.application.secrets.superadmin_email)
|
|
|
|
end
|
|
|
|
|
2018-12-03 15:10:04 +01:00
|
|
|
def training_machine?(machine)
|
2019-01-14 12:57:31 +01:00
|
|
|
return true if admin?
|
2018-12-03 15:10:04 +01:00
|
|
|
|
|
|
|
trainings.map(&:machines).flatten.uniq.include?(machine)
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def training_reservation_by_machine(machine)
|
|
|
|
reservations.where(reservable_type: 'Training', reservable_id: machine.trainings.map(&:id)).first
|
|
|
|
end
|
|
|
|
|
|
|
|
def subscribed_plan
|
2018-12-10 17:20:23 +01:00
|
|
|
return nil if subscription.nil? || subscription.expired_at < Time.now
|
2018-12-03 15:10:04 +01:00
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
subscription.plan
|
|
|
|
end
|
|
|
|
|
|
|
|
def subscription
|
2018-12-10 13:24:00 +01:00
|
|
|
subscriptions.order(:created_at).last
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
2019-01-14 12:57:31 +01:00
|
|
|
def admin?
|
2015-05-05 03:10:25 +02:00
|
|
|
has_role? :admin
|
|
|
|
end
|
|
|
|
|
2019-01-14 12:57:31 +01:00
|
|
|
def member?
|
2015-05-05 03:10:25 +02:00
|
|
|
has_role? :member
|
|
|
|
end
|
|
|
|
|
|
|
|
def all_projects
|
|
|
|
my_projects.to_a.concat projects
|
|
|
|
end
|
|
|
|
|
2019-03-18 11:11:09 +01:00
|
|
|
def generate_subscription_invoice(operator_id)
|
2018-12-03 15:10:04 +01:00
|
|
|
return unless subscription
|
|
|
|
|
2019-03-18 11:11:09 +01:00
|
|
|
subscription.generate_and_save_invoice(operator_id)
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def stripe_customer
|
|
|
|
Stripe::Customer.retrieve stp_customer_id
|
|
|
|
end
|
|
|
|
|
|
|
|
def active_for_authentication?
|
2018-12-03 15:10:04 +01:00
|
|
|
super && is_active?
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.from_omniauth(auth)
|
|
|
|
active_provider = AuthProvider.active
|
2019-06-04 13:33:00 +02:00
|
|
|
raise SecurityError, 'The identity provider does not match the activated one' if active_provider.strategy_name != auth.provider
|
2016-03-23 18:39:41 +01:00
|
|
|
|
|
|
|
where(provider: auth.provider, uid: auth.uid).first_or_create.tap do |user|
|
|
|
|
# execute this regardless of whether record exists or not (-> User#tap)
|
2016-10-07 11:11:58 +02:00
|
|
|
# this will init or update the user thanks to the information retrieved from the SSO
|
2016-03-23 18:39:41 +01:00
|
|
|
user.profile ||= Profile.new
|
|
|
|
auth.info.mapping.each do |key, value|
|
|
|
|
user.set_data_from_sso_mapping(key, value)
|
|
|
|
end
|
|
|
|
user.password = Devise.friendly_token[0,20]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def need_completion?
|
2019-06-04 13:33:00 +02:00
|
|
|
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?
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
## Retrieve the requested data in the User and user's Profile tables
|
|
|
|
## @param sso_mapping {String} must be of form 'user._field_' or 'profile._field_'. Eg. 'user.email'
|
|
|
|
def get_data_from_sso_mapping(sso_mapping)
|
|
|
|
parsed = /^(user|profile)\.(.+)$/.match(sso_mapping)
|
|
|
|
if parsed[1] == 'user'
|
|
|
|
self[parsed[2].to_sym]
|
|
|
|
elsif parsed[1] == 'profile'
|
2016-09-12 12:10:46 +02:00
|
|
|
case sso_mapping
|
2018-12-03 15:10:04 +01:00
|
|
|
when 'profile.avatar'
|
|
|
|
profile.user_avatar.remote_attachment_url
|
|
|
|
when 'profile.address'
|
2019-06-03 12:06:01 +02:00
|
|
|
invoicing_profile.address.address
|
2018-12-03 15:10:04 +01:00
|
|
|
when 'profile.organization_name'
|
2019-06-03 12:06:01 +02:00
|
|
|
invoicing_profile.organization.name
|
2018-12-03 15:10:04 +01:00
|
|
|
when 'profile.organization_address'
|
2019-06-03 12:06:01 +02:00
|
|
|
invoicing_profile.organization.address.address
|
2018-12-03 15:10:04 +01:00
|
|
|
else
|
|
|
|
profile[parsed[2].to_sym]
|
2016-09-12 12:10:46 +02:00
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
## Set some data on the current user, according to the sso_key given
|
|
|
|
## @param sso_mapping {String} must be of form 'user._field_' or 'profile._field_'. Eg. 'user.email'
|
|
|
|
## @param data {*} the data to put in the given key. Eg. 'user@example.com'
|
|
|
|
def set_data_from_sso_mapping(sso_mapping, data)
|
|
|
|
if sso_mapping.to_s.start_with? 'user.'
|
|
|
|
self[sso_mapping[5..-1].to_sym] = data unless data.nil?
|
|
|
|
elsif sso_mapping.to_s.start_with? 'profile.'
|
2016-08-02 15:11:26 +02:00
|
|
|
case sso_mapping.to_s
|
2018-12-03 15:10:04 +01:00
|
|
|
when 'profile.avatar'
|
|
|
|
profile.user_avatar ||= UserAvatar.new
|
|
|
|
profile.user_avatar.remote_attachment_url = data
|
|
|
|
when 'profile.address'
|
2019-06-03 12:06:01 +02:00
|
|
|
invoicing_profile.address ||= Address.new
|
|
|
|
invoicing_profile.address.address = data
|
2018-12-03 15:10:04 +01:00
|
|
|
when 'profile.organization_name'
|
2019-06-03 12:06:01 +02:00
|
|
|
invoicing_profile.organization ||= Organization.new
|
|
|
|
invoicing_profile.organization.name = data
|
2018-12-03 15:10:04 +01:00
|
|
|
when 'profile.organization_address'
|
2019-06-03 12:06:01 +02:00
|
|
|
invoicing_profile.organization ||= Organization.new
|
|
|
|
invoicing_profile.organization.address ||= Address.new
|
|
|
|
invoicing_profile.organization.address.address = data
|
2018-12-03 15:10:04 +01:00
|
|
|
else
|
|
|
|
profile[sso_mapping[8..-1].to_sym] = data unless data.nil?
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
## used to allow the migration of existing users between authentication providers
|
|
|
|
def generate_auth_migration_token
|
2018-12-06 18:26:01 +01:00
|
|
|
update_attributes(auth_token: Devise.friendly_token)
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
## link the current user to the given provider (omniauth attributes hash)
|
|
|
|
## and remove the auth_token to mark his account as "migrated"
|
|
|
|
def link_with_omniauth_provider(auth)
|
|
|
|
active_provider = AuthProvider.active
|
2019-06-04 13:33:00 +02:00
|
|
|
raise SecurityError, 'The identity provider does not match the activated one' if active_provider.strategy_name != auth.provider
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-12-03 15:10:04 +01:00
|
|
|
if User.where(provider: auth.provider, uid: auth.uid).size.positive?
|
2016-03-23 18:39:41 +01:00
|
|
|
raise DuplicateIndexError, "This #{active_provider.name} account is already linked to an existing user"
|
|
|
|
end
|
|
|
|
|
|
|
|
update_attributes(provider: auth.provider, uid: auth.uid, auth_token: nil)
|
|
|
|
end
|
|
|
|
|
|
|
|
## Merge the provided User's SSO details into the current user and drop the provided user to ensure the unity
|
|
|
|
## @param sso_user {User} the provided user will be DELETED after the merge was successful
|
|
|
|
def merge_from_sso(sso_user)
|
2018-12-06 18:26:01 +01:00
|
|
|
# update the attributes to link the account to the sso account
|
2016-03-23 18:39:41 +01:00
|
|
|
self.provider = sso_user.provider
|
|
|
|
self.uid = sso_user.uid
|
|
|
|
|
|
|
|
# remove the token
|
|
|
|
self.auth_token = nil
|
|
|
|
self.merged_at = DateTime.now
|
|
|
|
|
|
|
|
# check that the email duplication was resolved
|
|
|
|
if sso_user.email.end_with? '-duplicate'
|
|
|
|
email_addr = sso_user.email.match(/^<([^>]+)>.{20}-duplicate$/)[1]
|
2018-12-03 15:10:04 +01:00
|
|
|
raise(DuplicateIndexError, email_addr) unless email_addr == email
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
# update the user's profile to set the data managed by the SSO
|
|
|
|
auth_provider = AuthProvider.from_strategy_name(sso_user.provider)
|
|
|
|
auth_provider.sso_fields.each do |field|
|
|
|
|
value = sso_user.get_data_from_sso_mapping(field)
|
|
|
|
# we do not merge the email field if its end with the special value '-duplicate' as this means
|
|
|
|
# that the user is currently merging with the account that have the same email than the sso
|
2018-12-03 15:10:04 +01:00
|
|
|
set_data_from_sso_mapping(field, value) unless field == 'user.email' && value.end_with?('-duplicate')
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
2018-12-06 18:26:01 +01:00
|
|
|
# run the account transfer in an SQL transaction to ensure data integrity
|
2016-03-23 18:39:41 +01:00
|
|
|
User.transaction do
|
|
|
|
# remove the temporary account
|
|
|
|
sso_user.destroy
|
|
|
|
# finally, save the new details
|
2018-12-03 15:10:04 +01:00
|
|
|
save!
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-09-14 16:41:45 +02:00
|
|
|
def self.mapping
|
|
|
|
# we protect some fields as they are designed to be managed by the system and must not be updated externally
|
2018-12-03 15:10:04 +01:00
|
|
|
blacklist = %w[id encrypted_password reset_password_token reset_password_sent_at remember_created_at
|
|
|
|
sign_in_count current_sign_in_at last_sign_in_at current_sign_in_ip last_sign_in_ip confirmation_token
|
|
|
|
confirmed_at confirmation_sent_at unconfirmed_email failed_attempts unlock_token locked_at created_at
|
|
|
|
updated_at stp_customer_id slug provider auth_token merged_at]
|
2016-09-14 16:41:45 +02:00
|
|
|
User.column_types
|
2018-12-03 15:10:04 +01:00
|
|
|
.map { |k, v| [k, v.type.to_s] }
|
2016-09-14 16:41:45 +02:00
|
|
|
.delete_if { |col| blacklist.include?(col[0]) }
|
|
|
|
end
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
protected
|
2018-12-03 15:10:04 +01:00
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
def confirmation_required?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2015-05-05 03:10:25 +02:00
|
|
|
private
|
2018-12-03 15:10:04 +01:00
|
|
|
|
2015-05-05 03:10:25 +02:00
|
|
|
def assign_default_role
|
2018-12-03 15:10:04 +01:00
|
|
|
add_role(:member) if roles.blank?
|
2015-05-05 03:10:25 +02:00
|
|
|
end
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
def cached_has_role?(role)
|
2018-12-03 15:10:04 +01:00
|
|
|
roles = Rails.cache.fetch(
|
|
|
|
roles_for: { object_id: object_id },
|
|
|
|
expires_in: 1.day,
|
|
|
|
race_condition_ttl: 2.seconds
|
|
|
|
) { roles.map(&:name) }
|
2016-03-23 18:39:41 +01:00
|
|
|
roles.include?(role.to_s)
|
|
|
|
end
|
|
|
|
|
2015-05-05 03:10:25 +02:00
|
|
|
def cgu_must_accept
|
|
|
|
errors.add(:cgu, I18n.t('activerecord.errors.messages.empty')) if cgu == '0'
|
|
|
|
end
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
def create_stripe_customer
|
|
|
|
StripeWorker.perform_async(:create_stripe_customer, id)
|
|
|
|
end
|
|
|
|
|
2019-03-25 14:57:48 +01:00
|
|
|
def send_devise_notification(notification, *args)
|
|
|
|
devise_mailer.send(notification, self, *args).deliver_later
|
|
|
|
end
|
|
|
|
|
2015-05-05 03:10:25 +02:00
|
|
|
def notify_admin_when_user_is_created
|
2018-12-06 18:26:01 +01:00
|
|
|
if need_completion? && !provider.nil?
|
2016-03-23 18:39:41 +01:00
|
|
|
NotificationCenter.call type: 'notify_admin_when_user_is_imported',
|
|
|
|
receiver: User.admins,
|
|
|
|
attached_object: self
|
|
|
|
else
|
|
|
|
NotificationCenter.call type: 'notify_admin_when_user_is_created',
|
|
|
|
receiver: User.admins,
|
|
|
|
attached_object: self
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def notify_group_changed
|
2018-12-03 15:10:04 +01:00
|
|
|
return if changes[:group_id].first.nil?
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2018-12-03 15:10:04 +01:00
|
|
|
ex_group = Group.find(changes[:group_id].first)
|
|
|
|
meta_data = { ex_group_name: ex_group.name }
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-02-26 10:45:12 +01:00
|
|
|
NotificationCenter.call type: :notify_admin_user_group_changed,
|
|
|
|
receiver: User.admins,
|
|
|
|
attached_object: self,
|
|
|
|
meta_data: meta_data
|
2018-12-03 15:10:04 +01:00
|
|
|
|
|
|
|
NotificationCenter.call type: :notify_user_user_group_changed,
|
|
|
|
receiver: self,
|
|
|
|
attached_object: self
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
2019-06-03 12:06:01 +02:00
|
|
|
|
2019-06-03 17:25:04 +02:00
|
|
|
def invoicing_data_was_modified?
|
2019-06-04 16:50:23 +02:00
|
|
|
email_changed?
|
2019-06-03 17:25:04 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
def statistic_data_was_modified?
|
2019-06-04 16:50:23 +02:00
|
|
|
group_id_changed?
|
|
|
|
end
|
|
|
|
|
|
|
|
def init_dependencies
|
2019-06-05 12:11:51 +02:00
|
|
|
if invoicing_profile.nil?
|
|
|
|
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
|
2019-06-05 16:09:11 +02:00
|
|
|
if statistic_profile.nil?
|
|
|
|
StatisticProfile.create!(
|
|
|
|
user: self,
|
2019-06-06 13:58:49 +02:00
|
|
|
group_id: group_id,
|
|
|
|
role_id: roles.first.id
|
2019-06-05 16:09:11 +02:00
|
|
|
)
|
|
|
|
else
|
|
|
|
update_statistic_profile
|
|
|
|
end
|
2019-06-03 17:25:04 +02:00
|
|
|
end
|
|
|
|
|
2019-06-03 12:06:01 +02:00
|
|
|
def update_invoicing_profile
|
2019-06-05 12:11:51 +02:00
|
|
|
raise NoProfileError if invoicing_profile.nil?
|
2019-06-04 16:50:23 +02:00
|
|
|
|
|
|
|
invoicing_profile.update_attributes(
|
|
|
|
email: email
|
|
|
|
)
|
2019-06-03 12:06:01 +02:00
|
|
|
end
|
2019-06-03 17:25:04 +02:00
|
|
|
|
2019-06-06 13:58:49 +02:00
|
|
|
# will update the statistic_profile after a group switch. Updating the role is not supported
|
2019-06-03 17:25:04 +02:00
|
|
|
def update_statistic_profile
|
2019-06-05 12:11:51 +02:00
|
|
|
raise NoProfileError if statistic_profile.nil?
|
2019-06-04 16:50:23 +02:00
|
|
|
|
|
|
|
statistic_profile.update_attributes(
|
|
|
|
group_id: group_id
|
|
|
|
)
|
2019-06-03 17:25:04 +02:00
|
|
|
end
|
2015-05-05 03:10:25 +02:00
|
|
|
end
|