Скрипт управления деревом

В предыдущих статьях, мы рассмотрели теорию хранения и управления древовидных структур данных, а так же реализовали Perl модуль для облегчения управления ими. Теперь напишем небольшой скрипт упраления (администрирования). Идея скрипта проста — требуется легко и непринужденно, с помощью скрипта, управлять деревом каталогов.

Возьмем стандартную таблицу в которой будет хранится наше дерево каталогов, в ней мы будем хранить только одно дерево, реализацию принципа работы, при котором количество деревьев неограничено, я рассмотрю в следующих статьях. Итак, наша таблица:

CREATE TABLE
 my_tree (
 id INT(11) NOT NULL AUTO_INCREMENT,
 left_key INT(11) NOT NULL DEFAULT 0,
 right_key INT(11) NOT NULL DEFAULT 0,
 level INT(11) NOT NULL DEFAULT 1,
 name VARCHAR(150) NOT NULL,
 PRIMARY KEY
 id (id),
 INDEX
 left_key (left_key, right_key, level)
 );

Конечно, количество дополнительных полей (одно из них — name) может быть неограничено.
Концепция

Для начала определим, какие функции должен выполнять наш скрипт:

* создание узла;
* редактирование узла (с возможностью изменения подчиненности);
* удаление узла;
* перемещение узла на уровень вверх;
* перемещение узла на уровень вниз;
* перемещение узла на порядок вверх (в пределах подчиненности);
* перемещение узла на порядок вниз (в пределах подчиеннности);

Ограничение доступа — вообще не учитывал, то есть авторизация скрипта, проверка доступа — просто отсутсвуют. Оставлю это на Вашей совести. А вообще, проще всего, просто запаролировать директирию скрипта .htaccess и все…

HTML шаблон*, я все-таки вынес из скрипта — терпеть не могу править HTML в скрипте, и Вам того не советую. Шаблон состоит из трех частей:

* верхняя часть (header.html) — заголовки и прочая до момента вывода списка категорий (начало таблицы);
* строка списка категорий (row.html) — одна строка таблицы списка категорий;
* нижняя часть (footer.html) — конец таблицы вывода списка категорий, форма создания, редактирования категории;

* Эта структура шаблонов была придумана "на ходу", поэтому не будем заострять внимание на её правильности, не это важно.
HTML-код шаблонов:

header.html

<html><head>
<title>Скрипт управления деревом каталогов</title>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<script language="javascript">
function EditCat (IdCat, NameCat, ParentCat) {
 document.getElementById('TitleForm').innerHTML = 'ИЗМЕНИТЬ КАТЕГОРИЮ';
 document.getElementById('buttonForm').value = 'Изменить';
 document.FormCategory.id.value = IdCat;
 document.FormCategory.name.value = NameCat;
 document.FormCategory.parent.options[ParentCat].selected = true;
 document.FormCategory.doing.value = 'edit';
}
function ClearFormEdit () {
 document.getElementById('TitleForm').innerHTML = 'ДОБАВИТЬ КАТЕГОРИЮ';
 document.getElementById('buttonForm').value = 'Добавить';
 document.FormCategory.id.value = 'xx';
 document.FormCategory.name.value = 'Новая категория';
 document.FormCategory.doing.value = 'new';
}
</script>
</head>
<body>
<h1>Скрипт управления деревом каталогов</h1>
<h2>Список категорий фирм</h2>
<table width="100%" border="0" cellpadding="0" cellspacing="0">

row.html

<tr>
 <td>[$prefix$] [$name$]</td>
 <td width="80">
 <a href="#form" onClick="javascript: EditCat ('[$id$]','[$name$]','[$par$]');">
 изменить
 </a>
 </td>
 <td width="80"><a href="?ac=[$ac$]&doing=delete&id=[$id$]">удалить</a></td>
 <td width="80"><a href="?ac=[$ac$]&doing=level_up&id=[$id$]">влево</a></td>
 <td width="80"><a href="?ac=[$ac$]&doing=level_down&id=[$id$]">вправо</a></td>
 <td width="80"><a href="?ac=[$ac$]&doing=order_up&id=[$id$]">вверх</a></td>
 <td width="80"><a href="?ac=[$ac$]&doing=order_down&id=[$id$]">вниз</a></td>
