File Coverage

blib/lib/CPAN/Testers/WWW/Statistics.pm
Criterion Covered Total %
statement 42 233 18.0
branch 0 74 0.0
condition 0 22 0.0
subroutine 14 37 37.8
pod 18 18 100.0
total 74 384 19.2


line stmt bran cond sub pod time code
1             package CPAN::Testers::WWW::Statistics;
2              
3 15     15   958241 use warnings;
  15         40  
  15         478  
4 15     15   117 use strict;
  15         31  
  15         333  
5 15     15   77 use vars qw($VERSION);
  15         32  
  15         923  
6              
7             $VERSION = '1.22';
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   220 use base qw(Class::Accessor::Fast);
  15         33  
  15         6930  
26              
27 15     15   44644 use Config::IniFiles;
  15         372224  
  15         593  
28 15     15   8370 use CPAN::Testers::Common::DBUtils;
  15         272925  
  15         130  
29 15     15   674 use File::Basename;
  15         36  
  15         1096  
30 15     15   92 use File::Path;
  15         33  
  15         725  
31 15     15   8091 use HTML::Entities;
  15         70798  
  15         1217  
32 15     15   5900 use IO::File;
  15         10076  
  15         1631  
33 15     15   10597 use Regexp::Assemble;
  15         286615  
  15         611  
34              
35 15     15   7187 use CPAN::Testers::WWW::Statistics::Leaderboard;
  15         51  
  15         484  
36 15     15   8252 use CPAN::Testers::WWW::Statistics::Pages;
  15         57  
  15         690  
