|
- # == Schema Information
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # login :string(255) default(""), not null
- # hashed_password :string(40) default(""), not null
- # firstname :string(30) default(""), not null
- # lastname :string(255) default(""), not null
- # mail :string(60)
- # admin :boolean default("0"), not null
- # status :integer default("1"), not null
- # last_login_on :datetime
- # language :string(5) default("")
- # auth_source_id :integer
- # created_on :datetime
- # updated_on :datetime
- # type :string(255)
- # identity_url :string(255)
- # mail_notification :string(255) default(""), not null
- # salt :string(64)
- # gid :integer
- # visits :integer default("0")
- # excellent_teacher :integer default("0")
- # excellent_student :integer default("0")
- # phone :string(255)
- # authentication :boolean default("0")
- # grade :integer default("0")
- # experience :integer default("0")
- # nickname :string(255)
- # show_realname :boolean default("1")
- # professional_certification :boolean default("0")
- # ID_number :string(255)
- # certification :integer default("0")
- # homepage_teacher :boolean default("0")
- # homepage_engineer :boolean default("0")
- # is_test :integer default("0")
- # ecoder_user_id :integer default("0")
- # business :boolean default("0")
- # profile_completed :boolean default("0")
- # laboratory_id :integer
- # is_shixun_marker :boolean default("0")
- # admin_visitable :boolean default("0")
- # collaborator :boolean default("0")
- # gitea_uid :integer
- # is_sync_pwd :boolean default("1")
- # watchers_count :integer default("0")
- # devops_step :integer default("0")
- # gitea_token :string(255)
- # platform :string(255)
- #
- # Indexes
- #
- # index_users_on_ecoder_user_id (ecoder_user_id)
- # index_users_on_homepage_engineer (homepage_engineer)
- # index_users_on_homepage_teacher (homepage_teacher)
- # index_users_on_laboratory_id (laboratory_id)
- # index_users_on_login (login) UNIQUE
- # index_users_on_mail (mail) UNIQUE
- # index_users_on_phone (phone) UNIQUE
- # index_users_on_type (type)
- #
-
- class User < Owner
- default_scope {where(type: %w(User AnonymousUser))}
- extend Enumerize
-
- include Watchable
- include Likeable
- include BaseModel
- include Droneable
- include User::Avatar
- # include Searchable::Dependents::User
-
- # devops step
- # devops_step column: 0: 未填写服务器信息;1: 已填写服务器信息(未认证);2: 已认证
- DEVOPS_UNINIT = 0
- DEVOPS_UNVERIFIED = 1
- DEVOPS_CERTIFICATION = 2
-
- # Account statuses
- STATUS_ANONYMOUS = 0
- STATUS_ACTIVE = 1
- STATUS_REGISTERED = 2
- STATUS_LOCKED = 3
-
- # tpi tpm权限控制
- EDU_ADMIN = 1 # 超级管理员
- EDU_BUSINESS = 2 # 运营人员
- EDU_SHIXUN_MANAGER = 3 # 实训管理员
- EDU_SHIXUN_MEMBER = 4 # 实训成员
- EDU_CERTIFICATION_TEACHER = 5 # 平台认证的老师
- EDU_GAME_MANAGER = 6 # TPI的创建者
- EDU_TEACHER = 7 # 平台老师,但是未认证
- EDU_NORMAL = 8 # 普通用户
-
- VALID_EMAIL_REGEX = /^[a-zA-Z0-9]+([.\-_\\]*[a-zA-Z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/i
- VALID_PHONE_REGEX = /^1\d{10}$/
- # 身份证
- VALID_NUMBER_REGEX = /(^[1-9]\d{5}(18|19|20|(3\d))\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^([A-Z]\d{6,10}(\(\w{1}\))?)$)/
-
- LOGIN_LENGTH_LIMIT = 30
- MAIL_LENGTH_LMIT = 60
-
- MIX_PASSWORD_LIMIT = 8
-
- LOGIN_CHARS = %W(2 3 4 5 6 7 8 9 a b c f e f g h i j k l m n o p q r s t u v w x y z).freeze
-
- # FIX Invalid single-table inheritance type
- # self.inheritance_column = nil
-
- # educoder: 来自Educoder平台
- # trustie: 来自Trustie平台
- # forge: 平台本身注册的用户
- # military: 军科的用户
- enumerize :platform, in: [:forge, :educoder, :trustie, :military], default: :forge, scope: :shallow
-
- belongs_to :laboratory, optional: true
- has_many :composes, dependent: :destroy
- has_many :compose_users, dependent: :destroy
- has_one :user_extension, dependent: :destroy
- has_many :open_users, dependent: :destroy
- has_one :wechat_open_user, class_name: 'OpenUsers::Wechat'
- has_one :qq_open_user, class_name: 'OpenUsers::QQ'
- accepts_nested_attributes_for :user_extension, update_only: true
- has_many :fork_users, dependent: :destroy
-
- has_many :versions
- has_many :issue_times, :dependent => :destroy
-
- has_one :onclick_time, :dependent => :destroy
-
- # 新版私信
- has_many :private_messages, dependent: :destroy
- has_many :recent_contacts, through: :private_messages, source: :target
- has_many :tidings, :dependent => :destroy
- has_many :journals_for_messages, :as => :jour, :dependent => :destroy
-
- has_many :attachments,foreign_key: :author_id, :dependent => :destroy
-
- has_one :ci_cloud_account, class_name: 'Ci::CloudAccount', dependent: :destroy
-
- # 认证
- has_many :apply_user_authentication
- has_one :process_real_name_apply, -> { processing.real_name_auth.order(created_at: :desc) }, class_name: 'ApplyUserAuthentication'
- has_one :process_professional_apply, -> { processing.professional_auth.order(created_at: :desc) }, class_name: 'ApplyUserAuthentication'
- has_many :apply_actions, dependent: :destroy
- has_many :trail_auth_apply_actions, -> { where(container_type: 'TrialAuthorization') }, class_name: 'ApplyAction'
-
- # has_many :attendances
- has_many :applied_messages, dependent: :destroy
- has_many :operate_applied_messages, class_name: 'AppliedMessage', dependent: :destroy
- # 项目
- has_many :applied_projects, dependent: :destroy
- has_many :operate_applied_transfer_projects, class_name: 'AppliedTransferProject', dependent: :destroy
- has_many :members, dependent: :destroy
- has_many :team_users, dependent: :destroy
- has_many :teams, through: :team_users
-
- # 教学案例
- # has_many :libraries, dependent: :destroy
- has_many :project_trends, dependent: :destroy
- has_many :oauths , dependent: :destroy
-
- has_many :organization_users, dependent: :destroy
- has_many :organizations, through: :organization_users
- has_many :pinned_projects, dependent: :destroy
- has_many :is_pinned_projects, through: :pinned_projects, source: :project
- accepts_nested_attributes_for :is_pinned_projects
- has_many :issues, dependent: :destroy, foreign_key: :author_id
- has_many :pull_requests, dependent: :destroy
-
- # Groups and active users
- scope :active, lambda { where(status: STATUS_ACTIVE) }
- scope :like, lambda { |keywords|
- sql = "CONCAT(lastname, firstname) LIKE :search OR login LIKE :search OR mail LIKE :search OR nickname LIKE :search"
- where(sql, :search => "%#{keywords.split(" ").join('|')}%") unless keywords.blank?
- }
-
- scope :simple_select, -> {select(:id, :login, :lastname,:firstname, :nickname, :gitea_uid, :type)}
-
- attr_accessor :password, :password_confirmation
-
- delegate :gender, :department_id, :school_id, :location, :location_city, :technical_title, to: :user_extension, allow_nil: true
-
- before_save :update_hashed_password
- after_create do
- SyncTrustieJob.perform_later("user", 1) if allow_sync_to_trustie?
- end
-
- #
- # validations
- #
- # validates_presence_of :login, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }, case_sensitive: false
- validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, case_sensitive: false
- validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, case_sensitive: false
- # validates_uniqueness_of :phone, :if => Proc.new { |user| user.phone_changed? && user.phone.present? }, case_sensitive: false
- validates_length_of :login, maximum: LOGIN_LENGTH_LIMIT
- validates_length_of :mail, maximum: MAIL_LENGTH_LMIT
- validate :validate_sensitive_string
- validate :validate_password_length
-
- # 用户参与的所有项目
- def full_member_projects
- normal_projects = Project.members_projects(self.id).to_sql
- org_projects = Project.joins(teams: :team_users).where(team_users: {user_id: self.id}).to_sql
- return Project.from("( #{ normal_projects} UNION #{ org_projects } ) AS projects").distinct
- end
-
- def name
- login
- end
-
- # 删除自动登录的token,一旦退出下次会提示需要登录
- def delete_autologin_token(value)
- Token.where(:user_id => id, :action => 'autologin', :value => value).delete_all
- end
-
- def delete_session_token(value)
- Token.where(:user_id => id, :action => 'session', :value => value).delete_all
- end
-
- def git_mail
- mail.blank? ? "#{login}@educoder.net" : mail
- end
-
- def project_manager?(project)
- project.manager?(self) || self.admin?
- end
-
- # 学号
- def student_id
- self.user_extension.try(:student_id)
- end
-
- # 关注数
- def follow_count
- Watcher.where(user_id: self.id, watchable_type: %w(User)).count
- # User.watched_by(id).count
- end
-
- # 粉丝数
- def fan_count
- Watcher.where(watchable_type: %w(User), watchable_id: self.id).count
- # watchers.count
- end
-
- # 判断当前用户是否为老师
- def is_teacher?
- self.user_extension.teacher?
- end
-
- # 平台认证的老师
- def is_certification_teacher
- self.user_extension.teacher? && self.professional_certification
- end
-
- def certification_teacher?
- professional_certification? && user_extension.teacher?
- end
-
- # 判断用户的身份
- def identity
- ue = self.user_extension
- unless ue.blank?
- if ue.teacher?
- ue.technical_title ? ue.technical_title : "老师"
- elsif ue.student?
- "学生"
- else
- ue.technical_title ? ue.technical_title : "专业人士"
- end
- end
- end
-
- # 实名认证状态
- def auth_status
- status = if authentication
- "已认证"
- elsif process_real_name_apply.present?
- "待审核"
- else
- "未认证"
- end
- end
-
- # 职业认证状态
- def pro_status
- status = if professional_certification
- "已认证"
- elsif process_professional_apply.present?
- "待审核"
- else
- "未认证"
- end
- end
-
- # 判断当前用户是否通过职业认证
- def pro_certification?
- professional_certification
- end
-
- # 学校所在的地区
- def school_province
- user_extension&.school&.province || ''
- end
-
- # 用户的学校名称
- # def school_name
- # user_extension&.school&.name || ''
- # end
-
- # 用户的学院名称
- def department_name
- user_extension&.department&.name || ''
- end
-
- # 课堂的所有身份
- def course_role course
- course.course_members.where(user_id: id).pluck(:role)
- end
-
- # 课堂的老师(创建者、老师、助教)
- def teacher_of_course?(course)
- course.course_members.exists?(user_id: id, role: [1,2,3], is_active: 1) || admin? || business?
- end
-
- # 课堂的老师(创建者、老师、助教),不考虑超管和运营人员
- def none_admin_teacher_of_course?(course)
- course.course_members.exists?(user_id: id, role: [1,2,3], is_active: 1)
- end
-
- # 课堂的老师(创建者、老师、助教),不用考虑当前身份
- def teacher_of_course_non_active?(course)
- course.course_members.exists?(user_id: id, role: [1,2,3])
- end
-
- # 是否是教师,课堂管理员或者超级管理员
- def teacher_or_admin?(course)
- course.course_members.exists?(user_id: id, role: [1,2], is_active: 1) || admin? || business?
- end
-
- # 课堂的创建者(考虑到多重身份的用户)
- def creator_of_course?(course)
- course.course_members.exists?(user_id: id, role: 1, is_active: 1) || admin? || business?
- end
-
- # 课堂的学生
- def student_of_course?(course)
- course.course_members.exists?(user_id: id, role: %i[STUDENT], is_active: 1)
- end
-
- # 课堂成员
- def member_of_course?(course)
- course&.course_members.exists?(user_id: id)
- end
-
- # 实训路径管理员
- def creator_of_subject?(subject)
- subject.user_id == id || admin?
- end
-
- # 实训路径:合作者、admin
- def manager_of_subject?(subject)
- subject.subject_members.exists?(user_id: id, role: [1,2]) || admin? || business?
- end
-
- # 实训管理员:实训合作者、admin
- def manager_of_shixun?(shixun)
- logger.info("############id: #{id}")
- shixun.shixun_members.exists?(role: [1,2], user_id: id) || admin? || business?
- end
-
- # 实训管理员
- def creator_of_shixun?(shixun)
- id == shixun.user_id
- end
-
- # 实训的合作者
- def member_of_shixun?(shixun)
- #self.shixun_members.where(:role => 2, :shixun_id => shixun.id).present?
- shixun.shixun_members.exists?(role: 2, user_id: id)
- end
-
- # TPI的创建者
- def creator_of_game?(game)
- id == game.user_id
- end
-
- # 用户账号状态
- def active?
- status == STATUS_ACTIVE
- end
-
- def registered?
- status == STATUS_REGISTERED
- end
-
- def locked?
- status == STATUS_LOCKED
- end
-
- def activate
- self.status = STATUS_ACTIVE
- end
-
- def register
- self.status = STATUS_REGISTERED
- end
-
- def lock
- self.status = STATUS_LOCKED
- end
-
- def activate!
- update_attribute(:status, STATUS_ACTIVE)
- end
-
- def register!
- update_attribute(:status, STATUS_REGISTERED)
- end
-
- def lock!
- update_attribute(:status, STATUS_LOCKED)
- end
-
- # 课程用户身份
- def course_identity(course)
- if !logged?
- Course::Anonymous
- elsif admin?
- Course::ADMIN
- elsif business?
- Course::BUSINESS
- else
- role = course&.course_members&.find_by(user_id: id, is_active: 1)&.role
- case role
- when nil then Course::NORMAL
- when 'CREATOR' then Course::CREATOR
- when 'PROFESSOR' then Course::PROFESSOR
- when 'STUDENT' then Course::STUDENT
- when 'ASSISTANT_PROFESSOR' then Course::ASSISTANT_PROFESSOR
- end
- end
- end
-
- # 实训用户身份
- def shixun_identity(shixun)
- @identity =
- if admin?
- User::EDU_ADMIN
- elsif business?
- User::EDU_BUSINESS
- elsif creator_of_shixun?(shixun)
- User::EDU_SHIXUN_MANAGER
- elsif member_of_shixun?(shixun)
- User::EDU_SHIXUN_MEMBER
- elsif is_certification_teacher
- User::EDU_CERTIFICATION_TEACHER
- elsif is_teacher?
- User::EDU_TEACHER
- else
- User::EDU_NORMAL
- end
- return @identity
- end
-
- # tpi的用户身份
- def game_identity(game)
- shixun = game.myshixun.shixun
- @identity =
- if admin?
- User::EDU_ADMIN
- elsif business?
- User::EDU_BUSINESS
- elsif creator_of_shixun?(shixun)
- User::EDU_SHIXUN_MANAGER
- elsif member_of_shixun?(shixun)
- User::EDU_SHIXUN_MEMBER
- elsif is_certification_teacher
- User::EDU_CERTIFICATION_TEACHER
- elsif creator_of_game?(game)
- User::EDU_GAME_MANAGER
- elsif is_teacher?
- User::EDU_TEACHER
- else
- User::EDU_NORMAL
- end
- return @identity
- end
-
- # 我的实训
- def my_shixuns
- shixun_ids = shixun_members.pluck(:shixun_id) + myshixuns.pluck(:shixun_id)
- Shixun.where(:id => shixun_ids).visible
- end
-
- # 用户是否有权限查看实训
- # 1、实训删除只有管理员能看到
- # 2、实训隐藏了只有管理员、实训合作者能看到
- # 3、如果有限制学校范围,则学校的用户、管理员、实训合作者能看到
- def shixun_permission(shixun)
- case shixun.status
- when -1 # 软删除只有管理员能访问
- admin?
- when 0, 1, 3 # 申请发布或者已关闭的实训,只有实训管理员可以访问
- manager_of_shixun?(shixun)
- when 2
- if shixun.hidden
- manager_of_shixun?(shixun)
- else
- shixun.use_scope == 0 || manager_of_shixun?(shixun) || shixun.shixun_schools.exists?(school_id: school_id)
- end
- end
- end
-
- # 用户在平台名称的显示方式
- def full_name
- return '游客' unless logged?
-
- name = show_realname? ? lastname + firstname : nickname
- name.blank? ? (nickname.blank? ? login : nickname) : name
- end
-
- # 用户的真实姓名(不考虑用户是否隐藏了真实姓名,课堂模块都用真实姓名)
- def real_name
- return '游客' unless logged?
- name = lastname + firstname
- name = name.blank? ? (nickname.blank? ? login : nickname) : name
- name.gsub(/\s+/, '').strip #6.11 -hs
- end
-
- def only_real_name
- "#{lastname}#{firstname}"
- end
-
- # 用户是否选题毕设课题
- def selected_topic?(topic)
- student_graduation_topics.where(graduation_topic_id: topic.id).last.try(:status)
- end
-
- def click_time
- click_time = OnclickTime.find_by(user_id: id) || OnclickTime.create(user_id: id, onclick_time: created_on)
- click_time.onclick_time
- end
-
- def manager_of_memo?(memo)
- id == memo.author_id || admin? || business?
- end
-
- # 是否是项目管理者
- def manager_of_project?(project)
- project.project_members.where(user_id: id).count > 0
- end
-
- def logged?
- true
- end
-
- def active?
- status == STATUS_ACTIVE
- end
-
- def locked?
- status == STATUS_LOCKED
- end
-
- def phone_binded?
- phone.present?
- end
-
- def email_binded?
- mail.present?
- end
-
- # def self.current=(user)
- # Thread.current[:current_user] = user
- # end
- #
- # def self.current
- # Thread.current[:current_user] ||= User.anonymous
- # end
-
- def self.current=(user)
- RequestStore.store[:current_user] = user
- end
-
- def self.current
- RequestStore.store[:current_user] ||= User.anonymous
- end
-
- def self.anonymous
- anonymous_user = AnonymousUser.unscoped.take
- if anonymous_user.nil?
- anonymous_user = AnonymousUser.unscoped.create(lastname: 'Anonymous', firstname: '', login: '', mail: '358551897@qq.com', phone: '13333333333', status: 0, platform: User.platform.forge)
- raise "Unable to create the anonymous user: error_info:#{anonymous_user.errors.messages}" if anonymous_user.new_record?
- end
- anonymous_user
- end
-
- # Returns the user who matches the given autologin +key+ or nil
- def self.try_to_autologin(key)
- user = Token.find_active_user('autologin', key)
- user.update(last_login_on: Time.now) if user
- user
- end
-
- def self.hash_password(clear_password)
- Digest::SHA1.hexdigest(clear_password || "")
- end
-
- def check_password?(clear_password)
- # Preventing Timing Attack
- ActiveSupport::SecurityUtils.secure_compare(
- User.hash_password("#{salt}#{User.hash_password clear_password}"),
- hashed_password
- )
- end
-
- # 工程认证的学校
- def ec_school
- school_id = self.ec_school_users.pluck(:school_id).first ||
- self.ec_major_schools.pluck(:school_id).first ||
- (self.ec_course_users.first && self.ec_course_users.first.try(:ec_course).try(:ec_year).try(:ec_major_school).try(:school_id))
- end
-
- # 登录,返回用户名与密码匹配的用户
- def self.try_to_login(login, password)
- login = login.to_s.strip
- password = password.to_s
-
- # Make sure no one can sign in with an empty login or password
- return nil if login.empty? || password.empty?
- if (login =~ VALID_EMAIL_REGEX)
- user = find_by_mail(login)
- elsif (login =~ VALID_PHONE_REGEX)
- user = find_by_phone(login)
- else
- user = find_by_login(login)
- end
-
- user
- rescue => text
- raise text
- end
-
- def show_real_name
- name = lastname + firstname
- if name.blank?
- nickname.blank? ? login : nickname
- else
- name
- end
- end
-
- def update_hashed_password
- if password
- salt_password(password)
- end
- end
-
- def salt_password(clear_password)
- self.salt = User.generate_salt
- self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
- end
-
- def self.generate_salt
- Educoder::Utils.random_hex(16)
- end
-
- # 全部已认证
- def all_certified?
- authentication? && professional_certification?
- end
-
- # 是否绑定邮箱
- def email_binded?
- mail.present?
- end
-
- # 手机号:123***123
- def hidden_phone
- Util.conceal(phone, :phone).to_s
- end
-
- # 邮箱:w***l@qq.com
- def hidden_mail
- Util.conceal(mail, :email).to_s
- end
-
- # 学院的url标识
- # def college_identifier
- # Department.find_by_id(department_members.pluck(:department_id).first)&.identifier
- # end
-
- # 是否能申请试用
- def can_apply_trial?
- return false if certification == 1
-
- apply = ApplyAction.order(created_at: :desc).find_by(user_id: id, container_type: 'TrialAuthorization')
-
- apply.present? && !apply.status.zero?
- end
-
- def projects_count
- Project.includes(:members).joins(:members).where(members: { user_id: self.id }).select(:id).size
- end
-
- # 是否已经签到
- def attendance_signed?
- attendance = Attendance.find_by(user_id: id)
-
- attendance.present? && Util.days_between(Time.zone.now, attendance.created_at).zero?
- end
-
- # 明日签到金币
- def tomorrow_attendance_gold
- Attendance.find_by(user_id: id)&.next_gold || 60 # 基础50,连续签到+10
- end
-
- def admin_or_business?
- admin? || business?
- end
-
- def self.generate_login(prefix)
- login = prefix + LOGIN_CHARS.sample(8).join('')
- while User.exists?(login: login)
- login = prefix + LOGIN_CHARS.sample(8).join('')
- end
-
- login
- end
-
- def bind_open_user?(type)
- case type
- when 'wechat' then wechat_open_user.present?
- when 'qq' then qq_open_user.present?
- else false
- end
- end
-
- def reset_login_times!
- LimitForbidControl::UserLogin.new(self).clear
- end
-
- def from_sub_site?
- laboratory_id.present? && laboratory_id != 1
- end
-
- protected
- def validate_password_length
- # 管理员的初始密码是5位
- if password.present? && password.size < MIX_PASSWORD_LIMIT && !User.current.admin?
- raise("密码长度不能低于#{MIX_PASSWORD_LIMIT}位")
- end
-
- if password.present? && password.size > 16
- raise('密码长度不能超过16位')
- end
- end
-
- def validate_sensitive_string
- raise("真实姓名包含敏感词汇,请重新输入") if lastname && !HarmoniousDictionary.clean?(lastname)
- raise("昵称包含敏感词汇,请重新输入") if nickname && !HarmoniousDictionary.clean?(nickname)
- end
-
- def set_laboratory
- return unless new_record?
-
- self.laboratory = Laboratory.current if laboratory_id.blank?
- end
- end
-
-
- class AnonymousUser < User
- validate :validate_anonymous_uniqueness, :on => :create
-
- def validate_anonymous_uniqueness
- # There should be only one AnonymousUser in the database
- errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
- end
-
- def available_custom_fields
- []
- end
-
- # Overrides a few properties
- def logged?; false end
- def admin; false end
- def name(*args); I18n.t(:label_user_anonymous) end
- # def mail=(*args); nil end
- # def mail; nil end
- def time_zone; nil end
- def rss_key; nil end
-
-
- def membership(*args)
- nil
- end
-
- def member_of?(*args)
- false
- end
-
- # Anonymous user can not be destroyed
- def destroy
- false
- end
-
- protected
-
- def instantiate_email_address
- end
-
- end
|