Rails 7 help DRYing out image attachments? - ruby-on-rails

Bear with me, I am new to posting and Rails so sorry if I mess up phrasing!!
I am working on a Rails app with many similar models. Each view has a _form.html.haml partial that differs in content but contains similar components, such as buttons to submit, delete, etc. Every model has_many_attached photos, and in the form you can add or delete photos. The haml for deleting photos looks like this, where variable is replaced with whatever view _form.html.haml is in:
.gallery#form
- #VARIABLE.photo.each_with_index do |image, index|
.overlay-container
.img-square{ :style => "background-image: url(#{rails_blob_url(photo(#VARIABLE, index))})", :alt => "Photo of #{image}" }
= link_to("Delete", delete_image_attachment_VARIABLE_url(image), method: :delete, class: 'button delete overlay')
To make the delete work on each photo, this code is in each controller:
def delete_image_attachment
#photo = ActiveStorage::Attachment.find(params[:id])
#photo.purge
redirect_back fallback_location: #VARIABLE
flash[:success] = 'Photo was successfully deleted.'
end
And routes.rb has this chunk of code for each model:
resources :VARIABLE do
member do
delete :delete_image_attachment
end
end
However, I have about a dozen models I need to do this on. My goal is to bring the gallery into a new partial, since it will be used in every _form regardless of the other content. However, the delete function (though the same for every controller) is tied to the controller/routes.rb of each model.
There must be some way of DRYing this functionality into a couple files instead of copy-pasting for each model, but my Google searches have not turned up anything. So, any guidance or better Rails convention is greatly appreciated!

