##!/usr/bonsaitools/bin/perl -w # -*- Mode: perl; indent-tabs-mode: nil -*- # # The contents of this file are subject to the Mozilla Public # License Version 1.1 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of # the License at http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or # implied. See the License for the specific language governing # rights and limitations under the License. # # The Original Code is the Bugzilla Bug Tracking System. # # The Initial Developer of the Original Code is Netscape Communications # Corporation. Portions created by Netscape are # Copyright (C) 1998 Netscape Communications Corporation. All # Rights Reserved. # # Contributor(s): Rick Dean # use diagnostics; use strict; require "CGI.pl"; use Date::Parse; # # Sadly this report is going to be ugly (computationally expensive) to # compute, but the data is important and MIPS are cheap. # # We will run through every bug in order, simultaneously pulling from # the bugs and bugs_activity tables. From bugs we will only pull the # fields we are screening on. We will do the selection in # perl not by the SQL database. i.e. maybe no "where" clauses. Yuck! # # ConnectToDatabase(); $::FORM{'debug'} = 0; # Handy for debugging. # Load the fielddefs table because we will use it a lot # It is easier to load the whole darn thing even though we don't use it all. # The "sql" here is the name column. The table default is bugs. my(%field_id_to_fname,%field_fname_to_id,%field_fname_to_desc); SendSQL("SELECT fieldid, name, description FROM fielddefs"); while(1) { my($id,$fname,$descrip) = FetchSQLData(); last if ! defined $id; $field_id_to_fname{$id} = $fname; $field_fname_to_id{$fname} = $id; $field_fname_to_desc{$fname} = $descrip; } my($statfid,$resfid) = ($field_fname_to_id{'bug_status'},$field_fname_to_id{'resolution'}); # cause we use 'em a lot # Now build some data structure about our legal vertical axes. # The "nick" here should match the @axis_types of bug_count_trends.cgi that show in the form. # "sql" Is the query we make so it looks pretty. # "fname" matches the name column of the fielddefs table (which are the SQL field names of the bugs table). my(%vert_nick_to_sql,%vert_nick_to_rownames); # key is "nick" sub DefVert { my($nick,$sql,$fname,$rownames) = @_; if(! defined $field_fname_to_id{$fname} && $fname ne 'who' && $fname ne 'nothing') { print STDERR "### fielddefs missing fname=$fname nick=$nick\n"; } $vert_nick_to_sql{$nick} = $sql; $vert_nick_to_rownames{$nick} = $rownames; } # nick sql fname rowname-order DefVert("severity" ,"bug_severity" ,"bug_severity",\@::severities); DefVert("priority" ,"priority" ,"priority" ,\@::priorities); DefVert("who" ,"map_reporter.login_name" ,"who" ,undef); # This one cheats DefVert("nothing" ,"' '" ,"nothing" ,undef); # This one cheats DefVert("assigned_to" ,"map_assigned_to.login_name","assigned_to" ,undef); DefVert("reporter" ,"map_reporter.login_name" ,"reporter" ,undef); DefVert("status" ,"bug_status" ,"bug_status" ,['UNCONFIRMED','NEW','ASSIGNED','REOPENED','RESOLVED','VERIFIED','CLOSED']); DefVert("resolution" ,"resolution" ,"resolution" ,undef); DefVert("component" ,"component" ,"component" ,undef); DefVert("product" ,"product" ,"product" ,undef); DefVert("version" ,"version" ,"version" ,undef); DefVert("platform" ,"rep_platform" ,"rep_platform",undef); DefVert("os" ,"op_sys" ,"op_sys" ,undef); DefVert("qa_contact" ,"map_qa_contact.login_name","qa_contact" ,undef); # if Param("useqacontact"); DefVert("target_milestone","target_milestone" ,"target_milestone",undef); # if Param("usetargetmilestone"); DefVert("short_desc" ,"short_desc" ,"short_desc" ,undef); DefVert("bug_file_loc" ,"bug_file_loc" ,"bug_file_loc",undef); # This defines some Horizontal axis (time) times. my(%time_nick_to_secs,%time_nick_to_format); sub DefTime { my($nick,$secs,$format) = @_; $time_nick_to_secs{$nick} = $secs; $time_nick_to_format{$nick} = $format; } DefTime("months",3600*24*30,"%b %e"); DefTime("2weeks",3600*24*14,"%b %e"); DefTime("weekly" ,3600*24*7,"%b %e"); DefTime("3days" ,3600*24*3,"%b %e"); DefTime("daily" ,3600*24,"%b %e"); DefTime("12hours",3600*12,"%l%P %b %e"); DefTime("4hours",3600,"%l%P %b %e"); my $vertnick = $::FORM{'vertical'} || "priority"; # use default if not specified $vertnick = "priority" if ! defined $vert_nick_to_sql{$vertnick}; # must be a legal value my $horiznick = $::FORM{'horizontal'} || "weekly"; # use default if not specified $horiznick = "weekly" if ! defined $time_nick_to_secs{$horiznick}; # must be a legal value my $interval_secs = $time_nick_to_secs{$horiznick}; my $num_intervals = $::FORM{'num_intervals'} || 10; # use defaul if not specified $num_intervals = 2 if $num_intervals < 2; # disallow too small $num_intervals = 100 if $num_intervals > 100; # disallow too large # These are the fields we will fetch from the bugs table and track from bugs_activity. # We index it several different ways. Yeah, kind of ugly. my(@attribsql,%attrib_sql_to_index,%attrib_fieldid_to_index,@attriballowed,@substringy); sub AddAttrib { my($sql,$substry,@allowed) = @_; print("Noticing attrib sql=$sql ___ <<". join(">> <<",@allowed) .">>
\n") if $::FORM{'debug'}; if(! defined $attrib_sql_to_index{$sql}) { # if not already search push(@attribsql,$sql); $attrib_sql_to_index{$sql} = $#attribsql; # index to find stuff in @attribsql and friends. if($field_fname_to_id{$sql}) { $attrib_fieldid_to_index{$field_fname_to_id{$sql}} = $#attribsql; # index to find stuff in @attribsql and friends. }; }; my $index = $attrib_sql_to_index{$sql}; if($substry) { $substringy[$index] = 1; foreach (@allowed) { $_ =~ s/(\W)/\\$1/g; # escape non word characters for regexp pattern $attriballowed[$index]{$_} = 1 }; } else { # else not substring foreach (@allowed) { $attriballowed[$index]{$_} = 1 }; }; } AddAttrib($vert_nick_to_sql{$vertnick},0); # must be first AddAttrib("bug_status",0); AddAttrib("resolution",0); # For every enum attribute that can be filtered on foreach my $fname ("bug_status","resolution") { next unless $::FORM{$fname}; AddAttrib($fname,0,split(/\|/,$::default{$fname})); # add attrib with constraint } my @wheres; foreach my $fname ("priority","bug_severity","rep_platform","op_sys", "product","version","component","target_milestone") { next unless $::FORM{$fname}; my @values = split(/\|/,$::default{$fname}); if($::FORM{'retroactive'}) { push(@wheres,"(" . join(" OR ",map {"$fname = ". SqlQuote($_)} @values) .")"); } else { AddAttrib($fname,0,@values); # add attrib with constraint }; } foreach my $fname ("reporter","assigned_to","qa_contact") { next unless $::FORM{$fname}; $::FORM{$fname} =~ tr/,/ /; # map commas to spaces (email addresses don't have commas) AddAttrib($vert_nick_to_sql{$fname},1,split(/\s+/,$::FORM{$fname})); # add attrib with constraint } foreach my $fname ("short_desc","bug_file_loc") { next unless $::FORM{$fname}; AddAttrib($vert_nick_to_sql{$fname},1,split(/\s+/,$::FORM{$fname})); # add attrib with constraint } # show form stuff (if debug mode) print "", map { "\n" if $::FORM{$_} } keys %::FORM if $::FORM{'debug'}; # We will do all our time calcualtions as unix-timestamps (i.e. integer seconds since epoch of Jan 1, 1970) SendSQL("SELECT UNIX_TIMESTAMP()"); my $now = FetchOneColumn(); if($::FORM{'csv'}) { # if we are producing CSV (comma separated variables) print(time2str("%C",$now) ."\n"); } else { # else we are spitting HTML print("
". time2str("%C",$now) ."

