Module: CurrentScope::ApplicationHelper

Defined in:
app/helpers/current_scope/application_helper.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.warn_subject_label_broken_once(error) ⇒ Object

The configured value could not even be classified — see the rescue in configured_subject_label. Keyed by the error's class, and says nothing about the label itself, because reading the label is what failed.



217
218
219
220
221
222
223
224
225
# File 'app/helpers/current_scope/application_helper.rb', line 217

def warn_subject_label_broken_once(error)
  return unless new_subject_label_warning?(:broken, safe_class(error))

  Rails.logger&.warn(
    "[CurrentScope] config.subject_label could not be read — #{safe_class(error)}: " \
    "#{safe_message(error)}. Every subject is falling back to the default label. Set it to " \
    "a Symbol (e.g. :email), a Proc taking the subject, or nil."
  )
end

.warn_subject_label_raised_once(label, error) ⇒ Object



203
204
205
206
207
208
209
210
211
212
# File 'app/helpers/current_scope/application_helper.rb', line 203

def warn_subject_label_raised_once(label, error)
  return unless new_subject_label_warning?(:raised, label)

  Rails.logger&.warn(
    "[CurrentScope] config.subject_label raised #{safe_class(error)}: #{safe_message(error)} " \
    "— that subject fell back to the default label rather than erroring the page. A " \
    "subject_label must be total: it runs for every subject, including ones with nil or " \
    "blank attributes."
  )
end

.warn_unknown_subject_label_once(label) ⇒ Object

Warn ONCE per (reason, label): a misconfigured subject_label is the same mistake on every row, so one line is a signal and one-per-subject is noise the admin will scroll past. Keyed by the label value, so changing the config in dev warns again for the new value.

Always-on rather than behind a warn_on_* flag (cf. warn_on_nil_sod_record, which defaults off because a nil record is often legitimate): a label the subject can't answer, or one that raises, is unambiguously a mistake. Mirrors Event.warn_missing_events_table_once.

Says "this subject" rather than "subjects": under STI some subclasses may answer and others not, and warn-once means the first non-responder would otherwise speak for all of them.



178
179
180
181
182
183
184
185
186
187
# File 'app/helpers/current_scope/application_helper.rb', line 178

def warn_unknown_subject_label_once(label)
  return unless new_subject_label_warning?(:unknown, label)

  Rails.logger&.warn(
    "[CurrentScope] config.subject_label is #{label.inspect}, but this subject does not " \
    "respond to it — it is falling back to the default label. If no subject responds to it, " \
    "this config does nothing. Set it to a method the subject responds to (e.g. :email), a " \
    "Proc, or nil."
  )
end

.warn_unusable_subject_label_once(label) ⇒ Object

Not a Symbol, String, Proc, or nil — so it can't name a method and can't be called. Reports the TYPE, not the value: this is the one warning whose subject is an arbitrary host object, and calling inspect on it could itself raise — inside the very path that exists to stop a raise.



193
194
195
196
197
198
199
200
201
# File 'app/helpers/current_scope/application_helper.rb', line 193

def warn_unusable_subject_label_once(label)
  return unless new_subject_label_warning?(:unusable, label.class)

  Rails.logger&.warn(
    "[CurrentScope] config.subject_label is a #{label.class} — it can't name a method and " \
    "can't be called, so every subject is falling back to the default label. Set it to a " \
    "Symbol (e.g. :email), a Proc taking the subject, or nil."
  )
end

Instance Method Details

#configured_subject_label(subject) ⇒ Object

The configured subject_label (Symbol or Proc) applied to a subject, or nil when unset, when it resolves to a blank value, or when it fails.

subject_label is arbitrary host code running once per row on the admin's main tool for granting and reviewing roles. One subject with incomplete data (a Proc doing u.email.upcase on a subject whose email is nil) must not take the whole page down — the same intent the holder-label helpers below already encode for stale polymorphic types. A failure here costs one label; a raise costs the page.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'app/helpers/current_scope/application_helper.rb', line 45

def configured_subject_label(subject)
  label = CurrentScope.config.subject_label
  return if label.nil?
  return resolve_subject_label(label) { label.call(subject) } if label.respond_to?(:call)

  # Only a Symbol or String names a method. Checked BEFORE respond_to?,
  # which raises TypeError on anything else ("false is not a symbol nor a
  # string") — and that raise would land outside the rescue below and 500
  # the page, which is the bug this method exists to prevent, reached
  # through a different door.
  unless label.is_a?(Symbol) || label.is_a?(String)
    CurrentScope::ApplicationHelper.warn_unusable_subject_label_once(label)
    return
  end

  if subject.respond_to?(label)
    resolve_subject_label(label) { subject.public_send(label) }
  else
    # Not a mistake we can render around: this config does nothing for EVERY
    # subject, silently, which is why it needs saying out loud once.
    CurrentScope::ApplicationHelper.warn_unknown_subject_label_once(label)
    nil
  end
rescue StandardError => e
  # Last resort, and the reason it exists: every branch above calls a method
  # ON the host's config value to classify it — `nil?`, `respond_to?`,
  # `is_a?`. A pathological value breaks the classification itself, before
  # any specific guard can classify it, so the guards cannot be where this
  # is caught. A BasicObject has no #nil?; a delegator can raise from
  # respond_to?.
  #
  # Deliberately does NOT touch `label`: it is the thing that just proved it
  # cannot be touched. The warning is keyed off the error alone.
  CurrentScope::ApplicationHelper.warn_subject_label_broken_once(e)
  nil
