Add Second Product Grid in Category Admin for Featured Products in Magento 1.9

category-productscustom-collectiongridgrid-serlizationmagento-1.9

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

Best Answer

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>
Related Topic