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

pulibrary / pdc_describe / 4a26368f-294e-46cb-adec-4720abaf1502

pending completion
4a26368f-294e-46cb-adec-4720abaf1502

Pull #1060

circleci

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

142 of 142 new or added lines in 6 files covered. (100.0%)

2006 of 2049 relevant lines covered (97.9%)

201.53 hits per line

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

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

3
# rubocop:disable Metrics/ClassLength
4
class Work < ApplicationRecord
1✔
5
  # Errors for cases where there is no valid Collection
6
  class InvalidCollectionError < ::ArgumentError; end
1✔
7

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

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

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

19
  attr_accessor :user_entered_doi
1✔
20

21
  alias state_history user_work
1✔
22

23
  include AASM
1✔
24

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

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

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

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

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

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

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

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

57
    after_all_events :track_state_change
1✔
58
  end
59

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

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

79
  def editable_in_current_state?(user)
1✔
80
    # anyone with edit privleges can edit a work while it is in draft or awaiting approval
81
    return editable_by?(user) if draft? || awaiting_approval?
202✔
82

83
    # Only admisitrators can edit a work in other states
84
    administered_by?(user)
35✔
85
  end
86

87
  def submitted_by?(user)
1✔
88
    created_by_user_id == user.id
264✔
89
  end
90

91
  def administered_by?(user)
1✔
92
    user.has_role?(:collection_admin, collection)
88✔
93
  end
94

95
  class << self
1✔
96
    def find_by_doi(doi)
1✔
97
      prefix = "10.34770/"
3✔
98
      doi = "#{prefix}#{doi}" unless doi.start_with?(prefix)
3✔
99
      Work.find_by!("metadata @> ?", JSON.dump(doi: doi))
3✔
100
    end
101

102
    def find_by_ark(ark)
1✔
103
      prefix = "ark:/"
3✔
104
      ark = "#{prefix}#{ark}" unless ark.start_with?(prefix)
3✔
105
      Work.find_by!("metadata @> ?", JSON.dump(ark: ark))
3✔
106
    end
107

108
    delegate :resource_type_general_values, to: PDCMetadata::Resource
1✔
109

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

118
  include Rails.application.routes.url_helpers
1✔
119

120
  after_find do
1✔
121
    reload_snapshots
1,336✔
122
  end
123

124
  before_save do |work|
1✔
125
    # Ensure that the metadata JSONB postgres field is persisted properly
126
    work.metadata = JSON.parse(work.resource.to_json)
738✔
127
    work.save_pre_curation_uploads
738✔
128
  end
129

130
  after_save do |work|
1✔
131
    if work.approved?
737✔
132
      work.reload
93✔
133
    end
134
  end
135

136
  validate do |work|
1✔
137
    if none?
764✔
138
      work.validate_doi
105✔
139
    elsif draft?
659✔
140
      work.valid_to_draft
424✔
141
    else
142
      work.valid_to_submit
235✔
143
    end
144
  end
145

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

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

175
  def valid_to_draft
1✔
176
    errors.add(:base, "Must provide a title") if resource.main_title.blank?
773✔
177
    validate_ark
773✔
178
    validate_creators
773✔
179
    errors.count == 0
773✔
180
  end
181

182
  def valid_to_submit
1✔
183
    valid_to_draft
314✔
184
    validate_metadata
314✔
185
    errors.count == 0
314✔
186
  end
187

188
  def valid_to_approve(user)
1✔
189
    valid_to_submit
35✔
190
    unless user.has_role? :collection_admin, collection
35✔
191
      errors.add :base, "Unauthorized to Approve"
4✔
192
    end
193
    errors.count == 0
35✔
194
  end
195

196
  def title
1✔
197
    resource.main_title
319✔
198
  end
199

200
  def uploads_attributes
1✔
201
    return [] if approved? # once approved we no longer allow the updating of uploads via the application
56✔
202
    uploads.map do |upload|
