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

benwbrum / fromthepage / 15785795551

20 Jun 2025 06:53PM UTC coverage: 60.567% (-0.2%) from 60.807%
15785795551

push

github

web-flow
CWGK migration code and related subject graph for #4705 (#4706)

1618 of 3266 branches covered (49.54%)

Branch coverage included in aggregate %.

17 of 70 new or added lines in 7 files covered. (24.29%)

7424 of 11663 relevant lines covered (63.65%)

78.3 hits per line

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

55.29
/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'
1✔
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
      user: current_user,
30
      collection: @collection
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
      ).call
52

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

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

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

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

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

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

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

91
  def graph
1✔
92
    redirect_to :action => :show, :article_id => @article.id
×
93
  end
94

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

NEW
120
      document_association.each do |document|
×
NEW
121
        document_nodes << document
×
NEW
122
        center_article_to_document_links << document.id
×
NEW
123
        document.articles.each do |article|
×
NEW
124
          article_nodes << article
×
NEW
125
          second_document_to_article_links << [document.id, article.id]
×
126
        end
127
      end
128

129
      # replace with above
130
      # @article.page_article_links.each do |link|
131
      #   page_nodes << link.page
132
      #   work_nodes << link.page.work
133
      #   # now walk the network to find coocurrences
134
      #   center_article_to_page_links<<link.page_id
135
      #   center_article_to_work_links<<link.page.work_id
136
      #   link.page.page_article_links.each do |second_link|
137
      #     article_nodes << second_link.article
138
      #     second_page_to_article_links<<[second_link.page.id, second_link.article_id]
139
      #     second_work_to_article_links<<[second_link.page.work_id, second_link.article_id]
140
      #   end
141
      # end
142

143
      # now construct the JSON response
NEW
144
      nodes=[]
×
NEW
145
      nodes << {
×
146
        "id" => "S#{@article.id}", 
147
        "title" => @article.title, 
148
        "group" => @article.title, 
149
        "link" => collection_article_show_url(@collection.owner, @collection, @article)}
NEW
150
      article_nodes.uniq.each do |article|
×
NEW
151
        if article != @article
×
NEW
152
          nodes << {
×
153
            "id" => "S#{article.id}", 
154
            "title" => article.title,
155
            "bio" => xml_to_html(article.xml_text, false, nil, nil, nil, true),
156
            "group" => article.categories.first&.title, 
×
157
            "link" => collection_article_show_url(@collection.owner, @collection, article)
158
          }
159
        end
160
      end
NEW
161
      if @collection.pages_are_meaningful?
×
NEW
162
        document_nodes.uniq.each do |page|
×
NEW
163
          nodes << {
×
164
            "id" => "D#{page.id}", 
165
            "title" => page.title + " in " + page.work.title, 
166
            "group" => "Documents",
167
            "link" => collection_display_page_path(@collection.owner, @collection, page.work, page)
168
          }
169
        end
170
      else
×
NEW
171
        document_nodes.uniq.each do |work|
×
NEW
172
          nodes << {
×
173
            "id" => "D#{work.id}", 
174
            "title" => work.title, 
175
            "group" => "Documents",
176
            "link" => collection_read_work_url(@collection.owner, @collection, work)
177
          }
178
        end
179
      end
180
  
181
      # TODO decide on page vs. work here
182
      # work_nodes.uniq.each do |work|
183
      #   nodes << {
184
      #     "id" => "W#{work.id}", 
185
      #     "title" => work.title, 
186
      #     "group" => "Documents",
187
      #     "link" => collection_read_work_url(@collection.owner, @collection, work)
188
      #   }
189
      # end
NEW
190
      links=[]
×
NEW
191
      article_links.tally.each do |article_id,link_count|
×
NEW
192
        links << {
×
193
          "source"=>"S#{@article.id}", 
194
          "target"=>"S#{article_id}", 
195
          "value"=>link_count, 
196
          "group"=>"direct"
197
        }
198
      end
NEW
199
      center_article_to_document_links.tally.each do |work_id, link_count|
×
NEW
200
        links << {
×
201
          "source"=>"S#{@article.id}", 
202
          "target"=>"D#{work_id}", 
203
          "value"=>link_count, 
204
          "group"=>"mentioned in"
205
        }
206
      end
NEW
207
      second_document_to_article_links.tally.each do |link_pair, link_count|
×
NEW
208
        work_id,second_article_id = link_pair
×
NEW
209
        links << {
×
210
          "source"=>"D#{work_id}", 
211
          "target"=>"S#{second_article_id}", 
212
          "value"=>link_count, 
213
          "group"=>"mentions"
214
        }
215
      end
216

NEW
217
      doc={ "nodes" => nodes, "links" => links}
×
NEW
218
      File.write(@article.d3js_file, doc.to_json)
×
219
    end
220

NEW
221
    send_file @article.d3js_file,
×
222
      type:        "application/javascript; charset=utf-8",
223
      disposition: "inline"
224
  end
225

226

227
  def show
1✔
228
    sql =
229
      'SELECT count(*) as link_count, '+
13✔
230
      'a.title as title, '+
231
      'a.id as article_id '+
232
      'FROM page_article_links to_links '+
233
      'INNER JOIN page_article_links from_links '+
234
      '  ON to_links.page_id = from_links.page_id '+
235
      'INNER JOIN articles a '+
236
      '  ON from_links.article_id = a.id '+
237
      "WHERE to_links.article_id = #{@article.id} "+
238
      " AND from_links.article_id != #{@article.id} "
239
    sql += "GROUP BY a.title, a.id "
13✔
240
    logger.debug(sql)
13✔
241
    article_links = Article.connection.select_all(sql)
