mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-01-10 17:39:38 +08:00
5e90ae7383
These methods should behave as the rest of setters on ActiveRecords, ergo setting the model state, but not saving it. The save is called at the end of the action inside the controller anyway, so this save is redundant.
470 lines
15 KiB
Ruby
470 lines
15 KiB
Ruby
class User < ApplicationRecord
|
|
include SearchableModel, SettingsModel
|
|
include User::TeamRoles, User::ProjectRoles
|
|
|
|
acts_as_token_authenticatable
|
|
devise :invitable, :confirmable, :database_authenticatable, :registerable,
|
|
:async, :recoverable, :rememberable, :trackable, :validatable,
|
|
:timeoutable, :omniauthable,
|
|
omniauth_providers: Extends::OMNIAUTH_PROVIDERS,
|
|
stretches: Constants::PASSWORD_STRETCH_FACTOR
|
|
has_attached_file :avatar,
|
|
styles: {
|
|
medium: Constants::MEDIUM_PIC_FORMAT,
|
|
thumb: Constants::THUMB_PIC_FORMAT,
|
|
icon: Constants::ICON_PIC_FORMAT,
|
|
icon_small: Constants::ICON_SMALL_PIC_FORMAT
|
|
},
|
|
default_url: Constants::DEFAULT_AVATAR_URL
|
|
|
|
auto_strip_attributes :full_name, :initials, nullify: false
|
|
validates :full_name,
|
|
presence: true,
|
|
length: { maximum: Constants::NAME_MAX_LENGTH }
|
|
validates :initials,
|
|
presence: true,
|
|
length: { maximum: Constants::USER_INITIALS_MAX_LENGTH }
|
|
validates :email,
|
|
presence: true,
|
|
length: { maximum: Constants::EMAIL_MAX_LENGTH }
|
|
|
|
validates_attachment :avatar,
|
|
:content_type => { :content_type => ["image/jpeg", "image/png"] },
|
|
size: { less_than: Constants::AVATAR_MAX_SIZE_MB.megabyte,
|
|
message: I18n.t('client_api.user.avatar_too_big') }
|
|
validate :time_zone_check
|
|
|
|
store_accessor :settings, :time_zone, :notifications_settings
|
|
|
|
default_settings(
|
|
time_zone: 'UTC',
|
|
notifications_settings: {
|
|
assignments: true,
|
|
assignments_email: false,
|
|
recent: true,
|
|
recent_email: false,
|
|
system_message_email: false
|
|
}
|
|
)
|
|
|
|
# Relations
|
|
has_many :user_identities, inverse_of: :user
|
|
has_many :user_teams, inverse_of: :user
|
|
has_many :teams, through: :user_teams
|
|
has_many :user_projects, inverse_of: :user
|
|
has_many :projects, through: :user_projects
|
|
has_many :user_my_modules, inverse_of: :user
|
|
has_many :my_modules, through: :user_my_modules
|
|
has_many :comments, inverse_of: :user
|
|
has_many :activities, inverse_of: :user
|
|
has_many :results, inverse_of: :user
|
|
has_many :samples, inverse_of: :user
|
|
has_many :samples_tables, inverse_of: :user, dependent: :destroy
|
|
has_many :repositories, inverse_of: :user
|
|
has_many :repository_table_states, inverse_of: :user, dependent: :destroy
|
|
has_many :steps, inverse_of: :user
|
|
has_many :custom_fields, inverse_of: :user
|
|
has_many :reports, inverse_of: :user
|
|
has_many :created_assets, class_name: 'Asset', foreign_key: 'created_by_id'
|
|
has_many :modified_assets,
|
|
class_name: 'Asset',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :created_checklists,
|
|
class_name: 'Checklist',
|
|
foreign_key: 'created_by_id'
|
|
has_many :modified_checklists,
|
|
class_name: 'Checklist',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :created_checklist_items,
|
|
class_name: 'ChecklistItem',
|
|
foreign_key: 'created_by_id'
|
|
has_many :modified_checklist_items,
|
|
class_name: 'ChecklistItem',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :modified_comments,
|
|
class_name: 'Comment',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :modified_custom_fields,
|
|
class_name: 'CustomField',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :created_my_module_groups,
|
|
class_name: 'MyModuleGroup',
|
|
foreign_key: 'created_by_id'
|
|
has_many :created_my_module_tags,
|
|
class_name: 'MyModuleTag',
|
|
foreign_key: 'created_by_id'
|
|
has_many :created_my_modules,
|
|
class_name: 'MyModule',
|
|
foreign_key: 'created_by_id'
|
|
has_many :modified_my_modules,
|
|
class_name: 'MyModule',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :archived_my_modules,
|
|
class_name: 'MyModule',
|
|
foreign_key: 'archived_by_id'
|
|
has_many :restored_my_modules,
|
|
class_name: 'MyModule',
|
|
foreign_key: 'restored_by_id'
|
|
has_many :created_teams,
|
|
class_name: 'Team',
|
|
foreign_key: 'created_by_id'
|
|
has_many :modified_teams,
|
|
class_name: 'Team',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :created_projects,
|
|
class_name: 'Project',
|
|
foreign_key: 'created_by_id'
|
|
has_many :modified_projects,
|
|
class_name: 'Project',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :archived_projects,
|
|
class_name: 'Project',
|
|
foreign_key: 'archived_by_id'
|
|
has_many :restored_projects,
|
|
class_name: 'Project',
|
|
foreign_key: 'restored_by_id'
|
|
has_many :modified_reports,
|
|
class_name: 'Report',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :modified_results,
|
|
class_name: 'Result',
|
|
foreign_key: 'modified_by_id'
|
|
has_many :archived_results,
|
|
class_name: 'Result',
|
|
foreign_key: 'archived_by_id'
|
|
has_many :restored_results,
|
|
class_name: 'Result',
|
|
foreign_key: 'restored_by_id'
|
|
has_many :created_sample_groups,
|
|
class_name: 'SampleGroup',
|
|
foreign_key: 'created_by_id'
|
|
has_many :modified_sample_groups,
|
|
class_name: 'SampleGroup',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :assigned_sample_my_modules,
|
|
class_name: 'SampleMyModule',
|
|
foreign_key: 'assigned_by_id'
|
|
has_many :created_sample_types,
|
|
class_name: 'SampleType',
|
|
foreign_key: 'created_by_id'
|
|
has_many :modified_sample_types,
|
|
class_name: 'SampleType',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :modified_samples,
|
|
class_name: 'Sample',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :modified_steps, class_name: 'Step', foreign_key: 'modified_by_id'
|
|
has_many :created_tables, class_name: 'Table', foreign_key: 'created_by_id'
|
|
has_many :modified_tables,
|
|
class_name: 'Table',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :created_tags, class_name: 'Tag', foreign_key: 'created_by_id'
|
|
|
|
has_many :tokens,
|
|
class_name: 'Token',
|
|
foreign_key: 'user_id',
|
|
inverse_of: :user
|
|
|
|
has_many :modified_tags,
|
|
class_name: 'Tag',
|
|
foreign_key: 'last_modified_by_id'
|
|
has_many :assigned_user_my_modules,
|
|
class_name: 'UserMyModule',
|
|
foreign_key: 'assigned_by_id'
|
|
has_many :assigned_user_teams,
|
|
class_name: 'UserTeam',
|
|
foreign_key: 'assigned_by_id'
|
|
has_many :assigned_user_projects,
|
|
class_name: 'UserProject',
|
|
foreign_key: 'assigned_by_id'
|
|
has_many :added_protocols,
|
|
class_name: 'Protocol',
|
|
foreign_key: 'added_by_id',
|
|
inverse_of: :added_by
|
|
has_many :archived_protocols,
|
|
class_name: 'Protocol',
|
|
foreign_key: 'archived_by_id',
|
|
inverse_of: :archived_by
|
|
has_many :restored_protocols,
|
|
class_name: 'Protocol',
|
|
foreign_key: 'restored_by_id',
|
|
inverse_of: :restored_by
|
|
has_many :assigned_my_module_repository_rows,
|
|
class_name: 'MyModuleRepositoryRow',
|
|
foreign_key: 'assigned_by_id'
|
|
|
|
has_many :user_notifications, inverse_of: :user
|
|
has_many :notifications, through: :user_notifications
|
|
has_many :zip_exports, inverse_of: :user, dependent: :destroy
|
|
has_many :datatables_teams, class_name: '::Views::Datatables::DatatablesTeam'
|
|
|
|
# If other errors besides parameter "avatar" exist,
|
|
# they will propagate to "avatar" also, so remove them
|
|
# and put all other (more specific ones) in it
|
|
after_validation :filter_paperclip_errors
|
|
|
|
before_destroy :destroy_notifications
|
|
|
|
def name
|
|
full_name
|
|
end
|
|
|
|
def name=(name)
|
|
self.full_name = name
|
|
end
|
|
|
|
def avatar_remote_url=(url_value)
|
|
self.avatar = URI.parse(url_value)
|
|
# Assuming url_value is http://example.com/photos/face.png
|
|
# avatar_file_name == "face.png"
|
|
# avatar_content_type == "image/png"
|
|
@avatar_remote_url = url_value
|
|
end
|
|
|
|
def current_team
|
|
Team.find_by_id(self.current_team_id)
|
|
end
|
|
|
|
def self.from_omniauth(auth)
|
|
includes(:user_identities)
|
|
.where(
|
|
'user_identities.provider=? AND user_identities.uid=?',
|
|
auth.provider,
|
|
auth.uid
|
|
)
|
|
.references(:user_identities)
|
|
.take
|
|
end
|
|
|
|
# Search all active users for username & email. Can
|
|
# also specify which team to ignore.
|
|
def self.search(
|
|
active_only,
|
|
query = nil,
|
|
team_to_ignore = nil
|
|
)
|
|
result = User.all
|
|
|
|
if active_only
|
|
result = result.where.not(confirmed_at: nil)
|
|
end
|
|
|
|
if team_to_ignore.present?
|
|
ignored_ids =
|
|
UserTeam
|
|
.select(:user_id)
|
|
.where(team_id: team_to_ignore.id)
|
|
result =
|
|
result
|
|
.where("users.id NOT IN (?)", ignored_ids)
|
|
end
|
|
|
|
result
|
|
.where_attributes_like([:full_name, :email], query)
|
|
.distinct
|
|
end
|
|
|
|
def empty_avatar(name, size)
|
|
file_ext = name.split(".").last
|
|
self.avatar_file_name = name
|
|
self.avatar_content_type = Rack::Mime.mime_type(".#{file_ext}")
|
|
self.avatar_file_size = size.to_i
|
|
end
|
|
|
|
def filter_paperclip_errors
|
|
if errors.key? :avatar
|
|
errors.delete(:avatar)
|
|
messages = []
|
|
errors.each do |attribute|
|
|
errors.full_messages_for(attribute).each do |message|
|
|
messages << message.split(' ').drop(1).join(' ')
|
|
end
|
|
end
|
|
errors.clear
|
|
errors.add(:avatar, messages.join(','))
|
|
end
|
|
end
|
|
|
|
# Whether user is active (= confirmed) or not
|
|
def active?
|
|
confirmed_at.present?
|
|
end
|
|
|
|
def active_status_str
|
|
if active?
|
|
I18n.t('users.enums.status.active')
|
|
else
|
|
I18n.t('users.enums.status.pending')
|
|
end
|
|
end
|
|
|
|
def projects_by_teams(team_id = 0, sort_by = nil, archived = false)
|
|
archived = archived ? true : false
|
|
query = Project.all.joins(:user_projects)
|
|
sql = 'projects.team_id IN (SELECT DISTINCT team_id ' \
|
|
'FROM user_teams WHERE user_teams.user_id = :user_id)'
|
|
if team_id.zero? || !user_teams.find_by(team_id: team_id).try(:admin?)
|
|
# Admins see all projects of team
|
|
sql += ' AND (projects.visibility=1 OR user_projects.user_id=:user_id)'
|
|
end
|
|
sql += ' AND projects.archived = :archived '
|
|
|
|
sort =
|
|
case sort_by
|
|
when 'old'
|
|
{ created_at: :asc }
|
|
when 'atoz'
|
|
{ name: :asc }
|
|
when 'ztoa'
|
|
{ name: :desc }
|
|
else
|
|
{ created_at: :desc }
|
|
end
|
|
|
|
if team_id > 0
|
|
result = query
|
|
.where('projects.team_id = ?', team_id)
|
|
.where(sql, user_id: id, archived: archived)
|
|
.order(sort)
|
|
.distinct
|
|
.group_by(&:team)
|
|
else
|
|
result = query
|
|
.where(sql, user_id: id, archived: archived)
|
|
.order(sort)
|
|
.distinct
|
|
.group_by(&:team)
|
|
end
|
|
result || []
|
|
end
|
|
|
|
# Finds all activities of user that is assigned to project. If user
|
|
# is not an owner of the project, user must be also assigned to
|
|
# module.
|
|
def last_activities
|
|
Activity
|
|
.joins(project: :user_projects)
|
|
.joins(
|
|
'LEFT OUTER JOIN my_modules ON activities.my_module_id = my_modules.id'
|
|
)
|
|
.joins(
|
|
'LEFT OUTER JOIN user_my_modules ON my_modules.id = ' \
|
|
'user_my_modules.my_module_id'
|
|
)
|
|
.where(user_projects: { user_id: self })
|
|
.where(
|
|
'activities.my_module_id IS NULL OR ' \
|
|
'user_projects.role = 0 OR ' \
|
|
'user_my_modules.user_id = ?',
|
|
id
|
|
)
|
|
.order(created_at: :desc)
|
|
end
|
|
|
|
def self.find_by_valid_wopi_token(token)
|
|
Rails.logger.warn "WOPI: searching by token #{token}"
|
|
User
|
|
.joins('LEFT OUTER JOIN tokens ON user_id = users.id')
|
|
.where(tokens: { token: token })
|
|
.where('tokens.ttl = 0 OR tokens.ttl > ?', Time.now.to_i)
|
|
.first
|
|
end
|
|
|
|
def get_wopi_token
|
|
# WOPI does not have a good way to request a new token,
|
|
# so a new token should be provided each time this is called,
|
|
# while keeping any old tokens as long as they have not yet expired
|
|
tokens = Token.where(user_id: id).distinct
|
|
|
|
tokens.each do |token|
|
|
token.delete if token.ttl < Time.now.to_i
|
|
end
|
|
|
|
token_string = "#{Devise.friendly_token(20)}-#{id}"
|
|
# WOPI uses millisecond TTLs
|
|
ttl = (Time.now + 1.day).to_i
|
|
wopi_token = Token.create(token: token_string, ttl: ttl, user_id: id)
|
|
Rails.logger.warn("WOPI: generating new token #{wopi_token.token}")
|
|
wopi_token
|
|
end
|
|
|
|
def teams_ids
|
|
teams.pluck(:id)
|
|
end
|
|
|
|
# Returns a hash with user statistics
|
|
def statistics
|
|
statistics = {}
|
|
statistics[:number_of_teams] = teams.count
|
|
statistics[:number_of_projects] = projects.count
|
|
number_of_experiments = 0
|
|
projects.find_each do |pr|
|
|
number_of_experiments += pr.experiments.count
|
|
end
|
|
statistics[:number_of_experiments] = number_of_experiments
|
|
statistics[:number_of_protocols] =
|
|
added_protocols.where(
|
|
protocol_type: Protocol.protocol_types.slice(
|
|
:in_repository_private,
|
|
:in_repository_public,
|
|
:in_repository_archived
|
|
).values
|
|
).count
|
|
statistics
|
|
end
|
|
|
|
# json friendly attributes
|
|
NOTIFICATIONS_TYPES = %w(assignments_notification recent_notification
|
|
assignments_email_notification
|
|
recent_email_notification
|
|
system_message_email_notification)
|
|
# declare notifications getters
|
|
NOTIFICATIONS_TYPES.each do |name|
|
|
define_method(name) do
|
|
attr_name = name.gsub('_notification', '')
|
|
notifications_settings.fetch(attr_name.to_sym)
|
|
end
|
|
end
|
|
|
|
# declare notifications setters
|
|
NOTIFICATIONS_TYPES.each do |name|
|
|
define_method("#{name}=") do |value|
|
|
attr_name = name.gsub('_notification', '').to_sym
|
|
notifications_settings[attr_name] = value
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def confirmation_required?
|
|
Rails.configuration.x.enable_email_confirmations
|
|
end
|
|
|
|
def time_zone_check
|
|
if time_zone.nil? ||
|
|
ActiveSupport::TimeZone.new(time_zone).nil?
|
|
errors.add(:time_zone)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def destroy_notifications
|
|
# Find all notifications where user is the only reference
|
|
# on the notification, and destroy all such notifications
|
|
# (user_notifications are destroyed when notification is
|
|
# destroyed). We try to do this efficiently (hence in_groups_of).
|
|
nids_all = notifications.pluck(:id)
|
|
nids_all.in_groups_of(1000, false) do |nids|
|
|
Notification
|
|
.where(id: nids)
|
|
.joins(:user_notifications)
|
|
.group('notifications.id')
|
|
.having('count(notification_id) <= 1')
|
|
.destroy_all
|
|
end
|
|
|
|
# Now, simply destroy all user notification relations left
|
|
user_notifications.destroy_all
|
|
end
|
|
end
|