Начальные условия
Шаблонно созданный проект со стандартным набором таблиц. В частности, таблица documents выглядит при создании так:
create table documents
(
id integer not null primary key default nextval('public.documents_id_seq'::text),
class text not null,
ctime timestamp not null default now(),
mtime timestamp not null default now(),
dtime timestamp not null default now(),
status smallint not null default 0,
sections integer[],
name text,
data text
);
create index documents_sections on documents using gist ( "sections" "gist__int_ops" );
create index documents_dtime on documents (dtime);
.
Задача
Типовая задача – обеспечить отображение документов с помощью ЧПУ (человекопонятных урлов). URL конечной страницы при этом строится так:
http://mycoolsite.com/about/contacts.html
, где about – web-alias раздела, представленного секцией; contacts.html – веб-алиас текстового документа.
Расширяем таблицу documents.
Для решения задачи достаточно добавить поле alias, в котором будет храниться префикс документа, до .html. Данную информацию не следует размещать в extra-полях, так как нам предстоит выуживать документы из базы по названию, переданному в url.
alter table documents add column alias text;
create unique index documents_alias on documents (alias) where alias is not null and alias != '';
Индекс с условием, так как в таблице documents у нас могут храниться не только страницы веб-сайта, но и объекты иных классов.
Создаем проектный дескриптор таблицы documents
Наследуем его от Contenido::Document, наываем DocumentTable.pm и складываем в src/projects/myproject/lib/myproject/SQL/. Модуль будет совсем коротким:
package myproject::SQL::DocumentTable;
use strict;
use base 'SQL::DocumentTable';
sub available_filters {
my @available_filters = qw(
_class_filter
_status_filter
_in_id_filter
_id_filter
_name_filter
_class_excludes_filter
_sfilter_filter
_datetime_filter
_date_equal_filter
_date_filter
_previous_days_filter
_s_filter
_excludes_filter
_link_filter
_alias_filter
);
return \@available_filters;
}
sub required_properties
{
my $self = shift;
my @parent_properties = $self->SUPER::required_properties;
return (
@parent_properties,
{
'attr' => 'alias',
'type' => 'string',
'rusname' => 'Веб-алиас страницы',
'column' => 3,
'db_field' => 'alias',
'db_type' => 'text',
'rem' => 'Значение данного поля будет формировать веб-адрес конечной страницы в виде alias.html',
}
);
}
sub _alias_filter {
my ($self,%opts)=@_;
return undef unless ( exists $opts{alias} );
return &SQL::Common::_generic_text_filter('d.alias', $opts{alias});
}
1;
В данном модуле расширен метод required_properties, в него добавлено описание поля alias. Тип поля для формы редактирования – string, колонка в списке документов (column) – 3, в режиме редактирования также будет отображен небольшой комментарий (rem).
Для организации выбора по полю alias добавлен фильтр _alias_filter, в котором задействован станлартный фильтр _generic_text_filter, организующий поиск по точному совпадению.
Для регистрации фильтра по полю alias переписан метод available_filters; метод можно было бы переписать элегантней, но для повышения очевидности его содержимое просто скопировано с метода более высокого уровня.
Связываем объект Contenido с дескриптором
Создаем модуль myproject::Article, унаследованный от Contenido::Document, название файла Article.pm, место в файловой структуре – src/projects/myproject/lib/myproject/.
package myproject::Article;
use strict;
use base 'Contenido::Document';
sub extra_properties
{
return (
{ 'attr' => 'dtime', 'type' => 'date', 'rusname' => 'Дата', },
{ 'attr' => 'status', 'type' => 'status', 'rusname' => 'Статус',
cases => [
[0, 'Страница не активна'],
[1, 'Страница активна и присутствует в меню'],
[2, 'Страница активна и не присутствует в меню'],
[3, 'К удалению'],
],
},
{ 'attr' => 'abstr', 'type' => 'text', 'rusname' => 'Аннотация', rows => 5 },
{ 'attr' => 'body', 'type' => 'wysiwyg', 'rusname' => 'Полный текст документа', rows => 45 },
{ 'attr' => 'file', 'type' => 'multimedia_new', 'rusname' => 'Файл / документ', softrename => 1 },
{ 'attr' => 'icon', 'type' => 'image', 'rusname' => 'Иконка', preview => ['240x240'], crop => ['150x150'] },
{ 'attr' => 'pictures', 'type' => 'images', 'rusname' => 'Список изображений', preview => ['240x240','400x400','900x750'], crop => ['150x150'] },
)
}
sub class_name
{
return 'Статья или веб-страница';
}
sub class_description
{
return 'Статья или веб-страница';
}
sub class_table
{
return 'myproject::SQL::DocumentTable';
}
1;
Пока можно не обращать на список extra-полей, в данном примере важен класс class_table, связывающий наш объект myproject::Article с дескриптором таблицы.
Теперь после перегрузки проекта (make nano или make reload), класс myproject::Article будет автоматически зарегистрирован в списке документов проекта. В системе администрирования можно будет проводить все операции с данными документами (создавать, редактировать, переносить или связывать с дополнительными секциями, удалять); также документы данного класса будут доступны для создания в приложении и для выбора данных с помощью метода $keeper->get_documents, в частности по полю alias.
Организуем разбор url и поиск документа
Один из вариантов решения строится на базе dhandler (см документацию по mason). В src/projects/myproject/comps/www/ создаем файл dhandler примерно такого содержания:
% if ( $call ) {
<& $call, %ARGS &>
% } else {
% &abort404;
% }
<%init>
my $call;
my @dargs = split /\//, $m->dhandler_arg;
my (@path, $path);
my $s_alias = shift @dargs;
($section) = $keeper->get_sections (
class => 'myproject::MediaSection',
alias => $s_alias,
limit => 1,
);
&abort404 unless ref $section;
my $url = '/'.($section->alias || $section->id).'/';
$section->{path} = $url;
$ARGS{mainsection} = $section;
$ARGS{section} = $section;
push @path, $section;
my $tree = $keeper->get_section_tree( root_id => $section->id, light => undef );
$ARGS{tree} = $tree;
$section = $tree->{root};
$call = $ARGS{section}->template ? '/www/'.$ARGS{section}->template.'.html' : '/www/section.html';
while ( @dargs ) {
my $part = shift @dargs;
if ( $part !~ /\.(html)$/ ) {
my $subsection;
if ( exists $section->{children} && @{$section->{children}} ) {
if ( $part =~ /\D/ ) {
($subsection) = grep { $_->alias eq $part } @{$section->{children}};
} else {
($subsection) = grep { $_->id == int($part) } @{$section->{children}};
}
}
&abort404 unless ref $subsection;
$url .= ($subsection->alias || $subsection->id).'/';
$subsection->{path} = $url;
$ARGS{section} = $subsection;
$request->{section} = $section;
push @path, $subsection;
$call = $ARGS{section}->template ? '/www/'.$ARGS{section}->template.'.html' : '/www/section.html';
$section = $subsection;
} elsif ( $part eq 'index.html' ) {
} elsif ( $part =~ /^(.*)\.html$/ ) {
my $d_alias = $1;
my $document;
if ( $d_alias =~ /^\d+$/ ) {
$document = $keeper->get_document_by_id ( $d_alias,
class => 'myproject::Article',
status => 'positive',
);
} else {
($document) = $keeper->get_documents (
s => $ARGS{section}->id,
class => 'myproject::Article',
alias => $d_alias,
status => 'positive',
limit => 1,
);
}
&abort404 unless ref $document;
$ARGS{id} = $document->id;
$url .= ($document->alias || $document->id).'.html';
$document->{path} = $url;
$ARGS{doc} = $document;
$call = '/www/document.html';
push @path, $document;
}
}
$ARGS{path} = \@path;
$r->content_type('text/html; charset=utf-8');
Выделенный фрагмент демонстрирует выборку из БД документа, опираясь на его web-alias.