52✔
203
      {
204
        id: upload.id,
19✔
205
        key: upload.key,
206
        filename: upload.filename.to_s,
207
        created_at: upload.created_at,
208
        url: upload.url
209
      }
210
    end
211
  end
212

213
  def form_attributes
1✔
214
    {
215
      uploads: uploads_attributes
56✔
216
    }
217
  end
218

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

235
  def created_by_user
1✔
236
    User.find(created_by_user_id)
350✔
237
  rescue ActiveRecord::RecordNotFound
238
    nil
1✔
239
  end
240

241
  def resource=(resource)
1✔
242
    @resource = resource
492✔
243
    # Ensure that the metadata JSONB postgres field is persisted properly
244
    self.metadata = JSON.parse(resource.to_json)
492✔
245
  end
246

247
  def resource
1✔
248
    @resource ||= PDCMetadata::Resource.new_from_jsonb(metadata)
14,926✔
249
  end
250

251
  def url
1✔
252
    return unless persisted?
3✔
253

254
    @url ||= url_for(self)
3✔
255
  end
256

257
  def files_location_upload?
1✔
258
    files_location.blank? || files_location == "file_upload"
6✔
259
  end
260

261
  def files_location_cluster?
1✔
262
    files_location == "file_cluster"
63✔
263
  end
264

265
  def files_location_other?
1✔
266
    files_location == "file_other"
63✔
267
  end
268

269
  def change_curator(curator_user_id, current_user)
1✔
270
    if curator_user_id == "no-one"
5✔
271
      clear_curator(current_user)
1✔
272
    else
273
      update_curator(curator_user_id, current_user)
4✔
274
    end
275
  end
276

277
  def clear_curator(current_user)
1✔
278
    # Update the curator on the Work
279
    self.curator_user_id = nil
2✔
280
    save!
2✔
281

282
    # ...and log the activity
283
    WorkActivity.add_work_activity(id, "Unassigned existing curator", current_user.id, activity_type: WorkActivity::SYSTEM)
2✔
284
  end
285

286
  def update_curator(curator_user_id, current_user)
1✔
287
    # Update the curator on the Work
288
    self.curator_user_id = curator_user_id
5✔
289
    save!
5✔
290

291
    # ...and log the activity
292
    new_curator = User.find(curator_user_id)
4✔
293
    message = if curator_user_id == current_user.id
4✔
294
                "Self-assigned as curator"
1✔
295
              else
296
                "Set curator to @#{new_curator.uid}"
3✔
297
              end
298
    WorkActivity.add_work_activity(id, message, current_user.id, activity_type: WorkActivity::SYSTEM)
4✔
299
  end
300

301
  def curator_or_current_uid(user)
1✔
302
    persisted = if curator.nil?
12✔
303
                  user
11✔
304
                else
305
                  curator
1✔
306
                end
307
    persisted.uid
12✔
308
  end
309

310
  def add_message(message, current_user_id)
1✔
311
    WorkActivity.add_work_activity(id, message, current_user_id, activity_type: WorkActivity::MESSAGE)
11✔
312
  end
313

314
  def add_provenance_note(date, note, current_user_id)
1✔
315
    WorkActivity.add_work_activity(id, note, current_user_id, activity_type: WorkActivity::PROVENANCE_NOTES, created_at: date)
1✔
316
  end
317

318
  def log_changes(resource_compare, current_user_id)
1✔
319
    return if resource_compare.identical?
42✔
320
    WorkActivity.add_work_activity(id, resource_compare.differences.to_json, current_user_id, activity_type: WorkActivity::CHANGES)
40✔
321
  end
322

323
  def log_file_changes(changes, current_user_id)
1✔
324
    return if changes.count == 0
13✔
325
    WorkActivity.add_work_activity(id, changes.to_json, current_user_id, activity_type: WorkActivity::FILE_CHANGES)
13✔
326
  end
327

328
  def activities
