Rails forms for has_many through association with additional attributes? - ruby-on-rails

How can I generate form fields for a has_many :through association that has additional attributes?
The has_many :through relationship has an additional column called weight.
Here's the migration file for the join table:
create_table :users_widgets do |t|
t.integer :user_id
t.integer :widget_id
t.integer :weight
t.timestamps
end
The models look like this:
User
has_many :widgets, :through => :users_widgets,
:class_name => 'Widget',
:source => :widget
has_many :users_widgets
accepts_nested_attributes_for :widgets # not sure if this is necessary
Widget
has_many :users, :through => :users_widgets,
:class_name => 'User',
:source => :user
has_many :users_widgets
accepts_nested_attributes_for :users # not sure if this is necessary
UsersWidget
belongs_to :user
belongs_to :widget
For the sake of simplicity, Widget and User only have one field of their own called name, ergo User.first.name and Widget.first.name
Questions:
How would I append a dropdown selection for Widgets with the corresponding weight to the User create/edit form?
How can I dynamically add additional Widget forms to Users or User forms to Widgets to easily add or remove these relationships? The nested_form_for gem seems to be the perfect solution but I haven't been able to get it working.
Apart from the models and the form partials, are there any changes that need to be made to my controller?
Quick note.. I'm not interested in creating new Widgets in the User form or new Users in the Widget form, I only want the ability to select from existing objects.
I'm running Rails 3.1 and simple_form 2.0.0dev for generating my forms.

I will be solving your problem using cocoon, a gem I created to handle dynamically nested forms. I also have an example project to show examples of different types of relationships.
Yours is not literally included, but is not that hard to derive from it. In your model you should write:
class User
has_many :users_widgets
has_many :widgets, :through -> :user_widgets
accepts_nested_attributes_for :user_widgets, :reject_if => :all_blank, :allow_destroy => true
#...
end
Then you need to create a partial view which will list your linked UserWidgets. Place this partial in a file called users/_user_widget_fields.html.haml:
.nested-fields
= f.association :widget, :collection => Widget.all, :prompt => 'Choose an existing widget'
= f.input :weight, :hint => 'The weight will determine the order of the widgets'
= link_to_remove_association "remove tag", f
In your users/edit.html.haml you can then write:
= simple_form_for #user do |f|
= f.input :name
= f.simple_fields_for :user_widgets do |user_widget|
= render 'user_widget_fields', :f => user_widget
.links
= link_to_add_association 'add widget', f, :user_widgets
Hope this helps.

base your question. I made a simple App.
source is here: https://github.com/yakjuly/nest_form_example
I deployed it to heroku, you can open page: http://glowing-lightning-1954.heroku.com/users/new
answers
you want user form can select widget with weight, need do more work.dropdown selection can not satisfy your requirement.
I mix "nested_form" in a "bootstrap-rails" plugin, you can add the nested_fields easier
in my example, you need add a action calls select, and make WidgetsController#create can responsed_with :js
the code is based simple_form, you can have a try.

Thanks a lot for the cocoon pointer nathanvda. I have been scratching my head about some problems I had when trying to implement this under rails 4.0.0-rc1 and I thought I would share my findings just in case someone has the same problems when attempting this udner rails4.
Using the above code as an example, I did add user_id and widget_id to the permitted parameters as they are saved in the connecting table user_widgets. In rails 3 you did have to add them to attr_accesible in the user model but in rails 4 you have to add them to the allowed parameters in the controller of the main model you use for nesting, so here that would be the users_controller:
params.require(:user).permit(...user_fields...,
user_widgets_attributes: [:user_id, :widget_id])
Doing only this you end up with several of problems:
Every association (widget) gets multiplied when updating a user record. 1 becomes 2, 4, 8, and so on, when updating and saving the record.
removing an association does not work, the field is removed from the form but the association remains in the DB.
To fix these problems you also need to add :id and :_destroy to the list of permitted attributes:
params.require(:user).permit(...user_fields...,
user_widgets_attributes: [:user_id, :widget_id, :id, :_destroy])
after that it works flawlessly.
Juergen
PS: For now you have to use the git repository in your Gemfile to use cocoon under rails 4 until a rails 4 compatible gem is released. Thanks for the email nathanvda on my bug report!!

Related

Adding belongs to relationship to Ruby Gem Mailboxer

