bible270 0.9.0 → 0.11.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 +196 -237
- data/README.md +159 -79
- data/Rakefile +5 -4
- data/app/controllers/bible270/admin_controller.rb +147 -0
- data/app/controllers/bible270/application_controller.rb +24 -3
- 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/profiles_controller.rb +45 -0
- data/app/controllers/bible270/readers_controller.rb +5 -4
- data/app/controllers/bible270/sessions_controller.rb +89 -26
- data/app/helpers/bible270/plan_helper.rb +22 -12
- 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 +167 -17
- data/app/models/bible270/setting.rb +57 -0
- 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 +48 -0
- data/app/views/bible270/admin/show.html.erb +104 -0
- data/app/views/bible270/days/_start_date.html.erb +1 -1
- data/app/views/bible270/days/index.html.erb +9 -21
- data/app/views/bible270/profiles/edit.html.erb +44 -0
- 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 +17 -3
- data/app/views/bible270/shared/_day_index.html.erb +12 -0
- data/app/views/bible270/shared/_header.html.erb +2 -0
- data/app/views/bible270/shared/_styles.html.erb +17 -7
- data/app/views/layouts/bible270/application.html.erb +2 -4
- data/config/routes.rb +38 -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/db/migrate/20260101000007_create_bible270_settings.rb +15 -0
- data/lib/bible270/avatars.rb +46 -0
- data/lib/bible270/configuration.rb +96 -24
- data/lib/bible270/email_sign_in.rb +7 -7
- data/lib/bible270/engine.rb +1 -0
- data/lib/bible270/names.rb +47 -0
- data/lib/bible270/plan.rb +356 -102
- data/lib/bible270/versification.rb +84 -67
- data/lib/bible270/version.rb +2 -1
- data/lib/bible270.rb +8 -5
- data/lib/generators/bible270/install/install_generator.rb +521 -38
- data/lib/generators/bible270/install/templates/bible270.rb.tt +2 -2
- metadata +15 -44
|
@@ -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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bible270
|
|
4
|
+
# A reader editing their own name — the one thing about themselves they can
|
|
5
|
+
# change. Everything else about a reader comes from how they signed in.
|
|
6
|
+
class ProfilesController < ApplicationController
|
|
7
|
+
before_action :require_signed_in_reader
|
|
8
|
+
|
|
9
|
+
def edit
|
|
10
|
+
@names = current_reader.suggested_names
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def update
|
|
14
|
+
problems = []
|
|
15
|
+
problems << 'both a first and last name' unless current_reader.update_names(params[:first_name],
|
|
16
|
+
params[:last_name])
|
|
17
|
+
if params[:avatar].present? && !current_reader.attach_avatar(params[:avatar])
|
|
18
|
+
problems << (current_reader.errors[:avatar].first || 'a valid image')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if problems.empty?
|
|
22
|
+
redirect_to reader_path(current_reader), notice: 'Your profile has been updated.'
|
|
23
|
+
else
|
|
24
|
+
@names = { first_name: params[:first_name], last_name: params[:last_name] }
|
|
25
|
+
flash.now[:alert] = "Please give #{problems.to_sentence}."
|
|
26
|
+
render :edit, status: :unprocessable_entity
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def remove_avatar
|
|
31
|
+
current_reader.remove_avatar!
|
|
32
|
+
redirect_to profile_path, notice: 'Your picture has been removed.'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Unlike checking off a reading, there is nothing sensible to do here for a
|
|
38
|
+
# visitor who isn't signed in, so send them to sign in rather than 404.
|
|
39
|
+
def require_signed_in_reader
|
|
40
|
+
return if signed_in?
|
|
41
|
+
|
|
42
|
+
redirect_to sign_in_path(origin: request.fullpath), alert: 'Please sign in first.'
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -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,17 @@ module Bible270
|
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def create
|
|
20
|
-
auth = request.env[
|
|
21
|
-
unless auth
|
|
22
|
-
|
|
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
|
|
23
|
+
|
|
24
|
+
if enrollment_closed? && !Reader.omniauth_reader_exists?(auth['provider'], auth['uid'])
|
|
25
|
+
redirect_to(sign_in_path, alert: enrollment_closed_message) and return
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
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
|
|
29
|
+
redirect_to(sign_in_path, alert: "We couldn't set up your reader profile.") and return unless reader&.persisted?
|
|
29
30
|
|
|
30
|
-
destination = safe_origin(request.env[
|
|
31
|
+
destination = safe_origin(request.env['omniauth.origin']) || after_sign_in_path
|
|
31
32
|
|
|
32
33
|
# Rotate the session id to avoid session fixation, then sign in.
|
|
33
34
|
reset_session
|
|
@@ -40,11 +41,11 @@ module Bible270
|
|
|
40
41
|
def destroy
|
|
41
42
|
reset_session
|
|
42
43
|
@current_reader = nil
|
|
43
|
-
redirect_to(after_sign_out_path, notice:
|
|
44
|
+
redirect_to(after_sign_out_path, notice: 'Signed out.')
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def failure
|
|
47
|
-
message = params[:message].presence ||
|
|
48
|
+
message = params[:message].presence || 'unknown error'
|
|
48
49
|
redirect_to sign_in_path, alert: "Sign in failed (#{message.to_s.tr('_', ' ')})."
|
|
49
50
|
end
|
|
50
51
|
|
|
@@ -63,14 +64,23 @@ module Bible270
|
|
|
63
64
|
alert: "That doesn't look like an email address — please check and try again.") and return
|
|
64
65
|
end
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
# Names are optional here. Requiring them only for unknown addresses would
|
|
68
|
+
# make the response differ, which tells a stranger whether an address
|
|
69
|
+
# already has an account. A new reader is asked for their name after they
|
|
70
|
+
# click the link instead (see email_callback).
|
|
71
|
+
names = Names.normalize(params[:first_name], params[:last_name])
|
|
72
|
+
|
|
73
|
+
_record, raw = SignInToken.issue!(address, first_name: names&.dig(:first_name),
|
|
74
|
+
last_name: names&.dig(:last_name))
|
|
75
|
+
|
|
76
|
+
if raw && enrollment_closed? && !Reader.email_reader_exists?(address)
|
|
77
|
+
Rails.logger.info("[bible270] enrolment closed; no link sent to #{address}")
|
|
78
|
+
elsif raw
|
|
79
|
+
deliver_sign_in_link(address, email_sign_in_url(token: raw, origin: origin))
|
|
80
|
+
else
|
|
81
|
+
# Silent to the reader by design; say so in the log or this is undebuggable.
|
|
82
|
+
Rails.logger.warn("[bible270] no sign-in link issued for #{address} " \
|
|
83
|
+
'(rate limited — see config.email_sign_in_max_per_window)')
|
|
74
84
|
end
|
|
75
85
|
|
|
76
86
|
# Deliberately identical whether or not a link was actually sent, so this
|
|
@@ -84,33 +94,86 @@ module Bible270
|
|
|
84
94
|
token = SignInToken.claim!(params[:token])
|
|
85
95
|
if token.nil?
|
|
86
96
|
redirect_to(sign_in_path,
|
|
87
|
-
alert:
|
|
97
|
+
alert: 'That link has expired or was already used. Please request a new one.') and return
|
|
88
98
|
end
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
redirect_to(sign_in_path, alert: "We couldn't set up your reader profile.") and return
|
|
100
|
+
if enrollment_closed? && !Reader.email_reader_exists?(token.email)
|
|
101
|
+
redirect_to(sign_in_path, alert: enrollment_closed_message) and return
|
|
93
102
|
end
|
|
94
103
|
|
|
104
|
+
reader = Reader.from_email(token.email, first_name: token.first_name,
|
|
105
|
+
last_name: token.last_name, display_name: token.display_name)
|
|
106
|
+
redirect_to(sign_in_path, alert: "We couldn't set up your reader profile.") and return unless reader&.persisted?
|
|
107
|
+
|
|
95
108
|
destination = safe_origin(params[:origin]) || after_sign_in_path
|
|
96
109
|
|
|
97
110
|
reset_session
|
|
98
111
|
session[:bible270_reader_id] = reader.id
|
|
99
112
|
@current_reader = reader
|
|
100
113
|
|
|
114
|
+
if name_needed?(reader)
|
|
115
|
+
redirect_to(profile_path,
|
|
116
|
+
notice: 'Welcome. Please add your name, so others know who they are reading with.')
|
|
117
|
+
return
|
|
118
|
+
end
|
|
119
|
+
|
|
101
120
|
redirect_to destination, notice: "Welcome, #{reader.display_name}."
|
|
102
121
|
end
|
|
103
122
|
|
|
104
|
-
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# Delivery problems must not leak through the UI (the response is deliberately
|
|
126
|
+
# identical either way), so they are logged rather than raised. Without this a
|
|
127
|
+
# misconfigured mailer either 500s the request or fails silently, depending on
|
|
128
|
+
# config.action_mailer.raise_delivery_errors.
|
|
129
|
+
def deliver_sign_in_link(address, url)
|
|
130
|
+
minutes = (Bible270.config.email_sign_in_ttl / 60.0).round
|
|
131
|
+
mail = SignInMailer.magic_link(email: address, url: url, expires_in_minutes: minutes)
|
|
132
|
+
Bible270.config.email_sign_in_deliver_later ? mail.deliver_later : mail.deliver_now
|
|
133
|
+
Rails.logger.info("[bible270] sign-in email handed to Action Mailer for #{address}")
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
Rails.logger.error("[bible270] could not deliver the sign-in email to #{address}: #{e.class}: #{e.message}")
|
|
136
|
+
ensure
|
|
137
|
+
log_sign_in_link(address, url)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# In development and test, put the link in the log so you can sign in without
|
|
141
|
+
# working mail delivery. Never in production — the link is a bearer token.
|
|
142
|
+
def log_sign_in_link(address, url)
|
|
143
|
+
return unless log_sign_in_link?
|
|
144
|
+
|
|
145
|
+
Rails.logger.info("[bible270] sign-in link for #{address}: #{url}")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def log_sign_in_link?
|
|
149
|
+
flag = Bible270.config.email_sign_in_log_link
|
|
150
|
+
return flag unless flag.nil?
|
|
151
|
+
|
|
152
|
+
Rails.env.development? || Rails.env.test?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# A reader who signed in by email and still has no first/last name. Applies
|
|
156
|
+
# only to email sign-in: an OmniAuth reader carries whatever name their
|
|
157
|
+
# provider gave.
|
|
158
|
+
def name_needed?(reader)
|
|
159
|
+
Bible270.config.email_sign_in_require_name &&
|
|
160
|
+
reader.provider == 'email' &&
|
|
161
|
+
reader.full_name.nil?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def enrollment_closed_message
|
|
165
|
+
'This run of the plan is closed to new readers. If you have taken part before, ' \
|
|
166
|
+
'sign in with the same email or account.'
|
|
167
|
+
end
|
|
105
168
|
|
|
106
169
|
# Only ever redirect within this app, and never back into an auth route.
|
|
107
170
|
def safe_origin(value)
|
|
108
171
|
value = value.to_s
|
|
109
172
|
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?(
|
|
173
|
+
return nil unless value.start_with?('/')
|
|
174
|
+
return nil if value.include?('//') # protocol-relative URL
|
|
175
|
+
return nil if value.include?('\\') # browsers may normalise \ to /
|
|
176
|
+
return nil if value.include?('/auth/')
|
|
114
177
|
|
|
115
178
|
value
|
|
116
179
|
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,36 @@ 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
|
|
|
27
|
+
# An uploaded avatar wins over whatever the sign-in provider gave us.
|
|
28
|
+
# Active Storage routes live in the host app, hence main_app.
|
|
29
|
+
def b270_avatar_src(reader)
|
|
30
|
+
return main_app.rails_blob_path(reader.avatar, only_path: true) if reader.avatar_uploaded?
|
|
31
|
+
|
|
32
|
+
reader.avatar_url.presence
|
|
33
|
+
end
|
|
34
|
+
|
|
25
35
|
def b270_avatar(reader, size: 34)
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
src = b270_avatar_src(reader)
|
|
37
|
+
if src
|
|
38
|
+
image_tag src, class: 'b270-avatar', width: size, height: size, alt: reader.display_name
|
|
28
39
|
else
|
|
29
|
-
content_tag :span, reader.initials, class:
|
|
30
|
-
|
|
40
|
+
content_tag :span, reader.initials, class: 'b270-avatar b270-avatar-fallback',
|
|
41
|
+
style: "width:#{size}px;height:#{size}px;line-height:#{size}px"
|
|
31
42
|
end
|
|
32
43
|
end
|
|
33
44
|
|
|
34
45
|
def b270_day_status_class(reader, day)
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
else
|
|
41
|
-
""
|
|
46
|
+
return '' unless reader
|
|
47
|
+
|
|
48
|
+
case reader.day_status(day)
|
|
49
|
+
when :complete then 'complete'
|
|
50
|
+
when :partial then 'partial'
|
|
51
|
+
else ''
|
|
42
52
|
end
|
|
43
53
|
end
|
|
44
54
|
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
|