Line # Revision Author
1 8 ahitrov@rambler.ru package Contenido::Section;
2
3 # ----------------------------------------------------------------------------
4 # Класс Секция.
5 # Теперь это опять же - базовый класс, вводим в него дополнительную
6 # функциональность.
7 # ----------------------------------------------------------------------------
8
9 use strict;
10 use warnings;
11 use locale;
12
13 use vars qw($VERSION $ROOT);
14 $VERSION = '6.0';
15
16 use base 'Contenido::Object';
17 use Contenido::Globals;
18
19 $ROOT = 1; # Корневая секция
20
21 sub class_name {
22 return 'Секция';
23 }
24
25 sub class_description {
26 return 'Секция по умолчанию';
27 }
28
29 # DEFAULT клас реализации таблицы
30 sub class_table {
31 return 'SQL::SectionTable';
32 }
33
34 # ----------------------------------------------------------------------------
35 # Конструктор. Создает новый объект сессии.
36 #
37 # Формат использования:
38 # Contenido::Section->new()
39 # Contenido::Section->new($keeper)
40 # Contenido::Section->new($keeper,$id)
41 # Contenido::Section->new($keeper,$id,$pid)
42 # ----------------------------------------------------------------------------
43 sub new {
44 my ($proto, $keeper, $id, $pid) = @_;
45 my $class = ref($proto) || $proto;
46 my $self;
47
48 if (defined($id) && ($id>0) && defined($keeper)) {
49 $self=$keeper->get_section_by_id($id, class=>$class);
50 } else {
51 $self = {};
52 bless($self, $class);
53 $self->init();
54 $self->keeper($keeper) if (defined($keeper));
55 $self->{class} = $class;
56 $self->id($id) if (defined($id) && ($id > 0));
57 $self->pid($pid) if (defined($pid) && ($pid > 0));
58 }
59
60 return $self;
61 }
62
63 sub _get_table {
64 class_table()->new();
65 }
66
67 607 ahitrov
68 8 ahitrov@rambler.ru #доработка метода store
69 sub store {
70 608 ahitrov my $self = shift;
71 my (%opts) = @_;
72 8 ahitrov@rambler.ru
73 #для новосозданных секций ставим новый sorder
74 608 ahitrov if ( $self->{id} ) {
75 my $without_sort = delete $opts{without_sort};
76 unless ( $without_sort || $self->{__light} ) {
77 ### Autofill or autoflush documents order if applicable
78 if ( $self->_sorted ) {
79 my $ids = $self->_get_document_order;
80 $self->_sorted_order( join(',', @$ids) );
81 } elsif ( !$self->_sorted && $self->_sorted_order ) {
82 $self->_sorted_order( undef );
83 }
84 }
85 } else {
86 my ($sorder) = $self->keeper->SQL->selectrow_array("select max(sorder) from ".$self->class_table->db_table(), {});
87 $self->{sorder} = $sorder + 1;
88 8 ahitrov@rambler.ru }
89
90 return $self->SUPER::store();
91 }
92
93
94 ## Специальные свойства для секций с встроенной сортировкой документов
95 sub add_properties {
96 return (
97 616 ahitrov {
98 'attr' => '_page_size',
99 'type' => 'integer',
100 'rusname' => 'Количество документов, подгружаемых на страницу раздела в админке',
101 'default' => 40,
102 'hidden' => 1
103 },
104 { # Признак "секция с сортировкой"
105 8 ahitrov@rambler.ru 'attr' => '_sorted',
106 'type' => 'checkbox',
107 'rusname' => 'Ручная сортировка документов',
108 },
109 616 ahitrov { # Порядок документов (список id)
110 8 ahitrov@rambler.ru 'attr' => '_sorted_order',
111 'type' => 'string',
112 'rusname' => 'Порядок документов в секции',
113 'hidden' => 1
114 },
115 {
116 'attr' => 'default_document_class',
117 'type' => 'string',
118 'rusname' => 'Класс документов в секции, показываемый по умолчанию',
119 },
120 {
121 'attr' => 'default_table_class',
122 'type' => 'string',
123 'rusname' => 'Класс таблицы, документы которой будут показаны по умолчанию',
124 },
125 {
126 'attr' => 'order_by',
127 'type' => 'string',
128 'rusname' => 'Сортировка документов',
129 },
130 {
131 118 ahitrov 'attr' => 'no_count',
132 'type' => 'checkbox',
133 'rusname' => 'Не пересчитывать документы в разделе админки',
134 },
135 {
136 8 ahitrov@rambler.ru 'attr' => 'filters',
137 'type' => 'struct',
138 'rusname' => 'Дополнительные фильтры выборки',
139 },
140 );
141 }
142
143 # Конструктор для создания корневной секции
144 sub root {
145 return new(shift, shift, $ROOT);
146 }
147
148
149 # Заглушка для метода parent
150 sub parent {
151 return shift->pid(@_);
152 }
153
154 # ----------------------------------------------------------------------------
155 # Удаление секции. Перед удалением происходит проверка - а имеем
156 # ли мы право удалять (может есть детишки). Если есть детишки, то секция
157 # не удаляется. Проверка на наличие объектов в этой секции не производится,
158 # но секция вычитается из всех сообщений.
159 # ----------------------------------------------------------------------------
160 sub delete
161 {
162 my $self = shift;
163 do { $log->error("Метод ->delete() можно вызывать только у объектов, но не классов"); die } unless ref($self);
164 do { $log->warning("Вызов метода ->delete() без указания идентификатора для удаления"); return undef } unless ($self->{id});
165
166 # Проверка наличия детей...
167 my ($one_id) = $self->keeper->SQL->selectrow_array('select id from '.$self->class_table->db_table.' where pid = ?', {}, $self->id);
168 if (defined($one_id) && ($one_id > 0)) { return "Нельзя удалить секцию, у которой есть вложенные секции\n"; };
169
170 $self->SUPER::delete();
171
172 return 1;
173 }
174
175
176
177 # ----------------------------------------------------------------------------
178 # Метод, возвращающий массив детишек (в порядке sorder для
179 # каждого уровня). В параметре передается глубина.
180 #
181 # Формат вызова:
182 # $section->childs([глубина]);
183 # ----------------------------------------------------------------------------
184 sub childs {
185 my ($self, $depth) = @_;
186 do { $log->error("Метод ->childs() можно вызывать только у объектов, но не классов"); die } unless ref($self);
187
188 # Глубина по умолчанию - 1
189 $depth ||= 1;
190
191 my $SIDS = [];
192 my $NEW_SIDS = [$self->id];
193
194 #пока не достигнута нужная глубина и пока нашлись новые дети
195 while ($depth>0) {
196 $NEW_SIDS = $self->keeper->get_sections(s=>$NEW_SIDS, ids=>1, return_mode=>'array_ref', order_by=>'pid, sorder');
197 if (ref($NEW_SIDS) and @$NEW_SIDS) {
198 push (@$SIDS, @$NEW_SIDS);
199 } else {
200 last;
201 }
202 $depth--;
203 }
204 return @$SIDS;
205 }
206
207
208 608 ahitrov sub _get_document_order {
209 my ($self) = shift;
210 609 ahitrov return unless $self->_sorted;
211 8 ahitrov@rambler.ru
212 608 ahitrov my @order = $self->_sorted_order ? split( /,/, $self->_sorted_order ) : ();
213
214 my %opts;
215 if ( $self->default_document_class ) {
216 $opts{class} = $self->default_document_class;
217 } elsif ( $self->default_table_class ) {
218 $opts{table} = $self->default_table_class;
219 } else {
220 $opts{table} = 'Contenido::SQL::DocumentTable';
221 }
222 if ( $self->order_by ) {
223 $opts{order_by} = $self->order_by;
224 }
225 if ( $self->filters ) {
226 no strict 'vars';
227 my $filters = eval($self->filters);
228 if ($@) {
229 warn "Bad filter: " . $self->filters . " in section " . $self->id;
230 } elsif (ref $filters eq 'HASH') {
231 while ( my ($key, $val) = each %$filters ) {
232 $opts{$key} = $val;
233 }
234 }
235 }
236 my $ids = $keeper->get_documents( s => $self->id, %opts, ids => 1, return_mode => 'array_ref' );
237 my %ids = map { $_ => 1 } @$ids;
238 my @new_order;
239 foreach my $iid ( @order ) {
240 if ( exists $ids{$iid} ) {
241 push @new_order, $iid;
242 delete $ids{$iid};
243 }
244 }
245 foreach my $iid ( @$ids ) {
246 if ( exists $ids{$iid} ) {
247 push @new_order, $iid;
248 delete $ids{$iid};
249 }
250 }
251 return \@new_order;
252 }
253
254
255 609 ahitrov sub _get_document_pos {
256 my ($self) = shift;
257 my ($doc_id) = shift;
258 return unless $doc_id && $doc_id =~ /^\d+$/;
259 return unless $self->_sorted;
260
261 my @order = $self->_sorted_order ? split( /,/, $self->_sorted_order ) : ();
262 my %pos;
263 for ( my $i = 0; $i < scalar @order; $i++ ) {
264 if ( $order[$i] == $doc_id ) {
265 $pos{index} = $i;
266 if ( $i > 0 ) {
267 $pos{after} = $order[$i-1];
268 }
269 if ( $i < $#order ) {
270 $pos{before} = $order[$i+1];
271 }
272 if ( $i == $#order ) {
273 $pos{last} = 1;
274 } elsif ( $i == 0 ) {
275 $pos{first} = 1;
276 }
277 last;
278 }
279 }
280 return (exists $pos{index} ? \%pos : undef);
281 }
282
283
284 8 ahitrov@rambler.ru # ----------------------------------------------------------------------------
285 # Метод для перемещение секции вверх/вниз по рубрикатору (изменение
286 # sorder)...
287 #
288 # Формат вызова:
289 # $section->move($direction); Направление задается строкой 'up'/'down'
290 # ----------------------------------------------------------------------------
291 sub move {
292 my ($self, $direction) = @_;
293 do { $log->error("Метод ->move() можно вызывать только у объектов, но не классов"); die } unless ref($self);
294
295 return undef if ($self->keeper->state->readonly());
296
297 my $keeper = $self->keeper;
298 do { $log->error("В объекте секции не определена ссылка на базу данных"); die } unless ref($keeper);
299 do { $log->warning("Вызов метода ->move() без указания идентификатора секции"); return undef }
300 unless (exists($self->{id}) && ($self->{id} > 0));
301 do { $log->warning("Вызов метода ->move() без указания порядка сортировки (sorder)"); return undef }
302 unless (exists($self->{sorder}) && ($self->{sorder} >= 0));
303 do { $log->warning("Вызов метода ->childs() без указания родителя"); return undef } unless (exists($self->{pid}) && ($self->{pid} >= 0));
304
305 $direction = lc($direction);
306 if ( ($direction ne 'up') && ($direction ne 'down') ) { $log->warning("Направление перемещения секции задано неверено"); return undef };
307
308
309 $keeper->t_connect() || do { $keeper->error(); return undef; };
310 $keeper->TSQL->begin_work();
311
312
313 # Получение соседней секции для обмена...
314 my ($id_, $sorder_);
315 if ($direction eq 'up')
316 {
317 ($id_, $sorder_) = $keeper->TSQL->selectrow_array("select id, sorder from ".$self->class_table->db_table." where sorder < ? and pid = ? order by sorder desc limit 1", {}, $self->{sorder}, $self->{pid});
318 } else {
319 ($id_, $sorder_) = $keeper->TSQL->selectrow_array("select id, sorder from ".$self->class_table->db_table." where sorder > ? and pid = ? order by sorder asc limit 1", {}, $self->{sorder}, $self->{pid});
320 }
321
322
323 # Собственно обмен...
324 if ( defined($id_) && ($id_ > 0) && defined($sorder_) && ($sorder_ > 0) )
325 {
326 $keeper->TSQL->do("update ".$self->class_table->db_table." set sorder = ? where id = ?", {}, $sorder_, $self->{id})
327 || return $keeper->t_abort();
328 $keeper->TSQL->do("update ".$self->class_table->db_table." set sorder = ? where id = ?", {}, $self->{sorder}, $id_)
329 || return $keeper->t_abort();
330 } else {
331 $log->warning("Не могу поменяться с элементом (он неверно оформлен или его нет)"); return 2;
332 }
333
334 $keeper->t_finish();
335 $self->{sorder} = $sorder_;
336 return 1;
337 }
338
339
340
341 # ----------------------------------------------------------------------------
342 # Метод для перемещения документа с id = $doc_id вверх/вниз
343 # по порядку сортировки (в пределах текущей секции)...
344 #
345 # Формат вызова:
346 # $doc->dmove($doc_id, $direction); Направление задается строкой 'up'/'down'
347 # ----------------------------------------------------------------------------
348 sub dmove {
349 608 ahitrov my ($self, $doc_id, $direction, $anchor) = @_;
350 8 ahitrov@rambler.ru do { $log->error("Метод ->dmove() можно вызывать только у объектов, но не классов"); die } unless ref($self);
351
352 return undef if ($self->keeper->state->readonly());
353
354 my $keeper = $self->keeper;
355 do { $log->error("В объекте не определена ссылка на базу данных"); die } unless ref($keeper);
356 do { $log->warning("Вызов метода ->dmove() без указания идентификатора секции"); return undef }
357 unless (exists($self->{id}) && ($self->{id} > 0));
358
359 $direction = lc($direction);
360 608 ahitrov unless ( $direction eq 'up' || $direction eq 'down' || $direction eq 'first' || $direction eq 'last' || $direction eq 'before' || $direction eq 'after' ) {
361 $log->warning("Направление перемещения документа задано неверно"); return undef
362 };
363 my $anchor_flag = 0;
364 if ( ($direction eq 'before' || $direction eq 'after') && !$anchor ) {
365 $log->warning("Неверный вызов функции dmove для направления '$direction'. Необходимо указать дополнительно id в списке документов"); return undef;
366 } elsif ( $direction eq 'before' || $direction eq 'after' ) {
367 $anchor_flag = 1;
368 }
369 8 ahitrov@rambler.ru
370 if ($self->_sorted()) {
371 608 ahitrov my $order = $self->_get_document_order;
372 my @new_order;
373 if ( $direction eq 'first' ) {
374 push @new_order, $doc_id;
375 }
376 for ( my $i = 0; $i < scalar @$order; $i++ ) {
377 if ( ($direction eq 'first' || $direction eq 'last' || $direction eq 'after' || $direction eq 'before') && $order->[$i] == $doc_id ) {
378 next;
379 } elsif ( $direction eq 'up' && $order->[$i] == $doc_id ) {
380 if ( $i ) {
381 my $id = pop @new_order;
382 push @new_order, ($doc_id, $id);
383 } else {
384 push @new_order, $doc_id;
385 }
386 } elsif ( $direction eq 'down' && $order->[$i] == $doc_id ) {
387 if ( $i < scalar(@$order) - 1 ) {
388 push @new_order, ($order->[++$i], $doc_id);
389 } else {
390 push @new_order, $doc_id;
391 }
392 } elsif ( $direction eq 'before' && $order->[$i] == $anchor ) {
393 push @new_order, ($doc_id, $order->[$i]);
394 $anchor_flag = 0;
395 } elsif ( $direction eq 'after' && $order->[$i] == $anchor ) {
396 push @new_order, ($order->[$i], $doc_id);
397 $anchor_flag = 0;
398 } else {
399 push @new_order, $order->[$i];
400 }
401 }
402 if ( $anchor_flag ) {
403 $log->warning("Неверный вызов функции dmove для направления '$direction'. Не найден якорь [$anchor]"); return undef;
404 }
405 if ( $direction eq 'last' ) {
406 push @new_order, $doc_id;
407 }
408 8 ahitrov@rambler.ru
409 608 ahitrov $self->{_sorted_order} = join ',', @new_order;
410 $self->store( without_sort => 1 );
411 8 ahitrov@rambler.ru } else {
412 608 ahitrov $log->warning("dmove called for section without enabled sorted feature... ".$self->id."/".$self->class);
413 8 ahitrov@rambler.ru }
414
415 return 1;
416 }
417
418
419
420
421 # ----------------------------------------------------------------------------
422 # Метод для построения пути между двумя рубриками... Возвращает
423 # массив идентификаторов секций от рубрики секции до $root_id снизу
424 # вверх. $root_id обязательно должен быть выше по рубрикатору. В результат
425 # включаются обе стороны. Если рубрики равны, то возвращается массив из
426 # одного элемента, если пути нет - то пустой массив.
427 #
428 # Формат вызова:
429 # $section->trace($root_id)
430 # ----------------------------------------------------------------------------
431 sub trace {
432 my ($self, $root_id) = @_;
433 do { $log->error("Метод ->trace() можно вызывать только у объектов, но не классов"); die } unless ref($self);
434
435 do { $log->warning("Вызов метода ->trace() без указания идентификатора секции"); return () }
436 unless (exists($self->{id}) && ($self->{id} > 0));
437 $root_id ||= $ROOT;
438
439 my $id_ = $self->{id};
440 my @SIDS = ($id_);
441 my $sth = $self->keeper->SQL->prepare_cached("select pid from ".$self->class_table->db_table." where id = ?");
442
443 while ($id_ != $root_id)
444 {
445 $sth->execute($id_);
446 ($id_) = $sth->fetchrow_array();
447 if (defined($id_) && ($id_ > 0))
448 {
449 unshift (@SIDS, $id_);
450 } else {
451 # Мы закочили путешествие вверх по рубрикам, а до корня не дошли...
452 $sth->finish;
453 return ();
454 }
455 }
456 $sth->finish;
457 return @SIDS;
458 }
459
460
461 # ----------------------------------------------------------------------------
462 # Предки
463 # Возвращает массив идентификаторов всех предков (родителей на всех уровнях) данной секции
464 # ----------------------------------------------------------------------------
465 sub ancestors
466 {
467 my $self = shift;
468 do { $log->error("Метод ->ancestors() можно вызывать только у объектов, но не классов"); die } unless ref($self);
469
470 my $keeper = $self->keeper;
471 do { $log->error("В объекте секции не определена ссылка на базу данных"); die } unless ref($keeper);
472
473 do { $log->warning("Вызов метода ->ancestors() без указания идентификатора секции"); return () } unless (exists($self->{id}) && ($self->{id} > 0));
474
475 my @ancestors = ();
476 my $sectionid = $self->{id};
477 while ($sectionid)
478 {
479 $sectionid = $keeper->SQL->selectrow_array("select pid from ".$self->class_table->db_table." where id = ?", {}, $sectionid);
480 push @ancestors, $sectionid if defined $sectionid && $sectionid;
481 }
482 return @ancestors;
483 }
484
485 # ----------------------------------------------------------------------------
486 # Потомки
487 # Возвращает массив идентификаторов всех потомков (детей на всех уровнях) данной секции
488 # ----------------------------------------------------------------------------
489 sub descendants
490 {
491 my $self = shift;
492 do { $log->error("Метод ->descendants() можно вызывать только у объектов, но не классов"); die } unless ref($self);
493
494 my $keeper = $self->keeper;
495 do { $log->error("В объекте секции не определена ссылка на базу данных"); die } unless ref($keeper);
496
497 do { $log->warning("Вызов метода ->descendants() без указания идентификатора секции"); return () } unless (exists($self->{id}) && ($self->{id} > 0));
498
499 my @descendants = ();
500 my @ids = ($self->{id});
501 while (scalar @ids)
502 {
503 my $sth = $keeper->SQL->prepare("select id from ".$self->class_table->db_table." where pid in (" . (join ", ", @ids) . ")");
504 $sth->execute;
505 @ids = ();
506 while (my ($id) = $sth->fetchrow_array)
507 {
508 push @ids, $id;
509 }
510 $sth->finish();
511 push @descendants, @ids;
512 last if !$sth->rows;
513 }
514 return @descendants;
515 }
516
517 # -------------------------------------------------------------------------------------------------
518 # Получение деревца...
519 # Параметры:
520 # light => облегченная версия
521 # root => корень дерева (по умолчанию - 1)
522 # -------------------------------------------------------------------------------------------------
523 sub get_tree {
524 my ($self, %opts) = @_;
525 do { $log->warning("Метод ->get_tree() можно вызывать только у объектов, но не классов"); return undef } unless ref($self);
526
527 my $root = $opts{root} || $ROOT;
528
529
530 # ----------------------------------------------------------------------------------------
531 # Выбираем все секции
532 $opts{no_limit} = 1;
533 delete $opts{root};
534 my @sections = $self->keeper->get_sections(%opts);
535
536 my $CACHE = {};
537 foreach my $section (@sections) {
538 if (ref($section)) {
539 $CACHE->{$section->id()} = $section;
540 }
541 }
542
543 for my $id (sort { $CACHE->{$a}->sorder() <=> $CACHE->{$b}->sorder() } (keys(%{ $CACHE }))) {
544 my $pid = $CACHE->{$id}->pid() || '';
545 $CACHE->{$pid}->{childs} = [] if (! exists($CACHE->{$pid}->{childs}));
546 $CACHE->{$id}->{parent} = $CACHE->{$pid};
547 push (@{ $CACHE->{$pid}->{childs} }, $CACHE->{$id} );
548 }
549
550 return $CACHE->{$root};
551 }
552
553 1;
554