Calling and iterating over ActiveRecord::Relation objects - ruby-on-rails-3

I have the following AR associations:
Category :has_many :posts
Category :has_many :authors, :through => :posts
Post :belongs_to :author
Post :has_many :comments
In my view, my goal is to list comments by posts, and posts by author for a particular category. My attempt at a query looks like the following, using includes for eager loading.
category = Category.first
category.authors.includes(:posts => comments)
I'd like my view list out something like:
author1
post1
comment1
post2
comment2
author2
post3
comment3
However, when I try to iterate over the ActiveRecord::Relation object, I've noticed that the first level of authors has duplicates and with size equal to the size of posts. Is there a way for me to get unduplicated authors at the first level, and then be able to iterate through associated posts and their comments?
An alternative I thought of was to iterate through the ActiveRecord::Relation object and rewrite it as a hash, but first I wanted to see if there was an AR way of doing this.

One way is to set the :unique property on the relation.
Category :has_many :authors, :through => :posts, :uniq => true
So, category.authors will always return the set of authors.
Check out: rails guides - activerecord

Related

putting a condition on an includes

I have the following relationships:
Category has_many :posts
Post has_many :comments
Post has_many :commenters, :through => :comments
I have the following eager load, giving me posts, comments and commenters (note that I need all 3, and hence the includes as opposed to joins)
category.posts.includes(:comments, :commenters)
However, I'd like to limit comments (and if possible commenters) to only those created in the past two weeks while still returning the same set of posts. Initially I thought I could specify a condition on the includes:
category.posts.includes(:comments, :commenters).where("comments.created_at > ?", 2.weeks.ago)
But found that this returns only the posts that meet the condition. I'm thinking that I may need to do something like performing a subquery on comments and then performing a join. Is there an easy way to do this with AR of would I be better off doing this with sql?
Finally managed to figure this out from reading this page:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
I simply needed to create an association in my Post model like:
Post has_many :recent_comments, :class_name = 'Comment', :conditions => ["created_at > ?", 2.weeks.ago]
Then I could do the following to get the desired ActiveRecord::Association object:
category.posts.includes(:recent_comments => :commenters)
There was also a suggestion of doing this by using a scope on a model. However, I read somewhere (I think it was on SO) that scopes are on their way out and that ARel has taken their place so I decided to do this without scopes.
Try :
category.posts.all(:includes => {:comments =>:commenters}, :conditions => ["comments.created_at = ? AND commenters.created_at = ?", 2.weeks.ago, 2.weeks.ago]

How to find posts with multiple tags

I have a very simple tag model on Rails with postgresql:
class Tag < ActiveRecord::Base
has_many :taggings
has_many :posts, :through => :taggings,
:source => :tagged, :source_type => 'Post'
end
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :tagged, :polymorphic => true
end
class Post < ActiveRecord::Base
has_many :taggings, :as => :tagged
has_many :tags, :through => :taggings
end
Is there an easy way to find all posts that have 2 more specified tags? For example lets say there are tags of "style", "men", "women", "sale". I'd like to create a generic find statement that takes in an array of tags. So if the input is ["style"] then it should return all posts with that tag (easy) or if the input is ["style", "men"] then it should return all posts with the tag "style" AND "men".
Is there an easy way to find all posts that have 2 more specified tags?
For example lets say there are tags of "style", "men", "women", "sale"
The classic way is to use a pivot table : posts <-> posts_tags <-> tags
You could encode your tags like this, though, because it is the easiest way (maintains integrity, foreign keys, gives you an easy to scan list of tags, etc).
This way has decent performance for a small number of posts and a small number of tags, but is cumbersome to query (you'll need some aggregation, INTERSECT, or 1 JOIN per tag) and extremely slow if the tags are not very selective.
Obviously for the kind of searches you want to perform, this sucks. So you got 2 choices :
1- Materialize the list of tag ids of a post inside an INTEGER[] column in your posts table, put a gist (or gin) index on it, and use the "integer array is contained" operator, which is indexed, extremely fast, and trivial to query.
2- just put your tags as text and throw a full text index on them
Both are extremely fast with an advantage to the integer array.
I could write a really bad SQL here what would do JOINS and GROUP BY's but this is rails so you can do better, first your Post model should be defined like this:
class Post < ActiveRecord::Base
has_many :taggings, :as => :tagged, :couter_cache => true
has_many :tags, :through => :taggings
end
And you'll need a migration to add the taggings_count column to your posts table:
add_column :posts, :taggings_count, :integer, :default => 0
add_index :posts, :taggings_count
And with that whenever a Tagging is created for Post it's going to increment the taggings_count value and you can use it in your queries to efficiently find posts with two or more tags:
Post.all( :conditions => [ 'taggings_count >= ?' 2] )

Creating a news feed in Rails 3

I want to create an activity feed from recent article and comments in my rails app. They are two different types of activerecord (their table structures are different).
Ideally I would be able to create a mixed array of articles and comments and then show them in reverse chronological order.
So, I can figure out how to get an array of both articles and comments and then merge them together and sort by created_at, but I'm pretty sure that won't work as soon as I start using pagination as well.
Is there any way to create a scope like thing that will create a mixed array?
One of the other problems for me, is that it could be all articles and it could be all comments or some combination in between. So I can't just say I'll take the 15 last articles and the 15 last comments.
Any ideas on how to solve this?
When I've done this before I've managed it by having a denormalised UserActivity model or similar with a belongs_to polymorphic association to an ActivitySource - which can be any of the types of content that you want to display (posts, comments, up votes, likes, whatever...).
Then when any of the entities to be displayed are created, you have an Observer that fires and creates a row in the UserActivity table with a link to the record.
Then to display the list, you just query on UserActivity ordering by created_at descending, and then navigate through the polymorphic activity_source association to get the content data. You'll then need some smarts in your view templates to render comments and posts and whatever else differently though.
E.g. something like...
user_activity.rb:
class UserActivity < ActiveRecord::Base
belongs_to :activity_source, :polymorphic => true
# awesomeness continues here...
end
comment.rb (post/whatever)
class Comment < ActiveRecord::Base
# comment awesomeness here...
end
activity_source_observer.rb
class ActivitySourceObserver < ActiveRecord::Observer
observe :comment, :post
def after_create(activity_source)
UserActivity.create!(
:user => activity_source.user,
:activity_source_id => activity_source.id,
:activity_source_type => activity_source.class.to_s,
:created_at => activity_source.created_at,
:updated_at => activity_source.updated_at)
end
def before_destroy(activity_source)
UserActivity.destroy_all(:activity_source_id => activity_source.id)
end
end
Take a look at this railscast.
Then you can paginate 15 articles and in app/views/articles/index you can do something like this:
- #articles.each do |article|
%tr
%td= article.body
%tr
%td= nested_comments article.comment.descendants.arrange(:order => :created_at, :limit => 15)
This assumes the following relations:
#app/models/article.rb
has_one :comment # dummy root comment
#app/models/comment.rb
belongs_to :article
has_ancestry
And you add comments to an article as follows:
root_comment = #article.build_comment
root_comment.save
new_comment = root_comment.children.new
# add reply to new_comment
new_reply = new_comment.children.new
And so forth.

Post belongs to Topic and User. How do I create a Post with :topic_id and :user_id without exposing either with attr_accessible?

I'm making a conventional forum to learn/practice Rails. As you're familiar with, Users can create Topics and Posts. Topics and Posts belong to the User that created them, and Posts belong to the Topic they were posted in. Note: Post is a nested resource of Topic.
User Model
has_many :topics
has_many :posts
Topic Model
has_many :posts
belongs_to :user
Post Model
belongs_to :user
belongs_to :topic
So, when a User creates a new Post, the Post needs a user_id and a topic_id.
I know about scoping associations with:
#user.posts.create(:title => "My Topic.")
#topic.posts.create(:content => "My Post.")
But those examples only set user_id and topic_id respectively, not both.
My Question
How can I do something like this:
#topic.posts.create(:content => "This is Dan's post", :user_id => #dan.id)
Without having to expose user_id in the Post model via attr_accessible :user_id?
In other words, I don't want to have to explicitly define :user_id.
I tried things like:
dans_post = #user.posts.new(:content => "the content of my post")
#topic.posts.create(dans_post)
to no avail.
Use build for building associations, instead of new, as it will define the foreign key correctly. To solve your problem, use merge to merge in the user to the parameters for the post:
#topic.posts.build(params[:post].merge(:user => current_user))

rails: filter categories where there are at least one article

I have articles and categories in a n:m relation:
I looking for a find statement on the Category Model so that I can get all categories witch consist at least one article.
Should be easy, but I didn't find a efficient solution, without searching retrieving all the articles.
Thanks,
Maechi
I think that counter cache is your friend here. Take a look here.
You can add the counter cache to the categories table and in the CategoryArticles you do like
class CategoryArticles
belongs_to :article
belongs_to :category, :counter_cache => true
end
So you can find your Category with
#categories = Category.find(:all, :conditions => ["category_articles_count > ?", 0])