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

pulibrary / pdc_describe / 35525209-8c8b-426d-b992-849111d800ff

02 Jun 2026 07:47PM UTC coverage: 41.528% (-54.0%) from 95.52%
35525209-8c8b-426d-b992-849111d800ff

Pull #2324

circleci

carolyncole
Updating code to allow for multiple WorkActivity classes based on the transition
Pull Request #2324: Updating code to allow for multiple WorkActivity classes based on the…

35 of 87 new or added lines in 6 files covered. (40.23%)

2096 existing lines in 79 files now uncovered.

1647 of 3966 relevant lines covered (41.53%)

4.81 hits per line

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

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

3
require "nokogiri"
1✔
4
require "open-uri"
1✔
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
1✔
26
  include ERB::Util
1✔
27
  around_action :rescue_aasm_error, only: [:approve, :withdraw, :resubmit, :validate, :create]
1✔
28

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

32
  # This method allows any user to visit /works.rss
33
  # and a list is generated of all works in an RSS format
34
  # so that the works are harvestable by PDC Discovery.
35
  def index
1✔
UNCOV
36
    if rss_index_request?
×
UNCOV
37
      rss_index
×
38
    # If the user is authenticated as a super admin and visits /works it will return a list of all approved works in an HTML format.
UNCOV
39
    elsif current_user.super_admin?
×
UNCOV
40
      @works = Work.all
×
UNCOV
41
      respond_to do |format|
×
UNCOV
42
        format.html
×
43
      end
44
    else
45
      # If a user who is not a super admin attempts to visit /works they will be redirected to the root_path and receive a flash notice that they do not have access to that page.
UNCOV
46
      flash[:notice] = "You do not have access to this page."
×
UNCOV
47
      redirect_to root_path
×
48
    end
49
  end
50

51
  # only non wizard mode
52
  def new
1✔
UNCOV
53
    group = Group.find_by(code: params[:group_code]) || current_user.default_group
×
UNCOV
54
    @work = Work.new(created_by_user_id: current_user.id, group:)
×
UNCOV
55
    @work_decorator = WorkDecorator.new(@work, current_user)
×
UNCOV
56
    @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
57
  end
58

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

77
  ##
78
  # Show the information for the dataset with the given id
79
  # When requested as .json, return the internal json resource
80
  def show
1✔
UNCOV
81
    @work = Work.find(params[:id])
×
UNCOV
82
    UpdateSnapshotJob.perform_later(work_id: @work.id, last_snapshot_id: work.upload_snapshots.first&.id)
×
UNCOV
83
    @work_decorator = WorkDecorator.new(@work, current_user)
×
84

UNCOV
85
    respond_to do |format|
×
UNCOV
86
      format.html do
×
87
        # Ensure that the Work belongs to a Group
UNCOV
88
        group = @work_decorator.group
×
UNCOV
89
        raise(Work::InvalidGroupError, "The Work #{@work.id} does not belong to any Group") unless group
×
90

UNCOV
91
        @can_curate = current_user.can_admin?(group)
×
UNCOV
92
        @work.mark_new_notifications_as_read(current_user.id)
×
93
      end
UNCOV
94
      format.json { render json: @work.to_json }
×
95
    end
96
  end
97

98
  # only non wizard mode
99
  def file_list
1✔
UNCOV
100
    if params[:id] == "NONE"
×
101
      # This is a special case when we render the file list for a work being created
102
      # (i.e. it does not have an id just yet)
UNCOV
103
      render json: file_list_ajax_response(nil)
×
104
    else
UNCOV
105
      work = Work.find(params[:id])
×
UNCOV
106
      render json: file_list_ajax_response(work)
×
107
    end
108
  end
109

110
  def resolve_doi
1✔
UNCOV
111
    @work = Work.find_by_doi(params[:doi])
×
UNCOV
112
    redirect_to @work
×
113
  end
114

115
  def resolve_ark
1✔
UNCOV
116
    @work = Work.find_by_ark(params[:ark])
×
UNCOV
117
    redirect_to @work
×
118
  end
119

