Thermometer

Thermometer measures heat on your ActiveRecord Models and Associations

Download .zip Download .tar.gz View on GitHub

Thermometer

Code Climate Dependency Status Coverage Status Gem Version

Thermometer measures heat on your ActiveRecord Models. This initial release is for Rails 4, not yet tested for Rails 3.

Mixins

This plugin introduces two mixins to your recipe book:

  1. acts_as_thermometer : Extends Instance with temperature methods
  2. measure_temperature_for : Allows customization on an association

Motivation

This plugin was started as a way for Bots to quickly assess the state of a User across many dimensions of Game play to determine automated actions for retention. For example, if a User went cold with regards to reading messages, a reminder could be sent. Rather than dealing with dates and duration, coding uses a simplified syntax, for example :

user = User.first
user.*call_to_some_action* if user.unread_messages.has_temperature?(:cold)  # false|true
user.*call_to_some_action* if user.unread_messages.is_colder_than?(:warm)   # false|true

Installation

In your gemfile

gem "thermometer"
#or to pick up a dev branch
gem "temperature"  , github: 'blotto/temperature', :branch => "first_release"

Then at the command line

bundle update --source temperature
rails generate temperature:install

Configuration

After install a YAML file is placed in the config directory, config/thermometer.yml. For a detailed description of the available options, read the comments within the YAML file.

ActiveRecord Dependency

Models must have the managed columns created_at and updated_at in order to evaluate the temperature.

Usage

Measure the temperature on the Class , and on an Instance

class User < ActiveRecord::Base

  acts_as_thermometer

end
User.has_temperature        # lukewarm
User.is_colder_than? :warm  # false
User.is_warmer_than? :cold  # true

User.first.has_temperature        # lukewarm
User.first.is_colder_than? :warm  # false
User.first.is_warmer_than? :cold  # true

Measure the temperature on any association. NOTE: declare measures_temperature_for after your associations!

class User < ActiveRecord::Base

  acts_as_thermometer

  has_many :messages

  has_many :oldest_messages, -> {where('created_at < ?', 4.months.ago)} , class_name: "Message"
  has_many :recent_messages, -> {where('created_at > ? AND created_at < ?', 4.months.ago , 1.month.ago)} , class_name: "Message"
  has_many :newest_messages, -> {where('created_at > ?', 1.month.ago)} , class_name: "Message"

  measures_temperature_for :messages, :oldest_messages , :recent_messages , :newest_messages

 end
User.first.messages.has_temperature               # temperate
User.first.oldest_messages.has_temperature        # frigid
User.first.recent_messages.is_colder_than? :warm  # false
User.first.newest_messages.is_warmer_than? :cold  # true

Method Chaining

You can check the temperature on scopes...

class User < ActiveRecord::Base

  acts_as_thermometer

  class << self
      def name_like(substring)
        where("name LIKE '%#{substring}%'")
      end
   end

   def last_five_messages
     messages.limit(5)
   end

 end
User.name_like("Ms.").has_temperature               # temperate
User.name_like("Ms.").first.last_five_messages.has_temperature # warm

Reference Date

The temperature is measured by the age of a record or the average age of a set of records from a reference date. By default, this reference date is DateTime.now. However you can change the reference date when making calls to the three main methods of this Gem. For example , on has_temperature :

User.first.has_temperature
 => "frosty"
User.first.has_temperature date_reference: DateTime.now # equivalent to above call
 => "frosty"
User.first.has_temperature date_reference: DateTime.now - 2.month # what was the temperature 2 months ago.
 => "temperate"

User.last.oldest_messages.has_temperature
 => "frosty"
 User.last.oldest_messages.has_temperature date_reference: DateTime.now - 2.months
 => "chilly"

Why is this useful?

Iterating through a range of dates to find temperatures can provide data for a heat map. This code example iterates over the last month for a User.

heat_map = Hash.new
first = DateTime.now - 1.month
last  = DateTime.now
first.upto(last).each do |d| heat_map[d.strftime('%F')] = ( User.first.has_temperature date_reference: d ) end
heat_map
=> {"2014-01-25"=>"cold", "2014-01-26"=>"cold", "2014-01-27"=>"cold", "2014-01-28"=>"cold", "2014-01-29"=>"cold",
    "2014-01-30"=>"cold", "2014-01-31"=>"cold", "2014-02-01"=>"cold", "2014-02-02"=>"cold", "2014-02-03"=>"cold",
    "2014-02-04"=>"cold", "2014-02-05"=>"cold", "2014-02-06"=>"cold", "2014-02-07"=>"cold", "2014-02-08"=>"cold",
    "2014-02-09"=>"cold", "2014-02-10"=>"cold", "2014-02-11"=>"cold", "2014-02-12"=>"cold", "2014-02-13"=>"cold",
    "2014-02-14"=>"frosty", "2014-02-15"=>"frosty", "2014-02-16"=>"frosty", "2014-02-17"=>"frosty",
    "2014-02-18"=>"frosty", "2014-02-19"=>"frosty", "2014-02-20"=>"frosty", "2014-02-21"=>"frosty",
    "2014-02-22"=>"frosty", "2014-02-23"=>"frosty", "2014-02-24"=>"frosty", "2014-02-25"=>"frosty"}

