base_editing_bootstrap 1.5.1 → 1.7.0
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/CHANGELOG.md +25 -0
- data/README.md +2 -1
- data/app/controllers/base_editing_controller.rb +7 -1
- data/app/helpers/utilities/page_helper.rb +7 -3
- data/app/policies/base_model_policy.rb +2 -0
- data/app/views/base_editing/_editing_form_help_text.html.erb +7 -1
- data/lib/base_editing_bootstrap/VERSION +1 -1
- data/lib/base_editing_bootstrap/base_model.rb +11 -1
- data/lib/base_editing_bootstrap/forms/base.rb +9 -8
- data/lib/base_editing_bootstrap/searches/base.rb +4 -3
- data/lib/generators/base_editing_bootstrap/scaffold/templates/spec/model.rb.tt +4 -1
- data/spec/support/external_shared/base_editing_controller_helpers.rb +11 -6
- data/spec/support/external_shared/base_model.rb +17 -2
- data/spec/support/external_shared/pundit.rb +42 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c363b9569d715a8519f72345250ee7dd019de73ce87330e6a49d62acc0e0e10
|
4
|
+
data.tar.gz: 9d7499c1ba5fbf66c2d0f466237e7b69bce234d022d21cd9d74b920564b6960f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2243dc34ddba312f01465774a8df4662ea78e832885774966d29c685164201284aae102b8188a59c54d8b3c44a1c8af81227e04ef28a780651e6cdcd1abe6c1
|
7
|
+
data.tar.gz: 7c43ad6f7059a50c7719633e432d941ebd2433518712b9b5d20b5fff573cc802b24de785e81a96eebaf31c6d96639b2e3a99445dca3df086956d73a0529cc6d3
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
|
3
3
|
|
4
4
|
- - -
|
5
|
+
## 1.7.0 - 2025-02-11
|
6
|
+
#### Bug Fixes
|
7
|
+
- Add permits of ransack scopes - (5fa26c6) - Marino Bonetti
|
8
|
+
- Correct set select classes - (a5ef685) - Marino Bonetti
|
9
|
+
- Boolean_icon with nil value - (211c91f) - Marino Bonetti
|
10
|
+
- Better expection for spec helper - (65b26ce) - Marino Bonetti
|
11
|
+
#### Documentation
|
12
|
+
- Add documentation for help html version - (dbc1c01) - Marino Bonetti
|
13
|
+
#### Features
|
14
|
+
- Add HTML helper version in form field help - (f06c438) - Marino Bonetti
|
15
|
+
- Add Configurable distinct query in controller - (184d772) - Marino Bonetti
|
16
|
+
#### Tests
|
17
|
+
- Correct validation for selects - (e4b8016) - Marino Bonetti
|
18
|
+
|
19
|
+
- - -
|
20
|
+
|
21
|
+
## 1.6.0 - 2025-01-23
|
22
|
+
#### Features
|
23
|
+
- Add new test helper for association relation - (15690c7) - Marino Bonetti
|
24
|
+
#### Tests
|
25
|
+
- Fix association for order - (9e2bdd4) - Marino Bonetti
|
26
|
+
- Example order by ransack - (79cccc7) - Marino Bonetti
|
27
|
+
|
28
|
+
- - -
|
29
|
+
|
5
30
|
## 1.5.1 - 2025-01-22
|
6
31
|
#### Bug Fixes
|
7
32
|
- Ricerca template anche con contesto del controller - (ac93a9f) - Marino Bonetti
|
data/README.md
CHANGED
@@ -126,7 +126,8 @@ Utilizzo per modello base, in questo esempio prendiamo come modello Post come es
|
|
126
126
|
```
|
127
127
|
- è possibile customizzare
|
128
128
|
- un text help per ogni campo andando ad aggiungere nelle traduzioni la relativa
|
129
|
-
traduzione nella posizione: `it.activerecord.attributes.MODEL.FIELD/help_text`
|
129
|
+
traduzione nella posizione: `it.activerecord.attributes.MODEL.FIELD/help_text` oppure `help_text_html` in caso di
|
130
|
+
contenuto con html
|
130
131
|
- un blocco per l'unità di misura accanto al campo aggiungendo alle traduzioni:
|
131
132
|
`it.activerecord.attributes.MODEL.FIELD/unit`
|
132
133
|
|
@@ -14,13 +14,19 @@ class BaseEditingController < RestrictedAreaController
|
|
14
14
|
# Works like documented in https://activerecord-hackery.github.io/ransack/getting-started/sorting/#sorting-in-the-controller
|
15
15
|
class_attribute :default_sorts, default: ["id"]
|
16
16
|
|
17
|
+
##
|
18
|
+
# Configure default distinct results in the index query.
|
19
|
+
# Works like documented in https://activerecord-hackery.github.io/ransack/going-further/other-notes/#problem-with-distinct-selects
|
20
|
+
class_attribute :default_distinct, default: true
|
21
|
+
|
17
22
|
def index
|
18
23
|
authorize base_class
|
19
24
|
|
20
25
|
q = policy_scope(base_scope)
|
21
26
|
@search_instance = search_class.new(q, current_user,
|
22
27
|
params: params.permit(:page, :q => {}), # FIXME trovare modo per essere più "STRONG"
|
23
|
-
sorts: default_sorts
|
28
|
+
sorts: default_sorts,
|
29
|
+
distinct: default_distinct
|
24
30
|
)
|
25
31
|
@search_instance = yield(@search_instance) if block_given?
|
26
32
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
module Utilities::PageHelper
|
2
|
+
include Utilities::IconHelper
|
2
3
|
# @param [BaseModel] base_class
|
3
4
|
def title_mod_g(base_class)
|
4
5
|
"#{t("edit")} #{base_class.model_name.human}"
|
@@ -24,12 +25,15 @@ module Utilities::PageHelper
|
|
24
25
|
# end
|
25
26
|
# end
|
26
27
|
|
27
|
-
# @param [TrueClass, FalseClass] valore
|
28
|
+
# @param [TrueClass, FalseClass, NilClass] valore
|
28
29
|
def boolean_to_icon(valore)
|
29
|
-
|
30
|
+
case valore
|
31
|
+
when true
|
30
32
|
icon("check-lg", class: "text-success")
|
31
|
-
|
33
|
+
when false
|
32
34
|
icon("x-lg", class: "text-danger")
|
35
|
+
else
|
36
|
+
nil
|
33
37
|
end
|
34
38
|
end
|
35
39
|
|
@@ -6,7 +6,13 @@
|
|
6
6
|
%>
|
7
7
|
<%# locals: (object:,field:) -%>
|
8
8
|
<%
|
9
|
-
|
9
|
+
nf = "NOT_FOUND_HTML"
|
10
|
+
help_text = object.class.human_attribute_name("#{field}/help_text_html", default: nf)
|
11
|
+
if help_text != nf
|
12
|
+
help_text = help_text.html_safe
|
13
|
+
else
|
14
|
+
help_text = object.class.human_attribute_name("#{field}/help_text", default: "")
|
15
|
+
end
|
10
16
|
unless help_text.blank?
|
11
17
|
%>
|
12
18
|
<div class="form-text"><%= help_text %></div>
|
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.7.0
|
@@ -7,7 +7,9 @@ module BaseEditingBootstrap
|
|
7
7
|
included do
|
8
8
|
include IsValidated
|
9
9
|
include ActionTranslation
|
10
|
-
delegate :ransackable_attributes,
|
10
|
+
delegate :ransackable_attributes,
|
11
|
+
:ransackable_associations,
|
12
|
+
:ransackable_scopes, to: :@class
|
11
13
|
|
12
14
|
|
13
15
|
##
|
@@ -34,6 +36,14 @@ module BaseEditingBootstrap
|
|
34
36
|
Pundit.policy(User.new, self.new).permitted_associations_for_ransack.map(&:to_s)
|
35
37
|
end
|
36
38
|
end
|
39
|
+
|
40
|
+
def ransackable_scopes(auth_object = nil)
|
41
|
+
if auth_object
|
42
|
+
Pundit.policy(auth_object, self.new).permitted_scopes_for_ransack.map(&:to_s)
|
43
|
+
else
|
44
|
+
Pundit.policy(User.new, self.new).permitted_scopes_for_ransack.map(&:to_s)
|
45
|
+
end
|
46
|
+
end
|
37
47
|
end
|
38
48
|
end
|
39
49
|
end
|
@@ -24,8 +24,8 @@ module BaseEditingBootstrap::Forms
|
|
24
24
|
##
|
25
25
|
# Costruisce l'array delle classi che devono essere presenti sul campo della form
|
26
26
|
#
|
27
|
-
def form_style_class_for(method, options = {})
|
28
|
-
classes =
|
27
|
+
def form_style_class_for(method, options = {}, base_classes: ["form-control"])
|
28
|
+
classes = base_classes
|
29
29
|
classes << "is-invalid" if object.errors && object.errors.include?(method)
|
30
30
|
classes << options[:class].split(" ") if options[:class]
|
31
31
|
classes.flatten.compact.uniq.join(" ")
|
@@ -46,8 +46,8 @@ module BaseEditingBootstrap::Forms
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def select(method, choices = nil, options = {}, html_options = {}, &block)
|
49
|
-
html_options
|
50
|
-
super(method, choices, options,
|
49
|
+
html_options.merge!(class: form_style_class_for(method, html_options, base_classes: ["form-control", "form-select"]))
|
50
|
+
super(method, choices, options,html_options , &block)
|
51
51
|
end
|
52
52
|
|
53
53
|
def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
|
@@ -57,9 +57,10 @@ module BaseEditingBootstrap::Forms
|
|
57
57
|
else
|
58
58
|
label_tag = nil
|
59
59
|
end
|
60
|
-
|
61
|
-
|
62
|
-
|
60
|
+
@template.content_tag(:div, class: form_style_class_for(method, {class: (options.extract!(:class)[:class] || "")}, base_classes: ["form-check"])) do
|
61
|
+
checkbox_class = ["form-check-input"]
|
62
|
+
checkbox_class << "is-invalid" if object.errors && object.errors.include?(method)
|
63
|
+
super(method, options.reverse_merge(class: checkbox_class.join(" ")), checked_value, unchecked_value) + label_tag
|
63
64
|
end
|
64
65
|
end
|
65
66
|
|
@@ -75,7 +76,7 @@ module BaseEditingBootstrap::Forms
|
|
75
76
|
options = {},
|
76
77
|
html_options = {},
|
77
78
|
&block)
|
78
|
-
form_check_classes = (["form-check"] + [(html_options.delete(:form_check_class){""}).split(" ").collect(&:strip)]).compact.join(" ")
|
79
|
+
form_check_classes = (["form-check"] + [(html_options.delete(:form_check_class) { "" }).split(" ").collect(&:strip)]).compact.join(" ")
|
79
80
|
super do |builder|
|
80
81
|
@template.content_tag(:div, class: form_check_classes) do
|
81
82
|
builder.check_box(class: "form-check-input") + builder.label(class: "form-check-label")
|
@@ -7,17 +7,18 @@ module BaseEditingBootstrap::Searches
|
|
7
7
|
include ActiveModel::Naming
|
8
8
|
include ActiveModel::Conversion
|
9
9
|
|
10
|
-
attr_reader :model_klass, :user, :params, :scope, :sorts
|
10
|
+
attr_reader :model_klass, :user, :params, :scope, :sorts, :distinct
|
11
11
|
|
12
12
|
# @param [User] user
|
13
13
|
# @param [ActiveRecord::Associations::CollectionProxy] scope
|
14
14
|
# @param [Array<String (frozen)>] sort
|
15
|
-
def initialize(scope, user, params: {page: nil}, sorts: ["id"])
|
15
|
+
def initialize(scope, user, params: {page: nil}, sorts: ["id"], distinct: true)
|
16
16
|
@model_klass = scope.klass
|
17
17
|
@user = user
|
18
18
|
@scope = scope
|
19
19
|
@params = params
|
20
20
|
@sorts = sorts
|
21
|
+
@distinct = distinct
|
21
22
|
end
|
22
23
|
|
23
24
|
##
|
@@ -26,7 +27,7 @@ module BaseEditingBootstrap::Searches
|
|
26
27
|
def results
|
27
28
|
ransack_query
|
28
29
|
.tap { |r| r.sorts = @sorts if r.sorts.empty? }
|
29
|
-
.result(distinct:
|
30
|
+
.result(distinct: @distinct)
|
30
31
|
.tap { |q| Rails.logger.debug { "[Ransack] params:#{params} - sql: #{q.to_sql}" } }
|
31
32
|
.page(params[:page])
|
32
33
|
end
|
@@ -5,11 +5,14 @@ RSpec.describe <%= class_name %>, type: :model do
|
|
5
5
|
|
6
6
|
# it_behaves_like "a base model",
|
7
7
|
# ransack_permitted_attributes: %w[<%= attributes_names.join(" ") %>],
|
8
|
-
# ransack_permitted_associations: []
|
8
|
+
# ransack_permitted_associations: [],
|
9
|
+
# option_label_method: :to_s,
|
10
|
+
# ransack_permitted_scopes: [] do
|
9
11
|
# let(:auth_object) { :auth_object } <- default
|
10
12
|
# let(:auth_object) { create(:user, :as_admin) } <- in caso di necessità di override
|
11
13
|
# let(:new_user_ransack_permitted_attributes) { ransack_permitted_attributes }
|
12
14
|
# let(:new_user_ransack_permitted_associations) { ransack_permitted_associations }
|
15
|
+
# let(:new_user_ransack_permitted_scopes) { ransack_permitted_scopes }
|
13
16
|
# end
|
14
17
|
|
15
18
|
end
|
@@ -49,7 +49,9 @@ RSpec.shared_examples "base editing controller" do |factory: nil, only: [], exce
|
|
49
49
|
|
50
50
|
##
|
51
51
|
# Possibili override per la costruzione delle path
|
52
|
-
|
52
|
+
|
53
|
+
let(:default_sorts) { BaseEditingController.default_sorts } # configurazione nel controller per ordine di default del modello
|
54
|
+
let(:default_distinct) { BaseEditingController.default_distinct } # configurazione nel controller per distinct nella ricerca di ransack
|
53
55
|
let(:url_for_new) { url_for([model.new, action: :new]) }
|
54
56
|
let(:url_for_index) { url_for(model) }
|
55
57
|
let(:url_for_create) { url_for(model.new) }
|
@@ -89,10 +91,13 @@ RSpec.shared_examples "base editing controller" do |factory: nil, only: [], exce
|
|
89
91
|
params = {q: {"foo_eq": "foo"}}
|
90
92
|
get url_for_index, params: params
|
91
93
|
expect(response).to have_http_status(200)
|
92
|
-
expect(assigns[:search_instance]).to be_an_instance_of(BaseEditingBootstrap::Searches::Base)
|
93
|
-
|
94
|
-
|
95
|
-
|
94
|
+
expect(assigns[:search_instance]).to be_an_instance_of(BaseEditingBootstrap::Searches::Base)
|
95
|
+
.and(have_attributes(
|
96
|
+
user: user,
|
97
|
+
params: ActionController::Parameters.new(params).permit!,
|
98
|
+
sorts: default_sorts,
|
99
|
+
distinct: default_distinct,
|
100
|
+
))
|
96
101
|
end
|
97
102
|
end
|
98
103
|
end
|
@@ -194,7 +199,7 @@ default_unathorized_failure = -> { raise "TODO - passare proc con richiesta che
|
|
194
199
|
RSpec.shared_examples "fail with unauthorized" do |request: default_unathorized_failure|
|
195
200
|
it "is expected to redirect to root" do
|
196
201
|
|
197
|
-
if Gem::Version.create(
|
202
|
+
if Gem::Version.create(Pundit::VERSION) < Gem::Version.create('2.3.2')
|
198
203
|
allow(Pundit).to receive(:authorize).with(user, any_args).and_raise(Pundit::NotAuthorizedError)
|
199
204
|
else
|
200
205
|
allow_any_instance_of(Pundit::Context).to receive(:authorize).and_raise(Pundit::NotAuthorizedError)
|
@@ -1,4 +1,7 @@
|
|
1
|
-
RSpec.shared_examples "a base model" do |ransack_permitted_attributes: [],
|
1
|
+
RSpec.shared_examples "a base model" do |ransack_permitted_attributes: [],
|
2
|
+
ransack_permitted_associations: [],
|
3
|
+
option_label_method: :to_s,
|
4
|
+
ransack_permitted_scopes: []|
|
2
5
|
|
3
6
|
it_behaves_like "a validated? object"
|
4
7
|
|
@@ -10,15 +13,24 @@ RSpec.shared_examples "a base model" do |ransack_permitted_attributes: [], ransa
|
|
10
13
|
instance = described_class.new
|
11
14
|
expect(instance).to respond_to(:option_label)
|
12
15
|
|
13
|
-
expect(instance).to receive(option_label_method).and_call_original
|
16
|
+
expect(instance).to receive(option_label_method).and_call_original, "Expected `#{instance.class}#option_label` chiami il metodo `##{option_label_method}` per la traduzione del label nelle options"
|
14
17
|
instance.option_label
|
15
18
|
end
|
16
19
|
|
20
|
+
if ransack_permitted_scopes.any?
|
21
|
+
it "have scopes" do
|
22
|
+
ransack_permitted_scopes.each do |scope|
|
23
|
+
expect(described_class).to respond_to(scope)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
17
28
|
##
|
18
29
|
# Oggetto solitamente di classe User che identifichi l'utente a cui eseguire il check dei permessi
|
19
30
|
let(:auth_object) { :auth_object }
|
20
31
|
let(:new_user_ransack_permitted_attributes) { ransack_permitted_attributes }
|
21
32
|
let(:new_user_ransack_permitted_associations) { ransack_permitted_associations }
|
33
|
+
let(:new_user_ransack_permitted_scopes) { ransack_permitted_scopes }
|
22
34
|
|
23
35
|
describe "with ransackables" do
|
24
36
|
where(:base_model_method, :result, :new_user_result) do
|
@@ -29,6 +41,9 @@ RSpec.shared_examples "a base model" do |ransack_permitted_attributes: [], ransa
|
|
29
41
|
[
|
30
42
|
:ransackable_associations, ransack_permitted_associations.map(&:to_s), lazy { new_user_ransack_permitted_associations.map(&:to_s) }
|
31
43
|
],
|
44
|
+
[
|
45
|
+
:ransackable_scopes, ransack_permitted_scopes.map(&:to_s), lazy { new_user_ransack_permitted_scopes.map(&:to_s) }
|
46
|
+
],
|
32
47
|
]
|
33
48
|
end
|
34
49
|
|
@@ -25,6 +25,7 @@ RSpec.shared_examples "a standard base model policy" do |factory, check_default_
|
|
25
25
|
[:search_fields],
|
26
26
|
[:permitted_associations_for_ransack],
|
27
27
|
[:permitted_attributes_for_ransack],
|
28
|
+
[:permitted_scopes_for_ransack],
|
28
29
|
[:editable_attributes],
|
29
30
|
[:permitted_attributes],
|
30
31
|
]
|
@@ -37,6 +38,47 @@ RSpec.shared_examples "a standard base model policy" do |factory, check_default_
|
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
41
|
+
# Controllo che se negli elementi dei #sortable_search_result_fields è presente un elemento
|
42
|
+
# che ha come prefisso il nome di una relazione presente in #permitted_associations_for_ransack
|
43
|
+
# allora nella policy della relativa associazione dovrò trovare in #permitted_attributes_for_ransack
|
44
|
+
# il nome della colonna da ordinare.
|
45
|
+
# ES:
|
46
|
+
# relazione: User -> Post
|
47
|
+
# class PostPolicy
|
48
|
+
# def sortable_search_result_fields = %i[user_username]
|
49
|
+
# def permitted_associations_for_ransack = %i[user]
|
50
|
+
# end
|
51
|
+
# class UserPolicy
|
52
|
+
# def permitted_attributes_for_ransack = %[username]
|
53
|
+
# end
|
54
|
+
# ordinamento per #user_username, quindi devo cercare relazione in `user` e attribute `username`
|
55
|
+
it "check associated sortable attributes" do
|
56
|
+
klass = instance.record.class
|
57
|
+
# non facciamo nulla se non è un active record il record da controllare
|
58
|
+
if klass.respond_to?(:reflect_on_association)
|
59
|
+
|
60
|
+
# escludiamo le colonne del modello
|
61
|
+
col_to_check = (instance.sortable_search_result_fields.collect(&:to_s) - instance.record.class.column_names).uniq
|
62
|
+
|
63
|
+
elenco_campi_ordinabili_in_relazione = col_to_check.map do |field|
|
64
|
+
instance.permitted_associations_for_ransack.map do |rel|
|
65
|
+
if field.match(/^#{rel}_(.*)$/)
|
66
|
+
[rel, Regexp.last_match(1)]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end.flatten(1).compact.uniq
|
70
|
+
|
71
|
+
elenco_campi_ordinabili_in_relazione.each do |relation,field|
|
72
|
+
reflection = klass.reflect_on_association(relation.to_s)
|
73
|
+
policy = Pundit.policy(instance.user, reflection.class_name.constantize.new)
|
74
|
+
expect(policy.permitted_attributes_for_ransack.collect(&:to_sym).include?(field.to_sym)).to be_truthy, lambda{
|
75
|
+
"Mi aspetto che `#{policy.class.name}#permitted_attributes_for_ransack` includa `#{field}` per permettere l'ordinamento del campo tramite relazione"
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
40
82
|
describe "standard_methods" do
|
41
83
|
where(:method, :response) do
|
42
84
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: base_editing_bootstrap
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marino Bonetti
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-02-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|