GemsTracker/gemstracker-library

View on GitHub
classes/Gems/Model/RespondentModel.php

Summary

Maintainability
F
6 days
Test Coverage
C
74%
<?php

use Gems\Exception\RespondentAlreadyExists;

/**
 *
 * @package    Gems
 * @subpackage Model
 * @author     Matijs de Jong <mjong@magnafacta.nl>
 * @copyright  Copyright (c) 2011 Erasmus MC
 * @license    New BSD License
 */

/**
 * Standard Respondent model.
 *
 * When a project defines its own sub-class of this class and names
 * it <Project_name>_Model_RespondentModel, that class is loaded
 * instead.
 *
 * @package    Gems
 * @subpackage Model
 * @copyright  Copyright (c) 2011 Erasmus MC
 * @license    New BSD License
 * @since      Class available since version 1.0
 */
class Gems_Model_RespondentModel extends \Gems_Model_HiddenOrganizationModel
{
    /**
     * Store the SSN hashed in the database and display only '*****'
     */
    const SSN_HASH = 1;

    /**
     * Do not use the SSN
     */
    const SSN_HIDE = 2;

    /**
     * Store the SSN as is and display the value.
     */
    const SSN_OPEN = 4;

    /**
     *
     * @var array Of field names containing consents
     */
    public $consentFields = ['gr2o_consent'];

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

    /**
     * Determines the algorithm used to hash the social security number
     *
     * Can be changed is derived classes, set to null to use old md5() method
     *
     * @var int One of the SSN_ constants
     */
    public $hashAlgorithm = 'sha512';

    /**
     * Determines how the social security number is stored.
     *
     * Can be changed is derived classes.
     *
     * @var int One of the SSN_ constants
     */
    public $hashSsn = self::SSN_HASH;

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

    /**
     * Should the logincheck be added automatically
     *
     * @var boolean
     */
    protected $loginCheck = false;

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

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

    /**
     *
     * @var \Zend_View
     */
    public $view;

    /**
     * Self constructor
     */
    public function __construct()
    {
        // gems__respondents MUST be first table for INSERTS!!
        parent::__construct('respondents', 'gems__respondents', 'grs');

        $this->addTable('gems__respondent2org', array('grs_id_user' => 'gr2o_id_user'), 'gr2o');
        $this->addTable('gems__reception_codes', array('gr2o_reception_code' => 'grc_id_reception_code'));

        $this->setKeys($this->_getKeysFor('gems__respondent2org'));

        $this->addColumn(new \Zend_Db_Expr("CASE WHEN grc_success = 1 THEN '' ELSE 'deleted' END"), 'row_class');
        $this->addColumn(new \Zend_Db_Expr("CASE WHEN grc_success = 1 THEN 0 ELSE 1 END"), 'resp_deleted');

        foreach ($this->consentFields as $consent) {
            $this->addColumn(new \Zend_Db_Expr($consent), 'old_' . $consent);
        }

        if (! $this->has('grs_ssn')) {
            $this->hashSsn = self::SSN_HIDE;
        }
        if (self::SSN_HASH === $this->hashSsn) {
            $this->setSaveWhen('grs_ssn', array($this, 'whenSSN'));
            $this->setOnLoad('grs_ssn', array($this, 'hideSSN'));
            $this->setOnSave('grs_ssn', array($this, 'saveSSN'));
        }
    }

    /**
     * Add an organization filter if it wasn't specified in the filter.
     *
     * Checks the filter on sematic correctness and replaces the text seacrh filter
     * with the real filter.
     *
     * @param mixed $filter True for the filter stored in this model or a filter array
     * @return array The filter to use
     */
    protected function _checkFilterUsed($filter)
    {
        $filter = parent::_checkFilterUsed($filter);

        if (isset($filter['gr2o_id_organization'])) {
            // Check for option to check for any organization
            if (true === $filter['gr2o_id_organization']) {
                unset($filter['gr2o_id_organization']);
            }
        } else {
            // Add the correct filter
            if ($this->isMultiOrganization() && !isset($filter['gr2o_patient_nr'])) {
                $allowed = $this->currentUser->getAllowedOrganizations();

                // If we are not looking for a specific patient, we can look at all patients
                $filter['gr2o_id_organization'] = array_keys($allowed);
            } else {
                // Otherwise, we can only see in our current organization
                $filter['gr2o_id_organization'] = $this->getCurrentOrganization();
            }
        }

        if (self::SSN_HASH === $this->hashSsn) {
            // Make sure a search for a SSN is hashed when needed.
            array_walk_recursive($filter, array($this, 'applyHash'));
        }

        return $filter;
    }

    /**
     * Add the table and field to check for respondent login checks
     *
     * @return \Gems_Model_RespondentModel (continuation pattern)
     */
    public function addLoginCheck()
    {
        if (! $this->hasAlias('gems__user_logins')) {
            $this->addLeftTable(
                    'gems__user_logins',
                    array('gr2o_patient_nr' => 'gul_login', 'gr2o_id_organization' => 'gul_id_organization'),
                    'gul',
                    \MUtil_Model_DatabaseModelAbstract::SAVE_MODE_UPDATE |
                        \MUtil_Model_DatabaseModelAbstract::SAVE_MODE_DELETE);
        }

        $this->addColumn(
                "CASE WHEN gul_id_user IS NULL OR gul_user_class = 'NoLogin' OR gul_can_login = 0 THEN 0 ELSE 1 END",
                'has_login');

        return $this;
    }

