File Coverage

blib/lib/MojoMojo/Schema/ResultSet/Page.pm
Criterion Covered Total %
statement 132 139 94.9
branch 34 40 85.0
condition 11 17 64.7
subroutine 12 13 92.3
pod 9 9 100.0
total 198 218 90.8


line stmt bran cond sub pod time code
1             package MojoMojo::Schema::ResultSet::Page;
2              
3 40     40   58777 use strict;
  40         99  
  40         1005  
4 40     40   185 use warnings;
  40         91  
  40         1012  
5 40     40   188 use parent qw/MojoMojo::Schema::Base::ResultSet/;
  40         89  
  40         203  
6 40     40   5857 use URI::Escape ();
  40         12337  
  40         53653  
7              
8             =head1 NAME
9              
10             MojoMojo::Schema::ResultSet::Page - resultset methods on pages
11              
12             =head1 METHODS
13              
14             =head2 path_pages
15              
16             ( $path_pages, $proto_pages ) = __PACKAGE__->path_pages( $path, $id )
17              
18             Accepts a path in URL/Unix directory format, e.g. "/page1/page2".
19             Paths are assumed to be absolute, so a leading slash (/) is not
20             required.
21              
22             Returns a reference to an array of any pages that exist in the path,
23             starting with "/", and an additional reference to an array of "proto page"
24             hashes for any pages at the end of the path that do not exist. All paths
25             include the root (/), which must exist, so a path of at least one element
26             will always be returned.
27              
28             The "proto page" hash keys are shown in the example below, where we assume
29             that C</blog> exists and C</blog/My_New_Entry> doesn't exist yet:
30              
31             {
32             depth => 2,
33             name => "my_new_entry",
34             name_orig => "My_New_Entry",
35             path => "/blog/My_New_Entry",
36             },
37              
38             =cut
39              
40             sub path_pages {
41 359     359 1 415508 my ( $self, $path, $id ) = @_;
42              
43             # avoid recursive path resolution, if possible:
44 359         1228 my @path_pages;
45 359 100       1775 if ( $path eq '/' ) {
    50          
46 173         1224 @path_pages = $self->search( { lft => 1 } )->all;
47             }
48             elsif ($id) {
49              
50             # this only works if depth is at least 1
51 0         0 @path_pages = $self->path_pages_by_id($id);
52             }
53 359 100       512949 return ( \@path_pages, [] ) if ( @path_pages > 0 );
54              
55 186         876 my @proto_pages = $self->parse_path($path);
56              
57 186         916 my $depth = @proto_pages - 1; # depth starts at 0
58              
59 186         407 my @depths;
60 186         977 for my $proto (@proto_pages) {
61             push @depths, -and => [
62             depth => $proto->{depth},
63             name => $proto->{name},
64 389         3110 ];
65              
66             }
67              
68 186         1358 my @pages = $self->search( { -or => [@depths] }, {} );
69              
70 186         837600 my @query_pages;
71 186         878 for (@pages) {
72 298   50     8117 $query_pages[ $_->depth ] ||= [];
73 298         6182 push @{ $query_pages[ $_->depth ] }, $_;
  298         5060  
74             }
75              
76 186         2892 my $resolved = $self->resolve_path(
77             path_pages => \@path_pages,
78             proto_pages => \@proto_pages,
79             query_pages => \@query_pages,
80             current_depth => 0,
81             final_depth => $depth,
82             );
83              
84             # If there are any proto pages, put the original
85             # page names back into the paths, so they will
86             # be preserved upon page creation:
87 186 50       749 if (@path_pages) {
88 186         570 my $proto_path = $path_pages[-1]->{path};
89 186         590 for (@proto_pages) {
90 91 100       535 ( $proto_path =~ /\/$/ ) || ( $proto_path .= '/' );
91 91         305 $proto_path .= $_->{name_orig};
92 91         290 $_->{path} = $proto_path;
93             }
94             }
95 186         1643 return ( \@path_pages, \@proto_pages );
96             } # end sub get_path
97              
98             =head2 path_pages_by_id
99              
100             @path_pages = __PACKAGE__->path_pages_by_id( $id )
101              
102             Returns all the pages in the path to a page, given that page's id.
103              
104             =cut
105              
106             sub path_pages_by_id {
107 0     0 1 0 my ( $self, $id ) = @_;
108 0         0 return $self->search(
109             {
110             'start_page.lft' => 1,
111             'end_page.id' => $id,
112             'me.lft' => \'BETWEEN start_page.lft AND start_page.rgt',
113             'end_page.lft' => \'BETWEEN me.lft AND me.rgt',
114             },
115             {
116             from => "page AS start_page, page AS me, page AS end_page ",
117             order_by => 'me.lft'
118             }
119             );
120             }
121              
122             =head2 parse_path
123              
124             @proto_pages = __PACKAGE__->parse_path( $path )
125              
126             Create prototype page objects for each level in a given path.
127              
128             =cut
129              
130             sub parse_path {
131 186     186 1 637 my ( $self, $path ) = @_;
132              
133             # Remove leading and trailing slashes to make
134             # split happy. We'll add the root (/) back later...
135 186         1050 $path =~ s/^[\/]+//;
136 186         597 $path =~ s/[\/]+$//;
137              
138 186         828 my @proto_pages = map { { name_orig => $_ } } ( split /\/+/, $path );
  203         1038  
139 186 50 66     1096 if ( @proto_pages == 0 && $path =~ /\S/ ) {
140 0         0 @proto_pages = ($path);
141             }
142              
143 186         476 my $depth = 1;
144 186         458 my $page_path = '';
145 186         539 for (@proto_pages) {
146 203         898 ( $_->{name_orig}, $_->{name} ) = $self->normalize_name( $_->{name_orig} );
147 203         15112 $page_path .= '/' . $_->{name};
148 203         626 $_->{path} = $page_path;
149 203         586 $_->{depth} = $depth;
150 203         523 $depth++;
151             }
152              
153             # assume that all paths are absolute:
154 186         964 unshift @proto_pages, { name => '/', name_orig => '/', path => '/', depth => 0 };
155              
156 186         1130 return @proto_pages;
157              
158             } # end sub parse_path
159              
160             =head2 normalize_name
161              
162             ($name_orig, $name) = __PACKAGE__->normalize_name( $name_orig )
163              
164             Strip superfluous spaces, convert the rest to _, then lowercase the result.
165              
166             =cut
167              
168             sub normalize_name {
169 209     209 1 2791 my ( $self, $name_orig ) = @_;
170              
171 209         640 $name_orig =~ s/^\s+//;
172 209         678 $name_orig =~ s/\s+$//;
173 209         525 $name_orig =~ s/\s+/ /g;
174              
175 209         478 my $name = $name_orig;
176 209         473 $name =~ s/\s+/_/g;
177 209         578 $name = lc($name);
178             return (
179 209         1126 Encode::decode_utf8(URI::Escape::uri_unescape(Encode::encode_utf8($name_orig))),
180             Encode::decode_utf8(URI::Escape::uri_unescape(Encode::encode_utf8($name)))
181             );
182             }
183              
184             =head2 resolve_path
185              
186             $an_resolve = __PACKAGE__->resolve_path( %args )
187              
188             Takes the following args:
189              
190             =over 4
191              
192             =item path_pages
193              
194             =item proto_pages
195              
196             =item query_pages
197              
198             =item current_depth
199              
200             =item final_depth
201              
202             =back
203              
204             Returns true if the path can be resolved, or false otherwise.
205              
206             =cut
207              
208             sub resolve_path {
209 372     372 1 1773 my ( $class, %args ) = @_;
210              
211             my ( $path_pages, $proto_pages, $query_pages, $current_depth, $final_depth ) =
212 372         1291 @args{ qw/ path_pages proto_pages query_pages current_depth final_depth/ };
213              
214 372         742 while ( my $page = shift @{ $query_pages->[$current_depth] } ) {
  446         1604  
215 298 100       968 unless ( $current_depth == 0 ) {
216 112         327 my $parent = $path_pages->[ $current_depth - 1 ];
217 112 50 33     3202 next unless $page->parent && $page->parent->id == $parent->id;
218             }
219 298         839951 my $proto_page = shift @{$proto_pages};
  298         769  
220 298         1906 $page->path( $proto_page->{path} );
221 298         646 push @{$path_pages}, $page;
  298         765  
222             return 1
223             if (
224             $current_depth == $final_depth
225             ||
226              
227             # must pre-icrement for this to work when current_depth == 0
228 298 100 66     3253 ( ++$args{current_depth} && $class->resolve_path(%args) )
      66        
229             );
230             }
231 148         719 return 0;
232              
233             } # end sub resolve_path
234              
235             =head2 set_paths
236              
237             @pages = __PACKAGE__->set_paths( @pages )
238              
239             Sets the path for multiple pages, either a subtree or a group of
240             non-adjacent pages.
241              
242             =cut
243              
244             sub set_paths {
245 57     57 1 43098 my ( $class, @pages ) = @_;
246             return @pages
247 57 100 100     1126 if ( scalar @pages == 1 )
248             && $pages[0]->depth == 0;
249 53 100       793 return unless ( scalar @pages );
250 51         155 my %pages = map { $_->id => $_ } @pages;
  85         1912  
251              
252             # Preserve the original sort order, because the pages
253             # passed in may have been sorted differently than we
254             # need them sorted to set paths:
255 51         882 my @lft_sorted_pages = sort { $a->lft <=> $b->lft } @pages;
  52         1836  
256              
257             # In some cases, e.g. retrieving descendants, we
258             # may not have passed in the root of the subtree:
259 51 100       1648 unless ( $lft_sorted_pages[0]->name eq '/' ) {
260 33         1039 my $parent = $lft_sorted_pages[0]->parent;
261 33         198633 $pages{ $parent->id } = $parent;
262             }
263              
264             # Sorting by the rgt column ensures that we always set
265             # paths for parents before their children, allowing us
266             # to avoid recursion.
267 51         921 for (@lft_sorted_pages) {
268 85 100       1479 if ( $_->name eq '/' ) {
269 18         306 $_->path('/');
270 18         47 next;
271             }
272 67 100       1881 if ( $_->depth == 1 ) {
273 66         1781 $_->path( '/' . $_->name );
274 66         199 next;
275             }
276 1         25 my $parent = $pages{ $_->parent->id };
277 1 50       6229 if ( ref $parent ) {
278 1         4 $_->path( $parent->path . '/' . $_->name );
279             }
280              
281             # unless all pages were adjacent, i.e. a whole subtree,
282             # we still may not have the parent:
283             else {
284 0         0 my @path_pages = $class->path_pages_by_id( $_->id );
285              
286             # store these in case they're parents of other pages
287 0         0 for my $path_page (@path_pages) {
288 0         0 $pages{ $path_page->id } = $path_page;
289             }
290              
291             # don't know if this is necessary, but just in case
292             #my $current_page = pop @path_pages;
293             #$_->path( $current_page->path );
294             }
295             }
296 51         389 return @pages;
297              
298             } # end sub set_paths
299              
300              
301             =head2 create_path_pages
302              
303             $path_pages = __PACKAGE__->create_path_pages( %args )
304              
305             Find or creates a list of path_pages. Returns a reference to an array
306             of path_pages.
307              
308             =cut
309              
310             sub create_path_pages {
311 7     7 1 9287 my ( $self, %args ) = @_;
312 7         36 my ( $path_pages, $proto_pages, $creator ) = @args{qw/path_pages proto_pages creator/};
313              
314             # find the deepest existing page in the path, and save
315             # some of its data for later use
316 7         25 my $parent = $path_pages->[ @$path_pages - 1 ];
317 7         142 my %original_ancestor = ( id => $parent->id, rgt => $parent->rgt );
318              
319             # open a gap in the nested set numbers to accommodate the new pages
320 7         344 $parent = $self->open_gap( $parent, scalar @$proto_pages );
321              
322 7         88 my @version_columns = $self->related_resultset('page_version')->result_source->columns;
323              
324             # create all missing pages in the path
325 7         14303 for my $proto_page (@$proto_pages) {
326              
327             # since SQLite doesn't support sequences, just cheat
328             # for now and get the next id by creating a page record
329 9         344 my $page = $self->create( { parent => $parent->id, content_version => undef } );
330 9         64154 my %version_data = map { $_ => $proto_page->{$_} } @version_columns;
  135         431  
331              
332 9 50       409 @version_data{qw/page version parent parent_version creator status release_date/} = (
333             $page->id,
334             1, # FIXME: the version field remains '1' for all pages in a well-edited wiki
335             $page->parent->id,
336             # FIXME: the parent_version field remains '1' for all pages in a well-edited wiki
337             ( $page->parent ? $page->parent->version : undef ), # the '/' page doesn't have a parent
338             $creator,
339             'released',
340             DateTime->now,
341             );
342              
343 9         77912 my $page_version = $self->related_resultset('page_version')->create( \%version_data );
344             # copy $page columns form $page_version
345 9         77634 for ( $page->columns ) {
346 81 100       12417 next if $_ eq 'id'; # page already exists
347 72 100       227 next if $_ eq 'content_version'; # no content yet
348 63 100       606 next unless $page_version->can($_);
349 45         1035 $page->$_( $page_version->$_ );
350             }
351              
352             # set the nested set columns:
353             ## we always create the first page as a right child,
354             ## so if this is the first new page, its left number
355             ## will be the same as the parent's old right number
356             $page->lft(
357             $parent->id == $original_ancestor{id}
358             ? $original_ancestor{rgt}
359 9 100       228 : $parent->lft + 1
360             );
361 9         1584 $page->rgt( $parent->rgt - 1 );
362 9         1403 $page->update;
363 9         68907 push @$path_pages, $page;
364 9         53 $parent = $page;
365             }
366 7         532 return $path_pages;
367              
368             } # end sub create_path_pages
369              
370             =head2 open_gap
371              
372             $parent = __PACKAGE__->open_gap( $parent, $new_page_count )
373              
374             Opens a gap in the nested set numbers to allow the inserting
375             of new pages into the tree. Since nested sets number each node
376             twice, the size of the gap is always twice the number of new
377             pages. Also, since nested sets number the nodes from left to
378             right, we determine what nodes to re-number according to the
379             C<rgt> column of the parent of the top-most new node.
380              
381             Returns a new parent object that is updated with the new C<lft>
382             C<rgt> nested set numbers.
383              
384             =cut
385              
386             sub open_gap {
387 7     7 1 37 my ( $self, $parent, $new_page_count ) = @_;
388 7         153 my ( $gap_increment, $parent_rgt, $parent_id ) =
389             ( $new_page_count * 2, $parent->rgt, $parent->id );
390 7         259 $self->result_source->schema->storage->dbh->do(
391             qq{ UPDATE page
392             SET rgt = rgt + ?, lft = CASE
393             WHEN lft > ? THEN lft + ?
394             ELSE lft
395             END
396             WHERE rgt >= ? }, undef,
397             $gap_increment, $parent_rgt, $gap_increment, $parent_rgt
398             );
399              
400             # get the new nested set numbers for the parent
401 7         46305 $parent = $self->find($parent_id);
402 7         27461 return $parent;
403             }
404              
405             # XXX: Update index_page (Model::Search)
406              
407             =head2 create_page
408              
409             Create a new page in the wiki.
410              
411             =cut
412              
413             sub create_page {
414 1     1 1 705 my ($self,$url, $body, $person) = @_;
415              
416 1         7 my ($path_pages, $proto_pages) = $self->path_pages($url);
417              
418 1         33 $path_pages = $self->create_path_pages(
419             path_pages => $path_pages,
420             proto_pages => $proto_pages,
421             creator => $person->id,
422             );
423              
424 1         5 my $page = $path_pages->[ @$path_pages - 1 ];
425              
426 1         5 my %content;
427 1         66 $content{creator} = $person->id;
428 1         42 $content{body} = $body;
429              
430              
431 1         19 $page->update_content(%content);
432             #$c->model('Search')->index_page($page);
433 1         2376 $self->set_paths($page);
434             }
435              
436             1;