Build a module : Backend

Our module is supposed to manage authors and links between authors and articles.

We will need some database tables to do this properly.

Add the database tablesTop of Page

Remember, one of our first task was to create the config/config.php file, which gives infos about the module and makes the backend able to install it.

During the installation process, the module's installer is looking for the database.xml file.
If this file exists, the module's installer will use it to create tables and insert data.

Create the file /modules/Demo/database.xml and write the code :

<?xml version="1.0" encoding="UTF-8"?>
<sql>
<name>Ionize Demo Module Database Creation</name>
<version>1.0</version>
<license>Open Source MIT license</license>

<!--
Module's tables
Prefixed by module_<module name> to avoid collision
-->
<tables>

<!--
Author table
One author has a name and a gender
-->
<query>
CREATE TABLE IF NOT EXISTS module_demo_author (
id_author INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
gender TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
name VARCHAR(255) NOT NULL,
PRIMARY KEY (id_author)
)
ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_unicode_ci;
</query>

<!--
Author lang table
The author's bio or description should be available
for each language
-->
<query>
CREATE TABLE IF NOT EXISTS module_demo_author_lang (
id_author INT UNSIGNED NOT NULL,
lang VARCHAR(3) NOT NULL,
description VARCHAR(3000) NULL,
PRIMARY KEY (id_author,lang)
)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_unicode_ci;
</query>

<!--
Author media table
One author can have a photo
-->
<query>
CREATE TABLE IF NOT EXISTS module_demo_author_media (
id_author INT(11) UNSIGNED NOT NULL,
id_media INT(11) UNSIGNED NOT NULL DEFAULT 1,
ordering INT(11) UNSIGNED NULL DEFAULT 9999,
PRIMARY KEY (id_author, id_media)
)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_unicode_ci;
</query>

<!--
Link table
One author is linked to one page, article, other parent
-->
<query>
CREATE TABLE IF NOT EXISTS module_demo_links (
parent CHAR(25) NOT NULL,
id_parent INT(11) UNSIGNED NOT NULL,
id_author INT UNSIGNED NOT NULL,
ordering INT(11) UNSIGNED NULL DEFAULT 9999,
PRIMARY KEY (parent,id_parent,id_author)
)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_unicode_ci;
</query>

</tables>

<!--
Content inserted at module's installation
-->
<content>

<query>
INSERT IGNORE INTO module_demo_author (id_author,name,gender)
VALUES
(1, 'Roberto Chansez', 1),
(2, 'Elisabeth Fueller', 2),
(3, 'Emile Durand', 1);
</query>

<query>
INSERT IGNORE INTO module_demo_author_lang (id_author,lang,description)
VALUES
(1, 'en', 'Known as a famous explorator, he discovered more than 50 new kind of animals'),
(2, 'en', 'World known artist and designer'),
(3, 'en', 'Famous TV speakering in Marioland');
</query>

</content>
</sql>

Go to Ionize, in Modules > Administration, uninstall the module and install it again.

Open your favorite database editor, have a look to your Ionize's database : The installer has created the 4 tables :

  • module_demo_author
  • module_demo_author_lang
  • module_demo_author_media
  • module_demo_links

If you look to the tables content, you will see that some basic content already exists in the tables module_demo_author and module_demo_author_lang.

Display the authors listTop of Page

When we click on the module's dashboard icon, it calls the module's admin default controller (admin/demo.php), which loads the view /modules/Demo/views/admin/demo.php.

We will now create :

  • One controller to manage the authors (display list, edit, save, delete),
  • One model to get and set data in our tables,
  • The views to display the authors list and edit one author on the backend

Create the file /modules/Demo/models/demo_author_model.php containing the code :

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Demo_author_model extends Base_model
{
    // Author tables
    protected $_author_table = 'module_demo_author';
    protected $_author_lang_table = 'module_demo_author_lang';

    // Link table between authors and parents (page, article)
    protected $_link_table = 'module_demo_links';

    /**
     * Model Constructor
     *
     * @access  public
     */
    public function __construct()
    {
        $this->set_table($this->_author_table);
        $this->set_lang_table($this->_author_lang_table);
        $this->set_pk_name('id_author');

        parent::__construct();
    }
}

The model extends Base_model, which has good generic methods to set and retrieve data fro Ionize database.
As Demo_author_model extends Base_model, it inherits its methods.

We will enhance this model, but for the moment, this code is enough.

Create the file /modules/Demo/controllers/admin/author.php containing the code :

