Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial commit

  • Loading branch information...
commit fab25e0ddd32b38ab4f6645da340eb4369d37b06 0 parents
@justinfrench authored
20 MIT-LICENSE
@@ -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 README.textile
@@ -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 Rakefile
@@ -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  init.rb
@@ -0,0 +1,2 @@
+require 'active_time'
+require 'active_record_in_date_range_scope_extension'
1  install.rb
@@ -0,0 +1 @@
+# Install hook code here
12 lib/active_record_in_date_range_scope_extension.rb
@@ -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 lib/active_time.rb
@@ -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 tasks/active_time_tasks.rake
@@ -0,0 +1,4 @@
+# desc "Explaining what the task does"
+# task :active_time do
+# # Task goes here
+# end
8 test/active_time_test.rb
@@ -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  test/test_helper.rb
@@ -0,0 +1,3 @@
+require 'rubygems'
+require 'active_support'
+require 'active_support/test_case'
1  uninstall.rb
@@ -0,0 +1 @@
+# Uninstall hook code here
Please sign in to comment.
Something went wrong with that request. Please try again.