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

pulibrary / pdc_describe / 4e4e59fc-9df4-4838-9fd4-6c7ea33cdb7c

07 Apr 2025 06:36PM UTC coverage: 1.283% (-94.6%) from 95.862%
4e4e59fc-9df4-4838-9fd4-6c7ea33cdb7c

Pull #1994

circleci

hectorcorrea
Switched to use the autocomplete that we aleady use for ROR. Integrated it with the existing logic for creators
Pull Request #1994: Started adding auto complete to contributors

0 of 46 new or added lines in 2 files covered. (0.0%)

4806 existing lines in 74 files now uncovered.

65 of 5065 relevant lines covered (1.28%)

0.01 hits per line

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

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

UNCOV
3
require_relative "../lib/diff_tools"
×
4

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

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

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

UNCOV
22
  include Rails.application.routes.url_helpers
×
23

UNCOV
24
  belongs_to :work
×
UNCOV
25
  has_many :work_activity_notifications, dependent: :destroy
×
26

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

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

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

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

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

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

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

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

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

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

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

UNCOV
110
                  end
×
UNCOV
111
  end
×
112

UNCOV
113
  delegate :to_html, to: :renderer
×
114

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

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

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

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

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

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

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

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

UNCOV
156
  class MetadataChanges < Renderer
×
157
    # Returns the message formatted to display _metadata_ changes that were logged as an activity
UNCOV
158
    def body_html
×
UNCOV
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
UNCOV
162
      messages = if message_json.is_a?(Array)
×
163
                   message_json
×
UNCOV
164
                 else
×
UNCOV
165
                   Array.wrap(message_json)
×
UNCOV
166
                 end
×
167

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

UNCOV
183
      elements.flatten.join
×
UNCOV
184
    end
×
185

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
276
  class OtherLogEvent < BaseMessage
×
UNCOV
277
  end
×
278

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

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

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

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