scinote-web/app/models/user.rb

642 lines
21 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2017-06-23 21:19:08 +08:00
class User < ApplicationRecord
2018-10-12 06:48:12 +08:00
include SearchableModel
include SettingsModel
include VariablesModel
include InputSanitizeHelper
2019-09-23 22:33:57 +08:00
include ActiveStorageConcerns
2016-02-12 23:52:43 +08:00
devise :invitable, :confirmable, :database_authenticatable, :registerable,
:async, :recoverable, :rememberable, :trackable, :validatable,
2020-07-14 20:49:51 +08:00
:timeoutable, :omniauthable, :lockable,
2018-04-26 23:18:03 +08:00
omniauth_providers: Extends::OMNIAUTH_PROVIDERS,
stretches: Constants::PASSWORD_STRETCH_FACTOR
has_one_attached :avatar
2016-02-12 23:52:43 +08:00
auto_strip_attributes :full_name, :initials, nullify: false
validates :full_name,
presence: true,
length: { maximum: Constants::NAME_MAX_LENGTH }
validates :initials,
2016-09-16 21:41:31 +08:00
presence: true,
length: { maximum: Constants::USER_INITIALS_MAX_LENGTH }
validates :email,
presence: true,
length: { maximum: Constants::EMAIL_MAX_LENGTH }
2016-02-12 23:52:43 +08:00
validate :time_zone_check
validates :external_id, length: { maximum: Constants::EMAIL_MAX_LENGTH }
store_accessor :settings, :time_zone, :notifications_settings, :external_id
2021-04-08 23:40:16 +08:00
DEFAULT_SETTINGS = {
time_zone: 'UTC',
date_format: Constants::DEFAULT_DATE_FORMAT,
notifications_settings: {
assignments: true,
assignments_email: false,
recent: true,
recent_email: false,
2018-09-05 22:42:15 +08:00
system_message_email: false
}.merge(Extends::DEFAULT_USER_NOTIFICATION_SETTINGS)
2021-04-08 23:40:16 +08:00
}.freeze
2017-09-28 21:00:54 +08:00
DEFAULT_OTP_DRIFT_TIME_SECONDS = 10
store_accessor :variables, :export_vars
default_variables(
export_vars: {
num_of_export_all_last_24_hours: 0,
last_export_timestamp: Time.now.utc.beginning_of_day.to_i
}
)
2016-02-12 23:52:43 +08:00
# Relations
2017-08-30 00:49:07 +08:00
has_many :user_identities, inverse_of: :user
has_many :user_assignments, dependent: :destroy
2016-02-12 23:52:43 +08:00
has_many :user_projects, inverse_of: :user
has_many :teams, through: :user_assignments, source: :assignable, source_type: 'Team'
has_many :projects, through: :user_assignments, source: :assignable, source_type: 'Project'
2016-02-12 23:52:43 +08:00
has_many :user_my_modules, inverse_of: :user
has_many :my_modules, through: :user_assignments, source: :assignable, source_type: 'MyModule'
2016-02-12 23:52:43 +08:00
has_many :comments, inverse_of: :user
has_many :activities, inverse_of: :owner, foreign_key: 'owner_id'
2016-02-12 23:52:43 +08:00
has_many :results, inverse_of: :user
2017-06-06 23:35:29 +08:00
has_many :repositories, inverse_of: :user
2017-06-07 19:36:39 +08:00
has_many :repository_table_states, inverse_of: :user, dependent: :destroy
has_many :repository_table_filters, foreign_key: 'created_by_id', inverse_of: :created_by, dependent: :nullify
2016-02-12 23:52:43 +08:00
has_many :steps, inverse_of: :user
has_many :reports, inverse_of: :user
has_many :created_assets, class_name: 'Asset', foreign_key: 'created_by_id'
2017-01-25 22:00:14 +08:00
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 :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 :archived_project_folders,
class_name: 'ProjectFolder',
foreign_key: 'archived_by_id',
inverse_of: :arhived_by
has_many :restored_project_folders,
class_name: 'ProjectFolder',
foreign_key: 'restored_by_id',
inverse_of: :restored_by
2017-01-25 22:00:14 +08:00
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'
2016-02-12 23:52:43 +08:00
has_many :modified_steps, class_name: 'Step', foreign_key: 'modified_by_id'
has_many :created_tables, class_name: 'Table', foreign_key: 'created_by_id'
2017-01-25 22:00:14 +08:00
has_many :modified_tables,
class_name: 'Table',
foreign_key: 'last_modified_by_id'
2016-02-12 23:52:43 +08:00
has_many :created_tags, class_name: 'Tag', foreign_key: 'created_by_id'
has_many :tokens,
class_name: 'Token',
inverse_of: :user,
dependent: :destroy
2017-01-25 22:00:14 +08:00
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_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 :published_protocols,
class_name: 'Protocol',
foreign_key: 'published_by_id',
inverse_of: :published_by,
dependent: :nullify
2020-06-04 21:44:47 +08:00
has_many :archived_repositories,
class_name: 'Repository',
2020-06-08 22:22:37 +08:00
foreign_key: 'archived_by_id',
inverse_of: :archived_by,
dependent: :nullify
has_many :restored_repositories,
class_name: 'Repository',
foreign_key: 'restored_by_id',
inverse_of: :restored_by,
dependent: :nullify
has_many :created_repository_rows,
class_name: 'RepositoryRow',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
2020-06-05 05:29:37 +08:00
has_many :archived_repository_rows,
class_name: 'RepositoryRow',
2020-06-08 22:22:37 +08:00
foreign_key: 'archived_by_id',
inverse_of: :archived_by,
dependent: :nullify
2020-06-10 00:43:22 +08:00
has_many :restored_repository_rows,
class_name: 'RepositoryRow',
foreign_key: 'restored_by_id',
inverse_of: :restored_by,
dependent: :nullify
has_many :assigned_my_module_repository_rows,
class_name: 'MyModuleRepositoryRow',
2017-06-06 23:35:29 +08:00
foreign_key: 'assigned_by_id'
2019-12-10 16:40:40 +08:00
has_many :created_repository_status_types,
class_name: 'RepositoryStatusItem',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
2019-12-10 16:40:40 +08:00
has_many :modified_repository_status_types,
class_name: 'RepositoryStatusItem',
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
2019-12-10 16:40:40 +08:00
has_many :created_repository_status_value,
class_name: 'RepositoryStatusValue',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
2019-12-10 16:40:40 +08:00
has_many :modified_repository_status_value,
class_name: 'RepositoryStatusValue',
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
2019-12-10 16:40:40 +08:00
has_many :created_repository_date_time_values,
2019-11-18 22:21:57 +08:00
class_name: 'RepositoryDateTimeValue',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
2019-12-10 16:40:40 +08:00
has_many :modified_repository_date_time_values,
2019-11-18 22:21:57 +08:00
class_name: 'RepositoryDateTimeValue',
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
2019-12-09 20:38:40 +08:00
has_many :created_repository_checklist_values,
class_name: 'RepositoryChecklistValue',
2019-12-06 20:18:35 +08:00
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
2019-12-09 20:38:40 +08:00
has_many :modified_repository_checklist_values,
class_name: 'RepositoryChecklistValue',
2019-12-06 20:18:35 +08:00
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
has_many :created_repository_stock_value,
class_name: 'RepositoryStockValue',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
has_many :modified_repository_stock_value,
class_name: 'RepositoryStockValue',
foreign_key: 'created_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
2019-12-09 20:38:40 +08:00
has_many :created_repository_checklist_types,
class_name: 'RepositoryChecklistItem',
2019-12-06 20:18:35 +08:00
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
2019-12-09 20:38:40 +08:00
has_many :modified_repository_checklist_types,
class_name: 'RepositoryChecklistItem',
2019-12-06 20:18:35 +08:00
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
2019-12-10 19:02:08 +08:00
has_many :created_repository_number_values,
2019-12-09 23:38:21 +08:00
class_name: 'RepositoryNumberValue',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
2019-12-10 19:02:08 +08:00
has_many :modified_repository_number_values,
2019-12-09 23:38:21 +08:00
class_name: 'RepositoryNumberValue',
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
has_many :created_repository_text_values,
class_name: 'RepositoryTextValue',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
has_many :modified_repository_text_values,
class_name: 'RepositoryTextValue',
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
has_many :created_repository_stock_values,
class_name: 'RepositoryStockValue',
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :nullify
has_many :modified_repository_stock_values,
class_name: 'RepositoryStockValue',
foreign_key: 'last_modified_by_id',
inverse_of: :last_modified_by,
dependent: :nullify
has_many :shareable_links,
foreign_key: 'created_by_id',
inverse_of: :created_by,
dependent: :destroy
2023-10-11 19:43:20 +08:00
has_many :notifications, as: :recipient, dependent: :destroy, inverse_of: :recipient
2017-03-23 22:45:02 +08:00
has_many :zip_exports, inverse_of: :user, dependent: :destroy
has_many :view_states, dependent: :destroy
2016-02-12 23:52:43 +08:00
2018-08-17 17:59:47 +08:00
has_many :access_grants, class_name: 'Doorkeeper::AccessGrant',
foreign_key: :resource_owner_id,
dependent: :delete_all
has_many :access_tokens, class_name: 'Doorkeeper::AccessToken',
foreign_key: :resource_owner_id,
dependent: :delete_all
has_many :asset_sync_tokens, dependent: :destroy
2018-08-17 17:59:47 +08:00
has_many :hidden_repository_cell_reminders, dependent: :destroy
before_validation :downcase_email!
2016-02-12 23:52:43 +08:00
def name
full_name
end
def name=(name)
2017-07-06 15:07:05 +08:00
self.full_name = name
2016-02-12 23:52:43 +08:00
end
2018-02-26 18:05:05 +08:00
def avatar_remote_url=(url_value)
self.avatar = URI.parse(url_value)
# Assuming url_value is http://example.com/photos/face.png
# avatar.filename == "face.png"
# avatar.content_type == "image/png"
2018-02-26 18:05:05 +08:00
@avatar_remote_url = url_value
end
def avatar_variant(style)
2019-10-22 18:27:13 +08:00
return Constants::DEFAULT_AVATAR_URL.gsub(':style', style.to_s) unless avatar.attached?
format = case style.to_sym
when :medium
Constants::MEDIUM_PIC_FORMAT
when :thumb
Constants::THUMB_PIC_FORMAT
when :icon
Constants::ICON_PIC_FORMAT
when :icon_small
Constants::ICON_SMALL_PIC_FORMAT
else
Constants::ICON_SMALL_PIC_FORMAT
end
2019-09-12 23:21:48 +08:00
avatar.variant(resize_to_limit: format)
end
def avatar_url(style)
Rails.application.routes.url_helpers.url_for(avatar_variant(style))
end
def date_format
settings[:date_format] || Constants::DEFAULT_DATE_FORMAT
end
def date_format=(date_format)
return if settings[:date_format] == date_format
if Constants::SUPPORTED_DATE_FORMATS.include?(date_format)
settings[:date_format] = date_format
clear_view_cache
end
end
2017-08-03 22:03:15 +08:00
def current_team
Team.find_by_id(self.current_team_id)
end
def permission_team=(team)
@permission_team = teams.find_by(id: team.id)
end
def permission_team
@permission_team || current_team
end
2017-08-30 00:49:07 +08:00
def self.from_omniauth(auth)
includes(:user_identities)
.where(
'user_identities.provider=? AND user_identities.uid=?',
auth.provider,
auth.uid
)
.references(:user_identities)
.take
2016-02-12 23:52:43 +08:00
end
def member_of_team?(team)
team.user_assignments.exists?(user: self)
end
2016-02-12 23:52:43 +08:00
# Search all active users for username & email. Can
2017-01-24 23:34:21 +08:00
# also specify which team to ignore.
2016-02-12 23:52:43 +08:00
def self.search(
active_only,
query = nil,
2017-01-24 23:34:21 +08:00
team_to_ignore = nil
2016-02-12 23:52:43 +08:00
)
result = User.all
result = result.where.not(confirmed_at: nil) if active_only
2016-02-12 23:52:43 +08:00
2017-01-24 23:34:21 +08:00
if team_to_ignore.present?
ignored_ids = UserAssignment.select(:user_id).where(assignable: team_to_ignore)
2021-10-28 18:58:42 +08:00
result = result.where.not(users: { id: ignored_ids })
2016-02-12 23:52:43 +08:00
end
result.where_attributes_like(%i(full_name email), query).distinct
2016-07-21 19:11:15 +08:00
end
2016-02-12 23:52:43 +08:00
# Whether user is active (= confirmed) or not
def active?
confirmed_at.present?
2016-02-12 23:52:43 +08:00
end
def active_status_str
if active?
2017-01-25 22:00:14 +08:00
I18n.t('users.enums.status.active')
2016-02-12 23:52:43 +08:00
else
2017-01-25 22:00:14 +08:00
I18n.t('users.enums.status.pending')
2016-02-12 23:52:43 +08:00
end
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.
2017-12-14 21:02:40 +08:00
def last_activities
2016-02-12 23:52:43 +08:00
Activity
.joins(project: :user_projects)
2017-12-14 21:02:40 +08:00
.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'
)
2016-02-12 23:52:43 +08:00
.where(user_projects: { user_id: self })
.where(
2017-12-14 21:02:40 +08:00
'activities.my_module_id IS NULL OR ' \
'user_projects.role = 0 OR ' \
2016-02-12 23:52:43 +08:00
'user_my_modules.user_id = ?',
id
)
.order(created_at: :desc)
end
2016-08-03 21:31:25 +08:00
def self.find_by_valid_wopi_token(token)
Rails.logger.warn "WOPI: searching by token #{token}"
User.joins(:tokens)
.where(tokens: { token: token })
.find_by('tokens.ttl = 0 OR tokens.ttl > ?', Time.now.to_i)
2016-08-03 21:31:25 +08:00
end
def get_wopi_token
2016-12-21 23:52:15 +08:00
# 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
2016-12-01 20:48:11 +08:00
2016-12-21 23:52:15 +08:00
tokens.each do |token|
token.delete if token.ttl < Time.now.to_i
end
2016-12-01 20:48:11 +08:00
2016-12-21 23:52:15 +08:00
token_string = "#{Devise.friendly_token(20)}-#{id}"
# WOPI uses millisecond TTLs
2016-12-08 18:12:24 +08:00
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
2016-08-03 21:31:25 +08:00
end
2017-01-24 23:34:21 +08:00
def teams_ids
teams.pluck(:id)
2016-10-25 02:07:20 +08:00
end
# Returns a hash with user statistics
def statistics
statistics = {}
2017-01-24 23:34:21 +08:00
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[:in_repository_published_original]}
OR protocol_type=#{Protocol.protocol_types[:in_repository_draft]}) AND protocols.parent_id IS NULL"
).count
statistics
end
def self.from_azure_jwt_token(token_payload)
settings = ApplicationSettings.instance
provider_conf = settings.values['azure_ad_apps']&.find { |v| v['app_id'] == token_payload[:aud] }
return nil unless provider_conf
joins(:user_identities)
.find_by(user_identities: { provider: provider_conf['provider_name'], uid: token_payload[:sub] })
end
def has_linked_account?(provider)
user_identities.exists?(provider: provider)
end
def increase_daily_exports_counter!
range = Time.now.utc.beginning_of_day.to_i..Time.now.utc.end_of_day.to_i
last_export = export_vars[:last_export_timestamp] || 0
export_vars[:num_of_export_all_last_24_hours] ||= 0
if range.cover?(last_export)
export_vars[:num_of_export_all_last_24_hours] += 1
else
export_vars[:num_of_export_all_last_24_hours] = 1
end
export_vars[:last_export_timestamp] = Time.now.utc.to_i
save
end
def has_available_exports?
last_export_timestamp = export_vars[:last_export_timestamp] || 0
# limit 0 means unlimited exports
return true if TeamZipExport.exports_limit.zero? || last_export_timestamp < Time.now.utc.beginning_of_day.to_i
exports_left.positive?
end
def exports_left
if (export_vars[:last_export_timestamp] || 0) < Time.now.utc.beginning_of_day.to_i
return TeamZipExport.exports_limit
end
TeamZipExport.exports_limit - export_vars[:num_of_export_all_last_24_hours]
end
def global_activity_filter(filters, search_query)
query_teams = teams
query_teams = query_teams.where(id: filters[:teams].map(&:to_i)) if filters[:teams].present?
query_teams = query_teams.by_activity_subjects(filters[:subjects]) if filters[:subjects].present?
User.where(id: UserAssignment.where(assignable_id: query_teams, assignable_type: 'Team').select(:user_id))
.search(false, search_query)
end
def file_name
return '' unless avatar.attached?
avatar.blob&.filename&.sanitized
end
2020-06-30 20:16:00 +08:00
def valid_otp?(otp)
raise StandardError, 'Missing otp_secret' unless otp_secret
totp = ROTP::TOTP.new(otp_secret, issuer: 'sciNote')
totp.verify(
otp,
drift_behind: ENV.fetch('OTP_DRIFT_TIME_SECONDS', DEFAULT_OTP_DRIFT_TIME_SECONDS).to_i
)
2020-06-30 20:16:00 +08:00
end
2020-07-02 17:24:33 +08:00
def assign_2fa_token!
2020-07-01 21:44:52 +08:00
self.otp_secret = ROTP::Base32.random
2020-07-01 17:07:33 +08:00
save!
end
2020-07-01 21:44:52 +08:00
def enable_2fa!
2020-07-09 23:01:00 +08:00
recovery_codes = []
Constants::TWO_FACTOR_RECOVERY_CODE_COUNT.times do
recovery_codes.push(SecureRandom.hex(Constants::TWO_FACTOR_RECOVERY_CODE_LENGTH / 2))
end
2020-07-09 23:01:00 +08:00
update!(
two_factor_auth_enabled: true,
otp_recovery_codes: recovery_codes.map { |c| Devise::Encryptor.digest(self.class, c) }
)
recovery_codes
2020-07-01 20:41:55 +08:00
end
2020-07-01 21:44:52 +08:00
def disable_2fa!
2020-07-09 23:01:00 +08:00
update!(two_factor_auth_enabled: false, otp_secret: nil, otp_recovery_codes: nil)
end
def recover_2fa!(code)
return unless otp_recovery_codes
2020-07-09 23:01:00 +08:00
otp_recovery_codes.each do |recovery_code|
if Devise::Encryptor.compare(self.class, recovery_code, code)
2020-07-13 22:05:23 +08:00
update!(otp_recovery_codes: otp_recovery_codes.reject { |i| i == recovery_code })
2020-07-09 23:01:00 +08:00
return true
end
end
false
end
def after_database_authentication
if Rails.application.config.x.disable_local_passwords
throw(:warden, message: I18n.t('devise.failure.auth_method_disabled'))
end
end
2022-12-07 18:35:25 +08:00
def my_module_visible_table_columns
settings['visible_my_module_table_columns'].presence ||
2022-12-07 18:36:32 +08:00
%w(id due_date age results status archived assigned tags comments)
2022-12-07 18:35:25 +08:00
end
protected
2016-02-12 23:52:43 +08:00
def confirmation_required?
Rails.configuration.x.enable_email_confirmations
end
2016-02-12 23:52:43 +08:00
def time_zone_check
if time_zone.nil? ||
ActiveSupport::TimeZone.new(time_zone).nil?
2016-02-12 23:52:43 +08:00
errors.add(:time_zone)
end
end
2016-08-03 21:31:25 +08:00
private
def downcase_email!
return unless email
self.email = email.downcase
end
def clear_view_cache
Rails.cache.delete_matched(%r{^views\/users\/#{id}-})
end
2016-02-12 23:52:43 +08:00
end