diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index 6572e079d..5c3503718 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -64,7 +64,7 @@ class AttachmentsController < ApplicationController # modify by nwb # 下载添加权限设置 candown = false - if @attachment.container.class.to_s != "HomeworkAttach" &&(@attachment.container.has_attribute?(:project) || @attachment.container.has_attribute?(:project_id)) && @attachment.container.project + if (@attachment.container.has_attribute?(:project) || @attachment.container.has_attribute?(:project_id)) && @attachment.container.project project = @attachment.container.project candown= User.current.member_of?(project) || (project.is_public && @attachment.is_public == 1) elsif @attachment.container.is_a?(Project) @@ -89,6 +89,7 @@ class AttachmentsController < ApplicationController elsif @attachment.container_type == "Bid" && @attachment.container && @attachment.container.courses candown = User.current.member_of_course?(@attachment.container.courses.first) || (course.is_public == 1 && @attachment.is_public == 1) else + candown = @attachment.is_public == 1 end if candown || User.current.admin? || User.current.id == @attachment.author_id diff --git a/app/models/user.rb b/app/models/user.rb index 13b8c3a3a..29709519d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,1015 +1,1015 @@ -# Redmine - project management software -# Copyright (C) 2006-2013 Jean-Philippe Lang -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -require "digest/sha1" - -class User < Principal - TEACHER = 0 - STUDENT = 1 - ENTERPRISE = 2 - DEVELOPER = 3 - - include Redmine::SafeAttributes - - # Different ways of displaying/sorting users - USER_FORMATS = { - :firstname_lastname => { - :string => '#{firstname} #{lastname}', - :order => %w(firstname lastname id), - :setting_order => 1 - }, - :firstname_lastinitial => { - :string => '#{firstname} #{lastname.to_s.chars.first}.', - :order => %w(firstname lastname id), - :setting_order => 2 - }, - :firstname => { - :string => '#{firstname}', - :order => %w(firstname id), - :setting_order => 3 - }, - :lastname_firstname => { - :string => '#{lastname} #{firstname}', - :order => %w(lastname firstname id), - :setting_order => 4 - }, - :lastname_coma_firstname => { - :string => '#{lastname}, #{firstname}', - :order => %w(lastname firstname id), - :setting_order => 5 - }, - :lastname => { - :string => '#{lastname}', - :order => %w(lastname id), - :setting_order => 6 - }, - :username => { - :string => '#{login}', - :order => %w(login id), - :setting_order => 7 - }, - } - - MAIL_NOTIFICATION_OPTIONS = [ - ['all', :label_user_mail_option_all], - ['selected', :label_user_mail_option_selected], - ['only_my_events', :label_user_mail_option_only_my_events], - ['only_assigned', :label_user_mail_option_only_assigned], - ['only_owner', :label_user_mail_option_only_owner], - ['none', :label_user_mail_option_none] - ] - - has_many :homework_users - has_many :homework_attaches, :through => :homework_users - has_many :homework_evaluations - - has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)}, - :after_remove => Proc.new {|user, group| group.user_removed(user)} - has_many :changesets, :dependent => :nullify - has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' - has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'" - has_one :api_token, :class_name => 'Token', :conditions => "action='api'" - belongs_to :auth_source - belongs_to :ucourse, :class_name => 'Course', :foreign_key => :id #huang -## added by xianbo for delete - has_many :biding_projects, :dependent => :destroy - has_many :contesting_projects, :dependent => :destroy - belongs_to :softapplication, :foreign_key => 'id', :dependent => :destroy -##ended by xianbo - -#####fq - has_many :jours, :class_name => 'JournalsForMessage', :dependent => :destroy - has_many :journals_messages, :class_name => 'JournalsForMessage', :foreign_key => "user_id", :dependent => :destroy - has_many :bids, :foreign_key => 'author_id', :dependent => :destroy - has_many :contests, :foreign_key => 'author_id', :dependent => :destroy - has_many :softapplications, :foreign_key => 'user_id', :dependent => :destroy - has_many :journals_for_messages, :as => :jour, :dependent => :destroy - has_many :new_jours, :as => :jour, :class_name => 'JournalsForMessage', :conditions => "status=1" - has_many :journal_replies, :dependent => :destroy - has_many :activities, :dependent => :destroy - has_many :students_for_courses - #has_many :courses, :through => :students_for_courses, :source => :project - has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy - has_many :file_commit, :class_name => 'Attachment', :foreign_key => 'author_id', :conditions => "container_type = 'Project' or container_type = 'Version'" -#### -# added by bai - has_many :join_in_contests, :dependent => :destroy - has_many :news, :foreign_key => 'author_id' - has_many :contestnotification, :foreign_key => 'author_id' - has_many :comments, :foreign_key => 'author_id' - has_many :notificationcomments, :foreign_key => 'author_id' - has_many :wiki_contents, :foreign_key => 'author_id' - has_many :journals - has_many :messages, :foreign_key => 'author_id' - has_one :user_score, :dependent => :destroy - has_many :documents # 项目中关联的文档再次与人关联 -# end - -######added by nie - has_many :project_infos, :dependent => :destroy - has_one :user_status, :dependent => :destroy - ##### - has_many :shares ,:dependent => :destroy - - # add by zjc - has_one :level, :class_name => 'UserLevels', :dependent => :destroy - has_many :memos , :foreign_key => 'author_id' - ##### - scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") } - scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) } - scope :visible, lambda {|*args| - nil - } - - - acts_as_customizable - ############################added by william - acts_as_taggable - scope :by_join_date, order("created_on DESC") - ############################# added by liuping 关注 - acts_as_watchable - seems_rateable_rater - has_one :user_extensions,:dependent => :destroy - ## end - - # default_scope -> { includes(:user_extensions, :user_score) } - scope :teacher, -> { - joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::TEACHER) - } - scope :student, -> { - joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::STUDENT) - } - scope :developer, -> { - joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::DEVELOPER) - } - scope :enterprise, -> { - joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::ENTERPRISE) - } - - attr_accessor :password, :password_confirmation - attr_accessor :last_before_login_on - # Prevents unauthorized assignments - attr_protected :login, :admin, :password, :password_confirmation, :hashed_password - - LOGIN_LENGTH_LIMIT = 25 - MAIL_LENGTH_LIMIT = 60 - - validates_presence_of :login, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } - 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 - # Login must contain letters, numbers, underscores only - validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i - validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT - validates_length_of :firstname, :maximum => 30 - validates_length_of :lastname, :maximum => 30 - validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true - validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true - validates_confirmation_of :password, :allow_nil => true - validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true - validate :validate_password_length - - before_create :set_mail_notification - before_save :update_hashed_password - before_destroy :remove_references_before_destroy - # added by fq - after_create :act_as_activity - # end - - scope :in_group, lambda {|group| - group_id = group.is_a?(Group) ? group.id : group.to_i - where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id) - } - scope :not_in_group, lambda {|group| - group_id = group.is_a?(Group) ? group.id : group.to_i - where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id) - } - scope :sorted, lambda { order(*User.fields_for_order_statement)} - - scope :like, lambda {|arg, type| - if arg.blank? - where(nil) - else - pattern = "%#{arg.to_s.strip.downcase}%" - #where(" LOWER(concat(lastname, firstname)) LIKE :p ", :p => pattern) - if type == "0" - where(" LOWER(login) LIKE :p ", :p => pattern) - elsif type == "1" - where(" LOWER(concat(lastname, firstname)) LIKE :p ", :p => pattern) - else - where(" LOWER(mail) LIKE :p ", :p => pattern) - end - end - } - - - # ====================================================================== - - def extensions - self.user_extensions ||= UserExtensions.new - end - - def user_score_attr - self.user_score ||= UserScore.new - end - - # ====================================================================== - - #选择项目成员时显示的用户信息文字 - def userInfo - if self.realname.gsub(' ','') == "" || self.realname.nil? - info = self.nickname; - else - info=self.nickname + ' (' + self.realname + ')'; - end - info - end - - ###添加留言 fq - def add_jour(user, notes, reference_user_id = 0, options = {}) - if options.count == 0 - self.journals_for_messages << JournalsForMessage.new(:user_id => user.id, :notes => notes, :reply_id => reference_user_id, :status => true) - else - jfm = self.journals_for_messages.build(options) - jfm.save - jfm - end - end - - # 判断用户是否加入了竞赛中 fq - def join_in_contest?(bid) - joined = JoinInContest.where('user_id = ? and bid_id =?', self.id, bid.id) - if joined.size > 0 - true - else - false - end - end - - ### fq - def join_in?(course) - joined = StudentsForCourse.where('student_id = ? and course_id = ?', self.id, course.id) - if joined.size > 0 - true - else - false - end - end - - def show_name - unless self.user_extensions.nil? - if self.user_extensions.identity == 2 - firstname - else - lastname+firstname - end - else - lastname+firstname - end - end - ## end - - def count_new_jour - count = self.new_jours.count - end - - #added by nie - def count_new_journal_reply - count = self.journal_reply.count - end - - def set_mail_notification - ##add byxianbo - thread=Thread.new do - self.mail_notification = Setting.default_notification_option if self.mail_notification.blank? - true - end - end - - def update_hashed_password - # update hashed_password if password was set - if self.password && self.auth_source_id.blank? - salt_password(password) - end - end - - alias :base_reload :reload - def reload(*args) - @name = nil - @projects_by_role = nil - @courses_by_role = nil - @membership_by_project_id = nil - base_reload(*args) - end - - def mail=(arg) - write_attribute(:mail, arg.to_s.strip) - end - - def identity_url=(url) - if url.blank? - write_attribute(:identity_url, '') - else - begin - write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url)) - rescue OpenIdAuthentication::InvalidOpenId - # Invalid url, don't save - end - end - self.read_attribute(:identity_url) - end - - VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i - # VALID_EMAIL_REGEX = /^[0-9a-zA-Z_-]+@[0-9a-zA-Z_-]+(\.[0-9a-zA-Z_-]+)+$/ - # Returns the user that matches provided login and password, or nil - #登录,返回用户名与密码匹配的用户 - def self.try_to_login(login, password) - login = login.to_s.lstrip.rstrip - 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) - else - user = find_by_login(login) - end - if user - # user is already in local database - #return nil unless user.active? - return nil unless user.check_password?(password) - else - # user is not yet registered, try to authenticate with available sources - attrs = AuthSource.authenticate(login, password) - if attrs - user = new(attrs) - user.login = login - user.language = Setting.default_language - if user.save - user.reload - logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source - end - end - end - unless user.nil? - last_login_on = user.last_login_on.nil? ? '' : user.last_login_on.to_s - end - user.update_column(:last_login_on, Time.now) if user && !user.new_record? - [user, last_login_on] - rescue => text - raise text - 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, Setting.autologin.to_i) - if user - user.update_column(:last_login_on, Time.now) - user - end - end - - def self.name_formatter(formatter = nil) - USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname] - end - - # Returns an array of fields names than can be used to make an order statement for users - # according to how user names are displayed - # Examples: - # - # User.fields_for_order_statement => ['users.login', 'users.id'] - # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id'] - def self.fields_for_order_statement(table=nil) - table ||= table_name - name_formatter[:order].map {|field| "#{table}.#{field}"} - end - - # Return user's full name for display - def realname(formatter = nil) - f = self.class.name_formatter(formatter) - if formatter - eval('"' + f[:string] + '"') - else - @name ||= eval('"' + f[:string] + '"') - end - end - - def nickname(formatter = nil) - login - end - - def name(formatter = nil) - login - end - - def active? - self.status == STATUS_ACTIVE - end - - def registered? - self.status == STATUS_REGISTERED - end - - def locked? - self.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 - - # Returns true if +clear_password+ is the correct user's password, otherwise false - def check_password?(clear_password) - if auth_source_id.present? - auth_source.authenticate(self.login, clear_password) - else - User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password - end - end - - # Generates a random salt and computes hashed_password for +clear_password+ - # The hashed password is stored in the following form: SHA1(salt + SHA1(password)) - def salt_password(clear_password) - self.salt = User.generate_salt - self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}") - end - - # Does the backend storage allow this user to change their password? - def change_password_allowed? - return true if auth_source.nil? - return auth_source.allow_password_changes? - end - - # Generate and set a random password. Useful for automated user creation - # Based on Token#generate_token_value - # - def random_password - chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a - password = '' - 40.times { |i| password << chars[rand(chars.size-1)] } - self.password = password - self.password_confirmation = password - self - end - - def pref - self.preference ||= UserPreference.new(:user => self) - end - - def time_zone - @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone]) - end - - def wants_comments_in_reverse_order? - self.pref[:comments_sorting] == 'desc' - end - - def wants_notificationcomments_in_reverse_order? - self.pref[:notificationcomments_sorting] == 'desc' - end - # Return user's RSS key (a 40 chars long string), used to access feeds - def rss_key - if rss_token.nil? - create_rss_token(:action => 'feeds') - end - rss_token.value - end - - # Return user's API key (a 40 chars long string), used to access the API - def api_key - if api_token.nil? - create_api_token(:action => 'api') - end - api_token.value - end - - # Return an array of project ids for which the user has explicitly turned mail notifications on - def notified_projects_ids - @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id) - end - - def notified_project_ids=(ids) - Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id]) - Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? - @notified_projects_ids = nil - notified_projects_ids - end - - def valid_notification_options - self.class.valid_notification_options(self) - end - - # Only users that belong to more than 1 project can select projects for which they are notified - def self.valid_notification_options(user=nil) - # Note that @user.membership.size would fail since AR ignores - # :include association option when doing a count - if user.nil? || user.memberships.length < 1 - MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'} - else - MAIL_NOTIFICATION_OPTIONS - end - end - - # Find a user account by matching the exact login and then a case-insensitive - # version. Exact matches will be given priority. - #通过用户名查找相应的用户,若没有匹配到,则不区分大小写进行查询 - #修改:不再匹配不区分大小写情况 -zjc - def self.find_by_login(login) - if login.present? - login = login.to_s - # First look for an exact match - user = where(:login => login).all.detect {|u| u.login == login} - #unless user - # # Fail over to case-insensitive if none was found - # user = where("LOWER(login) = ?", login.downcase).first - #end - user - end - end - - def self.find_by_rss_key(key) - Token.find_active_user('feeds', key) - end - - def self.find_by_api_key(key) - Token.find_active_user('api', key) - end - - # Makes find_by_mail case-insensitive - def self.find_by_mail(mail) - where("LOWER(mail) = ?", mail.to_s.downcase).first - end - - # Returns true if the default admin account can no longer be used - def self.default_admin_account_changed? - !User.active.find_by_login("admin").try(:check_password?, "admin") - end - - def to_s - name - end - - CSS_CLASS_BY_STATUS = { - STATUS_ANONYMOUS => 'anon', - STATUS_ACTIVE => 'active', - STATUS_REGISTERED => 'registered', - STATUS_LOCKED => 'locked' - } - - def css_classes - "user #{CSS_CLASS_BY_STATUS[status]}" - end - - # Returns the current day according to user's time zone - def today - if time_zone.nil? - Date.today - else - Time.now.in_time_zone(time_zone).to_date - end - end - - # Returns the day of +time+ according to user's time zone - def time_to_date(time) - if time_zone.nil? - time.to_date - else - time.in_time_zone(time_zone).to_date - end - end - - def logged? - true - end - - def anonymous? - !logged? - end - - # Returns user's membership for the given project - # or nil if the user is not a member of project - def membership(project) - project_id = project.is_a?(Project) ? project.id : project - - @membership_by_project_id ||= Hash.new {|h, project_id| - h[project_id] = memberships.where(:project_id => project_id).first - } - @membership_by_project_id[project_id] - end - - def coursemembership(course) - course_id = course.is_a?(Course) ? course.id : course - - @membership_by_course_id ||= Hash.new {|h, course_id| - h[course_id] = coursememberships.where(:course_id => course_id).first - } - @membership_by_course_id[course_id] - end - - # Return user's roles for project - def roles_for_project(project) - roles = [] - # No role on archived projects - return roles if project.nil? || project.archived? - if logged? - # Find project membership - membership = membership(project) - if membership - roles = membership.roles - else - @role_non_member ||= Role.non_member - roles << @role_non_member - end - else - @role_anonymous ||= Role.anonymous - roles << @role_anonymous - end - roles - end - - # 用户课程权限判断 - def roles_for_course(course) - roles = [] - # No role on archived courses - return roles if course.nil? || course.archived? - if logged? - # Find course membership - membership = coursemembership(course) - if membership - roles = membership.roles - else - @role_non_member ||= Role.non_member - roles << @role_non_member - end - else - @role_anonymous ||= Role.anonymous - roles << @role_anonymous - end - roles - end - - # Return true if the user is a member of project - def member_of?(project) - projects.to_a.include?(project) - end - - def member_of_course?(course) - courses.to_a.include?(course) - end - - # Returns a hash of user's projects grouped by roles - def projects_by_role - return @projects_by_role if @projects_by_role - - @projects_by_role = Hash.new([]) - memberships.each do |membership| - if membership.project - membership.roles.each do |role| - @projects_by_role[role] = [] unless @projects_by_role.key?(role) - @projects_by_role[role] << membership.project - end - end - end - @projects_by_role.each do |role, projects| - projects.uniq! - end - - @projects_by_role - end - - # 课程的角色权限 - def courses_by_role - return @courses_by_role if @courses_by_role - - @courses_by_role = Hash.new([]) - coursememberships.each do |membership| - if membership.course - membership.roles.each do |role| - @courses_by_role[role] = [] unless @courses_by_role.key?(role) - @courses_by_role[role] << membership.course - end - end - end - @courses_by_role.each do |role, courses| - courses.uniq! - end - - @courses_by_role - end - # Returns true if user is arg or belongs to arg - def is_or_belongs_to?(arg) - if arg.is_a?(User) - self == arg - elsif arg.is_a?(Group) - arg.users.include?(self) - else - false - end - end - - - # Return true if the user is allowed to do the specified action on a specific context - # Action can be: - # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') - # * a permission Symbol (eg. :edit_project) - # Context can be: - # * a project : returns true if user is allowed to do the specified action on this project - # * an array of projects : returns true if user is allowed on every project - # * nil with options[:global] set : check if user has at least one role allowed for this action, - # or falls back to Non Member / Anonymous permissions depending if the user is logged - def allowed_to?(action, context, options={}, &block) - if context && context.is_a?(Project) - return false unless context.allows_to?(action) - # Admin users are authorized for anything else - return true if admin? - - roles = roles_for_project(context) - return false unless roles - roles.any? {|role| - (context.is_public? || role.member?) && - role.allowed_to?(action) && - (block_given? ? yield(role, self) : true) - } - #添加课程相关的权限判断 - elsif context && context.is_a?(Course) - return false unless context.allows_to?(action) - # Admin users are authorized for anything else - return true if admin? - - roles = roles_for_course(context) - return false unless roles - roles.any? {|role| - (context.is_public? || role.member?) && - role.allowed_to?(action) && - (block_given? ? yield(role, self) : true) - } - elsif context && context.is_a?(Array) - if context.empty? - false - else - # Authorize if user is authorized on every element of the array - context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&) - end - elsif options[:global] - # Admin users are always authorized - return true if admin? - - # authorize if user has at least one role that has this permission - roles = memberships.collect {|m| m.roles}.flatten.uniq - if roles.count == 0 - roles = coursememberships.collect {|m| m.roles}.flatten.uniq - end - roles << (self.logged? ? Role.non_member : Role.anonymous) - roles.any? {|role| - role.allowed_to?(action) && - (block_given? ? yield(role, self) : true) - } - else - if admin? - return true - end - #无项目时 查看Non member(id为1)角色是否有权限执行action - Role.find('1').allowed_to?(action) - # false - end - end - - # Is the user allowed to do the specified action on any project? - # See allowed_to? for the actions and valid options. - def allowed_to_globally?(action, options, &block) - allowed_to?(action, nil, options.reverse_merge(:global => true), &block) - end - - # Returns true if the user is allowed to delete his own account - def own_account_deletable? - Setting.unsubscribe? && - (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?) - end - - safe_attributes 'login', - 'firstname', - 'lastname', - 'mail', - 'mail_notification', - 'language', - 'custom_field_values', - 'custom_fields', - 'identity_url' - - safe_attributes 'status', - 'auth_source_id', - :if => lambda {|user, current_user| current_user.admin?} - - safe_attributes 'group_ids', - :if => lambda {|user, current_user| current_user.admin? && !user.new_record?} - - # Utility method to help check if a user should be notified about an - # event. - # - # TODO: only supports Issue events currently - def notify_about?(object) - if mail_notification == 'all' - true - elsif mail_notification.blank? || mail_notification == 'none' - false - else - case object - when Issue - case mail_notification - when 'selected', 'only_my_events' - # user receives notifications for created/assigned issues on unselected projects - object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was) - when 'only_assigned' - is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was) - when 'only_owner' - object.author == self - end - when News - # always send to project members except when mail_notification is set to 'none' - true - #判定用户是否接受留言提醒邮件 - when JournalsForMessage - ##如果是直接留言并且留言对象是Project并且Project类型是课程(课程留言) - if !object.at_user && object.jour.class.to_s.to_sym == :Project && object.jour.project_type == 1 - #根据用户设置邮件接收模式判定当前用户是否接受邮件提醒 - is_notified_project object.jour - end - - end - end - end - - #用户是否接收project的消息提醒 - def is_notified_project arg - if arg.is_a?(Project) - case mail_notification - when 'selected' - notified_projects_ids.include?(arg.id) - when 'only_my_events' - projects.include?(arg) - when 'only_assigned' - false - when 'only_owner' - course = Course.find_by_extra(arg.identifier) - course.teacher == self - end - #勾选的项目或用户的项目 TODO:需改 - #notified_projects_ids.include?(arg) || projects.include?(arg) - else - false - end - end - - def self.current=(user) - Thread.current[:current_user] = user - end - - def self.current - Thread.current[:current_user] ||= User.anonymous - end - - # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only - # one anonymous user per database. - def self.anonymous - anonymous_user = AnonymousUser.first - if anonymous_user.nil? - anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) - raise 'Unable to create the anonymous user.' if anonymous_user.new_record? - end - anonymous_user - end - - # Salts all existing unsalted passwords - # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password)) - # This method is used in the SaltPasswords migration and is to be kept as is - def self.salt_unsalted_passwords! - transaction do - User.where("salt IS NULL OR salt = ''").find_each do |user| - next if user.hashed_password.blank? - salt = User.generate_salt - hashed_password = User.hash_password("#{salt}#{user.hashed_password}") - User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password) - end - end - end - - protected - - def validate_password_length - # Password length validation based on setting - if !password.nil? && password.size < Setting.password_min_length.to_i - errors.add(:password, :too_short, :count => Setting.password_min_length.to_i) - end - end - private - - def act_as_activity - self.acts << Activity.new(:user_id => self.id) - end - - # Removes references that are not handled by associations - # Things that are not deleted are reassociated with the anonymous user - def remove_references_before_destroy - return if self.id.nil? - - substitute = User.anonymous - Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - Notificationcomment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id] - Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] - JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s] - JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s] - Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - # Remove private queries and keep public ones - ::Query.delete_all ['user_id = ? AND is_public = ?', id, false] - ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] - TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] - Token.delete_all ['user_id = ?', id] - Watcher.delete_all ['user_id = ?', id] - WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] - end - - # Return password digest - def self.hash_password(clear_password) - Digest::SHA1.hexdigest(clear_password || "") - end - - # Returns a 128bits random salt as a hex string (32 chars long) - def self.generate_salt - Redmine::Utils.random_hex(16) - 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; nil end - def time_zone; nil end - def rss_key; nil end - - def pref - UserPreference.new(:user => self) - end - - # def member_of?(project) - # false - # end - - # Anonymous user can not be destroyed - def destroy - false - end -end +# Redmine - project management software +# Copyright (C) 2006-2013 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require "digest/sha1" + +class User < Principal + TEACHER = 0 + STUDENT = 1 + ENTERPRISE = 2 + DEVELOPER = 3 + + include Redmine::SafeAttributes + + # Different ways of displaying/sorting users + USER_FORMATS = { + :firstname_lastname => { + :string => '#{firstname} #{lastname}', + :order => %w(firstname lastname id), + :setting_order => 1 + }, + :firstname_lastinitial => { + :string => '#{firstname} #{lastname.to_s.chars.first}.', + :order => %w(firstname lastname id), + :setting_order => 2 + }, + :firstname => { + :string => '#{firstname}', + :order => %w(firstname id), + :setting_order => 3 + }, + :lastname_firstname => { + :string => '#{lastname} #{firstname}', + :order => %w(lastname firstname id), + :setting_order => 4 + }, + :lastname_coma_firstname => { + :string => '#{lastname}, #{firstname}', + :order => %w(lastname firstname id), + :setting_order => 5 + }, + :lastname => { + :string => '#{lastname}', + :order => %w(lastname id), + :setting_order => 6 + }, + :username => { + :string => '#{login}', + :order => %w(login id), + :setting_order => 7 + }, + } + + MAIL_NOTIFICATION_OPTIONS = [ + ['all', :label_user_mail_option_all], + ['selected', :label_user_mail_option_selected], + ['only_my_events', :label_user_mail_option_only_my_events], + ['only_assigned', :label_user_mail_option_only_assigned], + ['only_owner', :label_user_mail_option_only_owner], + ['none', :label_user_mail_option_none] + ] + + has_many :homework_users + has_many :homework_attaches, :through => :homework_users + has_many :homework_evaluations + + has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)}, + :after_remove => Proc.new {|user, group| group.user_removed(user)} + has_many :changesets, :dependent => :nullify + has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' + has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'" + has_one :api_token, :class_name => 'Token', :conditions => "action='api'" + belongs_to :auth_source + belongs_to :ucourse, :class_name => 'Course', :foreign_key => :id #huang +## added by xianbo for delete + has_many :biding_projects, :dependent => :destroy + has_many :contesting_projects, :dependent => :destroy + belongs_to :softapplication, :foreign_key => 'id', :dependent => :destroy +##ended by xianbo + +#####fq + has_many :jours, :class_name => 'JournalsForMessage', :dependent => :destroy + has_many :journals_messages, :class_name => 'JournalsForMessage', :foreign_key => "user_id", :dependent => :destroy + has_many :bids, :foreign_key => 'author_id', :dependent => :destroy + has_many :contests, :foreign_key => 'author_id', :dependent => :destroy + has_many :softapplications, :foreign_key => 'user_id', :dependent => :destroy + has_many :journals_for_messages, :as => :jour, :dependent => :destroy + has_many :new_jours, :as => :jour, :class_name => 'JournalsForMessage', :conditions => "status=1" + has_many :journal_replies, :dependent => :destroy + has_many :activities, :dependent => :destroy + has_many :students_for_courses + #has_many :courses, :through => :students_for_courses, :source => :project + has_many :acts, :class_name => 'Activity', :as => :act, :dependent => :destroy + has_many :file_commit, :class_name => 'Attachment', :foreign_key => 'author_id', :conditions => "container_type = 'Project' or container_type = 'Version'" +#### +# added by bai + has_many :join_in_contests, :dependent => :destroy + has_many :news, :foreign_key => 'author_id' + has_many :contestnotification, :foreign_key => 'author_id' + has_many :comments, :foreign_key => 'author_id' + has_many :notificationcomments, :foreign_key => 'author_id' + has_many :wiki_contents, :foreign_key => 'author_id' + has_many :journals + has_many :messages, :foreign_key => 'author_id' + has_one :user_score, :dependent => :destroy + has_many :documents # 项目中关联的文档再次与人关联 +# end + +######added by nie + has_many :project_infos, :dependent => :destroy + has_one :user_status, :dependent => :destroy + ##### + has_many :shares ,:dependent => :destroy + + # add by zjc + has_one :level, :class_name => 'UserLevels', :dependent => :destroy + has_many :memos , :foreign_key => 'author_id' + ##### + scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") } + scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) } + scope :visible, lambda {|*args| + nil + } + + + acts_as_customizable + ############################added by william + acts_as_taggable + scope :by_join_date, order("created_on DESC") + ############################# added by liuping 关注 + acts_as_watchable + seems_rateable_rater + has_one :user_extensions,:dependent => :destroy + ## end + + # default_scope -> { includes(:user_extensions, :user_score) } + scope :teacher, -> { + joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::TEACHER) + } + scope :student, -> { + joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::STUDENT) + } + scope :developer, -> { + joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::DEVELOPER) + } + scope :enterprise, -> { + joins(:user_extensions).where('user_extensions.identity = ?', UserExtensions::ENTERPRISE) + } + + attr_accessor :password, :password_confirmation + attr_accessor :last_before_login_on + # Prevents unauthorized assignments + attr_protected :login, :admin, :password, :password_confirmation, :hashed_password + + LOGIN_LENGTH_LIMIT = 25 + MAIL_LENGTH_LIMIT = 60 + + validates_presence_of :login, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } + 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 + # Login must contain letters, numbers, underscores only + validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i + validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT + validates_length_of :firstname, :maximum => 30 + validates_length_of :lastname, :maximum => 30 + validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true + validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true + validates_confirmation_of :password, :allow_nil => true + validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true + validate :validate_password_length + + before_create :set_mail_notification + before_save :update_hashed_password + before_destroy :remove_references_before_destroy + # added by fq + after_create :act_as_activity + # end + + scope :in_group, lambda {|group| + group_id = group.is_a?(Group) ? group.id : group.to_i + where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id) + } + scope :not_in_group, lambda {|group| + group_id = group.is_a?(Group) ? group.id : group.to_i + where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id) + } + scope :sorted, lambda { order(*User.fields_for_order_statement)} + + scope :like, lambda {|arg, type| + if arg.blank? + where(nil) + else + pattern = "%#{arg.to_s.strip.downcase}%" + #where(" LOWER(concat(lastname, firstname)) LIKE :p ", :p => pattern) + if type == "0" + where(" LOWER(login) LIKE :p ", :p => pattern) + elsif type == "1" + where(" LOWER(concat(lastname, firstname)) LIKE :p ", :p => pattern) + else + where(" LOWER(mail) LIKE :p ", :p => pattern) + end + end + } + + + # ====================================================================== + + def extensions + self.user_extensions ||= UserExtensions.new + end + + def user_score_attr + self.user_score ||= UserScore.new + end + + # ====================================================================== + + #选择项目成员时显示的用户信息文字 + def userInfo + if self.realname.gsub(' ','') == "" || self.realname.nil? + info = self.nickname; + else + info=self.nickname + ' (' + self.realname + ')'; + end + info + end + + ###添加留言 fq + def add_jour(user, notes, reference_user_id = 0, options = {}) + if options.count == 0 + self.journals_for_messages << JournalsForMessage.new(:user_id => user.id, :notes => notes, :reply_id => reference_user_id, :status => true) + else + jfm = self.journals_for_messages.build(options) + jfm.save + jfm + end + end + + # 判断用户是否加入了竞赛中 fq + def join_in_contest?(bid) + joined = JoinInContest.where('user_id = ? and bid_id =?', self.id, bid.id) + if joined.size > 0 + true + else + false + end + end + + ### fq + def join_in?(course) + joined = StudentsForCourse.where('student_id = ? and course_id = ?', self.id, course.id) + if joined.size > 0 + true + else + false + end + end + + def show_name + unless self.user_extensions.nil? + if self.user_extensions.identity == 2 + firstname + else + lastname+firstname + end + else + lastname+firstname + end + end + ## end + + def count_new_jour + count = self.new_jours.count + end + + #added by nie + def count_new_journal_reply + count = self.journal_reply.count + end + + def set_mail_notification + ##add byxianbo + thread=Thread.new do + self.mail_notification = Setting.default_notification_option if self.mail_notification.blank? + true + end + end + + def update_hashed_password + # update hashed_password if password was set + if self.password && self.auth_source_id.blank? + salt_password(password) + end + end + + alias :base_reload :reload + def reload(*args) + @name = nil + @projects_by_role = nil + @courses_by_role = nil + @membership_by_project_id = nil + base_reload(*args) + end + + def mail=(arg) + write_attribute(:mail, arg.to_s.strip) + end + + def identity_url=(url) + if url.blank? + write_attribute(:identity_url, '') + else + begin + write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url)) + rescue OpenIdAuthentication::InvalidOpenId + # Invalid url, don't save + end + end + self.read_attribute(:identity_url) + end + + VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i + # VALID_EMAIL_REGEX = /^[0-9a-zA-Z_-]+@[0-9a-zA-Z_-]+(\.[0-9a-zA-Z_-]+)+$/ + # Returns the user that matches provided login and password, or nil + #登录,返回用户名与密码匹配的用户 + def self.try_to_login(login, password) + login = login.to_s.lstrip.rstrip + 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) + else + user = find_by_login(login) + end + if user + # user is already in local database + #return nil unless user.active? + return nil unless user.check_password?(password) + else + # user is not yet registered, try to authenticate with available sources + attrs = AuthSource.authenticate(login, password) + if attrs + user = new(attrs) + user.login = login + user.language = Setting.default_language + if user.save + user.reload + logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source + end + end + end + if user && !user.new_record? + last_login_on = user.last_login_on.nil? ? '' : user.last_login_on.to_s + user.update_column(:last_login_on, Time.now) + end + [user, last_login_on] + rescue => text + raise text + 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, Setting.autologin.to_i) + if user + user.update_column(:last_login_on, Time.now) + user + end + end + + def self.name_formatter(formatter = nil) + USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname] + end + + # Returns an array of fields names than can be used to make an order statement for users + # according to how user names are displayed + # Examples: + # + # User.fields_for_order_statement => ['users.login', 'users.id'] + # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id'] + def self.fields_for_order_statement(table=nil) + table ||= table_name + name_formatter[:order].map {|field| "#{table}.#{field}"} + end + + # Return user's full name for display + def realname(formatter = nil) + f = self.class.name_formatter(formatter) + if formatter + eval('"' + f[:string] + '"') + else + @name ||= eval('"' + f[:string] + '"') + end + end + + def nickname(formatter = nil) + login + end + + def name(formatter = nil) + login + end + + def active? + self.status == STATUS_ACTIVE + end + + def registered? + self.status == STATUS_REGISTERED + end + + def locked? + self.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 + + # Returns true if +clear_password+ is the correct user's password, otherwise false + def check_password?(clear_password) + if auth_source_id.present? + auth_source.authenticate(self.login, clear_password) + else + User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password + end + end + + # Generates a random salt and computes hashed_password for +clear_password+ + # The hashed password is stored in the following form: SHA1(salt + SHA1(password)) + def salt_password(clear_password) + self.salt = User.generate_salt + self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}") + end + + # Does the backend storage allow this user to change their password? + def change_password_allowed? + return true if auth_source.nil? + return auth_source.allow_password_changes? + end + + # Generate and set a random password. Useful for automated user creation + # Based on Token#generate_token_value + # + def random_password + chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + password = '' + 40.times { |i| password << chars[rand(chars.size-1)] } + self.password = password + self.password_confirmation = password + self + end + + def pref + self.preference ||= UserPreference.new(:user => self) + end + + def time_zone + @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone]) + end + + def wants_comments_in_reverse_order? + self.pref[:comments_sorting] == 'desc' + end + + def wants_notificationcomments_in_reverse_order? + self.pref[:notificationcomments_sorting] == 'desc' + end + # Return user's RSS key (a 40 chars long string), used to access feeds + def rss_key + if rss_token.nil? + create_rss_token(:action => 'feeds') + end + rss_token.value + end + + # Return user's API key (a 40 chars long string), used to access the API + def api_key + if api_token.nil? + create_api_token(:action => 'api') + end + api_token.value + end + + # Return an array of project ids for which the user has explicitly turned mail notifications on + def notified_projects_ids + @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id) + end + + def notified_project_ids=(ids) + Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id]) + Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? + @notified_projects_ids = nil + notified_projects_ids + end + + def valid_notification_options + self.class.valid_notification_options(self) + end + + # Only users that belong to more than 1 project can select projects for which they are notified + def self.valid_notification_options(user=nil) + # Note that @user.membership.size would fail since AR ignores + # :include association option when doing a count + if user.nil? || user.memberships.length < 1 + MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'} + else + MAIL_NOTIFICATION_OPTIONS + end + end + + # Find a user account by matching the exact login and then a case-insensitive + # version. Exact matches will be given priority. + #通过用户名查找相应的用户,若没有匹配到,则不区分大小写进行查询 + #修改:不再匹配不区分大小写情况 -zjc + def self.find_by_login(login) + if login.present? + login = login.to_s + # First look for an exact match + user = where(:login => login).all.detect {|u| u.login == login} + #unless user + # # Fail over to case-insensitive if none was found + # user = where("LOWER(login) = ?", login.downcase).first + #end + user + end + end + + def self.find_by_rss_key(key) + Token.find_active_user('feeds', key) + end + + def self.find_by_api_key(key) + Token.find_active_user('api', key) + end + + # Makes find_by_mail case-insensitive + def self.find_by_mail(mail) + where("LOWER(mail) = ?", mail.to_s.downcase).first + end + + # Returns true if the default admin account can no longer be used + def self.default_admin_account_changed? + !User.active.find_by_login("admin").try(:check_password?, "admin") + end + + def to_s + name + end + + CSS_CLASS_BY_STATUS = { + STATUS_ANONYMOUS => 'anon', + STATUS_ACTIVE => 'active', + STATUS_REGISTERED => 'registered', + STATUS_LOCKED => 'locked' + } + + def css_classes + "user #{CSS_CLASS_BY_STATUS[status]}" + end + + # Returns the current day according to user's time zone + def today + if time_zone.nil? + Date.today + else + Time.now.in_time_zone(time_zone).to_date + end + end + + # Returns the day of +time+ according to user's time zone + def time_to_date(time) + if time_zone.nil? + time.to_date + else + time.in_time_zone(time_zone).to_date + end + end + + def logged? + true + end + + def anonymous? + !logged? + end + + # Returns user's membership for the given project + # or nil if the user is not a member of project + def membership(project) + project_id = project.is_a?(Project) ? project.id : project + + @membership_by_project_id ||= Hash.new {|h, project_id| + h[project_id] = memberships.where(:project_id => project_id).first + } + @membership_by_project_id[project_id] + end + + def coursemembership(course) + course_id = course.is_a?(Course) ? course.id : course + + @membership_by_course_id ||= Hash.new {|h, course_id| + h[course_id] = coursememberships.where(:course_id => course_id).first + } + @membership_by_course_id[course_id] + end + + # Return user's roles for project + def roles_for_project(project) + roles = [] + # No role on archived projects + return roles if project.nil? || project.archived? + if logged? + # Find project membership + membership = membership(project) + if membership + roles = membership.roles + else + @role_non_member ||= Role.non_member + roles << @role_non_member + end + else + @role_anonymous ||= Role.anonymous + roles << @role_anonymous + end + roles + end + + # 用户课程权限判断 + def roles_for_course(course) + roles = [] + # No role on archived courses + return roles if course.nil? || course.archived? + if logged? + # Find course membership + membership = coursemembership(course) + if membership + roles = membership.roles + else + @role_non_member ||= Role.non_member + roles << @role_non_member + end + else + @role_anonymous ||= Role.anonymous + roles << @role_anonymous + end + roles + end + + # Return true if the user is a member of project + def member_of?(project) + projects.to_a.include?(project) + end + + def member_of_course?(course) + courses.to_a.include?(course) + end + + # Returns a hash of user's projects grouped by roles + def projects_by_role + return @projects_by_role if @projects_by_role + + @projects_by_role = Hash.new([]) + memberships.each do |membership| + if membership.project + membership.roles.each do |role| + @projects_by_role[role] = [] unless @projects_by_role.key?(role) + @projects_by_role[role] << membership.project + end + end + end + @projects_by_role.each do |role, projects| + projects.uniq! + end + + @projects_by_role + end + + # 课程的角色权限 + def courses_by_role + return @courses_by_role if @courses_by_role + + @courses_by_role = Hash.new([]) + coursememberships.each do |membership| + if membership.course + membership.roles.each do |role| + @courses_by_role[role] = [] unless @courses_by_role.key?(role) + @courses_by_role[role] << membership.course + end + end + end + @courses_by_role.each do |role, courses| + courses.uniq! + end + + @courses_by_role + end + # Returns true if user is arg or belongs to arg + def is_or_belongs_to?(arg) + if arg.is_a?(User) + self == arg + elsif arg.is_a?(Group) + arg.users.include?(self) + else + false + end + end + + + # Return true if the user is allowed to do the specified action on a specific context + # Action can be: + # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') + # * a permission Symbol (eg. :edit_project) + # Context can be: + # * a project : returns true if user is allowed to do the specified action on this project + # * an array of projects : returns true if user is allowed on every project + # * nil with options[:global] set : check if user has at least one role allowed for this action, + # or falls back to Non Member / Anonymous permissions depending if the user is logged + def allowed_to?(action, context, options={}, &block) + if context && context.is_a?(Project) + return false unless context.allows_to?(action) + # Admin users are authorized for anything else + return true if admin? + + roles = roles_for_project(context) + return false unless roles + roles.any? {|role| + (context.is_public? || role.member?) && + role.allowed_to?(action) && + (block_given? ? yield(role, self) : true) + } + #添加课程相关的权限判断 + elsif context && context.is_a?(Course) + return false unless context.allows_to?(action) + # Admin users are authorized for anything else + return true if admin? + + roles = roles_for_course(context) + return false unless roles + roles.any? {|role| + (context.is_public? || role.member?) && + role.allowed_to?(action) && + (block_given? ? yield(role, self) : true) + } + elsif context && context.is_a?(Array) + if context.empty? + false + else + # Authorize if user is authorized on every element of the array + context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&) + end + elsif options[:global] + # Admin users are always authorized + return true if admin? + + # authorize if user has at least one role that has this permission + roles = memberships.collect {|m| m.roles}.flatten.uniq + if roles.count == 0 + roles = coursememberships.collect {|m| m.roles}.flatten.uniq + end + roles << (self.logged? ? Role.non_member : Role.anonymous) + roles.any? {|role| + role.allowed_to?(action) && + (block_given? ? yield(role, self) : true) + } + else + if admin? + return true + end + #无项目时 查看Non member(id为1)角色是否有权限执行action + Role.find('1').allowed_to?(action) + # false + end + end + + # Is the user allowed to do the specified action on any project? + # See allowed_to? for the actions and valid options. + def allowed_to_globally?(action, options, &block) + allowed_to?(action, nil, options.reverse_merge(:global => true), &block) + end + + # Returns true if the user is allowed to delete his own account + def own_account_deletable? + Setting.unsubscribe? && + (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?) + end + + safe_attributes 'login', + 'firstname', + 'lastname', + 'mail', + 'mail_notification', + 'language', + 'custom_field_values', + 'custom_fields', + 'identity_url' + + safe_attributes 'status', + 'auth_source_id', + :if => lambda {|user, current_user| current_user.admin?} + + safe_attributes 'group_ids', + :if => lambda {|user, current_user| current_user.admin? && !user.new_record?} + + # Utility method to help check if a user should be notified about an + # event. + # + # TODO: only supports Issue events currently + def notify_about?(object) + if mail_notification == 'all' + true + elsif mail_notification.blank? || mail_notification == 'none' + false + else + case object + when Issue + case mail_notification + when 'selected', 'only_my_events' + # user receives notifications for created/assigned issues on unselected projects + object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was) + when 'only_assigned' + is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was) + when 'only_owner' + object.author == self + end + when News + # always send to project members except when mail_notification is set to 'none' + true + #判定用户是否接受留言提醒邮件 + when JournalsForMessage + ##如果是直接留言并且留言对象是Project并且Project类型是课程(课程留言) + if !object.at_user && object.jour.class.to_s.to_sym == :Project && object.jour.project_type == 1 + #根据用户设置邮件接收模式判定当前用户是否接受邮件提醒 + is_notified_project object.jour + end + + end + end + end + + #用户是否接收project的消息提醒 + def is_notified_project arg + if arg.is_a?(Project) + case mail_notification + when 'selected' + notified_projects_ids.include?(arg.id) + when 'only_my_events' + projects.include?(arg) + when 'only_assigned' + false + when 'only_owner' + course = Course.find_by_extra(arg.identifier) + course.teacher == self + end + #勾选的项目或用户的项目 TODO:需改 + #notified_projects_ids.include?(arg) || projects.include?(arg) + else + false + end + end + + def self.current=(user) + Thread.current[:current_user] = user + end + + def self.current + Thread.current[:current_user] ||= User.anonymous + end + + # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only + # one anonymous user per database. + def self.anonymous + anonymous_user = AnonymousUser.first + if anonymous_user.nil? + anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) + raise 'Unable to create the anonymous user.' if anonymous_user.new_record? + end + anonymous_user + end + + # Salts all existing unsalted passwords + # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password)) + # This method is used in the SaltPasswords migration and is to be kept as is + def self.salt_unsalted_passwords! + transaction do + User.where("salt IS NULL OR salt = ''").find_each do |user| + next if user.hashed_password.blank? + salt = User.generate_salt + hashed_password = User.hash_password("#{salt}#{user.hashed_password}") + User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password) + end + end + end + + protected + + def validate_password_length + # Password length validation based on setting + if !password.nil? && password.size < Setting.password_min_length.to_i + errors.add(:password, :too_short, :count => Setting.password_min_length.to_i) + end + end + private + + def act_as_activity + self.acts << Activity.new(:user_id => self.id) + end + + # Removes references that are not handled by associations + # Things that are not deleted are reassociated with the anonymous user + def remove_references_before_destroy + return if self.id.nil? + + substitute = User.anonymous + Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + Notificationcomment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id] + Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] + JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s] + JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s] + Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + # Remove private queries and keep public ones + ::Query.delete_all ['user_id = ? AND is_public = ?', id, false] + ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] + TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id] + Token.delete_all ['user_id = ?', id] + Watcher.delete_all ['user_id = ?', id] + WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id] + end + + # Return password digest + def self.hash_password(clear_password) + Digest::SHA1.hexdigest(clear_password || "") + end + + # Returns a 128bits random salt as a hex string (32 chars long) + def self.generate_salt + Redmine::Utils.random_hex(16) + 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; nil end + def time_zone; nil end + def rss_key; nil end + + def pref + UserPreference.new(:user => self) + end + + # def member_of?(project) + # false + # end + + # Anonymous user can not be destroyed + def destroy + false + end +end diff --git a/app/views/account/login.html.erb b/app/views/account/login.html.erb index e245b6a7f..4175282b3 100644 --- a/app/views/account/login.html.erb +++ b/app/views/account/login.html.erb @@ -1,95 +1,98 @@ -<% @nav_dispaly_home_path_label = 1 - @nav_dispaly_main_course_label = 1 - @nav_dispaly_main_project_label = 1 - @nav_dispaly_main_contest_label = 1 %> -<% @nav_dispaly_forum_label = 1%> -<%= call_hook :view_account_login_top %> - - - -
- - | -- <%= text_field_tag 'username', params[:username], :tabindex => '1' , :value => "#{l(:label_login_prompt)}", - :onfocus => "clearInfo('username','#{l(:label_login_prompt)}')", - :onblur => "showInfo('username','#{l(:label_login_prompt)}')", - :style => "resize: none;font-size: 12px;color: #818283;"%> - | -
- - | -- <%= password_field_tag 'password', nil, :tabindex => '2' %> - | -
- - | -- <%= text_field_tag "openid_url", nil, :tabindex => '3' %> - | -
- | - <% if Setting.autologin? %> - - <% end %> - | -
- - - <% if Setting.lost_password? %> - <%= link_to l(:label_password_lost), lost_password_path %> - <% end %> - - - - | -
+ + | ++ <%= text_field_tag 'username', params[:username], :tabindex => '1' , :value => "#{l(:label_login_prompt)}", + :onfocus => "clearInfo('username','#{l(:label_login_prompt)}')", + :onblur => "showInfo('username','#{l(:label_login_prompt)}')", + :style => "resize: none;font-size: 12px;color: #818283;"%> + | +
+ + | ++ <%= password_field_tag 'password', nil, :tabindex => '2' %> + | +
+ + | ++ <%= text_field_tag "openid_url", nil, :tabindex => '3' %> + | +
+ | + <% if Setting.autologin? %> + + <% end %> + | +
+ + + <% if Setting.lost_password? %> + <%= link_to l(:label_password_lost), lost_password_path %> + <% end %> + + + + | +
- - <%= l(:button_submit)%> - <%= link_to l(:button_cancel), "#", :onclick => '$("#add-message").hide(); return false;', :class => 'ButtonColor m3p10' %>
+ + <%= link_to l(:button_cancel), "#", :onclick => '$("#add-message").hide(); return false;', :class => 'whiteButton m3p10' %> <% end %> <% end %> diff --git a/app/views/files/_course_file.html.erb b/app/views/files/_course_file.html.erb index 0823fae4b..04ce917c8 100644 --- a/app/views/files/_course_file.html.erb +++ b/app/views/files/_course_file.html.erb @@ -31,11 +31,6 @@ $('#insite').attr("class", "re_schbtn b_dblue"); } } - function buttoncss() - { - $('#incourse').attr("class", "re_schbtn b_lblue"); - $('#insite').attr("class", "re_schbtn b_lblue"); - } @@ -44,8 +39,8 @@