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.
Related
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
I have 3 pertinent models:
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
end
class Group < ActiveRecord::Base
has_and_belongs_to_many :users
has_many :galleries
end
class Gallery < ActiveRecord::Base
belongs_to :group
end
I want to be able to create users and galleries within a group so that only users who are members of the group can view the galleries that belong to that group. I also want users to be able to view galleries of other groups they belong to (hence the HABTM association).
I'm having difficulty in conceptualizing how this works with controllers, and perhaps I'm over thinking the problem. If I create a Group, and then I go to create a user, what is the best way to go about adding the current group_id to the user model? Same thing goes for the gallery model...
Does that make sense?
Let me know if I need to clarify or add code samples.
Thank you very much for your help.
EDIT: Clarification
I definitely didn't make any sense in my initial question, but I did manage to find the answer, with help from a friend.
What I ended up doing is passing the group_id to the form via the params hash like so:
<%= link_to "Add User", new_admin_user_path(:group_id => #group.id) %>
<%= link_to "Add Gallery", new_gallery_path(:group_id => #group.id) %>
Then using a hidden field in my form, assigning the group_id to the "group_id" hidden field:
<%= hidden_field_tag :group_id, params[:group_id] %>
And, finally, in my create methods, adding these lines before the save assigns the group_id perfectly:
# Gallery only has one group
#gallery.group_id = params[:group_id]
# Users can belong to many groups
#user.groups << Group.find(params[:group_id])
I'll still need to sit down and wrap my head around the answers you both provided. Thank you very much for taking the time to help me out. I really appreciate it.
When you are using find method from your controller you can make it like this:
Gallery.find :all, :joins => "INNER JOIN groups ON groups.gallery_id = galleries.id INNER JOIN users ON users.group_id = groups.id", :conditions => "users.id = #{#your_current_user_id}"
It must find all galleries of groups which the user belongs.
I would not define this in the controller as Sebes suggests, but rather in the User model.
Adapting his idea:
def galleries
Gallery.joins(:groups => :users).where("users.id = ?", self.id)
end
Then to get a collection of the galleries for the current_user object:
current_user.galleries
I'm trying to model this inheritance for a simple blog system
Blog has many Entries, but they may be different in their nature. I don't want to model the Blog table, my concern is about the entries:
simplest entry is an Article that has title and text
Quote, however, does not have a title and has short text
Media has a url and a comment...
etc...
What is a proper way to model this with Ruby on Rails? That is
Should I use ActiverRecord for this or switch to DataMapper?
I would like to avoid the "one big table" approach with lots of empty cells
When I split the data into Entry + PostData, QuoteData etc can I have belongs_to :entry in these Datas without having has_one ??? in the Entry class? That would be standard way to do it in sql and entry.post_data may be resolved by the entry_id in the postdata table.
EDIT: I don't want to model the Blog table, I can do that, my concern is about the entries and how would the inheritance be mapped to the table(s).
I've come across this data problem several times and have tried a few different strategies. I think the one I'm a biggest fan of, is the STI approach as mentioned by cicloon. Make sure you have a type column on your entry table.
class Blog < ActiveRecord::Base
# this is your generic association that would return all types of entries
has_many :entries
# you can also add other associations specific to each type.
# through STI, rails is aware that a media_entry is in fact an Entry
# and will do most of the work for you. These will automatically do what cicloon.
# did manually via his methods.
has_many :articles
has_many :quotes
has_many :media
end
class Entry < ActiveRecord::Base
end
class Article < Entry
has_one :article_data
end
class Quote < Entry
has_one :quote_data
end
class Media < Entry
has_one :media_data
end
class ArticleData < ActiveRecord::Base
belongs_to :article # smart enough to know this is actually an entry
end
class QuoteData < ActiveRecord::Base
belongs_to :quote
end
class MediaData < ActiveRecord::Base
belongs_to :media
end
The thing I like about this approach, is you can keep the generic Entry data in the entry model. Abstract out any of the sub-entry type data into their own data tables, and have a has_one association to them, resulting in no extra columns on your entries table. It also works very well for when you're doing your views:
app/views/articles/_article.html.erb
app/views/quotes/_quote.html.erb
app/views/media/_media.html.erb # may be medium here....
and from your views you can do either:
<%= render #blog.entries %> <!-- this will automatically render the appropriate view partial -->
or have more control:
<%= render #blog.quotes %>
<%= render #blog.articles %>
You can find a pretty generic way of generating forms as well, I usually render the generic entry fields in an entries/_form.html.erb partial. Inside that partial, I also have a
<%= form_for #entry do |f| %>
<%= render :partial => "#{f.object.class.name.tableize}/#{f.object.class.name.underscore}_form", :object => f %>
<% end %>
type render for the sub form data. The sub forms in turn can use accepts_nested_attributes_for + fields_for to get the data passed through properly.
The only pain I have with this approach, is how to handle the controllers and route helpers. Since each entry is of its own type, you'll either have to create custom controllers / routes for each type (you may want this...) or make a generic one. If you take the generic approach, two things to remember.
1) You can't set a :type field through update attributes, your controller will have to instantiate the appropriate Article.new to save it (you may use a factory here).
2) You'll have to use the becomes() method (#article.becomes(Entry)) to work with the entry as an Entry and not a subclass.
Hope this helps.
Warning, I've actually used Media as a model name in the past. In my case it resulted in a table called medias in rails 2.3.x however in rails 3, it wanted my model to be named Medium and my table media. You may have to add a custom Inflection on this naming, though I'm not sure.
You can handle this easily using ActiveRecord STI. It requires you to have a type field in your Entries table. This way you can define your models like this:
def Blog > ActiveRecord::Base
has_many :entries
def articles
entries.where('Type =', 'Article')
end
def quotes
entries.where('Type =', 'Quote')
end
def medias
entries.where('Type =', 'Media')
end
end
def Entry > ActiveRecord::Base
belongs_to :blog
end
def Article > Entry
end
def Quote > Entry
end
def Media > Entry
end
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])
Hi: I'm struggling with a rails query. Finally, I want to make a JSON response with the following contents: A listing of all countries which have at least one company, associated through "Address" and a count of how of how many companies reside in a country.
Here is my simple model structure:
class Country < ActiveRecord::Base
has_many :addresses
end
class Address < ActiveRecord::Base
belongs_to :country
belongs_to :company
end
class Company < ActiveRecord::Base
has_one :address
end
What's the most elegant way to solve this with Rails?
You need to overload the to_json method :
class Country < ActiveRecord::Base
has_many :addresses
def to_json
# format your country as json with all the elements you need
end
end
Then in your controller do something like that:
countries = Country.find(:all).select{|c| c.companies.size > 0 }
json = countries.collect{|c| c.to_json }
You'll have to use has_many throught to get the companies from a country. Documentation here: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Sorry I don't have time for a more precise solution, but hopefully these pointers will help you. If you have specific problems, feel free to either comment or open a new question.
You might want to add a counter cache to Address
belongs_to :country, :counter_cache => :companies_count
It stores the number of companies in the country model, which saves you from the N+1 query issue.
Alternatively you could avoid it also by issuing a single query like this:
Country.find(:all, :select => 'countries.*, count(addresses.id) as companies_count', :joins => 'LEFT JOIN addresses ON addresses.country_id = countries.id', :group => 'countries.id')
This way all of the returned countries will have field companies_count containing the number of companies for that country.