\n"); } # Query the database my $query = "SELECT bugs.bug_id, UNIX_TIMESTAMP(creation_ts), ". join(",\n ",@attribsql) ." FROM bugs\n"; if(defined $attrib_sql_to_index{$vert_nick_to_sql{"qa_contact"}}) { $query .= "LEFT JOIN profiles map_qa_contact ON bugs.qa_contact = map_qa_contact.userid\n"; }; if(defined $attrib_sql_to_index{$vert_nick_to_sql{"reporter"}} || defined $attrib_sql_to_index{'who'}) { $query .= "LEFT JOIN profiles map_reporter ON bugs.reporter = map_reporter.userid\n"; } if(defined $attrib_sql_to_index{$vert_nick_to_sql{"assigned_to"}}) { $query .= "LEFT JOIN profiles map_assigned_to ON bugs.assigned_to = map_assigned_to.userid\n"; } if($::FORM{"cc"}) { $query .= "LEFT JOIN cc ON bugs.bug_id = cc.bug_id LEFT JOIN profiles map_cc2 ON cc.who = map_cc2.userid\n"; push(@wheres,"INSTR(LOWER(map_cc2.login_name),". SqlQuote(lc($::FORM{'cc'})) .")"); } if($::FORM{'comment_add'}) { $query .= "LEFT JOIN longdescs ld ON bugs.bug_id = ld.bug_id LEFT JOIN profiles map_ld2 ON ld.who = map_ld2.userid\n"; push(@wheres,"INSTR(LOWER(map_ld2.login_name),". SqlQuote(lc($::FORM{'comment_add'})) .")"); } #push(@wheres,"bugs.bug_id = 958\n"); $query .= "WHERE ". join(" AND\n ",@wheres) ."\n" if $#wheres >= 0; $query .= "ORDER BY bug_id DESC"; print "
$query
\n" if $::FORM{'debug'}; SendSQL($query); # Second (concurrent) query is not so pretty. # We need to pull from these concurrently so Push-pop of bugzilla 2.14 would not work. my $actquerystr = "SELECT bug_id, UNIX_TIMESTAMP(bug_when), fieldid, oldvalue, login_name\n". "FROM bugs_activity LEFT JOIN profiles ON who = userid\n"; $actquerystr .= "WHERE bug_when >= FROM_UNIXTIME(". ($now - $num_intervals * $interval_secs) .")\n"; $actquerystr .= "AND (fieldid = $field_fname_to_id{'bug_status'} OR fieldid = $field_fname_to_id{'resolution'})\n" if $::FORM{'retroactive'}; #$actquerystr .= "AND bug_id = 958\n"; $actquerystr .= "ORDER BY bug_id DESC, bug_when DESC"; my $actquery = $::db->submit($actquerystr) || die "$actquerystr: " . $::db->errstr; $actquery->execute || die "$actquerystr: " . $::db->errstr; my($act_bug_id,$act_when,$act_field_id,$act_old_value,$act_who) = $actquery->fetchrow(); # "row" and "col" refer to the final output table not the SQL database. # The horizontal axis will be time. Time flows to the right. my @bcount; # THIS IS WHERE THE MAGIC HAPPENS # It is a list (by time) of hash refs (by vert metric) containing bug counts. my @blist; # THIS IS WHERE THE MAGIC HAPPENS # It is a list (by time) of hash refs (by vert metric) containing a string of bug_id's. my %coltotal; # We use this to loop over the column names my %rowtotal; # We use this to loop over the row names # Here we know for sure which row name we want it counted as. # We are still going to check the time range here. sub count_bug($$$) { my($timestamp,$vertval,$bug_id) = @_; return if $timestamp > $now; # What the fuck? bug in future. my $xoffset = $num_intervals - int( ($now - $timestamp) / $interval_secs ) - 1; #print("count_bug($timestamp,$vertval) now=$now xoffset=$xoffset
\n"); return if $xoffset < 0; # if it is too old, return # book it! $bcount[$xoffset]{$vertval} += 1; $blist[$xoffset]{$vertval} .= ",$bug_id" if $bcount[$xoffset]{$vertval} <= 100; # only the first 100 $rowtotal{$vertval} += 1; $coltotal{$xoffset} += 1; } sub count_bug_since($$$$) { my($timestamp,$vertval,$bug_id,$since) = @_; my $censusTime; if($timestamp < $now - ($num_intervals * $interval_secs)) { # if timestamp is older than our table (e.g. creation_ts) $censusTime = $now - ($num_intervals * $interval_secs); # use the table starting time } else { # else the timestamp is during our table $censusTime = $timestamp + (($now - $timestamp) % $interval_secs); # round forward to next census interval }; for(;$censusTime <= $since;$censusTime += $interval_secs) { # for every census where... timestamp < censusTime <= since count_bug($censusTime,$vertval,$bug_id); } } # Count the metric so we know how descriptive to be on the vertical axis labels my $num_metrics = grep { /^metric_/ } keys %::FORM; if($num_metrics == 0) { # if no metric print("
(No metric was selected.)
\n"); return 1; } # We have done no screening at this point. sub log_change_event { my($bug_id,$timestamp,$bug_status_changed,$bug_resolution_changed,$since,@attribs) = @_; #print("log_change_event() bug_id=$bug_id timestamp=$timestamp (".time2str("%b %e",$timestamp).") stat=". # ($bug_status_changed||"") ." res=".($bug_resolution_changed||"") ." || ". join(" - ",@attribs) ."
\n"); # Screen the bug... foreach my $index (0..$#attribsql) { next if ! defined $attriballowed[$index]; # if not screening on this one if($substringy[$index]) { my $good; foreach (keys %{$attriballowed[$index]}) { $good = 1 if $attribs[$index] =~ /$_/; # attrib value must match at least one pattern } return if ! $good; } else { # else requires exact match return if ! defined $attriballowed[$index]{$attribs[$index]}; # attrib value must be in allowed hash } } # Log the bug for the chosen metrics... my $vertvalue = $attribs[0]; $vertvalue = "(empty)" if ! defined $vertvalue || $vertvalue eq ""; my $status = $attribs[$attrib_sql_to_index{'bug_status'}]; my $resolution = $attribs[$attrib_sql_to_index{'resolution'}]; if($bug_status_changed) { if($::FORM{'metric_openings'} && ($status eq "NEW" || $status eq "REOPENED")) { count_bug($timestamp,$num_metrics <= 1 ? $vertvalue : "$vertvalue - openings",$bug_id); } if($::FORM{'metric_resolves'} && $status eq "RESOLVED") { count_bug($timestamp,$num_metrics <= 1 ? $vertvalue : "$vertvalue - resolves",$bug_id); } if($::FORM{'metric_verifies'} && $status eq "VERIFIED") { count_bug($timestamp,$num_metrics <= 1 ? $vertvalue : "$vertvalue - verifies",$bug_id); } if($::FORM{'metric_closings'} && $status eq "CLOSED") { count_bug($timestamp,$num_metrics <= 1 ? $vertvalue : "$vertvalue - closings",$bug_id); } } if($bug_status_changed || $bug_resolution_changed) { if($::FORM{'metric_fixes'} && $status eq "RESOLVED" && $resolution eq "FIXED") { count_bug($timestamp,$num_metrics <= 1 ? $vertvalue : "$vertvalue - fixes",$bug_id); } if($::FORM{'metric_nonfixes'} && $status eq "RESOLVED" && $resolution ne "FIXED") { count_bug($timestamp,$num_metrics <= 1 ? $vertvalue : "$vertvalue - non-fixes",$bug_id); } }; if($::FORM{'metric_census'}) { count_bug_since($timestamp,$num_metrics <= 1 ? $vertvalue : "$vertvalue - census",$bug_id,$since); } if($::FORM{'metric_open'} && ($status eq "NEW" || $status eq "ASSIGNED" || $status eq "REOPENED")) { count_bug_since($timestamp,$num_metrics <= 1 ? $vertvalue : "$vertvalue - open",$bug_id,$since); } if($::FORM{'metric_resolved'} && $status eq "RESOLVED") { count_bug_since($timestamp,$num_metrics <= 1 ? $vertvalue : "$vertvalue - resolved",$bug_id,$since); } if($::FORM{'metric_fixed'} && $status eq "RESOLVED" && $resolution eq "FIXED") { count_bug_since($timestamp,$num_metrics <= 1 ? $vertvalue : "$vertvalue - fixed",$bug_id,$since); } if($::FORM{'metric_verified'} && $status eq "VERIFIED") { count_bug_since($timestamp,$num_metrics <= 1 ? $vertvalue : "$vertvalue - verified",$bug_id,$since); } if($::FORM{'metric_closed'} && $status eq "CLOSED") { count_bug_since($timestamp,$num_metrics <= 1 ? $vertvalue : "$vertvalue - closed",$bug_id,$since); } } # Now we shall compute the totals. my $prev_bug_id = -1; while(1) { # For every bug my($bug_id,$creation_ts,@attribs) = FetchSQLData(); # fetch from bugs table last if ! defined $bug_id; next if $prev_bug_id == $bug_id; # if bug repeat (e.g. because of longdescs join and repeated comment) $prev_bug_id = $bug_id; #print("bug start bug_id=$bug_id creation_ts=$creation_ts act_bug_id=$act_bug_id act_when=$act_when ___ ". join(" - ",@attribs) ."
\n"); next if $creation_ts > $now; # What the fuck? bug in future. Skip it. my $since = $now; # keep track of last change time for bug count, so we know which columns to score it on # for each change time which belongs to the current bug (may be several bugs_activity rows with same time stamp) for(;defined $act_bug_id && $act_bug_id >= $bug_id;) { my($bug_status_changed,$bug_resolution_changed,$when); # start with our flags false #print("___ event start act_bug_id=$act_bug_id act_when=$act_when since=$since
\n"); my @attribs2 = @attribs; # newer? older? Before processing event, thus for later in time. $attribs2[0] = $act_who if $vertnick eq "who"; # for each bugs_activity row for this change time for(;defined $act_bug_id && $act_bug_id >= $bug_id && (! defined $when || $when < $act_when + 3); ($act_bug_id,$act_when,$act_field_id,$act_old_value,$act_who) = $actquery->fetchrow()) { #print("______ attirb change act_bug_id=$act_bug_id act_when=$act_when $field_id_to_fname{$act_field_id} -> $act_old_value
\n"); next if $act_bug_id > $bug_id; # if bugs_activity has data on bug missing from bugs table next if $act_when > $now; # What the fuck? activity in the future? Skip it. $when = $act_when; $bug_status_changed = 1 if $act_field_id == $statfid; # $field_sql_to_id{"bug_status"}; $bug_resolution_changed = 1 if $act_field_id == $resfid; # $field_sql_to_id{"resolution"}; my $index = $attrib_fieldid_to_index{$act_field_id}; $attribs[$index] = $act_old_value if defined $index && (!$::FORM{'retroactive'} || $statfid == $act_field_id || $resfid == $act_field_id); } log_change_event($bug_id,$when,$bug_status_changed,$bug_resolution_changed,$since,@attribs2); # log changes at this time $since = $when-1; } log_change_event($bug_id,$creation_ts,1,1,$since,@attribs); # log bug creation } my @rownames; if(defined $vert_nick_to_rownames{$vertnick} && $num_metrics <= 1) { # if the order of the rownames is forced @rownames = @{$vert_nick_to_rownames{$vertnick}}; } else { # else use the rows we found in ascii-sort order @rownames = sort keys %rowtotal; } if($::FORM{"csv"}) { # if comma separated variable format # description my $descript = $::buffer; $descript =~ s/[^&]*=&//g; # remove parameters set to nothing $descript =~ s/[^&]*=substring&//g; # remove anything "=subsstring" $descript =~ s/csv=[^&]&//g; # remove csv= (first param) $descript =~ s/\&/\n/g; # convert "&" to newlines # top row print("$vertnick,"); foreach my $xtime (0..$num_intervals-1) { print(scalar(time2str($time_nick_to_format{$horiznick},($now - ($num_intervals-$xtime-1)*$interval_secs))) .","); }; print("totals\n"); # middle rows foreach my $rowname (@rownames) { # row title on left side print("\"$rowname\","); # center of table foreach my $xtime (0..$num_intervals-1) { my $val = $bcount[$xtime]{$rowname} || "0"; print("$val,"); }; # row total on right side my $val = $rowtotal{$rowname} || "0"; print("$val\n"); } # bottom row my $tottot = 0; print("totals,"); foreach my $xtime (0..$num_intervals-1) { my $val = $coltotal{$xtime} || "0"; $tottot += $val; print("$val,"); }; print("$tottot\n\n" . scalar(time2str("%C",time())) . "\n\n$descript"); return 1; }; # define color matrix for HTML my @colors = ("c3d3ed","dddddd", "dfefff","ffffff"); my $totcolor = "bgcolor=ffcccc"; # Now we shall print out the HTML results print("
$_$::FORM{$_}". $::default{$_} ."
\n"); # top row print("\n"); foreach my $xtime (0..$num_intervals-1) { my $color = $colors[2 + ($xtime & 1)]; print(" \n"); }; print(" \n"); # middle rows my $row=0; foreach my $rowname (@rownames) { print(" \n"); # row title on left side my $color = $colors[2 * ($row & 1) + 1]; print(" \n"); # center of table foreach my $xtime (0..$num_intervals-1) { my $color = $colors[2 * ($row & 1) + ($xtime & 1)]; if($bcount[$xtime]{$rowname}) { my $val = $bcount[$xtime]{$rowname}; my $url = "buglist.cgi?bug_id=". $blist[$xtime]{$rowname}; # don't bother trimming leading comma print(" \n"); } else { print(" \n"); }; }; # row total on right side my $val = $rowtotal{$rowname} || "0"; print(" \n"); print(" \n"); $row++; } # bottom row my $tottot = 0; print("\n"); foreach my $xtime (0..$num_intervals-1) { my $val = $coltotal{$xtime} || "0"; $tottot += $val; print(" \n"); }; print("\n"); print("
$vertnick". scalar(time2str($time_nick_to_format{$horiznick},($now - ($num_intervals-$xtime-1)*$interval_secs))) ."total
$rowname$val.$val
totals$val$tottot
\n"); # give some friendly warnings if($vertnick eq "resolution" and $::buffer =~ /status=[^&]&?/ && ! ($::buffer =~ /status=RESOLVED/ or $::buffer =~ /status=VERIFIED/ or $::buffer =~ /status=CLOSED/)) { print("
\"Resolution\" is pretty borring when you specify " . "bugs not of RESOLVED, VERIFIED, or CLOSED, huh?"); }; if($::buffer =~ /resolution=[^&]+&?/ and $::buffer =~ /status=[^&]&?/ and ! ($::buffer =~ /status=RESOLVED/ or $::buffer =~ /status=VERIFIED/ or $::buffer =~ /status=CLOSED/)) { print("
Did you really mean to specify a " . "resolution but not bugs of RESOLVED, VERIFIED, or CLOSED?"); }; if(($::FORM{'metric_resolved'} || $::FORM{'metric_fixed'} || $::FORM{'metric_resolves'} || $::FORM{'metric_fixes'} || $::FORM{'metric_nonfixes'}) && $::FORM{'bug_status'} && $::FORM{'bug_status'} !~ /RESOLVED/) { print("
Did you really mean to count resolves/resolved, but exclude the RESOLVED state?\n"); } if(($::FORM{'metric_openings'} || $::FORM{'metric_open'}) && $::FORM{'bug_status'} && $::FORM{'bug_status'} !~ /NEW/) { print("
Did you really mean to count open/openings, but exclude the NEW state?\n"); } if(($::FORM{'metric_openings'} || $::FORM{'metric_open'}) && $::FORM{'bug_status'} && $::FORM{'bug_status'} !~ /REOPENED/) { print("
Did you really mean to count open/openings, but exclude the REOPENED state?\n"); } if(($::FORM{'metric_verify'} || $::FORM{'metric_verified'}) && $::FORM{'bug_status'} && $::FORM{'bug_status'} !~ /VERIFIED/) { print("
Did you really mean to count verified/verifies, but exclude the VERIFY state?\n"); } if(($::FORM{'metric_closings'} || $::FORM{'metric_closed'}) && $::FORM{'bug_status'} && $::FORM{'bug_status'} !~ /CLOSED/) { print("
Did you really mean to count closed/closings, but exclude the CLOSED state?\n"); } if(($::FORM{'metric_resolved'} || $::FORM{'metric_resolves'}) && $::FORM{'resolution'}) { print("
Did you really mean to count resolves, and filter based on resolution?\n"); } if(($::FORM{'metric_fixed'} || $::FORM{'metric_fixes'}) && $::FORM{'resolution'} && $::FORM{'resolution'} !~ /FIXED/) { print("
Did you really mean to count fixed/fixes, but exclude resolution FIXED?\n"); } if(($::FORM{'metric_resolved'} || $::FORM{'metric_resolves'}) && $::FORM{'resolution'}) { print("
Did you really mean to count resolved/resolves non-fixed, and filter based on resolution?\n"); } 1; # needed last for return value