unread 0.6.3 → 0.7.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: d46186ae6d45d389e56f902320bbce46558ac342
4
- data.tar.gz: 704c5a93c4967d69dd13c0ce550cccfe5af25679
3
+ metadata.gz: 9a7a52ce94c2d2bf78f926c90869494d43c7c0d5
4
+ data.tar.gz: 3f75e1d043b89b5ec6cee60f5b1197106399df23
5
5
  SHA512:
6
- metadata.gz: 1289afd5a3a6f531ddc2aef5ee8598d58bd0a5b5292deab7a01ed8bd044e64ed79060e53310d682d3f6da88a538e57d2a1b26830e478ddeeb7171da861053c9e
7
- data.tar.gz: 055f316a97a375a2c6e266e1cbc34a625562d21c904c0c147b2267caaabe5c8d7d59399b3181e9cc39fc332e411a40f0953e486c8542faed0bf576c947491ecc
6
+ metadata.gz: 576527e97d9c23f90a6d96061598e890a86c3fcb06a36030472e4e12f04a5abfe231eea980c6a122335319f9630c9c71bb4604e77f01018a7b378046dda736ae
7
+ data.tar.gz: 34fd6607a8935bf86ca508def361545f0d1a56d6b9c5057102db317dca29769d27a47d98b8059750d6063bb74a6adfee348db47c18453848ed859f4c1e3f2a83
data/README.md CHANGED
@@ -9,10 +9,10 @@ Ruby gem to manage read/unread status of ActiveRecord objects - and it's fast.
9
9
 
10
10
  ## Features
11
11
 
12
- * Manages unread records for anything you want users to read (like messages, documents, comments etc.)
12
+ * Manages unread records for anything you want readers (e.g. users) to read (like messages, documents, comments etc.)
13
13
  * Supports _mark as read_ to mark a **single** record as read
14
14
  * Supports _mark all as read_ to mark **all** records as read in a single step
15
- * Gives you a scope to get the unread records for a given user
15
+ * Gives you a scope to get the unread records for a given reader
16
16
  * Needs only one additional database table
17
17
  * Most important: Great performance
18
18
 
@@ -51,6 +51,10 @@ rails g unread:migration
51
51
  rake db:migrate
