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

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.

Related

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.

ActiveAdmin / Formtastic sortable has_many through relationship

I may be missing something fundamental here, but I can't seem to get ActiveAdmin to work with a sortable has_many through relationship, with the ability to create new records.
So given the following models
class User < ActiveRecord::Base
has_many :user_videos
has_many :videos, through: :user_videos
accepts_nested_attributes_for :user_videos
accepts_nested_attributes_for :videos
...
end
class UserVideo < ActiveRecord::Base
belongs_to :user
belongs_to :video
accepts_nested_attributes_for :video
end
class Video < ActiveRecord::Base
has_many :user_videos
has_many :users, through: :user_videos
...
end
(I admit I'm throwing accepts_nested_attributes_for around somewhat in the hopes that something may work)
And Active Admin setup goes something like this (WIP of course):
f.inputs "User" do
f.has_many :user_videos, heading: 'Videos', sortable: :order, allow_destroy: true, new_record: 'New Record' do |v|
v.inputs for: :video do |video|
video.input :video_url
end
end
f.has_many :videos, heading: 'Videos', new_record: 'New Video' do |v|
v.input :video_url
end
end
f.actions
The first has_many on the :user_videos association does not seem to render any inputs. If there are records there, I can see that video.input :video_url is actually returning an li tag with label and input, however nothing gets rendered to the page. For new records the whole v.inputs bit does not get run (do I need to create the child records somehow there first?).
The second has_many will work in that you'll be able to add records, and update existing records, however it's impossible to sort as the order column is on the UserVideos model. I include this more as illustration than anything.
If anyone has any pointers for this, they would be most appreciated. :)
WHOA! I know I am late to the party, but this is a perfect opportunity to utilize the :delegate method!
Your UserVideo class would look something like this
class UserVideo < ActiveRecord::Base
belongs_to :user
belongs_to :video
validates_with VideoValidator
delegate :video_url, :video_url=, to: :video
end
Best of luck!
Since nobody seemed interested in tackling this, I took another approach - rather than trying to get ActiveAdmin / Formtastic to work with the existing model structure, I added getters and setters for the necessary field on the intersection model.
class UserVideo < ActiveRecord::Base
belongs_to :user
belongs_to :video
validates_with VideoValidator
def video_url
self.video = Video.create if video.nil?
self.video.video_url
end
def video_url=(video_url)
self.video = Video.create if video.nil?
self.video.video_url = video_url
# Video url is set via Active Admin, AA will not call save on the video as it does not realise it's changed
self.video.save! if video.present? and video.valid?
end
end
Doing this meant that Active Admin did not need to know about the Video model, and could just operate on the UserVideo model:
f.has_many :user_videos, heading: 'Videos', sortable: :order, allow_destroy: true, new_record: 'New Record' do |v|
v.input :video_url, :hint => (v.object.video.embed_code unless v.object.nil? or v.object.video.nil?)
end
If anyone has an actual solution rather than a work around, I'd love to hear it, but otherwise this is a possible solution for anyone searching for an answer to the same problem.

Formtastic polymorphic form with interchangeable model

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.

Need help constructing a query

