bible270 0.17.2 → 0.18.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e3c27f2336f3e2a809205242bbc42c46d241e49b9ea2b1bf49ca288523d7c88
4
- data.tar.gz: 8e8c25b59eafde481a17546ac14b563527dd591ca64d9cf7541d95a01dd1f36c
3
+ metadata.gz: 7ba4b388cfe909e84f06899d72159471aab1a506154fc72854659d64320a3390
4
+ data.tar.gz: ebcf629ca6a12f57bf87d72073115c9a4655cec15e018e9349813aaab11a2e41
5
5
  SHA512:
6
- metadata.gz: 06af092550c30ded960e3611301d941e10fdea25e79fcfcab8aa97c4d68fee7b3780961f22bc4f81be4ba3f065ebe57642386ba318c69f80e2f1bd1e07ce0dec
7
- data.tar.gz: ed69c75af0e88e8dfeb347b756c6c10692f46b6e90196e96851aecc026a664752ce6f2fbb3de3b324734eff316555c229d19aa15270d26061b370b4db643f3ae
6
+ metadata.gz: 1b202223f06388e4b7f95d53b52f84cc84018ed7ac7ad8e49c48467525e7d76a0bd60185e46bd9b6223a83d7939666b6d7e7d6f1360ca625967452691d1fc528
7
+ data.tar.gz: 8c0b363492837cf5fb715ad13effdee46829cb34dad426e2bbfa8b24c4834f61ea16b2411b5c2e0fc0098d6b0fa397e8fe0cee487a763670f75f8f9fbb2bbb6c
data/CHANGELOG.md CHANGED
@@ -6,7 +6,51 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
6
6
 
7
7
  See the changes since the last release:
8
8
 
9
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.16.0...main)**
9
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.17.2...main)**
10
+
11
+ ## [0.17.2] - 2026-08-08
12
+
13
+ ### Added
14
+
15
+ - reader Bible translation preferences editable by admin users
16
+ - admin broadcast email tools
17
+
18
+ ### Changed
19
+
20
+ - extract first and last names from OmniAuth display names
21
+ - expand CI coverage reporting and add Ruby 4.0 coverage
22
+
23
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.17.1...v0.17.2)**
24
+
25
+ ## [0.17.1] - 2026-08-06
26
+
27
+ ### Added
28
+
29
+ - likes for reflections
30
+
31
+ ### Changed
32
+
33
+ - display timestamps using the local time zone
34
+ - rename reflections to 'posts' in UI headers, because we like alliteration
35
+
36
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.17.0...v0.17.1)**
37
+
38
+ ## [0.17.0] - 2026-08-05
39
+
40
+ ### Added
41
+
42
+ - a reflections page for browsing recent comments
43
+ - the ability for readers to edit their reflections
44
+
45
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.16.1...v0.17.0)**
46
+
47
+ ## [0.16.1] - 2026-08-05
48
+
49
+ ### Changed
50
+
51
+ - refactor mailer URL helpers to use the engine's routes
52
+
53
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.16.0...v0.16.1)**
10
54
 
11
55
  ## [0.16.0] - 2026-08-05
12
56
 
@@ -19,8 +63,6 @@ See the changes since the last release:
19
63
 
20
64
  ## [0.15.3] - 2026-08-04
21
65
 
22
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.15.2...v0.15.3)**
23
-
24
66
  ### Added
25
67
 
26
68
  - `Bible270::Checkoff.reset_column_information` to migrations to prevent `UnknownAttributeError` during backfills.
@@ -30,9 +72,9 @@ See the changes since the last release:
30
72
 
31
73
  - 'Today' now derived from system time, not UTC
32
74
 
33
- ## [0.15.2] - 2026-08-04
75
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.15.2...v0.15.3)**
34
76
 
35
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.15.1...v0.15.2)**
77
+ ## [0.15.2] - 2026-08-04
36
78
 
37
79
  ### Added
38
80
 
@@ -42,9 +84,9 @@ See the changes since the last release:
42
84
 
43
85
  - consistent reader sort order by first name in 'People' and 'Admin'
44
86
 
45
- ## [0.15.1] - 2026-08-03
87
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.15.1...v0.15.2)**
46
88
 
47
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.15.0...v0.15.1)**
89
+ ## [0.15.1] - 2026-08-03
48
90
 
49
91
  ### Added
50
92
 
@@ -59,9 +101,9 @@ See the changes since the last release:
59
101
 
60
102
  - old zip file of tests
61
103
 
62
- ## [0.15.0] - 2026-08-02
104
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.15.0...v0.15.1)**
63
105
 
64
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.14.1...v0.15.0)**
106
+ ## [0.15.0] - 2026-08-02
65
107
 
66
108
  ### Added
67
109
 
@@ -73,9 +115,9 @@ See the changes since the last release:
73
115
 
74
116
  - mobile session requiring sign-in every time: use persistent sessions using 'remember-me' cookie
75
117
 
76
- ## [0.14.1] - 2026-07-30
118
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.14.1...v0.15.0)**
77
119
 
78
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.14.0...v0.14.1)**
120
+ ## [0.14.1] - 2026-07-30
79
121
 
80
122
  ### Changed
81
123
 
@@ -87,9 +129,9 @@ See the changes since the last release:
87
129
  - display 'Start' not 'Continue' if reader is on day 1
88
130
  - adjust badge vertical alignment in styles
89
131
 
90
- ## [0.14.0] - 2026-07-30
132
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.14.0...v0.14.1)**
91
133
 
92
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.13.1...v0.14.0)**
134
+ ## [0.14.0] - 2026-07-30
93
135
 
94
136
  ### Added
