ActiveRecord rollback in Rails - ruby-on-rails

Rails 5.2
I have a partial:
views/authors/_add_author_comment.html/slim
= form_for :author_note, url: author_notes_url, method: :post do |f|
= f.hidden_field :author, value: #book.author
= f.text_area :comment
button.btn.btn-primary type="button"
= f.submit t('authors.show.submit_comment')
In my controllers/author_notes_controller.rb, I have:
def create
#author_note = AuthorNote.new(author: params[:author_note][:author], user_id: current_user.id, comment: params[:author_note][:comment])
#author_note.save
end
When the form displays (part of a larger view), and I fill the comment out, and click on "Submit Comment", the comment is not saved. In the console, I see the following:
Processing by AuthorNotesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"Jju1cpsLjXLY/TaF9p/Zkh8JQ/+KajjxwQHgNU4tNU9bjL8BiZQ8xL3S7ske1KqflOPHVaB9UTWRvgxNqzLd7Q==", "author_note"=>{"author"=>"John Dow", "comment"=>"This is a test"}, "commit"=>"Save Note"}
User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = '5' ORDER BY `users`.`id` ASC LIMIT 1
↳ app/controllers/author_notes_controller.rb:23
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = '5' LIMIT 1
↳ app/controllers/author_notes_controller.rb:23
(0.2ms) BEGIN
↳ app/controllers/author_notes_controller.rb:28
(0.1ms) ROLLBACK
↳ app/controllers/author_notes_controller.rb:28
No template found for AuthorNotesController#create, rendering head :no_content
Completed 204 No Content in 60ms (ActiveRecord: 10.6ms)
Why is it ActiveRecord rolling back, and not saving the note to the author_notes table?
Resolution:
the author_note.rb model, I had: belongs_to :book, I commented it out

You didn't include your AuthorNote model, there might be some validation constraints that prevents the author_note from being saved.
Your code also doesn't handle validation error, so you might want to do that. But you can simply check for errors like:
def create
#author_note = AuthorNote.new(author: params[:author_note][:author], user_id: current_user.id, comment: params[:author_note][:comment])
#author_note.save
# puts works too
logger.debug "author_note save error: #{#author_note.errors.full_messages.join(' ')}"
end

This is highly likely due to rails 5 making belongs_to association required by default.
What that means is from rails > 5, if you define belongs_to on a model and if the corresponding record is not present. In this case, book_id should be nil in the AuthorNote record. ActiveRecord will error it out and rollback the transaction.
To fix this, instead of commenting out the belongs_to relationship all together, you can make it optional (beacause, removing the relationship might break the system)
class AuthorNote < ApplicationRecord
belongs_to :book, optional: true
end

Use ! along with the save or create that will show the error message Object.save! or Object.create!
def create
#author_note = AuthorNote.new(author: params[:author_note][:author], user_id: current_user.id, comment: params[:author_note][:comment])
#author_note.save!
end
OR
def create
#author_note = AuthorNote.create!(author: params[:author_note][:author], user_id: current_user.id, comment: params[:author_note][:comment])
end

Related

How to upload multiple files with carrierwave and create multiple records

