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)
(Minify your code)
Line 1,351: Line 1,351:
  
 
to minify all the javascript files. GO will use the minified script when $config['debug']=false; is set.
 
to minify all the javascript files. GO will use the minified script when $config['debug']=false; is set.
 +
 +
<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.

Revision as of 09:43, 27 January 2009

Group-Office makes it very easy to rapidly develop an application. You can create your first basic module in about 15 minutes!

The Basics

First set Group-Office to debugging mode so it won't use compressed javascript files. Set $config['debug']=true; in config.php.

In this wiki we will create a module called TestModule.

Create a folder testmodule in the modules folder (always use a name without special characters).

In this folder we now create a subfolder called classes, where we will keep our module class.

Create directories:

modules/testmodule/
modules/testmodule/classes/

Always use the following resources when developing for Group-Office:

  1. Group-Office PHP API documentation
  2. ExtJS 2.2 API documentation
  3. ExtJS learing center

Creating an empty module

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:

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

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

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

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

}

/*
 * Extend the base class
 */
Ext.extend(GO.testmodule.MainPanel, Ext.Panel,{

});

/*
 * This will add the module to the main tabpanel filled with all the modules
 */
GO.moduleManager.addModule('testmodule', GO.testmodule.MainPanel, {
	title : 'Test Module',
	iconCls : 'go-module-icon-testmodule'
});

Now we need to tell Group-Office to include this main panel each time our module is loaded.

For Group-Office to know what scripts to include we need a file called scripts.txt in our modules/testmodule/ folder.

scripts.txt:

modules/testmodule/MainPanel.js

Without this file your module will never work!

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

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.

Everything that works in the ExtJS framework works in GO. Do study the Ext extensions we already created for GO in the documentation.


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 as a zip file and can be downloaded here: Media:Testmodule.zip

Before we start

First, we need to import some data about the former Presidents of the USA.

Import data into MySQL:

CREATE TABLE IF NOT EXISTS `tm_parties` (
  `IDparty` int(11) NOT NULL auto_increment,
  `partyName` varchar(40) NOT NULL,
  PRIMARY KEY  (`IDparty`)
);

INSERT INTO `tm_parties` (`IDparty`, `partyName`) VALUES
(1, 'No Party'),
(2, 'Federalist'),
(3, 'Democratic-Republican'),
(4, 'Democratic'),
(5, 'Whig'),
(6, 'Republican');

CREATE TABLE IF NOT EXISTS `tm_presidents` (
  `id` int(11) NOT NULL auto_increment,
  `IDparty` 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,
  PRIMARY KEY  (`id`)
);

