File Coverage

lib/App/TimeTracker/Command/Trello.pm
Criterion Covered Total %
statement 26 152 17.1
branch 0 50 0.0
condition 0 5 0.0
subroutine 9 20 45.0
pod 0 2 0.0
total 35 229 15.2


line stmt bran cond sub pod time code
1             package App::TimeTracker::Command::Trello;
2 1     1   696 use strict;
  1         2  
  1         30  
3 1     1   5 use warnings;
  1         2  
  1         23  
4 1     1   21 use 5.010;
  1         3  
5              
6             # ABSTRACT: App::TimeTracker Trello plugin
7 1     1   500 use App::TimeTracker::Utils qw(error_message warning_message);
  1         11077  
  1         74  
8              
9             our $VERSION = "1.007";
10              
11 1     1   494 use Moose::Role;
  1         478206  
  1         5  
12 1     1   6697 use WWW::Trello::Lite;
  1         688283  
  1         67  
13 1     1   966 use JSON::XS qw(encode_json decode_json);
  1         3591  
  1         77  
14 1     1   476 use Path::Class;
  1         28548  
  1         2784  
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 = $card->{idShort};
117             if ($name) {
118             $branch .= '_'.$self->safe_branch_name($name);
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           return $name;
419             }
420              
421             sub _tag_listname {
422 0     0     my ( $self, $card ) = @_;
423              
424 0           my $list_id = $card->{idList};
425 0 0         return unless $list_id;
426 0           my $rv = $self->_do_trello( 'get', 'lists/' . $list_id . '/name' );
427 0           my $name = $rv->{_value};
428 0 0         $self->insert_tag($name) if $name;
429             }
430              
431             sub App::TimeTracker::Data::Task::trello_card_id {
432 0     0 0   my $self = shift;
433 0           foreach my $tag ( @{ $self->tags } ) {
  0            
434 0 0         next unless $tag =~ /^trello:(\w+)/;
435 0           return $1;
436             }
437             }
438              
439 1     1   11 no Moose::Role;
  1         3  
  1         12  
440              
441             q{ listening to: SarahBernhardt - langsam wiads wos }
442              
443             __END__
444              
445             =pod
446              
447             =encoding UTF-8
448              
449             =head1 NAME
450              
451             App::TimeTracker::Command::Trello - App::TimeTracker Trello plugin
452              
453             =head1 VERSION
454              
455             version 1.007
456              
457             =head1 DESCRIPTION
458              
459             This plugin takes a lot of hassle out of working with Trello
460             L<http://trello.com/>.
461              
462             Using the Trello plugin, tracker can fetch the name of a Card and use
463             it as the task's description; generate a nicely named C<git> branch
464             (if you're also using the C<Git> plugin); add the user as a member to
465             the Card; move the card to various lists; and use some hackish
466             extension to the Card name to store the time-worked in the Card.
467              
468             =head1 CONFIGURATION
469              
470             =head2 plugins
471              
472             Add C<Trello> to the list of plugins.
473              
474             =head2 trello
475              
476             add a hash named C<trello>, containing the following keys:
477              
478             =head3 key [REQUIRED]
479              
480             Your Trello Developer Key. Get it from
481             L<https://trello.com/1/appKey/generate> or via C<tracker
482             setup_trello>.
483              
484             =head3 token [REQUIRED]
485              
486             Your access token. Get it from
487             L<https://trello.com/1/authorize?key=YOUR_DEV_KEY&name=tracker&expiration=1day&response_type=token&scope=read,write>.
488             You maybe want to set a longer expiration timeframe.
489              
490             You can also get it via C<tracker setup_trello>.
491              
492             =head3 board_id [SORT OF REQUIRED]
493              
494             The C<board_id> of the board you want to use.
495              
496             Not stictly necessary, as we use ids to identify cards.
497              
498             If you specify the C<board_id>, C<tracker> will only search in this board.
499              
500             You can get the C<board_id> by going to "Share, print and export" in
501             the sidebar menu, click "Export JSON" and then find the C<id> in the
502             toplevel hash. Or run C<tracker setup_trello>.
503              
504             =head3 member_id
505              
506             Your trello C<member_id>.
507              
508             Needed for adding you to a Card's list of members. Currently a bit
509             hard to get from trello, so use C<tracker setup_trello>.
510              
511             =head3 update_time_worked
512              
513             If set to true, updates the time worked on this task on the Trello Card.
514              
515             As Trello does not provide time-tracking (yet?), we store the
516             time-worked in some simple markup in the Card name:
517              
518             Callibrate FluxCompensator [w:32m]
519              
520             C<[w:32m]> means that you worked 32 minutes on the task.
521              
522             Context: stopish commands
523              
524             =head3 listname_as_tag
525              
526             If set to true, will fetch the name of the list the current card
527             belongs to and store the name as an additional tag.
528              
529             Context: startish commands
530              
531             =head1 NEW COMMANDS
532              
533             =head2 setup_trello
534              
535             ~/perl/Your-Project$ tracker setup_trello
536              
537             This will launch an interactive process that walks you throught the setup.
538              
539             Depending on your config, you will be pointed to URLs to get your
540             C<key>, C<token> and C<member_id>. You can also set up a C<board_id>.
541             The data will be stored in your global / local config.
542              
543             You will need a web browser to access the URLs on trello.com.
544              
545             =head3 --token_expiry [1hour, 1day, 30days, never]
546              
547             Token expiry time when a new token is requested from trello. Defaults
548             to '1day'.
549              
550             'never' is the most comfortable option, but of course also the most
551             insecure.
552              
553             Please note that you can always invalidate tokens via trello.com (go
554             to Settings/Applications)
555              
556             =head1 CHANGES TO OTHER COMMANDS
557              
558             =head2 start, continue
559              
560             =head3 --trello
561              
562             ~/perl/Your-Project$ tracker start --trello s1d7prUx
563              
564             ~/perl/Your-Project$ tracker start --trello https://trello.com/c/s1d7prUx/card-title
565              
566             If C<--trello> is set and we can find a card with this id:
567              
568             =over
569              
570             =item * set or append the Card name in the task description ("Rev up FluxCompensator!!")
571              
572             =item * add the Card id to the tasks tags ("trello:s1d7prUx")
573              
574             =item * if C<Git> is also used, determine a save branch name from idShort and the Card name, and change into this branch ("42_rev_up_fluxcompensator")
575              
576             =item * add member to list of members (if C<member_id> is set in config)
577              
578             =item * move to C<Doing> list (if there is such a list, or another list is defined in C<list_map> in config)
579              
580             =back
581              
582             <C--trello> can either be the full URL of the card, or just the card
583             id. If you don't have access to the URL, click the 'Share and more'
584             link (rather hard to find in the bottom right corner of a card).
585              
586             If C<listname_as_tag> is set, will store the name of the card's list as a tag.
587              
588             =head2 stop
589              
590             =over
591              
592             =item * If <update_time_worked> is set in config, adds the time worked on this task to the Card.
593              
594             =back
595              
596             =head3 --move_to
597              
598             If --move_to is specified and a matching list is found in C<list_map> in config, move the Card to this list.
599              
600             =head1 AUTHOR
601              
602             Thomas Klausner <domm@cpan.org>
603              
604             =head1 COPYRIGHT AND LICENSE
605              
606             This software is copyright (c) 2016 by Thomas Klausner.
607              
608             This is free software; you can redistribute it and/or modify it under
609             the same terms as the Perl 5 programming language system itself.
610              
611             =cut