Skip to content

Commit 327e803

Browse files
authored
Merge branch 'main' into dependabot/bundler/rspec-rails-8.0.2
2 parents 47279c7 + f7ab3c5 commit 327e803

File tree

93 files changed

+3434
-216
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+3434
-216
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ group :test do
9898
# Capybara for integration testing
9999
gem 'capybara', '>= 2.15'
100100
gem 'capybara-screenshot'
101+
# WebMock for stubbing external HTTP requests in specs
102+
gem 'webmock'
101103
# Coveralls for test coverage reporting
102104
gem 'coveralls_reborn', require: false
103105
# Database cleaner for test database cleaning

Gemfile.lock

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ PATH
5757
reform-rails (>= 0.2, < 0.4)
5858
rswag (>= 2.3.1, < 2.17.0)
5959
ruby-openai
60+
sidekiq-scheduler
6061
simple_calendar
6162
sprockets-rails
6263
stackprof
@@ -229,6 +230,9 @@ GEM
229230
term-ansicolor (~> 1.7)
230231
thor (~> 1.2)
231232
tins (~> 1.32)
233+
crack (1.0.0)
234+
bigdecimal
235+
rexml
232236
crass (1.0.6)
233237
css_parser (1.21.1)
234238
addressable
@@ -297,6 +301,8 @@ GEM
297301
multi_json
298302
erb (5.0.2)
299303
erubi (1.13.1)
304+
et-orbi (1.3.0)
305+
tzinfo
300306
event_stream_parser (1.0.0)
301307
excon (1.2.8)
302308
logger
@@ -343,6 +349,9 @@ GEM
343349
friendly_id-mobility (1.0.4)
344350
friendly_id (>= 5.0.0, < 5.5)
345351
mobility (>= 1.0.1, < 2.0)
352+
fugit (1.11.2)
353+
et-orbi (~> 1, >= 1.2.11)
354+
raabro (~> 1.4)
346355
fuubar (2.5.1)
347356
rspec-core (~> 3.0)
348357
ruby-progressbar (~> 1.4)
@@ -362,6 +371,7 @@ GEM
362371
rake (>= 13)
363372
groupdate (6.7.0)
364373
activesupport (>= 7.1)
374+
hashdiff (1.2.0)
365375
hashie (5.0.0)
366376
highline (3.1.2)
367377
reline
@@ -521,6 +531,7 @@ GEM
521531
nio4r (~> 2.0)
522532
pundit (2.5.0)
523533
activesupport (>= 3.0.0)
534+
raabro (1.4.0)
524535
racc (1.8.1)
525536
rack (3.1.16)
526537
rack-attack (6.7.0)
@@ -699,6 +710,8 @@ GEM
699710
ffi (~> 1.12)
700711
logger
701712
rubyzip (3.0.1)
713+
rufus-scheduler (3.9.2)
714+
fugit (~> 1.1, >= 1.11.1)
702715
sass-embedded (1.86.3-aarch64-linux-gnu)
703716
google-protobuf (~> 4.30)
704717
sass-embedded (1.86.3-arm64-darwin)
@@ -732,6 +745,9 @@ GEM
732745
logger (>= 1.6.2)
733746
rack (>= 3.1.0)
734747
redis-client (>= 0.23.2)
748+
sidekiq-scheduler (6.0.1)
749+
rufus-scheduler (~> 3.2)
750+
sidekiq (>= 7.3, < 9)
735751
simple_calendar (3.1.0)
736752
rails (>= 6.1)
737753
simplecov (0.22.0)
@@ -801,6 +817,10 @@ GEM
801817
activemodel (>= 6.0.0)
802818
bindex (>= 0.4.0)
803819
railties (>= 6.0.0)
820+
webmock (3.25.1)
821+
addressable (>= 2.8.0)
822+
crack (>= 0.3.2)
823+
hashdiff (>= 0.4.0, < 2.0.0)
804824
websocket (1.2.11)
805825
websocket-driver (0.8.0)
806826
base64
@@ -873,6 +893,7 @@ DEPENDENCIES
873893
storext!
874894
uglifier (>= 1.3.0)
875895
web-console (>= 3.3.0)
896+
webmock
876897

