Subversion Repositories DevTools

Rev

Details | 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 metric data.
8
 
9
package Codestriker::Model::Metrics;
10
 
11
use strict;
12
use Encode qw(decode_utf8);
13
 
14
use Codestriker::DB::DBI;
15
 
16
sub new {
17
    my ($class, $topicid) = @_;
18
 
19
    my $self = {};
20
 
21
    $self->{topicmetrics} = undef;
22
    $self->{topicid} = $topicid;
23
    $self->{usermetrics} = {};
24
    $self->{topichistoryrows} = undef;
25
 
26
    bless $self, $class;
27
 
28
    return $self;
29
}
30
 
31
# Sets the topic metrics values. The values are passed in as an
32
# array. The array must be in the same order returned by
33
# get_topic_metric(). Metrics that are bad are silently not stored.
34
sub set_topic_metrics {
35
    my ($self,@metric_values) = @_;
36
 
37
    my @metrics = $self->get_topic_metrics();
38
 
39
    for (my $index = 0; $index < scalar(@metrics); ++$index) {
40
	next if ($metrics[$index]->{enabled} == 0);
41
	die "error: not enough metrics" if (scalar(@metric_values) == 0);
42
 
43
	my $value = shift @metric_values;
44
 
45
	if ($self->_verify_metric($metrics[$index], $value) eq '') {
46
	    $metrics[$index]->{value} = $value;
47
	}
48
    }
49
}
50
 
51
# Verifies that all of the topic metrics are well formed and valid. It will
52
# return a non-empty string if a problem is found.
53
sub verify_topic_metrics {
54
    my ($self,@metric_values) = @_;
55
 
56
    my $msg = '';
57
 
58
    my @metrics = $self->get_topic_metrics();
59
 
60
    for (my $index = 0; $index < scalar(@metrics); ++$index) {
61
	next if ($metrics[$index]->{enabled} == 0);
62
 
63
	# Disabled values may be in the database (somebody turned off
64
	# the metrics).  However, they are not paramters so the index
65
	# between the paramters and the metrics objects will not
66
	# match.
67
	my $value = shift @metric_values;
68
 
69
	$msg .= $self->_verify_metric($metrics[$index], $value);
70
    }
71
 
72
    return $msg;
73
}
74
 
75
 
76
# Returns the topic metrics as a collection of references to
77
# hashs. The hash that is returned has the same keys as the
78
# metrics_schema hash, plus a value key. If the user has not entered a
79
# value, it will be set to an empty string.
80
sub get_topic_metrics {
81
    my $self = shift;
82
 
83
    my @topic_metrics;
84
 
85
    if (defined($self->{topicmetrics})) {
86
	# The topic metrics have already been loaded from the
87
	# database, just return the cached data.
88
	@topic_metrics = @{$self->{topicmetrics}};
89
    }
90
    else {
91
	my @stored_metrics = ();
92
 
93
	if (defined($self->{topicid})) {
94
	    # Obtain a database connection.
95
	    my $dbh = Codestriker::DB::DBI->get_connection();
96
 
97
	    my $select_topic_metrics = 
98
		$dbh->prepare_cached('SELECT topicmetric.metric_name, 
99
					     topicmetric.value ' .
100
				     'FROM topicmetric ' .
101
		                     'WHERE topicmetric.topicid = ?');
102
 
103
	    $select_topic_metrics->execute($self->{topicid}); 
104
 
105
	    @stored_metrics = @{$select_topic_metrics->fetchall_arrayref()};
106
 
107
	    # Close the connection, and check for any database errors.
108
	    Codestriker::DB::DBI->release_connection($dbh, 1);
109
	}
110
 
111
	# Match the configured metrics to the metrics in the database. If 
112
	# the configured metric is found in the database, it is removed 
113
	# from the stored_metric list to find any data that is in the 
114
	# database, but is not configured.
115
	foreach my $metric_schema (Codestriker::get_metric_schema()) {
116
	    if ($metric_schema->{scope} eq 'topic') {
117
		my $metric =
118
		    { # This is the topic metric.
119
		    name        => $metric_schema->{name},
120
		    description => $metric_schema->{description},
121
		    value       => '',
122
		    filter      => $metric_schema->{filter},
123
		    enabled     => $metric_schema->{enabled},
124
		    in_database => 0
125
		    };
126
 
127
		for (my $index = 0; $index < scalar(@stored_metrics); ++$index) {
128
		    my $stored_metric = $stored_metrics[$index];
129
 
130
		    if ($stored_metric->[0] eq $metric_schema->{name}) {
131
			$metric->{value} = $stored_metric->[1];
132
			$metric->{in_database} = 1;
133
			splice @stored_metrics, $index, 1;
134
			last;
135
		    }
136
		}
137
 
138
		if ($metric_schema->{enabled} || $metric->{in_database}) {
139
		    push @topic_metrics, $metric;
140
		}
141
	    }
142
	}
143
 
144
	# Add in any metrics that are in the database but not
145
	# currently configured.  The system should display the
146
	# metrics, but not let the user modify them.
147
	for (my $index = 0; $index < scalar(@stored_metrics); ++$index) {
148
	    my $stored_metric = $stored_metrics[$index];
149
 
150
	    # This is the topic metric.
151
	    my $metric =
152
		{
153
		name         => $stored_metric->[0],
154
		description  => '',
155
		value        => $stored_metric->[1],
156
 
157
		# User can not change the metric, not configured.
158
		enabled      => 0,
159
		in_database  => 1
160
		};
161
 
162
	    push @topic_metrics, $metric;
163
	}
164
 
165
	push @topic_metrics, $self->_get_built_in_topic_metrics();
166
 
167
	$self->{topicmetrics} = \@topic_metrics;
168
    }
169
 
170
    return @topic_metrics;
171
}
172
 
