File Coverage

blib/lib/CPAN/Testers/WWW/Statistics.pm
Criterion Covered Total %
statement 31 33 93.9
branch n/a
condition n/a
subroutine 11 11 100.0
pod n/a
total 42 44 95.4


line stmt bran cond sub pod time code
1             package CPAN::Testers::WWW::Statistics;
2              
3 15     15   362927 use warnings;
  15         18  
  15         344  
4 15     15   49 use strict;
  15         15  
  15         333  
5 15     15   48 use vars qw($VERSION);
  15         15  
  15         707  
6              
7             $VERSION = '1.21';
8              
9             #----------------------------------------------------------------------------
10              
11             =head1 NAME
12              
13             CPAN::Testers::WWW::Statistics - CPAN Testers Statistics website.
14              
15             =head1 DESCRIPTION
16              
17             CPAN Testers Statistics comprises the actual website pages, a CGI tool to find
18             testers, and some backend code to help map tester address to a real identity.
19              
20             =cut
21              
22             # -------------------------------------
23             # Library Modules
24              
25 15     15   48 use base qw(Class::Accessor::Fast);
  15         12  
  15         6102  
26              
27 15     15   35266 use Config::IniFiles;
  15         400679  
  15         406  
28 15     15   6915 use CPAN::Testers::Common::DBUtils;
  15         203633  
  15         93  
29 15     15   425 use File::Basename;
  15         17  
  15         761  
30 15     15   53 use File::Path;
  15         17  
  15         484  
31 15     15   6429 use HTML::Entities;
  15         47357  
  15         954  
32 15     15   4934 use IO::File;
  15         7253  
  15         1348  
33 15     15   10928 use Regexp::Assemble;
  0            
  0            
