I'm having trouble using update_attributes with referenced documents. I've reduced my problem to a simple example that AFAICT should work, but doesn't:
class Account
include Mongoid::Document
has_many :submissions, :autosave => true
end
class Submission
include Mongoid::Document
belongs_to :account
end
a = Account.new
a.save!
s = Submission.new
s.update_attributes({"account" => {"id" => a.id}})
s.save!
a.id == s.account.id # false
The call to update_attributes is creating a new blank Account object instead of referencing the existing one that I'm telling it to use. What's going on?
UPDATE
To be clear, I'm trying to process an HTML form in an update action which adds an Account to a Submission. I understand there are other ways to link these documents by writing specific code. But the normal rails way should allow me to use an HTML form to update the documents this way, right?
Change your HTML form to make "account_id" not "account[id]" then it starts working:
s.update_attributes({"account_id" => a.id})
s.save!
a.id == s.account.id # true
a == s.account # true
Very odd what it's doing. Maybe mongoid bug?
That's not the way to add s to a. What you want to do is this:
a = Account.new
a.submissions << Submission.new
a.save!
Related
I am not able to store the record in the database following is my code
Form Parameters
Controller
def create
#sales_dailystatus_info = SalesDailystatusInfo.new(sales_dailystatus_info_params)
#sales_dailystatus_info.user_id = current_user.id
#project = #sales_dailystatus_info.project
respond_to do |format|
if #sales_dailystatus_info.save
byebug
format.js{}
format.html{redirect_to dashboard_project_path(#project)}
else
format.js{}
format.html{render nothing: true}
end
end
end
def sales_dailystatus_info_params
params.require(:sales_dailystatus_info).permit(:user_id, :project_id, :sales_task_id,
:task_time, :description)
end
Model
class SalesDailystatusInfo < ActiveRecord::Base
belongs_to :project
belongs_to :user, optional: true
belongs_to :sales_task
validates :user_id, :sales_tasks_id, :task_time, presence: true
end
You can see in the screenshot I got rollback while save.
Please help me.
Edit:
I have made the change, Now I am iterating the params and remove strong parameters. Following is my code
def create
params[:sales_dailystatus_info].values.each do |sales_dailystatus_info|
#sales_dailystatus_info = SalesDailystatusInfo.create(
project_id: sales_dailystatus_info[:project_id],
sales_tasks_id: sales_dailystatus_info[:sales_task_id],
task_time: sales_dailystatus_info[:task_time],
description: sales_dailystatus_info[:description],
user_id: current_user.id
);
byebug
end
respond_to do |format|
format.js{}
format.html{render nothing: true}
end
end
still not able to save it. Give me error Sales task must exist.
Looking at your logs, it looks like your are submitting two SalesDailyStatusInfo:
{... 'sales_daily_status_info" => { "0" => {"project_id" => ...}, "1" => { "project_id" => ... } } }
You don't allow those keys in your params sanitizer, hence the Unpermitted parameters: :0, :1. The result is that your don't whitelist any params you submit and the params hash is empty, your model validations fail.
In order for this to work you either need to send only one project at a time or loop through your params to create both SalesDailyStatusInfo.
Add the frontend form code to your question if you need further help.
Hope it helps !
Looks like your record is not valid. You validate presence of the user_id but it's not sent. Instead of rendering nothing try to render #sales_dailystatus_info.errors.messages
Why do you validate :user_id and put optional: true at the same time? Also in your model you have validation of :sales_task_id (belongs_to association default validation on Ruby on Rails version >= 5.0) and :sales_tasks_id. And in your controller I do see :sales_tasks_id key but I do not see :sales_task_id, that is why you receive Sales task must exist error. Remove unnecessary validations of :user_id and :sales_task_id, return back strong parameters and everything will be fine
Since you are assigning a project_id and a sales_task_id, those records must be created before you can create a sales_dailystatus_info record. The error you are getting is saying you don’t have a sales_task created.
I solved it by changing the name of the field.
I change the field sales_tasks_id in sales_daily status_info to sales_task_id.
I need a singular foreign key name instead of plural.
I made a real basic github project here that demonstrates the issue. Basically, when I create a new comment, it is saved as expected; when I update an existing comment, it isn't saved. However, that isn't what the docs for :autosave => true say ... they say the opposite. Here's the code:
class Post < ActiveRecord::Base
has_many :comments,
:autosave => true,
:inverse_of => :post,
:dependent => :destroy
def comment=(val)
obj=comments.find_or_initialize_by(:posted_at=>Date.today)
obj.text=val
end
end
class Comment < ActiveRecord::Base
belongs_to :post, :inverse_of=>:comments
end
Now in the console, I test:
p=Post.create(:name=>'How to groom your unicorn')
p.comment="That's cool!"
p.save!
p.comments # returns value as expected. Now we try the update case ...
p.comment="But how to you polish the rainbow?"
p.save!
p.comments # oops ... it wasn't updated
Why not? What am I missing?
Note if you don't use "find_or_initialize", it works as ActiveRecord respects the association cache - otherwise it reloads the comments too often, throwing out the change. ie, this implementation works
def comment=(val)
obj=comments.detect {|obj| obj.posted_at==Date.today}
obj = comments.build(:posted_at=>Date.today) if(obj.nil?)
obj.text=val
end
But of course, I don't want to walk through the collection in memory if I could just do it with the database. Plus, it seems inconsistent that it works with new object but not an existing object.
Here is another option. You can explicitly add the record returned by find_or_initialize_by to the collection if it is not a new record.
def comment=(val)
obj=comments.find_or_initialize_by(:posted_at=>Date.today)
unless obj.new_record?
association(:comments).add_to_target(obj)
end
obj.text=val
end
I don't think you can make this work. When you use find_or_initialize_by it looks like the collection is not used - just the scoping. So you are getting back a different object.
If you change your method:
def comment=(val)
obj = comments.find_or_initialize_by(:posted_at => Date.today)
obj.text = val
puts "obj.object_id: #{obj.object_id} (#{obj.text})"
puts "comments[0].object_id: #{comments[0].object_id} (#{comments[0].text})"
obj.text
end
You'll see this:
p.comment="But how to you polish the rainbow?"
obj.object_id: 70287116773300 (But how to you polish the rainbow?)
comments[0].object_id: 70287100595240 (That's cool!)
So the comment from find_or_initialize_by is not in the collection, it outside of it. If you want this to work, I think you need to use detect and build as you have in the question:
def comment=(val)
obj = comments.detect {|c| c.posted_at == Date.today } || comments.build(:posted_at => Date.today)
obj.text = val
end
John Naegle is right. But you can still do what you want without using detect. Since you are updating only today's comment you can order the association by posted_date and simply access the first member of the comments collection to updated it. Rails will autosave for you from there:
class Post < ActiveRecord::Base
has_many :comments, ->{order "posted_at DESC"}, :autosave=>true, :inverse_of=>:post,:dependent=>:destroy
def comment=(val)
if comments.empty? || comments[0].posted_at != Date.today
comments.build(:posted_at=>Date.today, :text => val)
else
comments[0].text=val
end
end
end
I have an app which updates a post if it exists, otherwise it creates a new one. This post contains embedded documents:
class Post
embeds_one :tag, :as => :taggable, :class_name => 'TagSnippet'
end
class TagSnippet
include Mongoid::Document
field :name
embedded_in :taggable, polymorphic: true
end
The post is updated in a controller with the following code:
#post = Post.where(--some criteria which work--).first
if #post
#post.attributes = params
else
#post = Post.new(params)
end
#post.save!
This code runs and updates the non-embedded documents, but does not update the embedded docs. Oddly, when I debug in Rubymine, all the attributes of the #post change appropriately (including the embedded ones), but regardless the database does not get updated.
This indicates to me it's some mongo or mongoid problem, but rolling back mongo and mongoid gems produced no change.
I guess that your embedded document is defined like this:
field :subdoc, type: Hash
I bumped into this a couple of times already. Short explanation: Mongoid doesn't track changes inside subhashes.
doc.subdoc.field_a = 1 # won't be tracked
sd = doc.subdoc.dup
sd.field_a = 1
doc.subdoc = sd # should be tracked
So, if Mongoid doesn't detect assignments, it doesn't mark attribute dirty, and therefore doesn't include it in the update operation.
Check this theory by printing doc.subdoc_changed? before saving.
I would like to use rails new dynamic attr_accessible feature. However each of my user has many roles (i am using declarative authorization). So i have the following in my model:
class Student < ActiveRecord::Base
attr_accessible :first_name, :as=> :admin
end
and i pass this in my controller:
#student.update_attributes(params[:student], :as => user_roles)
user_roles is an array of symbols:
user_roles = [:admin, :employee]
I would like my model to check if one of the symbols in the array matches with the declared attr_accessible. Therefore I avoid any duplication.
For example, given that user_roles =[:admin, :employee]. This works:
#student.update_attributes(params[:student], :as => user_roles.first)
but it is useless if I can only verify one role or symbol because all my users have many roles.
Any help would be greatly appreciated
***************UPDATE************************
You can download an example app here:
https://github.com/jalagrange/roles_test_app
There are 2 examples in this app: Students in which y cannot update any attributes, despite the fact that 'user_roles = [:admin, :student]'; And People in which I can change only the first name because i am using "user_roles.first" in the controller update action. Hope this helps. Im sure somebody else must have this issue.
You can monkey-patch ActiveModel's mass assignment module as follows:
# in config/initializers/mass_assignment_security.rb
module ActiveModel::MassAssignmentSecurity::ClassMethods
def accessible_attributes(roles = :default)
whitelist = ActiveModel::MassAssignmentSecurity::WhiteList.new
Array.wrap(roles).inject(whitelist) do |allowed_attrs, role|
allowed_attrs + accessible_attributes_configs[role].to_a
end
end
end
That way, you can pass an array as the :as option to update_attributes
Note that this probably breaks if accessible_attrs_configs contains a BlackList (from using attr_protected)
Heres what I'm trying to accomplish:
I have a tagging system in place.
Tags are created, when Posts are created (posts has_many :tags, :through => :tag_joins.
A tag join is automatically created when a post is created with tags).
I want to check if the tag already exists. If it does I want to use the existing tag for the tag_join record, rather than creating a new tag record.
Here is my current code, which isn't working.
class Tag < ActiveRecord :: Base
belongs_to :user
belongs_to :tag_join
belongs_to :post
before_create :check_exists
def check_exists
tag = Tag.where(:name => self.name, :user_id => current_user.id)
if tag.nil?
tag = Tag.create(:name => self.name, :user_id => current_user.id)
end
end
end
This doesn't work though, I'm getting an error upon task creation...(the server is actually just timing out - I don't receive a specific error).
Any ideas?
Tokland said I was creating an infinite loop by telling it to create tag again - so I tried this:
def check_exists
tag = Tag.find_by_name_and_user_id(:name => self.name, :user_id => current_user.id)
if tag != nil
self.id = tag.id
end
end
And still get the server timeout
Edit: I'm not sure if this matters, but the way the tags are being added is similar to "http://railscasts.com/episodes/73-complex-forms-part-1
they're nested in the post form, and use something like this:
def tag_attributes=(tag_attributes)
tag_attributes.each do |attributes|
tags.build(attributes)
end
end
I'm wondering if this is stopping this whole thing from working? Also, using current_user.id in the model definitely seems to be an issue...
EDIT:
Something I have figured out:
this had to change, the format we were using before was incorrect syntax - generally used for a .where method.
def check_exists
#tag = Tag.find_by_name_and_user_id(self.name, self.user_id)
if #tag != nil
#return false
#self=#tag
end
end
The problem now is this, I can learn if it the tag already exists. But then what? If I go with the return false option, there is an error upon post creation, and the join record isn't created... The other option "self=#tag" obviously just doesn't work.
You're going to find it hard to to this from within the Tag model. It seems like what you want is to update the Post using nested attributes, like so:
post = Post.create
post.update_attributes(:tags_attributes=>{"0"=>{:name=>"fish",:user_id=>"37"}})
This is actually pretty simple to do by using a virtual attribute setter method:
class Post < AR::Base
has_many :tags
def tags_attributes=(hash)
hash.each do |sequence,tag_values|
tags << Tag.find_or_create_by_name_and_user_id(tag_values[:name],\
tag_values[:user_id])
end
end
> post = Post.create
> post.update_attributes(:tags_attributes=>{"0"=>{:name=>"fish",:user_id=>"37"}})
> Tag.count # => 1
# updating again does not add dups
> post.update_attributes(:tags_attributes=>{"0"=>{:name=>"fish",:user_id=>"37"}})
> Tag.count # => 1
There's a find_or_create_by_ function built right in to Rails
# No 'Summer' tag exists
Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")
# Now the 'Summer' tag does exist
Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
http://api.rubyonrails.org/classes/ActiveRecord/Base.html (under Dynamic attribute-based finders)
You want to use the magic method find_or_create_by
def check_exists
tag = Tag.find_or_create_by_name_and_user_id(:name => self.name, :user_id => current_user.id)
end
Check out the ActiveRecord::Base docs for more info
The question I originally asked got pretty distorted by the end. So I'm separating it.
People who are trying to do what I originally asked can try this:
before_create :check_tag_exists
private
def check_tag_exists
#tag = Tag.find_by_name_and_user_id(self.name, self.user_id)
if #tag != nil
#
end
end
This will enable you to check if your record has already been created. Any further logic you can drop in that if statment.
I believe the other answers are a bit dated. Here's how you should probably accomplish this for Rails 4
tag = Tag.first_or_initialize(:name => self.name, :user_id => current_user.id)
if !tag.new_record?
tag.id = self.id
tag.save
end
try this
def check_exists
tag = Tag.where(:name => self.name, :user_id => current_user.id).first
tag = Tag.new({:name => self.name, :user_id => current_user.id}) unless tag
end
use Tag.new instead of Tag.create
where returns an empty ActiveRecord on finding no match.