Floppy/carbon-diet

View on GitHub
app/models/user.rb

Summary

Maintainability
D
1 day
Test Coverage
require 'digest/sha2'
require 'mini_magick'

class User < ActiveRecord::Base
  # Relationships
  belongs_to :country
  has_many :electricity_accounts do
    def current
      where(:current => true)
    end
  end
  has_many :electricity_readings, :through => :electricity_accounts
  has_many :gas_accounts do
    def current
      where(:current => true)
    end
  end
  has_many :gas_readings, :through => :gas_accounts
  has_many :vehicles do
    def current
      where(:current => true)
    end
  end
  has_many :vehicle_fuel_purchases, :through => :vehicles
  has_many :flights
  has_many :completed_actions
  has_many :actions, :through => :completed_actions
  has_many :group_memberships
  has_many :groups, :through => :group_memberships
  has_many :group_invitations
  has_many :sent_group_invitations, :class_name => 'GroupInvitation', :foreign_key => 'inviter_id'
  has_many :owned_groups, :class_name => 'Group', :foreign_key => 'owner_id'
  has_many :notes, :as => :notatable
  has_many :authored_comments, :class_name => 'Comment', :order => "created_at DESC"
  has_many :comments, :as => :commentable, :order => "created_at DESC"
  # Friendships - aargh, complicated!
  has_many :approved_friendships,
    :foreign_key =>       'user_id',
    :class_name =>        'Friendship',
    :conditions =>        { :approved => true }
  has_many :unapproved_friendships,
    :foreign_key =>       'user_id',
    :class_name =>        'Friendship',
    :conditions =>        { :approved => false }
  has_many :approved_befriendships,
    :foreign_key =>       'friend_id',
    :class_name =>        'Friendship',
    :conditions =>        { :approved => true }
  has_many :unapproved_befriendships,
    :foreign_key =>       'friend_id',
    :class_name =>        'Friendship',
    :conditions =>        { :approved => false }
  has_many :friends,
    :through => :approved_friendships,
    :source => :friend
  has_many :unapproved_friends,
    :through => :unapproved_friendships,
    :source => :friend
  has_many :fans,
    :through => :approved_befriendships,
    :source => :user
  has_many :unapproved_fans,
    :through => :unapproved_befriendships,
    :source => :user
      
  # Validation
  validates_uniqueness_of :login
  validates_format_of :email, :with => /^$|^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
  validates_format_of :login, :with => /^\w+$/i
  validates_numericality_of :people_in_household
  # Attributes
  attr_accessible :display_name, :country_id, :public, :reminder_frequency 
  attr_accessible :reminded_at, :email, :notify_friend_requests, :notify_profile_comments, :people_in_household

  def get_guid!
    if self.guid.nil?
      self.guid = Digest::SHA256.hexdigest(self.login + self.id.to_s + self.created_at.to_s + self.password_salt)
      self.save!
    end
    self.guid
  end

  def reset_password
    self.password_change_code = [Array.new(21){rand(256).chr}.join].pack("m").chomp
    save
  end

  def password=(passwd)
    # Create password salt
    generate_salt
    # Create password hash
    self.password_hash = Digest::SHA256.hexdigest(passwd + self.password_salt)
    # Clear change code in case it's set
    self.password_change_code = nil
  end
 
  def confirmed_email
    return nil unless confirmation_code.nil?
    return nil if email.blank?
    return self.email
  end

  def login=(name)
    # Store in lowercase
    write_attribute('login', name.nil? ? nil : name.downcase)
  end  

  def email=(email)
    # Check to see if address has changed
    return if self.email == email
    # Store email
    write_attribute('email', email)
    if self.save and not email.blank?
      # Create salt
      generate_salt
      # Create hashed version of email address for confirmation code
      self.confirmation_code = Digest::SHA256.hexdigest(self.email + self.password_salt)
      # Send confirmation email
      UserMailer.email_confirmation(self).deliver
    end
  end  

  def self.authenticate(login, passwd)
    user = User.where(:login => login.downcase).first
    if user.nil?
      user = User.where(:email => login).first
    end
    if user.nil?      
      return :no_such_user    
    else
      if user.email == login and not (user.login.blank? or user.login.nil?)
        return :logged_in_using_email
      elsif Digest::SHA256.hexdigest(passwd + user.password_salt) != user.password_hash
        return :wrong_password
      else
        return user.id
      end
    end
  end
  
  def avatar(small=false)
    if email
      "http://www.gravatar.com/avatar.php?gravatar_id=#{Digest::MD5.hexdigest(email)}&size=#{small ? 32 : 80}&d=monsterid"
    else
      small ? "avatar32.png" : "avatar80.png"
    end
  end
  
  def destroy
    # Remove all associated input data
    electricity_accounts.each { |x| x.destroy }
    gas_accounts.each { |x| x.destroy }
    vehicles.each { |x| x.destroy }
    flights.each { |x| x.destroy }
    # Remove completed actions
    completed_actions.each { |x| x.destroy }
    # Remove friend relations
    approved_friendships.each { |x| x.destroy }
    unapproved_friendships.each { |x| x.destroy }
    approved_befriendships.each { |x| x.destroy }
    unapproved_befriendships.each { |x| x.destroy }
    # Remove group memberships
    group_memberships.each { |x| x.destroy }
    # Remove notes
    notes.each { |x| x.destroy }
    # Remove all comments
    authored_comments.each { |x| x.destroy }
    comments.each { |x| x.destroy }
    # Call base
    super
  end
  
  def has_no_accounts
    electricity_accounts.blank? and gas_accounts.blank? and vehicles.blank?
  end

  def has_enough_data_to_analyse
    electricity_accounts.each do |account|
      return true if account.has_enough_data_to_analyse
    end
    gas_accounts.each do |account|
      return true if account.has_enough_data_to_analyse
    end
    vehicles.each do |vehicle|
      return true if vehicle.has_enough_data_to_analyse
    end
    return true if flights.count > 0
    return false
  end

  def all_emissions
    all_emissions_for_account_type(electricity_accounts) + all_emissions_for_account_type(gas_accounts) + all_emissions_for_account_type(vehicles) + all_emissions_for_flights
  end

  def count_elec_readings
    num = 0
    electricity_accounts.each do |account|
      num += account.count_readings
    end
    return num
  end

  def count_gas_readings
    num = 0
    gas_accounts.each do |account|
      num += account.count_readings
    end
    return num
  end

  def count_vehicle_fuel_purchases
    num = 0
    vehicles.each do |account|
      num += account.count_purchases
    end
    return num
  end

  def send_reminder
    # Check that a reminder is required
    return unless needs_reminding?
    # Send reminder if there is an email address confirmed
    unless self.confirmed_email.nil? or self.login.blank? # Can't send reminder to people without a login, because we can't save the reminder time
      UserMailer.reminder(self).deliver
      # Store todays date in reminder field
      self.reminded_at = Time::now
      self.save!
    end
  end

  def name
    return display_name unless display_name.nil? or display_name.blank?
    return login
  end

  def flag
    return country.flag_image unless country.nil?
    return ""
  end

  def image
    return 'admin.png' if admin
    return 'tester.png' if tester
    'user.png'
  end

  def self.find_public(search)    
    search = search.downcase
    like = "%" + search + "%"
    User.where(:public => true).where("(login LIKE ? OR LOWER(email) = ? OR LOWER(display_name) LIKE ?)", like, search, like).order(:login)
  end

  def calculate_totals(period)
    # Calculate start date
    start = Date::today - period + 1
    @date = Date::MONTHNAMES[start.month] + " " + start.day.to_s + ", " + start.year.to_s
    # Store total
    totals = []
    grand_total = 0.0
    # Calculate totals for each emissions source
    all_emissions.each do |item|
      totals << {:name => item[:name], :image => item[:image], :categories => item[:categories], :data => item[:data].calculate_total_over_period(period)}
      grand_total += totals.last[:data][:total]
    end
    # Calculate percentages and yearly totals
    totals.each do |item|
      unless item[:data][:total] == 0
        item[:data][:percentage] = (item[:data][:total] / grand_total) * 100
      end
    end
    # Sort
    totals = totals.sort {|x,y| y[:data][:total] <=> x[:data][:total] }    
    # Add overall total
    total_perday = total_perannum = total_total = 0
    totals.each { |x| total_total += x[:data][:total]; total_perday += x[:data][:perday]; total_perannum += x[:data][:perannum] }
    totals << { :name => "Total", :image => 'chart_pie.png', :data => { :total => total_total, :percentage => 100, :perday => total_perday, :perannum => total_perannum } }
    return totals
  end

  def add_friend(friend)
    # Add to friends list
    unless (friends.include?(friend) or unapproved_friends.include?(friend))
      unapproved_friendships.create(:friend => friend, :approved => false)
      # Send email to friend
      unless friend.confirmed_email.nil? or friend.notify_friend_requests == false
        UserMailer.friend_request(self, friend).deliver
      end
    end
  end

  def remove_friend(friend)
    # Remove friendship relation
    approved_friendships.find_by_friend_id(friend.id).destroy
  end

  def approve_friend_request(friend)
    # Approve the original friend link
    friendship = unapproved_befriendships.find_by_user_id(friend.id)
    if friendship
      friendship.approve
      # Add a reciprocal link to the friend
      approved_friendships.create(:friend => friend, :approved => true)
    end
  end
  
  def reject_friend_request(friend)
    # Reject the original friend link
    unapproved_befriendships.find_by_user_id(friend.id).reject
  end

  def reset_login_key!
    if login_key.nil? or login_key_expires_at < Time.now
          self.login_key = Digest::SHA1.hexdigest(Time.now.to_s + password_hash.to_s + rand(123456789).to_s).to_s
          self.login_key_expires_at = Time.now.utc+1.year
          save!
        end
        login_key
    end

  def clear_login_key!
        self.login_key = nil
        self.login_key_expires_at = nil
        save!
    end

  def all_notes(limit = nil)
    all_notes_array = []
    all_notes_array += notes.limit(limit).order("date DESC")
    electricity_accounts.each { |acc| all_notes_array += acc.notes.limit(limit).order("date DESC") }
    gas_accounts.each { |acc| all_notes_array += acc.notes.limit(limit).order("date DESC") }
    vehicles.each { |acc| all_notes_array += acc.notes.limit(limit).order("date DESC") }
    return all_notes_array.sort{ |x,y| y.date <=> x.date }
  end

  def annual_emissions
    update_stored_statistics! unless self.annual_emission_total
    total = self.annual_emission_total
    return total
  end

  def update_stored_statistics!(save = true)
    days = Date::today - date_of_first_data
    days = 365 if days > 365
    self.annual_emission_total = calculate_totals(days).last[:data][:perannum] / people_in_household
    self.save(:validate => false) if save
  end

  def needs_reminding?
    return false if reminder_frequency == 0
    cutoff = (reminder_frequency.weeks + 1.day).ago
    return false if created_at > cutoff # Don't remind if the user was created recently
    oldest_new_data = date_of_oldest_new_data
    return false if (not oldest_new_data.nil? and oldest_new_data.to_time > cutoff) # Don't remind if data was entered recently
    return false if (not reminded_at.nil? and reminded_at > cutoff) # Don't remind if the user was reminded recently
    return true
  end

  def needs_more_data
    date = date_of_oldest_new_data
    return true if date.nil?
    date_of_oldest_new_data < 1.week.ago.to_date
  end

  def date_of_first_data
    start = Date::today
    self.electricity_accounts.each { |x| start = x.start_date if x.start_date < start }
    self.gas_accounts.each { |x| start = x.start_date if x.start_date < start }
    self.vehicles.each { |x| start = x.start_date if x.start_date < start }
    self.flights.each { |x| start = x.outbound_on if x.outbound_on < start }
    start
  end
  
private

  def date_of_oldest_new_data
    oldest_date = nil
    electricity_accounts.each do |account|
      account_newest = account.date_of_newest_data
      oldest_date = account_newest if oldest_date.nil? or oldest_date > account_newest
    end
    gas_accounts.each do |account|
      account_newest = account.date_of_newest_data
      oldest_date = account_newest if oldest_date.nil? or oldest_date > account_newest
    end
    # Don't remind for vehicles - they get filled up as and when
    #for account in vehicles
    #  account_newest = account.date_of_newest_data
    #  oldest_date = account_newest if oldest_date > account_newest
    #end
    return oldest_date
  end

  def all_emissions_for_account_type(accounts)
    emissions = []
    # For each account, calculate emissions
    accounts.each do |account|
      # Find the emissions for the account
      emissions << {:name => account.name, :image => account.image, :data => account.emissions, :categories => account.action_categories}
    end
    return emissions    
  end

public

  def all_emissions_for_flights
    return [] if flights.count == 0
    # Adapt data into format used in all_emissions_for_* functions
    [{:name => 'Flights', :image => 'plane.png', :data => flight_emissions, :categories => [ActionCategory.find_by_name("Travel")]}]
  end

  def flight_emissions
    # Initialise result array
    emissiondata = EmissionArray.new
    # Analyse each reading
    flights.order("outbound_on").each do |flight|
      # Add to result array
      days = flight.return_on ? (flight.return_on - flight.outbound_on + 1) : 1
      co2 = flight.kg_of_co2
      emissiondata << { :start => flight.outbound_on - 1,
                        :end => flight.return_on ? flight.return_on : (flight.outbound_on),
                        :co2 => co2,
                        :days => days.to_i,
                        :co2_per_day => co2 / days }
    end    
    # Done - return data
    return emissiondata
  end    

  def to_param
    login
  end

  def points # Experimental
    ranges = [
      [0,1,"Newborn"],
      [1,10,"Beginner"],
      [10,25,"Apprentice"],
      [25,50,"Adept"],
      [50,100,"Master"],
      [100,250,"Expert"],
      [250,500,"Genius"],
      [500,1e100,"Wizard"]
    ]
    breakdown = {}
    breakdown[:entries] = {:value => electricity_readings.count(:conditions => {:automatic => false})+gas_readings.count+vehicle_fuel_purchases.count+flights.count, :description => "measurement"}
    breakdown[:actions] = {:value => actions.inject(0){|t,x| t += x.points}, :description => "actions"}
    breakdown[:sociability] = {:value => (friends.count+groups.count)*2, :description => "sociability"}
    breakdown[:gossip] = {:value => comments.count, :description => "gossip"}
    emissions_limit = -(breakdown.inject(0){|sum,(k,v)| sum += v[:value]} / 2)
    breakdown[:emissions] = {:value => [-((annual_emissions>1000?annual_emissions-1000:annual_emissions)/10).to_i,emissions_limit].max, :description => "emissions"}
    total = breakdown.inject(0){|sum,(k,v)| sum += v[:value]}
    level = ranges.find{|x| total >= x[0] && total < x[1]}
    {
      :total => total,
      :breakdown => breakdown,
      :level => level,
      :percentage => level != ranges.last ? (((total - level[0]).to_f / (level[1] - level[0]).to_f) * 100).to_i : 100
    }
  end

  # initialize the multipass object
  def self.multipass
    @multipass ||= MultiPass.new('carbondiet', ENV['MULTIPASS_API_KEY'])
  end

  # create a multipass for this user object
  def multipass
    self.class.multipass.encode(:email => email, :name => name, :expires => 30.minutes.from_now, :external_url => "http://www.carbondiet.org/profile/#{login}")
  end

private

  def generate_salt
    if self.password_salt.blank?
      self.password_salt = [Array.new(6){rand(256).chr}.join].pack("m").chomp
    end
  end

end