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
data/Rakefile
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks' # provides build / install / release
|
|
4
|
+
require 'rake/testtask'
|
|
4
5
|
|
|
5
6
|
Rake::TestTask.new(:test) do |t|
|
|
6
|
-
t.libs <<
|
|
7
|
-
t.pattern =
|
|
7
|
+
t.libs << 'test' << 'lib'
|
|
8
|
+
t.pattern = 'test/**/*_test.rb'
|
|
8
9
|
t.warning = false
|
|
9
10
|
end
|
|
10
11
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bible270
|
|
4
|
+
# Small administrative panel: remove readers, adjust their completions, and
|
|
5
|
+
# move them to a given day of the plan.
|
|
6
|
+
#
|
|
7
|
+
# Access is controlled by Bible270.config.admin_emails / admin_resolver. With
|
|
8
|
+
# neither set the panel is unreachable, and every action 404s rather than 403s
|
|
9
|
+
# so its existence isn't advertised.
|
|
10
|
+
class AdminController < ApplicationController
|
|
11
|
+
before_action :require_admin!
|
|
12
|
+
before_action :load_reader, only: %i[show destroy update_start complete_through toggle_day]
|
|
13
|
+
before_action :load_comment, only: %i[hide_comment unhide_comment destroy_comment]
|
|
14
|
+
|
|
15
|
+
def index
|
|
16
|
+
@readers = Reader.all.to_a.sort_by(&:sort_name)
|
|
17
|
+
counts = Checkoff.group(:reader_id, :day).count
|
|
18
|
+
@days_completed = Hash.new(0)
|
|
19
|
+
counts.each { |(rid, day), n| @days_completed[rid] += 1 if n >= Plan.required_track_count(day) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def show
|
|
23
|
+
@days_completed = @reader.days_completed
|
|
24
|
+
@comments = @reader.comments.order(created_at: :desc).limit(50)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# ---- reflections ------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
# Everything is visible by default; this lists it all so anything unsuitable
|
|
30
|
+
# can be taken down.
|
|
31
|
+
def comments
|
|
32
|
+
scope = Comment.includes(:reader).order(created_at: :desc)
|
|
33
|
+
@filter = params[:filter].to_s
|
|
34
|
+
scope = scope.hidden if @filter == 'hidden'
|
|
35
|
+
scope = scope.approved if @filter == 'visible'
|
|
36
|
+
@comments = scope.limit(200)
|
|
37
|
+
@hidden_count = Comment.hidden.count
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def hide_comment
|
|
41
|
+
@comment.hide!
|
|
42
|
+
redirect_back_to_comments "Hidden #{@comment.reader.display_name}'s reflection on day #{@comment.day}."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def unhide_comment
|
|
46
|
+
@comment.unhide!
|
|
47
|
+
redirect_back_to_comments "Restored #{@comment.reader.display_name}'s reflection on day #{@comment.day}."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def destroy_comment
|
|
51
|
+
day = @comment.day
|
|
52
|
+
@comment.destroy
|
|
53
|
+
redirect_back_to_comments "Deleted the reflection on day #{day}."
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def destroy
|
|
57
|
+
name = @reader.display_name
|
|
58
|
+
@reader.destroy
|
|
59
|
+
redirect_to admin_path, notice: "Removed #{name} and all their check-offs and reflections."
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Put the reader on a given day as of today, or set an explicit start date.
|
|
63
|
+
def update_start
|
|
64
|
+
if params[:start_date].present?
|
|
65
|
+
if @reader.update_start_date!(params[:start_date])
|
|
66
|
+
redirect_to admin_reader_path(@reader), notice: 'Start date updated.'
|
|
67
|
+
else
|
|
68
|
+
redirect_to admin_reader_path(@reader), alert: "That doesn't look like a date."
|
|
69
|
+
end
|
|
70
|
+
elsif params[:day].present? && @reader.restart_on!(day: params[:day])
|
|
71
|
+
redirect_to admin_reader_path(@reader),
|
|
72
|
+
notice: "#{@reader.display_name} is now on day #{params[:day]}."
|
|
73
|
+
else
|
|
74
|
+
redirect_to admin_reader_path(@reader), alert: 'Give a start date or a day between 1 and 270.'
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def complete_through
|
|
79
|
+
day = params[:day].to_i
|
|
80
|
+
if @reader.mark_through!(day)
|
|
81
|
+
redirect_to admin_reader_path(@reader),
|
|
82
|
+
notice: day.zero? ? 'All check-offs cleared.' : "Marked days 1-#{day} complete."
|
|
83
|
+
else
|
|
84
|
+
redirect_to admin_reader_path(@reader), alert: 'Give a day between 0 and 270.'
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def toggle_day
|
|
89
|
+
day = params[:day].to_i
|
|
90
|
+
if @reader.toggle_day!(day)
|
|
91
|
+
redirect_to admin_reader_path(@reader, anchor: "day-#{day}")
|
|
92
|
+
else
|
|
93
|
+
redirect_to admin_reader_path(@reader), alert: 'That day is outside the plan.'
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def require_admin!
|
|
100
|
+
return if Bible270.config.admin_configured? && Bible270.config.admin?(current_reader)
|
|
101
|
+
|
|
102
|
+
raise ActionController::RoutingError, 'Not Found'
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def load_comment
|
|
106
|
+
@comment = Comment.find_by(id: params[:id])
|
|
107
|
+
redirect_to(admin_comments_path, alert: 'Reflection not found.') if @comment.nil?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def redirect_back_to_comments(notice)
|
|
111
|
+
redirect_to(params[:return_to].presence || admin_comments_path, notice: notice)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def load_reader
|
|
115
|
+
@reader = Reader.find_by(id: params[:id])
|
|
116
|
+
redirect_to(admin_path, alert: 'Reader not found.') if @reader.nil?
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Bible270
|
|
3
4
|
class ApplicationController < Bible270.config.parent_controller.constantize
|
|
5
|
+
# Rails only auto-includes an isolated engine's helpers when the controller's
|
|
6
|
+
# superclass is *exactly* ActionController::Base:
|
|
7
|
+
#
|
|
8
|
+
# # ActionController::Railties::Helpers#inherited
|
|
9
|
+
# klass.helpers_path = namespace.railtie_helpers_paths
|
|
10
|
+
# klass.helper :all if klass.superclass == ActionController::Base && ...
|
|
11
|
+
#
|
|
12
|
+
# config.parent_controller normally points at the host's ApplicationController,
|
|
13
|
+
# so that check fails and the b270_* helpers are never mixed in — every view
|
|
14
|
+
# calling one then raises NoMethodError. helpers_path is still set to this
|
|
15
|
+
# engine's app/helpers, so asking for them explicitly is all that's needed.
|
|
16
|
+
helper :all
|
|
17
|
+
# ...and name it outright, in case helpers_path resolves to the host app's
|
|
18
|
+
# helpers rather than this engine's.
|
|
19
|
+
helper Bible270::PlanHelper
|
|
20
|
+
|
|
4
21
|
layout :bible270_layout
|
|
5
22
|
|
|
6
23
|
helper_method :current_reader, :signed_in?, :b270_config
|
|
7
24
|
|
|
8
|
-
|
|
25
|
+
private
|
|
9
26
|
|
|
10
27
|
def bible270_layout
|
|
11
28
|
Bible270.config.layout
|
|
@@ -39,7 +56,7 @@ module Bible270
|
|
|
39
56
|
format.turbo_stream { head :unauthorized }
|
|
40
57
|
format.html do
|
|
41
58
|
redirect_to sign_in_path(origin: request.fullpath),
|
|
42
|
-
alert:
|
|
59
|
+
alert: 'Please sign in to take part.'
|
|
43
60
|
end
|
|
44
61
|
end
|
|
45
62
|
false
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Bible270
|
|
3
4
|
class CheckoffsController < ApplicationController
|
|
4
5
|
def toggle
|
|
@@ -7,9 +8,7 @@ module Bible270
|
|
|
7
8
|
@day = params[:day].to_i
|
|
8
9
|
@track = params[:track].to_s
|
|
9
10
|
|
|
10
|
-
unless Plan.valid_day?(@day) && Plan.present_tracks(@day).include?(@track)
|
|
11
|
-
head :bad_request and return
|
|
12
|
-
end
|
|
11
|
+
head :bad_request and return unless Plan.valid_day?(@day) && Plan.present_tracks(@day).include?(@track)
|
|
13
12
|
|
|
14
13
|
existing = current_reader.checkoffs.find_by(day: @day, track: @track)
|
|
15
14
|
if existing
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Bible270
|
|
3
4
|
class CommentsController < ApplicationController
|
|
4
5
|
def create
|
|
@@ -17,7 +18,10 @@ module Bible270
|
|
|
17
18
|
end
|
|
18
19
|
else
|
|
19
20
|
respond_to do |format|
|
|
20
|
-
format.turbo_stream
|
|
21
|
+
format.turbo_stream do
|
|
22
|
+
render turbo_stream: turbo_stream.replace('new_comment_form', partial: 'bible270/comments/form',
|
|
23
|
+
locals: { day: @day, comment: @comment })
|
|
24
|
+
end
|
|
21
25
|
format.html { redirect_to day_path(@day), alert: @comment.errors.full_messages.to_sentence }
|
|
22
26
|
end
|
|
23
27
|
end
|
|
@@ -37,7 +41,7 @@ module Bible270
|
|
|
37
41
|
end
|
|
38
42
|
end
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
private
|
|
41
45
|
|
|
42
46
|
def comment_params
|
|
43
47
|
params.require(:comment).permit(:body, :track)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Bible270
|
|
3
4
|
class DaysController < ApplicationController
|
|
4
5
|
def index
|
|
@@ -19,9 +20,7 @@ module Bible270
|
|
|
19
20
|
|
|
20
21
|
def show
|
|
21
22
|
@day = params[:day].to_i
|
|
22
|
-
unless Plan.valid_day?(@day)
|
|
23
|
-
redirect_to(root_path, alert: "That day is outside the plan.") and return
|
|
24
|
-
end
|
|
23
|
+
redirect_to(root_path, alert: 'That day is outside the plan.') and return unless Plan.valid_day?(@day)
|
|
25
24
|
|
|
26
25
|
@readings = Plan.readings_for(@day)
|
|
27
26
|
@comments = Comment.for_day(@day).includes(:reader)
|
|
@@ -30,9 +29,9 @@ module Bible270
|
|
|
30
29
|
|
|
31
30
|
req = Plan.required_track_count(@day)
|
|
32
31
|
completer_ids = Checkoff.where(day: @day)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
.group(:reader_id)
|
|
33
|
+
.having('COUNT(*) >= ?', req)
|
|
34
|
+
.pluck(:reader_id)
|
|
36
35
|
@completers = Reader.where(id: completer_ids).order(:display_name)
|
|
37
36
|
|
|
38
37
|
@prev_day = @day > 1 ? @day - 1 : nil
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Bible270
|
|
3
4
|
class ReadersController < ApplicationController
|
|
4
5
|
def index
|
|
@@ -11,7 +12,7 @@ module Bible270
|
|
|
11
12
|
|
|
12
13
|
def show
|
|
13
14
|
@reader = Reader.find_by(id: params[:id])
|
|
14
|
-
redirect_to(community_path, alert:
|
|
15
|
+
redirect_to(community_path, alert: 'Reader not found.') and return unless @reader
|
|
15
16
|
|
|
16
17
|
@days_completed = @reader.days_completed
|
|
17
18
|
@recent_comments = @reader.comments.recent.limit(20)
|
|
@@ -21,7 +22,7 @@ module Bible270
|
|
|
21
22
|
return unless require_reader!
|
|
22
23
|
|
|
23
24
|
unless Bible270.config.allow_reader_start_date
|
|
24
|
-
redirect_to(root_path, alert:
|
|
25
|
+
redirect_to(root_path, alert: 'The start date is set for the whole community.') and return
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
if current_reader.update_start_date!(params[:start_date])
|
|
@@ -37,9 +38,9 @@ module Bible270
|
|
|
37
38
|
|
|
38
39
|
if Bible270.config.allow_reader_start_date
|
|
39
40
|
current_reader.clear_start_date!
|
|
40
|
-
redirect_to root_path, notice:
|
|
41
|
+
redirect_to root_path, notice: 'Start date cleared — the plan is now undated for you.'
|
|
41
42
|
else
|
|
42
|
-
redirect_to root_path, alert:
|
|
43
|
+
redirect_to root_path, alert: 'The start date is set for the whole community.'
|
|
43
44
|
end
|
|
44
45
|
end
|
|
45
46
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Bible270
|
|
3
4
|
# Built-in OmniAuth sign-in.
|
|
4
5
|
#
|
|
@@ -17,17 +18,13 @@ module Bible270
|
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def create
|
|
20
|
-
auth = request.env[
|
|
21
|
-
unless auth
|
|
22
|
-
redirect_to(sign_in_path, alert: "Sign in didn't complete. Please try again.") and return
|
|
23
|
-
end
|
|
21
|
+
auth = request.env['omniauth.auth']
|
|
22
|
+
redirect_to(sign_in_path, alert: "Sign in didn't complete. Please try again.") and return unless auth
|
|
24
23
|
|
|
25
24
|
reader = Reader.from_omniauth(auth)
|
|
26
|
-
unless reader&.persisted?
|
|
27
|
-
redirect_to(sign_in_path, alert: "We couldn't set up your reader profile.") and return
|
|
28
|
-
end
|
|
25
|
+
redirect_to(sign_in_path, alert: "We couldn't set up your reader profile.") and return unless reader&.persisted?
|
|
29
26
|
|
|
30
|
-
destination = safe_origin(request.env[
|
|
27
|
+
destination = safe_origin(request.env['omniauth.origin']) || after_sign_in_path
|
|
31
28
|
|
|
32
29
|
# Rotate the session id to avoid session fixation, then sign in.
|
|
33
30
|
reset_session
|
|
@@ -40,11 +37,11 @@ module Bible270
|
|
|
40
37
|
def destroy
|
|
41
38
|
reset_session
|
|
42
39
|
@current_reader = nil
|
|
43
|
-
redirect_to(after_sign_out_path, notice:
|
|
40
|
+
redirect_to(after_sign_out_path, notice: 'Signed out.')
|
|
44
41
|
end
|
|
45
42
|
|
|
46
43
|
def failure
|
|
47
|
-
message = params[:message].presence ||
|
|
44
|
+
message = params[:message].presence || 'unknown error'
|
|
48
45
|
redirect_to sign_in_path, alert: "Sign in failed (#{message.to_s.tr('_', ' ')})."
|
|
49
46
|
end
|
|
50
47
|
|
|
@@ -63,14 +60,22 @@ module Bible270
|
|
|
63
60
|
alert: "That doesn't look like an email address — please check and try again.") and return
|
|
64
61
|
end
|
|
65
62
|
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
first_name = params[:first_name].to_s.strip
|
|
64
|
+
last_name = params[:last_name].to_s.strip
|
|
65
|
+
|
|
66
|
+
if Bible270.config.email_sign_in_require_name && (first_name.empty? || last_name.empty?)
|
|
67
|
+
redirect_to(sign_in_path(origin: origin, email: address),
|
|
68
|
+
alert: 'Please give both your first and last name.') and return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
_record, raw = SignInToken.issue!(address, first_name: first_name, last_name: last_name)
|
|
68
72
|
|
|
69
73
|
if raw
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
deliver_sign_in_link(address, email_sign_in_url(token: raw, origin: origin))
|
|
75
|
+
else
|
|
76
|
+
# Silent to the reader by design; say so in the log or this is undebuggable.
|
|
77
|
+
Rails.logger.warn("[bible270] no sign-in link issued for #{address} " \
|
|
78
|
+
'(rate limited — see config.email_sign_in_max_per_window)')
|
|
74
79
|
end
|
|
75
80
|
|
|
76
81
|
# Deliberately identical whether or not a link was actually sent, so this
|
|
@@ -84,13 +89,12 @@ module Bible270
|
|
|
84
89
|
token = SignInToken.claim!(params[:token])
|
|
85
90
|
if token.nil?
|
|
86
91
|
redirect_to(sign_in_path,
|
|
87
|
-
alert:
|
|
92
|
+
alert: 'That link has expired or was already used. Please request a new one.') and return
|
|
88
93
|
end
|
|
89
94
|
|
|
90
|
-
reader = Reader.from_email(token.email,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
end
|
|
95
|
+
reader = Reader.from_email(token.email, first_name: token.first_name,
|
|
96
|
+
last_name: token.last_name, display_name: token.display_name)
|
|
97
|
+
redirect_to(sign_in_path, alert: "We couldn't set up your reader profile.") and return unless reader&.persisted?
|
|
94
98
|
|
|
95
99
|
destination = safe_origin(params[:origin]) || after_sign_in_path
|
|
96
100
|
|
|
@@ -101,16 +105,46 @@ module Bible270
|
|
|
101
105
|
redirect_to destination, notice: "Welcome, #{reader.display_name}."
|
|
102
106
|
end
|
|
103
107
|
|
|
104
|
-
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Delivery problems must not leak through the UI (the response is deliberately
|
|
111
|
+
# identical either way), so they are logged rather than raised. Without this a
|
|
112
|
+
# misconfigured mailer either 500s the request or fails silently, depending on
|
|
113
|
+
# config.action_mailer.raise_delivery_errors.
|
|
114
|
+
def deliver_sign_in_link(address, url)
|
|
115
|
+
minutes = (Bible270.config.email_sign_in_ttl / 60.0).round
|
|
116
|
+
mail = SignInMailer.magic_link(email: address, url: url, expires_in_minutes: minutes)
|
|
117
|
+
Bible270.config.email_sign_in_deliver_later ? mail.deliver_later : mail.deliver_now
|
|
118
|
+
Rails.logger.info("[bible270] sign-in email handed to Action Mailer for #{address}")
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
Rails.logger.error("[bible270] could not deliver the sign-in email to #{address}: #{e.class}: #{e.message}")
|
|
121
|
+
ensure
|
|
122
|
+
log_sign_in_link(address, url)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# In development and test, put the link in the log so you can sign in without
|
|
126
|
+
# working mail delivery. Never in production — the link is a bearer token.
|
|
127
|
+
def log_sign_in_link(address, url)
|
|
128
|
+
return unless log_sign_in_link?
|
|
129
|
+
|
|
130
|
+
Rails.logger.info("[bible270] sign-in link for #{address}: #{url}")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def log_sign_in_link?
|
|
134
|
+
flag = Bible270.config.email_sign_in_log_link
|
|
135
|
+
return flag unless flag.nil?
|
|
136
|
+
|
|
137
|
+
Rails.env.development? || Rails.env.test?
|
|
138
|
+
end
|
|
105
139
|
|
|
106
140
|
# Only ever redirect within this app, and never back into an auth route.
|
|
107
141
|
def safe_origin(value)
|
|
108
142
|
value = value.to_s
|
|
109
143
|
return nil if value.empty?
|
|
110
|
-
return nil unless value.start_with?(
|
|
111
|
-
return nil if value.include?(
|
|
112
|
-
return nil if value.include?(
|
|
113
|
-
return nil if value.include?(
|
|
144
|
+
return nil unless value.start_with?('/')
|
|
145
|
+
return nil if value.include?('//') # protocol-relative URL
|
|
146
|
+
return nil if value.include?('\\') # browsers may normalise \ to /
|
|
147
|
+
return nil if value.include?('/auth/')
|
|
114
148
|
|
|
115
149
|
value
|
|
116
150
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Bible270
|
|
3
4
|
module PlanHelper
|
|
4
5
|
def b270_track(track)
|
|
@@ -18,27 +19,29 @@ module Bible270
|
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def b270_passage_url(reference)
|
|
21
|
-
return
|
|
22
|
+
return '#' if reference.blank?
|
|
23
|
+
|
|
22
24
|
Bible270.config.passage_url_builder.call(reference, Bible270.config.bible_version)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def b270_avatar(reader, size: 34)
|
|
26
28
|
if reader.avatar_url.present?
|
|
27
|
-
image_tag reader.avatar_url, class:
|
|
29
|
+
image_tag reader.avatar_url, class: 'b270-avatar', width: size, height: size, alt: reader.display_name
|
|
28
30
|
else
|
|
29
|
-
content_tag :span, reader.initials, class:
|
|
30
|
-
|
|
31
|
+
content_tag :span, reader.initials, class: 'b270-avatar b270-avatar-fallback',
|
|
32
|
+
style: "width:#{size}px;height:#{size}px;line-height:#{size}px"
|
|
31
33
|
end
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def b270_day_status_class(reader, day)
|
|
35
|
-
return
|
|
37
|
+
return '' unless reader
|
|
38
|
+
|
|
36
39
|
if reader.day_complete?(day)
|
|
37
|
-
|
|
40
|
+
'complete'
|
|
38
41
|
elsif reader.read_tracks_for(day).any?
|
|
39
|
-
|
|
42
|
+
'partial'
|
|
40
43
|
else
|
|
41
|
-
|
|
44
|
+
''
|
|
42
45
|
end
|
|
43
46
|
end
|
|
44
47
|
end
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Bible270
|
|
3
4
|
class ApplicationMailer < ActionMailer::Base
|
|
4
5
|
default from: -> { Bible270.config.mailer_from }
|
|
5
|
-
layout nil
|
|
6
|
+
# `layout nil` means "normal lookup", which finds the host app's mailer layout and
|
|
7
|
+
# nests our complete HTML document inside theirs. Only `false` disables it.
|
|
8
|
+
layout false
|
|
6
9
|
end
|
|
7
10
|
end
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Bible270
|
|
3
4
|
# A single reader marking one track (ot / nt / pp) read on one day.
|
|
4
5
|
class Checkoff < ApplicationRecord
|
|
5
|
-
self.table_name =
|
|
6
|
+
self.table_name = 'bible270_checkoffs'
|
|
6
7
|
|
|
7
|
-
belongs_to :reader, class_name:
|
|
8
|
+
belongs_to :reader, class_name: 'Bible270::Reader'
|
|
8
9
|
|
|
9
10
|
validates :day, inclusion: { in: 1..Plan::DAYS }
|
|
10
11
|
validates :track, inclusion: { in: Plan::TRACKS.keys }
|
|
11
12
|
validates :track, uniqueness: { scope: %i[reader_id day] }
|
|
12
13
|
validate :track_present_on_day
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
private
|
|
15
16
|
|
|
16
17
|
def track_present_on_day
|
|
17
18
|
return if day.nil? || track.nil?
|
|
@@ -1,16 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Bible270
|
|
3
4
|
# A publicly visible reflection attached to a plan day (optionally a track).
|
|
4
5
|
class Comment < ApplicationRecord
|
|
5
|
-
self.table_name =
|
|
6
|
+
self.table_name = 'bible270_comments'
|
|
6
7
|
|
|
7
|
-
belongs_to :reader, class_name:
|
|
8
|
+
belongs_to :reader, class_name: 'Bible270::Reader'
|
|
8
9
|
|
|
9
10
|
validates :day, inclusion: { in: 1..Plan::DAYS }
|
|
10
11
|
validates :body, presence: true, length: { maximum: 4000 }
|
|
11
|
-
|
|
12
|
+
# A reflection can be about one track or about the whole day. The form's
|
|
13
|
+
# "The whole day" option submits an empty string, so normalise it to NULL
|
|
14
|
+
# before validating — allow_nil alone rejects "" with
|
|
15
|
+
# "Track is not included in the list".
|
|
16
|
+
before_validation :normalize_track
|
|
17
|
+
|
|
18
|
+
validates :track, inclusion: { in: Plan::TRACKS.keys }, allow_blank: true
|
|
19
|
+
|
|
20
|
+
scope :approved, -> { where(approved: true) }
|
|
21
|
+
scope :hidden, -> { where(approved: false) }
|
|
22
|
+
scope :for_day, ->(d) { approved.where(day: d).order(created_at: :asc) }
|
|
23
|
+
scope :recent, -> { approved.order(created_at: :desc) }
|
|
24
|
+
|
|
25
|
+
# Every reflection is visible when written; these are the moderation actions.
|
|
26
|
+
def hide! = update!(approved: false, moderated_at: Time.current)
|
|
27
|
+
def unhide! = update!(approved: true, moderated_at: Time.current)
|
|
28
|
+
def hidden? = !approved
|
|
29
|
+
def moderated? = moderated_at.present?
|
|
30
|
+
|
|
31
|
+
private
|
|
12
32
|
|
|
13
|
-
|
|
14
|
-
|
|
33
|
+
def normalize_track
|
|
34
|
+
self.track = Plan.normalize_track(track)
|
|
35
|
+
end
|
|
15
36
|
end
|
|
16
37
|
end
|