Edited to include SSCE
I have a Proc object which I add to a Hash with other Procs:
ten_percent_discount_over_sixty = Proc.new {
cart.each { |item| cart_total += item.price }
if cart_total >= 60.00
cart_total =- cart_total * 0.1
end
}
As you can see there is a cart array which contains items. I can't however get my cart instance variable into the scope of this proc. So later when I iterate through all of these rules in another method, I get undefined variable errors.
The point of doing this is that I have various promotional rules (functions) that need to be run on this cart object. It's possible there's a better way to store rules that can be applied iteratively to a objects instance variable.
I have checkout object which contains a cart (an array) of item objects; items have the attributes: code, price and name.
Next I have a Promotion object which holds a hash of promotional rules. This hash contains Proc objects that hold these rules (which are functions that each run on the cart object changing the final price of the cart). Rules maybe added and removed by calling a method and with the name as an argument.
The problem I am having is when I iterate through the hash of promotional rules. I don't know how to get the cart object into the scope of those Proc objects in order to run the functions contained in those Proc objects, changing the cart state.
Do I pass in the cart object when creating the Proc/lambda?
Create a cart instance variable in the Promotion object and set/get it?
Somehow pass in the cart variable within the checkout object?
I'm over-thinking or approaching the problem in an incorrect manner?
The full code is quite long so I'll post a gist below:
https://gist.github.com/3163127
Your rule procs must accept cart as an argument, act on it, and return modified cart.
cart = {total: 100}
rules = {ten_percent_discount: lambda{|c| c[:total] *= 0.9; c },
five_percent_discount: lambda{|c| c[:total] *= 0.95; c}}
rules.each do |name, rule|
cart = rule.call cart.dup
end
cart # => {:total=>85.5}
Related
I'm building a site with users in all 50 states. We need to display information for each user that is specific to their situation, e.g., the number of events they completed in that state. Each state's view (a partial) displays state-specific information and, therefore, relies upon state-specific calculations in a state-specific model. We'd like to do something similar to this:
##{user.state} = #{user.state.capitalize}.new(current_user)
in the users_controller instead of
#illinois = Illinois.new(current_user) if (#user.state == 'illinois')
.... [and the remaining 49 states]
#wisconsin = Wisconsin.new(current_user) if (#user.state == 'wisconsin')
to trigger the Illinois.rb model and, in turn, drive the view defined in the users_controller by
def user_state_view
#user = current_user
#events = Event.all
#illinois = Illinois.new(current_user) if (#user.state == 'illinois')
end
I'm struggling to find a better way to do this / refactor it. Thanks!
I would avoid dynamically defining instance variables if you can help it. It can be done with instance_variable_set but it's unnecessary. There's no reason you need to define the variable as #illinois instead of just #user_state or something like that. Here is one way to do it.
First make a static list of states:
def states
%{wisconsin arkansas new_york etc}
end
then make a dictionary which maps those states to their classes:
def state_classes
states.reduce({}) do |memo, state|
memo[state] = state.camelize.constantize
memo
end
end
# = { 'illinois' => Illinois, 'wisconsin' => Wisconsin, 'new_york' => NewYork, etc }
It's important that you hard-code a list of state identifiers somewhere, because it's not a good practice to pass arbitrary values to contantize.
Then instantiating the correct class is a breeze:
#user_state = state_classes[#user.state].new(current_user)
there are definitely other ways to do this (for example, it could be added on the model layer instead)
I've currently got a system that involves quite a lot of new class instances, so I've had to assign them using an array, as was suggested here: Create and initialize instances of a class with sequential names
However, I'll have to be constantly adding new instances whenever a new one appears, without overwriting existing ones. Might some validation and a modified version of my existing code be the best option?
This is my code, currently every time it runs, the existing data is overwritten. I want the status to be overwritten if it's changed, but I also want to be able to store one or two variables in there permanently.
E2A: Ignore the global variables, they're just there for testing.
$allids = []
$position = 0 ## Set position for each iteration
$ids.each do |x| ## For each ID, do
$allids = ($ids.length).times.collect { MyClass.new(x)} ## For each ID, make a new class instance, as part of an array
$browser.goto("http://www.foo.com/#{x}") ## Visit next details page
thestatus = Nokogiri::HTML.parse($browser.html).at_xpath("html/body/div[2]/div[3]/div[2]/div[3]/b/text()").to_s ## Grab the ID's status
theamount = Nokogiri::HTML.parse($browser.html).at_xpath("html/body/div[2]/div[3]/div[2]/p[1]/b[2]/text()").to_s ## Grab a number attached to the ID
$allids[$position].getdetails(thestatus, theamount) ## Passes the status to getdetails
$position += 1 ## increment position for next iteration
end
E2A2: Gonna paste this from my comment:
Hmm, I was just thinking, I started off by making the previous values dump into another variable, then another variable grabs the new values, and iterates over them to see if any match the previous values. That's quite a messy way to do it though, I was thinking, would self.create with a ||= work? – Joe 7 mins ago
If I understand you correctly, you need to store status and amount for each ID, right? If so, then something like this would help you:
# I'll store nested hash with class instance, status and amount for each id in processed_ids var
$processed_ids = {}
$ids.each do |id|
processed_ids[id] ||= {} #
processed_ids[id][:instance] ||= MyClass.new(id)
processed_ids[id][:status] = get_status # Nokogiri method
processed_ids[id][:amount] = get_amount # Nokogiri method
end
What does this code do: it only once creates instance of your class for each id, but always updates its status and amount.
I have a User model. I also have a form_for(#user...) form. This form spans 3 partials. In order for every partial to remember values I use the following command inside my create action in my UsersController:
session[:user_params].deep_merge!(params[:user]) if params[:user]
This way every partial adds params[:user] to session[:user_params]. I also have other form values stored inside the params hash which are not part of the User model. Is there a command which would allow me to add all single params values (not just the :user hash) to the session[:user_params] hash without adding every single value one by one like this:
session[:num_children] = params[:num_children] if params[:num_children]
...etc...
Try:
params.each {|key,value| session.deep_merge!(key=>value)}
I haven't been able to find where does the output of getPaymentHtml() comes from.
Its defined as:
public function getPaymentHtml() {
return $this->getChildHtml('payment_info');
}
I couldn't find out the template for payment_info block.
Basically I want to be able to retrieve credit card type and credit card number in the progress block of checkout.
How do I find out the method names? Something like $this->getCreditCardType()
Edit: OK! I understand that Magento figures out the payment method first which has their corresponding templates which are used to render output. But in progress.phtml of checkout, var_dump( $this instanceof Mage_Payment_Block_Info_Cc ); returns false, so how do I access that in current context?
The Progress block doen't have it's own template for Payment info. Mage_Checkout_Block_Onepage_Payment_Info block uses the selected Payment Method block to output html. Look at the Mage_Checkout_Block_Onepage_Payment_Info::_toHtml() method:
protected function _toHtml()
{
$html = '';
if ($block = $this->getChild($this->_getInfoBlockName())) {
$html = $block->toHtml();
}
return $html;
}
To find the actual template and block for the specific Payment method you use, you need to perform next steps:
First - get model alias for current payment method Mage::getStoreConfig('payment/'.$yourMethod.'/model') and instantiate it using Mage::getModel(alias)
then get block type using $model->getInfoBlockType() - so you'll be able to find the actual Block by it's type
For example for ccSave payment method the info block is Mage_Payment_Block_Info_Ccsave, and template for it is app\design\frontend\base\default\template\payment\info\default.phtml. You'll be able to find all data inside those.
Good luck ;)
For the sake of completeness, exact functions to fetch CC type and last 4 digits of CC number are:
echo Mage::getSingleton('checkout/session')->getQuote()->getPayment()->getCcType();
echo Mage::getSingleton('checkout/session')->getQuote()->getPayment()->getCcLast4();
The block class is declared in layout update XML; see the onepage checkout and multishipping directives from checkout.xml. The actual child block which is used depends on the payment model which is being used, but there is a common template that will be used unless overridden.
Example:
See the generic CC method model Mage_Payment_ModelMethod_Cc
From that see its info block Mage_Payent_Block_Info_Cc...
...which will lead you to the "base" payment info block Mage_Payment_Block_Info which sets a default template.
If I programmaticly add a item to a shopping cart (setting its custom options) then add another instance of the same item to the cart (with its custom options set to different values), "view cart" lists each item instance on a separate line (good). However, if when adding the items, I programmaticly set one item's special price (via SetSpecialPrice), both item prices change to that special price.
How do I limit the effects of SetSpecialPrice to only the item instance I call that method on?
Thank you,
Ben
To the code you are adding the same 'item' to the quote. This could probably be considered a bug.
You may have to go lower level. What method are you using to add the items to the cart? You may need to emulate what that method does yourself (violating DRY principles) to force it to create a new 'item.'
... going to look in the code now.
Ok, looking at Mage/Sales/Model/Quote.php line 935: public function getItemsByProduct - this is where it determines if the product you are adding already exists. It calls $item->representProduct, which is in Mage/Sales/Model/Quote/Item.php line 301: public function representProduct
If you override this class in your module/code and replace this method you should be able to add simple code that detects if there is a difference in special price and react accordingly.
Code snipet:
$specialPrice = $product->getSpecialPrice();
$thisSpecialPrice = $itemProduct->getSpecialPrice();
if((is_null($specialPrice) xor is_null($thisSpecialPrice))||
(!is_null($specialPrice) && !is_null($thisSpecialPrice && $specialPrice!=$thisSpecialPrice))){
return false;
}