diff options
Diffstat (limited to 'app/models/user.rb')
-rw-r--r-- | app/models/user.rb | 356 |
1 files changed, 176 insertions, 180 deletions
diff --git a/app/models/user.rb b/app/models/user.rb index 4b83d8572..1c6dc0eb0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -28,11 +28,7 @@ require 'digest/sha1' class User < ActiveRecord::Base strip_attributes! - validates_presence_of :email, :message => _("Please enter your email address") - - validates_presence_of :name, :message => _("Please enter your name") - - validates_presence_of :hashed_password, :message => _("Please enter a password") + attr_accessor :password_confirmation, :no_xapian_reindex has_many :info_requests, :order => 'created_at desc' has_many :user_info_request_sent_alerts @@ -43,9 +39,10 @@ class User < ActiveRecord::Base has_many :censor_rules, :order => 'created_at desc' has_many :info_request_batches, :order => 'created_at desc' - attr_accessor :password_confirmation, :no_xapian_reindex + validates_presence_of :email, :message => _("Please enter your email address") + validates_presence_of :name, :message => _("Please enter your name") + validates_presence_of :hashed_password, :message => _("Please enter a password") validates_confirmation_of :password, :message => _("Please enter the same password twice") - validates_inclusion_of :admin_level, :in => [ 'none', 'super', @@ -53,6 +50,10 @@ class User < ActiveRecord::Base validate :email_and_name_are_valid + after_initialize :set_defaults + after_save :purge_in_cache + after_update :reindex_referencing_models + acts_as_xapian :texts => [ :name, :about_me ], :values => [ [ :created_at_numeric, 1, "created_at", :number ] # for sorting @@ -60,11 +61,111 @@ class User < ActiveRecord::Base :terms => [ [ :variety, 'V', "variety" ] ], :if => :indexed_by_search? - after_initialize :set_defaults + # Return user given login email, password and other form parameters (e.g. name) + # + # The specific_user_login parameter says that login as a particular user is + # expected, so no parallel registration form is being displayed. + def self.authenticate_from_form(params, specific_user_login = false) + params[:email].strip! + + if specific_user_login + auth_fail_message = _("Either the email or password was not recognised, please try again.") + else + auth_fail_message = _("Either the email or password was not recognised, please try again. Or create a new account using the form on the right.") + end + + user = find_user_by_email(params[:email]) + if user + # There is user with email, check password + unless user.has_this_password?(params[:password]) + user.errors.add(:base, auth_fail_message) + end + else + # No user of same email, make one (that we don't save in the database) + # for the forms code to use. + user = User.new(params) + # deliberately same message as above so as not to leak whether registered + user.errors.add(:base, auth_fail_message) + end + user + end + + # Case-insensitively find a user from their email + def self.find_user_by_email(email) + self.find(:first, :conditions => [ 'lower(email) = lower(?)', email ] ) + end + + # The "internal admin" is a special user for internal use. + def self.internal_admin_user + user = User.find_by_email(AlaveteliConfiguration::contact_email) + if user.nil? + password = PostRedirect.generate_random_token + user = User.new( + :name => 'Internal admin user', + :email => AlaveteliConfiguration.contact_email, + :password => password, + :password_confirmation => password + ) + user.save! + end + + user + end + + def self.owns_every_request?(user) + !user.nil? && user.owns_every_request? + end + + # Can the user see every request, response, and outgoing message, even hidden ones? + def self.view_hidden?(user) + !user.nil? && user.super? + end + + # Should the user be kept logged into their own account + # if they follow a /c/ redirect link belonging to another user? + def self.stay_logged_in_on_redirect?(user) + !user.nil? && user.super? + end + + # Used for default values of last_daily_track_email + def self.random_time_in_last_day + earliest_time = Time.now - 1.day + latest_time = Time.now + earliest_time + rand(latest_time - earliest_time).seconds + end + + # Alters last_daily_track_email for every user, so alerts will be sent + # spread out fairly evenly throughout the day, balancing load on the + # server. This is intended to be called by hand from the Ruby console. It + # will mean quite a few users may get more than one email alert the day you + # do it, so have a care and run it rarely. + # + # This SQL statement is useful for seeing how spread out users are at the moment: + # select extract(hour from last_daily_track_email) as h, count(*) from users group by extract(hour from last_daily_track_email) order by h; + def self.spread_alert_times_across_day + self.find(:all).each do |user| + user.last_daily_track_email = User.random_time_in_last_day + user.save! + end + nil # so doesn't print all users on console + end + + def self.encrypted_password(password, salt) + string_to_hash = password + salt # TODO: need to add a secret here too? + Digest::SHA1.hexdigest(string_to_hash) + end + + def self.record_bounce_for_email(email, message) + user = User.find_user_by_email(email) + return false if user.nil? + + user.record_bounce(message) if user.email_bounced_at.nil? + return true + end def created_at_numeric # format it here as no datetime support in Xapian's value ranges - return self.created_at.strftime("%Y%m%d%H%M%S") + created_at.strftime("%Y%m%d%H%M%S") end def variety @@ -72,18 +173,18 @@ class User < ActiveRecord::Base end # requested_by: and commented_by: search queries also need updating after save - after_update :reindex_referencing_models def reindex_referencing_models return if no_xapian_reindex == true - if self.changes.include?('url_name') - for comment in self.comments - for info_request_event in comment.info_request_events + if changes.include?('url_name') + comments.each do |comment| + comment.info_request_events.each do |info_request_event| info_request_event.xapian_mark_needs_index end end - for info_request in self.info_requests - for info_request_event in info_request.info_request_events + + info_requests.each do |info_request| + info_request.info_request_events.each do |info_request_event| info_request_event.xapian_mark_needs_index end end @@ -91,11 +192,11 @@ class User < ActiveRecord::Base end def get_locale - (self.locale || I18n.locale).to_s + (locale || I18n.locale).to_s end def visible_comments - self.comments.find(:all, :conditions => 'visible') + comments.find(:all, :conditions => 'visible') end # Don't display any leading/trailing spaces @@ -106,62 +207,29 @@ class User < ActiveRecord::Base if not name.nil? name.strip! end - if self.public_banned? + if public_banned? # Use interpolation to return a string rather than a SafeBuffer so that # gsub can be called on it until we upgrade to Rails 3.2. The name returned # is not marked as HTML safe so will be escaped automatically in views. We # do this in two steps so the string still gets picked up for translation - name = _("{{user_name}} (Account suspended)", :user_name=> name.html_safe) + name = _("{{user_name}} (Account suspended)", :user_name => name.html_safe) name = "#{name}" end name end - # Return user given login email, password and other form parameters (e.g. name) - # - # The specific_user_login parameter says that login as a particular user is - # expected, so no parallel registration form is being displayed. - def User.authenticate_from_form(params, specific_user_login = false) - params[:email].strip! - - if specific_user_login - auth_fail_message = _("Either the email or password was not recognised, please try again.") - else - auth_fail_message = _("Either the email or password was not recognised, please try again. Or create a new account using the form on the right.") - end - - user = self.find_user_by_email(params[:email]) - if user - # There is user with email, check password - if !user.has_this_password?(params[:password]) - user.errors.add(:base, auth_fail_message) - end - else - # No user of same email, make one (that we don't save in the database) - # for the forms code to use. - user = User.new(params) - # deliberately same message as above so as not to leak whether registered - user.errors.add(:base, auth_fail_message) - end - user - end - - # Case-insensitively find a user from their email - def User.find_user_by_email(email) - return self.find(:first, :conditions => [ 'lower(email) = lower(?)', email ] ) - end - # When name is changed, also change the url name def name=(name) write_attribute(:name, name) - self.update_url_name + update_url_name end + def update_url_name - url_name = MySociety::Format.simplify_url_part(self.name, 'user', 32) + url_name = MySociety::Format.simplify_url_part(name, 'user', 32) # For user with same name as others, add on arbitary numeric identifier unique_url_name = url_name suffix_num = 2 # as there's already one without numeric suffix - while not User.find_by_url_name(unique_url_name, :conditions => self.id.nil? ? nil : ["id <> ?", self.id] ).nil? + while not User.find_by_url_name(unique_url_name, :conditions => id.nil? ? nil : ["id <> ?", id] ).nil? unique_url_name = url_name + "_" + suffix_num.to_s suffix_num = suffix_num + 1 end @@ -172,6 +240,7 @@ class User < ActiveRecord::Base def password @password end + def password=(pwd) @password = pwd if pwd.blank? @@ -179,40 +248,23 @@ class User < ActiveRecord::Base return end create_new_salt - self.hashed_password = User.encrypted_password(self.password, self.salt) + self.hashed_password = User.encrypted_password(password, salt) end def has_this_password?(password) - expected_password = User.encrypted_password(password, self.salt) - return self.hashed_password == expected_password + expected_password = User.encrypted_password(password, salt) + hashed_password == expected_password end # For use in to/from in email messages def name_and_email - return MailHandler.address_from_name_and_email(self.name, self.email) - end - - # The "internal admin" is a special user for internal use. - def User.internal_admin_user - u = User.find_by_email(AlaveteliConfiguration::contact_email) - if u.nil? - password = PostRedirect.generate_random_token - u = User.new( - :name => 'Internal admin user', - :email => AlaveteliConfiguration::contact_email, - :password => password, - :password_confirmation => password - ) - u.save! - end - - return u + MailHandler.address_from_name_and_email(name, email) end # Returns list of requests which the user hasn't described (and last # changed more than a day ago) def get_undescribed_requests - self.info_requests.find( + info_requests.find( :all, :conditions => [ 'awaiting_description = ? and ' + InfoRequest.last_event_time_clause + ' < ?', true, Time.now() - 1.day @@ -223,7 +275,7 @@ class User < ActiveRecord::Base # Can the user make new requests, without having to describe state of (most) existing ones? def can_leave_requests_undescribed? # TODO: should be flag in database really - if self.url_name == "heather_brooke" || self.url_name == "heather_brooke_2" + if url_name == "heather_brooke" || url_name == "heather_brooke_2" return true end return false @@ -232,140 +284,102 @@ class User < ActiveRecord::Base # Does the user magically gain powers as if they owned every request? # e.g. Can classify it def owns_every_request? - self.super? + super? end # Does this user have extraordinary powers? def super? - self.admin_level == 'super' - end - - def User.owns_every_request?(user) - !user.nil? && user.owns_every_request? - end - - # Can the user see every request, response, and outgoing message, even hidden ones? - def User.view_hidden?(user) - !user.nil? && user.super? - end - - # Should the user be kept logged into their own account - # if they follow a /c/ redirect link belonging to another user? - def User.stay_logged_in_on_redirect?(user) - !user.nil? && user.super? + admin_level == 'super' end # Does the user get "(admin)" links on each page on the main site? def admin_page_links? - self.super? + super? end # Is it public that they are banned? def public_banned? - !self.ban_text.empty? + !ban_text.empty? end # Various ways the user can be banned, and text to describe it if failed def can_file_requests? - self.ban_text.empty? && !self.exceeded_limit? + ban_text.empty? && !exceeded_limit? end def exceeded_limit? # Some users have no limit - return false if self.no_limit + return false if no_limit # Batch request users don't have a limit - return false if self.can_make_batch_requests? + return false if can_make_batch_requests? # Has the user issued as many as MAX_REQUESTS_PER_USER_PER_DAY requests in the past 24 hours? - return false if AlaveteliConfiguration::max_requests_per_user_per_day.blank? - recent_requests = InfoRequest.count(:conditions => ["user_id = ? and created_at > now() - '1 day'::interval", self.id]) + return false if AlaveteliConfiguration.max_requests_per_user_per_day.blank? + recent_requests = InfoRequest.count(:conditions => ["user_id = ? and created_at > now() - '1 day'::interval", id]) - return (recent_requests >= AlaveteliConfiguration::max_requests_per_user_per_day) + recent_requests >= AlaveteliConfiguration.max_requests_per_user_per_day end def next_request_permitted_at - return nil if self.no_limit + return nil if no_limit - n_most_recent_requests = InfoRequest.all(:conditions => ["user_id = ? and created_at > now() - '1 day'::interval", self.id], :order => "created_at DESC", :limit => AlaveteliConfiguration::max_requests_per_user_per_day) + n_most_recent_requests = InfoRequest.all(:conditions => ["user_id = ? and created_at > now() - '1 day'::interval", id], + :order => "created_at DESC", + :limit => AlaveteliConfiguration::max_requests_per_user_per_day) return nil if n_most_recent_requests.size < AlaveteliConfiguration::max_requests_per_user_per_day nth_most_recent_request = n_most_recent_requests[-1] - return nth_most_recent_request.created_at + 1.day + nth_most_recent_request.created_at + 1.day end def can_make_followup? - self.ban_text.empty? + ban_text.empty? end def can_make_comments? - self.ban_text.empty? + ban_text.empty? end def can_contact_other_users? - self.ban_text.empty? + ban_text.empty? end def can_fail_html if ban_text - text = self.ban_text.strip + text = ban_text.strip else raise "Unknown reason for ban" end text = CGI.escapeHTML(text) text = MySociety::Format.make_clickable(text, :contract => 1) text = text.gsub(/\n/, '<br>') - return text.html_safe + text.html_safe end # Returns domain part of user's email address def email_domain - return PublicBody.extract_domain_from_email(self.email) + PublicBody.extract_domain_from_email(email) end # A photograph of the user (to make it all more human) def set_profile_photo(new_profile_photo) ActiveRecord::Base.transaction do - if !self.profile_photo.nil? - self.profile_photo.destroy - end + profile_photo.destroy unless profile_photo.nil? self.profile_photo = new_profile_photo - self.save + save end end - # Used for default values of last_daily_track_email - def User.random_time_in_last_day - earliest_time = Time.now() - 1.day - latest_time = Time.now - return earliest_time + rand(latest_time - earliest_time).seconds - end - - # Alters last_daily_track_email for every user, so alerts will be sent - # spread out fairly evenly throughout the day, balancing load on the - # server. This is intended to be called by hand from the Ruby console. It - # will mean quite a few users may get more than one email alert the day you - # do it, so have a care and run it rarely. - # - # This SQL statement is useful for seeing how spread out users are at the moment: - # select extract(hour from last_daily_track_email) as h, count(*) from users group by extract(hour from last_daily_track_email) order by h; - def User.spread_alert_times_across_day - for user in self.find(:all) - user.last_daily_track_email = User.random_time_in_last_day - user.save! - end - nil # so doesn't print all users on console - end - # Return about me text for display as HTML # TODO: Move this to a view helper def get_about_me_for_html_display - text = self.about_me.strip + text = about_me.strip text = CGI.escapeHTML(text) text = MySociety::Format.make_clickable(text, :contract => 1) text = text.gsub(/\n/, '<br>') - return text.html_safe + text.html_safe end def json_for_api - return { - :id => self.id, - :url_name => self.url_name, - :name => self.name, - :ban_text => self.ban_text, - :about_me => self.about_me, + { + :id => id, + :url_name => url_name, + :name => name, + :ban_text => ban_text, + :about_me => about_me, # :profile_photo => self.profile_photo # ought to have this, but too hard to get URL out for now # created_at / updated_at we only show the year on the main page for privacy reasons, so don't put here } @@ -374,40 +388,41 @@ class User < ActiveRecord::Base def record_bounce(message) self.email_bounced_at = Time.now self.email_bounce_message = message - self.save! + save! end def should_be_emailed? - return (self.email_confirmed && self.email_bounced_at.nil?) + email_confirmed && email_bounced_at.nil? end def indexed_by_search? - return self.email_confirmed + email_confirmed end def for_admin_column(complete = false) if complete columns = self.class.content_columns else - columns = self.class.content_columns.map{|c| c if %w(created_at updated_at admin_level email_confirmed).include?(c.name) }.compact + columns = self.class.content_columns.map do |c| + c if %w(created_at updated_at admin_level email_confirmed).include?(c.name) + end.compact end columns.each do |column| - yield(column.human_name, self.send(column.name), column.type.to_s, column.name) + yield(column.human_name, send(column.name), column.type.to_s, column.name) end end - ## Private instance methods private def create_new_salt - self.salt = self.object_id.to_s + rand.to_s + self.salt = object_id.to_s + rand.to_s end def set_defaults - if self.admin_level.nil? + if admin_level.nil? self.admin_level = 'none' end - if self.new_record? + if new_record? # make alert emails go out at a random time for each new user, so # overall they are spread out throughout the day. self.last_daily_track_email = User.random_time_in_last_day @@ -415,35 +430,16 @@ class User < ActiveRecord::Base end def email_and_name_are_valid - if self.email != "" && !MySociety::Validate.is_valid_email(self.email) + if email != "" && !MySociety::Validate.is_valid_email(email) errors.add(:email, _("Please enter a valid email address")) end - if MySociety::Validate.is_valid_email(self.name) + if MySociety::Validate.is_valid_email(name) errors.add(:name, _("Please enter your name, not your email address, in the name field.")) end end - ## Class methods - def User.encrypted_password(password, salt) - string_to_hash = password + salt # TODO: need to add a secret here too? - Digest::SHA1.hexdigest(string_to_hash) - end - - def User.record_bounce_for_email(email, message) - user = User.find_user_by_email(email) - return false if user.nil? - - if user.email_bounced_at.nil? - user.record_bounce(message) - end - return true - end - - after_save(:purge_in_cache) - def purge_in_cache - if self.name_changed? - self.info_requests.each {|x| x.purge_in_cache} - end + def purge_in_cache + info_requests.each { |x| x.purge_in_cache } if name_changed? end end |