Subversion Repositories DevTools

Rev

Rev 1293 | Blame | Last modification | View Log | RSS feed

###############################################################################
# Copyright (c) 2003 Jason Remillard.  All rights reserved.
#
# This program is free software; you can redistribute it and modify it under
# the terms of the GPL.

# Model object for handling metric data.

package Codestriker::Model::Metrics;

use strict;
use Encode qw(decode_utf8);

use Codestriker::DB::DBI;

sub new {
    my ($class, $topicid) = @_;

    my $self = {};
        
    $self->{topicmetrics} = undef;
    $self->{topicid} = $topicid;
    $self->{usermetrics} = {};
    $self->{topichistoryrows} = undef;

    bless $self, $class;
   
    return $self;
}

# Sets the topic metrics values. The values are passed in as an
# array. The array must be in the same order returned by
# get_topic_metric(). Metrics that are bad are silently not stored.
sub set_topic_metrics {
    my ($self,@metric_values) = @_;

    my @metrics = $self->get_topic_metrics();

    for (my $index = 0; $index < scalar(@metrics); ++$index) {
        next if ($metrics[$index]->{enabled} == 0);
        die "error: not enough metrics" if (scalar(@metric_values) == 0);

        my $value = shift @metric_values;

        if ($self->_verify_metric($metrics[$index], $value) eq '') {
            $metrics[$index]->{value} = $value;
        }
    }
}

# Verifies that all of the topic metrics are well formed and valid. It will
# return a non-empty string if a problem is found.
sub verify_topic_metrics {
    my ($self,@metric_values) = @_;

    my $msg = '';

    my @metrics = $self->get_topic_metrics();

    for (my $index = 0; $index < scalar(@metrics); ++$index) {
        next if ($metrics[$index]->{enabled} == 0);

        # Disabled values may be in the database (somebody turned off
        # the metrics).  However, they are not paramters so the index
        # between the paramters and the metrics objects will not
        # match.
        my $value = shift @metric_values;

        $msg .= $self->_verify_metric($metrics[$index], $value);
    }

    return $msg;
}


# Returns the topic metrics as a collection of references to
# hashs. The hash that is returned has the same keys as the
# metrics_schema hash, plus a value key. If the user has not entered a
# value, it will be set to an empty string.
sub get_topic_metrics {
    my $self = shift;

    my @topic_metrics;

    if (defined($self->{topicmetrics})) {
        # The topic metrics have already been loaded from the
        # database, just return the cached data.
        @topic_metrics = @{$self->{topicmetrics}};
    }
    else {
        my @stored_metrics = ();

        if (defined($self->{topicid})) {
            # Obtain a database connection.
            my $dbh = Codestriker::DB::DBI->get_connection();

            my $select_topic_metrics = 
                $dbh->prepare_cached('SELECT topicmetric.metric_name, 
                                             topicmetric.value ' .
                                     'FROM topicmetric ' .
                                     'WHERE topicmetric.topicid = ?');
                                                    
            $select_topic_metrics->execute($self->{topicid}); 

            @stored_metrics = @{$select_topic_metrics->fetchall_arrayref()};

            # Close the connection, and check for any database errors.
            Codestriker::DB::DBI->release_connection($dbh, 1);
        }

        # Match the configured metrics to the metrics in the database. If 
        # the configured metric is found in the database, it is removed 
        # from the stored_metric list to find any data that is in the 
        # database, but is not configured.
        foreach my $metric_schema (Codestriker::get_metric_schema()) {
            if ($metric_schema->{scope} eq 'topic') {
                my $metric =
                    { # This is the topic metric.
                    name        => $metric_schema->{name},
                    description => $metric_schema->{description},
                    value       => '',
                    filter      => $metric_schema->{filter},
                    enabled     => $metric_schema->{enabled},
                    in_database => 0
                    };

                for (my $index = 0; $index < scalar(@stored_metrics); ++$index) {
                    my $stored_metric = $stored_metrics[$index];

                    if ($stored_metric->[0] eq $metric_schema->{name}) {
                        $metric->{value} = $stored_metric->[1];
                        $metric->{in_database} = 1;
                        splice @stored_metrics, $index, 1;
                        last;
                    }
                }

                if ($metric_schema->{enabled} || $metric->{in_database}) {
                    push @topic_metrics, $metric;
                }
            }
        }

        # Add in any metrics that are in the database but not
        # currently configured.  The system should display the
        # metrics, but not let the user modify them.
        for (my $index = 0; $index < scalar(@stored_metrics); ++$index) {
            my $stored_metric = $stored_metrics[$index];

            # This is the topic metric.
            my $metric =
                {
                name         => $stored_metric->[0],
                description  => '',
                value        => $stored_metric->[1],

                # User can not change the metric, not configured.
                enabled      => 0,
                in_database  => 1
                };

            push @topic_metrics, $metric;
        }

        push @topic_metrics, $self->_get_built_in_topic_metrics();

        $self->{topicmetrics} = \@topic_metrics;
    }

    return @topic_metrics;
}

