unread 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 641b3b24fbf159c92f7b4a29485b987454cd382f
4
- data.tar.gz: 2aa121f6822aad813d903014bf3284b7323a8cbb
3
+ metadata.gz: f5498050ac28f45c15fb7fddd5bd1288bf773d68
4
+ data.tar.gz: 784de80f41de7112583dbc1586d3827830fc20cb
5
5
  SHA512:
6
- metadata.gz: fd852349b8f7365896c4b2d35e88f18dd7cf3ce3275f52dc0b8c55a24334306d4a6cb22ac98d356d80b92588ebbe9b78d0cd9924785f7524a32055aec0d0c598
7
- data.tar.gz: 80038ecb572e07cc0ca3e1e21ae4e2879f21d80d13d03c6d6c524a7000f0098bac258d5b3f142a71cbf20b384c80c3e579ea4e6e08c1228eba351aa6e7251a8c
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
- [![Build Status](https://travis-ci.org/ledermann/unread.png?branch=master)](https://travis-ci.org/ledermann/unread)
7
- [![Code Climate](https://codeclimate.com/github/ledermann/unread.png)](https://codeclimate.com/github/ledermann/unread)
8
- [![Coverage Status](https://coveralls.io/repos/ledermann/unread/badge.png)](https://coveralls.io/r/ledermann/unread)
9
-
6
+ [![Build Status](https://travis-ci.org/ledermann/unread.svg?branch=master)](https://travis-ci.org/ledermann/unread)
7
+ [![Code Climate](https://codeclimate.com/github/ledermann/unread.svg)](https://codeclimate.com/github/ledermann/unread)
8
+ [![Coverage Status](https://coveralls.io/repos/ledermann/unread/badge.svg?branch=master)](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
- # Optional: Cleaning up unneeded markers.
91
- # Do this in a cron job once a day.
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 = 'Message'
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/scopes'
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
@@ -22,6 +22,8 @@ module Unread
22
22
  ReadMark.reader_options = options
23
23
 
24
24
  include Reader::InstanceMethods
25
+ extend Reader::ClassMethods
26
+ extend Reader::Scopes
25
27
  end
26
28
 
27
29
  def acts_as_readable(options={})
@@ -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
- result = reader_class
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
@@ -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(:user_id => user.id)
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
- ReadMark.transaction do
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)
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
- # 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(:readable_type => self.base_class.name)
74
- rm.timestamp = timestamp - 1.second
75
- rm.save!
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 #{ReadMark.table_name} (user_id, readable_type, timestamp)
83
- SELECT #{ReadMark.reader_class.primary_key}, '#{self.base_class.name}', '#{Time.current.to_s(:db)}'
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
- ReadMark.create! :readable_type => self.base_class.name, :user_id => user.id, :timestamp => Time.current
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(:user_id => user.id)
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
- instance_var_name = "@read_mark_global_#{klass.name.gsub('::','_')}"
6
- if instance_variables.include?(instance_var_name.to_sym)
7
- instance_variable_get(instance_var_name)
8
- else # memoize
9
- obj = self.read_marks.where(:readable_type => klass.base_class.name).global.first
10
- instance_variable_set(instance_var_name, obj)
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
@@ -1,3 +1,3 @@
1
1
  module Unread
2
- VERSION = '0.5.0'
2
+ VERSION = '0.6.0'
3
3
  end
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
@@ -4,5 +4,5 @@ class Reader < ActiveRecord::Base
4
4
  scope :not_foo, -> { where('name <> "foo"') }
5
5
  scope :not_bar, -> { where('name <> "bar"') }
6
6
 
7
- acts_as_reader :scopes => [:not_foo, :not_bar]
7
+ acts_as_reader :scope => -> { not_foo.not_bar }
8
8
  end
@@ -8,4 +8,8 @@ describe ReadMark do
8
8
  it "should have reader_scope" do
9
9
  expect(ReadMark.reader_scope).to eq Reader.not_foo.not_bar
10
10
  end
11
+
12
+ it "should have readable_classes" do
13
+ expect(ReadMark.readable_classes).to eq [Email]
14
+ end
11
15
  end
@@ -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! :readable_type => 'Foo', :readable_id => 42, :timestamp => 5.years.ago
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
@@ -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
@@ -40,6 +40,10 @@ RSpec.configure do |config|
40
40
  config.after :each do
41
41
  Timecop.return
42
42
  end
43
+
44
+ config.after :suite do
45
+ UnreadMigration.migrate(:down)
46
+ end
43
47
  end
44
48
 
45
49
  if I18n.respond_to?(:enforce_available_locales=)
data/unread.gemspec CHANGED
@@ -25,5 +25,5 @@ Gem::Specification.new do |s|
25
25
  s.add_development_dependency 'sqlite3'
26
26
  s.add_development_dependency 'rspec'
27
27
  s.add_development_dependency 'simplecov'
28
- s.add_development_dependency 'coveralls'
28
+ s.add_development_dependency 'coveralls', '>= 0.8.0'
29
29
  end
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.5.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-03-14 00:00:00.000000000 Z
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: '0'
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: '0'
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/scopes.rb
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.6
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