<?php if( ! defined('BASEPATH')) exit('No direct script access allowed');

class Author extends Module_Admin
{
    /**
     * Constructor
     *
     * @access  public
     * @return  void
     */
    public function construct()
    {
        // Loads the model as 'author_model'
        $this->load->model('demo_author_model', 'author_model', true);
    }

    /**
     * Outputs the authors list
     *
     */
    public function get_list()
    {
        $conds = array(
            'order_by' => 'name ASC'
        );

        $this->template['authors'] = $this->author_model->get_list($conds);

        $this->output('admin/author_list');
    }

    /**
     * Outputs the detail of one author
     * @param  int    ID of the author
     *
     */
    public function get($id)
    {
        /* has to be written */
    }

    /**
     * Saves one author
     *
     */
    public function save()
    {
        /* has to be written */
    }
}

Create the file /modules/Demo/views/admin/author_list.php and add the code :

<ul class="authorPanelList list mb20 mt10">

    <?php foreach($authors as $author) :?>

    <?php
    $id = $author['id_author'];
    ?>

    <li class="author<?php echo $id ?> pointer" id="author_<?php echo $id ?>" data-id="<?php echo $id ?>">
        <a class="icon drag left"></a>
        <a class="left pl5 edit title" data-id="<?php echo $id ?>">
            <?php echo $author['name'] ?>
        </a>
    </li>

    <?php endforeach ;?>

</ul>

Now, we have a view which is able to display the list of authors.

Let's modify the default admin view of the module to use this list.

Modify the view /modules/Demo/views/admin/demo.php so that the code look like this :

<div id="maincolumn">

    <h2 class="main demo"><?php echo lang('module_demo_title'); ?></h2>

    <div class="subtitle">

        <!-- About this module -->
        <p class="lite">
            <?php echo lang('module_demo_about'); ?>
        </p>

    </div>

    <!-- Will contains the authors list -->
    <div id="moduleDemoAuthorsList"></div>

</div>

<script type="text/javascript">

    // Init the panel toolbox is mandatory
    ION.initToolbox('empty_toolbox');

    // Update the authors list
    ION.HTML(
            'module/demo/author/get_list', // URL to the controller
            {}, // Data send by POST. Nothing
            {'update':'moduleDemoAuthorsList'} // JS request options
    );

</script>

In Ionize, go to Modules > Demo module. Now, it displays the authors list :

Ionize Administration : Module Demo authors list

To understand what happened :

  1. The view admin/demo.php calls through Ajax the controller admin/demo/author/get_list
  2. This controller uses the model demo_author_model to get the author list, send the data to the view and outputs the view
  3. The Ajax request receive the HTML output and updates the DIV with the ID "moduleDemoAuthorList"

Answer to common remarks :

The model "demo_author_model" hasn't any method "get_list"...

Yes, but this model extends "base_model", which has this method.
We set the table of the model with $this->set_table() and $this->set_lang_table() : That's enough to make most of the method of base_model working with the authors tables.

I have to know that the JS "ION" object has a method to make HTML request

Yes, the javascript ION object has several method to make requests, open windows and load data inside them, refreshing given DIV, init drag'n'drop and much more.
The Javascript documentation isn't written at this time (but we will write it! ), but you can have a look at the source files or use standard mootools requests to get the same result.

Edit and save one authorTop of Page

To edit one author, we will use one Ionize window :

  • One click on the author's name will open a window containing the author's form.
  • This window will have 2 buttons : one to save the author, one to cancel and close the window.

Before modify the view, prepare the controller's "get" and "save" actions :

<?php if( ! defined('BASEPATH')) exit('No direct script access allowed');

/**
 * Module Admin controller
 *
 *
 */
class Author extends Module_Admin
{
    /**
     * Constructor
     *
     * @access  public
     * @return  void
     */
    public function construct()
    {
        $this->load->model('demo_author_model', 'author_model', TRUE);
        $this->load->model('page_model', '', TRUE);
    }


    /**
     * Outputs the authors list
     *
     */
    public function get_list()
    {
        $conds = array(
            'order_by' => 'name ASC'
        );

        $this->template['authors'] = $this->author_model->get_list($conds);

        $this->output('admin/author_list');
    }


    /**
     * Outputs the detail of one author
     * @param int ID of the author
     *
     */
    public function get($id)
    {
        $where = array(
            'id_author' => $id
        );
        $this->template = $this->author_model->get($where);

        $this->author_model->feed_lang_template($id, $this->template);

        $this->output('admin/author_detail');
    }


