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

pulibrary / pdc_describe / e120814a-af4f-469a-9335-a1787b055802

14 Apr 2025 06:57PM UTC coverage: 95.43% (+0.07%) from 95.36%
e120814a-af4f-469a-9335-a1787b055802

Pull #2099

circleci

carolyncole
Add a service for moving embargo files in postcuration
This will move the files from post curation to the embargo bucket
Pull Request #2099: Add a service for moving embargo files in post-curation

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

13 existing lines in 2 files now uncovered.

3508 of 3676 relevant lines covered (95.43%)

392.09 hits per line

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

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

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

8
  has_many :work_activity, -> { order(updated_at: :desc) }, dependent: :destroy
502✔
9
  has_many :user_work, -> { order(updated_at: :desc) }, dependent: :destroy
20✔
10
  has_many :upload_snapshots, -> { order(updated_at: :desc) }, dependent: :destroy
638✔
11

12
  belongs_to :group, class_name: "Group"
2✔
13
  belongs_to :curator, class_name: "User", foreign_key: "curator_user_id", optional: true
2✔
14

15
  attribute :work_type, :string, default: "DATASET"
2✔
16
  attribute :profile, :string, default: "DATACITE"
2✔
17

18
  attr_accessor :user_entered_doi
2✔
19

20
  alias state_history user_work
2✔
21

22
  delegate :valid_to_submit, :valid_to_draft, :valid_to_approve, :valid_to_complete, to: :work_validator
2✔
23

24
  include AASM
2✔
25

26
  aasm column: :state do
2✔
27
    state :none, initial: true
2✔
28
    state :draft, :awaiting_approval, :approved, :withdrawn, :deletion_marker
2✔
29

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

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

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

42
    event :revert_to_draft do
2✔
43
      transitions from: :awaiting_approval, to: :draft, guard: :valid_to_draft
2✔
44
    end
45

46
    event :approve do
2✔
47
      transitions from: :awaiting_approval, to: :approved, guard: :valid_to_approve, after: :publish
2✔
48
    end
49

50
    event :withdraw do
2✔
51
      transitions from: [:draft, :awaiting_approval, :approved], to: :withdrawn
2✔
52
    end
53

54
    event :resubmit do
2✔
55
      transitions from: :withdrawn, to: :draft
2✔
56
    end
57

58
    event :remove do
2✔
59
      transitions from: :withdrawn, to: :deletion_marker
2✔
60
    end
61

62
    after_all_events :track_state_change
2✔
63
  end
64

65
  def state=(new_state)
2✔
66
    new_state_sym = new_state.to_sym
1,596✔
67
    valid_states = self.class.aasm.states.map(&:name)
1,596✔
68
    raise(StandardError, "Invalid state '#{new_state}'") unless valid_states.include?(new_state_sym)
1,596✔
69
    aasm_write_state_without_persistence(new_state_sym)
1,594✔
70
  end
71

72
  ##
73
  # Is this work editable by a given user?
74
  # A work is editable when:
75
  # * it is being edited by the person who made it
76
  # * it is being edited by a group admin of the group where is resides
77
  # * it is being edited by a super admin
78
  # @param [User]
79
  # @return [Boolean]
80
  def editable_by?(user)
2✔
81
    submitted_by?(user) || administered_by?(user)
732✔
82
  end
83

84
  def editable_in_current_state?(user)
2✔
85
    # anyone with edit privleges can edit a work while it is in draft
86
    return editable_by?(user) if draft?
482✔
87

88
    # Only admisitrators can edit a work in other states
89
    administered_by?(user)
110✔
90
  end
91

92
  def submitted_by?(user)
2✔
93
    created_by_user_id == user.id
732✔
94
  end
95

96
  def administered_by?(user)
2✔
97
    user.has_role?(:group_admin, group)
222✔
98
  end
99

100
  class << self
2✔
101
    def find_by_doi(doi)
2✔
102
      prefix = "10.34770/"
42✔
103
      doi = "#{prefix}#{doi}" unless doi.blank? || doi.start_with?(prefix)
42✔
104
      Work.find_by!("metadata @> ?", JSON.dump(doi:))
