unread 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +133 -0
- data/Rakefile +15 -0
- data/changelog.md +3 -0
- data/init.rb +1 -0
- data/lib/app/models/read_mark.rb +20 -0
- data/lib/unread/acts_as_readable.rb +183 -0
- data/lib/unread/version.rb +3 -0
- data/lib/unread.rb +3 -0
- data/test/database.yml +3 -0
- data/test/schema.rb +20 -0
- data/test/test_helper.rb +24 -0
- data/test/unread_test.rb +132 -0
- data/uninstall.rb +1 -0
- data/unread.gemspec +23 -0
- metadata +114 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010,2011 Georg Ledermann
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
Unread
|
2
|
+
======
|
3
|
+
|
4
|
+
Gem to manage read/unread status of ActiveRecord objects - and it's fast.
|
5
|
+
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
* Manages unread records for anything you want your users to read (like messages, documents, comments etc.)
|
10
|
+
* Supports "mark as read" to mark a **single** record as read
|
11
|
+
* Supports "mark all as read" to mark **all** records as read in a single step
|
12
|
+
* Gives you a named_scope to get the unread records for a given user
|
13
|
+
* Needs only one additional database table
|
14
|
+
* Most important: Great performance
|
15
|
+
|
16
|
+
|
17
|
+
## Requirements
|
18
|
+
|
19
|
+
* ActiveRecord (tested with SQLite and MySQL)
|
20
|
+
* Needs a timestamp field in your models (e.g. created_at) with a database index on it
|
21
|
+
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
Step 1: Add this to your Gemfile:
|
26
|
+
|
27
|
+
gem 'unread'
|
28
|
+
|
29
|
+
and run
|
30
|
+
|
31
|
+
bundle
|
32
|
+
|
33
|
+
|
34
|
+
Step 2: Add this migration:
|
35
|
+
|
36
|
+
class CreateReadMarks < ActiveRecord::Migration
|
37
|
+
def self.up
|
38
|
+
create_table :read_marks, :force => true do |t|
|
39
|
+
t.integer :readable_id
|
40
|
+
t.integer :user_id, :null => false
|
41
|
+
t.string :readable_type, :null => false, :limit => 20
|
42
|
+
t.datetime :timestamp
|
43
|
+
end
|
44
|
+
add_index :read_marks, [:user_id, :readable_type, :readable_id]
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.down
|
48
|
+
drop_table :read_marks
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
and run the migration:
|
53
|
+
|
54
|
+
rake db:migrate
|
55
|
+
|
56
|
+
|
57
|
+
## Usage
|
58
|
+
|
59
|
+
class User < ActiveRecord::Base
|
60
|
+
acts_as_reader
|
61
|
+
end
|
62
|
+
|
63
|
+
class Message < ActiveRecord::Base
|
64
|
+
acts_as_readable :on => :created_at
|
65
|
+
end
|
66
|
+
|
67
|
+
message1 = Message.create!
|
68
|
+
message2 = Message.create!
|
69
|
+
|
70
|
+
Message.unread_by(current_user)
|
71
|
+
# => [ message1, message2 ]
|
72
|
+
|
73
|
+
message1.mark_as_read! :for => current_user
|
74
|
+
Message.unread_by(current_user)
|
75
|
+
# => [ message2 ]
|
76
|
+
|
77
|
+
Message.mark_as_read! :all, :for => current_user
|
78
|
+
Message.unread_by(current_user)
|
79
|
+
# => [ ]
|
80
|
+
|
81
|
+
# Optional: Cleaning up unneeded markers
|
82
|
+
# Do this in a cron job once a day.
|
83
|
+
Message.cleanup_read_marks!
|
84
|
+
|
85
|
+
|
86
|
+
## How does it work?
|
87
|
+
|
88
|
+
The main idea of this gem is to manage a list of read items for every user **after** a certain timestamp.
|
89
|
+
|
90
|
+
The gem defines a named_scope doing a LEFT JOIN to this list, so your app can get the unread items in a performant manner. Of course, other scopes can be combined.
|
91
|
+
|
92
|
+
It will be ensured that the list of read items will not grow up too much:
|
93
|
+
|
94
|
+
* If a user uses "mark all as read", his list is deleted and the timestamp is set to the current time.
|
95
|
+
* If a user never uses "mark all as read", the list will grow and grow with each item he reads. But there is help: Your app can use a cleanup method which removes unnecessary list items.
|
96
|
+
|
97
|
+
Overall, this gem can be used for large tables, too. If you are in doubt, look at the generated SQL queries, here is an example:
|
98
|
+
|
99
|
+
# Assuming we have a user who has marked all messages as read on 2010-10-20 08:50
|
100
|
+
current_user = User.find(42)
|
101
|
+
|
102
|
+
# Get the unread messages for this user
|
103
|
+
Message.unread_by(current_user)
|
104
|
+
|
105
|
+
# =>
|
106
|
+
# SELECT messages.*
|
107
|
+
# FROM messages
|
108
|
+
# LEFT JOIN read_marks ON read_marks.readable_type = 'Message'
|
109
|
+
# AND read_marks.readable_id = messages.id
|
110
|
+
# AND read_marks.user_id = 42
|
111
|
+
# AND read_marks.timestamp >= messages.created_at
|
112
|
+
# WHERE read_marks.id IS NULL
|
113
|
+
# AND messages.created_at > '2010-10-20 08:50:00'
|
114
|
+
|
115
|
+
Hint: You should add a database index on messages.created_at.
|
116
|
+
|
117
|
+
|
118
|
+
## Similar tools
|
119
|
+
|
120
|
+
There a two other gems/plugins doing a similar job:
|
121
|
+
|
122
|
+
* http://github.com/jhnvz/mark_as_read
|
123
|
+
* http://github.com/mbleigh/acts-as-readable
|
124
|
+
|
125
|
+
Unfortunately, both of them have a lack of performance, because they calculate the unread records doing a _find(:all)_, which should be avoided for a large amount of records. This gem is based on a timestamp algorithm and therefore it's very fast.
|
126
|
+
|
127
|
+
|
128
|
+
## TODO
|
129
|
+
|
130
|
+
* Add more documentation
|
131
|
+
|
132
|
+
|
133
|
+
Copyright (c) 2010,2011 Georg Ledermann, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/testtask'
|
5
|
+
|
6
|
+
desc 'Default: run unit tests.'
|
7
|
+
task :default => :test
|
8
|
+
|
9
|
+
desc 'Test the unread plugin.'
|
10
|
+
Rake::TestTask.new(:test) do |t|
|
11
|
+
t.libs << 'lib'
|
12
|
+
t.libs << 'test'
|
13
|
+
t.pattern = 'test/**/*_test.rb'
|
14
|
+
t.verbose = true
|
15
|
+
end
|
data/changelog.md
ADDED
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'unread'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class ReadMark < ActiveRecord::Base
|
2
|
+
belongs_to :user
|
3
|
+
belongs_to :readable, :polymorphic => true
|
4
|
+
|
5
|
+
validates_presence_of :user_id, :readable_type
|
6
|
+
|
7
|
+
scope_method = respond_to?(:scope) ? :scope : :named_scope
|
8
|
+
|
9
|
+
send scope_method, :global, :conditions => { :readable_id => nil }
|
10
|
+
send scope_method, :single, :conditions => 'readable_id IS NOT NULL'
|
11
|
+
send scope_method, :readable_type, lambda { |readable_type | { :conditions => { :readable_type => readable_type }}}
|
12
|
+
send scope_method, :user, lambda { |user| { :conditions => { :user_id => user.id }}}
|
13
|
+
send scope_method, :older_than, lambda { |timestamp| { :conditions => [ 'timestamp < ?', timestamp] }}
|
14
|
+
|
15
|
+
if respond_to?(:class_attribute)
|
16
|
+
class_attribute :reader_class, :readable_classes
|
17
|
+
else
|
18
|
+
class_inheritable_accessor :reader_class, :readable_classes
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,183 @@
|
|
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.reader_class = self
|
9
|
+
|
10
|
+
has_many :read_marks, :dependent => :delete_all
|
11
|
+
|
12
|
+
after_create do |user|
|
13
|
+
(ReadMark.readable_classes || []).each do |klass|
|
14
|
+
klass.mark_as_read! :all, :for => user
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def acts_as_readable(options={})
|
20
|
+
options.reverse_merge!({ :on => :updated_at })
|
21
|
+
if respond_to?(:class_attribute)
|
22
|
+
class_attribute :readable_options
|
23
|
+
else
|
24
|
+
class_inheritable_accessor :readable_options
|
25
|
+
end
|
26
|
+
self.readable_options = options
|
27
|
+
|
28
|
+
self.has_many :read_marks, :as => :readable, :dependent => :delete_all
|
29
|
+
|
30
|
+
classes = ReadMark.readable_classes || []
|
31
|
+
classes << self
|
32
|
+
ReadMark.readable_classes = classes
|
33
|
+
|
34
|
+
scope_method = respond_to?(:scope) ? :scope : :named_scope
|
35
|
+
|
36
|
+
send scope_method, :unread_by, lambda { |user|
|
37
|
+
check_reader
|
38
|
+
raise ArgumentError unless user.is_a?(ReadMark.reader_class)
|
39
|
+
|
40
|
+
result = { :joins => "LEFT JOIN read_marks ON read_marks.readable_type = '#{self.base_class.name}'
|
41
|
+
AND read_marks.readable_id = #{self.table_name}.id
|
42
|
+
AND read_marks.user_id = #{user.id}
|
43
|
+
AND read_marks.timestamp >= #{self.table_name}.#{readable_options[:on]}",
|
44
|
+
:conditions => 'read_marks.id IS NULL' }
|
45
|
+
if last = read_timestamp(user)
|
46
|
+
result[:conditions] += " AND #{self.table_name}.#{readable_options[:on]} > '#{last.to_s(:db)}'"
|
47
|
+
end
|
48
|
+
result
|
49
|
+
}
|
50
|
+
|
51
|
+
extend ClassMethods
|
52
|
+
include InstanceMethods
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module ClassMethods
|
57
|
+
def mark_as_read!(target, options)
|
58
|
+
check_reader
|
59
|
+
raise ArgumentError unless target == :all || target.is_a?(Array)
|
60
|
+
|
61
|
+
user = options[:for]
|
62
|
+
raise ArgumentError unless user.is_a?(ReadMark.reader_class)
|
63
|
+
|
64
|
+
if target == :all
|
65
|
+
reset_read_marks!(user)
|
66
|
+
elsif target.is_a?(Array)
|
67
|
+
ReadMark.transaction do
|
68
|
+
last = read_timestamp(user)
|
69
|
+
|
70
|
+
target.each do |obj|
|
71
|
+
raise ArgumentError unless obj.is_a?(self)
|
72
|
+
|
73
|
+
rm = ReadMark.user(user).readable_type(self.base_class.name).find_by_readable_id(obj.id) ||
|
74
|
+
user.read_marks.build(:readable_id => obj.id, :readable_type => self.base_class.name)
|
75
|
+
rm.timestamp = obj.send(readable_options[:on])
|
76
|
+
rm.save!
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def read_mark(user)
|
83
|
+
check_reader
|
84
|
+
raise ArgumentError unless user.is_a?(ReadMark.reader_class)
|
85
|
+
|
86
|
+
user.read_marks.readable_type(self.base_class.name).global.first
|
87
|
+
end
|
88
|
+
|
89
|
+
def read_timestamp(user)
|
90
|
+
read_mark(user).try(:timestamp)
|
91
|
+
end
|
92
|
+
|
93
|
+
def set_read_mark(user, timestamp)
|
94
|
+
rm = read_mark(user) || user.read_marks.build(:readable_type => self.base_class.name)
|
95
|
+
rm.timestamp = timestamp
|
96
|
+
rm.save!
|
97
|
+
end
|
98
|
+
|
99
|
+
# A scope with all items accessable for the given user
|
100
|
+
# It's used in cleanup_read_marks! to support a filtered cleanup
|
101
|
+
# Should be overriden if a user doesn't have access to all items
|
102
|
+
# Default: User has access to all items and should read them all
|
103
|
+
#
|
104
|
+
# Example:
|
105
|
+
# def Message.read_scope(user)
|
106
|
+
# user.visible_messages
|
107
|
+
# end
|
108
|
+
def read_scope(user)
|
109
|
+
self
|
110
|
+
end
|
111
|
+
|
112
|
+
def cleanup_read_marks!
|
113
|
+
check_reader
|
114
|
+
|
115
|
+
ReadMark.reader_class.find_each do |user|
|
116
|
+
ReadMark.transaction do
|
117
|
+
# Get the timestamp of the oldest unread item the user has access to
|
118
|
+
oldest_timestamp = read_scope(user).unread_by(user).minimum(readable_options[:on])
|
119
|
+
|
120
|
+
if oldest_timestamp
|
121
|
+
# Delete markers OLDER than this timestamp and move the global timestamp for this user
|
122
|
+
user.read_marks.readable_type(self.base_class.name).single.older_than(oldest_timestamp).delete_all
|
123
|
+
set_read_mark(user, oldest_timestamp - 1.second)
|
124
|
+
else
|
125
|
+
# There is no unread item, so mark all as read (which deletes all markers)
|
126
|
+
mark_as_read!(:all, :for => user)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def reset_read_marks!(user = :all)
|
133
|
+
check_reader
|
134
|
+
|
135
|
+
ReadMark.transaction do
|
136
|
+
if user == :all
|
137
|
+
ReadMark.delete_all :readable_type => self.base_class.name
|
138
|
+
|
139
|
+
ReadMark.connection.execute("
|
140
|
+
INSERT INTO read_marks (user_id, readable_type, timestamp)
|
141
|
+
SELECT id, '#{self.base_class.name}', '#{Time.now.to_s(:db)}'
|
142
|
+
FROM #{ReadMark.reader_class.table_name}
|
143
|
+
")
|
144
|
+
else
|
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
|
+
true
|
150
|
+
end
|
151
|
+
|
152
|
+
def check_reader
|
153
|
+
raise RuntimeError, 'Plugin "unread": No reader defined!' unless ReadMark.reader_class
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
module InstanceMethods
|
158
|
+
def unread?(user)
|
159
|
+
self.class.unread_by(user).exists?(self)
|
160
|
+
end
|
161
|
+
|
162
|
+
def mark_as_read!(options)
|
163
|
+
self.class.check_reader
|
164
|
+
|
165
|
+
user = options[:for]
|
166
|
+
raise ArgumentError unless user.is_a?(ReadMark.reader_class)
|
167
|
+
|
168
|
+
ReadMark.transaction do
|
169
|
+
if unread?(user)
|
170
|
+
rm = read_mark(user) || read_marks.build(:user => user)
|
171
|
+
rm.timestamp = self.send(readable_options[:on])
|
172
|
+
rm.save!
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def read_mark(user)
|
178
|
+
read_marks.user(user).first
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
ActiveRecord::Base.send :include, Unread
|
data/lib/unread.rb
ADDED
data/test/database.yml
ADDED
data/test/schema.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 0) do
|
2
|
+
create_table :users, :force => true do |t|
|
3
|
+
t.string :name
|
4
|
+
end
|
5
|
+
|
6
|
+
create_table :emails, :force => true do |t|
|
7
|
+
t.string :subject
|
8
|
+
t.text :content
|
9
|
+
t.datetime :created_at
|
10
|
+
t.datetime :updated_at
|
11
|
+
end
|
12
|
+
|
13
|
+
create_table :read_marks, :force => true do |t|
|
14
|
+
t.integer :readable_id
|
15
|
+
t.integer :user_id, :null => false
|
16
|
+
t.string :readable_type, :null => false
|
17
|
+
t.datetime :timestamp
|
18
|
+
end
|
19
|
+
add_index :read_marks, [:user_id, :readable_type, :readable_id]
|
20
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
gem 'activerecord'
|
4
|
+
gem 'mocha'
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
require 'active_record'
|
8
|
+
require 'active_support'
|
9
|
+
require 'active_support/test_case'
|
10
|
+
|
11
|
+
require File.dirname(__FILE__) + '/../init.rb'
|
12
|
+
|
13
|
+
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
14
|
+
ActiveRecord::Base.establish_connection(config['sqlite3mem'])
|
15
|
+
ActiveRecord::Migration.verbose = false
|
16
|
+
load(File.dirname(__FILE__) + "/schema.rb")
|
17
|
+
|
18
|
+
class User < ActiveRecord::Base
|
19
|
+
acts_as_reader
|
20
|
+
end
|
21
|
+
|
22
|
+
class Email < ActiveRecord::Base
|
23
|
+
acts_as_readable :on => :updated_at
|
24
|
+
end
|
data/test/unread_test.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class UnreadTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@user = User.create! :name => 'David'
|
6
|
+
@other_user = User.create :name => 'Matz'
|
7
|
+
wait
|
8
|
+
@email1 = Email.create!
|
9
|
+
wait
|
10
|
+
@email2 = Email.create!
|
11
|
+
end
|
12
|
+
|
13
|
+
def teardown
|
14
|
+
User.delete_all
|
15
|
+
Email.delete_all
|
16
|
+
ReadMark.delete_all
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_schema_has_loaded_correctly
|
20
|
+
assert_equal [@email1, @email2], Email.all
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_scope
|
24
|
+
assert_equal [@email1, @email2], Email.unread_by(@user)
|
25
|
+
assert_equal [@email1, @email2], Email.unread_by(@other_user)
|
26
|
+
|
27
|
+
assert_equal 2, Email.unread_by(@user).count
|
28
|
+
assert_equal 2, Email.unread_by(@other_user).count
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_scope_after_reset
|
32
|
+
@email1.mark_as_read! :for => @user
|
33
|
+
|
34
|
+
assert_equal [@email2], Email.unread_by(@user)
|
35
|
+
assert_equal 1, Email.unread_by(@user).count
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_unread_by
|
39
|
+
assert_equal true, @email1.unread?(@user)
|
40
|
+
assert_equal true, @email1.unread?(@other_user)
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_unread_after_update
|
44
|
+
@email1.mark_as_read! :for => @user
|
45
|
+
wait
|
46
|
+
@email1.update_attributes! :subject => 'changed'
|
47
|
+
|
48
|
+
assert_equal true, @email1.unread?(@user)
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_mark_as_read
|
52
|
+
@email1.mark_as_read! :for => @user
|
53
|
+
|
54
|
+
assert_equal false, @email1.unread?(@user)
|
55
|
+
assert_equal [@email2], Email.unread_by(@user)
|
56
|
+
|
57
|
+
assert_equal true, @email1.unread?(@other_user)
|
58
|
+
assert_equal [@email1, @email2], Email.unread_by(@other_user)
|
59
|
+
|
60
|
+
assert_equal 1, @user.read_marks.single.count
|
61
|
+
assert_equal @email1, @user.read_marks.single.first.readable
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_mark_as_read_multiple
|
65
|
+
assert_equal true, @email1.unread?(@user)
|
66
|
+
assert_equal true, @email2.unread?(@user)
|
67
|
+
|
68
|
+
Email.mark_as_read! [ @email1, @email2 ], :for => @user
|
69
|
+
|
70
|
+
assert_equal false, @email1.unread?(@user)
|
71
|
+
assert_equal false, @email2.unread?(@user)
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_mark_as_read_with_marked_all
|
75
|
+
wait
|
76
|
+
|
77
|
+
Email.mark_as_read! :all, :for => @user
|
78
|
+
@email1.mark_as_read! :for => @user
|
79
|
+
|
80
|
+
assert_equal [], @user.read_marks.single
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_mark_as_read_twice
|
84
|
+
@email1.mark_as_read! :for => @user
|
85
|
+
@email1.mark_as_read! :for => @user
|
86
|
+
|
87
|
+
assert_equal 1, @user.read_marks.single.count
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_mark_all_as_read
|
91
|
+
Email.mark_as_read! :all, :for => @user
|
92
|
+
assert_equal Time.now.to_s, Email.read_mark(@user).timestamp.to_s
|
93
|
+
|
94
|
+
assert_equal [], @user.read_marks.single
|
95
|
+
assert_equal 0, ReadMark.single.count
|
96
|
+
assert_equal 2, ReadMark.global.count
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_cleanup_read_marks
|
100
|
+
assert_equal 0, @user.read_marks.single.count
|
101
|
+
|
102
|
+
@email1.mark_as_read! :for => @user
|
103
|
+
|
104
|
+
assert_equal [@email2], Email.unread_by(@user)
|
105
|
+
assert_equal 1, @user.read_marks.single.count
|
106
|
+
|
107
|
+
Email.cleanup_read_marks!
|
108
|
+
|
109
|
+
@user.reload
|
110
|
+
assert_equal 0, @user.read_marks.single.count
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_cleanup_read_marks_not_delete_from_other_readables
|
114
|
+
other_read_mark = @user.read_marks.create! :readable_type => 'Foo', :readable_id => 42, :timestamp => 5.years.ago
|
115
|
+
Email.cleanup_read_marks!
|
116
|
+
assert_equal true, ReadMark.exists?(other_read_mark.id)
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_reset_read_marks_for_all
|
120
|
+
Email.reset_read_marks!
|
121
|
+
|
122
|
+
assert_equal 0, ReadMark.single.count
|
123
|
+
assert_equal 2, ReadMark.global.count
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def wait
|
128
|
+
# Skip one second
|
129
|
+
now = Time.now + 1.second
|
130
|
+
Time.stubs(:now).returns(now)
|
131
|
+
end
|
132
|
+
end
|
data/uninstall.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Uninstall hook code here
|
data/unread.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "unread/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "unread"
|
7
|
+
s.version = Unread::VERSION
|
8
|
+
s.authors = ["Georg Ledermann"]
|
9
|
+
s.email = ["mail@georg-ledermann.de"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Manages read/unread status of ActiveRecord objects}
|
12
|
+
s.description = %q{This gem creates a scope for unread objects and adds methods to mark objects as read }
|
13
|
+
|
14
|
+
s.rubyforge_project = "unread"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency 'activerecord', '>= 2.3'
|
22
|
+
s.add_dependency 'activesupport', '>= 2.3'
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: unread
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Georg Ledermann
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-06-23 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activerecord
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 5
|
29
|
+
segments:
|
30
|
+
- 2
|
31
|
+
- 3
|
32
|
+
version: "2.3"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: activesupport
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 5
|
44
|
+
segments:
|
45
|
+
- 2
|
46
|
+
- 3
|
47
|
+
version: "2.3"
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
description: "This gem creates a scope for unread objects and adds methods to mark objects as read "
|
51
|
+
email:
|
52
|
+
- mail@georg-ledermann.de
|
53
|
+
executables: []
|
54
|
+
|
55
|
+
extensions: []
|
56
|
+
|
57
|
+
extra_rdoc_files: []
|
58
|
+
|
59
|
+
files:
|
60
|
+
- .gitignore
|
61
|
+
- Gemfile
|
62
|
+
- MIT-LICENSE
|
63
|
+
- README.md
|
64
|
+
- Rakefile
|
65
|
+
- changelog.md
|
66
|
+
- init.rb
|
67
|
+
- lib/app/models/read_mark.rb
|
68
|
+
- lib/unread.rb
|
69
|
+
- lib/unread/acts_as_readable.rb
|
70
|
+
- lib/unread/version.rb
|
71
|
+
- test/database.yml
|
72
|
+
- test/schema.rb
|
73
|
+
- test/test_helper.rb
|
74
|
+
- test/unread_test.rb
|
75
|
+
- uninstall.rb
|
76
|
+
- unread.gemspec
|
77
|
+
homepage: ""
|
78
|
+
licenses: []
|
79
|
+
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options: []
|
82
|
+
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
hash: 3
|
91
|
+
segments:
|
92
|
+
- 0
|
93
|
+
version: "0"
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
hash: 3
|
100
|
+
segments:
|
101
|
+
- 0
|
102
|
+
version: "0"
|
103
|
+
requirements: []
|
104
|
+
|
105
|
+
rubyforge_project: unread
|
106
|
+
rubygems_version: 1.8.5
|
107
|
+
signing_key:
|
108
|
+
specification_version: 3
|
109
|
+
summary: Manages read/unread status of ActiveRecord objects
|
110
|
+
test_files:
|
111
|
+
- test/database.yml
|
112
|
+
- test/schema.rb
|
113
|
+
- test/test_helper.rb
|
114
|
+
- test/unread_test.rb
|