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

pulibrary / pdc_describe / 9091a1ae-29be-458c-984a-339d213919c4

12 Dec 2024 07:41PM UTC coverage: 26.434% (-69.7%) from 96.113%
9091a1ae-29be-458c-984a-339d213919c4

Pull #2000

circleci

jrgriffiniii
Removing integration with ActiveStorage
Pull Request #2000: Bump actionpack from 7.2.1.1 to 7.2.2.1

945 of 3575 relevant lines covered (26.43%)

0.35 hits per line

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

33.73
/app/models/work_activity.rb
1
# frozen_string_literal: true
2

3
require_relative "../lib/diff_tools"
1✔
4

5
# rubocop:disable Metrics/ClassLength
6
class WorkActivity < ApplicationRecord
1✔
7
  MESSAGE = "COMMENT" # TODO: Migrate existing records to "MESSAGE"; then close #825.
1✔
8
  NOTIFICATION = "NOTIFICATION"
1✔
9
  MESSAGE_ACTIVITY_TYPES = [MESSAGE, NOTIFICATION].freeze
1✔
10

11
  CHANGES = "CHANGES"
1✔
12
  DATACITE_ERROR = "DATACITE-ERROR"
1✔
13
  FILE_CHANGES = "FILE-CHANGES"
1✔
14
  MIGRATION_START = "MIGRATION_START"
1✔
15
  MIGRATION_COMPLETE = "MIGRATION_COMPLETE"
1✔
16
  PROVENANCE_NOTES = "PROVENANCE-NOTES"
1✔
17
  SYSTEM = "SYSTEM"
1✔
18
  CHANGE_LOG_ACTIVITY_TYPES = [CHANGES, FILE_CHANGES, PROVENANCE_NOTES, SYSTEM, DATACITE_ERROR, MIGRATION_COMPLETE].freeze
1✔
19

20
  USER_REFERENCE = /@[\w]*/ # e.g. @xy123
1✔
21

22
  include Rails.application.routes.url_helpers
1✔
23

24
  belongs_to :work
1✔
25
  has_many :work_activity_notifications, dependent: :destroy
1✔
26

27
  def self.add_work_activity(work_id, message, user_id, activity_type:, created_at: nil)
1✔
28
    activity = WorkActivity.new(
×
29
      work_id:,
30
      activity_type:,
31
      message:,
32
      created_by_user_id: user_id,
33
      created_at: # If nil, will be set by activerecord at save.
34
    )
35
    activity.save!
×
36
    activity.notify_users
×
37
    activity
×
38
  end
39

40
  def self.activities_for_work(work_id, activity_types)
1✔
41
    where(work_id:, activity_type: activity_types)
×
42
  end
43

44
  def self.messages_for_work(work_id)
1✔
45
    activities_for_work(work_id, MESSAGE_ACTIVITY_TYPES)
×
46
  end
47

48
  def self.changes_for_work(work_id)
1✔
49
    activities_for_work(work_id, CHANGE_LOG_ACTIVITY_TYPES)
×
50
  end
51

52
  # Log notifications for each of the users references on the activity
53
  def notify_users
1✔
54
    users_referenced.each do |uid|
×
55
      user_id = User.where(uid:).first&.id
×
56
      if user_id.nil?
×
57
        notify_group(uid)
×
58
      else
59
        WorkActivityNotification.create(work_activity_id: id, user_id:)
×
60
      end
61
    end
62
  end
63

64
  def notify_group(groupid)
1✔
65
    group = Group.where(code: groupid).first
×
66
    if group.nil?
×
67
      Rails.logger.info("Message #{id} for work #{work_id} referenced an non-existing user: #{groupid}")
×
68
    else
69
      group.administrators.each do |admin|
×
70
        WorkActivityNotification.create(work_activity_id: id, user_id: admin.id)
×
71
      end
72
    end
73
  end
74

75
  # Returns the `uid` of the users referenced on the activity (without the `@` symbol)
76
  def users_referenced
1✔
77
    message.scan(USER_REFERENCE).map { |at_uid| at_uid[1..-1] }
×
78
  end
79

80
  def created_by_user
1✔
81
    return nil unless created_by_user_id
×
82
    User.find(created_by_user_id)
×
83
  end
84

85
  def message_event_type?
