1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
|
use strict;
use warnings;
package Net::Trac::TicketHistoryEntry;
use Any::Moose;
use Net::Trac::TicketPropChange;
use DateTime;
use HTTP::Date;
use URI::Escape qw(uri_escape);
=head1 NAME
Net::Trac::TicketHistoryEntry - A single history entry for a Trac ticket
=head1 DESCRIPTION
This class represents a single item in a Trac ticket history.
=head1 ACCESSORS
=head2 connection
Returns a L<Net::Trac::Connection>.
=head2 author
=head2 date
Returns a L<DateTime> object.
=head2 category
=head2 content
=head2 prop_changes
Returns a hashref (property names as the keys) of
L<Net::Trac::TicketPropChange>s associated with this history entry.
=head2 attachment
if there's attachment, return it, else, return undef
=head2 ticket
A weak reference to the ticket object for this ticket history entry
=head2 is_create
A boolean. Returns true if this is the transaction which created the ticket
=cut
has connection => (
isa => 'Net::Trac::Connection',
is => 'ro'
);
has prop_changes => ( isa => 'HashRef', is => 'rw' );
has is_create => ( isa => 'Bool', is => 'rw', default => 0 );
has author => ( isa => 'Str', is => 'rw' );
has date => ( isa => 'DateTime', is => 'rw' );
has category => ( isa => 'Str', is => 'rw' );
has content => ( isa => 'Str', is => 'rw' );
has attachment => ( isa => 'Net::Trac::TicketAttachment', is => 'rw' );
has ticket => ( isa => 'Net::Trac::Ticket', is => 'rw', weak_ref => 1 );
=head1 METHODS
=head2 parse_feed_entry
Takes a feed entry from a ticket history feed and parses it to fill
out the fields of this class.
=cut
sub parse_feed_entry {
my $self = shift;
my $e = shift;
# We use a reference to a copy of ticket state as it was after this feed
# entry to interpret what "x added, y removed" meant for absolute values
# of keywords
my $ticket_state = shift;
if ( $e =~ m|<dc:creator>(.*?)</dc:creator>|is ) {
my $author = $1;
$self->author($author);
}
if ( $e =~ m|<pubDate>(.*?)</pubDate>|is ) {
my $date = $1;
$self->date( DateTime->from_epoch( epoch => str2time($date) ) );
}
if ( $e =~ m|<category>(.*?)</category>|is ) {
my $c = $1;
$self->category($c);
}
if ( $e =~ m|<description>\s*(.*?)\s*</description>|is ) {
my $desc = $1;
if ( $desc =~ s|^\s*?<ul>(.*?)</ul>||is ) {
my $props = $1;
$self->prop_changes( $self->_parse_props( $props, $ticket_state ) );
}
$desc =~ s/>/>/gi;
$desc =~ s/</</gi;
$desc =~ s/&/&/gi;
$self->content($desc);
}
}
sub _parse_props {
my $self = shift;
my $raw = shift || '';
my $ticket_state = shift;
$raw =~ s/>/>/gi;
$raw =~ s/</</gi;
$raw =~ s/&/&/gi;
# throw out the wrapping <li>
$raw =~ s|^\s*?<li>(.*)</li>\s*?$|$1|is;
my @prop_lines = split( m#</li>\s*<li>#s, $raw );
my $props = {};
foreach my $line (@prop_lines) {
my ( $prop, $old, $new );
if ( $line =~ m{<strong>attachment</strong>} ) {
my ($name) = $line =~ m!<em>(.*?)</em>!;
my $content =
$self->connection->_fetch( "/attachment/ticket/"
. $self->ticket->id . '/'
. uri_escape($name) )
or next;
if ( $content =~ m{<div id="content" class="attachment">(.+?)</div>}is ) {
my $frag = $1;
my $att = Net::Trac::TicketAttachment->new(
connection => $self->connection,
ticket => $self->ticket->id,
filename => $name,
);
$att->_parse_html_chunk($frag);
$self->attachment($att);
}
next;
}
if ( $line =~ m{<strong>description</strong>} ) {
# We can't parse trac's crazy "go read a diff on a webpage handling
# of descriptions
next;
}
if ( $line =~ m{<strong>(keywords|cc)</strong>(.*)$}is ) {
my $value_changes = $2;
$prop = $1;
my ( @added, @removed );
if ( $value_changes =~ m{^\s*<em>(.*?)</em> added}is ) {
my $added = $1;
@added = split( m{</em>\s*<em>}is, $added );
}
if ( $value_changes =~ m{(?:^|added;)\s*<em>(.*)</em> removed}is ) {
my $removed = $1;
@removed = split( m{</em>\s*?<em>}is, $removed );
}
my @before = ();
my @after = grep defined && length, split( /\s+/, $ticket_state->{keywords} );
for my $value (@after) {
next if grep { $_ eq $value } @added;
push @before, $value;
}
$old = join( ' ', sort ( @before, @removed ) );
$new = join( ' ', sort (@after) );
$ticket_state->{$prop} = $old;
} elsif ( $line =~ m{<strong>(.*?)</strong>\s+changed\s+from\s+<em>(.*?)</em>\s+to\s+<em>(.*?)</em>}is ) {
$prop = $1;
$old = $2;
$new = $3;
} elsif ( $line =~ m{<strong>(.*?)</strong>\s+set\s+to\s+<em>(.*?)</em>}is ) {
$prop = $1;
$old = '';
$new = $2;
} elsif ( $line =~ m{<strong>(.*?)</strong>\s+<em>(.*?)</em>\s+deleted}is ) {
$prop = $1;
$old = $2;
$new = '';
} elsif ( $line =~ m{<strong>(.*?)</strong>\s+deleted}is ) {
$prop = $1;
$new = '';
} else {
warn "could not parse " . $line;
}
if ($prop) {
my $pc = Net::Trac::TicketPropChange->new(
property => $prop,
new_value => $new,
old_value => $old
);
$props->{$prop} = $pc;
} else {
warn "I found no prop in $line";
}
}
return $props;
}
=head1 LICENSE
Copyright 2008-2009 Best Practical Solutions.
This package is licensed under the same terms as Perl 5.8.8.
=cut
__PACKAGE__->meta->make_immutable;
no Any::Moose;
1;
|