acts_as_readable 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 44f09001b617edaeaaeb303302ff7806c787574e
4
+ data.tar.gz: aed97cd3809d30621c534da9f3cbe31c40f92336
5
+ SHA512:
6
+ metadata.gz: ca634e25c362eb653545e2ba6b2213ec636bd038946609e7de37331d67372addbe03c8261df81c88c979c9d17c50439d526b611fea61c739e7d222c95d14d8ca
7
+ data.tar.gz: b5ae7f2ad8c44ab52b340613efa8bd1c3d1fe9635e71b491e9c4904e6e2a59629ffb689f3caf2f821d0d6a30dad278264635335c81b2a4f9e880ad557d683892
data/README.rdoc ADDED
@@ -0,0 +1,35 @@
1
+ == ActsAsReadable
2
+
3
+ ActsAsReadable allows you to create a generic relationship of items which can
4
+ be marked as 'read' by users. This is useful for forums or any other kind of
5
+ situation where you might need to know whether or not a user has seen a particular
6
+ model.
7
+
8
+ === Installation
9
+
10
+ TODO...
11
+
12
+ === Example
13
+
14
+ class Post < ActiveRecord::Base
15
+ acts_as_readable
16
+ end
17
+
18
+ bob = User.find_by_name("bob")
19
+
20
+ bob.readings # => []
21
+
22
+ Post.unread_by(bob) # => [<Post 1>,<Post 2>,<Post 3>...]
23
+ Post.read_by(bob) # => []
24
+
25
+ Post.find(1).read_by?(bob) # => false
26
+ Post.find(1).read_by!(bob) # => <Reading 1>
27
+ Post.find(1).read_by?(bob) # => true
28
+ Post.find(1).readers # => [<User bob>]
29
+
30
+ Post.unread_by(bob) # => [<Post 2>,<Post 3>...]
31
+ Post.read_by(bob) # => [<Post 1>]
32
+
33
+ bob.readings # => [<Reading 1>]
34
+
35
+ Copyright (c) 2012 Culture Code Software Consulting. Released under the MIT license
@@ -0,0 +1,11 @@
1
+ class ActsAsReadableMigrationGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template 'migration.rb', 'db/migrate'
5
+ end
6
+ end
7
+
8
+ def file_name
9
+ "acts_as_readable_migration"
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ class ActsAsReadableMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :readings do |t|
4
+ t.string :readable_type
5
+ t.integer :readable_id
6
+ t.integer :user_id
7
+ t.string :state, :null => false, :default => :read
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :readings, [:readable_id, :readable_type, :user_id], :unique => true
12
+ end
13
+
14
+ def self.down
15
+ drop_table :readings
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ require 'acts_as_readable/acts_as_readable'
2
+ require 'acts_as_readable/reading'
3
+
4
+ ActiveRecord::Base.send :extend, ActsAsReadable::ActMethod
@@ -0,0 +1,124 @@
1
+ module ActsAsReadable
2
+ module ActMethod
3
+ # OPTIONS
4
+ # :cache => the name under which to cache timestamps for a "mark all as read" action to avoid the need to actually create readings for each record marked as read
5
+ def acts_as_readable(options = {})
6
+ class_attribute :acts_as_readable_options
7
+ self.acts_as_readable_options = options
8
+
9
+ has_many :readings, :as => :readable
10
+ has_many :readers, :through => :readings, :source => :user, :conditions => {:readings => {:state => 'read'}}
11
+
12
+ scope :read_by, lambda {|user| ActsAsReadable::HelperMethods.outer_join_readings(self, user).where(ActsAsReadable::HelperMethods.read_conditions(self, user))}
13
+ scope :unread_by, lambda {|user| ActsAsReadable::HelperMethods.outer_join_readings(self, user).where(ActsAsReadable::HelperMethods.unread_conditions(self, user))}
14
+
15
+ extend ActsAsReadable::ClassMethods
16
+ include ActsAsReadable::InstanceMethods
17
+ end
18
+ end
19
+
20
+ module HelperMethods
21
+ def self.read_conditions(readable_class, user)
22
+ ["(readable_type IS NULL AND COALESCE(#{readable_class.table_name}.updated_at < ?, TRUE)) OR (readable_type IS NOT NULL AND COALESCE(readings.updated_at < ?, TRUE)) OR (readings.state = 'read')", all_read_at(readable_class, user), all_read_at(readable_class, user)]
23
+ end
24
+
25
+ def self.unread_conditions(readable_class, user)
26
+ # IF there is no reading and it has been updated since we last read all OR there is an unreading and we haven't read all since then
27
+ ["(readable_type IS NULL AND COALESCE(#{readable_class.table_name}.updated_at > ?, TRUE)) OR (readings.state = 'unread' AND COALESCE(readings.updated_at > ?, TRUE))", all_read_at(readable_class, user), all_read_at(readable_class, user)]
28
+ end
29
+
30
+ def self.all_read_at(readable_class, user)
31
+ user[readable_class.acts_as_readable_options[:cache]] if readable_class.acts_as_readable_options[:cache]
32
+ end
33
+
34
+ def self.outer_join_readings(readable_class, user)
35
+ Reading.joins("LEFT OUTER JOIN readings ON readings.readable_type = '#{readable_class.name}' AND readings.readable_id = #{readable_class.table_name}.id AND readings.user_id = #{user.id}")
36
+ end
37
+ end
38
+
39
+ module ClassMethods
40
+ # Find all the readings of the readables by the user in a single SQL query and cache them in the readables for use in the view.
41
+ def cache_readings_for(readables, user)
42
+ readings = []
43
+ Reading.where(:readable_type => name, :readable_id => readables.collect(&:id), :user_id => user.id).each do |reading|
44
+ readings[reading.readable_id] = reading
45
+ end
46
+
47
+ for readable in readables
48
+ readable.cached_reading = readings[readable.id] || false
49
+ end
50
+
51
+ return readables
52
+ end
53
+
54
+ # Mark all records as read by the user
55
+ # If a :cache option has been set in acts_as_readable, a timestamp will be updated on the user instead of creating individual readings for each record
56
+ def read_by!(user)
57
+ if user.has_attribute?(acts_as_readable_options[:cache])
58
+ Reading.delete_all(:user_id => user.id, :readable_type => name)
59
+ user.update_column(acts_as_readable_options[:cache], Time.now)
60
+ else
61
+ unread_by(user).find_each do |record|
62
+ record.read_by!(user)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ module InstanceMethods
69
+ attr_accessor :cached_reading
70
+
71
+ def acts_like_readable?
72
+ true
73
+ end
74
+
75
+ def read_by!(user)
76
+ # Find an existing reading and update the record so we can know when the thing was first read, and the last time we read it
77
+ reading = Reading.find_or_initialize_by_user_id_and_readable_id_and_readable_type(user.id, self.id, self.class.name)
78
+ reading.updated_at = Time.now # Explicitly set the read time to now in order to force a save in case we haven't changed anything else about the reading
79
+ reading.state = :read
80
+ reading.save!
81
+ rescue ActiveRecord::RecordNotUnique
82
+ # Database-level uniqueness constraint failed.
83
+ return self
84
+ end
85
+
86
+ def unread_by!(user)
87
+ reading = Reading.find_or_initialize_by_user_id_and_readable_id_and_readable_type(user.id, self.id, self.class.name)
88
+ reading.state = :unread
89
+ reading.save!
90
+ end
91
+
92
+ def read_by?(user)
93
+ if cached_reading
94
+ cached_reading.read?
95
+ elsif cached_reading == false
96
+ user[acts_as_readable_options[:cache]].to_f > self.updated_at.to_f
97
+ elsif readers.loaded?
98
+ readers.include?(user)
99
+ elsif reading = readings.find_by_user_id(user.id)
100
+ reading.read?
101
+ else
102
+ user[acts_as_readable_options[:cache]].to_f > self.updated_at.to_f
103
+ end
104
+ end
105
+
106
+ # Returns true if the user has read this at least once, but it has been updated since the last reading
107
+ def updated?(user)
108
+ read_by?(user) && !latest_update_read_by?(user)
109
+ end
110
+
111
+ def latest_update_read_by?(user)
112
+ if cached_reading
113
+ cached_reading.updated_at > self.updated_at
114
+ elsif cached_reading == false
115
+ user[acts_as_readable_options[:cache]].to_f > self.updated_at.to_f
116
+ elsif reading = readings.where(:user_id => user.id, :state => :read).first
117
+ reading.updated_at > self.updated_at
118
+ else
119
+ user[acts_as_readable_options[:cache]].to_f > self.updated_at.to_f
120
+ end
121
+ end
122
+ end
123
+ end
124
+
@@ -0,0 +1,15 @@
1
+ class Reading < ActiveRecord::Base
2
+ belongs_to :user
3
+ belongs_to :readable, :polymorphic => true
4
+
5
+ validates_presence_of :user_id, :readable_id, :readable_type
6
+ validates_inclusion_of :state, :in => [:read, :unread, 'read', 'unread']
7
+
8
+ def read?
9
+ self.state == 'read'
10
+ end
11
+
12
+ def unread?
13
+ self.state == 'unread'
14
+ end
15
+ end
@@ -0,0 +1,81 @@
1
+ `createdb acts_as_readable_test`
2
+ require 'spec_helper'
3
+
4
+ describe 'acts_as_readable' do
5
+ before(:each) do
6
+ @comment = Comment.create
7
+ @user = User.create
8
+ end
9
+
10
+ describe "the unread scope" do
11
+ it "should return records without readings if the user hasn't 'read all'" do
12
+ Comment.unread_by(@user).exists?(@comment).should be_true
13
+ end
14
+
15
+ it "should return records explicitly marked as unread if the user hasn't 'read all'" do
16
+ @comment.unread_by! @user
17
+ Comment.unread_by(@user).exists?(@comment).should be_true
18
+ end
19
+
20
+ it "should return records explicitly marked as unread if the user has 'read all' before the record was marked unread" do
21
+ Comment.read_by! @user
22
+ @comment.unread_by! @user
23
+ Comment.unread_by(@user).exists?(@comment).should be_true
24
+ end
25
+
26
+ it "should not return records explicitly marked as unread if the user has 'read all' after the record was marked unread" do
27
+ @comment.unread_by! @user
28
+ Comment.read_by! @user
29
+ Comment.unread_by(@user).exists?(@comment).should be_false
30
+ end
31
+ end
32
+
33
+ describe "the read scope" do
34
+ it "should return records without readings if the user has 'read all' since the last time the record was updated" do
35
+ Comment.read_by! @user
36
+ Comment.read_by(@user).exists?(@comment).should be_true
37
+ end
38
+
39
+ it "should return records explicitly marked as read if the user hasn't 'read all'" do
40
+ @comment.read_by! @user
41
+ Comment.read_by(@user).exists?(@comment).should be_true
42
+ end
43
+
44
+ it "should return records explicitly marked as read if the user has 'read all' before the record was marked unread" do
45
+ Comment.read_by! @user
46
+ @comment.read_by! @user
47
+ Comment.read_by(@user).exists?(@comment).should be_true
48
+ end
49
+
50
+ it "should return records explicitly marked as unread if the user has 'read all' after the record was marked unread" do
51
+ @comment.unread_by! @user
52
+ Comment.read_by! @user
53
+ Comment.read_by(@user).exists?(@comment).should be_true
54
+ end
55
+ end
56
+
57
+ describe "when checking a specific record" do
58
+ it "should return true if the record hasn't explicitly been read, but the user has 'read all' since the last time the record was updated" do
59
+ Comment.read_by! @user
60
+ @comment.read_by?(@user).should be_true
61
+ end
62
+
63
+ it "should return true if the record has been explicitly marked as read and the user hasn't 'read all'" do
64
+ @comment.read_by! @user
65
+ @comment.read_by?(@user).should be_true
66
+ end
67
+
68
+ it "should return false if the user 'read all' before and then marked the record as unread" do
69
+ Comment.read_by! @user
70
+ @comment.unread_by! @user
71
+ @comment.read_by?(@user).should be_false
72
+ end
73
+
74
+ it "should return true if the user has explicitly marked it as unread and then 'reads all'" do
75
+ @comment.unread_by! @user
76
+ Comment.read_by! @user
77
+ @comment.read_by?(@user).should be_true
78
+ end
79
+ end
80
+ end
81
+ `dropdb acts_as_readable_test`
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
2
+ require 'active_record'
3
+ require 'acts_as_readable'
4
+
5
+ ActiveRecord::Base.establish_connection(:adapter => "postgresql", :database => "acts_as_readable_test")
6
+
7
+ ActiveRecord::Schema.define(:version => 0) do
8
+ create_table :users, :force => true do |t|
9
+ t.datetime :comments_read_at
10
+ end
11
+
12
+ create_table :comments, :force => true do |t|
13
+ t.timestamps
14
+ end
15
+
16
+ create_table :readings, :force => true do |t|
17
+ t.belongs_to :readable, :polymorphic => true
18
+ t.belongs_to :user
19
+ t.string :state, :null => false, :default => 'read'
20
+ t.timestamps
21
+ end
22
+ end
23
+
24
+ class User < ActiveRecord::Base
25
+ end
26
+
27
+ class Comment < ActiveRecord::Base
28
+ acts_as_readable :cache => :comments_read_at
29
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_readable
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nicholas Jakobsen
8
+ - Ryan Wallace
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-01-28 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description:
15
+ email: contact@culturecode.ca
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.rdoc
21
+ - generators/acts_as_readable_migration/acts_as_readable_migration_generator.rb
22
+ - generators/acts_as_readable_migration/templates/migration.rb
23
+ - lib/acts_as_readable.rb
24
+ - lib/acts_as_readable/acts_as_readable.rb
25
+ - lib/acts_as_readable/reading.rb
26
+ - spec/acts_as_readable_spec.rb
27
+ - spec/spec_helper.rb
28
+ homepage: http://github.com/culturecode/acts_as_readable
29
+ licenses: []
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 2.2.1
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: Allows records to be marked as readable. Optimized for bulk, 'mark all as
51
+ read' operations
52
+ test_files: []