Question object types
From LimeSurvey Manual
(Question module is a more accurate name by now.)
Question modules is about making questions in LS more object-oriented and modular, and letting users create and upload their own question types.
Basically, every place in the code which is doing a switch on question type should be replaced by a polymorphic call to an object, e.g. $object->renderFrontend();
Old wiki page: https://manual.limesurvey.org/Question_Objects
Use-cases
- Colour picker - user click on a picture with colours, and the position in the picture defines the colour.
- "... the surgeon would indicate the location of a fracture by drawing on a diagram of the knee."
- Signature question where user handwrites his/hers signature (https://help.surveygizmo.com/help/signature, http://www.surveygizmo.com/s3/1908889/Signature-Question-Type)
- Having more than one question in one "library" file, where some of the questions can be activated via a bought activation code.
- Text box with tags that will auto-complete with other respondents answers.
- Array question with one comment on each row: http://imgur.com/a/6JG0L
- Looping? http://www.sawtoothsoftware.com/support/videos/ssi-web-looping
- "Meta-question", like a bucket of questions, then pick x randomly to show?
TODO: Which of these use-cases are already covered by question themes?
Features
Must have
- Implemented as a Yii module
- BUT: We can't have multiple controllers in one request Olle (talk) 21:53, 30 August 2016 (CEST)
- We can use nested modules
- We can use something like : Yii::app()->getModule('moduleName');
- We can use something like: $this->renderPartial('module.views.myview');
- We can use widgets inside modules, and then call them with : $this->widget('myModule.components.MyWidget'); etc etc. Hard to say for now.
- Better to use Yii component? Question attributes can the be attached behaviours. Olle (talk) 21:55, 30 August 2016 (CEST)
- Another possibility is to subclass CWebModule and make our own module class, QuestionObjectModule, which includes some basic configuration etc common for all question objects. Then we can use Yii::app()->getQuestionModule() instead of ->getModule(). Olle (talk) 00:13, 14 October 2016 (CEST)
- Be able to add new question type without changing existing code
- User should be able to download and install questions types easily
- BUT: For security reasons we don't want to allow users to upload PHP (LimeSurvey Cloud). BUT: This is how all frameworks work. Maybe an option during install to allow?
- Have option in config.php:
allow_php_upload = true|false
- OR: Option during install: "Do you want to enable file upload for PHP files? Necessary for question object types."
- OR: Whitelist github accounts to download/update question types from
- Tutorial and examples on how to develop a new question type
Should have
- A question type manager, like plugin manager, where it's possible to install, deactivate and manager which questions are available on the system
- A system to implement unit-tests for questions
- UML diagram of all classes involved in the implementation
- Events that plugins can hook into if they want to change the overall behaviour of questions
Could have
- Support a drag-n-drop interface for survey creation.
- Dynamically create new question types by dragging together certain attributes, or picking attributes and properties from a list. Example: Array question with date inputs.
- WYSIWYG survey creation.
- Question wizard
- Market place where you can buy new question types.
Project management
- What features will be included? How to decide what to scrap and what to keep?
- How many prototypes will need to be made? How could me move from one prototype to the next?
- How high priority is this feature?
Open issues
- Is array it's own object type, or is it a property of a question? Could we have an array of any question typ, e.g. yes/no questions?
- Cross-cutting concerns:
- What if the user wants to add a comment field to all questions in the survey? Use a plugin? Or time limit, which can be added to all questions, but shouldn't take up space if it's not used. Shnoulle/Sam recommends EAV table; Olle thinks it's too schema-less and that question types could modify the database instead. Actually all this part was in QuestionAttribute.
- Sam:
- I think we can identify some properties that apply to (almost) all question types, these should be in the question table (think EM expression, or mandatory or always hidden).
- Then there are properties that are specific for a question type, for example the maximum / minimum number for a text question, or the step size for a slider. Storing these in the main table as separate column will make create lots of columns with very sparse data. Alternatives are EAV or serialization. Note: LS core will never do anything with this data except retrieve / store it, so serialization is a valid option.
- Then there are generic question settings that someone might want to add support for, for example time_limit. There should be some way for plugins to add these settings to all questions and then handle their values when the time comes. -- This is something new and needs to be further explored.
- Sam:
- Example of some part that can not be Question Type object, but must extend Question type
- Updating EM/core system : example for hidden or random_group, validation (em_validation, numeric, min , max ...)
- Updating HTML produced only maximum_chars, validation can update HTML part too : min+max+step on numeric for example (usage of type="number")
- Add some JS/css or some HTML element (addContent, registerScript ....) time_limit
- Other cross-cutting concerns could be handled better with plugins Olle (talk) 23:47, 10 October 2016 (CEST)
- What if the user wants to add a comment field to all questions in the survey? Use a plugin? Or time limit, which can be added to all questions, but shouldn't take up space if it's not used. Shnoulle/Sam recommends EAV table; Olle thinks it's too schema-less and that question types could modify the database instead. Actually all this part was in QuestionAttribute.
- Composability: Should it be possible to combine two questions into one, or pick-and-choose among "question elements"?
- Should we also change the way we save answer data? If yes, in which step - first or last? Which database design should we use instead?
- Can we inherit views?
- Interaction with plugins, e.g. Stata XML export? No other way around it than using switch in plugin, or each question type should itself supply a method the plugin can use?
- Can we release new features step by step as beta? E.g. the question object manager.
- Would a question object need its own database table, or is it enough to extend lime_questions? Should it be possible for question objects to create database tables at installation?
- Should it be possible to create new attributes, or should question only list which core attributes it is using? What if two different questions define the same attribute?
- Should be possible to override an existing question object? Add field in config.json: "override": "genderQuestion".
- To ease the development for client programmers, there could be a system that tests and checks a question object and spits out a list of what's missing and/or wrong.
- In the long run, we probably want to factor out translation elements from the Question object.
- If we move to Yii 2, a question module could be installed using composer.
- Permission. Should the base classes check for permission, so the client programmer doesn't have to? Or is it better to leave it to the programmer?
Diagrams
Domain
A simple domain diagram over LimeSurvey:
Module
Overview of the question module and its interaction point with the rest of the system (particularly the old front-end).
Class hierarchy
Below a simple UML diagram that highlights the problem between inheritance vs attribute composition: Numerical question is split between SingleAnswerQuestion and MultipleAnswerQuestion.
It's possible to imagine a question creation wizard where you first choose between single/multiple/no answer question, then pick "attributes" like text, numeric, yes/no, etc. "Do you want to add subquestion?" "Please add answer options." And so on.
Another point about this diagram is that we presently only have one model in LS: Question, which corresponds to exactly one database table. Adding more models, we can choose to continue to use only one table or model an inheritance relation in the database.
Properties like "date", "text" and "numeric" should be factored out as input attributes.
Implementation
Object hierarchy.
Implementation should be done in a piece by piece manner, meaning we first implement one question type fully, e.g. long free text, that co-exists with the old system. The old system is then replaced one question type at a time.
I'm currently developing a prototype in this branch (although I'm afraid I'm not following my own advice - I'm developing the backend part first, to see if all question types can be saved to database). Olle (talk) 03:06, 9 October 2016 (CEST)
Methods
This is a list of functions that question types could implement. They could be included in a "base interface" or abstract base class.
Function name | Meaning | Mandatory |
---|---|---|
renderFrontend | Former qanda code; render question for survey taker | Y |
renderDataEntryForm | Render form for manual insertion of data. NB: Same as renderFrontend? | Y |
getSettings | Return settings (array) that can be used by a settings widget for edit form in backend | Y |
renderCustomSettings | Possibly custom HTML settings? | |
getExportData | Way of exporting : data + "syntax of data" : SPSS/Stata/triple-S/etc ... Some "settings" can update this too (interger value for numeric for example) | |
getStatisticsData | Return statistics data that will be fed to a renderer (like HTML, PDF, etc) | Y |
Functions created during prototype 1:
Function | Input | Output | Description |
---|---|---|---|
getAnswer | $ia | HTML | Fill the answer part of the question. Inputs etc |
getQuestionCodes | $ia | string array | All question codes in answer |
getAttributeNames | - | attribute array | All attributes available for this question ('hidden', 'mandatory', etc). Method not necessary if we use config.json to define attributes. |
getQuestionText | $ia, $questionAttributes, $questionModel | array (question text) | Array of all different text element in question. |
Implemented as Yii module, with views, models and controllers (http://www.yiiframework.com/doc/guide/1.1/en/basics.module#creating-module).
Replace switch-cases in:
- Frontend render (render answers, qanda)
- Backend render (edit question, file
questions.php
) - Statistics
- SQL and database queries, included extra field the question might define
- Expression manager
Ideally, each switch case in the code should have a unit test or functional test to test it with.
Modules, controllers
To add modules dynamically into Yii without changing any config files, some hackery is required. Here is a forum post about it: http://www.yiiframework.com/forum/index.php/topic/23467-dynamically-load-modules-models-and-configurations/
If you want to be able to call controllers outside the controllers/ folder (in my case, in application/core/questions/QuestionObject), you need to dynamically update $controllerMap in the application class (LSYii_Application). http://www.yiiframework.com/doc/api/1.1/CWebApplication#controllerMap-detail
File hierarchy
The module structure imposes a certain file hierarchy to question objects:
QuestionTypeName/
config.json
NameQuestionObject.php
NameAdminController.php
views/
models/
Name.php
assets/
css/
js/
- BUT: Would we need models? The question object might just feed an array or data object to activateSurvey and createfieldmap, and then LS will do the rest.
config.json
Spec of config.json.
TODO: How to localize description?
TODO: Move to config.xml, as with question themes.
Note that the more behaviour that can be defined by a config file, the easier it is to code-generate question types. (This behaviour can be overridden manually for question type coders.)
{
"name": "MyNewQuestionModule", // This must correspond to the class name and folder name
"questionModel": "MyNewQuestion", // Class name of the question model; inherits from Question; derive from convention instead?
"description": "With this question you can mark a fracture on a picture", // Showed in question manager
"version": "1.0.0",
"attributes": [
"max_num_values"
],
"haveSubquestions": true, // Control the menu bar at question view
"haveAnswerOptions": true,
"haveDefaultValues": true,
// Scale counts control if x/y axis? See getQuestionTypeList()
"scaleCounts": [
"subquestions": 1,
"answerOptions": 1
],
"category": "array" // Used for question dropdown list in edit question
}
Installation
When a new object type is installed, it could run a config.json file to e.g. add column to the lime_questions table, or create a new table which maps an inheritance relation between BaseQuestion class and whatever model is defined in the question object extension.
Database
Curent changes need to be done to LimeSurvey database:
- Add new column
extended_type
to lime_questions table, varchar(31) - All new question types have
lime_question.type = '?'
- Add new database table
lime_extended_question_types
, which - like plugin table - will list all installed question objects.
Extended questions: lime_extended_question_types
lime_extended_question_types | |||
---|---|---|---|
Field | Type | Description | Example |
xqid | integer | Extended question type id | 1 |
name | string | Name of question type; showed in question manager | "TestObjectQuestion" |
description | text | Description of this question | "Question to answer the ultimate question" |
active | boolean | Active or not; managed in question manager | |
core | boolean | True if this is a core question type | |
config | text | raw string of config.json | {"name": "TestObjectQuestion"}
|
We can also create a new base model QuestionObjectBaseModel that simulates inheritance on the database level by fetching data from an EAV table automatically. The event onAfterFind can be used, e.g.. For more discussion, see this forum post about multiple table inheritance.
A model like that would need to populate additionally fields on:
- Save
- Load/find
- Search
Current question types
From statistics.php:
- 1 - Array Dual Scale
- 5 - 5 Point Choice
- A - Array (5 Point Choice)
- B - Array (10 Point Choice)
- C - Array (Yes/No/Uncertain)
- D - Date
- E - Array (Increase, Same, Decrease)
- F - Array (Flexible Labels)
- G - Gender
- H - Array (Flexible Labels) by Column
- I - Language Switch
- K - Multiple Numerical Input
- L - List (Radio)
- M - Multiple choice
- N - Numerical Input
- O - List With Comment
- P - Multiple choice with comments
- Q - Multiple Short Text
- R - Ranking
- S - Short Free Text
- T - Long Free Text
- U - Huge Free Text
- X - Boilerplate Question
- Y - Yes/No
- ! - List (Dropdown)
- : - Array (Flexible Labels) multiple drop down
- ; - Array (Flexible Labels) multiple texts
- | - File Upload
Categories:
- Single-choice questions
- Arrays
- Mask questions
- Text questions
- Multiple-choice questions
Prototype 1
As a first cycle of development (iteration), one could implement a completely new question type (say, drawing board) with the new system on top of the old one. The purpose would be to get a better feeling of the problem domain, what changes are needed, if the current approach is possible, and so on. The first prototype is developed in this fork.
After the prototype is done, the existent questions can be exported to the new system one by one. This represents a "horizontal" development style, where the complete problem is solved for _one_ case in the first cycle, in contrast to exporting the entire qanda to the new system, then the entire statistics module, etc. A horizontal style would give faster results and introduce lesser mechanical work, focusing on design and creativity which is needed in the beginning.
Example from qanda:
if (is_object($ia))
{
$ia->renderFrontend();
}
else
{
switch ($ia[4]) // $ia[4] is question type
{
case 'X': //BOILERPLATE QUESTION
$values = do_boilerplate($ia);
break;
// and so on for all question types
}
}
When all questions have been exported to the new system, this snippet will just be:
$ia->renderFrontend();
More sketches:
class QuestionObjectName extends QuestionObjectBase
{
/**
* @param Question $question The question record from database
* @param array $ia Same data that is fed to retrieveAnswers
* @return string HTML for this question
*/
public function renderFrontend(Question $question, array $ia)
{
return $this->renderPartial('frontend', array(), true);
}
}
TODO:
- Default answers with x/y scale (scale count = 2 for subquestions)
Sam's dev branch implementation
Note, this is the current state and some of these are mostly because it is a refactoring from LS2.
1. Uses an interface.
2. Defines what columns it needs via a fieldname => column type map.
3. Defines what EM expressions apply to it.
4. Defines a render functionality that renders the content.
5. Defines a list of classes that should be in the wrapping div.
Storing custom question attributes
Assumptions: 1. All questions are always (partly) stored in the `Questions` table. 2. There exist question attributes that are unknown (we don't know their names, their types and how many there are). 3. Known question attributes (like: mandatory) are always stored normalized as a column in the `Questions` table, the list below applies only to custom attributes. Options for storing custom question attributes: 1. Single Table Inheritance (Add columns to `questions` table). - Risk of collisions - Harder to import backups into environments that slightly differ. - Creates a very sparse table. + Fast for retrieving + Easy to support full model validation in Yii + Easy to interpret database / SQL exports. o Full support for advanced queries / indexes (LS does not do advanced queries on this table) 2. Use EAV. - Schemaless - No data types (everything is a string) + Current approach. + Easy to interpret database / SQL exports. o Little support for advanced queries (LS does not do advanced queries on this table) + Easy to support full model validation in Yii - Requires transactions if we want to have atomic writes. Olle: Why would we need atomic writes to question attributes? How could there be a race condition? 3. Use EBLOB - store question attributes in a serialized form like JSON / XML. - Schemaless o More datatypes than EAV: string, int, float, bool, NULL. Fewer datatypes than STI or CTI. + Fast for retrieving + Easy to support full model validation in Yii o Little / slow support for advanced queries (LS does not do advanced queries on this table) o Write amplification (Yii writes the whole record regardless of what columns changed) - Unfriendly for direct database editing. 4.Class table inheritance (Separate table per question type containing a PK that is a FK into `Questions`) + Strict schema + Easy to interpret database / SQL exports. - Requires transactions if we want to have atomic writes. - Complicated to implement in Yii. - Reading requires at least one query per question type, depending on implemention up to 1 query per question.