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

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

Related

Rails has many through association setting multiple attributes

So I have a has_many through association where between two tables posts and users:
class Post < ApplicationRecord
has_many :assignments
has_many :users, :through => :assignments
end
class User < ApplicationRecord
has_many :assignments
has_many :posts, :through => :assignments
end
class Assignment < ApplicationRecord
belongs_to :request
belongs_to :user
end
Now in my association table (assignment) there are additional attributes for creator:boolean and editor:boolean.
My question is what's the best way to set these secondary attributes from within the controller?
Having looked around I've got a current solution:
posts_controller.rb:
class PostsController < ApplicationController
def create
params.permit!
#post = Post.new(post_params)
if #post.save
Assignment.handle_post(#post.id, params[:creator], params[:editors])
redirect_to posts_path, notice: "The post #{#post.title} has been created."
else
render "new"
end
end
assignment.rb:
class Assignment < ApplicationRecord
belongs_to :request
belongs_to :user
def self.handle_post(post_id, creator, assignment)
Assignment.where(:post_id => post_id).delete_all
Assignment.create!(:post_id => post_id, :user_id => creator, :creator => true, :editor => false)
if editors.present?
editors.each do |e|
Assignment.create!(:post_id => post_id, :user_id => e, :creator => false, :editor => true)
end
end
end
end
So what is essentially happening is I'm getting the user_ids from the form via params (creator returns 1 id, editors returns an array), and AFTER creating the post I'm deleting all columns associated with the post and recreating them off the new attributes.
The issue I have here is I can't run post validations on these associations (e.g. check a creator is present).
My two questions are as follows:
Is this the correct way to handle secondary attributes?
Is there a way to set the association up and then save it all at once so validations can be performed?
This is a more Rails way to do this:
Use nested attributes
post.rb
class Post < ApplicationRecord
# Associations
has_many :assignments, inverse_of: :post
has_many :users, through: :assignments
accepts_nested_attributes_for :assignments
# Your logic
end
assignment.rb
class Assignment < ApplicationRecord
after_create :set_editors
belongs_to :request
belongs_to :user
belongs_to :post, inverse_of: :assignments
# I would create attribute accessors to handle the values passed to the model
attr_accessor :editors
# Your validations go here
validates :user_id, presence: true
# Your logic
private
def set_editors
# you can perform deeper vaidation here for the editors attribute
if editors.present?
editors.each do |e|
Assignment.create!(post_id: post_id, user_id: e, creator: false, editor: true)
end
end
end
end
And finally, add this to your PostsController
params.require(:post).permit(..., assignments_attributes: [...])
This allows you to create Assignments from the create Post action, will run validations on Post and Assignment and run callbacks for you.
I hope this helps!

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 multiple has_many :through and displaying associations

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.

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.

How can I use form_for to update an association's has_many :through association

In my form for member_profile, I would like to have role checkboxes that are visible for admins. I would like to used some nested form_for, but can't make it work, so I've resorted to manually creating the check_box_tags (see below), and then manually adding them to member_profile.member.
Note that the Member model is Devise, and I don't want to mix those fields in with my MemberProfile data, in case I change auth systems in the future.
class Member < ActiveRecord::Base
has_one :member_profile
has_many :member_roles
has_many :roles, :through => :member_roles
end
class MemberProfile < ActiveRecord::Base
belongs_to :member
has_many :member_roles, :through => :member
#has_many :roles, :through => :member_roles #can't make this work work
end
class Role < ActiveRecord::Base
has_many :member_roles
validates_presence_of :name
end
class MemberRole < ActiveRecord::Base
belongs_to :member
belongs_to :role
end
Form (haml)
= form_section do
- Role.all.each do |x|
=check_box_tag 'member[role_ids][]',
x.id,
begin #resource.member.role_ids.include?(x.id) rescue nil end
=x.name
member_profiles_controller.rb
def update
if #resource.update_attributes params[:member_profile]
#resource.member.role_ids = params[:member][:role_ids]
redirect_to(#resource, :notice => 'Member profile was successfully updated.')
else
render :action => "edit"
end
end
I've decided it only makes sense to do a nested has_many :through on Update, since the join model is what is being 'gone through' to get to the has_many :through model. Before the hmt is created, there is obviously no record in the join model.

Resources