DokuWiki

It's better when it's simple

User Tools

Site Tools


plugin:btable

BTable Plugin

Compatible with DokuWiki

No compatibility info given!

plugin Helps to note the attendance of people for meetings or courses.

Last updated on
2007-12-08
Provides
Syntax

Similar to doodle

Tagged with calculation, calendar, csv, form, poll, tables, todo

:!: File URL is dead :!:

This plugin is based on the doodle plugin and extends it with support for CSV export and the ability to predefine not only columns, but also rows.

Description

This plugin can help your team to schedule meetings or making other decisions in a team. The syntax looks like this:

<btable [id]>
  <columns>
    ^ [column] ^ [column] ^ ... ^
  </columns>
  <rows>
    ^ [row] ^ [row] ^ ... ^
  </rows>
</btable>

That means, you can simply put a <btable> tag on the page and gets a fully selectable boolean table with export functionality.

[id] the ID of the btable; must be unique1); appears as title required
[column] an option for which users can tick a checkbox whether it's 'true' or not ('false') required
[row] an option which users can select from a combobox to insert boolean values for it required

You can see this plugin in action here.

Download & Installation

You can download and install the btable.zip (16 KB) with the plugin manager.

Source Code

As the source code is included in the file above, please download this to view the source code.

Code Listing

syntax.php

<?php
/**
 * Boolean Table Plugin (Extended/Modified Doodle Plugin)
 *
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author     Oliver Horst <oliver.horst@uni-dortmund.de>  
 */
 
if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
require_once(DOKU_PLUGIN.'syntax.php');
 
/**
 * All DokuWiki plugins to extend the parser/rendering mechanism
 * need to inherit from this class
 */
class syntax_plugin_btable extends DokuWiki_Syntax_Plugin {
 
    /**
     * return some info
     */
    function getInfo(){
        return array(
            'author' => 'Oliver Horst',
            'email'  => 'oliver.horst@uni-dortmund.de',
            'date'   => '2007-12-06',
            'name'   => 'Boolean Table (modified Doodle Plugin)',
            'desc'   => 'Helps to save/input for example attendance information',
            'url'    => 'http://wiki.splitbrain.org/plugin:btable',
        );
    }
 
    function getType(){ return 'substition';}
    function getPType(){ return 'block';}
    function getSort(){ return 168; }
 
