bible270 0.18.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +67 -4
- data/Rakefile +178 -4
- data/app/controllers/bible270/checkoffs_controller.rb +34 -9
- data/app/controllers/bible270/comments_controller.rb +63 -28
- data/app/controllers/bible270/days_controller.rb +17 -6
- data/app/controllers/bible270/likes_controller.rb +15 -4
- data/app/controllers/bible270/profiles_controller.rb +37 -4
- data/app/controllers/bible270/readers_controller.rb +48 -3
- data/app/helpers/bible270/plan_helper.rb +17 -3
- data/app/mailers/bible270/notice_mailer.rb +22 -1
- data/app/models/bible270/comment.rb +48 -3
- data/app/models/bible270/reader.rb +74 -7
- data/app/views/bible270/checkoffs/toggle.turbo_stream.erb +8 -0
- data/app/views/bible270/comments/_edit_fields.html.erb +3 -1
- data/app/views/bible270/comments/_form.html.erb +10 -1
- data/app/views/bible270/comments/create.turbo_stream.erb +1 -1
- data/app/views/bible270/comments/index.html.erb +61 -10
- data/app/views/bible270/days/_completers.html.erb +12 -0
- data/app/views/bible270/days/_day_progress.html.erb +47 -0
- data/app/views/bible270/days/_reading.html.erb +10 -2
- data/app/views/bible270/days/_start_date.html.erb +1 -1
- data/app/views/bible270/days/index.html.erb +16 -8
- data/app/views/bible270/days/show.html.erb +38 -42
- data/app/views/bible270/likes/_likes.html.erb +5 -1
- data/app/views/bible270/notice_mailer/daily_reminder.html.erb +30 -0
- data/app/views/bible270/notice_mailer/daily_reminder.text.erb +14 -0
- data/app/views/bible270/notice_mailer/mentioned.html.erb +0 -1
- data/app/views/bible270/notice_mailer/mentioned.text.erb +1 -2
- data/app/views/bible270/profiles/edit.html.erb +41 -8
- data/app/views/bible270/readers/_next_steps.html.erb +63 -0
- data/app/views/bible270/readers/index.html.erb +6 -9
- data/app/views/bible270/readers/progress.html.erb +13 -3
- data/app/views/bible270/shared/_comment_drafts.html.erb +169 -0
- data/app/views/bible270/shared/_day_index.html.erb +1 -1
- data/app/views/bible270/shared/_header.html.erb +14 -17
- data/app/views/bible270/shared/_interaction_status.html.erb +2 -0
- data/app/views/bible270/shared/_interaction_ui.html.erb +69 -0
- data/app/views/bible270/shared/_nav_links.html.erb +15 -0
- data/app/views/bible270/shared/_progress.html.erb +24 -6
- data/app/views/bible270/shared/_reading_mode.html.erb +62 -0
- data/app/views/bible270/shared/_styles.html.erb +147 -10
- data/config/routes.rb +4 -3
- data/db/migrate/20260101000016_add_daily_reminders_to_bible270_readers.rb +9 -0
- data/db/migrate/20260101000017_add_reflections_seen_at_to_bible270_readers.rb +7 -0
- data/lib/bible270/configuration.rb +10 -4
- data/lib/bible270/daily_reminders.rb +57 -0
- data/lib/bible270/migration_reconciler.rb +13 -0
- data/lib/bible270/version.rb +1 -1
- data/lib/bible270.rb +1 -0
- data/lib/generators/bible270/install/install_generator.rb +6 -0
- data/lib/generators/bible270/install/templates/bible270.rb.tt +6 -0
- data/lib/tasks/bible270_reminders.rake +13 -0
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 167d9d42022755b068edda39ff37a00b1a159b05c2d0fadcce80173f1f4c6e7d
|
|
4
|
+
data.tar.gz: 2a97a8eab487cff56dc465e4e81eda2e4a63c60afde87f10f2ad018891cec67b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a8b0d0c963c24be7acfef4301be2ccbb2d95157a6aaa3177a42968a08e893e689ed0d747dce3375c02f3c912f1b23d4f69738659a96f3587d41cdb8bf2cd66c
|
|
7
|
+
data.tar.gz: 52dab82161413ee0aceecd4a67cd0a30411d36d0c4d3c6eeb03a6ea36e3ba98a754d29d36772c7e29aa5297bba31f323d1c19f17b68bc85ac6f154275b40569a
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
|
|
|
4
4
|
|
|
5
5
|
## Unreleased Changes
|
|
6
6
|
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- recovery choices for readers whose next unfinished reading is behind the community day, including an optional schedule reset that preserves progress and reflections
|
|
10
|
+
- a two-worker default `test` task with real-time colorized progress, one aggregate result and duration, merged coverage, a browsable project-local `coverage/` report, and per-file runtime recording, plus `test:serial` when a serial run is needed
|
|
11
|
+
- browser-local reflection and reply drafts that survive refreshes, failed submissions, and accidental navigation
|
|
12
|
+
- day and author filters, pagination, and new-activity markers on the Reflections page
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- replace progress-ranked community lists with transparent recent-participation and alphabetical views that emphasize reading together
|
|
17
|
+
- make Progress actionable with partial-day links, a calendar-week summary, and the reader's next milestone
|
|
18
|
+
- improve accessibility with AA text contrast, consistent focus indicators, descriptive day-grid labels, semantic boundary navigation, touch-friendly tooltips, responsive forms, and reduced-motion support
|
|
19
|
+
- keep profiles and scheduled jobs available while a newly installed daily-reminder database migration is pending
|
|
20
|
+
- add consistent pending, success, and retry feedback to reading, like, reflection, and reply actions, with repeat-safe checkoff and like requests
|
|
21
|
+
- add a remembered compact reading mode that keeps passages, checkoffs, navigation, and completion next steps in focus
|
|
22
|
+
|
|
7
23
|
See the changes since the last release:
|
|
8
24
|
|
|
9
25
|
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.18.2...main)**
|
data/README.md
CHANGED
|
@@ -24,6 +24,12 @@ The schedule is pure deterministic Ruby (`Bible270::Plan`) — none of it is sto
|
|
|
24
24
|
- **Rails** >= 7.0. On Ruby 4.0 your host app is the real constraint, not this engine — check your lockfile with RailsBump first.
|
|
25
25
|
- A host app, and Turbo if you want check-offs without a page reload.
|
|
26
26
|
|
|
27
|
+
Mutation controls expose pending, success, and retry feedback when JavaScript runs, while ordinary HTML
|
|
28
|
+
forms remain the fallback. Repeated checkoff and like requests converge on the intended state rather
|
|
29
|
+
than accidentally reversing it. Day pages also offer a browser-local **Compact reading** preference
|
|
30
|
+
that keeps passages, checkoffs, day navigation, and the completion next step while hiding community
|
|
31
|
+
and reflection content.
|
|
32
|
+
|
|
27
33
|
## Install
|
|
28
34
|
|
|
29
35
|
```ruby
|
|
@@ -272,6 +278,19 @@ Admins get an extra "Admin" link in the header. From a reader's page you can:
|
|
|
272
278
|
written; hiding one takes it off the day and community pages without deleting it, and is
|
|
273
279
|
reversible. Delete is there too, for anything that shouldn't be kept.
|
|
274
280
|
|
|
281
|
+
The Reflections page lists conversations by their latest approved reply, supports day and author
|
|
282
|
+
filters, and paginates older threads. Signed-in readers see which conversations became active since
|
|
283
|
+
their previous visit. Reflection and reply drafts are retained for up to 30 days in that browser and
|
|
284
|
+
cleared after a successful post. On a shared device, clearing browser storage also removes saved drafts.
|
|
285
|
+
|
|
286
|
+
After upgrading, copy and run migration `20260101000017`, which stores each reader's last visit to the
|
|
287
|
+
Reflections page:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
bin/rails bible270:install:migrations
|
|
291
|
+
bin/rails db:migrate
|
|
292
|
+
```
|
|
293
|
+
|
|
275
294
|
Break points can also live in your app rather than the gem, and be changed without a restart:
|
|
276
295
|
|
|
277
296
|
```ruby
|
|
@@ -313,6 +332,38 @@ Bible270::Plan.day_for(date, start, clamp: false) # => -3 or 271, to spot out-of
|
|
|
313
332
|
Bible270::Plan.before_start?(date, start)
|
|
314
333
|
```
|
|
315
334
|
|
|
335
|
+
## Daily reading reminders
|
|
336
|
+
|
|
337
|
+
Daily reminders are optional at both levels: the host enables the feature, then each reader chooses
|
|
338
|
+
whether to receive it and at what local time from their profile. The feature and reader opt-in both
|
|
339
|
+
default to off; each reader's time defaults to `08:00`.
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
# config/initializers/bible270.rb
|
|
343
|
+
Bible270.configure do |config|
|
|
344
|
+
config.daily_reminders = true
|
|
345
|
+
config.mailer_from = "no-reply@yourdomain.com"
|
|
346
|
+
config.mailer_host = "yourdomain.com" # builds links back to the day and profile
|
|
347
|
+
end
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
After upgrading, copy and run migration `20260101000016`, which adds the reader opt-in, preferred
|
|
351
|
+
time, and last-sent date. Bible270 does not install a scheduler. The host application should run this
|
|
352
|
+
task periodically—every 15 minutes is recommended—using its existing cron, platform scheduler, or
|
|
353
|
+
recurring-job system:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
bin/rails bible270:reminders:send
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Each run converts `Time.now` through `Bible270.local_time`, waits until each reader's chosen `HH:MM`
|
|
360
|
+
time, sends inline with `deliver_now`, and prints the number delivered. Set `config.time_zone` when the
|
|
361
|
+
plan should not follow the server's local clock. Running periodically is safe because delivery is
|
|
362
|
+
idempotent once per local date: readers already marked for that date are skipped. Undated readers,
|
|
363
|
+
calendar dates before or after their plan, completed days, blank email addresses, and readers who have
|
|
364
|
+
not opted in are also skipped. A failure for one reader is logged and does not stop the rest. The sent
|
|
365
|
+
date is recorded only after delivery succeeds.
|
|
366
|
+
|
|
316
367
|
## Configuration reference
|
|
317
368
|
|
|
318
369
|
```ruby
|
|
@@ -326,6 +377,9 @@ Bible270.configure do |config|
|
|
|
326
377
|
# config.start_date = Date.new(2026, 9, 6)
|
|
327
378
|
config.allow_reader_start_date = true
|
|
328
379
|
|
|
380
|
+
config.daily_reminders = false # schedule reminders:send every 15 minutes
|
|
381
|
+
config.mailer_host = "example.com" # links in reminder/notice email
|
|
382
|
+
|
|
329
383
|
config.parent_controller = "ActionController::Base" # or "::ApplicationController"
|
|
330
384
|
config.layout = "bible270/application"
|
|
331
385
|
|
|
@@ -397,7 +451,7 @@ The CMS serves your content; this is a separate mounted app. Either **just link
|
|
|
397
451
|
|
|
398
452
|
| Table | Holds |
|
|
399
453
|
|-------|-------|
|
|
400
|
-
| `bible270_readers` | identity, avatar, provider/uid or polymorphic `owner`, start date, translation
|
|
454
|
+
| `bible270_readers` | identity, avatar, provider/uid or polymorphic `owner`, start date, translation, passage-reader, and reminder preferences |
|
|
401
455
|
| `bible270_checkoffs` | one row per reader per day per track (`ot`/`nt`/`pp`), unique-indexed |
|
|
402
456
|
| `bible270_comments` | a reflection on a day, optionally scoped to a track, public to all |
|
|
403
457
|
| `bible270_sign_in_tokens` | magic-link tokens: email, **digest only**, expiry, consumed-at |
|
|
@@ -471,7 +525,7 @@ All computed at load and memoized — no generated schedule, so nothing to migra
|
|
|
471
525
|
|
|
472
526
|
## Scaling
|
|
473
527
|
|
|
474
|
-
|
|
528
|
+
Community pages count completed days in Ruby from grouped check-offs — fine for hundreds of readers. Past a few thousand, cache a `days_completed` counter on `Reader` via a `Checkoff` `after_commit` and read it directly in SQL.
|
|
475
529
|
|
|
476
530
|
## Troubleshooting
|
|
477
531
|
|
|
@@ -499,9 +553,18 @@ The engine's CSS rides in a partial its own layout renders. Using your layout? A
|
|
|
499
553
|
|
|
500
554
|
```bash
|
|
501
555
|
bundle install
|
|
502
|
-
rake test
|
|
556
|
+
bundle exec rake test
|
|
557
|
+
bundle exec rubocop --cache false
|
|
503
558
|
```
|
|
504
559
|
|
|
560
|
+
`rake test` runs two isolated processes, streams colorized progress in real time, then prints one
|
|
561
|
+
consolidated result and duration. It merges their SimpleCov results, writes a browsable report to
|
|
562
|
+
`coverage/index.html`, and
|
|
563
|
+
records per-file runtimes in `tmp/parallel_runtime_test.log` to improve
|
|
564
|
+
subsequent balancing. Set `PARALLEL_WORKERS` to tune the worker count. Use
|
|
565
|
+
`bundle exec rake test:serial` when a serial coverage run is specifically needed; it writes to the same
|
|
566
|
+
project-local `coverage/` directory.
|
|
567
|
+
|
|
505
568
|
### Get an email link for local testing
|
|
506
569
|
|
|
507
570
|
in the rails console: `rails c`
|
|
@@ -518,7 +581,7 @@ Bug reports and pull requests are welcome at <https://github.com/avonderluft/bib
|
|
|
518
581
|
|
|
519
582
|
1. Fork it and clone your fork.
|
|
520
583
|
2. Branch off `main`: `git checkout -b my-feature`.
|
|
521
|
-
3. Make your change, and add tests — `rake test` should stay green.
|
|
584
|
+
3. Make your change, and add tests — `bundle exec rake test` and `bundle exec rubocop --cache false` should stay green.
|
|
522
585
|
4. Commit with a clear message: `git commit -am "Add my feature"`.
|
|
523
586
|
5. Push: `git push origin my-feature`.
|
|
524
587
|
6. Open a pull request against `main`, saying what changed and why.
|
data/Rakefile
CHANGED
|
@@ -1,7 +1,138 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'bundler/gem_tasks' # provides build / install / release
|
|
4
|
+
require 'open3'
|
|
4
5
|
require 'rake/testtask'
|
|
6
|
+
require 'stringio'
|
|
7
|
+
|
|
8
|
+
module Bible270TestOutput
|
|
9
|
+
SUMMARY = %r{(?<assertions>\d+) assertions, (?<errors>\d+) errors, (?<failures>\d+) failures, (?<skips>\d+) skips?, (?<tests>\d+) tests}
|
|
10
|
+
COLORS = { green: 32, yellow: 33, red: 31, cyan: 36 }.freeze
|
|
11
|
+
|
|
12
|
+
class Progress
|
|
13
|
+
def initialize(io)
|
|
14
|
+
@io = io
|
|
15
|
+
@line = +''
|
|
16
|
+
@pending = +''
|
|
17
|
+
@awaiting_blank = false
|
|
18
|
+
@expecting_progress = false
|
|
19
|
+
@printing_progress = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def feed(chunk)
|
|
23
|
+
chunk.each_char { |character| consume(character) }
|
|
24
|
+
flush
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def finish
|
|
28
|
+
flush
|
|
29
|
+
@io.puts if @printing_progress
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def consume(character)
|
|
35
|
+
return if character == "\r"
|
|
36
|
+
return finish_line if character == "\n"
|
|
37
|
+
|
|
38
|
+
if @expecting_progress && character.match?(%r{[.EFS]})
|
|
39
|
+
@printing_progress = true
|
|
40
|
+
@pending << character
|
|
41
|
+
elsif @printing_progress
|
|
42
|
+
flush
|
|
43
|
+
@io.puts
|
|
44
|
+
@printing_progress = false
|
|
45
|
+
@expecting_progress = false
|
|
46
|
+
@line = +character
|
|
47
|
+
else
|
|
48
|
+
@expecting_progress = false if @expecting_progress
|
|
49
|
+
@line << character
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def finish_line
|
|
54
|
+
flush
|
|
55
|
+
if @printing_progress
|
|
56
|
+
@io.puts
|
|
57
|
+
@expecting_progress = false
|
|
58
|
+
elsif @line.start_with?('# Running tests')
|
|
59
|
+
@awaiting_blank = true
|
|
60
|
+
elsif @awaiting_blank && @line.empty?
|
|
61
|
+
@expecting_progress = true
|
|
62
|
+
@awaiting_blank = false
|
|
63
|
+
end
|
|
64
|
+
@line.clear
|
|
65
|
+
@printing_progress = false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def flush
|
|
69
|
+
return if @pending.empty?
|
|
70
|
+
|
|
71
|
+
@io.print Bible270TestOutput.progress_color(@pending)
|
|
72
|
+
@io.flush
|
|
73
|
+
@pending.clear
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
module_function
|
|
78
|
+
|
|
79
|
+
def capture(env, command)
|
|
80
|
+
output = +''
|
|
81
|
+
errors = +''
|
|
82
|
+
status = nil
|
|
83
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
84
|
+
progress = Progress.new($stdout)
|
|
85
|
+
|
|
86
|
+
Open3.popen3(env, *command) do |stdin, stdout, stderr, wait_thread|
|
|
87
|
+
stdin.close
|
|
88
|
+
error_reader = Thread.new { stderr.read }
|
|
89
|
+
loop do
|
|
90
|
+
chunk = stdout.readpartial(4096)
|
|
91
|
+
output << chunk
|
|
92
|
+
progress.feed(chunk)
|
|
93
|
+
end
|
|
94
|
+
rescue EOFError
|
|
95
|
+
errors = error_reader.value
|
|
96
|
+
status = wait_thread.value
|
|
97
|
+
ensure
|
|
98
|
+
progress.finish
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
|
102
|
+
[output, errors, status, elapsed]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def color(text, name)
|
|
106
|
+
return text unless $stdout.tty? && !ENV.key?('NO_COLOR')
|
|
107
|
+
|
|
108
|
+
"\e[#{COLORS.fetch(name)}m#{text}\e[0m"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def progress_color(text)
|
|
112
|
+
return text unless $stdout.tty? && !ENV.key?('NO_COLOR')
|
|
113
|
+
|
|
114
|
+
highlighted = text.gsub('S', "\e[33mS\e[32m")
|
|
115
|
+
.gsub(%r{[EF]}) { |character| "\e[31m#{character}\e[32m" }
|
|
116
|
+
"\e[32m#{highlighted}\e[0m"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def summary(output)
|
|
120
|
+
counts = output.match(SUMMARY)
|
|
121
|
+
raise 'Could not read the parallel test summary' unless counts
|
|
122
|
+
|
|
123
|
+
"#{counts[:tests]} tests, #{counts[:assertions]} assertions, " \
|
|
124
|
+
"#{counts[:failures]} failures, #{counts[:errors]} errors, " \
|
|
125
|
+
"#{counts[:skips]} #{counts[:skips] == '1' ? 'skip' : 'skips'}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def quietly
|
|
129
|
+
previous = $stdout
|
|
130
|
+
$stdout = StringIO.new
|
|
131
|
+
yield
|
|
132
|
+
ensure
|
|
133
|
+
$stdout = previous
|
|
134
|
+
end
|
|
135
|
+
end
|
|
5
136
|
|
|
6
137
|
desc 'Normalize permissions for files included in the gem'
|
|
7
138
|
task :normalize_gem_permissions do
|
|
@@ -13,10 +144,53 @@ end
|
|
|
13
144
|
|
|
14
145
|
Rake::Task[:build].enhance([:normalize_gem_permissions])
|
|
15
146
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
147
|
+
desc 'Run tests in isolated processes (default: 2 workers)'
|
|
148
|
+
namespace :test do
|
|
149
|
+
task :prepare_serial do
|
|
150
|
+
FileUtils.rm_rf('coverage')
|
|
151
|
+
ENV['TMPDIR'] = ENV.fetch('BIBLE270_TEST_TMPDIR', '/tmp')
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
desc 'Run tests serially with coverage'
|
|
155
|
+
Rake::TestTask.new(:serial) do |t|
|
|
156
|
+
t.libs << 'test' << 'lib'
|
|
157
|
+
t.pattern = 'test/**/*_test.rb'
|
|
158
|
+
t.warning = false
|
|
159
|
+
end
|
|
160
|
+
Rake::Task['test:serial'].enhance(['test:prepare_serial'])
|
|
161
|
+
|
|
162
|
+
task :parallel do
|
|
163
|
+
workers = ENV.fetch('PARALLEL_WORKERS', '2')
|
|
164
|
+
FileUtils.rm_rf('coverage')
|
|
165
|
+
env = {
|
|
166
|
+
'PARALLEL_COVERAGE' => 'true',
|
|
167
|
+
'RECORD_RUNTIME' => 'true',
|
|
168
|
+
'SKIP_COV' => nil,
|
|
169
|
+
'NO_COLOR' => '1',
|
|
170
|
+
'TMPDIR' => ENV.fetch('BIBLE270_TEST_TMPDIR', '/tmp')
|
|
171
|
+
}
|
|
172
|
+
command = ['bundle', 'exec', 'parallel_test', 'test', '--type', 'test', '-n', workers]
|
|
173
|
+
puts Bible270TestOutput.color("Running tests (#{workers} workers)…", :cyan)
|
|
174
|
+
stdout, stderr, status, elapsed = Bible270TestOutput.capture(env, command)
|
|
175
|
+
unless status.success?
|
|
176
|
+
$stdout.write(stdout)
|
|
177
|
+
$stderr.write(stderr)
|
|
178
|
+
raise "Parallel tests failed with status #{status.exitstatus}"
|
|
179
|
+
end
|
|
180
|
+
$stderr.write(stderr) unless stderr.empty?
|
|
181
|
+
puts Bible270TestOutput.color(Bible270TestOutput.summary(stdout), :green)
|
|
182
|
+
puts Bible270TestOutput.color(format('Completed in %.1f seconds', elapsed), :cyan)
|
|
183
|
+
|
|
184
|
+
require 'simplecov'
|
|
185
|
+
SimpleCov.coverage_dir File.expand_path('coverage', __dir__)
|
|
186
|
+
result = SimpleCov::ResultMerger.merged_result
|
|
187
|
+
Bible270TestOutput.quietly { SimpleCov::Formatter::HTMLFormatter.new.format(result) }
|
|
188
|
+
coverage = format('Coverage: %<percent>.2f%% (%<covered>d/%<total>d) — coverage/index.html',
|
|
189
|
+
percent: result.covered_percent, covered: result.covered_lines, total: result.total_lines)
|
|
190
|
+
puts Bible270TestOutput.color(coverage, :green)
|
|
191
|
+
end
|
|
20
192
|
end
|
|
21
193
|
|
|
194
|
+
desc 'Run tests in parallel'
|
|
195
|
+
task test: 'test:parallel'
|
|
22
196
|
task default: :test
|
|
@@ -15,21 +15,46 @@ module Bible270
|
|
|
15
15
|
Plan.present_tracks(@day).include?(@track) &&
|
|
16
16
|
Plan.valid_part?(@day, @track, @part)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
if
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
current_reader.ensure_started!
|
|
23
|
-
current_reader.checkoffs.create(day: @day, track: @track, part: @part)
|
|
24
|
-
end
|
|
18
|
+
requested_state = params[:checked] if params.key?(:checked)
|
|
19
|
+
head :bad_request and return if params.key?(:checked) && !%w[0 1].include?(requested_state)
|
|
20
|
+
|
|
21
|
+
marked_read = apply_checkoff(requested_state)
|
|
25
22
|
|
|
26
|
-
@reader = current_reader.reload
|
|
23
|
+
@reader = current_reader.reload.reload_progress
|
|
27
24
|
@reader_tracks = @reader.read_tracks_for(@day)
|
|
28
25
|
@readings = Plan.readings_for(@day)
|
|
29
26
|
|
|
27
|
+
required = Plan.total_parts(@day)
|
|
28
|
+
completer_ids = Checkoff.where(day: @day)
|
|
29
|
+
.group(:reader_id)
|
|
30
|
+
.having('COUNT(*) >= ?', required)
|
|
31
|
+
.pluck(:reader_id)
|
|
32
|
+
@completers = Reader.where(id: completer_ids).order(:display_name)
|
|
33
|
+
|
|
30
34
|
respond_to do |format|
|
|
31
35
|
format.turbo_stream
|
|
32
|
-
format.html
|
|
36
|
+
format.html do
|
|
37
|
+
message = marked_read ? 'Reading marked read.' : 'Reading marked unread.'
|
|
38
|
+
redirect_to day_path(@day), flash: { b270_interaction_status: message }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def apply_checkoff(requested_state)
|
|
46
|
+
existing = current_reader.checkoffs.find_by(day: @day, track: @track, part: @part)
|
|
47
|
+
if requested_state == '1'
|
|
48
|
+
current_reader.ensure_started!
|
|
49
|
+
current_reader.checkoffs.create_or_find_by!(day: @day, track: @track, part: @part)
|
|
50
|
+
true
|
|
51
|
+
elsif requested_state == '0' || existing
|
|
52
|
+
existing&.destroy
|
|
53
|
+
false
|
|
54
|
+
else
|
|
55
|
+
current_reader.ensure_started!
|
|
56
|
+
current_reader.checkoffs.create(day: @day, track: @track, part: @part)
|
|
57
|
+
true
|
|
33
58
|
end
|
|
34
59
|
end
|
|
35
60
|
end
|
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
module Bible270
|
|
4
4
|
class CommentsController < ApplicationController
|
|
5
5
|
def index
|
|
6
|
+
visited_at = Time.current
|
|
6
7
|
@days_with_reflections = Comment.approved.distinct.pluck(:day).sort
|
|
7
|
-
|
|
8
|
+
load_reflection_filters
|
|
9
|
+
load_reflection_page
|
|
10
|
+
load_new_reflections
|
|
11
|
+
current_reader&.mark_reflections_seen!(visited_at)
|
|
8
12
|
end
|
|
9
13
|
|
|
10
14
|
def create
|
|
@@ -19,13 +23,18 @@ module Bible270
|
|
|
19
23
|
if @comment.save
|
|
20
24
|
respond_to do |format|
|
|
21
25
|
format.turbo_stream
|
|
22
|
-
format.html
|
|
26
|
+
format.html do
|
|
27
|
+
message = @comment.reply? ? 'Reply posted.' : 'Reflection posted.'
|
|
28
|
+
redirect_to day_path(@day, anchor: "comment-#{@comment.id}"),
|
|
29
|
+
flash: { b270_interaction_status: message }
|
|
30
|
+
end
|
|
23
31
|
end
|
|
24
32
|
else
|
|
25
33
|
respond_to do |format|
|
|
26
34
|
format.turbo_stream do
|
|
27
35
|
render turbo_stream: turbo_stream.replace('new_comment_form', partial: 'bible270/comments/form',
|
|
28
|
-
locals: { day: @day, comment: @comment })
|
|
36
|
+
locals: { day: @day, comment: @comment }),
|
|
37
|
+
status: :unprocessable_entity
|
|
29
38
|
end
|
|
30
39
|
format.html { redirect_to day_path(@day), alert: @comment.errors.full_messages.to_sentence }
|
|
31
40
|
end
|
|
@@ -42,14 +51,18 @@ module Bible270
|
|
|
42
51
|
if @comment.update(comment_params.except(:parent_id))
|
|
43
52
|
respond_to do |format|
|
|
44
53
|
format.turbo_stream
|
|
45
|
-
format.html
|
|
54
|
+
format.html do
|
|
55
|
+
redirect_to day_path(@day, anchor: "comment-#{@comment.id}"),
|
|
56
|
+
flash: { b270_interaction_status: 'Changes saved.' }
|
|
57
|
+
end
|
|
46
58
|
end
|
|
47
59
|
else
|
|
48
60
|
respond_to do |format|
|
|
49
61
|
format.turbo_stream do
|
|
50
62
|
render turbo_stream: turbo_stream.replace("comment-#{@comment.id}",
|
|
51
63
|
partial: 'bible270/comments/edit_form',
|
|
52
|
-
locals: { comment: @comment })
|
|
64
|
+
locals: { comment: @comment }),
|
|
65
|
+
status: :unprocessable_entity
|
|
53
66
|
end
|
|
54
67
|
format.html { redirect_to day_path(@day), alert: @comment.errors.full_messages.to_sentence }
|
|
55
68
|
end
|
|
@@ -72,30 +85,52 @@ module Bible270
|
|
|
72
85
|
|
|
73
86
|
private
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
def load_reflection_filters
|
|
89
|
+
root_reflections = Comment.approved.where(parent_id: nil)
|
|
90
|
+
reader_ids = root_reflections.distinct.pluck(:reader_id)
|
|
91
|
+
@reflection_readers = Reader.where(id: reader_ids).to_a.sort_by(&:sort_name)
|
|
92
|
+
|
|
93
|
+
requested_day = params[:day].presence&.to_i
|
|
94
|
+
@reflection_day = requested_day if @days_with_reflections.include?(requested_day)
|
|
95
|
+
|
|
96
|
+
requested_reader = params[:reader_id].presence&.to_i
|
|
97
|
+
@reflection_reader_id = requested_reader if reader_ids.include?(requested_reader)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def load_reflection_page
|
|
101
|
+
result = Comment.thread_page(
|
|
102
|
+
day: @reflection_day,
|
|
103
|
+
reader_id: @reflection_reader_id,
|
|
104
|
+
page: params[:page],
|
|
105
|
+
per_page: Bible270.config.reflections_page_size
|
|
106
|
+
)
|
|
107
|
+
@threads = result.threads
|
|
108
|
+
@thread_activity = result.activity_by_id
|
|
109
|
+
@reflection_page = result.page
|
|
110
|
+
@reflection_pages = result.pages
|
|
111
|
+
@reflection_total = result.total
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def load_new_reflections
|
|
115
|
+
@reflections_since = reflections_since
|
|
116
|
+
@reflections_since_param = @reflections_since&.iso8601(6) || 'none'
|
|
117
|
+
@new_thread_ids = if @reflections_since
|
|
118
|
+
@thread_activity.filter_map do |id, activity_at|
|
|
119
|
+
id if activity_at > @reflections_since
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
[]
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def reflections_since
|
|
127
|
+
return nil unless current_reader&.class&.reflections_seen_column?
|
|
128
|
+
return current_reader.reflections_seen_at unless params.key?(:since)
|
|
129
|
+
return nil if params[:since] == 'none'
|
|
93
130
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.index_by(&:id)
|
|
98
|
-
ids.filter_map { |id| threads[id] }
|
|
131
|
+
Time.iso8601(params[:since].to_s)
|
|
132
|
+
rescue ArgumentError
|
|
133
|
+
current_reader.reflections_seen_at
|
|
99
134
|
end
|
|
100
135
|
|
|
101
136
|
def comment_params
|
|
@@ -5,12 +5,26 @@ module Bible270
|
|
|
5
5
|
def index
|
|
6
6
|
@plan_totals = Plan.totals
|
|
7
7
|
|
|
8
|
-
#
|
|
9
|
-
|
|
8
|
+
# Show recent participation without ranking readers by completion. A checkoff
|
|
9
|
+
# or reflection counts as activity; joining is the fallback for new readers.
|
|
10
|
+
@readers = Reader.all.to_a
|
|
11
|
+
checkoff_activity = Checkoff.group(:reader_id).maximum(:updated_at)
|
|
12
|
+
reflection_activity = Comment.approved.group(:reader_id).maximum(:updated_at)
|
|
13
|
+
@reader_activity = @readers.to_h do |reader|
|
|
14
|
+
candidates = [
|
|
15
|
+
{ kind: :joined, at: reader.created_at },
|
|
16
|
+
{ kind: :reading, at: checkoff_activity[reader.id] },
|
|
17
|
+
{ kind: :reflection, at: reflection_activity[reader.id] }
|
|
18
|
+
].select { |activity| activity[:at] }
|
|
19
|
+
[reader.id, candidates.max_by { |activity| activity[:at] }]
|
|
20
|
+
end
|
|
21
|
+
@readers.sort_by! do |reader|
|
|
22
|
+
[-@reader_activity.fetch(reader.id)[:at].to_f, reader.sort_name]
|
|
23
|
+
end
|
|
24
|
+
|
|
10
25
|
counts = Checkoff.group(:reader_id, :day).count
|
|
11
26
|
@days_completed = Hash.new(0)
|
|
12
27
|
counts.each { |(rid, day), n| @days_completed[rid] += 1 if n >= Plan.total_parts(day) }
|
|
13
|
-
@readers = @readers.sort_by { |r| -@days_completed[r.id] }
|
|
14
28
|
|
|
15
29
|
@start_day = current_reader&.current_day || 1
|
|
16
30
|
# nil outside the plan's window, so no "Go to today" before it begins
|
|
@@ -41,9 +55,6 @@ module Bible270
|
|
|
41
55
|
.having('COUNT(*) >= ?', required)
|
|
42
56
|
.pluck(:reader_id)
|
|
43
57
|
@completers = Reader.where(id: completer_ids).order(:display_name)
|
|
44
|
-
|
|
45
|
-
@prev_day = @day > 1 ? @day - 1 : nil
|
|
46
|
-
@next_day = @day < Plan::DAYS ? @day + 1 : nil
|
|
47
58
|
end
|
|
48
59
|
|
|
49
60
|
private
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Bible270
|
|
4
|
-
# A heart on someone's reflection.
|
|
5
|
-
#
|
|
4
|
+
# A heart on someone's reflection. The route accepts the intended state from
|
|
5
|
+
# current forms and retains toggle behavior for cached or external callers.
|
|
6
6
|
class LikesController < ApplicationController
|
|
7
7
|
def toggle
|
|
8
8
|
return unless require_reader!
|
|
@@ -10,12 +10,23 @@ module Bible270
|
|
|
10
10
|
@comment = Comment.approved.find_by(id: params[:id])
|
|
11
11
|
head :not_found and return unless @comment
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
requested_state = params[:liked] if params.key?(:liked)
|
|
14
|
+
head :bad_request and return if params.key?(:liked) && !%w[0 1].include?(requested_state)
|
|
15
|
+
|
|
16
|
+
liked = if requested_state
|
|
17
|
+
@comment.set_like!(current_reader, liked: requested_state == '1')
|
|
18
|
+
else
|
|
19
|
+
@comment.toggle_like!(current_reader)
|
|
20
|
+
end
|
|
14
21
|
@comment.reload
|
|
15
22
|
|
|
16
23
|
respond_to do |format|
|
|
17
24
|
format.turbo_stream
|
|
18
|
-
format.html
|
|
25
|
+
format.html do
|
|
26
|
+
message = liked ? 'Reflection liked.' : 'Like removed.'
|
|
27
|
+
redirect_to day_path(@comment.day, anchor: "comment-#{@comment.id}"),
|
|
28
|
+
flash: { b270_interaction_status: message }
|
|
29
|
+
end
|
|
19
30
|
end
|
|
20
31
|
end
|
|
21
32
|
end
|