    /**
     * Add the respondent name as a caclulated field to the model
     * @param \Gems_Model_JoinModel $model
     * @param string $label
     */
    public static function addNameToModel(\Gems_Model_JoinModel $model, $label)
    {
        $nameExpr[]  = "COALESCE(grs_last_name, '-')";
        $fieldList[] = 'grs_last_name';
        if ($model->has('grs_partner_last_name')) {
            if ($model->has('grs_partner_surname_prefix')) {
                $nameExpr[]  = "COALESCE(CONCAT(' ', grs_partner_surname_prefix), '')";
                $fieldList[] = 'grs_partner_surname_prefix';
            }

            $nameExpr[]  = "COALESCE(CONCAT(' ', grs_partner_last_name), '')";
            $fieldList[] = 'grs_partner_last_name';
        }
        $nameExpr[] = "', '";

        if ($model->has('grs_first_name')) {
            if ($model->has('grs_initials_name')) {
                $nameExpr[]  = "COALESCE(grs_first_name, grs_initials_name, '')";
                $fieldList[] = 'grs_first_name';
                $fieldList[] = 'grs_initials_name';
            } else {
                $nameExpr[]  = "COALESCE(grs_first_name, '')";
                $fieldList[] = 'grs_first_name';
            }
        } elseif ($model->has('grs_initials_name')) {
            $nameExpr[]  = "COALESCE(grs_initials_name, '')";
            $fieldList[] = 'grs_initials_name';
        }
        if ($model->has('grs_surname_prefix')) {
            $nameExpr[]  = "COALESCE(CONCAT(' ', grs_surname_prefix), '')";
            $fieldList[] = 'grs_surname_prefix';
        }
        $model->set('name',
                'label', $label,
                'column_expression', new Zend_Db_Expr("CONCAT(" . implode(', ', $nameExpr) . ")"),
                'fieldlist', $fieldList);
   }

    /**
     * Called after the check that all required registry values
     * have been set correctly has run.
     *
     * @return void
     */
    public function afterRegistry()
    {
        parent::afterRegistry();

        $this->setOnSave('gr2o_opened', new \MUtil_Db_Expr_CurrentTimestamp());
        $this->setSaveOnChange('gr2o_opened');
        $this->setOnSave('gr2o_opened_by', $this->currentUser->getUserId());
        $this->setSaveOnChange('gr2o_opened_by');
    }

    /**
     * Set those settings needed for the browse display
     *
     * @return \Gems_Model_RespondentModel
     */
    public function applyBrowseSettings()
    {
        $dbLookup   = $this->util->getDbLookup();
        $translated = $this->util->getTranslated();

        $this->resetOrder();

        if ($this->has('gr2o_id_organization') && $this->isMultiOrganization()) {
            // Add for sorting
            $this->addTable('gems__organizations', array('gr2o_id_organization' => 'gor_id_organization'));

            $this->setIfExists('gor_name', 'label', $this->translate->_('Organization'));

            $this->set('gr2o_id_organization',
                    'label', $this->_('Organization'),
                    'multiOptions', $dbLookup->getOrganizationsWithRespondents()
                    );
        }

        $this->setIfExists('gr2o_patient_nr', 'label', $this->_('Respondent nr'));

        self::addNameToModel($this, $this->_('Name'));

        $this->setIfExists('gr2o_email',       'label', $this->_('E-Mail'));
        $this->setIfExists('gr2o_mailable',   'label', $this->_('May be mailed'),
                'multiOptions', $dbLookup->getRespondentMailCodes()
                );

        $this->setIfExists('grs_address_1',   'label', $this->_('Street'));
        $this->setIfExists('grs_zipcode',     'label', $this->_('Zipcode'));
        $this->setIfExists('grs_city',        'label', $this->_('City'));

        $this->setIfExists('grs_phone_1',     'label', $this->_('Phone'));

        $this->setIfExists('grs_birthday',    'label', $this->_('Birthday'),
                'dateFormat', \Zend_Date::DATE_MEDIUM);

        $this->setIfExists('gr2o_opened',
                'label', $this->_('Opened'),
                'formatFunction', $translated->describeDateFromNow);
        $this->setIfExists('gr2o_consent',
                'label', $this->_('Consent'),
                'multiOptions', $dbLookup->getUserConsents()
                );

        $this->refreshGroupSettings();

        return $this;
    }

