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

pulibrary / pdc_describe / 7a0688c8-6a53-4d3c-9bea-20dad3bdcbaa

18 Mar 2025 05:24PM UTC coverage: 96.065% (-0.07%) from 96.13%
7a0688c8-6a53-4d3c-9bea-20dad3bdcbaa

Pull #2052

circleci

hectorcorrea
Fixed the work edit page
Pull Request #2052: Handle File Upload errors when the Load Balancer blocks the request

0 of 2 new or added lines in 2 files covered. (0.0%)

23 existing lines in 1 file now uncovered.

3345 of 3482 relevant lines covered (96.07%)

406.04 hits per line

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

87.56
/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
    @works = Work.all
8✔
34
    respond_to do |format|
8✔
35
      format.html
8✔
36
      format.rss { render layout: false }
12✔
37
    end
38
  end
39

40
  # only non wizard mode
41
  def new
2✔
42
    group = Group.find_by(code: params[:group_code]) || current_user.default_group
16✔
43
    @work = Work.new(created_by_user_id: current_user.id, group:)
16✔
44
    @work_decorator = WorkDecorator.new(@work, current_user)
16✔
45
    @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
16✔
46
  end
47

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

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

74
    respond_to do |format|
206✔
75
      format.html do
206✔
76
        # Ensure that the Work belongs to a Group
77
        group = @work_decorator.group
200✔
78
        raise(Work::InvalidGroupError, "The Work #{@work.id} does not belong to any Group") unless group
200✔
79

80
        @can_curate = current_user.can_admin?(group)
198✔
81
        @work.mark_new_notifications_as_read(current_user.id)
198✔
82
      end
83
      format.json { render json: @work.to_json }
212✔
84
    end
85
  end
86

87
  # only non wizard mode
88
  def file_list
2✔
89
    if params[:id] == "NONE"
306✔
90
      # This is a special case when we render the file list for a work being created
91
      # (i.e. it does not have an id just yet)
92
      render json: file_list_ajax_response(nil)
22✔
93
    else
94
      work = Work.find(params[:id])
284✔
95
      render json: file_list_ajax_response(work)
284✔
96
    end
97
  end
98

99
  def resolve_doi
2✔
100
    @work = Work.find_by_doi(params[:doi])
6✔
101
    redirect_to @work
4✔
102
  end
103

104
  def resolve_ark
2✔
105
    @work = Work.find_by_ark(params[:ark])
6✔
106
    redirect_to @work
4✔
107
  end
108

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

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

132
  def approve
2✔
133
    @work = Work.find(params[:id])
28✔
134
    @work.approve!(current_user)
28✔
135
    flash[:notice] = "Your files are being moved to the post-curation bucket in the background. Depending on the file sizes this may take some time."
12✔
136
    redirect_to work_path(@work)
12✔
137
  end
138

139
  def withdraw
2✔
140
    @work = Work.find(params[:id])
4✔
141
    @work.withdraw!(current_user)
4✔
142
    redirect_to work_path(@work)
2✔
143
  end
144

145
  def resubmit
2✔
146
    @work = Work.find(params[:id])
4✔
147
    @work.resubmit!(current_user)
4✔
148
    redirect_to work_path(@work)
2✔
149
  end
150

151
  def revert_to_draft
2✔
152
    @work = Work.find(params[:id])
6✔
153
    @work.revert_to_draft!(current_user)
6✔
154

155
    redirect_to work_path(@work)
6✔
156
  end
157

158
  def assign_curator
2✔
159
    work = Work.find(params[:id])
10✔
160
    work.change_curator(params[:uid], current_user)
10✔
161
    if work.errors.count > 0
8✔
162
      render json: { errors: work.errors.map(&:type) }, status: :bad_request
2✔
163
    else
164
      render json: {}
6✔
165
    end
166
  rescue => ex
167
    # This is necessary for JSON responses
168
    Rails.logger.error("Error changing curator for work: #{work.id}. Exception: #{ex.message}")
2✔
169
    Honeybadger.notify("Error changing curator for work: #{work.id}. Exception: #{ex.message}")
2✔
170
    render(json: { errors: ["Cannot save dataset"] }, status: :bad_request)
2✔
171
  end
172

173
  def add_message
2✔
174
    work = Work.find(params[:id])
12✔
175
    if params["new-message"].present?
12✔
176
      new_message_param = params["new-message"]
12✔
177
      sanitized_new_message = html_escape(new_message_param)
12✔
178

179
      work.add_message(sanitized_new_message, current_user.id)
12✔
180
    end
181
    redirect_to work_path(id: params[:id])
12✔
182
  end
183

184
  def add_provenance_note
2✔
185
    work = Work.find(params[:id])
4✔
186
    if params["new-provenance-note"].present?
4✔
187
      new_date = params["new-provenance-date"]
4✔
188
      new_label = params["change_label"]
4✔
189
      new_note = html_escape(params["new-provenance-note"])
4✔
190

