File Coverage

blib/lib/App/Toodledo.pm
Criterion Covered Total %
statement 90 92 97.8
branch 3 4 75.0
condition n/a
subroutine 49 50 98.0
pod 0 1 0.0
total 142 147 96.6


line stmt bran cond sub pod time code
1             package App::Toodledo;
2 8     8   435585 use strict;
  8         19  
  8         204  
3 8     8   41 use warnings;
  8         14  
  8         321  
4              
5             our $VERSION = '2.19';
6              
7 8     8   127 BEGIN { $PPI::XS_DISABLE = 1 } # PPI::XS throws deprecation warnings in 5.16
8              
9 8     8   44 use File::Spec;
  8         12  
  8         188  
10 8     8   39 use Digest::MD5 'md5_hex';
  8         14  
  8         343  
11 8     8   2769 use Moose;
  8         3047747  
  8         49  
12 8     8   58024 use MooseX::Method::Signatures;
  8         9671026  
  8         45  
13 8     8   4631 use MooseX::ClassAttribute;
  8         554628  
  8         35  
14 8     8   1724540 use JSON;
  8         47684  
  8         53  
15 8     8   3212 use URI::Encode qw(uri_encode);
  8         69216  
  8         491  
16 8     8   3149 use LWP::UserAgent;
  8         277732  
  8         273  
17 8     8   2477 use Date::Parse;
  8         34232  
  8         919  
18 8     8   2066 use YAML qw(LoadFile DumpFile);
  8         40188  
  8         422  
19 8     8   1860 use Log::Log4perl::Level;
  8         9175  
  8         37  
20             with 'MooseX::Log::Log4perl';
21              
22 8     8   3377 use App::Toodledo::TokenCache;
  8         31  
  8         481  
23 8     8   3131 use App::Toodledo::InfoCache;
  8         29  
  8         485  
24 8     8   3399 use App::Toodledo::Account;
  8         30  
  8         336  
25 8     8   2533 use App::Toodledo::Task;
  8         60  
  8         599  
26 8     8   3384 use App::Toodledo::TaskCache;
  8         30  
  8         546  
27 8     8   73 use App::Toodledo::Util qw(home arg_encode preferred_date_format);
  8         19  
  8         2452  