    /**
     * Set those settings needed for the detailed display
     *
     * @return \Gems_Model_RespondentModel
     */
    public function applyDetailSettings()
    {
        $dbLookup   = $this->util->getDbLookup();
        $localized  = $this->util->getLocalized();
        $translated = $this->util->getTranslated();

        if ($this->loginCheck) {
            $this->addLoginCheck();
        }
        $this->resetOrder();
        if ($this->has('gr2o_id_organization')) {
            $this->set('gr2o_id_organization',
                    'label', $this->_('Organization'),
                    'tab', $this->_('Identification'),
                    'multiOptions', $this->currentUser->getRespondentOrganizations()
                    );

            $this->set('gr2o_id_organization', 'default', $this->currentUser->getCurrentOrganizationId());

            if (count($this->currentUser->getAllowedOrganizations()) == 1) {
                $this->set('gr2o_id_organization', 'elementClass', 'Exhibitor');
            }
        }

        // The SSN
        if ($this->hashSsn !== self::SSN_HIDE) {
            $this->set('grs_ssn', 'label', $this->_('SSN'),
                    'tab', $this->_('Identification'));
        }

        $this->setIfExists('gr2o_patient_nr', 'label', $this->_('Respondent number'),
                'tab', $this->_('Identification'));

        $this->setIfExists('grs_initials_name',  'label', $this->_('Initials'));
        $this->setIfExists('grs_first_name',  'label', $this->_('First name'));
        $this->setIfExists('grs_surname_prefix', 'label', $this->_('Surname prefix'),
                'description', $this->_('de, ibn, Le, Mac, von, etc...'));

        $this->setIfExists('grs_last_name',   'label', $this->_('Last name'));

        $this->setIfExists('grs_partner_surname_prefix', 'label', $this->_('Partner surname prefix'),
                'description', $this->_('de, ibn, Le, Mac, von, etc...'));
        $this->setIfExists('grs_partner_last_name',   'label', $this->_('Partner last name'));

        $this->setIfExists('grs_gender',      'label', $this->_('Gender'),
                'multiOptions', $translated->getGenderHello()
                );

        $this->setIfExists('grs_birthday',    'label', $this->_('Birthday'),
                'dateFormat', \Zend_Date::DATE_MEDIUM
                );

        $this->setIfExists('gr2o_email',       'label', $this->_('E-Mail'),
                'tab', $this->_('Contact information'));
        $this->setIfExists('gr2o_mailable',   'label', $this->_('May be mailed'),
                'elementClass', 'radio',
                'separator', ' ',
                'multiOptions', $dbLookup->getRespondentMailCodes()
                );

        $this->setIfExists('gr2o_treatment',  'label', $this->_('Treatment'));
        $this->setIfExists('gr2o_comments',   'label', $this->_('Comments'));

        $this->setIfExists('grs_address_1',   'label', $this->_('Street'));
        $this->setIfExists('grs_address_2',   'label', ' ');

        // \MUtil_Echo::track($this->getItemsOrdered(), $this->getOrder('gr2o_email'));

        $this->setIfExists('grs_zipcode',     'label', $this->_('Zipcode'));
        $this->setIfExists('grs_city',        'label', $this->_('City'));
        $this->setIfExists('grs_iso_country', 'label', $this->_('Country'),
                'multiOptions', $localized->getCountries());

        $this->setIfExists('grs_phone_1',     'label', $this->_('Phone'));
        $this->setIfExists('grs_phone_2',     'label', $this->_('Phone 2'));
        $this->setIfExists('grs_phone_3',     'label', $this->_('Phone 3'));
        $this->setIfExists('grs_phone_4',     'label', $this->_('Phone 4'));

        $this->setIfExists('grs_iso_lang',    'label', $this->_('Language'),
                'multiOptions', $localized->getLanguages(),
                'tab', $this->_('Settings'), 'default', $this->project->getLocaleDefault());

        $this->setIfExists('gr2o_consent',    'label', $this->_('Consent'),
                'default', $this->util->getDefaultConsent(),
                'description', $this->_('Has the respondent signed the informed consent letter?'),
                'multiOptions', $dbLookup->getUserConsents()
                );

        $changers = $this->getChangersList();

        $this->setIfExists('gr2o_opened',     'label', $this->_('Opened'),
                'dateFormat', \Zend_Date::DATE_MEDIUM,
                'default', '',
                'elementClass', 'None',  // Has little use to show: is usually editor
                'formatFunction', array($translated, 'describeDateFromNow')
                );
        $this->setIfExists('gr2o_opened_by',  'label', $this->_('Opened'),
                'elementClass', 'None',  // Has little use to show: is usually editor
                'multiOptions', $changers
                );
        $this->setIfExists('gr2o_changed',    'label', $this->_('Changed on'),
                'dateFormat', \Zend_Date::DATE_MEDIUM,
                'default', '',
                'formatFunction', array($translated, 'describeDateFromNow')
                );
        $this->setIfExists('gr2o_changed_by', 'label', $this->_('Changed by'),
                'multiOptions', $changers
                );
        $this->setIfExists('gr2o_created',    'label', $this->_('Creation date'),
                'dateFormat', \Zend_Date::DATE_MEDIUM,
                'default', '',
                'formatFunction', array($translated, 'describeDateFromNow')
                );
        $this->setIfExists('gr2o_created_by', 'label', $this->_('Creation by'),
                'multiOptions', $changers
                );

        return $this;
    }

