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

pulibrary / pdc_describe / 6e0486a6-b0d3-473d-bdb0-ec772992e9da

10 Apr 2025 07:46PM UTC coverage: 95.367% (-0.03%) from 95.399%
6e0486a6-b0d3-473d-bdb0-ec772992e9da

Pull #2094

circleci

hectorcorrea
Fixed test

Co-authored-by: Robert-Anthony Lee-Faison <leefaisonr@users.noreply.github.com>
Pull Request #2094: Move files to embargo bucket for approved embargoed works

22 of 24 new or added lines in 3 files covered. (91.67%)

28 existing lines in 3 files now uncovered.

3479 of 3648 relevant lines covered (95.37%)

398.81 hits per line

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

87.76
/app/controllers/works_controller.rb
1
# frozen_string_literal: true
2

3
require "nokogiri"
2✔
4
require "open-uri"
2✔
5

6
# Currently this controller supports Multiple ways to create a work, wizard mode, create dataset, and migrate
7
# The goal is to eventually break some of these workflows into separate contorllers.
8
# For the moment I'm documenting which methods get called by each workflow below.
9
# Note: new, edit and update get called by both the migrate and Non wizard workflows
10
#
11
# Normal mode
12
#  new & file_list -> create -> show & file_list
13
#
14
#  Clicking Edit puts you in wizard mode for some reason :(
15
#
16
# migrate
17
#
18
#  new & file_list -> create -> show & file_list
19
#
20
#  Clicking edit
21
#   edit & file_list -> update -> show & file_list
22
#
23

24
# rubocop:disable Metrics/ClassLength
25
class WorksController < ApplicationController
2✔
26
  include ERB::Util
2✔
27
  around_action :rescue_aasm_error, only: [:approve, :withdraw, :resubmit, :validate, :create]
2✔
28

29
  skip_before_action :authenticate_user!
2✔
30
  before_action :authenticate_user!, unless: :public_request?
2✔
31

32
  def index
2✔
33
    if rss_index_request?
12✔
34
      rss_index
4✔
35
    elsif current_user.super_admin?
8✔
36
      @works = Work.all
4✔
37
      respond_to do |format|
4✔
38
        format.html
4✔
39
      end
40
    else
41
      flash[:notice] = "You do not have access to this page."
4✔
42
      redirect_to root_path
4✔
43
    end
44
  end
45

46
  # only non wizard mode
47
  def new
2✔
48
    group = Group.find_by(code: params[:group_code]) || current_user.default_group
16✔
49
    @work = Work.new(created_by_user_id: current_user.id, group:)
16✔
50
    @work_decorator = WorkDecorator.new(@work, current_user)
16✔
51
    @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
16✔
52
  end
53

54
  # only non wizard mode
55
  def create
2✔
56
    @work = Work.new(created_by_user_id: current_user.id, group_id: params_group_id, user_entered_doi: params["doi"].present?)
26✔
57
    @work.resource = FormToResourceService.convert(params, @work)
26✔
58
    @work.resource.migrated = migrated?
26✔
59
    if @work.valid?
26✔
60
      @work.draft!(current_user)
18✔
61
      upload_service = WorkUploadsEditService.new(@work, current_user)
16✔
62
      upload_service.update_precurated_file_list(added_files_param, deleted_files_param)
16✔
63
      redirect_to work_url(@work), notice: "Work was successfully created."
16✔
64
    else
65
      @work_decorator = WorkDecorator.new(@work, current_user)
8✔
66
      @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
8✔
67
      # return 200 so the loadbalancer doesn't capture the error
68
      render :new
8✔
69
    end
70
  end
71

72
  ##
73
  # Show the information for the dataset with the given id
74
  # When requested as .json, return the internal json resource
75
  def show
2✔
76
    @work = Work.find(params[:id])
208✔
77
    UpdateSnapshotJob.perform_later(work_id: @work.id, last_snapshot_id: work.upload_snapshots.first&.id)
206✔
78
    @work_decorator = WorkDecorator.new(@work, current_user)
206✔
79

80
    respond_to do |format|
206✔
81
      format.html do
206✔
82
        # Ensure that the Work belongs to a Group
83
        group = @work_decorator.group
200✔
84
        raise(Work::InvalidGroupError, "The Work #{@work.id} does not belong to any Group") unless group
200✔
85

86
        @can_curate = current_user.can_admin?(group)
198✔
87
        @work.mark_new_notifications_as_read(current_user.id)
198✔
88
      end
89
      format.json { render json: @work.to_json }
212✔
90
    end
91
  end
92

93
  # only non wizard mode
94
  def file_list
2✔
95
    if params[:id] == "NONE"
300✔
96
      # This is a special case when we render the file list for a work being created
97
      # (i.e. it does not have an id just yet)
98
      render json: file_list_ajax_response(nil)
22✔
99
    else
