Class: LeadsController

Inherits:
ApplicationController show all
Includes:
DataControllerConfiguration::ProjectDataControllerConfiguration
Defined in:
app/controllers/leads_controller.rb

Constant Summary

Constants included from ProjectsHelper

ProjectsHelper::CLASSIFIER, ProjectsHelper::CLASSIFIER_ANNOTATION

Instance Method Summary collapse

Methods included from DataControllerConfiguration::ProjectDataControllerConfiguration

#annotator_params

Methods inherited from ApplicationController

#set_time_zone

Methods included from RedirectHelper

#destroy_redirect

Methods included from RequestType

#json_request?

Methods included from LogRecent

#log_user_recent_route

Methods included from Cookies

#digest_cookie, #digested_cookie_exists?

Methods included from Whitelist

#whitelist_constantize

Methods included from ProjectsHelper

#cumulative_gb_per_year, #cumulative_projects_created_per_year, #document_cumulative_gb_per_year, #document_gb_per_year, #gb_per_year, #image_cumulative_gb_per_year, #image_gb_per_year, #invalid_object, #project_classification, #project_initials, #project_link, #project_login_link, #project_matches, #project_tag, #projects_list, #projects_search_form, #sound_cumulative_gb_per_year, #sound_gb_per_year, #taxonworks_classification, #week_in_review_graphs

Methods included from Api::Intercept

#intercept_api

Methods included from TokenAuthentication

#intercept_project, #intercept_user, #intercept_user_or_project, #project_token_authenticate, #token_authenticate

Instance Method Details

#add_children ⇒ Object

POST /leads/1/add_children.json



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'app/controllers/leads_controller.rb', line 163

def add_children
  num_to_add = params[:num_to_add]
  if num_to_add < 1 || num_to_add > 2
    @lead.errors.add(:add_children,
      "request must be for 1 or 2, was '#{num_to_add}'"
    )
    render json: @lead.errors, status: :unprocessable_content
    return
  end

  # currently infers num_to_add
  @lead.add_children(sessions_current_project_id, sessions_current_user_id)
  expand_lead
  render action: :show, status: :created, location: @lead
end

#api_eliminated_otus ⇒ Object



399
400
401
402
403
404
405
406
# File 'app/controllers/leads_controller.rb', line 399

def api_eliminated_otus
  lead = Lead.find(params[:id])

  @otus = lead.eliminated_otus

  render '/leads/api/v1/otus'

end

#api_index ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/controllers/leads_controller.rb', line 52

def api_index
  # Default excludes virtual (simple) keys so existing API consumers keep
  # the shape they expect. Callers that want simple keys pass is_virtual=true;
  # is_virtual=all returns both.
  is_virtual =
    if params[:is_virtual].to_s == 'all'
      :all
    elsif params[:is_virtual].present?
      ActiveRecord::Type::Boolean.new.cast(params[:is_virtual])
    else
      false
    end

  @leads = Lead
    .roots_with_data(sessions_current_project_id, true, is_virtual:)
    .where(is_public: true)

  if helpers.extend_response_with('citations')
    @leads = @leads.includes(citations: [:source, :topics, { citation_topics: :topic }])
  end

  @leads = @leads.page(params[:page]).per(params[:per])

  render '/leads/api/v1/index'
end

#api_key ⇒ Object



433
434
435
436
# File 'app/controllers/leads_controller.rb', line 433

def api_key
  @lead = Lead.where(project_id: sessions_current_project_id, is_public: true).find(params.require(:id))
  render '/leads/api/v1/key'
end

#api_remaining_otus ⇒ Object



390
391
392
393
394
395
396
397
# File 'app/controllers/leads_controller.rb', line 390

def api_remaining_otus
  lead = Lead.find(params[:id])

  @otus = lead.remaining_otus

  render '/leads/api/v1/otus'

end

#autocomplete ⇒ Object