INSERT INTO `tm_presidents` (`id`, `IDparty`, `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);

And another query to tell Group-Office what the nextid of the presidents table is.

INSERT INTO `go_db_sequence` (seq_name, nextid) VALUES ('tm_presidents',42)

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.

So let's first take a look at the PHP part.

PHP code

For this module we are going to need 3 PHP files:

  • 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:

<?php

require_once("../../Group-Office.php");

//Authenticate the user
$GO_SECURITY->json_authenticate('testmodule');

//Require the module class
require_once ($GO_MODULES->modules['testmodule']['class_path']."presidents.class.inc.php");
$presidents = new presidents();

$task=isset($_REQUEST['task']) ? $_REQUEST['task'] : '';

try{

	switch($task)
	{			

		/* {TASKSWITCH} */
		
		case 'save_president':
			/*
			 * This task is executed when we want to update a president with 
			 * 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);

json.php:

<?php

require_once('../../Group-Office.php');

//Authenticate the user
$GO_SECURITY->json_authenticate('testmodule');

//Require the module class
require_once ($GO_MODULES->modules['testmodule']['class_path'].'presidents.class.inc.php');
$presidents = new presidents();

$task=isset($_REQUEST['task']) ? $_REQUEST['task'] : '';

try{

	switch($task)
	{
		
		/* {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)
					{
						$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;

	}
}catch(Exception $e)
{
	$response['feedback']=$e->getMessage();
	$response['success']=false;
}

// return $response to the client in an JSON encoded string 
echo json_encode($response);

presidents.class.inc.php:

<?php

/*
* Each module class file extends the db class
*/
class presidents extends db {

	/**
	 * 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');
		if($this->insert_row('tm_presidents', $president))
		{
			return $president['id'];
		}
		return false;
	}

	/**
	 * 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);
	}

	/**
	 * 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);
	}

	/**
	 * 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);	
	}

 	/**
	 * Gets all Parties
	 *
	 * @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_parties($sortfield='IDparty', $sortorder='ASC', $start=0, $offset=0, $partyName='')
	{
		$sql = "SELECT ";
		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);
	}

}


Creating a GridPanel

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.

Create a file modules/testmodule/PresidentsGrid.js

PresidentsGrid.js:

// Creates a namespace to be used for scoping variables and classes
Ext.namespace('GO.testmodule');

/*
 * 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.split=true;
	config.store = new GO.data.JsonStore({
		/*
		 * Here we store our remotely-loaded JSON data from json.php?task=presidents
		 */
		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
	});	

Our PresidentsGrid uses a ColumnModel to display it's data.

A ColumnModel is a class initialized with an Array of column config objects

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.

PresidentsGrid.js:


	/*
	 * ColumnModel used by our PresidentsGrid
	 */
	var PresidentsColumnModel = new Ext.grid.ColumnModel(
		[{
			header: '#',
			readOnly: true,
			dataIndex: 'id',
			width: 50
		},{
			header: 'First Name',
			dataIndex: 'firstname',
			width: 120
		},{
			header: 'Last Name',
			dataIndex: 'lastname',
			width: 120
		},{
			header: 'ID party',
			readOnly: true,
			dataIndex: 'partyid',
			width: 50,
			hidden: true
		},{
			header: 'Party',
			dataIndex: 'partyName',    
			width: 120,
		},{
			header: 'Took Office',
			dataIndex: 'tookoffice',
			width: 80
		},{
			header: 'Left Office',
			dataIndex: 'leftoffice',
			width: 80
		},{
			header: "Income",
			dataIndex: 'income',
			width: 120
		}]
	);

Here we give config some basic parameters about our ColumnModel and Grid and call the parent constructor.

PresidentsGrid.js:

	PresidentsColumnModel.defaultSortable= true;
	config.cm=PresidentsColumnModel;

	config.view=new Ext.grid.GridView({
		emptyText: GO.lang['strNoItems']
	});

	config.sm=new Ext.grid.RowSelectionModel();
	config.loadMask=true;

	/*
	 * explicitly call the superclass constructor
	 */
	GO.testmodule.PresidentsGrid.superclass.constructor.call(this, config);	
				
};

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.

PresidentsGrid.js:

/*
 * Extend the base class
 */
Ext.extend(GO.testmodule.PresidentsGrid, GO.grid.GridPanel,{

	loaded : false,

	afterRender : function()
	{
		GO.testmodule.PresidentsGrid.superclass.afterRender.call(this);
	
		if(this.isVisible())
		{
			this.onGridShow();
		}
	},

	onGridShow : function(){
		if(!this.loaded && this.rendered)
		{
			this.store.load();
			this.loaded=true;
		}
	},
    
});

Let's not forget to add our new grid to scripts.txt.

scripts.txt:

modules/testmodule/MainPanel.js
modules/testmodule/PresidentsGrid.js

In the constructor in MainPanel.js we can now create an object of our PresidentsGrid.

MainPanel.js:

var centerPanel = new GO.testmodule.PresidentsGrid({
	region:'center',
	title:GO.lang.menu,
	autoScroll:true,		
	width:250,
	split:true
});

config.items=[
	centerPanel
];

config.layout='border';


Creating a Dialog

We now are going to create a Dialog which will make editing presidents very easy.

Create a file modules/testmodule/PresidentDialog.js

PresidentDialog.js:

// Creates a namespace to be used for scoping variables and classes
Ext.namespace('GO.testmodule');

/*
 * This is the constructor of our PresidentDialog
 */
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
		}					
	];

	/*
	 * explicitly call the superclass constructor
	 */
	GO.testmodule.PresidentDialog.superclass.constructor.call(this, config);
	
	this.addEvents({'save' : true});	

}

Extend the base class Ext.Window with a few more functions.

PresidentDialog.js:

/*
 * Extend the base class
 */
Ext.extend(GO.testmodule.PresidentDialog, Ext.Window,{
	
	show : function (president_id) {

		if(!this.rendered)
			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
		});
		
	},

Here we create a Panel that will represent our actual form for editing presidents.

PresidentDialog.js:

									
	buildForm : function () {
		
		this.presidentPanel = new Ext.Panel({
			border: false,
			cls:'go-form-panel',waitMsgTarget:true,
			layout:'form',
			autoScroll:true,
			items: [{   
				name: 'firstname',
				xtype: 'textfield',
				fieldLabel: 'First Name',
				anchor: '100%',
				allowBlank:false,
				maskRe: /([a-zA-Z0-9\s]+)$/
			},{
				name: 'lastname',
				xtype: 'textfield',
				fieldLabel: 'Last Name',
				anchor: '100%',
				allowBlank:false,
				maskRe: /([a-zA-Z0-9\s]+)$/
			},this.partyName = new GO.form.ComboBox({
				hiddenName:'IDparty',
				fieldLabel:'Party',
				anchor:'100%',			     
				store: new GO.data.JsonStore({
					url: GO.settings.modules.testmodule.url+ 'json.php',
					baseParams: {						    	
						task: 'parties'
					},
					root:'results',
					id:'partyName',
					totalProperty:'total',
					fields: ['IDparty', 'partyName'],
					remoteSort: true
				}),
				valueField:'IDparty',
				displayField:'partyName',
				mode: 'remote',
				triggerAction: 'all',
				forceSelection: true,
				selectOnFocus:true,
				allowBlank: false
			}),this.tookoffice = new Ext.form.DateField({
				name: 'tookoffice',
				fieldLabel: 'Entering Office',
				format: GO.settings['date_format'],
				allowBlank: false,
				anchor:'100%'
			}),this.leftoffice = new Ext.form.DateField({
				name: 'leftoffice',
				fieldLabel: 'Leaving Office',
				format: GO.settings['date_format'],			    
				allowBlank: false,
				anchor: '100%'
			}),this.income = new GO.form.NumberField({
				name: 'income',
				fieldLabel: 'Income',
				allowBlank: false,
				allowDecimals: true,
				allowNegative: false
			})]
		});


		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
		});	    
	}
});

We define a handler on a doubleclick event: rowdblclick.

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.

Add this within the MainPanel constructor.

MainPanel.js:

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);

To conclude this part we add our PresidentDialog.js to scripts.txt.

scripts.txt:

modules/testmodule/MainPanel.js
modules/testmodule/PresidentsGrid.js
modules/testmodule/PresidentDialog.js

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 MainPanel constructor.

MainPanel.js:

var northPanel = new Ext.Panel({
	region: 'north',
	baseCls:'x-plain',
	split: true,
	resizable:false,
	tbar: new Ext.Toolbar({		
		cls:'go-head-tb',
		items: [{
			iconCls: 'btn-add',							
			text: GO.lang['cmdAdd'],
			cls: 'x-btn-text-icon',
			handler: function(){
				this.presidentDialog = new GO.testmodule.PresidentDialog();
				this.presidentDialog.on('save', function(){
					centerPanel.getStore().reload();
				}, this);
				this.presidentDialog.show();
			},
			scope: this
		},{
			iconCls: 'btn-delete',
			text: GO.lang['cmdDelete'],
			cls: 'x-btn-text-icon',
			handler: function(){
				centerPanel.deleteSelected();
			},
			scope: this
		}]
	})
});

Show the toolbar at the top of the panel.

MainPanel.js:

config.items=[
	northPanel,
	centerPanel
];


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 constructor 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/testmodule/themes/
modules/testmodule/themes/Default/

Now we create the file modules/testmodule/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-testmodule {
}

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.

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, cell){ 
	var str = '';
 	if(value > 1000000){
    	str = "<span style='color:#336600;'>$ " + value + "</span>";
 	} else if (value > 100000){
    	str = "<span style='color:#FF9900;'>$ " + value + "</span>";
 	} else {
    	str = "<span style='color:#CC0000;'>$ " + value + "</span>";
	}
	return str; 
}

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

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:

<?php
require_once($GO_MODULES->modules['customfields']['class_path'].'customfields.class.inc.php');
$cf = new customfields();
echo $cf->get_javascript(3, 'Companies');

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. Now you can add them to a tabpanel for example: In this example variable items is an array of Ext Panels:

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]);
	}
}

Or we can add it to an Ext Xtemplate:

if(GO.customfields)
{
	template +=GO.customfields.displayPanelTemplate;
}

Where the var template is a basic Ext Xtemplate config string.

We must supply the JSON data for the template and/or the formpanels. In json.php for the Xtemplate:

if(isset($GO_MODULES->modules['customfields']))
{
	require_once($GO_MODULES->modules['customfields']['class_path'].'customfields.class.inc.php');
	$cf = new customfields();
	$response['data']['customfields']=
		$cf->get_all_fields_with_values(
			$GO_SECURITY->user_id, 3, $company_id);			
}

In json.php for the FormPanels:

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);			
}

We must save the submitted values in action.php:

if(isset($GO_MODULES->modules['customfields']))
{
	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);
}

That's it. Now you have your own custom fields in a module. You module must create on table for the custom fields. In this case:


CREATE TABLE IF NOT EXISTS `cf_3` (
  `link_id` int(11) NOT NULL default '0',
  PRIMARY KEY  (`link_id`)
) ENGINE=MyISAM;


Making your items linkable

  1. 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:
    /*
    * 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});
    }
  2. Create a style for the icon to display next to your linked item:
    .go-link-icon-100 {
    background-image
    url('images/16x16/icon-notepad.png') !important;
    width
    16px;
    height
    16px;
    }
    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.


Maintaining modules

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.

A module must have an "install" directory with the following files:

  • install.inc.php: Is executed when a module is installed
  • uninstall.inc.php: Is executed when a modules is uninstalled
  • 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:

<?php
$updates[] = "ALTER TABLE `ab_mailing_contacts` ADD `mail_sent` ENUM( '0', '1' ) NOT NULL ;";
$updates[] = "ALTER TABLE `ab_mailing_contacts` ADD `status` VARCHAR( 100 ) NOT NULL ";
$updates[] = "script:1.inc.php";

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.

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".

Important! Queries will be performed from top to bottom. So put new queries at the bottom of the updates.inc.php file.


Generate code for a module

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:

http://sourceforge.net/project/showfiles.php?group_id=76359

Extract it and enter the directory. You must have the php5 command line interface installed (On Debian/Ubuntu: apt-get install php5-cli).

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.

Edit config.inc.php and paste the following in it:

<?php
require('../../www/Group-Office.php');
//name of the module. No spaces or strange characters.
$module = 'notes2';

//Short name of the module. The prefix of the database tables.
$prefix = 'no';

$tables=array();
//Tables to create an interface for


/*
 * If you specify a link_type then linking will be enabled. Make sure your table also has a 
 * ctime and mtime column for this to work. Also either authenticate or authenticate related must be set.
 * 
 * If you specify authenticate. Then make sure your table has an acl_read and acl_write column
 */

$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;

$tables[] = array(
	'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

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.

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!


Adding code later on

If you want to add code for table later on you can do this easily. Change the config file into:

<?php
require('../../www/Group-Office.php');

//name of the module. No spaces or strange characters.
$module = 'announcements';

//Short name of the module. The prefix of the database tables.
$prefix = 'su';

$tables=array();
//Tables to create an interface for


$westpanel = array(
	'mainpanel_tag'=> 'GRID',
	'template'=>'GridPanel.tpl',
	'name'=>'su_announcements', 
	'friendly_single'=>'announcement', 
	'friendly_multiple'=>'announcements',
	'authenticate'=>false,
	'paging'=>true,
	'autoload'=>true,
	'files'=>false);

$tables[] = $westpanel;

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.

Now you have to change the generated code to do something with the grids and dialog.


Minify your code

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 ( http://sourceforge.net/project/showfiles.php?group_id=76359 ).

Execute: ./minify.php /path/to/go

to minify all the javascript files. GO will use the minified script when $config['debug']=false; is set.

Note: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.