1✔
86
    MESSAGE_ACTIVITY_TYPES.include? activity_type
×
87
  end
88

89
  def log_event_type?
1✔
90
    CHANGE_LOG_ACTIVITY_TYPES.include? activity_type
×
91
  end
92

93
  def renderer
1✔
94
    @renderer ||= begin
×
95
                    klass = if activity_type == CHANGES
×
96
                              MetadataChanges
×
97
                            elsif activity_type == FILE_CHANGES
×
98
                              FileChanges
×
99
                            elsif activity_type == MIGRATION_COMPLETE
×
100
                              Migration
×
101
                            elsif activity_type == PROVENANCE_NOTES
×
102
                              ProvenanceNote
×
103
                            elsif CHANGE_LOG_ACTIVITY_TYPES.include?(activity_type)
×
104
                              OtherLogEvent
×
105
                            else
106
                              Message
×
107
                            end
108
                    klass.new(self)
×
109

110
                  end
111
  end
112

113
  delegate :to_html, to: :renderer
1✔
114

115
  class Renderer
1✔
116
    def initialize(work_activity)
1✔
117
      @work_activity = work_activity
×
118
    end
119

120
    UNKNOWN_USER = "Unknown user outside the system"
1✔
121
    DATE_TIME_FORMAT = "%B %d, %Y %H:%M"
1✔
122
    DATE_FORMAT = "%B %d, %Y"
1✔
123
    SORTABLE_DATE_TIME_FORMAT = "%Y-%m-%d %H:%M"
1✔
124

125
    def to_html
1✔
126
      title_html + "<span class='message-html'>#{body_html.chomp}</span>"
×
127
    end
128

129
    def created_by_user_html
1✔
130
      return UNKNOWN_USER unless @work_activity.created_by_user
×
131

132
      "#{@work_activity.created_by_user.given_name_safe} (@#{@work_activity.created_by_user.uid})"
×
133
    end
134

135
    def created_sortable_html
1✔
136
      @work_activity.created_at.time.strftime(SORTABLE_DATE_TIME_FORMAT)
×
137
    end
138

139
    def created_updated_html
1✔
140
      created = @work_activity.created_at.time.strftime(DATE_TIME_FORMAT)
×
141
      updated = @work_activity.updated_at.time.strftime(DATE_TIME_FORMAT)
×
142
      created_date = @work_activity.created_at.time.strftime(DATE_FORMAT)
×
143
      updated_date = @work_activity.updated_at.time.strftime(DATE_FORMAT)
×
144
      if created_date == updated_date
×
145
        created
×
146
      else
147
        "#{created} (backdated event created #{updated})"
×
148
      end
149
    end
150

151
    def title_html
1✔
152
      "<span class='activity-history-title'>#{created_updated_html} by #{created_by_user_html}</span>"
×
153
    end
154
  end
155

156
  class MetadataChanges < Renderer
1✔
157
    # Returns the message formatted to display _metadata_ changes that were logged as an activity
158
    def body_html
1✔
159
      message_json = JSON.parse(@work_activity.message)
×
160

161
      # Messages should consistently be Arrays of Hashes, but this might require a migration from legacy field records
162
      messages = if message_json.is_a?(Array)
×
163
                   message_json
×
164
                 else
165
                   Array.wrap(message_json)
×
166
                 end
167

168
      elements = messages.map do |message|
×
169
        markup = if message.is_a?(Hash)
×
170
                   message.keys.map do |field|
×
171
                     mapped = message[field].map { |value| change_value_html(value) }
×
172
                     "<details class='message-html'><summary class='show-changes'>#{field&.titleize}</summary>#{mapped.join}</details>"
×
173
                   end
174
                 else
175
                   # For handling cases where WorkActivity#message only contains Strings, or Arrays of Strings
176
                   [
177
                     "<details class='message-html'><summary class='show-changes'></summary>#{message}</details>"
×
178
                   ]
179
                 end
180
        markup.join
×
181
      end
182

183
      elements.flatten.join
×
184
    end
185

186
    def change_value_html(value)
1✔
187
      if value["action"] == "changed"
×
188
        DiffTools::SimpleDiff.new(value["from"], value["to"]).to_html
×
189
      else
190
        "old change"
×
191
      end
192
    end
