Rev 1295 | 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 topicWHERE modified_ts >= ?GROUP BY authorORDER 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, topicWHERE topic.modified_ts >= ? ANDtopic.id = commentstate.topicid ANDLOWER(topic.author) <> LOWER(commentdata.author) ANDcommentdata.commentstateid = commentstate.idGROUP 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(TopicIDAuthorTitleStateCreationProject);# Comment metric counts.my @comment_metric_headers;my $comment_metrics = $dbh->selectall_arrayref('SELECT DISTINCT nameFROM commentstatemetricORDER 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_nameFROM topicmetricORDER 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_nameFROM topicusermetricORDER 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.nameFROM topic, projectWHERE topic.id = ? ANDtopic.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 commentstateWHERE 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, commentdataWHERE 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 commentstateWHERE commentstate.creation_ts > ? ANDcommentstate.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, commentdataWHERE commentstate.id = commentdata.commentstateid ANDcommentstate.creation_ts > ? ANDcommentstate.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 topicWHERE topic.creation_ts > ? ANDtopic.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,topicWHERE topic.creation_ts > ? ANDtopic.creation_ts <= ? ANDtopicmetric.topicid = topic.idGROUP BY topicmetric.metric_nameORDER 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,topicWHERE topic.creation_ts > ? ANDtopic.creation_ts <= ? ANDtopicusermetric.topicid = topic.idGROUP BY topicusermetric.metric_nameORDER 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 monthmy @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;