bible270 0.15.1 → 0.15.3
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 +49 -1
- data/README.md +1 -1
- data/app/controllers/bible270/readers_controller.rb +2 -1
- data/app/helpers/bible270/plan_helper.rb +13 -0
- data/app/models/bible270/reader.rb +11 -7
- data/app/models/bible270/sign_in_token.rb +7 -1
- data/app/views/bible270/days/_start_date.html.erb +1 -1
- data/app/views/bible270/shared/_header.html.erb +7 -2
- data/app/views/bible270/shared/_styles.html.erb +6 -1
- data/db/migrate/20260101000009_add_part_to_bible270_checkoffs.rb +9 -0
- data/lib/bible270/configuration.rb +13 -2
- data/lib/bible270/favicon.rb +11 -0
- data/lib/bible270/version.rb +1 -1
- data/lib/bible270.rb +16 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3bac102109d922307b87047b9151aa16354f5aacbfa1829a1419ca3bd02dc251
|
|
4
|
+
data.tar.gz: 825c3e6d6da35d724ea0cb7ece96244ac8555251c4c8b2165492139f49c7c4d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54bb44c5d582ad4f4795a68a09d1facd54a0c4b8a77890ed0fb900c7d357ece4cb4e0656570c4bb15241fbb6cf9c6ef161f4a3d11962dd706aa41f03241b1f98
|
|
7
|
+
data.tar.gz: 961462928a89a8d3b060a4101996851115f2b8d479567a3741c3613f83823e7a78225113af1c82f1fc125d04101b93a5ee2ac2b2c554909fef1e6d859151e050
|
data/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,49 @@ 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.15.
|
|
9
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.15.3...main)**
|
|
10
|
+
|
|
11
|
+
## [0.15.3] - 2026-08-04
|
|
12
|
+
|
|
13
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.15.2...v0.15.2)**
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `Bible270::Checkoff.reset_column_information` to migrations to prevent `UnknownAttributeError` during backfills.
|
|
18
|
+
- more tests (reader identity, sign-ins, and token management) to increase coverage to > 93%
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- 'Today' now derived from system time, not UTC
|
|
23
|
+
|
|
24
|
+
## [0.15.2] - 2026-08-04
|
|
25
|
+
|
|
26
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.15.1...v0.15.2)**
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- app icon to shared header partial
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- consistent reader sort order by first name in 'People' and 'Admin'
|
|
35
|
+
|
|
36
|
+
## [0.15.1] - 2026-08-03
|
|
37
|
+
|
|
38
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.15.0...v0.15.1)**
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- CSV file with versification by day
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- refactor 'progress' and 'reflections' into partials to be more DRY
|
|
47
|
+
- update Coverage badge to be less cached
|
|
48
|
+
|
|
49
|
+
### Removed
|
|
50
|
+
|
|
51
|
+
- old zip file of tests
|
|
10
52
|
|
|
11
53
|
## [0.15.0] - 2026-08-02
|
|
12
54
|
|
|
@@ -317,6 +359,12 @@ This is the initial public release.
|
|
|
317
359
|
|
|
318
360
|
* 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.
|
|
319
361
|
|
|
362
|
+
[0.15.3]: https://github.com/avonderluft/bible270/releases/tag/v0.15.3
|
|
363
|
+
[0.15.2]: https://github.com/avonderluft/bible270/releases/tag/v0.15.2
|
|
364
|
+
[0.15.1]: https://github.com/avonderluft/bible270/releases/tag/v0.15.1
|
|
365
|
+
[0.15.0]: https://github.com/avonderluft/bible270/releases/tag/v0.15.0
|
|
366
|
+
[0.14.1]: https://github.com/avonderluft/bible270/releases/tag/v0.14.1
|
|
367
|
+
[0.14.0]: https://github.com/avonderluft/bible270/releases/tag/v0.14.0
|
|
320
368
|
[0.13.1]: https://github.com/avonderluft/bible270/releases/tag/v0.13.1
|
|
321
369
|
[0.13.0]: https://github.com/avonderluft/bible270/releases/tag/v0.13.0
|
|
322
370
|
[0.12.0]: https://github.com/avonderluft/bible270/releases/tag/v0.12.0
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Three readings a day, sized by **verse count** so each day takes roughly the sam
|
|
|
14
14
|
- **New Testament** — once through, a reading every day. 260 chapters over 270 days, long chapters are divided to even out the daily reading
|
|
15
15
|
- **Psalms & Proverbs** — once each, interleaved in one track. 181 chapters have to cover 270 days, so some are divided, shared between the two books by verse load. **A psalm of 25 verses or fewer is never divided**, keeping 131 of the 150 whole; longer psalms and Proverbs chapters break at their turns of thought (see `CHAPTER_BREAKS`). **Psalm 119 is 11 sections of 16 verses** — two of its eight-verse acrostic stanzas each. Proverbs lands on 89 days, near its proportional share. **A divided chapter is always read on consecutive days**, so nothing is inserted between the parts of a split psalm.
|
|
16
16
|
|
|
17
|
-
Genesis → Malachi and Revelation 22 both land on day 270, as does the second Proverbs 31
|
|
17
|
+
Genesis → Malachi and Revelation 22 both land on day 270, as does the second part of Proverbs 31. Every day carries all three tracks. A typical day is ~119 verses (range 76–175).
|
|
18
18
|
|
|
19
19
|
The schedule is pure deterministic Ruby (`Bible270::Plan`) — none of it is stored. Portions come from per-chapter verse counts in `Bible270::Versification`. The DB only holds readers, check-offs, comments, and sign-in tokens.
|
|
20
20
|
|
|
@@ -7,7 +7,8 @@ module Bible270
|
|
|
7
7
|
counts = Checkoff.group(:reader_id, :day).count
|
|
8
8
|
@days_completed = Hash.new(0)
|
|
9
9
|
counts.each { |(rid, day), n| @days_completed[rid] += 1 if n >= Plan.total_parts(day) }
|
|
10
|
-
|
|
10
|
+
# Progress first, then by name — the same name ordering the admin list uses.
|
|
11
|
+
@readers.sort_by! { |r| [-@days_completed[r.id], r.sort_name] }
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
# The reader's own progress. Deliberately open to visitors: the panel shows an
|
|
@@ -51,6 +51,19 @@ module Bible270
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# The mark beside the title in the header. Larger than the favicon and inlined
|
|
55
|
+
# as SVG, so it stays crisp at any size and needs no asset pipeline.
|
|
56
|
+
def b270_mark(size: 38)
|
|
57
|
+
configured = Bible270.config.header_mark
|
|
58
|
+
return nil if configured == false
|
|
59
|
+
|
|
60
|
+
if configured.is_a?(String) && configured.present?
|
|
61
|
+
return image_tag(configured, width: size, height: size, alt: '', class: 'b270-mark')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
content_tag :span, Favicon.inline_svg(size: size).html_safe, class: 'b270-mark'
|
|
65
|
+
end
|
|
66
|
+
|
|
54
67
|
def b270_favicon_tag
|
|
55
68
|
favicon = Bible270.config.favicon
|
|
56
69
|
return if favicon == false
|
|
@@ -136,8 +136,12 @@ module Bible270
|
|
|
136
136
|
update(bible_version: Translations.normalize(code))
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
+
# Readers are listed by first name, matching how they are shown — the display
|
|
140
|
+
# name is "First Last", so sorting on it needs only case folding. Surname
|
|
141
|
+
# order was inconsistent with the community page and read oddly next to names
|
|
142
|
+
# displayed first-name-first.
|
|
139
143
|
def sort_name
|
|
140
|
-
|
|
144
|
+
display_name.to_s.strip.downcase
|
|
141
145
|
end
|
|
142
146
|
|
|
143
147
|
# ---- staying signed in -------------------------------------------------
|
|
@@ -344,7 +348,7 @@ module Bible270
|
|
|
344
348
|
|
|
345
349
|
# Put this reader on `day` as of `on` — i.e. back-date the start so that the
|
|
346
350
|
# given date lands on the given day of the plan.
|
|
347
|
-
def restart_on!(day:, on:
|
|
351
|
+
def restart_on!(day:, on: Bible270.today)
|
|
348
352
|
day = day.to_i
|
|
349
353
|
return false unless Plan.valid_day?(day)
|
|
350
354
|
|
|
@@ -402,7 +406,7 @@ module Bible270
|
|
|
402
406
|
|
|
403
407
|
# The plan day that today corresponds to (clamped into range), or nil when undated.
|
|
404
408
|
def calendar_day
|
|
405
|
-
Plan.day_for(
|
|
409
|
+
Plan.day_for(Bible270.today, effective_start_date)
|
|
406
410
|
end
|
|
407
411
|
|
|
408
412
|
# Raw, unclamped — lets callers distinguish "not started yet" / "finished".
|
|
@@ -421,15 +425,15 @@ module Bible270
|
|
|
421
425
|
end
|
|
422
426
|
|
|
423
427
|
def raw_calendar_day
|
|
424
|
-
Plan.day_for(
|
|
428
|
+
Plan.day_for(Bible270.today, effective_start_date, clamp: false)
|
|
425
429
|
end
|
|
426
430
|
|
|
427
431
|
def not_started_yet?
|
|
428
|
-
Plan.before_start?(
|
|
432
|
+
Plan.before_start?(Bible270.today, effective_start_date)
|
|
429
433
|
end
|
|
430
434
|
|
|
431
435
|
def past_end_date?
|
|
432
|
-
Plan.after_end?(
|
|
436
|
+
Plan.after_end?(Bible270.today, effective_start_date)
|
|
433
437
|
end
|
|
434
438
|
|
|
435
439
|
def plan_end_date
|
|
@@ -488,7 +492,7 @@ module Bible270
|
|
|
488
492
|
return if started_on.present?
|
|
489
493
|
return if config.start_date.present?
|
|
490
494
|
|
|
491
|
-
update!(started_on:
|
|
495
|
+
update!(started_on: Bible270.today)
|
|
492
496
|
end
|
|
493
497
|
end
|
|
494
498
|
end
|
|
@@ -54,9 +54,15 @@ module Bible270
|
|
|
54
54
|
|
|
55
55
|
# Too many outstanding requests for one address in the window.
|
|
56
56
|
def self.rate_limited?(address)
|
|
57
|
+
# Normalised, because issue! stores a normalised address: querying the raw
|
|
58
|
+
# one let 'Eager@example.org' and 'eager@example.org' each have their own
|
|
59
|
+
# allowance, so varying the case bypassed the limit entirely.
|
|
60
|
+
email = EmailSignIn.normalize_email(address)
|
|
61
|
+
return false if email.nil?
|
|
62
|
+
|
|
57
63
|
window = Bible270.config.email_sign_in_window
|
|
58
64
|
max = Bible270.config.email_sign_in_max_per_window
|
|
59
|
-
where(email:
|
|
65
|
+
where(email: email).where(created_at: (Time.current - window)..).count >= max
|
|
60
66
|
end
|
|
61
67
|
|
|
62
68
|
# Housekeeping: drop spent and stale rows. Safe to run from a cron/rake task.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<% end %>
|
|
9
9
|
· finishes <%= reader.plan_end_date.strftime("%B %-d, %Y") %>
|
|
10
10
|
<% if reader.not_started_yet? %>
|
|
11
|
-
· <em>begins in <%= (reader.effective_start_date -
|
|
11
|
+
· <em>begins in <%= (reader.effective_start_date - Bible270.today).to_i %> days</em>
|
|
12
12
|
<% elsif reader.past_end_date? %>
|
|
13
13
|
· <em>the 270 days have elapsed</em>
|
|
14
14
|
<% elsif (off = reader.days_off_pace) %>
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
<header class="b270-topbar">
|
|
2
2
|
<div class="b270-brand">
|
|
3
|
-
<%= link_to
|
|
4
|
-
|
|
3
|
+
<%= link_to root_path, class: "b270-brandlink" do %>
|
|
4
|
+
<%= b270_mark %>
|
|
5
|
+
<span class="b270-brandtext">
|
|
6
|
+
<span class="b270-title"><%= Bible270.config.app_name %></span>
|
|
7
|
+
<span class="b270-tagline"><%= Bible270.config.tagline %></span>
|
|
8
|
+
</span>
|
|
9
|
+
<% end %>
|
|
5
10
|
</div>
|
|
6
11
|
<nav class="b270-nav">
|
|
7
12
|
<%= link_to "Plan", root_path %>
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
.b270-wrap{max-width:820px;margin:0 auto;padding:clamp(14px,3.5vw,36px) clamp(14px,4vw,40px) 64px}
|
|
15
15
|
|
|
16
16
|
.b270-topbar{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding-bottom:20px;border-bottom:1px solid var(--line-soft);margin-bottom:26px}
|
|
17
|
-
.b270-brand{display:flex;
|
|
17
|
+
.b270-brand{display:flex;margin-right:auto}
|
|
18
|
+
/* The whole brand is one link, so the mark and the words highlight together. */
|
|
19
|
+
.b270-brandlink{display:flex;align-items:center;gap:12px;text-decoration:none}
|
|
20
|
+
.b270-brandtext{display:flex;flex-direction:column}
|
|
21
|
+
.b270-mark{display:flex;align-items:center;flex:none;line-height:0}
|
|
22
|
+
.b270-mark svg,.b270-mark img{display:block;border-radius:8px}
|
|
18
23
|
.b270-title{font-family:"Spectral",Georgia,serif;font-weight:600;font-size:22px;text-decoration:none}
|
|
19
24
|
.b270-tagline{font-size:12px;color:var(--faint);letter-spacing:.04em}
|
|
20
25
|
.b270-nav{display:flex;align-items:center;gap:16px;font-size:14px}
|
|
@@ -12,6 +12,11 @@ class AddPartToBible270Checkoffs < ActiveRecord::Migration[7.0]
|
|
|
12
12
|
add_index :bible270_checkoffs, %i[reader_id day track part], unique: true,
|
|
13
13
|
name: 'idx_b270_checkoffs_part_unique'
|
|
14
14
|
|
|
15
|
+
# The backfill goes through the model, which caches its columns. If Checkoff
|
|
16
|
+
# was loaded before this ran — likely, since the app boots first — it does not
|
|
17
|
+
# know about `part` yet and insert_all raises UnknownAttributeError.
|
|
18
|
+
Bible270::Checkoff.reset_column_information
|
|
19
|
+
|
|
15
20
|
expand_existing_checkoffs
|
|
16
21
|
end
|
|
17
22
|
|
|
@@ -28,6 +33,10 @@ class AddPartToBible270Checkoffs < ActiveRecord::Migration[7.0]
|
|
|
28
33
|
add_index :bible270_checkoffs, %i[reader_id day track], unique: true,
|
|
29
34
|
name: 'idx_b270_checkoffs_unique'
|
|
30
35
|
remove_column :bible270_checkoffs, :part
|
|
36
|
+
|
|
37
|
+
# Likewise on the way back down: leaving the cached column in place makes the
|
|
38
|
+
# model try to write one the table no longer has.
|
|
39
|
+
Bible270::Checkoff.reset_column_information
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
private
|
|
@@ -292,7 +292,7 @@ module Bible270
|
|
|
292
292
|
# from, so without this the notice names the reader but cannot link to them.
|
|
293
293
|
attr_accessor :mailer_host
|
|
294
294
|
|
|
295
|
-
attr_accessor :after_sign_out_path, :email_sign_in_max_per_window, :passage_url_builder, :tagline, :footer_html, :footer, :favicon, :avatar_max_bytes, :chapter_breaks, :admin_emails, :admin_resolver, :bible_version, :require_sign_in_to_participate
|
|
295
|
+
attr_accessor :after_sign_out_path, :email_sign_in_max_per_window, :passage_url_builder, :tagline, :footer_html, :footer, :favicon, :avatar_max_bytes, :chapter_breaks, :admin_emails, :admin_resolver, :bible_version, :header_mark, :remember_for, :require_sign_in_to_participate
|
|
296
296
|
|
|
297
297
|
# Public labels.
|
|
298
298
|
attr_accessor :app_name
|
|
@@ -318,7 +318,16 @@ module Bible270
|
|
|
318
318
|
#
|
|
319
319
|
# Expressed in seconds rather than 1.year because this file has to load
|
|
320
320
|
# without ActiveSupport.
|
|
321
|
-
|
|
321
|
+
# The mark beside the title in the header. nil uses the built-in loaf, false
|
|
322
|
+
# shows none, and a string is used as an image source — so a host can put its
|
|
323
|
+
# own logo there without overriding the partial.
|
|
324
|
+
# Which clock decides what day it is. nil follows the machine's own time zone,
|
|
325
|
+
# which is what a self-hosted plan usually wants — Rails defaults Time.zone to
|
|
326
|
+
# UTC, so a reader in Los Angeles saw the next day's reading from 4pm.
|
|
327
|
+
#
|
|
328
|
+
# config.time_zone = nil # the system clock (default)
|
|
329
|
+
# config.time_zone = 'America/Los_Angeles' # a named zone
|
|
330
|
+
attr_accessor :time_zone
|
|
322
331
|
|
|
323
332
|
def remember_signed_in?
|
|
324
333
|
remember_for.to_i.positive?
|
|
@@ -340,6 +349,8 @@ module Bible270
|
|
|
340
349
|
end
|
|
341
350
|
@app_name = 'Daily Bread'
|
|
342
351
|
@tagline = 'Journeying through Scripture in 9 months'
|
|
352
|
+
@time_zone = nil
|
|
353
|
+
@header_mark = nil
|
|
343
354
|
@remember_for = 365 * 24 * 60 * 60
|
|
344
355
|
@require_sign_in_to_participate = true
|
|
345
356
|
self.mount_at = '/daily-bread'
|
data/lib/bible270/favicon.rb
CHANGED
|
@@ -32,5 +32,16 @@ module Bible270
|
|
|
32
32
|
compact = SVG.gsub(%r{\s+}, ' ').strip
|
|
33
33
|
"data:image/svg+xml,#{compact.gsub(' ', '%20')}"
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
# The same mark for inline use in a page, at whatever size. The percent
|
|
37
|
+
# encoding above is only correct inside a data URI: inlined into HTML,
|
|
38
|
+
# '%23231f18' is not a colour, so it is decoded here.
|
|
39
|
+
def inline_svg(size: nil)
|
|
40
|
+
markup = SVG.gsub('%23', '#').strip
|
|
41
|
+
return markup if size.nil?
|
|
42
|
+
|
|
43
|
+
pixels = size.to_i
|
|
44
|
+
markup.sub('<svg ', "<svg width='#{pixels}' height='#{pixels}' ")
|
|
45
|
+
end
|
|
35
46
|
end
|
|
36
47
|
end
|
data/lib/bible270/version.rb
CHANGED
data/lib/bible270.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
3
5
|
require 'bible270/version'
|
|
4
6
|
require 'bible270/plan'
|
|
5
7
|
require 'bible270/email_sign_in'
|
|
@@ -11,4 +13,18 @@ require 'bible270/configuration'
|
|
|
11
13
|
require 'bible270/engine' if defined?(Rails::Engine)
|
|
12
14
|
|
|
13
15
|
module Bible270
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# The plan's idea of today.
|
|
19
|
+
#
|
|
20
|
+
# Date.current follows Rails' Time.zone, which is UTC unless the host app sets
|
|
21
|
+
# it; Date.today follows the machine. A self-hosted reading plan wants the
|
|
22
|
+
# latter, so that is the default, with config.time_zone to override.
|
|
23
|
+
def today
|
|
24
|
+
zone = config.time_zone
|
|
25
|
+
return Date.today if zone.nil? || zone.to_s.empty?
|
|
26
|
+
|
|
27
|
+
found = Time.respond_to?(:find_zone) ? Time.find_zone(zone) : nil
|
|
28
|
+
found ? found.today : Date.today
|
|
29
|
+
end
|
|
14
30
|
end
|