I am trying to modify my image upload routine in order to upload multiple images at the same time. Currently I am able to upload multiple images, but it creates only one record. How can I achieve it that one submit creates multiple records?
I do not want to go through an association, but instead be able to directly upload multiple images.
I currently have a Model:
class Diapo < ApplicationRecord
belongs_to :album
mount_uploaders :file_name, DiapoUploader
end
My Controller:
class Admin::DiaposController < ApplicationController
def new
#diapo = Diapo.new
#page_title = 'Create new Diapo'
end
def create
#diapo = Diapo.new(diapo_params)
if #diapo.save
flash[:notice] = 'Diapo #{#diapo.file_name} was successfully created.
redirect_to action: :index
else
#page_title = 'Create new Diapo'
render action: :new
end
end
.
.
.
private
def diapo_params
params.require(:diapo).permit({file_name: []}, :title, :alt, :author, :copyright, :album_id)
end
end
in the form I have:
<%= f.file_field :file_name, multiple: true, class: 'form-field' %>
Currently I get one post
Started POST "/admin/diapos" for ::1 at 2019-12-18 10:45:19 +0100
Processing by Admin::DiaposController#create as HTML
Parameters: {"authenticity_token"=>"something==", "diapo"=>{"file_name"=>[#<ActionDispatch::Http::UploadedFile:0x00007f9062b51a40 #tempfile=#<Tempfile:/var/folders/yg/pfjwzpkx5wq9d27svk760h0h0000gn/T/RackMultipart20191218-2329-7kffxo.jpg>, #original_filename="1.jpg", #content_type="image/jpeg", #headers="Content-Disposition: form-data; name=\"diapo[file_name][]\"; filename=\"1.jpg\"\r\nContent-Type: image/jpeg\r\n">, #<ActionDispatch::Http::UploadedFile:0x00007f9062b519f0 #tempfile=#<Tempfile:/var/folders/yg/pfjwzpkx5wq9d27svk760h0h0000gn/T/RackMultipart20191218-2329-o5qzhe.jpg>, #original_filename="2.jpg", #content_type="image/jpeg", #headers="Content-Disposition: form-data; name=\"diapo[file_name][]\"; filename=\"2.jpg\"\r\nContent-Type: image/jpeg\r\n">], "alt"=>"Test", "author_ids"=>"", "album_id"=>"124", "copyright"=>"Test"}, "commit"=>"Save"}
Admin Load (0.4ms) SELECT 'admins'.* FROM 'admins' WHERE 'admins'.'id' = 1 ORDER BY 'admins'.'id' ASC LIMIT 1
Unpermitted parameter: :author_ids
(0.2ms) BEGIN
↳ app/controllers/admin/diapos_controller.rb:14:in `create'
Album Load (0.3ms) SELECT `albums`.* FROM `albums` WHERE `albums`.`id` = 124 LIMIT 1
↳ app/controllers/admin/diapos_controller.rb:14:in `create'
Diapo Create (0.2ms) INSERT INTO `diapos` (`file_name`, `alt`, `copyright`, `album_id`, `created_at`, `updated_at`) VALUES ('[\"1.jpg\", \"2.jpg\"]', 'Test', 'Test', 124, '2019-12-18 09:45:34.406410', '2019-12-18 09:45:34.406410')
↳ app/controllers/admin/diapos_controller.rb:14:in `create'
(6.5ms) COMMIT
↳ app/controllers/admin/diapos_controller.rb:14:in `create'
Redirected to http://localhost:3000/admin/diapos
Completed 302 Found in 15350ms (ActiveRecord: 7.5ms | Allocations: 75512)
and 1 record is created which has the array of filenames in the filename field.
I suspect I can do it either in the controller or through javascript? How would I do this?
Thank you in advance!
You have to implement your requirement with has_many relation.
To save multiple record you may have parent child relationship just like below.
Ex :-
Post is a parent table and save its images as post_attachments which is child table
class Post < ActiveRecord::Base
has_many :post_attachments
accepts_nested_attributes_for :post_attachments
end
class PostAttachment < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader
belongs_to :post
end
this is how you can achieve to save multiple files for parent record
For more detail see this Article which may be helpful to you.
https://kolosek.com/carrierwave-upload-multiple-images/

Strange has_many Association Behavior in Rails 4

I've got two tables, User and Allergy. These are connected via another table, UserAllergy. The models are as would be expected:
class User
has_many :user_allergies
has_many :allergies, through: :user_allergies
end
class UserAllergy
belongs_to :user
belongs_to :allergy
end
class Allergy
has_many :user_allergies
has_many :users, through :user_allergies
end
What I'm confused about is creating allergies from a multiple-valued collection_select in my User form.
I have the following field:
<%= f.collection_select :allergy_ids,
Allergy.all,
:id,
:name,
{},
{ class: 'form-control', multiple: true }
%>
This correctly inserts a key into my params like so if I selected the Allergies with ids 1 and 2:
{ user: { id: "1", allergy_ids: ["", "1", "2"] } }
When I create the user instantiated with #user = User.new( my_params ), the weird behavior occurs. Instead of inserting the provided allergy_ids into the join table, Rails does a query to get all current user_allergies for the user, then deletes all of the current user_allergies:
Started PATCH "/employees/regular_user" for 127.0.0.1 at 2015-06-18 22:08:30 -0400
Processing by UsersController#update as HTML
Parameters: {"utf8"=>"✓", "user"=>{ "allergy_ids"=>["", "1", "2", "3"]}, "button"=>"", "id"=>"regular_user"}
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 2]]
(0.1ms) begin transaction
Allergy Load (0.1ms) SELECT "allergies".* FROM "allergies" INNER JOIN "user_allergies" ON "allergies"."id" = "user_allergies"."allergy_id" WHERE "user_allergies"."user_id" = ? [["user_id", 1]]
SQL (0.1ms) DELETE FROM "user_allergies" WHERE "user_allergies"."user_id" = ? AND "user_allergies"."allergy_id" = 1 [["user_id", 1]]
(27.4ms) commit transaction
Redirected to http://localhost:3000/employees/regular_user
Completed 302 Found in 32ms (ActiveRecord: 27.8ms)
Anyone knows what gives, or what I need to do to create allergies implicitly? I've tried accepts_nested_attributes_for and changing around the form to use fields_for.
So, I went and looked at code of mine that does a similar function. Here's what my create method looks like. This is creating a Student with assignment to Student Groups in a school setting (I didn't use "class" since Ruby wouldn't like that).
def create
#student = Student.new(student_params)
if #student.save
#student.student_groups = StudentGroup.where(id: params[:student][:student_group_ids])
flash[:success] = "Student was successfully created."
redirect_to #student
else
render 'new', notice: "Your student could not be created."
end
end
I completely ignore the Student Group IDs when creating the student_params, since I'm not using them for mass assignment.
Yes, one extra line of code. I'd be really interested to hear if there's a way to accomplish this via mass assignment.
You're missing one part of the puzzle which is the relation from Allergy to User.
class Allergy
has_many :user_allergies
has_many :users, through: :user_allergies
end
Just give the following code a try-
params.require(:user).permit(___, ____, {allergy_ids: []}, ____, ____)

Unpermitted parameters for an attribute that has an array of values

This is what the parameters look like for my Node#update:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"F1HGLeI9E=", "node"=>{"name"=>"Lesty", "parents"=>["13", "35", ""], "family_tree_id"=>"2"}, "commit"=>"Update Node", "id"=>"38"}
This is my NodesController:
private
def node_params
params.require(:node).permit(:user_id, :family_tree_id, :name, :description, :parent_id, :parent, :parents)
end
But when I try to update the #node object that produces the above log, I get this error:
Node Load (1.1ms) SELECT "nodes".* FROM "nodes" WHERE "nodes"."id" = $1 LIMIT 1 [["id", 38]]
Unpermitted parameters: parents
(1.7ms) BEGIN
(1.3ms) COMMIT
Redirected to http://localhost:3000/nodes/38
Completed 302 Found in 21ms (ActiveRecord: 4.0ms)
It is important to note that the parents attribute for my #node object is inherited via the gem ancestry and not directly on the Node model or via an association.
Thoughts?
You'll need to tell strong_parameters to expect an array if I recall correctly.
def node_params
params.require(:node).permit(:user_id, :family_tree_id, :name, :description, :parent_id, :parent, parents: [])
end
You can simply try this
private
def node_params
params.require(:node).permit(:name, :family_tree_id, parents: [])
end

How to add active record validation errors inside custom setter?

I have custom attribute setter in a Rails model in which i'm adding validation errors. However when record attributes are being updated 'true' returns as result, which kinda confusing to me. Any hints how to use validation errors inside custom setter?
Model:
class Post < ActiveRecord::Base
attr_accessible :body, :hidden_attribute, :title
def hidden_attribute=(value)
self.errors.add(:base, "not accepted")
self.errors.add(:hidden_attribute, "not_accepted")
write_attribute :hidden_attribute, value unless errors.any?
end
end
Console Output:
1.9.3p194 :024 > Post.last
Post Load (0.2ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" DESC LIMIT 1
=> #<Post id: 1, title: "asdsaD", body: "la", hidden_attribute: nil, created_at: "2013-11-13 16:55:44", updated_at: "2013-11-13 16:56:06">
1.9.3p194 :025 > Post.last.update_attribute :hidden_attribute, "ka"
Post Load (0.2ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" DESC LIMIT 1
(0.0ms) begin transaction
(0.0ms) commit transaction
=> true
I've made an example application for this case.
Okay, i understood the core of the issue. It's not possible to do what i want to achieve, because all the validations errors cleared out as soon as validation process starts.
https://github.com/rails/rails/blob/75b985e4e8b3319a4640a8d566d2f3eedce7918e/activemodel/lib/active_model/validations.rb#L178.
Custom setter kicks in way too early :(
In your setters, you can store the error messages in a temporary hash. Then you can create an ActiveRecord validation method to check if this temporary hash is empty and copy the error messages to errors.
For example,
def age=(age)
raise ArgumentError unless age.is_a? Integer
self.age = age
rescue ArgumentError
#setter_errors ||= {}
#setter_errors[:age] ||= []
#setter_errors[:age] << 'invalid input'
end
Here's the ActiveRecord validation
validate :validate_no_setter_errors
def validate_no_setter_errors
#setter_errors.each do |attribute, messages|
messages.each do |message|
errors.add(attribute, message)
end
end
#setter_errors.empty?
end
To see this in action:
[2] pry(main)> p.age = 'old'
=> "old"
[3] pry(main)> p.save!
(1.0ms) BEGIN
(1.2ms) ROLLBACK
ActiveRecord::RecordInvalid: Validation failed: Age invalid input
[4] pry(main)> p.errors.details
=> {:age=>[{:error=>"invalid input"}]}
This should work. Notice the change from update_attribute to update_attributes. update_attribute skips validation.
Post.last.update_attributes(:hidden_attribute => "ka")
def hidden_attribute=(value)
self.errors.add(:base, "not accepted")
self.errors.add(:hidden_attribute, "not_accepted")
write_attribute :hidden_attribute, value #removed the condition here because save doesn't do anything when the object is not changed
end
When you don't write the attribute, there is no change on the object and save does nothing but returns true.

Edit object with HABTM association modify database before validation with ActiveAdmin

I Having an issue when trying to update a model that has has_and_belongs_to_many association.
Let's say that Post has_and_belongs_to_many Tag, and Post validates the presence of title and Tags.
If I update Post, removing its title and tags, I get validation error in title and tags, ok.
But ActiveAdmin already removed the records that make association between Post and Tag, so, if I leave Post edit page, the post is left invalid on database, without tags.
Here my models:
class Tag < ActiveRecord::Base
attr_accessible :label
has_and_belongs_to_many :posts
end
class Post < ActiveRecord::Base
attr_accessible :content, :title, :tag_ids
has_and_belongs_to_many :tags
validates_presence_of :content, :title, :tags
end
ActiveAdmin.register Post do
form do |f|
f.inputs do
f.input :title
f.input :content
f.input :image
f.input :tags
end
f.buttons
end
end
I usign chosen-rails gem and it allows user to unselect all tags of post.
Summarizing, my problem is: ActiveAdmin updates relationships on database before perform model validations.
There a solution for this behavior or I doing something wrong?
Edit:
Here the request log when I trying to update post without title and tags:
Started PUT "/admin/posts/8" for 127.0.0.1 at 2013-04-01 10:32:07 -0300
Processing by Admin::PostsController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"amSbLlP/rgDrNn/N8lgq/KEaRXK1fMPShZDwpZ0QIJ4=", "post"=>{"title"=>"", "content"=>"content", "tag_ids"=>["", ""]}, "commit"=>"Update Post", "id"=>"8"}
AdminUser Load (0.2ms) SELECT `admin_users`.* FROM `admin_users` WHERE `admin_users`.`id` = 1 LIMIT 1
Post Load (0.2ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 8 LIMIT 1
Tag Load (0.2ms) SELECT `tags`.* FROM `tags` INNER JOIN `posts_tags` ON `tags`.`id` = `posts_tags`.`tag_id` WHERE `posts_tags`.`post_id` = 8
(0.1ms) BEGIN
SQL (12.3ms) DELETE FROM `posts_tags` WHERE `posts_tags`.`post_id` = 8 AND `posts_tags`.`tag_id` IN (1, 2)
(49.6ms) COMMIT
(0.1ms) BEGIN
(0.2ms) ROLLBACK
Post Load (0.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 8 LIMIT 1
Tag Load (0.2ms) SELECT `tags`.* FROM `tags`
Rendered /home/rodrigo/.rvm/gems/ruby-1.9.3-p125#blog/gems/activeadmin-0.5.1/app/views/active_admin/resource/edit.html.arb (192.3ms)
Completed 200 OK in 276ms (Views: 194.8ms | ActiveRecord: 63.3ms)
EDIT 2:
Ok, I sure that ActiveAdmin has this bug.
Looking at ActiveRecord behaviour, I think that validation flow is broken using only model class. See this example:
1.9.3p125 :064 > post = Post.find(8)
Post Load (0.3ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 8 LIMIT 1
=> #<Post id: 8, title: "title", content: "content", created_at: "2013-03-27 13:13:20", updated_at: "2013-03-27 13:13:20", image: "extrato.bmp">
1.9.3p125 :065 > post.tags
Tag Load (0.2ms) SELECT `tags`.* FROM `tags` INNER JOIN `posts_tags` ON `tags`.`id` = `posts_tags`.`tag_id` WHERE `posts_tags`.`post_id` = 8
=> [#<Tag id: 1, label: "tag", created_at: "2013-02-25 18:32:45", updated_at: "2013-02-25 18:32:45">, #<Tag id: 2, label: "new", created_at: "2013-02-25 18:32:50", updated_at: "2013-02-25 18:32:50">]
1.9.3p125 :066 > post.title = ""
=> ""
1.9.3p125 :067 > post.save #<<<<<<< It's invalid on title
=> false
1.9.3p125 :068 > post.tags = [] #<<<<<<< This shouldnt trigger database update
(0.3ms) BEGIN
SQL (0.5ms) DELETE FROM `posts_tags` WHERE `posts_tags`.`post_id` = 8 AND `posts_tags`.`tag_id` IN (1, 2)
(55.5ms) COMMIT
=> []
1.9.3p125 :069 > post.save #<<<<<<< It's invalid on title AND TAGS
(0.2ms) BEGIN
(0.2ms) ROLLBACK
=> false
1.9.3p125 :070 > post.reload
Post Load (0.2ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 8 LIMIT 1
=> #<Post id: 8, title: "title", content: "content", created_at: "2013-03-27 13:13:20", updated_at: "2013-03-27 13:13:20", image: "extrato.bmp">
1.9.3p125 :071 > post.valid? #<<<<<<< Now, I have this model in invalid state
Tag Load (0.6ms) SELECT `tags`.* FROM `tags` INNER JOIN `posts_tags` ON `tags`.`id` = `posts_tags`.`tag_id` WHERE `posts_tags`.`post_id` = 8
=> false
Has any way to update post attributes(including tags) and validate the model before doing any database update?
You can use this gem: https://github.com/MartinKoerner/deferred_associations
The deferred associations will fix this bug.
#Rodrigo I was able to reproduce your issue locally without active admin, the issue is actually that is one of the default operations when using HABTM relationships if you see [here][1]
[1]: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_and_belongs_to_many it says:
collection=objects
Replaces the collection’s content by deleting and adding objects as appropriate.
So apparently you will need to override this operation
Here is an example:
Override ActiveRecord << operator on has_many :through relationship, to accept data for the join model
Let me know how can I help you
I'd propose to add a tmp variable and store values in it. Then you should move them to database if validation passed.
Here is an example:
Your models/article.rb:
class Article < ActiveRecord::Base
validate :author_presence
has_and_belongs_to_many :authors
attr_writer :tmp_author_ids
def tmp_author_ids
#tmp_author_ids || author_ids
end
def author_presence
if tmp_author_ids.reject(&:blank?).empty?
errors.add(:tmp_author_ids, 'Author is missing')
else
self.author_ids = tmp_author_ids
end
end
end
Your admin/article.rb, block form:
f.input :tmp_author_ids, as: :select, multiple: true, collection: Author.all, label: 'Authors'
That's it
So, for anyone having the same issue, I found a workaround:
You can define a before_remove in the has_and_belongs_to_many association and raise an exception that then you can catch in ActiveAdmin.
For this it would be
class Post < ActiveRecord::Base
attr_accessible :content, :title, :tag_ids
has_and_belongs_to_many :tags, before_remove: :check_something
validates_presence_of :content, :title, :tags
end
def check_something(agent)
if self.tags.size == 1
raise ActiveModel::MissingAttributeError.new 'something'
end
end
more info: https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
if you want, you can redefine you update action in active_admin to prevent saving empty tags, in something like this style
ActiveAdmin.register Post do
controller do
def update
if params[:post][:tag_ids] == ["", ""]
flash.now[:alert] = "You can't remove all tags"
render :edit
else
super
end
end
end
...
end
and i think this stuff from model can be deleted
attr_accessor :new_tag_ids
validate :validate_new_tags_ids
after_save :update_tags
def update_tags
self.tag_ids = #new_tag_ids if defined?(#new_tag_ids)
#new_tag_ids = nil
end
private
def validate_new_tags_ids
errors[:tags] << "can't be blank (2)" if #new_tag_ids.blank?
end

Resources