120
  # GET /works/1/edit
121
  # only non wizard mode
122
  def edit
1✔
UNCOV
123
    @work = Work.find(params[:id])
×
UNCOV
124
    @work_decorator = WorkDecorator.new(@work, current_user)
×
UNCOV
125
    if validate_modification_permissions(work: @work,
×
126
                                         uneditable_message: "Can not update work: #{@work.id} is not editable by #{current_user.uid}",
127
                                         current_state_message: "Can not update work: #{@work.id} is not editable in current state by #{current_user.uid}")
UNCOV
128
      @uploads = @work.uploads
×
UNCOV
129
      @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
130
    end
131
  end
132

133
  # PATCH /works/1
134
  # only non wizard mode
135
  def update
1✔
UNCOV
136
    @work = Work.find(params[:id])
×
UNCOV
137
    if validate_modification_permissions(work: @work, uneditable_message: "Can not update work: #{@work.id} is not editable by #{current_user.uid}",
×
138
                                         current_state_message: "Can not update work: #{@work.id} is not editable in current state by #{current_user.uid}")
UNCOV
139
      update_work
×
140
    end
141
  end
142

143
  def approve
1✔
UNCOV
144
    @work = Work.find(params[:id])
×
UNCOV
145
    @work.approve!(current_user)
×
UNCOV
146
    flash[:notice] = "Your files are being moved to the #{@work.files_mode_human} bucket in the background. Depending on the file sizes this may take some time."
×
UNCOV
147
    redirect_to work_path(@work)
×
148
  end
149

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

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

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

UNCOV
166
    redirect_to work_path(@work)
×
167
  end
168

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

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

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

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

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

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

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

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

UNCOV
225
    params[:migrate]
×
226
  end
227
  helper_method :migrating?
1✔
228

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

238
  # POST /works/1/upload-files (called via Uppy)
239
  def upload_files
1✔
240
    @work = Work.find(params[:id])
×
241
    upload_service = WorkUploadsEditService.new(@work, current_user)
×
242
    upload_service.update_precurated_file_list(params["files"], [])
×
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
1✔
UNCOV
249
    @work = Work.find(params[:id])
×
UNCOV
250
    if @work.valid_to_complete
×
UNCOV
251
      redirect_to work_review_path(@work)
×
252
    else
UNCOV
253
      message = @work.errors.to_a.join(", ")
×
UNCOV
254
      transition_error_message = "We apologize, the following errors were encountered: #{message}. Please contact the PDC Describe administrators for any assistance."
×
UNCOV
255
      redirect_to edit_work_url(id: @work.id), notice: transition_error_message, params:
×
256
    end
257
  end
258

259
  private
1✔
260

261
    # Extract the Work ID parameter
262
    # @return [String]
263
    def work_id_param
1✔
UNCOV
264
      params[:id]
×
265
    end
266

267
    # Find the Work requested by ID
268
    # @return [Work]
269
    def work
1✔
UNCOV
270
      Work.find(work_id_param)
×
271
    end
272

273
    # Determine whether or not the request is for the :index action in the RSS
274
    # response format
275
    # This is to enable PDC Discovery to index approved content via the RSS feed
276
    def rss_index_request?
1✔
UNCOV
277
      action_name == "index" && request.format.symbol == :rss
×
278
    end
279

280
    # Determine whether or not the request is for the :show action in the JSON
281
    # response format
282
    # @return [Boolean]
283
    def json_show_request?
1✔
UNCOV
284
      action_name == "show" && request.format.symbol == :json
×
285
    end
286

287
    # Determine whether or not the requested Work has been approved
288
    # @return [Boolean]
289
    def work_approved?
1✔
290
      work&.state == "approved"
×
291
    end
292

293
    ##
294
    # Public requests are requests that do not require authentication.
295
    # This is to enable PDC Discovery to index approved content via the RSS feed
296
    # and .json calls to individual works without needing to log in as a user.
297
    # Note that only approved works can be fetched for indexing.
298
    def public_request?
1✔
UNCOV
299
      return true if rss_index_request?
