Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

updated for #bctampa 2010

  • Loading branch information...
commit b52976c429e661f35407d8ef56435b9f33eab3f8 1 parent 780ecb4
Barry Ezell authored September 15, 2010

Showing 29 changed files with 1,176 additions and 18 deletions. Show diff stats Hide diff stats

  1. 2  .gitignore
  2. 7  app/controllers/talks_controller.rb
  3. 7  app/models/talk.rb
  4. 15  app/views/talks/index.html.haml
  5. 16  app/views/talks/index.json.erb
  6. 2  config/environment.rb
  7. 8  db/migrate/20100912002200_add_day_to_talk.rb
  8. 8  db/migrate/20100912011427_add_deleted_at_to_talks.rb
  9. 4  db/schema.rb
  10. 79  vendor/plugins/acts_as_paranoid/CHANGELOG
  11. 20  vendor/plugins/acts_as_paranoid/MIT-LICENSE
  12. 5  vendor/plugins/acts_as_paranoid/README
  13. 10  vendor/plugins/acts_as_paranoid/RUNNING_UNIT_TESTS
  14. 180  vendor/plugins/acts_as_paranoid/Rakefile
  15. 34  vendor/plugins/acts_as_paranoid/init.rb
  16. 14  vendor/plugins/acts_as_paranoid/lib/caboose/acts/belongs_to_with_deleted_association.rb
  17. 27  vendor/plugins/acts_as_paranoid/lib/caboose/acts/has_many_through_without_deleted_association.rb
  18. 208  vendor/plugins/acts_as_paranoid/lib/caboose/acts/paranoid.rb
  19. 94  vendor/plugins/acts_as_paranoid/lib/caboose/acts/paranoid_find_wrapper.rb
  20. 18  vendor/plugins/acts_as_paranoid/test/database.yml
  21. 19  vendor/plugins/acts_as_paranoid/test/fixtures/categories.yml
  22. 12  vendor/plugins/acts_as_paranoid/test/fixtures/categories_widgets.yml
  23. 9  vendor/plugins/acts_as_paranoid/test/fixtures/taggings.yml
  24. 6  vendor/plugins/acts_as_paranoid/test/fixtures/tags.yml
  25. 8  vendor/plugins/acts_as_paranoid/test/fixtures/widgets.yml
  26. 287  vendor/plugins/acts_as_paranoid/test/paranoid_test.rb
  27. 30  vendor/plugins/acts_as_paranoid/test/schema.rb
  28. 47  vendor/plugins/acts_as_paranoid/test/test_helper.rb
  29. 18  vendor/plugins/haml/init.rb
2  .gitignore
@@ -5,3 +5,5 @@ doc/app/*
5 5
 log/*.log
6 6
 tmp/**/*
7 7
 config/deploy.rb
  8
+deploy.rb
  9
+
7  app/controllers/talks_controller.rb
@@ -2,10 +2,13 @@ class TalksController < ApplicationController
2 2
   before_filter :login_required, :except => :index
3 3
 
4 4
   def index    
5  
-    @talks    = Talk.all
  5
+
  6
+    conditions = (params[:day] ? ["day = ?", params[:day]] : nil)
  7
+    @talks = Talk.all(:conditions => conditions, :include => :room, :with_deleted => true)
6 8
 
7 9
     respond_to do |format|      
8  
-      format.json {}      
  10
+      format.html 
  11
+      format.json
9 12
     end
10 13
   end
11 14
   
7  app/models/talk.rb
... ...
@@ -1,4 +1,6 @@
1 1
 class Talk < ActiveRecord::Base
  2
+  acts_as_paranoid
  3
+
2 4
   belongs_to :room
3 5
   
4 6
   named_scope :by_time,   :order => "time(start_time)"
@@ -12,6 +14,11 @@ class Talk < ActiveRecord::Base
12 14
   validates_presence_of :name, :room_id, :start_time, :end_time
13 15
   validate :timecheck
14 16
 
  17
+  #to make talk.room.name easily accessible to to_json calls
  18
+  def room_name
  19
+    room.name
  20
+  end
  21
+
15 22
   def timecheck
16 23
     min = Date.new(2000,1,1)
17 24
     if start_time && end_time
15  app/views/talks/index.html.haml
... ...
@@ -0,0 +1,15 @@
  1
+%table
  2
+  %thead
  3
+    %tr
  4
+      %th Time
  5
+      %th Talk
  6
+      %th Room
  7
+  %tbody
  8
+    - @talks.each do |t|
  9
+      %tr
  10
+        %td 
  11
+          = talk_time(t)
  12
+        %td
  13
+          = t.name
  14
+        %td
  15
+          = t.room.name
16  app/views/talks/index.json.erb
... ...
@@ -1,15 +1 @@
1  
-{"talks":[
2  
-	<% @talks.each do |t| %>
3  
-		{ 
4  
-			"id": "<%=t.id%>",
5  
-			"name": "<%=t.name%>", 
6  
-			"description": "<%=t.description%>",
7  
-			"who": "<%=t.who%>",
8  
-			"link": "<%=t.url%>",
9  
-			"start_time": "<%=t.start_time.hour%>:<%=t.start_time.min%>",
10  
-			"end_time": "<%=t.end_time.hour%>:<%=t.end_time.min%>",
11  
-			"room_id": "<%=t.room.id%>",
12  
-			"room_name": "<%=t.room.name%>"
13  
-		 }<%= "," unless t == @talks.last %>     
14  
-	<%end%>
15  
-]}
  1
+{ "talks": <%= @talks.to_json :methods => [ :room_name ] %> }
2  config/environment.rb
@@ -9,6 +9,8 @@
9 9
 Rails::Initializer.run do |config|
10 10
   config.time_zone = 'UTC'
11 11
 
  12
+  config.gem "haml"
  13