100
      work = Work.find(params[:id])
278✔
101
      render json: file_list_ajax_response(work)
278✔
102
    end
103
  end
104

105
  def resolve_doi
2✔
106
    @work = Work.find_by_doi(params[:doi])
6✔
107
    redirect_to @work
4✔
108
  end
109

110
  def resolve_ark
2✔
111
    @work = Work.find_by_ark(params[:ark])
6✔
112
    redirect_to @work
4✔
113
  end
114

115
  # GET /works/1/edit
116
  # only non wizard mode
117
  def edit
2✔
118
    @work = Work.find(params[:id])
110✔
119
    @work_decorator = WorkDecorator.new(@work, current_user)
110✔
120
    if validate_modification_permissions(work: @work,
110✔
121
                                         uneditable_message: "Can not update work: #{@work.id} is not editable by #{current_user.uid}",
122
                                         current_state_message: "Can not update work: #{@work.id} is not editable in current state by #{current_user.uid}")
123
      @uploads = @work.uploads
100✔
124
      @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
100✔
125
    end
126
  end
127

128
  # PATCH /works/1
129
  # only non wizard mode
130
  def update
2✔
131
    @work = Work.find(params[:id])
104✔
132
    if validate_modification_permissions(work: @work, uneditable_message: "Can not update work: #{@work.id} is not editable by #{current_user.uid}",
104✔
133
                                         current_state_message: "Can not update work: #{@work.id} is not editable in current state by #{current_user.uid}")
134
      update_work
100✔
135
    end
136
  end
137

138
  def approve
2✔
139
    @work = Work.find(params[:id])
28✔
140
    @work.approve!(current_user)
28✔
141
    target_bucket = if @work.embargoed?
12✔
NEW
UNCOV
142
                      PULS3Client::EMBARGO
×
143
                    else
144
                      PULS3Client::POSTCURATION
12✔
145
                    end
146
    flash[:notice] = "Your files are being moved to the #{target_bucket} bucket in the background. Depending on the file sizes this may take some time."
12✔
147
    redirect_to work_path(@work)
12✔
148
  end
149

150
  def withdraw
2✔
151
    @work = Work.find(params[:id])
4✔
152
    @work.withdraw!(current_user)
4✔
153
    redirect_to work_path(@work)
2✔
154
  end
155

156
  def resubmit
2✔
157
    @work = Work.find(params[:id])
4✔
158
    @work.resubmit!(current_user)
4✔
159
    redirect_to work_path(@work)
2✔
160
  end
161

162
  def revert_to_draft
2✔
163
    @work = Work.find(params[:id])
6✔
164
    @work.revert_to_draft!(current_user)
6✔
165

166
    redirect_to work_path(@work)
6✔
167
  end
168

169
  def assign_curator
2✔
170
    work = Work.find(params[:id])
10✔
171
    work.change_curator(params[:uid], current_user)
10✔
172
    if work.errors.count > 0
8✔
173
      render json: { errors: work.errors.map(&:type) }, status: :bad_request
2✔
174
    else
175
      render json: {}
6✔
176
    end
177
  rescue => ex
178
    # This is necessary for JSON responses
179
    Rails.logger.error("Error changing curator for work: #{work.id}. Exception: #{ex.message}")
2✔
180
    Honeybadger.notify("Error changing curator for work: #{work.id}. Exception: #{ex.message}")
2✔
181
    render(json: { errors: ["Cannot save dataset"] }, status: :bad_request)
2✔
182
  end
183

184
  def add_message
2✔
185
    work = Work.find(params[:id])
12✔
186
    if params["new-message"].present?
12✔
187
      new_message_param = params["new-message"]
12✔
188
      sanitized_new_message = html_escape(new_message_param)
12✔
189

190
      work.add_message(sanitized_new_message, current_user.id)
12✔
191
    end
192
    redirect_to work_path(id: params[:id])
12✔
193
  end
194

195
  def add_provenance_note
2✔
196
    work = Work.find(params[:id])
4✔
197
    if params["new-provenance-note"].present?
4✔
198
      new_date = params["new-provenance-date"]
4✔
199
      new_label = params["change_label"]
4✔
200
      new_note = html_escape(params["new-provenance-note"])
4✔
201

202
      work.add_provenance_note(new_date, new_note, current_user.id, new_label)
4✔
203
    end
204
    redirect_to work_path(id: params[:id])
4✔
205
  end
206

207
  # Outputs the Datacite XML representation of the work
208
  def datacite
2✔
209
    work = Work.find(params[:id])
4✔
210
    render xml: work.to_xml
4✔
211
  end
212

213
  def datacite_validate
2✔
214
    @errors = []
6✔
215
    @work = Work.find(params[:id])
6✔
216
    validator = WorkValidator.new(@work)
6✔
217
    unless validator.valid_datacite?
