I have a Post model that looks like this:
# id :integer
# author_id :integer
# owner_id :integer
# owner_type :string(255)
# content :text
class Post < ActiveRecord::Base
belongs_to :author, class_name: 'User'
belongs_to :owner, polymorphic: true
end
The owner can be a User, Group, or a Place. I'm wondering what is the best approach to model a Comment. Considering that it shares most of its attributes with Post, I thought that the same Post model could serve as Comment using a relation like:
has_many :comments, class_name: 'Post', :as => :owner
But indeed I'm not happy at all with this solution since Post uses the same relation for storing its owner.
It's better to create a different model for comment? What about STI?
To make an abstraction of the real world and keep the things simple (clear, succinct), my suggestion is to use a Comment model:
class Comment < ActiveRecord::Base
belongs_to :post
end
class Post < ActiveRecord::Base
belongs_to :author, class_name: 'User'
belongs_to :owner, polymorphic: true
has_many :comments
end
If you are planning to add comments to another entity, for example, photos, use a Polymorphic Association:
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
class Post < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Photo < ActiveRecord::Base
has_many :comments, :as => :commentable
#...
end
The rails tutorial does exactly this: guides.rubyonrails.org/getting_started.html. It makes a blog with a posts model and a comments controller attached to it.
resources :posts do
resources :comments
end
run this
$ rails generate controller Comments
And add this to the generated 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
You should look at this railscast:
http://railscasts.com/episodes/154-polymorphic-association?view=asciicast
It's a bit old, but it is one of the free ones. You do want to create a different model for your comments, and you want to create a polymorphic association.
Related
In my Post model, I have
has_many :followers
In my Follower model, I have
belongs_to :model
belongs_to :owner,polymorphic: true
In my User and Admin devise models, I have
has_many :followers,as: :owner
Requirement: I want to have something like Post.owners and it should return me a list of all users and/or admins that are following this post.
I'm not sure, but I think that AR doesn't provide a way to load polymorphic associations in just one query. But you can use:
post = Post.find(1)
post_followers = post.followers.includes(:owner)
post_owners = post_followers.map(&:owner)
The solution you're looking for is polymorphic has many through. You can add these lines in your model of User and Admin.
has_many :followers
has_many :posts, through: :followers, source: :owner, source_type: 'Owner'
I think you want something like this:
class Post < ApplicationRecord
belongs_to :owner, polymorphic: true
end
class User < ApplicationRecord
has_many :posts, as: :owner
end
class Follower < ApplicationRecord
has_many :posts, as: :owner
end
From an instance of your User you can then retrieve their posts with #user.posts
The same goes for your Follower, #follower.posts
If you want to get to the parent of your post instance, you can do so via #post.owner. To make this work, however, we need to set up the schema correctly by declaring both a foreign key column and a type column in the model that declares the polymorphic interface using the references form:
class CreatePosts < ActiveRecord::Migration[5.0]
def change
create_table :posts do |t|
# your attribs here
t.references :owner, polymorphic: true, index: true
end
end
end
So I have a has_many through association where between two tables posts and users:
class Post < ApplicationRecord
has_many :assignments
has_many :users, :through => :assignments
end
class User < ApplicationRecord
has_many :assignments
has_many :posts, :through => :assignments
end
class Assignment < ApplicationRecord
belongs_to :request
belongs_to :user
end
Now in my association table (assignment) there are additional attributes for creator:boolean and editor:boolean.
My question is what's the best way to set these secondary attributes from within the controller?
Having looked around I've got a current solution:
posts_controller.rb:
class PostsController < ApplicationController
def create
params.permit!
#post = Post.new(post_params)
if #post.save
Assignment.handle_post(#post.id, params[:creator], params[:editors])
redirect_to posts_path, notice: "The post #{#post.title} has been created."
else
render "new"
end
end
assignment.rb:
class Assignment < ApplicationRecord
belongs_to :request
belongs_to :user
def self.handle_post(post_id, creator, assignment)
Assignment.where(:post_id => post_id).delete_all
Assignment.create!(:post_id => post_id, :user_id => creator, :creator => true, :editor => false)
if editors.present?
editors.each do |e|
Assignment.create!(:post_id => post_id, :user_id => e, :creator => false, :editor => true)
end
end
end
end
So what is essentially happening is I'm getting the user_ids from the form via params (creator returns 1 id, editors returns an array), and AFTER creating the post I'm deleting all columns associated with the post and recreating them off the new attributes.
The issue I have here is I can't run post validations on these associations (e.g. check a creator is present).
My two questions are as follows:
Is this the correct way to handle secondary attributes?
Is there a way to set the association up and then save it all at once so validations can be performed?
This is a more Rails way to do this:
Use nested attributes
post.rb
class Post < ApplicationRecord
# Associations
has_many :assignments, inverse_of: :post
has_many :users, through: :assignments
accepts_nested_attributes_for :assignments
# Your logic
end
assignment.rb
class Assignment < ApplicationRecord
after_create :set_editors
belongs_to :request
belongs_to :user
belongs_to :post, inverse_of: :assignments
# I would create attribute accessors to handle the values passed to the model
attr_accessor :editors
# Your validations go here
validates :user_id, presence: true
# Your logic
private
def set_editors
# you can perform deeper vaidation here for the editors attribute
if editors.present?
editors.each do |e|
Assignment.create!(post_id: post_id, user_id: e, creator: false, editor: true)
end
end
end
end
And finally, add this to your PostsController
params.require(:post).permit(..., assignments_attributes: [...])
This allows you to create Assignments from the create Post action, will run validations on Post and Assignment and run callbacks for you.
I hope this helps!
I would like to do an app, but I am stuck on how to design the model. I have these models so far: User, Post, Tag, Like, Comment. I created for each a model, controller, view and migration file. So far so good, BUT now my question is should I just do the relations between them or create these models too: PostTag, PostLike, PostComment. You know, they would just have the relation between those, so when somebody deletes a tag in a post, he actually just deletes the relation and not the tag itself because the tag is maybe used in another post.
You don't need separate models for relations. Just add has_many and belongs_to to existing models and add needed fields (post_id, user_id etc.) to your database tables.
And tags won't disappear when you delete it from a post in case you won't write a code for a tag destroy. Nothing happens by itself.
P.S. I recommend to use acts-as-taggable-on gem for tags
You will only need to add a Tagging model, and use a polymorphic association to handle Likes. So, you need a User, Post, Comment, Like, Tag, and Tagging
You will not need a controller for Tags or Taggings or Likes
If you want users to be able to create posts and comments as well as like both posts and comments, then here is one way to configure your models:
User model:
class User < ActiveRecord::Base
has_many :posts
has_many :comments
has_many :likes, as: :likeable, dependent: :destroy
end
Post model:
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments
has_many :likes, as: :likeable, dependent: :destroy
has_many :taggings
has_many :tags, through: :taggings
def self.tagged_with(name)
Tag.find_by_name!(name).posts
end
def self.tag_counts
Tag.select("tags.*, count(taggings.tag_id) as count").
joins(:taggings).group("taggings.tag_id")
end
def tag_list
tags.map(&:name).join(", ")
end
def tag_list=(names)
self.tags = names.split(",").map do |n|
Tag.where(name: n.strip).first_or_create!
end
end
end
Comment model:
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
Like model:
class Like < ActiveRecord::Base
belongs_to :likeable, polymorphic: true
end
Tag migration generator:
rails g model tag name
Tag model:
class Tag < ActiveRecord::Base
has_many :taggings
has_many :posts, through: :taggings
end
Tagging migration generator:
rails g model tagging tag:belongs_to post:belongs_to
Tagging model
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
The Post model assumes your tag uses the attribute :name
In order to access your tagged Posts add this route
get 'tags/:tag', to: 'posts#index', as: :tag
And change your index action in your Posts controller to this:
def index
if params[:tag]
#posts = Post.tagged_with(params[:tag])
else
#posts = Post.all
end
end
Finally, add a link to your Post Show view like this:
<%= raw #post.tags.map(&:name).map { |t| link_to t, tag_path(t) }.join(', ') %>
I have a User model and a Book model joined with a Like model.
class Book < ActiveRecord::Base
belongs_to :user
# the like associations
has_many :likes
has_many :liking_users, :through => :likes, :source => :user
class User < ActiveRecord::Base
has_many :books
# the like associations
has_many :likes
has_many :liked_books, :through => :likes, :source => :book
class Like < ActiveRecord::Base
belongs_to :user
belongs_to :book
I want to add an attribute to the Like model so while right now a User can Like a book to add it to their profile, I want the User to be able to write a recommendation.
I generated the attribute Recommendation:text to the Like (joining) model, but am unsure how to add this to views so a User can write a recommendation that will be tied to the Like (and thus that book and user).
I'm looking at this post - Rails has_many :through Find by Extra Attributes in Join Model - which describes something similar but does not explain how to implement this in the views.
Let me know if you can point me in the right direction. Thanks!
I think you should create a separate model for this Recommendation, dependent of the Like model:
class Book < ActiveRecord::Base
belongs_to :user
has_many :comments
has_many :commenters, :through => :comments, :source => :user
class User < ActiveRecord::Base
has_many :books
has_many :comments
has_many :commented_books, :through => :comments, :source => :book
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :book
Then, the logic to create a comment for a book:
# Books Controller
def show
#book = Book.find(params[:id])
#comment = #book.comments.build
end
# show view of the book
form_for #comment do |form|
form.hidden_field :user_id, value: current_user.id
form.hidden_field :book_id
form.text_area :content # the name of the attribute for the content of the comment
form.submit "Post comment!"
end
To list all the comments of a specific User:
# users controller (profile page)
def show
#user = User.find(params[:id])
end
# show view of Users
#user.comments.includes(:book).each do |comment|
"Comment on the book '#{comment.book.name}' :"
comment.content
end
I suppose that the like functionnality works with ajax ? (remote: :true or in pure javascript)
You can append a form with the response of your request with the id of the new like, and again handle the recommendation by an ajax request
This seems to be a fairly common problem over here, yet there is no definitive solution. To restate once again, say I have a model:
def Model < ActiveRecord::Base
has_many :somethings, ...
has_many :otherthings, ...
end
The question is then how to add a third association :combined that combines the two? I know this can be done with :finder_sql and similar result can be achieved with a scope, but neither of these gives me an actual association. The whole point of this is to be able to use it for another association with :through and things like Model.first.combined.some_scope.count
EDIT: the relevant portions of the actual code
class Donation < ActiveRecord::Base
# either Project or Nonprofit
belongs_to :donatable, :polymorphic => true
belongs_to :account
end
class Project < ActiveRecord::Base
belongs_to :nonprofit
end
class Nonprofit < ActiveRecord::Base
has_many :projects
# donations can be either direct or through a project
# the next two associations work fine on their own
# has_many :donations, :as => :donatable, :through => :projects
# has_many :donations, :as => :donatable
has_many :donations, .... # how do I get both here,
has_many :supporters, :through => :donations # for this to work?
end
Thanks.
If Something and Otherthing are sufficiently similar, use STI:
def Model < ActiveRecord::Base
has_many :somethings
has_many :otherthings
has_many :genericthings
end
def Genericthing < Activerecord::Base
# put a string column named "type" in the table
belongs_to :model
end
def Something < Genericthing
end
def Otherthing < Genericthing
end