Magento – Products showing in the wrong category

I came across something odd a while back, and until recently not had time to investigate the issue.

Products were showing up in categories that they were not explicitly in.

In summary, when you reindex catalog_category_product Magento goes and looks at all the products and the categories that they are in. For the categories that the product are in, it gets the category tree of that category.

So consider this category hierarchy:

  1. Category A
    1. Sub Category A 1
      1. Bottom Category 1
      2. Bottom Category 2
    2. Sub Category A 2
      1. Bottom Category 3
      2. Bottom Category 4
    3. Sub Category A 3
      1. Bottom Category 5
      2. Bottom Category 6
  2. Category B
    1. Sub Category B 1
      1. Bottom Category 7
      2. Bottom Category 8
    2. Sub Category B 2
      1. Bottom Category 9
      2. Bottom Category 10
    3. Sub Category B 3
      1. Bottom Category 11
      2. Bottom Category 12

Now you create a product and put it in Bottom Category 9 and Bottom Category 11. When you reindex it’s put into the list to show up in the category and the parents on that tree, so Category B and Category B 2, and so on for Bottom Category 11.

Now over time we add more products and more categories, and several months later we decide to rearrange our category tree, as we’ve gotten too wide and our needs have changed, or some other reason. Say to something like this, a strikethru indicates a disabled category (as in is_active is 0 (it’s a boolean integer))

  1. Category A
    1. Sub Category A 1
      1. Bottom Category 1
      2. Bottom Category 2
      3. Bottom Category 3
      4. Bottom Category 4
    2. Sub Category A 2
    3. Sub Category A 3
      1. Bottom Category 5
      2. Bottom Category 6
  2. Category B
    1. Sub Category B 1
      1. Bottom Category 7
      2. Bottom Category 8
    2. Sub Category B 2
      1. Bottom Category 9
      2. Bottom Category 10
    3. Sub Category B 3
      1. Bottom Category 11
      2. Bottom Category 12

Now of course we reindex, and the problems start.

The Problem

We have a problem, the Product correctly shows in Bottom Category 11 and it’s parents, but it is still showing up in Sub Category A 1 and it’s parents in error.

It seems that the reindexer takes categories that are disabled (as in is_active is false), and their tree, into account when building the list of category pages that a product can be displayed on.

So in order to fix this I went deep into Magento core to go and see which query was at fault. Turns out that it’s not exactly broken, it’s just Magento doesn’t check for it a give category is active or not when it builds the list.

The query at fault is:

INSERT INTO `catalog_category_anc_products_index_idx` (`category_id`, `product_id`, `position`) SELECT STRAIGHT_JOIN DISTINCT `ca`.`category_id`, `cp`.`product_id`, MIN(IF(ca.category_id = ce.entity_id, `cp`.`position`, (`ce`.`position` + 1) * (`ce`.`level` + 1 * 10000) + `cp`.`position`)) AS `position` FROM `catalog_category_anc_categs_index_idx` AS `ca`
 INNER JOIN `catalog_category_entity` AS `ce` ON `ce`.`path` LIKE `ca`.`path` OR ce.entity_id = ca.category_id
 INNER JOIN `catalog_category_product` AS `cp` ON cp.category_id = ce.entity_id
 INNER JOIN `catalog_category_product_index_enbl_idx` AS `pv` ON pv.product_id = cp.product_id GROUP BY `ca`.`category_id`,
	`cp`.`product_id`

As you can see it just collects from catalog_category_entity, which has a table structure of:

mysql> describe catalog_category_entity;
+------------------+----------------------+------+-----+---------+----------------+
| Field            | Type                 | Null | Key | Default | Extra          |
+------------------+----------------------+------+-----+---------+----------------+
| entity_id        | int(10) unsigned     | NO   | PRI | NULL    | auto_increment |
| entity_type_id   | smallint(5) unsigned | NO   |     | 0       |                |
| attribute_set_id | smallint(5) unsigned | NO   |     | 0       |                |
| parent_id        | int(10) unsigned     | NO   |     | 0       |                |
| created_at       | timestamp            | YES  |     | NULL    |                |
| updated_at       | timestamp            | YES  |     | NULL    |                |
| path             | varchar(255)         | NO   | MUL | NULL    |                |
| position         | int(11)              | NO   |     | 0       |                |
| level            | int(11)              | NO   | MUL | 0       |                |
| children_count   | int(11)              | NO   |     | 0       |                |
+------------------+----------------------+------+-----+---------+----------------+
10 rows in set (0.00 sec)

