Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
justinfrench committed Dec 29, 2008
0 parents commit fab25e0
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 0 deletions.
20 changes: 20 additions & 0 deletions MIT-LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2008 Justin French

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
98 changes: 98 additions & 0 deletions README.textile
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
h1. ActiveTime

ActiveTime is a Rails plugin that provides a parent object for has_many-ish associations to other ActiveRecord classes, but instead of a foreign key to scope the queries, a date range is used instead.

A year has many posts, a month has many comments, etc.

h2. A few quick examples:

<pre>
# All Posts created in 2008
ActiveTime.new(2008).posts

# All Users created on November 15, 2008
ActiveTime.new(2008, 11, 15).users

# All Comments created between two specific Times
ActiveTime.new(1.year.ago.utc, Time.now.utc).comments

# You aren't stuck with created_at either
ActiveTime.new(2008).posts(:published_at)

# It's a named_scope under the hood, so you can do normal stuff:
ActiveTime.new(2008).posts.public.newest_first.paginate
</pre>

h2. Installation

./script/plugin install git://github.com/justinfrench/active_time.git

h2. Why?

I'm not sure why *you* need it, but I'm building some RESTful controllers that need to present resources in a hierarchy based on dates (rather than a typical has_many/belongs_to association), so I wanted a model for the resource, upon which I could do the usual ActiveRecord has_many associations, scope chaining, pagination, etc.

h2. How?

