| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package App::TimeTracker::Command::Trello; | 
| 2 | 1 |  |  | 1 |  | 753 | use strict; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 30 |  | 
| 3 | 1 |  |  | 1 |  | 5 | use warnings; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 27 |  | 
| 4 | 1 |  |  | 1 |  | 17 | use 5.010; | 
|  | 1 |  |  |  |  | 3 |  | 
| 5 |  |  |  |  |  |  |  | 
| 6 |  |  |  |  |  |  | # ABSTRACT: App::TimeTracker Trello plugin | 
| 7 | 1 |  |  | 1 |  | 466 | use App::TimeTracker::Utils qw(error_message warning_message); | 
|  | 1 |  |  |  |  | 10471 |  | 
|  | 1 |  |  |  |  | 116 |  | 
| 8 |  |  |  |  |  |  |  | 
| 9 |  |  |  |  |  |  | our $VERSION = "1.006"; | 
| 10 |  |  |  |  |  |  |  | 
| 11 | 1 |  |  | 1 |  | 538 | use Moose::Role; | 
|  | 1 |  |  |  |  | 463537 |  | 
|  | 1 |  |  |  |  | 3 |  | 
| 12 | 1 |  |  | 1 |  | 5485 | use WWW::Trello::Lite; | 
|  | 1 |  |  |  |  | 566486 |  | 
|  | 1 |  |  |  |  | 60 |  | 
| 13 | 1 |  |  | 1 |  | 724 | use JSON::XS qw(encode_json decode_json); | 
|  | 1 |  |  |  |  | 2706 |  | 
|  | 1 |  |  |  |  | 52 |  | 
| 14 | 1 |  |  | 1 |  | 375 | use Path::Class; | 
|  | 1 |  |  |  |  | 22491 |  | 
|  | 1 |  |  |  |  | 2287 |  | 
| 15 |  |  |  |  |  |  |  | 
| 16 |  |  |  |  |  |  | has 'trello' => ( | 
| 17 |  |  |  |  |  |  | is            => 'rw', | 
| 18 |  |  |  |  |  |  | isa           => 'Str', | 
| 19 |  |  |  |  |  |  | documentation => 'Trello card id', | 
| 20 |  |  |  |  |  |  | predicate     => 'has_trello' | 
| 21 |  |  |  |  |  |  | ); | 
| 22 |  |  |  |  |  |  |  | 
| 23 |  |  |  |  |  |  | has 'trello_client' => ( | 
| 24 |  |  |  |  |  |  | is         => 'rw', | 
| 25 |  |  |  |  |  |  | isa        => 'Maybe[WWW::Trello::Lite]', | 
| 26 |  |  |  |  |  |  | lazy_build => 1, | 
| 27 |  |  |  |  |  |  | traits     => ['NoGetopt'], | 
| 28 |  |  |  |  |  |  | ); | 
| 29 |  |  |  |  |  |  |  | 
| 30 |  |  |  |  |  |  | has 'trello_card' => ( | 
| 31 |  |  |  |  |  |  | is         => 'ro', | 
| 32 |  |  |  |  |  |  | lazy_build => 1, | 
| 33 |  |  |  |  |  |  | traits     => ['NoGetopt'], | 
| 34 |  |  |  |  |  |  | predicate  => 'has_trello_card' | 
| 35 |  |  |  |  |  |  | ); | 
| 36 |  |  |  |  |  |  |  | 
| 37 |  |  |  |  |  |  | sub _build_trello_card { | 
| 38 | 0 |  |  | 0 |  |  | my ($self) = @_; | 
| 39 |  |  |  |  |  |  |  | 
| 40 | 0 | 0 |  |  |  |  | return unless $self->has_trello; | 
| 41 | 0 |  |  |  |  |  | return $self->_trello_fetch_card( $self->trello ); | 
| 42 |  |  |  |  |  |  | } | 
| 43 |  |  |  |  |  |  |  | 
| 44 |  |  |  |  |  |  | sub _build_trello_client { | 
| 45 | 0 |  |  | 0 |  |  | my $self   = shift; | 
| 46 | 0 |  |  |  |  |  | my $config = $self->config->{trello}; | 
| 47 |  |  |  |  |  |  |  | 
| 48 | 0 | 0 | 0 |  |  |  | unless ( $config->{key} && $config->{token} ) { | 
| 49 | 0 |  |  |  |  |  | error_message( | 
| 50 |  |  |  |  |  |  | "Please configure Trello in your TimeTracker config or run 'tracker setup_trello'" | 
| 51 |  |  |  |  |  |  | ); | 
| 52 | 0 |  |  |  |  |  | return; | 
| 53 |  |  |  |  |  |  | } | 
| 54 |  |  |  |  |  |  | return WWW::Trello::Lite->new( | 
| 55 |  |  |  |  |  |  | key   => $self->config->{trello}{key}, | 
| 56 |  |  |  |  |  |  | token => $self->config->{trello}{token}, | 
| 57 | 0 |  |  |  |  |  | ); | 
| 58 |  |  |  |  |  |  | } | 
| 59 |  |  |  |  |  |  |  | 
| 60 |  |  |  |  |  |  | around BUILDARGS => sub { | 
| 61 |  |  |  |  |  |  | my $orig  = shift; | 
| 62 |  |  |  |  |  |  | my $class = shift; | 
| 63 |  |  |  |  |  |  | my %args; | 
| 64 |  |  |  |  |  |  |  | 
| 65 |  |  |  |  |  |  | if (scalar @_ == 1) { | 
| 66 |  |  |  |  |  |  | my $ref = shift(@_); | 
| 67 |  |  |  |  |  |  | %args = %$ref; | 
| 68 |  |  |  |  |  |  | } | 
| 69 |  |  |  |  |  |  | else { | 
| 70 |  |  |  |  |  |  | %args = @_; | 
| 71 |  |  |  |  |  |  | } | 
| 72 |  |  |  |  |  |  |  | 
| 73 |  |  |  |  |  |  | if ( $args{trello} && $args{trello} =~ /^https/ ) { | 
| 74 |  |  |  |  |  |  | $args{trello} =~ m|https://trello.com/c/([^/]+)/?|; | 
| 75 |  |  |  |  |  |  | $args{trello} = $1; | 
| 76 |  |  |  |  |  |  | } | 
| 77 |  |  |  |  |  |  | return $class->$orig(%args); | 
| 78 |  |  |  |  |  |  | }; | 
| 79 |  |  |  |  |  |  |  | 
| 80 |  |  |  |  |  |  | after '_load_attribs_stop' => sub { | 
| 81 |  |  |  |  |  |  | my ( $class, $meta ) = @_; | 
| 82 |  |  |  |  |  |  |  | 
| 83 |  |  |  |  |  |  | $meta->add_attribute( | 
| 84 |  |  |  |  |  |  | 'move_to' => { | 
| 85 |  |  |  |  |  |  | isa           => 'Str', | 
| 86 |  |  |  |  |  |  | is            => 'ro', | 
| 87 |  |  |  |  |  |  | documentation => 'Move Card to ...', | 
| 88 |  |  |  |  |  |  | } | 
| 89 |  |  |  |  |  |  | ); | 
| 90 |  |  |  |  |  |  | }; | 
| 91 |  |  |  |  |  |  |  | 
| 92 |  |  |  |  |  |  | before [ 'cmd_start', 'cmd_continue', 'cmd_append' ] => sub { | 
| 93 |  |  |  |  |  |  | my $self = shift; | 
| 94 |  |  |  |  |  |  | return unless $self->has_trello; | 
| 95 |  |  |  |  |  |  |  | 
| 96 |  |  |  |  |  |  | my $cardname = 'trello:' . $self->trello; | 
| 97 |  |  |  |  |  |  | $self->insert_tag($cardname); | 
| 98 |  |  |  |  |  |  |  | 
| 99 |  |  |  |  |  |  | my $name; | 
| 100 |  |  |  |  |  |  | my $card = $self->trello_card; | 
| 101 |  |  |  |  |  |  | return unless $card; | 
| 102 |  |  |  |  |  |  |  | 
| 103 |  |  |  |  |  |  | if ( $self->config->{trello}{listname_as_tag} ) { | 
| 104 |  |  |  |  |  |  | $self->_tag_listname($card); | 
| 105 |  |  |  |  |  |  | } | 
| 106 |  |  |  |  |  |  |  | 
| 107 |  |  |  |  |  |  | $name = $self->_trello_just_the_name($card); | 
| 108 |  |  |  |  |  |  | if ( defined $self->description ) { | 
| 109 |  |  |  |  |  |  | $self->description( $self->description . ' ' . $name ); | 
| 110 |  |  |  |  |  |  | } | 
| 111 |  |  |  |  |  |  | else { | 
| 112 |  |  |  |  |  |  | $self->description($name); | 
| 113 |  |  |  |  |  |  | } | 
| 114 |  |  |  |  |  |  |  | 
| 115 |  |  |  |  |  |  | if ( $self->meta->does_role('App::TimeTracker::Command::Git') ) { | 
| 116 |  |  |  |  |  |  | my $branch = $self->trello; | 
| 117 |  |  |  |  |  |  | if ($name) { | 
| 118 |  |  |  |  |  |  | $branch = $self->safe_branch_name($name) . '_' . $branch; | 
| 119 |  |  |  |  |  |  | } | 
| 120 |  |  |  |  |  |  | $self->branch( lc($branch) ) unless $self->branch; | 
| 121 |  |  |  |  |  |  | } | 
| 122 |  |  |  |  |  |  | }; | 
| 123 |  |  |  |  |  |  |  | 
| 124 |  |  |  |  |  |  | after [ 'cmd_start', 'cmd_continue', 'cmd_append' ] => sub { | 
| 125 |  |  |  |  |  |  | my $self = shift; | 
| 126 |  |  |  |  |  |  | return unless $self->has_trello_card; | 
| 127 |  |  |  |  |  |  |  | 
| 128 |  |  |  |  |  |  | my $card = $self->trello_card; | 
| 129 |  |  |  |  |  |  | return unless $card; | 
| 130 |  |  |  |  |  |  |  | 
| 131 |  |  |  |  |  |  | if ( my $lists = $self->_trello_fetch_lists ) { | 
| 132 |  |  |  |  |  |  | if ( $lists->{doing} ) { | 
| 133 |  |  |  |  |  |  | if (  !$card->{idList} | 
| 134 |  |  |  |  |  |  | || $card->{idList} ne $lists->{doing}->{id} ) { | 
| 135 |  |  |  |  |  |  | $self->_do_trello( | 
| 136 |  |  |  |  |  |  | 'put', | 
| 137 |  |  |  |  |  |  | 'cards/' . $card->{id} . '/idList', | 
| 138 |  |  |  |  |  |  | { value => $lists->{doing}->{id} } | 
| 139 |  |  |  |  |  |  | ); | 
| 140 |  |  |  |  |  |  | } | 
| 141 |  |  |  |  |  |  | } | 
| 142 |  |  |  |  |  |  | } | 
| 143 |  |  |  |  |  |  |  | 
| 144 |  |  |  |  |  |  | if ( my $member_id = $self->config->{trello}{member_id} ) { | 
| 145 |  |  |  |  |  |  | unless ( grep { $_ eq $member_id } @{ $card->{idMembers} } ) { | 
| 146 |  |  |  |  |  |  | my $members = $card->{idMembers}; | 
| 147 |  |  |  |  |  |  | push( @$members, $member_id ); | 
| 148 |  |  |  |  |  |  | $self->_do_trello( | 
| 149 |  |  |  |  |  |  | 'put', | 
| 150 |  |  |  |  |  |  | 'cards/' . $card->{id} . '/idMembers', | 
| 151 |  |  |  |  |  |  | { value => join( ',', @$members ) } | 
| 152 |  |  |  |  |  |  | ); | 
| 153 |  |  |  |  |  |  | } | 
| 154 |  |  |  |  |  |  | } | 
| 155 |  |  |  |  |  |  | }; | 
| 156 |  |  |  |  |  |  |  | 
| 157 |  |  |  |  |  |  | after 'cmd_stop' => sub { | 
| 158 |  |  |  |  |  |  | my $self = shift; | 
| 159 |  |  |  |  |  |  |  | 
| 160 |  |  |  |  |  |  | my $task = $self->_previous_task; | 
| 161 |  |  |  |  |  |  | return unless $task; | 
| 162 |  |  |  |  |  |  |  | 
| 163 |  |  |  |  |  |  | my $oldid = $task->trello_card_id; | 
| 164 |  |  |  |  |  |  | return unless $oldid; | 
| 165 |  |  |  |  |  |  |  | 
| 166 |  |  |  |  |  |  | my $task_rounded_minutes = $task->rounded_minutes; | 
| 167 |  |  |  |  |  |  |  | 
| 168 |  |  |  |  |  |  | my $card = $self->_trello_fetch_card($oldid); | 
| 169 |  |  |  |  |  |  | unless ($card) { | 
| 170 |  |  |  |  |  |  | warning_message( | 
| 171 |  |  |  |  |  |  | "Last task did not contain a trello id, not updating time etc."); | 
| 172 |  |  |  |  |  |  | return; | 
| 173 |  |  |  |  |  |  | } | 
| 174 |  |  |  |  |  |  |  | 
| 175 |  |  |  |  |  |  | my $name = $card->{name}; | 
| 176 |  |  |  |  |  |  | my %update; | 
| 177 |  |  |  |  |  |  |  | 
| 178 |  |  |  |  |  |  | if (    $self->config->{trello}{update_time_worked} | 
| 179 |  |  |  |  |  |  | and $task_rounded_minutes ) { | 
| 180 |  |  |  |  |  |  | if ( $name =~ /\[w:(\d+)m\]/ ) { | 
| 181 |  |  |  |  |  |  | my $new_worked = $1 + $task_rounded_minutes; | 
| 182 |  |  |  |  |  |  | $name =~ s/\[w:\d+m\]/'[w:'.$new_worked.'m]'/e; | 
| 183 |  |  |  |  |  |  | } | 
| 184 |  |  |  |  |  |  | else { | 
| 185 |  |  |  |  |  |  | $name .= ' [w:' . $task_rounded_minutes . 'm]'; | 
| 186 |  |  |  |  |  |  | } | 
| 187 |  |  |  |  |  |  | $update{name} = $name; | 
| 188 |  |  |  |  |  |  | } | 
| 189 |  |  |  |  |  |  |  | 
| 190 |  |  |  |  |  |  | if ( $self->can('move_to') ) { | 
| 191 |  |  |  |  |  |  | if ( my $move_to = $self->move_to ) { | 
| 192 |  |  |  |  |  |  | if ( my $lists = $self->_trello_fetch_lists ) { | 
| 193 |  |  |  |  |  |  | if ( $lists->{$move_to} ) { | 
| 194 |  |  |  |  |  |  | $update{idList} = $lists->{$move_to}->{id}; | 
| 195 |  |  |  |  |  |  | $update{pos}    = 'top'; | 
| 196 |  |  |  |  |  |  | } | 
| 197 |  |  |  |  |  |  | else { | 
| 198 |  |  |  |  |  |  | warning_message("Could not find list >$move_to<"); | 
| 199 |  |  |  |  |  |  | } | 
| 200 |  |  |  |  |  |  | } | 
| 201 |  |  |  |  |  |  | else { | 
| 202 |  |  |  |  |  |  | warning_message("Could not load lists"); | 
| 203 |  |  |  |  |  |  | } | 
| 204 |  |  |  |  |  |  | } | 
| 205 |  |  |  |  |  |  | } | 
| 206 |  |  |  |  |  |  |  | 
| 207 |  |  |  |  |  |  | return unless keys %update; | 
| 208 |  |  |  |  |  |  |  | 
| 209 |  |  |  |  |  |  | $self->_do_trello( 'put', 'cards/' . $card->{id}, \%update ); | 
| 210 |  |  |  |  |  |  | }; | 
| 211 |  |  |  |  |  |  |  | 
| 212 |  |  |  |  |  |  | sub _load_attribs_setup_trello { | 
| 213 | 0 |  |  | 0 |  |  | my ( $class, $meta ) = @_; | 
| 214 |  |  |  |  |  |  |  | 
| 215 | 0 |  |  |  |  |  | $meta->add_attribute( | 
| 216 |  |  |  |  |  |  | 'token_expiry' => { | 
| 217 |  |  |  |  |  |  | isa => 'Str', | 
| 218 |  |  |  |  |  |  | is  => 'ro', | 
| 219 |  |  |  |  |  |  | documentation => | 
| 220 |  |  |  |  |  |  | 'Trello token expiry [1hour, 1day, 30days, never]', | 
| 221 |  |  |  |  |  |  | default => '1day', | 
| 222 |  |  |  |  |  |  | } | 
| 223 |  |  |  |  |  |  | ); | 
| 224 |  |  |  |  |  |  | } | 
| 225 |  |  |  |  |  |  |  | 
| 226 |  |  |  |  |  |  | sub cmd_setup_trello { | 
| 227 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 228 |  |  |  |  |  |  |  | 
| 229 | 0 |  |  |  |  |  | my $conf = $self->config->{trello}; | 
| 230 | 0 |  |  |  |  |  | my %global; | 
| 231 |  |  |  |  |  |  | my %local; | 
| 232 | 0 | 0 |  |  |  |  | if ( $conf->{key} ) { | 
| 233 | 0 |  |  |  |  |  | say "Trello Key is already set."; | 
| 234 |  |  |  |  |  |  | } | 
| 235 |  |  |  |  |  |  | else { | 
| 236 | 0 |  |  |  |  |  | say | 
| 237 |  |  |  |  |  |  | "Please open this URL in your favourite browser, and paste the Key:\nhttps://trello.com/1/appKey/generate"; | 
| 238 | 0 |  |  |  |  |  | my $key = <STDIN>; | 
| 239 | 0 |  |  |  |  |  | $key =~ s/\s+//; | 
| 240 | 0 |  |  |  |  |  | $conf->{key} = $global{key} = $key; | 
| 241 | 0 |  |  |  |  |  | print "\n"; | 
| 242 |  |  |  |  |  |  | } | 
| 243 |  |  |  |  |  |  |  | 
| 244 | 0 | 0 |  |  |  |  | if ( $conf->{token} ) { | 
| 245 |  |  |  |  |  |  | my $token_info = | 
| 246 | 0 |  |  |  |  |  | $self->trello_client->get( 'tokens/' . $conf->{token} )->data; | 
| 247 | 0 | 0 |  |  |  |  | if ( $token_info->{dateExpires} ) { | 
| 248 | 0 |  |  |  |  |  | say "Token valid until: " . $token_info->{dateExpires}; | 
| 249 |  |  |  |  |  |  | } | 
| 250 |  |  |  |  |  |  | else { | 
| 251 | 0 |  |  |  |  |  | say "Token no longer valid"; | 
| 252 | 0 |  |  |  |  |  | delete $conf->{token}; | 
| 253 |  |  |  |  |  |  | } | 
| 254 |  |  |  |  |  |  | } | 
| 255 | 0 | 0 |  |  |  |  | unless ( $conf->{token} ) { | 
| 256 |  |  |  |  |  |  | my $get_token_url = | 
| 257 |  |  |  |  |  |  | 'https://trello.com/1/authorize?key=' | 
| 258 |  |  |  |  |  |  | . $conf->{key} | 
| 259 | 0 |  |  |  |  |  | . '&name=App::TimeTracker&expiration=' | 
| 260 |  |  |  |  |  |  | . $self->token_expiry | 
| 261 |  |  |  |  |  |  | . '&response_type=token&scope=read,write'; | 
| 262 | 0 |  |  |  |  |  | say | 
| 263 |  |  |  |  |  |  | "Please open this URL in your favourite browser, click 'Allow', and paste the token:\n$get_token_url"; | 
| 264 |  |  |  |  |  |  |  | 
| 265 | 0 |  |  |  |  |  | my $token = <STDIN>; | 
| 266 | 0 |  |  |  |  |  | $token =~ s/\s+//; | 
| 267 | 0 |  |  |  |  |  | $conf->{token} = $global{token} = $token; | 
| 268 |  |  |  |  |  |  |  | 
| 269 | 0 | 0 |  |  |  |  | if ( $self->trello_client ) { | 
| 270 | 0 |  |  |  |  |  | $self->trello_client->token($token); | 
| 271 |  |  |  |  |  |  | } | 
| 272 |  |  |  |  |  |  | else { | 
| 273 | 0 |  |  |  |  |  | $self->config->{trello} = $conf; | 
| 274 | 0 |  |  |  |  |  | $self->trello_client( $self->_build_trello_client ); | 
| 275 |  |  |  |  |  |  | } | 
| 276 | 0 |  |  |  |  |  | print "\n"; | 
| 277 |  |  |  |  |  |  | } | 
| 278 | 0 |  |  |  |  |  | $self->config->{trello} = $conf; | 
| 279 |  |  |  |  |  |  |  | 
| 280 | 0 | 0 |  |  |  |  | if ( $conf->{member_id} ) { | 
| 281 | 0 |  |  |  |  |  | say "member_id is already set."; | 
| 282 |  |  |  |  |  |  | } | 
| 283 |  |  |  |  |  |  | else { | 
| 284 |  |  |  |  |  |  | $conf->{member_id} = $global{member_id} = | 
| 285 | 0 |  |  |  |  |  | $self->_do_trello( 'get', 'members/me' )->{id}; | 
| 286 | 0 |  |  |  |  |  | say "Your member_id is " . $conf->{member_id}; | 
| 287 | 0 |  |  |  |  |  | print "\n"; | 
| 288 |  |  |  |  |  |  | } | 
| 289 |  |  |  |  |  |  |  | 
| 290 | 0 | 0 |  |  |  |  | if ( $conf->{board_id} ) { | 
| 291 | 0 |  |  |  |  |  | say "board_id is already set."; | 
| 292 |  |  |  |  |  |  | } | 
| 293 | 0 | 0 |  |  |  |  | unless ( $conf->{board_id} ) { | 
| 294 | 0 |  |  |  |  |  | print "Do you want to set a Board? [y/N] "; | 
| 295 | 0 |  |  |  |  |  | my $in = <STDIN>; | 
| 296 | 0 |  |  |  |  |  | $in =~ s/\s+//; | 
| 297 | 0 | 0 |  |  |  |  | if ( $in =~ /^y/i ) { | 
| 298 | 0 |  |  |  |  |  | say "Your Boards:"; | 
| 299 |  |  |  |  |  |  | my $boards = $self->_do_trello( 'get', | 
| 300 | 0 |  |  |  |  |  | 'members/' . $conf->{member_id} . '/boards' ); | 
| 301 | 0 |  |  |  |  |  | my $cnt = 1; | 
| 302 | 0 |  |  |  |  |  | foreach (@$boards) { | 
| 303 | 0 |  |  |  |  |  | printf( "%i: %s\n", $cnt, $_->{name} ); | 
| 304 | 0 |  |  |  |  |  | $cnt++; | 
| 305 |  |  |  |  |  |  | } | 
| 306 | 0 |  |  |  |  |  | print "Your selection (number or nothing to skip): "; | 
| 307 | 0 |  |  |  |  |  | my $in = <STDIN>; | 
| 308 | 0 |  |  |  |  |  | $in =~ s/\D//; | 
| 309 | 0 | 0 |  |  |  |  | if ($in) { | 
| 310 |  |  |  |  |  |  | $conf->{board_id} = $local{board_id} = | 
| 311 | 0 |  |  |  |  |  | $boards->[ $in - 1 ]->{id}; | 
| 312 |  |  |  |  |  |  | } | 
| 313 |  |  |  |  |  |  | } | 
| 314 |  |  |  |  |  |  | } | 
| 315 |  |  |  |  |  |  |  | 
| 316 | 0 | 0 |  |  |  |  | if ( keys %global ) { | 
| 317 |  |  |  |  |  |  | $self->_trello_update_config( \%global, | 
| 318 | 0 |  |  |  |  |  | $self->config->{_used_config_files}->[-1], 'global' ); | 
| 319 |  |  |  |  |  |  | } | 
| 320 | 0 | 0 |  |  |  |  | if ( keys %local ) { | 
| 321 |  |  |  |  |  |  | $self->_trello_update_config( \%local, | 
| 322 | 0 |  |  |  |  |  | $self->config->{_used_config_files}->[0], 'local' ); | 
| 323 |  |  |  |  |  |  | } | 
| 324 |  |  |  |  |  |  | } | 
| 325 |  |  |  |  |  |  |  | 
| 326 |  |  |  |  |  |  | sub _do_trello { | 
| 327 | 0 |  |  | 0 |  |  | my ( $self, $method, $endpoint, @args ) = @_; | 
| 328 | 0 |  |  |  |  |  | my $client = $self->trello_client; | 
| 329 | 0 | 0 |  |  |  |  | exit 1 unless $client; | 
| 330 |  |  |  |  |  |  |  | 
| 331 | 0 |  |  |  |  |  | my $res = $client->$method( $endpoint, @args ); | 
| 332 | 0 | 0 |  |  |  |  | if ( $res->failed ) { | 
| 333 | 0 |  |  |  |  |  | error_message( | 
| 334 |  |  |  |  |  |  | "Cannot talk to Trello API: " . $res->error . ' ' . $res->code ); | 
| 335 | 0 | 0 |  |  |  |  | if ( $res->code == 401 ) { | 
| 336 | 0 |  |  |  |  |  | say "Maybe running 'tracker setup_trello' will help..."; | 
| 337 |  |  |  |  |  |  | } | 
| 338 | 0 |  |  |  |  |  | exit 1; | 
| 339 |  |  |  |  |  |  | } | 
| 340 |  |  |  |  |  |  | else { | 
| 341 | 0 |  |  |  |  |  | return $res->data; | 
| 342 |  |  |  |  |  |  | } | 
| 343 |  |  |  |  |  |  | } | 
| 344 |  |  |  |  |  |  |  | 
| 345 |  |  |  |  |  |  | sub _trello_update_config { | 
| 346 | 0 |  |  | 0 |  |  | my ( $self, $update, $file, $type ) = @_; | 
| 347 |  |  |  |  |  |  |  | 
| 348 | 0 |  |  |  |  |  | print "I will store the following keys\n\t" | 
| 349 |  |  |  |  |  |  | . join( ', ', sort keys %$update ) | 
| 350 |  |  |  |  |  |  | . "\nin your $type config file\n$file\n"; | 
| 351 | 0 |  |  |  |  |  | print "(Y|n): "; | 
| 352 | 0 |  |  |  |  |  | my $in = <STDIN>; | 
| 353 | 0 |  |  |  |  |  | $in =~ s/\s+//; | 
| 354 | 0 | 0 |  |  |  |  | unless ( $in =~ /^n/i ) { | 
| 355 | 0 |  |  |  |  |  | my $f   = file($file); | 
| 356 | 0 |  |  |  |  |  | my $old = JSON::XS->new->utf8->relaxed->decode( | 
| 357 |  |  |  |  |  |  | scalar $f->slurp( iomode => '<:encoding(UTF-8)' ) ); | 
| 358 | 0 |  |  |  |  |  | while ( my ( $k, $v ) = each %$update ) { | 
| 359 | 0 |  |  |  |  |  | $old->{trello}{$k} = $v; | 
| 360 |  |  |  |  |  |  | } | 
| 361 |  |  |  |  |  |  | $f->spew( | 
| 362 | 0 |  |  |  |  |  | iomode => '>:encoding(UTF-8)', | 
| 363 |  |  |  |  |  |  | JSON::XS->new->utf8->pretty->encode($old) | 
| 364 |  |  |  |  |  |  | ); | 
| 365 |  |  |  |  |  |  | } | 
| 366 |  |  |  |  |  |  | } | 
| 367 |  |  |  |  |  |  |  | 
| 368 |  |  |  |  |  |  | sub _trello_fetch_card { | 
| 369 | 0 |  |  | 0 |  |  | my ( $self, $trello_tag ) = @_; | 
| 370 |  |  |  |  |  |  |  | 
| 371 | 0 |  |  |  |  |  | my %search = ( | 
| 372 |  |  |  |  |  |  | query       => $trello_tag, | 
| 373 |  |  |  |  |  |  | card_fields => 'shortLink', | 
| 374 |  |  |  |  |  |  | modelTypes  => 'cards' | 
| 375 |  |  |  |  |  |  | ); | 
| 376 | 0 | 0 |  |  |  |  | if ( my $board_id = $self->config->{trello}{board_id} ) { | 
| 377 | 0 |  |  |  |  |  | $search{idBoards} = $board_id; | 
| 378 |  |  |  |  |  |  | } | 
| 379 |  |  |  |  |  |  |  | 
| 380 | 0 |  |  |  |  |  | my $result = $self->_do_trello( 'get', 'search', \%search ); | 
| 381 | 0 |  |  |  |  |  | my $cards = $result->{cards}; | 
| 382 | 0 | 0 |  |  |  |  | unless ( @$cards == 1 ) { | 
| 383 | 0 |  |  |  |  |  | warning_message( | 
| 384 |  |  |  |  |  |  | "Could not identify trello card via '" . $trello_tag . "'" ); | 
| 385 | 0 |  |  |  |  |  | return; | 
| 386 |  |  |  |  |  |  | } | 
| 387 | 0 |  |  |  |  |  | my $id = $cards->[0]{id}; | 
| 388 | 0 |  |  |  |  |  | my $card = $self->_do_trello( 'get', 'cards/' . $id ); | 
| 389 | 0 |  |  |  |  |  | return $card; | 
| 390 |  |  |  |  |  |  | } | 
| 391 |  |  |  |  |  |  |  | 
| 392 |  |  |  |  |  |  | sub _trello_fetch_lists { | 
| 393 | 0 |  |  | 0 |  |  | my $self     = shift; | 
| 394 | 0 |  |  |  |  |  | my $board_id = $self->config->{trello}{board_id}; | 
| 395 | 0 | 0 |  |  |  |  | return unless $board_id; | 
| 396 | 0 |  |  |  |  |  | my $rv = $self->_do_trello( 'get', 'boards/' . $board_id . '/lists' ); | 
| 397 |  |  |  |  |  |  |  | 
| 398 | 0 |  |  |  |  |  | my %lists; | 
| 399 |  |  |  |  |  |  | my $map = $self->config->{trello}{list_map} | 
| 400 |  |  |  |  |  |  | || { | 
| 401 | 0 |  | 0 |  |  |  | 'To Do' => 'todo', | 
| 402 |  |  |  |  |  |  | 'Doing' => 'doing', | 
| 403 |  |  |  |  |  |  | 'Done'  => 'done', | 
| 404 |  |  |  |  |  |  | }; | 
| 405 | 0 |  |  |  |  |  | foreach my $list (@$rv) { | 
| 406 | 0 | 0 |  |  |  |  | next unless my $tracker_name = $map->{ $list->{name} }; | 
| 407 | 0 |  |  |  |  |  | $lists{$tracker_name} = $list; | 
| 408 |  |  |  |  |  |  | } | 
| 409 | 0 |  |  |  |  |  | return \%lists; | 
| 410 |  |  |  |  |  |  | } | 
| 411 |  |  |  |  |  |  |  | 
| 412 |  |  |  |  |  |  | sub _trello_just_the_name { | 
| 413 | 0 |  |  | 0 |  |  | my ( $self, $card ) = @_; | 
| 414 | 0 |  |  |  |  |  | my $name = $card->{name}; | 
| 415 | 0 |  |  |  |  |  | my $tr   = $self->trello; | 
| 416 | 0 |  |  |  |  |  | $name =~ s/$tr:\s?//; | 
| 417 | 0 |  |  |  |  |  | $name =~ s/\[(.*?)\]//g; | 
| 418 | 0 |  |  |  |  |  | $name =~ s/\s+/_/g; | 
| 419 | 0 |  |  |  |  |  | $name =~ s/_$//; | 
| 420 | 0 |  |  |  |  |  | $name =~ s/^_//; | 
| 421 | 0 |  |  |  |  |  | return $name; | 
| 422 |  |  |  |  |  |  | } | 
| 423 |  |  |  |  |  |  |  | 
| 424 |  |  |  |  |  |  | sub _tag_listname { | 
| 425 | 0 |  |  | 0 |  |  | my ( $self, $card ) = @_; | 
| 426 |  |  |  |  |  |  |  | 
| 427 | 0 |  |  |  |  |  | my $list_id = $card->{idList}; | 
| 428 | 0 | 0 |  |  |  |  | return unless $list_id; | 
| 429 | 0 |  |  |  |  |  | my $rv = $self->_do_trello( 'get', 'lists/' . $list_id . '/name' ); | 
| 430 | 0 |  |  |  |  |  | my $name = $rv->{_value}; | 
| 431 | 0 | 0 |  |  |  |  | $self->insert_tag($name) if $name; | 
| 432 |  |  |  |  |  |  | } | 
| 433 |  |  |  |  |  |  |  | 
| 434 |  |  |  |  |  |  | sub App::TimeTracker::Data::Task::trello_card_id { | 
| 435 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 436 | 0 |  |  |  |  |  | foreach my $tag ( @{ $self->tags } ) { | 
|  | 0 |  |  |  |  |  |  | 
| 437 | 0 | 0 |  |  |  |  | next unless $tag =~ /^trello:(\w+)/; | 
| 438 | 0 |  |  |  |  |  | return $1; | 
| 439 |  |  |  |  |  |  | } | 
| 440 |  |  |  |  |  |  | } | 
| 441 |  |  |  |  |  |  |  | 
| 442 | 1 |  |  | 1 |  | 10 | no Moose::Role; | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 9 |  | 
| 443 |  |  |  |  |  |  | 1; | 
| 444 |  |  |  |  |  |  |  | 
| 445 |  |  |  |  |  |  | __END__ | 
| 446 |  |  |  |  |  |  |  | 
| 447 |  |  |  |  |  |  | =pod | 
| 448 |  |  |  |  |  |  |  | 
| 449 |  |  |  |  |  |  | =encoding UTF-8 | 
| 450 |  |  |  |  |  |  |  | 
| 451 |  |  |  |  |  |  | =head1 NAME | 
| 452 |  |  |  |  |  |  |  | 
| 453 |  |  |  |  |  |  | App::TimeTracker::Command::Trello - App::TimeTracker Trello plugin | 
| 454 |  |  |  |  |  |  |  | 
| 455 |  |  |  |  |  |  | =head1 VERSION | 
| 456 |  |  |  |  |  |  |  | 
| 457 |  |  |  |  |  |  | version 1.006 | 
| 458 |  |  |  |  |  |  |  | 
| 459 |  |  |  |  |  |  | =head1 DESCRIPTION | 
| 460 |  |  |  |  |  |  |  | 
| 461 |  |  |  |  |  |  | This plugin takes a lot of hassle out of working with Trello | 
| 462 |  |  |  |  |  |  | L<http://trello.com/>. | 
| 463 |  |  |  |  |  |  |  | 
| 464 |  |  |  |  |  |  | Using the Trello plugin, tracker can fetch the name of a Card and use | 
| 465 |  |  |  |  |  |  | it as the task's description; generate a nicely named C<git> branch | 
| 466 |  |  |  |  |  |  | (if you're also using the C<Git> plugin); add the user as a member to | 
| 467 |  |  |  |  |  |  | the Card; move the card to various lists; and use some hackish | 
| 468 |  |  |  |  |  |  | extension to the Card name to store the time-worked in the Card. | 
| 469 |  |  |  |  |  |  |  | 
| 470 |  |  |  |  |  |  | =head1 CONFIGURATION | 
| 471 |  |  |  |  |  |  |  | 
| 472 |  |  |  |  |  |  | =head2 plugins | 
| 473 |  |  |  |  |  |  |  | 
| 474 |  |  |  |  |  |  | Add C<Trello> to the list of plugins. | 
| 475 |  |  |  |  |  |  |  | 
| 476 |  |  |  |  |  |  | =head2 trello | 
| 477 |  |  |  |  |  |  |  | 
| 478 |  |  |  |  |  |  | add a hash named C<trello>, containing the following keys: | 
| 479 |  |  |  |  |  |  |  | 
| 480 |  |  |  |  |  |  | =head3 key [REQUIRED] | 
| 481 |  |  |  |  |  |  |  | 
| 482 |  |  |  |  |  |  | Your Trello Developer Key. Get it from | 
| 483 |  |  |  |  |  |  | L<https://trello.com/1/appKey/generate> or via C<tracker | 
| 484 |  |  |  |  |  |  | setup_trello>. | 
| 485 |  |  |  |  |  |  |  | 
| 486 |  |  |  |  |  |  | =head3 token [REQUIRED] | 
| 487 |  |  |  |  |  |  |  | 
| 488 |  |  |  |  |  |  | Your access token. Get it from | 
| 489 |  |  |  |  |  |  | L<https://trello.com/1/authorize?key=YOUR_DEV_KEY&name=tracker&expiration=1day&response_type=token&scope=read,write>. | 
| 490 |  |  |  |  |  |  | You maybe want to set a longer expiration timeframe. | 
| 491 |  |  |  |  |  |  |  | 
| 492 |  |  |  |  |  |  | You can also get it via C<tracker setup_trello>. | 
| 493 |  |  |  |  |  |  |  | 
| 494 |  |  |  |  |  |  | =head3 board_id [SORT OF REQUIRED] | 
| 495 |  |  |  |  |  |  |  | 
| 496 |  |  |  |  |  |  | The C<board_id> of the board you want to use. | 
| 497 |  |  |  |  |  |  |  | 
| 498 |  |  |  |  |  |  | Not stictly necessary, as we use ids to identify cards. | 
| 499 |  |  |  |  |  |  |  | 
| 500 |  |  |  |  |  |  | If you specify the C<board_id>, C<tracker> will only search in this board. | 
| 501 |  |  |  |  |  |  |  | 
| 502 |  |  |  |  |  |  | You can get the C<board_id> by going to "Share, print and export" in | 
| 503 |  |  |  |  |  |  | the sidebar menu, click "Export JSON" and then find the C<id> in the | 
| 504 |  |  |  |  |  |  | toplevel hash. Or run C<tracker setup_trello>. | 
| 505 |  |  |  |  |  |  |  | 
| 506 |  |  |  |  |  |  | =head3 member_id | 
| 507 |  |  |  |  |  |  |  | 
| 508 |  |  |  |  |  |  | Your trello C<member_id>. | 
| 509 |  |  |  |  |  |  |  | 
| 510 |  |  |  |  |  |  | Needed for adding you to a Card's list of members. Currently a bit | 
| 511 |  |  |  |  |  |  | hard to get from trello, so use C<tracker setup_trello>. | 
| 512 |  |  |  |  |  |  |  | 
| 513 |  |  |  |  |  |  | =head3 update_time_worked | 
| 514 |  |  |  |  |  |  |  | 
| 515 |  |  |  |  |  |  | If set to true, updates the time worked on this task on the Trello Card. | 
| 516 |  |  |  |  |  |  |  | 
| 517 |  |  |  |  |  |  | As Trello does not provide time-tracking (yet?), we store the | 
| 518 |  |  |  |  |  |  | time-worked in some simple markup in the Card name: | 
| 519 |  |  |  |  |  |  |  | 
| 520 |  |  |  |  |  |  | Callibrate FluxCompensator [w:32m] | 
| 521 |  |  |  |  |  |  |  | 
| 522 |  |  |  |  |  |  | C<[w:32m]> means that you worked 32 minutes on the task. | 
| 523 |  |  |  |  |  |  |  | 
| 524 |  |  |  |  |  |  | Context: stopish commands | 
| 525 |  |  |  |  |  |  |  | 
| 526 |  |  |  |  |  |  | =head3 listname_as_tag | 
| 527 |  |  |  |  |  |  |  | 
| 528 |  |  |  |  |  |  | If set to true, will fetch the name of the list the current card | 
| 529 |  |  |  |  |  |  | belongs to and store the name as an additional tag. | 
| 530 |  |  |  |  |  |  |  | 
| 531 |  |  |  |  |  |  | Context: startish commands | 
| 532 |  |  |  |  |  |  |  | 
| 533 |  |  |  |  |  |  | =head1 NEW COMMANDS | 
| 534 |  |  |  |  |  |  |  | 
| 535 |  |  |  |  |  |  | =head2 setup_trello | 
| 536 |  |  |  |  |  |  |  | 
| 537 |  |  |  |  |  |  | ~/perl/Your-Project$ tracker setup_trello | 
| 538 |  |  |  |  |  |  |  | 
| 539 |  |  |  |  |  |  | This will launch an interactive process that walks you throught the setup. | 
| 540 |  |  |  |  |  |  |  | 
| 541 |  |  |  |  |  |  | Depending on your config, you will be pointed to URLs to get your | 
| 542 |  |  |  |  |  |  | C<key>, C<token> and C<member_id>. You can also set up a C<board_id>. | 
| 543 |  |  |  |  |  |  | The data will be stored in your global / local config. | 
| 544 |  |  |  |  |  |  |  | 
| 545 |  |  |  |  |  |  | You will need a web browser to access the URLs on trello.com. | 
| 546 |  |  |  |  |  |  |  | 
| 547 |  |  |  |  |  |  | =head3 --token_expiry [1hour, 1day, 30days, never] | 
| 548 |  |  |  |  |  |  |  | 
| 549 |  |  |  |  |  |  | Token expiry time when a new token is requested from trello. Defaults | 
| 550 |  |  |  |  |  |  | to '1day'. | 
| 551 |  |  |  |  |  |  |  | 
| 552 |  |  |  |  |  |  | 'never' is the most comfortable option, but of course also the most | 
| 553 |  |  |  |  |  |  | insecure. | 
| 554 |  |  |  |  |  |  |  | 
| 555 |  |  |  |  |  |  | Please note that you can always invalidate tokens via trello.com (go | 
| 556 |  |  |  |  |  |  | to Settings/Applications) | 
| 557 |  |  |  |  |  |  |  | 
| 558 |  |  |  |  |  |  | =head1 CHANGES TO OTHER COMMANDS | 
| 559 |  |  |  |  |  |  |  | 
| 560 |  |  |  |  |  |  | =head2 start, continue | 
| 561 |  |  |  |  |  |  |  | 
| 562 |  |  |  |  |  |  | =head3 --trello | 
| 563 |  |  |  |  |  |  |  | 
| 564 |  |  |  |  |  |  | ~/perl/Your-Project$ tracker start --trello s1d7prUx | 
| 565 |  |  |  |  |  |  |  | 
| 566 |  |  |  |  |  |  | ~/perl/Your-Project$ tracker start --trello https://trello.com/c/s1d7prUx/card-title | 
| 567 |  |  |  |  |  |  |  | 
| 568 |  |  |  |  |  |  | If C<--trello> is set and we can find a card with this id: | 
| 569 |  |  |  |  |  |  |  | 
| 570 |  |  |  |  |  |  | =over | 
| 571 |  |  |  |  |  |  |  | 
| 572 |  |  |  |  |  |  | =item * set or append the Card name in the task description ("Rev up FluxCompensator!!") | 
| 573 |  |  |  |  |  |  |  | 
| 574 |  |  |  |  |  |  | =item * add the Card id to the tasks tags ("trello:s1d7prUx") | 
| 575 |  |  |  |  |  |  |  | 
| 576 |  |  |  |  |  |  | =item * if C<Git> is also used, determine a save branch name from the Card name, and change into this branch ("rev_up_fluxcompensator_s1d7prUx") | 
| 577 |  |  |  |  |  |  |  | 
| 578 |  |  |  |  |  |  | =item * add member to list of members (if C<member_id> is set in config) | 
| 579 |  |  |  |  |  |  |  | 
| 580 |  |  |  |  |  |  | =item * move to C<Doing> list (if there is such a list, or another list is defined in C<list_map> in config) | 
| 581 |  |  |  |  |  |  |  | 
| 582 |  |  |  |  |  |  | =back | 
| 583 |  |  |  |  |  |  |  | 
| 584 |  |  |  |  |  |  | <C--trello> can either be the full URL of the card, or just the card | 
| 585 |  |  |  |  |  |  | id. If you don't have access to the URL, click the 'Share and more' | 
| 586 |  |  |  |  |  |  | link (rather hard to find in the bottom right corner of a card). | 
| 587 |  |  |  |  |  |  |  | 
| 588 |  |  |  |  |  |  | If C<listname_as_tag> is set, will store the name of the card's list as a tag. | 
| 589 |  |  |  |  |  |  |  | 
| 590 |  |  |  |  |  |  | =head2 stop | 
| 591 |  |  |  |  |  |  |  | 
| 592 |  |  |  |  |  |  | =over | 
| 593 |  |  |  |  |  |  |  | 
| 594 |  |  |  |  |  |  | =item * If <update_time_worked> is set in config, adds the time worked on this task to the Card. | 
| 595 |  |  |  |  |  |  |  | 
| 596 |  |  |  |  |  |  | =back | 
| 597 |  |  |  |  |  |  |  | 
| 598 |  |  |  |  |  |  | =head3 --move_to | 
| 599 |  |  |  |  |  |  |  | 
| 600 |  |  |  |  |  |  | If --move_to is specified and a matching list is found in C<list_map> in config, move the Card to this list. | 
| 601 |  |  |  |  |  |  |  | 
| 602 |  |  |  |  |  |  | =head1 AUTHOR | 
| 603 |  |  |  |  |  |  |  | 
| 604 |  |  |  |  |  |  | Thomas Klausner <domm@cpan.org> | 
| 605 |  |  |  |  |  |  |  | 
| 606 |  |  |  |  |  |  | =head1 COPYRIGHT AND LICENSE | 
| 607 |  |  |  |  |  |  |  | 
| 608 |  |  |  |  |  |  | This software is copyright (c) 2016 by Thomas Klausner. | 
| 609 |  |  |  |  |  |  |  | 
| 610 |  |  |  |  |  |  | This is free software; you can redistribute it and/or modify it under | 
| 611 |  |  |  |  |  |  | the same terms as the Perl 5 programming language system itself. | 
| 612 |  |  |  |  |  |  |  | 
| 613 |  |  |  |  |  |  | =cut |