unread 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
- env:
10
- - DB=sqlite
11
- - DB=mysql
12
- before_script:
13
- - "mysql -e 'create database myapp_test;' >/dev/null"
10
+ - ci/Gemfile.rails-4.0.x
11
+ matrix:
12
+ exclude:
13
+ - rvm: 1.8.7
14
+ gemfile: ci/Gemfile.rails-4.0.x
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010,2012 Georg Ledermann
1
+ Copyright (c) 2010,2013 Georg Ledermann
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
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
- [![Build Status](https://secure.travis-ci.org/ledermann/unread.png)](http://travis-ci.org/ledermann/unread)
6
+ [![Build Status](https://travis-ci.org/ledermann/unread.png?branch=master)](https://travis-ci.org/ledermann/unread)
7
7
  [![Code Climate](https://codeclimate.com/github/ledermann/unread.png)](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). The use with Rails 2.3 there is a branch named "rails2"
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
 
@@ -1,52 +1,58 @@
1
- 0.2.0 - WIP
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
@@ -1,7 +1,6 @@
1
- source :rubygems
1
+ source 'https://rubygems.org'
2
2
 
3
3
  gem 'activerecord', '~> 3.0.10'
4
4
  gem 'sqlite3'
5
- gem 'mysql2', '~> 0.2.11'
6
5
  gem 'timecop'
7
6
  gem 'rake'
@@ -1,7 +1,6 @@
1
- source :rubygems
1
+ source 'https://rubygems.org'
2
2
 
3
3
  gem 'activerecord', '~> 3.1.1'
4
4
  gem 'sqlite3'
5
- gem 'mysql2', '>= 0.3.6'
6
5
  gem 'timecop'
7
6
  gem 'rake'
@@ -1,7 +1,6 @@
1
- source :rubygems
1
+ source 'https://rubygems.org'
2
2
 
3
3
  gem 'activerecord', '~> 3.2.1'
4
4
  gem 'sqlite3'
5
- gem 'mysql2', '>= 0.3.6'
6
5
  gem 'timecop'
7
6
  gem 'rake'
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '4.0.0.beta1'
4
+ gem 'sqlite3'
5
+ gem 'timecop'
6
+ gem 'rake'
@@ -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
- require 'app/models/read_mark'
3
- require 'unread/acts_as_readable'
7
+
8
+ ActiveRecord::Base.send :include, Unread
@@ -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
- attr_accessible :readable_id, :user_id, :readable_type, :timestamp
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 ActsAsReadable::acts_as_reader
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
@@ -1,3 +1,3 @@
1
1
  module Unread
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -1,8 +1,3 @@
1
1
  sqlite:
2
2
  adapter: sqlite3
3
3
  database: ":memory:"
4
- mysql:
5
- adapter: mysql2
6
- database: myapp_test
7
- username:
8
- encoding: utf8
@@ -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
- db_name = ENV['DB'] || 'sqlite'
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
 
@@ -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).all
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.travel(1.minute)
164
+ Timecop.freeze(1.minute.from_now.change(:usec => 0))
164
165
  end
165
166
  end
@@ -23,5 +23,4 @@ Gem::Specification.new do |s|
23
23
  s.add_development_dependency 'rake'
24
24
  s.add_development_dependency 'timecop'
25
25
  s.add_development_dependency 'sqlite3'
26
- s.add_development_dependency 'mysql2'
27
26
  end
metadata CHANGED
@@ -1,97 +1,72 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unread
3
3
  version: !ruby/object:Gem::Version
4
- prerelease:
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-02-18 00:00:00.000000000 Z
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
- none: false
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: '0'
37
- none: false
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
- none: false
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
- none: false
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
- none: false
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
- none: false
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
- none: false
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
- none: false
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
- - lib/app/models/read_mark.rb
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/acts_as_readable.rb
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: 1.8.25
121
+ rubygems_version: 2.0.3
144
122
  signing_key:
145
- specification_version: 3
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