    /**
     * Connect pattern to lexer
     */
    function connectTo($mode){
        $this->Lexer->addSpecialPattern('<btable.*?>.+?</btable>', $mode, 'plugin_btable');
    }
 
 
    /**
     * Handle the match
     */
    function handle($match, $state, $pos, &$handler){
 
        // strip markup
        $match = substr($match, 8, -9);
 
        // split into title and options
        list($title, $options) = preg_split('/>/u', $match, 2);
 
        // check if no title was specified
        if (!$options){
            $options = $title;
            $title   = NULL;
        }
 
        // split into ids and dates part
        list($first, $second) = preg_split('#(\s|\n|\r)*<\/columns>(\s|\n|\r)*<rows>(\s|\n|\r)*#u', $options);
 
        // get ids and dates
        list(, $ids) = preg_split('#(\S|\s|\n|\r)*<columns>(\s|\n|\r)*#u', $first);
        list($dates) = preg_split('#(\s|\n|\r)*<\/rows>(\s|\n|\r)*#u', $second);
 
        $ids = explode('^', $ids);
        $dates = explode('^', $dates);
 
        // remove whitespaces
        for($i = 0; $i < count($ids); $i++) {
            $ids[$i] = trim($ids[$i]);
        }
 
        for($i = 0; $i < count($dates); $i++) {
            $dates[$i] = trim($dates[$i]);
        }
 
 
        return array(trim($title), $ids, $dates);
    }
 
 
    /**
     * Create output
     */
    function render($mode, &$renderer, $data) {
 
        if ($mode == 'xhtml') {
 
            global $ID;
            global $INFO;
 
 
            $conf_groups = trim($this->getConf('btable_groups'));
 
            $user_groups = $INFO['userinfo']['grps'];
            $plugin_groups = split(';', $conf_groups);
 
            if ((strlen($conf_groups) > 0) && (count($plugin_groups) > 0)) {
                if (isset($user_groups) && is_array($user_groups)) {
                    $write_access = count(array_intersect($plugin_groups, $user_groups));
                } else {
                    $write_access = 0;
                }
            } else {
                $write_access = 1;
            }
 
 
            $title = $renderer->_xmlEntities($data[0]);
            $dID = cleanID($title);
 
            $rows = $data[2];
            $columns = $data[1];
 
            $rows_count = count($rows);
            $columns_count = count($columns);
 
 
            // prevent caching to ensure the poll results are fresh
            $renderer->info['cache'] = false;
 
            // get doodle file contents
            $dfile = metaFN(md5($dID), '.btable');
            $doodle = unserialize(@file_get_contents($dfile));
 
            if ($columns_count == 0) {
                // no rows given: reset the doodle
                $doodle = NULL;
            }
 
            // render form
            $renderer->doc  .= '<form id="btable__form__'.$dID.'" '.
                                    'method="post" '.
                                    'action="'.script().'" '.
                                    'accept-charset="'.$this->getLang('encoding').'">';
 
            $renderer->doc .= '    <input type="hidden" name="do" value="show" />';
            $renderer->doc .= '    <input type="hidden" name="id" value="'.$ID.'" />';
 
            if (($submit = $_REQUEST[$dID.'-add']) && $write_access) {
 
                // user has changed/added values -> update results
 
                $row = trim($_REQUEST['row']);
                $change_row = "";
 
                if (!empty($row)){
 
                    for ($i = 0; $i < $columns_count; $i++) {
 
                        $column = $renderer->_xmlEntities($columns[$i]);
 
                        if ($_REQUEST[$dID.'-column'.$i]) {
                            $doodle[$row][$column] = true;
                        } else {
                            $doodle[$row][$column] = false;
                        }
                    }
                }
 
                // write back changes
                $fh = fopen($dfile, 'w');
                fwrite($fh, serialize($doodle));
                fclose($fh);
 
            } else if (($submit = $_REQUEST[$dID.'-delete']) && $write_access) {
 
                // user has just deleted a row -> update results
                $row = trim($submit);
                $change_row = "";
 
                if (!empty($row)){
                    unset($doodle[$row]);
                }
 
                // write back changes
                $fh = fopen($dfile, 'w');
                fwrite($fh, serialize($doodle));
                fclose($fh);
 
            } else if (($submit = $_REQUEST[$dID.'-change']) && $write_access) {
 
                // user want to change a row
                $change_row = trim($submit);
            }
 
            // sort rows
            ksort ($doodle);
 
            // start outputing the data
            $renderer->table_open();
 
            if (count($doodle) >= 1) {
 
                $add_delete_row = 1;
 
                if ($write_access) {
                    $colspan = $columns_count + 2;
                } else {
                    $colspan = $columns_count + 1;
                }
 
            } else {
 
                $add_delete_row = 0;
 
                if ($write_access) {
                    $colspan = $columns_count + 1;
                } else {
                    $colspan = $columns_count;
                }
            }
 
 
            // render title if not null
            if ($title) {
                $renderer->tablerow_open();
                $renderer->tableheader_open($colspan);
                $renderer->doc .= $title;
                $renderer->tableheader_close();
                $renderer->tablerow_close();
            }
 
 
            // render column titles
            $renderer->tablerow_open();
 
            if ($write_access || (count($doodle) >= 1)) {
                $renderer->tableheader_open();
                $renderer->doc .= $this->getLang('btable_header');
                $renderer->tableheader_close();
            }
 
            foreach ($columns as $column) {
                $renderer->tableheader_open();
                $renderer->doc .= $renderer->_xmlEntities($column);
                $renderer->tableheader_close();
            }
 
            if ($write_access && (count($doodle) >= 1)) {
                $renderer->tableheader_open();
                $renderer->doc .= $this->getLang('btable_header_del');
                $renderer->tableheader_close();
            }
 
            $renderer->tablerow_close();
 
 
            // display results
            if (is_array($doodle) && count($doodle) >= 1) {
 
                $i = 0;
                foreach($rows as $row) {
                    if (!isset($doodle[$row])) {
                        $selectable_rows[$i] = $row;
                        $i++;
                    }
                }
 
                $renderer->doc .= $this->_doodleResults($dID, $doodle, $columns, $columns_count, $rows_count, $change_row, $write_access, $colspan);
 
            } else {
 
                $selectable_rows = $rows;
 
                if (!$write_access) {
 
                    $renderer->doc .= '<tr>';
                    $renderer->doc .= '  <td class="centeralign" colspan="'.$colspan.'">';
                    $renderer->doc .= '    '.$this->getLang('btable_no_entries');
                    $renderer->doc .= '  </td>';
                    $renderer->doc .= '</tr>';
                }
            }
 
 
            // display input form and export link
            $renderer->doc .= $this->_doodleForm($dID, $columns, $columns_count, $selectable_rows, $change_row, $write_access, $colspan, $add_delete_row);
 
            $renderer->table_close();
 
 
            // close input form
            $renderer->doc .= '</form>';
 
            return true;
        }
        return false;
    }
 
 
    function _doodleResults($dID, $doodle, $columns, $columns_count, $total_rows, $change_row, $allow_changes, $colspan) {
 
        global $ID;
 
 
        $ret   = '';
        $count = array();
        $rows  = array_keys($doodle);
 
        // render table entrys
        foreach ($rows as $row) {
 
            $ret .= '<tr>';
 
            $ret .= '  <td class="rightalign">';
            if ($allow_changes) {
                $ret .= '<input class="button" '.
                               'type="submit" '.
                               'name="'.$dID.'-change" '.
                               'value="'.$row.'" />';
            } else {
                $ret .= $row;
            }
            $ret .= '  </td>';
 
 
            if (($row != $change_row) || !$allow_changes) {
 
                foreach ($columns as $column) {
 
                    if ($doodle[$row][$column]) {
 
                        $class = 'okay';
                        $title = '<img src="'.DOKU_BASE.'lib/images/success.png" '.
                                      'alt="Okay" '.
                                      'width="16" '.
                                      'height="16" />';
                        $count[$column] += 1;
 
                    } elseif (!isset($doodle[$row][$column])) {
 
                        $class = 'centeralign';
                        $title = '&nbsp;';
 
                    } else {
                        $class = 'notokay';
                        $title = '&nbsp;';
                    }
 
                    $ret .= '<td class="'.$class.'">'.$title.'</td>';
                }
 
            } else {
 
                for ($i = 0; $i < $columns_count; $i++) {
 
                    $column = $columns[$i];
 
                    if ($doodle[$row][$column]) {
 
                        $class = 'centeralign';
                        $value = 'checked="checked"';
                        $count[$column] += 1;
 
                    } else {
                        $class = 'centeralign';
                        $value = '';
                    }
 
                    $ret .= '<td class="'.$class.'">';
                    $ret .= '    <input type="checkbox" '.
                                       'name="'.$dID.'-column'.$i.'" '.
                                       'value="1" '.
                                       $value.' />';
                    $ret .= '</td>';
                }
            }
 
            if ($allow_changes) {
                $ret .= '    <td>';
                $ret .= '        <input type="image" '.
                                       'name="'.$dID.'-delete" '.
                                       'value="'.$row.'" '.
                                       'src="'.DOKU_BASE.'lib/images/del.png" '.
                                       'alt="'.$this->getLang('btable_btn_delete').'" />';
                $ret .= '    </td>';
            }
            $ret .= '</tr>';
        }
 
        if ($this->getConf('btable_show_ratio') == true) {
 
            // render attendance factor
            $ret .= '<tr>';
            $ret .= '  <td>'.$this->getLang('btable_summary').'</td>';
 
            $rows_count = count($rows);
 
            foreach ($columns as $column) {
 
                $ccount = isset($count[$column]) ? $count[$column] : 0;
                $attendence = $count[$column] / $rows_count;
                $attendance_factor = $this->getConf('btable_ratio') / 100;
 
                if ($attendance_factor < 0 || $attendance_factor > 1) {
                    $attendance_factor = 0.7;
                }
 
                if ($attendence >= $attendance_factor) {
                    $class = 'okay';
                } else {
                    $class = 'notokay';
                }
 
                $ret .= '<td class="'.$class.'">';
                $ret .=    $ccount."/".$rows_count;
                $ret .= '</td>';
            }
 
            if ($allow_changes) {
                $ret .= '<td></td>';
            }
 
            $ret .= '</tr>';
        }
 
        return $ret;
    }
 
 
    function _doodleForm($dID, $columns, $columns_count, $rows, $change_row, $allow_changes, $colspan, $add_delete_row) {
 
        global $ID;
        global $INFO;
 
 
        $rows_count = count($rows);
 
        $max_row_length = 0;
        for ($i = 0; $i < $rows_count; $i++) {
            $length = strlen($rows[$i]);
            if ($length > $max_row_length) {
                $max_row_length = $length;
            }
        }
 
        if ($allow_changes) {
            if ($rows_count > 0) {
 
                $count = array();
 
                if (empty($change_row)) {
                    $ret .= '  <tr>';
 
                    // row selection (combobox)
                    $ret .= '    <td class="rightalign">';
                    $ret .= '      <select name="row" size="1" style="width: '.$max_row_length.'em;">';
 
                    for ($i = 0; $i < $rows_count; $i++) {
                        if ($i == 0) {
                            $ret .= '<option selected="selected">'.$rows[$i].'</option>';
                        } else {
                            $ret .= '<option>'.$rows[$i].'</option>';
                        }
                    }
 
                    $ret .= '      </select>';
                    $ret .= '    </td>';
 
 
                    // render column inputs (checkboxes)
                    for ($i = 0; $i < $columns_count; $i++) {
 
                        $ret .= '    <td class="centeralign">';
                        $ret .= '      <input type="checkbox" '.
                                             'name="'.$dID.'-column'.$i.'" '.
                                             'value="1" />';
                        $ret .= '    </td>';
                    }
                    if ($add_delete_row) {
                        $ret .= '    <td></td>';
                    }
                    $ret .= '  </tr>';
                }
            }
 
            if (($rows_count > 0) || (!empty($change_row))) {
 
                // render sumbit button
                $ret .= '  <tr>';
                $ret .= '    <td class="centeralign" colspan="'.$colspan.'">';
 
                if (!empty($change_row)) {
                    $ret .= '    <input type="hidden" name="row" value="'.$change_row.'" />';
                }
 
                $ret .= '      <input class="button" '.
                                     'type="submit" '.
                                     'name="'.$dID.'-add" '.
                                     'value="'.$this->getLang('btable_btn_submit').'" />';
                $ret .= '    </td>';
                $ret .= '  </tr>';
            }
        }
 
        if ($this->getConf('btable_show_export') == true) {
 
            // render export link
            $ret .= '  <tr>';
            $ret .= '    <td class="rightalign" colspan="'.$colspan.'">';
            $ret .= '      <a href="'.DOKU_BASE.'/lib/plugins/btable/export.php?id='.$dID.'">';
            $ret .=          $this->getLang('btable_export');
            $ret .= '      </a>';
            $ret .= '    </td>';
            $ret .= '  </tr>';
        }
 
        return $ret;
    }
}
 
