Rails multiple has_many :through and displaying associations - ruby-on-rails

I'm building an application where a user can build a lesson from an assortment of standards and questions to teach the standards, but I'm not exactly sure if I have set up everything correctly or not.
The 'new' page allows the user to use drop down menus to sort through to select the standards through the Lesson Controller
def new
#search = Standard.search(params[:q])
#standards = #search.result
#lesson = Lesson.new
end
def create
#lesson = current_user.selects.build(params[:lesson])
if #lesson.save
redirect_to edit_lesson_path(#lesson)
else
render :action => 'new'
end
end
def edit
#lesson = Lesson.find(params[:id])
#standards = #lesson.standards
end
Once the standards are selected, the user is redirected to the 'edit' page which shows each of the selected standards, but this is the part where I'm having trouble with and I'm not sure my models are set up correctly. There is a has_many through relationship between lessons and standards to select standards, and also a has_many through relationship between lessons and questions as well to select the questions associated with each standard.
I'm trying to list each of the questions associated with the standards underneath the parent standard, I have tried #questions = #standards.questions in the 'edit' method, but an ActiveRecord Relation NoMethod error is called. I have also tried #questions = Question.where(:standard_id => #standards) in the controller, but the page lists all of the questions for all of the selected standards underneath each standard.
My lesson model:
class Lesson < ActiveRecord::Base
attr_accessible :user_id, :name, :grade_id, :text_id, :date, :subject_id, :question_ids
has_many :select_standards
has_many :standards, through: :select_standards
has_many :select_questions
has_many :questions, through: :select_questions
end
Standard model:
class Standard < ActiveRecord::Base
attr_accessible :content, :grade_id, :subject_id
belongs_to :subject
belongs_to :grade
has_many :questions
end
Question model:
class Question < ActiveRecord::Base
attr_accessible :content, :standard_id
belongs_to :standard
has_many :select_questions
has_many :lessons, through: :select_questions
end
Select_standards:
class Selection < ActiveRecord::Base
attr_accessible :lesson_id, :standard_id
belongs_to :lesson
belongs_to :standard
end

The problem seems to be related to Rails not being able to figure out proper class names for your association names. For example, you've specified has_many :select_standards for your class Selection, by default Rails would search for SelectStandard class for association name :select_standards. The fix is easy in this case, which is to either change the association declaration to:
has_many :selections
or add class_name to association as:
has_many :select_standards, class_name: 'Selection'
You need to make sure that this is done for any and all the custom association names that do not match the actual ActiveRecord inherited class names.

Related

How to create a complex Nested -> Belongs To object?

I'm building a form where a user should be able to create a template with fields. However they should also be able to create an optional Collection which belongs_to each Field at the same time.
So far it looks like this:
Tables
templates: id, name
template_fields: id, template_id, collection_id, field_name
collections: id, name
collection_values: id, collection_id, value
Models
Template
class Template < ActiveRecord::Base
has_many :template_fields
accepts_nested_attributes_for :template_fields
end
Template Fields
class TemplateField < ActiveRecord::Base
belongs_to :template
belongs_to :collection
end
Collection
class Collection < ActiveRecord::Base
has_many :collection_values
end
CollectionValue
class CollectionValue < ActiveRecord::Base
belongs_to :collection
end
How can I create these objects? How do I go about this in the controller?
The only way I can think of is to create it as a nested association (even though Collection isn't really a nested attribute) or create the Collections before the Templates, and somehow link each Field to each Collection that was created.
Controller
def create
#template = Template.new(template_params)
if #template.save
flash[:notice] = "Template successfully created."
flash[:color]= "valid"
redirect_to templates_path
else
flash[:notice] = "Form is invalid"
flash[:color]= "invalid"
render :new
end
end
def template_params
params.require(:template).permit(:id,:name, template_fields_attributes: [:id, :template_id, :field_name, :collection_id, collection_values_attributes: [:value] ])
end
I think you need to add these kind of relationships to your model has_many :through and has_one :through to relate collection and collection_values to template and then modify your template.rb to include:
has_one :collection, through: :template_fields
The line above assumes that one template has only one collection
has_many :collection_values, through: :collection
accepts_nested_attributes_for :template_fields, :collection, :collection_values
You also need to modify the template_params in your controller to include collection_attributes and collection_values_attributes
For a more detailed implementation, your template_model will look like this
Template
class Template < ActiveRecord::Base
has_many :template_fields
has_many :collections, through: :template_fields
has_many :collection_values, through: :collection
accepts_nested_attributes_for :template_fields, :collections, :collection_values
end
For requirements given you may have:
class CollectionValue < ActiveRecord::Base
belongs_to :collection
end
class Collection < ActiveRecord::Base
has_many :collection_values
accepts_nested_attributes_for :collection_values
end
class TemplateField < ActiveRecord::Base
belongs_to :template
belongs_to :collection
accepts_nested_attributes_for :collection
end
class Template < ActiveRecord::Base
has_many :template_fields
accepts_nested_attributes_for :template_fields
end
That would make following code work:
Template.create name: 'template_name',
template_fields_attributes: [
{name: 'field_name', collection_attributes:
{name: 'collection_name', collection_values_attributes: [
{name: 'col_value_1'},
{name: 'col_value_2'}
]}
}
]
You may try it in rails console. This example would create all of records as it doesn't include any IDs. It would work same way in TemplateController#create
It's better for you to read and understand accepts_nested_attributes_for

Add an attribute to a joining model in Rails has_many through

I have a User model and a Book model joined with a Like model.
class Book < ActiveRecord::Base
belongs_to :user
# the like associations
has_many :likes
has_many :liking_users, :through => :likes, :source => :user
class User < ActiveRecord::Base
has_many :books
# the like associations
has_many :likes
has_many :liked_books, :through => :likes, :source => :book
class Like < ActiveRecord::Base
belongs_to :user
belongs_to :book
I want to add an attribute to the Like model so while right now a User can Like a book to add it to their profile, I want the User to be able to write a recommendation.
I generated the attribute Recommendation:text to the Like (joining) model, but am unsure how to add this to views so a User can write a recommendation that will be tied to the Like (and thus that book and user).
I'm looking at this post - Rails has_many :through Find by Extra Attributes in Join Model - which describes something similar but does not explain how to implement this in the views.
Let me know if you can point me in the right direction. Thanks!
I think you should create a separate model for this Recommendation, dependent of the Like model:
class Book < ActiveRecord::Base
belongs_to :user
has_many :comments
has_many :commenters, :through => :comments, :source => :user
class User < ActiveRecord::Base
has_many :books
has_many :comments
has_many :commented_books, :through => :comments, :source => :book
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :book
Then, the logic to create a comment for a book:
# Books Controller
def show
#book = Book.find(params[:id])
#comment = #book.comments.build
end
# show view of the book
form_for #comment do |form|
form.hidden_field :user_id, value: current_user.id
form.hidden_field :book_id
form.text_area :content # the name of the attribute for the content of the comment
form.submit "Post comment!"
end
To list all the comments of a specific User:
# users controller (profile page)
def show
#user = User.find(params[:id])
end
# show view of Users
#user.comments.includes(:book).each do |comment|
"Comment on the book '#{comment.book.name}' :"
comment.content
end
I suppose that the like functionnality works with ajax ? (remote: :true or in pure javascript)
You can append a form with the response of your request with the id of the new like, and again handle the recommendation by an ajax request

rails model assignment with has_many :through

I just can't figure out how to create a relation with a join table. I've read all the posts about them, but the main error seems to be that in the join table to models should be singular, which I have. I just can seem to create the models correctly and assign them. I have projects with datasets, and projects can have multiple datasets, while a dataset can belong to multiple projects. A dataset can be active or not, which is why I need the has_many through instead of the has_many_and_belongs_to setup.
My model definitions are:
class Project < ActiveRecord::Base
attr_accessible :name, :user_id
belongs_to :user
has_many :activedatasets
has_many :datasets, :through => :activedatasets
end
class DataSet < ActiveRecord::Base
attr_accessible :name, :project_id, :filename, :tempfilename
has_many :activedatasets
has_many :projects, :through => :activedatasets
end
class ActiveDataSet < ActiveRecord::Base
attr_accessible :active, :data_set_id, :project_id
belongs_to :project
belongs_to :dataset
end
When I create a new dataset I've got the project_id in the params, so I'm trying to setup the relationship like below:
class DataSetsController < ApplicationController
def new
#dataset = DataSet.new
#dataset.activedatasets.project_id = params[:project_id]
end
end
The error I'm getting seems famous:
NameError in DataSetsController#new
uninitialized constant DataSet::Activedataset
Can anybody point me in the right direction please?
Thanks for you attention.
You need to use:
has_many :active_data_sets
has_many :data_sets, :through => :active_data_sets
And in the DataSet model:
has_many :active_data_sets
has_many :projects, :through => :active_data_sets
Basically, rails expects you to use underscores to separate words in association names, and converts them to CamelCase. So active_data_sets becomes ActiveDataSet. Rails then uses this to work out which model class the association is with.
You also need to change your controller to this:
class DataSetsController < ApplicationController
def new
#dataset = DataSet.new
#dataset.active_data_sets.build(:project_id => params[:project_id])
end
end
Otherwise you'll get an error because you tried to set the project_id of the active_data_sets collection rather than creating a new ActiveDataSet.

Rails has_many through set an attribute on creation

I have setup a has_many through association between two models in Ruby on Rails. The setup is as follows. The models are User and Document, the join model is Ownership. The models are defined like this:
class Ownership < ActiveRecord::Base
attr_accessible :document_id, :user_id
belongs_to :user
belongs_to :document
end
class User < ActiveRecord::Base
has_many :ownerships
has_many :documents, :through => :ownerships
end
class Document < ActiveRecord::Base
has_many :ownerships
has_many :users, :as => :owners, :through => :ownerships
end
Now my question is how to set the user that creates a document as the owner of the document when it gets created. The project also uses devise, cancan and rolify for user handling. I tried to set it in the new action of the Codument controller like this but with no successs
def new
#document = Document.new
#document.users = current_user
respond_to do |format|
format.html # new.html.erb
format.json { render json: #document }
end
end
How can I do this properly? And is the new action of my Document controller the right place at all to something like this? Any help would be appreciated.
First off, you need to assign the user in the controller's create method. Second, since a document can have many users, #document.users is an enumerable and cannot simply be assigned a single user by doing
#document.users = current_user
You can rather do:
#document.owners << current_user
in the create method. Note that as per your model the document has owners rather than users.
Change
has_many :users, :as => :owners, :through => :ownerships
to
has_many :owners, source: :user, through: :ownerships, foreign_key: :user_id
in your document model.
This stores the current user when the document is saved.

RoR: Creating a relationship between a purchased product and a user

Hello and thanks for any help in advance.
I want to create a relationship between a user and a product(pdf) upon an order being saved.
class PDF
has_many :line_items
end
class LineItem
belongs_to :pdf
belongs_to :cart
end
class Cart
has_many :line_items
has_one :order
end
class Order
belongs_to :cart
end
Upon the user purchasing a line_item, I would like to create relationships through a join model between user and pdf (pdf_relationships).
I'm trying to find each PDF(found by foreign_key line_item.pdf_id) in a given cart and create pdf_relationships between a user and each pdf in the cart. I'll be making the user's id the owner id and making the pdf's id the owned_id.
My order controller looks like this:
def create
#order = current_cart.build_order(params[:order])
#order.ip_address = request.remote_ip
if #order.save
if #order.purchase
render :action => "success"
else
render :action => "failure"
end
else
render :action => 'new'
end
end
and what I'm having trouble with is this:
class Order
belongs_to :cart
before_save :create_owner
***def create_owner
self.cart.line_items.each do |item|
pdf.find_by_item_pdf_id(:pdf_id)
current_user.pdf_relationships.build(:owned_id => pdf.id)
end
end***
end
here is my user model:
class User
has_many :line_items
has_many :pdf_relationships, foreign_key: :owner_id, :dependent => :destroy
has_many :pdfs, foreign_key: :user_id, dependent: :destroy
has_many :pdf_ownings, :through => :pdf_relationships, :source => :owned
def owning?(owned)
pdf_relationships.find_by_pdf_owned_id(owned)
end
def own!(owned)
pdf_relationships.create!(:owned_id => owned.id)
end
def unown!(owned)
pdf_relationships.find_by_pdf_owned_id(owned).destroy
end
I hope this is clear enough. I've been trying to figure this out for sometime now and definitely trying to get past being just a novice. Suggestive readings are definitely welcome too!
It looks like you have the right idea.
Since LineItem has many PDFs, you can just use the pdf_id from the LineItem without bothering to fetch the record from the database. You can also append those IDs to the existing set of user-PDF associations as if you were just pushing items onto an array.
However, your model won't have access to the current session (since that's handled by the controller), so you'll have to pass the current user to the Order some other way, perhaps as part of the purchase method.
class Order
belongs_to :cart
def purchase(user)
# ... existing logic ...
user.pdf_relationship_ids << cart.line_items.map(&:pdf_id)
end
end
You will also have to declare the association between User and PDF and create the migrations, but it sounds like you're already planning to do that.
Update: I think you can greatly simplify your User model by taking advantage of has_many :through. Here's what I envision:
class User < ActiveRecord::Base
has_many :orders
has_many :line_items, :through => :orders
has_many :pdfs, :through => :line_items
end
class Order < ActiveRecord::Base
belongs_to :user
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :order
has_one :pdf
end
class Pdf < ActiveRecord::Base
belongs_to :line_item
end
Then you don't have to explicitly specify that a user owns a PDF. It's implicit in the user -> order -> line item -> PDF relationship.
This may not solve your problem as-is. You wouldn't be able to "disown" a PDF without deleting the original line item, which is probably not what you want. But I think you should try to aim for something like this. Take advantage of Rails's built-in associations as much as you can.

Resources