1✔
329
    WorkActivity.activities_for_work(id, WorkActivity::MESSAGE_ACTIVITY_TYPES + WorkActivity::CHANGE_LOG_ACTIVITY_TYPES)
81✔
330
  end
331

332
  def new_notification_count_for_user(user_id)
1✔
333
    WorkActivityNotification.joins(:work_activity)
82✔
334
                            .where(user_id: user_id, read_at: nil)
335
                            .where(work_activity: { work_id: id })
336
                            .count
337
  end
338

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

352
  def current_transition
1✔
353
    aasm.current_event.to_s.humanize.delete("!")
16✔
354
  end
355

356
  def uploads
1✔
357
    return post_curation_uploads if approved?
595✔
358

359
    pre_curation_uploads_fast
526✔
360
  end
361

362
  def pre_curation_s3_files
1✔
363
    loaded = pre_curation_uploads.sort_by(&:filename)
4,004✔
364

365
    loaded.map do |attachment|
4,004✔
366
      S3File.new(
640✔
367
        work: self,
368
        filename: attachment.key,
369
        last_modified: attachment.created_at,
370
        size: attachment.byte_size,
371
        checksum: attachment.checksum
372
      )
373
    end
374
  end
375

376
  # Fetches the data from S3 directly bypassing ActiveStorage
377
  def pre_curation_uploads_fast
1✔
378
    return local_pre_curation_s3_files if Rails.env.development?
651✔
379

380
    s3_query_service.client_s3_files.sort_by(&:filename)
651✔
381
  end
382

383
  # This ensures that new ActiveStorage::Attachment objects can be modified before they are persisted
384
  def save_pre_curation_uploads
1✔
385
    return if pre_curation_uploads.empty?
738✔
386

387
    new_attachments = pre_curation_uploads.reject(&:persisted?)
42✔
388
    return if new_attachments.empty?
42✔
389

390
    save_new_attachments(new_attachments: new_attachments)
30✔
391
  end
392

393
  # Accesses post-curation S3 Bucket Objects
394
  def post_curation_s3_resources
1✔
395
    return [] unless approved?
160✔
396

397
    s3_resources
103✔
398
  end
399
  alias post_curation_uploads post_curation_s3_resources
1✔
400

401
  def s3_files
1✔
402
    return s3_resources if approved?
5,344✔
403

404
    pre_curation_s3_files
4,004✔
405
  end
406

407
  def s3_client
1✔
408
    s3_query_service.client
22✔
409
  end
410

411
  delegate :bucket_name, to: :s3_query_service
1✔
412

413
  # Generates the S3 Object key
414
  # @return [String]
415
  def s3_object_key
1✔
416
    "#{doi}/#{id}"
51✔
417
  end
418

419
  # Transmit a HEAD request for the S3 Bucket directory for this Work
420
  # @param bucket_name location to be checked to be found
421
  # @return [Aws::S3::Types::HeadObjectOutput]
422
  def find_post_curation_s3_dir(bucket_name:)
1✔
423
    # TODO: Directories really do not exists in S3
424
    #      if we really need this check then we need to do something else to check the bucket
425
    s3_client.head_object({
22✔
426
                            bucket: bucket_name,
427
                            key: s3_object_key
428
                          })
429
    true
×
430
  rescue Aws::S3::Errors::NotFound
431
    nil
22✔
432
  end
433

434
  def as_json(*)
1✔
435
    # Pre-curation files are not accessible externally,
436
    # so we are not interested in listing them in JSON.
437
    # (The items in pre_curation_uploads also have different properties.)
438
    files = post_curation_uploads.map do |upload|
79✔
439
      {
440
        "filename": upload.filename,
42✔
441
        "size": upload.size,
442
        "url": upload.globus_url
443
      }
444
    end
445

446
    # to_json returns a string of serialized JSON.
447
    # as_json returns the corresponding hash.
448
    {
449
      "resource" => resource.as_json,
79✔
450
      "files" => files,
451
      "collection" => collection.as_json.except("id")
452
    }
