bible270 0.6.2
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 +7 -0
- data/CHANGELOG.md +181 -0
- data/MIT-LICENSE +18 -0
- data/README.md +373 -0
- data/Rakefile +11 -0
- data/app/controllers/bible270/application_controller.rb +48 -0
- data/app/controllers/bible270/checkoffs_controller.rb +32 -0
- data/app/controllers/bible270/comments_controller.rb +46 -0
- data/app/controllers/bible270/days_controller.rb +42 -0
- data/app/controllers/bible270/readers_controller.rb +46 -0
- data/app/controllers/bible270/sessions_controller.rb +126 -0
- data/app/helpers/bible270/plan_helper.rb +45 -0
- data/app/mailers/bible270/application_mailer.rb +7 -0
- data/app/mailers/bible270/sign_in_mailer.rb +14 -0
- data/app/models/bible270/application_record.rb +6 -0
- data/app/models/bible270/checkoff.rb +23 -0
- data/app/models/bible270/comment.rb +16 -0
- data/app/models/bible270/reader.rb +211 -0
- data/app/models/bible270/sign_in_token.rb +74 -0
- data/app/views/bible270/checkoffs/toggle.turbo_stream.erb +4 -0
- data/app/views/bible270/comments/_comment.html.erb +15 -0
- data/app/views/bible270/comments/_form.html.erb +13 -0
- data/app/views/bible270/comments/create.turbo_stream.erb +7 -0
- data/app/views/bible270/comments/destroy.turbo_stream.erb +1 -0
- data/app/views/bible270/days/_reading.html.erb +25 -0
- data/app/views/bible270/days/_start_date.html.erb +43 -0
- data/app/views/bible270/days/index.html.erb +72 -0
- data/app/views/bible270/days/show.html.erb +50 -0
- data/app/views/bible270/readers/index.html.erb +24 -0
- data/app/views/bible270/readers/show.html.erb +41 -0
- data/app/views/bible270/sessions/new.html.erb +52 -0
- data/app/views/bible270/shared/_header.html.erb +20 -0
- data/app/views/bible270/shared/_styles.html.erb +147 -0
- data/app/views/bible270/sign_in_mailer/magic_link.html.erb +20 -0
- data/app/views/bible270/sign_in_mailer/magic_link.text.erb +7 -0
- data/app/views/layouts/bible270/application.html.erb +28 -0
- data/config/routes.rb +30 -0
- data/db/migrate/20260101000001_create_bible270_readers.rb +17 -0
- data/db/migrate/20260101000002_create_bible270_checkoffs.rb +15 -0
- data/db/migrate/20260101000003_create_bible270_comments.rb +14 -0
- data/db/migrate/20260101000004_create_bible270_sign_in_tokens.rb +18 -0
- data/lib/bible270/configuration.rb +177 -0
- data/lib/bible270/email_sign_in.rb +68 -0
- data/lib/bible270/engine.rb +21 -0
- data/lib/bible270/plan.rb +352 -0
- data/lib/bible270/versification.rb +83 -0
- data/lib/bible270/version.rb +4 -0
- data/lib/bible270.rb +9 -0
- data/lib/generators/bible270/install/install_generator.rb +66 -0
- data/lib/generators/bible270/install/templates/bible270.rb.tt +41 -0
- data/lib/generators/bible270/install/templates/omniauth.rb.tt +25 -0
- metadata +158 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c9baaae6c81c3923b1997672d8f13ff5edd97dba42c56f29ec86fee53c508512
|
|
4
|
+
data.tar.gz: c564d896cd9144cee1aa4321e0cc39aa77f3197fb7a6f599045053b0fb674204
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0d2de30b25a9be283d994aef6db3c5743e7a3c112208eae6a3aa6b66cb45d893312cba18321f02dc240699ac774f13a9361cdb59b063c95e709cf934a0d602cc
|
|
7
|
+
data.tar.gz: cf3908da790617444a1ef1717c533912d1303ca6c5ca32624186262964a836997639940d578eea1c7f06c61ad3dd9d00f1e23401ce3cfb8e21d8f8084d62ba21
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to bible270. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
4
|
+
this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html), treating pre-1.0 minor
|
|
5
|
+
bumps as the place breaking changes may land.
|
|
6
|
+
|
|
7
|
+
## [0.6.2] — 2026-07-24
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Gem metadata now points at the actual repository rather than reusing the homepage:
|
|
11
|
+
`source_code_uri`, `bug_tracker_uri`, and `changelog_uri` all resolve to
|
|
12
|
+
<https://github.com/avonderluft/bible270>.
|
|
13
|
+
- `rubygems_mfa_required` is set, so pushes to rubygems.org require MFA.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- This changelog, packaged with the gem.
|
|
17
|
+
|
|
18
|
+
## [0.6.1] — 2026-07-24
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Author and copyright now read "Andrew vonderLuft".
|
|
22
|
+
- The display-name-from-email example in the docs and tests uses a neutral address, since the
|
|
23
|
+
algorithm can't recover internal capitals and shouldn't be shown rendering a real name wrongly.
|
|
24
|
+
|
|
25
|
+
## [0.6.0] — 2026-07-24
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **Passwordless email sign-in**, so readers without a GitHub/Google/etc. account can take part.
|
|
29
|
+
A reader enters their address, receives a single-use link, and clicks it — no password anywhere
|
|
30
|
+
in the gem.
|
|
31
|
+
- Tokens are 256-bit and URL-safe, expire after 20 minutes (`email_sign_in_ttl`), and are stored
|
|
32
|
+
**only as a SHA-256 digest**, so the table is useless to anyone reading the database.
|
|
33
|
+
- Claiming a link is a conditional `UPDATE`, so a double-clicked link cannot sign in twice.
|
|
34
|
+
- The "check your inbox" response is identical for known, unknown, and rate-limited addresses:
|
|
35
|
+
no account enumeration and no signal that a limit was hit.
|
|
36
|
+
- Rate limited per address per window (`email_sign_in_max_per_window`, `email_sign_in_window`).
|
|
37
|
+
- Readers may choose the display name shown beside their reflections
|
|
38
|
+
(`email_sign_in_ask_name`); otherwise it is derived from the address.
|
|
39
|
+
- `Bible270::SignInToken.sweep!` clears spent and stale rows from a cron or rake task.
|
|
40
|
+
- `Bible270::EmailSignIn` — Rails-free module holding the normalization and token logic, so the
|
|
41
|
+
security-relevant parts are unit testable on their own.
|
|
42
|
+
- `Bible270::SignInMailer` with text and HTML magic-link templates. The sign-in URL is built by the
|
|
43
|
+
controller, so the mailer needs no `default_url_options`.
|
|
44
|
+
- `Bible270::Reader.from_email`, using the same identity columns as OmniAuth with `provider: "email"`,
|
|
45
|
+
so every reader is treated identically downstream.
|
|
46
|
+
- New table: `bible270_sign_in_tokens` (migration `20260101000004`).
|
|
47
|
+
- `config.any_sign_in_method?`, letting views detect a deployment with no sign-in configured.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
- `display_name_from` raised `ArgumentError` on any address containing an underscore:
|
|
51
|
+
`tr("._-+", …)` was parsed as a character range. The hyphen now comes last.
|
|
52
|
+
|
|
53
|
+
### Requirements
|
|
54
|
+
- Email sign-in needs working Action Mailer delivery in the host application. Set
|
|
55
|
+
`config.mailer_from`. Delivery is inline by default; set `email_sign_in_deliver_later` if a
|
|
56
|
+
queue backend is available.
|
|
57
|
+
|
|
58
|
+
## [0.5.0] — 2026-07-24
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
- Built-in OmniAuth sign-in is now fully wired as the primary auth path.
|
|
62
|
+
- `config.omniauth_providers` accepts symbols or `[provider, label]` pairs, with labels for common
|
|
63
|
+
providers and titleisation for anything custom.
|
|
64
|
+
- Sign-in page at `GET <mount>/sign_in` listing every configured provider.
|
|
65
|
+
- Sign-in controls carry the current path as `origin`, so a reader who clicks a check-off while
|
|
66
|
+
signed out returns to that same day. Origins are validated as local paths only.
|
|
67
|
+
- `rails generate bible270:install`, which writes both initializers with OmniAuth's `path_prefix`
|
|
68
|
+
already aligned to the mount point, adds the `mount` line, and prints the callback URL to register.
|
|
69
|
+
- `omniauth` (>= 2.0) and `omniauth-rails_csrf_protection` (>= 1.0) as runtime dependencies. Provider
|
|
70
|
+
strategy gems remain the host's choice.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
- **Sign-in was broken.** The header rendered a GET link to `/auth/github`, but OmniAuth 2.0 disabled
|
|
74
|
+
GET on the request phase (CVE-2015-9284). All sign-in controls are now POST forms with CSRF tokens.
|
|
75
|
+
The old link also contained a dead ternary and hardcoded the provider.
|
|
76
|
+
- Open-redirect hardening on the `origin` parameter: protocol-relative URLs, backslashes (which
|
|
77
|
+
browsers may normalise to slashes), and auth routes are all rejected.
|
|
78
|
+
- `reset_session` on sign-in and sign-out, against session fixation.
|
|
79
|
+
- `Reader.from_omniauth` no longer assumes an `OmniAuth::AuthHash`; it tolerates plain hashes and
|
|
80
|
+
missing `info`, and returns `nil` rather than raising on a malformed payload.
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
- `sign_out` is `DELETE` only (previously `GET` or `DELETE`).
|
|
84
|
+
- Guarded actions redirect to the sign-in page instead of `request.referer`.
|
|
85
|
+
|
|
86
|
+
## [0.4.0] — 2026-07-24
|
|
87
|
+
|
|
88
|
+
### Added
|
|
89
|
+
- **Configurable start dates.** `config.start_date` sets a community-wide cohort date (accepting a
|
|
90
|
+
`Date`, `Time`, or `"YYYY-MM-DD"` string); `config.allow_reader_start_date` controls whether
|
|
91
|
+
individuals may set their own. The four combinations give undated, per-reader, community-with-
|
|
92
|
+
override, and fully pinned modes.
|
|
93
|
+
- Readers manage their own date from the overview (`PATCH`/`DELETE <mount>/start-date`).
|
|
94
|
+
- Calendar mapping as pure functions on `Bible270::Plan`: `date_for`, `day_for` (clamped by default,
|
|
95
|
+
`clamp: false` to detect out-of-range), `end_date_for`, `before_start?`, `after_end?`, `to_date`.
|
|
96
|
+
- Day pages show their calendar date with a **Today** badge; the overview gains a "Go to today" link
|
|
97
|
+
and a pace indicator ("3 days behind", "right on pace", "begins in 12 days").
|
|
98
|
+
|
|
99
|
+
### Notes
|
|
100
|
+
- Check-offs and comments are keyed to day numbers, never dates, so changing a start date only
|
|
101
|
+
re-maps the calendar — no reading history moves or is lost.
|
|
102
|
+
- A malformed `config.start_date` degrades to `nil` rather than raising at boot; the controller does
|
|
103
|
+
reject bad input with a flash message.
|
|
104
|
+
|
|
105
|
+
## [0.3.1] — 2026-07-24
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
- **Ruby 4.0 compatibility.** Dropped the `cgi` dependency: the CGI library was removed from Ruby's
|
|
109
|
+
default gems in 4.0 (only `cgi/escape` remains), so `require "cgi"` emits a deprecation warning
|
|
110
|
+
there. URL escaping now uses `URI.encode_www_form_component`, which produces byte-identical output
|
|
111
|
+
to `CGI.escape` and relies only on `uri`, a stable default gem.
|
|
112
|
+
|
|
113
|
+
### Added
|
|
114
|
+
- Regression test asserting the gem never loads the `cgi` library.
|
|
115
|
+
- Compatibility notes in the README: audited against the Ruby 4.0 breaking changes (`Set`/`SortedSet`,
|
|
116
|
+
`Ractor`, `Net::HTTP`, `Process::Status`, `ObjectSpace`, and the gems promoted from default to
|
|
117
|
+
bundled). Every file carries a `frozen_string_literal` comment and the suite passes with
|
|
118
|
+
`--enable-frozen-string-literal` forced.
|
|
119
|
+
|
|
120
|
+
## [0.3.0] — 2026-07-24
|
|
121
|
+
|
|
122
|
+
### Changed — BREAKING
|
|
123
|
+
- **Renamed from `bible_reading_plan` to `bible270`.** Module `Bible270`, tables `bible270_*`,
|
|
124
|
+
migrations `CreateBible270*`, CSS and helper prefix `b270`, install task
|
|
125
|
+
`bible270:install:migrations`, mount `Bible270::Engine`. Table names changed, so this is a fresh
|
|
126
|
+
install rather than an upgrade.
|
|
127
|
+
- **The New Testament is now read twice.** Each pass gets its own half of the plan (135 days), so
|
|
128
|
+
Revelation lands on day 135 and again on day 270. Whole chapters are verse-balanced within each
|
|
129
|
+
pass, tightening NT days from 11–140 verses to 20–104. The NT track now has content every day, so
|
|
130
|
+
there are no rest days and all three tracks are present on all 270 days.
|
|
131
|
+
- **Chapters are kept whole.** Only a chapter longer than `PP_LONG_CHAPTER` (100 verses) is divided,
|
|
132
|
+
which in the whole Psalter and Proverbs is Psalm 119 alone — split into exactly two readings
|
|
133
|
+
(1–88, 89–176). Psalms 78 and 89 are no longer split. Short chapters merge with a neighbour, so
|
|
134
|
+
Psalm 117 reads as "Psalm 116–117" rather than occupying a two-verse day.
|
|
135
|
+
- The Psalms/Proverbs companion resolves to exactly 135 portions, making two complete passes across
|
|
136
|
+
270 days and finishing on Proverbs 31 — so all three tracks finish together at the halfway mark
|
|
137
|
+
and at the end.
|
|
138
|
+
|
|
139
|
+
### Added
|
|
140
|
+
- `Plan.nt_days_per_pass`, `nt_second_pass_start_day`, `nt_verse_loads`, `pp_cycle_length`,
|
|
141
|
+
`pp_base_portions`, and tuning constants documented in the README.
|
|
142
|
+
|
|
143
|
+
## [0.2.0] — 2026-07-24
|
|
144
|
+
|
|
145
|
+
### Changed
|
|
146
|
+
- **Daily portions are balanced by verse count, not chapter count**, so each day takes roughly the
|
|
147
|
+
same time to read. Previously a day on Psalm 119 (176 verses) counted the same as a day on Psalm
|
|
148
|
+
117 (2 verses).
|
|
149
|
+
- Old Testament days are grouped to be even in verses (avg ~73/day).
|
|
150
|
+
|
|
151
|
+
### Added
|
|
152
|
+
- `Bible270::Versification` — per-chapter verse counts for all 66 books, generated from a
|
|
153
|
+
public-domain KJV text and validated against canonical anchors (1,189 chapters; Psalms 2,461
|
|
154
|
+
verses; Proverbs 915; Psalm 119 = 176; Psalm 117 = 2). Verse counts are facts about the text's
|
|
155
|
+
structure, so nothing copyrighted is bundled.
|
|
156
|
+
|
|
157
|
+
### Fixed
|
|
158
|
+
- Per-track progress bars compared days checked against *chapter* totals, so the Old Testament bar
|
|
159
|
+
could never exceed ~36%. Renamed `chapters_read_in` to `days_read_in` and made the totals
|
|
160
|
+
day-based.
|
|
161
|
+
|
|
162
|
+
## [0.1.0] — 2026-07-24
|
|
163
|
+
|
|
164
|
+
### Added
|
|
165
|
+
- Initial release: a mountable Rails engine providing a 270-day Bible reading plan with three daily
|
|
166
|
+
tracks (Old Testament, New Testament, Psalms/Proverbs).
|
|
167
|
+
- Per-reader check-offs, public per-day reflections, a community leaderboard, and reader profiles.
|
|
168
|
+
- Two identity paths: built-in session sign-in, or bridging the host application's users via
|
|
169
|
+
`config.current_reader_resolver` and `Reader.for_owner`.
|
|
170
|
+
- The schedule is pure, deterministic Ruby — no rows are stored for the plan itself; the database
|
|
171
|
+
holds only readers, check-offs, and comments.
|
|
172
|
+
|
|
173
|
+
[0.6.2]: https://github.com/avonderluft/bible270/releases/tag/v0.6.2
|
|
174
|
+
[0.6.1]: https://github.com/avonderluft/bible270/releases/tag/v0.6.1
|
|
175
|
+
[0.6.0]: https://github.com/avonderluft/bible270/releases/tag/v0.6.0
|
|
176
|
+
[0.5.0]: https://github.com/avonderluft/bible270/releases/tag/v0.5.0
|
|
177
|
+
[0.4.0]: https://github.com/avonderluft/bible270/releases/tag/v0.4.0
|
|
178
|
+
[0.3.1]: https://github.com/avonderluft/bible270/releases/tag/v0.3.1
|
|
179
|
+
[0.3.0]: https://github.com/avonderluft/bible270/releases/tag/v0.3.0
|
|
180
|
+
[0.2.0]: https://github.com/avonderluft/bible270/releases/tag/v0.2.0
|
|
181
|
+
[0.1.0]: https://github.com/avonderluft/bible270/releases/tag/v0.1.0
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright (c) 2026 Andrew vonderLuft
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
7
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
8
|
+
subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
17
|
+
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
18
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# bible270
|
|
2
|
+
|
|
3
|
+
A mountable **Rails engine** that adds a social, 270-day Bible reading plan to a
|
|
4
|
+
host Rails application such as [ComfortableMediaSurfer](https://github.com/shakacode/comfortable-mexican-sofa).
|
|
5
|
+
|
|
6
|
+
Each day presents three readings, sized by **verse count** so daily portions take roughly
|
|
7
|
+
the same time to read. Chapters are kept **whole** — the plan never chops a chapter into
|
|
8
|
+
verse ranges except for the one excessively long chapter, Psalm 119.
|
|
9
|
+
|
|
10
|
+
- **Old Testament** — read once, cover to cover. Whole chapters grouped so each day is
|
|
11
|
+
~equal in verses (avg ~73/day, range 38–116).
|
|
12
|
+
- **New Testament** — read **twice**. Each pass gets half the plan (135 days), so
|
|
13
|
+
Revelation lands on day 135 and again on day 270. ~2 chapters/day, every day, verse-balanced
|
|
14
|
+
within each pass.
|
|
15
|
+
- **Psalms & Proverbs** — a whole-chapter companion that runs **twice** over the 270 days
|
|
16
|
+
(135 portions x 2). Very short chapters merge with a neighbour so there are no trivial
|
|
17
|
+
two-verse days (Psalm 117 reads as "Psalm 116–117"), and **Psalm 119 (176 verses) is the
|
|
18
|
+
sole chapter divided** — into exactly two readings, 1–88 and 89–176.
|
|
19
|
+
|
|
20
|
+
Genesis→Malachi, the second pass through Revelation, and Proverbs 31 all finish together on
|
|
21
|
+
day 270. A typical day runs ~157 verses across the three tracks (range 102–238).
|
|
22
|
+
|
|
23
|
+
Readers check off each track, leave reflections (comments) on any day, and can see everyone
|
|
24
|
+
else's progress and reflections — similar to the social plans on Bible.com.
|
|
25
|
+
|
|
26
|
+
The plan itself is pure, deterministic Ruby (`Bible270::Plan`) — no rows are stored for the
|
|
27
|
+
schedule. Daily portions are sized using per-chapter verse counts (`Bible270::Versification`,
|
|
28
|
+
standard English/KJV versification; verse counts are facts about the text's structure, so
|
|
29
|
+
nothing copyrighted is bundled). The database only holds **readers, check-offs, and comments**.
|
|
30
|
+
|
|
31
|
+
## Requirements & compatibility
|
|
32
|
+
|
|
33
|
+
- **Ruby** >= 3.0 (the gemspec's `required_ruby_version`). The plan logic is plain Ruby with
|
|
34
|
+
no C extensions and no dependency on any gem that Ruby 4.0 unbundled.
|
|
35
|
+
- **Ruby 4.0 notes.** Audited against the 4.0 breaking changes: the gem does not use `cgi`
|
|
36
|
+
(removed from default gems in 4.0 — URL escaping goes through `URI.encode_www_form_component`
|
|
37
|
+
instead, which is byte-identical to `CGI.escape`), `Set`/`SortedSet`, `Ractor`, `Net::HTTP`,
|
|
38
|
+
`Process::Status`, `ObjectSpace`, or any of the gems promoted from default to bundled
|
|
39
|
+
(`ostruct`, `logger`, `benchmark`, `pstore`, `irb`, `rdoc`). Every file carries a
|
|
40
|
+
`# frozen_string_literal: true` magic comment and no string literal is mutated.
|
|
41
|
+
- **Rails.** The gemspec allows `rails >= 7.0`, but on Ruby 4.0 the binding constraint is your
|
|
42
|
+
host app, not this engine: Rails 8.0/8.1 require Ruby >= 3.2, and ComfortableMediaSurfer is a
|
|
43
|
+
"Rails 7.0+" engine. Verify your own lockfile (e.g. with RailsBump) before pairing an older
|
|
44
|
+
Rails with Ruby 4.0.
|
|
45
|
+
- A host application (this is an engine, not a standalone app).
|
|
46
|
+
- Turbo is used for seamless check-offs and degrades to full-page redirects if absent.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
Add to the host app's `Gemfile`:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
gem "bible270", path: "vendor/gems/bible270"
|
|
54
|
+
# or, once pushed to a git remote:
|
|
55
|
+
# gem "bible270", git: "https://github.com/avonderluft/bible270.git", branch: "main"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bundle install
|
|
62
|
+
bin/rails bible270:install:migrations
|
|
63
|
+
bin/rails db:migrate
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Mount it in `config/routes.rb`:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
mount Bible270::Engine, at: "/reading-plan"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The plan is now live at `/reading-plan`.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Authentication
|
|
77
|
+
|
|
78
|
+
Two built-in ways to sign in, so **nobody is excluded**:
|
|
79
|
+
|
|
80
|
+
1. **Email link (passwordless)** — the reader types their address, gets a one-time link, and
|
|
81
|
+
clicks it. No password, no account with anyone else. This is the default and it's what makes
|
|
82
|
+
the plan usable by people who don't have (or don't want to use) GitHub, Google, and friends.
|
|
83
|
+
2. **OmniAuth social sign-in** — optional convenience for those who do.
|
|
84
|
+
|
|
85
|
+
Either can run alone. `config.omniauth_providers = []` gives you an email-only site;
|
|
86
|
+
`config.email_sign_in = false` gives you social-only. Viewing the plan and reading others'
|
|
87
|
+
reflections stays public either way — only checking off and commenting require signing in.
|
|
88
|
+
|
|
89
|
+
### Email sign-in
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
Bible270.configure do |config|
|
|
93
|
+
config.email_sign_in = true
|
|
94
|
+
config.mailer_from = "no-reply@gknt.org"
|
|
95
|
+
config.email_sign_in_ask_name = true # reader picks their display name
|
|
96
|
+
# config.email_sign_in_ttl = 20 * 60 # link lifetime (seconds)
|
|
97
|
+
# config.email_sign_in_max_per_window = 5 # per address, per window
|
|
98
|
+
# config.email_sign_in_window = 15 * 60
|
|
99
|
+
# config.email_sign_in_deliver_later = true # needs an Active Job backend
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The host app needs working Action Mailer delivery — nothing else. How it behaves:
|
|
104
|
+
|
|
105
|
+
- Tokens are 256-bit, URL-safe, single-use, and expire (20 minutes by default).
|
|
106
|
+
- **Only a SHA-256 digest of the token is stored**, so the table is useless to anyone who reads
|
|
107
|
+
the database. There is no password column anywhere in this gem.
|
|
108
|
+
- Consuming a link is a conditional update, so a double-clicked link can't sign in twice.
|
|
109
|
+
- The "check your inbox" response is identical whether the address was known, unknown, or
|
|
110
|
+
rate-limited — no account enumeration and no hint that a limit was hit.
|
|
111
|
+
- Readers may set the display name shown beside their reflections; otherwise it's derived from the
|
|
112
|
+
address (`mary.anne.smith@…` → "Mary Anne Smith").
|
|
113
|
+
- Spent and stale tokens can be cleaned up with `Bible270::SignInToken.sweep!` from a cron/rake task.
|
|
114
|
+
|
|
115
|
+
An email reader gets `provider: "email"` in the same identity columns OmniAuth uses, so downstream
|
|
116
|
+
(progress, comments, leaderboard) treats every reader identically.
|
|
117
|
+
|
|
118
|
+
### OmniAuth social sign-in
|
|
119
|
+
|
|
120
|
+
### Quick setup
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
bin/rails generate bible270:install --mount-at=/reading-plan --providers=github
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
That writes `config/initializers/bible270.rb` (email sign-in enabled) and
|
|
127
|
+
`config/initializers/omniauth.rb` (with `path_prefix` already matching your mount point), and adds
|
|
128
|
+
the `mount` line to your routes. Pass `--providers=` with an empty value for an email-only site.
|
|
129
|
+
Then add the gems you need and migrate:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# Gemfile — omniauth and omniauth-rails_csrf_protection come in with bible270;
|
|
133
|
+
# the provider strategy is your choice:
|
|
134
|
+
gem "omniauth-github"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
bundle install
|
|
139
|
+
bin/rails bible270:install:migrations && bin/rails db:migrate
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Finally register the callback URL with the provider:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
https://gknt.org/reading-plan/auth/github/callback
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Manual setup
|
|
149
|
+
|
|
150
|
+
If you'd rather not use the generator, the only subtlety is that OmniAuth's `path_prefix` must
|
|
151
|
+
line up with where the engine is mounted, so its callback lands on the engine's route:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# config/initializers/omniauth.rb
|
|
155
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
|
156
|
+
path_prefix "/reading-plan/auth" # == <mount point>/auth
|
|
157
|
+
provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
OmniAuth.config.on_failure = proc { |env| Bible270::SessionsController.action(:failure).call(env) }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
# config/initializers/bible270.rb
|
|
165
|
+
Bible270.configure do |config|
|
|
166
|
+
config.omniauth_providers = [:github] # or [:github, [:google_oauth2, "Google"]]
|
|
167
|
+
config.parent_controller = "::ApplicationController"
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Notes worth knowing
|
|
172
|
+
|
|
173
|
+
- **Sign-in is a POST, not a link.** OmniAuth 2.0+ refuses GET on its request phase
|
|
174
|
+
(CVE-2015-9284), so every sign-in control the engine renders is a `button_to` carrying a CSRF
|
|
175
|
+
token, and `omniauth-rails_csrf_protection` provides the Rails-aware verifier. If you build your
|
|
176
|
+
own sign-in link, it must POST to `<mount>/auth/:provider`.
|
|
177
|
+
- **One provider vs several.** With a single configured provider the header shows a direct
|
|
178
|
+
"Sign in with X" button; with several it links to a sign-in page (`GET <mount>/sign_in`) listing
|
|
179
|
+
them all.
|
|
180
|
+
- **Return-to-origin.** Sign-in controls pass the current path as `origin`, so a reader who clicks
|
|
181
|
+
a check-off while signed out lands back on that same day afterwards. Origins are validated to be
|
|
182
|
+
local paths only.
|
|
183
|
+
- **Session hygiene.** `reset_session` runs on both sign-in and sign-out to avoid session fixation.
|
|
184
|
+
Sign-out is `DELETE <mount>/sign_out`.
|
|
185
|
+
- **What's stored.** Provider, uid, display name, email, and avatar URL — no tokens, no passwords.
|
|
186
|
+
|
|
187
|
+
### Alternative: bridge your host's own users
|
|
188
|
+
|
|
189
|
+
If the host app already has authentication and you'd rather not use OmniAuth at all, set a
|
|
190
|
+
resolver instead and the engine will never touch sessions:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
config.current_reader_resolver = lambda do |controller|
|
|
194
|
+
user = controller.send(:current_user)
|
|
195
|
+
next nil unless user
|
|
196
|
+
Bible270::Reader.for_owner(user, display_name: user.try(:name) || user.email,
|
|
197
|
+
email: user.try(:email), avatar_url: user.try(:avatar_url))
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`Reader.for_owner` links a reader to any host model polymorphically via `owner`. Note that
|
|
202
|
+
ComfortableMediaSurfer's admin authentication is separate from public site visitors, so for a
|
|
203
|
+
public plan on a CMS front-end the OmniAuth path above is usually the right fit.
|
|
204
|
+
|
|
205
|
+
## Start dates
|
|
206
|
+
|
|
207
|
+
The plan works either **undated** (day numbers only) or **dated** (day numbers mapped onto a
|
|
208
|
+
calendar). There are two levers:
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
Bible270.configure do |config|
|
|
212
|
+
# A community-wide start date — everyone reads together as a cohort.
|
|
213
|
+
# Accepts a Date, Time, or "YYYY-MM-DD" string. Default: nil (undated).
|
|
214
|
+
config.start_date = Date.new(2026, 9, 6)
|
|
215
|
+
|
|
216
|
+
# May an individual reader set/change their own start date? Default: true.
|
|
217
|
+
# Set false to pin everyone to config.start_date.
|
|
218
|
+
config.allow_reader_start_date = true
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
How the two combine:
|
|
223
|
+
|
|
224
|
+
| `start_date` | `allow_reader_start_date` | Behaviour |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| nil | true (default) | Each reader is stamped with their own start date the first time they check something off, and can change it. |
|
|
227
|
+
| a date | true | The community date is the default; any reader may override it with their own. |
|
|
228
|
+
| a date | false | Everyone is pinned to the community date. The per-reader form is hidden. |
|
|
229
|
+
| nil | false | Fully undated — day numbers only, no calendar anywhere. |
|
|
230
|
+
|
|
231
|
+
When a plan is dated, each day page shows its calendar date with a **Today** badge, the overview
|
|
232
|
+
gains a "Go to today" link, and readers see whether they're ahead of or behind the pace.
|
|
233
|
+
|
|
234
|
+
Readers manage their own date from the overview page (`PATCH /start-date`, `DELETE /start-date`).
|
|
235
|
+
Changing a start date only re-maps days onto the calendar — check-offs and reflections are keyed
|
|
236
|
+
to day numbers and are never touched.
|
|
237
|
+
|
|
238
|
+
The date helpers are pure functions on `Bible270::Plan`, so you can use them anywhere:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
Bible270::Plan.date_for(1, "2026-09-06") # => Sun, 06 Sep 2026
|
|
242
|
+
Bible270::Plan.end_date_for("2026-09-06") # => Wed, 02 Jun 2027 (day 270)
|
|
243
|
+
Bible270::Plan.day_for(Date.current, start) # => 42 (clamped to 1..270)
|
|
244
|
+
Bible270::Plan.day_for(date, start, clamp: false) # => -3 or 271, to detect out-of-range
|
|
245
|
+
Bible270::Plan.before_start?(date, start) # => true/false
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Configuration reference
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
Bible270.configure do |config|
|
|
252
|
+
config.app_name = "Daily Bread"
|
|
253
|
+
config.tagline = "A 270-day journey through Scripture"
|
|
254
|
+
|
|
255
|
+
config.start_date = nil # community start date, or nil for undated
|
|
256
|
+
config.allow_reader_start_date = true # may readers set their own?
|
|
257
|
+
|
|
258
|
+
config.parent_controller = "ActionController::Base" # or "::ApplicationController"
|
|
259
|
+
config.layout = "bible270/application"
|
|
260
|
+
|
|
261
|
+
config.email_sign_in = true # passwordless email link
|
|
262
|
+
config.mailer_from = "no-reply@example.com"
|
|
263
|
+
config.email_sign_in_ttl = 20 * 60
|
|
264
|
+
config.email_sign_in_ask_name = true
|
|
265
|
+
config.omniauth_providers = [:github] # optional social sign-in
|
|
266
|
+
config.omniauth_path_prefix = nil # nil = derive "<mount>/auth"
|
|
267
|
+
config.current_reader_resolver = nil # set to bridge host users instead
|
|
268
|
+
config.require_sign_in_to_participate = true
|
|
269
|
+
|
|
270
|
+
config.after_sign_in_path = nil # defaults to plan root
|
|
271
|
+
config.after_sign_out_path = nil
|
|
272
|
+
|
|
273
|
+
config.bible_version = "ESV"
|
|
274
|
+
config.passage_url_builder = ->(reference, version) {
|
|
275
|
+
"https://www.biblegateway.com/passage/?search=#{URI.encode_www_form_component(reference)}&version=#{version}"
|
|
276
|
+
}
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Every reading reference links out to the configured Bible reader (Bible Gateway by
|
|
281
|
+
default). Point `passage_url_builder` at your own reader if you host one.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Using it inside ComfortableMediaSurfer
|
|
286
|
+
|
|
287
|
+
ComfortableMediaSurfer serves marketing/content pages; this engine is a separate
|
|
288
|
+
mounted app. Two integration styles:
|
|
289
|
+
|
|
290
|
+
1. **Link to it** — mount at `/reading-plan` and add a CMS navigation link. The
|
|
291
|
+
engine renders its own themed pages. Simplest and fully featured.
|
|
292
|
+
|
|
293
|
+
2. **Match the site chrome** — set `config.parent_controller = "::ApplicationController"`
|
|
294
|
+
and `config.layout = "layouts/application"` (your site layout) so the plan pages
|
|
295
|
+
sit inside your normal header/footer. The engine's scoped CSS (all classes are
|
|
296
|
+
prefixed `b270-`) won't collide with CMS styles.
|
|
297
|
+
|
|
298
|
+
Because CMS admin auth (`ComfortableMediaSurfer`) is separate from public readers,
|
|
299
|
+
Option B (OmniAuth) is usually the right fit for public visitors on gknt.org.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Data model
|
|
304
|
+
|
|
305
|
+
| Table | Purpose |
|
|
306
|
+
|-------|---------|
|
|
307
|
+
| `bible270_readers` | identity: display name, avatar, provider/uid or polymorphic `owner`, `started_on` |
|
|
308
|
+
| `bible270_checkoffs` | one row per reader per day per track (`ot`/`nt`/`pp`); unique index prevents dups |
|
|
309
|
+
| `bible270_sign_in_tokens` | short-lived magic-link tokens: email, **digest only**, expiry, consumed-at |
|
|
310
|
+
| `bible270_comments` | a reflection on a day (optionally scoped to a track), public to all |
|
|
311
|
+
|
|
312
|
+
A day counts as "complete" for a reader when they've checked every track that has
|
|
313
|
+
content that day (2 on a light-NT day, otherwise 3). All progress and comments are
|
|
314
|
+
**public** to other readers by design (mirroring Bible.com's shared plans).
|
|
315
|
+
|
|
316
|
+
## Routes (within the mount point)
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
GET / days#index overview + community + calendar
|
|
320
|
+
GET /day/:day days#show three readings, completers, reflections
|
|
321
|
+
POST /day/:day/toggle/:track checkoffs#toggle
|
|
322
|
+
POST /day/:day/comments comments#create
|
|
323
|
+
DELETE /comments/:id comments#destroy
|
|
324
|
+
PATCH /start-date readers#update_start_date
|
|
325
|
+
DELETE /start-date readers#clear_start_date
|
|
326
|
+
GET /community readers#index leaderboard
|
|
327
|
+
GET /readers/:id readers#show a reader's progress + reflections
|
|
328
|
+
GET /sign_in sessions#new email form + provider list
|
|
329
|
+
POST /sign_in/email sessions#email_link send a magic link
|
|
330
|
+
GET /sign_in/email/:token sessions#email_callback consume a magic link
|
|
331
|
+
GET /auth/:provider/callback sessions#create (OmniAuth callback)
|
|
332
|
+
POST /auth/:provider/callback sessions#create
|
|
333
|
+
GET /auth/failure sessions#failure
|
|
334
|
+
DELETE /sign_out sessions#destroy
|
|
335
|
+
|
|
336
|
+
POST <mount>/auth/:provider handled by the OmniAuth middleware, not the engine
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Tuning the plan shape
|
|
340
|
+
|
|
341
|
+
The whole schedule is derived from a handful of constants in `Bible270::Plan`, so the shape
|
|
342
|
+
can be changed without touching anything else:
|
|
343
|
+
|
|
344
|
+
| Constant | Default | Effect |
|
|
345
|
+
|----------|---------|--------|
|
|
346
|
+
| `DAYS` | `270` | plan length |
|
|
347
|
+
| `NT_PASSES` | `2` | how many times the New Testament is read (each pass gets `DAYS / NT_PASSES` days) |
|
|
348
|
+
| `PP_LONG_CHAPTER` | `100` | a chapter longer than this is divided. At 100 only Psalm 119 (176v) qualifies, since the next longest is Psalm 78 at 72v. Raise it above 176 to never split anything |
|
|
349
|
+
| `PP_SPLIT_PARTS` | `2` | how many readings such a chapter becomes |
|
|
350
|
+
| `PP_MIN_DAY` / `PP_DAY_TARGET` | `12` / `22` | how aggressively short chapters merge. These two values are what make the companion come out to exactly 135 portions, i.e. two clean passes across 270 days |
|
|
351
|
+
|
|
352
|
+
Everything is computed at load time and memoized; there is no generated schedule to migrate
|
|
353
|
+
if you change these.
|
|
354
|
+
|
|
355
|
+
## Scaling note
|
|
356
|
+
|
|
357
|
+
The community leaderboard computes completed-days in Ruby from grouped check-off
|
|
358
|
+
counts. That's ideal for a homelab/parish-sized community (hundreds of readers). If
|
|
359
|
+
you grow to many thousands, add a cached `days_completed` counter on `Reader`
|
|
360
|
+
(updated in a `Checkoff` after_commit) and sort in SQL.
|
|
361
|
+
|
|
362
|
+
## Development / tests
|
|
363
|
+
|
|
364
|
+
The deterministic plan logic is fully unit-tested with no Rails dependency:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
bundle install
|
|
368
|
+
rake test
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## License
|
|
372
|
+
|
|
373
|
+
MIT. See `MIT-LICENSE`.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "bundler/gem_tasks" # provides build / install / release
|
|
3
|
+
require "rake/testtask"
|
|
4
|
+
|
|
5
|
+
Rake::TestTask.new(:test) do |t|
|
|
6
|
+
t.libs << "test" << "lib"
|
|
7
|
+
t.pattern = "test/**/*_test.rb"
|
|
8
|
+
t.warning = false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
task default: :test
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Bible270
|
|
3
|
+
class ApplicationController < Bible270.config.parent_controller.constantize
|
|
4
|
+
layout :bible270_layout
|
|
5
|
+
|
|
6
|
+
helper_method :current_reader, :signed_in?, :b270_config
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def bible270_layout
|
|
11
|
+
Bible270.config.layout
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def b270_config
|
|
15
|
+
Bible270.config
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def current_reader
|
|
19
|
+
return @current_reader if defined?(@current_reader)
|
|
20
|
+
|
|
21
|
+
@current_reader =
|
|
22
|
+
if (resolver = Bible270.config.current_reader_resolver)
|
|
23
|
+
resolver.call(self)
|
|
24
|
+
elsif (id = session[:bible270_reader_id])
|
|
25
|
+
Reader.find_by(id: id)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def signed_in?
|
|
30
|
+
current_reader.present?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Guard participation (checking off / commenting). Viewing is always open.
|
|
34
|
+
def require_reader!
|
|
35
|
+
return true if signed_in?
|
|
36
|
+
return true unless Bible270.config.require_sign_in_to_participate
|
|
37
|
+
|
|
38
|
+
respond_to do |format|
|
|
39
|
+
format.turbo_stream { head :unauthorized }
|
|
40
|
+
format.html do
|
|
41
|
+
redirect_to sign_in_path(origin: request.fullpath),
|
|
42
|
+
alert: "Please sign in to take part."
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Bible270
|
|
3
|
+
class CheckoffsController < ApplicationController
|
|
4
|
+
def toggle
|
|
5
|
+
return unless require_reader!
|
|
6
|
+
|
|
7
|
+
@day = params[:day].to_i
|
|
8
|
+
@track = params[:track].to_s
|
|
9
|
+
|
|
10
|
+
unless Plan.valid_day?(@day) && Plan.present_tracks(@day).include?(@track)
|
|
11
|
+
head :bad_request and return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
existing = current_reader.checkoffs.find_by(day: @day, track: @track)
|
|
15
|
+
if existing
|
|
16
|
+
existing.destroy
|
|
17
|
+
else
|
|
18
|
+
current_reader.ensure_started!
|
|
19
|
+
current_reader.checkoffs.create(day: @day, track: @track)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@reader = current_reader.reload
|
|
23
|
+
@reader_tracks = @reader.read_tracks_for(@day)
|
|
24
|
+
@readings = Plan.readings_for(@day)
|
|
25
|
+
|
|
26
|
+
respond_to do |format|
|
|
27
|
+
format.turbo_stream
|
|
28
|
+
format.html { redirect_to day_path(@day) }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|