+
12 14
   config.action_controller.session = {
13 15
     :session_key => '_barcamp_session',
14 16
     :secret      => 'aef25c44352947a8ba3d0dbf06c874e72a03c8d0c76dce0b0fef3c06274dcfb19792cb96a7d32cfe4f22c511293a8544998561fa19ca2a800cb7f5b65cd8bc3f'
8  db/migrate/20100912002200_add_day_to_talk.rb
... ...
@@ -0,0 +1,8 @@
  1
+class AddDayToTalk < ActiveRecord::Migration
  2
+  def self.up
  3
+    add_column :talks, :day, :date
  4
+  end
  5
+
  6
+  def self.down
  7
+  end
  8
+end
8  db/migrate/20100912011427_add_deleted_at_to_talks.rb
... ...
@@ -0,0 +1,8 @@
  1
+class AddDeletedAtToTalks < ActiveRecord::Migration
  2
+  def self.up
  3
+    add_column :talks, :deleted_at, :datetime
  4
+  end
  5
+
  6
+  def self.down
  7
+  end
  8
+end
4  db/schema.rb
@@ -9,7 +9,7 @@
9 9
 #
10 10
 # It's strongly recommended to check this file into your version control system.
11 11
 
12  
-ActiveRecord::Schema.define(:version => 20081008132251) do
  12
+ActiveRecord::Schema.define(:version => 20100912011427) do
13 13
 
14 14
   create_table "rooms", :force => true do |t|
15 15
     t.string   "name"
@@ -36,6 +36,8 @@
36 36
     t.datetime "created_at"
37 37
     t.datetime "updated_at"
38 38
     t.string   "url"
  39
+    t.date     "day"
  40
+    t.datetime "deleted_at"
39 41
   end
40 42
 
41 43
   create_table "users", :force => true do |t|
79  vendor/plugins/acts_as_paranoid/CHANGELOG
... ...
@@ -0,0 +1,79 @@
  1
+* (16 Apr 2009)
  2
+
  3
+Allow :with_deleted and :only_deleted options to work with count and calculate.
  4
+Fixes compatibility with will_paginate. [James Le Cuirot]
  5
+
  6
+* (4 Oct 2007)
  7
+
  8
+Update for Edge rails: remove support for legacy #count args 
  9
+
  10
+* (2 Feb 2007)
  11
+
  12
+Add support for custom primary keys [Jeff Dean]
  13
+
  14
+* (2 July 2006)
  15
+
  16
+Add paranoid delete_all implementation [Marshall Roch]
  17
+
  18
+* (23 May 2006)
  19
+
  20
+Allow setting of future dates for content expiration.
  21
+
  22
+* (15 May 2006)
  23
+
  24
+Added support for dynamic finders
  25
+
  26
+* (28 Mar 2006)
  27
+
  28
+Updated for Rails 1.1.  I love removing code.
  29
+
  30
+  Refactored #find method
  31
+  Nested Scopes
  32
+
  33
+*0.3.1* (20 Dec 2005)
  34
+
  35
+* took out deleted association code for 'chainsaw butchery of base classes' [sorry Erik Terpstra]
  36
+* verified tests pass on Rails 1.0
  37
+
  38
+*0.3* (27 Nov 2005)
  39
+
  40
+* Deleted models will find deleted associations by default now [Erik Terpstra]
  41
+* Added :group as valid option for find [Michael Dabney]
  42
+* Changed the module namespace to Caboose::Acts::Paranoid
  43
+
  44
+*0.2.0* (6 Nov 2005)
  45
+
  46
+* Upgrade to Rails 1.0 RC4.  ActiveRecord::Base#constrain has been replaced with scope_with.
  47
+
  48
+*0.1.7* (22 Oct 2005)
  49
+
  50
+* Added :with_deleted as a valid option of ActiveRecord::Base#find
  51
+
  52
+*0.1.6* (25 Sep 2005)
  53
+
  54
+* Fixed bug where nested constrains would get clobbered after multiple queries
  55
+
  56
+*0.1.5* (22 Sep 2005)
  57
+
  58
+* Fixed bug where acts_as_paranoid would clobber other constrains
  59
+* Simplified acts_as_paranoid mixin including.
  60
+
  61
+*0.1.4* (18 Sep 2005)
  62
+
  63
+* First RubyForge release
  64
+
  65
+*0.1.3* (18 Sep 2005)
  66
+
  67
+* ignore multiple calls to acts_as_paranoid on the same model
  68
+
  69
+*0.1.2* (18 Sep 2005)
  70
+
  71
+* fixed a bug that kept you from selecting the first deleted record
  72
+
  73
+*0.1.1* (18 Sep 2005)
  74
+
  75
+* Fixed bug that kept you from selecting deleted records by ID
  76
+
  77
+*0.1* (17 Sep 2005)
  78
+
  79
+* Initial gem
20  vendor/plugins/acts_as_paranoid/MIT-LICENSE
... ...
@@ -0,0 +1,20 @@
  1
+Copyright (c) 2005 Rick Olson
  2
+
  3
+Permission is hereby granted, free of charge, to any person obtaining
  4
+a copy of this software and associated documentation files (the
  5
+"Software"), to deal in the Software without restriction, including
  6
+without limitation the rights to use, copy, modify, merge, publish,
  7
+distribute, sublicense, and/or sell copies of the Software, and to
  8
+permit persons to whom the Software is furnished to do so, subject to
  9
+the following conditions:
  10
+
  11
+The above copyright notice and this permission notice shall be
  12
+included in all copies or substantial portions of the Software.
  13
+
  14
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  15
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  16
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  17
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  18
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  19
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  20
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
5  vendor/plugins/acts_as_paranoid/README
... ...
@@ -0,0 +1,5 @@
  1
+= acts_as_paranoid
  2
+
  3
+Overrides some basic methods for the current model so that calling #destroy sets a 'deleted_at' field to the current timestamp.  ActiveRecord is required.
  4
+
  5
+http://github.com/technoweenie/acts_as_paranoid
10  vendor/plugins/acts_as_paranoid/RUNNING_UNIT_TESTS
... ...
@@ -0,0 +1,10 @@
  1
+1. Pick Rails version.  Either dump this plugin in a Rails app and run it from there, or specify it as an ENV var:
  2
+
  3
+  RAILS=2.2.2 rake
  4
+  RAILS=2.2.2 ruby test/paranoid_test.rb
  5
+
  6
+2. Setup your database.  By default sqlite3 is used, and no further setup is necessary.  You can pick any of the listed databases in test/database.yml.  Be sure to create the database first.
  7
+
  8
+  DB=mysql rake
  9
+
  10
+3. Profit!!
180  vendor/plugins/acts_as_paranoid/Rakefile
... ...
@@ -0,0 +1,180 @@
  1
+require 'rubygems'
  2
+
  3
+Gem::manage_gems
  4
+
  5
+require 'rake/rdoctask'
  6
+require 'rake/packagetask'
  7
+require 'rake/gempackagetask'
  8
+require 'rake/testtask'
  9
+require 'rake/contrib/rubyforgepublisher'
  10
+
  11
+PKG_NAME      = 'acts_as_paranoid'
  12
+PKG_VERSION   = '0.3.1'
  13
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
  14
+PROD_HOST     = "technoweenie@bidwell.textdrive.com"
  15
+RUBY_FORGE_PROJECT = 'ar-paranoid'
  16
+RUBY_FORGE_USER    = 'technoweenie'
  17
+
  18
+desc 'Default: run unit tests.'
  19
+task :default => :test
  20
+
  21
+desc 'Test the calculations plugin.'
  22
+Rake::TestTask.new(:test) do |t|
  23
+  t.libs << 'lib'
  24
+  t.pattern = 'test/**/*_test.rb'
  25
+  t.verbose = true
  26
+end
  27
+
  28
+desc 'Generate documentation for the acts_as_paranoid plugin.'
  29
+Rake::RDocTask.new do |rdoc|
  30
+  rdoc.rdoc_dir = 'html'
  31
+  rdoc.title    = "#{PKG_NAME} -- protect your ActiveRecord objects from accidental deletion"
  32
+  rdoc.options << '--line-numbers --inline-source --accessor cattr_accessor=object'
  33
+  rdoc.template = "#{ENV['template']}.rb" if ENV['template']
  34
+  rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS')
  35
+  rdoc.rdoc_files.include('lib/**/*.rb')
  36
+end
  37
+
  38
+spec = Gem::Specification.new do |s|
  39
+  s.name            = PKG_NAME
  40
+  s.version         = PKG_VERSION
  41
+  s.platform        = Gem::Platform::RUBY
  42
+  s.summary         = "acts_as_paranoid keeps models from actually being deleted by setting a deleted_at field."
  43
+  s.files           = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS)
  44
