How to make an update on a Nested form in ruby? - ruby-on-rails

In my application, I have a entity called User, that has one Talent. A talent is a kind a user can be on my system (a model, a photographer, a videomaker, a client)
#user.rb
class User < ApplicationRecord
has_one :talent, dependent: :destroy
A Talent belongs to a user, and have many TalentAbilities.
#talent.rb
class Talent < ApplicationRecord
belongs_to :user
has_many :talent_talent_abilities, dependent: :destroy
has_many :talent_abilities, through: :talent_talent_abilities
So, we can create, for example, a TalentAbility that belongs to a Talent. Like this:
TalentType.create!([
{name: "Model", is_model: true, is_photo: false, is_makeup: false, is_hair: false},
{name: "Photographer", is_model: false, is_photo: true, is_makeup: false, is_hair: false
])
TalentAbility.create!([
{name: "Scuba diving", requires_description: false, talent_type_id: 1}
{name: "River rafting", requires_description: false, talent_type_id: 1},
I want to add a View, where the user, based on your TalentType, edit his profile, with a lot of checkboxes where he can click for example, "Scuba Diving -> true" . "River rafting -> false".
My question is: How is the better way to update those relationships this on my controller? First remove all the TalentTalentAbility that belongs to this talent, and then add all based on my form?
#profiles_controler.rb
def update
##delete all the entities
TalentTalentAbility.where(talent: u.talent).each do |tt|
tt.destroy
end
##some code to add the new relationships
end
Thanks

The problem with the accepted answer:
Say you have a user with 5 talents and he chooses that he's learned a sixth one. If you go the way of calling .destroy_all, this is what will happen:
5 separate, sequential DELETE statements sent to DB
6 separate, sequential INSERT statements sent to DB
The correct solution for such problem is to render the checkboxes with their respective ids and make use of _destroy (read more here) attribute.
In short, if you get a hash (you may need to clean it up after you get it from your form, I'm sorry, but I don't have anything on hand to give you a A-Z example) containing something like this:
..., talent_talent_abilities_attributes: [
{id: 1, _destroy: true},
{id: nil, talent_ability_id: 1}
{id: 2, talent_ability_id: 2, some_attribute: "value"}
]
And you save your user, ActiveRecord will:
Issue one DELETE statement for first item.
Issue one INSERT statement for second item.
Depending on whether your records are loaded and whether something changed for ability with id: 2, it might or might not issue an UPDATE statement.
I suggest you tinker a bit in rails console with creating a User and updating his abilities manually using the nested attribute array (which again, please read more about at the aforementioned link) and observing how many SQL queries it generates. If you have any further questions, leave a comment under this or create a separate, more specific question.

I've done something similar in the past and it's worked really well. I would move it out to the model though, so you'd have something like this.
class User < ActiveRecord::Base
ActiveModel::Dirty # if you need to track changes and only delete on attrs changed
before_save :update_talents
# ...
private
def update_talents
talents.destroy_all if talents_changed?
end
end
Then the new talents will be saved with the form data. Also, that way your controller won't be needlessly messy.
You should be careful, though, to ensure that that's the behaviour you want. You could add in a check to only update if the talents are being changed with ActiveModel::Dirty, too. Might wanna check the syntax on the *_changed method for associations.
Caveat
I tend to advocate this approach because my nested form used JS positioning to re-order items therein. There might be a saner approach.

Related

Render an ActiveReccord:relationship with specific cat value

My title is not really clear, I don't know how to formulate it, if you have suggestions, I can edit it.
I would like to render all the missions form my user_facturation. But I don't know how to do it with current db structure.
My relations :
mission has_many :mission_facturations
mission_facturation belongs_to :mission
Mission_facturation can be linked with many element facturation (user, partner, etc ...). So to save this relation I'm saving a fact_cat and a fact_id column. (A little bit has polymorphic relations)
Example, if I want to link my mission_facturation with the first user_facturation, I reccord :
mission_facturation.update(fact_cat: 8, fact_id: UserFacturation.first.id)
Now, I would like to access to all the missions directly from my user_facturation model. Initially, I was using that method into the model user_facturation.rb
def missions
MissionFacturation.where(fact_cat: 8, fact_id: self.id).where.not(mission_id: nil).map(&:mission)
end
But this is not rendering an ActiveReccord::Relation, so when I use it for a loop, it's not working well
Do you have any solutions to render a collection of missions into my user_facturation model ?
Start the query with the Mission model, something like this:
Mission.joins(:mission_facturations).where(mission_facturations: {fact_cat: 8, fact_id: self.id)
Or you can even set a relationship on the User class so AR handles the joins for you:
has_many :mission_facturations, -> { where(fact_cat: 8) }, :foreign_key => "fact_id"
has_many :missions, through: :mission_facturations

Rails - Manipulating the returned objects in an ActiveRecord query

So I have the following simplified models and associations:
class Barber < User
has_many :barber_styles, inverse_of: :barber
end
class Style < ActiveRecord::Base
end
class BarberStyle < ActiveRecord::Base
belongs_to :barber, inverse_of: :barber_styles
belongs_to :style
delegate :name, to: :style
end
If I wanted to make a query based on all BarberStyle's that belong to a specific Barber, is there a way I can include the name of the associated Style?
I want to use a line something like:
BarberStyle.joins(:barber).where(barber: 109).include(:style)
So I'd be able to access an associated style.name. But I don't think a line like this exists.
I know I could just use map to build an array of my own objects, but I have to use a similar query with many different models, and don't want to do a bunch of extra work if it's not necessary.
In one of my previous projects, I was able to render json with the following lines:
#colleges = College.includes(:sports).where(sports: { gender: "male" })
render json: #colleges, include: :sports
But I don't want to render json in this case.
Edit:
I don't really have any unincluded associations to show, but I'll try to elaborate further.
All I'm trying to do is have extra associated models/fields aggregated to each BarberStyle result within my ActiveRecord Relation query.
In an attempt to make this less confusing, here is an example of the type of data I want to pass into my JS view:
[
#<BarberStyle
id: 1,
barber_id: 116,
style_id: 91,
style: { name: "Cool Hairstyle" }
>,
#<BarberStyle
id: 2,
barber_id: 97,
style_id: 92,
style: { name: "Cooler Hairstyle" }
>,
etc...
]
The reason I want the data formatted this way is because I can't make any queries on associated models in my JS view (without an AJAX call to the server).
I had very similarly formatted data when I did render json: #colleges, include: :sports in the past. This gave me a collection of Colleges with associated Sport models aggregated to each College result. I don't want to build json in this fashion for my current situation, as it will complicate a few things. But I suppose that's a last resort.
I'm not sure if there's a way to structure an ActiveRecord query where it adds additional fields to the results, but I feel like it should, so here I am looking for it. Haven't found anything in the docs, but then again there is sooooo much left out of those.
Doug as far as I understand your code, BarberStyle must be a joining model between Barber and Style. You mentioned in your comment that you removed the has_many :styles, through: :barber_styles from your Barber model because you thought that it wasn't relevant. That's not true, it's exactly the point that would help you to achieve your goal. Add this relation again then you can do something like this:
barber = Barber.includes(:styles).where(barber: {id: 109})
barber.styles.each { |style| puts style.name }
Since barber.styles is a collection, I added a loop between all the possible styles you can have. But, from that, you can use your data as you feel like, looping through it or any other way you want.
Hope to have helped!
First off, in your case inverse_of does not accomplish anything, since you are setting the default values. Remove that.
Secondly, it seems you need to better understand the concept of HABTM relationships.
Using has many through is generally a good idea since you can add data and logic to the model in the middle.
It ideally suits your case, so you should set it up like this:
class Barber < User
has_many :barber_styles
has_many :styles, through: :barber_styles
end
class Style < ActiveRecord::Base
has_many :barber_styles
has_many :barbers, through: :barber_styles
end
class BarberStyle < ActiveRecord::Base
belongs_to :barber
belongs_to :style
end
Now it is an easy task to get the styles of a given barber. Just do this:
barber = Barber.find(1)
barber.styles
# => AR::Collection[<#Style name:'Cool Hairstyle'>,<#Style name:'Cooler Hairstyle'>]
Rails automatically uses the BarberStyle model in between to find the styles of a certain barber.
I assume this covers your need, if you have extra information stored only in BarberStyle, let me know.

Update deeply nested model upon save

I'm attempting to update a deeply nested model from a model that is itself nested using cocoon. (Don't worry, I'll clarify)
I have three Models: Stock Stockholder and Folder. Stock has_many :stockholders, which are all nested via cocoon in a form_for #stock. Stockholder has_one :folder.
Within my stock form, I have a table that lists out stockholders where I can add new ones (to reiterate, via cocoon). To create a Folder for each new Stockholder, I have a before_create filter in my Stockholder model that creates a new Folder for each new Stockholder (see below)
before_create :build_default_folder
def build_default_folder
logger.debug "Inside build_default_folder"
build_folder(name: "#{self.holder_index}. #{self.holder_name}", company_id: self.warrant.company.id, parent_id: self.warrant.company.folders.find_by_name("#{self.warrant.security_series} #{self.warrant.security_class} Warrant").id)
true
end
All of that works very well.
The problem
Is that I would like to add a method that updates the Folder attributes according to any changes in the stockholder information (say they change the name or something). To this end, I've attempted to add the following.
before_save :update_default_folder
def update_default_folder
logger.debug "Inside update_default_folder"
self.folder.update_attributes(name: "#{self.holder_index}. #{self.holder_name}", company_id: self.warrant.company.id, parent_id: self.warrant.company.folders.find_by_name("#{self.warrant.security_series} #{self.warrant.security_class} Warrant").id)
true
end
This however doesn't work and (particularly puzzling to me) is that before_save doesn't even seem to be firing. My best guess would be that this is because stockholder is itself nested using cocoon (but I could be completely wrong about this).
Anyway, how might I achieve the desired functionality? Thanks for any suggestions.

How to use `first_or_initialize` step with `accepts_nested_attributes_for` - Mongoid

I'd like to incorporate a step to check for an existing relation object as part of my model creation/form submission process. For example, say I have a Paper model that has_and_belongs_to_many :authors. On my "Create Paper" form, I'd like to have a authors_attributes field for :name, and then, in my create method, I'd like to first look up whether this author exists in the "database"; if so, then add that author to the paper's authors, if not, perform the normal authors_attributes steps of initializing a new author.
Basically, I'd like to do something like:
# override authors_attributes
def authors_attributes(attrs)
attrs.map!{ |attr| Author.where(attr).first_or_initialize.attributes }
super(attrs)
end
But this doesn't work for a number of reasons (it messes up Mongoid's definition of the method, and you can't include an id in the _attributes unless it's already registered with the model).
I know a preferred way of handling these types of situations is to use a "Form Object" (e.g., with Virtus). However, I'm somewhat opposed to this pattern because it requires duplicating field definitions and validations (at least as I understand it).
Is there a simple way to handle this kind of behavior? I feel like it must be a common situation, so I must be missing something...
The way I've approached this problem in the past is to allow existing records to be selected from some sort of pick list (either a search dialog for large reference tables or a select box for smaller ones). Included in the dialog or dropdown is a way to create a new reference instead of picking one of the existing items.
With that approach, you can detect whether the record already exists or needs to be created. It avoids the need for the first_or_initialize since the user's intent should be clear from what is submitted to the controller.
This approach struggles when users don't want to take the time to find what they want in the list though. If a validation error occurs, you can display something friendly for the user like, "Did you mean to pick [already existing record]?" That might help some as well.
If I have a model Paper:
class Paper
include Mongoid::Document
embeds_many :authors
accepts_nested_attributes_for :authors
field :title, type: String
end
And a model Author embedded in Paper:
class Author
include Mongoid::Document
embedded_in :paper, inverse_of: :authors
field :name, type: String
end
I can do this in the console:
> paper = Paper.create(title: "My Paper")
> paper.authors_attributes = [ {name: "Raviolicode"} ]
> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Raviolicode">]
> paper.authors_attributes = [ {id: paper.authors.first, name: "Lucia"}, {name: "Kardeiz"} ]
> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Lucia">, #<Author _id: 531cd95931302ea603010000, name: "Kardeiz">]
As you can see, I can update and add authors in the same authors_attributes hash.
For more information see Mongoid nested_attributes article
I followed the suggestion of the accepted answer for this question and implemented a reject_if guard on the accepts_nested_attributes_for statement like:
accepts_nested_attributes_for :authors, reject_if: :check_author
def check_author(attrs)
if existing = Author.where(label: attrs['label']).first
self.authors << existing
true
else
false
end
end
This still seems like a hack, but it works in Mongoid as well...

Mongoid: 'update_attribute' with [models, ...] vs with [model_ids, ...]

I was writing some tests and I ran into something I'm trying to understand.
What is the difference underneath when calling:
.update_attributes(:group_ids, [group1.id, group2.id])
vs
.update_attributes(:groups, [group1, group2])
These 2 models in question:
group.rb
class Group
include Mongoid::Document
has_and_belongs_to_many :users, class_name: "Users", inverse_of: :groups
end
user.rb
class User
include Mongoid::Document
has_and_belongs_to_many :groups, class_name: "Group", inverse_of: :users
end
test code in question:
g1 = create(:group)
u1 = create(:user, groups: [g1])
g1.update_attribute(:users, [u1])
# at this point all the associations look good
u1.update_attribute(:group_ids, [g1.id])
# associations looks good on both sides when i do u1.reload and g1.reload
u1.update_attribute(:groups, [g1])
# g1.reload, this is when g1.users is empty and u1 still has the association
Hope I made sense, thanks
Are all your attributes white listed properly?
Without schema for the models, your join object, and the actual tests I'm grasping at straws, but based purely on that example my guess would be that the first model contains an attribute that is mapping to an unintended field on your second model, and overwriting it when you pass an entire object, but not when you specify the attribute you want updated. Here's an example: (I'm not assuming you forgot your join table, I'm just using that because its the first thing that comes to mind)
so we create 2 models, each that have a field that maps to user_id
group.create(id:1, user_id:null)
group_user.create(id:1, group_id: 1, user_id:null)
group.update_attributes(user_id: (group_user.id))
So at this point, when you call group.users, it checks for a user with the id of 1, because that's the id of the group_user you just created & passed it, and assuming you have a User with that ID in your database, the test passes.
group_user.update_attributes(group_id: group.id)
In this case the method ONLY updates group_id, so everything still works.
group_user.update_attributes(group_id: group, user_id: group)
In this case you pass an entire object through, and leave it up to the method to decide what fields get updated. My guess is that some attribute from your group model is overwriting the relevant attribute from your user model, causing it to break ONLY when NO user_ids match whatever the new value is.
Or an attribute isn't white listed, or your test is wonky.

Resources