# -*- encoding : utf-8 -*- # == Schema Information # # Table name: users # # id :integer not null, primary key # email :string(255) not null # name :string(255) not null # hashed_password :string(255) not null # salt :string(255) not null # created_at :datetime not null # updated_at :datetime not null # email_confirmed :boolean default(FALSE), not null # url_name :text not null # last_daily_track_email :datetime default(Sat Jan 01 00:00:00 UTC 2000) # admin_level :string(255) default("none"), not null # ban_text :text default(""), not null # about_me :text default(""), not null # locale :string(255) # email_bounced_at :datetime # email_bounce_message :text default(""), not null # no_limit :boolean default(FALSE), not null # receive_email_alerts :boolean default(TRUE), not null # can_make_batch_requests :boolean default(FALSE), not null # require 'digest/sha1' class User < ActiveRecord::Base strip_attributes! attr_accessor :password_confirmation, :no_xapian_reindex has_many :info_requests, :order => 'created_at desc' has_many :user_info_request_sent_alerts has_many :post_redirects has_many :track_things, :foreign_key => 'tracking_user_id', :order => 'created_at desc' has_many :comments, :order => 'created_at desc' has_one :profile_photo has_many :censor_rules, :order => 'created_at desc' has_many :info_request_batches, :order => 'created_at desc' 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', ], :message => N_('Admin level is not included in list') 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 ], :terms => [ [ :variety, 'V', "variety" ] ], :if => :indexed_by_search? # 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 created_at.strftime("%Y%m%d%H%M%S") end def variety "user" end # requested_by: and commented_by: search queries also need updating after save def reindex_referencing_models return if no_xapian_reindex == true 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 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 end end def get_locale (locale || I18n.locale).to_s end def visible_comments comments.find(:all, :conditions => 'visible') end # Don't display any leading/trailing spaces # TODO: we have strip_attributes! now, so perhaps this can be removed (might # be still needed for existing cases) def name name = read_attribute(:name) if not name.nil? name.strip! end if 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 = "#{name}" end name end # When name is changed, also change the url name def name=(name) write_attribute(:name, name) update_url_name end def update_url_name 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 => id.nil? ? nil : ["id <> ?", id] ).nil? unique_url_name = url_name + "_" + suffix_num.to_s suffix_num = suffix_num + 1 end write_attribute(:url_name, unique_url_name) end # Virtual password attribute, which stores the hashed password, rather than plain text. def password @password end def password=(pwd) @password = pwd if pwd.blank? self.hashed_password = nil return end create_new_salt self.hashed_password = User.encrypted_password(password, salt) end def has_this_password?(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 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 info_requests.where( "awaiting_description = ? and #{ InfoRequest.last_event_time_clause } < ?", true, 1.day.ago ) end # 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 url_name == "heather_brooke" || url_name == "heather_brooke_2" return true end return false end # Does the user magically gain powers as if they owned every request? # e.g. Can classify it def owns_every_request? super? end # Does this user have extraordinary powers? def super? admin_level == 'super' end # Does the user get "(admin)" links on each page on the main site? def admin_page_links? super? end # Is it public that they are banned? def banned? !ban_text.empty? end def public_banned? warn %q([DEPRECATION] User#public_banned? will be replaced with User#banned? as of 0.22).squish banned? end # Various ways the user can be banned, and text to describe it if failed def can_file_requests? ban_text.empty? && !exceeded_limit? end def exceeded_limit? # Some users have no limit return false if no_limit # Batch request users don't have a limit 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", id]) recent_requests >= AlaveteliConfiguration.max_requests_per_user_per_day end def next_request_permitted_at return nil if no_limit 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] nth_most_recent_request.created_at + 1.day end def can_make_followup? ban_text.empty? end def can_make_comments? ban_text.empty? end def can_contact_other_users? ban_text.empty? end def can_fail_html if ban_text 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/, '
') text.html_safe end # Returns domain part of user's email address def email_domain 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 profile_photo.destroy unless profile_photo.nil? self.profile_photo = new_profile_photo save end end # Return about me text for display as HTML # TODO: Move this to a view helper def get_about_me_for_html_display text = about_me.strip text = CGI.escapeHTML(text) text = MySociety::Format.make_clickable(text, :contract => 1) text = text.gsub(/\n/, '
') text.html_safe end def json_for_api { :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 } end def record_bounce(message) self.email_bounced_at = Time.now self.email_bounce_message = message save! end def should_be_emailed? email_confirmed && email_bounced_at.nil? end def indexed_by_search? email_confirmed end def for_admin_column(complete = false) if complete columns = self.class.content_columns else 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, send(column.name), column.type.to_s, column.name) end end private def create_new_salt self.salt = object_id.to_s + rand.to_s end def set_defaults if admin_level.nil? self.admin_level = 'none' end 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 end end def email_and_name_are_valid if email != "" && !MySociety::Validate.is_valid_email(email) errors.add(:email, _("Please enter a valid email address")) end if MySociety::Validate.is_valid_email(name) errors.add(:name, _("Please enter your name, not your email address, in the name field.")) end end def purge_in_cache info_requests.each { |x| x.purge_in_cache } if name_changed? end end