I'm trying to build a small expense tracking app using Rails 4.1. When a user submits the expense request, it's state is marked as pending by default. The admin has to approve the request. I'm using state_machine gem to do this.
I just added comment functionality using acts_as_commentable gem, which works fine on its own. I wanted to combine the approval drop down and the comment box in the same form and used the following code in the expense show page:
<div class="row">
<div class="col-md-8">
<%= form_for [#expense, Comment.new] do |f| %>
<div class="form-group">
<%= f.label :state %><br />
<%= f.collection_select :state, #expense.state_transitions, :event, :human_to_name, :include_blank => #expense.human_state_name, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :comment %><br />
<%= f.text_area :comment, class: "form-control" %>
</div>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
</div>
</div>
<br>
The problem is I get the "NoMethodError in Expenses#show - undefined method `state' for #". Is there a way I can update both the approval status and comment in one go?
The updated show page with nested attributes:
<div class="row">
<div class="col-md-8">
<%= nested_form_for (#expense) do |f| %>
<div class="form-group">
<%= f.label :state %><br />
<%= f.collection_select :state, #expense.state_transitions, :event, :human_to_name, :include_blank => #expense.human_state_name, class: "form-control" %>
</div>
<%= f.fields_for :comments do |comment| %>
<div class="form-group">
<%= comment.label :comment%>
<%= comment.text_area :comment, class: "form-control" %>
</div>
<% end %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
</div>
</div>
Nested attributes are your friend. They are so easy to implement I'd recommend avoiding acts_as_commentable to remove an unnecessary dependency and so you understand what's going on.
Use them like this:
# expense.rb
class Expense < ActiveRecord::Base
has_many :comments # << if polymorphic add `, as: :commentable`
accepts_nested_attributes_for :comments
end
# expenses_controller.rb
def new
#expense = Expense.new
#expense.comments.build
end
# expenses/new.html.haml
= form_for(#expense) do |f|
- # expense inputs...
= f.nested_fields_for(:comments) do |comment|
= comment.text_area(:body)
= f.submit
There are many options, so check the docs for more details (they're quite good).
Related
I have 2 models:
class Page < ApplicationRecord
enum page_type: STATIC_PAGE_TYPES, _suffix: true
has_one :seo_setting
accepts_nested_attributes_for :seo_setting, update_only: true
validates :title, :subtitle, length: { maximum: 50 }
validates :page_type, uniqueness: true
def to_param
"#{id}-#{page_type}".parameterize
end
end
and
class SeoSetting < ApplicationRecord
mount_uploader :og_image, SeoSettingsOgImageUploader
belongs_to :page
validates :seo_title, :seo_description, :og_title, :og_description, :og_image, presence: true
end
My Page objects are created from the seeds.rb file, and when I want to edit them, I get an error: Failed to save the new associated seo_setting.
In the form I have this:
<div class="card-body">
<%= form_for([:admin, #page]) do |f| %>
<%= render 'shared/admin/error-messages', object: #page %>
<div class="form-group">
<%= f.label :title, t('admin.shared.title') %>
<%= f.text_field :title, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :subtitle, t('admin.shared.subtitle') %>
<%= f.text_field :subtitle, class: 'form-control' %>
</div>
<h3>SEO Settings</h3>
<%= f.fields_for :seo_setting, f.object.seo_setting ||= f.object.build_seo_setting do |form| %>
<div class="form-group">
<%= form.label :seo_title, t('admin.shared.seo_title') %>
<%= form.text_field :seo_title, class: 'form-control' %>
</div>
<div class="form-group">
<%= form.label :seo_description, t('admin.shared.seo_description') %>
<%= form.text_area :seo_description, class: 'form-control' %>
</div>
<div class="form-group">
<%= form.label :og_title, t('admin.shared.og_title') %>
<%= form.text_field :og_title, class: 'form-control' %>
</div>
<div class="form-group">
<%= form.label :og_description, t('admin.shared.og_description') %>
<%= form.text_area :og_description, class: 'form-control' %>
</div>
<div class="form-group">
<%= form.label :og_image, t('admin.shared.og_image') %>
<div class="row">
<div class="col-lg-12">
<%= image_tag(form.object.og_image.url, style: 'width: 100px') if form.object.og_image? %>
</div>
</div>
<%= form.file_field :og_image %>
<%= form.hidden_field :og_image_cache %>
</div>
<% end %>
<div class="form-group">
<%= f.submit t('admin.actions.submit'), class: 'btn btn-success' %>
<%= link_to t('admin.actions.cancel'), admin_page_path(#page) , class: 'btn btn-default' %>
</div>
<% end %>
</div>
If I remove validations from my SeoSetting model, everything is working. It seems Rails doesn't like this part: f.object.build_seo_setting, because it creates a record in my database. Any ideas of how can I solve this issue? Thanks ahead.
Just had to change this line:
<%= f.fields_for :seo_setting, f.object.seo_setting ||= f.object.build_seo_setting do |form| %>
for this one:
<%= f.fields_for :seo_setting, #page.seo_setting.nil? ? #page.build_seo_setting : #page.seo_setting do |form| %>
Looks as if the problem lies here:
accepts_nested_attributes_for :seo_setting, update_only: true
in that you only allow the updating of the seo_setting on update.
Then, when you use this code:
f.object.seo_setting ||= f.object.build_seo_setting
You're falling back to a new seo_setting if the associated object is missing.
For this to work, you'll need to either remove the update_only: true, or only render the fields for the association if the seo_setting already exists.
Accepted answer, a conditional inside fields_for line, by OP, works also with polymorphic association and nested form (fields_for). Thnx Alex.
My profile role is created and when the user logs in the profile controller's edit action view will be displayed. I want to show the role that is assigned to user in the edit action and the user cannot change the role. My edit.html.erb file is given as:
<div class="row">
<div class="col-md-6 offset-md-3">
<h3>Profile</h3>
<%= form_for(#profile) do |f| %>
<div class="form-group">
<%= f.label :first_name %><br />
<%= f.email_field :first_name, autofocus: true, class: "form-control"%>
</div>
<div class="form-group">
<%= f.label :last_name %><br />
<%= f.password_field :last_name, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :role %><br />
<%= f.select(:role, [['User', 'user'], ['Vip', 'vip'], ['Admin', 'admin']]) %>
</div>
<div class="actions form-group">
<%= f.submit "Submit", class: 'btn btn-primary' %>
</div>
<% end %>
</div>
The simplest possible way is just to use String#humanize from ActiveSupport.
Capitalizes the first word, turns underscores into spaces, and (by
default)strips a trailing '_id' if present. Like titleize, this is
meant for creating pretty output.
irb(main):008:0> roles.roles.keys.map(&:humanize)
=> ["User", "Vip", "Admin"]
irb(main):009:0> Profile.new(role: :admin).role.humanize
=> "Admin"
Profile.roles gives us the hash mapping for the Enum.
You can use this to generate the select tag with:
<%= form.select :role, Profile.roles.keys.map{|k| [k.humanize, k] } %>
You can get "vip".humanize to return "VIP" by setting up an inflection:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'VIP'
end
This may require restarting your Rails server before it kicks in.
Using the I18n module
But if you want a more flexible solution that lets you set the mappings yourself (and works with translations) use the I18n module:
# config/locales/en.yml
en:
activerecord:
attributes:
profile:
roles:
user: 'User'
vip: 'Very Important Person'
admin: 'Admin'
# app/helpers/users_helper.rb
module UsersHelper
def translate_role(role)
I18n.t("activerecord.attributes.user.roles.#{ role }", default: role.humanize)
end
def role_options
Profile.roles.keys.map{|k| [translate_role(k), k] }
end
end
You would then display the users role by:
<%= translate_role(#user.role) %>
And you can setup the form input as:
<%= form.select :role, role_options %>
You can use a helper method, For example in the application_helper.rb you can make a method like this:
class ApplicationHelper
def profile_role(profile)
# Perform your logic
# if you are using as_enum gem for example
Profile.roles.select{|symbol_role,integer_rep| integer_rep == profile.role}.keys.first.to_s
# of course there could be much better way to get the string.
end
end
Then in your view, If user should not be able to change it, there is no need to make it as drop-down list:
<div class="row">
<div class="col-md-6 offset-md-3">
<h3>Profile</h3>
<%= form_for(#profile) do |f| %>
<div class="form-group">
<%= f.label :first_name %><br />
<%= f.email_field :first_name, autofocus: true, class: "form-control"%>
</div>
<div class="form-group">
<%= f.label :last_name %><br />
<%= f.password_field :last_name, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :role %><br />
<%= text_field_tag 'role', profile_role(#profile), disabled: true %>
</div>
<div class="actions form-group">
<%= f.submit "Submit", class: 'btn btn-primary' %>
</div>
<% end %>
You can just show it as regular text, e.g.:
...
<div class="form-group">
<%= f.label :last_name %><br />
<%= f.password_field :last_name, class: "form-control" %>
</div>
<p>
Role:<br />
<%= #profile.role %>
</div>
<div class="actions form-group">
<%= f.submit "Submit", class: 'btn btn-primary' %>
</div>
...
I have a problem in my form, i want to create an entry in my "members" table, each member are connected to a "year" but I keep getting mismatches on the selection of the year.
This is my form:
<div class="field">
<%= f.label :name %><br>
<%= f.text_area :name %>
</div>
<div class="field">
<%= f.label :nickname %><br>
<%= f.text_area :nickname %>
</div>
<div class="field">
<%= f.label :year %>
<%= f.select :year, options_for_select(#years.all.map{|y| [y.year,y.id]}) %>
</div>
<div class="actions">
<%= f.submit %>
</div>
And here are the models:
class Member < ApplicationRecord
belongs_to :year
mount_uploader :image, ImageUploader
end
class Year < ApplicationRecord
has_many :members, dependent: :destroy
end
When I try to submit I get the following error:
Year(#70050157849460) expected, got "1" which is an instance of String(#9412380)
Where did I go wrong?
EDIT:
Here is the code for the controller
class MembersController < ApplicationController
def home
#members = Member.all
end
def new
#member = Member.new
#years = Year.all
end
def create
#member = Member.new(member_params)
if #member.save
flash[:success] = "Member Created"
redirect_to root_path
else
render 'form'
end
end
private
def member_params
params.require(:member).permit(:name,:nick,:position,:image,:year)
end
end
<div class="field">
<%= f.label :name %><br>
<%= f.text_area :name %>
</div>
<div class="field">
<%= f.label :nickname %><br>
<%= f.text_area :nickname %>
</div>
<div class="field">
<%= f.label :year %>
<%= f.select :year_id, options_for_select(#years.all.map{|y| [y.year,y.id]}) %>
</div>
<div class="actions">
<%= f.submit %>
</div>
or
<div class="field">
<%= f.label :name %><br>
<%= f.text_area :name %>
</div>
<div class="field">
<%= f.label :nickname %><br>
<%= f.text_area :nickname %>
</div>
<div class="field">
<%= f.label :year %>
<%= f.select :year, options_for_select(#years.all.map{|y| [y.year,y]}) %>
</div>
<div class="actions">
<%= f.submit %>
</div>
This second I did not check but first will work you need to say year_id because when you did map on collection you set id as parameter that is being passed
but on form select you basically told form to expect active record object.
I have a Rails 4 app running SQLite locally and PostgreSQL on Heroku.
I have a products class, which has a boolean column "isebook" that is set to either true or false in a form on products/new.html.erb.
<%= form_for(#product, :html => {:multipart => true}) do |f| %>
<% if #product.errors.any? %>
<div class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
<h3><%= pluralize(#product.errors.count, "error") %> prohibited this product from being saved:</h3>
<ul>
<% #product.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="form-group">
<%= f.label :name %><br>
<%= f.text_field :name, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :link %><br>
<%= f.text_field :link, class: "form-control" %>
<p style="font-size:12px; margin-top:2px;"><i>(Begin link with http:// or https://)</i></p>
</div>
<div class="form-group">
<%= f.label :description %><br>
<%= f.text_area :description, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label "Product is an Ebook?" %> <br />
<%= f.check_box :isebook %> <br />
</div>
<div class="form-group">
<%= f.label :image, 'Product Image' %><br>
<%= f.file_field :image, class: "form-control" %>
</div>
<div class="actions">
<%= f.submit "Create Product", class: "btn btn-primary" %>
</div>
<% end %>
In the controller, this field is being required:
def product_params
params.require(:product).permit(:name, :description, :isebook, :image)
end
The entire code for the controller method:
def new
# Only make new product if user has remaining products
#product = Product.new
if current_user.prodused < current_user.prodlimit
respond_with(#product)
else
redirect_to user_path(current_user.id), error: "You've reached your product limit. Upgrade your account to add more products."
end
end
This is the product model:
class Product < ActiveRecord::Base
validates :name, :description, presence:true
validates :link, :format => URI::regexp(%w(http https))
belongs_to :user
has_attached_file :image, :styles => {:medium => "300x300", :thumb => "200x200"}, :default_url => "/images/:style/missing.jpg"
end
This works correctly locally, when a new product is created the isebook field is assigned true or false based on if the checkbox is selected or not.
I'm now attempting to test on Heroku, I cleared the entire production database and ran all the migration files to clear all existing products. I then created an account successfully, when attempting to render products/new.html.erb I get the error ActionView::Template::Error: undefined method `isebook' for nil:NilClas
I cleared the entire production database and ran all the migration files to clear all existing products. I then created an account successfully
Are there any products?
ActionView::Template::Error: undefined method `isebook' for nil:NilClas
This error means you've called the method isebook on nil, likely meaning you tried to load one or all products, but in your case they don't exist, so calling the method on nil (nothing) throws an error.
I have two models: PointPage and PointPageClass, which belongs_to :point_page. When user create PointPage rails creates several (now there're 6 of them) PointPageClasses in database. I need to edit all of them on PointPage edit page. So, I'm using nested attributes in controller and fields_for in view.
point_page_form
<%= form_for #point_page, role: 'form' do |f| %>
<div class="form-group">
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
</div>
... # some other PointPage fields
<% #point_page.point_page_classes.each do |class| %>
<%= f.fields_for :point_page_classes do |builder| %>
<div class="row">
<div class="col-lg-6">
<%= builder.text_field :distance_price, class: 'form-control' %>
</div>
<div class="col-lg-6">
<%= builder.text_field :show_price, class: 'form-control' %>
</div>
</div>
<% end %>
<% end %>
<%= f.submit 'Save', class: 'btn btn-default' %>
<% end %>
point_page_controller
def update
#point_page = PointPage.find(params[:id])
if #point_page.update_attributes(point_page_params)
respond_to do |format|
format.html { redirect_to new_point_path }
format.js
end
end
end
private
def point_page_params
params.require(:point_page).permit(:name, :url, :article,
point_page_classes_attributes: [:distance_price, :show_price])
end
So, I've added in point_page_controller PointPageClass attributes, but when I save the changes, it only saves changes for PointPage, but not for PointPageClass (distance_price and show_price).
Thanks for any help!
One of the problem could be not permitting the :id in the point_page_params.For the update to take place you need to permit the :id.
Try changing your point_page_params to like this
def point_page_params
params.require(:point_page).permit(:id,:name, :url, :article,point_page_classes_attributes: [:id,:distance_price, :show_price])
end