bible270 0.6.3 → 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 +86 -0
- data/README.md +202 -265
- data/app/views/layouts/bible270/application.html.erb +4 -3
- data/lib/bible270/configuration.rb +29 -0
- 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 +38 -14
data/README.md
CHANGED
|
@@ -1,192 +1,158 @@
|
|
|
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. Installs and loads under
|
|
35
|
-
Ruby 4.0.6; the test suite is exercised on 3.2 and passes with
|
|
36
|
-
`--enable-frozen-string-literal` forced.
|
|
37
|
-
- **Ruby 4.0 notes.** Audited against the 4.0 breaking changes: the gem does not use `cgi`
|
|
38
|
-
(removed from default gems in 4.0 — URL escaping goes through `URI.encode_www_form_component`
|
|
39
|
-
instead, which is byte-identical to `CGI.escape`), `Set`/`SortedSet`, `Ractor`, `Net::HTTP`,
|
|
40
|
-
`Process::Status`, `ObjectSpace`, or any of the gems promoted from default to bundled
|
|
41
|
-
(`ostruct`, `logger`, `benchmark`, `pstore`, `irb`, `rdoc`). Every file carries a
|
|
42
|
-
`# frozen_string_literal: true` magic comment and no string literal is mutated.
|
|
43
|
-
- **Rails.** The gemspec allows `rails >= 7.0`, but on Ruby 4.0 the binding constraint is your
|
|
44
|
-
host app, not this engine: Rails 8.0/8.1 require Ruby >= 3.2, and ComfortableMediaSurfer is a
|
|
45
|
-
"Rails 7.0+" engine. Verify your own lockfile (e.g. with RailsBump) before pairing an older
|
|
46
|
-
Rails with Ruby 4.0.
|
|
47
|
-
- A host application (this is an engine, not a standalone app).
|
|
48
|
-
- Turbo is used for seamless check-offs and degrades to full-page redirects if absent.
|
|
1
|
+
# Bible 270
|
|
49
2
|
|
|
50
|
-
|
|
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.
|
|
12
|
+
|
|
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.
|
|
51
20
|
|
|
52
|
-
|
|
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
|
|
53
28
|
|
|
54
29
|
```ruby
|
|
30
|
+
# Gemfile
|
|
55
31
|
gem "bible270"
|
|
56
32
|
```
|
|
57
33
|
|
|
58
34
|
<details>
|
|
59
|
-
<summary>Other sources
|
|
35
|
+
<summary>Other sources</summary>
|
|
60
36
|
|
|
61
37
|
```ruby
|
|
62
|
-
# straight from the repository
|
|
63
38
|
gem "bible270", git: "https://github.com/avonderluft/bible270.git", branch: "main"
|
|
64
|
-
|
|
65
|
-
# a local checkout you're editing
|
|
66
|
-
gem "bible270", path: "../bible270"
|
|
39
|
+
gem "bible270", path: "<path_to_your_local_copy>/bible270" # local checkout
|
|
67
40
|
```
|
|
68
41
|
</details>
|
|
69
42
|
|
|
70
|
-
Then install, copy the migrations, and migrate:
|
|
71
|
-
|
|
72
43
|
```bash
|
|
73
44
|
bundle install
|
|
74
45
|
bin/rails bible270:install:migrations
|
|
75
46
|
bin/rails db:migrate
|
|
76
47
|
```
|
|
77
48
|
|
|
78
|
-
|
|
79
|
-
sign-in tokens). They belong to your application from then on: they get your timestamps, appear in
|
|
80
|
-
your `schema.rb`, and are yours to run, roll back, or edit. The engine does **not** add its own
|
|
81
|
-
migration directory to your app's paths, so the copies are the only definitions in play.
|
|
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.
|
|
82
50
|
|
|
83
|
-
Mount it
|
|
51
|
+
Mount it:
|
|
84
52
|
|
|
85
53
|
```ruby
|
|
86
|
-
|
|
54
|
+
# config/routes.rb
|
|
55
|
+
mount Bible270::Engine, at: Bible270.config.mount_at
|
|
87
56
|
```
|
|
88
57
|
|
|
89
|
-
Optionally generate the initializers
|
|
58
|
+
Optionally generate the initializers:
|
|
90
59
|
|
|
91
60
|
```bash
|
|
92
|
-
bin/rails generate bible270:install --
|
|
61
|
+
bin/rails generate bible270:install --providers=github
|
|
93
62
|
```
|
|
94
63
|
|
|
95
|
-
|
|
64
|
+
You're live at `/daily-bread`.
|
|
96
65
|
|
|
97
66
|
### Upgrading
|
|
98
67
|
|
|
99
|
-
When a new version adds a migration, re-run the copy step — already-copied migrations are skipped:
|
|
100
|
-
|
|
101
68
|
```bash
|
|
102
69
|
bundle update bible270
|
|
103
|
-
bin/rails bible270:install:migrations
|
|
70
|
+
bin/rails bible270:install:migrations # skips ones you already have
|
|
104
71
|
bin/rails db:migrate
|
|
105
72
|
```
|
|
106
73
|
|
|
107
|
-
|
|
74
|
+
## Bible270 Mount point in your Rails app
|
|
75
|
+
|
|
76
|
+
The path lives in exactly **one** place, `config.mount_at`:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# config/initializers/bible270.rb
|
|
80
|
+
Bible270.configure do |config|
|
|
81
|
+
config.mount_at = "/daily-bread" # or "/read270", "/manna", whatever you wish
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
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.
|
|
87
|
+
|
|
88
|
+
Values are normalised — `"read270"`, `"/read270"`, `"/read270/"` all become `/read270`. Nested paths and mounting at `/` work too.
|
|
89
|
+
|
|
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**.
|
|
91
|
+
|
|
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.
|
|
93
|
+
|
|
94
|
+
Need OmniAuth somewhere unrelated? Set `config.omniauth_path_prefix` and it wins.
|
|
108
95
|
|
|
109
96
|
## Authentication
|
|
110
97
|
|
|
111
|
-
Two
|
|
98
|
+
Two ways in, so **nobody's shut out**:
|
|
112
99
|
|
|
113
|
-
1. **Email link
|
|
114
|
-
|
|
115
|
-
the plan usable by people who don't have (or don't want to use) GitHub, Google, and friends.
|
|
116
|
-
2. **OmniAuth social sign-in** — optional convenience for those who do.
|
|
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.
|
|
117
102
|
|
|
118
|
-
Either
|
|
119
|
-
|
|
120
|
-
reflections stays public either way — only checking off and commenting require signing in.
|
|
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.
|
|
121
105
|
|
|
122
|
-
### Email
|
|
106
|
+
### Email links
|
|
123
107
|
|
|
124
108
|
```ruby
|
|
125
109
|
Bible270.configure do |config|
|
|
126
110
|
config.email_sign_in = true
|
|
127
|
-
config.mailer_from = "no-reply@
|
|
128
|
-
config.email_sign_in_ask_name = true #
|
|
129
|
-
# config.email_sign_in_ttl = 20 * 60 # link lifetime
|
|
130
|
-
# config.email_sign_in_max_per_window = 5 # per address
|
|
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
|
|
131
115
|
# config.email_sign_in_window = 15 * 60
|
|
132
|
-
# config.email_sign_in_deliver_later = true # needs
|
|
116
|
+
# config.email_sign_in_deliver_later = true # needs Active Job
|
|
133
117
|
end
|
|
134
118
|
```
|
|
135
119
|
|
|
136
|
-
|
|
120
|
+
All it needs is working Action Mailer delivery.
|
|
137
121
|
|
|
138
|
-
- Tokens
|
|
139
|
-
- **Only a SHA-256 digest
|
|
140
|
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
|
|
144
|
-
-
|
|
145
|
-
|
|
146
|
-
- Spent and stale tokens can be cleaned up with `Bible270::SignInToken.sweep!` from a cron/rake task.
|
|
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.
|
|
147
130
|
|
|
148
|
-
|
|
149
|
-
|
|
131
|
+
Email readers get `provider: "email"` in the same columns OmniAuth uses, so everything downstream
|
|
132
|
+
treats them identically.
|
|
150
133
|
|
|
151
|
-
###
|
|
134
|
+
### Social sign-in
|
|
152
135
|
|
|
153
|
-
|
|
136
|
+
The generator writes both initializers for you and adds the `mount` line:
|
|
154
137
|
|
|
155
138
|
```bash
|
|
156
|
-
bin/rails generate bible270:install --
|
|
139
|
+
bin/rails generate bible270:install --providers=github
|
|
157
140
|
```
|
|
158
141
|
|
|
159
|
-
|
|
160
|
-
`config/initializers/omniauth.rb` (with `path_prefix` already matching your mount point), and adds
|
|
161
|
-
the `mount` line to your routes. Pass `--providers=` with an empty value for an email-only site.
|
|
162
|
-
Then add the gems you need and migrate:
|
|
142
|
+
Add the strategy gem (`omniauth` and `omniauth-rails_csrf_protection` ride along with bible270):
|
|
163
143
|
|
|
164
144
|
```ruby
|
|
165
|
-
# Gemfile — omniauth and omniauth-rails_csrf_protection come in with bible270;
|
|
166
|
-
# the provider strategy is your choice:
|
|
167
145
|
gem "omniauth-github"
|
|
168
146
|
```
|
|
169
147
|
|
|
170
|
-
|
|
171
|
-
bundle install
|
|
172
|
-
bin/rails bible270:install:migrations && bin/rails db:migrate
|
|
173
|
-
```
|
|
148
|
+
Register the callback with the provider — `https://example.com/daily-bread/auth/github/callback`.
|
|
174
149
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
```
|
|
178
|
-
https://gknt.org/reading-plan/auth/github/callback
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### Manual setup
|
|
182
|
-
|
|
183
|
-
If you'd rather not use the generator, the only subtlety is that OmniAuth's `path_prefix` must
|
|
184
|
-
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:
|
|
185
151
|
|
|
186
152
|
```ruby
|
|
187
153
|
# config/initializers/omniauth.rb
|
|
188
154
|
Rails.application.config.middleware.use OmniAuth::Builder do
|
|
189
|
-
path_prefix
|
|
155
|
+
path_prefix Bible270.config.auth_path_prefix
|
|
190
156
|
provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"]
|
|
191
157
|
end
|
|
192
158
|
|
|
@@ -196,31 +162,26 @@ OmniAuth.config.on_failure = proc { |env| Bible270::SessionsController.action(:f
|
|
|
196
162
|
```ruby
|
|
197
163
|
# config/initializers/bible270.rb
|
|
198
164
|
Bible270.configure do |config|
|
|
199
|
-
config.
|
|
165
|
+
config.mount_at = "/daily-bread"
|
|
166
|
+
config.omniauth_providers = [:github] # or [:github, [:google_oauth2, "Google"]]
|
|
200
167
|
config.parent_controller = "::ApplicationController"
|
|
201
168
|
end
|
|
202
169
|
```
|
|
203
170
|
|
|
204
|
-
|
|
171
|
+
Worth knowing:
|
|
205
172
|
|
|
206
|
-
- **Sign-in is a POST
|
|
207
|
-
(CVE-2015-9284), so
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
-
|
|
214
|
-
a check-off while signed out lands back on that same day afterwards. Origins are validated to be
|
|
215
|
-
local paths only.
|
|
216
|
-
- **Session hygiene.** `reset_session` runs on both sign-in and sign-out to avoid session fixation.
|
|
217
|
-
Sign-out is `DELETE <mount>/sign_out`.
|
|
218
|
-
- **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.
|
|
219
181
|
|
|
220
|
-
###
|
|
182
|
+
### Or use your own users
|
|
221
183
|
|
|
222
|
-
|
|
223
|
-
resolver instead and the engine will never touch sessions:
|
|
184
|
+
Already have auth? Hand the engine a resolver and it'll never touch sessions:
|
|
224
185
|
|
|
225
186
|
```ruby
|
|
226
187
|
config.current_reader_resolver = lambda do |controller|
|
|
@@ -231,224 +192,200 @@ config.current_reader_resolver = lambda do |controller|
|
|
|
231
192
|
end
|
|
232
193
|
```
|
|
233
194
|
|
|
234
|
-
`Reader.for_owner` links
|
|
235
|
-
|
|
236
|
-
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.
|
|
237
197
|
|
|
238
198
|
## Start dates
|
|
239
199
|
|
|
240
|
-
The plan
|
|
241
|
-
calendar). There are two levers:
|
|
200
|
+
The plan runs **undated** (day numbers only) or **dated** (days mapped onto a calendar):
|
|
242
201
|
|
|
243
202
|
```ruby
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
# Accepts a Date, Time, or "YYYY-MM-DD" string. Default: nil (undated).
|
|
247
|
-
config.start_date = Date.new(2026, 9, 6)
|
|
248
|
-
|
|
249
|
-
# May an individual reader set/change their own start date? Default: true.
|
|
250
|
-
# Set false to pin everyone to config.start_date.
|
|
251
|
-
config.allow_reader_start_date = true
|
|
252
|
-
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?
|
|
253
205
|
```
|
|
254
206
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
| `start_date` | `allow_reader_start_date` | Behaviour |
|
|
207
|
+
| `start_date` | `allow_reader_start_date` | What happens |
|
|
258
208
|
|---|---|---|
|
|
259
|
-
| nil | true (default) | Each reader
|
|
260
|
-
| a date | true |
|
|
261
|
-
| a date | false | Everyone
|
|
262
|
-
| nil | false | Fully undated —
|
|
263
|
-
|
|
264
|
-
When a plan is dated, each day page shows its calendar date with a **Today** badge, the overview
|
|
265
|
-
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 |
|
|
266
213
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
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.
|
|
270
217
|
|
|
271
|
-
|
|
218
|
+
Date helpers are pure functions, usable anywhere:
|
|
272
219
|
|
|
273
220
|
```ruby
|
|
274
|
-
Bible270::Plan.date_for(1, "2026-09-06")
|
|
275
|
-
Bible270::Plan.end_date_for("2026-09-06")
|
|
276
|
-
Bible270::Plan.day_for(Date.current, start)
|
|
277
|
-
Bible270::Plan.day_for(date, start, clamp: false) # => -3 or 271, to
|
|
278
|
-
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)
|
|
279
226
|
```
|
|
280
227
|
|
|
281
228
|
## Configuration reference
|
|
282
229
|
|
|
283
230
|
```ruby
|
|
284
231
|
Bible270.configure do |config|
|
|
232
|
+
config.mount_at = "/daily-bread" # single source of truth for the path
|
|
285
233
|
config.app_name = "Daily Bread"
|
|
286
234
|
config.tagline = "A 270-day journey through Scripture"
|
|
287
235
|
|
|
288
236
|
config.start_date = nil # community start date, or nil for undated
|
|
289
|
-
config.allow_reader_start_date = true
|
|
237
|
+
config.allow_reader_start_date = true
|
|
290
238
|
|
|
291
|
-
config.parent_controller = "ActionController::Base"
|
|
239
|
+
config.parent_controller = "ActionController::Base" # or "::ApplicationController"
|
|
292
240
|
config.layout = "bible270/application"
|
|
293
241
|
|
|
294
|
-
config.email_sign_in = true
|
|
242
|
+
config.email_sign_in = true
|
|
295
243
|
config.mailer_from = "no-reply@example.com"
|
|
296
244
|
config.email_sign_in_ttl = 20 * 60
|
|
297
245
|
config.email_sign_in_ask_name = true
|
|
298
|
-
config.omniauth_providers = [:github]
|
|
299
|
-
config.omniauth_path_prefix = nil
|
|
300
|
-
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
|
|
301
249
|
config.require_sign_in_to_participate = true
|
|
302
250
|
|
|
303
|
-
config.after_sign_in_path = nil
|
|
251
|
+
config.after_sign_in_path = nil # defaults to the plan root
|
|
304
252
|
config.after_sign_out_path = nil
|
|
305
253
|
|
|
306
|
-
config.bible_version = "
|
|
254
|
+
config.bible_version = "NKJV"
|
|
307
255
|
config.passage_url_builder = ->(reference, version) {
|
|
308
256
|
"https://www.biblegateway.com/passage/?search=#{URI.encode_www_form_component(reference)}&version=#{version}"
|
|
309
257
|
}
|
|
310
258
|
end
|
|
311
259
|
```
|
|
312
260
|
|
|
313
|
-
|
|
314
|
-
default). Point `passage_url_builder` at your own reader if you host one.
|
|
315
|
-
|
|
316
|
-
---
|
|
317
|
-
|
|
318
|
-
## Using it inside ComfortableMediaSurfer
|
|
319
|
-
|
|
320
|
-
ComfortableMediaSurfer serves marketing/content pages; this engine is a separate
|
|
321
|
-
mounted app. Two integration 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.
|
|
322
262
|
|
|
323
|
-
|
|
324
|
-
engine renders its own themed pages. Simplest and fully featured.
|
|
263
|
+
## With ComfortableMediaSurfer
|
|
325
264
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
sit inside your normal header/footer. The engine's scoped CSS (all classes are
|
|
329
|
-
prefixed `b270-`) won't collide with CMS styles.
|
|
330
|
-
|
|
331
|
-
Because CMS admin auth (`ComfortableMediaSurfer`) is separate from public readers,
|
|
332
|
-
Option B (OmniAuth) is usually the right fit for public visitors on gknt.org.
|
|
333
|
-
|
|
334
|
-
---
|
|
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.
|
|
335
267
|
|
|
336
268
|
## Data model
|
|
337
269
|
|
|
338
|
-
| Table |
|
|
339
|
-
|
|
270
|
+
| Table | Holds |
|
|
271
|
+
|-------|-------|
|
|
340
272
|
| `bible270_readers` | identity: display name, avatar, provider/uid or polymorphic `owner`, `started_on` |
|
|
341
|
-
| `bible270_checkoffs` | one row per reader per day per track (`ot`/`nt`/`pp`)
|
|
342
|
-
| `
|
|
343
|
-
| `
|
|
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 |
|
|
344
276
|
|
|
345
|
-
A day
|
|
346
|
-
|
|
347
|
-
**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.
|
|
348
279
|
|
|
349
|
-
## Routes (
|
|
280
|
+
## Routes (inside the mount point)
|
|
350
281
|
|
|
351
282
|
```
|
|
352
283
|
GET / days#index overview + community + calendar
|
|
353
|
-
GET /day/:day days#show
|
|
284
|
+
GET /day/:day days#show readings, who's finished, reflections
|
|
354
285
|
POST /day/:day/toggle/:track checkoffs#toggle
|
|
355
286
|
POST /day/:day/comments comments#create
|
|
356
287
|
DELETE /comments/:id comments#destroy
|
|
357
288
|
PATCH /start-date readers#update_start_date
|
|
358
289
|
DELETE /start-date readers#clear_start_date
|
|
359
290
|
GET /community readers#index leaderboard
|
|
360
|
-
GET /readers/:id readers#show
|
|
361
|
-
GET /sign_in sessions#new
|
|
362
|
-
POST /sign_in/email sessions#email_link
|
|
363
|
-
GET /sign_in/email/:token sessions#email_callback
|
|
364
|
-
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
|
|
365
296
|
POST /auth/:provider/callback sessions#create
|
|
366
297
|
GET /auth/failure sessions#failure
|
|
367
298
|
DELETE /sign_out sessions#destroy
|
|
368
299
|
|
|
369
|
-
POST <mount>/auth/:provider
|
|
300
|
+
POST <mount>/auth/:provider OmniAuth middleware, not the engine
|
|
370
301
|
```
|
|
371
302
|
|
|
372
|
-
## Tuning the plan
|
|
303
|
+
## Tuning the plan
|
|
373
304
|
|
|
374
|
-
The whole schedule
|
|
375
|
-
can be changed without touching anything else:
|
|
305
|
+
The whole schedule falls out of a few constants in `Bible270::Plan`:
|
|
376
306
|
|
|
377
307
|
| Constant | Default | Effect |
|
|
378
308
|
|----------|---------|--------|
|
|
379
309
|
| `DAYS` | `270` | plan length |
|
|
380
|
-
| `
|
|
381
|
-
| `
|
|
382
|
-
| `PP_SPLIT_PARTS` | `2` | how many readings such a chapter becomes |
|
|
383
|
-
| `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 |
|
|
384
312
|
|
|
385
|
-
|
|
386
|
-
|
|
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.
|
|
387
316
|
|
|
388
|
-
|
|
317
|
+
All computed at load and memoized — no generated schedule, so nothing to migrate if you change them.
|
|
389
318
|
|
|
390
|
-
|
|
391
|
-
counts. That's ideal for a homelab/parish-sized community (hundreds of readers). If
|
|
392
|
-
you grow to many thousands, add a cached `days_completed` counter on `Reader`
|
|
393
|
-
(updated in a `Checkoff` after_commit) and sort in SQL.
|
|
319
|
+
## Scaling
|
|
394
320
|
|
|
395
|
-
|
|
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.
|
|
396
324
|
|
|
397
|
-
|
|
325
|
+
## Troubleshooting
|
|
398
326
|
|
|
399
|
-
|
|
400
|
-
migration paths *as well as* providing `install:migrations` — so copying them defined every
|
|
401
|
-
migration class twice. Upgrade to >= 0.6.3 and keep your copied migrations:
|
|
327
|
+
**`DuplicateMigrationNameError: Multiple migrations have the name CreateBible270Readers`**
|
|
402
328
|
|
|
403
|
-
|
|
404
|
-
bundle update bible270
|
|
405
|
-
|
|
406
|
-
```
|
|
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.
|
|
407
332
|
|
|
408
|
-
**
|
|
333
|
+
**404 on the provider callback**
|
|
409
334
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
the
|
|
413
|
-
writes both consistently.
|
|
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).
|
|
414
338
|
|
|
415
|
-
**
|
|
339
|
+
**Sign-in button does nothing, or `OmniAuth::AuthenticityError`**
|
|
416
340
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
already POST forms; if you've built your own link, convert it to `button_to`. Also confirm
|
|
420
|
-
`omniauth-rails_csrf_protection` is in the bundle.
|
|
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.
|
|
421
343
|
|
|
422
|
-
**No
|
|
344
|
+
**No email arrives**
|
|
423
345
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
reader, deliberately, since the "check your inbox" message is identical either way to avoid
|
|
428
|
-
disclosing which addresses exist.
|
|
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.
|
|
429
349
|
|
|
430
350
|
**"That link has expired or was already used"**
|
|
431
351
|
|
|
432
|
-
Links are single-use
|
|
433
|
-
|
|
434
|
-
|
|
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.
|
|
435
355
|
|
|
436
|
-
**Everything
|
|
356
|
+
**Everything's unstyled in my layout**
|
|
437
357
|
|
|
438
|
-
The engine
|
|
439
|
-
|
|
440
|
-
`<%= render "bible270/shared/styles" %>` to that layout's `<head>`, or style the `b270-*` classes
|
|
441
|
-
yourself.
|
|
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.
|
|
442
360
|
|
|
443
|
-
## Development
|
|
444
|
-
|
|
445
|
-
The deterministic plan logic is fully unit-tested with no Rails dependency:
|
|
361
|
+
## Development
|
|
446
362
|
|
|
447
363
|
```bash
|
|
448
364
|
bundle install
|
|
449
365
|
rake test
|
|
450
366
|
```
|
|
451
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
|
+
|
|
452
389
|
## License
|
|
453
390
|
|
|
454
391
|
MIT. See `MIT-LICENSE`.
|