I have a question regarding many-to-many relationships, specifically has_many :through associations.
All the tutorials I found just set you up with the models and migrations but leave you hanging when it comes to controllers.
What I want to do is to update the timestamps of the join table when an article that is already present is added again, so that it moves to the top of the "list". How do I do that?
This is what my create action looks like:
def create
#article = Article.find_or_create_by_url(params[:article])
if current_user.articles.find(#article)
# update the timestamps on the join table
# ie moved the old article to the top
flash[:notice] = "Successfully added article (again)."
redirect_to #article
else
#article.users << current_user
if #article.save
flash[:notice] = "Successfully added article."
redirect_to #article
else
render :action => 'new'
end
end
end
Thanks in advance!
Update:
#Ben Lee
Thanks for your answer, as I have a has_many through association my article model looks like this:
has_many :readinglist_items, :dependent => :destroy
has_many :users, :through => :readinglist_items
So I don't know if I can add a :touch => true to has_many as I just want to specific entry in the join table.
The point of the update in the create action is to move the article to the top (instead of adding it again) if a user adds an article it have already added in the past.
If I'm understanding you correctly, this isn't a controller issue, but a model issue. You can specify :touch => true on a belongs_to relationship. This will make it so that whenever the child is updated, the association's update_at is updated well.
So put something like this in your Article model:
belongs_to :whatever, :touch => true
Also, tangentially related: it's not clear from the code exactly what your code is doing, but it seems like maybe you are putting create and update functionality both in in the create method instead of splitting them up appropriately.
I solved it! (readinglist_item is the name of the join table):
def create
#article = Article.find_or_create_by_url(params[:article])
if current_user.articles.find(#article)
#article.readinglist_items.find_by_user_id(current_user.id).touch
flash[:notice] = "Successfully added article (again)."
redirect_to #article
else
#article.users << current_user
if #article.save
flash[:notice] = "Successfully added article."
redirect_to #article
else
render :action => 'new'
end
end
end
Related
Using: Rails 4.1.4, PostgreSQL 9.1.13
Hi. I'm have a simple problem, but for some reason I can't get it done. The picture is this:
Models
class Article < ActiveRecord::Base
belongs_to :user
has_many :votes
end
class User < ActiveRecord::Base
has_many :articles
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :article
belongs_to :user, scope: :hotel_id
validates_inclusion_of :value, in: 0..5
validates_uniqueness_of :user_id, :article_id
end
Idea
Each User can Vote for each Article but only once (to avoid multiple voting).
Vote model has a 'value' attribute that is the range 0..10.
ArticlesController except standard CRUD methods has action #showcase which must return 5 articles with the top votes rating from the DB and sort them in the descending order (and render the respective view).
So I understand that the proper way is to write the class method in the Article Model (smth. like "by_top_votes") and use it in the ArticlesController#showcase:
def showcase
#top_five_articles = Article.by_top_votes
end
The problem is that I can't write the proper query to the DB which will: 1)find articles, 2)find all votes of the each article, 3) sum all values of the respective article's votes, 4)sort them (this step I know how to do).
Thank you for reading and for the help.
P.S. Maybe my way to solve problem is almost wrong. If this so, please tell my the right one.
Ok, I've done it by myself. If anybody will stuck with the same problem, here is solution for it.
1. In Vote model summarize the vote's values:
def self.sum_value
sum(:value)
end
2. Add new attribute (and column) to Article - user_rating:integer.
3. In the Article model define two class methods:
# assign user_rating attribute with the sum of all votes values
def set_user_rating
user_rating = self.votes.sum_value
self.update_attribute(:user_rating, user_rating)
end
# get top 5 articles by user_rating value from db
def self.top_by_user_rating
Article.order(:user_rating).reverse_order.limit(5)
end
4. In the ArticlesController define showcase action:
def showcase
#top_articles = Article.top_by_user_rating
end
5. In the VotesController define create action:
def create
#article = Article.find(params[:article_id])
#vote = #article.votes.create(vote_params)
if #vote.save
#article.set_user_rating
redirect_to #article, notice: "Thanks for your vote"
else
.
end
end
It works and tests are passing.
Models:
A User has_one Ucellar
A Ucellar belongs_to User
I have confirmed from multiple sources that these are set up correctly. For posterity, here is the top portion of those two models.
class User < ActiveRecord::Base
has_many :authorizations
has_one :ucellar
validates :name, :email, :presence => true
This is actually the entire Ucellar model.
class Ucellar < ActiveRecord::Base
belongs_to :user
end
Ucellar has a column called user_id, which I know is necessary. The part of my application that creates a user uses the method create_with_oath. Below is the entire User class. Note the second line of the create method.
class User < ActiveRecord::Base
has_many :authorizations
has_one :ucellar
validates :name, :email, :presence => true
def create
#user = User.new(user_params)
#ucellar = #user.create_ucellar
end
def add_provider(auth_hash)
# Check if the provider already exists, so we don't add it twice unless authorizations.find_by_provider_and_uid(auth_hash["provider"], auth_hash["uid"])
Authorization.create :user => self, :provider => auth_hash["provider"], :uid => auth_hash["uid"]
end
end
def self.create_with_omniauth(auth)
user = User.create({:name => auth["info"]["name"], :email => auth["info"]["email"]})
end
private
def user_params
params.require(:user).permit(:name, :email)
end
end
EDIT:
Forgot to summarize the symptoms. On create, the user is in the db, with no exceptions thrown, and nothing to signify that anything went wrong. However, the related ucellar is never created. Per the documentation Here, the create method should create AND save the related ucellar.
It should create ucellar too.
Try to get the error messages after the creation by calling:
raise #user.errors.full_messages.to_sentence.inspect
I'm not sure why this wasn't working, but I ended up just moving this code out of the create action of the user controller, and putting it directly after an action that was creating a user. It solved my issue though. Thanks everyone for your help!
I am trying to create a polymorphic relationships and to show them.
Here my relationships
events
has_many :comments, :as => :commentable
users
has_many :comments, :as => :commentable
comments
belongs_to :comments, :polymorphic => true
comments has the following models
# commentable_id :integer
# commentable_type :string(255)
Now my goals would be show on event#index items related to the events but also to shows all comments.description according to each events sorta has follow
********************* *********************
Event.title Event.title
Event.description Event.description
{Comment.body} {Comment.body}
... ...
********************** **********************
and so on.
Here my controller
def index
#mosttop = Event.all[1..-1]
#loc = #mosttop.comments
end
If i do this however i get an undefined locations. I am wondering what i am doing wrong. Also, my routes is has follow
resources :events do
resources :comments do
end
Also at the moment I haven't create a relationship using build, but just when at the link event/1/comments and create it
the controller is has follow
def create
#comment = #commentable.comments.new(params[:comment])
...
end
private
def load_commentable
resource, id = request.path.split('/')[1,2]
#commentable = resource.singularize.classify.constantize.find(id)
end
end
I've setup a nested form in my rails 3.2.3 app, it's working fine, my models are:
class Recipe < ActiveRecord::Base
attr_accessible :title, :description, :excerpt, :date, :ingredient_lines_attributes
has_and_belongs_to_many :ingredient_lines
accepts_nested_attributes_for :ingredient_lines
end
and:
class IngredientLine < ActiveRecord::Base
attr_accessible :ingredient_id, :measurement_unit_id, :quantity
has_and_belongs_to_many :recipes
belongs_to :measurement_unit
belongs_to :ingredient
end
As above, a Recipe can have multiple IngredientLines and vice versa.
What I'm trying to avoid is record duplication on IngredienLine table.
For example imagine that for recipe_1 an IngredientLine with {"measurement_unit_id" => 1, "ingredient_id" => 1, "quantity" => 3.5} is associated, if for recipe_5 the IngredientLine child form is compiled by the user with the same values, I don't want a new record on IngredientLine table, but only a new association record in the join table ingredient_lines_recipes.
Note that currently I dont't have any IngredientLine controller as saving and updating IngredientLines is handled by nested form routines. Even my Recipe controller is plain and standard:
class RecipesController < ApplicationController
respond_to :html
def new
#recipe = Recipe.new
end
def create
#recipe = Recipe.new(params[:recipe])
flash[:notice] = 'Recipe saved.' if #recipe.save
respond_with(#recipe)
end
def destroy
#recipe = Recipe.find(params[:id])
#recipe.destroy
respond_with(:recipes)
end
def edit
respond_with(#recipe = Recipe.find(params[:id]))
end
def update
#recipe = Recipe.find(params[:id])
flash[:notice] = 'Recipe updated.' if #recipe.update_attributes(params[:recipe])
respond_with(#recipe)
end
end
My guess is that should be enough to override the standard create behavior for IngredientLine with find_or_create, but I don't know how to achieve it.
But there's another important point to take care, imagine the edit of a child form where some IngredientLines are present, if I add another IngredientLine, which is already stored in IngredientLine table, rails of course should not write anything on IngredientLine table, but should also distinguish between child records already associated to the parent, and the new child record for which needs to create the relation, writing a new record on the join table.
Thanks!
in Recipe model redefine method
def ingredient_lines_attributes=(attributes)
self.ingredient_lines << IngredientLine.where(attributes).first_or_initialize
end
Old question but I had the same problem. Forgot to add :id to white list with rails 4 strong_parameters.
For example:
widgets_controller.rb
def widget_params
params.require(:widget).permit(:name, :foos_attributes => [:id, :name, :_destroy],)
end
widget.rb
class Widget < ActiveRecord::Base
has_many :foos, dependent: :destroy
accepts_nested_attributes_for :foos, allow_destroy: true
end
foo.rb
class Foo < ActiveRecord::Base
belongs_to :widget
end
I have run into a similar situation and found inspiration in this answer. In short, I don't worry about the duplication of nested models until save time.
Translated to your example, I added autosave_associated_records_for_ingredient_lines to Recipe. It iterates through ingredient_lines and performs a find_or_create as your intuition said. If ingredient_lines are complex, Yuri's first_or_initialize approach may be cleaner.
I believe this has the behavior you're looking for: nested models are never duplicated, but editing one causes a new record rather than updating a shared one. There is the strong possibility of orphaned ingredient_lines but if that's a serious concern you could choose to update if that model has only one recipe with an id that matches the current one.
I have a "two middleman" model setup as shown below:
User
has_many :comments
has_many :ratings
Comment
belongs_to :user
belongs_to :movie
Rating
belongs_to :user
belongs_to :movie
Movie
has_many :comments
has_many :ratings
Whats the best way to get all Movies that a User is associated with (either commented on or rated)?
I'd like to be able to call User.get_movies(user_id) and get back an ActiveRecord::Relation object so that it's chainable (i.e. User.get_movies(user_id).limit(3).order(...)). This returns a regular old array, and I suspect I'm hitting the database way more than I need to be.
def self.get_movies(user_id)
user = self.where(:id => user_id).includes({:comments => :movie}, {:ratings => :movie})
movies = []
user.comments.each do |comment|
movies.push(comment.movie)
end
user.ratings.each do |rating|
movies.push(rating.movie)
end
movies.uniq!
end
def movies
Movie.includes(:ratings, :comments).where("`ratings`.user_id = ? OR `comments`.user_id = ?", self.id, self.id)
end
Untested, but I'm pretty sure using a joins instead of includes also works.