Rails association don't work with attributes - ruby-on-rails

I have a problem with my association in ruby on rails.
I have the association :
class Play < ApplicationRecord
has_many :program_plays
has_many :programs, through: :program_plays
end
class ProgramPlay < ApplicationRecord
belongs_to :program
belongs_to :play
end
class Program < ApplicationRecord
has_many :program_plays
has_many :plays, through: :program_plays
accepts_nested_attributes_for :plays
end
When i create a Program i want to associate one or several Play. The Play are create beforehand. The Play have got many attributes whose :title and :id of course.
The program_controller :
class Admin::ProgramsController < Admin::AdminController
def index
#programs = Program.all
end
def new
#program = Program.new
#program.plays.build
end
def create
#program = Program.new(program_params)
if #program.save
redirect_to admin_programs_path, notice: ''
else
end
end
private
def set_program
#program = Program.find(params[:id])
end
def program_params
params.require(:program).permit(:start_date, plays_attributes: [:title])
end
end
The form of Program (with the simple_form gem) :
<%= simple_form_for [:admin, #program] do |f| %>
<%= f.error_notification %>
<%= f.association :plays %>
<%= f.input :start_date, as: :datetime, minute_step: 15, label: 'Heure' %>
<%= f.button :submit, 'Poster', class: 'button red-full' %>
<% end %>
Now when i want to display the Program in my index, i also want to display the title of associated Play. But when i write :
program.plays.title
an error tell me that title is not a method of program... Why ? I don't understand. The association is real because when i puts in the rails console :
program.plays.to_a
i have :
Play Load (5.4ms) SELECT "plays".* FROM "plays" INNER JOIN "program_plays" ON "plays"."id" = "program_plays"."play_id" WHERE "program_plays"."program_id" = $1 [["program_id", 21]]
=> []
Can you see the problem ? I look for it for a longtime ago... I am desperate...
UPDATE :
In the rails console when i do program.plays.first.title :
Play Load (0.5ms) SELECT "plays".* FROM "plays" INNER JOIN "program_plays" ON "plays"."id" = "program_plays"."play_id" WHERE "program_plays"."program_id" = $1 ORDER BY "plays"."id" ASC LIMIT $2 [["program_id", 22], ["LIMIT", 1]]
NoMethodError: undefined method `title' for nil:NilClass
When i do :
program.plays.each do |play|
play.title
end
The result is :
Play Load (0.5ms) SELECT "plays".* FROM "plays" INNER JOIN "program_plays" ON "plays"."id" = "program_plays"."play_id" WHERE "program_plays"."program_id" = $1 [["program_id", 22]]
=> []
Here we can see that title is empty...
UPDATE 2 :
create_table "plays", force: :cascade do |t|
t.string "title"
t.text "body"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "program_plays", force: :cascade do |t|
t.bigint "program_id"
t.bigint "play_id"
t.index ["play_id"], name: "index_program_plays_on_play_id"
t.index ["program_id"], name: "index_program_plays_on_program_id"
end
create_table "programs", force: :cascade do |t|
t.datetime "start_date"
end
Maybe the problem is here...
UPDATE 3 :
> program = Program.new
=> #<Program:0x00000000068ce8e8 id: nil, start_date: nil>
[15] pry(main)> program.id = 1
=> 1
> program.plays.build(Play.find_by(id: 10))
Play Load (6.2ms) SELECT "plays".* FROM "plays" WHERE "plays"."id" = $1 LIMIT $2 [["id", 10], ["LIMIT", 1]]
ArgumentError: When assigning attributes, you must pass a hash as an argument.
from /home/coeurcoeur/.rvm/gems/ruby-2.5.0/gems/activemodel-5.1.4/lib/active_model/attribute_assignment.rb:28:in `assign_attributes'
> program.save
(0.1ms) BEGIN
(0.3ms) ROLLBACK
=> false

The associations looks good. the problem that you have is that you are calling program.plays.title but have in mind that program.plays will have a collection of plays and not just one so you can't call directly the title there.
it depends on what you are trying to do, for example if you want the title of the first play, you can do something like program.plays.first.title
or if you want to print all the plays title you can do something like
program.plays.each do |play|
play.title
end
and I am assuming program is filled with an existing program in your code
Another problem that you have is that you are not permitting the program_id on the controller so when you create the plays, the attribute program_id that has the association isn't saved because isn't permitted, so just add program_id on your params method like this
def program_params
params.require(:program).permit(:start_date, plays_attributes: [:title, :program_id])
end

Related

Issue with Rails query joins

I have a really dumb issue, I have two models:
class User < ApplicationRecord
belongs_to :role
end
and
class Role < ApplicationRecord
has_many :user
end
I'm trying to get the user and make a join with the role's table to get the role's name and id, like:
"user" : {
"name": "json",
"role": {"name":"admin", "id":1}
}
however, after using:
User.includes(:role).all
I just get the users with a "role_id" value, I've also tried:
User.joins(:role)
With the same result. I've been looking at the official docs at (https://guides.rubyonrails.org/active_record_querying.html) and it should be pretty straightforward but I don't know what I'm doing wrong. Do I need to add something to my migrations?, At my create_user migration I have:
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :username
t.string :email
t.integer :role_id
end
add_index :users, :email
add_index :users, :username
end
end
and my create_roles migration
class CreateRoles < ActiveRecord::Migration[6.0]
def change
create_table :roles do |t|
t.string :name
end
end
end
all by itself doesn't return such a custom data. You can use as_json for that:
User.all.as_json(only: :name, include: { role: { only: [:name, :id] } }, root: true)
# User Load (0.5ms) SELECT "users".* FROM "users"
# Role Load (0.6ms) SELECT "roles".* FROM "roles" WHERE "roles"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
# => [{"user"=>{"name"=>"json", "role"=>{"id"=>1, "name"=>"admin"}}}]

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.

Rails polymorphic has_many through association - form broken

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.

Rails 5.0.2 - unable to get data from many-many relationship models

I am stuck at getting data in development.sqlite3 from 3 tables that have many-to-many relationship. I can get the courses that belong to a given location or category but it does not work the way around.
I followed the way I was taught in my class but I think the models might have missed some constraints.
Anyone please takes a look and gives me some suggestion. Thank you so much!
This is my schema.rb:
create_table "categories", force: :cascade do |t|
t.string "title"
end
create_table "categories_courses", force: :cascade do |t|
t.integer "course_id", null: false
t.integer "category_id", null: false
end
create_table "courses", force: :cascade do |t|
t.string "title"
end
create_table "courses_locations", force: :cascade do |t|
t.integer "course_id", null: false
t.integer "location_id", null: false
end
create_table "locations", force: :cascade do |t|
t.string "address"
end
I want to get the locations or the categories that associate with a given course. Then I added the relationships in the models like below:
# Category model
class Category < ApplicationRecord
has_and_belongs_to_many :courses, join_table: 'categories_courses'
end
# Location model
class Location < ApplicationRecord
has_and_belongs_to_many :courses, join_table: 'courses_locations'
end
# Course model
class Course < ApplicationRecord
has_and_belongs_to_many :locations, join_table: 'courses_locations'
has_and_belongs_to_many :categories, join_table: 'categories_courses'
end
Then in courses_controller.rb, I get the locations and categories like this:
class CoursesController < ApplicationController
before_action :set_course, only: [:show, :edit, :update, :destroy]
def categories
#course = Course.find params[:id]
#categories = #course.categories
end
def locations
#course = Course.find params[:id]
#locations = #course.locations
end
private
def set_course
#course = Course.find(params[:id])
end
end
Finally I display data in html.erb file like this:
<% course.categories.each {|category| puts category.title + ' '} %>
<% course.locations.each {|location| puts location.address + ' '} %>
I am so sorry for putting lots of codes here, but I should provide enough information to examine the code.
Update:
I have this in route.rb
Rails.application.routes.draw do
resources :locations
resources :categories
resources :courses
end
I have edited the code as everyone's suggestions but it still prints nothing. I notice the query is correct but there is a warning in rails s console:
User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 35], ["LIMIT", 1]]
DEPRECATION WARNING: Passing an argument to force an association to reload is now deprecated and will be removed in Rails 5.1. Please call `reload` on the result collection proxy instead. (called from block in _app_views_courses_index_html_erb___1034719630_104325280 at c:/Users/lovea/RubymineProjects/YourCourse/app/views/courses/index.html.erb:41)
Category Load (0.0ms) SELECT "categories".* FROM "categories" INNER JOIN "categories_courses" ON "categories"."id" = "categories_courses"."category_id" WHERE "categories_courses"."course_id" = ? [["course_id", 4]]
Not sure, but you probably need to add the option join_table
# Category model
class Category < ApplicationRecord
has_and_belongs_to_many :courses, join_table: "categories_courses"
end
# Location model
class Location < ApplicationRecord
has_and_belongs_to_many :courses, join_table: "courses_locations"
end
# Course model
class Course < ApplicationRecord
has_and_belongs_to_many :locations, join_table: "courses_locations"
has_and_belongs_to_many :categories, join_table: "categories_courses"
end
I think the associations should be working. I have tried it myself and got it working in the console. It might be that the issue you are having with routing perhaps.
Also, in the view, instead of:
<% course.categories(course.id).each {|category| puts category.title + ' '} %>
<% course.locations(course.id).each {|location| puts location.address + ' '} %>
You could:
<% course.categories.each {|category| puts category.title + ' '} %>
<% course.locations.each {|location| puts location.address + ' '} %>
Because the association is already there without the need to pass the id.
you can easily create a view for many to many with collecion checkboxes
<%= f.collection_check_boxes(:course_id, Course.all, :id, :title) do |b| %>
<div class="collection-check-box">
<%= b.check_box %>
<%= b.label %>
<%end%>
<%= f.submit 'add course to location', class: "btn btn-primary" %>
<%end%>

