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.
data/README.md CHANGED
@@ -1,192 +1,158 @@
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. 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
- ## Install
3
+ [![Gem Version](https://badge.fury.io/rb/bible270.svg)](https://badge.fury.io/rb/bible270) [![Gem Downloads](https://img.shields.io/gem/dt/bible270.svg?style=flat)](http://rubygems.org/gems/bible270) [![GitHub Release Date - Published_At](https://img.shields.io/github/release-date/avonderluft/bible270?label=last%20release&color=seagreen)](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
- Add to the host app's `Gemfile`:
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 (unreleased changes, local development)</summary>
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
- `install:migrations` copies four migrations into your `db/migrate` (readers, check-offs, comments,
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 in `config/routes.rb`:
51
+ Mount it:
84
52
 
85
53
  ```ruby
86
- mount Bible270::Engine, at: "/reading-plan"
54
+ # config/routes.rb
55
+ mount Bible270::Engine, at: Bible270.config.mount_at
87
56
  ```
88
57
 
89
- Optionally generate the initializers (see [Authentication](#authentication)):
58
+ Optionally generate the initializers:
90
59
 
91
60
  ```bash
92
- bin/rails generate bible270:install --mount-at=/reading-plan --providers=github
61
+ bin/rails generate bible270:install --providers=github
93
62
  ```
94
63
 
95
- The plan is now live at `/reading-plan`.
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 built-in ways to sign in, so **nobody is excluded**:
98
+ Two ways in, so **nobody's shut out**:
112
99
 
113
- 1. **Email link (passwordless)** — the reader types their address, gets a one-time link, and
114
- clicks it. No password, no account with anyone else. This is the default and it's what makes
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 can run alone. `config.omniauth_providers = []` gives you an email-only site;
119
- `config.email_sign_in = false` gives you social-only. Viewing the plan and reading others'
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 sign-in
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@gknt.org"
128
- config.email_sign_in_ask_name = true # reader picks their display name
129
- # config.email_sign_in_ttl = 20 * 60 # link lifetime (seconds)
130
- # config.email_sign_in_max_per_window = 5 # per address, per window
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 an Active Job backend
116
+ # config.email_sign_in_deliver_later = true # needs Active Job
133
117
  end
134
118
  ```
135
119
 
136
- The host app needs working Action Mailer delivery — nothing else. How it behaves:
120
+ All it needs is working Action Mailer delivery.
137
121
 
138
- - Tokens are 256-bit, URL-safe, single-use, and expire (20 minutes by default).
139
- - **Only a SHA-256 digest of the token is stored**, so the table is useless to anyone who reads
140
- the database. There is no password column anywhere in this gem.
141
- - Consuming a link is a conditional update, so a double-clicked link can't sign in twice.
142
- - The "check your inbox" response is identical whether the address was known, unknown, or
143
- rate-limited — no account enumeration and no hint that a limit was hit.
144
- - Readers may set the display name shown beside their reflections; otherwise it's derived from the
145
- address (`mary.anne.smith@…` → "Mary Anne Smith").
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
- An email reader gets `provider: "email"` in the same identity columns OmniAuth uses, so downstream
149
- (progress, comments, leaderboard) treats every reader identically.
131
+ Email readers get `provider: "email"` in the same columns OmniAuth uses, so everything downstream
132
+ treats them identically.
150
133
 
151
- ### OmniAuth social sign-in
134
+ ### Social sign-in
152
135
 
153
- ### Quick setup
136
+ The generator writes both initializers for you and adds the `mount` line:
154
137
 
155
138
  ```bash
156
- bin/rails generate bible270:install --mount-at=/reading-plan --providers=github
139
+ bin/rails generate bible270:install --providers=github
157
140
  ```
158
141
 
159
- That writes `config/initializers/bible270.rb` (email sign-in enabled) and
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
- ```bash
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
- Finally register the callback URL with the provider:
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 "/reading-plan/auth" # == <mount point>/auth
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.omniauth_providers = [:github] # or [:github, [:google_oauth2, "Google"]]
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
- ### Notes worth knowing
171
+ Worth knowing:
205
172
 
206
- - **Sign-in is a POST, not a link.** OmniAuth 2.0+ refuses GET on its request phase
207
- (CVE-2015-9284), so every sign-in control the engine renders is a `button_to` carrying a CSRF
208
- token, and `omniauth-rails_csrf_protection` provides the Rails-aware verifier. If you build your
209
- own sign-in link, it must POST to `<mount>/auth/:provider`.
210
- - **One provider vs several.** With a single configured provider the header shows a direct
211
- "Sign in with X" button; with several it links to a sign-in page (`GET <mount>/sign_in`) listing
212
- them all.
213
- - **Return-to-origin.** Sign-in controls pass the current path as `origin`, so a reader who clicks
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
- ### Alternative: bridge your host's own users
182
+ ### Or use your own users
221
183
 
222
- If the host app already has authentication and you'd rather not use OmniAuth at all, set a
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 a reader to any host model polymorphically via `owner`. Note that
235
- ComfortableMediaSurfer's admin authentication is separate from public site visitors, so for a
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 works either **undated** (day numbers only) or **dated** (day numbers mapped onto a
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
- Bible270.configure do |config|
245
- # A community-wide start date — everyone reads together as a cohort.
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
- How the two combine:
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 is stamped with their own start date the first time they check something off, and can change it. |
260
- | a date | true | The community date is the default; any reader may override it with their own. |
261
- | a date | false | Everyone is pinned to the community date. The per-reader form is hidden. |
262
- | nil | false | Fully undated — day numbers only, no calendar anywhere. |
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
- Readers manage their own date from the overview page (`PATCH /start-date`, `DELETE /start-date`).
268
- Changing a start date only re-maps days onto the calendar — check-offs and reflections are keyed
269
- to day numbers and are never touched.
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
- The date helpers are pure functions on `Bible270::Plan`, so you can use them anywhere:
218
+ Date helpers are pure functions, usable anywhere:
272
219
 
273
220
  ```ruby
274
- Bible270::Plan.date_for(1, "2026-09-06") # => Sun, 06 Sep 2026
275
- Bible270::Plan.end_date_for("2026-09-06") # => Wed, 02 Jun 2027 (day 270)
276
- Bible270::Plan.day_for(Date.current, start) # => 42 (clamped to 1..270)
277
- Bible270::Plan.day_for(date, start, clamp: false) # => -3 or 271, to detect out-of-range
278
- Bible270::Plan.before_start?(date, start) # => true/false
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 # may readers set their own?
237
+ config.allow_reader_start_date = true
290
238
 
291
- config.parent_controller = "ActionController::Base" # or "::ApplicationController"
239
+ config.parent_controller = "ActionController::Base" # or "::ApplicationController"
292
240
  config.layout = "bible270/application"
293
241
 
294
- config.email_sign_in = true # passwordless email link
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] # optional social sign-in
299
- config.omniauth_path_prefix = nil # nil = derive "<mount>/auth"
300
- config.current_reader_resolver = nil # set to bridge host users instead
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 # defaults to plan root
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 = "ESV"
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
- Every reading reference links out to the configured Bible reader (Bible Gateway by
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
- 1. **Link to it** — mount at `/reading-plan` and add a CMS navigation link. The
324
- engine renders its own themed pages. Simplest and fully featured.
263
+ ## With ComfortableMediaSurfer
325
264
 
326
- 2. **Match the site chrome** — set `config.parent_controller = "::ApplicationController"`
327
- and `config.layout = "layouts/application"` (your site layout) so the plan pages
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 | Purpose |
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`); unique index prevents dups |
342
- | `bible270_sign_in_tokens` | short-lived magic-link tokens: email, **digest only**, expiry, consumed-at |
343
- | `bible270_comments` | a reflection on a day (optionally scoped to a track), public to all |
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 counts as "complete" for a reader when they've checked every track that has
346
- content that day (2 on a light-NT day, otherwise 3). All progress and comments are
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 (within the mount point)
280
+ ## Routes (inside the mount point)
350
281
 
351
282
  ```
352
283
  GET / days#index overview + community + calendar
353
- GET /day/:day days#show three readings, completers, reflections
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 a reader's progress + reflections
361
- GET /sign_in sessions#new email form + provider list
362
- POST /sign_in/email sessions#email_link send a magic link
363
- GET /sign_in/email/:token sessions#email_callback consume a magic link
364
- GET /auth/:provider/callback sessions#create (OmniAuth callback)
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 handled by the OmniAuth middleware, not the engine
300
+ POST <mount>/auth/:provider OmniAuth middleware, not the engine
370
301
  ```
371
302
 
372
- ## Tuning the plan shape
303
+ ## Tuning the plan
373
304
 
374
- The whole schedule is derived from a handful of constants in `Bible270::Plan`, so the shape
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
- | `NT_PASSES` | `2` | how many times the New Testament is read (each pass gets `DAYS / NT_PASSES` days) |
381
- | `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 |
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
- Everything is computed at load time and memoized; there is no generated schedule to migrate
386
- if you change these.
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
- ## Scaling note
317
+ All computed at load and memoized — no generated schedule, so nothing to migrate if you change them.
389
318
 
390
- The community leaderboard computes completed-days in Ruby from grouped check-off
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
- ## Troubleshooting
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
- **`ActiveRecord::DuplicateMigrationNameError: Multiple migrations have the name CreateBible270Readers`**
325
+ ## Troubleshooting
398
326
 
399
- You're on 0.6.2 or earlier, where the engine wrongly added its own `db/migrate` to the host app's
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
- ```bash
404
- bundle update bible270
405
- bin/rails db:migrate
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
- **Sign-in returns 404 on the provider callback**
333
+ **404 on the provider callback**
409
334
 
410
- OmniAuth's `path_prefix` doesn't match where the engine is mounted. They have to line up:
411
- mounting at `/reading-plan` means `path_prefix "/reading-plan/auth"`, and the URL registered with
412
- the provider must be `https://YOUR-HOST/reading-plan/auth/github/callback`. The install generator
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
- **Clicking "Sign in with …" does nothing, or raises `OmniAuth::AuthenticityError`**
339
+ **Sign-in button does nothing, or `OmniAuth::AuthenticityError`**
416
340
 
417
- The request has to be a POST with a CSRF token — OmniAuth 2.0+ refuses GET
418
- ([CVE-2015-9284](https://nvd.nist.gov/vuln/detail/CVE-2015-9284)). The engine's own controls are
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 sign-in email arrives**
344
+ **No email arrives**
423
345
 
424
- Email sign-in needs working Action Mailer delivery in the host app — the engine only calls
425
- `deliver_now` (or `deliver_later`). Check `config.mailer_from` is a real address your relay will
426
- accept, and watch the logs: delivery errors surface there. Nothing about a failure is shown to the
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 and live for `email_sign_in_ttl` (20 minutes by default). Some mail scanners
433
- and link-preview services fetch URLs before the recipient clicks, which consumes the token. If you
434
- see this a lot, lengthen the TTL or check whether something upstream is prefetching links.
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 renders unstyled inside my layout**
356
+ **Everything's unstyled in my layout**
437
357
 
438
- The engine ships scoped CSS in a partial rendered by its own layout. If you point
439
- `config.layout` at your application layout, add
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 / tests
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`.