unread 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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