Question

I would like to add a new category products tab on the category admin page with a product grid same as the default category product grid.

Magento Category admin

The only difference is, i need this grid to save selected products in a custom table with the following fields:

1.id
2.product_id
3.category_id
4.position

This will then allow me to have "featured products" with separate product ordering as shown in the wireframe below:

Screenshot of Subcategory displaying the "featured products". Category ordering

Initial thoughts

  1. Build a custom tab similar to
    Mage_Adminhtml_Block_Catalog_Category_Tab_Product
  2. Create an observer to save category relation on
    catalog_category_save_after
  3. Get product ids with current category id from custom table and mark as selected
Was it helpful?

Solution

Overview


After scratching my head, i finally built a module.

Models
Created a model that extends the Category model to avoid having a custom entity.

Observers

addCategoryEssentialBlock

This observer adds our new products tab and serialized grid in the category management page and listens to the adminhtml_catalog_category_tabs event.

saveCategoryEssentialData

This observer saves the category-essentials product relation on category save and listens to the catalog_category_save_after event.

Code


Block

app/code/local/Lg/Essentials/Block/Adminhtml/Catalog/Category/Tab/Essential.php

class Lg_Essentials_Block_Adminhtml_Catalog_Category_Tab_Essential extends Mage_Adminhtml_Block_Widget_Grid
{

    /**
     * Set grid params
     */
    public function __construct()
    {
        parent::__construct();
        $this->setId('catalog_category_essential');
        $this->setDefaultSort('position');
        $this->setDefaultDir('ASC');
        $this->setUseAjax(true);
    }

    /**
     * Get current category
     * @return Mage_Catalog_Model_Category
     */
    public function getCategory()
    {
        return Mage::registry('current_category');
    }

    /**
     * Add filter
     * @param $column
     * @return $this
     */
    protected function _addColumnFilterToCollection($column)
    {
        if ($column->getId() == 'in_essentials') {
            $essentialIds = $this->_getSelectedEssentials();
            if (empty($essentialIds)) {
                $essentialIds = 0;
            }
            if ($column->getFilter()->getValue()) {
                $this->getCollection()->addFieldToFilter('entity_id', array('in'=>$essentialIds));
            } else {
                if ($essentialIds) {
                    $this->getCollection()->addFieldToFilter('entity_id', array('nin'=>$essentialIds));
                }
            }
        } else {
            parent::_addColumnFilterToCollection($column);
        }
        return $this;
    }

    /**
     * Prepare the collection
     * @return Mage_Adminhtml_Block_Widget_Grid
     * @throws Exception
     */
    protected function _prepareCollection()
    {
        if ($this->getCategory()->getId()) {
            $this->setDefaultFilter(array('in_essentials'=>1));
        }
        $collection = Mage::getModel('catalog/product')->getCollection()
            ->addAttributeToSelect('name')
            ->addAttributeToSelect('sku')
            ->addAttributeToSelect('price')
            ->addStoreFilter($this->getRequest()->getParam('store'))
            ->joinField(
                'position',
                'lg_essentials/category_essential',
                'position',
                'product_id=entity_id',
                'category_id='.(int) $this->getRequest()->getParam('id', 0),
                'left'
            );

        $this->setCollection($collection);

        if ($this->getCategory()->getProductsReadonly()) {
            $essentialIds = $this->_getSelectedEssentials();
            if (empty($essentialIds)) {
                $essentialIds = 0;
            }
            $this->getCollection()->addFieldToFilter('entity_id', array('in'=>$essentialIds));
        }

        return parent::_prepareCollection();
    }

