diff --git a/application/core/plugins/statFunctions/config.xml b/application/core/plugins/statFunctions/config.xml
new file mode 100644
index 00000000000..847aa194350
--- /dev/null
+++ b/application/core/plugins/statFunctions/config.xml
@@ -0,0 +1,25 @@
+
+
+
+ statFunctions
+ plugin
+ 2019-09-19
+ 2019-09-19
+ Denis Chenu
+ https://www.limesurvey.org
+ 0.1.0
+ GNU General Public License version 2 or later
+
+ statCountIf(QuestionCode.sgqa, value[, submitted = true])
+ statCount(QuestionCode.sgqa[, submitted = true])
+
+]]>
+
+
+
+ 4.0
+
+
+
+
+
diff --git a/application/core/plugins/statFunctions/countFunctions.php b/application/core/plugins/statFunctions/countFunctions.php
new file mode 100644
index 00000000000..df99fcd2d09
--- /dev/null
+++ b/application/core/plugins/statFunctions/countFunctions.php
@@ -0,0 +1,77 @@
+ … see https://www.yiiframework.com/doc/api/1.1/CDbCriteria#compare-detail
+ * @param boolean $submitted (or not) response
+ * @return integer|string
+ */
+ public static function statCountIf($qCode, $comparaison, $submitted = true)
+ {
+ $surveyId = LimeExpressionManager::getLEMsurveyId();
+ if (!Survey::model()->findByPk($surveyId)->getIsActive()) {
+ return 0;
+ }
+ $questionCodeHelper = new \statFunctions\questionCodeHelper($surveyId);
+ $column = $questionCodeHelper->getColumnByQCode($qCode);
+ if (is_null($column)) {
+ if (Permission::model()->hasSurveyPermission($surveyId, 'surveycontent')) { // update ???
+ return sprintf(gT("Invalid question code %s"), CHtml::encode($qCode));
+ }
+ return "";
+ }
+ $sQuotedColumn=Yii::app()->db->quoteColumnName($column);
+ $oCriteria = new CDbCriteria;
+ $oCriteria->condition= "$sQuotedColumn IS NOT NULL";
+ if ($submitted) {
+ $oCriteria->addCondition("submitdate IS NOT NULL");
+ }
+ $oCriteria->compare($sQuotedColumn, $comparaison);
+ return intval(SurveyDynamic::model($surveyId)->count($oCriteria));
+ }
+
+ /**
+ * Return the count of reponse on current Expression Manager survey equal to a specific value
+ * @param string $qCode : code of question, currently must be existing sgqa. Sample Q01.sgqa.
+ * @param boolean $submitted (or not) response
+ * @return integer|string
+ */
+ public static function statCount($qCode, $submitted = true)
+ {
+ $surveyId = LimeExpressionManager::getLEMsurveyId();
+ if (!Survey::model()->findByPk($surveyId)->getIsActive()) {
+ return 0;
+ }
+ $questionCodeHelper = new \statFunctions\questionCodeHelper($surveyId);
+ $column = $questionCodeHelper->getColumnByQCode($qCode);
+ if (is_null($column)) {
+ if (Permission::model()->hasSurveyPermission($surveyId, 'surveycontent')) { // update ???
+ return sprintf(gT("Invalid question code %s"), CHtml::encode($qCode));
+ }
+ return "";
+ }
+
+ $sQuotedColumn=Yii::app()->db->quoteColumnName($column);
+ $oCriteria = new CDbCriteria;
+ $oCriteria->condition= "$sQuotedColumn IS NOT NULL and $sQuotedColumn <> ''";
+ if ($submitted) {
+ $oCriteria->addCondition("submitdate IS NOT NULL");
+ }
+ return intval(SurveyDynamic::model($surveyId)->count($oCriteria));
+ }
+}
diff --git a/application/core/plugins/statFunctions/questionCodeHelper.php b/application/core/plugins/statFunctions/questionCodeHelper.php
new file mode 100644
index 00000000000..328d85db162
--- /dev/null
+++ b/application/core/plugins/statFunctions/questionCodeHelper.php
@@ -0,0 +1,46 @@
+surveyId = $surveyId;
+ /* Throw error if surveyid is invalid ? */
+ }
+
+ /**
+ * Check the survey
+ * @param string $qCode question SGQA
+ * @return string|null : the final column name, null if not found
+ */
+ public function getColumnByQCode($qCode)
+ {
+ $availableColumns = SurveyDynamic::model($this->surveyId)->getAttributes();
+ /* Sample : Q01.sgqa Q01_SQ01.sgqa */
+ if (array_key_exists($qCode, $availableColumns)) {
+ return $qCode;
+ }
+
+ /* @todo : allow "Q0" and "Q0_SQ0" …
+ * But without using LimeExpressionManager::ProcessString or LimeExpressionManager::getLEMqcode2sgqa
+ * Because break logic file
+ * Wait for OK to merge to start it …
+ */
+ return null;
+ }
+}
diff --git a/application/core/plugins/statFunctions/statFunctions.php b/application/core/plugins/statFunctions/statFunctions.php
new file mode 100644
index 00000000000..a4d500fdc00
--- /dev/null
+++ b/application/core/plugins/statFunctions/statFunctions.php
@@ -0,0 +1,56 @@
+
+ * @copyright 2019 Denis Chenu
+ * @license GPL version 3
+ * @version 0.0.1
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+class statFunctions extends PluginBase
+{
+ protected static $description = 'Add some function in expression manager to get count from other responses';
+ protected static $name = 'statCountFunctions';
+
+ public function init()
+ {
+ $this->subscribe('ExpressionManagerStart', 'newValidFunctions');
+ }
+
+ public function newValidFunctions()
+ {
+ Yii::setPathOfAlias(get_class($this), dirname(__FILE__));
+ $newFunctions = array(
+ 'statCountIf' => array(
+ '\statFunctions\countFunctions::statCountIf',
+ null, // No javascript function : set as static function
+ $this->gT("Count the response done with value equal to a specific value"), // Description for admin
+ 'integer statCountIf(QuestionCode.sgqa, value[, submitted = true])', // Extra description
+ 'https://www.limesurvey.org', // Help url
+ 2, // Number of argument unsure it work here … , minimum 2, allow 3
+ 3
+ ),
+ 'statCount' => array(
+ '\statFunctions\countFunctions::statCount',
+ null, // No javascript function : set as static function
+ $this->gT("Count previous response done not empty"), // Description for admin
+ 'integer statCount(QuestionCode.sgqa[, submitted = true])', // Extra description
+ 'https://www.limesurvey.org', // Help url
+ 1, // Number of argument (time to make a good description of EM …) minimum 1, allow 2
+ 2,
+ ),
+ );
+ $this->getEvent()->append('functions', $newFunctions);
+ }
+}
diff --git a/application/helpers/expressions/em_core_helper.php b/application/helpers/expressions/em_core_helper.php
index 3f68780445f..01cf8924224 100644
--- a/application/helpers/expressions/em_core_helper.php
+++ b/application/helpers/expressions/em_core_helper.php
@@ -1240,70 +1240,106 @@ public function GetJavaScriptEquivalentOfExpression()
$tokens = $this->RDP_tokens;
$stringParts = array();
$numTokens = count($tokens);
+
+ /* Static function management */
+ $bracket = 0;
+ $staticStringToParse = "";
for ($i = 0; $i < $numTokens; ++$i) {
- $token = $tokens[$i];
- // When do these need to be quoted?
+ $token = $tokens[$i]; // When do these need to be quoted?
+ if(!empty($staticStringToParse)) { /* Currently inside a static function */
- switch ($token[2]) {
- case 'DQ_STRING':
- $stringParts[] = '"'.addcslashes($token[0], '\"').'"'; // htmlspecialchars($token[0],ENT_QUOTES,'UTF-8',false) . "'";
- break;
- case 'SQ_STRING':
- $stringParts[] = "'".addcslashes($token[0], "\'")."'"; // htmlspecialchars($token[0],ENT_QUOTES,'UTF-8',false) . "'";
- break;
- case 'SGQA':
- case 'WORD':
- if ($i + 1 < $numTokens && $tokens[$i + 1][2] == 'LP') {
- // then word is a function name
- $funcInfo = $this->RDP_ValidFunctions[$token[0]];
- if ($funcInfo[1] == 'NA') {
- return ''; // to indicate that this is trying to use a undefined function. Need more graceful solution
- }
- $stringParts[] = $funcInfo[1]; // the PHP function name
- } elseif ($i + 1 < $numTokens && $tokens[$i + 1][2] == 'ASSIGN') {
- $jsName = $this->GetVarAttribute($token[0], 'jsName', '');
- $stringParts[] = "document.getElementById('".$jsName."').value";
- if ($tokens[$i + 1][0] == '+=') {
- // Javascript does concatenation unless both left and right side are numbers, so refactor the equation
- $varName = $this->GetVarAttribute($token[0], 'varName', $token[0]);
- $stringParts[] = " = LEMval('".$varName."') + ";
- ++$i;
- }
- } else {
- $jsName = $this->GetVarAttribute($token[0], 'jsName', '');
- $code = $this->GetVarAttribute($token[0], 'code', '');
- if ($jsName != '') {
- $varName = $this->GetVarAttribute($token[0], 'varName', $token[0]);
- $stringParts[] = "LEMval('".$varName."') ";
+ switch ($token[2]) {
+ case 'LP':
+ $staticStringToParse .= $token[0];
+ $bracket ++;
+ break;
+ case 'RP':
+ $staticStringToParse .= $token[0];
+ $bracket --;
+ break;
+ case 'DQ_STRING':
+ // A string inside double quote : add double quote again
+ $staticStringToParse .= '"'.$token[0].'"';
+ break;
+ case 'SQ_STRING':
+ // A string inside single quote : add single quote again
+ $staticStringToParse .= "'".$token[0]."'";
+ break;
+ default:
+ // This set whole string inside function as a static var : must document clearly.
+ $staticStringToParse .= $token[0];
+ }
+ if($bracket == 0) { // Last close bracket : get the static final function and reset
+ //~ $staticString = LimeExpressionManager::ProcessStepString("{".$staticStringToParse."}",array(),3,true);
+ $staticString = $this->sProcessStringContainingExpressions("{".$staticStringToParse."}",0,3,1,-1,-1,true); // As static : no gseq,qseq etc …
+ $stringParts[] = $staticString;
+ $staticStringToParse = "";
+ }
+ } else {
+ switch ($token[2]) {
+ case 'DQ_STRING':
+ $stringParts[] = '"'.addcslashes($token[0], '\"').'"'; // htmlspecialchars($token[0],ENT_QUOTES,'UTF-8',false) . "'";
+ break;
+ case 'SQ_STRING':
+ $stringParts[] = "'".addcslashes($token[0], "\'")."'"; // htmlspecialchars($token[0],ENT_QUOTES,'UTF-8',false) . "'";
+ break;
+ case 'SGQA':
+ case 'WORD':
+ if ($i + 1 < $numTokens && $tokens[$i + 1][2] == 'LP') {
+ // then word is a function name
+ $funcInfo = $this->RDP_ValidFunctions[$token[0]];
+ if ($funcInfo[1] === null ) {
+ /* start a static function */
+ $staticStringToParse = $token[0]; // The function name
+ $bracket = 0; // Reset bracket (again)
+ } else {
+ $stringParts[] = $funcInfo[1]; // the PHP function name
+ }
+ } elseif ($i + 1 < $numTokens && $tokens[$i + 1][2] == 'ASSIGN') {
+ $jsName = $this->GetVarAttribute($token[0], 'jsName', '');
+ $stringParts[] = "document.getElementById('".$jsName."').value";
+ if ($tokens[$i + 1][0] == '+=') {
+ // Javascript does concatenation unless both left and right side are numbers, so refactor the equation
+ $varName = $this->GetVarAttribute($token[0], 'varName', $token[0]);
+ $stringParts[] = " = LEMval('".$varName."') + ";
+ ++$i;
+ }
} else {
- $stringParts[] = "'".addcslashes($code, "'")."'";
+ $jsName = $this->GetVarAttribute($token[0], 'jsName', '');
+ $code = $this->GetVarAttribute($token[0], 'code', '');
+ if ($jsName != '') {
+ $varName = $this->GetVarAttribute($token[0], 'varName', $token[0]);
+ $stringParts[] = "LEMval('".$varName."') ";
+ } else {
+ $stringParts[] = "'".addcslashes($code, "'")."'";
+ }
}
- }
- break;
- case 'LP':
- case 'RP':
- $stringParts[] = $token[0];
- break;
- case 'NUMBER':
- $stringParts[] = is_numeric($token[0]) ? $token[0] : ("'".$token[0]."'");
- break;
- case 'COMMA':
- $stringParts[] = $token[0].' ';
- break;
- default:
- // don't need to check type of $token[2] here since already handling SQ_STRING and DQ_STRING above
- switch (strtolower($token[0])) {
- case 'and': $stringParts[] = ' && '; break;
- case 'or': $stringParts[] = ' || '; break;
- case 'lt': $stringParts[] = ' < '; break;
- case 'le': $stringParts[] = ' <= '; break;
- case 'gt': $stringParts[] = ' > '; break;
- case 'ge': $stringParts[] = ' >= '; break;
- case 'eq': case '==': $stringParts[] = ' == '; break;
- case 'ne': case '!=': $stringParts[] = ' != '; break;
- default: $stringParts[] = ' '.$token[0].' '; break;
- }
- break;
+ break;
+ case 'LP':
+ case 'RP':
+ $stringParts[] = $token[0];
+ break;
+ case 'NUMBER':
+ $stringParts[] = is_numeric($token[0]) ? $token[0] : ("'".$token[0]."'");
+ break;
+ case 'COMMA':
+ $stringParts[] = $token[0].' ';
+ break;
+ default:
+ // don't need to check type of $token[2] here since already handling SQ_STRING and DQ_STRING above
+ switch (strtolower($token[0])) {
+ case 'and': $stringParts[] = ' && '; break;
+ case 'or': $stringParts[] = ' || '; break;
+ case 'lt': $stringParts[] = ' < '; break;
+ case 'le': $stringParts[] = ' <= '; break;
+ case 'gt': $stringParts[] = ' > '; break;
+ case 'ge': $stringParts[] = ' >= '; break;
+ case 'eq': case '==': $stringParts[] = ' == '; break;
+ case 'ne': case '!=': $stringParts[] = ' != '; break;
+ default: $stringParts[] = ' '.$token[0].' '; break;
+ }
+ break;
+ }
}
}
// for each variable that does not have a default value, add clause to throw error if any of them are NA
diff --git a/application/helpers/expressions/em_manager_helper.php b/application/helpers/expressions/em_manager_helper.php
index 998527213c0..4ec4dc603b8 100644
--- a/application/helpers/expressions/em_manager_helper.php
+++ b/application/helpers/expressions/em_manager_helper.php
@@ -865,13 +865,13 @@ public static function RevertUpgradeConditionsToRelevance($surveyId=NULL, $qid=N
* @return array
**/
public static function getLEMqcode2sgqa($iSurveyId){
- $LEM =& LimeExpressionManager::singleton();
- $LEM->SetSurveyId($iSurveyId); // This update session only if needed
- if( !in_array(Yii::app()->session['LEMlang'],Survey::model()->findByPk($iSurveyId)->getAllLanguages()) ) {
- $LEM->SetEMLanguage(Survey::model()->findByPk($iSurveyId)->language);// Reset language only if needed
- }
- $LEM->setVariableAndTokenMappingsForExpressionManager($iSurveyId);
- return $LEM->qcode2sgqa;
+ $LEM =& LimeExpressionManager::singleton();
+ $LEM->SetSurveyId($iSurveyId); // This update session only if needed
+ if( !in_array(Yii::app()->session['LEMlang'],Survey::model()->findByPk($iSurveyId)->getAllLanguages()) ) {
+ $LEM->SetEMLanguage(Survey::model()->findByPk($iSurveyId)->language);// Reset language only if needed
+ }
+ $LEM->setVariableAndTokenMappingsForExpressionManager($iSurveyId);
+ return $LEM->qcode2sgqa;
}
/**
@@ -8178,18 +8178,12 @@ public static function GetRelevanceAndTailoringJavaScript($bReturnArray=false)
$neededCanonicalAttr[] = $LEM->varNameAttr[$nc];
}
$neededAliases = array_unique($neededAliases);
- if (count($neededAliases) > 0)
- {
- $jsParts[] = "var LEMalias2varName = {\n";
- $jsParts[] = implode(",\n",$neededAliases);
- $jsParts[] = "};\n";
- }
- if (count($neededCanonicalAttr) > 0)
- {
- $jsParts[] = "var LEMvarNameAttr = {\n";
- $jsParts[] = implode(",\n",$neededCanonicalAttr);
- $jsParts[] = "};\n";
- }
+ $jsParts[] = "var LEMalias2varName = {\n";
+ $jsParts[] = implode(",\n",$neededAliases);
+ $jsParts[] = "};\n";
+ $jsParts[] = "var LEMvarNameAttr = {\n";
+ $jsParts[] = implode(",\n",$neededCanonicalAttr);
+ $jsParts[] = "};\n";
}
if (!$bReturnArray){
diff --git a/assets/packages/expressions/em_javascript.js b/assets/packages/expressions/em_javascript.js
index 73581e3e290..c3af91a42f8 100644
--- a/assets/packages/expressions/em_javascript.js
+++ b/assets/packages/expressions/em_javascript.js
@@ -1386,6 +1386,8 @@ function LEManyNA()
for (i=0;ifindByPk(self::$surveyId);
- if(empty($survey)) {
- throw new \Exception('getAllSurveyQuestions call with an invalid survey.');
- }
+ if(empty($survey)) {
+ throw new \Exception('getAllSurveyQuestions call with an invalid survey.');
+ }
$questions = [];
foreach($survey->groups as $group) {
$questionObjects = $group->questions;
@@ -108,7 +108,7 @@ public function getAllSurveyQuestions()
}
}
return $questions;
- }
+ }
/**
* @return void
@@ -133,4 +133,37 @@ public static function tearDownAfterClass()
self::$testSurvey = null;
}
}
+
+ /**
+ * Helper install and activate plugins by name
+ * @param string $pluginName
+ * @return void
+ */
+ public static function installAndActivatePlugin($pluginName)
+ {
+ $plugin = \Plugin::model()->findByAttributes(array('name'=>$pluginName));
+ if (!$plugin) {
+ $plugin = new \Plugin();
+ $plugin->name = $pluginName;
+ $plugin->active = 1;
+ $plugin->save();
+ } else {
+ $plugin->active = 1;
+ $plugin->save();
+ }
+ }
+
+ /**
+ * Helper dactivate plugins by name
+ * @param string $pluginName
+ * @return void
+ */
+ public static function deActivatePlugin($pluginName)
+ {
+ $plugin = \Plugin::model()->findByAttributes(array('name'=>$pluginName));
+ if ($plugin) {
+ $plugin->active = 0;
+ $plugin->save();
+ }
+ }
}
diff --git a/tests/data/surveys/survey_archive_statCountFunctionsTest.lsa b/tests/data/surveys/survey_archive_statCountFunctionsTest.lsa
new file mode 100644
index 00000000000..c80f7240a23
Binary files /dev/null and b/tests/data/surveys/survey_archive_statCountFunctionsTest.lsa differ
diff --git a/tests/functional/acceptance/15246-fixed-em-function/FixedFunctionExpressionPluginTest.php b/tests/functional/acceptance/15246-fixed-em-function/FixedFunctionExpressionPluginTest.php
new file mode 100644
index 00000000000..0f5c97810f4
--- /dev/null
+++ b/tests/functional/acceptance/15246-fixed-em-function/FixedFunctionExpressionPluginTest.php
@@ -0,0 +1,124 @@
+getAllSurveyQuestions();
+ $urlMan = \Yii::app()->urlManager;
+ $urlMan->setBaseUrl('http://' . self::$domain . '/index.php');
+ $url = $urlMan->createUrl(
+ 'survey/index',
+ [
+ 'sid' => self::$surveyId,
+ 'token' => 'tokenTest',
+ 'newtest' => "Y",
+ ]
+ );
+ try {
+ self::$webDriver->get($url);
+ /* 1st page */
+ $submit = self::$webDriver->findElement(WebDriverBy::id('ls-button-submit'));
+ $submit->click();
+ sleep(1); // Needed ?
+ /** Simple fixed value check **/
+ $textToCompare = self::$webDriver->findElement(WebDriverBy::id('statCountQ00'))->getText();
+ $this->assertEquals($textToCompare, "3", 'statCount(self.sgqa) usage broken : «' . $textToCompare ."» vs «3»");
+ $textToCompare = self::$webDriver->findElement(WebDriverBy::id('statCountQ01'))->getText();
+ $this->assertEquals($textToCompare, "3", 'statCount(Q01.sgqa) usage broken : «' . $textToCompare ."» vs «3»");
+ /** Relevance (and update) check **/
+ $this->assertFalse(
+ self::$webDriver->findElement(WebDriverBy::id('question'.$questions['Q01']->qid))->isDisplayed(),
+ "Q01 is not hidden by relevance"
+ );
+ $sgqa = self::$surveyId."X".$questions['Q00']->gid."X".$questions['Q00']->qid;
+ $Input = self::$webDriver->findElement(WebDriverBy::id('answer' . $sgqa ));
+ $Input->sendKeys('10');
+ $this->assertTrue(
+ self::$webDriver->findElement(WebDriverBy::id('question'.$questions['Q01']->qid))->isDisplayed(),
+ "Q01 is not shown by relevance after update Q00"
+ );
+ /** Submitted VS not submitted **/
+ $textToCompare = self::$webDriver->findElement(WebDriverBy::id('submitted'))->getText();
+ $this->assertEquals(
+ $textToCompare,
+ "3",
+ 'statCount(Q01.sgqa) usage broken in Q01: «' . $textToCompare ."» vs «3»"
+ );
+ $textToCompare = self::$webDriver->findElement(WebDriverBy::id('notSubmitted'))->getText();
+ $this->assertEquals(
+ $textToCompare,
+ "6",
+ 'statCount(Q01.sgqa) usage broken in Q01: «' . $textToCompare ."» vs «6»"
+ );
+ /* 2nd page */
+ $submit = self::$webDriver->findElement(WebDriverBy::id('ls-button-submit'));
+ $submit->click();
+ sleep(1); // Needed ?
+ /** Relevance on subquestion **/
+ $sgqa = self::$surveyId."X".$questions['Q03']->gid."X".$questions['Q03']->qid;
+ // Line to be relevant
+ $lineRelevance = self::$webDriver->findElements(
+ WebDriverBy::cssSelector("#javatbd".$sgqa."SQ001.ls-irrelevant")
+ );
+ $this->assertCount(0, $lineRelevance, 'Relevance is broken : SQ001 is irrelevant.');
+ // Line to be irrelevant
+ $lineRelevance = self::$webDriver->findElements(
+ WebDriverBy::cssSelector("#javatbd".$sgqa."SQ003.ls-irrelevant")
+ );
+ $this->assertCount(1, $lineRelevance, 'Relevance is broken : SQ003 is relevant.');
+ /** Text of subquestion **/
+ $textToCompare = self::$webDriver->findElement(WebDriverBy::id('answertext'.$sgqa.'SQ001'))->getText();
+ $this->assertEquals(
+ $textToCompare,
+ "Event #1 (still 7 places)",
+ 'Text on sub-questions broken «' . $textToCompare ."» vs «Event #1 (still 7 places)»"
+ );
+
+
+ } catch (\Exception $e) {
+ $filename = __CLASS__ ."_". __FUNCTION__;
+ self::$testHelper->takeScreenshot(self::$webDriver,$filename);
+ $this->assertFalse(
+ true,
+ 'Url: ' . $url . PHP_EOL .
+ 'Screenshot ' .$filename . PHP_EOL . $e->getMessage()
+ );
+ }
+ }
+
+ /**
+ * @inheritdoc
+ * @todo Deactivate and uninstall plugins ?
+ */
+ public static function tearDownAfterClass()
+ {
+ self::deActivatePlugin('statFunctions');
+ parent::tearDownAfterClass();
+ }
+
+}