42✔
105
    end
106

107
    def find_by_ark(ark)
2✔
108
      prefix = "ark:/"
440✔
109
      ark = "#{prefix}#{ark}" unless ark.blank? || ark.start_with?(prefix)
440✔
110
      Work.find_by!("metadata @> ?", JSON.dump(ark:))
440✔
111
    end
112

113
    delegate :resource_type_general_values, to: PDCMetadata::Resource
2✔
114

115
    def list_embargoed
2✔
116
      Work.where("embargo_date >= current_date").where(state: "approved")
2✔
117
    end
118

119
    def list_released_embargo
2✔
120
      Work.where("embargo_date = current_date-1").where(state: "approved")
2✔
121
    end
122
  end
123

124
  include Rails.application.routes.url_helpers
2✔
125

126
  before_save do |work|
2✔
127
    # Ensure that the metadata JSONB postgres field is persisted properly
128
    work.metadata = JSON.parse(work.resource.to_json)
2,456✔
129
  end
130

131
  after_save do |work|
2✔
132
    if work.approved?
2,454✔
133
      work.reload
282✔
134
    end
135
  end
136

137
  validate do |_work|
2✔
138
    work_validator.valid?
2,506✔
139
  end
140

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

159
  def title
2✔
160
    resource.main_title
888✔
161
  end
162

163
  def uploads_attributes
2✔
164
    return [] if approved? # once approved we no longer allow the updating of uploads via the application
170✔
165
    uploads.map do |upload|
158✔
166
      {
167
        id: upload.id,
88✔
168
        key: upload.key,
169
        filename: upload.filename.to_s,
170
        created_at: upload.created_at,
171
        url: upload.url
172
      }
173
    end
174
  end
175

176
  def form_attributes
2✔
177
    {
178
      uploads: uploads_attributes
170✔
179
    }
180
  end
181

182
  def draft_doi
2✔
183
    return if resource.doi.present?
66✔
184
    resource.doi = datacite_service.draft_doi
48✔
185
    save!
44✔
186
  end
187

188
  # Return the DOI formatted as a URL, so it can be used as a link on display pages
189
  # @return [String] A url formatted version of the DOI
190
  def doi_url
2✔
191
    return "https://doi.org/#{doi}" unless doi.starts_with?("https://doi.org")
2✔
192
    doi
×
193
  end
194

195
  def created_by_user
2✔
196
    User.find(created_by_user_id)
862✔
197
  rescue ActiveRecord::RecordNotFound
198
    nil
2✔
199
  end
200

201
  def resource=(resource)
2✔
202
    @resource = resource
1,966✔
203
    # Ensure that the metadata JSONB postgres field is persisted properly
204
    self.metadata = JSON.parse(resource.to_json)
1,966✔
205
  end
206

207
  def resource
2✔
208
    @resource ||= PDCMetadata::Resource.new_from_jsonb(metadata)
52,478✔
209
  end
210

211
  def url
2✔
212
    return unless persisted?
×
213

214
    @url ||= url_for(self)
×
215
  end
216

217
  def files_location_upload?
2✔
218
    files_location.blank? || files_location == "file_upload"
380✔
219
  end
220

221
  def files_location_cluster?
2✔
222
    files_location == "file_cluster"
4✔
223
  end
224

225
  def files_location_other?
2✔
226
    files_location == "file_other"
34✔
227
  end
228

229
  def change_curator(curator_user_id, current_user)
2✔
230
    if curator_user_id == "no-one"
12✔
231
      clear_curator(current_user)
2✔
232
    else
233
      update_curator(curator_user_id, current_user)
10✔
234
    end
235
  end
236

237
  def clear_curator(current_user)
2✔
238
    # Update the curator on the Work
239
    self.curator_user_id = nil
4✔
240
    save!
4✔
241

242
    # ...and log the activity
243
    WorkActivity.add_work_activity(id, "Unassigned existing curator", current_user.id, activity_type: WorkActivity::SYSTEM)
4✔
244
  end
245

246
  def update_curator(curator_user_id, current_user)
2✔
247
    # Update the curator on the Work