If I'm understanding the structure properly, it sounds like you would want to do something like the following:
Create a top-level controller that handles deleting images
This could be used by any page.
This would require the following query parameters:
id the photo's ID to delete by.
redirect where to redirect the user after the action is completed.
Routes
Now there will only be 1 route to handle the controller above.
Create a reusable partial
For rendering a collection of images with a redirect url that allows you to set the following state for the partial:
photos a collection of images to render.
redirect_url this is passed as a query parameter to the centralized delete image controller.
Thoughts & Mocked Examples
This should be able to DRY up your implementation. Currently the only thing I see that couples the view with the deletion is the redirect URL. By abstracting that out and moving that essentially to a parameter for the partial will allow for re-use and flexibility.
You've already identified your coupling via #VARIABLE, here's a quick mock of how I would expect it to end up looking like:
Partial Template
.gallery#form
- #photos.each_with_index do |image, index|
.overlay-container
.img-square{ :style => "background-image: url(#{rails_blob_url(photo(#VARIABLE, index))})", :alt => "Photo of #{image}" }
= link_to("Delete", delete_image_attachment_url(image, redirect: #redirect_url), method: :delete, class: 'button delete overlay')
Ths would require: photos, and redirect_url
So make sure to set #photos and #redirect_url on the consuming controller.
Example with instance properties to access in the template
#photos = GET_PHOTOS_HERE
#redirect_url = 'some-redirect-path'
render partial: 'photos_partial'
Example with locals parameter for the template
photos = GET_PHOTOS_HERE
render partial: 'photos_partial', locals: { photos: photos, redirect: 'some-redirect-path' }`
Note: You may need to change how you access the local variables in the template.
https://guides.rubyonrails.org/layouts_and_rendering.html#passing-local-variables
Controller
def delete_image_attachment
#photo = ActiveStorage::Attachment.find(params[:id])
#photo.purge
redirect_back fallback_location: params[:redirect]
flash[:success] = 'Photo was successfully deleted.'
end
Routes
Here you would only have the single route for deleting any image attachment, and point at the single controller above.
delete 'resources/image_attachment/:id', to: 'resources#delete_image_attachment'
Note: Replace "resources" with whatever your controller name is, or the scoping/naming you would like.
PS: It's been a while since I've done Rails so I'm not completely certain on the accuracy or your environment.

Related

Rails link_to new with parameters not saving parameters

So, I have two objects, Floor, which has_many FloorMonsters, and FloorMonster which belongs_to Floor.
I have a link_to new_floor_monster_path in the show page on Floor, and I intend to pass the Floor id as a parameter. The link_to currently looks like this:
= link_to "Add a Monster to this Floor", new_floor_monster_path(floor_id: #floor.id)
In the controller for FloorMonster, the new method is this:
def new
#floor_monster = FloorMonster.new(floor_id: params[:floor_id])
respond_to do |format|
format.html # new.html.erb
format.json { render json: #floor_monster }
end
end
When I click the link, the URL shows the parameters:
http://.../floor_monsters/new?floor_id=4
But when I save, the floor_id is nil and the program crashes. What am I missing here? I've tracked down sources that do exactly what I did, but I am getting no succes. The accepted answer here and this blog post are doing exactly what I am doing as far as I can tell, but they managed to get it to work.
I assume that you are exposing the new FloorMonster in a form to allow setting other attributes and then saving it. In order to pass the floor_id attribute along to the create action, you will need to add a field to your form for it. If you don't want it to be seen or to be editable, use a hidden_field. In your form, add:
= f.hidden_field :floor_id

Render view from application_controller

So, I have search form, and search is avaliable obviously from any page.
I thought that it makes sense, that such action from application controller is placed in layouts/views folder.
But I just don't get- Rails doesn't see it. So I can't do this? How then should I provide action, avaliable from any page?
Code:
def tests_search
#tests=Test.test_search(params[:query])
respond_to do |format|
format.html
end
end
Route:
search_tests GET /search_tests(.:format) application#tests_search
Form:
<%= form_tag search_tests_path, {:id=>'test_search',:method => :get} do%>
Error:
Unknown action
The action 'tests_search' could not be found for ApplicationControllerr
You should create a new search controller. Use rails g controller search index which will create a search controller with a index action (you could also call the action something like result). Then add a search/_form.html.erb file in your search view folder, with the form:
<%= form_tag search_path, {:id=>'test_search',:method => :get} do |f| %>
and render this in your layout/application.html.erb where you want it to be:
<%= render "search/form" %>
This way you have a search form on any pages, that uses the SearchController to handle the search requests.
I would recommend using other controller to do this. It can be for example SearchController even if there will be only one method.
Notice that ApplicationController is controller that every other controller in application inherits from by default. So if it wouldn't be the case, it could make sense, but now every controller will inherit your test_search action, which is not desired.
If your search form will be a partial, then there is no difference whether this is in ApplicationController or in any other controller. You just have to point to right route.
Initially you have to explain yourself the flow. What you need is some partial which is rendered on all pages, and if a user adds some input to it and submits, he gets some output. Right? Good. So you start by creating a new partial somewhere in
app/views/shared/_search.html.erb
Then, you create your route in routes.rb to point to a controller's action. You don't have to place this in application_controller. Instead, create your search_controller.rb and create some action which responds to the form submission there.
Whenever you want to render your search form on other pages, you simply call render partial (more on that here) with something like
<%= render "shared/search" %>
This is good if you created the file above. Make sure your action exists and the name is correct, in your case it should be:
def test_search
...
end
Good luck.

Rails Forms for custom actions

I'm trying to link the input of a form to a specific action in my rails app.
Currently if I go to www.myapp.com/check/:idNumber, I'll be able to trigger the action just fine (which means routes is setup properly?). This action is basically a function call to a ruby/rails script with the parameter "idNumber" being passed to it. If the function is successful, it would return a newly created "Person" object and nil otherwise. This is different than the standard new operation as it determines the the attributes based on some information that it obtained from a database somewhere else.
Rake routes does give me the following:
check /check/:idNumber(.:format) person#check {:id=>/\d+/}
What I'm having trouble implementing is the form itself.
<%= form_tag("/check", :method => "get") do %>
<%= text_field_tag(:idNumber) %>
<% end %>
Controller action:
def check
regCheck = RegCheck.new
#person = regCheck.check_id(params[:idNumber])
if #person.name == nil
redirect_to root_path
end
end
submitting the form above would bring me to myapp.com/check?utf8=✓&idNumber=1234 instead. Can someone tell me what am I doing wrong?
I believe that using the check_path helper that is generated from the routes file is your best bet.
The form should look like this then.
<%= form_tag(check_path) do %>
<%= text_field_tag(:idNumber) %>
<% end %>
Rails forms can be finicky, especially when trying to build really customized forms.
This line
= form_for [#object]
Determines where the form goes, as well as the object that is being implemented. If you want to route the form to a different place, you can user the :url option. This options determines the path of the form, however you must keep in mind that the method is determined by the #object. If it is a new object, the method will be POST, an existing object will use a PUT method.
Let's suppose you want to update an existing object, but you want to send in data for a new object belonging to the existing object. That would look like
= form_for [#object], :as => #child_object, :url => my_optional_custom_path do |f|
# etc...
This generates a form sending a PUT request to the custom url (or the update path for #object if no custom url is supplied. The PUT request is sent with the parameter params[:child_object].
Hopefully this helps!
Best,
-Brian
I don't think it's possible the way you're trying.. The URL for the form is created before the user inputs any data.. So you need to remove the :idNumber from your routing..
If you do you get the following route:
check /check(.:format) person#check
Because the regex is removed now, you need to do this in you're controller:
def check
# Make sure ID is digits only
idNumber = params[:idNumber].gsub(/[^\d]/, '')
regCheck = RegCheck.new
#person = regCheck.check_id(idNumber)
if #person.name == nil
redirect_to root_path
end
end
You're form is allright, but you may want to use check_path like TheBinaryhood suggests..
If you really want it to be check/:idNumber you may also be able to submit the form to another action and redirect it to the right path from there..

link_to custom action but wrong method?

all, I'm trying to get a custom action to work with a put method: in the
in _post.html.erb i have a link_to statement:
<%= link_to 'End now', post, :method => :put, :action => endnow %>
routes.rb contains:
resources :posts do
member do
put :endnow
end
and posts_controller.rb looks like:
class PostsController < ApplicationController
helper_method :endnow
[.. code for create, edit, destroy, etc ..]
def endnow
puts params
end
end
rake routes's relevant line looks like:
endnow_post PUT /posts/:id/endnow(.:format) posts#endnow
However, the action endnow helper doesn't run when clicking on this link.
Strangely, it does run with an index action (which i can tell from the puts command.
Of course, eventually the code for endnow will update #post, but for now, it just doesn't run properly.
Maybe i'm going about this the wrong way - all I'm trying to achieve is to update #post upon clicking the link to that post, and before showing it.
Any ideas / Alternatives?
Why not use the route helper method provided to you? Change your link to
<%= link_to 'End now', endnow_post_path(#post), method: :put %>
Things you're doing wrong:
If you want to specify the :action, use the Symbol for the action (you're missing a colon). :action => endnow should be action: :endnow
I will assume you have a #post instance variable you're passing from your controller to your action. You should be using that instead of post (unless you do in fact have a local post variable you're omitting from your code)
You are using endnow as an action; you should remove the helper_method :endnow line in your controller because it's not something you want to/should be accessing from your view.
This can all be avoided by using the route helper (for endnow_post you'd append _path to get the local route path: endnow_post_path), and pass in your #post as an argument.
Because you're trying to do a PUT request, you must make sure you have something like jquery-ujs included in your asset pipeline to convert these links to form submissions behind the scenes; browsers don't support PUT via the click of a link on their own.
As for why you're getting the template error when you get your link_to working, Rails is telling you that you need to create a app/views/posts/endnow.html.erb file. Your action has only puts params which does not terminate execution, leaving Rails to assume you still are trying to render some endnow.html.erb template.
Are there other ways to do what you're trying to do (change a single attribute of a specific model)? Sure. Are there better ways? That's pretty subjective; it may not be the most RESTful way, but it's arguably easier to deal with (if for example there are very specific authorization rules to check before updating the attribute you are modifying in endnow. Does the way you've started fleshing out work? Absolutely.
Finally, as a bump in the right direction, after you fix your link_to and remove the the helper_method as I have described above, your endnow action might look like this:
def endnow
post = Post.find!(params[:id])
post.some_attribute_here = some_new_value_here
post.save
redirect_to :root and return # <- this line sets a redirect back to your homepage and terminates execution, telling rails to do the redirect and **not** to render some endnow.html.erb file
end

Rails Rendering Action in Different Controller

So I have a controller called Music with one action which is index. On the music/index.html page I list out a bunch of Songs (via a Song model) and at the bottom I have a for to create a new Song.
Song has validations which I have tested and work just fine. When the new Song saves in controller Songs action create I redirect_to => 'music_index_path' so the person can see the new song in the list. However when the Song does not save (does not pass validations) then I cannot use redirect_to since the form error_messages do not carry over. I need to use render but cannot say render :controller => 'music', :action => 'index.
My goal is to be able to show the error messages for the Song form ON the music/index.html page.
How should I go about this. I am open to other ideas (like changing up controllers) as well.
Thanks!
It sounds to me like Music should be a part of Song, or the other way around. You can always use routes to disguise it as one or the other to the user.
song/index sounds to me like it should display all songs, which is all music does anyway.
My first thought is that we need to rethink this process. Assuming you're using RESTful controllers, it's unclear to me why you would need a Music controller and a Song controller... how are those resources different? The next relevant question would be, why is it not sufficient to show errors via Song#create ? I mean, they couldn't get it right when it was just a form, is the distraction of additional content likely to help? :)
With that said, here is a possible solution. (Given that you didn't paste your code, I'm making a lot of assumptions here.)
<hack>
first, extract the form parts from songs/new to songs/_form, then from the music/index view, render :partial => songs/_form, and in the songs controller, render :action => '../music/index' (this is called a hackity-hack.) Because it's a hack, you will almost certainly need to go into music#index and add #song = Song.new
</hack>
If you start running on edge, the ability to pass a flash through a redirect was just added...but that doesn't really get you there.
The simplest solution though, is that you need to render index, but set up all of the variables that are needed for that page. If you factor that out into a separate module or method, you can call it from both the index action and the save failure.
def index
setup_for_index
end
def create
#song = Song.new(params[:song])
#song.save
#...
#on failure
setup_for_index
render :controller => "music", :action => "index"
end
def setup_for_index
#songs = Song.all
#etc
end
The other thing you could do is use form_remote_for and have the song form just update the div on failure. Then use an RJS template return type reload the whole song list on success.
While I want to reiterate what others have stated about your resource architecture deserving a second look, you can certainly render views for other resources using the :template option:
render template: 'music/index'
Why not a simple if request.post? conditional in the index action's view rather than redirecting to another page?
You can also try using flash[:song_save_error] to pass the error conditions back to your Music controller.
http://api.rubyonrails.org/classes/ActionController/Flash.html
You could try render :file => ...

Resources