I have a bit of a problem with a "has one" association on my app.
What I want to achieve is to be able to attach an optional quote to the topic. The quote can only be used once (in other words, if it's used for topic 1, it can't be used for any other topics).
I have a Topic model and a Quote model.
Topic has one quote.
Quote belongs to topic.
I also want to be able to attach a quote to other models (ex. Profile Model).
I'm really confused on what to do on my "edit topic" view as well as in the controller. I thought it would work like a "one to many" association, which I had no problem configuring. Somehow the "has one" is more complicated (for me!)
What I'd like is to have in the "edit topic" view a radio list of the available quotes which I can freely update. (Same for the "new topic" view).
My current controller:
def edit
#topic = Topic.find(params[:id])
#quote = #topic.quote
#packages = #topic.packages
#books = #topic.books
#tasklists = #topic.tasklists
#links = #topic.links
#terms = #topic.terms
end
def update
#topic = Topic.find(params[:id])
if #topic.update_attributes(topic_params)
flash[:success] = t('helpers.success-update', model: "topic")
redirect_to backend_topics_url
else
render partial: 'edit'
end
end
def topic_params
params.require(:topic).permit(:topic_id, :theme_id, :cover, :topic_status, :topic_access, :slug, *Topic.globalize_attribute_names, :quote_attributes => [:id, :topic_id], :package_ids => [], :book_ids => [], :link_ids => [], :tasklist_ids => [], :term_ids => [])
end
My current Topic model:
has_one :quote
accepts_nested_attributes_for :quote
My current Quote model:
belongs_to :topic
And my "Edit Topic" view:
<h4>Quote</h4>
<% if #quote %>
<h5>Current quote</h5>
<%= #quote.quote %> <%= link_to('[change]', '#') %>
<% end %>
<%= f.input :quote, :collection => Quote.all, :label_method => :quote, :label_value => :id, :checked => #quote.id, as: :radio_buttons %>
I'm sure there is something obvious that I'm missing but I can't figure out what.
Any ideas?
Thanks!
- Vincent
First off, if you want to have quote belong to multiple models, you will need a polymorphic association. Otherwise, you would need to add multiple foreign ids to quote like this: topic_id, profile_id etc and that will get messy fast. You can view a screencast on polymorphism here: http://railscasts.com/episodes/154-polymorphic-association-revised
has_one and belongs_to is basically the exact same as has_many and belongs_to except you are only dealing with 1 record instead of a collection of records.
For your current setup - in your edit action you need to fetch all the quotes that are not associated to any Topics. You can do that like this:
#available_quotes = Quote.where(topic_id: nil)
and then:
<%= f.input :quote, :collection => #available_quotes, :label_method => :quote, :label_value => :id, :checked => #quote.id, as: :radio_buttons %>
instead of
Quote.all in your form which is returning all quotes.
If you move to a polymorphic model, watch the screencast, and you would replace "commentable_id" in the screencast with something like "quotable_id" and then in your edit action to find the unassigned quotes you would do this:
#quotes = Quote.where(quotable_id: nil)
Related
I'm trying to turn my choices column from my Questionnaire table into separate strings as multiple choice.
Questionnaire Controller:
class QuestionnairesController < ApplicationController
def index
#questions = Questionnaire.find(params[:category_id])
#params[:category_id]= <%=category.id%>
#category = Category.find(params[:category_id])
#videos = VideoClue.find(params[:category_id])
###This finds all the questions from the question table by their category_id. Whenever I select a category, it matches the question related to the category
render :show
###render :show Renders Html page
end
def choose_answer
# binding.pry
#questions = Questionnaire.find(params[:id])
#params[:id] = /:id = /1
render :choose_answer
end
end
Questionnaire Model:
class Questionnaire < ActiveRecord::Base
belongs_to :categories
end
Chose_answer.html.erb
<h1>Congrats You Hit The Choices Page!</h1>
<%= semantic_form_for #questions.choices do |c| %>
<%= c.inputs do %>
<%= c.input :choices, :as => :check_boxes , :collection =>
[#questions.choices].map(&:inspect).join(', ') %>
<%end%>
<%end%>
Questionnaire table seed:
Questionnaire.create({question: "In that year did MTV (Music Television)
premiere and what was the first music video the channel aired?", choices:
["1982 Michael Jackson 'Bille Jean'", "1984 Madonna 'Like a virgn'", "1981
The Buggles 'Video Killed The Radio Star'"], correct_answer:"1981 The
Buggles 'Video Killed The Radio Star' ", category_id:1})
#question.choices returns
["1982 Michael Jackson 'Bille Jean'", "1984
Madonna 'Like a virgn'", "1981 The Buggles 'Video Killed The Radio Star'"],
How do I separate the strings from the choices column array and use formtastic to turn the strings into a mutiple choice format?
<%= c.input :choices, :as => :check_boxes , :collection => #questions.choices %> should do what you want here, no need to modify it if it's already an array IIRC, unless I'm misunderstanding the situation.
I have two models Project and ProjectPipeline.
I want to create a Project form that also has fields from the ProjectPipeline model. I have created the form successfully but when I hit save the values aren't stored on the database.
project.rb
class Project < ActiveRecord::Base
has_one :project_pipeline
accepts_nested_attributes_for :project_pipeline
self.primary_key = :project_id
end
projectpipeline.rb
class ProjectPipeline < ActiveRecord::Base
belongs_to :project, autosave: :true
validates_uniqueness_of :project_id
end
I don't always want a project pipeline but under the right conditions based on a user viewing a project. I want the project pipeline fields to be built, but only saved if the user chooses to save/populate them.
So when the project is shown I build a temporary project pipeline using the project_id: from params[:id] (not sure if I really need to do this). Then when the project is saved I use the create_attributes. But if it has already been created or built I just want to let the has_one and belongs_to association kick in and then use update_attributes.
My issue is when I am trying to save, I am either hitting a 'Forbidden Attribute' error if I use params[:project_pipeline] or having nothing saved at all if I used project_params. I have checked and rechecked all my fields are in project_params and even tried using a project_pipeline_params but that didn't feel right.
It is driving me nuts and I need to sleep.
projects_controller.rb
def show
#project = Project.find(params[:id])
if #project.project_pipeline
else
#project.build_project_pipeline(project_id: params[:id])
end
autopopulate
end
def update
#project = Project.find(params[:id])
if #project.project_pipeline
else
#project.build_project_pipeline(project_id: params[:id], project_type: params[:project_pipeline][:project_type], project_stage: params[:project_pipeline][:project_stage])
end
if #project.update_attributes(project_params)
flash[:success] = "Project Updated"
redirect_to [#project]
else
render 'edit'
end
end
def project_params
params.require(:project).permit(:user_id, project_pipeline_attributes:[:project_id,:project_type,:project_stage,
:product_volume,:product_value,:project_status,:outcome, :_destroy])
end
show.html.haml
- provide(:title, "Show Project")
%h1= #project.project_title
= simple_form_for(#project) do |f|
= f.input :id, :as => :hidden, :value => #project, :readonly => true
= f.input :user_id, label: 'Assigned to Account Manager', :collection => #account_managers, :label_method => lambda { |r| "#{r.first_name} #{r.last_name}" }
= f.input :project_id, :readonly => true
= f.input :status, :readonly => true
= f.input :project_stage, :readonly => true
- if #project.project_codename = "project pipeline"
= simple_fields_for #project.project_pipeline do |i|
%h2 Project Pipeline
- if #project.user_id == current_user.id
= i.input :project_volume, label: 'Project Status', collection: #project_status
= i.input :project_value, label: 'Project Status', collection: #project_status
= i.input :project_status, label: 'Project Status', collection: #project_status
= i.input :outcome, label: 'Outcome', collection: #outcome
= f.submit 'Save'
If you've gotten this far I sincerely thank you.
Solution
You need to change few things here. Firstly:
= simple_fields_for #project.project_pipeline do |i|
When you pass the object, rails have no idea it is to be associated with the parent object and as a result will create a field named project[project_pipeline] instead of project[project_pipeline_attributes]. Instead you need to pass the association name and call this method on the form builder:
= f.simple_fields_for :project_pipeline do |i|
This will check find out that you have defined project_pipeline_attributes= method (using accept_nested_attributes_for` and will treat it as association. Then in your controller change your show action to:
def update
#project = Project.find(params[:id])
#project.assign_attributes(project_params)
if #project.save
flash[:success] = "Project Updated"
redirect_to #project
else
render 'edit'
end
end
And all should work. As a separate note, since you are allowing :_destroy attribute in nested params, I am assuming you want to be able to remove the record using nested attributes. If so, you need to add allow_destroy: true to your accepts_nested_attributes_for call.
Now a bit of styling:
You can improve your show action a bit. First of all, I've noticed you are building an empty pipeline in every single action if none has been declared yet. That mean that you probably should move this logic into your model:
class Project < AR::Base
after_initalize :add_pipeline
private
def add_pipeline
project_pipeline || build_project_pipeline
end
end
You also have the mysterious method prepopulate - most likely it should be model concern as well.
Another point: This syntax:
if something
else
# do sth
end
is somehow quite popular and makes the code unreadable as hell. Instead, use:
if !something
# do something
end
or (preferred)
unless something
# do something
end
I'm not sure from your description if this is the problem, but one the thing is that a update_attributes with a has_one, by default, will rebuild the children(!), so you would lose the attributes you initialised. You should provide de update_only: true option to accepts_nested_attributes_for.
You can find more on this here, in the rails docs. The line would be this:
accepts_nested_attributes_for :project_pipeline, update_only: true
Considering the after_initialize, that would result in every project always having a pipeline. While that could be desirable, it isn't necessarily, depending on your domain, so I'd be a bit careful with that.
Cheers,
Niels
I have a form with a <select> element for the Group.
[The application stores bookmark links and groups for them]
When doing a 'new', everything work correctly. The new form works, the <select> dropdown has the list of groups and has their ID's.
The problem now is that I want to call the 'new link' from a different place that will already know the group. In this case I don't want an option to select the group, I just want to use the group id passed to it.
The form itself has:
= f.select :group_id, #groups
My routes include:
resources :groups do
resources :links # Added so that I can do group/:id/link/new ...
collection do
post 'order_links'
end
end
match 'search' => 'links#index'
match 'advanced_search' => 'links#advanced_search'
resources :links do
collection do
get 'groups'
end
end
The controller that's showing the form for this "new link" has:
def new
#link = Link.new
#groups = Group.all.collect { |g| [g.group_name, g.id] }
#group_name =
if params[:group_id]
'for the '+Group.find(params[:group_id]).group_name + ' group.'
else
''
end
respond_to do |format|
format.html
end
end
I tried changing the view to have this:
-if params[:group_id]
= f.hidden_field :group_id, :value => params[:group_id]
-else
= f.select :group_id, #groups
but it didn't work, I still got the <select> element, defaulting to its first <option>.
I actually had this working as some point in the past but has broken since, so hopefully I'm fairly close.
The indentation in your HAML template is wrong. It should be:
- if params[:group_id]
= f.hidden_field :group_id, :value => params[:group_id]
- else
= f.select :group_id, #groups
(If that was just the way it came out when you entered the question, I will delete this answer.)
I have a model named UserPrice which has the attribute :purchase_date(a date_select) in its table. With my form I can create multiple user_prices at once but for user convenience I made a virtual attribute inside of my UserPrice model called :all_dates that's also a date_select field and its job is to be the replacement of the :purchase_dates so users only have to select the :all_dates field for the date.
Problem & Question
The :all_dates field is not updating the :purchase_date fields of my user_prices that are being created. What do I need to do in order to get my :all_dates field to update the :purchase_date fields of my new UserPrices?
Does anyone have any tips on how to do this?
Parameters
Parameters:
"user_price"=> {
"all_dates(2i)"=>"10",
"all_dates(3i)"=>"27",
"all_dates(1i)"=>"2011"
},
"user_prices"=>
{
"0"=>{"product_name"=>"Item1", "store"=>"Apple Store","price"=>"6"},
"1"=>{"product_name"=>"Item2", "store"=>"Apple Store", "price"=>"7"}
},
"commit"=>"Submit"}
Code
class CreateUserPrices < ActiveRecord::Migration
def self.up
create_table :user_prices do |t|
t.decimal :price
t.integer :product_id
t.date :purchase_date
t.timestamps
end
end
end
I took out the :purchase_date field so it isn't inside of the user_price loop.
<%= form_tag create_multiple_user_prices_path, :method => :post do %>
<%= date_select("user_price", "all_dates" ) %>
<% #user_prices.each_with_index do |user_price, index| %>
<%= fields_for "user_prices[#{index}]", user_price do |up| %>
<%= render "user_price_fields", :f => up %>
<% end %>
<% end %>
<% end %>
class UserPrice < ActiveRecord::Base
attr_accessible :price, :product_name, :purchase_date, :all_dates, :store
attr_accessor :all_dates
after_save :save_all_dates_to_user_prices
composed_of :all_dates, :class_name => "DateTime",
:mapping => %w(Time to_s),
:constructor => Proc.new { |item| item },
:converter => Proc.new { |item| item }
def user_prices
#user_prices = Array.new() { UserPrice.new }
end
protected
def save_all_dates_to_user_prices
if !self.all_dates.nil?
self.user_prices.each {|up| up.purchase_date = self.all_dates if up.new_record?}
end
end
class UserPricesController < ApplicationController
def new
#user_prices = Array.new(5) { UserPrice.new }
end
def create_multiple
#user_prices = params[:user_prices].values.collect { |up| UserPrice.new(up) }
if #user_prices.all?(&:valid?)
#user_prices.each(&:save!)
redirect_to :back, :notice => "Successfully added prices."
else
redirect_to :back, :notice => "Error, please try again."
end
end
This is a case of trying to do in a model what is better left to the controller. All you're trying to do here is to auto-assign a certain attribute on creation from a parameter not directly tied to your model. But you're not even passing that extra parameter to the model anywhere - you're creating your model instances from the user_prices parts of the parameter hash, but the user_price sub-hash is not used anywhere. In any case, this is behavior that is more closely related to the view and action taken than the model, so keep it in the controller.
Try this:
Throw out the virtual attribute, and get rid of the whole after_save callback stuff
Throw away the user_prices method in your model
Change the all_dates attribute name back to purchase_date in the form
Then your parameter hash should look like this:
{"user_price"=> {
"purchase_date(2i)"=>"10",
"purchase_date(3i)"=>"27",
"purchase_date(1i)"=>"2011"
},
"user_prices"=>
{
"0"=>{"product_name"=>"Item1", "store"=>"Apple Store","price"=>"6"},
"1"=>{"product_name"=>"Item2", "store"=>"Apple Store", "price"=>"7"}
}}
All that's left to do is to merge the single user_price attributeS into each user_prices sub-hash in your create_multiple action. Replace the first line in that action with this:
#user_prices = params[:user_prices].values.collect do |attributes|
UserPrice.new(attributes.merge(params[:user_price]))
end
I'm not sure why you are even using that virtual attribute is there more to this implementation? If you are just trying to save an associated model, you might simply want a accepts_nested_attributes_for :user_prices in your User model
This works great and many developers use this method, so it's nice to know for working on other projects as well as for the people who might end up maintaining yours.
http://railscasts.com/episodes/196-nested-model-form-part-1
http://railscasts.com/episodes/197-nested-model-form-part-2
Background:
I followed the tutorial here to setup a polymorphic User favorites data model in my application. This allows me to let a User make pretty much any Entity in the system which I add 'has_many :favorites, :as => :favorable' line to its model a favorite. I plan on using this to implement a Facebook style 'Like' system as well as several other similar systems.
To start off I added the favoritability to a Post model (each user can create status updates like on Facebook). I have it all done and unit tested so I know the data model is sound and functioning from either side of the relationship (User and Post).
Details:
I have a Home controller with a single index method and view.
on the index view I render out the posts for the user and the user's friends
I want the user to be able to like posts from their friends
The Posts controller has only a create and a destroy method with associated routes (not a full fledged resource) and through the Post method via AJAX posts are created and deleted without issue
Where I am stuck
How do I add the link or button to add the post to the user's Favorites?
According to the tutorial the way to create a new Favorite through the polymorphic association is to do it from the Post.favorites.build(:user_id => current_user.id). From this direction the build handles pulling out the Post's ID and TYPE and all I have to do is pass in the user's id
Do I use an AJAX form post to a Favorites controller with a Create and Destroy method similar to the Post controller?
I am still struggling to uncross the wires in my brain from ASP.Net N-Tier web application development over to Rails MVC. Hasn't been too bad until now ;)
I bet there are Gems out there that might do this but I need to learn and the best way is to suffer through it. Maybe a tutorial or sample code from someone who has implemented liking functionality within their application would be helpful.
Thanks in advance for the assistance!
Jaap, I appreciate your comment on my question. After writing the question I pretty much didn't want to wait because the real learning takes place through trial and error, so I errored it up ;)
It turns out that what you suggested was pretty much in line with exactly what I ended up doing myself (it's always nice to find out that what you decide to do is what others would do as well, I love the sanity check value of it all).
So here is what I did and it is all working through post-backs. Now I just need to implement AJAX and style it:
My favorite model because my Polymorphic Favorites model requires that an Entity can only be favorited once by a user I added to the validations 'Scopes' which indicate that for each attribute it has to be unique in the scope of the other 2 required attributes. This solves the issue of multiple favorites by the same user.
class Favorite < ActiveRecord::Base
before_save :associate_user
belongs_to :favorable
belongs_to :user
# Validations
validates :user_id, :presence => true,
:uniqueness => {:scope => [:favorable_id, :favorable_type], :message => "item is already in favorites list."}
validates :favorable_id, :presence => true,
:uniqueness => {:scope => [:user_id, :favorable_type], :message => "item is already in favorites list."}
validates :favorable_type, :presence => true,
:uniqueness => {:scope => [:favorable_id, :user_id], :message => "item is already in favorites list."}
# Callbacks
protected
def associate_user
unless self.user_id
return self.user_id = session[:user_id] if session[:user_id]
return false
end
end
end
My User Model (that which is relevant): I added 2 methods, the get_favorites which is the same as favorable one from the tutorial and a Favorite? method which checks to see if the Entity in question has already been added to the user's favorites.
class User < ActiveRecord::Base
# Relationships
has_many :microposts, :dependent => :destroy
has_many :favorites
# Methods
def favorite?(id, type)
if get_favorites({:id => id, :type => type}).length > 0
return true
end
return false
end
def get_favorites(opts={})
# Polymorphic Favoritability: allows any model in the
# application to be favorited by the user.
# favorable_type
type = opts[:type] ? opts[:type] : :topic
type = type.to_s.capitalize
# add favorable_id to condition if id is provided
con = ["user_id = ? AND favorable_type = ?", self.id, type]
# append favorable id to the query if an :id is passed as an option into the
# function, and then append that id as a string to the "con" Array
if opts[:id]
con[0] += " AND favorable_id = ?"
con << opts[:id].to_s
end
# Return all Favorite objects matching the above conditions
favs = Favorite.all(:conditions => con)
case opts[:delve]
when nil, false, :false
return favs
when true, :true
# get a list of all favorited object ids
fav_ids = favs.collect{|f| f.favorable_id.to_s}
if fav_ids.size > 0
# turn the Capitalized favorable_type into an actual class Constant
type_class = type.constantize
# build a query that only selects
query = []
fav_ids.size.times do
query << "id = ?"
end
type_conditions = [query.join(" AND ")] + fav_ids
return type_class.all(:conditions => type_conditions)
else
return []
end
end
end
end
My Micropost Model (that which is relevant): note the Polymorphic association in the has_many relationship titled :favorites.
class Micropost < ActiveRecord::Base
attr_accessible :content
# Scopes
default_scope :order => 'microposts.created_at DESC'
# Relationships
belongs_to :user
has_many :favorites, :as => :favorable # Polymorphic Association
# Validations
validates :content, :presence => true, :length => { :minimum => 1, :maximum => 140 }
validates :user_id, :presence => true
end
My Micropost Form: as you can see I am passing in the entity that will be mapped to the Favorite model as a local variable to the 2 Favorite forms as 'local_entity'. This way I can pull out the ID and the TYPE of the Entity for the Polymorphic association.
<div class="post">
<span class="value">
<%= micropost.content %>
</span>
<span>
<% if current_user.favorite?(micropost.id, micropost.class.to_s) %>
<%= render :partial => 'favorites/remove_favorite', :locals => {:local_entity => micropost} %>
<% else %>
<%= render :partial => 'favorites/make_favorite', :locals => {:local_entity => micropost} %>
<% end %>
</span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
</span>
<div class="clear"></div>
</div>
My Make Favorite Form:
<%= form_for current_user.favorites.build do |f| %>
<div><%= f.hidden_field :favorable_id, :value => local_entity.id %></div>
<div><%= f.hidden_field :favorable_type, :value => local_entity.class.to_s %></div>
<div class="actions"><%= f.submit "make favorite" %></div>
<% end %>
My Remove Favorite Form:
<%= form_for current_user.get_favorites(
{:id => local_entity.id,
:type => local_entity.class.to_s}),
:html => { :method => :delete } do |f| %>
<div class="actions"><%= f.submit "remove favorite" %></div>
<% end %>
If you don't want to call this on the current_user, you would have to have these routes in your config/routes.rb to make nested routes for favorites on a user. I assume you have a Favorite model which belongs_to :user:
resources :users do
resources :favorites
end
Then make sure your favorites controller loads the user in some kind of before_filter:
def load_user
#user = User.load params[:user_id]
end
And then you can render a remote form to create a new favorite for any kind of object (it will only show a button):
<%= remote_form_for [#user, Favorite.new] do |f| -%>
<%= f.hidden_field :favorable_type, object.class.to_s %>
<%= f.hidden_field :favorable_id, object.id %>
<%= f.submit 'Like' %>
<%- end -%>
You would have to render that form as a partial sending along an object (e.g. a Post) and then it will create an AJAX POST call to /users/:id/favorites/ which will create the favorite object and render some kind of javascript response in a create.rjs file.
I hope this helps. The code itself is untested, but it might get you moving.