bible270 0.6.3 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +150 -164
- data/README.md +287 -272
- data/Rakefile +5 -4
- data/app/controllers/bible270/admin_controller.rb +119 -0
- data/app/controllers/bible270/application_controller.rb +19 -2
- data/app/controllers/bible270/checkoffs_controller.rb +2 -3
- data/app/controllers/bible270/comments_controller.rb +6 -2
- data/app/controllers/bible270/days_controller.rb +5 -6
- data/app/controllers/bible270/readers_controller.rb +5 -4
- data/app/controllers/bible270/sessions_controller.rb +60 -26
- data/app/helpers/bible270/plan_helper.rb +11 -8
- data/app/mailers/bible270/application_mailer.rb +4 -1
- data/app/mailers/bible270/sign_in_mailer.rb +1 -0
- data/app/models/bible270/application_record.rb +1 -0
- data/app/models/bible270/checkoff.rb +4 -3
- data/app/models/bible270/comment.rb +26 -5
- data/app/models/bible270/reader.rb +78 -14
- data/app/models/bible270/sign_in_token.rb +5 -2
- data/app/views/bible270/admin/comments.html.erb +44 -0
- data/app/views/bible270/admin/index.html.erb +30 -0
- data/app/views/bible270/admin/show.html.erb +89 -0
- data/app/views/bible270/days/index.html.erb +10 -13
- data/app/views/bible270/readers/index.html.erb +2 -2
- data/app/views/bible270/readers/show.html.erb +5 -5
- data/app/views/bible270/sessions/new.html.erb +9 -3
- data/app/views/bible270/shared/_header.html.erb +1 -0
- data/app/views/bible270/shared/_styles.html.erb +10 -7
- data/app/views/layouts/bible270/application.html.erb +4 -3
- data/config/routes.rb +32 -18
- data/db/migrate/20260101000001_create_bible270_readers.rb +3 -2
- data/db/migrate/20260101000002_create_bible270_checkoffs.rb +3 -2
- data/db/migrate/20260101000003_create_bible270_comments.rb +4 -3
- data/db/migrate/20260101000004_create_bible270_sign_in_tokens.rb +4 -3
- data/db/migrate/20260101000005_add_names_to_bible270_readers.rb +12 -0
- data/db/migrate/20260101000006_add_approval_to_bible270_comments.rb +11 -0
- data/lib/bible270/configuration.rb +91 -19
- data/lib/bible270/email_sign_in.rb +7 -7
- data/lib/bible270/engine.rb +1 -0
- data/lib/bible270/plan.rb +449 -150
- data/lib/bible270/versification.rb +84 -67
- data/lib/bible270/version.rb +2 -1
- data/lib/bible270.rb +6 -5
- data/lib/generators/bible270/install/install_generator.rb +520 -39
- data/lib/generators/bible270/install/templates/bible270.rb.tt +8 -2
- data/lib/generators/bible270/install/templates/omniauth.rb.tt +6 -4
- metadata +17 -29
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
class CreateBible270Readers < ActiveRecord::Migration[7.0]
|
|
3
4
|
def change
|
|
4
5
|
create_table :bible270_readers do |t|
|
|
@@ -8,10 +9,10 @@ class CreateBible270Readers < ActiveRecord::Migration[7.0]
|
|
|
8
9
|
t.string :provider
|
|
9
10
|
t.string :uid
|
|
10
11
|
t.references :owner, polymorphic: true, index: true
|
|
11
|
-
t.date
|
|
12
|
+
t.date :started_on
|
|
12
13
|
t.timestamps
|
|
13
14
|
end
|
|
14
15
|
add_index :bible270_readers, %i[provider uid], unique: true,
|
|
15
|
-
|
|
16
|
+
name: 'idx_b270_readers_provider_uid'
|
|
16
17
|
end
|
|
17
18
|
end
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
class CreateBible270Checkoffs < ActiveRecord::Migration[7.0]
|
|
3
4
|
def change
|
|
4
5
|
create_table :bible270_checkoffs do |t|
|
|
5
6
|
t.references :reader, null: false, index: true,
|
|
6
|
-
|
|
7
|
+
foreign_key: { to_table: :bible270_readers }
|
|
7
8
|
t.integer :day, null: false
|
|
8
9
|
t.string :track, null: false
|
|
9
10
|
t.timestamps
|
|
10
11
|
end
|
|
11
12
|
add_index :bible270_checkoffs, %i[reader_id day track], unique: true,
|
|
12
|
-
|
|
13
|
+
name: 'idx_b270_checkoffs_unique'
|
|
13
14
|
add_index :bible270_checkoffs, :day
|
|
14
15
|
end
|
|
15
16
|
end
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
class CreateBible270Comments < ActiveRecord::Migration[7.0]
|
|
3
4
|
def change
|
|
4
5
|
create_table :bible270_comments do |t|
|
|
5
6
|
t.references :reader, null: false, index: true,
|
|
6
|
-
|
|
7
|
-
t.integer :day,
|
|
7
|
+
foreign_key: { to_table: :bible270_readers }
|
|
8
|
+
t.integer :day, null: false
|
|
8
9
|
t.string :track
|
|
9
|
-
t.text :body,
|
|
10
|
+
t.text :body, null: false
|
|
10
11
|
t.timestamps
|
|
11
12
|
end
|
|
12
13
|
add_index :bible270_comments, %i[day created_at]
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
class CreateBible270SignInTokens < ActiveRecord::Migration[7.0]
|
|
3
4
|
def change
|
|
4
5
|
create_table :bible270_sign_in_tokens do |t|
|
|
5
6
|
t.string :email, null: false
|
|
6
7
|
t.string :token_digest, null: false
|
|
7
8
|
t.string :display_name
|
|
8
|
-
t.datetime :expires_at,
|
|
9
|
+
t.datetime :expires_at, null: false
|
|
9
10
|
t.datetime :consumed_at
|
|
10
11
|
t.timestamps
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
add_index :bible270_sign_in_tokens, :token_digest, unique: true,
|
|
14
|
-
|
|
15
|
+
name: 'idx_b270_signin_tokens_digest'
|
|
15
16
|
add_index :bible270_sign_in_tokens, %i[email created_at],
|
|
16
|
-
name:
|
|
17
|
+
name: 'idx_b270_signin_tokens_email'
|
|
17
18
|
end
|
|
18
19
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddNamesToBible270Readers < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
add_column :bible270_readers, :first_name, :string
|
|
6
|
+
add_column :bible270_readers, :last_name, :string
|
|
7
|
+
add_column :bible270_sign_in_tokens, :first_name, :string
|
|
8
|
+
add_column :bible270_sign_in_tokens, :last_name, :string
|
|
9
|
+
|
|
10
|
+
add_index :bible270_readers, %i[last_name first_name], name: 'idx_b270_readers_name'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddApprovalToBible270Comments < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
# Reflections are visible as soon as they are written; moderation is for
|
|
6
|
+
# taking something down after the fact, not for gatekeeping every post.
|
|
7
|
+
add_column :bible270_comments, :approved, :boolean, null: false, default: true
|
|
8
|
+
add_column :bible270_comments, :moderated_at, :datetime
|
|
9
|
+
add_index :bible270_comments, %i[approved day]
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# here: the CGI library was removed from Ruby's default gems in Ruby 4.0 (only
|
|
5
5
|
# cgi/escape remains), so requiring it emits a deprecation warning there.
|
|
6
6
|
# URI.encode_www_form_component produces byte-identical output to CGI.escape.
|
|
7
|
-
require
|
|
8
|
-
require
|
|
7
|
+
require 'uri'
|
|
8
|
+
require 'bible270/plan'
|
|
9
9
|
|
|
10
10
|
module Bible270
|
|
11
11
|
class Configuration
|
|
@@ -27,9 +27,36 @@ module Bible270
|
|
|
27
27
|
# Leave nil to use the built-in session-based sign-in (OmniAuth).
|
|
28
28
|
attr_accessor :current_reader_resolver
|
|
29
29
|
|
|
30
|
+
# Where the engine is mounted in the host application. Set this once, in
|
|
31
|
+
# config/initializers/bible270.rb, and use it everywhere the path is needed:
|
|
32
|
+
#
|
|
33
|
+
# # config/routes.rb
|
|
34
|
+
# mount Bible270::Engine, at: Bible270.config.mount_at
|
|
35
|
+
#
|
|
36
|
+
# # config/initializers/omniauth.rb
|
|
37
|
+
# path_prefix Bible270.config.auth_path_prefix
|
|
38
|
+
#
|
|
39
|
+
# Accepts "daily-bread" or "/daily-bread"; stored with a leading slash and
|
|
40
|
+
# no trailing one.
|
|
41
|
+
attr_reader :mount_at
|
|
42
|
+
|
|
43
|
+
def mount_at=(value)
|
|
44
|
+
path = value.to_s.strip
|
|
45
|
+
path = "/#{path}" unless path.start_with?('/')
|
|
46
|
+
path = path.chomp('/')
|
|
47
|
+
@mount_at = path.empty? ? '/' : path
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The prefix OmniAuth's middleware should serve its routes under. Derived
|
|
51
|
+
# from mount_at unless omniauth_path_prefix is set explicitly.
|
|
52
|
+
def auth_path_prefix
|
|
53
|
+
return omniauth_path_prefix if omniauth_path_prefix
|
|
54
|
+
|
|
55
|
+
mount_at == '/' ? '/auth' : "#{mount_at}/auth"
|
|
56
|
+
end
|
|
57
|
+
|
|
30
58
|
# Where to send the reader after a successful sign-in / sign-out.
|
|
31
59
|
attr_accessor :after_sign_in_path
|
|
32
|
-
attr_accessor :after_sign_out_path
|
|
33
60
|
|
|
34
61
|
# ---- OmniAuth (built-in sign-in) --------------------------------------
|
|
35
62
|
|
|
@@ -50,17 +77,17 @@ module Bible270
|
|
|
50
77
|
|
|
51
78
|
# Human labels for providers we know; anything else is titleised.
|
|
52
79
|
PROVIDER_LABELS = {
|
|
53
|
-
github:
|
|
54
|
-
facebook:
|
|
55
|
-
microsoft_graph:
|
|
56
|
-
openid_connect:
|
|
80
|
+
github: 'GitHub', gitlab: 'GitLab', google_oauth2: 'Google', google: 'Google',
|
|
81
|
+
facebook: 'Facebook', twitter: 'Twitter', apple: 'Apple', discord: 'Discord',
|
|
82
|
+
microsoft_graph: 'Microsoft', azure_activedirectory_v2: 'Microsoft',
|
|
83
|
+
openid_connect: 'OpenID Connect', saml: 'SSO'
|
|
57
84
|
}.freeze
|
|
58
85
|
|
|
59
86
|
def omniauth_providers=(list)
|
|
60
87
|
@omniauth_providers = Array(list).map do |entry|
|
|
61
88
|
key, label = entry.is_a?(Array) ? entry : [entry, nil]
|
|
62
89
|
key = key.to_sym
|
|
63
|
-
[key,
|
|
90
|
+
[key, label || PROVIDER_LABELS[key] || key.to_s.tr('_', ' ').split.map(&:capitalize).join(' ')]
|
|
64
91
|
end
|
|
65
92
|
end
|
|
66
93
|
|
|
@@ -81,12 +108,52 @@ module Bible270
|
|
|
81
108
|
|
|
82
109
|
# Simple abuse guard: at most N links per address per window (seconds).
|
|
83
110
|
attr_accessor :email_sign_in_window
|
|
84
|
-
attr_accessor :email_sign_in_max_per_window
|
|
111
|
+
attr_accessor :after_sign_out_path, :email_sign_in_max_per_window, :passage_url_builder, :tagline, :admin_emails, :admin_resolver
|
|
85
112
|
|
|
86
113
|
# Whether a reader may type the display name shown beside their reflections
|
|
87
114
|
# when signing in by email (otherwise it's derived from the address).
|
|
88
115
|
attr_accessor :email_sign_in_ask_name
|
|
89
116
|
|
|
117
|
+
# Write the sign-in link to the log. nil (default) means development and test
|
|
118
|
+
# only; true forces it on, for smoke-testing a production build locally
|
|
119
|
+
# before mail is wired up; false disables it everywhere. The link is a bearer
|
|
120
|
+
# credential, so true is not for a deployment real users can reach.
|
|
121
|
+
attr_accessor :email_sign_in_log_link
|
|
122
|
+
|
|
123
|
+
# Require a first AND last name when signing in by email. Names identify
|
|
124
|
+
# people to each other beside their reflections, so this is on by default.
|
|
125
|
+
attr_accessor :email_sign_in_require_name
|
|
126
|
+
|
|
127
|
+
# Who may reach the admin panel. Either a list of email addresses, or a
|
|
128
|
+
# lambda taking the current reader and returning true/false:
|
|
129
|
+
#
|
|
130
|
+
# config.admin_emails = %w[andrew@example.org]
|
|
131
|
+
# config.admin_resolver = ->(reader) { reader.email.end_with?('@example.org') }
|
|
132
|
+
#
|
|
133
|
+
# With neither set the panel is unreachable and its routes 404.
|
|
134
|
+
# Break points set from the host app, overriding Plan::CHAPTER_BREAKS.
|
|
135
|
+
# Keys may be ['Psalm', 18] or 'Psalm 18'; [] keeps a chapter whole.
|
|
136
|
+
attr_accessor :chapter_breaks
|
|
137
|
+
|
|
138
|
+
# Optional YAML file holding the same thing, re-read whenever it changes so
|
|
139
|
+
# breaks can be tuned without restarting:
|
|
140
|
+
#
|
|
141
|
+
# # config/bible270_breaks.yml
|
|
142
|
+
# Psalm 35: []
|
|
143
|
+
# Psalm 78: [20, 39, 55]
|
|
144
|
+
attr_accessor :chapter_breaks_path
|
|
145
|
+
|
|
146
|
+
def admin?(reader)
|
|
147
|
+
return false if reader.nil?
|
|
148
|
+
return !!admin_resolver.call(reader) if admin_resolver.respond_to?(:call)
|
|
149
|
+
|
|
150
|
+
Array(admin_emails).map { |e| e.to_s.downcase }.include?(reader.email.to_s.downcase)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def admin_configured?
|
|
154
|
+
admin_resolver.respond_to?(:call) || Array(admin_emails).any?
|
|
155
|
+
end
|
|
156
|
+
|
|
90
157
|
# Send the sign-in email via Active Job (deliver_later) instead of inline.
|
|
91
158
|
# Leave false unless you have a queue backend configured.
|
|
92
159
|
attr_accessor :email_sign_in_deliver_later
|
|
@@ -104,16 +171,14 @@ module Bible270
|
|
|
104
171
|
found = omniauth_providers.find { |k, _| k.to_s == key.to_s }
|
|
105
172
|
return found.last if found
|
|
106
173
|
|
|
107
|
-
PROVIDER_LABELS[key.to_sym] || key.to_s.tr(
|
|
174
|
+
PROVIDER_LABELS[key.to_sym] || key.to_s.tr('_', ' ').split.map(&:capitalize).join(' ')
|
|
108
175
|
end
|
|
109
176
|
|
|
110
177
|
# Default Bible translation and a builder for external passage links.
|
|
111
178
|
attr_accessor :bible_version
|
|
112
|
-
attr_accessor :passage_url_builder
|
|
113
179
|
|
|
114
180
|
# Public labels.
|
|
115
181
|
attr_accessor :app_name
|
|
116
|
-
attr_accessor :tagline
|
|
117
182
|
|
|
118
183
|
# Optional community-wide start date for the plan. Set this when everyone
|
|
119
184
|
# reads together as a cohort (e.g. a church starting on a set Sunday):
|
|
@@ -132,30 +197,37 @@ module Bible270
|
|
|
132
197
|
attr_accessor :require_sign_in_to_participate
|
|
133
198
|
|
|
134
199
|
def initialize
|
|
135
|
-
@parent_controller =
|
|
136
|
-
@layout =
|
|
200
|
+
@parent_controller = 'ActionController::Base'
|
|
201
|
+
@layout = 'bible270/application'
|
|
137
202
|
@current_reader_resolver = nil
|
|
138
203
|
@after_sign_in_path = nil
|
|
139
204
|
@after_sign_out_path = nil
|
|
140
|
-
@bible_version =
|
|
141
|
-
@passage_url_builder =
|
|
205
|
+
@bible_version = 'NKJV'
|
|
206
|
+
@passage_url_builder = ->(reference, version) do
|
|
142
207
|
search = URI.encode_www_form_component(reference)
|
|
143
208
|
version = URI.encode_www_form_component(version)
|
|
144
209
|
"https://www.biblegateway.com/passage/?search=#{search}&version=#{version}"
|
|
145
210
|
end
|
|
146
|
-
@app_name =
|
|
147
|
-
@tagline =
|
|
211
|
+
@app_name = 'Daily Bread'
|
|
212
|
+
@tagline = 'Journeying through Scripture in 9 months'
|
|
148
213
|
@require_sign_in_to_participate = true
|
|
214
|
+
self.mount_at = '/daily-bread'
|
|
149
215
|
@start_date = nil
|
|
150
216
|
@allow_reader_start_date = true
|
|
151
217
|
self.omniauth_providers = [:github]
|
|
152
218
|
@omniauth_path_prefix = nil
|
|
153
219
|
@email_sign_in = true
|
|
154
|
-
@mailer_from =
|
|
220
|
+
@mailer_from = 'no-reply@example.com'
|
|
155
221
|
@email_sign_in_ttl = 20 * 60 # 20 minutes
|
|
156
222
|
@email_sign_in_window = 15 * 60 # 15 minutes
|
|
157
223
|
@email_sign_in_max_per_window = 5
|
|
158
224
|
@email_sign_in_ask_name = true
|
|
225
|
+
@email_sign_in_require_name = true
|
|
226
|
+
@email_sign_in_log_link = nil
|
|
227
|
+
@chapter_breaks = {}
|
|
228
|
+
@chapter_breaks_path = nil
|
|
229
|
+
@admin_emails = []
|
|
230
|
+
@admin_resolver = nil
|
|
159
231
|
@email_sign_in_deliver_later = false
|
|
160
232
|
end
|
|
161
233
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'digest'
|
|
5
5
|
|
|
6
6
|
module Bible270
|
|
7
7
|
# Pure helpers for passwordless ("magic link") email sign-in. Kept free of
|
|
@@ -11,14 +11,14 @@ module Bible270
|
|
|
11
11
|
# digest of it, so a leaked database can't be used to sign in.
|
|
12
12
|
module EmailSignIn
|
|
13
13
|
# Deliberately permissive: real validation is "did the link get clicked".
|
|
14
|
-
EMAIL_RE =
|
|
14
|
+
EMAIL_RE = %r{\A[^@\s,;:<>"]+@[^@\s,;:<>"]+\.[A-Za-z]{2,}\z}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
module_function
|
|
17
17
|
|
|
18
18
|
# Trim, strip a mailto:, and downcase. Returns nil for anything unusable.
|
|
19
19
|
def normalize_email(value)
|
|
20
20
|
email = value.to_s.strip
|
|
21
|
-
email = email[7..] if email.downcase.start_with?(
|
|
21
|
+
email = email[7..] if email.downcase.start_with?('mailto:')
|
|
22
22
|
email = email.strip.downcase
|
|
23
23
|
return nil if email.empty?
|
|
24
24
|
return nil unless email.match?(EMAIL_RE)
|
|
@@ -58,11 +58,11 @@ module Bible270
|
|
|
58
58
|
# The part of an address we can use as a default display name:
|
|
59
59
|
# "mary.anne.smith@example.com" => "Mary Anne Smith"
|
|
60
60
|
def display_name_from(email)
|
|
61
|
-
local = normalize_email(email)&.split(
|
|
61
|
+
local = normalize_email(email)&.split('@')&.first
|
|
62
62
|
return nil if local.nil? || local.empty?
|
|
63
63
|
|
|
64
64
|
# NB: the hyphen must come last or tr reads "_-+" as a range.
|
|
65
|
-
local.tr(
|
|
65
|
+
local.tr('._+-', ' ').split.map { |w| w[0].upcase + (w[1..] || '') }.join(' ')
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
end
|