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

pulibrary / pdc_describe / 95ff1d10-5774-4415-8b66-ffb4f4f58024

pending completion
95ff1d10-5774-4415-8b66-ffb4f4f58024

Pull #725

circleci

Carolyn Cole
Updating manifest not to include the javascript for edit This seems to do nothing, so I am removing it
Pull Request #725: Updating manifest not to include the javascript for edit

1638 of 1684 relevant lines covered (97.27%)

102.75 hits per line

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

97.42
/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
74✔
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

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, after: :notify_collection_curators
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
199✔
62
    valid_states = self.class.aasm.states.map(&:name)
199✔
63
    raise(StandardError, "Invalid state '#{new_state}'") unless valid_states.include?(new_state_sym)
199✔
64
    aasm_write_state_without_persistence(new_state_sym)
198✔
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)
58✔
77
  end
78

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

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

87
  class << self
1✔
88
    def unfinished_works(user)
1✔
89
      works_by_user_state(user, ["none", "draft", "awaiting_approval"])
22✔
90
    end
91

92
    def completed_works(user)
1✔
93
      works_by_user_state(user, "approved")
19✔
94
    end
95

96
    def withdrawn_works(user)
1✔
97
      works_by_user_state(user, "withdrawn")
19✔
98
    end
99

100
    def find_by_doi(doi)
1✔
101
      models = all.select { |work| !work.doi.nil? && work.doi.include?(doi) }
4✔
102
      raise ActiveRecord::RecordNotFound if models.empty?
2✔
103
      models.first
2✔
104
    end
105

106
    def find_by_ark(ark)
1✔
107
      models = all.select { |work| !work.ark.nil? && work.ark.include?(ark) }
4✔
108
      raise ActiveRecord::RecordNotFound if models.empty?
2✔
109
      models.first
2✔
110
    end
111

112
    delegate :resource_type_general_options, to: PDCMetadata::Resource
1✔
113

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

121
    private
1✔
122

123
      def works_by_user_state(user, state)
1✔
124
        # The user's own works by state (if any)
125
        works = Work.where(created_by_user_id: user, state: state).to_a
60✔
126

127
        if user.admin_collections.count > 0
60✔
128
          # The works that match the given state, in all the collections the user can admin
129
          # (regardless of who created those works)
130
          user.admin_collections.each do |collection|
23✔
131
            works += Work.where(collection_id: collection.id, state: state)
27✔
132
          end
133
        end
134

135
        # Any other works where the user is mentioned
136
        works_mentioned_by_user_state(user, state).each do |work|
60✔
137
          already_included = !works.find { |existing_work| existing_work[:id] == work.id }.nil?
3✔
138
          works << work unless already_included
1✔
139
        end
140

141
        works.uniq(&:id).sort_by(&:updated_at).reverse
60✔
142
      end
143

144
      # Returns an array of work ids where a particular user has been mentioned
145
      # and the work is in a given state.
146
      def works_mentioned_by_user_state(user, state)
1✔
147
        Work.joins(:work_activity)
60✔
148
            .joins('INNER JOIN "work_activity_notifications" ON "work_activities"."id" = "work_activity_notifications"."work_activity_id"')
149
            .where(state: state)
150
            .where('"work_activity_notifications"."user_id" = ?', user.id)
151
      end
152
  end
153

154
  include Rails.application.routes.url_helpers
1✔
155

156
  before_save do |work|
1✔
157
    # Ensure that the metadata JSON is persisted properly
158
    work.metadata = work.resource.to_json
537✔
159
    work.save_pre_curation_uploads
537✔
160
  end
161

162
  after_save do |work|
1✔
163
    if work.approved?
536✔
164
      work.attach_s3_resources if !work.pre_curation_uploads.empty? && work.pre_curation_uploads.length > work.post_curation_uploads.length
50✔
165
      work.reload
50✔
166
    end
167
  end
168

169
  validate do |work|
1✔
170
    if none?
547✔
171
      work.validate_doi
62✔
172
    elsif draft?
485✔
173
      work.valid_to_draft
323✔
174
    else
175
      work.valid_to_submit
162✔
176
    end
177
  end
178

179
  # Overload ActiveRecord.reload method
180
  # https://apidock.com/rails/ActiveRecord/Base/reload
181
  #
