File Coverage

blib/lib/WebService/Basecamp.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package WebService::Basecamp;
2              
3 1     1   117303 use strict;
  1         2  
  1         46  
4 1     1   7772 use LWP::UserAgent;
  1         329227  
  1         40  
5 1     1   476 use XML::Simple;
  0            
  0            
6              
7             our $VERSION = 0.1.4;
8              
9             =pod
10              
11             =head1 NAME
12              
13             WebService::Basecamp - Perl interface to the Basecamp API webservice
14              
15             =head1 SYNOPSIS
16              
17             use WebService::Basecamp;
18            
19             my $bc = WebService::Basecamp->new( url => 'http://mysite.clientsection.com',
20             user => 'username',
21             pass => 'password' );
22              
23             my $test = $bc->ping || die $bc->error();
24              
25             my $projects = $bc->projects; # a list of all projects
26              
27              
28             =head1 DESCRIPTION
29              
30             Basecamp is a web based project collaboration tool that makes it simple to
31             communicate and collaborate on projects. Basecamp is built on the Ruby on Rails
32             platform but provides a webservice API to many of the application functions.
33             WebService::Basecamp is a Perl interface to the Basecamp web service API.
34              
35             For more information on Basecamp, visit the Basecamp website.
36             http://www.basecamphq.com.
37              
38             This module does much of the heavy lifting for you when accessing the Basecamp
39             API. Once initialising a WebService::Basecamp object you can access the API
40             function via method calls. The module takes care of the creation and parsing of
41             the XML (using XML::Simple) that relays the data across the web service, however
42             there is an option to access the XML directly (see new()).
43              
44             The documentation for this module is based on the Basecamp API docs available at
45             http://www.basecamphq.com/api. It is recommended you read the official docs to
46             become familiar with the data reference.
47              
48             =head1 METHODS
49              
50             =over 4
51              
52             =item new(url => $url, user => $username, pass => $password, [ xml => $xml ])
53              
54             Call new() to create a new Basecamp object. You must pass the url of your
55             Basecamp account, a username and password.
56              
57             my $bc = WebService::Basecamp->new( url => 'http://mysite.clientsection.com',
58             user => $username,
59             pass => $password );
60              
61             By default, all methods return a data reference. If you would prefer to receive
62             the raw XML from the webservice you can pass the 'xml' parameter. E.g.
63              
64             my $bc = WebService::Basecamp->new( url => 'http://mysite.clientsection.com',
65             user => $username,
66             pass => $password,
67             xml => 1 );
68              
69             =cut
70              
71             sub new {
72             my $class = shift;
73             my %hash = @_;
74             if (!defined($hash{'url'}) || !defined($hash{'user'}) || !defined($hash{'pass'}) ) {
75             die "Must define a url, user and pass to initialise object";
76             }
77             my $self = { _burl => $hash{'url'},
78             _buser => $hash{'user'},
79             _bpass => $hash{'pass'},
80             _xml => $hash{'xml'}
81             };
82             return bless($self, $class);
83             }
84              
85             ###############################################################
86             #
87             # ERROR MESSAGES
88             #
89              
90             =pod
91              
92             =item error()
93              
94             Returns any error messages as a string.
95              
96             =cut
97              
98             sub error {
99             return shift->{'_error'};
100             }
101              
102             ###############################################################
103             #
104             # CONNECTION TEST
105             #
106              
107             =pod
108              
109             =item ping()
110              
111             Tests the connection with the Basecamp web service. Returns 1 for success.
112              
113             =cut
114              
115             sub ping {
116             my $self = shift;
117             my $result = $self->projects ? 1 : 0;
118             return $result;
119             }
120              
121             ###############################################################
122             #
123             # GENERAL QUERIES
124             #
125              
126             =pod
127              
128             =back
129              
130             =head2 General Queries
131              
132             =over 4
133              
134             =item projects([$key])
135              
136             This will return a list of all active, on-hold, and archived
137             projects that you have access to. The list is not ordered.
138              
139             This method returns a reference to a hash containing an array of file category
140             names and id.
141              
142             use Data::Dumper;
143             my $projects = $bc->projects;
144             print Dumper($projects);
145              
146             returns:
147              
148             $VAR1 = [
149             {
150             'start-page' => 'all',
151             'show-writeboards' => 'false',
152             'status' => 'active',
153             'name' => 'Create World Peace',
154             'created-on' => '2004-05-31',
155             'last-changed-on' => '2004-09-07T02:49:12Z',
156             'id' => '123456',
157             'announcement' => {},
158             'show-announcement' => 'false',
159             'company' => {
160             'name' => 'Earth',
161             'id' => '888'
162             }
163             },
164             {
165             'start-page' => 'log',
166             'show-writeboards' => 'false',
167             'status' => 'active',
168             'name' => 'Basecamp CPAN Module',
169             'created-on' => '2006-07-26',
170             'last-changed-on' => '2006-07-29T04:08:34Z',
171             'id' => '654321',
172             'announcement' => {},
173             'show-announcement' => 'false',
174             'company' => {
175             'name' => 'Internal',
176             'id' => '555'
177             }
178             }
179             ];
180              
181             If you pass the optional $key parameter to the method you will
182             recieve a keyed hash of the project data. The key must be
183             either 'name' or 'id', e.g.:
184              
185             use Data::Dumper
186             my $projects = $bc->projects('name');
187             print Dumper($projects);
188            
189             returns:
190              
191             $VAR1 = [
192             'Create World Peace' => {
193             'start-page' => 'all',
194             'status' => 'active',
195             'show-writeboards' => 'false',
196             'created-on' => '2004-05-31',
197             'last-changed-on' => '2004-09-07T02:49:12Z',
198             'show-announcement' => 'false',
199             'id' => '123456',
200             'announcement' => {},
201             'company' => {
202             'name' => 'Earth',
203             'id' => '888'
204             }
205             },
206             'Basecamp CPAN Module' => {
207             'start-page' => 'log',
208             'status' => 'active',
209             'show-writeboards' => 'false',
210             'created-on' => '2006-07-26',
211             'last-changed-on' => '2006-07-29T04:08:34Z',
212             'show-announcement' => 'false',
213             'id' => '654321',
214             'announcement' => {},
215             'company' => {
216             'name' => 'Internal',
217             'id' => '555'
218             }
219             }
220             ];
221              
222             =cut
223              
224             sub projects {
225             my $self = shift;
226             my $key = shift;
227             my @keyoptions = qw(name id);
228             return 0 unless $self->_key_val($key,\@keyoptions);
229             my $qs = "/project/list";
230             return $self->_perform($qs, 'project', $key);
231             }
232              
233             =pod
234              
235             =item file_categories($project_id [,$key])
236              
237             This will return an alphabetical list of all file categories in the referenced
238             project. Requires the $project_id to be passed as an argument.
239              
240             By default this method returns a reference to an array of hashes. If you would
241             prefer a keyed hash, you can specify the optional key. The available key options
242             are 'name' or 'id'.
243              
244             =cut
245              
246             sub file_categories {
247             my $self = shift;
248             my $project_id = shift || return $self->_val_error('project');
249             my $key = shift;
250             my @keyoptions = qw(name id);
251             return 0 unless $self->_key_val($key,\@keyoptions);
252             my $qs = '/projects/'.$project_id.'/attachment_categories';
253             return $self->_perform($qs, 'attachment-category',$key);
254             }
255              
256             =pod
257              
258             =item message_categories($project_id [,$key])
259              
260             This will return an alphabetical list of all message categories in the
261             referenced project. Requires the $project_id to be passed as an argument.
262              
263             By default this method returns a reference to an array of hashes. If you would
264             prefer a keyed hash, you can specify the optional key. The available key options
265             are 'name' or 'id'.
266              
267             =cut
268              
269             sub message_categories {
270             my $self = shift;
271             my $project_id = shift || return $self->_val_error('project');
272             my $key = shift;
273             my @keyoptions = qw(name id);
274             return 0 unless $self->_key_val($key,\@keyoptions);
275             my $qs = '/projects/'.$project_id.'/post_categories';
276             return $self->_perform($qs,'post-category',$key);
277             }
278              
279              
280             ###############################################################
281             #
282             # MESSAGES AND COMMENTS
283             #
284              
285             =pod
286              
287             =back
288              
289             =head2 Messages and Comments
290              
291             =over 4
292              
293             =item comment($comment_id)
294              
295             Retrieve a specific comment by its id.
296              
297             =cut
298              
299             sub comment {
300             my $self = shift;
301             my $comment_id = shift || return $self->_val_error('comment');
302             my $qs = "/msg/comment/$comment_id";
303             return $self->_perform($qs);
304             }
305              
306             =pod
307              
308             =item comments($message_id)
309              
310             Return the list of comments associated with the specified message.
311              
312             =cut
313              
314             sub comments {
315             my $self = shift;
316             my $message_id = shift || return $self->_val_error('message');
317             my $qs = "/msg/comments/$message_id";
318             return $self->_perform($qs, 'comment');
319             }
320              
321             =pod
322              
323             =item create_comment($message_id, $comment)
324              
325             Create a new comment, associating it with a specific message. Returns a hash
326             containing all of the comment details.
327              
328             my $message_id = 1234;
329             my $comment = "This looks too easy!";
330             my $new_comment = $bc->create_comment($message_id, $comment);
331              
332             =cut
333              
334             sub create_comment {
335             my $self = shift;
336             my $message_id = shift || return $self->_val_error('message');
337             my $comment = shift;
338             my $qs = "/msg/create_comment";
339             my $xml = <
340            
341            
342             $message_id
343             $comment
344            
345            
346             XML
347             $self->{'_content'} = $xml;
348             return $self->_perform($qs);
349             }
350              
351             =pod
352              
353             =item create_message($project_id, $message)
354              
355             Creates a new message, optionally sending notifications to a selected list of
356             people. The available fields are;
357              
358             category_id - the id of the message category
359             title - message title
360             body - summary text of main message
361             extended_body - the main body of the message
362             textile - optional boolean value. Set to '1' to use Basecamp's
363             textile formatting for your message. Defaults to '0'.
364             private - optional boolean value. Set to '1' to make this message
365             visible only to the logged in user. Defaults to '0'.
366             notify - optional list of person ids. Each person in this list will
367             receive an email notification of the message.
368              
369             Returns a hash containing all of the message details.
370              
371             my $project_id = 1234;
372             my $message = { category_id => 654321,
373             title => 'New Message Title',
374             body => 'This text is a summary of the message',
375             extended_body => 'This is the main body of the message',
376             textile => 1, # optional field
377             private => 0, # optional field
378             notify => qw(1234 5678) # optional field
379             };
380             my $data = $bc->create_message($project_id,$message);
381              
382             =cut
383              
384             sub create_message {
385             my $self = shift;
386             my $project_id = shift || return $self->_val_error('project');
387             my $data = shift;
388             my $qs = "/projects/$project_id/msg/create";
389             my $category_id = int($data->{'category_id'});
390             my $milestone_id = int($data->{'milestone_id'});
391             my $title = $data->{'title'};
392             my $body = $data->{'body'};
393             my $extended_body = $data->{'extended_body'};
394             my $textile = defined $data->{'textile'} ? '1' : '0';
395             my $private = defined $data->{'private'} ? '1' : '0';
396             my $notify = $data->{'notify'};
397             my $xml = <
398            
399            
400             $category_id
401             $title
402             $body
403             $extended_body
404             $textile
405             $private
406             XML
407             if ($milestone_id) {
408             $xml .= "$milestone_id\n";
409             }
410             $xml .= "";
411             foreach my $pid (@$notify) {
412             $pid = int($pid);
413             $xml .= "$pid";
414             }
415             $xml .= "\n";
416            
417             $self->{'_content'}= $xml;
418             return $self->_perform($qs);
419             }
420              
421             =pod
422              
423             =item delete_comment($comment_id)
424              
425             Delete the comment with the given id.
426              
427             =cut
428              
429             sub delete_comment {
430             my $self = shift;
431             my $comment_id = shift || return $self->_val_error('comment');
432             my $qs = "/msg/delete_comment/$comment_id";
433             return $self->_perform($qs);
434             }
435              
436             =pod
437              
438             =item delete_message($message_id)
439              
440             Delete the specified message from the project.
441              
442             =cut
443              
444             sub delete_message {
445             my $self = shift;
446             my $message_id = shift || return $self->_val_error('message');
447             my $qs = "/msg/delete/$message_id";
448             return $self->_perform($qs);
449             }
450              
451             =pod
452              
453             =item message('$message_id, [$message_id2, $message_id3, ...]')
454              
455             This will return information about the referenced message. If the id is given as
456             a comma-delimited list, one record will be returned for each id. In this way you
457             can query a set of messages in a single request. Note that you can only give up
458             to 25 ids per request--more than that will return an error.
459              
460             =cut
461              
462             sub message {
463             my $self = shift;
464             my $message_id = shift || return $self->_val_error('message');
465             my $qs = "/msg/get/$message_id";
466             return $self->_perform($qs);
467             }
468              
469             =pod
470              
471             =item message_archive($project_id)
472              
473             This will return a summary record for each message in a project. If you specify
474             a category_id, only messages in that category will be returned. (Note that a
475             summary record includes only a few bits of information about a post, not the
476             complete record.)
477              
478             =cut
479              
480             sub message_archive {
481             my $self = shift;
482             my $project_id = shift || return $self->_val_error('project');
483             my $category_id = shift;
484             my $qs = "/projects/$project_id/msg/archive";
485             my $xml = "".int($project_id)."";
486             $xml .= "".int($category_id)."" if
487             int($category_id);
488             $xml .= "\n";
489             $self->{'_content'}= $xml;
490             return $self->_perform($qs);
491             }
492              
493             =pod
494              
495             =item message_archive_per_category($project_id, $category_id)
496              
497             This will return a summary record for each message in a particular category.
498             (Note that a summary record includes only a few bits of information about a
499             post, not the complete record.)
500              
501             =cut
502              
503             sub message_archive_per_category {
504             my $self = shift;
505             my $project_id = shift || return $self->_val_error('project');
506             my $category_id = shift;
507             return $self->_val_error('category') unless $category_id;
508             my $qs = "/projects/$project_id/msg/cat/$category_id/archive";
509             return $self->_perform($qs);
510             }
511              
512             =pod
513              
514             =item update_comment($comment_id, $comment)
515              
516             Update a specific comment. This can be used to edit the content of an existing
517             comment. Returns a hash containing all of the comment details.
518              
519             my $comment_id = 99999;
520             my $comment = "This looks too easy!!";
521             my $new_comment = $bc->update_comment($comment_id, $comment);
522              
523             =cut
524              
525             sub update_comment {
526             my $self = shift;
527             my $comment_id = shift || return $self->_val_error('comment');
528             my $comment = shift;
529             my $qs = "/msg/update_comment";
530             $self->{'_content'} =
531             "$comment_id$comment
532             ";
533             return $self->_perform($qs);
534             }
535              
536             =pod
537              
538             =item update_message($message_id, $message)
539              
540             Updates an existing message, optionally sending notifications to a selected list
541             of people. Available fields are as per the create_message method.
542              
543             Returns a hash containing all of the message details.
544              
545             =cut
546              
547             sub update_message {
548             my $self = shift;
549             my $message_id = shift || return $self->_val_error('message');
550             my $data = shift;
551             my $qs = "/msg/update/$message_id";
552             my $category_id = int($data->{'category_id'});
553             my $title = $data->{'title'};
554             my $body = $data->{'body'};
555             my $extended_body = $data->{'extended_body'};
556             my $textile = defined $data->{'textile'} ? '1' : '0';
557             my $private = defined $data->{'private'} ? '1' : '0';
558             my $notify = $data->{'notify'};
559             my $xml = "";
560             $xml .= "$category_id" if $category_id;
561             $xml .= "$title" if $title;
562             $xml .= "$body" if $body;
563             $xml .= "$extended_body" if $extended_body;
564             $xml .= "$textile";
565             $xml .= "$private";
566             $xml .= "";
567             foreach my $pid (@$notify) {
568             $pid = int($pid);
569             $xml .= "$pid";
570             }
571             $xml .= "\n";
572            
573             $self->{'_content'} = $xml;
574             return $self->_perform($qs);
575             }
576              
577              
578             ###############################################################
579             #
580             # TO-DO LISTS AND ITEMS
581             #
582              
583             =pod
584              
585             =back
586              
587             =head2 To-Do Lists and Items
588              
589             =over 4
590              
591             =item complete_item($item_id)
592              
593             Marks the specified item as "complete". If the item is already completed, this
594             does nothing.
595              
596             =cut
597              
598             sub complete_item {
599             my $self = shift;
600             my $item_id = shift || return $self->_val_error('item');
601             my $qs = "/todos/complete_item/$item_id";
602             return $self->_perform($qs);
603             }
604              
605             =pod
606              
607             =item create_item($list_id, $item_data)
608              
609             This call lets you add an item to an existing list. The item is added to the
610             bottom of the list. If a person is responsible for the item, give their id as
611             the party_id value. If a company is responsible, prefix their company id with a
612             'c' and use that as the party_id value. If the item has a person as the
613             responsible party, you can use the notify key to indicate whether an email
614             should be sent to that person to tell them about the assignment.
615              
616             my $list_id = 4321;
617             my $item_data = { content => "Turn the lights out",
618             party_id => 555,
619             notify => 1 };
620             my $new_item = $bc->create_item($list_id, $item_data);
621              
622             Returns a hash containing all of the item details.
623              
624             =cut
625              
626             sub create_item {
627             my $self = shift;
628             my $list_id = shift;
629             return $self->_val_error('list') unless $list_id;
630             my $data = shift;
631             my $qs = "/todos/create_item/$list_id";
632             my $content = $data->{'content'};
633             my $party_id = $data->{'party_id'};
634             my $notify = $data->{'notify'} ? 'true' : 'false';
635             my $xml = "$content";
636             if ($party_id) {
637             $xml .=
638             "$party_id$notify";
639             }
640             $xml .= "";
641             $self->{'_content'} = $xml;
642             return $self->_perform($qs);
643             }
644              
645             =pod
646              
647             =item create_list($project_id, $list_data)
648              
649             This will create a new, empty list. You can create the list explicitly, or by
650             giving it a list template id to base the new list off of. The available fields
651             are:
652              
653             milestone_id - optional id of an associated milestone
654             private - optional boolean value. Set to '1' to make this list visible
655             only to the logged in user. Defaults to '0'.
656             track - optional boolean value. Set to '1' to enable time tracking on
657             items in this list. Defaults to '0';
658              
659             Basecamp allows you to create list templates for easy creation of common task
660             lists. When creating a new list using this method you can provide the id of a
661             predefined list template:
662              
663             template_id - id of predefined template
664              
665             or pass the name and description for the list:
666              
667             name - list title
668             description - optional description of list
669              
670              
671             my $project_id = 654321;
672             my $list_data = { milestone_id => 5436,
673             private => 0,
674             track => 1,
675             name => 'Closing up procedures',
676             };
677             my $data = $bc->create_list($project_id,$list_data);
678              
679             =cut
680              
681             sub create_list {
682             my $self = shift;
683             my $project_id = shift || return $self->_val_error('project');
684             my $data = shift;
685             my $qs = "/projects/$project_id/todos/create_list";
686             my $milestone_id = int($data->{'milestone_id'});
687             my $private = $data->{'private'} ? 'true' : 'false';
688             my $tracked = $data->{'track'} ? 'true' : 'false';
689             my $name = $data->{'name'};
690             my $description = $data->{'description'};
691             my $template_id = int($data->{'template_id'});
692             my $xml = "";
693             $xml .= "$milestone_id" if $milestone_id;
694             $xml .= "$private$tracked";
695             if ($template_id) {
696             $xml .=
697             "true$template_id
698             >";
699             } else {
700             $xml .= "$name$description";
701             }
702             $xml .= "\n";
703             $self->{'_content'}= $xml;
704             return $self->_perform($qs);
705             }
706              
707             =pod
708              
709             =item delete_item($item_id)
710              
711             Deletes the specified item, removing it from its parent list.
712              
713             =cut
714              
715             sub delete_item {
716             my $self = shift;
717             my $item_id = shift || return $self->_val_error('item');
718             my $qs = "/todos/delete_item/$item_id";
719             return $self->_perform($qs);
720             }
721              
722             =pod
723              
724             =item delete_list($list_id)
725              
726             This call will delete the entire referenced list and all items associated with
727             it. Use it with caution, because a deleted list cannot be restored!
728              
729             =cut
730              
731             sub delete_list {
732             my $self = shift;
733             my $list_id = shift || return $self->_val_error('list');
734             my $qs = "/todos/delete_list/$list_id";
735             return $self->_perform($qs);
736             }
737              
738             =pod
739              
740             =item list($list_id)
741              
742             This will return the metadata and items for a specific list.
743              
744             =cut
745              
746             sub list {
747             my $self = shift;
748             my $list_id = shift || return $self->_val_error('list');
749             my $qs = "/todos/list/$list_id";
750             return $self->_perform($qs);
751             }
752              
753             =pod
754              
755             =item lists($project_id, [$filter], [$key])
756              
757             This will return the metadata for all of the lists in a given project. You can
758             further constrain the query to only return those lists that are "complete" (have
759             no uncompleted items) or "uncomplete" (have uncompleted items remaining).
760              
761             To receive only complete lists pass $filter = 'true'
762             To receive only incomplete lists, pass $filter = 'false'
763             To receive all lists do not pass $filter
764              
765             Available keys for this method are 'name' and 'id'. (optional)
766              
767             =cut
768              
769             sub lists {
770             my $self = shift;
771             my $project_id = shift || return $self->_val_error('project');
772             my $complete = shift;
773             my $key = shift;
774             my @keyoptions = qw(name id);
775             return 0 unless $self->_key_val($key,\@keyoptions);
776             $self->{'_content'} = "$complete" if $complete;
777             my $qs = "/projects/$project_id/todos/lists";
778             return $self->_perform($qs, 'todo-list', $key);
779             }
780              
781             =pod
782              
783             =item move_item($item_id, $position)
784              
785             Changes the position of an item within its parent list. It does not currently
786             support reparenting an item. Position 1 is at the top of the list. Moving an
787             item beyond the end of the list puts it at the bottom of the list.
788              
789             =cut
790              
791             sub move_item {
792             my $self = shift;
793             my $item_id = shift || return $self->_val_error('item');
794             my $position = shift;
795             my $qs = "/todos/move_item/$item_id";
796             $self->{'_content'}= "$position";
797             return $self->_perform($qs);
798             }
799              
800             =pod
801              
802             =item move_list($list_id, $position)
803              
804             This allows you to reposition a list relative to the other lists in the project.
805             A list with position 1 will show up at the top of the page. Moving lists around
806             lets you prioritize. Moving a list to a position less than 1, or more than the
807             number of lists in a project, will force the position to be between 1 and the
808             number of lists (inclusive).
809              
810             =cut
811              
812             sub move_list {
813             my $self = shift;
814             my $list_id = shift || return $self->_val_error('list');
815             my $position = shift;
816             my $qs = "/todos/move_list/$list_id";
817             $self->{'_content'}= "$position";
818             return $self->_perform($qs);
819             }
820              
821             =pod
822              
823             =item move_list($item_id)
824              
825             Marks the specified item as "uncomplete". If the item is already uncompleted,
826             this does nothing.
827              
828             =cut
829              
830             sub uncomplete_item {
831             my $self = shift;
832             my $item_id = shift || return $self->_val_error('item');
833             my $qs = "/todos/uncomplete_item/$item_id";
834             return $self->_perform($qs);
835             }
836              
837             =pod
838              
839             =item update_item($item_id, $item_data)
840              
841             Modifies an existing item. Available fields are as per the create_item method.
842              
843             =cut
844              
845             sub update_item {
846             my $self = shift;
847             my $item_id = shift || return $self->_val_error('item');
848             my $data = shift;
849             my $qs = "/todos/update_item/$item_id";
850             my $content = $data->{'comment'};
851             my $party_id = $data->{'party_id'};
852             my $notify = $data->{'notify'} ? 'true' : 'false';
853             my $xml = "$content";
854             if ($party_id) {
855             $xml .=
856             "$party_id$notify";
857             }
858             $xml .= "";
859             $self->{'_content'} = $xml;
860             return $self->_perform($qs);
861             }
862              
863             =pod
864              
865             =item update_list($list_id, $list_data)
866              
867             Modifies an the metadata for an existing list. Available fields are as per the
868             create_list method, with the exclusion of the template_id.
869              
870             =cut
871              
872             sub update_list {
873             my $self = shift;
874             my $list_id = shift || return $self->_val_error('list');
875             my $data = shift;
876             my $qs = "/todos/update_list/$list_id";
877             my $milestone_id = int($data->{'category_id'});
878             my $private = $data->{'private'} ? 'true' : 'false';
879             my $tracked = $data->{'track'} ? 'true' : 'false';
880             my $name = $data->{'name'};
881             my $description = $data->{'description'};
882             my $template_id = int($data->{'template_id'});
883             my $xml = "";
884             $xml .= "$name" if $name;
885             $xml .= "$description" if $description;
886             $xml .= "$milestone_id" if $milestone_id;
887             $xml .= "$private" if $private;
888             $xml .= "$tracked" if $tracked;
889             $xml .= "\n";
890             $self->{'_content'} = $xml;
891             return $self->_perform($qs);
892             }
893              
894             ###############################################################
895             #
896             # MILESTONES
897             #
898              
899             =pod
900              
901             =back
902              
903             =head2 Milestones
904              
905             =over 4
906              
907             =item complete_milestone($milestone_id)
908              
909             Marks the specified milestone as complete.
910              
911             =cut
912              
913             sub complete_milestone {
914             my $self = shift;
915             my $milestone_id = shift || return $self->_val_error('milestone');
916             my $qs = "/milestones/complete/$milestone_id";
917             return $self->_perform($qs);
918             }
919              
920             =pod
921              
922             =item create_milestone($project_id, $milestone_data)
923              
924             Creates a single milestone. If a company is responsible, prefix their company id
925             with a 'c' and use that as the party_id value. If the milestone has a person as
926             the responsible party, you can use the notify key to indicate whether an email
927             should be sent to that person to tell them about the milestone. The available
928             fields are:
929              
930             title - Title for the milestone
931             deadline - date the milestone is due to be completed. Must be in the format
932             of YYYYMMDD
933             party_id - id of a person or company responsible for the milestone. If it is
934             a company, prefix the id with a 'c', e.g. 'c123'
935             notify - optional boolean value. Set to '1' to send an email about the
936             milestone to the responsible party.
937              
938             my $project_id = 654321;
939             my $milestone_data = { title => 'Launch Party',
940             deadline => '20060828',
941             party_id => 555,
942             notify => 1 };
943             my $new_milestone = $bc->create_milestone($project_id, $milestone_data);
944              
945             Returns a hash containing all of the milestone details.
946              
947             =cut
948              
949             sub create_milestone {
950             my $self = shift;
951             my $project_id = shift || return $self->_val_error('project');
952             my $data = shift;
953             my $qs = "/projects/$project_id/milestones/create";
954             my $title = $data->{'title'};
955             my $deadline = $data->{'deadline'};
956             my $party_id = $data->{'party_id'};
957             my $notify = $data->{'notify'} ? 'true' : 'false';
958             my $xml = <
959            
960            
961             $title
962             $deadline
963             $party_id
964             $notify
965            
966            
967             XML
968             $self->{'_content'} = $xml;
969             return $self->_perform($qs);
970             }
971              
972             =pod
973              
974             =item delete_milestone($milestone_id)
975              
976             Deletes the given milestone from the project.
977              
978             =cut
979              
980             sub delete_milestone {
981             my $self = shift;
982             my $milestone_id = shift || return $self->_val_error('milestone');
983             my $qs = "/milestones/delete/$milestone_id";
984             return $self->_perform($qs);
985             }
986              
987             =pod
988              
989             =item list_milestones($project_id, [$filter])
990              
991             This lets you query the list of milestones for a project. You can either return
992             all milestones, or only those that are late, completed, or upcoming.
993              
994             To receive only complete milestones pass $filter = 'complete'
995             To receive only upcoming milesones, pass $filter = 'upcoming'
996             To receive only late milesones, pass $filter = 'late'
997             To receive all milestones do not pass $filter
998              
999             =cut
1000              
1001             sub list_milestones {
1002             my $self = shift;
1003             my $project_id = shift || return $self->_val_error('project');
1004             my $filter = shift || 'all';
1005             $self->{'_content'} = "$filter";
1006             my $qs = "/projects/$project_id/milestones/list";
1007             return $self->_perform($qs);
1008             }
1009              
1010             =pod
1011              
1012             =item uncomplete_milestone($milestone_id)
1013              
1014             Marks the specified milestone as uncomplete.
1015              
1016             =cut
1017              
1018             sub uncomplete_milestone {
1019             my $self = shift;
1020             my $milestone_id = shift || return $self->_val_error('milestone');
1021             my $qs = "/milestones/uncomplete/$milestone_id";
1022             return $self->_perform($qs);
1023             }
1024              
1025             =pod
1026              
1027             =item update_milestone($milestone_id, $milestone_data)
1028              
1029             Modifies a single milestone. You can use this to shift the deadline of a single
1030             milestone, and optionally shift the deadlines of subsequent milestones as well.
1031             The available fields are as per the create_milestone() method with the addition
1032             of two extra fields:
1033              
1034             move_upcoming - optional boolean value. Set to '1' to move subsequent
1035             milestone deadlines whne updating the deadline for this
1036             milestone
1037             move_weekends - optional boolean value. If using the 'move_upcoming'
1038             parameter, you can set this value to '1' to make sure that any
1039             subsequent milestone deadlines do not get moved to a Saturday
1040             or Sunday.
1041              
1042             my $milestone_id = 98765;
1043             my $milestone_data = { title => 'Launch Party',
1044             deadline => '20061028',
1045             party_id => 555,
1046             notify => 1,
1047             move_upcoming => 1,
1048             move_weekends => 1 };
1049             my $new_milestone = $bc->update_milestone($milestone_id, $milestone_data);
1050              
1051             Returns a hash containing all of the milestone details.
1052              
1053             =cut
1054              
1055             sub update_milestone {
1056             my $self = shift;
1057             my $milestone_id = shift || return $self->_val_error('milestone');
1058             my $data = shift;
1059             my $qs = "/milestones/update/$milestone_id}";
1060             my $title = $data->{'title'};
1061             my $deadline = $data->{'deadline'};
1062             my $party_id = $data->{'party_id'};
1063             my $notify = $data->{'notify'} ? 'true' : 'false';
1064             my $upcoming = $data->{'move_upcoming'} ? 'true' : 'false';
1065             my $weekends = $data->{'move_weekends'} ? 'true' : 'false';
1066             my $xml = "";
1067             $xml .= "$title" if $title;
1068             $xml .= "$deadline" if $deadline;
1069             $xml .= "$party_id" if $party_id;
1070             $xml .= "$notify";
1071             $xml .= "$upcoming" if
1072             $upcoming;
1073             $xml .= "" .
1074             "$weekends" if $weekends;
1075             $xml .= "";
1076             $self->{'_content'} = $xml;
1077             return $self->_perform($qs);
1078             }
1079              
1080             ###############################################################
1081             #
1082             # TIME TRACKING
1083             #
1084              
1085             =pod
1086              
1087             =back
1088              
1089             =head2 Time Tracking
1090              
1091             =over 4
1092              
1093             =item create_time($data)
1094              
1095             With this method you can create a new time entry for a particular person and
1096             project. The available fields are:
1097              
1098             project_id - id for the project associated with the task
1099             person_id - id of the person who completed the work
1100             date - date the work took place. Date format = YYYYMMDD, e.g. 20060801
1101             hours - time worked, in hours
1102              
1103             If the task being time tracked is an existing item from a to do list, you can
1104             pass the item_id:
1105              
1106             item_id - id of an existing to do list item
1107              
1108             or you can provide a description of the task
1109              
1110             description - txt description of the task
1111              
1112             my $project_id = 654321;
1113             my $person_id = 555;
1114             my $data = { project_id => $project_id,
1115             person_id => $person_id,
1116             date => '20060801',
1117             hours => '1.25',
1118             description => 'Meeting with world leaders' };
1119             my $new_time = $bc->create_time($data);
1120              
1121              
1122             =cut
1123              
1124             sub create_time {
1125             my $self = shift;
1126             my $data = shift;
1127             my $qs = "/time/save_entry";
1128             my $project_id = $data->{'project_id'};
1129             my $person_id = $data->{'person_id'};
1130             my $date = $data->{'date'};
1131             my $hours = $data->{'hours'};
1132             my $item_id = $data->{'item_id'};
1133             my $description = $data->{'description'};
1134             my $xml = <
1135            
1136            
1137             $project_id
1138             $person_id
1139             $date
1140             $hours
1141             XML
1142             if ($item_id) {
1143             $xml .= "$item_id";
1144             } else {
1145             $xml .= "$description";
1146             }
1147             $xml .= "";
1148             $self->{'_content'} = $xml;
1149             return $self->_perform($qs);
1150             }
1151              
1152             =pod
1153              
1154             =item delete_time($project_id, $time_id)
1155              
1156             Deletes the identified time entry.
1157              
1158             =cut
1159              
1160             sub delete_time {
1161             my $self = shift;
1162             my $project_id = shift || return $self->_val_error('project');
1163             my $time_id = shift || return $self->_val_error('time');
1164             my $qs = "/projects/$project_id/time/delete_entry/$time_id";
1165             return $self->_perform($qs);
1166             }
1167              
1168             =pod
1169              
1170             =item report_time($person_id, $from, $to, [$filter])
1171              
1172             This method lets you query the time entries in a variety of ways. If you do not
1173             want to query by $person_id, put a 0 in that position. Likewise for $from and
1174             $to (to use default from/to values). In no case can you query more than 90 days'
1175             worth of data. The $filter parameter can be blank, or an id number prefixed by a
1176             'p' (to filter by a specific project) or 'c' (to filter by a specific company).
1177              
1178             my $report = $bc->report_time(5,'20060101','20060207','c7');
1179            
1180             - would return all time entries for the person with id 5, for all projects
1181             associated with the company with id 7, between the dates 2006-01-01 and
1182             2006-02-07 (inclusive).
1183              
1184             Date values passed for $from and $to must be in the format of YYYYMMDD
1185              
1186             =cut
1187              
1188             sub report_time {
1189             my $self = shift;
1190             my $person_id = shift || '0';
1191             my $from = shift || '0';
1192             my $to = shift || '0';
1193             my $filter = shift;
1194             my $qs = "/time/report/$person_id/$from/$to/$filter";
1195             return $self->_perform($qs);
1196             }
1197              
1198             =pod
1199              
1200             =item update_time($time_id, $data)
1201              
1202             With this method you can modify a specific time entry. The available fields are
1203             as per the create_time() method.
1204              
1205             =cut
1206              
1207             sub update_time {
1208             my $self = shift;
1209             my $time_id = shift || return $self->_val_error('time');
1210             my $data = shift;
1211             my $qs = "/time/save_entry/$time_id";
1212             my $project_id = $data->{'project_id'};
1213             my $person_id = $data->{'person_id'};
1214             my $date = $data->{'date'};
1215             my $hours = $data->{'hours'};
1216             my $item_id = $data->{'item_id'};
1217             my $description = $data->{'description'};
1218             my $xml = "";
1219             $xml .= "$project_id" if $project_id;
1220             $xml .= "$person_id" if $person_id;
1221             $xml .= "$date" if $date;
1222             $xml .= "$hours" if $hours;
1223             $xml .= "$item_id" if $item_id;
1224             $xml .= "$description" if $description;
1225             $xml .= "";
1226             $self->{'_content'} = $xml;
1227             return $self->_perform($qs);
1228             }
1229              
1230             ###############################################################
1231             #
1232             # CONTACT MANAGEMENT
1233             #
1234              
1235             =pod
1236              
1237             =back
1238              
1239             =head2 Contact Management
1240              
1241             =over 4
1242              
1243             =item companies()
1244              
1245             Returns a list of all companies visible to the given person. This is only
1246             accessible to employees of the "firm" (the company assoicated with the account).
1247             Client employees will get a 403 response if they attempt to access this method.
1248              
1249             =cut
1250              
1251             sub companies {
1252             my $self = shift;
1253             my $qs = "/contacts/companies";
1254             return $self->_perform($qs);
1255             }
1256              
1257             =pod
1258              
1259             =item company($company_id)
1260              
1261             This will return the information for the referenced company.
1262              
1263             =cut
1264              
1265             sub company {
1266             my $self = shift;
1267             my $company_id = shift || return $self->_val_error('company');
1268             my $qs = "/contacts/company/$company_id";
1269             return $self->_perform($qs);
1270             }
1271              
1272             =pod
1273              
1274             =item people($company_id)
1275              
1276             This will return all of the people in the given company.
1277              
1278             =cut
1279              
1280             sub people {
1281             my $self = shift;
1282             my $company_id = shift || return $self->_val_error('company');
1283             my $qs = "/contacts/people/$company_id";
1284             return $self->_perform($qs);
1285             }
1286              
1287             =pod
1288              
1289             =item people_per_project($project_id, $company_id)
1290              
1291             This will return all of the people in the given company that can access the
1292             given project.
1293              
1294             =cut
1295              
1296             sub people_per_project {
1297             my $self = shift;
1298             my $project_id = shift || return $self->_val_error('project');
1299             my $company_id = shift || return $self->_val_error('company');
1300             my $qs = "/projects/$project_id/contacts/people/$company_id";
1301             return $self->_perform($qs);
1302             }
1303              
1304             =pod
1305              
1306             =item person($person_id)
1307              
1308             This will return information about the referenced person.
1309              
1310             =cut
1311              
1312             sub person {
1313             my $self = shift;
1314             my $person_id = shift || return $self->_val_error('person');
1315             my $qs = "/contacts/person/$person_id";
1316             return $self->_perform($qs);
1317             }
1318              
1319             ###############################################################
1320             #
1321             # PRIVATE METHODS
1322             #
1323              
1324             sub _perform {
1325             my $self = shift;
1326             my $qs = shift;
1327             my $list = shift;
1328             my $key = shift;
1329             my $url = $self->{'_burl'}.$qs;
1330             my $ua = LWP::UserAgent->new;
1331             my $req = HTTP::Request->new(POST => $url);
1332             $req->header('Accept' => 'application/xml');
1333             $req->content_type('application/xml');
1334             $req->authorization_basic($self->{'_buser'}, $self->{'_bpass'});
1335             $req->content_length(length($self->{'_content'}));
1336             $req->content($self->{'_content'});
1337             my $body = $ua->request($req);
1338             my $data = {};
1339             if ($body->is_success) {
1340             return $body->content if $self->{'_xml'};
1341             $data = XMLin($body->content, keyattr => [$key], NoAttr => 1, NormaliseSpace => 2);
1342             if (!$key && $list) {
1343             if (ref $data->{$list} eq 'ARRAY') {
1344             return \@{$data->{$list}};
1345             } else {
1346             return [$data->{$list}];
1347             }
1348             } elsif ($key && $list) {
1349             return $data->{$list};
1350             } else {
1351             return $data;
1352             }
1353             } else {
1354             $self->{'_error'} = $body->status_line;
1355             return 0;
1356             }
1357             return 0;
1358             }
1359              
1360             sub _val_error {
1361             my $self = shift;
1362             my $id = shift;
1363             $self->{'_error'} = "Must provide a $id id";
1364             return 0;
1365             }
1366              
1367             sub _key_val {
1368             my $self = shift;
1369             my $key = shift;
1370             my $options = shift;
1371             return 1 if (!$key);
1372             foreach my $opt (@$options) {
1373             return 1 if ($key eq $opt);
1374             }
1375             my $option = join('|',@$options);
1376             $self->{'_error'} = "Invalid key ($key). Options: [$option]";
1377             return 0;
1378             }
1379              
1380             =pod
1381              
1382             =back
1383              
1384             =head1 TODO
1385              
1386             This module does not currently support all of the Basecamp API functions. In
1387             particular, the following methods need to be added:
1388              
1389             =over 4
1390              
1391             =item
1392              
1393             File uploads
1394              
1395             =item
1396              
1397             Attaching files to messages
1398              
1399             =item
1400              
1401             batch creation of milestones
1402              
1403             =back
1404              
1405             Add more tests
1406              
1407             =head1 BUGS
1408              
1409             This is alpha software and as such, the features and interface
1410             are subject to change. So please check the Changes file when upgrading.
1411              
1412              
1413             =head1 SEE ALSO
1414              
1415             L, L
1416              
1417              
1418             =head1 AUTHOR
1419              
1420             David Baxter
1421              
1422             =head1 CREDITS
1423              
1424             Thanks to SiteSuite (http://www.sitesuite.com.au) for funding the
1425             development of this plugin and for releasing it to the world.
1426              
1427             Thanks to Patrick Mulvaney for contributions to this module.
1428              
1429             =head1 LICENCE AND COPYRIGHT
1430              
1431             Copyright (c) 2006, SiteSuite. All rights reserved.
1432              
1433             This module is free software; you can redistribute it and/or modify it under the
1434             same terms as Perl itself.
1435              
1436             =head1 DISCLAIMER OF WARRANTY
1437              
1438             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE
1439             SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
1440             STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE
1441             SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
1442             INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
1443             FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
1444             PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE,
1445             YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.
1446              
1447             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY
1448             COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE
1449             SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES,
1450             INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT
1451             OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS
1452             OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
1453             PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN
1454             IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
1455             DAMAGES.
1456              
1457             =cut
1458              
1459             1;