unread 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in unread.gemspec
4
+ gemspec
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
@@ -0,0 +1,3 @@
1
+ 0.0.1 - 2011/06/23
2
+
3
+ * Released as Gem
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
@@ -0,0 +1,3 @@
1
+ module Unread
2
+ VERSION = "0.0.1"
3
+ end
data/lib/unread.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'unread/version'
2
+ require 'app/models/read_mark'
3
+ require 'unread/acts_as_readable'
data/test/database.yml ADDED
@@ -0,0 +1,3 @@
1
+ sqlite3mem:
2
+ adapter: sqlite3
3
+ database: ":memory:"
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
@@ -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
@@ -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