As you can see that table structure is somewhat limited. Since this is the bare bones basics of a category. And thus the query to collect categories of a product is in, considers inactive categories as if doesn’t have this information.

The Solution

The fix here is simple. Find where the query is defined and take into account the is_active category attribute, and we’ll do it the Magento way. Rather than hardcoding entity_id’s.

A quick note about how I found the query. I opened up public_html/lib/Varien/Db/Adapter/Pdo/Mysql.php and changed the variable $_debug from false to true, it’s around about line 103. This will output all the SQL queries to public_html/var/debug/pdo_mysql.log.

After that I just grep’ed the source catalog_category_anc_products_index_idx which told me where the table was defined in a xml config, and then grep’ed again for the internal reference name, (there was much much grep’ing for bit’s and pieces).

The query runs under the reindexAll function of Mage_Catalog_Model_Resource_Category_Indexer_Product. So basically take the ENTIRE contents of this function and setup a Model Resource override, and fix the query.

Module Basics

Creating our Module in the directory local/Company/Catalog

Normally I wouldn’t cover this, but we are overriding a resource which is a little different from just overriding a model.

etc/config.xml
<?xml version="1.0"?>
<config>
    <modules>
        <Company_Catalog>
            <version>0.0.1</version>
        </Company_Catalog>
    </modules>

    <global>
        <models>
            <companycatalog>
                <class>Company_Catalog_Model</class>
            </companycatalog>

            <catalog_resource_eav_mysql4>
                <rewrite>
                    <category_indexer_product>Company_Catalog_Model_Resource_Category_Indexer_Product</category_indexer_product>
                </rewrite>
            </catalog_resource_eav_mysql4>
        </models>
    </global>
</config>
Model/Resource/Category/Indexer/Product.php

Literally just a copy of the contents of reindexAll with some modifications. Starting at // Query Correction and ending at // end Query Correction.

<?php