193
  end
194

195
  class FileChanges < Renderer
1✔
196
    # Returns the message formatted to display _file_ changes that were logged as an activity
197
    def body_html
1✔
198
      changes = JSON.parse(@work_activity.message)
×
199
      if changes.is_a?(Hash)
×
200
        changes = [changes]
×
201
      end
202

203
      files_added = changes.select { |v| v["action"] == "added" }
×
204
      files_deleted = changes.select { |v| v["action"] == "removed" }
×
205
      files_replaced = changes.select { |v| v["action"] == "replaced" }
×
206

207
      changes_html = []
×
208
      unless files_added.empty?
×
209
        label = "Files Added: "
×
210
        label += files_added.length.to_s
×
211
        changes_html << "<tr><td>#{label}</td></tr>"
×
212
      end
213

214
      unless files_deleted.empty?
×
215
        label = "Files Deleted: "
×
216
        label += files_deleted.length.to_s
×
217
        changes_html << "<tr><td>#{label}</td></tr>"
×
218
      end
219

220
      unless files_replaced.empty?
×
221
        label = "Files Replaced: "
×
222
        label += files_replaced.length.to_s
×
223
        changes_html << "<tr><td>#{label}</td></tr>"
×
224
      end
225

226
      "<table>#{changes_html.join}</table>"
×
227
    end
228
  end
229

230
  class Migration < Renderer
1✔
231
    # Returns the message formatted to display _file_ changes that were logged as an activity
232
    def body_html
1✔
233
      changes = JSON.parse(@work_activity.message)
×
234
      "<p>#{changes['message']}</p>"
×
235
    end
236
  end
237

238
  class BaseMessage < Renderer
1✔
239
    def body_html
1✔
240
      text = user_refernces(@work_activity.message)
×
241
      mark_down_to_html(text)
×
242
    end
243

244
    def mark_down_to_html(text_in)
1✔
245
      # allow ``` for code blocks (Kramdown only supports ~~~)
246
      text = text_in.gsub("```", "~~~")
×
247
      Kramdown::Document.new(text).to_html
×
248
    end
249

250
    def user_refernces(text_in)
1✔
251
      # convert user references to user links
252
      text_in.gsub(USER_REFERENCE) do |at_uid|
×
253
        uid = at_uid[1..-1]
×
254

255
        if uid
×
256
          group = Group.find_by(code: uid)
×
257
          if group
×
258
            "<a class='message-user-link' title='#{group.title}' href='#{@work_activity.group_path(group)}'>#{group.title}</a>"
×
259
          else
260
            user = User.find_by(uid:)
×
261
            user_info = if user
×
262
                          user.given_name_safe
×
263
                        else
264
                          uid
×
265
                        end
266
            "<a class='message-user-link' title='#{user_info}' href='#{@work_activity.users_path}/#{uid}'>#{at_uid}</a>"
×
267
          end
268
        else
269
          Rails.logger.warn("Failed to extract the user ID from #{uid}")
×
270
          UNKNOWN_USER
×
271
        end
272
      end
273
    end
274
  end
275

276
  class OtherLogEvent < BaseMessage
1✔
277
  end
278

279
  class Message < BaseMessage
1✔
280
    # Override the default:
281
    def created_by_user_html
1✔
282
      return UNKNOWN_USER unless @work_activity.created_by_user
×
283

284
      user = @work_activity.created_by_user
×
285
      "#{user.given_name_safe} (@#{user.uid})"
×
286
    end
287

288
    def title_html
1✔
289
      "<span class='activity-history-title'>#{created_by_user_html} at #{created_updated_html}</span>"
×
290
    end
291
  end
292

293
  class ProvenanceNote < BaseMessage
1✔
294
    def body_html
1✔
295
      message_hash = JSON.parse(@work_activity.message)
×
296
      text = user_refernces(message_hash["note"])
×
297
      message = mark_down_to_html(text)
×
298
      change_label = message_hash["change_label"]&.titleize
×
299
      change_label ||= "Change"
×
300
      # TODO: Make this show the change label with the note under see changes
301
      "<details class='message-html'><summary class='show-changes'>#{change_label}</summary>#{message}</details>"
×
302
    end
303
  end
304
end
305
# rubocop:enable Metrics/ClassLength
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