173
# Get just the list of users that have actually looked at the review. This is
174
# used on the main page to out users that are not doing the reviews when invited.
175
sub get_list_of_actual_topic_participants {
176
    my ($self) = @_;
177
 
178
    my $dbh = Codestriker::DB::DBI->get_connection();
179
 
180
    my $actual_user_list_ref = 
181
         $dbh->selectall_arrayref(
182
	        'SELECT DISTINCT LOWER(email) FROM topicviewhistory ' .
183
	        'WHERE topicid = ?',{}, $self->{topicid});
184
 
185
    my @actual_user_list = ();
186
    foreach my $user ( @$actual_user_list_ref ) {
187
        push @actual_user_list,$user->[0] if defined $user->[0] && $user->[0] ne "";
188
    }
189
 
190
    # Close the connection, and check for any database errors.
191
    Codestriker::DB::DBI->release_connection($dbh, 1);
192
 
193
    return @actual_user_list;
194
}
195
 
196
# Get a list of users that have metric data for this topic. People can 
197
# look at the topic even if they were not invited, so if somebody touches the 
198
# topic, they will appear in this list. Using this function rather than the 
199
# invite list from the topic will insure that people don't get missed from 
200
# the metric data.
201
sub get_complete_list_of_topic_participants {
202
 
203
    my ($self) = @_;
204
 
205
    my $dbh = Codestriker::DB::DBI->get_connection();
206
 
207
 
208
    my @metric_user_list = @{ $dbh->selectall_arrayref('
209
	    SELECT distinct LOWER(email) 
210
	    from participant WHERE topicid = ?',{}, $self->{topicid})};
211
 
212
    push @metric_user_list, @{ $dbh->selectall_arrayref('
213
	    SELECT LOWER(author) FROM topic WHERE id = ?',{}, $self->{topicid})};
214
 
215
    push @metric_user_list, @{ $dbh->selectall_arrayref('
216
	    SELECT DISTINCT LOWER(email) FROM topicusermetric 
217
	    WHERE topicid = ?',{}, $self->{topicid})};
218
 
219
    push @metric_user_list, @{ $dbh->selectall_arrayref(
220
	    'SELECT DISTINCT LOWER(author) FROM commentdata, commentstate ' .
221
	    'WHERE commentstate.topicid = ? AND
222
		   commentstate.id = commentdata.commentstateid ',
223
		   {}, $self->{topicid})};
224
 
225
    push @metric_user_list, @{ $dbh->selectall_arrayref(
226
	    'SELECT DISTINCT LOWER(email) FROM topicviewhistory ' .
227
	    'WHERE topicid = ? AND email IS NOT NULL',{}, $self->{topicid})};
228
 
229
    # remove the duplicates.
230
 
231
    my %metric_user_hash;
232
    foreach my $user (@metric_user_list) {
233
	$metric_user_hash{$user->[0]} = 1;
234
    }
235
 
236
    # Need to sort the empty user name last so that the template parameters 
237
    # that are done by index don't start at 1, and therefor not allow users
238
    # to save the metrics.
239
    @metric_user_list = sort { 
240
        return 1  if ( $a eq "");
241
        return -1 if ( $b eq "");
242
        return $a cmp $b; 
243
    } keys %metric_user_hash;
244
 
245
    # Close the connection, and check for any database errors.
246
    Codestriker::DB::DBI->release_connection($dbh, 1);
247
 
248
    return @metric_user_list;
249
}
250
 
251
# Get a list of users that are invited for this review.
252
sub get_list_of_topic_participants {
253
 
254
    my ($self) = @_;
255
 
256
    my $dbh = Codestriker::DB::DBI->get_connection();
257
 
258
 
259
    my @metric_user_list = @{ $dbh->selectall_arrayref('
260
	    SELECT distinct LOWER(email) 
261
	    from participant WHERE topicid = ?',{}, $self->{topicid})};
262
 
263
    push @metric_user_list, @{ $dbh->selectall_arrayref('
264
	    SELECT LOWER(author) FROM topic WHERE id = ?',{}, $self->{topicid})};
265
 
266
    # remove the duplicates.
267
 
268
    my %metric_user_hash;
269
    foreach my $user (@metric_user_list) {
270
	$metric_user_hash{$user->[0]} = 1;
271
    }
272
 
273
    # Need to sort the empty user name last so that the template parameters 
274
    # that are done by index don't start at 1, and therefor not allow users
275
    # to save the metrics.
276
    @metric_user_list = sort { 
277
        return 1  if ( $a eq "");
278
        return -1 if ( $b eq "");
279
        return $a cmp $b; 
280
    } keys %metric_user_hash;
281
 
282
    # Close the connection, and check for any database errors.
283
    Codestriker::DB::DBI->release_connection($dbh, 1);
284
 
285
    return @metric_user_list;
286
}
287
 
288
# Sets the metrics for a specific user, both authors and reviewers. cc's don't 
289
# get metrics. The metrics are sent in as an array, that must in the same 
290
# order as the get_user_metric() call returns them. Metrics that are bad are 
291
# silently not stored.
292
sub set_user_metric {
293
    my ($self, $user, @metric_values) = @_;
294
 
295
    my @metrics = $self->get_user_metrics($user);
296
 
297
    for (my $index = 0; $index < scalar(@metrics); ++$index) {
298
	next if ($metrics[$index]->{enabled} == 0);
299
	die "error: not enough metrics" if (scalar(@metric_values) == 0);
300
 
301
	# Disabled values may be in the database (somebody turned off
302
	# the metrics).  However, they are not paramters so the index
303
	# between the paramters and the metrics objects will not
304
	# match.
305
	my $value = shift @metric_values;
306
 
307
	if ($self->_verify_metric($metrics[$index], $value) eq '') {
308
	    $metrics[$index]->{value} = $value
309
	}
310
    }
311
}
312
 
313
# Verifies that all of the user metrics are well formed and valid inputs. If a 
314
# problem is found the function will return a non-empty string.
315
sub verify_user_metrics {
316
    my ($self, $user, @metric_values) = @_;
317
 
318
    my $msg = '';
319
 
320
    my @metrics = $self->get_user_metrics($user);
321
 
322
    for (my $index = 0; $index < scalar(@metrics); ++$index) {
323
	next if ($metrics[$index]->{enabled} == 0);
324
 
325
	# Disabled values may be in the database (somebody turned off
326
	# the metrics).  However, they are not paramters so the index
327
	# between the paramters and the metrics objects will not
328
	# match.
329
	my $value = shift @metric_values;
330
 
331
	$msg .= $self->_verify_metric($metrics[$index], $value);
332
    }
333
 
334
    return $msg;
335
}
336
 
337
 
338
# Returns the user metrics as a collection of references to hashs. The
339
# hash that is returned has the same keys as the metrics_schema hash,
340
# plus a value key. If the user has not entered a value, it will be
341
# set to an empty string.
342
sub get_user_metrics {
343
 
344
    my ($self, $username) = @_;
345
 
346
    my @user_metrics;
347
 
348
    if (exists($self->{usermetrics}->{$username})) {
349
	# If the metrics for this user has already been loaded from
350
	# the database, return the cached result of that load.
351
	@user_metrics = @{$self->{usermetrics}->{$username}};
352
    }
353
    else {    
354
	my @stored_metrics = ();
355
 
356
	if (defined($self->{topicid})) {
357
	    # Obtain a database connection.
358
	    my $dbh = Codestriker::DB::DBI->get_connection();
359
 
360
 
361
	    # Get all of the user outputs for this topic regardless of
362
	    # the user.
363
	    my $selected_all_user_metrics = 
364
		$dbh->prepare_cached('SELECT DISTINCT metric_name ' .
365
				     'FROM topicusermetric ' .
366
				     'WHERE topicid = ? ' .
367
				     'ORDER BY metric_name');
368
	    $selected_all_user_metrics->execute($self->{topicid}); 
369
	    @stored_metrics =
370
		@{$selected_all_user_metrics->fetchall_arrayref()};
371
 
372
	    # Get the outputs for this user.
373
	    my $select_user_metrics = 
374
		$dbh->prepare_cached('SELECT metric_name, value ' .
375
				     'FROM topicusermetric ' .
376
				     'WHERE topicid = ? AND LOWER(email) = LOWER(?) ' .
377
				     'ORDER BY metric_name');
378
 
379
	    $select_user_metrics->execute($self->{topicid}, $username);
380
 
381
	    my @user_stored_metrics =
382
		@{$select_user_metrics->fetchall_arrayref()};
383
 
384
	    # Stuff the complete list with the values from the current
385
	    # user list.  Handle displaying metrics that are in the
386
	    # db, but not enabled for new topics.
387
	    foreach my $metric (@stored_metrics) {
388
		my $foundit = 0;
389
		foreach my $user_metric (@user_stored_metrics) {
390
		    if ($user_metric->[0] eq $metric->[0]) {
391
			$foundit = 1;
392
			push @$metric, $user_metric->[1];
393
			last;
394
		    }
395
		}
396
 
397
		if ($foundit == 0) {
398
		    push @$metric,'';
399
		}
400
	    }
401
 
402
	    # Close the connection, and check for any database errors.
403
	    Codestriker::DB::DBI->release_connection($dbh, 1);
404
	}
405
 
406
	foreach my $metric_schema (Codestriker::get_metric_schema()) {
407
	    if ($metric_schema->{scope} ne 'topic') {
408
		my $metric = 
409
		{ 
410
		    name        => $metric_schema->{name},
411
		    description => $metric_schema->{description},
412
		    value       => '',
413
		    enabled     => $metric_schema->{enabled},
414
		    scope       => $metric_schema->{scope},
415
		    filter      => $metric_schema->{filter},
416
		};
417
 
418
 
419
		for (my $index = 0; $index < scalar(@stored_metrics);
420
		     ++$index) {
421
		    my $stored_metric = $stored_metrics[$index];
422
 
423
		    if ($stored_metric->[0] eq $metric_schema->{name}) {
424
			$metric->{value} = $stored_metric->[1];
425
			$metric->{in_database} = 1;
426
			splice @stored_metrics, $index,1;
427
			last;
428
		    }
429
		}
430
 
431
		if ($metric_schema->{enabled} || $metric->{in_database}) {
432
 
433
                    if ($username eq "") {
434
                        # don't let any metrics be set into the db for unknown users.
435
                        $metric->{enabled} = 0;
436
                    }
437
 
438
		    push @user_metrics, $metric;
439
		}
440
	    }
441
	}
442
 
443
	# Clean up any metrics that are in the database but not in the
444
	# schema, we will not let them change them, and we don't have
445
	# the description anymore.
446
	for (my $index = 0; $index < scalar(@stored_metrics); ++$index) {
447
	    my $stored_metric = $stored_metrics[$index];
448
 
449
	    my $metric =
450
		{ # this is the topic metric  
451
		name=>$stored_metric->[0],
452
		description=>'',
453
		value=>$stored_metric->[1],
454
		scope=>'participant',
455
		enabled=>0, # user can not change the metric, no schema.
456
		in_database=>1
457
		};
458
 
459
	    push @user_metrics, $metric;
460
	}
461
 
462
	$self->{usermetrics}->{$username} = \@user_metrics;
463
    }
464
 
465
    push @user_metrics, $self->_get_built_in_user_metrics($username);
466
 
467
    return @user_metrics;
468
}
469
 
470
 
471
# Returns the user metrics as a collection of references to hashs. 
472
sub get_user_metrics_totals {
473
    my ($self,@users) = @_;
474
 
475
    my @user_metrics;
476
 
477
    if (exists($self->{usermetrics_totals})) {
478
	@user_metrics = @$self->{usermetrics_totals};
479
    }
480
 
481
    my @total_metrics;
482
 
483
    foreach my $user (@users) {
484
	my @metrics = $self->get_user_metrics($user);
485
 
486
	if (scalar(@total_metrics) == 0) {
487
	    # Copy the metrics in.
488
 
489
	    foreach my $metric (@metrics) {
490
		my %temp = %$metric;
491
		push @total_metrics, \%temp;
492
	    }
493
 
494
	}
495
	else {
496
	    # Add them up!
497
	    for (my $index = 0; $index < scalar(@total_metrics) ; ++$index) {
498
		if ($metrics[$index]->{value} ne '') {
499
		    if ($total_metrics[$index]->{value} eq '') {
500
			$total_metrics[$index]->{value} = 0;
501
		    }
502
 
503
		    $total_metrics[$index]->{value} +=
504
			$metrics[$index]->{value};		
505
		}
506
	    }
507
	}
508
 
509
    }
510
 
511
    $self->{usermetrics_totals}= \@total_metrics;
512
 
513
    return @total_metrics;
514
}
515
 
516
# Returns a list of hashes. Each hash is an event. In the hash is stored who 
517
# caused the event, when it happened, and what happened. The hashes are defined
518
# as: 
519
#   email -> the email address of the user who caused the event.
520
#   date  -> when the event happened.
521
#   description -> the event description.
522
#
523
# The topic must be loaded from the db before this function can be called.
524
sub get_topic_history {
525
    my ($self) = @_;
526
 
527
    my @topic_history = $self->_get_topic_history_rows();
528
 
529
    my @event_list;
530
 
531
    my $last_history_row;
532
 
533
    foreach my $current_history_row (@topic_history) {
534
	if ( !defined($last_history_row) ) {
535
	    # The first event is always the topic creation, so lets make 
536
	    # that now.
537
 
538
	    my $filteredemail = 
539
		Codestriker->filter_email($current_history_row->{author});
540
 
541
	    my $formatted_time = 
542
	        Codestriker->format_short_timestamp($current_history_row->{modified_ts});
543
 
544
	    push @event_list, 
545
	    { 
546
		email=>$filteredemail,
547
		date =>$formatted_time,
548
		description=>'The topic is created.' 
549
	    };
550
	}
551
	else {
552
	    my %event = 
553
	    ( 
554
		email=> Codestriker->filter_email(
555
			$current_history_row->{modified_by}),
556
		date => Codestriker->format_short_timestamp(
557
			$current_history_row->{modified_ts}),
558
		description=>'' 
559
	    );
560
 
561
	    # Look for changes in all of the fields. Several fields could have 
562
	    # changed at once.
563
 
564
	    if ($current_history_row->{author} ne $last_history_row->{author}) {
565
		my %new_event = %event;
566
		$new_event{description} = 
567
		    "Author changed: $last_history_row->{author} to " . 
568
		    "$current_history_row->{author}.";
569
		push @event_list, \%new_event;
570
	    }
571
 
572
	    if ($current_history_row->{title} ne $last_history_row->{title}) {
573
		my %new_event = %event;
574
		$new_event{description} = 
575
		    "Title changed to: \"$current_history_row->{title}\".";
576
		push @event_list, \%new_event;
577
	    }
578
 
579
	    if ($current_history_row->{description} ne $last_history_row->{description}) {
580
		my %new_event = %event;
581
		$new_event{description} = "Description changed to: " . 
582
		    "$current_history_row->{description}.";
583
		push @event_list, \%new_event;
584
 
585
	    }
586
 
587
	    if ($current_history_row->{state} ne $last_history_row->{state}) {
588
		my %new_event = %event;
589
		$new_event{description} = 
590
		    "Topic state changed to: " . 
591
		    $Codestriker::topic_states[$current_history_row->{state}];
592
		push @event_list, \%new_event;
593
 
594
	    }
595
 
596
	    if ($current_history_row->{repository} ne $last_history_row->{repository}) {
597
		my %new_event = %event;
598
		$new_event{description} = 
599
		    "Repository changed to: $current_history_row->{repository}.";
600
		push @event_list, \%new_event;
601
 
602
	    }
603
 
604
	    if ($current_history_row->{project} ne $last_history_row->{project}) {
605
		my %new_event = %event;
606
		$new_event{description} = 
607
		    "Project changed to: $current_history_row->{project}.";
608
		push @event_list, \%new_event;
609
	    }
610
 
611
	    if ($current_history_row->{reviewers} ne $last_history_row->{reviewers}) {
612
		my %new_event = %event;
613
 
614
		# Figure out who was removed, and who was added to the list.
615
		my @reviewers = split /,/,$current_history_row->{reviewers};
616
		my @l_reviewers = split /,/,$last_history_row->{reviewers};
617
		my @new;
618
		my @removed;
619
 
620
                Codestriker::set_differences(\@reviewers, \@l_reviewers, \@new, \@removed);
621
 
622
		if (@new == 0) {
623
    		    $new_event{description} = 
624
			"Reviewers removed: " . join(',',@removed);;
625
		}
626
		elsif (@removed == 0) {
627
    		    $new_event{description} = 
628
			"Reviewers added: " . join(',',@new);
629
		}
630
		else {
631
    		    $new_event{description} = 
632
			"Reviewers added: " . join(',',@new) . 
633
			" and reviewers removed: " . join(',',@removed);
634
		}
635
 
636
		push @event_list, \%new_event;
637
	    }
638
 
639
	    if ($current_history_row->{cc} ne $last_history_row->{cc}) {
640
		my %new_event = %event;
641
		$new_event{description} = 
642
		    "CC changed to $current_history_row->{cc}.";
643
		push @event_list, \%new_event;
644
	    }
645
	}
646
 
647
	$last_history_row = $current_history_row
648
    }
649
 
650
    return @event_list;
651
}
652
 
653
# Returns the topic metrics as a collection of references to
654
# hashes. The hash that is returned has the same keys as the
655
# metrics_schema hash, plus a value key. This private function
656
# returns "built in" metrics derived from the topic history
657
# table.
658
sub _get_built_in_topic_metrics {
659
    my $self = shift;
660
 
661
    my @topic_metrics;
662
 
663
    my @topic_history = $self->_get_topic_history_rows();
664
 
665
    my %state_times;
666
 
667
    my $last_history_row;
668
 
669
    # Figure out how long the topic has spent in each state.
670
 
671
    for ( my $topic_history_index = 0; 
672
	  $topic_history_index <= scalar(@topic_history);
673
	  ++$topic_history_index) {
674
 
675
	my $current_history_row;
676
 
677
	if ($topic_history_index < scalar(@topic_history)) {
678
	    $current_history_row = $topic_history[$topic_history_index];
679
	}
680
 
681
	if (defined($last_history_row)) {
682
	    my $start = 
683
		Codestriker->convert_date_timestamp_time( 
684
		    $last_history_row->{modified_ts});
685
	    my $end   = 0;
686
 
687
	    if (defined($current_history_row)) {
688
		$end = Codestriker->convert_date_timestamp_time( 
689
		    $current_history_row->{modified_ts});
690
	    }
691
	    else {
692
		$end = time();
693
	    }
694
 
695
	    if (exists($state_times{$last_history_row->{state}})) {
696
		$state_times{$last_history_row->{state}} += $end - $start;
697
	    }
698
	    else {
699
		$state_times{$last_history_row->{state}} = $end - $start;
700
	    }
701
	}
702
 
703
	$last_history_row = $current_history_row
704
    }
705
 
706
    foreach my $state ( sort keys %state_times) {
707
	my $statename = $Codestriker::topic_states[$state];
708
	my $time_days = sprintf("%1.1f",$state_times{$state} / (60*60*24));
709
 
710
	# This is the topic metric.
711
	my $metric =
712
	    {
713
	    name         => 'Time In ' . $statename,
714
	    description  => 
715
		'Time in days the topic spent in the ' . $statename . ' state.',
716
	    value        => $time_days,
717
 
718
	    # User can not change the metric, not configured.
719
	    enabled      => 0,
720
	    in_database  => 0,
721
	    filter       =>"count",
722
	    builtin      => 1,
723
	    };	
724
 
725
	push @topic_metrics, $metric;
726
    }
727
 
728
    return @topic_metrics;
729
}
730
 
731
 
732
# Returns the user topic metrics as a collection of references to
733
# hashs. The hash that is returned has the same keys as the
734
# metrics_schema hash, plus a value key. This private function
735
# returns "built in" metrics derived from the topic history
736
# table and the topic view history table.
737
sub _get_built_in_user_metrics {
738
 
739
    my ($self,$username) = @_;
740
 
741
    my @user_metrics;
742
 
743
    my $dbh = Codestriker::DB::DBI->get_connection();
744
 
745
    # Setup the prepared statements.
746
    my $select_topic = $dbh->prepare_cached('SELECT creation_ts ' .
747
					    'FROM topicviewhistory ' .
748
					    'WHERE topicid = ? AND ' .
749
					    'LOWER(email) = LOWER(?) ' .
750
					    'ORDER BY creation_ts');
751
 
752
    $select_topic->execute($self->{topicid}, $username);
753
 
754
#    my $total_time = $self->calculate_topic_view_time($select_topic);
755
    my $total_time = 0;
756
 
757
    Codestriker::DB::DBI->release_connection($dbh, 1);
758
 
759
    if ($total_time == 0) {
760
	$total_time = "";
761
    }
762
    else {
763
	$total_time = sprintf("%1.0f",$total_time / (60));
764
    }
765
 
766
    # This is the topic metric.
767
    my $metric =
768
	{
769
	name         => 'Codestriker Time',
770
	description  => 
771
	    'Time in minutes spent in Codestriker looking at this topic.',
772
	value        => $total_time,
773
	enabled      => 0,
774
	in_database  => 0,
775
	filter       =>"minutes",
776
	builtin      => 1,
777
	scope        =>'participant',
778
	};	
779
 
780
    push @user_metrics, $metric;
781
 
782
    return @user_metrics;
783
}
784
 
785
# Given a DBI statement that returns a sorted collection of timestamps from 
786
# the topicviewhistory table, return the total time.
787
sub calculate_topic_view_time {
788
 
789
    my ($self,$select_topic) = @_;
790
 
791
    # The amount of time you give to people after a click assuming no other
792
    # clicks are after it.
793
    my $time_increment = 4*60;
794
 
795
    my $total_time = 0;
796
    my $last_time = 0;    
797
 
798
    while ( my @row_array = $select_topic->fetchrow_array) {
799
	my ($creation_ts) = @row_array;
800
 
801
	my $time = Codestriker->convert_date_timestamp_time($creation_ts);
802
 
803
	if ($last_time) {
804
 
805
	    if ($time - $last_time > $time_increment) {
806
		$total_time += $time_increment
807
	    }
808
	    else {
809
		$total_time += $time - $last_time;
810
	    }
811
	}
812
 
813
	$last_time = $time;
814
    }
815
 
816
    if ($last_time) {
817
	$total_time += $time_increment;
818
    }
819
 
820
    return $total_time;
821
 
822
}
823
 
824
# Returns the topichistory rows as an array of hashes. Each element in the 
825
# array is a row, each field in the table is a key. It will only fetch if 
826
# from the db once.
827
sub _get_topic_history_rows {
828
 
829
    my ($self) = @_;
830
 
831
    if (defined( $self->{topichistoryrows}))  {
832
	return @{$self->{topichistoryrows}};
833
    }
834
    else {
835
	my $dbh = Codestriker::DB::DBI->get_connection();
836
 
837
	my @history_list;
838
 
839
	# Setup the prepared statements.
840
	my $select_topic = $dbh->prepare_cached('SELECT topichistory.author, ' .
841
						'topichistory.title, ' .
842
						'topichistory.description, ' .
843
						'topichistory.state, ' .
844
						'topichistory.modified_ts, ' .
845
						'topichistory.version, ' .
846
						'topichistory.repository, ' .
847
						'project.name, ' .
848
						'topichistory.reviewers, ' .
849
						'topichistory.cc, ' .
850
						'topichistory.modified_by_user ' .
851
						'FROM topichistory, project ' .
852
						'WHERE topichistory.topicid = ? AND ' .
853
						'topichistory.projectid = project.id ' .
854
						'ORDER BY topichistory.version');
855
 
856
	$select_topic->execute($self->{topicid});
857
 
858
	while ( my @row_array = $select_topic->fetchrow_array) {
859
	    my ($author,$title,$description,$state,$modified_ts, $version,
860
		$repository,$project,$reviewers,$cc, $modified_by) = @row_array;
861
 
862
	    my %entry = ( 
863
	      author=>$author,
864
	      title=>decode_utf8($title),
865
	      description=>decode_utf8($description),
866
	      state=>$state,
867
	      modified_ts=>$modified_ts,
868
	      version=>$version,
869
	      repository=>$repository,
870
	      project=>decode_utf8($project),
871
	      reviewers=>lc($reviewers),
872
	      cc=>lc($cc), 
873
	      modified_by=>lc($modified_by)
874
	      );
875
 
876
	    push @history_list, \%entry;
877
	}
878
 
879
	Codestriker::DB::DBI->release_connection($dbh, 1);
880
 
881
	$self->{topichistoryrows} = \@history_list;
882
 
883
	return @history_list;
884
    }
885
}
886
 
887
 
888
# Returns an error message if a number is not a valid value for a given metric.
889
sub _verify_metric {
890
    my ($self, $metric, $value) = @_;
891
 
892
    my $msg = '';
893
    if ($metric->{enabled}) {
894
	my $input_ok = 0;
895
 
896
	if ($metric->{filter} eq "hours") {
897
	    $input_ok = ($value =~ /(^[\d]+([\.:][\d]*)?$)|(^$)/);
898
	    $msg = $metric->{name} .
899
		   " must be a valid time in hours. " . HTML::Entities::encode($value) . " was " . 
900
	           "not saved.<BR>" unless $input_ok;
901
	}
902
	elsif ($metric->{filter} eq "minutes") {
903
	    $input_ok = ($value =~ /(^[\d]+)|(^$)/);
904
	    $msg = $metric->{name} .
905
		   " must be a valid time in minutes. " . HTML::Entities::encode($value) . " was " . 
906
	           "not saved.<BR>" unless $input_ok;
907
	}
908
	elsif ($metric->{filter} eq "count") {
909
	    $input_ok = ($value =~ /(^[\d]+$)|(^$)/);
910
	    $msg = $metric->{name} . 
911
		   " must be a valid count. " . HTML::Entities::encode($value) . " was not " . 
912
	           "saved.<BR>" unless $input_ok;
913
	}
914
	elsif ($metric->{filter} eq "percent") {
915
	    $input_ok = ($value =~ /(^[\d]+(\.[\d]*)?$)|(^$)/);
916
 
917
	    if ($input_ok && $value ne '') {
918
		$input_ok = 0 unless ($value >= 0.0 && $value <= 100.0);
919
	    }
920
	    $msg = $metric->{name} . 
921
		   " must be a valid percent, between 0 and 100. " . 
922
	           HTML::Entities::encode($value) . " was not saved.<BR>" unless $input_ok;
923
	}
924
	else {
925
	    # invalid config.
926
	    $input_ok = 0;
927
	    $msg = HTML::Entities::encode($metric->{name}) . 
928
		   " invalid filter type in configuration. Must " . 
929
	           "be hours, count, or percent.<BR>";
930
	}
931
    }
932
 
933
    return $msg;
934
}
935
 
936
# Stores all of the metrics to the database.
937
sub store {
938
    my ($self) = @_;
939
 
940
    $self->_store_topic_metrics();
941
    $self->_store_user_metrics();
942
}
943
 
944
# Stores the topic metrics to the database.
945
sub _store_user_metrics {
946
    my ($self) = @_;
947
 
948
    foreach my $user (keys %{$self->{usermetrics}}) {
949
	$self->get_user_metrics($user);
950
    }
951
 
952
    # Obtain a database connection.
953
    my $dbh = Codestriker::DB::DBI->get_connection();
954
 
955
    # flush out the user metrics from the topic,
956
    my $delete_alluser_metric =
957
	$dbh->prepare_cached('DELETE FROM topicusermetric ' .
958
			     'WHERE topicid = ?');
959
 
960
    $delete_alluser_metric->execute($self->{topicid});
961
 
962
    my $insert_user_metric =
963
	$dbh->prepare_cached('INSERT INTO topicusermetric (topicid, 
964
						    email, 
965
						    metric_name, 
966
						    value) ' .
967
			     'VALUES (?, LOWER(?), ?, ? )');
968
 
969
    foreach my $user (keys %{$self->{usermetrics}}) {
970
	my @metrics = $self->get_user_metrics($user);
971
 
972
	foreach my $metric (@metrics) {
973
 
974
	    next if ($metric->{builtin});
975
 
976
	    if ($metric->{value} ne '') {
977
		$insert_user_metric->execute($self->{topicid}, 
978
					     $user, 
979
					     $metric->{name}, 
980
					     $metric->{value});	    
981
	    }
982
	}
983
    }
984
 
985
    # Close the connection, and check for any database errors.
986
    Codestriker::DB::DBI->release_connection($dbh, 1);
987
}
988
 
989
# Stores the topic metrics to the database.
990
sub _store_topic_metrics {
991
    my ($self) = @_;
992
 
993
    # Obtain a database connection.
994
    my $dbh = Codestriker::DB::DBI->get_connection();
995
 
996
    # Store the topic metrics first.
997
    my @topic_metrics = $self->get_topic_metrics();
998
 
999
    my $insert_topic_metric =
1000
	$dbh->prepare_cached('INSERT INTO topicmetric (topicid, 
1001
						       metric_name, 
1002
						       value) ' .
1003
			     'VALUES (?, ?, ? )');
1004
    my $update_topic_metric =
1005
	$dbh->prepare_cached('UPDATE topicmetric SET value = ? ' .
1006
			     'WHERE topicid = ? and metric_name = ?');
1007
 
1008
    my $delete_topic_metric =
1009
	$dbh->prepare_cached('DELETE FROM topicmetric ' .
1010
			     'WHERE topicid = ? and metric_name = ?');
1011
 
1012
    foreach my $metric (@topic_metrics) {
1013
	# don't save built in metrics
1014
 
1015
	next if ($metric->{builtin});
1016
 
1017
	if ($metric->{in_database}) {
1018
 
1019
	    if ($metric->{value} ne '') {
1020
		$update_topic_metric->execute($metric->{value}, 
1021
					      $self->{topicid}, 
1022
					      $metric->{name});
1023
	    }
1024
	    else {
1025
		# Delete the row.
1026
		$delete_topic_metric->execute($self->{topicid},
1027
					      $metric->{name});
1028
		$metric->{in_database} = 0;
1029
	    }
1030
	}
1031
	else {
1032
 
1033
	    # New metric that is not in the datbase.
1034
	    if ($metric->{value} ne '') {
1035
		$insert_topic_metric->execute($self->{topicid}, 
1036
					      $metric->{name},
1037
					      $metric->{value});
1038
		$metric->{in_database} = 1;
1039
	    }
1040
	}
1041
 
1042
	$metric->{in_database} = 1;
1043
    }
1044
 
1045
    # Close the connection, and check for any database errors.
1046
    Codestriker::DB::DBI->release_connection($dbh, 1);
1047
}
1048
 
1049
1;
1050