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

pulibrary / pdc_describe / c0e87805-f6a1-49a6-bff2-ecd1f2371edf

26 Mar 2024 05:45PM UTC coverage: 95.523% (-0.6%) from 96.167%
c0e87805-f6a1-49a6-bff2-ecd1f2371edf

Pull #1715

circleci

carolyncole
Split out the work wizard into its own controller
refs #1684
Pull Request #1715: Split out the work wizard into its own controller

133 of 143 new or added lines in 3 files covered. (93.01%)

16 existing lines in 1 file now uncovered.

3222 of 3373 relevant lines covered (95.52%)

203.36 hits per line

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

87.87
/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
  def index
1✔
33
    @works = Work.all
4✔
34
    respond_to do |format|
4✔
35
      format.html
4✔
36
      format.rss { render layout: false }
6✔
37
    end
38
  end
39

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

48
  # only non wizard mode
49
  def create
1✔
50
    @work = Work.new(created_by_user_id: current_user.id, group_id: params_group_id, user_entered_doi: params["doi"].present?)
32✔
51
    @work.resource = FormToResourceService.convert(params, @work)
32✔
52
    @work.resource.migrated = migrated?
32✔
53
    if @work.valid?
32✔
54
      @work.draft!(current_user)
28✔
55
      upload_service = WorkUploadsEditService.new(@work, current_user)
27✔
56
      upload_service.update_precurated_file_list(added_files_param, deleted_files_param)
27✔
57
      redirect_to work_url(@work), notice: "Work was successfully created."
27✔
58
    else
59
      @work_decorator = WorkDecorator.new(@work, current_user)
4✔
60
      @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
4✔
61
      render :new, status: :unprocessable_entity
4✔
62
    end
63
  end
64

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

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

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

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

98
  def resolve_doi
1✔
99
    @work = Work.find_by_doi(params[:doi])
3✔
100
    redirect_to @work
2✔
101
  end
102

103
  def resolve_ark
1✔
104
    @work = Work.find_by_ark(params[:ark])
3✔
105
    redirect_to @work
2✔
106
  end
107

108
  # GET /works/1/edit_wizard
109
  # only wizard
110
  def edit_wizard
1✔
UNCOV
111
    @work = Work.find(params[:id])
×
UNCOV
112
    @work_decorator = WorkDecorator.new(@work, current_user)
×
UNCOV
113
    @wizard_mode = true
×
UNCOV
114
    if handle_modification_permissions
×
UNCOV
115
      @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
116
    end
117
  end
118

119
  # GET /works/1/edit
120
  # only non wizard mode
121
  def edit
1✔
122
    @work = Work.find(params[:id])
54✔
123
    @work_decorator = WorkDecorator.new(@work, current_user)
54✔
124
    if handle_modification_permissions
54✔
125
      @uploads = @work.uploads
49✔
126
      @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
49✔
127
    end
128
  end
129

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

140
  def approve
1✔
141
    @work = Work.find(params[:id])
14✔
142
    @work.approve!(current_user)
14✔
143
    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."
6✔
144
    redirect_to work_path(@work)
6✔
145
  end
146

147
  def withdraw
1✔
148
    @work = Work.find(params[:id])
2✔
149
    @work.withdraw!(current_user)
2✔
150
    redirect_to work_path(@work)
1✔
151
  end
152

153
  def resubmit
1✔
154
    @work = Work.find(params[:id])
2✔
155
    @work.resubmit!(current_user)
2✔
156
    redirect_to work_path(@work)
1✔
157
  end
158

159
  def assign_curator
1✔
160
    work = Work.find(params[:id])
5✔
161
    work.change_curator(params[:uid], current_user)
5✔
162
    if work.errors.count > 0
4✔
163
      render json: { errors: work.errors.map(&:type) }, status: :bad_request
1✔
164
    else
165
      render json: {}
3✔
166
    end
167
  rescue => ex
168
    Rails.logger.error("Error changing curator for work: #{work.id}. Exception: #{ex.message}")
1✔
169
    render json: { errors: ["Cannot save dataset"] }, status: :bad_request
1✔
170
  end
171

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

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

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

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

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

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

210
  def migrating?
1✔
211
    return @work.resource.migrated if @work&.resource && !params.key?(:migrate)
62✔
212

213
    params[:migrate]
44✔
214
  end
215
  helper_method :migrating?
1✔
216

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

226
  private
1✔
227

228
    # Extract the Work ID parameter
229
    # @return [String]
230
    def work_id_param
