Step by Step Todo module Chapter3 - mymagic/open_hub GitHub Wiki
SCENARIO: after publishing the module, you receive a new request from your boss:
As admin, I would like to see any pending TODO tasks associated with a specific startup or company.
After analyzing the requirement, these are the action plans (translated to developer language):
- A TODO to be associated to an organization
- A TODO to have statuses (new, pending, in progress, done)
- Users to be able to see list of TODO associated to a specific organization in the organization view page.
We will need to modify the database structure of todo table to add in the following new columns:
-
organization_id- foreign key to link to organization table -
status- an enum to keep track of the TODO current status
-
The modular architecture of OpenHub comes with built-in versioning control. Since we are upgrading Todo module, we should change its version numbering in
protected/modules/todo/config/about.yaml- Set
versionfrom1.0to1.1
- Set
-
Create a new
migrationinstruction file atprotected/modules/todo/upgrades/upgrade-1.1.php:
<?php
function upgrade_module_1_1($about)
{
$migration = Yii::app()->db->createCommand();
$migration->addColumn('todo', 'organization_id', 'integer NULL');
$migration->addColumn('todo', 'status', "enum('new','pending','processing','done','cancel','fail') DEFAULT 'new'");
// addForeignKey ( $name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null )
$migration->addForeignKey('fk_todo-organization_id', 'todo', 'organization_id', 'organization', 'id', 'SET NULL', 'CASCADE');
return "Upgraded to version 1.1\n";
}Instructions in this file will run automatically when you upgrade your module.
Click on Upgrade button to upgrade Todo module from backend.

You will see this when Upgrade migration completed successfully.

Table changes will be reflected in database.

