Rails has many through association setting multiple attributes - ruby-on-rails

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!

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

How would I get all posts that a tag belongs to? Rails

So the way I did things for these model set ups is a bit different then what you might actually do. How ever I did things like this:
Post Model
class Post < ActiveRecord::Base
belongs_to :blog
belongs_to :user
has_and_belongs_to_many :tags, join_table: 'tags_posts', :dependent => :destroy
has_and_belongs_to_many :categories, join_table: 'categories_posts', :dependent => :destroy
has_many :comments, :dependent => :destroy
validates :title, presence: true
def has_tag?(tag_name)
tags.where(name: tag_name).any?
end
def tag_names
tags.pluck(:name)
end
def tag_names=(names)
self.tags = names.map{ |name| Tag.where(name: name).first }
end
def tag_name=(tag_name)
single_tag = [tag_name]
self.tag_names = single_tag
end
def has_category?(category_name)
categories.where(name: category_name).any?
end
def category_names
categories.pluck(:name)
end
def category_names=(names)
self.categories = names.map{ |name| Category.where(name: name).first }
end
def category_name=(category_name)
single_category_name = [category_name]
self.category_names = single_category_name
end
def user=(id)
user = User.find_by(id: id)
self.user_id = user.id if user
end
end
The above allows us to assign tags and categories and a post to a user (the last part is being refactored out as we speak). You can also get all tags and categories for a post and see if that post has a particular category.
Now what I want to do, in the tags model (for now) is get all the posts that a tag belongs to. But I am not sure how to do that ...
this is my tags model:
Tags Model
class Tag < ActiveRecord::Base
belongs_to :blog
validates :name, presence: true, uniqueness: true
end
How do I accomplish what I want?
I am not sure how to do this with has_and_belong_to_many. However, it would be pretty easy using has many through. By Rails conventions, the same of your join table should be tag_posts or post_tags (the first model is singular).
In your Post model:
has_many :tag_posts
has_many :tags, :through => :tag_posts
Then in your Tag model, a similar setup:
has_many :tag_posts
has_many :posts, :through => :tag_posts
Finally, you would create a TagPost model
belongs_to :tag
belongs_to :post
After that, calling tag.posts should return all posts for a given tag.

Dependent destroy does not destroy dependencies

I have the following models:
class Article < ActiveRecord::Base
has_many :comments, :as => :subject, :dependent => :destroy
has_many :deleted_comments, :as => :subject, :dependent => :destroy
end
class DeletedComment < ActiveRecord::Base
belongs_to :subject, :polymorphic => true
end
class Comment < ActiveRecord::Base
belongs_to :subject, :polymorphic => true
before_destroy :create_deleted_comment
def create_deleted_comment
DeletedComment.create!(....)
end
end
In my database, I have quite a few DeletedComment objects where the subject is nil. The DeletedComment (and Comment) model stores :article_id, and for the ones where the subject is nil, Article.find(deleted_comment.article_id) raises an ActiveRecord::RecordNotFound error.
Are there any cases where the :dependent => :destroy would destroy the parent record but leave the dependencies untouched?
Is it possible that in some cases when I delete an Article the deleted_comments are destroyed before comments? and when the comments are destroyed, deleted_comments are created and not destroyed (because ActiveRecord has already checked the dependent deleted_comment and tried to destroy any dependencies)?
According to official documentation:
Using polymorphic associations in combination with single table inheritance (STI) is a little tricky. In order for the associations to work as expected, ensure that you store the base model for the STI models in the type column of the polymorphic association. To continue with the asset example above, suppose there are guest posts and member posts that use the posts table for STI. In this case, there must be a type column in the posts table.
class Asset < ActiveRecord::Base
belongs_to :attachable, polymorphic: true
def attachable_type=(sType)
super(sType.to_s.classify.constantize.base_class.to_s)
end
end
class Post < ActiveRecord::Base
# because we store "Post" in attachable_type now dependent: :destroy will work
has_many :assets, as: :attachable, dependent: :destroy
end
class GuestPost < Post
end
class MemberPost < Post
end
I guess you could use examle and do something like:
class Article < ActiveRecord::Base
# for deletion only
has_many :abstract_comments, :as => :subject, :dependent => :destroy
# for 'manual' access/edition
has_many :comments, :as => :subject
has_many :deleted_comments, :as => :subject
end
class AbstractComment < ActiveRecord::Base
belongs_to :subject, :polymorphic => true
def attachable_type=(sType)
super(sType.to_s.classify.constantize.base_class.to_s)
end
end
class DeletedComment < AbstractComment
end
class Comment < AbstractComment
before_destroy :create_deleted_comment
def create_deleted_comment
DeletedComment.create!(....)
end
end

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