New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add context to Page #2457
base: main
Are you sure you want to change the base?
Add context to Page #2457
Conversation
Is it better to create Administrate::Page::Context because the contents of the context are too free and it's concerning? |
cab44ec
to
793187e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies, it's been 4 months 😓 I'm starting to look into this now. I had a first quick look today, but couldn't quite get everything yet. I thought perhaps I can start asking questions and that way I'll force myself to be on top of this, if you are still available?
Thank you for the PR, by the way!
@@ -62,8 +72,9 @@ def update | |||
notice: translate_with_resource("update.success"), | |||
) | |||
else | |||
page = Administrate::Page::Form.new(dashboard, requested_resource) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this one missing the context assignment?
page = Administrate::Page::Form.new(dashboard, requested_resource) | |
page = Administrate::Page::Form.new(dashboard, requested_resource) | |
page.context = self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you're right. Sorry.
authorize_scope(scoped_resource).find(param) | ||
end | ||
|
||
def authorize_scope(scope) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this introduction of authorize_scope
is off-topic in this PR? I can see the value of separating this concern, but it's different from the goal of adding context to the pages. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you're right, this change is off-topic. I appreciate your accurate understanding of my intention.
However, I thought this change was also necessary for effective use of the contextualize_resource
method, so I included it in the same PR. If necessary, I can split the PR.
# | ||
# @param resource [ActiveRecord::Base] A resource to be contextualized. | ||
# @return nothing | ||
def contextualize_resource(resource); end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that I understand this method. It's intended to make changes on the resource, but the context seems better suited to live in the Page
object (which has access to the resource anyway). I'm not sure I would want to alter the model in this way.
Would you be able to explain this a bit more? 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your review!! I have been waiting for your response for a long time.
I would like to contextualize the Page
object, but I also intend to contextualize the resource in the Controller. By contextualizing the resource, we can use it to branch validations or trigger callbacks.
I believe that contextualize_resource
should be separate from the Page
's context, as it serves a similar function to scoped_resources
and authorize_resource
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I think I see now what is going on 👍
When contextualizing, we can hide fields and then put default values on them. In your example you do this to use the customer
field from non-admins, then set its value to the current user on new/create.
I'm not convinced about the following:
- Doing this as part of
authorize_resource
, which is a separate concern. - Doing this only when
resource.is_a?(ActiveRecord::Base)
, which seems artificial. OK, it's not like we have currently support for anything other than ActiveRecord, but this explicit check seems strange to me. - Having this new API instead of the existing
new_resource
andrequested_resource
instead. It's two separate steps, but it uses existing, known APIs. I have tried locally and it appears to work (I'm having issues running specs so I can't be 100% sure).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your review.
I have separated contextize_resource
from authorize_resource
. As a result, the resource no longer passes as a model class from the index, which also resolves the ActiveRecord::Base
issue.
I believe the current form of requested_resource
is ideal. What do you think? I would like to introduce the same form for new
and create
. Do you have any good suggestions on how to do this?
For example, what do you think about this form?
def built_resource(params: {})
@built_resource = new_resource(params).tap do |resource|
authorize_resource(resource)
contextualize_resource(resource)
end
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still not sure, since the same effect can be achieved by overriding new_resource
and requested_resource
as mentioned. I have created an experimental branch with your PR that shows an example of this: main...pablobm:use-cases-for-contextualize I'll experiment a bit to see what the possibilities are.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shifting slightly to a different topic, I feel that the current action methods in the Administrate's ApplicationController are quite fat, which I believe limits the customizability for us, the application developers.
Therefore, it would be beneficial if these methods were broken down further, allowing us to override specific methods during application development.
I believe it would be ideal if we could proceed with method division and achieve a more unified form as shown below.
(It might be beneficial to also divide page = Administrate::Page::Form.new
and the part where context is injected into Page
into separate methods.)
def new
page = Administrate::Page::Form.new(dashboard, built_resource)
render ...
def edit
page = Administrate::Page::Form.new(dashboard, requested_resource)
render ...
def create
if built_resource(resource_params).save
...
def update
if requested_resource.update(resource_params)
...
def built_resource(params: {})
@built_resource ||= new_resource(params).tap do |resource|
authorize_resource(resource)
contextualize_resource(resource)
end
end
def requested_resource
@requested_resource ||= find_resource(params[:id]).tap do |resource|
authorize_resource(resource)
contextualize_resource(resource)
end
end
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the other two commits are good ideas. Thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I prepared built_resource
in addition to new_resource
is because I thought it might affect the following process that is used as a helper_method from the view. Do you foresee any issues with this?
https://github.com/search?q=repo%3Athoughtbot%2Fadministrate+new_resource+language%3AHTML%2BERB&type=code&l=HTML%2BERB
resources = apply_collection_includes(resources) | ||
resources = order.apply(resources) | ||
resources = paginate_resources(resources) | ||
page = Administrate::Page::Collection.new(dashboard, order: order) | ||
page.context = self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following on my comment about contextualize_resource
, I wonder if this is what it should be doing instead? Acting on the page instead of the resource here:
page.context = self | |
contextualize_page(page) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the Controller object is almost essential as a Context for the Page, so how about passing it to the initializer? Like the following:
page = Administrate::Page::Collection.new(dashboard, context: self, order: order)
or
page = Administrate::Page::Collection.new(dashboard, controller: self, order: order)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially that makes sense, but then the developers wouldn't be able to alter the context easily with a hook like contextualize_page
. Having said that, perhaps there isn't a use case for that...? I have no idea yet. I'm experimenting now to see the possibilities of your PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had another look 🙂 While we are doing this, would you be able to rebase this in top of master, please? It will help me run the specs, as the way webdrivers are loaded has changed since the PR was created.
post( | ||
:create, | ||
params: { | ||
order: attributes_for(:order, customer: nil, customer_id: user.id), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not do this with the customer object? I feel it would be more natural. I had to read this a couple of times and check the factory to see what was going on:
order: attributes_for(:order, customer: nil, customer_id: user.id), | |
order: attributes_for(:order, customer: user), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a request parameter, I was unable to format it as customer: user
.
However, this spec had many mistakes overall, so I have made corrections.
order: attributes_for(:order, customer: nil, customer_id: user.id), | ||
}, | ||
) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would inline the method into the example, as it's only used once.
# | ||
# @param resource [ActiveRecord::Base] A resource to be contextualized. | ||
# @return nothing | ||
def contextualize_resource(resource); end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I think I see now what is going on 👍
When contextualizing, we can hide fields and then put default values on them. In your example you do this to use the customer
field from non-admins, then set its value to the current user on new/create.
I'm not convinced about the following:
- Doing this as part of
authorize_resource
, which is a separate concern. - Doing this only when
resource.is_a?(ActiveRecord::Base)
, which seems artificial. OK, it's not like we have currently support for anything other than ActiveRecord, but this explicit check seems strange to me. - Having this new API instead of the existing
new_resource
andrequested_resource
instead. It's two separate steps, but it uses existing, known APIs. I have tried locally and it appears to work (I'm having issues running specs so I can't be 100% sure).
8d152bc
to
4772467
Compare
4772467
to
83e593d
Compare
As mentioned in comments above, I have created a branch that uses your code and I'm experimenting with it. It's at main...pablobm:use-cases-for-contextualize. My goals are:
|
I have some proposal regarding Issue #2363.
I believe this PR can still be improved, so if anyone has any opinions, please review and let me know.
Administrate::Page#context
By keeping the context in the Page as follows,
We can now define it as follows in the Dashboard.
With this, we can now differentiate the BelongsTo options based on the current user's permissions.
Also, by using context in form_attributes like this, we can now differentiate form elements based on the current user's permissions.
Administrate::ApplicationController#contextualize_resource
I've prepared a hook point to add context to the resource.
This allows us to automatically set the current_user(pundit_user) to the customer form element that was omitted in the previous section.
Also, we can change the context here for models where the content of the validation changes under certain conditions.
How do you think about?