Save has_and_belongs_to_many child - ruby-on-rails

I have a User model and a Role model. They are joined by a has_and_belongs_to_many relationship. When an admin creates a user I want them to be able to assign a role to the user and have it saved when I call #user.save
The thing is though is that I get a warning that I can't mass-assign the roles relationship.
Any suggestions on how to go about this, I am on Rails 2.3.2
Thanks.
Edit: Code as requested.
user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :roles,
:join_table => "users_roles",
:foreign_key => "role_id",
:associated_foreign_key => "user_id"
end
role.rb
class Role < ActiveRecord::Base
has_and_belongs_to_many :users,
:join_table => "users_roles",
:foreign_key => "user_id",
:association_foreign_key => "role_id"
end
View: new.html.haml
- form_for(#user, users_path(:subdomain => current_account.subdomain)) do |f|
.input_set
= f.label(:login, "Username")
= f.text_field(:login)
.input_set
= f.label(:name)
= f.text_field(:name)
.input_set
= f.label(:email)
= f.text_field(:email)
- fields_for("user[roles][]", Role)do |user_role|
.input_set
%label Role
= user_role.select(:name, Role.all.map{|r| [r.name, r.id] })
.input_set
= f.label(:password)
= f.password_field(:password)
.input_set
= f.label(:password_confirmation, "Password Again")
= f.password_field(:password_confirmation)
.input_set
%label
= f.submit "Add User"
And I want the Role to be saved to the user by calling #user.save in my create option. Is that possible? Or is this a relationship I can't use that way, would it need to a has_many relationship for me to be able to do this.

I believe you need to call attr_accessible on the attributes in the model that you want to save in order to avoid the mass-assign error.

You cannot use accepts_nested_attributes_for for a habtm relationship.
You can however set the role_ids, see Railscast Episode 17 for details
In your case the problem is that you set only a single role but have a habtm relationship, why not a belongs_to?

Are you using the new accepts_nested_attributes_for method?
It will probably look something like this:
class User < ActiveRecord::Base
accepts_nested_attributes_for :roles, :allow_destroy => true
end
Check out this sample app for more detail.

Given the time since the question was asked, you've probably worked this out on your own...
The thing is though is that I get a
warning that I can't mass-assign the
roles relationship.
This is caused by one of two things in your User model.
You called attr_accessible and the list of symbols provide does not include :roles
You called attr_protected and the list of symbols includes :roles
Either add :roles to your attr_accessible call or remove it from the attr_protected call.

If I could edit/add to an answer I would. I had something similar required to what #EmFi mentioned. I had attr_accessible set, and had to add the equivalent of
:role_ids
to the attr_accessible of the user model. Note the pluralization. The following options did not work:
:role
:roles
:role_id
Just to be clear about the error message that I got:
WARNING: Can't mass-assign these protected attributes: role_ids
The warning didn't make a lot of sense to me since I'm using a habtm relationship. Nevertheless, it was correct.

Related

Validate presence of nested attributes within a form

I have the following associations:
#models/contact.rb
class Contact < ActiveRecord::Base
has_many :contacts_teams
has_many :teams, through: :contacts
accepts_nested_attributes_for :contacts_teams, allow_destroy: true
end
#models/contacts_team.rb
class ContactsTeam < ActiveRecord::Base
belongs_to :contact
belongs_to :team
end
#models/team.rb
class Team < ActiveRecord::Base
has_many :contacts_team
has_many :contacts, through: :contacts_teams
end
A contact should always have at least one associated team (which is specified in the rich join table of contacts_teams).
If the user tried to create a contact without an associated team: a validation should be thrown. If the user tries to remove all of a contact's associated teams: a validation should be thrown.
How do I do that?
I did look at the nested attributes docs. I also looked at this article and this article which are both a bit dated.
For completion: I am using the nested_form_fields gem to dynamically add new associated teams to a contact. Here is the relevant part on the form (which works, but currently not validating that at least one team was associated to the contact):
<%= f.nested_fields_for :contacts_teams do |ff| %>
<%= ff.remove_nested_fields_link %>
<%= ff.label :team_id %>
<%= ff.collection_select(:team_id, Team.all, :id, :name) %>
<% end %>
<br>
<div><%= f.add_nested_fields_link :contacts_teams, "Add Team"%></div>
So when "Add Team" is not clicked then nothing gets passed through the params related to teams, so no contacts_team record gets created. But when "Add Team" is clicked and a team is selected and form submitted, something like this gets passed through the params:
"contacts_teams_attributes"=>{"0"=>{"team_id"=>"1"}}
This does the validations for both creating and updating a contact: making sure there is at least one associated contacts_team. There is a current edge case which leads to a poor user experience. I posted that question here. For the most part though this does the trick.
#custom validation within models/contact.rb
class Contact < ActiveRecord::Base
...
validate :at_least_one_contacts_team
private
def at_least_one_contacts_team
# when creating a new contact: making sure at least one team exists
return errors.add :base, "Must have at least one Team" unless contacts_teams.length > 0
# when updating an existing contact: Making sure that at least one team would exist
return errors.add :base, "Must have at least one Team" if contacts_teams.reject{|contacts_team| contacts_team._destroy == true}.empty?
end
end
In Rails 5 this can be done using:
validates :contacts_teams, :presence => true
If you have a Profile model nested in a User model, and you want to validate the nested model, you can write something like this: (you also need validates_presence_of because validates_associated doesn't validate the profile if the user doesn't have any associated profile)
class User < ApplicationRecord
has_one :profile
accepts_nested_attributes_for :profile
validates_presence_of :profile
validates_associated :profile
docs recommend using reject_if and passing it a proc:
accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Model Names:
1: approval
2: approval_sirs
Associations:
1: approval
has_many :approval_sirs, :foreign_key => 'approval_id', :dependent => :destroy
accepts_nested_attributes_for :approval_sirs, :allow_destroy => true
2: approval_sirs
belongs_to :approval , :foreign_key => 'approval_id'
In approvals.rb
## nested form validations
validate :mandatory_field_of_demand_report_sirs
private
def mandatory_field_of_demand_report_sirs
self.approval_sirs.each do |approval_sir|
unless approval_sir.marked_for_destruction?
errors.add(:base, "Demand Report Field are mandatory in SIRs' Detail") unless approval_sir.demand_report.present?
end
end
end

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

Retrieving model attribute from table+column name

Let's say you have the following models:
class User < ActiveRecord::Base
has_many :comments, :as => :author
end
class Comment < ActiveRecord::Base
belongs_to :user
end
Let's say User has an attribute name, is there any way in Ruby/Rails to access it using the table name and column, similar to what you enter in a select or where query?
Something like:
Comment.includes(:author).first.send("users.name")
# or
Comment.first.send("comments.id")
Edit: What I'm trying to achieve is accessing a model object's attribute using a string. For simple cases I can just use object.send attribute_name but this does not work when accessing "nested" attributes such as Comment.author.name.
Basically I want to retrieve model attributes using the sql-like syntax used by ActiveRecord in the where() and select() methods, so for example:
c = Comment.first
c.select("users.name") # should return the same as c.author.name
Edit 2: Even more precisely, I want to solve the following problem:
obj = ANY_MODEL_OBJECT_HERE
# Extract the given columns from the object
columns = ["comments.id", "users.name"]
I don't really understand what you are trying to achieve. I see that you are using polymorphic associations, do you need to access comment.user.name while having has_many :comments, :as => :author in your User model?
For you polymorphic association, you should have
class Comment < ActiveRecord::Base
belongs_to :author, :polymorphic => true
end
And if you want to access comment.user.name, you can also have
class Comment < ActiveRecord::Base
belongs_to :author, :polymorphic => true
belongs_to :user
end
class User < ActiveRecord::Base
has_many :comments, :as => :author
has_many :comments
end
Please be more specific about your goal.
I think you're looking for a way to access the user from a comment.
Let #comment be the first comment:
#comment = Comment.first
To access the author, you just have to type #comment.user and If you need the name of that user you would do #comment.user.name. It's just OOP.
If you need the id of that comment, you would do #comment.id
Because user and id are just methods, you can call them like that:
comments.send('user').send('id')
Or, you can build your query anyway you like:
Comment.includes(:users).where("#{User::columns[1]} = ?", #some_name)
But it seems like you're not doing thinks really Rails Way. I guess you have your reasons.

How to create a model attached to two users? Without interfering with attr_accessible

I am making a game, and have a Game model and a User model.
The Game model looks like the following:
class Game < ActiveRecord::Base
belongs_to :first_user, :class_name => 'User', :foreign_key =>'first_user_id'
belongs_to :second_user, :class_name => 'User', :foreign_key =>'second_user_id'
validates_presence_of :first_user, :second_user
attr_accessible :created_at, :finished_datetime, :first_user_id, :second_user_id, :status, :winner_user_id
...
Now, in my controller for the game, I call Game.new. I'm certain that it is being called with current_user and challenge_user, because I checked with logging.
Game.new(:first_user => current_user, :second_user => challenge_user)
Unfortunately, I get the error:
Can't mass-assign protected attributes: first_user, second_user
I don't understand this since I used attr_accessible, not attr_accessor, so they should be assignable. What should I do differently, Rails?
Everything you pass in on e.g .new or .update_attributes as attributes is "mass-assignment". You need to assign them "manually", like this:
#game = current_user.games.new(params[:my_game_mass_assignment_attributes])
#game.second_user = # your second user
Assigning one attribute at a time is not "mass-assignment" and will work for security reasons (see http://guides.rubyonrails.org/security.html#mass-assignment)

has_and_belongs_to_many validation such that users can't apply multiple times to a workshop

Currently I'm building a system where users can apply to workshops... the only problem is that a user can apply multiple times.
This is the code for applying
#apply to workshop
def apply
#workshop = Workshop.find(params[:id])
#workshop.users << #current_user
if #workshop.save
#workshop.activities.create!({:user_id => #current_user.id, :text => "applied to workshop"})
flash[:success] = "You successfully applied for the workshop"
redirect_to workshop_path(#workshop)
else
flash[:error] = "You can't apply multiple times for the same workshop"
redirect_to workshop_path(#workshop)
end
end
The Workshop model does the following validation:
has_and_belongs_to_many :users #relationship with users...
validate :unique_apply
protected
def unique_apply
if self.users.index(self.users.last) != self.users.length - 1
errors.add(:users, "User can't apply multiple times to a workshop")
end
end
And the save fails because the message "You can't apply multiple times for the same workshop" shows up.
But the user is still added to the workshop as an attendee?
I think the problem is that the user is already added to the array before the save applies, then the save fails but the user isn't removed from the array.
How can I fix this issue?
Thanks!
Marcel
UPDATE
Added this in the migration so there are no duplicates in the database only ruby on rails doesn't catch the sql error, so it crashes ugly.
add_index(:users_workshops, [:user_id, :workshop_id], :unique => true)
UPDATE SOLUTION
Fixed the problem by doing the following:
Create a join model instead of a has_and_belongs_to_many relation
This is the join model:
class UserWorkshop < ActiveRecord::Base
belongs_to :user
belongs_to :workshop
validates_uniqueness_of :user_id, :scope => :workshop_id
end
This is the relationship definition in the other models:
In User:
has_many :workshops, :through => :user_workshops
has_many :user_workshops
In Workshop:
has_many :users, :through => :user_workshops, :uniq => true
has_many :user_workshops
Because you can only do a uniqueness validation on the current model you can't validate uniqueness on a has_and_belongs_to_many relation. Now we have a join model where we join users and workshops through, so the relationship in user and workshop stays the same the only BIG difference is that you can do validation in the join model. This is exactly what we want, we want to verify that there is only one :user_id per :workshop_id and therefore we use validates_uniqueness_of :user_id, :scope => :workshop_id
Case solved!
P.S. Watch carefully that you mention the through relation (:user_workshops) as a separate has_many relation otherwise the model can't find the association!!
According to "The Rails 3 Way", the "has_and belongs_to_man" is practically obsolete.
You should use has_many :through with an intermediate table.

Resources