File Coverage

blib/lib/Net/Google/Code/Issue.pm
Criterion Covered Total %
statement 136 308 44.1
branch 24 88 27.2
condition 3 30 10.0
subroutine 14 18 77.7
pod 8 8 100.0
total 185 452 40.9


line stmt bran cond sub pod time code
1             package Net::Google::Code::Issue;
2 7     7   185036 use Any::Moose;
  7         160577  
  7         55  
3 7     7   7420 use Params::Validate qw(:all);
  7         42634  
  7         1833  
4             with 'Net::Google::Code::TypicalRoles';
5 7     7   4088 use Net::Google::Code::DateTime;
  7         24  
  7         314  
6 7     7   5928 use Net::Google::Code::Issue::Comment;
  7         26  
  7         348  
7 7     7   65 use Net::Google::Code::Issue::Attachment;
  7         14  
  7         205  
8 7     7   39 use Scalar::Util qw/blessed/;
  7         15  
  7         478  
9              
10 7     7   50 use Net::Google::Code::Issue::Util;
  7         17  
  7         348  
11             extends 'Net::Google::Code::Issue::Base';
12              
13             # set this to true to enable hybrid load, create and update
14             our $USE_HYBRID;
15              
16 7     7   35 use XML::FeedPP;
  7         15  
  7         40376  