+  s.files.delete      "acts_as_paranoid_plugin.sqlite.db"
  45
+  s.files.delete      "acts_as_paranoid_plugin.sqlite3.db"
  46
+  s.require_path    = 'lib'
  47
+  s.autorequire     = 'acts_as_paranoid'
  48
+  s.has_rdoc        = true
  49
+  s.test_files      = Dir['test/**/*_test.rb']
  50
+  s.author          = "Rick Olson"
  51
+  s.email           = "technoweenie@gmail.com"
  52
+  s.homepage        = "http://techno-weenie.net"
  53
+end
  54
+
  55
+Rake::GemPackageTask.new(spec) do |pkg|
  56
+  pkg.need_tar = true
  57
+end
  58
+
  59
+desc "Publish the API documentation"
  60
+task :pdoc => [:rdoc] do
  61
+  Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
  62
+end
  63
+
  64
+desc 'Publish the gem and API docs'
  65
+task :publish => [:pdoc, :rubyforge_upload]
  66
+
  67
+desc "Publish the release files to RubyForge."
  68
+task :rubyforge_upload => :package do
  69
+  files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
  70
+
  71
+  if RUBY_FORGE_PROJECT then
  72
+    require 'net/http'
  73
+    require 'open-uri'
  74
+
  75
+    project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
  76
+    project_data = open(project_uri) { |data| data.read }
  77
+    group_id = project_data[/[?&]group_id=(\d+)/, 1]
  78
+    raise "Couldn't get group id" unless group_id
  79
+
  80
+    # This echos password to shell which is a bit sucky
  81
+    if ENV["RUBY_FORGE_PASSWORD"]
  82
+      password = ENV["RUBY_FORGE_PASSWORD"]
  83
+    else
  84
+      print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
  85
+      password = STDIN.gets.chomp
  86
+    end
  87
+
  88
+    login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
  89
+      data = [
  90
+        "login=1",
  91
+        "form_loginname=#{RUBY_FORGE_USER}",
  92
+        "form_pw=#{password}"
  93
+      ].join("&")
  94
+      http.post("/account/login.php", data)
  95
+    end
  96
+
  97
+    cookie = login_response["set-cookie"]
  98
+    raise "Login failed" unless cookie
  99
+    headers = { "Cookie" => cookie }
  100
+
  101
+    release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
  102
+    release_data = open(release_uri, headers) { |data| data.read }
  103
+    package_id = release_data[/[?&]package_id=(\d+)/, 1]
  104
+    raise "Couldn't get package id" unless package_id
  105
+
  106
+    first_file = true
  107
+    release_id = ""
  108
+
  109
+    files.each do |filename|
  110
+      basename  = File.basename(filename)
  111
+      file_ext  = File.extname(filename)
  112
+      file_data = File.open(filename, "rb") { |file| file.read }
  113
+
  114
+      puts "Releasing #{basename}..."
  115
+
  116
+      release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
  117
+        release_date = Time.now.strftime("%Y-%m-%d %H:%M")
  118
+        type_map = {
  119
+          ".zip"    => "3000",
  120
+          ".tgz"    => "3110",
  121
+          ".gz"     => "3110",
  122
+          ".gem"    => "1400"
  123
+        }; type_map.default = "9999"
  124
+        type = type_map[file_ext]
  125