    /**
     * Set those values needed for editing
     *
     * @param boolean $create True when creating
     * @return \Gems_Model_RespondentModel
     */
    public function applyEditSettings($create = false)
    {
        $this->applyDetailSettings();
        $this->copyKeys(); // The user can edit the keys.

        $translated = $this->util->getTranslated();
        $ucfirst    = new \Zend_Filter_Callback('ucfirst');

        if ($create && ($this->hashSsn !== self::SSN_HIDE)) {
            $onblur = new \MUtil_Html_JavascriptArrayAttribute('onblur');
            $onblur->addSubmitOnChange('this.value');

            $this->set('grs_ssn',
                    'onblur', $onblur->render($this->view),  // Render needed as element does not know HtmlInterface
                    'validator[]', $this->createUniqueValidator('grs_ssn')
                    );
        }

        $this->set('gr2o_id_organization', 'default', $this->currentUser->getCurrentOrganizationId());
        if (! $create) {
            $this->set('gr2o_id_organization', 'elementClass', 'Exhibitor');
        }
        $this->setIfExists('gr2o_patient_nr',
                'size', 15,
                'minlength', 4,
                'validators[unique]', $this->createUniqueValidator(
                        array('gr2o_patient_nr', 'gr2o_id_organization'),
                        array('gr2o_id_user' => 'grs_id_user', 'gr2o_id_organization')
                        ),
                'validators[regex]', new Zend_Validate_Regex('/^[^\/\\%&]*$/'), // Between start and end no \/%& allowed
                'validators[csvinj]', 'NoCsvInjectionChars'
                );
        $this->set('grs_id_user');

        $this->set('gr2o_email', 'filter', new Gems_Filter_CleanEmail,
                'required', true,
                'autoInsertNotEmptyValidator', false, // Make sure it works ok with next
                'size', 30,
                'validator', 'SimpleEmail');
        $this->addColumn('CASE WHEN gr2o_email IS NULL OR LENGTH(TRIM(gr2o_email)) = 0 THEN 1 ELSE 0 END', 'calc_email');
        $this->set('calc_email',
                'label', $this->_('Respondent has no e-mail'),
                'elementClass', 'Checkbox',
                'required', true,
                'order', $this->getOrder('gr2o_email') + 1,
                'validator', new \Gems_Validate_OneOf(
                        $this->_('Respondent has no e-mail'),
                        'gr2o_email',
                        $this->get('gr2o_email', 'label')
                        )
                );
        $this->set('gr2o_mailable',
                'label', $this->_('May be mailed'),
                'elementClass', 'radio',
                'multiOptions', $this->util->getDbLookup()->getRespondentMailCodes(),
                'separator', ' '
                );

        $this->setIfExists('grs_first_name', 'filter', $ucfirst,
                'validators[csvinj]', 'NoCsvInjectionChars'
                );
        $this->setIfExists('grs_surname_prefix',
                'validators[csvinj]', 'NoCsvInjectionChars'
                );
        $this->setIfExists('grs_last_name',  'filter', $ucfirst,
                'required', true,
                'validators[csvinj]', 'NoCsvInjectionChars'
                );
        $this->setIfExists('grs_partner_surname_prefix',
                'validators[csvinj]', 'NoCsvInjectionChars'
                );
        $this->setIfExists('grs_partner_last_name',  'filter', $ucfirst,
                'validators[csvinj]', 'NoCsvInjectionChars'
                );

        $this->setIfExists('grs_gender',
                'elementClass', 'Radio',
                'separator', '',
                'multiOptions', $translated->getGenders(),
                'tab', $this->_('Medical data')
                );

        $this->setIfExists('grs_birthday',
                'jQueryParams', array('defaultDate' => '-30y', 'maxDate' => 0, 'yearRange' => 'c-130:c0'),
                // 'dateFormat', 'dd-MM-yyyy',
                'elementClass', 'Date',
                'validator', new \MUtil_Validate_Date_DateBefore()
                );

        $this->setIfExists('gr2o_treatment', 'size', 30);
        $this->setIfExists('gr2o_comments',  'elementClass', 'Textarea', 'rows', 4, 'cols', 60);

        $this->setIfExists('grs_address_1',
                'size',  40,
                'description', $this->_('With housenumber'),
                'filter', $ucfirst
                );
        $this->setIfExists('grs_address_2', 'size', 40);
        $this->setIfExists('grs_city', 'filter', $ucfirst);
        $this->setIfExists('grs_phone_1', 'size', 15);
        $this->setIfExists('grs_phone_2', 'size', 15);
        $this->setIfExists('grs_phone_3', 'size', 15);
        $this->setIfExists('grs_phone_4', 'size', 15);

        $this->setIfExists('gr2o_consent',
                'default', $this->util->getDefaultConsent(),
                'elementClass', 'Radio',
                'separator', ' ',
                'required', true);

        $this->setMulti(array('name', 'row_class', 'resp_deleted'), 'elementClass', 'None');
        $this->setMulti($this->getItemsFor('table', 'gems__reception_codes'), 'elementClass', 'None');

        if ($create) {
            $this->setIfExists('gr2o_changed',    'elementClass', 'None');
            $this->setIfExists('gr2o_changed_by', 'elementClass', 'None');
            $this->setIfExists('gr2o_created',    'elementClass', 'None');
            $this->setIfExists('gr2o_created_by', 'elementClass', 'None');
        } else {
            $this->setIfExists('gr2o_changed',    'elementClass', 'Exhibitor');
            $this->setIfExists('gr2o_changed_by', 'elementClass', 'Exhibitor');
            $this->setIfExists('gr2o_created',    'elementClass', 'Exhibitor');
            $this->setIfExists('gr2o_created_by', 'elementClass', 'Exhibitor');
        }

        return $this;
    }

    /**
     * Apply hash function for array_walk_recursive in _checkFilterUsed()
     *
     * @see _checkFilterUsed()
     *
     * @param string $filterValue
     * @param string $filterKey
     */
    public function applyHash(&$filterValue, $filterKey)
    {
        if ('grs_ssn' === $filterKey) {
            $filterValue = $this->project->getValueHash($filterValue, $this->hashAlgorithm);
        }
    }

    public function copyKeys($reset = false)
    {
        $keys = $this->_getKeysFor('gems__respondent2org');

        foreach ($keys as $key) {
            $this->addColumn('gems__respondent2org.' . $key, $this->getKeyCopyName($key));
        }

        return $this;
    }

