Subversion Repositories DevTools

Rev

Rev 1295 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
1293 dpurdie 1
###############################################################################
2
# Copyright (c) 2003 Jason Remillard.  All rights reserved.
3
#
4
# This program is free software; you can redistribute it and modify it under
5
# the terms of the GPL.
6
 
7
# Model object for handling metrics reports. This object is not an object, it
8
# is a normal module.
9
 
10
package Codestriker::Model::MetricStats;
11
 
12
use strict;
13
use Encode qw(decode_utf8);
14
 
15
use Codestriker::DB::DBI;
16
 
17
my $total_participants_header = 'Total Participants';
18
my $topic_size_lines_header = 'Topic Size In Lines';
19
 
20
# Returns the list of users that have participated in a Codestriker topics.
21
#
22
# 
23
# Returns a collection of the following hash references.
24
# {
25
#   name
26
#   date_last_authored
27
#   date_last_participated
28
#   total_topics
29
# }
30
 
31
sub get_basic_user_metrics {
32
    # Gather basic user metrics for the past 16 weeks.
33
    my $last_n_days = 16*7;
34
 
35
    my $date = Codestriker->get_timestamp(time-($last_n_days*24*60*60));
36
 
37
    # Obtain a database connection.
38
    my $dbh = Codestriker::DB::DBI->get_connection();
39
 
40
    # Get the list of authors.
41
    my $author_list = $dbh->selectall_arrayref(
42
	    'SELECT LOWER(author),MAX(modified_ts),COUNT(id)
43
	     FROM topic 
44
	     WHERE modified_ts >= ?
45
	     GROUP BY author
46
	     ORDER BY 2 desc', {}, $date);
47
 
48
    my @users_metrics;
49
 
50
    foreach my $row (@$author_list) {
51
	my ($name, $last_authored_date, $count) = @$row;
52
 
53
	my $metrics = 
54
	{
55
	    name=>$name,
56
	    date_last_authored=>
57
		int((time() - Codestriker->convert_date_timestamp_time($last_authored_date))/(60*60*24)),
58
	    date_last_participated=>'',
59
	    total_codestriker_time => 
60
		calculate_topic_view_time_for_user($date,$name),
61
	    total_topics=>$count,
62
	};
63
 
64
	push(@users_metrics, $metrics);
65
    }
66
 
67
    # Get the list of participants from all these topics. You need to 
68
    # submit at least one comment to be counted.
69
    my $participant_list = $dbh->selectall_arrayref(
70
	    'SELECT LOWER(commentdata.author), 
71
		    MAX(topic.modified_ts), 
72
		    COUNT(DISTINCT topic.id)
73
	     FROM commentdata, commentstate, topic 
74
	     WHERE topic.modified_ts >= ? AND 
75
		   topic.id = commentstate.topicid AND 
76
		   LOWER(topic.author) <> LOWER(commentdata.author) AND
77
		   commentdata.commentstateid = commentstate.id
78
	     GROUP BY LOWER(commentdata.author)
79
	     ORDER BY 2 desc',{}, $date);
80
 
81
    foreach my $row (@$participant_list) {
82
	my ($name, $last_participated_date, $count) = @$row;
83
 
84
	my $found = 0;
85
	foreach my $user (@users_metrics) {
86
	    if ($user->{name} eq $name) {
87
		$user->{date_last_participated} = 
88
		    int((time() - 
89
		    Codestriker->convert_date_timestamp_time($last_participated_date))/(60*60*24));
90
		$user->{total_topics} += $count;
91
		$found = 1;
92
	    }
93
	}
94
 
95
	if ($found == 0) {
96
	    my $metrics = 
97
	    {
98
		name=>$name,
99
		date_last_authored=>'',
100
		date_last_participated=>
101
		    int((time() - 
102
		    Codestriker->convert_date_timestamp_time($last_participated_date))/(60*60*24)),
103
		total_topics=>$count,
104
		total_codestriker_time => 
105
		    calculate_topic_view_time_for_user($date,$name),
106
	    };
107
 
108
	    push(@users_metrics, $metrics);
109
	}
110
    }
111
 
112
    # Close the connection, and check for any database errors.
113
    Codestriker::DB::DBI->release_connection($dbh, 1);
114
 
115
    return @users_metrics;
116
}
117
 
118
# Given the username, and the oldest date, calculate the total amount of 
119
# codestriker time that was recorded in hours.
120
sub calculate_topic_view_time_for_user {
121
    my ($date,$user) = @_;
122
 
123
    # get the total time for this user.
124
    my $dbh = Codestriker::DB::DBI->get_connection();
125
 
126
    my $select_topic = $dbh->prepare_cached('SELECT creation_ts ' .
127
					    'FROM topicviewhistory ' .
128
					    'WHERE creation_ts > ? AND ' .
129
					    'LOWER(email) = LOWER(?) ' .
130
					    'ORDER BY creation_ts');
131
 
132
    $select_topic->execute($date,$user);
133
 
134
    my $total_time = 
135
	Codestriker::Model::Metrics->calculate_topic_view_time($select_topic);
136
 
137
    Codestriker::DB::DBI->release_connection($dbh, 1);
138
 
139
    $total_time = sprintf("%1.1f",$total_time / (60*60));
140
 
141
    return $total_time;
142
}
143
 
144
 
145
# Returns a list of all the topic ids in the database.
146
sub get_topic_ids {
147
    # Obtain a database connection.
148
    my $dbh = Codestriker::DB::DBI->get_connection();
149
 
150
    # Get the list of authors.
151
    my $topicid_list  = $dbh->selectall_arrayref(
152
	    'SELECT id FROM topic ORDER BY creation_ts');
153
 
154
    # Close the connection, and check for any database errors.
155
    Codestriker::DB::DBI->release_connection($dbh, 1);
156
 
157
    return @$topicid_list;
158
 
159
}
160
 
161
# Returns in a hash the column names for the raw metric download feature.
162
sub get_download_headers {
163
    # Obtain a database connection.
164
    my $dbh = Codestriker::DB::DBI->get_connection();
165
 
166
    my @base_headers = qw(
167
	TopicID
168
	Author
169
	Title
170
	State
171
	Creation
172
	Project
173
	);
174
 
175
    # Comment metric counts.
176
    my @comment_metric_headers;
177
    my $comment_metrics = $dbh->selectall_arrayref(
178
	    'SELECT DISTINCT name
179
	     FROM commentstatemetric 
180
	     ORDER BY name
181
	    ');
182
    foreach my $metric (@$comment_metrics) {
183
	push @comment_metric_headers, $metric->[0];
184
    }
185
 
186
    # Do the built-in comment metrics.
187
    push @comment_metric_headers, 'Comment Threads';
188
    push @comment_metric_headers, 'Submitted Comments';
189
 
190
    # Topic metrics counts.
191
    my @topic_metric_headers;
192
    my $topic_metrics = $dbh->selectall_arrayref(
193
	    'SELECT DISTINCT metric_name 
194
	     FROM topicmetric
195
	     ORDER by metric_name
196
	    ');
197
 
198
    foreach my $metric (@$topic_metrics) {
199
	push @topic_metric_headers, $metric->[0];
200
    }
201
 
202
    # Do the built in topic metrics.
203
    for (my $state = 0; $state < scalar(@Codestriker::topic_states);
204
	 ++$state) {
205
	push @topic_metric_headers, 
206
	    'Time In ' . $Codestriker::topic_states[$state];
207
    }
208
 
209
    push @topic_metric_headers, $topic_size_lines_header;
210
 
211
    my @topic_user_metric_headers;
212
 
213
    # User topic metrics counts.
214
    my $user_topic_metrics  = $dbh->selectall_arrayref(
215
	    'SELECT DISTINCT metric_name 
216
	     FROM topicusermetric
217
	     ORDER by metric_name
218
	    ');
219
 
220
    foreach my $metric (@$user_topic_metrics) {
221
	push @topic_user_metric_headers, $metric->[0];
222
    }
223
 
224
    # Do the built in user metrics.
225
    push @topic_user_metric_headers, 'Codestriker Time';
226
    push @topic_user_metric_headers, $total_participants_header;
227
 
228
    my $headers = 
229
    {
230
	base => \@base_headers,
231
	comment => \@comment_metric_headers,
232
	topic => \@topic_metric_headers,
233
	user => \@topic_user_metric_headers
234
    };
235
 
236
    # Close the connection, and check for any database errors.
237
    Codestriker::DB::DBI->release_connection($dbh, 1);
238
 
239
    return $headers;
240
}
241
 
242
# Given a topic id, and a header hash, return an list with all of the
243
# topics metric data returned in the order given by the header
244
# information.
245
sub get_raw_metric_data {
246
    my ($topicid, $headers) = @_;
247
 
248
    # Obtain a database connection.
249
    my $dbh = Codestriker::DB::DBI->get_connection();
250
 
251
    my @basic_topic_info = $dbh->selectrow_array('
252
	    SELECT topic.id, 
253
		   LOWER(topic.author), 
254
		   topic.title, 
255
		   topic.state, 
256
		   topic.creation_ts, 
257
		   project.name 
258
	    FROM topic, project
259
	    WHERE topic.id = ? AND 
260
		  topic.projectid = project.id',{}, $topicid);
261
 
262
    # Decode topic title and project name.
263
    $basic_topic_info[2] = decode_utf8($basic_topic_info[2]);
264
    $basic_topic_info[5] = decode_utf8($basic_topic_info[5]);
265
 
266
    if ($basic_topic_info[3] < @Codestriker::topic_states) {
267
	$basic_topic_info[3] = 
268
	    @Codestriker::topic_states[$basic_topic_info[3]];
269
    }
270
 
271
    $basic_topic_info[4] =
272
	Codestriker->format_date_timestamp($basic_topic_info[4]);
273
 
274
    $basic_topic_info[1] =
275
	Codestriker->filter_email($basic_topic_info[1]);
276
 
277
    my @row;
278
 
279
    push @row, @basic_topic_info;
280
 
281
    # Process the comment metric values.
282
    my $count_query =
283
	$dbh->prepare_cached('SELECT COUNT(*) ' .
284
			     'FROM topic, commentstate, commentstatemetric ' .
285
			     'WHERE topic.id = ? AND ' .
286
                             'topic.id = commentstate.topicid AND ' .
287
			     'commentstate.id = commentstatemetric.id AND '.
288
			     'commentstatemetric.name = ? ');
289
    foreach my $metric (@{$headers->{comment}}) {
290
	# Get the count for this metric name.
291
	$count_query->execute($topicid, $metric);
292
	my ($count) = $count_query->fetchrow_array();
293
	$count_query->finish();
294
	push @row, $count;
295
    }
296
 
297
    # Now process the 'Comment Threads' metric.
298
    my $comment_threads = $dbh->selectall_arrayref(
299
	    'SELECT COUNT(id)
300
	     FROM commentstate 
301
	     WHERE topicid = ?
302
	    ',{}, $topicid);
303
    push @row, $comment_threads->[0]->[0];
304
 
305
    # Now process the 'Submitted Comments' metric.
306
    my $submitted_comments = $dbh->selectall_arrayref(
307
	    'SELECT COUNT(id)
308
	     FROM commentstate, commentdata
309
	     WHERE commentstate.topicid = ?
310
             AND commentstate.id = commentdata.commentstateid
311
	    ',{}, $topicid);
312
    push @row, $submitted_comments->[0]->[0];
313
 
314
    my $topic = Codestriker::Model::Topic->new($basic_topic_info[0]);     
315
 
316
    my $metrics = $topic->get_metrics();
317
 
318
    my @topic_metrics = $metrics->get_topic_metrics();
319
 
320
    for (my $index = 0; $index < scalar(@{$headers->{topic}}); ++$index) {
321
	my $count = "";
322
 
323
	foreach my $metric ( @topic_metrics ) {
324
	    $count = $metric->{value} 
325
		if ($metric->{name} eq $headers->{topic}->[$index]);
326
	}
327
 
328
	if ($headers->{topic}->[$index] eq $topic_size_lines_header ) {
329
	    $count = $topic->get_topic_size_in_lines();
330
	}
331
 
332
	push @row, $count;
333
    } 
334
 
335
    # Get the list of users for this review.
336
    my @users = $metrics->get_complete_list_of_topic_participants();
337
 
338
    my @user_metrics = $metrics->get_user_metrics_totals(@users);
339
 
340
    for (my $index = 0; $index < scalar(@{$headers->{user}}); ++$index) {
341
	my $count = "";
342
 
343
	foreach my $metric ( @user_metrics ) {
344
	    $count = $metric->{value} 
345
		if ($metric->{name} eq $headers->{user}->[$index]);
346
	}
347
 
348
	if ($headers->{user}->[$index] eq $total_participants_header) {
349
	    # Add the total number of participants in the topic.
350
	    $count = scalar(@users);
351
	}
352
 
353
	push @row, $count;
354
    } 
355
 
356
 
357
    # Close the connection, and check for any database errors.
358
    Codestriker::DB::DBI->release_connection($dbh, 1);
359
 
360
    return @row;
361
}
362
 
363
# Returns 12 months of comment metrics data.
364
#
365
# returns a collection of the following hash references
366
# {
367
#   name  = the comment metric name
368
#   results = collection ref to 
369
#             {
370
#               name = the comment value name. 
371
#               counts = array ref to metric counts per month
372
#               monthnames = array ref to month names
373
#             }
374
# }
375
sub get_comment_metrics {
376
    # Stores the collection results.
377
    my @results = ();
378
 
379
    # Obtain a database connection.
380
    my $dbh = Codestriker::DB::DBI->get_connection();
381
 
382
    # Get the comment metric totals.
383
    foreach my $metric_config (@{ $Codestriker::comment_state_metrics }) {
384
	my $metric_name = $metric_config->{name};
385
	my $query =
386
	    "SELECT commentstatemetric.value, COUNT(commentstate.id) " .
387
	    "FROM commentstate, commentstatemetric " .
388
	    "WHERE commentstatemetric.id = commentstate.id AND " .
389
	    "commentstatemetric.name = " . $dbh->quote($metric_name) . " AND " .
390
	    "commentstate.creation_ts > ? AND " .
391
	    "commentstate.creation_ts <= ? " .
392
	    "GROUP BY commentstatemetric.value " .
393
	    "ORDER BY commentstatemetric.value";
394
 
395
	my @metrics = _get_monthly_metrics(12, $query);
396
	my $months_ref = $metrics[0]->{monthnames};
397
 
398
	# Make sure all enumerated values are catered for.
399
	my %handled_value = ();
400
	foreach my $value (@metrics) {
401
	    $handled_value{$value->{name}} = 1;
402
	}
403
	foreach my $value (@{ $metric_config->{values} }) {
404
	    if (! defined $handled_value{$value}) {
405
		push @metrics, { name => $value,
406
				 counts => [0,0,0,0,0,0,0,0,0,0,0,0],
407
				 monthnames => $months_ref };
408
	    }
409
	}
410
 
411
	my $result = { name => $metric_name, results => \@metrics };
412
	push @results, $result;
413
    }
414
 
415
    # Get comment thread totals.
416
    my @total_metrics = ();
417
    my @thread_total = _get_monthly_metrics(12,
418
	'SELECT \'Comment Threads\', COUNT(commentstate.id) 
419
	FROM commentstate
420
	WHERE commentstate.creation_ts >  ? AND
421
	      commentstate.creation_ts <= ?');
422
    push @total_metrics, @thread_total;
423
 
424
    # Get submitted comment totals.
425
    my @submitted_total = _get_monthly_metrics(12,
426
	'SELECT \'Submitted Comments\', COUNT(commentstate.id) 
427
	FROM commentstate, commentdata
428
	WHERE commentstate.id = commentdata.commentstateid AND
429
              commentstate.creation_ts >  ? AND
430
	      commentstate.creation_ts <= ?');
431
    push @total_metrics, @submitted_total;
432
 
433
    my $result = { name => 'Total', results => \@total_metrics };
434
    push @results, $result;
435
 
436
    # Close the connection, and check for any database errors.
437
    Codestriker::DB::DBI->release_connection($dbh, 1);
438
 
439
    return @results;
440
}
441
 
442
# Returns 12 months of data with a break down of topic metrics.
443
#
444
# Returns a collection of the following hash references:
445
# {
446
#   name  = the metric name
447
#   counts = array ref to metric counts per month
448
#   monthnames = array ref to month names
449
# }
450
sub get_topic_metrics {
451
    my @metrics;
452
 
453
    # Get total.
454
    my @total = _get_monthly_metrics(12,
455
	'SELECT \'Total Topics\', COUNT(topic.id) 
456
	FROM topic
457
	WHERE topic.creation_ts >  ? AND
458
	      topic.creation_ts <= ?');
459
 
460
    push @metrics, @total;
461
 
462
    # Get totals for the topic metrics.
463
    @total = _get_monthly_metrics(12,
464
	'SELECT topicmetric.metric_name, SUM(topicmetric.value) 
465
	FROM topicmetric,topic
466
	WHERE topic.creation_ts >  ? AND
467
	      topic.creation_ts <= ? AND 
468
	      topicmetric.topicid = topic.id
469
	      GROUP BY topicmetric.metric_name
470
	      ORDER BY topicmetric.metric_name');
471
 
472
    push @metrics, @total;
473
 
474
    # Get totals for the topic user metrics.
475
    @total = _get_monthly_metrics(12,
476
	'SELECT topicusermetric.metric_name, SUM(topicusermetric.value) 
477
	FROM topicusermetric,topic
478
	WHERE topic.creation_ts >  ? AND
479
	      topic.creation_ts <= ? AND 
480
	      topicusermetric.topicid = topic.id
481
	      GROUP BY topicusermetric.metric_name
482
	      ORDER BY topicusermetric.metric_name');
483
 
484
    push @metrics, @total;
485
 
486
 
487
    return @metrics;
488
}
489
 
490
 
491
# Returns $total_months of data for the given query. The query must return
492
# name, count collection of rows between two times.
493
#
494
# returns a collection of the following hash references
495
# {
496
#   name  = the metric name
497
#   counts = array ref to metric counts per month
498
#   monthnames = array ref to month names
499
# }
500
sub _get_monthly_metrics {
501
    my ($total_months, $dbi_query_string) = @_;
502
 
503
    # Obtain a database connection.
504
    my $dbh = Codestriker::DB::DBI->get_connection();
505
 
506
    my @month_dbi_times = _dbi_month_time_stamp($total_months);
507
    my @month_names = _user_month_name_list($total_months);
508
 
509
    my @metrics;
510
 
511
    # For the past year, get the metrics counts for each month. 
512
    for (my $month_count = 0;
513
	 $month_count+1 < @month_dbi_times;
514
	 ++$month_count) {
515
 
516
	# Do the db query.
517
	my $comment_counts = $dbh->selectall_arrayref(
518
	    $dbi_query_string,
519
	    {}, 
520
	    $month_dbi_times[$month_count],
521
	    $month_dbi_times[$month_count+1]);
522
 
523
	foreach my $row (@$comment_counts) {
524
	    my ($db_metric_name, $db_count) = @$row;
525
 
526
	    my $found = 0;
527
 
528
	    # See if we can find the metric.
529
	    foreach my $metric (@metrics) {
530
		if ($metric->{name} eq $db_metric_name) {
531
		    push @{$metric->{counts}}, $db_count;
532
		    $found = 1;
533
		    last;
534
		}
535
	    }
536
 
537
	    if ($found == 0) {
538
		my $metric = 
539
		    {
540
		    name=>$db_metric_name,
541
		    counts=>[],
542
		    monthnames=>\@month_names
543
		    };
544
 
545
		# Catch up the collection of counts on any missed months.
546
		for( my $missingmonths = 0; 
547
		     $missingmonths < $month_count; 
548
		     ++$missingmonths) {
549
		    push @{$metric->{counts}}, 0;
550
		}
551
 
552
		push @{$metric->{counts}}, $db_count;
553
 
554
		push @metrics, $metric;	    
555
	    }
556
	}
557
 
558
	# Add zero's to any metrics not present.
559
    	foreach my $metric (@metrics) {
560
	    if (@{$metric->{counts}} eq $month_count) {
561
		push @{$metric->{counts}}, 0;
562
	    }
563
	}
564
    }
565
 
566
    # Close the connection, and check for any database errors.
567
    Codestriker::DB::DBI->release_connection($dbh, 1);
568
 
569
    return @metrics;
570
}
571
 
572
# Return a list of dbi time stamp on 1 month boundaries back for n months. This
573
# is used to do db queries to get data cut up by month.
574
sub _dbi_month_time_stamp {
575
    my ($number_months_back) = @_;
576
 
577
    # Get the start time of this month
578
 
579
    my @month = _add_month(-$number_months_back+1,localtime(time()));
580
 
581
    my @month_dbi_ts;
582
 
583
    for (my $count = 0; $count < $number_months_back +1; ++$count) {
584
	# Calculate the start of this month dbi string.
585
	my $month_start = sprintf("%04d-%02d-01 00:00:00", 
586
	    $month[5]+1900,
587
	    $month[4]+1);
588
 
589
	push @month_dbi_ts, $month_start;
590
 
591
	@month = _add_month(1, @month);
592
    }
593
 
594
    return @month_dbi_ts; 
595
}
596
 
597
# Return a list of user displayable time stamps on 1 month bondaries
598
# back for n months.
599
sub _user_month_name_list {
600
    my ($number_months_back) = @_;
601
 
602
    # Get the start time of this month.
603
    my @month = _add_month(-$number_months_back+1,localtime(time()));
604
 
605
    my @month_names;
606
 
607
    for (my $count = 0; $count < $number_months_back; ++$count) {
608
	my $month_name = $Codestriker::short_months[$month[4]] .
609
	    " " . ($month[5]+1900);
610
 
611
	push @month_names, $month_name;
612
 
613
	@month = _add_month(1, @month);
614
    }
615
 
616
    return @month_names; 
617
}
618
 
619
 
620
# Add or substracts count months on to a time array.
621
sub _add_month {
622
    my ($count,@time) = @_;
623
 
624
    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = @time;
625
 
626
    if ($count > 0) {
627
	for (my $i = 0; $i < $count; ++$i) {
628
	    # Calculate the end of this month dbi string.
629
	    ++$mon;
630
 
631
	    if ($mon >= 12) {
632
		$mon = 0;
633
		++$year;
634
    	    }
635
	}
636
    }
637
    elsif ($count < 0) {
638
	for (my $i = $count; $i < 0; ++$i) {
639
	    # Calculate the end of this month dbi string.
640
	    --$mon;
641
 
642
	    if ($mon < 0) {
643
		$mon = 11;
644
		--$year;
645
    	    }
646
	}
647
    }
648
 
649
    $time[4] = $mon;
650
    $time[5] = $year;
651
 
652
    return @time;
653
}
654
 
655
1;