+        boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
  126
+
  127
+        query_hash = if first_file then
  128
+          {
  129
+            "group_id" => group_id,
  130
+            "package_id" => package_id,
  131
+            "release_name" => PKG_FILE_NAME,
  132
+            "release_date" => release_date,
  133
+            "type_id" => type,
  134
+            "processor_id" => "8000", # Any
  135
+            "release_notes" => "",
  136
+            "release_changes" => "",
  137
+            "preformatted" => "1",
  138
+            "submit" => "1"
  139
+          }
  140
+        else
  141
+          {
  142
+            "group_id" => group_id,
  143
+            "release_id" => release_id,
  144
+            "package_id" => package_id,
  145
+            "step2" => "1",
  146
+            "type_id" => type,
  147
+            "processor_id" => "8000", # Any
  148
+            "submit" => "Add This File"
  149
+          }
  150
+        end
  151
+
  152
+        query = "?" + query_hash.map do |(name, value)|
  153
+          [name, URI.encode(value)].join("=")
  154
+        end.join("&")
  155
+
  156
+        data = [
  157
+          "--" + boundary,
  158
+          "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
  159
+          "Content-Type: application/octet-stream",
  160
+          "Content-Transfer-Encoding: binary",
  161
+          "", file_data, ""
  162
+          ].join("\x0D\x0A")
  163
+
  164
+        release_headers = headers.merge(
  165
+          "Content-Type" => "multipart/form-data; boundary=#{boundary}"
  166
+        )
  167
+
  168
+        target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
  169
+        http.post(target + query, data, release_headers)
  170
+      end
  171
+
  172
+      if first_file then
  173
+        release_id = release_response.body[/release_id=(\d+)/, 1]
  174
+        raise("Couldn't get release id") unless release_id
  175
+      end
  176
+
  177
+      first_file = false
  178
+    end
  179
+  end
  180
+end
34  vendor/plugins/acts_as_paranoid/init.rb
... ...
@@ -0,0 +1,34 @@
  1
+class << ActiveRecord::Base
  2
+  def belongs_to_with_deleted(association_id, options = {})
  3
+    with_deleted = options.delete :with_deleted
  4
+    returning belongs_to_without_deleted(association_id, options) do
  5
+      if with_deleted
  6
+        reflection = reflect_on_association(association_id)
  7
+        association_accessor_methods(reflection,            Caboose::Acts::BelongsToWithDeletedAssociation)
  8
+        association_constructor_method(:build,  reflection, Caboose::Acts::BelongsToWithDeletedAssociation)
  9
+        association_constructor_method(:create, reflection, Caboose::Acts::BelongsToWithDeletedAssociation)
  10
+      end
  11
+    end
  12
+  end
  13
+  
  14
+  def has_many_without_deleted(association_id, options = {}, &extension)
  15
+    with_deleted = options.delete :with_deleted
  16
+    returning has_many_with_deleted(association_id, options, &extension) do
  17
+      if options[:through] && !with_deleted
  18
+        reflection = reflect_on_association(association_id)
  19
+        collection_reader_method(reflection, Caboose::Acts::HasManyThroughWithoutDeletedAssociation)
  20
+        collection_accessor_methods(reflection, Caboose::Acts::HasManyThroughWithoutDeletedAssociation, false)
  21
+      end
  22
+    end
  23
+  end
  24
+  
  25
+  alias_method_chain :belongs_to, :deleted
  26
+  alias_method :has_many_with_deleted, :has_many
  27
+  alias_method :has_many, :has_many_without_deleted
  28
+  alias_method :exists_with_deleted?, :exists?
  29
+end
  30
+ActiveRecord::Base.send :include, Caboose::Acts::Paranoid
  31
+ActiveRecord::Base.send :include, Caboose::Acts::ParanoidFindWrapper
  32
+class << ActiveRecord::Base
  33
+  alias_method_chain :acts_as_paranoid, :find_wrapper
  34
+end
14  vendor/plugins/acts_as_paranoid/lib/caboose/acts/belongs_to_with_deleted_association.rb
... ...
@@ -0,0 +1,14 @@
  1
+module Caboose # :nodoc:
  2
+  module Acts # :nodoc:
  3
+    class BelongsToWithDeletedAssociation < ActiveRecord::Associations::BelongsToAssociation
  4
+      private
  5
+        def find_target
  6
+          @reflection.klass.find_with_deleted(
  7
+            @owner[@reflection.primary_key_name], 
  8
+            :conditions => conditions,
  9
+            :include    => @reflection.options[:include]
  10
+          )
  11
+        end
  12
+    end
  13
+  end
  14
+end
27  vendor/plugins/acts_as_paranoid/lib/caboose/acts/has_many_through_without_deleted_association.rb
... ...
@@ -0,0 +1,27 @@
  1
+module Caboose # :nodoc:
  2
+  module Acts # :nodoc:
  3
+    class HasManyThroughWithoutDeletedAssociation < ActiveRecord::Associations::HasManyThroughAssociation
  4
+      protected
  5
+        def current_time
  6
+          ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
  7
+        end
  8
+
  9
+        def construct_conditions
  10
+          return super unless @reflection.through_reflection.klass.paranoid?
  11
+          table_name = @reflection.through_reflection.table_name
  12
+          conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
  13
+            "#{table_name}.#{attr} = #{value}"
  14
+          end
  15
+
  16
+          deleted_attribute = @reflection.through_reflection.klass.deleted_attribute
  17
+          quoted_current_time = @reflection.through_reflection.klass.quote_value(
  18
+            current_time,
  19
+            @reflection.through_reflection.klass.columns_hash[deleted_attribute.to_s])
  20
+          conditions << "#{table_name}.#{deleted_attribute} IS NULL OR #{table_name}.#{deleted_attribute} > #{quoted_current_time}"
  21
+
  22
+          conditions << sql_conditions if sql_conditions
  23
+          "(" + conditions.join(') AND (') + ")"
  24