408
409
410
411
412
413
# File 'app/controllers/leads_controller.rb', line 408

def autocomplete
  @leads = ::Queries::Lead::Autocomplete.new(
    params.require(:term),
    project_id: sessions_current_project_id,
  ).autocomplete
end

#batch_create_lead_items ⇒ Object

Creates a new key populated with otus from params.



439
440
441
442
443
444
445
446
447
448
449
450
# File 'app/controllers/leads_controller.rb', line 439

def batch_create_lead_items
  @lead = Lead.create!(
    params.require(:lead).permit(:text, :observation_matrix_id)
  )
  @lead.batch_populate_lead_items(params[:otu_query],
    sessions_current_project_id, sessions_current_user_id
  )

  @lead.add_children(sessions_current_project_id, sessions_current_user_id)

  render json: @lead
end

#cite_key_bootstrap ⇒ Object

GET /leads/cite_key_bootstrap.json Accepts either an explicit otu_ids[] list or a nested otu_query (a Queries::Otu::Filter param hash). Returns the MRCA OTU (parent taxon) for the resulting OTU set and the embedded OTU details so the cite_key task can prefill Step 1.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'app/controllers/leads_controller.rb', line 83

def cite_key_bootstrap
  if params[:otu_ids].present?
    otu_ids = Array(params[:otu_ids]).flatten.compact.map(&:to_i).uniq
    scope = ::Otu.where(project_id: sessions_current_project_id, id: otu_ids)
  elsif params[:otu_query].present?
    scope = ::Queries::Otu::Filter.new(params[:otu_query]).all
      .where(project_id: sessions_current_project_id)
  else
    scope = ::Otu.none
  end

  cap = 2000
  total = scope.count
  otus = scope.includes(:taxon_name).limit(cap).to_a
  taxon_name_ids = otus.map(&:taxon_name_id).compact.uniq

  mrca_taxon = ::TaxonName.mrca(taxon_name_ids)
  parent_otu = mrca_taxon &&
    ::Otu.where(project_id: sessions_current_project_id, taxon_name_id: mrca_taxon.id).first

  render json: {
    parent_otu: parent_otu && otu_payload(parent_otu),
    otus: otus.map { |o| otu_payload(o) },
    total: total,
    truncated: total > cap
  }
end

#create ⇒ Object

POST /leads POST /leads.json



149
150
151
152
153
154
155
156
157
158
159
160
# File 'app/controllers/leads_controller.rb', line 149

def create
  @lead = Lead.new(lead_params)
  respond_to do |format|
    if @lead.save
      new_couplet unless @lead.is_virtual
      expand_lead
      format.json { render action: :show, status: :created, location: @lead }
    else
      format.json { render json: @lead.errors, status: :unprocessable_content}
    end
  end
end

#delete_children ⇒ Object

For deleting all children at a particular level of the key where at most one of the children has its own children; the grandchildren, if any, are reparented.



264
265
266
267
268
269
270
271
272
273
274
275
# File 'app/controllers/leads_controller.rb', line 264

def delete_children
  respond_to do |format|
    if @lead.destroy_children
      format.json { head :no_content }
    else
      @lead.errors.add(:delete, 'failed!')
      format.json {
        render json: @lead.errors, status: :unprocessable_content
      }
    end
  end
end

#depictions ⇒ Object

POST /leads/:id/depictions.json



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'app/controllers/leads_controller.rb', line 328

def depictions
  image_ids = params.permit(image_ids: [])[:image_ids] || []
  created = 0

  render json: { success: true, created: } if image_ids.empty?

  Depiction.transaction do
    image_ids.each do |image_id|
      depiction = Depiction.find_or_initialize_by(
        depiction_object: @lead,
        image_id: image_id
      )
      next unless depiction.new_record?

      unless depiction.save
        render json: depiction.errors, status: :unprocessable_content
        raise ActiveRecord::Rollback
      end
      created += 1
    end
  end

  render json: { success: true, created: created }