×
UNCOV
300
      return true if json_show_request?
×
UNCOV
301
      false
×
302
    end
303

304
    def work_params
1✔
UNCOV
305
      params[:work] || {}
×
306
    end
307

308
    # @note No testing coverage but not a route, not called
309
    def patch_params
1✔
310
      return {} unless params.key?(:patch)
×
311

312
      params[:patch]
×
313
    end
314

315
    # @note No testing coverage but not a route, not called
316
    def pre_curation_uploads_param
1✔
317
      return if patch_params.nil?
×
318

319
      patch_params[:pre_curation_uploads]
×
320
    end
321

322
    # @note No testing coverage but not a route, not called
323
    def readme_file_param
1✔
324
      return if patch_params.nil?
×
325

326
      patch_params[:readme_file]
×
327
    end
328

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

340
    rescue_from StandardError do |generic_error|
1✔
UNCOV
341
      Honeybadger.notify("We apologize, an error was encountered: #{generic_error.message}.")
×
UNCOV
342
      redirect_to error_url, notice: "We apologize, an error was encountered: #{generic_error.message}. Please contact the PDC Describe administrators."
×
343
    end
344

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

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

374
    # @note No testing coverage but not a route, not called
375
    def error_action
1✔
376
      @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
377
      if action_name == "create"
×
378
        :new
×
379
      elsif action_name == "validate"
×
380
        :edit
×
381
      elsif action_name == "new_submission"
×
382
        :new_submission
×
383
      else
384
        @work_decorator = WorkDecorator.new(@work, current_user)
×
385
        :show
×
386
      end
387
    end
388

389
    def wizard_mode?
1✔
UNCOV
390
      params[:wizard] == "true"
×
391
    end
392

393
    def update_work
1✔
UNCOV
394
      check_for_stale_update(@work, params)
×
UNCOV
395
      upload_service = WorkUploadsEditService.new(@work, current_user)
×
UNCOV
396
      if @work.approved?
×
UNCOV
397
        upload_keys = deleted_files_param || []
×
UNCOV
398
        deleted_uploads = upload_service.find_post_curation_uploads(upload_keys:)
×
399

UNCOV
400
        return head(:forbidden) unless deleted_uploads.empty?
×
401
      else
UNCOV
402
        @work = upload_service.update_precurated_file_list(added_files_param, deleted_files_param)
×
403
      end
404

UNCOV
405
      process_updates
×
406
    end
407

408
    def added_files_param
1✔
UNCOV
409
      Array(work_params[:pre_curation_uploads_added])
×
410
    end
411

412
    def deleted_files_param
1✔
UNCOV
413
      deleted_count = (work_params["deleted_files_count"] || "0").to_i
×
UNCOV
414
      (1..deleted_count).map { |i| work_params["deleted_file_#{i}"] }.select(&:present?)
×
415
    end
416

417
    def process_updates
1✔
UNCOV
418
      if WorkCompareService.update_work(work: @work, update_params:, current_user:)
×
UNCOV
419
        redirect_to work_url(@work), notice: "Work was successfully updated."
×
420
      else
421
        # This is needed for rendering HTML views with validation errors
UNCOV
422
        @uploads = @work.uploads
×
UNCOV
423
        @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
UNCOV
424
        @work_decorator = WorkDecorator.new(@work, current_user)
×
425

426
        # return 200 so the loadbalancer doesn't capture the error
UNCOV
427
        render :edit
×
428
      end
429
    end
430

431
    def migrated?
1✔
UNCOV
432
      return false unless params.key?(:submit)
×
433

UNCOV
434
      params[:submit] == "Migrate"
×
435
    end
436

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

456
    # Include all works in the RSS feed so that each dataset is harvestable
457
    # so that PDC Discovery has landing pages regardless of the Work state.
458
    def rss_index
1✔
UNCOV
459
      @works = Work.all
×
UNCOV
460
      respond_to do |format|
×
UNCOV
461
        format.rss { render layout: false }
×
462
      end
463
    end
464
end
465
# 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

© 2026 Coveralls, Inc