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 +7 -0
- data/README.rdoc +35 -0
- data/generators/acts_as_readable_migration/acts_as_readable_migration_generator.rb +11 -0
- data/generators/acts_as_readable_migration/templates/migration.rb +17 -0
- data/lib/acts_as_readable.rb +4 -0
- data/lib/acts_as_readable/acts_as_readable.rb +124 -0
- data/lib/acts_as_readable/reading.rb +15 -0
- data/spec/acts_as_readable_spec.rb +81 -0
- data/spec/spec_helper.rb +29 -0
- metadata +52 -0
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,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,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`
|
data/spec/spec_helper.rb
ADDED
@@ -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: []
|