Options

In most cases, how you measure the temperature is consistent across Models, and Associations, therefore keep your customizations global by changing options in the YAML file. However, you could pass through customizations specific to a particular use case.

Configs can be passed through in a number of scenarios.

Overriding options via measures_temperature_for

class User < ActiveRecord::Base

  acts_as_thermometer

  has_many :messages

  has_many :oldest_messages, -> {where('created_at < ?', 4.months.ago)} , class_name: "Message"
  has_many :recent_messages, -> {where('created_at > ? AND created_at < ?', 4.months.ago , 1.month.ago)} , class_name: "Message"
  has_many :newest_messages, -> {where('created_at > ?', 1.month.ago)} , class_name: "Message"

  measures_temperature_for :messages, {:explicit=>true, :date => :updated_at}  #only use options defined here
  measures_temperature_for :recent_messages, {:date => 'updated_at'} #use this date, and default options
  measures_temperature_for :newest_messages,
                    {:date => 'messages.updated_at', :sample => 3} #clearly defined date field, sampling 3 records

 end

> User.first.messages.has_temperature
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
   (1.4ms)  SELECT "messages"."updated_at" FROM "messages" WHERE "messages"."user_id" = ?  [["user_id", 14035331]]
 => "frosty"

> User.first.newest_messages.has_temperature
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
    (0.6ms)  SELECT messages.created_at FROM "messages" WHERE "messages"."user_id" = ? AND (created_at >
    '2014-01-19 23:44:59.413221') ORDER BY messages.created_at DESC LIMIT 3  [["user_id", 14035331]]
 => :none

Overriding options via method calls

You can pass options on all available methods.

> User.first.newest_messages.has_temperature :sample => 5
  User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
   (0.4ms)  SELECT messages.created_at FROM "messages" WHERE "messages"."user_id" = ? AND (created_at > '2014-01-20 00:04:54.558920') ORDER BY messages.created_at DESC LIMIT 5  [["user_id", 14035331]]
 => :none

> User.first.messages.has_temperature :date => 'created_at'
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
   (0.4ms)  SELECT created_at FROM "messages" WHERE "messages"."user_id" = ?  [["user_id", 14035331]]
 => "freezing"

> User.is_colder_than? :warm, :sample => 5
   (0.4ms)  SELECT updated_at FROM "users" ORDER BY updated_at DESC LIMIT 5
 => false

> User.first.is_colder_than? :warm, :sample => 5
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
 => true

Using AREL Relations directly

You can chain methods on relations. Note, that without explicitly excluding them, defaults will be applied. This example, only samples the last record in the ActiveRecord collection because the default sample size is 1.

> User.where('created_at > ?', (Time.now - 3.weeks)).has_temperature
   (0.5ms)  SELECT updated_at FROM "users" WHERE (created_at > '2014-01-30 00:18:13.260406') ORDER BY updated_at DESC LIMIT 1
 => "lukewarm"

However, you can disable this by passing the option :explicit=>true and sample the entire set :

> User.where('created_at > ?', (Time.now - 3.weeks)).has_temperature :explicit=>true
   (0.4ms)  SELECT updated_at FROM "users" WHERE (created_at > '2014-01-30 00:18:33.207988')
 => "temperate"

You can still apply other options :

> User.where('created_at > ?', (Time.now - 3.weeks)).has_temperature :explicit=>true , :date => :updated_at
   (28.7ms)  SELECT "users"."updated_at" FROM "users" WHERE (created_at > '2014-01-30 00:25:23.563479')
 => "temperate"

> User.where('created_at > ?', (Time.now - 3.weeks)).has_temperature :explicit=>true , :date => :updated_at, :sample => 6, :order => 'desc'
   (0.4ms)  SELECT "users"."updated_at" FROM "users" WHERE (created_at > '2014-01-30 00:30:26.123044') ORDER BY updated_at DESC LIMIT 6
 => "temperate"

Copyright

Copyright (c) 2014 Nick Newell. See LICENSE.txt for further details.