248
    self.curator_user_id = curator_user_id
12✔
249
    save!
12✔
250

251
    # ...and log the activity
252
    new_curator = User.find(curator_user_id)
10✔
253

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

256
    # Troubleshooting https://github.com/pulibrary/pdc_describe/issues/1783
257
    if work_url.include?("/describe/describe/")
10✔
258
      Rails.logger.error("URL #{work_url} included /describe/describe/ and was fixed. See https://github.com/pulibrary/pdc_describe/issues/1783")
×
259
      work_url = work_url.gsub("/describe/describe/", "/describe/")
×
260
    end
261

262
    message = if curator_user_id.to_i == current_user.id
10✔
263
                "Self-assigned @#{current_user.uid} as curator for work #{work_url}"
4✔
264
              else
265
                "Set curator to @#{new_curator.uid} for work #{work_url}"
6✔
266
              end
267
    WorkActivity.add_work_activity(id, message, current_user.id, activity_type: WorkActivity::SYSTEM)
10✔
268
  end
269

270
  def add_message(message, current_user_id)
2✔
271
    WorkActivity.add_work_activity(id, message, current_user_id, activity_type: WorkActivity::MESSAGE)
22✔
272
  end
273

274
  def add_provenance_note(date, note, current_user_id, change_label = "")
2✔
275
    WorkActivity.add_work_activity(id, { note:, change_label: }.to_json, current_user_id, activity_type: WorkActivity::PROVENANCE_NOTES, created_at: date)
58✔
276
  end
277

278
  def log_changes(resource_compare, current_user_id)
2✔
279
    return if resource_compare.identical?
142✔
280
    WorkActivity.add_work_activity(id, resource_compare.differences.to_json, current_user_id, activity_type: WorkActivity::CHANGES)
112✔
281
  end
282

283
  def log_file_changes(current_user_id)
2✔
284
    return if changes.count == 0
22✔
285
    WorkActivity.add_work_activity(id, changes.to_json, current_user_id, activity_type: WorkActivity::FILE_CHANGES)
22✔
286
  end
287

288
  def activities
2✔
289
    WorkActivity.activities_for_work(id, WorkActivity::MESSAGE_ACTIVITY_TYPES + WorkActivity::CHANGE_LOG_ACTIVITY_TYPES)
10✔
290
  end
291

292
  def new_notification_count_for_user(user_id)
2✔
293
    WorkActivityNotification.joins(:work_activity)
148✔
294
                            .where(user_id:, read_at: nil)
295
                            .where(work_activity: { work_id: id })
296
                            .count
297
  end
298

299
  # Marks as read the notifications for the given user_id in this work.
300
  # In practice, the user_id is the id of the current user and therefore this method marks the current's user
301
  # notifications as read.
302
  def mark_new_notifications_as_read(user_id)
2✔
303
    # Notice that we fetch and update the information in batches
304
    # so that we don't issue individual SQL SELECT + SQL UPDATE
305
    # for each notification.
306
    #
307
    # Rails batching information:
308
    #   https://guides.rubyonrails.org/active_record_querying.html
309
    #   https://api.rubyonrails.org/classes/ActiveRecord/Batches.html
310

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

318
  def current_transition
2✔
319
    aasm.current_event.to_s.humanize.delete("!")
32✔
320
  end
321

322
  # Retrieve the S3 file uploads associated with the Work
323
  # @return [Array<S3File>]
324
  def uploads
2✔
325
    return post_curation_uploads if approved?
430✔
326

327
    pre_curation_uploads
410✔
328
  end
329

330
  # Retrieve the S3 file uploads named "README"
331
  # @return [Array<S3File>]
332
  def readme_uploads
2✔
333
    uploads.select { |s3_file| s3_file.filename.include?("README") }
×
334
  end
335

336
  # Retrieve the S3 file uploads which are research artifacts proper (not README or other files providing metadata/documentation)
337
  # @return [Array<S3File>]
338
  def artifact_uploads
2✔
339
    uploads.reject { |s3_file| s3_file.filename.include?("README") }
×
340
  end
341

342
  # Returns the list of files for the work with some basic information about each of them.