453
  end
454

455
  def pre_curation_uploads_count
1✔
456
    s3_query_service.file_count
2✔
457
  end
458

459
  delegate :ark, :doi, :resource_type, :resource_type=, :resource_type_general, :resource_type_general=,
1✔
460
           :to_xml, to: :resource
461

462
  # S3QueryService object associated with this Work
463
  # @return [S3QueryService]
464
  def s3_query_service
1✔
465
    @s3_query_service ||= S3QueryService.new(self, !approved?)
2,206✔
466
  end
467

468
  def reload_snapshots
1✔
469
    results = s3_files.map do |s3_file|
1,336✔
470
      UploadSnapshot.find_or_initialize_by(url: s3_file.url, filename: s3_file.filename, work: self)
474✔
471
    end
472

473
    s3_resource_urls = s3_files.map(&:url)
1,336✔
474
    s3_resource_filenames = s3_files.map(&:filename)
1,336✔
475

476
    # remove the snapshots for s3 file resources which have been deleted
477
    persisted = results.reject do |snapshot|
1,336✔
478
      removed = !s3_resource_urls.include?(snapshot.url) && !s3_resource_filenames.include?(snapshot.filename)
474✔
479

480
      if removed
474✔
481
        snapshot.destroy
×
482
        changes = {
×
483
          action: "removed"
484
        }
485
        WorkActivity.add_work_activity(id, changes.to_json, nil, activity_type: WorkActivity::FILE_CHANGES)
×
486
      end
487

488
      removed
474✔
489
    end
490

491
    s3_files.map do |s3_file|
1,336✔
492
      snapshot = persisted.find { |s| s.url == s3_file.url && s.filename == s3_file.filename }
1,111✔
493

494
      if snapshot.checksum != s3_file.checksum
474✔
495
        # cases where the s3 resources are replaced
496
        snapshot.checksum = s3_file.checksum
84✔
497
        changes = if !snapshot.persisted?
84✔
498
                    {
84✔
499
                      action: "added"
500
                    }
501
                  else
502
                    {
×
503
                      action: "replaced"
504
                    }
505
                  end
506
        snapshot.save
84✔
507

508
        WorkActivity.add_work_activity(id, changes.to_json, nil, activity_type: WorkActivity::FILE_CHANGES)
84✔
509
      end
510

511
      snapshot.reload
474✔
512
    end
513
  end
514

515
  protected
1✔
516

517
    # This must be protected, NOT private for ActiveRecord to work properly with this attribute.
518
    #   Protected will still keep others from setting the metatdata, but allows ActiveRecord the access it needs
519
    def metadata=(metadata)
1✔
520
      super
1,230✔
521
      @resource = PDCMetadata::Resource.new_from_jsonb(metadata)
1,230✔
522
    end
523

524
  private
1✔
525

526
    def publish(user)
1✔
527
      publish_doi(user)
30✔
528
      update_ark_information
30✔
529
      publish_precurated_files
30✔
530
      save!
30✔
531
    end
532

533
    # Update EZID (our provider of ARKs) with the new information for this work.
534
    def update_ark_information
1✔
535
      # We only want to update the ark url under certain conditions.
536
      # Set this value in config/update_ark_url.yml
537
      if Rails.configuration.update_ark_url
30✔
538
        if ark.present?
5✔
539
          Ark.update(ark, url)
3✔
540
        end
541
      end
542
    end
543

544
    # Generates the key for ActiveStorage::Attachment and Attachment::Blob objects
545
    # @param attachment [ActiveStorage::Attachment]
546
    # @return [String]
547
    def generate_attachment_key(attachment)
1✔
548
      attachment_filename = attachment.filename.to_s
26✔
549
      attachment_key = attachment.key
26✔
550

551
      # Files actually coming from S3 include the DOI and bucket as part of the file name
552
      #  Files being attached in another manner may not have it, so we should include it.