    /**
     * Prepare the columns
     * @return $this
     * @throws Exception
     */
    protected function _prepareColumns()
    {

        if (!$this->getCategory()->getProductsReadonly()) {
            $this->addColumn(
                'in_essentials',
                array(
                    'header_css_class' => 'a-center',
                    'type'      => 'checkbox',
                    'name'      => 'in_essentials',
                    'values'    => $this->_getSelectedEssentials(),
                    'align'     => 'center',
                    'index'     => 'entity_id'
                ));
        }
        $this->addColumn(
            'entity_id',
            array(
                'header' => Mage::helper('lg_essentials')->__('ID'),
                'sortable'  => true,
                'width'     => '60',
                'index'     => 'entity_id'
            )
        );
        $this->addColumn('name', array(
            'header'    => Mage::helper('lg_essentials')->__('Name'),
            'index'     => 'name'
        ));
        $this->addColumn('sku', array(
            'header'    => Mage::helper('lg_essentials')->__('SKU'),
            'width'     => '80',
            'index'     => 'sku'
        ));
        $this->addColumn('price', array(
            'header'    => Mage::helper('lg_essentials')->__('Price'),
            'type'  => 'currency',
            'width'     => '1',
            'currency_code' => (string) Mage::getStoreConfig(Mage_Directory_Model_Currency::XML_PATH_CURRENCY_BASE),
            'index'     => 'price'
        ));
        $this->addColumn(
            'position',
            array(
                'header'         => Mage::helper('lg_essentials')->__('Position'),
                'width'     => '1',
                'type'      => 'number',
                'index'     => 'position',
                'editable'  => !$this->getCategory()->getProductsReadonly()
            )
        );
        return parent::_prepareColumns();
    }

    /**
     * Get grid url
     * @return string
     */
    public function getGridUrl()
    {
        return $this->getUrl(
            'adminhtml/essentials_essential_catalog_category/essentialsgrid',
            array(
                'id'=>$this->getCategory()->getId()
            )
        );
    }

    /**
     * Get list of selected Essential Ids
     * @return array
     */
    protected function _getSelectedEssentials()
    {
        $essential = Mage::getModel('lg_essentials/category');
        $essentials = $this->getCategoryEssentials();
        if (!is_array($essentials)) {
            $essentials = $essential->getEssentialsPosition($this->getCategory());
            return array_keys($essentials);
        }
        return $essentials;
    }

    /**
     * Get list of selected Essential Id & positions
     * @return array
     */
    public function getSelectedEssentials()
    {
        $essentials = array();
        $selected = Mage::getModel('lg_essentials/category')->getEssentialsPosition(Mage::registry('current_category'));
        if (!is_array($selected)) {
            $selected = array();
        }
        foreach ($selected as $essentialId => $position) {
            $essentials[$essentialId] = array('position' => $position);
        }
        return $essentials;
    }
}

app/code/local/Lg/Essentials/Block/Catalog/Category/List/Essential.php

class Lg_Essentials_Block_Catalog_Category_List_Essential extends Mage_Core_Block_Template
{
    /**
     * Get the list of Essentials
     * @return Mage_Catalog_Model_Resource_Product_Collection
     */
    public function getEssentialProductsCollection()
    {
        $category = Mage::registry('current_category');

        $collection = Mage::getModel('catalog/product')->getCollection()
            ->addAttributeToSelect('name')
            ->addAttributeToSelect('sku')
            ->addAttributeToSelect('price')
            ->joinField(
                'position',
                'lg_essentials/category_essential',
                'position',
                'product_id=entity_id',
                'at_position.category_id='.(int) $category->getId(),
                'right'
            );

        $collection->addOrder('position', 'asc');

        return $collection;
    }
}

controllers

app/code/local/Lg/Essentials/controllers/Adminhtml/Essentials/Essential/Catalog/CategoryController.php

require_once ("Mage/Adminhtml/controllers/Catalog/CategoryController.php");

