How can i make multiple model associations through an html form? - ruby-on-rails

I would like to create pages for Discussions on a website, and on those discussion pages have users be able to write posts. The posts need to belong to the discussion and a user, and the discussion to a user.
I have thus created two models, two controllers, and one partial to put on the discussion show page. Note that the redirects from the controllers are just assigned to root_pages and others in no logical fashion, as I wanted to deal with redirects once I got the form working. I didn't attach the user model, as it is long and I didn't think it was necessary.
My problem is that I can't get the post controller to assign the correct discussion id to a new post. I'd like for this to be recorded, so that posts are associated to the author user_id (which works) and the discussion_id. I know that using #post.discussion_id = #discussion.id won't assign this properly, but I have tested #post.discussion_id = 1 to see if the rest of the code works (it does).
How do I change the set-up of the forms/controllers to assign the discussion_id here? Any help would be much appreciated!
Discussion Controller:
class DiscussionsController < ApplicationController
def show
#discussion = Discussion.find(params[:id])
#title = #discussion.title
#post = Post.new if signed_in?
end
end
Discussion Model:
class Discussion < ActiveRecord::Base
attr_accessible :title, :prompt
belongs_to :user
validates :title, :presence => true, :length => { :within => 5..100 }
validates :prompt, :presence => true, :length => { :within => 5..250 }
validates :user_id, :presence => true
has_many :posts, :dependent => :destroy
default_scope :order => 'discussions.created_at DESC'
end
Post Controller:
class PostsController < ApplicationController
def create
#post = current_user.posts.build(params[:post])
#post.discussion_id = #discussion.id
if #post.save
redirect_to discussion_path
else
redirect_to user_path
end
end
end
Post Model:
class Post < ActiveRecord::Base
attr_accessible :content
validates :content, :presence => true, :length => { :maximum => 10000 }
validates :user_id, :presence => true
validates :discussion_id, :presence => true
belongs_to :user
belongs_to :discussion
default_scope :order => 'posts.created_at ASC'
end
Partial for post form:
<%= form_for #post do |f| %>
<%= render 'shared/error_messages' %>
<div class="field">
<%= f.text_area :content, :class => "inputform largeinputform round" %>
</div>
<div class="actions">
<%= f.submit "Post", :class => "submitbutton round" %>
</div>
<% end %>

Your issue is that you do not have a mechanism to get the #discussion into the post controller One approach might be to put the discussion id in a hidden field in your partial form and then read it in your controller as a param.
Lance

in PostsController you're not creating a #discussion in create method.

Related

Rails: how to add user ownership of a picture album in the album/new form?

