This manual is deprecated. Please visit https://groupoffice.readthedocs.io for the latest documentation.

Difference between revisions of "Creating a module"

From Group-Office Groupware and CRM Documentation
Jump to: navigation, search
(Minify your code)
(Designing the views)
 
(15 intermediate revisions by 3 users not shown)
Line 1: Line 1:
Group-Office makes it very easy to rapidly develop an application. You can create your first basic module in about 15 minutes!
+
== Creating an empty module ==
  
== The Basics ==
+
Now we will create the main module class.
  
<b>Important!</b> First set Group-Office to debugging mode so it won't use compressed javascript files. Set <b>$config['debug']=true;</b> in config.php.
+
A Group-Office module will extend the GO_Base_Module class.
  
In this wiki we will create a module called TestModule.
+
Create the file module/presidents/PresidentsModule.php
  
Create a folder testmodule in the modules folder (always use a name without special characters).
+
'''PresidentsModule.php:'''
 
+
In this folder we now create a subfolder called classes, where we will keep our module class.
+
 
+
Create directories:
+
 
<pre>
 
<pre>
modules/testmodule/
+
class GO_Presidents_PresidentsModule extends GO_Base_Module{
modules/testmodule/classes/
+
 +
public function autoInstall() {
 +
return true;
 +
}
 +
 +
public function author() {
 +
return 'Merijn Schering';
 +
}
 +
 +
public function authorEmail() {
 +
return 'mschering@intermesh.nl';
 +
}
 +
}
 
</pre>
 
</pre>
  
Always use the following resources when developing for Group-Office:
+
=== Creating our first view ===
  
# [http://www.group-office.com/phpdoc Group-Office PHP API documentation]
+
Now we will create the main panel of the module.
# [http://www.extjs.com/deploy/dev/docs/ ExtJS 2.2 API documentation]
+
All views in Group-Office view is an extension of an ExtJS Panel. So it can be put in any other ExtJS Container.
# [http://www.extjs.com/learn ExtJS learing center]
+
  
== Creating an empty module ==
+
For the main panel create the file: module/presidents/views/Extjs3/MainPanel.js
 
+
Now we will create the main panel of the module.
+
 
+
A Group-Office module is an extension of an Ext Panel. So it can be put in any other Ext container.
+
 
+
Create the file module/testmodule/MainPanel.js
+
  
 
'''MainPanel.js:'''
 
'''MainPanel.js:'''
 
<pre>
 
<pre>
 
// Creates namespaces to be used for scoping variables and classes so that they are not global.
 
// Creates namespaces to be used for scoping variables and classes so that they are not global.
Ext.namespace('GO.testmodule');
+
Ext.namespace('GO.presidents');
  
 
/*
 
/*
 
  * This is the constructor of our MainPanel
 
  * This is the constructor of our MainPanel
 
  */
 
  */
GO.testmodule.MainPanel = function(config){
+
GO.presidents.MainPanel = function(config){
  
 
if(!config)
 
if(!config)
Line 51: Line 52:
 
* Explicitly call the superclass constructor
 
* Explicitly call the superclass constructor
 
*/
 
*/
  GO.testmodule.MainPanel.superclass.constructor.call(this, config);
+
  GO.presidents.MainPanel.superclass.constructor.call(this, config);
  
 
}
 
}
  
 
/*
 
/*
  * Extend the base class
+
  * Extend our MainPanel from the ExtJS Panel
 
  */
 
  */
Ext.extend(GO.testmodule.MainPanel, Ext.Panel,{
+
Ext.extend(GO.presidents.MainPanel, Ext.Panel,{
  
 
});
 
});
  
 
/*
 
/*
  * This will add the module to the main tabpanel filled with all the modules
+
  * This will add the module to the main tab-panel filled with all the modules
 
  */
 
  */
GO.moduleManager.addModule('testmodule', GO.testmodule.MainPanel, {
+
GO.moduleManager.addModule(
title : 'Test Module',
+
'presidents', //Module alias
iconCls : 'go-module-icon-testmodule'
+
GO.presidents.MainPanel, //The main panel for this module
});
+
{
 +
title : 'Presidents', //Module name in start-menu
 +
iconCls : 'go-module-icon-presidents' //The css class with icon for start-menu
 +
}
 +
);
 +
 
 
</pre>
 
</pre>
  
Now we need to tell Group-Office to include this main panel each time our module is loaded.
+
This view will only render the text 'Hello World'
  
For Group-Office to know what scripts to include we need a file called scripts.txt in our modules/testmodule/ folder.
+
For the main panel to be loaded when the module is active, we'll need to create a file called scripts.txt in our /modules/presidents/views/ExtJs3/ folder.
  
 
'''scripts.txt:'''
 
'''scripts.txt:'''
 
<pre>
 
<pre>
modules/testmodule/MainPanel.js
+
modules/presidents/views/Extjs3/MainPanel.js
 
</pre>
 
</pre>
  
 
Without this file your module will never work!
 
Without this file your module will never work!
  
In Group-Office go to 'Admin menu->modules' and install your newly created module.
+
=== Multilingual functions ===
  
Now we have a very simple module that is only an empty panel. Now it's up to you to add some useful functionality or you can continue to read this wiki.
+
To make the module translatable create the follow file: modules/presidents/language/en.php:
  
Everything that works in the ExtJS framework works in GO. Do study the Ext extensions we already created for GO in the documentation.
+
'''en.php'''
 +
<pre>
 +
<?php
 +
/**
 +
* This $l array excists of key-value pairs
 +
* key is what should be called in the view eg. GO.presidents.name
 +
* value is the string that will be displayed in your view
 +
*/
 +
$l = array(
 +
'name' => ' Presidents', //this name will appear in the Start menu -> modules section
 +
'description' => 'A simple test module', //this description will appear in the Start menu -> modules section
 +
);
 +
</pre>
  
 +
These two string are the name and the description of the Group-Office module and are use by the module installation dialog.
 +
 +
After completing these steps you should be able to install your module and be able to display the hello world view that was created.
 +
 +
In Group-Office go to 'Admin menu->modules' and install your newly created module.
  
 
== The Tutorial ==
 
== The Tutorial ==
Line 95: Line 118:
 
The example module we are going to make will show you how to create a simple module that displays data from a database and lets us add items to it, change them and delete them.
 
The example module we are going to make will show you how to create a simple module that displays data from a database and lets us add items to it, change them and delete them.
  
All files are also available as a zip file and can be downloaded here: [[Media:Testmodule.zip]]
+
All files are also available in the Group-Office download. You can find them in modules/presidents/.
  
 
=== Before we start ===
 
=== Before we start ===
  
First, we need to import some data about the former Presidents of the USA.
+
First, we need to create the database containing the data we are going to work with.
  
Import data into MySQL:
+
We will create 2 tables named 'pm_parties' and 'pm_presidents'
 +
It is always a good idea to prefix the database tables used for a module (pm_ is short for President Module)
 +
 
 +
Run the following MySQL query on the Group-Office database of your installation:
 
<pre>
 
<pre>
CREATE TABLE IF NOT EXISTS `tm_parties` (
+
CREATE TABLE IF NOT EXISTS `pm_parties` (
   `IDparty` int(11) NOT NULL auto_increment,
+
   `id` int(11) NOT NULL auto_increment,
   `partyName` varchar(40) NOT NULL,
+
   `name` varchar(40) NOT NULL,
   PRIMARY KEY  (`IDparty`)
+
   PRIMARY KEY  (`id`)
 
);
 
);
  
INSERT INTO `tm_parties` (`IDparty`, `partyName`) VALUES
+
INSERT INTO `pm_parties` (`id`, `name`) VALUES
 
(1, 'No Party'),
 
(1, 'No Party'),
 
(2, 'Federalist'),
 
(2, 'Federalist'),
Line 117: Line 143:
 
(6, 'Republican');
 
(6, 'Republican');
  
CREATE TABLE IF NOT EXISTS `tm_presidents` (
+
CREATE TABLE IF NOT EXISTS `pm_presidents` (
 
   `id` int(11) NOT NULL auto_increment,
 
   `id` int(11) NOT NULL auto_increment,
   `IDparty` int(11) NOT NULL,
+
   `party_id` int(11) NOT NULL,
 
   `firstname` varchar(20) NOT NULL,
 
   `firstname` varchar(20) NOT NULL,
 
   `lastname` varchar(20)  NOT NULL,
 
   `lastname` varchar(20)  NOT NULL,
Line 125: Line 151:
 
   `leftoffice` date NOT NULL,
 
   `leftoffice` date NOT NULL,
 
   `income` decimal(14,2) NOT NULL,
 
   `income` decimal(14,2) NOT NULL,
 +
  `ctime` int(11) NOT NULL DEFAULT '0',
 +
  `mtime` int(11) NOT NULL DEFAULT '0',
 
   PRIMARY KEY  (`id`)
 
   PRIMARY KEY  (`id`)
 
);
 
);
  
INSERT INTO `tm_presidents` (`id`, `IDparty`, `firstname`, `lastname`, `tookoffice`, `leftoffice`, `income`) VALUES
+
INSERT INTO `pm_presidents` (`id`, `party_id`, `firstname`, `lastname`, `tookoffice`, `leftoffice`, `income`) VALUES
 
(1, 1, 'George', 'Washington', '1789-04-30', '1797-03-04', 135246.32),
 
(1, 1, 'George', 'Washington', '1789-04-30', '1797-03-04', 135246.32),
 
(2, 2, 'John', 'Adams', '1797-03-04', '1801-03-04', 236453.34),
 
(2, 2, 'John', 'Adams', '1797-03-04', '1801-03-04', 236453.34),
Line 173: Line 201:
 
</pre>
 
</pre>
  
And another query to tell Group-Office what the nextid of the presidents table is.
+
<b>Note:</b> We can insert this data manually into the database. Or this script can be executed automatically when installing the module. (See section Installation and upgrades)
  
 +
=== Building the models ===
 +
 +
We have 2 types of data that is going to be used on this module '''President''' and '''Party'''
 +
For this we will create to Model classes in the modules/presidents/models folder
 +
 +
'''Party.php'''
 
<pre>
 
<pre>
INSERT INTO `go_db_sequence` (seq_name, nextid) VALUES ('tm_presidents',42)
+
<?php
 +
/**
 +
* The Party model
 +
*
 +
* @property int $id
 +
* @property string $name
 +
*/
 +
class GO_Presidents_Model_Party extends GO_Base_Db_ActiveRecord {
 +
/**
 +
* Returns a static model of itself
 +
*
 +
* @param String $className
 +
* @return GO_Presidents_Model_Party
 +
*/
 +
public static function model($className=__CLASS__)
 +
{
 +
return parent::model($className);
 +
}
 +
 
 +
public function tableName() {
 +
return 'pm_parties';
 +
}
 +
 
 +
public function relations() {
 +
return array(
 +
'presidents' => array('type' => self::HAS_MANY, 'model' => 'GO_Presidents_Model_President', 'field' => 'party_id', 'delete' => true)
 +
);
 +
}
 +
}
 
</pre>
 
</pre>
  
At this point we could go on and create a GrindPanel, but without any PHP code our GridPanel is not going to do anything anyway.
+
Our model class will extend from GO_Base_Db_ActiveRecord. Also the name of the class is important and should be in the following format: '''GO_[modulename]_Model_[modelname]''' where model name should be the same as the filename.
  
So let's first take a look at the PHP part.
+
This is all there is to create a model class. The properties of the model are already known since the database table is specified in the tableName() function
  
=== PHP code ===
+
Also a relation is defined between the Party Model and the President model.
For this module we are going to need 3 PHP files:
+
Create the president model as follows:
* modules/testmodule/action.php
+
:Is called when we want to create or update a president.
+
* modules/testmodule/json.php
+
:Is called when we want to retrieve the president(s) or delete president(s).
+
* modules/testmodule/classes/president.class.inc.php
+
:Is the module class file and stores all the functions used by action.php and json.php
+
  
'''action.php:'''
+
'''President.php'''
 
<pre>
 
<pre>
 
<?php
 
<?php
 +
/**
 +
* The President model
 +
*
 +
* @property int $id
 +
* @property int $party_id
 +
* @property string $firstname
 +
* @property string $lastame
 +
* @property date $tookoffice
 +
* @property date $leftoffice
 +
* @property float $income
 +
*/
 +
class GO_Presidents_Model_President extends GO_Base_Db_ActiveRecord {
 +
 +
/**
 +
* Returns a static model of itself
 +
*
 +
* @param String $className
 +
* @return GO_Presidents_Model_President
 +
*/
 +
public static function model($className=__CLASS__)
 +
{
 +
return parent::model($className);
 +
}
 +
 +
protected function init() {
 +
$this->columns["income"]["gotype"]="number";
 +
return parent::init();
 +
}
 +
 +
public function tableName(){
 +
return 'pm_presidents';
 +
}
  
require_once("../../Group-Office.php");
+
public function relations(){
 +
return array(
 +
'party' => array('type'=>self::BELONGS_TO, 'model'=>'GO_Presidents_Model_Party', 'field'=>'party_id'), );
 +
}
  
//Authenticate the user
+
}
$GO_SECURITY->json_authenticate('testmodule');
+
</pre>
  
//Require the module class
+
That's all there is to creating models for this example module
require_once ($GO_MODULES->modules['testmodule']['class_path']."presidents.class.inc.php");
+
$presidents = new presidents();
+
  
$task=isset($_REQUEST['task']) ? $_REQUEST['task'] : '';
+
=== Creating the controllers ===
  
try{
+
The controllers we stick into the 'modules/presidents/controller' folder
  
switch($task)
+
Create the following 2 files:
{
+
  
/* {TASKSWITCH} */
+
'''PartyController.php'''
+
<pre>
case 'save_president':
+
<?php
/*
+
class GO_Presidents_Controller_Party extends GO_Base_Controller_AbstractModelController{
* This task is executed when we want to update a president with
+
protected $model = 'GO_Presidents_Model_Party';
* president_id = $_REQUEST['president_id'] or
+
* whenever we want to create a new president
+
* president_id = 0;
+
*/
+
$president_id=$president['id']=isset($_POST['president_id']) ? $_POST['president_id'] : 0;
+
$president['firstname']= $_POST['firstname'];
+
$president['lastname']= $_POST['lastname'];
+
$president['IDparty'] = $_POST['IDparty'];
+
$president['tookoffice'] = $_POST['tookoffice'];
+
$president['leftoffice'] = $_POST['leftoffice'];
+
$president['income'] = $_POST['income'];
+
   
+
if($president['id']>0)
+
{
+
// update an existing president
+
$president['income'] = Number::to_phpnumber($president['income']);
+
$president['tookoffice']=Date::to_input_format($president['tookoffice']);
+
$president['leftoffice']=Date::to_input_format($president['leftoffice']);
+
+
$presidents->update_president($president);
+
$response['success']=true;
+
 
+
}else
+
{
+
// create a new president
+
$president['income'] = Number::to_phpnumber($president['income']);
+
$president['tookoffice']=Date::to_input_format($president['tookoffice']);
+
$president['leftoffice']=Date::to_input_format($president['leftoffice']);
+
+
$president_id= $presidents->add_president($president);
+
$response['president_id']=$president_id;
+
$response['success']=true;
+
}
+
break;
+
+
}
+
}catch(Exception $e)
+
{
+
$response['feedback']=$e->getMessage();
+
$response['success']=false;
+
 
}
 
}
 
