save association in pivot table in rails 5 - ruby-on-rails

I have a common many to many relationship, these are the models:
class Employee < ApplicationRecord
has_many :related_professions
has_many :professions, through: :related_professions
accepts_nested_attributes_for :related_professions
end
class RelatedProfession < ApplicationRecord
belongs_to :employee
belongs_to :profession
accepts_nested_attributes_for :profession
end
class Profession < ApplicationRecord
has_many :related_professions
has_many :employees ,through: :related_professions
end
I also have a form for saving Employees. In this form I would like to render all the Professions in a multiple select for the user to choose as needed. I want that when the user submits the form, the IDs of all the selected professions be saved in the RelatedProfession pivot table (which just have three columns: id, employee_id, profession_id). This is the part of my form for the select:
<div class="field">
<%= form.label :professions %>
<%= form.fields_for :related_professions do |rp| %>
<%= rp.collection_select :profession_id, Profession.all, :id, :name, {}, {multiple: true} %>
<% end %>
</div>
And this is the part in my EmployeeController that allows the parameters:
# Never trust parameters from the scary internet, only allow the white list through.
def employee_params
params.require(:employee).permit(:name, related_professions_attributes: [:id, profession_id: [:id]])
end
The first problem is that the form does not load the Professions if the Employee does not have any assigned. I had to manually add one to the DB and then it would populate the select.
Second problem is that when I try to update the Employee (and also the RelatedProfession pivot table) by selecting a different Profession, it won't work, and I get this error:
Related professions profession must exist
I know there must be something wrong in the permit parameters and form that is not building the select correctly.
I appreciate the help. Thanks in advance.

You no need nested attributes to created has_many through relations,
You can just pass it as array of ids.
class Employee < ApplicationRecord
has_many :related_professions
has_many :professions, through: :related_professions
end
class RelatedProfession < ApplicationRecord
belongs_to :employee
belongs_to :profession
end
class Profession < ApplicationRecord
has_many :related_professions
has_many :employees ,through: :related_professions
end
In form also just select ids of Professions.
<div class="field">
<%= form.label :professions %>
<%= rp.collection_select :profession_ids, Profession.all, :id, :name, {}, {multiple: true} %>
</div>
change strong params to allow profession_ids as array.
def employee_params
params.require(:employee).permit(:name, profession_ids: [])
end
Hope this solves your problem.

Related

Rails: Structuring rental orders & order form in ecommerce-like setting