class Lg_Essentials_Adminhtml_Essentials_Essential_Catalog_CategoryController
extends Mage_Adminhtml_Catalog_CategoryController
{
    /**
     * construct
     */
    protected function _construct()
    {
    // Define module dependent translate
    $this->setUsedModuleName('Lg_Essentials');
    }

    /**
     * Essentials grid in category page
     */
    public function essentialsgridAction()
    {
    $this->_initCategory();
    $this->loadLayout();
    $this->getLayout()->getBlock('category.edit.tab.essential')
    ->setCategoryEssentials($this->getRequest()->getPost('category_essentials', null));
    $this->renderLayout();
    }
}

etc

app/code/local/Lg/Essentials/etc/config.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Lg_Essentials>
            <version>0.1.0</version>
        </Lg_Essentials>
    </modules>
    <global>
        <resources>
            <lg_essentials_setup>
                <setup>
                    <module>Lg_Essentials</module>
                    <class>Lg_Essentials_Model_Resource_Setup</class>
                </setup>
            </lg_essentials_setup>
        </resources>
        <blocks>
            <lg_essentials>
                <class>Lg_Essentials_Block</class>
            </lg_essentials>
        </blocks>
        <helpers>
            <lg_essentials>
                <class>Lg_Essentials_Helper</class>
            </lg_essentials>
        </helpers>
        <models>
            <lg_essentials>
                <class>Lg_Essentials_Model</class>
                <resourceModel>lg_essentials_resource</resourceModel>
            </lg_essentials>
            <lg_essentials_resource>
                <class>Lg_Essentials_Model_Resource</class>
                <entities>
                    <category_essential>
                        <table>lg_essentials_category_essential</table>
                    </category_essential>
                </entities>
            </lg_essentials_resource>
        </models>
    </global>
    <adminhtml>
        <layout>
            <updates>
                <lg_essentials>
                    <file>lg/essentials.xml</file>
                </lg_essentials>
            </updates>
        </layout>
        <events>
            <adminhtml_catalog_category_tabs>
                <observers>
                    <lg_essentials_essential_category>
                        <type>singleton</type>
                        <class>lg_essentials/adminhtml_observer</class>
                        <method>addCategoryEssentialBlock</method>
                    </lg_essentials_essential_category>
                </observers>
            </adminhtml_catalog_category_tabs>
            <catalog_category_save_after>
                <observers>
                    <lg_essentials_essential_category>
                        <type>singleton</type>
                        <class>lg_essentials/adminhtml_observer</class>
                        <method>saveCategoryEssentialData</method>
                    </lg_essentials_essential_category>
                </observers>
            </catalog_category_save_after>
        </events>
    </adminhtml>
    <admin>
        <routers>
            <adminhtml>
                <args>
                    <modules>
                        <Lg_Essentials before="Mage_Adminhtml">Lg_Essentials_Adminhtml</Lg_Essentials>
                    </modules>
                </args>
            </adminhtml>
        </routers>
    </admin>
    <frontend>
        <layout>
            <updates>
                <lg_essentials>
                    <file>lg/essentials.xml</file>
                </lg_essentials>
            </updates>
        </layout>
    </frontend>
</config>

Helper

app/code/local/Lg/Essentials/Helper/Data.php

class Lg_Essentials_Helper_Data extends Mage_Core_Helper_Abstract
{
}

Model

app/code/local/Lg/Essentials/Model/Adminhtml/Observer.php

class Lg_Essentials_Model_Adminhtml_Observer
{
    /**
     * Add the Essential tab to categories
     * @param Varien_Event_Observer $observer
     * @return Lg_Essentials_Model_Adminhtml_Observer $this
     */
    public function addCategoryEssentialBlock($observer)
    {
        $tabs = $observer->getEvent()->getTabs();
        $content = $tabs->getLayout()->createBlock(
            'lg_essentials/adminhtml_catalog_category_tab_essential',
            'category.essential.grid'
        )->toHtml();
        $serializer = $tabs->getLayout()->createBlock(
            'adminhtml/widget_grid_serializer',
            'category.essential.grid.serializer'
        );
        $serializer->initSerializerBlock(
            'category.essential.grid',
            'getSelectedEssentials',
            'essentials',
            'category_essentials'
        );
        $serializer->addColumnInputName('position');
        $content .= $serializer->toHtml();
        $tabs->addTab(
            'essential',
            array(
                'label'   => Mage::helper('lg_essentials')->__('Essentials'),
                'content' => $content,
            )
        );
        return $this;
    }