37 15     15   7435 use CPAN::Testers::WWW::Statistics::Graphs;
  15         74  
  15         30025  
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 0     0     sub _alarm_handler { return; }
78              
79             sub new {
80 0     0 1   my $class = shift;
81 0           my %hash = @_;
82              
83 0           my $self = {};
84 0           bless $self, $class;
85              
86             # ensure we have a configuration file
87 0 0         die "Must specify the configuration file\n" unless( $hash{config});
88 0 0         die "Configuration file [$hash{config}] not found\n" unless(-f $hash{config});
89              
90             # load configuration file
91 0           my $cfg;
92 0           local $SIG{'__WARN__'} = \&_alarm_handler;
93 0           eval { $cfg = Config::IniFiles->new( -file => $hash{config} ); };
  0            
94 0 0 0       die "Cannot load configuration file [$hash{config}]\n" unless($cfg && !$@);
95 0           $self->{cfg} = $cfg;
96              
97             # configure databases
98 0           for my $db (qw(CPANSTATS TESTERS)) {
99 0 0         die "No configuration for $db database\n" unless($cfg->SectionExists($db));
100 0 0         my %opts = map {my $v = $cfg->val($db,$_); defined($v) ? ($_ => $v) : () }
  0            
  0            
101             qw(driver database dbfile dbhost dbport dbuser dbpass);
102 0           $self->{$db} = CPAN::Testers::Common::DBUtils->new(%opts);
103 0 0         die "Cannot configure $db database\n" unless($self->{$db});
104             }
105              
106 0           my %OSNAMES;
107 0           my @rows = $self->{CPANSTATS}->get_query('array',q{SELECT osname,ostitle FROM osname ORDER BY id});
108 0           for my $row (@rows) {
109 0   0       $OSNAMES{lc $row->[0]} ||= $row->[1];
110             }
111 0           $self->osnames( \%OSNAMES );
112              
113 0           my $ra = Regexp::Assemble->new();
114 0           my @NOREPORTS = split("\n", $cfg->val('NOREPORTS','list'));
115 0           for(@NOREPORTS) {
116 0           s/\s+\#.*$//; #remove comments
117 0           $ra->add($_);
118             }
119 0           $self->noreports($ra->re);
120              
121 0           my @TOCOPY = split("\n", $cfg->val('TOCOPY','LIST'));
122 0           $self->tocopy(\@TOCOPY);
123              
124 0           my %TOLINK;
125 0           for my $link ($cfg->Parameters('TOLINK')) {
126 0           my $file = $cfg->val('TOLINK',$link);
127 0           $TOLINK{$link} = $file;
128             }
129 0           $self->tolink(\%TOLINK);
130              
131 0           $self->known_t( 0 );
132 0           $self->known_s( 0 );
133              
134 0           $self->mainstore( _defined_or( $hash{mainstore}, $cfg->val('MASTER','mainstore' ), 'cpanstats-%s.json' ));
135 0           $self->templates( _defined_or( $hash{templates}, $cfg->val('MASTER','templates' ) ));
136 0           $self->address( _defined_or( $hash{address}, $cfg->val('MASTER','address' ) ));
137 0           $self->missing( _defined_or( $hash{missing}, $cfg->val('MASTER','missing' ) ));
138 0           $self->mailrc( _defined_or( $hash{mailrc}, $cfg->val('MASTER','mailrc' ) ));
139 0           $self->logfile( _defined_or( $hash{logfile}, $cfg->val('MASTER','logfile' ) ));
140 0           $self->logclean( _defined_or( $hash{logclean}, $cfg->val('MASTER','logclean' ), 0 ));
141 0           $self->directory( _defined_or( $hash{directory}, $cfg->val('MASTER','directory' ) ));
142 0           $self->copyright( $cfg->val('MASTER','copyright' ) );
143 0           $self->builder( _defined_or( $hash{builder}, $cfg->val('MASTER','builder' ) ));
144 0           $self->build_history( _defined_or( $hash{build_history}, $cfg->val('MASTER','build_history' ) ));
145              
146 0           for my $dir (qw(dir_cpan dir_backpan dir_reports)) {
147 0           $self->$dir( _defined_or( $hash{$dir}, $cfg->val('MASTER',$dir ) ));
148             }
149              
150             $self->_log(sprintf "%-12s=%s", $_, ($self->$_() || ''))
151 0   0       for(qw(mainstore templates address missing mailrc logfile logclean directory builder dir_cpan dir_backpan dir_reports));
152              
153 0 0         die "Must specify the output directory\n" unless($self->directory);
154 0 0         die "Must specify the template directory\n" unless($self->templates);
155 0 0 0       die "Must specify a valid mailrc path\n" unless($self->mailrc && -f $self->mailrc);
156              
157 0           return $self;
158             }
159              
160             =head2 Public Methods
161              
162             =over 4
163              
164             =item * leaderboard
165              
166             Maintain the leaderboard table as requested.
167              
168             =item * make_pages
169              
170             Method to manage the data update and creation of all the statistics web pages.
171              
172             Note that this method incorporate all of the method functionality of update,
173             make_basics, make_matrix and make_stats.
174              
175             =item * update
176              
177             Method to manage the data update only.
178              
179             =item * make_basics
180              
181             Method to manage the creation of the basic statistics web pages.
182              
183             =item * make_matrix
184              
185             Method to manage the creation of the matrix style statistics web pages.
186              
187             =item * make_stats
188              
189             Method to manage the creation of the tabular style statistics web pages.
190              
191             =item * make_cpan
192              
193             Method to manage the creation of the CPAN specific statistics files and web pages.
194              
195             =item * make_leaders
196              
197             Method to manage the creation of the OS leaderboard web pages.
198              
199             =item * make_noreports
200              
201             Method to manage the creation of the no reports pages.
202              
203             =item * make_performance
204              
205             Method to manage the creation/update of the builder performance data file.
206              
207             =item * make_graphs
208              
209             Method to manage the creation of all the statistics graphs.
210              
211             =item * storage
212              
213             Method to return specific JSON data currently stored.
214              
215             =cut
216              
217             __PACKAGE__->mk_accessors(
218             qw( directory mainstore templates address builder missing mailrc
219             logfile logclean copyright noreports tocopy tolink osnames
220             address profile known_t known_s dir_cpan dir_backpan dir_reports
221             build_history));
222              
223             sub leaderboard {
224 0     0 1   my ($self,%options) = @_;
225              
226 0           my $lb = CPAN::Testers::WWW::Statistics::Leaderboard->new(parent => $self);
227              
228 0 0         return $lb->results( $options{results} ) if($options{results});
229 0 0         return $lb->check() if($options{check});
230 0 0         return $lb->renew() if($options{renew});
231            
232 0 0         $lb->update() if($options{update});
233 0 0         $lb->postdate( $options{postdate} ) if($options{postdate});
234             }
235              
236             sub make_pages {
237 0     0 1   my $self = shift;
238 0           $self->_check_files();
239              
240 0           my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
241 0           $stats->update_full();
242             }
243              
244             sub update {
245 0     0 1   my $self = shift;
246 0           $self->_check_files();
247              
248 0           my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
249 0           $stats->update_data();
250             }
251              
252             sub make_basics {
253 0     0 1   my $self = shift;
254 0           $self->_check_files();
255              
256 0           my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
257 0           $stats->build_basics();
258             }
259              
260             sub make_matrix {
261 0     0 1   my $self = shift;
262 0           $self->_check_files();
263              
264 0           my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
265 0           $stats->build_matrices();
266             }
267              
268             sub make_stats {
269 0     0 1   my $self = shift;
270 0           $self->_check_files();
271              
272 0           my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
273 0           $stats->build_stats();
274             }
275              
276             sub make_cpan {
277 0     0 1   my $self = shift;
278 0           $self->_check_files();
279              
280 0           my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
281 0           $stats->build_cpan();
282             }
283              
284             sub make_leaders {
285 0     0 1   my $self = shift;
286 0           $self->_check_files();
287              
288 0           my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
289 0           $stats->build_leaders();
290             }
291              
292             sub make_noreports {
293 0     0 1   my $self = shift;
294 0           $self->_check_files();
295              
296 0           my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
297 0           $stats->build_noreports();
298             }
299              
300             sub make_performance {
301 0     0 1   my $self = shift;
302 0           $self->_check_files();
303              
304 0           my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
305 0           $stats->build_performance();
306             }
307              
308             sub make_graphs {
309 0     0 1   my $self = shift;
310 0           my $stats = CPAN::Testers::WWW::Statistics::Graphs->new(parent => $self);
311 0           $stats->create();
312             }
313              
314             sub storage {
315 0     0 1   my $self = shift;
316 0           my $type = shift;
317 0           $self->_check_files();
318              
319 0           my $stats = CPAN::Testers::WWW::Statistics::Pages->new(parent => $self);
320 0           $stats->storage_read($type);
321             }
322              
323             =item * ranges
324              
325             Returns the specific date range array reference, as held in the configuration
326             file.
327              
328             =item * osname
329              
330             Returns the print form of a recorded OS name.
331              
332             =item * tester
333              
334             Returns either the known name of the tester for the given email address, or
335             returns a doctored version of the address for displaying in HTML.
336              
337             =item * tester_lookup
338              
339             Returns the name or email address, if found, of the stored profile or address
340             for the given addressid and testerid.
341              
342             =item * tester_loader
343              
344             Look up the number of know addresses and testers in the database.
345              
346             =back
347              
348             =cut
349              
350             sub ranges {
351 0     0 1   my ($self,$section) = @_;
352 0 0         return unless($section);
353 0           my @now = localtime(time);
354 0 0         if($now[4]==0) { $now[5]--; $now[4]=12; }
  0            
  0            
355 0           my $now = sprintf "%04d%02d", $now[5]+1900, $now[4];
356              
357 0           my @RANGES;
358 0 0         if($section eq 'NONE') {
359 0           @RANGES = ('00000000-99999999');
360             } else {
361 0           my @ranges = split("\n", $self->{cfg}->val($section,'LIST'));
362 0           for my $range (@ranges) {
363 0           my ($fdate,$tdate) = split('-',$range,2);
364 0 0         next if($fdate > $now);
365 0 0         $tdate = $now if($tdate > $now);
366 0           push @RANGES, "$fdate-$tdate";
367             }
368             }
369              
370 0           return \@RANGES;
371             }
372              
373             sub osname {
374 0     0 1   my ($self,$name) = @_;
375 0           my $osnames = $self->osnames();
376 0   0       return $osnames->{lc $name} || $name;
377             }
378              
379             sub tester {
380 0     0 1   my ($self,$name) = @_;
381              
382 0 0         return @{$self->{addresses}{$name}} if($self->{addresses}{$name});
  0            
383            
384 0           my @rows = $self->{TESTERS}->get_query('hash',q{
385             SELECT a.email,p.name,p.pause,a.addressid,a.testerid
386             FROM address a
387             LEFT JOIN profile p ON p.testerid=a.testerid
388             WHERE a.address=? OR a.email=?
389             },$name,$name);
390            
391 0           my @addr = ( $name, 0, 0 );
392 0 0         if(@rows) {
393 0 0         if($rows[0]->{name}) {
394 0 0         $addr[0] = $rows[0]->{name} . ($rows[0]->{pause} ? " ($rows[0]->{pause})" : '');
395             } else {
396 0           $addr[0] = $rows[0]->{email};
397             }
398              
399 0           $addr[1] = $rows[0]->{addressid};
400 0           $addr[2] = $rows[0]->{testerid};
401             }
402              
403 0           $addr[0] = _html_name($addr[0]);
404              
405 0           $self->{addresses}{$name} = \@addr;
406 0           return @addr;
407             }
408              
409             sub tester_lookup {
410 0     0 1   my ($self,$addressid,$testerid) = @_;
411            
412 0 0         $self->tester_loader() unless($self->known_t);
413 0           my $address = $self->address;
414 0           my $profile = $self->profile;
415              
416 0 0 0       return $profile->{$testerid}{html} if($testerid && $profile->{$testerid});
417 0 0 0       return $address->{$addressid}{html} if($addressid && $address->{$addressid});
418 0           return;
419             }
420              
421             sub tester_loader {
422 0     0 1   my $self = shift;
423 0           my (%address,%profile);
424              
425 0           my @rows = $self->{TESTERS}->get_query('hash',q{SELECT * FROM address});
426 0           for my $row (@rows) {
427 0           $row->{html} = _html_name($row->{email});
428 0           $address{$row->{addressid}} = $row;
429             }
430 0           $self->address( \%address );
431              
432 0           @rows = $self->{TESTERS}->get_query('hash',q{SELECT * FROM profile});
433 0           for my $row (@rows) {
434 0 0         my $name = $row->{name} . ($row->{pause} ? " ($row->{pause})" : '');
435 0           $row->{html} = _html_name($name);
436 0           $profile{$row->{testerid}} = $row;
437             }
438 0           $self->profile( \%profile );
439              
440 0           @rows = $self->{TESTERS}->get_query('array',q{
441             SELECT count(addressid),count(distinct testerid) FROM address WHERE testerid > 0
442             });
443 0           $self->known_s( $rows[0]->[0] );
444 0           $self->known_t( $rows[0]->[1] );
445             }
446              
447             # -------------------------------------
448             # Private Methods
449              
450             sub _html_name {
451 0   0 0     my $name = shift || return '';
452              
453 0 0         $name = $name =~ /\&(\#x?\d+|\w+)\;/
454             ? $name
455             : encode_entities( $name );
456 0 0         $name =~ s/\./ /g if($name =~ /\@/);
457 0           $name =~ s/\@/ \+ /g;
458 0           $name =~ s/</&lt;/g;
459 0           $name =~ s/>/&gt;/g;
460              
461 0           return $name;
462             }
463              
464             sub _check_files {
465 0     0     my $self = shift;
466 0 0         die "Template directory not found\n" unless(-d $self->templates);
467 0 0         die "Must specify the path of the address file\n" unless( $self->address);
468 0 0         die "Address file not found\n" unless(-f $self->address);
469             }
470              
471             sub _log {
472 0     0     my $self = shift;
473 0 0         my $log = $self->logfile or return;
474 0 0         mkpath(dirname($log)) unless(-f $log);
475              
476 0 0         my $mode = $self->logclean ? 'w+' : 'a+';
477 0           $self->logclean(0);
478              
479 0           my @dt = localtime(time);
480 0           my $dt = sprintf "%04d/%02d/%02d %02d:%02d:%02d", $dt[5]+1900,$dt[4]+1,$dt[3],$dt[2],$dt[1],$dt[0];
481              
482 0 0         my $fh = IO::File->new($log,$mode) or die "Cannot write to log file [$log]: $!\n";
483 0           print $fh "$dt ", @_, "\n";
484 0           $fh->close;
485             }
486              
487             sub _defined_or {
488 0     0     while(@_) {
489 0           my $value = shift;
490 0 0         return $value if(defined $value);
491             }
492              
493 0           return;
494             }
495              
496             q("I am NOT a number!");
497              
498             __END__
499              
500             =head1 CPAN TESTERS FUND
501              
502             CPAN Testers wouldn't exist without the help and support of the Perl
503             community. However, since 2008 CPAN Testers has grown far beyond the
504             expectations of it's original creators. As a consequence it now requires
505             considerable funding to help support the infrastructure.
506              
507             In early 2012 the Enlightened Perl Organisation very kindly set-up a
508             CPAN Testers Fund within their donatation structure, to help the project
509             cover the costs of servers and services.
510              
511             If you would like to donate to the CPAN Testers Fund, please follow the link
512             below to the Enlightened Perl Organisation's donation site.
513              
514             F<https://members.enlightenedperl.org/drupal/donate-cpan-testers>
515              
516             If your company would like to support us, you can donate financially via the
517             fund link above, or if you have servers or services that we might use, please
518             send an email to admin@cpantesters.org with details.
519              
520             Our full list of current sponsors can be found at our I <3 CPAN Testers site.
521              
522             F<http://iheart.cpantesters.org>
523              
524             =head1 BUGS, PATCHES & FIXES
525              
526             There are no known bugs at the time of this release. However, if you spot a
527             bug or are experiencing difficulties, that is not explained within the POD
528             documentation, please send bug reports and patches to the RT Queue (see below).
529              
530             Fixes are dependent upon their severity and my availability. Should a fix not
531             be forthcoming, please feel free to (politely) remind me.
532              
533             RT Queue -
534             http://rt.cpan.org/Public/Dist/Display.html?Name=CPAN-Testers-WWW-Statistics
535              
536             =head1 SEE ALSO
537              
538             L<CPAN::Testers::Data::Generator>,
539             L<CPAN::Testers::WWW::Reports>
540              
541             F<http://www.cpantesters.org/>,
542             F<http://stats.cpantesters.org/>,
543             F<http://wiki.cpantesters.org/>
544              
545             =head1 AUTHOR
546              
547             Barbie, <barbie@cpan.org>
548             for Miss Barbell Productions <http://www.missbarbell.co.uk>.
549              
550             =head1 COPYRIGHT AND LICENSE
551              
552             Copyright (C) 2005-2017 Barbie for Miss Barbell Productions.
553              
554             This distribution is free software; you can redistribute it and/or
555             modify it under the Artistic Licence v2.
556              
557             =cut