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

pulibrary / pdc_describe / 4ca00168-2447-4396-8312-1c4c02d32f16

pending completion
4ca00168-2447-4396-8312-1c4c02d32f16

Pull #822

circleci

Hector Correa
More of less automates the process to export data from spec/system/data_migration and import it into a new instance via a rake task
Pull Request #822: PDC Discovery indexing (work in progress)

1726 of 1737 relevant lines covered (99.37%)

134.74 hits per line

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

99.15
/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
131✔
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, after: :notify_collection_curators
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
201✔
63
    valid_states = self.class.aasm.states.map(&:name)
201✔
64
    raise(StandardError, "Invalid state '#{new_state}'") unless valid_states.include?(new_state_sym)
201✔
65
    aasm_write_state_without_persistence(new_state_sym)
200✔
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 unfinished_works(user, search_terms = nil)
1✔
90
      works_by_user_state(user, ["none", "draft", "awaiting_approval"], search_terms)
41✔
91
    end
92

93
    def completed_works(user, search_terms = nil)
1✔
94
      works_by_user_state(user, "approved", search_terms)
38✔
95
    end
96

97
    def withdrawn_works(user, search_terms = nil)
1✔
98
      works_by_user_state(user, "withdrawn", search_terms)
38✔
99
    end
100

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

109
    def find_by_ark(ark)
1✔
110
      # This does a string compare to allow for partial matches
111
      # I'm not entirely sure why we are allowing partial matches in a find by
112
      Work.find_by!("metadata->>'ark' like ? ", "%#{ark}")
2✔
113
      # I think we should be doing the following instead, which matches exactly
114
      # Work.find_by!('metadata @> ?', "{\"ark\":\"#{ark}\"}")
115
    end
116

117
    delegate :resource_type_general_options, to: PDCMetadata::Resource
1✔
118

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

126
    private
1✔
127

128
      def search_terms_where_clause(search_terms)
1✔
129
        if search_terms.nil?
117✔
130
          Work.where("true")
90✔
131
        else
132
          Work.where("CAST(metadata AS VARCHAR) ILIKE :search_terms", search_terms: "%" + search_terms.strip + "%")
27✔
133
        end
134
      end
135

136
      def works_by_user_state(user, state, search_terms)
1✔
137
        search_terms_filter = search_terms_where_clause(search_terms)
117✔
138

139
        # The user's own works (if any) by state and search terms
140
        works = Work.where(created_by_user_id: user, state: state).and(search_terms_filter).to_a
117✔
141

142
        if user.admin_collections.count > 0
117✔
143
          # The works that match the given state, in all the collections the user can admin
144
          # (regardless of who created those works)
145
          user.admin_collections.each do |collection|
59✔
146
            works += Work.where(collection_id: collection.id, state: state).and(search_terms_filter)
72✔
147
          end
148
        end
149

150
        # Any other works where the user is mentioned
151
        works_mentioned_by_user_state(user, state, search_terms_filter).each do |work|
117✔
152
          already_included = !works.find { |existing_work| existing_work[:id] == work.id }.nil?
10✔
153
          works << work unless already_included
5✔
154
        end
155

156
        works.uniq(&:id).sort_by(&:updated_at).reverse
117✔
157
      end
158

159
      # Returns an array of work ids where a particular user has been mentioned
160
      # and the work is in a given state.
161
      def works_mentioned_by_user_state(user, state, search_terms_filter)
1✔
162
        Work.joins(:work_activity)
117✔
163
            .joins('INNER JOIN "work_activity_notifications" ON "work_activities"."id" = "work_activity_notifications"."work_activity_id"')
164
            .where(state: state)
165
            .where('"work_activity_notifications"."user_id" = ?', user.id)
166
            .and(search_terms_filter)
167
      end
168
  end
169

170
  include Rails.application.routes.url_helpers
1✔
171

172
  before_save do |work|
1✔
173
    # Ensure that the metadata JSON is persisted properly
174
    work.metadata = JSON.parse(work.resource.to_json)
613✔
175
    work.save_pre_curation_uploads
613✔
176
  end
177

178
  after_save do |work|
1✔
179
    if work.approved?
612✔
180
      work.attach_s3_resources if !work.pre_curation_uploads.empty? && work.pre_curation_uploads.length > work.post_curation_uploads.length
53✔
181
      work.reload
53✔
182
    end
183
  end