343
  # This method is much faster than `uploads` because it does not return the actual S3File
344
  # objects to the client, instead it returns just a few selected data elements.
345
  # rubocop:disable Metrics/MethodLength
346
  def file_list
2✔
347
    start = Time.zone.now
316✔
348
    s3_files = approved? ? post_curation_uploads : pre_curation_uploads
316✔
349
    files_info = s3_files.map do |s3_file|
316✔
350
      {
351
        "safe_id": s3_file.safe_id,
228✔
352
        "filename": s3_file.filename,
353
        "filename_display": s3_file.filename_display,
354
        "last_modified": s3_file.last_modified,
355
        "last_modified_display": s3_file.last_modified_display,
356
        "size": s3_file.size,
357
        "display_size": s3_file.display_size,
358
        "url": s3_file.url,
359
        "is_folder": s3_file.is_folder
360
      }
361
    end
362
    log_performance(start, "file_list called for #{id}")
316✔
363
    files_info
316✔
364
  end
365
  # rubocop:enable Metrics/MethodLength
366

367
  def total_file_size
2✔
368
    total_size = 0
32✔
369
    file_list.each do |file|
32✔
370
      total_size += file[:size]
×
371
    end
372
    total_size
32✔
373
  end
374

375
  # Calculates the total file size from a given list of files
376
  # This is so that we don't fetch the list twice from AWS since it can be expensive when
377
  # there are thousands of files on the work.
378
  def total_file_size_from_list(files)
2✔
379
    files.sum { |file| file[:size] }
512✔
380
  end
381

382
  # Fetches the data from S3 directly bypassing ActiveStorage
383
  def pre_curation_uploads
2✔
384
    s3_query_service.client_s3_files.sort_by(&:filename)
1,082✔
385
  end
386

387
  # Accesses post-curation S3 Bucket Objects
388
  def post_curation_s3_resources
2✔
389
    if approved?
102✔
390
      s3_resources
82✔
391
    else
392
      []
20✔
393
    end
394
  end
395

396
  # Returns the files in post-curation for the work
397
  def post_curation_uploads(force_post_curation: false)
2✔
398
    if force_post_curation
112✔
399
      # Always use the post-curation data regardless of the work's status
400
      post_curation_s3_query_service = S3QueryService.new(self, "postcuration")
10✔
401
      post_curation_s3_query_service.data_profile.fetch(:objects, [])
10✔
402
    else
403
      # Return the list based of files honoring the work status
404
      post_curation_s3_resources
102✔
405
    end
406
  end
407

408
  def s3_files
2✔
409
    pre_curation_uploads
×
410
  end
411

412
  def s3_client
2✔
413
    s3_query_service.client
44✔
414
  end
415

416
  delegate :bucket_name, :prefix, to: :s3_query_service
2✔
417
  delegate :doi_attribute_url, :curator_or_current_uid, to: :datacite_service
2✔
418

419
  # Generates the S3 Object key
420
  # @return [String]
421
  def s3_object_key
2✔
422
    "#{doi}/#{id}"
120✔
423
  end
424

425
  # Transmit a HEAD request for the S3 Bucket directory for this Work
426
  # @param bucket_name location to be checked to be found
427
  # @return [Aws::S3::Types::HeadObjectOutput]
428
  def find_post_curation_s3_dir(bucket_name:)
2✔
429
    # TODO: Directories really do not exists in S3
430
    #      if we really need this check then we need to do something else to check the bucket
431
    s3_client.head_object({
44✔
432
                            bucket: bucket_name,
433
                            key: s3_object_key
434
                          })
435
    true
2✔
436
  rescue Aws::S3::Errors::NotFound
437
    nil
42✔
438
  end
439

440
  # Generates the JSON serialized expression of the Work
441
  # @param args [Array<Hash>]
442
  # @option args [Boolean] :force_post_curation Force the request of AWS S3
443
  #   Resources, clearing the in-memory cache
444
  # @return [String]
445
  def as_json(*args)
2✔
446
    files = files_as_json(*args)
36✔
447

448
    # to_json returns a string of serialized JSON.
