To teach myself Rails, im building an extremely simple Voting app.
There are 2 models, Question and Option. Question has_many Options and Option belongs_to Question.
Using the standard scaffolding, I have reached a stage where you can add a question, view it, and add options to it and see these options.
What I would like to do now is add the code that increases an option.count value by one when clicking on a link. I have a vote_up method in the Option model:
class Option < ActiveRecord::Base
validates :text, :presence => :true
belongs_to :question
def vote_up
self.count += 1
end
end
My Options controller looks like:
class OptionsController < ApplicationController
def create
#question = Question.find(params[:question_id])
#option = #question.options.create(params[:option])
redirect_to question_path(#question)
end
end
My Question model looks like:
class Question < ActiveRecord::Base
validates :text, :presence => {:message => 'A question normally has text...'}
has_many :options, :dependent => :destroy
def vote
# Maybe the vote code will go here???
end
end
And my Question controller has the usual new, create, edit, destroy methods that the scaffold creates. V little customisation here.
My show.html.erb view where I would like to put the link to the vote method looks like:
<p id="notice"><%= notice %></p>
<p>
<b>Question <%= #question.guid %></b>:
<%= #question.text %>
</p>
<% if #question.options.count == 0 %>
<p>Shame! there are currently no options to vote on. Add some! </p>
<% elsif #question.options.count == 1 %>
<p>One option in a vote is a dictatorship... Maybe add some more?</p>
<% end %>
<h2>Options:</h2>
<% #question.options.each do |option| %>
<p>
<%= option.text %>: ** Link to vote here!
</p>
<% end %>
<h2>Add an option to vote on</h2>
<%= form_for([#question, #question.options.build]) do |f| %>
<div class="field">
<%= f.label :text %><br />
<%= f.text_field :text %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
<% if #question.options.count == 0 # Only show edit if no options saved. %>
<%= link_to 'Edit', edit_question_path(#question) %> |
<% end %>
<%= link_to 'Back', questions_path %>
So what I am trying to do is add a "vote" link next to each option that calls the vote_up method in the options model. This is probably laughably easy, but i've hit a wall and would really appreciate any help.
Also, any suggestions on how to do this better would be welcome!
Thanks
Simon
I think #oded-harth has showed a right way, but I have two remarks:
First of all, Rails is a beautiful language and is written to simplify our developer lives ;) When programming in Rails you must never forget that. With this in mind, I want to point you to "increment()" method. So you can simply up-vote without unnecessary += 1. To down-vote use decrement(). I believe you can use it like this: option.increment(:count)
Second, I think it's a little dirty to have a whole form for a simple vote action. You can use something like this
<%= link_to "Vote Up", :url => { :action => :vote_up, :option_id => option.id }, :method => :put %>
To make it work you'll have to set your route something like this:
resources :votes
put :vote_up
end
What I would do is make the vote_up method in the controller:
def vote_up
option = Option.find(params[:option_id])
option.count += 1
redirect (to where do you want...)
end
And in the view I would call that method this way:
<%= form_for( option, :url => { :action => "vote_up", :option_id => option.id} ) do |f| %>
<%= f.submit("vote up") %>
<% end %>
Related
I am using a link from an index page that has a group of nested records (row) that I need to update all at once. The link goes to an edit action that I need to make update the attributes of nested records (prisms).
I tried using the simple_form gem methods for nested models. It gives me a field for all of the objects, when I only want one field to enter a value to them all. The builder from that looks usable, but I don't know how to use it to update the fields. Either way, the form isn't right.
I have tried every variation of form_for and fields_for I could find on Google to develop the edit form. It looks like I'm the only one on Earth trying to solve this problem.
This is how I have my routes set up:
resources :gardens, shallow: true do
resources :prisms
resources :rows
Here is how my garden model is now:
class Garden < ApplicationRecord
mount_uploader :picture, ImageUploader
belongs_to :user
has_one :photo
has_many :rows, :dependent => :destroy
has_many :prisms
geocoded_by :address
after_validation :geocode
after_commit :populate_garden!
def id
self[:id]
end
def populate_garden!
# row 0
(0..length-1).each do |i|
Row.create(row_num: i, garden_id: id)
end
end
end
The garden model creates my rows through the populate_garden! method.
Here is the row model:
class Row < ApplicationRecord
belongs_to :garden
has_many :prisms, :dependent => :destroy
accepts_nested_attributes_for :prisms
after_commit :populate_rows
def id
self[:id]
end
def populate_rows
# row 0
(0..garden.width-1).each do |i|
Prism.create(:row_id => self.id, :row_num => self.row_num, :col_num => i, :garden_id => self.garden_id)
end
end
end
The row model creates prisms in the populate_rows method.
Here is the prism model:
class Prism < ApplicationRecord
belongs_to :row
belongs_to :garden
include RankedModel
ranks :column_order
end
Here is the table from my index.html.erb that I click to open the edit action.
<table>
<% #rows.each_with_index do |gardenrow, index| %>
<% #rows.select { | row | row.row_num == index}.each do |row| %>
<td class="prism-cols">
<%= link_to 'Edit Row', edit_row_path(row), class:"dark-link" %>
<br /><i class="fas fa-arrow-down"></i>
</td>
<% end %>
<% end %>
</table>
The row passes nicely into the edit action, and I currently have this incorrect form:
<h1>The row brought into this form page is: </h1>
<%= #row.inspect %>
<div class="container">
<%= simple_form_for #row do |m| %>
<%= m.simple_fields_for :prisms do |p| %>
<%= p.input :crop_name %>
<% end %>
<%= m.button :submit %>
<% end %>
</div>
The rows_controller update method looks like this:
def update
#row = Row.find(params[:row_id])
#row.prisms.build
redirect_to root_path
end
I need one form field for crop_name that will change all of the prisms in the selected row with a single submit. I don't have any problems updating one prism at a time through an edit action on the prism. The difficulty I'm having is working through the nesting of prisms inside of a specific row.
With the help of my mentor below I was able to develop a form that works with the controller to make this work. Here is the updated code for later use with this type of problem.
Here is the form data:
<%= form_tag({controller: "rows", action: "update"}, method: "patch") %>
<%= label_tag(:crop_name, "Crop Name") %>
<%= text_field_tag(:crop_name) %>
<%= hidden_field_tag(:row_id, #row.id) %>
<%= submit_tag("submit") %>
Here is the controller update method:
def update
#row = Row.find(params[:id])
#garden = Garden.find_by_id(:garden_id)
#row.prisms.each do |p|
p.crop_name = params[:crop_name]
p.save!
end
redirect_to :controller => 'gardens', :action => 'show', id: #row.garden_id
end
Thanks for the help. I don't think I could have figured this out from the documentation alone.
If I'm understanding correctly, I think simple_form may be limiting you. A basic ruby form may do what you want. I'm not 100% sure what the best way is to do a simple_form on nested fields but this stackoverflow answer may be able to help more.
Using a basic ruby form
You want a form that has one field. When submitted, it will take the value from the submitted form and update that field for all prisms of that row. I would recommend digging more into the basics of ruby forms for this kind of scenario and then do something like this.
// html.erb
<%= form_tag({controller: "rows", action: "update_prism_crop_name"}, method: "post", class: "nifty_form") %>
<%= label_tag(:crop_name, "Crop name") %>
<%= text_field_tag(:crop_name) %>
<%= hidden_field_tag(:row_id, #row.id) %>
<%= submit_tag("Submit") %>
<% end %>
// rows_controller
def update_prism_crop_name
#row = Row.find(params[:row_id])
#row.prisms.each do |prism|
prism.crop_name = params[:crop_name]
prism.save!
end
# other redirect stuff
end
The form_tag explicitly calls out an action but I have to imagine that you'll need to build a route for this custom action as well.
I haven't tested any of this and I'm a bit rusty in rails but I believe something like this would work.
What's the best (simplest) way to walk through MVC and check if everything is set up right?
I get a bit frazzled and I feel like there must be a really simple fix to error messages like these:
undefined method `invitations_path' for #<#<Class:0x00000105ad5cb8>:0x00000105820b30>
After adding small amounts of code to my app things break and I want to trouble shoot them myself.
Thanks for the tips!
EDIT
Perhaps troubleshooting the specific issue will lead way to a generalized approach,
Link_to is not linking Used <%= %> instead of <% %>.
The above error is generated when visting localhost:3000/invitation/new
view (in home/index.erb.html)
<% if #user.invitation_limit > 0 %>
<% link_to 'Send Invitations', new_invitation_path %>
(<%= #user.invitation_limit %> left)
<% end %>
view (in invitation/new.erb.html)
<%= error_messages_for :invitation %>
<% form_for #invitation do |f| %>
<p>
<%= f.label :recipient_email, "Friend's email address" %><br />
<%= f.text_field :recipient_email %>
</p>
<p><%= f.submit "Invite!" %></p>
<% end %>
controller
class InvitationController < ApplicationController
def new
#invitation = Invitation.new
end
def create
#invitation = Invitation.new(params[:invitation])
#invitation.sender = current_user
if #invitation.save
if logged_in?
Mailer.deliver_invitation(#invitation, signup_url(#invitation.token))
flash[:notice] = "Thank you, invitation sent."
redirect_to projects_url
else
flash[:notice] = "Thank you, we will notify when we are ready."
redirect_to root_url
end
else
render :action => 'new'
end
end
end
model
class Invitation < ActiveRecord::Base
belongs_to :sender, :class_name => 'User'
has_one :recipient, :class_name => 'User'
attr_accessible :recipient_email, :sender_id, :sent_at, :token
end
routes.rb
resources :home, :only => :index
resources :invitation
You can create request specs for each of your controller actions. Request specs follow the request all the way from the controller to rendering the view, and if there is an error it will show up in the request spec.
This may take time to set up, but will save you lots of time in the future, as you don't have to manually test every page when you want to roll out a new version of your website.
I have the following model:
class Contact
attr_accessor :name, :emails, :message
def initialize(attrs = {})
attrs.each do |k, v|
self.send "#{k}=", v
end
end
def persisted?
false
end
end
I am calling to a contact form in my view like so:
<div class="email_form">
<%= render 'form' %>
</div>
Here is the controller:
class ShareController < ApplicationController
layout "marketing_2013"
respond_to :html, :js
def index
#contact = Contact.new
end
end
Here is the Form:
<%= form_for(#contact) do |f| %>
<%= f.label :name, "Your Name" %>
<%= f.text_field :name %>
<%= f.label :text, "Send to (separate emails with a comma)" %>
<%= f.text_field :emails %>
<%= f.label :message, "Email Text" %>
<%= f.text_area :message %>
<%= f.submit %>
<% end %>
For some reason I keep getting this error:
undefined method model_name for Contact:Class
Any reason why what I have currently wouldn't work?
Besides the correct route in your config/routes.rb, you will also need these two instructions on your model:
include ActiveModel::Conversion
extend ActiveModel::Naming
Take a look at this question: form_for without ActiveRecord, form action not updating.
For the route part of these answer, you could add this to your config/routes.rb:
resources :contacts, only: 'create'
This will generate de following route:
contacts POST /contacts(.:format) contacts#create
Then you can use this action (contacts#create) to handle the form submission.
add include ActiveModel::Model to your Contact file
your route probably doesn't go where you think it's going and therefore #contact is probably nill
run "rake routes" and check the new path.. if you are using defaults, the route is
new_contact_path.. and the erb should be in file: app/views/contacts/new.html.erb
def new
#contact = Contact.new
end
I have a Sezzion model:
attr_accessible :description
has_many :session_instructors, :dependent => :destroy
has_many :instructors, :through => :session_instructors
accepts_nested_attributes_for :session_instructors
accepts_nested_attributes_for :instructors
Instructor model:
attr_accessible :bio
has_many :sezzions, :through => :session_instructors
has_many :session_instructors, :dependent => :destroy
SessionInstructor model:
attr_accessible :instructor_id, :sezzion_id
belongs_to :sezzion
belongs_to :instructor
Lastly, User model:
has_many :sezzions
has_many :instructors
I'm trying to create a form for Sezzion with nested form for SessionInstructor which has multiple select option for Instructors.
How can I do the following:
nested form for SessionInstructor
use multiple select option to get all the selected Instructor's instructor_id
hidden field to pass in the created/updated session_id with each select instructor
I have the following code as of now:
<%= form_for(#sezzion) do |f| %>
<%= f.label :description %>
<%= f.text_area :description %>
<%= f.label :instructors %>
<%= fields_for :session_instructors do |f| %>
<select multiple>
<% current_user.instructors.each do |instructor| %>
<option><%= instructor.name %></option>
<% end %>
</select>
<% end %>
<%= f.submit %>
<% end %>
Thank you so much!
This is something that seems ridiculously hard in Rails.
I think something like this might work:
<%= f.fields_for :session_instructors do |si| %>
<%= si.collection_select :instructor_ids, current_user.instructors, :id, :name, multiple: true>
<% end %>
This should create a form element which will set sezzion[session_instructors_attributes][instructor_ids].
Although I'm not sure if that's actually what you want. I've never tried this using a multi select. If it doesn't work, you could also try getting rid of the fields_for and just using f.collection_select. If you're willing to use a checkbox, I can show you how to do that for sure.
I hope that helps.
Edit:
Here's how I would usually do it with a check_box:
<%= f.fields_for :session_instructors do |si| %>
<%= si.hidden_field "instructor_ids[]" %>
<% current_user.instructors.each do |i| %>
<%= si.check_box "instructor_ids[]", i.id, i.sezzions.include?(#sezzion), id: "instructor_ids_#{i.id}" %>
<%= label_tag "instructor_ids_#{i.id}", i.name %>
<% end %>
<% end%>
There are a couple "gotchas!" with this method. When editing a model, if you deselect all checkboxes then it won't send the parameter at all. That's why the hidden_field is necessary. Also, you need to make sure each form element has a unique id field. Otherwise only the last entry is sent. That's why I manually set the value myself.
I copy pasted and then edited. Hopefully I got the syntax close enough where you can get it to work.
FINAL EDIT:
Per Sayanee's comment below, the answer was a bit simpler than I thought:
<%= f.collection_select :instructor_ids, current_user.instructors, :id, :name, {}, {:multiple => true} %>
#Sayanee, can you post how your instructors, sezzions table look like. Also for note, you can not get instructor_ids from Instructor object, hence you are getting "undefined method" error. With the current association that you shared, you can get instructor_ids from a Sezzion object. So you need to loop through current_user.sezzions in stead of current_user.instructors.
This is a way to implement fields_for nested form with html multiple_select in case of a has_many :through association. Solved it by doing something like this. The form view:
<%= form_for(#sezzion) do |f| %>
...
<%= fields_for :session_instructors do |g| %>
<%= g.label :instructor, "Instructees List (Ctrl+Click to select multiple)" %>:
<%= g.select(:instructor_id, Instructor.all.collect { |m| [m.name, m.id] }, {}, { :multiple => true, :size => 5 }) %>
<%= g.hidden_field :your_chosen_variable_id, value: your_chosen.id %>
<% end %>
...
<%= f.submit %>
<% end %>
Note:Since the #sezzion would not be saved at the time of generating the form you cannot pass that id (#sezzion.id) in place of your_chosen.id through the form. You could handle that save in the controller.
Make sure that your controller Initializes the Variables while generating the form: Your def new could look something like this:
def new
#sezzion = Sezzion.new
#sezzion.session_instructor.build
#sezzion.instructors.build
end
Now the create controller has to be able to accept the strong params required for the multiple select, so the sezzion_params method may look something like this:
def sezzion_params
params.require(:sezzion).permit(:description, :any_other_fields,
:session_instructors_attributes =>
[:instructor_id => [], :your_chosen_id => Integer])
end
In the create function, the first session_instructor variable is linked to the #sezzion instance variable through our new function. The other session_instructors in our multiple select must be built after the Sezzion instance is saved, if you want to pass in the created #sezzion.id with each select instructor. .
def create
#sezzion = Sezzion.new(sezzion_params)
#startcount=1 #The first element of the array passed back from the form with multiple select is blank
#sezzion.session_instructors.each do |m|
m.instructor_id = sezzion_params["session_instructors_attributes"]["0"]["instructor_id"][#startcount]
m.your_chosen_variable_id = your_chosen.id
#startcount +=1
end
if #sezzion.save
sezzion_params["session_instructors_attributes"]["0"]["instructor_id"].drop(#startcount).each do |m|
#sezzion.session_instructors.build(:instructor_id => sezzion_params["session_instructors_attributes"]["0"]["instructor_id"][#startcount],
:your_chosen_variable_id => your_chosen.id).save
#startcount += 1
end
flash[:success] = "Sezzion created!"
redirect_to root_url
else
flash[:danger] = "There were errors in your submission"
redirect_to new_sezzion_path
end
end
I have an rails 3 application where there are multiple registrations (diagnosis, patient, laboratory test, service, client, user, supplier). Initially these will be populated by seeding the database. The requirement is for the description codes to be mixed case (capitalised first word) when either
1. specified by the application (some configuration setting - yet to be determined)
2. specified by data entry user
At present I have a model, view & controller for Diagnosis which contains two fields:
1. code (always to be capitalised)
2. description (First word capitalised based on check_box_tag value)
Presently I am using a before_save callback in the model to implement the conversion, but I cannot get it to only work when the check_box_tag is not selected i.e. its ignoring the check_box_tag.
I have tried changing the check_box_tag to a check_box adding an attr_assessor to the model (but not the sqlite3 db as it is not required to be stored).
This didn't work either.
How do I accomplish this? How do I override the option to use a checkbox from an internal application configuration file which results in either the checkbox being 'unavailable' or not visible if the application configuration specifies not user selectable?
Model (diagnosis.rb)
require 'DescriptionHelper'
class Diagnosis < ActiveRecord::Base
attr_accessible :code, description
string_correct_case = DescriptionHelper.new([:code, :description])
validates :code, :presence => true, :length => { :minimum => 4, :maximum => 4 }
validates :description, :presence => true
before_save string_correct_case
end
Callback in DescriptionHelper.rb
class DescriptionHelper
def initialize(attribute)
#attrs_to_manage = attribute
end
def before_save(record)
#attrs_to_manage.each do |attribute|
record.send("#{attribute}=", capitaliseWords(record.send("#{attribute}")))
end
end
private
def capitaliseWords(value)
value = value.mb_chars.downcase.to_s.gsub(/\b\w/) { |first| first.upcase }
end
end
Controller (diagnoses_controller.rb)
class DiagnosesController < ApplicationController
def new
#diagnosis = Diagnosis.new
end
def create
#diagnosis = Diagnosis.new(params[:diagnosis])
if #diagnosis.save
flash[:notice] = "Diagnosis created with params [#{#diagnosis.attributes.inspect}" #for debugging, once fixed will be just 'Diagnosis created.'
redirect_to #diagnosis
else
flash[:alert] = "Diagnosis not created."
render :action => "new"
end
end
.. other controller actions - edit, show, destroy
end
View (_form.html.erb)
<%= form_for(#daignosis) do |f| %>
<div class="field">
<%= f.label :code %>
<%= f.text_field :code %>
</div>
<div class="field">
<%= f.label :description %>
<%= f.text_field :description %>
</div>
<div class="field">
<%= check_box_tag("diagnosis_desc_dont_convert", 1, false) %><%= f.label "Leave as entered" %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
When this runs currently the check_box_tag is ignored.
When adding in the model an attar_assessor :description_correctcase and changing the view to use f.check_box 'description_correctcase' this is still ignored.
How does one get this to work?
Thanks in advance from a rails aspiring developer.
Finally got a solution to the problem, after reading and re-reading various SO solutions to component parts of my question. I'm not sure its correct in terms of rails, but it works.
If you can offer me a better solution I would certainly learn from this.
Here is my solution.
Model (diagnosis.rb)
require 'DescriptionHelper'
class Diagnosis < ActiveRecord::Base
attr_accessor :do_not_correctcase
attr_accessible :code, :description, :do_not_correctcase
before_save DescriptionHelper.new([:code, :description]), :if =>
lambda { |d| d.do_not_correctcase.to_s == '0' }
validates :code, :presence => true, :length => { :minimum => 4, :maximum => 4 }
validates :description, :presence => true
end
This I referenced from the following SO solution - https://stackoverflow.com/a/6388691/1108010
Controller (diagnoses_controller.rb)
class DiagnosesController < ApplicationController
def new
#diagnosis = Diagnosis.new
end
def create
#diagnosis = Diagnosis.new(params[:diagnosis])
#diagnosis.do_not_correctcase = params[:diagnosis][:do_not_correctcase]
logger.debug "New diagnoses: #{#diagnosis.attributes.inspect}"
logger.debug "Diagnosis should be valid: #{#diagnosis.valid?}"
logger.debug "code has value #{params[:code]}"
if #diagnosis.save
flash[:notice] = "Diagnosis created with params [#{#diagnosis.attributes.inspect}" #for debugging
redirect_to #diagnosis
else
flash[:alert] = "Diagnosis not created."
render :action => "new"
end
end
.. other controller actions - edit, show, destroy
end
I also changed the view to replace the check_box_tag with a check_box.
View (_form.html.erb)
<%= form_for(#daignosis) do |f| %>
<div class="field">
<%= f.label :code %>
<%= f.text_field :code %>
</div>
<div class="field">
<%= f.label :description %>
<%= f.text_field :description %>
</div>
<div class="field">
<%= f.check_box 'do_not_correctcase' %><%= f.label "Leave as entered" %><br />
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
So despite getting this to work I'm not clear on are the following:
When inspecting the attributes with "#{#diagnosis.attributes.inspect}".
I assume that the reason the attr_accessor variable is not included in the New diagnosis output is that it is not part of the database table and therefore Active Reocrd does not instanciate it as part of the new record with #diagnosis.new
Could someone be kind enough to confirm that.
Why does the log have no value for logger.debug "code has value #{params[:code]}"? What causes the params[:code] to be null in the logger output?
Logfile contained the following entry:
Started POST "/diagnoses" for 127.0.0.1 at 2012-03-05 09:36:38 +0000
Processing by DiagnosesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"RW/mzkhavGeaIW0hVLn0ortTnbCDlrX+FfzH4neLLsA=", "diagnosis"=>{"code"=>"tt02", "description"=>"description for tt02", "do_not_correctcase"=>"1"}, "commit"=>"Create Diagnosis"}
New diagnosis: {"code"=>"tt02", "created_at"=>nil, "description"=>"description for tt02", "updated_at"=>nil}
Diagnosis should be valid: true
code has value
I would dearly like to know what is the correct way to do all this, as I feel this is not very DRY or clean.