Creating a module

From Group-Office Groupware and CRM Documentation
Jump to: navigation, search

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.