184

185
  validate do |work|
1✔
186
    if none?
638✔
187
      work.validate_doi
87✔
188
    elsif draft?
551✔
189
      work.valid_to_draft
376✔
190
    else
191
      work.valid_to_submit
175✔
192
    end
193
  end
194

195
  # Overload ActiveRecord.reload method
196
  # https://apidock.com/rails/ActiveRecord/Base/reload
197
  #
198
  # NOTE: Usually `after_save` is a better place to put this kind of code:
199
  #
200
  #   after_save do |work|
201
  #     work.resource = nil
202
  #   end
203
  #
204
  # but that does not work in this case because the block points to a different
205
  # memory object for `work` than the we want we want to reload.
206
  def reload(options = nil)
1✔
207
    super
149✔
208
    # Force `resource` to be reloaded
209
    @resource = nil
149✔
210
    self
149✔
211
  end
212

213
  def validate_doi
1✔
214
    return true unless user_entered_doi
87✔
215
    if /^10.\d{4,9}\/[-._;()\/:a-z0-9\-]+$/.match?(doi.downcase)
12✔
216
      response = Faraday.get("#{Rails.configuration.datacite.doi_url}#{doi}")
11✔
217
      errors.add(:base, "Invalid DOI: can not verify it's authenticity") unless response.success? || response.status == 302
11✔
218
    else
219
      errors.add(:base, "Invalid DOI: does not match format")
1✔
220
    end
221
    errors.count == 0
12✔
222
  end
223

224
  def valid_to_draft
1✔
225
    errors.add(:base, "Must provide a title") if resource.main_title.blank?
649✔
226
    validate_ark
649✔
227
    validate_creators
649✔
228
    validate_uploads
649✔
229
    errors.count == 0
649✔
230
  end
231

232
  def valid_to_submit
1✔
233
    valid_to_draft
244✔
234
    validate_metadata
244✔
235
    validate_uploads
244✔
236
    errors.count == 0
244✔
237
  end
238

239
  def valid_to_approve(user)
1✔
240
    valid_to_submit
26✔
241
    unless user.has_role? :collection_admin, collection
26✔
242
      errors.add :base, "Unauthorized to Approve"
4✔
243
    end
244
    errors.count == 0
26✔
245
  end
246

247
  def title
1✔
248
    resource.main_title
133✔
249
  end
250

251
  def uploads_attributes
1✔
252
    return [] if approved? # once approved we no longer allow the updating of uploads via the application
51✔
253
    uploads.map do |upload|
47✔
254
      {
255
        id: upload.id,
21✔
256
        key: upload.key,
257
        filename: upload.filename.to_s,
258
        created_at: upload.created_at,
259
        url: rails_blob_path(upload, disposition: "attachment")
260
      }
261
    end
262
  end
263

264
  def form_attributes
1✔
265
    {
266
      uploads: uploads_attributes
51✔
267
    }
268
  end
269

270
  def draft_doi
1✔
271
    return if resource.doi.present?
29✔
272
    resource.doi = if self.class.publish_test_doi?
19✔
273
                     Rails.logger.info "Using hard-coded test DOI during development."
1✔
274
                     "10.34770/tbd"
1✔
275
                   else
276
                     result = data_cite_connection.autogenerate_doi(prefix: Rails.configuration.datacite.prefix)
18✔
277
                     if result.success?
18✔
278
                       result.success.doi
17✔
279
                     else
280
                       raise("Error generating DOI. #{result.failure.status} / #{result.failure.reason_phrase}")
1✔
281
                     end
282
                   end
283
    save!
18✔
284
  end
285

286
  def created_by_user
1✔
287
    User.find(created_by_user_id)
155✔
288
  rescue ActiveRecord::RecordNotFound
289
    nil
1✔
290
  end
291

292
  def resource=(resource)
1✔
293
    @resource = resource
323✔
294
    self.metadata = JSON.parse(resource.to_json)
323✔
295
  end
296

297
  def resource
1✔
298
    @resource ||= PDCMetadata::Resource.new_from_json(metadata)
11,626✔
299
  end
300

301
  def url
1✔
302
    return unless persisted?
3✔
303

304
    @url ||= url_for(self)
3✔
305
  end
306

307
  def files_location_upload?
1✔
308
    files_location.blank? || files_location == "file_upload"
6✔
309
  end
310

311
  def files_location_cluster?
