HEX
Server: Apache
System: Linux WWW 6.1.0-40-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.153-1 (2025-09-20) x86_64
User: web11 (1011)
PHP: 8.2.29
Disabled: NONE
Upload Files
File: /var/www/apklausos/application/models/Question.php
<?php

/*
* LimeSurvey
* Copyright (C) 2013-2022 The LimeSurvey Project Team / Carsten Schmitz
* All rights reserved.
* License: GNU/GPL License v2 or later, see LICENSE.php
* LimeSurvey is free software. This version may have been modified pursuant
* to the GNU General Public License, and as distributed it includes or
* is derivative of works licensed under the GNU General Public License or
* other free or open source software licenses.
* See COPYRIGHT.php for copyright notices and details.
*
*/

use LimeSurvey\Helpers\questionHelper;

/**
 * Class Question
 *
 * @property integer $qid Question ID.
 * @property integer $sid Survey ID
 * @property integer $gid QuestionGroup ID where question is displayed
 * @property string $type
 * @property string $title Question Code
 * @property string $preg
 * @property string $other Other option enabled for question (Y/N)
 * @property string $mandatory Whether question is mandatory (Y/S/N)
 * @property string $encrypted Whether question is encrypted (Y/N)
 * @property string $question_theme_name
 * @property integer $question_order Question order in greoup
 * @property integer $parent_qid Questions parent question ID eg for subquestions
 * @property integer $scale_id  The scale ID
 * @property integer $same_default Saves if user set to use the same default value across languages in default options dialog ('Edit default answers')
 * @property string $relevance Questions relevane equation
 * @property string $modulename
 * @property integer $same_script Whether the same script should be used for all languages
 *
 * @property Survey $survey
 * @property QuestionGroup $group
 * @property Question $parent
 * @property Question[] $subquestions
 * @property QuestionAttribute[] $questionAttributes NB! returns all QuestionArrtibute Models fot this QID regardless of the specified language
 * @property QuestionL10n[] $questionl10ns Question Languagesettings indexd by language code
 * @property string[] $quotableTypes Question types that can be used for quotas
 * @property Answer[] $answers
 * @property QuestionType $questionType
 * @property array $allSubQuestionIds QID-s of all question subquestions, empty array returned if no subquestions
 * @inheritdoc
 */
class Question extends LSActiveRecord
{
    const QT_1_ARRAY_DUAL = '1'; // Array Dual scale
    const QT_5_POINT_CHOICE = '5';
    const QT_A_ARRAY_5_POINT = 'A'; // Array of 5 point choice questions
    const QT_B_ARRAY_10_CHOICE_QUESTIONS = 'B'; // Array of 10 point choice questions
    const QT_C_ARRAY_YES_UNCERTAIN_NO = 'C'; // ARRAY OF YES\No\gT("Uncertain") QUESTIONS
    const QT_D_DATE = 'D';
    const QT_E_ARRAY_INC_SAME_DEC = 'E';
    const QT_F_ARRAY = 'F';
    const QT_G_GENDER = 'G';
    const QT_H_ARRAY_COLUMN = 'H';
    const QT_I_LANGUAGE = 'I';
    const QT_K_MULTIPLE_NUMERICAL = 'K';
    const QT_L_LIST = 'L';
    const QT_M_MULTIPLE_CHOICE = 'M';
    const QT_N_NUMERICAL = 'N';
    const QT_O_LIST_WITH_COMMENT = 'O';
    const QT_P_MULTIPLE_CHOICE_WITH_COMMENTS = 'P';
    const QT_Q_MULTIPLE_SHORT_TEXT = 'Q';
    const QT_R_RANKING = 'R';
    const QT_S_SHORT_FREE_TEXT = 'S';
    const QT_T_LONG_FREE_TEXT = 'T';
    const QT_U_HUGE_FREE_TEXT = 'U';
    const QT_X_TEXT_DISPLAY = 'X';
    const QT_Y_YES_NO_RADIO = 'Y';
    const QT_EXCLAMATION_LIST_DROPDOWN = '!';
    const QT_VERTICAL_FILE_UPLOAD = '|';
    const QT_ASTERISK_EQUATION = '*';
    const QT_COLON_ARRAY_NUMBERS = ':';
    const QT_SEMICOLON_ARRAY_TEXT = ';';

    const START_SORTING_VALUE = 1; //this is the start value for question_order

    const DEFAULT_QUESTION_THEME = 'core';  // The question theme name to use when no theme is specified

    /** @var string $group_name Stock the active group_name for questions list filtering */
    public $group_name;
    public $gid;
    /** Defaut relevance **/
    public $relevance = '';
    /** defaut same_script , avoid public break during update **/
    public $same_script = 0;

    /** @var QuestionTheme cached question theme*/
    private $relatedQuestionTheme;

    /**
     * @inheritdoc
     * @return Question
     */
    public static function model($className = __CLASS__)
    {
        /** @var self $model */
        $model = parent::model($className);
        return $model;
    }

    /** @inheritdoc */
    public function tableName()
    {
        return '{{questions}}';
    }

    /** @inheritdoc */
    public function primaryKey()
    {
        return 'qid';
    }

    /** @inheritdoc */
    public function relations()
    {
        return array(
            'survey' => array(self::BELONGS_TO, 'Survey', 'sid'),
            'group' => array(self::BELONGS_TO, 'QuestionGroup', 'gid', 'together' => true),
            'parent' => array(self::HAS_ONE, 'Question', array("qid" => "parent_qid")),
            'questionattributes' => array(self::HAS_MANY, 'QuestionAttribute', 'qid'),
            'questionl10ns' => array(self::HAS_MANY, 'QuestionL10n', 'qid', 'together' => true),
            'subquestions' => array(
                self::HAS_MANY,
                'Question',
                array('parent_qid' => 'qid'),
                'order' => 'subquestions.question_order ASC',
                'together' => false
            ),
            'conditions' => array(self::HAS_MANY, 'Condition', 'qid'),
            'answers' => array(self::HAS_MANY, 'Answer', 'qid'),
            // This relation will fail for non saved questions, which is often the case
            // when using question editor on create mode. Better use getQuestionTheme()
            'question_theme' => [self::HAS_ONE, 'QuestionTheme', ['question_type' => 'type', 'name' => 'question_theme_name']],
        );
    }

    /**
     * @inheritdoc
     * replace under condition $oQuestion->survey to use Survey::$findByPkCache
     */
    public function getRelated($name, $refresh = false, $params = array())
    {
        if ($name == 'survey' && !$refresh && empty($params)) {
            return Survey::model()->findByPk($this->sid);
        }
        return parent::getRelated($name, $refresh, $params);
    }

