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

benwbrum / fromthepage / 19111591805

05 Nov 2025 06:03PM UTC coverage: 66.296%. Remained the same
19111591805

push

github

web-flow
Merge pull request #5037 from benwbrum/5036-flash-message-unhandled-nil

5036 - Flash message unhandled nil

1978 of 3497 branches covered (56.56%)

Branch coverage included in aggregate %.

0 of 1 new or added line in 1 file covered. (0.0%)

8453 of 12237 relevant lines covered (69.08%)

115.72 hits per line

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

80.35
/app/controllers/article_controller.rb
1
class ArticleController < ApplicationController
1✔
2
  ARTICLES_BATCH_SIZE = 900
1✔
3

4
  include AbstractXmlController
1✔
5
  include AbstractXmlHelper
1✔
6

7
  skip_before_action :verify_authenticity_token, only: [:relationship_graph]
1✔
8
  before_action :authorized?, except: [:list, :items, :show, :tooltip, :graph, :relationship_graph]
1✔
9

10
  def tooltip
1✔
11
    render partial: 'tooltip'
8✔
12
  end
13

14
  def list
1✔
15
    articles = @collection.articles.includes(:categories)
33✔
16
    @uncategorized_articles = articles.where(categories: { id: nil })
33✔
17
    @categories = Category.recursive_tree_for(@collection.is_a?(DocumentSet) ? @collection.collection_id : @collection.id)
33✔
18
    @categories_tree = @categories.group_by(&:parent_id)
33✔
19

20
    if params[:selected_category_id] == 'uncategorized'
33✔
21
      @selected_category = 'uncategorized'
1✔
22
      @articles = @uncategorized_articles
1✔
23
      @ancestor_ids = []
1✔
24
    elsif params[:selected_category_id].present?
32✔
25
      @selected_category = @categories.find { |category| category.id == params[:selected_category_id].to_i }
50✔
26
      @articles = articles.where(categories: { id: @selected_category.id })
15✔
27
      @ancestor_ids = Category.ancestors_for(@selected_category.id).pluck(:id)
15✔
28
    else
17✔
29
      @selected_category = @categories_tree.dig(nil).first
17✔
30
      if @selected_category.present?
17✔
31
        @articles = articles.where(categories: { id: @selected_category.id })
17✔
32
        @ancestor_ids = Category.ancestors_for(@selected_category.id).pluck(:id)
17✔
33
      else
×
34
        @articles = articles.none
×
35
        @ancestor_ids = []
×
36
      end
37
    end
38

39
    respond_to do |format|
33✔
40
      format.html
33✔
41
      format.turbo_stream
33✔
42
    end
43
  end
44

45
  def items
1✔
46
    articles_scope = @collection.articles.includes(:categories)
17✔
47
    @batch = params[:batch].to_i
17✔
48
    @timestamp = params[:timestamp]
17✔
49

50
    if params[:selected_category_id] == 'uncategorized'
17✔
51
      @category = 'uncategorized'
1✔
52
      articles_scope = articles_scope.where(categories: { id: nil })
1✔
53
    else
16✔
54
      @category = @collection.categories.find(params[:selected_category_id])
16✔
55
      articles_scope = articles_scope.where(categories: { id: @category.id })
16✔
56
    end
57

58
    @next_batch = @batch + 1 if articles_scope.count > (@batch + 1) * ARTICLES_BATCH_SIZE
17✔
59
    @pages_count_map = articles_scope.left_joins(:page_article_links)
17✔
60
                                     .group('articles.id')
61
                                     .pluck('articles.id, COUNT(page_article_links.id)')
62
                                     .to_h
63

64
    @articles = Article.sort_vertically(articles_scope)
17✔
65
                       .limit(ARTICLES_BATCH_SIZE)
66
                       .offset(@batch * ARTICLES_BATCH_SIZE)
67

68
    render turbo_stream: turbo_stream.replace(
17✔
69
      "lazy_items_#{@timestamp}", partial: 'items', locals: { articles: @articles, category: @category, pages_count_map: @pages_count_map }
70
    )
71
  end
72

73
  def delete
