unread 0.0.1
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.
- 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
|