Nested attributes not being associated - ruby-on-rails

I am having some unexpected behaviour with nested attributes in that the nested attributes are saving only the default values defined in the model. I have read a lot of documentation and tried many configurations of the build_ method prefixes in the controller, but this is the best I have come up with so far. Seem like the data is being persisted to the DB, however only the default values, so I suspect the issue lies with the form or the build method in controller.
I thought the foreign keys were possibly wrong, although I think that's not the issue any more.
Newish to rails (please forgive). I have read many questions and answers on related issues, but none have done the trick. Thanks for any tips and pointers.
I have 3 models, user, year and months. All belonging to each other in the following fashion:
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :years, dependent: :destroy
end
class Year < ApplicationRecord
belongs_to :user
has_one :month, dependent: :destroy, inverse_of: :year
validates :year, presence: true
accepts_nested_attributes_for :month
end
class Month < ApplicationRecord
belongs_to :year, inverse_of: :month
attribute :january, :integer, default: 110
attribute :febuary, :integer, default: 0
....etc
end
The years controller is like this
def new
#year = current_user.years.build
end
def create
#year = current_user.years.build(year_params)
...etc...
end
def year_params
params.require(:year).permit(:year, :monthly_target, :user_id,
months_attributes: [:january, :febuary, :march, :april, :may, :june, :july, :august,
:september, :october, :november, :december])
end
The form is like this:
<div>
<%= form.label :year, style: "display: block" %>
<%= form.number_field :year, min:0, in: 1990...2051 %><span><small>add a year from 1990
to 2050</small></span>
</div>
<div>
<%= form.label :monthly_target, style: "display: block" %>
<%= form.number_field :monthly_target, min: 0, step: 25 %>
</div>
<%= form.fields_for :months do |month_form| %>
<div>
<%= month_form.label :january, style: "display: block" %>
<%= month_form.number_field :january, min: 0, step: 25%>
</div>
<div>
<%= month_form.label :febuary, style: "display: block" %>
<%= month_form.number_field :febuary, min: 0, step: 25 %>
</div>
Schema is like this:
ActiveRecord::Schema[7.0].define(version: 2022_09_20_123059) do
create_table "months", force: :cascade do |t|
t.integer "febuary"
t.integer "march"
t.integer "april"
t.integer "may"
t.integer "june"
t.integer "july"
t.integer "august"
t.integer "september"
t.integer "october"
t.integer "november"
t.integer "december"
t.integer "year_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["year_id"], name: "index_months_on_year_id"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
create_table "years", force: :cascade do |t|
t.integer "year"
t.integer "monthly_target"
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_years_on_user_id"
end
add_foreign_key "months", "years"
add_foreign_key "years", "users"
end
ps: this is rails 7 on windows, not using WSL2 (yet...!)
edit: here are params being sent through>>
{"authenticity_token"=>"[FILTERED]",
"year"=>
{"year"=>"2022",
"monthly_target"=>"25",
"months"=>{"january"=>"", "febuary"=>"", "march"=>"", "april"=>"",
"may"=>"", "june"=>"", "july"=>"", "august"=>"", "september"=>"",
"october"=>"", "november"=>"50", "december"=>""}},
"commit"=>"Create Year"}
also, here is the instance for the #year variable
>>
#year
=> #<Year id: nil, year: 2022, monthly_target: 25, user_id: 1,
created_at: nil, updated_at: nil>
you can see I added in the form 50 to november and the rest are empty fields. This is ok though as I opted for default values of 0 on all fields in the months model.
I must say that I have found a decent work around, which I think is in the Rails ethos. I redesigned everything to just use one model "Year" and have added methods on the "Year" model to do the things I want, keeping the controller quite empty. This seems to work well, but I am slightly frustrated that I was unable to get the association between two models to work... Thanks for your help.

Related

Multiple Checkbox in Ruby