182
  # NOTE: Usually `after_save` is a better place to put this kind of code:
183
  #
184
  #   after_save do |work|
185
  #     work.resource = nil
186
  #   end
187
  #
188
  # but that does not work in this case because the block points to a different
189
  # memory object for `work` than the we want we want to reload.
190
  def reload(options = nil)
1✔
191
    super
136✔
192
    # Force `resource` to be reloaded
193
    @resource = nil
136✔
194
    self
136✔
195
  end
196

197
  def validate_doi
1✔
198
    return true unless user_entered_doi
62✔
199
    if /^10.\d{4,9}\/[-._;()\/:a-z0-9\-]+$/.match?(doi.downcase)
×
200
      response = Faraday.get("#{Rails.configuration.datacite.doi_url}#{doi}")
×
201
      errors.add(:base, "Invalid DOI: can not verify it's authenticity") unless response.success? || response.status == 302
×
202
    else
203
      errors.add(:base, "Invalid DOI: does not match format")
×
204
    end
205
    errors.count == 0
×
206
  end
207

208
  def valid_to_draft
1✔
209
    errors.add(:base, "Must provide a title") if resource.main_title.blank?
556✔
210
    validate_ark
556✔
211
    validate_creators
556✔
212
    validate_uploads
556✔
213
    errors.count == 0
556✔
214
  end
215

216
  def valid_to_submit
1✔
217
    valid_to_draft
222✔
218
    validate_metadata
222✔
219
    validate_uploads
222✔
220
    errors.count == 0
222✔
221
  end
222

223
  def valid_to_approve(user)
1✔
224
    valid_to_submit
26✔
225
    unless user.has_role? :collection_admin, collection
26✔
226
      errors.add :base, "Unauthorized to Approve"
4✔
227
    end
228
    errors.count == 0
26✔
229
  end
230

231
  def title
1✔
232
    resource.main_title
64✔
233
  end
234

235
  def curator
1✔
236
    return nil if curator_user_id.nil?
16✔
237
    User.find(curator_user_id)
5✔
238
  end
239

240
  def uploads_attributes
1✔
241
    return [] if approved? # once approved we no longer allow the updating of uploads via the application
38✔
242
    uploads.map do |upload|
34✔
243
      {
244
        id: upload.id,
19✔
245
        key: upload.key,
246
        filename: upload.filename.to_s,
247
        created_at: upload.created_at,
248
        url: rails_blob_path(upload, disposition: "attachment")
249
      }
250
    end
251
  end
252

253
  def form_attributes
1✔
254
    {
255
      uploads: uploads_attributes
38✔
256
    }
257
  end
258

259
  def draft_doi
1✔
260
    return if resource.doi.present?
12✔
261
    resource.doi = if self.class.publish_test_doi?
11✔
262
                     Rails.logger.info "Using hard-coded test DOI during development."
1✔
263
                     "10.34770/tbd"
1✔
264
                   else
265
                     result = data_cite_connection.autogenerate_doi(prefix: Rails.configuration.datacite.prefix)
10✔
266
                     if result.success?
10✔
267
                       result.success.doi
9✔
268
                     else
269
                       raise("Error generating DOI. #{result.failure.status} / #{result.failure.reason_phrase}")
1✔
270
                     end
271
                   end
272
    save!
10✔
273
  end
274

275
  def created_by_user
1✔
276
    User.find(created_by_user_id)
114✔
277
  rescue ActiveRecord::RecordNotFound
278
    nil
1✔
279
  end
280

281
  def resource=(resource)
1✔
282
    @resource = resource
272✔
283
    self.metadata = resource.to_json
272✔
284
  end
285

286
  def resource
1✔
287
    @resource ||= PDCMetadata::Resource.new_from_json(metadata)
8,918✔
288
  end
289

290
  def url
1✔
291
    return unless persisted?
3✔
292

293
    @url ||= url_for(self)
3✔
294
  end
295

296
  def files_location_upload?
1✔
297
    files_location.blank? || files_location == "file_upload"
×
298
  end
299

300
  def files_location_cluster?
1✔
301
    files_location == "file_cluster"
21✔
302
  end
303

304
  def files_location_other?
1✔
305
    files_location == "file_other"
21✔
306
  end
307

308
  def change_curator(curator_user_id, current_user)
1✔
309
    if curator_user_id == "no-one"
