Commit 2e73f774fe6dddafd09791e362a06b774e2dda77

Authored by bshuttle
1 parent 907deac9

Workflow can now generate notifications (tentative)


git-svn-id: https://kt-dms.svn.sourceforge.net/svnroot/kt-dms/trunk@4725 c91229c3-7414-0410-bfa2-8a42b809f60b
lib/dashboard/Notification.inc.php
@@ -9,6 +9,10 @@ require_once(KT_LIB_DIR . '/users/User.inc'); @@ -9,6 +9,10 @@ require_once(KT_LIB_DIR . '/users/User.inc');
9 require_once(KT_LIB_DIR . '/documentmanagement/Document.inc'); 9 require_once(KT_LIB_DIR . '/documentmanagement/Document.inc');
10 require_once(KT_LIB_DIR . '/foldermanagement/Folder.inc'); 10 require_once(KT_LIB_DIR . '/foldermanagement/Folder.inc');
11 11
  12 +require_once(KT_LIB_DIR . '/workflow/workflowutil.inc.php');
  13 +
  14 +require_once(KT_LIB_DIR . '/templating/templating.inc.php');
  15 +
12 /** 16 /**
13 * class Notification 17 * class Notification
14 * 18 *
@@ -71,6 +75,9 @@ class KTNotification extends KTEntity { @@ -71,6 +75,9 @@ class KTNotification extends KTEntity {
71 function render() { 75 function render() {
72 $notificationRegistry =& KTNotificationRegistry::getSingleton(); 76 $notificationRegistry =& KTNotificationRegistry::getSingleton();
73 $handler = $notificationRegistry->getHandler($this->sType); 77 $handler = $notificationRegistry->getHandler($this->sType);
  78 +
  79 + if (is_null($handler)) { return null; }
  80 +
74 return $handler->handleNotification($this); 81 return $handler->handleNotification($this);
75 } 82 }
76 83
@@ -313,4 +320,59 @@ class KTSubscriptionNotification extends KTNotificationHandler { @@ -313,4 +320,59 @@ class KTSubscriptionNotification extends KTNotificationHandler {
313 320
314 $notificationRegistry->registerNotificationHandler("ktcore/subscriptions","KTSubscriptionNotification"); 321 $notificationRegistry->registerNotificationHandler("ktcore/subscriptions","KTSubscriptionNotification");
315 322
  323 +class KTWorkflowNotification extends KTNotificationHandler {
  324 +
  325 + function & clearNotificationsForDocument($oDocument) {
  326 + $aNotifications = KTNotification::getList('data_int_1 = ' . $oDocument->getId());
  327 + foreach ($aNotifications as $oNotification) {
  328 + $oNotification->delete();
  329 + }
  330 +
  331 + }
  332 +
  333 + function & newNotificationForDocument($oDocument, $oUser, $oState, $oActor, $sComments) {
  334 + $aInfo = array();
  335 + $aInfo['sData1'] = $oState->getName();
  336 + $aInfo['sData2'] = $sComments;
  337 + $aInfo['iData1'] = $oDocument->getId();
  338 + $aInfo['iData2'] = $oActor->getId();
  339 + $aInfo['sType'] = 'ktcore/workflow';
  340 + $aInfo['dCreationDate'] = getCurrentDateTime();
  341 + $aInfo['iUserId'] = $oUser->getId();
  342 + $aInfo['sLabel'] = $oDocument->getName();
  343 +
  344 + return KTNotification::createFromArray($aInfo);
  345 + }
  346 +
  347 + function handleNotification($oKTNotification) {
  348 + $oTemplating =& KTTemplating::getSingleton();
  349 + $oTemplate =& $oTemplating->loadTemplate('ktcore/workflow/workflow_notification');
  350 + $oTemplate->setData(array(
  351 + 'context' => $this,
  352 + 'document_id' => $oKTNotification->getIntData1(),
  353 + 'state_name' => $oKTNotification->getStrData1(),
  354 + 'actor' => User::get($oKTNotification->getIntData2()),
  355 + 'document_name' => $oKTNotification->getLabel(),
  356 + 'notify_id' => $oKTNotification->getId(),
  357 + ));
  358 + return $oTemplate->render();
  359 + }
  360 +
  361 + function resolveNotification($oKTNotification) {
  362 + $notify_action = KTUtil::arrayGet($_REQUEST, 'notify_action', null);
  363 + if ($notify_action == 'clear') {
  364 + $_SESSION['KTInfoMessage'][] = _('Workflow Notification cleared.');
  365 + $oKTNotification->delete();
  366 + exit(redirect(generateControllerLink('dashboard')));
  367 + }
  368 +
  369 + $params = 'fDocumentId=' . $oKTNotification->getIntData1();
  370 + $url = generateControllerLink('viewDocument', $params);
  371 + $oKTNotification->delete(); // clear the alert.
  372 + exit(redirect($url));
  373 + }
  374 +}
  375 +
  376 +$notificationRegistry->registerNotificationHandler("ktcore/workflow","KTWorkflowNotification");
  377 +
316 ?> 378 ?>
lib/roles/roleallocation.inc.php
@@ -121,7 +121,7 @@ class RoleAllocation extends KTEntity { @@ -121,7 +121,7 @@ class RoleAllocation extends KTEntity {
121 foreach ($aUsers as $iUserId) { 121 foreach ($aUsers as $iUserId) {
122 $oUser = User::get($iUserId); 122 $oUser = User::get($iUserId);
123 if (!(PEAR::isError($oUser) || ($oUser == false))) { 123 if (!(PEAR::isError($oUser) || ($oUser == false))) {
124 - $aFullUsers[] = $oUser; 124 + $aFullUsers[$iUserId] = $oUser;
125 } 125 }
126 } 126 }
127 127
@@ -144,7 +144,7 @@ class RoleAllocation extends KTEntity { @@ -144,7 +144,7 @@ class RoleAllocation extends KTEntity {
144 foreach ($aGroups as $iGroupId) { 144 foreach ($aGroups as $iGroupId) {
145 $oGroup = Group::get($iGroupId); 145 $oGroup = Group::get($iGroupId);
146 if (!(PEAR::isError($oGroup) || ($oGroup == false))) { 146 if (!(PEAR::isError($oGroup) || ($oGroup == false))) {
147 - $aFullGroups[] = $oGroup; 147 + $aFullGroups[$iGroupId] = $oGroup;
148 } 148 }
149 } 149 }
150 150
lib/workflow/workflowutil.inc.php
@@ -10,6 +10,13 @@ require_once(KT_LIB_DIR . '/documentmanagement/DocumentTransaction.inc'); @@ -10,6 +10,13 @@ require_once(KT_LIB_DIR . '/documentmanagement/DocumentTransaction.inc');
10 require_once(KT_LIB_DIR . '/search/searchutil.inc.php'); 10 require_once(KT_LIB_DIR . '/search/searchutil.inc.php');
11 require_once(KT_LIB_DIR . '/roles/roleallocation.inc.php'); 11 require_once(KT_LIB_DIR . '/roles/roleallocation.inc.php');
12 12
  13 +require_once(KT_LIB_DIR . '/groups/Group.inc');
  14 +require_once(KT_LIB_DIR . '/users/User.inc');
  15 +require_once(KT_LIB_DIR . '/roles/Role.inc');
  16 +
  17 +require_once(KT_LIB_DIR . '/dashboard/Notification.inc.php');
  18 +
  19 +
13 class KTWorkflowUtil { 20 class KTWorkflowUtil {
14 // {{{ saveTransitionsFrom 21 // {{{ saveTransitionsFrom
15 /** 22 /**
@@ -102,7 +109,16 @@ class KTWorkflowUtil { @@ -102,7 +109,16 @@ class KTWorkflowUtil {
102 'state_id' => $iStartStateId, 109 'state_id' => $iStartStateId,
103 ); 110 );
104 $sTable = KTUtil::getTableName('workflow_documents'); 111 $sTable = KTUtil::getTableName('workflow_documents');
105 - return DBUtil::autoInsert($sTable, $aValues, $aOptions); 112 + $res = DBUtil::autoInsert($sTable, $aValues, $aOptions);
  113 +
  114 + // FIXME does this function as expected?
  115 + $oUser = User::get($_SESSION['userID']);
  116 +
  117 + $oTargetState = KTWorkflowState::get($iStartStateId);
  118 + KTWorkflowUtil::informUsersForState($oTargetState,
  119 + KTWorkflowUtil::getInformedForState($oTargetState), $oDocument, $oUser, '');
  120 +
  121 + return $res;
106 } 122 }
107 // }}} 123 // }}}
108 124
@@ -142,6 +158,14 @@ class KTWorkflowUtil { @@ -142,6 +158,14 @@ class KTWorkflowUtil {
142 'state_id' => $iStartStateId, 158 'state_id' => $iStartStateId,
143 ); 159 );
144 $sTable = KTUtil::getTableName('workflow_documents'); 160 $sTable = KTUtil::getTableName('workflow_documents');
  161 +
  162 + $oUser = User::get($_SESSION['userID']);
  163 + $oTargetState = KTWorkflowState::get($iStartStateId);
  164 +
  165 + KTWorkflowUtil::informUsersForState($oTargetState,
  166 + KTWorkflowUtil::getInformedForState($oTargetState), $oDocument, $oUser, '');
  167 +
  168 +
145 return DBUtil::autoInsert($sTable, $aValues, $aOptions); 169 return DBUtil::autoInsert($sTable, $aValues, $aOptions);
146 } 170 }
147 // }}} 171 // }}}
@@ -457,10 +481,103 @@ class KTWorkflowUtil { @@ -457,10 +481,103 @@ class KTWorkflowUtil {
457 $oDocumentTransaction = & new DocumentTransaction($oDocument, $sTransactionComments, 'ktcore.transactions.workflow_state_transition'); 481 $oDocumentTransaction = & new DocumentTransaction($oDocument, $sTransactionComments, 'ktcore.transactions.workflow_state_transition');
458 $oDocumentTransaction->create(); 482 $oDocumentTransaction->create();
459 483
  484 + KTWorkflowUtil::informUsersForState($oTargetState, KTWorkflowUtil::getInformedForState($oTargetState), $oDocument, $oUser, $sComments);
  485 +
460 return true; 486 return true;
461 } 487 }
462 // }}} 488 // }}}
463 489
  490 + // {{{ informUsersForState
  491 + function informUsersForState($oState, $aInformed, $oDocument, $oUser, $sComments) {
  492 + // say no to duplicates.
  493 +
  494 + KTWorkflowNotification::clearNotificationsForDocument($oDocument);
  495 +
  496 + $aUsers = array();
  497 + $aGroups = array();
  498 + $aRoles = array();
  499 +
  500 + foreach (KTUtil::arrayGet($aInformed,'user',array()) as $iUserId) {
  501 + $oU = User::get($iUserId);
  502 + if (PEAR::isError($oU) || ($oU == false)) {
  503 + continue;
  504 + } else {
  505 + $aUsers[$oU->getId()] = $oU();
  506 + }
  507 + }
  508 +
  509 + foreach (KTUtil::arrayGet($aInformed,'group',array()) as $iGroupId) {
  510 + $oG = Group::get($iGroupId);
  511 + if (PEAR::isError($oG) || ($oG == false)) {
  512 + continue;
  513 + } else {
  514 + $aGroups[$oG->getId()] = $oG();
  515 + }
  516 + }
  517 +
  518 + foreach (KTUtil::arrayGet($aInformed,'role',array()) as $iRoleId) {
  519 + $oR = Role::get($iRoleId);
  520 + if (PEAR::isError($oR) || ($oR == false)) {
  521 + continue;
  522 + } else {
  523 + $aRoles[] = $oR;
  524 + }
  525 + }
  526 +
  527 +
  528 +
  529 + // FIXME extract this into a util - I see us using this again and again.
  530 + // start with roles ... roles _only_ ever contain groups.
  531 + foreach ($aRoles as $oRole) {
  532 + $oRoleAllocation = RoleAllocation::getAllocationsForFolderAndRole($oDocument->getFolderID(), $oRole->getId());
  533 + $aRoleUsers = $oRoleAllocation->getUsers();
  534 + $aRoleGroups = $oRoleAllocation->getGroups();
  535 +
  536 + foreach ($aRoleUsers as $id => $oU) {
  537 + $aUsers[$id] = $oU;
  538 + }
  539 + foreach ($aRoleGroups as $id => $oGroup) {
  540 + $aGroups[$id] = $oGroup;
  541 + }
  542 + }
  543 +
  544 +
  545 +
  546 + // we now have a (potentially overlapping) set of groups, which may
  547 + // have subgroups.
  548 + //
  549 + // what we need to do _now_ is build a canonical set of groups, and then
  550 + // generate the singular user-base.
  551 +
  552 + $aGroupMembershipSet = GroupUtil::buildGroupArray();
  553 + $aAllIds = array_keys($aGroups);
  554 + foreach ($aGroups as $id => $oGroup) {
  555 + $aAllIds = array_merge($aGroupMembershipSet[$id], $aAllIds);
  556 + }
  557 +
  558 + foreach ($aAllIds as $id) {
  559 + if (!array_key_exists($id, $aGroups)) {
  560 + $aGroups[$id] = Group::get($id);
  561 + }
  562 + }
  563 +
  564 + // now, merge this (again) into the user-set.
  565 + foreach ($aGroups as $oGroup) {
  566 + $aNewUsers = $oGroup->getUsers();
  567 + foreach ($aNewUsers as $id => $oU) {
  568 + if (!array_key_exists($id, $aUsers)) {
  569 + $aUsers[$id] = $oU;
  570 + }
  571 + }
  572 + }
  573 +
  574 + // and done.
  575 + foreach ($aUsers as $oU) {
  576 + KTWorkflowNotification::newNotificationForDocument($oDocument, $oU, $oState, $oUser, $sComments);
  577 + }
  578 + }
  579 + // }}}
  580 +
464 // {{{ setInformedForState 581 // {{{ setInformedForState
465 /** 582 /**
466 * Sets which users/groups/roles are to be informed when a state is 583 * Sets which users/groups/roles are to be informed when a state is
plugins/ktcore/admin/workflows.php
@@ -20,6 +20,10 @@ require_once(KT_LIB_DIR . '/actions/documentaction.inc.php'); @@ -20,6 +20,10 @@ require_once(KT_LIB_DIR . '/actions/documentaction.inc.php');
20 20
21 require_once(KT_LIB_DIR . '/widgets/portlet.inc.php'); 21 require_once(KT_LIB_DIR . '/widgets/portlet.inc.php');
22 22
  23 +require_once(KT_LIB_DIR . '/users/User.inc');
  24 +require_once(KT_LIB_DIR . '/groups/Group.inc');
  25 +require_once(KT_LIB_DIR . '/roles/Role.inc');
  26 +
23 class WorkflowNavigationPortlet extends KTPortlet { 27 class WorkflowNavigationPortlet extends KTPortlet {
24 var $oWorkflow; 28 var $oWorkflow;
25 29
@@ -202,7 +206,58 @@ class KTWorkflowDispatcher extends KTAdminDispatcher { @@ -202,7 +206,58 @@ class KTWorkflowDispatcher extends KTAdminDispatcher {
202 } 206 }
203 207
204 function getNotificationStringForState($oState) { 208 function getNotificationStringForState($oState) {
205 - return _('No roles and groups notified.'); 209 + $aAllowed = KTWorkflowUtil::getInformedForState($oState);
  210 +
  211 + $aUsers = array();
  212 + $aGroups = array();
  213 + $aRoles = array();
  214 +
  215 + foreach (KTUtil::arrayGet($aAllowed,'user',array()) as $iUserId) {
  216 + $oU = User::get($iUserId);
  217 + if (PEAR::isError($oU) || ($oU == false)) {
  218 + continue;
  219 + } else {
  220 + $aUsers[] = $oU->getName();
  221 + }
  222 + }
  223 +
  224 + foreach (KTUtil::arrayGet($aAllowed,'group',array()) as $iGroupId) {
  225 + $oG = Group::get($iGroupId);
  226 + if (PEAR::isError($oG) || ($oG == false)) {
  227 + continue;
  228 + } else {
  229 + $aGroups[] = $oG->getName();
  230 + }
  231 + }
  232 +
  233 + foreach (KTUtil::arrayGet($aAllowed,'role',array()) as $iRoleId) {
  234 + $oR = Role::get($iRoleId);
  235 + if (PEAR::isError($oR) || ($oR == false)) {
  236 + continue;
  237 + } else {
  238 + $aRoles[] = $oR->getName();
  239 + }
  240 + }
  241 +
  242 + $sNotify = '';
  243 + if (!empty($aUsers)) {
  244 + $sNotify .= '<em>' . _('Users:') . '</em>';
  245 + $sNotify .= implode(', ', $aUsers);
  246 + }
  247 +
  248 + if (!empty($aGroups)) {
  249 + $sNotify .= '<em>' . _('Groups:') . '</em>';
  250 + $sNotify .= implode(', ', $aGroups);
  251 + }
  252 +
  253 + if (!empty($aRoles)) {
  254 + $sNotify .= '<em>' . _('Roles:') . '</em>';
  255 + $sNotify .= implode(', ', $aRoles);
  256 + }
  257 +
  258 + if (empty($sNotify)) { $sNotify = _('No users to be notified.'); }
  259 +
  260 + return $sNotify;
206 } 261 }
207 262
208 function transitionAvailable($oTransition, $oState) { 263 function transitionAvailable($oTransition, $oState) {
@@ -735,33 +790,28 @@ class KTWorkflowDispatcher extends KTAdminDispatcher { @@ -735,33 +790,28 @@ class KTWorkflowDispatcher extends KTAdminDispatcher {
735 $oState =& $this->oValidator->validateWorkflowState($_REQUEST['fStateId']); 790 $oState =& $this->oValidator->validateWorkflowState($_REQUEST['fStateId']);
736 $sTargetAction = 'editState'; 791 $sTargetAction = 'editState';
737 $sTargetParams = 'fWorkflowId=' . $oWorkflow->getId() . '&fStateId=' . $oState->getId(); 792 $sTargetParams = 'fWorkflowId=' . $oWorkflow->getId() . '&fStateId=' . $oState->getId();
738 - $aRoleIds = KTUtil::arrayGet($_REQUEST, 'fRoleIds');  
739 - if (empty($aRoleIds)) {  
740 - $aRoleIds = array(); 793 + $aNotification = (array) KTUtil::arrayGet($_REQUEST, 'fNotification');
  794 +
  795 + if (empty($aNotification['role'])) {
  796 + $aNotification['role'] = array();
741 } 797 }
742 - if (!is_array($aRoleIds)) { 798 + if (!is_array($aNotification['role'])) {
743 $this->errorRedirectTo($sTargetAction, _('Invalid roles specified'), $sTargetParams); 799 $this->errorRedirectTo($sTargetAction, _('Invalid roles specified'), $sTargetParams);
744 } 800 }
745 - $aGroupIds = KTUtil::arrayGet($_REQUEST, 'fGroupIds');  
746 - if (empty($aGroupIds)) {  
747 - $aGroupIds = array(); 801 +
  802 + if (empty($aNotification['group'])) {
  803 + $aNotification['group'] = array();
748 } 804 }
749 - if (!is_array($aGroupIds)) { 805 + if (!is_array($aNotification['group'])) {
750 $this->errorRedirectTo($sTargetAction, _('Invalid groups specified'), $sTargetParams); 806 $this->errorRedirectTo($sTargetAction, _('Invalid groups specified'), $sTargetParams);
751 } 807 }
752 - $aUserIds = KTUtil::arrayGet($_REQUEST, 'fUserIds');  
753 - if (empty($aUserIds)) {  
754 - $aUserIds = array();  
755 - }  
756 - if (!is_array($aUserIds)) {  
757 - $this->errorRedirectTo($sTargetAction, _('Invalid users specified'), $sTargetParams); 808 +
  809 + $aNotification['user'] = array(); // force override
  810 +
  811 + $res = KTWorkflowUtil::setInformedForState($oState, $aNotification);
  812 + if (PEAR::isError($res)) {
  813 + $this->errorRedirectTo($sTargetAction, sprintf(_('Failed to update the notification lists: %s'),$res->getMessage()), $sTargetParams);
758 } 814 }
759 - $aAllowed = array(  
760 - 'role' => $aRoleIds,  
761 - 'group' => $aGroupIds,  
762 - 'user' => $aUserIds,  
763 - );  
764 - KTWorkflowUtil::setInformedForState($oState, $aAllowed);  
765 $this->successRedirectTo($sTargetAction, _('Changes saved'), $sTargetParams); 815 $this->successRedirectTo($sTargetAction, _('Changes saved'), $sTargetParams);
766 } 816 }
767 // }}} 817 // }}}
templates/ktcore/workflow/editState.smarty
@@ -19,7 +19,8 @@ the &quot;sent&quot; &lt;strong&gt;transition&lt;/strong&gt; has been performed by a user.&lt;/p&gt; @@ -19,7 +19,8 @@ the &quot;sent&quot; &lt;strong&gt;transition&lt;/strong&gt; has been performed by a user.&lt;/p&gt;
19 </div> 19 </div>
20 </fieldset> 20 </fieldset>
21 </form> 21 </form>
22 -{* 22 +
  23 +
23 <form action="{$smarty.server.PHP_SELF}" method="POST"> 24 <form action="{$smarty.server.PHP_SELF}" method="POST">
24 <input type="hidden" name="action" value="saveInform" /> 25 <input type="hidden" name="action" value="saveInform" />
25 <input type="hidden" name="fWorkflowId" value="{$oWorkflow->getId()}" /> 26 <input type="hidden" name="fWorkflowId" value="{$oWorkflow->getId()}" />
@@ -33,7 +34,7 @@ informed when this state is reached.{/i18n}&lt;/p&gt; @@ -33,7 +34,7 @@ informed when this state is reached.{/i18n}&lt;/p&gt;
33 34
34 {if $aRoles} 35 {if $aRoles}
35 <h3>{i18n}Roles{/i18n}</h3> 36 <h3>{i18n}Roles{/i18n}</h3>
36 -{entity_checkboxes entities=$aRoles name="fRoleIds" multiple="true" selected=$aInformed.role assign=aBoxes} 37 +{entity_checkboxes entities=$aRoles name="fNotification[role]" multiple="true" selected=$aInformed.role assign=aBoxes}
37 {foreach from=$aBoxes item=sBox} 38 {foreach from=$aBoxes item=sBox}
38 {$sBox}<br /> 39 {$sBox}<br />
39 {/foreach} 40 {/foreach}
@@ -41,7 +42,7 @@ informed when this state is reached.{/i18n}&lt;/p&gt; @@ -41,7 +42,7 @@ informed when this state is reached.{/i18n}&lt;/p&gt;
41 42
42 {if $aGroups} 43 {if $aGroups}
43 <h3>{i18n}Groups{/i18n}</h3> 44 <h3>{i18n}Groups{/i18n}</h3>
44 -{entity_checkboxes entities=$aGroups name="fGroupIds" multiple="true" selected=$aInformed.group assign=aBoxes} 45 +{entity_checkboxes entities=$aGroups name="fNotification[group]" multiple="true" selected=$aInformed.group assign=aBoxes}
45 {foreach from=$aBoxes item=sBox} 46 {foreach from=$aBoxes item=sBox}
46 {$sBox}<br /> 47 {$sBox}<br />
47 {/foreach} 48 {/foreach}
@@ -49,10 +50,14 @@ informed when this state is reached.{/i18n}&lt;/p&gt; @@ -49,10 +50,14 @@ informed when this state is reached.{/i18n}&lt;/p&gt;
49 50
50 {if (empty($aGroups) && empty($aRoles))} 51 {if (empty($aGroups) && empty($aRoles))}
51 <div class="ktInfo"><p>{i18n}No groups or roles are defined in the DMS.{/i18n}</p></div> 52 <div class="ktInfo"><p>{i18n}No groups or roles are defined in the DMS.{/i18n}</p></div>
  53 +{else}
  54 +<div class="form_actions">
  55 + <input type="submit" value="{i18n}Update users to inform{/i18n}" />
  56 +</div>
52 {/if} 57 {/if}
53 58
54 </fieldset> 59 </fieldset>
55 -*} 60 +</form>
56 61
57 {* 62 {*
58 <h3>{i18n}Assigned Permissions{/i18n}</h3> 63 <h3>{i18n}Assigned Permissions{/i18n}</h3>
templates/ktcore/workflow/workflow_notification.smarty 0 → 100644
  1 +<dt class="actionitem">{$document_name}</dt>
  2 +<dd class="actionmessage">
  3 + {i18n arg_name=$document_name arg_state=$state_name}The document <strong>#name#</strong> has changed to
  4 + state <strong>#state#</strong>, and you are specified as one of the users to inform
  5 + about documents in this state.{/i18n}
  6 + <div class="actionoptions">
  7 + <a href="{$absoluteRootUrl}/notify.php?id={$notify_id}">{i18n}View Document{/i18n}</a>
  8 + | <a href="{$absoluteRootUrl}/notify.php?id={$notify_id}&notify_action=clear">{i18n}Clear Alert{/i18n}</a>
  9 + </div>
  10 +</dd>