Rails: Accept optional nested Image; allow to add later if nil - ruby-on-rails

I have a simple Article model associated with an Image model. The relationship is currently one-to-one (might change later but is fine for now).
Everything works fine when I create an Article from scratch and add an Image to it. Here's the catch: I would like the Image to be optional at creation, but also I would retain the option to add an Image at a later stage.
However, I am not sure how to handle that through the edit action. I have tried this:
def edit
#article = Article.find(params[:id])
if #article.image.nil?
#article.image = Image.new
end
render 'articles/edit'
end
... which results in:
ActiveRecord::RecordNotSaved in Admin::ArticlesController#edit
Failed to save the new associated image.
The form currently looks like this:
<%= f.fields_for :image do |image| %>
<div class="form-group">
<%= image.label :image, "Article image" %><br/>
<%= image_tag(#article.image.path.thumb.url) %>
<%= image.file_field :path %>
</div>
<div class="form-group">
<%= image.label :caption %>
<%= image.text_field :caption, class: 'form-control' %>
</div>
<div class="form-group">
<%= image.label :credits %>
<%= image.text_field :credits, class: 'form-control' %>
</div>
<% end %>
How can I accomplish optional nested images that can be added through the edit form later on?
Thanks!

The simplicity of Rails never ceases to amaze me. Here's how I solved it by adding one line (#image = ...):
def edit
#article = Article.find(params[:id])
#image = #article.image || #article.build_image
render 'articles/edit'
end
... inspired by Can't update my nested model form for has_one association

Related

Ruby on Rails multiple images connected to one object

I've been trying to create a form that would get parameters for multiple models. I have a photo model that belongs to a product model and I want to make it so that when you create a new product you can also upload images that are linked to that product by id.
<%= form_for #product, html:{multipart:true} do |f| %>
<div class="field">
<%= f.label :price %>
<%= f.text_field :price %>
</div>
<%=form_for #photo do |t| %>
<%t.productID = f.id%>
<div class="field">
<%= t.label (:image) %>
<%= t.file_field (:image) %>
</div>
<%end%>
<div class="actions">
<%= f.submit %>
</div>
<%end%>
right now I'm using paperclip for image attachments and the photo model accepts the images as parameters. I've used paperclip before but the product could only have one image connected to it. If I use the form above I get "First argument in form cannot contain nil or be empty" error and it points to where the form_for #photo starts.I have controllers for both with the usual methods of new, create, update, etc. I've routed resources to both product and photos but I'm still pretty new to rails and don't fully understand how this stuff works.
I think what you're trying to do is a good application for nested forms using the fields_for helper.
First, you'll need to ensure that your product model and photo model have the right associations (A product probably has_many photos, and a photo belongs to a product, right?). Then you'll make sure the product class 'accepts nested attributes for photo's which allows you to add attributes to the photos model from a products form.
in products.rb
class Product
has_many :photos
accepts_nested_attributes_for :photos
end
and in photo.rb
class Photo
belongs_to :product
end
Then you'll want to make sure any attributes you need for the photo are white-listed in your product params.
in products_controller.rb
private
def product_params
params.require(product).permit(:first_product_attribute, :second_produtc_attribute, photo_attributes: [:image])
end
Last, you'll create the form using the special helper fields_for
in your view
<%= form_for #product, html:{multipart:true} do |f| %>
<div class="field">
<%= f.label :price %>
<%= f.text_field :price %>
</div>
<%= f.fields_for :photo do |t| %>
<div>
<%= t.label :image %>
<%= t.file_field :image, :multiple => true %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<%end%>
You'll also need to make sure you're actually creating new photo objects in your product's create action:
in products_controller.rb
def create
#product = Product.new(product_params)
if #product.save!
params[:photo]['image'].each do |img|
#photo = #product.photos.create!(:image => img)
end
flash[:success] = 'product saved!'
redirect_to #product
end
end
Some of this is based on my experience doing the same thing but with Carrierwave instead of Paperclip so your specific implementation might be a little different.
I dont think this is a proper method <%t.productID = f.id%>. Maybe try <% t.text_field :productID, value = f.id, type = hidden %> or something along those lines?
heres some docs for the form helper so you know what to put after t.abcd
http://apidock.com/rails/v3.2.3/ActionView/Helpers/FormHelper/form_for
You're getting the
"First argument in form cannot contain nil or be empty"
..error because #photo is nil, you need to set it in your controller #photo = Photo.new.
Also, form tags inside form tags are invalid HTML.
https://www.w3.org/TR/html5/forms.html#the-form-element
Forms
Content model: Flow content, but with no form element
descendants.
You want to use f.fields_for instead. Learn how to use it here here
I have controllers for both with the usual methods of new, create,
update, etc.
You only ever hit one controller and action when you go to a path, say /photos will only hit the photos controller (as configured in your routes.rb). This I think is where you're messing up the #photo variable. Set both in the same controller in order for the view to be able to see both variables.

Reject creation of nested attribute if checkbox checked

I have a form in my rails app that accepts nested attributes. However, what I want to do is for rails to reject the creation of the nested model if a checkbox (outside the model itself) is checked.
Any idea on how to pass an attribute to the :reject_if option of the accepts_nested_attributes_for in the model from the controller?
Thank you very much in advance.
EDIT:
My controller looks like this:
def new
#course = Course.new
#course.course_template = CourseTemplate.new
end
def create
#course = Course.new(course_params)
#course.user = current_user
if #course.save
flash[:success] = t(".new_course_created_succefully")
redirect_to courses_path
else
render 'new'
end
end
And the form:
<%= form_for #course do |f| %>
<%= render 'shared/error_messages', error_model: #course %>
<div class="form-group has-feedback mb">
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
</div>
<div class="form-group has-feedback mb">
<div class="checkbox c-checkbox needsclick">
<label class="needsclick">
<%= check_box_tag "template", "1", false, {class: "needsclick"} %>
<span class="fa fa-check"></span>Is Template?
</label>
</div>
</div>
<%= f.fields_for :course_template do |ff| %>
<div class="form-group has-feedback mb">
<%= ff.label :name %>
<%= ff.text_field :name %>
</div>
<% end %>
<% end %>
send that checkbox as a parameter from the form and put the build operation inside an if statement. No need to bother with the reject_if
You need to handle your create and build operations separately. so instead of passing your model all attributes, youll pass the model the model attributes, and the association, the nested attributes
# controller
course = Course.new(course_params.reject{|attrib| attrib == :course_template_attributes})
unless params[:skip_create]
course.course_templates.build(course_params[:course_template_attributes]
end
...
what you need to do is conditionally create the course_templates, so you can just pass Course.new all your course_params because that creates both the course and the templates, which needs to be done separately.
Note I'm shorthanding with that reject statement up there. you can either manually add in the various params or better yet create another method with strong params and whitelist only the model attributes (not including the course_template_attributes)
additionally. the params[:skip_create] is whatever the parameter is for that checkbox that decides whether or not you want to create the templates

Rails - Multiple entries for a single attribute

I am making an app in Rails 4. I use Simple Form.
I have a profile model and a qualifications model.
The associations are:
profile.rb
belongs_to :profile
qualifications.rb
has_many :qualifications
I have a form in my profile views, which includes a part of a form from my qualifications view.
profiles#form
<%= simple_form_for(#profile) do |f| %>
<%= f.error_notification %>
<div class="form-inputs">
<div class="row">
<div class="intpol2">
Your professional qualifications
</div>
<%= render 'qualifications/form', f: f %>
</div>
Qualifications#form
<%= simple_fields_for :qualification do |f| %>
<div class="form-inputs">
<div class="row">
<div class="col-md-6">
<%= f.input :title, :label => "Your award" %>
</div>
<div class="col-md-6">
</div>
</div>
<div class="row">
<div class="col-md-6">
<%= f.input :level, collection: [ "Bachelor's degree", "Master's degree", "Ph.D", "Post Doctoral award"] %>
</div>
<div class="col-md-6">
<%= f.input :year_earned, :label => "When did you graduate?", collection: (Date.today.year - 50)..(Date.today.year) %>
</div>
</div>
Users may have more than one degree. I want to add a field that is a button which says 'add another qualification' and then a new set of the qualification form fields is available.
I found this post which tries to do something slightly different. I don't want 10 blank sets of the form field (it will make the form look too long).
Creating multiple records for a model in a single view in Rails
Is there another way to achieve this?
You'll be looking for a gem called cocoon; you can also watch this Railscast (Nested forms) which is woefully outdated but still explains the structure very well.
The pattern is very simple, but requires some extra parts:
Have an ajax button which calls the controller
The controller needs to return a form and built fields_for
You'll use JS to append the new fields_for to the original form
The biggest problem is the id of your new fields_for - new implementations of this pattern use child_index: Time.now.to_i
I've written about this here.
Here's a new version:
Ajax
Firstly, you need an "Add Qualification" button, which links to your controller through ajax:
#app/views/profiles/_form.html.erb
<%= simple_form_for(#profile) do |f| %>
<%= f.error_notification %>
<div class="form-inputs">
<div class="row">
<div class="intpol2">Your professional qualifications</div>
<%= render 'qualifications/form', f: f %>
</div>
</div>
<%= button_to "+", new_profile_path, method: :get %>
<% end %>
Controller
This will go through the new controller method, which we should be able to manage to return the specific response for the ajax request:
#app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
respond_to :js, :html, only: :new
def new
#profile = Profile.new
#profile.qualifications.build
respond_with #profile #-> will invoke app/views/profiles/new.js.erb
end
end
Response
Once your new.js.erb has fired, we need to build the new HTML form, extract the fields_for and append it to your view:
#app/views/profiles/new.js.erb
var fields_for = $("<%=j render "profiles/form" %>").html(); //-> might need tweaking
$(fields_for).appendTo("#new_profile.form-inputs");
child_index
You should also change your qualifications/form to include the child_index:
#app/views/qualifications/form.html.erb
<%= simple_fields_for :qualifications, child_index: Time.now.to_i do ...
Child index is meant to denote the index of the fields_for elements. In our case (since we're making all new records), it doesn't matter. Using Time.now.to_i ensures a totally unique id each time.
Finally, you need to make sure you're calling:
<%= simple_fields_for :qualifications ... %>
... plural
It seems like that you have to use nested form. You have to try your link tutorial because I will use this too. For another tutorial you can use this as reference nested_forms-rails-4.2.
I hope this help you.

Rails 4 : Checkbox array is not updating my attribute

My starting place was this discussion: Syntax for form_for when building an array from checkboxes
I have a call to my model passing back an array of valid options. This array then makes a series of check_box_tag
<%= form_for #game, :url => wizard_path do |f| %>
<div>
<% #game.select_races.each do |a| %>
<%= f.label a %>
<%= check_box_tag 'game[races][]', a , true %>
<% end %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
This successfully creates an array called 'races' containing the desired output. The problem is that it doesn't actually update the races attribute. So my races attribute is still nil.
I'm sure this is a painful Rails beginner question. Any help is appreciated.
UPDATE
My allowed params were:
def game_params
params.require(:game).permit(:shattered_empire, :shards_of_the_throne, :number_of_players, :rules, :strategy_cards, :players, :races)
end
Which needed to be updated to:
def game_params
params.require(:game).permit(:shattered_empire, :shards_of_the_throne, :number_of_players, :rules, :strategy_cards, :players, {:races => []})
end

Rails form file upload newbie issue

I have an issue retrieving my file upload information. I am just starting to learn Rails.
I am using ruby 2.0.0p0
And Rails 4.0.0.beta1
Here is my form:
<%= form_for(#person, :html => { :multipart => true }) do |f| %>
<div class="field">
<%= f.label :photo %><br />
<%= f.file_field :photo %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
And in my person.rb model:
def photo=(file_data)
logger.debug("PHOTO")
logger.debug(file_data)
logger.debug("END OUTPUT PHOTO")
unless file_data.blank?
#file_data = file_data
self.extension = file_data.original_filename.split('.').last.downcase
end
end
I can see in my console that nothing happens (no "PHOTO" output), the photo method is never called.
Why is that?
When I looked in the console I also saw this line that got me worried:
Unpermitted parameters: photo
What does that mean?
In your controller, where you're dealing with params, you need to use .permit to list the attributes the form is allowed to post:
#person = Person.new(params.require(:person).permit(:photo))
Alternatively, if you used Rails' scaffolding generator, you might instead have a person_params method where you would need to add the :photo attribute:
def person_params
params.require(:person).permit(:photo, :name, etc...)
end

Resources