877898
RUBY VERSION
878899
ruby 3.4.4p34
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# frozen_string_literal: true
2+
3+
module BetterTogether
4+
module Metrics
5+
# Controller for creating and downloading Link Checker reports
6+
class LinkCheckerReportsController < ApplicationController
7+
before_action :set_report, only: %i[download]
8+
9+
def index
10+
@link_checker_reports = BetterTogether::Metrics::LinkCheckerReport.order(created_at: :desc)
11+
12+
if request.headers['Turbo-Frame'].present?
13+
render partial: 'better_together/metrics/link_checker_reports/index',
14+
locals: { reports: @link_checker_reports },
15+
layout: false
16+
else
17+
render :index
18+
end
19+
end
20+
21+
def new
22+
@link_checker_report = BetterTogether::Metrics::LinkCheckerReport.new
23+
end
24+
25+
# rubocop:todo Metrics/AbcSize
26+
# rubocop:todo Metrics/MethodLength
27+
# rubocop:todo Metrics/BlockLength
28+
def create
29+
opts = {
30+
from_date: params.dig(:metrics_link_checker_report, :filters, :from_date),
31+
to_date: params.dig(:metrics_link_checker_report, :filters, :to_date),
32+
file_format: params.dig(:metrics_link_checker_report, :file_format) || 'csv'
33+
}
34+
35+
@link_checker_report = BetterTogether::Metrics::LinkCheckerReport.create_and_generate!(**opts)
36+
37+
respond_to do |format| # rubocop:todo Metrics/BlockLength
38+
if @link_checker_report.persisted?
39+
flash[:notice] = t('flash.generic.created', resource: t('resources.report'))
40+
format.html { redirect_to metrics_link_checker_reports_path, notice: flash[:notice] }
41+
format.turbo_stream do
42+
render turbo_stream: [
43+
turbo_stream.prepend(
44+
'link_checker_reports_table_body',
45+
partial: 'better_together/metrics/link_checker_reports/link_checker_report',
46+
locals: { link_checker_report: @link_checker_report }
47+
),
48+
turbo_stream.replace(
49+
'flash_messages',
50+
partial: 'layouts/better_together/flash_messages',
51+
locals: { flash: flash }
52+
),
53+
turbo_stream.replace('new_link_checker_report',
54+
'<turbo-frame id="new_link_checker_report"></turbo-frame>')
55+
]
56+
end
57+
else
58+
flash.now[:alert] = t('flash.generic.error_create', resource: t('resources.report'))
59+
format.html { render :new, status: :unprocessable_content }
60+
format.turbo_stream do
61+
render turbo_stream: [
62+
turbo_stream.update('form_errors', partial: 'layouts/errors', locals: { object: @link_checker_report }),
63+
turbo_stream.replace(
64+
'flash_messages',
65+
partial: 'layouts/better_together/flash_messages',
66+
locals: { flash: flash }
67+
)
68+
]
69+
end
70+
end
71+
end
72+
end
73+
# rubocop:enable Metrics/BlockLength
74+
# rubocop:enable Metrics/MethodLength
75+
# rubocop:enable Metrics/AbcSize
76+
77+
# rubocop:todo Metrics/AbcSize
78+
# rubocop:todo Metrics/MethodLength
79+
def download
80+
if @link_checker_report.report_file.attached?
81+
BetterTogether::Metrics::TrackDownloadJob.perform_later(
82+
@link_checker_report,
83+
@link_checker_report.report_file.filename.to_s,
84+
@link_checker_report.report_file.content_type,
85+
@link_checker_report.report_file.byte_size,
86+
I18n.locale.to_s
87+
)
88+
89+
send_data @link_checker_report.report_file.download,
90+
filename: @link_checker_report.report_file.filename.to_s,
91+
type: @link_checker_report.report_file.content_type,
92+
disposition: 'attachment'
93+
94+
return
95+
end
96+
97+
redirect_to metrics_link_checker_reports_path, alert: t('resources.download_failed')
98+
end
99+
# rubocop:enable Metrics/MethodLength
100+
# rubocop:enable Metrics/AbcSize
101+
102+
private
103+
104+
def set_report
105+
@link_checker_report = BetterTogether::Metrics::LinkCheckerReport.find(params[:id])
106+
end
107+
end
108+
end
109+
end

