-
Notifications
You must be signed in to change notification settings - Fork 986
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Dev #17261: Create new code artifacts to handle QuestionAttributes fe…
…tching (& saving) (#1893) Co-authored-by: encuestabizdevgit <devgit@encuesta.biz>
- Loading branch information
1 parent
b85f012
commit 693c6cb
Showing
10 changed files
with
625 additions
and
157 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
application/models/services/CoreQuestionAttributeProvider.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
<?php | ||
|
||
namespace LimeSurvey\Models\Services; | ||
|
||
/** | ||
* Provides question attribute definitions from question types | ||
*/ | ||
|
||
class CoreQuestionAttributeProvider extends QuestionAttributeProvider | ||
{ | ||
/** @inheritdoc */ | ||
public function getDefinitions($options = []) | ||
{ | ||
/** @var string question type */ | ||
$questionType = self::getQuestionType($options); | ||
if (empty($questionType)) { | ||
return []; | ||
} | ||
|
||
/** @var boolean */ | ||
$advancedOnly = !empty($options['advancedOnly']); | ||
|
||
return $this->getQuestionAttributes($questionType, $advancedOnly); | ||
} | ||
|
||
/** | ||
* Return the question attribute settings for the passed type (parameter) | ||
* | ||
* @param string $questionType : type of question (this is the attribute 'question_type' in table question_theme) | ||
* @param boolean $advancedOnly If true, only fetch advanced attributes | ||
* @return array<string,array> the attribute settings for this question type | ||
* returns values from getGeneralAttributesFromXml and getAdvancedAttributesFromXml if this fails | ||
* getAttributesDefinition and getDefaultSettings are returned | ||
* | ||
* @throws \CException | ||
*/ | ||
protected function getQuestionAttributes($questionType, $advancedOnly = false) | ||
{ | ||
$xmlFilePath = \QuestionTheme::getQuestionXMLPathForBaseType($questionType); | ||
if ($advancedOnly) { | ||
$generalAttributes = []; | ||
} else { | ||
// Get attributes from config.xml | ||
$generalAttributes = $this->getGeneralAttibutesFromXml($xmlFilePath); | ||
} | ||
$advancedAttributes = $this->getAdvancedAttributesFromXml($xmlFilePath); | ||
|
||
/** @var array<string,array> An array of question attributes */ | ||
$attributes = array_merge($generalAttributes, $advancedAttributes); | ||
|
||
return $attributes; | ||
} | ||
|
||
/** | ||
* Read question attributes from XML file and convert it to array | ||
* | ||
* @param string $xmlFilePath Path to XML | ||
* | ||
* @return array<string,array> The general attribute settings for this question type | ||
*/ | ||
protected function getGeneralAttibutesFromXml($xmlFilePath) | ||
{ | ||
/** @var array<string,array> An array of question attributes */ | ||
$attributes = []; | ||
|
||
if (file_exists($xmlFilePath)) { | ||
$extensionConfig = \ExtensionConfig::loadConfigFromFile($xmlFilePath); | ||
$xmlAttributes = $extensionConfig->getNodeAsArray('generalattributes'); | ||
// if only one attribute, then it doesn't return numeric index | ||
if (!empty($xmlAttributes) && !array_key_exists('0', $xmlAttributes['attribute'])) { | ||
$temp = $xmlAttributes['attribute']; | ||
unset($xmlAttributes); | ||
$xmlAttributes['attribute'][0] = $temp; | ||
} | ||
} else { | ||
return []; | ||
} | ||
|
||
// set $attributes array with attribute data | ||
if (!empty($xmlAttributes['attribute'])) { | ||
foreach ($xmlAttributes['attribute'] as $xmlAttribute) { | ||
/* settings the default value */ | ||
$attributes[$xmlAttribute] = self::getBaseDefinition(); | ||
/* settings the xml value */ | ||
$attributes[$xmlAttribute]['name'] = $xmlAttribute; | ||
} | ||
} | ||
|
||
return $attributes; | ||
} | ||
|
||
/** | ||
* Read question attributes from XML file and convert it to array | ||
* | ||
* @param string $xmlFilePath Path to XML | ||
* | ||
* @return array<string,array> The advanced attribute settings for this question type | ||
*/ | ||
protected function getAdvancedAttributesFromXml($xmlFilePath) | ||
{ | ||
/** @var array<string,array> An array of question attributes */ | ||
$attributes = []; | ||
|
||
if (file_exists($xmlFilePath)) { | ||
$extensionConfig = \ExtensionConfig::loadConfigFromFile($xmlFilePath); | ||
$xmlAttributes = $extensionConfig->getNodeAsArray('attributes'); | ||
// if only one attribute, then it doesn't return numeric index | ||
if (!empty($xmlAttributes) && !array_key_exists('0', $xmlAttributes['attribute'])) { | ||
$temp = $xmlAttributes['attribute']; | ||
unset($xmlAttributes); | ||
$xmlAttributes['attribute'][0] = $temp; | ||
} | ||
if (\PHP_VERSION_ID < 80000) { | ||
libxml_disable_entity_loader(true); | ||
} | ||
} else { | ||
return []; | ||
} | ||
|
||
// set $attributes array with attribute data | ||
if (!empty($xmlAttributes['attribute'])) { | ||
foreach ($xmlAttributes['attribute'] as $attribute) { | ||
if (empty($attribute['name'])) { | ||
/* Allow comments in attributes */ | ||
continue; | ||
} | ||
/* settings the default value */ | ||
$attributes[$attribute['name']] = self::getBaseDefinition(); | ||
/* settings the xml value */ | ||
foreach ($attribute as $property => $propertyValue) { | ||
if ($property === 'options' && !empty($propertyValue)) { | ||
foreach ($propertyValue['option'] as $option) { | ||
if (isset($option['value'])) { | ||
$value = is_array($option['value']) ? '' : $option['value']; | ||
$attributes[$attribute['name']]['options'][$value] = $option['text']; | ||
} | ||
} | ||
} else { | ||
$attributes[$attribute['name']][$property] = $propertyValue; | ||
} | ||
} | ||
} | ||
} | ||
|
||
return $attributes; | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
application/models/services/PluginQuestionAttributeProvider.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<?php | ||
|
||
namespace LimeSurvey\Models\Services; | ||
|
||
/** | ||
* Provides question attribute definitions from plugins | ||
*/ | ||
|
||
class PluginQuestionAttributeProvider extends QuestionAttributeProvider | ||
{ | ||
/** @inheritdoc */ | ||
public function getDefinitions($options = []) | ||
{ | ||
/** @var string question type */ | ||
$questionType = self::getQuestionType($options); | ||
if (empty($questionType)) { | ||
return []; | ||
} | ||
|
||
return $this->getAttributesFromPlugin($questionType); | ||
} | ||
|
||
/** | ||
* Returns the question attributes added by plugins ('newQuestionAttributes' event) for | ||
* the specified question type. | ||
* | ||
* @param string $questionType the question type to retrieve the attributes for. | ||
* | ||
* @return array<string,array> the question attributes added by plugins | ||
*/ | ||
protected function getAttributesFromPlugin($questionType) | ||
{ | ||
$event = new \LimeSurvey\PluginManager\PluginEvent('newQuestionAttributes'); | ||
$result = App()->getPluginManager()->dispatchEvent($event); | ||
|
||
$allPluginAttributes = (array) $result->get('questionAttributes'); | ||
if (empty($allPluginAttributes)) { | ||
return []; | ||
} | ||
|
||
$questionAttributeHelper = new QuestionAttributeHelper(); | ||
|
||
// Filter to get this question type attributes | ||
$questionTypeAttributes = $questionAttributeHelper->filterAttributesByQuestionType($allPluginAttributes, $questionType); | ||
|
||
// Complete category if missing | ||
$questionTypeAttributes = $questionAttributeHelper->fillMissingCategory($questionTypeAttributes, gT('Plugin')); | ||
|
||
return $questionTypeAttributes; | ||
} | ||
} |
162 changes: 162 additions & 0 deletions
162
application/models/services/QuestionAttributeFetcher.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
<?php | ||
|
||
namespace LimeSurvey\Models\Services; | ||
|
||
/** | ||
* Fetches question attribute definitions from the available providers | ||
*/ | ||
|
||
class QuestionAttributeFetcher | ||
{ | ||
/** @var \Question the question where the attributes should apply */ | ||
private $question; | ||
|
||
/** @var array<string,mixed> array of options to pass to the providers */ | ||
private $options = []; | ||
|
||
/** @var array<QuestionAttributeProvider> array of question attribute providers */ | ||
private $providers = []; | ||
|
||
public function __construct($providers = null) | ||
{ | ||
if (is_null($providers)) { | ||
$providers = [ | ||
new CoreQuestionAttributeProvider(), | ||
new ThemeQuestionAttributeProvider(), | ||
new PluginQuestionAttributeProvider(), | ||
]; | ||
} | ||
$this->providers = $providers; | ||
} | ||
|
||
/** | ||
* Returns the question attribute definitions according to the specified filters, | ||
* from all available sources. | ||
* | ||
* @return array<string,array> array of question attribute definitions | ||
* @throws \InvalidArgumentException if no question is specified | ||
*/ | ||
public function fetch() | ||
{ | ||
if (empty($this->question)) { | ||
throw new \InvalidArgumentException(gT("No question specified.")); | ||
} | ||
|
||
$questionAttributeHelper = new QuestionAttributeHelper(); | ||
|
||
/** @var array<string,array> retrieved attribute definitions*/ | ||
$allAttributes = []; | ||
|
||
// We retrieve the attributes from each provider, sanitize them, and merge them. | ||
foreach ($this->providers as $provider) { | ||
$options = array_merge($this->options, ['question' => $this->question]); | ||
$attributes = $provider->getDefinitions($options); | ||
$sanitizedAttributes = $questionAttributeHelper->sanitizeQuestionAttributes($attributes); | ||
$allAttributes = $questionAttributeHelper->mergeQuestionAttributes($allAttributes, $sanitizedAttributes); | ||
} | ||
|
||
// Sort by category | ||
uasort($allAttributes, 'categorySort'); | ||
|
||
return $allAttributes; | ||
} | ||
|
||
/** | ||
* Populates the $attributeDefinitions with their corresponding values. | ||
* If no $language is specified, the values for all survey languages are retrieved. | ||
* A question must be set with QuestionAttributeFetcher::setQuestion() before calling this method. | ||
* | ||
* @param array<string,array> $attributeDefinitions the array of attribute definitions that will be filled with values | ||
* @param string|null $language the language to use for i18n enabled attributes. If null, all survey languages are considered. | ||
* | ||
* @return array<string,array> the attributes from $attributeDefinitions, with their values. | ||
* @throws \Exception if the question () | ||
* | ||
* TODO: Move to QuestionAttributeHelper? Not sure if it belongs here. | ||
*/ | ||
public function populateValues($attributeDefinitions, $language = null) | ||
{ | ||
if (empty($attributeDefinitions)) { | ||
return []; | ||
} | ||
|
||
if (empty($this->question)) { | ||
return $attributeDefinitions; | ||
} | ||
|
||
$survey = $this->question->survey; | ||
if (empty($survey)) { | ||
throw new \Exception(gT(sprintf('This question has no survey - qid = %s', json_encode($this->question->qid)))); | ||
} | ||
|
||
$questionAttributeHelper = new QuestionAttributeHelper(); | ||
|
||
// Get attribute values | ||
$attributeValues = \QuestionAttribute::model()->getAttributesAsArrayFromDB($this->question->qid); | ||
|
||
// Fill attributes with values | ||
$languages = is_null($language) ? $survey->allLanguages : [$language]; | ||
$attributesWithValues = $questionAttributeHelper->fillAttributesWithValues($attributeDefinitions, $attributeValues, $languages); | ||
|
||
return $attributesWithValues; | ||
} | ||
|
||
/** | ||
* Sets the question to use when fetching the attributes | ||
* | ||
* @param \Question $question | ||
*/ | ||
public function setQuestion($question) | ||
{ | ||
$this->question = $question; | ||
} | ||
|
||
/** | ||
* Clears the filters | ||
*/ | ||
public function resetOptions() | ||
{ | ||
$this->options = []; | ||
} | ||
|
||
/** | ||
* Adds a new filter or overrides an existing one | ||
* | ||
* @param string $key the name of the filter | ||
* @param mixed $value | ||
*/ | ||
public function setOption($key, $value) | ||
{ | ||
$this->options[$key] = $value; | ||
} | ||
|
||
/** | ||
* Convenience method to add a question type filter | ||
* | ||
* @param string $questionType the name of the question theme | ||
*/ | ||
public function setQuestionType($questionType) | ||
{ | ||
$this->setOption('questionType', $questionType); | ||
} | ||
|
||
/** | ||
* Convenience method to add a question theme filter | ||
* | ||
* @param string $questionTheme the name of the question theme | ||
*/ | ||
public function setTheme($questionTheme) | ||
{ | ||
$this->setOption('questionTheme', $questionTheme); | ||
} | ||
|
||
/** | ||
* Convenience method to add the 'advancedOnly' filter | ||
* | ||
* @param boolean $advancedOnly | ||
*/ | ||
public function setAdvancedOnly($advancedOnly) | ||
{ | ||
$this->setOption('advancedOnly', $advancedOnly); | ||
} | ||
} |
Oops, something went wrong.