5✔
310
      clear_curator(current_user)
1✔
311
    else
312
      update_curator(curator_user_id, current_user)
4✔
313
    end
314
  end
315

316
  def clear_curator(current_user)
1✔
317
    # Update the curator on the Work
318
    self.curator_user_id = nil
2✔
319
    save!
2✔
320

321
    # ...and log the activity
322
    WorkActivity.add_system_activity(id, "Unassigned existing curator", current_user.id)
2✔
323
  end
324

325
  def update_curator(curator_user_id, current_user)
1✔
326
    # Update the curator on the Work
327
    self.curator_user_id = curator_user_id
5✔
328
    save!
5✔
329

330
    # ...and log the activity
331
    new_curator = User.find(curator_user_id)
4✔
332
    message = if curator_user_id == current_user.id
4✔
333
                "Self-assigned as curator"
1✔
334
              else
335
                "Set curator to @#{new_curator.uid}"
3✔
336
              end
337
    WorkActivity.add_system_activity(id, message, current_user.id)
4✔
338
  end
339

340
  def curator_or_current_uid(user)
1✔
341
    persisted = if curator.nil?
4✔
342
                  user
3✔
343
                else
344
                  curator
1✔
345
                end
346
    persisted.uid
4✔
347
  end
348

349
  def add_comment(comment, current_user_id)
1✔
350
    WorkActivity.add_system_activity(id, comment, current_user_id, activity_type: "COMMENT")
6✔
351
  end
352

353
  def log_changes(resource_compare, current_user_id)
1✔
354
    return if resource_compare.identical?
16✔
355
    WorkActivity.add_system_activity(id, resource_compare.differences.to_json, current_user_id, activity_type: "CHANGES")
16✔
356
  end
357

358
  def log_file_changes(changes, current_user_id)
1✔
359
    return if changes.count == 0
37✔
360
    WorkActivity.add_system_activity(id, changes.to_json, current_user_id, activity_type: "FILE-CHANGES")
16✔
361
  end
362

363
  def activities
1✔
364
    WorkActivity.where(work_id: id).sort_by(&:updated_at).reverse
77✔
365
  end
366

367
  def new_notification_count_for_user(user_id)
1✔
368
    WorkActivityNotification.joins(:work_activity)
8✔
369
                            .where(user_id: user_id, read_at: nil)
370
                            .where(work_activity: { work_id: id })
371
                            .count
372
  end
373

374
  # Marks as read the notifications for the given user_id in this work.
375
  # In practice, the user_id is the id of the current user and therefore this method marks the current's user
376
  # notifications as read.
377
  def mark_new_notifications_as_read(user_id)
1✔
378
    activities.each do |activity|
36✔
379
      unread_notifications = WorkActivityNotification.where(user_id: user_id, work_activity_id: activity.id, read_at: nil)
11✔
380
      unread_notifications.each do |notification|
11✔
381
        notification.read_at = Time.now.utc
1✔
382
        notification.save
1✔
383
      end
384
    end
385
  end
386

387
  def current_transition
1✔
388
    aasm.current_event.to_s.humanize.delete("!")
14✔
389
  end
390

391
  def uploads
1✔
392
    return post_curation_uploads if approved?
143✔
393

394
    pre_curation_uploads
126✔
395
  end
396

397
  # This ensures that new ActiveStorage::Attachment objects can be modified before they are persisted
398
  def save_pre_curation_uploads
1✔
399
    return if pre_curation_uploads.empty?
537✔
400

401
    new_attachments = pre_curation_uploads.reject(&:persisted?)
184✔
402
    return if new_attachments.empty?
184✔
403

404
    save_new_attachments(new_attachments: new_attachments)
158✔
405
  end
406

407
  # Accesses post-curation S3 Bucket Objects
408
  def post_curation_s3_resources
1✔
409
    return [] unless approved?
77✔
410

411
    s3_resources
77✔
412
  end
413
  alias post_curation_uploads post_curation_s3_resources
1✔
414

415
  def s3_client
1✔
416
    s3_query_service.client
67✔
417
  end
418

419
  delegate :bucket_name, to: :s3_query_service
1✔
420

421
  # Transmit a HEAD request for an S3 Object in the post-curation Bucket
422
  # @param key [String]
423
  # @param bucket_name [String]