    /**
     * Copy a respondent to a new organization
     *
     * If you want to share a respondent(id) with another organization use this
     * method.
     *
     * @param int $fromOrgId            Id of the sending organization
     * @param string $fromPid           Respondent number of the sending organization
     * @param int $toOrgId              Id of the receiving organization
     * @param string $toPid             Respondent number of the sending organization
     * @param bool $keepConsent         Should new organization inherit the consent of the old organization or not?
     * @return array The new respondent
     * @throws \Gems_Exception|RespondentAlreadyExists
     */
    public function copyToOrg($fromOrgId, $fromPid, $toOrgId, $toPid, $keepConsent = false)
    {
        // Maybe we should disable masking, just to be sure
        $this->currentUser->disableMask();

        // Do some sanity checks
        $fromPatient = $this->loadFirst(['gr2o_id_organization' => $fromOrgId, 'gr2o_patient_nr' => $fromPid]);
        if (empty($fromPatient)) {
            throw new \Gems_Exception($this->_('Respondent not found in sending organization.'));
        }

        $toPatientByPid        = $this->loadFirst(['gr2o_id_organization' => $toOrgId, 'gr2o_patient_nr' => $toPid]);
        $toPatientByRespondent = $this->loadFirst(['gr2o_id_organization' => $toOrgId, 'gr2o_id_user' => $fromPatient['gr2o_id_user']]);
        if (!empty($toPatientByPid) && $toPatientByPid['gr2o_id_user'] == $fromPatient['gr2o_id_user']) {
            // We already have the same respondent, nothing to do
            throw new RespondentAlreadyExists($this->_('Respondent already exists in destination organization.'), 200, null, RespondentAlreadyExists::SAME);
        }
        if (!empty($toPatientByPid) && empty($toPatientByRespondent)) {
            // Could be the same, or someone else... just return an error for now maybe offer to mark as duplicate and merge records later on
            throw new RespondentAlreadyExists($this->_('Respondent with requested respondent number already exists in receiving organization.'), 200, null, RespondentAlreadyExists::OTHERUID);
        }
        if (!empty($toPatientByRespondent)) {
            // No action needed, maybe also report the number we found?
            throw new RespondentAlreadyExists($this->_('Respondent already exists in destination organization, but with different respondent number.'), 200, null, RespondentAlreadyExists::OTHERPID);
        }

        // Ready to go, unset consent if needed
        $toPatient = $fromPatient;
        if (!$keepConsent) {
            unset($toPatient['gr2o_consent']);
        }

        // And save the record
        $toPatient['gr2o_patient_nr']      = $toPid;
        $toPatient['gr2o_id_organization'] = $toOrgId;
        $toPatient['gr2o_reception_code']  = \GemsEscort::RECEPTION_OK;

        $loginToSaveTable = false;
        if (isset($this->_saveTables['gems__user_logins'])) {
            $loginToSaveTable = $this->_saveTables['gems__user_logins'];
            unset($this->_saveTables['gems__user_logins']);
        }
        $result = $this->save($toPatient);

        if ($loginToSaveTable) {
            $this->_saveTables['gems__user_logins'] = $loginToSaveTable;
        }

        // Now re-enable the mask feature
        $this->currentUser->enableMask();

        return $result;
    }

    /**
     * Count the number of tracks the respondent has for this organization
     *
     * @param string $patientId   Can be empty if $respondentId is passed
     * @param int $organizationId When null looks at all organizations
     * @param int $respondentId   Pass when at hand, is looked up otherwise
     * @param boolean $active     When true only tracks with a success code are returned
     * @return boolean
     */
    public function countTracks($patientId, $organizationId, $respondentId = null, $active = false)
    {
        $db = $this->getAdapter();
        $select = $db->select();
        $select->from('gems__respondent2track', array('COALESCE(COUNT(*), 0)'));

        if (null === $respondentId) {
            $respondentId = $this->util->getDbLookup()->getRespondentId($patientId, $organizationId);
        }

        $select->where('gr2t_id_user = ?', $respondentId);

        if (null !== $organizationId) {
            $select->where('gr2t_id_organization = ?', $organizationId);
        }

        if ($active) {
            $select->joinInner('gems__reception_codes', 'gr2t_reception_code = grc_id_reception_code')
                    ->where('grc_success = 1');
        }

        return $db->fetchOne($select);
    }

    /**
     * Return a list of those who could be on the created_by, changed_by or opened_by fields
     *
     * @return array id => name
     */
    protected function getChangersList()
    {
        return $this->util->getDbLookup()->getStaff();
    }

    /**
     * Get the current reception code for a respondent
     *
     * @param string $patientId Can be empty if $respondentId is passed
     * @param int $organizationId
     * @param int $respondentId   Pass when at hand, is looked up otherwise
     * @return string The current reception code
     */
    public function getReceptionCode($patientId, $organizationId, $respondentId = null)
    {
        $db     = $this->getAdapter();
        $select = $db->select();
        $select->from('gems__respondent2org', array('gr2o_reception_code'));

        if ($patientId) {
            $select->where('gr2o_patient_nr = ?', $patientId);
        } else {
            $select->where('gr2o_id_user = ?', $respondentId);
        }

        $select->where('gr2o_id_organization = ?', $organizationId);

        return $db->fetchOne($select);
    }

