blotto/thermometer

View on GitHub
README.md

Summary

Maintainability
Test Coverage
Thermometer
=======

[<img src="https://secure.travis-ci.org/blotto/thermometer.png" />](http://travis-ci.org/blotto/thermometer)
[![Code Climate](https://codeclimate.com/github/blotto/thermometer.png)](https://codeclimate.com/github/blotto/thermometer)
[![Dependency Status](https://gemnasium.com/blotto/thermometer.png)](https://gemnasium.com/blotto/thermometer)
[![Coverage Status](https://coveralls.io/repos/blotto/thermometer/badge.png)](https://coveralls.io/r/blotto/thermometer)
[![Gem Version](https://badge.fury.io/rb/thermometer.png)](http://badge.fury.io/rb/thermometer)

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

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

Then at the command line

    bundle update --source thermometer
    rails generate thermometer: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

```ruby
class User < ActiveRecord::Base

  acts_as_thermometer

end
```

```ruby
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!

```ruby
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
```

```ruby
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...

```ruby
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
```

```ruby
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` :

```ruby
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.

```ruby
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

```ruby
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

```

```ruby
> 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.

```ruby
> 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.

```ruby
> 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 :

```ruby
> 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 :

```ruby
> 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.