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

benwbrum / fromthepage / 18198125262

02 Oct 2025 03:43PM UTC coverage: 65.028%. Remained the same
18198125262

push

github

web-flow
Rubocop lint remove array whitespaces (#4950)

1848 of 3359 branches covered (55.02%)

Branch coverage included in aggregate %.

154 of 201 new or added lines in 62 files covered. (76.62%)

8087 of 11919 relevant lines covered (67.85%)

109.57 hits per line

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

78.15
/app/controllers/dashboard_controller.rb
1
# frozen_string_literal: true
2

3
class DashboardController < ApplicationController
1✔
4
  protect_from_forgery except: [:new_upload]
1✔
5

6
  include AddWorkHelper
1✔
7
  include DashboardHelper
1✔
8
  include OwnerExporter
1✔
9
  PAGES_PER_SCREEN = 20
1✔
10

11
  before_action :authorized?,
1✔
12
    only: [:owner, :staging, :startproject, :summary]
13

14
  before_action :get_data,
1✔
15
    only: [:owner, :staging, :upload, :new_upload,
16
           :startproject, :empty_work, :create_work, :summary, :exports]
17

18
  before_action :remove_col_id
1✔
19

20
  def dashboard_role
1✔
21
    if user_signed_in?
12✔
22
      if current_user.owner
11✔
23
        redirect_to dashboard_owner_path
8✔
24
      elsif current_user.guest?
3✔
25
        redirect_to guest_dashboard_path
1✔
26
      else
2✔
27
        redirect_to dashboard_watchlist_path
2✔
28
      end
29
    else
1✔
30
      redirect_to guest_dashboard_path
1✔
31
    end
32
  end
33

34
  def index
1✔
35
    if Collection.all.count > 1000
22✔
36
      redirect_to landing_page_path
1✔
37
    else
21✔
38
      redirect_to collections_list_path
21✔
39
    end
40
  end
41

42
  def collections_list(private_only: false)
1✔
43
    if private_only
41✔
44
      cds = []
18✔
45
    else
23✔
46
      public_collections   = Collection.unrestricted.includes(:owner, next_untranscribed_page: :work)
23✔
47
      public_document_sets = DocumentSet.unrestricted.includes(:owner, next_untranscribed_page: :work)
23✔
48

49
      cds = public_collections + public_document_sets
23✔
50
    end
51

52
    if user_signed_in?
41✔
53
      cds |= current_user.all_owner_collections.restricted.includes(:owner, next_untranscribed_page: :work)
39✔
54
      cds |= current_user.document_sets.restricted.includes(:owner, next_untranscribed_page: :work)
39✔
55

56
      cds |= current_user.collection_collaborations.includes(:owner, next_untranscribed_page: :work)
39✔
57
      cds |= current_user.document_set_collaborations.includes(:owner, next_untranscribed_page: :work)
39✔
58
    end
59

60
    @collections_and_document_sets = cds.sort { |a, b| a.slug <=> b.slug }
922✔
61
  end
62

63
  # Owner Dashboard - start project
64
  # other methods in AddWorkHelper
65
  def startproject
1✔
66
    @work = Work.new
30✔
67
    @work.collection = @collection
30✔
68
    @document_upload = DocumentUpload.new
30✔
69
    @document_upload.collection = @collection
30✔
70
    @sc_collections = ScCollection.all
30✔
71
  end
72

73
  def your_hours
1✔
74
    unless user_signed_in?
3✔
75
      redirect_to landing_page_path
1✔
76
      return
1✔
77
    end
78

79
    load_user_hours_data
2✔
80

81
    return unless @start_date_hours > @end_date_hours
2✔
82

83
    flash[:error] = 'Invalid date range. Please make sure the end date is greater than the start date.'
1✔
84
    redirect_to dashboard_your_hours_path
1✔
85
  end
86

87
  def download_hours_letter
1✔
88
    load_user_hours_data
×
89
    @time_duration=params[:time_duration]
×
90
    markdown_text = generate_markdown_text
×
91

92
    # write the string to a temp directory
93
    temp_dir = File.join(Rails.root, 'public', 'printable')
×
94
    Dir.mkdir(temp_dir) unless Dir.exist? temp_dir
×
95

96
    time_stub = Time.now.gmtime.iso8601.gsub(/\D/, '')
×
97
    temp_dir = File.join(temp_dir, time_stub)
×
98
    Dir.mkdir(temp_dir) unless Dir.exist? temp_dir
×
99

100
    file_stub = "letter_#{time_stub}"
×
101
    md_file = File.join(temp_dir, "#{file_stub}.md")
×
102
    output_file = File.join(temp_dir, "#{file_stub}.pdf")
×
103

104
    generate_pdf(md_file, output_file, markdown_text)
×
105

106
    send_generated_pdf(output_file)
×
107
  end
108

109
  # Owner Dashboard - list of works
110
  def owner
1✔
111
    collections = current_user.all_owner_collections
70✔
112
    @active_collections = @collections.select { |c| c.active? }
223✔
113
    @inactive_collections = @collections.select { |c| !c.active? }
223✔
114
    # Needs to be active collections first, then inactive collections
115
    @collections = @active_collections + @inactive_collections
70✔
116
  end
117

118
  # Owner Summary Statistics - statistics for all owned collections
119
  def summary
1✔
120
    start_d = params[:start_date]
5✔
121
    end_d = params[:end_date]
5✔
122

123
    max_date = 1.day.ago
5✔
124

125
    # Give a week fo data if there are no dates
126
    @start_date = start_d&.to_datetime&.beginning_of_day || 1.week.ago.beginning_of_day
5✔
127
    @end_date = end_d&.to_datetime&.end_of_day || max_date
5✔
128
    @end_date = max_date if max_date < @end_date
5!
129

130
    @statistics_object = current_user
5✔
131
    @subjects_disabled = @statistics_object.collections.all?(&:subjects_disabled)
5✔
132

133
    # Stats
134
    owner_collections = current_user.all_owner_collections.map { |c| c.id }
12✔
135
    contributor_ids_for_dates = AhoyActivitySummary
5✔
136
        .where(collection_id: owner_collections)
137
        .where('date BETWEEN ? AND ?', @start_date, @end_date).distinct.pluck(:user_id)
138

139
    @contributors = User.where(id: contributor_ids_for_dates).order(:display_name)
5✔
140

141
    @activity = AhoyActivitySummary
5✔
142
        .where(collection_id: owner_collections)
143
        .where('date BETWEEN ? AND ?', @start_date, @end_date)
144
        .group(:user_id)
145
        .sum(:minutes)
146
  end
147

148
  # Collaborator Dashboard - watchlist
149
  def watchlist
1✔
150
    works = Work.joins(:deeds).where(deeds: { user_id: current_user.id }).distinct
18✔
151
    recent_collections = Collection.joins(:deeds).where(deeds: { user_id: current_user.id }).where('deeds.created_at > ?', Time.now-2.days).distinct.order_by_recent_activity.limit(5)
18✔
152
    collections = Collection.where(id: current_user.ahoy_activity_summaries.pluck(:collection_id)).distinct.order_by_recent_activity.limit(5)
18✔
153
    document_sets = DocumentSet.joins(works: :deeds).where(works: { id: works.ids }).order('deeds.created_at DESC').distinct.limit(5)
18✔
154
    collections_list(private_only: true) # assigns @collections_and_document_sets for private collections only
18✔
155
    @collections = (collections + recent_collections + document_sets)
18✔
156
               .uniq
157
               .sort_by do |collection|
158
                 if collection.is_a?(Collection)
36✔
159
                   collection.created_on
27✔
160
                 elsif collection.is_a?(DocumentSet)
9!
161
                   collection.created_at
9✔
162
                 end
163
               end
164
               .reverse
165
               .take(10)
166
  end
167

168
  def exports
1✔
169
    @bulk_exports = current_user.bulk_exports.order('id DESC').paginate page: params[:page], per_page: PAGES_PER_SCREEN
2✔
170
  end
171

172
  # Collaborator Dashboard - activity
173
  def editor
1✔
174
    @user = current_user
×
175
  end
176

177
  # Guest Dashboard - activity
178
  def guest
1✔
179
    @collections = Collection.order_by_recent_activity.unrestricted.distinct.limit(5)
1✔
180
  end
181

182
  def landing_page
1✔
183
    if ELASTIC_ENABLED && params[:search].present?
16✔
184
      @es_query = Elasticsearch::MultiQuery.new(
4✔
185
        query: params[:search],
186
        query_params: {
187
          org: params[:org],
188
          mode: params[:mode],
189
          slug: params[:slug]
190
        },
191
        page: params[:page] || 1,
192
        scope: params[:filter],
193
        user: current_user
194
      ).call
195

196
      @breadcrumb_scope = { site: true }
4✔
197
      @org_filter = @es_query.org_filter
4✔
198
      @collection_filter = @es_query.collection_filter
4✔
199
      @docset_filter = @es_query.docset_filter
4✔
200
      @work_filter = @es_query.work_filter
4✔
201

202
      @search_results = @es_query.results
4✔
203
      @full_count = @es_query.total_count
4✔
204
      @type_counts = @es_query.type_counts
4✔
205
    elsif params[:search].present?
12✔
206
      @search_results = search_results(params[:search])&.paginate(page: params[:page], per_page: PAGES_PER_SCREEN)
3!
207
      @search_results_map = {}
3✔
208
      @search_results.each do |result|
3✔
209
        @search_results_map[result.owner_user_id] ||= []
3✔
210
        @search_results_map[result.owner_user_id] << result
3✔
211
      end
212
    end
213

214
    users = User.owners.joins(:collections).left_outer_joins(:document_sets)
16✔
215

216
    org_owners = users.findaproject_orgs.with_owner_works
16✔
217
    individual_owners = users.findaproject_individuals.with_owner_works
16✔
218
    @owners = users.where(id: org_owners.select(:id)).or(users.where(id: individual_owners.select(:id))).distinct
16✔
219
                   .order(Arel.sql("COALESCE(NULLIF(display_name, ''), login) ASC"))
220
    @collections = Collection.where(owner_user_id: users.select(:id)).unrestricted
16✔
221

222
    new_collections = Collection.includes(:owner)
16✔
223
                                .featured_projects
224
                                .order(featured_at: :desc)
225
                                .limit(20)
226

227
    new_document_sets = DocumentSet.includes(:owner)
16✔
228
                                   .featured_projects
229
                                   .order(featured_at: :desc)
230
                                   .limit(20)
231

232
    respond_to do |format|
16✔
233
      format.html do
16✔
234
        @new_projects = (new_collections + new_document_sets).sort_by(&:featured_at).reverse
13✔
235
        @tag_map = Tag.featured_tags.group(:ai_text).count
13✔
236
      end
237

238
      format.turbo_stream
16✔
239
    end
240
  end
241

242
  def browse_tag
1✔
243
    @tag = Tag.find_by(ai_text: params[:ai_text])
1✔
244
    tag_collections = @tag.collections
1✔
245
    collections = tag_collections.includes(:owner, { works: :work_statistic })
1✔
246
                                 .unrestricted
247
                                 .has_intro_block
248
                                 .not_empty
249

250
    document_sets = DocumentSet.includes(:owner, :collection, { works: :work_statistic })
1✔
251
                               .where(collection: { id: tag_collections.select(:id) })
252
                               .unrestricted
253
                               .has_intro_block
254
                               .not_empty
255

256
    @collections = collections + document_sets
1✔
257
  end
258

259
  def collaborator_time_export
1✔
260
    start_date = params[:start_date]
×
261
    end_date = params[:end_date]
×
262

263
    start_date = start_date.to_date
×
264
    end_date = end_date.to_date
×
265

266
    dates = (start_date..end_date)
×
267

268
    headers = [
×
269
      'Username',
270
      'Email'
271
    ]
272

273
    headers += dates.map { |d| d.strftime('%b %d, %Y') }
×
274

275
    # Get Row Data (Users)
276
    owner_collections = current_user.all_owner_collections.map { |c| c.id }
×
277

278
    contributor_ids_for_dates = AhoyActivitySummary
×
279
      .where(collection_id: owner_collections)
280
      .where('date BETWEEN ? AND ?', start_date, end_date).distinct.pluck(:user_id)
281

282
    contributors = User.where(id: contributor_ids_for_dates).order(:display_name)
×
283

284
    csv = CSV.generate(headers: true) do |records|
×
285
      records << headers
×
286
      contributors.each do |user|
×
NEW
287
        row = [user.display_name, user.email]
×
288

289
        activity = AhoyActivitySummary
×
290
          .where(user_id: user.id)
291
          .where(collection_id: owner_collections)
292
          .where('date BETWEEN ? AND ?', start_date, end_date)
293
          .group(:date)
294
          .sum(:minutes)
295
          .transform_keys { |k| k.to_date }
×
296

297
        user_activity = dates.map { |d| activity[d.to_date] || 0 }
×
298

299
        row += user_activity
×
300

301
        records << row
×
302
      end
303
    end
304

305
    send_data(csv,
×
306
              filename: "#{start_date.strftime('%Y-%m%b-%d')}-#{end_date.strftime('%Y-%m%b-%d')}_activity_summary.csv",
307
              type: 'application/csv')
308
  end
309

310
  private
1✔
311

312
  def authorized?
1✔
313
    return if user_signed_in? && current_user.owner
109✔
314

315
    redirect_to dashboard_path
4✔
316
  end
317

318
  def document_upload_params
1✔
319
    params.require(:document_upload).permit(:document_upload, :file, :preserve_titles, :ocr, :collection_id)
4✔
320
  end
321

322
  def load_user_hours_data
1✔
323
    if params['start_date'].present? && params['end_date'].present?
2✔
324
      @start_date_hours = Date.parse(params['start_date'])
1✔
325
      @end_date_hours = Date.parse(params['end_date'])
1✔
326
    else
1✔
327
      @start_date_hours = 7.days.ago.to_date
1✔
328
      @end_date_hours = Date.today
1✔
329
    end
330
    @time_duration = time_spent_in_date_range(current_user.id, @start_date_hours, @end_date_hours)
2✔
331

332
    last_summary = AhoyActivitySummary.where(user_id: current_user.id)
2✔
333
                                      .where.not(collection_id: nil)
334
                                      .order(date: :desc)
335
                                      .first
336
    @last_calculation_date = last_summary&.date&.end_of_day
2!
337

338
    raw = Deed.where(user_id: current_user.id, created_at: [@start_date_hours..@end_date_hours]).pluck(:collection_id, :page_id).uniq
2✔
339
    @collection_id_to_page_count = raw.select { |collection_id, page_id| !page_id.nil? }.map { |collection_id, page_id| collection_id }.tally
2✔
340
    @user_collections = Collection.find(@collection_id_to_page_count.keys).sort { |a, b| a.owner.display_name <=> b.owner.display_name }
2✔
341
  end
342

343
  def generate_markdown_text
1✔
344
    <<~MARKDOWN
×
345
      ![](app/assets/images/logo.png){width=300px style='display: block; margin-left: 300px auto;'}
346
      &nbsp; &nbsp;
347
      \n#{generated_format_date(Time.now.to_date)}\n
348
      &nbsp; &nbsp;
349
      \n#{I18n.t('dashboard.hours_letter.to_whom_it_may_concern')}\n
350
      #{I18n.t('dashboard.hours_letter.certification_text', user_name: current_user.real_name, time_duration: @time_duration, start_date: generated_format_date(@start_date_hours), end_date: generated_format_date(@end_date_hours))}\n
351
      #{I18n.t('dashboard.hours_letter.worked_on_collections', user_name: current_user.real_name)}\n
352
      #{I18n.t('dashboard.hours_letter.institutions_header')}
353
      #{I18n.t('dashboard.hours_letter.institutions_separator')}
354
      #{generate_collection_rows(@user_collections)}
355
      #{I18n.t('dashboard.hours_letter.volunteer_text', user_display_name: current_user.display_name)}\n
356
      |
357
      #{I18n.t('dashboard.hours_letter.regards_text')}\n
358
      |
359
      | Sara Brumfield
360
      | Partner, FromThePage
361
    MARKDOWN
362
  end
363

364
  def generate_collection_rows(user_collections)
1✔
365
    user_collections.map do |collection|
×
366
      "| #{collection.owner.display_name} | #{collection.title} | #{@collection_id_to_page_count[collection.id]} |"
×
367
    end.join("\n")
368
  end
369

370
  def generated_format_date(date)
1✔
371
    formatted_date = date.strftime('%B %d, %Y')
×
372
  end
373

374
  def generate_pdf(input_path, output_path, markdown_text)
1✔
375
    File.write(input_path, markdown_text)
×
376

377
    system("pandoc #{input_path} -s --pdf-engine=xelatex -o #{output_path}")
×
378
  end
379

380
  def send_generated_pdf(output_path)
1✔
381
    # spew the output to the browser
382
    send_data(File.read(output_path),
×
383
      filename: File.basename('letter.pdf'),
384
      content_type: 'application/pdf')
385
    cookies['download_finished'] = 'true'
×
386
  end
387

388
  def search_results(search_key)
1✔
389
    return nil if search_key.nil?
3!
390

391
    collections_query = Collection.search(search_key).unrestricted.includes(:owner)
3✔
392
    document_sets_query = DocumentSet.search(search_key).unrestricted.includes(:owner)
3✔
393

394
    collections_query + document_sets_query
3✔
395
  end
396
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