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

pulibrary / pdc_describe / c262aacc-a10e-4c38-ae9d-017519865590

pending completion
c262aacc-a10e-4c38-ae9d-017519865590

Pull #851

circleci

mccalluc
indent + post_curation
Pull Request #851: Include files in Work JSON

7 of 7 new or added lines in 2 files covered. (100.0%)

1761 of 1774 relevant lines covered (99.27%)

146.96 hits per line

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

98.77
/app/models/work.rb
1
# frozen_string_literal: true
2

3
# rubocop:disable Metrics/ClassLength
4
class Work < ApplicationRecord
1✔
5
  MAX_UPLOADS = 20
1✔
6

7
  # Errors for cases where there is no valid Collection
8
  class InvalidCollectionError < ::ArgumentError; end
1✔
9

10
  has_many :work_activity, -> { order(updated_at: :desc) }, dependent: :destroy
164✔
11
  has_many :user_work, -> { order(updated_at: :desc) }, dependent: :destroy
8✔
12
  has_many_attached :pre_curation_uploads, service: :amazon_pre_curation
1✔
13

14
  belongs_to :collection
1✔
15
  belongs_to :curator, class_name: "User", foreign_key: "curator_user_id", optional: true
1✔
16

17
  attribute :work_type, :string, default: "DATASET"
1✔
18
  attribute :profile, :string, default: "DATACITE"
1✔
19

20
  attr_accessor :user_entered_doi
1✔
21

22
  alias state_history user_work
1✔
23

24
  include AASM
1✔
25

26
  aasm column: :state do
1✔
27
    state :none, inital: true
1✔
28
    state :draft, :awaiting_approval, :approved, :withdrawn, :tombstone
1✔
29

30
    event :draft, after: :draft_doi do
1✔
31
      transitions from: :none, to: :draft, guard: :valid_to_draft
1✔
32
    end
33

34
    event :complete_submission do
1✔
35
      transitions from: :draft, to: :awaiting_approval, guard: :valid_to_submit
1✔
36
    end
37

38
    event :request_changes do
1✔
39
      transitions from: :awaiting_approval, to: :awaiting_approval, guard: :valid_to_submit
1✔
40
    end
41

42
    event :approve do
1✔
43
      transitions from: :awaiting_approval, to: :approved, guard: :valid_to_approve, after: :publish
1✔
44
    end
45

46
    event :withdraw do
1✔
47
      transitions from: [:draft, :awaiting_approval, :approved], to: :withdrawn
1✔
48
    end
49

50
    event :resubmit do
1✔
51
      transitions from: :withdrawn, to: :draft
1✔
52
    end
53

54
    event :remove do
1✔
55
      transitions from: :withdrawn, to: :tombstone
1✔
56
    end
57

58
    after_all_events :track_state_change
1✔
59
  end
60

61
  def state=(new_state)
1✔
62
    new_state_sym = new_state.to_sym
236✔
63
    valid_states = self.class.aasm.states.map(&:name)
236✔
64
    raise(StandardError, "Invalid state '#{new_state}'") unless valid_states.include?(new_state_sym)
236✔
65
    aasm_write_state_without_persistence(new_state_sym)
235✔
66
  end
67

68
  ##
69
  # Is this work editable by a given user?
70
  # A work is editable when:
71
  # * it is being edited by the person who made it
72
  # * it is being edited by a collection admin of the collection where is resides
73
  # * it is being edited by a super admin
74
  # @param [User]
75
  # @return [Boolean]
76
  def editable_by?(user)
1✔
77
    submitted_by?(user) || administered_by?(user)
88✔
78
  end
79

80
  def submitted_by?(user)
1✔
81
    created_by_user_id == user.id
93✔
82
  end
83

84
  def administered_by?(user)
1✔
85
    user.has_role?(:collection_admin, collection)
30✔
86
  end
87

88
  class << self
1✔
89
    def find_by_doi(doi)
1✔
90
      # This does a string compare to allow for partial matches
91
      # I'm not entirely sure why we are allowing partial matches in a find by
92
      Work.find_by!("metadata->>'doi' like ? ", "%#{doi}")
