Module: Studio::ProfileSections
- Defined in:
- lib/studio/profile_sections.rb
Constant Summary collapse
- PAGES =
Which page a row belongs to.
:showis the read page (/profile) — things you look at and occasionally act on;:editis the form (/profile/edit). %i[show edit].freeze
- DEFAULTS =
The rows every consumer gets for free.
THE AVATAR IS NOT HERE, and that is a change rather than an omission: it moved into the identity header both pages render, where it sits with the display name and the address. It stopped being a row when it stopped looking like one.
[ # --- the read page ------------------------------------------------------ # Google is an identity you CONNECT, not a field you type, which is why it # reads rather than edits. Gated on the app OFFERING Google, not merely on # having the columns — the same question app/views/sessions/new.html.erb # already asks before drawing this identical button on the login page. { key: :google, title: "Google account", page: :show, partial: "studio/profiles/google_section", requires: %i[provider uid], if: -> { Studio.auth_method?(:google) } }, # --- the edit page ------------------------------------------------------ # These are FIELDS in one form with one Save, so they carry no buttons of # their own. Email included: it needed a separate action only while it was # out-of-band, and now that it applies directly there is no reason for a # second mechanism on the same page. Its side effects — the Google lock, # notifying the old address, invalidating other sessions — are the server's # business, not the form's. { key: :name, title: "Name", page: :edit, partial: "studio/profiles/name_fields", requires: :first_name }, { key: :email, title: "Email", page: :edit, partial: "studio/profiles/email_fields", requires: :email }, { key: :birthday, title: "Birthday", page: :edit, partial: "studio/profiles/birthday_fields", requires: %i[birth_day birth_month birth_year] } ].freeze
Class Method Summary collapse
-
.defaults ⇒ Object
A fresh, mutable copy every call.
-
.enabled?(condition, view) ⇒ Boolean
The app-capability gate.
-
.page_of(section) ⇒ Object
Can this host's user model serve the row? No requirement means yes — a row that reads nothing off the user (a static explainer, a link out) is always served.
-
.resolve(declared, view, page: nil) ⇒ Object
Declared may be nil (the host has said nothing — it gets the defaults), an Array, or a callable receiving the view context.
- .served_by?(user, requires) ⇒ Boolean
- .symbolize(hash) ⇒ Object
-
.warn_gate(message) ⇒ Object
Pure Ruby: this file loads without Rails, so it cannot assume a logger.
Class Method Details
.defaults ⇒ Object
A fresh, mutable copy every call. Hosts compose with + and sometimes
reject, and handing out the frozen literal would let one host's edit
leak into the next request's page.
96 97 98 |
# File 'lib/studio/profile_sections.rb', line 96 def defaults DEFAULTS.map { |section| section.dup } end |
.enabled?(condition, view) ⇒ Boolean
The app-capability gate. No if: means always enabled. The callable takes
the view when it wants one and nothing when it does not, so a host can
write either -> { Studio.feature?(:x) } or ->(view) { view.admin? }.
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/studio/profile_sections.rb', line 126 def enabled?(condition, view) return true if condition.nil? # A Symbol/String names a method on the VIEW — Rails' own # `before_action ..., if: :method_name` convention, and therefore the most # natural thing a host will write here. # # It is handled explicitly because the alternative FAILS OPEN: a Symbol # does not answer `call`, so the earlier `return !!condition` coerced # `:some_predicate` to true and rendered the row unconditionally — a gate # that silently does nothing, in the same permissive direction as the bug # this whole `if:` key was added to fix. A host would have had no signal. if condition.is_a?(Symbol) || condition.is_a?(String) unless view.respond_to?(condition) # SILENT would repeat the bug this key was added to fix. The old # coercion gave the host no signal; dropping the row without one only # moves the silence somewhere safer. A typo'd gate now says so. warn_gate("profile_sections: `if: #{condition.inspect}` names a method the view " \ "does not answer — the row was dropped. Check the spelling.") return false end # Arity-aware, matching the lambda branch below: a predicate written as # `def visible?(view)` is as natural as one written without an argument, # and calling it wrong raises ArgumentError — which surfaces as a 500 on # /profile rather than as a dropped row. method = view.method(condition) return !!(method.arity.zero? ? view.public_send(condition) : view.public_send(condition, view)) end return !!condition unless condition.respond_to?(:call) !!(condition.arity.zero? ? condition.call : condition.call(view)) end |
.page_of(section) ⇒ Object
Can this host's user model serve the row? No requirement means yes — a row that reads nothing off the user (a static explainer, a link out) is always served. A nil user means we are rendering for nobody, and nothing is served. A row that never says defaults to :edit — someone adding a row is usually adding a field, and the read page is a deliberate, curated surface.
166 167 168 |
# File 'lib/studio/profile_sections.rb', line 166 def page_of(section) (section[:page] || :edit).to_sym end |
.resolve(declared, view, page: nil) ⇒ Object
Declared may be nil (the host has said nothing — it gets the defaults), an Array, or a callable receiving the view context.
nil meaning "the defaults" rather than "no sections" is the load-bearing
choice here: it is what makes a brand-new app's profile page work with an
empty initializer, which is the entire point of standardizing this.
page: selects a subset. nil means every page, which is what the pre-split
callers passed and what a host asking "all of them" wants.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/studio/profile_sections.rb', line 108 def resolve(declared, view, page: nil) sections = declared.respond_to?(:call) ? declared.call(view) : declared sections = defaults if sections.nil? admin = view.respond_to?(:admin?) && view.admin? user = view.respond_to?(:current_user) ? view.current_user : nil Array(sections) .map { |section| symbolize(section) } .reject { |section| page && page_of(section) != page.to_sym } .reject { |section| section[:admin] && !admin } .select { |section| enabled?(section[:if], view) } .select { |section| served_by?(user, section[:requires]) } end |
.served_by?(user, requires) ⇒ Boolean
170 171 172 173 174 175 176 |
# File 'lib/studio/profile_sections.rb', line 170 def served_by?(user, requires) needed = Array(requires).compact return true if needed.empty? return false if user.nil? needed.all? { |attribute| user.respond_to?(attribute) } end |
.symbolize(hash) ⇒ Object
187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/studio/profile_sections.rb', line 187 def symbolize(hash) out = hash.to_h.each_with_object({}) { |(key, value), acc| acc[key.to_sym] = value } # `key` is an IDENTIFIER, so its VALUE symbolizes too — unlike every other # field, whose value is data. A host composing by key writes # `reject { |s| s[:key] == :avatar }`, and that silently matches nothing if # a section declared with string keys kept `"avatar"` as a String. Removing # a standard row is the documented seam; it must not depend on which # spelling the host happened to use. out[:key] = out[:key].to_sym if out[:key].respond_to?(:to_sym) out end |
.warn_gate(message) ⇒ Object
Pure Ruby: this file loads without Rails, so it cannot assume a logger.
179 180 181 182 183 184 185 |
# File 'lib/studio/profile_sections.rb', line 179 def warn_gate() if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger Rails.logger.warn() else Kernel.warn() end end |