Class: Work

Inherits:
ApplicationRecord show all
Includes:
AASM
Defined in:
app/models/work.rb

Overview

rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: InvalidGroupError

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#user_entered_doiObject

Returns the value of attribute user_entered_doi.



18
19
20
# File 'app/models/work.rb', line 18

def user_entered_doi
  @user_entered_doi
end

Class Method Details

.find_by_ark(ark) ⇒ Object



107
108
109
110
111
# File 'app/models/work.rb', line 107

def find_by_ark(ark)
  prefix = "ark:/"
  ark = "#{prefix}#{ark}" unless ark.blank? || ark.start_with?(prefix)
  Work.find_by!("metadata @> ?", JSON.dump(ark:))
end

.find_by_doi(doi) ⇒ Object



101
102
103
104
105
# File 'app/models/work.rb', line 101

def find_by_doi(doi)
  prefix = "10.34770/"
  doi = "#{prefix}#{doi}" unless doi.blank? || doi.start_with?(prefix)
  Work.find_by!("metadata @> ?", JSON.dump(doi:))
end

.presenter_classObject



500
501
502
# File 'app/models/work.rb', line 500

def self.presenter_class
  WorkPresenter
end

Instance Method Details

#activitiesObject



280
281
282
# File 'app/models/work.rb', line 280

def activities
  WorkActivity.activities_for_work(id, WorkActivity::MESSAGE_ACTIVITY_TYPES + WorkActivity::CHANGE_LOG_ACTIVITY_TYPES)
end

#add_message(message, current_user_id) ⇒ Object



262
263
264
# File 'app/models/work.rb', line 262

def add_message(message, current_user_id)
  WorkActivity.add_work_activity(id, message, current_user_id, activity_type: WorkActivity::MESSAGE)
end

#add_provenance_note(date, note, current_user_id, change_label = "") ⇒ Object



266
267
268
# File 'app/models/work.rb', line 266

def add_provenance_note(date, note, current_user_id, change_label = "")
  WorkActivity.add_work_activity(id, { note:, change_label: }.to_json, current_user_id, activity_type: WorkActivity::PROVENANCE_NOTES, created_at: date)
end

#administered_by?(user) ⇒ Boolean

Returns:

  • (Boolean)


96
97
98
# File 'app/models/work.rb', line 96

def administered_by?(user)
  user.has_role?(:group_admin, group)
end

#artifact_uploadsArray<S3File>

Retrieve the S3 file uploads which are research artifacts proper (not README or other files providing metadata/documentation)

Returns:



330
331
332
# File 'app/models/work.rb', line 330

def artifact_uploads
  uploads.reject { |s3_file| s3_file.filename.include?("README") }
end

#as_json(*args) ⇒ String

Generates the JSON serialized expression of the Work

Parameters:

  • args (Array<Hash>)

Options Hash (*args):

  • :force_post_curation (Boolean)

    Force the request of AWS S3 Resources, clearing the in-memory cache

Returns:

  • (String)


437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'app/models/work.rb', line 437

def as_json(*args)
  files = files_as_json(*args)

  # to_json returns a string of serialized JSON.
  # as_json returns the corresponding hash.
  {
    "resource" => resource.as_json,
    "files" => files,
    "group" => group.as_json.except("id"),
    "embargo_date" => embargo_date_as_json,
    "created_at" => format_date_for_solr(created_at),
    "updated_at" => format_date_for_solr(updated_at)
  }
end

#change_curator(curator_user_id, current_user) ⇒ Object



221
222
223
224
225
226
227
# File 'app/models/work.rb', line 221

def change_curator(curator_user_id, current_user)
  if curator_user_id == "no-one"
    clear_curator(current_user)
  else
    update_curator(curator_user_id, current_user)
  end
end

#changesObject



508
509
510
# File 'app/models/work.rb', line 508

def changes
  @changes ||= []
end

#clear_curator(current_user) ⇒ Object