2✔
93
      # I think we should be doing the following instead, which matches exactly
94
      # Work.find_by!('metadata @> ?', "{\"doi\":\"#{doi}\"}")
95
    end
96

97
    def find_by_ark(ark)
1✔
98
      # This does a string compare to allow for partial matches
99
      # I'm not entirely sure why we are allowing partial matches in a find by
100
      Work.find_by!("metadata->>'ark' like ? ", "%#{ark}")
2✔
101
      # I think we should be doing the following instead, which matches exactly
102
      # Work.find_by!('metadata @> ?', "{\"ark\":\"#{ark}\"}")
103
    end
104

105
    delegate :resource_type_general_values, to: PDCMetadata::Resource
1✔
106

107
    # Determines whether or not a test DOI should be referenced
108
    # (this avoids requests to the DOI API endpoint for non-production deployments)
109
    # @return [Boolean]
110
    def publish_test_doi?
1✔
111
      (Rails.env.development? || Rails.env.test?) && Rails.configuration.datacite.user.blank?
41✔
112
    end
113
  end
114

115
  include Rails.application.routes.url_helpers
1✔
116

117
  before_save do |work|
1✔
118
    # Ensure that the metadata JSONB postgres field is persisted properly
119
    work.metadata = JSON.parse(work.resource.to_json)
665✔
120
    work.save_pre_curation_uploads
665✔
121
  end
122

123
  after_save do |work|
1✔
124
    if work.approved?
664✔
125
      work.attach_s3_resources if !work.pre_curation_uploads.empty? && work.pre_curation_uploads.length > work.post_curation_uploads.length
73✔
126
      work.reload
73✔
127
    end
128
  end
129

130
  validate do |work|
1✔
131
    if none?
690✔
132
      work.validate_doi
88✔
133
    elsif draft?
602✔
134
      work.valid_to_draft
379✔
135
    else
136
      work.valid_to_submit
223✔
137
    end
138
  end
139

140
  # Overload ActiveRecord.reload method
141
  # https://apidock.com/rails/ActiveRecord/Base/reload
142
  #
143
  # NOTE: Usually `after_save` is a better place to put this kind of code:
144
  #
145
  #   after_save do |work|
146
  #     work.resource = nil
147
  #   end
148
  #
149
  # but that does not work in this case because the block points to a different
150
  # memory object for `work` than the we want we want to reload.
151
  def reload(options = nil)
1✔
152
    super
169✔
153
    # Force `resource` to be reloaded
154
    @resource = nil
169✔
155
    self
169✔
156
  end
157

158
  def validate_doi
1✔
159
    return true unless user_entered_doi
88✔
160
    if /^10.\d{4,9}\/[-._;()\/:a-z0-9\-]+$/.match?(doi.downcase)
12✔
161
      response = Faraday.get("#{Rails.configuration.datacite.doi_url}#{doi}")
11✔
162
      errors.add(:base, "Invalid DOI: can not verify it's authenticity") unless response.success? || response.status == 302
11✔
163
    else
164
      errors.add(:base, "Invalid DOI: does not match format")
1✔
165
    end
166
    errors.count == 0
12✔
167
  end
168

169
  def valid_to_draft
1✔
170
    errors.add(:base, "Must provide a title") if resource.main_title.blank?
705✔
171
    validate_ark
705✔
172
    validate_creators
705✔
173
    validate_uploads
705✔
174
    errors.count == 0
705✔
175
  end
176

177
  def valid_to_submit
1✔
178
    valid_to_draft
295✔
179
    validate_metadata
295✔
180
    validate_uploads
295✔
181
    errors.count == 0
295✔
182
  end
183

184
  def valid_to_approve(user)
1✔
185
    valid_to_submit
28✔
186
    unless user.has_role? :collection_admin, collection
28✔
187
      errors.add :base, "Unauthorized to Approve"
4✔
188
    end
189
    errors.count == 0
28✔
190
  end
191

192
  def title
1✔
193
    resource.main_title
144✔
194
  end
195

196
  def uploads_attributes
1✔
197
    return [] if approved? # once approved we no longer allow the updating of uploads via the application