13✔
242
    link_total = 0
13✔
243
    link_max = 0
13✔
244
    count_per_rank = { 0 => 0 }
13✔
245
    article_links.each do |l|
13✔
246
      link_count = l['link_count'].to_i
2✔
247
      link_total += link_count
2✔
248
      link_max = [link_count, link_max].max
2✔
249

250
      count_per_rank[link_count] ||= 0
2✔
251
      count_per_rank[link_count] += 1
2✔
252
    end
253

254
    min_rank = 0
13✔
255
    # now we know how many articles each link count has, as well as the size
256
    if params[:min_rank]
13✔
257
      # use the min rank from the params
×
258
      min_rank = params[:min_rank].to_i
×
259
    else
260
      # calculate whether we should reduce the rank
13✔
261
      num_articles = article_links.count
13✔
262
      while num_articles > DEFAULT_ARTICLES_PER_GRAPH && min_rank < link_max
13✔
263
        # remove the outer rank
×
264
        num_articles -= count_per_rank[min_rank] || 0 # hash is sparse
×
265
        min_rank += 1
×
266
        logger.debug("DEBUG: \tnum articles now #{num_articles}\n")
×
267
      end
268
    end
269

270
    dot_source =
271
      render_to_string(:partial => "graph.dot",
13✔
272
                       :layout => false,
273
                       :locals => { :article_links => article_links,
274
                                    :link_total => link_total,
275
                                    :link_max => link_max,
276
                                    :min_rank => min_rank })
277

278
    dot_file = "#{Rails.root}/public/images/working/dot/#{@article.id}.dot"
13✔
279
    File.open(dot_file, "w") do |f|
13✔
280
      f.write(dot_source)
13✔
281
    end
282
    dot_out = "#{Rails.root}/public/images/working/dot/#{@article.id}.png"
13✔
283
    dot_out_map = "#{Rails.root}/public/images/working/dot/#{@article.id}.map"
13✔
284

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

287
    @map = File.read(dot_out_map)
13✔
288
    @article.graph_image = dot_out
13✔
289
    @article.save!
13✔
290
    session[:col_id] = @collection.slug
13✔
291
  end
292

293
  # display the article upload form
294
  def upload_form
1✔
295
  end
296

297
  # actually process the uploaded CSV
298
  def subject_upload
1✔
299
    @collection = Collection.find params[:upload][:collection_id]
×
300
    # read the file
301
    file = params[:upload][:file].tempfile
×
302

303
    # csv = CSV.read(params[:upload][:file].tempfile, :headers => true)
304
    begin
305
      csv = CSV.read(params[:upload][:file].tempfile, :headers=>true)
×
306
    rescue
307
      contents = File.read(params[:upload][:file].tempfile)
×
308
      detection = CharlockHolmes::EncodingDetector.detect(contents)
×
309

310
      csv = CSV.read(params[:upload][:file].tempfile,
×
311
                      :encoding => "bom|#{detection[:encoding]}",
312
                      :liberal_parsing => true,
313
                      :headers => true)
314
    end
315

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

318
    # check the values
319
    if csv.headers.include?('HEADING') && csv.headers.include?('URI') && csv.headers.include?('ARTICLE') && csv.headers.include?('CATEGORY')
×
320
      # create subjects if heading checks out
×
321
      csv.each do |row|
×
322
        title = row['HEADING']
×
323
        article = @collection.articles.where(:title => title).first || Article.new(:title => title, :provenance => provenance)
×
324
        article.collection = @collection
×
325
        article.source_text = row['ARTICLE']
×
326
        article.uri = row['URI']
×
327
        article.categories << find_or_create_category(@collection, row['CATEGORY'])
×
328
        article.save!
×
329
      end
330
      # redirect to subject list
331
      redirect_to collection_subjects_path(@collection.owner, @collection)
×
332
    else
333
      # flash message and redirect to upload form on problems
×
334
      flash[:error] = t('.csv_file_must_contain_headers')
×
335
      redirect_to article_upload_form_path(@collection)
×
336
    end
337
  end
338

339
  def upload_example
1✔
340
    example = File.read(File.join(Rails.root, 'app', 'views', 'static', 'subject_example.csv'))
×
341
    send_data example, filename: "subject_example.csv"
×
342
  end
343

344
  protected
1✔
345

346
  def record_deed
1✔
347
    deed = Deed.new
4✔
348
    deed.article = @article
4✔
349
    deed.deed_type = DeedType::ARTICLE_EDIT
4✔
350
    deed.collection = @article.collection
4✔
351
    deed.user = current_user
4✔
352
    deed.save!
4✔
353
    update_search_attempt_contributions
4✔
354
  end
355

356
  private
1✔
357

358
  def authorized?
1✔
359
    redirect_to dashboard_path unless user_signed_in?
30✔
360
  end
361

362
  def article_params
1✔
363
    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✔
364
  end
365

366
  def sort_vertically(articles)
1✔
367
    return [] unless articles.any?
74✔
368

369
    rows = (articles.length.to_f / LIST_NUM_COLUMNS).ceil
52✔
370
    vertical_articles = Array.new(rows) { Array.new(LIST_NUM_COLUMNS) }
105✔
371

372
    articles.each_with_index do |article, index|
52✔
373
      row = index % rows
87✔
374
      col = index / rows
87✔
375
      vertical_articles[row][col] = article
87✔
376
    end
377

378
    vertical_articles
52✔
379
  end
380

381
  def find_or_create_category(collection, title)
1✔
382
    category = collection.categories.where(title: title).first
×
383
    if category.nil?
×
384
      category = Category.new(title: title)
×
385
      collection.categories << category
×
386
    end
387

388
    category
×
389
  end
390
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