I am building an e-com application and would like to implement something like a messaging system. In the application, all conversation will be related to either a Product model or an Order model. In that case, I would like to store the relating object (type + id, I supposed) to the Conversation object.
To add the fields, of course I can generate and run a migration, however, since the Model and Controller are included within the gem, how can I declare the relationship? (belongs_to :linking_object, :polymorphic) and the controller? Any idea?
Thank you.
I ended up customizing the Mailboxer gem to allow for a conversationable object to be attached to a conversation.
In models/mailboxer/conversation.rb
belongs_to :conversationable, polymorphic: true
Add the migration to make polymorphic associations work:
add_column :mailboxer_conversations, :conversationable_id, :integer
add_column :mailboxer_conversations, :conversationable_type, :string
In lib/mailboxer/models/messageable.rb you add the conversationable_object to the parameters for send_message:
def send_message(recipients, msg_body, subject, sanitize_text=true, attachment=nil, message_timestamp = Time.now, conversationable_object=nil)
convo = Mailboxer::ConversationBuilder.new({
:subject => subject,
:conversationable => conversationable_object,
:created_at => message_timestamp,
:updated_at => message_timestamp
}).build
message = Mailboxer::MessageBuilder.new({
:sender => self,
:conversation => convo,
:recipients => recipients,
:body => msg_body,
:subject => subject,
:attachment => attachment,
:created_at => message_timestamp,
:updated_at => message_timestamp
}).build
message.deliver false, sanitize_text
end
Then you can have conversations around objects:
class Pizza < ActiveRecord::Base
has_many :conversations, as: :conversationable, class_name: "::Mailboxer::Conversation"
...
end
class Photo < ActiveRecord::Base
has_many :conversations, as: :conversationable, class_name: "::Mailboxer::Conversation"
...
end
Assuming you have some users set up to message each other
bob = User.find(1)
joe = User.find(2)
pizza = Pizza.create(:name => "Bacon and Garlic")
bob.send_message(joe, "My Favorite", "Let's eat this", true, nil, Time.now, pizza)
Now inside your Message View you can refer to the object:
Pizza Name: <%= #message.conversation.conversationable.name %>
Although rewriting a custom Conversation system will be the best long-term solution providing the customization requirement (Like linking with other models for instance), to save some time at the moment I have implement the link with a ConversationLink Model. I hope it would be useful for anyone in the future who are at my position.
Model: conversation_link.rb
class ConversationLink < ActiveRecord::Base
belongs_to :conversation
belongs_to :linkingObject, polymorphic: true
end
then in each models I target to link with the conversation, I just add:
has_many :conversation_link, as: :linkingObject
This will only allow you to get the related conversation from the linking object, but the coding for reverse linking can be done via functions defined in a Module.
This is not a perfect solution, but at least I do not need to monkey patch the gem...
The gem automatically take care of this for you, as they have built a solution that any model in your own domain logic can act as a messagble object.
Simply declaring
acts_as_messagable
In your Order or Product model will accomplish what you are looking for.
You could just use something like:
form_helper :products
and add those fields to the message form
but mailboxer comes with attachment functionality(carrierwave) included
this might help if you need something like attachments in your messages:
https://stackoverflow.com/a/12199364/1230075

Creating a form that breaks up a has_many association?

