Практический пример. Расширение свойств SQL::DocumentTable

Начальные условия

Шаблонно созданный проект со стандартным набором таблиц. В частности, таблица 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.

Практический пример. Расширение свойств SQL::DocumentTable
Документы, построенные на собственных (кастомных) таблицах
Связи, построенные на собственных (кастомных) таблицах
Дескриптор атрибутов объектов