449
    # as_json returns the corresponding hash.
450
    {
451
      "resource" => resource.as_json,
36✔
452
      "files" => files,
453
      "group" => group.as_json.except("id"),
454
      "embargo_date" => embargo_date_as_json,
455
      "created_at" => format_date_for_solr(created_at),
456
      "updated_at" => format_date_for_solr(updated_at)
457
    }
458
  end
459

460
  # Format the date for Apache Solr
461
  # @param date [ActiveSupport::TimeWithZone]
462
  # @return [String]
463
  def format_date_for_solr(date)
2✔
464
    date.strftime("%Y-%m-%dT%H:%M:%SZ")
72✔
465
  end
466

467
  def pre_curation_uploads_count
2✔
468
    s3_query_service.file_count
4✔
469
  end
470

471
  delegate :ark, :doi, :resource_type, :resource_type=, :resource_type_general, :resource_type_general=,
2✔
472
           :to_xml, to: :resource
473

474
  # S3QueryService object associated with this Work
475
  # @return [S3QueryService]
476
  def s3_query_service
2✔
477
    mode = approved? ? "postcuration" : "precuration"
2,036✔
478
    @s3_query_service ||= S3QueryService.new(self, mode)
2,036✔
479
  end
480

481
  def past_snapshots
2✔
UNCOV
482
    UploadSnapshot.where(work: self)
×
483
  end
484

485
  # Build or find persisted UploadSnapshot models for this Work
486
  # @param [integer] user_id optional user to assign the snapshot to
487
  # @return [UploadSnapshot]
488
  def reload_snapshots(user_id: nil)
2✔
489
    work_changes = []
76✔
490
    s3_files = pre_curation_uploads
76✔
491
    s3_filenames = s3_files.map(&:filename)
76✔
492

493
    upload_snapshot = latest_snapshot
76✔
494

495
    upload_snapshot.snapshot_deletions(work_changes, s3_filenames)
76✔
496

497
    upload_snapshot.snapshot_modifications(work_changes, s3_files)
76✔
498

499
    # Create WorkActivity models with the set of changes
500
    unless work_changes.empty?
76✔
501
      new_snapshot = UploadSnapshot.new(work: self, url: s3_query_service.prefix)
66✔
502
      new_snapshot.store_files(s3_files)
66✔
503
      new_snapshot.save!
66✔
504
      WorkActivity.add_work_activity(id, work_changes.to_json, user_id, activity_type: WorkActivity::FILE_CHANGES)
66✔
505
    end
506
  end
507

508
  def self.presenter_class
2✔
509
    WorkPresenter
240✔
510
  end
511

512
  def presenter
2✔
513
    self.class.presenter_class.new(work: self)
240✔
514
  end
515

516
  def changes
2✔
517
    @changes ||= []
94✔
518
  end
519

520
  def track_change(action, filename)
2✔
521
    changes << { action:, filename: }
50✔
522
  end
523

524
  # rubocop:disable Naming/PredicateName
525
  def has_rights?(rights_id)
2✔
526
    resource.rights_many.index { |rights| rights.identifier == rights_id } != nil
2,920✔
527
  end
528
  # rubocop:enable Naming/PredicateName
529

530
  # This is the solr id / work show page in PDC Discovery
531
  def pdc_discovery_url
2✔
532
    "https://datacommons.princeton.edu/discovery/catalog/doi-#{doi.tr('/', '-').tr('.', '-')}"
354✔
533
  end
534

535
  # Determine whether or not the Work is under active embargo
536
  # @return [Boolean]
537
  def embargoed?
2✔
538
    return false if embargo_date.blank?
424✔
539

540
    current_date = Time.zone.now
14✔
541
    embargo_date >= current_date
14✔
542
  end
543

544
  protected
2✔
545

546
    def work_validator
2✔
547
      @work_validator ||= WorkValidator.new(self)
2,762✔
548
    end
549

550
    # This must be protected, NOT private for ActiveRecord to work properly with this attribute.
551
    #   Protected will still keep others from setting the metatdata, but allows ActiveRecord the access it needs
552
    def metadata=(metadata)
2✔
553
      super
