Commit 2e73f774fe6dddafd09791e362a06b774e2dda77
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
Showing
6 changed files
with
272 additions
and
28 deletions
lib/dashboard/Notification.inc.php
| ... | ... | @@ -9,6 +9,10 @@ require_once(KT_LIB_DIR . '/users/User.inc'); |
| 9 | 9 | require_once(KT_LIB_DIR . '/documentmanagement/Document.inc'); |
| 10 | 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 | 17 | * class Notification |
| 14 | 18 | * |
| ... | ... | @@ -71,6 +75,9 @@ class KTNotification extends KTEntity { |
| 71 | 75 | function render() { |
| 72 | 76 | $notificationRegistry =& KTNotificationRegistry::getSingleton(); |
| 73 | 77 | $handler = $notificationRegistry->getHandler($this->sType); |
| 78 | + | |
| 79 | + if (is_null($handler)) { return null; } | |
| 80 | + | |
| 74 | 81 | return $handler->handleNotification($this); |
| 75 | 82 | } |
| 76 | 83 | |
| ... | ... | @@ -313,4 +320,59 @@ class KTSubscriptionNotification extends KTNotificationHandler { |
| 313 | 320 | |
| 314 | 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 | 121 | foreach ($aUsers as $iUserId) { |
| 122 | 122 | $oUser = User::get($iUserId); |
| 123 | 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 | 144 | foreach ($aGroups as $iGroupId) { |
| 145 | 145 | $oGroup = Group::get($iGroupId); |
| 146 | 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 | 10 | require_once(KT_LIB_DIR . '/search/searchutil.inc.php'); |
| 11 | 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 | 20 | class KTWorkflowUtil { |
| 14 | 21 | // {{{ saveTransitionsFrom |
| 15 | 22 | /** |
| ... | ... | @@ -102,7 +109,16 @@ class KTWorkflowUtil { |
| 102 | 109 | 'state_id' => $iStartStateId, |
| 103 | 110 | ); |
| 104 | 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 | 158 | 'state_id' => $iStartStateId, |
| 143 | 159 | ); |
| 144 | 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 | 169 | return DBUtil::autoInsert($sTable, $aValues, $aOptions); |
| 146 | 170 | } |
| 147 | 171 | // }}} |
| ... | ... | @@ -457,10 +481,103 @@ class KTWorkflowUtil { |
| 457 | 481 | $oDocumentTransaction = & new DocumentTransaction($oDocument, $sTransactionComments, 'ktcore.transactions.workflow_state_transition'); |
| 458 | 482 | $oDocumentTransaction->create(); |
| 459 | 483 | |
| 484 | + KTWorkflowUtil::informUsersForState($oTargetState, KTWorkflowUtil::getInformedForState($oTargetState), $oDocument, $oUser, $sComments); | |
| 485 | + | |
| 460 | 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 | 581 | // {{{ setInformedForState |
| 465 | 582 | /** |
| 466 | 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 | 20 | |
| 21 | 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 | 27 | class WorkflowNavigationPortlet extends KTPortlet { |
| 24 | 28 | var $oWorkflow; |
| 25 | 29 | |
| ... | ... | @@ -202,7 +206,58 @@ class KTWorkflowDispatcher extends KTAdminDispatcher { |
| 202 | 206 | } |
| 203 | 207 | |
| 204 | 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 | 263 | function transitionAvailable($oTransition, $oState) { |
| ... | ... | @@ -735,33 +790,28 @@ class KTWorkflowDispatcher extends KTAdminDispatcher { |
| 735 | 790 | $oState =& $this->oValidator->validateWorkflowState($_REQUEST['fStateId']); |
| 736 | 791 | $sTargetAction = 'editState'; |
| 737 | 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 | 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 | 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 | 815 | $this->successRedirectTo($sTargetAction, _('Changes saved'), $sTargetParams); |
| 766 | 816 | } |
| 767 | 817 | // }}} | ... | ... |
templates/ktcore/workflow/editState.smarty
| ... | ... | @@ -19,7 +19,8 @@ the "sent" <strong>transition</strong> has been performed by a user.</p> |
| 19 | 19 | </div> |
| 20 | 20 | </fieldset> |
| 21 | 21 | </form> |
| 22 | -{* | |
| 22 | + | |
| 23 | + | |
| 23 | 24 | <form action="{$smarty.server.PHP_SELF}" method="POST"> |
| 24 | 25 | <input type="hidden" name="action" value="saveInform" /> |
| 25 | 26 | <input type="hidden" name="fWorkflowId" value="{$oWorkflow->getId()}" /> |
| ... | ... | @@ -33,7 +34,7 @@ informed when this state is reached.{/i18n}</p> |
| 33 | 34 | |
| 34 | 35 | {if $aRoles} |
| 35 | 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 | 38 | {foreach from=$aBoxes item=sBox} |
| 38 | 39 | {$sBox}<br /> |
| 39 | 40 | {/foreach} |
| ... | ... | @@ -41,7 +42,7 @@ informed when this state is reached.{/i18n}</p> |
| 41 | 42 | |
| 42 | 43 | {if $aGroups} |
| 43 | 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 | 46 | {foreach from=$aBoxes item=sBox} |
| 46 | 47 | {$sBox}<br /> |
| 47 | 48 | {/foreach} |
| ... | ... | @@ -49,10 +50,14 @@ informed when this state is reached.{/i18n}</p> |
| 49 | 50 | |
| 50 | 51 | {if (empty($aGroups) && empty($aRoles))} |
| 51 | 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 | 57 | {/if} |
| 53 | 58 | |
| 54 | 59 | </fieldset> |
| 55 | -*} | |
| 60 | +</form> | |
| 56 | 61 | |
| 57 | 62 | {* |
| 58 | 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}¬ify_action=clear">{i18n}Clear Alert{/i18n}</a> | |
| 9 | + </div> | |
| 10 | +</dd> | ... | ... |