As referenced in my earlier post rails has_many manager, I am trying to create a polymorphic imaging system which will allow any item to inherit the ability to have a cover photo and additional photos.
To accomplish that kind of imaging system, I sided with a polymorphic model with belongs_to :imageable and extended its active record capabilities out to a module named Imageable.
My main question is, given that for example we have a class called Object, how can I create a form that only targets the first Object's has_many association (the cover), and then separately administer the other has_many associations?
The form would look like..
--- Form for cover photo ----
Upload button for object[image_attributes][0][public_id]
--- Form for additional photos ---
Upload button for object[image_attributes[1][public_id]
image.rb
class Image < ActiveRecord::Base
attr_accessible :public_id
# Setup the interface that models will use
belongs_to :imageable, :polymorphic => true
end
Imageable.rb
module Imageable
extend ActiveSupport::Concern
included do
has_many :images, :as => :imageable, :dependent => :destroy # remove this from your model file
accepts_nested_attributes_for :images
validates :images, :presence => { :message => "At least one image is required" }
end
def cover
cover = images.where(:cover => true).first
if not cover
return Image.new
end
return cover
end
def additional_images
images.where(:cover => false).all
end
end
Form
<%= form.semantic_fields_for :images do |image_fields| %>
<%= image_fields.cl_image_upload(:public_id, :crop => :limit, :width => 1000, :height => 1000,
:html => {:class => "cloudinary-fileupload"}) %>
...
The above produces appropriate routes like object[image_attributes][0][public_id]
Thanks!
I would recommend that you model your relationships slightly differently by using an explicit has_one relationship from the 'Object' to the cover Imageable and a separate has_many relationship for the additional images.
If you want all the images in the same collection then have a look at this post:
How to apply a scope to an association when using fields_for?
It explains how you can specify a 'scope' or subset of the entries in the has_many collection when setting up the fields_for helper. It should work with the semantic_fields_for helper as well since it simply wraps the Rails fields_for helper.

Editing many-to-many relations in Activeadmin

I am looking for a way to edit/add keywords related to an article, inline in Activeadmin.
I have defined a simple many-to-many setup:
class Area < ActiveRecord::Base
has_many :area_keywords
has_many :keywords, :through => :area_keywords
accepts_nested_attributes_for :keywords, :reject_if => :all_blank, :allow_destroy => true
end
class AreaKeyword < ActiveRecord::Base
belongs_to :area
belongs_to :keyword
end
class Keyword < ActiveRecord::Base
has_many :area_keywords
has_many :areas, :through => :area_keywords
end
I would like to add and edit the keywords in en Area form, so I setup this in Aciveadmin:
ActiveAdmin.register Area do
form do |f|
f.inputs "Area details" do
f.input :title
f.input :description
end
f.has_many :keywords do |k|
if k.object.nil?
k.input :word, :label => 'Keyword'
else
k.input :word, :label => k.object.word
k.input :_destroy, :as => :boolean, :label => "delete"
end
end
end
end
This works as expected.
But if I add the same Keyword to two different areas, the Keyword will just be created twice.
When entering a new keyword (in the Area form), I would like it to automatically create a relation to an existing keyword, or create a new keyword, if it does not exist. What would be the best way to go about it?
This is a pretty late answer :) but I actually have encountered kind of a similar issue in one of my projects...I had to add keywords/tags to two different models, but they could share them. At first I did just like you, and it was creating a record for each time you "attach" a keyword/tag to a model.
A better way to handle it is to use a tagging system. And I achieved a pretty neat system by combining two really good gems: 'acts-as-taggable-on' (https://github.com/mbleigh/acts-as-taggable-on) and 'select2-rails' (https://github.com/argerim/select2-rails)
In my own project, I actually did something similar to you and created a model just to have a list of all the appropriate keywords I wanted. But 'act-as-taggable-on' doesn't necesarilly requires a list of accepted keywords...so you can create them on the fly, and it will automatically handle duplicates, counts etc.
'select2-rails' just allows you to have an nice interface to add and remove keywords in one field, rather than using checkboxes, select options, or a text input where you would have to manually separate your string with commas or any separators.
If anyone need more details on how I implemented all, I would be more than glad to provide more code .. but the documentation for both of them are pretty straightforward!
EDIT: Well, I have a feeling some code would actually be useful :)
Bundle install both gem in your Gemfile
gem 'acts-as-taggable-on'
gem 'select2-rails'
In your Area model, you could add the following and do something like
class Area < ActiveRecord::Base
# .. your code
attr_accessible :area_keyword_list
acts_as_taggable_on :area_keywords
end
And in your ActiveAdmin file
ActiveAdmin.register Area do
form do |f|
f.inputs do
# .. whatever fields you have
f.input :area_keyword_list,
:as => :select,
:multiple => :true,
:collection => # here either a list of accepted keyword..or just left open,
:input_html => { :class => "multiple-select" }
end
end
end
and the JS for select2 is quite simple ...
$(".multiple-select").select2();
VoilĂ  !

Updating multiple objects with select_tag in Rails

