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

benwbrum / fromthepage / 17387282326

01 Sep 2025 09:13PM UTC coverage: 64.405%. Remained the same
17387282326

push

github

web-flow
4857 - Require rubocop step in CI (#4858)

* 4857 - Require rubocop step in CI

* 4865 - Organize gemfiles

1790 of 3303 branches covered (54.19%)

Branch coverage included in aggregate %.

839 of 1497 new or added lines in 133 files covered. (56.05%)

43 existing lines in 29 files now uncovered.

7928 of 11786 relevant lines covered (67.27%)

103.82 hits per line

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

76.44
/app/controllers/article_controller.rb
1
class ArticleController < ApplicationController
1✔
2
  include AbstractXmlController
1✔
3
  include AbstractXmlHelper
1✔
4

5
  skip_before_action :verify_authenticity_token, only: [ :relationship_graph ]
1✔
6
  before_action :authorized?, except: [ :list, :show, :tooltip, :graph, :relationship_graph ]
1✔
7

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

12
  def list
1✔
13
    articles = @collection.articles.includes(:categories)
21✔
14
    @categories = @collection.categories
21✔
15
    @vertical_articles = {}
21✔
16
    @categories.each do |category|
21✔
17
      current_articles = articles.where(categories: { id: category.id }).reorder(:title)
53✔
18
      @vertical_articles[category] = sort_vertically(current_articles)
53✔
19
    end
20

21
    @uncategorized_articles = sort_vertically(
21✔
22
      articles.where(categories: { id: nil }).reorder(:title)
23
    )
24
  end
25

26
  def delete
1✔
27
    result = Article::Destroy.new(
2✔
28
      article: @article,
29
      collection: @collection,
30
      user: current_user
31
    ).call
32

33
    if result.success?
2✔
34
      redirect_to collection_subjects_path(@collection.owner, @collection)
2✔
35
    else
×
36
      flash.alert = result.message
×
37
      redirect_to collection_article_show_path(@collection.owner, @collection, @article.id)
×
38
    end
39
  end
40

41
  def edit
1✔
42
    @sex_autocomplete=@collection.articles.distinct.pluck(:sex).select { |e| !e.blank? }
24✔
43
    @race_description_autocomplete=@collection.articles.distinct.pluck(:race_description).select { |e| !e.blank? }
24✔
44
  end
45

46
  def update
1✔
47
    if params[:save]
8✔
48
      result = Article::Update.new(
5✔
49
        article: @article,
50
        article_params: article_params,
51
        user: current_user
52
      ).call
53

54
      if result.success?
5✔
55
        record_deed
4✔
56

57
        flash[:notice] = result.notice
4✔
58
        redirect_to collection_article_edit_path(@collection.owner, @collection, @article)
4✔
59
      else
1✔
60
        @article = result.article
1✔
61
        render :edit, status: :unprocessable_entity
1✔
62
      end
3✔
63
    elsif params[:autolink]
3✔
64
      @article.source_text = autolink(@article.source_text)
2✔
65

66
      flash[:notice] = t('.subjects_auto_linking')
2✔
67
      render :edit
2✔
68
    else
69
      # Default to redirect
1✔
70
      redirect_to collection_article_edit_path(@collection.owner, @collection, @article)
1✔
71
    end
72
  end
73

74
  def article_category
1✔
75
    categories = Category.where(id: params[:category_ids])
1✔
76
    @article.categories = categories
1✔
77
    @article.save!
1✔
78

79
    respond_to(&:turbo_stream)
1✔
80
  end
81

82
  def combine_duplicate
1✔
83
    Article::Combine.new(
3✔
84
      article: @article,
85
      from_article_ids: params[:from_article_ids],
86
      user: current_user
87
    ).call
88

89
    flash[:notice] = t('.selected_subjects_combined', title: @article.title)
3✔
90
    redirect_to collection_article_edit_path(@collection.owner, @collection, @article)
3✔
91
  end
92

93
  def graph
1✔
NEW
94
    redirect_to action: :show, article_id: @article.id
×
95
  end
96

97
  def relationship_graph
1✔
98
    unless File.exist? @article.d3js_file
1!
99
      article_links=[]
1✔
100
      article_nodes=[]
1✔
101
      # get all the source article links
102
      @article.source_article_links.each do |link|
1✔
103
        article_nodes << link.target_article
1✔
104
        article_links << link.target_article_id
1✔
105
      end
106
      # get all the source article links
107
      @article.target_article_links.each do |link|
1✔
108
        article_nodes << link.source_article
×
109
        article_links << link.source_article_id
×
110
      end
111
      document_nodes=[]
1✔
112
      work_nodes=[]
1✔
113
      center_article_to_document_links=[]
1✔
114
      second_document_to_article_links=[]
1✔
115
      # get all the pages and works linking to this article
116
      if @collection.pages_are_meaningful?
1✔
117
        document_association = @article.pages
1✔
118
      else
×
119
        document_association = @article.works
×
120
      end
121

122
      document_association.each do |document|
1✔
123
        document_nodes << document
1✔
124
        center_article_to_document_links << document.id
1✔
125
        document.articles.each do |article|
1✔
126
          article_nodes << article
2✔
127
          second_document_to_article_links << [ document.id, article.id ]
2✔
128
        end
129
      end
130

131
      # now construct the JSON response
132
      nodes=[]
1✔
133
      nodes << {
1✔
134
        'id' => "S#{@article.id}",
135
        'title' => @article.title,
136
        'group' => @article.title,
137
        'link' => collection_article_show_url(@collection.owner, @collection, @article) }
138
      article_nodes.uniq.each do |article|
1✔
139
        if article != @article
2✔
140
          nodes << {
1✔
141
            'id' => "S#{article.id}",
142
            'title' => article.title,
143
            'bio' => xml_to_html(article.xml_text, false, nil, nil, nil, true),
144
            'group' => article.categories.first&.title,
1!
145
            'link' => collection_article_show_url(@collection.owner, @collection, article)
146
          }
147
        end
148
      end
149
      if @collection.pages_are_meaningful?
1✔
150
        document_nodes.uniq.each do |page|
1✔
151
          nodes << {
1✔
152
            'id' => "D#{page.id}",
153
            'title' => page.title + ' in ' + page.work.title,
154
            'group' => 'Documents',
155
            'link' => collection_display_page_path(@collection.owner, @collection, page.work, page)
156
          }
157
        end
158
      else
×
159
        document_nodes.uniq.each do |work|
×
160
          nodes << {
×
161
            'id' => "D#{work.id}",
162
            'title' => work.title,
163
            'group' => 'Documents',
164
            'link' => collection_read_work_url(@collection.owner, @collection, work)
165
          }
166
        end
167
      end
168

169
      links=[]
1✔
170
      article_links.tally.each do |article_id, link_count|
1✔
171
        links << {
1✔
172
          'source'=>"S#{@article.id}",
173
          'target'=>"S#{article_id}",
174
          'value'=>link_count,
175
          'group'=>'direct'
176
        }
177
      end
178
      center_article_to_document_links.tally.each do |work_id, link_count|
1✔
179
        links << {
1✔
180
          'source'=>"S#{@article.id}",
181
          'target'=>"D#{work_id}",
182
          'value'=>link_count,
183
          'group'=>'mentioned in'
184
        }
185
      end
186
      second_document_to_article_links.tally.each do |link_pair, link_count|
1✔
187
        work_id, second_article_id = link_pair
2✔
188
        links << {
2✔
189
          'source'=>"D#{work_id}",
190
          'target'=>"S#{second_article_id}",
191
          'value'=>link_count,
192
          'group'=>'mentions'
193
        }
194
      end
195

196
      doc={ 'nodes' => nodes, 'links' => links }
1✔
197
      File.write(@article.d3js_file, doc.to_json)
1✔
198
    end
199

200
    # now render the d3js file
201
    render file: @article.d3js_file, type: 'application/javascript; charset=utf-8', layout: false
1✔
202
  end
203

204

205
  def show
1✔
206
    sql =
207
      'SELECT count(*) as link_count, '+
13✔
208
      'a.title as title, '+
209
      'a.id as article_id '+
210
      'FROM page_article_links to_links '+
211
      'INNER JOIN page_article_links from_links '+
212
      '  ON to_links.page_id = from_links.page_id '+
213
      'INNER JOIN articles a '+
214
      '  ON from_links.article_id = a.id '+
215
      "WHERE to_links.article_id = #{@article.id} "+
216
      " AND from_links.article_id != #{@article.id} "
217
    sql += 'GROUP BY a.title, a.id '
13✔
218
    logger.debug(sql)
13✔
219
    article_links = Article.connection.select_all(sql)
13✔
220
    link_total = 0
13✔
221
    link_max = 0
13✔
222
    count_per_rank = { 0 => 0 }
13✔
223
    article_links.each do |l|
13✔
224
      link_count = l['link_count'].to_i
2✔
225
      link_total += link_count
2✔
226
      link_max = [ link_count, link_max ].max
2✔
227

228
      count_per_rank[link_count] ||= 0
2✔
229
      count_per_rank[link_count] += 1
2✔
230
    end
231

232
    min_rank = 0
13✔
233
    # now we know how many articles each link count has, as well as the size
234
    if params[:min_rank]
13✔
235
      # use the min rank from the params
×
236
      min_rank = params[:min_rank].to_i
×
237
    else
238
      # calculate whether we should reduce the rank
13✔
239
      num_articles = article_links.count
13✔
240
      while num_articles > DEFAULT_ARTICLES_PER_GRAPH && min_rank < link_max
13✔
241
        # remove the outer rank
×
242
        num_articles -= count_per_rank[min_rank] || 0 # hash is sparse
×
243
        min_rank += 1
×
244
        logger.debug("DEBUG: \tnum articles now #{num_articles}\n")
×
245
      end
246
    end
247

248
    dot_source = render_to_string(
13✔
249
      partial: 'graph',
250
      layout: false,
251
      locals: {
252
        article_links: article_links,
253
        link_total: link_total,
254
        link_max: link_max,
255
        min_rank: min_rank
256
      },
257
      formats: [ :dot ]
258
    )
259

260
    dot_file = "#{Rails.root}/public/images/working/dot/#{@article.id}.dot"
13✔
261
    File.open(dot_file, 'w') do |f|
13✔
262
      f.write(dot_source)
13✔
263
    end
264
    dot_out = "#{Rails.root}/public/images/working/dot/#{@article.id}.png"
13✔
265
    dot_out_map = "#{Rails.root}/public/images/working/dot/#{@article.id}.map"
13✔
266

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

269
    @map = File.read(dot_out_map)
13✔
270
    @article.graph_image = dot_out
13✔
271
    @article.save!
13✔
272
    session[:col_id] = @collection.slug
13✔
273
  end
274

275
  # display the article upload form
276
  def upload_form
1✔
277
  end
278

279
  # actually process the uploaded CSV
280
  def subject_upload
1✔
281
    @collection = Collection.find params[:upload][:collection_id]
×
282
    # read the file
283
    file = params[:upload][:file].tempfile
×
284

285
    # csv = CSV.read(params[:upload][:file].tempfile, :headers => true)
286
    begin
NEW
287
      csv = CSV.read(params[:upload][:file].tempfile, headers: true)
×
288
    rescue
289
      contents = File.read(params[:upload][:file].tempfile)
×
290
      detection = CharlockHolmes::EncodingDetector.detect(contents)
×
291

292
      csv = CSV.read(params[:upload][:file].tempfile,
×
293
                      encoding: "bom|#{detection[:encoding]}",
294
                      liberal_parsing: true,
295
                      headers: true)
296
    end
297

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

300
    # check the values
301
    if csv.headers.include?('HEADING') && csv.headers.include?('URI') && csv.headers.include?('ARTICLE') && csv.headers.include?('CATEGORY')
×
302
      # create subjects if heading checks out
×
303
      csv.each do |row|
×
304
        title = row['HEADING']
×
NEW
305
        article = @collection.articles.where(title: title).first || Article.new(title: title, provenance: provenance)
×
306
        article.collection = @collection
×
307
        article.source_text = row['ARTICLE']
×
308
        article.uri = row['URI']
×
309
        article.categories << find_or_create_category(@collection, row['CATEGORY'])
×
310
        article.save!
×
311
      end
312
      # redirect to subject list
313
      redirect_to collection_subjects_path(@collection.owner, @collection)
×
314
    else
315
      # flash message and redirect to upload form on problems
×
316
      flash[:error] = t('.csv_file_must_contain_headers')
×
317
      redirect_to article_upload_form_path(@collection)
×
318
    end
319
  end
320

321
  def upload_example
1✔
322
    example = File.read(File.join(Rails.root, 'app', 'views', 'static', 'subject_example.csv'))
×
NEW
323
    send_data example, filename: 'subject_example.csv'
×
324
  end
325

326
  protected
1✔
327

328
  def record_deed
1✔
329
    deed = Deed.new
4✔
330
    deed.article = @article
4✔
331
    deed.deed_type = DeedType::ARTICLE_EDIT
4✔
332
    deed.collection = @article.collection
4✔
333
    deed.user = current_user
4✔
334
    deed.save!
4✔
335
    update_search_attempt_contributions
4✔
336
  end
337

338
  private
1✔
339

340
  def authorized?
1✔
341
    redirect_to dashboard_path unless user_signed_in?
30✔
342
  end
343

344
  def article_params
1✔
345
    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✔
346
  end
347

348
  def sort_vertically(articles)
1✔
349
    return [] unless articles.any?
74✔
350

351
    rows = (articles.length.to_f / LIST_NUM_COLUMNS).ceil
52✔
352
    vertical_articles = Array.new(rows) { Array.new(LIST_NUM_COLUMNS) }
105✔
353

354
    articles.each_with_index do |article, index|
52✔
355
      row = index % rows
87✔
356
      col = index / rows
87✔
357
      vertical_articles[row][col] = article
87✔
358
    end
359

360
    vertical_articles
52✔
361
  end
362

363
  def find_or_create_category(collection, title)
1✔
364
    category = collection.categories.where(title: title).first
×
365
    if category.nil?
×
366
      category = Category.new(title: title)
×
367
      collection.categories << category
×
368
    end
369

370
    category
×
371
  end
372
end
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