95
137
 
@@ -107,16 +149,10 @@ See the changes since the last release:
107
149
  wrote an initializer that crashed the host app on boot. A test now renders every generator template
108
150
  and compiles the result, across three mount points.
109
151
 
110
- ## [0.13.1] — 2026-07-29
111
-
112
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.13.0...v0.13.1)**
113
-
114
- Nothing consequential.
152
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.13.0...v0.14.0)**
115
153
 
116
154
  ## [0.13.0] — 2026-07-29
117
155
 
118
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.12.0...v0.13.0)**
119
-
120
156
  ### Added
121
157
 
122
158
  - favicon support
@@ -127,9 +163,9 @@ Nothing consequential.
127
163
 
128
164
  - Correct verb for reading prompt: 'Start' and 'Continue
129
165
 
130
- ## [0.12.0] — 2026-07-29
166
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.12.0...v0.13.0)**
131
167
 
132
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.11.0...v0.12.0)**
168
+ ## [0.12.0] — 2026-07-29
133
169
 
134
170
  ### Added
135
171
 
@@ -141,9 +177,9 @@ Nothing consequential.
141
177
  - Top menu shows 'Progress' in place of user display name
142
178
  - Dependabot gem updates to minitest and rubocop
143
179
 
144
- ## [0.11.0] — 2026-07-28
180
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.11.0...v0.12.0)**
145
181
 
146
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.10.0...v0.11.0)**
182
+ ## [0.11.0] — 2026-07-28
147
183
 
148
184
  ### Added
149
185
 
@@ -195,9 +231,9 @@ Nothing consequential.
195
231
  - Migrations: `20260101000007` (settings). Active Storage tables are the host app's
196
232
  (`bin/rails active_storage:install`).
197
233
 
198
- ## [0.10.0] — 2026-07-26
234
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.10.0...v0.11.0)**
199
235
 
200
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.9.0...v0.10.0)**
236
+ ## [0.10.0] — 2026-07-26
201
237
 
202
238
  ### Reading plan
203
239
 
@@ -270,9 +306,9 @@ Nothing consequential.
270
306
 
271
307
  - Table names changed from `bible_reading_plan_*` in the 0.9.0 line; this is a fresh install.
272
308
 
273
- ## [0.9.0] — 2026-07-25
309
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.9.0...v0.10.0)**
274
310
 
275
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.6.2...v0.9.0)**
311
+ ## [0.9.0] — 2026-07-25
276
312
 
277
313
  ### Changed
278
314
 
@@ -290,6 +326,8 @@ Nothing consequential.
290
326
  * `Plan.nt_groups`, `format_pp_segment`, `pp_segment_length` (superseded by the shared `format_segment` and `segment_length`), and `totals[:nt_rest_days]`.
291
327
  * `nt_rest_days` remains but now always returns an empty array.
292
328
 
329
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.6.2...v0.9.0)**
330
+
293
331
  ## [0.6.2] — 2026-07-25
294
332
 
295
333
  This is the initial public release.
@@ -352,6 +390,10 @@ This is the initial public release.
352
390
 
353
391
  * Email sign-in requires working Action Mailer delivery in the host application. Set `config.mailer_from`; delivery is inline by default, or set `email_sign_in_deliver_later` when a queue backend is available.
354
392
 
393
+ [0.17.2]: https://github.com/avonderluft/bible270/releases/tag/v0.17.2
394
+ [0.17.1]: https://github.com/avonderluft/bible270/releases/tag/v0.17.1
395
+ [0.17.0]: https://github.com/avonderluft/bible270/releases/tag/v0.17.0
396
+ [0.16.1]: https://github.com/avonderluft/bible270/releases/tag/v0.16.1
355
397
  [0.16.0]: https://github.com/avonderluft/bible270/releases/tag/v0.16.0
356
398
  [0.15.3]: https://github.com/avonderluft/bible270/releases/tag/v0.15.3
357
399
  [0.15.2]: https://github.com/avonderluft/bible270/releases/tag/v0.15.2
@@ -359,7 +401,6 @@ This is the initial public release.
359
401
  [0.15.0]: https://github.com/avonderluft/bible270/releases/tag/v0.15.0
360
402
  [0.14.1]: https://github.com/avonderluft/bible270/releases/tag/v0.14.1
361
403
  [0.14.0]: https://github.com/avonderluft/bible270/releases/tag/v0.14.0
362
- [0.13.1]: https://github.com/avonderluft/bible270/releases/tag/v0.13.1
363
404
  [0.13.0]: https://github.com/avonderluft/bible270/releases/tag/v0.13.0
364
405
  [0.12.0]: https://github.com/avonderluft/bible270/releases/tag/v0.12.0
365
406
  [0.11.0]: https://github.com/avonderluft/bible270/releases/tag/v0.11.0
data/README.md CHANGED
@@ -117,6 +117,29 @@ bin/rails bible270:install:migrations # skips ones you already have
117
117
  bin/rails db:migrate