553
      #  This is really for testing only.
554
      key_base = "#{doi}/#{id}"
26✔
555
      attachment_key = [key_base, attachment_filename].join("/") unless attachment_key.include?(key_base)
26✔
556

557
      attachment_ext = File.extname(attachment_filename)
26✔
558
      attachment_query = attachment_key.gsub(attachment_ext, "")
26✔
559
      results = ActiveStorage::Blob.where("key LIKE :query", query: "%#{attachment_query}%")
26✔
560
      blobs = results.to_a
26✔
561

562
      if blobs.present?
26✔
563
        index = blobs.length + 1
2✔
564
        attachment_key = attachment_key.gsub(/\.([a-zA-Z0-9\.]+)$/, "_#{index}.\\1")
2✔
565
      end
566

567
      attachment_key
26✔
568
    end
569

570
    def track_state_change(user, state = aasm.to_state)
1✔
571
      uw = UserWork.new(user_id: user.id, work_id: id, state: state)
140✔
572
      uw.save!
140✔
573
      WorkActivity.add_work_activity(id, "marked as #{state.to_s.titleize}", user.id, activity_type: WorkActivity::SYSTEM)
140✔
574
      WorkStateTransitionNotification.new(self, user.id).send
140✔
575
    end
576

577
    def data_cite_connection
1✔
578
      @data_cite_connection ||= Datacite::Client.new(username: Rails.configuration.datacite.user,
45✔
579
                                                     password: Rails.configuration.datacite.password,
580
                                                     host: Rails.configuration.datacite.host)
581
    end
582

583
    def validate_ark
1✔
584
      return if ark.blank?
773✔
585
      first_save = id.blank?
144✔
586
      changed_value = metadata["ark"] != ark
144✔
587
      if first_save || changed_value
144✔
588
        errors.add(:base, "Invalid ARK provided for the Work: #{ark}") unless Ark.valid?(ark)
51✔
589
      end
590
    end
591

592
    # rubocop:disable Metrics/AbcSize
593
    def validate_metadata
1✔
594
      return if metadata.blank?
314✔
595
      errors.add(:base, "Must provide a title") if resource.main_title.blank?
314✔
596
      errors.add(:base, "Must provide a description") if resource.description.blank?
314✔
597
      errors.add(:base, "Must indicate the Publisher") if resource.publisher.blank?
314✔
598
      errors.add(:base, "Must indicate the Publication Year") if resource.publication_year.blank?
314✔
599
      errors.add(:base, "Must indicate a Rights statement") if resource.rights.nil?
314✔
600
      errors.add(:base, "Must provide a Version number") if resource.version_number.blank?
314✔
601
      validate_creators
314✔
602
      validate_related_objects
314✔
603
    end
604
    # rubocop:enable Metrics/AbcSize
605

606
    def validate_creators
1✔
607
      if resource.creators.count == 0
1,087✔
608
        errors.add(:base, "Must provide at least one Creator")
1✔
609
      else
610
        resource.creators.each do |creator|
1,086✔
611
          if creator.orcid.present? && Orcid.invalid?(creator.orcid)
1,839✔
612
            errors.add(:base, "ORCID for creator #{creator.value} is not in format 0000-0000-0000-0000")
1✔
613
          end
614
        end
615
      end
616
    end
617

618
    def validate_related_objects
1✔
619
      return if resource.related_objects.empty?
314✔
620
      invalid = resource.related_objects.reject(&:valid?)
6✔
621
      errors.add(:base, "Related Objects are invalid: #{invalid.map(&:errors).join(', ')}") if invalid.count.positive?
6✔
622
    end
623

624
    def publish_doi(user)
1✔
625
      return Rails.logger.info("Publishing hard-coded test DOI during development.") if self.class.publish_test_doi?
28✔
626

627
      if doi.starts_with?(Rails.configuration.datacite.prefix)
28✔
628
        result = data_cite_connection.update(id: doi, attributes: doi_attributes)