229
230
231
232
233
234
235
236
# File 'app/models/work.rb', line 229

def clear_curator(current_user)
  # Update the curator on the Work
  self.curator_user_id = nil
  save!

  # ...and log the activity
  WorkActivity.add_work_activity(id, "Unassigned existing curator", current_user.id, activity_type: WorkActivity::SYSTEM)
end

#created_by_userObject



187
188
189
190
191
# File 'app/models/work.rb', line 187

def created_by_user
  User.find(created_by_user_id)
rescue ActiveRecord::RecordNotFound
  nil
end

#current_transitionObject



310
311
312
# File 'app/models/work.rb', line 310

def current_transition
  aasm.current_event.to_s.humanize.delete("!")
end

#doi_urlString

Return the DOI formatted as a URL, so it can be used as a link on display pages

Returns:

  • (String)

    A url formatted version of the DOI



182
183
184
185
# File 'app/models/work.rb', line 182

def doi_url
  return "https://doi.org/#{doi}" unless doi.starts_with?("https://doi.org")
  doi
end

#draft_doiObject



174
175
176
177
178
# File 'app/models/work.rb', line 174

def draft_doi
  return if resource.doi.present?
  resource.doi = datacite_service.draft_doi
  save!
end

#editable_by?(user) ⇒ Boolean

Is this work editable by a given user? A work is editable when:

  • it is being edited by the person who made it

  • it is being edited by a group admin of the group where is resides

  • it is being edited by a super admin

Parameters:

Returns:

  • (Boolean)


80
81
82
# File 'app/models/work.rb', line 80

def editable_by?(user)
  (user) || administered_by?(user)
end

#editable_in_current_state?(user) ⇒ Boolean

Returns:

  • (Boolean)


84
85
86
87
88
89
90
# File 'app/models/work.rb', line 84

def editable_in_current_state?(user)
  # anyone with edit privleges can edit a work while it is in draft
  return editable_by?(user) if draft?

  # Only admisitrators can edit a work in other states
  administered_by?(user)
end

#embargoed?Boolean

Determine whether or not the Work is under active embargo

Returns:

  • (Boolean)


529
530
531
532
533
534
# File 'app/models/work.rb', line 529

def embargoed?
  return false if embargo_date.blank?

  current_date = Time.zone.now
  embargo_date >= current_date
end

#file_listObject

