1<?php
2# MantisBT - A PHP based bugtracking system
3
4# MantisBT is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 2 of the License, or
7# (at your option) any later version.
8#
9# MantisBT is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with MantisBT.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Summary API
19 *
20 * @package CoreAPI
21 * @subpackage SummaryAPI
22 * @copyright Copyright 2000 - 2002  Kenzaburo Ito - kenito@300baud.org
23 * @copyright Copyright 2002  MantisBT Team - mantisbt-dev@lists.sourceforge.net
24 * @link http://www.mantisbt.org
25 *
26 * @uses access_api.php
27 * @uses authentication_api.php
28 * @uses bug_api.php
29 * @uses config_api.php
30 * @uses constant_inc.php
31 * @uses current_user_api.php
32 * @uses database_api.php
33 * @uses filter_constants_inc.php
34 * @uses helper_api.php
35 * @uses project_api.php
36 * @uses string_api.php
37 * @uses user_api.php
38 * @uses utility_api.php
39 */
40
41require_api( 'access_api.php' );
42require_api( 'authentication_api.php' );
43require_api( 'bug_api.php' );
44require_api( 'config_api.php' );
45require_api( 'constant_inc.php' );
46require_api( 'current_user_api.php' );
47require_api( 'database_api.php' );
48require_api( 'filter_constants_inc.php' );
49require_api( 'helper_api.php' );
50require_api( 'project_api.php' );
51require_api( 'string_api.php' );
52require_api( 'user_api.php' );
53require_api( 'utility_api.php' );
54
55/**
56 * Print row with percentage in summary table
57 *
58 * @param string $p_label    The summary row label.
59 * @param string $p_open     Count of open issues - normally string with hyperlink to filter.
60 * @param string $p_resolved Count of resolved issues - normally string with hyperlink to filter.
61 * @param string $p_closed   Count of closed issues - normally string with hyperlink to filter.
62 * @param string $p_total    Count of total issues - normally string with hyperlink to filter.
63 * @param string $p_resolved_ratio  Ratio of resolved
64 * @param string $p_ratio    Ratio of total bugs
65 * @return void
66 */
67function summary_helper_print_row( $p_label, $p_open, $p_resolved, $p_closed, $p_total, $p_resolved_ratio, $p_ratio) {
68	echo '<tr>';
69	printf( '<td class="width50">%s</td>', $p_label );
70	printf( '<td class="width12 align-right">%s</td>', $p_open );
71	printf( '<td class="width12 align-right">%s</td>', $p_resolved );
72	printf( '<td class="width12 align-right">%s</td>', $p_closed );
73	printf( '<td class="width12 align-right">%s</td>', $p_total );
74	printf( '<td class="width12 align-right">%s</td>', $p_resolved_ratio );
75	printf( '<td class="width12 align-right">%s</td>', $p_ratio );
76	echo '</tr>';
77}
78
79/**
80 * Returns a string representation of the user, together with a link to the issues
81 * acted on by the user ( reported, handled or commented on )
82 *
83 * @param integer $p_user_id A valid user identifier.
84 * @param array $p_filter Filter array.
85 * @return string
86 */
87function summary_helper_get_developer_label( $p_user_id, array $p_filter = null ) {
88	$t_user = string_display_line( user_get_name( $p_user_id ) );
89
90	$t_link_prefix = summary_get_link_prefix( $p_filter );
91
92	return '<a class="subtle" href="' . $t_link_prefix
93		. '&amp;' . FILTER_PROPERTY_REPORTER_ID . '=' . $p_user_id
94		. '&amp;' . FILTER_PROPERTY_HANDLER_ID . '=' . $p_user_id
95		. '&amp;' . FILTER_PROPERTY_NOTE_USER_ID . '=' . $p_user_id
96		. '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . META_FILTER_NONE
97		. '&amp;' . FILTER_PROPERTY_MATCH_TYPE . '=' . FILTER_MATCH_ANY
98		. '">' . $t_user . '</a>';
99
100}
101
102/**
103 * Calculate bug status count according to 'open', 'resolved' and 'closed',
104 * then put the numbers into $p_cache array
105 *
106 * @param array &$p_cache    The cache array.
107 * @param string $p_key      The key of the array.
108 * @param string $p_status   The status of issues.
109 * @param integer $p_bugcount The bug count of $p_status issues.
110 * @return void
111 */
112function summary_helper_build_bugcount( &$p_cache, $p_key, $p_status, $p_bugcount ) {
113	$t_resolved_val = config_get( 'bug_resolved_status_threshold' );
114	$t_closed_val = config_get( 'bug_closed_status_threshold' );
115
116	if( $t_closed_val <= $p_status ) {
117		if( isset( $p_cache[$p_key]['closed'] ) ) {
118			$p_cache[$p_key]['closed'] += $p_bugcount;
119		} else {
120			$p_cache[$p_key]['closed'] = $p_bugcount;
121		}
122	} else if( $t_resolved_val <= $p_status ) {
123		if( isset( $p_cache[$p_key]['resolved'] ) ) {
124			$p_cache[$p_key]['resolved'] += $p_bugcount;
125		} else {
126			$p_cache[$p_key]['resolved'] = $p_bugcount;
127		}
128	} else {
129		if( isset( $p_cache[$p_key]['open'] ) ) {
130			$p_cache[$p_key]['open'] += $p_bugcount;
131		} else {
132			$p_cache[$p_key]['open'] = $p_bugcount;
133		}
134	}
135}
136/**
137 * Build bug links for 'open', 'resolved' and 'closed' issue counts
138 *
139 * @param string $p_bug_link            The base bug link.
140 * @param string &$p_bugs_open          The open bugs count, return open bugs link.
141 * @param string &$p_bugs_resolved      The resolved bugs count, return resolved bugs link.
142 * @param string &$p_bugs_closed        The closed bugs count, return closed bugs link.
143 * @param string &$p_bugs_total         The total bugs count, return total bugs link.
144 * @return void
145 */
146function summary_helper_build_buglinks( $p_bug_link, &$p_bugs_open, &$p_bugs_resolved, &$p_bugs_closed, &$p_bugs_total) {
147	$t_resolved_val = config_get( 'bug_resolved_status_threshold' );
148	$t_closed_val = config_get( 'bug_closed_status_threshold' );
149
150	if( 0 < $p_bugs_open ) {
151		$p_bugs_open = '<a class="subtle" href="' . $p_bug_link . '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . $t_resolved_val . '">' . $p_bugs_open . '</a>';
152	}
153	if( 0 < $p_bugs_resolved ) {
154		$p_bugs_resolved = '<a class="subtle" href="' . $p_bug_link . '&amp;' . FILTER_PROPERTY_STATUS . '=' . $t_resolved_val . '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . $t_closed_val . '">' . $p_bugs_resolved . '</a>';
155	}
156	if( 0 < $p_bugs_closed ) {
157		$p_bugs_closed = '<a class="subtle" href="' . $p_bug_link . '&amp;' . FILTER_PROPERTY_STATUS . '=' . $t_closed_val . '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . META_FILTER_NONE . '">' . $p_bugs_closed . '</a>';
158	}
159	if( 0 < $p_bugs_total ) {
160		$p_bugs_total = '<a class="subtle" href="' . $p_bug_link . '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . META_FILTER_NONE . '">' . $p_bugs_total . '</a>';
161	}
162}
163
164/**
165 * Calculate bug ratio
166 * @param integer $p_bugs_open            The open bugs count.
167 * @param integer $p_bugs_resolved        The resolved bugs count.
168 * @param integer $p_bugs_closed          The closed bugs count.
169 * @param integer $p_bugs_total_count     The total bugs count.
170 * @return array  array of ($t_bugs_resolved_ratio, $t_bugs_ratio)
171 */
172function summary_helper_get_bugratio( $p_bugs_open, $p_bugs_resolved, $p_bugs_closed, $p_bugs_total_count) {
173	$t_bugs_total = $p_bugs_open + $p_bugs_resolved + $p_bugs_closed;
174	$t_bugs_resolved_ratio = ( $p_bugs_resolved + $p_bugs_closed ) / ( $t_bugs_total == 0 ? 1 : $t_bugs_total );
175	$t_bugs_ratio = $t_bugs_total / ( $p_bugs_total_count == 0 ? 1 : $p_bugs_total_count );
176	$t_bugs_resolved_ratio = sprintf( "%.1f%%", $t_bugs_resolved_ratio * 100 );
177	$t_bugs_ratio = sprintf( "%.1f%%", $t_bugs_ratio * 100 );
178	return array($t_bugs_resolved_ratio, $t_bugs_ratio);
179}
180
181/**
182 * Used in summary reports - this function prints out the summary for the given enum setting.
183 * The enum field name is passed in through $p_enum.
184 * A filter can be used to limit the visibility.
185 *
186 * @param string $p_enum Enum field name.
187 * @param array $p_filter Filter array.
188 * @return void
189 */
190function summary_print_by_enum( $p_enum, array $p_filter = null ) {
191	$t_project_id = helper_get_current_project();
192
193	$t_project_filter = helper_project_specific_where( $t_project_id );
194	if( ' 1<>1' == $t_project_filter ) {
195		return;
196	}
197	$t_link_prefix = summary_get_link_prefix( $p_filter );
198
199	$t_status_query = ( 'status' == $p_enum ) ? '' : ' ,status ';
200	$t_query = new DBQuery();
201	$t_sql = 'SELECT COUNT(id) as bugcount, ' . $p_enum . ' ' . $t_status_query
202		. ' FROM {bug} WHERE ' . $t_project_filter;
203	if( !empty( $p_filter ) ) {
204		$t_subquery = filter_cache_subquery( $p_filter );
205		$t_sql .= ' AND {bug}.id IN :filter';
206		$t_query->bind( 'filter', $t_subquery );
207	}
208	$t_sql .= ' GROUP BY ' . $p_enum . ' ' . $t_status_query
209		. ' ORDER BY ' . $p_enum . ' ' . $t_status_query;
210	$t_query->sql( $t_sql );
211
212	$t_cache = array();
213	$t_bugs_total_count = 0;
214
215	while( $t_row = $t_query->fetch() ) {
216		$t_enum = $t_row[$p_enum];
217		$t_status = $t_row['status'];
218		$t_bugcount = $t_row['bugcount'];
219		$t_bugs_total_count += $t_bugcount;
220
221		summary_helper_build_bugcount( $t_cache, $t_enum, $t_status, $t_bugcount );
222	}
223
224	switch( $p_enum ) {
225		case 'status':
226			$t_filter_property = FILTER_PROPERTY_STATUS;
227			break;
228		case 'severity':
229			$t_filter_property = FILTER_PROPERTY_SEVERITY;
230			break;
231		case 'resolution':
232			$t_filter_property = FILTER_PROPERTY_RESOLUTION;
233			break;
234		case 'priority':
235			$t_filter_property = FILTER_PROPERTY_PRIORITY;
236			break;
237		default:
238			# Unknown Enum type
239			trigger_error( ERROR_GENERIC, ERROR );
240	}
241
242	foreach( $t_cache as $t_enum => $t_item) {
243		# Build up the hyperlinks to bug views
244		$t_bugs_open = isset( $t_item['open'] ) ? $t_item['open'] : 0;
245		$t_bugs_resolved = isset( $t_item['resolved'] ) ? $t_item['resolved'] : 0;
246		$t_bugs_closed = isset( $t_item['closed'] ) ? $t_item['closed'] : 0;
247		$t_bugs_total = $t_bugs_open + $t_bugs_resolved + $t_bugs_closed;
248		$t_bugs_ratio = summary_helper_get_bugratio( $t_bugs_open, $t_bugs_resolved, $t_bugs_closed, $t_bugs_total_count);
249
250		$t_bug_link = $t_link_prefix . '&amp;' . $t_filter_property . '=' . $t_enum;
251
252		if( !is_blank( $t_bug_link ) ) {
253			$t_resolved_val = config_get( 'bug_resolved_status_threshold' );
254			$t_closed_val = config_get( 'bug_closed_status_threshold' );
255
256			if( 0 < $t_bugs_open ) {
257				$t_bugs_open = '<a class="subtle" href="' . $t_bug_link
258					. '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . $t_resolved_val . '">'
259					. $t_bugs_open . '</a>';
260			} else {
261				if( ( 'status' == $p_enum ) && ( $t_enum >= $t_resolved_val ) ) {
262					$t_bugs_open = '-';
263				}
264			}
265			if( 0 < $t_bugs_resolved ) {
266				$t_bugs_resolved = '<a class="subtle" href="' . $t_bug_link
267					# Only add status filter if not already part of the link
268					. ( 'status' != $p_enum ? '&amp;' . FILTER_PROPERTY_STATUS . '=' . $t_resolved_val : '' )
269					. '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . $t_closed_val . '">'
270					. $t_bugs_resolved . '</a>';
271			} else {
272				if( ( 'status' == $p_enum ) && (( $t_enum < $t_resolved_val ) || ( $t_enum >= $t_closed_val ) ) ) {
273					$t_bugs_resolved = '-';
274				}
275			}
276			if( 0 < $t_bugs_closed ) {
277				$t_bugs_closed = '<a class="subtle" href="' . $t_bug_link
278					# Only add status filter if not already part of the link
279					. ( 'status' != $p_enum ? '&amp;' . FILTER_PROPERTY_STATUS . '=' . $t_closed_val : '' )
280					. '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . META_FILTER_NONE . '">'
281					. $t_bugs_closed . '</a>';
282			} else {
283				if( ( 'status' == $p_enum ) && ( $t_enum < $t_closed_val ) ) {
284					$t_bugs_closed = '-';
285				}
286			}
287			if( 0 < $t_bugs_total ) {
288				$t_bugs_total = '<a class="subtle" href="' . $t_bug_link
289					. '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '='
290					. META_FILTER_NONE . '">' . $t_bugs_total . '</a>';
291			}
292			if( 'status' == $p_enum )  $t_bugs_ratio[0] = '-';
293		}
294		summary_helper_print_row( get_enum_element( $p_enum, $t_enum ), $t_bugs_open, $t_bugs_resolved, $t_bugs_closed, $t_bugs_total, $t_bugs_ratio[0], $t_bugs_ratio[1] );
295	}
296}
297
298/**
299 * Print list of open bugs with the highest activity score the score is calculated assigning
300 * one "point" for each history event associated with the bug.
301 * A filter can be used to limit the visibility.
302 *
303 * @param array $p_filter Filter array.
304 * @return void
305 */
306function summary_print_by_activity( array $p_filter = null ) {
307	$t_project_id = helper_get_current_project();
308	$t_resolved = config_get( 'bug_resolved_status_threshold' );
309	$t_specific_where = helper_project_specific_where( $t_project_id );
310	if( ' 1<>1' == $t_specific_where ) {
311		return;
312	}
313	$t_query = new DBQuery();
314	$t_sql = 'SELECT COUNT(h.id) as count, b.id, b.summary, b.view_state'
315		. ' FROM {bug} b JOIN {bug_history} h ON h.bug_id = b.id'
316		. ' WHERE b.status < ' . $t_query->param( (int)$t_resolved )
317		. ' AND ' . $t_specific_where;
318	if( !empty( $p_filter ) ) {
319		$t_subquery = filter_cache_subquery( $p_filter );
320		$t_sql .= ' AND b.id IN :filter';
321		$t_query->bind( 'filter', $t_subquery );
322	}
323	$t_sql .= ' GROUP BY h.bug_id, b.id, b.summary, b.last_updated, b.view_state'
324		. ' ORDER BY count DESC, b.last_updated DESC';
325	$t_query->sql( $t_sql );
326
327	$t_count = 0;
328	$t_private_bug_threshold = config_get( 'private_bug_threshold' );
329	$t_summarydata = array();
330	$t_summarybugs = array();
331	while( $t_row = $t_query->fetch() ) {
332		# Skip private bugs unless user has proper permissions
333		if( ( VS_PRIVATE == $t_row['view_state'] ) && ( false == access_has_bug_level( $t_private_bug_threshold, $t_row['id'] ) ) ) {
334			continue;
335		}
336
337		if( $t_count++ == 10 ) {
338			break;
339		}
340
341		$t_summarydata[] = array(
342			'id' => $t_row['id'],
343			'summary' => $t_row['summary'],
344			'count' => $t_row['count'],
345		);
346		$t_summarybugs[] = $t_row['id'];
347	}
348
349	bug_cache_array_rows( $t_summarybugs );
350
351	foreach( $t_summarydata as $t_row ) {
352		$t_bugid = string_get_bug_view_link( $t_row['id'], false );
353		$t_summary = string_display_line( $t_row['summary'] );
354		$t_notescount = $t_row['count'];
355
356		echo '<tr>' . "\n";
357		echo '<td class="small">' . $t_bugid . ' - ' . $t_summary . '</td><td class="align-right">' . $t_notescount . '</td>' . "\n";
358		echo '</tr>' . "\n";
359	}
360}
361
362/**
363 * Print list of bugs opened from the longest time.
364 * A filter can be used to limit the visibility.
365 *
366 * @param array $p_filter Filter array.
367 * @return void
368 */
369function summary_print_by_age( array $p_filter = null ) {
370	$t_project_id = helper_get_current_project();
371	$t_resolved = config_get( 'bug_resolved_status_threshold' );
372
373	$t_specific_where = helper_project_specific_where( $t_project_id );
374	if( ' 1<>1' == $t_specific_where ) {
375		return;
376	}
377	$t_query = new DBQuery();
378	$t_sql = 'SELECT * FROM {bug} WHERE status < ' . $t_query->param( (int)$t_resolved )
379		. ' AND ' . $t_specific_where;
380	if( !empty( $p_filter ) ) {
381		$t_subquery = filter_cache_subquery( $p_filter );
382		$t_sql .= ' AND {bug}.id IN :filter';
383		$t_query->bind( 'filter', $t_subquery );
384	}
385	$t_sql .= ' ORDER BY date_submitted ASC, priority DESC';
386	$t_query->sql( $t_sql );
387
388	$t_count = 0;
389	$t_private_bug_threshold = config_get( 'private_bug_threshold' );
390
391	while( $t_row = $t_query->fetch() ) {
392		# as we select all from bug_table, inject into the cache.
393		bug_cache_database_result( $t_row );
394
395		# Skip private bugs unless user has proper permissions
396		if( ( VS_PRIVATE == bug_get_field( $t_row['id'], 'view_state' ) ) && ( false == access_has_bug_level( $t_private_bug_threshold, $t_row['id'] ) ) ) {
397			continue;
398		}
399
400		if( $t_count++ == 10 ) {
401			break;
402		}
403
404		$t_bugid = string_get_bug_view_link( $t_row['id'], false );
405		$t_summary = string_display_line( $t_row['summary'] );
406		$t_days_open = intval( ( time() - $t_row['date_submitted'] ) / SECONDS_PER_DAY );
407
408		echo '<tr>' . "\n";
409		echo '<td class="small">' . $t_bugid . ' - ' . $t_summary . '</td><td class="align-right">' . $t_days_open . '</td>' . "\n";
410		echo '</tr>' . "\n";
411	}
412}
413
414/**
415 * print bug counts by assigned to each developer.
416 * A filter can be used to limit the visibility.
417 *
418 * @param array $p_filter Filter array.
419 * @return void
420 */
421function summary_print_by_developer( array $p_filter = null ) {
422	$t_project_id = helper_get_current_project();
423
424	$t_specific_where = helper_project_specific_where( $t_project_id );
425	if( ' 1<>1' == $t_specific_where ) {
426		return;
427	}
428
429	$t_query = new DBQuery();
430	$t_sql = 'SELECT COUNT(id) as bugcount, handler_id, status'
431		. ' FROM {bug} WHERE handler_id>0 AND ' . $t_specific_where;
432	if( !empty( $p_filter ) ) {
433		$t_subquery = filter_cache_subquery( $p_filter );
434		$t_sql .= ' AND {bug}.id IN :filter';
435		$t_query->bind( 'filter', $t_subquery );
436	}
437	$t_sql .= ' GROUP BY handler_id, status'
438		. ' ORDER BY handler_id, status';
439	$t_query->sql( $t_sql );
440
441	$t_summaryusers = array();
442	$t_cache = array();
443	$t_bugs_total_count = 0;
444
445	while( $t_row = $t_query->fetch() ) {
446		$t_summaryusers[] = $t_row['handler_id'];
447		$t_status = $t_row['status'];
448		$t_bugcount = $t_row['bugcount'];
449		$t_bugs_total_count += $t_bugcount;
450		$t_label = $t_row['handler_id'];
451
452		summary_helper_build_bugcount( $t_cache, $t_label, $t_status, $t_bugcount );
453	}
454
455	user_cache_array_rows( array_unique( $t_summaryusers ) );
456
457	foreach( $t_cache as $t_label => $t_item) {
458		# Build up the hyperlinks to bug views
459		$t_bugs_open = isset( $t_item['open'] ) ? $t_item['open'] : 0;
460		$t_bugs_resolved = isset( $t_item['resolved'] ) ? $t_item['resolved'] : 0;
461		$t_bugs_closed = isset( $t_item['closed'] ) ? $t_item['closed'] : 0;
462		$t_bugs_total = $t_bugs_open + $t_bugs_resolved + $t_bugs_closed;
463		$t_bugs_ratio = summary_helper_get_bugratio( $t_bugs_open, $t_bugs_resolved, $t_bugs_closed, $t_bugs_total_count);
464
465		$t_link_prefix = summary_get_link_prefix( $p_filter );
466
467		$t_bug_link = $t_link_prefix . '&amp;' . FILTER_PROPERTY_HANDLER_ID . '=' . $t_label;
468		$t_label = summary_helper_get_developer_label( $t_label, $p_filter );
469		summary_helper_build_buglinks( $t_bug_link, $t_bugs_open, $t_bugs_resolved, $t_bugs_closed, $t_bugs_total );
470		summary_helper_print_row( $t_label, $t_bugs_open, $t_bugs_resolved, $t_bugs_closed, $t_bugs_total, $t_bugs_ratio[0], $t_bugs_ratio[1] );
471	}
472}
473
474/**
475 * Print bug counts by reporter id.
476 * A filter can be used to limit the visibility.
477 *
478 * @param array $p_filter Filter array.
479 * @return void
480 */
481function summary_print_by_reporter( array $p_filter = null ) {
482	$t_reporter_summary_limit = config_get( 'reporter_summary_limit' );
483
484	$t_project_id = helper_get_current_project();
485
486	$t_specific_where = helper_project_specific_where( $t_project_id );
487	if( ' 1<>1' == $t_specific_where ) {
488		return;
489	}
490	$t_query = new DBQuery();
491	$t_sql = 'SELECT reporter_id, COUNT(*) as num FROM {bug} WHERE ' . $t_specific_where;
492	if( !empty( $p_filter ) ) {
493		$t_subquery = filter_cache_subquery( $p_filter );
494		$t_sql .= ' AND {bug}.id IN :filter';
495		$t_query->bind( 'filter', $t_subquery );
496	}
497	$t_sql .= ' GROUP BY reporter_id ORDER BY num DESC';
498	$t_query->sql( $t_sql );
499
500	$t_reporters = array();
501	$t_bugs_total_count = 0;
502	$t_reporters_count = 0;
503	while( $t_row = $t_query->fetch() ) {
504		$t_reporters[] = (int)$t_row['reporter_id'];
505		$t_bugs_total_count += $t_row['num'];
506		$t_reporters_count++;
507		if( $t_reporters_count == $t_reporter_summary_limit ) {
508			break;
509		}
510	}
511
512	if( empty( $t_reporters ) ) {
513		return;
514	}
515	user_cache_array_rows( $t_reporters );
516
517	$t_query = new DBQuery();
518	$t_sql = 'SELECT reporter_id, status, COUNT(id) AS bugcount FROM {bug}'
519		. ' WHERE ' . $t_query->sql_in( 'reporter_id', $t_reporters )
520		. ' AND ' . $t_specific_where;
521	if( !empty( $p_filter ) ) {
522		$t_subquery = filter_cache_subquery( $p_filter );
523		$t_sql .= ' AND {bug}.id IN :filter';
524		$t_query->bind( 'filter', $t_subquery );
525	}
526	$t_sql .= ' GROUP BY reporter_id, status ORDER BY reporter_id, status';
527	$t_query->sql( $t_sql );
528
529	$t_resolved_status = config_get( 'bug_resolved_status_threshold' );
530	$t_closed_status = config_get( 'bug_closed_status_threshold' );
531	$t_reporter_stats = array();
532	while( $t_row = $t_query->fetch() ) {
533		$t_reporter_id = (int)$t_row['reporter_id'];
534		if( !isset( $t_reporter_stats[$t_reporter_id] ) ) {
535			$t_reporter_stats[$t_reporter_id] = array(
536				'open' => 0,
537				'resolved' => 0,
538				'closed' => 0,
539				'total' => 0,
540				'reporter_id' => $t_reporter_id
541				);
542		}
543		$t_bugcount = (int)$t_row['bugcount'];
544		$t_status = (int)$t_row['status'];
545		$t_reporter_stats[$t_reporter_id]['total'] += $t_bugcount;
546		if( $t_status >= $t_closed_status ) {
547			$t_reporter_stats[$t_reporter_id]['closed'] += $t_bugcount;
548		} elseif ( $t_status >= $t_resolved_status ) {
549			$t_reporter_stats[$t_reporter_id]['resolved'] += $t_bugcount;
550		} else {
551			$t_reporter_stats[$t_reporter_id]['open'] += $t_bugcount;
552		}
553	}
554
555	# calculate ratios
556	foreach( $t_reporter_stats as $t_reporter_id => $t_stats ) {
557		$t_reporter_stats[$t_reporter_id]['ratios'] =summary_helper_get_bugratio(
558				$t_stats['open'],
559				$t_stats['resolved'],
560				$t_stats['closed'],
561				$t_bugs_total_count
562				);
563	}
564
565	# sort based on total issue count
566	# note that after array_multisort, we lose the numeric indexes, but we stored
567	# the reporter id inside each sub-array
568	array_multisort ( array_column( $t_reporter_stats, 'total' ), SORT_DESC, $t_reporter_stats );
569
570	# print results
571	foreach( $t_reporter_stats as $t_stats ) {
572		if( $t_stats['total'] == 0 ) {
573			continue;
574		}
575		$t_reporter_id = $t_stats['reporter_id'];
576		$t_user = string_display_line( user_get_name( $t_reporter_id ) );
577		$t_link_prefix = summary_get_link_prefix( $p_filter );
578
579		$t_bug_link = $t_link_prefix . '&amp;' . FILTER_PROPERTY_REPORTER_ID . '=' . $t_reporter_id;
580		if( 0 < $t_stats['open'] ) {
581			$t_bugs_open = '<a class="subtle" href="' . $t_bug_link . '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . $t_resolved_status . '">'. $t_stats['open'] . '</a>';
582		} else {
583			$t_bugs_open = 0;
584		}
585		if( 0 < $t_stats['resolved'] ) {
586			$t_bugs_resolved = '<a class="subtle" href="' . $t_bug_link . '&amp;' . FILTER_PROPERTY_STATUS . '=' . $t_resolved_status . '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . $t_closed_status . '">' . $t_stats['resolved'] . '</a>';
587		} else {
588			$t_bugs_resolved = 0;
589		}
590		if( 0 < $t_stats['closed'] ) {
591			$t_bugs_closed = '<a class="subtle" href="' . $t_bug_link . '&amp;' . FILTER_PROPERTY_STATUS . '=' . $t_closed_status . '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . META_FILTER_NONE . '">' . $t_stats['closed'] . '</a>';
592		} else {
593			$t_bugs_closed = 0;
594		}
595		if( 0 < $t_stats['total'] ) {
596			$t_bugs_total = '<a class="subtle" href="' . $t_bug_link . '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . META_FILTER_NONE . '">' . $t_stats['total'] . '</a>';
597		} else {
598			$t_bugs_total = 0;
599		}
600		$t_bugs_ratio = $t_stats['ratios'];
601		summary_helper_print_row( $t_user, $t_bugs_open, $t_bugs_resolved, $t_bugs_closed, $t_bugs_total, $t_bugs_ratio[0], $t_bugs_ratio[1] );
602	}
603}
604
605/**
606 * Print a bug count per category.
607 * A filter can be used to limit the visibility.
608 *
609 * @param array $p_filter Filter array.
610 * @return void
611 */
612function summary_print_by_category( array $p_filter = null ) {
613	$t_summary_category_include_project = config_get( 'summary_category_include_project' );
614
615	$t_project_id = helper_get_current_project();
616
617	$t_specific_where = trim( helper_project_specific_where( $t_project_id ) );
618	if( '1<>1' == $t_specific_where ) {
619		return;
620	}
621	$t_project_query = ( ON == $t_summary_category_include_project ) ? 'b.project_id, ' : '';
622
623	$t_query = new DBQuery();
624	$t_sql = 'SELECT COUNT(b.id) as bugcount, ' . $t_project_query . ' c.name AS category_name, category_id, b.status'
625		. ' FROM {bug} b JOIN {category} c ON b.category_id=c.id'
626		. ' WHERE b.' . $t_specific_where;
627	if( !empty( $p_filter ) ) {
628		$t_subquery = filter_cache_subquery( $p_filter );
629		$t_sql .= ' AND b.id IN :filter';
630		$t_query->bind( 'filter', $t_subquery );
631	}
632	$t_sql .= ' GROUP BY ' . $t_project_query . ' c.name, b.category_id, b.status'
633		. ' ORDER BY ' . $t_project_query . ' c.name';
634	$t_query->sql( $t_sql );
635
636	$t_cache = array();
637	$t_bugs_total_count = 0;
638
639	while( $t_row = $t_query->fetch() ) {
640		$t_status = $t_row['status'];
641		$t_bugcount = $t_row['bugcount'];
642		$t_bugs_total_count += $t_bugcount;
643		$t_label = $t_row['category_name'];
644		if( ( ON == $t_summary_category_include_project ) && ( ALL_PROJECTS == $t_project_id ) ) {
645			$t_label = sprintf( '[%s] %s', project_get_name( $t_row['project_id'] ), $t_label );
646		}
647
648		summary_helper_build_bugcount( $t_cache, $t_label, $t_status, $t_bugcount );
649	}
650
651	foreach( $t_cache as $t_label => $t_item) {
652		# Build up the hyperlinks to bug views
653		$t_bugs_open = isset( $t_item['open'] ) ? $t_item['open'] : 0;
654		$t_bugs_resolved = isset( $t_item['resolved'] ) ? $t_item['resolved'] :0;
655		$t_bugs_closed = isset( $t_item['closed'] ) ? $t_item['closed'] : 0;
656		$t_bugs_total = $t_bugs_open + $t_bugs_resolved + $t_bugs_closed;
657		$t_bugs_ratio = summary_helper_get_bugratio( $t_bugs_open, $t_bugs_resolved, $t_bugs_closed, $t_bugs_total_count);
658
659		$t_link_prefix = summary_get_link_prefix( $p_filter );
660
661		$t_bug_link = $t_link_prefix . '&amp;' . FILTER_PROPERTY_CATEGORY_ID . '=' . urlencode( $t_label );
662		summary_helper_build_buglinks( $t_bug_link, $t_bugs_open, $t_bugs_resolved, $t_bugs_closed, $t_bugs_total );
663		summary_helper_print_row( string_display_line( $t_label ), $t_bugs_open, $t_bugs_resolved, $t_bugs_closed, $t_bugs_total, $t_bugs_ratio[0], $t_bugs_ratio[1] );
664	}
665}
666
667/**
668 * Print bug counts by project.
669 * A filter can be used to limit the visibility.
670 * @todo check p_cache - static?
671 *
672 * @param array   $p_projects Array of project id's.
673 * @param integer $p_level    Indicates the depth of the project within the sub-project hierarchy.
674 * @param array   $p_cache    Summary cache.
675 * @param array   $p_filter   Filter array.
676 * @return void
677 */
678function summary_print_by_project( array $p_projects = array(), $p_level = 0, array $p_cache = null, array $p_filter = null ) {
679	$t_project_id = helper_get_current_project();
680
681	if( empty( $p_projects ) ) {
682		if( ALL_PROJECTS == $t_project_id ) {
683			$p_projects = current_user_get_accessible_projects();
684		} else {
685			$p_projects = array(
686				$t_project_id,
687			);
688		}
689	}
690
691	# Retrieve statistics one time to improve performance.
692	if( null === $p_cache ) {
693		$t_query = new DBQuery();
694		$t_sql = 'SELECT project_id, status, COUNT( status ) AS bugcount FROM {bug}';
695		if( !empty( $p_filter ) ) {
696			$t_subquery = filter_cache_subquery( $p_filter );
697			$t_sql .= ' WHERE {bug}.id IN :filter';
698			$t_query->bind( 'filter', $t_subquery );
699		}
700		$t_sql .= ' GROUP BY project_id, status';
701		$t_query->sql( $t_sql );
702
703		$p_cache = array();
704		$t_bugs_total_count = 0;
705		while( $t_row = $t_query->fetch() ) {
706			$t_project_id = $t_row['project_id'];
707			$t_status = $t_row['status'];
708			$t_bugcount = $t_row['bugcount'];
709			$t_bugs_total_count += $t_bugcount;
710
711			summary_helper_build_bugcount( $p_cache, $t_project_id, $t_status, $t_bugcount );
712		}
713		$p_cache["_bugs_total_count_"] = $t_bugs_total_count;
714	}
715
716	$t_bugs_total_count = $p_cache["_bugs_total_count_"];
717	foreach( $p_projects as $t_project ) {
718		$t_name = str_repeat( '&raquo; ', $p_level ) . project_get_name( $t_project );
719
720		$t_pdata = isset( $p_cache[$t_project] ) ? $p_cache[$t_project] : array( 'open' => 0, 'resolved' => 0, 'closed' => 0 );
721
722		$t_bugs_open = isset( $t_pdata['open'] ) ? $t_pdata['open'] : 0;
723		$t_bugs_resolved = isset( $t_pdata['resolved'] ) ? $t_pdata['resolved'] : 0;
724		$t_bugs_closed = isset( $t_pdata['closed'] ) ? $t_pdata['closed'] : 0;
725		$t_bugs_total = $t_bugs_open + $t_bugs_resolved + $t_bugs_closed;
726
727		$t_bugs_ratio = summary_helper_get_bugratio( $t_bugs_open, $t_bugs_resolved, $t_bugs_closed, $t_bugs_total_count);
728
729# FILTER_PROPERTY_PROJECT_ID filter by project does not work ??
730#		$t_bug_link = '<a class="subtle" href="' . config_get( 'bug_count_hyperlink_prefix' ) . '&amp;' . FILTER_PROPERTY_PROJECT_ID . '=' . urlencode( $t_project );
731#		summary_helper_build_buglinks( $t_bug_link, $t_bugs_open, $t_bugs_resolved, $t_bugs_closed, $t_bugs_total );
732
733		summary_helper_print_row( string_display_line( $t_name ), $t_bugs_open, $t_bugs_resolved, $t_bugs_closed, $t_bugs_total, $t_bugs_ratio[0], $t_bugs_ratio[1]);
734
735		if( count( project_hierarchy_get_subprojects( $t_project ) ) > 0 ) {
736			$t_subprojects = current_user_get_accessible_subprojects( $t_project );
737
738			if( count( $t_subprojects ) > 0 ) {
739				summary_print_by_project( $t_subprojects, $p_level + 1, $p_cache );
740			}
741		}
742	}
743}
744
745/**
746 * Print developer / resolution report.
747 * A filter can be used to limit the visibility.
748 *
749 * @param string $p_resolution_enum_string Resolution enumeration string value.
750 * @param array $p_filter Filter array.
751 * @return void
752 */
753function summary_print_developer_resolution( $p_resolution_enum_string, array $p_filter = null ) {
754	$t_project_id = helper_get_current_project();
755
756	# Get the resolution values to use
757	$c_res_s = MantisEnum::getValues( $p_resolution_enum_string );
758	$t_enum_res_count = count( $c_res_s );
759
760	$t_specific_where = helper_project_specific_where( $t_project_id );
761	if( ' 1<>1' == $t_specific_where ) {
762		return;
763	}
764
765	$t_specific_where .= ' AND handler_id > 0';
766
767	# Get all of the bugs and split them up into an array
768	$t_query = new DBQuery();
769	$t_sql = 'SELECT COUNT(id) as bugcount, handler_id, resolution'
770		. ' FROM {bug} WHERE ' . $t_specific_where;
771	if( !empty( $p_filter ) ) {
772		$t_subquery = filter_cache_subquery( $p_filter );
773		$t_sql .= ' AND {bug}.id IN :filter';
774		$t_query->bind( 'filter', $t_subquery );
775	}
776	$t_sql .= ' GROUP BY handler_id, resolution'
777		. ' ORDER BY handler_id, resolution';
778	$t_query->sql( $t_sql );
779
780	$t_handler_res_arr = array();
781	$t_arr = $t_query->fetch();
782	while( $t_arr ) {
783		if( !isset( $t_handler_res_arr[$t_arr['handler_id']] ) ) {
784			$t_handler_res_arr[$t_arr['handler_id']] = array();
785			$t_handler_res_arr[$t_arr['handler_id']]['total'] = 0;
786		}
787		if( !isset( $t_handler_res_arr[$t_arr['handler_id']][$t_arr['resolution']] ) ) {
788			$t_handler_res_arr[$t_arr['handler_id']][$t_arr['resolution']] = 0;
789		}
790		$t_handler_res_arr[$t_arr['handler_id']][$t_arr['resolution']] += $t_arr['bugcount'];
791		$t_handler_res_arr[$t_arr['handler_id']]['total'] += $t_arr['bugcount'];
792
793		$t_arr = $t_query->fetch();
794	}
795
796	# Sort array so devs with highest number of bugs are listed first
797	uasort( $t_handler_res_arr,
798		function( $a, $b ) {
799			return $b['total'] - $a['total'];
800		}
801	);
802
803	$t_threshold_fixed = config_get( 'bug_resolution_fixed_threshold' );
804	$t_threshold_notfixed = config_get( 'bug_resolution_not_fixed_threshold' );
805
806	$t_link_prefix = summary_get_link_prefix( $p_filter );
807
808	$t_row_count = 0;
809
810	# We now have a multi dimensional array of users and resolutions, with the value of each resolution for each user
811	foreach( $t_handler_res_arr as $t_handler_id => $t_arr2 ) {
812		$t_total = $t_arr2['total'];
813
814		# Only print developers who have had at least one bug assigned to them. This helps
815		# prevent divide by zeroes, showing developers not on this project, and showing
816		# users that aren't actually developers...
817
818		if( $t_total > 0 ) {
819			echo '<tr>';
820			$t_row_count++;
821			echo '<td>';
822			echo summary_helper_get_developer_label( $t_handler_id, $p_filter );
823			echo "</td>\n";
824
825			# We need to track the percentage of bugs that are considered fixed, as well as
826			# those that aren't considered bugs to begin with (when looking at %age)
827			$t_bugs_fixed = 0;
828			$t_bugs_notbugs = 0;
829			for( $j = 0;$j < $t_enum_res_count;$j++ ) {
830				$t_res_bug_count = 0;
831
832				if( isset( $t_arr2[$c_res_s[$j]] ) ) {
833					$t_res_bug_count = $t_arr2[$c_res_s[$j]];
834				}
835
836				echo '<td class="align-right">';
837				if( 0 < $t_res_bug_count ) {
838					$t_bug_link = $t_link_prefix .
839						'&amp;' . FILTER_PROPERTY_HANDLER_ID . '=' . $t_handler_id .
840						'&amp;' . FILTER_PROPERTY_RESOLUTION . '=' . $c_res_s[$j] .
841						'&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . META_FILTER_NONE;
842					echo '<a class="subtle" href="' . $t_bug_link . '">' . $t_res_bug_count . '</a>';
843				} else {
844					echo $t_res_bug_count;
845				}
846				echo "</td>\n";
847
848				if( $c_res_s[$j] >= $t_threshold_fixed ) {
849					if( $c_res_s[$j] < $t_threshold_notfixed ) {
850						# Count bugs with a resolution between fixed and not fixed thresholds
851						$t_bugs_fixed += $t_res_bug_count;
852					} else {
853						# Count bugs with a resolution above the not fixed threshold
854						$t_bugs_notbugs += $t_res_bug_count;
855					}
856				}
857
858			}
859
860			# Display Total
861			echo '<td class="align-right">';
862			$t_bug_link =  $t_link_prefix .
863				'&amp;' . FILTER_PROPERTY_HANDLER_ID . '=' . $t_handler_id .
864				'&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . META_FILTER_NONE;
865			echo '<a class="subtle" href="' . $t_bug_link . '">' . $t_total . '</a>';
866			echo "</td>\n";
867
868			# Percentage
869			$t_percent_fixed = 0;
870			if( ( $t_total - $t_bugs_notbugs ) > 0 ) {
871				$t_percent_fixed = ( $t_bugs_fixed / ( $t_arr2['total'] - $t_bugs_notbugs ) );
872			}
873			echo '<td class="align-right">';
874			printf( '% 1.0f%%', ( $t_percent_fixed * 100 ) );
875			echo "</td>\n";
876			echo '</tr>';
877		}
878	}
879}
880
881/**
882 * Print reporter / resolution report.
883 * A filter can be used to limit the visibility.
884 *
885 * @param string $p_resolution_enum_string Resolution enumeration string value.
886 * @param array $p_filter Filter array.
887 * @return void
888 */
889function summary_print_reporter_resolution( $p_resolution_enum_string, array $p_filter = null ) {
890	$t_reporter_summary_limit = config_get( 'reporter_summary_limit' );
891
892	$t_project_id = helper_get_current_project();
893
894	# Get the resolution values to use
895	$c_res_s = MantisEnum::getValues( $p_resolution_enum_string );
896	$t_enum_res_count = count( $c_res_s );
897
898	# Checking if it's a per project statistic or all projects
899	$t_specific_where = helper_project_specific_where( $t_project_id );
900	if( ' 1<>1' == $t_specific_where ) {
901		return;
902	}
903
904	# Get all of the bugs and split them up into an array
905
906	$t_query = new DBQuery();
907	$t_sql = 'SELECT COUNT(id) as bugcount, reporter_id, resolution'
908		. ' FROM {bug} WHERE ' . $t_specific_where;
909	if( !empty( $p_filter ) ) {
910		$t_subquery = filter_cache_subquery( $p_filter );
911		$t_sql .= ' AND {bug}.id IN :filter';
912		$t_query->bind( 'filter', $t_subquery );
913	}
914	$t_sql .= ' GROUP BY reporter_id, resolution';
915	$t_query->sql( $t_sql );
916
917	$t_reporter_res_arr = array();
918	$t_reporter_bugcount_arr = array();
919	$t_arr = $t_query->fetch();
920	while( $t_arr ) {
921		if( !isset( $t_reporter_res_arr[$t_arr['reporter_id']] ) ) {
922			$t_reporter_res_arr[$t_arr['reporter_id']] = array();
923			$t_reporter_bugcount_arr[$t_arr['reporter_id']] = 0;
924		}
925		if( !isset( $t_reporter_res_arr[$t_arr['reporter_id']][$t_arr['resolution']] ) ) {
926			$t_reporter_res_arr[$t_arr['reporter_id']][$t_arr['resolution']] = 0;
927		}
928		$t_reporter_res_arr[$t_arr['reporter_id']][$t_arr['resolution']] += $t_arr['bugcount'];
929		$t_reporter_bugcount_arr[$t_arr['reporter_id']] += $t_arr['bugcount'];
930
931		$t_arr = $t_query->fetch();
932	}
933
934	# Sort our total bug count array so that the reporters with the highest number of bugs are listed first,
935	arsort( $t_reporter_bugcount_arr );
936
937	$t_threshold_fixed = config_get( 'bug_resolution_fixed_threshold' );
938	$t_threshold_notfixed = config_get( 'bug_resolution_not_fixed_threshold' );
939
940	$t_link_prefix = summary_get_link_prefix( $p_filter );
941
942	$t_row_count = 0;
943
944	# We now have a multi dimensional array of users and resolutions, with the value of each resolution for each user
945	foreach( $t_reporter_bugcount_arr as $t_reporter_id => $t_total_user_bugs ) {
946
947		# Limit the number of reporters listed
948		if( $t_row_count >= $t_reporter_summary_limit ) {
949			break;
950		}
951
952		# Only print reporters who have reported at least one bug. This helps
953		# prevent divide by zeroes, showing reporters not on this project, and showing
954		# users that aren't actually reporters...
955		if( $t_total_user_bugs > 0 ) {
956			$t_arr2 = $t_reporter_res_arr[$t_reporter_id];
957
958			echo '<tr>';
959			$t_row_count++;
960			echo '<td>';
961			echo string_display_line( user_get_name( $t_reporter_id ) );
962			echo "</td>\n";
963
964			# We need to track the percentage of bugs that are considered fix, as well as
965			# those that aren't considered bugs to begin with (when looking at %age)
966			$t_bugs_fixed = 0;
967			$t_bugs_notbugs = 0;
968			for( $j = 0;$j < $t_enum_res_count;$j++ ) {
969				$t_res_bug_count = 0;
970
971				if( isset( $t_arr2[$c_res_s[$j]] ) ) {
972					$t_res_bug_count = $t_arr2[$c_res_s[$j]];
973				}
974
975				echo '<td class="align-right">';
976				if( 0 < $t_res_bug_count ) {
977					$t_bug_link = $t_link_prefix .
978						'&amp;' . FILTER_PROPERTY_REPORTER_ID . '=' . $t_reporter_id .
979						'&amp;' . FILTER_PROPERTY_RESOLUTION . '=' . $c_res_s[$j] .
980						'&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . META_FILTER_NONE;
981					echo '<a class="subtle" href="' . $t_bug_link . '">' . $t_res_bug_count . '</a>';
982				} else {
983					echo $t_res_bug_count;
984				}
985				echo "</td>\n";
986
987				if( $c_res_s[$j] >= $t_threshold_fixed ) {
988					if( $c_res_s[$j] < $t_threshold_notfixed ) {
989						# Count bugs with a resolution between fixed and not fixed thresholds
990						$t_bugs_fixed += $t_res_bug_count;
991					} else {
992						# Count bugs with a resolution above the not fixed threshold
993						$t_bugs_notbugs += $t_res_bug_count;
994					}
995				}
996
997			}
998
999			# Display Total
1000			echo '<td class="align-right">';
1001			$t_bug_link =  $t_link_prefix .
1002				'&amp;' . FILTER_PROPERTY_REPORTER_ID . '=' . $t_reporter_id .
1003				'&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . META_FILTER_NONE;
1004			echo '<a class="subtle" href="' . $t_bug_link . '">' . $t_total_user_bugs . '</a>';
1005			echo "</td>\n";
1006
1007			# Percentage
1008			$t_percent_errors = 0;
1009			if( $t_total_user_bugs > 0 ) {
1010				$t_percent_errors = ( $t_bugs_notbugs / $t_total_user_bugs );
1011			}
1012			echo '<td class="align-right">';
1013			printf( '% 1.0f%%', ( $t_percent_errors * 100 ) );
1014			echo "</td>\n";
1015			echo '</tr>';
1016		}
1017	}
1018}
1019
1020/**
1021 * Print reporter effectiveness report.
1022 * A filter can be used to limit the visibility.
1023 *
1024 * @param string $p_severity_enum_string   Severity enumeration string.
1025 * @param string $p_resolution_enum_string Resolution enumeration string.
1026 * @param array $p_filter Filter array.
1027 * @return void
1028 */
1029function summary_print_reporter_effectiveness( $p_severity_enum_string, $p_resolution_enum_string, array $p_filter = null ) {
1030	$t_reporter_summary_limit = config_get( 'reporter_summary_limit' );
1031
1032	$t_project_id = helper_get_current_project();
1033
1034	$t_severity_multipliers = config_get( 'severity_multipliers' );
1035	$t_resolution_multipliers = config_get( 'resolution_multipliers' );
1036
1037	# Get the severity values to use
1038	$c_sev_s = MantisEnum::getValues( $p_severity_enum_string );
1039	$t_enum_sev_count = count( $c_sev_s );
1040
1041	# Get the resolution values to use
1042	$c_res_s = MantisEnum::getValues( $p_resolution_enum_string );
1043
1044	# Checking if it's a per project statistic or all projects
1045	$t_specific_where = helper_project_specific_where( $t_project_id );
1046	if( ' 1<>1' == $t_specific_where ) {
1047		return;
1048	}
1049
1050	# Get all of the bugs and split them up into an array
1051	$t_query = new DBQuery();
1052	$t_sql = 'SELECT COUNT(id) as bugcount, reporter_id, resolution, severity'
1053		. ' FROM {bug} WHERE ' . $t_specific_where;
1054	if( !empty( $p_filter ) ) {
1055		$t_subquery = filter_cache_subquery( $p_filter );
1056		$t_sql .= ' AND {bug}.id IN :filter';
1057		$t_query->bind( 'filter', $t_subquery );
1058	}
1059	$t_sql .= ' GROUP BY reporter_id, resolution, severity';
1060	$t_query->sql( $t_sql );
1061
1062	$t_reporter_ressev_arr = array();
1063	$t_reporter_bugcount_arr = array();
1064	$t_arr = $t_query->fetch();
1065	while( $t_arr ) {
1066		if( !isset( $t_reporter_ressev_arr[$t_arr['reporter_id']] ) ) {
1067			$t_reporter_ressev_arr[$t_arr['reporter_id']] = array();
1068			$t_reporter_bugcount_arr[$t_arr['reporter_id']] = 0;
1069		}
1070		if( !isset( $t_reporter_ressev_arr[$t_arr['reporter_id']][$t_arr['severity']] ) ) {
1071			$t_reporter_ressev_arr[$t_arr['reporter_id']][$t_arr['severity']] = array();
1072			$t_reporter_ressev_arr[$t_arr['reporter_id']][$t_arr['severity']]['total'] = 0;
1073		}
1074		if( !isset( $t_reporter_ressev_arr[$t_arr['reporter_id']][$t_arr['severity']][$t_arr['resolution']] ) ) {
1075			$t_reporter_ressev_arr[$t_arr['reporter_id']][$t_arr['severity']][$t_arr['resolution']] = 0;
1076		}
1077		$t_reporter_ressev_arr[$t_arr['reporter_id']][$t_arr['severity']][$t_arr['resolution']] += $t_arr['bugcount'];
1078		$t_reporter_ressev_arr[$t_arr['reporter_id']][$t_arr['severity']]['total'] += $t_arr['bugcount'];
1079		$t_reporter_bugcount_arr[$t_arr['reporter_id']] += $t_arr['bugcount'];
1080
1081		$t_arr = $t_query->fetch();
1082	}
1083
1084	# Sort our total bug count array so that the reporters with the highest number of bugs are listed first,
1085	arsort( $t_reporter_bugcount_arr );
1086
1087	$t_row_count = 0;
1088
1089	# We now have a multi dimensional array of users, resolutions and severities, with the
1090	# value of each resolution and severity for each user
1091	foreach( $t_reporter_bugcount_arr as $t_reporter_id => $t_total_user_bugs ) {
1092
1093		# Limit the number of reporters listed
1094		if( $t_row_count >= $t_reporter_summary_limit ) {
1095			break;
1096		}
1097
1098		# Only print reporters who have reported at least one bug. This helps
1099		# prevent divide by zeroes, showing reporters not on this project, and showing
1100		# users that aren't actually reporters...
1101		if( $t_total_user_bugs > 0 ) {
1102			$t_arr2 = $t_reporter_ressev_arr[$t_reporter_id];
1103
1104			echo '<tr>';
1105			$t_row_count++;
1106			echo '<td>';
1107			echo string_display_line( user_get_name( $t_reporter_id ) );
1108			echo '</td>';
1109
1110			$t_total_severity = 0;
1111			$t_total_errors = 0;
1112			for( $j = 0; $j < $t_enum_sev_count; $j++ ) {
1113				if( !isset( $t_arr2[$c_sev_s[$j]] ) ) {
1114					continue;
1115				}
1116
1117				$t_sev_bug_count = $t_arr2[$c_sev_s[$j]]['total'];
1118				$t_sev_mult = 1;
1119				if( isset( $t_severity_multipliers[$c_sev_s[$j]] ) ) {
1120					$t_sev_mult = $t_severity_multipliers[$c_sev_s[$j]];
1121				}
1122
1123				if( $t_sev_bug_count > 0 ) {
1124					$t_total_severity += ( $t_sev_bug_count * $t_sev_mult );
1125				}
1126
1127				foreach( $t_resolution_multipliers as $t_res => $t_res_mult ) {
1128					if( isset( $t_arr2[$c_sev_s[$j]][$t_res] ) ) {
1129						$t_total_errors += ( $t_sev_mult * $t_res_mult );
1130					}
1131				}
1132			}
1133			echo '<td class="align-right">' . $t_total_severity . '</td>';
1134			echo '<td class="align-right">' . $t_total_errors . '</td>';
1135			printf( '<td class="align-right">%d</td>', $t_total_severity - $t_total_errors );
1136			echo '</tr>';
1137		}
1138	}
1139}
1140
1141/**
1142 * Calculate time stats for resolved issues.
1143 * A filter can be used to limit the visibility.
1144 *
1145 * @param integer $p_project_id.
1146 * @param array $p_filter Filter array.
1147 * @return array
1148 */
1149function summary_helper_get_time_stats( $p_project_id, array $p_filter = null ) {
1150	$t_specific_where = helper_project_specific_where( $p_project_id );
1151	$t_resolved = config_get( 'bug_resolved_status_threshold' );
1152
1153	# The issue may have passed through the status we consider resolved
1154	# (e.g. bug is CLOSED, not RESOLVED). The linkage to the history field
1155	# will look up the most recent 'resolved' status change and return it as well
1156
1157	$t_stats = array(
1158		'bug_id'       => 0,
1159		'largest_diff' => 0,
1160		'total_time'   => 0,
1161		'average_time' => 0,
1162		);
1163
1164	$t_sql_inner = ' FROM {bug} b LEFT JOIN {bug_history} h'
1165		. ' ON b.id = h.bug_id  AND h.type = :hist_type'
1166		. ' AND h.field_name = :hist_field AND b.date_submitted <= h.date_modified'
1167		. ' WHERE b.status >= :int_resolved'
1168		. ' AND h.new_value >= :str_resolved AND h.old_value < :str_resolved'
1169		. ' AND ' . $t_specific_where;
1170	$t_params = array(
1171		'hist_type' => 0,
1172		'hist_field' => 'status',
1173		'int_resolved' => (int)$t_resolved,
1174		'str_resolved' => (string)$t_resolved
1175		);
1176	if( !empty( $p_filter ) ) {
1177		$t_subquery = filter_cache_subquery( $p_filter );
1178		$t_sql_inner .= ' AND b.id IN :filter';
1179		$t_params['filter'] = $t_subquery;
1180	}
1181
1182	if( db_has_capability( DB_CAPABILITY_WINDOW_FUNCTIONS ) ) {
1183		if(db_is_mssql() ) {
1184			# sqlserver by default uses the column datatype, which is INT. This datatype can be overflowed
1185			# when a big number of issues are included, since we are adding the total number of seconds.
1186			$t_diff_expr = 'CAST(diff AS BIGINT)';
1187		} else {
1188			$t_diff_expr = 'diff';
1189		}
1190		$t_sql = 'SELECT id, diff, SUM(' . $t_diff_expr . ') OVER () AS total_time, AVG(' . $t_diff_expr . ') OVER () AS avg_time'
1191			. ' FROM ( SELECT b.id, MAX(h.date_modified) - b.date_submitted AS diff'
1192			. $t_sql_inner
1193			. ' GROUP BY b.id,b.date_submitted ) subquery'
1194			. ' ORDER BY diff DESC';
1195		$t_query = new DbQuery( $t_sql, $t_params );
1196		$t_query->set_limit(1);
1197		if( $t_row = $t_query->fetch() ) {
1198			$t_stats = array(
1199				'bug_id'       => $t_row['id'],
1200				'largest_diff' => number_format( (int)$t_row['diff'] / SECONDS_PER_DAY, 2 ),
1201				'total_time'   => number_format( (int)$t_row['total_time'] / SECONDS_PER_DAY, 2 ),
1202				'average_time' => number_format( (int)$t_row['avg_time'] / SECONDS_PER_DAY, 2 ),
1203				);
1204		}
1205	} else {
1206		$t_sql = 'SELECT b.id, b.date_submitted, b.last_updated, MAX(h.date_modified) AS hist_update, b.status'
1207			. $t_sql_inner
1208			. ' GROUP BY b.id, b.status, b.date_submitted, b.last_updated ORDER BY b.id ASC';
1209		$t_query = new DbQuery( $t_sql, $t_params );
1210
1211		$t_bug_count = 0;
1212		$t_largest_diff = 0;
1213		$t_total_time = 0;
1214		while( $t_row = $t_query->fetch() ) {
1215			$t_bug_count++;
1216			$t_date_submitted = $t_row['date_submitted'];
1217			$t_last_updated = $t_row['hist_update'] !== null ? $t_row['hist_update'] : $t_row['last_updated'];
1218
1219			if( $t_last_updated < $t_date_submitted ) {
1220				$t_last_updated = 0;
1221				$t_date_submitted = 0;
1222			}
1223
1224			$t_diff = $t_last_updated - $t_date_submitted;
1225			$t_total_time += $t_diff;
1226			if( $t_diff > $t_largest_diff ) {
1227				$t_largest_diff = $t_diff;
1228				$t_bug_id = $t_row['id'];
1229			}
1230		}
1231
1232		if( $t_bug_count > 0 ) {
1233			$t_average_time = $t_total_time / $t_bug_count;
1234		} else {
1235			$t_average_time = 0;
1236			$t_bug_id = 0;
1237		}
1238
1239		$t_stats = array(
1240			'bug_id'       => $t_bug_id,
1241			'largest_diff' => number_format( $t_largest_diff / SECONDS_PER_DAY, 2 ),
1242			'total_time'   => number_format( $t_total_time / SECONDS_PER_DAY, 2 ),
1243			'average_time' => number_format( $t_average_time / SECONDS_PER_DAY, 2 ),
1244		);
1245	}
1246	return $t_stats;
1247}
1248
1249/**
1250 * Returns a filter to be used in summary pages.
1251 * A temporary filter is retrieved if a valid temporary filter key is submitted
1252 * to the page as request parameter "filter".
1253 * If no filter key was provided, returns a generic filter that shows all
1254 * accesible issues by the user.
1255 *
1256 * @return array	Filter array
1257 */
1258function summary_get_filter() {
1259	$t_filter = null;
1260	$f_tmp_key = gpc_get_string( 'filter', null );
1261	if( null !== $f_tmp_key ) {
1262		$t_filter = filter_temporary_get( $f_tmp_key, null );
1263	}
1264	# if filter parameter doesn't exist or can't be loaded, return a default filter
1265	if( null === $t_filter ) {
1266			# TODO: for summary, as default, we want to show all status.
1267			# Until a better implementation for default/empty filters, we need to adjust here
1268			$t_filter = filter_get_default();
1269			$t_filter[FILTER_PROPERTY_HIDE_STATUS] = array( META_FILTER_NONE );
1270			$t_filter['_view_type'] = FILTER_VIEW_TYPE_SIMPLE;
1271	}
1272	return $t_filter;
1273}
1274
1275/**
1276 * Print filter related information for summary page.
1277 * If a filter has been applied, display a notice, bug count, link to view issues.
1278 * @param array $p_filter Filter array.
1279 * @return void
1280 */
1281function summary_print_filter_info( array $p_filter = null ) {
1282	if( null === $p_filter ) {
1283		return;
1284	}
1285	# If filter is temporary, then it has been provided explicitly.
1286	# When no filter is specified for summary page, we receive a defaulted filter
1287	# which don't have any specific id.
1288	if( !filter_is_temporary( $p_filter ) ) {
1289		return;
1290	}
1291	$t_filter_query = filter_cache_subquery( $p_filter );
1292	$t_bug_count = $t_filter_query->get_bug_count();
1293	$t_view_issues_link = helper_url_combine( 'view_all_bug_page.php', filter_get_temporary_key_param( $p_filter ) );
1294	?>
1295	<div class="space-10"></div>
1296	<div class="col-md-12 col-xs-12">
1297		<div class="alert alert-warning center">
1298		<?php
1299		echo '<a href="', $t_view_issues_link, '" title="', lang_get( 'view_bugs_link' ), '">';
1300		echo lang_get( 'summary_notice_filter_is_applied' ), '&nbsp;',
1301			'(', $t_bug_count, ' ', lang_get( 'bugs' ) , ')';
1302		echo '</a>';
1303		?>
1304		</div>
1305	</div>
1306	<?php
1307}
1308
1309/**
1310 * Calculate the number of "open" and "resolve" issues actions in the last X days.
1311 * This includes each and successive resolution transitions.
1312 * A filter can be used to limit the visibility.
1313 *
1314 * @param array $p_date_array   An array of integers representing days is passed in.
1315 * @param array $p_filter       Filter array.
1316 * @return array	Accumulated count for each day range.
1317 */
1318function summary_by_dates_bug_count( array $p_date_array, array $p_filter = null ) {
1319	$t_project_id = helper_get_current_project();
1320	$t_specific_where = helper_project_specific_where( $t_project_id );
1321	$t_resolved = config_get( 'bug_resolved_status_threshold' );
1322	$t_date_array = array_values( $p_date_array );
1323	sort( $t_date_array );
1324
1325	$t_query = new DBQuery();
1326	$t_now = db_now();
1327	$t_prev_days = 0;
1328	$t_sql_ranges = 'CASE';
1329	foreach( $t_date_array as $t_ix => $t_days ) {
1330		$c_days = (int)$t_days;
1331		$t_range_start = $t_now - $c_days * SECONDS_PER_DAY + 1;
1332		$t_range_end = $t_now - $t_prev_days * SECONDS_PER_DAY;
1333		$t_sql_ranges .= ' WHEN date_modified'
1334				. ' BETWEEN ' . $t_query->param( $t_range_start )
1335				. ' AND ' . $t_query->param( $t_range_end )
1336				. ' THEN ' . $t_ix;
1337		$t_prev_days = $c_days;
1338	}
1339	$t_sql_ranges .= ' ELSE -1 END';
1340
1341	$t_sql_inner = 'SELECT CASE WHEN h.type = :hist_type_new THEN :action_open'
1342		. ' WHEN h.type = :hist_type_upd AND h.old_value >= :status AND h.new_value  < :status THEN :action_open'
1343		. ' WHEN h.type = :hist_type_upd AND h.old_value < :status AND h.new_value  >= :status THEN :action_close'
1344		. ' ELSE null END AS action, date_modified'
1345        . ' FROM {bug_history} h JOIN {bug} b ON b.id = h.bug_id'
1346		. ' WHERE h.date_modified > :mint_ime'
1347		. ' AND ( h.type = :hist_type_new OR h.type = :hist_type_upd AND h.field_name = :hist_field )'
1348		. ' AND ' . $t_specific_where;
1349	$t_query->bind( array (
1350		'hist_type_upd' => NORMAL_TYPE,
1351		'hist_type_new' => NEW_BUG,
1352		'hist_field' => 'status',
1353		'status' => (string)$t_resolved,
1354		'action_open' => 'O',
1355		'action_close' => 'C',
1356		'mint_ime' => $t_now - $t_prev_days * SECONDS_PER_DAY
1357		) );
1358
1359	if( !empty( $p_filter ) ) {
1360		$t_subquery = filter_cache_subquery( $p_filter );
1361		$t_sql_inner .= ' AND b.id IN :filter';
1362		$t_query->bind( 'filter', $t_subquery );
1363	}
1364
1365	$t_sql = 'SELECT action, date_range, COUNT(*) AS range_count FROM'
1366		. ' ( SELECT action, ' . $t_sql_ranges . ' AS date_range'
1367		. ' FROM (' . $t_sql_inner . ') sub_actions'
1368		. ' WHERE action IS NOT NULL ) sub_count'
1369		. ' GROUP BY action, date_range ORDER BY date_range, action';
1370	$t_query->sql( $t_sql );
1371
1372	# initialize the result array to 0
1373	$t_count_array = array();
1374	foreach( $t_date_array as $t_ix => $t_value ) {
1375		$t_count_array['open'][$t_ix] = 0;
1376		$t_count_array['close'][$t_ix] = 0;
1377	}
1378
1379	# The query returns the count specific to each date range (some ranges may
1380	# not exist in the query result if the count is 0).
1381	# Fill the array with those values first.
1382	while( $t_row = $t_query->fetch() ) {
1383		$t_index = (int)$t_row['date_range'];
1384		if( $t_index >= 0 ) {
1385			switch( $t_row['action'] ) {
1386				case 'O':
1387					$t_count_array['open'][$t_index] = $t_row['range_count'];
1388					break;
1389				case 'C':
1390					$t_count_array['close'][$t_index] = $t_row['range_count'];
1391			}
1392		}
1393	}
1394
1395	# This function returns the accumulated count. Process the array to add
1396	# each successive date range
1397	$t_count_open = 0;
1398	$t_count_closed = 0;
1399	foreach( $t_date_array as $t_ix => $t_value ) {
1400		$t_count_open += $t_count_array['open'][$t_ix];
1401		$t_count_array['open'][$t_ix] = $t_count_open;
1402		$t_count_closed += $t_count_array['close'][$t_ix];
1403		$t_count_array['close'][$t_ix] = $t_count_closed;
1404	}
1405	return $t_count_array;
1406}
1407
1408/**
1409 * This function shows the number of "open" and "resolve" issues actions in the
1410 * last X days. This includes each issue submission, and it's succesive resolve
1411 * and reopen transitions.
1412 * A filter can be used to limit the visibility.
1413 *
1414 * @param array $p_date_array An array of integers representing days is passed in.
1415 * @param array $p_filter Filter array.
1416 * @return void
1417 */
1418function summary_print_by_date( array $p_date_array, array $p_filter = null ) {
1419	# clean and sort dates array
1420	$t_date_array = array_values( $p_date_array );
1421	sort( $t_date_array );
1422
1423	$t_by_dates_count = summary_by_dates_bug_count( $t_date_array, $p_filter );
1424	$t_open_count_array = $t_by_dates_count['open'];
1425	$t_resolved_count_array = $t_by_dates_count['close'];
1426
1427	foreach( $t_date_array as $t_ix => $t_days ) {
1428		$t_new_count = $t_open_count_array[$t_ix];
1429		$t_resolved_count = $t_resolved_count_array[$t_ix];
1430		$t_start_date = mktime( 0, 0, 0, date( 'm' ), ( date( 'd' ) - $t_days ), date( 'Y' ) );
1431		$t_end_date = mktime( 0, 0, 0 ) + SECONDS_PER_DAY;
1432
1433		$t_link_prefix = summary_get_link_prefix( $p_filter );
1434
1435		# if we come from a filter, don't clear status properties
1436		if( !filter_is_temporary( $p_filter ) ) {
1437			$t_status_prop = '&amp;' . FILTER_PROPERTY_HIDE_STATUS . '=' . META_FILTER_NONE;
1438		} else {
1439			$t_status_prop = '';
1440		}
1441		$t_new_bugs_link = $t_link_prefix
1442				. '&amp;' . FILTER_PROPERTY_FILTER_BY_DATE_SUBMITTED . '=' . ON
1443				. '&amp;' . FILTER_PROPERTY_DATE_SUBMITTED_START_YEAR . '=' . date( 'Y', $t_start_date )
1444				. '&amp;' . FILTER_PROPERTY_DATE_SUBMITTED_START_MONTH . '=' . date( 'm', $t_start_date )
1445				. '&amp;' . FILTER_PROPERTY_DATE_SUBMITTED_START_DAY . '=' . date( 'd', $t_start_date )
1446				. '&amp;' . FILTER_PROPERTY_DATE_SUBMITTED_END_YEAR . '=' . date( 'Y', $t_end_date )
1447				. '&amp;' . FILTER_PROPERTY_DATE_SUBMITTED_END_MONTH . '=' . date( 'm', $t_end_date )
1448				. '&amp;' . FILTER_PROPERTY_DATE_SUBMITTED_END_DAY . '=' . date( 'd', $t_end_date )
1449				. $t_status_prop . '">';
1450		echo '<tr>' . "\n";
1451		echo '    <td class="width50">' . $t_days . '</td>' . "\n";
1452
1453		if( $t_new_count > 0 ) {
1454			echo '    <td class="align-right"><a class="subtle" href="' . $t_new_bugs_link . $t_new_count . '</a></td>' . "\n";
1455		} else {
1456			echo '    <td class="align-right">' . $t_new_count . '</td>' . "\n";
1457		}
1458		echo '    <td class="align-right">' . $t_resolved_count . '</td>' . "\n";
1459
1460		$t_balance = $t_new_count - $t_resolved_count;
1461		$t_style = '';
1462		if( $t_balance > 0 ) {
1463
1464			# we are talking about bugs: a balance > 0 is "negative" for the project...
1465			$t_style = ' red';
1466			$t_balance = sprintf( '%+d', $t_balance );
1467
1468			# "+" modifier added in PHP >= 4.3.0
1469		} else if( $t_balance < 0 ) {
1470			$t_style = ' green';
1471			$t_balance = sprintf( '%+d', $t_balance );
1472		}
1473
1474		echo '    <td class="align-right' . $t_style . '">' . $t_balance . "</td>\n";
1475		echo '</tr>' . "\n";
1476	}
1477}
1478
1479function summary_get_link_prefix( array $p_filter = null ) {
1480	$t_filter_action = filter_is_temporary( $p_filter ) ? FILTER_ACTION_PARSE_ADD : FILTER_ACTION_PARSE_NEW;
1481	$t_link_prefix = 'view_all_set.php?type=' . $t_filter_action . '&temporary=y&new=1';
1482	$t_link_prefix = helper_url_combine( $t_link_prefix, filter_get_temporary_key_param( $p_filter ) );
1483	return $t_link_prefix;
1484}
1485