• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ledermann / unread / #698

17 Aug 2024 12:57PM UTC coverage: 97.788% (-0.8%) from 98.63%
#698

push

ledermann
Add support for Rails 7.2

6 of 8 new or added lines in 2 files covered. (75.0%)

3 existing lines in 1 file now uncovered.

221 of 226 relevant lines covered (97.79%)

46.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.0
/lib/unread/readable.rb
1
module Unread
1✔
2
  module Readable
1✔
3
    module ClassMethods
1✔
4
      def mark_as_read!(target, options)
1✔
5
        raise ArgumentError unless options.is_a?(Hash)
221✔
6

7
        reader = options[:for]
220✔
8
        assert_reader(reader)
220✔
9

10
        if target == :all
220✔
11
          reset_read_marks_for_user(reader)
213✔
12
        elsif target.respond_to?(:each)
7✔
13
          mark_collection_as_read(target, reader)
6✔
14
        else
15
          raise ArgumentError
1✔
16
        end
17
      end
18

19
      def mark_collection_as_read(collection, reader)
1✔
20
        ReadMark.transaction do
6✔
21
          global_timestamp = reader.read_mark_global(self).try(:timestamp)
6✔
22

23
          collection.each do |obj|
6✔
24
            raise ArgumentError unless obj.is_a?(self)
10✔
25
            timestamp = obj.send(readable_options[:on])
10✔
26

27
            if global_timestamp && global_timestamp >= timestamp
10✔
28
              # The object is implicitly marked as read, so there is nothing to do
29
            else
30
              mark_collection_item_as_read(obj, reader, timestamp)
8✔
31
            end
32
          end
33
        end
34
      end
35

36
      def mark_collection_item_as_read(obj, reader, timestamp)
1✔
37
        marking_proc = proc {
8✔
38
          rm = obj.read_marks.find_or_initialize_by(reader: reader)
8✔
39
          rm.timestamp = timestamp
8✔
40
          rm.save!
8✔
41
        }
42

43
        if using_postgresql?
8✔
44
          # With PostgreSQL, a transaction is unusable after a unique constraint vialoation.
45
          # To avoid this, nested transactions are required.
46
          # http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Exception+handling+and+rolling+back
UNCOV
47
          ReadMark.transaction(requires_new: true) do
×
48
            begin
UNCOV
49
              marking_proc.call
×
50
            rescue ActiveRecord::RecordNotUnique
51
              # The object is explicitly marked as read, so rollback the inner transaction
UNCOV
52
              raise ActiveRecord::Rollback
×
53
            end
54
          end
55
        else
56
          begin
57
            marking_proc.call
8✔
58
          rescue ActiveRecord::RecordNotUnique
59
            # The object is explicitly marked as read, so there is nothing to do
60
          end
61
        end
62
      end
63

64
      # A scope with all items accessable for the given reader
65
      # It's used in cleanup_read_marks! to support a filtered cleanup
66
      # Should be overriden if a reader doesn't have access to all items
67
      # Default: reader has access to all items and should read them all
68
      #
69
      # Example:
70
      #   def Message.read_scope(reader)
71
      #     reader.visible_messages
72
      #   end
73
      def read_scope(reader)
1✔
74
        self
2✔
75
      end
76

77
      def readable_parent
1✔
78
        self.ancestors.find { |ancestor| ReadMark.readable_classes.include?(ancestor) }
1,851✔
79
      end
80

81
      def cleanup_read_marks!
1✔
82
        assert_reader_class
1✔
83
        Unread::GarbageCollector.new(self).run!
1✔
84
      end
85

86
      def reset_read_marks_for_user(reader)
1✔
87
        assert_reader(reader)
214✔
88

89
        ReadMark.transaction do
214✔
90
          reader.read_marks.where(readable_type: self.readable_parent.name).delete_all
214✔
91
          rm = reader.read_marks.new
214✔
92
          rm.readable_type = self.readable_parent.name
214✔
93
          rm.timestamp = Time.current
214✔
94
          rm.save!
214✔
95
        end
96

97
        reader.forget_memoized_read_mark_global
214✔
98
      end
99

100
      def assert_reader(reader)
1✔
101
        assert_reader_class
556✔
102

103
        raise ArgumentError, "Class #{reader.class.name} is not registered by acts_as_reader." unless ReadMark.reader_classes.any? { |klass| reader.is_a?(klass) }
1,402✔
104
        raise ArgumentError, "The given reader has no id." unless reader.id
530✔
105
      end
106

107
      def assert_reader_class
1✔
108
        raise RuntimeError, 'There is no class using acts_as_reader.' unless ReadMark.reader_classes
557✔
109
      end
110
    end
111

112
    module InstanceMethods
1✔
113
      def unread?(reader)
1✔
114
        if self.respond_to?(:read_mark_id) && read_mark_id_belongs_to?(reader)
44✔
115
          # For use with scope "with_read_marks_for"
116
          return false if self.read_mark_id
5✔
117

118
          if global_timestamp = reader.read_mark_global(self.class).try(:timestamp)
3✔
119
            self.send(readable_options[:on]) > global_timestamp
1✔
120
          else
121
            true
2✔
122
          end
123
        else
124
          self.class.unread_by(reader).exists?(self.id)
39✔
125
        end
126
      end
127

128
      def mark_as_read!(options)
1✔
129
        reader = options[:for]
22✔
130
        self.class.assert_reader(reader)
22✔
131

132
        ReadMark.transaction do
22✔
133
          if unread?(reader)
22✔
134
            rm = read_mark(reader) || read_marks.build
20✔
135
            rm.reader_id   = reader.id
20✔
136
            rm.reader_type = reader.class.base_class.name
20✔
137
            rm.timestamp   = self.send(readable_options[:on])
20✔
138
            rm.save!
20✔
139
          end
140
        end
141
      end
142

143
      private
1✔
144

145
      def read_mark(reader)
1✔
146
        read_marks.where(reader_id: reader.id, reader_type: reader.class.base_class.name).first
20✔
147
      end
148

149
      def read_mark_id_belongs_to?(reader)
1✔
150
        self.read_mark_reader_id.to_i == reader.id &&
6✔
151
          self.read_mark_reader_type == reader.class.base_class.name
152
      end
153
    end
154
  end
155
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc