Class: CurrentScope::PermissionGrid
- Inherits:
-
Object
- Object
- CurrentScope::PermissionGrid
- Defined in:
- lib/current_scope/permission_grid.rb
Overview
Presents the route-derived permission catalog as an ALIGNED matrix for the role editor: fixed columns, one row per controller, blank cells where a controller doesn't route a column's actions (never a shifted cell).
By default the columns are CRUD groups (config.permission_grid_groups): ticking one grants every routed action in the group, so the RESTful form actions fold into their mutation (new→create, edit→update) and index+show read as one. Actions outside any group (e.g. "approve") get their own column. With groups set to nil/{} every raw action becomes its own column — still aligned.
Defined Under Namespace
Instance Method Summary collapse
- #actions_for(controller) ⇒ Object
-
#cell(controller, column, granted) ⇒ Object
One cell for (controller, column) against a role's granted key set.
-
#columns ⇒ Object
Ordered columns: config groups that apply to at least one controller (in config order), then leftover actions not covered by any group (sorted).
- #controllers ⇒ Object
-
#expand(tokens) ⇒ Object
Expand submitted "controller:group" tokens into routed permission keys.
-
#initialize(catalog: CurrentScope.catalog, groups: CurrentScope.config.permission_grid_groups, gating: GatingReflection.new) ⇒ PermissionGrid
constructor
The gating default is evaluated at CALL time, so every bare PermissionGrid.new (the edit view AND role_params on every role save) constructs a GatingReflection.
-
#ungated?(controller) ⇒ Boolean
Is this row's controller provably never gated? Advisory only — a pure delegation the view reads to annotate the row; no other grid method consults the reflection, so the answer cannot affect a cell or an expansion (pinned byte-identical in the tests).
Constructor Details
#initialize(catalog: CurrentScope.catalog, groups: CurrentScope.config.permission_grid_groups, gating: GatingReflection.new) ⇒ PermissionGrid
The gating default is evaluated at CALL time, so every bare PermissionGrid.new (the edit view AND role_params on every role save) constructs a GatingReflection. That is fine only because its constructor is inert by contract — all reflection work happens inside #ungated?, and nothing here calls it during initialize or #expand (KTD-8; pinned by the spy test).
26 27 28 29 30 31 |
# File 'lib/current_scope/permission_grid.rb', line 26 def initialize(catalog: CurrentScope.catalog, groups: CurrentScope.config., gating: GatingReflection.new) @grouped = catalog.grouped # { "controller" => ["action", ...] } @groups = groups || {} @gating = gating end |
Instance Method Details
#actions_for(controller) ⇒ Object
105 106 107 |
# File 'lib/current_scope/permission_grid.rb', line 105 def actions_for(controller) @grouped[controller] || [] end |
#cell(controller, column, granted) ⇒ Object
One cell for (controller, column) against a role's granted key set. Blank when the controller routes none of the column's actions. Otherwise a checkbox.
A GROUP cell is only checked when EVERY routed action is granted — a
partial group is rendered unchecked+indeterminate and its existing keys are
preserved verbatim via hidden inputs (see the edit view). This is the
escalation guard: a checked group token expands to the whole group on save,
so treating "some granted" as checked would silently promote a partial
grant to a full one just by re-saving the role. granted_keys carries the
exact subset to round-trip for a partial cell.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/current_scope/permission_grid.rb', line 65 def cell(controller, column, granted) routed = column.actions & actions_for(controller) return Cell.new(blank: true) if routed.empty? keys = routed.map { |action| "#{controller}##{action}" } present_keys = keys.select { |key| granted.include?(key) } partial = present_keys.any? && present_keys.size < keys.size Cell.new( blank: false, group: column.group, name: column.group ? "role[permission_groups][]" : "role[permission_keys][]", value: column.group ? "#{controller}:#{column.label}" : keys.first, checked: column.group ? (present_keys.any? && !partial) : present_keys.any?, partial: partial, granted_keys: partial ? present_keys : [] ) end |
#columns ⇒ Object
Ordered columns: config groups that apply to at least one controller (in config order), then leftover actions not covered by any group (sorted).
47 48 49 50 51 52 |
# File 'lib/current_scope/permission_grid.rb', line 47 def columns grouped = @groups.filter_map do |label, actions| Column.new(label: label, actions: actions, group: true) if any_controller_has?(actions) end grouped + leftover_actions.map { |action| Column.new(label: action, actions: [ action ], group: false) } end |
#controllers ⇒ Object
33 34 35 |
# File 'lib/current_scope/permission_grid.rb', line 33 def controllers @grouped.keys.sort end |
#expand(tokens) ⇒ Object
Expand submitted "controller:group" tokens into routed permission keys.
A token the grid could not have produced (unknown group, unknown controller, nothing routed) passes through RAW so the model's catalog validation rejects it BY NAME — the same loud contract a hand-crafted permission_keys entry gets. One save, one error story: the grid's two submission channels must not differ on whether a crafted request is reported or silently swallowed. (The form's blank hidden padding is the one legitimate non-grid value; it alone drops out.)
92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/current_scope/permission_grid.rb', line 92 def (tokens) Array(tokens).flat_map do |token| next [] if token.blank? controller, label = token.to_s.split(":", 2) actions = @groups[label] routed = actions ? (actions & actions_for(controller)) : [] next [ token.to_s ] if routed.empty? routed.map { |action| "#{controller}##{action}" } end end |
#ungated?(controller) ⇒ Boolean
Is this row's controller provably never gated? Advisory only — a pure delegation the view reads to annotate the row; no other grid method consults the reflection, so the answer cannot affect a cell or an expansion (pinned byte-identical in the tests).
41 42 43 |
# File 'lib/current_scope/permission_grid.rb', line 41 def ungated?(controller) @gating.ungated?(controller) end |