17              
18             has 'id' => (
19             isa => 'Int',
20             is => 'rw',
21             );
22              
23             has 'status' => (
24             isa => 'Str',
25             is => 'rw',
26             );
27              
28             has 'owner' => (
29             isa => 'Str',
30             is => 'rw',
31             );
32              
33             has 'cc' => (
34             isa => 'Str',
35             is => 'rw',
36             );
37              
38             has 'summary' => (
39             isa => 'Str',
40             is => 'rw',
41             );
42              
43             has 'reporter' => (
44             isa => 'Str',
45             is => 'rw',
46             );
47              
48             has 'reported' => (
49             isa => 'DateTime',
50             is => 'rw',
51             );
52              
53             has 'merged' => (
54             isa => 'Int',
55             is => 'rw',
56             );
57              
58             has 'stars' => (
59             isa => 'Int',
60             is => 'rw',
61             );
62              
63             has 'closed' => (
64             isa => 'Str',
65             is => 'rw',
66             );
67              
68             has 'description' => (
69             isa => 'Str',
70             is => 'rw',
71             );
72              
73             has 'labels' => (
74             isa => 'ArrayRef',
75             is => 'rw',
76             default => sub { [] },
77             );
78              
79             has 'comments' => (
80             isa => 'ArrayRef[Net::Google::Code::Issue::Comment]',
81             is => 'rw',
82             default => sub { [] },
83             );
84              
85             has 'attachments' => (
86             isa => 'ArrayRef[Net::Google::Code::Issue::Attachment]',
87             is => 'rw',
88             default => sub { [] },
89             );
90              
91             sub load {
92 2     2 1 1244 my $self = shift;
93 2   33     12 my $id = shift || $self->id;
94 2 50       9 die "current object doesn't have id and load() is not passed an id either"
95             unless $id;
96              
97 2 50       8 if ($USE_HYBRID) {
98 0 0 0     0 unless ( $self->{loaded_way}
      0        
99             && $self->{loaded_way} eq 'api'
100             && $id == $self->id )
101             {
102 0         0 my ($issue) = $self->list( id => $id );
103 0         0 %$self = %$issue;
104             }
105 0         0 $self->{loaded_way} = 'hybrid';
106              
107 0         0 $self->load_comments;
108              
109             # here we do scraping to get stuff not can be seen from feeds
110 0         0 my $content =
111             $self->fetch( $self->base_url . "issues/detail?id=" . $id );
112 0         0 return $self->parse_hybrid($content);
113             }
114             else {
115 2         20 my $content =
116             $self->fetch( $self->base_url . "issues/detail?id=" . $id );
117 2         41 $self->id( $id );
118 2         27 $self->{loaded_way} = 'scraping';
119 2         10 return $self->parse($content);
120             }
121             }
122              
123             sub parse {
124 2     2 1 4 my $self = shift;
125 2         5 my $tree = shift;
126              
127 2         8 my $need_delete = not blessed $tree;
128 2 50       26 $tree = $self->html_tree( html => $tree ) unless blessed $tree;
129              
130             # extract summary
131 2         15 my ($summary) = $tree->look_down( class => 'h3' );
132 2         3092 $self->summary( $summary->as_text );
133              
134             # extract reporter, reported and description
135 2         84 my $description = $tree->look_down( class => 'vt issuedescription' );
136 2         1910 my $author_tag = $description->look_down( class => "author" );
137 2         83 $self->reporter( $author_tag->content_array_ref->[1]->as_text );
138 2         63 $self->reported( Net::Google::Code::DateTime->new_from_string($author_tag->look_down( class => 'date' )->attr('title') ));
139              
140              
141 2         22 my $text = $description->find_by_tag_name('pre')->as_text;
142 2         298 $text =~ s/^\s+//;
143 2         54 $text =~ s/\s+$//;
144 2         26 $text =~ s/\r\n/\n/g;
145 2         23 $self->description( $text );
146              
147 2         15 my $att_tag = $description->look_down( class => 'attachments' );
148 2         224 my @attachments;
149 2 100       122 @attachments =
150             Net::Google::Code::Issue::Attachment->parse_attachments($att_tag)
151             if $att_tag;
152 2         27 $self->attachments( \@attachments );
153              
154 2         11 my ($meta) = $tree->look_down( id => 'issuemeta' );
155             {
156              
157             # let's find stars
158 2         3144 my ($header) = $tree->look_down( id => 'issueheader' );
  2         12  
159 2 50 33     2983 if ( $header
160             && $header->as_text =~ /(\d+) \w+ starred this issue/ )
161             {
162             # the \w+ is person or people, I don't know if google will change that word
163             # some time, so just use \w+
164 2         284 my $stars = $1;
165 2         32 $self->stars($stars);
166             }
167             }
168              
169 2         12 my @meta = $meta->find_by_tag_name('tr');
170 2         336 my @labels;
171 2         6 for my $meta (@meta) {
172 9         12 my ( $key, $value );
173 9 100       23 if ( my $k = $meta->find_by_tag_name('th') ) {
174 5         132 my $v = $meta->find_by_tag_name('td');
175 5         156 my $k_content = $k->content_array_ref->[0];
176 5         52 while ( ref $k_content ) {
177 0         0 $k_content = $k_content->content_array_ref->[0];
178             }
179 5         8 $key = $k_content; # $key is like 'Status:#'
180 5         21 $key =~ s/:.$//; # s/:#$// doesn't work, no idea why
181 5         12 $key = lc $key;
182              
183 5 50       11 if ($v) {
184 5         18 $value = $v->as_text;
185 5         115 $value =~ s/^\s+//;
186 5         16 $value =~ s/\s+$//;
187             }
188              
189 5 50       59 if ( $self->can( $key ) ) {
190 5 50 33     17 if ( $key eq 'merged' && $value =~ /issue\s+(\d+)/ ) {
191 0         0 $value = $1;
192             }
193 5         39 $self->$key( $value );
194             }
195             else {
196 0         0 warn "no idea where to keep $key";
197             }
198             }
199             else {
200 4         170 my $href = $meta->look_down( class => 'label' )->attr('href');
201 4 50       224 if ( $href =~ /list\?q=label:(.+)/ ) {
202 4         14 push @labels, $1;
203             }
204             }
205             }
206 2         20 $self->labels( \@labels );
207              
208             # extract comments
209 2         9 my @comments_tag = $tree->look_down( class => 'vt issuecomment' );
210 2         3237 my @comments;
211 2         6 for my $tag (@comments_tag) {
212 9 100       32 next unless $tag->look_down( class => 'author' );
213 7         405 my $comment =
214             Net::Google::Code::Issue::Comment->new( project => $self->project );
215 7         78 $comment->parse($tag);
216 7         15 push @comments, $comment;
217             }
218              
219 2         178 my $initial_comment = Net::Google::Code::Issue::Comment->new(
220             project => $self->project,
221             sequence => 0,
222             date => $self->reported,
223             author => $self->reporter,
224             content => $self->description,
225             attachments => $self->attachments,
226             );
227              
228 2         8 my @initial_labels = @{$self->labels};
  2         15  
229 2         6 my %meta = map { $_ => 1 } qw/summary status cc owner/;
  8         26  
230 2         8 for my $c ( reverse @comments ) {
231 7         22 my $updates = $c->updates;
232 7         20 for ( keys %meta ) {
233             # once these changes, we can't know the inital value
234 23 100       64 delete $meta{$_} if exists $updates->{$_};
235             }
236 7 50       27 if ( $updates->{labels} ) {
237 0         0 my @labels = @{$updates->{labels}};
  0         0  
238 0         0 for my $label (@labels) {
239 0 0       0 if ( $label =~ /^-(.*)$/ ) {
240 0         0 unshift @initial_labels, $1;
241             }
242             else {
243 0         0 @initial_labels = grep { $_ ne $label } @initial_labels;
  0         0  
244             }
245             }
246             }
247             }
248              
249 2         31 $initial_comment->updates->{labels} = \@initial_labels;
250 2         9 for ( keys %meta ) {
251 6         38 $initial_comment->updates->{$_} = $self->$_;
252             }
253              
254 2         8 unshift @comments, $initial_comment;
255              
256 2         39 $self->comments( \@comments );
257 2 50       20 $tree->delete if $need_delete;
258 2         7907 return 1;
259             }
260              
261             sub load_comments {
262 0     0 1 0 my $self = shift;
263 0         0 require Net::Google::Code::Issue::Comment;
264 0         0 my $comment = Net::Google::Code::Issue::Comment->new(
265             issue_id => $self->id,
266 0         0 map { $_ => $self->$_ }
267 0         0 grep { $self->$_ } qw/project email password token/
268             );
269              
270             # $comment is for initial comment we will work out
271 0         0 $self->comments( [ $comment, $comment->list ] );
272             }
273              
274             sub parse_hybrid {
275 0     0 1 0 my $self = shift;
276 0         0 my $tree = shift;
277 0         0 my $need_delete = not blessed $tree;
278 0 0       0 $tree = $self->html_tree( html => $tree ) unless blessed $tree;
279              
280 0         0 my $description = $tree->look_down( class => 'vt issuedescription' );
281 0         0 my $att_tag = $description->look_down( class => 'attachments' );
282 0         0 my @attachments;
283 0 0       0 @attachments =
284             Net::Google::Code::Issue::Attachment->parse_attachments($att_tag)
285             if $att_tag;
286 0         0 $self->attachments( \@attachments );
287              
288 0         0 my ($meta) = $tree->look_down( id => 'issuemeta' );
289 0         0 my @meta = $meta->find_by_tag_name('tr');
290 0         0 my @labels;
291 0         0 for my $meta (@meta) {
292              
293 0         0 my ( $key, $value );
294 0 0       0 if ( my $k = $meta->find_by_tag_name('th') ) {
295 0         0 my $v = $meta->find_by_tag_name('td');
296 0         0 my $k_content = $k->content_array_ref->[0];
297 0         0 while ( ref $k_content ) {
298 0         0 $k_content = $k_content->content_array_ref->[0];
299             }
300 0         0 $key = $k_content; # $key is like 'Status:#'
301 0         0 $key =~ s/:.$//; # s/:#$// doesn't work, no idea why
302 0         0 $key = lc $key;
303              
304 0 0       0 if ($v) {
305 0         0 $value = $v->as_text;
306 0         0 $value =~ s/^\s+//;
307 0         0 $value =~ s/\s+$//;
308             }
309 0 0       0 if ( $self->can( $key ) ) {
310 0 0 0     0 if ( $key eq 'merged' && $value =~ /issue\s+(\d+)/ ) {
311 0         0 $value = $1;
312             }
313 0         0 $self->$key( $value );
314             }
315             else {
316 0         0 warn "no idea where to keep $key";
317             }
318             }
319             }
320              
321             # extract comments
322 0         0 my @comments_tag = $tree->look_down( class => 'vt issuecomment' );
323 0         0 ( undef, my @comments ) = @{$self->comments};
  0         0  
324 0         0 my $number = 1; # 0 is for initial comment
325 0         0 for my $tag (@comments_tag) {
326 0 0       0 next unless $tag->look_down( class => 'author' );
327 0         0 my $comment = $self->comments->[$number++];
328 0         0 $comment->parse_hybrid($tag);
329             }
330              
331 0         0 my $initial_comment = Net::Google::Code::Issue::Comment->new(
332             sequence => 0,
333             date => $self->reported,
334             author => $self->reporter,
335             content => $self->description,
336             attachments => $self->attachments,
337             issue_id => $self->id,
338 0         0 map { $_ => $self->$_ }
339 0         0 grep { $self->$_ } qw/project email password token/
340             );
341              
342 0         0 my @initial_labels = @{$self->labels};
  0         0  
343 0         0 my %meta = map { $_ => 1 } qw/summary status cc owner/;
  0         0  
344 0         0 for my $c ( reverse @comments ) {
345 0         0 my $updates = $c->updates;
346 0         0 for ( keys %meta ) {
347             # once these changes, we can't know the inital value
348 0 0       0 delete $meta{$_} if exists $updates->{$_};
349             }
350 0 0       0 if ( $updates->{labels} ) {
351 0         0 my @labels = @{$updates->{labels}};
  0         0  
352 0         0 for my $label (@labels) {
353 0 0       0 if ( $label =~ /^-(.*)$/ ) {
354 0         0 unshift @initial_labels, $1;
355             }
356             else {
357 0         0 @initial_labels = grep { $_ ne $label } @initial_labels;
  0         0  
358             }
359             }
360             }
361             }
362              
363 0         0 $initial_comment->updates->{labels} = \@initial_labels;
364 0         0 for ( keys %meta ) {
365 0         0 $initial_comment->updates->{$_} = $self->$_;
366             }
367 0         0 $self->comments->[0] = $initial_comment;
368 0 0       0 $tree->delete if $need_delete;
369 0         0 return 1;
370             }
371              
372             sub _load_from_xml {
373 22     22   30 my $self = shift;
374 22         105 my $ref =
375             Net::Google::Code::Issue::Util->translate_from_xml( shift,
376             type => 'issue' );
377              
378 22         100 for my $k ( keys %$ref ) {
379 332 100       984 if ( $self->can($k) ) {
380 222         495 $self->{$k} = $ref->{$k};
381             }
382             }
383 22         91 return $self;
384             }
385              
386             sub create {
387 0     0 1 0 my $self = shift;
388 0         0 my %args = validate(
389             @_,
390             {
391             labels => { type => ARRAYREF, optional => 1 },
392             files => { type => ARRAYREF, optional => 1 },
393 0         0 map { $_ => { type => SCALAR, optional => 1 } }
394             qw/comment summary status owner cc/,
395             }
396             );
397              
398 0 0 0     0 if ( $args{files} || !$USE_HYBRID) {
399 0         0 $self->sign_in;
400 0         0 $self->fetch( $self->base_url . 'issues/entry' );
401              
402 0 0       0 if ( $args{files} ) {
403              
404             # hack hack hack
405             # manually add file fields since we don't have them in page.
406 0         0 my $html = $self->mech->content;
407 0         0 for ( 1 .. @{ $args{files} } ) {
  0         0  
408 0         0 $html =~
409             s{(?<=id="attachmentareadeventry">)}{};
410             }
411 0         0 $self->mech->update_html($html);
412             }
413              
414 0         0 $self->mech->form_with_fields( 'comment', 'summary' );
415              
416             # leave labels alone unless there're labels.
417 0 0       0 $self->mech->field( 'label', $args{labels} ) if $args{labels};
418              
419 0 0       0 if ( $args{files} ) {
420 0         0 for ( my $i = 0 ; $i < scalar @{ $args{files} } ; $i++ ) {
  0         0  
421 0         0 $self->mech->field( 'file' . ( $i + 1 ), $args{files}[$i] );
422             }
423             }
424              
425             $self->mech->submit_form(
426 0         0 fields => {
427 0         0 map { $_ => $args{$_} }
428 0         0 grep { exists $args{$_} } qw/comment summary status owner cc/
429             }
430             );
431              
432 0         0 my ( $contains, $id ) = $self->html_tree_contains(
433             html => $self->mech->content,
434             look_down => [ class => 'notice' ],
435             as_text => qr/Issue\s+(\d+)/i,
436             );
437              
438 0 0       0 if ($contains) {
439 0         0 $self->load($id);
440 0         0 return $id;
441             }
442             else {
443 0         0 warn 'create issue failed';
444 0         0 return;
445             }
446             }
447             else {
448              
449             # we can use google's official api here
450 0         0 my $author = $self->email;
451 0         0 $author =~ s/@.*//;
452 0         0 my %args = ( author => $author, @_ );
453              
454 0         0 my $xml =
455             Net::Google::Code::Issue::Util->translate_to_xml( \%args,
456             type => 'create' );
457 0         0 my $ua = $self->ua;
458              
459 0         0 my $url = $self->feeds_issues_url . '/full';
460 0         0 my $request = HTTP::Request->new( 'POST', $url, undef, $xml );
461 0         0 my $res = $ua->request($request);
462 0 0       0 if ( $res->is_success ) {
463 0         0 my $content = $res->content;
464              
465             # let's fake wrap the entry with
466 0         0 $content =~ s!
467 0         0 $content =~ s{$}{};
468 0         0 my $feed = XML::FeedPP->new($content);
469 0         0 my ($item) = $feed->get_item;
470 0         0 $self->_load_from_xml($item);
471 0         0 $self->load( $self->id );
472 0         0 return 1;
473             }
474             else {
475 0         0 die "try to POST $url failed: "
476             . $res->status_line . "\n"
477             . $res->content;
478             }
479              
480             }
481             }
482              
483             sub update {
484 0     0 1 0 my $self = shift;
485 0         0 my %args = validate(
486             @_,
487             {
488             labels => { type => ARRAYREF, optional => 1 },
489             files => { type => ARRAYREF, optional => 1 },
490 0         0 map { $_ => { type => SCALAR, optional => 1 } }
491             qw/comment summary status owner merge_into cc blocked_on/,
492             }
493             );
494              
495 0 0 0     0 if ( $args{files}
      0        
      0        
496             || $args{merge_into}
497             || $args{blocked_on}
498             || !$USE_HYBRID )
499             {
500              
501 0         0 $self->sign_in;
502 0         0 $self->fetch( $self->base_url . 'issues/detail?id=' . $self->id );
503              
504 0 0       0 if ( $args{files} ) {
505              
506             # hack hack hack
507             # manually add file fields since we don't have them in page.
508 0         0 my $html = $self->mech->content;
509 0         0 for ( 1 .. @{ $args{files} } ) {
  0         0  
510 0         0 $html =~
511             s{(?<=id="attachmentarea">)}{};
512             }
513 0         0 $self->mech->update_html($html);
514             }
515              
516 0         0 $self->mech->form_with_fields( 'comment', 'summary' );
517              
518             # leave labels alone unless there're labels.
519 0 0       0 $self->mech->field( 'label', $args{labels} ) if $args{labels};
520 0 0       0 if ( $args{files} ) {
521 0         0 for ( my $i = 0 ; $i < scalar @{ $args{files} } ; $i++ ) {
  0         0  
522 0         0 $self->mech->field( 'file' . ( $i + 1 ), $args{files}[$i] );
523             }
524             }
525              
526             $self->mech->submit_form(
527 0         0 fields => {
528 0         0 map { $_ => $args{$_} }
529 0         0 grep { exists $args{$_} }
530             qw/comment summary status owner merge_into cc blocked_on/
531             }
532             );
533              
534 0 0       0 if (
535             $self->html_tree_contains(
536             html => $self->mech->content,
537             look_down => [ class => 'notice' ],
538             as_text => qr/has been updated/,
539             )
540             )
541             {
542 0         0 $self->load( $self->id ); # maybe this is too much?
543 0         0 return 1;
544             }
545             else {
546 0         0 warn 'update failed';
547 0         0 return;
548             }
549             }
550             else {
551 0         0 my $author = $self->email;
552 0         0 $author =~ s/@.*//;
553 0         0 my %args = (
554             author => $author,
555             (
556 0         0 map { $_ => $self->$_ } qw/title content status owner cc labels/
557             ),
558             @_,
559             );
560              
561 0         0 my $xml =
562             Net::Google::Code::Issue::Util->translate_to_xml( \%args,
563             type => 'update' );
564 0         0 my $ua = $self->ua;
565 0         0 my $url = $self->feeds_issues_url . '/' . $self->id . '/comments/full';
566              
567 0         0 my $request = HTTP::Request->new( 'POST', $url, undef, $xml );
568 0         0 my $res = $ua->request($request);
569 0 0       0 if ( $res->is_success ) {
570 0         0 $self->load( $self->id ); # let's reload
571 0         0 return 1;
572             }
573             else {
574 0         0 die "try to POST $url failed: "
575             . $res->status_line . "\n"
576             . $res->content;
577             }
578              
579             }
580             }
581              
582             sub updated {
583 1     1 1 20261 my $self = shift;
584 1         6 my $last_comment = $self->comments->[-1];
585 1 50       28 return $last_comment ? $last_comment->date : undef;
586             }
587              
588             sub list {
589 2     2 1 19 my $self = shift;
590 2         120 validate(
591             @_,
592             {
593             q => { optional => 1, type => SCALAR },
594             can => { optional => 1, type => SCALAR },
595             author => { optional => 1, type => SCALAR },
596             id => { optional => 1, type => SCALAR },
597             label => { optional => 1, type => SCALAR },
598             max_results => { optional => 1, type => SCALAR },
599             owner => { optional => 1, type => SCALAR },
600             published_min => { optional => 1, type => SCALAR },
601             published_max => { optional => 1, type => SCALAR },
602             updated_min => { optional => 1, type => SCALAR },
603             updated_max => { optional => 1, type => SCALAR },
604             start_index => { optional => 1, type => SCALAR },
605             }
606             );
607              
608 2         22 my %args = @_;
609 2         19 my $url = $self->feeds_issues_url . '/full?';
610 2         19 require URI::Escape;
611 2         8 for my $k ( keys %args ) {
612 1 50       6 next unless $args{$k};
613 1         2 my $v = $args{$k};
614 1         3 $k =~ s/_/-/g;
615 1         7 $url .= "$k=" . URI::Escape::uri_escape($v) . '&';
616             }
617              
618 2         58 my $ua = $self->ua;
619 2         11 my $res = $ua->get($url);
620 2 50       157 if ( $res->is_success ) {
621 2         113 my $feed = XML::FeedPP->new($res->content);
622 2         58594 my @items = $feed->get_item;
623 22         495 my @list = map {
624 2         35 my $t = Net::Google::Code::Issue->new(
625             loaded_way => 'api',
626 88         284 map { $_ => $self->$_ }
627 22         50 grep { $self->$_ } qw/project email password token/
628             );
629 22         88 $t->_load_from_xml($_);
630             } @items;
631 2 50       233 return wantarray ? @list : \@list;
632             }
633             else {
634 0           die "try to get $url failed: "
635             . $res->status_line . "\n"
636             . $res->content;
637             }
638             }
639              
640 7     7   77 no Any::Moose;
  7         14  
  7         70  
641             __PACKAGE__->meta->make_immutable;
642              
643             1;
644              
645             __END__