4,422✔
554
      @resource = PDCMetadata::Resource.new_from_jsonb(metadata)
4,422✔
555
    end
556

557
  private
2✔
558

559
    def publish(user)
2✔
560
      datacite_service.publish_doi(user)
56✔
561
      update_ark_information
56✔
562
      publish_precurated_files(user)
56✔
563
      save!
54✔
564
    end
565

566
    # Update EZID (our provider of ARKs) with the new information for this work.
567
    def update_ark_information
2✔
568
      # We only want to update the ark url under certain conditions.
569
      # Set this value in config/update_ark_url.yml
570
      if Rails.configuration.update_ark_url
56✔
571
        if ark.present?
10✔
572
          Ark.update(ark, datacite_service.doi_attribute_url)
6✔
573
        end
574
      end
575
    end
576

577
    def track_state_change(user, state = aasm.to_state)
2✔
578
      uw = UserWork.new(user_id: user.id, work_id: id, state:)
268✔
579
      uw.save!
268✔
580
      WorkActivity.add_work_activity(id, "marked as #{state.to_s.titleize}", user.id, activity_type: WorkActivity::SYSTEM)
268✔
581
      WorkStateTransitionNotification.new(self, user.id).send
268✔
582
    end
583

584
    # Request S3 Bucket Objects associated with this Work
585
    # @return [Array<S3File>]
586
    def s3_resources
2✔
587
      data_profile = s3_query_service.data_profile
82✔
588
      data_profile.fetch(:objects, [])
82✔
589
    end
590
    alias pre_curation_s3_resources s3_resources
2✔
591

592
    def s3_object_persisted?(s3_file)
2✔
UNCOV
593
      uploads_keys = uploads.map(&:key)
×
UNCOV
594
      uploads_keys.include?(s3_file.key)
×
595
    end
596

597
    def publish_precurated_files(user)
2✔
598
      # We need to explicitly check the to post-curation bucket here.
599
      s3_post_curation_query_service = S3QueryService.new(self, "postcuration")
44✔
600

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

604
      # Copy the pre-curation S3 Objects to the post-curation S3 Bucket...
605
      s3_query_service.publish_files(user)
42✔
606
    end
607

608
    def latest_snapshot
2✔
609
      return upload_snapshots.first unless upload_snapshots.empty?
76✔
610

611
      UploadSnapshot.new(work: self, files: [])
40✔
612
    end
613

614
    def datacite_service
2✔
615
      @datacite_service ||= PULDatacite.new(self)
114✔
616
    end
617

618
    def files_as_json(*args)
2✔
619
      return [] if embargoed?
36✔
620

621
      force_post_curation = args.any? { |arg| arg[:force_post_curation] == true }
40✔
622

623
      # Pre-curation files are not accessible externally,
624
      # so we are not interested in listing them in JSON.
625
      post_curation_uploads(force_post_curation:).map do |upload|
30✔
626
        {
627
          "filename": upload.filename,
20✔
628
          "size": upload.size,
629
          "display_size": upload.display_size,
630
          "url": upload.globus_url
631
        }
632
      end
633
    end
634

635
    def embargo_date_as_json
2✔
636
      if embargo_date.present?
36✔
637
        embargo_datetime = embargo_date.to_datetime
10✔
638
        embargo_date_iso8601 = embargo_datetime.iso8601
10✔
639
        # Apache Solr timestamps require the following format:
640
        # 1972-05-20T17:33:18Z
641
        # https://solr.apache.org/guide/solr/latest/indexing-guide/date-formatting-math.html
642
        embargo_date_iso8601.gsub(/\+.+$/, "Z")
10✔
643
      end
644
    end
645

646
    def log_performance(start, message)
2✔
647
      elapsed = Time.zone.now - start
316✔
648
      if elapsed > 20
316✔
UNCOV
649
        Rails.logger.warn("PERFORMANCE: #{message}. Elapsed: #{elapsed} seconds")
×
650
      else
651
        Rails.logger.info("PERFORMANCE: #{message}. Elapsed: #{elapsed} seconds")
316✔
652
      end
653
    end
654
end
655
# 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