+        end
  25
+    end
  26
+  end
  27
+end
208  vendor/plugins/acts_as_paranoid/lib/caboose/acts/paranoid.rb
... ...
@@ -0,0 +1,208 @@
  1
+module Caboose #:nodoc:
  2
+  module Acts #:nodoc:
  3
+    # Overrides some basic methods for the current model so that calling #destroy sets a 'deleted_at' field to the current timestamp.
  4
+    # This assumes the table has a deleted_at date/time field.  Most normal model operations will work, but there will be some oddities.
  5
+    #
  6
+    #   class Widget < ActiveRecord::Base
  7
+    #     acts_as_paranoid
  8
+    #   end
  9
+    #
  10
+    #   Widget.find(:all)
  11
+    #   # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
  12
+    #
  13
+    #   Widget.find(:first, :conditions => ['title = ?', 'test'], :order => 'title')
  14
+    #   # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL AND title = 'test' ORDER BY title LIMIT 1
  15
+    #
  16
+    #   Widget.find_with_deleted(:all)
  17
+    #   # SELECT * FROM widgets
  18
+    #
  19
+    #   Widget.find_only_deleted(:all)
  20
+    #   # SELECT * FROM widgets WHERE widgets.deleted_at IS NOT NULL
  21
+    #
  22
+    #   Widget.find_with_deleted(1).deleted?
  23
+    #   # Returns true if the record was previously destroyed, false if not 
  24
+    #
  25
+    #   Widget.count
  26
+    #   # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NULL
  27
+    #
  28
+    #   Widget.count ['title = ?', 'test']
  29
+    #   # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NULL AND title = 'test'
  30
+    #
  31
+    #   Widget.count_with_deleted
  32
+    #   # SELECT COUNT(*) FROM widgets
  33
+    #
  34
+    #   Widget.count_only_deleted
  35
+    #   # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NOT NULL
  36
+    #
  37
+    #   Widget.delete_all
  38
+    #   # UPDATE widgets SET deleted_at = '2005-09-17 17:46:36'
  39
+    #
  40
+    #   Widget.delete_all!
  41
+    #   # DELETE FROM widgets
  42
+    #
  43
+    #   @widget.destroy
  44
+    #   # UPDATE widgets SET deleted_at = '2005-09-17 17:46:36' WHERE id = 1
  45
+    #
  46
+    #   @widget.destroy!
  47
+    #   # DELETE FROM widgets WHERE id = 1
  48
+    # 
  49
+    module Paranoid
  50
+      def self.included(base) # :nodoc:
  51
+        base.extend ClassMethods
  52
+      end
  53
+
  54
+      module ClassMethods
  55
+        def acts_as_paranoid(options = {})
  56
+          unless paranoid? # don't let AR call this twice
  57
+            cattr_accessor :deleted_attribute
  58
+            self.deleted_attribute = options[:with] || :deleted_at
  59
+            alias_method :destroy_without_callbacks!, :destroy_without_callbacks
  60
+            class << self
  61
+              alias_method :find_every_with_deleted,    :find_every
  62
+              alias_method :calculate_with_deleted,     :calculate
  63
+              alias_method :delete_all!,                :delete_all
  64
+            end
  65
+          end
  66
+          include InstanceMethods
  67
+        end
  68
+
  69
+        def paranoid?
  70
+          self.included_modules.include?(InstanceMethods)
  71
+        end
  72
+      end
  73
+
  74
+      module InstanceMethods #:nodoc:
  75
+        def self.included(base) # :nodoc:
  76
+          base.extend ClassMethods
  77
+        end
  78
+
  79
+        module ClassMethods
  80
+          def find_with_deleted(*args)
  81
+            options = args.extract_options!
  82
+            validate_find_options(options)
  83
+            set_readonly_option!(options)
  84
+            options[:with_deleted] = true # yuck!
  85
+
  86
+            case args.first
  87
+              when :first then find_initial(options)
  88
+              when :all   then find_every(options)
  89
+              else             find_from_ids(args, options)
  90
+            end
  91
+          end
  92
+
  93
+          def find_only_deleted(*args)
  94
+            options = args.extract_options!
  95
+            validate_find_options(options)
  96
+            set_readonly_option!(options)
  97
+            options[:only_deleted] = true # yuck!
  98
+
  99
+            case args.first
  100
+              when :first then find_initial(options)
  101
+              when :all   then find_every(options)
  102
+              else             find_from_ids(args, options)
  103
+            end
  104
+          end
  105
+
  106
+          def exists?(*args)
  107
+            with_deleted_scope { exists_with_deleted?(*args) }
  108
+          end
  109
+
  110
+          def exists_only_deleted?(*args)
  111
+            with_only_deleted_scope { exists_with_deleted?(*args) }
  112
+          end
  113
+
  114
+          def count_with_deleted(*args)
  115
+            calculate_with_deleted(:count, *construct_count_options_from_args(*args))
  116
+          end
  117
+
  118
+          def count_only_deleted(*args)
  119
+            with_only_deleted_scope { count_with_deleted(*args) }
  120
+          end
  121
+
  122
+          def count(*args)
  123
+            with, only = extract_deleted_options(args.last) if args.last.is_a?(Hash)
  124
+            
  125
+            with ? count_with_deleted(*args) :
  126
+              only ? count_only_deleted(*args) :
  127
+                with_deleted_scope { count_with_deleted(*args) }
  128
+          end
  129
+
  130
+          def calculate(*args)
  131
+            with, only = extract_deleted_options(args.last) if args.last.is_a?(Hash)
  132
+            
  133
+            with ? calculate_with_deleted(*args) :
  134
+              only ? calculate_only_deleted(*args) :
  135
+                with_deleted_scope { calculate_with_deleted(*args) }
  136
+          end
  137
+
  138
+          def delete_all(conditions = nil)
  139
+            self.update_all ["#{self.deleted_attribute} = ?", current_time], conditions
  140
+          end
  141
+
  142
+          protected
  143