how to remove records from the joined table?

please help solve the problem.
table posts
create_table "posts", force: :cascade do |t|
t.string "title"
t.text "body"
end
table tags
create_table "tags", force: :cascade do |t|
t.string "tagname"
end
joined table
create_table "posts_tags", id: false, force: :cascade do |t|
t.integer "post_id"
t.integer "tag_id"
end
model Post
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
end
model Tag
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
I need to remove some of the association table posts_tags.
I get from the form set id tags:
[3, 18, 21]
Here is my controller that handles this set id_tags:
def update
if #post.update_attributes(post_params)
#add_new_tags(#post)
p '------------------1'
p params['delete_tags']
p '------------------2'
destroy_tags(params['delete_tags'], #post)
flash[:success] = t :post_updated
redirect_to user_post_path(#user, #post)
else
flash[:error] = t :post_not_updated
render 'edit'
end
end
private
def destroy_tags(tags,post)
tags.each do |tag|
p '=================='
p tag
tag_del = post.tags.find_by_post_id(:post_id => post.id)
if teg_del
post.tags.delete(tag_del)
end
end
end
def post_params
params.require(:post).permit(:delete_tags)
end
as a result, I get the following error message:
undefined method `find_by_post' for #<Tag::ActiveRecord_Associations_CollectionProxy:0x007f097ed78ac8>
the console displays the following message:
Processing by PostsController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"jfNoFJVn2dDqIwIdK/SWerFKIPFJ9bTYdWjy4QFBFL8gHrO7TnzhBq4Mcw+uDyDn9atLEAmfcPdlmHBVHMSDHQ==", "post"=>{"title"=>"Corrupti.ggh", "body"=>"Suscipit ut odit labore fugiat quia aliquam."}, "tagnames"=>"", "delete_tags"=>["3", "18", "21"], "commit"=>"Сохранить Post", "locale"=>"ru", "user_id"=>"24", "id"=>"359"}
Post Load (0.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT 1 [["id", 359]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 24]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."remember_token" = ? LIMIT 1 [["remember_token", "15166203712e74cc4638f34991c141f85c04a0e0"]]
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", "24"]]
(0.1ms) begin transaction
(0.0ms) commit transaction
"------------------1"
["3", "18", "21"]
"------------------2"
"=================="
"3"
Completed 500 Internal Server Error in 6ms (ActiveRecord: 0.5ms)
Since you have a join table, the tags table itself doesn't have a post_id, so that find_by won't work. It isn't needed though because post.tags would retrieve all the tags connected to that post anyway.
You get a lot of the functionality for handling habtm relationships and forms out of the box with rails using things like collection_check_boxes where I don't think you need the extra destroy tags method unless I'm misunderstanding what you are trying to accomplish. You might consider adding/removing tag associations to posts like this:
Post - has_and_belongs_to_many :tags
Tag - has_and_belongs_to_many :posts
Post Update Form:
<legend>Tags</legend>
<div class="form-group">
<%= f.collection_check_boxes(:tag_ids, Tag.all, :id, :name) %>
</div>
Make sure to allow :tag_ids => [] in your controller's post_params
The will allow you to update the tags belonging to the post by adding any new tags that were checked and removing any tags that were unchecked in the form and you can have normal create and update controller actions without need for the extra destroy_tags method.

Resources