34              
35             use CPAN::Testers::WWW::Statistics::Leaderboard;
36             use CPAN::Testers::WWW::Statistics::Pages;
37             use CPAN::Testers::WWW::Statistics::Graphs;
38              
39             # -------------------------------------
40             # Public Methods
41              
42             =head1 INTERFACE
43              
44             =head2 The Constructor
45              
46             =over 4
47              
48             =item * new
49              
50             Statistics creation object. Provides all the configuration and logging
51             functionality, as well the interface to the lower level functionality for Page
52             and Graph creation.
53              
54             new() takes an option hash as an argument, which may contain the following
55             keys.
56              
57             config => path to configuration file [required]
58              
59             directory => path to output directory
60             mainstore => path/format to data storage files
61             templates => path to templates directory
62             address => path to address file
63             mailrc => path to 01mailrc.txt file
64             builder => path to output file from builder log parser
65              
66             logfile => path to logfile
67             logclean => will overwrite any existing logfile if set
68              
69             Note that while 'directory', 'templates' and 'address' are optional as
70             parameters, if they are not provided as parameters, then they MUST be
71             specified within the 'MASTER' section of the configuration file.
72              
73             =back
74              
75             =cut
76              
77             sub _alarm_handler { return; }
78              
79             sub new {
80             my $class = shift;
81             my %hash = @_;
82              
83             my $self = {};
84             bless $self, $class;
85              
86             # ensure we have a configuration file
87             die "Must specify the configuration file\n" unless( $hash{config});
88             die "Configuration file [$hash{config}] not found\n" unless(-f $hash{config});
89              
90             # load configuration file
91             my $cfg;
92             local $SIG{'__WARN__'} = \&_alarm_handler;
93             eval { $cfg = Config::IniFiles->new( -file => $hash{config} ); };
94             die "Cannot load configuration file [$hash{config}]\n" unless($cfg && !$@);
95             $self->{cfg} = $cfg;
96              
97             # configure databases
98             for my $db (qw(CPANSTATS TESTERS)) {
99             die "No configuration for $db database\n" unless($cfg->SectionExists($db));
100             my %opts = map {my $v = $cfg->val($db,$_); defined($v) ? ($_ => $v) : () }
101             qw(driver database dbfile dbhost dbport dbuser dbpass);
102             $self->{$db} = CPAN::Testers::Common::DBUtils->new(%opts);
103             die "Cannot configure $db database\n" unless($self->{$db});
104             }
105              
106             my %OSNAMES;
107             my @rows = $self->{CPANSTATS}->get_query('array',q{SELECT osname,ostitle FROM osname ORDER BY id});
108             for my $row (@rows) {
109             $OSNAMES{lc $row->[0]} ||= $row->[1];
110             }
111             $self->osnames( \%OSNAMES );
112              
113             my $ra = Regexp::Assemble->new();
114             my @NOREPORTS = split("\n", $cfg->val('NOREPORTS','list'));
115             for(@NOREPORTS) {
116             s/\s+\#.*$//; #remove comments
117             $ra->add($_);
118             }
119             $self->noreports($ra->re);
120              
121             my @TOCOPY = split("\n", $cfg->val('TOCOPY','LIST'));
122             $self->tocopy(\@TOCOPY);
123              
124             my %TOLINK;
125             for my $link ($cfg->Parameters('TOLINK')) {
126             my $file = $cfg->val('TOLINK',$link);
127             $TOLINK{$link} = $file;
128             }
129             $self->tolink(\%TOLINK);
130              
131             $self->known_t( 0 );
132             $self->known_s( 0 );
133              
134             $self->mainstore( _defined_or( $hash{mainstore}, $cfg->val('MASTER','mainstore' ), 'cpanstats-%s.json' ));
135             $self->templates( _defined_or( $hash{templates}, $cfg->val('MASTER','templates' ) ));
136             $self->address( _defined_or( $hash{address}, $cfg->val('MASTER','address' ) ));
137             $self->missing( _defined_or( $hash{missing}, $cfg->val('MASTER','missing' ) ));
138             $self->mailrc( _defined_or( $hash{mailrc}, $cfg->val('MASTER','mailrc' ) ));
139             $self->logfile( _defined_or( $hash{logfile}, $cfg->val('MASTER','logfile' ) ));
140             $self->logclean( _defined_or( $hash{logclean}, $cfg->val('MASTER','logclean' ), 0 ));
141             $self->directory( _defined_or( $hash{directory}, $cfg->val('MASTER','directory' ) ));
142             $self->copyright( $cfg->val('MASTER','copyright' ) );
143             $self->builder( _defined_or( $hash{builder}, $cfg->val('MASTER','builder' ) ));
144              
145             for my $dir (qw(dir_cpan dir_backpan dir_reports)) {
146             $self->$dir( _defined_or( $hash{$dir}, $cfg->val('MASTER',$dir ) ));
147             }
148              
149             $self->_log(sprintf "%-12s=%s", $_, ($self->$_() || ''))
150             for(qw(mainstore templates address missing mailrc logfile logclean directory builder dir_cpan dir_backpan dir_reports));
151              
152             die "Must specify the output directory\n" unless($self->directory);
153             die "Must specify the template directory\n" unless($self->templates);
154             die "Must specify a valid mailrc path\n" unless($self->mailrc && -f $self->mailrc);
155              
156             return $self;
157             }
158              
159             =head2 Public Methods
160              
161             =over 4
162              
163             =item * leaderboard
164              
165             Maintain the leaderboard table as requested.
166              
167             =item * make_pages
168              
169             Method to manage the data update and creation of all the statistics web pages.
170              
171             Note that this method incorporate all of the method functionality of update,
172             make_basics, make_matrix and make_stats.
173              
174             =item * update
175              
176             Method to manage the data update only.
177              
178             =item * make_basics
179              
180             Method to manage the creation of the basic statistics web pages.
181              
182             =item * make_matrix
183              
184             Method to manage the creation of the matrix style statistics web pages.
185              
186             =item * make_stats
187              
188             Method to manage the creation of the tabular style statistics web pages.
189              
190             =item * make_cpan
191              
192             Method to manage the creation of the CPAN specific statistics files and web pages.
193              
194             =item * make_leaders
195              
196             Method to manage the creation of the OS leaderboard web pages.
197              
198             =item * make_noreports
199              
200             Method to manage the creation of the no reports pages.
201              
202             =item * make_performance
203              
204             Method to manage the creation/update of the builder performance data file.
205              
206             =item * make_graphs
207              
208             Method to manage the creation of all the statistics graphs.
209              
210             =item * storage
211              
212             Method to return specific JSON data currently stored.
213              
214             =cut
215              
216             __PACKAGE__->mk_accessors(
217             qw( directory mainstore templates address builder missing mailrc
218             logfile logclean copyright noreports tocopy tolink osnames
219             address profile known_t known_s dir_cpan dir_backpan dir_reports));
220              
221             sub leaderboard {
222             my ($self,%options) = @_;
223              
224             my $lb = CPAN::Testers::WWW::Statistics::Leaderboard->new(parent => $self);
225              
226             return $lb->results( $options{results} ) if($options{results});
227             return $lb->check() if($options{check});
228             return $lb->renew() if($options{renew});
229            
230             $lb->update() if($options{update});
231             $lb->postdate( $options{postdate} ) if($options{postdate});
232             }
233              
234             sub make_pages {
235             my $self = shift;
236             $self->_check_files();
237              
238             my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
239             $stats->update_full();
240             }
241              
242             sub update {
243             my $self = shift;
244             $self->_check_files();
245              
246             my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
247             $stats->update_data();
248             }
249              
250             sub make_basics {
251             my $self = shift;
252             $self->_check_files();
253              
254             my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
255             $stats->build_basics();
256             }
257              
258             sub make_matrix {
259             my $self = shift;
260             $self->_check_files();
261              
262             my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
263             $stats->build_matrices();
264             }
265              
266             sub make_stats {
267             my $self = shift;
268             $self->_check_files();
269              
270             my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
271             $stats->build_stats();
272             }
273              
274             sub make_cpan {
275             my $self = shift;
276             $self->_check_files();
277              
278             my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
279             $stats->build_cpan();
280             }
281              
282             sub make_leaders {
283             my $self = shift;
284             $self->_check_files();
285              
286             my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
287             $stats->build_leaders();
288             }
289              
290             sub make_noreports {
291             my $self = shift;
292             $self->_check_files();
293              
294             my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
295             $stats->build_noreports();
296             }
297              
298             sub make_performance {
299             my $self = shift;
300             $self->_check_files();
301              
302             my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
303             $stats->build_performance();
304             }
305              
306             sub make_graphs {
307             my $self = shift;
308             my $stats = CPAN::Testers::WWW::Statistics::Graphs->new(parent => $self);
309             $stats->create();
310             }
311              
312             sub storage {
313             my $self = shift;
314             my $type = shift;
315             $self->_check_files();
316              
317             my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
318             $stats->storage_read($type);
319             }
320              
321             =item * ranges
322              
323             Returns the specific date range array reference, as held in the configuration
324             file.
325              
326             =item * osname
327              
328             Returns the print form of a recorded OS name.
329              
330             =item * tester
331              
332             Returns either the known name of the tester for the given email address, or
333             returns a doctored version of the address for displaying in HTML.
334              
335             =item * tester_lookup
336              
337             Returns the name or email address, if found, of the stored profile or address
338             for the given addressid and testerid.
339              
340             =item * tester_loader
341              
342             Look up the number of know addresses and testers in the database.
343              
344             =back
345              
346             =cut
347              
348             sub ranges {
349             my ($self,$section) = @_;
350             return unless($section);
351             my @now = localtime(time);
352             if($now[4]==0) { $now[5]--; $now[4]=12; }
353             my $now = sprintf "%04d%02d", $now[5]+1900, $now[4];
354              
355             my @RANGES;
356             if($section eq 'NONE') {
357             @RANGES = ('00000000-99999999');
358             } else {
359             my @ranges = split("\n", $self->{cfg}->val($section,'LIST'));
360             for my $range (@ranges) {
361             my ($fdate,$tdate) = split('-',$range,2);
362             next if($fdate > $now);
363             $tdate = $now if($tdate > $now);
364             push @RANGES, "$fdate-$tdate";
365             }
366             }
367              
368             return \@RANGES;
369             }
370              
371             sub osname {
372             my ($self,$name) = @_;
373             my $osnames = $self->osnames();
374             return $osnames->{lc $name} || $name;
375             }
376              
377             sub tester {
378             my ($self,$name) = @_;
379              
380             return @{$self->{addresses}{$name}} if($self->{addresses}{$name});
381            
382             my @rows = $self->{TESTERS}->get_query('hash',q{
383             SELECT a.email,p.name,p.pause,a.addressid,a.testerid
384             FROM address a
385             LEFT JOIN profile p ON p.testerid=a.testerid
386             WHERE a.address=? OR a.email=?
387             },$name,$name);
388            
389             my @addr = ( $name, 0, 0 );
390             if(@rows) {
391             if($rows[0]->{name}) {
392             $addr[0] = $rows[0]->{name} . ($rows[0]->{pause} ? " ($rows[0]->{pause})" : '');
393             } else {
394             $addr[0] = $rows[0]->{email};
395             }
396              
397             $addr[1] = $rows[0]->{addressid};
398             $addr[2] = $rows[0]->{testerid};
399             }
400              
401             $addr[0] = _html_name($addr[0]);
402              
403             $self->{addresses}{$name} = \@addr;
404             return @addr;
405             }
406              
407             sub tester_lookup {
408             my ($self,$addressid,$testerid) = @_;
409            
410             $self->tester_loader() unless($self->known_t);
411             my $address = $self->address;
412             my $profile = $self->profile;
413              
414             return $profile->{$testerid}{html} if($testerid && $profile->{$testerid});
415             return $address->{$addressid}{html} if($addressid && $address->{$addressid});
416             return;
417             }
418              
419             sub tester_loader {
420             my $self = shift;
421             my (%address,%profile);
422              
423             my @rows = $self->{TESTERS}->get_query('hash',q{SELECT * FROM address});
424             for my $row (@rows) {
425             $row->{html} = _html_name($row->{email});
426             $address{$row->{addressid}} = $row;
427             }
428             $self->address( \%address );
429              
430             @rows = $self->{TESTERS}->get_query('hash',q{SELECT * FROM profile});
431             for my $row (@rows) {
432             my $name = $row->{name} . ($row->{pause} ? " ($row->{pause})" : '');
433             $row->{html} = _html_name($name);
434             $profile{$row->{testerid}} = $row;
435             }
436             $self->profile( \%profile );
437              
438             @rows = $self->{TESTERS}->get_query('array',q{
439             SELECT count(addressid),count(distinct testerid) FROM address WHERE testerid > 0
440             });
441             $self->known_s( $rows[0]->[0] );
442             $self->known_t( $rows[0]->[1] );
443             }
444              
445             # -------------------------------------
446             # Private Methods
447              
448             sub _html_name {
449             my $name = shift || return '';
450              
451             $name = $name =~ /\&(\#x?\d+|\w+)\;/
452             ? $name
453             : encode_entities( $name );
454             $name =~ s/\./ /g if($name =~ /\@/);
455             $name =~ s/\@/ \+ /g;
456             $name =~ s/
457             $name =~ s/>/>/g;
458              
459             return $name;
460             }
461              
462             sub _check_files {
463             my $self = shift;
464             die "Template directory not found\n" unless(-d $self->templates);
465             die "Must specify the path of the address file\n" unless( $self->address);
466             die "Address file not found\n" unless(-f $self->address);
467             }
468              
469             sub _log {
470             my $self = shift;
471             my $log = $self->logfile or return;
472             mkpath(dirname($log)) unless(-f $log);
473              
474             my $mode = $self->logclean ? 'w+' : 'a+';
475             $self->logclean(0);
476              
477             my @dt = localtime(time);
478             my $dt = sprintf "%04d/%02d/%02d %02d:%02d:%02d", $dt[5]+1900,$dt[4]+1,$dt[3],$dt[2],$dt[1],$dt[0];
479              
480             my $fh = IO::File->new($log,$mode) or die "Cannot write to log file [$log]: $!\n";
481             print $fh "$dt ", @_, "\n";
482             $fh->close;
483             }
484              
485             sub _defined_or {
486             while(@_) {
487             my $value = shift;
488             return $value if(defined $value);
489             }
490              
491             return;
492             }
493              
494             q("I am NOT a number!");
495              
496             __END__