I need to link Comments to a Post. However the Comment could be (user generated) a simple text, (system generated) a link or an (system generated) image.
At first they all shared the same attributes. So I just needed to create a category attribute, and do different stuff with the text attribute based on that category.
example:
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :author, :class_name => "User"
CATEGORY_POST = "post"
CATEGORY_IMAGE = "image"
CATEGORY_LINK = "link"
validates :text, :author, :category, :post, :presence => true
validates_inclusion_of :category, :in => [CATEGORY_POST, CATEGORY_IMAGE, CATEGORY_LINK]
attr_accessible :author, :text, :category, :post
def is_post?
self.category == CATEGORY_POST
end
def is_link?
self.category == CATEGORY_LINK
end
def is_image?
self.category == CATEGORY_IMAGE
end
end
However this wil not suffice now, because I doesn't feel clean to dump every value in a generic "text" property. So I was thinking about create a polymorphic model (and if needed in a factory pattern). But when I googled about polymorphic models, I get examples like a Comment on a Post, but the same Comment on a Page, kind of relations. Is my understanding of polymorphic different (a model that acts different in different situations, compared to a model that acts the same under different scopes)?
So how would I set up this kind of relationship?
I was thinking of (and please correct me)
Post
id
Comment
id
post_id
category (a enum/string or integer)
type_id (references either PostComment, LinkComment or ImageComment based on category)
author_id
PostComment
id
text
LinkComment
id
link
ImageComment
id
path
User (aka Author)
id
name
But I have no clue how to setup the model so that I can call post.comments (or author.comments) to get all comments. A nice to have would be that the creation of a comment would be through comment and not link/image/postcomment (comment acting as the factory)
My main question is, how to setup up the activerecord models, so the relations stay intact (a author has comments and a post has comments. Comments being either a Link, Image or Postcomment)
I'm going to answer only your main question, the model setup. Given the columns and tables you used in your question, with the exception of Comment, you can use the following setup.
# comment.rb
# change category to category_type
# change type_id to category_id
class Comment < ActiveRecord::Base
belongs_to :category, polymorphic: true
belongs_to :post
belongs_to :author, class_name: 'User'
end
class PostComment < ActiveRecord::Base
has_one :comment, as: :category
end
class LinkComment < ActiveRecord::Base
has_one :comment, as: :category
end
class ImageComment < ActiveRecord::Base
has_one :comment, as: :category
end
with that setup, you can do the following.
>> post = Post.first
>> comments = post.comments
>> comments.each do |comment|
case comment.category_type
when 'ImageComment'
puts comment.category.path
when 'LinkComment'
puts comment.category.link
when 'PostComment'
puts comment.category.text
end
end
Related
I have some problems getting a nested form working. In the below example I have a User that can define multiple custom labels for a post. The user should be able to enter a value for each particular label.
So one Post can have multiple labels, but should only have one value for each label! In example: Post can have a label named "Date" and also have a label named "Mood". For both labels there should be just one value.
The problem is when a User creates a Label -let say "Date"- it should only be possible to enter one value for this post for this particular label. So if a value for date is given, the form shouldn't build another field for date again. (the post already has a date)
User creates custom labels (this works)
On the edit page of the Post, User sees the labels he created in step 1 (this works)
User can enter a value for each of the Label (here is the problem)
I have the following models:
class User < ActiveRecord::Base
has_many :posts
has_many :labels
end
class Post < ActiveRecord::Base
has_many :label_values
belongs_to :user
accepts_nested_attributes_for :label_values, allow_destroy: true
end
class Label < ActiveRecord::Base
has_many :label_values
belongs_to :user
end
class LabelValue < ActiveRecord::Base
belongs_to :post
belongs_to :label
end
In my controller I have
class PostsController < ApplicationController
def edit
#labels = current_user.labels.all
#post.label_values.build
end
end
My form:
= simple_form_for #post do |f|
=f.fields_for :label_values do |s|
=s.association :label, :include_blank => false
=s.input :value
= f.button :submit
Like this, every time a User enters a value for a particular label, the next time a new label_value is build again and that is not what I want. For each label the User should be able to enter one value.
If you intend to use Label to attach metadata to Post you may want to consider using a relationship somewhat like the following:
class User < ActiveRecord::Base
has_many :user_labels # Labels which have been used by this user.
has_many :labels, through: :user_labels
has_many :posts
end
class Post < ActiveRecord::Base
has_many :post_labels
belongs_to :user, as: :author
has_many :labels, through: :post_labels
accepts_nested_attributes_for :post_labels
def available_labels
Label.where.not(id: labels.pluck(:label_id))
end
end
class Label < ActiveRecord::Base
# #attribute name [String] The name of the label
has_many :user_labels
has_many :post_labels
has_many :users, through: :user_labels
has_many :posts, through: :post_labels
validates_uniqueness_of :name
end
# Used to store label values that are attached to a post
class PostLabel < ActiveRecord::Base
belongs_to :post
belongs_to :label
validates_uniqueness_of :label, scope: :post # only one label per post
# #attribute value [String] the value of the label attached to a post
end
# Used for something like displaying the most used labels by a user
class UserLabel < ActiveRecord::Base
belongs_to :user
belongs_to :label
end
In this setup labels act somewhat like "tags" on Stackoverflow. Each separate label is unique in the system but may be attached to many posts - by many users.
Note the validates_uniqueness_of :label, scope: :post validation which ensures this.
The actuals values attached to a tag are stored on the value attribute of PostLabel. One major drawback to this approach is that you are really limited on by type of the PostLabel value column and may have to do typecasting. I really would not use it for dates as in your example as it will be difficult to do a query based on date.
def PostController
def new
#available_labels = Label.all()
end
def edit
#post = Post.joins(:labels).find(params[:id])
#available_labels = #post.available_labels
end
def create
#post = Post.new(create_params)
if (#post.save)
#post.labels.each do |l|
#post.user.labels << l
end
# ...
else
# ...
end
end
private
def create_params
params.permit(:post).allow(:post_labels)
end
end
Added
This is simply some opinionated recommendations:
To add new labels to a post I would add a text input below the already assigned labels.
I would autocomplete using GET /labels. and filter against the inputs already present in the form. You can allow users to create new labels on the fly by doing an ajax request to POST /labels.
I finally came to something. In the Post model I created:
def empty_labels
Label.where.not(:id => labelValue.select(:label_id).uniq)
end
def used_simfields
LabelValue.where(post_id: id)
end
Then in the view I did:
= simple_form_for #post do |f|
-#post.used_labels.each do |used_label|
=f.fields_for :label_values, used_label do |old_label|
=old_label :value
-#post.empty_labels.each do |empty_label|
=empty_label.fields_for :label_values, #post.label_values.new do |new_label|
=new_label.association :label
=new_label.input :value
I am very sure there are nicer ways to achieve this, so new ideas are very welcome.
I'm using of Ruby on Rails.
I have some questions about definition of foreign key.
I defined some models.
When I access book title from class Trade via ISBN like this.
trade = Trade.first
trade.isbn #=> just get isbn in case 1.
trade.isbn.title #=> get book title in case 2.
Why case 2 doesn't work as expected??
class Trade < ActiveRecord::Base
attr_accessible :cost, :isbn, :shop_id, :volume
# belongs_to :book, foreign_key: "isbn" # case 1
belongs_to :isbn, class_name: :Book, foreign_key: :isbn # case 2
belongs_to :shop
end
class Author < ActiveRecord::Base
attr_accessible :age, :name
has_many :books
has_many :trades, through
end
class Book < ActiveRecord::Base
self.primary_key = :isbn
attr_accessible :author_id, :cost, :isbn, :publish_date, :title
belongs_to :author
end
class Shop < ActiveRecord::Base
attr_accessible :name
has_many :trades
end
I am not entirely sure what you're asking, what behavior you're seeing, or what behavior you expected. That said, this is what's happening with the code you've pasted (case 2?):
trade = Trade.first
trade.isbn
This returns the Book instance referenced by Trade#isbn.
trade.isbn.title
This is equivalent to
book = trade.isbn
book.title
which returns the title of the Book instance referenced by Trades#isbn. Is this not what you expected?
So Your question is what is difference between symbol (:isbn) and string ("isbn")?
In shot symbols are considered Rubys immutable strings You can read more here:
http://www.robertsosinski.com/2009/01/11/the-difference-between-ruby-symbols-and-strings/
In general convention is to use symbols as keys inside You options hashes that You pass to methods, though some libs/gems etc support both. But in particular case of Yours it looks like that this value is being typecasted to string, so everything that is passed as option to foreign_key will converted to string using to_s.
I have a need to do one query on a record set and get list of many type objects.
In this example I will use a blog post which a blog post has many different types.
Base Post:
class Post < ActiveRecord::Base
belongs_to :postable, :polymorphic => true
attr_accessible :body, :title
end
Audio Post:
class AudioPost < ActiveRecord::Base
attr_accessible :sound
has_one :postable, :as => :postable
end
Graphic Post:
class GraphicPost < ActiveRecord::Base
attr_accessible :image
has_one :postable, :as => :postable
end
This will allow me to do something like this.
#post = Post.all
#post.each do |post|
post.title
post.body
post.postable.image if post.postable_type == "GraphicPost"
post.postable.sound if post.postable_type == "AudioPost"
end
Though this works, it feels wrong to check the type because that goes against the duck type principle. I would assume there is a better way then this to do the same thing.
What is be a better design to achieve this same goal or am I just over thinking my design?
See my comments.
Anyway, if you want polymorphic, I would write logic in model:
class Post
delegate :content, to: :postable
class AudioPost
alias_method :sound, :content
class GraphicPost
alias_method :image, :content
You will want to render images different than a sound, for that part, I would use a helper:
module MediaHelper
def medium(data)
case # make your case detecting data type
# you could print data.class to see if you can discriminate with that.
and call in view
= medium post.content
What i have created is a "active" field in my topics table which i can use to display the active topics, which will contain at first the time the topic was created and when someone comments it will use the comment.created_at time and put it in the active field in the topics table, like any other forum system.
I found i similar question here
How to order by the date of the last comment and sort by last created otherwise?
But it wont work for me, im not sure why it wouldn't. And i also don't understand if i need to use counter_cache in this case or not. Im using a polymorphic association for my comments, so therefore im not sure how i would use counter_cache. It works fine in my topic table to copy the created_at time to the active field. But it wont work when i create a comment.
Error:
NoMethodError in CommentsController#create
undefined method `topic' for
Topic.rb
class Topic < ActiveRecord::Base
attr_accessible :body, :forum_id, :title
before_create :init_sort_column
belongs_to :user
belongs_to :forum
validates :forum_id, :body, :title, presence: true
has_many :comments, :as => :commentable
default_scope order: 'topics.created_at DESC'
private
def init_sort_column
self.active = self.created_at || Time.now
end
end
Comment.rb
class Comment < ActiveRecord::Base
attr_accessible :body, :commentable_id, :commentable_type, :user_id
belongs_to :user
belongs_to :commentable, :polymorphic => true
before_create :update_parent_sort_column
private
def update_parent_sort_column
self.topic.active = self.created_at if self.topic
end
end
Didn't realise you were using a polymorphic association. Use the following:
def update_parent_sort_column
commentable.active = created_at if commentable.is_a?(Topic)
commentable.save!
end
Should do the trick.
Let's say you have the following models:
class User < ActiveRecord::Base
has_many :comments, :as => :author
end
class Comment < ActiveRecord::Base
belongs_to :user
end
Let's say User has an attribute name, is there any way in Ruby/Rails to access it using the table name and column, similar to what you enter in a select or where query?
Something like:
Comment.includes(:author).first.send("users.name")
# or
Comment.first.send("comments.id")
Edit: What I'm trying to achieve is accessing a model object's attribute using a string. For simple cases I can just use object.send attribute_name but this does not work when accessing "nested" attributes such as Comment.author.name.
Basically I want to retrieve model attributes using the sql-like syntax used by ActiveRecord in the where() and select() methods, so for example:
c = Comment.first
c.select("users.name") # should return the same as c.author.name
Edit 2: Even more precisely, I want to solve the following problem:
obj = ANY_MODEL_OBJECT_HERE
# Extract the given columns from the object
columns = ["comments.id", "users.name"]
I don't really understand what you are trying to achieve. I see that you are using polymorphic associations, do you need to access comment.user.name while having has_many :comments, :as => :author in your User model?
For you polymorphic association, you should have
class Comment < ActiveRecord::Base
belongs_to :author, :polymorphic => true
end
And if you want to access comment.user.name, you can also have
class Comment < ActiveRecord::Base
belongs_to :author, :polymorphic => true
belongs_to :user
end
class User < ActiveRecord::Base
has_many :comments, :as => :author
has_many :comments
end
Please be more specific about your goal.
I think you're looking for a way to access the user from a comment.
Let #comment be the first comment:
#comment = Comment.first
To access the author, you just have to type #comment.user and If you need the name of that user you would do #comment.user.name. It's just OOP.
If you need the id of that comment, you would do #comment.id
Because user and id are just methods, you can call them like that:
comments.send('user').send('id')
Or, you can build your query anyway you like:
Comment.includes(:users).where("#{User::columns[1]} = ?", #some_name)
But it seems like you're not doing thinks really Rails Way. I guess you have your reasons.