    /**
     * Saves one author
     *
     */
    public function save()
    {
        // The name must be set
        if ($this->input->post('name') != '')
        {
            $id_author = $this->author_model->save($this->input->post());

            // Update the authors list
            $this->update[] = array(
                'element' => 'moduleDemoAuthorsList',
                'url' => admin_url() . 'module/demo/author/get_list'
            );

            // Send the user a message
            $this->success(lang('ionize_message_operation_ok'));
        }
        else
        {
            // Send the user a message
            $this->error(lang('ionize_message_operation_nok'));
        }
    }
}

To be able to save the author, edit the demo_author_model and add the method save() :

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Demo_author_model extends Base_model
{
    // Author tables
    protected $_author_table = 'module_demo_author';
    protected $_author_lang_table = 'module_demo_author_lang';

    // Link table between authors and parents (page, article)
    protected $_link_table = 'module_demo_links';


    /**
     * Model Constructor
     *
     * @access  public
     */
    public function __construct()
    {
        $this->set_table($this->_author_table);
        $this->set_lang_table($this->_author_lang_table);
        $this->set_pk_name('id_author');

        parent::__construct();
    }


    public function save($inputs)
    {
        // Arrays of data which will be saved
        $data = $data_lang = array();

        // Fields of the author table
        $fields = $this->list_fields();

        // Set the data to the posted value.
        foreach ($fields as $field)
            $data[$field] = $inputs[$field];

        $lang_fields = $this->list_fields($this->_author_lang_table);

        foreach(Settings::get_languages() as $language)
        {
            foreach ($lang_fields as $field)
            {
                if ($field != $this->pk_name && $field != 'lang')
                {
                    $input_field = $field.'_'.$language['lang'];
                    if ($inputs[$input_field] !== FALSE)
                        $data_lang[$language['lang']][$field] = $inputs[$input_field];
                }
            }
        }

        return parent::save($data, $data_lang);
    }
}

We need one view to display the author form.

Create the file /modules/Demo/views/admin/author_detail.php :

<?php
$id = $id_author;
?>

<form name="authorForm<?php echo $id ?>" id="authorForm<?php echo $id ?>" action="<?php echo admin_url() ?>module/demo/author/save">

    <!-- Hidden fields -->
    <input id="id_author<?php echo $id ?>" name="id_author" type="hidden" value="<?php echo $id ?>" />

    <!-- Name -->
    <dl class="small">
        <dt>
            <label for="name<?php echo $id ?>"><?php echo lang('ionize_label_name')?></label>
        </dt>
        <dd>
            <!--
  The validation of this mandatory field is first done by JS
  by adding the attribute data-validators="required"
  see : http://mootools.net/docs/more/Forms/Form.Validator#Validators
  -->
            <input id="name<?php echo $id ?>" name="name" class="inputtext required" type="text" value="<?php echo $name ?>" data-validators="required"/>
        </dd>
    </dl>

    <!-- Gender -->
    <dl class="small">
        <dt>
            <label><?php echo lang('module_demo_label_gender')?></label>
        </dt>
        <dd>
            <input id="genderMale<?php echo $id ?>" name="gender" <?php if ($gender == 1):?>checked="checked" <?php endif; ?>class="radio" type="radio" value="1" />
            <label for="genderMale<?php echo $id ?>">
                <?php echo lang('module_demo_label_male')?>
            </label>
            <br/>
            <input id="genderFemale<?php echo $id ?>" name="gender" <?php if ($gender == 2):?>checked="checked" <?php endif; ?> class="radio" type="radio" value="2" />
            <label for="genderFemale<?php echo $id ?>">
                <?php echo lang('module_demo_label_female')?>
            </label>
        </dd>
    </dl>

    <fieldset>

        <!-- Tabs -->
        <div id="authorTab<?php echo $id ?>" class="mainTabs">
            <ul class="tab-menu">
                <?php foreach(Settings::get_languages() as $l) :?>
                <li class="tab_edit_author<?php echo $id ?><?php if($l['def'] == '1') :?> dl<?php endif ;?>"><a><span><?php echo ucfirst($l['name']) ?></span></a></li>
                <?php endforeach ;?>
            </ul>
            <div class="clear"></div>
        </div>

        <div id="authorTabContent<?php echo $id ?>">

            <!-- Text block -->
            <?php foreach(Settings::get_languages() as $language) :?>

            <?php $lang = $language['lang']; ?>

            <div class="tabcontent<?php echo $id ?>">

                <!-- description -->
                <textarea id="description_<?php echo $lang ?><?php echo $id ?>" name="description_<?php echo $lang ?>" class="textarea autogrow" rel="<?php echo $lang ?>"><?php echo $languages[$lang]['description'] ?></textarea>

            </div>

            <?php endforeach ;?>

        </div>

    </fieldset>

