unread 0.6.3 → 0.7.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: 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