424
  # @return [Aws::S3::Types::HeadObjectOutput]
425
  def find_post_curation_s3_object(bucket_name:, key:)
1✔
426
    s3_client.head_object({
23✔
427
                            bucket: bucket_name,
428
                            key: key
429
                          })
430
    true
23✔
431
  rescue Aws::S3::Errors::NotFound
432
    nil
×
433
  end
434

435
  # Generates the S3 Object key
436
  # @return [String]
437
  def s3_object_key
1✔
438
    "#{doi}/#{id}"
290✔
439
  end
440

441
  # Transmit a HEAD request for the S3 Bucket directory for this Work
442
  # @param bucket_name location to be checked to be found
443
  # @return [Aws::S3::Types::HeadObjectOutput]
444
  def find_post_curation_s3_dir(bucket_name:)
1✔
445
    s3_client.head_object({
22✔
446
                            bucket: bucket_name,
447
                            key: s3_object_key
448
                          })
449
    true
×
450
  rescue Aws::S3::Errors::NotFound
451
    nil
22✔
452
  end
453

454
  # Transmit a DELETE request for the S3 directory in the pre-curation Bucket
455
  # @return [Aws::S3::Types::DeleteObjectOutput]
456
  def delete_pre_curation_s3_dir
1✔
457
    s3_client.delete_object({
22✔
458
                              bucket: bucket_name,
459
                              key: s3_object_key
460
                            })
461
  rescue Aws::S3::Errors::ServiceError => error
462
    raise(StandardError, "Failed to delete the pre-curation S3 Bucket directory #{s3_object_key}: #{error}")
×
463
  end
464

465
  # 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).
466
  # However, a consequence of this is that #after_save is invoked whenever a new attached Blob or Attachment object is persisted.
467
  def attach_s3_resources
1✔
468
    return if approved?
62✔
469
    changes = []
28✔
470
    # This retrieves and adds S3 uploads if they do not exist
471
    pre_curation_s3_resources.each do |s3_file|
28✔
472
      if add_pre_curation_s3_object(s3_file)
16✔
473
        changes << { action: :added, filename: s3_file.filename }
13✔
474
      end
475
    end
476

477
    # Log the new files, but don't link the change to the current_user since we really don't know
478
    # who added the files directly to AWS S3.
479
    log_file_changes(changes, nil)
28✔
480
  end
481

482
  delegate :ark, :doi, :resource_type, :resource_type=, :resource_type_general, :resource_type_general=,
1✔
483
           :to_xml, :to_json, to: :resource
484

485
  protected
1✔
486

487
    # This must be protected, NOT private for ActiveRecord to work properly with this attribute.
488
    #   Protected will still keep others from setting the metatdata, but allows ActiveRecord the access it needs
489
    def metadata=(metadata)
1✔
490
      super
809✔
491
      @resource = PDCMetadata::Resource.new_from_json(metadata)
809✔
492
    end
493

494
  private
1✔
495

496
    def s3_file_to_blob(s3_file)
1✔
497
      existing_blob = ActiveStorage::Blob.find_by(key: s3_file.filename)
13✔
498

499
      if existing_blob.present?
13✔
500
        Rails.logger.warn("There is a blob existing for #{s3_file.filename}, which we are not expecting!  It will be reattached #{existing_blob.inspect}")
1✔
501
        return existing_blob
1✔
502
      end
503

504
      params = { filename: s3_file.filename, content_type: "", byte_size: s3_file.size, checksum: s3_file.checksum }
12✔
505
      blob = ActiveStorage::Blob.create_before_direct_upload!(**params)
12✔
506
      blob.key = s3_file.filename
12✔
507
      blob
12✔
508
    end
509

510
    def publish(user)
1✔
511
      publish_doi(user)
22✔
512
      update_ark_information
22✔
513
      publish_precurated_files
22✔
514
      save!
22✔
515
    end
516

517
    # Update EZID (our provider of ARKs) with the new information for this work.
518
    def update_ark_information
1✔
519
      # We only want to update the ark url under certain conditions.
520
      # Set this value in config/update_ark_url.yml
521
      if Rails.configuration.update_ark_url
22✔
522
        if ark.present?
5✔
523
          Ark.update(ark, url)
3✔
524
        end
525
      end
526
    end
527

528
    # Generates the key for ActiveStorage::Attachment and Attachment::Blob objects