    /**
     * @inheritdoc
     * TODO: make it easy to read (if possible)
     */
    public function rules()
    {
        /* Basic rules */
        $aRules = array(
            array('title', 'required', 'on' => 'update, insert, saveall', 'message' => gT('The question code is mandatory.', 'unescaped')),
            array('title', 'length', 'min' => 1, 'max' => 20, 'on' => 'update, insert, saveall'),
            array('qid,sid,gid,parent_qid', 'numerical', 'integerOnly' => true),
            array('qid', 'unique','message' => sprintf(gT("Question id (qid) : '%s' is already in use."), $this->qid)),// Still needed ?
            array('other', 'in', 'range' => array('Y', 'N'), 'allowEmpty' => true),
            array('mandatory', 'in', 'range' => array('Y', 'S', 'N'), 'allowEmpty' => true),
            array('encrypted', 'in', 'range' => array('Y', 'N'), 'allowEmpty' => true),
            array('question_order', 'numerical', 'integerOnly' => true, 'allowEmpty' => true),
            array('scale_id', 'numerical', 'integerOnly' => true, 'allowEmpty' => true),
            array('same_default', 'numerical', 'integerOnly' => true, 'allowEmpty' => true),
            array('type', 'length', 'min' => 1, 'max' => 1),
            array('relevance', 'LSYii_FilterValidator', 'filter' => 'trim', 'skipOnEmpty' => true),
            array('preg', 'safe'),
            array('modulename', 'length', 'max' => 255),
            array('same_script', 'numerical', 'integerOnly' => true, 'allowEmpty' => true),
        );
        /* Filtering */
        /* other must be no when other is not allowed */
        $aRules[] = array('other', 'filter', 'filter' => function ($value) {
            if ($this->getAllowOther()) {
                return $value;
            }
            return 'N';
        });
        /* Don't save empty or 'core' question theme name */
        $aRules[] = ['question_theme_name', 'filter', 'filter' =>  [$this, 'questionThemeNameValidator'] ];
        /* Specific rules to avoid collapse with column name in database */
        if ($this->parent_qid) {
            /* Subquestion specific rules */
            /* unicity of title by scale */
            $aRules[] = array('title', 'unique', 'caseSensitive' => false,
                'criteria' => array(
                    'condition' => 'sid=:sid AND parent_qid=:parent_qid and scale_id=:scale_id',
                    'params' => array(
                        ':sid' => $this->sid,
                        ':parent_qid' => $this->parent_qid,
                        ':scale_id' => $this->scale_id
                        )
                    ),
                    'message' => gT('Subquestion codes must be unique.'),
                    'except' => 'saveall'
            );
            /* Disallow other title if question allow other */
            $oParentQuestion = Question::model()->findByPk(array("qid" => $this->parent_qid));
            if ($oParentQuestion->other == "Y") {
                $aRules[] = array(
                    'title',
                    'LSYii_CompareInsensitiveValidator',
                    'compareValue' => 'other',
                    'operator' => '!=',
                    'message' => sprintf(gT("'%s' can not be used if the 'Other' option for this question is activated."), "other"),
                    'except' => 'archiveimport'
                );
            }
            /* #14495: comment suffix can't be used with P Question */
            if ($oParentQuestion->type == "P") {
                $aRules[] = array('title', 'match', 'pattern' => '/comment$/', 'not' => true, 'message' => gT("'comment' suffix can not be used with multiple choice with comments."));
            }
        } else {
            /* Question specific rules*/
            if ($this->getHasOtherSubquestions()) {
                // Disallow other if sub question have 'other' for title
                $aRules[] = array('other', 'compare', 'compareValue' => 'Y', 'operator' => '!=', 'message' => sprintf(gT("'%s' can not be used if the 'Other' option for this question is activated."), 'other'));
            }
        }
        if ($this->survey->isActive) {
            $aRules = array_merge($aRules, $this->rulesForActiveSurvey());
        }
        /* When question exist and are already set with title, allow keep bad title */
        if (!$this->isNewRecord) {
            $oActualValue = Question::model()->findByPk(array("qid" => $this->qid));
            if ($oActualValue && $oActualValue->title == $this->title) {
                /* We don't change title, then don't put rules on title */
                /* We don't want to broke existing survey,  We only disallow to set it or update it according to this value */
                return $aRules;
            }
        }
        /**
         * Question was new or title was updated : we add minor rules.
         * This rules don't broke DB, only potential “ExpressionScript Engine” issue.
         * usage of 'archiveimport' scenaruio for import LSA (survey archive) file
         **/
        if (empty($this->parent_qid)) {
            /* Unicity for ExpressionManager */
            $aRules[] = array('title', 'unique', 'caseSensitive' => true,
                'criteria' => array(
                    'condition' => 'sid=:sid AND parent_qid=0',
                    'params' => array(
                        ':sid' => $this->sid
                        )
                    ),
                'message' => gT('Question codes must be unique.'),
                'except' => 'archiveimport'
            );
            /* ExpressionManager basic rule */
            $aRules[] = array('title', 'match', 'pattern' => '/^[a-z,A-Z][[:alnum:]]*$/',
                'message' => gT('Question codes must start with a letter and may only contain alphanumeric characters.'),
                'except' => 'archiveimport'
            );
            /* ExpressionManager reserved word (partial) */
            $aRules[] = array('title', 'in', 'not' => true,
                'range' => array(
                    'LANG','SID', // Global var
                    'SAVEDID','TOKEN', // current survey related var
                    'QID','GID','SGQ', // current question related var
                    'self','that','this', // EM reserved variables
                ),
                'message' => sprintf(gT("Code: '%s' is a reserved word."), $this->title), // Usage of {attribute} need attributeLabels, {value} never exist in message
                'except' => 'archiveimport'
            );
        } else {
            $aRules[] = array(
                'title', 'compare', 'compareValue' => 'time', 'operator' => '!=',
                'message' => gT("'time' is a reserved word and can not be used for a subquestion."),
                'except' => 'archiveimport'
            );
            $aRules[] = array(
                'title', 'match', 'pattern' => '/^[a-zA-z0-9]*$/',
                'message' => gT('Subquestion codes may only contain alphanumeric characters.'),
                'except' => 'archiveimport'
            );
        }
        return $aRules;
    }

    /**
     * return rules specific for activated survey
     * @return array
     */
    private function rulesForActiveSurvey()
    {
        $aRules = array();
        /* can not update group */
        $aRules[] = array('gid', 'LSYii_DisableUpdateValidator');
        return $aRules;
    }

    /**
     * Rewrites sort order for questions in a group
     *
     * @static
     * @access public
     * @param int $gid
     * @param int $surveyid
     * @return void
     */
    public static function updateSortOrder($gid, $surveyid)
    {
        $questions = self::model()->findAllByAttributes(
            array('gid' => $gid, 'sid' => $surveyid, 'parent_qid' => 0),
            array('order' => 'question_order')
        );
        $p = 0;
        foreach ($questions as $question) {
            $question->question_order = $p;
            $question->save();
            $p++;
        }
    }


    /**
     * Fix sort order for questions in a group
     * All questions in the group will be assigned a sequential question order,
     * starting in the specified value
     * @param int $gid
     * @param int $startingOrder   the starting question order.
     */
    public function updateQuestionOrder($gid, $startingOrder = 1)
    {
        $data = Yii::app()->db->createCommand()->select('qid')
            ->where(array('and', 'gid=:gid', 'parent_qid=0'))
            ->order('question_order, title ASC')
            ->from('{{questions}}')
            ->bindParam(':gid', $gid, PDO::PARAM_INT)
            ->query();

        $position = intval($startingOrder);
        foreach ($data->readAll() as $row) {
            Yii::app()->db->createCommand()->update(
                $this->tableName(),
                array('question_order' => $position),
                'qid=' . $row['qid']
            );
            $position++;
        }
    }

    /**
     * This function returns an array of the advanced attributes for the particular question
     * including their values set in the database
     *
     * @param string|null $sLanguage If you give a language then only the attributes for that language are returned
     * @param string|null $sQuestionThemeOverride   Name of the question theme to use instead of the question's current theme
     * @return array
     */
    public function getAdvancedSettingsWithValues($sLanguage = null, $sQuestionThemeOverride = null)
    {
        $questionAttributeHelper = new LimeSurvey\Models\Services\QuestionAttributeHelper();
        $aAttributes = $questionAttributeHelper->getQuestionAttributesWithValues($this, $sLanguage, $sQuestionThemeOverride, true);
        return $aAttributes;
    }

    /**
     * Add custom attributes (if there are any custom attributes). It also removes all attributeNames where inputType is
     * empty. Otherwise (not adding and removing anything)it returns the incoming parameter $aAttributeNames.
     *
     * @param array $aAttributeNames  the values from getQuestionAttributesSettings($sType)
     * @param array $aAttributeValues  $attributeValues['question_template'] != 'core', only if this is true the function changes something
     * @param Question $oQuestion      this is needed to check if a questionTemplate has custom attributes
     * @return mixed  returns the incoming parameter $aAttributeNames or
     *
     * @deprecated use QuestionTheme::getAdditionalAttrFromExtendedTheme() to retrieve question theme attributes and
     *             QuestionAttributeHelper->mergeQuestionAttributes() to merge with base attributes.
     */
    public static function getQuestionTemplateAttributes($aAttributeNames, $aAttributeValues, $oQuestion)
    {
        if (isset($aAttributeValues['question_template']) && ($aAttributeValues['question_template'] != 'core')) {
            if (empty($oQuestion)) {
                throw new Exception('oQuestion cannot be empty');
            }
            $oQuestionTemplate = QuestionTemplate::getInstance($oQuestion);
            if ($oQuestionTemplate->bHasCustomAttributes) {
                // Add the custom attributes to the list
                foreach ($oQuestionTemplate->oConfig->attributes->attribute as $attribute) {
                    $sAttributeName = (string)$attribute->name;
                    $sInputType = (string)$attribute->inputtype;
                    // remove attribute if inputtype is empty
                    if (empty($sInputType)) {
                        unset($aAttributeNames[$sAttributeName]);
                    } else {
                        $aCustomAttribute = json_decode(json_encode((array)$attribute), 1);
                        $aCustomAttribute = array_merge(
                            QuestionAttribute::getDefaultSettings(),
                            array("category" => gT("Template")),
                            $aCustomAttribute
                        );
                        $aAttributeNames[$sAttributeName] = $aCustomAttribute;
                    }
                }
            }
        }
        return $aAttributeNames;
    }

