Ilija Studen:Thanks for taking time to write this post. There is no module development documentation so all content explaining it is more than welcome.
My main problem with this is that I don't have a perspective of someone new to module development so I don't know which subjects to cover first. There are four articles in Developer Blog that describe some of the key concepts used by modules:
1. Anatomy of activeCollab Modules
2. Controllers
3. Request
4. Persistence Layer
If you have any specific questions, or if there are things you wish you knew when you first started, please post them and I'll see to write articles covering them in more details when I find some time.
Thanks again!
Ilija Studen:There is currently no document that describes available events, but you can search for event_trigger() function and you'll have all places where events are called.
on_admin_sections on_after_init on_after_object_save on_after_object_validation on_before_init on_before_object_deleted on_before_object_insert on_before_object_save on_before_object_update on_before_object_validation on_before_save_project on_build_menu on_class_missing on_comment_added on_comment_deleted on_comment_updated on_company_options on_company_quick_options on_company_tabs on_copy_project_items on_daily on_dashboard_important_section on_dashboard_sections on_frequently on_get_completable_project_object_types on_get_day_project_object_types on_get_project_object_types on_hourly on_master_categories on_milestone_add_links on_milestone_objects on_milestone_objects_by_visibility on_new_revision on_object_deleted on_object_inserted on_object_updated on_portal_milestone_add_links on_portal_milestone_objects on_portal_object_quick_options on_portal_permissions on_prepare_email on_project_completed on_project_created on_project_deleted on_project_export on_project_object_completed on_project_object_copied on_project_object_locked on_project_object_moved on_project_object_opened on_project_object_options on_project_object_quick_options on_project_object_ready on_project_object_reassigned on_project_object_restored on_project_object_trashed on_project_object_unlocked on_project_opened on_project_options on_project_overview_sidebars on_project_permissions on_project_tabs on_project_updated on_project_user_added on_project_user_removed on_project_user_updated on_quick_add on_shutdown on_system_permissions on_time_report_footer_options on_user_cleanup on_user_options on_user_quick_options
AngieObject->AngieApplication
on_after_init
on_before_init
on_shutdown
AngieObject->ApplicationMailer
on_prepare_email, array($tpl, $recipient_email, $context, $body, $subject, $attachments, $language)
AngieObject->DataObject
on_after_object_save, array('object' => $this)
on_after_object_validation, array('object' => $this, 'errors' => $errors)
on_before_object_deleted, array('object' => $this)
on_before_object_insert, array('object' => $this)
on_before_object_save, array('object' => $this)
on_before_object_update, array('object' => $this)
on_before_object_validation, array('object' => $this)
on_object_deleted, array('object' => $this)
on_object_inserted, array('object' => $this)
on_object_updated, array('object' => $this)
AngieObject->ProjectExporterOutputBuilder
on_project_export, array($exportable_modules, $this->active_project)
ApplicationController->AdminController
on_admin_sections, array($admin_sections)
ApplicationController->DashboardController
on_dashboard_important_section,array($important_items, $this->logged_user)
on_dashboard_sections, array($dashboard_sections, $this->logged_user)
on_quick_add, array($quick_add_urls)
BaseCompany->Company
on_company_options, array($this, $options, $user)
on_company_quick_options, array($this, $options, $user)
BaseProject->Project
on_before_save_project, array('project' => $this)
on_copy_project_items, array($this, $to, $milestones_map)
on_project_completed, array($this, $by, $status)
on_project_created, array($this, $template)
on_project_deleted, array($this)
on_project_opened, array($this, $status)
on_project_options, array($options, $this, $user)
on_project_updated, array($this)
on_project_user_added, array($this, $user, $role, $permissions)
on_project_user_removed, array($this, $user)
on_project_user_updated, array($this, $user, $role, $permissions)
BaseProjectObject->ProjectObject
on_portal_object_quick_options, array($options, $this, $portal, $commit, $file)
on_project_object_completed, array($this, $by, $comment)
on_project_object_copied, array($this, $copy, $project, $cascade)
on_project_object_locked, array($this, $by)
on_project_object_moved, array($this, $old_project, $project)
on_project_object_opened, array($this, $by)
on_project_object_options, array($options, $this, $user)
on_project_object_quick_options, array($options, $this, $user)
on_project_object_ready, array($this)
on_project_object_reassigned, array($this, $this->old_assignees, array($assignees, $owner_id))
on_project_object_restored, array($this)
on_project_object_trashed, array($this)
on_project_object_unlocked, array($this, $by)
BaseProjectObject->ProjectObject->Comment
on_comment_added, array($this, $parent)
on_comment_deleted, array($this, $parent)
on_comment_updated, array($this, $parent)
BaseTimeReport->TimeReport
on_time_report_footer_options, array($this, $this->footer_options, $project, $user)
BaseUser->User
on_user_cleanup, array($cleanup)
on_user_options, array($this, $options, $user)
on_user_quick_options, array($this, $options, $user)
DataManager
on_class_missing, array($item_class)
PageController->CronController
on_daily
on_frequently
on_hourly
PeopleController->CompaniesController
on_company_tabs, array($tabs, $this->logged_user, $this->active_company)
Permissions
on_portal_permissions, array($permissions)
on_project_permissions, array($permissions)
on_system_permissions, array($permissions)
ProjectsController->ProjectController
on_master_categories, array($category_definitions)
on_project_overview_sidebars, array($home_sidebars, $this->active_project, $this->logged_user)
on_project_tabs, array($tabs, $this->logged_user, $this->active_project)
ProjectsController->ProjectController->FilesController
on_new_revision, array($this->active_file, $last_revision, $this->logged_user)
ProjectsController->ProjectController->MilestonesController
on_milestone_add_links, array($this->active_milestone, $this->logged_user, $links)
on_milestone_objects_by_visibility, array($milestone, $objects, $object_visibility)
ProjectsController->ProjectController->Milestone
on_milestone_objects, array($this, $objects, $user)
on_portal_milestone_add_links, array($this, $links, $portal)
on_portal_milestone_objects, array($this, $portal_objects, $portal)
ProjectsController->ProjectController->PagesController
on_new_revision, array($this->active_page, $new_version, $this->logged_user)
ProjectsController->ProjectController->ProjectExporterController
on_project_export, array($exportable_modules, $this->active_project)
SettingsController->CategoriesAdminController
on_master_categories, array($category_definitions)
function.menu.php
on_build_menu, array($menu, $logged_user)
functions.php
on_get_completable_project_object_types, array(), array()
on_get_day_project_object_types, array(), array()
on_get_project_object_types, array(), array()
----last update: Feb 18, 2009 --------
I was trying to develop a module (plugin) for ac in the past couple days. Because there is not a really detailed step-by-step document for module development. It really took me a while to finally get what I want.
Now I am writing down my experience as a rough guide for developing an ac module. Hope it could help any of you and feel free to correct me on my mistakes. (To admin: please go ahead directly edit my post or add notes for any serious misleading issues.)
Basically how I learned to develop modules is to look at codes from official modules such as checklists and timetracking.
1. Location of the modules
All modules are placed in activecollab/application/modules. You can put your own modules under this directory and you will see them from the ac administration panel, under "Modules" page.
2. File structure of a module
From any other modules, you will see the following structure. What I did when I developing my module is just following the structure.
controllers ------- Controller action classes are here.
handlers ------- Handler classes are here. They define how this module behaves upon different events in ac. E.g. if you want your module shows on the tab in project detail page, you will need a on_project_tabs.php; if you want your module shows a link in the quick add list, you will need a on_quick_add.php.
helpers -------- Helper functions for the view layer (which are custom helper functions for the Smarty template engine ac using in its view layer).
layouts -------- If your module is going to have its whole new style, you will need write smarty template files for the overall design (header, footer stuff) of the new style.
modules -------- Model classes are here.
views -------- Smarty templates for the view layer.
handlers.php-------- Registers your own defined handler functions to ac events.
info.php -------- Provides module description, version info and uninstallation warning message.
init.php -------- Defines module name, path information, global functions.
lang_index.php and lang_index_js.php --------- I *think* they are for the {lang} tag in the smarty template files.
routes.php -------- This file basically encodes all the URL link patterns, use assemble_url() inside your controller to generate actual urls.
3. Model (Persistence) Layer
Now we can start to think about the design of model layer. Usually, a new module will introduce one or more object classes to deal with. There are two choices of writing an object class:
(1) if you want to have a separate database table to store the your object, you should write your class as a subclass of ApplicationObject. You must specify all the fields as they are in the db table.
class Note extends ApplicationObject { /** * All table fields * * @var array */ var $fields = array('id', 'body'); /** * Primary key fields * * @var array */ var $primary_key = array('id'); /** * Name of AI field (if any) * * @var string */ var $auto_increment = 'id'; /** * Construct the object and if $id is present load record from database * * @param mixed $id * @return */ function __construct($id = null) { $this->table_name = TABLE_PREFIX . 'mediabuyings'; parent::__construct($id); } function getId() { return $this->getFieldValue('id'); } function setId($value) { return $this->setFieldValue('id', $value); } function getBody() { return $this->getFieldValue('body'); } function setBody($value) { $this->setFieldValue('body', $value); } ... ... }How about the db table you are having in mind? Yes, you need to write a install.php contains sql queries of creating the db table. See install.php in the tickets module for the example.
(2) An alternative way to manage your objects is reuse the project_object table, this table has many basic fields such as user, created time, updated time. It also has a few integer, float and string fields to store your customized information. So what you have to do is write your class as a subclass of ProjectObject.
Such as Checklist.class.php in the checklists module. You can declare which fields you want to use in your case. (however I think id, type and module are mandatory).
var $fields = array( 'id', 'type', 'module', 'name', 'integer_field_1', 'float_field_1', ... );Now we have the model class. We should have another management class for manipulating model objects. It should contains common data retrieving functions such as findById(), findAll(), findByProject() etc., depending the design. For ApplicationObject models, management class can extends DataManager;
function find($arguments = null) { return DataManager::find($arguments, TABLE_PREFIX . 'your_table_name', 'YourModelClassName'); } // findAnd for ProjectObject models, retrieving will be handled by the helper functions provided by ProjectObject:
ProjectObjects::find(array( 'conditions' => array('project_id = ? AND type = ? AND state >= ? AND visibility >= ?', $project->getId(), $YourModule, $min_state, $min_visibility), 'order' => 'created_on', ));Naming practice of models in ac is singular for model class, plural for management class. But it's totally up to you. You will need to load them manually in the init.php anyway:
set_for_autoload(array( 'YourModel' => YOUR_MODULE_PATH . '/models/YourModel.class.php', 'YourManagement' => YOUR_MODULE_PATH . '/models/YourManagement.class.php' ));4. Controller Layer
Basically you will need a controller class to dispatch different action requests such as index, add, edit and delete. In each function of the class, you process data based on request parameters and use smarty->assign to store the data you want to display in the template.
First of all, a very different but useful design pattern in ac is to put common codes into the constructor. Usually in a constructor of an ac controller class, we will have codes dealing with adding breadcrumbs, retrieve an object if there is a request parameter named "id", assemble common URLs etc.
//constructor example function __construct($request){ //add some breadcrumb $this->wireframe->addBreadCrumb(lang('Your Module'), assemble_url('yourmappedurl', $this->active_project->getId())); //build some url $add_a_new_object_url = assemble_url('yourmappedurl', $this->active_project->getId()); //deal with request id $object_id = $request->getId('id'); if($object_id) { $this->active_your_object = YourDataManager::findById($object_id); } //assign with smarty $this->smarty->assign(array( 'active_your_object' => $this->active_your_object, 'add_a_new_object_url' => $add_a_new_object_url, )); }Consider the index page, we will usually want to list all objects. So a typical action will be like:
//index example function __index($request){ //retrieve all objects and assign with smarty //assign with smarty $this->smarty->assign(array( 'all_objects' => YourDataManager::find() )); }Add/Edit pages are similiar except edit page will need display the old value of the object. This is achieved by the following example code in the edit action:
$your_object_data = $this->request->post('your_object'); if(!is_array($your_object_data)) { $your_object_data = array( 'field1' => $this->active_your_object->getField1(), 'field2' => $this->active_your_object->getField2(), 'field3' => $this->active_your_object->getField3(), 'field4' => $this->active_your_object->getField4(), ); } // if $this->smarty->assign('your_object_data', $your_object_data);We can observe that this piece of codes will work in both displaying the form (the "if" body) and processing the submitted form.
5. Presentation Layer
If you want your controller action shows a web page eventually, you will need a smarty template, inside of which you will display the variables assigned by "$this->smarty->assign" in the action.
aC has a lot of built-in custom smarty tags that can really save your time, such as {title}, {add_bread_crumb}, {select_user}, {select_project} tag. They are usually named function.tag_name.php. When there's need, you can also write your tags for future reuse.
Besides aC's own tags, there's not much to say about the templates. They're standard smarty (http://www.smarty.net/manual/en/).
Some of the most useful smarty functions/tags include {assign}, {foreach}, {$your_variable|filter1|filter2}, {link}, {form}, {if}.
Yes, filters are very useful. For example you want to trim a long text to the size of 30, just use "excerpt" like {$object->getBody()|exerpt:30}.
6. Some tips when developing aC
Personally I prefer Eclipse with PHPEclipse plugin to develop aC. You can use Ctrl+Shirt+R (Open Resource) to open most of the php classes directly. For example you want to open the class for Task, just use Ctrl+Shift+R and enter Task. From the naming practice we know that "Task" is the actual model, "Tasks" contains business logics of Task and TasksController is the controller of the built-in Task module.
Also, open Ctrl+Shift+R and enter "function.*", you will get most of the aC customized smarty helper functions (other types of helpers are "block.*", "modifier.*"). Looking into these source codes will let you know what parameters these functions accept.
Although not very precise, the auto-completion feature of PHPEclipse can also help us a lot. Global variables defined by aC such as object state beginning with STATE_ and visibility beginning with VISIBILITY_, as well as global functions such as database functions beginning with db_, can also be auto-completed.
---------------to be continued-------------