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.
Related
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
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
I am trying to create a unique json data structure, and I have run into a problem that I can't seem to figure out.
In my controller, I am doing:
favorite_ids = Favorites.all.map(&:photo_id)
data = { :albums => PhotoAlbum.all.to_json,
:photos => Photo.all.to_json(:favorite => lambda {|photo| favorite_ids.include?(photo.id)}) }
render :json => data
and in my model:
def as_json(options = {})
{ :name => self.name,
:favorite => options[:favorite].is_a?(Proc) ? options[:favorite].call(self) : options[:favorite] }
end
The problem is, rails encodes the values of 'photos' & 'albums' (in my data hash) as JSON twice, and this breaks everything... The only way I could get this to work is if I call 'as_json' instead of 'to_json':
data = { :albums => PhotoAlbum.all.as_json,
:photos => Photo.all.as_json(:favorite => lambda {|photo| favorite_ids.include?(photo.id)}) }
However, when I do this, my :favorite => lambda option no longer makes it into the model's as_json method.......... So, I either need a way to tell 'render :json' not to encode the values of the hash so I can use 'to_json' on the values myself, or I need a way to get the parameters passed into 'as_json' to actually show up there.......
I hope someone here can help... Thanks!
Ok I gave up... I solved this problem by adding my own array methods to handle performing the operations on collections.
class Array
def to_json_objects(*args)
self.map do |item|
item.respond_to?(:to_json_object) ? item.to_json_object(*args) : item
end
end
end
class Asset < ActiveRecord::Base
def to_json_object(options = {})
{:id => self.id,
:name => self.name,
:is_favorite => options[:favorite].is_a?(Proc) ? options[:favorite].call(self) : !!options[:favorite] }
end
end
class AssetsController < ApplicationController
def index
#favorite_ids = current_user.favorites.map(&:asset_id)
render :json => {:videos => Videos.all.to_json_objects(:favorite => lambda {|v| #favorite_ids.include?(v.id)}),
:photos => Photo.all.to_json_objects(:favorite => lambda {|p| #favorite_ids.include?(p.id)}) }
end
end
I think running this line of code
render :json => {:key => "value"}
is equal to
render :text => {:key => "value"}.to_json
In other words, don't use both to_json and :json.
Orders can have many states. I would like to create named routes for those. I need the state to be passed in to the controller as a param. Here is what I was thinking, but it obviously does not work.
match "order/:state/:id" => "orders#%{state}", as: "%{state}"
So I would like order/address/17 to route to orders#address, with :state and :id being passed in as params. Likewise, order/shipping/17 would route to orders#shipping, again :state and :id would be passed in.
Here is the controller.
class OrdersController < ApplicationController
before_filter :load_order, only: [:address, :shipping, :confirmation, :receipt]
before_filter :validate_state, only: [:address, :shipping, :confirmation, :receipt]
def address
#order.build_billing_address unless #order.billing_address
#order.build_shipping_address unless #order.shipping_address
end
def shipping
#shipping_rates = #order.calculate_shipping_rates
end
def confirmation
end
def receipt
end
private
def load_order
#order = Order.find(params[:id])
end
# Check to see if the user is on the correct action
def validate_state
if params[:state]
unless params[:state] == #order.state
redirect_to eval("#{#order.state}_path(:#{#order.state},#{#order.id})")
return
end
end
end
end
Here is what we ended up going with:
routes.rb
%w(address shipping confirmation receipt).each do |state|
match "order/#{state}/:id", :to => "orders##{state}", :as => state, :state => state
end
orders_controller.rb
def validate_state
if params[:state]
unless params[:state] == #order.state
redirect_to(eval("#{#order.state}_path(#order)"))
return
end
end
end
You aren't going to be able to create dynamic named routes with that sort of syntax, but you're basically just using :state as the :action. If you replace :state with :action and specify the controller manually, it'll work. Obviously, you will have to change your code to look at params[:action] rather than params[:state] (or map that variable in a before_filter), but beyond that it should work fine.
match "order/:action/:id", :controller => "orders"
Be aware that if orders has RESTful resource mappings like create or delete, this route would allow GET requests to them, which would be bad; you may just want to add explicit routes for each action you want to complete. This will let you get params[:state], as well:
%w(address shipping).each do |state|
match "order/#{state}/:id", :to => "orders##{state}", :as => state, :state => state
end
I currently have three methods which I want to collapse into one:
def send_email(contact,email)
end
def make_call(contact, call)
return link_to "Call", new_contact_call_path(:contact => contact, :call => call, :status => 'called')
end
def make_letter(contact, letter)
return link_to "Letter", new_contact_letter_path(:contact => contact, :letter => letter, :status => 'mailed')
end
I want to collapse the three into one so that I can just pass the Model as one of the parameters and it will still correctly create the path_to. I am trying to do this with the following, but stuck:
def do_event(contact, call_or_email_or_letter)
model_name = call_or_email_or_letter.class.name.tableize.singularize
link_to "#{model_name.camelize}", new_contact_#{model_name}_path(contact, call_or_email_or_letter)"
end
Thanks to the answers here, I have tried the following, which gets me closer:
link_to( "#{model_name.camelize}", send("new_contact_#{model_name}_path",
:contact => contact,
:status => "done",
:model_name => model_name) )
But I can't seem to figure out how to past the #{model_name} when it is an :attribute and then send the value of model_name, not as a string, but referring the object.
I got this to work: -- giving points to Kadada because he got me in the right direction :)
def do_event(contact, call_or_email_or_letter)
model_name = call_or_email_or_letter.class.name.tableize.singularize
link_to( "#{model_name.camelize}", send("new_contact_#{model_name}_path",
:contact => contact,
:status => 'done',
:"#{model_name}" => call_or_email_or_letter ) )
end
Try this:
def do_event(contact, call_or_email_or_letter)
model_name = call_or_email_or_letter.class.name.tableize.singularize
link_to( "#{model_name.camelize}", send("new_contact_#{model_name}_path",
contact, call_or_email_or_letter) )
end