rails associations - How would you represent this relationship? - ruby-on-rails

I am trying to figure out a best way to represent the following relationship.
Newspaper has_many Articles
Newspaper has_many Subscribers
Subscribers are allowed to save the articles for their personal page.
Two Questions:
1) How would the relationship look like in rails? How would the action 'save' look like?
The following using has_many does not seem right to me:
class ArticleController < ApplicationController
def save
a = Article.find(101)
#user.saved_articles << a
end
end
2) Do I need a join table Saved_Articles that looked like this?
Saved_Articles
----------------
user_id, article_id

What you're describing is a many-to-many relationship. In terms of Rails associations, this is one way of defining it:
class Newspaper
has_many :articles
has_many :subscribers
end
class Subscriber
belongs_to :newspaper
has_and_belongs_to_many :articles
end
class Article
belongs_to :newspaper
has_and_belongs_to_many :subscribers
end
By using has_and_belongs_to_many, you will need a join table, but it would need to be called articles_subscribers and would have subscriber_id and article_id fields:
articles_subscribers
--------------------
article_id
subscriber_id
Your save action would then look something like the following, assuming #user was an instance of Subscriber:
class ArticlesController < ApplicationController
def save
#user.articles << Article.find(params[:id])
# handle the response - render or redirect
end
end
See http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_and_belongs_to_many for more information on has_and_belongs_to_many

Answer QUestion #1:
in models/newspaper.rb:
has_many :articles
has_many :subscribers
in controllers/newspapers_controller.rb
def create
#newspaper = NewsPaper.new(params[:newspaper])
if #newspaper.save
redirect_to
else
render :new
end
end

Answer for Question #2:
You don't need a join table for this. Just 3 separate tables (newspapers, articles, and subscribers) would be fine. The foreign keys for newspaper_id would be in articles and subscribers.

Related

Tags per post per user, the user isn't listed.

My main models are that I have users and I have recipes.
I'm trying to implement a tagging structure such that each user can tag a recipe with individual tags. So when viewing a recipe, they would only see tags that they themselves have added.
I created two models hashtags, and hashtagging that is the join table. It is set up as so:
models/hashtags.rb
class Hashtag < ActiveRecord::Base
has_many :hashtaggings
has_many :recipes, through: :hashtaggings
has_many :users, through: :hashtaggings
end
models/hashtagging.rb
class Hashtagging < ActiveRecord::Base
belongs_to :user
belongs_to :hashtag
belongs_to :recipe
end
models/recipe.rb
class Recipe < ActiveRecord::Base
...
has_and_belongs_to_many :users
has_many :hashtaggings
has_many :hashtags, through: :hashtaggings
....
def all_hashtags=(name)
self.hashtags = name.split(",").map do |name|
Hashtag.where(name: name.strip).first_or_create!
end
end
def all_hashtags
self.hashtags.map(&:name).join(",")
end
end
class User < ActiveRecord::Base
...
has_many :hashtaggings
has_many :hashtags, through: :hashtaggings
...
end
This works great for creating the hash tags however I'm at a loss for how to incorporate the user aspect of it. How when assigning the tags can I also assign the current user to those tags and then just return those?
There are two steps, creation and display...
Creation
This one is going to be tricky, because you can't simply do something like...
#recipe.hashtags.create(name: "Tasty as heck!")
...because neither the recipe, nor the hashtag, knows anything about the user. It's a two-step process.
class RecipeHashtagsController
def create
current_hashtag = Hashtag.find_or_create_by(name: "Scrumptious!")
current_recipe = Recipe.find(params[:recipe_id])
hashtagging = Hashtagging.find_or_create_by(hashtag: current_hashtag, user: current_user, recipe: current_recipe)
# redirect_to somewhere_else...
end
end
A few things I did there:
I'm using find_or_create_by since I'm assuming you don't want either duplicate hashtags or duplicate hashtaggings. You could also just create.
I'm assuming you have some kind of current_user method in ApplicationController or through a gem like Devise.
I'm assuming you have a controller like RecipeHashtags, and a nested resource that matches and will provide an id from the route. I recommend nesting here since you aren't simply creating a hashtag, but you are creating a hashtag within the specific context of a recipe.
Displaying
This gets tricky, because you want to display recipe.hashtags but with a condition on the join table hashtaggings. This is not super straightforward.
What I'm thinking is you might want to be able to do something like...
#recipe.hashtags_for_user(current_user)
...which could be in the form of a method on Recipe.
class Recipe
def hashtags_for_user(user)
Hashtags.joins(:hashtaggings).where(hashtaggings: { user_id: user.id, recipe_id: self.id })
end
end
You can read more about the hash inside the .where call in the Active Record Querying Rails Guide (check out section 12.3).
Edit: The Controller
I recommend creating RecipeHashtags as a nested route pointing to separate controller, since the creation of a hashtag is dependent on which recipe it's being created for.
routes.rb
resources :recipes do
resources :hashtags, only: [:create]
end
...which will show something like the following when you do rake routes in the terminal...
POST /recipes/:recipe_id/hashtags(.:format) recipe_hashtags#create
Note: I'm assuming you have a resource for resource for recipes. If you don't, this may duplicate some routes and have other, unintended results.
The default behavior for Rails is to assume you've got a controller like recipe_hashtags_controller based on how you defined your resources. You can always override this if you like.