    public function getTypeGroup()
    {
    }

    /**
     * TODO: replace this function call by $oSurvey->questions defining a relation in SurveyModel
     * @param integer $sid
     * @param integer $gid
     * @return CDbDataReader
     */
    public function getQuestions($sid, $gid)
    {
        return Yii::app()->db->createCommand()
            ->select()
            ->from(self::tableName())
            ->where(array('and', 'sid=:sid', 'gid=:gid', 'parent_qid=0'))
            ->order('question_order asc')
            ->bindParam(":sid", $sid, PDO::PARAM_INT)
            ->bindParam(":gid", $gid, PDO::PARAM_INT)
            //->bindParam(":language", $language, PDO::PARAM_STR)
            ->query();
    }

    /**
     * Delete a bunch of questions in one go
     *
     * @param mixed $questionsIds
     * @return void
     */
    public static function deleteAllById($questionsIds)
    {
        if (!is_array($questionsIds)) {
            $questionsIds = array($questionsIds);
        }

        Yii::app()->db->createCommand()->delete(Condition::model()->tableName(), array('in', 'qid', $questionsIds));
        Yii::app()->db->createCommand()->delete(QuestionAttribute::model()->tableName(), array('in', 'qid', $questionsIds));
        Yii::app()->db->createCommand()->delete(Answer::model()->tableName(), array('in', 'qid', $questionsIds));
        Yii::app()->db->createCommand()->delete(Question::model()->tableName(), array('in', 'parent_qid', $questionsIds));
        Yii::app()->db->createCommand()->delete(Question::model()->tableName(), array('in', 'qid', $questionsIds));
        Yii::app()->db->createCommand()->delete(DefaultValue::model()->tableName(), array('in', 'qid', $questionsIds));
        Yii::app()->db->createCommand()->delete(QuotaMember::model()->tableName(), array('in', 'qid', $questionsIds));
    }

    /**
     * Deletes a question and ALL its relations (subquestions, answers, etc, etc)
     * @return bool
     * @throws CDbException
     */
    public function delete()
    {
        $ids = array_merge([$this->qid], $this->allSubQuestionIds);
        $qidsCriteria = (new CDbCriteria())->addInCondition('qid', $ids);

        self::model()->deleteAll((new CDbCriteria())->addInCondition('parent_qid', $ids));
        QuestionAttribute::model()->deleteAll($qidsCriteria);
        QuestionL10n::model()->deleteAll($qidsCriteria);
        QuotaMember::model()->deleteAll($qidsCriteria);
        Condition::model()->deleteAll($qidsCriteria);

        // delete defaultvalues and defaultvalueL10ns
        $oDefaultValues = DefaultValue::model()->findAll((new CDbCriteria())->addInCondition('qid', $ids));
        foreach ($oDefaultValues as $defaultvalue) {
            DefaultValue::model()->deleteAll('dvid = :dvid', array(':dvid' => $defaultvalue->dvid));
            DefaultValueL10n::model()->deleteAll('dvid = :dvid', array(':dvid' => $defaultvalue->dvid));
        }

        $this->deleteAllAnswers();
        $this->removeFromLastVisited();

        if (parent::delete()) {
            Question::model()->updateQuestionOrder($this->gid);
            return true;
        }
        return false;
    }

    /**
     * remove question from lastVisited
     */
    public function removeFromLastVisited()
    {
        $oCriteria = new CDbCriteria();
        $oCriteria->compare('stg_name', 'last_question');
        $oCriteria->compare('stg_value', $this->qid);
        SettingsUser::model()->deleteAll($oCriteria);
    }

    /**
     * Delete all subquestions that belong to this question.
     *
     * @param ?array $exceptIds Don't delete subquestions with these ids.
     * @return void
     */
    public function deleteAllSubquestions($exceptIds = [])
    {
        $ids = !empty($exceptIds)
            ? array_diff($this->allSubQuestionIds, $exceptIds)
            : $this->allSubQuestionIds;

        $questions = Question::model()->findAll((new CDbCriteria())->addInCondition('qid', $ids));
        foreach ($questions as $question) {
            $question->delete();
        }
    }

    /**
     * Delete all question and its subQuestion Answers
     * @param array $exceptIds Don't delete answers with these ids.
     * @return void
     */
    public function deleteAllAnswers(array $exceptIds = [])
    {
        $ids = array_merge([$this->qid], $this->allSubQuestionIds);
        $qidsCriteria = (new CDbCriteria())->addInCondition('qid', $ids);
        $qidsCriteria->addNotInCondition('aid', $exceptIds);
        $answers = Answer::model()->findAll($qidsCriteria);
        if (!empty($answers)) {
            foreach ($answers as $answer) {
                $answerId = $answer->aid;
                if ($answer->delete()) {
                    AnswerL10n::model()->deleteAllByAttributes(['aid' => $answerId]);
                }
            }
        }
    }

    /**
     * TODO: replace it everywhere by Answer::model()->findAll([Critieria Object])
     * @param string $fields
     * @param mixed $condition
     * @param string|false $orderby
     * @return array
     */
    public function getQuestionsForStatistics($fields, $condition, $orderby = false)
    {
        if ($orderby === false) {
            $oQuestions = Question::model()->with('questionl10ns')->findAll(array('condition' => $condition));
        } else {
            $oQuestions = Question::model()->with('questionl10ns')->findAll(array('condition' => $condition, 'order' => $orderby));
        }
        $arr = array();
        foreach ($oQuestions as $key => $question) {
            $arr[$key] = array_merge($question->attributes, current($question->questionl10ns)->attributes);
        }
        return $arr;
    }

    /**
     * @param integer $surveyid
     * @param string $language
     * @return Question[]
     */
    public function getQuestionList($surveyid)
    {
        return Question::model()
            ->with('group')
            ->findAll(
                array(
                    'condition' => 't.sid=:sid',
                    /* table name not needed , see #17777 */
                    'order'     => 'group_order,question_order',
                    'params'    => array(':sid' => $surveyid)
                )
            );
    }

    /**
     * @return string
     * NOTE: Not used anymore. Based on a deprecated method. Should be deprecated.
     */
    public function getTypedesc()
    {
        $types = self::typeList();
        $typeDesc = $types[$this->type]["description"];

        if (YII_DEBUG) {
            $typeDesc .= ' <em>' . $this->type . '</em>';
        }

        return $typeDesc;
    }

    /**
     * This function contains the question type definitions.
     * @param string $language Language for translation
     * @return array The question type definitions
     *
     * Explanation of questiontype array:
     *
     * description : Question description
     * subquestions : 0= Does not support subquestions x=Number of subquestion scales
     * answerscales : 0= Does not need answers x=Number of answer scales (usually 1, but e.g. for dual scale question set to 2)
     * assessable : 0=Does not support assessment values when editing answerd 1=Support assessment values
     * @deprecated use QuestionTheme::findQuestionMetaDataForAllTypes() instead
     */
    public static function typeList($language = null)
    {
        $QuestionTypes = QuestionType::modelsAttributes($language);

        /**
         * @todo Check if this actually does anything, since the values are arrays.
         */
        asort($QuestionTypes);

        return $QuestionTypes;
    }

    /**
     * This function return the name by question type
     * @param string question type
     * @return string Question type name
     *
     * Maybe move class in typeList ?
     * @deprecated use $this->>questionType->description instead
     */
    public static function getQuestionTypeName($sType)
    {
        $typeList = self::typeList();
        return $typeList[$sType]['description'];
    }

