MDL-27490 Implement a manage question behaviours admin page

While doing this, I found various bugs in the manages question types admin page, and so fixed them, and updated the code
there to use $OUTPUT and html_writer.

AMOS BEGIN
 MOV [cannotdeletemissingqtype,admin],[cannotdeletemissingqtype,question]
 MOV [cannotdeleteqtypeinuse,admin],[cannotdeleteqtypeinuse,question]
 MOV [cannotdeleteqtypeneeded,admin],[cannotdeleteqtypeneeded,question]
 MOV [deleteqtypeareyousure,admin],[deleteqtypeareyousure,question]
 MOV [deleteqtypeareyousuremessage,admin],[deleteqtypeareyousuremessage,question]
 MOV [deletingqtype,admin],[deletingqtype,question]
 MOV [numquestions,admin],[numquestions,question]
 MOV [numquestionsandhidden,admin],[numquestionsandhidden,question]
 MOV [qtypedeletefiles,admin],[qtypedeletefiles,question]
 MOV [uninstallqtype,admin],[uninstallqtype,question]
AMOS END
This commit is contained in:
Tim Hunt 2011-06-15 20:18:33 +01:00
parent d037c6bc3c
commit fde4560dae
15 changed files with 549 additions and 130 deletions

302
admin/qbehaviours.php Normal file
View File

