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

pulibrary / pdc_describe / 5607114a-7f5f-4c65-95e9-98369d1fd2b8

03 Oct 2024 02:13PM UTC coverage: 96.129% (+0.2%) from 95.901%
5607114a-7f5f-4c65-95e9-98369d1fd2b8

push

circleci

jrgriffiniii
wip

3328 of 3462 relevant lines covered (96.13%)

204.47 hits per line

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

87.04
/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
8✔
43
    @work = Work.new(created_by_user_id: current_user.id, group:)
8✔
44
    @work_decorator = WorkDecorator.new(@work, current_user)
8✔
45
    @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
8✔
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?)
13✔
51
    @work.resource = FormToResourceService.convert(params, @work)
13✔
52
    @work.resource.migrated = migrated?
13✔
53
    if @work.valid?
13✔
54
      @work.draft!(current_user)
9✔
55
      upload_service = WorkUploadsEditService.new(@work, current_user)
8✔
56
      upload_service.update_precurated_file_list(added_files_param, deleted_files_param)
8✔
57
      redirect_to work_url(@work), notice: "Work was successfully created."
8✔
58
    else
59
      @work_decorator = WorkDecorator.new(@work, current_user)
4✔
60
      @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
4✔
61
      # return 200 so the loadbalancer doesn't capture the error
62
      render :new
4✔
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
1✔
70
    @work = Work.find(params[:id])
103✔
71
    UpdateSnapshotJob.perform_later(work_id: @work.id, last_snapshot_id: work.upload_snapshots.first&.id)
103✔
72
    @work_decorator = WorkDecorator.new(@work, current_user)
103✔
73

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

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

87
  # only non wizard mode
88
  def file_list
1✔
89
    if params[:id] == "NONE"
156✔
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: []
11✔
93
    else
94
      @work = Work.find(params[:id])
145✔
95
      render json: @work.file_list
145✔
96
    end
97
  end
98

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

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

109
  # GET /works/1/edit
110
  # only non wizard mode
111
  def edit
1✔
112
    @work = Work.find(params[:id])
55✔
113
    @work_decorator = WorkDecorator.new(@work, current_user)
55✔
114
    if validate_modification_permissions(work: @work,
55✔
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
50✔
118
      @form_resource_decorator = FormResourceDecorator.new(@work, current_user)
50✔
119
    end
120
  end
121

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

132
  def approve
1✔
133
    @work = Work.find(params[:id])
14✔
134
    @work.approve!(current_user)
14✔
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."
6✔
136
    redirect_to work_path(@work)
6✔
137
  end
138

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

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

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

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

158
  def assign_curator
1✔
159
    work = Work.find(params[:id])
5✔
160
    work.change_curator(params[:uid], current_user)
5✔
161
    if work.errors.count > 0
4✔
162
      render json: { errors: work.errors.map(&:type) }, status: :bad_request
1✔
163
    else
164
      render json: {}
3✔
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}")
1✔
169
    Honeybadger.notify("Error changing curator for work: #{work.id}. Exception: #{ex.message}")
1✔
170
    respond_to do |format|
1✔
171
      format.json do
1✔
172
        render json: { errors: ["Cannot save dataset"] }, status: :bad_request
×
173
      end
174
    end
175
  end
176

177
  def add_message
1✔
178
    work = Work.find(params[:id])
6✔
179
    if params["new-message"].present?
6✔
180
      new_message_param = params["new-message"]
6✔
181
      sanitized_new_message = html_escape(new_message_param)
6✔
182

183
      work.add_message(sanitized_new_message, current_user.id)
6✔
184
    end
185
    redirect_to work_path(id: params[:id])
6✔
186
  end
187

188
  def add_provenance_note
1✔
189
    work = Work.find(params[:id])
2✔
190
    if params["new-provenance-note"].present?
2✔
191
      new_date = params["new-provenance-date"]
2✔
192
      new_label = params["change_label"]
2✔
193
      new_note = html_escape(params["new-provenance-note"])
2✔
194

195
      work.add_provenance_note(new_date, new_note, current_user.id, new_label)
2✔
196
    end
197
    redirect_to work_path(id: params[:id])
2✔
198
  end
199

