Associating notes with different entities in a database - ruby-on-rails

At my job, we have judges perform a variety of tasks, e.g., rate movies or compare two pieces of text.
We're in the process of designing a new database to hold all our data (we have some data already, but the database it's in is pretty hack), and I'm starting to build a Rails analytics application that will serve as a dashboard on these judgments. Tables will include things like Judges, Movies, Text, MovieRatings, TextComparisons.
As part of the application, we want to be able to add comments or flag items from these tables. For example, someone might want to add a comment to Judge 1 saying "This judge is very inconsistent" and add a comment to Rating 2 saying "This rating is unexpected", or flag different types of movies or texts for review.
What is the best way to handle adding comments or flags to the database? For example, do we want to create a new Comment table for each entity (add a JudgesComments, MoviesComments, TextComments, etc.)? Or do we want to have a single Comments table with (id, comment) columns [which, I guess, would require ids throughout the database to be globally unique within the database, instead of unique only within its table]?

You should use polymorphic associations, so that you will have a single Comment model and controller. According to the excellent #154 "Polymorphic Association" Railscast, after adding a commentable_type:string and commentable_id:integer to your comments table, your code should look something like this:
# app/models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
# app/models/judge.rb
class Judge < ActiveRecord::Base
has_many :comments, :as => :commentable
end
# app/models/movie.rb
class Movie < ActiveRecord::Base
has_many :comments, :as => :commentable
end
# app/models/text.rb
class Text < ActiveRecord::Base
has_many :comments, :as => :commentable
end
# app/controllers/comments_controller.rb
def index
#commentable = find_commentable
#comments = #commentable.comments
end
def create
#commentable = find_commentable
#comment = #commentable.comments.build(params[:comment])
if #comment.save
flash[:notice] = "Successfully created comment."
redirect_to :id => nil
else
render :action => 'new'
end
end
private
def find_commentable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
nil
end
in the routes:
# config/routes.rb
map.resources :judges, :has_many => :comments
map.resources :movies, :has_many => :comments
map.resources :texts, :has_many => :comments
and in the view:
<!-- app/views/comments/index.html.erb -->
<div id="comments">
<% for comment in #comments %>
<div class="comment">
<%=simple_format comment.content %>
</div>
<% end %>
</div>
<h2>New Comment</h2>
<%= form_for [#commentable, Comment.new] do |f| %>
<p>
<%= f.label :content %><br />
<%= f.text_area :content %>
</p>
<p><%= f.submit "Submit" %></p>
<% end %>

I have worked in a system where there was a single Comments table, and a globally unique ID for each record in each table you could have a comment for. The system worked fine, wasn't hard to maintain, and it was easy for new people to see how it worked. Generating new records in the comment-able tables was slow by computer standards, but it wasn't really an issue for the users of the system.

Related

Ruby on Rails: Add form for another model on a form

Within my Ruby on Rails application I am trying to implement a relationship between Group and Contact, whereby one group can contain many contacts and one contact can be part of many groups. I am using a model called Contactgroup to deal with this relationship, and so the tables are:
Group (id, name)
Contact (id, firstname, surname)
Contactgroup (group_id, contact_id)
With example data being:
Groups:
ID Name
1 Singers
2 Drummers
Contacts:
ID Firstname Surname
1 Freddy Mercury
2 Roger Taylor
3 Kurt Cobain
4 Dave Grohl
Contact Groups:
Group_ID Contact_ID
1 1
1 3
1 4
2 2
2 4
What I am trying to do is get it so that when a user creates a group, they can select the contacts that they want to add to that group. This means that there is the group form, whereby the user types the group name, and on this form I want to display checkboxes for each of the user's contacts so that the user can select the contacts they want to add to the group, and when they click submit the new group will be saved in the Group table and the new contact group records will be saved in the Contactgroup table.
This is the app/views/groups/_form.html.erb code:
<%= form_for #group do |f| %>
<% if #group.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#group.errors.count, "error") %> prohibited this group from being saved:
</h2>
<ul>
<% #group.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :name %><br>
<%= f.text_field :name %>
</p>
<h2>Add members:</h2>
<%= form_for([#group, #group.contactgroups.build]) do |f| %>
<p>
<%= f.collection_check_boxes(:contact_id, #contacts, :id, :firstname) %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<p>
<%= f.submit %>
</p>
<% end %>
On here you can see the code I am trying to use to do this:
<h2>Add members:</h2>
<%= form_for([#group, #group.contactgroups.build]) do |f| %>
<p>
<%= f.collection_check_boxes(:contact_id, #contacts, :id, :firstname) %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<p>
<%= f.submit %>
</p>
<% end %>
I have got this from rails guides (http://guides.rubyonrails.org/getting_started.html) but I get the error undefined methodcontactgroups' for #` and don't think this will give me what I want.
My routes file is:
Rails.application.routes.draw do
get 'sessions/new'
get 'sessions/create'
get 'sessions/destroy'
resources :users
get 'welcome/index'
root 'welcome#index'
resources :contacts
resources :groups do
resources :contactgroups
end
resources :contactgroups
get 'sessions/new'
get 'sessions/create'
get 'sessions/destroy'
controller :sessions do
get 'login' => :new
post 'login' => :create
get 'logout' => :destroy
end
end
My groups_controller:
class GroupsController < ApplicationController
def index
#groups = Group.where(user_id: session[:user_id])
end
def show
#group = Group.find(params[:id])
#members = Contactgroup.where(group_id: #group.id)
end
def new
#group = Group.new
#contacts = Contact.where(user_id: session[:user_id])
end
def edit
#group = Group.find(params[:id])
end
def create
#group = Group.new(group_params)
#group.user_id = session[:user_id]
if #group.save
redirect_to #group
else
render 'new'
end
end
def update
#group = Group.find(params[:id])
if #group.update(group_params)
redirect_to #group
else
render 'edit'
end
end
def destroy
#group = Group.find(params[:id])
#group.destroy
redirect_to groups_path
end
private
def group_params
params.require(:group).permit(:name, :user_id)
end
end
And contactgroups_controller:
class ContactgroupsController < ApplicationController
def destroy
#contactgroup = Contactgroup.find(params[:id])
#contactgroup.destroy
redirect_to(:back)
end
end
My models are as follows:
Contact.rb:
class Contact < ActiveRecord::Base
end
Group.rb:
class Group < ActiveRecord::Base
end
Contactgroup.rb:
class Contactgroup < ActiveRecord::Base
belongs_to :contact
belongs_to :group
end
There must be a simple solution to solve this as I assume it is commonly done on other systems, but I am not sure how to do this.
Can someone please help.
You cannot use form inside form. The correct way to use collection_check_boxes is following.
Replace
<%= form_for([#group, #group.contactgroups.build]) do |f| %>
<p>
<%= f.collection_check_boxes(:contact_id, #contacts, :id, :firstname) %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
With just
<p>
<%= f.collection_check_boxes(:contact_ids, #contacts, :id, :firstname) %>
</p>
This was much simpler than initially thought/suggested.
What I needed to do was change the models to:
Contactgroup
belongs_to :contact
belongs_to :group
Contact
has_many :contactgroups
has_many :groups, through: :contactgroups, :dependent => :destroy
Group
has_many :contactgroups
has_many :contacts, through: :contactgroups, :dependent => :destroy
In the groups_controller I needed to change the new method and params to:
def new
#group = Group.new
#group.contactgroups.build
end
private
def group_params
params.require(:group).permit(:name, :user_id, { contact_ids: [] })
end
And then add the following line of code into app/views/groups/_form.html.erb:
<%= f.collection_check_boxes :contact_ids, Contact.where(user_id: session[:user_id]), :id, :firstname ,{ prompt: "firstname" } %>
This provides me with a checkbox for each contact, and allows contactgroup records to be created from the group form.
Ok so the issue is very simple. You are calling #group.contactgroups but you haven't actually set up that association on the group model yet. only have associations set up from the contactgroup side. So you can do contactgroup.group but not group.contactgroups
Your best bet is to actually model this as habtm - as I mentioned earlier. This is how you'd do that:
Contact.rb:
class Contact < ActiveRecord::Base
has_and_belongs_to_many :groups
end
Group.rb:
class Group < ActiveRecord::Base
has_and_belongs_to_many :contacts
end
Note: you still have the concept of the contact-group for HABTM but using Rails standard naming it would be in your database as the contacts_groups table. Then you could build your forms that way.
With a quick google, here's a S/O question on using checkboxes with HABTM (haven't vetted it for usefulness to your situation): Rails 4 - checkboxes for has_and_belongs_to_many association
Using HABTM is Rails standard practice for lots of very good reasons. It really does actually fit your situation (honest!) and it does not actually break the requirement you have of wanting to see it in the SQL (seriously!).
Give it a try first :)
I can tell you how to break Rails conventions... but it's generally well-understood that you shouldn't break conventions until you know what the conventions are there for.

Rails wizard form

I have an enrollment form where a user can enroll to some sort of event.
However, I want to give the posibility for teams to enroll also and I was thinking about a wizard like form.
Basically create 5 records at a time.
The problem is, I'll have a new enrollment creation on each step, so I thought the wicked gem would not do it for this scenario.
Can you give me a few guidelines on how should I approach this?
Maybe just render new after creation if a i.e. team attr is sent from the form?
Maybe use self join?
That's off the top of my head but I know there has to be a clever way to do this.
I'm not sure how your models are structured, but if you have something like:
class Attendee
has_many :enrolments
has_many :events, through: :enrolments
end
class Enrolment
has_many :attendees
belongs_to :event
end
class Event
has_many :enrolments
has_many :attendees, through: :enrolments
accepts_nested_attributes_for :enrolments
end
Then you can do something like:
# controllers/enrolments_controller.rb
class EnrolmentController < ApplicationController
def new
#event = Event.find(params[:event_id])
pax = params[:persons].to_i
pax.times do
#event.enrolments.build
end
end
def create
#event = Event.find(params[:event_id])
#event.enrolments.build(enrolment_params)
#event.save
end
protected
def enrolment_params
# require specific parameters here
params.require(:event).permit(:attendee_attributes => [])
end
end
# views/enrolments/new.html.erb
<%= form_for #event, url: event_enrolments_path(#event) do |f| %>
<%= f.hidden_field :event_id %>
<%= f.fields_for :enrolments do |af| %>
<%= af.select :attendee_id, Attendee.all.collect {|p| [ p.name, p.id ] } %>
<% end %>
<%= f.submit %>
<% end %>
# routes.rb
resources :events do
resources :enrolments
end
That's off the top of my head, but the general idea is that you build the nested fields by running event.enrolments.build based on the number of people passed in the params.
This uses fields_for and accepts_nested_attributes_for. This also makes it really convenient to reuse existing forms by passing in the form context in the partial:
<%= f.fields_for :enrolments do |af| %>
<%= render "enrolments/form", f: af %>
<% end %>

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!

Nested Models with Comments

I have an application where there are a few nested models... Two parent models and two child models.
I'm trying to create comments on the child models and I had it working great for the first one until I realized I have to create comments on the second child so I realized I had to scrap my work because I was targeting the first parent + child model in the comments controller. So I decided to watch Ryan Bates screencast (http://railscasts.com/episodes/154-polymorphic-association) on creating comments that belong to multiple models...unfortunately, it's not working for me and I'm assuming its because I am trying to create comments on the child models. I will show you what I was using before that worked for the one model and I'll show you what im doing now that doesnt work...
here is what i had for the comments controller
def create
#collection = Collection.find(params[:collection_id])
#design = #collection.designs.find(params[:design_id])
#comment = #design.comments.create(comment_params)
#comment.user = current_user
#comment.save
redirect_to collection_design_path(#collection, #design)
end
and here is what it is now after i tried to implement it to work for multiple models
def create
#commentable = find_commentable
#comment = #commentable.comments.build(comment_params)
#comment.user = current_user
#comment.save
end
private
def find_commentable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
nil
end
here are my crazy routes
resources :collections do
member do
post :like
post :unlike
end
resources :designs do
resources :comments
member do
post :like
post :unlike
end
end
end
anyone have any other different ideas for created comments for multiple nested models? Thanks in advance for the help.
EDIT:
Here was the form I was using for the one model
<%= form_for([#collection, #design, #design.comments.build]) do |f| %>
<%= f.text_area :comment %>
<%= f.submit "Comment", :class => "btn" %>
<% end %>
and here is the one i'm using now
<%= form_for([#collection, #design, #commentable, Comment.new]) do |f| %>
<%= f.text_area :comment %>
<%= f.submit "Comment", :class => "btn" %>
<% end %>
Right now when I try to submit the new comment form I get this error
undefined method `comments' for #<Collection:0x0000010150cf88>
which points back to the create method
EDIT 2
Here is my comment model
belongs_to :commentable, :polymorphic => true
belongs_to :user
Here is my design model (which is a child of the collection model)
has_many :comments, :dependent => :destroy, :as => :commentable
belongs_to :user
belongs_to :collection
and my collection model (which has the child model: design)
belongs_to :user
has_many :designs, :dependent => :destroy
and there is more to the models but its not related to the problem.

Adding an object to an array of favorite objects

I'm working on a site which lists many events and I want users who are logged in to be able to add an event to a personal list of favorite/watched events.
I have
event.rb
has_and_belongs_to_many :profiles
and
profile.rb
has_and_belongs_to_many :events
If a user is logged in, I want to a button on each event view that adds that event to the current_user's profile. Basically, this functionality:
current_user.profile.events << event
It works to do that in the console but I can't figure out how to create a form which does this. From this looking at various answers on this website, this is what I have:
Routes.rb
map.resources :events, :member => { :create_calendar => :post }
_event.html.erb
<%= form_for( current_user.profile.events(event) ) do |f| %>
<%= f.submit "Add to Calendar" %>
<% end %>
events_controller.rb
def create_calendar
#event = Event.find(params[:event_id])
current_user.profile.events << #event
end
Any guidance, advice, or tips would be GREATLY appreciated!
I'm not a fan of has_and_belongs_to_many so my advice would be to ditch it and make a new model. Maybe something like this
class SavedEvent < ActiveRecord::Base
belongs_to :user
belongs_to :event
end
Then you can do something like this:
Events View:
form_for SavedEvent.new, :url => "/events/save_event" do |f|
f.hidden_field :event_id, :value => #event.id
f.submit "Save Event"
end
Then to save it:
def save_event
#saved_event = SavedEvent.new(params[:saved_event]
#saved_event.user = current_user
#saved_event.save
end
Naturally you can trim this down a bit and make it a bit more RESTful, but that would be the main building block of how I'd go about doing it.

Resources