bible270 0.6.2 → 0.9.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 +100 -0
- data/README.md +235 -217
- data/app/views/layouts/bible270/application.html.erb +4 -3
- data/lib/bible270/configuration.rb +29 -0
- data/lib/bible270/engine.rb +8 -8
- data/lib/bible270/plan.rb +168 -123
- data/lib/bible270/version.rb +1 -1
- data/lib/generators/bible270/install/install_generator.rb +4 -6
- data/lib/generators/bible270/install/templates/bible270.rb.tt +6 -0
- data/lib/generators/bible270/install/templates/omniauth.rb.tt +6 -4
- metadata +36 -9
data/README.md
CHANGED
|
@@ -1,61 +1,44 @@
|
|
|
1
|
-
#
|
|
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.
|
|
1
|
+
# Bible 270
|
|
47
2
|
|
|
48
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/bible270) [](http://rubygems.org/gems/bible270) [](https://github.com/avonderluft/bible270/releases)
|
|
4
|
+
|
|
5
|
+
A mountable **Rails engine** that drops a 270-day (9 month) interactive Bible reading plan into any Rails app, built with [ComfortableMediaSurfer](https://github.com/shakacode/comfortable-media-surfer) CMS in mind. Readers tick off each day's readings, post reflections, and see how everyone else is getting on, much like the social plans on Bible.com.
|
|
6
|
+
|
|
7
|
+
Initially created for use by students and faculty of [Kingdom Movement School of Ministry](https://www.kmsm.life/), to read through all of Scripture together, during the school year. Thus it is potentially useful for any Bible School, Seminary, or Discipleship school with a 9 month school year.
|
|
8
|
+
|
|
9
|
+
## The plan
|
|
10
|
+
|
|
11
|
+
Three readings a day, sized by **verse count** so each day takes roughly the same time.
|
|
49
12
|
|
|
50
|
-
|
|
13
|
+
- **Old Testament** — once through, with Psalms and Proverbs live in their own track. Whole chapters grouped so each day is ~73 verses (range 38–116).
|
|
14
|
+
- **New Testament** — once through, a reading every day. 260 chapters over 270 days, so the 10 longest are halved (Luke 1, Matthew 26–27, Mark 14, Luke 9, 12, 22, John 6,8, Acts 7).
|
|
15
|
+
- **Psalms & Proverbs** — Psalms once, Proverbs twice, interleaved in one track. Proverbs supplies 62 whole-chapter readings, leaving 208 days for the Psalms; longer psalms are divided to fill them. **Psalm 119 is pinned to 11 sections of exactly 16 verses** (176 = 11 × 16, i.e. two of its eight-verse acrostic stanzas per reading), and 41 psalms in total get divided so no single reading tops 20 verses.
|
|
16
|
+
|
|
17
|
+
Genesi → Malachi and Revelation 22 both land on day 270, as does the second Proverbs 31 — the first falls on day 135, the midpoint. Psalm 150 comes in on day 269. Every day carries all three tracks. A typical day is ~119 verses (range 76–175).
|
|
18
|
+
|
|
19
|
+
The schedule is pure deterministic Ruby (`Bible270::Plan`) — none of it is stored. Portions come from per-chapter verse counts in `Bible270::Versification`. The DB only holds readers, check-offs, comments, and sign-in tokens.
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- **Ruby** >= 3.0. Plain Ruby, no C extensions. Runs on 4.0.6 — it avoids everything 4.0 dropped or unbundled (notably `cgi`; URL escaping uses `URI.encode_www_form_component`).
|
|
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
|
+
- A host app, and Turbo if you want check-offs without a page reload.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
51
28
|
|
|
52
29
|
```ruby
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# gem "bible270", git: "https://github.com/avonderluft/bible270.git", branch: "main"
|
|
30
|
+
# Gemfile
|
|
31
|
+
gem "bible270"
|
|
56
32
|
```
|
|
57
33
|
|
|
58
|
-
|
|
34
|
+
<details>
|
|
35
|
+
<summary>Other sources</summary>
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
gem "bible270", git: "https://github.com/avonderluft/bible270.git", branch: "main"
|
|
39
|
+
gem "bible270", path: "<path_to_your_local_copy>/bible270" # local checkout
|
|
40
|
+
```
|
|
41
|
+
</details>
|
|
59
42
|
|
|
60
43
|
```bash
|
|
61
44
|
bundle install
|
|
@@ -63,97 +46,113 @@ bin/rails bible270:install:migrations
|
|
|
63
46
|
bin/rails db:migrate
|
|
64
47
|
```
|
|
65
48
|
|
|
66
|
-
|
|
49
|
+
That copies four migrations into your `db/migrate` — yours from then on, in your `schema.rb`. The engine doesn't touch your migration paths, so the copies are the only definitions in play.
|
|
50
|
+
|
|
51
|
+
Mount it:
|
|
67
52
|
|
|
68
53
|
```ruby
|
|
69
|
-
|
|
54
|
+
# config/routes.rb
|
|
55
|
+
mount Bible270::Engine, at: Bible270.config.mount_at
|
|
70
56
|
```
|
|
71
57
|
|
|
72
|
-
|
|
58
|
+
Optionally generate the initializers:
|
|
73
59
|
|
|
74
|
-
|
|
60
|
+
```bash
|
|
61
|
+
bin/rails generate bible270:install --providers=github
|
|
62
|
+
```
|
|
75
63
|
|
|
76
|
-
|
|
64
|
+
You're live at `/daily-bread`.
|
|
77
65
|
|
|
78
|
-
|
|
66
|
+
### Upgrading
|
|
79
67
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
68
|
+
```bash
|
|
69
|
+
bundle update bible270
|
|
70
|
+
bin/rails bible270:install:migrations # skips ones you already have
|
|
71
|
+
bin/rails db:migrate
|
|
72
|
+
```
|
|
84
73
|
|
|
85
|
-
|
|
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.
|
|
74
|
+
## Bible270 Mount point in your Rails app
|
|
88
75
|
|
|
89
|
-
|
|
76
|
+
The path lives in exactly **one** place, `config.mount_at`:
|
|
90
77
|
|
|
91
78
|
```ruby
|
|
79
|
+
# config/initializers/bible270.rb
|
|
92
80
|
Bible270.configure do |config|
|
|
93
|
-
config.
|
|
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
|
|
81
|
+
config.mount_at = "/daily-bread" # or "/read270", "/manna", whatever you wish
|
|
100
82
|
end
|
|
101
83
|
```
|
|
102
84
|
|
|
103
|
-
|
|
85
|
+
Routes read it directly; OmniAuth reads `Bible270.config.auth_path_prefix` (just
|
|
86
|
+
`"<mount_at>/auth"`). The engine builds its own sign-in links from `request.script_name` at runtime, so there's no third spot to update.
|
|
104
87
|
|
|
105
|
-
|
|
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.
|
|
88
|
+
Values are normalised — `"read270"`, `"/read270"`, `"/read270/"` all become `/read270`. Nested paths and mounting at `/` work too.
|
|
114
89
|
|
|
115
|
-
|
|
116
|
-
(progress, comments, leaderboard) treats every reader identically.
|
|
90
|
+
Two things sit outside your app and still need doing by hand when you move it: the **OAuth callback URLs** in each provider's dashboard, and any **CMS links**.
|
|
117
91
|
|
|
118
|
-
|
|
92
|
+
> **Load order matters.** `bible270.rb` has to be read before `omniauth.rb`, since the latter asks for `auth_path_prefix`. Rails loads initializers alphabetically so the default names work fine. If you rename either, keep that order — or set `mount_at` in `config/application.rb`, which loads before all initializers.
|
|
119
93
|
|
|
120
|
-
|
|
94
|
+
Need OmniAuth somewhere unrelated? Set `config.omniauth_path_prefix` and it wins.
|
|
121
95
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
96
|
+
## Authentication
|
|
97
|
+
|
|
98
|
+
Two ways in, so **nobody's shut out**:
|
|
99
|
+
|
|
100
|
+
1. **Email link** — type an address, get a one-time link, click it. No password, no third-party account. The default, and what makes this usable by people with no GitHub or Google login.
|
|
101
|
+
2. **Social sign-in** via OmniAuth — for people who'd rather.
|
|
125
102
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
103
|
+
Either works alone: `omniauth_providers = []` for email-only, `email_sign_in = false` for
|
|
104
|
+
social-only. Reading is always public; only ticking off and commenting need an account.
|
|
105
|
+
|
|
106
|
+
### Email links
|
|
130
107
|
|
|
131
108
|
```ruby
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
109
|
+
Bible270.configure do |config|
|
|
110
|
+
config.email_sign_in = true
|
|
111
|
+
config.mailer_from = "no-reply@example.com"
|
|
112
|
+
config.email_sign_in_ask_name = true # let readers pick their display name
|
|
113
|
+
# config.email_sign_in_ttl = 20 * 60 # link lifetime, seconds
|
|
114
|
+
# config.email_sign_in_max_per_window = 5 # per address
|
|
115
|
+
# config.email_sign_in_window = 15 * 60
|
|
116
|
+
# config.email_sign_in_deliver_later = true # needs Active Job
|
|
117
|
+
end
|
|
135
118
|
```
|
|
136
119
|
|
|
120
|
+
All it needs is working Action Mailer delivery.
|
|
121
|
+
|
|
122
|
+
- Tokens: 256-bit, URL-safe, single-use, 20-minute expiry.
|
|
123
|
+
- **Only a SHA-256 digest is stored** — no password column anywhere, so the table is useless to
|
|
124
|
+
anyone reading your DB.
|
|
125
|
+
- Claiming is a conditional update, so a double-clicked link still signs you in once.
|
|
126
|
+
- "Check your inbox" reads the same whether the address was known, unknown, or rate-limited — no
|
|
127
|
+
account enumeration.
|
|
128
|
+
- Display name comes from the reader, else the address (`mary.anne.smith@…` → "Mary Anne Smith").
|
|
129
|
+
- `Bible270::SignInToken.sweep!` clears spent tokens. Good cron fodder.
|
|
130
|
+
|
|
131
|
+
Email readers get `provider: "email"` in the same columns OmniAuth uses, so everything downstream
|
|
132
|
+
treats them identically.
|
|
133
|
+
|
|
134
|
+
### Social sign-in
|
|
135
|
+
|
|
136
|
+
The generator writes both initializers for you and adds the `mount` line:
|
|
137
|
+
|
|
137
138
|
```bash
|
|
138
|
-
|
|
139
|
-
bin/rails bible270:install:migrations && bin/rails db:migrate
|
|
139
|
+
bin/rails generate bible270:install --providers=github
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
Add the strategy gem (`omniauth` and `omniauth-rails_csrf_protection` ride along with bible270):
|
|
143
143
|
|
|
144
|
-
```
|
|
145
|
-
|
|
144
|
+
```ruby
|
|
145
|
+
gem "omniauth-github"
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
Register the callback with the provider — `https://example.com/daily-bread/auth/github/callback`.
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
line up with where the engine is mounted, so its callback lands on the engine's route:
|
|
150
|
+
Doing it by hand? The one thing to get right is lining up `path_prefix` with the mount point:
|
|
152
151
|
|
|
153
152
|
```ruby
|
|
154
153
|
# config/initializers/omniauth.rb
|
|
155
154
|
Rails.application.config.middleware.use OmniAuth::Builder do
|
|
156
|
-
path_prefix
|
|
155
|
+
path_prefix Bible270.config.auth_path_prefix
|
|
157
156
|
provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"]
|
|
158
157
|
end
|
|
159
158
|
|
|
@@ -163,31 +162,26 @@ OmniAuth.config.on_failure = proc { |env| Bible270::SessionsController.action(:f
|
|
|
163
162
|
```ruby
|
|
164
163
|
# config/initializers/bible270.rb
|
|
165
164
|
Bible270.configure do |config|
|
|
166
|
-
config.
|
|
165
|
+
config.mount_at = "/daily-bread"
|
|
166
|
+
config.omniauth_providers = [:github] # or [:github, [:google_oauth2, "Google"]]
|
|
167
167
|
config.parent_controller = "::ApplicationController"
|
|
168
168
|
end
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
Worth knowing:
|
|
172
172
|
|
|
173
|
-
- **Sign-in is a POST
|
|
174
|
-
(CVE-2015-9284), so
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
-
|
|
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.
|
|
173
|
+
- **Sign-in is a POST.** OmniAuth 2.0+ refuses GET
|
|
174
|
+
([CVE-2015-9284](https://nvd.nist.gov/vuln/detail/CVE-2015-9284)), so the engine's controls are
|
|
175
|
+
`button_to` forms with CSRF tokens. Build your own and it must POST to `<mount>/auth/:provider`.
|
|
176
|
+
- One provider → a button in the header. Several → a sign-in page at `<mount>/sign_in`.
|
|
177
|
+
- Sign-in carries the current path as `origin`, so clicking a check-off while signed out brings you
|
|
178
|
+
back to that day. Origins must be local paths.
|
|
179
|
+
- `reset_session` on sign-in and sign-out, against session fixation.
|
|
180
|
+
- Stored: provider, uid, display name, email, avatar URL. No tokens, no passwords.
|
|
186
181
|
|
|
187
|
-
###
|
|
182
|
+
### Or use your own users
|
|
188
183
|
|
|
189
|
-
|
|
190
|
-
resolver instead and the engine will never touch sessions:
|
|
184
|
+
Already have auth? Hand the engine a resolver and it'll never touch sessions:
|
|
191
185
|
|
|
192
186
|
```ruby
|
|
193
187
|
config.current_reader_resolver = lambda do |controller|
|
|
@@ -198,176 +192,200 @@ config.current_reader_resolver = lambda do |controller|
|
|
|
198
192
|
end
|
|
199
193
|
```
|
|
200
194
|
|
|
201
|
-
`Reader.for_owner` links
|
|
202
|
-
|
|
203
|
-
public plan on a CMS front-end the OmniAuth path above is usually the right fit.
|
|
195
|
+
`Reader.for_owner` links to any host model polymorphically. Heads up: CMS admin auth is separate
|
|
196
|
+
from public visitors, so for a public plan the email/OmniAuth route above is usually what you want.
|
|
204
197
|
|
|
205
198
|
## Start dates
|
|
206
199
|
|
|
207
|
-
The plan
|
|
208
|
-
calendar). There are two levers:
|
|
200
|
+
The plan runs **undated** (day numbers only) or **dated** (days mapped onto a calendar):
|
|
209
201
|
|
|
210
202
|
```ruby
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
203
|
+
config.start_date = Date.new(2026, 9, 6) # Date, Time, or "YYYY-MM-DD". nil = undated
|
|
204
|
+
config.allow_reader_start_date = true # can readers set their own?
|
|
220
205
|
```
|
|
221
206
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
| `start_date` | `allow_reader_start_date` | Behaviour |
|
|
207
|
+
| `start_date` | `allow_reader_start_date` | What happens |
|
|
225
208
|
|---|---|---|
|
|
226
|
-
| nil | true (default) | Each reader
|
|
227
|
-
| a date | true |
|
|
228
|
-
| a date | false | Everyone
|
|
229
|
-
| nil | false | Fully undated —
|
|
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.
|
|
209
|
+
| nil | true *(default)* | Each reader gets stamped on first check-off, and can change it |
|
|
210
|
+
| a date | true | Community date is the default; anyone may override |
|
|
211
|
+
| a date | false | Everyone pinned to the community date, form hidden |
|
|
212
|
+
| nil | false | Fully undated — no calendar anywhere |
|
|
233
213
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
to day numbers
|
|
214
|
+
Dated plans show each day's date with a **Today** badge, a "Go to today" link, and whether you're
|
|
215
|
+
ahead or behind. Readers set their own from the overview. Changing a start date only re-maps the
|
|
216
|
+
calendar — check-offs and reflections are keyed to day numbers, so nothing moves.
|
|
237
217
|
|
|
238
|
-
|
|
218
|
+
Date helpers are pure functions, usable anywhere:
|
|
239
219
|
|
|
240
220
|
```ruby
|
|
241
|
-
Bible270::Plan.date_for(1, "2026-09-06")
|
|
242
|
-
Bible270::Plan.end_date_for("2026-09-06")
|
|
243
|
-
Bible270::Plan.day_for(Date.current, start)
|
|
244
|
-
Bible270::Plan.day_for(date, start, clamp: false) # => -3 or 271, to
|
|
245
|
-
Bible270::Plan.before_start?(date, start)
|
|
221
|
+
Bible270::Plan.date_for(1, "2026-09-06") # => Sun, 06 Sep 2026
|
|
222
|
+
Bible270::Plan.end_date_for("2026-09-06") # => Wed, 02 Jun 2027 (day 270)
|
|
223
|
+
Bible270::Plan.day_for(Date.current, start) # => 42 (clamped 1..270)
|
|
224
|
+
Bible270::Plan.day_for(date, start, clamp: false) # => -3 or 271, to spot out-of-range
|
|
225
|
+
Bible270::Plan.before_start?(date, start)
|
|
246
226
|
```
|
|
247
227
|
|
|
248
228
|
## Configuration reference
|
|
249
229
|
|
|
250
230
|
```ruby
|
|
251
231
|
Bible270.configure do |config|
|
|
232
|
+
config.mount_at = "/daily-bread" # single source of truth for the path
|
|
252
233
|
config.app_name = "Daily Bread"
|
|
253
234
|
config.tagline = "A 270-day journey through Scripture"
|
|
254
235
|
|
|
255
236
|
config.start_date = nil # community start date, or nil for undated
|
|
256
|
-
config.allow_reader_start_date = true
|
|
237
|
+
config.allow_reader_start_date = true
|
|
257
238
|
|
|
258
|
-
config.parent_controller = "ActionController::Base"
|
|
239
|
+
config.parent_controller = "ActionController::Base" # or "::ApplicationController"
|
|
259
240
|
config.layout = "bible270/application"
|
|
260
241
|
|
|
261
|
-
config.email_sign_in = true
|
|
242
|
+
config.email_sign_in = true
|
|
262
243
|
config.mailer_from = "no-reply@example.com"
|
|
263
244
|
config.email_sign_in_ttl = 20 * 60
|
|
264
245
|
config.email_sign_in_ask_name = true
|
|
265
|
-
config.omniauth_providers = [:github]
|
|
266
|
-
config.omniauth_path_prefix = nil
|
|
267
|
-
config.current_reader_resolver = nil
|
|
246
|
+
config.omniauth_providers = [:github]
|
|
247
|
+
config.omniauth_path_prefix = nil # nil = derive "<mount_at>/auth"
|
|
248
|
+
config.current_reader_resolver = nil # set to bridge your own users
|
|
268
249
|
config.require_sign_in_to_participate = true
|
|
269
250
|
|
|
270
|
-
config.after_sign_in_path = nil
|
|
251
|
+
config.after_sign_in_path = nil # defaults to the plan root
|
|
271
252
|
config.after_sign_out_path = nil
|
|
272
253
|
|
|
273
|
-
config.bible_version = "
|
|
254
|
+
config.bible_version = "NKJV"
|
|
274
255
|
config.passage_url_builder = ->(reference, version) {
|
|
275
256
|
"https://www.biblegateway.com/passage/?search=#{URI.encode_www_form_component(reference)}&version=#{version}"
|
|
276
257
|
}
|
|
277
258
|
end
|
|
278
259
|
```
|
|
279
260
|
|
|
280
|
-
|
|
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.
|
|
261
|
+
References link to Bible Gateway by default — point `passage_url_builder` at your another reader if you wish, or your own reader if you host one.
|
|
297
262
|
|
|
298
|
-
|
|
299
|
-
Option B (OmniAuth) is usually the right fit for public visitors on gknt.org.
|
|
263
|
+
## With ComfortableMediaSurfer
|
|
300
264
|
|
|
301
|
-
|
|
265
|
+
The CMS serves your content; this is a separate mounted app. Either **just link to it** (mount, add a nav link, done — the engine renders its own themed pages), or **match your chrome** by setting `config.parent_controller = "::ApplicationController"` and `config.layout = "layouts/application"`
|
|
266
|
+
so it sits inside your header and footer. All engine CSS is prefixed `b270-`, so nothing collides.
|
|
302
267
|
|
|
303
268
|
## Data model
|
|
304
269
|
|
|
305
|
-
| Table |
|
|
306
|
-
|
|
270
|
+
| Table | Holds |
|
|
271
|
+
|-------|-------|
|
|
307
272
|
| `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`)
|
|
309
|
-
| `
|
|
310
|
-
| `
|
|
273
|
+
| `bible270_checkoffs` | one row per reader per day per track (`ot`/`nt`/`pp`), unique-indexed |
|
|
274
|
+
| `bible270_comments` | a reflection on a day, optionally scoped to a track, public to all |
|
|
275
|
+
| `bible270_sign_in_tokens` | magic-link tokens: email, **digest only**, expiry, consumed-at |
|
|
311
276
|
|
|
312
|
-
A day
|
|
313
|
-
|
|
314
|
-
**public** to other readers by design (mirroring Bible.com's shared plans).
|
|
277
|
+
A day is "complete" once all three tracks are ticked. Progress and comments are **public** by
|
|
278
|
+
design, same as Bible.com's shared plans.
|
|
315
279
|
|
|
316
|
-
## Routes (
|
|
280
|
+
## Routes (inside the mount point)
|
|
317
281
|
|
|
318
282
|
```
|
|
319
283
|
GET / days#index overview + community + calendar
|
|
320
|
-
GET /day/:day days#show
|
|
284
|
+
GET /day/:day days#show readings, who's finished, reflections
|
|
321
285
|
POST /day/:day/toggle/:track checkoffs#toggle
|
|
322
286
|
POST /day/:day/comments comments#create
|
|
323
287
|
DELETE /comments/:id comments#destroy
|
|
324
288
|
PATCH /start-date readers#update_start_date
|
|
325
289
|
DELETE /start-date readers#clear_start_date
|
|
326
290
|
GET /community readers#index leaderboard
|
|
327
|
-
GET /readers/:id readers#show
|
|
328
|
-
GET /sign_in sessions#new
|
|
329
|
-
POST /sign_in/email sessions#email_link
|
|
330
|
-
GET /sign_in/email/:token sessions#email_callback
|
|
331
|
-
GET /auth/:provider/callback sessions#create
|
|
291
|
+
GET /readers/:id readers#show
|
|
292
|
+
GET /sign_in sessions#new email form + provider list
|
|
293
|
+
POST /sign_in/email sessions#email_link
|
|
294
|
+
GET /sign_in/email/:token sessions#email_callback
|
|
295
|
+
GET /auth/:provider/callback sessions#create
|
|
332
296
|
POST /auth/:provider/callback sessions#create
|
|
333
297
|
GET /auth/failure sessions#failure
|
|
334
298
|
DELETE /sign_out sessions#destroy
|
|
335
299
|
|
|
336
|
-
POST <mount>/auth/:provider
|
|
300
|
+
POST <mount>/auth/:provider OmniAuth middleware, not the engine
|
|
337
301
|
```
|
|
338
302
|
|
|
339
|
-
## Tuning the plan
|
|
303
|
+
## Tuning the plan
|
|
340
304
|
|
|
341
|
-
The whole schedule
|
|
342
|
-
can be changed without touching anything else:
|
|
305
|
+
The whole schedule falls out of a few constants in `Bible270::Plan`:
|
|
343
306
|
|
|
344
307
|
| Constant | Default | Effect |
|
|
345
308
|
|----------|---------|--------|
|
|
346
309
|
| `DAYS` | `270` | plan length |
|
|
347
|
-
| `
|
|
348
|
-
| `
|
|
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 |
|
|
310
|
+
| `PROVERBS_PASSES` | `2` | Proverbs laps. Each pass is 31 readings, so this decides how many days are left for the Psalms |
|
|
311
|
+
| `PSALM_119_SECTION_SIZE` | `16` | verses per Psalm 119 section. 176 divides evenly by 8, 11, 16, 22 and 44 |
|
|
351
312
|
|
|
352
|
-
|
|
353
|
-
|
|
313
|
+
Change `PROVERBS_PASSES` or `DAYS` and both the New Testament and the Psalms re-divide automatically
|
|
314
|
+
to fill whatever's left. Divisions always go to whichever chapter currently carries the heaviest
|
|
315
|
+
reading, so the longest get split first and nothing is divided that doesn't need to be.
|
|
354
316
|
|
|
355
|
-
|
|
317
|
+
All computed at load and memoized — no generated schedule, so nothing to migrate if you change them.
|
|
356
318
|
|
|
357
|
-
|
|
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.
|
|
319
|
+
## Scaling
|
|
361
320
|
|
|
362
|
-
|
|
321
|
+
The leaderboard counts completed days in Ruby from grouped check-offs — fine for hundreds of
|
|
322
|
+
readers. Past a few thousand, cache a `days_completed` counter on `Reader` via a `Checkoff`
|
|
323
|
+
`after_commit` and sort in SQL.
|
|
363
324
|
|
|
364
|
-
|
|
325
|
+
## Troubleshooting
|
|
326
|
+
|
|
327
|
+
**`DuplicateMigrationNameError: Multiple migrations have the name CreateBible270Readers`**
|
|
328
|
+
|
|
329
|
+
0.6.2 and earlier wrongly added the engine's `db/migrate` to your app's paths *and* shipped
|
|
330
|
+
`install:migrations`, so copying defined everything twice. `bundle update bible270` to >= 0.6.3,
|
|
331
|
+
keep your copies, migrate.
|
|
332
|
+
|
|
333
|
+
**404 on the provider callback**
|
|
334
|
+
|
|
335
|
+
`path_prefix` doesn't match the mount point. Use `Bible270.config.auth_path_prefix` rather than a
|
|
336
|
+
literal, and check the URL registered with the provider matches. Setting `mount_at` in an initializer
|
|
337
|
+
Rails loads *after* `omniauth.rb` gives you the default prefix — see [Mount point](#mount-point).
|
|
338
|
+
|
|
339
|
+
**Sign-in button does nothing, or `OmniAuth::AuthenticityError`**
|
|
340
|
+
|
|
341
|
+
Has to be a POST with a CSRF token. The engine's controls already are — if you built your own, make
|
|
342
|
+
it a `button_to`. Also check `omniauth-rails_csrf_protection` is in the bundle.
|
|
343
|
+
|
|
344
|
+
**No email arrives**
|
|
345
|
+
|
|
346
|
+
Needs working Action Mailer delivery — the engine just calls `deliver_now`. Check `mailer_from` is
|
|
347
|
+
an address your relay accepts, and watch the logs. Failures never surface to the reader, on purpose,
|
|
348
|
+
since that message can't reveal which addresses exist.
|
|
349
|
+
|
|
350
|
+
**"That link has expired or was already used"**
|
|
351
|
+
|
|
352
|
+
Links are single-use, 20-minute life. Some mail scanners and link-preview bots fetch URLs before the
|
|
353
|
+
recipient clicks, burning the token. If it keeps happening, lengthen `email_sign_in_ttl` or find
|
|
354
|
+
what's prefetching.
|
|
355
|
+
|
|
356
|
+
**Everything's unstyled in my layout**
|
|
357
|
+
|
|
358
|
+
The engine's CSS rides in a partial its own layout renders. Using your layout? Add
|
|
359
|
+
`<%= render "bible270/shared/styles" %>` to the `<head>`, or style the `b270-*` classes yourself.
|
|
360
|
+
|
|
361
|
+
## Development
|
|
365
362
|
|
|
366
363
|
```bash
|
|
367
364
|
bundle install
|
|
368
365
|
rake test
|
|
369
366
|
```
|
|
370
367
|
|
|
368
|
+
Plan logic is fully unit-tested with no Rails dependency.
|
|
369
|
+
|
|
370
|
+
## Contributing
|
|
371
|
+
|
|
372
|
+
Bug reports and pull requests are welcome at <https://github.com/avonderluft/bible270>.
|
|
373
|
+
|
|
374
|
+
1. Fork it and clone your fork.
|
|
375
|
+
2. Branch off `main`: `git checkout -b my-feature`.
|
|
376
|
+
3. Make your change, and add tests — `rake test` should stay green.
|
|
377
|
+
4. Commit with a clear message: `git commit -am "Add my feature"`.
|
|
378
|
+
5. Push: `git push origin my-feature`.
|
|
379
|
+
6. Open a pull request against `main`, saying what changed and why.
|
|
380
|
+
|
|
381
|
+
A few things that'll make review quick:
|
|
382
|
+
|
|
383
|
+
- Keep plan logic free of Rails so it stays unit-testable.
|
|
384
|
+
- Anything touching the reading schedule needs a test pinning the expected references — the plan is
|
|
385
|
+
deterministic, so assert exact values.
|
|
386
|
+
- Prefix new CSS classes and helpers with `b270`.
|
|
387
|
+
- Note anything user-visible in `CHANGELOG.md`.
|
|
388
|
+
|
|
371
389
|
## License
|
|
372
390
|
|
|
373
391
|
MIT. See `MIT-LICENSE`.
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
<% if flash[:notice] %><div class="b270-flash notice"><%= flash[:notice] %></div><% end %>
|
|
20
20
|
<%= yield %>
|
|
21
21
|
<footer class="b270-footer">
|
|
22
|
-
<p>The Old Testament
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
<p>The Old Testament and New Testament are each read once across the 270 days, with a
|
|
23
|
+
reading from all three tracks every day. Alongside them the Psalms run once and
|
|
24
|
+
Proverbs twice; the longest chapters are divided so the readings fill each day
|
|
25
|
+
evenly, and Psalm 119 comes in eleven sections.</p>
|
|
25
26
|
</footer>
|
|
26
27
|
</div>
|
|
27
28
|
</body>
|