6✔
218
      @errors = @work.errors.full_messages
4✔
219
    end
220
  end
221

222
  def migrating?
2✔
223
    return @work.resource.migrated if @work&.resource && !params.key?(:migrate)
48✔
224

225
    params[:migrate]
12✔
226
  end
227
  helper_method :migrating?
2✔
228

229
  # Returns the raw BibTex citation information
230
  def bibtex
2✔
231
    work = Work.find(params[:id])
2✔
232
    creators = work.resource.creators.map { |creator| "#{creator.family_name}, #{creator.given_name}" }
30✔
233
    citation = DatasetCitation.new(creators, [work.resource.publication_year], work.resource.titles.first.title, work.resource.resource_type, work.resource.publisher, work.resource.doi)
2✔
234
    bibtex = citation.bibtex
2✔
235
    send_data bibtex, filename: "#{citation.bibtex_id}.bibtex", type: "text/plain", disposition: "attachment"
2✔
236
  end
237

238
  # POST /works/1/upload-files (called via Uppy)
239
  def upload_files
2✔
UNCOV
240
    @work = Work.find(params[:id])
×
UNCOV
241
    upload_service = WorkUploadsEditService.new(@work, current_user)
×
UNCOV
242
    upload_service.update_precurated_file_list(params["files"], [])
×
UNCOV
243
    render plain: params["files"].map(&:original_filename).join(",")
×
244
  end
245

246
  # Validates that the work is ready to be approved
247
  # GET /works/1/validate
248
  def validate
2✔
249
    @work = Work.find(params[:id])
16✔
250
    @work.complete_submission!(current_user)
16✔
251
    redirect_to user_path(current_user)
10✔
252
  end
253

254
  private
2✔
255

256
    # Extract the Work ID parameter
257
    # @return [String]
258
    def work_id_param
2✔
259
      params[:id]
214✔
260
    end
261

262
    # Find the Work requested by ID
263
    # @return [Work]
264
    def work
2✔
265
      Work.find(work_id_param)
214✔
266
    end
267

268
    # Determine whether or not the request is for the :index action in the RSS
269
    # response format
270
    # This is to enable PDC Discovery to index approved content via the RSS feed
271
    def rss_index_request?
2✔
272
      action_name == "index" && request.format.symbol == :rss
920✔
273
    end
274

275
    # Determine whether or not the request is for the :show action in the JSON
276
    # response format
277
    # @return [Boolean]
278
    def json_show_request?
2✔
279
      action_name == "show" && request.format.symbol == :json
904✔
280
    end
281

282
    # Determine whether or not the requested Work has been approved
283
    # @return [Boolean]
284
    def work_approved?
2✔
285
      work&.state == "approved"
8✔
286
    end
287

288
    ##
289
    # Public requests are requests that do not require authentication.
290
    # This is to enable PDC Discovery to index approved content via the RSS feed
291
    # and .json calls to individual works without needing to log in as a user.
292
    # Note that only approved works can be fetched for indexing.
293
    def public_request?
2✔
294
      return true if rss_index_request?
908✔
295
      return true if json_show_request? && work_approved?
904✔
296
      false
900✔
297
    end
298

299
    def work_params
2✔
300
      params[:work] || {}
240✔
301
    end
302

303
    # @note No testing coverage but not a route, not called
304
    def patch_params
2✔
UNCOV
305
      return {} unless params.key?(:patch)
×
306

307
      params[:patch]
×
308
    end
309

310
    # @note No testing coverage but not a route, not called
311
    def pre_curation_uploads_param
2✔
UNCOV
312
      return if patch_params.nil?
×
313

314
      patch_params[:pre_curation_uploads]
×
315
    end
316

317
    # @note No testing coverage but not a route, not called
318
    def readme_file_param
2✔
UNCOV
319
      return if patch_params.nil?
×
320

UNCOV
321
      patch_params[:readme_file]
×
322
    end
323

324
    # @note No testing coverage but not a route, not called
325
    def rescue_aasm_error
2✔
326
      super
78✔
327
    rescue StandardError => generic_error
328
      if action_name == "create"
2✔
329
        handle_error_for_create(generic_error)
2✔
330
      else
UNCOV
331
        redirect_to error_url, notice: "We apologize, an error was encountered: #{generic_error.message}. Please contact the PDC Describe administrators."
×
332
      end
333
    end
334

335
    rescue_from StandardError do |generic_error|
2✔
336
      Honeybadger.notify("We apologize, an error was encountered: #{generic_error.message}.")
8✔
337
      redirect_to error_url, notice: "We apologize, an error was encountered: #{generic_error.message}. Please contact the PDC Describe administrators."
8✔
338
    end
339

340
    # @note No testing coverage but not a route, not called
341
    def handle_error_for_create(generic_error)
2✔
342
      if @work.persisted?