multiple habtm relationships on one model

I am trying to figure out the best way to accomplish my problem. I've got a pages table, and a user_types table. I am trying to specify multiple user types on a page. They will act as permission groups. I need to do this twice however. Once for read permissions, and once for edit permissions. Here is an example:
Home page has 3 user types that can read it - admin, super admin, public
It has 2 user types that can edit it - admin, super admin
I have one user_types table:
admin
super admin
public
etc
I have created two mapping tables (one for read, and one for edit):
pages_user_read_types
pages_user_edit_types
they both have page_id, and user_type_id
Is there a better way to accomplish this? If this is the best way, I need help figuring out the relationships for the models. I have this for one relationship
has_and_belongs_to_many :user_types, :join_table => :pages_user_read_types
How do i specify two relationships for seperate fields?
Thanks
The HABTM relationship in Rails has seemed to fall out of favor over the last couple of years with Rails developers to the has_many :through relationship. The only time you should use HABTM is when you have no need for any additional information about the relationship between two models. In your case, you are trying to emulate this by creating two HABTM relationships when you could effectively accomplish by having a join model with a editable attribute.
In code, it would look something like this:
class Page < ActiveRecord::Base
has_many :page_permissions
has_many :user_types, :through => page_permissions
def editable_user_types
page_permissions.includes(:user_types).where(:editable => true).map(&:user_type)
end
def read_only_user_types
page_permissions.includes(:user_types).where(:editable => false).map(&:user_type)
end
end
class PagePermission < ActiveRecord::Base
belongs_to :page
belongs_to :user_type
# When you create this model, you should have a boolean attribute for editable
end
class UserType < ActiveRecord::Base
has_many :page_permissions
has_many :pages, :through => :page_permissions
end
I think following this approach will allow you to consolidate to one join table which will be better in the future if you need to add additional attributes to the relationship (PagePermission) between Page and UserType.
At the very least, you probably want to add a Permission model. If it ever gets more complicated than what you've described, I would also recommend using CanCan.
class Permission < ActiveRecord::Base
#table is id, page_id, user_type_id, and permission_type (string).
belongs_to :page
belongs_to :user_type
end
In your controller, you can construct a filter chain like this:
class PagesController < ApplicationController
before_filter :load_page
before_filter :authorize_view!, only: [ :show ]
before_filter :authorize_edit!, only: [ :edit ]
def show
end
def edit
end
private
def load_page
#page = Page.find(params[:id])
end
def authorize_view!
if !#page.permissions.where(user_type_id: current_user.user_type_id, permission_type: "view").exists?
flash[:notice] = "You do not have permission to view that page."
redirect to root_path
end
end
def authorize_edit!
if !#page.permissions.where(user_type_id: current_user.user_type_id, permission_type: "edit").exists?
flash[:notice] = "You do not have permission to edit that page."
redirect to root_path
end
end
end
(This assumes you have a current_user method in your app).

Can I access the id of the parent object inside a named_scope when fetching associated objects with the 'others' method?

Let's say you have two models: articles and comments.
class Article < ActiveRecord::Base
has_many :comments
end
You know you can fetch associated comments to an article like this:
article = Article.first
article.comments # => SELECT * FROM "comments" WHERE ("comments".article_id = 123)
Is there a way to explicitly access the article_id (123) within a named_scope?
I need this for a complex named_scope that joins another table. Basically the named_scope will depend on to be called from the associated parent object to make sense (article.comments.my_named_scope and not Comments.my_named_scope).
I don't want to pass the id as a parameter for the named_scope. So, instead of passing the article_id to the named scope with ... lambda { |article| ...} and access the id with "... #{article.id} ...", I want to somehow access this article_id that the others method uses, which I get from the has_many association.
Sounds like what you're actually after is an association extension:
http://guides.rubyonrails.org/association_basics.html#association-extensions
In particular, proxy_owner, which will be the #article in question
eg:
class Article < ActiveRecord::Base
has_many :posts do
def sample_extension
puts "Proxy Owner #{proxy_owner}"
end
end
end
#article.posts.sample_extension
Been struggling with the same issue. You can try this, which is a more elegant than using association extensions:
class Article < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
def self.get_article_id
self.new.article_id
end
end
#article = Article.new
#article.posts.get_article_id
Within class methods for Post, you can now just use get_article_id anywhere you need the ID of the parent article. With proxy associations, I wasn't able to do that.
I like #ajkochanowicz's solution but looks like there's a DB hit involved there (Rails 3.2.x), so just a heads up, not really ideal considering the fact that you already have the parent object on hand somewhere.
For Rails 4 and above
The newer way to do it in Rails4+ is:
class Article < ActiveRecord::Base
has_many :comments do
def my_named_scope
puts "Scope Owner = #{#association.owner}"
end
end
end
article = #article.comments.my_named_scope
Inside the scope my_named_scope, #association.owner returns the Article object that .comments was called on. Hence the article returned by the code above is same as the #article object.
Alternative method
If you don't want to use extensions and would rather avoid the "create a new object and get id from there" method (as described by Chanpory's answer), here is how to do it:
class Article < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
def self.get_article_id
Comment.scope_attributes["article_id"] # scope_attributes returns a hash of all the attributes inherited from the owner of this scope
end
end
#article = Article.find(10)
#article.comments.get_article_id # returns 10