1✔
231
      params[:id]
119✔
232
    end
233

234
    # Find the Work requested by ID
235
    # @return [Work]
236
    def work
1✔
237
      Work.find(work_id_param)
119✔
238
    end
239

240
    # Determine whether or not the request is for the :index action in the RSS
241
    # response format
242
    # This is to enable PDC Discovery to index approved content via the RSS feed
243
    def rss_index_request?
1✔
244
      action_name == "index" && request.format.symbol == :rss
507✔
245
    end
246

247
    # Determine whether or not the request is for the :show action in the JSON
248
    # response format
249
    # @return [Boolean]
250
    def json_show_request?
1✔
251
      action_name == "show" && request.format.symbol == :json
505✔
252
    end
253

254
    # Determine whether or not the requested Work has been approved
255
    # @return [Boolean]
256
    def work_approved?
1✔
257
      work&.state == "approved"
4✔
258
    end
259

260
    ##
261
    # Public requests are requests that do not require authentication.
262
    # This is to enable PDC Discovery to index approved content via the RSS feed
263
    # and .json calls to individual works without needing to log in as a user.
264
    # Note that only approved works can be fetched for indexing.
265
    def public_request?
1✔
266
      return true if rss_index_request?
507✔
267
      return true if json_show_request? && work_approved?
505✔
268
      false
503✔
269
    end
270

271
    def work_params
1✔
272
      params[:work] || {}
154✔
273
    end
274

275
    def patch_params
1✔
UNCOV
276
      return {} unless params.key?(:patch)
×
277

UNCOV
278
      params[:patch]
×
279
    end
280

281
    def pre_curation_uploads_param
1✔
UNCOV
282
      return if patch_params.nil?
×
283

UNCOV
284
      patch_params[:pre_curation_uploads]
×
285
    end
286

287
    def readme_file_param
1✔
UNCOV
288
      return if patch_params.nil?
×
289

UNCOV
290
      patch_params[:readme_file]
×
291
    end
292

293
    # rubocop:disable Metrics/AbcSize
294
    # rubocop:disable Metrics/BlockNesting
295
    # rubocop:disable Metrics/CyclomaticComplexity
296
    # rubocop:disable Metrics/MethodLength
297
    # rubocop:disable Metrics/PerceivedComplexity
298
    def rescue_aasm_error
1✔
299
      yield
50✔
300
    rescue AASM::InvalidTransition => error
301
      message = error.message
10✔
302
      if @work.errors.count > 0
10✔
303
        message = @work.errors.to_a.join(", ")
5✔
304
      end
305
      message.chop! if message.last == "."
10✔
306
      Honeybadger.notify("Invalid #{@work.current_transition}: #{error.message} errors: #{message}")
10✔
307
      transition_error_message = "We apologize, the following errors were encountered: #{message}. Please contact the PDC Describe administrators for any assistance."
10✔
308
      @errors = [transition_error_message]
10✔
309

310
      if @work.persisted?
10✔
311
        redirect_to edit_work_url(id: @work.id), notice: transition_error_message, params:
10✔
312
      else
UNCOV
313
        new_params = {}
×
UNCOV
314
        new_params[:wizard] = wizard_mode? if wizard_mode?
×
UNCOV
315
        new_params[:migrate] = migrating? if migrating?
×
UNCOV
316
        @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
UNCOV
317
        redirect_to new_work_url(params: new_params), notice: transition_error_message, params: new_params
×
318
      end
319
    rescue StandardError => generic_error
320
      if action_name == "create"
1✔
321
        if @work.persisted?
1✔
322
          Honeybadger.notify("Failed to create the new Dataset #{@work.id}: #{generic_error.message}")
×
323
          @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
324
          redirect_to edit_work_url(id: @work.id), notice: "Failed to create the new Dataset #{@work.id}: #{generic_error.message}", params:
×
325
        else
326
          Honeybadger.notify("Failed to create a new Dataset #{@work.id}: #{generic_error.message}")
1✔
327
          new_params = {}
1✔
328
          new_params[:wizard] = wizard_mode? if wizard_mode?
1✔
329
          new_params[:migrate] = migrating? if migrating?
1✔
330
          @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
1✔
331
          redirect_to new_work_url(params: new_params), notice: "Failed to create a new Dataset: #{generic_error.message}", params: new_params
1✔
332
        end
333
      else
334
        redirect_to root_url, notice: "We apologize, an error was encountered: #{generic_error.message}. Please contact the PDC Describe administrators."
×
335
      end
336
    end