    /**
     * Save category - essential relation
     * @param Varien_Event_Observer $observer
     * @return Lg_Essentials_Model_Adminhtml_Observer $this
     */
    public function saveCategoryEssentialData($observer)
    {
        $post = Mage::app()->getRequest()->getPost('essentials', -1);

        if ($post != '-1') {
            $post = Mage::helper('adminhtml/js')->decodeGridSerializedInput($post);
            $category = Mage::registry('category');
            $category->setPostedEssentials($post);
            $essentialResource = Mage::getResourceModel('lg_essentials/category');
            $essentialResource->saveCategoryEssentials($category);
        }
        return $this;
    }
}

app/code/local/Lg/Essentials/Model/Resource/Category.php

class Lg_Essentials_Model_Resource_Category extends Mage_Catalog_Model_Resource_Category
{
    /**
     * Save category essentials relation
     * @param Mage_Catalog_Model_Category $category
     * @return Mage_Catalog_Model_Resource_Category
     */
    public function saveCategoryEssentials($category)
    {
        $categoryEssentialTable = $this->getTable('lg_essentials/category_essential');

        $category->setIsChangedEssentialList(false);
        $id = $category->getId();
        /**
         * new category-essential relationships
         */
        $products = $category->getPostedEssentials();

        /**
         * Example re-save category
         */
        if ($products === null) {
            return $this;
        }

        /**
         * old category-essential relationships
         */
        $oldProducts = Mage::getModel('lg_essentials/category')->getEssentialsPosition($category);

        $insert = array_diff_key($products, $oldProducts);
        $delete = array_diff_key($oldProducts, $products);

        /**
         * Find product ids which are presented in both arrays
         * and saved before (check $oldProducts array)
         */
        $update = array_intersect_key($products, $oldProducts);

        if ($update) {
            $update = $this->cleanPositions($update);
        }

        $update = array_diff_assoc($update, $oldProducts);

        $adapter = $this->_getWriteAdapter();

        /**
         * Delete essentials from category
         */
        if (!empty($delete)) {
            $cond = array(
                'product_id IN(?)' => array_keys($delete),
                'category_id=?' => $id
            );
            $adapter->delete($categoryEssentialTable, $cond);
        }

        /**
         * Add essentials to category
         */
        if (!empty($insert)) {
            $data = array();
            foreach ($insert as $productId => $position) {
                $data[] = array(
                    'category_id' => (int)$id,
                    'product_id'  => (int)$productId,
                    'position'    => (int)$position['position']
                );
            }
            $adapter->insertMultiple($categoryEssentialTable, $data);
        }

        /**
         * Update essential positions in category
         */
        if (!empty($update)) {
            foreach ($update as $productId => $position) {
                $where = array(
                    'category_id = ?'=> (int)$id,
                    'product_id = ?' => (int)$productId
                );
                $bind  = array('position' => (int)$position);
                $adapter->update($categoryEssentialTable, $bind, $where);
            }
        }

        if (!empty($insert) || !empty($delete)) {
            $productIds = array_unique(array_merge(array_keys($insert), array_keys($delete)));
            Mage::dispatchEvent('catalog_category_change_essentials', array(
                'category'      => $category,
                'product_ids'   => $productIds
            ));
        }

        if (!empty($insert) || !empty($update) || !empty($delete)) {
            $category->setIsChangedEssentialList(true);

            /**
             * Setting affected essentials to category for third party engine index refresh
             */
            $productIds = array_keys($insert + $delete + $update);
            $category->setAffectedEssentialIds($productIds);
        }
        return $this;
    }

