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 +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: []
|