Rails polymorphic has_many through association - form broken - ruby-on-rails

In my Rails 5.1 app I am trying to create a tagging system from scratch.
I want Tags to be a polymorphic has_many :through association so that I can tag multiple models.
Currently I'm able to create a Tag (and the associated Tagging) in the console by doing: Note.last.tags.create(name: "example") which generates the correct SQL:
Note Load (0.2ms) SELECT "notes".* FROM "notes" ORDER BY "notes"."id" DESC LIMIT $1 [["LIMIT", 1]]
(0.2ms) BEGIN
SQL (0.4ms) INSERT INTO "tags" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "example"], ["created_at", "2017-10-21 14:41:43.961516"], ["updated_at", "2017-10-21 14:41:43.961516"]]
Note Load (0.3ms) SELECT "notes".* FROM "notes" WHERE "notes"."id" = $1 LIMIT $2 [["id", 4], ["LIMIT", 1]]
SQL (0.4ms) INSERT INTO "taggings" ("created_at", "updated_at", "tag_id", "taggable_id", "taggable_type") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["created_at", "2017-10-21 14:41:43.978286"], ["updated_at", "2017-10-21 14:41:43.978286"], ["tag_id", 9], ["taggable_id", 4], ["taggable_type", "Note"]]
But when trying to create a Tag and its associations through my form it doesn't work. I can create the Tag but no Tagging.
controllers/notes/tags_controller.rb
class Notes::TagsController < TagsController
before_action :set_taggable
private
def set_taggable
#taggable = Note.find(params[:note_id])
end
end
controllers/tags_controller.rb
class TagsController < ApplicationController
before_action :authenticate_user!
def create
#tag = #taggable.tags.new(tag_params)
#tag.user_id = current_user.id
if #tag.save
redirect_to #taggable, success: "New tag created."
else
render :new
end
end
private
def tag_params
params.require(:tag).permit(:name)
end
end
routes.rb
...
resources :notes, except: [:index] do
resources :tags, module: :notes
end
...
.
class Note < ApplicationRecord
belongs_to :notable, polymorphic: true
has_many :taggings, as: :taggable
has_many :tags, through: :taggings
end
class Tag < ApplicationRecord
has_many :taggings
has_many :taggables, through: :taggings
end
class Tagging < ApplicationRecord
belongs_to :tag
belongs_to :taggable, polymorphic: true
end
notes/show.html.erb
<p><%= #note.body %></p>
<%= render partial: 'tags/tags', locals: { taggable: #note } %>
<%= render partial: 'tags/form', locals: { taggable: #note } %>
tags/form.html.erb
<%= simple_form_for [taggable, Tag.new] do |f| %>
<%= f.input :name %>
<%= f.submit %>
<% end %>

The error might be that the Tagging is not getting saved due to the :tag association being required by default.
Try:
class Tagging < ApplicationRecord
belongs_to :tag, required: false
belongs_to :taggable, polymorphic: true
end

Your approach is fundamentially flawed in that it will create duplicates of each tag instead of creating a join record. It also adds unessicary complication in that you have to create nested controllers for each taggable resource.
The fact that this does not fail a uniqueness validation for tags.name shows a shortcoming in your application - you should have a unique index in the DB and a validation in the model to avoid duplicates.
This would be a perfectly fine approach for something like comments where each created record should be unique but is not for this case where you're linking to an indirect association.
To assign existing tags to a record you can use a select or checkboxes to pass an array of ids:
<%= form_for(#note) do |f| %>
# ...
<%= f.collection_checkboxes(:tags_ids, Tag.all, :id, :name) %>
<% end %>
To create new tags you can use nested attributes or use ajax to send a POST request to /tags and update the view so that the tag ends up in the list of checkboxes.

Related

Rails 6: Can't delete nested model. Random Insert statement

I using Rails 6 with Postgres and having issues deleting a nested model.
A random insert statement gets generated after the association has been deleted.
Let me explain my set up.
Migrations
class CreateEntries < ActiveRecord::Migration[6.0]
def change
create_table :entries do |t|
t.string :name
t.timestamps
end
end
end
class Cards < ActiveRecord::Migration[6.0]
def change
create_table :cards do |t|
t.string :card_number
t.belongs_to :entry, null: true, foreign_key: true
t.timestamps
end
end
end
Models
class Entry < ApplicationRecord
has_one :card, dependent: :destroy
accepts_nested_attributes_for :card, allow_destroy: true
end
class Card < ApplicationRecord
belongs_to :entry
end
Controller
class EntriesController < ApplicationController
before_action :set_entry
def update
#entry.update(entry_params)
end
def set_entry
#entry = Entry.find(params[:id])
end
def entry_params
params.require(:entry).permit(:name,
card_attributes: [:id, :card_number, :_destroy]
)
end
end
Request Params
Parameters: {"authenticity_token"=>"CQ...Ucw==", "entry"=>{"card_attributes"=>{"_destroy"=>"true"}}, "id"=>"1"}
These are the logs
(0.2ms) BEGIN
ConcessionCard Load (0.2ms) SELECT "cards".* FROM "cards" WHERE "cards"."entry_id" = $1 LIMIT $2 [["entry_id", 1], ["LIMIT", 1]]
Card Destroy (0.4ms) DELETE FROM "cards" WHERE "cards"."id" = $1 [["id", 2]]
Card Create (0.6ms) INSERT INTO "cards" ("entry_id", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["entry_id", 1], ["created_at", "2019-09-06 13:50:41.100718"], ["updated_at", "2019-09-06 13:50:41.100718"]]
(0.3ms) COMMIT
Why is insert being generated after the delete call? It's not even a rollback.
Note: I have tried both null:true and null:false in the Cards belongs_to migration. I also tried setting optional:true in the belongs_to :entry statement in the Card model
Unless you include an id in card_attributes then Rails sees this as a new record, so it just replaces the has_one with a newly created Card for you (which because of your dependent: :destroy option deletes the existing associated Card).
Best to use a form.fields_for :card block in your form partial/view, which will automatically add the hidden id tag for an existing Card.

Has_many association mapped to different model

I want users to be able to select the languages they speak. I have setup the associations, the table attributes and the part of the form. When I select a language and submit the form I go to the rails console and do a u.languages but I get an empty array back: => []
Here are the logs when I submit the form:
Started POST "/update_user_via_user" for 127.0.0.1 at 2016-03-18 13:26:03 +0200
ActiveRecord::SchemaMigration Load (0.4ms) SELECT "schema_migrations".* FROM "schema_migrations"
Processing by UsersController#update_user_via_user as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"CB1Qca0VrBcap9qO6VpKfoi2dG8GNG+tGGNDgCnFEv4E=", "user"=>{ "fullname"=>"John Doe", "languages"=>["", "1", "2"]}, "commit"=>"Save"}
User Load (28.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = 3 ORDER BY "users"."id" ASC LIMIT 1
Unpermitted parameters: languages
(0.1ms) begin transaction
(0.1ms) commit transaction
Redirected to http://127.0.0.1:3000/setup_profile
Completed 302 Found in 163ms (ActiveRecord: 29.5ms)
Now if you look closely on the loogs you will see "Unpermitted parameters: languages".
In my users_controller I have the followings:
def user_params
params.require(:user).permit(:languages, :fullname)
end
and the custom action:
def update_user_via_user
if current_user.update(user_params)
flash.notice = "Your profile was sent for moderation. We will moderate it asap!"
else
flash.alert = "Something went wrong! Please try again."
end
redirect_to root_path
end
Some other references: (my question at the end)
schema.rb:
languages table:
create_table "languages", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "user_id"
end
users table:
t.string "languages"
language.rb model:
class Language < ActiveRecord::Base
belongs_to :user
end
and user.rb model:
class User < ActiveRecord::Base
has_many :languages
end
The view:
<%= f.label :languages %>
<%= f.select :languages, Language.all.map{ |l| [l.name, "#{l.id}"] }, {}, { :multiple => true } %>
I am not sure why "languages" is not permitted and also if my concept of code is correct
class Language < ActiveRecord::Base
belongs_to :user
end
This would setup a one two many relation between users and languages which is not what you want. What you want is a many to many relation:
Users can speak many languages
Languages have many speakers (users)
So to keep track of this we need a third table:
language_users is what is known as a join table. You can actually name the table whatever you want - calling it a + _ + bs is just a convention.
We also need to setup our models to use the join table.
class User < ActiveRecord::Base
has_many :language_users
has_many :languages, through: :language_users
end
class Language < ActiveRecord::Base
has_many :language_users
has_many :users, through: :language_users
end
# this is a join model that links User & Language
class LanguageUser < ActiveRecord::Base
belongs_to :user
belongs_to :language
end
To create a form element where users can select languages you would use:
<%= f.collection_select(:languages_ids, Language.all, :id, :name, multiple: true) %>
Or:
<%= f.collection_check_boxes(:languages_ids, Language.all, :id, :name) %>
languages_ids is a special accessor that ActiveRecord creates for has_many associations that lets you set multiple associations at one by passing an array of ids.
I was thinking Deep's answer was the right one, based on this exchange.
I fired up my console and did this:
irb(main):001:0> x = ActionController::Parameters.new(user: {fullname: "John Doe", languages: ['','1','2']})
=> {"user"=>{"fullname"=>"John Doe", "languages"=>["", "1", "2"]}}
irb(main):002:0> y = x.require(:user).permit(:fullname, :languages => [])
=> {"fullname"=>"John Doe", "languages"=>["", "1", "2"]}
irb(main):003:0> y[:languages]
=> ["", "1", "2"]
So, hm.
What's the error message you're getting now? Same as original?

Implement a "Add to collection" in rails 4

I'm working on a app where users can find recipes made by chefs. I have implemented already a "like/dislike" fonction but now I would like to give to the user the ability to save a recipe in a collection.
The user can create many collections, so when he want to save a recipe in a collection, he should be able to see his current collections or create a new one.
Here are my models:
class User < ActiveRecord::Base
has_many :collections
accepts_nested_attributes_for :collections
end
class Collection < ActiveRecord::Base
has_many :recipe_collections
has_many :recipes, through: :recipe_collections
end
class Recipe < ActiveRecord::Base
has_many :recipe_collections
end
class RecipeCollection < ActiveRecord::Base
belongs_to :recipe
belongs_to :collection
end
my recipe_collections_controller.rb
class RecipeCollectionsController < ApplicationController
before_action :set_recipe, only: [:create]
before_action :set_user, only: [:create]
def create
#recipe_collection = #recipe.recipe_collections.build(recipe_collection_params)
if current_user
#recipe_collection.save
respond_to do |format|
format.html {redirect_to :back }
end
else
respond_to do |format|
format.html { render 'recipes/show' }
end
end
end
private
def set_user
#user = current_user
end
def set_recipe
#recipe = Recipe.find(params[:recipe_id])
end
def recipe_collection_params
params.require(:recipe_collection).permit(:collection_id, :recipe_id, collection_attributes: [:id], recipe_attributes: [:id])
end
end
In the recipe show, I have a render, show.html.erb:
<%= render "recipe_collections/form", collection: #recipe_collection || #recipe.recipe_collections.build%>
my partial _form.html.erb
<%= simple_form_for ([ collection.recipe, collection ]) do |form| %>
<%= form.association :collection, as: :check_boxes %>
<%= form.button :submit %>
<% end %>
my routes.rb
resources :recipes, :concerns => :paginatable do
member do
get "like", to: "recipes#upvote"
get "dislike", to: "recipes#downvote"
end
resources :reviews, only: :create
resources :recipe_collections
end
What is the problem:
1: in my show I have a checkbox form, but it don't display the current user collections but all.
2: when I submit the form, it don't save the recipe in the collection that I chose.
edit:
here are an exemple of my logs when I submit the form.
Started POST "/recipes/66/recipe_collections" for ::1 at 2015-04-06 23:58:06 +0200
Processing by RecipeCollectionsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"GhMIJs4nNmwsLpzBj4l5ta/OW6fN9dfuzBBnciJsjEa0YlfjQMQYDEmwhcXb9++oEmS36yEbgICNsSdbLgiigA==", "recipe_collection"=>{"collection_id"=>["24", ""]}, "commit"=>"Créer un(e) Recipe collection", "recipe_id"=>"66"}
(0.4ms) SELECT COUNT(*) FROM "recipes"
(0.2ms) SELECT COUNT(*) FROM "publishers"
(0.2ms) SELECT COUNT(*) FROM "votes"
Recipe Load (0.2ms) SELECT "recipes".* FROM "recipes" WHERE "recipes"."id" = $1 LIMIT 1 [["id", 66]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT 1 [["id", 5]]
Unpermitted parameter: collection_id
(0.1ms) BEGIN
SQL (0.3ms) INSERT INTO "recipe_collections" ("recipe_id", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["recipe_id", 66], ["created_at", "2015-04-06 21:58:06.566948"], ["updated_at", "2015-04-06 21:58:06.566948"]]
(0.4ms) COMMIT
Redirected to http://localhost:3000/recipes/66
Completed 302 Found in 11ms (ActiveRecord: 2.0ms)
Its a bit confusing whether you are setting up a relationship between recipes and collections as many-to-many or one-to-many. Your text implies a collection has many recipes and a recipe belongs to one collection. While your code, by using a join table and stating a recipe has many recipe-collections implies you are setting up a many-to-many relationship.
In other words you are a bit mixed up...
If a recipe belongs to a collection. Get rid of the join table. Recipe belongs_to :collection, while Collection has_many :recipes. Get rid of the other relationship stuff in those two models. In your migration table for Recipe, make sure there is a t.belongs_to :collection to set up the foreign key.
If a recipe has and belongs to many collections. Keep the join table, but name it collections_recipes as Rails can only find it if both sides are pluralized and in alphabetical order. Recipe has_and_belongs_to_many :collections, Collection has_and_belong_to_many :recipes. Drop the has_many :recipe_collections and the through: bit, do not replace either, Rails does this for you. Check your join table in your migration has t.belongs_to :recipe and :collection
Hope this helps.

has_many relationship, accepts_nested_attributes_for - nested record not created

I am trying to create a relation_level, with a score attribute, for each interest_question/feed pair. I am doing this with an accepts_nested_attributes_for :relation_levels in the feed form. Everything renders as it should, and when the form is submitted, a feed is created, but no relation_levels are created.
I've also tried adding the feed_id as a hidden field in the form.
(using rails 4 and haml)
app/models/interest_question.rb
class InterestQuestion < ActiveRecord::Base
has_many :relation_levels, dependent: :destroy
has_many :interest_answers, dependent: :destroy
end
app/models/relation_level.rb
class RelationLevel < ActiveRecord::Base
belongs_to :feed
belongs_to :interest_question
end
app/models/feed.rb
class Feed < ActiveRecord::Base
has_many :relation_levels, dependent: :destroy
has_many :interest_questions, through: :relation_levels
accepts_nested_attributes_for :relation_levels
end
app/views/feeds/_form.html.haml
=form_for(#feed) do |f|
...other fields
...other fields
-#interest_questions.each do |iq|
=f.fields_for #feed.relation_levels.build(interest_question_id: iq.id) do |rl|
=rl.label iq.question_text
=rl.range_field :score, max: 100, min: 0, default: 0
=f.submit
app/controllers/feeds_controller.rb
class FeedsController < ApplicationController
def new
#feed = Feed.new
#sources = Source.all
#interest_questions = InterestQuestion.all
end
def create
#feed = Feed.new(feed_params)
if #feed.save
redirect_to '/feeds', notice: 'Feed created.'
else
render action: 'new'
end
end
...
private
def feed_params
params.require(:feed).permit(..., relation_levels_attributes:
[:interest_question_id, :score, :feed_id])
end
Server output:
Started POST "/feeds" for 127.0.0.1 at 2013-12-30 15:00:58 -0600
Processing by FeedsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"/+TOOdpZZk85YvVlxkRIpLNfPfVVtGUTlKPb9Ctkvh8=", "feed"=>{"url"=>"lkas6df.com", "source_id"=>"1", "section"=>"kasl6d6fa", "area_importance"=>"", "is_local_news"=>"0", "relation_level"=>{"score"=>"17"}}, "commit"=>"Create Feed"}
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."remember_token" = '6fcabf7c7b1376250b1ffa589ff4f2279854d066' LIMIT 1
Unpermitted parameters: relation_level
(0.1ms) begin transaction
Source Load (0.1ms) SELECT "sources".* FROM "sources" WHERE "sources"."id" = ? ORDER BY "sources"."id" ASC LIMIT 1 [["id", 1]]
Feed Exists (0.2ms) SELECT 1 AS one FROM "feeds" WHERE "feeds"."url" = 'lkas6df.com' LIMIT 1
SQL (1.7ms) INSERT INTO "feeds" ("created_at", "section", "source_id", "updated_at", "url") VALUES (?, ?, ?, ?, ?) [["created_at", Mon, 30 Dec 2013 21:00:58 UTC +00:00], ["section", "kasl6d6fa"], ["source_id", 1], ["updated_at", Mon, 30 Dec 2013 21:00:58 UTC +00:00], ["url", "lkas6df.com"]]
(170.8ms) commit transaction
Redirected to http://localhost:3000/feeds
I've been stuck on this for a while, thanks in advance for any help :)
This should be a comment, but it's too big:
Maybe this could be your issue?
=f.fields_for #feed.relation_levels.build(interest_question_id: iq.id) do |rl|
This builds an ActiveRecord object on the fly, which IMO is bad practice. I'd build the object in the controller, and then call the object in the f.fields_for, like this:
#app/controllers/feeds_controller.rb
def new
#feed = Feed.new
#sources = Source.all
#interest_questions = InterestQuestion.all
#interest_questions.count.times do
#feed.relation_levels.build
end
end
You can then call:
=f.fields_for :relation_levels do |rl|
Much cleaner, and will likely help with your debugging!

Nested forms in Rails using has_many :through

I am having trouble figuring out how to make a nested form using a has_many :through relationship. I used this Railscast and I took at look at this tutorial and lots of the questions on Stack Overflow and elsewhere around Google.
I'm trying to make a way to create tags via the articles form. My code has gone through lots of iterations based on information from lots of different sources and none of them have worked, but right now I have
A class for articles
class Article < ActiveRecord::Base
attr_accessible :content, :heading, :image, :tag_ids, :tags, :tag_name, :tag_attributes
belongs_to :user
has_many :comments, :dependent => :destroy
has_many :article_tags
has_many :tags, :through => :article_tags
accepts_nested_attributes_for :tags, :reject_if => proc { |attributes| attributes['tag_name'].blank? }
...
end
A class for tags
class Tag < ActiveRecord::Base
attr_accessible :tag_name
has_many :article_tags
has_many :articles, :through => :article_tags
end
A class for article_tags
class ArticleTag < ActiveRecord::Base
belongs_to :article
belongs_to :tag
end
The New in my articles_controller.rb is like this:
def new
#article = Article.new
#tags = Tag.find(:all)
article_tag = #article.article_tags.build()
#article_tags = #article.tags.all
#article.article_tags.build.build_tag
3.times do
article_tag = #article.article_tags.build()
end
end
And my form for articles is currently like this (I have gone back and forth between nesting the fields_for :tags inside the fields_for :article_tags or just letting them be on their own):
<%= form_for #article , :html => { :multipart => true } do |f| %>
...excerpted...
<%= f.fields_for :article_tags do |t| %>
<%= t.fields_for :tags do |ta| %>
<%= ta.label :tag_name, "Tag name" %>
<%= ta.text_field :tag_name %>
<% end %>
<% end %>
I realize this is probably messy; I'm pretty new at this and I'm trying to figure it out. Do I have to add anything to the articles_controller create? Is it something to do with the attr_accessible? Or should I do something completely different?
EDIT:
Here are the request parameters after making the change suggested by Hck and creating a new article, selecting an existing tag with tag_id 3 and trying to also create a new tag at the same time:
Started POST "/articles" for 127.0.0.1 at 2011-08-10 19:05:46 +1000
Processing by ArticlesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"5CQuV4RWfFZD1uDjv1DrZbIe+GB/sDQ6yiAETZutmZ4=", "article"=>{"heading"=>"Test heading", "content"=>"Test Content", "tag_ids"=>["3"], "article_tags"=>{"tags"=>{"tag_name"=>"Test tag"}}}, "commit"=>"Submit"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
WARNING: Can't mass-assign protected attributes: article_tags
Tag Load (0.4ms) SELECT "tags".* FROM "tags" WHERE "tags"."id" = 3 LIMIT 1
AREL (0.4ms) INSERT INTO "articles" ("content", "user_id", "created_at", "updated_at", "heading", "image_file_name", "image_content_type", "image_file_size") VALUES ('Test Content', 1, '2011-08-10 09:05:46.228951', '2011-08-10 09:05:46.228951', 'Test heading', NULL, NULL, NULL)
AREL (0.2ms) INSERT INTO "article_tags" ("article_id", "tag_id", "created_at", "updated_at") VALUES (88, 3, '2011-08-10 09:05:46.243076', '2011-08-10 09:05:46.243076')
[paperclip] Saving attachments.
Redirected to [localhost]
Completed 302 Found in 212ms
And if I add :article_tags to the attr_accessible for Article and try again, I get:
Started POST "/articles" for 127.0.0.1 at 2011-08-10 19:11:49 +1000
Processing by ArticlesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"5CQuV4RWfFZD1uDjv1DrZbIe+GB/sDQ6yiAETZutmZ4=", "article"=>{"heading"=>"Test heading", "content"=>"Test content", "tag_ids"=>["3"], "article_tags"=>{"tags"=>{"tag_name"=>"Test tag "}}}, "commit"=>"Submit"}
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
Tag Load (0.4ms) SELECT "tags".* FROM "tags" WHERE "tags"."id" = 3 LIMIT 1
Completed in 119ms
ActiveRecord::AssociationTypeMismatch (ArticleTag(#2165285820) expected, got Array(#2151973780)):
app/controllers/articles_controller.rb:32:in `create'
Try to replace #article.article_tags.build.build_tag with #article.tags.build in your controller`s action.
I don't think you have to nest the article tags in it too. Article tags is just an association between the articles and tags. You can simply create the new tag within the articles because you already associated with them. I believe it is from the magic of the "accepts_nested_attributes". try this.
<%= form_for #article , :html => { :multipart => true } do |f| %>
...excerpted...
<%= f.fields_for :tags, Tag.new do |t| %>
<%= t.label :tag_name, "Tag name" %>
<%= t.text_field :name %>
<% end %>
<% end %>
Also, you should try to mass assign it instead of saving every attribute piece by piece by using private params. I had a nested forms problem before too, so you can take a look at how I wrote my code:
Cannot save record to database RAILS nested forms
The only thing I left out there was the private params section, which I recommended you to do.
private
def venue_params
params.require(:venue).permit(:name, :address, :discount, :latitude, :longitude, :tags_attributes =>[:name],:tag_ids => [])
end
I also wrote a blog post about nested forms, so you can take a look at it too
http://minling.github.io/

Resources