Rails 3 - Displaying submit errors on polymorphic comment model - ruby-on-rails-3

Fairly new to Rails 3 and have been Googling every which way to no avail to solve the following problem, with most tutorials stopping short of handling errors.
I have created a Rails 3 project with multiple content types/models, such as Articles, Blogs, etc. Each content type has comments, all stored in a single Comments table as a nested resource and with polymorphic associations. There is only one action for comments, the 'create' action, because there is no need for the show, etc as it belongs to the parent content type and should simply redisplay that page on submit.
Now I have most of this working and comments submit and post just fine, but the last remaining issue is displaying errors when the user doesn't fill out a required field. If the fields aren't filled out, it should return to the parent page and display validation errors like Rails typically does with an MVC.
The create action of my Comments controller looks like this, and this is what I first tried...
def create
#commentable = find_commentable
#comment = #commentable.comments.build(params[:comment])
respond_to do |format|
if #comment.save
format.html { redirect_to(#commentable, :notice => 'Comment was successfully created.') }
else
format.html { redirect_to #commentable }
format.xml { render :xml => #commentable.errors, :status => :unprocessable_entity }
end
end
end
When you fill nothing out and submit the comments form, the page does redirect back to it's appropriate parent, but no flash or nothing is displayed. Now I figured out why, from what I understand, the flash won't persist on a redirect_to, only on a render. Now here's where the trouble lies.
There is only the 'create' action in the comment controller, so I needed to point the render towards 'blogs/show' (NOTE: I know this isn't polymorphic, but once I get this working I'll worry about that then). I tried this in the "else" block of the above code...
else
format.html { render 'blogs/show' }
format.xml { render :xml => #commentable.errors, :status => :unprocessable_entity }
end
Anyway, when I try to submit an invalid comment on a blog, I get an error message saying "Showing [...]/app/views/blogs/show.html.erb where line #1 raised: undefined method `title' for nil:NilClass."
Looking at the URL, I think I know why...instead of directing to /blogs/the-title-of-my-article (I'm using friendly_id), it's going to /blogs/the-title-of-my-article/comments. I figure that extra "comments" is throwing the query off and returning it nil.
So how can I get the page to render without throwing that extra 'comments' on there? Or is there a better way to go about this issue?
Not sure if it matters or helps, but the route.rb for comments / blogs looks like this...
resources :blogs, :only => [:show] do
resources :comments, :only => [:create]
end

I've been plugging away at this over the last few weeks and I think I've finally pulled it off, errors/proper direction on render, filled out fields remain filled in and all. I did consider AJAX, however I would prefer to do it with graceful degradation if at all possible.
In addition, I admit I had to go about this a very hacky-sack way, including pulling in a way to pluralize the parent model to render the appropriate content type's show action, and at this stage I need the code to simply work, not necessarily look pretty doing it.
I KNOW it can be refactored way better, and I hope to do so as I get better with Rails. Or, anyone else who thinks they can improve this is welcomed to have at it. Anyway, here is all my code, just wanted to share back and hope this helps someone in the same scenario.
comments_controller.rb
class CommentsController < ApplicationController
# this include will bring all the Text Helper methods into your Controller
include ActionView::Helpers::TextHelper
def create
#commentable = find_commentable
#comment = #commentable.comments.build(params[:comment])
respond_to do |format|
if #comment.save
format.html { redirect_to(#commentable, :notice => 'Comment was successfully created.') }
else
# Transform class of commentable into pluralized content type
content_type = find_commentable.class.to_s.downcase.pluralize
# Choose appropriate instance variable based on #commentable, rendered page won't work without it
if content_type == 'blogs'
#blog = #commentable
elsif content_type == 'articles'
#article = #commentable
end
format.html { render "#{content_type}/show" }
format.xml { render :xml => #commentable.errors, :status => :unprocessable_entity }
end
end
end
private
# Gets the ID/type of parent model, see Comment#create in controller
def find_commentable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
end
end
articles_controller.rb
class ArticlesController < ApplicationController
def show
#article = Article.where(:status => 1).find_by_cached_slug(params[:id])
#comment = Comment.new
# On another content type like blogs_controller.rb, replace with appropriate instance variable
#content = #article
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #article }
end
end
end
show.html.erb for articles (change appropriate variables for blog or whatever)
<h1><%= #article.title %></h1>
<%= #article.body.html_safe %>
<%= render :partial => 'shared/comments', :locals => { :commentable => #article } %>
shared/_comments.html.erb (I'm leaving out the displaying of posted comments here for simplification, just showing the form to submit them)
<%= form_for([commentable, #comment]) do |f| %>
<h3>Post a new comment</h3>
<%= render :partial => 'shared/errors', :locals => { :content => #comment } %>
<div class="field">
<%= f.label :name, :value => params[:name] %>
<%= f.text_field :name, :class => 'textfield' %>
</div>
<div class="field">
<%= f.label :mail, :value => params[:mail] %>
<%= f.text_field :mail, :class => 'textfield' %>
</div>
<div class="field">
<%= f.text_area :body, :rows => 10, :class => 'textarea full', :value => params[:body] %>
</div>
<%= f.submit :class => 'button blue' %>
<% end %>
shared/_errors.html.erb (I refactored this as a partial to reuse for articles, blogs, comments, etc, but this is just a standard error code)
<% if content.errors.any? %>
<div class="flash error">
<p><strong><%= pluralize(content.errors.count, "error") %> prohibited this page from being saved:</strong></p>
<ul>
<% content.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>

I slightly refactored #Shannon answer to make it more dynamic. In my 'find_parent' method I'm grabbing the url path and fetching the controller name. In the 'create' method I'm creating an 'instance_variable_set' which creates a dynamic variable for either Articles (#article) or Blogs (#blog) or what ever it may be.
Hopefully you'll like what I've done? Please let me know if you have any doubts or if something can be improved?
def create
#comment = #commentable.comments.new(params[:comment])
if #comment.save
redirect_to #commentable, notice: "Comment created."
else
content_type = find_parent
instance_variable_set "##{content_type.singularize}".to_sym, #commentable
#comments = #commentable.comments
render "#{content_type}/show"
end
end
def find_parent
resource = request.path.split('/')[1]
return resource.downcase
end

You're getting an error because the blogs/show view likely refers to the #blog object, which isn't present when you render it in the comments controller.
You should go back to using the redirect_to rather than render. It wasn't displaying a flash when you made an invalid comment because you weren't telling it to set a flash if the comment wasn't saved. A flash will persist till the next request.

Related

Do I need to reload the comment object or the partial that contains the comment in case of using Ajax in Rails 3?

I have been trying to figure out adding comments without reloading the page using Ajax, after reading few different tutorials this is what I came up to so far, and it's not working:
inside user_comments/_comments.html.erb
<div id="comment_form">
<%= simple_form_for [#commentable, #comment], :html => { :multipart => true }, :remote => true do |f| %>
<div class="picture"><%= image_tag current_user.avatar.url(:thumb) %></div>
<%= f.input :content, label: false, :placeholder => "Add Comment", :input_html => { :rows => 4 } %>
<%= f.submit "Add Comment" %>
<% end %>
</div>
Inside the controller:
def create
#users = User.all
#comment = #commentable.user_comments.new(params[:user_comment])
#comment.user_id = current_user[:id]
##commentable.user_comments.create(:user_id => current_user[:id])
if #comment.save
flash[:notice] = "Successfully created comment."
respond_to do |format|
format.html { redirect_to #commentable }
format.js
#format.js #{ render 'create.js.erb' }
end
else
render :new
end
end
and inside the create.js.erb
// Display a Javascript alert
<% if remotipart_submitted? %>
$("#comments_list").append("<%= escape_javascript(render(:partial => 'user_comments/comments')) %>");
<% end %>
I'm using a Gem called: remotipart
I don't know what I'm missing in the process.
in the console I get:
POST http://localhost:3000/assignments/2/user_comments
200 OK
134ms
which means the post goes through, but the comment doesnt get added back to the partial.
Ok after 2 days! I fixed it, here is what I can share and might help:
1- make sure to include the :remote => true to the form that is about to be submitted
2- Check the controller and see what the Create action is being redirected, in my case I changed to this:
def create
#users = User.all
#comment = #commentable.user_comments.new(params[:user_comment])
#comment.user_id = current_user[:id]
##commentable.user_comments.create(:user_id => current_user[:id])
if #comment.save
flash[:notice] = "Successfully created comment."
respond_to do |format|
format.html
format.js {#comments = #commentable.user_comments}
end
else
render :new
end
end
Then make sure the create.js.erb is written properly:
$("#comments_list").empty()
$("#comments_list").append("<%= escape_javascript(render(:partial => 'comments')) %>");
there you go! I hope some creates a proper tutorial for newbies like me :)!

How to move database operations from controller to model

I've started to work on old project with new tools. This time - Rails on Ruby. I managed to make some progress, and now i want to improve one element of my code.
Whole project is about bugtracking with full history search of all tracked bugs. Now i'm on stage where user is entering bugs. Every bug belong to table which belongs to projects.
Only problem - now - is autocompletion of table name when i'm using completely new name (with tables that are already present in database it's working just fine, fills table_id in Bug entry).
Part of view responsible for entering (or selecting from existing) table looks like this:
<div class="control-group">
<%= f.label :table_name, :class => 'control-label' %>
<div class="controls">
<%= f.autocomplete_field :table_name, autocomplete_table_name_bugs_path %>
</div>
</div>
Nothing unusual. This one goes to the model (Bug.rb)
class Bug < ActiveRecord::Base
attr_accessible :bugid, :fixdate, :fixnote, :fixstate_id, :milestone, :newtarget, :notes, :oldtarget, :project_id, :bugreason, :reason_id, :regressiondate, :regressionstate_id, :source, :stringid, :table_id
belongs_to :project
belongs_to :table
belongs_to :reason
belongs_to :state
def table_name
table.name if table
end
#def table_name=(name)
# self.table = Table.find_or_create_by_name(name) unless name.blank?
#end
end
No validation for now as you can see. table_name=(name) commented as it's apparently doing nothing in my code.
This is working with bugs controller (bugs_controller.rb)
def create
if params[:bug][:table_id].nil?
if Table.find_or_create_by_name(params[:bug][:table_name])
params[:bug][:table_id] = Table.find_by_name(params[:bug][:table_name]).id
params[:bug].delete :table_name
end
end
#bug = Bug.new(params[:bug])
respond_to do |format|
if #bug.save
format.html { redirect_to bugs_path, notice: 'Bug was successfully created.' }
format.json { render json: #bug, status: :created, location: #bug }
else
format.html { render action: "new" }
format.json { render json: #bug.errors, status: :unprocessable_entity }
end
end
end
I put here only part responsible for saving new bugs, i'll manage to handle update part when i do this part right.
What i want to improve is first part. Now it's responsible not only for changing table_name to table_id but for creation of new table if it doesn't exist. I'm aware that this part should be handled by model, but i've no idea how to do that, could use some help.
Another part, as btw. is my dropdown menu where user can select active project. It's handled by partial:
<% #projects.each do |project| %>
<% if project.id == session[:current_project].to_i %>
<li class="disabled"><%= link_to project.name, '#' %></li>
<% else %>
<li><%= link_to project.name, choose_project_path(project.id) %></li>
<% end %>
<% end %>
But it works fine only when used from projects controller. How - by the book - i can handle this from other controllers? To be exact, i want it working same way in whole project.
For now i'm handling it by snippet in every controller, but i'm pretty sure that RoR gods are not happy with me for that.
before_filter :projects
def projects
#projects = Project.all
end
How it should be done in proper way? :)
So i managed to move logic from controller to model. Also, it's saving all data i need - table_id to bugs table and project_id (stored in session) to tables table when creating new table.
So part of form render is not changed:
<div class="control-group">
<%= f.label :table_name, :class => 'control-label' %>
<div class="controls">
<%= f.autocomplete_field :table_name, autocomplete_table_name_bugs_path %>
</div>
</div>
Model now looks like this:
class Bug < ActiveRecord::Base
attr_accessible :table_name, :bugid, :fixdate, :fixnote, :fixstate_id, :milestone, :newtarget, :notes, :oldtarget, :project_id, :bugreason, :reason_id, :regressiondate, :regressionstate_id, :source, :stringid, :table_id
belongs_to :project
belongs_to :table
belongs_to :reason
belongs_to :state
def table_name
table.name if table
end
def table_name=(name)
self.table = Table.find_or_create_by_name(name, project_id: self.project_id ) unless name.blank?
end
end
So only change is uncommenting table_name= method and adding project_id to creation of new table (and lack of project_id was the reason it didn't work earlier).
Controller looks like this:
def create
#bug = Bug.new(params[:bug])
#bug.project_id = session[:current_project]
respond_to do |format|
if #bug.save
format.html { redirect_to bugs_path, notice: 'Bug was successfully created.' }
format.json { render json: #bug, status: :created, location: #bug }
else
format.html { render action: "new" }
format.json { render json: #bug.errors, status: :unprocessable_entity }
end
end
end
It's working like charm. Still i've question remaining, but will post it as separate one.

Create New Rails Record without HTML Redirect

I have a Rails app where I have Article and Like models. I want someone to be able to create a Like record, similar to Facebook, where the database records the new record without redirecting them. The two requirements I have are: create a new Like record without redirecting the user and have a flash or js message that confirms the record was created.
I tried putting this in my view. However, when I do this, it creates two identical records:
<%= form_for #like, :remote=> true do |f| %>
<%= f.hidden_field(:article_id, :value => article.id) %>
<%= f.submit "Like" %>
<% end %>
I also, tried this which resulted in one record creating, but it took me to localhost:3000/likes:
<%= form_for #like do |f| %>
<%= f.hidden_field(:article_id, :value => article.id) %>
<%= f.submit "Like" %>
<% end %>
and then in the Like Controller commenting out the format.html and json:
def create
#like = Like.new(params[:like])
respond_to do |format|
if #like.save
# format.html { redirect_to #like, notice: 'Like was successfully created.' }
# format.json { render json: #like, status: :created, location: #like}
else
format.html { render action: "new" }
format.json { render json: #like.errors, status: :unprocessable_entity }
end
end
end
What's the best way to meet my two requirements? Thank you!
I assume Articles and Likes are related. When you want to 'like' an article you are actually updating that article, right? In other words, you would have to change the redirect within the update action of the articles controller.
P.S. I would use some javascript for submitting a form, for example:
<script type="text/javascript">
$(document).ready(function() {
$('.edit_[here_comes_the_name] input[type=checkbox]').click(function() {
$(this).parent('form').submit();
});
});
</script>
This example submits the input from a particular form (the parent) when clicking on the check_box. Of course you would have to adjust it to fit your needs.

Adding ajax to Rails 3 form_for

I'm learning to program and got a form running in my Rails 3 app. Now I'm attempting to add ajax to the form so the page doesn't reload after submitting.
I've followed the numerous tutorials but can't quite seem to figure out how to bring it together. The form adds new Objects to the Profile through the following model:
class Profile < ActiveRecord::Base
has_many :objects
end
class Object < ActiveRecord::Base
belongs_to :profile
end
My form in views/profiles/_object_form.html.erb:
<%= form_for(#object, :remote => true) do |f| %>
<% end %>
Where the form and its created objects are rendered in my views/profiles/_about.html.erb:
<div id="newObjects">
<%= render :partial => 'object_form' %>
</div>
<div id="objectList">
<%= render :partial => 'object', :collection => #profile.objects, :locals => {:object_count => #profile.objects.length) %>
</div>
In my objects_controller.rb I have the following create action:
def create
#object = Object.new(params[:object].merge(:author_id => current_user.id))
respond_to do |format|
if #object.save!
format.html {redirect_to profile_path(#object.profile) }
format.js { render }
else
format.html { redirect_to #profile, :alert => 'Unable to add object' }
end
end
end
In views/objects/create.js.erb:
$('#objectList').append("<%= escape_javascript(render #profile.object)) %>");
So I have a form calling an action in another controller to which I want to add ajax. What happens at the moment is that I need to reload the profile to show the newly created object. What am I doing wrong?
CLARIFICATION: Other than the create action in the ObjectsController, I only reference #object once elsewhere. That's in the ProfilesController's show action:
def show
#profile = Profile.find(params[:id])
#superlative = #profile.superlatives.new`
end
Not sure if this is a full code snippet for your create action, but looks like you are trying to call render on an instance variable that doesn't exist... #profile is never set in the create method in the ObjectController...
Perhaps you meant to type $('#objectList').append("<%= escape_javascript(render #object)) %>");
Also noticed that in your existing code you're making a call to render #profile.object, but the Profile class has a has_many relationship with your Object class, so if that was the right code, then you should type render #profile.objects (plural, not singular).
But I would think you would likely want the code I mentioned above, since you are appending onto the list of objects, not rendering the list again?

New record not being saved with any values

I started the question differently, about a collection_select, but I found out that is not the problem.
This particular model won't save any data at all. It just ignores the values in the parameters. I can only save new records with NULL values (except for the timestamp fields).
See my comment for my latest try to fix it.
I have generated a few models with the handy scaffold command. Now I have tried to change a textbox to a collection_select for linking the new entity to the correct related one.
Using rails 3.1RC4 (hopefully this is not a bug).
In the _form.html.erb I use the following code:
<div class="field">
<%= f.label :category_id %><br />
<%= f.collection_select(:category_id, Admin::Category.all, :id, :name) %>
</div>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
...all other items...
<div class="actions">
<%= f.submit %>
</div>
After I click the submit button I receive error messages. It says that the name and permalink do not comply to the validation. I don't understand however, because in the logfiles I find this:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"my token is here", "admin_branche"=>{"category_id"=>"3", "name"=>"Verzekeraars", "permalink"=>"verzekeraars", "visible"=>"1"}, "commit"=>"Create Branche"}
To me it seems that the params contain all the needed values.
For the sake of completeness I will post my create method and model below.
So far I have tried switching back and forth between collection_select and f.coll... with no success. The current setup seems most appropriate to me, based on the logs.
I have also googled a lot, but haven't been able to find the answer. Question 2280106 on this site looks the same, but it had to do with attr_accessible which I have commented out in the model (I restarted the server afterwards and retried, just to be sure).
Help is much appreciated!
branche.rb:
class Admin::Branche < ActiveRecord::Base
# attr_accessible :name, :permalink
#relationships
has_many :courses, :as => :parent
belongs_to :category
#validations
validates :name, :presence => true, :length => {:maximum => 255}
validates :permalink, :presence => true, :length => { :within => 4..25 }
end
create action in the controller:
def create
#admin_branch = Admin::Branche.new(params[:admin_branch])
respond_to do |format|
if #admin_branch.save
format.html { redirect_to #admin_branch, notice: 'Branche was successfully created.' }
format.json { render json: #admin_branch, status: :created, location: #admin_branch }
else
format.html { render action: "new" }
format.json { render json: #admin_branch.errors, status: :unprocessable_entity }
end
end
end
In the controller, you're doing this:
#admin_branch = Admin::Branche.new(params[:admin_branch])
You should do this:
#admin_branch = Admin::Branche.new(params[:admin_branche])
If you look at the request parameters, the attributes are under "admin_branche", not "admin_branch".
I think that should solve your problems, if not, please let us know.
If you have problems with the generated inflections, you can completely customize them in the config/initializers/inflections.rb
just add something like this:
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'branch', 'branches'
end