Nested Resource Not Properly Destroyed on Dependent Destroy - youtube

I'm having issue with a youtube video being destroyed properly in a nested belongs_to has_one relationship between a sermon and its sermon video when using :dependent => :destroy.
I'm using the youtube_it gem and have a fairly vanilla setup.
The relevant bits below:
the video controller --
def destroy
#sermon = Sermon.find(params[:sermon_id])
#sermon_video = #sermon.sermon_video
if SermonVideo.delete_video(#sermon_video)
flash[:notice] = "video successfully deleted"
else
flash[:error] = "video unsuccessfully deleted"
end
redirect_to dashboard_path
end
the video model --
belongs_to :sermon
def self.yt_session
#yt_session ||= YouTubeIt::Client.new(:username => YouTubeITConfig.username , :password => YouTubeITConfig.password , :dev_key => YouTubeITConfig.dev_key)
end
def self.delete_video(video)
yt_session.video_delete(video.yt_video_id)
video.destroy
rescue
video.destroy
end
the sermon model --
has_one :sermon_video, :dependent => :destroy
accepts_nested_attributes_for :sermon_video, :allow_destroy => true
In the above setup, all local data is removed successfully; however, the video on youtube is not.
I have tried to override the destroy action with a method in the model, but probably due a failing of my understanding, can only get either the video deleted from youtube, or the record deleted locally, never both at the same time (I posted the two variants below and their results).
This only serves to destroy the local record --
def self.destroy
#yt_session ||= YouTubeIt::Client.new(:username => YouTubeITConfig.username , :password => YouTubeITConfig.password , :dev_key => YouTubeITConfig.dev_key)
#yt_session.video_delete(self.yt_video_id)
#sermon_video.destory
end
This only serves to destroy the video on youtube, but not the local resource --
def self.destroy
#yt_session ||= YouTubeIt::Client.new(:username => YouTubeITConfig.username , :password => YouTubeITConfig.password , :dev_key => YouTubeITConfig.dev_key)
#yt_session.video_delete(self.yt_video_id)
end
Lastly, the link I'm using to destroy the sermon, in case it helps --
<%= link_to "Delete", [#sermon.church, #sermon], :method => :delete %>
Thanks for your help, very much appreciated!

It looks as though I have just solved the issue; however, I'll leave it open for a bit in case someone has a more elegant / appropriate solution.
In the sermon video model I added --
before_destroy :kill_everything
def kill_everything
#yt_session ||= YouTubeIt::Client.new(:username => YouTubeITConfig.username , :password => YouTubeITConfig.password , :dev_key => YouTubeITConfig.dev_key)
#yt_session.video_delete(self.yt_video_id)
end
And the key thing, I believe, to have added in the sermon model was this --
accepts_nested_attributes_for :sermon_video, :allow_destroy => true

Related

Handling multiple carts in rails

I'm wondering if there is a shorter solution for my problem.
I'm currently building a multiple shop system, which means a website with different shops where you have 1 open cart for each shop.
This cart can be owned from a guest or a user.
The cart/order should only be created when an item is added.
The following definition is placed in the application controller.
def find_order_by_shop(shop)
shop = Shop.find(shop.to_i) if !(shop.class == Shop)
if session["order_id_for_shop_#{shop.try(:id)}"]
order = Order.find_by_id_and_state(session["order_id_for_shop_#{shop.id}"],'open')
if order
if current_user
current_user.orders.where(:shop_id => shop.id , :state => 'open').where.not(:id => session["order_id_for_shop_#{shop.id}"]).delete_all
order.update_attribute(:user_id, current_user.id)
order = current_user.orders.where(:shop_id => shop.id , :state => 'open').first
if order
# delete all exept first
current_user.orders.where(:shop_id => shop.id , :state => 'open').where.not(:id => order.id).delete_all
else
# create new
order = current_user.orders.new(:shop_id => shop.id , :state => 'open')
end
end
else
order = Order.new(:shop_id => shop.id, :state => 'open')
end
else
if current_user
order = current_user.orders.where(:shop_id => shop.id , :state => 'open').first
if order
current_user.orders.where(:shop_id => shop.id , :state => 'open').where.not(:id => order.id).delete_all
else
order = current_user.orders.new(:shop_id => shop.id , :state => 'open')
end
else
# guest
order = Order.new(:shop_id => shop.id , :state => 'open')
end
end
session["order_id_for_shop_#{shop.try(:id)}"] = order.id
return order
end
Updating the session while adding an item to the cart/order
...
def create # add item to order
#order = find_order_by_shop(params[:shop_id].to_i)
#order.save # save cart
session["order_id_for_shop_#{params[:shop_id]}"] = #order.id
...
This doesn't seem to be the correct rails way.
Any suggestions ?
I don't know about 'shorter', but just glancing at your code I would make a few recommendations.
Each change you make, test the code. Hopefully you have unit and functional tests, if not then check expected behaviour in a browser.
Try to split your logic into sensible, small, tight units with sensible names. Turn them into methods. As you do so, you may find some problems or points where you could optimise. For example:
if order
if current_user
current_user.orders.where(:shop_id => shop.id , :state => 'open').where.not(:id => session["order_id_for_shop_#{shop.id}"]).delete_all
order.update_attribute(:user_id, current_user.id)
order = current_user.orders.where(:shop_id => shop.id , :state => 'open').first
if order
# delete all exept first
current_user.orders.where(:shop_id => shop.id , :state => 'open').where.not(:id => order.id).delete_all
else
# create new
order = current_user.orders.new(:shop_id => shop.id , :state => 'open')
end
end
else
order = Order.new(:shop_id => shop.id, :state => 'open')
end
As there's an 'if order' conditional around this whole block, the internal 'if order' is redundant and the whole 'else' branch can be removed:
if order
if current_user
current_user.orders.where(:shop_id => shop.id , :state => 'open').where.not(:id => session["order_id_for_shop_#{shop.id}"]).delete_all
order.update_attribute(:user_id, current_user.id)
order = current_user.orders.where(:shop_id => shop.id , :state => 'open').first
# delete all except first
current_user.orders.where(:shop_id => shop.id , :state => 'open').where.not(:id => order.id).delete_all
end
else
order = Order.new(:shop_id => shop.id, :state => 'open')
end
Now you can split the functionality up into small, reusable blocks:
def order_from_shop(shop_id)
Order.new(:shop_id => shop_id, :state => 'open')
end
Look through your code and you can see at least two places that you can use this method.
Notice that there is no return statement. The Ruby/Rails 'way' is to allow the automatic return to kick in - the result of the last statement in a method is returned without explicitly declaring it. You can apply this to the end of your main method:
...
session["order_id_for_shop_#{shop.try(:id)}"] = order.id
order
end
Back to the rest of the code, start extracting more methods, like:
def user_order_from_shop(shop_id)
current_user.orders.where(:shop_id => shop.id , :state => 'open').first
end
There's a few places you can use that, too.
Encapsulate your if statements in small methods, of the form:
def method
if xxx
a_method
else
a_different_method
end
end
According to Sandi Metz, no method should have more than 5 lines. You don't have to be that extreme but it's useful to do so.
Eventually you'll have something that reads a lot more like English, so will be much easier to read and to determine the behaviour of at a glace. At that point you may well notice a lot more duplication or unnecessary, dead chunks of unreachable code. Be ruthless with both.
Finally, this whole thing looks like it needs its own class.
# app/services/shop_order_query.rb
class ShopOrderQuery
attr_accessor :shop, :order, :current_user
def initialize(shop, order, current_user)
self.shop = shop
self.order = order
self.current_user = current_user
end
def find_order_by_shop
...
...
end
private
# support methods for main look-up
def order_from_shop(shop_id)
...
end
...
...
end
Then call it with
ShopOrderQuery.new(shop, order, current_user).find_order_by_shop
Then it's all nicely tucked away, and usable from wherever you can pass it a shop, order and current user... And it's not cluttering up your controller.
For further reading, look for blog posts on making thin controllers, extracting service objects and Sandi Metz's Rules. Also, buy Sandi's book on Ruby OO programming.

Rails3 fails validation and loses child fields

I have a form that prompts for a job and job_files to attach. When the job save fails due to validation, the selected job_files disappear when the form redisplays. How do I retain the child fields? All parent fields are retained in the form but the child fields are gone.
job model:
class Job < ActiveRecord::Base
has_many :job_files, :dependent => :destroy
accepts_nested_attributes_for :job_files, :allow_destroy => true
validates_acceptance_of :disclaimer, :allow_nil => false, :accept => true, :on => :create, :message=>'Must accept Terms of Service'
end
job_files model:
class JobFile < ActiveRecord::Base
belongs_to :job
has_attached_file :file
end
jobs_controller:
def new
#upload_type = UploadType.find_by_id(params[:upload_job_id])
#job = Job.new
#job.startdate = Time.now
10.times {#job.job_files.build}
#global_settings = GlobalSettings.all
end
def create
#global_settings = GlobalSettings.all
if params[:cancel_button]
redirect_to root_path
else
#job = Job.new(params[:job])
#job.user_id = current_user.id
#upload_type = UploadType.find_by_id(#job.upload_type_id)
if #job.save
JobMailer.job_uploaded_notification(#upload_type,#job).deliver
flash[:notice] = "Job Created"
redirect_to root_path
else
# retain selected files if save fails
(10 - #job.job_files.size).times { #job.job_files.build }
flash[:error] = "Job Creation Failed"
render :action => :new
end
end
end
job_files partial within job form:
<%= form.fields_for :job_files, :html=>{ :multipart => true } do |f| %>
<li class="files_to_upload">
<%= f.file_field :file %>
</li>
<% end %>
Any help would be appreciated since I can not find any solution online.
That's standard-issue behavior. Really, the only way to workaround it would be to persist the files first, regardless of whether validation passes. Then, run your validations. But that's definitely a workaround.
You could also do client-side validations before the server-side ones; that way, you'd have better assurances that the user's data is valid BEFORE they submit to the server, such that they won't encounter any server-side validation failures, and the files will always make it. I'd probably go that route if I were in your shoes. Note that client-side validation is NOT a replacement for server-side validation, just will help your users in this case.
Hope that helps!

Why is Routes.rb not loading the IPs from cache?

I am testing this in local. My ip is 127.0.0.1. The ip_permissions table, is empty. When I browse the site, everything works as expected.
Now, I want to simulate browsing the site with a banned IP. So I add the IP into the ip_permissions table via:
IpPermission.create!(:ip => '127.0.0.1', :note => 'foobar', :category => 'blacklist')
In Rails console, I clear the cache via; Rails.cache.clear. I browse the site. I don't get sent to pages#blacklist.
If I restart the server. And browse the site, then I get sent to pages#blacklist. Why do I need to restart the server every time the ip_permissions table is updated? Shouldn't it fetch it based on cache?
Routes look like:
class BlacklistConstraint
def initialize
#blacklist = IpPermission.blacklist
end
def matches?(request)
#blacklist.map { |b| b.ip }.include? request.remote_ip
end
end
Foobar::Application.routes.draw do
match '/(*path)' => 'pages#blacklist', :constraints => BlacklistConstraint.new
....
end
My model looks like:
class IpPermission < ActiveRecord::Base
validates_presence_of :ip, :note, :category
validates_uniqueness_of :ip, :scope => [:category]
validates :category, :inclusion => { :in => ['whitelist', 'blacklist'] }
def self.whitelist
Rails.cache.fetch('whitelist', :expires_in => 1.month) { self.where(:category => 'whitelist').all }
end
def self.blacklist
Rails.cache.fetch('blacklist', :expires_in => 1.month) { self.where(:category => 'blacklist').all }
end
end
You are initializing BlacklistConstraint in your routes file which is loaded only once at the start. There you call IpPermission.blacklist and store it in an instance variable. Initialize is not called anymore and therefore you check against the same records.
You should load the records on each request if you want them to be updated:
class BlacklistConstraint
def blacklist
IpPermission.blacklist
end
def matches?(request)
blacklist.map { |b| b.ip }.include? request.remote_ip
end
end

How should I refactor create_unique methods in Rails 3?

I have following ugly create_unique method in few models ex:
def self.create_unique(p)
s = Subscription.find :first, :conditions => ['user_id = ? AND app_id = ?', p[:user_id], p[:app_id]]
Subscription.create(p) if !s
end
And then in controllers #create actions I have
s = Subscription.create_unique({:user_id => current_user.id, :app_id => app.id})
if s
raise Exceptions::NotAuthorized unless current_user == s.user
#app = s.app
s.destroy
flash[:notice] = 'You have been unsubscribed from '+#app.name+'.'
redirect_to '/'
end
did you try dynamic finders ?
find_or_initialize_by_user_id_and_app_id
find_or_create_by_user_id_and_app_id
first_or_initialize...
first_or_create....
check manual http://guides.rubyonrails.org/active_record_querying.html#dynamic-finders
also option is to create validation rule for check unique value
class Subscription < ActiveRecord::Base
validates_uniqueness_of :user_id, :scope => :app_id
end
then
sub = Subscription.new({:user_id => current_user.id, :app_id => app.id})
sub.valid? #false
You can use validates_uniquness_of :app_id,:scope=>:user_id so app id is uniq for respected user_id

How do I catch/solve an ActiveRecord:RecordInvalid exception caused when I save a built assocation

I am hoping to get some help solving a problem that I'm sure many of you could avoid in your sleep.
I have two models in a habtm relationship. A package can have many locations, and a location can have many packages. If my location model fails validation (due to an empty location address, for example), I get anActiveRecord:RecordInvalid exception. I understand that I'm getting this error because when I call package.save, rails automatically calls save! on the location association.
I'm not sure how to avoid the error or at least rescue the error. Do any of you have any good advice, both on how to solve the problem and on Rails best practices?
Here is the code:
def create
#package = current_user.package.build(params[:package])
package_location
if #package.save
flash[:success] = "Package created!"
redirect_to root_path
else
render 'pages/home'
end
end
def package_location
gps_processing if !session[:gps_aware]
#package.locations.build(:address => session[:address])
end
def gps_processing
session[:address] = [params[:story][:street_address], params[:story][:city], params[:story][:state], params[:story][:country]].compact.join(', ')
end
class Package< ActiveRecord::Base
belongs_to :user
has_and_belongs_to_many :locations
validates :content, :presence => true,
:length => {:maximum => 140}
validates :user_id, :presence => true
default_scope :order => 'package.created_at DESC'
end
class Location < ActiveRecord::Base
attr_accessible :lng, :lat, :address
validates :lng, :presence => true
validates :lat, :presence => true
validates :address, :presence => true
geocoded_by :full_street_address, :latitude => :lat, :longitude => :lng
before_validation :geocode
has_and_belongs_to_many :packages
def full_street_address
address
end
end
`
Thanks in advance for your help!
The selected answer is not accurate. According to documentation here there's a simple way to catch rescue this exception:
begin
complex_operation_that_calls_save!_internally
rescue ActiveRecord::RecordInvalid => invalid
puts invalid.record.errors
end
You can access the messages instance variable of errors and get the field and error message associated.
A couple ideas off the top of my head:
Use #package.save! and a rescue block:
def create
#package = current_user.package.build(params[:package])
package_location
#package.save!
flash[:success] = "Package created!"
redirect_to root_path
rescue
render 'pages/home'
end
Use validates_associated in your Package model, and only save if it's valid:
def create
#package = current_user.package.build(params[:package])
package_location
# You might be able to just use if(#package.save), but I'm not positive.
if(#package.valid?)
#package.save!
flash[:success] = "Package created!"
redirect_to root_path
else
render 'pages/home'
end
end
And I'm sure there are a couple more ways, too, as you're working in Ruby...
Hope that helps!
Here's the code that I used to solve the problem while giving the user good feedback on the why the save failed. Please forgive my inelegant ruby code.
One small problem remains . . . if the package and the location both fail validation, only the location error message is displayed on reload. If the user then corrects the location error but not the package error, he is shown the package error message. I'm working on how to show all of the errors on the first reload
def create
#package= current_user.package.build(params[:package])
if package_location && #package.save
flash[:success] = "Package created!"
redirect_to root_path
else
render 'pages/home'
end
end
def package_location
gps_processing if !session[:gps_aware]
location = #package.locations.build(:address => session[:address])
if !location.valid?
#package.errors.add(:address, "You have entered an invalid address")
return false
else
return true
end
end
def gps_processing
session[:address] = [params[:story][:street_address], params[:story][:city],
params[:story][:state], params[:story][:country]].compact.join(', ')
end

Resources