Class: Bible270::Reader

Inherits:
ApplicationRecord show all
Defined in:
app/models/bible270/reader.rb

Overview

A reader identity. Either self-contained (created via OmniAuth) or bridged to one of the host application's users through the polymorphic :owner.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.for_owner(owner, display_name:, email: nil, avatar_url: nil) ⇒ Object

Find or create a reader bridged to a host user (or any model).



70
71
72
73
74
75
76
77
# File 'app/models/bible270/reader.rb', line 70

def self.for_owner(owner, display_name:, email: nil, avatar_url: nil)
  reader = find_or_initialize_by(owner: owner)
  reader.display_name = display_name.presence || reader.display_name || "Reader"
  reader.email      ||= email
  reader.avatar_url ||= avatar_url
  reader.save!
  reader
end

.from_email(email, display_name: nil) ⇒ Object

Find or create the reader behind a verified email address. Uses the same provider/uid identity columns as OmniAuth, with provider "email", so an email reader is indistinguishable from any other downstream.



39
40
41
42
43
44
45
46
47
48
49
50
# File 'app/models/bible270/reader.rb', line 39

def self.from_email(email, display_name: nil)
  address = EmailSignIn.normalize_email(email)
  return nil if address.nil?

  reader = find_or_initialize_by(provider: "email", uid: address)
  chosen = first_present(display_name, reader.display_name,
                         EmailSignIn.display_name_from(address), "Reader")
  reader.display_name = chosen
  reader.email = address
  reader.save
  reader
end

.from_omniauth(auth) ⇒ Object

Build/refresh a reader from an OmniAuth auth hash. Tolerant of the various shapes strategies return (OmniAuth::AuthHash, plain Hash, missing info).



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'app/models/bible270/reader.rb', line 17

def self.from_omniauth(auth)
  provider = dig_auth(auth, :provider).to_s
  uid      = dig_auth(auth, :uid).to_s
  return nil if provider.empty? || uid.empty?

  info = dig_auth(auth, :info) || {}
  name  = first_present(dig_auth(info, :name), dig_auth(info, :nickname),
                        dig_auth(info, :first_name), dig_auth(info, :email))

  reader = find_or_initialize_by(provider: provider, uid: uid)
  reader.display_name = first_present(name, reader.display_name, "Reader")
  email = dig_auth(info, :email)
  image = first_present(dig_auth(info, :image), dig_auth(info, :avatar_url))
  reader.email      = email if email.present?
  reader.avatar_url = image if image.present?
  reader.save
  reader
end

Instance Method Details

#calendar_dayObject

The plan day that today corresponds to (clamped into range), or nil when undated.



144
145
146
# File 'app/models/bible270/reader.rb', line 144

def calendar_day
  Plan.day_for(Date.current, effective_start_date)
end

#checked_countsObject

=> number of tracks checked off



84
85
86
# File 'app/models/bible270/reader.rb', line 84

def checked_counts
  @checked_counts ||= checkoffs.group(:day).count
end

#clear_start_date!Object



196
197
198
# File 'app/models/bible270/reader.rb', line 196

def clear_start_date!
  update!(started_on: nil)
end

#completion_percentObject



109
110
111
# File 'app/models/bible270/reader.rb', line 109

def completion_percent
  (days_completed.to_f / Plan::DAYS * 100).round
end

#current_dayObject

First day not yet fully complete (where the reader "is").



114
115
116
# File 'app/models/bible270/reader.rb', line 114

def current_day
  (1..Plan::DAYS).find { |d| !day_complete?(d) } || Plan::DAYS
end

#date_for_day(day) ⇒ Object



165
166
167
# File 'app/models/bible270/reader.rb', line 165

def date_for_day(day)
  Plan.date_for(day, effective_start_date)
end

#dated?Boolean

Returns:

  • (Boolean)


134
135
136
# File 'app/models/bible270/reader.rb', line 134

def dated?
  effective_start_date.present?
end

#day_complete?(day) ⇒ Boolean

Returns:

  • (Boolean)


96
97
98
99
# File 'app/models/bible270/reader.rb', line 96

def day_complete?(day)
  n = checked_counts[day].to_i
  n.positive? && n >= Plan.required_track_count(day)