1✔
312
    files_location == "file_cluster"
48✔
313
  end
314

315
  def files_location_other?
1✔
316
    files_location == "file_other"
48✔
317
  end
318

319
  def change_curator(curator_user_id, current_user)
1✔
320
    if curator_user_id == "no-one"
5✔
321
      clear_curator(current_user)
1✔
322
    else
323
      update_curator(curator_user_id, current_user)
4✔
324
    end
325
  end
326

327
  def clear_curator(current_user)
1✔
328
    # Update the curator on the Work
329
    self.curator_user_id = nil
2✔
330
    save!
2✔
331

332
    # ...and log the activity
333
    WorkActivity.add_system_activity(id, "Unassigned existing curator", current_user.id)
2✔
334
  end
335

336
  def update_curator(curator_user_id, current_user)
1✔
337
    # Update the curator on the Work
338
    self.curator_user_id = curator_user_id
5✔
339
    save!
5✔
340

341
    # ...and log the activity
342
    new_curator = User.find(curator_user_id)
4✔
343
    message = if curator_user_id == current_user.id
4✔
344
                "Self-assigned as curator"
1✔
345
              else
346
                "Set curator to @#{new_curator.uid}"
3✔
347
              end
348
    WorkActivity.add_system_activity(id, message, current_user.id)
4✔
349
  end
350

351
  def curator_or_current_uid(user)
1✔
352
    persisted = if curator.nil?
4✔
353
                  user
3✔
354
                else
355
                  curator
1✔
356
                end
357
    persisted.uid
4✔
358
  end
359

360
  def add_comment(comment, current_user_id)
1✔
361
    WorkActivity.add_system_activity(id, comment, current_user_id, activity_type: "COMMENT")
6✔
362
  end
363

364
  def log_changes(resource_compare, current_user_id)
1✔
365
    return if resource_compare.identical?
36✔
366
    WorkActivity.add_system_activity(id, resource_compare.differences.to_json, current_user_id, activity_type: "CHANGES")
36✔
367
  end
368

369
  def log_file_changes(changes, current_user_id)
1✔
370
    return if changes.count == 0
63✔
371
    WorkActivity.add_system_activity(id, changes.to_json, current_user_id, activity_type: "FILE-CHANGES")
18✔
372
  end
373

374
  def activities
1✔
375
    WorkActivity.where(work_id: id).sort_by(&:updated_at).reverse
189✔
376
  end
377

378
  def changes
1✔
379
    activities.select(&:log_event_type?)
61✔
380
  end
381

382
  def comments
1✔
383
    activities.select(&:comment_event_type?)
61✔
384
  end
385

386
  def new_notification_count_for_user(user_id)
1✔
387
    WorkActivityNotification.joins(:work_activity)
33✔
388
                            .where(user_id: user_id, read_at: nil)
389
                            .where(work_activity: { work_id: id })
390
                            .count
391
  end
392

393
  # Marks as read the notifications for the given user_id in this work.
394
  # In practice, the user_id is the id of the current user and therefore this method marks the current's user
395
  # notifications as read.
396
  def mark_new_notifications_as_read(user_id)
1✔
397
    activities.each do |activity|
60✔
398
      unread_notifications = WorkActivityNotification.where(user_id: user_id, work_activity_id: activity.id, read_at: nil)
46✔
399
      unread_notifications.each do |notification|
46✔
400
        notification.read_at = Time.now.utc
1✔
401
        notification.save
1✔
402
      end
403
    end
404
  end
405

406
  def current_transition
1✔
407
    aasm.current_event.to_s.humanize.delete("!")
18✔
408
  end
409

410
  def uploads
1✔
411
    return post_curation_uploads if approved?
184✔
412

413
    pre_curation_uploads
167✔
414
  end
415

416
  # This ensures that new ActiveStorage::Attachment objects can be modified before they are persisted
417
  def save_pre_curation_uploads
1✔
418
    return if pre_curation_uploads.empty?
613✔
419

420
    new_attachments = pre_curation_uploads.reject(&:persisted?)
191✔
421
    return if new_attachments.empty?
191✔
422

423
    save_new_attachments(new_attachments: new_attachments)
160✔
424
  end
425

426
  # Accesses post-curation S3 Bucket Objects
427
  def post_curation_s3_resources
1✔
428
    return [] unless approved?
77✔
429

430
    s3_resources
