Skip to content

Commit

Permalink
CaseGroups request spec & custom policy generator (#5999)
Browse files Browse the repository at this point in the history
* Add CaseGroups request spec

* Create PolicyGenerator: replaces pundit:policy gen

* generated policy and spec

* post-generation to implement prior behavior

* use CaseGroupPolicy in CaseGroupsController

* Include PolicyGenerator in Scaffold/Controller generation

* policy#same_org? improvement

* policy_scope for set_case_group

* no fancy super subject

* add volunteer? method for AllCasaAdmin

* case_group slight improvement

* case group factory case casa org association

* better action-less generator plus spec notes
  • Loading branch information
thejonroberts authored Aug 31, 2024
1 parent 9296150 commit ac35c95
Show file tree
Hide file tree
Showing 14 changed files with 595 additions and 20 deletions.
1 change: 0 additions & 1 deletion .allow_skipping_tests
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
blueprints/api/v1/session_blueprint.rb
channels/application_cable/channel.rb
channels/application_cable/connection.rb
controllers/case_groups_controller.rb
controllers/concerns/court_date_params.rb
controllers/followup_reports_controller.rb
controllers/learning_hour_topics_controller.rb
Expand Down
27 changes: 17 additions & 10 deletions app/controllers/case_groups_controller.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
class CaseGroupsController < ApplicationController
before_action :require_organization!
before_action :authorize_admin_or_supervisor!
before_action :set_case_group, only: %i[edit update destroy]

def index
@case_groups = current_organization.case_groups.includes(:casa_cases)
authorize CaseGroup
@case_groups = policy_scope(CaseGroup).includes(:casa_cases)
end

def new
@case_group = CaseGroup.new
@case_group = CaseGroup.new(casa_org: current_organization)
authorize @case_group
end

def edit
@case_group = current_organization.case_groups.find(params[:id])
authorize @case_group
end

def create
@case_group = current_organization.case_groups.build(case_group_params)
authorize @case_group

if @case_group.save
redirect_to case_groups_path, notice: "Case group created!"
else
Expand All @@ -24,27 +28,30 @@ def create
end

def update
@case_group = current_organization.case_groups.find(params[:id])
authorize @case_group

if @case_group.update(case_group_params)
redirect_to case_groups_path, notice: "Case group updated!"
else
render :new, status: :unprocessable_entity
render :edit, status: :unprocessable_entity
end
end

def destroy
case_group = current_organization.case_groups.find(params[:id])
case_group.destroy
authorize @case_group

@case_group.destroy
redirect_to case_groups_path, notice: "Case group deleted!"
end

private

def case_group_params
params.merge(casa_org: current_organization)
params.require(:case_group).permit(:name, casa_case_ids: [])
end

def authorize_admin_or_supervisor!
authorize :application, :admin_or_supervisor?
def set_case_group
@case_group = policy_scope(CaseGroup).find(params[:id])
end
end
12 changes: 12 additions & 0 deletions app/models/all_casa_admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ class AllCasaAdmin < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :invitable, :recoverable, :validatable, :timeoutable, invite_for: 1.weeks

def casa_admin?
false
end

def supervisor?
false
end

def volunteer?
false
end
end

# == Schema Information
Expand Down
8 changes: 4 additions & 4 deletions app/policies/application_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def destroy?
end

def is_admin?
user.casa_admin?
user&.casa_admin?
end

def same_org?
Expand All @@ -60,16 +60,16 @@ def same_org?

def is_admin_same_org?
# eventually everything should use this
user.casa_admin? && same_org?
user&.casa_admin? && same_org?
end

def is_supervisor?
user.supervisor?
user&.supervisor?
end

def is_supervisor_same_org?
# eventually everything should use this
user.supervisor? && same_org?
is_supervisor? && same_org?
end

def is_volunteer? # deprecated in favor of is_volunteer_same_org?
Expand Down
46 changes: 46 additions & 0 deletions app/policies/case_group_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
class CaseGroupPolicy < ApplicationPolicy
class Scope < ApplicationPolicy::Scope
def resolve
case user
when CasaAdmin, Supervisor
scope.where(casa_org: @user.casa_org)
when Volunteer
scope.none
else
scope.none
end
end
end

def index?
is_admin? || is_supervisor?
end

def new?
admin_or_supervisor_same_org?
end

def show?
admin_or_supervisor_same_org?
end

def create?
admin_or_supervisor_same_org?
end

def edit?
admin_or_supervisor_same_org?
end

def update?
admin_or_supervisor_same_org?
end

def destroy?
admin_or_supervisor_same_org?
end

def same_org?
user&.casa_org.present? && user&.casa_org == record&.casa_org
end
end
2 changes: 1 addition & 1 deletion config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Application < Rails::Application
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
# Common ones are `templates`, `generators`, or `middleware`, for example.
config.autoload_lib(ignore: %w[assets tasks mailers])
config.autoload_lib(ignore: %w[assets generators tasks mailers])

# Configuration for the application, engines, and railties goes here.
#
Expand Down
38 changes: 38 additions & 0 deletions config/initializers/generators.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Allows rails generators (scaffold/controller) to use custom policy generator.
# Arguments for rails generators will be passed to the policy generator.
# Options will be shown in the help text for the rails generators,
# including the option to skip the policy generator (--skip-policy).
module PolicyGenerator
module ControllerGenerator
extend ActiveSupport::Concern

included do
hook_for :policy, in: nil, default: true, type: :boolean do |generator|
# use actions from controller invocation
invoke generator, [name.singularize, *actions]
end
end
end

module ScaffoldControllerGenerator
extend ActiveSupport::Concern

included do
hook_for :policy, in: nil, default: true, type: :boolean do |generator|
# prevent attribute arguments (name:string) being confused with actions
scaffold_actions = %w[index new create show edit update destroy]
invoke generator, [name.singularize, *scaffold_actions]
end
end
end
end

module ActiveModel
class Railtie < Rails::Railtie
generators do |app|
Rails::Generators.configure! app.config.generators
Rails::Generators::ControllerGenerator.include PolicyGenerator::ControllerGenerator
Rails::Generators::ScaffoldControllerGenerator.include PolicyGenerator::ScaffoldControllerGenerator
end
end
end
9 changes: 9 additions & 0 deletions lib/generators/rails/policy/USAGE
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Description:
Generates a pundit policy and corresponding spec.

Example:
bin/rails generate policy CasaThing

This will create:
- app/policies/casa_thing_policy.rb
- spec/policies/casa_thing_policy_spec.rb
20 changes: 20 additions & 0 deletions lib/generators/rails/policy/policy_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# NOTE: Rails namespace in order to be able to called from rails generators (see initializers/generators.rb)
class Rails::PolicyGenerator < Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)