</form>

<!-- Save / Cancel buttons
   Must be named bSave[windows_id] where 'window_id' is the used ID
   or the window opening through ION.formWindow()
-->
<div class="buttons">
    <button id="bSaveauthor<?php echo $id ?>" type="button" class="button yes right"><?php echo lang('ionize_button_save_close') ?></button>
    <button id="bCancelauthor<?php echo $id ?>"  type="button" class="button no right"><?php echo lang('ionize_button_cancel') ?></button>
</div>

<script type="text/javascript">

    // Tabs init
    new TabSwapper({
        tabsContainer: 'authorTab<?php echo $id ?>',
        sectionsContainer: 'authorTabContent<?php echo $id ?>',
        selectedClass: 'selected',
        tabs: 'li',
        clickers: 'li a',
        sections: 'div.tabcontent<?php echo $id ?>'
    });

    // Autogrow textareas of the given form ID
    ION.initFormAutoGrow('authorForm<?php echo $id ?>');

</script>

Some comments about this view :

We see that the form's HTML ID is postfixed with the author's ID : This is needed, because the "Save" button will send the data to the controller through XHR (Ajax). The Ajax function has to know which form to send. Also, 2 forms cannot have the same ID and more than one author edition window can be opened in the same time.

Every form element is also postfixed : this is to be sure that no other HTML element will have the same ID.

Now, we will add the event which will open the author editing window on each author's name of the list.

Open and edit the file /modules/Demo/views/admin/author_list.php, to add some javascript :

<ul class="authorPanelList list mb20 mt10">

    <?php foreach($authors as $author) :?>

    <?php
    $id = $author['id_author'];
    ?>

    <li class="author<?php echo $id ?> pointer" id="author_<?php echo $id ?>" data-id="<?php echo $id ?>">
        <a class="icon drag left"></a>
        <a class="left pl5 edit title" data-id="<?php echo $id ?>">
            <?php echo $author['name'] ?>
        </a>
    </li>

    <?php endforeach ;?>

</ul>

<script type="text/javascript">

    // Click Event to display the details of one creator
    $$('.authorPanelList li').each(function(item, idx)
    {
        var id = item.getProperty('data-id');
        var a = item.getElement('a.title');

        a.removeEvents('click');
        a.addEvent('click', function(e)
        {
            // see : /themes/admin/javascript/ionize/ionize_window.js
            // ION.formWindow : function(id, form, title, wUrl, wOptions, data)
            ION.formWindow(
                    'author' + id, // ID of the window
                    'authorForm' + id, // ID of the author form
                    'module_demo_title_edit_author', // term of the window title
                    'module/demo/author/get/' + id, // URL of the controller
                    {
                        'width':350,
                        'height':200
                    }
            );
        });
    });

</script>


Some explanation :

The JS function ION.formWindow() will open one Ionize window, display the form by using the given controller and automatically add the "save" / "cancel" events on the window buttons.

To be able to do that, this function has to know the ID of the form to use.

Also, the "cancel" and "save" buttons of the form must have one given ID, set in the author_detail view, which must be :

  • Save button: bSave<window_id>. Example : bSaveauthor22
  • Cancel button : bCancel<window_id>. Example : bCancelauthor22

