Rails collection on form wanting extra (Tenant ID) field - ruby-on-rails

I have a dilemma...I have put together a multi-tenant app and I'm having an issue with one form and action. The form (Profile) has a collection select where one selects the skills applicable. There is a many-to-many with Profile <- Taggings -> Tags. The Tag records appear in the collection for select, with multi-select enabled.
When you select some skills and save the profile record, in the profile update action, Rails throws:
Validation failed: Tenant must exist
This seems to be coming from the Taggings table, as when I remove the relationship with Tenant, no error and the profile record saves successfully. If I create a tagging record directly from Rails console, the tenant_id populates and the record saves.
respond_to do |format|
if #profile.update(profile_params) < fails here
format.html { redirect_to profiles_path, notice: 'Profile was successfully updated.' }
format.json { render :show, status: :ok, location: #profile }
else
Params (if I must insert tenant_id here, I expect within the tag id array, but how?):
{"utf8"=>"✓",
"_method"=>"patch",
"authenticity_token"=>"longobscurestring",
"profile"=>
{"description"=>"",
"tag_ids"=>
["",
"b39b38eb-a90b-434b-9457-1f3b67cee54e",
"08d90ee7-3194-4fec-acee-bcecfae1e8e7",
"ee8de96d-1206-4d73-bcf0-0b99f995569a",
"469ce954-b2bd-49d5-9dbc-0636b4da43c8",
"38b90691-d3f0-4c9d-8b5f-2c644a894d45",
"77a332d9-feed-4f88-8133-19066b5d33bc",
"05c145ce-a8ff-4105-a713-073da60184b5",
"8d6f98e3-9c3e-4f45-8c7d-36b177b557af"],
"contact_direct"=>"false"},
"commit"=>"Save",
"id"=>"1728fcc4-f2e2-49de-9a39-5c67502b8a85"}
Profile form:
<%= simple_form_for #profile, url: profile_path do |f| %>
<%= f.input :description, :as => :text, :input_html => {:cols=>100, :rows=>5} %>
<%= f.input :tag_ids, label: 'Skills', as: :select, collection: Tag.active.order(:name), label_method: :name, input_html: { multiple: true } %>
<div>
<span>People may contact me:</br></span>
<div class="radio-inline margin-10" style="text-align:left">
<%= f.input :contact_direct, label: '', as: :radio_buttons, collection: [['Directly', true], ['Through my manager', false]] %>
</div>
</div>
<br/>
<div class="actions">
<% if ( can? :manage, :all or current_user.id == #profile.user_id) %>
<%= f.button :submit, 'Save', class: "btn btn-success" %>
<% end %>
<%= link_to "Back", profiles_path(:search => {:column => 'First', :direction => 'Up', :name => ''}), class: "btn btn-primary link-as-button" %>
</div>
<% end %>
Profile model associations:
class Profile < ApplicationRecord
belongs_to :user
belongs_to :tenant
has_many :taggings, :dependent => :destroy
has_many :tags, through: :taggings
Taggings model associations:
class Tagging < ApplicationRecord
attribute :competence, :integer, default: 0
belongs_to :tenant
belongs_to :tag
belongs_to :profile, optional: true
has_one :user, through: :profile
has_many :endorsements, inverse_of: 'tagging'
has_many :endorsers, through: :endorsements
All tables have had RLS implemented through pg policies. Without tenant_id on the taggings record though, one can access the record through another tenant.
Please let me know anything else required here to debug. Thankyou in advance!

I managed to solve this by adding into the Tagging model an after_initialize callback to the following:
def set_tenant_id
if new_record?
self.tenant_id = self.tag.tenant_id
end
end
Feel free to say if this is bad practice...but I can't see any other way at present.

Related

Rails 5 - find_or_create_by with nested attributes

I'm trying to create a new object with its associated records in the same form but would like the associated records to use find_or_create_by instead of just create (as the associated model records may and most of the time already will exist). I have spent the last two days digging through every post and article that I can find related to this subject trying to get this to work but still my form tries to create the new object only, not search for existing.
Models
#order.rb
has_many :order_owners, dependent: :destroy, inverse_of: :order
has_many :owners, through: :order_owners
accepts_nested_attributes_for :order_owners
#owner.rb
has_many :order_owners, dependent: :destroy
has_many :orders, through: :order_owners
validates :name, presence: true, uniqueness: { case_sensitive: false }
#order_owner.rb
belongs_to :owner
belongs_to :order
accepts_nested_attributes_for :owner
Form
orders/new.html.erb
<%= bootstrap_form_for(#orders, layout: :horizontal, label_col: "col-sm-2", control_col: "col-sm-6") do |f| %>
<%= render 'shared/error_messages', object: f.object %>
...
<%= f.fields_for :order_owners do |orderowner| %>
<%= render 'orders/new_order_owners', f: orderowner, render_partial: 'orders/new_order_owners' %>
<% end %>
...
<%= f.form_group do %>
<%= f.submit "Create Order", class: "btn btn-primary" %>
<%= link_to_add_association fa_icon("plus", text: "Add Owner"), f, :order_owners,
class: "btn btn-outline pull-right #{orderBtnDisable(#properties)}", partial: "orders/new_order_owners", id: "newOrderOwnerAdd" %>
<% end %>
<% end %>
orders/new_order_owners partial
<div class="m-t-md m-b-md border-bottom form-horizontal nested-fields">
<%= link_to_remove_association(fa_icon("remove", text: ""), f, { class: "btn btn-danger btn-outline pull-right" }) %>
<% f.object.build_owner unless f.object.owner %>
<%= f.fields_for :owner do |owner| %>
<%= owner.select :name, options_from_collection_for_select(#owners, "name", "name"),
{ label: "Name:", include_blank: true }, { id: "orderPropOwnerSelect", data: { placeholder: "Select an existing Owner or type a new one.."} } %>
<% end %>
</div>
Controller
orders/new
def new
#order = Order.new
#order.build_property
#order.order_owners.build.build_owner
#properties = Property.find_by_id(params[:property_id])
if #properties
#owners = #properties.owners
else
#owners = []
end
respond_to do |format|
format.html
format.js
end
end
orders/create
def create
#properties = Property.find(params[:order][:property_id])
#order = #properties.orders.create(order_params)
respond_to do |format|
format.html { if #order.save
if params[:order][:owners_attributes]
order_prop_owner_check(#order, #properties)
end
flash[:success] = "Order created successfully!"
redirect_to property_order_path(#properties, #order)
else
#properties
#owner = #properties.owner
render 'new'
end
}
format.js {
if #order.save
flash.now[:success] = "Order Updated Successfully!"
else
flash.now[:danger] = #order.errors.full_messages.join(", ")
end
}
end
end
So as you can see in the new action, I instantiate the new Order, build its associated property (its what the Order belongs_to), build the new order_owner relationship, and build the owner for that relationship. Then on submit it creates the order via #properties.orders.create(order_params).
The error that I get is "Order owners owner name already exists." so clearly its not looking up an owner by name. I have tried:
Redefining autosave_associated_records_for_owner in order.rb and order_owner.rb, using both belongs_to and has_many variations, but it seems like they never get called so I must be doing something wrong. (I have tried variations of almost every answer I could find on SO)
before_add: callback on both has_many :owners, through: :order_owners and has_many :order_owners in order.rb.
Extending has_many :owners and has_many :owners, through: :order_owners in order.rb as well as belongs_to :owner in order_order.rb
I've also tried different variations of calling associations and such within the form so I must be just misunderstanding something. I'm also using Cocoon to manage the nested forms but I've talked to the author on unrelated issues and Cocoon is essentially just a view helper for nested forms so the solution must something in the models/controller.
Any and all ideas welcome. Thanks in advance.
P.s. I left code in the controller actions that may/may not pertain to this exact post but I wanted to show the entire action for completeness. If it matters, I manually set the owners select via AJAX when a property is selected in another field. Basically it just looks up the property and adds existing owners to the owners select.
The owner is nested inside the order. So when you call order.save, it runs all validations (including owner's). If you want to use find_or_create_by you need to do it inside a before_save, so you can make modifications to the owner before the validation hits.
#order.rb
before_save :find_or_create_owner
def find_or_create_owner
self.order_owners.each do |order_owner|
order_owner.owner = Owner.find_or_create_by(name: final_owner.name)
end
end
Further customization may be needed depending on your form and business logic, but that's the main concept.

Rails Mass Assignment

I have a site that keeps track of SAT tutoring sessions. The curriculum that the students learn is a collection of rules. I have a model for each tutoring session called "Sittings" and the rules model is called "Rules". I want the site admin to be able to enter a Sitting by date, and then use checkboxes to select which "rules" the student got wrong in that sitting. I'm a little confused as to how I can create the form to pull out specific rules without adding attributes to my Sitting model of rule1, rule2, etc. I'm using simple_form to create my forms.
My Sitting model:
class Sitting < ActiveRecord::Base
attr_accessible :date, :comment, :rule_id, :user_id
validates :date, presence: true
belongs_to :user
has_many :combos
has_many :rules, :through => :combos
end
My Rules model:
class Rule < ActiveRecord::Base
attr_accessible :name, :subject, :session_id, :hint_id, :question_id, :trigger_id
validates :name, presence: true
validates :subject, presence: true
has_many :questions
has_many :triggers
has_many :hints
has_many :combos
has_many :sittings, :through => :combos
end
My Combo model:
class Combo < ActiveRecord::Base
belongs_to :sitting
belongs_to :rule
end
Edit:
Here's what I have tried for the form. It does create the checkbox form, but my DB isn't updating the rule_id. (shows as nil when I create a Sitting)
form:
<%= simple_form_for(#sitting, html: { class: "form-horizontal"}) do |f| %>
<%= f.error_notification %>
<% Rule.all.each do |rule| %>
<%= check_box_tag "sitting[rule_ids][]", rule.id, #sitting.rule_ids.include?(rule.id) %> <%= rule.id %>
<% end %>
<div class="form-group">
<%= f.input :comment, as: :text, input_html: { rows: "2", :class => "form-control" }, label: "Comments:" %>
</div>
<div class="form-group">
<%= f.date_select :date, as: :date, label: "Taken Date:" %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
I updated my strong params to allow an array:
def create
#sitting = Sitting.new(sitting_params)
respond_to do |format|
if #sitting.save
format.html { redirect_to #sitting, notice: 'Sitting was successfully created.' }
format.json { render action: 'show', status: :created, location: #sitting }
else
format.html { render action: 'new' }
format.json { render json: #sitting.errors, status: :unprocessable_entity }
end
end
end
def sitting_params
params.require(:sitting).permit(:comment, :date, :user_id, :rule_id => [])
end
Am I missing something in order to update the Sitting.rule_id properly? I get the following error in my logs:
WARNING: Can't mass-assign protected attributes for Sitting: rule_ids
app/controllers/sittings_controller.rb:27:in `create'
Just to summarize what we got through over the chat.
Firstly, you do not need both attr_accessible and strong_params at the same time. I've posted another answer some time ago explaining how those two approaches differs from each other.
You're running rails 4, so you should take advantage of strong params and not use protected_attributes gem. In short, remove this gem from you Gemfile as well as all attr_accessible calls.
As Marian noticed, you have a typo in your strong params method, you need to permit rule_ids, not rule_id. rule_id column is obsolete, as sitting has_many :rules :through rather than sitting belongs_to :rule - most likely it is an artifact of old association code.
As soon as rule_ids are assigned within your model, it will create new join models in your join table, creating an association between given sitting and passed rules.

Create and update with nested models using strong parameters Rails

Here are my 3 models.
User
has_many :memberships
has_many :teams, through: :memberships, dependent: :destroy
accepts_nested_attributes_for :memberships
Team
has_many :memberships
has_many :users, through: :memberships, dependent: :destroy
accepts_nested_attributes_for :memberships
Membership
belongs_to :team
belongs_to :user
Here are some portions of my Team controller. My objective here is to add/update members to a certain team. Note that the source for adding members already exists as a group of users.
TeamsController
def create
#team = Team.new(team_params)
#team.users << User.find(member_ids) #add leader and members to team
if #team.save
#redirect to created team
else
#show errors
end
end
def update
#TO DO: update team roster here
if #team.update(team_params)
#redirect to updated team
else
#show errors
end
end
Strong parameters for Team controller
#parameters for team details
def team_params
params.require(:team).permit(:name, :department)
end
#parameters for members (includes leader)
def members_params
params.require(:team).permit(:leader, members:[])
end
#get id values from members_params and store in an array
def member_ids
members_params.values.flatten
end
For the form, I only have:
Name (text field)
Department (combo box)
Leader (combo box, with options generated depending on the selected department, submits as a selected user's user id)
Members (combo box, multiple, with options generated depending on the selected department, submits as an array of selected users' user ids)
I can successfully create a team, together with the passing of validations (both team and membership model), on my create. However, I have no idea on how to update the team, because if I use #team.users.clear and then simply do the same thing from create (I know, it's a bit stupid to do this), it will validate, but it will save it regardless if there's an error or not.
FORM CODE
<%= form_for(#team, remote: true) do |f| %>
<%= f.label :name, "Name" %>
<%= f.text_field :name %>
<%= f.label :department, "Department" %>
<%= f.select :department, options_for_select(["Architectural", "Interior Design"], department), include_blank: true %>
<%= f.label :leader, "Leader" %>
<%= f.select :leader, select_leaders(department, #team.id), {include_blank: true, selected: pre_select(:leader)} %>
<%= f.label :members, "Members" %>
<%= f.select :members, select_members(department, #team.id), {include_blank: true}, {id: "team_members", multiple: :multiple, data: {member_ids: pre_select(:members)}}%>
<% end %>
Note for the form:
This form works for both blank and populated forms.
The :members field is select2 enabled.
So my questions here are:
How can I update members of team? Is it possible to update based from what my strong parameters currently have, or do they need to be revised?
Should my create method be revised too?
SOME OPTIONS LEADING TO SOLUTION
Option #1 (best solution so far)
I only did a first-aid solution for this, so I think there's a better approach than what I did below. What I did here is to create users params with the users found from the member_ids as values.
TeamsController
def create
team = Team.new(team_params.merge({users: User.find(member_ids)}))
...
end
def update
...
if #team.update(team_params.merge({users: User.find(member_ids)}))
..
end
Option #2
Independent from solution 1, I only had team_params as strong parameter.
TeamsController
...
private
def team_params
params.require(:team).permit(:name, :department, :leader, members:[])
end
I created setter methods for both leader and members. But it seems that members overwrites the leader setter because I used the update method for both setters, and the update uses the same resource which is users. A workaround seems to be possible with this option.
Team
...
def leader=(leader_id)
#self.update(users: User.find(leader_id))
end
def members=(members_ids)
#self.update(users: User.find(members_id))
end
Since, leader and members are not so different in your scenario. You can change your models and view form to something like this:
class Team < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships, dependent: :destroy
accepts_nested_attributes_for :memberships
end
class User < ActiveRecord::Base
has_many :memberships
has_many :teams, through: :memberships, dependent: :destroy
end
class Membership < ActiveRecord::Base
belongs_to :team
belongs_to :user
accepts_nested_attributes_for :user
end
and form view code:
<%= form_for(#team) do |f| %>
<% if #team.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#team.errors.count, "error") %> prohibited this team from being saved:</h2>
<ul>
<% #team.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %>
</div>
<%= f.fields_for :memberships do |m| %>
<div class="field">
<%= m.label :memberships_name %><br>
<%= m.text_field :name %>
</div>
<%= m.fields_for :user do |u| %>
<div class="field">
<%= u.label :user_name %><br>
<%= u.text_field :name %>
</div>
<% end %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Also, please make sure you change this in your controller:
# GET /teams/new
def new
#team = Team.new
3.times do # number of members you need to generate!
#team.memberships.build{ |m| m.build_user }
end
end
# GET /teams/1/edit
def edit
end
# POST /teams
# POST /teams.json
def create
#team = Team.new(team_params)
respond_to do |format|
if #team.save
format.html { redirect_to #team, notice: 'Team was successfully created.' }
format.json { render action: 'show', status: :created, location: #team }
else
format.html { render action: 'new' }
format.json { render json: #team.errors, status: :unprocessable_entity }
end
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_team
#team = Team.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def team_params
params.require(:team).permit(:name, memberships_attributes: [:id, :name, user_attributes: [:id, :name]])
end
Although you can do this in Rails console to do a quick code validation:
team_params = {"name"=>"Team", "memberships_attributes"=>{"0"=>{"name"=>"Membership 1", "user_attributes"=>{"name"=>"User 1"}}, "1"=>{"name"=>"Membership 2", "user_attributes"=>{"name"=>"User 2"}}, "2"=>{"name"=>"Membership 3", "user_attributes"=>{"name"=>"User 3"}}}}
team = Team.new(team_params)
team.save
team.users
#=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 1, name: "User 1", email: nil, created_at: "2014-09-04 11:25:48", updated_at: "2014-09-04 11:25:48">, #<User id: 2, name: "User 2", email: nil, created_at: "2014-09-04 11:25:48", updated_at: "2014-09-04 11:25:48">, #<User id: 3, name: "User 3", email: nil, created_at: "2014-09-04 11:25:48", updated_at: "2014-09-04 11:25:48">]>
I hope it helps.

reject_if empty? not working - table row not deleted when field is empty

I would appreciate your help on this:
I have 2 models:
class Author < ActiveRecord::Base
has_one :authornote, :foreign_key => "author_fk", :dependent => :destroy
accepts_nested_attributes_for :authornote, :reject_if => lambda { |a| a[:note_value].blank? }, :allow_destroy => true
end
class Authornote < ActiveRecord::Base
belongs_to :author, :foreign_key => :author_fk
end
In my form, i have got the following field_for:
<% remote_form_for #author, :url => { :controller => "authors", :action => "update_author_note" } do |f| %>
<div>
<% f.fields_for :authornote do |builder| %>
<%= builder.text_area :note_value, :rows => 4, :cols => 50 %>
<% end %>
</div>
<%= f.submit 'Update' %>
<%end%>
Controller code:
def update_author_note
author_id = session[:author_id]
#author = Author.find(author_id)
if #author.update_attributes(params[:author])
respond_to do |format|
format.js
end
else
respond_to do |format|
format.html { redirect_to add_author_note_path(:sub_id=> session[:sub_id]) }
end
end
end
Currently, if a user removes all the contents in the form field 'note_value' and update the form, the data row is still present in the 'authornote' table, with the column 'note_value' empty.
What i would like is that, if field 'note_value' is empty and user clicks on the update button, i would like the row with the empty column 'note_value' to be deleted in the 'authornote' table.
Any suggestion is most appreciated
What you're trying to do is not supported by Rails as you're trying to do it. In order for a record to be deleted through accepts_nested_attributes_for you have to pass _delete: true along with the ID of the record to be deleted. When you reject the nested records that don't have a note_value there's no way for Rails to know you're trying to update those records—they're just missing from the params entirely.

Rails 3, nested multi-level forms and has_many through

I'm trying to get it to work but it dosen't!
I have
class User < ActiveRecord::Base
has_many :events, :through => :event_users
has_many :event_users
accepts_nested_attributes_for :event_users
end
class Event < ActiveRecord::Base
has_many :event_users
has_many :users, :through => :event_users
accepts_nested_attributes_for :users
end
class EventUser < ActiveRecord::Base
set_table_name :events_users
belongs_to :event
belongs_to :user
accepts_nested_attributes_for :events
accepts_nested_attributes_for :users
end
And also the table-layout
event_users
user_id
event_id
user_type
events
id
name
users
id
name
And this is my form
<%= semantic_form_for #event do |f| %>
<%= f.semantic_fields_for :users, f.object.users do |f1| %>
<%= f1.text_field :name, "Name" %>
<%= f1.semantic_fields_for :event_users do |f2| %>
<%= f2.hidden_field :user_type, :value => 'participating' %>
<% end %>
<% end %>
<%= link_to_add_association 'add task', f, :users %>
<% end %>
The problem is that if I create a new user this way, it doesn't set the value of user_type (but it creates a user and a event_users with user_id and event_id). If I go back to the edit-form after the creation of a user and submit, then the value of user_type is set in events_users. (I have also tried without formtastic)
Any suggestions? Thanks!
----edit----
I have also tried to have the event_users before users
<%= semantic_form_for #event do |f| %>
<%= f.semantic_fields_for :event_users do |f1| %>
<%= f1.hidden_field :user_type, :value => 'participating' %>
<%= f1.semantic_fields_for :users do |f2| %>
<%= f2.text_field :name, "Name" %>
<% end %>
<% end %>
<%= link_to_add_association 'add task', f, :event_users %>
<% end %>
but then it only throws me an error:
User(#2366531740) expected, got
ActiveSupport::HashWithIndifferentAccess(#2164210940)
--edit--
the link_to_association is a formtastic-cocoon method (https://github.com/nathanvda/formtastic-cocoon) but I have tried to do other approaches but with the same result
---edit----
def create
#event = Event.new(params[:event])
respond_to do |format|
if #event.save
format.html { redirect_to(#event, :notice => 'Event was successfully created.') }
format.xml { render :xml => #event, :status => :created, :location => #event }
else
format.html { render :action => "new" }
format.xml { render :xml => #event.errors, :status => :unprocessable_entity }
end
end
end
To be honest, i have never tried to edit or create a has_many :through in that way.
It took a little while, and had to fix the js inside formtastic_cocoon to get it working, so here is a working solution.
You need to specift the EventUser model, and then fill the User model (the other way round will never work).
So inside the models you write:
class Event < ActiveRecord::Base
has_many :event_users
has_many :users, :through => :event_users
accepts_nested_attributes_for :users, :reject_if => proc {|attributes| attributes[:name].blank? }, :allow_destroy => true
accepts_nested_attributes_for :event_users, :reject_if => proc {|attributes| attributes[:user_attributes][:name].blank? }, :allow_destroy => true
end
class EventUser < ActiveRecord::Base
belongs_to :user
belongs_to :event
accepts_nested_attributes_for :user
end
class User < ActiveRecord::Base
has_many :events, :through => :event_users
has_many :event_users
end
Then the views. Start with the events/_form.html.haml
= semantic_form_for #event do |f|
- f.inputs do
= f.input :name
%h3 Users (with user-type)
#users_with_usertype
= f.semantic_fields_for :event_users do |event_user|
= render 'event_user_fields', :f => event_user
.links
= link_to_add_association 'add user with usertype', f, :event_users
.actions
= f.submit 'Save'
(i ignore errors for now)
Then, you will need to specify the partial _event_user_fields.html.haml partial (here comes a little bit of magic) :
.nested-fields
= f.inputs do
= f.input :user_type, :as => :hidden, :value => 'participating'
- if f.object.new_record?
- f.object.build_user
= f.fields_for(:user, f.object.user, :child_index => "new_user") do |builder|
= render("user_fields", :f => builder, :dynamic => true)
and to end the _user_fields partial (which does not really have to be a partial)
.nested-fields
= f.inputs do
= f.input :name
This should work.
Do note that i had to update the formtastic_cocoon gem, so you will need to update to version 0.0.2.
Now it would be easily possible to select the user_type from a simple dropdown, instead of a hidden field, e.g. use
= f.input :user_type, :as => :select, :collection => ["Participator", "Organizer", "Sponsor"]
Some thoughts (now i proved it works):
this will always create new users on the fly, actually eliminating the need for the EventUser. Will you allow selecting existing users from a dropdown too?
personally i would turn it around: let users assign themselves to an event!
Does the events_users model not have an ID column? Since there's an additional field (user_type) then EventUser is a model and should probably have an ID. Maybe that's why user_type isn't being set in your first case.

Resources