class Company_Catalog_Model_Resource_Category_Indexer_Product extends Mage_Catalog_Model_Resource_Category_Indexer_Product
{
    /**
     * Rebuild all index data
     *
     * @return Mage_Catalog_Model_Resource_Category_Indexer_Product
     */
    public function reindexAll()
    {
        $this->useIdxTable(true);
        $this->beginTransaction();
        try {
            $this->clearTemporaryIndexTable();
            $idxTable = $this->getIdxTable();
            $idxAdapter = $this->_getIndexAdapter();
            $stores = $this->_getStoresInfo();
            /**
             * Build index for each store
             */
            foreach ($stores as $storeData) {
                $storeId    = $storeData['store_id'];
                $websiteId  = $storeData['website_id'];
                $rootPath   = $storeData['root_path'];
                $rootId     = $storeData['root_id'];
                /**
                 * Prepare visibility for all enabled store products
                 */
                $enabledTable = $this->_prepareEnabledProductsVisibility($websiteId, $storeId);
                /**
                 * Select information about anchor categories
                 */
                $anchorTable = $this->_prepareAnchorCategories($storeId, $rootPath);
                /**
                 * Add relations between not anchor categories and products
                 */
                $select = $idxAdapter->select();
                /** @var $select Varien_Db_Select */
                $select->from(
                    array('cp' => $this->_categoryProductTable),
                    array('category_id', 'product_id', 'position', 'is_parent' => new Zend_Db_Expr('1'),
                        'store_id' => new Zend_Db_Expr($storeId))
                )
                ->joinInner(array('pv' => $enabledTable), 'pv.product_id=cp.product_id', array('visibility'))
                ->joinLeft(array('ac' => $anchorTable), 'ac.category_id=cp.category_id', array())
                ->where('ac.category_id IS NULL');

                $query = $select->insertFromSelect(
                    $idxTable,
                    array('category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'),
                    false
                );
                $idxAdapter->query($query);

                /**
                 * Assign products not associated to any category to root category in index
                 */

                $select = $idxAdapter->select();
                $select->from(
                    array('pv' => $enabledTable),
                    array(new Zend_Db_Expr($rootId), 'product_id', new Zend_Db_Expr('0'), new Zend_Db_Expr('1'),
                        new Zend_Db_Expr($storeId), 'visibility')
                )
                ->joinLeft(array('cp' => $this->_categoryProductTable), 'pv.product_id=cp.product_id', array())
                ->where('cp.product_id IS NULL');

                $query = $select->insertFromSelect(
                    $idxTable,
                    array('category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'),
                    false
                );
                $idxAdapter->query($query);

                /**
                 * Prepare anchor categories products
                 */
                $anchorProductsTable = $this->_getAnchorCategoriesProductsTemporaryTable();
                $idxAdapter->delete($anchorProductsTable);

                $position = 'MIN('.
                    $idxAdapter->getCheckSql(
                        'ca.category_id = ce.entity_id',
                        $idxAdapter->quoteIdentifier('cp.position'),
                        '('.$idxAdapter->quoteIdentifier('ce.position').' + 1) * '
                        .'('.$idxAdapter->quoteIdentifier('ce.level').' + 1 * 10000)'
                        .' + '.$idxAdapter->quoteIdentifier('cp.position')
                    )
                .')';

                // Query Correction

                // load the is_active category attribute
                $attributeModel = Mage::getModel('eav/entity_attribute')->loadByCode(Mage_Catalog_Model_Category::ENTITY, 'is_active');
                $attributeTable = 'catalog_category_entity_' . $attributeModel->getBackendType();

                $select = $idxAdapter->select()
                ->useStraightJoin(true)
                ->distinct(true)
                ->from(array('ca' => $anchorTable), array('category_id'))
                ->joinInner(
                    array('ce' => $this->_categoryTable),
                    $idxAdapter->quoteIdentifier('ce.path') . ' LIKE ' .
                    $idxAdapter->quoteIdentifier('ca.path') . ' OR ce.entity_id = ca.category_id',
                    array()
                )

                ->joinInner(
                    array('cp' => $this->_categoryProductTable),
                    'cp.category_id = ce.entity_id',
                    array('product_id')
                )

                // left join the attribute
                ->joinInner(
                    array('ccei' => $attributeTable),
                    'ccei.entity_id = cp.category_id',
                    array()//select nothing
                )

                ->joinInner(
                    array('pv' => $enabledTable),
                    'pv.product_id = cp.product_id',
                    array('position' => $position)
                )

                ->where('ccei.attribute_id=' . $attributeModel->getAttributeId())//is_active
                ->where('ccei.value=1')

                ->group(array('ca.category_id', 'cp.product_id'));
                $query = $select->insertFromSelect($anchorProductsTable,
                    array('category_id', 'product_id', 'position'), false);
                $idxAdapter->query($query);

                // end Query Correction

                /**
                 * Add anchor categories products to index
                 */
                $select = $idxAdapter->select()
                ->from(
                    array('ap' => $anchorProductsTable),
                    array('category_id', 'product_id',
                        'position', // => new Zend_Db_Expr('MIN('. $idxAdapter->quoteIdentifier('ap.position').')'),
                        'is_parent' => $idxAdapter->getCheckSql('cp.product_id > 0', 1, 0),
                        'store_id' => new Zend_Db_Expr($storeId))
                )
                ->joinLeft(
                    array('cp' => $this->_categoryProductTable),
                    'cp.category_id=ap.category_id AND cp.product_id=ap.product_id',
                    array()
                )
                ->joinInner(array('pv' => $enabledTable), 'pv.product_id = ap.product_id', array('visibility'));

                $query = $select->insertFromSelect(
                    $idxTable,
                    array('category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'),
                    false
                );
                $idxAdapter->query($query);

                $select = $idxAdapter->select()
                    ->from(array('e' => $this->getTable('catalog/product')), null)
                    ->join(
                        array('ei' => $enabledTable),
                        'ei.product_id = e.entity_id',
                        array())
                    ->joinLeft(
                        array('i' => $idxTable),
                        'i.product_id = e.entity_id AND i.category_id = :category_id AND i.store_id = :store_id',
                        array())
                    ->where('i.product_id IS NULL')
                    ->columns(array(
                        'category_id'   => new Zend_Db_Expr($rootId),
                        'product_id'    => 'e.entity_id',
                        'position'      => new Zend_Db_Expr('0'),
                        'is_parent'     => new Zend_Db_Expr('1'),
                        'store_id'      => new Zend_Db_Expr($storeId),
                        'visibility'    => 'ei.visibility'
                    ));

                $query = $select->insertFromSelect(
                    $idxTable,
                    array('category_id', 'product_id', 'position', 'is_parent', 'store_id', 'visibility'),
                    false
                );

                $idxAdapter->query($query, array('store_id' => $storeId, 'category_id' => $rootId));
            }

            $this->syncData();

            /**
             * Clean up temporary tables
             */
            $this->clearTemporaryIndexTable();
            $idxAdapter->delete($enabledTable);
            $idxAdapter->delete($anchorTable);
            $idxAdapter->delete($anchorProductsTable);
            $this->commit();
        } catch (Exception $e) {
            $this->rollBack();
            throw $e;
        }
        return $this;
    }
}

