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
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
<%= b270_avatar(r, size: 40) %>
|
|
12
12
|
<div>
|
|
13
13
|
<div class="name"><%= link_to r.display_name, reader_path(r), style: "text-decoration:none" %></div>
|
|
14
|
-
<div class="sub"><%= @days_completed[r.id] %> of <%= Plan::DAYS %> days · joined <%= r.created_at.strftime("%b %Y") %></div>
|
|
14
|
+
<div class="sub"><%= @days_completed[r.id] %> of <%= Bible270::Plan::DAYS %> days · joined <%= r.created_at.strftime("%b %Y") %></div>
|
|
15
15
|
</div>
|
|
16
16
|
<div class="stat">
|
|
17
|
-
<span class="big"><%= (@days_completed[r.id].to_f / Plan::DAYS * 100).round %>%</span>
|
|
17
|
+
<span class="big"><%= (@days_completed[r.id].to_f / Bible270::Plan::DAYS * 100).round %>%</span>
|
|
18
18
|
</div>
|
|
19
19
|
</div>
|
|
20
20
|
<% end %>
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
<%= b270_avatar(@reader, size: 56) %>
|
|
4
4
|
<div>
|
|
5
5
|
<h1 style="margin:0"><%= @reader.display_name %></h1>
|
|
6
|
-
<p style="margin:2px 0 0;color:var(--faint);font-size:14px"><%= @days_completed %> of <%= Plan::DAYS %> days complete · <%= @reader.completion_percent %>%</p>
|
|
6
|
+
<p style="margin:2px 0 0;color:var(--faint);font-size:14px"><%= @days_completed %> of <%= Bible270::Plan::DAYS %> days complete · <%= @reader.completion_percent %>%</p>
|
|
7
7
|
</div>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
10
|
<div class="b270-panel" style="margin-top:18px">
|
|
11
11
|
<div class="b270-tracks">
|
|
12
|
-
<% [["ot", @reader.days_read_in("ot"), Plan::DAYS],
|
|
13
|
-
["nt", @reader.days_read_in("nt"), Plan.nt_content_days],
|
|
14
|
-
["pp", @reader.days_read_in("pp"), Plan::DAYS]].each do |track, read, total| %>
|
|
12
|
+
<% [["ot", @reader.days_read_in("ot"), Bible270::Plan::DAYS],
|
|
13
|
+
["nt", @reader.days_read_in("nt"), Bible270::Plan.nt_content_days],
|
|
14
|
+
["pp", @reader.days_read_in("pp"), Bible270::Plan::DAYS]].each do |track, read, total| %>
|
|
15
15
|
<% t = b270_track(track) %>
|
|
16
16
|
<div class="b270-tm">
|
|
17
17
|
<span class="lab"><span class="b270-rib" style="background:<%= t[:color] %>"></span><%= t[:label] %></span>
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
</div>
|
|
24
24
|
|
|
25
25
|
<div class="b270-grid">
|
|
26
|
-
<% (1..Plan::DAYS).each do |d| %>
|
|
26
|
+
<% (1..Bible270::Plan::DAYS).each do |d| %>
|
|
27
27
|
<%= link_to d, day_path(d), class: "b270-cell #{b270_day_status_class(@reader, d)}" %>
|
|
28
28
|
<% end %>
|
|
29
29
|
</div>
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
leave reflections alongside everyone else. Browsing the plan and reading others' reflections
|
|
6
6
|
never requires an account.</p>
|
|
7
7
|
|
|
8
|
+
<% if enrollment_closed? %>
|
|
9
|
+
<div class="b270-flash alert" style="margin-bottom:18px">
|
|
10
|
+
This run of the plan is closed to new readers. If you have taken part before, sign in with the
|
|
11
|
+
same email or account and you'll pick up where you left off.
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
8
15
|
<div class="b270-panel">
|
|
9
16
|
<% if Bible270.config.email_sign_in? %>
|
|
10
17
|
<h2 class="b270-subhead">Sign in with your email</h2>
|
|
@@ -13,14 +20,21 @@
|
|
|
13
20
|
<%= form_with url: email_sign_in_link_path, method: :post, class: "b270-emailform" do |f| %>
|
|
14
21
|
<%= hidden_field_tag :origin, @origin if @origin %>
|
|
15
22
|
<% if Bible270.config.email_sign_in_ask_name %>
|
|
23
|
+
<p class="b270-hint" style="margin:0">
|
|
24
|
+
If you have read with us before, just your email is enough — we already know your name.
|
|
25
|
+
</p>
|
|
26
|
+
<label class="b270-field">
|
|
27
|
+
<span>First name <em>(new readers)</em></span>
|
|
28
|
+
<%= text_field_tag :first_name, params[:first_name], autocomplete: "given-name" %>
|
|
29
|
+
</label>
|
|
16
30
|
<label class="b270-field">
|
|
17
|
-
<span>
|
|
18
|
-
<%= text_field_tag :
|
|
31
|
+
<span>Last name <em>(new readers)</em></span>
|
|
32
|
+
<%= text_field_tag :last_name, params[:last_name], autocomplete: "family-name" %>
|
|
19
33
|
</label>
|
|
20
34
|
<% end %>
|
|
21
35
|
<label class="b270-field">
|
|
22
36
|
<span>Email address</span>
|
|
23
|
-
<%= email_field_tag :email,
|
|
37
|
+
<%= email_field_tag :email, params[:email], required: true, placeholder: "you@example.com",
|
|
24
38
|
autocomplete: "email", inputmode: "email" %>
|
|
25
39
|
</label>
|
|
26
40
|
<%= f.submit "Email me a sign-in link", class: "b270-btn" %>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% if Bible270::Plan::DAYS.positive? %>
|
|
2
|
+
<details class="b270-index">
|
|
3
|
+
<summary>All <%= Bible270::Plan::DAYS %> days</summary>
|
|
4
|
+
<div class="b270-grid">
|
|
5
|
+
<% today = signed_in? ? current_reader.current_day : nil %>
|
|
6
|
+
<% (1..Bible270::Plan::DAYS).each do |d| %>
|
|
7
|
+
<%= link_to d, day_path(d),
|
|
8
|
+
class: "b270-cell #{b270_day_status_class(current_reader, d)} #{'now' if d == today}" %>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
</details>
|
|
12
|
+
<% end %>
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
<%= link_to "Plan", root_path %>
|
|
8
8
|
<%= link_to "Community", community_path %>
|
|
9
9
|
<% if signed_in? %>
|
|
10
|
+
<% if Bible270.config.admin?(current_reader) %><%= link_to "Admin", admin_path %><% end %>
|
|
10
11
|
<%= link_to current_reader.display_name, reader_path(current_reader), class: "b270-me" %>
|
|
12
|
+
<%= link_to "Profile", profile_path %>
|
|
11
13
|
<%= button_to "Sign out", sign_out_path, method: :delete, class: "b270-linkbtn",
|
|
12
14
|
form: { style: "display:inline" } %>
|
|
13
15
|
<% elsif Bible270.config.any_sign_in_method? %>
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
color:var(--ink); font-family:"Inter",system-ui,sans-serif; line-height:1.5; -webkit-font-smoothing:antialiased;
|
|
8
8
|
}
|
|
9
9
|
.b270 *{box-sizing:border-box}
|
|
10
|
+
/* Blanket anchor colour. NB: this is (0,1,1), so any single-class rule
|
|
11
|
+
colouring an <a> (.b270-btn, .b270-cell, .b270-arrow ...) must be
|
|
12
|
+
qualified with .b270 to outrank it, or the link inherits body colour. */
|
|
10
13
|
.b270 a{color:inherit}
|
|
11
14
|
.b270-wrap{max-width:820px;margin:0 auto;padding:clamp(14px,3.5vw,36px) clamp(14px,4vw,40px) 64px}
|
|
12
15
|
|
|
@@ -17,9 +20,16 @@
|
|
|
17
20
|
.b270-nav{display:flex;align-items:center;gap:16px;font-size:14px}
|
|
18
21
|
.b270-nav a{color:var(--muted);text-decoration:none}
|
|
19
22
|
.b270-nav a:hover{color:var(--gold)}
|
|
23
|
+
/* button_to renders a form; make it a flex item so the button sits on the same
|
|
24
|
+
line as the links, and drop the margin/underline the shared class carries. */
|
|
25
|
+
.b270-nav form{display:flex;align-items:center;margin:0}
|
|
26
|
+
.b270-nav .b270-linkbtn{margin:0;padding:0;font-size:14px;color:var(--muted);text-decoration:none;line-height:1}
|
|
27
|
+
.b270-nav .b270-linkbtn:hover{color:var(--gold)}
|
|
20
28
|
.b270-signin,.b270-me{color:var(--ink)!important;font-weight:600}
|
|
21
29
|
|
|
22
30
|
.b270-subhead{font-family:"Spectral",serif;font-size:17px;font-weight:600;margin:0 0 4px}
|
|
31
|
+
.b270-avatarrow{display:flex;align-items:center;gap:14px}
|
|
32
|
+
.b270-avatarrow input[type=file]{font:inherit;font-size:13px;max-width:100%}
|
|
23
33
|
.b270-emailform{display:grid;gap:12px;max-width:340px;margin:12px 0 0}
|
|
24
34
|
.b270-field{display:grid;gap:5px;font-size:12.5px;color:var(--muted)}
|
|
25
35
|
.b270-field input{font:inherit;font-size:15px;padding:10px 12px;border:1px solid var(--line);border-radius:9px;background:#fff;color:var(--ink);width:100%}
|
|
@@ -28,10 +38,10 @@
|
|
|
28
38
|
.b270-or{display:flex;align-items:center;gap:12px;margin:22px 0 18px;color:var(--faint);font-size:12px;letter-spacing:.1em;text-transform:uppercase}
|
|
29
39
|
.b270-or::before,.b270-or::after{content:"";flex:1;height:1px;background:var(--line-soft)}
|
|
30
40
|
.b270-providers{display:grid;gap:10px;max-width:340px}
|
|
31
|
-
.b270-provider{display:block;width:100%;text-align:center;border:1px solid var(--line);background:#fff;color:var(--ink);border-radius:10px;padding:12px 16px;font:inherit;font-weight:600;font-size:14.5px;cursor:pointer;transition:border-color .15s,transform .05s}
|
|
41
|
+
.b270 .b270-provider{display:block;width:100%;text-align:center;border:1px solid var(--line);background:#fff;color:var(--ink);border-radius:10px;padding:12px 16px;font:inherit;font-weight:600;font-size:14.5px;cursor:pointer;transition:border-color .15s,transform .05s}
|
|
32
42
|
.b270-provider:hover{border-color:var(--gold);color:var(--gold)}
|
|
33
43
|
.b270-provider:active{transform:translateY(1px)}
|
|
34
|
-
.b270-signinbtn{border:1px solid var(--line);background:var(--card);color:var(--ink);border-radius:8px;padding:7px 13px;font:inherit;font-weight:600;font-size:13px;cursor:pointer}
|
|
44
|
+
.b270 .b270-signinbtn{border:1px solid var(--line);background:var(--card);color:var(--ink);border-radius:8px;padding:7px 13px;font:inherit;font-weight:600;font-size:13px;cursor:pointer}
|
|
35
45
|
.b270-signinbtn:hover{border-color:var(--gold);color:var(--gold)}
|
|
36
46
|
.b270-signin-hint a{color:var(--gold);font-weight:600}
|
|
37
47
|
.b270-flash{border-radius:10px;padding:11px 15px;margin-bottom:18px;font-size:14px}
|
|
@@ -41,7 +51,7 @@
|
|
|
41
51
|
.b270-eyebrow{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--gold);font-weight:600;margin:0 0 10px}
|
|
42
52
|
.b270 h1{font-family:"Spectral",serif;font-weight:600;font-size:clamp(28px,6vw,42px);line-height:1.03;letter-spacing:-.01em;margin:0 0 6px}
|
|
43
53
|
.b270 h1 em{font-style:italic;color:var(--muted);font-weight:400}
|
|
44
|
-
.b270 .lede{color:var(--muted);max-width:
|
|
54
|
+
.b270 .lede{color:var(--muted);max-width:77ch;margin:0 0 24px;font-size:15px}
|
|
45
55
|
|
|
46
56
|
.b270-panel{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:20px 22px;box-shadow:var(--shadow);margin-bottom:24px}
|
|
47
57
|
.b270-bignum{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
|
|
@@ -72,7 +82,7 @@
|
|
|
72
82
|
|
|
73
83
|
/* day nav */
|
|
74
84
|
.b270-daynav{display:flex;align-items:center;gap:10px;margin-bottom:12px}
|
|
75
|
-
.b270-arrow{width:38px;height:38px;border-radius:10px;border:1px solid var(--line);background:var(--card);cursor:pointer;font-size:17px;color:var(--muted);display:grid;place-items:center;text-decoration:none}
|
|
85
|
+
.b270 .b270-arrow{width:38px;height:38px;border-radius:10px;border:1px solid var(--line);background:var(--card);cursor:pointer;font-size:17px;color:var(--muted);display:grid;place-items:center;text-decoration:none}
|
|
76
86
|
.b270-arrow:hover{border-color:var(--gold);color:var(--gold)}
|
|
77
87
|
.b270-arrow.disabled{opacity:.35;pointer-events:none}
|
|
78
88
|
.b270-dayhead{flex:1}
|
|
@@ -101,7 +111,7 @@
|
|
|
101
111
|
.b270-ref a:hover{border-bottom-color:var(--gold)}
|
|
102
112
|
.b270-reading.done .b270-ref{color:var(--muted)}
|
|
103
113
|
.b270-reading.rest .b270-ref{font-style:italic;font-size:16px;color:var(--faint)}
|
|
104
|
-
.b270-signin-hint{font-size:12px;color:var(--faint)}
|
|
114
|
+
.b270 .b270-signin-hint{font-size:12px;color:var(--faint)}
|
|
105
115
|
|
|
106
116
|
/* completers */
|
|
107
117
|
.b270-completers{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:16px;font-size:13px;color:var(--muted)}
|
|
@@ -118,7 +128,7 @@
|
|
|
118
128
|
.b270-cdel{font-size:12px;color:var(--oxblood);background:none;border:none;cursor:pointer;padding:0;margin-left:8px}
|
|
119
129
|
.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}
|
|
120
130
|
.b270-form .row{display:flex;align-items:center;gap:10px;margin-top:10px}
|
|
121
|
-
.b270-btn{border:none;border-radius:10px;padding:10px 16px;font:inherit;font-weight:600;font-size:14px;cursor:pointer;background:var(--ink);color:var(--paper);text-decoration:none;display:inline-block}
|
|
131
|
+
.b270 .b270-btn{border:none;border-radius:10px;padding:10px 16px;font:inherit;font-weight:600;font-size:14px;cursor:pointer;background:var(--ink);color:var(--paper);text-decoration:none;display:inline-block}
|
|
122
132
|
.b270-btn:hover{background:#3a352b}
|
|
123
133
|
|
|
124
134
|
/* community */
|
|
@@ -133,7 +143,7 @@
|
|
|
133
143
|
|
|
134
144
|
/* calendar grid */
|
|
135
145
|
.b270-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(30px,1fr));gap:6px;margin-top:16px}
|
|
136
|
-
.b270-cell{aspect-ratio:1;border-radius:6px;border:1px solid var(--line);background:var(--card);font-size:10.5px;color:var(--faint);display:grid;place-items:center;text-decoration:none;font-variant-numeric:tabular-nums}
|
|
146
|
+
.b270 .b270-cell{aspect-ratio:1;border-radius:6px;border:1px solid var(--line);background:var(--card);font-size:10.5px;color:var(--faint);display:grid;place-items:center;text-decoration:none;font-variant-numeric:tabular-nums}
|
|
137
147
|
.b270-cell:hover{border-color:var(--gold);color:var(--gold)}
|
|
138
148
|
.b270-cell.partial{background:linear-gradient(135deg,var(--card) 50%,rgba(165,129,44,.35) 50%);color:var(--muted)}
|
|
139
149
|
.b270-cell.complete{background:var(--teal);border-color:var(--teal);color:#eef3f2;font-weight:600}
|
|
@@ -18,11 +18,9 @@
|
|
|
18
18
|
<% if flash[:alert] %><div class="b270-flash alert"><%= flash[:alert] %></div><% end %>
|
|
19
19
|
<% if flash[:notice] %><div class="b270-flash notice"><%= flash[:notice] %></div><% end %>
|
|
20
20
|
<%= yield %>
|
|
21
|
+
<%= render 'bible270/shared/day_index' %>
|
|
21
22
|
<footer class="b270-footer">
|
|
22
|
-
<p>The Old Testament
|
|
23
|
-
reading from all three tracks every day. Alongside them the Psalms run once and
|
|
24
|
-
Proverbs twice; the longest chapters are divided so the readings fill each day
|
|
25
|
-
evenly, and Psalm 119 comes in eleven sections.</p>
|
|
23
|
+
<p>The Old Testament, New Testament and Psalms/Proverbs are each read once across the 270 days. Long chapters are divided so the readings fill each day evenly, e.g. Psalm 119 comes in eleven sections.</p>
|
|
26
24
|
</footer>
|
|
27
25
|
</div>
|
|
28
26
|
</body>
|
data/config/routes.rb
CHANGED
|
@@ -1,30 +1,50 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
Bible270::Engine.routes.draw do
|
|
3
|
-
root to:
|
|
4
|
+
root to: 'days#index'
|
|
5
|
+
|
|
6
|
+
get 'day/:day', to: 'days#show', as: :day, constraints: { day: %r{\d+} }
|
|
7
|
+
|
|
8
|
+
post 'day/:day/toggle/:track', to: 'checkoffs#toggle', as: :toggle_checkoff,
|
|
9
|
+
constraints: { day: %r{\d+} }
|
|
10
|
+
post 'day/:day/comments', to: 'comments#create', as: :day_comments,
|
|
11
|
+
constraints: { day: %r{\d+} }
|
|
12
|
+
delete 'comments/:id', to: 'comments#destroy', as: :comment
|
|
13
|
+
|
|
14
|
+
# Admin panel — 404s unless config.admin_emails / admin_resolver allows you.
|
|
15
|
+
get 'admin', to: 'admin#index', as: :admin
|
|
16
|
+
get 'admin/readers/:id', to: 'admin#show', as: :admin_reader
|
|
17
|
+
delete 'admin/readers/:id', to: 'admin#destroy'
|
|
18
|
+
patch 'admin/readers/:id/name', to: 'admin#update_name', as: :admin_reader_name
|
|
19
|
+
patch 'admin/readers/:id/start', to: 'admin#update_start', as: :admin_reader_start
|
|
20
|
+
patch 'admin/readers/:id/through', to: 'admin#complete_through', as: :admin_reader_through
|
|
21
|
+
post 'admin/readers/:id/days/:day', to: 'admin#toggle_day', as: :admin_reader_day
|
|
4
22
|
|
|
5
|
-
|
|
23
|
+
patch 'admin/enrollment', to: 'admin#update_enrollment', as: :admin_enrollment
|
|
24
|
+
get 'admin/comments', to: 'admin#comments', as: :admin_comments
|
|
25
|
+
patch 'admin/comments/:id/hide', to: 'admin#hide_comment', as: :admin_hide_comment
|
|
26
|
+
patch 'admin/comments/:id/unhide', to: 'admin#unhide_comment', as: :admin_unhide_comment
|
|
27
|
+
delete 'admin/comments/:id', to: 'admin#destroy_comment', as: :admin_comment
|
|
6
28
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
constraints: { day: /\d+/ }
|
|
11
|
-
delete "comments/:id", to: "comments#destroy", as: :comment
|
|
29
|
+
get 'profile', to: 'profiles#edit', as: :profile
|
|
30
|
+
patch 'profile', to: 'profiles#update'
|
|
31
|
+
delete 'profile/avatar', to: 'profiles#remove_avatar', as: :profile_avatar
|
|
12
32
|
|
|
13
|
-
get
|
|
14
|
-
get
|
|
33
|
+
get 'community', to: 'readers#index', as: :community
|
|
34
|
+
get 'readers/:id', to: 'readers#show', as: :reader
|
|
15
35
|
|
|
16
|
-
patch
|
|
17
|
-
delete
|
|
36
|
+
patch 'start-date', to: 'readers#update_start_date', as: :start_date
|
|
37
|
+
delete 'start-date', to: 'readers#clear_start_date', as: :clear_start_date
|
|
18
38
|
|
|
19
39
|
# Built-in OmniAuth sign-in. The request phase (POST <mount>/auth/:provider)
|
|
20
40
|
# is handled by the OmniAuth middleware in the host app, not by these routes.
|
|
21
|
-
get
|
|
41
|
+
get 'sign_in', to: 'sessions#new', as: :sign_in
|
|
22
42
|
|
|
23
43
|
# Email (magic link) sign-in, for readers without a social account.
|
|
24
|
-
post
|
|
25
|
-
get
|
|
26
|
-
get
|
|
27
|
-
post
|
|
28
|
-
get
|
|
29
|
-
delete
|
|
44
|
+
post 'sign_in/email', to: 'sessions#email_link', as: :email_sign_in_link
|
|
45
|
+
get 'sign_in/email/:token', to: 'sessions#email_callback', as: :email_sign_in
|
|
46
|
+
get 'auth/:provider/callback', to: 'sessions#create'
|
|
47
|
+
post 'auth/:provider/callback', to: 'sessions#create'
|
|
48
|
+
get 'auth/failure', to: 'sessions#failure'
|
|
49
|
+
delete 'sign_out', to: 'sessions#destroy', as: :sign_out
|
|
30
50
|
end
|
|
@@ -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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateBible270Settings < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
# A tiny key/value store for state an admin can change at runtime. Closing a
|
|
6
|
+
# run to new readers can't live in config, since that would need a deploy.
|
|
7
|
+
create_table :bible270_settings do |t|
|
|
8
|
+
t.string :key, null: false
|
|
9
|
+
t.string :value
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :bible270_settings, :key, unique: true, name: 'idx_b270_settings_key'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bible270
|
|
4
|
+
# What counts as an acceptable avatar. Kept free of Rails and of Active Storage
|
|
5
|
+
# so the rules can be unit tested, and so the engine still loads in an app that
|
|
6
|
+
# has Active Storage disabled.
|
|
7
|
+
module Avatars
|
|
8
|
+
CONTENT_TYPES = %w[image/png image/jpeg image/gif image/webp].freeze
|
|
9
|
+
|
|
10
|
+
# Two megabytes. Large enough for a photo off a phone, small enough that
|
|
11
|
+
# nobody's profile page crawls.
|
|
12
|
+
DEFAULT_MAX_BYTES = 2 * 1024 * 1024
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def content_types = CONTENT_TYPES
|
|
17
|
+
|
|
18
|
+
def max_bytes
|
|
19
|
+
configured = Bible270.config.avatar_max_bytes if Bible270.config.respond_to?(:avatar_max_bytes)
|
|
20
|
+
configured.to_i.positive? ? configured.to_i : DEFAULT_MAX_BYTES
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def acceptable_type?(content_type)
|
|
24
|
+
CONTENT_TYPES.include?(content_type.to_s.downcase.split(';').first.to_s.strip)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def acceptable_size?(byte_size)
|
|
28
|
+
size = byte_size.to_i
|
|
29
|
+
size.positive? && size <= max_bytes
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# nil when the upload is fine, otherwise the reason — phrased for a reader.
|
|
33
|
+
def problem_with(content_type:, byte_size:)
|
|
34
|
+
return 'must be a PNG, JPEG, GIF or WebP image' unless acceptable_type?(content_type)
|
|
35
|
+
return 'looks empty' unless byte_size.to_i.positive?
|
|
36
|
+
return "must be under #{human_size(max_bytes)}" unless acceptable_size?(byte_size)
|
|
37
|
+
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def human_size(bytes)
|
|
42
|
+
mb = bytes.to_f / (1024 * 1024)
|
|
43
|
+
mb >= 1 ? "#{format('%g', mb.round(1))}MB" : "#{(bytes.to_f / 1024).round}KB"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|