//Setup VIM: ex: et ts=4 enc=utf-8 :

style.css

div.dokuwiki table.inline td.notokay {
  background-color: #fcc;
  text-align: center;
}

export.php.css

<?php
 
    /**
     * Exports the saved information of the plugin to an csv file
     *
     * Parameters:
     * - id        The id of the plugin instance which should be exported
     */
 
 
    // Farmpatch: 
    // $tmp = explode('/',$_SERVER['SCRIPT_FILENAME']); array_pop($tmp); array_pop($tmp); 
    // if(!defined('DOKU_INC')) define('DOKU_INC',dirname(join('/',$tmp)).'/../');
 
    if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../../').'/');
    if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
    if(!defined('DOKU_CONF')) define('DOKU_CONF',DOKU_INC.'conf/');
 
    require_once(DOKU_CONF."dokuwiki.php");
    require_once(DOKU_INC."inc/utf8.php");
 
    // set metadir acording to config (see inc/init.php)
    $conf['metadir'] = $conf['savedir'].'/meta';
 
 
    // read parameters
    $dID = $_GET['id'];
 
 
    // get doodle file contents
    $file = DOKU_INC.metaFN(md5($dID), '.btable');
    $content = unserialize(@file_get_contents($file));
 
    // write out header
	  header("Content-type: text/csv");
	  header("Content-Disposition: attachment; filename=".$dID.".csv");
 
    // write out content
    if (is_array($content)) {
 
        $rows = array_keys($content);
        $columns = array_keys($content[$rows[0]]);
 
        if (count($columns) > 0) {
 
            foreach($columns as $column) {
                echo(",".$column);
            }
 
            echo("\n");
 
            foreach ($rows as $row) {
 
                echo($row);
 
                foreach ($columns as $column) {
 
                    if ($content[$row][$column]) {
                        echo(",1");
                    } else {
                        echo(",0");
                    }
                }
 
                echo("\n");
            }
        }
    }
 
 
 
 
    /**
     * Remove unwanted chars from ID
     *
     * Cleans a given ID to only use allowed characters. Accented characters are
     * converted to unaccented ones
     *
     * @author Andreas Gohr <andi@splitbrain.org>
     * @param  string  $raw_id    The pageid to clean
     * @param  boolean $ascii     Force ASCII
     * @see inc/pageutils.php
     */
    function cleanID($raw_id,$ascii=false){
        global $conf;
        static $sepcharpat = null;
 
        global $cache_cleanid;
        $cache = & $cache_cleanid;
 
        // check if it's already in the memory cache
        if (isset($cache[$raw_id])) {
            return $cache[$raw_id];
        }
 
        $sepchar = $conf['sepchar'];
        if($sepcharpat == null) // build string only once to save clock cycles
            $sepcharpat = '#\\'.$sepchar.'+#';
 
        $id = trim($raw_id);
        $id = utf8_strtolower($id);
 
        //alternative namespace seperator
        $id = strtr($id,';',':');
        if($conf['useslash']) {
            $id = strtr($id,'/',':');
        }else{
            $id = strtr($id,'/',$sepchar);
        }
 
        if($conf['deaccent'] == 2 || $ascii) $id = utf8_romanize($id);
        if($conf['deaccent'] || $ascii) $id = utf8_deaccent($id,-1);
 
        //remove specials
        $id = utf8_stripspecials($id,$sepchar,'\*');
 
        if($ascii) $id = utf8_strip($id);
 
        //clean up
        $id = preg_replace($sepcharpat,$sepchar,$id);
        $id = preg_replace('#:+#',':',$id);
        $id = trim($id,':._-');
        $id = preg_replace('#:[:\._\-]+#',':',$id);
 
        $cache[$raw_id] = $id;
 
        return($id);
    }
 
 
    /**
     * returns the full path to the meta file specified by ID and extension
     *
     * The filename is URL encoded to protect Unicode chars
     *
     * @author Steven Danz <steven-danz@kc.rr.com>
     * @see inc/pageutils.php
     */
    function metaFN($id,$ext){
        global $conf;
 
        $id = cleanID($id);
        $id = str_replace(':','/',$id);
        $fn = $conf['metadir'].'/'.utf8_encodeFN($id).$ext;
 
        return $fn;
    }
?>

conf/default.php

<?php
$conf['btable_groups']      = "";
$conf['btable_show_export'] = 1;
$conf['btable_show_ratio']  = 1;
$conf['btable_ratio']       = 0.7;
?>

conf/metadata.php

<?php
$meta['btable_groups']      = array('string');
$meta['btable_show_export'] = array('onoff');
$meta['btable_show_ratio']  = array('onoff');
$meta['btable_ratio']       = array('numeric');
?>

lang/en/lang.php

<?php
 
$lang['btable_btn_submit'] = 'Submit';
$lang['btable_btn_delete'] = 'delete complete row';
$lang['btable_header']     = '';
$lang['btable_header_del'] = '';
$lang['btable_summary']    = 'ratio';
$lang['btable_export']     = 'Export CSV';
$lang['btable_no_entries'] = 'currently no entries';
//
//Setup VIM: ex: et ts=4 enc=utf-8 :

lang/en/settings.php

<?php
$lang['btable_groups']      = "List of groups which are allowed to changes values (separated by `;`)";
$lang['btable_show_export'] = "Should the 'Export' link be displayed?";
$lang['btable_show_ratio']  = "Should the attendance ration column be shown?";
$lang['btable_ratio']       = "Above which ratio should the attendance field be green? (in percent)";
?>

