GemsTracker/gemstracker-library

View on GitHub
classes/Gems/Tracker/Token.php

Summary

Maintainability
F
1 wk
Test Coverage
F
39%
<?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 MUtil\Translate\TranslateableTrait;


/**
 * Object class for checking and changing tokens.
 *
 * @package    Gems
 * @subpackage Tracker
 * @copyright  Copyright (c) 2011 Erasmus MC
 * @license    New BSD License
 * @since      Class available since version 1.4
 */
class Gems_Tracker_Token extends \Gems_Registry_TargetAbstract
{
    use TranslateableTrait;

    const COMPLETION_NOCHANGE = 0;
    const COMPLETION_DATACHANGE = 1;
    const COMPLETION_EVENTCHANGE = 2;

    /**
     *
     * @var array Can hold any data the source likes to store for the token
     */
    private $_cache = array();

    /**
     *
     * @var string The token id of the token this one was copied from, null when not loaded, false when does not exist
     */
    protected $_copiedFromTokenId = null;

    /**
     *
     * @var array The token id's of the tokens this one was copied to, null when not loaded, [] when none exist
     */
    protected $_copiedToTokenIds = null;

    /**
     *
     * @var array The gems token data
     */
    protected $_gemsData = array();

    /**
     * Helper var for preventing infinite loops
     *
     * @var boolean
     */
    protected $_loopCheck = false;

    /**
     *
     * @var \Gems_Tracker_Token
     */
    private $_nextToken = null;

    /**
     *
     * @var \Gems_Tracker_Token
     */
    private $_previousToken = null;

    /**
     * Holds the relation (if any) for this token
     *
     * @var array
     */
    protected $_relation = null;

    /**
     *
     * @var \Gems_Tracker_Respondent
     */
    protected $_respondentObject = null;

    /**
     *
     * @var array The answers in raw format
     */
    private $_sourceDataRaw;

    /**
     *
     * @var string The id of the token
     */
    protected $_tokenId;

    /**
     *
     * @var \Gems_User_User
     */
    protected $currentUser;

    /**
     *
     * @var \Zend_Db_Adapter_Abstract
     */
    protected $db;

    /**
     * @var \Gems\Event\EventDispatcher
     */
    protected $event;

    /**
     * True when the token does exist.
     *
     * @var boolean
     */
    public $exists = true;

    /**
     *
     * @var \Gems_Loader
     */
    protected $loader;

    /**
     *
     * @var \Zend_Locale
     */
    protected $locale;

    /**
     * Logger instance
     *
     * @var \Gems_Log
     */
    protected $logger;

    /**
     *
     * @var \Gems_Project_ProjectSettings
     */
    protected $project;

    /**
     *
     * @var \Gems_Tracker_RespondentTrack
     */
    protected $respTrack;

    /**
     * The size of the result field, calculated from meta data when null,
     * but can be set by project specific class to fixed value
     *
     * @var int The maximum character length of the result field
     */
    protected $resultFieldLength = null;

    /**
     * Cache for storing the calculation of the length
     *
     * @var int the character length of the result field
     */
    protected static $staticResultFieldLength = null;

    /**
     *
     * @var \Gems_Tracker_Survey
     */
    protected $survey;

    /**
     *
     * @var \Gems_Tracker
     */
    protected $tracker;

    /**
     *
     * @var \Gems_Util
     */
    protected $util;

    /**
     * Creates the token object
     *
     * @param mixed $gemsTokenData Token Id or array containing token record
     */
    public function __construct($gemsTokenData)
    {
        if (is_array($gemsTokenData)) {
            $this->_gemsData = $gemsTokenData;
            $this->_tokenId  = $gemsTokenData['gto_id_token'];
        } else {
            $this->_tokenId  = $gemsTokenData;
            // loading occurs in checkRegistryRequestAnswers
        }
    }

    /**
     * Add relation to the select statement
     *
     * @param Gems_Tracker_Token_TokenSelect $select
     */
    protected function _addRelation($select)
    {
        if (!is_null($this->_gemsData['gto_id_relation'])) {
            $select->forWhere('gto_id_relation = ?', $this->_gemsData['gto_id_relation']);
        } else {
            $select->forWhere('gto_id_relation IS NULL');
        }
    }