+            def current_time
  144
+              default_timezone == :utc ? Time.now.utc : Time.now
  145
+            end
  146
+
  147
+            def with_deleted_scope(&block)
  148
+              with_scope({:find => { :conditions => ["#{table_name}.#{deleted_attribute} IS NULL OR #{table_name}.#{deleted_attribute} > ?", current_time] } }, :merge, &block)
  149
+            end
  150
+
  151
+            def with_only_deleted_scope(&block)
  152
+              with_scope({:find => { :conditions => ["#{table_name}.#{deleted_attribute} IS NOT NULL AND #{table_name}.#{deleted_attribute} <= ?", current_time] } }, :merge, &block)
  153
+            end
  154
+
  155
+          private
  156
+            # all find calls lead here
  157
+            def find_every(options)
  158
+              with, only = extract_deleted_options(options)
  159
+
  160
+              with ? find_every_with_deleted(options) :
  161
+                only ? with_only_deleted_scope { find_every_with_deleted(options) } :
  162
+                  with_deleted_scope { find_every_with_deleted(options) }
  163
+            end
  164
+
  165
+            def extract_deleted_options(options)
  166
+              return options.delete(:with_deleted), options.delete(:only_deleted)
  167
+            end
  168
+        end
  169
+
  170
+        def destroy_without_callbacks
  171
+          unless new_record?
  172
+            self.class.update_all self.class.send(:sanitize_sql, ["#{self.class.deleted_attribute} = ?", (self.deleted_at = self.class.send(:current_time))]), ["#{self.class.primary_key} = ?", id]
  173
+          end
  174
+          freeze
  175
+        end
  176
+
  177
+        def destroy_with_callbacks!
  178
+          return false if callback(:before_destroy) == false
  179
+          result = destroy_without_callbacks!
  180
+          callback(:after_destroy)
  181
+          result
  182
+        end
  183
+
  184
+        def destroy!
  185
+          transaction { destroy_with_callbacks! }
  186
+        end
  187
+
  188
+        def deleted?
  189
+          !!read_attribute(:deleted_at)
  190
+        end
  191
+
  192
+        def recover!
  193
+          self.deleted_at = nil
  194
+          save!
  195
+        end
  196
+        
  197
+        def recover_with_associations!(*associations)
  198
+          self.recover!
  199
+          associations.to_a.each do |assoc|
  200
+            self.send(assoc).find_with_deleted(:all).each do |a|
  201
+              a.recover! if a.class.paranoid?
  202
+            end
  203
+          end
  204
+        end
  205
+      end
  206
+    end
  207
+  end
  208
+end
94  vendor/plugins/acts_as_paranoid/lib/caboose/acts/paranoid_find_wrapper.rb
... ...
@@ -0,0 +1,94 @@
  1
+module Caboose #:nodoc:
  2
+  module Acts #:nodoc:
  3
+    # Adds a wrapper find method which can identify :with_deleted or :only_deleted options
  4
+    # and would call the corresponding acts_as_paranoid finders find_with_deleted or
  5
+    # find_only_deleted methods.
  6
+    #
  7
+    # With this wrapper you can easily change from using this pattern:
  8
+    #
  9
+    #   if some_condition_enabling_access_to_deleted_records?
  10
+    #     @post = Post.find_with_deleted(params[:id])
  11
+    #   else
  12
+    #     @post = Post.find(params[:id])
  13
+    #   end
  14
+    #
  15
+    # to this:
  16
+    #
  17
+    #   @post = Post.find(params[:id], :with_deleted => some_condition_enabling_access_to_deleted_records?)
  18
+    #
  19
+    # Examples
  20
+    #
  21
+    #   class Widget < ActiveRecord::Base
  22
+    #     acts_as_paranoid
  23
+    #   end
  24
+    #
  25
+    #   Widget.find(:all)
  26
+    #   # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
  27
+    #
  28
+    #   Widget.find(:all, :with_deleted => false)
  29
+    #   # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
  30
+    #
  31
+    #   Widget.find_with_deleted(:all)
  32
+    #   # SELECT * FROM widgets
  33
+    #
  34
+    #   Widget.find(:all, :with_deleted => true)
  35
+    #   # SELECT * FROM widgets
  36
+    #
  37
+    #   Widget.find_only_deleted(:all)
  38
+    #   # SELECT * FROM widgets WHERE widgets.deleted_at IS NOT NULL
  39
+    #
  40
+    #   Widget.find(:all, :only_deleted => true)
  41
+    #   # SELECT * FROM widgets WHERE widgets.deleted_at IS NOT NULL
  42
+    #
  43
+    #   Widget.find(:all, :only_deleted => false)
  44
+    #   # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
  45
+    #
  46
+    module ParanoidFindWrapper
  47
+      def self.included(base) # :nodoc:
  48
+        base.extend ClassMethods
  49
+      end
  50
+
  51
+      module ClassMethods
  52
+        def acts_as_paranoid_with_find_wrapper(options = {})
  53
+          unless paranoid? # don't let AR call this twice
  54
+            acts_as_paranoid_without_find_wrapper(options)
  55
+            class << self
  56
+              alias_method :find_without_find_wrapper, :find
  57
+              alias_method :validate_find_options_without_find_wrapper, :validate_find_options
  58
+            end
  59
+          end
  60
+          include InstanceMethods
  61
+        end
  62
+      end
  63
+
  64
+      module InstanceMethods #:nodoc:
  65
+        def self.included(base) # :nodoc:
  66
+          base.extend ClassMethods
  67
+        end
  68
+
  69
+        module ClassMethods
  70
+          # This is a wrapper for the regular "find" so you can pass acts_as_paranoid related
  71
+          # options and determine which finder to call.
  72
+          def find(*args)
  73
+            options = args.extract_options!
  74
+            # Determine who to call.
  75
+            finder_option = VALID_PARANOID_FIND_OPTIONS.detect { |key| options.delete(key) } || :without_find_wrapper
  76