Returns the list of files for the work with some basic information about each of them. This method is much faster than ‘uploads` because it does not return the actual S3File objects to the client, instead it returns just a few selected data elements. rubocop:disable Metrics/MethodLength



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'app/models/work.rb', line 338

def file_list
  start = Time.zone.now
  s3_files = approved? ? post_curation_uploads : pre_curation_uploads
  files_info = s3_files.map do |s3_file|
    {
      "safe_id": s3_file.safe_id,
      "filename": s3_file.filename,
      "filename_display": s3_file.filename_display,
      "last_modified": s3_file.last_modified,
      "last_modified_display": s3_file.last_modified_display,
      "size": s3_file.size,
      "display_size": s3_file.display_size,
      "url": s3_file.url,
      "is_folder": s3_file.is_folder
    }
  end
  log_performance(start, "file_list called for #{id}")
  files_info
end

#files_location_cluster?Boolean

Returns:

  • (Boolean)


213
214
215
# File 'app/models/work.rb', line 213

def files_location_cluster?
  files_location == "file_cluster"
end

#files_location_other?Boolean

Returns:

  • (Boolean)


217
218
219
# File 'app/models/work.rb', line 217

def files_location_other?
  files_location == "file_other"
end

#files_location_upload?Boolean

Returns:

  • (Boolean)


209
210
211
# File 'app/models/work.rb', line 209

def files_location_upload?
  files_location.blank? || files_location == "file_upload"
end

#find_post_curation_s3_dir(bucket_name:) ⇒ Aws::S3::Types::HeadObjectOutput

Transmit a HEAD request for the S3 Bucket directory for this Work

Parameters:

  • bucket_name

    location to be checked to be found

Returns:

  • (Aws::S3::Types::HeadObjectOutput)


420
421
422
423
424
425
426
427
428
429
430
# File 'app/models/work.rb', line 420

def find_post_curation_s3_dir(bucket_name:)
  # TODO: Directories really do not exists in S3
  #      if we really need this check then we need to do something else to check the bucket
  s3_client.head_object({
                          bucket: bucket_name,
                          key: s3_object_key
                        })
  true
rescue Aws::S3::Errors::NotFound
  nil
end

#form_attributesObject



168
169
170
171
172
# File 'app/models/work.rb', line 168

def form_attributes
  {
    uploads: uploads_attributes
  }
end

#format_date_for_solr(date) ⇒ String

Format the date for Apache Solr

Parameters:

  • date (ActiveSupport::TimeWithZone)

Returns:

  • (String)


455
456
457
# File 'app/models/work.rb', line 455

def format_date_for_solr(date)
  date.strftime("%Y-%m-%dT%H:%M:%SZ")
end

#has_rights?(rights_id) ⇒ Boolean

rubocop:disable Naming/PredicateName

Returns:

  • (Boolean)


517
518
519
# File 'app/models/work.rb', line 517

def has_rights?(rights_id)
  resource.rights_many.index { |rights| rights.identifier == rights_id } != nil
end

#log_changes(resource_compare, current_user_id) ⇒ Object



270
271
272
273
# File 'app/models/work.rb', line 270

def log_changes(resource_compare, current_user_id)
  return if resource_compare.identical?
  WorkActivity.add_work_activity(id, resource_compare.differences.to_json, current_user_id, activity_type: WorkActivity::CHANGES)
end

#log_file_changes(current_user_id) ⇒ Object



275
276
277
278
# File 'app/models/work.rb', line 275

def log_file_changes(current_user_id)
  return if changes.count == 0
  WorkActivity.add_work_activity(id, changes.to_json, current_user_id, activity_type: WorkActivity::FILE_CHANGES)
end

#mark_new_notifications_as_read(user_id) ⇒ Object

Marks as read the notifications for the given user_id in this work. In practice, the user_id is the id of the current user and therefore this method marks the current’s user notifications as read.



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'app/models/work.rb', line 294

def mark_new_notifications_as_read(user_id)
  # Notice that we fetch and update the information in batches
  # so that we don't issue individual SQL SELECT + SQL UPDATE
  # for each notification.
  #
  # Rails batching information:
  #   https://guides.rubyonrails.org/active_record_querying.html
  #   https://api.rubyonrails.org/classes/ActiveRecord/Batches.html

  # Disable this validation since we want to force a SQL UPDATE.
  # rubocop:disable Rails/SkipsModelValidations
  now_utc = Time.now.utc
  WorkActivityNotification.joins(:work_activity).where("user_id=? and work_id=?", user_id, id).in_batches(of: 1000).update_all(read_at: now_utc)
  # rubocop:enable Rails/SkipsModelValidations
end

#new_notification_count_for_user(user_id) ⇒ Object



284
285
286
287
288
289
# File 'app/models/work.rb', line 284

def new_notification_count_for_user(user_id)
  WorkActivityNotification.joins(:work_activity)
                          .where(user_id:, read_at: nil)
                          .where(work_activity: { work_id: id })
                          .count
end

#past_snapshotsObject



473
474
475
# File 'app/models/work.rb', line 473

def past_snapshots
  UploadSnapshot.where(work: self)
end

#pdc_discovery_urlObject

This is the solr id / work show page in PDC Discovery



523
524
525
# File 'app/models/work.rb', line 523

def pdc_discovery_url
  "https://datacommons.princeton.edu/discovery/catalog/doi-#{doi.tr('/', '-').tr('.', '-')}"
end

#post_curation_s3_resourcesObject

Accesses post-curation S3 Bucket Objects



380
381
382
383
384
385
386
# File 'app/models/work.rb', line 380

def post_curation_s3_resources
  if approved?
    s3_resources
  else
    []
  end
end

#post_curation_uploads(force_post_curation: false) ⇒ Object

Returns the files in post-curation for the work



389
390
391
392
393
394
395
396
397
398
# File 'app/models/work.rb', line 389

def post_curation_uploads(force_post_curation: false)
  if force_post_curation
    # Always use the post-curation data regardless of the work's status
    post_curation_s3_query_service = S3QueryService.new(self, "postcuration")
    post_curation_s3_query_service.data_profile.fetch(:objects, [])
  else
    # Return the list based of files honoring the work status
    post_curation_s3_resources
  end
end

#pre_curation_uploadsObject

Fetches the data from S3 directly bypassing ActiveStorage



375
376
377
# File 'app/models/work.rb', line 375

def pre_curation_uploads
  s3_query_service.client_s3_files.sort_by(&:filename)
end

#pre_curation_uploads_countObject



459
460
461
# File 'app/models/work.rb', line 459

def pre_curation_uploads_count
  s3_query_service.file_count
end

#presenterObject



504
505
506
# File 'app/models/work.rb', line 504

def presenter
  self.class.presenter_class.new(work: self)
end

#readme_uploadsArray<S3File>

Retrieve the S3 file uploads named “README”

Returns:



324
325
326
# File 'app/models/work.rb', line 324

def readme_uploads
  uploads.select { |s3_file| s3_file.filename.include?("README") }
end

#reload(options = nil) ⇒ Object

Overload ActiveRecord.reload method apidock.com/rails/ActiveRecord/Base/reload

NOTE: Usually ‘after_save` is a better place to put this kind of code:

