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

pulibrary / pdc_describe / 77768dec-7663-499a-a72c-449cd45d41f7

pending completion
77768dec-7663-499a-a72c-449cd45d41f7

Pull #861

circleci

GitHub
Merge branch 'main' into mccalluc-work-activity-more-oo
Pull Request #861: Make work_activity render code more OO, and hopefully more maintainable

46 of 46 new or added lines in 1 file covered. (100.0%)

1763 of 1775 relevant lines covered (99.32%)

156.14 hits per line

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

96.12
/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:, date: nil)
1✔
26
    activity = WorkActivity.new(
325✔
27
      work_id: work_id,
28
      activity_type: activity_type,
29
      message: message,
30
      created_by_user_id: user_id,
31
      created_at: date # If nil, will be set by activerecord at save.
32
    )
33
    activity.save!
325✔
34
    activity.notify_users
325✔
35
    activity
325✔
36
  end
37

38
  def self.activities_for_work(work_id, types = [])
1✔
39
    context = where(work_id: work_id).order(updated_at: :desc)
233✔
40
    if types.count > 0
233✔
41
      context = context.where(activity_type: types)
160✔
42
    end
43
    context
233✔
44
  end
45

46
  def self.messages_for_work(work_id)
1✔
47
    activities_for_work(work_id, MESSAGE_ACTIVITY_TYPES)
77✔
48
  end
49

50
  def self.changes_for_work(work_id)
1✔
51
    activities_for_work(work_id, CHANGE_LOG_ACTIVITY_TYPES)
78✔
52
  end
53

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

66
  # Returns the `uid` of the users referenced on the activity (without the `@` symbol)
67
  def users_referenced
1✔
68
    message.scan(USER_REFERENCE).map { |at_uid| at_uid[1..-1] }
524✔
69
  end
70

71
  def created_by_user
1✔
72
    return nil unless created_by_user_id
158✔
73

74
    User.find(created_by_user_id)
153✔
75
  end
76

77
  def message_event_type?
1✔
78
    MESSAGE_ACTIVITY_TYPES.include? activity_type
×
79
  end
80

81
  def log_event_type?
1✔
82
    CHANGE_LOG_ACTIVITY_TYPES.include? activity_type
×
83
  end
84

85
  def to_html
1✔
86
    klass = if activity_type == CHANGES
79✔
87
              MetadataChanges
18✔
88
            elsif activity_type == FILE_CHANGES
61✔
89
              FileChanges
7✔
90
            elsif CHANGE_LOG_ACTIVITY_TYPES.include?(activity_type)
54✔
91
              OtherLogEvent
24✔
92
            else
93
              Message
30✔
94
            end
95
    renderer = klass.new(self)
79✔
96
    renderer.to_html
79✔
97
  end
98

99
  class Renderer
1✔
100
    def initialize(work_activity)
1✔
101
      @work_activity = work_activity
79✔
102
    end
103

104
    UNKNOWN_USER = "Unknown user outside the system"
1✔
105

106
    def to_html
1✔
107
      title_html + "<span class='message-html'>#{body_html.chomp}</span>"
79✔
108
    end
109

110
    def created_by_user_html
1✔
111
      return UNKNOWN_USER unless @work_activity.created_by_user
79✔
112

113
      @work_activity.created_by_user.display_name_safe
74✔
114
    end
115

116
    def created_at_html
1✔
117
      @work_activity.created_at.time.strftime("%B %d, %Y %H:%M")
79✔
118
    end
119

120
    def title_html
1✔
121
      "<span class='activity-history-title'>#{created_at_html} by #{created_by_user_html}</span>"
49✔
122
    end
123
  end
124

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

130
      changes.keys.map do |field|
18✔
131
        mapped = changes[field].map { |value| change_value_html(value) }
88✔
132
        "<details class='message-html'><summary class='show-changes'>#{field}</summary>#{mapped.join}</details>"
44✔
133
      end.join
134
    end
135

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

145
  class FileChanges < Renderer
1✔
146
    # Returns the message formatted to display _file_ changes that were logged as an activity
147
    def body_html
1✔
148
      changes = JSON.parse(@work_activity.message)
7✔
149
      changes_html = changes.map do |change|
7✔
150
        icon = if change["action"] == "deleted"
13✔
151
                 '<i class="bi bi-file-earmark-minus-fill file-deleted-icon"></i>'
2✔
152
               else
153
                 '<i class="bi bi-file-earmark-plus-fill file-added-icon"></i>'
11✔
154
               end
155
        "<tr><td>#{icon}</td><td>#{change['action']}</td> <td>#{change['filename']}</td>"
13✔
156
      end
157

158
      "<p><b>Files updated:</b></p><table>#{changes_html.join}</table>"
7✔
159
    end
160
  end
161

162
  class BaseMessage < Renderer
1✔
163
    # rubocop:disable Metrics/MethodLength
164
    def body_html
1✔
165
      # convert user references to user links
166
      text = @work_activity.message.gsub(USER_REFERENCE) do |at_uid|
54✔
167
        uid = at_uid[1..-1]
48✔
168
        user_info = UNKNOWN_USER
48✔
169

170
        if uid
48✔
171
          user = User.find_by(uid: uid)
48✔
172
          user_info = if user
48✔
173
                        user.display_name_safe
48✔
174
                      else
175
                        uid
×
176
                      end
177
        end
178

179
        "<a class='message-user-link' title='#{user_info}' href='#{@work_activity.users_path}/#{uid}'>#{at_uid}</a>"
48✔
180
      end
181

182
      # allow ``` for code blocks (Kramdown only supports ~~~)
183
      text = text.gsub("```", "~~~")
54✔
184
      Kramdown::Document.new(text).to_html
54✔
185
    end
186
    # rubocop:enable Metrics/MethodLength
187
  end
188

189
  class OtherLogEvent < BaseMessage
1✔
190
  end
191

192
  class Message < BaseMessage
1✔
193
    # Override the default:
194
    def title_html
1✔
195
      "<span class='activity-history-title'>#{created_by_user_html} at #{created_at_html}</span>"
30✔
196
    end
197
  end
198
end
199
# 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