@ -0,0 +1,302 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle 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.
//
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Allows the admin to manage question behaviours.
*
* @package moodlecore
* @subpackage questionengine
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../config.php');
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->libdir . '/adminlib.php');
require_once($CFG->libdir . '/tablelib.php');
// Check permissions.
require_login();
$systemcontext = get_context_instance(CONTEXT_SYSTEM);
require_capability('moodle/question:config', $systemcontext);
admin_externalpage_setup('manageqbehaviours');
$thispageurl = new moodle_url('/admin/qbehaviours.php');
$behaviours = get_plugin_list('qbehaviour');
// Get some data we will need - question counts and which types are needed.
$counts = $DB->get_records_sql_menu("
SELECT behaviour, COUNT(1)
FROM {question_attempts} GROUP BY behaviour");
$needed = array();
$archetypal = array();
foreach ($behaviours as $behaviour => $notused) {
if (!array_key_exists($behaviour, $counts)) {
$counts[$behaviour] = 0;
}
$needed[$behaviour] = $counts[$behaviour] > 0;
$archetypal[$behaviour] = question_engine::is_behaviour_archetypal($behaviour);
}
foreach ($behaviours as $behaviour => $notused) {
foreach (question_engine::get_behaviour_required_behaviours($behaviour) as $reqbehaviour) {
$needed[$reqbehaviour] = true;
}
}
foreach ($counts as $behaviour => $count) {
if (!array_key_exists($behaviour, $behaviours)) {
$counts['missingtype'] += $count;
}
}
// Work of the correct sort order.
$config = get_config('question');
$sortedbehaviours = array();
foreach ($behaviours as $behaviour => $notused) {
$sortedbehaviours[$behaviour] = question_engine::get_behaviour_name($behaviour);
}
if (!empty($config->behavioursortorder)) {
$sortedbehaviours = question_engine::sort_behaviours($sortedbehaviours,
$config->behavioursortorder, '');
}
if (!empty($config->disabledbehaviours)) {
$disabledbehaviours = explode(',', $config->disabledbehaviours);
} else {
$disabledbehaviours = array();
}
// Process actions ============================================================
// Disable.
if (($disable = optional_param('disable', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
if (!isset($behaviours[$disable])) {
print_error('unknownbehaviour', 'question', $thispageurl, $disable);
}
if (array_search($disable, $disabledbehaviours) === false) {
$disabledbehaviours[] = $disable;
set_config('disabledbehaviours', implode(',', $disabledbehaviours), 'question');
}
redirect($thispageurl);
}
// Enable.
if (($enable = optional_param('enable', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
if (!isset($behaviours[$enable])) {
print_error('unknownbehaviour', 'question', $thispageurl, $enable);
}
if (!$archetypal[$enable]) {
print_error('cannotenablebehaviour', 'question', $thispageurl, $enable);
}
if (($key = array_search($enable, $disabledbehaviours)) !== false) {
unset($disabledbehaviours[$key]);
set_config('disabledbehaviours', implode(',', $disabledbehaviours), 'question');
}
redirect($thispageurl);
}
// Move up in order.
if (($up = optional_param('up', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
if (!isset($behaviours[$up])) {
print_error('unknownbehaviour', 'question', $thispageurl, $up);
}
// This function works fine for behaviours, as well as qtypes.
$neworder = question_reorder_qtypes($sortedbehaviours, $up, -1);
set_config('behavioursortorder', implode(',', $neworder), 'question');
redirect($thispageurl);
}
// Move down in order.
if (($down = optional_param('down', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
if (!isset($behaviours[$down])) {
print_error('unknownbehaviour', 'question', $thispageurl, $down);
}
// This function works fine for behaviours, as well as qtypes.
$neworder = question_reorder_qtypes($sortedbehaviours, $down, +1);
set_config('behavioursortorder', implode(',', $neworder), 'question');
redirect($thispageurl);
}
// Delete.
if (($delete = optional_param('delete', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
// Check it is OK to delete this question type.
if ($delete == 'missing') {
print_error('cannotdeletemissingbehaviour', 'question', $thispageurl);
}
if (!isset($behaviours[$delete])) {
print_error('unknownbehaviour', 'question', $thispageurl, $delete);
}
$behaviourname = $sortedbehaviours[$delete];
if ($counts[$delete] > 0) {
print_error('cannotdeletebehaviourinuse', 'question', $thispageurl, $behaviourname);
}
if ($needed[$delete] > 0) {
print_error('cannotdeleteneededbehaviour', 'question', $thispageurl, $behaviourname);
}
// If not yet confirmed, display a confirmation message.
if (!optional_param('confirm', '', PARAM_BOOL)) {
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('deletebehaviourareyousure', 'question', $behaviourname));
echo $OUTPUT->confirm(
get_string('deletebehaviourareyousuremessage', 'question', $behaviourname),
new moodle_url($thispageurl, array('delete' => $delete, 'confirm' => 1)),
$thispageurl);
echo $OUTPUT->footer();
exit;
}
// Do the deletion.
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('deletingbehaviour', 'question', $behaviourname));
// Delete any configuration records.
if (!unset_all_config_for_plugin('qbehaviour_' . $delete)) {
echo $OUTPUT->notification(get_string('errordeletingconfig', 'admin', 'qbehaviour_' . $delete));
}
if (($key = array_search($delete, $disabledbehaviours)) !== false) {
unset($disabledbehaviours[$key]);
set_config('disabledbehaviours', implode(',', $disabledbehaviours), 'question');
}
$behaviourorder = explode(',', $config->behavioursortorder);
if (($key = array_search($delete, $behaviourorder)) !== false) {
unset($behaviourorder[$key]);
set_config('behavioursortorder', implode(',', $behaviourorder), 'question');
}
// Then the tables themselves
drop_plugin_tables($delete, get_plugin_directory('qbehaviour', $delete) . '/db/install.xml', false);
// Remove event handlers and dequeue pending events
events_uninstall('qbehaviour_' . $delete);
$a->behaviour = $behaviourname;
$a->directory = get_plugin_directory('qbehaviour', $delete);
echo $OUTPUT->box(get_string('qbehaviourdeletefiles', 'question', $a), 'generalbox', 'notice');
echo $OUTPUT->continue_button($thispageurl);
echo $OUTPUT->footer();
exit;
}
// End of process actions ==================================================
// Print the page heading.
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('manageqbehaviours', 'admin'));
// Set up the table.
$table = new flexible_table('qbehaviouradmintable');
$table->define_baseurl($thispageurl);
$table->define_columns(array('behaviour', 'numqas', 'version', 'requires',
'available', 'delete'));
$table->define_headers(array(get_string('behaviour', 'question'), get_string('numqas', 'question'),
get_string('version'), get_string('requires', 'admin'),
get_string('availableq', 'question'), get_string('delete')));
$table->set_attribute('id', 'qbehaviours');
$table->set_attribute('class', 'generaltable generalbox boxaligncenter boxwidthwide');
$table->setup();
// Add a row for each question type.
foreach ($sortedbehaviours as $behaviour => $behaviourname) {
$row = array();
// Question icon and name.
$row[] = $behaviourname;
// Count
$row[] = $counts[$behaviour];
// Question version number.
$version = get_config('qbehaviour_' . $behaviour, 'version');
if ($version) {
$row[] = $version;
} else {
$row[] = html_writer::tag('span', get_string('nodatabase', 'admin'), array('class' => 'disabled'));
}
// Other question types required by this one.
$requiredbehaviours = question_engine::get_behaviour_required_behaviours($behaviour);
if (!empty($requiredbehaviours)) {
$strrequiredbehaviours = array();
foreach ($requiredbehaviours as $required) {
$strrequiredbehaviours[] = $sortedbehaviours[$required];
}
$row[] = implode(', ', $strrequiredbehaviours);
} else {
$row[] = '';
}
// Are people allowed to create new questions of this type?
$rowclass = '';
if ($archetypal[$behaviour]) {
$enabled = array_search($behaviour, $disabledbehaviours) === false;
$icons = question_behaviour_enable_disable_icons($behaviour, $enabled);
if (!$enabled) {
$rowclass = 'dimmed_text';
}
} else {
$icons = $OUTPUT->spacer() . ' ';
}
// Move icons.
$icons .= question_behaviour_icon_html('up', $behaviour, 't/up', get_string('up'), null);
$icons .= question_behaviour_icon_html('down', $behaviour, 't/down', get_string('down'), null);
$row[] = $icons;
// Delete link, if available.
if ($needed[$behaviour]) {
$row[] = '';
} else {
$row[] = html_writer::link(new moodle_url($thispageurl,
array('delete' => $behaviour, 'sesskey' => sesskey())), get_string('delete'),
array('title' => get_string('uninstallbehaviour', 'question')));
}
$table->add_data($row, $rowclass);
}
$table->finish_output();
echo $OUTPUT->footer();
function question_behaviour_enable_disable_icons($behaviour, $enabled) {
if ($enabled) {
return question_behaviour_icon_html('disable', $behaviour, 'i/hide',
get_string('enabled', 'question'), get_string('disable'));
} else {
return question_behaviour_icon_html('enable', $behaviour, 'i/show',
get_string('disabled', 'question'), get_string('enable'));
}
}
function question_behaviour_icon_html($action, $behaviour, $icon, $alt, $tip) {
global $OUTPUT;
return $OUTPUT->action_icon(new moodle_url('/admin/qbehaviours.php',
array($action => $behaviour, 'sesskey' => sesskey())),
new pix_icon($icon, $alt, 'moodle', array('title' => '')),
null, array('title' => $tip)) . ' ';
}

View File

@ -37,6 +37,7 @@ require_capability('moodle/question:config', $systemcontext);
$canviewreports = has_capability('report/questioninstances:view', $systemcontext);
admin_externalpage_setup('manageqtypes');
$thispageurl = new moodle_url('/admin/qtypes.php');
$qtypes = question_bank::get_all_qtypes();
@ -80,84 +81,84 @@ $sortedqtypes = question_bank::sort_qtype_array($sortedqtypes, $config);
// Disable.
if (($disable = optional_param('disable', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
if (!isset($qtypes[$disable])) {
print_error('unknownquestiontype', 'question', new moodle_url('/admin/qtypes.php'), $disable);
print_error('unknownquestiontype', 'question', $thispageurl, $disable);
}
set_config($disable . '_disabled', 1, 'question');
redirect(admin_url('qtypes.php'));
redirect($thispageurl);
}
// Enable.
if (($enable = optional_param('enable', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
if (!isset($qtypes[$enable])) {
print_error('unknownquestiontype', 'question', new moodle_url('/admin/qtypes.php'), $enable);
print_error('unknownquestiontype', 'question', $thispageurl, $enable);
}
if (!$qtypes[$enable]->menu_name()) {
print_error('cannotenable', 'question', new moodle_url('/admin/qtypes.php'), $enable);
print_error('cannotenable', 'question', $thispageurl, $enable);
}
unset_config($enable . '_disabled', 'question');
redirect(new moodle_url('/admin/qtypes.php'));
redirect($thispageurl);
}
// Move up in order.
if (($up = optional_param('up', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
if (!isset($qtypes[$up])) {
print_error('unknownquestiontype', 'question', new moodle_url('/admin/qtypes.php'), $up);
print_error('unknownquestiontype', 'question', $thispageurl, $up);
}
$neworder = question_reorder_qtypes($sortedqtypes, $up, -1);
question_save_qtype_order($neworder, $config);
redirect(new moodle_url('/admin/qtypes.php'));
redirect($thispageurl);
}
// Move down in order.
if (($down = optional_param('down', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
if (!isset($qtypes[$down])) {
print_error('unknownquestiontype', 'question', admin_url('qtypes.php'), $down);
print_error('unknownquestiontype', 'question', $thispageurl, $down);
}
$neworder = question_reorder_qtypes($sortedqtypes, $down, +1);
question_save_qtype_order($neworder, $config);
redirect(new moodle_url('/admin/qtypes.php'));
redirect($thispageurl);
}
// Delete.
if (($delete = optional_param('delete', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
// Check it is OK to delete this question type.
if ($delete == 'missingtype') {
print_error('cannotdeletemissingqtype', 'admin', new moodle_url('/admin/qtypes.php'));
print_error('cannotdeletemissingqtype', 'question', $thispageurl);
}
if (!isset($qtypes[$delete])) {
print_error('unknownquestiontype', 'question', new moodle_url('/admin/qtypes.php'), $delete);
print_error('unknownquestiontype', 'question', $thispageurl, $delete);
}
$qtypename = $qtypes[$delete]->local_name();
if ($counts[$delete]->numquestions + $counts[$delete]->numhidden > 0) {
print_error('cannotdeleteqtypeinuse', 'admin', new moodle_url('/admin/qtypes.php'), $qtypename);
print_error('cannotdeleteqtypeinuse', 'question', $thispageurl, $qtypename);
}
if ($needed[$delete] > 0) {
print_error('cannotdeleteqtypeneeded', 'admin', new moodle_url('/admin/qtypes.php'), $qtypename);
print_error('cannotdeleteqtypeneeded', 'question', $thispageurl, $qtypename);
}
// If not yet confirmed, display a confirmation message.
if (!optional_param('confirm', '', PARAM_BOOL)) {
$qtypename = $qtypes[$delete]->local_name();
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('deleteqtypeareyousure', 'admin', $qtypename));
echo $OUTPUT->confirm(get_string('deleteqtypeareyousuremessage', 'admin', $qtypename),
new moodle_url('/admin/qtypes.php', array('delete' => $delete, 'confirm' => 1)),
new moodle_url('/admin/qtypes.php'));
echo $OUTPUT->heading(get_string('deleteqtypeareyousure', 'question', $qtypename));
echo $OUTPUT->confirm(get_string('deleteqtypeareyousuremessage', 'question', $qtypename),
new moodle_url($thispageurl, array('delete' => $delete, 'confirm' => 1)),
$thispageurl);
echo $OUTPUT->footer();
exit;
}
// Do the deletion.
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('deletingqtype', 'admin', $qtypename));
echo $OUTPUT->heading(get_string('deletingqtype', 'question', $qtypename));
// Delete any configuration records.
if (!unset_all_config_for_plugin('qtype_' . $delete)) {
@ -170,12 +171,12 @@ if (($delete = optional_param('delete', '', PARAM_SAFEDIR)) && confirm_sesskey()
drop_plugin_tables($delete, $qtypes[$delete]->plugin_dir() . '/db/install.xml', false);
// Remove event handlers and dequeue pending events
events_uninstall('qtype/' . $delete);
events_uninstall('qtype_' . $delete);
$a->qtype = $qtypename;
$a->directory = $qtypes[$delete]->plugin_dir();
echo $OUTPUT->box(get_string('qtypedeletefiles', 'admin', $a), 'generalbox', 'notice');
echo $OUTPUT->continue_button(new moodle_url('/admin/qtypes.php'));
echo $OUTPUT->box(get_string('qtypedeletefiles', 'question', $a), 'generalbox', 'notice');
echo $OUTPUT->continue_button($thispageurl);
echo $OUTPUT->footer();
exit;
}
@ -188,10 +189,10 @@ echo $OUTPUT->heading(get_string('manageqtypes', 'admin'));
// Set up the table.
$table = new flexible_table('qtypeadmintable');
$table->define_baseurl(new moodle_url('/admin/qtypes.php'));
$table->define_baseurl($thispageurl);
$table->define_columns(array('questiontype', 'numquestions', 'version', 'requires',
'availableto', 'delete', 'settings'));
$table->define_headers(array(get_string('questiontype', 'admin'), get_string('numquestions', 'admin'),
$table->define_headers(array(get_string('questiontype', 'question'), get_string('numquestions', 'question'),
get_string('version'), get_string('requires', 'admin'), get_string('availableq', 'question'),
get_string('delete'), get_string('settings')));
$table->set_attribute('id', 'qtypes');
@ -213,13 +214,13 @@ foreach ($sortedqtypes as $qtypename => $localname) {
// Number of questions of this type.
if ($counts[$qtypename]->numquestions + $counts[$qtypename]->numhidden > 0) {
if ($counts[$qtypename]->numhidden > 0) {
$strcount = get_string('numquestionsandhidden', 'admin', $counts[$qtypename]);
$strcount = get_string('numquestionsandhidden', 'question', $counts[$qtypename]);
} else {
$strcount = $counts[$qtypename]->numquestions;
}
if ($canviewreports) {
$row[] = '<a href="' . new moodle_url('/admin/report/questioninstances/index.php', array('qtype' => $qtypename)) .
'" title="' . get_string('showdetails', 'admin') . '">' . $strcount . '</a>';
$row[] = html_writer::link(new moodle_url('/admin/report/questioninstances/index.php',
array('qtype' => $qtypename)), $strcount, array('title' => get_string('showdetails', 'admin')));
} else {
$strcount;
}
@ -232,7 +233,7 @@ foreach ($sortedqtypes as $qtypename => $localname) {
if ($version) {
$row[] = $version;
} else {
$row[] = '<span class="disabled">' . get_string('nodatabase', 'admin') . '</span>';
$row[] = html_writer::tag('span', get_string('nodatabase', 'admin'), array('class' => 'disabled'));
}
// Other question types required by this one.
@ -251,36 +252,35 @@ foreach ($sortedqtypes as $qtypename => $localname) {
$rowclass = '';
if ($qtype->menu_name()) {
$createable = isset($createabletypes[$qtypename]);
$icons = enable_disable_button($qtypename, $createable);
$icons = question_types_enable_disable_icons($qtypename, $createable);
if (!$createable) {
$rowclass = 'dimmed_text';
}
} else {
$icons = '<img src="' . $OUTPUT->pix_url('spacer') . '" alt="" class="spacer" />';
$icons = $OUTPUT->spacer() . ' ';
}
// Move icons.
$icons .= icon_html('up', $qtypename, 't/up', get_string('up'), '');
$icons .= icon_html('down', $qtypename, 't/down', get_string('down'), '');
$icons .= question_type_icon_html('up', $qtypename, 't/up', get_string('up'), '');
$icons .= question_type_icon_html('down', $qtypename, 't/down', get_string('down'), '');
$row[] = $icons;
// Delete link, if available.
if ($needed[$qtypename]) {
$row[] = '';
} else {
$row[] = '<a href="' . new moodle_url('/admin/qtypes.php', array('delete' => $qtypename,
'sesskey' => sesskey())) . '" title="' .
get_string('uninstallqtype', 'admin') . '">' . get_string('delete') . '</a>';
$row[] = html_writer::link(new moodle_url($thispageurl,
array('delete' => $qtypename, 'sesskey' => sesskey())), get_string('delete'),
array('title' => get_string('uninstallqtype', 'question')));
}
// Settings link, if available.
$settings = admin_get_root()->locate('qtypesetting' . $qtypename);
if ($settings instanceof admin_externalpage) {
$row[] = '<a href="' . $settings->url .
'">' . get_string('settings') . '</a>';
$row[] = html_writer::link($settings->url, get_string('settings'));
} else if ($settings instanceof admin_settingpage) {
$row[] = '<a href="' . new moodle_url('/admin/settings.php', array('section' => 'qtypesetting' . $qtypename)) .
'">' . get_string('settings') . '</a>';
$row[] = html_writer::link(new moodle_url('/admin/settings.php',
array('section' => 'qtypesetting' . $qtypename)), get_string('settings'));
} else {
$row[] = '';
}
@ -292,24 +292,21 @@ $table->finish_output();
echo $OUTPUT->footer();
function enable_disable_button($qtypename, $createable) {
function question_types_enable_disable_icons($qtypename, $createable) {
if ($createable) {
return icon_html('disable', $qtypename, 'i/hide', get_string('enabled', 'question'), get_string('disable'));
return question_type_icon_html('disable', $qtypename, 'i/hide',
get_string('enabled', 'question'), get_string('disable'));
} else {
return icon_html('enable', $qtypename, 'i/show', get_string('disabled', 'question'), get_string('enable'));
return question_type_icon_html('enable', $qtypename, 'i/show',
get_string('disabled', 'question'), get_string('enable'));
}
}
function icon_html($action, $qtypename, $icon, $alt, $tip) {
function question_type_icon_html($action, $qtypename, $icon, $alt, $tip) {
global $OUTPUT;
if ($tip) {
$tip = 'title="' . $tip . '" ';
}
$html = ' <form action="' . new moodle_url('/admin/qtypes.php') . '" method="post"><div>';
$html .= '<input type="hidden" name="sesskey" value="' . sesskey() . '" />';
$html .= '<input type="image" name="' . $action . '" value="' . $qtypename .
'" src="' . $OUTPUT->pix_url($icon) . '" alt="' . $alt . '" ' . $tip . '/>';
$html .= '</div></form>';
return $html;
return $OUTPUT->action_icon(new moodle_url('/admin/qtypes.php',
array($action => $qtypename, 'sesskey' => sesskey())),
new pix_icon($icon, $alt, 'moodle', array('title' => '')),
null, array('title' => $tip)) . ' ';
}

View File

@ -394,6 +394,10 @@ if ($hassiteconfig) {
// Question type settings
if ($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) {
// Question behaviour settings.
$ADMIN->add('modules', new admin_category('qbehavioursettings', get_string('questionbehaviours', 'admin')));
$ADMIN->add('qbehavioursettings', new admin_page_manageqbehaviours());
// Question type settings.
$ADMIN->add('modules', new admin_category('qtypesettings', get_string('questiontypes', 'admin')));
$ADMIN->add('qtypesettings', new admin_page_manageqtypes());

View File

@ -92,10 +92,7 @@ $string['cachetype'] = 'Cache type';
$string['calendarexportsalt'] = 'Calendar export salt';
$string['calendarsettings'] = 'Calendar';
$string['calendar_weekend'] = 'Weekend days';
$string['cannotdeletemissingqtype'] = 'You cannot delete the missing question type. It is needed by the system.';
$string['cannotdeletemodfilter'] = 'You cannot uninstall the \'{$a->filter}\' because it is part of the \'{$a->module}\' module.';
$string['cannotdeleteqtypeinuse'] = 'You cannot delete the question type \'{$a}\'. There are questions of this type in the question bank.';
$string['cannotdeleteqtypeneeded'] = 'You cannot delete the question type \'{$a}\'. There are other question types installed that rely on it.';
$string['cfgwwwrootslashwarning'] = 'You have defined $CFG->wwwroot incorrectly in your config.php file. You have included a \'/\' character at the end. Please remove it, or you will experience strange bugs like <a href=\'http://tracker.moodle.org/browse/MDL-11061\'>MDL-11061</a>.';
$string['cfgwwwrootwarning'] = 'You have defined $CFG->wwwroot incorrectly in your config.php file. It does not match the URL you are using to access this page. Please correct it, or you will experience strange bugs like <a href=\'http://tracker.moodle.org/browse/MDL-11061\'>MDL-11061</a>.';
$string['clamfailureonupload'] = 'On clam AV failure';
@ -429,12 +426,9 @@ $string['deletefilterareyousure'] = 'Are you sure you want to delete the filter
$string['deletefilterareyousuremessage'] = 'You are about to completely delete the filter \'{$a}\'. Are you sure you want to uninstall it?';
$string['deletefilterfiles'] = 'All data associated with the filter \'{$a->filter}\' has been deleted from the database. To complete the deletion (and to prevent the filter from re-installing itself), you should now delete this directory from your server: {$a->directory}';
$string['deleteincompleteusers'] = 'Delete incomplete users after';
$string['deleteqtypeareyousure'] = 'Are you sure you want to delete the question type \'{$a}\'';
$string['deleteqtypeareyousuremessage'] = 'You are about to completely delete the question type \'{$a}\'. Are you sure you want to uninstall it?';
$string['deleteunconfirmed'] = 'Delete not fully setup users after';
$string['deleteuser'] = 'Delete user';
$string['deletingfilter'] = 'Deleting filter \'{$a}\'';
$string['deletingqtype'] = 'Deleting question type \'{$a}\'';
$string['density'] = 'Density';
$string['denyemailaddresses'] = 'Denied email domains';
$string['development'] = 'Development';
@ -668,6 +662,7 @@ $string['maintfileopenerror'] = 'Error opening maintenance files!';
$string['maintinprogress'] = 'Maintenance is in progress...';
$string['managelang'] = 'Manage';
$string['managelicenses'] = 'Manage licences';
$string['manageqbehaviours'] = 'Manage question behaviours';
$string['manageqtypes'] = 'Manage question types';
$string['maturity50'] = 'Alpha';
$string['maturity100'] = 'Beta';
@ -752,8 +747,6 @@ $string['notifyloginthreshold'] = 'Threshold for email notifications';
$string['notloggedinroleid'] = 'Role for visitors';
$string['numberofmissingstrings'] = 'Number of missing strings: {$a}';
$string['numberofstrings'] = 'Total number of strings: {$a->strings}<br />Missing: {$a->missing} ({$a->missingpercent}&nbsp;%)';
$string['numquestions'] = 'No. questions';
$string['numquestionsandhidden'] = '{$a->numquestions} (+{$a->numhidden} hidden)';
$string['numcoursesincombo'] = 'Maximum number of courses in combo list';
$string['numcoursesincombo_help'] = 'The combo list doesn\'t work well with large numbers of courses. When the total number of courses in the site is higher than this setting then a link to the dedicated course listing will be shown instead of trying to display all the courses on the front page.';
$string['opensslrecommended'] = 'Installing the optional OpenSSL library is highly recommended -- it enables Moodle Networking functionality.';
@ -866,11 +859,11 @@ $string['proxypassword'] = 'Proxy password';
$string['proxyport'] = 'Proxy port';
$string['proxytype'] = 'Proxy type';
$string['proxyuser'] = 'Proxy username';
$string['qtypedeletefiles'] = 'All data associated with the question type \'{$a->qtype}\' has been deleted from the database. To complete the deletion (and to prevent the question type from re-installing itself), you should now delete this directory from your server: {$a->directory}';
$string['qtyperqpwillberemoved'] = 'During the upgrade, the RQP question type will be removed. You were not using this question type, so you should not experience any problems.';
$string['qtyperqpwillberemovedanyway'] = 'During the upgrade, the RQP question type will be removed. You have some RQP questions in your database, and these will stop working unless you reinstall the code from http://moodle.org/mod/data/view.php?d=13&amp;rid=797 before continuing with the upgrade.';
$string['quarantinedir'] = 'Quarantine directory';
$string['question'] = 'Question';
$string['questionbehaviours'] = 'Question behaviours';
$string['questioncwqpfscheck'] = 'One or more \'random\' questions in a quiz are set up to select questions from a mixture of shared and unshared question categories. There is a more detailed report <a href="{$a->reporturl}">here</a> and see Moodle Docs page <a href="{$a->docsurl}">here</a>.';
$string['questioncwqpfsok'] = 'Good. There are no \'random\' questions in your quizzes that are set up to select questions from a mixture of shared and unshared question categories.';
$string['questiontype'] = 'Question type';
@ -995,7 +988,6 @@ $string['unicodeupgradenotice'] = 'In Moodle 1.6 we have migrated all languages
$string['uninstall'] = 'Uninstall selected language pack';
$string['uninstallconfirm'] = 'You are about to completely uninstall language pack {$a}, are you sure?';
$string['uninstallplugin'] = 'Uninstall';
$string['uninstallqtype'] = 'Uninstall this question type.';
$string['unsupported'] = 'Unsupported';
$string['updateaccounts'] = 'Update existing accounts';
$string['updatecomponent'] = 'Update component';

View File

@ -27,13 +27,21 @@ $string['addcategory'] = 'Add category';
$string['adminreport'] = 'Report on possible problems in your question database.';
$string['availableq'] = 'Available?';
$string['badbase'] = 'Bad base before **: {$a}**';
$string['behaviour'] = 'Behaviour';
$string['broken'] = 'This is a "broken link", it points to a nonexistent file.';
$string['byandon'] = 'by <em>{$a->user}</em> on <em>{$a->time}</em>';
$string['cannotcopybackup'] = 'Could not copy backup file';
$string['cannotcreate'] = 'Could not create new entry in question_attempts table';
$string['cannotcreatepath'] = 'Cannot create path: {$a}';
$string['cannotdeletebehaviourinuse'] = 'You cannot delete the behaviour \'{$a}\'. It is used by question attempts.';
$string['cannotdeletecate'] = 'You can\'t delete that category it is the default category for this context.';
$string['cannotdeletemissingbehaviour'] = 'You cannot uninstall the missing behaviour. It is required by the system.';
$string['cannotdeletemissingqtype'] = 'You cannot uninstall the missing question type. It is needed by the system.';
$string['cannotdeleteneededbehaviour'] = 'Cannot delete the question behaviour \'{$a}\'. There are other behaviours installed that rely on it.';
$string['cannotdeleteqtypeinuse'] = 'You cannot delete the question type \'{$a}\'. There are questions of this type in the question bank.';
$string['cannotdeleteqtypeneeded'] = 'You cannot delete the question type \'{$a}\'. There are other question types installed that rely on it.';
$string['cannotenable'] = 'Question type {$a} cannot be created directly.';
$string['cannotenablebehaviour'] = 'Question behaviour {$a} cannot be used directly. It is for internal use only.';
$string['cannotfindcate'] = 'Could not find category record';
$string['cannotfindquestionfile'] = 'Could not find question data file in zip';
$string['cannotgetdsfordependent'] = 'Cannot get the specified dataset for a dataset dependent question! (question: {$a->id}, datasetitem: {$a->item})';
@ -78,9 +86,15 @@ affected will continue to work in all existing quizzes until you remove them fro
$string['cwrqpfsnoprob'] = 'No question categories in your site are affected by the \'Random questions selecting questions from sub categories\' issue.';
$string['defaultfor'] = 'Default for {$a}';
$string['defaultinfofor'] = 'The default category for questions shared in context \'{$a}\'.';
$string['deletebehaviourareyousure'] = 'Delete behaviour {$a}: are you sure?';
$string['deletebehaviourareyousuremessage'] = 'You are about to completely delete the question behaviour {$a}. This will completely delete everything in the database associated with this question behaviour. Are you SURE you want to continue?';
$string['deletecoursecategorywithquestions'] = 'There are questions in the question bank associated with this course category. If you proceed, they will be deleted. You may wish to move them first, using the question bank interface.';
$string['deleteqtypeareyousure'] = 'Delete question type {$a}: are you sure?';
$string['deleteqtypeareyousuremessage'] = 'You are about to completely delete the question type {$a}. This will completely delete everything in the database associated with this question type. Are you SURE you want to continue?';
$string['deletequestioncheck'] = 'Are you absolutely sure you want to delete \'{$a}\'?';
$string['deletequestionscheck'] = 'Are you absolutely sure you want to delete the following questions?<br /><br />{$a}';
$string['deletingbehaviour'] = 'Deleting question behaviour \'{$a}\'';
$string['deletingqtype'] = 'Deleting question type \'{$a}\'';
$string['disabled'] = 'Disabled';
$string['disterror'] = 'The distribution {$a} caused problems';
$string['donothing'] = 'Don\'t copy or move files or change links.';
@ -205,6 +219,9 @@ $string['notenoughdatatoeditaquestion'] = 'Neither a question id, nor a category
$string['notenoughdatatomovequestions'] = 'You need to provide the question ids of questions you want to move.';
$string['notflagged'] = 'Not flagged';
$string['novirtualquestiontype'] = 'No virtual question type for question type {$a}';
$string['numqas'] = 'No. question attempts';
$string['numquestions'] = 'No. questions';
$string['numquestionsandhidden'] = '{$a->numquestions} (+{$a->numhidden} hidden)';
$string['page-question-x'] = 'Any question page';
$string['page-question-edit'] = 'Question editing page';
$string['page-question-category'] = 'Question category page';
@ -225,6 +242,8 @@ $string['permissionmove'] = 'Move this question';
$string['permissionsaveasnew'] = 'Save this as a new question';
$string['permissionto'] = 'You have permission to :';
$string['published'] = 'shared';
$string['qbehaviourdeletefiles'] = 'All data associated with the question behaviour \'{$a->behaviour}\' has been deleted from the database. To complete the deletion (and to prevent the behaviour from re-installing itself), you should now delete this directory from your server: {$a->directory}';
$string['qtypedeletefiles'] = 'All data associated with the question type \'{$a->qtype}\' has been deleted from the database. To complete the deletion (and to prevent the question type from re-installing itself), you should now delete this directory from your server: {$a->directory}';
$string['qtypeveryshort'] = 'T';
$string['questionaffected'] = '<a href="{$a->qurl}">Question "{$a->name}" ({$a->qtype})</a> is in this question category but is also being used in <a href="{$a->qurl}">quiz "{$a->quizname}"</a> in another course "{$a->coursename}".';
$string['questionbank'] = 'Question bank';
@ -251,6 +270,8 @@ $string['stoponerror'] = 'Stop on error';
$string['stoponerror_help'] = 'This setting determines whether the import process stops when an error is detected, resulting in no questions being imported, or whether any questions containing errors are ignored and any valid questions are imported.';
$string['tofilecategory'] = 'Write category to file';
$string['tofilecontext'] = 'Write context to file';
$string['uninstallbehaviour'] = 'Uninstall this question behaviour.';
$string['uninstallqtype'] = 'Uninstall this question type.';
$string['unknown'] = 'Unknown';
$string['unknownquestiontype'] = 'Unknown question type: {$a}.';
$string['unknowntolerance'] = 'Unknown tolerance type {$a}';
@ -368,8 +389,10 @@ $string['submissionoutofsequencefriendlymessage'] = "You have entered data outsi
$string['submit'] = 'Submit';
$string['submitandfinish'] = 'Submit and finish';
$string['submitted'] = 'Submit: {$a}';
$string['unknownbehaviour'] = 'Unknown behaviour: {$a}.';
$string['unknownquestion'] = 'Unknown question: {$a}.';
$string['unknownquestioncatregory'] = 'Unknown question category: {$a}.';
$string['unknownquestiontype'] = 'Unknown question type: {$a}.';
$string['whethercorrect'] = 'Whether correct';
$string['withselected'] = 'With selected';
$string['xoutofmax'] = '{$a->mark} out of {$a->max}';

View File

@ -5017,6 +5017,57 @@ class admin_page_defaultmessageoutputs extends admin_page_managemessageoutputs {
}
}
/**
* Manage question behaviours page
*
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class admin_page_manageqbehaviours extends admin_externalpage {
/**
* Constructor
*/
public function __construct() {
global $CFG;
parent::__construct('manageqbehaviours', get_string('manageqbehaviours', 'admin'),
new moodle_url('/admin/qbehaviours.php'));
}
/**
* Search question behaviours for the specified string
*
* @param string $query The string to search for in question behaviours
* @return array
*/
public function search($query) {
global $CFG;
if ($result = parent::search($query)) {
return $result;
}
$found = false;
$textlib = textlib_get_instance();
require_once($CFG->dirroot . '/question/engine/lib.php');
foreach (get_plugin_list('qbehaviour') as $behaviour => $notused) {
if (strpos($textlib->strtolower(question_engine::get_behaviour_name($behaviour)),
$query) !== false) {
$found = true;
break;
}
}
if ($found) {
$result = new stdClass();
$result->page = $this;
$result->settings = array();
return array($this->name => $result);
} else {
return array();
}
}
}
/**
* Question type manage page
*

View File

@ -40,6 +40,10 @@ require_once(dirname(__FILE__) . '/../adaptive/behaviour.php');
class qbehaviour_adaptivenopenalty extends qbehaviour_adaptive {
const IS_ARCHETYPAL = true;
public static function get_required_behaviours() {
return array('adaptive');
}
protected function adjusted_fraction($fraction, $prevtries) {
return $fraction;
}

View File

@ -77,6 +77,10 @@ abstract class question_behaviour {
}
}
public static function get_required_behaviours() {
return array();
}
/**
* Most behaviours can only work with {@link question_definition}s
* of a particular subtype, or that implement a particular interface.

View File

@ -45,6 +45,10 @@ require_once(dirname(__FILE__) . '/../deferredfeedback/behaviour.php');
class qbehaviour_deferredcbm extends qbehaviour_deferredfeedback {
const IS_ARCHETYPAL = true;
public static function get_required_behaviours() {
return array('deferredfeedback');
}
public static function get_unused_display_options() {
return array('correctness', 'marks', 'specificfeedback', 'generalfeedback',
'rightanswer');

View File

@ -45,6 +45,10 @@ require_once(dirname(__FILE__) . '/../immediatefeedback/behaviour.php');
class qbehaviour_immediatecbm extends qbehaviour_immediatefeedback {
const IS_ARCHETYPAL = true;
public static function get_required_behaviours() {
return array('immediatefeedback', 'deferredcbm');
}
public function get_min_fraction() {
return question_cbm::adjust_fraction(parent::get_min_fraction(), question_cbm::HIGH);
}

View File

@ -64,6 +64,10 @@ require_once(dirname(__FILE__) . '/../interactive/behaviour.php');
class qbehaviour_interactivecountback extends qbehaviour_interactive {
const IS_ARCHETYPAL = false;
public static function get_required_behaviours() {
return array('interactive');
}
public function required_question_definition_type() {
return 'question_automatically_gradable_with_countback';
}

View File

@ -109,11 +109,11 @@ abstract class question_bank {
* Load the question configuration data from config_plugins.
* @return object get_config('question') with caching.
*/
protected static function get_config() {
public static function get_config() {
if (is_null(self::$questionconfig)) {
$questionconfig = get_config('question');
self::$questionconfig = get_config('question');
}
return $questionconfig;
return self::$questionconfig;
}
/**

View File

@ -99,7 +99,6 @@ abstract class question_engine {
* @param int $qubaid the id of the usage to delete.
*/
public static function delete_questions_usage_by_activity($qubaid) {
global $CFG;
self::delete_questions_usage_by_activities(new qubaid_list(array($qubaid)));
}
@ -224,12 +223,9 @@ abstract class question_engine {
*/
public static function get_archetypal_behaviours() {
$archetypes = array();
$behaviours = get_list_of_plugins('question/behaviour');
foreach ($behaviours as $path) {
$behaviour = basename($path);
self::load_behaviour_class($behaviour);
$plugin = 'qbehaviour_' . $behaviour;
if (constant($plugin . '::IS_ARCHETYPAL')) {
$behaviours = get_plugin_list('qbehaviour');
foreach ($behaviours as $behaviour => $notused) {
if (self::is_behaviour_archetypal($behaviour)) {
$archetypes[$behaviour] = self::get_behaviour_name($behaviour);
}
}
@ -238,75 +234,89 @@ abstract class question_engine {
}
/**
* Return an array where the keys are the internal names of the behaviours
* in preferred order and the values are a human-readable name.
*
* @param array $archetypes, array of behaviours
* @param string $questionbehavioursorder, a comma separated list of behaviour names
* @param string $questionbehavioursdisabled, a comma separated list of behaviour names
* @param string $currentbahaviour, current behaviour name
* @return array model name => lang string for this behaviour name.
* @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
* @return bool whether this is an archetypal behaviour.
*/
public static function sort_behaviours($archetypes, $questionbehavioursorder,
$questionbehavioursdisabled, $currentbahaviour) {
$behaviourorder = array();
$behaviourdisabled = array();
// Get disabled behaviours
if ($questionbehavioursdisabled) {
$behaviourdisabledtemp = preg_split('/[\s,;]+/', $questionbehavioursdisabled);
} else {
$behaviourdisabledtemp = array();
}
if ($questionbehavioursorder) {
$behaviourordertemp = preg_split('/[\s,;]+/', $questionbehavioursorder);
} else {
$behaviourordertemp = array();
}
foreach ($behaviourdisabledtemp as $key) {
if (array_key_exists($key, $archetypes)) {
// Do not disable the current behaviour
if ($key != $currentbahaviour) {
$behaviourdisabled[$key] = $archetypes[$key];
}
}
}
// Get behaviours in preferred order
foreach ($behaviourordertemp as $key) {
if (array_key_exists($key, $archetypes)) {
$behaviourorder[$key] = $archetypes[$key];
}
}
// Get the rest of behaviours and sort them alphabetically
$leftover = array_diff_key($archetypes, $behaviourdisabled, $behaviourorder);
asort($leftover, SORT_LOCALE_STRING);
// Set up the final order to be displayed
$finalorder = $behaviourorder + $leftover;
return $finalorder;
public static function is_behaviour_archetypal($behaviour) {
self::load_behaviour_class($behaviour);
$plugin = 'qbehaviour_' . $behaviour;
return constant($plugin . '::IS_ARCHETYPAL');
}
/**
* Return an array where the keys are the internal names of the behaviours
* in preferred order and the values are a human-readable name.
*
* @param string $currentbahaviour
* @param array $archetypes, array of behaviours
* @param string $orderlist, a comma separated list of behaviour names
* @param string $disabledlist, a comma separated list of behaviour names
* @param string $current, current behaviour name
* @return array model name => lang string for this behaviour name.
*/
public static function get_behaviour_options($currentbahaviour) {
global $CFG;
public static function sort_behaviours($archetypes, $orderlist, $disabledlist, $current=null) {
// Get disabled behaviours
if ($disabledlist) {
$disabled = explode(',', $disabledlist);
} else {
$disabled = array();
}
if ($orderlist) {
$order = explode(',', $orderlist);
} else {
$order = array();
}
foreach ($disabled as $behaviour) {
if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) {
unset($archetypes[$behaviour]);
}
}
// Get behaviours in preferred order
$behaviourorder = array();
foreach ($order as $behaviour) {
if (array_key_exists($behaviour, $archetypes)) {
$behaviourorder[$behaviour] = $archetypes[$behaviour];
}
}
// Get the rest of behaviours and sort them alphabetically
$leftover = array_diff_key($archetypes, $behaviourorder);
asort($leftover, SORT_LOCALE_STRING);
// Set up the final order to be displayed
return $behaviourorder + $leftover;
}
/**
* Return an array where the keys are the internal names of the behaviours
* in preferred order and the values are a human-readable name.
*
* @param string $currentbehaviour
* @return array model name => lang string for this behaviour name.
*/
public static function get_behaviour_options($currentbehaviour) {
$config = question_bank::get_config();
$archetypes = self::get_archetypal_behaviours();
// If no admin setting return all behavious
if (empty($CFG->questionbehavioursdisabled) && empty($CFG->questionbehavioursorder)) {
if (empty($config->disabledbehaviours) && empty($config->behavioursortorder)) {
return $archetypes;
}
return self::sort_behaviours($archetypes, $CFG->questionbehavioursorder,
$CFG->questionbehavioursdisabled, $currentbahaviour);
if (empty($config->behavioursortorder)) {
$order = '';
} else {
$order = $config->behavioursortorder;
}
if (empty($config->disabledbehaviours)) {
$disabled = '';
} else {
$disabled = $config->disabledbehaviours;
}
return self::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour);
}
/**
@ -318,6 +328,16 @@ abstract class question_engine {
return get_string('pluginname', 'qbehaviour_' . $behaviour);
}
/**
* Get the translated name of an behaviour, for display in the UI.
* @param string $behaviour the internal name of the model.
* @return string name from the current language pack.
*/
public static function get_behaviour_required_behaviours($behaviour) {
$class = 'qbehaviour_' . $behaviour;
return $class::get_required_behaviours();
}
/**
* @return array all the file area names that may contain response files.
*/

View File

@ -81,16 +81,16 @@ class question_engine_test extends UnitTestCase {
$this->assertIdentical($out, question_engine::sort_behaviours($in, '', 'b1,b2,b3,b4', 'b4'));
$out = array('b6' => 'Behave 6', 'b1' => 'Behave 1', 'b4' => 'Behave 4');
$this->assertIdentical($out, question_engine::sort_behaviours($in, 'b6, b1, b4', 'b1, b2, b3, b4, b5', 'b4'));
$this->assertIdentical($out, question_engine::sort_behaviours($in, 'b6,b1,b4', 'b2,b3,b4,b5', 'b4'));
$out = array('b6' => 'Behave 6', 'b5' => 'Behave 5', 'b4' => 'Behave 4');
$this->assertIdentical($out, question_engine::sort_behaviours($in, 'b6, b5, b4', 'b1, b2, b3', 'b4'));
$this->assertIdentical($out, question_engine::sort_behaviours($in, 'b6,b5,b4', 'b1,b2,b3', 'b4'));
$out = array('b1' => 'Behave 1', 'b6' => 'Behave 6', 'b5' => 'Behave 5', 'b4' => 'Behave 4');
$this->assertIdentical($out, question_engine::sort_behaviours($in, 'b1, b6, b5', 'b1, b2, b3, b4, b5', 'b4'));
$out = array('b6' => 'Behave 6', 'b5' => 'Behave 5', 'b4' => 'Behave 4');
$this->assertIdentical($out, question_engine::sort_behaviours($in, 'b1,b6,b5', 'b1,b2,b3,b4', 'b4'));
$out = array('b2' => 'Behave 2', 'b4' => 'Behave 4', 'b6' => 'Behave 6');
$this->assertIdentical($out, question_engine::sort_behaviours($in, 'b2, b4, b6', 'b1, b3, b5', 'b2'));
$this->assertIdentical($out, question_engine::sort_behaviours($in, 'b2,b4,b6', 'b1,b3,b5', 'b2'));
// Ignore unknown input in the order argument.
$this->assertIdentical($in, question_engine::sort_behaviours($in, 'unknown', '', ''));

View File

@ -47,10 +47,20 @@
#page-admin-report-capability-index .rolecaps th {text-align: left;}
#page-admin-report-capability-index #settingsform #capabilitysearch {width: 30em;}
#page-admin-qbehaviours .disabled {color: gray;}
#page-admin-qbehaviours th {white-space: normal;}
#page-admin-qbehaviours .cell.c1,
#page-admin-qbehaviours .cell.c2 {text-align: right;}
#page-admin-qbehaviours .cell.c3 {font-size: 0.7em;}
#page-admin-qbehaviours #qbehaviours div,
#page-admin-qbehaviours #qbehaviours form {display: inline;}
#page-admin-qbehaviours #qbehaviours img.spacer {width: 16px;}
#page-admin-qtypes .disabled {color: gray;}
#page-admin-qtypes th {white-space: normal;}
#page-admin-qtypes .cell.c1,
#page-admin-qtypes .cell.c2 {text-align: right;}
#page-admin-qtypes .cell.c3 {font-size: 0.7em;}
#page-admin-qtypes #qtypes div,
#page-admin-qtypes #qtypes form {display: inline;}
#page-admin-qtypes #qtypes img.spacer {width: 16px;}