Class: CurrentScope::PermissionGrid

Inherits:
Object
  • Object
show all
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

Classes: Cell, Column

Instance Method Summary collapse

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.permission_grid_groups,
               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

#columnsObject

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

#controllersObject



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 expand(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).

Returns:

  • (Boolean)


41
42
43
# File 'lib/current_scope/permission_grid.rb', line 41

def ungated?(controller)
  @gating.ungated?(controller)
end