I've a model that has a nested model of skills. Its a common has_many example. Elastic search is indexing the skills as an array of strings.
My question is, I am attempting to match on those skills by way of two different inputs.
Required skills and bonus skills.
So if I have two query terms one for required and one for bonus, I want to query the skills attribute with required input, if none found, query with the bonus input.
I'm using elasticsearch-rails gem. Didn't think I needed to post any code as this is more theory.
UPDATE
class Profile
has_many :skills
...
end
class Skill
belongs_to :profile
end
Mappings
settings index: { number_of_shards: 1, number_of_replicas: 0 } do
...
mapping dynamic: 'false' do
indexes :skills, analyzer: 'keyword'
end
...
end
Overriden as_json
def as_indexed_json(options={})
hash = self.as_json(
include: {location: { methods: [:coordinates], only: [:coordinates] },
locations_of_interest: { methods: [:coordinates], only: [:coordinates]}
})
hash['skills'] = self.skills.map(&:name)
hash['interests'] = self.interests.map(&:name)
hash
end
I guess in essence i'm looking to perform the reverse of a multi_match on multiple fields and boosting one but instead searching one field with multiple inputs (required and bonus) and depending no the results of required search with bonus input. Does this makes things more clear?
This is my query so far, first attempt.
if options[:required_skills].present? && options[:bonus_skills].present?
bool do
must do
term skills: options[:required_skills]
end
should do
term skills: options[:bonus_skills]
end
end
end
class SkillContainer < ActiveRecord::Base
has_many :skill_links, dependent: :destroy, inverse_of: :skill_container
has_many :skills, through: :skill_links
has_many
end
##################################
create_table :skill_link do |t|
t.references :skill
t.references :skill_container
t.boolean :required
t.boolean :bonus
end
##################################
class SkillLink
belongs_to :skill_container
belongs_to :skill
scope :required, -> {
where(required: true)
}
scope :bonus, -> {
where(bonus: true)
}
end
class Skill < ActiveRecord::Base
has_many :skill_links, dependent: :destroy, inverse_of: :skill
has_many :skills, through: :skill_links
end
#required skills from any skill container
SkillContainer.last.skills.merge(SkillLink.required)
#bonus skills from any skill container
SkillContainer.last.skills.merge(SkillLink.bonus)
scopes can be combined with your elastic search

Multiple counter_cache in Rails model

I'm learning Rails, and got into a little problem. I'm writing dead simple app with lists of tasks, so models look something like that:
class List < ActiveRecord::Base
has_many :tasks
has_many :undone_tasks, :class_name => 'Task',
:foreign_key => 'task_id',
:conditions => 'done = false'
# ... some validations
end
Table for List model has columns tasks_counter and undone_tasks_counter.
class Task < ActiveRecord::Base
belongs_to :list, :counter_cache => true
# .. some validations
end
With such code there is attr_readonly :tasks_counter for List instances but I would like to have a counter for undone tasks as well. Is there any way of having multiple counter cached automagically by Rails.
So far, I've managed to create TasksObserver that increments or decrements Task#undone_tasks_counter, but maybe there is a simpler way.
Have you tried it with a custom-counter-cache column?
The doc here:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
It suggests that you can pass a column-name to the counter_cache option, which you may well be able to call twice eg
belongs_to :list, :counter_cache => true # will setup tasks_count
belongs_to :list, :counter_cache => :undone_tasks_count
Note: not actually tested.
ez way.
1) first counter - will do automatically
2) Manually "correct"
AnotherModelHere
belongs_to :user, counter_cache: :first_friends_count
after_create :provide_correct_create_counter_2
after_destroy :provide_correct_destroy_counter_2
def provide_correct_create_counter_2
User.increment_counter(:second_friends_count, another_user.id)
end
def provide_correct_destroy_counter_2
User.decrement_counter(:second_friends_count, another_user.id)
end
Most probably you will need counter_culture gem, as it can handle counters with custom conditions and will update counter value not only on create and destroy, but for updates too:
class CreateContainers < ActiveRecord::Migration[5.0]
create_table :containers, comment: 'Our awesome containers' do |t|
t.integer :items_count, default: 0, null: false, comment: 'Caching counter for total items'
t.integer :loaded_items_count, default: 0, null: false, comment: 'Caching counter for loaded items'
end
end
class Container < ApplicationRecord
has_many :items, inverse_of: :container
has_many :loaded_items, -> { where.not(loaded_at: nil) },
class_name: 'Item',
counter_cache: :loaded_items_count
# Notice that you can specify custom counter cache column name
# in has_many definition and AR will use it!
end
class Item < ApplicationRecord
belongs_to :container, inverse_of: :items, counter_cache: true
counter_culture :container, column_name: proc { |model| model.loaded_at.present? ? 'loaded_items_count' : nil }
# But this column value will be handled by counter_culture gem
end
I'm not aware of any "automagical" method for this. Observers seems good for this, but I personally prefer using callbacks in model (before_save, after_save).

Resources