1✔
74
    result = Article::Destroy.new(
2✔
75
      article: @article,
76
      collection: @collection,
77
      user: current_user
78
    ).call
79

80
    if result.success?
2✔
81
      redirect_to collection_subjects_path(@collection.owner, @collection)
2✔
82
    else
×
NEW
83
      flash[:alert] = result.message || t('errors.error')
×
84
      redirect_to collection_article_show_path(@collection.owner, @collection, @article.id)
×
85
    end
86
  end
87

88
  def edit
1✔
89
    @sex_autocomplete=@collection.articles.distinct.pluck(:sex).select { |e| !e.blank? }
24✔
90
    @race_description_autocomplete=@collection.articles.distinct.pluck(:race_description).select { |e| !e.blank? }
24✔
91
  end
92

93
  def update
1✔
94
    if params[:save]
8✔
95
      result = Article::Update.new(
5✔
96
        article: @article,
97
        article_params: article_params,
98
        user: current_user
99
      ).call
100

101
      if result.success?
5✔
102
        record_deed
4✔
103

104
        flash[:notice] = result.notice
4✔
105
        redirect_to collection_article_edit_path(@collection.owner, @collection, @article)
4✔
106
      else
1✔
107
        @article = result.article
1✔
108
        render :edit, status: :unprocessable_entity
1✔
109
      end
3✔
110
    elsif params[:autolink]
3✔
111
      @article.source_text = autolink(@article.source_text)
2✔
112

113
      flash[:notice] = t('.subjects_auto_linking')
2✔
114
      render :edit
2✔
115
    else
116
      # Default to redirect
1✔
117
      redirect_to collection_article_edit_path(@collection.owner, @collection, @article)
1✔
118
    end
119
  end
120

121
  def article_category
1✔
122
    categories = Category.where(id: params[:category_ids])
1✔
123
    @article.categories = categories
1✔
124
    @article.save!
1✔
125

126
    respond_to(&:turbo_stream)
1✔
127
  end
128

129
  def combine_duplicate
1✔
130
    Article::Combine.new(
3✔
131
      article: @article,
132
      from_article_ids: params[:from_article_ids],
133
      user: current_user
134
    ).call
135

136
    flash[:notice] = t('.selected_subjects_combined', title: @article.title)
3✔
137
    redirect_to collection_article_edit_path(@collection.owner, @collection, @article)
3✔
138
  end
139

140
  def graph
1✔
141
    redirect_to action: :show, article_id: @article.id
×
142
  end
143

144
  def relationship_graph
1✔
145
    unless File.exist? @article.d3js_file
4!
146
      article_links=[]
4✔
147
      article_nodes=[]
4✔
148
      # get all the source article links
149
      @article.source_article_links.each do |link|
4✔
150
        article_nodes << link.target_article
3✔
151
        article_links << link.target_article_id
3✔
152
      end
153
      # get all the source article links
154
      @article.target_article_links.each do |link|
4✔
155
        article_nodes << link.source_article
×
156
        article_links << link.source_article_id
×
157
      end
158
      document_nodes=[]
4✔
159
      work_nodes=[]
4✔
160
      center_article_to_document_links=[]
4✔
161
      second_document_to_article_links=[]
4✔
162
      # get all the pages and works linking to this article
163
      if @collection.pages_are_meaningful?
4✔
164
        document_association = @article.pages
4✔
165
      else
×
166
        document_association = @article.works
×
167
      end
168

169
      document_association.each do |document|
4✔
170
        document_nodes << document
4✔
171
        center_article_to_document_links << document.id
4✔
172
        document.articles.each do |article|
4✔
173
          article_nodes << article
7✔
174
          second_document_to_article_links << [document.id, article.id]
7✔
175
        end
176
      end
177

178
      # now construct the JSON response
179
      nodes=[]
4✔
180
      nodes << {
4✔
181
        'id' => "S#{@article.id}",
182
        'title' => @article.title,
183
        'group' => @article.title,
184
        'link' => collection_article_show_url(@collection.owner, @collection, @article) }
185
      article_nodes.uniq.each do |article|
4✔
186
        if article != @article