    /**
     * Get positions of associated to category essentials
     * @param Mage_Catalog_Model_Category $category
     * @return array
     */
    public function getEssentialsPosition($category)
    {
        $categoryEssentialTable = $this->getTable('lg_essentials/category_essential');

        $select = $this->_getWriteAdapter()->select()
            ->from($categoryEssentialTable, array('product_id', 'position'))
            ->where('category_id = :category_id');
        $bind = array('category_id' => (int)$category->getId());

        return $this->_getWriteAdapter()->fetchPairs($select, $bind);
    }

    /**
     * Get Essential count in category
     * @param Mage_Catalog_Model_Category $category
     * @return int
     */
    public function getEssentialCount($category)
    {
        $categoryEssentialTable = Mage::getSingleton('core/resource')->getTableName('lg_essentials/category_essential');

        $select = $this->getReadConnection()->select()
            ->from(
                array('main_table' => $categoryEssentialTable),
                array(new Zend_Db_Expr('COUNT(main_table.product_id)'))
            )
            ->where('main_table.category_id = :category_id');

        $bind = array('category_id' => (int)$category->getId());
        $counts = $this->getReadConnection()->fetchOne($select, $bind);

        return intval($counts);
    }

    /**
     * Clean Essentials positions
     * @param $products
     * @return array
     */
    protected function cleanPositions($products)
    {
        $cleanPositions = array();

        foreach ($products as $productid => $info) {
            $cleanPositions[$productid] = $info['position'];
        }

        return $cleanPositions;
    }
}

app/code/local/Lg/Essentials/Model/Resource/Setup.php

class Lg_Essentials_Model_Resource_Setup extends Mage_Catalog_Model_Resource_Setup
{
}

app/code/local/Lg/Essentials/Model/Category.php

class Lg_Essentials_Model_Category extends Mage_Catalog_Model_Category
{
    /**
     * Retrieve array of essential id's for category
     * array($productId => $position)
     * @param $category
     * @return array
     */
    public function getEssentialsPosition($category)
    {
        if (!$category->getId()) {
            return array();
        }
        $array = $category->getData('essentials_position');
        if (is_null($array)) {
            $array = Mage::getResourceModel('lg_essentials/category')->getEssentialsPosition($category);
            $category->setData('essentials_position', $array);
        }
        return $array;
    }

    /**
     * Retrieve count essentials of category
     * @param $category
     * @return int
     */
    public function getEssentialCount($category)
    {
        if (!$this->hasEssentialCount()) {
            $count = Mage::getResourceModel('lg_essentials/category')->getEssentialCount($category);
            $this->setData('essential_count', $count);
        }
        return $this->getData('essential_count');
    }
}

sql

app/code/local/Lg/Essentials/sql/lg_essentials_setup/install-0.1.0.php

/* @var $installer Mage_Catalog_Model_Resource_Setup */
$installer = $this;
$installer->startSetup();

/**
 * Create table 'lg_essentials/category_essential'
 */