28              
29             my $HOST = 'api.toodledo.com';
30             my $ROOT_URL = "https://$HOST/2/";
31              
32             class_has Info_File_Name => ( is => 'rw', default => '.toodledorc' );
33             class_has Token_File_Name => ( is => 'rw', default => '.toodledo_token' );
34              
35             has app_id => ( is => 'rw', isa => 'Str', required => 1, );
36             has app_token => ( is => 'rw', isa => 'Str', );
37             has user_id => ( is => 'rw', isa => 'Str' );
38             has password => ( is => 'rw', isa => 'Str', );
39             has key => ( is => 'rw', isa => 'Str', );
40             has session_token => ( is => 'rw', isa => 'Str', );
41             has session_key => ( is => 'rw', isa => 'Str', );
42             has user_agent => ( is => 'ro', default => \&_make_user_agent );
43             has info_cache => ( is => 'rw', isa => 'App::Toodledo::InfoCache' );
44             has account_info => ( is => 'rw', isa => 'App::Toodledo::Account' );
45             has task_cache => ( is => 'rw', isa => 'App::Toodledo::TaskCache' );
46              
47             #initializes log4perl to log to screen if it hasn't been initialized
48             #Will default to $ERROR, if $ENV{APP_TOODLEDO_DEBUG} then will set to
49             #logger to $debug
50             sub BUILD
51             {
52 5 100   5 0 18542 if (!Log::Log4perl->initialized())#TODO: make it smart
53             {
54 4 50       39 if (defined $ENV{APP_TOODLEDO_DEBUG})
55             {
56 0         0 Log::Log4perl->easy_init($DEBUG);
57             }
58             else
59             {
60 4         23 Log::Log4perl->easy_init($ERROR);
61             }
62             }
63             }
64              
65              
66 8     8   832845 method get_session_token ( Str :$app_token?, Str :$user_id? ) {
67             my $app_id = $self->app_id;
68             $user_id ||= $self->user_id or $self->log->logdie("No user_id");
69             $app_token ||= $self->app_token or $self->log->logdie("No app_token");
70             $self->user_id( $user_id );
71             $self->app_token( $app_token );
72              
73             my $session_token = $self->_session_token_from_cache( $user_id, $app_id,
74             $app_token);
75             $self->session_token( $session_token );
76             $session_token;
77             }
78              
79              
80 8     8   700394 method _session_token_from_cache ( Str $user_id!, Str $app_id!, Str $app_token! ) {
81             my $token_cache = $self->_token_cache;
82             my $session_token;
83             if ( my $token_info = $token_cache->valid_token( user_id => $user_id,
84             app_id => $app_id ) )
85             {
86             $self->log->debug( "Have valid saved token\n" );
87             $session_token = $token_info->token;
88             }
89             else
90             {
91             $session_token = $self->new_session_token( $app_token );
92             $token_cache->add_token_info( user_id => $user_id,
93             app_id => $app_id,
94             token => $session_token );
95             $token_cache->save;
96             }
97             $session_token;
98             }
99              
100              
101 8     8   382753 method get_session_token_from_rc ( Str $user_id? ) {
102             $user_id ||= $self->user_id || $self->default_user_id
103             or $self->log->logdie( "No user_id and no default user_id");
104             my $app_id = $self->app_id;
105             my $app_token = $self->app_token_of( $app_id )
106             or $self->log->logdie("Cannot get app_token for $app_id");
107             $self->get_session_token( app_token => $app_token, user_id => $user_id );
108             }
109              
110              
111 8     8   704805 method _make_session_key ( Str $password!, Str $app_token!, Str $session_token! ) {
112             md5_hex( md5_hex( $password ) . $app_token . $session_token );
113             }
114              
115              
116 8     8   350457 method connect ( Str $password! ) {
117             my $session_token = $self->session_token
118             or $self->log->logdie("Need to get session token first");
119             my $key = $self->_make_session_key( $password, $self->app_token,
120             $session_token );
121             $self->session_key( $key );
122             my $account_ref = $self->get( 'account' ) or $self->log->logdie( "No account info");
123             $self->account_info( $account_ref );
124             $key;
125             }
126              
127              
128 8     8   1112795 method login ( Str :$user_id, Str :$password!, Str :$app_token! ) {
129             $self->app_token( $app_token );
130             $self->get_session_token( user_id => $user_id, app_token => $app_token );
131             $self->connect( $password );
132             }
133              
134              
135 8     8   388554 method login_from_rc ( Str $user_id? ) {
136             my @args = $user_id ? $user_id : ();
137             $self->get_session_token_from_rc( @args );
138             my $password = $self->password_of( $self->user_id )
139             or $self->log->logdie("Cannot get password");
140             $self->log->debug( "Loaded password from info cache\n" );
141             $self->connect( $password );
142             }
143              
144              
145             sub _token_cache
146             {
147 1     1   4 my $file = _token_cache_name();
148              
149 1         13 App::Toodledo::TokenCache->new_from_file( $file );
150             }
151              
152              
153             sub _token_cache_name
154             {
155 2     2   1426 File::Spec->catfile( home(), __PACKAGE__->Token_File_Name );
156             }
157              
158              
159 8     8   363699 method app_token_of ( Str $app_id! ) {
160             my $cache = $self->_get_info_cache;
161             $cache->app_token_ref->{$app_id};
162             }
163              
164              
165 8     8   359116 method password_of ( Str $user_id! ) {
166             my $cache = $self->_get_info_cache;
167             $cache->password_ref->{$user_id};
168             }
169              
170              
171 8     8   171626 method default_user_id () {
172             my $cache = $self->_get_info_cache;
173             $cache->default_user_id;
174             }
175              
176              
177 8     8   164523 method _get_info_cache () {
178             my $file = _info_cache_name();
179              
180             $self->info_cache and return $self->info_cache;
181             $self->log->debug( "Fetching info cache\n" );
182             my $cache = App::Toodledo::InfoCache->new_from_file( $file );
183             $self->info_cache( $cache );
184             $cache;
185             }
186              
187              
188             sub _info_cache_name
189             {
190 0     0   0 File::Spec->catfile( home(), __PACKAGE__->Info_File_Name );
191             }
192              
193              
194 8     8   348843 method new_session_token ( Str $app_token! ) {
195             my $sig = $self->_signature( $self->user_id, $app_token );
196             my $argref = { appid => $self->app_id,
197             userid => $self->user_id,
198             sig => $sig };
199             $self->log->debug( "Creating new session token\n" );
200             my $ref = $self->call_func( account => token => $argref );
201             $ref->{token};
202             }
203              
204              
205 8     8   527888 method _signature( Str $user_id!, Str $app_token! ) {
206             md5_hex( "$user_id$app_token" );
207             }
208              
209              
210 8     8   584466 method get ( Str $type!, %param ) {
211             my $class = __PACKAGE__ . '::' . ucfirst( $type );
212             $class =~ s/s\z//;
213             eval "require $class";
214              
215             if ( $type eq 'tasks' )
216             {
217             $param{fields} ||= join ',' => $class->optional_attributes; # All fields
218             $param{start} ||= 0;
219             }
220              
221             my @things;
222             FETCH: {
223             my $ref = $self->call_func( $type => 'get', \%param );
224             my @returned = ref $ref eq 'ARRAY' ? @$ref : $ref;
225              
226             my $counter = $type eq 'tasks' ? shift @returned : ();
227             push @things, map { $class->new( %$_ ) } @returned;
228             if ( $type eq 'tasks' && @returned ) # They have a different first field
229             {
230             if ( $param{start} + $counter->{num} != $counter->{total} )
231             {
232             $self->log->debug( "Start = $param{start}, Total = $counter->{total}, "
233              
234             . " Num = $counter->{num}\n" );
235             $param{start} += $counter->{num};
236             redo FETCH;
237             }
238             }
239             } # FETCH
240              
241             @things = sort { $a->ord <=> $b->ord } @things
242             if @things && $things[0]->{ord};
243             wantarray ? @things : shift @things;
244             }
245              
246              
247             sub _make_user_agent # Might want to use Mechanize some day?
248             {
249 4     4   4687 LWP::UserAgent->new;
250             }
251              
252              
253 8     8   750420 method call_func ( Str $func!, Str $subfunc!, HashRef $argref? ) {
254             my $user_agent = $self->user_agent;
255             $argref ||= {};
256             $argref->{key} = $self->session_key if $self->session_key;
257             $self->log->debug( "Calling function $func/$subfunc\n" );
258             my %encoded_args = map { $_, arg_encode( $argref->{$_} ) }
259             keys %$argref;
260             my $res = $user_agent->post( "$ROOT_URL$func/$subfunc.php",
261             \%encoded_args );
262             $res->code != 200
263             and $self->log->logdie( "Unable to contact Toodledo\n");
264             my $ref = decode_json( $res->content )
265             or $self->log->logdie( "Content invalid\n");
266              
267             $self->log->logdie( $ref->{errorCode} == 500 ? "Toodledo offline\n"
268             : "Error: " . $ref->{errorDesc})
269             if ref $ref eq 'HASH' && $ref->{errorCode};
270             $ref;
271             }
272              
273              
274 8     8   563457 method select ( ArrayRef[Object] $o_ref, Str $expr ) {
275             my $prototype = $o_ref->[0] or return;
276              
277             # XXX CODE SMELL: refactor to polymorphic method
278             if ( ref( $prototype ) =~ /task/i )
279             {
280             $expr =~ s/(.*)/($1) && completed == 0/ unless $expr =~ /completed/;
281             }
282              
283             $expr =~ s/\b$_\b/\$self->$_/g for $prototype->attribute_list;
284             $self->log->debug( "Searching in " . @$o_ref . "objects for '$expr'\n" );
285             my $selector = sub { my $self = shift; eval $expr };
286             $self->grep_objects( $o_ref, $selector );
287             }
288              
289              
290 8     8   557410 method grep_objects ( ArrayRef[Object] $o_ref, CodeRef $selector ) {
291             grep { $selector->( $_ ) } @$o_ref;
292             }
293              
294              
295 8     8   754139 method foreach ( ArrayRef[Object] $o_ref, CodeRef $callback, @args ) {
296             for ( @$o_ref )
297             {
298             $callback->( $_, @args );
299             $App::Toodledo::Task::can_use_cache = 1;
300             }
301             }
302              
303              
304             # @args here is just for testing purposes. If used for real code, will
305             # produce unexpected and erroneous results.
306 8     8   368510 method get_tasks_with_cache ( @args ) {
307             $self->task_cache_valid and return $self->task_cache->tasks;
308             # -1 => Completed & uncompleted tasks
309             my @tasks = $self->get( tasks => comp => -1, @args );
310             $self->store_tasks_in_cache( @tasks );
311             @tasks;
312             }
313              
314              
315 8     8   162596 method task_cache_valid () {
316             my $ai = $self->account_info;
317             unless ( $self->task_cache )
318             {
319             $self->task_cache( App::Toodledo::TaskCache->new );
320             return unless $self->task_cache->exists;
321             $self->task_cache->fetch;
322             }
323              
324             my $fetched = $self->task_cache->last_updated;
325             my $logstr = "Edited: " . localtime( $ai->lastedit_task )
326             . ", Deleted: " . localtime( $ai->lastdelete_task )
327             . " Fetched: " . localtime( $fetched );
328             if ( $ai->lastedit_task >= $fetched || $ai->lastdelete_task >= $fetched )
329             {
330             $self->log->debug( "Task cache invalid ($logstr)\n" );
331             return;
332             }
333             $self->log->debug( "Task cache valid ($logstr)\n" );
334             return 1;
335             }
336              
337              
338 8     8   397293 method store_tasks_in_cache ( App::Toodledo::Task @tasks ) {
339             $self->task_cache or $self->task_cache( App::Toodledo::TaskCache->new );
340             $self->task_cache->store( @tasks );
341             }
342              
343              
344             # Add a new whatever
345 8     8   350266 method add( Object $object! ) {
346             $object->add( $self );
347             }
348              
349              
350 8     8   563611 method edit ( Object $object, @more ) {
351             $object->edit( $self, @more );
352             }
353              
354              
355             # Remove a whatever... it needs only have the id field populated
356 8     8   353437 method delete( Object $object ) {
357             $object->delete( $self );
358             }
359              
360              
361 8     8   525709 method readable ( Object $object, Str $attribute ) {
362             my $value = $object->$attribute;
363             if ( $attribute =~ /date\z/ )
364             {
365             $value or return '';
366             return preferred_date_format( $self->account_info->dateformat, $value );
367             }
368             $value;
369             }
370              
371              
372             1;
373              
374             __END__
375              
376             =head1 NAME
377              
378             App::Toodledo - Interacting with the Toodledo task management service.
379              
380             =head1 SYNOPSIS
381              
382             use App::Toodledo;
383              
384             my $todo = App::Toodledo->new( user_id => 'rudolph', app_id => 'MyAppID' );
385             $todo->login( password => 'secret', app_token => 'api2729372' )
386              
387             $todo = App::Toodledo->new( app_id => 'MyAppID' );
388             $todo->login_from_rc;
389              
390             my @folders = $todo->get( 'folders' );
391             my @tasks = $todo->get_tasks_with_cache;
392             my $time = time;
393              
394             # Tasks due in next day
395             my @wanted = $todo->select( \@tasks,
396             "duedate < $time + $ONEDAY && duedate > $time" );
397             my @privates = $todo->select( \@folders, "private > 0" );
398              
399             $todo->foreach( \@tasks, \&manipulate );
400             $todo->edit( @tasks );
401              
402             =head1 DESCRIPTION
403              
404             Toodledo (L<http://www.toodledo.com/>) is a web-based capability for managing
405             to-do lists along Getting Things Done (GTD) lines. This module
406             provides a Perl-based access to its API.
407              
408             B<This version is a minimal port to version 2 of the Toodledo API.
409             It is not at all backwards compatible with version 0.07 or earlier of this
410             module.>
411             Toodledo now frowns upon using version 1 of the API; not using an
412             application token makes it almost impossible to get anything useful
413             done.
414              
415             What do you need the API for? Doesn't the web interface do everything
416             you want? Not always. See the examples included with this distribution.
417             For instance, Toodledo has only one level of notification and it's either
418             on or off. With the API you can customize the heck out of notification.
419             Or suppose you want to find tasks where the due date has erroneously
420             been set to before the start date. Toodledo lets you do this and the
421             online search function can't find them. But with C<App::Toodledo> it's
422             as simple as:
423              
424             say $_->title for $todo->select( \@tasks => q{duedate && startdate > duedate} )
425              
426             This is a very basic, preliminary Toodledo module. I wrote it to do the
427             few things I wanted out of an API and when I feel a need for some
428             additional capability, I'll add it. In the mean time, if there's something
429             you want it to do, feel free to submit a patch. Or, heck, if you're
430             sufficiently motivated, I'll let you take over the whole thing.
431              
432             This module uses L<MooseX::Method::Signatures> to perform argument validation.
433             If you violate the type checking you will quite probably get upwards of a
434             hundred lines of error messages. That's the way it goes.
435              
436             =head1 METHODS
437              
438             =head2 $todo = App::Toodledo->new( %option );
439              
440             Construct a new Toodledo handle. No connection to the service is made.
441             Options are:
442              
443             =over 4
444              
445             =item app_id
446              
447             Application ID. See the Toodledo API documentation for details.
448              
449             =item app_token
450              
451             Application token.
452              
453             =item user_id
454              
455             User ID.
456              
457             =back
458              
459             The app_id entry in the option hash is mandatory. The others may be
460             left out and supplied elsewhere.
461              
462             =head2 $todo->get_session_token( user_id => $user_id, app_token => $app_token )
463              
464             This call creates a session token and caches it in a file in your home
465             directory called C<.toodledo_token>, unless that file already exists and
466             contains a token younger than three hours, in which case
467             that one will be used. The
468             published lifespan of a Toodledo token is four hours. The
469             C<$app_token> must be the token given to you by the Toodledo site when
470             you registered the application that this code is running. The user_id is
471             the long string on your Toodledo account's "Settings" page.
472              
473             If the user_id is not supplied here it must have been given in the
474             constructor. Ditto for the app_token.
475              
476             =head2 $todo->get_session_token_from_rc( [ $user_id ] )
477              
478             Same as C<get_session_token>, only it obtains the arguments
479             from a YAML file in your home directory called C<.toodledorc>.
480             See the FILES section below for instructions on how to format
481             and populate that file. If no C<user_id> is specified it will
482             look for and use a C<default_user_id> in the .toodledorc file.
483              
484             =head2 $todo->login( %option )
485              
486             The C<%option> hash must include the entries for C<password>
487             and C<app_token>. Optionally it can include C<user_id>; if not
488             specified here, it must have been sent in the constructor.
489              
490             =head2 $todo->login_from_rc( [$user_id] )
491              
492             Optionally specify the user_id, else the same rules apply as for
493             C<get_session_token_from_rc>. The password will be taken from the
494             one associated with that user_id in the .toodledorc file.
495              
496             =head2 $todo->call_func( $function, $subfunction, $argref )
497              
498             Low-level Toodledo API access. You should not need to use this unless
499             you're extending the App::Toodledo::Account functionality. (Please
500             contribute patches.) C<$argref> is a hashref of arguments to the
501             call. Refer to the Toodledo API documentation for formatting and
502             encoding.
503              
504             =head2 $app_token = $todo->app_token_of( $app_id )
505              
506             Convenience function for returning the application token of a given
507             application id by reading it from the .toodledorc file.
508              
509             =head2 $password = $todo->password_of( $user_id )
510              
511             Convenience function for returning the password for a given
512             user_id by reading it from the .toodledorc file.
513              
514             =head2 $user_id = $todo->default_user_id
515              
516             Convenience function for returning the default user_id by
517             reading it from the .toodledorc file.
518              
519             $token = $todo->new_session_token( $app_token )
520              
521             Return the temporary session token given the application token.
522             The user_id and app_id are read from the object.
523              
524             =head2 @objects = $todo->get( $type )
525              
526             Fetch and return a list of some kind of thing, the choices being the following
527             strings:
528              
529             =over 4
530              
531             =item tasks
532              
533             =item folders
534              
535             =item goals
536              
537             =item contexts
538              
539             =item notebooks
540              
541             =back
542              
543             The returned list will be of the corresponding App::Toodledo::I<whatever>
544             objects. There are optional arguments for tasks:
545              
546             =head2 @tasks = $todo->get( tasks => %param )
547              
548             The optional named parameters correspond to the parameters that can be
549             specified in the Toodledo tasks/get API call: modbefore, modafter,
550             comp, start, num, fields. Note that this call will not cache the
551             tasks returned, so it is safe to play with these parameters. This
552             method will default C<fields> to all available fields. It does I<not>
553             change C<comp>, which the Toodledo API defaults to all uncompleted tasks
554             only.
555              
556             =head2 @tasks = $todo->get_tasks_with_cache( %param )
557              
558             Same as get( tasks => %param ), except that the tasks are fetched from
559             the cache file C<~/.toodledo_task_cache> if it is still valid (Toodledo
560             reports no changes since cache update). If there is no cache file, it
561             is populated after the tasks are fetched from Toodledo. This fetches
562             all tasks, including completed ones, so can take a while.
563              
564             =head2 $id = $todo->add( $object )
565              
566             The argument should be a new App::Toodledo::I<whatever> object to be created.
567             The result is the id of the new object. Any of the standard object types
568             can be added.
569             Note: this method is overridden in App::Toodledo::Task.
570              
571             =head2 $todo->delete( $object )
572              
573             Delete the given object from Toodledo. The C<id> attribute of the object
574             must be correctly set. No other attributes will be used.
575             Note: this method is overridden in App::Toodledo::Task.
576              
577             =head2 $todo->edit( $object )
578              
579             The given object will be updated in Toodledo to match the one passed.
580             Note: this method is overridden in App::Toodledo::Task. When the object
581             is a task, the signature is:
582              
583             =head2 $todo->edit( $task, [@tasks] )
584              
585             All of the tasks will be edited. You are responsible for ensuring
586             that you do not exceed Toodledo limits on the number of tasks passed
587             (currently 50).
588              
589             =head2 @objects = $todo->select( \@objects, $expr );
590              
591             Select just the objects you need from the given array, based upon the
592             expression. Any attribute of the given objects specified in the exprssion
593             will br turned into an object accessor for that attribute and the resulting
594             expression must be syntactically correct. Any Perl code can be used; it will
595             be passed through C<eval>. Examples:
596              
597             =over 4
598              
599             =item tag eq "garden" && status > 3
600              
601             Must have the 'garden' tag (and only that tag) amd a status greater
602             than the index for the status value 'Planning'. (Only makes sense for
603             a task list.) To access (or change) the status as a string,
604             use C<status_str>.
605              
606             =item title =~ /deliver/i && comp == 1
607              
608             Title must match regex and task must be completed.
609              
610             =back
611              
612             The type of object is determined from the first one in the arrayref.
613              
614             =head2 @objects = $todo->grep_objects( \@objects, $coderef )
615              
616             Run $coderef for each object in the list. Called by the
617             C<select> method but can be used by the user. Ones for which
618             the C<$coderef> returns true will be passed through to the result.
619              
620             =head2 $todo->foreach( \@objects, $coderef, [@args] )
621              
622             Run the coderef on each object in the arrayref. C<$coderef>
623             will be called with the object as the first argument and
624             any C<@args> as the rest.
625              
626             =head2 $todo->readable( $object, $attribute )
627              
628             Currently just looks to see if the given C<$attribute> of C<$object>
629             is a date and if so, returns the C<preferred_date_formst> string
630             from L<App::Toodledo::Util> instead of the stored epoch second count.
631             If the date is null, returns an empty string (rather than the Toodledo
632             display of "no date").
633              
634             =head1 ERRORS
635              
636             Any API call may croak if it returns an error.
637              
638             =head1 FILES
639              
640             =head2 ~/.toodledo_token
641              
642             This file is in YAML format and caches the session token for one or
643             more application ids. You should not need to edit it.
644              
645             =head2 ~/.toodledorc
646              
647             This file is in YAML format and is where you keep information to save
648             having to enter it in login calls. It is not written by App::Toodledo.
649             It is of the following format:
650              
651             ---
652             app_tokens:
653             <app_id>: <app_token>
654             default_user_id: <user_id>
655             passwords:
656             <user_id>: <password>
657              
658             The app_id line may be repeated for as many application ids that you have.
659             It supplies the application token corresponding to each app_id. Since
660             the app_id is a mnemonic string like 'cpantest' and the app_token is
661             a hex identifier supplied by Toodledo like 'api4e49ce90e5c31', this saves
662             the trouble of copying arcane strings into every program.
663             The password line may be repeated for as many user ids that you want
664             to manage.
665             The default_user_id is optional and will be used if none is specified
666             in a login call.
667              
668             =head2 ~/.toodledo_task_cache
669              
670             This file is in YAML format and is used by App::Toodledo to store a
671             cache of tasks. You should not need to edit it. If App::Toodledo is
672             using this cache and you believe it to be invalid, delete this file.
673              
674             =head1 ENVIRONMENT
675              
676             App::Toodledo uses log4perl for error logging and debug messages. By
677             default they will be outputted to STDOUT, and STDERR. A log4perl can
678             be specified in the users application, if one is not set App::Toodledo
679             will use Log::Log4perl::easy_init($ERROR); Setting the environment
680             variable C<APP_TOODLEDO_DEBUG> will cause debugging-type information
681             to be output to log4perl logger. If a logger hasn't been set
682             App::Toodledo will use Log::Log4perl::easy_init($DEBUG);
683              
684              
685             =head1 AUTHOR
686              
687             Peter J. Scott, C<< <cpan at psdt.com> >>
688              
689             =head1 CONTRIBUTORS
690              
691             Thanks to Edward Ash for the Log4Perl integration!
692              
693             =head1 BUGS
694              
695             Please report any bugs or feature requests to
696             C<bug-app-toodledo at rt.cpan.org>, or through
697             the web interface at
698             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=App-Toodledo>.
699             I will be notified, and then you'll
700             automatically be notified of progress on your bug as I make changes.
701              
702             Realistically, I am not likely to have the time to respond to any bug
703             reports that don't impact code I use personally unless they include
704             complete fixes in the form of a patch file. New functionality should
705             include documentation and test patches.
706              
707             =head1 TODO
708              
709             Help improve App::Toodledo! Some low-hanging fruit you might want to
710             submit a patch for:
711              
712             =over 4
713              
714             =item *
715              
716             Improve task caching to not be all-or-nothing. Use SQLite and check
717             only for which tasks need to be added or removed.
718              
719             =item *
720              
721             Bulk addition of tasks. (Bulk editing is enabled but currently
722             undocumented - see App::Toodledo::Task::edit.)
723              
724             =item *
725              
726             Separate task cache age testing from loading the whole cache, takes
727             too long.
728              
729             =item *
730              
731             Flesh out the L<App::Toodledo::Account> class with the methods
732             for querying an account.
733              
734             =item *
735              
736             Handling of the *date/*time attributes needs to be coordinated
737             so it is useful.
738              
739             =back
740              
741             =head1 EXAMPLES
742              
743             To find all tasks with the 'Home' context and add a 'DIY' tag if not there:
744              
745             use App::Toodledo;
746             my $todo = App::Toodledo->new( app_id => 'myregisteredappid' );
747             $todo->login_from_rc;
748             my @all_tasks = $todo->get_tasks_from_cache;
749             for my $task ( $todo->select( \@tasks, 'context eq "Home" ) )
750             {
751             next if $task->has_tag( 'DIY' );
752             $task->tag( $task->tag . ',DIY' );
753             $todo->edit( $task );
754             }
755              
756             Feel free to contribute more examples as short and complete as that one
757             via email!
758              
759             =head1 OBJECT MODEL
760              
761             App::Toodledo is Moose-based. Each of the object types (task, folder,
762             context, goal, location, notebook, and the account singleton) is
763             represented via an object class App::Toodledo::Task, App::Toodledo::Folder,
764             etc. Each one of those classes contains each Toodledo attribute as
765             a writable attribute, e.g. $task->context( 12345 ). Additional methods
766             can be added (e.g., 'tags' is a simple convenience method for
767             App::Toodledo::Task) in each of those classes; the Toodledo attributes
768             are handled by being delegated to an internal object (e.g.,
769             App::Toodledo::TaskInternal) which implements a Role (e.g.,
770             App::Toodledo::TaskRole) that contains precisely and only the list
771             of Toodledo attributes. (See L<App::Toodledo::Task> for details
772             on the C<context_name> method for accessing contexts via their names
773             instead of IDs.)
774              
775             Therefore when Toodledo changes its attribute lists, change only
776             the corresponding Role class and everything will continue working.
777             You can add methods to the object class (e.g. App::Toodledo::Task)
778             without the class being cluttered with native Toodledo attributes.
779             You can override a Toodledo attribute if you ensure that the
780             base functionality still works; just call the method in the
781             delegated object directly. (This delegate is named 'object'.)
782              
783             =head1 DISCLAIMER
784              
785             THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
786             APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
787             HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM 'AS IS' WITHOUT
788             WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
789             LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
790             A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
791             PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
792             DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
793             CORRECTION.
794              
795             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
796             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
797             CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
798             INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
799             ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
800             NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
801             LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
802             TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
803             PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
804              
805              
806             =head1 SUPPORT
807              
808             You can find documentation for this module with the perldoc command:
809              
810             perldoc App::Toodledo
811              
812             You can also look for information at:
813              
814             =over 4
815              
816             =item * RT: CPAN's request tracker
817              
818             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=App-Toodledo>
819              
820             =item * AnnoCPAN: Annotated CPAN documentation
821              
822             L<http://annocpan.org/dist/App-Toodledo>
823              
824             =item * CPAN Ratings
825              
826             L<http://cpanratings.perl.org/d/App-Toodledo>
827              
828             =item * Search CPAN
829              
830             L<http://search.cpan.org/dist/App-Toodledo/>
831              
832             =back
833              
834             =head1 SEE ALSO
835              
836             Toodledo API documentation: L<http://api.toodledo.com/2/account/>.
837              
838             Getting Things Done, David Allen, ISBN 978-0142000281.
839              
840             =head1 COPYRIGHT & LICENSE
841              
842             Copyright 2009 - 2012 Peter J. Scott, all rights reserved.
843              
844             This program is free software; you can redistribute it and/or modify it
845             under the same terms as Perl itself.
846              
847             =cut