2✔
UNCOV
343
        Honeybadger.notify("Failed to create the new Dataset #{@work.id}: #{generic_error.message}")
×
UNCOV
344
        @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
UNCOV
345
        redirect_to edit_work_url(id: @work.id), notice: "Failed to create the new Dataset #{@work.id}: #{generic_error.message}", params:
×
346
      else
347
        Honeybadger.notify("Failed to create a new Dataset #{@work.id}: #{generic_error.message}")
2✔
348
        new_params = {}
2✔
349
        new_params[:wizard] = wizard_mode? if wizard_mode?
2✔
350
        new_params[:migrate] = migrating? if migrating?
2✔
351
        @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
2✔
352
        redirect_to new_work_url(params: new_params), notice: "Failed to create a new Dataset: #{generic_error.message}", params: new_params
2✔
353
      end
354
    end
355

356
    # @note No testing coverage but not a route, not called
357
    def redirect_aasm_error(transition_error_message)
2✔
358
      if @work.persisted?
26✔
359
        redirect_to edit_work_url(id: @work.id), notice: transition_error_message, params:
26✔
360
      else
UNCOV
361
        new_params = {}
×
UNCOV
362
        new_params[:wizard] = wizard_mode? if wizard_mode?
×
UNCOV
363
        new_params[:migrate] = migrating? if migrating?
×
UNCOV
364
        @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
UNCOV
365
        redirect_to new_work_url(params: new_params), notice: transition_error_message, params: new_params
×
366
      end
367
    end
368

369
    # @note No testing coverage but not a route, not called
370
    def error_action
2✔
371
      @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
372
      if action_name == "create"
×
UNCOV
373
        :new
×
374
      elsif action_name == "validate"
×
375
        :edit
×
UNCOV
376
      elsif action_name == "new_submission"
×
UNCOV
377
        :new_submission
×
378
      else
UNCOV
379
        @work_decorator = WorkDecorator.new(@work, current_user)
×
UNCOV
380
        :show
×
381
      end
382
    end
383

384
    def wizard_mode?
2✔
385
      params[:wizard] == "true"
2✔
386
    end
387

388
    def update_work
2✔
389
      check_for_stale_update(@work, params)
100✔
390
      upload_service = WorkUploadsEditService.new(@work, current_user)
100✔
391
      if @work.approved?
100✔
392
        upload_keys = deleted_files_param || []
10✔
393
        deleted_uploads = upload_service.find_post_curation_uploads(upload_keys:)
10✔
394

395
        return head(:forbidden) unless deleted_uploads.empty?
10✔
396
      else
397
        @work = upload_service.update_precurated_file_list(added_files_param, deleted_files_param)
90✔
398
      end
399

400
      process_updates
98✔
401
    end
402

403
    def added_files_param
2✔
404
      Array(work_params[:pre_curation_uploads_added])
106✔
405
    end
406

407
    def deleted_files_param
2✔
408
      deleted_count = (work_params["deleted_files_count"] || "0").to_i
116✔
409
      (1..deleted_count).map { |i| work_params["deleted_file_#{i}"] }.select(&:present?)
134✔
410
    end
411

412
    def process_updates
2✔
413
      if WorkCompareService.update_work(work: @work, update_params:, current_user:)
98✔
414
        redirect_to work_url(@work), notice: "Work was successfully updated."
92✔
415
      else
416
        # This is needed for rendering HTML views with validation errors
417
        @uploads = @work.uploads
6✔
418
        @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
6✔
419
        @work_decorator = WorkDecorator.new(@work, current_user)
6✔
420

421
        # return 200 so the loadbalancer doesn't capture the error
422
        render :edit
6✔
423
      end
424
    end
425

426
    def migrated?
2✔
427
      return false unless params.key?(:submit)
26✔
428

429
      params[:submit] == "Migrate"
18✔
430
    end
431

432
    # Returns a hash object that can be serialized into something that DataTables
433
    # can consume. The `data` elements includes the work's file list all other
434
    # properties are used for displaying different data elements related but not
435
    # directly on the DataTable object (e.g. the total file size)
436
    def file_list_ajax_response(work)
2✔
437
      files = []
300✔
438
      total_size = 0
300✔
439
      unless work.nil?
300✔
440
        files = work.file_list
278✔
441
        total_size = work.total_file_size_from_list(files)
278✔
442
      end
443
      {
444
        data: files,
300✔
445
        total_size:,
446
        total_size_display: ActiveSupport::NumberHelper.number_to_human_size(total_size),
447
        total_file_count: files.count
448
      }
449
    end
450

451
    def rss_index
2✔
452
      # Only include approved works in the RSS feed
453
      @approved_works = Work.all.select(&:approved?)
4✔
454
      respond_to do |format|
4✔
455
        format.rss { render layout: false }
8✔
456
      end
457
    end
458
end
459
# 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