end

#destroy ⇒ Object

DELETE /leads/1 DELETE /leads/1.json



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'app/controllers/leads_controller.rb', line 199

def destroy
  if @lead.parent_id
    respond_to do |format|
      flash[:error] = 'Delete aborted - you can only delete on root nodes.'
      format.html { redirect_back(fallback_location: (request.referer || root_path)) }
      format.json { head :no_content, status: :unprocessable_content }
    end
    return
  end

  begin
    @lead.transaction_nuke
    respond_to do |format|
      flash[:notice] = 'Key was successfully destroyed.'
      format.html { destroy_redirect @lead }
      format.json { head :no_content }
    end
  rescue ActiveRecord::RecordInvalid
    respond_to do |format|
      flash[:error] = 'Delete failed!'
      format.html { redirect_back(fallback_location: (request.referer || root_path)) }
      format.json { render json: @lead.errors, status: :unprocessable_content }
    end
  end
end

#destroy_children ⇒ Object

For destroying all children at a particular level of the key where none of the children have their own children.



247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'app/controllers/leads_controller.rb', line 247

def destroy_children
  if @lead.parent_id.present?
    if @lead.destroy_children
      head :no_content
    else
      @lead.errors.add(:delete, 'failed!')
      render json: @lead.errors, status: :unprocessable_content
    end
  else
    @lead.errors.add(:destroy, "failed - can't delete the only couplet.")
    render json: @lead.errors, status: :unprocessable_content
  end
end

#destroy_simple_lead ⇒ Object

