bible270 0.19.0 → 1.1.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 +66 -2
- data/README.md +71 -11
- data/Rakefile +178 -4
- data/app/controllers/bible270/admin_controller.rb +2 -3
- 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 +1 -25
- data/app/controllers/bible270/likes_controller.rb +15 -4
- data/app/controllers/bible270/profiles_controller.rb +57 -1
- data/app/controllers/bible270/readers_controller.rb +13 -33
- data/app/helpers/bible270/plan_helper.rb +17 -3
- data/app/mailers/bible270/notice_mailer.rb +52 -7
- data/app/models/bible270/comment.rb +78 -9
- data/app/models/bible270/reader.rb +69 -27
- 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 -3
- data/app/views/bible270/comments/index.html.erb +56 -29
- data/app/views/bible270/days/_completers.html.erb +10 -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/index.html.erb +2 -20
- data/app/views/bible270/days/show.html.erb +27 -43
- data/app/views/bible270/likes/_likes.html.erb +5 -1
- data/app/views/bible270/notice_mailer/{mentioned.html.erb → comment_notice.html.erb} +5 -2
- data/app/views/bible270/notice_mailer/{mentioned.text.erb → comment_notice.text.erb} +5 -2
- 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/profiles/edit.html.erb +49 -1
- data/app/views/bible270/readers/_next_steps.html.erb +27 -0
- data/app/views/bible270/readers/index.html.erb +4 -8
- data/app/views/bible270/readers/progress.html.erb +7 -21
- data/app/views/bible270/readers/show.html.erb +1 -6
- data/app/views/bible270/shared/_comment_drafts.html.erb +169 -0
- data/app/views/bible270/shared/_day_index.html.erb +6 -4
- data/app/views/bible270/shared/_header.html.erb +21 -4
- 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/_progress.html.erb +14 -7
- data/app/views/bible270/shared/_styles.html.erb +96 -19
- data/config/routes.rb +1 -4
- data/db/migrate/20260101000011_add_mention_notices_to_bible270_readers.rb +3 -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 +18 -9
- data/lib/bible270/daily_reminders.rb +57 -0
- data/lib/bible270/mentions.rb +0 -9
- 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 +8 -0
- data/lib/generators/bible270/install/templates/bible270.rb.tt +8 -0
- data/lib/tasks/bible270_reminders.rake +13 -0
- metadata +15 -4
- data/app/views/bible270/days/_start_date.html.erb +0 -43
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48e80937debff292b2c896ae9eaba55fe5eae8624ef28c6eb9274236fed4fb2c
|
|
4
|
+
data.tar.gz: 72f8fbd17194fd00bbc8602fd69180cb91565d54f995868dacf8edfb4419657f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b493f52caadc3fa2bbb7e3ef4dd59c9822d0045b9fb6f42151f4fad0c9057d017b7604064188e35a3dce03c8c6c3c02cabce8a95a342e84e6fbf5a2682a46309
|
|
7
|
+
data.tar.gz: a38f6e3b979b76ccaf5c666e3698d29302ac97b82692f7c2a33ac1ef0d28f139b28ddaf89705e4a13c132713533edd87c8e804eff06974c089ad2bb8b1322c0b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,71 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
|
|
|
4
4
|
|
|
5
5
|
## Unreleased Changes
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### Removed
|
|
8
|
+
|
|
9
|
+
- remove the Compact reading preference so daily reading pages have one simple, predictable presentation
|
|
10
|
+
- replace the homepage reader preview with one clear link to the Community page
|
|
11
|
+
- remove the redundant schedule-restart shortcut from progress recovery choices
|
|
12
|
+
- remove reader-facing start-date status, controls, and self-service endpoints while retaining automatic and admin-managed personal calendars
|
|
13
|
+
- remove weekly progress, its unused schedule helper, milestones, and explanatory schedule-recovery copy from My Progress
|
|
14
|
+
- replace the Reflections page’s separate day grid with the standard “View all 270 days” control
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- notify reflection authors directly about replies, share one opt-out with explicit mentions, and stop inserting handles into reply text
|
|
19
|
+
- consolidate duplicate visitor sign-in prompts on day pages into one invitation
|
|
20
|
+
- show “Finish what you started” on My Progress only when partially read days need attention
|
|
21
|
+
- clarify the global day-index control as “View all 270 days” and use that standard collapsed control on My Progress
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- make the mobile navigation menu reliably open and close when its button is tapped
|
|
26
|
+
|
|
27
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.0.0...main)**
|
|
28
|
+
|
|
29
|
+
## [1.0.0] - 2026-08-20
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- optional daily reading reminders with per-reader opt-in and delivery time, reminder emails, and a scheduler-friendly Rake task
|
|
34
|
+
- immediate day-completion feedback with a preserved next-day or Progress action
|
|
35
|
+
- actionable Progress sections for unfinished portions, the current calendar week, milestones, and schedule recovery without losing reading or reflection history
|
|
36
|
+
- browser-local reflection and reply drafts, day and author filters, pagination, and new-activity markers on the Reflections page
|
|
37
|
+
- a remembered compact reading mode that keeps passages, checkoffs, navigation, and completion next steps in focus
|
|
38
|
+
- 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
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- replace progress-ranked community lists with transparent recent-participation and alphabetical views that emphasize reading together
|
|
43
|
+
- show why each reader appears in Reading together, including their latest reading, reflection, or join activity
|
|
44
|
+
- add consistent pending, success, and retry feedback to reading, like, reflection, and reply actions, with repeat-safe checkoff and like requests
|
|
45
|
+
- 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
|
|
46
|
+
- replace the reminder time field with aligned hour and minute selectors and an accessible time-zone tooltip
|
|
47
|
+
- hide personal start-date controls and calendar prompts when the community configuration does not allow readers to change dates
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- keep profiles and scheduled reminder jobs available while a newly installed daily-reminder database migration is pending
|
|
52
|
+
|
|
53
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.19.0...v1.0.0)**
|
|
54
|
+
|
|
55
|
+
## [0.19.0] - 2026-08-17
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- shared responsive navigation with clearer Community, Reflections, My Progress, and Profile labels plus a mobile menu
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- align page headings with navigation destinations and clarify the Start or Continue reading and Go to today actions
|
|
64
|
+
- always email explicitly mentioned readers, including the original reflection author when a reply prefills their mention
|
|
65
|
+
- normalize packaged file permissions before gem builds
|
|
66
|
+
|
|
67
|
+
### Removed
|
|
68
|
+
|
|
69
|
+
- the per-reader mention-email opt-out; writing a reader's name explicitly now always sends the mention notice
|
|
8
70
|
|
|
9
|
-
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.18.2...
|
|
71
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.18.2...v0.19.0)**
|
|
10
72
|
|
|
11
73
|
## [0.18.2] - 2026-08-15
|
|
12
74
|
|
|
@@ -426,6 +488,8 @@ This is the initial public release.
|
|
|
426
488
|
|
|
427
489
|
* Email sign-in requires working Action Mailer delivery in the host application. Set `config.mailer_from`; delivery is inline by default, or set `email_sign_in_deliver_later` when a queue backend is available.
|
|
428
490
|
|
|
491
|
+
[1.0.0]: https://github.com/avonderluft/bible270/releases/tag/v1.0.0
|
|
492
|
+
[0.19.0]: https://github.com/avonderluft/bible270/releases/tag/v0.19.0
|
|
429
493
|
[0.18.2]: https://github.com/avonderluft/bible270/releases/tag/v0.18.2
|
|
430
494
|
[0.18.1]: https://github.com/avonderluft/bible270/releases/tag/v0.18.1
|
|
431
495
|
[0.18.0]: https://github.com/avonderluft/bible270/releases/tag/v0.18.0
|
data/README.md
CHANGED
|
@@ -24,6 +24,10 @@ 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.
|
|
30
|
+
|
|
27
31
|
## Install
|
|
28
32
|
|
|
29
33
|
```ruby
|
|
@@ -272,6 +276,19 @@ Admins get an extra "Admin" link in the header. From a reader's page you can:
|
|
|
272
276
|
written; hiding one takes it off the day and community pages without deleting it, and is
|
|
273
277
|
reversible. Delete is there too, for anything that shouldn't be kept.
|
|
274
278
|
|
|
279
|
+
The Reflections page lists conversations by their latest approved reply, supports day and author
|
|
280
|
+
filters, and paginates older threads. Signed-in readers see which conversations became active since
|
|
281
|
+
their previous visit. Reflection and reply drafts are retained for up to 30 days in that browser and
|
|
282
|
+
cleared after a successful post. On a shared device, clearing browser storage also removes saved drafts.
|
|
283
|
+
|
|
284
|
+
After upgrading, copy and run migration `20260101000017`, which stores each reader's last visit to the
|
|
285
|
+
Reflections page:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
bin/rails bible270:install:migrations
|
|
289
|
+
bin/rails db:migrate
|
|
290
|
+
```
|
|
291
|
+
|
|
275
292
|
Break points can also live in your app rather than the gem, and be changed without a restart:
|
|
276
293
|
|
|
277
294
|
```ruby
|
|
@@ -291,17 +308,17 @@ The plan runs **undated** (day numbers only) or **dated** (days mapped onto a ca
|
|
|
291
308
|
|
|
292
309
|
```ruby
|
|
293
310
|
config.start_date = Date.new(2026, 9, 6) # Date, Time, or "YYYY-MM-DD". nil = undated
|
|
294
|
-
config.allow_reader_start_date = true #
|
|
311
|
+
config.allow_reader_start_date = true # allow personal per-reader calendars
|
|
295
312
|
```
|
|
296
313
|
|
|
297
314
|
| `start_date` | `allow_reader_start_date` | What happens |
|
|
298
315
|
|---|---|---|
|
|
299
|
-
| nil | true *(default)* | Each reader gets stamped on first check-off
|
|
300
|
-
| a date | true |
|
|
301
|
-
| a date | false | Everyone pinned to the community date
|
|
316
|
+
| nil | true *(default)* | Each reader gets stamped on their first check-off; an admin may adjust the date |
|
|
317
|
+
| a date | true | The community date is the default; an admin may assign an individual date |
|
|
318
|
+
| a date | false | Everyone is pinned to the community date; stored individual dates are ignored |
|
|
302
319
|
| nil | false | Fully undated — no calendar anywhere |
|
|
303
320
|
|
|
304
|
-
Dated plans show each day's date with a **Today** badge
|
|
321
|
+
Dated plans show each day's date with a **Today** badge and a "Go to today" link. Administrators may re-map an individual reader's calendar from the admin panel; check-offs and reflections remain keyed to day numbers, so nothing in their history moves.
|
|
305
322
|
|
|
306
323
|
Date helpers are pure functions, usable anywhere:
|
|
307
324
|
|
|
@@ -313,6 +330,38 @@ Bible270::Plan.day_for(date, start, clamp: false) # => -3 or 271, to spot out-of
|
|
|
313
330
|
Bible270::Plan.before_start?(date, start)
|
|
314
331
|
```
|
|
315
332
|
|
|
333
|
+
## Daily reading reminders
|
|
334
|
+
|
|
335
|
+
Daily reminders are optional at both levels: the host enables the feature, then each reader chooses
|
|
336
|
+
whether to receive it and at what local time from their profile. The feature and reader opt-in both
|
|
337
|
+
default to off; each reader's time defaults to `08:00`.
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
# config/initializers/bible270.rb
|
|
341
|
+
Bible270.configure do |config|
|
|
342
|
+
config.daily_reminders = true
|
|
343
|
+
config.mailer_from = "no-reply@yourdomain.com"
|
|
344
|
+
config.mailer_host = "yourdomain.com" # builds links back to the day and profile
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
After upgrading, copy and run migration `20260101000016`, which adds the reader opt-in, preferred
|
|
349
|
+
time, and last-sent date. Bible270 does not install a scheduler. The host application should run this
|
|
350
|
+
task periodically—every 15 minutes is recommended—using its existing cron, platform scheduler, or
|
|
351
|
+
recurring-job system:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
bin/rails bible270:reminders:send
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Each run converts `Time.now` through `Bible270.local_time`, waits until each reader's chosen `HH:MM`
|
|
358
|
+
time, sends inline with `deliver_now`, and prints the number delivered. Set `config.time_zone` when the
|
|
359
|
+
plan should not follow the server's local clock. Running periodically is safe because delivery is
|
|
360
|
+
idempotent once per local date: readers already marked for that date are skipped. Undated readers,
|
|
361
|
+
calendar dates before or after their plan, completed days, blank email addresses, and readers who have
|
|
362
|
+
not opted in are also skipped. A failure for one reader is logged and does not stop the rest. The sent
|
|
363
|
+
date is recorded only after delivery succeeds.
|
|
364
|
+
|
|
316
365
|
## Configuration reference
|
|
317
366
|
|
|
318
367
|
```ruby
|
|
@@ -326,6 +375,10 @@ Bible270.configure do |config|
|
|
|
326
375
|
# config.start_date = Date.new(2026, 9, 6)
|
|
327
376
|
config.allow_reader_start_date = true
|
|
328
377
|
|
|
378
|
+
config.daily_reminders = false # schedule reminders:send every 15 minutes
|
|
379
|
+
config.mention_notifications = true # replies and @mentions; readers may opt out
|
|
380
|
+
config.mailer_host = "example.com" # links in reminder/notice email
|
|
381
|
+
|
|
329
382
|
config.parent_controller = "ActionController::Base" # or "::ApplicationController"
|
|
330
383
|
config.layout = "bible270/application"
|
|
331
384
|
|
|
@@ -397,7 +450,7 @@ The CMS serves your content; this is a separate mounted app. Either **just link
|
|
|
397
450
|
|
|
398
451
|
| Table | Holds |
|
|
399
452
|
|-------|-------|
|
|
400
|
-
| `bible270_readers` | identity, avatar, provider/uid or polymorphic `owner`, start date, translation
|
|
453
|
+
| `bible270_readers` | identity, avatar, provider/uid or polymorphic `owner`, start date, translation, passage-reader, and reminder preferences |
|
|
401
454
|
| `bible270_checkoffs` | one row per reader per day per track (`ot`/`nt`/`pp`), unique-indexed |
|
|
402
455
|
| `bible270_comments` | a reflection on a day, optionally scoped to a track, public to all |
|
|
403
456
|
| `bible270_sign_in_tokens` | magic-link tokens: email, **digest only**, expiry, consumed-at |
|
|
@@ -413,8 +466,6 @@ GET /day/:day days#show readings, who's finished, refle
|
|
|
413
466
|
POST /day/:day/toggle/:track checkoffs#toggle
|
|
414
467
|
POST /day/:day/comments comments#create
|
|
415
468
|
DELETE /comments/:id comments#destroy
|
|
416
|
-
PATCH /start-date readers#update_start_date
|
|
417
|
-
DELETE /start-date readers#clear_start_date
|
|
418
469
|
GET /community readers#index leaderboard
|
|
419
470
|
GET /readers/:id readers#show
|
|
420
471
|
GET /sign_in sessions#new email form + provider list
|
|
@@ -471,7 +522,7 @@ All computed at load and memoized — no generated schedule, so nothing to migra
|
|
|
471
522
|
|
|
472
523
|
## Scaling
|
|
473
524
|
|
|
474
|
-
|
|
525
|
+
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
526
|
|
|
476
527
|
## Troubleshooting
|
|
477
528
|
|
|
@@ -499,9 +550,18 @@ The engine's CSS rides in a partial its own layout renders. Using your layout? A
|
|
|
499
550
|
|
|
500
551
|
```bash
|
|
501
552
|
bundle install
|
|
502
|
-
rake test
|
|
553
|
+
bundle exec rake test
|
|
554
|
+
bundle exec rubocop --cache false
|
|
503
555
|
```
|
|
504
556
|
|
|
557
|
+
`rake test` runs two isolated processes, streams colorized progress in real time, then prints one
|
|
558
|
+
consolidated result and duration. It merges their SimpleCov results, writes a browsable report to
|
|
559
|
+
`coverage/index.html`, and
|
|
560
|
+
records per-file runtimes in `tmp/parallel_runtime_test.log` to improve
|
|
561
|
+
subsequent balancing. Set `PARALLEL_WORKERS` to tune the worker count. Use
|
|
562
|
+
`bundle exec rake test:serial` when a serial coverage run is specifically needed; it writes to the same
|
|
563
|
+
project-local `coverage/` directory.
|
|
564
|
+
|
|
505
565
|
### Get an email link for local testing
|
|
506
566
|
|
|
507
567
|
in the rails console: `rails c`
|
|
@@ -518,7 +578,7 @@ Bug reports and pull requests are welcome at <https://github.com/avonderluft/bib
|
|
|
518
578
|
|
|
519
579
|
1. Fork it and clone your fork.
|
|
520
580
|
2. Branch off `main`: `git checkout -b my-feature`.
|
|
521
|
-
3. Make your change, and add tests — `rake test` should stay green.
|
|
581
|
+
3. Make your change, and add tests — `bundle exec rake test` and `bundle exec rubocop --cache false` should stay green.
|
|
522
582
|
4. Commit with a clear message: `git commit -am "Add my feature"`.
|
|
523
583
|
5. Push: `git push origin my-feature`.
|
|
524
584
|
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
|
|
@@ -130,9 +130,8 @@ module Bible270
|
|
|
130
130
|
|
|
131
131
|
def update_start
|
|
132
132
|
if params[:start_date].present?
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
# their own dates.
|
|
133
|
+
# The administrator may store an individual date even when personal
|
|
134
|
+
# calendars are currently disabled for this installation.
|
|
136
135
|
if @reader.set_start_date!(params[:start_date])
|
|
137
136
|
redirect_to admin_reader_path(@reader),
|
|
138
137
|
notice: "Start date set to #{@reader.started_on.strftime('%B %-d, %Y')}."
|
|
@@ -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
|