51✔
198
    uploads.map do |upload|
47✔
199
      {
200
        id: upload.id,
21✔
201
        key: upload.key,
202
        filename: upload.filename.to_s,
203
        created_at: upload.created_at,
204
        url: rails_blob_path(upload, disposition: "attachment")
205
      }
206
    end
207
  end
208

209
  def form_attributes
1✔
210
    {
211
      uploads: uploads_attributes
51✔
212
    }
213
  end
214

215
  def draft_doi
1✔
216
    return if resource.doi.present?
31✔
217
    resource.doi = if self.class.publish_test_doi?
20✔
218
                     Rails.logger.info "Using hard-coded test DOI during development."
1✔
219
                     "10.34770/tbd"
1✔
220
                   else
221
                     result = data_cite_connection.autogenerate_doi(prefix: Rails.configuration.datacite.prefix)
19✔
222
                     if result.success?
19✔
223
                       result.success.doi
18✔
224
                     else
225
                       raise("Error generating DOI. #{result.failure.status} / #{result.failure.reason_phrase}")
1✔
226
                     end
227
                   end
228
    save!
19✔
229
  end
230

231
  def created_by_user
1✔
232
    User.find(created_by_user_id)
298✔
233
  rescue ActiveRecord::RecordNotFound
234
    nil
1✔
235
  end
236

237
  def resource=(resource)
1✔
238
    @resource = resource
359✔
239
    # Ensure that the metadata JSONB postgres field is persisted properly
240
    self.metadata = JSON.parse(resource.to_json)
359✔
241
  end
242

243
  def resource
1✔
244
    @resource ||= PDCMetadata::Resource.new_from_jsonb(metadata)
12,545✔
245
  end
246

247
  def url
1✔
248
    return unless persisted?
3✔
249

250
    @url ||= url_for(self)
3✔
251
  end
252

253
  def files_location_upload?
1✔
254
    files_location.blank? || files_location == "file_upload"
6✔
255
  end
256

257
  def files_location_cluster?
1✔
258
    files_location == "file_cluster"
49✔
259
  end
260

261
  def files_location_other?
1✔
262
    files_location == "file_other"
49✔
263
  end
264

265
  def change_curator(curator_user_id, current_user)
1✔
266
    if curator_user_id == "no-one"
5✔
267
      clear_curator(current_user)
1✔
268
    else
269
      update_curator(curator_user_id, current_user)
4✔
270
    end
271
  end
272

273
  def clear_curator(current_user)
1✔
274
    # Update the curator on the Work
275
    self.curator_user_id = nil
2✔
276
    save!
2✔
277

278
    # ...and log the activity
279
    WorkActivity.add_system_activity(id, "Unassigned existing curator", current_user.id)
2✔
280
  end
281

282
  def update_curator(curator_user_id, current_user)
1✔
283
    # Update the curator on the Work
284
    self.curator_user_id = curator_user_id
5✔
285
    save!
5✔
286

287
    # ...and log the activity
288
    new_curator = User.find(curator_user_id)
4✔
289
    message = if curator_user_id == current_user.id
4✔
290
                "Self-assigned as curator"
1✔
291
              else
292
                "Set curator to @#{new_curator.uid}"
3✔
293
              end
294
    WorkActivity.add_system_activity(id, message, current_user.id)
4✔
295
  end
296

297
  def curator_or_current_uid(user)
1✔
298
    persisted = if curator.nil?
4✔
299
                  user
3✔
300
                else
301
                  curator
1✔
302
                end
303
    persisted.uid
4✔
304
  end
305

306
  def add_message(message, current_user_id)
1✔
307
    WorkActivity.add_system_activity(id, message, current_user_id, activity_type: WorkActivity::MESSAGE)
6✔
308
  end
309

310
  def add_provenance_note(date, note, current_user_id)
1✔
311
    WorkActivity.add_system_activity(id, note, current_user_id, activity_type: WorkActivity::PROVENANCE_NOTES, date: date)
1✔
312
  end
313

314
  def log_changes(resource_compare, current_user_id)
1✔
315
    return if resource_compare.identical?
