Adding threading and email subscriptions to comments [closed] - ruby-on-rails

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)

Related

How to allow users to submit answers to a questionnaire app?

I'm building a questionnaire app.
for now I've 3 models
1- Exam -> has_many :questions
2- Question -> has_many :answers && belongs_to :exam
class QuestionsController < ApplicationController
def index
#questions = Question.all
end
def show
exam = Exam.find(params[:exam_id])
#question = exam.questions.find(params[:id])
end
def new
exam = Exam.find(params[:exam_id])
#question = exam.questions.build
#question = Question.new
#question.answers.build
end
def create
exam = Exam.find(params[:exam_id])
#question = exam.questions.create(question_params)
if #question.save
redirect_to #question.exam, notice: "Exam created!"
else
render :new
end
end
def destroy
#question = Question.find(params[:id])
#question.destroy
redirect_to #question.exam
end
private
def question_params
params.require(:question).permit(:title,:timer,:exam_id,answers_attributes:[:title,:correct, :question_id], :sort => [])
end
end
3- Answer -> belongs_to :question
I can add an exam, and add questions to the exam and add answers for each question. (through nested form)
So for now the admin is the only user who can add exams,questions,answers. I want make the exam visible to other users so the can take and exam and see the result.
I thought about making a 4th model for submission that belongs to Exam where users can see the questions with the options and chose the option that they want and submitting it. After the submit they get a page with the result if the pass the exam or not!
BUT HOW TO DO IT?
UPDATE!!!
The code above works fine!. I'm only searching for some way to make the exam visible or takable for the users
<%= form_for [#exam, #submission] do |f| %>
<% #questions.each do |question| %>
<p><%= question.title %></p>
<ul>
<% question.answers.each do |answer| %>
<li>
<%= #chosen_option = answer.title %>
<%= f.fields_for :option do |o| %>
<%= render 'option_fields', :f => o %>
<% end %>
</li>
<% end %>
</ul>
<% end %>
<br>
<br>
<%= f.hidden_field :user_id, :value => current_user.id %>
<%= f.submit "Submit", class: "btn btn-primary " %>
<% end %>
First, you need to work some more on your models.
I am assuming that every question has at least one correct answer (unless you are building a survey tool). If that is the case, you also need the notion of correct answer(s) to be encapsulated in the models.
Case 1: Single correct answer per question
Add a correct_answer_id to the question model.
Case 2: Multiple correct answers per question
If a question can have multiple correct answers you need to create a join table/model
# migration for model CorrectAnswer
create_table :correct_answers do |t|
t.integer question_id
t.integer answer_id #answer_id is the id of the answer selected by a user
end
# Also update the Question class
class Question < ActiveRecord::Base
...
has_many :correct_answers
...
end
Note: I will recommend the second option even if you are planning on a single correct answer. This will allow your models to be flexible for any future requirements.
For creating submissions, you need to create a new Model called Submission. The fields of the submission model with following fields
#migration for Submission model
create_table :submissions do |t|
t.integer user_id
t.integer exam_id
t.integer question_id
t.integer answer_id
end
You will need to create a corresponding Submissions controller which will allow you non-admin users to submit answers to an exam.
Do not forget to create appropriate index for the tables
So, I already solved this problem by changing the questions model.
I added a answers array to the question model
create_table "questions", force: :cascade do |t|
t.string "answers", default: [], array: true
end
changed also something in the view of the form of submission
<% question.answers.each_with_index do |answer, index| %>
<p><%= answer %><span>
<%= radio_button_tag 'submission[answers][]',"#{answer}", id: "#{answer}", class: "form-control"%>
</span></p>
<% end %>
The problem now is that I get the questions like this
radio_button
I can only select one radio button! So if I select answer_A from question 1 and then I select answer_B from question 2. Question 1 then will be deselect?

Threading on polymorphic comments

I've setup two models that are commentable through same comments table:
My comments schema:
create_table "comments", force: true do |t|
t.text "body"
t.integer "commentable_id"
t.string "commentable_type"
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
end
My comment model:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
belongs_to :user
acts_as_votable
end
My movie model
class Movie < ActiveRecord::Base
belongs_to :user
has_many :comments, as: :commentable
end
My book model:
class Book < ActiveRecord::Base
belongs_to :user
has_many :comments, as: :commentable
end
My comments controller:
def index
#commentable = find_commentable
#comments = #commentable.comments
end
def create
#commentable = find_commentable
#comment = #commentable.comments.build(params[:comment])
#comment.user = current_user
if #comment.save
flash[:notice] = "Successfully created comment."
redirect_to #commentable
else
render :action => 'new'
end
end
def upvote_movie
#movie = Movie.find(params[:movie_id])
#comment = #movie.comments.find(params[:id])
#comment.liked_by current_user
respond_to do |format|
format.html {redirect_to :back}
end
end
def upvote_book
#book = Book.find(params[:book_id])
#comment = #book.comments.find(params[:id])
#comment.liked_by current_user
respond_to do |format|
format.html {redirect_to :back}
end
end
private
def find_commentable
params[:commentable_type].constantize.find(params[:commentable_id])
end
end
How can I add threading(reply to comments) to what I already have?
Here is a blog that talks about threading:http://www.davychiu.com/blog/threaded-comments-in-ruby-on-rails.html
I'm just not sure how to put the two together.
Here is what I have in my movie show view:
<%= render partial: "comments/form", locals: { commentable: #movie } %>
<% #comments.each do |comment| %>
<hr>
<p>
<strong><%= link_to comment.user.username, user_path(comment.user), :class => "user" %>
</strong> <a><%= "(#{time_ago_in_words(comment.created_at)} ago)" %></a>
</p>
<p>
<%= simple_format(auto_link(comment.body, :html => { :target => '_blank' } )) %>
<% end %>
Here is what my comment form looks like:
<%= form_for [commentable, Comment.new] do |f| %>
<%= hidden_field_tag :commentable_type, commentable.class.to_s %>
<%= hidden_field_tag :commentable_id, commentable.id %>
<p>
<%= f.text_area :body %>
</p>
<p><%= f.submit "Submit" %></p>
<% end %>
I scanned the article you mentioned and found the solution there is quite limited.
The basic idea in the article is to set a comment itself as commentable. So a nested comment is actually NOT a comment of the post, but of the parent comment.
The drawbacks are apparent and unacceptable:
It's hard to get other things right. For example, posts.comments.size is no long correct.
You'll have hard dependency on this structure. If in one day you don't want to display comments in thread but plainly, you...will kick a stone.
If you want to do it on current comment system, it's hard.
Actually a simple solution could solve the problem:
Add an extra field reply_to to comment model, referring to other comment's id.
When adding comment, add a reply_to id if it replied to one.
When showing, show a list of all comments with reply_to null.
Then for each comment, show nested comments has its id. And do it recursively.
If you want to limit the nested level, you can add an extra nested_level field, getting in from the front-end. If nest limit is 3, no comments is allowed to reply a comment with nest level of 3.
add: demo helper to render recursively
def render_replied_comments(comment)
if comment.has_reply
comments.replies.each do |reply|
render partial: 'comment', locals: {comment: reply}
render_replied_comment(reply)
end
end
end
# View
#post.top_level_comments.each do |comment|
render partial: 'comment', locals: {comment: comment}
end
You would add a parent_id to the comments model that is a self-referencing relationship. So parent comments would have a parent_id of nil and all child comments would have a parent_id of that parent comment. You are essentially constructing a tree.
The Ancestory Gem is ideal for this or roll your own, good learning experience.

Simple comments with voting and karma

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!

Comments on multiple models

Within my rails app, I currently have comments setup to work with my posts model, which is functioning properly. How do I add comments to my books model?
Here is what I have so far:
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"
t.integer "book_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 book model:
class Book < ActiveRecord::Base
has_many :comments
end
In my comment model:
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :book
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 %>
What do I add to my comments controller to get comments working on both posts and books? What do I add to my routes file?
Thanks in advance for any help.
You don't want to specify each type of object that can hold Comment objects. That creates a headache of if-elsif-else blocks all over the place. Instead, you want things to be Commentable, and they all will have .comments on them.
This is called a polymorphic association in Active Record. So you would have your models something like:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
class Post < ActiveRecord::Base
has_many :comments, as: :commentable
end
class Book < ActiveRecord::Base
has_many :comments, as: :commentable
end
And modify your database accordingly, it's all in the linked article. Now when you build a Comment object for a form, it will have pre-populated a commentable_id and commentable_type, which you can toss in hidden fields. Now it doesn't matter what the Comment is associated with, you always treat it the same.
I'd leave User as a separate association, since it's not really the same idea.

Create comments for two models from comments controller ruby on rails

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!

Resources