Class: Bible270::Reader
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Bible270::Reader
- 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.
Constant Summary collapse
- AVATAR_UPLOADS =
Active Storage is optional: an app may have it disabled, and the engine has to keep working there — readers simply can't upload, and any avatar from their sign-in provider is used instead.
respond_to?(:has_one_attached)
- PASSAGE_SOURCES =
%w[bible_gateway blue_letter_bible].freeze
- DEFAULT_PASSAGE_SOURCE =
'bible_gateway'- DAILY_REMINDER_TIME_FORMAT =
%r{\A(?:[01]\d|2[0-3]):[0-5]\d\z}- DAILY_REMINDER_MINUTES =
%w[00 15 30 45].freeze
- DAILY_REMINDER_COLUMNS =
%w[daily_reminders daily_reminder_time last_daily_reminder_sent_on].freeze
- REFLECTIONS_SEEN_COLUMN =
'reflections_seen_at'
Class Method Summary collapse
- .avatar_uploads? ⇒ Boolean
- .completed_days_by_id ⇒ Object
- .daily_reminder_columns? ⇒ Boolean
-
.email_reader_exists?(email) ⇒ Boolean
Find or create the reader behind a verified email address.
-
.for_owner(owner, display_name:, email: nil, avatar_url: nil) ⇒ Object
Find or create a reader bridged to a host user (or any model).
- .from_email(email, first_name: nil, last_name: nil, display_name: nil) ⇒ Object
-
.from_omniauth(auth) ⇒ Object
Build/refresh a reader from an OmniAuth auth hash.
- .from_remember_cookie(reader_id, token) ⇒ Object
-
.mentioned_in(text) ⇒ Object
The readers a piece of text mentions.
- .omniauth_reader_exists?(provider, uid) ⇒ Boolean
- .reflections_seen_column? ⇒ Boolean
- .valid_daily_reminder_time?(value) ⇒ Boolean
Instance Method Summary collapse
- #answers_to?(handle) ⇒ Boolean
-
#attach_avatar(upload) ⇒ Object
Returns false and sets an error when the upload isn't acceptable.
- #avatar_uploaded? ⇒ Boolean
- #bible_gateway? ⇒ Boolean
- #bible_version_label ⇒ Object
- #blue_letter_bible? ⇒ Boolean
-
#calendar_day ⇒ Object
The plan day that today corresponds to (clamped into range), or nil when undated.
-
#checked_count(day) ⇒ Object
Tracks ticked on a day, counted from the single grouped query in checked_counts.
-
#checked_counts ⇒ Object
=> number of tracks checked off.
- #clear_day!(day) ⇒ Object
- #completion_percent ⇒ Object
-
#current_day ⇒ Object
First day not yet fully complete (where the reader "is").
- #daily_reminder_due_at?(local_time) ⇒ Boolean
- #date_for_day(day) ⇒ Object
- #dated? ⇒ Boolean
- #day_complete?(day) ⇒ Boolean
- #day_status(day) ⇒ Object
- #days_completed ⇒ Object
-
#days_off_pace ⇒ Object
How many days behind (positive) or ahead (negative) of the calendar the reader's actual progress is.
-
#days_read_in(track) ⇒ Object
Days on which this track is finished.
-
#effective_bible_version ⇒ Object
The translation this reader reads in.
-
#effective_start_date ⇒ Object
The start date that actually governs this reader.
-
#ensure_started! ⇒ Object
Called when a reader first participates.
-
#first_with_last_initial ⇒ Object
Shorter than the full name, for lists where a surname is more than needed.
-
#forget! ⇒ Object
Rotating the token invalidates every device at once.
- #full_name ⇒ Object
- #initials ⇒ Object
-
#mark_day_complete!(day) ⇒ Object
Tick every track that has content on this day.
- #mark_reflections_seen!(at) ⇒ Object
-
#mark_through!(day) ⇒ Object
Mark everything up to and including
daycomplete, and clear anything after. - #not_started_yet? ⇒ Boolean
-
#own_start_date? ⇒ Boolean
Whether this reader is following a personal date or the shared cohort one.
- #partial_days ⇒ Object
- #past_end_date? ⇒ Boolean
- #plan_end_date ⇒ Object
- #raw_calendar_day ⇒ Object
- #read?(day, track, part = nil) ⇒ Boolean
-
#read_parts_for(day, track) ⇒ Object
Which chapters of a track the reader has ticked on this day.
- #read_tracks_for(day) ⇒ Object
- #reload_progress ⇒ Object
- #remaining_parts_for(day) ⇒ Object
-
#remember_token! ⇒ Object
Generated on first use rather than at sign-up, so readers who never stay signed in never carry one.
- #remove_avatar! ⇒ Object
-
#restart_on!(day:, on: Bible270.today) ⇒ Object
Put this reader on
dayas ofon— i.e. -
#set_start_date!(value) ⇒ Object
Set an individual start date for administrative use.
-
#sort_name ⇒ Object
Readers are listed by first name, matching how they are shown — the display name is "First Last", so sorting on it needs only case folding.
- #start_date ⇒ Object
-
#start_date=(value) ⇒ Object
Set or change this reader's own start date.
-
#suggested_names ⇒ Object
Fill first/last from the display name where we only have one string, e.g.
- #today?(day) ⇒ Boolean
-
#today_day ⇒ Object
Raw, unclamped — lets callers distinguish "not started yet" / "finished".
- #toggle_day!(day) ⇒ Object
- #track_partially_read?(day, track) ⇒ Boolean
- #update_bible_version(code) ⇒ Object
-
#update_names(first, last) ⇒ Object
Day number implied by the calendar, if a start date is set.
- #update_passage_source(source) ⇒ Object
-
#wants_comment_notifications? ⇒ Boolean
Older copied databases may briefly lack the preference column while their migrations are being reconciled.
Class Method Details
.avatar_uploads? ⇒ Boolean
20 |
# File 'app/models/bible270/reader.rb', line 20 def self.avatar_uploads? = AVATAR_UPLOADS |
.completed_days_by_id ⇒ Object
58 59 60 61 62 |
# File 'app/models/bible270/reader.rb', line 58 def self.completed_days_by_id Checkoff.group(:reader_id, :day).count.each_with_object(Hash.new(0)) do |((reader_id, day), done), totals| totals[reader_id] += 1 if done >= Plan.total_parts(day) end end |
.daily_reminder_columns? ⇒ Boolean
48 49 50 51 52 |
# File 'app/models/bible270/reader.rb', line 48 def self.daily_reminder_columns? (DAILY_REMINDER_COLUMNS - column_names).empty? rescue ActiveRecord::StatementInvalid false end |
.email_reader_exists?(email) ⇒ Boolean
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. Used when enrolment is closed: an existing reader may still sign in, a new one may not be created.
144 145 146 147 148 149 |
# File 'app/models/bible270/reader.rb', line 144 def self.email_reader_exists?(email) address = EmailSignIn.normalize_email(email) return false if address.nil? exists?(provider: 'email', uid: address) end |
.for_owner(owner, display_name:, email: nil, avatar_url: nil) ⇒ Object
Find or create a reader bridged to a host user (or any model).
190 191 192 193 194 195 196 197 |
# File 'app/models/bible270/reader.rb', line 190 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, first_name: nil, last_name: nil, display_name: nil) ⇒ Object
157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'app/models/bible270/reader.rb', line 157 def self.from_email(email, first_name: nil, last_name: nil, display_name: nil) address = EmailSignIn.normalize_email(email) return nil if address.nil? reader = find_or_initialize_by(provider: 'email', uid: address) reader.first_name = first_name.to_s.strip if first_name.present? reader.last_name = last_name.to_s.strip if last_name.present? reader.display_name = first_present(reader.full_name, display_name, reader.display_name, EmailSignIn.display_name_from(address), 'Reader') 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).
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'app/models/bible270/reader.rb', line 93 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? assign_names_from(reader, info) reader.save reader end |
.from_remember_cookie(reader_id, token) ⇒ Object
290 291 292 293 294 295 296 297 298 299 300 |
# File 'app/models/bible270/reader.rb', line 290 def self.(reader_id, token) return nil if reader_id.blank? || token.blank? reader = find_by(id: reader_id) return nil if reader.nil? || reader.remember_token.blank? # Constant-time comparison: the token is a credential. return nil unless ActiveSupport::SecurityUtils.secure_compare(reader.remember_token, token.to_s) reader end |
.mentioned_in(text) ⇒ Object
The readers a piece of text mentions. A handle matching more than one reader resolves to nobody: mailing the wrong person is worse than mailing none, and the writer sees the mention left unlinked.
257 258 259 260 261 262 263 264 265 266 |
# File 'app/models/bible270/reader.rb', line 257 def self.mentioned_in(text) handles = Mentions.extract(text) return [] if handles.empty? candidates = where.not(first_name: [nil, '']).to_a handles.filter_map do |handle| matches = candidates.select { |reader| reader.answers_to?(handle) } matches.first if matches.one? end.uniq end |
.omniauth_reader_exists?(provider, uid) ⇒ Boolean
151 152 153 154 155 |
# File 'app/models/bible270/reader.rb', line 151 def self.omniauth_reader_exists?(provider, uid) return false if provider.blank? || uid.blank? exists?(provider: provider.to_s, uid: uid.to_s) end |
.reflections_seen_column? ⇒ Boolean
64 65 66 67 68 |
# File 'app/models/bible270/reader.rb', line 64 def self.reflections_seen_column? column_names.include?(REFLECTIONS_SEEN_COLUMN) rescue ActiveRecord::StatementInvalid false end |
.valid_daily_reminder_time?(value) ⇒ Boolean
54 55 56 |
# File 'app/models/bible270/reader.rb', line 54 def self.valid_daily_reminder_time?(value) value.to_s.match?(DAILY_REMINDER_TIME_FORMAT) end |
Instance Method Details
#answers_to?(handle) ⇒ Boolean
268 269 270 |
# File 'app/models/bible270/reader.rb', line 268 def answers_to?(handle) Mentions.handles_for(first_name, last_name).include?(handle) end |
#attach_avatar(upload) ⇒ Object
Returns false and sets an error when the upload isn't acceptable.
307 308 309 310 311 312 313 314 315 316 317 318 |
# File 'app/models/bible270/reader.rb', line 307 def attach_avatar(upload) return false unless self.class.avatar_uploads? problem = Avatars.problem_with(content_type: upload.content_type, byte_size: upload.size) if problem errors.add(:avatar, problem) return false end avatar.attach(upload) true end |
#avatar_uploaded? ⇒ Boolean
302 303 304 |
# File 'app/models/bible270/reader.rb', line 302 def avatar_uploaded? self.class.avatar_uploads? && avatar.attached? end |
#bible_gateway? ⇒ Boolean
234 |
# File 'app/models/bible270/reader.rb', line 234 def bible_gateway? = passage_source == 'bible_gateway' |
#bible_version_label ⇒ Object
217 218 219 |
# File 'app/models/bible270/reader.rb', line 217 def bible_version_label Translations.label(effective_bible_version) end |
#blue_letter_bible? ⇒ Boolean
235 |
# File 'app/models/bible270/reader.rb', line 235 def blue_letter_bible? = passage_source == 'blue_letter_bible' |
#calendar_day ⇒ Object
The plan day that today corresponds to (clamped into range), or nil when undated.
544 545 546 |
# File 'app/models/bible270/reader.rb', line 544 def calendar_day Plan.day_for(Bible270.today, effective_start_date) end |
#checked_count(day) ⇒ Object
Tracks ticked on a day, counted from the single grouped query in checked_counts. Use this rather than read_tracks_for when rendering a grid: read_tracks_for costs a query per day, which is 270 of them per page.
337 338 339 |
# File 'app/models/bible270/reader.rb', line 337 def checked_count(day) checked_counts[day].to_i end |
#checked_counts ⇒ Object
=> number of tracks checked off
330 331 332 |
# File 'app/models/bible270/reader.rb', line 330 def checked_counts @checked_counts ||= checkoffs.group(:day).count end |
#clear_day!(day) ⇒ Object
461 462 463 464 465 466 467 |
# File 'app/models/bible270/reader.rb', line 461 def clear_day!(day) return false unless Plan.valid_day?(day) checkoffs.where(day: day).destroy_all reload_progress true end |
#completion_percent ⇒ Object
397 398 399 |
# File 'app/models/bible270/reader.rb', line 397 def completion_percent (days_completed.to_f / Plan::DAYS * 100).round end |
#current_day ⇒ Object
First day not yet fully complete (where the reader "is").
402 403 404 |
# File 'app/models/bible270/reader.rb', line 402 def current_day (1..Plan::DAYS).find { |d| !day_complete?(d) } || Plan::DAYS end |
#daily_reminder_due_at?(local_time) ⇒ Boolean
79 80 81 82 83 84 85 86 87 88 89 |
# File 'app/models/bible270/reader.rb', line 79 def daily_reminder_due_at?(local_time) return false unless self.class.daily_reminder_columns? return false unless daily_reminders? return false unless self.class.valid_daily_reminder_time?(daily_reminder_time) return false unless local_time.respond_to?(:hour) && local_time.respond_to?(:min) hour, minute = daily_reminder_time.split(':').map(&:to_i) (local_time.hour * 60) + local_time.min >= (hour * 60) + minute rescue StandardError false end |
#date_for_day(day) ⇒ Object
579 580 581 |
# File 'app/models/bible270/reader.rb', line 579 def date_for_day(day) Plan.date_for(day, effective_start_date) end |
#dated? ⇒ Boolean
534 535 536 |
# File 'app/models/bible270/reader.rb', line 534 def dated? effective_start_date.present? end |
#day_complete?(day) ⇒ Boolean
381 382 383 384 |
# File 'app/models/bible270/reader.rb', line 381 def day_complete?(day) n = checked_counts[day].to_i n.positive? && n >= Plan.total_parts(day) end |
#day_status(day) ⇒ Object
341 342 343 344 345 346 |
# File 'app/models/bible270/reader.rb', line 341 def day_status(day) done = checked_count(day) return :none if done.zero? done >= Plan.total_parts(day) ? :complete : :partial end |
#days_completed ⇒ Object
386 387 388 |
# File 'app/models/bible270/reader.rb', line 386 def days_completed checked_counts.count { |day, n| n >= Plan.total_parts(day) } end |
#days_off_pace ⇒ Object
How many days behind (positive) or ahead (negative) of the calendar the reader's actual progress is. Nil when undated.
585 586 587 588 589 590 |
# File 'app/models/bible270/reader.rb', line 585 def days_off_pace today = calendar_day return nil unless today today - days_completed end |
#days_read_in(track) ⇒ Object
Days on which this track is finished. Counting rows would count chapters now that an Old Testament reading has one per chapter.
392 393 394 395 |
# File 'app/models/bible270/reader.rb', line 392 def days_read_in(track) checkoffs.where(track: track.to_s).group(:day).count .count { |day, done| done >= Plan.part_count(day, track) } end |
#effective_bible_version ⇒ Object
The translation this reader reads in. Null means "whatever the site default is", so changing config.bible_version moves everyone who hasn't chosen.
213 214 215 |
# File 'app/models/bible270/reader.rb', line 213 def effective_bible_version Translations.resolve(bible_version) end |
#effective_start_date ⇒ Object
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 current run's shared date applies. Nil means the plan is undated for this reader and no calendar mapping exists.
526 527 528 529 530 531 532 |
# File 'app/models/bible270/reader.rb', line 526 def effective_start_date if Bible270.config.allow_reader_start_date && started_on started_on else Setting.run_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.
614 615 616 617 618 619 620 |
# File 'app/models/bible270/reader.rb', line 614 def ensure_started! return unless Bible270.config.allow_reader_start_date return if started_on.present? return if Setting.run_start_date.present? update!(started_on: Bible270.today) end |
#first_with_last_initial ⇒ Object
Shorter than the full name, for lists where a surname is more than needed. Falls back to the display name for readers who arrived with only one.
205 206 207 208 209 |
# File 'app/models/bible270/reader.rb', line 205 def first_with_last_initial return display_name if first_name.blank? Names.first_with_last_initial(first_name, last_name).presence || display_name end |
#forget! ⇒ Object
Rotating the token invalidates every device at once.
285 286 287 288 |
# File 'app/models/bible270/reader.rb', line 285 def forget! update_column(:remember_token, nil) true end |
#full_name ⇒ Object
199 200 201 |
# File 'app/models/bible270/reader.rb', line 199 def full_name [first_name, last_name].map { |n| n.to_s.strip }.reject(&:empty?).join(' ').presence end |
#initials ⇒ Object
325 326 327 |
# File 'app/models/bible270/reader.rb', line 325 def initials display_name.to_s.split(%r{\s+}).first(2).map { |w| w[0] }.join.upcase.presence || '?' end |
#mark_day_complete!(day) ⇒ Object
Tick every track that has content on this day.
427 428 429 430 431 432 433 434 435 436 437 438 439 440 |
# File 'app/models/bible270/reader.rb', line 427 def mark_day_complete!(day) return false unless Plan.valid_day?(day) # Deliberately not find_or_create_by!: since Rails 8.1 that creates first # and rescues RecordNotUnique, but Checkoff validates uniqueness, so a # duplicate raises RecordInvalid before the database is reached and is # never rescued. Reading what exists first is also one query per day rather # than one per chapter. missing = missing_parts_on(day) insert_checkoffs(day, missing) if missing.any? reload_progress true end |
#mark_reflections_seen!(at) ⇒ Object
70 71 72 73 74 75 76 77 |
# File 'app/models/bible270/reader.rb', line 70 def mark_reflections_seen!(at) return false unless self.class.reflections_seen_column? update_column(:reflections_seen_at, at) true rescue ActiveRecord::StatementInvalid false end |
#mark_through!(day) ⇒ Object
Mark everything up to and including day complete, and clear anything after.
474 475 476 477 478 479 480 481 482 483 484 |
# File 'app/models/bible270/reader.rb', line 474 def mark_through!(day) day = day.to_i return false unless day.between?(0, Plan::DAYS) transaction do checkoffs.where(day: (day + 1)..).delete_all (1..day).each { |d| mark_day_complete!(d) } end reload_progress true end |
#not_started_yet? ⇒ Boolean
567 568 569 |
# File 'app/models/bible270/reader.rb', line 567 def not_started_yet? Plan.before_start?(Bible270.today, effective_start_date) end |
#own_start_date? ⇒ Boolean
Whether this reader is following a personal date or the shared cohort one.
539 540 541 |
# File 'app/models/bible270/reader.rb', line 539 def own_start_date? Bible270.config.allow_reader_start_date && started_on.present? end |
#partial_days ⇒ Object
348 349 350 |
# File 'app/models/bible270/reader.rb', line 348 def partial_days checked_counts.keys.select { |day| day_status(day) == :partial }.sort end |
#past_end_date? ⇒ Boolean
571 572 573 |
# File 'app/models/bible270/reader.rb', line 571 def past_end_date? Plan.after_end?(Bible270.today, effective_start_date) end |
#plan_end_date ⇒ Object
575 576 577 |
# File 'app/models/bible270/reader.rb', line 575 def plan_end_date Plan.end_date_for(effective_start_date) end |
#raw_calendar_day ⇒ Object
563 564 565 |
# File 'app/models/bible270/reader.rb', line 563 def raw_calendar_day Plan.day_for(Bible270.today, effective_start_date, clamp: false) end |
#read?(day, track, part = nil) ⇒ Boolean
369 370 371 372 373 374 |
# File 'app/models/bible270/reader.rb', line 369 def read?(day, track, part = nil) return read_parts_for(day, track).include?(part) if part # No part given: the track counts as read only when every chapter is. read_parts_for(day, track).size >= Plan.part_count(day, track) end |
#read_parts_for(day, track) ⇒ Object
Which chapters of a track the reader has ticked on this day.
365 366 367 |
# File 'app/models/bible270/reader.rb', line 365 def read_parts_for(day, track) checkoffs.where(day: day, track: track.to_s).pluck(:part) end |
#read_tracks_for(day) ⇒ Object
360 361 362 |
# File 'app/models/bible270/reader.rb', line 360 def read_tracks_for(day) checkoffs.where(day: day).pluck(:track).uniq end |
#reload_progress ⇒ Object
515 516 517 518 |
# File 'app/models/bible270/reader.rb', line 515 def reload_progress @checked_counts = nil self end |
#remaining_parts_for(day) ⇒ Object
352 353 354 355 356 357 358 |
# File 'app/models/bible270/reader.rb', line 352 def remaining_parts_for(day) return [] unless Plan.valid_day?(day) missing_parts_on(day).map do |track, part| { track: track, part: part, reference: Plan.parts_for(day, track).fetch(part) } end end |
#remember_token! ⇒ Object
Generated on first use rather than at sign-up, so readers who never stay signed in never carry one.
276 277 278 279 280 281 282 |
# File 'app/models/bible270/reader.rb', line 276 def remember_token! return remember_token if remember_token.present? token = SecureRandom.urlsafe_base64(32) update_column(:remember_token, token) token end |
#remove_avatar! ⇒ Object
320 321 322 323 |
# File 'app/models/bible270/reader.rb', line 320 def remove_avatar! avatar.purge if avatar_uploaded? true end |
#restart_on!(day:, on: Bible270.today) ⇒ Object
Put this reader on day as of on — i.e. back-date the start so that the
given date lands on the given day of the plan.
488 489 490 491 492 493 |
# File 'app/models/bible270/reader.rb', line 488 def restart_on!(day:, on: Bible270.today) day = day.to_i return false unless Plan.valid_day?(day) update!(started_on: Plan.to_date(on) - (day - 1)) end |
#set_start_date!(value) ⇒ Object
Set an individual start date for administrative use. This remains ungated so an administrator can prepare or preserve a personal calendar even when config.allow_reader_start_date currently makes the community date authoritative. Returns false only when the value isn't a date.
605 606 607 608 609 610 |
# File 'app/models/bible270/reader.rb', line 605 def set_start_date!(value) date = Plan.to_date(value) return false if date.nil? update!(started_on: date) end |
#sort_name ⇒ Object
Readers are listed by first name, matching how they are shown — the display name is "First Last", so sorting on it needs only case folding. Surname order was inconsistent with the community page and read oddly next to names displayed first-name-first.
241 242 243 |
# File 'app/models/bible270/reader.rb', line 241 def sort_name display_name.to_s.strip.downcase end |
#start_date ⇒ Object
597 598 599 |
# File 'app/models/bible270/reader.rb', line 597 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.
593 594 595 |
# File 'app/models/bible270/reader.rb', line 593 def start_date=(value) self.started_on = Plan.to_date(value) end |
#suggested_names ⇒ Object
Fill first/last from the display name where we only have one string, e.g. a reader who arrived through OmniAuth.
418 419 420 421 422 |
# File 'app/models/bible270/reader.rb', line 418 def suggested_names return { first_name: first_name, last_name: last_name } if full_name Names.split_display_name(display_name) || { first_name: display_name, last_name: nil } end |
#today?(day) ⇒ Boolean
559 560 561 |
# File 'app/models/bible270/reader.rb', line 559 def today?(day) today_day == day end |
#today_day ⇒ Object
Raw, unclamped — lets callers distinguish "not started yet" / "finished". The plan day that today actually is, or nil when today falls outside the plan's window. calendar_day clamps, so before the start date it reports day 1 — which made day 1 claim to be "today" for anyone whose plan hadn't begun.
552 553 554 555 556 557 |
# File 'app/models/bible270/reader.rb', line 552 def today_day raw = raw_calendar_day return nil if raw.nil? Plan.valid_day?(raw) ? raw : nil end |
#toggle_day!(day) ⇒ Object
469 470 471 |
# File 'app/models/bible270/reader.rb', line 469 def toggle_day!(day) day_complete?(day) ? clear_day!(day) : mark_day_complete!(day) end |
#track_partially_read?(day, track) ⇒ Boolean
376 377 378 379 |
# File 'app/models/bible270/reader.rb', line 376 def track_partially_read?(day, track) done = read_parts_for(day, track).size done.positive? && done < Plan.part_count(day, track) end |
#update_bible_version(code) ⇒ Object
221 222 223 224 225 |
# File 'app/models/bible270/reader.rb', line 221 def update_bible_version(code) return false unless Translations.valid?(code) update(bible_version: Translations.normalize(code)) end |
#update_names(first, last) ⇒ Object
Day number implied by the calendar, if a start date is set. Set the name shown beside this reader's reflections. Returns false when either half is missing, so callers can re-render with a message.
409 410 411 412 413 414 |
# File 'app/models/bible270/reader.rb', line 409 def update_names(first, last) names = Names.normalize(first, last) return false if names.nil? update(**names) end |
#update_passage_source(source) ⇒ Object
227 228 229 230 231 232 |
# File 'app/models/bible270/reader.rb', line 227 def update_passage_source(source) source = source.to_s return false unless PASSAGE_SOURCES.include?(source) update(passage_source: source) end |
#wants_comment_notifications? ⇒ Boolean
Older copied databases may briefly lack the preference column while their migrations are being reconciled. Preserve the historical opted-in behavior until the column is available rather than breaking reflection delivery.
250 251 252 |
# File 'app/models/bible270/reader.rb', line 250 def wants_comment_notifications? !has_attribute?(:notify_on_mention) || self[:notify_on_mention] != false end |