36✔
316
    WorkActivity.add_system_activity(id, resource_compare.differences.to_json, current_user_id, activity_type: WorkActivity::CHANGES)
35✔
317
  end
318

319
  def log_file_changes(changes, current_user_id)
1✔
320
    return if changes.count == 0
65✔
321
    WorkActivity.add_system_activity(id, changes.to_json, current_user_id, activity_type: WorkActivity::FILE_CHANGES)
18✔
322
  end
323

324
  def activities
1✔
325
    WorkActivity.where(work_id: id).sort_by(&:updated_at).reverse
195✔
326
  end
327

328
  def changes
1✔
329
    activities.select(&:log_event_type?)
63✔
330
  end
331

332
  def messages
1✔
333
    activities.select(&:message_event_type?)
63✔
334
  end
335

336
  def new_notification_count_for_user(user_id)
1✔
337
    WorkActivityNotification.joins(:work_activity)
70✔
338
                            .where(user_id: user_id, read_at: nil)
339
                            .where(work_activity: { work_id: id })
340
                            .count
341
  end
342

343
  # Marks as read the notifications for the given user_id in this work.
344
  # In practice, the user_id is the id of the current user and therefore this method marks the current's user
345
  # notifications as read.
346
  def mark_new_notifications_as_read(user_id)
1✔
347
    activities.each do |activity|
62✔
348
      unread_notifications = WorkActivityNotification.where(user_id: user_id, work_activity_id: activity.id, read_at: nil)
68✔
349
      unread_notifications.each do |notification|
68✔
350
        notification.read_at = Time.now.utc
31✔
351
        notification.save
31✔
352
      end
353
    end
354
  end
355

356
  def current_transition
1✔
357
    aasm.current_event.to_s.humanize.delete("!")
18✔
358
  end
359

360
  def uploads
1✔
361
    return post_curation_uploads if approved?
193✔
362

363
    pre_curation_uploads
176✔
364
  end
365

366
  # This ensures that new ActiveStorage::Attachment objects can be modified before they are persisted
367
  def save_pre_curation_uploads
1✔
368
    return if pre_curation_uploads.empty?
665✔
369

370
    new_attachments = pre_curation_uploads.reject(&:persisted?)
194✔
371
    return if new_attachments.empty?
194✔
372

373
    save_new_attachments(new_attachments: new_attachments)
163✔
374
  end
375

376
  # Accesses post-curation S3 Bucket Objects
377
  def post_curation_s3_resources
1✔
378
    return [] unless approved?
87✔
379

380
    s3_resources
78✔
381
  end
382
  alias post_curation_uploads post_curation_s3_resources
1✔
383

384
  def s3_client
1✔
385
    s3_query_service.client
70✔
386
  end
387

388
  delegate :bucket_name, to: :s3_query_service
1✔
389

390
  # Transmit a HEAD request for an S3 Object in the post-curation Bucket
391
  # @param key [String]
392
  # @param bucket_name [String]
393
  # @return [Aws::S3::Types::HeadObjectOutput]
394
  def find_post_curation_s3_object(bucket_name:, key:)
1✔
395
    s3_client.head_object({
24✔
396
                            bucket: bucket_name,
397
                            key: key
398
                          })
399
    true
24✔
400
  rescue Aws::S3::Errors::NotFound
401
    nil
×
402
  end
403

404
  # Generates the S3 Object key
405
  # @return [String]
406
  def s3_object_key
1✔
407
    "#{doi}/#{id}"
314✔
408
  end
409

410
  # Transmit a HEAD request for the S3 Bucket directory for this Work
411
  # @param bucket_name location to be checked to be found
412
  # @return [Aws::S3::Types::HeadObjectOutput]
413
  def find_post_curation_s3_dir(bucket_name:)
1✔
414
    s3_client.head_object({
23✔
415
                            bucket: bucket_name,
416
                            key: s3_object_key
417
                          })
418
    true
×
419
  rescue Aws::S3::Errors::NotFound
420
    nil
23✔
421
  end
422

423
  # Transmit a DELETE request for the S3 directory in the pre-curation Bucket
424
  # @return [Aws::S3::Types::DeleteObjectOutput]
