unread 0.5.0 → 0.6.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 +4 -4
- data/README.md +59 -17
- data/lib/unread.rb +2 -1
- data/lib/unread/base.rb +2 -0
- data/lib/unread/read_mark.rb +2 -10
- data/lib/unread/readable.rb +46 -23
- data/lib/unread/readable_scopes.rb +47 -0
- data/lib/unread/reader.rb +36 -6
- data/lib/unread/reader_scopes.rb +29 -0
- data/lib/unread/version.rb +1 -1
- data/spec/base_spec.rb +7 -0
- data/spec/model/reader.rb +1 -1
- data/spec/read_mark_spec.rb +4 -0
- data/spec/readable_spec.rb +142 -1
- data/spec/reader_spec.rb +161 -0
- data/spec/spec_helper.rb +4 -0
- data/unread.gemspec +1 -1
- metadata +9 -6
- data/lib/unread/scopes.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f5498050ac28f45c15fb7fddd5bd1288bf773d68
|
4
|
+
data.tar.gz: 784de80f41de7112583dbc1586d3827830fc20cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf9a770272bfeb81435d347e8a8fafb4471b4e696f9d13a3a363cb82b9167c0d6d74c1422816bb50526dff6554cc104d936b54c2774a25b53e9b20ec48ba2b22
|
7
|
+
data.tar.gz: 5e6961f8caa78f6a3fabd24a9cdb220936eaf8b50a5c6af741c76224b96dfc1f9a4d726f005e40975513c2746f4dba6ebeaff3be9c90d7bc8c138811341240b2
|
data/README.md
CHANGED
@@ -3,10 +3,9 @@ Unread
|
|
3
3
|
|
4
4
|
Ruby gem to manage read/unread status of ActiveRecord objects - and it's fast.
|
5
5
|
|
6
|
-
[](https://travis-ci.org/ledermann/unread)
|
7
|
+
[](https://codeclimate.com/github/ledermann/unread)
|
8
|
+
[](https://coveralls.io/r/ledermann/unread?branch=master)
|
10
9
|
|
11
10
|
## Features
|
12
11
|
|
@@ -58,6 +57,10 @@ rake db:migrate
|
|
58
57
|
```ruby
|
59
58
|
class User < ActiveRecord::Base
|
60
59
|
acts_as_reader
|
60
|
+
|
61
|
+
# or, if only a subset of users are readers:
|
62
|
+
scope :admins, -> { where(:is_admin => true) }
|
63
|
+
acts_as_reader :scope => -> { admins }
|
61
64
|
end
|
62
65
|
|
63
66
|
class Message < ActiveRecord::Base
|
@@ -75,6 +78,14 @@ message1.mark_as_read! :for => current_user
|
|
75
78
|
Message.unread_by(current_user)
|
76
79
|
# => [ message2 ]
|
77
80
|
|
81
|
+
## Get read messages for a given user
|
82
|
+
Message.read_by(current_user)
|
83
|
+
# => [ ]
|
84
|
+
|
85
|
+
message1.mark_as_read! :for => current_user
|
86
|
+
Message.read_by(current_user)
|
87
|
+
# => [ message1 ]
|
88
|
+
|
78
89
|
## Get all messages including the read status for a given user
|
79
90
|
messages = Message.with_read_marks_for(current_user)
|
80
91
|
# => [ message1, message2 ]
|
@@ -87,8 +98,49 @@ Message.mark_as_read! :all, :for => current_user
|
|
87
98
|
Message.unread_by(current_user)
|
88
99
|
# => [ ]
|
89
100
|
|
90
|
-
|
91
|
-
#
|
101
|
+
Message.read_by(current_user)
|
102
|
+
# => [ message1, message2 ]
|
103
|
+
|
104
|
+
## Get users that have not read a given message
|
105
|
+
user1 = User.create!
|
106
|
+
user2 = User.create!
|
107
|
+
|
108
|
+
User.have_not_read(message1)
|
109
|
+
# => [ user1, user2 ]
|
110
|
+
|
111
|
+
message1.mark_as_read! :for => user1
|
112
|
+
User.have_not_read(message1)
|
113
|
+
# => [ user2 ]
|
114
|
+
|
115
|
+
## Get users that have read a given message
|
116
|
+
User.have_read(message1)
|
117
|
+
# => [ user1 ]
|
118
|
+
|
119
|
+
message1.mark_as_read! :for => user2
|
120
|
+
User.have_read(message1)
|
121
|
+
# => [ user1, user2 ]
|
122
|
+
|
123
|
+
Message.mark_as_read! :all, :for => user1
|
124
|
+
User.have_not_read(message1)
|
125
|
+
# => [ ]
|
126
|
+
User.have_not_read(message2)
|
127
|
+
# => [ user2 ]
|
128
|
+
|
129
|
+
User.have_read(message1)
|
130
|
+
# => [ user1, user2 ]
|
131
|
+
User.have_read(message2)
|
132
|
+
# => [ user1 ]
|
133
|
+
|
134
|
+
## Get all users including their read status for a given message
|
135
|
+
users = User.with_read_marks_for(message1)
|
136
|
+
# => [ user1, user2 ]
|
137
|
+
users[0].have_read?(message1)
|
138
|
+
# => true
|
139
|
+
users[1].have_read?(message2)
|
140
|
+
# => false
|
141
|
+
|
142
|
+
# Optional: Cleaning up unneeded markers
|
143
|
+
# Do this in a cron job once a day
|
92
144
|
Message.cleanup_read_marks!
|
93
145
|
```
|
94
146
|
|
@@ -119,7 +171,7 @@ Generated query:
|
|
119
171
|
```sql
|
120
172
|
SELECT messages.*
|
121
173
|
FROM messages
|
122
|
-
LEFT JOIN read_marks ON read_marks.readable_type =
|
174
|
+
LEFT JOIN read_marks ON read_marks.readable_type = "Message"
|
123
175
|
AND read_marks.readable_id = messages.id
|
124
176
|
AND read_marks.user_id = 42
|
125
177
|
AND read_marks.timestamp >= messages.created_at
|
@@ -130,14 +182,4 @@ AND messages.created_at > '2010-10-20 08:50:00'
|
|
130
182
|
Hint: You should add a database index on `messages.created_at`.
|
131
183
|
|
132
184
|
|
133
|
-
## Similar tools
|
134
|
-
|
135
|
-
There are two other gems/plugins doing a similar job:
|
136
|
-
|
137
|
-
* http://github.com/jhnvz/mark_as_read
|
138
|
-
* http://github.com/mbleigh/acts-as-readable
|
139
|
-
|
140
|
-
Unfortunately, both of them have a lack of performance, because they calculate the unread records doing a `find(:all)`, which should be avoided for a large amount of records. This gem is based on a timestamp algorithm and therefore it's very fast.
|
141
|
-
|
142
|
-
|
143
185
|
Copyright (c) 2010-2015 [Georg Ledermann](http://www.georg-ledermann.de), released under the MIT license
|
data/lib/unread.rb
CHANGED
@@ -2,7 +2,8 @@ require 'unread/base'
|
|
2
2
|
require 'unread/read_mark'
|
3
3
|
require 'unread/readable'
|
4
4
|
require 'unread/reader'
|
5
|
-
require 'unread/
|
5
|
+
require 'unread/readable_scopes'
|
6
|
+
require 'unread/reader_scopes'
|
6
7
|
require 'unread/version'
|
7
8
|
|
8
9
|
ActiveRecord::Base.send :include, Unread
|
data/lib/unread/base.rb
CHANGED
data/lib/unread/read_mark.rb
CHANGED
@@ -1,8 +1,5 @@
|
|
1
1
|
class ReadMark < ActiveRecord::Base
|
2
2
|
belongs_to :readable, :polymorphic => true
|
3
|
-
if ActiveRecord::VERSION::MAJOR < 4
|
4
|
-
attr_accessible :readable_id, :user_id, :readable_type, :timestamp
|
5
|
-
end
|
6
3
|
|
7
4
|
validates_presence_of :user_id, :readable_type
|
8
5
|
|
@@ -10,7 +7,7 @@ class ReadMark < ActiveRecord::Base
|
|
10
7
|
scope :single, lambda { where('readable_id IS NOT NULL') }
|
11
8
|
scope :older_than, lambda { |timestamp| where([ 'timestamp < ?', timestamp ]) }
|
12
9
|
|
13
|
-
# Returns the class defined by acts_as_reader
|
10
|
+
# Returns the class and options defined by acts_as_reader
|
14
11
|
class_attribute :reader_class
|
15
12
|
class_attribute :reader_options
|
16
13
|
|
@@ -18,11 +15,6 @@ class ReadMark < ActiveRecord::Base
|
|
18
15
|
class_attribute :readable_classes
|
19
16
|
|
20
17
|
def self.reader_scope
|
21
|
-
|
22
|
-
|
23
|
-
Array(reader_options[:scopes]).each do |scope|
|
24
|
-
result = result.send(scope)
|
25
|
-
end
|
26
|
-
result
|
18
|
+
reader_options[:scope].try(:call) || self
|
27
19
|
end
|
28
20
|
end
|
data/lib/unread/readable.rb
CHANGED
@@ -27,7 +27,8 @@ module Unread
|
|
27
27
|
if global_timestamp && global_timestamp >= timestamp
|
28
28
|
# The object is implicitly marked as read, so there is nothing to do
|
29
29
|
else
|
30
|
-
rm = obj.read_marks.where(:user_id => user.id).first || obj.read_marks.build
|
30
|
+
rm = obj.read_marks.where(:user_id => user.id).first || obj.read_marks.build
|
31
|
+
rm.user_id = user.id
|
31
32
|
rm.timestamp = timestamp
|
32
33
|
rm.save!
|
33
34
|
end
|
@@ -52,36 +53,44 @@ module Unread
|
|
52
53
|
assert_reader_class
|
53
54
|
|
54
55
|
ReadMark.reader_scope.find_each do |user|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
reset_read_marks_for_user(user)
|
63
|
-
end
|
56
|
+
if oldest_timestamp = read_scope(user).unread_by(user).minimum(readable_options[:on])
|
57
|
+
# There are unread items, so update the global read_mark for this user to the oldest
|
58
|
+
# unread item and delete older read_marks
|
59
|
+
update_read_marks_for_user(user, oldest_timestamp)
|
60
|
+
else
|
61
|
+
# There is no unread item, so deletes all markers and move global timestamp
|
62
|
+
reset_read_marks_for_user(user)
|
64
63
|
end
|
65
64
|
end
|
66
65
|
end
|
67
66
|
|
68
67
|
def update_read_marks_for_user(user, timestamp)
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
68
|
+
ReadMark.transaction do
|
69
|
+
# Delete markers OLDER than the given timestamp
|
70
|
+
user.read_marks.where(:readable_type => self.base_class.name).single.older_than(timestamp).delete_all
|
71
|
+
|
72
|
+
# Change the global timestamp for this user
|
73
|
+
rm = user.read_mark_global(self) || user.read_marks.build
|
74
|
+
rm.readable_type = self.base_class.name
|
75
|
+
rm.timestamp = timestamp - 1.second
|
76
|
+
rm.save!
|
77
|
+
end
|
76
78
|
end
|
77
79
|
|
78
80
|
def reset_read_marks_for_all
|
79
81
|
ReadMark.transaction do
|
80
82
|
ReadMark.delete_all :readable_type => self.base_class.name
|
83
|
+
|
84
|
+
# Build a SELECT statement with all relevant readers
|
85
|
+
reader_sql = ReadMark.
|
86
|
+
reader_scope.
|
87
|
+
select("#{ReadMark.reader_scope.quoted_table_name}.#{ReadMark.reader_scope.quoted_primary_key},
|
88
|
+
'#{self.base_class.name}',
|
89
|
+
'#{connection.quoted_date Time.current}'").to_sql
|
90
|
+
|
81
91
|
ReadMark.connection.execute <<-EOT
|
82
|
-
INSERT INTO
|
83
|
-
|
84
|
-
FROM #{ReadMark.reader_class.table_name}
|
92
|
+
INSERT INTO read_marks (user_id, readable_type, timestamp)
|
93
|
+
#{reader_sql}
|
85
94
|
EOT
|
86
95
|
end
|
87
96
|
end
|
@@ -91,8 +100,15 @@ module Unread
|
|
91
100
|
|
92
101
|
ReadMark.transaction do
|
93
102
|
ReadMark.delete_all :readable_type => self.base_class.name, :user_id => user.id
|
94
|
-
|
103
|
+
|
104
|
+
ReadMark.create! do |rm|
|
105
|
+
rm.readable_type = self.base_class.name
|
106
|
+
rm.user_id = user.id
|
107
|
+
rm.timestamp = Time.current
|
108
|
+
end
|
95
109
|
end
|
110
|
+
|
111
|
+
user.forget_memoized_read_mark_global
|
96
112
|
end
|
97
113
|
|
98
114
|
def assert_reader(user)
|
@@ -109,7 +125,7 @@ module Unread
|
|
109
125
|
|
110
126
|
module InstanceMethods
|
111
127
|
def unread?(user)
|
112
|
-
if self.respond_to?(:read_mark_id)
|
128
|
+
if self.respond_to?(:read_mark_id) && read_mark_id_belongs_to?(user)
|
113
129
|
# For use with scope "with_read_marks_for"
|
114
130
|
return false if self.read_mark_id
|
115
131
|
|
@@ -129,7 +145,8 @@ module Unread
|
|
129
145
|
|
130
146
|
ReadMark.transaction do
|
131
147
|
if unread?(user)
|
132
|
-
rm = read_mark(user) || read_marks.build
|
148
|
+
rm = read_mark(user) || read_marks.build
|
149
|
+
rm.user_id = user.id
|
133
150
|
rm.timestamp = self.send(readable_options[:on])
|
134
151
|
rm.save!
|
135
152
|
end
|
@@ -139,6 +156,12 @@ module Unread
|
|
139
156
|
def read_mark(user)
|
140
157
|
read_marks.where(:user_id => user.id).first
|
141
158
|
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def read_mark_id_belongs_to?(user)
|
163
|
+
self.read_mark_user_id == user.id
|
164
|
+
end
|
142
165
|
end
|
143
166
|
end
|
144
167
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Unread
|
2
|
+
module Readable
|
3
|
+
module Scopes
|
4
|
+
def join_read_marks(user)
|
5
|
+
assert_reader(user)
|
6
|
+
|
7
|
+
joins "LEFT JOIN read_marks
|
8
|
+
ON read_marks.readable_type = '#{base_class.name}'
|
9
|
+
AND read_marks.readable_id = #{quoted_table_name}.#{quoted_primary_key}
|
10
|
+
AND read_marks.user_id = #{user.id}
|
11
|
+
AND read_marks.timestamp >= #{quoted_table_name}.#{connection.quote_column_name(readable_options[:on])}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def unread_by(user)
|
15
|
+
result = join_read_marks(user)
|
16
|
+
|
17
|
+
if global_time_stamp = user.read_mark_global(self).try(:timestamp)
|
18
|
+
result = result.where("read_marks.id IS NULL
|
19
|
+
AND #{quoted_table_name}.#{connection.quote_column_name(readable_options[:on])} > ?", global_time_stamp)
|
20
|
+
else
|
21
|
+
result = result.where('read_marks.id IS NULL')
|
22
|
+
end
|
23
|
+
|
24
|
+
result
|
25
|
+
end
|
26
|
+
|
27
|
+
def read_by(user)
|
28
|
+
result = join_read_marks(user)
|
29
|
+
|
30
|
+
if global_time_stamp = user.read_mark_global(self).try(:timestamp)
|
31
|
+
result = result.where("read_marks.id IS NOT NULL
|
32
|
+
OR #{quoted_table_name}.#{connection.quote_column_name(readable_options[:on])} <= ?", global_time_stamp)
|
33
|
+
else
|
34
|
+
result = result.where('read_marks.id IS NOT NULL')
|
35
|
+
end
|
36
|
+
|
37
|
+
result
|
38
|
+
end
|
39
|
+
|
40
|
+
def with_read_marks_for(user)
|
41
|
+
join_read_marks(user).select("#{quoted_table_name}.*,
|
42
|
+
read_marks.id AS read_mark_id,
|
43
|
+
#{user.id} AS read_mark_user_id")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/unread/reader.rb
CHANGED
@@ -1,15 +1,45 @@
|
|
1
1
|
module Unread
|
2
2
|
module Reader
|
3
|
+
module ClassMethods
|
4
|
+
def assert_readable(readable)
|
5
|
+
assert_readable_class
|
6
|
+
|
7
|
+
unless ReadMark.readable_classes.include?(readable.class)
|
8
|
+
raise ArgumentError, "Class #{readable.class.name} is not registered by acts_as_readable."
|
9
|
+
end
|
10
|
+
raise ArgumentError, "The given #{readable.class.name} has no id." unless readable.id
|
11
|
+
end
|
12
|
+
|
13
|
+
def assert_readable_class
|
14
|
+
raise RuntimeError, 'There is no class using acts_as_readable.' unless ReadMark.readable_classes.try(:any?)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
3
18
|
module InstanceMethods
|
4
19
|
def read_mark_global(klass)
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
20
|
+
@read_mark_global ||= {}
|
21
|
+
@read_mark_global[klass] ||= read_marks.where(:readable_type => klass.base_class.name).global.first
|
22
|
+
end
|
23
|
+
|
24
|
+
def forget_memoized_read_mark_global
|
25
|
+
@read_mark_global = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def have_read?(readable)
|
29
|
+
if self.respond_to?(:read_mark_id) && read_mark_id_belongs_to?(readable)
|
30
|
+
# For use with scope "with_read_marks_for"
|
31
|
+
!self.read_mark_id.nil?
|
32
|
+
else
|
33
|
+
!self.class.have_not_read(readable).exists?(self.id)
|
11
34
|
end
|
12
35
|
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def read_mark_id_belongs_to?(readable)
|
40
|
+
self.read_mark_readable_type == readable.class.base_class.name &&
|
41
|
+
(self.read_mark_readable_id.nil? || self.read_mark_readable_id == readable.id)
|
42
|
+
end
|
13
43
|
end
|
14
44
|
end
|
15
45
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Unread
|
2
|
+
module Reader
|
3
|
+
module Scopes
|
4
|
+
def join_read_marks(readable)
|
5
|
+
assert_readable(readable)
|
6
|
+
|
7
|
+
joins "LEFT JOIN read_marks
|
8
|
+
ON read_marks.readable_type = '#{readable.class.base_class.name}'
|
9
|
+
AND (read_marks.readable_id = #{readable.id} OR read_marks.readable_id IS NULL)
|
10
|
+
AND read_marks.user_id = #{quoted_table_name}.#{quoted_primary_key}
|
11
|
+
AND read_marks.timestamp >= '#{connection.quoted_date readable.send(readable.class.readable_options[:on])}'"
|
12
|
+
end
|
13
|
+
|
14
|
+
def have_not_read(readable)
|
15
|
+
join_read_marks(readable).where("read_marks.id IS NULL")
|
16
|
+
end
|
17
|
+
|
18
|
+
def have_read(readable)
|
19
|
+
join_read_marks(readable).where('read_marks.id IS NOT NULL')
|
20
|
+
end
|
21
|
+
|
22
|
+
def with_read_marks_for(readable)
|
23
|
+
join_read_marks(readable).select("#{quoted_table_name}.*, read_marks.id AS read_mark_id,
|
24
|
+
'#{readable.class.base_class.name}' AS read_mark_readable_type,
|
25
|
+
#{readable.id} AS read_mark_readable_id")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/unread/version.rb
CHANGED
data/spec/base_spec.rb
CHANGED
@@ -20,6 +20,13 @@ describe Unread::Base do
|
|
20
20
|
it "should reset read_marks for created reader" do
|
21
21
|
expect(Email.unread_by(@reader)).to be_empty
|
22
22
|
end
|
23
|
+
|
24
|
+
it "should memoize read_mark_global" do
|
25
|
+
expect {
|
26
|
+
rm1 = @reader.read_mark_global(Email)
|
27
|
+
rm2 = @reader.read_mark_global(Email)
|
28
|
+
}.to perform_queries(1)
|
29
|
+
end
|
23
30
|
end
|
24
31
|
|
25
32
|
describe :acts_as_readable do
|
data/spec/model/reader.rb
CHANGED
data/spec/read_mark_spec.rb
CHANGED
data/spec/readable_spec.rb
CHANGED
@@ -21,6 +21,8 @@ describe Unread::Readable do
|
|
21
21
|
|
22
22
|
expect(Email.unread_by(@reader)).to eq [@email2]
|
23
23
|
expect(Email.unread_by(@reader).count).to eq 1
|
24
|
+
|
25
|
+
expect(Email.unread_by(@other_reader)).to eq [@email1, @email2]
|
24
26
|
end
|
25
27
|
|
26
28
|
it "should not allow invalid parameter" do
|
@@ -38,8 +40,119 @@ describe Unread::Readable do
|
|
38
40
|
Email.unread_by(unsaved_reader)
|
39
41
|
}.to raise_error(ArgumentError)
|
40
42
|
end
|
43
|
+
|
44
|
+
describe "should work without any read_marks" do
|
45
|
+
before do
|
46
|
+
ReadMark.delete_all
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should return all objects" do
|
50
|
+
expect(Email.unread_by(@reader)).to eq [@email1, @email2]
|
51
|
+
expect(Email.unread_by(@other_reader)).to eq [@email1, @email2]
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should return unread records" do
|
55
|
+
@email1.mark_as_read! :for => @reader
|
56
|
+
|
57
|
+
expect(Email.unread_by(@reader)).to eq [@email2]
|
58
|
+
expect(Email.unread_by(@reader).count).to eq 1
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should not allow invalid parameter" do
|
62
|
+
[ 42, nil, 'foo', :foo, {} ].each do |not_a_reader|
|
63
|
+
expect {
|
64
|
+
Email.unread_by(not_a_reader)
|
65
|
+
}.to raise_error(ArgumentError)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should not allow unsaved reader" do
|
70
|
+
unsaved_reader = Reader.new
|
71
|
+
|
72
|
+
expect {
|
73
|
+
Email.unread_by(unsaved_reader)
|
74
|
+
}.to raise_error(ArgumentError)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe :read_by do
|
80
|
+
it "should return an empty array" do
|
81
|
+
expect(Email.read_by(@reader)).to be_empty
|
82
|
+
expect(Email.read_by(@other_reader)).to be_empty
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should return read records" do
|
86
|
+
@email1.mark_as_read! :for => @reader
|
87
|
+
|
88
|
+
expect(Email.read_by(@reader)).to eq [@email1]
|
89
|
+
expect(Email.read_by(@reader).count).to eq 1
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should return all records when all read" do
|
93
|
+
Email.mark_as_read! :all, :for => @reader
|
94
|
+
|
95
|
+
expect(Email.read_by(@reader)).to eq [@email1, @email2]
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should not allow invalid parameter" do
|
99
|
+
[ 42, nil, 'foo', :foo, {} ].each do |not_a_reader|
|
100
|
+
expect {
|
101
|
+
Email.read_by(not_a_reader)
|
102
|
+
}.to raise_error(ArgumentError)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should not allow unsaved reader" do
|
107
|
+
unsaved_reader = Reader.new
|
108
|
+
|
109
|
+
expect {
|
110
|
+
Email.read_by(unsaved_reader)
|
111
|
+
}.to raise_error(ArgumentError)
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "should work without any read_marks" do
|
115
|
+
before do
|
116
|
+
ReadMark.delete_all
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should return an empty array" do
|
120
|
+
expect(Email.read_by(@reader)).to be_empty
|
121
|
+
expect(Email.read_by(@other_reader)).to be_empty
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should return read records" do
|
125
|
+
@email1.mark_as_read! :for => @reader
|
126
|
+
|
127
|
+
expect(Email.read_by(@reader)).to eq [@email1]
|
128
|
+
expect(Email.read_by(@reader).count).to eq 1
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should return all records when all read" do
|
132
|
+
Email.mark_as_read! :all, :for => @reader
|
133
|
+
|
134
|
+
expect(Email.read_by(@reader)).to eq [@email1, @email2]
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should not allow invalid parameter" do
|
138
|
+
[ 42, nil, 'foo', :foo, {} ].each do |not_a_reader|
|
139
|
+
expect {
|
140
|
+
Email.read_by(not_a_reader)
|
141
|
+
}.to raise_error(ArgumentError)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should not allow unsaved reader" do
|
146
|
+
unsaved_reader = Reader.new
|
147
|
+
|
148
|
+
expect {
|
149
|
+
Email.read_by(unsaved_reader)
|
150
|
+
}.to raise_error(ArgumentError)
|
151
|
+
end
|
152
|
+
end
|
41
153
|
end
|
42
154
|
|
155
|
+
|
43
156
|
describe :with_read_marks_for do
|
44
157
|
it "should return readables" do
|
45
158
|
expect(Email.with_read_marks_for(@reader).to_a).to eq([@email1, @email2])
|
@@ -97,6 +210,22 @@ describe Unread::Readable do
|
|
97
210
|
expect(emails[1].unread?(@reader)).to be_truthy
|
98
211
|
}.to perform_queries(1)
|
99
212
|
end
|
213
|
+
|
214
|
+
it "should work without any read_marks" do
|
215
|
+
ReadMark.delete_all
|
216
|
+
|
217
|
+
emails = Email.with_read_marks_for(@reader).to_a
|
218
|
+
expect(emails[0].unread?(@reader)).to be_truthy
|
219
|
+
expect(emails[1].unread?(@reader)).to be_truthy
|
220
|
+
end
|
221
|
+
|
222
|
+
it "should work with eager-loaded read marks for the correct reader" do
|
223
|
+
@email1.mark_as_read! :for => @reader
|
224
|
+
|
225
|
+
emails = Email.with_read_marks_for(@reader).to_a
|
226
|
+
expect(emails[0].unread?(@reader)).to be_falsey
|
227
|
+
expect(emails[0].unread?(@other_reader)).to be_truthy
|
228
|
+
end
|
100
229
|
end
|
101
230
|
|
102
231
|
describe '#mark_as_read!' do
|
@@ -158,6 +287,13 @@ describe Unread::Readable do
|
|
158
287
|
expect(@reader.read_marks.single).to eq []
|
159
288
|
end
|
160
289
|
|
290
|
+
it "should reset memoized global read mark" do
|
291
|
+
rm_global = @reader.read_mark_global(Email)
|
292
|
+
|
293
|
+
Email.mark_as_read! :all, :for => @reader
|
294
|
+
expect(@reader.read_mark_global(Email)).not_to eq(rm_global)
|
295
|
+
end
|
296
|
+
|
161
297
|
it "should not allow invalid arguments" do
|
162
298
|
expect {
|
163
299
|
Email.mark_as_read! :foo, :for => @reader
|
@@ -205,7 +341,12 @@ describe Unread::Readable do
|
|
205
341
|
end
|
206
342
|
|
207
343
|
it "should not delete read marks from other readables" do
|
208
|
-
other_read_mark = @reader.read_marks.create!
|
344
|
+
other_read_mark = @reader.read_marks.create! do |rm|
|
345
|
+
rm.readable_type = 'Foo'
|
346
|
+
rm.readable_id = 42
|
347
|
+
rm.timestamp = 5.years.ago
|
348
|
+
end
|
349
|
+
|
209
350
|
Email.cleanup_read_marks!
|
210
351
|
|
211
352
|
expect(ReadMark.exists?(other_read_mark.id)).to be_truthy
|
data/spec/reader_spec.rb
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Unread::Reader do
|
4
|
+
before :each do
|
5
|
+
@reader = Reader.create! :name => 'David'
|
6
|
+
@other_reader = Reader.create :name => 'Matz'
|
7
|
+
wait
|
8
|
+
@email1 = Email.create!
|
9
|
+
wait
|
10
|
+
@email2 = Email.create!
|
11
|
+
end
|
12
|
+
|
13
|
+
describe :have_not_read do
|
14
|
+
it "should return all readers that have not read a given object" do
|
15
|
+
expect(Reader.have_not_read(@email1)).to eq [@reader, @other_reader]
|
16
|
+
expect(Reader.have_not_read(@email2)).to eq [@reader, @other_reader]
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should return *only* the readers that have not read a given object" do
|
20
|
+
@email1.mark_as_read! :for => @reader
|
21
|
+
|
22
|
+
expect(Reader.have_not_read(@email1)).to eq [@other_reader]
|
23
|
+
expect(Reader.have_not_read(@email1).count).to eq 1
|
24
|
+
|
25
|
+
expect(Reader.have_not_read(@email2)).to eq [@reader, @other_reader]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should not allow invalid parameter" do
|
29
|
+
[ 42, nil, 'foo', :foo, {} ].each do |not_a_readable|
|
30
|
+
expect {
|
31
|
+
Reader.have_not_read(not_a_readable)
|
32
|
+
}.to raise_error(ArgumentError)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should not allow unsaved readable" do
|
37
|
+
unsaved_readable = Email.new
|
38
|
+
|
39
|
+
expect {
|
40
|
+
Reader.have_not_read(unsaved_readable)
|
41
|
+
}.to raise_error(ArgumentError)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe :have_read do
|
46
|
+
it "should return an empty array" do
|
47
|
+
expect(Reader.have_read(@email1)).to be_empty
|
48
|
+
expect(Reader.have_read(@email2)).to be_empty
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should return *only* the readers that have read the given object" do
|
52
|
+
@email1.mark_as_read! :for => @reader
|
53
|
+
|
54
|
+
expect(Reader.have_read(@email1)).to eq [@reader]
|
55
|
+
expect(Reader.have_read(@email1).count).to eq 1
|
56
|
+
|
57
|
+
expect(Reader.have_read(@email2)).to be_empty
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should return the reader for all the object when all read" do
|
61
|
+
Email.mark_as_read! :all, :for => @reader
|
62
|
+
|
63
|
+
expect(Reader.have_read(@email1)).to eq [@reader]
|
64
|
+
expect(Reader.have_read(@email1).count).to eq 1
|
65
|
+
|
66
|
+
expect(Reader.have_read(@email2)).to eq [@reader]
|
67
|
+
expect(Reader.have_read(@email2).count).to eq 1
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should not allow invalid parameter" do
|
71
|
+
[ 42, nil, 'foo', :foo, {} ].each do |not_a_readable|
|
72
|
+
expect {
|
73
|
+
Reader.have_read(not_a_readable)
|
74
|
+
}.to raise_error(ArgumentError)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should not allow unsaved readable" do
|
79
|
+
unsaved_readable = Email.new
|
80
|
+
|
81
|
+
expect {
|
82
|
+
Reader.have_read(unsaved_readable)
|
83
|
+
}.to raise_error(ArgumentError)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe :with_read_marks_for do
|
88
|
+
it "should return readers" do
|
89
|
+
expect(Reader.with_read_marks_for(@email1).to_a).to eq([@reader, @other_reader])
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should have elements that respond to :read_mark_id" do
|
93
|
+
all_respond_to_read_mark_id = Reader.with_read_marks_for(@email1).to_a.all? do |reader|
|
94
|
+
reader.respond_to?(:read_mark_id)
|
95
|
+
end
|
96
|
+
|
97
|
+
expect(all_respond_to_read_mark_id).to be_truthy
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should be countable" do
|
101
|
+
expect(Reader.with_read_marks_for(@email1).count(:number)).to eq(2)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should not allow invalid parameter" do
|
105
|
+
[ 42, nil, 'foo', :foo, {} ].each do |not_a_readable|
|
106
|
+
expect {
|
107
|
+
Reader.with_read_marks_for(not_a_readable)
|
108
|
+
}.to raise_error(ArgumentError)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should not allow unsaved readable" do
|
113
|
+
unsaved_readable = Email.new
|
114
|
+
|
115
|
+
expect {
|
116
|
+
Reader.with_read_marks_for(unsaved_readable)
|
117
|
+
}.to raise_error(ArgumentError)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe :have_read? do
|
122
|
+
it "should recognize read objects" do
|
123
|
+
expect(@reader.have_read?(@email1)).to be_falsey
|
124
|
+
expect(@reader.have_read?(@email2)).to be_falsey
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should handle updating object" do
|
128
|
+
@email1.mark_as_read! :for => @reader
|
129
|
+
wait
|
130
|
+
expect(@reader.have_read?(@email1)).to be_truthy
|
131
|
+
|
132
|
+
@email1.update_attributes! :subject => 'changed'
|
133
|
+
expect(@reader.have_read?(@email1)).to be_falsey
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should raise error for invalid argument" do
|
137
|
+
expect {
|
138
|
+
@reader.have_read?(42)
|
139
|
+
}.to raise_error(ArgumentError)
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should work with eager-loaded read marks" do
|
143
|
+
@email1.mark_as_read! :for => @reader
|
144
|
+
|
145
|
+
expect {
|
146
|
+
readers = Reader.with_read_marks_for(@email1).to_a
|
147
|
+
|
148
|
+
expect(readers[0].have_read?(@email1)).to be_truthy
|
149
|
+
expect(readers[1].have_read?(@email1)).to be_falsey
|
150
|
+
}.to perform_queries(1)
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should work with eager-loaded read marks for the correct readable" do
|
154
|
+
@email1.mark_as_read! :for => @reader
|
155
|
+
|
156
|
+
readers = Reader.with_read_marks_for(@email1).to_a
|
157
|
+
expect(readers[0].have_read?(@email1)).to be_truthy
|
158
|
+
expect(readers[0].have_read?(@email2)).to be_falsey
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
data/spec/spec_helper.rb
CHANGED
data/unread.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unread
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Georg Ledermann
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-05-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -100,14 +100,14 @@ dependencies:
|
|
100
100
|
requirements:
|
101
101
|
- - ">="
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
103
|
+
version: 0.8.0
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
110
|
+
version: 0.8.0
|
111
111
|
description: 'This gem creates a scope for unread objects and adds methods to mark
|
112
112
|
objects as read '
|
113
113
|
email:
|
@@ -134,14 +134,16 @@ files:
|
|
134
134
|
- lib/unread/base.rb
|
135
135
|
- lib/unread/read_mark.rb
|
136
136
|
- lib/unread/readable.rb
|
137
|
+
- lib/unread/readable_scopes.rb
|
137
138
|
- lib/unread/reader.rb
|
138
|
-
- lib/unread/
|
139
|
+
- lib/unread/reader_scopes.rb
|
139
140
|
- lib/unread/version.rb
|
140
141
|
- spec/base_spec.rb
|
141
142
|
- spec/model/email.rb
|
142
143
|
- spec/model/reader.rb
|
143
144
|
- spec/read_mark_spec.rb
|
144
145
|
- spec/readable_spec.rb
|
146
|
+
- spec/reader_spec.rb
|
145
147
|
- spec/spec_helper.rb
|
146
148
|
- spec/support/matchers/perform_queries.rb
|
147
149
|
- spec/support/query_counter.rb
|
@@ -166,7 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
168
|
version: '0'
|
167
169
|
requirements: []
|
168
170
|
rubyforge_project: unread
|
169
|
-
rubygems_version: 2.4.
|
171
|
+
rubygems_version: 2.4.7
|
170
172
|
signing_key:
|
171
173
|
specification_version: 4
|
172
174
|
summary: Manages read/unread status of ActiveRecord objects
|
@@ -176,6 +178,7 @@ test_files:
|
|
176
178
|
- spec/model/reader.rb
|
177
179
|
- spec/read_mark_spec.rb
|
178
180
|
- spec/readable_spec.rb
|
181
|
+
- spec/reader_spec.rb
|
179
182
|
- spec/spec_helper.rb
|
180
183
|
- spec/support/matchers/perform_queries.rb
|
181
184
|
- spec/support/query_counter.rb
|
data/lib/unread/scopes.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
module Unread
|
2
|
-
module Readable
|
3
|
-
module Scopes
|
4
|
-
def join_read_marks(user)
|
5
|
-
assert_reader(user)
|
6
|
-
|
7
|
-
joins "LEFT JOIN #{ReadMark.table_name} as read_marks ON read_marks.readable_type = '#{base_class.name}'
|
8
|
-
AND read_marks.readable_id = #{table_name}.#{primary_key}
|
9
|
-
AND read_marks.user_id = #{user.id}
|
10
|
-
AND read_marks.timestamp >= #{table_name}.#{readable_options[:on]}"
|
11
|
-
end
|
12
|
-
|
13
|
-
def unread_by(user)
|
14
|
-
result = join_read_marks(user).
|
15
|
-
where('read_marks.id IS NULL')
|
16
|
-
|
17
|
-
if global_time_stamp = user.read_mark_global(self).try(:timestamp)
|
18
|
-
result = result.where("#{table_name}.#{readable_options[:on]} > '#{global_time_stamp.to_s(:db)}'")
|
19
|
-
end
|
20
|
-
|
21
|
-
result
|
22
|
-
end
|
23
|
-
|
24
|
-
def with_read_marks_for(user)
|
25
|
-
join_read_marks(user).select("#{table_name}.*, read_marks.id AS read_mark_id")
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|