Skip to content

Commit

Permalink
Dev #17261: Create new code artifacts to handle QuestionAttributes fe…
Browse files Browse the repository at this point in the history
…tching (& saving) (#1893)

Co-authored-by: encuestabizdevgit <devgit@encuesta.biz>
  • Loading branch information
gabrieljenik and encuestabizdevgit committed Jun 7, 2021
1 parent b85f012 commit 693c6cb
Show file tree
Hide file tree
Showing 10 changed files with 625 additions and 157 deletions.
15 changes: 15 additions & 0 deletions application/models/ExtensionConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,19 @@ public function createVersionFetchers()

return $fetchers;
}

/**
* Returns the $nodeName XML node as an array
*
* @param string $nodeName the name of the node to retrieve
* @return array<mixed> the node contents as an array
*/
public function getNodeAsArray($nodeName)
{
if (empty($this->xml)) {
throw new Exception(gT("No XML config loaded"));
}
$node = json_decode(json_encode((array)$this->xml->$nodeName), true);
return $node;
}
}
11 changes: 11 additions & 0 deletions application/models/Question.php
Original file line number Diff line number Diff line change
Expand Up @@ -1533,4 +1533,15 @@ public function getScaledSubquestions()
}
return $results;
}

/**
* Get the question theme name
*
* @return string
*/
public function getQuestionThemeName()
{
$questionTheme = $this->getQuestionAttribute('question_template');
return !empty($questionTheme) ? $questionTheme : self::DEFAULT_QUESTION_THEME;
}
}
147 changes: 147 additions & 0 deletions application/models/services/CoreQuestionAttributeProvider.php
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 application/models/services/PluginQuestionAttributeProvider.php
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 application/models/services/QuestionAttributeFetcher.php
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);
}
}

0 comments on commit 693c6cb

Please sign in to comment.