    /**
     * This function return the class by question type
     * @param string question type
     * @return string Question class to be added to the container
     *
     * Maybe move class in typeList ?
     * //TODO move to QuestionType
     */
    public static function getQuestionClass($sType)
    {
        switch ($sType) {
            case Question::QT_1_ARRAY_DUAL:
                return 'array-flexible-dual-scale';
            case Question::QT_5_POINT_CHOICE:
                return 'choice-5-pt-radio';
            case Question::QT_A_ARRAY_5_POINT:
                return 'array-5-pt';
            case Question::QT_B_ARRAY_10_CHOICE_QUESTIONS:
                return 'array-10-pt';
            case Question::QT_C_ARRAY_YES_UNCERTAIN_NO:
                return 'array-yes-uncertain-no';
            case Question::QT_D_DATE:
                return 'date';
            case Question::QT_E_ARRAY_INC_SAME_DEC:
                return 'array-increase-same-decrease';
            case Question::QT_F_ARRAY:
                return 'array-flexible-row';
            case Question::QT_G_GENDER:
                return 'gender';
            case Question::QT_H_ARRAY_COLUMN:
                return 'array-flexible-column';
            case Question::QT_I_LANGUAGE:
                return 'language';
            case Question::QT_K_MULTIPLE_NUMERICAL:
                return 'numeric-multi';
            case Question::QT_L_LIST:
                return 'list-radio';
            case Question::QT_M_MULTIPLE_CHOICE:
                return 'multiple-opt';
            case Question::QT_N_NUMERICAL:
                return 'numeric';
            case Question::QT_O_LIST_WITH_COMMENT:
                return 'list-with-comment';
            case Question::QT_P_MULTIPLE_CHOICE_WITH_COMMENTS:
                return 'multiple-opt-comments';
            case Question::QT_Q_MULTIPLE_SHORT_TEXT:
                return 'multiple-short-txt';
            case Question::QT_R_RANKING:
                return 'ranking';
            case Question::QT_S_SHORT_FREE_TEXT:
                return 'text-short';
            case Question::QT_T_LONG_FREE_TEXT:
                return 'text-long';
            case Question::QT_U_HUGE_FREE_TEXT:
                return 'text-huge';
            case Question::QT_X_TEXT_DISPLAY:
                return 'boilerplate';
            case Question::QT_Y_YES_NO_RADIO:
                return 'yes-no';
            case Question::QT_EXCLAMATION_LIST_DROPDOWN:
                return 'list-dropdown';
            case Question::QT_COLON_ARRAY_NUMBERS:
                return 'array-multi-flexi';
            case Question::QT_SEMICOLON_ARRAY_TEXT:
                return 'array-multi-flexi-text';
            case Question::QT_VERTICAL_FILE_UPLOAD:
                return 'upload-files';
            case Question::QT_ASTERISK_EQUATION:
                return 'equation';
            default:
                return 'generic_question'; // fallback
        };
    }


    public function getbuttons()
    {
        $url         = App()->createUrl("questionAdministration/view/surveyid/$this->sid/gid/$this->gid/qid/$this->qid");
        $previewUrl  = Yii::app()->createUrl("survey/index/action/previewquestion/sid/");
        $previewUrl .= '/' . $this->sid . '/gid/' . $this->gid . '/qid/' . $this->qid;
        $editurl     = Yii::app()->createUrl("questionAdministration/edit/questionId/$this->qid/tabOverviewEditor/editor");

        $permission_edit_question = Permission::model()->hasSurveyPermission($this->sid, 'surveycontent', 'update');
        $permission_summary_question = Permission::model()->hasSurveyPermission($this->sid, 'surveycontent', 'read');
        $permission_delete_question = Permission::model()->hasSurveyPermission($this->sid, 'surveycontent', 'delete');

        $dropdownItems = [];
        $dropdownItems[] = [
            'title'            => gT('Edit question'),
            'iconClass'        => 'ri-pencil-fill',
            'url'              => $editurl,
            'enabledCondition' => $permission_edit_question
        ];
        $dropdownItems[] = [
            'title'            => gT('Question preview'),
            'iconClass'        => 'ri-eye-fill',
            'linkClass'        => 'open-preview',
            'linkAttributes'   => [
                'data-bs-toggle' => 'tooltip',
                'aria-data-url' => $previewUrl,
                'aria-data-sid' => $this->sid,
                'aria-data-gid' => $this->gid,
                'aria-data-qid' => $this->qid,
                'aria-data-language' => $this->survey->language
            ]
        ];
        $dropdownItems[] = [
            'title'            => gT('Question summary'),
            'iconClass'        => 'ri-list-unordered',
            'url'              => $url,
            'enabledCondition' => $permission_summary_question,
            'linkAttributes'   => [
                'data-bs-toggle' => 'tooltip',
            ]
        ];

        $oSurvey = Survey::model()->findByPk($this->sid);
        $surveyIsNotActive = $oSurvey->active !== 'Y';
        $dropdownItems[] = [
            'title'            => gT('Delete question'),
            'iconClass'        => 'ri-delete-bin-fill text-danger',
            'enabledCondition' => $surveyIsNotActive && $permission_delete_question,
            'linkAttributes'   => [
                'data-bs-toggle' => 'tooltip',
                'onclick' => '$.fn.bsconfirm("'
                    . CHtml::encode(gT("Deleting will also delete any answer options and subquestions it includes. Are you sure you want to continue?"))
                    . '", {"confirm_ok": "'
                    . gT("Delete")
                    . '", "confirm_cancel": "'
                    . gT("Cancel")
                    . '"}, function() {'
                    . convertGETtoPOST(Yii::app()->createUrl("questionAdministration/delete/", ["qid" => $this->qid]))
                    . "});"
            ]
        ];

        return App()->getController()->widget(
            'ext.admin.grid.GridActionsWidget.GridActionsWidget',
            ['dropdownItems' => $dropdownItems],
            true
        );
    }

    /**
     * get the ordered answers
     * @param null|integer scale
     * @param null|string $language
     * @return array
     */
    public function getOrderedAnswers($scale_id = null, $language = null)
    {
        //reset answers set prior to this call
        $aAnswerOptions = [
            0 => []
        ];

        foreach ($this->answers as $oAnswer) {
            if ($scale_id !== null && $oAnswer->scale_id != $scale_id) {
                continue;
            }
            $aAnswerOptions[$oAnswer->scale_id][] = $oAnswer;
        }


        if ($scale_id !== null) {
            return $aAnswerOptions[$scale_id];
        }

        $aAnswerOptions = $this->sortAnswerOptions($aAnswerOptions, $language);
        return $aAnswerOptions;
    }

    /**
     * Returns the specified answer options sorted according to the question attributes.
     * Refactored from getOrderedAnswers();
     * @param array<int,Answer[]> The answer options to sort
     * @param null|string $language
     * @return array<int,Answer[]>
     */
    private function sortAnswerOptions($answerOptions, $language = null)
    {
        // Sort randomly if applicable
        if ($this->shouldOrderAnswersRandomly()) {
            foreach ($answerOptions as $scaleId => $scaleArray) {
                $keys = array_keys($scaleArray);
                shuffle($keys); // See: https://forum.yiiframework.com/t/order-by-rand-and-total-posts/68099

                $sortedScaleAnswers = array();
                foreach ($keys as $key) {
                    $sortedScaleAnswers[$key] = $scaleArray[$key];
                }
                $answerOptions[$scaleId] = $sortedScaleAnswers;
            }
            return $answerOptions;
        }

        // Sort alphabetically if applicable
        if ($this->shouldOrderAnswersAlphabetically()) {
            if (empty($language) || !in_array($language, $this->survey->allLanguages)) {
                $language = $this->survey->language;
            }
            foreach ($answerOptions as $scaleId => $scaleArray) {
                $sorted = array();
                // We create an array sorted that will use the answer in the current language as value, and keep key
                foreach ($scaleArray as $key => $answer) {
                    $sorted[$key] = $answer->answerl10ns[$language]->answer;
                }
                LimeSurvey\Helpers\SortHelper::getInstance($language)->asort($sorted, LimeSurvey\Helpers\SortHelper::SORT_STRING);
                // Now, we create a new array that store the old values of $answerOptions in the order of $sorted
                $sortedScaleAnswers = array();
                foreach ($sorted as $key => $answer) {
                    $sortedScaleAnswers[] = $scaleArray[$key];
                }
                $answerOptions[$scaleId] = $sortedScaleAnswers;
            }
            return $answerOptions;
        }

        // Sort by Answer's own sort order
        foreach ($answerOptions as $scaleId => $scaleArray) {
            usort($scaleArray, function ($a, $b) {
                return $a->sortorder > $b->sortorder
                    ? 1
                    : ($a->sortorder < $b->sortorder ? -1 : 0);
            });
            $answerOptions[$scaleId] = $scaleArray;
        }
        return $answerOptions;
    }