DELETE /leads/1/destroy_simple_lead.json Simple (virtual) keys are flat by construction and are managed by the cite_key task. This action lets that task destroy either a species-child leaf or the entire simple key from the same endpoint. It refuses anything that isn't virtual so it can't be repurposed to remove a lead from an ordinary dichotomous couplet (which #destroy still guards against).



283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'app/controllers/leads_controller.rb', line 283

def destroy_simple_lead
  unless @lead.is_virtual
    render json: { errors: { base: ['Only virtual (simple key) leads can be destroyed here.'] } },
      status: :unprocessable_content
    return
  end

  begin
    @lead.destroy!
    head :no_content
  rescue ActiveRecord::RecordNotDestroyed, ActiveRecord::RecordInvalid
    render json: @lead.errors, status: :unprocessable_content
  end
end

#destroy_subtree ⇒ Object

A separate action from destroy to be called in different contexts: this one can be called on any lead, not just root. POST /leads/1/destroy_subtree.json



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'app/controllers/leads_controller.rb', line 228

def destroy_subtree
  parent = @lead.parent
  begin
    Lead.destroy_lead_and_descendants(
      @lead, sessions_current_project_id, sessions_current_user_id
    )
  rescue TaxonWorks::Error => e
    parent.errors.add(:error, e)
    render json: parent.errors, status: :unprocessable_content
    return
  end

  @lead = parent.reload
  expand_lead
  render :show, status: :ok, location: @lead
end

#download ⇒ Object

GET /leads/download



425
426
427
428
429
430
431
# File 'app/controllers/leads_controller.rb', line 425

def download
  send_data Export::CSV.generate_csv(
      Lead.where(project_id: sessions_current_project_id)
    ),
    type: 'text',
    filename: "leads_#{DateTime.now}.tsv"
end

#duplicate ⇒ Object

POST /leads/1/duplicate.html



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'app/controllers/leads_controller.rb', line 299

def duplicate
  respond_to do |format|
    format.html {
      if !@lead.parent_id
        if @lead.dupe
          flash[:notice] = 'Key cloned.'
        else
          flash[:error] = @lead.errors.full_messages.join('; ')
        end
      else
        flash[:error] = 'Clone aborted - you can only clone on a root node.'
      end

      redirect_to action: :list
    }
  end
end

#edit ⇒ Object

GET /leads/1/edit



139
140
141
142
143
144
145
# File 'app/controllers/leads_controller.rb', line 139

def edit
  if @lead.is_virtual
    redirect_to cite_key_task_path(lead_id: @lead.id)
  else
    redirect_to new_lead_task_path(lead_id: @lead.id)
  end
end

#eliminated_otus ⇒ Object



381
382
383
384
385
386
387
388
# File 'app/controllers/leads_controller.rb', line 381

def eliminated_otus
  lead = Lead.find(params[:id])

  @otus = lead.eliminated_otus

  render '/leads/otus'

end

#expand_lead ⇒ Object (private)



503
504
505
506
# File 'app/controllers/leads_controller.rb', line 503

def expand_lead
  @children = @lead.children
  @lead_item_otus = @lead.apportioned_lead_item_otus
end

#index ⇒ Object

GET /leads GET /leads.json



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'app/controllers/leads_controller.rb', line 13

def index
  respond_to do |format|
    format.html {
      one_week_ago = Time.now.utc.to_date - 7
      @recent_objects = Lead
        .roots_with_data(sessions_current_project_id, false, is_virtual: :all)
        .where('key_updated_at > ?', one_week_ago)
        .reorder(key_updated_at: :desc)
        .limit(10)

      render '/shared/data/all/index'
    }
    format.json {
      is_virtual =
        if params[:is_virtual].to_s == 'all'
          :all
        elsif params[:is_virtual].present?
          ActiveRecord::Type::Boolean.new.cast(params[:is_virtual])
        else
          nil
        end

      @leads = Lead.roots_with_data(
        sessions_current_project_id,
        !!params[:load_root_otus],
        is_virtual:
      )

      @leads = @leads.where(id: params[:id]) if params[:id].present?

      if params[:recent].present? && ActiveRecord::Type::Boolean.new.cast(params[:recent])
        @leads = @leads.reorder('key_updated_at DESC NULLS LAST')
      end

      @leads = @leads.page(params[:page]).per(params[:per])
    }
  end
end

#insert_couplet ⇒ Object

POST /leads/1/insert_couplet.json



180
181
182
183
# File 'app/controllers/leads_controller.rb', line 180

def insert_couplet
  @lead.insert_couplet
  head :no_content
end

#insert_key ⇒ Object

POST /leads/1/insert_key.json?key_to_insert=:id



318
319
320
321
322
323
324
325
# File 'app/controllers/leads_controller.rb', line 318

def insert_key
  if !@lead.insert_key(params[:key_to_insert])
    render json: @lead.errors, status: :unprocessable_content
    return
  end

  head :no_content
end

#lead_params ⇒ Object (private)



491
492
493
494
495
496
497
# File 'app/controllers/leads_controller.rb', line 491

def lead_params()
  params.require(:lead).permit(
    :parent_id,
    :otu_id, :text, :origin_label, :description, :redirect_id,
    :link_out, :link_out_text, :is_public, :is_virtual, :position
  )
end

#list ⇒ Object



111
112
113
114
115
# File 'app/controllers/leads_controller.rb', line 111

def list
  @leads = Lead
    .roots_with_data(sessions_current_project_id, false, is_virtual: :all)
    .page(params[:page])
end

#new ⇒ Object

GET /leads/new



134
135
136
# File 'app/controllers/leads_controller.rb', line 134

def new
  redirect_to new_lead_task_path
end

#new_couplet ⇒ Object (private)



508
509
510
511
512
# File 'app/controllers/leads_controller.rb', line 508

def new_couplet
  2.times do
    @lead.children.create!
  end
end

#otu_payload(otu) ⇒ Hash (private)

Returns the OTU + taxon_name shape the cite_key task expects.

Returns:

  • (Hash) —

    the OTU + taxon_name shape the cite_key task expects



471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# File 'app/controllers/leads_controller.rb', line 471

def otu_payload(otu)
  tn = otu.taxon_name
  {
    id: otu.id,
    object_tag: helpers.object_tag(otu),
    taxon_name: tn && {
      id: tn.id,
      cached_html: tn.cached_html,
      cached_author_year: tn.cached_author_year,
      cached_nomenclature_date: tn.cached_nomenclature_date,
      cached_is_valid: tn.cached_is_valid,
      cached_misspelling: tn.cached_misspelling
    }
  }
end

#otus ⇒ Object

GET /leads/1/otus.json



368
369
370
# File 'app/controllers/leads_controller.rb', line 368

def otus
  @otus = Otu.associated_with_key(@lead)
end

#redirect_option_texts ⇒ Object

GET /leads/1/redirect_option_texts.json



118
119
120
# File 'app/controllers/leads_controller.rb', line 118

def redirect_option_texts
  @texts = @lead.redirect_options(sessions_current_project_id)
end

#remaining_otus ⇒ Object



372
373
374
375
376
377
378
379
# File 'app/controllers/leads_controller.rb', line 372

def remaining_otus
  lead = Lead.find(params[:id])

  @otus = lead.remaining_otus

  render '/leads/otus'

end

#reorder_children ⇒ Object

PATCH /leads/1/reorder_children.json



354
355
356
357
358
359
360
361
362
363
364
365
# File 'app/controllers/leads_controller.rb', line 354

def reorder_children
  begin
    @lead.reorder_children(reorder_params[:reorder_list])
  rescue TaxonWorks::Error => e
    @lead.errors.add(:reorder_failed, e.to_s)
    render json: @lead.errors, status: :unprocessable_content
    return
  end

  @leads = @lead.reload.children
  @lead_item_otus = @lead.apportioned_lead_item_otus
end

#reorder_params ⇒ Object (private)



499
500
501
# File 'app/controllers/leads_controller.rb', line 499

def reorder_params
  params.permit(reorder_list: [])
end

#reset_lead_items ⇒ Object



452
453
454
455
456
# File 'app/controllers/leads_controller.rb', line 452

def reset_lead_items
  @lead.reset_lead_items

  head :no_content
end

#search ⇒ Object



415
416
417
418
419
420
421
422
# File 'app/controllers/leads_controller.rb', line 415

def search
  if params[:id].blank?
    redirect_to(lead_path,
                alert: 'You must select an item from the list with a click or tab press before clicking show.')
  else
    redirect_to lead_path(params[:id])
  end
end

#set_lead ⇒ Object (private)



487
488
489
# File 'app/controllers/leads_controller.rb', line 487

def set_lead
  @lead = Lead.find(params[:id])
end

#set_observation_matrix ⇒ Object



458
459
460
461
462
463
464
465
466
# File 'app/controllers/leads_controller.rb', line 458

def set_observation_matrix
  if params[:observation_matrix_id].nil?
    @lead.errors.add(:observation_matrix_id, 'is required.')
    render json: @lead.errors, status: :unprocessable_content
  end

  @lead.update!(observation_matrix_id: params[:observation_matrix_id])
  head :no_content
end

#show ⇒ Object

GET /leads/1 GET /leads/1.json



124
125
126
127
128
129
130
131
# File 'app/controllers/leads_controller.rb', line 124

def show
  if @lead.children.present?
    expand_lead
  else
    @children = nil
    @lead_item_otus = @lead.apportioned_lead_item_otus
  end
end

#update ⇒ Object

PATCH/PUT /leads/1.json



186
187
188
189
190
191
192
193
194
195
# File 'app/controllers/leads_controller.rb', line 186

def update
  begin
    @lead.update!(lead_params)
    @lead_item_otus = @lead.parent_id ?
      @lead.parent.apportioned_lead_item_otus :
      { parent: [], children: [] }
  rescue ActiveRecord::RecordInvalid
    render json: @lead.errors, status: :unprocessable_content
  end
end