Model failing to save - ruby-on-rails

I searched for a solution for the problem, but I cannot find the root cause of the issue.
I have a couple of models in my Rails application, two of which, Category and Activity, have a many-to-many relationship. In both the Category and Activity models, I defined this relationship using has_and_belongs_to_many. I created a joining table between the two. Please find my DB scheme here: https://pastebin.com/wc4TsPQQ.
The form to create a new Activity contains a field to select one or more categories to match:
<%= f.collection_select :category_id, Category.all, :id, :name, {include_hidden: false}, {:multiple=>'true', :class=>'form-control'} %>
When I try to submit the form the activities.category_id field seems to not be "registred" (not sure about the right terminology). Rails either throws a
"category cannot be blank"
error or, when I temporary disable presence validation on Category, the error
SQLite3::ConstraintException: NOT NULL constraint failed: activities.category_id
Looking in the request parameters however, the category ID is being sent by the form:
{"authenticity_token"=>"MD5eM5H7DG6RkjD+/QSZ5RqkAnJupJ3L/V044r+QP6s0651mv4hoTSSa8NXB3x959dpwKsSQhBTi58idDMm9hA==",
"activity"=>{"name"=>"fsdffdf", "description"=>"sddfdsf", "location"=>"sdfsdf", "category_id"=>["3"], "user_id"=>"3"},
"commit"=>"Save Activity"}

First off, you seem to have created a activities.category_id column with a not-null constraint which is not needed here. You might want to re-read "The has_and_belongs_to_many Association" documentation.
To get rid of the column you can either roll back and alter the migration that created the table in the first place or create a migration to remove the column:
class RemoveCategoryIdFromActivities < ActiveRecord::Migration[6.0]
def change
remove_reference :activities, :category, index: true, foreign_key: true
end
end
Also remove the belongs_to :category association in your Activity model which causes the "category cannot be blank" validation error.
Then you should be using category_ids and not the singular _id.
<%= f.collection_select :category_ids, Category.all, :id, :name, {include_hidden: false}, {multiple: 'true', class: 'form-control' } %>
And make sure you correctly whitelist it:
params.require(:activity)
.permit(:name, :description, :current_location, category_ids: [])

You want category "ids" not "id" since you have multiple ids to set.
Change to f.collection_select :category_ids, ....., make sure you are permitting an array for that attribute on your controller too.

Related

Proper generation of Rails' field_with_errors for select list of belongs_to during validation failure?

[This similar question would have been all right for older versions of Rails, but not since belongs_to is required by default.]
Given I have a WorkOrder model and a Customer model, where the WorkOrder belongs_to a Customer:
class WorkOrder < ApplicationRecord
belongs_to :customer
And a typical select list in the work order form:
= form.collection_select :customer_id, customers, :id, :full_name, {include_blank: ''}, class: 'form-select'
When I do not select a customer, the validation fails since belongs_to requires an associated Customer in order for the WorkOrder to be valid. Ok. But then, when the form renders with the error, I do not see that the select list is wrapped with the usual field_with_errors element. As such, while the main error message about "Customer must exist" is there, the select list is not bordered in red.
This is because the error message key is on customer, and not customer_id, and the select list is for customer_id.
If I add a validator for customer_id to the WorkOrder model:
class WorkOrder < ApplicationRecord
belongs_to :customer
validates :customer_id, presence: true
Then the select list does get wrapped with field_with_errors and appears with a red border. However, now the error messages report "2 errors prohibited..." instead of "1 error prohibited...". In addition, the error messages are redundant: One states "Customer must exist" and another states "Customer can't be blank."
I can remove the validator for :customer_id and change the select tag to use customer:
= form.collection_select :customer, customers, :id, :full_name, {include_blank: ''}
But this isn't correct, since the field needs to use the name of the attribute, customer_id.
Instead, I could make a change in the model:
class WorkOrder < ApplicationRecord
belongs_to :customer, optional: true
validates :customer_id, presence: true
But this is redundant in the model and feels wrong: the association is not optional, nor do I want it to be. The optionality followed by a presence validator feels like a hack.
Lastly, I could keep the model clean:
class WorkOrder < ApplicationRecord
belongs_to :customer
And then in the view, inspect the errors:
<% if form.object.errors.include?(:customer) %>
<div class='field_with_errors'>
<% end %>
<div class="mb-3">
<%= form.label :customer_id %>
<%= form.collection_select :customer_id, customers, :id, :full_name, {include_blank: ''} %>
</div>
<% if form.object.errors.include?(:customer) %>
</div>
<% end %>
And this works, but is now an ugly hack in the view.
I'd like to have a normal belongs_to relationship and I would like Rails to generate a proper field_with_errors around a select element just like it does with text fields.
What is the proper way to get this behavior? (I swear Rails used to do this out of the box, but perhaps not, since belongs_to used to be optional by default, and we used to have to have the explicit presence validator on the foreign key attribute.)

F.select input not saving value on edit