    /**
     * Returns true if the answer options should be ordered randomly.
     * @return bool
     */
    private function shouldOrderAnswersRandomly()
    {
        // Question types supporting both Random Order and Alphabetical Order should
        // implement the 'answer_order' attribute instead of using separate attributes.
        $answerOrder = $this->getQuestionAttribute('answer_order');
        if (!is_null($answerOrder)) {
            return $answerOrder == 'random';
        }
        return $this->getQuestionAttribute('random_order') == 1 && $this->getQuestionType()->subquestions == 0;
    }

    /**
     * Returns true if the answer options should be ordered alphabetically.
     * @return bool
     */
    private function shouldOrderAnswersAlphabetically()
    {
        // Question types supporting both Random Order and Alphabetical Order should
        // implement the 'answer_order' attribute instead of using separate attributes.
        $answerOrder = $this->getQuestionAttribute('answer_order');
        if (!is_null($answerOrder)) {
            return $answerOrder == 'alphabetical';
        }
        return $this->getQuestionAttribute('alphasort') == 1;
    }

    /**
     * get subquestions fort the current question object in the right order
     * @param int $random
     * @param string $exclude_all_others
     * @return array
     */
    public function getOrderedSubQuestions($scale_id = null)
    {


        //reset subquestions set prior to this call
        $aSubQuestions = [
            0 => []
        ];

        $aOrderedSubquestions = $this->subquestions;

        if ($this->getQuestionAttribute('random_order') == 1) {
            require_once(Yii::app()->basePath . '/libraries/MersenneTwister.php');
            ls\mersenne\setSeed($this->sid);

            $aOrderedSubquestions = ls\mersenne\shuffle($aOrderedSubquestions);
        } else {
            usort($aOrderedSubquestions, function ($oQuestionA, $oQuestionB) {
                if ($oQuestionA->question_order == $oQuestionB->question_order) {
                    return 0;
                }
                return $oQuestionA->question_order < $oQuestionB->question_order ? -1 : 1;
            });
        }


        $excludedSubquestion = null;
        foreach ($aOrderedSubquestions as $i => $oSubquestion) {
            if ($scale_id !== null && $oSubquestion->scale_id != $scale_id) {
                continue;
            }
            //if  exclude_all_others is set then the related answer should keep its position at all times
            //thats why we have to re-position it if it has been randomized
            if (
                ($this->getQuestionAttribute('exclude_all_others') != '' && $this->getQuestionAttribute('random_order') == 1)
                && ($oSubquestion->title == $this->getQuestionAttribute('exclude_all_others'))
            ) {
                $excludedSubquestionPosition = (safecount($aSubQuestions[$oSubquestion->scale_id]) - 1);
                $excludedSubquestion = $oSubquestion;
                continue;
            }
            $aSubQuestions[$oSubquestion->scale_id][] = $oSubquestion;
        }

        if ($excludedSubquestion != null) {
            array_splice($aSubQuestions[$excludedSubquestion->scale_id], ($excludedSubquestion->question_order - 1), 0, [$excludedSubquestion]);
        }

        if ($scale_id !== null) {
            return $aSubQuestions[$scale_id];
        }

        return $aSubQuestions;
    }

    public function getMandatoryIcon()
    {
        if ($this->type != Question::QT_X_TEXT_DISPLAY && $this->type != Question::QT_VERTICAL_FILE_UPLOAD) {
            if ($this->mandatory == "Y") {
                $sIcon = '<span class="ri-star-fill text-danger"></span>';
            } elseif ($this->mandatory == "S") {
                $sIcon = '<span class="ri-star-fill text-danger"> ' . gT('Soft') . '</span>';
            } else {
                $sIcon = '<span></span>';
            }
        } else {
            $sIcon = '<span class="ri-forbid-2-line text-danger" data-bs-toggle="tooltip" title="' . gT('Not relevant for this question type') . '"></span>';
        }
        return $sIcon;
    }

    /**
     * Return other icon according to state
     * @return boolean
     */
    public function getOtherIcon()
    {
        if ($this->getAllowOther()) {
            $sIcon = ($this->other === "Y") ? '<span class="ri-record-circle-line"></span>' : '<span></span>';
        } else {
            $sIcon = '<span class="ri-forbid-2-line text-danger" data-bs-toggle="tooltip" title="' . gT('Not relevant for this question type') . '"></span>';
        }
        return $sIcon;
    }


    /**
     * Return if question allow other
     * @return boolean
     */
    public function getAllowOther()
    {
        return (
            !$this->parent_qid
            && $this->getQuestionType()->other
        );
    }

    /**
     * Return if question has managed subquestions
     * usage in rules : allow set other even if existing subquestions 'other' exist (but deleted after)
     * @return boolean
     */
    public function getAllowSubquestions()
    {
        return (
            !$this->parent_qid
            && $this->getQuestionType()->subquestions
        );
    }

    /**
     * Get an new title/code for a question
     * @param integer $index base for question code (example : inde of question when survey import)
     * @return string|null : new title, null if impossible
     */
    public function getNewTitle($index = 0)
    {
        $sOldTitle = $this->title;
        if ($this->validate(array('title'))) {
            return $sOldTitle;
        }
        /* Maybe it's an old invalid title : try to fix it */
        $sNewTitle = preg_replace("/[^A-Za-z0-9]/", '', $sOldTitle);
        if (is_numeric(substr($sNewTitle, 0, 1))) {
            $sNewTitle = 'q' . $sNewTitle;
        }
        /* Maybe there are another question with same title try to fix it 10 times */
        $attempts = 0;
        while (!$this->validate(array('title'))) {
            $rand = mt_rand(0, 1024);
            $sNewTitle = 'q' . $index . 'r' . $rand;
            $this->title = $sNewTitle;
            $attempts++;
            if ($attempts > 10) {
                $this->addError('title', 'Failed to resolve question code problems after 10 attempts.');
                return null;
            }
        }
        return $sNewTitle;
    }

    /*public function language($sLanguage)
    {
        $this->with(
            array(
                'questionl10ns'=>array('condition'=>"language='".$sLanguage."'"),
                'group'=>array('condition'=>"language='".$sLanguage."'")
            )
        );
        return $this;
    }*/

    public function getQuestionListColumns()
    {
        return array(
            array(
                'id' => 'id',
                'class' => 'CCheckBoxColumn',
                'selectableRows' => '100',
            ),
            array(
                'header' => gT('Question ID'),
                'name' => 'question_id',
                'value' => '$data->qid',
            ),
            array(
                'header' => gT("Group / Question order"),
                'name' => 'question_order',
                'value' => '$data->group->group_order ." / ". $data->question_order',
            ),
            array(
                'header' => gT('Code'),
                'name' => 'title',
                'value' => '$data->title',
                'htmlOptions' => array('class' => ''),
            ),
            array(
                'header' => gT('Question'),
                'name' => 'question',
                'value' => 'array_key_exists($data->survey->language, $data->questionl10ns) ? viewHelper::flatEllipsizeText($data->questionl10ns[$data->survey->language]->question,true,0) : ""',
                'htmlOptions' => array('class' => ''),
            ),
            array(
                'header' => gT('Question type'),
                'name' => 'type',
                'type' => 'raw',
                'value' => 'gT($data->question_theme->title) . (YII_DEBUG ? " <em>{$data->type}</em>" : "")',
                'htmlOptions' => array('class' => ''),
            ),

            array(
                'header' => gT('Group'),
                'name' => 'group',
                'value' => '$data->group->questiongroupl10ns[$data->survey->language]->group_name',
            ),

            array(
                'header' => gT('Mandatory'),
                'type' => 'raw',
                'name' => 'mandatory',
                'value' => '$data->mandatoryIcon',
                'htmlOptions' => array('class' => 'text-center'),
            ),

            array(
                'header' => gT('Other'),
                'type' => 'raw',
                'name' => 'other',
                'value' => '$data->otherIcon',
                'htmlOptions' => array('class' => 'text-center'),
            ),
            array(
                'header' => gT('Action'),
                'name' => 'actions',
                'type' => 'raw',
                'value' => '$data->buttons',
                'headerHtmlOptions' => ['class' => 'ls-sticky-column'],
                'htmlOptions'       => ['class' => 'text-center button-column ls-sticky-column'],
            ),
        );
    }

