ActiveRecord raising exception on save without a bang in Rails - mysql

I have this code in my controller :
#user = User.new(params.require(:user).permit(:email,:password))
if #user.save
redirect_to(home_users_path, :notice => "Success")
else
redirect_to(new_user_path , :notice => 'Signup Failed.')
end
but I am getting ActiveRecord::RecordNotUnique in UsersController#create.
I know the record is not unique, my question is I am using .save which should not generate any exception but return false. But in my application .save is behaving as save!

Indeed, when you have validates :status, uniqueness: true in the model , save! will raise an exception, but save won't.
But ActiveRecord::RecordNotUnique is raised, because there's a uniqueness index on the column, so the validation is performed on database level. Just like ActiveRecord::RecordNotFound, when find method is called, though find is not a bang-method

Related

Can you show a flash message conditionally on what happened inside a yield block in an around_action filter?

I think I have an understanding of how an around_action works, basically performing what is before the yield as a before_action and what happens after the yield as an after_action.
I would like to know how to effectively handle errors and feedback given to the user if something wrong happens along the way, since yield runs all the code in the block (in this exampe, the index action of a controller) no matter what.
How can I display flash messages conditionally of wether an error was raised or if a rescue from error was performed or not?
Problem: A flash[:success] is rendered even when a rescue from error is performed (misleading).
controller:
class ReportsController
around_action :wrap_in_transaction, only: %i(index)
rescue_from FileExportError, with: :file_export_error
def index
#tickets = Ticket.all
respond_to do |format|
format.html
format.xlsx do
response.headers["Content-Disposition"] = "attachment; filename=report"
end
end
flash[:success] = "Success"
update_tickets(#tickets) # rolls back if a rescue happens
end
end
private
def wrap_in_transaction
ActiveRecord::Base.transaction do
yield
rescue FileExportError
raise ActiveRecord::Rollback
end
end
def file_export_error
flash[:danger] = t(".file_export_error")
redirect_to reports_path
end
def update_tickets(tickets)
tickets.each do |ticket|
ticket.update(status: "unpaid")
end
end
end
.xls.erb raise error if corrupted data trying to build file:
#tickets.each do |ticket|
if ticket.some_data.nil?
raise FileExportError
end
sheet.add_row [ticket.user.full_name,
ticket.user.phone,
...]
(Disregard my prev answer, I was skimming and misread how you were triggering the rollback.)
Your code does the following:
Loads all tickets
Configures the response format
Sets the flash message to Success
Performs some record updates
At some point after this, an export error might be raised; this triggers the rollback of the DB transaction, but the flash[:success] assignment isn't going to roll back because it's not a DB transaction. Why not try doing a flash[:error] rescue block instead of success? Or maybe moving success after yield in the transaction block might work?

can't create a record in a database

I am using rails version 4.2 and ruby version 2.2.0. I am trying to save a record to lollypops table. No exceptions indicating reasons.
TASK: As soon as a member is created and saved, I want to populate the lollypops table by calling the create_lollypop(#member.id) in members controller's create method like this:
# POST /members
# POST /members.json
def create
#member = Member.create(members_params)
return unless request.post?
#member.save!
self.current_user = #member
c = Country.find(#member.country_id)
#member.update_attributes(
:country_code=>c.code)
create_lollypop(#member.id) #From here I want to create lollypop
MemberMailer.signup_notification(#member).deliver_now
redirect_to(:controller => '/admin/members', :action => 'show',
:id=> #member.id)
flash[:notice] = "Thanks for signing up! Check your email now to
confirm that your email is correct!"
rescue ActiveRecord::RecordInvalid
load_data
render :action => 'new'
end
def create_lollypop(member_id)
#member = Member.find(member_id)
Lollypop.create(
:member_id=>#member.id,
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country,
:postcode_index=>#member.postcode_index,
:name=>#member.name)
end
The 'member' is created but the 'lollypops' table is not populated. The associations are:
MEMBER model:
has_one :lollypop, :dependent=>:destroy
LOLLYPOP model
belongs_to :member
If I use generic SQL command then the lollypops table gets populated but I do not want to do that:
def self.create_lollypop(member_id)
member = Member.find(member_id)
ActiveRecord::Base.connection.execute("insert into lollypops (member_id,product_name,product_price,email,house_flat,street,city_town,country,postcode_index,name)
values(#{member.id},'lollypop',#{100},'#{member.email}','#{member.house_flat}','#{member.street}','#{member.city_town}','#{member.country_code}','#{member.postcode_index}','#{member.name}')")
end
Any advice would be welcomed. Thank you.
In your create_lollypop(), You are not defining #member.
def create_lollypop(member_id)
#member = Member.find member_id
Lollypop.create!(
:member_id=>#member.id,
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country,
:postcode_index=>#member.postcode_index,
:name=>#member.name
)
end
Also use create! so in case any validation failed then it will raise exception. So it will help you sort out issue.
For the moment try to create lollypop using the association method create_lollypop directly in your controller. use this code in you create controller method, note that create_lollypop method will fill (member_id field automatically):
#member = Member.create(members_params)
return unless request.post?
#member.save!
self.current_user = #member
c = Country.find(#member.country_id)
#member.update_attributes(
:country_code=>c.code)
#From here I want to create lollypop
#member.create_lollypop(
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country,
:postcode_index=>#member.postcode_index,
:name=>#member.name
)
MemberMailer.signup_notification(#member).deliver_now
redirect_to(:controller => '/admin/members', :action => 'show',
:id=> #member.id)
flash[:notice] = "Thanks for signing up! Check your email now to
confirm that your email is correct!"
rescue ActiveRecord::RecordInvalid
load_data
render :action => 'new'
This is not exactly an answer, more like tips and notes, it's a little long and I hope you don't mind.
return unless request.post?
This is more of a php thing not a rails thing, in rails already the routing is checking this, so you don't need to do this check inside the controller, if it isn't a post it will be routed elsewhere.
#member = Member.create(members_params)
return unless request.post?
#member.save!
Saving after creating is meaningless, because create already saves the data, if you are doing it for the sake of the bang save!, then you could use the create with bang create!, not to mention that you do the redirection check after the member's create, so if this did work, it would leave you with stray members.
c = Country.find(#member.country_id)
#member.update_attributes(:country_code=>c.code)
If you have your assocciations correctly, you don't need to save the code like this, because the member knows that this country_id belongs to a country.
So add this to the member model
class Member < ActiveRecord::Base
has_one :lollypop, dependent: :destroy
belongs_to :country
end
This way you could always call #member.country to return the country object, then the code could come from there, like #member.country.code, or you could just write a method to shorten that up
def country_code
country.code
end
this way will get the code through an extra query, but it has an advantage, if you for any reason change a country's code, you don't need to loop on all members who have that country and update their codes too, you could also shorten this up even more using #delegate
#member.save!
#member.update_attributes(:country_code=>c.code)
Here you are updating the attributes of member after saving the member, which is kinda a waste, because you are doing 2 queries for what could be done with 1 query, programmatically it is correct and it will work, but it's bad for scaling, when more users start using your app the database will be more busy and the responses will be slower.
Instead i would recommend to postpone the creation of member till you have all the data you want
#member = Member.new(members_params) # this won't save to the database yet
#memeber.code = Country.find(#member.country_id).code
#member.save
This will only do 1 query at the end when all data is ready to be saved.
redirect_to(:controller => '/admin/members', :action => 'show', :id=> #member.id)
This is ok, but you probably have a better shorter path name in your routes, something like members_admin_path, check your routes name by doing a bin/rake routes in your terminal.
redirect_to members_admin_path(id: #member)
redirect_to ...
flash[:notice] = "message"
I'm not sure this will work, because the redirection needs to be returned, but when you added the flash after it, either the redirection will happen without the flash, or the flash will be set and returned as it's the last statement, but the redirection won't happen, not sure which will happen, to fix it you can simply swap the two statements, create the flash first and then redirect, or use the more convenient way of setting the flash while redirecting, cause that's supported
redirect_to ....., notice: 'my message'
rescue ActiveRecord::RecordInvalid
load_data
render :action => 'new'
This will do the job, but it isn't conventional, people tend to use the soft save and then do an if condition on the return value, either true or false, here's a short layout
# prepare #member's data
if #member.save
# set flash and redirect
else
load_data
render :new
end
The lollypop creation
Now there's a few things about this, first you have the method in the controller, which is bad cause it shouldn't be the controller's concern, the second method the self.create_lollypop is better cause it's created on the model level, but it's a class method, then the better way is creating it as a member method, this way the member who creates the lollypop already knows the data because it's his own self, notice i don't need to call #member because i am already inside member, so simple calls like id, email will return the member's data
# inside member.rb
def create_lollypop
Lollypop.create!(
member_id: id,
product_name: 'lollypop',
product_price: 100,
email: email,
house_flat: house_flat,
street: street,
city_town: city_town,
country: country,
postcode_index: postcode_index,
name: name
)
end
if you want you can also add this as an after create callback
after_create :create_lollypop
ps: This method name will probably conflict with the ActiveRecords create_lollypop method, so maybe you should pick a different name for this method.
As Mohammad had suggested to me, I changed Lollypop.create to Lollypop.create! and
while running my code, one validation error popped up. After correcting it and
altering my code to:
Lollypop.create!(
:member_id=> #member.id,
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country_code,
:postcode_index=>#member.postcode_index,
:name=>#member.name
)
The 'lollypops' table got populated.

How do you Skip an Object in an ActiveModel Serializer Array?

I have searched through all the active model serializer (v 0.9.0) documentation and SO questions I can find, but can't figure this out.
I have objects which can be marked as "published" or "draft". When they aren't published, only the user who created the object should be able to see it. I can obviously set permissions for "show" in my controller, but I also want to remove these objects from the json my "index" action returns unless it is the correct user. Is there a way to remove this object from the json returned by the serializer completely?
In my activemodel serializer, I am able to user filter(keys) and overloaded attributes to remove the data, as shown using my code below, but I can't just delete the entire object (I'm left having to return an empty {} in my json, trying to return nil breaks the serializer).
I'm probably missing something simple. Any help would be much appreciated!
class CompleteExampleSerializer < ExampleSerializer
attributes :id, :title
has_many :children
def attributes
data = super
(object.published? || object.user == scope || scope.admin?) ? data : {}
end
def filter(keys)
keys = super
(object.published? || object.user == scope || scope.admin?) ? keys : {}
end
end
That looks correct, try returning an array instead of a hash when you dont want any keys. Also, I don't think calling super is necessary b/c the filter takes in the keys.
Also, I don't think defining an attributes method is necessary.
I have chapters that can either be published or unpublished. They're owned by a story so I ended doing something like below.
has_many :unpublished_chapters, -> { where published: false }, :class_name => "Chapter", dependent: :destroy
has_many :published_chapters, -> { where published: true }, :class_name => "Chapter", dependent: :destroy
Inside of my serializer, I choose to include unpublished_chapters only if the current_user is the owner of those chapters. In ams 0.8.0 the syntax is like so.
def include_associations!
include! :published_chapters if ::Authorization::Story.include_published_chapters?(current_user,object,#options)
include! :unpublished_chapters if ::Authorization::Story.include_unpublished_chapters?(current_user,object,#options)
end
In my case, it's not so bad to differentiate the two and it saves me the trouble of dealing with it on the client. Our situations are similar but say you want to get all of the chapters by visiting the chapters index route. This doesn't make much sense in my app but you could go to that controller and render a query on that table.

Ruby on Rails: Decompression (Zlib::Deflate) doesn't work after certain amount of time

I have a need to compress large chunk of text before saving it to the database and decompress it back once client requests it.
The method I am using right now seems to work fine when I insert new records using the Rails console and query for the newly inserted record right away. i.e., I can decompress the compressed description successfully.
But I am not able to decompress the compressed description for any of my other records added prior to this date. It is really confusing for me especially being a beginnner to the ROR world.
I am using MySQL as a database.
See my Model below to better understand it.
require "base64"
class Video < ActiveRecord::Base
before_save :compress_description
def desc
unless description.blank?
return decompress(description)
end
end
private
def compress_description
unless description.blank?
self.description = compress(description)
end
end
def compress(text)
Base64.encode64(Zlib::Deflate.new(nil, -Zlib::MAX_WBITS).deflate(text, Zlib::FINISH))
end
def decompress(text)
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(Base64.decode64(text))
end
end
Ok it's actually very easy to reproduce your problem. In rails console do the following
Video.create(:description => "This is a test")
Video.last.description
=> "C8nILFYAokSFktTiEgA=\n"
Video.last.desc
=> "This is a test"
Video.last.save #This update corrupts the description
Video.last.desc
=> "C8nILFYAokSFktTiEgA=\n"
The reason the corruption happens is because you are compressing an already compressed string
You should probably modify your class as follows and you should be fine
require 'base64'
class Video < ActiveRecord::Base
before_save :compress_description
after_find :decompress_description
attr_accessor :uncompressed_description
private
def compress_description
unless #uncompressed_description.blank?
self.description = compress(#uncompressed_description)
end
end
def decompress_description
unless description.blank?
#uncompressed_description = decompress(description)
end
end
def compress(text)
Base64.encode64(Zlib::Deflate.new(nil, -Zlib::MAX_WBITS).deflate(text, Zlib::FINISH))
end
def decompress(text)
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(Base64.decode64(text))
end
end
Now use your class as follows
Video.create(:uncompressed_description => "This is a test")
Video.last.description
=> "C8nILFYAokSFktTiEgA=\n"
Video.last.uncompressed_description
=> "This is a test"
Video.last.save
Video.last.uncompressed_description
=> "This is a test"

How to make Authlogic sessions work for all subdomains

When a user logs into my site at example.com, I want him to be logged in when he visits something.example.com. How can I accomplish this? (I'm using subdomain-fu if relevant)
Well, you can, just add following lines into /etc/hosts after "127.0.0.1 localhost"
127.0.0.1 localhost.com
127.0.0.1 sub.localhost.com
Then edit your environments/development.rb and add
config.action_controller.session = { :domain => '.localhost.com' }
From now on use http://localhost.com:3000 or the same but with sub-domain to access your app locally.
[update] oops, it was the answer to Horace Loeb
For Rails3 the code above will raise NoMethodError:
undefined method `session=' for ActionController::Base:Class
So, for Rails3 you should not change you environment config but should set your app/config/initializers/session_store.rb to look like:
YourAppName::Application.config.session_store :active_record_store,
{:key => '_your_namespace_session', :domain => '.yourdomain.com'}
Also after changing the initializer you'll need to restart a webserver in order to apply the initializer.
Notice, that users who were logged in before code update won't be able to logout after that because the default logout action which is looking something like:
destroy
current_user_session.destroy
flash[:notice] = "You have been logged out"
redirect_to root_path
end
is not sufficient - it doesn't delete user_credentials cookie set for a non-wildcard domain yourdomain.com by default. So you should add cookies.delete :user_credentials to the destroy action so it will look like this:
destroy
current_user_session.destroy
cookies.delete :user_credentials
flash[:notice] = "You have been logged out"
redirect_to root_path
end
And that's odd but it should be added after destroying user session despite of cookies[:user_credentials].is_nil? == true at this point. Also there is a problem that after a user logouts and then logins having cookies.delete :user_credentials in the destroy action also makes users to be unable to logout and it should be removed. Does anybody have a solution for this?
Update. Finally I came up to this - I added a boolean flag to User model via migration:
class AddReloginedToUsers < ActiveRecord::Migration
def change
add_column :users, :relogined, :boolean, :default => false
end
end
and changed the destroy action this way:
def destroy
current_user_session.destroy
if !current_user.relogined
current_user.relogined = true
current_user.save
cookies.delete(:user_credentials)
end
session = nil
flash[:notice] = "You have been logged out"
redirect_to root_path
end
Now everything works as expected although that's not a very beautiful solution. I'll be glad if anyone provides something smarter.
The fix is to add this to production.rb:
if config.action_controller.session
config.action_controller.session[:domain] = '.your-site.com'
else
config.action_controller.session = { :domain => '.your-site.com' }
end
I still can't get it to work in development with localhost:3000, but whatever