7✔
187
          nodes << {
3✔
188
            'id' => "S#{article.id}",
189
            'title' => article.title,
190
            'group' => article.categories.first&.title,
3!
191
            'link' => collection_article_show_url(@collection.owner, @collection, article)
192
          }
193
        end
194
      end
195
      if @collection.pages_are_meaningful?
4✔
196
        document_nodes.uniq.each do |page|
4✔
197
          nodes << {
4✔
198
            'id' => "D#{page.id}",
199
            'title' => page.title + ' in ' + page.work.title,
200
            'group' => 'Documents',
201
            'link' => collection_display_page_path(@collection.owner, @collection, page.work, page),
202
            'identifier' => page.work.identifier
203
          }
204
        end
205
      else
×
206
        document_nodes.uniq.each do |work|
×
207
          nodes << {
×
208
            'id' => "D#{work.id}",
209
            'title' => work.title,
210
            'group' => 'Documents',
211
            'link' => collection_read_work_url(@collection.owner, @collection, work),
212
            'identifier' => work.identifier
213
          }
214
        end
215
      end
216

217
      links=[]
4✔
218
      article_links.tally.each do |article_id, link_count|
4✔
219
        links << {
3✔
220
          'source'=>"S#{@article.id}",
221
          'target'=>"S#{article_id}",
222
          'value'=>link_count,
223
          'group'=>'direct'
224
        }
225
      end
226
      center_article_to_document_links.tally.each do |work_id, link_count|
4✔
227
        links << {
4✔
228
          'source'=>"S#{@article.id}",
229
          'target'=>"D#{work_id}",
230
          'value'=>link_count,
231
          'group'=>'mentioned in'
232
        }
233
      end
234
      second_document_to_article_links.tally.each do |link_pair, link_count|
4✔
235
        work_id, second_article_id = link_pair
7✔
236
        links << {
7✔
237
          'source'=>"D#{work_id}",
238
          'target'=>"S#{second_article_id}",
239
          'value'=>link_count,
240
          'group'=>'mentions'
241
        }
242
      end
243

244
      doc={ 'nodes' => nodes, 'links' => links }
4✔
245
      File.write(@article.d3js_file, doc.to_json)
4✔
246
    end
247

248
    # now render the d3js file
249
    render file: @article.d3js_file, type: 'application/javascript; charset=utf-8', layout: false
4✔
250
  end
251

252
  def show
1✔
253
    sql =
254
      'SELECT count(*) as link_count, '+
13✔
255
      'a.title as title, '+
256
      'a.id as article_id '+
257
      'FROM page_article_links to_links '+
258
      'INNER JOIN page_article_links from_links '+
259
      '  ON to_links.page_id = from_links.page_id '+
260
      'INNER JOIN articles a '+
261
      '  ON from_links.article_id = a.id '+
262
      "WHERE to_links.article_id = #{@article.id} "+
263
      " AND from_links.article_id != #{@article.id} "
264
    sql += 'GROUP BY a.title, a.id '
13✔
265
    logger.debug(sql)
13✔
266
    article_links = Article.connection.select_all(sql)
13✔
267
    link_total = 0
13✔
268
    link_max = 0
13✔
269
    count_per_rank = { 0 => 0 }
13✔
270
    article_links.each do |l|
13✔
271
      link_count = l['link_count'].to_i
2✔
272
      link_total += link_count
2✔
273
      link_max = [link_count, link_max].max
2✔
274

275
      count_per_rank[link_count] ||= 0
2✔
276
      count_per_rank[link_count] += 1
2✔
277
    end
278

279
    min_rank = 0
13✔
280
    # now we know how many articles each link count has, as well as the size
281
    if params[:min_rank]
13✔
282
      # use the min rank from the params
×
283
      min_rank = params[:min_rank].to_i
×
284
    else
285
      # calculate whether we should reduce the rank
13✔
286
      num_articles = article_links.count
13✔
287
      while num_articles > DEFAULT_ARTICLES_PER_GRAPH && min_rank < link_max
13✔
288
        # remove the outer rank
×
289
        num_articles -= count_per_rank[min_rank] || 0 # hash is sparse
×
290
        min_rank += 1
×
291
        logger.debug("DEBUG: \tnum articles now #{num_articles}\n")
×
292
      end
293
    end