</tr>

footer.html

</table>
<a name="form"></a>
<table align="center" width="95%" border="0" cellpadding="0" cellspacing="0">
 <form action="?" method="post" name="FormCategory">
 <tr><td colspan="2"><h1 id="TitleForm">ДОБАВИТЬ КАТЕГОРИЮ</h1></td></tr>
 <tr>
 <td>Название категории</td>
 <td><input type="text" value="Новая категория" name="name" size="60"></td>
 </tr>
 <tr>
 <td>Подчинение категории</td>
 <td><select name="parent">
 <option value="root">-- Без подчинения --</option>
 [$list_select$]
 </select></td>
 </tr>
 <tr>
 <td colspan="2">&nbsp;
 <input type="hidden" name="id" value="xx">
 <input type="hidden" name="doing" value="new">
 <input type="hidden" name="ac" value="[$ac$]">
 </td>
 </tr>
 <tr>
 <td colspan="2" align="center">
 <input type="submit" name="buttonForm" id="buttonForm" value="Добавить">
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
 <input type="reset" value="Вернуть" onClick="javascript: ClearFormEdit ();">
 </td>
 </tr>
 </form>
</table>
</body></html>

Теперь поясню отдельные моменты:

* В первом файле включен JavaSript, так как форма редактирования и создания одна, то при нажатии на ссылку "изменить" нужно внести соответствующие данные в форму. По кнопке "Вернуть" формы, требуется очистить форму от данных редактируемого узла;
* текст заключенный в квадратные скобки и знак доллара ([$текст$]), то что будет динамически заменяться нашим скриптом, где:
o [$id$] — идентификатор узла;
o [$name$] — поле name узла, или имя узла;
o [$prefix$] — отступ на который смещается имя узла (зависит от уровня узла);
o [$par$] — порядковый номер родительского узла в select формы редактирования (не путать с id родительского узла!);
o [$ac$] — случайный набор символов, я буду использовать текущее время (против кеширования страниц);

Код скрипта

Для начала определим где какие файлы у нас будут лежать:

* cgi-bin/admin_tree/
 o lib/
 + MP/
 # NestedSets.pm
 o template/
 + header.html
 + row.html
 + footer.html
 o admin.pl

Что за файл NestedSets.pm, я думаю, объяснять не нужно (это модуль описанный в предыдущих статьях), с .html файлами — тоже понятно, остался только один файл — admin.pl, его мы как раз и опишем. Итак, код скрипта:

#!/usr/bin/perl
# Подключение основных модулей
use strict;
use CGI;
use DBI;
use vars '$query',
 '$dbh', # объект подключения в базе данных
 '$nested', # объект работы с деревои NestedSets
 '%user_vars'; # Глобальные пользовательские переменные

# Подключаем модуль для работы с деревои NestedSets
use lib 'lib/';
use Global::NestedSets;
# Указываем переменные пользовательские переменные
$user_vars{'table'} = 'my_tree'; # Имя таблицы БД
# Коннект к базе
$dbh = 'DBI'->connect('DBI:mysql:database=mybase:host=localhost:port=3306',
 'user', 'password') || die $DBI::errstr;
# Выбираем переданные данные
$query = new CGI;
$user_vars{'id'} = $query->param('id') || undef; # Идентификатор узла
$user_vars{'doing'} = $query->param('doing') || undef; # Производимое действие
# Определяем объект Global::NestedSets
$nested = new Global::NestedSets {DBI=>$dbh, table=>$user_vars{'table'}};

