Open main menu

Extension:QPoll/Developing scripts

qpuserchoice parser functionEdit

This function can be used to extract the answer (user's category choice(s)) of some previously submitted poll. Possible parameters are:

{{#qpuserchoice:poll_address
|question_id
|proposal_id
|default_answer
|category0_answer|category1_answer|...|categoryX_answer|...|categoryN_answer}}
  • poll_address: mandatory. the same as in the poll dependance attribute, given without double quotes.
  • question_id: mandatory, number, starting from 1.
  • proposal_id: mandatory, number, starting from 0.
  • default_answer: optional. if given, returns it's value when categoryX_answer was not given. If omitted or has a blank value, returns category_X_answer in case it's present in the list of parameters, otherwise returns the name of category number X for the current user, poll_address, question_id, proposal_id. Since v0.4.2, this parameter may optionally have special value $ which indicates that number of category (counting from 0) chosen by current user, will be returned instead (in case the user had answered to the polls question). User selected numbers of categories of particular question can be used in template to calculate overall results (for example, in psychological tests).
  • categoryX_answer: optional, (X: 0..N). If given, returns it's value when category X was chosen by current user in poll_address, question_id, proposal_id. In case the value is blank or omitted, returns the text answer (in case it's not blank), or just the name of category number X for the current user, poll_address, question_id, proposal_id.

Category names of checkboxes "[]" and radiobuttons "()" are treated as pre-defined (fixed) user proposal answers. Textfields "<>" may have "custom" texts of the user proposal answers (non-blank field).

Since v0.6.1, #iferror parser function may be used to detect non-existent / non-voted poll and to display a specified message in such case.