26✔
629
        if result.failure?
26✔
630
          resolved_user = curator_or_current_uid(user)
11✔
631
          message = "@#{resolved_user} Error publishing DOI. #{result.failure.status} / #{result.failure.reason_phrase}"
11✔
632
          WorkActivity.add_work_activity(id, message, user.id, activity_type: WorkActivity::DATACITE_ERROR)
11✔
633
        end
634
      elsif ark.blank? # we can not update the url anywhere
2✔
635
        Honeybadger.notify("Publishing for a DOI we do not own and no ARK is present: #{doi}")
1✔
636
      end
637
    end
638

639
    def doi_attribute_url
1✔
640
      "https://datacommons.princeton.edu/discovery/doi/#{doi}"
26✔
641
    end
642

643
    def doi_attribute_resource
1✔
644
      PDCMetadata::Resource.new_from_jsonb(metadata)
26✔
645
    end
646

647
    def doi_attribute_xml
1✔
648
      unencoded = doi_attribute_resource.to_xml
26✔
649
      Base64.encode64(unencoded)
26✔
650
    end
651

652
    def doi_attributes
1✔
653
      {
654
        "event" => "publish",
26✔
655
        "xml" => doi_attribute_xml,
656
        "url" => doi_attribute_url
657
      }
658
    end
659

660
    # This needs to be called #before_save
661
    # This ensures that new ActiveStorage::Attachment objects are persisted with custom keys (which are generated from the file name and DOI)
662
    # @param new_attachments [Array<ActiveStorage::Attachment>]
663
    def save_new_attachments(new_attachments:)
1✔
664
      new_attachments.each do |attachment|
30✔
665
        # There are cases (race conditions?) where the ActiveStorage::Blob objects are not persisted
666
        next if attachment.frozen?
30✔
667

668
        # This ensures that the custom key for the ActiveStorage::Attachment and ActiveStorage::Blob objects are generated
669
        generated_key = generate_attachment_key(attachment)
26✔
670
        attachment.blob.key = generated_key
26✔
671
        attachment.blob.save
26✔
672

673
        attachment.save
26✔
674
      end
675
    end
676

677
    # Request S3 Bucket Objects associated with this Work
678
    # @return [Array<S3File>]
679
    def s3_resources
1✔
680
      data_profile = s3_query_service.data_profile
1,443✔
681
      data_profile.fetch(:objects, [])
1,443✔
682
    end
683
    alias pre_curation_s3_resources s3_resources
1✔
684

685
    def s3_object_persisted?(s3_file)
1✔
686
      uploads_keys = uploads.map(&:key)
×
687
      uploads_keys.include?(s3_file.key)
×
688
    end
689

690
    def add_pre_curation_s3_object(s3_file)
1✔
691
      return if s3_object_persisted?(s3_file)
×
692

693
      persisted = s3_file.to_blob
×
694
      pre_curation_uploads.attach(persisted)
×
695
    end
696

697
    def publish_precurated_files
1✔
698
      # An error is raised if there are no files to be moved
699
      raise(StandardError, "Attempting to publish a Work without attached uploads for #{s3_object_key}") if pre_curation_uploads_fast.empty? && post_curation_uploads.empty?
22✔
700

701
      # We need to explicitly access to post-curation services here.
702
      # Lets explicitly create it so the state of the work does not have any impact.
703
      s3_post_curation_query_service = S3QueryService.new(self, false)
22✔
704

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

708
      # Copy the pre-curation S3 Objects to the post-curation S3 Bucket...
709
      transferred_file_errors = s3_query_service.publish_files
22✔
710

711
      # ...check that the files are indeed now in the post-curation bucket...
712
      if transferred_file_errors.count > 0
22✔
713
        raise(StandardError, "Failed to validate the uploaded S3 Object #{transferred_file_errors.map(&:key).join(', ')}")
×
714
      end
715
    end
716
end
717
# 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