+            finder_method = "find_#{finder_option}".to_sym
  77
+            # Put back the options in the args now that they don't include the extended keys.
  78
+            args << options
  79
+            send(finder_method, *args)
  80
+          end
  81
+
  82
+          protected
  83
+
  84
+            VALID_PARANOID_FIND_OPTIONS = [:with_deleted, :only_deleted]
  85
+
  86
+            def validate_find_options(options) #:nodoc:
  87
+              cleaned_options = options.reject { |k, v| VALID_PARANOID_FIND_OPTIONS.include?(k) }
  88
+              validate_find_options_without_find_wrapper(cleaned_options)
  89
+            end
  90
+        end
  91
+      end
  92
+    end
  93
+  end
  94
+end
18  vendor/plugins/acts_as_paranoid/test/database.yml
... ...
@@ -0,0 +1,18 @@
  1
+sqlite:
  2
+  :adapter: sqlite
  3
+  :dbfile: acts_as_paranoid_plugin.sqlite.db
  4
+sqlite3:
  5
+  :adapter: sqlite3
  6
+  :dbfile: acts_as_paranoid_plugin.sqlite3.db
  7
+postgresql:
  8
+  :adapter: postgresql
  9
+  :username: postgres
  10
+  :password: postgres
  11
+  :database: acts_as_paranoid_plugin_test
  12
+  :min_messages: ERROR
  13
+mysql:
  14
+  :adapter: mysql
  15
+  :host: localhost
  16
+  :username: rails
  17
+  :password:
  18
+  :database: acts_as_paranoid_plugin_test
19  vendor/plugins/acts_as_paranoid/test/fixtures/categories.yml
... ...
@@ -0,0 +1,19 @@
  1
+category_1:
  2
+  id: 1
  3
+  widget_id: 1
  4
+  title: 'category 1'
  5
+category_2:
  6
+  id: 2
  7
+  widget_id: 1
  8
+  title: 'category 2'
  9
+  deleted_at: '2005-01-01 00:00:00'
  10
+category_3:
  11
+  id: 3
  12
+  widget_id: 2
  13
+  title: 'category 3'
  14
+  deleted_at: '2005-01-01 00:00:00'
  15
+category_4:
  16
+  id: 4
  17
+  widget_id: 2
  18
+  title: 'category 4'
  19
+  deleted_at: '2005-01-01 00:00:00'
12  vendor/plugins/acts_as_paranoid/test/fixtures/categories_widgets.yml
... ...
@@ -0,0 +1,12 @@
  1
+cw_1:
  2
+  category_id: 1
  3
+  widget_id: 1
  4
+cw_2:
  5
+  category_id: 2
  6
+  widget_id: 1
  7
+cw_3:
  8
+  category_id: 3
  9
+  widget_id: 2
  10
+cw_4:
  11
+  category_id: 4
  12
+  widget_id: 2
9  vendor/plugins/acts_as_paranoid/test/fixtures/taggings.yml
... ...
@@ -0,0 +1,9 @@
  1
+tagging_1:
  2
+  id: 1
  3
+  tag_id: 1
  4
+  widget_id: 1
  5
+  deleted_at: '2005-01-01 00:00:00'
  6
+tagging_2:
  7
+  id: 2
  8
+  tag_id: 2
  9
+  widget_id: 1
6  vendor/plugins/acts_as_paranoid/test/fixtures/tags.yml
... ...
@@ -0,0 +1,6 @@
  1
+tag_1:
  2
+  id: 1
  3
+  name: 'tag 1'
  4
+tag_2:
  5
+  id: 2
  6
+  name: 'tag 1'
8  vendor/plugins/acts_as_paranoid/test/fixtures/widgets.yml
... ...
@@ -0,0 +1,8 @@
  1
+widget_1:
  2
+  id: 1
  3
+  title: 'widget 1'
  4
+widget_2:
  5
+  id: 2
  6
+  title: 'deleted widget 2'
  7
+  deleted_at: '2005-01-01 00:00:00'
  8
+  category_id: 3
287  vendor/plugins/acts_as_paranoid/test/paranoid_test.rb
... ...
@@ -0,0 +1,287 @@
  1
+require File.join(File.dirname(__FILE__), 'test_helper')
  2
+
  3
+class Widget < ActiveRecord::Base
  4
+  acts_as_paranoid
  5
+  has_many :categories, :dependent => :destroy
  6
+  has_and_belongs_to_many :habtm_categories, :class_name => 'Category'
  7
+  has_one :category
  8
+  belongs_to :parent_category, :class_name => 'Category'
  9
+  has_many :taggings
  10
+  has_many :tags, :through => :taggings
  11
+  has_many :any_tags, :through => :taggings, :class_name => 'Tag', :source => :tag, :with_deleted => true
  12
+end
  13
+
  14
+class Category < ActiveRecord::Base
  15
+  belongs_to :widget
  16
+  belongs_to :any_widget, :class_name => 'Widget', :foreign_key => 'widget_id', :with_deleted => true
  17
+  acts_as_paranoid
  18
+
  19
+  def self.search(name, options = {})
  20
+    find :all, options.merge(:conditions => ['LOWER(title) LIKE ?', "%#{name.to_s.downcase}%"])
  21
+  end
  22
+
  23
+  def self.search_with_deleted(name, options = {})
  24
+    find_with_deleted :all, options.merge(:conditions => ['LOWER(title) LIKE ?', "%#{name.to_s.downcase}%"])
  25
+  end
  26
+end
  27
+
  28
+class Tag < ActiveRecord::Base
  29
+  has_many :taggings
  30
+  has_many :widgets, :through => :taggings
  31
+end
  32
+
  33
+class Tagging < ActiveRecord::Base
  34
+  belongs_to :tag
  35
+  belongs_to :widget
  36
+  acts_as_paranoid
  37
+end
  38
+
  39
+class NonParanoidAndroid < ActiveRecord::Base
  40
+end
  41
+
  42
+class ParanoidTest < Test::Unit::TestCase
  43