    /**
     * Create a database model for retrieving the respondent tracks
     *
     * @return \Gems_Model_JoinModel A NEW JoinModel, not a continuation pattern return
     */
    public function getRespondentTracksModel()
    {
        $model = new \Gems_Model_JoinModel('surveys', 'gems__respondent2track');
        $model->addTable('gems__tracks', array('gr2t_id_track' => 'gtr_id_track'));
        $model->addTable('gems__respondent2org', array('gr2t_id_user' => 'gr2o_id_user'));

        return $model;
    }

    /**
     *
     * @param string $patientId
     * @param int|\Gems_User_Organization $organization
     * @param int $respondentId
     * @return boolean True when something changed
     */
    public function handleRespondentChanged($patientId, $organization, $respondentId = null)
    {
        if ($organization instanceof \Gems_User_Organization) {
            $org   = $organization;
            $orgId = $organization->getId();
        } else {
            $org   = $this->loader->getOrganization($organization);
            $orgId = $organization;
        }

        $changeEventClass = $org->getRespondentChangeEventClass();

        if ($changeEventClass) {
            $event = $this->loader->getEvents()->loadRespondentChangedEvent($changeEventClass);

            if ($event) {
                $respondent = $this->loader->getRespondent($patientId, $orgId, $respondentId);

                if ($event->processChangedRespondent($respondent)) {
                    // If no change was registered yet, do so now
                    if (! $this->getChanged()) {
                        $this->addChanged();
                    }

                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Has the respondent tracks
     *
     * @param string $patientId   Can be empty if $respondentId is passed
     * @param int $organizationId When null looks at all organizations
     * @param int $respondentId   Pass when at hand, is looked up otherwise
     * @param boolean $active     When true we look only for tracks with a success code
     * @return boolean
     */
    public function hasTracks($patientId, $organizationId, $respondentId = null, $active = false)
    {
        return (boolean) $this->countTracks($patientId, $organizationId, $respondentId, $active);
    }

    /**
     * Return a hashed version of the input value.
     *
     * @param mixed $value The value being saved
     * @param boolean $isNew True when a new item is being saved
     * @param string $name The name of the current field
     * @param array $context Optional, the other values being saved
     * @return string The output to display
     */
    public function hideSSN($value, $isNew = false, $name = null, array $context = array(), $isPost = false)
    {
        if ($value && (! $isPost)) {
            $this->set('grs_ssn', 'description', $this->_('Empty this field to remove the SSN'));
            return str_repeat('*', 9);
        } else {
            return $value;
        }
    }

    /**
     * True when the default filter can contain multiple organizations
     *
     * @return boolean
     */
    public function isMultiOrganization()
    {
        return $this->currentUser->hasPrivilege('pr.respondent.multiorg');
    }

    /**
     *
     * @param array $newValues The values to store for a single model item.
     * @param int $userId A user id or otherwise the current user is used
     * @return int Number of consent changes logged
     */
    public function logConsentChanges(array $newValues, $userId = null)
    {
        $changes = 0;
        foreach ($this->consentFields as $consent) {
            $oldConsent = 'old_' . $consent;
            if (isset($newValues['gr2o_id_user'], $newValues['gr2o_id_organization'], $newValues[$consent]) &&
                    array_key_exists($oldConsent, $newValues) &&  // Old consent can be empty
                    ($newValues[$consent] != $newValues[$oldConsent]) ) {

                $values['glrc_id_user']         = $newValues['gr2o_id_user'];
                $values['glrc_id_organization'] = $newValues['gr2o_id_organization'];
                $values['glrc_consent_field']   = $consent;
                $values['glrc_old_consent']     = $newValues[$oldConsent];
                $values['glrc_new_consent']     = $newValues[$consent];
                $values['glrc_created']         = new \MUtil_Db_Expr_CurrentTimestamp();
                $values['glrc_created_by']      = $userId ?: $this->currentUser->getUserId();

                $this->db->insert('gems__log_respondent_consents', $values);
                $changes++;
            }
        }

        return $changes;
    }

    /**
     * Merge two patients (in the same organization)
     *
     * A respondent can only exist twice in the same organization when the respondent
     * has multiple respondent id's. When ssn is set correctly this can not happen
     * so probably one with and one without or both without ssn.
     *
     * @param string $newPid
     * @param string $oldPid
     * @param int $orgId
     *
     * @return \Gems\Model\MergeResult | false The result or false in case of failure
     */
    public function merge($newPid, $oldPid, $orgId)
    {
        // Maybe we should disable masking, just to be sure
        $this->currentUser->disableMask();

        $patients = $this->load([
            'gr2o_id_organization' => $orgId,
            'gr2o_patient_nr'      => array($oldPid, $newPid)
        ]);

        // Now re-enable the mask feature
        $this->currentUser->enableMask();

        $cnt = count($patients);

        switch ($cnt) {
            case 1:
                // We only have one, check if it is the new number
                $patient = reset($patients);
                if ($patient['gr2o_patient_nr'] == $newPid) {
                    return \Gems\Model\MergeResult::FIRST;
                }

                // Not the new number, we can simply move
                $this->move($orgId, $oldPid, $orgId, $newPid);
                return \Gems\Model\MergeResult::SECOND;
                break;

            case 2:
                // We really need to merge all related records for the patients
                $patient = $patients[0];
                if ($patient['gr2o_patient_nr'] == $newPid) {
                    $newPatient = $patient;
                    $oldPatient = $patients[1];
                } else {
                    $oldPatient = $patient;
                    $newPatient = $patients[1];
                }

                // Due to key contraints the respondent id's should be different but check anyway
                if ($oldPatient['grs_id_user'] !== $newPatient['grs_id_user']) {
                    // It could be that the 'old' patient has a ssn, this could lead to problems later
                    // To prevent this we clear the ssn for the old patient
                    if (!empty($oldPatient['grs_ssn'])) {
                        $oldPatient['grs_ssn'] = '';
                        $changed = $this->db->update(
                                'gems__respondents',
                                ['grs_ssn' => null],
                                ['grs_id_user = ?' => $oldPatient['grs_id_user']]
                                );
                        // We seem to be unable to save an empty ssn
                        //$this->save($oldPatient);
                    }
                }

                $tables = array(
                    'gems__respondent2track'              => ['gr2t_id_user',      'gr2t_id_organization'],
                    'gems__tokens'                        => ['gto_id_respondent', 'gto_id_organization'],
                    'gems__appointments'                  => ['gap_id_user',       'gap_id_organization'],
                    'gems__log_respondent_communications' => ['grco_id_to',        'grco_organization'],
                    'gems__respondent_relations'          => ['grr_id_respondent', null],
                    'gems__log_activity'                  => ['gla_respondent_id', 'gla_organization']
                );

                $changed   = 0;
                $currentTs = new \MUtil_Db_Expr_CurrentTimestamp();
                $userId    = $this->currentUser->getUserId();

                foreach ($tables as $tableName => $settings) {
                    list($respIdField, $orgIdField) = $settings;

                    $start = \MUtil_String::beforeChars($respIdField, '_');

                    $values = [
                        $respIdField           => $newPatient['grs_id_user'],
                        $start . '_changed'    => $currentTs,
                        $start . '_changed_by' => $userId,
                        ];

                    if ($tableName == 'gems__log_activity') {
                        unset($values[$start . '_changed']);
                        unset($values[$start . '_changed_by']);
                    }

                    $where = [];
                    $where["$respIdField = ?"] = $oldPatient['grs_id_user'];
                    if (!is_null($orgIdField)) {
                        $where["$orgIdField = ?"] = $orgId;
                    };
                    $changed += $this->db->update($tableName, $values, $where);
                }

                return \Gems\Model\MergeResult::BOTH;
                break;

            default:
                // Not found
                return \Gems\Model\MergeResult::NONE;
                break;
        }

        return false;
    }

    /**
     * Move a respondent to a new organization and/or change it's number
     *
     * This not be a copy, it will be renamed. Use copyToOrg if you want to make a copy.
     *
     * @param int $fromOrgId            Id of the sending organization
     * @param string $fromPid           Respondent number of the sending organization
     * @param int $toOrgId              Id of the receiving organization
     * @param string $toPid             Respondent number of the sending organization
     * @return array The new respondent
     * @throws RespondentAlreadyExists
     */
    public function move($fromOrgId, $fromPid, $toOrgId, $toPid)
    {
        // Maybe we should disable masking, just to be sure
        $this->currentUser->disableMask();

        $patientFrom = $this->loadFirst([
            'gr2o_id_organization' => $fromOrgId,
            'gr2o_patient_nr'      => $fromPid
        ]);
        $patientTo = $this->loadFirst([
            'gr2o_id_organization' => $toOrgId,
            'gr2o_patient_nr'      => $toPid
        ]);

        if ($fromPid !== $toPid) {
            $patientFrom['gr2o_patient_nr'] = $toPid;
            $copyKey = $this->getKeyCopyName('gr2o_patient_nr');
            $patientFrom[$copyKey] = $fromPid;
        }
        if ($fromOrgId !== $toOrgId) {
            $patientFrom['gr2o_id_organization'] = $toOrgId;
            $copyKey = $this->getKeyCopyName('gr2o_id_organization');
            $patientFrom[$copyKey] = $fromOrgId;
        }

        if (empty($patientTo)) {
            $result = $this->save($patientFrom);
        } else {
            // already exists, return current patient
            // we can not delete the other records, maybe mark as inactive or throw an error
            $result = $patientTo;
            $this->currentUser->enableMask();
            throw new RespondentAlreadyExists($this->_('Respondent already exists in destination, please delete current record manually if needed.'), 200, null, RespondentAlreadyExists::OTHERUID);
        }

        // Now re-enable the mask feature
        $this->currentUser->enableMask();

        return $result;
    }

    /**
     * Save a single model item.
     *
     * @param array $newValues The values to store for a single model item.
     * @param array $filter If the filter contains old key values these are used
     * to decide on update versus insert.
     * @return array The values as they are after saving (they may change).
     */
    public function save(array $newValues, array $filter = null, array $saveTables = null)
    {
        // If the respondent id is not set, check using the
        // patient number and then the ssn
        if (! (isset($newValues['grs_id_user']) && $newValues['grs_id_user'])) {
            $id = false;

            if (isset($newValues['gr2o_patient_nr'], $newValues['gr2o_id_organization'])) {
                $sql = 'SELECT gr2o_id_user
                        FROM gems__respondent2org
                        WHERE gr2o_patient_nr = ? AND gr2o_id_organization = ?';

                $id = $this->db->fetchOne($sql, array($newValues['gr2o_patient_nr'], $newValues['gr2o_id_organization']));
            }

            if ((!$id) &&
                    isset($newValues['grs_ssn']) && !empty($newValues['grs_ssn']) &&
                    ($this->hashSsn !== self::SSN_HIDE)) {

                if (self::SSN_HASH === $this->hashSsn) {
                    $search = $this->saveSSN($newValues['grs_ssn']);
                } else {
                    $search = $newValues['grs_ssn'];
                }

                $sql = 'SELECT grs_id_user FROM gems__respondents WHERE grs_ssn = ?';

                $id = $this->db->fetchOne($sql, $search);

                // Check for change in patient ID
                if ($id && isset($newValues['gr2o_id_organization'])) {

                    $sql = 'SELECT gr2o_patient_nr
                            FROM gems__respondent2org
                            WHERE gr2o_id_user = ? AND gr2o_id_organization = ?';

                    $patientId = $this->db->fetchOne($sql, array($id, $newValues['gr2o_id_organization']));

                    if ($patientId) {
                        $copyId             = $this->getKeyCopyName('gr2o_patient_nr');
                        $newValues[$copyId] = $patientId;
                    }
                }
            }

            if ($id) {
                $newValues['grs_id_user']  = $id;
                $newValues['gr2o_id_user'] = $id;
            }
            // If empty, then set by \Gems_Model->createGemsUserId()
        }

        $result = parent::save($newValues, $filter, $saveTables);

        $this->logConsentChanges($result);

        if (isset($result['gr2o_id_organization']) && isset($result['grs_id_user'])) {
            // Tell the organization it has at least one user
            $org = $this->loader->getOrganization($result['gr2o_id_organization']);
            if ($org) {
                $org->setHasRespondents($this->currentUser->getUserId());
            }

            $this->handleRespondentChanged($result['gr2o_patient_nr'], $org, $result['grs_id_user']);
        }

        return $result;
    }

    /**
     * Return a hashed version of the input value.
     *
     * @param mixed $value The value being saved
     * @param boolean $isNew True when a new item is being saved
     * @param string $name The name of the current field
     * @param array $context Optional, the other values being saved
     * @return string The salted hash as a 32-character hexadecimal number.
     */
    public function saveSSN($value, $isNew = false, $name = null, array $context = array())
    {
        if ($value) {
            return $this->project->getValueHash($value, $this->hashAlgorithm);
        }
    }

    /**
     * Set the reception code for a respondent and cascade non-success codes to the
     * tracks / surveys.
     *
     * @param string $patientId   Can be empty if $respondentId is passed
     * @param int $organizationId
     * @param string $newCode     String or \Gems_Util_ReceptionCode
     * @param int $respondentId   Pass when at hand, is looked up otherwise
     * @param string $oldCode     Pass when at hand as string or \Gems_Util_ReceptionCode, is looked up otherwise
     * @return \Gems_Util_ReceptionCode The new code reception code object for further processing
     */
    public function setReceptionCode($patientId, $organizationId, $newCode, $respondentId = null, $oldCode = null)
    {
        if (!$newCode instanceof \Gems_Util_ReceptionCode) {
            $newCode = $this->util->getReceptionCode($newCode);
        }
        $userId = $this->currentUser->getUserId();

        // Perform actual save, but not for simple stop codes.
        if ($newCode->isForRespondents()) {
            if (null === $oldCode) {
                $oldCode = $this->getReceptionCode($patientId, $organizationId, $respondentId);
            }
            if ($oldCode instanceof \Gems_Util_ReceptionCode) {
                $oldCode = $oldCode->getCode();
            }

            // If the code wasn't set already
            if ($oldCode !== $newCode->getCode()) {
                $values['gr2o_reception_code'] = $newCode->getCode();
                $values['gr2o_changed']        = new \MUtil_Db_Expr_CurrentTimestamp();
                $values['gr2o_changed_by']     = $userId;

                if ($patientId) {
                    // Update though primamry key is prefered
                    $where = 'gr2o_patient_nr = ? AND gr2o_id_organization = ?';
                    $where = $this->db->quoteInto($where, $patientId, null, 1);
                } else {
                    $where = 'gr2o_id_user = ? AND gr2o_id_organization = ?';
                    $where = $this->db->quoteInto($where, $respondentId, null, 1);
                }
                $where = $this->db->quoteInto($where, $organizationId, null, 1);

                $this->db->update('gems__respondent2org', $values, $where);
            }
        }

        // Is the respondent really removed
        if (! $newCode->isSuccess()) {
            // Only check for $respondentId when it is really needed
            if (null === $respondentId) {
                $respondentId = $this->util->getDbLookup()->getRespondentId($patientId, $organizationId);
            }

            // Cascade to tracks
            // the responsiblilty to handle it correctly is on the sub objects now.
            $tracks = $this->loader->getTracker()->getRespondentTracks($respondentId, $organizationId);
            foreach ($tracks as $track) {
                if ($track->setReceptionCode($newCode, null, $userId)) {
                    $this->addChanged();
                }
            }
        }

        if ($newCode->isForRespondents()) {
            $this->handleRespondentChanged($patientId, $organizationId, $respondentId);
        }

        return $newCode;
    }

    /**
     * Return a hashed version of the input value.
     *
     * @param mixed $value The value being saved
     * @param boolean $isNew True when a new item is being saved
     * @param string $name The name of the current field
     * @param array $context Optional, the other values being saved
     * @return boolean
     */
    public function whenSSN($value, $isNew = false, $name = null, array $context = array())
    {
        if (! $value) {
            return true;
        }
        return $value && ($value !== $this->hideSSN($value, $isNew, $name, $context));
    }
}