// return $response to the client in an JSON encoded string
 
echo json_encode($response);
 
 
</pre>
 
</pre>
  
'''json.php:'''
+
'''PresidentController.php'''
 
<pre>
 
<pre>
 
<?php
 
<?php
 +
class GO_Presidents_Controller_President extends GO_Base_Controller_AbstractModelController {
 +
 +
protected $model = 'GO_Presidents_Model_President';
  
require_once('../../Group-Office.php');
+
/**
 +
* Tell the controller to change some column values
 +
*/
 +
protected function formatColumns(GO_Base_Data_ColumnModel $columnModel) {
 +
$columnModel->formatColumn('party_id','$model->party->name');
 +
$columnModel->formatColumn('income_val','$model->income');
 +
return parent::formatColumns($columnModel);
 +
}
  
//Authenticate the user
+
/**
$GO_SECURITY->json_authenticate('testmodule');
+
* Display current value in combobox
 +
*/
 +
protected function remoteComboFields(){
 +
return array('party_id'=>'$model->party->name');
 +
}
 +
}
 +
</pre>
  
//Require the module class
+
To the PartyController there is not much to understand.
require_once ($GO_MODULES->modules['testmodule']['class_path'].'presidents.class.inc.php');
+
Extend from <b>GO_Base_Controller_AbstractModelController</b> Tell it what model class it should use and it knows how to Load, Save and Load JSON data for a grid.
$presidents = new presidents();
+
  
$task=isset($_REQUEST['task']) ? $_REQUEST['task'] : '';
+
In the President Controller 2 extra functions are added.
 +
<b>formatColumns()</b> Will make sure that in the datagrid won't show the id, but the name of the party that to president is member of.
  
try{
+
<b>remoteComboFields()</b> This will make sure that the Combobox is filled with the right data when the President Dialog (Edit form) is shown.
  
switch($task)
+
=== Designing the views ===
{
+
+
/* {TASKSWITCH} */
+
+
case 'president':
+
/*
+
* This task is executed when we need to edit a president
+
* with president_id = $_REQUEST['president_id']
+
*/
+
$president = $presidents->get_president($_REQUEST['president_id']);
+
$president['income']=Number::format($president['income']);
+
$response['data']=$president;
+
$response['success']=true;
+
break;
+
+
case 'presidents':
+
/*
+
* This task is executed when we need to delete 1 or more president(s) or
+
* whenever we want to retrieve the data of all the presidents
+
* (for example: to list them in our GridPanel)
+
*/
+
if(isset($_POST['delete_keys']))
+
/*
+
* $_POST['delete_keys'] holds the president_id(s) of
+
* all the president(s) we need to delete 1 by 1 
+
*/
+
{
+
try{
+
$response['deleteSuccess']=true;
+
$delete_presidents = json_decode($_POST['delete_keys']);
+
  
foreach($delete_presidents as $president_id)
+
Now for the fun part: The client-side of your module.
{
+
We'll create the views that display on the screen. This is what our end-users will actually see:
$presidents->delete_president($president_id);
+
}
+
}catch(Exception $e)
+
{
+
$response['deleteSuccess']=false;
+
$response['deleteFeedback']=$e->getMessage();
+
}
+
}
+
+
$sort = isset($_REQUEST['sort']) ? $_REQUEST['sort'] : 'id';
+
$dir = isset($_REQUEST['dir']) ? $_REQUEST['dir'] : 'ASC';
+
$start = isset($_REQUEST['start']) ? $_REQUEST['start'] : '0';
+
$limit = isset($_REQUEST['limit']) ? $_REQUEST['limit'] : '0';
+
$query = isset($_REQUEST['query']) ? '%'.$_REQUEST['query'].'%' : '';
+
+
$presidents->get_presidents($sort, $dir, $start, $limit);
+
$response['results']=array();
+
while($presidents->next_record())
+
{
+
$president = $presidents->record;
+
$president['tookoffice']=Date::format($president['tookoffice'],false);
+
$president['leftoffice']=Date::format($president['leftoffice'],false);
+
$response['results'][] = $president;
+
}
+
$response['total'] = $presidents->found_rows();
+
break;
+
+
case 'parties':
+
/*
+
* This task is executed when the combobox of our Dialog is activated
+
* it task returns a list of all the available parties
+
*/
+
$response['total'] = $presidents->get_parties();
+
$response['results']=array();
+
while($presidents->next_record())
+
{
+
$parties = $presidents->record;
+
$response['results'][] = $parties;
+
}
+
break;
+
  
}
+
We'll need 3 different view:
}catch(Exception $e)
+
# a Datagrid for displaying all records
{
+
# a Form dialog for editing a record
$response['feedback']=$e->getMessage();
+
# a Main panel where we will put the above.
$response['success']=false;
+
}
+
  
// return $response to the client in an JSON encoded string
+
We already created '''MainPanel.js''' in the modules/views/extjs3/ folder in the beginning of this guide.
echo json_encode($response);
+
Lets modify that file with the following code:
</pre>
+
  
'''presidents.class.inc.php:'''
+
'''MainPanel.js'''
 
<pre>
 
<pre>
<?php
+
// Creates namespaces to be used for scoping variables and classes so that they are not global.
 +
Ext.namespace('GO.presidents');
  
 
/*
 
/*
* Each module class file extends the db class
+
* This will add the module to the main tab-panel filled with all the modules
*/
+
*/
class presidents extends db {
+
GO.moduleManager.addModule(
 
+
'presidents', //Module alias
/**
+
GO.presidents.PresidentsGrid, //The main panel for this module
* Add a President
+
*
+
* @param Array $president Associative array of record fields
+
*
+
* @access public
+
* @return int New record ID created
+
*/
+
function add_president($president)
+
 
{
 
{
$president['id']=$this->nextid('tm_presidents');
+
title : 'Presidents', //Module name in start-menu
if($this->insert_row('tm_presidents', $president))
+
iconCls : 'go-module-icon-presidents' //The css class with icon for start-menu
{
+
return $president['id'];
+
}
+
return false;
+
 
}
 
}
 +
);
 +
</pre>
  
/**
+
This file will do no more then search for '''GO.presidents.PresidentsGrid''' to fill the modules MainPanel with.
* Update a President
+
*
+
* @param Array $president Associative array of record fields
+
*
+
* @access public
+
* @return bool True on success
+
*/
+
function update_president($president)
+
{
+
return $this->update_row('tm_presidents', 'id', $president);
+
}
+
  
/**
+
For our GridPanel we are going to use a Group-Office class that extends Ext.grid.GridPanel.
* Delete a President
+
*
+
* @param Int $president_id ID of the president
+
*
+
* @access public
+
* @return bool True on success
+
*/
+
function delete_president($president_id)
+
{
+
return $this->query("DELETE FROM tm_presidents WHERE id=?", 'i', $president_id);
+
}
+
  
/**
+
GO.grid.GridPanel is an extension of the default Ext grid and implements some basic Group-Office functionality like deleting items.
* Gets a President record
+
*
+
* @param Int $president_id ID of the president
+
*
+
* @access public
+
* @return Array Record properties
+
*/
+
function get_president($president_id)
+
{
+
$this->query("SELECT * FROM tm_presidents pr, tm_parties pa WHERE pr.IDparty = pa.IDparty AND pr.id=?", 'i', $president_id);
+
return $this->next_record();
+
}
+
+
/**
+
* Gets all Presidents
+
*
+
* @param Int $start First record of the total record set to return
+
* @param Int $offset Number of records to return
+
* @param String $sortfield The field to sort on
+
* @param String $sortorder The sort order
+
*
+
* @access public
+
* @return Int Number of records found
+
*/
+
function get_presidents($sortfield='id', $sortorder='ASC', $start=0, $offset=0)
+
{
+
$sql = "SELECT ";
+
if($offset>0)
+
{
+
$sql .= "SQL_CALC_FOUND_ROWS ";
+
}
+
$sql .= "* FROM tm_presidents pr, tm_parties pa WHERE pr.IDparty = pa.IDparty"
+
." ORDER BY ".$this->escape($sortfield.' '.$sortorder);
+
if($offset>0)
+
{
+
$sql .= " LIMIT ".intval($start).",".intval($offset);
+
}
+
  
return $this->query($sql);
+
We can now delete selected items in the grid with only the [delete] button.
}
+
  
/**
+
Lets create this 'PresidentsGrid' in the modules/presidents/views/extjs3/ folder
* Gets all Parties
+
 
*
+
'''PresidentsGrid.js'''
* @param Int $start First record of the total record set to return
+
<pre>
* @param Int $offset Number of records to return
+
GO.presidents.PresidentsGrid = function(config){
* @param String $sortfield The field to sort on
+
* @param String $sortorder The sort order
+
if(!config)
*
+
* @access public
+
* @return Int Number of records found
+
*/
+
function get_parties($sortfield='IDparty', $sortorder='ASC', $start=0, $offset=0, $partyName='')
+
 
{
 
{
$sql = "SELECT ";
+
config = {};
if($offset>0)
+
{
+
$sql .= "SQL_CALC_FOUND_ROWS ";
+
}
+
$sql .= "* FROM tm_parties";
+
$types='';
+
$params=array();
+
if(!empty($partyName))
+
{
+
$sql .= " WHERE partyName=?";
+
$types .= 's';
+
$params[]=$query;
+
}
+
$sql .= " ORDER BY ".$this->escape($sortfield.' '.$sortorder);
+
if($offset>0)
+
{
+
$sql .= " LIMIT ".intval($start).",".intval($offset);
+
}
+
return $this->query($sql, $types, $params);
+
 
}
 
}
  
 +
//Construct the Gridpanel with the above configuration
 +
GO.presidents.PresidentsGrid.superclass.constructor.call(this, config);
 
}
 
}
 +
 +
//Extend the PresidentsGrid from GridPanel
 +
Ext.extend(GO.presidents.PresidentsGrid, GO.grid.GridPanel,{
 +
 +
});
 
</pre>
 
</pre>
  
 +
This is the code that is needed for a data-grid but we haven't configured anything yet so it won't do anything for until we do.
  
=== Creating a GridPanel ===
+
For this grid we are going to configure the following properties:
With the PHP part done, we can now focus on the Client-side.
+
  
First off, we are going to create a GridPanel for listing data about the presidents.
+
# Some default properties like how to layout and we'll give it a title
 +
# Set the Edit dialog class we will use to edit a record.
 +
# A data-store from which the grid needs to pull its data
 +
# A column model, what column we want to display and how.
  
Create a file modules/testmodule/PresidentsGrid.js
+
Just before the 'Construct the Gridpanel with the above configuration' comment add the following code to configure what's listed above:
  
'''PresidentsGrid.js:'''
+
Some default values for the grid each commented with its use:
 
<pre>
 
<pre>
// Creates a namespace to be used for scoping variables and classes
+
config.title = GO.presidents.lang.presidents; //Title of this panel
Ext.namespace('GO.testmodule');
+
config.layout='fit'; //How to lay out the panel
 
+
/*
+
* This is the constructor of our PresidentsGrid
+
*/
+
GO.testmodule.PresidentsGrid = function(config){
+
 
+
if(!config)
+
{
+
config = {};
+
}
+
 
+
config.title = 'Presidents of the USA';
+
config.layout='fit';
+
 
config.autoScroll=true;
 
config.autoScroll=true;
 
config.split=true;
 
config.split=true;
config.store = new GO.data.JsonStore({
+
config.autoLoadStore=true;
/*
+
config.sm=new Ext.grid.RowSelectionModel();
* Here we store our remotely-loaded JSON data from json.php?task=presidents
+
config.loadMask=true; //Mask the grid when it is loading data
*/
+
url: GO.settings.modules.testmodule.url+ 'json.php',
+
baseParams: {
+
    task: 'presidents'
+
},
+
root: 'results',
+
id: 'id',
+
totalProperty:'total',
+
fields: ['id','firstname','lastname','partyid','partyName','tookoffice','leftoffice','income'],
+
remoteSort: true
+
});
+
 
</pre>
 
</pre>
  
Our PresidentsGrid uses a ColumnModel to display it's data.
+
Setting the editDialogClass this is rather important. We will create the PresidentDialog later on.
 +
Let's first finish configuring this grid.
 +
<pre>
 +
//This dialog will be opened when double clicking a row or calling showEditDialog()
 +
config.editDialogClass = GO.presidents.PresidentDialog;
 +
</pre>
  
A ColumnModel is a class initialized with an Array of column config objects
+
Setting the column model of the data-gird.
 
+
What column do you want to display and what should be left out?
An individual column's config object defines the header string, the Ext.data.Record field the column draws its data from, an optional rendering function to provide customized data formatting, and the ability to apply a CSS class to all cells in a column through its id config option.
+
For all the properties a ColumnModel can have please go to [http://docs.sencha.com/ext-js/3-4/#!/api/Ext.grid.ColumnModel Ext.grid.ColumnModel]
 
+
This is just the basic usage.
'''PresidentsGrid.js:'''
+
 
<pre>
 
<pre>
 
+
//Configuring the column model
/*
+
config.cm = new Ext.grid.ColumnModel({
* ColumnModel used by our PresidentsGrid
+
defaults:{
*/
+
sortable: true
var PresidentsColumnModel = new Ext.grid.ColumnModel(
+
},
[{
+
columns : [{
 
header: '#',
 
header: '#',
readOnly: true,
 
 
dataIndex: 'id',
 
dataIndex: 'id',
width: 50
+
hidden:true,
 +
sortable: false
 
},{
 
},{
header: 'First Name',
+
header: GO.presidents.lang.firstname,
dataIndex: 'firstname',
+
dataIndex: 'firstname'
width: 120
+
 
},{
 
},{
header: 'Last Name',
+
header: GO.presidents.lang.lastname,
dataIndex: 'lastname',
+
dataIndex: 'lastname'
width: 120
+
 
},{
 
},{
header: 'ID party',
+
header: GO.presidents.lang.party_id,
readOnly: true,
+
dataIndex: 'party_id'
dataIndex: 'partyid',
+
width: 50,
+
hidden: true
+
 
},{
 
},{
header: 'Party',
+
header: GO.presidents.lang.tookoffice,
dataIndex: 'partyName',   
+
dataIndex: 'tookoffice'
width: 120,
+
 
},{
 
},{
header: 'Took Office',
+
header: GO.presidents.lang.leftoffice,
dataIndex: 'tookoffice',
+
dataIndex: 'leftoffice'
width: 80
+
 
},{
 
},{
header: 'Left Office',
+
header: GO.presidents.lang.income,
dataIndex: 'leftoffice',
+
width: 80
+
},{
+
header: "Income",
+
 
dataIndex: 'income',
 
dataIndex: 'income',
width: 120
+
align: "right"
 
}]
 
}]
);
+
});
 +