When saving the author, the data will be send to the form "action" URL by POST.
The save controller will :

  • Send the posted data to the save() method of the author model (demo_author_model)
  • Update the authors list and display one message to the user.
    Ionize has a JS callback integrated system : Define the "update" array of the controller, set the HTML DOM element which needs to be updated and the corresponding URL (here the controller's authors list) is enough to execute this request after the save request.

Now, when we reload the module and click on one author name, one window opens and we're able to edit the author's data and save them.


Ionize Administration : Module Demo author edition

Create one authorTop of Page

To create one author, we will need :

  • One button to open a "Author creation" window : We will put this button on the main panel of the module.
  • Modify a little the "get" method of the controller.

Add the "Create Author" button

We will add this button in the module's toolbar.

The module "toolbar" is the buttons container next to the main panel title :
When you edit one article, this toolbar contains the main actions buttons : Add media, Delete, Save, etc.

When we created the module's main view (/modules/Demo/views/demo.php) you probably noticed than we added this little JS code:

ION.initToolbox('empty_toolbox');

This is mandatory because the toolbar is loaded through XHR after the panel has loaded, to give each panel the ability to load its own toolbox.

Note : On panels which hasn't any toolbox, it is mandatory to load the "empty_toolbox" to remove the previous panel buttons.

Create the folder /modules/Demo/views/admin/toolboxes/

Create the file /modules/Demo/views/admin/toolboxes/demo_toolbox.php and add the code :

<div class="divider">
    <a class="button light" id="newAuthorToolbarButton">
        <i class="icon-plus"></i><?php echo lang('module_demo_button_create_author'); ?>
    </a>
</div>

<script type="text/javascript">

    $('newAuthorToolbarButton').addEvent('click', function(e)
    {
        ION.formWindow(
            'author',
            'authorForm',
            Lang.get('module_demo_label_new_author'),
            admin_url + 'module/demo/author/create',
            {
               'width':350,
               'height':250
            }
        );
    });

</script>

This little piece of code will display the button and add the "Open window form" event on the button.

We need now to use this toolbar in the main panel of the module.

Open and edit the file /modules/Demo/views/demo.php :

Replace the code :

ION.initToolbox('empty_toolbox');

by :

ION.initModuleToolbox('demo','demo_toolbox');

Reloading the module will display the button "Create Author" :

Ionize Module : Toolbar create button

Modify the author controller

To display one empty author form, add the following method to the file /modules/Demo/controllers/admin/authors.php :

    /**
     * Displays the author form
     *
     */
    function create()
    {
        $this->author_model->feed_blank_template($this->template);
        $this->author_model->feed_blank_lang_template($this->template);

        $this->output('admin/author_detail');
    }

Now, the button opens one empty form and saves the author.

Delete one authorTop of Page

We will add one delete icon to each author in the authors list, because this will not need to open again the author window to delete the author.

Add the delete button

Open the file /modules/Demo/views/admin/author_list.php and :

Find the code :

<a class="icon drag left"></a>

Just after, add :

<a class="icon delete right"></a>

Modify the javascript block of this file so it looks like :

<script type="text/javascript">

    // Click Event to display the details of one creator
    $$('.authorPanelList li').each(function(item, idx)
    {
        var id = item.getProperty('data-id');
        var a = item.getElement('a.title');
        var del = item.getElement('a.delete');

        a.addEvent('click', function(e)
        {
            // see : /themes/admin/javascript/ionize/ionize_window.js
            // ION.formWindow : function(id, form, title, wUrl, wOptions, data)
            ION.formWindow(
                    'author' + id,      // ID of the window
                    'authorForm' + id,    // ID of the author form
                    'module_demo_title_edit_author',  // lang term of the window title
                    'module/demo/author/get/' + id,    // URL of the controller
                    {
                        'width':350,
                        'height':200
                    }
            );
        });

        ION.initRequestEvent(
                del, // The item to add the event on
                admin_url + 'module/demo/author/delete/' + id, // URL to call
                {}, // Data to send. Here nothing.
                // Confirmation object
                {
                    'confirm': true,
                    'message': Lang.get('ionize_confirm_element_delete')
                }
        );

    });

</script>

Modify the model

Open the file /modules/Demo/models/demo_author_model.php and add the following method:

    /**
     * Deletes one Author
     * and the corresponding lang data
     *
     * @param int   $id
     *
     * @return int  Number of delete items in main table
     *
     */
    public function delete($id)
    {
        $nb_rows = parent::delete($id, $this->_author_table);
    
        if ($nb_rows > 0)
        parent::delete($id, $this->_author_lang_table);
    
        return $nb_rows;
    }

Modify the controller

Open the file /modules/Demo/controllers/admin/author.php and add the following method:

/**
 * Delete one author
 *
 */
public function delete($id)
{
    if ($this->author_model->delete($id) > 0)
    {
        // Update the authors list
        $this->update[] = array(
            'element' => 'moduleDemoAuthorsList',
            'url' => admin_url() . 'module/demo/author/get_list'
        );

        // Send the user a message
        $this->success(lang('ionize_message_operation_ok'));
    }
    else
    {
        // Send the user a message
        $this->error(lang('ionize_message_operation_nok'));
    }
}

Conclusion

We have now one module which is able to create, edit and delete authors.

In the next part, we will create a module addon, to be able to easily link authors to one article, simply by using Drag'n'drop.