I've got a User model, a Team model, and a CoachingRole model. The CoachingRoles model serves to associate Users and Teams because a one to many association already exists between Users and Teams. The respective associations are set up like this:
User:
has_many :coaching_roles
has_many :teams_coaching, :through => :coaching_roles, :source => :team
Team:
has_many :coaching_roles
has_many :coaches, :through => :coaching_roles, :source => :user
CoachingRole
belongs_to :team
belongs_to :user
I have a form to edit a User and part of it is to add Teams to its CoachingRoles. Here's the select_tag that I have so far:
= select_tag "user[coaching_role_ids]", options_for_select(#teams.map {|t| [t.name, t.id]})
That particular line will add a Team id to the User's coaching_role_ids, but will clearly not add the inverse. Eventually I'd like to add :multiple => true to that and allow the selection of multiple Teams to be added to a User's CoachingRoles. What is the best way to go about doing this?
I used this in my implementation.
Assuming that Team has attribute for name and a method selected_team_ids which returns an array of team ids selected for that user.
select_tag ("user[coaching_role_ids][]"), options_from_collection_for_select(Team.all, :id, :name, #user.coaching_role_ids), :multiple => true
Hopefully it helps!
EDIT: I edited the solution above based on #nilmethod's own discovery.
My original solution was:
select_tag ("user[teams][]"), options_from_collection_for_select(Team.all, :id, :name, #user.team_ids), :multiple => true
I'm guessing my tables were setup a little bit differently then. Thanks!

How to write properly a nested form with a N-N relation

While working on Rails 3 app, I came to the problem of nested forms.
One collection is a set of predefined objects (created with db:seed).
The other collection should show a form to allow to choose a few elements.
An example is better than a long description, so here it is.
Suppose you have 2 models: User and Group.
Suppose there are a few groups: Member, Admins, Guests, ...
You want your users to have multiple groups, and so you need a intermediate table: memberships.
The model code is obvious:
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, :through => :memberships
accepts_nested_attributes_for :memberships, :allow_destroy => true
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
end
The controller should not need to be changed.
The view, however is more complicated.
I want to show a list of checkboxes to choose a few groups of the predefined ones.
I am using here the special _destroy field, with reversed value, to destroy when it is actually unchecked (and so add the user to the group when it is checked)
%p
= f.label :name
%br
= f.text_field :name
%ul
= f.fields_for :memberships, #groups do |g|
%li
- group = g.object
= g.hidden_field :group_id, :value => group.id
= g.check_box :_destroy, {:checked => #user.groups.include?(group)}, 0, 1
= g.label :_destroy, group.name
However, this do not work as expected, because the form g will always create an input with an arbitrary id after each group (and even break the layout by including it after the </li>):
<input id="user_memberships_attributes_0_id" name="user[memberships_attributes][0][id]" type="hidden" value="1" />
<input id="user_memberships_attributes_1_id" name="user[memberships_attributes][1][id]" type="hidden" value="2" />
# ...
Knowing the syntax of nested attributes is the following:
{:group_id => group.id, :_destroy => 0} # Create
{:group_id => group.id, :_destroy => 0, :id => membership.id} # Update
{:group_id => group.id, :_destroy => 1, :id => membership.id} # Destroy
{:group_id => group.id, :_destroy => 1} # Do nothing
Sending every time the id will not work, because it will try to update a record which does not exist instead of creating it, and try to destroy when the record does not exist.
The current solution I found is to remove all the ids, which are wrong anyway (they should be the ids of the memberships, instead of simple indexes), and add the real id when the user already has the group.
(this is called in the controller before create and update)
def clean_memberships_attributes
if membership_params = params[:user][:memberships_attributes]
memberships = Membership.find_all_by_user_id params[:id]
membership_params.each_value { |membership_param|
membership_param.delete :id
if m = memberships.find { |m| m[:group_id].to_s == membership_param[:group_id] }
membership_param[:id] = m.id
end
}
end
end
This seems so wrong, and it adds a lot of logic in the controller, just to control the bad behavior of the view fields_for helper.
Another solution is to create all the form html yourself, trying to mimic the Rails conventions, and avoid the id problem, but that is really noisy in the code and I believe there is a better way.
Is it a way to make fields_for work better?
Is there any helper more appropriate ?
Am I reasoning wrong somewhere in this question?
How would you do to achieve this?
Thanks
I hope I understand you correctly?
The groups are predefined and you want to be able to add a user to a group.
On the edit User screen you show all or some of the predefined groups.
You want to add a user by ticking the checkbox and saving the record.
If you untick the checkbox the membership of this user in the unticked group should be nil.
Here's how I am doing this with companies and projects:
Class Company
has_many :datasets
has_many :projects, :through => :datasets
Class Project
has_many :datasets
has_many :companies, :through => :datasets
<% for company in Company.all %>
<tr>
<td>
<%= check_box_tag 'project[company_ids][]', company.id, #project.companies.include?(company) %>
</td>
</tr>
<% end %>
I list all Companies and check the ones I want to include in the project.
Please tell me if this already helps you? I believe you are using haml in your example. I am not really used to that notation.
If you want to use a subset you could use a scope:
scope :recent, Company.where(:created_at => (Time.now.midnight - 1.day)..Time.now.midnight)
Then you can use this scope like the .all method:
Company.recent
Does this help?

Resources