425
  def delete_pre_curation_s3_dir
1✔
426
    s3_client.delete_object({
23✔
427
                              bucket: bucket_name,
428
                              key: s3_object_key
429
                            })
430
  rescue Aws::S3::Errors::ServiceError => error
431
    raise(StandardError, "Failed to delete the pre-curation S3 Bucket directory #{s3_object_key}: #{error}")
×
432
  end
433

434
  # This is invoked within the scope of #after_save. Attachment objects require that the parent record be persisted (hence, #before_save is not an option).
435
  # However, a consequence of this is that #after_save is invoked whenever a new attached Blob or Attachment object is persisted.
436
  def attach_s3_resources
1✔
437
    return if approved?
89✔
438
    changes = []
54✔
439
    # This retrieves and adds S3 uploads if they do not exist
440
    pre_curation_s3_resources.each do |s3_file|
54✔
441
      if add_pre_curation_s3_object(s3_file)
16✔
442
        changes << { action: :added, filename: s3_file.filename }
13✔
443
      end
444
    end
445

446
    # Log the new files, but don't link the change to the current_user since we really don't know
447
    # who added the files directly to AWS S3.
448
    log_file_changes(changes, nil)
54✔
449
  end
450

451
  def as_json(options = nil)
1✔
452
    if options&.present?
9✔
453
      raise(StandardError, "Received options #{options}, but not supported")
×
454
      # Included in signature for compatibility with Rails.
455
    end
456
    # to_json returns a string of serialized JSON.
457
    # as_json returns the corresponding hash.
458
    files = (pre_curation_uploads + post_curation_uploads).map { |upload| { "filename": upload.filename.to_s, "created_at": upload.created_at } }
13✔
459
    {
460
      "resource" => resource.as_json,
9✔
461
      "files" => files
462
    }
463
  end
464

465
  delegate :ark, :doi, :resource_type, :resource_type=, :resource_type_general, :resource_type_general=,
1✔
466
           :to_xml, to: :resource
467

468
  protected
1✔
469

470
    # This must be protected, NOT private for ActiveRecord to work properly with this attribute.
471
    #   Protected will still keep others from setting the metatdata, but allows ActiveRecord the access it needs
472
    def metadata=(metadata)
1✔
473
      super
1,024✔
474
      @resource = PDCMetadata::Resource.new_from_jsonb(metadata)
1,024✔
475
    end
476

477
  private
1✔
478

479
    def publish(user)
1✔
480
      publish_doi(user)
23✔
481
      update_ark_information
23✔
482
      publish_precurated_files
23✔
483
      save!
23✔
484
    end
485

486
    # Update EZID (our provider of ARKs) with the new information for this work.
487
    def update_ark_information
1✔
488
      # We only want to update the ark url under certain conditions.
489
      # Set this value in config/update_ark_url.yml
490
      if Rails.configuration.update_ark_url
23✔
491
        if ark.present?
16✔
492
          Ark.update(ark, url)
3✔
493
        end
494
      end
495
    end
496

497
    # Generates the key for ActiveStorage::Attachment and Attachment::Blob objects
498
    # @param attachment [ActiveStorage::Attachment]
499
    # @return [String]
500
    def generate_attachment_key(attachment)
1✔
501
      attachment_filename = attachment.filename.to_s
112✔
502
      attachment_key = attachment.key
112✔
503

504
      # Files actually coming from S3 include the DOI and bucket as part of the file name
505
      #  Files being attached in another manner may not have it, so we should include it.
506
      #  This is really for testing only.
507
      key_base = "#{doi}/#{id}"
112✔
508
      attachment_key = [key_base, attachment_filename].join("/") unless attachment_key.include?(key_base)
112✔
509

510
      attachment_ext = File.extname(attachment_filename)
112✔
511
      attachment_query = attachment_key.gsub(attachment_ext, "")
112✔
512
      results = ActiveStorage::Blob.where("key LIKE :query", query: "%#{attachment_query}%")
112✔
513
      blobs = results.to_a
112✔
514

515
      if blobs.present?
112✔
516
        index = blobs.length + 1
