Formtastic polymorphic form with interchangeable model - ruby-on-rails

So I'm having a problem implementing a form for a model with a polymorphic association. The problem is, I need to offer the choice between two different models to choose from. I'm using HAML and Formtastic. In case anyone is wondering about the models, it's about firewall access list rules.
class Rule < ActiveRecord::Base
belongs_to :ipable_from, foreign_key: 'ipable_from_id', polymorphic: true
belongs_to :ipable_to, foreign_key: 'ipable_to_id', polymorphic: true
end
class Ip < ActiveRecord::Base
has_many :rules_from, class_name: "Rule", foreign_key: 'ipable_from_id', as: :ipable_from
has_many :rules_to, class_name: "Rule", foreign_key: 'ipable_to_id', as: :ipable_to
has_and_belongs_to_many :ip_groups
end
class IpGroup < ActiveRecord::Base
has_many :rules_from, class_name: "Rule", foreign_key: 'ipable_from_id', as: :ipable_from
has_many :rules_to, class_name: "Rule", foreign_key: 'ipable_to_id', as: :ipable_to
has_and_belongs_to_many :ips
end
= semantic_form_for #rule do |f|
-#...
= f.semantic_fields_for :ipable_from do |ipf|
= ipf.input :ip_string, as: :string
= ipf.input :name, as: :select, collection: #ip_groups
-#...
The same input fields exists for :ipable_to. I want it to be processed like this: If the Select-Box for an IpGroup is left blank, the ip_string is processed, yielding an Ip, otherwise, the ipable references the chosen IpGroup. However, when evaluating the form, Formtastic automatically assumes the current model of the objects ipable_from and ipable_to, so when ipable_from is currently an Ip, it tells me, that the attribute name could not be found for Ip. That is because it's an attribute of IpGroup. Is there a way of telling Formtastic to build the form as follows:
= semantic_form_for #rule do |f|
-#...
= f.semantic_fields_for :ipable_from, as: :ip do |ipf|
= ipf.input :ip_string, as: :string
= f.semantic_fields_for :ipable_from, as: :ip_group do |ipf|
= ipf.input :name, as: :select, collection: #ip_groups
-#...
Like, if ipable_from is an Ip, display the ip_string and leave id untouched. If ipable_from is an IpGroup, the ip_string is left blank and the id of IpGroup is selected in the Select-Box.

Related

Rails bi-directional / self join, populate form and show page

Problem: I have a model Exercise, this model can have exercises that are variations of each other. So a self join table I thought.
Like a join table of Foo, Boo, Coo, where Foo and Boo are models link by the joining table Coo allows for #foo.boo and #boo.foo
So I looked and read these posts describing bi and uni directional relation ships 1, 2. So the table stores the data correctly but now I'm have trouble on creating the correct query.
EDIT:
After reading more on how a join table works and naming conventions I see that the methods are working like they should. My question should be about how do I not cause if statements in the show and form page.
The way that it is set, if I was editing object a that had a relation to object b it would populate via the #a.variations. However, if I go to object b I have to get the relation via #b.exercises which does not seem correct but works. Further more, the same idea would have to be repeated on the show page. How does one go about making this a uniform "call" i.e. #a.variations and #b.variations to populate the forms and the show? Is it even possible?
UPDATE/Clarification: To question below regarding variations.
If there are many objects that are referenced, i.e. A is a variation of B and C is a variation of B then A would also be a variation of C. So a query of #a.variations = [B,C]; #b.variations = [A,C]; #c.variations = [A,B]. Hopefully this clarifies the question and my thinking behind this relation/query.
(extra info)- The reason being some people wont be able to squat so they will have to start with a different exercise that target the same muscles and build there way up to the squat. Or you could have been injured or on a recovery day and to target the muscles with a less complex movement.
Schema migration, Models, Controllers, Forms:
create_table :exercise_variation_relations do |t|
t.references :exercise, foreign_key: true, null: false, index: true
t.references :exercise_variation, foreign_key: { to_table: :exercises }, null: false, index: true
end
class ExerciseVariationRelation < ApplicationRecord
belongs_to :exercise, foreign_key: "exercise_id", class_name: "Exercise"
belongs_to :exercise_variation, foreign_key: "exercise_variation_id", class_name: "Exercise"
end
class Exercise < ApplicationRecord
**other code
has_many :exercise_drills, foreign_key: :exercise_variation_id, class_name: "ExerciseVariationRelation"
has_many :exercises, through: :exercise_drills, source: :exercise
has_many :variation_exercises, foreign_key: :exercise_id, class_name: "ExerciseVariationRelation"
has_many :variations, through: :variation_exercises, source: :exercise_variation
validates_associated: :variation_exercises
accepts_nested_attributes_for :variation_exercises
** other code
end
Contoller
params.fetch(:exercise, {}).permit( **other params
variation_exercises_attributes: [:id, :exercise_variation_id, :_destroy], )
Form
<%= f.fields_for :variation_exercises, ExerciseVariationRelation.new, child_index: 'NEW_RECORD' do |e| %>
<%= render "form_variation", form: e %>
<% end %>
Form_variation
<%= content_tag :div, class: "nested-fields" do %>
<%= form.collection_select(:exercise_variation_id, Exercise.all, :id, :name, {}, {class: 'form-control'}) %>
<% end %>
I understand how join tables work but this self join/relation is confusing still.
i think your model (at least how it is presented in your question) needs some clarification :) for instance:
do these relations act like a transitive network? if A is a variation of B and B is a variation of C, does that make A a variation of C?
if A is a variation of B, does that make B a variation of A?
in the simplest case, this should work i think:
create_table :exercise_variations do |t|
t.references :exercise, foreign_key: true, null: false, index: true
t.references :variation, foreign_key: { to_table: :exercises }, null: false, index: true
end
class ExerciseVariation < ApplicationRecord
belongs_to :exercise, foreign_key: "exercise_id"
belongs_to :variation, foreign_key: "variation_id", class_name: "Exercise"
end
class Exercise < ApplicationRecord
has_many :exercise_variations, class_name: "ExerciseVariation"
has_many :variations, through: :exercise_variations
has_many :variation_exercises, foreign_key: :variation_id, class_name: "ExerciseVariation"
has_many :variations, through: :variation_exercises
end
ActiveModel::UnknownAttributeError (unknown attribute 'exercise_variation_id' for Exercise.)
The error message is clearly Exercise doesn't have exercise_variation_id.
Could you show your app/db/schema.rb file content?
Make sure your migration creates exercise_variation_id for Exercise in the database.

