Subversion Repositories DevTools

Rev

Rev 1295 | Go to most recent revision | Blame | Compare with Previous | 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 metrics reports. This object is not an object, it
# is a normal module.

package Codestriker::Model::MetricStats;

use strict;
use Encode qw(decode_utf8);

use Codestriker::DB::DBI;

my $total_participants_header = 'Total Participants';
my $topic_size_lines_header = 'Topic Size In Lines';

# Returns the list of users that have participated in a Codestriker topics.
#
# 
# Returns a collection of the following hash references.
# {
#   name
#   date_last_authored
#   date_last_participated
#   total_topics
# }

sub get_basic_user_metrics {
    # Gather basic user metrics for the past 16 weeks.
    my $last_n_days = 16*7;

    my $date = Codestriker->get_timestamp(time-($last_n_days*24*60*60));

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

    # Get the list of authors.
    my $author_list = $dbh->selectall_arrayref(
            'SELECT LOWER(author),MAX(modified_ts),COUNT(id)
             FROM topic 
             WHERE modified_ts >= ?
             GROUP BY author
             ORDER BY 2 desc', {}, $date);

    my @users_metrics;

    foreach my $row (@$author_list) {
        my ($name, $last_authored_date, $count) = @$row;

        my $metrics = 
        {
            name=>$name,
            date_last_authored=>
                int((time() - Codestriker->convert_date_timestamp_time($last_authored_date))/(60*60*24)),
            date_last_participated=>'',
            total_codestriker_time => 
                calculate_topic_view_time_for_user($date,$name),
            total_topics=>$count,
        };

        push(@users_metrics, $metrics);
    }
 
    # Get the list of participants from all these topics. You need to 
    # submit at least one comment to be counted.
    my $participant_list = $dbh->selectall_arrayref(
            'SELECT LOWER(commentdata.author), 
                    MAX(topic.modified_ts), 
                    COUNT(DISTINCT topic.id)
             FROM commentdata, commentstate, topic 
             WHERE topic.modified_ts >= ? AND 
                   topic.id = commentstate.topicid AND 
                   LOWER(topic.author) <> LOWER(commentdata.author) AND
                   commentdata.commentstateid = commentstate.id
             GROUP BY LOWER(commentdata.author)
             ORDER BY 2 desc',{}, $date);
     
    foreach my $row (@$participant_list) {
        my ($name, $last_participated_date, $count) = @$row;

        my $found = 0;
        foreach my $user (@users_metrics) {
            if ($user->{name} eq $name) {
                $user->{date_last_participated} = 
                    int((time() - 
                    Codestriker->convert_date_timestamp_time($last_participated_date))/(60*60*24));
                $user->{total_topics} += $count;
                $found = 1;
            }
        }

        if ($found == 0) {
            my $metrics = 
            {
                name=>$name,
                date_last_authored=>'',
                date_last_participated=>
                    int((time() - 
                    Codestriker->convert_date_timestamp_time($last_participated_date))/(60*60*24)),
                total_topics=>$count,
                total_codestriker_time => 
                    calculate_topic_view_time_for_user($date,$name),
            };

            push(@users_metrics, $metrics);
        }
    }

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

    return @users_metrics;
}

# Given the username, and the oldest date, calculate the total amount of 
# codestriker time that was recorded in hours.
sub calculate_topic_view_time_for_user {
    my ($date,$user) = @_;

    # get the total time for this user.
    my $dbh = Codestriker::DB::DBI->get_connection();

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

    $select_topic->execute($date,$user);

    my $total_time = 
        Codestriker::Model::Metrics->calculate_topic_view_time($select_topic);

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

    $total_time = sprintf("%1.1f",$total_time / (60*60));

    return $total_time;
}


# Returns a list of all the topic ids in the database.
sub get_topic_ids {
    # Obtain a database connection.
    my $dbh = Codestriker::DB::DBI->get_connection();

    # Get the list of authors.
    my $topicid_list  = $dbh->selectall_arrayref(
            'SELECT id FROM topic ORDER BY creation_ts');

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

    return @$topicid_list;

}

# Returns in a hash the column names for the raw metric download feature.
sub get_download_headers {
    # Obtain a database connection.
    my $dbh = Codestriker::DB::DBI->get_connection();

    my @base_headers = qw(
        TopicID
        Author
        Title
        State
        Creation
        Project
        );

    # Comment metric counts.
    my @comment_metric_headers;
    my $comment_metrics = $dbh->selectall_arrayref(
            'SELECT DISTINCT name
             FROM commentstatemetric 
             ORDER BY name
            ');
    foreach my $metric (@$comment_metrics) {
        push @comment_metric_headers, $metric->[0];
    }

    # Do the built-in comment metrics.
    push @comment_metric_headers, 'Comment Threads';
    push @comment_metric_headers, 'Submitted Comments';

    # Topic metrics counts.
    my @topic_metric_headers;
    my $topic_metrics = $dbh->selectall_arrayref(
            'SELECT DISTINCT metric_name 
             FROM topicmetric
             ORDER by metric_name
            ');

    foreach my $metric (@$topic_metrics) {
        push @topic_metric_headers, $metric->[0];
    }

    # Do the built in topic metrics.
    for (my $state = 0; $state < scalar(@Codestriker::topic_states);
         ++$state) {
        push @topic_metric_headers, 
            'Time In ' . $Codestriker::topic_states[$state];
    }

    push @topic_metric_headers, $topic_size_lines_header;

    my @topic_user_metric_headers;

    # User topic metrics counts.
    my $user_topic_metrics  = $dbh->selectall_arrayref(
            'SELECT DISTINCT metric_name 
             FROM topicusermetric
             ORDER by metric_name
            ');

    foreach my $metric (@$user_topic_metrics) {
        push @topic_user_metric_headers, $metric->[0];
    }
    
    # Do the built in user metrics.
    push @topic_user_metric_headers, 'Codestriker Time';
    push @topic_user_metric_headers, $total_participants_header;
    
    my $headers = 
    {
        base => \@base_headers,
        comment => \@comment_metric_headers,
        topic => \@topic_metric_headers,
        user => \@topic_user_metric_headers
    };

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

    return $headers;
}

# Given a topic id, and a header hash, return an list with all of the
# topics metric data returned in the order given by the header
# information.
sub get_raw_metric_data {
    my ($topicid, $headers) = @_;

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

    my @basic_topic_info = $dbh->selectrow_array('
            SELECT topic.id, 
                   LOWER(topic.author), 
                   topic.title, 
                   topic.state, 
                   topic.creation_ts, 
                   project.name 
            FROM topic, project
            WHERE topic.id = ? AND 
                  topic.projectid = project.id',{}, $topicid);

    # Decode topic title and project name.
    $basic_topic_info[2] = decode_utf8($basic_topic_info[2]);
    $basic_topic_info[5] = decode_utf8($basic_topic_info[5]);

    if ($basic_topic_info[3] < @Codestriker::topic_states) {
        $basic_topic_info[3] = 
            @Codestriker::topic_states[$basic_topic_info[3]];
    }

    $basic_topic_info[4] =
        Codestriker->format_date_timestamp($basic_topic_info[4]);

    $basic_topic_info[1] =
        Codestriker->filter_email($basic_topic_info[1]);

    my @row;

    push @row, @basic_topic_info;

    # Process the comment metric values.
    my $count_query =
        $dbh->prepare_cached('SELECT COUNT(*) ' .
                             'FROM topic, commentstate, commentstatemetric ' .
                             'WHERE topic.id = ? AND ' .
                             'topic.id = commentstate.topicid AND ' .
                             'commentstate.id = commentstatemetric.id AND '.
                             'commentstatemetric.name = ? ');
    foreach my $metric (@{$headers->{comment}}) {
        # Get the count for this metric name.
        $count_query->execute($topicid, $metric);
        my ($count) = $count_query->fetchrow_array();
        $count_query->finish();
        push @row, $count;
    }

    # Now process the 'Comment Threads' metric.
    my $comment_threads = $dbh->selectall_arrayref(
            'SELECT COUNT(id)
             FROM commentstate 
             WHERE topicid = ?
            ',{}, $topicid);
    push @row, $comment_threads->[0]->[0];

    # Now process the 'Submitted Comments' metric.
    my $submitted_comments = $dbh->selectall_arrayref(
            'SELECT COUNT(id)
             FROM commentstate, commentdata
             WHERE commentstate.topicid = ?
             AND commentstate.id = commentdata.commentstateid
            ',{}, $topicid);
    push @row, $submitted_comments->[0]->[0];
    
    my $topic = Codestriker::Model::Topic->new($basic_topic_info[0]);     

    my $metrics = $topic->get_metrics();

    my @topic_metrics = $metrics->get_topic_metrics();
    
    for (my $index = 0; $index < scalar(@{$headers->{topic}}); ++$index) {
        my $count = "";

        foreach my $metric ( @topic_metrics ) {
            $count = $metric->{value} 
                if ($metric->{name} eq $headers->{topic}->[$index]);
        }

        if ($headers->{topic}->[$index] eq $topic_size_lines_header ) {
            $count = $topic->get_topic_size_in_lines();
        }

        push @row, $count;
    } 

    # Get the list of users for this review.
    my @users = $metrics->get_complete_list_of_topic_participants();
    
    my @user_metrics = $metrics->get_user_metrics_totals(@users);
    
    for (my $index = 0; $index < scalar(@{$headers->{user}}); ++$index) {
        my $count = "";

        foreach my $metric ( @user_metrics ) {
            $count = $metric->{value} 
                if ($metric->{name} eq $headers->{user}->[$index]);
        }

        if ($headers->{user}->[$index] eq $total_participants_header) {
            # Add the total number of participants in the topic.
            $count = scalar(@users);
        }

        push @row, $count;
    } 


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

    return @row;
}

# Returns 12 months of comment metrics data.
#
# returns a collection of the following hash references
# {
#   name  = the comment metric name
#   results = collection ref to 
#             {
#               name = the comment value name. 
#               counts = array ref to metric counts per month
#               monthnames = array ref to month names
#             }
# }
sub get_comment_metrics {
    # Stores the collection results.
    my @results = ();

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

    # Get the comment metric totals.
    foreach my $metric_config (@{ $Codestriker::comment_state_metrics }) {
        my $metric_name = $metric_config->{name};
        my $query =
            "SELECT commentstatemetric.value, COUNT(commentstate.id) " .
            "FROM commentstate, commentstatemetric " .
            "WHERE commentstatemetric.id = commentstate.id AND " .
            "commentstatemetric.name = " . $dbh->quote($metric_name) . " AND " .
            "commentstate.creation_ts > ? AND " .
            "commentstate.creation_ts <= ? " .
            "GROUP BY commentstatemetric.value " .
            "ORDER BY commentstatemetric.value";

        my @metrics = _get_monthly_metrics(12, $query);
        my $months_ref = $metrics[0]->{monthnames};

        # Make sure all enumerated values are catered for.
        my %handled_value = ();
        foreach my $value (@metrics) {
            $handled_value{$value->{name}} = 1;
        }
        foreach my $value (@{ $metric_config->{values} }) {
            if (! defined $handled_value{$value}) {
                push @metrics, { name => $value,
                                 counts => [0,0,0,0,0,0,0,0,0,0,0,0],
                                 monthnames => $months_ref };
            }
        }

        my $result = { name => $metric_name, results => \@metrics };
        push @results, $result;
    }

    # Get comment thread totals.
    my @total_metrics = ();
    my @thread_total = _get_monthly_metrics(12,
        'SELECT \'Comment Threads\', COUNT(commentstate.id) 
        FROM commentstate
        WHERE commentstate.creation_ts >  ? AND
              commentstate.creation_ts <= ?');
    push @total_metrics, @thread_total;

    # Get submitted comment totals.
    my @submitted_total = _get_monthly_metrics(12,
        'SELECT \'Submitted Comments\', COUNT(commentstate.id) 
        FROM commentstate, commentdata
        WHERE commentstate.id = commentdata.commentstateid AND
              commentstate.creation_ts >  ? AND
              commentstate.creation_ts <= ?');
    push @total_metrics, @submitted_total;

    my $result = { name => 'Total', results => \@total_metrics };
    push @results, $result;

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

    return @results;
}

# Returns 12 months of data with a break down of topic metrics.
#
# Returns a collection of the following hash references:
# {
#   name  = the metric name
#   counts = array ref to metric counts per month
#   monthnames = array ref to month names
# }
sub get_topic_metrics {
    my @metrics;

    # Get total.
    my @total = _get_monthly_metrics(12,
        'SELECT \'Total Topics\', COUNT(topic.id) 
        FROM topic
        WHERE topic.creation_ts >  ? AND
              topic.creation_ts <= ?');

    push @metrics, @total;

    # Get totals for the topic metrics.
    @total = _get_monthly_metrics(12,
        'SELECT topicmetric.metric_name, SUM(topicmetric.value) 
        FROM topicmetric,topic
        WHERE topic.creation_ts >  ? AND
              topic.creation_ts <= ? AND 
              topicmetric.topicid = topic.id
              GROUP BY topicmetric.metric_name
              ORDER BY topicmetric.metric_name');

    push @metrics, @total;

    # Get totals for the topic user metrics.
    @total = _get_monthly_metrics(12,
        'SELECT topicusermetric.metric_name, SUM(topicusermetric.value) 
        FROM topicusermetric,topic
        WHERE topic.creation_ts >  ? AND
              topic.creation_ts <= ? AND 
              topicusermetric.topicid = topic.id
              GROUP BY topicusermetric.metric_name
              ORDER BY topicusermetric.metric_name');

    push @metrics, @total;


    return @metrics;
}


# Returns $total_months of data for the given query. The query must return
# name, count collection of rows between two times.
#
# returns a collection of the following hash references
# {
#   name  = the metric name
#   counts = array ref to metric counts per month
#   monthnames = array ref to month names
# }
sub _get_monthly_metrics {
    my ($total_months, $dbi_query_string) = @_;

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

    my @month_dbi_times = _dbi_month_time_stamp($total_months);
    my @month_names = _user_month_name_list($total_months);

    my @metrics;

    # For the past year, get the metrics counts for each month. 
    for (my $month_count = 0;
         $month_count+1 < @month_dbi_times;
         ++$month_count) {

        # Do the db query.
        my $comment_counts = $dbh->selectall_arrayref(
            $dbi_query_string,
            {}, 
            $month_dbi_times[$month_count],
            $month_dbi_times[$month_count+1]);

        foreach my $row (@$comment_counts) {
            my ($db_metric_name, $db_count) = @$row;

            my $found = 0;

            # See if we can find the metric.
            foreach my $metric (@metrics) {
                if ($metric->{name} eq $db_metric_name) {
                    push @{$metric->{counts}}, $db_count;
                    $found = 1;
                    last;
                }
            }

            if ($found == 0) {
                my $metric = 
                    {
                    name=>$db_metric_name,
                    counts=>[],
                    monthnames=>\@month_names
                    };

                # Catch up the collection of counts on any missed months.
                for( my $missingmonths = 0; 
                     $missingmonths < $month_count; 
                     ++$missingmonths) {
                    push @{$metric->{counts}}, 0;
                }

                push @{$metric->{counts}}, $db_count;

                push @metrics, $metric;     
            }
        }

        # Add zero's to any metrics not present.
        foreach my $metric (@metrics) {
            if (@{$metric->{counts}} eq $month_count) {
                push @{$metric->{counts}}, 0;
            }
        }
    }

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

    return @metrics;
}

# Return a list of dbi time stamp on 1 month boundaries back for n months. This
# is used to do db queries to get data cut up by month.
sub _dbi_month_time_stamp {
    my ($number_months_back) = @_;

    # Get the start time of this month

    my @month = _add_month(-$number_months_back+1,localtime(time()));

    my @month_dbi_ts;

    for (my $count = 0; $count < $number_months_back +1; ++$count) {
        # Calculate the start of this month dbi string.
        my $month_start = sprintf("%04d-%02d-01 00:00:00", 
            $month[5]+1900,
            $month[4]+1);

        push @month_dbi_ts, $month_start;

        @month = _add_month(1, @month);
    }
    
    return @month_dbi_ts; 
}

# Return a list of user displayable time stamps on 1 month bondaries
# back for n months.
sub _user_month_name_list {
    my ($number_months_back) = @_;

    # Get the start time of this month.
    my @month = _add_month(-$number_months_back+1,localtime(time()));

    my @month_names;

    for (my $count = 0; $count < $number_months_back; ++$count) {
        my $month_name = $Codestriker::short_months[$month[4]] .
            " " . ($month[5]+1900);
         
        push @month_names, $month_name;

        @month = _add_month(1, @month);
    }
    
    return @month_names; 
}


# Add or substracts count months on to a time array.
sub _add_month {
    my ($count,@time) = @_;

    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = @time;
    
    if ($count > 0) {
        for (my $i = 0; $i < $count; ++$i) {
            # Calculate the end of this month dbi string.
            ++$mon;

            if ($mon >= 12) {
                $mon = 0;
                ++$year;
            }
        }
    }
    elsif ($count < 0) {
        for (my $i = $count; $i < 0; ++$i) {
            # Calculate the end of this month dbi string.
            --$mon;

            if ($mon < 0) {
                $mon = 11;
                --$year;
            }
        }
    }

    $time[4] = $mon;
    $time[5] = $year;

    return @time;
}

1;