after_save do |work|
  work.resource = nil
end

but that does not work in this case because the block points to a different memory object for ‘work` than the we want we want to reload.



144
145
146
147
148
149
# File 'app/models/work.rb', line 144

def reload(options = nil)
  super
  # Force `resource` to be reloaded
  @resource = nil
  self
end

#reload_snapshots(user_id: nil) ⇒ UploadSnapshot

Build or find persisted UploadSnapshot models for this Work

Parameters:

  • user_id (integer) (defaults to: nil)

    optional user to assign the snapshot to

Returns:



480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# File 'app/models/work.rb', line 480

def reload_snapshots(user_id: nil)
  work_changes = []
  s3_files = pre_curation_uploads
  s3_filenames = s3_files.map(&:filename)

  upload_snapshot = latest_snapshot

  upload_snapshot.snapshot_deletions(work_changes, s3_filenames)

  upload_snapshot.snapshot_modifications(work_changes, s3_files)

  # Create WorkActivity models with the set of changes
  unless work_changes.empty?
    new_snapshot = UploadSnapshot.new(work: self, url: s3_query_service.prefix)
    new_snapshot.store_files(s3_files)
    new_snapshot.save!
    WorkActivity.add_work_activity(id, work_changes.to_json, user_id, activity_type: WorkActivity::FILE_CHANGES)
  end
end

#resourceObject



199
200
201
# File 'app/models/work.rb', line 199

def resource
  @resource ||= PDCMetadata::Resource.new_from_jsonb()
end

#resource=(resource) ⇒ Object



193
194
195
196
197
# File 'app/models/work.rb', line 193

def resource=(resource)
  @resource = resource
  # Ensure that the metadata JSONB postgres field is persisted properly
  self. = JSON.parse(resource.to_json)
end

#s3_clientObject



404
405
406
# File 'app/models/work.rb', line 404

def s3_client
  s3_query_service.client
end

#s3_filesObject



400
401
402
# File 'app/models/work.rb', line 400

def s3_files
  pre_curation_uploads
end

#s3_object_keyString

Generates the S3 Object key

Returns:

  • (String)


413
414
415
# File 'app/models/work.rb', line 413

def s3_object_key
  "#{doi}/#{id}"
end

#s3_query_serviceS3QueryService

S3QueryService object associated with this Work

Returns:



468
469
470
471
# File 'app/models/work.rb', line 468

def s3_query_service
  mode = approved? ? "postcuration" : "precuration"
  @s3_query_service ||= S3QueryService.new(self, mode)
end

#state=(new_state) ⇒ Object

Raises:

  • (StandardError)


65
66
67
68
69
70
# File 'app/models/work.rb', line 65

def state=(new_state)
  new_state_sym = new_state.to_sym
  valid_states = self.class.aasm.states.map(&:name)
  raise(StandardError, "Invalid state '#{new_state}'") unless valid_states.include?(new_state_sym)
  aasm_write_state_without_persistence(new_state_sym)
end

#submitted_by?(user) ⇒ Boolean

Returns:

  • (Boolean)


92
93
94
# File 'app/models/work.rb', line 92

def (user)
  created_by_user_id == user.id
end

#titleObject



151
152
153
# File 'app/models/work.rb', line 151

def title
  resource.main_title
end

#total_file_sizeObject

rubocop:enable Metrics/MethodLength



359
360
361
362
363
364
365
# File 'app/models/work.rb', line 359

def total_file_size
  total_size = 0
  file_list.each do |file|
    total_size += file[:size]
  end
  total_size
end

#total_file_size_from_list(files) ⇒ Object

Calculates the total file size from a given list of files This is so that we don’t fetch the list twice from AWS since it can be expensive when there are thousands of files on the work.



370
371
372
# File 'app/models/work.rb', line 370

def total_file_size_from_list(files)
  files.sum { |file| file[:size] }
end

#track_change(action, filename) ⇒ Object



512
513
514
# File 'app/models/work.rb', line 512

def track_change(action, filename)
  changes << { action:, filename: }
end

#update_curator(curator_user_id, current_user) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'app/models/work.rb', line 238

def update_curator(curator_user_id, current_user)
  # Update the curator on the Work
  self.curator_user_id = curator_user_id
  save!

  # ...and log the activity
  new_curator = User.find(curator_user_id)

  work_url = "[#{title}](#{Rails.application.routes.url_helpers.work_url(self)})"

  # Troubleshooting https://github.com/pulibrary/pdc_describe/issues/1783
  if work_url.include?("/describe/describe/")
    Rails.logger.error("URL #{work_url} included /describe/describe/ and was fixed. See https://github.com/pulibrary/pdc_describe/issues/1783")
    work_url = work_url.gsub("/describe/describe/", "/describe/")
  end

  message = if curator_user_id.to_i == current_user.id
              "Self-assigned @#{current_user.uid} as curator for work #{work_url}"
            else
              "Set curator to @#{new_curator.uid} for work #{work_url}"
            end
  WorkActivity.add_work_activity(id, message, current_user.id, activity_type: WorkActivity::SYSTEM)
end

#upload_countObject



536
537
538
# File 'app/models/work.rb', line 536

def upload_count
  @upload_count ||= s3_query_service.count_objects
end

#uploadsArray<S3File>

Retrieve the S3 file uploads associated with the Work

Returns:



316
317
318
319
320
# File 'app/models/work.rb', line 316

def uploads
  return post_curation_uploads if approved?

  pre_curation_uploads
end

#uploads_attributesObject



155
156
157
158
159
160
161
162
163
164
165
166
# File 'app/models/work.rb', line 155

def uploads_attributes
  return [] if approved? # once approved we no longer allow the updating of uploads via the application
  uploads.map do |upload|
    {
      id: upload.id,
      key: upload.key,
      filename: upload.filename.to_s,
      created_at: upload.created_at,
      url: upload.url
    }
  end
end

#urlObject



203
204
205
206
207
# File 'app/models/work.rb', line 203

def url
  return unless persisted?

  @url ||= url_for(self)
end