end

#days_completedObject



101
102
103
# File 'app/models/bible270/reader.rb', line 101

def days_completed
  checked_counts.count { |day, n| n >= Plan.required_track_count(day) }
end

#days_off_paceObject

How many days behind (positive) or ahead (negative) of the calendar the reader's actual progress is. Nil when undated.



171
172
173
174
175
176
# File 'app/models/bible270/reader.rb', line 171

def days_off_pace
  today = calendar_day
  return nil unless today

  today - days_completed
end

#days_read_in(track) ⇒ Object



105
106
107
# File 'app/models/bible270/reader.rb', line 105

def days_read_in(track)
  checkoffs.where(track: track.to_s).count
end

#effective_start_dateObject

The start date that actually governs this reader. A reader's own started_on wins when per-reader dates are allowed; otherwise (or if they haven't got one) the community-wide config.start_date applies. Nil means the plan is undated for this reader and no calendar mapping exists.



125
126
127
128
129
130
131
132
# File 'app/models/bible270/reader.rb', line 125

def effective_start_date
  config = Bible270.config
  if config.allow_reader_start_date && started_on
    started_on
  else
    config.start_date
  end
end

#ensure_started!Object

Called when a reader first participates. Only stamps a personal start date when per-reader dates are enabled and a shared date isn't already in force.



202
203
204
205
206
207
208
209
# File 'app/models/bible270/reader.rb', line 202

def ensure_started!
  config = Bible270.config
  return unless config.allow_reader_start_date
  return if started_on.present?
  return if config.start_date.present?

  update!(started_on: Date.current)
end

#initialsObject



79
80
81
# File 'app/models/bible270/reader.rb', line 79

def initials
  display_name.to_s.split(/\s+/).first(2).map { |w| w[0] }.join.upcase.presence || "?"
end

#not_started_yet?Boolean

Returns:

  • (Boolean)


153
154
155
# File 'app/models/bible270/reader.rb', line 153

def not_started_yet?
  Plan.before_start?(Date.current, effective_start_date)
end

#own_start_date?Boolean

Whether this reader is following a personal date or the shared cohort one.

Returns:

  • (Boolean)


139
140
141
# File 'app/models/bible270/reader.rb', line 139

def own_start_date?
  Bible270.config.allow_reader_start_date && started_on.present?
end

#past_end_date?Boolean

Returns:

  • (Boolean)


157
158
159
# File 'app/models/bible270/reader.rb', line 157

def past_end_date?
  Plan.after_end?(Date.current, effective_start_date)
end

#plan_end_dateObject



161
162
163
# File 'app/models/bible270/reader.rb', line 161

def plan_end_date
  Plan.end_date_for(effective_start_date)
end

#raw_calendar_dayObject

Raw, unclamped — lets callers distinguish "not started yet" / "finished".



149
150
151
# File 'app/models/bible270/reader.rb', line 149

def raw_calendar_day
  Plan.day_for(Date.current, effective_start_date, clamp: false)
end

#read?(day, track) ⇒ Boolean

Returns:

  • (Boolean)


92
93
94
# File 'app/models/bible270/reader.rb', line 92

def read?(day, track)
  read_tracks_for(day).include?(track.to_s)
end

#read_tracks_for(day) ⇒ Object



88
89
90
# File 'app/models/bible270/reader.rb', line 88

def read_tracks_for(day)
  checkoffs.where(day: day).pluck(:track)
end

#start_dateObject



183
184
185
# File 'app/models/bible270/reader.rb', line 183

def start_date
  started_on
end

#start_date=(value) ⇒ Object

Set or change this reader's own start date. Accepts a Date or a string.



179
180
181
# File 'app/models/bible270/reader.rb', line 179

def start_date=(value)
  self.started_on = Plan.to_date(value)
end

#update_start_date!(value) ⇒ Object



187
188
189
190
191
192
193
194
# File 'app/models/bible270/reader.rb', line 187

def update_start_date!(value)
  return false unless Bible270.config.allow_reader_start_date

  date = Plan.to_date(value)
  return false if date.nil?

  update!(started_on: date)
end