app/controllers/better_together/metrics/reports_controller.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ def index # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
6767
}
6868
end
6969
}
70+
71+
# Link Checker charts: aggregate data from stored links
72+
links_scope = BetterTogether::Content::Link.all
73+
@links_by_host = links_scope.group(:host).count
74+
@invalid_by_host = links_scope.where(valid_link: false).group(:host).count
75+
@failures_daily = links_scope.where(valid_link: false).group_by_day(:last_checked_at).count
7076
end
7177

7278
# A helper method to generate a random color for each platform (this can be customized).

app/controllers/better_together/pages_controller.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ module BetterTogether
44
# Responds to requests for pages
55
class PagesController < FriendlyResourceController # rubocop:todo Metrics/ClassLength
66
before_action :set_page, only: %i[show edit update destroy]
7+
8+
skip_before_action :check_platform_setup, unless: -> { ::BetterTogether::Platform.where(host: true).any? }
9+
710
before_action only: %i[new edit], if: -> { Rails.env.development? } do
811
# Make sure that all BLock subclasses are loaded in dev to generate new block buttons
912
BetterTogether::Content::Block.load_all_subclasses

app/controllers/concerns/better_together/wizard_methods.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ def determine_wizard_outcome # rubocop:todo Metrics/AbcSize
1515
raise StandardError, "Wizard #{wizard_identifier} was not found. Have you run the seeds?" unless wizard
1616

1717
if wizard.completed?
18-
flash[:notice] = wizard.success_message
19-
# TODO: This needs to be adjusted for private platforms. Flash message is not retained after wizard completion
20-
redirect_to wizard.success_path
18+
flash.keep(:notice)
19+
redirect_to wizard.success_path, notice: wizard.success_message
2120
else
2221
next_step_path, flash_key, message = wizard_next_step_info
2322
flash[flash_key] = message if message

app/helpers/better_together/content/blocks_helper.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ def temp_id_for(model, temp_id: SecureRandom.uuid)
1717
# Sanitize HTML content for safe rendering in custom blocks
1818
def sanitize_block_html(html)
1919
allowed_tags = %w[p br strong em b i ul ol li a span h1 h2 h3 h4 h5 h6 img figure figcaption blockquote pre
20-
code]
21-
allowed_attrs = %w[href src alt title class target rel]
20+
code iframe div]
21+
allowed_attrs = %w[href src alt style title class target rel]
2222
sanitize(html.to_s, tags: allowed_tags, attributes: allowed_attrs)
2323
end
2424

app/javascript/controllers/better_together/metrics_charts_controller.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const platformBorderColors = {
5858
};
5959