</pre>
  
 +
As you can see in the above code we created a ColumnModel object and added it to the config
 +
For the header value of the columns that will display in our view
 +
 +
Defining a data-store for the grid:
 +
<pre>
 +
//Defining the data store for the grid
 +
config.store = new GO.data.JsonStore({
 +
url: GO.url('presidents/president/store'),
 +
fields: ['id','firstname','lastname','party_id','tookoffice','leftoffice','income'],
 +
remoteSort: true
 +
});
 
</pre>
 
</pre>
  
Here we give config some basic parameters about our ColumnModel and Grid and call the parent constructor.
+
<b>Note:</b> The url: GO.url('presidents/president/store') refers to the methode 'actionStore()' in the President Controller wich it inherits from the AbstractController class. The url will return a JSON string once called.
  
'''PresidentsGrid.js:'''
+
After setting the data-store we'll add the add the GridView component to the panel:
 
<pre>
 
<pre>
PresidentsColumnModel.defaultSortable= true;
+
//Adding the gridview to the grid panel
config.cm=PresidentsColumnModel;
+
 
+
 
config.view=new Ext.grid.GridView({
 
config.view=new Ext.grid.GridView({
emptyText: GO.lang['strNoItems']
+
emptyText: GO.lang.strNoItemse
 
});
 
});
 
config.sm=new Ext.grid.RowSelectionModel();
 
config.loadMask=true;
 
 
/*
 
* explicitly call the superclass constructor
 
*/
 
GO.testmodule.PresidentsGrid.superclass.constructor.call(this, config);
 
 
};
 
 
</pre>
 
</pre>
  
For our GridPanel we are going to use a Group-Office class that extends Ext.grid.GridPanel.
+
In this example we only set the emptyText value this is the text that gets displayed when no records are found by the data store. We are not using a string here but we'll reference to a global language string provided by Group-Office.
  