I was wondering if someone could help me out with an application that has some ecommerce characteristics.
Context: Via the application a bike shop chain ('chains') can rent out
bikes ('bikes'),
by picking out a bike type such as mountainbike, city bike etc. ('bike_types) and
bike options, such as helmets etc. ('bike_options')
which are dependent on the individual bike store ('bike_stores')
this rental of the bikes & options will all be captured in an order ('orders')
the relationship between orders and bikes is many-to-many, therefore I created a table to bridge this ('order_bikes')
Final notes:
Before the rental process, the chain owner first created his/her (i) bike_stores, (ii) bike_types, (iii) bikes and (iv) bike_options, this part of the application is working. Therefore, he/she only needs to select bike_types/bikes/options out of the existing inventory previously created.
I limit the scope of the question by leaving out the bike_options, this was mainly to provide some context in order to understand the db schema build up.
Error message: Unpermitted parameter: :bike_id
Code:
models
class Order < ApplicationRecord
belongs_to :bike_store
has_many :bike_types, through: :bike_store
has_many :order_bikes, inverse_of: :order, dependent: :destroy
accepts_nested_attributes_for :order_bikes, allow_destroy: true
end
class OrderBike < ApplicationRecord
belongs_to :bike
belongs_to :order
accepts_nested_attributes_for :bike
end
class Bike < ApplicationRecord
belongs_to :bike_type
validates :name, presence: true
has_many :order_bikes
has_many :orders, through: :order_bikes
end
class BikeType < ApplicationRecord
belongs_to :bike_store
has_many :bikes, dependent: :destroy
accepts_nested_attributes_for :bikes, allow_destroy: true
has_many :bike_options, dependent: :destroy
accepts_nested_attributes_for :bike_options, allow_destroy: true
validates :name, :bike_count, presence: true
end
class BikeStore < ApplicationRecord
has_many :bike_types, dependent: :destroy
has_many :orders, dependent: :destroy
end
Order controller
class OrdersController < ApplicationController
def new
#bike_store = BikeStore.find(params[:bike_store_id])
#order = Order.new
#order.order_bikes.build
#bike_type_list = #bike_store.bike_types
end
def create
#order = Order.new(order_params)
#bike_store = BikeStore.find(params[:bike_store_id])
#order.bike_store = #bike_store
#order.save
redirect_to root_path
end
private
def order_params
params.require(:order).permit(:arrival, :departure,
order_bikes_attributes: [:id, :bike_quantity, :_destroy,
bikes_attributes: [:id, :name,
bike_types_attributes: [:id, :name]]])
end
end
view
<%= simple_form_for [#bike_store, #order] do |f|%>
<%= f.simple_fields_for :order_bikes do |order_bike| %>
<%= order_bike.input :bike_quantity %>
<%= order_bike.association :bike %>
<% end %>
<%= f.input :arrival %>
<%= f.input :departure %>
<%= f.submit %>
<% end %>
If you check coed from simple form here, you will see what actually method association does.
def association(association, options = {}, &block)
# ... simple form code here ...
attribute = build_association_attribute(reflection, association, options)
input(attribute, options.merge(reflection: reflection))
end
We are interested in build_association_attribute method call. here
def build_association_attribute(reflection, association, options)
case reflection.macro
when :belongs_to
(reflection.respond_to?(:options) && reflection.options[:foreign_key]) || :"#{reflection.name}_id"
# ... the rest of code ...
end
end
Your order bike model has belongs_to :bike association. So when you call order_bike.association :bike it builds :bike_id attribute in your form. If you check params hash that comes to your controller, I believe you'll see that attribute coming from your view.
I added bike_id to permitted parameters. I hope it will fix your problem..
def order_params
params.require(:order).permit(:arrival, :departure,
order_bikes_attributes: [:id, :bike_id, :bike_quantity, :_destroy,
bikes_attributes: [:id, :name,
bike_types_attributes: [:id, :name]]])
end

Setting limitations across has_many through models

Those are my models:
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, through: :memberships
end
class Group < ActiveRecord::Base
has_many : memberships
has_many :users, through: :memberships
belongs_to :group_type
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
class GroupType < ActiveRecord::Base
has_many :groups
end
I would like to assign groups to users. there are several types of groups, users can choose only one group from each type. in the view there should a select tag for each group type (a user don't have to be a member of each of the group types).
I can add a type_id column to the membership model and create a instance for each group for each new user, and then update rows, with something like (for type 1):
<%= form_for (#user) do |f|
<% #membership = Membership.where (user_id: :#user.id, group_type: :1) %>
<%= f.collection_select (:membership, :group_id, Group.all.where(type: 1), :id, :name) %>
<% end %>
(not sure that it will work)
anyway I'm sure there is a better way.. how can I connect the models with the one group for each type limitation? (with, ofcourse, a way to edit existing memberships).
I would appriciate any assistance...
Thank you!
Your model structure look good. You could also include the group_type in the membership and than validate against uniqueness.
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
belongs_to :group_type
validates :user_id, uniqueness: { scope: [:group_id, :group_type_id],
message: "can only join one group of this type." }
end
The form should create a membership.
<%= form_for (Membership.new) do |f|
<%= f.hidden_field :user_id, value: #user.id %>
<%# your selection for a group %>
<% end %>
In the controller you have to load the group from the DB, than add the group_type_id to params and .save -> The validation will handle the rest.

Using join model with nested form (has_many :through relationship)

I have a Form_for for a Show model. I would like to use a fields_for within the form_for to add bands. The thing is I don't want the fields tied to the bands when using the form to update records. If the name of the band changes I would like to update the performance with the new band.
Shows are joined with Bands through Performances
class Show < ActiveRecord::Base
has_many :performances
has_many :bands, through: :performances
accepts_nested_attributes_for :bands
end
class Band < ActiveRecord::Base
attr_accessible :name, :website, :country, :state
has_many :performances
has_many :shows, through: :performances
validates :name, presence: true, uniqueness: true
end
class Performance < ActiveRecord::Base
attr_accessible :show, :band
belongs_to :show
belongs_to :band
end
Here is my form. (simplified)
<%= form_for #show do |f| %>
#fields
<%= f.fields_for :bands do |b| %>
<%= b.text_field :name %>
<% end %>
<%end>
The problem is if this is used to change a bands name, it changes the bands name (crazy right?). I don't want it to update the Band record-- I want it to do a Band.find_or_create and update the performance record with the new band's id. This way users can replace a band in a show by removing the name and adding in another band name.
The rendered html should include the performance id not the band id (I think)
Something like:
<input id="show_performance_attributes_1_id" name="show[performance_attributes][1][id]" type="hidden" value="62">
How is this done?
Ok, so I was able to find the solution to my own question. I may have not provided enough details in the original question.
But the solution is simply to use the performance model as the nested field in the Fields_for instead of the bands model. Change the show model to accepts_nested_attributes_for performances and change the performance model to accepts_nested_attributes_for band

Data Modeling Structure - Rails

I have a project-specific question. I'm trying to determine the most efficient and logical way of setting up my models in my app. The relevant players to model here are: Vendors, Vendor Inventories, and Products. Here is a breakdown of what each item should be able to return in some way:
Inventories:
Store ID
Products and associated details (Price, Name, Brand, Details, Product Code)
Vendors: Store ID, Location, Name
Products: Price, Name, Brand, Details, Product Code
Obviously there is a lot of duplication in this scheme between products and inventories. My issue is that while vendors might have similar items in their inventories, the price will always be different. So I can't simply relate the models by product code. Because most vendors will have the same products, if I were to model store inventories with all of the product information, wouldn't that be a lot of duplication? It's also possible that I don't need to have a separate model for vendors and could just try and keep it all inventories, but I'm lost. Help please?! Thanks in advance.
EDIT:
Here is my model structure, though I'm not sure it's ideal.
class Vendor < ActiveRecord::Base
attr_accessible :name, :address
has_one :inventory
has_many :products, through: :inventories
end
class Inventory < ActiveRecord::Base
has_many :products
belongs_to :vendor
end
class Product < ActiveRecord::Base
attr_accessible :upc, :brand, :product, :details, :price
has_many :inventories
has_many :vendors, through: :inventories
end
I'm not sure if I understand your specific needs, especially if the set of models are just for example, but judging from the names, here would be my implementation.
A Vendor is the main entity, it will own many products through the join table inventory,
class Vendor < ActiveRecord::Base
attr_accessible :name, :address
has_many :inventories
has_many :products, through: :inventories
end
However there are aspects of the product that need to differ between Vendors, in this case, price and quantity( I just made that up), which can be stored in the join table, since it would be independent and made for each product and vendor relationship, is ideal for storing info like price and quantity.
class Inventory < ActiveRecord::Base
attr_accessible :product_id, :vendor_id, :price, :quantity
belongs_to :product
belongs_to :vendor
end
Product should only have attributes that would be universal regardless how the vendor handles it, likewise if the product where to be changed, it should be okay with all the vendors.
class Product < ActiveRecord::Base
attr_accessible :upc, :brand, :product, :details
has_many :inventories
has_many :vendors, through: :inventories
end
Doing this you may have difficulties editing this in the view, setting prices and quantity on inventory. Again, my way of doing it would be to use nested fields and interact directly with the join table, and not products, here's some quick partial code using simple_form gem,
on you form,
<%= f.simple_fields_for :inventories do |inventory_fields| %>
<%= render partial: "inventory_fields", locals: {f: inventory_fields }%>
<% end %>
<%= link_to_add_fields "Add", f, :f %>
and in inventory_fields.html.erb
<%= link_to "Remove", '#', class: "remove_fields btn btn-danger" %>
<%= f.input :product_id, collection: Product.all.somethingsomething, include_blank: false %>
<%= f.input :price %>
<%= f.input :quantity %>
Now you can choose your products from a dropdown, and set price and quantity, and whatever you need dynamically.

collection_select with has_many :through relationship table data

I have such terrible models:
class ParentalRelation < ActiveRecord::Base
belongs_to :parent
belongs_to :student
belongs_to :counselor
belongs_to :parental_relation_type
end
class ParentalRelationType < ActiveRecord::Base
has_many :parental_relations
end
class Parent < ActiveRecord::Base
has_many :parental_relations
has_many :students, :through => :parental_relations
has_many :counselors, :through=> :parental_relations
has_many :parental_relation_types, :through=> :parental_relations
belongs_to :user, :dependent=> :destroy
belongs_to :occupation_type
accepts_nested_attributes_for :user
end
Parental relation types are like father, mother, etc. The reasoning is that a parental relation between one counselor, one parent and one student is unique and counselors should not see the relations that belong other counselors.
In controllers/parent_controller/edit action I have:
#parental_relation= ParentalRelation.find_by_counselor_id_and_student_id_and_parent_id(x, y, z)
In views/parent/_form.html.erb I have:
<%= form_for #parent do |f| %>
inside that form I need a collection_select for ParentalRelationType.all and select the parent's parental_relation_type_id for that particular parental relation, but I can't find the syntax to do it.
I tried adding
<%= collection_select(#parental_relation, :parental_relation_type_id, ParentalRelationType.all, :id, :name) %>
underneath the form_for, but the relation type id is 2, and default 1 is selected instead.
Added this to parents/_form
<%= fields_for #counselor_student_parent do |csp| %>
<%= f.label :parental_relation_type_id %>
<%= collection_select(:student_counselor_parent, :parental_relation_type_id, ParentalRelationType.all, :id, :name) %>
<% end %>
And this to parents_controller/new
def new
#counselor= Counselor.find(params[:counselor_id])
#student= Student.find(params[:student_id])
#parent= #student.parents.build
#parent_user= #parent.build_user
#counselor_student_parent= #counselor.student_counselor_parents.build
end

Resources