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

pulibrary / pdc_describe / 10e67590-c335-4eef-8035-ba8ad42eff83

pending completion
10e67590-c335-4eef-8035-ba8ad42eff83

Pull #1060

circleci

jrgriffiniii
wip
Pull Request #1060: Integrating Support for Upload Snapshots

155 of 155 new or added lines in 8 files covered. (100.0%)

2095 of 2149 relevant lines covered (97.49%)

174.83 hits per line

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

94.17
/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
  FILE_CHANGES = "FILE-CHANGES"
1✔
13
  PROVENANCE_NOTES = "PROVENANCE-NOTES"
1✔
14
  SYSTEM = "SYSTEM"
1✔
15
  DATACITE_ERROR = "DATACITE-ERROR"
1✔
16
  CHANGE_LOG_ACTIVITY_TYPES = [CHANGES, FILE_CHANGES, PROVENANCE_NOTES, SYSTEM, DATACITE_ERROR].freeze
1✔
17

18
  USER_REFERENCE = /@[\w]*/.freeze # e.g. @xy123
1✔
19

20
  include Rails.application.routes.url_helpers
1✔
21

22
  belongs_to :work
1✔
23
  has_many :work_activity_notifications, dependent: :destroy
1✔
24

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

38
  def self.activities_for_work(work_id, activity_types)
1✔
39
    where(work_id: work_id, activity_type: activity_types)
272✔
40
  end
41

42
  def self.messages_for_work(work_id)
1✔
43
    activities_for_work(work_id, MESSAGE_ACTIVITY_TYPES)
91✔
44
  end
45

46
  def self.changes_for_work(work_id)
1✔
47
    activities_for_work(work_id, CHANGE_LOG_ACTIVITY_TYPES)
92✔
48
  end
49

50
  # Log notifications for each of the users references on the activity
51
  def notify_users
1✔
52
    users_referenced.each do |uid|
408✔
53
      user_id = User.where(uid: uid).first&.id
199✔
54
      if user_id.nil?
199✔
55
        Rails.logger.info("Message #{id} for work #{work_id} referenced an non-existing user: #{uid}")
3✔
56
      else
57
        WorkActivityNotification.create(work_activity_id: id, user_id: user_id)
196✔
58
      end
59
    end
60
  end
61

62
  # Returns the `uid` of the users referenced on the activity (without the `@` symbol)
63
  def users_referenced
1✔
64
    message.scan(USER_REFERENCE).map { |at_uid| at_uid[1..-1] }
607✔
65
  end
66

67
  def created_by_user
1✔
68
    return nil unless created_by_user_id
269✔
69

70
    User.find(created_by_user_id)
248✔
71
  end
72

73
  def message_event_type?
1✔
74
    MESSAGE_ACTIVITY_TYPES.include? activity_type
×
75
  end
76

77
  def log_event_type?
1✔
78
    CHANGE_LOG_ACTIVITY_TYPES.include? activity_type
×
79
  end
80

81
  def to_html
1✔
82
    klass = if activity_type == CHANGES
142✔
83
              MetadataChanges
26✔
84
            elsif activity_type == FILE_CHANGES
116✔
85
              FileChanges
25✔
86
            elsif CHANGE_LOG_ACTIVITY_TYPES.include?(activity_type)
91✔
87
              OtherLogEvent
43✔
88
            else
89
              Message
48✔
90
            end
91
    renderer = klass.new(self)
142✔
92
    renderer.to_html
142✔
93
  end
94

95
  class Renderer
1✔
96
    def initialize(work_activity)
1✔
97
      @work_activity = work_activity
142✔
98
    end
99

100
    UNKNOWN_USER = "Unknown user outside the system"
1✔
101
    DATE_TIME_FORMAT = "%B %d, %Y %H:%M"
1✔
102

103
    def to_html
1✔
104
      title_html + "<span class='message-html'>#{body_html.chomp}</span>"
142✔
105
    end
106

107
    def created_by_user_html
1✔
108
      return UNKNOWN_USER unless @work_activity.created_by_user
94✔
109

110
      @work_activity.created_by_user.display_name_safe
74✔
111
    end
112

113
    def created_updated_html
1✔
114
      created = @work_activity.created_at.time.strftime(DATE_TIME_FORMAT)