28✔
517
        attachment_key = attachment_key.gsub(/\.([a-zA-Z0-9\.]+)$/, "_#{index}.\\1")
28✔
518
      end
519

520
      attachment_key
112✔
521
    end
522

523
    def track_state_change(user, state = aasm.to_state)
1✔
524
      uw = UserWork.new(user_id: user.id, work_id: id, state: state)
128✔
525
      uw.save!
128✔
526
      WorkActivity.add_system_activity(id, "marked as #{state.to_s.titleize}", user.id)
128✔
527
      WorkStateTransitionNotification.new(self, user.id).send
128✔
528
    end
529

530
    def data_cite_connection
1✔
531
      @data_cite_connection ||= Datacite::Client.new(username: Rails.configuration.datacite.user,
38✔
532
                                                     password: Rails.configuration.datacite.password,
533
                                                     host: Rails.configuration.datacite.host)
534
    end
535

536
    def validate_ark
1✔
537
      if ark.present?
705✔
538
        errors.add(:base, "Invalid ARK provided for the Work: #{ark}") unless Ark.valid?(ark)
108✔
539
      end
540
    end
541

542
    # rubocop:disable Metrics/AbcSize
543
    def validate_metadata
1✔
544
      return if metadata.blank?
295✔
545
      errors.add(:base, "Must provide a title") if resource.main_title.blank?
295✔
546
      errors.add(:base, "Must provide a description") if resource.description.blank?
295✔
547
      errors.add(:base, "Must indicate the Publisher") if resource.publisher.blank?
295✔
548
      errors.add(:base, "Must indicate the Publication Year") if resource.publication_year.blank?
295✔
549
      errors.add(:base, "Must indicate a Rights statement") if resource.rights.nil?
295✔
550
      errors.add(:base, "Must provide a Version number") if resource.version_number.blank?
295✔
551
      validate_creators
295✔
552
      validate_related_objects
295✔
553
    end
554
    # rubocop:enable Metrics/AbcSize
555

556
    def validate_creators
1✔
557
      if resource.creators.count == 0
1,000✔
558
        errors.add(:base, "Must provide at least one Creator")
1✔
559
      else
560
        resource.creators.each do |creator|
999✔
561
          if creator.orcid.present? && Orcid.invalid?(creator.orcid)
1,474✔
562
            errors.add(:base, "ORCID for creator #{creator.value} is not in format 0000-0000-0000-0000")
1✔
563
          end
564
        end
565
      end
566
    end
567

568
    def validate_related_objects
1✔
569
      return if resource.related_objects.empty?
295✔
570
      invalid = resource.related_objects.reject(&:valid?)
6✔
571
      errors.add(:base, "Related Objects are invalid: #{invalid.map(&:errors).join(', ')}") if invalid.count.positive?
6✔
572
    end
573

574
    def publish_doi(user)
1✔
575
      return Rails.logger.info("Publishing hard-coded test DOI during development.") if self.class.publish_test_doi?
21✔
576

577
      if doi.starts_with?(Rails.configuration.datacite.prefix)
21✔
578
        result = data_cite_connection.update(id: doi, attributes: doi_attributes)
19✔
579
        if result.failure?
19✔
580
          resolved_user = curator_or_current_uid(user)
3✔
581
          message = "@#{resolved_user} Error publishing DOI. #{result.failure.status} / #{result.failure.reason_phrase}"
3✔
582
          WorkActivity.add_system_activity(id, message, user.id, activity_type: "DATACITE_ERROR")
3✔
583
        end
584
      elsif ark.blank? # we can not update the url anywhere
2✔
585
        Honeybadger.notify("Publishing for a DOI we do not own and no ARK is present: #{doi}")
1✔
586
      end
587
    end
588

589
    def doi_attribute_url
1✔
590
      "https://datacommons.princeton.edu/discovery/doi/#{doi}"
19✔
591
    end
592

593
    def doi_attribute_resource
1✔
594
      PDCMetadata::Resource.new_from_jsonb(metadata)
19✔
595
    end
596

597
    def doi_attribute_xml
1✔
598
      unencoded = doi_attribute_resource.to_xml