$table = $installer->getConnection()
    ->newTable($installer->getTable('lg_essentials/category_essential'))
    ->addColumn(
        'category_id',
        Varien_Db_Ddl_Table::TYPE_INTEGER,
        null,
        array(
            'unsigned'  => true,
            'nullable'  => false,
            'primary'   => true,
            'default'   => '0',
        ),
        'Category ID'
    )
    ->addColumn(
        'product_id',
        Varien_Db_Ddl_Table::TYPE_INTEGER,
        null,
        array(
            'unsigned'  => true,
            'nullable'  => false,
            'primary'   => true,
            'default'   => '0',
        ),
        'Product ID'
    )
    ->addColumn(
        'position',
        Varien_Db_Ddl_Table::TYPE_INTEGER,
        null,
        array(
            'nullable'  => false,
            'default'   => '0',
        ),
        'Position'
    )
    ->addIndex(
        $installer->getIdxName(
            'lg_essentials/category_essential',
            array('product_id')
        ),
        array('product_id')
    )
    ->addForeignKey(
        $installer->getFkName(
            'lg_essentials/category_essential',
            'category_id',
            'catalog/category',
            'entity_id'
        ),
        'category_id',
        $installer->getTable('catalog/category'),
        'entity_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE,
        Varien_Db_Ddl_Table::ACTION_CASCADE
    )
    ->addForeignKey(
        $installer->getFkName(
            'lg_essentials/category_essential',
            'product_id',
            'catalog/product',
            'entity_id'
        ),
        'product_id',
        $installer->getTable('catalog/product'),
        'entity_id',
        Varien_Db_Ddl_Table::ACTION_CASCADE,
        Varien_Db_Ddl_Table::ACTION_CASCADE
    )
    ->addIndex(
        $this->getIdxName(
            'lg_essentials/category_essential',
            array('product_id', 'category_id'),
            Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE
        ),
        array('product_id', 'category_id'),
        array('type' => Varien_Db_Adapter_Interface::INDEX_TYPE_UNIQUE)
    )
    ->setComment('Essential To Category Linkage Table');
$installer->getConnection()->createTable($table);
$installer->endSetup();

adminthml

app/design/adminhtml/default/default/layout/lg/essentials.xml

<?xml version="1.0"?>
<layout>
    <adminhtml_essentials_essential_catalog_category_essentialsgrid>
        <block type="core/text_list" name="root" output="toHtml">
            <block type="lg_essentials/adminhtml_catalog_category_tab_essential" name="category.edit.tab.essential"/>
        </block>
    </adminhtml_essentials_essential_catalog_category_essentialsgrid>
</layout>

frontend

app/design/frontend/base/default/layout/lg/essentials.xml

<?xml version="1.0"?>
<layout>
    <lg_essentials_category>
        <reference name="category.products">
            <block type="lg_essentials/catalog_category_list_essential" name="category.info.essentials"
                   as="category_essentials" template="lg/essentials/catalog/category/list/essential.phtml"/>
        </reference>
    </lg_essentials_category>
    <catalog_category_default>
        <update handle="lg_essentials_category" />
    </catalog_category_default>
    <catalog_category_layered>
        <update handle="lg_essentials_category" />
    </catalog_category_layered>
</layout>

app/design/frontend/base/default/template/lg/essentials/catalog/category/list/essential.phtml

<?php $essentials = $this->getEssentialProductsCollection();?>

<?php if ($essentials && $essentials->count() > 0) :?>
    <div class="box-collateral box-essentials box-up-sell">
        <h2>Essentials</h2>
        <?php foreach ($essentials as $_essential) : ?>
            <div class="item">
                <?php echo $_essential->getName();?>
            <br />
            </div>
        <?php endforeach; ?>
    </div>
<?php endif;?>

app/etc/modules/Lg_Essentials.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Lg_Essentials>
            <active>true</active>
            <codePool>local</codePool>
            <depends>
                <Mage_Catalog />
             </depends>
        </Lg_Essentials>
    </modules>
</config>

OTHER TIPS

It's not clear what your custom tab is supposed to look like. Does it look identical to Category Products but just serves a different purpose?

If so, your initial thoughts sounds good. Create a new class for your custom tab. Mage_Adminhtml_Block_Catalog_Category_Tabs::_prepareLayout shows you one way to add a new tab. You'll see that this method fires adminhtml_catalog_category_tabs, which an observer can hook onto to add a another tab.'

To save the custom data, I think it would be good to rewrite the "save category action", so that you can intercept the posted data for your custom table and save them accordingly. I'm not sure how you would retrieve the custom data from catalog_category_save_after if you decide to select different products under your custom tab vs. Category Products tab.

atalog_category_save_after observer will only work if your custom tab and Category Products always have the same products.

Licensed under: CC-BY-SA with attribution
Not affiliated with magento.stackexchange
scroll top