(Magento 1.4 & Enterprise 1.9)
A conceptually obvious task of associating simple products with required options to a grouped product proved to be more of a challenge than expected. By default, Magento doesn’t allow this relationship; however, with some simple core overrides, it is possible with the following steps:
1. Create two basic local modules with the following structures:
app/code/local/{module}/Catalog
- Block/Adminhtml/Product/Edit/Tab/Super/Group.php
- Model/Product/Type/Grouped.php
- etc/config.xml
app/code/local/{module}/CatalogInventory
- Model/Mysql4/Indexer/Stock/Grouped.php
- etc/config.xml
2. Edit Group.php by extending Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Super_Group and only overriding the method _prepareCollection(). In the core file, Magento specifies a required option filter; simply remove this and you’re left with:
protected function _prepareCollection()
{
$allowProductTypes = array();
foreach (Mage::getConfig()->getNode('global/catalog/product/type/grouped/allow_product_types')->children() as $type) {
$allowProductTypes[] = $type->getName();
}
$collection = Mage::getModel('catalog/product_link')->useGroupedLinks()
->getProductCollection()
->setProduct($this->_getProduct())
->addAttributeToSelect('*')
->addAttributeToFilter('type_id', $allowProductTypes);
$this->setCollection($collection);
return Mage_Adminhtml_Block_Widget_Grid::_prepareCollection();
}
3. Edit Grouped.php by extending Mage_Catalog_Model_Product_Type_Grouped and overriding its getAssociatedProducts() method. Remove the required option filter, and you’re left with:
public function getAssociatedProducts($product = null)
{
if (!$this->getProduct($product)->hasData($this->_keyAssociatedProducts)) {
$associatedProducts = array();
if (!Mage::app()->getStore()->isAdmin()) {
$this->setSaleableStatus($product);
}
$collection = $this->getAssociatedProductCollection($product)
->addAttributeToSelect('*')
->setPositionOrder()
->addStoreFilter($this->getStoreFilter($product))
->addAttributeToFilter('status', array('in' => $this->getStatusFilters($product)));
foreach ($collection as $product) {
$associatedProducts[] = $product;
}
$this->getProduct($product)->setData($this->_keyAssociatedProducts, $associatedProducts);
}
return $this->getProduct($product)->getData($this->_keyAssociatedProducts);
}
4. The last update for the /Catalog sub-module is to create the config.xml file. This file should look like this:
<?xml version="1.0"?>
<config>
<modules>
<{Module}_Catalog>
<version>0.1.0</version>
</{Module}_Catalog>
</modules>
<global>
<blocks>
<adminhtml>
<rewrite> <catalog_product_edit_tab_super_group>{Module}_Catalog_Block_Adminhtml_Product_Edit_Tab_Super_Group</catalog_product_edit_tab_super_group>
</rewrite>
</adminhtml>
</blocks>
<models>
<catalog>
<rewrite> <product_type_grouped>{Module}_Catalog_Model_Product_Type_Grouped</product_type_grouped>
</rewrite>
</catalog>
</models>
</global>
</config>
5. Now edit Grouped.php under the CatalogInventory sub-module. Extend Mage_CatalogInventory_Model_Mysql4_Indexer_Stock_Grouped and override its _getStockStatusSelect(…) method. Remove the required option clause from the sql query, and you’re code should look as such:
protected function _getStockStatusSelect($entityIds = null, $usePrimaryTable = false)
{
$adapter = $this->_getWriteAdapter();
$idxTable = $usePrimaryTable ? $this->getMainTable() : $this->getIdxTable();
$select = $adapter->select()
->from(array('e' => $this->getTable('catalog/product')), array('entity_id'));
$this->_addWebsiteJoinToSelect($select, true);
$this->_addProductWebsiteJoinToSelect($select, 'cw.website_id', 'e.entity_id');
$select->columns('cw.website_id')
->join(
array('cis' => $this->getTable('cataloginventory/stock')),
'',
array('stock_id'))
->joinLeft(
array('cisi' => $this->getTable('cataloginventory/stock_item')),
'cisi.stock_id = cis.stock_id AND cisi.product_id = e.entity_id',
array())
->joinLeft(
array('l' => $this->getTable('catalog/product_link')),
'e.entity_id = l.product_id AND l.link_type_id=' . Mage_Catalog_Model_Product_Link::LINK_TYPE_GROUPED,
array())
->joinLeft(
array('le' => $this->getTable('catalog/product')),
'le.entity_id = l.linked_product_id',
array())
->joinLeft(
array('i' => $idxTable),
'i.product_id = l.linked_product_id AND cw.website_id = i.website_id AND cis.stock_id = i.stock_id',
array())
->columns(array('qty' => new Zend_Db_Expr('0')))
->where('cw.website_id != 0')
->where('e.type_id = ?', $this->getTypeId())
->group(array('e.entity_id', 'cw.website_id', 'cis.stock_id'));
// add limitation of status
$psExpr = $this->_addAttributeToSelect($select, 'status', 'e.entity_id', 'cs.store_id');
$psCond = $adapter->quoteInto($psExpr . '=?', Mage_Catalog_Model_Product_Status::STATUS_ENABLED);
if ($this->_isManageStock()) {
$statusExpr = new Zend_Db_Expr('IF(cisi.use_config_manage_stock = 0 AND cisi.manage_stock = 0,'
. ' 1, cisi.is_in_stock)');
} else {
$statusExpr = new Zend_Db_Expr('IF(cisi.use_config_manage_stock = 0 AND cisi.manage_stock = 1,'
. 'cisi.is_in_stock, 1)');
}
$stockStatusExpr = new Zend_Db_Expr("LEAST(MAX(IF({$psCond}, i.stock_status, 0))"
. ", {$statusExpr})");
$select->columns(array(
'status' => $stockStatusExpr
));
if (!is_null($entityIds)) {
$select->where('e.entity_id IN(?)', $entityIds);
}
return $select;
}
6. Edit config.xml under CatalogInventory, which should look as such:
<?xml version="1.0"?>
<config>
<modules>
<{Module}_CatalogInventory>
<version>0.1.0</version>
</{Module}_CatalogInventory>
</modules>
<global>
<models>
<cataloginventory_mysql4>
<rewrite>
<indexer_stock_grouped>{Module}_CatalogInventory_Model_Mysql4_Indexer_Stock_Grouped</indexer_stock_grouped>
</rewrite>
</cataloginventory_mysql4>
</models>
</global>
</config>
7. As with any module, you’ll want to define it within your app/etc/modules/{Module}_App.xml file. Simple add:
<{Module}_Catalog>
<active>true</active>
<codePool>local</codePool>
</{Module}_Catalog>
<{Module}_CatalogInventory>
<active>true</active>
<codePool>local</codePool>
</{Module}_CatalogInventory>
That’s all there is to it. Now, from the admin section, you can associate simple products that have required options to a grouped product. Also, with the CatalogInventory modifications, the stock_status will be set appropriately, and the product will be visible from the catalog search and catalog landings pages.
I didn’t dive into the meanings of my changes, but if you’ve done any Magento core overrides in the past, it should be self explanatory.
To see an example, check out http://www.motionsavers.com/catalogsearch/result/?q=Cotterman . As of this posting, each Cotterman ladder is a grouped product with numerous simple product associations, each of which contains required options. Hope this saves someone time…
– Update (1/31/11) –
One piece of this configuration that I accidentally left out is the simple product override. With the steps above, you’ll be able to associate the grouped-to-simple products and you’ll be able to display their options on the grouped page; however, you need to override the simple product type to implement the logic which handles the custom options for the cart. This class is as follows:
class Brim_Groupedsimple_Model_Product_Type_Simple extends Mage_Catalog_Model_Product_Type_Simple
{
/**
* Check custom defined options for product
*
* @param Varien_Object $buyRequest
* @param Mage_Catalog_Model_Product $product
* @return array || string
*/
protected function _prepareOptionsForCart(Varien_Object $buyRequest, $product = null)
{
$newOptions = array();
foreach ($this->getProduct($product)->getProductOptionsCollection() as $_option) {
/* @var $_option Mage_Catalog_Model_Product_Option */
$group = $_option->groupFactory($_option->getType())
->setOption($_option)
->setProduct($this->getProduct($product))
->setRequest($buyRequest);
$superOptions = $buyRequest->getSuperOptions();
if($superOptions && isset($superOptions[$product->getId()])){
$group->validateUserValue($superOptions[$product->getId()]);
}else{
$group->validateUserValue($buyRequest->getOptions());
}
$preparedValue = $group->prepareForCart();
if ($preparedValue !== null) {
$newOptions[$_option->getId()] = $preparedValue;
}
}
return $newOptions;
}
}
– Update (2/10/11) –
In the months since I originally wrote this article, I have taken the code above and put it within its own module. You can download the module here. Unzip the module and place it within your app/code/local/Brim directory (create the Brim folder if its not already there). Then create and add the module xml file for the module (Brim_Groupedsimple).
– Update (6/28/11) –
We have packaged this codebase up into an easy to install Magento extension available in our new Magento Extension Store. The new extension is now called Grouped Options.
{ 97 comments }
Next Comments →
it was very interesting to read.
I want to quote your post in my blog. It can?
And you et an account on Twitter?
Sure, you can quote this information. I’m happy you found it useful. My Twitter is tmillhouse.
Thanks for reading!
Hi bud,
I am going to this is a go right now. I have been having huge trouble with this very problem.
You should post a few front end screenshots of what it looks like after as well
Hi there,
Are you considering releasing further more extensive documentation on this?
Kind Regards
Ryan
Ask and you shall receive
Here is the full source of the custom modules : http://www.brimllc.com/wp-content/uploads/2010/07/app.zip
Be sure to change “Brim” module name to whatever yours is, and then you’ll need to reference this from your main app/etc/{module xml file} .
If you have questions with this, you can always email me at tim@brimllc.com.
Thank you for the guidance, much appreciated.
FYI – Thought you might like to know that your MotionSavers.com website does not work as expected for IE7. When you click on a simple product IE generates an ‘operation aborted’ error (can’t find website). No way for IE7 users to purchase (or configure) a ladder. Works well in Firefox.
Many thanks.
Hmm … that’s odd that you’re seeing issues with IE7. I’m running IE7 in parallels and don’t see the issue. Thanks for the heads up, and I’ll test from a Windows box later tonight.
I would like to exchange links with your site http://www.brimllc.com
Is this possible?
Thank you for your interest in our site; however, at this time, we are currently undergoing some restructuring, so we wont be able to handle any link sharing at this time.
I’m actually trying to do this same thing, so stumbling across this website was a godsend. Unfortunately, however, I can’t find a product on your example site that seems to work as advertised. For instance, this product:
http://www.motionsavers.com/series-2600-ladders.html
The first ladder in the 32″ grouping (2609R3232) definitely has at least one required custom option (Thread Type) but nothing like that shows up on the grouped product page. If I try to add to my cart a quantity of 1 of that product from the grouped product page, it brings me back to that page with an error that says “Please specify the required product(s) options”.
Is your mod not working currently?
Hey Craig, for my requirements on motionsavers, my client only needed simple products with required options to be displayed visually with other simple products without required options; however, if a customer wishes to purchase a simple product with a required option, he needs to go to the simple product page to make the modifications.
Good catch on the ladder page. The intended page flow here should actually redirect to the simple page with that message rather than the grouped page.
Although this is how my requirements were specified, you can still configure options on the grouped page with some additional code. Basically, in the grouped template file, get the options for each simple product and use some code like this:
This code allows you to print out the options inputs and allow the customer to select on the grouped page.
EDIT: the previous link Craig has mentioned has been fixed
By the way, here is an example of how the required simple products are suppose to work on the site: http://www.motionsavers.com/ladders/tilt-and-roll-ladders-9.html . They simply display “Choose Options” if there are required options.
I’ll have to fix the link you found. Thanks
.
Thank you for the guidance, much appreciated
Do you have a back end example or images.
Many thanks.
The source can be downloaded here: http://www.brimllc.com/wp-content/uploads/2010/07/app.zip .
I’d be happy to provide more information on the subject; is there a specific piece that you’d like more clarification on?
Also, as this seems to be a fairly common request, Brim LLC would be more than happy to offer quotes on the work to implement this module.
I’ve been looking for a Magneto solution to support what I’ll call Grouped – Configured products or Grouped Simple Products with Options that appear on one product page. I realize this could be done as one big Configured product but my client would like to merchandise their bedding similar to another site.
http://www.garnethill.com/garnet-hill-signature-flannel-bedding/bedding-home/sheets/flannel-sheets/131926
Ideally every individual sku would be controlled under inventory, but i realize that may not be possible so I’m looking for a solution that has groups of “configurable products”. This approach looked promising but I don’t think my client will want to have 2 pages.
Do you know of any solutions to this. Appears to be a somewhat common Magento request.
Thanks, Jeff
Check out my response to Craig Anderson above. He requested the same example of how to display the options on a single page. Really, this isn’t too difficult since you have access to all of the simple/configurable products from the grouped phtml template. All you need to do is create the necessary html inputs. The example I listed above is using a select input.
Thanks. I’m new to Magento. I understand the concepts but I’m having a difficult time with the syntax. I’m using the grouped.phtml file to loop through the $_associatedProducts as $_item. So in theory I should be able to access each item and get the options assigned to each item then display in a drop down like you have suggested above.
I must be missing something b/c it keeps coming back blank.
Thanks for your help
I sent you an email. If you send me your grouped file, I’ll take a look and make the necessary modifications that will display the options with the necessary html inputs.
hi….
its very helpful…
Thank u so much…..
I’ve gotten my Simple product custom options (select inputs) to display nicely on the associated Grouped product pages. Thanks. But when I set the “Is Required” Custom Options setting to “Yes” in my Simple product, the “grouped-items-table” block on the product detail page returns “No options of this product available” and all HTML beneath it disappears (no custom options select input, no “Add to Cart” button, no product image, etc.).
What must I do to make my custom options function properly as required fields?
Hmm … if everything below it disappears, it sounds like there is some error happening. Check your php error log and see if there’s anything in there. If not, I try to recreate the scenario later to identify the issue.
Hi,
I really hop eyou can help me out.
I have a client who uses configurable products. These configurable products have an attribute set assigned to them called ‘Clothing Options’ with two attributes (dropdowns) in called ‘size’ and ‘colour’.
Now, these configurable products have simple products with the same attribute set assigned to them.
On the product detail page I have a configurable product, to the right I also display related and upsell products.
The client would like the the configurable attribute options (dropdowns) to display with the related and upsell products, as well as an add to cart button. Is this possible? I would really appreciate your thoughts on this please. Thanks.
Link is http://www.simplymr.co.uk.
Gareth, I don’t believe this will be a problem; however, I wont have time to look into the necessary code changes until later this evening. I’m thinking that you could just modify the related/upsell product templates to check for type == configurable, and then pull the dropdowns as necessary … Thinking back, I’ve done something like this similar for another client. I’ll get back to you this evening.
Hi,
THanks a lot for this, I really appreciate it. They want to show the options something similar to a figleaves.com page.
http://www.figleaves.com/uk/product.asp?product=Wonderbra-Gel-Push-Up-Bra&product_id=WA-7925&size=&colour=White
You can get me on my email if you do have time to look at this. I would really appreciate it. I’ll throw you a PayPal donation for a couple of beers for your trouble too.
Thanks again.
Hey, I shot you an email just now. Would you be available for a quick chat tomorrow morning around 11 EST?
I have some code I wrote a while back to manually iterate through a configurable product. My idea would be to check if the product type is configurable in the template you’d like to show the dropdowns, then inject the configurable template to display the options. I can explain further on the phone and provide some more detailed code tomorrow.
In the meantime, here’s a simple idea of what I’m thinking. The pseudo code would like something like:
if product is configurable, then…
$_associatedProducts = $this->getAssociatedProducts()
foreach ($_associatedProducts as $_item){
echo $view->getLayout()->createBlock(‘catalog/product_view_type_configurable’)
->setTemplate(‘location/of/your/custom/template.phtml’)
->setProduct($product)
->toHtml();
}
I can deliver some example code for what your template should look like after we speak. There are a couple helper methods you’ll want to implement, but nothing too tricky. Anyway, let me know when you’re available, and I’ll be happy to help…
Hi Man,
Thank you so much, though I didn’t receive an email sorry. Can you resend. Thanks.
Gareth
I’m working from a secure site today and don’t have access to email. I can have the code ready by this afternoon, but I wont be able to shoot it over to you until I get home a little later.
Gareth,
Alright, I have the code working and its tested locally. The only thing is that I can’t email it until I get home later this evening. I’ll be sending you a stripped-down module I wrote a while back to support your requested functionality. The files I’ll be sending you are:
- Brim.zip (this contains the Groupedconfigured module): you’ll unzip this and place this under your app/code/local folder
- groupedconfigured.zip (this contains the necessary template): you’ll unzip this and place it under your app/design/frontend/././template directory
- Brim_Groupedconfigured.xml (you’ll place this in your app/etc/module directory)
Once you have these files, it will be as easy as checking the product type ID in the recently viewed products template, and then call a helper method to inject the configured template. This will allow you to “add to cart” as well.
I’ll send the files, some code snippets, and a screenshot around 5 EST.
If you’d like to sync up on this before then, give me a contact number, and I’ll give you a call in a while.
Thanks,
Tim
Hi Tim,
Thank you so much mate, you’ve gone over and above and I really do appreciate it.
I’ve sent a contact message through this site with my telephone number on. Thanks again mate.
Kind Regards
Gareth
Hey Gareth, I didn’t get the email earlier. I did; however, just now send you the code and instructions. Take a look and let me know if you have any questions.
Hi Tim,
I don’t know what it is but I’m not receiving nay of your emails mate, and I’ve checked my spam folder too? Any ideas why dude? Thanks.
Gareth
Odd, I see it in my ‘send’ folder. I’ll attempt again, and if you don’t recieve it this time, let me know, and I’ll just upload the files to the site.
Following up on a post from 7 weeks ago.
- – - – - – - – - – - – - – - – - – - – - – - – - – - – - – - – - –
I’ve gotten my Simple product custom options (select inputs) to display nicely on the associated Grouped product pages. Thanks.
But when I set the “Is Required” Custom Options setting to “Yes” in my Simple product, the “grouped-items-table” block on the product detail page returns “No options of this product available” and all HTML beneath it disappears…
- – - – - – - – - – - – - – - – - – - – - – - – - – - – - – - – - –
At that time you suggested there must be an error associated with this. Quite so. Here is that error message…
Fatal error: Call to a member function getProductOptionsCollection() on a non-object in /var/www/vhosts/casamias.net/httpdocs/app/design/frontend/casamias01/defaultstyle/template/catalog/product/view/type/grouped.phtml on line 88
And this the code at line 88 in grouped.phtml
- – - – - – - – - – - – - – - – - – - – - – - – - – - – - – - – - –
getProductOptionsCollection();
foreach($options as $option){
$select = $this->getLayout()->createBlock(‘core/html_select’);
$select->setClass(‘product-custom-option’);
$select->setName(‘super_options['.$_item->getId().']['.$option->getId().']‘);
$select->addOption(”, ‘– Select ‘.$option->getTitle().’ –’);
foreach ($option->getValues() as $value) {
$select->addOption($value->getOptionTypeId(), $value->getTitle());
}
echo ‘‘.$option->getTitle().’:‘;
echo $select->getHtml();
}
?>
- – - – - – - – - – - – - – - – - – - – - – - – - – - – - – - – - –
What must I do to make my custom options function properly as required fields?
One more time. Here’s that PHP code found at line 88 in grouped.phtml…
<?php
$options = $_item->getProductOptionsCollection();
foreach($options as $option){
$select = $this->getLayout()->createBlock(‘core/html_select’);
$select->setClass(‘product-custom-option’);
$select->setName(‘super_options['.$_item->getId().']['.$option->getId().']‘);
$select->addOption(”, ‘– Select ‘.$option->getTitle().’ –’);
foreach ($option->getValues() as $value) {
$select->addOption($value->getOptionTypeId(), $value->getTitle());
}
echo ‘<p><b>’.$option->getTitle().’:</b></p>’;
echo $select->getHtml();
}
?>
From the sounds of the error, it looks like you don’t have a variable named $_item. Is your product variable called something else? If you could email me your grouped.phtml file to tim@brimllc.com, I should have some time later to take a better look at it.
Has anyone tried this code change on version 1.3? Will it work for previous versions?
@Jonathan, I haven’t implemented the solution mentioned above in any 1.3 installs.
After e-mailing Tim my problem file (grouped.phtml), he wrote back:
…I see your issue. Your code that is pulling the options is currently located outside of the for loop. At line 57, you’re iterating over all the associated products (this is where the scope of your $_item is ), but your code that is pulling the options is at the bottom of the file. If you put your code within the for loop, it should work as expected.
Let me know if you have any questions.
——————————————————————–
Thanks for the advice. I follow what you’re saying about variable scope. I’ve relocated that block of code which generates the select input for my product options inside the foreach loop — <?php foreach ($_associatedProducts as $_item): ?>
When I change my product’s Custom Options “Is Required” setting to yes, the product detail page now displays in its entirety now, but, as before, no product data appears (“No options of this product available”).
Moreover, though, even when “Is Required” set to no, I’ve never gotten a selected product option to appear anywhere (shopping cart, customer receipt, back-end order detail page).
I seem to have a pretty common issue which has brought me here and thought for sure this would work… My situation is…
I’d like to show the custom options for associated simple products on the grouped.phtml page.
I was able to use the code above
getProductOptionsCollection();
foreach($options as $option){
$select = $this->getLayout()->createBlock('core/html_select');
$select->setClass('product-custom-option');
$select->setName('super_options['.$_item->getId().']['.$option->getId().']');
$select->addOption('', '– Select '.$option->getTitle().' –');
foreach ($option->getValues() as $value) {
$select->addOption($value->getOptionTypeId(), $value->getTitle());
}
echo $select->getHtml();
}
?>
to render drop downs for products which had custom options.
However when I choose a custom option and add the product to my cart, the custom options do not seem to show in the cart, order or invoice. Its as if nothing was selected and its just showing the options rather then executing an action on the selection.
any advice?
Now that I look back through the posted code, it does seems I forgot to include some observer code that attaches the options correctly to the cart. I’ll post the update to the article this evening. Email me (tim@brimllc.com) so that I can send it to your directly.
Kevin, I’ve added the required code at the bottom of the article. You’ll want to define the override in your config.xml and you can name the class to be consistent with the module you’re implementing this under. Let me know if you have any questions.
Has anyone tried this code change on version 1.9? Will it work for this versions? I think this works well on version 1.4…
Please advise..
Regards
Khalid
Hi Khalid,
Yes, the above code has been implemented in Enterprise 1.9.
Thank You Tim,
This article really helped me alot. Everything is working fine on my local Windows system but when I upload this module to Linux hosting. I get following error
Call to a member function setConfig() on a non-object in /home/leather/public_html/includes/src/Mage_Catalog_Model_Product_Type.php
I have uploaded Brim_Groupedsimple.xml in app/etc/modules and it contains following code.
true
local
Can you please help me out?
Best Regards,
Farrukh
Tim,
I figured it out. There was a “src” folder in “includes” folder. I am a Magento beginner so I do not know that how that src folder got created (possibally with some optimization extension, which I did not installed locally) but this issue has been solved.
Thanks again for your great work.
Farrukh
Hi Farrukh,
I’m glad you figured out the issue. That’s odd the ‘src’ folder go created, but your assumption may be correct in that some module on the linux system created it.
If you have any other questions, let me know!
Thanks,
Tim
Has anyone considered making thus into a extension. I would buy it immediately!
All I need is a grouped product where customer can write his order nr.
Thanks for letting me know in the future when extension is ready.
Robert
Hi Robert, thanks for the interest in our work. We are definitely in the process of turning this into an installable module. We’ll let you know when we get it ready.
I’ve installed the module by unzipping the app.zip and then uploading that folder to my store directory and I am still not able to pick up associated products with custom options. Any clue what I missed?
– EDIT — (I fixed the typos in my xml below. Sorry for the confusion everyone!)
Hi Ashley,
The easiest way to get this to work is to use the latest code that I updated at the bottom of the article. You can download it here: http://www.brimllc.com/wp-content/uploads/2011/02/Groupedsimple.zip . Basically the code I included in the most recent module is the same code as described in the article above, but I bundled it all into a single module instead of breaking it out into separate. Once unzipped, place all the contents your app/code/local directory. Then, as stated in step #7, you’ll create a Brim_Groupedsimple.xml file and put it in etc/modules directory. It will look something like this:
<?xml version=”1.0″?>
<config>
<modules>
<Brim_Groupedsimple>
<active>true</active>
<codePool>local</codePool>
</Brim_Groupedsimple>
</modules>
</config>
If you still can’t get it to work, let me know, and I’ll respond to you via your email.
Good luck!
Next Comments →
Comments on this entry are closed.
{ 2 trackbacks }