remove_class_option :skip_namespace
remove_class_option :skip_collision_check

argument :actions, type: :array, banner: "action action", default: []

class_option :headless, type: :boolean, default: false,
desc: "Policy for non-model routes (dashboard, collection, etc)"

def create_policy
template "policy.rb", File.join("app/policies", class_path, "#{file_name}_policy.rb")
end

def create_policy_spec
template "policy_spec.rb", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb")
end
end
52 changes: 52 additions & 0 deletions lib/generators/rails/policy/templates/policy.rb.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<%- module_namespacing do -%>
class <%= class_name %>Policy < ApplicationPolicy
<%- if options[:headless] -%>
# Headless policy (used when no corresponding ActiveRecord/Model):
# - use `authorize(:<%= singular_name %>, :action?)` in controller actions
# - see https://github.com/varvet/pundit#headless-policies
def initialize(user, _record)
@user = user
end
<%- else -%>
class Scope < ApplicationPolicy::Scope
def resolve
case user
when CasaAdmin, Supervisor
scope.where(casa_org: @user.casa_org)
when Volunteer
# REMOVE IF NOT APPLICABLE (just an example, doesn't work for all cases)
# scope.assigned_to_user(@user)
scope.none
else
scope.none
end
end
end
<%- end -%>
<%- user_example = "is_admin? || is_supervisor?" -%>
<%- record_example = "admin_or_supervisor_same_org?" -%>
<%- if actions.empty? -%>

# No actions specified, Example usage:
# def index?
# <%= user_example %>
# end
#
# def show?
# <%= record_example %>
# end
<%- end -%>
<%- actions.each do |action| -%>

<%- if action == "index" -%>
def index?
<%= user_example %>
end
<%- else -%>
def <%= action %>?
<%= options[:headless] ? user_example : record_example %>
end
<%- end -%>
<%- end -%>
end
<% end -%>
Loading

0 comments on commit ac35c95

Please sign in to comment.