# Get just the list of users that have actually looked at the review. This is
# used on the main page to out users that are not doing the reviews when invited.
sub get_list_of_actual_topic_participants {
    my ($self) = @_;

    my $dbh = Codestriker::DB::DBI->get_connection();

    my $actual_user_list_ref = 
         $dbh->selectall_arrayref(
                'SELECT DISTINCT LOWER(email) FROM topicviewhistory ' .
                'WHERE topicid = ?',{}, $self->{topicid});

    my @actual_user_list = ();
    foreach my $user ( @$actual_user_list_ref ) {
        push @actual_user_list,$user->[0] if defined $user->[0] && $user->[0] ne "";
    }

    # Close the connection, and check for any database errors.
    Codestriker::DB::DBI->release_connection($dbh, 1);

    return @actual_user_list;
}

# Get a list of users that have metric data for this topic. People can 
# look at the topic even if they were not invited, so if somebody touches the 
# topic, they will appear in this list. Using this function rather than the 
# invite list from the topic will insure that people don't get missed from 
# the metric data.
sub get_complete_list_of_topic_participants {

    my ($self) = @_;

    my $dbh = Codestriker::DB::DBI->get_connection();


    my @metric_user_list = @{ $dbh->selectall_arrayref('
            SELECT distinct LOWER(email) 
            from participant WHERE topicid = ?',{}, $self->{topicid})};

    push @metric_user_list, @{ $dbh->selectall_arrayref('
            SELECT LOWER(author) FROM topic WHERE id = ?',{}, $self->{topicid})};

    push @metric_user_list, @{ $dbh->selectall_arrayref('
            SELECT DISTINCT LOWER(email) FROM topicusermetric 
            WHERE topicid = ?',{}, $self->{topicid})};
    
    push @metric_user_list, @{ $dbh->selectall_arrayref(
            'SELECT DISTINCT LOWER(author) FROM commentdata, commentstate ' .
            'WHERE commentstate.topicid = ? AND
                   commentstate.id = commentdata.commentstateid ',
                   {}, $self->{topicid})};

    push @metric_user_list, @{ $dbh->selectall_arrayref(
            'SELECT DISTINCT LOWER(email) FROM topicviewhistory ' .
            'WHERE topicid = ? AND email IS NOT NULL',{}, $self->{topicid})};

    # remove the duplicates.

    my %metric_user_hash;
    foreach my $user (@metric_user_list) {
        $metric_user_hash{$user->[0]} = 1;
    }

    # Need to sort the empty user name last so that the template parameters 
    # that are done by index don't start at 1, and therefor not allow users
    # to save the metrics.
    @metric_user_list = sort { 
        return 1  if ( $a eq "");
        return -1 if ( $b eq "");
        return $a cmp $b; 
    } keys %metric_user_hash;

    # Close the connection, and check for any database errors.
    Codestriker::DB::DBI->release_connection($dbh, 1);

    return @metric_user_list;
}

# Get a list of users that are invited for this review.
sub get_list_of_topic_participants {

    my ($self) = @_;

    my $dbh = Codestriker::DB::DBI->get_connection();


    my @metric_user_list = @{ $dbh->selectall_arrayref('
            SELECT distinct LOWER(email) 
            from participant WHERE topicid = ?',{}, $self->{topicid})};

    push @metric_user_list, @{ $dbh->selectall_arrayref('
            SELECT LOWER(author) FROM topic WHERE id = ?',{}, $self->{topicid})};

    # remove the duplicates.

    my %metric_user_hash;
    foreach my $user (@metric_user_list) {
        $metric_user_hash{$user->[0]} = 1;
    }

    # Need to sort the empty user name last so that the template parameters 
    # that are done by index don't start at 1, and therefor not allow users
    # to save the metrics.
    @metric_user_list = sort { 
        return 1  if ( $a eq "");
        return -1 if ( $b eq "");
        return $a cmp $b; 
    } keys %metric_user_hash;

    # Close the connection, and check for any database errors.
    Codestriker::DB::DBI->release_connection($dbh, 1);

    return @metric_user_list;
}

# Sets the metrics for a specific user, both authors and reviewers. cc's don't 
# get metrics. The metrics are sent in as an array, that must in the same 
# order as the get_user_metric() call returns them. Metrics that are bad are 
# silently not stored.
sub set_user_metric {
    my ($self, $user, @metric_values) = @_;

    my @metrics = $self->get_user_metrics($user);

    for (my $index = 0; $index < scalar(@metrics); ++$index) {
        next if ($metrics[$index]->{enabled} == 0);
        die "error: not enough metrics" if (scalar(@metric_values) == 0);

        # Disabled values may be in the database (somebody turned off
        # the metrics).  However, they are not paramters so the index
        # between the paramters and the metrics objects will not
        # match.
        my $value = shift @metric_values;

        if ($self->_verify_metric($metrics[$index], $value) eq '') {
            $metrics[$index]->{value} = $value
        }
    }
}

# Verifies that all of the user metrics are well formed and valid inputs. If a 
# problem is found the function will return a non-empty string.
sub verify_user_metrics {
    my ($self, $user, @metric_values) = @_;

    my $msg = '';

    my @metrics = $self->get_user_metrics($user);

    for (my $index = 0; $index < scalar(@metrics); ++$index) {
        next if ($metrics[$index]->{enabled} == 0);

        # Disabled values may be in the database (somebody turned off
        # the metrics).  However, they are not paramters so the index
        # between the paramters and the metrics objects will not
        # match.
        my $value = shift @metric_values;

        $msg .= $self->_verify_metric($metrics[$index], $value);
    }

    return $msg;
}


# Returns the user metrics as a collection of references to hashs. The
# hash that is returned has the same keys as the metrics_schema hash,
# plus a value key. If the user has not entered a value, it will be
# set to an empty string.
sub get_user_metrics {

    my ($self, $username) = @_;

    my @user_metrics;

    if (exists($self->{usermetrics}->{$username})) {
        # If the metrics for this user has already been loaded from
        # the database, return the cached result of that load.
        @user_metrics = @{$self->{usermetrics}->{$username}};
    }
    else {    
        my @stored_metrics = ();

        if (defined($self->{topicid})) {
            # Obtain a database connection.
            my $dbh = Codestriker::DB::DBI->get_connection();


            # Get all of the user outputs for this topic regardless of
            # the user.
            my $selected_all_user_metrics = 
                $dbh->prepare_cached('SELECT DISTINCT metric_name ' .
                                     'FROM topicusermetric ' .
                                     'WHERE topicid = ? ' .
                                     'ORDER BY metric_name');
            $selected_all_user_metrics->execute($self->{topicid}); 
            @stored_metrics =
                @{$selected_all_user_metrics->fetchall_arrayref()};

            # Get the outputs for this user.
            my $select_user_metrics = 
                $dbh->prepare_cached('SELECT metric_name, value ' .
                                     'FROM topicusermetric ' .
                                     'WHERE topicid = ? AND LOWER(email) = LOWER(?) ' .
                                     'ORDER BY metric_name');

            $select_user_metrics->execute($self->{topicid}, $username);

            my @user_stored_metrics =
                @{$select_user_metrics->fetchall_arrayref()};

            # Stuff the complete list with the values from the current
            # user list.  Handle displaying metrics that are in the
            # db, but not enabled for new topics.
            foreach my $metric (@stored_metrics) {
                my $foundit = 0;
                foreach my $user_metric (@user_stored_metrics) {
                    if ($user_metric->[0] eq $metric->[0]) {
                        $foundit = 1;
                        push @$metric, $user_metric->[1];
                        last;
                    }
                }

                if ($foundit == 0) {
                    push @$metric,'';
                }
            }

            # Close the connection, and check for any database errors.
            Codestriker::DB::DBI->release_connection($dbh, 1);
        }

        foreach my $metric_schema (Codestriker::get_metric_schema()) {
            if ($metric_schema->{scope} ne 'topic') {
                my $metric = 
                { 
                    name        => $metric_schema->{name},
                    description => $metric_schema->{description},
                    value       => '',
                    enabled     => $metric_schema->{enabled},
                    scope       => $metric_schema->{scope},
                    filter      => $metric_schema->{filter},
                };


                for (my $index = 0; $index < scalar(@stored_metrics);
                     ++$index) {
                    my $stored_metric = $stored_metrics[$index];

                    if ($stored_metric->[0] eq $metric_schema->{name}) {
                        $metric->{value} = $stored_metric->[1];
                        $metric->{in_database} = 1;
                        splice @stored_metrics, $index,1;
                        last;
                    }
                }

                if ($metric_schema->{enabled} || $metric->{in_database}) {
                    
                    if ($username eq "") {
                        # don't let any metrics be set into the db for unknown users.
                        $metric->{enabled} = 0;
                    }

                    push @user_metrics, $metric;
                }
            }
        }

        # Clean up any metrics that are in the database but not in the
        # schema, we will not let them change them, and we don't have
        # the description anymore.
        for (my $index = 0; $index < scalar(@stored_metrics); ++$index) {
            my $stored_metric = $stored_metrics[$index];

            my $metric =
                { # this is the topic metric  
                name=>$stored_metric->[0],
                description=>'',
                value=>$stored_metric->[1],
                scope=>'participant',
                enabled=>0, # user can not change the metric, no schema.
                in_database=>1
                };

            push @user_metrics, $metric;
        }

        $self->{usermetrics}->{$username} = \@user_metrics;
    }

    push @user_metrics, $self->_get_built_in_user_metrics($username);

    return @user_metrics;
}


# Returns the user metrics as a collection of references to hashs. 
sub get_user_metrics_totals {
    my ($self,@users) = @_;

    my @user_metrics;

    if (exists($self->{usermetrics_totals})) {
        @user_metrics = @$self->{usermetrics_totals};
    }

    my @total_metrics;

    foreach my $user (@users) {
        my @metrics = $self->get_user_metrics($user);

        if (scalar(@total_metrics) == 0) {
            # Copy the metrics in.

            foreach my $metric (@metrics) {
                my %temp = %$metric;
                push @total_metrics, \%temp;
            }
            
        }
        else {
            # Add them up!
            for (my $index = 0; $index < scalar(@total_metrics) ; ++$index) {
                if ($metrics[$index]->{value} ne '') {
                    if ($total_metrics[$index]->{value} eq '') {
                        $total_metrics[$index]->{value} = 0;
                    }

                    $total_metrics[$index]->{value} +=
                        $metrics[$index]->{value};              
                }
            }
        }

    }

    $self->{usermetrics_totals}= \@total_metrics;

    return @total_metrics;
}

# Returns a list of hashes. Each hash is an event. In the hash is stored who 
# caused the event, when it happened, and what happened. The hashes are defined
# as: 
#   email -> the email address of the user who caused the event.
#   date  -> when the event happened.
#   description -> the event description.
#
# The topic must be loaded from the db before this function can be called.
sub get_topic_history {
    my ($self) = @_;

    my @topic_history = $self->_get_topic_history_rows();

    my @event_list;

    my $last_history_row;

    foreach my $current_history_row (@topic_history) {
        if ( !defined($last_history_row) ) {
            # The first event is always the topic creation, so lets make 
            # that now.
            
            my $filteredemail = 
                Codestriker->filter_email($current_history_row->{author});

            my $formatted_time = 
                Codestriker->format_short_timestamp($current_history_row->{modified_ts});
    
            push @event_list, 
            { 
                email=>$filteredemail,
                date =>$formatted_time,
                description=>'The topic is created.' 
            };
        }
        else {
            my %event = 
            ( 
                email=> Codestriker->filter_email(
                        $current_history_row->{modified_by}),
                date => Codestriker->format_short_timestamp(
                        $current_history_row->{modified_ts}),
                description=>'' 
            );

            # Look for changes in all of the fields. Several fields could have 
            # changed at once.

            if ($current_history_row->{author} ne $last_history_row->{author}) {
                my %new_event = %event;
                $new_event{description} = 
                    "Author changed: $last_history_row->{author} to " . 
                    "$current_history_row->{author}.";
                push @event_list, \%new_event;
            }

            if ($current_history_row->{title} ne $last_history_row->{title}) {
                my %new_event = %event;
                $new_event{description} = 
                    "Title changed to: \"$current_history_row->{title}\".";
                push @event_list, \%new_event;
            }

            if ($current_history_row->{description} ne $last_history_row->{description}) {
                my %new_event = %event;
                $new_event{description} = "Description changed to: " . 
                    "$current_history_row->{description}.";
                push @event_list, \%new_event;

            }

            if ($current_history_row->{state} ne $last_history_row->{state}) {
                my %new_event = %event;
                $new_event{description} = 
                    "Topic state changed to: " . 
                    $Codestriker::topic_states[$current_history_row->{state}];
                push @event_list, \%new_event;

            }

            if ($current_history_row->{repository} ne $last_history_row->{repository}) {
                my %new_event = %event;
                $new_event{description} = 
                    "Repository changed to: $current_history_row->{repository}.";
                push @event_list, \%new_event;

            }

            if ($current_history_row->{project} ne $last_history_row->{project}) {
                my %new_event = %event;
                $new_event{description} = 
                    "Project changed to: $current_history_row->{project}.";
                push @event_list, \%new_event;
            }

            if ($current_history_row->{reviewers} ne $last_history_row->{reviewers}) {
                my %new_event = %event;

                # Figure out who was removed, and who was added to the list.
                my @reviewers = split /,/,$current_history_row->{reviewers};
                my @l_reviewers = split /,/,$last_history_row->{reviewers};
                my @new;
                my @removed;

                Codestriker::set_differences(\@reviewers, \@l_reviewers, \@new, \@removed);

                if (@new == 0) {
                    $new_event{description} = 
                        "Reviewers removed: " . join(',',@removed);;
                }
                elsif (@removed == 0) {
                    $new_event{description} = 
                        "Reviewers added: " . join(',',@new);
                }
                else {
                    $new_event{description} = 
                        "Reviewers added: " . join(',',@new) . 
                        " and reviewers removed: " . join(',',@removed);
                }

                push @event_list, \%new_event;
            }

            if ($current_history_row->{cc} ne $last_history_row->{cc}) {
                my %new_event = %event;
                $new_event{description} = 
                    "CC changed to $current_history_row->{cc}.";
                push @event_list, \%new_event;
            }
        }

        $last_history_row = $current_history_row
    }
    
    return @event_list;
}

# Returns the topic metrics as a collection of references to
# hashes. The hash that is returned has the same keys as the
# metrics_schema hash, plus a value key. This private function
# returns "built in" metrics derived from the topic history
# table.
sub _get_built_in_topic_metrics {
    my $self = shift;

    my @topic_metrics;

    my @topic_history = $self->_get_topic_history_rows();

    my %state_times;

    my $last_history_row;

    # Figure out how long the topic has spent in each state.

    for ( my $topic_history_index = 0; 
          $topic_history_index <= scalar(@topic_history);
          ++$topic_history_index) {
        
        my $current_history_row;
        
        if ($topic_history_index < scalar(@topic_history)) {
            $current_history_row = $topic_history[$topic_history_index];
        }

        if (defined($last_history_row)) {
            my $start = 
                Codestriker->convert_date_timestamp_time( 
                    $last_history_row->{modified_ts});
            my $end   = 0;
            
            if (defined($current_history_row)) {
                $end = Codestriker->convert_date_timestamp_time( 
                    $current_history_row->{modified_ts});
            }
            else {
                $end = time();
            }

            if (exists($state_times{$last_history_row->{state}})) {
                $state_times{$last_history_row->{state}} += $end - $start;
            }
            else {
                $state_times{$last_history_row->{state}} = $end - $start;
            }
        }

        $last_history_row = $current_history_row
    }

    foreach my $state ( sort keys %state_times) {
        my $statename = $Codestriker::topic_states[$state];
        my $time_days = sprintf("%1.1f",$state_times{$state} / (60*60*24));

        # This is the topic metric.
        my $metric =
            {
            name         => 'Time In ' . $statename,
            description  => 
                'Time in days the topic spent in the ' . $statename . ' state.',
            value        => $time_days,

            # User can not change the metric, not configured.
            enabled      => 0,
            in_database  => 0,
            filter       =>"count",
            builtin      => 1,
            };  

        push @topic_metrics, $metric;
    }

    return @topic_metrics;
}


# Returns the user topic metrics as a collection of references to
# hashs. The hash that is returned has the same keys as the
# metrics_schema hash, plus a value key. This private function
# returns "built in" metrics derived from the topic history
# table and the topic view history table.
sub _get_built_in_user_metrics {

    my ($self,$username) = @_;

    my @user_metrics;

    my $dbh = Codestriker::DB::DBI->get_connection();

    # Setup the prepared statements.
    my $select_topic = $dbh->prepare_cached('SELECT creation_ts ' .
                                            'FROM topicviewhistory ' .
                                            'WHERE topicid = ? AND ' .
                                            'LOWER(email) = LOWER(?) ' .
                                            'ORDER BY creation_ts');

    $select_topic->execute($self->{topicid}, $username);

#    my $total_time = $self->calculate_topic_view_time($select_topic);
    my $total_time = 0;

    Codestriker::DB::DBI->release_connection($dbh, 1);

    if ($total_time == 0) {
        $total_time = "";
    }
    else {
        $total_time = sprintf("%1.0f",$total_time / (60));
    }

    # This is the topic metric.
    my $metric =
        {
        name         => 'Codestriker Time',
        description  => 
            'Time in minutes spent in Codestriker looking at this topic.',
        value        => $total_time,
        enabled      => 0,
        in_database  => 0,
        filter       =>"minutes",
        builtin      => 1,
        scope        =>'participant',
        };      

    push @user_metrics, $metric;

    return @user_metrics;
}

# Given a DBI statement that returns a sorted collection of timestamps from 
# the topicviewhistory table, return the total time.
sub calculate_topic_view_time {

    my ($self,$select_topic) = @_;

    # The amount of time you give to people after a click assuming no other
    # clicks are after it.
    my $time_increment = 4*60;

    my $total_time = 0;
    my $last_time = 0;    

    while ( my @row_array = $select_topic->fetchrow_array) {
        my ($creation_ts) = @row_array;

        my $time = Codestriker->convert_date_timestamp_time($creation_ts);

        if ($last_time) {

            if ($time - $last_time > $time_increment) {
                $total_time += $time_increment
            }
            else {
                $total_time += $time - $last_time;
            }
        }

        $last_time = $time;
    }

    if ($last_time) {
        $total_time += $time_increment;
    }

    return $total_time;

}

# Returns the topichistory rows as an array of hashes. Each element in the 
# array is a row, each field in the table is a key. It will only fetch if 
# from the db once.
sub _get_topic_history_rows {
    
    my ($self) = @_;

    if (defined( $self->{topichistoryrows}))  {
        return @{$self->{topichistoryrows}};
    }
    else {
        my $dbh = Codestriker::DB::DBI->get_connection();

        my @history_list;

        # Setup the prepared statements.
        my $select_topic = $dbh->prepare_cached('SELECT topichistory.author, ' .
                                                'topichistory.title, ' .
                                                'topichistory.description, ' .
                                                'topichistory.state, ' .
                                                'topichistory.modified_ts, ' .
                                                'topichistory.version, ' .
                                                'topichistory.repository, ' .
                                                'project.name, ' .
                                                'topichistory.reviewers, ' .
                                                'topichistory.cc, ' .
                                                'topichistory.modified_by_user ' .
                                                'FROM topichistory, project ' .
                                                'WHERE topichistory.topicid = ? AND ' .
                                                'topichistory.projectid = project.id ' .
                                                'ORDER BY topichistory.version');

        $select_topic->execute($self->{topicid});

        while ( my @row_array = $select_topic->fetchrow_array) {
            my ($author,$title,$description,$state,$modified_ts, $version,
                $repository,$project,$reviewers,$cc, $modified_by) = @row_array;

            my %entry = ( 
              author=>$author,
              title=>decode_utf8($title),
              description=>decode_utf8($description),
              state=>$state,
              modified_ts=>$modified_ts,
              version=>$version,
              repository=>$repository,
              project=>decode_utf8($project),
              reviewers=>lc($reviewers),
              cc=>lc($cc), 
              modified_by=>lc($modified_by)
              );

            push @history_list, \%entry;
        }

        Codestriker::DB::DBI->release_connection($dbh, 1);

        $self->{topichistoryrows} = \@history_list;

        return @history_list;
    }
}


# Returns an error message if a number is not a valid value for a given metric.
sub _verify_metric {
    my ($self, $metric, $value) = @_;

    my $msg = '';
    if ($metric->{enabled}) {
        my $input_ok = 0;

        if ($metric->{filter} eq "hours") {
            $input_ok = ($value =~ /(^[\d]+([\.:][\d]*)?$)|(^$)/);
            $msg = $metric->{name} .
                   " must be a valid time in hours. " . HTML::Entities::encode($value) . " was " . 
                   "not saved.<BR>" unless $input_ok;
        }
        elsif ($metric->{filter} eq "minutes") {
            $input_ok = ($value =~ /(^[\d]+)|(^$)/);
            $msg = $metric->{name} .
                   " must be a valid time in minutes. " . HTML::Entities::encode($value) . " was " . 
                   "not saved.<BR>" unless $input_ok;
        }
        elsif ($metric->{filter} eq "count") {
            $input_ok = ($value =~ /(^[\d]+$)|(^$)/);
            $msg = $metric->{name} . 
                   " must be a valid count. " . HTML::Entities::encode($value) . " was not " . 
                   "saved.<BR>" unless $input_ok;
        }
        elsif ($metric->{filter} eq "percent") {
            $input_ok = ($value =~ /(^[\d]+(\.[\d]*)?$)|(^$)/);

            if ($input_ok && $value ne '') {
                $input_ok = 0 unless ($value >= 0.0 && $value <= 100.0);
            }
            $msg = $metric->{name} . 
                   " must be a valid percent, between 0 and 100. " . 
                   HTML::Entities::encode($value) . " was not saved.<BR>" unless $input_ok;
        }
        else {
            # invalid config.
            $input_ok = 0;
            $msg = HTML::Entities::encode($metric->{name}) . 
                   " invalid filter type in configuration. Must " . 
                   "be hours, count, or percent.<BR>";
        }
    }

    return $msg;
}

# Stores all of the metrics to the database.
sub store {
    my ($self) = @_;

    $self->_store_topic_metrics();
    $self->_store_user_metrics();
}

# Stores the topic metrics to the database.
sub _store_user_metrics {
    my ($self) = @_;

    foreach my $user (keys %{$self->{usermetrics}}) {
        $self->get_user_metrics($user);
    }

    # Obtain a database connection.
    my $dbh = Codestriker::DB::DBI->get_connection();

    # flush out the user metrics from the topic,
    my $delete_alluser_metric =
        $dbh->prepare_cached('DELETE FROM topicusermetric ' .
                             'WHERE topicid = ?');

    $delete_alluser_metric->execute($self->{topicid});

    my $insert_user_metric =
        $dbh->prepare_cached('INSERT INTO topicusermetric (topicid, 
                                                    email, 
                                                    metric_name, 
                                                    value) ' .
                             'VALUES (?, LOWER(?), ?, ? )');

    foreach my $user (keys %{$self->{usermetrics}}) {
        my @metrics = $self->get_user_metrics($user);

        foreach my $metric (@metrics) {

            next if ($metric->{builtin});

            if ($metric->{value} ne '') {
                $insert_user_metric->execute($self->{topicid}, 
                                             $user, 
                                             $metric->{name}, 
                                             $metric->{value});     
            }
        }
    }

    # Close the connection, and check for any database errors.
    Codestriker::DB::DBI->release_connection($dbh, 1);
}

# Stores the topic metrics to the database.
sub _store_topic_metrics {
    my ($self) = @_;

    # Obtain a database connection.
    my $dbh = Codestriker::DB::DBI->get_connection();

    # Store the topic metrics first.
    my @topic_metrics = $self->get_topic_metrics();

    my $insert_topic_metric =
        $dbh->prepare_cached('INSERT INTO topicmetric (topicid, 
                                                       metric_name, 
                                                       value) ' .
                             'VALUES (?, ?, ? )');
    my $update_topic_metric =
        $dbh->prepare_cached('UPDATE topicmetric SET value = ? ' .
                             'WHERE topicid = ? and metric_name = ?');

    my $delete_topic_metric =
        $dbh->prepare_cached('DELETE FROM topicmetric ' .
                             'WHERE topicid = ? and metric_name = ?');

    foreach my $metric (@topic_metrics) {
        # don't save built in metrics

        next if ($metric->{builtin});

        if ($metric->{in_database}) {

            if ($metric->{value} ne '') {
                $update_topic_metric->execute($metric->{value}, 
                                              $self->{topicid}, 
                                              $metric->{name});
            }
            else {
                # Delete the row.
                $delete_topic_metric->execute($self->{topicid},
                                              $metric->{name});
                $metric->{in_database} = 0;
            }
        }
        else {

            # New metric that is not in the datbase.
            if ($metric->{value} ne '') {
                $insert_topic_metric->execute($self->{topicid}, 
                                              $metric->{name},
                                              $metric->{value});
                $metric->{in_database} = 1;
            }
        }

        $metric->{in_database} = 1;
    }

    # Close the connection, and check for any database errors.
    Codestriker::DB::DBI->release_connection($dbh, 1);
}

1;