529
    # @param attachment [ActiveStorage::Attachment]
530
    # @return [String]
531
    def generate_attachment_key(attachment)
1✔
532
      attachment_filename = attachment.filename.to_s
108✔
533
      attachment_key = attachment.key
108✔
534

535
      # Files actually coming from S3 include the DOI and bucket as part of the file name
536
      #  Files being attached in another manner may not have it, so we should include it.
537
      #  This is really for testing only.
538
      key_base = "#{doi}/#{id}"
108✔
539
      attachment_key = [key_base, attachment_filename].join("/") unless attachment_key.include?(key_base)
108✔
540

541
      attachment_ext = File.extname(attachment_filename)
108✔
542
      attachment_query = attachment_key.gsub(attachment_ext, "")
108✔
543
      results = ActiveStorage::Blob.where("key LIKE :query", query: "%#{attachment_query}%")
108✔
544
      blobs = results.to_a
108✔
545

546
      if blobs.present?
108✔
547
        index = blobs.length + 1
28✔
548
        attachment_key = attachment_key.gsub(/\.([a-zA-Z0-9\.]+)$/, "_#{index}.\\1")
28✔
549
      end
550

551
      attachment_key
108✔
552
    end
553

554
    def track_state_change(user, state = aasm.to_state)
1✔
555
      uw = UserWork.new(user_id: user.id, work_id: id, state: state)
89✔
556
      uw.save!
89✔
557
      WorkActivity.add_system_activity(id, "marked as #{state}", user.id)
89✔
558
    end
559

560
    def data_cite_connection
1✔
561
      @data_cite_connection ||= Datacite::Client.new(username: Rails.configuration.datacite.user,
28✔
562
                                                     password: Rails.configuration.datacite.password,
563
                                                     host: Rails.configuration.datacite.host)
564
    end
565

566
    def validate_ark
1✔
567
      if ark.present?
556✔
568
        errors.add(:base, "Invalid ARK provided for the Work: #{ark}") unless Ark.valid?(ark)
64✔
569
      end
570
    end
571

572
    # rubocop:disable Metrics/AbcSize
573
    def validate_metadata
1✔
574
      return if metadata.blank?
222✔
575
      errors.add(:base, "Must provide a title") if resource.main_title.blank?
222✔
576
      errors.add(:base, "Must provide a description") if resource.description.blank?
222✔
577
      errors.add(:base, "Must indicate the Publisher") if resource.publisher.blank?
222✔
578
      errors.add(:base, "Must indicate the Publication Year") if resource.publication_year.blank?
222✔
579
      errors.add(:base, "Must indicate a Rights statement") if resource.rights.nil?
222✔
580
      errors.add(:base, "Must provide a Version number") if resource.version_number.blank?
222✔
581
      validate_creators
222✔
582
      validate_related_objects
222✔
583
    end
584
    # rubocop:enable Metrics/AbcSize
585

586
    def validate_creators
1✔
587
      if resource.creators.count == 0
778✔
588
        errors.add(:base, "Must provide at least one Creator")
1✔
589
      else
590
        resource.creators.each do |creator|
777✔
591
          if creator.orcid.present? && Orcid.invalid?(creator.orcid)
787✔
592
            errors.add(:base, "ORCID for creator #{creator.value} is not in format 0000-0000-0000-0000")
1✔
593
          end
594
        end
595
      end
596
    end
597

598
    def validate_related_objects
1✔
599
      return if resource.related_objects.empty?
222✔
600
      invalid = resource.related_objects.reject(&:valid?)
1✔
601
      errors.add(:base, "Related Objects are invalid: #{invalid.map(&:errors).join(', ')}") if invalid.count.positive?
1✔
602
    end
603

604
    def publish_doi(user)
1✔
605
      return Rails.logger.info("Publishing hard-coded test DOI during development.") if self.class.publish_test_doi?
20✔
606

607
      if doi.starts_with?(Rails.configuration.datacite.prefix)
20✔
608
        result = data_cite_connection.update(id: doi, attributes: doi_attributes)
18✔
609
        if result.failure?
18✔
610
          resolved_user = curator_or_current_uid(user)
3✔
611
          message = "@#{resolved_user} Error publishing DOI. #{result.failure.status} / #{result.failure.reason_phrase}"