191
      work.add_provenance_note(new_date, new_note, current_user.id, new_label)
4✔
192
    end
193
    redirect_to work_path(id: params[:id])
4✔
194
  end
195

196
  # Outputs the Datacite XML representation of the work
197
  def datacite
2✔
198
    work = Work.find(params[:id])
4✔
199
    render xml: work.to_xml
4✔
200
  end
201

202
  def datacite_validate
2✔
203
    @errors = []
6✔
204
    @work = Work.find(params[:id])
6✔
205
    validator = WorkValidator.new(@work)
6✔
206
    unless validator.valid_datacite?
6✔
207
      @errors = @work.errors.full_messages
4✔
208
    end
209
  end
210

211
  def migrating?
2✔
212
    return @work.resource.migrated if @work&.resource && !params.key?(:migrate)
48✔
213

214
    params[:migrate]
12✔
215
  end
216
  helper_method :migrating?
2✔
217

218
  # Returns the raw BibTex citation information
219
  def bibtex
2✔
220
    work = Work.find(params[:id])
2✔
221
    creators = work.resource.creators.map { |creator| "#{creator.family_name}, #{creator.given_name}" }
30✔
222
    citation = DatasetCitation.new(creators, [work.resource.publication_year], work.resource.titles.first.title, work.resource.resource_type, work.resource.publisher, work.resource.doi)
2✔
223
    bibtex = citation.bibtex
2✔
224
    send_data bibtex, filename: "#{citation.bibtex_id}.bibtex", type: "text/plain", disposition: "attachment"
2✔
225
  end
226

227
  # POST /works/1/upload-files (called via Uppy)
228
  def upload_files
2✔
UNCOV
229
    @work = Work.find(params[:id])
×
UNCOV
230
    upload_service = WorkUploadsEditService.new(@work, current_user)
×
UNCOV
231
    upload_service.update_precurated_file_list(params["files"], [])
×
NEW
232
    render plain: params["files"].map(&:original_filename).join(",")
×
233
  end
234

235
  # Validates that the work is ready to be approved
236
  # GET /works/1/validate
237
  def validate
2✔
238
    @work = Work.find(params[:id])
16✔
239
    @work.complete_submission!(current_user)
16✔
240
    redirect_to user_path(current_user)
10✔
241
  end
242

243
  private
2✔
244

245
    # Extract the Work ID parameter
246
    # @return [String]
247
    def work_id_param
2✔
248
      params[:id]
214✔
249
    end
250

251
    # Find the Work requested by ID
252
    # @return [Work]
253
    def work
2✔
254
      Work.find(work_id_param)
214✔
255
    end
256

257
    # Determine whether or not the request is for the :index action in the RSS
258
    # response format
259
    # This is to enable PDC Discovery to index approved content via the RSS feed
260
    def rss_index_request?
2✔
261
      action_name == "index" && request.format.symbol == :rss
902✔
262
    end
263

264
    # Determine whether or not the request is for the :show action in the JSON
265
    # response format
266
    # @return [Boolean]
267
    def json_show_request?
2✔
268
      action_name == "show" && request.format.symbol == :json
898✔
269
    end
270

271
    # Determine whether or not the requested Work has been approved
272
    # @return [Boolean]
273
    def work_approved?
2✔
274
      work&.state == "approved"
8✔
275
    end
276

277
    ##
278
    # Public requests are requests that do not require authentication.
279
    # This is to enable PDC Discovery to index approved content via the RSS feed
280
    # and .json calls to individual works without needing to log in as a user.
281
    # Note that only approved works can be fetched for indexing.
282
    def public_request?
2✔
283
      return true if rss_index_request?
902✔
284
      return true if json_show_request? && work_approved?
898✔
285
      false
894✔
286
    end
287

288
    def work_params
2✔
289
      params[:work] || {}
240✔
290
    end
291

292
    # @note No testing coverage but not a route, not called
293
    def patch_params
2✔
UNCOV
294
      return {} unless params.key?(:patch)
×
295

UNCOV
296
      params[:patch]
×
297
    end
298

299
    # @note No testing coverage but not a route, not called
300
    def pre_curation_uploads_param
2✔
UNCOV
301
      return if patch_params.nil?
×
302

UNCOV
303
      patch_params[:pre_curation_uploads]
×
304
    end
305

306
    # @note No testing coverage but not a route, not called
307
    def readme_file_param
2✔
UNCOV
308
      return if patch_params.nil?
×
309

UNCOV
310
      patch_params[:readme_file]
×
311
    end
312

313
    # @note No testing coverage but not a route, not called
314
    def rescue_aasm_error
2✔
315
      super
78✔
316
    rescue StandardError => generic_error
317
      if action_name == "create"
2✔
318
        handle_error_for_create(generic_error)
2✔
319
      else
UNCOV
320
        redirect_to error_url, notice: "We apologize, an error was encountered: #{generic_error.message}. Please contact the PDC Describe administrators."
×
321
      end
322
    end
323