77✔
431
  end
432
  alias post_curation_uploads post_curation_s3_resources
1✔
433

434
  def s3_client
1✔
435
    s3_query_service.client
67✔
436
  end
437

438
  delegate :bucket_name, to: :s3_query_service
1✔
439

440
  # Transmit a HEAD request for an S3 Object in the post-curation Bucket
441
  # @param key [String]
442
  # @param bucket_name [String]
443
  # @return [Aws::S3::Types::HeadObjectOutput]
444
  def find_post_curation_s3_object(bucket_name:, key:)
1✔
445
    s3_client.head_object({
23✔
446
                            bucket: bucket_name,
447
                            key: key
448
                          })
449
    true
23✔
450
  rescue Aws::S3::Errors::NotFound
451
    nil
×
452
  end
453

454
  # Generates the S3 Object key
455
  # @return [String]
456
  def s3_object_key
1✔
457
    "#{doi}/#{id}"
301✔
458
  end
459

460
  # Transmit a HEAD request for the S3 Bucket directory for this Work
461
  # @param bucket_name location to be checked to be found
462
  # @return [Aws::S3::Types::HeadObjectOutput]
463
  def find_post_curation_s3_dir(bucket_name:)
1✔
464
    s3_client.head_object({
22✔
465
                            bucket: bucket_name,
466
                            key: s3_object_key
467
                          })
468
    true
×
469
  rescue Aws::S3::Errors::NotFound
470
    nil
22✔
471
  end
472

473
  # Transmit a DELETE request for the S3 directory in the pre-curation Bucket
474
  # @return [Aws::S3::Types::DeleteObjectOutput]
475
  def delete_pre_curation_s3_dir
1✔
476
    s3_client.delete_object({
22✔
477
                              bucket: bucket_name,
478
                              key: s3_object_key
479
                            })
480
  rescue Aws::S3::Errors::ServiceError => error
481
    raise(StandardError, "Failed to delete the pre-curation S3 Bucket directory #{s3_object_key}: #{error}")
×
482
  end
483

484
  # 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).
485
  # However, a consequence of this is that #after_save is invoked whenever a new attached Blob or Attachment object is persisted.
486
  def attach_s3_resources
1✔
487
    return if approved?
86✔
488
    changes = []
52✔
489
    # This retrieves and adds S3 uploads if they do not exist
490
    pre_curation_s3_resources.each do |s3_file|
52✔
491
      if add_pre_curation_s3_object(s3_file)
16✔
492
        changes << { action: :added, filename: s3_file.filename }
13✔
493
      end
494
    end
495

496
    # Log the new files, but don't link the change to the current_user since we really don't know
497
    # who added the files directly to AWS S3.
498
    log_file_changes(changes, nil)
52✔
499
  end
500

501
  delegate :ark, :doi, :resource_type, :resource_type=, :resource_type_general, :resource_type_general=,
1✔
502
           :to_xml, :to_json, to: :resource
503

504
  protected
1✔
505

506
    # This must be protected, NOT private for ActiveRecord to work properly with this attribute.
507
    #   Protected will still keep others from setting the metatdata, but allows ActiveRecord the access it needs
508
    def metadata=(metadata)
1✔
509
      super
936✔
510
      @resource = PDCMetadata::Resource.new_from_json(metadata)
936✔
511
    end
512

513
  private
1✔
514

515
    def s3_file_to_blob(s3_file)
1✔
516
      existing_blob = ActiveStorage::Blob.find_by(key: s3_file.filename)
13✔
517

518
      if existing_blob.present?
13✔
519
        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✔
520
        return existing_blob
1✔
521
      end
522

523
      params = { filename: s3_file.filename, content_type: "", byte_size: s3_file.size, checksum: s3_file.checksum }
12✔
524
      blob = ActiveStorage::Blob.create_before_direct_upload!(**params)
12✔
525
      blob.key = s3_file.filename
12✔
526
      blob
12✔
527
    end
528

529
    def publish(user)
1✔
530
      publish_doi(user)
22✔
531
      update_ark_information
22✔
532
      publish_precurated_files
22✔
533
      save!
22✔
534
    end
535

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

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

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

560
      attachment_ext = File.extname(attachment_filename)
111✔
561
      attachment_query = attachment_key.gsub(attachment_ext, "")
111✔
562
      results = ActiveStorage::Blob.where("key LIKE :query", query: "%#{attachment_query}%")