Associating a User to model via id

I'm having some difficulty expressing a linkage of a User to my Listing model.
I set up a Rails form when I associated a designer_id (added to listing_params as a private controller method) that would link a selected user to a Listing model when created:
#migration
add_column :listings, :designer_id, :integer
_form.html.erb
<%= collection_select :listing, :designer_id, #account.users, :id, :name, prompt: "Choose..." %>
Checking in the console, the form returned the correct user id as designer_id. Success!
What I need to do now is access the User name using a Listing method, but I'm getting stuck- the issue is primarily making the translation from the id procured to the User referenced:
#listing.rb
def designer
self.designer_id == User.find_by_id(params[:name])
if self.designer_id = nil
return "N/A"
else
return "#{User.name}"
end
Much appreciated!
in the migration if you are on at least rails 4 you can do
add_reference(:listings, :designer)
you may need to do
add_reference(:listings, :designer, :foreign_key => { to_table: 'users'}
other options I often use
add_reference(:listings, :designer, :foreign_key => { to_table: 'users'} index: true, limit: 8)
Migration aside you can do this in the models.
class Listing
belongs_to :designer, class_name: 'User', inverse_of: :listings
end
and in users
class User
has_many :listings, inverse_of: :designer, dependent: :destroy
end
Getting the users name would then be,
listing.designer.name
if you are doing this in a controller you will want to pre-load the association so you are not introducing an n+1 query to a list of listings.

Edit relationship of parent record in activeadmin

I have the following model structure:
Composition has many Score (Score belongs to Composition)
Composition has and belongs to many Countries (and viceversa)
score.rb:
class Score < ApplicationRecord
belongs_to :composition
end
composition.rb:
class Composition < ApplicationRecord
has_many :scores
has_and_belongs_to_many :countries, join_table: :rights_countries
end
country.rb:
class Country < ApplicationRecord
has_and_belongs_to_many :compositions, join_table: :rights_countries
end
In activeadmin, I want to be able to edit the countries of a composition, but in the edit form of its scores.
Of course, the form will import this data from composition, and default inputs will be equal for all the scores (children) of a composition.
I found no way to implement this in activeadmin up to now.
Is this even possible? If yes, is the solution easy or cumbersome?
Following this link, I added an inputs within an inputs and updated the corresponding params. I also added accepts_nested_attributes_for :composition in the score model.
app/models/score.rb
...
accepts_nested_attributes_for :composition
...
app/admin/score.rb
...
permit_params ...,
composition_attributes: [:id, country_ids: []]
...
form do |f|
f.inputs do
...
f.inputs "", for: [:composition, score.composition] do |c|
c.input :countries, as: :select, collection: Country.order_by_name.uniq.map { |p| [p.name, p.id] }
end
end
end
Let me know if there's a cleaner solution.

rails 4 associated models with class_name in select, shows all. Some advise needed

As I am learning RoR now, I would like to know a more appropriated (rails) way to achieve that the application only shows associated resources.
Right now I have the following models:
class Account < ActiveRecord::Base
has_many :billing_accounts
has_many :addresses
end
class BillingAccount < ActiveRecord::Base
belongs_to :invoice_address,
class_name: "Address",
foreign_key:"invoice_address_id"
end
class Address < ActiveRecord::Base
has_many :billing_accounts
belongs_to :account
end
In my edit.billing_account I have this form:
= simple_form_for([:account, #billing_account]) do |f|
= f.association :invoice_address
I expected that only the associated address will be shwon, but this shows "all" address records in the database (also from other user accounts).
Users only should be able to see account.addresses and for now I do this with:
= f.association :invoice_address, collection: current_user.account.addresses.all
But I am sure there is better way to do this inside the models. For every form I now use current_user.account.MODEL.all but that is not very DRY I think.
So basically what I want is only to use =f.association :invoice_address and BillingAccount should know it only can show the account.addresses.
Suggestions are welcome. Thanks!
You just need to set default_scope for nested models:
class Address < ActiveRecord::Base
default_scope { where(account_id: current_user.account_id) }
But in this case you should define current_user in models
In your case you should use f.simple_fields_for instead of f.association as described here: https://github.com/plataformatec/simple_form/wiki/Nested-Models
class BillingAccount < ActiveRecord::Base
belongs_to :invoice_address,
class_name: "Address",
foreign_key:"invoice_address_id"
accepts_nested_attributes_for :invoice_address
end
View:
= simple_form_for([:account, #billing_account]) do |f|
= f.simple_fields_for :invoice_address do |f_address|
= f_address.input :street
= f_address.input :zipcode
...
Don't forget to build invoice_address of account in a controller if it is needed. For example:
class BillingAccountController < ApplicationController
def new
#billing_account = BillingAccount.new
#billing_account.build_invoice_address
end
Since you're using has_many you can use the plural version of the model name rather than current_user.account.MODEL.all.
Like this:
current_user.account.addresses
or
current_user.account.billing_accounts
It even works the other way with belongs_to:
#address = Address.last
#address.accounts
Try to add conditions to belongs_to association:
class BillingAccount < ActiveRecord::Base
belongs_to :invoice_address,
->(billing_account) { where "account_id = #{billing_account.account_id}" },
class_name: "Address",
foreign_key:"invoice_address_id"
end

How can I elegantly construct a form for a model that has a polymorphic association?

Here are my models:
class Lesson < ActiveRecord::Base
belongs_to :topic, :polymorphic => true
validates_presence_of :topic_type, :topic_id
end
class Subject < ActiveRecord::Base
has_many :lessons, :as => :topic
end
class Category < ActiveRecord::Base
has_many :lessons, :as => :topic
end
Now, what I need is a form that will allow the user to create or update Lessons. The questions is, how can I provide a select menu that offers a mix of Subjects and Categories? (To the user, on this particular form, Subjects and Categories are interchangeable, but that's not the case elsewhere.)
Ideally, this would look something like this:
views/lessons/_form.html.haml
= simple_form_for(#lesson) do |f|
= f.input :title
= f.association :topic, :collection => (#subjects + #categories)
That won't work because we'd only be specifying the topic_id, and we need the topic_types as well. But how can we specify those values?
I guess the crux of the problem is that I really want a single select menu that specifies two values corresponding to two different attributes (topic_id and topic_type). Is there any elegant railsy way to do this?
A few notes:
a) Single table inheritance would make this issue go away, but I'd like to avoid this, as Categories and Subjects have their own relationship… I'll spare you the details.
b) I might could pull some javascript shenanigans, yes? But that sounds messy, and if there's a cleaner way to do it, some magic form helper or something, then that's certainly preferable.
c) Though I'm using simple_form, I'm not wedded to it, in case that's complicating matters.
Thanks
If you don't wish to use STI, you can do something similar: create a new model Topic(name:string) which will polymorphically reference Subject or Category.
class Lesson < ActiveRecord::Base
belongs_to :topic
validates_presence_of :topic_id
end
class Topic < ActiveRecord::Base
belongs_to :topicable, :polymorphic => true
end
class Subject < ActiveRecord::Base
has_one :topic, :as => :topicable
has_many :lessons, :through => :topic
accepts_nested_attributes_for :topic
end
class Category < ActiveRecord::Base
has_one :topic, :as => :topicable
has_many :lessons, :through => :topic
accepts_nested_attributes_for :topic
end
In the view where you create a new Subject/Category:
<%= form_for #subject do |subject_form| %>
<%= subject_form.fields_for :topic do |topic_fields| %>
<%= topic_fields.text_field :name %>
<% end %>
<% end %>
After thinking this through, the less dirty implementation IMO would be to hire the JS shenanigans (b):
= f.input_field :topic_type, as: :hidden, class: 'topic_type'
- (#subjects + #categories).each do |topic|
= f.radio_button :topic_id, topic.id, {:'data-type' => topic.class.name, class: 'topic_id'}
With a sprinkle of JS (your needs may vary):
$('input:radio.topic_id').change(function() {
$('input:hidden.topic_type').val($(this).attr('data-type'));
});
Notes:
I use a radio button to select a topic (category or subject) in a list
The class name of each of possible topic is stored in an attribute 'data-type'
When a radio button is selected, the class name is copied to the hidden input via JS
Using: HTML5, jQuery, haml, simple_form

Resources