end

#current_scope_gid_label(gid) ⇒ Object

Best-effort label for a stored GID string (event actor/subject). Falls back to the raw GID when the record is gone — the ledger outlives the identities it names.



133
134
135
136
137
138
139
140
# File 'app/helpers/current_scope/application_helper.rb', line 133

def current_scope_gid_label(gid)
  record = GlobalID::Locator.locate(gid)
  record ? current_scope_label(record) : gid
rescue ActiveRecord::RecordNotFound, NameError
  # locate RAISES for a deleted record or renamed class (nil is only for
  # unparseable strings) — same contract the holder helpers rescue above.
  gid
end

#current_scope_grant_diagnosis_badge(scoped_assignment) ⇒ Object

#134: the console's one read of GrantDiagnosis. Returns [css_class, badge_text, caveat] or nil. Never the word "inert" (#90's badge, different state, different fix).



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'app/helpers/current_scope/application_helper.rb', line 99

def current_scope_grant_diagnosis_badge(scoped_assignment)
  verdict = CurrentScope::GrantDiagnosis.verdict_for(scoped_assignment)
  if verdict
    return [
      "cs-dead-badge", "cannot match",
      "#{CurrentScope::GrantDiagnosis.verdict_label(verdict).capitalize}. " \
      "#{CurrentScope::GrantDiagnosis.verdict_fix(verdict)}"
    ]
  end

  return nil unless CurrentScope::GrantDiagnosis.type_untargeted?(scoped_assignment, verdict: verdict)

  [
    "cs-check-badge", "check hooks",
    "No permission on this role names a controller for this record's type. " +
      CurrentScope::GrantDiagnosis.untargeted_caveat
  ]
end

#current_scope_holder_resource_label(scoped_assignment) ⇒ Object



118
119
120
121
122
123
124
125
126
127
128
# File 'app/helpers/current_scope/application_helper.rb', line 118

def current_scope_holder_resource_label(scoped_assignment)
  if scoped_assignment.respond_to?(:orphaned_resource?) && scoped_assignment.orphaned_resource?
    return "#{scoped_assignment.resource_type} ##{scoped_assignment.resource_id} (unavailable — inert)"
  end

  current_scope_label(scoped_assignment.resource)
rescue NameError, ActiveRecord::RecordNotFound
  # Labeler failed — not proof the resource is orphaned. Raw type#id only
  # (orphaned_resource? above owns the inert wording) — PR #104 review.
  "#{scoped_assignment.resource_type} ##{scoped_assignment.resource_id}"
end

#current_scope_holder_subject_label(assignment) ⇒ Object

Members-list labels that survive a stale/renamed polymorphic type — a removed subject or resource class must not 500 the page; fall back to "Type #id" the way the audit ledger does.



85
86
87
88
89
90
91
92
93
94
# File 'app/helpers/current_scope/application_helper.rb', line 85

def current_scope_holder_subject_label(assignment)
  # Through the canonical guard, not assignment.subject: a non-canonical
  # stored subject_id (a pre-#151 collapsed value, or a host insert_all) would
  # otherwise cast "7f00…" to integer record 7 and label the row a live,
  # unrelated subject. nil falls back to "Type #id" like the audit ledger.
  subject = assignment.current_scope_resolved_record("subject")
  subject ? current_scope_subject_label(subject) : "#{assignment.subject_type} ##{assignment.subject_id}"
rescue NameError, ActiveRecord::RecordNotFound
  "#{assignment.subject_type} ##{assignment.subject_id}"
end

#current_scope_label(record) ⇒ Object

Best-effort human label for any host subject/resource. Delegates to the one shared chain (CurrentScope.label_for) — the same definition the audit ledger denormalizes (see Event.label_for), so the picker, the chips, and the ledger agree by construction, not by parallel maintenance.



7
8
9
10
11
# File 'app/helpers/current_scope/application_helper.rb', line 7

def current_scope_label(record)
  return "(none)" if record.nil?

  CurrentScope.label_for(record)
end

#current_scope_subject_label(subject) ⇒ Object

Human label for a subject (user/account), honouring config.subject_label so a host on UUID keys can show email or a full name instead of an opaque id. Falls back to the best-effort current_scope_label.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'app/helpers/current_scope/application_helper.rb', line 16

def current_scope_subject_label(subject)
  return "(none)" if subject.nil?

  # A configured label that resolves to nil/blank (e.g. :email on a subject
  # with no email yet) must not render as an empty row — fall through to the
  # default human-identifier chain rather than showing "".
  configured = configured_subject_label(subject)
  return configured if configured

  # Default: a subject is a person, so prefer human identifiers over a
  # generic "Class #id" — including when the model includes Scopeable, whose
  # current_scope_label is id-based. Covers `email` and Rails 8 auth's
  # `email_address`. Config overrides this entirely.
  full_name = [ subject.try(:first_name), subject.try(:last_name) ].compact.join(" ").presence
  # .presence on each identifier: a stored empty/whitespace email ("") is
  # truthy in Ruby and would otherwise short-circuit to a blank label.
  subject.try(:email).presence || subject.try(:email_address).presence ||
    subject.try(:name).presence || full_name || current_scope_label(subject)
end