3✔
612
          WorkActivity.add_system_activity(id, message, user.id, activity_type: "DATACITE_ERROR")
3✔
613
        end
614
      elsif ark.blank? # we can not update the url anywhere
2✔
615
        Honeybadger.notify("Publishing for a DOI we do not own and no ARK is present: #{doi}")
1✔
616
      end
617
    end
618

619
    def doi_attribute_url
1✔
620
      "https://datacommons.princeton.edu/discovery/doi/#{doi}"
18✔
621
    end
622

623
    def doi_attribute_resource
1✔
624
      PDCMetadata::Resource.new_from_json(metadata)
18✔
625
    end
626

627
    def doi_attribute_xml
1✔
628
      unencoded = doi_attribute_resource.to_xml
18✔
629
      Base64.encode64(unencoded)
18✔
630
    end
631

632
    def doi_attributes
1✔
633
      {
634
        "event" => "publish",
18✔
635
        "xml" => doi_attribute_xml,
636
        "url" => doi_attribute_url
637
      }
638
    end
639

640
    def validate_uploads
1✔
641
      # The number of pre-curation uploads should be validated, as these are mutated directly
642
      if pre_curation_uploads.length > MAX_UPLOADS
778✔
643
        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✔
644
      end
645
    end
646

647
    # This needs to be called #before_save
648
    # This ensures that new ActiveStorage::Attachment objects are persisted with custom keys (which are generated from the file name and DOI)
649
    # @param new_attachments [Array<ActiveStorage::Attachment>]
650
    def save_new_attachments(new_attachments:)
1✔
651
      new_attachments.each do |attachment|
158✔
652
        # There are cases (race conditions?) where the ActiveStorage::Blob objects are not persisted
653
        next if attachment.frozen?
166✔
654

655
        # This ensures that the custom key for the ActiveStorage::Attachment and ActiveStorage::Blob objects are generated
656
        generated_key = generate_attachment_key(attachment)
108✔
657
        attachment.blob.key = generated_key
108✔
658
        attachment.blob.save
108✔
659

660
        attachment.save
108✔
661
      end
662
    end
663

664
    # S3QueryService object associated with this Work
665
    # @return [S3QueryService]
666
    def s3_query_service
1✔
667
      @s3_query_service = S3QueryService.new(self, !approved?)
194✔
668
    end
669

670
    # Request S3 Bucket Objects associated with this Work
671
    # @return [Array<S3File>]
672
    def s3_resources
1✔
673
      data_profile = s3_query_service.data_profile
105✔
674
      data_profile.fetch(:objects, [])
105✔
675
    end
676
    alias pre_curation_s3_resources s3_resources
1✔
677

678
    def s3_object_persisted?(s3_file)
1✔
679
      uploads_keys = uploads.map(&:key)
16✔
680
      uploads_keys.include?(s3_file.key)
16✔
681
    end
682

683
    def add_pre_curation_s3_object(s3_file)
1✔
684
      return if s3_object_persisted?(s3_file)
16✔
685

686
      persisted = s3_file_to_blob(s3_file)
13✔
687
      pre_curation_uploads.attach(persisted)
13✔
688
    end
689

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

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

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

701
      # Copy the pre-curation S3 Objects to the post-curation S3 Bucket...
702
      transferred_files = s3_post_curation_query_service.publish_files
22✔
703

704
      # ...check that the files are indeed now in the post-curation bucket...
705
      pre_curation_uploads.each do |attachment|
22✔
706
        s3_object = find_post_curation_s3_object(bucket_name: s3_post_curation_query_service.bucket_name, key: attachment.key)
23✔
707
        raise(StandardError, "Failed to validate the uploaded S3 Object #{attachment.key}") if s3_object.nil?
23✔
708
      end
709

710
      # ...and delete them from the pre-curation bucket.
711
      transferred_files.each(&:purge)
22✔
712
      delete_pre_curation_s3_dir
22✔
713
    end
714

715
    def notify_collection_curators(current_user)
1✔
716
      curators = collection.administrators.map { |admin| "@#{admin.uid}" }.join(", ")
35✔
717
      notification = "#{curators} The [work](#{work_url(self)}) is ready for review."
31✔
718
      WorkActivity.add_system_activity(id, notification, current_user.id)
31✔
719
    end
720
end
721
# rubocop:ensable 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