How to associate a new model with existing models using has_and_belongs_to_many

I have two models with a many to many relationship using has_and_belongs_to_many. Like so:
class Competition < ActiveRecord::Base
has_and_belongs_to_many :teams
accepts_nested_attributes_for :teams
end
class Team < ActiveRecord::Base
has_and_belongs_to_many :competitions
accepts_nested_attributes_for :competitions
end
If we assume that I have already created several Competitions in the database, when I create a new Team, I would like to use a nested form to associate the new Team with any relevant Competitions.
It's at this point onwards that I really do need help (have been stuck on this for hours!) and I think my existing code has already gone about this the wrong way, but I'll show it just in case:
class TeamsController < ApplicationController
def new
#team = Team.new
#competitions.all
#competitions.size.times {#team.competitions.build}
end
def create
#team = Team.new params[:team]
if #team.save
# .. usual if logic on save
end
end
end
And the view... this is where I'm really stuck so I won't both posting my efforts so far. What I'd like it a list of checkboxes for each competition so that the user can just select which Competitions are appropriate, and leave unchecked those that aren't.
I'm really stuck with this one so appreciate any pointing in the right direction you can provide :)
The has_and_belongs_to_many method of joining models together is deprecated in favor of the new has_many ... :through approach. It is very difficult to manage the data stored in a has_and_belongs_to_many relationship, as there are no default methods provided by Rails, but the :through method is a first-class model and can be manipulated as such.
As it relates to your problem, you may want to solve it like this:
class Competition < ActiveRecord::Base
has_many :participating_teams
has_many :teams,
:through => :participating_teams,
:source => :team
end
class Team < ActiveRecord::Base
has_many :participating_teams
has_many :competitions,
:through => :participating_teams,
:source => :competition
end
class ParticipatingTeam < ActiveRecord::Base
belongs_to :competition
belongs_to :team
end
When it comes to creating the teams themselves, you should structure your form so that one of the parameters you receive is sent as an array. Typically this is done by specifying all the check-box fields to be the same name, such as 'competitions[]' and then set the value for each check-box to be the ID of the competition. Then the controller would look something like this:
class TeamsController < ApplicationController
before_filter :build_team, :only => [ :new, :create ]
def new
#competitions = Competitions.all
end
def create
#team.save!
# .. usual if logic on save
rescue ActiveRecord::RecordInvalid
new
render(:action => 'new')
end
protected
def build_team
# Set default empty hash if this is a new call, or a create call
# with missing params.
params[:team] ||= { }
# NOTE: HashWithIndifferentAccess requires keys to be deleted by String
# name not Symbol.
competition_ids = params[:team].delete('competitions')
#team = Team.new(params[:team])
#team.competitions = Competition.find_all_by_id(competition_ids)
end
end
Setting the status of checked or unchecked for each element in your check-box listing is done by something like:
checked = #team.competitions.include?(competition)
Where 'competition' is the one being iterated over.
You can easily add and remove items from your competitions listing, or simply re-assign the whole list and Rails will figure out the new relationships based on it. Your update method would not look that different from the new method, except that you'd be using update_attributes instead of new.

Pull objects from an association into an array in Rails

A user has_many :donations, a project has_many :donations, and a donation belongs_to :user and belongs_to :project.
I'm looking for a sensible way to extract the projects associated with a user (through donations) into an array.
I'm currently doing:
def index
#user = User.find params[:user_id]
#projects = []
#user.donations.each do |donation|
#projects << donation.project
end
end
I feel like I'm missing something obvious, as this seems lame. Is there a better way to do this?
Edit
I accidentally simplified this too far. A user can also be associated with a project through other models, so #projects = #user.projects isn't going to do what I need it to.
class User < AR::Base
has_many :donations
has_many :projects, :through => :donations
…
end
#user.projects
should work.
For gathering many association collections see my previous answer. You will need to adapt it to use the through associations (just treat them as normal has_masnys), but the same applies.

Resources