    public function search()
    {
        $pageSize = Yii::app()->user->getState('pageSize', Yii::app()->params['defaultPageSize']);

        $sort = new CSort();
        $sort->attributes = array(
            'question_id' => array(
                'asc' => 't.qid asc',
                'desc' => 't.qid desc',
            ),
            'question_order' => array(
                'asc' => 'g.group_order asc, t.question_order asc',
                'desc' => 'g.group_order desc,t.question_order desc',
            ),
            'title' => array(
                'asc' => 't.title asc',
                'desc' => 't.title desc',
            ),
            'question' => array(
                'asc' => 'ql10n.question asc',
                'desc' => 'ql10n.question desc',
            ),

            'group' => array(
                'asc' => 'g.gid asc',
                'desc' => 'g.gid desc',
            ),

            'mandatory' => array(
                'asc' => 't.mandatory asc',
                'desc' => 't.mandatory desc',
            ),

            'encrypted' => array(
                'asc' => 't.encrypted asc',
                'desc' => 't.encrypted desc',
            ),

            'other' => array(
                'asc' => 't.other asc',
                'desc' => 't.other desc',
            ),
        );

        $sort->defaultOrder = array(
            'question_order' => CSort::SORT_ASC,
        );

        $criteria = new LSDbCriteria();
        $criteria->compare("t.sid", $this->sid, false, 'AND');
        $criteria->compare("t.parent_qid", 0, false, 'AND');
        //$criteria->group = 't.qid, t.parent_qid, t.sid, t.gid, t.type, t.title, t.preg, t.other, t.mandatory, t.question_order, t.scale_id, t.same_default, t.relevance, t.modulename, t.encrypted';
        $criteria->with = [
            'group' => ['alias' => 'g'],
            'questionl10ns' => ['alias' => 'ql10n', 'condition' => "language='" . $this->survey->language . "'"],
            'question_theme' => ['alias' => 'qt']
        ];

        if (!empty($this->title)) {
            $criteria2 = new LSDbCriteria();
            $criteria2->compare('t.title', $this->title, true, 'OR');
            $criteria2->compare('ql10n.question', $this->title, true, 'OR');
            $criteria2->compare('t.type', $this->title, true, 'OR');
            $criteria2->compare('qt.description', $this->title, true, 'OR');
            /* search exact qid and make sure it's a numeric */
            if (is_numeric($this->title)) {
                $criteria2->compare('t.qid', $this->title, false, 'OR');
            }
            $criteria->mergeWith($criteria2, 'AND');
        }

        /* make sure gid is a numeric */
        if ($this->gid != '' and is_numeric($this->gid)) {
            $criteria->compare('g.gid', $this->gid, false, 'AND');
        }

        $dataProvider = new CActiveDataProvider('Question', array(
            'criteria' => $criteria,
            'sort' => $sort,
            'pagination' => array(
                'pageSize' => $pageSize,
            ),
        ));
        return $dataProvider;
    }

    /** @inheritdoc */
    public function scopes()
    {
        return array(
            'primary' => array('condition' => "parent_qid = 0"),
        );
    }