The fix in depth

The first thing done here is to Magento Safe load the Category Attribute details for is_active.

                // load the is_active category attribute
                $attributeModel = Mage::getModel('eav/entity_attribute')->loadByCode(Mage_Catalog_Model_Category::ENTITY, 'is_active');
                $attributeTable = 'catalog_category_entity_' . $attributeModel->getBackendType();

After this join,

                ->joinInner(
                    array('cp' => $this->_categoryProductTable),
                    'cp.category_id = ce.entity_id',
                    array('product_id')
                )

we need to add another join, in this case we are joining to the table that contains the is_active information, it’s usually catalog_category_entity_int but we Magento Safe loaded it above.

                // left join the attribute
                ->joinInner(
                    array('ccei' => $attributeTable),
                    'ccei.entity_id = cp.category_id',
                    array()//select nothing
                )

Finally we need to adjust to add a where to clean up the join and select active categories only, after the last join and before the group function call:

                ->where('ccei.attribute_id=' . $attributeModel->getAttributeId())//is_active
                ->where('ccei.value=1')

This changes the query to:

INSERT INTO `catalog_category_anc_products_index_idx` (`category_id`, `product_id`, `position`) SELECT STRAIGHT_JOIN DISTINCT `ca`.`category_id`, `cp`.`product_id`, MIN(IF(ca.category_id = ce.entity_id, `cp`.`position`, (`ce`.`position` + 1) * (`ce`.`level` + 1 * 10000) + `cp`.`position`)) AS `position` FROM `catalog_category_anc_categs_index_idx` AS `ca`
 INNER JOIN `catalog_category_entity` AS `ce` ON `ce`.`path` LIKE `ca`.`path` OR ce.entity_id = ca.category_id
 INNER JOIN `catalog_category_product` AS `cp` ON cp.category_id = ce.entity_id
 INNER JOIN `catalog_category_entity_int` AS `ccei` ON ccei.entity_id = cp.category_id
 INNER JOIN `catalog_category_product_index_enbl_idx` AS `pv` ON pv.product_id = cp.product_id WHERE (ccei.attribute_id=119) AND (ccei.value=1) GROUP BY `ca`.`category_id`,
	`cp`.`product_id`

And Finally

Now with this module in place (remember to activate it in etc/modules) clear your cache and reindex. And hey presto, the product is no longer showing up in a category that is a parent of a category that the product is in that is now disabled. (Bit of a mouthful that)

In Summary

Much nicer, and not too much of a performance hit really.

Overall it means your products only show in the right active categories.

Generally it is only a issue when you disable a category and don’t delete it and then rearrange things leaving remnants of the old category tree floating about, so basically anything a common user would do.

I don’t tend to delete stuff myself, in case things get reactivated, and I’ll reuse the records.

Hopefully this is of help, rather than deleting records!

Magento – The missing view type!

Magento has four options when it comes to display products on your e-commerce website:

  1. Not Visible Individually
  2. Catalog
  3. Search
  4. Catalog, Search

Now, normally that about covers most use cases. But if you have configurable products it is somewhat more common place to set the children to Not Visible Individually.

This is great and all, but what if you are using Google Shopping real-time attribute/schema scraping which requires the Child to be visible individually to read those tags (and can be handy for direct to a page when SKU searching on the front end), and you don’t want the children to show in either your category or search grid pages?

This is where an additional catalog visibility type comes in useful, and it is actually very straight forward to achieve!

First setup a basic Magento module/plugin, which I won’t bore you with the complete details of, but we are binding to a model of the Mage Catalog Module under a rewrite!

Set up your

local/Company/Catalog/etc/config.xml

as follows:

<?xml version="1.0"?>
<config>
    <modules>
        <Company_Catalog>
            <version>0.0.1</version>
        </Company_Catalog>
    </modules>
    <global>
        <models>
            <catalog>
                <rewrite>
                    <product_visibility>Company_Catalog_Model_Product_Visibility</product_visibility>
               </rewrite>
            </catalog>
        </models>
    </global>
</config>

