diff --git a/lib/dispatcher.inc.php b/lib/dispatcher.inc.php index 3312a39..d1ae37b 100644 --- a/lib/dispatcher.inc.php +++ b/lib/dispatcher.inc.php @@ -55,7 +55,7 @@ class KTDispatcher { $this->oRedirector =& new KTDispatchStandardRedirector($this); } - function redispatch($event_var, $action_prefix = null) { + function redispatch($event_var, $action_prefix = null, $orig_dispatcher = null) { $previous_event = KTUtil::arrayGet($_REQUEST, $this->event_var); if ($previous_event) { $this->persistParams(array($this->event_var)); @@ -64,7 +64,22 @@ class KTDispatcher { if ($action_prefix) { $this->action_prefix = $action_prefix; } - + + if (!is_null($orig_dispatcher)) { + $this->persistParams($orig_dispatcher->aPersistParams); + $core = array('aBreadcrumbs', + 'bTransactionStarted', + 'oUser', + 'session', + 'action_prefix', + 'bJSONMode'); + foreach($core as $k) { + if(isset($orig_dispatcher->$k)) { + $this->$k = $orig_dispatcher->$k; + } + } + } + return $this->dispatch(); } @@ -99,6 +114,10 @@ class KTDispatcher { $this->startTransaction(); } + if (method_exists($this, 'predispatch')) { + $this->predispatch(); + } + $ret = $this->$method(); $this->handleOutput($ret); @@ -108,17 +127,18 @@ class KTDispatcher { } function subDispatch(&$oOrigDispatcher) { - foreach(array('aBreadcrumbs', - 'bTransactionStarted', - 'oUser', - 'session', - 'event_var', - 'action_prefix', - 'bJSONMode') as $k) { - if(isset($oOrigDispatcher->$k)) { - $this->$k = $oOrigDispatcher->$k; - } - } + $core = array('aBreadcrumbs', + 'bTransactionStarted', + 'oUser', + 'session', + 'event_var', + 'action_prefix', + 'bJSONMode'); + foreach($core as $k) { + if(isset($oOrigDispatcher->$k)) { + $this->$k = $oOrigDispatcher->$k; + } + } return $this->dispatch(); } diff --git a/lib/util/ktutil.inc b/lib/util/ktutil.inc index 29bd9cd..30c7cf0 100644 --- a/lib/util/ktutil.inc +++ b/lib/util/ktutil.inc @@ -702,6 +702,16 @@ class KTUtil { return $sRet; } + function keyArray($aEntities, $sIdFunc = 'getId') { + $aRet = array(); + foreach ($aEntities as $oEnt) { + $meth = array(&$oEnt, $sIdFunc); + $id = call_user_func($meth); + $aRet[$id] = $oEnt; + } + return $aRet; + } + } /** diff --git a/lib/workflow/workflow.inc.php b/lib/workflow/workflow.inc.php index dbde454..9e42936 100644 --- a/lib/workflow/workflow.inc.php +++ b/lib/workflow/workflow.inc.php @@ -37,12 +37,14 @@ class KTWorkflow extends KTEntity { var $sName; var $sHumanName; var $iStartStateId; + var $bEnabled; var $_aFieldToSelect = array( "iId" => "id", "sName" => "name", "sHumanName" => "human_name", "iStartStateId" => "start_state_id", + 'bEnabled' => 'enabled', ); var $_bUsePearError = true; @@ -51,10 +53,12 @@ class KTWorkflow extends KTEntity { function getName() { return $this->sName; } function getHumanName() { return $this->sHumanName; } function getStartStateId() { return $this->iStartStateId; } + function getIsEnabled() { return ($this->bEnabled == true); } function setID($iId) { $this->iId = $iId; } function setName($sName) { $this->sName = $sName; } function setHumanName($sHumanName) { $this->sHumanName = $sHumanName; } function setStartStateId($iStartStateId) { $this->iStartStateId = $iStartStateId; } + function setIsEnabled($mValue) { $this->bEnabled = ($mValue == true); } function _table () { return KTUtil::getTableName('workflows'); @@ -79,10 +83,14 @@ class KTWorkflow extends KTEntity { function &getByName($sName) { return KTEntityUtil::getBy('KTWorkflow', 'name', $sName); } + + function getIsFunctional() { + return (($this->getStartStateId() != false) && ($this->getIsEnabled())); + } // STATIC function &getFunctional() { - return KTEntityUtil::getList2('KTWorkflow', 'start_state_id IS NOT NULL'); + return KTEntityUtil::getList2('KTWorkflow', 'start_state_id IS NOT NULL AND enabled = 1'); } function &getByDocument($oDocument) { diff --git a/lib/workflow/workflowadminutil.inc.php b/lib/workflow/workflowadminutil.inc.php new file mode 100644 index 0000000..351eb52 --- /dev/null +++ b/lib/workflow/workflowadminutil.inc.php @@ -0,0 +1,118 @@ +getId()), + ); + $res = DBUtil::runQuery($aQuery); + if (PEAR::isError($res)) { + return $res; + } + $aOptions = array('noid' => true); + if (empty($aTransitionIds)) { + return; // don't fail if there are no transitions. + } + foreach ($aTransitionIds as $iTransitionId) { + $res = DBUtil::autoInsert($sTable, array( + 'state_id' => $oState->getId(), + 'transition_id' => $iTransitionId, + ), $aOptions); + if (PEAR::isError($res)) { + return $res; + } + } + return; + } + + function saveTransitionSources($oTransition, $aStateIds) { + $sTable = KTUtil::getTableName('workflow_state_transitions'); + $aQuery = array( + "DELETE FROM $sTable WHERE transition_id = ?", + array($oTransition->getId()), + ); + $res = DBUtil::runQuery($aQuery); + if (PEAR::isError($res)) { + return $res; + } + $aOptions = array('noid' => true); + if (empty($aStateIds)) { + return; // don't fail if there are no transitions. + } + foreach ($aStateIds as $iStateId) { + $res = DBUtil::autoInsert($sTable, array( + 'state_id' => $iStateId, + 'transition_id' => $oTransition->getId(), + ), $aOptions); + if (PEAR::isError($res)) { + return $res; + } + } + return; + } + +// {{{ getTransitionsFrom + /** + * Gets which workflow transitions are available to be chosen from + * this workflow state. + * + * Workflow transitions have only destination workflow states, and + * it is up to the workflow state to decide which workflow + * transitions it wants to allow to leave its state. + * + * This function optionally will return the database id numbers of + * the workflow transitions using the 'ids' option. + */ + function getTransitionsFrom($oState, $aOptions = null) { + $bIds = KTUtil::arrayGet($aOptions, 'ids'); + $sTable = KTUtil::getTableName('workflow_state_transitions'); + $aQuery = array( + "SELECT transition_id FROM $sTable WHERE state_id = ?", + array($oState->getId()), + ); + $aTransitionIds = DBUtil::getResultArrayKey($aQuery, 'transition_id'); + if (PEAR::isError($aTransitionIds)) { + return $aTransitionIds; + } + if ($bIds) { + return $aTransitionIds; + } + $aRet = array(); + foreach ($aTransitionIds as $iId) { + $aRet[] =& KTWorkflowTransition::get($iId); + } + return $aRet; + } + // }}} + + function getSourceStates($oTransition, $aOptions = null) { + $bIds = KTUtil::arrayGet($aOptions, 'ids'); + $sTable = KTUtil::getTableName('workflow_state_transitions'); + $aQuery = array( + "SELECT state_id FROM $sTable WHERE transition_id = ?", + array($oTransition->getId()), + ); + $aStateIds = DBUtil::getResultArrayKey($aQuery, 'state_id'); + if (PEAR::isError($aStateIds)) { + return $aStateIds; + } + if ($bIds) { + return $aStateIds; + } + $aRet = array(); + foreach ($aStateIds as $iId) { + $aRet[] =& KTWorkflowState::get($iId); + } + return $aRet; + } + +} diff --git a/lib/workflow/workflowstate.inc.php b/lib/workflow/workflowstate.inc.php index 3d03c52..9e1f6c0 100644 --- a/lib/workflow/workflowstate.inc.php +++ b/lib/workflow/workflowstate.inc.php @@ -106,6 +106,23 @@ class KTWorkflowState extends KTEntity { return KTWorkflowState::get($iStateId); } + // STATIC + function nameExists($sName, $oWorkflow) { + $iWorkflowId = KTUtil::getId($oWorkflow); + $res = KTEntityUtil::getByDict( + 'KTWorkflowState', array( + 'name' => $sName, + 'workflow_id' => $iWorkflowId + ) + ); + // expect KTEntityNoObjects + if (PEAR::isError($res)) { + return false; + } + + return true; + } + } ?> diff --git a/lib/workflow/workflowtransition.inc.php b/lib/workflow/workflowtransition.inc.php index 9ae0cc8..9d3321c 100644 --- a/lib/workflow/workflowtransition.inc.php +++ b/lib/workflow/workflowtransition.inc.php @@ -130,6 +130,24 @@ class KTWorkflowTransition extends KTEntity { $oWorkflowState =& KTWorkflowState::get($this->getTargetStateId()); return sprintf("%s (to state %s)", $this->getName(), $oWorkflowState->getName()); } + + // STATIC + function nameExists($sName, $oWorkflow) { + $iWorkflowId = KTUtil::getId($oWorkflow); + $res = KTEntityUtil::getByDict( + 'KTWorkflowTransition', array( + 'name' => $sName, + 'workflow_id' => $iWorkflowId + ) + ); + // expect KTEntityNoObjects + if (PEAR::isError($res)) { + return false; + } + + return true; + } + } ?> diff --git a/plugins/ktcore/KTCorePlugin.php b/plugins/ktcore/KTCorePlugin.php index 6f92394..290439a 100644 --- a/plugins/ktcore/KTCorePlugin.php +++ b/plugins/ktcore/KTCorePlugin.php @@ -218,9 +218,9 @@ class KTCorePlugin extends KTPlugin { $this->registerAdminPage("workflows", 'KTWorkflowDispatcher', 'documents', _kt('Workflows'), _kt('Configure the process documents go through.'), 'admin/workflows.php', null); - //$this->registerAdminPage("workflows_2", 'KTWorkflowDispatcher', 'documents', - // _kt('Workflows v2'), _kt('Configure the process documents go through.'), - // 'admin/workflowsNew.php', null); + $this->registerAdminPage("workflows_2", 'KTWorkflowAdminV2', 'documents', + _kt('Workflows v2'), _kt('Configure the process documents go through.'), + 'admin/workflowsv2.php', null); // storage $this->registerAdminPage("checkout", 'KTCheckoutAdminDispatcher', 'storage', diff --git a/plugins/ktcore/admin/workflow/newworkflow.inc.php b/plugins/ktcore/admin/workflow/newworkflow.inc.php new file mode 100644 index 0000000..0c00157 --- /dev/null +++ b/plugins/ktcore/admin/workflow/newworkflow.inc.php @@ -0,0 +1,339 @@ +persistParams(array('fWizardKey')); + } + + function &form_step1() { + $oForm = new KTForm; + + $oForm->setOptions(array( + 'action' => 'process_step1', + 'cancel_url' => KTUtil::addQueryStringSelf(''), // NBM: is there a cleaner way to reference the parent? + 'fail_action' => 'main', + 'label' => _kt('Workflow Details'), + 'submit_label' => _kt('Next'), + 'description' => _kt('This first step requires that you provide basic details about the workflow: its name, etc.'), + 'context' => $this, + )); + $oForm->setWidgets(array( + array('ktcore.widgets.string',array( + 'label' => _kt('Workflow Name'), + 'description' => _kt('Each workflow must have a unique name.'), + 'required' => true, + 'name' => 'workflow_name', + )), + array('ktcore.widgets.text',array( + 'label' => _kt('States'), + 'description' => _kt('As documents progress through their lifecycle, they pass through a number of states. These states describe a step in the process the document must follow. Examples of states include "reviewed","submitted" or "pending". Note that the first state you list is the one in which documents will start the workflow - this can be changed later on. Please enter a list of states, one per line. State names must be unique.'), + 'required' => true, + 'name' => 'states', + 'rows' => 15, + )), + array('ktcore.widgets.text',array( + 'label' => _kt('Transitions'), + 'description' => _kt('In order to move between states, users will cause "transitions" to occur. These transitions represent processes followed, e.g. "review document", "distribute invoice" or "publish". Please enter a list of transitions, one per line. Transition names must be unique. You\'ll assign transitions to states in the next step.'), + 'required' => false, + 'name' => 'transitions', + )), + )); + + $oForm->setValidators(array( + array('ktcore.validators.string', array( + 'test' => 'workflow_name', + 'output' => 'workflow_name', + )), + array('ktcore.validators.string', array( + 'test' => 'states', + 'output' => 'states', + 'max_length' => 9999, + )), + array('ktcore.validators.string', array( + 'test' => 'transitions', + 'output' => 'transitions', + 'max_length' => 9999, + )), + )); + + return $oForm; + } + + function do_main() { + $oTemplate =& $this->oValidator->validateTemplate('ktcore/workflow/admin/new_wizard_step1'); + + $oForm =& $this->form_step1(); + + $oTemplate->setData(array( + 'context' => $this, + 'form' => $oForm, + )); + return $oTemplate->render(); + } + + function do_process_step1() { + $oForm =& $this->form_step1(); + $res = $oForm->validate(); + $data = $res['results']; + // perform additional validation. + $extra_errors = array(); + + $oWorkflow = KTWorkflow::getByName($data['workflow_name']); + if (!PEAR::isError($oWorkflow)) { + $extra_errors['workflow_name'][] = _kt("A workflow with that name already exists. Please choose a different name for this workflow."); + } + + $initial_states = (array) explode("\n", $data['states']); // must be there, we validated it. + $failed = array(); + $states = array(); + $is_first = true; + $initial_state = ''; + foreach ($initial_states as $sInitialStateName) { + $state_name = trim($sInitialStateName); + if (empty($state_name)) { + continue; + } + + if ($states[$state_name]) { + $failed[] = $state_name; + continue; + } + if ($is_first) { + $is_first = false; + $initial_state = $state_name; + } + $states[$state_name] = $state_name; + } + if (empty($states)) { + $extra_errors['states'][] = _kt('You must provide at least one state name.'); + } + if (!empty($failed)) { + $extra_errors['states'] = sprintf(_kt("You cannot have duplicate state names: %s"), implode(', ', $failed)); + } + $data['states'] = $states; + $data['initial_state'] = $initial_state; + + $initial_transitions = (array) explode("\n", $data['transitions']); // must be there, we validated it. + $failed = array(); + $transitions = array(); + foreach ($initial_transitions as $sInitialTransitionName) { + $transition_name = trim($sInitialTransitionName); + if (empty($transition_name)) { + continue; + } + + if ($transitions[$transition_name]) { + $failed[] = $transition_name; + continue; + } + + $transitions[$transition_name] = $transition_name; + } + + if (!empty($failed)) { + $extra_errors['transitions'] = sprintf(_kt("You cannot have duplicate transition names: %s"), implode(', ', $failed)); + } + $data['transitions'] = $transitions; + + // handle errors. + if (!empty($res['errors']) || !empty($extra_errors)) { + $oForm->handleError(null, $extra_errors); + } + + // store the data for a while. + $fWizardKey = KTUtil::randomString(); + $wiz_data = (array) $_SESSION['_wiz_data']; + $wiz_data[$fWizardKey] = $data; + $_SESSION['_wiz_data'] =& $wiz_data; + + if (empty($data['transitions'])) { + return $this->finalise(); // finish and go. + } + + $this->successRedirectTo("step2",_kt("Initial data stored."), sprintf('fWizardKey=%s', $fWizardKey)); + } + + function do_step2() { + $fWizardKey = KTUtil::arrayGet($_REQUEST, 'fWizardKey'); + $wiz_data = (array) $_SESSION['_wiz_data'][$fWizardKey]; + + if (empty($wiz_data)) { + $this->errorRedirectToMain(_kt("Unable to find previous value. Please try again.")); + } + + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/new_wizard_step2'); + + $transitions = (array) $wiz_data['transitions']; + $args = $this->meldPersistQuery("", "process_step2", true); + + $oTemplate->setData(array( + 'context' => $this, + 'args' => $args, + 'transitions' => $wiz_data['transitions'], + 'states' => $wiz_data['states'], + )); + return $oTemplate->render(); + } + + function do_process_step2() { + $fWizardKey = KTUtil::arrayGet($_REQUEST, 'fWizardKey'); + $wiz_data = $_SESSION['_wiz_data'][$fWizardKey]; + if (empty($wiz_data)) { + $this->errorRedirectToMain(_kt("Unable to locate stored data. Please try again.")); + } + + // we can't use the form "stuff" here since we don't have a grid widget yet + // and hopefully never will. + + $fToData = (array) KTUtil::arrayGet($_REQUEST, 'fTo'); + $fFromData = (array) KTUtil::arrayGet($_REQUEST, 'fFrom'); + + // these are data[transition][state] = true + + $fTo = array(); + $initial_state = $wiz_data['initial_state']; + foreach ($wiz_data['transitions'] as $transition) { + $candidate = $fToData[$transition]; + if (empty($wiz_data['states'][$candidate])) { + $candidate = $initial_state; + } + $fTo[$transition] = $candidate; + } + + $fFrom = array(); + foreach ($wiz_data['transitions'] as $transition) { + $d = (array) KTUtil::arrayGet($fFromData, $transition); + $final = array(); + foreach ($d as $state => $discard) { + if (!empty($wiz_data['states'][$state])) { + $final[] = $state; + } + } + $fFrom[$transition] = $final; + } + + $wiz_data['from'] = $fFrom; + $wiz_data['to'] = $fTo; + + $_SESSION['_wiz_data'][$fWizardKey] = $wiz_data; + + return $this->finalise(); + } + + function finalise() { + $fWizardKey = KTUtil::arrayGet($_REQUEST, 'fWizardKey'); + $wiz_data = $_SESSION['_wiz_data'][$fWizardKey]; + + // gather all our data. we're sure this is all good and healthy. + + $states = $wiz_data['states']; + $transitions = $wiz_data['transitions']; + $from = $wiz_data['from']; + $to = $wiz_data['to']; + $initial_state = $wiz_data['initial_state']; + $workflow_name = $wiz_data['workflow_name']; + + $this->startTransaction(); + // create the initial workflow + $oWorkflow = KTWorkflow::createFromArray(array( + 'name' => $workflow_name, + 'humanname' => $workflow_name, + 'enabled' => true, + )); + if (PEAR::isError($oWorkflow)) { + $this->errorRedirectToMain(sprintf(_kt("Failed to create workflow: %s"), $oWorkflow->getMessage())); + } + $iWorkflowId = $oWorkflow->getId(); + // create the states. + $aStates = array(); + foreach ($states as $state_name) { + $oState = KTWorkflowState::createFromArray(array( + 'workflowid' => $iWorkflowId, + 'name' => $state_name, + 'humanname' => $state_name, + )); + if (PEAR::isError($oState)) { + $this->errorRedirectToMain(sprintf(_kt("Failed to create state: %s"), $oState->getMessage())); + } + $aStates[$state_name] = $oState; + } + + // update the initial state on workflow + $oInitialState = $aStates[$initial_state]; + $oWorkflow->setStartStateId($oInitialState->getId()); + $res = $oWorkflow->update(); + if (PEAR::isError($res)) { + $this->errorRedirectToMain(sprintf(_kt("Failed to update workflow: %s"), $res->getMessage())); + } + + // next, we create and hook up the transitions. + $aTransitions = array(); + foreach ($transitions as $transition) { + $dest_name = $to[$transition]; + $oDestState = $aStates[$dest_name]; + $oTransition = KTWorkflowTransition::createFromArray(array( + "WorkflowId" => $iWorkflowId, + "Name" => $transition, + "HumanName" => $transition, + "TargetStateId" => $oDestState->getId(), + // FIXME we need to deprecate these, eventually + "GuardPermissionId" => null, + "GuardGroupId" => null, + "GuardRoleId" => null, + "GuardConditionId" => null, + )); + if (PEAR::isError($oTransition)) { + $this->errorRedirectToMain(sprintf(_kt("Failed to create transition: %s"), $oTransition->getMessage())); + } + + // hook up source states. + $state_ids = array(); + $sources = (array) $from[$transition]; + foreach ($sources as $state_name) { + + // must exist. + $oState = $aStates[$state_name]; + $state_ids[] = $oState->getId(); + } + + $res = KTWorkflowAdminUtil::saveTransitionSources($oTransition, $state_ids); + if (PEAR::isError($res)) { + $this->errorRedirectToMain(sprintf(_kt("Failed to set transition origins: %s"), $res->getMessage())); + } + } + + $this->commitTransaction(); + + // finally, we want to redirect the user to the parent dispatcher somehow. + // FIXME nbm: how do you recommend we do this? + + $base = $_SERVER['PHP_SELF']; + $qs = sprintf("action=view&fWorkflowId=%d",$oWorkflow->getId()); + $url = KTUtil::addQueryString($base, $qs); + $this->addInfoMessage(_kt("Your new workflow has been created. You may want to configure security and notifications from the menu on the left.")); + redirect($url); + } +} + +?> diff --git a/plugins/ktcore/admin/workflowsv2.php b/plugins/ktcore/admin/workflowsv2.php new file mode 100644 index 0000000..cadde84 --- /dev/null +++ b/plugins/ktcore/admin/workflowsv2.php @@ -0,0 +1,1077 @@ +oWorkflow = $oWorkflow; + parent::KTPortlet($sTitle); + } + + function render() { + if (is_null($this->oWorkflow)) { return _kt('No Workflow Selected.'); } + + $aAdminPages = array(); + $aAdminPages[] = array('name' => _kt('Overview'), 'query' => 'action=view&fWorkflowId=' . $this->oWorkflow->getId()); + $aAdminPages[] = array('name' => _kt('States and Transitions'), 'query' => 'action=basic&fWorkflowId=' . $this->oWorkflow->getId()); + $aAdminPages[] = array('name' => _kt('Security'), 'query' => 'action=security&fWorkflowId=' . $this->oWorkflow->getId()); + $aAdminPages[] = array('name' => _kt('Workflow Effects'), 'query' => 'action=effects&fWorkflowId=' . $this->oWorkflow->getId()); + $aAdminPages[] = array('name' => _kt('Select different workflow'), 'query' => 'action=main&fWorkflowId=' . $this->oWorkflow->getId()); + + $oTemplating =& KTTemplating::getSingleton(); + $oTemplate = $oTemplating->loadTemplate("ktcore/workflow/admin_portlet"); + $aTemplateData = array( + "context" => $this, + "aAdminPages" => $aAdminPages, + ); + + return $oTemplate->render($aTemplateData); + } +} + +class KTWorkflowAdminV2 extends KTAdminDispatcher { + var $oWorkflow; + var $oState; + var $oTransition; + + function check() { + $res = parent::check(); + $this->persistParams(array('fWorkflowId', 'fStateId', 'fTransitionId')); + + $iWorkflowId = KTUtil::arrayGet($_REQUEST, 'fWorkflowId'); + $iStateId = KTUtil::arrayGet($_REQUEST, 'fStateId'); + $iTransitionId = KTUtil::arrayGet($_REQUEST, 'fTransitionId'); + + if (!is_null($iWorkflowId)) { + $oWorkflow =& KTWorkflow::get($iWorkflowId); + if (!PEAR::isError($oWorkflow)) { + $this->oWorkflow =& $oWorkflow; + } + } + + if (!is_null($iStateId)) { + $oState =& KTWorkflowState::get($iStateId); + if (!PEAR::isError($oState)) { + $this->oState =& $oState; + } + } + + if (!is_null($iTransitionId)) { + $oTransition =& KTWorkflowTransition::get($iTransitionId); + if (!PEAR::isError($oTransition)) { + $this->oTransition =& $oTransition; + } + } + + $this->aBreadcrumbs[] = array( + 'url' => $_SERVER['PHP_SELF'], + 'name' => _kt('Workflows'), + ); + + if (!is_null($this->oWorkflow)) { + $this->oPage->addPortlet(new WorkflowNavigationPortlet(_kt("Workflow Administration"), $this->oWorkflow)); + + $this->aBreadcrumbs[] = array( + 'url' => KTUtil::addQueryStringSelf(sprintf('action=view&fWorkflowId=%d', $iWorkflowId)), + 'name' => $this->oWorkflow->getName(), + ); + } + + return $res; + } + + function do_main() { + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/list'); + + $aWorkflows = KTWorkflow::getList(); + + $oTemplate->setData(array( + 'context' => $this, + 'workflows' => $aWorkflows, + )); + return $oTemplate->render(); + } + + function do_newWorkflow() { + // subdispatch this to the NewWorkflowWizard. + require_once(dirname(__FILE__) . '/workflow/newworkflow.inc.php'); + + $oSubDispatcher =& new KTNewWorkflowWizard; + $oSubDispatcher->redispatch('wizard', null, $this); + exit(0); + } + + // -------------------- Overview ----------------- + // basic view page. + + function do_view() { + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/view'); + + $this->oPage->setBreadcrumbDetails(_kt("Overview")); + + if (!$this->oWorkflow->getIsEnabled()) { + $this->addInfoMessage(sprintf(_kt("This workflow is currently marked as disabled. No new documents can be assigned to this workflow until it is enabled. To change this, please edit the workflow's base properties."), KTUtil::addQueryString($_SERVER['PHP_SELF'], $this->meldPersistQuery("","editcore")))); + } + + if ($this->oWorkflow->getStartStateId() == false) { + $this->addErrorMessage(sprintf(_kt("No start state is specified for this workflow. No new documents can be assigned to this workflow until one is assigned. To change this, please edit the workflow's base properties."), KTUtil::addQueryString($_SERVER['PHP_SELF'], $this->meldPersistQuery("","editcore")))); + } + + // for the basic view + $start_state_id = $this->oWorkflow->getStartStateId(); + $oState = KTWorkflowState::get($start_state_id); + + if (PEAR::isError($oState)) { + $state_name = _kt('No starting state.'); + } else { + $state_name = $oState->getName(); + } + + + + $oTemplate->setData(array( + 'context' => $this, + 'workflow_name' => $this->oWorkflow->getName(), + 'state_name' => $state_name, + 'workflow' => $this->oWorkflow, + )); + return $oTemplate->render(); + } + + function form_coreedit() { + $oForm = new KTForm; + + $oForm->setOptions(array( + 'context' => $this, + 'action' => 'setcore', + 'fail_action' => 'editcore', + 'cancel_action' => 'view', + 'label' => _kt('Edit Workflow Details'), + 'submit_label' => _kt('Update Workflow Details'), + )); + + $oForm->setWidgets(array( + array('ktcore.widgets.string',array( + 'label' => _kt("Workflow Name"), + 'description' => _kt("Each workflow must have a unique name."), + 'name' => 'workflow_name', + 'required' => true, + 'value' => $this->oWorkflow->getName(), + )), + array('ktcore.widgets.entityselection', array( + 'label' => _kt("Starting State"), + 'description' => _kt('When a document has this workflow applied to it, which state should it initially have.'), + 'name' => 'start_state', + 'label_method' => 'getHumanName', + 'vocab' => KTWorkflowState::getByWorkflow($this->oWorkflow), + 'value' => $this->oWorkflow->getStartStateId(), + 'required' => true, + )), + array('ktcore.widgets.boolean', array( + 'label' => _kt('Enabled'), + 'description' => _kt('If a workflow is disabled, no new documents may be placed in it. Documents which were previously in the workflow continue to be able to change state, however.'), + 'name' => 'enabled', + 'value' => $this->oWorkflow->getIsEnabled(), + )), + )); + + $oForm->setValidators(array( + array('ktcore.validators.string', array( + 'test' => 'workflow_name', + 'output' => 'workflow_name', + )), + array('ktcore.validators.entity', array( + 'test' => 'start_state', + 'class' => 'KTWorkflowState', + 'output' => 'start_state', + )), + array('ktcore.validators.boolean', array( + 'test' => 'enabled', + 'output' => 'enabled', + )) + )); + + return $oForm; + } + + function do_editcore() { + + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/edit_core'); + $this->oPage->setBreadcrumbDetails(_kt("Edit Details")); + + $oForm = $this->form_coreedit(); + + $oTemplate->setData(array( + 'context' => $this, + 'workflow_name' => $this->oWorkflow->getName(), + 'edit_form' => $oForm, + )); + return $oTemplate->render(); + } + + function do_setcore() { + $oForm = $this->form_coreedit(); + $res = $oForm->validate(); + $data = $res['results']; + $errors = $res['errors']; + if (!empty($errors)) { + $oForm->handleError(); + } + + $this->startTransaction(); + $this->oWorkflow->setName($data['workflow_name']); + $this->oWorkflow->setHumanName($data['workflow_name']); + $this->oWorkflow->setStartStateId($data['start_state']->getId()); + $this->oWorkflow->setIsEnabled($data['enabled']); + $res = $this->oWorkflow->update(); + if (PEAR::isError($res)) { + $oForm->handleError(sprintf(_kt("Failed to update workflow: %s"), $res->getMessage())); + } + + $this->successRedirectTo("view",_kt("Workflow updated.")); + } + + // ----------------- Basic - States & Transition --------------------- + function do_basic() { + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/basic_overview'); + $this->aBreadcrumbs[] = array( + 'url' => KTUtil::addQueryStringSelf($this->meldPersistQuery("", "basic")), + 'name' => _kt("States and Transitions"), + ); + $this->oPage->setBreadcrumbDetails(_kt("Overview")); + + $aStates = KTWorkflowState::getByWorkflow($this->oWorkflow); + $aTransitions = KTWorkflowTransition::getByWorkflow($this->oWorkflow); + + $oTemplate->setData(array( + 'context' => $this, + 'workflow_name' => $this->oWorkflow->getName(), + 'states' => $aStates, + 'transitions' => $aTransitions, + )); + return $oTemplate->render(); + } + + function form_transitionconnections() { + $oForm = new KTForm; + $oForm->setOptions(array( + 'label' => _kt('Configure Workflow Process'), + 'description' => _kt('The process a document follows is controlled by the way that the transitions between states are setup. A document starts the workflow in the initial state, and then follows transitions between states. Which users can perform these transitions can be configured in the "Security" section.'), + 'submit_label' => _kt('Update Process'), + 'cancel_action' => 'basic', + 'action' => 'setconnections', + 'fail_action' => 'transitionconnections', // consistency - this is not really used. + 'context' => $this, + )); + + return $oForm; + } + + function do_transitionconnections() { + // we don't use a traditional form here, since the grid is too complex + // and its essentially one-shot. + // + // + $oForm = $this->form_transitionconnections(); + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/configure_process'); + + // we want to re-use this for *subsets*. + $transition_ids = KTUtil::arrayGet($_REQUEST, 'transition_ids'); + $bRestrict = is_array($transition_ids); + + $transitions = KTWorkflowTransition::getByWorkflow($this->oWorkflow); + $availability = array(); + foreach ($transitions as $oTransition) { + if ($bRestrict) { + if ($transition_ids[$oTransition->getId()]) { + $final_transitions[] = $oTransition; + } else { + continue; + } + } + + $sources = KTWorkflowAdminUtil::getSourceStates($oTransition, array('ids' => true)); + $aSources = array(); + foreach ($sources as $source) { $aSources[$source] = $source; } + $availability[$oTransition->getId()] = $aSources; + } + + + if ($bRestrict) { + $transitions = $final_transitions; + } + + $oTemplate->setData(array( + 'context' => $this, + 'form' => $oForm, + 'states' => KTWorkflowState::getByWorkflow($this->oWorkflow), + 'transitions' => $transitions, + 'availability' => $availability, + )); + + return $oTemplate->render(); + } + + function do_setconnections() { + // we *must* ensure that transitions are not set to originate from their + // destination. + // + // we can ignore it here, because its dealt with in workflowadminutil + + $to = (array) KTUtil::arrayGet($_REQUEST, 'fTo'); + $from = (array) KTUtil::arrayGet($_REQUEST, 'fFrom'); + + // we do not trust any of this data. + $states = KTWorkflowState::getByWorkflow($this->oWorkflow); + $states = KTUtil::keyArray($states); + $transitions = KTWorkflowTransition::getByWorkflow($this->oWorkflow); + + $this->startTransaction(); + + foreach ($transitions as $oTransition) { + $dest_id = $to[$oTransition->getId()]; + $oDestState = $states[$dest_id]; + + if (!is_null($oDestState)) { + $oTransition->setTargetStateId($dest_id); + $res = $oTransition->update(); + if (PEAR::isError($res)) { + $this->errorRedirectTo('basic', sprintf(_kt("Unexpected error updating transition: %s"), $res->getMessage())); + } + } + + // hook up source states. + $source_state_ids = array(); + $sources = (array) $from[$oTransition->getId()]; + + foreach ($sources as $state_id => $discard) { + // test existence + $oState = $states[$state_id]; + if (!is_null($oState) && ($dest_id != $state_id)) { + $source_state_ids[] = $oState->getId(); + } + } + + $res = KTWorkflowAdminUtil::saveTransitionSources($oTransition, $source_state_ids); + if (PEAR::isError($res)) { + $this->errorRedirectTo('basic', sprintf(_kt("Failed to set transition origins: %s"), $res->getMessage())); + } + } + + $this->successRedirectTo('basic', _kt("Workflow process updated.")); + } + + function form_addstates() { + $oForm = new KTForm; + $oForm->setOptions(array( + 'context' => $this, + 'label' => _kt("Add New States"), + 'submit_label' => _kt("Add States"), + 'action' => 'createstates', + 'cancel_action' => 'basic', + 'fail_action' => 'addstates', + )); + + $oForm->setWidgets(array( + array('ktcore.widgets.text',array( + 'label' => _kt('New States'), + 'description' => _kt('As documents progress through their lifecycle, they pass through a number of states. These states describe a step in the process the document must follow. Examples of states include "reviewed","submitted" or "pending". Note that the first state you list is the one in which documents will start the workflow - this can be changed later on. Please enter a list of states, one per line. State names must be unique, and this includes states already in this workflow.'), + 'required' => true, + 'name' => 'states', + 'rows' => 15, + )), + )); + $oForm->setValidators(array( + array('ktcore.validators.string', array( + 'test' => 'states', + 'output' => 'states', + 'max_length' => 9999, + )), + )); + + return $oForm; + } + + function do_addstates() { + $oForm = $this->form_addstates(); + + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/add_states'); + $oTemplate->setData(array( + 'context' => $this, + 'form' => $oForm + )); + return $oTemplate->render(); + } + + function do_createstates() { + $oForm = $this->form_addstates(); + $res = $oForm->validate(); + $data = $res['results']; + $errors = $res['errors']; + $extra_errors = array(); + + // we want to check for duplicates, empties, etc. + + $initial_states = (array) explode("\n", $data['states']); + $failed = array(); + $old_states = array(); + $states = array(); + foreach ($initial_states as $sName) { + $state_name = trim($sName); + if (empty($state_name)) { + continue; + } + + if ($states[$state_name]) { + $failed[] = $state_name; + continue; + } + + // check for pre-existing states. + $exists = KTWorkflowState::nameExists($sName, $this->oWorkflow); + if ($exists) { + $old_states[] = $sName; + } + + $states[$state_name] = $state_name; + } + if (empty($states)) { + $extra_errors['states'][] = _kt('You must provide at least one state name.'); + } + if (!empty($failed)) { + $extra_errors['states'][] = sprintf(_kt("You cannot have duplicate state names: %s"), implode(', ', $failed)); + } + if (!empty($old_states)) { + $extra_errors['states'][] = sprintf(_kt("You cannot use state names that are in use: %s"), implode(', ', $old_states)); + } + + // handle any errors. + if (!empty($errors) || !empty($extra_errors)) { + $oForm->handleError(null, $extra_errors); + } + + $this->startTransaction(); + // now act + foreach ($states as $state_name) { + $oState = KTWorkflowState::createFromArray(array( + 'workflowid' => $this->oWorkflow->getId(), + 'name' => $state_name, + 'humanname' => $state_name, + )); + if (PEAR::isError($oState)) { + $oForm->handleError(sprintf(_kt("Unexpected failure creating state: %s"), $oState->getMessage())); + } + } + + $this->successRedirectTo('basic', _kt("New States Created.")); + } + + + function form_addtransitions() { + $oForm = new KTForm; + $oForm->setOptions(array( + 'context' => $this, + 'label' => _kt("Add New Transitions"), + 'submit_label' => _kt("Add Transitions"), + 'action' => 'createtransitions', + 'cancel_action' => 'basic', + 'fail_action' => 'addtransitions', + )); + + $oForm->setWidgets(array( + array('ktcore.widgets.text',array( + 'label' => _kt('Transitions'), + 'description' => _kt('In order to move between states, users will cause "transitions" to occur. These transitions represent processes followed, e.g. "review document", "distribute invoice" or "publish". Please enter a list of transitions, one per line. Transition names must be unique. You\'ll assign transitions to states in the next step.'), + 'required' => false, + 'name' => 'transitions', + )), + )); + $oForm->setValidators(array( + array('ktcore.validators.string', array( + 'test' => 'transitions', + 'output' => 'transitions', + 'max_length' => 9999, + )), + )); + + return $oForm; + } + + function do_addtransitions() { + $oForm = $this->form_addtransitions(); + + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/add_transitions'); + $oTemplate->setData(array( + 'context' => $this, + 'form' => $oForm + )); + return $oTemplate->render(); + } + + function do_createtransitions() { + $oForm = $this->form_addtransitions(); + $res = $oForm->validate(); + $data = $res['results']; + $errors = $res['errors']; + $extra_errors = array(); + + // we want to check for duplicates, empties, etc. + + $initial_transitions = (array) explode("\n", $data['transitions']); + $failed = array(); + $old_transitions = array(); + $transitions = array(); + foreach ($initial_transitions as $sName) { + $transition_name = trim($sName); + if (empty($transition_name)) { + continue; + } + + if ($transitions[$transition_name]) { + $failed[] = $transition_name; + continue; + } + + // check for pre-existing states. + $exists = KTWorkflowTransition::nameExists($sName, $this->oWorkflow); + if ($exists) { + $old_transitions[] = $sName; + } + + $transitions[$transition_name] = $transition_name; + } + if (empty($transitions)) { + $extra_errors['transitions'][] = _kt('You must provide at least one transition name.'); + } + if (!empty($failed)) { + $extra_errors['transitions'][] = sprintf(_kt("You cannot have duplicate transition names: %s"), implode(', ', $failed)); + } + if (!empty($old_states)) { + $extra_errors['transitions'][] = sprintf(_kt("You cannot use transition names that are in use: %s"), implode(', ', $old_transitions)); + } + + // handle any errors. + if (!empty($errors) || !empty($extra_errors)) { + $oForm->handleError(null, $extra_errors); + } + + $this->startTransaction(); + $transition_ids = array(); + $oState = KTWorkflowState::get($this->oWorkflow->getStartStateId()); + foreach ($transitions as $transition_name) { + $oTransition = KTWorkflowTransition::createFromArray(array( + "WorkflowId" => $this->oWorkflow->getId(), + "Name" => $transition_name, + "HumanName" => $transition_name, + "TargetStateId" => $oState->getId(), + "GuardPermissionId" => null, + "GuardGroupId" => null, + "GuardRoleId" => null, + "GuardConditionId" => null, + )); + if (PEAR::isError($oTransition)) { + $oForm->handleError(sprintf(_kt("Unexpected failure creating transition: %s"), $oTransition->getMessage())); + } + $transition_ids[] = $oTransition->getId(); + } + + $transition_ids_query = array(); + foreach ($transition_ids as $id) { + $transition_ids_query[] = sprintf('transition_ids[%s]=%s',$id, $id); + } + $transition_ids_query = implode('&', $transition_ids_query); + + $this->successRedirectTo('transitionconnections', _kt("New States Created."), $transition_ids_query); + } + + function form_editstate($oState) { + $oForm = new KTForm; + + $oForm->setOptions(array( + 'context' => $this, + 'label' => _kt('Edit State'), + 'submit_label' => _kt('Update State'), + 'action' => 'savestate', + 'fail_action' => 'editstate', + 'cancel_action' => 'basic', + )); + + $oForm->setWidgets(array( + array('ktcore.widgets.string', array( + 'name' => 'name', + 'label' => _kt('State Name'), + 'description' => _kt('As documents progress through their lifecycle, they pass through a number of states. These states describe a step in the process the document must follow. Examples of states include "reviewed","submitted" or "pending". Note that the first state you list is the one in which documents will start the workflow - this can be changed later on. Please enter a list of states, one per line. State names must be unique, and this includes states already in this workflow.'), + 'required' => true, + 'value' => $oState->getName(), + )), + )); + + $oForm->setValidators(array( + array('ktcore.validators.string', array( + 'test' => 'name', + 'output' => 'name', + )), + )); + + return $oForm; + } + + function do_editstate() { + // remember that we check for state, + // and its null if none or an error was passed. + if (is_null($this->oState)) { + $this->errorRedirectTo('basic', _kt("No state specified.")); + } + + $oTemplate =& $this->oValidator->validateTemplate('ktcore/workflow/admin/edit_state'); + $this->oPage->setBreadcrumbDetails(_kt('Manage State Details')); + + $oForm = $this->form_editstate($this->oState); + + $oTemplate->setData(array( + 'context' => $this, + 'edit_form' => $oForm, + )); + + return $oTemplate->render(); + } + + function do_savestate() { + $oForm = $this->form_editstate(); + $res = $oForm->validate(); + $data = $res['results']; + $errors = $res['errors']; + $extra_errors = array(); + + // check if any *other* states have this name. + if ($data['name'] == $this->oState->getName()) { + $this->successRedirectTo('editstate',_kt("No change in name.")); + } + + // otherwise we're looking for something different if there's a conflict. + + if (KTWorkflowState::nameExists($data['name'], $this->oWorkflow)) { + $extra_errors['name'][] = _kt('There is already a state with that name in this workflow.'); + } + + if (!empty($errors) || !empty($extra_errors)) { + $oForm->handleError(null, $extra_errors); + } + + $this->startTransaction(); + + $this->oState->setName($data['name']); + $this->oState->setHumanName($data['name']); + $res = $this->oState->update(); + + if (PEAR::isError($res)) { + $oForm->handleError(sprintf(_kt("Unable to update state: %s"), $res->getMessage())); + } + + $this->successRedirectTo('editstate', _kt("State updated.")); + } + + function form_edittransition($oTransition) { + $oForm = new KTForm; + + $oForm->setOptions(array( + 'context' => $this, + 'label' => _kt('Edit Transition'), + 'submit_label' => _kt('Update Transition'), + 'action' => 'savetransition', + 'fail_action' => 'edittransition', + 'cancel_action' => 'basic', + )); + + $oForm->setWidgets(array( + array('ktcore.widgets.string', array( + 'name' => 'name', + 'label' => _kt('Transition Name'), + 'description' => _kt('In order to move between states, users will cause "transitions" to occur. These transitions represent processes followed, e.g. "review document", "distribute invoice" or "publish". Transition names must be unique within the workflow (e.g. within this workflow, you can only have one transition called "publish")'), + 'required' => true, + 'value' => $oTransition->getName(), + )), + )); + + $oForm->setValidators(array( + array('ktcore.validators.string', array( + 'test' => 'name', + 'output' => 'name', + )), + )); + + return $oForm; + } + + function do_edittransition() { + // remember that we check for state, + // and its null if none or an error was passed. + if (is_null($this->oTransition)) { + $this->errorRedirectTo('basic', _kt("No transition specified.")); + } + + $oTemplate =& $this->oValidator->validateTemplate('ktcore/workflow/admin/edit_transition'); + $this->oPage->setBreadcrumbDetails(_kt('Manage Transition')); + + $oForm = $this->form_edittransition($this->oTransition); + + $oTemplate->setData(array( + 'context' => $this, + 'edit_form' => $oForm, + )); + + return $oTemplate->render(); + } + + function do_savetransition() { + $oForm = $this->form_edittransition(); + $res = $oForm->validate(); + $data = $res['results']; + $errors = $res['errors']; + $extra_errors = array(); + + // check if any *other* states have this name. + if ($data['name'] == $this->oTransition->getName()) { + $this->successRedirectTo('edittransition',_kt("No change in name.")); + } + + // otherwise we're looking for something different if there's a conflict. + + if (KTWorkflowTransitions::nameExists($data['name'], $this->oWorkflow)) { + $extra_errors['name'][] = _kt('There is already a transition with that name in this workflow.'); + } + + if (!empty($errors) || !empty($extra_errors)) { + $oForm->handleError(null, $extra_errors); + } + + $this->startTransaction(); + + $this->oTransition->setName($data['name']); + $this->oTransition->setHumanName($data['name']); + $res = $this->oTransition->update(); + + if (PEAR::isError($res)) { + $oForm->handleError(sprintf(_kt("Unable to update transition: %s"), $res->getMessage())); + } + + $this->successRedirectTo('transition', _kt("Transition updated.")); + } + + // ----------------- Security --------------------- + function do_security() { + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/security_overview'); + $this->oPage->setBreadcrumbDetails(_kt("Security")); + + + $oTemplate->setData(array( + 'context' => $this, + 'workflow_name' => $this->oWorkflow->getName(), + )); + return $oTemplate->render(); + } + + + // == PERMISSIONS + function do_permissionsoverview() { + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/permissions_overview'); + + // we want to give a complete overview. + // this involves a grid of: + // permission permissions + // state x - + // state - x + + $aStates = KTWorkflowState::getByWorkflow($this->oWorkflow); + $aUsefulPermissions = KTPermission::getDocumentRelevantList(); + $aPermissionGrid = array(); + $aControllers = array(); + foreach ($aStates as $oState) { + $perms = array(); + $aStatePermAssigns = KTWorkflowStatePermissionAssignment::getByState($oState); + $aControllers[$oState->getId()] = (!empty($aStatePermAssigns)); + + foreach ($aStatePermAssigns as $oPermAssign) { + $perms[$oPermAssign->getPermissionId()] = $oPermAssign; // we only care about non-null in *this* map. + } + + $aPermissionGrid[$oState->getId()] = $perms; + } + + $oTemplate->setData(array( + 'context' => $this, + 'controllers' => $aControllers, + 'perm_grid' => $aPermissionGrid, + 'perms' => $aUsefulPermissions, + 'states' => $aStates, + )); + return $oTemplate->render(); + } + + function form_managepermissions() { + $oForm = new KTForm; + $oForm->setOptions(array( + 'label' => _kt("Controlled Permissions"), + 'submit_label' => _kt("Set controlled permissions"), + 'action' => 'setcontrolledpermissions', + 'fail_action' => 'managepermissions', + 'cancel_action' => 'permissionsoverview', + 'context' => $this, + )); + + return $oForm; + } + + // == PERMISSIONS + function do_managepermissions() { + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/managepermissions'); + + $oForm = $this->form_managepermissions(); + + $aUsefulPermissions = KTPermission::getDocumentRelevantList(); + $aPermissionGrid = array(); + $aStatePermAssigns = KTWorkflowStatePermissionAssignment::getByState($this->oState); + + foreach ($aStatePermAssigns as $oPermAssign) { + $aPermissionGrid[$oPermAssign->getPermissionId()] = $oPermAssign; + } + + $oTemplate->setData(array( + 'context' => $this, + 'perm_grid' => $aPermissionGrid, + 'perms' => $aUsefulPermissions, + 'form' => $oForm, + )); + return $oTemplate->render(); + } + + function do_setcontrolledpermissions() { + $active = (array) KTUtil::arrayGet($_REQUEST, 'fControlled'); + + $aUsefulPerms = KTPermission::getDocumentRelevantList(); + $aStatePermAssigns = KTWorkflowStatePermissionAssignment::getByState($this->oState); + $aStatePermAssigns = KTUtil::keyArray($aStatePermAssigns, 'getPermissionId'); + $assigns = array(); + + $this->startTransaction(); + // delete those who don't know want + // create those we don't have. + + foreach ($aStatePermAssigns as $perm_id => $assign) { + if (!$active[$perm_id]) { + $assign->delete(); + } + } + $emptydescriptor = KTPermissionUtil::getOrCreateDescriptor(array()); + if (PEAR::isError($emptydescriptor)) { + $this->errorRedirectTo("managepermissions", sprintf(_kt("Failed to create assignment: %s"), $emptydescriptor->getMessage())); + } + foreach ($active as $perm_id => $discard) { + if (!$aStatePermAssigns[$perm_id]) { + $assign = KTWorkflowStatePermissionAssignment::createFromArray(array( + "iStateId" => $this->oState->getId(), + "iPermissionId" => $perm_id, + "iDescriptorId" => $emptydescriptor->getId(), + )); + if (PEAR::isError($assign)) { + $this->errorRedirectTo("managepermissions", sprintf(_kt("Failed to create assignment: %s"), $assign->getMessage())); + } + } + } + + $this->successRedirectTo("managepermissions", _kt("Controlled permission updated.")); + } + + // == PERMISSIONS + function do_allocatepermissions() { + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/allocate_permissions'); + + $oForm = $this->form_managepermissions(); + + $aUsefulPermissions = KTPermission::getDocumentRelevantList(); + $aPermissionGrid = array(); + $aStatePermAssigns = KTWorkflowStatePermissionAssignment::getByState($this->oState); + + foreach ($aStatePermAssigns as $oPermAssign) { + $aPermissionGrid[$oPermAssign->getPermissionId()] = $oPermAssign; + } + + $aPermissionsToJSON = array(); + foreach($aUsefulPermissions as $oP) { + $perm_id = $oP->getId(); + if ($aPermissionGrid[$perm_id]) { + $aPermissionsToJSON[] = array('id'=>$oP->getId(), 'name'=>$oP->getHumanName()); + } + } + + $oJSON = new Services_JSON; + $sJSONPermissions = $oJSON->encode($aPermissionsToJSON); + + $oTemplate->setData(array( + 'context' => $this, + 'perm_grid' => $aPermissionGrid, + 'perms' => $aUsefulPermissions, + 'form' => $oForm, + 'jsonpermissions' => $sJSONPermissions, + 'args' => $this->meldPersistQuery("","setpermissionallocations",true), + )); + return $oTemplate->render(); + } + + // JSON helper. from permissions. + + function &_getPermissionsMap() { + $aStatePermAssigns = KTWorkflowStatePermissionAssignment::getByState($this->oState); + $aPermissionsMap = array('role'=>array(), 'group'=>array()); + + foreach ($aStatePermAssigns as $oPermAssign) { + $oDescriptor = KTPermissionDescriptor::get($oPermAssign->getDescriptorId()); + $iPermissionId = $oPermAssign->getPermissionId(); + + // groups + $aGroupIds = $oDescriptor->getGroups(); + foreach ($aGroupIds as $iId) { + $aPermissionsMap['group'][$iId][$iPermissionId] = true; + } + + // roles + $aRoleIds = $oDescriptor->getRoles(); + foreach ($aRoleIds as $iId) { + $aPermissionsMap['role'][$iId][$iPermissionId] = true; + } + } + return $aPermissionsMap; + } + + function json_getEntities($optFilter = null) { + $sFilter = KTUtil::arrayGet($_REQUEST, 'filter', false); + if($sFilter == false && $optFilter != null) { + $sFilter = $optFilter; + } + + $bSelected = KTUtil::arrayGet($_REQUEST, 'selected', false); + + $aEntityList = array('off'=>'-- Please filter --'); + + // get permissions map + $aPermissionsMap =& $this->_getPermissionsMap(); + + if($bSelected || $sFilter && trim($sFilter)) { + if(!$bSelected) { + $aEntityList = array(); + } + + $aGroups = Group::getList(sprintf('name like "%%%s%%"', $sFilter)); + foreach($aGroups as $oGroup) { + $aPerm = @array_keys($aPermissionsMap['group'][$oGroup->getId()]); + if(!is_array($aPerm)) { + $aPerm = array(); + } + + if($bSelected) { + if(count($aPerm)) + $aEntityList['g'.$oGroup->getId()] = array('type' => 'group', + 'display' => 'Group: ' . $oGroup->getName(), + 'name' => $oGroup->getName(), + 'permissions' => $aPerm, + 'id' => $oGroup->getId(), + 'selected' => true); + } else { + $aEntityList['g'.$oGroup->getId()] = array('type' => 'group', + 'display' => 'Group: ' . $oGroup->getName(), + 'name' => $oGroup->getName(), + 'permissions' => $aPerm, + 'id' => $oGroup->getId()); + } + } + + $aRoles = Role::getList(sprintf('name like "%%%s%%"', $sFilter)); + foreach($aRoles as $oRole) { + $aPerm = @array_keys($aPermissionsMap['role'][$oRole->getId()]); + if(!is_array($aPerm)) { + $aPerm = array(); + } + + if($bSelected) { + if(count($aPerm)) + $aEntityList['r'.$oRole->getId()] = array('type' => 'role', + 'display' => 'Role: ' . $oRole->getName(), + 'name' => $oRole->getName(), + 'permissions' => $aPerm, + 'id' => $oRole->getId(), + 'selected' => true); + } else { + $aEntityList['r'.$oRole->getId()] = array('type' => 'role', + 'display' => 'Role: ' . $oRole->getName(), + 'name' => $oRole->getName(), + 'permissions' => $aPerm, + 'id' => $oRole->getId()); + } + } + } + return $aEntityList; + } + + + function do_setpermissionallocations() { + $aPermissionAllowed = (array) KTUtil::arrayGet($_REQUEST, 'foo'); // thanks BD. + + $this->startTransaction(); + + $aStatePermAssigns = KTWorkflowStatePermissionAssignment::getByState($this->oState); + + // we now walk the alloc'd perms, and go. + foreach ($aStatePermAssigns as $oPermAssign) { + $aAllowed = (array) $aPermissionAllowed[$oPermAssign->getPermissionId()]; // is already role, group, etc. + $oDescriptor = KTPermissionUtil::getOrCreateDescriptor($aAllowed); + if (PEAR::isError($oDescriptor)) { $this->errorRedirectTo('allocatepermissions', _kt('Failed to allocate as specified.')); } + + $oPermAssign->setDescriptorId($oDescriptor->getId()); + $res = $oPermAssign->update(); + if (PEAR::isError($res)) { $this->errorRedirectTo('allocatepermissions', _kt('Failed to allocate as specified.')); } + } + + KTPermissionUtil::updatePermissionLookupForState($oState); + + $this->successRedirectTo('managepermissions', _kt('Permissions Allocated.')); + } + + + // ----------------- Effects --------------------- + function do_effects() { + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/effects_overview'); + $this->oPage->setBreadcrumbDetails(_kt("Workflow Effects")); + + + $oTemplate->setData(array( + 'context' => $this, + 'workflow_name' => $this->oWorkflow->getName(), + )); + return $oTemplate->render(); + } +} + +?> diff --git a/sql/mysql/upgrade/3.1.6/workflow-sanity.sql b/sql/mysql/upgrade/3.1.6/workflow-sanity.sql new file mode 100644 index 0000000..3fffbc1 --- /dev/null +++ b/sql/mysql/upgrade/3.1.6/workflow-sanity.sql @@ -0,0 +1,3 @@ +ALTER TABLE `workflows` ADD `enabled` INT(1) UNSIGNED NOT NULL DEFAULT 1; +ALTER TABLE `workflow_states` ADD `manage_permissions` INT(1) UNSIGNED NOT NULL DEFAULT 0; +ALTER TABLE `workflow_states` ADD `manage_actions` INT(1) UNSIGNED NOT NULL DEFAULT 0; diff --git a/templates/ktcore/workflow/admin/add_states.smarty b/templates/ktcore/workflow/admin/add_states.smarty new file mode 100644 index 0000000..adf4562 --- /dev/null +++ b/templates/ktcore/workflow/admin/add_states.smarty @@ -0,0 +1,3 @@ +

{i18n}Add States to Workflow{/i18n}

+ +{$form->render()} diff --git a/templates/ktcore/workflow/admin/add_transitions.smarty b/templates/ktcore/workflow/admin/add_transitions.smarty new file mode 100644 index 0000000..da9f978 --- /dev/null +++ b/templates/ktcore/workflow/admin/add_transitions.smarty @@ -0,0 +1,3 @@ +

{i18n}Add Transitions to Workflow{/i18n}

+ +{$form->render()} diff --git a/templates/ktcore/workflow/admin/allocate_permissions.smarty b/templates/ktcore/workflow/admin/allocate_permissions.smarty new file mode 100644 index 0000000..cf345f5 --- /dev/null +++ b/templates/ktcore/workflow/admin/allocate_permissions.smarty @@ -0,0 +1,69 @@ + +{$context->oPage->requireJSResource("resources/js/jsonlookup.js")} +{$context->oPage->requireJSResource("resources/js/permissions.js")} + +{capture assign=sJavascript}initializePermissions('entities', '{addQS context=$context}action=json&json_action=getEntities{/addQS}', {$jsonpermissions});{/capture} +{$context->oPage->requireJSStandalone($sJavascript)} + +
+
+ +

{i18n}Select roles and groups for whom you wish to change permission assignement from the box on the left, and move them over to the box on the right using the button with right-pointing arrows. You can then allocate or remove permissions from these entities and save by pressing the 'Update Permission Assignments' button'.{/i18n}

+ + + + + + + + + + + + + + + + + + + + +
 
+ + + +

Show All
+
+ + +

+ + +
+ + +
+
+ + + + + +
+ + +
+ +{foreach from=$args key=k item=v} + +{/foreach} +
+ + {i18n}Cancel{/i18n} +
+
+ + diff --git a/templates/ktcore/workflow/admin/basic_overview.smarty b/templates/ktcore/workflow/admin/basic_overview.smarty new file mode 100644 index 0000000..ae799e1 --- /dev/null +++ b/templates/ktcore/workflow/admin/basic_overview.smarty @@ -0,0 +1,75 @@ +

{i18n arg_name=$workflow_name}States and Transitions: #name#{/i18n}

+ +

The core of a workflow is the process +that documents in that workflow follow. These processes are made up of states +(which documents are in, e.g. "reviewed" or "published") and transitions +which documents follow (e.g. "submit for review" or "publish").

+ +

Configure Workflow Process +Configure Workflow Process (e.g. which transitions lead to which states)

+ +
+ +

States

+ Add New States + Add New States

+ + + + + + + + + + + + {foreach from=$states item=oState} + + + + + + {/foreach} + +
State NameEditDelete
+ {$oState->getName()} + + Edit State + + Delete State +
+
+
+

Transitions

+ + Add New Transitions + Add New Transitions

+ + + + + + + + + + + + + {foreach from=$transitions item=oTransition} + + + + + + {/foreach} + +
Transition NameEditDelete
+ {$oTransition->getName()} + + Edit + + Delete +
+
diff --git a/templates/ktcore/workflow/admin/configure_process.smarty b/templates/ktcore/workflow/admin/configure_process.smarty new file mode 100644 index 0000000..3db37c4 --- /dev/null +++ b/templates/ktcore/workflow/admin/configure_process.smarty @@ -0,0 +1,41 @@ +

{i18n arg_name=$context->oWorkflow->getName()}States and Transitions: #name#{/i18n}

+ +{capture assign=widgets} + + + + + + + + {foreach from=$states item=oState} + + {/foreach} + + + + {foreach from=$transitions item=oTransition} + + + + {foreach from=$states item=oState} + {assign value=$oTransition->getId() var=trans_id} + {assign value=$oState->getId() var=state_id} + + {/foreach} + + {/foreach} + +
{i18n}Transition{/i18n}{i18n}Leads to state{/i18n}{$oState->getName()}
{$oTransition->getName()}
+ +{/capture} +{assign value=$form->renderButtons() var=buttons} + +{$form->renderContaining($widgets, $buttons)} diff --git a/templates/ktcore/workflow/admin/edit_core.smarty b/templates/ktcore/workflow/admin/edit_core.smarty new file mode 100644 index 0000000..3aa82f8 --- /dev/null +++ b/templates/ktcore/workflow/admin/edit_core.smarty @@ -0,0 +1,3 @@ +

{i18n arg_name=$workflow_name}Edit Workflow Details: #name#{/i18n}

+ +{$edit_form->render()} diff --git a/templates/ktcore/workflow/admin/edit_state.smarty b/templates/ktcore/workflow/admin/edit_state.smarty new file mode 100644 index 0000000..5f01e7c --- /dev/null +++ b/templates/ktcore/workflow/admin/edit_state.smarty @@ -0,0 +1,26 @@ +

Manage State

+ +{$edit_form->render()} + +{* The real meat is down here. We define and list a set of actions and links to them. *} + +

State Effects

+ +

{i18n}One of the reasons that workflow is so key to +the way KnowledgeTree is used is that states can have a variety of effects on +the way other systems work. For example: workflow states can override the permissions +on a document, and reaching a state can cause notifications to be sent out.{/i18n}

+ +
+
{i18n}Security at this state{/i18n}
+
{i18n}When a document is in a workflow state, that + state can override some or all of the permissions that would "normally" be + assigned to the document (e.g. via the folder it is in). It can also restrict + which document actions are available.{/i18n}
+ +
{i18n}Notifications{/i18n}
+
{i18n}In order to progress through a workflow, a document + will usually require collaboration between a number of different users. + One way to help this process is to inform certain groups or roles about + the document's current state.{/i18n}
+
diff --git a/templates/ktcore/workflow/admin/edit_transition.smarty b/templates/ktcore/workflow/admin/edit_transition.smarty new file mode 100644 index 0000000..143da82 --- /dev/null +++ b/templates/ktcore/workflow/admin/edit_transition.smarty @@ -0,0 +1,13 @@ +

Manage Transition

+ +{$edit_form->render()} + +{* The real meat is down here. We define and list a set of actions and links to them. *} + +
+
{i18n}Transition Requirements{/i18n}
+
{i18n}You can control when and by whom transitions can + be performed by setting up various guards. These can include permissions, roles, + groups or a variety of other restriction conditions.{/i18n}
+ +
diff --git a/templates/ktcore/workflow/admin/effects_overview.smarty b/templates/ktcore/workflow/admin/effects_overview.smarty new file mode 100644 index 0000000..0bc61ff --- /dev/null +++ b/templates/ktcore/workflow/admin/effects_overview.smarty @@ -0,0 +1,7 @@ +

{i18n arg_name=$workflow_name}Workflow Effects Overview: #name#{/i18n}

+ +

As a document moves through a workflow, it can cause +varies other actions to occur. For example, you can attach a "Move" action to a transition, +which will cause any document moving through that workflow to be moved to a particular folder. +Or you can specify that when a document reaches the "Pending Review" state, users +with the role "Reviewer" on that document are informed.

diff --git a/templates/ktcore/workflow/admin/list.smarty b/templates/ktcore/workflow/admin/list.smarty new file mode 100644 index 0000000..2f410e7 --- /dev/null +++ b/templates/ktcore/workflow/admin/list.smarty @@ -0,0 +1,34 @@ +

{i18n}Workflow Admin{/i18n}

+ +

{i18n}Workflow is a description of a document's lifecycle. It is made up of workflow states, which describe where in the lifecycle the document is, and workflow transitions, which describe the next steps within the lifecycle of the document.{/i18n}

+ +{i18n}Create New Workflow{/i18n}{i18n}Create New Workflow{/i18n} + +{if !empty($workflows)} + +

{i18n}Existing workflows{/i18n}

+

{i18n}Select a workflow to modify. To enable a disabled workflow, edit it and set a proper starting state.{/i18n}

+ + + + + + + + + + + + +{foreach from=$workflows item=oWorkflow} + + + + + + +{/foreach} + +
{i18n}Name{/i18n}{i18n}Status{/i18n}{i18n}Edit{/i18n}
{$oWorkflow->getName()}{if $oWorkflow->getIsFunctional()} {i18n}Enabled{/i18n} {else} {i18n}Disabled{/i18n} {/if}{i18n}Edit{/i18n}
+ +{/if} diff --git a/templates/ktcore/workflow/admin/managepermissions.smarty b/templates/ktcore/workflow/admin/managepermissions.smarty new file mode 100644 index 0000000..f779698 --- /dev/null +++ b/templates/ktcore/workflow/admin/managepermissions.smarty @@ -0,0 +1,41 @@ +

{i18n arg_statename=$context->oState->getName()}Manage Permissions: #statename#{/i18n}

+ +{if empty($perm_grid)} +

{i18n}No permissions are controlled by this state. +Indicate below which permissions are controlled to allocate them.{/i18n}

+{else} + +

{i18n}Once you've selected the permissions you want to control +for this workflow state, you should allocate these to the appropriate groups +and roles.{/i18n}

+ +Allocate permissions +Allocate permissions + +{/if} + +

Specify permissions

+ +{capture assign=permgrid} + +

{i18n}Select the permissions you want controlled by this state.{/i18n}

+ + + + + {foreach from=$perms item=oPerm} + + {/foreach} + + + + + {foreach from=$perms item=oPerm} + {assign value=$oPerm->getId() var=perm_id} + + {/foreach} + + +
{i18n}{$oPerm->getHumanName()}{/i18n}
+{/capture} +{$form->renderContaining($permgrid, $form->renderButtons())} diff --git a/templates/ktcore/workflow/admin/new_wizard_step1.smarty b/templates/ktcore/workflow/admin/new_wizard_step1.smarty new file mode 100644 index 0000000..dc29223 --- /dev/null +++ b/templates/ktcore/workflow/admin/new_wizard_step1.smarty @@ -0,0 +1,3 @@ +

{i18n}Step 1: Basic Workflow Details{/i18n}

+ +{$form->render()} diff --git a/templates/ktcore/workflow/admin/new_wizard_step2.smarty b/templates/ktcore/workflow/admin/new_wizard_step2.smarty new file mode 100644 index 0000000..9082dee --- /dev/null +++ b/templates/ktcore/workflow/admin/new_wizard_step2.smarty @@ -0,0 +1,44 @@ +

{i18n}Step 2: Connect transitions to states{/i18n}

+ +

{i18n}In order to move between states, the transitions +you specified earlier must be configured to move from a set of states to a "destination" +states. Use the table below to configure this behaviour.{/i18n}

+ +
+ {foreach from=$args key=k item=v} + + {/foreach} + + + + + + + {foreach from=$states item=state} + + {/foreach} + + + + {foreach from=$transitions item=transition} + + + + {foreach from=$states item=state} + + {/foreach} + + {/foreach} + +
{i18n}Transition{/i18n}{i18n}Leads to state{/i18n}{$state}
{$transition}
+ + +
+ + Cancel +
+
diff --git a/templates/ktcore/workflow/admin/permissions_overview.smarty b/templates/ktcore/workflow/admin/permissions_overview.smarty new file mode 100644 index 0000000..c4a587c --- /dev/null +++ b/templates/ktcore/workflow/admin/permissions_overview.smarty @@ -0,0 +1,38 @@ +

{i18n}Permissions Overview{/i18n}

+ +

A particular workflow state can override some, all, +or none of the permissions that would normally apply to a document. In this +way you can (for example) let the folder's permissions decide who can see +the document (with Read permissions), while having the workflow +restrict access to the "edit" permission.

+ +

States which control permissions have a tick in +the "Control" column. Permissions which are not controlled by a state (e.g. which +are controlled by the folder a document is in) are marked with a dash (—). +Controlled permissions are marked with a tick. Click on the state name to +specify how it controls permissions.

+ + + + + + + {foreach from=$perms item=oPerm} + + {/foreach} + + + + {foreach from=$states item=oState} + {assign value=$oState->getId() var=state_id} + + + + {foreach from=$perms item=oPerm} + {assign value=$oPerm->getId() var=perm_id} + + {/foreach} + + {/foreach} + +
StatesControl{i18n}{$oPerm->getHumanName()}{/i18n}
{$oState->getName()}{if ($controllers.$state_id)} yes {else} no {/if}{if ($perm_grid.$state_id.$perm_id)} managed {else} — {/if}
diff --git a/templates/ktcore/workflow/admin/security_overview.smarty b/templates/ktcore/workflow/admin/security_overview.smarty new file mode 100644 index 0000000..38a134f --- /dev/null +++ b/templates/ktcore/workflow/admin/security_overview.smarty @@ -0,0 +1,18 @@ +

{i18n arg_name=$workflow_name}Security Overview: #name#{/i18n}

+ +

KnowledgeTree has a powerful security model, in which users +can only see documents they have permissions to see. Workflow is the finest-grained +way to allocate permissions to a document, since it can override the permissions +assigned at a folder level.

+ +

There are 3 different ways in which workflows interact +with the system's security:

+ +
    +
  1. Document Permissions (by state)
  2. +
  3. Action Restrictions (by state)
  4. +
  5. Transition Restrictions
  6. + +
+ +

Each of these can be managed from this area.

diff --git a/templates/ktcore/workflow/admin/view.smarty b/templates/ktcore/workflow/admin/view.smarty new file mode 100644 index 0000000..a0d779a --- /dev/null +++ b/templates/ktcore/workflow/admin/view.smarty @@ -0,0 +1,9 @@ +

{i18n arg_name=$workflow_name}Workflow: #name#{/i18n}

+ +

One of the most powerful features of KnowledgeTree is the workflow +system. This allows you to direct the lifecycle of a document from start to finish. The +"Workflow Administration" menu on the left allows you to access and update information +about states, transitions, security and notifications as they apply to this workflow.

+ +

Edit Workflow Details +Edit Workflow Details (e.g. workflow name, starting state)