I'm trying to create an "ingredient" checkbox list derived from my "recipes", I'd like for the values to be saved in the database so that when it's checked and I refresh the page, it still shows as checked.
The error says "uninitialized constant #Class:0x00007f8f2d360830::Parties"
Here's an example of what i am trying to do
Controller:
# parties_controller.rb
def ingredients
#party = Party.find(params[:party_id])
#party_recipe = #party.recipes
#party_recipe.each do |recipe|
#ingredients = recipe.ingredients
end
The models:
Party model
#party.rb
class Party < ApplicationRecord
has_many :party_recipes
has_many :recipes, through: :party_recipes
end
Recipe model
#recipe_ingredient.rb
class RecipeIngredient < ApplicationRecord
belongs_to :recipe
belongs_to :ingredient
end
Ingredient model
#ingredient.rb
class Ingredient < ApplicationRecord
has_many :recipe_ingredients
has_many :recipes, through: :recipe_ingredients
end
Form:
#ingredients.html.erb
<% form_for "/parties/#{#party.id}/ingredients" do |f| %>
<% Parties::Recipes::Ingredients.each do |ingredient| %>
<%= check_box_tag(ingredient) %>
<%= ingredient %>
<% end %>
<% end %>
Schema:
create_table "ingredients", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "parties", force: :cascade do |t|
t.string "title"
t.string "address"
t.bigint "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "theme"
t.date "date"
t.integer "attendancy"
t.integer "appetizers"
t.integer "mains"
t.integer "desserts"
t.string "status", default: "pending"
t.index ["user_id"], name: "index_parties_on_user_id"
end
create_table "party_recipes", force: :cascade do |t|
t.bigint "recipe_id", null: false
t.bigint "party_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["party_id"], name: "index_party_recipes_on_party_id"
t.index ["recipe_id"], name: "index_party_recipes_on_recipe_id"
end
create_table "recipe_ingredients", force: :cascade do |t|
t.bigint "recipe_id", null: false
t.bigint "ingredient_id", null: false
t.string "amount"
t.boolean "included", default: false
t.index ["ingredient_id"], name: "index_recipe_ingredients_on_ingredient_id"
t.index ["recipe_id"], name: "index_recipe_ingredients_on_recipe_id"
end
create_table "recipes", force: :cascade do |t|
t.string "title"
t.text "description"
end
add_foreign_key "party_recipes", "parties"
add_foreign_key "party_recipes", "recipes"
add_foreign_key "recipe_ingredients", "ingredients"
add_foreign_key "recipe_ingredients", "recipes"
I'm not entirely sure where exactly needs to be corrected, any help appreciated, thank you so much!
Well the error message is correct, you don't have any model called Parties, in fact in Rails, models are always singular, camel-case. So that explains the error message.
However that won't fix your problem! The iterator in the view should be
<% #ingredients.each do |ingredient| %>
<%= check_box_tag(ingredient) %>
<%= ingredient %>
<% end %>
Because I think you are trying to populate an #ingredients variable in your controller. However it still won't work, b/c the value of the #ingredients variable is not being correctly assigned...
Personally I much prefer the "fat model skinny controller" design style for Rails. So I would have a PartiesController#ingredients method that looks like this:
# parties_controller.rb
def ingredients
#party = Party.find(params[:party_id])
#ingredients = #party.ingredients
end
then in your Party model:
# app/models/party.rb
def ingredients
recipes.map(&:ingredients).flatten
end
Why do it this way? Well you're just getting started with Rails, but eventually (soon hopefully) you'll be writing tests, and it's much much easier to write tests on models than controllers.
Now, there could well be some other issues in your code, but try my suggestions and see where that gets you.
#Les Nightingill's answer should work well for organizing your controller and model! Regarding when you click refresh and the value of the boxes are saved either;
Set up some listeners in javascript and send a request to your update controller method every time there is a value change for one of your check boxes.
Or add a save button at the bottom of your form that points to your update controller method to save the values of the checkboxes. Something like:
<%= submit_tag "Save", data: { disable_with: "Saving..." } %>
https://api.rubyonrails.org/v5.2.3/classes/ActionView/Helpers/FormTagHelper.html#method-i-submit_tag

How to implement counter with buttons to a post in a polymorphic associations?