Fairly straight forward here, setup a basic module and override Mage/Catalog/Product/Visibility.

Next setup the Model

local/Company/Catalog/Model/Product/Visibility.php
class Company_Catalog_Model_Product_Visibility extends Mage_Catalog_Model_Product_Visibility {
    const VISIBILITY_PAGE = 5;
    
    public function getVisibleInSiteIds()
    {
        return array(self::VISIBILITY_IN_SEARCH, self::VISIBILITY_IN_CATALOG, self::VISIBILITY_BOTH, self::VISIBILITY_PAGE);
    }
    
    static public function getOptionArray()
    {
        return array(
            self::VISIBILITY_NOT_VISIBLE=> Mage::helper('catalog')->__('Not Visible Individually'),
            self::VISIBILITY_IN_CATALOG => Mage::helper('catalog')->__('Catalog'),
            self::VISIBILITY_IN_SEARCH  => Mage::helper('catalog')->__('Search'),
            self::VISIBILITY_BOTH       => Mage::helper('catalog')->__('Catalog, Search'),
            self::VISIBILITY_PAGE       => Mage::helper('catalog')->__('Own Page Only')
        );
    }
    
    /**
     * Retrieve all options
     *
     * @return array
     */
    static public function getAllOption()
    {
        $options = self::getOptionArray();
        array_unshift($options, array('value'=>'', 'label'=>''));
        return $options;
    }
    
    /**
     * Retireve all options
     *
     * @return array
     */
    static public function getAllOptions()
    {
        $res = array();
        $res[] = array('value'=>'', 'label'=> Mage::helper('catalog')->__('-- Please Select --'));
        foreach (self::getOptionArray() as $index => $value) {
            $res[] = array(
               'value' => $index,
               'label' => $value
            );
        }
        return $res;
    }
    
    /**
     * Retrieve option text
     *
     * @param int $optionId
     * @return string
     */
    static public function getOptionText($optionId)
    {
        $options = self::getOptionArray();
        return isset($options[$optionId]) ? $options[$optionId] : null;
    }
}

First of all we add a new Constant, logically named

VISIBILITY_PAGE

and we give it the next free “id” of 5. (Check core/Mage/Catalog/Model/Product/Visibility.php for the base list).

Next the important function

getVisibleInSiteIds

tells Magento which Products to allow to be shown individually, so we just add our constant to the array here.

All the other functions deal with rendering the new Visibility type in the admin interface for editing products (as otherwise the parent functions are used and they can’t “see” the new visibility type).

As per usual when you add/edit a new module make sure to clear the cache accordingly!

That is it all done, short and sweet!

Fun with WP.me and Twitter Cards

I was trying to figure out why my Twitter Cards weren’t working when I was tinkering with someone else’s WordPress install.

Turns out at some point Twitter decided to follow the WP.me redirect and grab the page behind is differently.

So, now you need to whitelist your domain rather than WP.me.

To do that pop along to The Card Validator with one of your none WP.me Blog Post URL’s in hand and pop it in the validator box.

twitter_card_validation

Click request approval, fill in the little form and wack submit! It might throw an error but retest your page and all should be good!

pending

Give it a few minutes:

waiting

And Finally:

finally

A working Tweet!

Updated like so:

Barry_Carlyon_on_Twitter___Fun_with_WP_me_and_Twitter_Cards_http___t_co_WYcrgwtMgG_

Writing PHP Code to Standard

So with the release of AWS SDK Version 2, I’ve been inspired a little to write better PHP Code, at least in stuff I have on my GitHub. The new SDK is written to match the PHP Framework Interop Group PSR-0, PSR-1 and PSR-2, as well as Composer/Packagist Support.

I been working a on project to make starting a new project easier, (create a repo, setup the deployments, Apache VHost etc). Primarily working off of the ATech Media Suite of products.

Starting from a base of peterjaap’s Codebase PHP Wrapper, where I was just adding missing API endpoints to the class, at which point the new AWS SDK was released and inspiration of a sort struck.

I started a new project from scratch, to implement all the API’s that Atech runs, it’s very much in progress, I’ve currently got full support for DeployHQ and PointHQ end points, (as well as having gotten a few obvious missing end points added to the API).

I’m clunking my way thru the CodeBase API, it’s taking some time as its got a truck load more end points, that DeployHQ and PointHQ put together. It has Projects, which have repos, which have tickets, just to scratch the surface.