19✔
599
      Base64.encode64(unencoded)
19✔
600
    end
601

602
    def doi_attributes
1✔
603
      {
604
        "event" => "publish",
19✔
605
        "xml" => doi_attribute_xml,
606
        "url" => doi_attribute_url
607
      }
608
    end
609

610
    def validate_uploads
1✔
611
      # The number of pre-curation uploads should be validated, as these are mutated directly
612
      if pre_curation_uploads.length > MAX_UPLOADS
1,000✔
613
        errors.add(:base, "Only #{MAX_UPLOADS} files may be uploaded by a user to a given Work. #{pre_curation_uploads.length} files were uploaded for the Work: #{ark}")
2✔
614
      end
615
    end
616

617
    # This needs to be called #before_save
618
    # This ensures that new ActiveStorage::Attachment objects are persisted with custom keys (which are generated from the file name and DOI)
619
    # @param new_attachments [Array<ActiveStorage::Attachment>]
620
    def save_new_attachments(new_attachments:)
1✔
621
      new_attachments.each do |attachment|
163✔
622
        # There are cases (race conditions?) where the ActiveStorage::Blob objects are not persisted
623
        next if attachment.frozen?
172✔
624

625
        # This ensures that the custom key for the ActiveStorage::Attachment and ActiveStorage::Blob objects are generated
626
        generated_key = generate_attachment_key(attachment)
112✔
627
        attachment.blob.key = generated_key
112✔
628
        attachment.blob.save
112✔
629

630
        attachment.save
112✔
631
      end
632
    end
633

634
    # S3QueryService object associated with this Work
635
    # @return [S3QueryService]
636
    def s3_query_service
1✔
637
      @s3_query_service = S3QueryService.new(self, !approved?)
225✔
638
    end
639

640
    # Request S3 Bucket Objects associated with this Work
641
    # @return [Array<S3File>]
642
    def s3_resources
1✔
643
      data_profile = s3_query_service.data_profile
132✔
644
      data_profile.fetch(:objects, [])
132✔
645
    end
646
    alias pre_curation_s3_resources s3_resources
1✔
647

648
    def s3_object_persisted?(s3_file)
1✔
649
      uploads_keys = uploads.map(&:key)
16✔
650
      uploads_keys.include?(s3_file.key)
16✔
651
    end
652

653
    def add_pre_curation_s3_object(s3_file)
1✔
654
      return if s3_object_persisted?(s3_file)
16✔
655

656
      persisted = s3_file.to_blob
13✔
657
      pre_curation_uploads.attach(persisted)
13✔
658
    end
659

660
    def publish_precurated_files
1✔
661
      # An error is raised if there are no files to be moved
662
      raise(StandardError, "Attempting to publish a Work without attached uploads for #{s3_object_key}") if pre_curation_uploads.empty? && post_curation_uploads.empty?
23✔
663

664
      # We need to explicitly access to post-curation services here.
665
      # Lets explicitly create it so the state of the work does not have any impact.
666
      s3_post_curation_query_service = S3QueryService.new(self, false)
23✔
667

668
      s3_dir = find_post_curation_s3_dir(bucket_name: s3_post_curation_query_service.bucket_name)
23✔
669
      raise(StandardError, "Attempting to publish a Work with an existing S3 Bucket directory for: #{s3_object_key}") unless s3_dir.nil?
23✔
670

671
      # Copy the pre-curation S3 Objects to the post-curation S3 Bucket...
672
      transferred_files = s3_post_curation_query_service.publish_files
23✔
673

674
      # ...check that the files are indeed now in the post-curation bucket...
675
      pre_curation_uploads.each do |attachment|
23✔
676
        s3_object = find_post_curation_s3_object(bucket_name: s3_post_curation_query_service.bucket_name, key: attachment.key)
24✔
677
        raise(StandardError, "Failed to validate the uploaded S3 Object #{attachment.key}") if s3_object.nil?
24✔
678
      end
679

680
      # ...and delete them from the pre-curation bucket.
681
      transferred_files.each(&:purge)
23✔
682
      delete_pre_curation_s3_dir
23✔
683
    end
684
end
685
# 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