# Если производится какое-либо действие
if ($user_vars{'doing'}) {
# Действие - поднять узел на уровень вверх
 if ($user_vars{'doing'} eq 'level_up') {
 $nested->set_unit_level(unit=>$user_vars{'id'}, move=>'up');
# Действие - опустить узел на уровень вниз
 } elsif ($user_vars{'doing'} eq 'level_down') {
 $nested->set_unit_level(unit=>$user_vars{'id'}, move=>'down');
# Действие - поднять узел на порядок вверх
 } elsif ($user_vars{'doing'} eq 'order_up') {
 $nested->set_unit_order(unit=>$user_vars{'id'}, move=>'up');
# Действие - опустить узел на порядок вниз
 } elsif ($user_vars{'doing'} eq 'order_down') {
 $nested->set_unit_order(unit=>$user_vars{'id'}, move=>'down');
# Действие - удалить узел
 } elsif ($user_vars{'doing'} eq 'delete') {
 $nested->delete_unit(unit=>$user_vars{'id'});
# Действие - создать узел
 } elsif ($user_vars{'doing'} eq 'new') {
# Выбираем данные формы
 $user_vars{'name'} = $query->param('name') || 'Новая';
 $user_vars{'parent'} = $query->param('parent') || 'root';
# Создаем узел в дереве и получаем его ID
 $user_vars{'id'} = $nested->insert_unit(under=>$user_vars{'parent'});
# Обновляем дополнительные поля узла
 $dbh->do('UPDATE '.$user_vars{'table'}.
 ' SET name = ''.$user_vars{'name'}.''
 WHERE id = '.$user_vars{'id'}) || die $DBI::errstr;
# Действие - отредактировать
 } elsif ($user_vars{'doing'} eq 'edit') {
# Выбираем данные формы
 $user_vars{'name'} = $query->param('name') || 'Новая';
 $user_vars{'parent'} = $query->param('parent') || 'root';
# Выбираем ID родителя редактируемого узла
 my $check = ($nested->get_parent_id(unit=>$user_vars{'id'}))->[0];
# Если меняется родительский узел, то производим перемещение
 if ($check ne $user_vars{'parent'}) {
 $nested->set_unit_under(unit => $user_vars{'id'},
 under => $user_vars{'parent'})
 }
# Обновляем дополнительные поля узла
 $dbh->do('UPDATE '.$user_vars{'table'}.
 ' SET name = ''.$user_vars{'name'}.''
 WHERE id = '.$user_vars{'id'}) || die $DBI::errstr;
 }
}

# Выдаем заголовок браузеру
 print "Content-type: text/html; charset=windows-1251nn";
# Открываем шаблон верхней части страницы и выводим его на экран
 open (HTML, './template/header.html') || die 'Can not open file header.html!';
 print <HTML>;
 close HTML;

# Открываем шаблон строки списка и заносим его в переменную
 open (HTML, './template/row.html') || die 'Can not open file row.html!';
 my $line = join('', <HTML>);
 close HTML;

# Выбираем полностью все дерево и сортируем по левому ключу
 my $sql = 'SELECT id, name, level
 FROM '.$user_vars{'table'}.'
 ORDER BY left_key';
 my $sth = $dbh->prepare($sql); $sth->execute() || die $DBI::errstr;
# Объявляем хеш и переменную (счетчик) с помощью которого будем определять
# порядок родительского узла в списке select формы
 my %par = (0 => '0', root => '0'); my $i = 1;
# Объявляем переменную для формирования списка select формы
 my $list_select;
 while (my $row = $sth->fetchrow_hashref()) {
# Копируем шаблон строки во временную переменную
 my $temp_line = $line;
# Формируем переменную для "антикеша"
 $$row{'ac'} = time;
# Формируем отступ перед названием узла
 $$row{'prefix'} = '&nbsp;&nbsp;&nbsp;&nbsp;' x ($$row{'level'} - 1);
# Определяем порядок родительского узла в списке select формы
 $$row{'par'} = $par{($nested->get_parent_id(unit=>$$row{'id'}))->[0]};
 $par{$$row{'id'}} = $i; $i++;
# Обрабатываем строку заменяя соотвествующие 
 $temp_line =~s /[$(w+)$]/$$row{$1}/g;
 print $temp_line;
 $list_select .= '<option value="'.$$row{'id'}.'">'.
 $$row{'prefix'}.$$row{'name'}.'</option>';
 }
 $sth->finish();

# Открываем шаблон нижней части страницы и записываем его в переменную
 open (HTML, './template/footer.html') || die 'Can not open file footer.html!';
 my $footer = join('', <HTML>);
 close HTML;
# Вносиим в шаблон список select формы
 $footer =~s /[$list_select$]/$list_select/g;
# ... и выводим на экран
 print $footer;
# Все...
exit;
1;

Вот собственно и все. Как видно, никаких сложностей нет, сам скрипт размером менее чем сто строк благодаря использованию модуля. Конечно, нужно еще проверить данные формы, может быть, некоторые операции…

Меню