At the moment it’s largely functional, it’s just missing some extras, the Time Tracking, File Uploads, and so on.
It’s very much a learning experience in an attempt to follow the coding standards as well as laying the code out properly.

At the moment the CodeBase class itself is pretty large, and I’m pondering about splitting it out to sub classes. Classes to represent Projects and Repos and Tickets and so on, but its the first thing I’ve tried to build in such a way, so much consideration is needed, to hopefully get it right. I keep looking at other Project like the AWS SDK and GoCardless’ PHP Class.

The only thing thats really missing, is Unit Testing, which I still need to sit down and learn properly, but I’m still trying to find a suitable entry point to teach myself, and of late been expanding my knowledge using Code Academy, more on that in another post!

So thats a short update on PHP Development in general, you can checkout the ATech API Class so far on GitHub. Feedback always welcome!

(I’m trying to write more on my blog, as well as more PHP in general, hence a in progress post instead of a “It’s completely finished give it a whirl” post)

Using Zombaio with WordPress, A WordPress Plugin

Zombaio Logo

Building on my work at Your Members, specifically the Adult Payment Gateway Zombaio, I’ve finally gotten around to building a better “recommended WordPress Plugin”.
As the one on Zombaio’s site is little more than a nasty hack sitting in front of WordPress without using any WordPress functions or its Database Abstraction Layer, (it’s using Raw Database functions and is not very WordPress’y at all).

It has now been released (current version at time of writing is V1.0.4) and made available via WordPress extend called WP-Zombaio.

What does the Plugin do?

Logs

Essentially the plugin listens for ping backs for the Payment Gateway and responds accordingly, creating or deleting the user, using the details passed back from Zombaio, and logging the full result for later reference.

Installing the plugin first guides you thru a easy to use install procedure, meaning no fiddly file editing, or trying to setup and understand ZScript.

Just download, unzip, upload and activate.
Nip thru the (optional) four step quick start wizard and away, you, go!

It’s a Wizard Harry
Settings all Done

In addition to the quick setup and user create/delete, the plugin provides a few short codes and widgets, to direct the user from your site off to the Zombaio Join Form and back again.

ShortCodes and Widgets!

Guide

Included in the plugin is a guide, which describes how to setup and use all the features as well as general information about running a Membership Site and getting the most out of the plugin. Especially the Zombaio Seal, which can be a little fiddly to get the code for.

Getting the Seal Code

Security and Access Control

On top of the basic functionality, is included a method to block non members from accessing the site and providing a Splash Page/adult content warning page, meaning no need for additional plugins required to do this, saving you overhead. All guests are redirected to this page, so content is protected!

A Basic Landing Page

As well as this, included in the plugin is the ability to create a menu to show to logged out users that is different to logged in users, in case your theme doesn’t provide this.

Menus

To top it all, the plugin is designed to run your WordPress site with “Anyone can Register” disabled, meaning the only way users can join is via the Zombaio Payment Form, and this means users/members have paid for access.

What is Coming Next?

I’m currently working on a update, which includes credit purchase, and the spending of credits on access to posts/pages, (this can be configured to be timed access, for Live Broadcasts/Webcam Events, or permanent access, like a image gallery), again helpful instructions and information is included in the guide on how to setup and use these features.

(The update is currently in beta testing to a couple of users who have contacted me about the plugin, but you can always grab the SVN trunk/current build and give the current credits implementation a go! You can grab this from Extend, no guarantee that it is stable or safe to use in production/live, you can get it from the WP Zombaio Developers Tab)

Suddenly Graphs

In addition I’m adding graphs and reporting data, to supplement the Zombaio Graphs and reporting to help Admins set how their site is doing. And showing what users are spending their credits on, and how many unspent credits are in the user kitty!

During the development of the next version, to fully support credit purchase in a admin easy to setup and use method, further development time is needed to create a little form builder, as there are several options, which can be difficult to easily describe using just WordPress short code Arguments, hence this blog post is talking about the current release and not about the next release including credits!).

So I shall finish this blog post off quickly and get back to finishing off the next version!

Can I see more Screenshots?

Sure, just pop over to the WordPress Extend page, there are a few more there!

Where can I get it

You can check out the Plugin here on the site, or over on Extend.

I need Help!

If you have any suggestions, feature requests or need help setting up and using the plugin, please do not hesitate to drop me a line, and I’ll be able to give you a hand and get you up and running!

I’d like to Donate

Cool! I put a quick an easy PayPal button on the local WP Zombaio Page for you!