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 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