337
    # rubocop:enable Metrics/PerceivedComplexity
338
    # rubocop:enable Metrics/MethodLength
339
    # rubocop:enable Metrics/CyclomaticComplexity
340
    # rubocop:enable Metrics/BlockNesting
341
    # rubocop:enable Metrics/AbcSize
342

343
    def error_action
1✔
344
      @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
×
345
      if action_name == "create"
×
346
        :new
×
347
      elsif action_name == "validate"
×
348
        :edit
×
349
      elsif action_name == "new_submission"
×
350
        :new_submission
×
351
      else
352
        @work_decorator = WorkDecorator.new(@work, current_user)
×
353
        :show
×
354
      end
355
    end
356

357
    def wizard_mode?
1✔
358
      params[:wizard] == "true"
1✔
359
    end
360

361
    def update_work
1✔
362
      upload_service = WorkUploadsEditService.new(@work, current_user)
48✔
363
      if @work.approved?
48✔
364
        upload_keys = deleted_files_param || []
5✔
365
        deleted_uploads = upload_service.find_post_curation_uploads(upload_keys:)
5✔
366

367
        return head(:forbidden) unless deleted_uploads.empty?
5✔
368
      else
369
        @work = upload_service.update_precurated_file_list(added_files_param, deleted_files_param)
43✔
370
      end
371

372
      process_updates
47✔
373
    end
374

375
    def embargo_date_param
1✔
376
      params["embargo-date"]
50✔
377
    end
378

379
    def embargo_date
1✔
380
      return nil if embargo_date_param.blank?
47✔
381

382
      Date.parse(embargo_date_param)
2✔
383
    rescue Date::Error
384
      Rails.logger.error("Failed to parse the embargo date #{embargo_date_param} for Work #{@work.id}")
1✔
385
      nil
1✔
386
    end
387

388
    def update_params
1✔
389
      {
390
        group_id: params_group_id,
47✔
391
        embargo_date:,
392
        resource: FormToResourceService.convert(params, @work)
393
      }
394
    end
395

396
    def added_files_param
1✔
397
      Array(work_params[:pre_curation_uploads_added])
70✔
398
    end
399

400
    def deleted_files_param
1✔
401
      deleted_count = (work_params["deleted_files_count"] || "0").to_i
75✔
402
      (1..deleted_count).map { |i| work_params["deleted_file_#{i}"] }.select(&:present?)
84✔
403
    end
404

405
    def process_updates
1✔
406
      work_before = @work.dup
47✔
407
      if @work.update(update_params)
47✔
408
        work_compare = WorkCompareService.new(work_before, @work)
45✔
409
        @work.log_changes(work_compare, current_user.id)
45✔
410

411
        redirect_to work_url(@work), notice: "Work was successfully updated."
45✔
412
      else
413
        # This is needed for rendering HTML views with validation errors
414
        @uploads = @work.uploads
2✔
415
        @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
2✔
416

417
        render :edit, status: :unprocessable_entity
2✔
418
      end
419
    end
420

421
    def params_group_id
1✔
422
      # Do not allow a nil for the group id
423
      @params_group_id ||= begin
79✔
424
        group_id = params[:group_id]
77✔
425
        if group_id.blank?
77✔
426
          group_id = current_user.default_group.id
7✔
427
          Honeybadger.notify("We got a nil group as part of the parameters #{params} #{request}")
7✔
428
        end
429
        group_id
77✔
430
      end
431
    end
432

433
    def migrated?
1✔
434
      return false unless params.key?(:submit)
32✔
435

436
      params[:submit] == "Migrate"
28✔
437
    end
438

439
    # @returns false if an error occured
440
    def handle_modification_permissions(uneditable_message: "Can not edit work: #{@work.id} is not editable by #{current_user.uid}",
1✔
441
                                        current_state_message: "Can not edit work: #{@work.id} is not editable in current state by #{current_user.uid}")
442
      no_error = false
104✔
443
      if current_user.blank? || !@work.editable_by?(current_user)
104✔
444
        Honeybadger.notify(uneditable_message)
4✔
445
        redirect_to root_path, notice: I18n.t("works.uneditable.privs")
4✔
446
      elsif !@work.editable_in_current_state?(current_user)
100✔
447
        Honeybadger.notify(current_state_message)
3✔
448
        redirect_to root_path, notice: I18n.t("works.uneditable.approved")
3✔
449
      else
450
        no_error = true
97✔
451
      end
452

453
      no_error
104✔
454
    end
455
end
456
# 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