classes/Gems/Tracker/RespondentTrack.php
<?php
/**
*
* @package Gems
* @subpackage Tracker
* @author Matijs de Jong <mjong@magnafacta.nl>
* @copyright Copyright (c) 2011 Erasmus MC
* @license New BSD License
*/
use Gems\Event\Application\TokenEvent;
use Gems\Event\Application\RespondentTrackFieldUpdateEvent;
use Gems\Event\Application\RespondentTrackFieldEvent;
use Gems\Tracker\Engine\FieldsDefinition;
use Gems\Tracker\Model\FieldMaintenanceModel;
use Gems\Translate\DbTranslateUtilTrait;
/**
* Object representing a track assignment to a respondent.
*
* @package Gems
* @subpackage Tracker
* @copyright Copyright (c) 2011 Erasmus MC
* @license New BSD License
* @since Class available since version 1.4
*/
class Gems_Tracker_RespondentTrack extends \Gems_Registry_TargetAbstract
{
use DbTranslateUtilTrait;
/**
*
* @var array of round_id => \Gems_Tracker_Token
*/
protected $_activeTokens = array();
/**
* @var \Gems_Tracker_Token
*/
protected $_checkStart;
/**
* If a field has a code name the value will occur both using
* the code name and using the id.
*
* @var array Field data id/code => value
*/
protected $_fieldData = null;
/**
*
* @var \Gems_Tracker_Token
*/
protected $_firstToken;
/**
*
* @var \Gems_Tracker_Respondent
*/
protected $_respondentObject = null;
/**
*
* @var array The gems__respondent2track data
*/
protected $_respTrackData;
/**
*
* @var int The gems__respondent2track id
*/
protected $_respTrackId;
/**
*
* @var array The gems__rounds data
*/
protected $_rounds = null;
/**
*
* @var array of \Gems_Tracker_Token
*/
protected $_tokens;
/**
* @var array
*/
protected $_tablesForTranslations = [
'gems__respondent2track' => 'gr2t_id_respondent_track',
'gems__tracks' => 'gtr_id_track',
];
/**
*
* @var \Gems_User_User
*/
protected $currentUser;
/**
* @var \Gems\Event\EventDispatcher
*/
protected $event;
/**
*
* @var \Gems_Loader
*/
protected $loader;
/**
*
* @var \Gems_Tracker
*/
protected $tracker;
/**
*
* @var \Gems_Util
*/
protected $util;
/**
*
* @param mixed $respTracksData Track Id or array containing reps2track record
*/
public function __construct($respTracksData)
{
if (is_array($respTracksData)) {
$this->_respTrackData = $respTracksData;
$this->_respTrackId = $respTracksData['gr2t_id_respondent_track'];
} else {
$this->_respTrackId = $respTracksData;
}
}
/**
* Check this respondent track for the number of tokens completed / to do
*
* @param int $userId Id of the user who takes the action (for logging)
* @return int 1 if the track was changed by this code
*/
public function _checkTrackCount($userId)
{
$sqlCount = 'SELECT COUNT(*) AS count,
SUM(CASE WHEN gto_completion_time IS NULL THEN 0 ELSE 1 END) AS completed
FROM gems__tokens INNER JOIN
gems__reception_codes ON gto_reception_code = grc_id_reception_code AND grc_success = 1
WHERE gto_id_respondent_track = ?';
$counts = $this->db->fetchRow($sqlCount, $this->_respTrackId);
if (! $counts) {
$counts = array('count' => 0, 'completed' => 0);
}
$values['gr2t_count'] = intval($counts['count']);
$values['gr2t_completed'] = intval($counts['completed']);
if (! $this->_respTrackData['gr2t_end_date_manual']) {
$values['gr2t_end_date'] = $this->calculateEndDate();
}
if ($values['gr2t_count'] == $values['gr2t_completed']) {
if (null === $this->_respTrackData['gr2t_end_date']) {
$now = new \MUtil_Date();
$values['gr2t_end_date'] = $now->toString(\Gems_Tracker::DB_DATETIME_FORMAT);
}
//Handle TrackCompletionEvent, send only changed fields in $values array
$this->handleTrackCompletion($values, $userId);
}
// Remove unchanged values
$this->tracker->filterChangesOnly($this->_respTrackData, $values);
return $this->_updateTrack($values, $userId);
}
/**
* Makes sure the fieldData is in $this->_fieldData
*
* @param boolean $reload Optional parameter to force reload.
*/
private function _ensureFieldData($reload = false)
{
if ($this->_respTrackData && (null === $this->_fieldData) || $reload) {
$this->_fieldData = $this->getTrackEngine()->getFieldsData($this->_respTrackId);
$this->_fixFieldData();
}
}
/**
* Adds the code fields to the fieldData array
*/
public function _fixFieldData()
{
$fieldMap = $this->getTrackEngine()->getFieldCodes();
foreach ($this->_fieldData as $key => $value) {
if (isset($fieldMap[$key])) {
// The old name remains in the data set of course,
// using the code is a second occurence
$this->_fieldData[$fieldMap[$key]] = $value;
}
}
}
/**
* Makes sure the respondent data is part of the $this->_respTrackData
*/
protected function _ensureRespondentData()
{
if (! isset($this->_respTrackData['grs_id_user'], $this->_respTrackData['gr2o_id_user'], $this->_respTrackData['gco_code'])) {
$sql = "SELECT *
FROM gems__respondents INNER JOIN
gems__respondent2org ON grs_id_user = gr2o_id_user INNER JOIN
gems__consents ON gr2o_consent = gco_description
WHERE gr2o_id_user = ? AND gr2o_id_organization = ? LIMIT 1";
$respId = $this->_respTrackData['gr2t_id_user'];
$orgId = $this->_respTrackData['gr2t_id_organization'];
if ($row = $this->db->fetchRow($sql, array($respId, $orgId))) {
$this->_respTrackData = $this->_respTrackData + $row;
} else {
$trackId = $this->_respTrackId;
throw new \Gems_Exception("Respondent data missing for track $trackId.");
}
}
}
/**
* Makes sure the rounds info is loaded
*
* @param boolean $reload
*/
protected function _ensureRounds($reload = false)
{
if ((null === $this->_rounds) || $reload) {
$rounds = $this->getTrackEngine()->getRoundModel(true, 'index')
->load(['gro_id_track'=>$this->getTrackId()]);
$this->_rounds = array();
foreach($rounds as $round) {
$this->_rounds[$round['gro_id_round']] = $round;
}
}
}
/**
* Makes sure the track data is part of the $this->_respTrackData
*/
protected function _ensureTrackData()
{
if (! isset($this->_respTrackData['gtr_code'], $this->_respTrackData['gtr_name'])) {
$trackData = $this->fetchTranslatedRow('gems__tracks', 'gtr_id_track', $this->_respTrackData['gr2t_id_track']);
if ($trackData) {
$this->_respTrackData = $this->_respTrackData + $trackData;
} else {
$trackId = $this->_respTrackId;
throw new \Gems_Exception("Track data missing for respondent track $trackId.");
}
}
}
/**
* Processes the field values and returns the new complete field data
*
* @param array $newFieldData The new field values, may be partial, field set by code overwrite field set by key
* @param array $oldFieldData The old field values
* @param \Gems_Tracker_Engine_TrackEngineInterface $trackEngine
* @return array The processed data in the format key1 => val1, code1 => val1, key2 => val2
*/
protected function _mergeFieldValues(array $newFieldData, array $oldFieldData, \Gems_Tracker_Engine_TrackEngineInterface $trackEngine)
{
$fieldDef = $trackEngine->getFieldsDefinition();
$fieldMap = $fieldDef->getFieldCodes() + $fieldDef->getManualFields();
$output = array();
// \MUtil_Echo::track($fieldMap);
foreach ($fieldMap as $key => $code) {
if ($code) {
if (array_key_exists($code, $newFieldData)) {
$output[$key] = $newFieldData[$code];
$output[$code] = $newFieldData[$code];
} elseif (array_key_exists($key, $newFieldData)) {
$output[$key] = $newFieldData[$key];
$output[$code] = $newFieldData[$key];
} elseif (isset($oldFieldData[$code])) {
$output[$key] = $oldFieldData[$code];
$output[$code] = $oldFieldData[$code];
} elseif (isset($oldFieldData[$key])) {
$output[$key] = $oldFieldData[$key];
$output[$code] = $oldFieldData[$key];
} else {
$output[$key] = null;
$output[$code] = null;
}
} else {
if (array_key_exists($key, $newFieldData)) {
$output[$key] = $newFieldData[$key];
} elseif (isset($oldFieldData[$key])) {
$output[$key] = $oldFieldData[$key];
} else {
$output[$key] = null;
}
}
}
return $output;
}
/**
* Save the values if any have been changed
*
* @param array $values
* @param int $userId
* @return int
*/
protected function _updateTrack(array $values, $userId = null)
{
if (null === $userId) {
$userId = $this->currentUser->getUserId();
}
// \MUtil_Echo::track($values);
if ($this->tracker->filterChangesOnly($this->_respTrackData, $values)) {
$where = $this->db->quoteInto('gr2t_id_respondent_track = ?', $this->_respTrackId);
if (\Gems_Tracker::$verbose) {
$echo = '';
foreach ($values as $key => $val) {
$echo .= $key . ': ' . $this->_respTrackData[$key] . ' => ' . $val . "\n";
}
\MUtil_Echo::r($echo, 'Updated values for ' . $this->_respTrackId);
}
if (! isset($values['gr2t_changed'])) {
$values['gr2t_changed'] = new \MUtil_Db_Expr_CurrentTimestamp();
}
if (! isset($values['gr2t_changed_by'])) {
$values['gr2t_changed_by'] = $userId;
}
$this->_respTrackData = $values + $this->_respTrackData;
// \MUtil_Echo::track($values);
// return 1;
return $this->db->update('gems__respondent2track', $values, $where);
} else {
return 0;
}
}
/**
* Add a one-off survey to the existing track.
*
* @param int $surveyId the gsu_id of the survey to add
* @param array $surveyData
* @param int $userId
* @param boolean $checkTrack Should the track be checked? Set to false when adding more then one and check manually
* @return \Gems_Tracker_Token
*/
public function addSurveyToTrack($surveyId, $surveyData, $userId, $checkTrack = true )
{
//Do something to get a token and add it
$tokenLibrary = $this->tracker->getTokenLibrary();
//Now make sure the data to add is correct:
$surveyData['gto_id_respondent_track'] = $this->_respTrackId;
$surveyData['gto_id_organization'] = $this->_respTrackData['gr2t_id_organization'];
$surveyData['gto_id_track'] = $this->_respTrackData['gr2t_id_track'];
$surveyData['gto_id_respondent'] = $this->_respTrackData['gr2t_id_user'];
$surveyData['gto_id_survey'] = $surveyId;
if (! isset($surveyData['gto_id_round'])) {
$surveyData['gto_id_round'] = 0;
}
$tokenId = $tokenLibrary->createToken($surveyData, $userId);
if ($checkTrack === true) {
//Now refresh the track to include the survey we just added (easiest way as order may change)
$this->getTokens(true);
$this->checkTrackTokens($userId, $this->_tokens[$tokenId]);
// Update the track counter
//$this->_checkTrackCount($userId);
return $this->_tokens[$tokenId];
}
return $this->tracker->getToken($tokenId);
}
/**
* Add a one-off survey to the existing track.
*
* @param type $surveyId the gsu_id of the survey to add
* @param type $surveyData
* @param int $userId
* @param boolean $checkTrack Should the track be checked? Set to false when adding more then one and check manually
* @return \Gems_Tracker_Token
*/
public function addTokenToTrack(\Gems_Tracker_Token $token, $tokenData, $userId, $checkTrack = true)
{
//Now make sure the data to add is correct:
$tokenData['gto_id_respondent_track'] = $this->_respTrackId;
$tokenData['gto_id_organization'] = $this->_respTrackData['gr2t_id_organization'];
$tokenData['gto_id_track'] = $this->_respTrackData['gr2t_id_track'];
$tokenData['gto_id_respondent'] = $this->_respTrackData['gr2t_id_user'];
$tokenData['gto_changed'] = new \MUtil_Db_Expr_CurrentTimestamp();
$tokenData['gto_changed_by'] = $userId;
$where = $this->db->quoteInto('gto_id_token = ?', $token->getTokenId());
$this->db->update('gems__tokens', $tokenData, $where);
$token->refresh();
if ($checkTrack === true) {
//Now refresh the track to include the survey we just added (easiest way as order may change)
$this->getTokens(true);
$this->checkTrackTokens($userId, $token);
// Update the track counter
//$this->_checkTrackCount($userId);
}
return $token;
}
/**
* Set menu parameters from this token
*
* @param \Gems_Menu_ParameterSource $source
* @return \Gems_Tracker_RespondentTrack (continuation pattern)
*/
public function applyToMenuSource(\Gems_Menu_ParameterSource $source)
{
$source->setRespondentTrackId($this->_respTrackId);
$source->offsetSet(
'gr2t_active',
(isset($this->_respTrackData['gr2t_active']) ? $this->_respTrackData['gr2t_active'] : 0)
);
$source->offsetSet('can_edit', $this->hasSuccesCode() ? 1 : 0);
$source->offsetSet('track_can_be_created', 0);
$this->getRespondent()->applyToMenuSource($source);
$this->getTrackEngine()->applyToMenuSource($source);
return $this;
}
/**
* Assign the tokens to the correct relation
*
* Only surveys that have not yet been answered will be assigned to the correct relation.
*
* @return int Number of changes tokens
*/
public function assignTokensToRelations()
{
// Find out if we have relation fields and return when none exists in this track
$relationFields = $this->getTrackEngine()->getFieldsOfType('relation');
if (empty($relationFields)) {
return 0;
}
// Check if we have a respondent relation id (grr_id) in the track fields
// and assign the token to the correct relation or leave open when no
// relation is defined.
$this->_ensureRounds();
$relationFields = $this->getFieldData();
$fieldPrefix = FieldsDefinition::makeKey(FieldMaintenanceModel::FIELDS_NAME, '');
$changes = 0;
foreach ($this->getTokens() as $token) {
/* @var $token \Gems_Tracker_Token */
if ((!$token->isCompleted()) && $token->getReceptionCode()->isSuccess()) {
$roundId = $token->getRoundId();
if (!array_key_exists($roundId, $this->_rounds)) {
// If not a current round for this track, do check the round when it still exists
$round = $this->getTrackEngine()->getRoundModel(true, 'index')->loadFirst(array('gro_id_round' => $roundId));
} else {
$round = $this->_rounds[$roundId];
}
$relationFieldId = null;
$relationId = null;
// Read from the round
if (!empty($round) && $round['gro_id_track'] == $this->getTrackId() && $round['gro_active'] == 1) {
if ($round['gro_id_relationfield'] > 0) {
$relationFieldId = $round['gro_id_relationfield'];
}
} else {
// Try to read from token, as this is a token without a round
$relationFieldId = $token->getRelationFieldId();
}
if ($relationFieldId>0) {
$fieldKey = $fieldPrefix . $relationFieldId;
if (isset($relationFields[$fieldKey])) {
$relationId = (int) $relationFields[$fieldKey];
} else {
$relationId = -1 * $relationFieldId;
}
}
$changes = $changes + $token->assignTo($relationId, $relationFieldId);
}
}
if (\MUtil_Model::$verbose && $changes > 0) {
\MUtil_Echo::r(sprintf('%s tokens changed due to changes in respondent relation assignments.', $changes));
}
return $changes;
}
/**
* Calculates the track end date
*
* The end date can be calculated when:
* - all active tokens have a completion date
* - or all active tokens have a valid until date
* - or the end date of the tokens is calculated using the end date
*
* You can overrule this calculation at the project level.
*
* @return string or null
*/
public function calculateEndDate()
{
// Exclude the tokens whose end date is calculated from the track end date
$excludeWheres[] = sprintf(
"gro_valid_for_source = '%s' AND gro_valid_for_field = 'gr2t_end_date'",
\Gems_Tracker_Engine_StepEngineAbstract::RESPONDENT_TRACK_TABLE
);
// Exclude the tokens whose start date is calculated from the track end date, while the
// end date is calculated using that same start date
$excludeWheres[] = sprintf(
"gro_valid_after_source = '%s' AND gro_valid_after_field = 'gr2t_end_date' AND
gro_id_round = gro_valid_for_id AND
gro_valid_for_source = '%s' AND gro_valid_for_field = 'gto_valid_from'",
\Gems_Tracker_Engine_StepEngineAbstract::RESPONDENT_TRACK_TABLE,
\Gems_Tracker_Engine_StepEngineAbstract::TOKEN_TABLE
);
// In future we may want to add some nesting to this, e.g. tokens with an end date calculated
// from another token whose... for the time being users should use the end date directly in
// each token, otherwise the end date will not be calculated
$maxExpression = "
CASE
WHEN SUM(
CASE WHEN COALESCE(gto_completion_time, gto_valid_until) IS NULL THEN 1 ELSE 0 END
) > 0
THEN NULL
ELSE MAX(COALESCE(gto_completion_time, gto_valid_until))
END as enddate";
$tokenSelect = $this->tracker->getTokenSelect([new \Zend_Db_Expr($maxExpression)]);
$tokenSelect->andReceptionCodes([], false)
->andRounds([])
->forRespondentTrack($this->_respTrackId)
->onlySucces();
foreach ($excludeWheres as $where) {
$tokenSelect->forWhere('NOT (' . $where . ')');
}
$endDate = $tokenSelect->fetchOne();
// \MUtil_Echo::track($endDate, $tokenSelect->getSelect()->__toString());
if (false === $endDate) {
return null;
} else {
return $endDate;
}
}
/**
* Should be called after answering the request to allow the Target
* to check if all required registry values have been set correctly.
*
* @return boolean False if required are missing.
*/
public function checkRegistryRequestsAnswers()
{
$this->initDbTranslations();
if ($this->_respTrackData) {
$this->_respTrackData = $this->translateTables($this->_tablesForTranslations, $this->_respTrackData);
if ($this->currentUser instanceof \Gems_User_User) {
$this->_respTrackData = $this->currentUser->applyGroupMask($this->_respTrackData);
}
} else {
if ($this->db instanceof \Zend_Db_Adapter_Abstract) {
$this->refresh();
}
}
return (boolean) $this->_respTrackData;
}
/**
* Check this respondent track for changes to the tokens
*
* @param int $userId Id of the user who takes the action (for logging)
* @param \Gems_Tracker_Token $fromToken Optional token to start from
* @param \Gems_Tracker_Token $skipToken Optional token to skip in the recalculation when $fromToken is used
* @return int The number of tokens changed by this code
*/
public function checkTrackTokens($userId, \Gems_Tracker_Token $fromToken = null, \Gems_Tracker_Token $skipToken = null)
{
// Execute any defined functions
$count = $this->handleTrackCalculation($userId);
$engine = $this->getTrackEngine();
$this->db->beginTransaction();
// Check for validFrom and validUntil dates that have changed.
if ($fromToken) {
$count += $engine->checkTokensFrom($this, $fromToken, $userId, $skipToken);
} elseif ($this->_checkStart) {
$count += $engine->checkTokensFrom($this, $this->_checkStart, $userId);
} else {
$count += $engine->checkTokensFromStart($this, $userId);
}
$this->db->commit();
// Update token completion count and possible enddate
$this->_checkTrackCount($userId);
return $count;
}
/**
* Returns a token with a success reception code for this round or null
*
* @param int $roundId Gems round id
* @param \Gems_Tracker_Token $token Optional token to add as a round (for speed optimization)
* @return \Gems_Tracker_Token
*/
public function getActiveRoundToken($roundId, \Gems_Tracker_Token $token = null)
{
if ((null !== $token) && $token->hasSuccesCode()) {
// Cache the token
//
// WARNING: This may cause bugs for tracks where two tokens exists
// with this roundId and a success reception code, but this does speed
// this function with track engines where that should not occur.
$this->_activeTokens[$token->getRoundId()] = $token;
}
// Nothing to find
if (! $roundId) {
return null;
}
// Use array_key_exists since there may not be a valid round
if (! array_key_exists($roundId, $this->_activeTokens)) {
$tokenSelect = $this->tracker->getTokenSelect();
$tokenSelect->andReceptionCodes()
->forRespondentTrack($this->_respTrackId)
->forRound($roundId)
->onlySucces();
// \MUtil_Echo::track($tokenSelect->__toString());
if ($tokenData = $tokenSelect->fetchRow()) {
$this->_activeTokens[$roundId] = $this->tracker->getToken($tokenData);
} else {
$this->_activeTokens[$roundId] = null;
}
}
return $this->_activeTokens[$roundId];
}
/**
*
* @return string Internal code of the track
*/
public function getCode()
{
if (!isset($this->_respTrackData['gtr_code'])) {
$this->_ensureTrackData();
}
return $this->_respTrackData['gtr_code'];
}
/**
* Return all possible code fields with the values filled for those that exist for this track,
*
* @return array code => value
*/
public function getCodeFields()
{
$fieldDef = $this->getTrackEngine()->getFieldsDefinition();
$codes = $this->tracker->getAllCodeFields();
$results = array_fill_keys($codes, null);
$this->_ensureFieldData();
foreach ($this->_fieldData as $id => $value) {
if (!isset($codes[$id])) {
continue;
}
$fieldCode = $codes[$id];
$results[$fieldCode] = $value;
$field = $fieldDef->getFieldByCode($fieldCode);
if (!is_null($field)) {
$results[$fieldCode] = $field->calculateFieldInfo($value, $this->_fieldData);
}
}
return $results;
}
/**
*
* @return string Comment field
*/
public function getComment()
{
if (isset($this->_respTrackData['gr2t_comment'])) {
return $this->_respTrackData['gr2t_comment'];
}
return null;
}
/**
*
* @return int The number of rounds completed
*/
public function getCompleted()
{
if (isset($this->_respTrackData['gr2t_completed'])) {
return $this->_respTrackData['gr2t_completed'];
}
return 0;
}
/**
*
* @return int The number of rounds
*/
public function getCount()
{
if (isset($this->_respTrackData['gr2t_count'])) {
return $this->_respTrackData['gr2t_count'];
}
return 0;
}
/**
* The round description of the first round that has not been answered.
*
* @return string Round description or Stopped/Completed if not found.
*/
public function getCurrentRound()
{
$isStop = false;
$today = new \Zend_Date();
$tokens = $this->getTokens();
$stop = $this->util->getReceptionCodeLibrary()->getStopString();
foreach ($tokens as $token) {
$validUntil = $token->getValidUntil();
if (! empty($validUntil) && $validUntil->isEarlier($today)) {
continue;
}
if ($token->isCompleted()) {
continue;
}
$code = $token->getReceptionCode();
if (! $code->isSuccess()) {
if ($code->getCode() === $stop) {
$isStop = true;
}
continue;
}
return $token->getRoundDescription();
}
if ($isStop) {
return $this->translate->_('Track stopped');
}
return $this->translate->_('Track completed');
}
/**
*
* @param string $fieldName
* @return \MUtil_Date
*/
public function getDate($fieldName)
{
if (isset($this->_respTrackData[$fieldName])) {
$date = $this->_respTrackData[$fieldName];
} else {
$this->_ensureFieldData();
if (isset($this->_fieldData[$fieldName])) {
$date = $this->_fieldData[$fieldName];
if ($this->getTrackEngine()->isAppointmentField($fieldName)) {
$appointment = $this->tracker->getAppointment($date);
if ($appointment->isActive()) {
$date = $appointment->getAdmissionTime();
} else {
$date = false;
}
}
} else {
$date = false;
}
}
if ($date) {
return \MUtil_Date::ifDate($date, array(\Gems_Tracker::DB_DATETIME_FORMAT, \Gems_Tracker::DB_DATE_FORMAT));
}
}
/**
*
* @return array of snippet names for deleting the track
*/
public function getDeleteSnippets()
{
return $this->getTrackEngine()->getTrackDeleteSnippetNames($this);
}
/**
*
* @return array of snippet names for editing this respondent track
* @deprecated since version 1.7.1 Snippets defined TrackAction
*/
public function getEditSnippets()
{
return $this->getTrackEngine()->getTrackEditSnippetNames($this);
}
/**
* The end date of this track
*
* @return \MUtil_Date or null
*/
public function getEndDate()
{
if (isset($this->_respTrackData['gr2t_end_date'])) {
return new \MUtil_Date($this->_respTrackData['gr2t_end_date'], \Gems_Tracker::DB_DATETIME_FORMAT);
}
}
/**
*
* @return string Name of the track
*/
public function getExternalTrackName()
{
if (!isset($this->_respTrackData['gtr_track_name'])) {
$this->_ensureTrackData();
}
if (isset($this->_respTrackData['gtr_external_description']) && $this->_respTrackData['gtr_external_description']) {
return $this->_respTrackData['gtr_external_description'];
}
return $this->getTrackName();
}
/**
* Returns the field data for this respondent track id.
*
* The values of fields with a field code occur twice: once using the field
* id and once using the code name.
*
* @return array of the existing field values for this respondent track
*/
public function getFieldData()
{
$this->_ensureFieldData();
return $this->_fieldData;
}
/**
* Returns the description of this track as stored in the fields.
*
* @return string
*/
public function getFieldsInfo()
{
return $this->_respTrackData['gr2t_track_info'];
}
/**
* Returns the first token in this track
*
* @return \Gems_Tracker_Token
*/
public function getFirstToken()
{
if (! $this->_firstToken) {
if (! $this->_tokens) {
//No cache yet, but we might need all tokens later
$this->getTokens();
}
$this->_firstToken = reset($this->_tokens);
}
return $this->_firstToken;
}
/**
* Returns the first token in this track
*
* @return \Gems_Tracker_Token
*/
public function getLastToken()
{
if (! $this->_tokens) {
//No cache yet, but we might need all tokens later
$this->getTokens();
}
return end($this->_tokens);
}
/**
*
* @return string The respondents patient number
*/
public function getPatientNumber()
{
if (! isset($this->_respTrackData['gr2o_patient_nr'])) {
$this->_ensureRespondentData();
}
return $this->_respTrackData['gr2o_patient_nr'];
}
/**
*
* @return int The organization id
*/
public function getOrganizationId()
{
return $this->_respTrackData['gr2t_id_organization'];
}
/**
* Return the \Gems_Util_ReceptionCode object
*
* @return \Gems_Util_ReceptionCode reception code
*/
public function getReceptionCode()
{
return $this->util->getReceptionCode($this->_respTrackData['gr2t_reception_code']);
}
/**
* Get the respondent linked to this token
*
* @return \Gems_Tracker_Respondent
*/
public function getRespondent()
{
$patientNumber = $this->getPatientNumber();
$organizationId = $this->getOrganizationId();
if (! ($this->_respondentObject instanceof \Gems_Tracker_Respondent)
|| $this->_respondentObject->getPatientNumber() !== $patientNumber
|| $this->_respondentObject->getOrganizationId() !== $organizationId) {
$this->_respondentObject = $this->loader->getRespondent($patientNumber, $organizationId);
}
return $this->_respondentObject;
}
/**
*
* @return int The respondent id
*/
public function getRespondentId()
{
return $this->_respTrackData['gr2t_id_user'];
}
/**
* Return the default language for the respondent
*
* @return string Two letter language code
*/
public function getRespondentLanguage()
{
if (! isset($this->_respTrackData['grs_iso_lang'])) {
$this->_ensureRespondentData();
if (! isset($this->_respTrackData['grs_iso_lang'])) {
// Still not set in a project? The it is single language
$this->_respTrackData['grs_iso_lang'] = $this->locale->getLanguage();
}
}
return $this->_respTrackData['grs_iso_lang'];
}
/**
* Return the name of the respondent
*
* @return string The respondents name
*/
public function getRespondentName()
{
if (! isset($this->_respTrackData['grs_first_name'], $this->_respTrackData['grs_last_name'])) {
$this->_ensureRespondentData();
}
return trim($this->_respTrackData['grs_first_name'] . ' ' . $this->_respTrackData['grs_surname_prefix']) . ' ' . $this->_respTrackData['grs_last_name'];
}
/**
*
* @return int The respondent2track id
*/
public function getRespondentTrackId()
{
return $this->_respTrackId;
}
/**
* Return the appointment (if any) linked to the valid after setting of given roundId
*
* @param int $roundId
* @return int | null | false False when RoundId not found or not an appointment otherwise appointment id or null when not set
*/
public function getRoundAfterAppointmentId($roundId)
{
$this->_ensureFieldData();
$this->_ensureRounds();
if (isset($this->_rounds[$roundId])) {
$round = $this->_rounds[$roundId];
if (isset($round['gro_valid_after_source'], $round['gro_valid_after_field']) &&
('app' === $round['gro_valid_after_source'])) {
if (isset($this->_fieldData[$round['gro_valid_after_field']])) {
return $this->_fieldData[$round['gro_valid_after_field']];
} else {
return null;
}
}
}
return false;
}
/**
* Return the round code for a given roundId
*
* @param int $roundId
* @return string|null Null when RoundId not found
*/
public function getRoundCode($roundId)
{
$this->_ensureRounds();
$roundCode = null;
if (array_key_exists($roundId, $this->_rounds) && array_key_exists('gro_code', $this->_rounds[$roundId])) {
$roundCode = $this->_rounds[$roundId]['gro_code'];
}
return $roundCode;
}
/**
* The start date of this track
*
* @return \MUtil_Date
*/
public function getStartDate()
{
if (isset($this->_respTrackData['gr2t_start_date'])) {
return new \MUtil_Date($this->_respTrackData['gr2t_start_date'], \Gems_Tracker::DB_DATETIME_FORMAT);
}
}
/**
* Returns all the tokens in this track
*
* @param boolean $refresh When true, always reload
* @return \Gems_Tracker_Token[]
*/
public function getTokens($refresh = false)
{
if (! $this->_tokens || $refresh) {
if ($refresh) {
$this->_firstToken = null;
}
$this->_tokens = array();
$this->_activeTokens = array();
$tokenSelect = $this->tracker->getTokenSelect();
$tokenSelect->andReceptionCodes()
->forRespondentTrack($this->_respTrackId);
$tokenRows = $tokenSelect->fetchAll();
$prevToken = null;
foreach ($tokenRows as $tokenData) {
$token = $this->tracker->getToken($tokenData);
$this->_tokens[$token->getTokenId()] = $token;
// While we are busy, set this
if ($token->hasSuccesCode()) {
$this->_activeTokens[$token->getRoundId()] = $token;
}
// Link the tokens
if ($prevToken) {
$prevToken->setNextToken($token);
}
$prevToken = $token;
}
}
return $this->_tokens;
}
/**
*
* @return string Check if track is active
*/
public function getTrackActive()
{
if (!isset($this->_respTrackData['gtr_active'])) {
$this->_ensureTrackData();
}
return (bool)$this->_respTrackData['gtr_active'];
}
/**
*
* @return \Gems_Tracker_Engine_TrackEngineInterface
*/
public function getTrackEngine()
{
return $this->tracker->getTrackEngine($this->_respTrackData['gr2t_id_track']);
}
/**
*
* @return int The track id
*/
public function getTrackId()
{
return $this->_respTrackData['gr2t_id_track'];
}
/**
*
* @return string Name of the track
*/
public function getTrackName()
{
if (!isset($this->_respTrackData['gtr_track_name'])) {
$this->_ensureTrackData();
}
return $this->_respTrackData['gtr_track_name'];
}
/**
* Find out if there are before field update events and delegate to the event if needed
*
* @param array $fieldData fieldname => value + codename => value
* @return array Of changed fields. Codename using items overwrite any key using items
*/
public function handleBeforeFieldUpdate(array $fieldData)
{
static $running = array();
// Process any events
$trackEngine = $this->getTrackEngine();
if (! $trackEngine) {
return array();
}
$beforeFieldUpdateEvent = $trackEngine->getFieldBeforeUpdateEvent();
$eventName = 'gems.track.before-field-update';
if (! $beforeFieldUpdateEvent && !$this->event->hasListeners($eventName)) {
return [];
}
if (isset($running[$this->_respTrackId])) {
throw new \Gems_Exception(sprintf(
"Nested calls to '%s' track before field update event are not allowed.",
$trackEngine->getName()
));
}
$running[$this->_respTrackId] = true;
if ($beforeFieldUpdateEvent) {
$eventFunction = function (RespondentTrackFieldEvent $event) use ($beforeFieldUpdateEvent) {
$respondentTrack = $event->getRespondentTrack();
$fieldData = $event->getFieldData();
try {
$changed = $beforeFieldUpdateEvent->prepareFieldUpdate($fieldData, $respondentTrack);
$event->addChanged($changed);
$fieldData = $changed + $fieldData;
$event->setFieldData($fieldData);
} catch (\Exception $e) {
throw new \Exception('Event: ' . $beforeFieldUpdateEvent->getEventName() . '. ' . $e->getMessage());
}
};
$this->event->addListener($eventName, $eventFunction, 100);
}
$respondentTrackFieldEvent = new RespondentTrackFieldEvent($this, $this->currentUser->getUserId());
$respondentTrackFieldEvent->setFieldData($fieldData);
$this->event->dispatch($respondentTrackFieldEvent, $eventName);
unset($running[$this->_respTrackId]);
return $respondentTrackFieldEvent->getChanged();
}
/**
* Find out if there are field update events and delegate to the event if needed
*
* @param array $fieldData Optional field data to use instead of data currently stored at object
* @return void
*/
public function handleFieldUpdate(array $oldFieldData = null)
{
static $running = array();
// Process any events
$trackEngine = $this->getTrackEngine();
if (! $trackEngine) {
return;
}
$fieldUpdateEvent = $trackEngine->getFieldUpdateEvent();
$eventName = 'gems.track.field-update';
if (! $fieldUpdateEvent && !$this->event->hasListeners($eventName)) {
return;
}
if (isset($running[$this->_respTrackId])) {
throw new \Gems_Exception(sprintf(
"Nested calls to '%s' track after field update event are not allowed.",
$trackEngine->getName()
));
}
$running[$this->_respTrackId] = true;
if ($fieldUpdateEvent) {
$eventFunction = function (RespondentTrackFieldUpdateEvent $event) use ($fieldUpdateEvent) {
$respondentTrack = $event->getRespondentTrack();
$userId = $event->getUserId();
try {
$fieldUpdateEvent->processFieldUpdate($respondentTrack, $userId);
} catch (\Exception $e) {
throw new \Exception('Event: ' . $fieldUpdateEvent->getEventName() . '. ' . $e->getMessage());
}
};
$this->event->addListener($eventName, $eventFunction, 100);
}
$respondentTrackEvent = new RespondentTrackFieldUpdateEvent($this, $this->currentUser->getUserId(), $oldFieldData);
$this->event->dispatch($respondentTrackEvent, $eventName);
unset($running[$this->_respTrackId]);
}
/**
*
* @param mixed $token
* @param int $userId The current user
* @return int The number of tokens changed by this event
*/
public function handleRoundCompletion($token, $userId)
{
if (! $token instanceof \Gems_Tracker_Token) {
$token = $this->tracker->getToken($token);
}
// \MUtil_Echo::track($token->getRawAnswers());
// Store the current token as startpoint if it is the first changed token
if ($this->_checkStart) {
if ($this->_checkStart->getRoundId() > $token->getRoundId()) {
// Replace current token
$this->_checkStart = $token;
}
} else {
$this->_checkStart = $token;
}
$eventName = 'gems.round.changed';
// Process any events
if ($roundChangedEvent = $this->getTrackEngine()->getRoundChangedEvent($token->getRoundId())) {
$eventFunction = function (TokenEvent $event) use ($roundChangedEvent, $userId) {
$token = $event->getToken();
$respondentTrack = $token->getRespondentTrack();
try {
$changed = $roundChangedEvent->processChangedRound($token, $respondentTrack, $userId);
if (is_array($changed)) {
$event->addChanged($changed);
}
} catch (\Exception $e) {
throw new \Exception('Event: ' . $roundChangedEvent->getEventName() . '. ' . $e->getMessage());
}
};
$this->event->addListener($eventName, $eventFunction, 100);
}
$tokenEvent = new TokenEvent($token);
try {
$this->event->dispatch($tokenEvent, $eventName);
} catch (\Exception $e) {
$this->logger->log(sprintf(
"Round changed after event error for token %s on survey '%s': %s",
$token->getTokenId(),
$token->getSurveyName(),
$e->getMessage()
), \Zend_Log::ERR);
}
return 0;
}
/**
* Find out if there are track calculation events and delegate to the event if needed
*
* @param int $userId
*/
public function handleTrackCalculation($userId)
{
// Process any events
$trackEngine = $this->getTrackEngine();
// Places here instead of only in handle field update so it will run on new tracks too
$this->assignTokensToRelations();
if ($event = $trackEngine->getTrackCalculationEvent()) {
return $event->processTrackCalculation($this, $userId);
}
return 0;
}
/**
* Find out if there are track completion events and delegate to the event if needed
*
* @param array $values The values changed before entering this event
* @param int $userId
*/
public function handleTrackCompletion(&$values, $userId)
{
// Process any events
$trackEngine = $this->getTrackEngine();
if ($event = $trackEngine->getTrackCompletionEvent()) {
$event->processTrackCompletion($this, $values, $userId);
}
}
/**
*
* @return boolean
*/
public function hasSuccesCode()
{
return $this->getReceptionCode()->isSuccess();
}
/**
* Are there still unanswered rounds
*
* @return boolean
*/
public function isOpen()
{
if (isset($this->_respTrackData['gr2t_count'], $this->_respTrackData['gr2t_completed'])) {
return $this->_respTrackData['gr2t_count'] > $this->_respTrackData['gr2t_completed'];
}
return true;
}
/**
* Can mails be sent for this track?
*
* Cascades to the respondent mailable setting too
*
* @return boolean
*/
public function isMailable()
{
if (!array_key_exists('gr2t_mailable', $this->_respTrackData)) {
$this->refresh();
}
$mailCode = $this->util->getDbLookup()->getRespondentTrackNoMailCodeValue();
return $this->_respTrackData['gr2t_mailable'] > $mailCode && $this->getRespondent()->isMailable();
}
/**
* Processes the field values and and changes them as required
*
* @param array $newFieldData The new field values, may be partial, field set by code overwrite field set by key
* @return array The processed data in the format key1 => val1, code1 => val1, key2 => val2
*/
public function processFieldsBeforeSave(array $newFieldData)
{
$trackEngine = $this->getTrackEngine();
if (! $trackEngine) {
return $newFieldData;
}
// \MUtil_Echo::track($newFieldData);
$step1Data = $this->_mergeFieldValues($newFieldData, $this->getFieldData(), $trackEngine);
$step2Data = $trackEngine->getFieldsDefinition()->processBeforeSave($step1Data, $this->_respTrackData);
$step3Data = $this->handleBeforeFieldUpdate($this->_mergeFieldValues($step2Data, $step1Data, $trackEngine));
if ($step3Data) {
return $this->_mergeFieldValues($step3Data, $step2Data, $trackEngine);
} else {
return $step2Data;
}
}
/**
* Refresh the fields (to reflect any changed appointments)
*
* @param boolean $trackEngine Set to true when changed
* @return int The number of tokens changed as a result of this update
*/
public function recalculateFields(&$fieldsChanged = false)
{
$fieldDef = $this->getTrackEngine()->getFieldsDefinition();
$this->_ensureFieldData();
$oldFieldData = $this->_fieldData;
$this->_fieldData = $this->processFieldsBeforeSave($this->_fieldData);
$fieldsChanged = $fieldDef->changed;
$changes = $fieldDef->saveFields($this->_respTrackId, $this->_fieldData);
$fieldsChanged = (boolean) $changes;
$this->handleFieldUpdate($oldFieldData);
$info = $fieldDef->calculateFieldsInfo($this->_fieldData);
if ($info != $this->_respTrackData['gr2t_track_info']) {
$this->_updateTrack(array('gr2t_track_info' => $info), $this->currentUser->getUserId());
}
// We always update the fields, but recalculate the token dates
// only when this respondent track is still running.
if ($this->hasSuccesCode() && $this->isOpen()) {
return $this->checkTrackTokens($this->currentUser->getUserId());
}
}
/**
*
* @param array $gemsData Optional, the data refresh with, otherwise refresh from database.
* @return \Gems_Tracker_RespondentTrack (continuation pattern)
*/
public function refresh(array $gemsData = null)
{
if (is_array($gemsData)) {
$this->_respTrackData = $this->translateTables($this->_tablesForTranslations, $gemsData) + $this->_respTrackData;
} else {
$this->_respTrackData = $this->fetchTranslatedRow('gems__respondent2track', 'gr2t_id_respondent_track', $this->_respTrackId);
}
if ($this->_respTrackData && $this->currentUser instanceof \Gems_User_User) {
$this->_respTrackData = $this->currentUser->applyGroupMask($this->_respTrackData);
}
$this->_ensureFieldData(true);
$this->_rounds = null;
$this->_tokens = null;
return $this;
}
/**
* Restores tokens for this track, when the reception code matches the given $oldCode
*
* Used when restoring a respondent or this tracks, and the restore tracks/tokens
* box is checked.
*
* @param \Gems_Util_ReceptionCode $oldCode The old reception code
* @param \Gems_Util_ReceptionCode $newCode the new reception code
* @return int The number of restored tokens
*/
public function restoreTokens(\Gems_Util_ReceptionCode $oldCode, \Gems_Util_ReceptionCode $newCode) {
$count = 0;
if (!$oldCode->isSuccess() && $newCode->isSuccess()) {
foreach ($this->getTokens() as $token) {
if ($token instanceof \Gems_Tracker_Token) {
if ($oldCode->getCode() === $token->getReceptionCode()->getCode()) {
$token->setReceptionCode($newCode, null, $this->currentUser->getUserId());
$count++;
}
}
}
}
return $count;
}
/**
* Saves the field data for the respondent track id.
*
* @param array $fieldData The values to save, only the key is used, not the code
* @return int The number of changed fields
*/
public function saveFields(array $fieldData)
{
$trackEngine = $this->getTrackEngine();
if (! $trackEngine) {
return 0;
}
//\MUtil_Echo::track($fieldData);
$oldFieldData = $this->getFieldData();
$this->_fieldData = $this->_mergeFieldValues($fieldData, $oldFieldData, $trackEngine);
$changed = $trackEngine->getFieldsDefinition()->saveFields($this->_respTrackId, $this->_fieldData);
if ($changed) {
$this->_ensureFieldData(true);
}
$this->handleFieldUpdate($oldFieldData);
return $changed;
}
/**
* Set the end date for this respondent track.
*
* @param mixed $endDate The new end date for this track
* @param int $userId The current user
* @return int 1 if the token has changed, 0 otherwise
*/
public function setEndDate($endDate, $userId)
{
$values['gr2t_end_date'] = $endDate;
$values['gr2t_end_date_manual'] = 1;
return $this->_updateTrack($values, $userId);
}
/**
* Update one or more values for this track's fields.
*
* Return the complete set of fielddata
*
* @param array $newFieldData The new field values, may be partial, field set by code overwrite field set by key
* @return array
*/
public function setFieldData($newFieldData)
{
$trackEngine = $this->getTrackEngine();
if (! $trackEngine) {
return $newFieldData;
}
$this->_fieldData = $this->processFieldsBeforeSave($newFieldData);
$changes = $this->saveFields(array());
if ($changes) {
$info = $trackEngine->getFieldsDefinition()->calculateFieldsInfo($this->_fieldData);
if ($info != $this->_respTrackData['gr2t_track_info']) {
$this->_updateTrack(array('gr2t_track_info' => $info), $this->currentUser->getUserId());
}
}
return $this->_fieldData;
}
/**
* Set the mailability for this respondent track.
*
* @param boolean $mailable Should this respondent track be st to mailable
* @param int $userId The current user
* @return int 1 if the token has changed, 0 otherwise
*/
public function setMailable($mailable)
{
$mailCodes = array_keys($this->util->getDbLookup()->getRespondentTrackMailCodes());
$values['gr2t_mailable'] = $mailable ? max($mailCodes) : min($mailCodes);
return $this->_updateTrack($values, $this->currentUser->getUserId());
}
/**
* Set the reception code for this respondent track and make sure the
* necessary cascade to the tokens and thus the source takes place.
*
* @param string $code The new (non-success) reception code or a \Gems_Util_ReceptionCode object
* @param string $comment Comment for tokens. False values leave value unchanged
* @param int $userId The current user
* @return int 1 if the token has changed, 0 otherwise
*/
public function setReceptionCode($code, $comment, $userId)
{
// Make sure it is a \Gems_Util_ReceptionCode object
if (! $code instanceof \Gems_Util_ReceptionCode) {
$code = $this->util->getReceptionCode($code);
}
$changed = 0;
// Apply this code both only when it is a track code.
// Patient level codes are just cascaded to the tokens.
//
// The exception is of course when the exiting values must
// be overwritten, e.g. when cooperation is retracted.
if ($code->isForTracks() || $code->isOverwriter()) {
$values['gr2t_reception_code'] = $code->getCode();
}
$values['gr2t_comment'] = $comment;
$changed = $this->_updateTrack($values, $userId);
// Stopcodes have a different logic.
if ($code->isStopCode()) {
// Cascade stop to unanswered tokens
foreach ($this->getTokens() as $token) {
if ($token->getReceptionCode()->isSuccess() && (! $token->isCompleted())) {
$changed += $token->setReceptionCode($code, $comment, $userId);
}
}
$changed = max($changed, 1);
// Update token count / completion
$this->_checkTrackCount($userId);
} elseif (! $code->isSuccess()) {
// Cascade code to tokens
foreach ($this->getTokens() as $token) {
if ($token->getReceptionCode()->isSuccess()) {
$token->setReceptionCode($code, $comment, $userId);
}
}
}
return $changed;
}
}