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

В предыдущих статьях, мы рассмотрели теорию хранения и управления древовидных структур данных, а так же реализовали 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;

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

28 февраля 2007 в 13:39
Материалы по теме
{"url":"http://www.fastvps.ru/", "src":"/images/advbanners/fastvps.png", "alt":"Хостинг Fastvps.ru. Наш выбор!"}
Заработок