Languages

Currently only German (de), English (en) and Italian (it) translations are available. Feel free to translate it to another language and send it back to me and I will add it to the release!

Revision History

  • 2007-12-08:
    • added Italian translation - thanks a lot to Hirzel Settantacinque
    • added plugin configuration section
      • allow only some groups of users to change/add columns
      • choose the percentage above which a 'ratio' column entry gets green
      • choose to display the 'ratio' column and/or the export link
  • 2007-10-15:
    • first version

Discussion

Question: \\

How can I close the poll? (remove the check Buttons)? (2010-12-20)

Feedback

  • Great Plugin! Thank you! Fred - 20071130
  • Agreed! Is it possible to modify to include non-predefined rows?
    And could there be an option to limit the user to one check box per row? Thanks Bob - 20071209

  • Bug with dokuwiki 2008-05-05 FIXME

Warning: ksort() expects parameter 1 to be array, boolean given in /../wiki/lib/plugins/btable/syntax.php on line 192

used code:

 \\
<btable Testanfrage: Palmenfest in der Karibik am 16.06 bis 29.08>
  <columns>
    ^ JA ^ NEIN ^ unsicher ^
  </columns>
  <rows>
    ^ siggi ^ Andreas ^ Tomatensaft ^
  </rows>
</btable>

