bible270 0.17.1 → 0.17.2
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/Rakefile +3 -0
- data/app/controllers/bible270/admin_controller.rb +74 -0
- data/app/controllers/bible270/sessions_controller.rb +3 -1
- data/app/mailers/bible270/broadcast.html.erb +17 -0
- data/app/mailers/bible270/broadcast.text.erb +7 -0
- data/app/mailers/bible270/notice_mailer.rb +20 -0
- data/app/models/bible270/reader.rb +27 -0
- data/app/views/bible270/admin/index.html.erb +27 -0
- data/app/views/bible270/admin/show.html.erb +22 -0
- data/app/views/bible270/notice_mailer/broadcast.html.erb +17 -0
- data/app/views/bible270/notice_mailer/broadcast.text.erb +7 -0
- data/app/views/bible270/shared/_styles.html.erb +1 -0
- data/config/routes.rb +2 -0
- data/lib/bible270/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e3c27f2336f3e2a809205242bbc42c46d241e49b9ea2b1bf49ca288523d7c88
|
|
4
|
+
data.tar.gz: 8e8c25b59eafde481a17546ac14b563527dd591ca64d9cf7541d95a01dd1f36c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06af092550c30ded960e3611301d941e10fdea25e79fcfcab8aa97c4d68fee7b3780961f22bc4f81be4ba3f065ebe57642386ba318c69f80e2f1bd1e07ce0dec
|
|
7
|
+
data.tar.gz: ed69c75af0e88e8dfeb347b756c6c10692f46b6e90196e96851aecc026a664752ce6f2fbb3de3b324734eff316555c229d19aa15270d26061b370b4db643f3ae
|
data/Rakefile
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'bundler/gem_tasks' # provides build / install / release
|
|
4
4
|
require 'rake/testtask'
|
|
5
|
+
require 'coveralls/rake/task'
|
|
5
6
|
|
|
6
7
|
Rake::TestTask.new(:test) do |t|
|
|
7
8
|
t.libs << 'test' << 'lib'
|
|
@@ -9,4 +10,6 @@ Rake::TestTask.new(:test) do |t|
|
|
|
9
10
|
t.warning = false
|
|
10
11
|
end
|
|
11
12
|
|
|
13
|
+
Coveralls::RakeTask.new
|
|
14
|
+
|
|
12
15
|
task default: :test
|
|
@@ -8,6 +8,9 @@ module Bible270
|
|
|
8
8
|
# neither set the panel is unreachable, and every action 404s rather than 403s
|
|
9
9
|
# so its existence isn't advertised.
|
|
10
10
|
class AdminController < ApplicationController
|
|
11
|
+
LAST_BROADCAST_AT = 'last_broadcast_at'
|
|
12
|
+
LAST_BROADCAST_SUBJECT = 'last_broadcast_subject'
|
|
13
|
+
|
|
11
14
|
before_action :require_admin!
|
|
12
15
|
before_action :load_reader,
|
|
13
16
|
only: %i[show destroy update_start update_profile remove_avatar complete_through toggle_day]
|
|
@@ -32,6 +35,9 @@ module Bible270
|
|
|
32
35
|
counts = Checkoff.group(:reader_id, :day).count
|
|
33
36
|
@days_completed = Hash.new(0)
|
|
34
37
|
counts.each { |(rid, day), n| @days_completed[rid] += 1 if n >= Plan.total_parts(day) }
|
|
38
|
+
@reachable = Reader.where.not(email: [nil, '']).count
|
|
39
|
+
@last_broadcast_at = parsed_time(Setting.read(LAST_BROADCAST_AT))
|
|
40
|
+
@last_broadcast_subject = Setting.read(LAST_BROADCAST_SUBJECT)
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
def show
|
|
@@ -75,6 +81,47 @@ module Bible270
|
|
|
75
81
|
end
|
|
76
82
|
|
|
77
83
|
# Put the reader on a given day as of today, or set an explicit start date.
|
|
84
|
+
def broadcast
|
|
85
|
+
subject = params[:subject].to_s.strip
|
|
86
|
+
body = params[:body].to_s.strip
|
|
87
|
+
|
|
88
|
+
if subject.empty? || body.empty?
|
|
89
|
+
return redirect_to(admin_path, alert: 'A message needs both a subject and something to say.')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
recipients = Reader.where.not(email: [nil, '']).order(:id)
|
|
93
|
+
return redirect_to(admin_path, alert: 'Nobody has an email address.') if recipients.empty?
|
|
94
|
+
|
|
95
|
+
sent = deliver_broadcast(recipients, subject, body)
|
|
96
|
+
Setting.write(LAST_BROADCAST_AT, Time.current.iso8601)
|
|
97
|
+
Setting.write(LAST_BROADCAST_SUBJECT, subject)
|
|
98
|
+
|
|
99
|
+
redirect_to admin_path,
|
|
100
|
+
notice: "Sent to #{sent} #{'reader'.pluralize(sent)}."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def update_bible_version
|
|
104
|
+
reader = Reader.find_by(id: params[:id])
|
|
105
|
+
return redirect_to(admin_path, alert: 'No such reader.') if reader.nil?
|
|
106
|
+
|
|
107
|
+
code = params[:bible_version].to_s
|
|
108
|
+
# Blank means "no preference": the reader follows the site default, which is
|
|
109
|
+
# not the same as choosing whichever translation the site happens to use now.
|
|
110
|
+
if code.empty?
|
|
111
|
+
reader.update(bible_version: nil)
|
|
112
|
+
return redirect_to admin_reader_path(reader),
|
|
113
|
+
notice: "#{reader.display_name} now follows the site default " \
|
|
114
|
+
"(#{Bible270.config.bible_version})."
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if reader.update_bible_version(code)
|
|
118
|
+
redirect_to admin_reader_path(reader),
|
|
119
|
+
notice: "#{reader.display_name} now reads the #{reader.bible_version_label}."
|
|
120
|
+
else
|
|
121
|
+
redirect_to admin_reader_path(reader), alert: 'That is not a translation on the list.'
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
78
125
|
def update_start
|
|
79
126
|
if params[:start_date].present?
|
|
80
127
|
# set_start_date!, not update_start_date!: an admin is not subject to
|
|
@@ -138,6 +185,33 @@ module Bible270
|
|
|
138
185
|
end
|
|
139
186
|
end
|
|
140
187
|
|
|
188
|
+
# Failures are counted rather than raised: one bad address must not stop the
|
|
189
|
+
# rest of the message going out, and the admin is told how many were sent.
|
|
190
|
+
# A stored timestamp that cannot be read is treated as absent rather than
|
|
191
|
+
# blowing up the admin page.
|
|
192
|
+
def parsed_time(value)
|
|
193
|
+
return nil if value.blank?
|
|
194
|
+
|
|
195
|
+
Time.parse(value.to_s)
|
|
196
|
+
rescue ArgumentError, TypeError
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def deliver_broadcast(recipients, subject, body)
|
|
201
|
+
later = Bible270.config.registration_notice_deliver_later
|
|
202
|
+
sent = 0
|
|
203
|
+
|
|
204
|
+
recipients.find_each do |reader|
|
|
205
|
+
mail = NoticeMailer.broadcast(reader_id: reader.id, subject: subject, body: body)
|
|
206
|
+
later ? mail.deliver_later : mail.deliver_now
|
|
207
|
+
sent += 1
|
|
208
|
+
rescue StandardError => e
|
|
209
|
+
Rails.logger.error("[bible270] broadcast to reader #{reader.id} failed: #{e.class}: #{e.message}")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
sent
|
|
213
|
+
end
|
|
214
|
+
|
|
141
215
|
private
|
|
142
216
|
|
|
143
217
|
def require_admin!
|
|
@@ -36,7 +36,9 @@ module Bible270
|
|
|
36
36
|
remember_reader!(reader)
|
|
37
37
|
@current_reader = reader
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
# first_name is filled in for every route now, but a bridged host user may
|
|
40
|
+
# still arrive without one.
|
|
41
|
+
redirect_to destination, notice: "Welcome #{reader.first_name.presence || reader.display_name}."
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def destroy
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<body style="margin:0;padding:24px;background:#ece6da;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#231f18">
|
|
4
|
+
<div style="max-width:520px;margin:0 auto;background:#f4efe6;border:1px solid #d7cfbf;border-radius:14px;padding:28px">
|
|
5
|
+
<p style="margin:0 0 16px;font-size:15px">
|
|
6
|
+
<%= @reader.first_name.presence || @reader.display_name %>,
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<div style="font-size:15px;line-height:1.6"><%= simple_format(@body) %></div>
|
|
10
|
+
|
|
11
|
+
<p style="margin:24px 0 0;padding-top:16px;border-top:1px solid #d7cfbf;font-size:12px;color:#9a9082">
|
|
12
|
+
<%= @app_name %><% if @plan_url %> ·
|
|
13
|
+
<a href="<%= @plan_url %>" style="color:#a5812c"><%= @plan_url %></a><% end %>
|
|
14
|
+
</p>
|
|
15
|
+
</div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -31,6 +31,22 @@ module Bible270
|
|
|
31
31
|
subject: "#{@app_name}: #{@author.display_name} mentioned you on day #{@comment.day}"
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# A message from whoever runs the plan to one reader. Sent individually rather
|
|
35
|
+
# than as one email with everyone in bcc: a bcc list of a hundred addresses is
|
|
36
|
+
# a spam signal, and it means nobody can be greeted by name or unsubscribe
|
|
37
|
+
# meaningfully.
|
|
38
|
+
def broadcast(reader_id:, subject:, body:)
|
|
39
|
+
@reader = Reader.find_by(id: reader_id)
|
|
40
|
+
return message.perform_deliveries = false if @reader.nil? || @reader.email.blank?
|
|
41
|
+
return message.perform_deliveries = false if subject.to_s.strip.empty? || body.to_s.strip.empty?
|
|
42
|
+
|
|
43
|
+
@body = body.to_s
|
|
44
|
+
@app_name = Bible270.config.app_name
|
|
45
|
+
@plan_url = plan_url
|
|
46
|
+
|
|
47
|
+
mail to: @reader.email, subject: subject.to_s.strip
|
|
48
|
+
end
|
|
49
|
+
|
|
34
50
|
private
|
|
35
51
|
|
|
36
52
|
# A mailer has no request, so the host is config.mailer_host or nothing. No
|
|
@@ -41,6 +57,10 @@ module Bible270
|
|
|
41
57
|
# config.mount_at was worse than useless — ignored when blank, and overriding
|
|
42
58
|
# the true prefix when not, so a config that disagreed with routes.rb would
|
|
43
59
|
# have produced links to nowhere.
|
|
60
|
+
def plan_url
|
|
61
|
+
engine_url(:root_url)
|
|
62
|
+
end
|
|
63
|
+
|
|
44
64
|
def day_url_for(day)
|
|
45
65
|
engine_url(:day_url, day)
|
|
46
66
|
end
|
|
@@ -45,10 +45,37 @@ module Bible270
|
|
|
45
45
|
image = first_present(dig_auth(info, :image), dig_auth(info, :avatar_url))
|
|
46
46
|
reader.email = email if email.present?
|
|
47
47
|
reader.avatar_url = image if image.present?
|
|
48
|
+
assign_names_from(reader, info)
|
|
48
49
|
reader.save
|
|
49
50
|
reader
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
# Providers hand back one display name far more often than two fields, so the
|
|
54
|
+
# name is split when they do not. Without this an OmniAuth reader had no
|
|
55
|
+
# first_name — which made the greeting read "Welcome ." and, more seriously,
|
|
56
|
+
# made them unmentionable: Reader.mentioned_in only considers readers who have
|
|
57
|
+
# one.
|
|
58
|
+
#
|
|
59
|
+
# Only filled in when empty, so a reader who has since corrected their name on
|
|
60
|
+
# their profile does not have it overwritten at every sign-in.
|
|
61
|
+
def self.assign_names_from(reader, info)
|
|
62
|
+
return if reader.first_name.present?
|
|
63
|
+
|
|
64
|
+
given = dig_auth(info, :first_name)
|
|
65
|
+
family = dig_auth(info, :last_name)
|
|
66
|
+
|
|
67
|
+
if given.present?
|
|
68
|
+
reader.first_name = given
|
|
69
|
+
reader.last_name = family
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
split = Names.split_display_name(reader.display_name)
|
|
74
|
+
reader.first_name = split ? split[:first_name] : reader.display_name.to_s.split.first
|
|
75
|
+
reader.last_name = split ? split[:last_name] : nil
|
|
76
|
+
end
|
|
77
|
+
private_class_method :assign_names_from
|
|
78
|
+
|
|
52
79
|
# Find or create the reader behind a verified email address. Uses the same
|
|
53
80
|
# provider/uid identity columns as OmniAuth, with provider "email", so an
|
|
54
81
|
# email reader is indistinguishable from any other downstream.
|
|
@@ -47,4 +47,31 @@
|
|
|
47
47
|
<p style="color:var(--faint)">No readers yet.</p>
|
|
48
48
|
<% end %>
|
|
49
49
|
|
|
50
|
+
<div class="b270-panel">
|
|
51
|
+
<h2 class="b270-subhead">Write to everyone</h2>
|
|
52
|
+
|
|
53
|
+
<% if @reachable.zero? %>
|
|
54
|
+
<p class="b270-hint" style="margin-top:0">No reader has an email address yet.</p>
|
|
55
|
+
<% else %>
|
|
56
|
+
<p class="b270-hint" style="margin-top:0">
|
|
57
|
+
Goes to <strong><%= @reachable %></strong>
|
|
58
|
+
<%= @reachable == 1 ? 'reader' : 'readers' %> individually, not as one message with
|
|
59
|
+
everyone in bcc — so each is greeted by name.
|
|
60
|
+
<% if @last_broadcast_at.present? %>
|
|
61
|
+
<br>Last sent <%= b270_datetime(@last_broadcast_at) %><%
|
|
62
|
+
%><% if @last_broadcast_subject.present? %>: <em><%= @last_broadcast_subject %></em><% end %>.
|
|
63
|
+
<% end %>
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
<%= form_with url: admin_broadcast_path, method: :post, class: "b270-form",
|
|
67
|
+
data: { turbo_confirm: "Send this to #{@reachable} #{@reachable == 1 ? 'reader' : 'readers'}?" } do %>
|
|
68
|
+
<%= text_field_tag :subject, params[:subject], placeholder: "Subject", class: "b270-input" %>
|
|
69
|
+
<%= text_area_tag :body, params[:body], placeholder: "What would you like to say?", rows: 6 %>
|
|
70
|
+
<div class="row">
|
|
71
|
+
<%= submit_tag "Send", class: "b270-btn" %>
|
|
72
|
+
</div>
|
|
73
|
+
<% end %>
|
|
74
|
+
<% end %>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
50
77
|
<p style="margin-top:18px"><%= link_to '← Back to the plan', root_path %></p>
|
|
@@ -92,6 +92,28 @@
|
|
|
92
92
|
<%= render "bible270/shared/reflections", comments: @comments, moderate: true %>
|
|
93
93
|
</div>
|
|
94
94
|
|
|
95
|
+
<div class="b270-panel">
|
|
96
|
+
<h2 class="b270-subhead">Which translation they read</h2>
|
|
97
|
+
<p class="b270-hint" style="margin-top:0">
|
|
98
|
+
Reading links open in this translation.
|
|
99
|
+
<% if @reader.bible_version.blank? %>
|
|
100
|
+
They have expressed no preference, so they follow the site default
|
|
101
|
+
(<%= Bible270.config.bible_version %>).
|
|
102
|
+
<% end %>
|
|
103
|
+
</p>
|
|
104
|
+
|
|
105
|
+
<%= form_with url: admin_reader_version_path(@reader), method: :patch, class: "b270-form" do %>
|
|
106
|
+
<div class="row">
|
|
107
|
+
<%= select_tag :bible_version,
|
|
108
|
+
options_for_select([["Site default (#{Bible270.config.bible_version})", ""]] +
|
|
109
|
+
Bible270::Translations.options,
|
|
110
|
+
@reader.bible_version.to_s),
|
|
111
|
+
class: "b270-select", style: "width:auto" %>
|
|
112
|
+
<%= submit_tag "Save", class: "b270-btn" %>
|
|
113
|
+
</div>
|
|
114
|
+
<% end %>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
95
117
|
<div class="b270-panel">
|
|
96
118
|
<h2 class="b270-subhead">Remove</h2>
|
|
97
119
|
<p class="b270-hint" style="margin-top:0">Deletes the reader together with every check-off and
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<body style="margin:0;padding:24px;background:#ece6da;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#231f18">
|
|
4
|
+
<div style="max-width:520px;margin:0 auto;background:#f4efe6;border:1px solid #d7cfbf;border-radius:14px;padding:28px">
|
|
5
|
+
<p style="margin:0 0 16px;font-size:15px">
|
|
6
|
+
<%= @reader.first_name.presence || @reader.display_name %>,
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<div style="font-size:15px;line-height:1.6"><%= simple_format(@body) %></div>
|
|
10
|
+
|
|
11
|
+
<p style="margin:24px 0 0;padding-top:16px;border-top:1px solid #d7cfbf;font-size:12px;color:#9a9082">
|
|
12
|
+
<%= @app_name %><% if @plan_url %> ·
|
|
13
|
+
<a href="<%= @plan_url %>" style="color:#a5812c"><%= @plan_url %></a><% end %>
|
|
14
|
+
</p>
|
|
15
|
+
</div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -183,6 +183,7 @@
|
|
|
183
183
|
.b270-comment.b270-editing{background:rgba(165,129,44,.05);border-radius:10px;padding:12px}
|
|
184
184
|
.b270-editing textarea{min-height:64px}
|
|
185
185
|
.b270-cdel{font-size:12px;color:var(--oxblood);background:none;border:none;cursor:pointer;padding:0;margin-left:8px}
|
|
186
|
+
.b270-input{width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:9px;background:#fff;font:inherit;font-size:14px;margin-bottom:8px}
|
|
186
187
|
.b270-form textarea{width:100%;font:inherit;font-size:15px;border:1px solid var(--line);border-radius:10px;padding:11px 13px;background:#fff;resize:vertical;min-height:76px}
|
|
187
188
|
.b270-form .row{display:flex;align-items:center;gap:10px;margin-top:10px}
|
|
188
189
|
.b270 .b270-btn{border:none;border-radius:10px;padding:8px 16px;margin-top:4px;font:inherit;font-weight:600;font-size:14px;cursor:pointer;background:var(--ink);color:var(--paper);text-decoration:none;display:inline-block}
|
data/config/routes.rb
CHANGED
|
@@ -22,10 +22,12 @@ Bible270::Engine.routes.draw do
|
|
|
22
22
|
delete 'admin/readers/:id', to: 'admin#destroy'
|
|
23
23
|
patch 'admin/readers/:id/profile', to: 'admin#update_profile', as: :admin_reader_profile
|
|
24
24
|
delete 'admin/readers/:id/avatar', to: 'admin#remove_avatar', as: :admin_reader_avatar
|
|
25
|
+
patch 'admin/readers/:id/version', to: 'admin#update_bible_version', as: :admin_reader_version
|
|
25
26
|
patch 'admin/readers/:id/start', to: 'admin#update_start', as: :admin_reader_start
|
|
26
27
|
patch 'admin/readers/:id/through', to: 'admin#complete_through', as: :admin_reader_through
|
|
27
28
|
post 'admin/readers/:id/days/:day', to: 'admin#toggle_day', as: :admin_reader_day
|
|
28
29
|
|
|
30
|
+
post 'admin/broadcast', to: 'admin#broadcast', as: :admin_broadcast
|
|
29
31
|
patch 'admin/enrollment', to: 'admin#update_enrollment', as: :admin_enrollment
|
|
30
32
|
get 'admin/comments', to: 'admin#comments', as: :admin_comments
|
|
31
33
|
patch 'admin/comments/:id/hide', to: 'admin#hide_comment', as: :admin_hide_comment
|
data/lib/bible270/version.rb
CHANGED
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: 0.17.
|
|
4
|
+
version: 0.17.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew vonderLuft
|
|
@@ -91,6 +91,8 @@ files:
|
|
|
91
91
|
- app/controllers/bible270/sessions_controller.rb
|
|
92
92
|
- app/helpers/bible270/plan_helper.rb
|
|
93
93
|
- app/mailers/bible270/application_mailer.rb
|
|
94
|
+
- app/mailers/bible270/broadcast.html.erb
|
|
95
|
+
- app/mailers/bible270/broadcast.text.erb
|
|
94
96
|
- app/mailers/bible270/notice_mailer.rb
|
|
95
97
|
- app/mailers/bible270/sign_in_mailer.rb
|
|
96
98
|
- app/models/bible270/application_record.rb
|
|
@@ -119,6 +121,8 @@ files:
|
|
|
119
121
|
- app/views/bible270/days/show.html.erb
|
|
120
122
|
- app/views/bible270/likes/_likes.html.erb
|
|
121
123
|
- app/views/bible270/likes/toggle.turbo_stream.erb
|
|
124
|
+
- app/views/bible270/notice_mailer/broadcast.html.erb
|
|
125
|
+
- app/views/bible270/notice_mailer/broadcast.text.erb
|
|
122
126
|
- app/views/bible270/notice_mailer/mentioned.html.erb
|
|
123
127
|
- app/views/bible270/notice_mailer/mentioned.text.erb
|
|
124
128
|
- app/views/bible270/notice_mailer/new_reader.html.erb
|