Hi I'm currently working on my first rails project, a site for users to make albums and upload pics. I have the registration, logging in, and friending installed into my app. I'm trying to make it so that in the album creation form, you can see a list of your friends and select who you want to share access to the album with (meaning whoever you select would also be part of #album.users. I'm planning on using a checkbox (I can't think of any better way) to make this selection. However, I am not sure how to link the friendship model with the album/new form. This is how my form looks like:
album/new.html.erb
<%= form_for ([#user, #album]), :html => { :id => "uploadform", :multipart => true } do |f| %>
<div class="formholder">
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.check_box :friends %>
<%= f.label :description %>
<%= f.text_area :description %>
<br>
<%=f.submit %>
</div>
<% end %>
an error occurs on line #6 (
<%= f.check_box :friends %>
the error:
undefined method 'friends' for #<Album:0x007fa3a4a8abc0>
I can understand why, but I don't know how to fix it. I have the typical friendship join model to add friends, and I want to be able to see a list of all the friends and select them. I think a following step would be to add something like #album.users << #user.friendships.find_by_name(params[:friends]) in the create action in the albums controller, but I don't know how I would loop through the form that only returns one param for friends?
Here are my files:
Albums controller create action:
def create
#user = User.find(params[:user_id])
#album = #user.albums.build(params[:album])
# not so sure about the following line.
#album.users << #user.friendships.find_by_name(params[:friends])
respond_to do |format|
if #user.save
format.html { redirect_to user_album_path(#user, #album), notice: 'Album was successfully created.' }
format.json { render json: #album, status: :created, location: #album}
else
format.html { render action: "new" }
format.json { render json: #album.errors, status: :unprocessable_entity }
end
end
end
album model
class Album < ActiveRecord::Base
attr_accessible :name, :description
validates_presence_of :name
has_many :album_users
has_many :users, :through => :album_user
has_many :photos
end
user model
class User < ActiveRecord::Base
has_secure_password
attr_accessible :email, :name, :password, :password_confirmation
validates_presence_of :password, :on => :create
validates_format_of :name, :with => /[A-Za-z]+/, :on => :create
validates_format_of :email, :with => /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
validates_length_of :password, :minimum => 5, :on => :create
has_many :album_users
has_many :albums, :through => :album_users
accepts_nested_attributes_for :albums
has_many :friendships
has_many :friends, :through => :friendships
before_save { |user| user.email = email.downcase }
before_save :create_remember_token
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64
end
end
album_user model (join table to make many-to-many relationship between album, which has many users, and users, which has many albums)
class AlbumUser < ActiveRecord::Base
belongs_to :album
belongs_to :user
end
friendship model
class Friendship < ActiveRecord::Base
attr_accessible :friend_id
belongs_to :user
belongs_to :friend, :class_name => "User"
end
let me know if you need any more info!! Thanks in advance!!!
You should add users_ids (yes, two "s") to the list of accessible attributes of Album, and then use a "select multiple" on the :users_ids field.
<%= f.collection_select(:users_ids, User.all, :id, :name, :multiple => true) %>

Ruby on Rails: How to validate nested attributes on certain condition?

I have these models:
class Organisation < ActiveRecord::Base
has_many :people
has_one :address, :as => :addressable,
:dependent => :destroy
accepts_nested_attributes_for :address, :allow_destroy => true
end
class Person < ActiveRecord::Base
attr_accessible :first_name, :last_name, :email, :organisation_id, :address_attributes
belongs_to :user
belongs_to :organisation
has_one :address, :as => :addressable,
:dependent => :destroy
accepts_nested_attributes_for :address, :allow_destroy => true
# These two methods seem to have no effect at all!
validates_presence_of :organisation, :unless => "address.present?"
validates_associated :address, :unless => "organisation.present?"
end
class Address < ActiveRecord::Base
belongs_to :addressable, :polymorphic => true
validates_presence_of :line1, :line2, :city, :zip
end
...and these views:
_fields.html.erb:
<%= render 'shared/error_messages', :object => f.object %>
<fieldset>
<div class="left">
<%= f.label :first_name %><br/>
<%= f.text_field :first_name %>
</div>
<div>
<%= f.label :last_name %><br/>
<%= f.text_field :last_name %>
</div>
<div>
<%= f.label :email %><br/>
<%= f.text_field :email %>
</div>
<div>
<%= f.label :organisation_id %><br/>
<%= f.select(:organisation_id, current_user.organisation_names, {:include_blank => "--- None ---"}, :id => 'organisation_select') %>
</div>
</fieldset>
<%= f.fields_for :address do |address| %>
<%= render 'shared/address', :f => address %>
<% end %>
_address.html.erb:
<fieldset id="address_fields">
<div>
<%= f.label :line1 %>
<%= f.text_field :line1 %>
</div>
<div>
<%= f.label :line2 %>
<%= f.text_field :line2 %>
</div>
<div>
<%= f.label :zip %>
<%= f.text_field :zip %>
</div>
<div>
<%= f.label :city %>
<%= f.text_field :city %>
</div>
</fieldset>
people_controller.rb:
def new
puts params.inspect
#person = Person.new(:organisation_id => params[:organisation_id])
#person.build_address
#title = "New person"
end
{"action"=>"new", "controller"=>"people"}
def edit
puts params.inspect
#title = #person.name
end
{"action"=>"edit", "id"=>"69", "controller"=>"people"}
def create
puts params.inspect
if params[:organisation_id]
#person = current_user.organisations.build_person(params[:person])
else
#person = current_user.people.build(params[:person])
end
if #person.save
flash[:success] = "Person created."
redirect_to people_path
else
render :action => "new"
end
end
{"commit"=>"Create", "action"=>"create", "person"=>{"last_name"=>"Doe", "organisation_id"=>"9", "email"=>"john.doe#email.com", "first_name"=>"John", "address_attributes"=>{"city"=>"Chicago", "zip"=>"12345", "line2"=>"Apt 1", "line1"=>"1 Main Street"}}, "authenticity_token"=>"Jp3XVLbA3X1SOigPezYFfEol0FGjcMHRTy6jQeM1OuI=", "controller"=>"people", "utf8"=>"✓"}
Inside my Person model I need to make sure that only if a person's organisation_id is blank, that person's address fields have to be present.
I tried something like this:
validates :address, :presence => true, :if => "organisation_id.blank?"
But it's not working.
How can this be done?
Thanks for any help.
First of all, I want to be sure that you mean blank? rather than present?. Typically, I see this:
validate :address, :presence_of => true, :if => 'organisation.present?'
Meaning, you only want to validate address if organisation is also present.
Regarding, :accepts_nested_attributes_for, are you using this feature by passing in nested form attributes, or some such thing? I just want to make sure you absolutely need to use this functionality. If you are not actually dealing with nested form attributes, you can implement cascading validation using:
validates_associated :address
If you do need to use :accepts_nested_attributes, be sure to check out the :reject_if parameter. Basically, you can reject adding an attribute (and it's descendants) altogether if certain conditions apply:
accepts_nested_attributes_for :address, :allow_destroy => true, :reject_if => :no_organisation
def no_organisation(attributes)
attributes[:organisation_id].blank?
end
Now, if none of the above apply, let's take a look at your syntax:
It should work, :if/:unless take symbols, strings and procs. You don't need to point to the foreign_key, but can simplify by pointing to:
:if => "organisation.blank?"
You have other validations in the Address model, correct? Is Address being validated when you don't want it to? Or is Address not being validated? I can help you test it out in the console if you can give me some additional details.
To make things easier for myself re: mass-assignment, I changed the rails config: config.active_record.whitelist_attributes = false
I created a gist for you to follow along
I have a sample project as well. Let me know if you are interested.
Basic points:
Added the following to Person to ensure that either Org or Address are valid:
validates_presence_of :organisation, :unless => "address.present?"
validates_associated :address, :unless => "organisation.present?"
Added validation to Address to trigger errors when Org is not present:
validates_presence_of :line1, :line2, :city, :zip
I was able to produce the requirements you are seeking. Please look at the gist I created where I have a full console test plan.
I added a controller file to the previous gist.
Overview:
All you should need to create the person is:
#person = current_user.people.build(params[:person])
:organisation_id will always be found off of the :person param node, like so:
params[:person][:organisation_id]
So you're if will never be true.
I updated the gist with the necessary changes to the controller, the model and the form.
Overview:
You need to cleanup your controller. You are using accepts_nested_attribute, so in the :create, you only care about params[:person]. Additionally, in the render :new, you need to setup any instance variables that the partial will use. This does NOT go back through the :new action. The :new and :edit actions also need to be simplified.
Your Person model needs to use the :reject_if argument because the Address fields are coming back to the :create action as :address_attributes => {:line1 => '', :line2 => '', etc}. you only want to create the association if any have values. Then your validates_presence_of for :organisation will work just fine.
Your form needs to pass the organisation id to the controller, rather than the organisation names
It's all in the gist
Should be the final gist.
Overview:
Add the following to your edit action right after building the #person:
#person.build_address if #person.address.nil?
This ensure that you have the address inputs, even if the #person.address does not exist. It doesn't exist, because of the :reject_if condition on accepts_nested_attributes
I DRYed up the :reject_if as follows. It's a little hacky, but has some utility:
accepts_nested_attributes_for :address, :allow_destroy => true, :reject_if => :attributes_blank?
def attributes_blank?(attrs)
attrs.except('id').values.all?(&:blank?)
end
a. attrs -> the result of params[:person][:address]
b. .except('id') -> return all key-values except for 'id'
c. .values -> return all values from a hash as an array
d. .all? -> do all elements in the array satisfy the following check
e. &:blank -> ruby shorthand for a block, like this: all?{ |v| v.blank? }
Are you sure you didn't mean:
validates :address, :presence => true, :if => organisation_id.nil?
A more simple approach might be to add a custom validator. It's super easy, and you don't have to stumble on syntax or try to figure out why Rails' magic isn't working.
Inside my Person model I need to make sure that only if a person's organisation_id is blank, that person's address fields have to be present.
class Person < ActiveRecord::Base
...
validate :address_if_organisation_id_is_present
private
def address_if_organisation_id_is_present
return true unless organisation_id
errors.add(:address, "cannot be blank") unless address
end
end
Adding to a model's errors will prevent it from saving. Note: you may wish to use address.blank? or address.empty? as discussed in other answers, but you can define this for the behavior you'd like.

Rails nested form with has_many :through, not saving the data to joining table

I am kinda new to Rails and this is my first post to StackOverflow.
Say I have 3 models:
class Product < ActiveRecord::Base
default_scope :order => :title
has_many :line_items
has_many :promo_products
has_many :promotions, :through => :promo_products, :foreign_key => :promotion_id
before_destroy :ensure_not_referenced_by_any_line_item
before_destroy :ensure_not_referenced_by_any_promo_product
validates :title, :presence => true, :uniqueness => true
validates :description, :presence => true
validates :price, :numericality => {:greater_than_or_equal_to => 0.01}
private
def ensure_not_referenced_by_any_line_item
if line_items.empty?
return true
else
errors.add(:base, 'Line Items present')
return false
end
end
def ensure_not_referenced_by_any_promo_product
if promo_products.empty?
return true
else
errors.add(:base, 'Some promotions are still in effect')
return false
end
end
end
class Promotion < ActiveRecord::Base
CART_OR_PRODUCT = ['Cart', 'Product']
PROMOTION_TYPE = ['Percentage based', 'Value based']
has_many :promo_products
accepts_nested_attributes_for :promo_products
has_many :products, :through => :promo_products, :foreign_key => :product_id
accepts_nested_attributes_for :products
#attr_accessible :promo_products_attributes, :title, :description, :cart_or_product, :promotion_type, :discount, :minimum_price, :minimum_quantity
validates :title, :description, :presence => true
validates :cart_or_product, :inclusion => {:in => CART_OR_PRODUCT, :message =>
"is invlaid. Please select a valid option"}
validates :promotion_type, :inclusion => {:in => PROMOTION_TYPE, :message =>
"is invalid. Please select a valid option"}
validates :discount, :minimum_price, :numericality => {:greater_than_or_equal_to => 0.00}
validates :minimum_quantity, :numericality => {:greater_than_or_equal_to => 0}
end
class PromoProduct < ActiveRecord::Base
belongs_to :promotion
belongs_to :product
accepts_nested_attributes_for :products
end
In the promotions new page, I would like to show list of products that could be part of a promotion. A user may select 0, 1 or more products, depending on the type of promotion.
In the action new of promotions_controller, I built like this:
#promotion.promo_products.build.build_product
In the _form of promotions, I needed to show the list of products for user to select. I made a nested form like:
<%= form_for(#promotion) do |f| %>
<!-- other promotion fields -->
<%= f.fields_for :promo_products do |pp| %>
<%= pp.fields_for :products do |p| %>
<div class="field">
<%= f.label "Products" %><br />
<%= collection_select :promo_product, :product_id, Product.all, :id, :title {:selected => #promotion.product_ids}, {:multiple => true} %>
</div>
<% end %>
<% end %>
<% end %>
I have 2 issues.
First my code throws an error:
ArgumentError in PromotionsController#new
No association found for name `products'. Has it been defined yet?
If I change the line in PromoProduct model:
accepts_nested_attributes_for :products
to
accepts_nested_attributes_for :product
Then there are no errors, and everything works fine.
The data doesn't get saved to promo_product table. I have the create action in promo_product controller as:
def create
#promotion = current_promotion
products = Product.select(:id => params[:product_id])
products.each do |p|
promo_product = #promotion.promo_products.build(p)
promo_product.save
end
##promo_product = PromoProduct.new(params[:promo_product])
redirect_to promotions_path
end
How can I go about it?
Thank you.
You shouldn't put the "accept_nested_attribute_for" in the association table PromoProducts. It should exist in the model that you want to use for creating association to another model. "accept_nested_attribute_for" IIRC simply inserts an "[association]_attributes=" method for your model. For instance, if you add this method to your Product class for Promotion, you will get "promotion_attributes=" method inserted in the Product class. Then a nested form can use this function to create new objects with a hash that represents the model and association.
Base on the above, the create action shouldn't be in PromoProduct controller, instead it should be in Promotion controller.
<%= form_for(#promotion) do |f| %>
<!-- other promotion fields -->
<%= f.fields_for :products do |pp| %>
<div class="field">
<%= f.label "Products" %><br />
<%= collection_select :promo_product, :product_id, Product.all, :id, :title {:selected => #promotion.product_ids}, {:multiple => true} %>
</div>
<% end %>
<% end %>
I don't know without trying if the above collection_select line is correct. But you can debug this by checking the parameter returned by the form to the controller in the server console log. Basically you should see a nested hash of
{:promotion => {:products => ...}}
Let me know if you need more help on this. In my solution I used a combination of select_tag and options_from_collection_for_select. (But I don't recall the behavior of all these offhand without looking at the API doc.)
Lastly, do you need the :through model? I think since you created the through model you need to handle saving that in your create action. But since you don't have other attributes on the PromoProducts table I wonder if you want to simply leave it as a HABTM association and let rails deal with the rest?

Using .build method to create through 1to1 association

I have a one to one relationship with a simple users model and a profiles model:
models/user
class User < ActiveRecord::Base
authenticates_with_sorcery!
attr_accessible :email, :password, :password_confirmation
has_one :profile, :dependent => :destroy
validates_presence_of :password, :on => :create
validates :password, :confirmation => true,
:length => { :within => 6..100 }
email_regex = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, :presence => true,
:format => { :with => email_regex },
:uniqueness => {:case_sensitive => false},
:length => { :within => 3..50 }
end
models/profile
# == Schema Information
#
# Table name: profiles
#
# id :integer not null, primary key
# weight :decimal(, )
# created_at :datetime
# updated_at :datetime
#
class Profile < ActiveRecord::Base
attr_accessible :weight
belongs_to :user
end
I am doing it this way because I would like users to be able to track weight over time as well as store other more static data like height in profiles.
However, my new and create methods don't seem to be working correctly. I on submit of the new action I get this error:
undefined method `build' for nil:NilClass
profile_controller
class ProfilesController < ApplicationController
def new
#profile = Profile.new if current_user
end
def create
#profile = current_user.profile.build(params[:profile])
if #profile.save
flash[:success] = "Profile Saved"
redirect_to root_path
else
render 'pages/home'
end
end
def destory
end
end
and profile view for new
<%= form_for #profile do |f| %>
<div class="field">
<%= f.text_field :weight %>
</div>
<div class="actions">
<%= f.submit "Submit" %>
</div>
<% end %>
Thanks in advance for any help you might be able to give. Noob here!
The build syntax for has_one association is different from has_many association.
Change your code as follows:
#profile = current_user.build_profile(params[:profile])
Reference: SO Answer

Issues calling a method with multiple arguments

I am new to Ruby and Rails so sorry if this looks too noob.
I have created a resource called stream and another resource called tasks and have mapped them properly using has_many and belong_to. Everything works until I decided to add a "Quick Task Add form" on my Stream.show view:
Here is the view code for the form:
<%= form_for(#task) do |f| %>
<%= render 'shared/error_messages', :object => f.object %>
<div class="field">
<%= f.text_field :title %> <%= f.submit "Add Task" %>
<%= hidden_field_tag(:stream_id, #stream.id) %>
</div>
<% end %>
Here is my Stream.show action:
def show
#stream = Stream.find(params[:id])
#user = User.find(#stream.user_id)
#tasks = #stream.tasks.paginate(:page => params[:page])
#title = #stream.title
#task = Task.new
end
And here is my task controller:
class TasksController < ApplicationController
def create
#stream = Stream.find(params[:stream_id])
#stream.tasks.create!({:title => params[:task][:title], :user_id => 1, :owner => 1})
if #stream.save
flash[:success] = "Task created succesfully!"
else
flash[:error] = "Error creating task"
end
redirect_to #stream
end
end
Looks pretty basic to me. The problem is when it executes tasks.create, I get the following error message: "Validation failed: User can't be blank, Owner can't be blank"
What am I doing wrong?
edit: adding model code from comment
class Stream < ActiveRecord::Base
attr_accessible :title
belongs_to :user
has_many :tasks, :dependent => :destroy
validates :title, :presence=> true, :length => { :maximum =>50 }
validates :user_id, :presence => true
end
class Task < ActiveRecord::Base
attr_accessible :title
belongs_to :stream
validates :title, :presence=> true, :length => { :maximum =>70 }
validates :user_id, :presence => true
validates :owner, :presence => true
validates :stream_id, :presence => true
default_scope :order => "updated_at"
end
You should set your user_id and owner fro STREAM object
class TasksController < ApplicationController
def create
#stream = Stream.find(params[:stream_id])
#stream.tasks.create!({:title => params[:task][:title], :user_id => 1, :owner => 1})
#stream.user_id = 1
#stream.owner = 1
if #stream.save
flash[:success] = "Task created succesfully!"
else
flash[:error] = "Error creating task"
end
redirect_to #stream
end
end
Unfortunately i can't test my suggestion currently but you might have to add
Attr_accessible :user,:owner
To the task model because you are mass-assigning these field using the hash.

Resources