I have the same problem. I can see it when I create new tables, old ones do not show it anymore, I do not why. —Ayla 14/09/08

Same problem. I commented out the ksort so the error went away but the rows appear as a single combo-box rather than multiple rows. Looks like it would be a great pluging for what I need :-( Andy 26-Sep-08

  • Deleting entries does not seem to work for anything except Mozilla Firefox.
    Tried Internet Explorer and Opera Marcin - 20080707

  • A fix for 'Bug with dokuwiki 2008-05-05'

The array $boolean is not created until needed, that is, when a first entry into the table is made. Hence, it is false, causing ksort() to complain because it expects parameter 1 to be array, boolean given. A quick fix is to call ksort() only if $boolean exists. For this, replace lines 191-192

// sort rows
ksort($doodle);

in file ~/plugins/btable/syntax.php with

// sort rows
if ($doodle) {
  ksort($doodle);
}

Hope this helps Miquel 2008-11-26


  • Is the syntax above the right one?

I have a problem when using the <btable> syntax mentioned above: the extra carets (^) before the first column label and after the last column label translate into two extra unlabeled columns, but when removing those extra carets the table displays correctly. Accordingly, with the present code the syntax seems to be,

<btable [id]>
  <columns>
    [column] ^ [column] ^ ... 
  </columns>
  <rows>
    [row] ^ [row] ^ ...
  </rows>
</btable>

* Feature request: multi-valued entries instead of boolean flags

This is a great plugin and almost what I have been searching for. Almost because a boolean table does not suffice for my problem. I need to coordinate a (fixed) bunch of people who are regularly attending to events. Now every single person of this bunch of people may bring additional people to the event. So instead of a boolean tick-box I would need a drop-down box with a selectable integer. Preferably the content of this drop-down box would be configurable (but could be the same for the whole table). I would set the possible values to 0, 1, 2, 3, 4, 5, 6, 7, 8 - indicating the person count in my case.

Martin 2008-11-27

  • I wanna support this message: great plugin, but drop-down box instead of (or additionally to) check box would really be very helpful for me.
    [Stefan] 2009-03-08
  • I also would very much like the ability to select from a drop-down box. If anyone can add this feature it would be greatly appreciated.
    [Larry] 2009-04-08
  • Seems like a good idea. Why not let the user enter free text as a comment?
    [jolz] 2009-08-04

—————————— * Show row options as text instead of a drop-down box?
Is this possible? On the demo at http://home.edo.uni-dortmund.de/~horst/btable/doku.php the rows are shown as buttons. The current version renders them as a dropdown box. I'd like just to have plain text in the row down the left hand side. Any hints please email Laurence 2008-12-16-

1) If it is not, metadata of the btable with the same id gets mixed up, i.e. answers to choices in a previous btable appear in the new when the choices are the same.
plugin/btable.txt · Last modified: 2015-02-11 00:52 by 203.5.137.74