GO.grid.GridPanel is an extension of the default Ext grid and implements some basic Group-Office functionality like deleting items.
+
For fore info on the Grid view see the [http://docs.sencha.com/ext-js/3-4/#!/api/Ext.grid.GridView Ext.grid.GridView] docs
  
We can now delete selected items in the grid with only the [delete] button.
+
Our data-grid is now ready for usage. Let's not forget to add our new PresidentsGrid to scripts.txt or else it wont be loaded by the system and or MainPanel.js cant find the Grid
  
'''PresidentsGrid.js:'''
+
'''scripts.txt:'''
 
<pre>
 
<pre>
/*
+
modules/presidents/views/Extjs3/PresidentsGrid.js
* Extend the base class
+
modules/presidents/views/Extjs3/MainPanel.js
*/
+
</pre>
Ext.extend(GO.testmodule.PresidentsGrid, GO.grid.GridPanel,{
+
  
loaded : false,
+
<b>Note:</b> As the PresidentGrid class is used in MainPanel.js the PresidentsGrid.js file needs the be loaded first.
  
afterRender : function()
+
The data-grid should now work and display the presidents of the United States. Note that because of the 'formatColumns()' method we made in the PresidentController not the ID of a party will be displayed in the grid but the name of the party.
{
+
GO.testmodule.PresidentsGrid.superclass.afterRender.call(this);
+
+
if(this.isVisible())
+
{
+
this.onGridShow();
+
}
+
},
+
  
onGridShow : function(){
+
=== Creating a Dialog ===
if(!this.loaded && this.rendered)
+
{
+
this.store.load();
+
this.loaded=true;
+
}
+
},
+
   
+
});
+
</pre>
+
  
Let's not forget to add our new grid to scripts.txt.
+
Now we are going to create a Dialog which will make editing presidents very easy.
  
'''scripts.txt:'''
+
Create a file modules/president/module/PresidentDialog.js
 +
 
 +
'''PresidentDialog.js:'''
 
<pre>
 
<pre>
modules/testmodule/MainPanel.js
+
GO.presidents.PresidentDialog = Ext.extend(GO.dialog.TabbedFormDialog , {
modules/testmodule/PresidentsGrid.js
+
</pre>
+
  
In the constructor in MainPanel.js we can now create an object of our PresidentsGrid.
+
initComponent : function(){
 +
 +
Ext.apply(this, {
 +
titleField:'lastname',
 +
goDialogId:'president',
 +
title:GO.presidents.lang.president,
 +
width: 300,
 +
height: 280,
 +
formControllerUrl: 'presidents/president'
 +
});
 +
 +
GO.presidents.PresidentDialog.superclass.initComponent.call(this);
 +
}
  
'''MainPanel.js:'''
 
<pre>
 
var centerPanel = new GO.testmodule.PresidentsGrid({
 
region:'center',
 
title:GO.lang.menu,
 
autoScroll:true,
 
width:250,
 
split:true
 
 
});
 
});
 +
</pre>
  
config.items=[
+
This will create an empty dialog that will open when we double click a row in the grid. But it doesn't work yet. This is because we haven't loaded the PresidentDialog.js file. Lets do this by changing your 'scripts.txt' file again.
centerPanel
+
];
+
  
config.layout='border';
+
'''scripts.txt:'''
 +
<pre>
 +
modules/presidents/views/Extjs3/PresidentsGrid.js
 +
modules/presidents/views/Extjs3/PresidentDialog.js
 +
modules/presidents/views/Extjs3/MainPanel.js
 
</pre>
 
</pre>
  
 +
=== Adding some language strings ===
  
=== Creating a Dialog ===
+
In the PresidentDialog.js file you can see that the <b>title</b> property of the dialog is set to <b>GO.presidents.lang.president</b> This language string doesn't exists yet. So let's add some translatable strings that we can use in our PresidentDialog.
We now are going to create a Dialog which will make editing presidents very easy.
+
  
Create a file modules/testmodule/PresidentDialog.js
+
Open up modules/language/en.php and write the following:
  
'''PresidentDialog.js:'''
+
'''en.php:'''
 
<pre>
 
<pre>
// Creates a namespace to be used for scoping variables and classes
+
$l = array(
Ext.namespace('GO.testmodule');
+
'name' => ' Presidents', //this name will appear in the Start menu -> modules section
 +
'description' => 'A simple test module', //this description will appear in the Start menu -> modules section
  
/*
+
'presidents' => 'Presidents',
* This is the constructor of our PresidentDialog
+
'president' => 'President',
*/
+
'party_id' => 'Party',
GO.testmodule.PresidentDialog = function(config){
+
+
if(!config)
+
{
+
config={};
+
}
+
+
this.buildForm();
+
+
var focusFirstField = function(){
+
this.presidentPanel.items.items[0].focus();
+
};
+
+
config.maximizable=true;
+
config.layout='fit';
+
config.modal=false;
+
config.resizable=false;
+
config.width=300;
+
config.height=250;
+
config.closeAction='hide';
+
config.title= 'President Dialog';
+
config.items= this.formPanel;
+
config.focus= focusFirstField.createDelegate(this);
+
config.buttons=[{
+
text: GO.lang['cmdOk'],
+
handler: function(){
+
this.submitForm(true);
+
},
+
scope: this
+
},{
+
text: GO.lang['cmdApply'],
+
handler: function(){
+
this.submitForm();
+
},
+
scope:this
+
},{
+
text: GO.lang['cmdClose'],
+
handler: function(){
+
this.hide();
+
},
+
scope:this
+
}
+
];
+
  
/*
+
'tookoffice' => 'Entering Office',
* explicitly call the superclass constructor
+
'leftoffice' => 'Leaving Office',
*/
+
'income' => 'Income',
GO.testmodule.PresidentDialog.superclass.constructor.call(this, config);
+
'firstname' => 'First name',
+
'lastname' => 'Last name',
this.addEvents({'save' : true});
+
);
 
+
}
+
 
</pre>
 
</pre>
  
Extend the base class Ext.Window with a few more functions.
+
<b>GO.presidents.lang.president</b> in the title of our PresidentDialog will now display: "President"
 +
The other language strings you see will be used for the form that will be created inside the dialog.
  
'''PresidentDialog.js:'''
+
=== Creating a form inside the dialog ===
<pre>
+
/*
+
* Extend the base class
+
*/
+
Ext.extend(GO.testmodule.PresidentDialog, Ext.Window,{
+
+
show : function (president_id) {
+
  
if(!this.rendered)
+
In the PresidentDialog.js file we have created a PresidentDialog extended from TabbedFormDialog and overwritten the function initComponents and set some properties there.
this.render(Ext.getBody());
+
+
if(!president_id)
+
{
+
president_id=0;
+
}
+
+
this.setPresidentID(president_id);
+
+
if(this.president_id>0)
+
{
+
this.formPanel.load({
+
url : GO.settings.modules.testmodule.url+'json.php',
+
+
success:function(form, action)
+
{
+
this.partyName.setRemoteText(action.result.data.partyName);
+
GO.testmodule.PresidentDialog.superclass.show.call(this);
+
},
+
failure:function(form, action)
+
{
+
Ext.Msg.alert(GO.lang['strError'], action.result.feedback)
+
},
+
scope: this
+
});
+
} else
+
{
+
this.formPanel.form.reset();
+
+
GO.testmodule.PresidentDialog.superclass.show.call(this);
+
}
+
},
+
+
setPresidentID : function(president_id)
+
{
+
this.formPanel.form.baseParams['president_id']=president_id;
+
this.president_id=president_id;
+
},
+
+
submitForm : function(hide){
+
this.formPanel.form.submit(
+
{
+
url:GO.settings.modules.testmodule.url+'action.php',
+
params: {'task' : 'save_president'},
+
waitMsg:GO.lang['waitMsgSave'],
+
success:function(form, action){
+
this.fireEvent('save', this);
+
+
if(hide)
+
{
+
this.hide();
+
}else
+
{
+
if(action.result.president_id)
+
{
+
this.setPresidentID(action.result.president_id);
+
+
}
+
}
+
},
+
failure: function(form, action) {
+
if(action.failureType == 'client')
+
{
+
Ext.MessageBox.alert(GO.lang['strError'], GO.lang['strErrorsInForm']);
+
} else {
+
Ext.MessageBox.alert(GO.lang['strError'], action.result.feedback);
+
}
+
},
+
scope: this
+
});
+
+
},
+
</pre>
+
  
Here we create a Panel that will represent our actual form for editing presidents.
+
We are going to overwrite one more method in the PresidentDialog.
 +
Add a comma (,) at the end of the initComponent function and add the following code afterwards:
  
 
'''PresidentDialog.js:'''
 
'''PresidentDialog.js:'''
<pre>
+
<pre>
buildForm : function () {
+
buildForm : function () {
+
 
this.presidentPanel = new Ext.Panel({
+
this.propertiesPanel = new Ext.Panel({
border: false,
+
title:GO.lang.strProperties,
cls:'go-form-panel',waitMsgTarget:true,
+
cls:'go-form-panel',
 
layout:'form',
 
layout:'form',
autoScroll:true,
+
items:[{
items: [{  
+
name: 'firstname',
+
 
xtype: 'textfield',
 
xtype: 'textfield',
fieldLabel: 'First Name',
+
name: 'firstname',
 +
width:300,
 
anchor: '100%',
 
anchor: '100%',
 +
maxLength: 100,
 
allowBlank:false,
 
allowBlank:false,
maskRe: /([a-zA-Z0-9\s]+)$/
+
fieldLabel: GO.presidents.lang.firstname
},{
+
},
 +
{
 +
xtype: 'textfield',
 
name: 'lastname',
 
name: 'lastname',
xtype: 'textfield',
+
fieldLabel: GO.presidents.lang.lastname,
fieldLabel: 'Last Name',
+
anchor: '100%'
anchor: '100%',
+
},
allowBlank:false,
+
{
maskRe: /([a-zA-Z0-9\s]+)$/
+
xtype:'combo',
},this.partyName = new GO.form.ComboBox({
+
fieldLabel: GO.presidents.lang.party_id,
hiddenName:'IDparty',
+
hiddenName:'party_id',
fieldLabel:'Party',
+
anchor:'100%',
anchor:'100%',    
+
emptyText:GO.lang.strPleaseSelect,
 
store: new GO.data.JsonStore({
 
store: new GO.data.JsonStore({
url: GO.settings.modules.testmodule.url+ 'json.php',
+
url: GO.url('presidents/party/store'),
baseParams: {    
+
baseParams: {
task: 'parties'
+
permissionLevel:GO.permissionLevels.write
},
+
},
root:'results',
+
fields: ['id', 'name']
id:'partyName',
+
totalProperty:'total',
+
fields: ['IDparty', 'partyName'],
+
remoteSort: true
+
 
}),
 
}),
valueField:'IDparty',
+
valueField:'id',
displayField:'partyName',
+
displayField:'name',
mode: 'remote',
+
 
triggerAction: 'all',
 
triggerAction: 'all',
 +
editable: true,
 
forceSelection: true,
 
forceSelection: true,
selectOnFocus:true,
 
 
allowBlank: false
 
allowBlank: false
}),this.tookoffice = new Ext.form.DateField({
+
},
 +
{
 +
xtype: 'datefield',
 
name: 'tookoffice',
 
name: 'tookoffice',
fieldLabel: 'Entering Office',
+
fieldLabel: GO.presidents.lang.tookoffice,
format: GO.settings['date_format'],
+
 
allowBlank: false,
 
allowBlank: false,
anchor:'100%'
+
anchor: '100%'
}),this.leftoffice = new Ext.form.DateField({
+
},
 +
{
 +
xtype: 'datefield',
 
name: 'leftoffice',
 
name: 'leftoffice',
fieldLabel: 'Leaving Office',
+
fieldLabel: GO.presidents.lang.leftoffice,    
format: GO.settings['date_format'],    
+
 
allowBlank: false,
 
allowBlank: false,
 
anchor: '100%'
 
anchor: '100%'
}),this.income = new GO.form.NumberField({
+
},
 +
{
 +
xtype: 'numberfield',
 
name: 'income',
 
name: 'income',
fieldLabel: 'Income',
+
value: GO.util.numberFormat(0),
 +
fieldLabel: GO.presidents.lang.income,
 
allowBlank: false,
 
allowBlank: false,
allowDecimals: true,
+
anchor: '100%'
allowNegative: false
+
}]
})]
+
 
});
 
});
  
 
+
this.addPanel(this.propertiesPanel);
var items = [this.presidentPanel];
+
   
+
this.formPanel = new Ext.form.FormPanel({
+
waitMsgTarget:true,
+
url: GO.settings.modules.testmodule.url+'json.php',
+
border: false,
+
baseParams: {task: 'president'},
+
items: items
+
});    
+
 
}
 
}
});
 
 
</pre>
 
</pre>
  
We define a handler on a doubleclick event: rowdblclick.
+
This will add a panel to our PresidentDialog and add 6 form-fields inside.
 +
The combo/dropdown field is a bit tricky.  
  
This will bring up the Dialog when we doubleclick on the grid and it tells the store to reload it's data when we save the Dialog.
+
It's needs to know where to pull the data that it is going to list from.
 +
for this we set the store property of the field and fill it with a <b>JsonStore</b> object. This object needs to know a controller action that returns JSON data. We'll refer to the 'store' action in the PartyController we've created earlier. Also we specify the fields the be used for the key and value of the dropdown menu. In this case 'id' and 'name'.
  
Add this within the MainPanel constructor.
+
When double clicking a row the President dialog will now show a form with the above defined fields.
 
+
'''MainPanel.js:'''
+
<pre>
+
centerPanel.on('rowdblclick', function(grid, rowIndex)
+
{
+
this.presidentDialog = new GO.testmodule.PresidentDialog();
+
  this.presidentDialog.on('save', function(){
+
grid.store.reload();
+
}, this);
+
+
var record = grid.getStore().getAt(rowIndex);
+
this.presidentDialog.show(record.data.id);
+
+
}, this);
+
</pre>
+
 
+
To conclude this part we add our PresidentDialog.js to scripts.txt.
+
 
+
'''scripts.txt:'''
+
<pre>
+
modules/testmodule/MainPanel.js
+
modules/testmodule/PresidentsGrid.js
+
modules/testmodule/PresidentDialog.js
+
</pre>
+
  
 
In the next section we are going to add a toolbar which will allow us to create new presidents.
 
In the next section we are going to add a toolbar which will allow us to create new presidents.
 
  
 
=== Adding a Toolbar ===
 
=== Adding a Toolbar ===
Line 953: Line 662:
 
We are now going to add a toolbar in the top with links to create or delete a president.
 
We are now going to add a toolbar in the top with links to create or delete a president.
  
All of the following code has to be added within the MainPanel constructor.
+
All of the following code has to be added within the PresidentsGrid configuration.
  
'''MainPanel.js:'''
+
'''PresidentsGrid.js:'''
 
<pre>
 
<pre>
var northPanel = new Ext.Panel({
+
//Setup a toolbar for the grid panel
region: 'north',
+
config.tbar = new Ext.Toolbar({
baseCls:'x-plain',
+
cls:'go-head-tb',
split: true,
+
items: [
resizable:false,
+
{
tbar: new Ext.Toolbar({
+
iconCls: 'btn-add',
cls:'go-head-tb',
+
text: GO.lang['cmdAdd'],
items: [{
+
cls: 'x-btn-text-icon',
iconCls: 'btn-add',
+
handler: function(){
text: GO.lang['cmdAdd'],
+
this.showEditDialog();
cls: 'x-btn-text-icon',
+
},
handler: function(){
+
scope: this
this.presidentDialog = new GO.testmodule.PresidentDialog();
+
},
this.presidentDialog.on('save', function(){
+
{
centerPanel.getStore().reload();
+
iconCls: 'btn-delete',
}, this);
+
text: GO.lang['cmdDelete'],
this.presidentDialog.show();
+
cls: 'x-btn-text-icon',
},
+
handler: function(){
scope: this
+
this.deleteSelected();
},{
+
},
iconCls: 'btn-delete',
+
scope: this
text: GO.lang['cmdDelete'],
+
},
cls: 'x-btn-text-icon',
+
GO.lang.strSearch + ':',
handler: function(){
+
new GO.form.SearchField({
centerPanel.deleteSelected();
+
store: config.store,
},
+
width:320
scope: this
+
})
}]
+
]
})
+
});
});
+
 
</pre>
 
</pre>
  
Show the toolbar at the top of the panel.
+
Setting a <b>Ext.Toolbar</b> object to the tbar property of the GridPanel will render a toolbar on top of the grid. In this example we'll add 4 items to the toolbar:
  
'''MainPanel.js:'''
+
# An add button that will call the PresidentDialog but this time it's empty and can be used to add a new record
<pre>
+
# A delete button that will delete the selected row.
config.items=[
+
# A string
northPanel,
+
# A search Field.
centerPanel
+
 
];
+
For more information about the see the [http://docs.sencha.com/ext-js/3-4/#!/api/Ext.Toolbar Ext.Toolbar] docs
</pre>
+
  
  
Line 1,004: Line 711:
 
This one is very easy to do because we used a GO.grid.GridPanel instead of the Ext one.
 
This one is very easy to do because we used a GO.grid.GridPanel instead of the Ext one.
  
We only need to add 1 line to our PresidentsGrid constructor and our PHP takes care of the rest.
+
We only need to add 1 line to our PresidentsGrid configuration and our PHP takes care of the rest.
  
PresidentsGrid.js:
+
'''PresidentsGrid.js:'''
 
<pre>
 
<pre>
 
/* config.paging is an added functionality by Group-Office
 
/* config.paging is an added functionality by Group-Office
Line 1,013: Line 720:
 
config.paging=true,
 
config.paging=true,
 
</pre>
 
</pre>
 
  
 
=== Adding some style ===
 
=== Adding some style ===
Line 1,021: Line 727:
 
Create Directories:
 
Create Directories:
 
<pre>
 
<pre>
modules/testmodule/themes/
+
modules/presidents/themes/
modules/testmodule/themes/Default/
+
modules/presidents/themes/Default/
 
</pre>
 
</pre>
  
Now we create the file modules/testmodule/themes/Default/style.css
+
Now we create the file modules/presidents/themes/Default/style.css
  
 
Most modules will want to have it's own module-icon, so we add this default rule for later use.
 
Most modules will want to have it's own module-icon, so we add this default rule for later use.
Line 1,032: Line 738:
 
<pre>
 
<pre>
 
/* This class is used to show the icon in the main tabpanel of GO */
 
/* This class is used to show the icon in the main tabpanel of GO */
.go-module-icon-testmodule {
+
.go-module-icon-presidents {
 
}
 
}
 
</pre>
 
</pre>
  
In our Grid we can't change the ID's of presidents. To make it apparent that this field is treated differently we will add a renderer that sets the cell css style so it will use a different background.
+
In our Grid we can't change the ID's of presidents. To make it apparent that this field is treated differently we will add a renderer that sets the cell CSS style so it will use a different background.
  
To do this we need to change the corresponding column config object in our PresidentsColumnModel.
+
To do this we need to change the corresponding column config object in our PresidentsGrid ColumnModel.
  
 
'''PresidentsGrid.js:'''
 
'''PresidentsGrid.js:'''
 
<pre>
 
<pre>
header: '#',
+
{
readOnly: true,
+
header: '#',
dataIndex: 'id',
+
readOnly: true,
renderer: function(value, cell){  
+
dataIndex: 'id',
cell.css = "readonlycell";
+
renderer: function(value, cell){  
return value;
+
cell.css = "readonlycell";
},
+
return value;
width: 50
+
},
 +
width: 50
 +
}
 
</pre>
 
</pre>
  
Line 1,065: Line 773:
 
'''PresidentsGrid.js:'''
 
'''PresidentsGrid.js:'''
 
<pre>
 
<pre>
header: "Income",
+
{
dataIndex: 'income',
+
header: "Income",
width: 120,
+
dataIndex: 'income',
renderer: function(value, cell){  
+
width: 120,
var str = '';
+
renderer: function(value, metaData, record){
if(value > 1000000){
+
if(record.data.income_val > 1000000){
    str = "<span style='color:#336600;'>$ " + value + "</span>";
+
metaData.attr = 'style="color:#336600;"';
} else if (value > 100000){
+
} else if (record.data.income_val > 100000){
    str = "<span style='color:#FF9900;'>$ " + value + "</span>";
+
metaData.attr = 'style="color:#FF9900;"';
} else {
+
} else {
    str = "<span style='color:#CC0000;'>$ " + value + "</span>";
+
metaData.attr = 'style="color:#CC0000;"';
}
+
}
return str;
+
return "$ "+value;
 +
},
 +
align: "right"
 
}
 
}
 
</pre>
 
</pre>
Line 1,084: Line 794:
  
 
We now have a fully functional module which lets us add, update and delete presidents.
 
We now have a fully functional module which lets us add, update and delete presidents.
 
  
 
==Putting custom fields in module items==
 
==Putting custom fields in module items==
Create a file called 'scripts.inc.php' in the module directory. A script with this name will automatically be included by the Group-Office framework.
 
Put this in it:
 
  
 +
This will only work with the professional version and you must have the Custom fields module installed. To create custom fields for a model, we must create a custom field module. We'll place it in the following directory:
 +
modules/presidents/customfields/model/
 +
 +
'''President.php'''
 
<pre>
 
<pre>
<?php
+
class GO_Presidents_Customfields_Model_President extends GO_Customfields_Model_AbstractCustomFieldsRecord{
require_once($GO_MODULES->modules['customfields']['class_path'].'customfields.class.inc.php');
+
/**
$cf = new customfields();
+
* Returns a static model of itself
echo $cf->get_javascript(3, 'Companies');
+
*
</pre>
+
* @param String $className
 +
* @return GO_Presidents_Model_CustomFieldsRecord
 +
*/
 +
public static function model($className=__CLASS__)
 +
{
 +
return parent::model($className);
 +
}
  
The link type in this case is 3. The name for these custom fields is 'Companies'. Now we can mange the fields in the Custom fields admin module.
+
public function extendsModel(){
Now you can add them to a tabpanel for example:
+
return "GO_Presidents_Model_President";
In this example variable items is an array of Ext Panels:
+
 
+
<pre>
+
if(GO.customfields && GO.customfields.types["3"])
+
{
+
for(var i=0;i<GO.customfields.types["3"].panels.length;i++)
+
{  
+
items.push(GO.customfields.types["3"].panels[i]);
+
 
}
 
}
 
}
 
}
 
</pre>
 
</pre>
  
Or we can add it to an Ext Xtemplate:
+
Our President model still needs to know what the classname of the CustomerFields model is. For this we'll add the Following method the our President <b>Model</b>:
  
 +
'''President.php'''
 
<pre>
 
<pre>
if(GO.customfields)
+
public function customfieldsModel(){
{
+
return "GO_Presidents_Customfields_Model_President";
template +=GO.customfields.displayPanelTemplate;
+
 
}
 
}
 
</pre>
 
</pre>
  
Where the var template is a basic Ext Xtemplate config string.
+
Also the customer fields we specify need to be saved into the database For this we'll create a table named 'cf_pm_presidents':
 
+
We must supply the JSON data for the template and/or the formpanels.
+
In json.php for the Xtemplate:
+
  
 
<pre>
 
<pre>
if(isset($GO_MODULES->modules['customfields']))
+
CREATE TABLE IF NOT EXISTS `cf_pm_presidents` (
{
+
  `model_id` int(11) NOT NULL DEFAULT '0',
require_once($GO_MODULES->modules['customfields']['class_path'].'customfields.class.inc.php');
+
  PRIMARY KEY (`model_id`)
$cf = new customfields();
+
);
$response['data']['customfields']=
+
 
$cf->get_all_fields_with_values(
+
INSERT INTO `cf_pm_presidents` (`model_id`) VALUES (1);
$GO_SECURITY->user_id, 3, $company_id);
+
}
+
 
</pre>
 
</pre>
  
In json.php for the FormPanels:
+
<b>Note:</b> The table name needs to be the same as the table name where the records are saved but prefixed with ' cf_'
  
<pre>
+
This is all there is to add custom field functionality. After this we still need to change our views to display the input fields in our edit dialog. Add the following line just before the <b>initComponent</b> function
if(isset($GO_MODULES->modules['customfields']))
+
{
+
require_once($GO_MODULES->modules['customfields']['class_path'].'customfields.class.inc.php');
+
$cf = new customfields();
+
$values = $cf->get_values($GO_SECURITY->user_id, 3, $company_id);
+
$response['data']=array_merge($response['data'], $values);
+
}
+
</pre>
+
  
We must save the submitted values in action.php:
+
'''PresidentDialog.php'''
 
<pre>
 
<pre>
if(isset($GO_MODULES->modules['customfields']))
+
customFieldType : "GO_Presidents_Model_President",
{
+
require_once($GO_MODULES->modules['customfields']['class_path'].'customfields.class.inc.php');
+
$cf = new customfields();
+
$cf->update_fields($GO_SECURITY->user_id, $company_id, 3, $_POST);
+
}
+
 
</pre>
 
</pre>
  
That's it. Now you have your own custom fields in a module.
+
This should make the customfields work all we have to do is create some fields.
You module must create on table for the custom fields. In this case:
+
Install the <b>Custom fields</b> module if you haven't done this already. This module is only available in the pro version of Group-Office.
  
 +
When it's installed it will show up in the Admin menu of the start-menu. Start it and in the left sidebar you will see the the President Model is there. Click it and add a Category. Name it: 'Appearance'.
  
<pre>
+
We'll add the following field to this category:
CREATE TABLE IF NOT EXISTS `cf_3` (
+
Name: Hair color, Type: Text
  `link_id` int(11) NOT NULL default '0',
+
Name: Birthday, Type: Date
  PRIMARY KEY  (`link_id`)
+
Name: Glasses, Type: Checkbox
) ENGINE=MyISAM;
+
  
 +
To check if the fields will work. We first need to reload Group Office. Refresh the page, go to the Presidents module and edit a record. See the extra tab Appearance in to PresidentDialog?
 +
 +
It's also possible to display customfields in the data-grid if you like. The reason that the fields aren't displaying in the grid yet is because the DataStore of the grid doesn't know what the model type is. It only has attributes.
 +
 +
Lets change to config.store attribute of the configuration of our PresidentGrid
 +
'''PresidentGrid.js:'''
 +
<pre>
 +
//Defining the data store for the grid
 +
config.store = new GO.data.JsonStore({
 +
url: GO.url('presidents/president/store'),
 +
fields: ['id','firstname','lastname','party_id','tookoffice','leftoffice','income', 'income_val'],
 +
remoteSort: true,
 +
model: 'GO_Presidents_Model_President'
 +
});
 
</pre>
 
</pre>
  
 +
All your defines customfields will be visible into the data-grid.
 +
<b>Note:</b> The following fieldtypes will never be rendered in the grid: 'textarea','html', 'heading', 'infotext'
  
 
==Making your items linkable==
 
==Making your items linkable==
  
#Assign a link_type for your item. eg. An appointment has link_type: 1 a contact has link_type 2. You can find already used link types in classes/base/links.class.inc.php. Use a number higher then 100 so that it will never conflicted with new types officially released by Intermesh. MainPanel.js:
+
In Group-Office we can link one item to another. If your newly create module has an item that should be linkable we add the following overwrite the hasLinks() method in our model
#;<pre>
+
#;/*
+
#; * If your module has a linkable item, you should add a link handler like this.
+
#; * The index (no. 12 in this case) should be a unique identifier of your item.
+
#; * See classes/base/links.class.inc for an overview.
+
#; *
+
#; * Basically this function opens a task window when a user clicks on it from a
+
#; * panel with links.
+
#; */
+
#;GO.linkHandlers[12]=function(id){
+
#;
+
#; var taskDialog = new GO.mymodule.TaskDialog();
+
#;
+
#; taskDialog.show({task_id: id});
+
#;}
+
#;</pre>
+
#Create a style for the icon to display next to your linked item:
+
#; <pre>.go-link-icon-100 {
+
#; background-image: url('images/16x16/icon-notepad.png') !important;
+
#; width:16px;
+
#; height:16px;
+
#; }</pre>
+
#; where 100 is the link type
+
  
More information will follow. You can have a look at modules/notes/NoteDialog.js on how to add a links panel.
+
We are going to make the presidents linkable, for this we add the following methods to our President Model:
  
 +
'''President.php'''
 +
<pre>
 +
public function hasLinks() {
 +
return true;
 +
}
  
==Maintaining modules==
+
public function getFullname() {
When bugs are fixed or new features are made to a module the database has to be updated in some cases. Group-Office has a very nice mechanism to maintain modules. You can create SQL and PHP scripts for installing, updating and removing modules.
+
return $this->firstname . " " . $this->lastname;
 +
}
  
A module must have an "install" directory with the following files:
+
protected function getCacheAttributes() {
 
+
return array("name"=>$this->fullname, "description"=>$this->party->name);
*install.inc.php: Is executed when a module is installed
+
}
*uninstall.inc.php: Is executed when a modules is uninstalled
+
</pre>
*install.sql: Plain SQL code that is executed when a module is installed
+
*uninstall.sql: Plain SQL code that is executed when a module is uninstalled
+
*updates.inc.php: An array of SQL queries and scripts that are executed on an upgrade.
+
  
For eg. if you put the following in updates.inc.php:
+
Next our database needs a table where to links for the presidents will be saved. We execute the following query on our database:
  
 +
'''SQL'''
 
<pre>
 
<pre>
<?php
+
CREATE TABLE IF NOT EXISTS `go_links_pm_presidents` (
$updates[] = "ALTER TABLE `ab_mailing_contacts` ADD `mail_sent` ENUM( '0', '1' ) NOT NULL ;";
+
  `id` int(11) NOT NULL,
$updates[] = "ALTER TABLE `ab_mailing_contacts` ADD `status` VARCHAR( 100 ) NOT NULL ";
+
  `folder_id` int(11) NOT NULL,
$updates[] = "script:1.inc.php";
+
  `model_id` int(11) NOT NULL,
 +
  `model_type_id` int(11) NOT NULL,
 +
  `description` varchar(100) DEFAULT NULL,
 +
  `ctime` int(11) NOT NULL,
 +
  KEY `link_id` (`model_id`,`model_type_id`),
 +
  KEY `id` (`id`,`folder_id`),
 +
  KEY `ctime` (`ctime`)
 +
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
</pre>
 
</pre>
  
When the administrator runs install/update.php Group-Office will check how many queries of updates.inc.php it has already performed. If the number is lower then the amount of queries in the file then it will perform those queries and store the amount of queries performed in the database.
+
<b>Note:</b> the table name is the same as where the president records are stored but prefixed with 'go_links_'
  
Sometimes you will need some PHP scripting in the update process. Then you can put script:filename.inc.php in the updates.inc.php file. This script must be put in the folder "updatescripts".
+
In the PresidentDialog we will add a field to add links to the record after the income numberfield we'll add a new Go.form.SelectLink componentent. To code will look like this:
  
<b>Important!</b> Queries will be performed from top to bottom. So put new queries at the bottom of the updates.inc.php file.
+
'''PresidentDialog.js'''
 +
<pre>
 +
{
 +
xtype: 'numberfield',
 +
name: 'income',
 +
value: GO.util.numberFormat(0),
 +
fieldLabel: GO.presidents.lang.income, 
 +
allowBlank: false,
 +
anchor: '100%'
 +
},
 +
new GO.form.SelectLink({
 +
anchor:'100%'
 +
})
 +
</pre>
  
 +
This is all what's needed for adding other linkable Group-Office record to the president record. But where are they? Let's make a panel to display the links. We'll place this on the right side of the grid.
  
==Generate code for a module==
+
The MainPanel will be seperated into 2 divided panels. Our PresidentGrid in the middle and the (still to build) PresidentPanel on the right. Let's build the PresidentPanel
Because the base of a lot of code is the same. We created a script that can generate the grids and dialogs of database tables. The idea is that you create tables for modules first and then generate code to manipulate the interface.
+
  
The modulegenerator is in the "tools" package. Get it from Sourceforge here:
+
'''PresidentPanel.js'''
 +
<pre>
 +
GO.presidents.PresidentPanel = Ext.extend(GO.DisplayPanel,{
 +
model_name : "GO_Presidents_Model_President",
 +
noFileBrowser : true,
 +
stateId : 'pm-president-panel',
 +
 +
editHandler : function(){
 +
if(!GO.presidents.presidentDialog)
 +
GO.presidents.presidentDialog = new GO.presidents.PresidentDialog();
 +
GO.presidents.presidentDialog.show(this.link_id);
 +
},
 +
 +
initComponent : function(){
 +
 +
this.loadUrl=GO.url('presidents/president/display');
 +
 +
this.template =
 +
'<table class="display-panel" cellpadding="0" cellspacing="0" border="0">'+
 +
'<tr>'+
 +
'<td colspan="2" class="display-panel-heading">{firstname} {lastname}</td>'+
 +
'</tr>'+
 +
'<tr>'+
 +
'<td>ID:</td>'+
 +
'<td>{id}</td>'+
 +
'</tr>'+
 +
'<tr>'+
 +
'<td>'+GO.presidents.lang.party_id+':</td>'+
 +
'<td>{partyName}</td>'+
 +
'</tr>'+
 +
'<tr>'+
 +
'<td>'+GO.presidents.lang.tookoffice+':</td>'+
 +
'<td>{tookoffice}</td>'+
 +
'</tr>'+
 +
'<tr>'+
 +
'<td>'+GO.presidents.lang.leftoffice+':</td>'+
 +
'<td>{leftoffice}</td>'+
 +
'</tr>'+
 +
'<tr>'+
 +
'<td>'+GO.presidents.lang.income+':</td>'+
 +
'<td>{income}</td>'+
 +
'</tr>'+
 +
'</table>';
 +
 +
if(GO.customfields)
 +
{
 +
this.template +=GO.customfields.displayPanelTemplate;
 +
}
  
http://sourceforge.net/project/showfiles.php?group_id=76359
+
if(GO.tasks)
 +
this.template +=GO.tasks.TaskTemplate;
  
Extract it and enter the directory. You must have the php5 command line interface installed (On Debian/Ubuntu: apt-get install php5-cli).
+
if(GO.calendar)
 +
this.template += GO.calendar.EventTemplate;
 +
 +
if(GO.workflow)
 +
this.template +=GO.workflow.WorkflowTemplate;
  
In this example we will generate a second notes module. I actually used this to generate the base of the Notes module. It will not create a perfect module, but it's a good start.
+
this.template += GO.linksTemplate;
 +
 +
Ext.apply(this.templateConfig, GO.linksTemplateConfig);
 +
 +
if(GO.comments)
 +
{
 +
this.template += GO.comments.displayPanelTemplate;
 +
}
 +
 
 +
GO.presidents.PresidentPanel.superclass.initComponent.call(this);
 +
}
 +
});
 +
</pre>
  
Edit config.inc.php and paste the following in it:
+
After creating this Panel we still have to load it when our module loads. Add the following line on top of the scripts.txt file in your view folder.
  
 +
'''scripts.txt'''
 
<pre>
 
<pre>
<?php
+
modules/presidents/views/Extjs3/PresidentPanel.js
require('../../www/Group-Office.php');
+
</pre>
//name of the module. No spaces or strange characters.
+
$module = 'notes2';
+
  
//Short name of the module. The prefix of the database tables.
+
Lets make a split panel. where the center is our grid listing presidents. en the right panel will display the presidents details and links. Change the code in MainPanel.js to the following:
$prefix = 'no';
+
  
$tables=array();
+
'''MainPanel.js'''
//Tables to create an interface for
+
<pre>
 +
// Creates namespaces to be used for scoping variables and classes so that they are not global.
 +
Ext.namespace('GO.presidents');
  
 +
GO.presidents.MainPanel = function(config){
 +
 +
if(!config)
 +
{
 +
config = {};
 +
}
 +
 +
this.centerPanel = new GO.presidents.PresidentsGrid({
 +
region:'center',
 +
id:'pm-center-panel',
 +
border:true
 +
});
 +
 +
this.centerPanel.on("delayedrowselect",function(grid, rowIndex, r){
 +
this.eastPanel.load(r.data.id);
 +
}, this);
  
/*
+
this.centerPanel.store.on('load', function(){
* If you specify a link_type then linking will be enabled. Make sure your table also has a
+
this.eastPanel.reset();
* ctime and mtime column for this to work. Also either authenticate or authenticate related must be set.
+
}, this);
*
+
* If you specify authenticate. Then make sure your table has an acl_read and acl_write column
+
this.eastPanel = new GO.presidents.PresidentPanel({
*/
+
region:'east',
 +
id:'pm-east-panel',
 +
width:440,
 +
border:true
 +
});
  
$westpanel = array(
 
'mainpanel_tag'=> 'WESTPANEL', //{WESTPANEL} will be replaced in the MainPanel template defined below.
 
'template'=>'GridPanel.tpl', //The template to use for the grid. This is the only option at the moment
 
'name'=>'no_categories',  //Name of the table
 
'friendly_single'=>'category', //Name for a single item in this table. Must be lower case and alphanummeric
 
'friendly_multiple'=>'categories',//Name for a multiple items in this table. Must be lower case and alphanummeric
 
'authenticate'=>true,//Secure these items with authentication? If true then acl_read and acl_write columns must be defined in the table
 
'paging'=>false, //Use pagination in the grid?
 
'autoload'=>true, //Automatically load this table with data after rendering?
 
'files'=>false //Can files be uploaded to these items?
 
);
 
  
$tables[] = $westpanel;
+
//Setup a toolbar for the grid panel
 +
config.tbar = new Ext.Toolbar({
 +
cls:'go-head-tb',
 +
items: [
 +
{
 +
iconCls: 'btn-add',
 +
text: GO.lang['cmdAdd'],
 +
cls: 'x-btn-text-icon',
 +
handler: function(){
 +
this.centerPanel.showEditDialog();
 +
},
 +
scope: this
 +
},
 +
{
 +
iconCls: 'btn-delete',
 +
text: GO.lang['cmdDelete'],
 +
cls: 'x-btn-text-icon',
 +
handler: function(){
 +
this.centerPanel.deleteSelected();
 +
},
 +
scope: this
 +
}
 +
]
 +
});
 +
 +
config.items=[
 +
this.centerPanel,
 +
this.eastPanel
 +
];
 +
 +
config.layout='border';
 +
GO.presidents.MainPanel.superclass.constructor.call(this, config);
 +
};
  
$tables[] = array(
+
Ext.extend(GO.presidents.MainPanel, Ext.Panel, {});
'mainpanel_tag'=> 'CENTERPANEL',
+
'mainpanel_tags'=>array(
+
'centerpanel_related_field'=>'category_id',
+
'centerpanel_related_friendly_multiple_ucfirst'=>'Categories',
+
'centerpanel_related_friendly_multiple'=>'categories',
+
'centerpanel_friendly_single_ucfirst'=>'Note',
+
'centerpanel_friendly_single'=>'note',
+
'EASTPANEL'=>'GO.notes.NotePanel'
+
), //Custom tags for the mainpanel template that will be replaced
+
'template'=>'GridPanel.tpl',
+
'name'=>'no_notes',
+
'friendly_single'=>'note',
+
'friendly_multiple'=>'notes',
+
'paging'=>true,
+
'autoload'=>false,
+
'authenticate'=>false,
+
'authenticate_relation'=>true, //Authenticate a related table. In this example the notes categories.
+
'files'=>true,
+
'link_type'=>4, //If a link type is specified then this item will be linkable to other items. Choose a free identifier above 100!
+
'relation'=>array('field'=>'category_id', 'remote_field'=>'id', 'remote_table'=>$westpanel)); //Define a relation between the tables
+
+
$main_template='MainPanel.tpl'; //The template for MainPanel.js
+
</pre>
+
  
You can also find this content in the file notes_config.inc.php
 
  
Adjust the path to Group-Office.php at the top of the config file.
+
/*
 +
* This will add the module to the main tabpanel filled with all the modules
 +
*/
 +
GO.moduleManager.addModule(
 +
'presidents', //Module alias
 +
GO.presidents.MainPanel, //The main panel for this module
 +
{
 +
title : 'Presidents', //Module name in startmenu
 +
iconCls : 'go-module-icon-presidents' //The css class with icon for startmenu
 +
}
 +
);
  
Now run: "php create.php" from the command line. It will create the folder "modules/notes2". Now login to Group-Office as an administrator and install the new module!
+
/*
 +
* Add linkHandeler for linking models from this module to other GroupOffice models
 +
*/
 +
GO.linkHandlers["GO_Presidents_Model_President"]=function(id){
 +
if(!GO.presidents.linkWindow){
 +
var presidentPanel = new GO.presidents.PresidentPanel();
 +
GO.presidents.linkWindow= new GO.LinkViewWindow({
 +
title: GO.presidents.lang.president,
 +
items: presidentPanel,
 +
presidentPanel: presidentPanel,
 +
closeAction:"hide"
 +
});
 +
}
 +
GO.presidents.linkWindow.presidentPanel.load(id);
 +
GO.presidents.linkWindow.show();
 +
return GO.presidents.linkWindow;
 +
}
 +
</pre>
  
 +
As you can see in the New MainPanel.js view this panel has a 'config.tbar' defines with the 2 'add' and 'delete' buttons. Note that the reference to the showEditDialog() method has changed to 'this.centerPanel' instead of 'this' because the centerPanel now represents the PresidentsGrid.
  
==Adding code later on==
+
So this buttons can be removed from the toolbar in the PresidentGrid.js
  
If you want to add code for table later on you can do this easily. Change the config file into:
+
So change to config.tbar option in the PresidentGrid.js to the following.
  
 +
'''PresidentGrid.js:'''
 
<pre>
 
<pre>
<?php
+
config.tbar = new Ext.Toolbar({
require('../../www/Group-Office.php');
+
items: [
 +
GO.lang.strSearch + ':',
 +
new GO.form.SearchField({
 +
store: config.store,
 +
width:320
 +
})
 +
]
 +
})
 +
</pre>
  
//name of the module. No spaces or strange characters.
+
This way only the searchfield will remain in the toolbar of the PresidentsGrid. The Add and Delete button are already placed on the MainPanel's toolbar
$module = 'announcements';
+
  
//Short name of the module. The prefix of the database tables.
+
== Extending the defaults ==
$prefix = 'su';
+
  
$tables=array();
+
Do you see the Add and Edit button in top of your panel being grayed out. The main reason for this it because the edit panel has build in functionality the check if you have permission the read or write a record. this way you can set this permission per record.
//Tables to create an interface for
+
  
 +
For our presidents example it's not very logic to have these permission per record. We didn't specify this in the database and are not going to use it in this example. for this reason. we need to overwrite the data that we get from the server.
  
$westpanel = array(
+
Lets have o look at what's returned:
'mainpanel_tag'=> 'GRID',
+
<pre>
'template'=>'GridPanel.tpl',
+
{"data":
'name'=>'su_announcements',  
+
{"id":"40",
'friendly_single'=>'announcement',  
+
"party_id":"6",
'friendly_multiple'=>'announcements',
+
"firstname":"Ronald",
'authenticate'=>false,
+
"lastname":"Reagan",
'paging'=>true,
+
"tookoffice":"20-01-1981",
'autoload'=>true,
+
"leftoffice":"20-01-1989",
'files'=>false);
+
"income":"99,867,297.35",
 +
"model":"GO_Presidents_Model_President",
 +
"permission_level":4,
 +
"write_permission":true,
 +
"customfields":[{
 +
"id":"1",
 +
"name":"Appearance",
 +
"fields":[{
 +
"name":"Glasses",
 +
"value":"No"
 +
}]
 +
}],
 +
"links":[],
 +
"events":[],
 +
"tasks":[],
 +
"files":[],
 +
"comments":[]
 +
},
 +
"success":true}
 +
</pre>
  
$tables[] = $westpanel;
+
All display view in Group-Office are ExtJS components or Group-Office components that are extended from ExtJS components. All of these components are filled with JSON data. This data is loaded Asynchrone with Javascript. Let look at what data will be loaded
  
 +
Let's modify the JSON data that is returned. Add the following function to the PresidentController
 +
 +
'''PresidentController.php:'''
 +
<pre>
 +
protected function afterDisplay(&$response, &$model, &$params)
 +
{
 +
$response['data']['partyName'] = $model->party->name;
 +
return parent::beforeDisplay($response, $model, $params);
 +
}
 
</pre>
 
</pre>
  
This will create grids and dialogs for the announcements table. This actually doesn't make any sense for this module but it's good for an example.
+
With this the JSON data that is returned in actionDisplay() will be overwritten. a field partyName will be added to our returned JSON data. this will be used to display the party name in the PresidentPanel
  
Now you have to change the generated code to do something with the grids and dialog.
+
== Installation and upgrades ==
  
 +
Files in the modules/presidents/install folder are important for installing, uninstalling and upgrading modules.
 +
install.sql and uninstall.sql are automatically applied to the database when installing or uninstalling.
 +
updates.php is used for upgrading.
  
==Minify your code==
+
It looks like this for example:
When Group-Office is deployed it will use minified scripts for better performance. When you have changed your Javascript files you must minify them using the minify.php script. If you checkout the trunk from subversion or you download the tools from Sourceforge ( https://sourceforge.net/projects/group-office/files/ ).
+
 
 +
<pre>
 +
<?php
 +
$updates["201108131011"][]="ALTER TABLE `ab_companies` CHANGE `id` `id` INT( 11 ) NOT NULL AUTO_INCREMENT";
 +
$updates["201108131011"][]="ALTER TABLE `ab_contacts` CHANGE `id` `id` INT( 11 ) NOT NULL AUTO_INCREMENT";
 +
$updates["201108131011"][]="ALTER TABLE `ab_contacts` ADD `go_user_id` INT NOT NULL , ADD INDEX ( `go_user_id` )";
 +
$updates["201204270920"][]="script:1_some_script.php";
 +
</pre>
  
Execute: ./minify.php /path/to/go on Linux
+
The timestamp is in this format:
  
Execute: php.exe minify.php path\to\go on Windows
+
"YYYYMMDDHHMM"
  
to minify all the javascript files. Java must be installed for the minify script to work.  
+
The timestamp is important because all updates are executed chronologically. Because modules can depend on each others updates.
  
GO will use the minified script when $config['debug']=false; is set.
+
You can also use scripts. It's good practice to avoid using update scripts as much as possible. When scripts use functions of the framework and they change later they might break.
 +
The entry "script:1_some_script.php" will include modules/[moduleid]/install/updatescripts/1_some_script.php.
  
<b>Note:</b>From all minified scripts, GO will built two large scripts for optimal performance. GO will rebuilt those scripts only when javascript/go-all-min.js is modified or when the $mtime var in classes/base/config.class.inc.php is set. So when these values don't change and you rebuilt it you must empty the <local_path>/cache/ directory or run install/upgrade.php.
+
<b>Note:</b> When putting update queries in the updates.php file, don't forget to update install.sql too.

Latest revision as of 16:04, 10 June 2014

Creating an empty module

Now we will create the main module class.

A Group-Office module will extend the GO_Base_Module class.

Create the file module/presidents/PresidentsModule.php

PresidentsModule.php:

class GO_Presidents_PresidentsModule extends GO_Base_Module{
	
	public function autoInstall() {
		return true;
	}
	
	public function author() {
		return 'Merijn Schering';
	}
	
	public function authorEmail() {
		return 'mschering@intermesh.nl';
	}
}

Creating our first view

Now we will create the main panel of the module. All views in Group-Office view is an extension of an ExtJS Panel. So it can be put in any other ExtJS Container.

For the main panel create the file: module/presidents/views/Extjs3/MainPanel.js

MainPanel.js:

// Creates namespaces to be used for scoping variables and classes so that they are not global.
Ext.namespace('GO.presidents');

/*
 * This is the constructor of our MainPanel
 */
GO.presidents.MainPanel = function(config){

	if(!config)
	{
		config = {};
	} 

	config.html='Hello World';
	
	/*
	 * Explicitly call the superclass constructor
	 */
 	GO.presidents.MainPanel.superclass.constructor.call(this, config);

}

/*
 * Extend our MainPanel from the ExtJS Panel
 */
Ext.extend(GO.presidents.MainPanel, Ext.Panel,{

});

/*
 * This will add the module to the main tab-panel filled with all the modules
 */
GO.moduleManager.addModule(
	'presidents', //Module alias
	GO.presidents.MainPanel, //The main panel for this module
	{
		title : 'Presidents', //Module name in start-menu
		iconCls : 'go-module-icon-presidents' //The css class with icon for start-menu
	}
);

This view will only render the text 'Hello World'

For the main panel to be loaded when the module is active, we'll need to create a file called scripts.txt in our /modules/presidents/views/ExtJs3/ folder.

scripts.txt:

modules/presidents/views/Extjs3/MainPanel.js

Without this file your module will never work!

Multilingual functions

To make the module translatable create the follow file: modules/presidents/language/en.php:

en.php

<?php
/**
 * This $l array excists of key-value pairs
 * key is what should be called in the view eg. GO.presidents.name
 * value is the string that will be displayed in your view
 */
$l = array(
	'name' => ' Presidents', //this name will appear in the Start menu -> modules section
	'description' => 'A simple test module', //this description will appear in the Start menu -> modules section
);

These two string are the name and the description of the Group-Office module and are use by the module installation dialog.

After completing these steps you should be able to install your module and be able to display the hello world view that was created.

In Group-Office go to 'Admin menu->modules' and install your newly created module.

The Tutorial

For this tutorial we will assume that you finished the first sections on this page.

The example module we are going to make will show you how to create a simple module that displays data from a database and lets us add items to it, change them and delete them.

All files are also available in the Group-Office download. You can find them in modules/presidents/.

Before we start

First, we need to create the database containing the data we are going to work with.

We will create 2 tables named 'pm_parties' and 'pm_presidents' It is always a good idea to prefix the database tables used for a module (pm_ is short for President Module)

Run the following MySQL query on the Group-Office database of your installation:

CREATE TABLE IF NOT EXISTS `pm_parties` (
  `id` int(11) NOT NULL auto_increment,
  `name` varchar(40) NOT NULL,
  PRIMARY KEY  (`id`)
);

INSERT INTO `pm_parties` (`id`, `name`) VALUES
(1, 'No Party'),
(2, 'Federalist'),
(3, 'Democratic-Republican'),
(4, 'Democratic'),
(5, 'Whig'),
(6, 'Republican');

CREATE TABLE IF NOT EXISTS `pm_presidents` (
  `id` int(11) NOT NULL auto_increment,
  `party_id` int(11) NOT NULL,
  `firstname` varchar(20) NOT NULL,
  `lastname` varchar(20)  NOT NULL,
  `tookoffice` date NOT NULL,
  `leftoffice` date NOT NULL,
  `income` decimal(14,2) NOT NULL,
  `ctime` int(11) NOT NULL DEFAULT '0',
  `mtime` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY  (`id`)
);

INSERT INTO `pm_presidents` (`id`, `party_id`, `firstname`, `lastname`, `tookoffice`, `leftoffice`, `income`) VALUES
(1, 1, 'George', 'Washington', '1789-04-30', '1797-03-04', 135246.32),
(2, 2, 'John', 'Adams', '1797-03-04', '1801-03-04', 236453.34),
(3, 3, 'Thomas', 'Jefferson', '1801-03-04', '1809-03-04', 468043.25),
(4, 3, 'James', 'Madison', '1809-03-04', '1817-03-04', 649273.00),
(5, 3, 'James', 'Monroe', '1817-03-04', '1825-03-04', 374937.23),
(6, 3, 'John', 'Quincy Adams', '1825-03-04', '1829-03-04', 649824.35),
(7, 4, 'Andrew', 'Jackson', '1829-03-04', '1837-03-04', 3972753.12),
(8, 4, 'Martin', 'Van Buren', '1837-03-04', '1841-03-04', 325973.24),
(9, 5, 'William', 'Harrison', '1841-03-04', '1841-04-04', 25532.08),
(10, 5, 'John', 'Tyler', '1841-04-04', '1845-03-04', 235542.35),
(11, 4, 'James', 'Polk', '1845-03-04', '1849-03-04', 3264972.35),
(12, 5, 'Zachary', 'Taylor', '1849-03-04', '1850-07-09', 35974.35),
(13, 5, 'Millard', 'Fillmore', '1850-07-09', '1853-03-04', 35792.45),
(14, 4, 'Franklin', 'Pierce', '1853-03-04', '1857-03-04', 357938.35),
(15, 4, 'James', 'Buchanan', '1857-03-04', '1861-03-04', 357937.35),
(16, 6, 'Abraham', 'Lincoln', '1861-03-04', '1865-04-15', 25073.40),
(17, 4, 'Andrew', 'Johnson', '1865-04-15', '1869-03-04', 356279.35),
(18, 6, 'Ulysses', 'Grant', '1869-03-04', '1877-03-04', 357938.35),
(19, 6, 'Rutherford', 'Hayes', '1877-03-04', '1881-03-04', 2359737.35),
(20, 6, 'James', 'Garfield', '1881-03-04', '1881-09-19', 3579.24),
(21, 6, 'Chester', 'Arthur', '1881-09-19', '1995-03-04', 357932.35),
(22, 4, 'Grover', 'Cleveland', '1885-03-04', '1889-03-04', 369723.35),
(23, 6, 'Benjamin', 'Harrison', '1889-03-04', '1893-03-04', 357392.35),
(24, 4, 'Grover', 'Cleveland', '1893-03-04', '1897-03-04', 3275935.35),
(25, 6, 'William', 'McKinley', '1897-03-04', '1901-09-14', 35793.35),
(26, 6, 'Theodore', 'Roosevelt', '1901-09-14', '1909-03-04', 0.00),
(27, 6, 'William', 'Taft', '1909-03-04', '1913-03-04', 239597.35),
(28, 4, 'Woodrow', 'Wilson', '1913-03-04', '1921-03-04', 32579743.34),
(29, 6, 'Warren', 'Harding', '1921-03-04', '1923-08-02', 35793.24),
(30, 6, 'Calvin', 'Coolidge', '1923-08-02', '1929-03-04', 296529.24),
(31, 6, 'Herbert', 'Hoover', '1929-03-04', '1933-03-04', 3525.25),
(32, 4, 'Franklin', 'Roosevelt', '1933-03-04', '1945-04-12', 35293734.35),
(33, 4, 'Harry', 'Truman', '1945-04-12', '1953-01-20', 23579358.35),
(34, 6, 'Dwight', 'Eisenhower', '1953-01-20', '1961-01-20', 25973535.35),
(35, 4, 'John', 'Kennedy', '1961-01-20', '1963-11-22', 46081.24),
(36, 4, 'Lyndon', 'Johnson', '1963-11-22', '1969-01-20', 2503759.35),
(37, 6, 'Richard', 'Nixon', '1969-01-20', '1974-08-09', 3259744.53),
(38, 6, 'Gerald', 'Ford', '1974-08-09', '1977-01-20', 643076.05),
(39, 4, 'Jimmy', 'Carter', '1977-01-20', '1981-01-20', 1205735.25),
(40, 6, 'Ronald', 'Reagan', '1981-01-20', '1989-01-20', 99867297.35),
(41, 6, 'George H.', 'Bush', '1989-01-20', '1993-01-20', 92048204.24),
(42, 4, 'Bill', 'Clinton', '1993-01-20', '2001-01-20', 12073975.24);

Note: We can insert this data manually into the database. Or this script can be executed automatically when installing the module. (See section Installation and upgrades)

Building the models

We have 2 types of data that is going to be used on this module President and Party For this we will create to Model classes in the modules/presidents/models folder

Party.php

<?php
/**
 * The Party model
 * 
 * @property int $id
 * @property string $name
 */
class GO_Presidents_Model_Party extends GO_Base_Db_ActiveRecord {
	/**
	 * Returns a static model of itself
	 * 
	 * @param String $className
	 * @return GO_Presidents_Model_Party 
	 */
	public static function model($className=__CLASS__)
	{	
		return parent::model($className);
	}

	public function tableName() {
		return 'pm_parties';
	}

	public function relations() {
		return array(
			'presidents' => array('type' => self::HAS_MANY, 'model' => 'GO_Presidents_Model_President', 'field' => 'party_id', 'delete' => true)		
		);
	}
}

Our model class will extend from GO_Base_Db_ActiveRecord. Also the name of the class is important and should be in the following format: GO_[modulename]_Model_[modelname] where model name should be the same as the filename.

This is all there is to create a model class. The properties of the model are already known since the database table is specified in the tableName() function

Also a relation is defined between the Party Model and the President model. Create the president model as follows:

President.php

<?php
/**
 * The President model
 * 
 * @property int $id
 * @property int $party_id
 * @property string $firstname
 * @property string $lastame
 * @property date $tookoffice
 * @property date $leftoffice
 * @property float $income
 */
class GO_Presidents_Model_President extends GO_Base_Db_ActiveRecord {
	
	/**
	 * Returns a static model of itself
	 * 
	 * @param String $className
	 * @return GO_Presidents_Model_President 
	 */
	public static function model($className=__CLASS__)
	{	
		return parent::model($className);
	}
	
	protected function init() {
		$this->columns["income"]["gotype"]="number";
		return parent::init();
	}
	
	public function tableName(){
		return 'pm_presidents';
	}

	public function relations(){
		return array(	
				'party' => array('type'=>self::BELONGS_TO, 'model'=>'GO_Presidents_Model_Party', 'field'=>'party_id'),		);
	}

}

That's all there is to creating models for this example module

Creating the controllers

The controllers we stick into the 'modules/presidents/controller' folder

Create the following 2 files:

PartyController.php

<?php
class GO_Presidents_Controller_Party extends GO_Base_Controller_AbstractModelController{
	protected $model = 'GO_Presidents_Model_Party';
}

PresidentController.php

<?php
class GO_Presidents_Controller_President extends GO_Base_Controller_AbstractModelController {
	
	protected $model = 'GO_Presidents_Model_President';

	/**
	 * Tell the controller to change some column values
	 */
	protected function formatColumns(GO_Base_Data_ColumnModel $columnModel) {
		$columnModel->formatColumn('party_id','$model->party->name');
		$columnModel->formatColumn('income_val','$model->income');
		return parent::formatColumns($columnModel);
	}

	/**
	 * Display current value in combobox
	 */
	protected function remoteComboFields(){
		return array('party_id'=>'$model->party->name');
	}
}

To the PartyController there is not much to understand. Extend from GO_Base_Controller_AbstractModelController Tell it what model class it should use and it knows how to Load, Save and Load JSON data for a grid.

In the President Controller 2 extra functions are added. formatColumns() Will make sure that in the datagrid won't show the id, but the name of the party that to president is member of.

remoteComboFields() This will make sure that the Combobox is filled with the right data when the President Dialog (Edit form) is shown.

Designing the views

Now for the fun part: The client-side of your module. We'll create the views that display on the screen. This is what our end-users will actually see:

We'll need 3 different view:

  1. a Datagrid for displaying all records
  2. a Form dialog for editing a record
  3. a Main panel where we will put the above.

We already created MainPanel.js in the modules/views/extjs3/ folder in the beginning of this guide. Lets modify that file with the following code:

MainPanel.js

// Creates namespaces to be used for scoping variables and classes so that they are not global.
Ext.namespace('GO.presidents');

/*
 * This will add the module to the main tab-panel filled with all the modules
 */
GO.moduleManager.addModule(
	'presidents', //Module alias
	GO.presidents.PresidentsGrid, //The main panel for this module
	{
		title : 'Presidents', //Module name in start-menu
		iconCls : 'go-module-icon-presidents' //The css class with icon for start-menu
	}
);

This file will do no more then search for GO.presidents.PresidentsGrid to fill the modules MainPanel with.

For our GridPanel we are going to use a Group-Office class that extends Ext.grid.GridPanel.

GO.grid.GridPanel is an extension of the default Ext grid and implements some basic Group-Office functionality like deleting items.

We can now delete selected items in the grid with only the [delete] button.

Lets create this 'PresidentsGrid' in the modules/presidents/views/extjs3/ folder

PresidentsGrid.js

GO.presidents.PresidentsGrid = function(config){
	
	if(!config)
	{
		config = {};
	}

	//Construct the Gridpanel with the above configuration
	GO.presidents.PresidentsGrid.superclass.constructor.call(this, config);
}

//Extend the PresidentsGrid from GridPanel
Ext.extend(GO.presidents.PresidentsGrid, GO.grid.GridPanel,{
	
});

This is the code that is needed for a data-grid but we haven't configured anything yet so it won't do anything for until we do.

For this grid we are going to configure the following properties:

  1. Some default properties like how to layout and we'll give it a title
  2. Set the Edit dialog class we will use to edit a record.
  3. A data-store from which the grid needs to pull its data
  4. A column model, what column we want to display and how.

Just before the 'Construct the Gridpanel with the above configuration' comment add the following code to configure what's listed above:

Some default values for the grid each commented with its use:

	config.title = GO.presidents.lang.presidents; //Title of this panel
	config.layout='fit'; //How to lay out the panel
	config.autoScroll=true;
	config.split=true;
	config.autoLoadStore=true;
	config.sm=new Ext.grid.RowSelectionModel();
	config.loadMask=true; //Mask the grid when it is loading data

Setting the editDialogClass this is rather important. We will create the PresidentDialog later on. Let's first finish configuring this grid.

	//This dialog will be opened when double clicking a row or calling showEditDialog()
	config.editDialogClass = GO.presidents.PresidentDialog;

Setting the column model of the data-gird. What column do you want to display and what should be left out? For all the properties a ColumnModel can have please go to Ext.grid.ColumnModel This is just the basic usage.

	//Configuring the column model
	config.cm =  new Ext.grid.ColumnModel({
		defaults:{
			sortable: true
		},
		columns : [{
			header: '#',
			dataIndex: 'id',
			hidden:true,
			sortable: false
		},{
			header: GO.presidents.lang.firstname,
			dataIndex: 'firstname'
		},{
			header: GO.presidents.lang.lastname,
			dataIndex: 'lastname'
		},{
			header: GO.presidents.lang.party_id,
			dataIndex: 'party_id'
		},{
			header: GO.presidents.lang.tookoffice,
			dataIndex: 'tookoffice'
		},{
			header: GO.presidents.lang.leftoffice,
			dataIndex: 'leftoffice'
		},{
			header: GO.presidents.lang.income,
			dataIndex: 'income',
			align: "right"
		}]
	});

As you can see in the above code we created a ColumnModel object and added it to the config For the header value of the columns that will display in our view

Defining a data-store for the grid:

	//Defining the data store for the grid
	config.store = new GO.data.JsonStore({
		url: GO.url('presidents/president/store'),
		fields: ['id','firstname','lastname','party_id','tookoffice','leftoffice','income'],
		remoteSort: true
	});

Note: The url: GO.url('presidents/president/store') refers to the methode 'actionStore()' in the President Controller wich it inherits from the AbstractController class. The url will return a JSON string once called.

After setting the data-store we'll add the add the GridView component to the panel:

	//Adding the gridview to the grid panel
	config.view=new Ext.grid.GridView({
		emptyText: GO.lang.strNoItemse	
	});

In this example we only set the emptyText value this is the text that gets displayed when no records are found by the data store. We are not using a string here but we'll reference to a global language string provided by Group-Office.

For fore info on the Grid view see the Ext.grid.GridView docs

Our data-grid is now ready for usage. Let's not forget to add our new PresidentsGrid to scripts.txt or else it wont be loaded by the system and or MainPanel.js cant find the Grid

scripts.txt:

modules/presidents/views/Extjs3/PresidentsGrid.js
modules/presidents/views/Extjs3/MainPanel.js

Note: As the PresidentGrid class is used in MainPanel.js the PresidentsGrid.js file needs the be loaded first.

The data-grid should now work and display the presidents of the United States. Note that because of the 'formatColumns()' method we made in the PresidentController not the ID of a party will be displayed in the grid but the name of the party.

Creating a Dialog

Now we are going to create a Dialog which will make editing presidents very easy.

Create a file modules/president/module/PresidentDialog.js

PresidentDialog.js:

GO.presidents.PresidentDialog = Ext.extend(GO.dialog.TabbedFormDialog , {

	initComponent : function(){
		
		Ext.apply(this, {
			titleField:'lastname',
			goDialogId:'president',
			title:GO.presidents.lang.president,
			width: 300,
			height: 280,
			formControllerUrl: 'presidents/president'
		});
		
		GO.presidents.PresidentDialog.superclass.initComponent.call(this);	
	}

});

This will create an empty dialog that will open when we double click a row in the grid. But it doesn't work yet. This is because we haven't loaded the PresidentDialog.js file. Lets do this by changing your 'scripts.txt' file again.

scripts.txt:

modules/presidents/views/Extjs3/PresidentsGrid.js
modules/presidents/views/Extjs3/PresidentDialog.js
modules/presidents/views/Extjs3/MainPanel.js

Adding some language strings

In the PresidentDialog.js file you can see that the title property of the dialog is set to GO.presidents.lang.president This language string doesn't exists yet. So let's add some translatable strings that we can use in our PresidentDialog.

Open up modules/language/en.php and write the following:

en.php:

$l = array(
	'name' => ' Presidents', //this name will appear in the Start menu -> modules section
	'description' => 'A simple test module', //this description will appear in the Start menu -> modules section

	'presidents' => 'Presidents',
	'president' => 'President',
	'party_id' => 'Party',

	'tookoffice' => 'Entering Office',
	'leftoffice' => 'Leaving Office',
	'income' => 'Income',
	'firstname' => 'First name',
	'lastname' => 'Last name',
);

GO.presidents.lang.president in the title of our PresidentDialog will now display: "President" The other language strings you see will be used for the form that will be created inside the dialog.

Creating a form inside the dialog

In the PresidentDialog.js file we have created a PresidentDialog extended from TabbedFormDialog and overwritten the function initComponents and set some properties there.

We are going to overwrite one more method in the PresidentDialog. Add a comma (,) at the end of the initComponent function and add the following code afterwards:

PresidentDialog.js:

buildForm : function () {

		this.propertiesPanel = new Ext.Panel({
			title:GO.lang.strProperties,			
			cls:'go-form-panel',
			layout:'form',
			items:[{
				xtype: 'textfield',
				name: 'firstname',
				width:300,
				anchor: '100%',
				maxLength: 100,
				allowBlank:false,
				fieldLabel: GO.presidents.lang.firstname
			},
			{
				xtype: 'textfield',
				name: 'lastname',
				fieldLabel: GO.presidents.lang.lastname,
				anchor: '100%'
			},
			{
				xtype:'combo',
				fieldLabel: GO.presidents.lang.party_id,
				hiddenName:'party_id',
				anchor:'100%',
				emptyText:GO.lang.strPleaseSelect,
				store: new GO.data.JsonStore({
					url: GO.url('presidents/party/store'),
					baseParams: {
						permissionLevel:GO.permissionLevels.write
					},	
					fields: ['id', 'name']	
				}),
				valueField:'id',
				displayField:'name',
				triggerAction: 'all',
				editable: true,
				forceSelection: true,
				allowBlank: false
			},
			{
				xtype: 'datefield',
				name: 'tookoffice',
				fieldLabel: GO.presidents.lang.tookoffice,
				allowBlank: false,
				anchor: '100%'
			},
			{
				xtype: 'datefield',
				name: 'leftoffice',
				fieldLabel: GO.presidents.lang.leftoffice,	    
				allowBlank: false,
				anchor: '100%'
			},
			{
				xtype: 'numberfield',
				name: 'income',
				value: GO.util.numberFormat(0),
				fieldLabel: GO.presidents.lang.income,
				allowBlank: false,
				anchor: '100%'
			}]				
		});

		this.addPanel(this.propertiesPanel);
	}

This will add a panel to our PresidentDialog and add 6 form-fields inside. The combo/dropdown field is a bit tricky.

It's needs to know where to pull the data that it is going to list from. for this we set the store property of the field and fill it with a JsonStore object. This object needs to know a controller action that returns JSON data. We'll refer to the 'store' action in the PartyController we've created earlier. Also we specify the fields the be used for the key and value of the dropdown menu. In this case 'id' and 'name'.

When double clicking a row the President dialog will now show a form with the above defined fields.

In the next section we are going to add a toolbar which will allow us to create new presidents.

Adding a Toolbar

We are now going to add a toolbar in the top with links to create or delete a president.

All of the following code has to be added within the PresidentsGrid configuration.

PresidentsGrid.js:

	//Setup a toolbar for the grid panel
	config.tbar = new Ext.Toolbar({		
			cls:'go-head-tb',
			items: [
				{
					iconCls: 'btn-add',							
					text: GO.lang['cmdAdd'],
					cls: 'x-btn-text-icon',
					handler: function(){					
						this.showEditDialog();
					},
					scope: this
				},
				{
					iconCls: 'btn-delete',
					text: GO.lang['cmdDelete'],
					cls: 'x-btn-text-icon',
					handler: function(){
						this.deleteSelected();
					},
					scope: this
				},
				GO.lang.strSearch + ':', 
				new GO.form.SearchField({
					store: config.store,
					width:320
				})
			]
	});

Setting a Ext.Toolbar object to the tbar property of the GridPanel will render a toolbar on top of the grid. In this example we'll add 4 items to the toolbar:

  1. An add button that will call the PresidentDialog but this time it's empty and can be used to add a new record
  2. A delete button that will delete the selected row.
  3. A string
  4. A search Field.

For more information about the see the Ext.Toolbar docs


Adding a PagingToolbar

This one is very easy to do because we used a GO.grid.GridPanel instead of the Ext one.

We only need to add 1 line to our PresidentsGrid configuration and our PHP takes care of the rest.

PresidentsGrid.js:

/* config.paging is an added functionality by Group-Office
 * which makes it very easy to add a PagingToolbar.	
 */ 
config.paging=true,	

Adding some style

Before we start styling we need to create a few more folders.

Create Directories:

modules/presidents/themes/
modules/presidents/themes/Default/

Now we create the file modules/presidents/themes/Default/style.css

Most modules will want to have it's own module-icon, so we add this default rule for later use.

style.css:

/* This class is used to show the icon in the main tabpanel of GO */
.go-module-icon-presidents {
}

In our Grid we can't change the ID's of presidents. To make it apparent that this field is treated differently we will add a renderer that sets the cell CSS style so it will use a different background.

To do this we need to change the corresponding column config object in our PresidentsGrid ColumnModel.

PresidentsGrid.js:

{
	header: '#',
	readOnly: true,
	dataIndex: 'id',
	renderer: function(value, cell){ 
		cell.css = "readonlycell";
		return value;
	},
	width: 50
}

In our style.css we add a .readonlycell rule

style.css:

.readonlycell { 
  background-color:#CCCCCC !important;
}

To make it more clear which president had the better income we will change the color of the income according to its value. We need to change the renderer function of the Income column.

PresidentsGrid.js:

{
	header: "Income",
	dataIndex: 'income',
	width: 120,
	renderer: function(value, metaData, record){
		if(record.data.income_val > 1000000){
			metaData.attr = 'style="color:#336600;"';
		} else if (record.data.income_val > 100000){
			metaData.attr = 'style="color:#FF9900;"';
		} else {
			metaData.attr = 'style="color:#CC0000;"';
		}
		return "$ "+value;
	},
	align: "right"
}

This concludes this tutorial.

We now have a fully functional module which lets us add, update and delete presidents.

Putting custom fields in module items

This will only work with the professional version and you must have the Custom fields module installed. To create custom fields for a model, we must create a custom field module. We'll place it in the following directory: modules/presidents/customfields/model/

President.php

class GO_Presidents_Customfields_Model_President extends GO_Customfields_Model_AbstractCustomFieldsRecord{
	/**
	 * Returns a static model of itself
	 * 
	 * @param String $className
	 * @return GO_Presidents_Model_CustomFieldsRecord 
	 */
	public static function model($className=__CLASS__)
	{	
		return parent::model($className);
	}

	public function extendsModel(){
		return "GO_Presidents_Model_President";
	}
}

Our President model still needs to know what the classname of the CustomerFields model is. For this we'll add the Following method the our President Model:

President.php

public function customfieldsModel(){
	return "GO_Presidents_Customfields_Model_President";
}

Also the customer fields we specify need to be saved into the database For this we'll create a table named 'cf_pm_presidents':

CREATE TABLE IF NOT EXISTS `cf_pm_presidents` (
  `model_id` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`model_id`)
);

INSERT INTO `cf_pm_presidents` (`model_id`) VALUES (1);

Note: The table name needs to be the same as the table name where the records are saved but prefixed with ' cf_'

This is all there is to add custom field functionality. After this we still need to change our views to display the input fields in our edit dialog. Add the following line just before the initComponent function

PresidentDialog.php

customFieldType : "GO_Presidents_Model_President",

This should make the customfields work all we have to do is create some fields. Install the Custom fields module if you haven't done this already. This module is only available in the pro version of Group-Office.

When it's installed it will show up in the Admin menu of the start-menu. Start it and in the left sidebar you will see the the President Model is there. Click it and add a Category. Name it: 'Appearance'.

We'll add the following field to this category: Name: Hair color, Type: Text Name: Birthday, Type: Date Name: Glasses, Type: Checkbox

To check if the fields will work. We first need to reload Group Office. Refresh the page, go to the Presidents module and edit a record. See the extra tab Appearance in to PresidentDialog?

It's also possible to display customfields in the data-grid if you like. The reason that the fields aren't displaying in the grid yet is because the DataStore of the grid doesn't know what the model type is. It only has attributes.

Lets change to config.store attribute of the configuration of our PresidentGrid PresidentGrid.js:

	//Defining the data store for the grid
	config.store = new GO.data.JsonStore({
		url: GO.url('presidents/president/store'),
		fields: ['id','firstname','lastname','party_id','tookoffice','leftoffice','income', 'income_val'],
		remoteSort: true,
		model: 'GO_Presidents_Model_President'
	});

All your defines customfields will be visible into the data-grid. Note: The following fieldtypes will never be rendered in the grid: 'textarea','html', 'heading', 'infotext'

Making your items linkable

In Group-Office we can link one item to another. If your newly create module has an item that should be linkable we add the following overwrite the hasLinks() method in our model

We are going to make the presidents linkable, for this we add the following methods to our President Model:

President.php

public function hasLinks() {
	return true;
}

public function getFullname() {
	return $this->firstname . " " . $this->lastname;
}

protected function getCacheAttributes() {
	return array("name"=>$this->fullname, "description"=>$this->party->name);
}

Next our database needs a table where to links for the presidents will be saved. We execute the following query on our database:

SQL

CREATE TABLE IF NOT EXISTS `go_links_pm_presidents` (
  `id` int(11) NOT NULL,
  `folder_id` int(11) NOT NULL,
  `model_id` int(11) NOT NULL,
  `model_type_id` int(11) NOT NULL,
  `description` varchar(100) DEFAULT NULL,
  `ctime` int(11) NOT NULL,
  KEY `link_id` (`model_id`,`model_type_id`),
  KEY `id` (`id`,`folder_id`),
  KEY `ctime` (`ctime`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Note: the table name is the same as where the president records are stored but prefixed with 'go_links_'

In the PresidentDialog we will add a field to add links to the record after the income numberfield we'll add a new Go.form.SelectLink componentent. To code will look like this:

PresidentDialog.js

{
	xtype: 'numberfield',
	name: 'income',
	value: GO.util.numberFormat(0),
	fieldLabel: GO.presidents.lang.income,   
	allowBlank: false,
	anchor: '100%'
},
new GO.form.SelectLink({
	anchor:'100%'
})

This is all what's needed for adding other linkable Group-Office record to the president record. But where are they? Let's make a panel to display the links. We'll place this on the right side of the grid.

The MainPanel will be seperated into 2 divided panels. Our PresidentGrid in the middle and the (still to build) PresidentPanel on the right. Let's build the PresidentPanel

PresidentPanel.js

GO.presidents.PresidentPanel = Ext.extend(GO.DisplayPanel,{
	model_name : "GO_Presidents_Model_President",
	noFileBrowser : true,
	stateId : 'pm-president-panel',
	
	editHandler : function(){
		if(!GO.presidents.presidentDialog)
			GO.presidents.presidentDialog = new GO.presidents.PresidentDialog();
		GO.presidents.presidentDialog.show(this.link_id);		
	},	
		
	initComponent : function(){	
		
		this.loadUrl=GO.url('presidents/president/display');
		
		this.template = 
				'<table class="display-panel" cellpadding="0" cellspacing="0" border="0">'+
					'<tr>'+
						'<td colspan="2" class="display-panel-heading">{firstname} {lastname}</td>'+
					'</tr>'+
					'<tr>'+
						'<td>ID:</td>'+
						'<td>{id}</td>'+
					'</tr>'+
					'<tr>'+
						'<td>'+GO.presidents.lang.party_id+':</td>'+
						'<td>{partyName}</td>'+
					'</tr>'+
					'<tr>'+
						'<td>'+GO.presidents.lang.tookoffice+':</td>'+
						'<td>{tookoffice}</td>'+
					'</tr>'+
					'<tr>'+
						'<td>'+GO.presidents.lang.leftoffice+':</td>'+
						'<td>{leftoffice}</td>'+
					'</tr>'+
					'<tr>'+
						'<td>'+GO.presidents.lang.income+':</td>'+
						'<td>{income}</td>'+
					'</tr>'+
				'</table>';																		
				
		if(GO.customfields)
		{
			this.template +=GO.customfields.displayPanelTemplate;
		}

		if(GO.tasks)
			this.template +=GO.tasks.TaskTemplate;

		if(GO.calendar)
			this.template += GO.calendar.EventTemplate;
		
		if(GO.workflow)
			this.template +=GO.workflow.WorkflowTemplate;

		this.template += GO.linksTemplate;	
				
		Ext.apply(this.templateConfig, GO.linksTemplateConfig);
		
		if(GO.comments)
		{
			this.template += GO.comments.displayPanelTemplate;
		}

		GO.presidents.PresidentPanel.superclass.initComponent.call(this);
	}
});

After creating this Panel we still have to load it when our module loads. Add the following line on top of the scripts.txt file in your view folder.

scripts.txt

modules/presidents/views/Extjs3/PresidentPanel.js

Lets make a split panel. where the center is our grid listing presidents. en the right panel will display the presidents details and links. Change the code in MainPanel.js to the following:

MainPanel.js

// Creates namespaces to be used for scoping variables and classes so that they are not global.
Ext.namespace('GO.presidents');

GO.presidents.MainPanel = function(config){
	
	if(!config)
	{
		config = {};
	}
	
	this.centerPanel = new GO.presidents.PresidentsGrid({
		region:'center',
		id:'pm-center-panel',
		border:true
	});
	
	this.centerPanel.on("delayedrowselect",function(grid, rowIndex, r){
		this.eastPanel.load(r.data.id);		
	}, this);

	this.centerPanel.store.on('load', function(){
			this.eastPanel.reset();
	}, this);
	
	this.eastPanel = new GO.presidents.PresidentPanel({
		region:'east',
		id:'pm-east-panel',
		width:440,
		border:true
	});


	//Setup a toolbar for the grid panel
	config.tbar = new Ext.Toolbar({		
			cls:'go-head-tb',
			items: [
				{
					iconCls: 'btn-add',							
					text: GO.lang['cmdAdd'],
					cls: 'x-btn-text-icon',
					handler: function(){					
						this.centerPanel.showEditDialog();
					},
					scope: this
				},
				{
					iconCls: 'btn-delete',
					text: GO.lang['cmdDelete'],
					cls: 'x-btn-text-icon',
					handler: function(){
						this.centerPanel.deleteSelected();
					},
					scope: this
				}
			]
	});
	
	config.items=[
	this.centerPanel,
	this.eastPanel
	];	
	
	config.layout='border';
	GO.presidents.MainPanel.superclass.constructor.call(this, config);	
};

Ext.extend(GO.presidents.MainPanel, Ext.Panel, {});


/*
 * This will add the module to the main tabpanel filled with all the modules
 */
GO.moduleManager.addModule(
	'presidents', //Module alias
	GO.presidents.MainPanel, //The main panel for this module
	{
		title : 'Presidents', //Module name in startmenu
		iconCls : 'go-module-icon-presidents' //The css class with icon for startmenu
	}
);

/*
 * Add linkHandeler for linking models from this module to other GroupOffice models
 */
GO.linkHandlers["GO_Presidents_Model_President"]=function(id){
	if(!GO.presidents.linkWindow){
		var presidentPanel = new GO.presidents.PresidentPanel();
		GO.presidents.linkWindow= new GO.LinkViewWindow({
			title: GO.presidents.lang.president,
			items: presidentPanel,
			presidentPanel: presidentPanel,
			closeAction:"hide"
		});
	}
	GO.presidents.linkWindow.presidentPanel.load(id);
	GO.presidents.linkWindow.show();
	return GO.presidents.linkWindow;
}

As you can see in the New MainPanel.js view this panel has a 'config.tbar' defines with the 2 'add' and 'delete' buttons. Note that the reference to the showEditDialog() method has changed to 'this.centerPanel' instead of 'this' because the centerPanel now represents the PresidentsGrid.

So this buttons can be removed from the toolbar in the PresidentGrid.js

So change to config.tbar option in the PresidentGrid.js to the following.

PresidentGrid.js:

config.tbar = new Ext.Toolbar({
			items: [
				GO.lang.strSearch + ':',
				new GO.form.SearchField({
					store: config.store,
					width:320
				})
			]
	})

This way only the searchfield will remain in the toolbar of the PresidentsGrid. The Add and Delete button are already placed on the MainPanel's toolbar

Extending the defaults

Do you see the Add and Edit button in top of your panel being grayed out. The main reason for this it because the edit panel has build in functionality the check if you have permission the read or write a record. this way you can set this permission per record.

For our presidents example it's not very logic to have these permission per record. We didn't specify this in the database and are not going to use it in this example. for this reason. we need to overwrite the data that we get from the server.

Lets have o look at what's returned:

{"data":
	{"id":"40",
	"party_id":"6",
	"firstname":"Ronald",
	"lastname":"Reagan",
	"tookoffice":"20-01-1981",
	"leftoffice":"20-01-1989",
	"income":"99,867,297.35",
	"model":"GO_Presidents_Model_President",
	"permission_level":4,
	"write_permission":true,
	"customfields":[{
		"id":"1",
		"name":"Appearance",
		"fields":[{
			"name":"Glasses",
			"value":"No"
		}]
	}],
	"links":[],
	"events":[],
	"tasks":[],
	"files":[],
	"comments":[]
	},
"success":true}

All display view in Group-Office are ExtJS components or Group-Office components that are extended from ExtJS components. All of these components are filled with JSON data. This data is loaded Asynchrone with Javascript. Let look at what data will be loaded

Let's modify the JSON data that is returned. Add the following function to the PresidentController

PresidentController.php:

protected function afterDisplay(&$response, &$model, &$params)
{
	$response['data']['partyName'] = $model->party->name;
	return parent::beforeDisplay($response, $model, $params);
}

With this the JSON data that is returned in actionDisplay() will be overwritten. a field partyName will be added to our returned JSON data. this will be used to display the party name in the PresidentPanel

Installation and upgrades

Files in the modules/presidents/install folder are important for installing, uninstalling and upgrading modules. install.sql and uninstall.sql are automatically applied to the database when installing or uninstalling. updates.php is used for upgrading.

It looks like this for example:

<?php
$updates["201108131011"][]="ALTER TABLE `ab_companies` CHANGE `id` `id` INT( 11 ) NOT NULL AUTO_INCREMENT";
$updates["201108131011"][]="ALTER TABLE `ab_contacts` CHANGE `id` `id` INT( 11 ) NOT NULL AUTO_INCREMENT";
$updates["201108131011"][]="ALTER TABLE `ab_contacts` ADD `go_user_id` INT NOT NULL , ADD INDEX ( `go_user_id` )";
$updates["201204270920"][]="script:1_some_script.php";

The timestamp is in this format:

"YYYYMMDDHHMM"

The timestamp is important because all updates are executed chronologically. Because modules can depend on each others updates.

You can also use scripts. It's good practice to avoid using update scripts as much as possible. When scripts use functions of the framework and they change later they might break. The entry "script:1_some_script.php" will include modules/[moduleid]/install/updatescripts/1_some_script.php.

Note: When putting update queries in the updates.php file, don't forget to update install.sql too.