Rails multiple Has_one relationship to same model - ruby-on-rails

I am working on a Rails application and currently I have 2 models - Subjects and Lessons.
A Subject has 3 different types of lessons - Lecture, Tutorial and Laboratory. I modelled such that there are 3 has_one to the Lesson model.
Right now, I am trying to create a nested form for subjects and lessons but the lecture, tutorial and laboratory being saved was always the first form that was rendered.
i.e. I have 3 nested forms separately for Lecture, Tutorial and Laboratory but the Lecture, Tutorial and Laboratory that was saved was always the one that was first built. In my codes the lecture was first built so the attributes for tutorial and laboratory would follow the one that I have filled in for my lecture.
I am not sure where I have went wrong or even if having multiple has_one relationship works in this case so any advice would be appreciated.
The related codes are as follows:
The subject model
class Subject < ActiveRecord::Base
has_one :lecture, :class_name => "Lesson"
has_one :laboratory,:class_name => "Lesson"
has_one :tutorial, :class_name => "Lesson"
accepts_nested_attributes_for :lecture
accepts_nested_attributes_for :laboratory
accepts_nested_attributes_for :tutorial
end
The lesson model
class Lesson < ActiveRecord::Base
belongs_to :subject
end
The Subject and lesson nested form
<%= form_for(#subject_list) do |f| %>
<div class="field">
<%= f.label :subject_code %><br />
<%= f.text_field :subject_code %>
</div>
<div>
<%= f.fields_for :lecture do |lecture| %>
<%= render "lecture_fields", :f => lecture %>
<% end %>
</div>
<div>
<%= f.fields_for :tutorial do |tutorial| %>
<%= render "tutorial_fields", :f => tutorial %>
<% end %>
</div>
<div>
<%= f.fields_for :laboratory do |laboratory| %>
<%= render "laboratory_fields", :f => laboratory %>
<% end %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
The new action in the subject controller
def new
#subject = Subject.new
lecture = #subject.build_lecture
laboratory = #subject.build_laboratory
tutorial = #subject.build_tutorial
respond_to do |format|
format.html # new.html.erb
format.json { render json: #subject }
end
end
I would appreciate if someone could help me out in identifying where I have went wrong. If in the case that I should not be creating such multiple relationships, I would like to have some advice on how could I actually render out 3 forms with a default field indicating the lesson type.

I'm not really sure if that works, but my advise is to use AR inheritance
class Lesson < ActiveRecord::Base
end
class LectureLesson < Lesson
belongs_to :subject
end
class LaboratyLesson < Lesson
belongs_to :subject
end
class TutorialLesson < Lesson
belongs_to :subject
end
class Subject
has_one :lecture_lesson
has_one :laboratory_lesson
has_one :tutorial_lesson
accepts_nested_attributes_for :lecture_lesson
accepts_nested_attributes_for :laboratory_lesson
accepts_nested_attributes_for :tutorial_lesson
end
Migration
class LessonsAndSubjects < ActiveRecord::Migration
def up
remove_column :subjects, :lesson_id
add_column :subjects, :lecture_lesson_id, :integer
add_column :subjects, :laboratory_lesson_id, :integer
add_column :subjects, :tutorial_lesson_id, :integer
add_column :lessons, :type, :string
add_index :subjects, :lecture_lesson_id
add_index :subjects, :laboratory_lesson_id
add_index :subjects, :tutorial_lesson_id
end
def down
remove_column :subjects, :lecture_lesson_id
remove_column :subjects, :laboratory_lesson_id
remove_column :subjects, :tutorial_lesson_id
remove_column :lessons, :type
add_column :subjects, :lesson_id, :integer
end
end
it makes more sense and it may be fix you issue with nested attributes

Actually from the answer from rorra one point is missing, you need to add a polymorphic association for each "children" to not incurr in query problems
class Lesson < ActiveRecord::Base
belongs_to :subject
end
class LectureLesson < Lesson
belongs_to :polymorphic_lecture_lesson, polymorphic: true
end
class Subject
has_one :lesson
has_one :lecture_lesson, as: :polymorphic_lecture_lesson
accepts_nested_attributes_for :lesson
accepts_nested_attributes_for :lecture_lesson
end
in migration you have then to add
add_column :lessons, :polymorphic_lecture_lesson_id, :integer, index: true
add_column :lessons, :polymorphic_lecture_lesson_type, :integer, index: true

Interestingly, I interpreted this question much differently than the other answers seem to have.
If you are looking to have 2 has_one to a single model/table, then one can do the following:
Given a Person has one best pet and one worst pet, which are represented by the same Pet table/model that has an attribute to differentiate between the two...
class Person < ApplicationRecord
has_one :best_pet, -> { where(pet_type: "best") }, class_name: "Pet"
has_one :worst_pet, -> { where(pet_type: "worst") }, class_name: "Pet"
...
end
class Pet < ApplicationRecord
belongs_to :person
validates_presence_of :pet_type
validates_uniqueness_of :pet_type, scope: :person_id
...
end
Now, whether or not this is good database design is up for debate.

Related

Rails - modeling multiple many to many relationships

I have the following use cases for creating an app that handles courses;
Class A is taught by Curt in Bos on 11/1
Class A is taught by Curt in NY on 10/19
Class A is taught by Jane in SF on 12/5
Class A is taught by Jane in Bos on 11/1
What's the best way to create models with many to many relationships for this app?
Should the app have a teachings model that belongs to courses, teachers, and locations with a column for the date?
What you want is to create a model for each entity:
Course
Teacher
Location
You then create a join model of sorts which I have choosen to call Lesson:
class Course < ActiveRecord::Base
has_many :lessons
has_many :locations, through: :lessons
has_many :teachers, through: :lessons
end
class Lesson < ActiveRecord::Base
belongs_to :course
belongs_to :teacher
belongs_to :location
end
class Teacher < ActiveRecord::Base
has_many :lessons
has_many :courses, through: :lessons
end
class Location < ActiveRecord::Base
has_many :lessons
has_many :courses, through: :lessons
has_many :teachers, through: :lessons
end
I've been playing with this structure for the models but what I
noticed is that when submitting the course with a fields_for
:locations and a fields_for :instructors, the associations table is
creating two separate entries for course_id + instructor_id, course_id
+ location_id, I would expect a single entry for course_id, instructor_id, location_id. Any thoughts as to why that might happen?
ActiveRecords only ever keeps track of one assocation when you create join models implicitly. To do three way joins you need to create the join model explicitly.
<%= form_for(#course) do |f| %>
<div class="field>
<% f.label :name %>
<% f.text_field :name %>
</div>
<fieldset>
<legend>Lesson plan<legend>
<%= f.fields_for(:lessons) do |l| %>
<div class="field>
<% l.label :name %>
<% l.text_field :name %>
</div>
<div class="field">
<% l.label :starts_at %>
<% l.datetime_select :starts_at %>
</div>
<div class="field">
<% l.label :teacher_ids %>
<% l.collection_select :teacher_ids, Teacher.all, :id, :name, multiple: true %>
</div>
<div class="field">
<% l.label :location_id %>
<% l.collection_select :location_id, Location.all, :id, :name %>
</div>
<% end %>
</fieldset>
<% end %>
fields_for and accepts_nested_attributes are powerful tools. However passing attributes nested several levels down can be seen as an anti-pattern of sorts since it creates god classes and unexpected complexity.
A better alternative is to use AJAX to send separate requests to create teachers and locations. It gives a better UX, less validation headaches and better application design.
You are on the right track. Here is how I would model these relationships. Let's say you have a Teacher model, a Course model and a TeacherCourses model that will be our join table between teachers and courses:
class Teacher < ActiveRecord::Base
has_many :courses, through: :teacher_courses
end
class Course < ActiveRecord::Base
has_many :teachers, through: :teacher_courses
end
class TeacherCourse < ActiveRecord::Base
belongs_to :course
belongs_to :teacher
end
Your teacher_courses table would also have location attribute differentiating a record from the same course/teacher combo:
create_table :teacher_courses do |t|
t.integer :teacher_id
t.integer :course_id
t.string :location
t.timestamps
end

Rails where to create :through object

Hi I`m learning has_many :through and I have a association like this.
student:
has_many :subjects, through: :participations
has_many :participations
subject:
has_many :students, through: :participations
has_many :participations
belongs_to :student
participation:
belongs_to :student
belongs_to :subject
The student subjects are updated through checkboxes in update view:
= f.association :subjecs, label_method: :title, value_method: :id, label: 'Subjects', as: :check_boxes
And I went so far :( My student have subjects id, but it can`t get them since no participation is created.
My update action:
def create
student = Student.new(student_params)
if student.save
redirect_to students_path
else
render 'edit'
end
end
My question is when should I create participation object, and where is the appropriate place for the function ?
When you have a has_many relationship, you actually get a bunch of methods which can help you.
One of these is association_singular_ids- which if you populate it correctly, will automatically create the associative data you need.
The way to do this will be to use the following:
#app/views/students/new.html.erb
<%= form_for #student do |f| %>
<%= f.collection_check_boxes :subject_ids, Subject.all, :id, :name %>
<%= f.submit %>
<% end %>
I know you're using f.association (which is built through simple_form) - you'll be much better suited to using collection_check_boxes (it even explains an example of what you're having problems with).
You shouldn't need to pass the params or anything - and because your participations model acts as a join, it should be populated automatically if you use the above code.
HABTM
You may also wish to look at has_and_belongs_to_many:
#app/models/student.rb
class Student < ActiveRecord::Base
has_and_belongs_to_many :subjects
end
#app/models/subject.rb
class Subject < ActiveRecord::Base
has_and_belongs_to_many :students
end
#join table - students_subjects
This is often preferred over has_many :through because it requires less maintenance (as described in the link above).
Nested Attributes
Finally, to give you some more perspective, you'll need to know about the nested attributes aspect of Rails.
I originally thought your answer would be that you're not using accepts_nested_attributes_for, but I don't think so now. Nonetheless, you'll still gain benefit from knowing about it.
--
One of the reasons you'd use has_many through would be to populate the join model with other attributes. HABTM does not allow this; because has_many :through has a join model (in your case participations), it allows you to add extra attributes into it.
As such, if you're looking to change any of those attributes, you'll need to pass them through your various models:
#app/models/student.rb
class Student < ActiveRecord::Base
has_many :participations
has_many :subjects, through: :participations
accepts_nested_attributes_for :participations
end
#app/models/participation.rb
class Participation < ActiveRecord::Base
belongs_to :student
belongs_to :subject
accepts_nested_attributes_for :student
end
#app/models/subject.rb
class Subject< ActiveRecord::Base
has_many :participations
has_many :students, through: :participations
end
This will allow you to use the following:
#app/controllers/students_controller.rb
class StudentsController < ApplicationController
def new
#student = Student.new
#student.participations.build
end
def create
#student = Student.new student_params
#student.save
end
private
def student_params
params.require(:student).permit(:student, :params, participations_attributes: [subject_attributes:[]])
end
end
This should allow you to use the following:
#app/views/students/new.html.erb
<%= form_for #student do |f| %>
<%= f.fields_for :participations do |p| %>
<%= p.text_field :name %>
<%= p.collection_check_boxes :subject_id, Subject.all, :id, :name %>
<% end %>
<%= f.submit %>
<% end %>
My problem was that I didn`t permited the chenge od subject_ids in the student controller:
params.require(:student).permit(:first_name, :last_name, subject_ids: [])

Using a Many-to-Many Relationship with the Public Activity gem in Rails

I have the scenario where an author has and belongs to many books, vice versa. Following the instructions for setting up associations in a one-to-many relationship works fine but when a many-to-many relationship introduced I get this error message whenever I try to create or update my book model.
undefined method `author' for #<Book:0x007fb91ae56a70>
As far as setting up how authors are chosen for a book I'm using the code provided by the token-input railscast here with a few alterations.
class Author < ActiveRecord::Base
has_many :authorships
has_many :books, through: :authorships
def self.tokens(query)
authors = where("name like ?", "%#{query}%")
if authors.empty?
[{id: "<<<#{query}>>>", name: "Add New Author: \"#{query}\""}]
else
authors
end
end
def self.ids_from_tokens(tokens)
tokens.gsub!(/<<<(.+?)>>>/) {create!(name: $1).id}
tokens.split(',')
end
end
class Book < ActiveRecord::Base
attr_reader :author_tokens
include PublicActivity::Model
tracked owner: :author
has_many :authorships
has_many :authors, through: :authorships
def author_tokens=(ids)
self.author_ids = Author.ids_from_tokens(ids)
end
end
Form View
<%= form_for(#book) do |f| %>
...
<div class="field">
<%= f.text_field :author_tokens, label: 'Author', input_html: {"data-pre" => #book.authors.to_json} %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
There is no author relationship in your Book model.
What
tracked owner: :author
does is basically calling method author on your Book instance. You should try :authors
But!
That won't solve your problem because owner can only be one. So you can do something like:
tracked owner: proc {|_, book| book.authors.first }
to set the owner to the first author the book has.
class Author < ActiveRecord::Base
has_many :author_books, inverse_of: :author, dependent: :destroy
accepts_nested_attributes_for :author_books
has_many :books, through: :author_books
end
class Book < ActiveRecord::Base
has_many :author_books, inverse_of: :book, dependent: :destroy
accepts_nested_attributes_for :author_books
has_many :authors, through: :author_books
end
class AuthorBook < ActiveRecord::Base
validates_presence_of :book, :author
end
============= view ==============
<%= form_for #book do |f| %>
<%= f.text_field :title %>
<%= f.fields_for :author_books do |f2| %>
<%# will look through all author_books in the form builder.. %>
<%= f2.fields_for :author do |f3| %>
<%= f3.text_field :name %>
<% end %>
<% end %>
<% end %>

Rails: Creating form fields for join-table dependent on own model

I'm trying to build a form for a model with a join-table containing references to itself.
I'll give you a quick example of what i'm trying to achive:
Lets say i have the subject physics. To learn physics you will need to know basic math, i.e physics is dependent on math. Any subject should be able to have multiple dependencies both ways.
What i'm having problems with is submitting this relationship through a form.
My code:
Model:
class Subject < ActiveRecord::Base
has_many :needs, foreign_key: :target_id
has_many :subjects, through: :needs
accepts_nested_attributes_for :needs,:subjects,allow_destroy: true
end
class Need < ActiveRecord::Base
belongs_to :target, class_name: :subject
belongs_to :prerequisite, class_name: :subject
end
Controller:
class SubjectsController < ApplicationController
def create
#subject = Subject.new secure_params
if #subject.save
redirect_to root_path, success: 'Subject created'
else
render :new
end
end
def new
#subject = Subject.new
end
private
def secure_params
params.require(:subject).permit(:name, :content, needs_attributes: [:target,:prerequisite])
end
end
Form:
<div class="row">
<div class="col-md-8">
<%= simple_nested_form_for #subject do |f| %>
<%= f.input :name %>
<%= f.input :content %>
<%= f.fields_for :needs do |d| %>
<%=d.association :prerequisite %>
<% end %>
<%= f.link_to_add "Add a prerequisite", :needs %>
<%= f.submit class: 'btn btn-primary btn-lg' %>
<% end %>
</div>
</div>
Using this approach i get "uninitialized constant Need::subject" on entering the "new" action.
Any ideas on mistakes in my approach or code will be greatly appreciated.
Edit: adding join table
class CreateNeeds < ActiveRecord::Migration
def change
create_table :needs do |t|
t.references :target
t.references :prerequisite
end
end
end
Edit2: working code (only changed parts)
Model
class Subject < ActiveRecord::Base
has_many :needs, foreign_key: :target_id
has_many :prerequisites, class_name: "Subject",through: :needs, source: :prerequisites
accepts_nested_attributes_for :needs,:prerequisites,allow_destroy: true
end
class Need < ActiveRecord::Base
belongs_to :target, class_name: "Subject"
belongs_to :prerequisite, class_name: "Subject"
end
Controller
def secure_params
params.require(:subject).permit(:name, :content, needs_attributes: [:prerequisite_id])
end
end
Like i said in my comment, focus on making sure all the associations are hooked up properly before you worry about forms. The console, or automated tests, are both a good place to test this.
I think it maybe should be like so:
class Subject < ActiveRecord::Base
has_many :needs, as: :target
has_many :subjects, :class_name: "Subject", through: :needs, source: :prerequisite
accepts_nested_attributes_for :needs,:subjects,allow_destroy: true
end
#assuming you have fields target_id and prerequisite_id in this table
class Need < ActiveRecord::Base
belongs_to :target, class_name: :subject
belongs_to :prerequisite, class_name: :subject
end
I think the association name "subjects" is potentially confusing as it will return the subjects that this subject requires. This isn't immediately obvious to the reader, which could include you in the future.
At some point you may want to list the subjects that require this subject. It may be worth differentiating between these now, so you can clearly tell the difference. You could do this like so:
class Subject < ActiveRecord::Base
has_many :required_needs, as: :target
has_many :required_by_needs, as: :prerequisite
has_many :required_subjects, :class_name: "Subject", through: :required_needs, source: :prerequisite
has_many :required_by_subjects, :class_name: "Subject", through: :required_by_needs, source: :target
accepts_nested_attributes_for :required_needs, :required_by_needs, :required_subjects, :required_by_subjects, allow_destroy: true
end

Rails: How do I create a controller for a has_many :through using a join model?

I have two models "Stores" and "Vendors". I created a separate join model called "Partnerships" so a store can have many vendors and the Vendor can have many stores. When the user is logged in they are affiliated to either a store or vendor. I want them to be able to create partnerships. I think I have the model down based on my research, but I can't seem to get the controller right. Most of the online examples only show the models not the controller.
class Store < ActiveRecord::Base
attr_accessible :industry, :name
has_many :users
has_many :workorders
has_many :locations
has_many :partnerships
has_many :vendors, :through => :partnerships
class Vendor < ActiveRecord::Base
attr_accessible :industry, :name
has_many :users
has_many :workorders
has_many :locations
has_many :partnerships
has_many :stores, :through => :partnerships
class Partnership < ActiveRecord::Base
belongs_to :store
belongs_to :vendor
attr_accessible :store_id, :vendor_id, :store, :vendor
This is my current partnerships_controller#new where I get a type mismatch error during my test.
if params[:store_id]
#store = Store.where(:id => params[:store_id])
#partnership = Partnership.new(store: #store)
else
flash[:error] = "Store partnership required"
end
Here is my new.html.erb for Partnerships:
<% if flash[:error] %>
Not found.
<% else %>
<% if current_user.affiliation == 'Vendor' %>
<div class="page-header">
<h1> <%= #store.name %></h1>
</div>
<% else %>
<div class="page-header">
<h1> <%= #vendor.name %></h1>
</div>
<% end %>
<% end %>
My User model includes an affiliation field that is either "Store" or "Vendor" and also a company_id field.
What would my controller look like to create a new partnership if I am a user whose affiliation = 'Store'? Would I do it in the partnership controller?

Resources