Firstly, the plugin adds a named scope to ActiveRecord::Base (so it's added to all your AR classes) called in_date_range, which you can use directly if needed. It takes two Time objects in UTC as arguments (start time, end time) and optionally, a third argument that specifies which column the date range applies to (the default is :created_at). Examples:

<pre>
Post.in_date_range(1.year.ago.utc, Time.now_utc)
Post.in_date_range(1.year.ago.utc, Time.now_utc, :published_at)
</pre>

If you have two Time objects for the start and end of the range, ActiveTime is simply a wrapper around this named scope. These both result in the same database query:

<pre>
Post.in_date_range(1.year.ago.utc, Time.now_utc)
ActiveTime.new(1.year.ago.utc, Time.now.utc).posts
</pre>

Given that the second version is two characters *longer*, it's not all that impressive, but what I really needed was to pass in just the start date, or part of one (like params[:year]) and automatically figure out what range I wanted (a whole year, a month, a day).

<pre>
ActiveTime.new(params[:year]).posts
ActiveTime.new(params[:year], params[:month]).posts
</pre>

h2. And there's more!

<pre>
# Get the calculated start and end times:
ActiveTime.new(2008).starting # => Tue Jan 01 00:00:00 UTC 2008
ActiveTime.new(2008).ending # => Wed Dec 31 23:59:59 UTC 2008

# Get a description of the range
ActiveTime.new(2008) # => in 2008
ActiveTime.new(2008, 11) # => in November 2008
ActiveTime.new(2008, 11, 18) # => on November 18 2008
ActiveTime.new(2008, 11, 18, 14) # => from 14:00 to 15:00 on November 18 2008
ActiveTime.new(2008, 11, 18, 14, 18) # => from 14:18 to 14:19 on November 18 2008
ActiveTime.new(2008, 11, 18, 14, 18, 22) # => from 14:18:22 to 14:19:23 on November 18 2008
</pre>


h2. Status

I wrote it and published it to Github in a few hours, so it's really really fresh and not battle tested at all. Working on it!


h2. Next

* tests!
* tidy up the code a bit
* generate and publish the rdoc
* support hour, minute and second
* provide Day, Year and Month wrapper classes
* anything
* parse more date formats for input
* figure out if I need to care about time zones (everything assumes UTC right now)
* figure out if I care about localization
* figure out if I can replace the method_missing magic with something more declarative


h2. Project Info

"ActiveTime is hosted on Github":http://github.com/justinfrench/active_time/, where your contributions, forkings, comments and feedback are greatly welcomed.


Copyright (c) 2008 Justin French, released under the MIT license.
23 changes: 23 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'

desc 'Default: run unit tests.'
task :default => :test

desc 'Test the active_time plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end

desc 'Generate documentation for the active_time plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'ActiveTime'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end
2 changes: 2 additions & 0 deletions init.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require 'active_time'
require 'active_record_in_date_range_scope_extension'
1 change: 1 addition & 0 deletions install.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Install hook code here
12 changes: 12 additions & 0 deletions lib/active_record_in_date_range_scope_extension.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# A new named_scope <tt>in_date_range</tt> is added to ActiveRecord::Base so that all AR classes
# will have a suitable scope for the has_many-style associations. You can also call it directly:
#
# start_date = Time.gm(2006)
# end_date = Time.gm(2007)
# Post.in_date_range(start_date, end_date)
# # => returns all posts with a created_at between start_date and end_date
class ActiveRecord::Base
named_scope :in_date_range, lambda { |start_date, end_date, column_name| {
:conditions => ["#{column_name} between ? and ?", start_date.to_s(:db), end_date.to_s(:db)]
} }
end
107 changes: 107 additions & 0 deletions lib/active_time.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
class ActiveTime

attr_accessor :starting, :ending
alias_method :time, :starting

# Creates an object representing a period of time with a starting and ending
# time. There's a few ways to create the ActiveTime object:
#
# # A whole year (eg Tue Jan 01 00:00:00 UTC 2008 - Wed Dec 31 23:59:59 UTC 2008):
# ActiveTime.new(2008)
#
# # A whole month (eg Sat Nov 01 00:00:00 UTC 2008 - Sun Nov 30 23:59:59 UTC 2008):
# ActiveTime.new(2008, 11)
#
# # A whole day (eg Wed Nov 12 00:00:00 UTC 2008 - Wed Nov 12 23:59:59 UTC 2008)
# ActiveTime.new(2008, 11, 12)
#
# # Any two Time objects:
# ActiveTime.new(Time.now.beginning_of_day, Time.now.end_of_day)
#
# TODO: Allow an hour, minute and even second.
def initialize(*args)
case args.first
when Fixnum, String
@year, @month, @day, @hour, @min, @sec = *args
@starting = Time.gm(year, month, day)
@ending = @starting.send("end_of_#{range}")
when Time
raise(ArgumentError, "both a starting and ending time must be supplied") unless (args[1] && args[1].is_a?(Time))
@starting, @ending = *args
else
raise ArgumentError, "arguments must either be (starting_time, ending_time) or (year,[month,[day],[hour],[min],[sec]])"
end
end

# A symbol representing the range:
#
# * :custom if no year was supplied (Time objects were supplied instead)
# * :year if no month is supplied
# * :month if no day is supplied
# * :day if year, month and day are supplied
# * :custom if a specific start and end Time were supplied
def range
return :custom if @year.nil?
return :year if @month.nil?
return :month if @day.nil?
return :day if @hour.nil?
return :hour
end
memoize :range

# Allows association calls similar to has_many to be called on the ActiveTime object, with the
# missing method name used to infer the class name (posts => Post). We then call in_date_range
# (a named scope which is expected) on the class to get all objects within the date range.
#
# Example method, given a Post class:
#
# def posts
# Post.in_date_range(starting, ending, :created_at)
# end
#
# TODO: memoize the results, to avoid multiple queries for the same method call.
def method_missing(method_name, *args)
if method_name.to_s =~ /[a-z_]+s$/
args[0] ||= :created_at
begin
klass_name = method_name.to_s.singularize.classify
klass = klass_name.constantize
return klass.in_date_range(starting, ending, args[0]) # Post.in_date_range(start_time, end_time, :created_at)
rescue NoMethodError
raise NoMethodError, "expected #{klass_name} to have a named scoped of 'in_date_range'"
rescue NameError
raise NameError, "called #{method_name} on an ActiveTime object, but could not find a #{klass_name} class"
end
else
super
end
end

# Provides a human friendly string description of the date or time range being used. Examples:
#
# ActiveTime.new(2008) # => "in 2008"
# ActiveTime.new(2008, 11) # => "in November 2008"
# ActiveTime.new(2008, 11, 18) # => "on November 18 2008"
# ActiveTime.new(2008, 11, 18, 14) # => "from 14:00 to 15:00 on November 18 2008"
# ActiveTime.new(2008, 11, 18, 14, 18) # => "from 14:18 to 14:19 on November 18 2008"
# ActiveTime.new(2008, 11, 18, 14, 18, 22) # => "from 14:18:22 to 14:19:23 on November 18 2008"
#
# TODO: could be a whole lot DRYer and configurable
def description
case range
when :year
"in #{starting.year}" # in 2008
when :month
"in #{starting.strftime("%B %Y")}" # in November 2008
when :day
"on #{starting.strftime("%B %d, %Y")}" # on November 18, 2008
when :hour, :min, :sec
"between #{starting.strftime("%H:%M")} and #{ending.strftime("%H:%M")} on #{starting.strftime("%B %d, %Y")}" # between 22:00 and 23:00 on November 18, 2008
when :sec
"between #{starting.strftime("%H:%M:%S")} and #{ending.strftime("%H:%M:%S")} on #{starting.strftime("%B %d, %Y")}" # between 22:18 and 23:19 on November 18, 2008
else
"between #{starting.strftime("%B %d %Y %H:%M:%S")} and #{starting.strftime("%B %d %Y %H:%M:%S")}" # from 14:18:22 to 14:19:23 on November 18 2008
end
end

end
4 changes: 4 additions & 0 deletions tasks/active_time_tasks.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# desc "Explaining what the task does"
# task :active_time do
# # Task goes here
# end
8 changes: 8 additions & 0 deletions test/active_time_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'test_helper'

class ActiveTimeTest < ActiveSupport::TestCase
# Replace this with your real tests.
test "the truth" do
assert true
end
end
3 changes: 3 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require 'rubygems'
require 'active_support'
require 'active_support/test_case'
1 change: 1 addition & 0 deletions uninstall.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Uninstall hook code here

0 comments on commit fab25e0

Please sign in to comment.