I have an app where I can make post and create comments to them. And I need to implement 2 buttons (like and unlike) in my view to every post(tweet) and comment to this tweet. Also I have 2 routes to my likes: post and destroy: (all routes are here)
resource :user_profiles, only: %i[edit update]
resources :tweets
resources :comments, only: %i[create destroy]
resources :likes, only: %i[create destroy]
root to: 'home#index'
But also I have polymorphic associations:
class Like < ApplicationRecord
belongs_to :likable, polymorphic: true
belongs_to :user
end
class Tweet < ApplicationRecord
belongs_to :user
has_many :likes, as: :likable
has_many :comments, dependent: :destroy
end
class Comment < ApplicationRecord
has_many :likes, as: :likable
belongs_to :tweet
belongs_to :user
end
I wrote some code in my tweet.preview
<% if tweet.likes.find_by(user_id: current_user.id)%>
<%= button_to 'Unlike', like_path(likable_id: tweet.id, likable_type: tweet.class.name, user_id: current_user.id), method: :delete %>
<% else %>
<%= button_to 'Like', likes_path(likable_id: tweet.id, likable_type: tweet.class.name, user_id: current_user.id), method: :post%>
<% end %>
But I don't know how to implement like controller to count likes and display 2 buttons with counter to view.
My db.schema is here:
create_table "comments", force: :cascade do |t|
t.string "content"
t.integer "user_id"
t.integer "tweet_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "likes", force: :cascade do |t|
t.integer "user_id"
t.string "likable_type"
t.integer "likable_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "tweets", force: :cascade do |t|
t.string "content"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_tweets_on_user_id"
end
create_table "user_profiles", force: :cascade do |t|
t.string "username"
t.string "first_name"
t.string "last_name"
t.date "birthday"
t.string "bio"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_user_profiles_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
Thanks in advance!
Well you just need to pass the parent of the future like record (whereas it is a post or a comment) to the create action of the like controller.
You need to add a link to your buttons such as : like_path(parent_type: "comment", parent_id: #comment_id), method: "post"
(here #comment_id may not be an instance variable, especially if you loop through all comment of a specific tweet, it may instead look like comment.id if in your loop you did something like #tweet.comments.each do |comment|)
Then in your like controller, you can just assign them to the record.
I assume here that the user id "is already known" and you create the like from the user record.
#like = current_user.likes.new(likable_id: params[:parent_id], likable_type: params[:parent_type])
#like.save
So most of the job is made in your view by creating the correct link for your different buttons...
So i'm not sure about your setup. Are you using a gem?
So the secret to do this is to create two seperated paths:
resources :likes do
member do
get :like
get :unlike
end
end
inside your controller, you can then execute the logic inside each method:
def like
//find the user
//follow the user
//redirect him
end
def unlike
//find the user
//unfollow the user
//redirect him
end
Also, you can then simply use your buttons for each like/unlike path:
<% if tweet.likes.find_by(user_id: current_user.id)%>
<%= button_to 'Unlike', likes_unlike_path(likable_id: tweet.id, likable_type: tweet.class.name, user_id: current_user.id), method: :delete %>
<% else %>
<%= button_to 'Like', likes_like_path(likable_id: tweet.id, likable_type: tweet.class.name, user_id: current_user.id), method: :post%>
<% end %>
Now, to display the sizes of a like, you could retrieve the amount of users that have liked the tweet:
<h3>People who liked this tweet: <%= likes.count %> </h3>
obviously, only those people who have liked something will be displayed. If you also need a negative number, you could do two things: Either create a join table for your likes or add a new integer and call it "number_likes"
Then, inside your like method, simply increase the number_likes integer by 1. Whenvever someone clicks on the like link, the number will increase.
Inside the unlike method, simply substract by 1 whenever unlikes something. This should give you more or less the same result, but it would also allow you to add a negativ like number.
def like
number_likes += 1
//your previous logic here
end
def unlike
number_likes -=1
end
in your view, you could then simply get the tweet.likes.number_likes to display the amount of likes.
If something is still unclear, let me know!
Happy coding!

Rails - Nested Forms

I'm having a problem with nested forms, I'm new to rails so probably I'm doing something wrong, so I hope you can help me. Thanks in advance.
I have a model Poi (Points of Interest), a model PoiDescription and a model DescriptionType. An instance of Poi has many PoiDescriptions and a DescriptionType has many PoiDescriptions. What I want is this: when I'm creating a new Poi, I want to create multiple descriptions to it. A description has a category associated to it and there can only be one description for each category(ex: 10 categories = 10 descriptions). So here is my code:
Poi model
has_many :poi_descriptions
accepts_nested_attributes_for :poi_descriptions
PoiDescription model
belongs_to :poi
belongs_to :description_type
DescriptionType model
has_many :poi_descriptions
Poi controller
def new
#poi = Poi.new
#poi.poi_descriptions.build
#availableType = DescriptionType.where.not(id: #poi.poi_descriptions.pluck(:description_type_id))
end
def poi_params
params.require(:poi).permit(:name, :image, :longitude, :latitude, :monument_id, :beacon_id, poi_descriptions_attributes: [:description, :description_type_id, :id])
end
routes
resources :pois do
resources :poi_descriptions
end
resources :description_types
Poi _form
<%= form_with model: #poi do |f| %>
...
<%= f.fields_for :poi_descriptions do |p| %>
<%= p.collection_select :description_type_id, #availableType,:id,:name %>
<%= p.text_area :description %>
<% end %>
...
schema
create_table "description_types", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "poi_descriptions", force: :cascade do |t|
t.text "description"
t.bigint "poi_id"
t.bigint "description_type_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["description_type_id"], name: "index_poi_descriptions_on_description_type_id"
t.index ["poi_id"], name: "index_poi_descriptions_on_poi_id"
end
create_table "pois", id: :serial, force: :cascade do |t|
t.text "name"
t.float "longitude"
t.float "latitude"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "monument_id"
t.integer "beacon_id"
t.string "image_file_name"
t.string "image_content_type"
t.integer "image_file_size"
t.datetime "image_updated_at"
t.index ["beacon_id"], name: "index_pois_on_beacon_id"
t.index ["monument_id"], name: "index_pois_on_monument_id"
end
Now, the problem. Every time I try to create a new Poi I have this error:
Does anyone know why this is happening? Thanks.
P.S: Sorry for the long post :)
I've found the solution. Before having the PoiDescription model I had an attribute 'description' on the Poi model and I've put a validation on that attribute. When I changed the model, I forgot to remove that validation and that was the origin of my problem.

Counter Cache customizing for zeros and large numbers

I'm still learning rails so any help you can provide would be super helpful. I've set a count for my likes on my book app. Thus, every time a user likes a book - the number increases by one or decreases if the unlike it. However, if no one has liked a book yet - a 0 appears. I'd like that to be blank so that only when a user has liked it will the number appear. I've listed all my relevant code below. Thank you so much.
Schema.rb
create_table "books", force: :cascade do |t|
t.string "title"
t.integer "user_id"
t.integer "book_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "avatar_file_name"
t.string "avatar_content_type"
t.integer "avatar_file_size"
t.datetime "avatar_updated_at"
t.integer "likes_count", default: 0, null: false
end
create_table "likes", force: :cascade do |t|
t.integer "user_id"
t.integer "book_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Book.rb
class Book < ApplicationRecord
has_many :likes, :counter_cache => true
has_many :users, through: :likes
belongs_to :user
end
Likes.rb
class Like < ApplicationRecord
belongs_to :book, :counter_cache => true
belongs_to :user
end
Likes Count Migration
class AddLikecountsToBook < ActiveRecord::Migration[5.0]
def change
add_column :books, :likes_count, :integer, :null => false, :default => 0
end
end
With associations in rails you get several interogation methods such as .any? and .none? which can be used to create conditional expressions.
<% if book.likes.any? %>
<%= number_to_human(book.likes.size) %>
<% end %>
# or
<%= number_to_human(book.likes.size) unless book.likes.none? %>
This uses the counter cache as well to avoid n+1 queries.
If you do not want your view to display 0 you could add a if statement in your view.
<% if #votes == 0 %>
be the first to rate this book
<% else %>
<%= #votes %>
<% end %>
Or when returning the variable to the view from the controller
def
if #votes == 0
#votes = ''
end
end

Rails 4 Nested Form

I have three models:
class Item < ActiveRecord::Base
has_many :item_points
has_many :groups, through: :item_points
accepts_nested_attributes_for :item_points
end
class ItemPoint < ActiveRecord::Base
belongs_to :group
belongs_to :item
accepts_nested_attributes_for :group
end
class Group < ActiveRecord::Base
has_many :item_points
has_many :items, through: :item_points
end
The schema for items
create_table "items", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
The schema for item_points
create_table "item_points", force: :cascade do |t|
t.decimal "points", null: false
t.integer "item_id", null: false
t.integer "group_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
The schema for groups
create_table "groups", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
In my groups table, I've created a number of rows, e.g. group 1 and group 2.
In the form for creating items I'd really like to see a field each for group 1 and group 2, so that I might be able to enter the points for that item. e.g. In item X, group 1 is worth 10 points, and group 2 is worth 5 points.
EDIT Added the form
The form:
<%= form_for(#item) do |item_form| %>
<div class="form-group">
<%= item_form.label :name %>
<%= item_form.text_field :name, :class => 'form-control' %>
</div>
<%= item_form.fields_for(:groups) do |groups_form| %>
<% group = groups_form.object_id.to_s%>
<%= groups_form.hidden_field :id %>
<%= groups_form.fields_for(:item_point) do |entity_form| %>
<%= entity_form.text_field :points %>
<% end %>
<% end %>
This provides me with a form, which contains one extra entry box, called item[groups][item_point][points], and has no label.
What I'd like to know is how do I get all of the rows I've added into groups as fields into a Rails Form? And when I do, how do I save the associated item_points data using strong parameters?
I've spent quite some time looking for an answer, and I can't seem to find anything other than a series of StackOverflow questions, which don't quite seem to have the same problem as me.
All help is wonderfully appreciated.
There's a helpful post with some examples at: https://robots.thoughtbot.com/accepts-nested-attributes-for-with-has-many-through. It specifically talks about adding inverse_of on your associations.
In your view you'll need to use fields_for. You could (for example) put that in a table or a list and have each entry be a row.
If you share what you've tried in your view so far you may be able to get a more detailed response if you need it.
As for permitted_params in your controller, you can nest them something like:
def permitted_params
params.permit(item: [
:name,
item_points_attributes: [
:id,
:points,
:group_id,
]
)
end
Update:
Rather than fields_for(:groups) I think you want your controller to build the models for all the item_points (#item_points = Group.all.collect {|group| ItemPoint.new({group_id: group.id, item_id: #item.id}).
Then you can use a fields_for(:item_points, #item_points).
You can add a label for the field so it's not just an unlabeled field using the HTML label tag.

Resources