    /**
     * Makes sure the respondent data is part of the $this->_gemsData
     */
    protected function _ensureRespondentData()
    {
        if (! isset($this->_gemsData['grs_id_user'], $this->_gemsData['gr2o_id_user'], $this->_gemsData['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->_gemsData['gto_id_respondent'];
            $orgId  = $this->_gemsData['gto_id_organization'];
            // \MUtil_Echo::track($this->_gemsData);

            if ($row = $this->db->fetchRow($sql, array($respId, $orgId))) {
                $this->_gemsData = $this->_gemsData + $row;
            } else {
                $token = $this->_tokenId;
                throw new \Gems_Exception("Respondent data missing for token $token.");
            }
        }
    }

    /**
     * The maximum length of the result field
     *
     * @return int
     */
    protected function _getResultFieldLength()
    {
        if (null !== $this->resultFieldLength) {
            return $this->resultFieldLength;
        }

        if (null !== self::$staticResultFieldLength) {
            $this->resultFieldLength = self::$staticResultFieldLength;
            return $this->resultFieldLength;
        }

        $model = new \MUtil_Model_TableModel('gems__tokens');
        self::$staticResultFieldLength = $model->get('gto_result', 'maxlength');
        $this->resultFieldLength = self::$staticResultFieldLength;

        return $this->resultFieldLength;
    }

    /**
     * Update the token, both in the database and in memory.
     *
     * @param array $values The values that this token should be set to
     * @param int $userId The current user
     * @return int 1 if data changed, 0 otherwise
     */
    protected function _updateToken(array $values, $userId)
    {
        if (!$this->tracker->filterChangesOnly($this->_gemsData, $values)) {
            return 0;   // No changes
        }

        if (\Gems_Tracker::$verbose) {
            $echo = '';
            foreach ($values as $key => $val) {
                $echo .= $key . ': ' . $this->_gemsData[$key] . ' => ' . $val . "\n";
            }
            \MUtil_Echo::r($echo, 'Updated values for ' . $this->_tokenId);
        }

        $defaults = [
            'gto_changed'    => new \MUtil_Db_Expr_CurrentTimestamp(),
            'gto_changed_by' => $userId
        ];

        // Update values in this object
        $this->_gemsData = $values + $defaults + (array) $this->_gemsData;

        // return 1;
        return $this->db->update('gems__tokens', $values, array('gto_id_token = ?' => $this->_tokenId));
    }

    /**
     * Called after the check that all required registry values
     * have been set correctly has run.
     *
     * This function is no needed if the classes are setup correctly
     *
     * @return void
     */
    public function afterRegistry()
    {
        parent::afterRegistry();

        $this->initTranslateable();
    }

    /**
     * Set menu parameters from this token
     *
     * @param \Gems_Menu_ParameterSource $source
     * @return \Gems_Tracker_Token (continuation pattern)
     */
    public function applyToMenuSource(\Gems_Menu_ParameterSource $source)
    {
        $source->setTokenId($this->_tokenId);
        if (!$this->exists) return $this;

        if (! isset($this->_gemsData['gr2o_patient_nr'])) {
            $this->_ensureRespondentData();
        }
        $this->getRespondentTrack()->applyToMenuSource($source);

        $completed = $this->_gemsData['gto_completion_time'] ? 1 : 0;
        $source->offsetSet('gsu_id_survey', $this->_gemsData['gto_id_survey']);
        $source->offsetSet('is_completed', $completed);
        $source->offsetSet('show_answers', $completed);
        $source->offsetSet('gto_in_source', $this->_gemsData['gto_in_source']);
        $source->offsetSet('gto_reception_code', $this->_gemsData['gto_reception_code']);

        $receptionCode = $this->getReceptionCode();
        $source->offsetSet('grc_success', $receptionCode->isSuccess() ? 1 : 0);
        $canBeTaken = false;
        if ($receptionCode->isSuccess() &&
                ($completed == 0) &&
                ($validFrom = $this->getValidFrom())) {

            $validUntil = $this->getValidUntil();
            $today = new \MUtil_Date();

            $canBeTaken = $validFrom->isEarlier($today) && ($validUntil ? $validUntil->isLater($today) : true);
        }
        $source->offsetSet('can_be_taken', $canBeTaken);

        return $this;
    }

    /**
     * Assign this token to a specific relation
     *
     * @param int $respondentRelationId
     * @param int $relationFieldId
     * @return int 1 if data changed, 0 otherwise
     */
    public function assignTo($respondentRelationId, $relationFieldId)
    {
        if (($this->getRelationFieldId() == $relationFieldId) && ($this->getRelationId() == $respondentRelationId)) {
            return 0;
        }

        return $this->_updateToken([
            'gto_id_relation'      => $respondentRelationId,
            'gto_id_relationfield' => $relationFieldId,
            ], $this->currentUser->getUserId());
    }

    /**
     * Retrieve a certain $key from the local cache
     *
     * For speeding up things the token can hold a local cache, living as long as the
     * token object exists in memory. Sources can use this to store reusable information.
     *
     * To reset the cache on an update, the source can use the cacheReset method or the
     * setCache method to update the changed value.
     *
     * @param string $key             The key used in the cache
     * @param mixed  $defaultValue    The optional default value to use when it is not present
     * @return mixed
     */
    public function cacheGet($key, $defaultValue = null) {
        if ($this->cacheHas($key)) {
            return $this->_cache[$key];
        } else {
            return $defaultValue;
        }
    }

    /**
     * find out if a certain key is present in the cache
     *
     * @param string $key
     * @return boolean
     */
    public function cacheHas($key) {
        return isset($this->_cache[$key]);

    }

    /**
     * Reset the local cache for this token
     *
     * You can pass in an optional $key parameter to reset just that key, otherwise all
     * the cache will be reset
     *
     * @param string|null $key The key to reset
     */
    public function cacheReset($key = null) {
        if (is_null($key)) {
            $this->_cache = array();
        } else {
            unset($this->_cache[$key]);
        }
    }

    /**
     * Set a $key in the local cache
     *
     * @param string $key
     * @param mixed  $value
     */
    public function cacheSet($key, $value) {
        $this->_cache[$key] = $value;
    }

    /**
     * Returns the full url Gems should forward to after survey completion.
     *
     * This fix allows multiple sites with multiple url's to share a single
     * installation.
     *
     * @return string
     */
    protected function calculateReturnUrl()
    {
        if ($this->currentUser->isActive(false)) {
            $currentUri = $this->util->getCurrentURI();
        } else {
            $currentUri = $this->getOrganization()->getPreferredSiteUrl();
        }

        /*
        // Referrer would be powerful when someone is usng multiple windows, but
        // the loop does not always provide a correct referrer.
        $referrer   = $_SERVER["HTTP_REFERER"];

        // If a referrer was specified and that referral is from the current site, then use it
        // as it is more dependable when the user has multiple windows open on the application.
        if ($referrer && (0 == strncasecmp($referrer, $currentUri, strlen($currentUri)))) {
            return $referrer;
            // \MUtil_Echo::track($referrer);
        } // */

        // Use the survey return if available.
        $surveyReturn = $this->currentUser->getSurveyReturn();
        if ($surveyReturn) {
            // Do not show the base url as it is in $currentUri
            $surveyReturn['NoBase'] = true;

            // Add route reset to prevet the current parameters to be
            // added to the url.
            $surveyReturn['RouteReset'] = true;

            // \MUtil_Echo::track($currentUri, \MUtil_Html::urlString($surveyReturn));
            return $currentUri . \MUtil_Html::urlString($surveyReturn);
        }

        // Ultimate backup solution for return
        return $currentUri . '/ask/forward/' . \MUtil_Model::REQUEST_ID . '/' . urlencode($this->getTokenId());
    }

    /**
     * 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()
    {
        if ($this->_gemsData) {
            if ($this->currentUser instanceof \Gems_User_User) {
                $this->_gemsData = $this->currentUser->applyGroupMask($this->_gemsData);
            }
        } else {
            if (!$this->db instanceof \Zend_Db_Adapter_Abstract) {
                return false;
            }
            $this->refresh();
        }

        return $this->exists;
    }

    /**
     * Checks whether the survey for this token was completed and processes the result
     *
     * @param int $userId The id of the gems user
     * @return int self::COMPLETION_NOCHANGE || (self::COMPLETION_DATACHANGE | self::COMPLETION_EVENTCHANGE)
     */
    public function checkTokenCompletion($userId)
    {
        $result = self::COMPLETION_NOCHANGE;

        // Some defaults
        $values['gto_completion_time'] = null;
        $values['gto_duration_in_sec'] = null;
        $values['gto_result']          = null;

        if ($this->inSource()) {
            $values['gto_in_source'] = 1;
            $survey                  = $this->getSurvey();
            $startTime               = $survey->getStartTime($this);
            if ($startTime instanceof \MUtil_Date) {
                // Value from source overrules any set date time
                $values['gto_start_time'] = $startTime->toString(\Gems_Tracker::DB_DATETIME_FORMAT);

            } else {
                // Otherwise use the time kept by Gems.
                $startTime = $this->getDateTime('gto_start_time');

                //What if we have older tokens... where no gto_start_time was set??
                if (is_null($startTime)) {
                    $startTime = new \MUtil_Date();
                }

                // No need to set $values['gto_start_time'], it does not change
            }

            if ($survey->isCompleted($this)) {
                $complTime         = $survey->getCompletionTime($this);
                $setCompletionTime = true;

                if (! $complTime instanceof \MUtil_Date) {
                    // Token is completed but the source cannot tell the time
                    //
                    // Try to see it was stored already
                    $complTime = $this->getDateTime('gto_completion_time');

                    if ($complTime instanceof \MUtil_Date) {
                        // Again no need to change a time that did not change
                        unset($values['gto_completion_time']);
                        $setCompletionTime = false;
                    } else {
                        // Well anyhow it was completed now or earlier. Get the current moment.
                        $complTime = new \MUtil_Date();
                    }
                }

                //Save the old completiontime
                $oldCompletionTime = $this->_gemsData['gto_completion_time'];
                //Set completion time for completion event
                if ($setCompletionTime) {
                    $values['gto_completion_time']          = $complTime->toString(\Gems_Tracker::DB_DATETIME_FORMAT);
                    $this->_gemsData['gto_completion_time'] = $values['gto_completion_time'];
                }

                // Process any Gems side survey dependent changes
                if ($changed = $this->handleAfterCompletion()) {
                    // Communicate change
                    $result += self::COMPLETION_EVENTCHANGE;

                    if (\Gems_Tracker::$verbose) {
                        \MUtil_Echo::r($changed, 'Source values for ' . $this->_tokenId . ' changed by event.');
                    }
                }

                //Reset completiontime to old value, so changes will be picked up
                $this->_gemsData['gto_completion_time'] = $oldCompletionTime;
                $values['gto_duration_in_sec']          = max($complTime->diffSeconds($startTime), 0);

                //If the survey has a resultfield, store it
                if ($resultField = $survey->getResultField()) {
                    $rawAnswers = $this->getRawAnswers();
                    if (isset($rawAnswers[$resultField])) {
                        // Cast to string, because that is the way the result is stored in the db
                        // not casting to strings means e.g. float results always result in
                        // an update, even when they did not change.
                        $values['gto_result'] = (string) $rawAnswers[$resultField];

                        // Chunk of text that is too long
                        if ($len = $this->_getResultFieldLength()) {
                            $values['gto_result'] = substr($values['gto_result'], 0, $len);
                        }
                    }
                }

                if ($this->project->hasResponseDatabase()) {
                    $this->toResponseDatabase($userId);
                }
            }
        } else {
            $values['gto_in_source']  = 0;
            $values['gto_start_time'] = null;
        }

        if ($this->_updateToken($values, $userId)) {
            // Communicate change
            $result += self::COMPLETION_DATACHANGE;
        }

        return $result;
    }

    /**
     * Creates an almost exact copy of this token at the same place in the track,
     * only without answers and other source data
     *
     * Returns the new token id
     *
     * @param string $newComment Description of why the token was replaced
     * @param int $userId The current user
     * @param array $otherValues Other values to set in the token
     * @return string The new token
     */
    public function createReplacement($newComment, $userId, array $otherValues = array())
    {
        $values['gto_id_respondent_track'] = $this->_gemsData['gto_id_respondent_track'];
        $values['gto_id_round']            = $this->_gemsData['gto_id_round'];
        $values['gto_id_respondent']       = $this->_gemsData['gto_id_respondent'];
        $values['gto_id_organization']     = $this->_gemsData['gto_id_organization'];
        $values['gto_id_track']            = $this->_gemsData['gto_id_track'];
        $values['gto_id_survey']           = $this->_gemsData['gto_id_survey'];
        $values['gto_round_order']         = $this->_gemsData['gto_round_order'];
        $values['gto_round_description']   = $this->_gemsData['gto_round_description'];
        $values['gto_valid_from']          = $this->_gemsData['gto_valid_from'];
        $values['gto_valid_from_manual']   = $this->_gemsData['gto_valid_from_manual'];
        $values['gto_valid_until']         = $this->_gemsData['gto_valid_until'];
        $values['gto_valid_until_manual']  = $this->_gemsData['gto_valid_until_manual'];
        $values['gto_mail_sent_date']      = $this->_gemsData['gto_mail_sent_date'];
        $values['gto_comment']             = $newComment;

        $newValues = $otherValues + $values;
        // Now make sure there are no more date objects
        foreach($newValues as &$value)
        {
            if ($value instanceof \Zend_Date) {
                $value = $value->toString(\Gems_Tracker::DB_DATETIME_FORMAT);
            }
        }

        $tokenId = $this->tracker->createToken($newValues, $userId);

        $replacementLog['gtrp_id_token_new'] = $tokenId;
        $replacementLog['gtrp_id_token_old'] = $this->_tokenId;
        $replacementLog['gtrp_created']      = new \MUtil_Db_Expr_CurrentTimestamp();
        $replacementLog['gtrp_created_by']   = $userId;

        $this->db->insert('gems__token_replacements', $replacementLog);

        return $tokenId;
    }

    /**
     * Get all unanswered tokens for the person answering this token
     *
     * Similar to @see $this->getNextUnansweredToken()
     * Similar to @see $this->getTokenCountUnanswered()
     *
     * @return array of tokendata
     */
    public function getAllUnansweredTokens($where = '')
    {
        $select = $this->tracker->getTokenSelect();
        $select->andReceptionCodes()
                ->andRespondentTracks()
                ->andRounds(array())
                ->andSurveys(array())
                ->andTracks()
                ->forGroupId($this->getSurvey()->getGroupId())
                ->forRespondent($this->getRespondentId(), $this->getOrganizationId())
                ->onlySucces()
                ->forWhere('gsu_active = 1')
                ->forWhere('gro_active = 1 OR gro_active IS NULL')
                ->order('gtr_track_name')
                ->order('gr2t_track_info')
                ->order('gto_valid_until')
                ->order('gto_valid_from')
                ->order('gto_round_order');

        $this->_addRelation($select);

        if (!empty($where)) {
            $select->forWhere($where);
        }

        return $select->fetchAll();
    }

    /**
     * Returns a field from the raw answers as a date object.
     *
     * @param string $fieldName Name of answer field
     * @return \MUtil_Date date time or null
     */
    public function getAnswerDateTime($fieldName)
    {
        $survey = $this->getSurvey();
        return $survey->getAnswerDateTime($fieldName, $this);
    }

    /**
     * Returns an array of snippetnames that can be used to display the answers to this token.
     *
     * @return array Of snippet names
     */
    public function getAnswerSnippetNames()
    {
        if (! $this->exists) {
            return ['Token\\TokenNotFoundSnippet'];
        }

        if (! $this->_loopCheck) {
            // Events should not call $this->getAnswerSnippetNames() but
            // $this->getTrackEngine()->getAnswerSnippetNames(). Just in
            // case the code writer made a mistake we have a guard here.
            $this->_loopCheck = true;

            $snippets = $this->getTrackEngine()->getRoundAnswerSnippets($this);

            if (! $snippets) {
                $snippets = $this->getSurvey()->getAnswerSnippetNames($this);
            }

            if ($snippets) {
                $this->_loopCheck = false;
                return $snippets;
            }
        }

        return $this->getTrackEngine()->getAnswerSnippetNames();
    }

    /**
     * A copy of the data array
     *
     * @return array
     */
    public function getArrayCopy()
    {
        return $this->_gemsData;
    }

    /**
     * Returns the staff or respondent id of the person
     * who last changed this token.
     *
     * @return int
     */
    public function getChangedBy()
    {
        return $this->_gemsData['gto_changed_by'];
    }

    /**
     * Return the comment for this token
     *
     * @return string
     */
    public function getComment()
    {
        return $this->_gemsData['gto_comment'];
    }

    /**
     *
     * @return \MUtil_Date Completion time as a date or null
     */
    public function getCompletionTime()
    {
        if (isset($this->_gemsData['gto_completion_time']) && $this->_gemsData['gto_completion_time']) {
            if ($this->_gemsData['gto_completion_time'] instanceof \MUtil_Date) {
                return $this->_gemsData['gto_completion_time'];
            }
            return new \MUtil_Date($this->_gemsData['gto_completion_time'], \Gems_Tracker::DB_DATETIME_FORMAT);
        }
    }

    /**
     *
     * @return string
     */
    public function getConsentCode()
    {
        if ($this->getReceptionCode()->isSuccess()) {
            if (! isset($this->_gemsData['gco_code'])) {
                $this->_ensureRespondentData();
            }

            return $this->_gemsData['gco_code'];
        } else {
            return $this->util->getConsentRejected();
        }
    }

    /**
     * Get the token id of the token this one was copied from, null when not loaded, false when does not exist
     *
     * @return string
     */
    public function getCopiedFrom()
    {
        if (null === $this->_copiedFromTokenId) {
            $this->_copiedFromTokenId = $this->db->fetchOne(
                    "SELECT gtrp_id_token_old FROM gems__token_replacements WHERE gtrp_id_token_new = ?",
                    $this->_tokenId
                    );
        }

        return $this->_copiedFromTokenId;
    }

    /**
     * The token id's of the tokens this one was copied to, null when not loaded, [] when none exist
     *
     * @return array tokenId => tokenId
     */
    public function getCopiedTo()
    {
        if (null === $this->_copiedToTokenIds) {
            $this->_copiedToTokenIds = $this->db->fetchPairs(
                    "SELECT gtrp_id_token_new, gtrp_id_token_new
                        FROM gems__token_replacements
                        WHERE gtrp_id_token_old = ?",
                    $this->_tokenId
                    );

            if (! $this->_copiedToTokenIds) {
                $this->_copiedToTokenIds = [];
            }
        }

        return $this->_copiedToTokenIds;
    }

    /**
     * Returns the staff or respondent id of the person
     * who created this token.
     *
     * @return int
     */
    public function getCreatedBy()
    {
        return $this->_gemsData['gto_created_by'];
    }

    /**
     *
     * @param string $fieldName
     * @return \MUtil_Date
     */
    public function getDateTime($fieldName)
    {
        if (isset($this->_gemsData[$fieldName])) {
            if ($this->_gemsData[$fieldName] instanceof \MUtil_Date) {
                return $this->_gemsData[$fieldName];
            }

            return \MUtil_Date::ifDate(
                    $this->_gemsData[$fieldName],
                    array(\Gems_Tracker::DB_DATETIME_FORMAT, \Gems_Tracker::DB_DATE_FORMAT)
                    );
        }
    }

    /**
     * Returns an array of snippet names that can be used to delete this token.
     *
     * @return array of strings
     */
    public function getDeleteSnippetNames()
    {
        if ($this->exists) {
            return $this->getTrackEngine()->getTokenDeleteSnippetNames($this);
        } else {
            return 'Token\\TokenNotFoundSnippet';
        }
    }

    /**
     * Returns an array of snippet names that can be used to edit this token.
     *
     * @return array of strings
     */
    public function getEditSnippetNames()
    {
        if ($this->exists) {
            return $this->getTrackEngine()->getTokenEditSnippetNames($this);
        } else {
            return 'Token\\TokenNotFoundSnippet';
        }
    }

    /**
     * Get the email address of the person who needs to fill out this survey.
     *
     * This method will return null when no address available
     *
     * @return string|null Email address of the person who needs to fill out the survey or null
     */
    public function getEmail()
    {
        // If staff, return null, we don't know who to email
        if ($this->getSurvey()->isTakenByStaff()) {
            return null;
        }

        // If we have a relation, return that address
        if ($this->hasRelation()) {
            if ($relation = $this->getRelation()) {
                return $relation->getEmail();
            }

            return null;
        }

        // It can only be the respondent
        return $this->getRespondent()->getEmailAddress();
    }

    /**
     *
     * @return string Last mail sent date
     */
    public function getMailSentDate()
    {
        return $this->_gemsData['gto_mail_sent_date'];
    }

    /**
     * Returns a model that can be used to save, edit, etc. the token
     *
     * @return \Gems_Tracker_Model_StandardTokenModel
     */
    public function getModel()
    {
        if ($this->exists) {
            return $this->getTrackEngine()->getTokenModel();
        } else {
            return $this->tracker->getTokenModel();
        }
    }

    /**
     * Returns the next token in this track
     *
     * @return \Gems_Tracker_Token
     */
    public function getNextToken()
    {
        if (null === $this->_nextToken) {
            $tokenSelect = $this->tracker->getTokenSelect();
            $tokenSelect
                    ->andReceptionCodes()
                    ->forPreviousTokenId($this->_tokenId);

            if ($tokenData = $tokenSelect->fetchRow()) {
                $this->_nextToken = $this->tracker->getToken($tokenData);
                $this->_nextToken->_previousToken = $this;
            } else {
                $this->_nextToken = false;
            }
        }

        return $this->_nextToken;
    }

    /**
     * Returns the next unanswered token for the person answering this token
     *
     * @return \Gems_Tracker_Token
     */
    public function getNextUnansweredToken()
    {
        $tokenSelect = $this->tracker->getTokenSelect();
        $tokenSelect
                ->andReceptionCodes()
                // ->andRespondents()
                // ->andRespondentOrganizations()
                // ->andConsents
                ->andRounds(array())
                ->andSurveys(array())
                ->forRespondent($this->getRespondentId())
                ->forGroupId($this->getSurvey()->getGroupId())
                ->onlySucces()
                ->onlyValid()
                ->forWhere('gsu_active = 1')
                ->forWhere('gro_active = 1 OR gro_active IS NULL')
                ->order(array('gto_valid_from', 'gto_round_order'));

        $this->_addRelation($tokenSelect);

        if ($tokenData = $tokenSelect->fetchRow()) {
            return $this->tracker->getToken($tokenData);
        }
    }

    /**
     *
     * @return \Gems_User_Organization
     */
    public function getOrganization()
    {
        return $this->loader->getOrganization($this->getOrganizationId());
    }

    /**
     *
     * @return int
     */
    public function getOrganizationId()
    {
        return $this->_gemsData['gto_id_organization'];
    }

    /**
     *
     * @return string The respondents patient number
     */
    public function getPatientNumber()
    {
        if (! isset($this->_gemsData['gr2o_patient_nr'])) {
            $this->_ensureRespondentData();
        }

        return $this->_gemsData['gr2o_patient_nr'];
    }

    /**
     * Get the phone number of the person who needs to fill out this survey.
     *
     * This method will return null when no number is available
     *
     * @return string|null phone number of the person who needs to fill out the survey or null
     */
    public function getPhoneNumber()
    {
        // If staff, return null, we don't know who to message
        if ($this->getSurvey()->isTakenByStaff()) {
            return null;
        }

        // If we have a relation, return that address
        if ($this->hasRelation()) {
            if ($relation = $this->getRelation()) {
                return $relation->getPhoneNumber();
            }

            return null;
        }

        // It can only be the respondent
        return $this->getRespondent()->getMobilePhoneNumber();
    }

    /**
     * Returns the previous token that has succes in this track
     *
     * @return \Gems_Tracker_Token
     */
    public function getPreviousSuccessToken()
    {
        $prev = $this->getPreviousToken();

        while ($prev && (! $prev->hasSuccesCode())) {
            $prev = $prev->getPreviousToken();
        }

        return $prev;
    }

    /**
     * Returns the previous token in this track
     *
     * @return \Gems_Tracker_Token
     */
    public function getPreviousToken()
    {
        if (null === $this->_previousToken) {
            $tokenSelect = $this->tracker->getTokenSelect();
            $tokenSelect
                    ->andReceptionCodes()
                    ->forNextTokenId($this->_tokenId);

            if ($tokenData = $tokenSelect->fetchRow()) {
                $this->_previousToken = $this->tracker->getToken($tokenData);
                $this->_previousToken->_nextToken = $this;
            } else {
                $this->_previousToken = false;
            }
        }

        return $this->_previousToken;
    }

    /**
     * Returns the answers in simple raw array format, without value processing etc.
     *
     * Function may return more fields than just the answers.
     *
     * @return array Field => Value array
     */
    public function getRawAnswers()
    {
        if (! is_array($this->_sourceDataRaw)) {
            $this->_sourceDataRaw = $this->getSurvey()->getRawTokenAnswerRow($this->_tokenId);
        }
        return $this->_sourceDataRaw;
    }

    /**
     * Return the \Gems_Util_ReceptionCode object
     *
     * @return \Gems_Util_ReceptionCode reception code
     */
    public function getReceptionCode()
    {
        return $this->util->getReceptionCode($this->_gemsData['gto_reception_code']);
    }

    /**
     * Get the relation object if any
     *
     * @return \Gems_Model_RespondentRelationInstance
     */
    public function getRelation()
    {
        if (is_null($this->_relation) || $this->_relation->getRelationId() !== $this->getRelationId()) {
            $model = $this->loader->getModels()->getRespondentRelationModel();
            $relationObject = $model->getRelation($this->getRespondentId(), $this->getRelationId());
            $this->_relation = $relationObject;
        }

        return $this->_relation;
    }

    /**
     * Return the id of the relation field
     *
     * This is not the id of the relation, but the id of the trackfield that defines
     * the relation.
     *
     * @return int
     */
    public function getRelationFieldId()
    {
        return $this->hasRelation() ? (int) $this->_gemsData['gto_id_relationfield'] : null;
    }

    /**
     * Get the name of the relationfield for this token
     *
     * @return string
     */
    public function getRelationFieldName()
    {
        if ($relationFieldId = $this->getRelationFieldId()) {
            $names = $this->getRespondentTrack()->getTrackEngine()->getFieldNames();
            $fieldPrefix = \Gems\Tracker\Model\FieldMaintenanceModel::FIELDS_NAME . \Gems\Tracker\Engine\FieldsDefinition::FIELD_KEY_SEPARATOR;
            $key = $fieldPrefix . $relationFieldId;

            return array_key_exists($key, $names) ? lcfirst($names[$key]) : null;
        }

        return null;
    }

    /**
     * Return the id of the relation currently assigned to this token
     *
     * @return int
     */
    public function getRelationId()
    {
        return $this->hasRelation() ? $this->_gemsData['gto_id_relation'] : null;
    }

    /**
     * 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;
    }

    /**
     * Returns the gender as a letter code
     *
     * @return string
     */
    public function getRespondentGender()
    {
        return $this->getRespondent()->getGender();
    }

    /**
     * Returns the gender for use as part of a sentence, e.g. Dear Mr/Mrs
     *
     * @return string
     */
    public function getRespondentGenderHello()
    {
        $greetings = $this->util->getTranslated()->getGenderGreeting();
        $gender    = $this->getRespondentGender();

        if (isset($greetings[$gender])) {
            return $greetings[$gender];
        }
    }

    /**
     *
     * @return int
     */
    public function getRespondentId()
    {
        if (array_key_exists('gto_id_respondent', $this->_gemsData)) {
            return $this->_gemsData['gto_id_respondent'];
        } else {
            throw new \Gems_Exception(sprintf('Token not loaded correctly', $this->getTokenId()));
        }
    }

    /**
     * Return the default language for the respondent
     *
     * @return string Two letter language code
     */
    public function getRespondentLanguage()
    {
        if (! isset($this->_gemsData['grs_iso_lang'])) {
            $this->_ensureRespondentData();

            if (! isset($this->_gemsData['grs_iso_lang'])) {
                // Still not set in a project? The it is single language
                $this->_gemsData['grs_iso_lang'] = $this->locale->getLanguage();
            }
        }

        return $this->_gemsData['grs_iso_lang'];
    }

    /**
     *
     * @return string
     */
    public function getRespondentLastName()
    {
        return $this->getRespondent()->getLastName();
    }

    /**
     * Get the name of the person answering this token
     *
     * Could be the patient or the relation when assigned to one
     *
     * @return string
     */
    public function getRespondentName()
    {
        if ($this->hasRelation()) {
            if ($relation = $this->getRelation()) {
                return $relation->getName();
            } else {
                return null;
            }
        }

        return $this->getRespondent()->getName();
    }

    /**
     *
     * @return \Gems_Tracker_RespondentTrack
     */
    public function getRespondentTrack()
    {
        if (! $this->respTrack) {
            $this->respTrack = $this->tracker->getRespondentTrack($this->_gemsData['gto_id_respondent_track']);
        }

        return $this->respTrack;
    }

    /**
     *
     * @return int
     */
    public function getRespondentTrackId()
    {
        return $this->_gemsData['gto_id_respondent_track'];
    }

    /**
     * The result value
     *
     * @return string
     */
    public function getResult()
    {
        return $this->_gemsData['gto_result'];
    }

    /**
     * The full return url for a redirect
     *
     * @return string
     */
    public function getReturnUrl()
    {
        return $this->_gemsData['gto_return_url'];
    }

    /**
     * Get the round code for this token
     *
     * @return string|null Null when no round id is present or round no longer exists
     */
    public function getRoundCode()
    {
        $roundCode = null;
        $roundId = $this->getRoundId();
        if ($roundId > 0) {
            $roundCode = $this->getRespondentTrack()->getRoundCode($roundId);
        }

        return $roundCode;
    }

    /**
     *
     * @return string Round description
     */
    public function getRoundDescription()
    {
        return $this->_gemsData['gto_round_description'];
    }

    /**
     *
     * @return int round id
     */
    public function getRoundId()
    {
        return $this->_gemsData['gto_id_round'];
    }

    /**
     *
     * @return int round order
     */
    public function getRoundOrder()
    {
        return $this->_gemsData['gto_round_order'];
    }

    /**
     * Return the name of the respondent
     *
     * To be used when there is a relation and you need to know the name of the respondent
     *
     * @return string
     */
    public function getSubjectname()
    {
        return $this->getRespondent()->getName();
    }

    /**
     * Returns a snippet name that can be used to display this token.
     *
     * @return string
     */
    public function getShowSnippetNames()
    {
        if ($this->exists) {
            return $this->getTrackEngine()->getTokenShowSnippetNames($this);
        } else {
            return 'Token\\TokenNotFoundSnippet';
        }
    }

    /**
     * Returns a string that tells if the token is open, completed or any other
     * status you might like. This will not be interpreted by the tracker it is
     * for display purposes only
     *
     * @return string Token status description
     */
    public function getStatus()
    {
        return $this->util->getTokenData()->getStatusDescription($this->getStatusCode());
    }

    /**
     * Returns token status code
     *
     * @return string Token status code in one letter
     */
    public function getStatusCode()
    {
        return $this->_gemsData['token_status'];
    }

    /**
     *
     * @return \Gems_Tracker_Survey
     */
    public function getSurvey()
    {
        if (! $this->survey) {
            $this->survey = $this->tracker->getSurvey($this->_gemsData['gto_id_survey']);
        }

        return $this->survey;
    }

    /**
     *
     * @return int Gems survey id
     */
    public function getSurveyId()
    {
        return $this->_gemsData['gto_id_survey'];
    }

    /**
     *
     * @param string $language (ISO) language string
     * @return \MUtil_Model_ModelAbstract
     */
    public function getSurveyAnswerModel($language)
    {
        $survey = $this->getSurvey();
        return $survey->getAnswerModel($language);
    }

    /**
     *
     * @return string Name of the survey
     */
    public function getSurveyName()
    {
        $survey = $this->getSurvey();
        return $survey->getName();
    }

    /**
     * Returns the number of unanswered tokens for the person answering this token,
     * minus this token itself
     *
     * @return int
     */
    public function getTokenCountUnanswered()
    {
        $tokenSelect = $this->tracker->getTokenSelect(new \Zend_Db_Expr('COUNT(*)'));
        $tokenSelect
                ->andReceptionCodes(array(), false)
                ->andSurveys(array())
                ->andRounds(array())
                ->forRespondent($this->getRespondentId())
                ->forGroupId($this->getSurvey()->getGroupId())
                ->onlySucces()
                ->onlyValid()
                ->forWhere('gsu_active = 1')
                ->forWhere('gro_active = 1 OR gro_active IS NULL')
                ->withoutToken($this->_tokenId);

        $this->_addRelation($tokenSelect);

        return $tokenSelect->fetchOne();
    }

    /**
     *
     * @return string token
     */
    public function getTokenId()
    {
        return $this->_tokenId;
    }

    /**
     * Get the track engine that generated this token
     *
     * @return \Gems_Tracker_Engine_TrackEngineInterface
     */
    public function getTrackEngine()
    {
        if ($this->exists) {
            return $this->tracker->getTrackEngine($this->_gemsData['gto_id_track']);
        }

        throw new \Gems_Exception_Coding('Coding error: requesting track engine for non existing token.');
    }

    /**
     *
     * @return int gems_tracks track id
     */
    public function getTrackId()
    {
        return $this->_gemsData['gto_id_track'];
    }

    public function getTrackName()
    {
        $trackData = $this->db->fetchRow("SELECT gtr_track_name FROM gems__tracks WHERE gtr_id_track = ?", $this->getTrackId());
        return $trackData['gtr_track_name'];
    }

    /**
     *
     * @param string $language The language currently used by the user
     * @param int $userId The id of the gems user
     * @throws \Gems_Tracker_Source_SurveyNotFoundException
     */
    public function getUrl($language, $userId)
    {
        $survey = $this->getSurvey();

        $survey->copyTokenToSource($this, $language);

        if (! $this->_gemsData['gto_in_source']) {
            $values['gto_start_time'] = new \MUtil_Db_Expr_CurrentTimestamp();
            $values['gto_in_source']  = 1;

            $oldTokenId = $this->getCopiedFrom();
            if ($oldTokenId) {
                $oldToken = $this->tracker->getToken($oldTokenId);
                if ($oldToken->getReceptionCode()->hasRedoCopyCode()) {
                    $this->setRawAnswers($oldToken->getRawAnswers());
                }
            }
        }
        $values['gto_by']         = $userId;
        $values['gto_return_url'] = $this->calculateReturnUrl();

        $this->_updateToken($values, $userId);

        $this->handleBeforeAnswering();

        return $survey->getTokenUrl($this, $language);
    }

    /**
     *
     * @return \MUtil_Date Valid from as a date or null
     */
    public function getValidFrom()
    {
        return $this->getDateTime('gto_valid_from');
    }

    /**
     *
     * @return \MUtil_Date Valid until as a date or null
     */
    public function getValidUntil()
    {
        return $this->getDateTime('gto_valid_until');
    }

    /**
     * Survey dependent calculations / answer changes that must occur after a survey is completed
     *
     * @param type $tokenId The tokend the answers are for
     * @param array $tokenAnswers Array with answers. May be changed in process
     * @return array The changed values
     */
    public function handleAfterCompletion()
    {
        $survey = $this->getSurvey();
        $completedEvent = $survey->getSurveyCompletedEvent();

        $eventName = 'gems.survey.completed';

        if ($this->event->hasListeners($eventName)) {
            // Remove previous gems survey completed events if set
            $listeners = $this->event->getListeners($eventName);
            foreach($listeners as $listener) {
                $order = $this->event->getListenerPriority($eventName, $listener);
                if ($order === 100) {
                    $this->event->removeListener($eventName, $listener);
                }
            }
        }

        if (! $completedEvent && !$this->event->hasListeners($eventName)) {
            return;
        }

        if ($completedEvent) {
            $eventFunction = function (TokenEvent $event) use ($completedEvent) {
                $token = $event->getToken();
                try {
                    $changed = $completedEvent->processTokenData($token);
                    if (is_array($changed)) {
                        $event->addChanged($changed);
                    }
                } catch (\Exception $e) {
                    throw new \Exception('Event: ' . $completedEvent->getEventName() . '. ' . $e->getMessage());
                }
            };
            $this->event->addListener($eventName, $eventFunction, 100);
        }

        $tokenEvent = new TokenEvent($this);
        try {
            $this->event->dispatch($tokenEvent, $eventName);
        } catch (\Exception $e) {
            $this->logger->log(sprintf(
                "After completion event error for token %s on survey '%s': %s",
                $this->_tokenId,
                $this->getSurveyName(),
                $e->getMessage()
            ), \Zend_Log::ERR);
        }
        if ($completedEvent) {
            // Remove this event to prevent double triggering
            $this->event->removeListener($eventName, $eventFunction);
        }

        $changed = $tokenEvent->getChanged();
        if ($changed && is_array($changed)) {

            $this->setRawAnswers($changed);
        }

        return $changed;
    }

    /**
     * Survey dependent calculations / answer changes that must occur after a survey is completed
     *
     * @param type $tokenId The tokend the answers are for
     * @param array $tokenAnswers Array with answers. May be changed in process
     * @return array The changed values
     */
    public function handleBeforeAnswering()
    {
        $survey = $this->getSurvey();
        $beforeAnswerEvent  = $survey->getSurveyBeforeAnsweringEvent();

        $eventName = 'gems.survey.before-answering';

        if (! $beforeAnswerEvent && !$this->event->hasListeners($eventName)) {
            return;
        }

        if ($beforeAnswerEvent) {
            $eventFunction = function (TokenEvent $event) use ($beforeAnswerEvent) {
                $token = $event->getToken();
                try {
                    $changed = $beforeAnswerEvent->processTokenInsertion($token);
                    if (is_array($changed) && $changed) {
                        $event->addChanged($changed);
                    }
                } catch (\Exception $e) {
                    throw new \Exception('Event: ' . $beforeAnswerEvent->getEventName() . '. ' . $e->getMessage());
                }
            };
            $this->event->addListener($eventName, $eventFunction, 100);
        }

        $tokenEvent = new TokenEvent($this);

        try {
            $this->event->dispatch($tokenEvent, $eventName);
        } catch (\Exception $e) {
            $this->logger->log(sprintf(
                "Before answering before event error for token %s on survey '%s': %s",
                $this->_tokenId,
                $this->getSurveyName(),
                $e->getMessage()
            ), \Zend_Log::ERR);
        }
        if ($beforeAnswerEvent) {
            // Remove this event to prevent double triggering
            $this->event->removeListener($eventName, $eventFunction);
        }

        $changed = $tokenEvent->getChanged();
        if ($changed && is_array($changed)) {

            $this->setRawAnswers($changed);

            if (\Gems_Tracker::$verbose) {
                \MUtil_Echo::r($changed, 'Source values for ' . $this->_tokenId . ' changed by event.');
            }
        }

        return $changed;
    }

    /**
     * Returns true when the answers are loaded.
     *
     * There may not be any answers, but the attemt to retrieve them was made.
     *
     * @return boolean
     */
    public function hasAnswersLoaded()
    {
        return (boolean) $this->_sourceDataRaw;
    }

    /**
     *
     * @deprecated Use the ReceptionCode->hasRedoCode
     * @return boolean
     */
    public function hasRedoCode()
    {
        return $this->getReceptionCode()->hasRedoCode();
    }

    /**
     * True if the reception code is a redo survey copy.
     *
     * @deprecated Use the ReceptionCode->hasRedoCopyCode
     * @return boolean
     */
    public function hasRedoCopyCode()
    {
        return $this->getReceptionCode()->hasRedoCopyCode();
    }

    /**
     * Is this token linked to a relation?
     *
     * @return boolean
     */
    public function hasRelation()
    {
        if (array_key_exists('gto_id_relationfield', $this->_gemsData) && $this->_gemsData['gto_id_relationfield'] > 0) {
            // We have a relation
            return true;
        }

        // no relation
        return false;
    }

    /**
     *
     * @return boolean
     */
    public function hasResult()
    {
        return $this->_gemsData['gto_result'];
    }

    /**
     *
     * @deprecated Use the ReceptionCode->isSuccess
     * @return boolean
     */
    public function hasSuccesCode()
    {
        return $this->getReceptionCode()->isSuccess();
    }

    /**
     * True is this token was exported to the source.
     *
     * @return boolean
     */
    public function inSource()
    {
        if ($this->exists) {
            $survey = $this->getSurvey();

            return $survey->inSource($this);
        } else {
            return false;
        }
    }

    /**
     *
     * @return boolean
     */
    public function isCompleted()
    {
        return isset($this->_gemsData['gto_completion_time']) && $this->_gemsData['gto_completion_time'];
    }

    /**
     * True when the valid from is set and in the past and the valid until is not set or is in the future
     *
     * @return boolean
     */
    public function isCurrentlyValid()
    {
        if ($this->isNotYetValid()) {
            return false;
        }
        if ($this->isExpired()) {
            return false;
        }
        return true;
    }

    /**
     * True when the valid until is set and is in the past
     * @return boolean
     */
    public function isExpired()
    {
        $date = $this->getValidUntil();

        if ($date instanceof \MUtil_Date) {
            return $date->isEarlierOrEqual(time());
        }

        return false;
    }

    /**
     * Can mails be sent for this token?
     *
     * Cascades to track and respondent level mailable setting
     * also checks is the email field for respondent or relation is not null
     *
     * @return boolean
     */
    public function isMailable()
    {
        $email = $this->getEmail();
        if ($this->hasRelation()) {
            $filler = $this->getRelation();
        } else {
            $filler = $this->getRespondent();
        }
        $mailable = !empty($email) && $this->getRespondentTrack()->isMailable() && $filler->isMailable();

        return $mailable;
    }

    /**
     * True when the valid from is in the future or not yet set
     *
     * @return boolean
     */
    public function isNotYetValid()
    {
        $date = $this->getValidFrom();

        if ($date instanceof \MUtil_Date) {
            return $date->isLaterOrEqual(time());
        }

        return true;
    }

    /**
     *
     * @return boolean True when this date was set by user input
     */
    public function isValidFromManual()
    {
        return isset($this->_gemsData['gto_valid_from_manual']) && $this->_gemsData['gto_valid_from_manual'];
    }

    /**
     *
     * @return boolean True when this date was set by user input
     */
    public function isValidUntilManual()
    {
        return isset($this->_gemsData['gto_valid_until_manual']) && $this->_gemsData['gto_valid_until_manual'];
    }

    /**
     *
     * @return boolean True when this user has the right to view these answers
     */
    public function isViewable()
    {
        if (isset($this->_gemsData['show_answers']) && $this->_gemsData['show_answers']) {
            return $this->currentUser->isAllowedOrganization($this->getOrganizationId());
        }

        return false;
    }

    /**
     *
     * @param array $gemsData Optional, the data refresh with, otherwise refresh from database.
     * @return \Gems_Tracker_Token (continuation pattern)
     */
    public function refresh(array $gemsData = null)
    {
        if (is_array($gemsData)) {
            $this->_gemsData = $gemsData + $this->_gemsData;
        } else {
            $tokenSelect = $this->tracker->getTokenSelect();

            $tokenSelect
                    ->andReceptionCodes()
                    ->andRespondents()
                    ->andRespondentOrganizations()
                    ->addStatus()
                    ->addShowAnswers($this->currentUser->getGroupId(true))
                    ->forTokenId($this->_tokenId);

            $this->_gemsData = $tokenSelect->fetchRow();
            if (! $this->_gemsData) {
                // on failure, reset to empty array
                $this->_gemsData = [];
            }
        }
        if ($this->currentUser instanceof \Gems_User_User) {
            $this->_gemsData = $this->currentUser->applyGroupMask($this->_gemsData);
        }
        $this->exists = isset($this->_gemsData['gto_id_token']);

        return $this;
    }

    /**
     * Refresh the consent Code
     *
     * @param string $consentCode
     */
    public function refreshConsent()
    {
        if (isset($this->_gemsData['gco_code'])) {
            // Setting the gco_code to false will make sure the data is reloaded
            $this->_gemsData['gco_code'] = false;
            $this->getConsentCode();
        }
    }

    /**
     *
     * @param string|\MUtil_Date $completionTime Completion time as a date or null
     * @param int $userId The current user
     * @return \Gems_Tracker_Token (continuation pattern)
     */
    public function setCompletionTime($completionTime, $userId)
    {
        $values['gto_completion_time'] = null;
        if (!is_null($completionTime)) {
            if (! $completionTime instanceof \Zend_Date) {
                $completionTime = \MUtil_Date::ifDate(
                        $completionTime,
                        array(\Gems_Tracker::DB_DATETIME_FORMAT, \Gems_Tracker::DB_DATE_FORMAT, null)
                        );
            }
            if ($completionTime instanceof \Zend_Date) {
                $values['gto_completion_time'] = $completionTime->toString(\Gems_Tracker::DB_DATETIME_FORMAT);
            }
        }
        $this->_updateToken($values, $userId);

        $survey = $this->getSurvey();
        $source = $survey->getSource();
        $source->setTokenCompletionTime($this, $completionTime, $survey->getSurveyId(), $survey->getSourceSurveyId());

        $this->refresh();
        $this->checkTokenCompletion($userId);

        return $this;
    }

    /**
     * Add 1 to the number of messages sent and change the sent date
     */
    public function setMessageSent()
    {
        $values = [
            'gto_mail_sent_num' => new \Zend_Db_Expr('gto_mail_sent_num + 1'),
            'gto_mail_sent_date' => \MUtil_Date::format(new \Zend_Date(), 'yyyy-MM-dd'),
        ];

        $this->_updateToken($values, $this->currentUser->getUserId());
    }

    /**
     * Sets the next token in this track
     *
     * @param \Gems_Tracker_Token $token
     * @return \Gems_Tracker_Token (continuation pattern)
     */
    public function setNextToken(\Gems_Tracker_Token $token)
    {
        $this->_nextToken = $token;

        $token->_previousToken = $this;

        return $this;
    }

    /**
     * Sets answers for this token to the values defined in the $answers array. Also handles updating the
     * internal answercache if present
     *
     * @param array $answers
     */
    public function setRawAnswers($answers)
    {
        $survey = $this->getSurvey();
        $source = $survey->getSource();

        $source->setRawTokenAnswers($this, $answers, $survey->getSurveyId(), $survey->getSourceSurveyId());

        // They are not always loaded
        if ($this->hasAnswersLoaded()) {
            //Now update internal answer cache
            $this->_sourceDataRaw = $answers + $this->_sourceDataRaw;
        }
    }

    /**
     * Set the reception code for this token and make sure the necessary
     * cascade to the source takes place.
     *
     * @param string $code The new (non-success) reception code or a \Gems_Util_ReceptionCode object
     * @param string $comment Comment 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);
        }
        $values['gto_reception_code'] = $code->getCode();
        if ($comment) {
            $values['gto_comment'] = $comment;
        }
        // \MUtil_Echo::track($values);

        $changed = $this->_updateToken($values, $userId);

        if ($changed) {
            if ($code->isOverwriter() || (! $code->isSuccess())) {
                $survey = $this->getSurvey();

                // Update the consent code in the source
                if ($survey->inSource($this)) {
                    $survey->updateConsent($this);
                }
            }
        }

        return $changed;
    }

    /**
     * Set a round description for the token
     *
     * @param  string The new round description
     * @param int $userId The current user
     * @return int 1 if data changed, 0 otherwise
     */
    public function setRoundDescription($description, $userId)
    {
        $values = $this->_gemsData;
        $values['gto_round_description'] = $description;
        return $this->_updateToken($values, $userId);
    }

    /**
     *
     * @param mixed $validFrom \Zend_Date or string
     * @param mixed $validUntil null, \Zend_Date or string. False values leave values unchangeds
     * @param int $userId The current user
     * @return int 1 if the token has changed, 0 otherwise
     */
    public function setValidFrom($validFrom, $validUntil, $userId)
    {
        $mailSentDate = $this->getMailSentDate();
        if (! $mailSentDate instanceof \MUtil_Date) {
            $mailSentDate = \MUtil_Date::ifDate($mailSentDate, [\Gems_Tracker::DB_DATE_FORMAT, \Gems_Tracker::DB_DATETIME_FORMAT]);
        }
        if ($validFrom && $mailSentDate) {
            // Check for newerness

            if ($validFrom instanceof \Zend_Date) {
                $start = $validFrom;
            } else {
                $start = new \MUtil_Date($validFrom, \Gems_Tracker::DB_DATETIME_FORMAT);
            }

            if ($start->isLater($mailSentDate)) {
                $values['gto_mail_sent_date'] = null;
                $values['gto_mail_sent_num']  = 0;

                $now = new \MUtil_Date();
                $newComment = sprintf(
                    $this->_('%s: Reset number of contact moments because new start date %s is later than last contact date (%s).'),
                    $now->toString('yyyy-MM-dd HH:mm:ss'),
                    $start->toString('yyyy-MM-dd HH:mm:ss'),
                    $mailSentDate->toString('yyyy-MM-dd HH:mm:ss')
                );
                $comment = $this->getComment();
                if (!empty($comment)) {
                    $comment .= "\n";
                }
                $values['gto_comment'] = $comment .= $newComment;
            }
        }

        if ($validFrom instanceof \Zend_Date) {
            $validFrom = $validFrom->toString(\Gems_Tracker::DB_DATETIME_FORMAT);
        } elseif ('' === $validFrom) {
            $validFrom = null;
        }
        if ($validUntil instanceof \Zend_Date) {
            $validUntil = $validUntil->toString(\Gems_Tracker::DB_DATETIME_FORMAT);
        } elseif ('' === $validUntil) {
            $validUntil = null;
        }

        $values['gto_valid_from'] = $validFrom;
        $values['gto_valid_until'] = $validUntil;

        return $this->_updateToken($values, $userId);
    }

    /**
     * Handle sending responses to the response database (if used)
     *
     * Triggered by checkTokenCompletion
     *
     * @param int $userId The id of the gems user
     */
    protected function toResponseDatabase($userId)
    {
        $responseDb = $this->project->getResponseDatabase();

        // WHY EXPLANATION!!
        //
        // For some reason mysql prepared parameters do nothing with a \Zend_Db_Expr
        // object and that causes an error when using CURRENT_TIMESTAMP
        $current = \MUtil_Date::now()->toString(\Gems_Tracker::DB_DATETIME_FORMAT);
        $rValues = array(
            'gdr_id_token'   => $this->_tokenId,
            'gdr_changed'    => $current,
            'gdr_changed_by' => $userId,
            'gdr_created'    => $current,
            'gdr_created_by' => $userId,
        );
        $responses = $this->getRawAnswers();

        $source = $this->getSurvey()->getSource();
        if ($source instanceof \Gems_Tracker_Source_SourceAbstract) {
            $metaFields = $source::$metaFields;
            foreach ($metaFields as $field) {
                if (array_key_exists($field, $responses)) {
                    unset($responses[$field]);
                }
            }
        }

        // first read current responses to differentiate between insert and update
        $responseSelect = $responseDb->select()->from('gemsdata__responses', array('gdr_answer_id', 'gdr_response'))
                ->where('gdr_id_token = ?', $this->_tokenId);
        $currentResponses = $responseDb->fetchPairs($responseSelect);

        if (! $currentResponses) {
            $currentResponses = array();
        }
        // \MUtil_Echo::track($currentResponses, $responses);

        // Prepare sql
        $sql = "UPDATE gemsdata__responses
            SET `gdr_response` = ?, `gdr_changed` = ?, `gdr_changed_by` = ?
            WHERE gdr_id_token = ? AND gdr_answer_id = ? AND gdr_answer_row = 1";
        $statement = $responseDb->prepare($sql);

        $inserts = array();
        foreach ($responses as $fieldName => $response) {
            $rValues['gdr_answer_id']  = $fieldName;
            if (is_array($response)) {
                $response = join('|', $response);
            }
            $rValues['gdr_response']  = $response;

            if (array_key_exists($fieldName, $currentResponses)) {    // Already exists, do update
                // But only if value changed
                if ($currentResponses[$fieldName] != $response) {
                    try {
                        // \MUtil_Echo::track($sql, $rValues['gdr_id_token'], $fieldName, $response);
                        $statement->execute(array(
                            $response,
                            $rValues['gdr_changed'],
                            $rValues['gdr_changed_by'],
                            $rValues['gdr_id_token'],
                            $fieldName
                        ));
                    } catch (\Zend_Db_Statement_Exception $e) {
                        error_log($e->getMessage());
                        \Gems_Log::getLogger()->logError($e);
                    }
                }
            } else {
                // We add the inserts together in the next prepared statement to improve speed
                $inserts[$fieldName] = $rValues;
            }
        }

        if (count($inserts)>0) {
            // \MUtil_Echo::track($inserts);
            try {
                $fields = array_keys(reset($inserts));
                $fields = array_map(array($responseDb, 'quoteIdentifier'), $fields);
                $sql = 'INSERT INTO gemsdata__responses (' .
                        implode(', ', $fields) . ') VALUES (' .
                        implode(', ', array_fill(1, count($fields), '?')) . ')';

                // \MUtil_Echo::track($sql);
                $statement = $responseDb->prepare($sql);

                foreach($inserts as $insert) {
                    // \MUtil_Echo::track($insert);
                    $statement->execute($insert);
                }

            } catch (\Zend_Db_Statement_Exception $e) {
                error_log($e->getMessage());
                \Gems_Log::getLogger()->logError($e);
            }
        }
    }
}