200
  # Outputs the Datacite XML representation of the work
201
  def datacite
1✔
202
    work = Work.find(params[:id])
2✔
203
    render xml: work.to_xml
2✔
204
  end
205

206
  def datacite_validate
1✔
207
    @errors = []
3✔
208
    @work = Work.find(params[:id])
3✔
209
    validator = WorkValidator.new(@work)
3✔
210
    unless validator.valid_datacite?
3✔
211
      @errors = @work.errors.full_messages
2✔
212
    end
213
  end
214

215
  def migrating?
1✔
216
    return @work.resource.migrated if @work&.resource && !params.key?(:migrate)
24✔
217

218
    params[:migrate]
6✔
219
  end
220
  helper_method :migrating?
1✔
221

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

231
  # POST /works/1/upload-files (called via Uppy)
232
  def upload_files
1✔
233
    @work = Work.find(params[:id])
×
234
    upload_service = WorkUploadsEditService.new(@work, current_user)
×
235
    upload_service.update_precurated_file_list(params["files"], [])
×
236
  end
237

238
  # Validates that the work is ready to be approved
239
  # GET /works/1/validate
240
  def validate
1✔
241
    @work = Work.find(params[:id])
8✔
242
    @work.complete_submission!(current_user)
8✔
243
    redirect_to user_path(current_user)
5✔
244
  end
245

246
  private
1✔
247

248
    # Extract the Work ID parameter
249
    # @return [String]
250
    def work_id_param
1✔
251
      params[:id]
107✔
252
    end
253

254
    # Find the Work requested by ID
255
    # @return [Work]
256
    def work
1✔
257
      Work.find(work_id_param)
107✔
258
    end
259

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

267
    # Determine whether or not the request is for the :show action in the JSON
268
    # response format
269
    # @return [Boolean]
270
    def json_show_request?
1✔
271
      action_name == "show" && request.format.symbol == :json
448✔
272
    end
273

274
    # Determine whether or not the requested Work has been approved
275
    # @return [Boolean]
276
    def work_approved?
1✔
277
      work&.state == "approved"
4✔
278
    end
279

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

291
    def work_params
1✔
292
      params[:work] || {}
120✔
293
    end
294

295
    # @note No testing coverage but not a route, not called
296
    def patch_params
1✔
297
      return {} unless params.key?(:patch)
×
298

299
      params[:patch]
×
300
    end
301

302
    # @note No testing coverage but not a route, not called
303
    def pre_curation_uploads_param
1✔
304
      return if patch_params.nil?
×
305

306
      patch_params[:pre_curation_uploads]
×
307
    end
308

309
    # @note No testing coverage but not a route, not called
310
    def readme_file_param
1✔
311
      return if patch_params.nil?
×
312

313
      patch_params[:readme_file]
×
314
    end
315

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

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

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

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

371
    def wizard_mode?
1✔
372
      params[:wizard] == "true"
1✔
373
    end
374

375
    def update_work
1✔
376
      upload_service = WorkUploadsEditService.new(@work, current_user)
50✔
377
      if @work.approved?
50✔
378
        upload_keys = deleted_files_param || []
5✔
379
        deleted_uploads = upload_service.find_post_curation_uploads(upload_keys:)
5✔
380

381
        return head(:forbidden) unless deleted_uploads.empty?
5✔
382
      else
383
        @work = upload_service.update_precurated_file_list(added_files_param, deleted_files_param)
45✔
384
      end
385

386
      process_updates
49✔
387
    end
388

389
    def added_files_param
1✔
390
      Array(work_params[:pre_curation_uploads_added])
53✔
391
    end
392

393
    def deleted_files_param
1✔
394
      deleted_count = (work_params["deleted_files_count"] || "0").to_i
58✔
395
      (1..deleted_count).map { |i| work_params["deleted_file_#{i}"] }.select(&:present?)
67✔
396
    end
397

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

407
        # return 200 so the loadbalancer doesn't capture the error
408
        render :edit
3✔
409
      end
410
    end
411

412
    def migrated?
1✔
413
      return false unless params.key?(:submit)
13✔
414

415
      params[:submit] == "Migrate"
9✔
416
    end
417
end
418
# 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