File Coverage

lib/App/TimeTracker/Command/Trello.pm
Criterion Covered Total %
statement 26 155 16.7
branch 0 50 0.0
condition 0 5 0.0
subroutine 9 20 45.0
pod 0 2 0.0
total 35 232 15.0


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