    /**
     * Make sure we don't save a new question group
     * while the survey is active.
     *
     * @return bool
     */
    protected function beforeSave()
    {
        if (parent::beforeSave()) {
            /* No update when surey activated */
            $surveyIsActive = Survey::model()->findByPk($this->sid)->active !== 'N';
            if ($surveyIsActive) {
                //don't override questiontype when survey is active, set it back to what it was...
                $oActualValue = Question::model()->findByPk(array("qid" => $this->qid));
                $this->type = $oActualValue->type;
            }
            if ($surveyIsActive && $this->getIsNewRecord()) {
                return false;
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * Fix sub question of a parent question
     * Must be call after base language subquestion is set
     * @todo : move other fix here ?
     * @return void
     */
    public function fixSubQuestions()
    {
        if ($this->parent_qid) {
            return;
        }
        $oSurvey = $this->survey;

        /* Delete subquestion l10n for unknown languages */
        $criteria = new CDbCriteria();
        $criteria->with = array("question", array('condition' => array('sid' => $this->qid)));
        $criteria->together = true;
        $criteria->addNotInCondition('language', $oSurvey->getAllLanguages());
        QuestionL10n::model()->deleteAll($criteria);

        /* Delete invalid subquestions (not in primary language */
        $validSubQuestion = Question::model()->findAll(array(
            'select' => 'title',
            'condition' => 'parent_qid=:parent_qid',
            'params' => array('parent_qid' => $this->qid)
        ));
        $criteria = new CDbCriteria();
        $criteria->compare('parent_qid', $this->qid);
        $criteria->addNotInCondition('title', CHtml::listData($validSubQuestion, 'title', 'title'));
        Question::model()->deleteAll($criteria);
    }
    /** @return string[] */
    public static function getQuotableTypes()
    {
        return array('G', 'M', 'Y', 'A', 'B', 'I', 'L', 'O', '!', '*');
    }


    public function getBasicFieldName()
    {
        if ($this->parent_qid != 0) {
            /* Fix #15228: This survey throw a Error when try to print : seems subquestion gid can be outdated */
            // Use parents relation
            if (!empty($this->parents)) { // Maybe need to throw error or find it if it's not set ?
                return "{$this->parents->sid}X{$this->parents->gid}X{$this->parent_qid}";
            }
            return "{$this->sid}X{$this->gid}X{$this->parent_qid}";
        }
        return "{$this->sid}X{$this->gid}X{$this->qid}";
    }

    /**
     * @return QuestionAttribute[]
     */
    public function getQuestionAttribute($sAttribute)
    {
        $criteria = new CDbCriteria();
        $criteria->addCondition('qid=:qid');
        $criteria->addCondition('attribute=:attribute');
        $criteria->params = [':qid' => $this->qid, ':attribute' => $sAttribute];
        $oQuestionAttribute =  QuestionAttribute::model()->find($criteria);

        if ($oQuestionAttribute != null) {
            return $oQuestionAttribute->value;
        }

        return null;
    }

    /**
     * @return null|QuestionType
     */
    public function getQuestionType()
    {
        $model = QuestionType::findOne($this->type);
        if (!empty($model)) {
            $model->applyToQuestion($this);
        }
        return $model;
    }

    /**
     * @return array
     */
    public function getAllSubQuestionIds()
    {
        $result = [];
        if (!empty($this->subquestions)) {
            foreach ($this->subquestions as $subquestion) {
                $result[] = $subquestion->qid;
            }
        }
        return $result;
    }

    public function getRenderererObject($aFieldArray, $type = null)
    {
        $type = $type ?? $this->type;
        LoadQuestionTypes::load($type);
        switch ($type) {
            case Question::QT_X_TEXT_DISPLAY:
                $oRenderer = new RenderBoilerplate($aFieldArray);
                break;
            case Question::QT_5_POINT_CHOICE:
                $oRenderer = new RenderFivePointChoice($aFieldArray);
                break;
            case Question::QT_ASTERISK_EQUATION:
                $oRenderer = new RenderEquation($aFieldArray);
                break;
            case Question::QT_D_DATE:
                $oRenderer = new RenderDate($aFieldArray);
                break;
            case Question::QT_1_ARRAY_DUAL:
                $oRenderer = new RenderArrayMultiscale($aFieldArray);
                break;
            case Question::QT_L_LIST:
                $oRenderer = new RenderListRadio($aFieldArray);
                break;
            case Question::QT_EXCLAMATION_LIST_DROPDOWN:
                $oRenderer = new RenderListDropdown($aFieldArray);
                break;
            case Question::QT_O_LIST_WITH_COMMENT:
                $oRenderer = new RenderListComment($aFieldArray);
                break;
            case Question::QT_R_RANKING:
                $oRenderer = new RenderRanking($aFieldArray);
                break;
            case Question::QT_M_MULTIPLE_CHOICE:
                $oRenderer = new RenderMultipleChoice($aFieldArray);
                break;
            case Question::QT_P_MULTIPLE_CHOICE_WITH_COMMENTS:
                $oRenderer = new RenderMultipleChoiceWithComments($aFieldArray);
                break;
            case Question::QT_I_LANGUAGE:
                $oRenderer = new RenderLanguageSelector($aFieldArray);
                break;
            case Question::QT_Q_MULTIPLE_SHORT_TEXT:
                $oRenderer = new RenderMultipleShortText($aFieldArray);
                break;
            case Question::QT_T_LONG_FREE_TEXT:
                $oRenderer = new RenderLongFreeText($aFieldArray);
                break;
            case Question::QT_U_HUGE_FREE_TEXT:
                $oRenderer = new RenderHugeFreeText($aFieldArray);
                break;
            case Question::QT_K_MULTIPLE_NUMERICAL:
                $oRenderer = new RenderMultipleNumerical($aFieldArray);
                break;
            case Question::QT_A_ARRAY_5_POINT:
                $oRenderer = new RenderArray5ChoiceQuestion($aFieldArray);
                break;
            case Question::QT_B_ARRAY_10_CHOICE_QUESTIONS:
                $oRenderer = new RenderArray10ChoiceQuestion($aFieldArray);
                break;
            case Question::QT_C_ARRAY_YES_UNCERTAIN_NO:
                $oRenderer = new RenderArrayYesUncertainNo($aFieldArray);
                break;
            case Question::QT_E_ARRAY_INC_SAME_DEC:
                $oRenderer = new RenderArrayOfIncSameDecQuestions($aFieldArray);
                break;
            case Question::QT_F_ARRAY:
                $oRenderer = new RenderArrayFlexibleRow($aFieldArray);
                break;
            case Question::QT_G_GENDER:
                $oRenderer = new RenderGenderDropdown($aFieldArray);
                break;
            case Question::QT_H_ARRAY_COLUMN:
                $oRenderer = new RendererArrayFlexibleColumn($aFieldArray);
                break;
            case Question::QT_N_NUMERICAL:
                $oRenderer = new RenderNumerical($aFieldArray);
                break;
            case Question::QT_S_SHORT_FREE_TEXT:
                $oRenderer = new RenderShortFreeText($aFieldArray);
                break;
            case Question::QT_Y_YES_NO_RADIO:
                $oRenderer = new RenderYesNoRadio($aFieldArray);
                break;
            case Question::QT_COLON_ARRAY_NUMBERS:
                $oRenderer = new RenderArrayMultiFlexNumbers($aFieldArray);
                break;
            case Question::QT_SEMICOLON_ARRAY_TEXT:
                $oRenderer = new RenderArrayMultiFlexText($aFieldArray);
                break;
            case Question::QT_VERTICAL_FILE_UPLOAD:
                $oRenderer = new RenderFileUpload($aFieldArray);
                break;
            default:
                throw new InvalidArgumentException('Missing question type in getRenderererObject');
                break;
        };

        return $oRenderer;
    }

    public function getDataSetObject($type = null)
    {
        $type = $type ?? $this->type;
        LoadQuestionTypes::load($type);

        switch ($type) {
            case Question::QT_X_TEXT_DISPLAY:
                return new DataSetBoilerplate($this->qid);
            case Question::QT_5_POINT_CHOICE:
                return new DataSetFivePointChoice($this->qid);
            case Question::QT_ASTERISK_EQUATION:
                return new DataSetEquation($this->qid);
            case Question::QT_D_DATE:
                return new DataSetDate($this->qid);
            case Question::QT_1_ARRAY_DUAL:
                return new DataSetArrayMultiscale($this->qid);
            case Question::QT_L_LIST:
                return new DataSetListRadio($this->qid);
            case Question::QT_EXCLAMATION_LIST_DROPDOWN:
                return new DataSetListDropdown($this->qid);
            case Question::QT_O_LIST_WITH_COMMENT:
                return new DataSetListWithComment($this->qid);
            case Question::QT_R_RANKING:
                return new DataSetRanking($this->qid);
            case Question::QT_M_MULTIPLE_CHOICE:
                return new DataSetMultipleChoice($this->qid);
            case Question::QT_P_MULTIPLE_CHOICE_WITH_COMMENTS:
                return new DataSetMultipleChoiceWithComments($this->qid);
            case Question::QT_I_LANGUAGE:
                return new DataSetLanguage($this->qid);
            case Question::QT_Q_MULTIPLE_SHORT_TEXT:
                return new DataSetMultipleShortText($this->qid);
            case Question::QT_T_LONG_FREE_TEXT:
                return new DataSetLongFreeText($this->qid);
            case Question::QT_U_HUGE_FREE_TEXT:
                return new DataSetHugeFreeText($this->qid);
            case Question::QT_K_MULTIPLE_NUMERICAL:
                return new DataSetMultipleNumerical($this->qid);
            case Question::QT_A_ARRAY_5_POINT:
                return new DataSetArray5ChoiceQuestion($this->qid);
            case Question::QT_B_ARRAY_10_CHOICE_QUESTIONS:
                return new DataSetArray10ChoiceQuestion($this->qid);
            case Question::QT_C_ARRAY_YES_UNCERTAIN_NO:
                return new DataSetArrayYesUncertainNo($this->qid);
            case Question::QT_E_ARRAY_INC_SAME_DEC:
                return new DataSetArrayOfIncSameDecQuestions($this->qid);
            case Question::QT_F_ARRAY:
                return new DataSetArrayFlexibleRow($this->qid);
            case Question::QT_G_GENDER:
                return new DataSetGenderDropdown($this->qid);
            case Question::QT_H_ARRAY_COLUMN:
                return new DataSetArrayFlexibleColumn($this->qid);
            case Question::QT_N_NUMERICAL:
                return new DataSetNumerical($this->qid);
            case Question::QT_S_SHORT_FREE_TEXT:
                return new DataSetShortFreeText($this->qid);
            case Question::QT_Y_YES_NO_RADIO:
                return new DataSetYesNoRadio($this->qid);
            case Question::QT_COLON_ARRAY_NUMBERS:
                return new DataSetArrayMultiFlexNumbers($this->qid);
            case Question::QT_SEMICOLON_ARRAY_TEXT:
                return new DataSetArrayMultiFlexText($this->qid);
            case Question::QT_VERTICAL_FILE_UPLOAD:
                return new DataSetFileUpload($this->qid);
            default:
                throw new InvalidArgumentException('Missing question type in getDataSetObject');
        };
    }

    /**
     * @param array $data
     * @return boolean|null
     */
    public function insertRecords($data)
    {
        $oRecord = new self();
        foreach ($data as $k => $v) {
            $oRecord->$k = $v;
        }
        if ($oRecord->validate()) {
            return $oRecord->save();
        }
        Yii::log(\CVarDumper::dumpAsString($oRecord->getErrors()), 'warning', 'application.models.Question.insertRecords');
    }

    /**
     * In some cases question have o wrong questio_sort=0, The sort number should always be
     * greater then 0, starting with 1. For this reason, the question_order has to be checked
     * for a specific group and question_sort has be set correctly.
     *
     * @param $questionGroupId
     *
     * @return boolean true if sort numbers had to be set, false otherwise
     */
    public static function setQuestionOrderForGroup($questionGroupId)
    {
        $criteriaHighestOrderNumber = new CDbCriteria();
        $criteriaHighestOrderNumber->condition = 't.gid=:gid';
        $criteriaHighestOrderNumber->addCondition("parent_qid=0"); //no subquestions here ...
        $criteriaHighestOrderNumber->addCondition('t.question_order=0');//find only those which has to be set
        $criteriaHighestOrderNumber->params = ['gid' => $questionGroupId];
        $criteriaHighestOrderNumber->order = 't.qid ASC';

        $questionsWithZeroSortNumber = Question::model()->findAll($criteriaHighestOrderNumber);
        $isAlreadySorted = count($questionsWithZeroSortNumber) === 0; //means no questions, so resort needed
        if (!$isAlreadySorted) {
            $sortValue = self::START_SORTING_VALUE;
            /* @var Question $question  */
            foreach ($questionsWithZeroSortNumber as $question) {
                $question->question_order = $sortValue;
                $question->save();
                $sortValue++;
            }
        }

        return !$isAlreadySorted;
    }


    /**
     * Returns the highest question_order value that exists for a questiongroup inside the related questions.
     * ($question->question_order).
     *
     * @param int $questionGroupId  the question group id
     *
     * @return int|null question highest order number or null if there are no questions belonging to the group
     */
    public static function getHighestQuestionOrderNumberInGroup($questionGroupId)
    {
        $criteriaHighestOrderNumber = new CDbCriteria();
        $criteriaHighestOrderNumber->limit = 1;
        $criteriaHighestOrderNumber->condition = 't.gid=:gid';
        $criteriaHighestOrderNumber->addCondition("parent_qid=0"); //no subquestions here ...
        $criteriaHighestOrderNumber->params = ['gid' => $questionGroupId];
        $criteriaHighestOrderNumber->order = 't.question_order DESC';

        $oQuestionHighestOrderNumber = Question::model()->find($criteriaHighestOrderNumber);

        return ($oQuestionHighestOrderNumber === null) ? null : $oQuestionHighestOrderNumber->question_order;
    }

    /**
     * Increases all question_order numbers for questions belonging to the group by +1
     *
     * @param int $questionGroupId
     * @param integer|null $after questionorder
     */
    public static function increaseAllOrderNumbersForGroup($questionGroupId, $after = null)
    {
        $criteria = new CDbCriteria();
        $criteria->compare("gid", $questionGroupId);
        if ($after) {
            $criteria->compare("question_order", ">=" . $after);
        }
        $questionsInGroup = Question::model()->findAll($criteria);
        foreach ($questionsInGroup as $question) {
            $question->question_order = $question->question_order + 1;
            $question->save();
        }
    }

    /**
     * @deprecated 5.3.x
     * unknow usage
     */
    public function getHasSubquestions()
    {
    }

    /**
     * @deprecated 5.3.x
     * unknow usage
     */
    public function getHasAnsweroptions()
    {
    }

    /**
     * Check if this question have subquestion with other code
     * @return boolean
     */
    public function getHasOtherSubquestions()
    {
        if (!$this->getAllowSubquestions()) {
            return false;
        }
        $otherSubQuestionCount = Question::model()->count(
            "parent_qid=:parent_qid and LOWER(title)='other'",
            array("parent_qid" => $this->qid)
        );
        return boolval($otherSubQuestionCount);
    }

    /**
     * Override update() method to "clean" subquestions after saving a parent question
     */
    public function update($attributes = null)
    {
        if (parent::update($attributes)) {
            $this->removeInvalidSubquestions();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Remove subquestion if needed when update question type
     * @return void
     */
    protected function removeInvalidSubquestions()
    {
        if ($this->getAllowSubquestions()) {
            return;
        }

        // Remove subquestions if the question's type doesn't allow subquestions
        $aSubquestions = Question::model()->findAll("parent_qid=:parent_qid", array("parent_qid" => $this->qid));
        if (!empty($aSubquestions)) {
            foreach ($aSubquestions as $oSubquestion) {
                /* Use delete to delete related model */
                $oSubquestion->delete();
            }
        }
    }

    /**
     * Used by question create form.
     *
     * @return Question
     */
    public function getEmptySubquestion()
    {
        $question = new Question();
        $question->qid = 0;
        $question->title = (SettingsUser::getUserSettingValue('subquestionprefix', App()->user->id) ?? 'SQ') . '001';
        $question->relevance = 1;
        return $question;
    }

    /**
     * Used by question create form.
     *
     * @return Answer
     */
    public function getEmptyAnswerOption()
    {
        $answer = new Answer();
        // TODO: Assuming no collision.
        $answer->aid = 'new' . rand(1, 100000);
        $answer->sortorder = 0;
        $answer->code = (SettingsUser::getUserSettingValue('answeroptionprefix', App()->user->id) ?? 'AO') . '01';

        $l10n = [];
        foreach ($this->survey->allLanguages as $language) {
            $l10n[$language] = new AnswerL10n();
            $l10n[$language]->setAttributes(
                [
                    'aid'      => 0,
                    'answer'   => '',
                    'language' => $language,
                ],
                false
            );
        }
        $answer->answerl10ns = $l10n;

        return $answer;
    }

    /**
     * Get array of answers options, depending on scale count for this question type.
     *
     * @return array Like [0 => Answer[]] or [0 => Answer[], 1 => Answer[]]
     */
    public function getScaledAnswerOptions()
    {
        $answerScales = $this->questionType->answerscales;
        $results = [];
        for ($scale_id = 0; $scale_id < $answerScales; $scale_id++) {
            if (!empty($this->qid)) {
                $criteria = new CDbCriteria();
                $criteria->condition = 'qid = :qid AND scale_id = :scale_id';
                $criteria->params = [':qid' => $this->qid, ':scale_id' => $scale_id];
                $results[$scale_id] = Answer::model()->findAll($criteria);
            }
            if (empty($results[$scale_id])) {
                $results[$scale_id] = [$this->getEmptyAnswerOption()];
            }
        }
        return $results;
    }

    /**
     * Get array of subquestions, depending on scale count for this question type.
     *
     * @return array Like [0 => Question[]] or [0 => Question[], 1 => Question[]]
     */
    public function getScaledSubquestions()
    {
        $subquestionScale = $this->questionType->subquestions;
        $results = [];
        for ($scale_id = 0; $scale_id < $subquestionScale; $scale_id++) {
            if (!empty($this->qid)) {
                $criteria = new CDbCriteria();
                $criteria->condition = 'parent_qid = :parent_qid AND scale_id = :scale_id';
                $criteria->order = 'question_order, title ASC';
                $criteria->params = [':parent_qid' => $this->qid, ':scale_id' => $scale_id];
                $results[$scale_id] = Question::model()->findAll($criteria);
            }
            if (empty($results[$scale_id])) {
                $results[$scale_id] = [$this->getEmptySubquestion()];
            }
        }
        return $results;
    }

    /**
     * Validates the question theme name, making sure it's not empty or 'core'
     */
    public function questionThemeNameValidator()
    {
        /* need a type */
        if (empty($this->type)) {
            return null;
        }
        /* not needed in child question */
        if (!empty($this->parent_qid)) {
            return null;
        }
        if (!empty($this->question_theme_name) && $this->question_theme_name != 'core') {
            $criteria = new CDbCriteria();
            $criteria->addCondition('question_type = :question_type AND name = :name');
            $criteria->params = [':question_type' => $this->type, ':name' => $this->question_theme_name];
            $questionTheme = QuestionTheme::model()->query($criteria, false);
            if ($questionTheme) {
                return $this->question_theme_name;
            }
        }
        /* Get default theme name from type */
        $baseQuestionThemeName = QuestionTheme::model()->getBaseThemeNameForQuestionType($this->type);
        if (!empty($baseQuestionThemeName)) {
            return $baseQuestionThemeName;
        }
        /* Not a valid type ? */
        return null;
    }

    /**
     * Returns the QuestionTheme related to this question.
     * It's not implemented as a relation because relations only work on
     * persisted models.
     *
     * @return QuestionTheme|null
     */
    public function getQuestionTheme()
    {
        return $this->getRelated("question_theme", $this->isNewRecord);
    }

    /**
     * Is it a dual scale type.
     */
    public function getIsDualScale()
    {
        $dualScaleTypes = $this->getDualScaleTypes();
        return in_array($this->type, $dualScaleTypes);
    }

    /**
     * Returns the question types that are dual scale.
     */
    public function getDualScaleTypes()
    {
        $dualScaleTypes = array(
            Question::QT_1_ARRAY_DUAL
        );

        return $dualScaleTypes;
    }
}