324
    rescue_from StandardError do |generic_error|
2✔
325
      Honeybadger.notify("We apologize, an error was encountered: #{generic_error.message}.")
8✔
326
      redirect_to error_url, notice: "We apologize, an error was encountered: #{generic_error.message}. Please contact the PDC Describe administrators."
8✔
327
    end
328

329
    # @note No testing coverage but not a route, not called
330
    def handle_error_for_create(generic_error)
2✔
331
      if @work.persisted?
2✔
UNCOV
332
        Honeybadger.notify("Failed to create the new Dataset #{@work.id}: #{generic_error.message}")
×
UNCOV
333
        @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
UNCOV
334
        redirect_to edit_work_url(id: @work.id), notice: "Failed to create the new Dataset #{@work.id}: #{generic_error.message}", params:
×
335
      else
336
        Honeybadger.notify("Failed to create a new Dataset #{@work.id}: #{generic_error.message}")
2✔
337
        new_params = {}
2✔
338
        new_params[:wizard] = wizard_mode? if wizard_mode?
2✔
339
        new_params[:migrate] = migrating? if migrating?
2✔
340
        @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
2✔
341
        redirect_to new_work_url(params: new_params), notice: "Failed to create a new Dataset: #{generic_error.message}", params: new_params
2✔
342
      end
343
    end
344

345
    # @note No testing coverage but not a route, not called
346
    def redirect_aasm_error(transition_error_message)
2✔
347
      if @work.persisted?
26✔
348
        redirect_to edit_work_url(id: @work.id), notice: transition_error_message, params:
26✔
349
      else
UNCOV
350
        new_params = {}
×
UNCOV
351
        new_params[:wizard] = wizard_mode? if wizard_mode?
×
UNCOV
352
        new_params[:migrate] = migrating? if migrating?
×
UNCOV
353
        @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
UNCOV
354
        redirect_to new_work_url(params: new_params), notice: transition_error_message, params: new_params
×
355
      end
356
    end
357

358
    # @note No testing coverage but not a route, not called
359
    def error_action
2✔
360
      @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
UNCOV
361
      if action_name == "create"
×
UNCOV
362
        :new
×
UNCOV
363
      elsif action_name == "validate"
×
UNCOV
364
        :edit
×
UNCOV
365
      elsif action_name == "new_submission"
×
366
        :new_submission
×
367
      else
368
        @work_decorator = WorkDecorator.new(@work, current_user)
×
369
        :show
×
370
      end
371
    end
372

373
    def wizard_mode?
2✔
374
      params[:wizard] == "true"
2✔
375
    end
376

377
    def update_work
2✔
378
      upload_service = WorkUploadsEditService.new(@work, current_user)
100✔
379
      if @work.approved?
100✔
380
        upload_keys = deleted_files_param || []
10✔
381
        deleted_uploads = upload_service.find_post_curation_uploads(upload_keys:)
10✔
382

383
        return head(:forbidden) unless deleted_uploads.empty?
10✔
384
      else
385
        @work = upload_service.update_precurated_file_list(added_files_param, deleted_files_param)
90✔
386
      end
387

388
      process_updates
98✔
389
    end
390

391
    def added_files_param
2✔
392
      Array(work_params[:pre_curation_uploads_added])
106✔
393
    end
394

395
    def deleted_files_param
2✔
396
      deleted_count = (work_params["deleted_files_count"] || "0").to_i
116✔
397
      (1..deleted_count).map { |i| work_params["deleted_file_#{i}"] }.select(&:present?)
134✔
398
    end
399

400
    def process_updates
2✔
401
      if WorkCompareService.update_work(work: @work, update_params:, current_user:)
98✔
402
        redirect_to work_url(@work), notice: "Work was successfully updated."
92✔
403
      else
404
        # This is needed for rendering HTML views with validation errors
405
        @uploads = @work.uploads
6✔
406
        @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
6✔
407
        @work_decorator = WorkDecorator.new(@work, current_user)
6✔
408

409
        # return 200 so the loadbalancer doesn't capture the error
410
        render :edit
6✔
411
      end
412
    end
413

414
    def migrated?
2✔
415
      return false unless params.key?(:submit)
26✔
416

417
      params[:submit] == "Migrate"
18✔
418
    end
419

420
    # Returns a hash object that can be serialized into something that DataTables
421
    # can consume. The `data` elements includes the work's file list all other
422
    # properties are used for displaying different data elements related but not
423
    # directly on the DataTable object (e.g. the total file size)
424
    def file_list_ajax_response(work)
2✔
425
      files = []
306✔
426
      total_size = 0
306✔
427
      unless work.nil?
306✔
428
        files = work.file_list
284✔
429
        total_size = work.total_file_size_from_list(files)
284✔
430
      end
431
      {
432
        data: files,
306✔
433
        total_size:,
434
        total_size_display: ActiveSupport::NumberHelper.number_to_human_size(total_size),
435
        total_file_count: files.count
436
      }
437
    end
438
end
439
# 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