File Coverage

blib/lib/WWW/SourceForge/Project.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package WWW::SourceForge::Project;
2 3     3   117416 use strict;
  3         7  
  3         130  
3 3     3   2236 use WWW::SourceForge;
  0            
  0            
4             use WWW::SourceForge::User;
5             use LWP::Simple;
6             use Data::Dumper;
7              
8             our $VERSION = '0.40';
9             our $DEFAULT_ICON = 'http://a.fsdn.com/con/img/project_default.png';
10              
11             =head2 new
12              
13             Usage:
14            
15             my $proj = new WWW::SourceForge::Project( id => 1234 );
16             my $proj2 = new WWW::SourceForge::Project( name => 'flightics' );
17              
18             my @admins = $proj->admins(); # WWW::SourceForge::User objects
19             my @developers = $proj->developers(); # Ditto
20              
21             Returns: WWW::SourceForge::Project object;
22              
23             =cut
24              
25             sub new {
26              
27             my ( $class, %parameters ) = @_;
28             my $self = bless( {}, ref($class) || $class );
29              
30             my $api = new WWW::SourceForge;
31             $self->{api} = $api;
32              
33             my $json;
34             if ( $parameters{id} ) {
35             $json = $api->call(
36             method => 'project',
37             id => $parameters{id}
38             );
39             } elsif ( $parameters{name} ) {
40             $json = $api->call(
41             method => 'project',
42             name => $parameters{name}
43             );
44             } else {
45             warn('You must provide an id or name. Bad monkey.');
46             return 0;
47             }
48              
49             $self->{data} = $json->{Project};
50             return $self;
51             }
52              
53             =head2 admins
54              
55             @admins = $project->admins();
56              
57             Returns a list of WWW::SourceForge::User objects which are the admins on this
58             project.
59              
60             =cut
61              
62             sub admins {
63             my $self = shift;
64             return @{ $self->{data}->{_admins} } if ref( $self->{data}->{_admins} );
65              
66             my @admins;
67              
68             my $a_ref = $self->{data}->{maintainers};
69             foreach my $u_ref ( @$a_ref ) {
70             my $user = new WWW::SourceForge::User( username => $u_ref->{name} );
71             push @admins, $user;
72             }
73              
74             $self->{data}->{_admins} = \@admins;
75             return @admins;
76             }
77              
78             =head2 developers
79              
80             @devs = $project->devs();
81              
82             Returns a list of WWW::SourceForge::User objects which are the developers on
83             the project. This does not include the admins.
84              
85             =cut
86              
87             sub developers { # not admins
88             my $self = shift;
89             return @{ $self->{data}->{_developers} } if ref( $self->{data}->{_developers} );
90              
91             my @devs;
92              
93             my $a_ref = $self->{data}->{developers};
94             foreach my $u_ref ( @$a_ref ) {
95             my $user = new WWW::SourceForge::User( username => $u_ref->{name} );
96             push @devs, $user;
97             }
98              
99             $self->{data}->{_developers} = \@devs;
100             return @devs;
101             }
102              
103             =head2 users
104              
105             All project users - admins and non-admins.
106              
107             =cut
108              
109             sub users {
110             my $self = shift;
111              
112             my @users = ( $self->admins(), $self->developers() );
113             return @users;
114             }
115              
116             =head2 files
117              
118             List of recent released files
119              
120             =cut
121              
122             sub files {
123             my $self = shift;
124              
125             return @{ $self->{data}->{files} } if $self->{data}->{files};
126             my %args = @_;
127            
128             my $api = new WWW::SourceForge;
129             # http://sourceforge.net/api/file/index/project-id/14603/crtime/desc/rss
130              
131             # Passing a full uri feels evil, but it's necessary because the file
132             # api cares about argument order.
133             my $files = $self->{api}->call(
134             uri => '/file/index/project-id/' . $self->id() . '/crtime/desc/rss',
135             format => 'rss',
136             );
137              
138             my @files;
139             foreach my $f ( @{ $files->{entries} } ) {
140             push @files, $f->{entry};
141             }
142             $self->{data}->{files} = \@files;
143             return @files;
144             }
145              
146             =head2 latest_release
147              
148             Date of the latest released file. It's a string. The format is pretty
149             much guaranteed to change in the future. For example, it'll probably be
150             a DateTime object.
151              
152             =cut
153              
154             sub latest_release {
155             my $self = shift;
156             my @files = $self->files();
157             return $files[0]->{pubDate}; # TODO This is an object, and
158             # presumably I should be calling
159             # object methods.
160             }
161              
162             =head2 downloads
163              
164             Download counts for the specified date range. If no date range is
165             supplied, assume the 7 days leading up to today.
166              
167             WARNING: This API is subject to change any moment. The downloads API
168             gives us a LOT more information than just a count, and it may be that we
169             want to expose all of it later one. Right now I just want a count.
170              
171             my $dl_count = $project->downloads(
172             start_date => '2012-07-01',
173             end_date -> '2012-07-25'
174             );
175              
176             =cut
177              
178             # https://sourceforge.net/projects/xbmc/files/stats/json?start_date=2010-05-01&end_date=2010-05-11
179             sub downloads {
180             my $self = shift;
181             my %args = @_;
182              
183             my $data_api = $self->{data_api} || WWW::SourceForge->new( api => 'download' );
184             $self->{data_api} = $data_api;
185              
186             my $json = $data_api->call( %args, project => $self->shortdesc() );
187              
188             return $json->{summaries}->{time}->{downloads};
189             }
190              
191             =head2 logo
192              
193             For Allura projects, the logo is at https://sourceforge.net/p/PROJECT/icon
194             For Classic projects, who the heck knows?
195              
196             WARNING WARNING WARNING
197             This method will break the next time SF redesigns the project summary
198             page. On the other hand, by then all projects will be Allura projects,
199             and the else clause will never execute.
200             WARNING WARNING WARNING
201              
202             =cut
203              
204             sub logo {
205             my $self = shift;
206             my %args = @_;
207              
208             if ( $self->type == 10 ) {
209             my $icon = 'http://sourceforge.net/p/' . $self->shortdesc() . '/icon';
210              
211             # Need to verify that it's actually there
212             my $verify = get( $icon );
213             return $verify
214             ? $icon
215             : $DEFAULT_ICON;
216             } else {
217              
218             # Screen scrape to get the icon
219             # my $psp_content = get( $self->psp() );
220             my $psp_content = $self->_psp_content();
221              
222             my $m = $1 if $psp_content =~ m/img itemscope.*? Icon" src="(.*?)"/s;
223             my $icon =
224             $m
225             ? 'http:' . $m
226             : $DEFAULT_ICON;
227             return $icon;
228             }
229             }
230              
231             # Fetch and cache PSP contents
232             sub _psp_content {
233             my $self = shift;
234             unless ( $self->{psp_content} ) {
235             $self->{psp_content} = get( $self->psp() ) || '';
236             }
237             return $self->{psp_content};
238             }
239              
240             # Alias
241             sub icon {
242             my $self = shift;
243             return $self->logo( @_ );
244             }
245              
246             =head2 summary
247              
248             Returns summary statement of project, if any.
249              
250             WARNING WARNING WARNING
251             This method relies on particular HTML IDs, and so will break the next
252             time the site is redesigned. Hopefully by then this will be directly
253             available in the API.
254             WARNING WARNING WARNING
255              
256             =cut
257              
258             sub summary {
259             my $self = shift;
260             my $psp_content = $self->_psp_content();
261              
262             my $summary = $1 if $psp_content =~ m!id="summary">(.*?)

!s;
263             $summary =~ s/^\s+//; $summary =~ s/\s+$//;
264             return $summary;
265             }
266              
267             # Project Summary Page URL
268             sub psp {
269             my $self = shift;
270             return 'http://sourceforge.net/projects/'.$self->shortdesc();
271             }
272              
273             # Alias to shortdesc
274             sub unix_name {
275             my $self = shift;
276             return $self->shortdesc();
277             }
278              
279             =head2 activity
280              
281             Contents of the project activity RSS feed. It's an array, and each item
282             looks like
283              
284             {
285             'pubDate' => 'Tue, 12 Jun 2012 19:33:05 +0000',
286             'title' => 'sf-robot changed the public information on the Flight ICS project',
287             'link' => 'http://sourceforge.net/projects/flightics',
288             'description' => 'sf-robot changed the public information on the Flight ICS project'
289             }
290              
291             =cut
292              
293             sub activity {
294             my $self = shift;
295              
296             # Cached
297             return @{ $self->{data}->{activity} } if $self->{data}->{activity};
298              
299             my $rss = $self->{api}->call(
300             method => 'proj_activity',
301             project => $self,
302             );
303            
304             my @activity;
305             foreach my $e ( @{ $rss->{entries} } ) {
306             push @activity, $e->{entry};
307             }
308             $self->{data}->{activity} = \@activity;
309             return @activity;
310             }
311              
312             =head2 Data access AUTOLOADER
313              
314             Handles most of the data access for the Project object. Some parts of
315             the data require special treatment.
316              
317             =cut
318              
319             sub AUTOLOAD {
320             my $self = shift;
321             our $AUTOLOAD;
322             my $sub = $AUTOLOAD;
323             $sub =~ s/^.*:://;
324             ( my $method = $sub ) =~ s/.*:://;
325             return $self->{data}->{$sub};
326             }
327              
328             =head1 NAME
329              
330             WWW::SourceForge::Project - SourceForge project objects
331              
332             =head1 SYNOPSIS
333              
334             Uses the SourceForge API to load project details. This is a work in
335             progress, and the interface will change. Mostly I'm just poking about to
336             see what this needs to support. Please feel free to play along.
337              
338             http://sf.net/projects/sfprojecttools/
339              
340             =head1 DESCRIPTION
341              
342             Implements a Perl interface to SourceForge projects. See http://sourceforge.net/p/forge/documentation/API/
343              
344             =head1 USAGE
345              
346             use WWW::SourceForge::Project;
347             my $project = WWW::SourceForge::Project->new( name => 'moodle' );
348             print $project->id();
349             print $project->type();
350             print $project->status();
351             print $project->latest_release();
352              
353             See the 'project_details.pl' script in scripts/perl/ for more details.
354              
355             =head1 BUGS
356              
357             None
358              
359             =head1 SUPPORT
360              
361             http://sourceforge.net/p/sfprojecttools/tickets/
362              
363             =head1 AUTHOR
364              
365             Rich Bowen
366             CPAN ID: RBOW
367             SourceForge
368             rbowen@sourceforge.net
369             http://sf.net
370              
371             =head1 COPYRIGHT
372              
373             This program is free software; you can redistribute
374             it and/or modify it under the same terms as Perl itself.
375              
376             The full text of the license can be found in the
377             LICENSE file included with this module.
378              
379              
380             =head1 SEE ALSO
381              
382             perl(1).
383              
384             =cut
385              
386             #################### main pod documentation end ###################
387              
388             1;