6060
export default class extends Controller {
61-
static targets = ["pageViewsChart", "dailyPageViewsChart", "linkClicksChart", "dailyLinkClicksChart", "downloadsChart", "sharesChart", "sharesPerUrlPerPlatformChart"]
61+
static targets = ["pageViewsChart", "dailyPageViewsChart", "linkClicksChart", "dailyLinkClicksChart", "downloadsChart", "sharesChart", "sharesPerUrlPerPlatformChart", "linksByHostChart", "invalidByHostChart", "failuresDailyChart"]
6262

6363
connect() {
6464
this.renderPageViewsChart()
@@ -68,6 +68,9 @@ export default class extends Controller {
6868
this.renderDownloadsChart()
6969
this.renderSharesChart()
7070
this.renderSharesPerUrlPerPlatformChart()
71+
this.renderLinksByHostChart()
72+
this.renderInvalidByHostChart()
73+
this.renderFailuresDailyChart()
7174
}
7275

7376
renderPageViewsChart() {
@@ -203,4 +206,58 @@ export default class extends Controller {
203206
})
204207
})
205208
}
209+
210+
renderLinksByHostChart() {
211+
const data = JSON.parse(this.linksByHostChartTarget.dataset.chartData)
212+
new Chart(this.linksByHostChartTarget, {
213+
type: 'bar',
214+
data: {
215+
labels: data.labels,
216+
datasets: [{
217+
label: 'Links by Host',
218+
data: data.values,
219+
backgroundColor: 'rgba(99, 132, 255, 0.2)',
220+
borderColor: 'rgba(99, 132, 255, 1)',
221+
borderWidth: 1
222+
}]
223+
},
224+
options: Object.assign({}, sharedChartOptions)
225+
})
226+
}
227+
228+
renderInvalidByHostChart() {
229+
const data = JSON.parse(this.invalidByHostChartTarget.dataset.chartData)
230+
new Chart(this.invalidByHostChartTarget, {
231+
type: 'bar',
232+
data: {
233+
labels: data.labels,
234+
datasets: [{
235+
label: 'Invalid Links by Host',
236+
data: data.values,
237+
backgroundColor: 'rgba(255, 159, 64, 0.2)',
238+
borderColor: 'rgba(255, 159, 64, 1)',
239+
borderWidth: 1
240+
}]
241+
},
242+
options: Object.assign({}, sharedChartOptions)
243+
})
244+
}
245+
246+
renderFailuresDailyChart() {
247+
const data = JSON.parse(this.failuresDailyChartTarget.dataset.chartData)
248+
new Chart(this.failuresDailyChartTarget, {
249+
type: 'line',
250+
data: {
251+
labels: data.labels,
252+
datasets: [{
253+
label: 'Invalid Links Over Time',
254+
data: data.values,
255+
backgroundColor: 'rgba(255, 99, 132, 0.2)',
256+
borderColor: 'rgba(255, 99, 132, 1)',
257+
borderWidth: 1
258+
}]
259+
},
260+
options: Object.assign({}, sharedChartOptions)
261+
})
262+
}
206263
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Controller } from "stimulus"
2+
3+
export default class extends Controller {
4+
static targets = [ "select" ]
5+
6+
connect() {
7+
// Called when the controller is initialized and the element is in the DOM
8+
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
9+
10+
if (this.hasSelectTarget) {
11+
const options = this.selectTarget.options;
12+
for (let i = 0; i < options.length; i++) {
13+
if (options[i].value === userTimeZone) {
14+
this.selectTarget.selectedIndex = i;
15+
break;
16+
}
17+
}
18+
}
19+
}
20+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
module BetterTogether
4+
# Scans upcoming events and schedules per-event reminders via
5+
# BetterTogether::EventReminderSchedulerJob. This job is intended to be run
6+
# periodically by the scheduler (sidekiq-scheduler / sidekiq-cron).
7+
class EventReminderScanJob < ApplicationJob
8+
queue_as :notifications
9+
10+
# Keep lightweight: find events starting in the near future and enqueue the
11+
# existing per-event scheduler job which handles cancellation/rescheduling.
12+
# default: next 7 days
13+
def perform(window_hours: 168)
14+
cutoff = Time.current + window_hours.hours
15+
16+
BetterTogether::Event.where('starts_at <= ? AND starts_at >= ?', cutoff, Time.current).find_each do |event|
17+
# Use the id to avoid serializing AR objects into the job payload
18+
BetterTogether::EventReminderSchedulerJob.perform_later(event.id)
19+
rescue StandardError => e
20+
Rails.logger.error "Failed to enqueue reminder scheduler for event #{event&.id}: #{e.message}"
21+
end
22+
end
23+
end
24+
end

0 commit comments

Comments
 (0)