wor-simple_crud 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +5 -0
- data/CHANGELOG.md +46 -0
- data/Gemfile +1 -0
- data/README.md +200 -2
- data/lib/simple_crud/action_lambdas.rb +81 -0
- data/lib/simple_crud/authorization/adapter.rb +7 -0
- data/lib/simple_crud/authorization/pundit_adapter.rb +6 -0
- data/lib/simple_crud/config.rb +6 -1
- data/lib/simple_crud/controller_helpers.rb +128 -0
- data/lib/simple_crud/pagination/adapter.rb +7 -1
- data/lib/simple_crud/pagination/kaminari_adapter.rb +23 -2
- data/lib/simple_crud/pagination/pagy_adapter.rb +7 -2
- data/lib/simple_crud/pagination/will_paginate_adapter.rb +7 -3
- data/lib/simple_crud/pagination/wor_paginate_adapter.rb +7 -2
- data/lib/simple_crud/rspec/config.rb +74 -0
- data/lib/simple_crud/rspec/helpers/authentication.rb +32 -0
- data/lib/simple_crud/rspec/helpers/controller_options.rb +19 -0
- data/lib/simple_crud/rspec/helpers/models.rb +55 -0
- data/lib/simple_crud/rspec/helpers/policies.rb +25 -0
- data/lib/simple_crud/rspec/helpers/requests.rb +45 -0
- data/lib/simple_crud/rspec/helpers/settings.rb +49 -0
- data/lib/simple_crud/rspec/helpers.rb +25 -0
- data/lib/simple_crud/rspec.rb +35 -0
- data/lib/simple_crud/simple_crud_controller.rb +27 -75
- data/lib/simple_crud/version.rb +1 -1
- data/lib/spec/shared_contexts/authenticate_user.rb +1 -7
- data/lib/spec/shared_examples/authorization_adapter_authorize.rb +11 -0
- data/lib/spec/shared_examples/simple_crud_for_create.rb +46 -11
- data/lib/spec/shared_examples/simple_crud_for_create_with_block.rb +36 -0
- data/lib/spec/shared_examples/simple_crud_for_create_with_build.rb +15 -0
- data/lib/spec/shared_examples/simple_crud_for_destroy.rb +44 -8
- data/lib/spec/shared_examples/simple_crud_for_destroy_with_block.rb +21 -0
- data/lib/spec/shared_examples/simple_crud_for_destroy_with_finder.rb +35 -0
- data/lib/spec/shared_examples/simple_crud_for_edit.rb +79 -0
- data/lib/spec/shared_examples/simple_crud_for_index.rb +9 -24
- data/lib/spec/shared_examples/simple_crud_for_index_with_block.rb +22 -0
- data/lib/spec/shared_examples/simple_crud_for_index_with_scope.rb +33 -0
- data/lib/spec/shared_examples/simple_crud_for_new.rb +29 -0
- data/lib/spec/shared_examples/simple_crud_for_new_with_block.rb +20 -0
- data/lib/spec/shared_examples/simple_crud_for_new_with_build.rb +19 -0
- data/lib/spec/shared_examples/simple_crud_for_show.rb +14 -10
- data/lib/spec/shared_examples/simple_crud_for_show_with_block.rb +22 -0
- data/lib/spec/shared_examples/simple_crud_for_show_with_finder.rb +33 -0
- data/lib/spec/shared_examples/simple_crud_for_update.rb +61 -14
- data/lib/spec/shared_examples/simple_crud_for_update_with_finder.rb +39 -0
- data/lib/spec/shared_examples/simple_crud_not_found_with_finder.rb +14 -0
- data/lib/spec/shared_examples/simple_crud_renders_template.rb +8 -0
- data/lib/spec/shared_examples/simple_crud_when_not_authorized.rb +18 -0
- data/lib/spec/shared_examples/simple_crud_without_authenticated_user.rb +9 -0
- data/lib/spec/shared_examples/unauthorized_when_not_logged_in.rb +2 -2
- data/spec/block/dummy_models_controller_spec.rb +8 -0
- data/spec/block_create/dummy_models_controller_spec.rb +7 -0
- data/spec/block_destroy/dummy_models_controller_spec.rb +7 -0
- data/spec/block_finder/dummy_models_controller_spec.rb +10 -0
- data/spec/block_json/dummy_models_controller_spec.rb +9 -0
- data/spec/block_new/dummy_models_controller_spec.rb +7 -0
- data/spec/block_redirect/dummy_models_controller_spec.rb +7 -0
- data/spec/block_show/dummy_models_controller_spec.rb +7 -0
- data/spec/built/dummy_models_controller_spec.rb +8 -0
- data/spec/dummy/app/controllers/block/dummy_models_controller.rb +17 -0
- data/spec/dummy/app/controllers/block_create/dummy_models_controller.rb +13 -0
- data/spec/dummy/app/controllers/block_destroy/dummy_models_controller.rb +9 -0
- data/spec/dummy/app/controllers/block_finder/dummy_models_controller.rb +25 -0
- data/spec/dummy/app/controllers/block_json/dummy_models_controller.rb +17 -0
- data/spec/dummy/app/controllers/block_modes/base_controller.rb +11 -0
- data/spec/dummy/app/controllers/block_new/dummy_models_controller.rb +9 -0
- data/spec/dummy/app/controllers/block_redirect/dummy_models_controller.rb +13 -0
- data/spec/dummy/app/controllers/block_show/dummy_models_controller.rb +9 -0
- data/spec/dummy/app/controllers/built/dummy_models_controller.rb +21 -0
- data/spec/dummy/app/controllers/dummy_models_controller.rb +2 -1
- data/spec/dummy/app/controllers/finder/dummy_models_controller.rb +23 -0
- data/spec/dummy/app/controllers/html/dummy_models_controller.rb +7 -0
- data/spec/dummy/app/controllers/html_create/dummy_models_controller.rb +7 -0
- data/spec/dummy/app/controllers/html_destroy/dummy_models_controller.rb +7 -0
- data/spec/dummy/app/controllers/html_finder/dummy_models_controller.rb +17 -0
- data/spec/dummy/app/controllers/html_modes/base_controller.rb +17 -0
- data/spec/dummy/app/controllers/html_new/dummy_models_controller.rb +7 -0
- data/spec/dummy/app/controllers/html_scoped/dummy_models_controller.rb +8 -0
- data/spec/dummy/app/controllers/html_show/dummy_models_controller.rb +7 -0
- data/spec/dummy/app/controllers/html_update/dummy_models_controller.rb +7 -0
- data/spec/dummy/app/controllers/invalid_status/dummy_models_controller.rb +13 -0
- data/spec/dummy/app/controllers/nested/dummy_models_controller.rb +21 -0
- data/spec/dummy/app/controllers/nested_route/dummy_models_controller.rb +26 -0
- data/spec/dummy/app/controllers/redirect/dummy_models_controller.rb +10 -0
- data/spec/dummy/app/controllers/redirect_auth/dummy_models_controller.rb +28 -0
- data/spec/dummy/app/controllers/scoped/dummy_models_controller.rb +16 -0
- data/spec/dummy/app/controllers/scoped_params/dummy_models_controller.rb +16 -0
- data/spec/dummy/app/controllers/strict/dummy_models_controller.rb +21 -0
- data/spec/dummy/app/controllers/unpaginated_scoped/dummy_models_controller.rb +16 -0
- data/spec/dummy/app/models/dummy_model.rb +4 -0
- data/spec/dummy/app/models/dummy_model_policy.rb +9 -1
- data/spec/dummy/app/views/block/dummy_models/index.html.erb +3 -0
- data/spec/dummy/app/views/block_new/dummy_models/new.html.erb +1 -0
- data/spec/dummy/app/views/block_show/dummy_models/show.html.erb +1 -0
- data/spec/dummy/app/views/html/dummy_models/index.html.erb +3 -0
- data/spec/dummy/app/views/html_create/dummy_models/new.html.erb +1 -0
- data/spec/dummy/app/views/html_destroy/dummy_models/show.html.erb +1 -0
- data/spec/dummy/app/views/html_finder/dummy_models/edit.html.erb +1 -0
- data/spec/dummy/app/views/html_finder/dummy_models/new.html.erb +1 -0
- data/spec/dummy/app/views/html_finder/dummy_models/show.html.erb +1 -0
- data/spec/dummy/app/views/html_new/dummy_models/new.html.erb +1 -0
- data/spec/dummy/app/views/html_scoped/dummy_models/index.html.erb +3 -0
- data/spec/dummy/app/views/html_show/dummy_models/show.html.erb +1 -0
- data/spec/dummy/app/views/html_update/dummy_models/edit.html.erb +1 -0
- data/spec/dummy/app/views/invalid_status/dummy_models/new.html.erb +1 -0
- data/spec/dummy/app/views/layouts/application.html.erb +4 -0
- data/spec/dummy/app/views/redirect/dummy_models/new.html.erb +1 -0
- data/spec/dummy/config/routes.rb +110 -0
- data/spec/dummy/db/migrate/20260806000000_add_slug_to_dummy_models.rb +7 -0
- data/spec/dummy/db/migrate/20260807000000_add_classroom_slug_to_dummy_models.rb +7 -0
- data/spec/dummy/db/schema.rb +3 -1
- data/spec/dummy/spec/factories/dummy_models.rb +1 -0
- data/spec/dummy_controller_spec.rb +1 -0
- data/spec/dummy_model_policy_spec.rb +6 -4
- data/spec/finder/dummy_models_controller_spec.rb +13 -0
- data/spec/html/dummy_models_controller_spec.rb +7 -0
- data/spec/html_create/dummy_models_controller_spec.rb +7 -0
- data/spec/html_destroy/dummy_models_controller_spec.rb +7 -0
- data/spec/html_finder/dummy_models_controller_spec.rb +11 -0
- data/spec/html_new/dummy_models_controller_spec.rb +7 -0
- data/spec/html_scoped/dummy_models_controller_spec.rb +7 -0
- data/spec/html_show/dummy_models_controller_spec.rb +7 -0
- data/spec/html_update/dummy_models_controller_spec.rb +7 -0
- data/spec/invalid_status/dummy_models_controller_spec.rb +10 -0
- data/spec/nested/dummy_models_controller_spec.rb +12 -0
- data/spec/nested_route/dummy_models_controller_spec.rb +19 -0
- data/spec/redirect/dummy_models_controller_spec.rb +23 -0
- data/spec/redirect_auth/dummy_models_controller_spec.rb +11 -0
- data/spec/scoped/dummy_models_controller_spec.rb +7 -0
- data/spec/scoped_params/dummy_models_controller_spec.rb +12 -0
- data/spec/simple_crud/authorization/action_policy_adapter_spec.rb +1 -9
- data/spec/simple_crud/authorization/can_can_can_adapter_spec.rb +1 -7
- data/spec/simple_crud/authorization/pundit_adapter_spec.rb +41 -0
- data/spec/simple_crud/controller_helpers_spec.rb +49 -0
- data/spec/simple_crud/pagination/adapters_spec.rb +37 -13
- data/spec/simple_crud/rspec_config_spec.rb +100 -0
- data/spec/simple_crud_configuration_spec.rb +19 -16
- data/spec/simple_crud_controller_spec.rb +4 -4
- data/spec/spec_helper.rb +1 -0
- data/spec/strict/dummy_models_controller_spec.rb +23 -0
- data/spec/unpaginated_scoped/dummy_models_controller_spec.rb +7 -0
- metadata +108 -4
- data/lib/spec/shared_examples/helpers.rb +0 -50
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e280efd8ead44768d16e0ba663e25a40d6972b53e253281d1c7eac0735421b64
|
|
4
|
+
data.tar.gz: 78625a2a759ca5fda1e7d41e421db92ed721f2e2c343524f0580cd810d32ac7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 406a83cc98deaa7fa3ba5f7ccb9a12486a903b6aef81bddd0a5f4f9c595ffa64e7414cb8a718d4a4fe96f987fcefaf34a8b201cbe16aab4db2721eea0bd19397
|
|
7
|
+
data.tar.gz: 7177530ba1a01fcf526c406a14f3bf64bfcf895ce5675f4acca5f24f28c57db90859ddab43866d314f70e723cfebda24bc5930d64ac66a01d29bc6246792c04a
|
data/.gitignore
CHANGED
|
@@ -147,6 +147,11 @@ config/initializers/secret_token.rb
|
|
|
147
147
|
## Environment normalization:
|
|
148
148
|
/.bundle
|
|
149
149
|
/vendor/bundle
|
|
150
|
+
# Appraisal gemfiles vendor their own bundles; never commit those.
|
|
151
|
+
/gemfiles/.bundle/
|
|
152
|
+
/gemfiles/vendor/
|
|
153
|
+
# Local tooling logs.
|
|
154
|
+
/.setting/
|
|
150
155
|
|
|
151
156
|
# these should all be checked in to normalize the environment:
|
|
152
157
|
# Gemfile.lock, .ruby-version, .ruby-gemset
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
## Change log
|
|
2
2
|
|
|
3
|
+
### V0.3.1
|
|
4
|
+
|
|
5
|
+
Shared examples configuration:
|
|
6
|
+
* Every setting can be overridden per controller or per example via `simple_crud:` RSpec metadata. Metadata takes precedence over `SimpleCrud::RSpec.configure` and the defaults, and all reads happen at example runtime, so per-resource tweaks need no around hooks, no global mutation and no restore logic.
|
|
7
|
+
* New `model_attributes` setting (default: `{ owner_association => current_user }`) decides how records are built for the examples, so multi-key (`{ user:, project: }`), non-user-owned (`{ project: project }`) or owner-less models work without editing the shared examples.
|
|
8
|
+
* Dedicated examples (`*_with_block`, `*_with_build`, `*_with_scope`) no longer assume the gem's dummy app: request params derive from `params_for`/`owner_params`, authentication wraps conditionally on the action's `authenticate:` option, and render-block actions are asserted on status and persistence effects only (a block's response body is app-defined by definition).
|
|
9
|
+
* The finder/build dedicated families (`show/update/destroy with finder`, `not found with finder`, `new with build`) adapt to each action's declared options too: render-block actions are asserted success-only, `html: true` actions assert the rendered template or redirect, and plain JSON keeps the status/body assertions. Server-rendered apps no longer need to skip these families.
|
|
10
|
+
* `index with scope` builds its records through new `scoped_attributes` (default: `model_attributes`) and `other_scoped_attributes` (default: `model_attributes.merge(owner_association => other_user)`) settings instead of hardcoding `owner_association => user`, so factories without that attribute keep working and multi-key models express their scope via metadata. The assertion follows the controller: `assigns(:records)` ids for `html: true`, the wor-paginate envelope for paginated JSON, and the bare serialized array otherwise.
|
|
11
|
+
* Actions declared with a render block expose `block: true` in the controller metadata, and the shared examples treat them accordingly: failure/invalid paths assert persistence effects only (the block decides how the response is rendered), while default flows keep their pinned statuses/templates.
|
|
12
|
+
* The base destroy example covers the failure path (record always kept; non-block flows respond 422 as JSON or re-render `show` as HTML).
|
|
13
|
+
* New optional `created_record_check` setting: a lambda receiving the persisted record in the create/update success paths, to assert ownership/scope that `count == 1` alone can't catch.
|
|
14
|
+
|
|
15
|
+
### V0.3.0
|
|
16
|
+
|
|
17
|
+
Server-rendered (HTML) support:
|
|
18
|
+
* `html: true` or a render block on every action: `:index` renders `index.html.erb` with the paginated records as `@records`; `:show`/`:new` expose `@record`; `:create`/`:update` redirect to the record on success and re-render the form with errors on failure; `:destroy` redirects to the collection (re-rendering `show.html.erb` when a callback aborts). Blocks receive the records (`:index`), the record (`:show`/`:new`) or `(record, saved)` (`:create`/`:update`/`:destroy`) and override the auto-render.
|
|
19
|
+
* New `:new` action: builds a record, authorizes it, and renders `new.html.erb` (or returns JSON in API mode).
|
|
20
|
+
|
|
21
|
+
Custom lookups, scoping and building:
|
|
22
|
+
* `finder:` on `:show`/`:update`/`:destroy`/`:edit`: a `Proc`/lambda invoked with the controller's params, or a `Symbol` naming a class method on the model. Defaults to `klass.find(params[:id])`.
|
|
23
|
+
* New `simple_crud_for :edit`: the find-instead-of-build twin of `:new`. It looks the record up (honoring `finder:`), authorizes it and renders `edit.html.erb` with `@record` in HTML mode, returning the record as JSON otherwise. A matching `simple crud for edit` shared example covers the authorized/denied, not-found and html/json/block paths.
|
|
24
|
+
* `build:` on `:new`/`:create` for owner-scoped or nested builds (`current_user.projects.build`); runs with the controller as `self`, and `:create` assigns the permitted params to the built record before saving.
|
|
25
|
+
* With `authorize: true`, `:index` paginates the Pundit `policy_scope` of the model (falling back to the full relation when no `Scope` is defined) instead of `klass.all`, with a per-action `scope: ->(user[, params]) { ... }` override.
|
|
26
|
+
* `redirect:` on HTML-mode `:create`/`:update`/`:destroy`: a `Proc` called with the record, or a literal path, overriding the default success redirect.
|
|
27
|
+
|
|
28
|
+
Validation errors:
|
|
29
|
+
* `:create` returns `201` / `422 { errors: [...] }` and `:update` returns `200` / `422` instead of raising; `raise_on_invalid: true` keeps strict `create!`/`update!` semantics.
|
|
30
|
+
|
|
31
|
+
Authorization changes:
|
|
32
|
+
* Breaking: `authorize: true` now always enforces policy checks, including for `authenticate: false` actions, which reach policies with a `nil` user. Policies must tolerate a `nil` user.
|
|
33
|
+
* Breaking: `:index` authorizes the model class instead of an unsaved instance, matching the Pundit/CanCanCan collection conventions. `index?` policies can no longer inspect record state (there is none), so write them against the user.
|
|
34
|
+
* `SimpleCrud::Config.user_method` (default `:current_user`) selects the controller method providing the user to policies and scope lambdas, so apps with other conventions (`current_admin`, ...) need no shims.
|
|
35
|
+
|
|
36
|
+
Adapter interfaces:
|
|
37
|
+
* Authorization adapters gain `policy_scope`; pagination adapters gain `paginated_records`, so HTML-mode `:index` can fetch the current page without rendering. Both are implemented by every shipped adapter (Pundit, CanCanCan, Action Policy / wor-paginate, Kaminari, will_paginate, Pagy).
|
|
38
|
+
|
|
39
|
+
Fixes:
|
|
40
|
+
* Kaminari adapter: string query params corrupted offsets (`?page=3&per_page=10` resolved to `OFFSET 1010`), and `per_page` now respects `Kaminari.config.max_per_page`.
|
|
41
|
+
* Custom finders returning anything but a single record raise `ActiveRecord::RecordNotFound` instead of failing later with an obscure error.
|
|
42
|
+
* Invalid `simple_crud_for` usage (unknown action, missing policy, missing serializer) raises `ArgumentError` instead of throwing uncatchable string tags.
|
|
43
|
+
|
|
44
|
+
Testing library:
|
|
45
|
+
* The shared examples are a reusable library: configure via `SimpleCrud::RSpec.configure`, wire up explicitly with `SimpleCrud::RSpec.install!`. Every app-specific assumption has an overridable default matching the gem's own stack (Devise-JWT + FactoryBot + Pundit + AMS): `authenticate`/`current_user`/`other_user`, `create_record`/`create_records`/`params_for`, `owner_association`/`required_attribute`/`required_error`, `finder_key`, `params_key`, `route_params`, `invalid_status`, `unauthenticated_status`, `assert_html_template`, `policy_class`, `serializer_class`.
|
|
46
|
+
* Examples are HTML-aware (branching on the `html:` option, inferring it for block-form actions) and issue requests by action name, so one set covers API and server-rendered controllers, namespaced and nested routes alike.
|
|
47
|
+
* Refactor `SimpleCrudController` helpers into `SimpleCrud::ControllerHelpers`, dedupe repeated test code, and require `rails-controller-testing` for template assertions. Tested across Rails 6.1-8.1.
|
|
48
|
+
|
|
3
49
|
### V0.2.1
|
|
4
50
|
* Rename the published gem from `simple_crud` to `wor-simple_crud`. `simple_crud` is already taken on RubyGems by an unrelated gem, and RubyGems also rejects names that differ from an existing one only by hyphen/underscore (so `simple-crud` was rejected too). Install with `gem 'wor-simple_crud', require: 'simple_crud'` (the require path is unchanged).
|
|
5
51
|
* Drop the last Wolox references now that this gem lives at icoluccio/simple-crud, and point the wor-paginate link and CI/gem badges at their new homes.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -14,7 +14,10 @@ SimpleCrud
|
|
|
14
14
|
- [Options](#options)
|
|
15
15
|
- [Paginate](#paginate)
|
|
16
16
|
- [Authorize](#authorize)
|
|
17
|
+
- [Authenticate](#authenticate)
|
|
17
18
|
- [Serializer](#serializer)
|
|
19
|
+
- [HTML](#html)
|
|
20
|
+
- [Finder](#finder)
|
|
18
21
|
- [Shared examples](#shared-examples)
|
|
19
22
|
- [Contributing](#contributing)
|
|
20
23
|
- [Releases](#releases)
|
|
@@ -64,7 +67,7 @@ Before SimpleCrud can be used, some boilerplate is needed. Add the following to
|
|
|
64
67
|
```ruby
|
|
65
68
|
include Pundit::Authorization
|
|
66
69
|
include Wor::Paginate
|
|
67
|
-
extend
|
|
70
|
+
extend SimpleCrudController
|
|
68
71
|
|
|
69
72
|
before_action :set_params
|
|
70
73
|
|
|
@@ -91,6 +94,8 @@ simple_crud_for :show
|
|
|
91
94
|
simple_crud_for :index
|
|
92
95
|
simple_crud_for :create
|
|
93
96
|
simple_crud_for :destroy
|
|
97
|
+
simple_crud_for :new
|
|
98
|
+
simple_crud_for :edit
|
|
94
99
|
```
|
|
95
100
|
|
|
96
101
|
Each method supports different options, as in:
|
|
@@ -102,6 +107,12 @@ simple_crud_for :index, paginate: false, authorize: false, serializer: CustomSer
|
|
|
102
107
|
- Authorize: whether it should check authorization via the configured authorization adapter (Pundit by default)
|
|
103
108
|
- Authenticate: whether it should use Devise to check for a current_user
|
|
104
109
|
- Serializer: specify a particular serializer you should use
|
|
110
|
+
- Html: renders the action's ERB template instead of JSON (valid for `:index`, `:show`, `:new`, `:edit`, `:create`, `:update` and `:destroy`). Only meaningful in controllers that render templates
|
|
111
|
+
- Scope: only valid for `:index`. A `Proc`/`lambda` taking `current_user` (plus the controller's `params` if it takes a second argument) that returns the relation to list, overriding the default `policy_scope`. The user is resolved via `SimpleCrud::Config.user_method` (`:current_user` by default; set it to e.g. `:current_admin`)
|
|
112
|
+
- Finder: only valid for `:show`, `:update`, `:destroy` and `:edit`. A `Proc`/`lambda` (invoked with the controller's params) or a `Symbol` naming a class method on the model, used to look up the record instead of `klass.find(params[:id])`.
|
|
113
|
+
- Build: only valid for `:new` and `:create`. A `Proc`/`lambda` that builds the record (invoked with the controller as `self`, so `current_user`, `params` and any instance variables are available), for building nested or owner-scoped records like `current_user.projects.build`. `:create` then assigns the permitted params to the built record before saving
|
|
114
|
+
- Raise_on_invalid: only valid for `:create` and `:update`. Keeps the strict `create!`/`update!` semantics (raising on invalid input) instead of returning `422` with the validation errors
|
|
115
|
+
- Redirect: only valid for HTML-mode `:create`, `:update` and `:destroy`. A `Proc`/`lambda` (called with the record) or a literal path used as the success redirect target. Defaults to the record (`:create`/`:update`) or the model's collection path (`:destroy`)
|
|
105
116
|
|
|
106
117
|
You'll need a few things so they work correctly:
|
|
107
118
|
|
|
@@ -143,10 +154,11 @@ end
|
|
|
143
154
|
|
|
144
155
|
SimpleCrud.configure { |config| config.pagination_adapter = MyPaginationAdapter.new }
|
|
145
156
|
```
|
|
146
|
-
|
|
147
157
|
#### Authorize
|
|
148
158
|
Authorization checks go through [Pundit](https://github.com/varvet/pundit) by default. Name the policy after the model plus `Policy`, e.g. `AuthorPolicy`, written as a regular Pundit policy:
|
|
149
159
|
|
|
160
|
+
> **Note:** authorization runs whenever `authorize: true`, independently of `authenticate`. With no signed-in user the policy receives `nil` as the user, so write policies that tolerate it (e.g. `user.nil? ? false : ...`). Devise provides `authenticate_user!` and `current_user` for you; non-Devise apps must define a `current_user` (or point `SimpleCrud::Config.user_method` at their own method) for policies and `scope:` lambdas to receive the user.
|
|
161
|
+
|
|
150
162
|
```ruby
|
|
151
163
|
class AuthorPolicy
|
|
152
164
|
attr_reader :user, :author
|
|
@@ -159,10 +171,30 @@ class AuthorPolicy
|
|
|
159
171
|
def show?
|
|
160
172
|
user.present?
|
|
161
173
|
end
|
|
174
|
+
|
|
175
|
+
class Scope
|
|
176
|
+
def initialize(user, scope)
|
|
177
|
+
@user = user
|
|
178
|
+
@scope = scope
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Used by :index to scope the listed records to the current user.
|
|
182
|
+
def resolve
|
|
183
|
+
@scope.where(user: @user)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
162
186
|
end
|
|
163
187
|
|
|
164
188
|
```
|
|
165
189
|
|
|
190
|
+
When `authorize: true`, the `:index` action paginates the Pundit `policy_scope` of the model (falling back to the full relation when no `Scope` is defined) instead of `klass.all`, so "only my records" scoping works out of the box. Override the scope per action with the `scope:` option, a callable that receives the user resolved via `SimpleCrud::Config.user_method` (`nil` when there is none, and `params` too when it takes a second argument):
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
SimpleCrud.configure { |c| c.user_method = :current_admin } # non-Devise naming conventions
|
|
194
|
+
simple_crud_for :index, scope: ->(user) { Model.visible_to(user) }
|
|
195
|
+
simple_crud_for :index, scope: ->(user, params) { Model.where(status: params[:status]).visible_to(user) }
|
|
196
|
+
```
|
|
197
|
+
|
|
166
198
|
Prefer CanCanCan or Action Policy instead? Both have adapters ready to go:
|
|
167
199
|
|
|
168
200
|
```ruby
|
|
@@ -200,6 +232,13 @@ class MyAuthorizationAdapter
|
|
|
200
232
|
def policy_defined?(model_class)
|
|
201
233
|
Kernel.const_defined?("#{model_class}Policy")
|
|
202
234
|
end
|
|
235
|
+
|
|
236
|
+
# Called when rendering :index with authorize: true. Must return the
|
|
237
|
+
# relation the current user is allowed to list. The default returns
|
|
238
|
+
# the full relation.
|
|
239
|
+
def policy_scope(controller, model_class)
|
|
240
|
+
model_class.all
|
|
241
|
+
end
|
|
203
242
|
end
|
|
204
243
|
|
|
205
244
|
SimpleCrud.configure do |config|
|
|
@@ -219,6 +258,67 @@ class AuthorSerializer < ActiveModel::Serializer
|
|
|
219
258
|
end
|
|
220
259
|
```
|
|
221
260
|
|
|
261
|
+
#### HTML
|
|
262
|
+
For server-rendered apps, `html: true` renders the action's ERB template instead of JSON, so be sure the controller can render templates (e.g. `ActionController::Base`). Behavior per action:
|
|
263
|
+
|
|
264
|
+
- `:index` renders `index.html.erb` with the paginated records exposed as `@records` (pagination still applies, or use `paginate: false`).
|
|
265
|
+
- `:show` renders `show.html.erb` with the record exposed as `@record` (custom `finder:` still applies).
|
|
266
|
+
- `:new` builds a new record, authorizes it, and renders `new.html.erb` with it exposed as `@record`.
|
|
267
|
+
- `:edit` looks the record up (honoring `finder:`), authorizes it, and renders `edit.html.erb` with it exposed as `@record`.
|
|
268
|
+
- `:create` saves and redirects to the created record on success, or re-renders `new.html.erb` (with `@record` and its errors) on failure.
|
|
269
|
+
- `:update` saves and redirects to the record on success, or re-renders `edit.html.erb` on failure.
|
|
270
|
+
- `:destroy` destroys and redirects to the collection (`redirect_to Model`) on success, or re-renders `show.html.erb` if a callback aborts the destroy.
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
simple_crud_for :index, html: true
|
|
274
|
+
simple_crud_for :show, html: true
|
|
275
|
+
simple_crud_for :new, html: true
|
|
276
|
+
simple_crud_for :edit, html: true
|
|
277
|
+
simple_crud_for :create, html: true
|
|
278
|
+
simple_crud_for :update, html: true
|
|
279
|
+
simple_crud_for :destroy, html: true
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Or pass a block that renders explicitly, overriding the auto-render. The block receives the records for `:index`, the record for `:show`/`:new`, or the record plus a saved flag for `:create`/`:update`/`:destroy`. Passing a block also implies `html: true` for the shared examples (so a server-rendered block is asserted as HTML); if your block renders JSON instead, pass `html: false` explicitly:
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
simple_crud_for :index do |records|
|
|
286
|
+
render :index, locals: { models: records }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
simple_crud_for :create do |record, saved|
|
|
290
|
+
saved ? redirect_to(record) : render(:new, locals: { model: record })
|
|
291
|
+
end
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### Build
|
|
295
|
+
`simple_crud_for :new` and `simple_crud_for :create` build the record with `klass.new`, which can't express owner-scoped or nested records (`current_user.projects.build`, `@project.tasks.build`). Pass a `build:` lambda; it runs with the controller as `self`, so `current_user`, `params` and any instance variables set by a `before_action` are available:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
simple_crud_for :new, build: -> { current_user.projects.build }
|
|
299
|
+
simple_crud_for :create, build: -> { @project.tasks.build }
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`:create` assigns the permitted params to the built record before saving, so the owner/parent association survives. `:update` keeps finding the record via the `finder:`.
|
|
303
|
+
|
|
304
|
+
#### Edit
|
|
305
|
+
`simple_crud_for :edit` is the find-instead-of-build twin of `:new`, the last hand-written CRUD action in server-rendered apps. It looks the record up (honoring `finder:` exactly like `:show`), authorizes it, and renders `edit.html.erb` with the record exposed as `@record`; in JSON mode it returns the record:
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
simple_crud_for :edit, finder: ->(params) { Project.find_by!(slug: params[:slug]) }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### Finder
|
|
312
|
+
By default records are looked up by primary key via `klass.find(params[:id])`. The `finder:` option on `:show`, `:update` and `:destroy` replaces that with any lookup you want: pretty URLs (`resources :posts, param: :slug`), tokens, composite keys, or scoping by a parent resource. Pass a `Proc`/`lambda` that maps the controller's `params` to a record, or a `Symbol` naming a class method on the model that takes the params:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
simple_crud_for :show, finder: ->(params) { Model.find_by!(token: params[:token]) }
|
|
316
|
+
simple_crud_for :update, finder: :find_by_slug
|
|
317
|
+
simple_crud_for :destroy, finder: ->(params) { current_user.models.find(params[:id]) }
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
When omitted it defaults to `klass.find(params[:id])`, and `not_found` is still returned whenever the finder finds no record.
|
|
321
|
+
|
|
222
322
|
### Shared examples
|
|
223
323
|
While optional, using the included shared examples saves you from writing the standard test cases for the methods. You can even use them if you didn't use `simple_crud_for`, as a set of basic tests. To include them, just add `require 'simple_crud/rspec'` to your `rails_helper.rb` file and add the lines you need to your `*_spec.rb` files:
|
|
224
324
|
```ruby
|
|
@@ -233,8 +333,106 @@ describe V1::Backoffice::AuthorsController do
|
|
|
233
333
|
end
|
|
234
334
|
```
|
|
235
335
|
|
|
336
|
+
The `create` and `update` examples cover the `422` response with validation errors (skipped when `raise_on_invalid: true`, or whenever the action uses a render block, since the block decides how the response is rendered), and all base examples adapt to `html: true` controllers (asserting the rendered template/redirect instead of JSON). Controllers using the extra options can include their dedicated examples too:
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
include_examples 'simple crud for new' # the :new action
|
|
340
|
+
include_examples 'simple crud for edit' # the :edit action
|
|
341
|
+
include_examples 'simple crud for index with block' # render block
|
|
342
|
+
include_examples 'simple crud for index with scope' # scope: ->(user) { ... }
|
|
343
|
+
include_examples 'simple crud for show with block' # render block on :show
|
|
344
|
+
include_examples 'simple crud for new with block' # render block on :new
|
|
345
|
+
include_examples 'simple crud for create with block' # render block on :create
|
|
346
|
+
include_examples 'simple crud for destroy with block' # render block on :destroy
|
|
347
|
+
include_examples 'simple crud for show with finder' # finder on :show
|
|
348
|
+
include_examples 'simple crud for update with finder' # finder on :update
|
|
349
|
+
include_examples 'simple crud for destroy with finder' # finder on :destroy
|
|
350
|
+
include_examples 'simple crud for new with build' # build: -> { ... } on :new
|
|
351
|
+
include_examples 'simple crud for create with build' # build: -> { ... } on :create
|
|
352
|
+
```
|
|
353
|
+
|
|
236
354
|
It's not needed to specify paginate: true and such, since the shared examples will use the configuration that was originally passed to simple_crud_for
|
|
237
355
|
|
|
356
|
+
#### Adopting the shared examples
|
|
357
|
+
The shared examples assume the gem's own stack by default (Devise-JWT authentication, FactoryBot, Pundit, ActiveModel Serializers). Wiring is opt-in: require the file and call `SimpleCrud::RSpec.install!` in `spec/spec_helper.rb` (or `rails_helper.rb`), then configure anything your app differs on:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
SimpleCrud::RSpec.configure do |config|
|
|
361
|
+
# How the current user (and a secondary "other user") is built.
|
|
362
|
+
config.current_user = -> { User.create!(email: 'user@example.com', password: 'secret') }
|
|
363
|
+
config.other_user = -> { User.create!(email: 'other@example.com', password: 'secret') }
|
|
364
|
+
|
|
365
|
+
# How to sign the current user in for a request (no Devise-JWT here).
|
|
366
|
+
config.authenticate = -> { request.session[:user_id] = current_user.id }
|
|
367
|
+
|
|
368
|
+
# How records and create/update params are built (no FactoryBot here).
|
|
369
|
+
config.create_record = ->(klass, attributes) { klass.create!(attributes) }
|
|
370
|
+
config.create_records = ->(klass, count, attributes) { count.times.map { klass.create!(attributes) } }
|
|
371
|
+
config.params_for = ->(klass) { klass.new.attributes.slice('title') }
|
|
372
|
+
|
|
373
|
+
# The owner association used when building records for the examples
|
|
374
|
+
# (model_attributes defaults to `{ owner_association => current_user }`;
|
|
375
|
+
# override model_attributes directly for multi-key or non-user-owned models).
|
|
376
|
+
config.owner_association = :owner
|
|
377
|
+
config.model_attributes = -> { { owner: current_user } }
|
|
378
|
+
config.required_attribute = :title
|
|
379
|
+
config.required_error = "Title can't be blank"
|
|
380
|
+
|
|
381
|
+
# Redirect-based apps: unauthenticated requests get a redirect, not a 401.
|
|
382
|
+
# And if the rendered template name doesn't match the action, drop the
|
|
383
|
+
# render_template assertion.
|
|
384
|
+
config.unauthenticated_status = :found
|
|
385
|
+
config.assert_html_template = false
|
|
386
|
+
|
|
387
|
+
# Server-rendered apps usually use nested strong params
|
|
388
|
+
# (params.require(:task).permit(:name)); wrap request bodies under the
|
|
389
|
+
# model's params key instead of posting flat params.
|
|
390
|
+
config.params_key = :task
|
|
391
|
+
|
|
392
|
+
# Nested resources (/projects/:project_slug/tasks): extra params
|
|
393
|
+
# (e.g. the parent slug) added to every request.
|
|
394
|
+
config.route_params = -> { { project_slug: project.slug } }
|
|
395
|
+
|
|
396
|
+
# Re-render the form with a 422 on validation failure (instead of 200).
|
|
397
|
+
config.invalid_status = :unprocessable_entity
|
|
398
|
+
|
|
399
|
+
# How the 'simple crud for index with scope' example builds the records
|
|
400
|
+
# that match the controller's scope and the ones that must be excluded.
|
|
401
|
+
# Defaults derive from model_attributes (mine) plus an owner_association
|
|
402
|
+
# => other_user variant (theirs); override both when your scope keys on
|
|
403
|
+
# something other than the owner association.
|
|
404
|
+
config.scoped_attributes = -> { { project: project } }
|
|
405
|
+
config.other_scoped_attributes = -> { { project: other_project } }
|
|
406
|
+
|
|
407
|
+
# Pundit policy and serializer class lookup.
|
|
408
|
+
config.policy_class = ->(klass) { "#{klass}Policy".constantize }
|
|
409
|
+
config.serializer_class = ->(model) { "#{model.class}Serializer".constantize }
|
|
410
|
+
|
|
411
|
+
# Optional extra assertions on the persisted record in the create/update
|
|
412
|
+
# success paths (catches regressions that save into the wrong parent).
|
|
413
|
+
config.created_record_check = ->(record) { expect(record.project).to eq(project) }
|
|
414
|
+
end
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Each setting has a sensible default, so you only override what differs. Callable settings (`current_user`, `authenticate`, `create_record`, `create_records`, `params_for`, `model_attributes`, `scoped_attributes`, `other_scoped_attributes`, `policy_class`, `serializer_class`) run in the example-group context, so they can call `request`, `create`, `current_user`, etc.; `created_record_check` receives the persisted record. The examples are controller-agnostic (they issue requests by action name, not hardcoded paths), so they work for namespaced and nested controllers alike. If you keep `assert_html_template` on (the default), add `gem 'rails-controller-testing'` for the `render_template` matcher.
|
|
418
|
+
|
|
419
|
+
#### Per-controller overrides via metadata
|
|
420
|
+
|
|
421
|
+
Any setting can also be overridden per controller (or per example) with `simple_crud:` metadata instead of globally, useful when the app matches the defaults except for one resource. Metadata wins over global config, and every read happens at example runtime, so there are no around hooks or state to restore:
|
|
422
|
+
|
|
423
|
+
```ruby
|
|
424
|
+
RSpec.describe TasksController, type: :controller, simple_crud: {
|
|
425
|
+
model_attributes: -> { { project: project } },
|
|
426
|
+
route_params: -> { { project_slug: project.slug } }
|
|
427
|
+
} do
|
|
428
|
+
let(:project) { create(:project, owner: current_user) }
|
|
429
|
+
|
|
430
|
+
include_examples 'simple crud for show'
|
|
431
|
+
end
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Callable settings resolve in the example context, so their lambdas can close over the spec's `let`s; that's how records land in the same project the route params point at.
|
|
435
|
+
|
|
238
436
|
## Contributing
|
|
239
437
|
|
|
240
438
|
1. Fork it
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
# Builds the lambda installed as each action declared with simple_crud_for.
|
|
5
|
+
module ActionLambdas
|
|
6
|
+
def crud_lambda_for_show(klass, parameters = {}, &block)
|
|
7
|
+
lambda do
|
|
8
|
+
authenticate_user! if parameters[:authenticate]
|
|
9
|
+
requested = SimpleCrudController.find_record(klass, self, parameters)
|
|
10
|
+
|
|
11
|
+
options = {}.merge(serializer: parameters[:serializer]).compact
|
|
12
|
+
SimpleCrudController.maybe_authorize(self, requested, parameters)
|
|
13
|
+
SimpleCrudController.render_show(self, requested, options, parameters, &block)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def crud_lambda_for_new(klass, parameters = {}, &block)
|
|
18
|
+
lambda do
|
|
19
|
+
authenticate_user! if parameters[:authenticate]
|
|
20
|
+
record = SimpleCrudController.build_record(self, klass, parameters)
|
|
21
|
+
SimpleCrudController.maybe_authorize(self, record, parameters)
|
|
22
|
+
SimpleCrudController.render_new(self, record, parameters, &block)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The find-instead-of-build twin of :new.
|
|
27
|
+
def crud_lambda_for_edit(klass, parameters = {}, &block)
|
|
28
|
+
lambda do
|
|
29
|
+
authenticate_user! if parameters[:authenticate]
|
|
30
|
+
requested = SimpleCrudController.find_record(klass, self, parameters)
|
|
31
|
+
SimpleCrudController.maybe_authorize(self, requested, parameters)
|
|
32
|
+
SimpleCrudController.render_edit(self, requested, parameters, &block)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def crud_lambda_for_index(klass, parameters = {}, &block)
|
|
37
|
+
lambda do
|
|
38
|
+
authenticate_user! if parameters[:authenticate]
|
|
39
|
+
SimpleCrudController.maybe_authorize(self, klass, parameters)
|
|
40
|
+
options = {}.merge(each_serializer: parameters[:serializer]).compact
|
|
41
|
+
SimpleCrudController.render_index(self, klass, options, parameters, &block)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def crud_lambda_for_create(klass, parameters = {}, &block)
|
|
46
|
+
lambda do
|
|
47
|
+
authenticate_user! if parameters[:authenticate]
|
|
48
|
+
permitted_params = send("#{self.class.simple_crud_controller_model.to_s.underscore}_params")
|
|
49
|
+
record = SimpleCrudController.build_record(self, klass, parameters)
|
|
50
|
+
record.assign_attributes(permitted_params)
|
|
51
|
+
SimpleCrudController.maybe_authorize(self, record, parameters)
|
|
52
|
+
persist = ->(bang:) { bang ? record.save! : record.save }
|
|
53
|
+
options = { status: :created, failure_template: :new }
|
|
54
|
+
SimpleCrudController.save_and_render(self, record, parameters, options, persist, &block)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def crud_lambda_for_update(klass, parameters = {}, &block)
|
|
59
|
+
lambda do
|
|
60
|
+
authenticate_user! if parameters[:authenticate]
|
|
61
|
+
requested = SimpleCrudController.find_record(klass, self, parameters)
|
|
62
|
+
SimpleCrudController.maybe_authorize(self, requested, parameters)
|
|
63
|
+
permitted_params = send("#{self.class.simple_crud_controller_model.to_s.underscore}_params")
|
|
64
|
+
persist = ->(bang:) { bang ? requested.update!(permitted_params) : requested.update(permitted_params) }
|
|
65
|
+
options = { status: :ok, failure_template: :edit }
|
|
66
|
+
SimpleCrudController.save_and_render(self, requested, parameters, options, persist, &block)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def crud_lambda_for_destroy(klass, parameters = {}, &block)
|
|
71
|
+
lambda do
|
|
72
|
+
authenticate_user! if parameters[:authenticate]
|
|
73
|
+
requested = SimpleCrudController.find_record(klass, self, parameters)
|
|
74
|
+
SimpleCrudController.maybe_authorize(self, requested, parameters)
|
|
75
|
+
options = { status: :ok, failure_template: :show, redirect: parameters[:redirect] || klass }
|
|
76
|
+
persist = ->(bang:) { bang ? requested.destroy! : requested.destroy }
|
|
77
|
+
SimpleCrudController.persist_and_render(self, requested, parameters, options, persist, &block)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -15,6 +15,13 @@ module SimpleCrud
|
|
|
15
15
|
def policy_defined?(model_class)
|
|
16
16
|
raise NotImplementedError
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
# Called when rendering :index with authorize: true. Must return the
|
|
20
|
+
# relation the current user is allowed to list. Defaults to the full
|
|
21
|
+
# relation so no scoping dependency is required.
|
|
22
|
+
def policy_scope(_controller, model_class)
|
|
23
|
+
model_class.all
|
|
24
|
+
end
|
|
18
25
|
end
|
|
19
26
|
end
|
|
20
27
|
end
|
|
@@ -16,6 +16,12 @@ module SimpleCrud
|
|
|
16
16
|
def policy_defined?(model_class)
|
|
17
17
|
Kernel.const_defined?("#{model_class}Policy")
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
def policy_scope(controller, model_class)
|
|
21
|
+
controller.send(:policy_scope, model_class)
|
|
22
|
+
rescue Pundit::NotDefinedError
|
|
23
|
+
model_class.all
|
|
24
|
+
end
|
|
19
25
|
end
|
|
20
26
|
end
|
|
21
27
|
end
|
data/lib/simple_crud/config.rb
CHANGED
|
@@ -4,7 +4,7 @@ module SimpleCrud
|
|
|
4
4
|
# Holds simple_crud's configurable pieces, set via SimpleCrud.configure.
|
|
5
5
|
class Config
|
|
6
6
|
class << self
|
|
7
|
-
attr_writer :authorization_adapter, :pagination_adapter
|
|
7
|
+
attr_writer :authorization_adapter, :pagination_adapter, :user_method
|
|
8
8
|
|
|
9
9
|
def authorization_adapter
|
|
10
10
|
@authorization_adapter ||= begin
|
|
@@ -19,6 +19,11 @@ module SimpleCrud
|
|
|
19
19
|
Pagination::WorPaginateAdapter.new
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
# Controller method providing the current user; override for non-Devise apps.
|
|
24
|
+
def user_method
|
|
25
|
+
@user_method ||= :current_user
|
|
26
|
+
end
|
|
22
27
|
end
|
|
23
28
|
end
|
|
24
29
|
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
# Class-level helpers shared by the CRUD lambdas defined in SimpleCrudController.
|
|
5
|
+
module ControllerHelpers
|
|
6
|
+
def maybe_authorize(controller, record, parameters)
|
|
7
|
+
return unless parameters[:authorize]
|
|
8
|
+
|
|
9
|
+
SimpleCrud::Config.authorization_adapter.authorize(controller, record)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render_index(controller, klass, options, parameters, &block)
|
|
13
|
+
relation = index_relation(controller, klass, parameters)
|
|
14
|
+
|
|
15
|
+
if parameters[:html] || block
|
|
16
|
+
records = index_records(controller, relation, options, parameters)
|
|
17
|
+
controller.instance_variable_set(:@records, records)
|
|
18
|
+
block ? controller.instance_exec(records, &block) : controller.render(:index)
|
|
19
|
+
elsif parameters[:paginate]
|
|
20
|
+
SimpleCrud::Config.pagination_adapter.paginate(controller, relation, options)
|
|
21
|
+
else
|
|
22
|
+
controller.render({ json: relation }.merge(options))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_show(controller, record, options, parameters, &block)
|
|
27
|
+
if parameters[:html] || block
|
|
28
|
+
controller.instance_variable_set(:@record, record)
|
|
29
|
+
block ? controller.instance_exec(record, &block) : controller.render(:show)
|
|
30
|
+
else
|
|
31
|
+
controller.render({ json: record }.merge(options))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def render_new(controller, record, parameters, &block)
|
|
36
|
+
render_form(controller, record, :new, parameters, &block)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def render_edit(controller, record, parameters, &block)
|
|
40
|
+
render_form(controller, record, :edit, parameters, &block)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def render_form(controller, record, template, parameters, &block)
|
|
44
|
+
if parameters[:html] || block
|
|
45
|
+
controller.instance_variable_set(:@record, record)
|
|
46
|
+
block ? controller.instance_exec(record, &block) : controller.render(template)
|
|
47
|
+
else
|
|
48
|
+
options = {}.merge(serializer: parameters[:serializer]).compact
|
|
49
|
+
controller.render({ json: record }.merge(options))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_record(controller, klass, parameters)
|
|
54
|
+
parameters[:build] ? controller.instance_exec(¶meters[:build]) : klass.new
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def index_relation(controller, klass, parameters)
|
|
58
|
+
if parameters[:scope]
|
|
59
|
+
call_scope(parameters[:scope], controller)
|
|
60
|
+
elsif parameters[:authorize]
|
|
61
|
+
SimpleCrud::Config.authorization_adapter.policy_scope(controller, klass)
|
|
62
|
+
else
|
|
63
|
+
klass.all
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def call_scope(scope, controller)
|
|
68
|
+
user_method = SimpleCrud::Config.user_method
|
|
69
|
+
user = controller.respond_to?(user_method) ? controller.public_send(user_method) : nil
|
|
70
|
+
return scope.call(user) if scope.arity == 1
|
|
71
|
+
|
|
72
|
+
scope.call(user, controller.params)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def index_records(controller, relation, options, parameters)
|
|
76
|
+
return relation unless parameters[:paginate]
|
|
77
|
+
|
|
78
|
+
SimpleCrud::Config.pagination_adapter.paginated_records(controller, relation, options)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def find_record(klass, controller, parameters)
|
|
82
|
+
record = lookup_record(klass, controller, parameters)
|
|
83
|
+
raise ActiveRecord::RecordNotFound, "couldn't find #{klass}" if record.nil?
|
|
84
|
+
unless record.is_a?(ActiveRecord::Base)
|
|
85
|
+
raise ActiveRecord::RecordNotFound, "#{klass} finder must return a single record"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
record
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def lookup_record(klass, controller, parameters)
|
|
92
|
+
finder = parameters[:finder]
|
|
93
|
+
return klass.find(controller.params[:id]) if finder.nil?
|
|
94
|
+
return klass.send(finder, controller.params) unless finder.respond_to?(:call)
|
|
95
|
+
|
|
96
|
+
finder.call(controller.params)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def persist_and_render(controller, record, parameters, options, persist, &block)
|
|
100
|
+
saved = parameters[:raise_on_invalid] ? persist.call(bang: true) : persist.call(bang: false)
|
|
101
|
+
return render_persisted(controller, record, saved, options) unless block || parameters[:html]
|
|
102
|
+
|
|
103
|
+
controller.instance_variable_set(:@record, record)
|
|
104
|
+
return controller.instance_exec(record, saved, &block) if block
|
|
105
|
+
|
|
106
|
+
if saved
|
|
107
|
+
controller.redirect_to(redirect_target(controller, record, options[:redirect]))
|
|
108
|
+
else
|
|
109
|
+
controller.render(options[:failure_template])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def save_and_render(controller, record, parameters, options, persist, &block)
|
|
114
|
+
persist_and_render(controller, record, parameters,
|
|
115
|
+
options.merge(redirect: parameters[:redirect] || record), persist, &block)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def redirect_target(controller, record, target)
|
|
119
|
+
target.is_a?(Proc) ? controller.instance_exec(record, &target) : target
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def render_persisted(controller, record, saved, options)
|
|
123
|
+
return controller.render(json: record, status: options[:status]) if saved
|
|
124
|
+
|
|
125
|
+
controller.render json: { errors: record.errors.full_messages }, status: 422
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -6,7 +6,13 @@ module SimpleCrud
|
|
|
6
6
|
# (the default, see WorPaginateAdapter).
|
|
7
7
|
module Adapter
|
|
8
8
|
# Called when paginate: true. Must render the response itself.
|
|
9
|
-
def paginate(controller,
|
|
9
|
+
def paginate(controller, relation, options)
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Called when rendering :index as HTML (html: true or a block given) and
|
|
14
|
+
# paginate: true. Must return the current page's records without rendering.
|
|
15
|
+
def paginated_records(controller, relation, options)
|
|
10
16
|
raise NotImplementedError
|
|
11
17
|
end
|
|
12
18
|
end
|