unread 0.2.0 → 0.3.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 +7 -0
- data/.travis.yml +6 -5
- data/MIT-LICENSE +1 -1
- data/README.md +3 -4
- data/changelog.md +17 -11
- data/ci/Gemfile.rails-3.0.x +1 -2
- data/ci/Gemfile.rails-3.1.x +1 -2
- data/ci/Gemfile.rails-3.2.x +1 -2
- data/ci/Gemfile.rails-4.0.x +6 -0
- data/lib/unread.rb +7 -2
- data/lib/unread/base.rb +40 -0
- data/lib/{app/models → unread}/read_mark.rb +6 -4
- data/lib/unread/readable.rb +135 -0
- data/lib/unread/reader.rb +13 -0
- data/lib/unread/scopes.rb +29 -0
- data/lib/unread/version.rb +1 -1
- data/test/database.yml +0 -5
- data/test/test_helper.rb +1 -2
- data/test/unread_test.rb +5 -4
- data/unread.gemspec +0 -1
- metadata +30 -52
- data/lib/unread/acts_as_readable.rb +0 -207
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 15bcd9b02294f143149629be616cb1299000b90a
|
|
4
|
+
data.tar.gz: 02641fa74f93b396f2368c0766c51c32b41f0620
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3784de0accd1416b0fd9d3f1e7948e23636c1aa57c9f4a843b613a5da34ae29c2edba5a8720c3ef10aa3c8c23755f9b7df239305cdbe81e38d4f248a12004b5e
|
|
7
|
+
data.tar.gz: 11aa961213755d8d9740a3782d2f91192a7d584776ecabea1ded9ccafe30a61d56c5b1eb80452d756684b7bb08818720f19ae7ce36cd4fc13265edf75c7cc9c9
|
data/.travis.yml
CHANGED
|
@@ -2,12 +2,13 @@ language: ruby
|
|
|
2
2
|
rvm:
|
|
3
3
|
- 1.8.7
|
|
4
4
|
- 1.9.3
|
|
5
|
+
- 2.0.0
|
|
5
6
|
gemfile:
|
|
6
7
|
- ci/Gemfile.rails-3.0.x
|
|
7
8
|
- ci/Gemfile.rails-3.1.x
|
|
8
9
|
- ci/Gemfile.rails-3.2.x
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
- ci/Gemfile.rails-4.0.x
|
|
11
|
+
matrix:
|
|
12
|
+
exclude:
|
|
13
|
+
- rvm: 1.8.7
|
|
14
|
+
gemfile: ci/Gemfile.rails-4.0.x
|
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@ Unread
|
|
|
3
3
|
|
|
4
4
|
Ruby gem to manage read/unread status of ActiveRecord objects - and it's fast.
|
|
5
5
|
|
|
6
|
-
[](https://travis-ci.org/ledermann/unread)
|
|
7
7
|
[](https://codeclimate.com/github/ledermann/unread)
|
|
8
8
|
|
|
9
9
|
## Features
|
|
@@ -18,9 +18,8 @@ Ruby gem to manage read/unread status of ActiveRecord objects - and it's fast.
|
|
|
18
18
|
|
|
19
19
|
## Requirements
|
|
20
20
|
|
|
21
|
-
* Ruby 1.8.7 or 1.9.3
|
|
22
|
-
* Rails 3 (including 3.0, 3.1, 3.2).
|
|
23
|
-
* Tested with SQLite and MySQL
|
|
21
|
+
* Ruby 1.8.7 or 1.9.3 or 2.0.0
|
|
22
|
+
* Rails 3 (including 3.0, 3.1, 3.2) and Rails 4. For use with Rails 2.3 there is a branch named "rails2"
|
|
24
23
|
* Needs a timestamp field in your models (like created_at or updated_at) with a database index on it
|
|
25
24
|
|
|
26
25
|
|
data/changelog.md
CHANGED
|
@@ -1,52 +1,58 @@
|
|
|
1
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 - 2013/03/17
|
|
4
|
+
|
|
5
|
+
* Support for Rails 4 (beta1)
|
|
6
|
+
|
|
7
|
+
## 0.2.0 - 2013/02/18
|
|
2
8
|
|
|
3
9
|
* Support for Rails 2 dropped
|
|
4
10
|
* Refactoring
|
|
5
11
|
* Added migration generator
|
|
6
12
|
|
|
7
|
-
0.1.2 - 2013/01/27
|
|
13
|
+
## 0.1.2 - 2013/01/27
|
|
8
14
|
|
|
9
15
|
* Scopes: Improved parameter check
|
|
10
16
|
|
|
11
|
-
0.1.1 - 2012/05/01
|
|
17
|
+
## 0.1.1 - 2012/05/01
|
|
12
18
|
|
|
13
19
|
* Fixed handling namespaced classes. Closes #10 (thanks to @stanislaw)
|
|
14
20
|
|
|
15
|
-
0.1.0 - 2012/04/21
|
|
21
|
+
## 0.1.0 - 2012/04/21
|
|
16
22
|
|
|
17
23
|
* Added scope "with_read_marks_for"
|
|
18
24
|
* Fixed #7: Added attr_accessible to all ReadMark attributes (thanks to @negative)
|
|
19
25
|
|
|
20
|
-
0.0.7 - 2012/02/29
|
|
26
|
+
## 0.0.7 - 2012/02/29
|
|
21
27
|
|
|
22
28
|
* Cleanup files
|
|
23
29
|
* acts_as_reader: Using inverse_of (available since Rails 2.3.6)
|
|
24
30
|
|
|
25
|
-
0.0.6 - 2011/11/11
|
|
31
|
+
## 0.0.6 - 2011/11/11
|
|
26
32
|
|
|
27
33
|
* Fixed #5: Gemspec dependency fix (thanks to @bricker88)
|
|
28
34
|
* Fixed #6: Removed hard coded dependency on a class named "User" (thanks to @mixandgo)
|
|
29
35
|
* Some cleanup
|
|
30
36
|
|
|
31
|
-
0.0.5 - 2011/09/09
|
|
37
|
+
## 0.0.5 - 2011/09/09
|
|
32
38
|
|
|
33
39
|
* Fixed class loading issue in development environment
|
|
34
40
|
|
|
35
|
-
0.0.4 - 2011/08/31
|
|
41
|
+
## 0.0.4 - 2011/08/31
|
|
36
42
|
|
|
37
43
|
* Ignore multiple calls of acts_as_*
|
|
38
44
|
* Improved error messages
|
|
39
45
|
* Tested with Rails 3.1
|
|
40
46
|
|
|
41
|
-
0.0.3 - 2011/08/01
|
|
47
|
+
## 0.0.3 - 2011/08/01
|
|
42
48
|
|
|
43
49
|
* Fixed gemspec by adding development dependencies
|
|
44
50
|
* Testing with Travis CI
|
|
45
51
|
|
|
46
|
-
0.0.2 - 2011/06/23
|
|
52
|
+
## 0.0.2 - 2011/06/23
|
|
47
53
|
|
|
48
54
|
* Fixed scoping for ActiveRecord 2.x
|
|
49
55
|
|
|
50
|
-
0.0.1 - 2011/06/23
|
|
56
|
+
## 0.0.1 - 2011/06/23
|
|
51
57
|
|
|
52
58
|
* Released as Gem
|
data/ci/Gemfile.rails-3.0.x
CHANGED
data/ci/Gemfile.rails-3.1.x
CHANGED
data/ci/Gemfile.rails-3.2.x
CHANGED
data/lib/unread.rb
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
require 'unread/base'
|
|
2
|
+
require 'unread/read_mark'
|
|
3
|
+
require 'unread/readable'
|
|
4
|
+
require 'unread/reader'
|
|
5
|
+
require 'unread/scopes'
|
|
1
6
|
require 'unread/version'
|
|
2
|
-
|
|
3
|
-
|
|
7
|
+
|
|
8
|
+
ActiveRecord::Base.send :include, Unread
|
data/lib/unread/base.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Unread
|
|
2
|
+
def self.included(base)
|
|
3
|
+
base.extend Base
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
module Base
|
|
7
|
+
def acts_as_reader
|
|
8
|
+
ReadMark.belongs_to :user, :class_name => self.to_s
|
|
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
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
include Reader::InstanceMethods
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def acts_as_readable(options={})
|
|
25
|
+
class_attribute :readable_options
|
|
26
|
+
|
|
27
|
+
options.reverse_merge!(:on => :updated_at)
|
|
28
|
+
self.readable_options = options
|
|
29
|
+
|
|
30
|
+
has_many :read_marks, :as => :readable, :dependent => :delete_all
|
|
31
|
+
|
|
32
|
+
ReadMark.readable_classes ||= []
|
|
33
|
+
ReadMark.readable_classes << self unless ReadMark.readable_classes.include?(self)
|
|
34
|
+
|
|
35
|
+
include Readable::InstanceMethods
|
|
36
|
+
extend Readable::ClassMethods
|
|
37
|
+
extend Readable::Scopes
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
class ReadMark < ActiveRecord::Base
|
|
2
2
|
belongs_to :readable, :polymorphic => true
|
|
3
|
-
|
|
3
|
+
if ActiveRecord::VERSION::MAJOR < 4
|
|
4
|
+
attr_accessible :readable_id, :user_id, :readable_type, :timestamp
|
|
5
|
+
end
|
|
4
6
|
|
|
5
7
|
validates_presence_of :user_id, :readable_type
|
|
6
8
|
|
|
7
|
-
scope :global, where(:readable_id => nil)
|
|
8
|
-
scope :single, where('readable_id IS NOT NULL')
|
|
9
|
+
scope :global, lambda { where(:readable_id => nil) }
|
|
10
|
+
scope :single, lambda { where('readable_id IS NOT NULL') }
|
|
9
11
|
scope :older_than, lambda { |timestamp| where([ 'timestamp < ?', timestamp]) }
|
|
10
12
|
|
|
11
|
-
# Returns the class defined by
|
|
13
|
+
# Returns the class defined by acts_as_reader
|
|
12
14
|
def self.reader_class
|
|
13
15
|
reflect_on_all_associations(:belongs_to).find { |assoc| assoc.name == :user }.try(:klass)
|
|
14
16
|
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
module Unread
|
|
2
|
+
module Readable
|
|
3
|
+
module ClassMethods
|
|
4
|
+
def mark_as_read!(target, options)
|
|
5
|
+
user = options[:for]
|
|
6
|
+
assert_reader(user)
|
|
7
|
+
|
|
8
|
+
if target == :all
|
|
9
|
+
reset_read_marks_for_user(user)
|
|
10
|
+
elsif target.is_a?(Array)
|
|
11
|
+
mark_array_as_read(target, user)
|
|
12
|
+
else
|
|
13
|
+
raise ArgumentError
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def mark_array_as_read(array, user)
|
|
18
|
+
ReadMark.transaction do
|
|
19
|
+
array.each do |obj|
|
|
20
|
+
raise ArgumentError unless obj.is_a?(self)
|
|
21
|
+
|
|
22
|
+
rm = obj.read_marks.where(:user_id => user.id).first || obj.read_marks.build(:user_id => user.id)
|
|
23
|
+
rm.timestamp = obj.send(readable_options[:on])
|
|
24
|
+
rm.save!
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A scope with all items accessable for the given user
|
|
30
|
+
# It's used in cleanup_read_marks! to support a filtered cleanup
|
|
31
|
+
# Should be overriden if a user doesn't have access to all items
|
|
32
|
+
# Default: User has access to all items and should read them all
|
|
33
|
+
#
|
|
34
|
+
# Example:
|
|
35
|
+
# def Message.read_scope(user)
|
|
36
|
+
# user.visible_messages
|
|
37
|
+
# end
|
|
38
|
+
def read_scope(user)
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cleanup_read_marks!
|
|
43
|
+
assert_reader_class
|
|
44
|
+
|
|
45
|
+
ReadMark.reader_class.find_each do |user|
|
|
46
|
+
ReadMark.transaction do
|
|
47
|
+
if oldest_timestamp = read_scope(user).unread_by(user).minimum(readable_options[:on])
|
|
48
|
+
# There are unread items, so update the global read_mark for this user to the oldest
|
|
49
|
+
# unread item and delete older read_marks
|
|
50
|
+
update_read_marks_for_user(user, oldest_timestamp)
|
|
51
|
+
else
|
|
52
|
+
# There is no unread item, so deletes all markers and move global timestamp
|
|
53
|
+
reset_read_marks_for_user(user)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def update_read_marks_for_user(user, timestamp)
|
|
60
|
+
# Delete markers OLDER than the given timestamp
|
|
61
|
+
user.read_marks.where(:readable_type => self.base_class.name).single.older_than(timestamp).delete_all
|
|
62
|
+
|
|
63
|
+
# Change the global timestamp for this user
|
|
64
|
+
rm = user.read_mark_global(self) || user.read_marks.build(:readable_type => self.base_class.name)
|
|
65
|
+
rm.timestamp = timestamp - 1.second
|
|
66
|
+
rm.save!
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def reset_read_marks_for_all
|
|
70
|
+
ReadMark.transaction do
|
|
71
|
+
ReadMark.delete_all :readable_type => self.base_class.name
|
|
72
|
+
ReadMark.connection.execute <<-EOT
|
|
73
|
+
INSERT INTO read_marks (user_id, readable_type, timestamp)
|
|
74
|
+
SELECT id, '#{self.base_class.name}', '#{Time.now.to_s(:db)}'
|
|
75
|
+
FROM #{ReadMark.reader_class.table_name}
|
|
76
|
+
EOT
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def reset_read_marks_for_user(user)
|
|
81
|
+
assert_reader(user)
|
|
82
|
+
|
|
83
|
+
ReadMark.transaction do
|
|
84
|
+
ReadMark.delete_all :readable_type => self.base_class.name, :user_id => user.id
|
|
85
|
+
ReadMark.create! :readable_type => self.base_class.name, :user_id => user.id, :timestamp => Time.now
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def assert_reader(user)
|
|
90
|
+
assert_reader_class
|
|
91
|
+
|
|
92
|
+
raise ArgumentError, "Class #{user.class.name} is not registered by acts_as_reader!" unless user.is_a?(ReadMark.reader_class)
|
|
93
|
+
raise ArgumentError, "The given user has no id!" unless user.id
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def assert_reader_class
|
|
97
|
+
raise RuntimeError, 'There is no class using acts_as_reader!' unless ReadMark.reader_class
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
module InstanceMethods
|
|
102
|
+
def unread?(user)
|
|
103
|
+
if self.respond_to?(:read_mark_id)
|
|
104
|
+
# For use with scope "with_read_marks_for"
|
|
105
|
+
return false if self.read_mark_id
|
|
106
|
+
|
|
107
|
+
if global_timestamp = user.read_mark_global(self.class).try(:timestamp)
|
|
108
|
+
self.send(readable_options[:on]) > global_timestamp
|
|
109
|
+
else
|
|
110
|
+
true
|
|
111
|
+
end
|
|
112
|
+
else
|
|
113
|
+
!!self.class.unread_by(user).exists?(self) # Rails4 does not return true/false, but nil/count instead.
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def mark_as_read!(options)
|
|
118
|
+
user = options[:for]
|
|
119
|
+
self.class.assert_reader(user)
|
|
120
|
+
|
|
121
|
+
ReadMark.transaction do
|
|
122
|
+
if unread?(user)
|
|
123
|
+
rm = read_mark(user) || read_marks.build(:user_id => user.id)
|
|
124
|
+
rm.timestamp = self.send(readable_options[:on])
|
|
125
|
+
rm.save!
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def read_mark(user)
|
|
131
|
+
read_marks.where(:user_id => user.id).first
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Unread
|
|
2
|
+
module Reader
|
|
3
|
+
module InstanceMethods
|
|
4
|
+
def read_mark_global(klass)
|
|
5
|
+
instance_var_name = "@read_mark_global_#{klass.name.gsub('::','_')}"
|
|
6
|
+
instance_variable_get(instance_var_name) || begin # memoize
|
|
7
|
+
obj = self.read_marks.where(:readable_type => klass.base_class.name).global.first
|
|
8
|
+
instance_variable_set(instance_var_name, obj)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
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 ON read_marks.readable_type = '#{base_class.name}'
|
|
8
|
+
AND read_marks.readable_id = #{table_name}.id
|
|
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
|
data/lib/unread/version.rb
CHANGED
data/test/database.yml
CHANGED
data/test/test_helper.rb
CHANGED
|
@@ -7,8 +7,7 @@ require 'timecop'
|
|
|
7
7
|
configs = YAML.load_file(File.dirname(__FILE__) + '/database.yml')
|
|
8
8
|
ActiveRecord::Base.configurations = configs
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
ActiveRecord::Base.establish_connection(db_name)
|
|
10
|
+
ActiveRecord::Base.establish_connection('sqlite')
|
|
12
11
|
ActiveRecord::Migration.verbose = false
|
|
13
12
|
load(File.dirname(__FILE__) + "/schema.rb")
|
|
14
13
|
|
data/test/unread_test.rb
CHANGED
|
@@ -14,6 +14,7 @@ class UnreadTest < ActiveSupport::TestCase
|
|
|
14
14
|
Reader.delete_all
|
|
15
15
|
Email.delete_all
|
|
16
16
|
ReadMark.delete_all
|
|
17
|
+
Timecop.return
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def test_schema_has_loaded_correctly
|
|
@@ -39,7 +40,7 @@ class UnreadTest < ActiveSupport::TestCase
|
|
|
39
40
|
def test_with_read_marks_for
|
|
40
41
|
@email1.mark_as_read! :for => @reader
|
|
41
42
|
|
|
42
|
-
emails = Email.with_read_marks_for(@reader).
|
|
43
|
+
emails = Email.with_read_marks_for(@reader).to_a
|
|
43
44
|
|
|
44
45
|
assert emails[0].read_mark_id.present?
|
|
45
46
|
assert emails[1].read_mark_id.nil?
|
|
@@ -124,8 +125,8 @@ class UnreadTest < ActiveSupport::TestCase
|
|
|
124
125
|
|
|
125
126
|
def test_mark_all_as_read
|
|
126
127
|
Email.mark_as_read! :all, :for => @reader
|
|
127
|
-
assert_equal Time.now.to_s, @reader.read_mark_global(Email).try(:timestamp).to_s
|
|
128
128
|
|
|
129
|
+
assert_equal Time.now.utc, @reader.read_mark_global(Email).timestamp.utc
|
|
129
130
|
assert_equal [], @reader.read_marks.single
|
|
130
131
|
assert_equal 0, ReadMark.single.count
|
|
131
132
|
assert_equal 2, ReadMark.global.count
|
|
@@ -148,7 +149,7 @@ class UnreadTest < ActiveSupport::TestCase
|
|
|
148
149
|
def test_cleanup_read_marks_not_delete_from_other_readables
|
|
149
150
|
other_read_mark = @reader.read_marks.create! :readable_type => 'Foo', :readable_id => 42, :timestamp => 5.years.ago
|
|
150
151
|
Email.cleanup_read_marks!
|
|
151
|
-
assert_equal true, ReadMark.exists?(other_read_mark.id)
|
|
152
|
+
assert_equal true, !!ReadMark.exists?(other_read_mark.id) # Rails4 does not return true, but count instead.
|
|
152
153
|
end
|
|
153
154
|
|
|
154
155
|
def test_reset_read_marks_for_all
|
|
@@ -160,6 +161,6 @@ class UnreadTest < ActiveSupport::TestCase
|
|
|
160
161
|
|
|
161
162
|
private
|
|
162
163
|
def wait
|
|
163
|
-
Timecop.
|
|
164
|
+
Timecop.freeze(1.minute.from_now.change(:usec => 0))
|
|
164
165
|
end
|
|
165
166
|
end
|
data/unread.gemspec
CHANGED
metadata
CHANGED
|
@@ -1,97 +1,72 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: unread
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
|
|
5
|
-
version: 0.2.0
|
|
4
|
+
version: 0.3.0
|
|
6
5
|
platform: ruby
|
|
7
6
|
authors:
|
|
8
7
|
- Georg Ledermann
|
|
9
8
|
autorequire:
|
|
10
9
|
bindir: bin
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date: 2013-
|
|
11
|
+
date: 2013-03-17 00:00:00.000000000 Z
|
|
13
12
|
dependencies:
|
|
14
13
|
- !ruby/object:Gem::Dependency
|
|
15
|
-
prerelease: false
|
|
16
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
17
|
-
requirements:
|
|
18
|
-
- - ! '>='
|
|
19
|
-
- !ruby/object:Gem::Version
|
|
20
|
-
version: '3'
|
|
21
|
-
none: false
|
|
22
|
-
type: :runtime
|
|
23
14
|
name: activerecord
|
|
24
15
|
requirement: !ruby/object:Gem::Requirement
|
|
25
16
|
requirements:
|
|
26
|
-
- -
|
|
17
|
+
- - '>='
|
|
27
18
|
- !ruby/object:Gem::Version
|
|
28
19
|
version: '3'
|
|
29
|
-
|
|
30
|
-
- !ruby/object:Gem::Dependency
|
|
20
|
+
type: :runtime
|
|
31
21
|
prerelease: false
|
|
32
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
33
23
|
requirements:
|
|
34
|
-
- -
|
|
24
|
+
- - '>='
|
|
35
25
|
- !ruby/object:Gem::Version
|
|
36
|
-
version: '
|
|
37
|
-
|
|
38
|
-
type: :development
|
|
26
|
+
version: '3'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
39
28
|
name: rake
|
|
40
29
|
requirement: !ruby/object:Gem::Requirement
|
|
41
30
|
requirements:
|
|
42
|
-
- -
|
|
31
|
+
- - '>='
|
|
43
32
|
- !ruby/object:Gem::Version
|
|
44
33
|
version: '0'
|
|
45
|
-
|
|
46
|
-
- !ruby/object:Gem::Dependency
|
|
34
|
+
type: :development
|
|
47
35
|
prerelease: false
|
|
48
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
49
37
|
requirements:
|
|
50
|
-
- -
|
|
38
|
+
- - '>='
|
|
51
39
|
- !ruby/object:Gem::Version
|
|
52
40
|
version: '0'
|
|
53
|
-
|
|
54
|
-
type: :development
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
55
42
|
name: timecop
|
|
56
43
|
requirement: !ruby/object:Gem::Requirement
|
|
57
44
|
requirements:
|
|
58
|
-
- -
|
|
45
|
+
- - '>='
|
|
59
46
|
- !ruby/object:Gem::Version
|
|
60
47
|
version: '0'
|
|
61
|
-
|
|
62
|
-
- !ruby/object:Gem::Dependency
|
|
48
|
+
type: :development
|
|
63
49
|
prerelease: false
|
|
64
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
51
|
requirements:
|
|
66
|
-
- -
|
|
52
|
+
- - '>='
|
|
67
53
|
- !ruby/object:Gem::Version
|
|
68
54
|
version: '0'
|
|
69
|
-
|
|
70
|
-
type: :development
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
71
56
|
name: sqlite3
|
|
72
57
|
requirement: !ruby/object:Gem::Requirement
|
|
73
58
|
requirements:
|
|
74
|
-
- -
|
|
59
|
+
- - '>='
|
|
75
60
|
- !ruby/object:Gem::Version
|
|
76
61
|
version: '0'
|
|
77
|
-
|
|
78
|
-
- !ruby/object:Gem::Dependency
|
|
62
|
+
type: :development
|
|
79
63
|
prerelease: false
|
|
80
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
81
65
|
requirements:
|
|
82
|
-
- -
|
|
83
|
-
- !ruby/object:Gem::Version
|
|
84
|
-
version: '0'
|
|
85
|
-
none: false
|
|
86
|
-
type: :development
|
|
87
|
-
name: mysql2
|
|
88
|
-
requirement: !ruby/object:Gem::Requirement
|
|
89
|
-
requirements:
|
|
90
|
-
- - ! '>='
|
|
66
|
+
- - '>='
|
|
91
67
|
- !ruby/object:Gem::Version
|
|
92
68
|
version: '0'
|
|
93
|
-
|
|
94
|
-
description: ! 'This gem creates a scope for unread objects and adds methods to mark
|
|
69
|
+
description: 'This gem creates a scope for unread objects and adds methods to mark
|
|
95
70
|
objects as read '
|
|
96
71
|
email:
|
|
97
72
|
- mail@georg-ledermann.de
|
|
@@ -109,11 +84,15 @@ files:
|
|
|
109
84
|
- ci/Gemfile.rails-3.0.x
|
|
110
85
|
- ci/Gemfile.rails-3.1.x
|
|
111
86
|
- ci/Gemfile.rails-3.2.x
|
|
112
|
-
-
|
|
87
|
+
- ci/Gemfile.rails-4.0.x
|
|
113
88
|
- lib/generators/unread/migration/migration_generator.rb
|
|
114
89
|
- lib/generators/unread/migration/templates/migration.rb
|
|
115
90
|
- lib/unread.rb
|
|
116
|
-
- lib/unread/
|
|
91
|
+
- lib/unread/base.rb
|
|
92
|
+
- lib/unread/read_mark.rb
|
|
93
|
+
- lib/unread/readable.rb
|
|
94
|
+
- lib/unread/reader.rb
|
|
95
|
+
- lib/unread/scopes.rb
|
|
117
96
|
- lib/unread/version.rb
|
|
118
97
|
- test/database.yml
|
|
119
98
|
- test/schema.rb
|
|
@@ -122,27 +101,26 @@ files:
|
|
|
122
101
|
- unread.gemspec
|
|
123
102
|
homepage: ''
|
|
124
103
|
licenses: []
|
|
104
|
+
metadata: {}
|
|
125
105
|
post_install_message:
|
|
126
106
|
rdoc_options: []
|
|
127
107
|
require_paths:
|
|
128
108
|
- lib
|
|
129
109
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
130
110
|
requirements:
|
|
131
|
-
- -
|
|
111
|
+
- - '>='
|
|
132
112
|
- !ruby/object:Gem::Version
|
|
133
113
|
version: '0'
|
|
134
|
-
none: false
|
|
135
114
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
136
115
|
requirements:
|
|
137
|
-
- -
|
|
116
|
+
- - '>='
|
|
138
117
|
- !ruby/object:Gem::Version
|
|
139
118
|
version: '0'
|
|
140
|
-
none: false
|
|
141
119
|
requirements: []
|
|
142
120
|
rubyforge_project: unread
|
|
143
|
-
rubygems_version:
|
|
121
|
+
rubygems_version: 2.0.3
|
|
144
122
|
signing_key:
|
|
145
|
-
specification_version:
|
|
123
|
+
specification_version: 4
|
|
146
124
|
summary: Manages read/unread status of ActiveRecord objects
|
|
147
125
|
test_files:
|
|
148
126
|
- test/database.yml
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
module Unread
|
|
2
|
-
def self.included(base)
|
|
3
|
-
base.extend ActsAsReadable
|
|
4
|
-
end
|
|
5
|
-
|
|
6
|
-
module ActsAsReadable
|
|
7
|
-
def acts_as_reader
|
|
8
|
-
ReadMark.belongs_to :user, :class_name => self.to_s
|
|
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
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
include ReaderInstanceMethods
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def acts_as_readable(options={})
|
|
25
|
-
class_attribute :readable_options
|
|
26
|
-
|
|
27
|
-
options.reverse_merge!(:on => :updated_at)
|
|
28
|
-
self.readable_options = options
|
|
29
|
-
|
|
30
|
-
has_many :read_marks, :as => :readable, :dependent => :delete_all
|
|
31
|
-
|
|
32
|
-
ReadMark.readable_classes ||= []
|
|
33
|
-
ReadMark.readable_classes << self unless ReadMark.readable_classes.include?(self)
|
|
34
|
-
|
|
35
|
-
scope :join_read_marks, lambda { |user|
|
|
36
|
-
assert_reader(user)
|
|
37
|
-
|
|
38
|
-
joins "LEFT JOIN read_marks ON read_marks.readable_type = '#{self.base_class.name}'
|
|
39
|
-
AND read_marks.readable_id = #{self.table_name}.id
|
|
40
|
-
AND read_marks.user_id = #{user.id}
|
|
41
|
-
AND read_marks.timestamp >= #{self.table_name}.#{readable_options[:on]}"
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
scope :unread_by, lambda { |user|
|
|
45
|
-
result = join_read_marks(user).
|
|
46
|
-
where('read_marks.id IS NULL')
|
|
47
|
-
|
|
48
|
-
if global_time_stamp = user.read_mark_global(self).try(:timestamp)
|
|
49
|
-
result = result.where("#{self.table_name}.#{readable_options[:on]} > '#{global_time_stamp.to_s(:db)}'")
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
result
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
scope :with_read_marks_for, lambda { |user|
|
|
56
|
-
join_read_marks(user).select("#{self.table_name}.*, read_marks.id AS read_mark_id")
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
extend ReadableClassMethods
|
|
60
|
-
include ReadableInstanceMethods
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
module ReadableClassMethods
|
|
65
|
-
def mark_as_read!(target, options)
|
|
66
|
-
user = options[:for]
|
|
67
|
-
assert_reader(user)
|
|
68
|
-
|
|
69
|
-
if target == :all
|
|
70
|
-
reset_read_marks_for_user(user)
|
|
71
|
-
elsif target.is_a?(Array)
|
|
72
|
-
mark_array_as_read(target, user)
|
|
73
|
-
else
|
|
74
|
-
raise ArgumentError
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def mark_array_as_read(array, user)
|
|
79
|
-
ReadMark.transaction do
|
|
80
|
-
array.each do |obj|
|
|
81
|
-
raise ArgumentError unless obj.is_a?(self)
|
|
82
|
-
|
|
83
|
-
rm = obj.read_marks.where(:user_id => user.id).first || obj.read_marks.build(:user_id => user.id)
|
|
84
|
-
rm.timestamp = obj.send(readable_options[:on])
|
|
85
|
-
rm.save!
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# A scope with all items accessable for the given user
|
|
91
|
-
# It's used in cleanup_read_marks! to support a filtered cleanup
|
|
92
|
-
# Should be overriden if a user doesn't have access to all items
|
|
93
|
-
# Default: User has access to all items and should read them all
|
|
94
|
-
#
|
|
95
|
-
# Example:
|
|
96
|
-
# def Message.read_scope(user)
|
|
97
|
-
# user.visible_messages
|
|
98
|
-
# end
|
|
99
|
-
def read_scope(user)
|
|
100
|
-
self
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def cleanup_read_marks!
|
|
104
|
-
assert_reader_class
|
|
105
|
-
|
|
106
|
-
ReadMark.reader_class.find_each do |user|
|
|
107
|
-
ReadMark.transaction do
|
|
108
|
-
if oldest_timestamp = read_scope(user).unread_by(user).minimum(readable_options[:on])
|
|
109
|
-
# There are unread items, so update the global read_mark for this user to the oldest
|
|
110
|
-
# unread item and delete older read_marks
|
|
111
|
-
update_read_marks_for_user(user, oldest_timestamp)
|
|
112
|
-
else
|
|
113
|
-
# There is no unread item, so deletes all markers and move global timestamp
|
|
114
|
-
reset_read_marks_for_user(user)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def update_read_marks_for_user(user, timestamp)
|
|
121
|
-
# Delete markers OLDER than the given timestamp
|
|
122
|
-
user.read_marks.where(:readable_type => self.base_class.name).single.older_than(timestamp).delete_all
|
|
123
|
-
|
|
124
|
-
# Change the global timestamp for this user
|
|
125
|
-
rm = user.read_mark_global(self) || user.read_marks.build(:readable_type => self.base_class.name)
|
|
126
|
-
rm.timestamp = timestamp - 1.second
|
|
127
|
-
rm.save!
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def reset_read_marks_for_all
|
|
131
|
-
ReadMark.transaction do
|
|
132
|
-
ReadMark.delete_all :readable_type => self.base_class.name
|
|
133
|
-
ReadMark.connection.execute <<-EOT
|
|
134
|
-
INSERT INTO read_marks (user_id, readable_type, timestamp)
|
|
135
|
-
SELECT id, '#{self.base_class.name}', '#{Time.now.to_s(:db)}'
|
|
136
|
-
FROM #{ReadMark.reader_class.table_name}
|
|
137
|
-
EOT
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def reset_read_marks_for_user(user)
|
|
142
|
-
assert_reader(user)
|
|
143
|
-
|
|
144
|
-
ReadMark.transaction do
|
|
145
|
-
ReadMark.delete_all :readable_type => self.base_class.name, :user_id => user.id
|
|
146
|
-
ReadMark.create! :readable_type => self.base_class.name, :user_id => user.id, :timestamp => Time.now
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def assert_reader(user)
|
|
151
|
-
assert_reader_class
|
|
152
|
-
|
|
153
|
-
raise ArgumentError, "Class #{user.class.name} is not registered by acts_as_reader!" unless user.is_a?(ReadMark.reader_class)
|
|
154
|
-
raise ArgumentError, "The given user has no id!" unless user.id
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def assert_reader_class
|
|
158
|
-
raise RuntimeError, 'There is no class using acts_as_reader!' unless ReadMark.reader_class
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
module ReadableInstanceMethods
|
|
163
|
-
def unread?(user)
|
|
164
|
-
if self.respond_to?(:read_mark_id)
|
|
165
|
-
# For use with scope "with_read_marks_for"
|
|
166
|
-
return false if self.read_mark_id
|
|
167
|
-
|
|
168
|
-
if global_timestamp = user.read_mark_global(self.class).try(:timestamp)
|
|
169
|
-
self.send(readable_options[:on]) > global_timestamp
|
|
170
|
-
else
|
|
171
|
-
true
|
|
172
|
-
end
|
|
173
|
-
else
|
|
174
|
-
self.class.unread_by(user).exists?(self)
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def mark_as_read!(options)
|
|
179
|
-
user = options[:for]
|
|
180
|
-
self.class.assert_reader(user)
|
|
181
|
-
|
|
182
|
-
ReadMark.transaction do
|
|
183
|
-
if unread?(user)
|
|
184
|
-
rm = read_mark(user) || read_marks.build(:user_id => user.id)
|
|
185
|
-
rm.timestamp = self.send(readable_options[:on])
|
|
186
|
-
rm.save!
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def read_mark(user)
|
|
192
|
-
read_marks.where(:user_id => user.id).first
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
module ReaderInstanceMethods
|
|
197
|
-
def read_mark_global(klass)
|
|
198
|
-
instance_var_name = "@read_mark_global_#{klass.name.gsub('::','_')}"
|
|
199
|
-
instance_variable_get(instance_var_name) || begin # memoize
|
|
200
|
-
obj = self.read_marks.where(:readable_type => klass.base_class.name).global.first
|
|
201
|
-
instance_variable_set(instance_var_name, obj)
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
ActiveRecord::Base.send :include, Unread
|