I've tried every commenting gem out there and they pretty much all suck.
Take a look at this question I previously asked:Finding user total votes recieved on posts by other users
Per recommendation, I've decided to build my own commenting system from scratch.
Here is the goal:
To have a post model, user model(using devise), comment model.
The users can create comments. These comments are votable. The amount sum of votes users receive on the comments they made is their score or karma.
How do I implement something like this?
So far this is what I have:
I ran
rails generate model Comment commenter:string body:text post:references
The migration
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.string :commenter
t.text :body
t.references :post
t.timestamps
end
add_index :comments, :post_id
end
end
In my comment model
class Comment < ActiveRecord::Base
belongs_to :post
attr_accessible :commenter, :body
end
In my post model
class Post < ActiveRecord::Base
has_many :comments
end
My routes file
resources :posts do
resources :comments
end
My comments controller
class CommentsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.create(params[:comment].permit(:commenter, :body))
redirect_to post_path(#post)
end
end
Within my posts show view
<h2>Comments</h2>
<% #post.comments.each do |comment| %>
<p>
<strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
<strong>Comment:</strong>
<%= comment.body %>
</p>
<% end %>
<%= form_for([#post, #post.comments.build]) do |f| %>
<p>
<%= f.label :commenter %><br />
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body %><br />
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Edit Post', edit_post_path(#post) %> |
<%= link_to 'Back to Posts', posts_path %>
I really need help here: Instead of picking a commenter name, I need the controller to require the user to be logged in, and pass the current user as the commentor of the comment. How do I implement in place editing of a comment?
I then need to use either one of these gems to make the comments votable:
https://github.com/bouchard/thumbs_up
https://github.com/ryanto/acts_as_votable
Lastly I need to be able to calculate the total votes a given user has received on all of their posted comments. Something like #user.comments.votes.size
To handle assigning the current_user to the comment, first you'll need to change the commenter column to an id that references Users (I would also rename it to commenter_id). So, you'll want to generate the model like so:
rails generate migration ChangeCommentsCommenterToCommenterId
# db/migrate/<timestamp>_change_comments_commenter_to_commenter_id.rb
class ChangeCommentsCommenterToCommenterId < ActiveRecord::Migration
def change
remove_column :comments, :commenter
add_column :comments, :commenter_id, :integer, null: false
add_index :comments, :commenter_id
end
end
Or, regenerate the model from scratch:
rails generate model Comment commenter_id:integer:index body:text post:references
Note that I've added an index to the column. In your Comment model:
# app/models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :commenter, class_name: 'User'
attr_accessible :body
end
Note that, since we're using belongs_to here, when you send the commenter message to an instance of Comment, you'll get back an instance of User.
Next you'll need to update your controller to make the proper user assignment. I would also recommend a bit of refactoring to private methods to make the implementation more expressive of the domain:
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
post.comments.create(new_comment_params) do |comment|
comment.commenter = current_user
end
redirect_to post_path(post)
end
private
def new_comment_params
params.require(:comment).permit(:body)
end
def post
#post ||= Post.find(params[:post_id])
end
end
Since you're redirecting to post_path, I've assumed that you don't need to keep the #comment instance variable.
Finally, you'll want to remove the commenter field from the form in the view. Does that do the trick?
[EDIT: Adding this section to address the question about voting...]
I'm not completely clear on exactly what parts of the voting you're looking for help with, but at least from a high-level perspective, I'd guess you'd probably want a VotesController that's accessible from a nested route:
# config/routes.rb
...
resources :comments do
resources :votes, except: :index
end
Hope this helps!
Related
So I have two models
class First < ActiveRecord::Base
belongs_to :story
end
class Story < ActiveRecord::Base
has_one :first
end
I want to create a new first, which is associated with a story. Each story can only have one first. I'm trying to use this as my form_for
<%= form_for ([#story, #first]) do |f| %>
<%= f.label :message %><br/>
<%= f.text_field :message %><br/>
<%= f.submit %>
<% end %>
However, I don't know how to set this up in my routes to cater for this. At the moment, I get a "undefined method story_firsts_path" error. Here is my firsts controller
class FirstsController < ApplicationController
def new
#story = Story.new
#first = #story.build_first
end
end
Am I way off here or am I somewhat on the right track?
Thanks!
You should use nested routes to define story and first. Like this:
resources :stories do
resources :firsts
end
rails newbie here.
I want my questions/view/index to show the name of the language associated with a question, rather than the language_id.
My question model is:
class Question < ActiveRecord::Base
validates :phrase, presence: true
has_many :answers, dependent: :delete_all
belongs_to :language
end
class CreateQuestions < ActiveRecord::Migration
def change
create_table :questions do |t|
t.string :phrase
t.string :language
t.timestamps
end
end
end
class AddLanguageIdToQuestions < ActiveRecord::Migration
def change
add_column :questions, :language_id, :integer
end
end
My language model is:
class Language < ActiveRecord::Base
end
class CreateLanguages < ActiveRecord::Migration
def change
create_table :languages do |t|
t.string :name
t.timestamps
end
end
end
In my questions controller:
def index
#questions = Question.all
#language = Language.find(#questions.language_id)
end
In the questions/_form.html.erb:
<p>
<%= f.label :language_id %><br>
<%= f.select :language_id, #languages.map { |l| [l.name, l.id] }, {:prompt => 'select language'} %>
</p>
And in the questions/view/index.html.erb:
<% #questions.each do |question| %>
<li>"<%= link_to question.phrase, question %>" in <%= question.language.name %>?%></li>
<% end %>
The error I keep getting, despite trying several variations of "question.language.name" (which works just fine in the show view) is "undefined method "language_id" in the index view.
Any help would be very much appreciated.
just change your index action to:
def index
#questions = Question.all.includes(:language)
end
edit
<% #questions.each do |question| %>
<% unless question.language.nil? %>
<li>"<%= link_to question.phrase, question %>" in <%= question.language.name %>?%></li> <% end %>
<% end %>
OR
<% #questions.each do |question| %>
<li>"<%= link_to question.phrase, question %>" in <%= question.language.name unless question.language.nil? %>?%></li>
<% end %>
Both will work fine depends on you what do you want.
feel free to ask if problem continues or not solved.
Your Problem
What you were doing wrong was:
finding all questions and then this line was wrong finding all question's language at once.
#language = Language.find(#questions.language_id)
And to avoid this: better solution is to avoid N + 1 query problem using includes
ActiveRecord Associations
This sounds like a job for ActiveRecord Associations, specifically the has_many association:
ActiveRecord associations basically use a foreign_key in your database to pull relational data & append to your object. Currently, you're only focused on using a single object, without any associated data.
--
Your problem can be fixed using the following:
#app/models/question.rb
Class Question < ActiveRecord::Base
belongs_to :language
end
#app/models/language.rb
Class Language < ActiveRecord::Base
has_many :questions
end
This will allow you to call the following in your controller & view:
#app/controllers/questions_controller.rb
Class QuestionsController < ApplicationController
def index
#questions = Question.all
end
end
#app/views/questions/index.html.erb
<% #questions.each do |question| %>
<%= question.language.name %>
<% end %>
--
Bonus
You can use the .delegate method to provide you with the ability to stop the law of dementer issue:
#app/models/question.rb
Class Question < ActiveRecord::Base
belongs_to :language
delegate :name, to: :language, prefix: true #-> #question.language_name
end
#questions.language_id is incorrect because you are trying to retrieve a single id from an array. i.e. Question.all returns an array. Also, Language.find requires a single id parameter to return a single language.
What exactly are you trying to return with Language.find(#questions.language_id) ? An array of languages that have a question that belong to it?
Also, where is the _form.html.erb partial called? By default, rails scaffold will call the partial in the new and edit actions and therefore if you are trying to set #languages for the select field in the form partial, then you would not do it in the index action. Also, Consider using a collection select for this field as it is for an association.
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 8 years ago.
Improve this question
I've built a simple rails app with three models, Posts, Users, and Comments.
I've tried every commenting gem out there and they all have some shortfall.
So I build my own comments system.
Users can comment on Posts. Every comment is votable (using acts_as_votable gem). A users score is made up by the sum total votes they have received on their comments.
Here is what I have in my schema for the comments:
create_table "comments", force: true do |t|
t.text "body"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "user_id"
t.integer "post_id"
end
In my user model:
class User < ActiveRecord::Base
has_many :comments
acts_as_voter
end
In my post model:
class Post < ActiveRecord::Base
has_many :comments
end
In my comment model:
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :user
acts_as_votable
end
In my comments controller:
class CommentsController < ApplicationController
def create
post.comments.create(new_comment_params) do |comment|
comment.user = current_user
end
respond_to do |format|
format.html {redirect_to post_path(post)}
end
end
def upvote
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
#comment.liked_by current_user
respond_to do |format|
format.html {redirect_to #post}
end
end
private
def new_comment_params
params.require(:comment).permit(:body)
end
def post
#post = Post.find(params[:post_id])
end
end
In my routes file:
resources :posts do
resources :comments do
member do
put "like", to: "comments#upvote"
end
end
end
In my view:
<% #post.comments.each do |comment| %>
<%= comment.body %>
<% if user_signed_in? && (current_user != comment.user) && !(current_user.voted_for? comment) %>
<%= link_to “up vote”, like_post_comment_path(#post, comment), method: :put %>
<%= comment.votes.size %>
<% else %>
<%= comment.votes.size %></a>
<% end %>
<% end %>
<br />
<%= form_for([#post, #post.comments.build]) do |f| %>
<p><%= f.text_area :body, :cols => "80", :rows => "10" %></p>
<p><%= f.submit “comment” %></p>
<% end %>
In the user profile views: (this shows the users score
<%= (#user.comments.map{|c| c.votes.count}.inject(:+))%>
How do I implement threading?(at one level, I'm assuming multiple levels just makes it really messy)
How do I make the threaded comments votable?(both the parents and children) What has to be done with the routes?
How do I ad a simple email notification a user can subscribe to to receive a simple message saying that a new comment has been posted to their thread?
How do I pull the users score calculated by all votes received on comments made by the user including child comments?
If I understand the question correctly, you want to allow comments to be commented on. In that case, in your Comment model you will need a parent_id:integer attribute. Then add the following associations :
class Comment
...
belongs_to :parent, class_name: 'Comment'
has_many :children, class_name: 'Comment', foreign_key: 'parent_id'
...
end
Now you have a tree structure for your comments. This allows comments of comments of comments and so on.
if my_comment.parent.nil? then you are at the root. if my_comment.children.empty? then there are no comments on the comment.
Trees can be expensive to move through, so adding a max depth could be smart.
How do you implement threading? (To answer one of your questions)
Make the comment-to-user association polymorphic, and then you can add a comment-to-comment association the same way.
What was the 'shortfall' you found with existing gems that prevented you doing this? (Since acts_as_commentable supports this out if the box)
Ruby on rails newbie here, I have previously been using CakePHP and wanted to allow my create comments controller to create events for two models, any help please?
My comments controller:
def create
#event = Event.find(params[:event_id])
#comment = #event.comments.create(params[:comment].permit(:commenter, :body))
redirect_to event_path(#event)
end
def create
#venue = Venue.find(params[:venue_id])
#comment = #venue.comments.create(params[:comment].permit(:commenter, :body))
redirect_to venue_path(#venue)
end
My create comments view:
<h2>Add a comment:</h2>
<%= form_for([#event, #event.comments.build]) do |f| %>
<p>
<%= f.label :commenter %><br />
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body %><br />
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
This is a classic example for polymorphic association.
There would be a bit of tweaking to get it to work right.
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
...
end
class Venue < ActiveRecord::Base
has_many :comments, as: :commentable
...
end
class Event < ActiveRecord::Base
has_many :comments, as: :commentable
...
end
This will add to your comments Model another attribute called commentable_type so you could differ the types of comments for each Model (Venue, Event)
You would have to run a migration that looks mostly like this
def change
create_table :comments do |t|
t.integer :commenter
t.text :body
t.integer :commentable_id
t.string :commentable_type
t.timestamps
end
end
Now when you migrate you can go to your rails console and see that if you try
Venue.first.comments << Comment.create!(:body => "Body", :commenter => "Guy") # or commenter => 1 depending on your schema
It will be saved to the database as a comment and you can also do the same thing for Event
Now as for your comments controller, I would advise against creating a global comments controller and rather have follow the RESTful approach and have each controller handle his comments.
I.E
# routes.rb
resources :venues
resources :comments
end
resources :events do
resources :comments
end
This way you can both tweak your views according to each controller (venue / events), you follow the RESTful approach as you can use this with either HTML/JSON/XML, you get nicer routes
/events/1/comments # index for all the comments for event 1
/events/1/comments/new # your add a comment form
and same goes for venue.
You can find more info on associations in here http://guides.rubyonrails.org/association_basics.html#polymorphic-associations
Good luck!
Ruby Version: 2.0
Rails Version: 4.0
I have a controller Question, which has an embedded form for a model Answer.
question.rb
class Question < ActiveRecord::Base
has_many :answers, :dependent => :destroy
accepts_nested_attributes_for :answers, :allow_destroy => true
end
answer.rb
class Answer < ActiveRecord::Base
belongs_to :question
end
answers migration
class CreateAnswers < ActiveRecord::Migration
def change
create_table :answers do |t|
t.string :text
t.integer :question_id
t.boolean :correct?
t.timestamps
end
end
end
in the form, when editing or creating a new question - the user may enter up to 4 possible answers, and mark a check box for the "correct" answer(s).
/views/questions/_form.html.erb
<%= form_for(#question) do |f| %>
<div class="field">
<%= f.label :text %><br>
<%= f.text_area :text %>
</div>
<p>Enter up to 4 posisble answer choices.</p>
<%= f.fields_for :answers do |answer| %>
<div class="field">
<%= answer.text_field :text %>
<%= answer.check_box :correct? %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
relevant snippets from questions_controller.rb
def new
#question = Question.new
4.times { #question.answers.build }
end
private
# Use callbacks to share common setup or constraints between actions.
def set_question
#question = Question.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def question_params
params.require(:question).permit(:text, :quiz_id, answers_attributes: [:id, :text, :correct?])
end
Finally - on to my problem
Everything listed above worked perfectly until I added the checkboxes for answer.correct?. When I submit the form as is I get this message in my logs:
Unpermitted parameters: correct
Unpermitted parameters: correct
Unpermitted parameters: correct
Unpermitted parameters: correct
Weird... there is definitely a question mark at the end of that parameter. Allowing this to pass through without the question mark be editing the allowed parameters in the controller gets me this error message:
unknown attribute: correct (this one actually throws an error message, I don't have to go digging in the logs to find this.)
How do I get the form helper to read the question mark?
? is not a valid character for inclusion within a column name. First, create a new database migration:
# from command line
rails generate migration ChangeCorrectInAnswers
Rename your column from correct? to correct:
# in the resulting migration
class ChangeCorrectInAnswers < ActiveRecord::Migration
def up
rename_column :answers, :correct?, :correct
end
end
Run the migration:
# from command line
rake db:migrate
Finally, remove the ? from your field in the view:
# app/views/questions/_form.html.erb
<%= answer.check_box :correct %>