294

295
    dot_source = render_to_string(
13✔
296
      partial: 'graph',
297
      layout: false,
298
      locals: {
299
        article_links: article_links,
300
        link_total: link_total,
301
        link_max: link_max,
302
        min_rank: min_rank
303
      },
304
      formats: [:dot]
305
    )
306

307
    dot_file = "#{Rails.root}/public/images/working/dot/#{@article.id}.dot"
13✔
308
    File.open(dot_file, 'w') do |f|
13✔
309
      f.write(dot_source)
13✔
310
    end
311
    dot_out = "#{Rails.root}/public/images/working/dot/#{@article.id}.png"
13✔
312
    dot_out_map = "#{Rails.root}/public/images/working/dot/#{@article.id}.map"
13✔
313

314
    system "#{Rails.application.config.neato} -Tcmapx -o#{dot_out_map} -Tpng #{dot_file} -o #{dot_out}"
13✔
315

316
    @map = File.read(dot_out_map)
13✔
317
    @article.graph_image = dot_out
13✔
318
    @article.save!
13✔
319
    session[:col_id] = @collection.slug
13✔
320
  end
321

322
  # display the article upload form
323
  def upload_form
1✔
324
  end
325

326
  # actually process the uploaded CSV
327
  def subject_upload
1✔
328
    @collection = Collection.find params[:upload][:collection_id]
×
329
    # read the file
330
    file = params[:upload][:file].tempfile
×
331

332
    # csv = CSV.read(params[:upload][:file].tempfile, :headers => true)
333
    begin
334
      csv = CSV.read(params[:upload][:file].tempfile, headers: true)
×
335
    rescue
336
      contents = File.read(params[:upload][:file].tempfile)
×
337
      detection = CharlockHolmes::EncodingDetector.detect(contents)
×
338

339
      csv = CSV.read(params[:upload][:file].tempfile,
×
340
                      encoding: "bom|#{detection[:encoding]}",
341
                      liberal_parsing: true,
342
                      headers: true)
343
    end
344

345
    provenance = params[:upload][:file].original_filename + " (uploaded #{Time.now} UTC)"
×
346

347
    # check the values
348
    if csv.headers.include?('HEADING') && csv.headers.include?('URI') && csv.headers.include?('ARTICLE') && csv.headers.include?('CATEGORY')
×
349
      # create subjects if heading checks out
×
350
      csv.each do |row|
×
351
        title = row['HEADING']
×
352
        article = @collection.articles.where(title: title).first || Article.new(title: title, provenance: provenance)
×
353
        article.collection = @collection
×
354
        article.source_text = row['ARTICLE']
×
355
        article.uri = row['URI']
×
356
        article.categories << find_or_create_category(@collection, row['CATEGORY'])
×
357
        article.save!
×
358
      end
359
      # redirect to subject list
360
      redirect_to collection_subjects_path(@collection.owner, @collection)
×
361
    else
362
      # flash message and redirect to upload form on problems
×
363
      flash[:error] = t('.csv_file_must_contain_headers')
×
364
      redirect_to article_upload_form_path(@collection)
×
365
    end
366
  end
367

368
  def upload_example
1✔
369
    example = File.read(File.join(Rails.root, 'app', 'views', 'static', 'subject_example.csv'))
×
370
    send_data example, filename: 'subject_example.csv'
×
371
  end
372

373
  protected
1✔
374

375
  def record_deed
1✔
376
    deed = Deed.new
4✔
377
    deed.article = @article
4✔
378
    deed.deed_type = DeedType::ARTICLE_EDIT
4✔
379
    deed.collection = @article.collection
4✔
380
    deed.user = current_user
4✔
381
    deed.save!
4✔
382
    update_search_attempt_contributions
4✔
383
  end
384

385
  private
1✔
386

387
  def authorized?
1✔
388
    redirect_to dashboard_path unless user_signed_in?
30✔
389
  end
390

391
  def article_params
1✔
392
    params.require(:article).permit(:title, :uri, :short_summary, :source_text, :latitude, :longitude, :birth_date, :death_date, :race_description, :sex, :bibliography, :begun, :ended, category_ids: [])
5✔
393
  end
394
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc