acts_as_readable 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []