Simple association architecture and implementation in ruby on rails - 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

Related

Model failing to save

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.

Rails - Can't create a record that has an attribute with the same name of the class

I'm having troubles trying to create (or even update) a database record, because both the parent class name and the name of an attribute are equal. I'm using a consolidated database, so I can't alter the tables.
I have a class Period with an attribute 'period'. One period has many tax scales.
#period model
has_many :tax_scales,
foreign_key: 'period',
primary_key: 'period'
#tax_scale model
belongs_to :period,
foreign_key: :period,
primary_key: :period
So, when I use this in my tax_scale view:
#new.html.haml
= f.label :period
= f.number_field :period
I get this error:
Period(#97477176) expected, got String(#8598408)
My controller params are:
params.require(:tax_scale).permit(:minimum, :maximum, ..., :period)
How can I edit (or create) a tax_scale record succesfully?
I found the solution: use methods using self[:attribute], to be sure the attribute is accessed
#tax_scale model
def current_period
self[:period]
end
def current_period=(p)
self[:period] = p
end
#new.html.haml
= f.label :current_period
= f.number_field :current_period
Notice I have to use self[:period] and not self.period, because the second one is still accessing the parent record, not the attribute.

How to customize ActiveAdmin form for HABTM relationship in rails 4

I have two models Trip and Attraction for admin panel I have used ActiveAdmin. Now while creating new trip from activeadmin I wished to add user can select multiple Attractions and it's data should be stored in another table called trip_attractions (:trip_id , :attraction_id)
Here is my model:
class Trip < ActiveRecord::Base
has_and_belongs_to :attractions
end
class Attraction < ActiveRecord::Base
has_and_belongs_to_many :trips
end
class TripAttraction < ActiveRecord::Base
has_many :trips
has_many :attractions
end
and for ActiveAdmin form customization I tried in app/admin/trip.rb
ActiveAdmin.register Trip do
.....
form multipart: true do |f|
f.inputs "Trip" do
f.input :title
f.input :description
# Here I want to show all attractions and as multiple select it should be stored in trip_attractions table
f.input 'Attractions' do
f.input :attractions, :as => :select, :multiple => true, collection: Attraction.all
end
end
Where I do mistake? Please guide me how can I show all attractions on new trip form and on multiple select it should be stored in trip_attractions table. I really appreciate for your guideline and help. If you required more details then please feel free to ask.
In addition : I also tried to add attraction_ids field in trip table and in model I defined it as serialize to stored as an array of attraction's ids but I think it's not good approach as attraction may deleted it's id will remain in trip table. So I avoid this option and also not saved in database while submitting form. :(

How to design model with two, independent and optional relations to the same model?

I've got a problem with designing my User model and making a decent form for it. I just want to ensure myself that I'm doing it wrong :)
So it goes like this:
User has got two Addresses:
a mandatory Address for identification and billing,
an optional shipping Address that he could fill in or leave blank
I tried like this:
class User < ActiveRecord::Base
has_one :address
has_one :shipping_address, :class_name => 'Address', :foreign_key => 'shipping_address_id'
accepts_nested_attributes_for :address
accepts_nested_attributes_for :shipping_address
#validations for user
end
and:
class Address < ActiveRecord::Base
#validations for address
end
And then I make a form for User using form_for and nested fields_for. Like this:
= form_for #user, :url => '...' do |a|
= f.error_messages
...
= fields_for :address, #user.build_address do |a|
...
But then, despite that f.error_messages generates errors for all models, fields for Addresses don't highlight when wrong.
Also I have problems with disabling validation of the second address when the user chose not to fill it in.
And I have doubts that my approach is correct. I mean the has_one relation and overall design of this contraption.
So the question:
Am I doing it wrong? How would You do that in my place?
What is wrong in your form is that it will build a new address every time the view is rendered, thus losing all validation errors.
In your controller, in the new action you should do something like
#user.build_address
and in your view write:
= fields_for :address, #user.address do |a|
Hope this helps.

Save has_and_belongs_to_many link in basic RoR app

I try to learn the has_and_belongs_to_many relationship between my 2 fresh new and simple models Product and Author, where a Product can have many authors and where author can have a lots of products.
I wrote this :
class Author < ActiveRecord::Base
has_and_belongs_to_many :products
end
class Product < ActiveRecord::Base
has_and_belongs_to_many :authors
end
In the partial form of view for the products, I have :
<p>Products</p>
<%= collection_select(:product, :author_ids, #authors, :id, :name, :prompt => " ", :multiple => true) %>
but when I hit the update button, I get this strange message I can't resolve myself :
NoMethodError in ProductsController#update
undefined method `reject' for "1":String
Rails.root: /home/stephane/www/HABTM
Application Trace | Framework Trace | Full Trace
app/controllers/products_controller.rb:63:in block in update'
app/controllers/products_controller.rb:62:inupdate'
Request
Parameters:
{"utf8"=>"✓",
"_method"=>"put",
"authenticity_token"=>"2GlTssOFjTVZ9BikrIFgx22cdTOIJuAB70liYhhLf+4=",
"product"=>{"title"=>"Le trésor des Templiers",
"original_title"=>"",
"number"=>"1",
"added_by"=>"",
"author_ids"=>"1"},
"commit"=>"Update Product",
"id"=>"1"}
What's wrong ? Is there a problem with :product_ids... I saw on internet I had to pu a "s" but I'm not sure of what it represents....
How can I link the table authors_products to the key which is given back by the drop-down menu ? (here "author_ids"=>"1")
Thx !
More info :
May be solved with this info, but still no saving of the relationship :
collection_select("sales_agent", "customer_id", #customers, "id", "name")
Assuming you had a customer model with an ID attribute and a name attribute, this would produce exactly the above code. So looking at the values we pass into the collection_select call:
The first parameter is the model that contains one element from the collection (eg. sales_agent)
Next is the field name in the model that refers to the collection element it contains (eg. customer_id)
Next is the variable containing the collection of items that we want to list (eg. #customers)
Next is the value attribute of the option tag (eg. the customer id)
Next is the display attribute of the option tag (eg. the customer name)
So I now wrote
<p>Products</p>
<%= collection_select(:author, :author_id, #authors, :id, :name, :prompt => " ", :multiple => true) %>
and it worked, but without saving the link, for the moment... (only the update of the normal fields are saved, not the relationship :-(
Do you have a separate model called author_products for the HABTM relationship?
You'll need to run another migration by doing something like rails g model author_product and the table should only contain two fields:
belongs_to :author
belongs_to :product
Make sure there is no primary key.
Something like:
def self.up
create_table(:author_products), :id => false do |t|
t.references :author
t.references :product
end
end

Resources