+  fixtures :widgets, :categories, :categories_widgets, :tags, :taggings
  44
+  
  45
+  def test_should_recognize_with_deleted_option
  46
+    assert_equal [1, 2], Widget.find(:all, :with_deleted => true).collect { |w| w.id }
  47
+    assert_equal [1], Widget.find(:all, :with_deleted => false).collect { |w| w.id }
  48
+  end
  49
+  
  50
+  def test_should_recognize_only_deleted_option
  51
+    assert_equal [2], Widget.find(:all, :only_deleted => true).collect { |w| w.id }
  52
+    assert_equal [1], Widget.find(:all, :only_deleted => false).collect { |w| w.id }
  53
+  end
  54
+  
  55
+  def test_should_exists_with_deleted
  56
+    assert Widget.exists_with_deleted?(2)
  57
+    assert !Widget.exists?(2)
  58
+  end
  59
+
  60
+  def test_should_exists_only_deleted
  61
+    assert Widget.exists_only_deleted?(2)
  62
+    assert !Widget.exists_only_deleted?(1)
  63
+  end
  64
+
  65
+  def test_should_count_with_deleted
  66
+    assert_equal 1, Widget.count
  67
+    assert_equal 2, Widget.count_with_deleted
  68
+    assert_equal 1, Widget.count_only_deleted
  69
+    assert_equal 2, Widget.calculate_with_deleted(:count, :all)
  70
+  end
  71
+
  72
+  def test_should_set_deleted_at
  73
+    assert_equal 1, Widget.count
  74
+    assert_equal 1, Category.count
  75
+    widgets(:widget_1).destroy
  76
+    assert_equal 0, Widget.count
  77
+    assert_equal 0, Category.count
  78
+    assert_equal 2, Widget.calculate_with_deleted(:count, :all)
  79
+    assert_equal 4, Category.calculate_with_deleted(:count, :all)
  80
+  end
  81
+  
  82
+  def test_should_destroy
  83
+    assert_equal 1, Widget.count
  84
+    assert_equal 1, Category.count
  85
+    widgets(:widget_1).destroy!
  86
+    assert_equal 0, Widget.count
  87
+    assert_equal 0, Category.count
  88
+    assert_equal 1, Widget.count_only_deleted
  89
+    assert_equal 1, Widget.calculate_with_deleted(:count, :all)
  90
+    # Category doesn't get destroyed because the dependent before_destroy callback uses #destroy
  91
+    assert_equal 4, Category.calculate_with_deleted(:count, :all)
  92
+  end
  93
+  
  94
+  def test_should_delete_all
  95
+    assert_equal 1, Widget.count
  96
+    assert_equal 2, Widget.calculate_with_deleted(:count, :all)
  97
+    assert_equal 1, Category.count
  98
+    Widget.delete_all
  99
+    assert_equal 0, Widget.count
  100
+    # delete_all doesn't call #destroy, so the dependent callback never fires
  101
+    assert_equal 1, Category.count
  102
+    assert_equal 2, Widget.calculate_with_deleted(:count, :all)
  103
+  end
  104
+  
  105
+  def test_should_delete_all_with_conditions
  106
+    assert_equal 1, Widget.count
  107
+    assert_equal 2, Widget.calculate_with_deleted(:count, :all)
  108
+    Widget.delete_all("id < 3")
  109
+    assert_equal 0, Widget.count
  110
+    assert_equal 2, Widget.calculate_with_deleted(:count, :all)
  111
+  end
  112
+  
  113
+  def test_should_delete_all2
  114
+    assert_equal 1, Category.count
  115
+    assert_equal 4, Category.calculate_with_deleted(:count, :all)
  116
+    Category.delete_all!
  117
+    assert_equal 0, Category.count
  118
+    assert_equal 0, Category.calculate_with_deleted(:count, :all)
  119
+  end
  120
+  
  121
+  def test_should_delete_all_with_conditions2
  122
+    assert_equal 1, Category.count
  123
+    assert_equal 4, Category.calculate_with_deleted(:count, :all)
  124
+    Category.delete_all!("id < 3")
  125
+    assert_equal 0, Category.count
  126
+    assert_equal 2, Category.calculate_with_deleted(:count, :all)    
  127
+  end
  128
+  
  129
+  def test_should_not_count_deleted
  130
+    assert_equal 1, Widget.count
  131
+    assert_equal 1, Widget.count(:all, :conditions => ['title=?', 'widget 1'])
  132
+    assert_equal 2, Widget.calculate_with_deleted(:count, :all)
  133
+    assert_equal 1, Widget.count_only_deleted
  134
+  end
  135
+  
  136
+  def test_should_find_only_deleted
  137
+    assert_equal [2], Widget.find_only_deleted(:all).collect { |w| w.id }
  138
+    assert_equal [1, 2], Widget.find_with_deleted(:all, :order => 'id').collect { |w| w.id }
  139
+  end
  140
+  
  141
+  def test_should_not_find_deleted
  142
+    assert_equal [widgets(:widget_1)], Widget.find(:all)
  143
+    assert_equal [1, 2], Widget.find_with_deleted(:all, :order => 'id').collect { |w| w.id }
  144
+  end
  145
+  
  146
+  def test_should_not_find_deleted_has_many_associations
  147
+    assert_equal 1, widgets(:widget_1).categories.size
  148
+    assert_equal [categories(:category_1)], widgets(:widget_1).categories
  149
+  end
  150
+  
  151
+  def test_should_not_find_deleted_habtm_associations
  152
+    assert_equal 1, widgets(:widget_1).habtm_categories.size
  153
+    assert_equal [categories(:category_1)], widgets(:widget_1).habtm_categories
  154
+  end
  155
+  
  156
+  def test_should_not_find_deleted_has_many_through_associations
  157
+    assert_equal 1, widgets(:widget_1).tags.size
  158
+    assert_equal [tags(:tag_2)], widgets(:widget_1).tags
  159
+  end