Modules (Adding custom)

From Snapp CMS Developer Documentation

Jump to: navigation, search

This page describes the steps to creating a custom SnappCMS module.

We will demonstrate each step in creating an example module named "Sexy" - a mixed HTML content/gallery style module which allows a page to have basic HTML content added and multiple images attached to it.

Let's begin!

Contents

Create new cms_modules table entry

In order for your new module to be usable, it needs to be added to the cms_modulesdatabase table.

INSERT INTO cms_modules VALUES (
0,                                                              -- Auto-increment value
'Sexy Module',                                                  -- User-friendly module name            
'My cool new sexy module',                                      -- Description
'sexy',                                                         -- Module name
1,                                                              -- 1 = active, 0 = hidden
1,                                                              -- The position the module will be displayed in the "Add new page" dialog list
1,                                                              -- 1 = managed by Website Manager, 0 = displayed in top level tab.  Any other value = displayed under that module's tab
1,                                                              -- 1 = This is a custom module (all non-core modules should have this set)
'view:View Module,add:Add,edit:Edit Sexy,delete:Delete Sexy' -- Possible admin permissions for this module
)

Add user permissions

  • Log into the administration system at http://[website-name]/cms and switch to the "Administration" tab.
  • Select "CMS Modules" and verify that "Sexy Module" shows up under "Website Manager" and that "Active" is checked. If it isn't, recheck your SQL above.
  • Select "CMS Users" and for each user you wish to have access to the module:
 * Double click on the user
 * Click the "User Access" tab
 * Locate "Sexy Module" and check all permissions (you'll notice these correspond to the last column of the DB row you created above)
 * Click "Save"
  • If one of those users was the one you're currently logged in as, you'll need to log out and back in to have access changes take effect.

Create the Front-end MVC structure

You'll end up creating the following files:

/custom/moduleSexy.php The model class
/modules/sexy/index.php The controller script
/modules/sexy/templates/default/template.info The view template metadata
/modules/sexy/templates/default/index.php The view template HTML file

Front-end Module model class

The front-end module model class is accessed by end-users of the site and doesn't contain any code related to the admin module. You would add all of the code to implement the module's data manipulation functionality here. For this demo, we'll just add a couple of methods to demonstrate accessing the DB and returning some module properties.

Create the front-end module for Sexy in custom/moduleSexy.php, with the following code:

class moduleSexy {
    protected $module_id = NULL;

    protected $cms = NULL;

    public function __construct($module_id) {
        global $cms;
        $this->module_id = $module_id;
        $this->cms = $cms;
    }

    public function getModuleId() {
        return $this->module_id;
    }

    public function getTitle() {
       $result = $this->cms->db->getObject("SELECT title FROM _custom_sexy WHERE id = $this->module_id");
       return $result->title;
    }
}

Controller script

Add a module controller file modules/sexy/index.php, with the following content:

<?
/*************************************************************************************************
	Sexy Module

	Author		: Joe Bloggs
	Version		: 1.0
	Copyright	: My company 2010

**************************************************************************************************/

// Check if this file is being included and NOT accessed directly

	if (!isset($cms)) {header('Location: /error/invalid_access.html');}
	if (!empty($cms->page->module_id)) {
		$sexy = new moduleSexy($cms->page->module_id);
	}
	include($cms->getModulePath().'index.php');

View Templates

Metadata

Add the module view file for the default template - modules/sexy/templates/default/index.php

Add a file named modules/sexy/templates/default/template.info and give it the following content:

title="Default"
description="Default Sexy Module template"
author="Joe Bloggs"
version="1.0"

Template HTML File

Here's a basic template file:

<h1>Hello</h1>
This is the Default template view file for <b>Sexy</b><br/>
The current module ID is <?php echo $sexy->getModuleId(); ?><br/>
The module title is <?php echo $sexy->getTitle(); ?>

Create the Admin Module

Each administration module is comprised of a Javascript file and a corresponding PHP file. This section describes how to set up the basics with each file:

Javascript File

The Admin Javascript file handles the layout of the control panel for your module.

The admin system interface is written using the ExtJS framework (see http://www.sencha.com/deploy/dev/docs/ for the API).

The naming convention for control panel Javascript files is:

cms.admin.modules.[parent_module_name].modules.[your_module_name].js

In this case, create the file: cms.admin.modules.website.modules.sexy.js

It will define the class: cms.admin.modules.website.modules.sexy

Initial Bootstrapping

In order to bootstrap the admin control panel, the class must have the property module defined as the name of the module you created in the cms_modules table. It must also have a property sitemapID prepared but initialized to 0.

The class must also have the method init() defined. This method is called when the admin system is first started.

A basic bootstrap can look as follows:

cms.admin.modules.website.modules.sexy = {     // A subclass of website.modules
    module : 'sexy',                       // Make sure this matches class name and cms_modules row module_id
    sitemapID : 0,

    /**
         * Module constructor
         */
    init : function () {
    // Any essential construction code goes here.  Nothing required by default

    }
};

"Add New Page" Wizard handling

Next, the "Add new page" wizard needs instructions on what to do when the user chooses to add a new "sexy" module. This is done by defining the object cms.admin.modules.website.modules.sexy.wizard with an addPagePanel() method.

Here's some boiler plate code to get the module up and running. It allows users to choose between linking their new page to an existing Sexy module instance that may have been created earlier, or creating a new one from scratch.

This can be appended to the bottom of cms.admin.modules.website.modules.sexy.js:

/**
 * This object defines the wizard step that displays at step 4, immediately
 * after the "Select template" page in the setup wizard.
 *
 */
cms.admin.modules.website.modules.sexy.wizard = {
    addPagePanel :function () {
        return new Ext.form.FormPanel({
            bodyStyle:'padding:5px;',
            id:'card-3',
            items : [
            {
                html:'<h2><b>Step 4:</b> Sexy Method</h2>\n\
                                                <div class="instructions">You have the option to either create a new sexy page or select a existing one</div>'
            },
            {
                xtype:'checkbox',
                fieldLabel:'New ',
                name:'newPage',
                labelStyle:'text-align:right;font-weight:bold;',
                value:1,
                boxLabel: 'Automatically Create New Sexy page'

            },
            {
                html:'OR',
                bodyStyle:'text-align:center;font-weight:bold;padding:20px;'
            },
            {
                xtype:'combo',
                fieldLabel:'Existing ',
                valueField: 'id',
                displayField:'title',
                hiddenName:'module_id',
                allowBlank:false,
                typeAhead:false,
                triggerAction:'all',
                editable:false,
                singleSelect:true,
                width:300,
                labelStyle:'text-align:right;font-weight:bold;',
                msgTarget:'side',
                allowBlankText:'Please Select an existing Sexy page',
                mode:'remote',
                store: new Ext.data.JsonStore({
                    url:'load.php',
                    autoLoad:true,
                    root:'data',
                    baseParams:{
                        module:'adminSexy',
                        action:'loadLookupCombo'
                    },
                    fields:[{
                        name:'id'
                    },{
                        name:'title'
                    }]
                })
            }
            ]
        })
    }
}

Back-end PHP class

You'll notice that the addPagePanel() method above makes calls to load.php, calling adminSexy::loadLookupCombo() when generating a list of existing Sexy pages to link the new Sexy sitemap item to. The adminSexy class will be filled with all of the backend code needed to drive the administrative functions of the Sexy module. Now would be a good time to create this admin class with this method.

Create the file custom/adminSexy.php with the following code:

class adminSexy {

    protected $cms = NULL;

    public function __construct() {
        global $cms;
        $this->cms = $cms;
    }

    /**
     * Look up existing Sexy pages for the Add New Page wizard
     * @param array $data
     */
    public function loadLookupCombo($data) {
        $output = array();
        $sql = "SELECT id,title FROM _custom_sexy ORDER BY title ASC";

        $data = $this->cms->db->getObjects($sql);
        if (!empty($data)) {
            $output['success'] = true;
            $output['data'] = $data;
        } else {
            $output['success'] = false;
            $output['msg'] = 'No Sexy Thangs Found';
        }
        $this->cms->jsonOutput($output);
    }
}

Notice that this function doesn't actually return anything. Instead, it directly outputs a JSON-encoded array at the end of processing.

This is the way that all admin methods transmit data back to the ExtJS front-end. The array structure is as follows:

  • It must always contain an element with the key "success", which is a boolean.
  • If operation was successful, the array must contain an element "data" with the data to be returned.
  • If the operation was unsuccessful, the array must contain an element with the key "msg" explaining the nature of the failure. This element can also optionally be returned on a successful operation if required by the ExtJS front-end.

Create Database Tables

By convention, all database tables are named _custom_[modulename]_*. A master table for identifying instances of pages for a certain module is named _custom_[modulename]

For our Sexy module, create the following main table:

CREATE TABLE `_custom_sexy` (
  `id` int(11) NOT NULL auto_increment,
  `title` varchar(50) NOT NULL,
  `content` blob NOT NULL,
  `thumbsize` varchar(20) NOT NULL,
  `largesize` varchar(20) NOT NULL,
  `settings` varchar(100) NOT NULL,
  `last_updated` timestamp NOT NULL default '0000-00-00 00:00:00' on update CURRENT_TIMESTAMP,
  `created_by` int(11) NOT NULL,
  `last_updated_by` int(11) NOT NULL,
  `date_created` timestamp NOT NULL default '0000-00-00 00:00:00',
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=5 DEFAULT CHARSET=utf8

While we're here, let's create the image assets table for the module:

CREATE TABLE `_custom_sexy_images` (
  `id` int(11) NOT NULL auto_increment,
  `title` varchar(50) NOT NULL,
  `file` varchar(50) NOT NULL,
  `description` blob NOT NULL,
  `url` varchar(255) NOT NULL,
  `sort_order` int(11) NOT NULL,
  `sexy_id` int(11) NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `sexy_id` (`sexy_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8

Create Editor panel

In the steps above, we created our new module and set up the "Add Page" wizard functionality for it. Now we need to create the main editor panel, where the user can set the content to be displayed in their Sexy page.

For our purposes (Sexy combines HTML content with a gallery list), we'll create a tab that contains an HTML editor and an asset list in which we can add images.

Javascript file

Define the interface by creating the method cms.admin.modules.website.modules.sexy.load().

This method lays out the UI, sets up listeners and callbacks. It can get complex and long winded, so it may be appropriate to break it down into smaller components. For the sake of simplicity and cut and pastability however, we'll just lay it out here in one big monolithic function. By checking it against the ExtJS API reference, it should be reasonably obvious what's going on.

Append the following to the end of cms.admin.modules.website.modules.sexy.js


/**
 * This is the method that sets up edit panel for a sexy page
 */
cms.admin.modules.website.modules.sexy.load = function (cfg) {
    var pageTitle = 'New Sexy Thang';     // This is the default title for a new Sexy page
    var moduleTitle = 'Sexy Stuff';       // This is the that goes in each Sexy page tab to distinguish it against other Website Manager-level tabs
    var id = 0;
    var module = cms.admin.user.modules.website.subModules[this.module];
    for (var key in cfg) {
        switch (key) {
            case 'title':
                pageTitle = cfg[key];
                break;
            case 'id' :
                id = cfg[key];
                break;
            case 'module' :
                module = cfg[key];
                break;
            case 'sitemap_id':
                this.sitemapID = cfg[key];
                break;
        }
    }
    if (cms.admin.modules.website.pagesPanel.getComponent('website-' + this.module + '-' + id)) {
        // If the tab has already been opened, bring it to the front
        cms.admin.modules.website.pagesPanel.getComponent('website-' + this.module + '-' + id).show();

    } else {
        // Otherwise, build a new tab


        // Build the title field and HTML editor field to take up the majority RHS (East) of the new Sexy tab

         var form = new Ext.form.FormPanel({
            bodyStyle: 'padding:5px;',
            defaultType: 'textfield',
            border:false,
            lauout:'fit',
            region:'center',
            items : [
            {
                xtype:'textfield',
                inputType:'hidden',
                name: 'id'
            },
            {
                itemCls:'hide-label',
                xtype:'textfield',
                name: 'title',
                blankText: 'Please Enter Page Title',
                msgTarget:'side',
                anchor:'98%',
                allowBlank:false
            },
            {
                xtype:'htmleditor',
                id:'website-' + this.module + '-' + id + '-editor',
                itemCls:'hide-label',
                enableFontSize:false,
                enableFont:false,
                enableLinks:false,
                enableColors : false,
                name:'content',
                anchor:'98% -28'
            }
            ]
        });


        // Build the assets (images) list panel, to go on the LHS (west) side

        var tree = new Ext.tree.TreePanel({
            title:'Sexy Assets',
            region:'west',
            cls:'galleryImagesTree',
            width:120,
            maxWidth:120,
            id:'website-'+this.module+'-imageTree-'+id,
            layout:'fit',
            autoScroll:true,
            enableDD:true,
            border:true,
            margins:'5 0 5 5',
            rootVisible:false,
            containerScroll:true,
            loader: new Ext.tree.TreeLoader({
                dataUrl:'load.php',
                baseParams : {
                    module:'adminSexy',
                    action:'loadImagesTree',       // Need to define this method in adminSexy
                    id:id
                }
            }),
            root : new Ext.tree.AsyncTreeNode({
                draggable:false,
                expanded:true,
                leaf:false,
                id:'imagesRoot'
            }),
            listeners : {
                beforenodedrop : function (e) {
                    Ext.MessageBox.progress('Please wait...', 'Saving New Image Order');
                    cms.temp = e;
                    var event = e;
                    Ext.Ajax.request({
                        method:'POST',
                        url: 'load.php',
                        disableCaching:true,
                        params: {
                            module:'adminSexy',
                            action:'changeImageOrder',  // Need to define this method in adminSexy
                            id:id,
                            position:e.point,
                            target:e.target.id,
                            node:e.dropNode.id
                        },
                        success: function (obj,success) {
                            var response  = Ext.decode(obj.responseText);
                            if (response.success == true) {
                                Ext.MessageBox.hide();
                            } else cms.admin.errors.display(response.msg);
                        },
                        failure: function (obj,success) {
                            cms.admin.errors.process(obj);
                        }
                    });
                },
                dblclick : function (node,e) {
                    var imageForm = new Ext.form.FormPanel({
                        labelWidth:70,
                        bodyStyle:'padding:5px;',
                        border:false,
                        items:[
                        {
                            xtype:'numberfield',
                            inputType:'hidden',
                            name:'id'
                        },
                        {
                            xtype:'textfield',
                            fieldLabel: 'Image Title',
                            name:'title',
                            maxLength:100,
                            anchor:'98%'
                        },
                        {
                            xtype:'textfield',
                            fieldLabel: 'Link URL',
                            name:'url',
                            maxLength:255,
                            anchor:'98%'
                        },
                        {
                            xtype:'htmleditor',
                            name:'description',
                            maxLength:255,
                            itemCls:'hide-label',
                            width: 375,
                            height:200,
                            enableFontSize:false,
                            enableFont:false,
                            enableLinks:false,
                            enableColors : false
                        }
                        ]
                    });
                    var window =  new Ext.Window({
                        title:'Sexy Image : ' + node.text,
                        iconCls:'iconPreview',
                        closable:true,
                        modal:true,
                        width:400,
                        height:350,
                        resizable:false,
                        id: 'optionsWindow',
                        layout:'fit',
                        activeTab:0,
                        items:[
                        imageForm
                        ],
                        buttons: [
                        {
                            text:'Save',
                            disabled: (cms.admin.user.userFullAccess('website,sexy','edit',cms.admin.modules.website.modules.sexy.sitemapID,'edit')==true)? false:true,
                            handler: function () {
                                imageForm.getForm().submit({
                                    url: 'load.php',
                                    method:'POST',
                                    disableCaching:true,
                                    params: {
                                        module:'adminSexy',
                                        action:'saveImage',     // Need to define this method in adminSexy
                                        id:id
                                    },
                                    success: function (form,action) {
                                        var response = Ext.decode(action.response.responseText);
                                        if (response.success == true) {
                                            window.close();
                                            tree.root.reload();

                                        } else cms.admin.errors.display(response.msg);
                                    },
                                    failure: function (form,action) {
                                        var response = Ext.decode(action.response.responseText);
                                        if (response.success != true) {
                                            cms.admin.errors.display(response.msg);
                                        } else cms.admin.errors.display(action.response.responseText);
                                        delete response;
                                    },
                                    waitMsg: 'Saving.....'
                                });
                            },
                            scope:this

                        },
                        {
                            text:'Cancel',
                            handler: function () {
                                window.close();
                            },
                            scope:this
                        }

                        ]
                    });
                    imageForm.load({
                        url:'load.php',
                        method:'POST',
                        disableCaching:true,
                        params:{
                            module:'adminSexy',
                            action:'loadImage',     // Need to define this method in adminSexy
                            id:node.id
                            },
                        waitMsg: 'Loading.....'
                    });
                    window.show();

                }
            },
            tbar:[
            {
                iconCls:'iconAdd',
                text:'Add Image',
                tooltip:'Add Image this Sexy page',
                disabled: (cms.admin.user.userFullAccess('website,sexy','edit',this.sitemapID,'edit')==true)? false:true,
                handler: function () {
                    cms.admin.modules.media.chooser(form,this.addImage);
                },
                scope:this
            },
            '->',
            '-',
            {
                iconCls:'iconDelete',
                tooltip:'Delete Selected Image',
                disabled: (cms.admin.user.userFullAccess('website,sexy','edit',this.sitemapID,'edit')==true)? false:true,
                handler: function () {
                    var node = tree.getSelectionModel().getSelectedNode();
                    if (node) {
                        Ext.Ajax.request({
                            url:'load.php',
                            params:{
                                module:'adminSexy',
                                action:'deleteImage',     // Need to define this method in adminSexy
                                id:node.id
                                },
                            disableCaching: false,
                            success: function (o) {
                                node.remove();
                            },
                            failure: cms.admin.errors.process
                        });
                    } else {
                        Ext.Msg.alert('Warning','Please select a image to delete first');
                    }
                },
                scope:this
            }

            ]
        });


        // Build the main tab for the Sexy page

        cms.admin.modules.website.pagesPanel.add({
            layout:'border',
            border:false,
            title: moduleTitle + ' - ' + pageTitle,
            id: 'website-' + this.module + '-' + id,
            closable:true,
            region:'center',

            // Build the toolbar - this could probably be simplified somewhat

            tbar : [
            {
                text:'Save',
                iconCls: 'iconSave',
                tooltip: 'Saves Changes',
                disabled: (cms.admin.user.userFullAccess('website,sexy','edit',this.sitemapID,'edit')==true)? false:true,
                handler: function () {
                    form.getForm().submit({
                        url: 'load.php',
                        method:'POST',
                        disableCaching:true,
                        params: {
                            module:'adminSexy',
                            action:'save',     // Need to define this method in adminSexy
                            id:id
                        },
                        failure: function (form,action) {
                            var response = Ext.decode(action.response.responseText);
                            if (response.success != true) {
                                Ext.MessageBox.show({
                                    title:'Error',
                                    msg:response.msg,
                                    icon:Ext.MessageBox.ERROR,
                                    buttons: Ext.MessageBox.OK
                                });
                            } else Ext.MessageBox.alert('Error',action.response.responseText);
                            delete response;
                        },
                        waitMsg: 'Saving.....'
                    });
                },
                scope:this
            },
            '-',
            {
                text:'Save & Close',
                tooltip: 'Saves Changes',
                disabled: (cms.admin.user.userFullAccess('website,sexy','edit',this.sitemapID,'edit')==true)? false:true,
                handler: function () {
                    form.getForm().submit({
                        url: 'load.php',
                        method:'POST',
                        disableCaching:true,
                        params: {
                            module:'adminSexy',
                            action:'save',     // Need to define this method in adminSexy
                            id:id
                        },
                        failure: function (form,action) {
                            var response = Ext.decode(action.response.responseText);
                            if (response.success != true) {
                                Ext.MessageBox.show({
                                    title:'Error',
                                    msg:response.msg,
                                    icon:Ext.MessageBox.ERROR,
                                    buttons: Ext.MessageBox.OK
                                });
                            } else Ext.MessageBox.alert('Error',action.response.responseText);
                            delete response;
                        },
                        success : function (form,action) {
                            var response = Ext.decode(action.response.responseText);
                            if (response.success == true) {
                                cms.admin.modules.website.pagesPanel.remove('website-' + this.module + '-' + id);
                            } else cms.admin.errors.display(response.msg);
                        },
                        waitMsg: 'Saving.....'
                    });
                },
                scope:this
            },
            '-',
            {
                iconCls: 'iconRefresh',
                text: 'Refresh',
                tooltip: 'Reload Sexy Details',
                handler: function () {
                    tree.root.reload();
                    form.load({
                        url: 'load.php',
                        method:'POST',
                        disableCaching:true,
                        params: {
                            module:'adminSexy',
                            action:'load',     // Need to define this method in adminSexy
                            id:id
                        },
                        waitMsg: 'Re-Loading.....'
                    });
                },
                scope:this
            },
            '->',
            {
                iconCls: 'iconDelete',
                text:'Delete Page',
                tooltip: 'Delete This Page and Its Content',
                disabled: (cms.admin.user.userFullAccess('website,sexy','delete',this.sitemapID,'delete')==true)? false:true,
                handler: function () {
                    Ext.MessageBox.show({
                        title:'Confirm Delete',
                        msg : '<center><b>Are you sure?</b><br/><br/><i>(This will delete this page and all related sitemap )</i></center>',
                        width:350,
                        icon : Ext.MessageBox.WARNING,
                        buttons: Ext.MessageBox.YESNO,
                        fn: function (btn) {
                            if (btn=='yes') {
                                Ext.Ajax.request({
                                    url:'load.php',
                                    params: {
                                        module:'adminSexy',
                                        action:'delete',
                                        id:id
                                    },
                                    disableCaching:true,
                                    success: function (obj,success) {
                                        if (success) {
                                            var response = Ext.decode(obj.responseText);
                                            if (response.success === true) {
                                                Ext.MessageBox.hide();
                                            } else {
                                                if (response.confirm == true) {
                                                    Ext.MessageBox.show({
                                                        title:'Confirm Delete',
                                                        msg : response.msg,
                                                        width:350,
                                                        icon : Ext.MessageBox.WARNING,
                                                        buttons: Ext.MessageBox.YESNO,
                                                        fn: function (btn) {
                                                            if (btn=='yes') {
                                                                Ext.Ajax.request({
                                                                    url:'load.php',
                                                                    params: {
                                                                        module:'adminSexy',
                                                                        action:'delete',
                                                                        id:id,
                                                                        confirmed:true
                                                                    },
                                                                    disableCaching:true,
                                                                    success: function (obj,success) {
                                                                        var response = Ext.decode(obj.responseText);
                                                                        if (response.success === true) {
                                                                            cms.admin.modules.website.sitemapTree.getRootNode().reload();
                                                                            cms.admin.modules.website.pagesPanel.getComponent('website-' + this.module + '-' + id).remove();
                                                                            Ext.MessageBox.hide();
                                                                        } else cms.admin.errors.display(response.msg);
                                                                    }

                                                                });
                                                            }
                                                        }
                                                    });
                                                } else {
                                                    Ext.MessageBox.show({
                                                        title:'Error',
                                                        msg : response.msg,
                                                        icon : Ext.MessageBox.ERROR,
                                                        buttons: Ext.MessageBox.OK
                                                    });
                                                }
                                            }
                                        } else Ext.MessageBox.alert('Error',obj.responseText);
                                    },
                                    failure : function (obj,success) {
                                        Ext.MessageBox.show({
                                            title:'Error',
                                            msg:obj.responseText,
                                            icon:Ext.MessageBox.ERROR,
                                            buttons: Ext.MessageBox.OK
                                        });
                                    }
                                });
                            }
                        }
                    });
                },
                scope: this
            },
            '-',
            {
                text:'Options',
                disabled:id==0?true:((cms.admin.user.userFullAccess('website,sexy','edit',this.sitemapID,'edit')==true)? false:true),
                iconCls:'iconOptions',
                handler: function () {
                    var optionsForm =  new Ext.form.FormPanel({
                        labelWidth:150,
                        bodyStyle:'padding:5px;',
                        border:false,
                        items:[
                        {
                            xtype:'numberfield',
                            inputType:'hidden',
                            name:'id'
                        },
                        {
                            xtype:'textfield',
                            fieldLabel: 'Thumbnail Size',
                            name:'thumbsize',
                            maxLength:20,
                            anchor:'100%'
                        },
                        {
                            xtype:'textfield',
                            fieldLabel: 'Preview Size',
                            name:'largesize',
                            maxLength:20,
                            anchor:'100%'
                        },
                        {
                            xtype:'textarea',
                            name:'settings',
                            fieldLabel:'Additional Settings',
                            maxLength:255,
                            anchor:'100%',
                            height:100
                        }
                        ]
                    });
                    var win = new Ext.Window({
                        title:'Sexy Options',
                        iconCls:'iconGallery',
                        closable:true,
                        modal:true,
                        width:400,
                        height:250,
                        resizable:false,
                        id: 'optionsWindow',
                        layout:'fit',
                        activeTab:0,
                        items: optionsForm,
                        buttons: [
                        {
                            text:'Save',
                            disabled:(cms.admin.user.checkAccess('website,sexy','edit')==true)?false:true,
                            handler: function () {
                                optionsForm.getForm().submit({
                                    url: 'load.php',
                                    method:'POST',
                                    disableCaching:true,
                                    params: {
                                        module:'adminSexy',
                                        action:'saveOptions',     // Need to define this method in adminSexy
                                        id:id
                                    },
                                    success: function (form,action) {
                                        var response = Ext.decode(action.response.responseText);
                                        if (response.success == true) {
                                            win.close();
                                        } else cms.admin.errors.display(response.msg);
                                    },
                                    failure: function (form,action) {
                                        var response = Ext.decode(action.response.responseText);
                                        if (response.success != true) {
                                            cms.admin.errors.display(response.msg);
                                        } else cms.admin.errors.display(action.response.responseText);
                                        delete response;
                                    },
                                    waitMsg: 'Saving.....'
                                });
                            }
                        },
                        {
                            text:'Cancel',
                            handler: function () {
                                win.close();
                            }
                        }

                        ]
                    });
                    optionsForm.load({
                        url:'load.php',
                        method:'POST',
                        disableCaching:true,
                        params:{
                            module:'adminSexy',
                            action:'loadOptions',     // Need to define this method in adminSexy
                            id:id
                        },
                        waitMsg: 'Loading.....'
                    });
                    win.show();
                },
                scope:this
            },
            '-',
            {
                iconCls:'iconHelp',
                tooltip: 'Open Help for ' + module.title,
                handler: function () {
                    cms.help.open(module);
                },
                scope: cms.help
            }
            ],
            items:[
            form,                       // This is where we add the Asset list and HTML editor components to the main tab
            tree
            ]
        }).show();


        // Load Sexy Details from Database
        form.load({
            url: 'load.php',
            method:'POST',
            params: {
                module:'adminSexy',
                action:'load',                            // Need to define this method in adminSexy
                id:id
            },
            waitMsg: 'Loading.....'
        });
    }
}


/** The callback function for adding gallery images */

cms.admin.modules.website.modules.sexy.addImage = function (img,form) {
    Ext.Msg.show({
        title:'Adding Image',
        msg:'Adding Image to Sexy gallery',
        wait:true
    });
    var id = form.getForm().findField('id').getValue()
    Ext.Ajax.request({
        url:'load.php',
        params:{module:'adminSexy',action:'addImage',id:id,file:img.path},
        disableCaching:true,
        success: function (o) {
            var response = Ext.decode(o.responseText);

            if (response.success == true) {
                cms.admin.modules.website.pagesPanel.getComponent('website-' + this.module + '-' + id).getComponent('website-'+this.module+'-imageTree-'+id).root.appendChild(
                new Ext.tree.TreeNode({
                    id:response.img.id,
                    text:response.img.name,
                    icon:response.img.icon,
                    leaf:true
                })
            );
                Ext.Msg.hide();
            } else cms.admin.errors.display(response.msg);
        },
        failure: cms.admin.errors.process
    });
}

Admin module methods

Finally, define all the methods in the adminSexy class that are called from the Javascript above. The completed class is shown below:

<?php

class adminSexy {

    protected $cms = NULL;

    public function __construct() {
        global $cms;
        $this->cms = $cms;
    }

    /**
     * Look up existing Sexy pages for the Add New Page wizard
     * @param array $data
     */
    public function loadLookupCombo($data) {
        $output = array();
        $sql = "SELECT id,title FROM _custom_sexy ORDER BY title ASC";

        $data = $this->cms->db->getObjects($sql);
        if (!empty($data)) {
            $output['success'] = true;
            $output['data'] = $data;
        } else {
            $output['success'] = false;
            $output['msg'] = 'No Sexy Thangs Found';
        }
        $this->cms->jsonOutput($output);
    }

    /**
     * Creates a new Sexy instance - last step of the Add New Page wizard
     * @param string $title The title to be given to the new instance of Sexy
     * @return int 0 on failure, otherwise the ID of the newly created instance
     */
    public function createPage($title = 'New Sexy Page') {
        $fields = array();
        $values = array();

        $fields[] = 'title';
        $values[] = "'" . $this->cms->db->escape($title) . "'";

        $fields[] = 'date_created';
        $values[] = 'NOW()';
        $fields[] = 'created_by';
        $values[] = $this->cms->user->id;
        $fields[] = 'last_updated_by';
        $values[] = $this->cms->user->id;


        $sql = "INSERT INTO _custom_sexy (" . implode(',', $fields) . ") VALUES (" . implode(',', $values) . ");";
        if ($this->cms->db->query($sql)) {
            $id = $this->cms->db->insertID;
            $this->cms->log->change('sexy', $this->cms->user->name . ' Successfully Created ' . $title);
            return $id;
        } else {
            $this->cms->logs->change('Error Creating Sexy Page', '<b>' . $this->cms->db->lastError . '</b><br/>' . $sql);
            return '0';
        }
    }

    /* Editor layout methods */

    /**
     * Load the images for the sexy page
     * @param array $data 
     * 
     * $data parameters:
     * - id: (int) The ID of the page
     */
    public function loadImagesTree($data='') {
        $id = 0;
        $output = array();
        if (is_array($data) || is_object($data)) {
            foreach ($data AS $name => $value) {
                switch ($name) {
                    case 'id': $id = (int) $value;
                        break;
                }
            }
        }
        if (!empty($id)) {
            if ($this->cms->user->checkRights(array('website', 'sexy'), 'view')) {
                $sql = "SELECT id,title,file " .
                        "FROM _custom_sexy_images " .
                        "WHERE sexy_id=" . $id . " " .
                        "ORDER BY sort_order ASC;";
                foreach ($this->cms->db->getObjects($sql) AS $image) {
                    $output[] = array(
                        'id' => $image->id,
                        'text' => $image->title,
                        'icon' => 'image.php?f=' . $image->file . '&s=80x60&p=1',
                        'leaf' => true,
                        'tooltip' => 'Double Click to Edit<br/>Drag Image to Reorder'
                    );
                }
            } else {
                $output['success'] = false;
                $output['msg'] = 'You DO NOT have sufficent privileges to complete this action';
            }
        } else {
            $output['success'] = false;
            $output['msg'] = 'No Sexy ID was passed';
        }

        $this->cms->jsonOutput($output);
    }

    /**
     * Update the order of image assets attached to a sexy page
     * @param array $data 
     * 
     * $data options:
     * - id: (int) sexy page ID
     * - node: (int) The image to move
     * - target: (int) The image to swap with
     */
    public function changeImageOrder($data='') {
        $table = '_custom_sexy_images';
        $id = 0;
        $file = '';
        $target = 0;
        $node = 0;
        $position = 'below';
        $output = array();
        if (is_array($data) || is_array($data)) {
            foreach ($data AS $name => $value) {
                switch ($name) {
                    case 'id':
                    case 'node':
                    case 'target':
                        $$name = (int) $value;
                        break;
                    case 'position':
                        $$name = $value;
                        break;
                }
            }
        }
        if (!empty($id)) {
            if (!empty($node)) {
                if (!empty($target)) {
                    if ($this->cms->user->checkRights(array('website', 'sexy'), 'edit')) {
                        $sql = "SELECT id FROM " . $table . " WHERE sexy_id=" . $id . " ORDER BY sort_order ASC;";
                        $output['sql'][] = $sql;
                        $change = false;
                        $order = 1;
                        foreach ($this->cms->db->getObjects($sql) AS $image) {
                            if ($image->id == $target) {
                                switch ($position) {
                                    case 'above':
                                        // Set Moved Node
                                        $sql = "UPDATE " . $table . " SET sort_order=" . $order . " WHERE id=" . $node;
                                        $output['sql'][] = $sql;
                                        if (!$this->cms->db->query($sql)) {
                                            $output['msg'] = $this->cms->db->lastError;
                                            $output['success'] = false;
                                        }
                                        $order++;
                                        //Set Target Node
                                        $sql = "UPDATE " . $table . " SET sort_order=" . $order . " WHERE id=" . $image->id;
                                        $output['sql'][] = $sql;
                                        if (!$this->cms->db->query($sql)) {
                                            $output['msg'] = $this->cms->db->lastError;
                                            $output['success'] = false;
                                        }
                                        $order++;
                                        break;
                                    case 'below':
                                        //Set Target Node
                                        $sql = "UPDATE " . $table . " SET sort_order=" . $order . " WHERE id=" . $image->id;
                                        $output['sql'][] = $sql;
                                        if (!$this->cms->db->query($sql)) {
                                            $output['msg'] = $this->cms->db->lastError;
                                            $output['success'] = false;
                                        }
                                        $order++;

                                        $sql = "UPDATE " . $table . " SET sort_order=" . $order . " WHERE id=" . $node;
                                        $output['sql'][] = $sql;
                                        if (!$this->cms->db->query($sql)) {
                                            $output['msg'] = $this->cms->db->lastError;
                                            $output['success'] = false;
                                        }
                                        $order++;
                                        break;
                                }
                            } else if ($node == $image->id) {

                            } else {
                                $sql = "UPDATE " . $table . " SET sort_order=" . $order . " WHERE id=" . $image->id;
                                $output['sql'][] = $sql;
                                if (!$this->cms->db->query($sql)) {
                                    $output['msg'] = $this->cms->db->lastError;
                                    $output['success'] = false;
                                }
                                $order++;
                            }
                        }
                        if (!isset($output['success'])) {
                            $output['success'] = true;
                        }
                    } else {
                        $output['success'] = false;
                        $output['msg'] = 'You DO NOT have sufficent privileges to complete this action';
                    }
                } else {
                    $output['success'] = false;
                    $output['msg'] = 'Missing or Invalid Target Image ID';
                }
            } else {
                $output['success'] = false;
                $output['msg'] = 'Missing or Invalid Image ID';
            }
        } else {
            $output['success'] = false;
            $output['msg'] = 'Missing or Invalid Sexy Page ID';
        }
        $this->cms->jsonOutput($output);
    }

    /**
     * Save a sexy image
     * 
     * @param array $data 
     * 
     * $data params (all self explanatory:
     * - id: (int)
     * - title: (string)
     * - url: (string)
     * - description: (string) 
     */
    public function saveImage($data='') {

        $id = 0;
        $output = array();
        if (is_array($data) || is_object($data)) {
            foreach ($data AS $name => $value) {
                switch ($name) {
                    case 'id': $id = (int) $value;
                        break;
                    case 'title':
                    case 'url':
                        $fields[] = $name . "='" . $this->cms->db->escape($value) . "'";
                        break;
                    case 'description':
                        $fields[] = $name . "='" . $this->cms->db->escape($this->cms->processContent($value)) . "'";
                        break;
                        break;
                }
            }
        }
        if (!empty($id)) {
            if ($this->cms->user->checkRights(array('website', 'sexy'), 'edit')) {
                $sql = "UPDATE _custom_sexy_images SET " . implode(',', $fields) . " WHERE id=" . $id;
                if ($this->cms->db->query($sql)) {
                    $output['success'] = true;
                } else {
                    $output['success'] = false;
                    $output['msg'] = 'Error Saving Image Details';
                }
            } else {
                $output['success'] = false;
                $output['msg'] = 'You DO NOT have sufficent privileges to complete this action';
            }
        } else {
            $output['success'] = false;
            $output['msg'] = 'Missing or Invalid Image ID';
        }
        $this->cms->jsonOutput($output);
    }

    /**
     * Load a specific image
     * @param array $data
     * 
     * $data options:
     * - id: Image ID 
     */
    public function loadImage($data='') {
        $output = array();
        $id = 0;
        if (is_array($data) || is_object($data)) {
            foreach ($data AS $name => $value) {
                switch ($name) {
                    case 'id': $id = (int) $value;
                        break;
                }
            }
        }
        if (!empty($id)) {
            if ($this->cms->user->checkRights(array('website', 'sexy'), 'view')) {
                $sql = "SELECT id,title,description,url " .
                        "FROM _custom_sexy_images " .
                        "WHERE id=" . $id;
                $output['data'] = $this->cms->db->getObject($sql);
                if (!empty($output['data'])) {
                    $output['success'] = true;
                } else {
                    $output['success'] = false;
                    $output['msg'] = 'Image Details Not Found';
                }
            } else {
                $output['success'] = false;
                $output['msg'] = 'You DO NOT have sufficent privileges to complete this action';
            }
        } else {
            $output['success'] = false;
            $output['msg'] = 'Missing or Invalid Image ID';
        }
        $this->cms->jsonOutput($output);
    }

    /**
     * Delete a specific image
     * @param array $data
     * 
     * $data options:
     * - id: Image ID 
     */
    public function deleteImage($data='') {
        $id = 0;
        $output = array();
        if (is_array($data) || is_array($data)) {
            foreach ($data AS $name => $value) {
                switch ($name) {
                    case 'id': $id = (int) $value;
                        break;
                }
            }
        }
        if (!empty($id)) {
            if ($this->cms->user->checkRights(array('website', 'sexy'), 'delete')) {
                $sql = "DELETE FROM _custom_sexy_images WHERE id=" . $id;
                if ($this->cms->db->query($sql)) {
                    $output['success'] = true;
                } else {
                    $output['success'] = false;
                    $output['msg'] = 'Error Occured Deleting Image from Sexy page';
                }
            } else {
                $output['success'] = false;
                $output['msg'] = 'You DO NOT have sufficent privileges to complete this action';
            }
        } else {
            $output['success'] = false;
            $output['msg'] = 'No Sexy page ID was passed';
        }
        $this->cms->jsonOutput($output);
    }

    /**
     * Save the current sexy page
     * @param array $data 
     * 
     * $data options (self explanatory):
     * - id: (int)
     * - title: (string)
     * - content: (string)
     */
    public function save($data) {

        $output = array();
        if ($this->cms->user->checkRights(array('website', 'sexy'), 'edit')) {
            if (isset($data['id'])) {
                $id = (int) $data['id'];
                if (!empty($id)) {

                    // Updating Exisiting Page
                    if ($this->archivePage($id)) {
                        // Process Data for Database Update
                        $fields = array();
                        foreach ($data AS $name => $value) {
                            switch ($name) {
                                case 'title':
                                    $fields[] = $name . "='" . $this->cms->db->escape($value) . "'";
                                    break;
                                case 'content':
                                    $fields[] = $name . "='" . $this->cms->db->escape($this->cms->processContent($value)) . "'";
                            }
                        }
                        $fields[] = 'last_updated_by=' . $this->cms->user->id;
                        $sql = "UPDATE _custom_sexy SET " . implode(',', $fields) . " WHERE id=" . $id;
                        if ($this->cms->db->query($sql)) {
                            $output['success'] = true;
                        } else {
                            $output['success'] = false;
                            $output['msg'] = 'Error Saving Page Details';
                        }
                    } else {
                        $output['success'] = false;
                        $output['msg'] = 'Error Archiving page Details';
                    }
                } else {
                    // Insert New Content
                    $output['success'] = false;
                    $output['msg'] = 'Function Has Not Been Completed';
                }
            } else {
                $output['success'] = false;
                $output['msg'] = 'No content ID was passed';
            }
        } else {
            $output['success'] = false;
            $output['msg'] = 'You DO NOT have sufficent privileges to complete this action';
        }
        $this->cms->jsonOutput($output);
    }

    /**
     * Load a sexy page
     * @param array $data 
     * 
     * $data options:
     * - id: (int) ID of Sexy page to load
     */
    public function load($data) {
        $output = array();
        if (is_array($data) || is_object($data)) {
            foreach ($data AS $name => $value) {
                switch ($name) {
                    case 'id': $this->id = (int) $value;
                        break;
                }
            }
        }
        if (!empty($this->id)) {
            if ($this->cms->user->checkRights(array('website', 'sexy'), 'view')) {
                $sql = "SELECT * FROM _custom_sexy WHERE id=" . $this->id . " LIMIT 1;";
                $data = $this->cms->db->getObject($sql);
                if (!empty($data)) {
                    $output['success'] = true;
                    foreach ($this->cms->db->getObject($sql) AS $name => $value) {
                        switch ($name) {
                            case 'title':
                            case 'content':
                                $output['data'][$name] = str_replace('\"', '"', $value);
                                break;
                            default:
                                $output['data'][$name] = $value;
                        }
                    }
                } else {
                    $output['success'] = false;
                    $output['msg'] = 'Page was Not Found';
                }
            } else {
                $output['success'] = false;
                $output['msg'] = 'You DO NOT have sufficent privileges to complete this action';
            }
        } else {
            $output['success'] = false;
            $output['msg'] = 'No content ID was passed';
        }
        $this->cms->jsonOutput($output);
    }

    /**
     * Delete a sexy page
     * @param array $data 
     * 
     * $data options:
     * - id: (int) ID of Sexy page to delete
     */
    public function delete($data) {
        $output = array();
        $confirmed = (isset($data['confirmed']) && $data['confirmed'] == true) ? true : false;
        if ($this->cms->user->checkRights(array('website', 'sexy'), 'delete')) {
            if (isset($data['id'])) {
                $id = (int) $data['id'];
                if (!empty($id)) {
                    // Check for linked Menu Items
                    $sql = "SELECT count(id) AS total FROM website_sitemap WHERE module='sexy' AND module_id=" . $id . ";";
                    $menuItems = $this->cms->db->getObject($sql);
                    if (empty($menuItems) || $confirmed == true) {

                        $sql = "DELETE FROM _custom_sexy WHERE id=" . $id . ";";
                        if ($this->cms->db->query($sql)) {
                            $sql = "DELETE FROM website_sitemap WHERE module='sexy' AND module_id=" . $id . ";";
                            if ($this->cms->db->query($sql)) {
                                $output['success'] = true;
                            } else {
                                $output['success'] = false;
                                $output['confirm'] = false;
                                $output['msg'] = 'Error Occured While Deleting Sitemap Links';
                                $this->cms->error->logError('Website Sitemap Delete Error', '<b>' . $this->cms->db->lastError . '</b><br/>' . $sql);
                            }
                        } else {
                            $output['success'] = false;
                            $output['confirm'] = false;
                            $output['msg'] = 'Error Occured While Deleting Page';
                            $this->cms->error->logError('Website Sexy Delete Error', $output['msg']);
                        }
                    } else {
                        $output['success'] = false;
                        $output['confirm'] = true;
                        $output['msg'] = '<div style="margin-left:50px;">There are ' . ($menuItems->total > 1 ? $menuItems->total . ' menu items' : '1 menu item ') . ' still linked to this page.<br/><br/><center><b>Are You Sure?</b></center><br/><center><i>This will delete all linked menu items</i></div>';
                    }
                } else {
                    $output['success'] = false;
                    $output['confirm'] = false;
                    $output['msg'] = 'No Sexy ID or Invalid ID was passed';
                    $this->cms->error->logError('Website Sexy Delete Error', $output['msg']);
                }
            } else {
                $output['success'] = false;
                $output['confirm'] = false;
                $output['msg'] = 'Invalid or Missing Sexy ID';
                $this->cms->error->logError('Website Sexy Delete Error', $output['msg']);
            }
        } else {
            $output['success'] = false;
            $output['confirm'] = false;
            $output['msg'] = 'You DO NOT have sufficent privileges to complete this action';
            $this->cms->error->logError('Security Error', 'Trying to Delete Sexy Page' . chr(10) . 'Module: website,sexy' . chr(10) . 'ID: ' . $id);
        }
        $this->cms->jsonOutput($output);
    }

    /**
     * Save the options for this sexy page
     * @param array $data 
     * 
     * $data options (self explnatory):
     * - id: (int)
     * - thumbsize: (string)
     * - largesize: (string)
     * - settings: (string)
     */
    public function saveOptions($data='') {
        $id = 0;

        $output = array();
        if (is_array($data) || is_object($data)) {
            foreach ($data AS $name => $value) {
                switch ($name) {
                    case 'id': $id = (int) $value;
                        break;
                    case 'thumbsize':
                    case 'largesize':
                    case 'settings':
                        $fields[] = $name . "='" . $this->cms->db->escape($value) . "'";
                        break;
                }
            }
        }
        if (!empty($id)) {
            if ($this->cms->user->checkRights(array('website', 'gallery'), 'edit')) {
                $sql = "UPDATE _custom_sexy SET " . implode(',', $fields) . " WHERE id=" . $id;
                if ($this->cms->db->query($sql)) {
                    $output['success'] = true;
                } else {
                    $output['success'] = false;
                    $output['msg'] = 'Error Saving Gallery Options Details';
                }
            } else {
                $output['success'] = false;
                $output['msg'] = 'You DO NOT have sufficent privledges to complete this action';
            }
        } else {
            $output['success'] = false;
            $output['msg'] = 'Missing or Invalid Image ID';
        }
        $this->cms->jsonOutput($output);
    }

    /**
     * Make an archive backup copy of the nominated module row
     *
     * @param int $id The row ID
     */
    private function archivePage($id) {
        $sql =  "INSERT INTO _custom_sexy_archive (id,title,content,date_created,created_by,last_updated_by,last_updated) ".
                     "SELECT id,title,content,date_created,created_by,last_updated_by,last_updated FROM _custom_sexy WHERE id=".intval($id);
        return $this->cms->db->query($sql)?true:false;
    }

       /**
     * Add an image to the current gallery
     *
     * @param array $data The image data
     * 
     * $data options:
     * - id: (int) The sexy ID
     * - file: (string) The filename to add
     */
    public function addImage($data='') {
        global $cms;
        $id = 0;
        $file = '';
        if (is_array($data) || is_object($data)) {
            foreach ($data AS $name => $value) {
                switch ($name) {
                    case 'id': $id = (int) $value;
                        break;
                    case 'file': $file = $value;
                        break;
                }
            }
        }
        if (!empty($id)) {
            if (!empty($file)) {
                if ($cms->user->checkRights(array('website', 'gallery'), 'add')) {
                    $sql = "SELECT count(id) AS total FROM _custom_sexy_images WHERE sexy_id=" . $id;
                    $count = $cms->db->getObject($sql);
                    if (isset($count->total)) {
                        $fields = array();
                        $values = array();

                        $fields[] = 'sexy_id';
                        $values[] = $id;

                        $fields[] = 'file';
                        $values[] = "'" . $cms->db->escape($file) . "'";

                        $fields[] = 'sort_order';
                        $values[] = $count->total;

                        $sql = "INSERT INTO _custom_sexy_images (" . implode(',', $fields) . ") VALUES (" . implode(',', $values) . ");";
                        if ($cms->db->query($sql)) {
                            $output['success'] = true;
                            $output['img'] = array(
                                'icon' => 'image.php?f=' . $file . '&s=80x60&p=1',
                                'id' => $cms->db->insertID,
                                'text' => ''
                            );
                        } else {
                            $output['success'] = false;
                            $output['msg'] = 'Error Adding Image to Sexy Gallery';
                        }
                    } else {
                        $output['success'] = false;
                        $output['msg'] = 'Error Counting No of Existing Images';
                    }
                } else {
                    $output['success'] = false;
                    $output['msg'] = 'You DO NOT have sufficent privledges to complete this action';
                }
            } else {
                $output['success'] = false;
                $output['msg'] = 'Missing or Invalid File Name';
            }
        } else {
            $output['success'] = false;
            $output['msg'] = 'Missing or Invalid Sexy Gallery ID';
        }
        $this->cms->jsonOutput($output);
    }



}

That's It!

At this point, you've learned how to:

  • Use PHP to create a new custom module
  • Use PHP and ExtJS to set up the module's customization features in the "Add Page" wizard
  • Build an administration system for the module
  • Create a basic template for the module
  • Retrieve module-related data from the database and display it to the screen

There are a myriad of ways in which modules can be extended. There is an ever-growing API for Snapp for your reference at http://cms_api.crudigital.com.au

Further information on ExtJS can be found at http://www.sencha.com/deploy/dev/docs/

Personal tools
Core Components
Standalone Installs