For more information on Module Upgrade, please refer to Module Install & Upgrade
Next, we will need to regenerate the model, controller and view to reflect changes in the database.
Update protected/modules/todo/data/todo.build.php to
<?php
return array(
'layout' => 'backend',
'menuTemplate' => array(
'index' => 'admin, create',
'admin' => 'create',
'create' => 'admin',
'update' => 'admin, create, view',
'view' => 'admin, create, update, delete',
),
'admin' => array(
// version 1.1
'list' => array('id', 'organization_id', 'title', 'user_id', 'status', 'date_added', 'date_modified'),
),
'structure' => array(
'json_extra' => array('isJson' => true),
// version 1.1
'status' => array(
'isEnum' => true, 'enumSelections' => array(
'new' => 'New',
'pending' => 'Pending',
'processing' => 'Processing',
'done' => 'Done',
'cancel' => 'Cancel',
'fail' => 'Fail',
),
),
),
// this foreignKey is mainly for crud view generation. model relationship will not use this at the moment
'foreignKey' => array(
'user_id' => array('relationName' => 'user', 'model' => 'User', 'foreignReferAttribute' => 'username'),
// version 1.1
'organization_id' => array('relationName' => 'organization', 'model' => 'Organization', 'foreignReferAttribute' => 'title'),
),
'json' => array(
'extra' => array(
)
),
);Go to Yee Model Generator at https://mydomain.com/yee/model
- Set
Table Nametotodo - Set
Model Pathtoapplication.modules.todo.models - Set
Module Nametotodo - [!important] Uncheck
Build Extend Class - Click Preview
- If
modules/todo/models/TodoBase.phpis in red, clickdifflink to compare the difference between new and current model codes - You should also takes the opportunity to compare any changes to those functions you had overwritten in
modules/todo/models/Todo.phpin this case, and update accordingly manually - Check [overwrite] and click the Generate button when everything is good to go
Go to Yee CRUD Generator https://mydomain.com/yee/crud/index
- Set
Model Classtoapplication.modules.todo.models.Todo - Set
Controller IDtotodo/todo - Do not jump straightaway into regenerate the code files to replace existing files. Remember we had done quite some changes here on the view
_form.php? It's best to use the[diff]to carefully check what changes will be done to each of the files and apply them manually. - Edit
protected/modules/todo/views/todo/_form.php, add in the following blocks:
<div class="form-group <?php echo $model->hasErrors('organization_id') ? 'has-error':'' ?>">
<?php echo $form->bsLabelEx2($model,'organization_id'); ?>
<div class="col-sm-10">
<?php echo $form->bsForeignKeyDropDownList($model, 'organization_id'); ?>
<?php echo $form->bsError($model,'organization_id'); ?>
</div>
</div>
<div class="form-group <?php echo $model->hasErrors('status') ? 'has-error':'' ?>">
<?php echo $form->bsLabelEx2($model,'status'); ?>
<div class="col-sm-10">
<?php echo $form->bsEnumDropDownList($model, 'status'); ?>
<?php echo $form->bsError($model,'status'); ?>
</div>
</div>- Go back to the Yee CRUD Generator, check and regenerate for all files except
modules/todo/views/todo/_form.php.
Now, we can create a TODO record and link it to a specific company.
Remember one of the requirements is to allow admin to click into an organization and get a list of associated TODO? Luckily, OpenHub support Model Behavior Injection, where module developer can attach custom functions to existing core model, e.g. Organization in this case.
- Rename
protected/modules/todo/components/BoilerplateStartOrganizationBehavior.phptoprotected/modules/todo/components/TodoOrganizationBehavior.phpand replace its content with:
<?php
Yii::import('modules.todo.models.*');
// this allow you to inject method into organization object
class TodoOrganizationBehavior extends Behavior
{
// the organization model
public $model;
//
// items
public function countAllTodoItems()
{
return HubTodo::countAllTodos($this->model);
}
public function getTodoItems($limit = 100)
{
return HubTodo::getTodos($this->model, $limit);
}
public function shout()
{
return 'I am from todo module!';
}
}- Modify
protected/modules/todo/models/HubTodo.php, replace with content:
<?php
class HubTodo
{
public function countAllTodos($organization)
{
return Todo::model()->countByAttributes(array(
'organization_id'=> $organization->id
));
}
public function getTodos($organization, $limit = 100)
{
return Todo::model()->findAll(array(
'condition' => 'organization_id=:organizationId',
'params' => array(':organizationId'=> $organization->id),
'limit' => $limit,
'order' => 'id DESC'
));
}
}- Replace the
modelBehaviorssection ofprotected/modules/todo/config/console.phpandprotected/modules/todo/config/main.php, also their respectivedistfiles with the following codes to enablemodules > todo > modelBehaviors > Organization. Ensure brackets are enclosed.
'modelBehaviors' => array(
'Organization' => array(
'class' => 'application.modules.todo.components.TodoOrganizationBehavior',
),- Modify
protected/modules/todo/controllers/TestController.php, replaceactionBoilerplateStartOrganizationBehavior()withactionTodoOrganizationBehavior()and with content:
public function actionTodoOrganizationBehavior($id)
{
$organization = Organization::model()->findByPk($id);
foreach($organization->getTodoItems() as $todo)
{
echo sprintf('<li>#%d - %s</li>', $todo->id, $todo->title);
}
}Next, make sure you have created few todo items from backend and linked it to a specific organization. Get its organization id, append that to the end of URL, e.g. https://mydomain.com/todo/test/todoOrganizationBehavior?id=999
You should now be able to see list of todos associated to this organization on the page.
- It will be convenient for us to see the list of TODOs in the organization details page. Let's add a new tab beneath the page.
Modify protected/modules/todo/TodoModule.php, change getOrganizationViewTabs() function to:
public function getOrganizationViewTabs($model, $realm = 'backend')
{
$tabs = array();
if ($realm == 'backend') {
if (Yii::app()->user->accessBackend) {
$tabs['todo'][] = array(
'key' => 'todo',
'title' => 'Todo',
'viewPath' => 'modules.todo.views.backend._view-organization-todo',
);
}
}
return $tabs;
}Create a new partial view at protected/modules/todo/views/backend/_view-organization-todo.php:
<?php $todos = $model->getTodoItems() ?>
<ol>
<?php foreach($todos as $todo): ?>
<li><span class="label label-default"><?php echo $todo->formatEnumStatus($todo->status) ?></span> <a href="<?php echo $this->createUrl('todo/todo/view', array('id'=>$todo->id)) ?>"><?php echo $todo->title ?></a> by <?php echo $todo->user->username ?> created on <?php echo Html::formatDateTime($todo->date_added) ?></li>
<?php endforeach; ?>
</ol>Now, we view list of TODO in the tab of the organization details page at https://yourdomain.com/organization/view?id=999.
For more information on view tab injection, please refer to Module Function Hooks \ getOrganizationViewTabs
Next, we would like to add an extra button to easily create new TODO in the tab.
- Modify
getOrganizationActions()inprotected/modules/todo/TodoModule.php
public function getOrganizationActions($model, $realm = 'backend')
{
if ($realm == 'backend') {
if (!Yii::app()->user->accessBackend) {
return;
}
$actions['todo'][] = array(
'visual' => 'primary',
'label' => 'Create TODO',
'title' => 'Create TODO for this organization',
'url' => Yii::app()->controller->createUrl('/todo/todo/create', array('organizationId' => $model->id)),
);
} elseif ($realm == 'cpanel') {
}
return $actions;
}- It will be better if we can auto select the specific organization in create page, saving user's time of selecting from the list. Edit
actionCreate()inprotected/modules/todo/controllers/TodoController.php
public function actionCreate($organizationId='')
{
$model=new Todo;
$model->organization_id = $organizationId;
...- Replace the content of _view-organization-todo.php with this code:
<?php $todos = $model->getTodoItems() ?>
<?php if(!empty($actions['todo'])): ?>
<div class="row">
<div class="col col-md-12">
<div class="well text-center">
<?php foreach($actions['todo'] as $action): ?>
<a class="margin-bottom-sm btn btn-<?php echo $action['visual']?>" href="<?php echo $action[url] ?>" title="<?php echo $action['title'] ?>"><?php echo $action[label] ?></a>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
<ol>
<?php foreach($todos as $todo): ?>
<li><span class="label label-default"><?php echo $todo->formatEnumStatus($todo->status) ?></span> <a href="<?php echo $this->createUrl('todo/todo/view', array('id'=>$todo->id)) ?>"><?php echo $todo->title ?></a> by <?php echo $todo->user->username ?> created on <?php echo Html::formatDateTime($todo->date_added) ?></li>
<?php endforeach; ?>
</ol>#todo...
Chapter 4 - Implement Web API