52
52
  ```
53
53
 
54
+ ## Upgrade from previous releases
55
+
56
+ If you upgrade from an older release of this gem, you should read the [upgrade notes](UPGRADE.md).
57
+
54
58
 
55
59
  ## Usage
56
60
 
@@ -58,9 +62,10 @@ rake db:migrate
58
62
  class User < ActiveRecord::Base
59
63
  acts_as_reader
60
64
 
61
- # or, if only a subset of users are readers:
62
- scope :admins, -> { where(:is_admin => true) }
63
- acts_as_reader :scope => -> { admins }
65
+ # Optional: Allow a subset of users as readers only
66
+ def self.reader_scope
67
+ where(:is_admin => true)
68
+ end
64
69
  end
65
70
 
66
71
  class Message < ActiveRecord::Base
@@ -173,7 +178,8 @@ SELECT messages.*
173
178
  FROM messages
174
179
  LEFT JOIN read_marks ON read_marks.readable_type = "Message"
175
180
  AND read_marks.readable_id = messages.id
176
- AND read_marks.user_id = 42
181
+ AND read_marks.reader_id = 42
182
+ AND read_marks.reader_type = 'User'
177
183
  AND read_marks.timestamp >= messages.created_at
178
184
  WHERE read_marks.id IS NULL
179
185
  AND messages.created_at > '2010-10-20 08:50:00'
@@ -182,4 +188,4 @@ AND messages.created_at > '2010-10-20 08:50:00'
182
188
  Hint: You should add a database index on `messages.created_at`.
183
189
 
184
190
 
185
- Copyright (c) 2010-2015 [Georg Ledermann](http://www.georg-ledermann.de), released under the MIT license
191
+ Copyright (c) 2010-2015 [Georg Ledermann](http://www.georg-ledermann.de) and [contributors](https://github.com/ledermann/unread/graphs/contributors), released under the MIT license
data/UPGRADE.md ADDED
@@ -0,0 +1,49 @@
1
+ # Upgrade notes
2
+
3
+ ## Breaking changes with v0.7.0
4
+
5
+ There are two important changes needing your attention. Please read the following hints carefully!
6
+
7
+
8
+ ### Polymorphic readers
9
+
10
+ The gem accepts any type of classes as reader and it's not limited to `User` class anymore. So you can do stuff like:
11
+
12
+ ```ruby
13
+ Customer.have_not_read(message1)
14
+ message1.mark_as_read! :for => Customer.find(1)
15
+ ```
16
+
17
+ If you are upgrading from v0.6.3 or older, you need to do the following after upgrading:
18
+
19
+ ```shell
20
+ rails g unread:polymorphic_reader_migration
21
+ rake db:migrate
22
+ ```
23
+
24
+ This will alter the `read_marks` table to replace `user` association to a polymorphic association named `reader`. Therefore, `user_id` is going to be renamed to `reader_id` and `reader_type` is going to be added.
25
+
26
+ This change should not break your code unless you've worked with `ReadMark` model directly.
27
+
28
+
29
+ ### Defining reader_scope
30
+
31
+ The class method `acts_as_reader` doesn't take the option `:scope` anymore. If you have used it, please change this ...
32
+
33
+ ```ruby
34
+ class User < ActiveRecord::Base
35
+ acts_as_reader :scope => -> { where(:is_admin => true) }
36
+ end
37
+ ```
38
+
39
+ ... to this
40
+
41
+ ```ruby
42
+ class User < ActiveRecord::Base
43
+ acts_as_reader
44
+
45
+ def self.reader_scope
46
+ where(:is_admin => true)
47
+ end
48
+ end
49
+ ```
data/bin/console ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'unread'
5
+
6
+ $LOAD_PATH.unshift(File.expand_path('..', File.dirname(__FILE__)))
7
+ require 'lib/generators/unread/migration/templates/migration.rb'
8
+ require 'spec/support/spec_migration.rb'
9
+
10
+ require 'spec/app/models/reader'
11
+ require 'spec/app/models/different_reader'
12
+ require 'spec/app/models/sti_reader'
13
+ require 'spec/app/models/email'
14
+
15
+ # You can add fixtures and/or initialization code here to make experimenting
16
+ # with your gem easier. You can also use a different console, if you like.
17
+
18
+ # (If you use this, don't forget to add pry to your Gemfile!)
19
+ # require "pry"
20
+ # Pry.start
21
+
22
+ ActiveRecord::Base.configurations = YAML.load_file('spec/database.yml')
23
+ ActiveRecord::Base.establish_connection(:sqlite)
24
+ ActiveRecord::Migration.verbose = false
25
+
26
+ UnreadMigration.up
27
+ SpecMigration.up
28
+
29
+ require 'irb'
30
+ IRB.start
@@ -2,11 +2,11 @@ class UnreadMigration < ActiveRecord::Migration
2
2
  def self.up
3
3
  create_table :read_marks, force: true do |t|
4
4
  t.references :readable, polymorphic: { null: false }
5
- t.references :user, null: false
5
+ t.references :reader, polymorphic: { null: false }
6
6
  t.datetime :timestamp
7
7
  end
8
8
 
9
- add_index :read_marks, [:user_id, :readable_type, :readable_id]
9
+ add_index :read_marks, [:reader_id, :reader_type, :readable_type, :readable_id], name: 'read_marks_reader_readable_index'
10
10
  end
11
11
 
12
12
  def self.down
@@ -0,0 +1,23 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ module Unread
5
+ class PolymorphicReaderMigrationGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ desc "Generates update migration to make reader of read_markers polymorphic"
9
+ source_root File.expand_path('../templates', __FILE__)
10
+
11
+ def create_migration_file
12
+ migration_template 'unread_polymorphic_reader_migration.rb', 'db/migrate/unread_polymorphic_reader_migration.rb'
13
+ end
14
+
15
+ def self.next_migration_number(dirname)
16
+ if ActiveRecord::Base.timestamped_migrations
17
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
18
+ else
19
+ "%.3d" % (current_migration_number(dirname) + 1)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ class UnreadPolymorphicReaderMigration < ActiveRecord::Migration
2
+ def self.up
3
+ remove_index :read_marks, [:user_id, :readable_type, :readable_id]
4
+ rename_column :read_marks, :user_id, :reader_id
5
+ add_column :read_marks, :reader_type, :string
6
+ execute "update read_marks set reader_type = 'User'"
7
+ add_index :read_marks, [:reader_id, :reader_type, :readable_type, :readable_id], name: 'read_marks_reader_readable_index'
8
+ end
9
+
10
+ def self.down
11
+ remove_index :read_marks, name: 'read_marks_reader_readable_index'
12
+ remove_column :read_marks, :reader_type
13
+ rename_column :read_marks, :reader_id, :user_id
14
+ add_index :read_marks, [:user_id, :readable_type, :readable_id]
15
+ end
16
+ end
data/lib/unread.rb CHANGED
@@ -1,9 +1,12 @@
1
+ require 'active_record'
2
+
1
3
  require 'unread/base'
2
4
  require 'unread/read_mark'
3
5
  require 'unread/readable'
4
6
  require 'unread/reader'
5
7
  require 'unread/readable_scopes'
6
8
  require 'unread/reader_scopes'
9
+ require 'unread/garbage_collector'
7
10
  require 'unread/version'
8
11
 
9
- ActiveRecord::Base.send :include, Unread
12
+ ActiveRecord::Base.send :include, Unread
data/lib/unread/base.rb CHANGED
@@ -4,22 +4,17 @@ module Unread
4
4
  end
5
5
 
6
6
  module Base
7
- def acts_as_reader(options={})
8
- ReadMark.belongs_to :user, :class_name => self.to_s, inverse_of: :read_marks
9
-
10
- has_many :read_marks, :dependent => :delete_all, :foreign_key => 'user_id', :inverse_of => :user
11
-
12
- after_create do |user|
13
- # We assume that a new user should not be tackled by tons of old messages
14
- # created BEFORE he signed up.
15
- # Instead, the new user starts with zero unread messages
16
- (ReadMark.readable_classes || []).each do |klass|
17
- klass.mark_as_read! :all, :for => user
18
- end
7
+ def acts_as_reader
8
+ unless ReadMark.reflections.include?(:reader)
9
+ ReadMark.belongs_to :reader, :polymorphic => true, inverse_of: :read_marks
19
10
  end
20
11
 
21
- ReadMark.reader_class = self
22
- ReadMark.reader_options = options
12
+ has_many :read_marks, :dependent => :delete_all, as: :reader, :inverse_of => :reader
13
+
14
+ after_create :setup_new_reader
15
+
16
+ ReadMark.reader_classes ||= []
17
+ ReadMark.reader_classes << self
23
18
 
24
19
  include Reader::InstanceMethods
25
20
  extend Reader::ClassMethods
@@ -0,0 +1,54 @@
1
+ module Unread
2
+ class GarbageCollector
3
+ def initialize(readable_class)
4
+ @readable_class = readable_class
5
+ end
6
+ attr_reader :readable_class
7
+
8
+ def run!
9
+ ReadMark.reader_classes.each do |reader_class|
10
+ readers_to_cleanup(reader_class).each do |reader|
11
+ if oldest_timestamp = readable_class.read_scope(reader).
12
+ unread_by(reader).
13
+ minimum(readable_class.readable_options[:on])
14
+ # There are unread items, so update the global read_mark for this reader to the oldest
15
+ # unread item and delete older read_marks
16
+ update_read_marks_for_user(reader, oldest_timestamp)
17
+ else
18
+ # There is no unread item, so deletes all markers and move global timestamp
19
+ readable_class.reset_read_marks_for_user(reader)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+ # Not for every reader a cleanup is needed.
27
+ # Look for those readers with at least one single read mark
28
+ def readers_to_cleanup(reader_class)
29
+ reader_class.
30
+ reader_scope.
31
+ joins(:read_marks).
32
+ where(:read_marks => { :readable_type => readable_class.base_class.name }).
33
+ group('read_marks.reader_type, read_marks.reader_id').
34
+ having('COUNT(read_marks.id) > 1')
35
+ end
36
+
37
+ def update_read_marks_for_user(reader, timestamp)
38
+ ReadMark.transaction do
39
+ # Delete markers OLDER than the given timestamp
40
+ reader.read_marks.
41
+ where(:readable_type => readable_class.base_class.name).
42
+ single.
43
+ older_than(timestamp).
44
+ delete_all
45
+
46
+ # Change the global timestamp for this reader
47
+ rm = reader.read_mark_global(readable_class) || reader.read_marks.build
48
+ rm.readable_type = readable_class.base_class.name
49
+ rm.timestamp = timestamp - 1.second
50
+ rm.save!
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,20 +1,15 @@
1
1
  class ReadMark < ActiveRecord::Base
2
2
  belongs_to :readable, :polymorphic => true
3
3
 
4
- validates_presence_of :user_id, :readable_type
4
+ validates_presence_of :reader_id, :reader_type, :readable_type
5
5
 
6
6
  scope :global, lambda { where(:readable_id => nil) }
7
7
  scope :single, lambda { where('readable_id IS NOT NULL') }
8
8
  scope :older_than, lambda { |timestamp| where([ 'timestamp < ?', timestamp ]) }
9
9
 
10
- # Returns the class and options defined by acts_as_reader
11
- class_attribute :reader_class
12
- class_attribute :reader_options
10
+ # Returns the classes defined by acts_as_reader
11
+ class_attribute :reader_classes
13
12
 
14
13
  # Returns the classes defined by acts_as_readable
15
14
  class_attribute :readable_classes
16
-
17
- def self.reader_scope
18
- reader_options[:scope].try(:call) || reader_class
19
- end
20
15
  end
@@ -4,21 +4,21 @@ module Unread
4
4
  def mark_as_read!(target, options)
5
5
  raise ArgumentError unless options.is_a?(Hash)
6
6
 
7
- user = options[:for]
8
- assert_reader(user)
7
+ reader = options[:for]
8
+ assert_reader(reader)
9
9
 
10
10
  if target == :all
11
- reset_read_marks_for_user(user)
11
+ reset_read_marks_for_user(reader)
12
12
  elsif target.is_a?(Array)
13
- mark_array_as_read(target, user)
13
+ mark_array_as_read(target, reader)
14
14
  else
15
15
  raise ArgumentError
16
16
  end
17
17
  end
18
18
 
19
- def mark_array_as_read(array, user)
19
+ def mark_array_as_read(array, reader)
20
20
  ReadMark.transaction do
21
- global_timestamp = user.read_mark_global(self).try(:timestamp)
21
+ global_timestamp = reader.read_mark_global(self).try(:timestamp)
22
22
 
23
23
  array.each do |obj|
24
24
  raise ArgumentError unless obj.is_a?(self)
@@ -27,140 +27,103 @@ 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
31
- rm.user_id = user.id
32
- rm.timestamp = timestamp
30
+ rm = obj.read_marks.where(:reader_id => reader.id, :reader_type => reader.class.base_class.name).first || obj.read_marks.build
31
+ rm.reader_id = reader.id
32
+ rm.reader_type = reader.class.base_class.name
33
+ rm.timestamp = timestamp
33
34
  rm.save!
34
35
  end
35
36
  end
36
37
  end
37
38
  end
38
39
 
39
- # A scope with all items accessable for the given user
40
+ # A scope with all items accessable for the given reader
40
41
  # It's used in cleanup_read_marks! to support a filtered cleanup
41
- # Should be overriden if a user doesn't have access to all items
42
- # Default: User has access to all items and should read them all
42
+ # Should be overriden if a reader doesn't have access to all items
43
+ # Default: reader has access to all items and should read them all
43
44
  #
44
45
  # Example:
45
- # def Message.read_scope(user)
46
- # user.visible_messages
46
+ # def Message.read_scope(reader)
47
+ # reader.visible_messages
47
48
  # end
48
- def read_scope(user)
49
+ def read_scope(reader)
49
50
  self
50
51
  end
51
52
 
52
53
  def cleanup_read_marks!
53
54
  assert_reader_class
54
-
55
- ReadMark.reader_scope.find_each do |user|
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
64
- end
65
- end
66
-
67
- def update_read_marks_for_user(user, timestamp)
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
78
- end
79
-
80
- def reset_read_marks_for_all
81
- ReadMark.transaction do
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
-
91
- ReadMark.connection.execute <<-EOT
92
- INSERT INTO read_marks (user_id, readable_type, timestamp)
93
- #{reader_sql}
94
- EOT
95
- end
55
+ Unread::GarbageCollector.new(self).run!
96
56
  end
97
57
 
98
- def reset_read_marks_for_user(user)
99
- assert_reader(user)
58
+ def reset_read_marks_for_user(reader)
59
+ assert_reader(reader)
100
60
 
101
61
  ReadMark.transaction do
102
- ReadMark.delete_all :readable_type => self.base_class.name, :user_id => user.id
62
+ ReadMark.delete_all :readable_type => self.base_class.name, :reader_id => reader.id, :reader_type => reader.class.base_class.name
103
63
 
104
64
  ReadMark.create! do |rm|
105
65
  rm.readable_type = self.base_class.name
106
- rm.user_id = user.id
66
+ rm.reader_id = reader.id
67
+ rm.reader_type = reader.class.base_class.name
107
68
  rm.timestamp = Time.current
108
69
  end
109
70
  end
110
71
 
111
- user.forget_memoized_read_mark_global
72
+ reader.forget_memoized_read_mark_global
112
73
  end
113
74
 
114
- def assert_reader(user)
75
+ def assert_reader(reader)
115
76
  assert_reader_class
116
77
 
117
- raise ArgumentError, "Class #{user.class.name} is not registered by acts_as_reader." unless user.is_a?(ReadMark.reader_class)
118
- raise ArgumentError, "The given user has no id." unless user.id
78
+ raise ArgumentError, "Class #{reader.class.name} is not registered by acts_as_reader." unless ReadMark.reader_classes.any? { |klass| reader.is_a?(klass) }
79
+ raise ArgumentError, "The given reader has no id." unless reader.id
119
80
  end
120
81
 
121
82
  def assert_reader_class
122
- raise RuntimeError, 'There is no class using acts_as_reader.' unless ReadMark.reader_class
83
+ raise RuntimeError, 'There is no class using acts_as_reader.' unless ReadMark.reader_classes
123
84
  end
124
85
  end
125
86
 
126
87
  module InstanceMethods
127
- def unread?(user)
128
- if self.respond_to?(:read_mark_id) && read_mark_id_belongs_to?(user)
88
+ def unread?(reader)
89
+ if self.respond_to?(:read_mark_id) && read_mark_id_belongs_to?(reader)
129
90
  # For use with scope "with_read_marks_for"
130
91
  return false if self.read_mark_id
131
92
 
132
- if global_timestamp = user.read_mark_global(self.class).try(:timestamp)
93
+ if global_timestamp = reader.read_mark_global(self.class).try(:timestamp)
133
94
  self.send(readable_options[:on]) > global_timestamp
134
95
  else
135
96
  true
136
97
  end
137
98
  else
138
- self.class.unread_by(user).exists?(self.id)
99
+ self.class.unread_by(reader).exists?(self.id)
139
100
  end
140
101
  end
141
102
 
142
103
  def mark_as_read!(options)
143
- user = options[:for]
144
- self.class.assert_reader(user)
104
+ reader = options[:for]
105
+ self.class.assert_reader(reader)
145
106
 
146
107
  ReadMark.transaction do
147
- if unread?(user)
148
- rm = read_mark(user) || read_marks.build
149
- rm.user_id = user.id
150
- rm.timestamp = self.send(readable_options[:on])
108
+ if unread?(reader)
109
+ rm = read_mark(reader) || read_marks.build
110
+ rm.reader_id = reader.id
111
+ rm.reader_type = reader.class.base_class.name
112
+ rm.timestamp = self.send(readable_options[:on])
151
113
  rm.save!
152
114
  end
153
115
  end
154
116
  end
155
117
 
156
- def read_mark(user)
157
- read_marks.where(:user_id => user.id).first
118
+ def read_mark(reader)
119
+ read_marks.where(:reader_id => reader.id, reader_type: reader.class.base_class.name).first
158
120
  end
159
121
 
160
122
  private
161
123
 
162
- def read_mark_id_belongs_to?(user)
163
- self.read_mark_user_id == user.id
124
+ def read_mark_id_belongs_to?(reader)
125
+ self.read_mark_reader_id == reader.id &&
126
+ self.read_mark_reader_type == reader.class.base_class.name
164
127
  end
165
128
  end
166
129
  end
@@ -1,20 +1,21 @@
1
1
  module Unread
2
2
  module Readable
3
3
  module Scopes
4
- def join_read_marks(user)
5
- assert_reader(user)
4
+ def join_read_marks(reader)
5
+ assert_reader(reader)
6
6
 
7
7
  joins "LEFT JOIN read_marks
8
8
  ON read_marks.readable_type = '#{base_class.name}'
9
9
  AND read_marks.readable_id = #{quoted_table_name}.#{quoted_primary_key}
10
- AND read_marks.user_id = #{quote_bound_value(user.id)}
10
+ AND read_marks.reader_id = #{quote_bound_value(reader.id)}
11
+ AND read_marks.reader_type = #{quote_bound_value(reader.class.base_class.name)}
11
12
  AND read_marks.timestamp >= #{quoted_table_name}.#{connection.quote_column_name(readable_options[:on])}"
12
13
  end
13
14
 
14
- def unread_by(user)
15
- result = join_read_marks(user)
15
+ def unread_by(reader)
16
+ result = join_read_marks(reader)
16
17
 
17
- if global_time_stamp = user.read_mark_global(self).try(:timestamp)
18
+ if global_time_stamp = reader.read_mark_global(self).try(:timestamp)
18
19
  result.where("read_marks.id IS NULL
19
20
  AND #{quoted_table_name}.#{connection.quote_column_name(readable_options[:on])} > ?", global_time_stamp)
20
21
  else
@@ -22,10 +23,10 @@ module Unread
22
23
  end
23
24
  end
24
25
 
25
- def read_by(user)
26
- result = join_read_marks(user)
26
+ def read_by(reader)
27
+ result = join_read_marks(reader)
27
28
 
28
- if global_time_stamp = user.read_mark_global(self).try(:timestamp)
29
+ if global_time_stamp = reader.read_mark_global(self).try(:timestamp)
29
30
  result.where("read_marks.id IS NOT NULL
30
31
  OR #{quoted_table_name}.#{connection.quote_column_name(readable_options[:on])} <= ?", global_time_stamp)
31
32
  else
@@ -33,10 +34,11 @@ module Unread
33
34
  end
34
35
  end
35
36
 
36
- def with_read_marks_for(user)
37
- join_read_marks(user).select("#{quoted_table_name}.*,
37
+ def with_read_marks_for(reader)
38
+ join_read_marks(reader).select("#{quoted_table_name}.*,
38
39
  read_marks.id AS read_mark_id,
39
- #{quote_bound_value(user.id)} AS read_mark_user_id")
40
+ #{quote_bound_value(reader.class.base_class.name)} AS read_mark_reader_type,
41
+ #{quote_bound_value(reader.id)} AS read_mark_reader_id")
40
42
  end
41
43
  end
42
44
  end
data/lib/unread/reader.rb CHANGED
@@ -37,6 +37,15 @@ module Unread
37
37
  self.read_mark_readable_type == readable.class.base_class.name &&
38
38
  (self.read_mark_readable_id.nil? || self.read_mark_readable_id == readable.id)
39
39
  end
40
+
41
+ # We assume that a new reader should not be tackled by tons of old messages created BEFORE he signed up.
42
+ # Instead, the new reader should start with zero unread messages.
43
+ # If you don't want this, you can override this method in your reader class
44
+ def setup_new_reader
45
+ (ReadMark.readable_classes || []).each do |klass|
46
+ klass.mark_as_read! :all, :for => self
47
+ end
48
+ end
40
49
  end
41
50
  end
42
- end
51
+ end
@@ -1,13 +1,20 @@
1
1
  module Unread
2
2
  module Reader
3
3
  module Scopes
4
+ # This class method may be overriden to restrict readers to a subset of records
5
+ # It must return self or a ActiveRecord::Relation
6
+ def reader_scope
7
+ self
8
+ end
9
+
4
10
  def join_read_marks(readable)
5
11
  assert_readable(readable)
6
12
 
7
13
  joins "LEFT JOIN read_marks
8
14
  ON read_marks.readable_type = '#{readable.class.base_class.name}'
9
15
  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}
16
+ AND read_marks.reader_id = #{quoted_table_name}.#{quoted_primary_key}
17
+ AND read_marks.reader_type = '#{connection.quote_string base_class.name}'
11
18
  AND read_marks.timestamp >= '#{connection.quoted_date readable.send(readable.class.readable_options[:on])}'"
12
19
  end
13
20
 
@@ -1,3 +1,3 @@
1
1
  module Unread
2
- VERSION = '0.6.3'
2
+ VERSION = '0.7.0'
3
3
  end
@@ -0,0 +1,5 @@
1
+ class DifferentReader < ActiveRecord::Base
2
+ self.primary_key = 'number'
3
+
4
+ acts_as_reader
5
+ end
File without changes
@@ -4,5 +4,9 @@ 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 :scope => -> { not_foo.not_bar }
7
+ acts_as_reader
8
+
9
+ def self.reader_scope
10
+ not_foo.not_bar
11
+ end
8
12
  end
@@ -0,0 +1,6 @@
1
+ class Customer < ActiveRecord::Base
2
+ end
3
+
4
+ class StiReader < Customer
5
+ acts_as_reader
6
+ end
data/spec/spec_helper.rb CHANGED
@@ -9,12 +9,14 @@ SimpleCov.start do
9
9
  add_filter '/spec/'
10
10
  end
11
11
 
12
- require 'active_record'
13
12
  require 'timecop'
14
13
  require 'unread'
14
+ require 'generators/unread/migration/templates/migration.rb'
15
15
 
16
- require 'model/reader'
17
- require 'model/email'
16
+ require 'app/models/reader'
17
+ require 'app/models/different_reader'
18
+ require 'app/models/sti_reader'
19
+ require 'app/models/email'
18
20
 
19
21
  # Requires supporting ruby files with custom matchers and macros, etc,
20
22
  # in spec/support/ and its subdirectories.
@@ -42,7 +44,7 @@ RSpec.configure do |config|
42
44
  end
43
45
 
44
46
  config.after :suite do
45
- UnreadMigration.migrate(:down)
47
+ UnreadMigration.down
46
48
  end
47
49
  end
48
50
 
@@ -62,26 +64,14 @@ def setup_db
62
64
  ActiveRecord::Base.default_timezone = :utc
63
65
  ActiveRecord::Migration.verbose = false
64
66
 
65
- require File.expand_path('../../lib/generators/unread/migration/templates/migration.rb', __FILE__)
66
- UnreadMigration.migrate(:up)
67
-
68
- ActiveRecord::Schema.define(:version => 1) do
69
- create_table :readers, :primary_key => 'number', :force => true do |t|
70
- t.string :name
71
- end
72
-
73
- create_table :documents, :primary_key => 'uid', :force => true do |t|
74
- t.string :type
75
- t.string :subject
76
- t.text :content
77
- t.datetime :created_at
78
- t.datetime :updated_at
79
- end
80
- end
67
+ UnreadMigration.up
68
+ SpecMigration.up
81
69
  end
82
70
 
83
71
  def clear_db
84
72
  Reader.delete_all
73
+ DifferentReader.delete_all
74
+ StiReader.delete_all
85
75
  Email.delete_all
86
76
  ReadMark.delete_all
87
77
  end
@@ -0,0 +1,23 @@
1
+ class SpecMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :readers, :primary_key => 'number', :force => true do |t|
4
+ t.string :name
5
+ end
6
+
7
+ create_table :different_readers, :primary_key => 'number', :force => true do |t|
8
+ t.string :name
9
+ end
10
+
11
+ create_table :customers, :force => true do |t|
12
+ t.string :type
13
+ end
14
+
15
+ create_table :documents, :primary_key => 'uid', :force => true do |t|
16
+ t.string :type
17
+ t.string :subject
18
+ t.text :content
19
+ t.datetime :created_at
20
+ t.datetime :updated_at
21
+ end
22
+ end
23
+ end
@@ -14,7 +14,7 @@ describe Unread::Base do
14
14
  end
15
15
 
16
16
  it "should define association for ReadMark" do
17
- expect(@reader.read_marks.first.user).to eq(@reader)
17
+ expect(@reader.read_marks.first.reader).to eq(@reader)
18
18
  end
19
19
 
20
20
  it "should reset read_marks for created reader" do
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ describe Unread::GarbageCollector do
4
+ before :each do
5
+ @reader = Reader.create! :name => 'David'
6
+ @other_reader = Reader.create :name => 'Matz'
7
+ @sti_reader = StiReader.create!
8
+ wait
9
+ @email1 = Email.create!
10
+ wait
11
+ @email2 = Email.create!
12
+ end
13
+
14
+ describe :run! do
15
+ it "should delete all single read marks" do
16
+ expect(@reader.read_marks.single.count).to eq 0
17
+
18
+ @email1.mark_as_read! :for => @reader
19
+
20
+ expect(Email.unread_by(@reader)).to eq [@email2]
21
+ expect(@reader.read_marks.single.count).to eq 1
22
+
23
+ Unread::GarbageCollector.new(Email).run!
24
+
25
+ @reader.reload
26
+ expect(@reader.read_marks.single.count).to eq 0
27
+ end
28
+
29
+ it "should reset if all objects are read" do
30
+ @email1.mark_as_read! :for => @reader
31
+ @email2.mark_as_read! :for => @reader
32
+
33
+ expect(@reader.read_marks.single.count).to eq 2
34
+
35
+ Unread::GarbageCollector.new(Email).run!
36
+
37
+ expect(@reader.read_marks.single.count).to eq 0
38
+ end
39
+
40
+ it "should not delete read marks from other readables" do
41
+ other_read_mark = @reader.read_marks.create! do |rm|
42
+ rm.readable_type = 'Foo'
43
+ rm.readable_id = 42
44
+ rm.timestamp = 5.years.ago
45
+ end
46
+
47
+ Unread::GarbageCollector.new(Email).run!
48
+
49
+ expect(ReadMark.exists?(other_read_mark.id)).to be_truthy
50
+ end
51
+ end
52
+ end
@@ -2,11 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe ReadMark do
4
4
  it "should have reader_class" do
5
- expect(ReadMark.reader_class).to eq Reader
6
- end
7
-
8
- it "should have reader_scope" do
9
- expect(ReadMark.reader_scope).to eq Reader.not_foo.not_bar
5
+ expect(ReadMark.reader_classes).to eq [Reader, DifferentReader, StiReader]
10
6
  end
11
7
 
12
8
  it "should have readable_classes" do
@@ -4,6 +4,7 @@ describe Unread::Readable do
4
4
  before :each do
5
5
  @reader = Reader.create! :name => 'David'
6
6
  @other_reader = Reader.create :name => 'Matz'
7
+ @sti_reader = StiReader.create!
7
8
  wait
8
9
  @email1 = Email.create!
9
10
  wait
@@ -279,7 +280,7 @@ describe Unread::Readable do
279
280
  expect(@reader.read_mark_global(Email).timestamp).to eq Time.current
280
281
  expect(@reader.read_marks.single).to eq []
281
282
  expect(ReadMark.single.count).to eq 0
282
- expect(ReadMark.global.count).to eq 2
283
+ expect(ReadMark.global.count).to eq 3
283
284
  end
284
285
 
285
286
  it "should mark all objects as read with existing read objects" do
@@ -309,51 +310,10 @@ describe Unread::Readable do
309
310
  end
310
311
  end
311
312
 
312
- describe :reset_read_marks_for_all do
313
- it "should reset read marks" do
314
- Email.reset_read_marks_for_all
315
-
316
- expect(ReadMark.single.count).to eq 0
317
- expect(ReadMark.global.count).to eq 2
318
- end
319
- end
320
-
321
313
  describe :cleanup_read_marks! do
322
- it "should delete all single read marks" do
323
- expect(@reader.read_marks.single.count).to eq 0
324
-
325
- @email1.mark_as_read! :for => @reader
326
-
327
- expect(Email.unread_by(@reader)).to eq [@email2]
328
- expect(@reader.read_marks.single.count).to eq 1
329
-
314
+ it "should run garbage collector" do
315
+ expect(Unread::GarbageCollector).to receive(:new).with(Email).and_return(double :run! => true)
330
316
  Email.cleanup_read_marks!
331
-
332
- @reader.reload
333
- expect(@reader.read_marks.single.count).to eq 0
334
- end
335
-
336
- it "should reset if all objects are read" do
337
- @email1.mark_as_read! :for => @reader
338
- @email2.mark_as_read! :for => @reader
339
-
340
- expect(@reader.read_marks.single.count).to eq 2
341
-
342
- Email.cleanup_read_marks!
343
-
344
- expect(@reader.read_marks.single.count).to eq 0
345
- end
346
-
347
- it "should not delete read marks from other readables" do
348
- other_read_mark = @reader.read_marks.create! do |rm|
349
- rm.readable_type = 'Foo'
350
- rm.readable_id = 42
351
- rm.timestamp = 5.years.ago
352
- end
353
-
354
- Email.cleanup_read_marks!
355
-
356
- expect(ReadMark.exists?(other_read_mark.id)).to be_truthy
357
317
  end
358
318
  end
359
319
  end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe Unread::Reader::Scopes do
4
+ it 'should define reader_scope' do
5
+ expect(Reader.reader_scope).to eq Reader.not_foo.not_bar
6
+ expect(DifferentReader.reader_scope).to eq DifferentReader
7
+ expect(StiReader.reader_scope).to eq StiReader
8
+ end
9
+ end
@@ -4,6 +4,7 @@ describe Unread::Reader do
4
4
  before :each do
5
5
  @reader = Reader.create! :name => 'David'
6
6
  @other_reader = Reader.create :name => 'Matz'
7
+ @different_reader = DifferentReader.create! :name => 'Behrooz', :number => @reader.number
7
8
  wait
8
9
  @email1 = Email.create!
9
10
  wait
@@ -25,6 +26,25 @@ describe Unread::Reader do
25
26
  expect(Reader.have_not_read(@email2)).to eq [@reader, @other_reader]
26
27
  end
27
28
 
29
+ it "should take the type of reader into account" do
30
+ # even though the id of @reader and @different_reader is the same because
31
+ # they are different object types, the @email1 should only be marked as
32
+ # read for @reader.
33
+ @email1.mark_as_read! :for => @reader
34
+
35
+ expect(Reader.have_not_read(@email1)).to eq [@other_reader]
36
+ expect(Reader.have_not_read(@email1).count).to eq 1
37
+
38
+ expect(DifferentReader.have_not_read(@email1)).to eq [@different_reader]
39
+ expect(DifferentReader.have_not_read(@email1).count).to eq 1
40
+
41
+ @email1.mark_as_read! :for => @different_reader
42
+
43
+ expect(DifferentReader.have_not_read(@email1).count).to eq 0
44
+
45
+ expect(Reader.have_not_read(@email2)).to eq [@reader, @other_reader]
46
+ end
47
+
28
48
  it "should not allow invalid parameter" do
29
49
  [ 42, nil, 'foo', :foo, {} ].each do |not_a_readable|
30
50
  expect {
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.6.3
4
+ version: 0.7.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-09-27 00:00:00.000000000 Z
11
+ date: 2015-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -126,7 +126,8 @@ description: 'This gem creates a scope for unread objects and adds methods to ma
126
126
  objects as read '
127
127
  email:
128
128
  - mail@georg-ledermann.de
129
- executables: []
129
+ executables:
130
+ - console
130
131
  extensions: []
131
132
  extra_rdoc_files: []
132
133
  files:
@@ -136,6 +137,8 @@ files:
136
137
  - MIT-LICENSE
137
138
  - README.md
138
139
  - Rakefile
140
+ - UPGRADE.md
141
+ - bin/console
139
142
  - ci/Gemfile-rails-3-0
140
143
  - ci/Gemfile-rails-3-1
141
144
  - ci/Gemfile-rails-3-2
@@ -144,25 +147,33 @@ files:
144
147
  - ci/Gemfile-rails-4-2
145
148
  - lib/generators/unread/migration/migration_generator.rb
146
149
  - lib/generators/unread/migration/templates/migration.rb
150
+ - lib/generators/unread/polymorphic_reader_migration/polymorphic_reader_migration_generator.rb
151
+ - lib/generators/unread/polymorphic_reader_migration/templates/unread_polymorphic_reader_migration.rb
147
152
  - lib/unread.rb
148
153
  - lib/unread/base.rb
154
+ - lib/unread/garbage_collector.rb
149
155
  - lib/unread/read_mark.rb
150
156
  - lib/unread/readable.rb
151
157
  - lib/unread/readable_scopes.rb
152
158
  - lib/unread/reader.rb
153
159
  - lib/unread/reader_scopes.rb
154
160
  - lib/unread/version.rb
155
- - spec/base_spec.rb
161
+ - spec/app/models/different_reader.rb
162
+ - spec/app/models/email.rb
163
+ - spec/app/models/reader.rb
164
+ - spec/app/models/sti_reader.rb
156
165
  - spec/database.yml
157
- - spec/model/email.rb
158
- - spec/model/reader.rb
159
- - spec/read_mark_spec.rb
160
- - spec/readable_spec.rb
161
- - spec/reader_spec.rb
162
166
  - spec/spec_helper.rb
163
167
  - spec/support/matchers/perform_queries.rb
164
168
  - spec/support/query_counter.rb
169
+ - spec/support/spec_migration.rb
165
170
  - spec/support/timecop.rb
171
+ - spec/unread/base_spec.rb
172
+ - spec/unread/garbage_collector_spec.rb
173
+ - spec/unread/read_mark_spec.rb
174
+ - spec/unread/readable_spec.rb
175
+ - spec/unread/reader_scopes_spec.rb
176
+ - spec/unread/reader_spec.rb
166
177
  - unread.gemspec
167
178
  homepage: https://github.com/ledermann/unread
168
179
  licenses:
@@ -184,19 +195,24 @@ required_rubygems_version: !ruby/object:Gem::Requirement
184
195
  version: '0'
185
196
  requirements: []
186
197
  rubyforge_project: unread
187
- rubygems_version: 2.4.8
198
+ rubygems_version: 2.5.0
188
199
  signing_key:
189
200
  specification_version: 4
190
201
  summary: Manages read/unread status of ActiveRecord objects
191
202
  test_files:
192
- - spec/base_spec.rb
203
+ - spec/app/models/different_reader.rb
204
+ - spec/app/models/email.rb
205
+ - spec/app/models/reader.rb
206
+ - spec/app/models/sti_reader.rb
193
207
  - spec/database.yml
194
- - spec/model/email.rb
195
- - spec/model/reader.rb
196
- - spec/read_mark_spec.rb
197
- - spec/readable_spec.rb
198
- - spec/reader_spec.rb
199
208
  - spec/spec_helper.rb
200
209
  - spec/support/matchers/perform_queries.rb
201
210
  - spec/support/query_counter.rb
211
+ - spec/support/spec_migration.rb
202
212
  - spec/support/timecop.rb
213
+ - spec/unread/base_spec.rb
214
+ - spec/unread/garbage_collector_spec.rb
215
+ - spec/unread/read_mark_spec.rb
216
+ - spec/unread/readable_spec.rb
217
+ - spec/unread/reader_scopes_spec.rb
218
+ - spec/unread/reader_spec.rb