mirror of
https://github.com/TracksApp/tracks.git
synced 2025-12-24 11:10:12 +01:00
Grouping isn't as lax in PostgreSQL as it is in MySQL or SQLite. All sort fields also need to be in the GROUP BY, or be aggregated. The order isn't relevant when counting, so simply don't order in that case. Fix #1336
243 lines
7.8 KiB
Ruby
243 lines
7.8 KiB
Ruby
require 'digest/sha1'
|
|
require 'bcrypt'
|
|
|
|
class User < ActiveRecord::Base
|
|
# Virtual attribute for the unencrypted password
|
|
attr_accessor :password
|
|
attr_protected :is_admin # don't allow mass-assignment for this
|
|
|
|
attr_accessible :login, :first_name, :last_name, :password_confirmation, :password, :auth_type, :open_id_url
|
|
#for will_paginate plugin
|
|
cattr_accessor :per_page
|
|
@@per_page = 5
|
|
|
|
has_many :contexts,
|
|
:order => 'position ASC',
|
|
:dependent => :delete_all do
|
|
def find_by_params(params)
|
|
find_by_id(params['id'] || params['context_id']) || nil
|
|
end
|
|
def update_positions(context_ids)
|
|
context_ids.each_with_index {|id, position|
|
|
context = self.detect { |c| c.id == id.to_i }
|
|
raise I18n.t('models.user.error_context_not_associated', :context => id, :user => @user.id) if context.nil?
|
|
context.update_attribute(:position, position + 1)
|
|
}
|
|
end
|
|
end
|
|
has_many :projects,
|
|
:order => 'projects.position ASC',
|
|
:dependent => :delete_all do
|
|
def find_by_params(params)
|
|
find_by_id(params['id'] || params['project_id'])
|
|
end
|
|
def update_positions(project_ids)
|
|
project_ids.each_with_index {|id, position|
|
|
project = self.detect { |p| p.id == id.to_i }
|
|
raise I18n.t('models.user.error_project_not_associated', :project => id, :user => @user.id) if project.nil?
|
|
project.update_attribute(:position, position + 1)
|
|
}
|
|
end
|
|
def projects_in_state_by_position(state)
|
|
self.sort{ |a,b| a.position <=> b.position }.select{ |p| p.state == state }
|
|
end
|
|
def next_from(project)
|
|
self.offset_from(project, 1)
|
|
end
|
|
def previous_from(project)
|
|
self.offset_from(project, -1)
|
|
end
|
|
def offset_from(project, offset)
|
|
projects = self.projects_in_state_by_position(project.state)
|
|
position = projects.index(project)
|
|
return nil if position == 0 && offset < 0
|
|
projects.at( position + offset)
|
|
end
|
|
def cache_note_counts
|
|
project_note_counts = Note.group(:project_id).count
|
|
self.each do |project|
|
|
project.cached_note_count = project_note_counts[project.id] || 0
|
|
end
|
|
end
|
|
def alphabetize(scope_conditions = {})
|
|
projects = where(scope_conditions)
|
|
projects.sort!{ |x,y| x.name.downcase <=> y.name.downcase }
|
|
self.update_positions(projects.map{ |p| p.id })
|
|
return projects
|
|
end
|
|
def actionize(scope_conditions = {})
|
|
todos_in_project = where(scope_conditions).includes(:todos)
|
|
todos_in_project.sort!{ |x, y| -(x.todos.active.count <=> y.todos.active.count) }
|
|
todos_in_project.reject{ |p| p.todos.active.count > 0 }
|
|
sorted_project_ids = todos_in_project.map {|p| p.id}
|
|
|
|
all_project_ids = all.map {|p| p.id}
|
|
other_project_ids = all_project_ids - sorted_project_ids
|
|
|
|
update_positions(sorted_project_ids + other_project_ids)
|
|
|
|
return where(scope_conditions)
|
|
end
|
|
end
|
|
has_many :todos,
|
|
:order => 'todos.completed_at DESC, todos.created_at DESC',
|
|
:dependent => :delete_all do
|
|
def count_by_group(g)
|
|
except(:order).group(g).count
|
|
end
|
|
end
|
|
has_many :recurring_todos,
|
|
:order => 'recurring_todos.completed_at DESC, recurring_todos.created_at DESC',
|
|
:dependent => :delete_all
|
|
has_many :deferred_todos,
|
|
:class_name => 'Todo',
|
|
:conditions => [ 'state = ?', 'deferred' ],
|
|
:order => 'show_from ASC, todos.created_at DESC' do
|
|
def find_and_activate_ready
|
|
where('show_from <= ?', Time.zone.now).collect { |t| t.activate! }
|
|
end
|
|
end
|
|
has_many :notes, :order => "created_at DESC", :dependent => :delete_all
|
|
has_one :preference, :dependent => :destroy
|
|
|
|
validates_presence_of :login
|
|
validates_presence_of :password, :if => :password_required?
|
|
validates_length_of :password, :within => 5..40, :if => :password_required?
|
|
validates_presence_of :password_confirmation, :if => :password_required?
|
|
validates_confirmation_of :password
|
|
validates_length_of :login, :within => 3..80
|
|
validates_uniqueness_of :login, :on => :create
|
|
validate :validate_auth_type
|
|
|
|
before_create :crypt_password, :generate_token
|
|
before_update :crypt_password
|
|
|
|
def validate_auth_type
|
|
unless Tracks::Config.auth_schemes.include?(auth_type)
|
|
errors.add("auth_type", "not a valid authentication type (#{auth_type})")
|
|
end
|
|
end
|
|
|
|
alias_method :prefs, :preference
|
|
|
|
def self.authenticate(login, pass)
|
|
return nil if login.blank?
|
|
candidate = where("login = ?", login).first
|
|
return nil if candidate.nil?
|
|
|
|
if Tracks::Config.auth_schemes.include?('database')
|
|
return candidate if candidate.auth_type == 'database' and
|
|
candidate.password_matches? pass
|
|
end
|
|
|
|
if Tracks::Config.auth_schemes.include?('ldap')
|
|
return candidate if candidate.auth_type == 'ldap' && SimpleLdapAuthenticator.valid?(login, pass)
|
|
end
|
|
|
|
if Tracks::Config.auth_schemes.include?('cas')
|
|
# because we can not auth them with out thier real password we have to settle for this
|
|
return candidate if candidate.auth_type.eql?("cas")
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
def self.no_users_yet?
|
|
count == 0
|
|
end
|
|
|
|
def self.find_admin
|
|
where(:is_admin => true).first
|
|
end
|
|
|
|
def to_param
|
|
login
|
|
end
|
|
|
|
def display_name
|
|
if first_name.blank? && last_name.blank?
|
|
return login
|
|
elsif first_name.blank?
|
|
return last_name
|
|
elsif last_name.blank?
|
|
return first_name
|
|
end
|
|
"#{first_name} #{last_name}"
|
|
end
|
|
|
|
def change_password(pass,pass_confirm)
|
|
self.password = pass
|
|
self.password_confirmation = pass_confirm
|
|
save!
|
|
end
|
|
|
|
def time
|
|
Time.now.in_time_zone(prefs.time_zone)
|
|
end
|
|
|
|
def date
|
|
time.midnight
|
|
end
|
|
|
|
def at_midnight(date)
|
|
return ActiveSupport::TimeZone[prefs.time_zone].local(date.year, date.month, date.day, 0, 0, 0)
|
|
end
|
|
|
|
def generate_token
|
|
self.token = Digest::SHA1.hexdigest "#{Time.now.to_i}#{rand}"
|
|
end
|
|
|
|
def remember_token?
|
|
remember_token_expires_at && Time.now.utc < remember_token_expires_at
|
|
end
|
|
|
|
# These create and unset the fields required for remembering users between browser closes
|
|
def remember_me
|
|
self.remember_token_expires_at = 2.weeks.from_now.utc
|
|
self.remember_token ||= Digest::SHA1.hexdigest("#{login}--#{remember_token_expires_at}")
|
|
save
|
|
end
|
|
|
|
def forget_me
|
|
self.remember_token_expires_at = nil
|
|
self.remember_token = nil
|
|
save
|
|
end
|
|
|
|
# Returns true if the user has a password hashed using SHA-1.
|
|
def uses_deprecated_password?
|
|
crypted_password =~ /^[a-f0-9]{40}$/i
|
|
end
|
|
|
|
def password_matches?(pass)
|
|
if uses_deprecated_password?
|
|
crypted_password == sha1(pass)
|
|
else
|
|
BCrypt::Password.new(crypted_password) == pass
|
|
end
|
|
end
|
|
|
|
def salted(s)
|
|
"#{Tracks::Config.salt}--#{s}--"
|
|
end
|
|
|
|
def sha1(s)
|
|
Digest::SHA1.hexdigest(salted(s))
|
|
end
|
|
|
|
def create_hash(s)
|
|
BCrypt::Password.create(s)
|
|
end
|
|
|
|
protected
|
|
|
|
def crypt_password
|
|
return if password.blank?
|
|
write_attribute("crypted_password", self.create_hash(password)) if password == password_confirmation
|
|
end
|
|
|
|
def password_required?
|
|
auth_type == 'database' && crypted_password.blank? || !password.blank?
|
|
end
|
|
|
|
end
|