142✔
115
      updated = @work_activity.updated_at.time.strftime(DATE_TIME_FORMAT)
142✔
116
      if created == updated
142✔
117
        created
132✔
118
      else
119
        "#{created} (backdated event created #{updated})"
10✔
120
      end
121
    end
122

123
    def title_html
1✔
124
      "<span class='activity-history-title'>#{created_updated_html} by #{created_by_user_html}</span>"
94✔
125
    end
126
  end
127

128
  class MetadataChanges < Renderer
1✔
129
    # Returns the message formatted to display _metadata_ changes that were logged as an activity
130
    def body_html
1✔
131
      changes = JSON.parse(@work_activity.message)
26✔
132

133
      changes.keys.map do |field|
26✔
134
        mapped = changes[field].map { |value| change_value_html(value) }
112✔
135
        "<details class='message-html'><summary class='show-changes'>#{field}</summary>#{mapped.join}</details>"
56✔
136
      end.join
137
    end
138

139
    def change_value_html(value)
1✔
140
      if value["action"] == "changed"
56✔
141
        DiffTools::SimpleDiff.new(value["from"], value["to"]).to_html
56✔
142
      else
143
        "old change"
×
144
      end
145
    end
146
  end
147

148
  class FileChanges < Renderer
1✔
149
    # Returns the message formatted to display _file_ changes that were logged as an activity
150
    def body_html
1✔
151
      changes = JSON.parse(@work_activity.message)
25✔
152

153
      files_added = changes.select { |v| v["action"] == "added" }
52✔
154
      files_deleted = changes.select { |v| v["action"] == "deleted" }
52✔
155
      files_replaced = changes.select { |v| v["action"] == "replaced" }
52✔
156

157
      changes_html = []
25✔
158
      unless files_added.empty?
25✔
159
        label = "Files Added: "
3✔
160
        label += files_added.length.to_s
3✔
161
        changes_html << "<tr><td>#{label}</td></tr>"
3✔
162
      end
163

164
      unless files_deleted.empty?
25✔
165
        label = "Files Deleted: "
2✔
166
        label += files_deleted.length.to_s
2✔
167
        changes_html << "<tr><td>#{label}</td></tr>"
2✔
168
      end
169

170
      unless files_replaced.empty?
25✔
171
        label = "Files Replaced: "
×
172
        label += files_replaced.length.to_s
×
173
        changes_html << "<tr><td>#{label}</td></tr>"
×
174
      end
175

176
      "<table>#{changes_html.join}</table>"
25✔
177
    end
178
  end
179

180
  class BaseMessage < Renderer
1✔
181
    # rubocop:disable Metrics/MethodLength
182
    def body_html
1✔
183
      # convert user references to user links
184
      text = @work_activity.message.gsub(USER_REFERENCE) do |at_uid|
91✔
185
        uid = at_uid[1..-1]
47✔
186
        user_info = UNKNOWN_USER
47✔
187

188
        if uid
47✔
189
          user = User.find_by(uid: uid)
47✔
190
          user_info = if user
47✔
191
                        user.display_name_safe
47✔
192
                      else
193
                        uid
×
194
                      end
195
        end
196

197
        "<a class='message-user-link' title='#{user_info}' href='#{@work_activity.users_path}/#{uid}'>#{at_uid}</a>"
47✔
198
      end
199

200
      # allow ``` for code blocks (Kramdown only supports ~~~)
201
      text = text.gsub("```", "~~~")
91✔
202
      Kramdown::Document.new(text).to_html
91✔
203
    end
204
    # rubocop:enable Metrics/MethodLength
205
  end
206

207
  class OtherLogEvent < BaseMessage
1✔
208
  end
209

210
  class Message < BaseMessage
1✔
211
    # Override the default:
212
    def created_by_user_html
1✔
213
      return UNKNOWN_USER unless @work_activity.created_by_user
48✔
214

215
      user = @work_activity.created_by_user
47✔
216
      "#{user.display_name_safe} (@#{user.uid})"
47✔
217
    end
218

219
    def title_html
1✔
220
      "<span class='activity-history-title'>#{created_by_user_html} at #{created_updated_html}</span>"
48✔
221
    end
222
  end
223
end
224
# 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