A simple example how proposal text of chained (dependent) poll can be altered via {{#qpuserchoice}}:

<qpoll dependance="Mainpage#almaz1" id="poll0">
{Checking various types of questions, 2
|type="()"}
|| Two categories span "A"|| Two categories span "B"
| 1 | 2 | 3 | 4
understanding the real goals
Did you mean {{#qpuserchoice:Mainpage#almaz1|1|2|the default|the Roman number one||the Roman number third||the Roman number fifth}} choice?
guessing true goals of the project
digging even more deeply into the problem
</qpoll>

Take look at examples for a longer more complete sample.

Interpretation scriptsEdit

General introductionEdit

Since v0.8.0 it's possible to analyze user-submitted poll form values via server-side scripts. Such interpretation scripts can validate user input, and either reject to store invalid user input (no poll voting data will be written into DB in such case) or to display calculated results (for example to implement real psychological or language tests). To edit and store such server-side scripts, new namespace is registered by extension:

[[Interpretation:YourScriptName]]

Script created at Interpretation:YourScriptName page should be usual php code, enclosed into qpinterpret xml tag:

<qpinterpret lang="php">
...
</qpinterpret>

Because no feature-rich editor is provided yet, it's better to use your favorite editor like vim or notepad++ then copy / paste text inside <qpinterpret lang="php"></qpinterpret> enclosure.

  • To bind an existing poll to newly created interpretation script, interpretation attribute should be added to qpoll definition header:
<qpoll id="YourPollId" interpretation="YourScriptName" max_attempts="99">
...
  • Optional max_attempts attribute allows to limit number of poll answer submissions by current user. Such feature might be very useful for education tests.

All of usual poll / question / proposal / category attributes can be used to define poll forms. Users are encouraged to use question "name" attribute and proposal "name" attribute to make interpretation scripts code cleaner and simplier.

Technical details of interpretationEdit

  • Currently scripts are executed in limited subset of PHP language, with (most of) dangerous built-in functions and classes being disallowed via php tokenizer. However, wise admin probably should restrict 'read' / 'edit' rights of 'NS_QP_INTERPRETATION' and 'NS_QP_INTERPRETATION_TALK' namespaces to sysops and bureaucrats only. Ordinary users and especially anonymous should be disallowed from looking at scripts code.
  • Syntax is checked via php lint when page in Interpretation namespace is saved. In case of errors these will be reported at the top of the page, usually with line number provided for convenience.
  • To see the list of allowed / disallowed language constructs look at qp_eval.php source.
  • There are plans to create Lua-bindings interpretation backend in the future.

Working exampleEdit

Polls definition codeEdit

Imagine one is having the following two polls defined at arbitrary wiki page. Copy / paste the following wiki code into arbitrary main namespace page of your wiki after installation of Extension:QPoll :

<qpoll id="personal_data" interpretation="check_personal_data" layout="tabular">
{Personal information (mandatory fields are marked with *)
|type="text" emptytext="yes" name="person"}
:|name="age" emptytext="no"|Age* <<::width=2>>
:|name="sex" emptytext="no"|Sex* <<male|female>>
:|name="ethnic_origin" emptytext="no" catreq=1|Ethnic origin<sup>*</sup> <<Russian|Tatar|Ukrainian|Bashkir>> or (''arbitrary'') : <<>>
:|birthplace|Birth place <<>>
:|livingplace|Living place <<>>
:|education|Education <<>>
:|qualification|Qualification <<>>
:|workplace|Workplace <<>>
:|occupance|Occupance <<>>
</qpoll>

----
Check your knowledge of English language.
<qpoll id="english_language_test" interpretation="check_english_language_test" max_attempts="10"  dependance="#personal_data">
{Please conjugate verb "accept".
|type="text" emptytext="no" name="verb_test"}
:|name="infinitive"|Infinitive <<|to accept|accepted|accepting>>
:|name="participle"|Participle <<|to accept|accepted|accepting>>
:|name="gerund"|Gerund  <<|to accept|accepted|accepting>>
{Complete the sentences.
|type="text" emptytext="no" name="sentences"}
:|name="short_text"|A duck is a type of <<|fish|mammal|bird>>.<br />Hi! My <<|name|country|city>> is Andrew. How are you?<br />Today is Sunday. Tomorrow is <<|Friday|Saturday|Monday>>.
</qpoll>
 
A successful submission of both polls will result in output similar to provided at the screenshot.
  • First poll has id personal_data. It's attached (bound) to interpretation script page Interpretation:Check_personal_data. The poll has one question with name person. That question has proposals with the following names defined: age, sex, ethnic_origin, birthplace, livingplace, education, qualification, workplace, occupance.
  • Second poll has id english_language_test. It's attached (bound) to interpretation script page Interpretation:Check_english_language_test. The poll has two question with the following names: verb_test, sentences. The question with name verb_test has proposals with the following names defined: infinitive, participle, gerund. The question with name sentences has proposal with the name short_text.
  • Also note that second poll has dependance="#personal_data" attribute, which means that it will be available for voting only after successful submission of first poll.
  • Let's explain declaration of proposal with name ethnic_origin:

:|name="ethnic_origin" emptytext="no" catreq=1|Ethnic origin<sup>*</sup> <<Russian|Tatar|Ukrainian|Bashkir>> or (''arbitrary'') : <<>>

  • This proposal defines two categories: first one is select with four pre-defined text options: <<Russian|Tatar|Ukrainian|Bashkir>>. Second category is ordinary text field: <<>>. It would be very hard to list all possible ethnic origins of the world, so we place the pre-defined list of most common ethnic origins into first field (select) while allowing to supply arbitrary ethnic_origin proposal value via second text field. We disable the submission of empty text input (which would be meaningless) via emptytext="no" proposal attribute. We allow to fill only one, not all (not both) of category fields via catreq=1 attribute, because we assume that there is no multiple ethnic origins at once (which actually might be false, but that's just an example). Note that both fields still could be submitted by user (which is not mandatory yet possible), thus we'll address such case in the interpretation script below.
  • We use tabular layout of first poll to align form fields into the grid. Try removing that attribute from first poll header. You'll see that fields will float in the proposal text unaligned.
  • Second poll is language test so it's categories (fields) intentionally are made unaligned, which is more common to "natural" text.

Definition of interpretation scriptsEdit

Note that both polls have interpretation attribute in their headers. Poll with id personal_data is bound to interpretation script which should be located at the page Interpretation:Check_personal_data . You may create such page at your wiki then place the following code into it:

<qpinterpret lang="php">
# we have only one question; this is a shortcut
$q = $answer['person'];
qp_debug('person',$q);

$result = array(
  'options' => array( 'store_erroneous' => false ),
  # possible qpc errors. 'person' is question name (we have only one question)
  'error' => array( 'person' => array() ),
  'structured' => array(
    'age' => $q['age'][0],
    'sex' => $q['sex'][0],
    'birthplace' => $q['birthplace'][0],
    'livingplace' => $q['livingplace'][0],
    'education' => $q['education'][0],
    'qualification' => $q['qualification'][0],
    'workplace' => $q['workplace'][0],
    'occupance' => $q['occupance'][0]
  )
);

# age must be a number from 18 to 100
$min_age = 18;
$max_age = 120;
if ( !ctype_digit( $q['age'][0] ) ||
     $q['age'][0] < $min_age || $q['age'][0] > $max_age ) {
  $result['error']['person']['age'] = 'Age must be a integer number from ' . $min_age . ' to ' . $max_age;
}

# We assume that one person may have only one ethnic_origin (that's just an example of category restriction).
if ( count( $q['ethnic_origin'] ) > 1 ) {
  $result['error']['person']['ethnic_origin'] = 'There can be only one ethnic origin of person.';
} else {
  $result['structured']['ethnic_origin'] = isset( $q['ethnic_origin'][0] ) ? $q['ethnic_origin'][0] : $q['ethnic_origin'][1];
}

# do not display any result to end user
$result['long'] = 'You are sucessfully registered for our polls.';

return $result;
</qpinterpret>

Poll with id english_language_test is bound to interpretation script which should be located at the page Interpretation:Check_english_language_test . You may create such page at your wiki then place the following code into it:

<qpinterpret lang="php">
qp_debug('answer',$answer);

$personal_data = qp_getStructuredInterpretation( 'QPoll01#personal_data' );
qp_debug('personal_data',$personal_data);

$result = array(
  'options' => array( 'store_erroneous' => true ),
  # Possible qpc errors.
  # 'verb_test' and 'sentences' keys are question names.
  'error' => array(
    'verb_test' => array(),
    'sentences' => array(),
  ),
  'structured' => array(
    # Structured data from previosely answered polls can be processed and stored again,
    'approx_age_in_days' => $personal_data['age'] * 365,
    'err_num' => 0,
  )
);

if ( $answer['verb_test']['infinitive'][0] !== 'to accept' ) {
  $result['error']['verb_test']['infinitive'] = 'Incorrect infinitive conjugation';
  $result['structured']['err_num']++;
}

if ( $answer['verb_test']['participle'][0] !== 'accepted' ) {
  $result['error']['verb_test']['participle'] = 'Incorrect participle conjugation';
  $result['structured']['err_num']++;
}

if ( $answer['verb_test']['gerund'][0] !== 'accepting' ) {
  $result['error']['verb_test']['gerund'] = 'Incorrect gerund conjugation';
  $result['structured']['err_num']++;
}

if ( $answer['sentences']['short_text'][0] !== 'bird' ) {
  $result['error']['sentences']['short_text'][0] = 'A bird is not a ' . $answer['sentences']['short_text'][0];
  $result['structured']['err_num']++;
}

if ( $answer['sentences']['short_text'][1] !== 'name' ) {
  $result['error']['sentences']['short_text'][1] = 'A person is not a ' . $answer['sentences']['short_text'][1];
  $result['structured']['err_num']++;
}

if ( $answer['sentences']['short_text'][2] !== 'Monday' ) {
  $result['error']['sentences']['short_text'][2] = 'Please study English days of the week.';
  $result['structured']['err_num']++;
}

if ( $result['structured']['err_num'] === 0 ) {
  $result['short'] = 'Well done!';
  $result['long'] = 'Congratulations!';
  if ( intval( $personal_data['age'] ) > 40 ) {
    $result['long'] .= " Especially because it is harder for people past 40 to learn foreign languages.";
  }
} else {
  $result['short'] = 'Not so good.';
  $result['long'] = "You have made " . $result['structured']['err_num'] . " error(s). Try better next time.";
}

qp_debug('result',$result);
return $result;
</qpinterpret>

Notes about general logic of interpretationEdit

 
Result of successful interpretation of poll with id personal_data with user vote displayed at Special:PollResults page.
  • Interpretation script executes only in case question / proposal restrictions are met: such as default or current catreq, max_attempts and emptytext attribute values.
  • When interpretation script begins to execute, user-submitted categories are placed into nested array $answers. When question names and proposal names are used (which is recommended), $answers keys will have form of $answer['question_name']['proposal_name'][$cidx], where $cidx is category index (0..n). Remember that each proposal may have one or multiple categories selected via multiple fields (text inputs, checkboxes) or selects with multiple options. Because of that, even proposals with single category defined are returned as $answer['question_name']['proposal_name'][0] for orthogonality of interpretation scripts.
  • When no question names / proposal names are used, $answer array will have the form of $answer[$qidx][$pidx][$cidx], where $qidx is (1..n), $pidx is (0..n), $cidx is (0..n).
  • Interpretation script is wrapped into function call so the result of interpretation should be provided via PHP return statement at any point of execution. For convinience it's placed into variable $result in the provided samples, which is a nested array with the following keys:
Key Purpose
'options' Set intepretation options, currently the only option available is store_erroneous, which boolean value indicates whether user-submitted data should be stored into DB even if script considers these to be invalid.
  • $result['options']['store_erroneous'] = true;
if you want to save incorrect results of education test (let's say foreign language test).
  • $result['options']['store_erroneous'] = false;
if you want to reject storage of incorrect result (let's say invalid personal information).
'error' Allows to specify exact error message for selected category via "qpc" coordinate (Question, Proposal, Category).
  • $result['error']['question_name']['proposal_name'][$cidx] = 'category message / category error';
will result in red notice ??? with provided message as title being displayed near the selected category input and the user vote considered incorrect. Multiple categories can be highlighted with error messages such way.
  • User data will be considered incorrect and results either will be saved or rejected depending on $result['options']['store_erroneous'] value.
'errmsg'
  • $result['errmsg'] = 'General error message.';
Will display error message that is related to the poll at whole. Use when it is impossible to pin-point error to precise question proposal category.
  • User data will be considered incorrect and results either will be saved or rejected depending on $result['options']['store_erroneous'] value.
'short'
  • $result['short'] = "Short message";
Short result of interpretation displayed to user after script finishes. It will be stored into DB and will be available to check at Special:PollResults page. Can be used both on success and on error.
  • Value is a plain text (escaped).
  • String longer than 255 bytes will be cut (possible data loss).
'long'
  • $result['long'] = "Long message";
Long result of interpretation displayed to user after script finishes. It will be stored into DB and will be available to check at Special:PollResults page. Can be used both on success and on error.
  • Value is a wiki text, processed by MediaWiki Parser in current parsing context. This allows to use all wikitext features, including wikitables to show to end-user on script completion.
  • String longer than 65535 bytes will be cut (possible data loss).
'structured'
  • $result['structured'] = array( 'key1' => 'val1', 'key2' => array( 'keyA' => 'valA', ... 'keyZ' => 'valZ' ), ... 'keyN' => 'valN' );
Structured result of interpretation is optional nested / associative array (max serialized length 65535 bytes). This result is not displayed to end-user. It will be stored into DB, will be available to check and for XLS export at Special:PollResults page.
  • Serialized representation of structured result longer than 65535 bytes will be rejected with error message, so no data loss should occur.
  • A good idea for school tests interpretation scripts is to create a simple error counter or more complex error logger as subvalue of structured answer. Such way, each error can be counted then intermediate or final mark can be calculated:
if ( $answer['sentences']['short_text'][0] !== 'bird' ) {
  # Provide exact question / proposal / category ("qpc") error message.
  $result['error']['sentences']['short_text'][0] = 'A bird is not a ' . $answer['sentences']['short_text'][0];
  # Increase error counter.
  $result['structured']['err_num']++;
}

List of built-in functionsEdit

function name purpose
qp_debug($name,$var) To simplify script development, extension provides built-in logging function qp_debug().
  • $name parameter is supposed to be name of dumped variable or log message.
  • $var parameter of the function is PHP variable which content is dumped into log file.

Log file named qpoll_debug_log.txt is written to $IP directory of the wiki. MediaWiki logger is used for writting that file. It's advised to comment out or to remove debugging calls in production environment. To make debugging sessions more secure, change debug logging file name in LocalSettings.php via:

require_once( "$IP/extensions/QPoll/QPoll/qp_user.php" );
$wgDebugLogGroups['qpoll'] = 'my_arbitrary_qpoll_debug_file.txt';
qp_lc($str) Returns lowercase version of $str according to wiki content language. Allows to compare user submitted text in case-insensitive manner.
qp_getStructuredInterpretation($poll_address) Return associative (possibly nested) array of user's submitted data structured interpretation of previously voted poll by it's address $poll_address.

Structured result of previously voted polls can be obtained in the currently voted poll attached script via qp_getStructuredInterpretation() function, which takes previously voted poll address as it's argument, for example:

$personal_data = qp_getStructuredInterpretation( 'QPoll01#personal_data' );

This way results of previous polls can be analyzed and processed further in the current poll attached script:

$result['structured']['approx_age_in_days'] = $personal_data['age'] * 365;

Random questionsEdit

Some kinds of education tests, such as Russian Unified State Exam, allow to randomly choose some questions from large list of total questions. Such kind of polls are supported since v0.8.0 via qpoll tag randomize XML-like attribute:

<qpoll randomize=3 ...>
{question1
|question_attributes}
metacategories
categories
{question2
|question_attributes}
metacategories
categories
...
{question10
|question_attributes}
metacategories
categories
</qpoll>

In such fictional example, 3 random questions will be presented to user from total number of 10. Interpretation script can detect whether currently submitted poll has randomized questions and their numbers / names via passed $usedQuestions script variable, which can be dumped into log file via the following call:

qp_debug( 'usedQuestions', $usedQuestions );

It's value will be array for randomized questions, boolean false for non-randomized ones. One may even specify randomize attribute value equals to total number of questions to make them shuffle.