Rails has_many through set an attribute on creation - ruby-on-rails

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.

Related

Button to connect an association in Rails is not working. Transfer of ownership

I am working my first Rails project, an adoption app and trying to bridge an association to a new potential owner in Rails. My controller action is moving through my adoption_request method, but no changes are being persisted to my join table in ActiveRecord. Can someone please tell me what I am missing here?
The app:
Owners sign up or log in to their account. They can add their Ferret using a form. Later, the Owner may want to create an Opportunity listing to adopt/rehome their animal. People browsing should be able to click on an Opportunity they are interested in, which should establish an association in the join table Opportunity, :adopter_id.
My Models:
class Owner < ApplicationRecord
has_secure_password
has_many :ferrets, dependent: :destroy
has_many :opportunities, dependent: :destroy
has_many :ferret_adoptions, through: :opportunities, source: :ferret
accepts_nested_attributes_for :ferrets, :opportunities
end
class Ferret < ApplicationRecord
belongs_to :owner
has_many :opportunities
has_many :owners, through: :opportunities
end
class Opportunity < ApplicationRecord
belongs_to :ferret
belongs_to :owner
end
In Opportunities Controller, my adoption_request method:
def adoption_request
#owner = Owner.find(session[:owner_id])
#opportunity = Opportunity.find(params[:id])
#opportunity.adopter_id = [] << current_user.id
current_user.req_id = [] << #opportunity.id
flash[:message] = "Adoption request submitted."
redirect_to questions_path
end
I am using a button to do this, but I am open to change that if something may work better:
<button><%= link_to 'Adoption Request', adoption_request_path, method: :post %> <i class='fas fa-heart' style='color:crimson'></i></button>
As an Owner when I click the button to make an Adoption Request, I am seeing all the working parts in byebug, and I am being redirected to the next page with the success message as if everything worked, but there is no Association actually being persisted to the database.
I appreciate any feedback you can offer.
I'm assuming here that Opportunity should represent something like a listing (it needs a less vague name).
If so you're missing a model and its table if ever want more then one user to be able to respond to an Opportunity:
class Owner < ApplicationRecord
has_secure_password
has_many :ferrets, dependent: :destroy
has_many :opportunities, dependent: :destroy
has_many :adoption_requests_as_adopter,
foreign_key: :adopter_id,
class_name: 'AdoptionRequest'
has_many :adoption_requests_as_owner,
through: :opportunities,
source: :adoption_requests
accepts_nested_attributes_for :ferrets, :opportunities
end
class Ferret < ApplicationRecord
belongs_to :owner
has_many :opportunities
has_many :owners, through: :opportunities
has_many :adoption_requests, through: :opportunities
end
class Opportunity < ApplicationRecord
belongs_to :ferret
belongs_to :owner
has_many :adoption_requests
end
class AdoptionRequest < ApplicationRecord
belongs_to :adopter, class_name: 'Owner' # ???
belongs_to :opportunity
has_one :ferret, through: :opportunity
has_one :owner, through: :opportunity
end
If you just have a adopter_id on your opportunities table it can only ever hold a single value.
I would just set the route / controller up as a normal CRUD controller for a nested resource:
# routes.rb
resources :opportunities do
resources :adoption_requests, only: [:create, :index]
end
<%= button_to "Adopt this ferret", opportunity_adoption_requests_path(#opportunity), method: :post %>
class AdoptionRequestsController < ApplicationController
before_action :set_opportunity
# #todo authorize so that it can only be viewed by the owner
# GET /opportunities/1/adoption_requests
def index
#adoption_requests = #opportunity.adoption_requests
end
# #todo authorize so that a current owner can't create adoption_requests
# for their own ferrets
# POST /opportunities/1/adoption_requests
def create
#adoption_request = #opportunity.adoption_requests.new(
adopter: current_user
)
if #adoption_request.save
redirect_to #opportunity, notice: 'Thank you for your reply! The owner of the ferret will be notified.'
# #todo send notification to owner
else
redirect_to #opportunity, notice: 'Oh noes!'
end
end
private
def set_opportunity
#opportunity = Opportunity.find(params[:opportunity_id])
end
end
Its only later when the owner actually accepts a adoption_request that you will actually update the opportunity and this is a seperate question for a later time.

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

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!

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.

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