bible270 1.4.0 → 1.4.1
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 +32 -3
- data/app/controllers/bible270/admin_controller.rb +60 -27
- data/app/views/bible270/days/show.html.erb +0 -1
- data/app/views/bible270/shared/_interaction_ui.html.erb +3 -0
- data/app/views/bible270/shared/_styles.html.erb +3 -3
- data/lib/bible270/version.rb +1 -1
- data/lib/generators/bible270/install/install_generator.rb +130 -42
- metadata +1 -2
- data/app/views/bible270/shared/_day_completion_effect.html.erb +0 -67
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bcb59bf731c08118644e9303ed749397492c6c7fea523a20cf4eb3c17b84281f
|
|
4
|
+
data.tar.gz: e1f6b1be1b245e859370ae745eb05f8adf47df953f89ad7cb28c700ad705830c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 312efc5cd9bfcb503430d1adc601b21a93658c9d378060182d3453b747d43674a736a83b753accaf0f0d738b0da005656c87490f17a51814800b3798632c09ab
|
|
7
|
+
data.tar.gz: c45129d6950f6b40f8bb0477b9de95ed9e98bb5ad3c1f9fa36bfb1b44eb5c1b26e337fc9d31e7dc9554115f2c4ecde14427127333bf2287925a0c0a33457b470
|
data/CHANGELOG.md
CHANGED
|
@@ -4,17 +4,43 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
|
|
|
4
4
|
|
|
5
5
|
## Unreleased Changes
|
|
6
6
|
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- simplify day completion to one continuous server-confirmed dove flight from the lower center, eliminating the stagger between a pre-submit animation and the Turbo or HTML response
|
|
10
|
+
|
|
11
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.4.0...main)**
|
|
12
|
+
|
|
13
|
+
## [1.4.0] - 2026-09-02
|
|
14
|
+
|
|
7
15
|
### Added
|
|
8
16
|
|
|
9
17
|
- mark a newly completed day with a brief, translucent dove animation that begins immediately on the final checkoff in JavaScript-capable browsers and remains available after Turbo or HTML-fallback responses; it respects reduced-motion preferences, replaces the final generic success message, and never blocks interaction
|
|
10
18
|
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- refine the completion animation to 1.6 seconds with smoother easing, limit checkoff transitions to relevant visual properties, and make reduced-motion transitions effectively instantaneous
|
|
22
|
+
|
|
11
23
|
### Fixed
|
|
12
24
|
|
|
13
|
-
- correct Day 27's New Testament reading to include Matthew 26:75
|
|
14
|
-
- silently recover all Turbo interactions from stale sessions, retry once with a fresh CSRF token, and reload rather than submit with a known-stale token if refreshing fails
|
|
15
25
|
- keep repeated or concurrent explicit checkoffs idempotent instead of raising a duplicate-validation Rails 422 error
|
|
16
26
|
|
|
17
|
-
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.3.0
|
|
27
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.3.2...v1.4.0)**
|
|
28
|
+
|
|
29
|
+
## [1.3.2] - 2026-09-02
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- silently recover all Turbo interactions from stale sessions, retry once with a fresh CSRF token, and reload rather than submit with a known-stale token if refreshing fails
|
|
34
|
+
|
|
35
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.3.1...v1.3.2)**
|
|
36
|
+
|
|
37
|
+
## [1.3.1] - 2026-09-01
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- correct Day 27's New Testament reading to include Matthew 26:75
|
|
42
|
+
|
|
43
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.3.0...v1.3.1)**
|
|
18
44
|
|
|
19
45
|
## [1.3.0] - 2026-08-29
|
|
20
46
|
|
|
@@ -596,6 +622,9 @@ This is the initial public release.
|
|
|
596
622
|
|
|
597
623
|
* 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.
|
|
598
624
|
|
|
625
|
+
[1.4.0]: https://github.com/avonderluft/bible270/releases/tag/v1.4.0
|
|
626
|
+
[1.3.2]: https://github.com/avonderluft/bible270/releases/tag/v1.3.2
|
|
627
|
+
[1.3.1]: https://github.com/avonderluft/bible270/releases/tag/v1.3.1
|
|
599
628
|
[1.3.0]: https://github.com/avonderluft/bible270/releases/tag/v1.3.0
|
|
600
629
|
[1.2.0]: https://github.com/avonderluft/bible270/releases/tag/v1.2.0
|
|
601
630
|
[1.1.7]: https://github.com/avonderluft/bible270/releases/tag/v1.1.7
|
|
@@ -20,12 +20,15 @@ module Bible270
|
|
|
20
20
|
# ---- enrolment --------------------------------------------------------
|
|
21
21
|
|
|
22
22
|
def update_enrollment
|
|
23
|
-
|
|
23
|
+
case params[:state]
|
|
24
|
+
when 'closed'
|
|
24
25
|
Setting.close_enrollment!
|
|
25
26
|
redirect_to admin_path, notice: 'Closed to new readers. Existing readers can still sign in.'
|
|
26
|
-
|
|
27
|
+
when 'open'
|
|
27
28
|
Setting.open_enrollment!
|
|
28
29
|
redirect_to admin_path, notice: 'Open to new readers.'
|
|
30
|
+
else
|
|
31
|
+
redirect_to admin_path, alert: 'Choose whether enrollment is open or closed. Nothing was changed.'
|
|
29
32
|
end
|
|
30
33
|
end
|
|
31
34
|
|
|
@@ -122,12 +125,23 @@ module Bible270
|
|
|
122
125
|
recipients = Reader.where.not(email: [nil, '']).order(:id)
|
|
123
126
|
return redirect_to(admin_path, alert: 'Nobody has an email address.') if recipients.empty?
|
|
124
127
|
|
|
128
|
+
recipient_count = recipients.count
|
|
125
129
|
sent = deliver_broadcast(recipients, subject, body)
|
|
130
|
+
later = Bible270.config.registration_notice_deliver_later
|
|
131
|
+
|
|
132
|
+
if sent.zero?
|
|
133
|
+
action = later ? 'queued' : 'sent'
|
|
134
|
+
return redirect_to admin_path, alert: "The message could not be #{action} to any readers."
|
|
135
|
+
end
|
|
136
|
+
|
|
126
137
|
Setting.write(LAST_BROADCAST_AT, Time.current.iso8601)
|
|
127
138
|
Setting.write(LAST_BROADCAST_SUBJECT, subject)
|
|
128
139
|
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
action = later ? 'Queued for' : 'Sent to'
|
|
141
|
+
failures = recipient_count - sent
|
|
142
|
+
notice = "#{action} #{sent} #{'reader'.pluralize(sent)}."
|
|
143
|
+
notice += " #{failures} #{'reader'.pluralize(failures)} could not be #{later ? 'queued' : 'sent'}." if failures.positive?
|
|
144
|
+
redirect_to admin_path, notice: notice
|
|
131
145
|
end
|
|
132
146
|
|
|
133
147
|
def update_bible_version
|
|
@@ -169,9 +183,14 @@ module Bible270
|
|
|
169
183
|
redirect_to admin_reader_path(@reader),
|
|
170
184
|
alert: "Couldn't read #{params[:start_date].inspect} as a date."
|
|
171
185
|
end
|
|
172
|
-
elsif params[:day].present?
|
|
173
|
-
|
|
174
|
-
|
|
186
|
+
elsif params[:day].present?
|
|
187
|
+
day = strict_integer(params[:day], 1..Plan::DAYS)
|
|
188
|
+
if day && @reader.restart_on!(day: day)
|
|
189
|
+
redirect_to admin_reader_path(@reader),
|
|
190
|
+
notice: "#{@reader.display_name} is now on day #{day}."
|
|
191
|
+
else
|
|
192
|
+
redirect_to admin_reader_path(@reader), alert: 'Give a start date or a day between 1 and 270.'
|
|
193
|
+
end
|
|
175
194
|
else
|
|
176
195
|
redirect_to admin_reader_path(@reader), alert: 'Give a start date or a day between 1 and 270.'
|
|
177
196
|
end
|
|
@@ -181,12 +200,15 @@ module Bible270
|
|
|
181
200
|
# themselves on their own profile.
|
|
182
201
|
def update_profile
|
|
183
202
|
problems = []
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
problems
|
|
203
|
+
@reader.transaction do
|
|
204
|
+
if (params[:first_name].present? || params[:last_name].present?) && !@reader.update_names(params[:first_name],
|
|
205
|
+
params[:last_name])
|
|
206
|
+
problems << 'both a first and last name'
|
|
207
|
+
end
|
|
208
|
+
if problems.empty? && params[:avatar].present? && !@reader.attach_avatar(params[:avatar])
|
|
209
|
+
problems << (@reader.errors[:avatar].first || 'a valid image')
|
|
210
|
+
end
|
|
211
|
+
raise ActiveRecord::Rollback if problems.any?
|
|
190
212
|
end
|
|
191
213
|
|
|
192
214
|
if problems.empty?
|
|
@@ -220,22 +242,24 @@ module Bible270
|
|
|
220
242
|
end
|
|
221
243
|
|
|
222
244
|
def complete_through
|
|
223
|
-
day = params[:day]
|
|
224
|
-
|
|
225
|
-
redirect_to admin_reader_path(@reader),
|
|
226
|
-
notice: day.zero? ? 'All check-offs cleared.' : "Marked days 1-#{day} complete."
|
|
227
|
-
else
|
|
228
|
-
redirect_to admin_reader_path(@reader), alert: 'Give a day between 0 and 270.'
|
|
245
|
+
day = strict_integer(params[:day], 0..Plan::DAYS)
|
|
246
|
+
unless day
|
|
247
|
+
return redirect_to admin_reader_path(@reader), alert: 'Give a day between 0 and 270.'
|
|
229
248
|
end
|
|
249
|
+
|
|
250
|
+
@reader.mark_through!(day)
|
|
251
|
+
redirect_to admin_reader_path(@reader),
|
|
252
|
+
notice: day.zero? ? 'All check-offs cleared.' : "Marked days 1-#{day} complete."
|
|
230
253
|
end
|
|
231
254
|
|
|
232
255
|
def toggle_day
|
|
233
|
-
day = params[:day]
|
|
234
|
-
|
|
235
|
-
redirect_to admin_reader_path(@reader,
|
|
236
|
-
else
|
|
237
|
-
redirect_to admin_reader_path(@reader), alert: 'That day is outside the plan.'
|
|
256
|
+
day = strict_integer(params[:day], 1..Plan::DAYS)
|
|
257
|
+
unless day
|
|
258
|
+
return redirect_to admin_reader_path(@reader), alert: 'That day is outside the plan.'
|
|
238
259
|
end
|
|
260
|
+
|
|
261
|
+
@reader.toggle_day!(day)
|
|
262
|
+
redirect_to admin_reader_path(@reader, anchor: "day-#{day}")
|
|
239
263
|
end
|
|
240
264
|
|
|
241
265
|
# Failures are counted rather than raised: one bad address must not stop the
|
|
@@ -282,16 +306,25 @@ module Bible270
|
|
|
282
306
|
|
|
283
307
|
def load_comment
|
|
284
308
|
@comment = Comment.find_by(id: params[:id])
|
|
285
|
-
|
|
309
|
+
return if @comment
|
|
310
|
+
|
|
311
|
+
redirect_to admin_comments_path, alert: 'Reflection not found.'
|
|
286
312
|
end
|
|
287
313
|
|
|
288
314
|
def redirect_back_to_comments(notice)
|
|
289
|
-
redirect_to(params[:return_to]
|
|
315
|
+
redirect_to(url_from(params[:return_to]) || admin_comments_path, notice: notice)
|
|
290
316
|
end
|
|
291
317
|
|
|
292
318
|
def load_reader
|
|
293
319
|
@reader = Reader.find_by(id: params[:id])
|
|
294
|
-
|
|
320
|
+
return if @reader
|
|
321
|
+
|
|
322
|
+
redirect_to admin_path, alert: 'Reader not found.'
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def strict_integer(value, range)
|
|
326
|
+
integer = Integer(value.to_s, 10, exception: false)
|
|
327
|
+
integer if range.cover?(integer)
|
|
295
328
|
end
|
|
296
329
|
end
|
|
297
330
|
end
|
|
@@ -201,6 +201,9 @@
|
|
|
201
201
|
}
|
|
202
202
|
});
|
|
203
203
|
document.addEventListener("turbo:load", reset);
|
|
204
|
+
document.addEventListener("animationend", (event) => {
|
|
205
|
+
if (event.target.matches("[data-b270-completion-dove]")) event.target.remove();
|
|
206
|
+
});
|
|
204
207
|
|
|
205
208
|
window.Bible270InteractionUI = { reset };
|
|
206
209
|
reset();
|
|
@@ -147,14 +147,14 @@
|
|
|
147
147
|
.b270-day-complete strong{font-family:"Spectral",serif;font-size:16px}
|
|
148
148
|
.b270-day-complete span{font-size:12px;color:var(--muted)}
|
|
149
149
|
.b270 .b270-day-complete .b270-btn{flex:none;margin:0 0 0 auto}
|
|
150
|
-
.b270-completion-dove{position:fixed;z-index:1000;top:
|
|
150
|
+
.b270-completion-dove{position:fixed;z-index:1000;top:72vh;left:50vw;
|
|
151
151
|
width:min(75vw,97.5vh);height:auto;overflow:visible;pointer-events:none;opacity:0;
|
|
152
152
|
filter:drop-shadow(0 0 8px rgba(255,255,255,1)) drop-shadow(0 0 24px rgba(255,252,220,1)) drop-shadow(0 0 52px rgba(225,191,92,.82));
|
|
153
153
|
will-change:top,left,transform,opacity;animation:b270-dove-flight 1.6s cubic-bezier(.18,.72,.28,1) both}
|
|
154
154
|
.b270-dove-silhouette{fill:url(#b270-dove-light);stroke:none}
|
|
155
155
|
@keyframes b270-dove-flight{
|
|
156
|
-
0%{top:
|
|
157
|
-
8%{top:calc(
|
|
156
|
+
0%{top:72vh;left:50vw;opacity:.22;visibility:visible;transform:translate(-50%,-50%) scale(.02) rotate(-8deg)}
|
|
157
|
+
8%{top:calc(72vh - 10px);left:50vw;opacity:.78;transform:translate(-50%,-50%) scale(.08) rotate(-5deg)}
|
|
158
158
|
64%{top:38vh;left:52vw;opacity:.92;transform:translate(-50%,-50%) scale(1) rotate(4deg)}
|
|
159
159
|
99%{visibility:visible}
|
|
160
160
|
100%{top:-25vh;left:115vw;opacity:0;visibility:hidden;transform:translate(-50%,-50%) scale(1.55) rotate(12deg)}
|
data/lib/bible270/version.rb
CHANGED
|
@@ -18,10 +18,12 @@ module Bible270
|
|
|
18
18
|
# --mailer-from=no-reply@example.org
|
|
19
19
|
#
|
|
20
20
|
class InstallGenerator < Rails::Generators::Base
|
|
21
|
-
MOUNT_LINE
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
MOUNT_LINE = 'mount Bible270::Engine, at: Bible270.config.mount_at'
|
|
22
|
+
MOUNT_LINE_PATTERN = %r{^\s*mount\s*\(?\s*Bible270::Engine\b}
|
|
23
|
+
PROVIDER_IDENTIFIER = %r{\A[a-z][a-z0-9_]*\z}
|
|
24
|
+
DEFAULT_MOUNT = '/daily-bread'
|
|
25
|
+
DEFAULT_PROVIDERS = 'github'
|
|
26
|
+
KNOWN_PROVIDERS = {
|
|
25
27
|
'github' => 'omniauth-github',
|
|
26
28
|
'google_oauth2' => 'omniauth-google-oauth2',
|
|
27
29
|
'facebook' => 'omniauth-facebook',
|
|
@@ -97,18 +99,23 @@ module Bible270
|
|
|
97
99
|
def add_strategy_gems
|
|
98
100
|
return if @providers.empty?
|
|
99
101
|
|
|
100
|
-
missing = @providers.filter_map { |
|
|
101
|
-
|
|
102
|
+
missing = @providers.filter_map { |provider| KNOWN_PROVIDERS[provider] }
|
|
103
|
+
.reject { |gem_name| gemfile_has?(gem_name) }
|
|
104
|
+
@unknown_providers = @providers.reject { |provider| KNOWN_PROVIDERS.key?(provider) }
|
|
105
|
+
@strategy_gems_added = missing.any?
|
|
102
106
|
|
|
103
107
|
missing.each { |name| gem name }
|
|
104
108
|
|
|
105
|
-
return if
|
|
109
|
+
return if @unknown_providers.empty?
|
|
106
110
|
|
|
107
|
-
|
|
111
|
+
identifiers = @unknown_providers.map { |provider| provider.to_sym.inspect }.join(', ')
|
|
112
|
+
say " Unrecognised provider(s): #{@unknown_providers.join(', ')}.", :yellow
|
|
113
|
+
say " Add an OmniAuth strategy gem that registers #{identifiers} to the Gemfile, then run bundle install.",
|
|
114
|
+
:yellow
|
|
108
115
|
end
|
|
109
116
|
|
|
110
117
|
def install_bundle
|
|
111
|
-
return
|
|
118
|
+
return unless @strategy_gems_added
|
|
112
119
|
|
|
113
120
|
@bundled = decide(:bundle, 'Run bundle install now (needed before the app can boot)?', true)
|
|
114
121
|
run 'bundle install' if @bundled
|
|
@@ -121,7 +128,16 @@ module Bible270
|
|
|
121
128
|
# rather than assuming a fresh app has it.
|
|
122
129
|
|
|
123
130
|
def ensure_active_storage
|
|
124
|
-
|
|
131
|
+
if skip_rails_commands?
|
|
132
|
+
@active_storage = if !active_storage_enabled?
|
|
133
|
+
:unavailable
|
|
134
|
+
elsif active_storage_installed?
|
|
135
|
+
:present
|
|
136
|
+
else
|
|
137
|
+
:pending
|
|
138
|
+
end
|
|
139
|
+
return
|
|
140
|
+
end
|
|
125
141
|
|
|
126
142
|
unless active_storage_enabled?
|
|
127
143
|
@active_storage = :unavailable
|
|
@@ -153,11 +169,15 @@ module Bible270
|
|
|
153
169
|
# Migrations run before the initializers are written, so a problem in the
|
|
154
170
|
# generated config can never block the database work.
|
|
155
171
|
def copy_migrations
|
|
156
|
-
|
|
172
|
+
if skip_rails_commands?
|
|
173
|
+
@migrations_copied = false
|
|
174
|
+
return
|
|
175
|
+
end
|
|
157
176
|
|
|
158
177
|
say ''
|
|
159
178
|
say 'Copying migrations into db/migrate', :yellow
|
|
160
179
|
rails_command 'bible270:install:migrations'
|
|
180
|
+
@migrations_copied = true
|
|
161
181
|
end
|
|
162
182
|
|
|
163
183
|
def run_migrations
|
|
@@ -191,7 +211,11 @@ module Bible270
|
|
|
191
211
|
|
|
192
212
|
def add_route
|
|
193
213
|
if routes_mounted?
|
|
194
|
-
|
|
214
|
+
if move_mount_above_catch_all
|
|
215
|
+
say ' config/routes.rb already mounted Bible270::Engine; moved it above the catch-all route.', :yellow
|
|
216
|
+
else
|
|
217
|
+
say " config/routes.rb already mounts Bible270::Engine — left as is.#{cms_order_hint}", :yellow
|
|
218
|
+
end
|
|
195
219
|
return
|
|
196
220
|
end
|
|
197
221
|
|
|
@@ -202,7 +226,9 @@ module Bible270
|
|
|
202
226
|
# across files, an unusual layout, a missing file). Verify rather than
|
|
203
227
|
# assume, since a silent miss leaves the plan unreachable.
|
|
204
228
|
if routes_mounted?
|
|
205
|
-
|
|
229
|
+
moved = move_mount_above_catch_all
|
|
230
|
+
detail = moved ? ' Mounted above the catch-all route.' : cms_order_hint
|
|
231
|
+
say " Mounted in config/routes.rb.#{detail}", :green
|
|
206
232
|
else
|
|
207
233
|
@route_failed = true
|
|
208
234
|
say ' Could not edit config/routes.rb automatically.', :red
|
|
@@ -220,7 +246,7 @@ module Bible270
|
|
|
220
246
|
say " Plan mounted at #{@mount_at}"
|
|
221
247
|
say " Sign-in #{auth_summary}"
|
|
222
248
|
say " Initializers #{initializer_summary}"
|
|
223
|
-
say " Migrations #{
|
|
249
|
+
say " Migrations #{migration_summary}"
|
|
224
250
|
say " Picture uploads #{active_storage_summary}"
|
|
225
251
|
say ''
|
|
226
252
|
|
|
@@ -241,16 +267,13 @@ module Bible270
|
|
|
241
267
|
# bin/rails call fail ("Could not find ... in locally installed gems"), so
|
|
242
268
|
# don't even try.
|
|
243
269
|
def skip_rails_commands?
|
|
244
|
-
return false unless @
|
|
270
|
+
return false unless @strategy_gems_added && @bundled == false
|
|
245
271
|
return true if @warned_unbundled
|
|
246
272
|
|
|
247
273
|
@warned_unbundled = true
|
|
248
274
|
say ''
|
|
249
275
|
say ' Gemfile changed but not bundled, so bin/rails cannot run yet.', :yellow
|
|
250
|
-
say ' Skipping
|
|
251
|
-
say ' bundle install'
|
|
252
|
-
say ' bin/rails bible270:install:migrations'
|
|
253
|
-
say ' bin/rails db:migrate'
|
|
276
|
+
say ' Skipping Active Storage and migration commands until bundle install succeeds.', :yellow
|
|
254
277
|
true
|
|
255
278
|
end
|
|
256
279
|
|
|
@@ -320,10 +343,10 @@ module Bible270
|
|
|
320
343
|
|
|
321
344
|
def ask_mailer_from
|
|
322
345
|
default = "no-reply@#{app_domain}"
|
|
323
|
-
address = prompt('From: address for sign-in emails?', default)
|
|
346
|
+
address = options[:mailer_from] || prompt('From: address for sign-in emails?', default)
|
|
324
347
|
|
|
325
348
|
until valid_email?(address)
|
|
326
|
-
say "
|
|
349
|
+
say " #{address.inspect} doesn't look like an email address.", :red
|
|
327
350
|
address = prompt('From: address for sign-in emails?', default)
|
|
328
351
|
end
|
|
329
352
|
address
|
|
@@ -352,7 +375,15 @@ module Bible270
|
|
|
352
375
|
end
|
|
353
376
|
|
|
354
377
|
def parse_providers(value)
|
|
355
|
-
value.to_s.split(',').map { |
|
|
378
|
+
providers = value.to_s.split(',').map { |provider| provider.strip.downcase.tr('-', '_') }
|
|
379
|
+
.reject(&:empty?).uniq
|
|
380
|
+
invalid = providers.grep_v(PROVIDER_IDENTIFIER)
|
|
381
|
+
if invalid.any?
|
|
382
|
+
say " Ignoring unsafe provider identifier(s): #{invalid.join(', ')}.", :red
|
|
383
|
+
say ' Provider names must start with a letter and contain only lowercase letters, numbers, and underscores.',
|
|
384
|
+
:red
|
|
385
|
+
end
|
|
386
|
+
providers - invalid
|
|
356
387
|
end
|
|
357
388
|
|
|
358
389
|
def app_domain
|
|
@@ -363,7 +394,11 @@ module Bible270
|
|
|
363
394
|
end
|
|
364
395
|
|
|
365
396
|
def providers_literal
|
|
366
|
-
@providers.map { |
|
|
397
|
+
@providers.map { |provider| provider.to_sym.inspect }.join(', ')
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def ruby_literal(value)
|
|
401
|
+
value.to_s.dump
|
|
367
402
|
end
|
|
368
403
|
|
|
369
404
|
def auth_path_prefix
|
|
@@ -382,15 +417,40 @@ module Bible270
|
|
|
382
417
|
path = File.join(destination_root, 'config', 'routes.rb')
|
|
383
418
|
return '' unless File.exist?(path)
|
|
384
419
|
|
|
385
|
-
|
|
386
|
-
|
|
420
|
+
lines = File.readlines(path)
|
|
421
|
+
mount_index = lines.index { |line| line.match?(MOUNT_LINE_PATTERN) }
|
|
422
|
+
catch_all_index = catch_all_route_index(lines)
|
|
423
|
+
return '' unless catch_all_index && (!mount_index || mount_index > catch_all_index)
|
|
387
424
|
|
|
388
|
-
'
|
|
425
|
+
' Move the mount line ABOVE the catch-all route.'
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def move_mount_above_catch_all
|
|
429
|
+
path = File.join(destination_root, 'config', 'routes.rb')
|
|
430
|
+
lines = File.readlines(path)
|
|
431
|
+
mount_index = lines.index { |line| line.match?(MOUNT_LINE_PATTERN) }
|
|
432
|
+
catch_all_index = catch_all_route_index(lines)
|
|
433
|
+
return false unless mount_index && catch_all_index && mount_index > catch_all_index
|
|
434
|
+
|
|
435
|
+
mount_line = lines.delete_at(mount_index)
|
|
436
|
+
lines.insert(catch_all_index, mount_line)
|
|
437
|
+
File.write(path, lines.join)
|
|
438
|
+
true
|
|
439
|
+
rescue StandardError
|
|
440
|
+
false
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def catch_all_route_index(lines)
|
|
444
|
+
lines.index do |line|
|
|
445
|
+
code = line.sub(%r{#.*\z}, '')
|
|
446
|
+
code.match?(%r{\bcomfy_route\s+:cms\b.*\bpath:\s*['"]/['"]}) ||
|
|
447
|
+
code.match?(%r{\b(?:get|match)\s*\(?\s*['"]\*})
|
|
448
|
+
end
|
|
389
449
|
end
|
|
390
450
|
|
|
391
451
|
def routes_mounted?
|
|
392
452
|
path = File.join(destination_root, 'config', 'routes.rb')
|
|
393
|
-
File.exist?(path) && File.
|
|
453
|
+
File.exist?(path) && File.foreach(path).any? { |line| line.match?(MOUNT_LINE_PATTERN) }
|
|
394
454
|
end
|
|
395
455
|
|
|
396
456
|
def report_auth_choice
|
|
@@ -434,17 +494,30 @@ module Bible270
|
|
|
434
494
|
body.include?('path_prefix') && !body.include?('Bible270.config.auth_path_prefix')
|
|
435
495
|
end
|
|
436
496
|
|
|
497
|
+
def migration_summary
|
|
498
|
+
if @migrations_copied
|
|
499
|
+
@migrated ? 'copied and run' : 'copied (not yet run)'
|
|
500
|
+
else
|
|
501
|
+
'not copied (not run)'
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
437
505
|
def active_storage_summary
|
|
438
506
|
case @active_storage
|
|
439
507
|
when :installed then 'Active Storage installed'
|
|
440
508
|
when :present then 'Active Storage already present'
|
|
441
509
|
when :declined then 'off — Active Storage not installed'
|
|
442
510
|
when :unavailable then 'off — Active Storage not enabled in this app'
|
|
511
|
+
when :pending then 'not installed — waiting for bundle install'
|
|
443
512
|
else 'unchanged'
|
|
444
513
|
end
|
|
445
514
|
end
|
|
446
515
|
|
|
447
516
|
def outstanding_steps
|
|
517
|
+
route_and_initializer_steps + installation_steps + provider_setup_steps + mailer_steps
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def route_and_initializer_steps
|
|
448
521
|
steps = []
|
|
449
522
|
steps << "Add to config/routes.rb: #{MOUNT_LINE}" if @route_failed
|
|
450
523
|
if @providers.any? && !initializer_present?('omniauth.rb')
|
|
@@ -453,22 +526,36 @@ module Bible270
|
|
|
453
526
|
if stale_omniauth_prefix?
|
|
454
527
|
steps << "config/initializers/omniauth.rb sets its own path_prefix — make it 'Bible270.config.auth_path_prefix'"
|
|
455
528
|
end
|
|
456
|
-
steps
|
|
457
|
-
|
|
529
|
+
steps
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def installation_steps
|
|
533
|
+
steps = []
|
|
534
|
+
steps << 'bundle install' if @strategy_gems_added && !@bundled
|
|
535
|
+
steps << unknown_provider_gem_step if @unknown_providers&.any?
|
|
536
|
+
steps << 'bin/rails active_storage:install' if @active_storage == :pending
|
|
537
|
+
steps << 'bin/rails bible270:install:migrations' unless @migrations_copied
|
|
458
538
|
steps << 'bin/rails db:migrate' unless @migrated
|
|
459
539
|
if @active_storage == :declined
|
|
460
540
|
steps << 'bin/rails active_storage:install && bin/rails db:migrate — for picture uploads'
|
|
461
541
|
end
|
|
542
|
+
steps
|
|
543
|
+
end
|
|
462
544
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
545
|
+
def unknown_provider_gem_step
|
|
546
|
+
providers = @unknown_providers.map { |provider| provider.to_sym.inspect }.join(', ')
|
|
547
|
+
"Add an OmniAuth strategy gem registering #{providers} to Gemfile, then run bundle install"
|
|
548
|
+
end
|
|
467
549
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
550
|
+
def provider_setup_steps
|
|
551
|
+
return [] if @providers.empty?
|
|
552
|
+
|
|
553
|
+
['Set each provider\'s client id/secret in credentials or ENV'] +
|
|
554
|
+
@providers.map { |provider| "Register callback URL: https://YOUR-HOST#{auth_path_prefix}/#{provider}/callback" }
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def mailer_steps
|
|
558
|
+
@email ? ['Confirm Action Mailer can deliver mail in this environment'] : []
|
|
472
559
|
end
|
|
473
560
|
|
|
474
561
|
# ---- file bodies ------------------------------------------------------
|
|
@@ -489,7 +576,7 @@ module Bible270
|
|
|
489
576
|
# Change this one value to move the plan. config/routes.rb and
|
|
490
577
|
# config/initializers/omniauth.rb both read it, so nothing else needs editing
|
|
491
578
|
# (but do update the callback URLs registered with each OAuth provider).
|
|
492
|
-
config.mount_at =
|
|
579
|
+
config.mount_at = #{ruby_literal(@mount_at)}
|
|
493
580
|
|
|
494
581
|
config.app_name = 'Daily Bread'
|
|
495
582
|
config.tagline = 'A 270-day journey through Scripture'
|
|
@@ -532,7 +619,7 @@ module Bible270
|
|
|
532
619
|
# Passwordless email sign-in: a one-time link, no password, no third-party
|
|
533
620
|
# account. This is what lets anyone take part. Needs Action Mailer delivery.
|
|
534
621
|
config.email_sign_in = true
|
|
535
|
-
config.mailer_from =
|
|
622
|
+
config.mailer_from = #{ruby_literal(@mailer_from)}
|
|
536
623
|
config.email_sign_in_ask_name = true
|
|
537
624
|
# config.email_sign_in_ttl = 20 * 60 # link lifetime, seconds
|
|
538
625
|
# config.email_sign_in_max_per_window = 5 # per address, per window
|
|
@@ -607,11 +694,12 @@ module Bible270
|
|
|
607
694
|
end
|
|
608
695
|
|
|
609
696
|
def provider_line(provider)
|
|
697
|
+
provider_symbol = provider.to_sym.inspect
|
|
610
698
|
env = provider.upcase
|
|
611
699
|
<<~RUBY.chomp
|
|
612
|
-
provider
|
|
613
|
-
Rails.application.credentials.dig(
|
|
614
|
-
Rails.application.credentials.dig(
|
|
700
|
+
provider #{provider_symbol},
|
|
701
|
+
Rails.application.credentials.dig(#{provider_symbol}, :client_id) || ENV.fetch(#{ruby_literal("#{env}_CLIENT_ID")}, nil),
|
|
702
|
+
Rails.application.credentials.dig(#{provider_symbol}, :client_secret) || ENV.fetch(#{ruby_literal("#{env}_CLIENT_SECRET")}, nil)
|
|
615
703
|
RUBY
|
|
616
704
|
end
|
|
617
705
|
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: 1.4.
|
|
4
|
+
version: 1.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew vonderLuft
|
|
@@ -139,7 +139,6 @@ files:
|
|
|
139
139
|
- app/views/bible270/shared/_comment_drafts.html.erb
|
|
140
140
|
- app/views/bible270/shared/_comment_notification_choices.html.erb
|
|
141
141
|
- app/views/bible270/shared/_day_completion_dove.html.erb
|
|
142
|
-
- app/views/bible270/shared/_day_completion_effect.html.erb
|
|
143
142
|
- app/views/bible270/shared/_day_index.html.erb
|
|
144
143
|
- app/views/bible270/shared/_footer.html.erb
|
|
145
144
|
- app/views/bible270/shared/_grid_legend.html.erb
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
<%# locals: (day:) %>
|
|
2
|
-
<template data-b270-completion-dove-template data-b270-day="<%= day %>">
|
|
3
|
-
<%= render "bible270/shared/day_completion_dove", day: day %>
|
|
4
|
-
</template>
|
|
5
|
-
<script nonce="<%= content_security_policy_nonce %>">
|
|
6
|
-
(() => {
|
|
7
|
-
if (window.Bible270DayCompletionEffect) return;
|
|
8
|
-
|
|
9
|
-
function willCompleteDay(form) {
|
|
10
|
-
const desiredState = form.querySelector("input[name='checked']");
|
|
11
|
-
if (!desiredState || desiredState.value !== "1") return false;
|
|
12
|
-
|
|
13
|
-
const unfinished = Array.from(
|
|
14
|
-
document.querySelectorAll("form[data-b270-checkoff='true'] input[name='checked'][value='1']")
|
|
15
|
-
);
|
|
16
|
-
return unfinished.length === 1 && unfinished[0].form === form;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function startDove(day) {
|
|
20
|
-
if (document.querySelector("[data-b270-completion-dove][data-b270-immediate]")) return;
|
|
21
|
-
|
|
22
|
-
const template = document.querySelector("[data-b270-completion-dove-template]");
|
|
23
|
-
if (!template || template.dataset.b270Day !== day) return;
|
|
24
|
-
|
|
25
|
-
const dove = template.content.querySelector("[data-b270-completion-dove]").cloneNode(true);
|
|
26
|
-
dove.dataset.b270Immediate = "true";
|
|
27
|
-
document.body.append(dove);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function removeServerDuplicate() {
|
|
31
|
-
const immediate = document.querySelector("[data-b270-completion-dove][data-b270-immediate]");
|
|
32
|
-
if (!immediate) return;
|
|
33
|
-
|
|
34
|
-
document.querySelectorAll("[data-b270-completion-dove]").forEach((dove) => {
|
|
35
|
-
if (dove !== immediate) dove.remove();
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
document.addEventListener("submit", (event) => {
|
|
40
|
-
const form = event.target.closest("form[data-b270-checkoff='true']");
|
|
41
|
-
const check = form && form.querySelector(".b270-check");
|
|
42
|
-
if (!check) return;
|
|
43
|
-
|
|
44
|
-
const bounds = check.getBoundingClientRect();
|
|
45
|
-
document.documentElement.style.setProperty(
|
|
46
|
-
"--b270-dove-origin-x",
|
|
47
|
-
`${bounds.left + bounds.width / 2}px`
|
|
48
|
-
);
|
|
49
|
-
document.documentElement.style.setProperty(
|
|
50
|
-
"--b270-dove-origin-y",
|
|
51
|
-
`${bounds.top + bounds.height / 2}px`
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
if (willCompleteDay(form)) startDove(form.action.match(/\/day\/(\d+)/)?.[1] || "");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
document.addEventListener("animationend", (event) => {
|
|
58
|
-
if (event.target.matches("[data-b270-completion-dove]")) event.target.remove();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
new MutationObserver(removeServerDuplicate).observe(document.documentElement, {
|
|
62
|
-
childList: true,
|
|
63
|
-
subtree: true
|
|
64
|
-
});
|
|
65
|
-
window.Bible270DayCompletionEffect = true;
|
|
66
|
-
})();
|
|
67
|
-
</script>
|