I have an f.select input on my Rails app that comes from this helper method.
def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method)
collection.map do |group|
option_tags = options_from_collection_for_select(
group.send(group_method), option_key_method, option_value_method)
content_tag(:optgroup, option_tags, :label => group.send(group_label_method))
end.join.html_safe
end
The select in the view is shown below.
<%= f.select(:type_id, option_groups_from_collection_for_select(#categories, :types, :category, :id, :name)) %>
When saving the Post, the correct type_id is getting saved, but when I go and edit the post, the select doesn't show the currently selected item like it's supposed to. I'm assuming something is wrong in my code.
Here is my category model
has_many :posts
has_many :types, :order => "name"
and here is my type model
belongs_to :category
You have to provide a 5th argument which is the selected key. Try the code below:
<%= f.select(:type_id, option_groups_from_collection_for_select(#categories, :types, :category, :id, :name, f.object.type_id)) %>
f.object.type_id returns the type_id attribute of the object passed in the form if it has one. Otherwise, it would be nil and will not have anything selected.
I had the same problem and I found that I had simply misspelled the object's name in my controller file. It did not save because the matching object could not be found.

How to create Categories in Rails

i'm trying to Add Categories to my Rails app, but don't quite know how to do this.
I have many Pins(Images) and want the user to be able to assign a category on those Pins.
ASSIGN not create, edit, or delete a Category, just selecting one for their Pin.
Meaning that, When a user uploads a pin, he can choose from a dropdown list a Category.
Then, another user can choose from the Menu a Category, and ONLY the Pins in this Category will be listed.
How do i do this? Where to start ?
Thank you
First If you don't want to manage categories in your application, then you can simply add a category field in your table and category select in your application :
<%= f.select :category, [ 'Box', 'Cover', 'Poster' ], :prompt => 'Select One' %>
Second, If you want to manage categories in your application, than you have to maintain a separate model and table for it. So you can start with generating your model:
rails g model category
it will add model and migration in your application directory. Add stuff to your migration:
class CreateCategories < ActiveRecord::Migration
def change
create_table :categories do |t|
t.string :name
t.text :description
## you can add more stuff as per your requirements
t.timestamps
end
end
end
Define associations in category & Pin model add validation for this :-
In Category Model:
has_many :pins
In Pin Model :
belongs_to :category
validates :category, presence: true
Create some categories by categories controller and form (I don't think, I need to tell you that stuff, you are able to do it yourself)
In your pin uploading form add this select :-
<%= f.select :category, Category.all, :prompt => "Select One" %>
Hope, It will help.
you might wanna add a def to_s method on your Category model. I believe it will display some weird memory address code just by using plain Category.all on the f.select option. Everything else looks great!

Simple association architecture and implementation in ruby on rails

Quick question about a simple problem I am facing (and I want to use as a way to understand a few things about associations and rails in a deeper level). Here goes:
The two associated models are
class Employee < ActiveRecord::Base
attr_accessible :name
attr_accessible :age
belongs_to :role
attr_accessible :role_id
end
class Role < ActiveRecord::Base
attr_accessible :title
attr_accessible :salary
has_many :employees
end
so that every new employee has a fixed salary, according to his role (which is the case most of the times). However, what if I want to set a different salary for a specific employee?
Using simple_form I have so far written the following:
<%= f.input :name, label: 'Employee Name', :required => true %>
<%= f.association :role, as: :radio_buttons, :required => true %>
<%= f.input :salary, label: 'Employee Salary', :input_html => { :value => 0 }, :required => true %>
Which of course gives me a can't mass assign protected attributes: salary error.
To fix that, I added attr_accessible :salary to the Employee model but that just changed the error to unknown attribute: salary.
From what I understand I have to first change something in the new employee and then also in the employee model and controller so it accepts a value for the salary and knows how to handle it, right?
I 've also seen accepts_nested_attributes_for used but I am not entirely sure in which side of the association it should go - as I am not entirely sure the association is architectured in the best way either.
You need to add a salary column to your employees table if you are in fact wanting to allow a custom salary to be specified on the Employee. In your terminal, create a new migration and apply it
rails generate migration AddSalaryToEmployees salary:integer
RAILS_ENV=development rake db:migrate
By the way, you don't need to call attr_accessible multiple times; it accepts an arbitrary # of symbols
attr_accessible :name, :age, :role_id, :salary
Also, since you mentioned it, I'll comment on it: accepts_nested_attributes_for currently has no place in your models (given the code you've shown so far).
To answer the questions raised in your comment:
Isn't that duplication of code (having salary in both models I mean)?
No, they serve two different purposes. :salary in Role is the default salary applied to all Employees associated with that Role. :salary on Employee is an 'override' for special circumstances where an Employee's salary doesn't fit the mold of the Role they're associated with.
It wouldn't make sense to create a custom Role just for this purpose (assuming the custom salary is the only difference for the Employee)
You can't change the salary on the Role itself, because that would affect the salary of the other Employees associated with that Role
And doesn't that need another method (to make sure that the role salary is set as the employee's salary if none is specifically set)?
Another method? No. Customizing the existing attr_reader for salary on Employee to return the default from the Role if an 'override' hasn't been set? If you want
def salary
return role.salary if read_attribute(:salary).blank?
read_attribute(:salary)
end

option_groups_from_collection_for_select in single table

I need to group option-tags, but the groups and values are in the same table.
Lets say i have the model "Person" with the methods :age and :name and i want to group by age and list all names for the corresponding age. This would be something like:
option_groups_from_collection_for_select(#people, :name, :age, :id, :name, 1)
Obviously this does not work because :name is a single value. Is there a simple solution? Maybe by creating a method ":names" for Person, but how do i set the connection to :age?
-Bump (Noone an idea? Maybe i can just build a string with all those groups and options?)
I think your are missing the group_label_method parameter of the option_groups_from_collection_for_select method as shown here.
Try:
option_groups_from_collection_for_select(#people, :name, :name, :age, :id, :name, 1)

Resources