111✔
563
      blobs = results.to_a
111✔
564

565
      if blobs.present?
111✔
566
        index = blobs.length + 1
28✔
567
        attachment_key = attachment_key.gsub(/\.([a-zA-Z0-9\.]+)$/, "_#{index}.\\1")
28✔
568
      end
569

570
      attachment_key
111✔
571
    end
572

573
    def track_state_change(user, state = aasm.to_state)
1✔
574
      uw = UserWork.new(user_id: user.id, work_id: id, state: state)
114✔
575
      uw.save!
114✔
576
      WorkActivity.add_system_activity(id, "marked as #{state}", user.id)
114✔
577
    end
578

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

585
    def validate_ark
1✔
586
      if ark.present?
649✔
587
        errors.add(:base, "Invalid ARK provided for the Work: #{ark}") unless Ark.valid?(ark)
92✔
588
      end
589
    end
590

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

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

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

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

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

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

642
    def doi_attribute_resource
1✔
643
      PDCMetadata::Resource.new_from_json(metadata)
18✔
644
    end
645

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

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

659
    def validate_uploads
1✔
660
      # The number of pre-curation uploads should be validated, as these are mutated directly
661
      if pre_curation_uploads.length > MAX_UPLOADS
893✔
662
        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✔
663
      end
664
    end
665

666
    # This needs to be called #before_save
667
    # This ensures that new ActiveStorage::Attachment objects are persisted with custom keys (which are generated from the file name and DOI)
668
    # @param new_attachments [Array<ActiveStorage::Attachment>]
669
    def save_new_attachments(new_attachments:)
1✔
670
      new_attachments.each do |attachment|
160✔
671
        # There are cases (race conditions?) where the ActiveStorage::Blob objects are not persisted
672
        next if attachment.frozen?
169✔
673

674
        # This ensures that the custom key for the ActiveStorage::Attachment and ActiveStorage::Blob objects are generated
675
        generated_key = generate_attachment_key(attachment)
111✔
676
        attachment.blob.key = generated_key
111✔
677
        attachment.blob.save
111✔
678

679
        attachment.save
111✔
680
      end
681
    end
682

683
    # S3QueryService object associated with this Work
684
    # @return [S3QueryService]
685
    def s3_query_service
1✔
686
      @s3_query_service = S3QueryService.new(self, !approved?)
218✔
687
    end
688

689
    # Request S3 Bucket Objects associated with this Work
690
    # @return [Array<S3File>]
691
    def s3_resources
1✔
692
      data_profile = s3_query_service.data_profile
129✔
693
      data_profile.fetch(:objects, [])
129✔
694
    end
695
    alias pre_curation_s3_resources s3_resources
1✔
696

697
    def s3_object_persisted?(s3_file)
1✔
698
      uploads_keys = uploads.map(&:key)
16✔
699
      uploads_keys.include?(s3_file.key)
16✔
700
    end
701

702
    def add_pre_curation_s3_object(s3_file)
1✔
703
      return if s3_object_persisted?(s3_file)
16✔
704

705
      persisted = s3_file_to_blob(s3_file)
13✔
706
      pre_curation_uploads.attach(persisted)
13✔
707
    end
708

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

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

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

720
      # Copy the pre-curation S3 Objects to the post-curation S3 Bucket...
721
      transferred_files = s3_post_curation_query_service.publish_files
22✔
722

723
      # ...check that the files are indeed now in the post-curation bucket...
724
      pre_curation_uploads.each do |attachment|
22✔
725
        s3_object = find_post_curation_s3_object(bucket_name: s3_post_curation_query_service.bucket_name, key: attachment.key)
23✔
726
        raise(StandardError, "Failed to validate the uploaded S3 Object #{attachment.key}") if s3_object.nil?
23✔
727
      end
728

729
      # ...and delete them from the pre-curation bucket.
730
      transferred_files.each(&:purge)
22✔
731
      delete_pre_curation_s3_dir
22✔
732
    end
733

734
    def notify_collection_curators(current_user)
1✔
735
      curators = collection.administrators.map { |admin| "@#{admin.uid}" }.join(", ")
47✔
736
      notification = "#{curators} The [work](#{work_url(self)}) is ready for review."
39✔
737
      WorkActivity.add_system_activity(id, notification, current_user.id)
39✔
738
    end
739
end
740
# 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