118
118
  ```
119
119
 
120
+ ### Reconciling a copied database
121
+
122
+ A copied database can contain the Bible270 tables while its `schema_migrations` versions do not match
123
+ the timestamped migration copies in the local app. In that case, preview the reconciliation:
124
+
125
+ ```bash
126
+ bin/rails bible270:migrations:reconcile
127
+ ```
128
+
129
+ The task recognizes Bible270 migrations—and the related generated Active Storage migration used for
130
+ reader avatars—by name, then verifies their tables, columns, current indexes, and foreign keys. It
131
+ does not treat an existing table alone as sufficient. After reviewing the list and backing up the
132
+ database, record only the migrations already reflected in the schema:
133
+
134
+ ```bash
135
+ APPLY=1 bin/rails bible270:migrations:reconcile
136
+ bin/rails db:migrate
137
+ ```
138
+
139
+ Preview mode never writes to the database. Apply mode only inserts verified local versions into
140
+ `schema_migrations`; it does not create, remove, or alter application tables. Migrations whose changes
141
+ are missing remain pending for the final `db:migrate` command.
142
+
120
143
  ## Bible270 Mount point in your Rails app
121
144
 
122
145
  The path lives in exactly **one** place, `config.mount_at`:
@@ -361,7 +384,10 @@ config.favicon = false # none; the host's favicon applies
361
384
  It's emitted by the engine's layout, so if you point `config.layout` at your own layout, add
362
385
  `<%= b270_favicon_tag %>` to its `<head>`.
363
386
 
364
- 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.
387
+ References link to Bible Gateway by default. Readers choose Bible Gateway or Blue Letter Bible from
388
+ their profile, and an admin can set the same source for any reader. The `HEB/GRK` option always uses
389
+ Blue Letter Bible: WLC for Old Testament, Psalms, and Proverbs readings, and mGNT for New Testament
390
+ readings. Set `passage_url_builder` to use another default reader, or a reader you host yourself.
365
391
 
366
392
  ## With ComfortableMediaSurfer
367
393
 
@@ -371,7 +397,7 @@ The CMS serves your content; this is a separate mounted app. Either **just link
371
397
 
372
398
  | Table | Holds |
373
399
  |-------|-------|
374
- | `bible270_readers` | identity: display name, avatar, provider/uid or polymorphic `owner`, `started_on` |
400
+ | `bible270_readers` | identity, avatar, provider/uid or polymorphic `owner`, start date, translation and passage-reader preferences |
375
401
  | `bible270_checkoffs` | one row per reader per day per track (`ot`/`nt`/`pp`), unique-indexed |
376
402
  | `bible270_comments` | a reflection on a day, optionally scoped to a track, public to all |
377
403
  | `bible270_sign_in_tokens` | magic-link tokens: email, **digest only**, expiry, consumed-at |
@@ -105,16 +105,22 @@ module Bible270
105
105
  return redirect_to(admin_path, alert: 'No such reader.') if reader.nil?
106
106
 
107
107
  code = params[:bible_version].to_s
108
+ source = params[:passage_source].presence || Reader::DEFAULT_PASSAGE_SOURCE
109
+ unless Reader::PASSAGE_SOURCES.include?(source)
110
+ return redirect_to admin_reader_path(reader), alert: 'That is not a reading-link source on the list.'
111
+ end
112
+
108
113
  # Blank means "no preference": the reader follows the site default, which is
109
114
  # not the same as choosing whichever translation the site happens to use now.
110
115
  if code.empty?
111
- reader.update(bible_version: nil)
116
+ reader.update(bible_version: nil, passage_source: source)
112
117
  return redirect_to admin_reader_path(reader),
113
118
  notice: "#{reader.display_name} now follows the site default " \
114
119
  "(#{Bible270.config.bible_version})."
115
120
  end
116
121
 
117
122
  if reader.update_bible_version(code)
123
+ reader.update_column(:passage_source, source)
118
124
  redirect_to admin_reader_path(reader),
119
125
  notice: "#{reader.display_name} now reads the #{reader.bible_version_label}."
120
126
  else
@@ -21,6 +21,8 @@ module Bible270
21
21
  if params[:bible_version].present? && !current_reader.update_bible_version(params[:bible_version])
22
22
  problems << 'a translation from the list'
23
23
  end
24
+ source = params[:passage_source].presence || Reader::DEFAULT_PASSAGE_SOURCE
25
+ problems << 'a reading-link source from the list' unless current_reader.update_passage_source(source)
24
26
  if params[:avatar].present? && !current_reader.attach_avatar(params[:avatar])
25
27
  problems << (current_reader.errors[:avatar].first || 'a valid image')
26
28
  end
@@ -112,13 +112,24 @@ module Bible270
112
112
  def b270_passage_url(reference, version = nil)
113
113
  return '#' if reference.blank?
114
114
 
115
- Bible270.config.passage_url_builder.call(reference, version || b270_bible_version)
115
+ selected_version = version || b270_bible_version
116
+ if selected_version != Translations::ORIGINAL_LANGUAGES && current_reader&.blue_letter_bible?
117
+ return Translations.passage_url(reference, selected_version, blue_letter: true)
118
+ end
119
+
120
+ Bible270.config.passage_url_builder.call(reference, selected_version)
116
121
  end
117
122
 
118
123
  # Small grey "(NKJV)" after a reading, so it's clear which translation the
119
- # link opens in.
120
- def b270_version_tag(version = nil)
121
- content_tag :span, "(#{version || b270_bible_version})", class: 'b270-version'
124
+ # link opens in. The original-language preference names the actual language
125
+ # used for each track rather than repeating the combined preference code.
126
+ def b270_version_tag(version = nil, track: nil)
127
+ label = version || b270_bible_version
128
+ if label == Translations::ORIGINAL_LANGUAGES && track
129
+ label = track.to_s == 'nt' ? 'Greek' : 'Hebrew'
130
+ end
131
+
132
+ content_tag :span, "(#{label})", class: 'b270-version'
122
133
  end
123
134
 
124
135
  def b270_avatar_src(reader)
@@ -25,8 +25,12 @@ module Bible270
25
25
  # transaction rolls back.
26
26
  after_create_commit :notify_of_registration
27
27
 
28
+ PASSAGE_SOURCES = %w[bible_gateway blue_letter_bible].freeze
29
+ DEFAULT_PASSAGE_SOURCE = 'bible_gateway'
30
+
28
31
  validates :display_name, presence: true
29
32
  validates :uid, uniqueness: { scope: :provider }, allow_nil: true
33
+ validates :passage_source, inclusion: { in: PASSAGE_SOURCES }
30
34
 
31
35
  # Build/refresh a reader from an OmniAuth auth hash. Tolerant of the various
32
36
  # shapes strategies return (OmniAuth::AuthHash, plain Hash, missing info).
@@ -164,6 +168,16 @@ module Bible270
164
168
  update(bible_version: Translations.normalize(code))
165
169
  end
166
170
 
171
+ def update_passage_source(source)
172
+ source = source.to_s
173
+ return false unless PASSAGE_SOURCES.include?(source)
174
+
175
+ update(passage_source: source)
176
+ end
177
+
178
+ def bible_gateway? = passage_source == 'bible_gateway'
179
+ def blue_letter_bible? = passage_source == 'blue_letter_bible'
180
+
167
181
  # Readers are listed by first name, matching how they are shown — the display
168
182
  # name is "First Last", so sorting on it needs only case folding. Surname
169
183
  # order was inconsistent with the community page and read oddly next to names
@@ -109,8 +109,9 @@
109
109
  Bible270::Translations.options,
110
110
  @reader.bible_version.to_s),
111
111
  class: "b270-select", style: "width:auto" %>
112
- <%= submit_tag "Save", class: "b270-btn" %>
113
112
  </div>
113
+ <%= render "bible270/shared/passage_source", reader: @reader %>
114
+ <%= submit_tag "Save", class: "b270-btn" %>
114
115
  <% end %>
115
116
  </div>
116
117
 
@@ -35,7 +35,7 @@
35
35
  <% end %>
36
36
  </ul>
37
37
  <div class="b270-parts-meta">
38
- <%= read_parts.size %> of <%= parts.size %> chapters <%= b270_version_tag %>
38
+ <%= read_parts.size %> of <%= parts.size %> chapters <%= b270_version_tag(track: track) %>
39
39
  </div>
40
40
  </div>
41
41
  <% else %>
@@ -50,7 +50,7 @@
50
50
  <% end %>
51
51
  <div class="b270-rbody">
52
52
  <div class="b270-cat"><%= t[:label] %></div>
53
- <div class="b270-ref"><%= link_to ref, b270_passage_url(ref), target: "_blank", rel: "noopener" %> <%= b270_version_tag %></div>
53
+ <div class="b270-ref"><%= link_to ref, b270_passage_url(ref), target: "_blank", rel: "noopener" %> <%= b270_version_tag(track: track) %></div>
54
54
  </div>
55
55
  <% end %>
56
56
  </div>
@@ -39,6 +39,7 @@
39
39
  class: "b270-select" %>
40
40
  <span class="b270-hint" style="margin:0">Reading links open in this translation.</span>
41
41
  </label>
42
+ <%= render "bible270/shared/passage_source", reader: current_reader %>
42
43
 
43
44
  <%= submit_tag "Save", class: "b270-btn" %>
44
45
  <% end %>
@@ -0,0 +1,13 @@
1
+ <% original_languages = reader.effective_bible_version == Bible270::Translations::ORIGINAL_LANGUAGES %>
2
+ <div class="b270-source-choices" role="radiogroup" aria-label="Reading link source">
3
+ <label class="b270-source-choice <%= 'disabled' if original_languages %>">
4
+ <%= radio_button_tag :passage_source, "bible_gateway",
5
+ !original_languages && reader.bible_gateway?, disabled: original_languages %>
6
+ <span>Bible Gateway</span>
7
+ </label>
8
+ <label class="b270-source-choice <%= 'disabled' if original_languages %>">
9
+ <%= radio_button_tag :passage_source, "blue_letter_bible",
10
+ original_languages || reader.blue_letter_bible?, disabled: original_languages %>
11
+ <span>Blue Letter Bible</span>
12
+ </label>
13
+ </div>
@@ -134,6 +134,15 @@
134
134
  .b270-parts-meta{margin-top:10px;font-size:12.5px;color:var(--faint)}
135
135
  .b270-version{font-size:11.5px;color:var(--faint);font-weight:400;letter-spacing:.02em;white-space:nowrap}
136
136
  .b270-select{font:inherit;font-size:14px;padding:10px 30px 10px 12px;border:1px solid var(--line);border-radius:9px;background:#fff;color:var(--ink);width:100%;max-width:100%;text-overflow:ellipsis}
137
+ .b270-source-choices{display:flex;align-items:center;gap:18px;flex-wrap:wrap;margin:8px 0 2px;color:var(--muted);font-size:12.5px}
138
+ .b270-source-choice{display:inline-flex;align-items:center;gap:7px;cursor:pointer;white-space:nowrap}
139
+ .b270-source-choice input{appearance:none;width:17px;height:17px;margin:0;border:1px solid var(--line);border-radius:3px;background:#fff;display:grid;place-content:center;cursor:pointer}
140
+ .b270-source-choice input::before{content:"";width:9px;height:9px;transform:scale(0);transition:transform .1s;clip-path:polygon(14% 44%,0 59%,38% 100%,100% 18%,82% 0,37% 65%);background:var(--paper)}
141
+ .b270-source-choice input:checked{background:var(--gold);border-color:var(--gold)}
142
+ .b270-source-choice input:checked::before{transform:scale(1)}
143
+ .b270-source-choice input:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
144
+ .b270-source-choice.disabled{cursor:default;opacity:.55}
145
+ .b270-source-choice input:disabled{cursor:default}
137
146
  .b270-ref a{text-decoration:none;border-bottom:1px solid transparent}
138
147
  .b270-ref a:hover{border-bottom-color:var(--gold)}
139
148
  .b270-reading.done .b270-ref{color:var(--muted)}
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddBlueLetterBibleToBible270Readers < ActiveRecord::Migration[7.0]
4
+ def change
5
+ add_column :bible270_readers, :blue_letter_bible, :boolean, default: false, null: false
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddPassageSourceToBible270Readers < ActiveRecord::Migration[7.0]
4
+ def up
5
+ add_column :bible270_readers, :passage_source, :string, default: 'bible_gateway', null: false
6
+
7
+ return unless column_exists?(:bible270_readers, :blue_letter_bible)
8
+
9
+ execute <<~SQL.squish
10
+ UPDATE bible270_readers
11
+ SET passage_source = 'blue_letter_bible'
12
+ WHERE blue_letter_bible = TRUE
13
+ SQL
14
+ remove_column :bible270_readers, :blue_letter_bible
15
+ end
16
+
17
+ def down
18
+ add_column :bible270_readers, :blue_letter_bible, :boolean, default: false, null: false
19
+ execute <<~SQL.squish
20
+ UPDATE bible270_readers
21
+ SET blue_letter_bible = TRUE
22
+ WHERE passage_source = 'blue_letter_bible'
23
+ SQL
24
+ remove_column :bible270_readers, :passage_source
25
+ end
26
+ end
@@ -347,12 +347,7 @@ module Bible270
347
347
  @after_sign_out_path = nil
348
348
  @bible_version = 'NKJV'
349
349
  @bible_versions = Translations::VERSIONS.keys
350
- @passage_url_builder = ->(reference, version) do
351
- search = URI.encode_www_form_component(reference)
352
- # Translations.gateway_code maps 'NASB95' to Bible Gateway's 'NASB1995'.
353
- version = URI.encode_www_form_component(Translations.gateway_code(version))
354
- "https://www.biblegateway.com/passage/?search=#{search}&version=#{version}"
355
- end
350
+ @passage_url_builder = ->(reference, version) { Translations.passage_url(reference, version) }
356
351
  @app_name = 'Daily Bread'
357
352
  @tagline = 'Journeying through Scripture in 9 months'
358
353
  @time_zone = nil
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bible270
4
+ # Repairs schema_migrations after a database is copied without matching migration
5
+ # bookkeeping. A migration is safe to mark only when every table, column and
6
+ # current index it contributes is already present.
7
+ class MigrationReconciler
8
+ Status = Struct.new(:migration, :missing, keyword_init: true) do
9
+ def complete? = missing.empty?
10
+ end
11
+
12
+ CHECKS = {
13
+ 'CreateBible270Readers' => :check_readers,
14
+ 'CreateBible270Checkoffs' => :check_checkoffs,
15
+ 'CreateBible270Comments' => :check_comments,
16
+ 'CreateBible270SignInTokens' => :check_sign_in_tokens,
17
+ 'AddNamesToBible270Readers' => :check_names,
18
+ 'AddApprovalToBible270Comments' => :check_approval,
19
+ 'CreateBible270Settings' => :check_settings,
20
+ 'AddBibleVersionToBible270Readers' => :check_bible_version,
21
+ 'AddPartToBible270Checkoffs' => :check_checkoff_parts,
22
+ 'AddRememberTokenToBible270Readers' => :check_remember_token,
23
+ 'AddMentionNoticesToBible270Readers' => :check_mention_notices,
24
+ 'AddParentToBible270Comments' => :check_comment_parent,
25
+ 'CreateBible270Likes' => :check_likes,
26
+ 'AddBlueLetterBibleToBible270Readers' => :check_blue_letter_bible,
27
+ 'AddPassageSourceToBible270Readers' => :check_passage_source,
28
+ 'CreateActiveStorageTables' => :check_active_storage
29
+ }.freeze
30
+
31
+ def self.for_current_database
32
+ context = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths)
33
+ new(connection: ActiveRecord::Base.connection, migration_context: context)
34
+ end
35
+
36
+ def initialize(connection:, migration_context:)
37
+ @connection = connection
38
+ @migration_context = migration_context
39
+ end
40
+
41
+ def statuses
42
+ applied = migration_context.get_all_versions.map(&:to_i)
43
+ migration_context.migrations.filter_map do |migration|
44
+ check = CHECKS[migration.name]
45
+ next unless check
46
+ next if applied.include?(migration.version.to_i)
47
+
48
+ Status.new(migration: migration, missing: public_send(check))
49
+ end
50
+ end
51
+
52
+ def mark_applied!(status)
53
+ raise ArgumentError, "#{status.migration.name} is not reflected in the schema" unless status.complete?
54
+
55
+ migration_context.schema_migration.create_version(status.migration.version.to_s)
56
+ end
57
+
58
+ def check_readers
59
+ table_missing(
60
+ :bible270_readers,
61
+ columns: %i[id display_name email avatar_url provider uid owner_type owner_id started_on created_at updated_at],
62
+ indexes: [
63
+ { columns: %i[owner_type owner_id] },
64
+ { columns: %i[provider uid], unique: true }
65
+ ]
66
+ )
67
+ end
68
+
69
+ def check_checkoffs
70
+ table_missing(
71
+ :bible270_checkoffs,
72
+ columns: %i[id reader_id day track created_at updated_at],
73
+ indexes: [{ columns: %i[reader_id] }, { columns: %i[day] }],
74
+ foreign_keys: [{ column: :reader_id, to_table: :bible270_readers }]
75
+ )
76
+ end
77
+
78
+ def check_comments
79
+ table_missing(
80
+ :bible270_comments,
81
+ columns: %i[id reader_id day track body created_at updated_at],
82
+ indexes: [{ columns: %i[reader_id] }, { columns: %i[day created_at] }],
83
+ foreign_keys: [{ column: :reader_id, to_table: :bible270_readers }]
84
+ )
85
+ end
86
+
87
+ def check_sign_in_tokens
88
+ table_missing(
89
+ :bible270_sign_in_tokens,
90
+ columns: %i[id email token_digest display_name expires_at consumed_at created_at updated_at],
91
+ indexes: [
92
+ { columns: %i[token_digest], unique: true },
93
+ { columns: %i[email created_at] }
94
+ ]
95
+ )
96
+ end
97
+
98
+ def check_names
99
+ table_missing(
100
+ :bible270_readers,
101
+ columns: %i[first_name last_name],
102
+ indexes: [{ columns: %i[last_name first_name] }]
103
+ ) + table_missing(:bible270_sign_in_tokens, columns: %i[first_name last_name])
104
+ end
105
+
106
+ def check_approval
107
+ table_missing(
108
+ :bible270_comments,
109
+ columns: %i[approved moderated_at],
110
+ indexes: [{ columns: %i[approved day] }]
111
+ )
112
+ end
113
+
114
+ def check_settings
115
+ table_missing(
116
+ :bible270_settings,
117
+ columns: %i[id key value created_at updated_at],
118
+ indexes: [{ columns: %i[key], unique: true }]
119
+ )
120
+ end
121
+
122
+ def check_bible_version
123
+ table_missing(:bible270_readers, columns: %i[bible_version])
124
+ end
125
+
126
+ def check_checkoff_parts
127
+ table_missing(
128
+ :bible270_checkoffs,
129
+ columns: %i[part],
130
+ indexes: [{ columns: %i[reader_id day track part], unique: true }]
131
+ )
132
+ end
133
+
134
+ def check_remember_token
135
+ table_missing(
136
+ :bible270_readers,
137
+ columns: %i[remember_token],
138
+ indexes: [{ columns: %i[remember_token] }]
139
+ )
140
+ end
141
+
142
+ def check_mention_notices
143
+ table_missing(:bible270_readers, columns: %i[notify_on_mention])
144
+ end
145
+
146
+ def check_comment_parent
147
+ table_missing(
148
+ :bible270_comments,
149
+ columns: %i[parent_id],
150
+ indexes: [{ columns: %i[parent_id] }],
151
+ foreign_keys: [{ column: :parent_id, to_table: :bible270_comments }]
152
+ )
153
+ end
154
+
155
+ def check_likes
156
+ table_missing(
157
+ :bible270_likes,
158
+ columns: %i[id reader_id comment_id created_at updated_at],
159
+ indexes: [
160
+ { columns: %i[reader_id comment_id], unique: true },
161
+ { columns: %i[comment_id] }
162
+ ],
163
+ foreign_keys: [
164
+ { column: :reader_id, to_table: :bible270_readers },
165
+ { column: :comment_id, to_table: :bible270_comments }
166
+ ]
167
+ )
168
+ end
169
+
170
+ def check_blue_letter_bible
171
+ return [] if connection.column_exists?(:bible270_readers, :passage_source)
172
+
173
+ table_missing(:bible270_readers, columns: %i[blue_letter_bible])
174
+ end
175
+
176
+ def check_passage_source
177
+ table_missing(:bible270_readers, columns: %i[passage_source])
178
+ end
179
+
180
+ # The Bible270 installer can install Active Storage for reader avatars. A
181
+ # copied database can therefore have the same timestamp mismatch for Rails'
182
+ # generated Active Storage migration as it has for the engine migrations.
183
+ def check_active_storage
184
+ table_missing(
185
+ :active_storage_blobs,
186
+ columns: %i[id key filename content_type metadata service_name byte_size checksum created_at],
187
+ indexes: [{ columns: %i[key], unique: true }]
188
+ ) + table_missing(
189
+ :active_storage_attachments,
190
+ columns: %i[id name record_type record_id blob_id created_at],
191
+ indexes: [
192
+ { columns: %i[blob_id] },
193
+ { columns: %i[record_type record_id name blob_id], unique: true }
194
+ ],
195
+ foreign_keys: [{ column: :blob_id, to_table: :active_storage_blobs }]
196
+ ) + table_missing(
197
+ :active_storage_variant_records,
198
+ columns: %i[id blob_id variation_digest],
199
+ indexes: [{ columns: %i[blob_id variation_digest], unique: true }],
200
+ foreign_keys: [{ column: :blob_id, to_table: :active_storage_blobs }]
201
+ )
202
+ end
203
+
204
+ private
205
+
206
+ attr_reader :connection, :migration_context
207
+
208
+ def table_missing(table, columns:, indexes: [], foreign_keys: [])
209
+ return ["table #{table}"] unless connection.data_source_exists?(table)
210
+
211
+ missing_columns(table, columns) +
212
+ missing_indexes(table, indexes) +
213
+ missing_foreign_keys(table, foreign_keys)
214
+ end
215
+
216
+ def missing_columns(table, expected_columns)
217
+ existing = connection.columns(table).map { |column| column.name.to_s }
218
+ expected_columns.filter_map do |column|
219
+ "column #{table}.#{column}" unless existing.include?(column.to_s)
220
+ end
221
+ end
222
+
223
+ def missing_indexes(table, expected_indexes)
224
+ existing = connection.indexes(table)
225
+ expected_indexes.filter_map do |expected|
226
+ columns = expected.fetch(:columns).map(&:to_s)
227
+ next if existing.any? { |index| matching_index?(index, columns, expected) }
228
+
229
+ qualifier = expected[:unique] ? 'unique index' : 'index'
230
+ "#{qualifier} #{table}(#{columns.join(', ')})"
231
+ end
232
+ end
233
+
234
+ def matching_index?(index, columns, expected)
235
+ index.columns == columns && (!expected.key?(:unique) || index.unique == expected[:unique])
236
+ end
237
+
238
+ def missing_foreign_keys(table, expected_foreign_keys)
239
+ existing = connection.foreign_keys(table)
240
+ expected_foreign_keys.filter_map do |expected|
241
+ next if existing.any? { |foreign_key| matching_foreign_key?(foreign_key, expected) }
242
+
243
+ "foreign key #{table}.#{expected[:column]} -> #{expected[:to_table]}"
244
+ end
245
+ end
246
+
247
+ def matching_foreign_key?(foreign_key, expected)
248
+ foreign_key.column.to_s == expected.fetch(:column).to_s &&
249
+ foreign_key.to_table.to_s == expected.fetch(:to_table).to_s
250
+ end
251
+ end
252
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uri'
4
+ require 'bible270/versification'
5
+
3
6
  module Bible270
4
7
  # The translations a reader may choose between, and how each maps onto the
5
8
  # external reader's own code. Kept free of Rails so it can be unit tested.
@@ -11,14 +14,22 @@ module Bible270
11
14
  # `label` is the full name, for prose. `short` is what goes in the select,
12
15
  # where "New American Standard Bible 1995" overflows the box.
13
16
  VERSIONS = {
14
- 'NKJV' => { label: 'New King James Version', short: 'New King James Version', gateway: 'NKJV' },
15
- 'NASB95' => { label: 'New American Standard Bible 1995', short: 'New American Standard 1995', gateway: 'NASB1995' },
16
- 'LSB' => { label: 'Legacy Standard Bible', short: 'Legacy Standard Version', gateway: 'LSB' },
17
- 'ESV' => { label: 'English Standard Version', short: 'English Standard Version', gateway: 'ESV' },
18
- 'KJV' => { label: 'King James Version', short: 'King James Version', gateway: 'KJV' }
17
+ 'NKJV' => { label: 'New King James Version', short: 'New King James Version', gateway: 'NKJV', blue_letter: 'nkjv' },
18
+ 'NASB95' => { label: 'New American Standard Bible 1995', short: 'New American Standard 1995', gateway: 'NASB1995', blue_letter: 'nasb95' },
19
+ 'LSB' => { label: 'Legacy Standard Bible', short: 'Legacy Standard Version', gateway: 'LSB', blue_letter: 'lsb' },
20
+ 'ESV' => { label: 'English Standard Version', short: 'English Standard Version', gateway: 'ESV', blue_letter: 'esv' },
21
+ 'KJV' => { label: 'King James Version', short: 'King James Version', gateway: 'KJV', blue_letter: 'kjv' },
22
+ 'HEB/GRK' => { label: 'Hebrew and Greek', short: 'Hebrew and Greek', gateway: 'WLC', blue_letter: 'wlc/mgnt' }
19
23
  }.freeze
20
24
 
21
25
  DEFAULT = 'NKJV'
26
+ ORIGINAL_LANGUAGES = 'HEB/GRK'
27
+ BLUE_LETTER_BASE_URL = 'https://www.blueletterbible.org'
28
+ BLUE_LETTER_BOOK_CODES = %w[
29
+ gen exo lev num deu jos jdg rut 1sa 2sa 1ki 2ki 1ch 2ch ezr neh est job psa pro ecc sng
30
+ isa jer lam eze dan hos joe amo oba jon mic nah hab zep hag zec mal mat mar luk joh act rom
31
+ 1co 2co gal eph php col 1th 2th 1ti 2ti tit phm heb jas 1pe 2pe 1jo 2jo 3jo jud rev
32
+ ].freeze
22
33
 
23
34
  module_function
24
35
 
@@ -57,6 +68,51 @@ module Bible270
57
68
  codes.map { |code| ["#{code} — #{short_label(code)}", code] }
58
69
  end
59
70
 
71
+ def passage_url(reference, version, blue_letter: false)
72
+ if blue_letter || normalize(version) == ORIGINAL_LANGUAGES
73
+ return blue_letter_url(reference, version)
74
+ end
75
+
76
+ search = URI.encode_www_form_component(reference)
77
+ gateway = URI.encode_www_form_component(gateway_code(version))
78
+ "https://www.biblegateway.com/passage/?search=#{search}&version=#{gateway}"
79
+ end
80
+
81
+ # Blue Letter Bible identifies a verse with its canonical chapter number times
82
+ # 1,000 plus the verse number: Genesis 1:1 is 1001 and Matthew 1:1 is 930001.
83
+ # A plan reference can span chapters or books; BLB has no equivalent passage
84
+ # query URL, so link to the first chapter/verse and let its chapter navigation
85
+ # carry the reader onward.
86
+ def blue_letter_url(reference, version = ORIGINAL_LANGUAGES)
87
+ books = Versification::VERSES.keys
88
+ book_index = books.index { |book| reference.to_s.start_with?("#{book} ") }
89
+ return "#{BLUE_LETTER_BASE_URL}/" unless book_index
90
+
91
+ book = books[book_index]
92
+ location = reference.to_s.delete_prefix("#{book} ")
93
+ match = location.match(%r{\A(\d+)(?::(\d+))?})
94
+ return "#{BLUE_LETTER_BASE_URL}/" unless match
95
+
96
+ chapter = match[1].to_i
97
+ verse = match[2]&.to_i || 1
98
+ chapter_verses = Versification::VERSES[book]
99
+ return "#{BLUE_LETTER_BASE_URL}/" unless chapter.between?(1, chapter_verses.length)
100
+ return "#{BLUE_LETTER_BASE_URL}/" unless verse.between?(1, chapter_verses[chapter - 1])
101
+
102
+ canonical_chapter = books.first(book_index).sum { |prior| Versification::VERSES[prior].length } + chapter
103
+ new_testament = book_index >= books.index('Matthew')
104
+ reader = if normalize(version) == ORIGINAL_LANGUAGES
105
+ new_testament ? 'mgnt' : 'wlc'
106
+ else
107
+ VERSIONS.dig(normalize(version), :blue_letter)
108
+ end
109
+ return "#{BLUE_LETTER_BASE_URL}/" if reader.to_s.empty?
110
+
111
+ anchor = (canonical_chapter * 1000) + verse
112
+ "#{BLUE_LETTER_BASE_URL}/#{reader}/#{BLUE_LETTER_BOOK_CODES[book_index]}/" \
113
+ "#{chapter}/#{verse}/s_#{anchor}"
114
+ end
115
+
60
116
  # Falls back to the configured default, then to ESV, so a link is never built
61
117
  # with a blank version.
62
118
  def resolve(code)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bible270
4
- VERSION = '0.17.2'
4
+ VERSION = '0.18.0'
5
5
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bible270/migration_reconciler'
4
+
5
+ namespace :bible270 do
6
+ namespace :migrations do
7
+ desc 'Reconcile copied Bible270/Active Storage tables with schema_migrations (set APPLY=1 to write)'
8
+ task reconcile: :environment do
9
+ reconciler = Bible270::MigrationReconciler.for_current_database
10
+ statuses = reconciler.statuses
11
+
12
+ if statuses.empty?
13
+ puts 'No pending Bible270 or related Active Storage migrations were found.'
14
+ next
15
+ end
16
+
17
+ complete, incomplete = statuses.partition(&:complete?)
18
+
19
+ puts 'Already reflected in the database schema:'
20
+ if complete.empty?
21
+ puts ' (none)'
22
+ else
23
+ complete.each do |status|
24
+ puts " #{status.migration.version} #{status.migration.name}"
25
+ end
26
+ end
27
+
28
+ unless incomplete.empty?
29
+ puts
30
+ puts 'Still missing schema changes (these will remain pending):'
31
+ incomplete.each do |status|
32
+ puts " #{status.migration.version} #{status.migration.name}"
33
+ status.missing.each { |item| puts " - #{item}" }
34
+ end
35
+ end
36
+
37
+ puts
38
+ unless ENV['APPLY'] == '1'
39
+ puts 'Preview only; schema_migrations was not changed.'
40
+ puts 'Review the list, back up the database, then rerun with APPLY=1.'
41
+ next
42
+ end
43
+
44
+ if complete.empty?
45
+ puts 'Nothing was safe to mark as applied.'
46
+ next
47
+ end
48
+
49
+ ActiveRecord::Base.transaction do
50
+ complete.each { |status| reconciler.mark_applied!(status) }
51
+ end
52
+
53
+ puts "Marked #{complete.size} verified #{'migration'.pluralize(complete.size)} as applied."
54
+ puts 'Run bin/rails db:migrate to apply the migrations whose schema changes are still missing.'
55
+ end
56
+ end
57
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bible270
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.2
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew vonderLuft
@@ -136,6 +136,7 @@ files:
136
136
  - app/views/bible270/shared/_footer.html.erb
137
137
  - app/views/bible270/shared/_grid_legend.html.erb
138
138
  - app/views/bible270/shared/_header.html.erb
139
+ - app/views/bible270/shared/_passage_source.html.erb
139
140
  - app/views/bible270/shared/_progress.html.erb
140
141
  - app/views/bible270/shared/_reflections.html.erb
141
142
  - app/views/bible270/shared/_styles.html.erb
@@ -157,6 +158,8 @@ files:
157
158
  - db/migrate/20260101000011_add_mention_notices_to_bible270_readers.rb
158
159
  - db/migrate/20260101000012_add_parent_to_bible270_comments.rb
159
160
  - db/migrate/20260101000013_create_bible270_likes.rb
161
+ - db/migrate/20260101000014_add_blue_letter_bible_to_bible270_readers.rb
162
+ - db/migrate/20260101000015_add_passage_source_to_bible270_readers.rb
160
163
  - lib/bible270.rb
161
164
  - lib/bible270/avatars.rb
162
165
  - lib/bible270/configuration.rb
@@ -164,6 +167,7 @@ files:
164
167
  - lib/bible270/engine.rb
165
168
  - lib/bible270/favicon.rb
166
169
  - lib/bible270/mentions.rb
170
+ - lib/bible270/migration_reconciler.rb
167
171
  - lib/bible270/names.rb
168
172
  - lib/bible270/plan.rb
169
173
  - lib/bible270/translations.rb
@@ -172,6 +176,7 @@ files:
172
176
  - lib/generators/bible270/install/install_generator.rb
173
177
  - lib/generators/bible270/install/templates/bible270.rb.tt
174
178
  - lib/generators/bible270/install/templates/omniauth.rb.tt
179
+ - lib/tasks/bible270_migrations.rake
175
180
  homepage: https://github.com/avonderluft/bible270
176
181
  licenses:
177
182
  - MIT