Update class_sort.php
[bugdar.git] / includes / class_sort.php
1 <?php
2 /*=====================================================================*\
3 || ###################################################################
4 || # Bugdar
5 || # Copyright ©2002-2007 Blue Static
6 || #
7 || # This program is free software; you can redistribute it and/or modify
8 || # it under the terms of the GNU General Public License as published by
9 || # the Free Software Foundation; version 2 of the License.
10 || #
11 || # This program is distributed in the hope that it will be useful, but
12 || # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 || # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 || # more details.
15 || #
16 || # You should have received a copy of the GNU General Public License along
17 || # with this program; if not, write to the Free Software Foundation, Inc.,
18 || # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
19 || ###################################################################
20 \*=====================================================================*/
21
22 /**
23 * Bug Listing Sorter
24 *
25 * This class is used to sort bugs based on user-sent options and variables.
26 *
27 * @author Blue Static
28 * @copyright Copyright ©2002 - 2007, Blue Static
29 * @version $Revision$
30 * @package Bugdar
31 *
32 */
33 class ListSorter
34 {
35 /**
36 * Bugsys registry
37 * @var object
38 */
39 private $db;
40
41 /**
42 * Page name
43 * @var string
44 */
45 public $page = '';
46
47 /**
48 * Current sort key
49 * @var string
50 */
51 public $sortkey = '';
52
53 /**
54 * Current sort direction
55 * @var string
56 */
57 private $direction = '';
58
59 /**
60 * Column array for table heads
61 * @var array
62 */
63 private $columns;
64
65 /**
66 * Constructor: set the page name
67 *
68 * @param string File name without the .php extension
69 */
70 public function __construct($page)
71 {
72 $this->db = BSApp::$db;
73 $this->page = $page;
74 $this->_processIncoming();
75 }
76
77 /**
78 * Processes the incoming variables and then sets all the sort order
79 * information appropriately
80 */
81 private function _processIncoming()
82 {
83 $this->sortkey = BSApp::$input->in['by'];
84 if (!self::fetch_by_text(BSApp::$input->in['by']))
85 {
86 $this->sortkey = (isset(bugdar::$userinfo['defaultsortkey']) ? bugdar::$userinfo['defaultsortkey'] : bugdar::$options['defaultsortkey']);
87 }
88
89 $this->direction = BSApp::$input->in['as'];
90 if (!in_array($this->direction, array('asc', 'desc')))
91 {
92 $this->direction = (isset(bugdar::$userinfo['defaultsortas']) ? bugdar::$userinfo['defaultsortas'] : bugdar::$options['defaultsortas']);
93 }
94 }
95
96 /**
97 * Fetch a SQL query to gather bugs with the sort filters applied
98 *
99 * @param string Additional WHERE clauses in an array
100 * @param string A LIMIT clause
101 *
102 * @return string Compiled SQL query
103 */
104 public function fetchSqlQuery($where = null, $limit = null)
105 {
106 // this WHERE clause is used for all the queries
107 $basewhere = "bug.product IN (" . fetch_on_bits('canviewbugs') . ")
108 AND (!bug.hidden OR (bug.hidden AND bug.product IN (" . fetch_on_bits('canviewhidden') . "))" . (can_perform('canviewownhidden') ? " OR (bug.hidden AND bug.userid = " . bugdar::$userinfo['userid'] . " AND bug.product IN (" . fetch_on_bits('canviewownhidden') . "))" : "") . ")" .
109 ((bugdar::$options['hidestatuses'] || isset(bugdar::$userinfo['hidestatuses'])) ? "
110 AND bug.status NOT IN (" . (bugdar::$userinfo['hidestatuses'] != '' ? bugdar::$userinfo['hidestatuses'] : bugdar::$options['hidestatuses']) . ")" : "");
111
112 // remap the sort keys to be actual SQL fields
113 $querykeys = array(
114 'bugid' => 'bugid',
115 'summary' => 'summary',
116 'reporter' => 'userid',
117 'lastpost' => (can_perform('canviewhidden') ? "lastposttime" : "hiddenlastposttime"),
118 'assignedto'=> 'assignedto'
119 );
120
121 switch ($this->sortkey)
122 {
123 case 'bugid':
124 case 'summary':
125 case 'reporter':
126 case 'lastpost':
127 case 'assignedto':
128 $query = "
129 SELECT bug.*, vote.votefor, vote.voteagainst FROM " . TABLE_PREFIX . "bug AS bug
130 LEFT JOIN " . TABLE_PREFIX . "vote AS vote
131 ON (bug.bugid = vote.bugid)
132 WHERE $basewhere" .
133 (is_array($where) ? "
134 AND " . implode("\nAND ", $where) : "") . "
135 ORDER BY " . $querykeys[ $this->sortkey ] . " " . strtoupper($this->direction) . ($this->sortkey != 'lastpost' ? ", " . $querykeys['lastpost'] . " " . strtoupper($this->direction) : "") . ($limit ? "
136 LIMIT $limit" : "");
137 break;
138 case 'product':
139 case 'component':
140 case 'version':
141 case 'status':
142 case 'resolution':
143 case 'priority':
144 case 'severity':
145 $key = ($this->sortkey != 'component' ? $this->sortkey : 'product');
146 $query = "
147 SELECT $key.*, bug.*, vote.votefor, vote.voteagainst FROM " . TABLE_PREFIX . "$key AS $key
148 RIGHT JOIN " . TABLE_PREFIX . "bug AS bug
149 ON (bug.$key = $key.{$key}id)
150 LEFT JOIN " . TABLE_PREFIX . "vote AS vote
151 ON (bug.bugid = vote.bugid)
152 WHERE $basewhere" .
153 (is_array($where) ? "
154 AND " . implode("\nAND ", $where) : "") . "
155 ORDER BY $key.displayorder " . strtoupper($this->direction) . ", bug.$querykeys[lastpost] " . strtoupper($this->direction) . ($limit ? "
156 LIMIT $limit" : "");
157 break;
158 case 'votes':
159 $query = "
160 SELECT bug.*, vote.votefor, vote.voteagainst FROM " . TABLE_PREFIX . "bug AS bug
161 LEFT JOIN " . TABLE_PREFIX . "vote AS vote
162 ON (bug.bugid = vote.bugid)
163 WHERE $basewhere" .
164 (is_array($where) ? "
165 AND " . implode("\nAND ", $where) : "") . "
166 ORDER BY vote.votefor " . strtoupper($this->direction) . ", vote.voteagainst " . strtoupper($this->fetchOppositeSortDirection()) . ", bug.$querykeys[lastpost] " . strtoupper($this->direction) . ($limit ? "
167 LIMIT $limit" : "");
168 break;
169 default:
170 if (substr($this->sortkey, 0, 6) != 'custom')
171 {
172 return;
173 }
174
175 $query = "
176 SELECT bug.*, vote.votefor, vote.voteagainst FROM " . TABLE_PREFIX . "bug AS bug
177 LEFT JOIN " . TABLE_PREFIX . "vote AS vote
178 ON (bug.bugid = vote.bugid)
179 WHERE $basewhere" .
180 (is_array($where) ? "
181 AND " . implode("\nAND ", $where) : "") . "
182 ORDER BY {$this->sortkey} " . strtoupper($this->direction) . ", " . $querykeys['lastpost'] . " " . strtoupper($this->direction) . ($limit ? "
183 LIMIT $limit" : "");
184 }
185
186 return $query;
187 }
188
189 /**
190 * Returns the display text for a given sort order key
191 *
192 * @param string Sort order key, or FALSE for the array
193 * @param bool Permission check the custom fields?
194 *
195 * @return mixed Display text if param is string, or array of all key=>text if param is NULL
196 */
197 public static function fetch_by_text($key, $doPerm = true)
198 {
199 $keys = array(
200 'lastpost' => T('Last Post Time'),
201 'bugid' => T('Bug ID'),
202 'summary' => T('Summary'),
203 'reporter' => T('Reporter'),
204 'product' => T('Product'),
205 'component' => T('Component'),
206 'version' => T('Version'),
207 'status' => T('Status'),
208 'resolution' => T('Resolution'),
209 'priority' => T('Priority'),
210 'severity' => T('Severity'),
211 'votes' => T('Votes'),
212 'assignedto' => T('Assigned To')
213 );
214
215 $fields = self::_fetch_custom_fields($doPerm);
216 foreach ($fields AS $field)
217 {
218 $keys['custom' . $field['fieldid']] = $field['name'];
219 }
220
221 if ($key === false)
222 {
223 return $keys;
224 }
225 else
226 {
227 return $keys["$key"];
228 }
229 }
230
231 /**
232 * Returns the display text for a given sort order direction
233 *
234 * @param string Sort direction, or FALSE for the array
235 *
236 * @return mixed Display text if param is string, or array of all key=>text if param is NULL
237 */
238 public static function fetch_as_text($key)
239 {
240 $keys = array(
241 'desc' => T('Descending'),
242 'asc' => T('Ascending')
243 );
244
245 if ($key === false)
246 {
247 return $keys;
248 }
249 else
250 {
251 return $keys["$key"];
252 }
253 }
254
255 /**
256 * Returns a multi-dimensional array with sort by keys indexing arrays
257 * with 'image' and 'href' keys that store the values from
258 * fetchSortImage() and fetchSortLink(), respectively
259 *
260 * @param string Extra GET parameters to pass to fetchSortLink()
261 *
262 * @return array Array as described above
263 */
264 public function fetchDisplayArray($params = null)
265 {
266 $return = self::fetch_by_text(false);
267
268 foreach ($return as $key => $nil)
269 {
270 $return["$key"] = array('image' => ($this->sortkey == $key ? $this->fetchSortImage() : ''), 'href' => $this->fetchSortLink($key, $params, true));
271 }
272
273 return $return;
274 }
275
276 /**
277 * Returns the entire <img> tag for the sort arrow
278 *
279 * @return string HTML <img> tag
280 */
281 public function fetchSortImage()
282 {
283 return '<img src="templates/images/arrow_' . $this->fetchSortDirection() . '.gif" alt="" style="vertical-align: top; border: none" />';
284 }
285
286 /**
287 * Returns the href value for an <a> tag by generating all the necessary
288 * bits and concat'ing it onto an extra string of GETs (optional)
289 *
290 * @param string Sorting key
291 * @param string Additional GET parameters
292 * @param bool Highlight the current sortkey if that's passed?
293 *
294 * @return string HREF
295 */
296 public function fetchSortLink($key, $params = null, $highlight = false)
297 {
298 if ($params)
299 {
300 $params .= '&amp;';
301 }
302
303 return $this->page . '.php?' . $params . 'by=' . $key . '&amp;as=' . (($this->sortkey == $key && $highlight) ? $this->fetchOppositeSortDirection() . '" class="select' : $this->fetchSortDirection());
304 }
305
306 /**
307 * Returns the OPPOSITE direction to sort when you click on a link
308 *
309 * @return string Either asc or desc
310 */
311 public function fetchOppositeSortDirection()
312 {
313 if ($this->direction == 'asc')
314 {
315 return 'desc';
316 }
317 else
318 {
319 return 'asc';
320 }
321 }
322
323 /**
324 * Returns the current sorted direction for the image path
325 *
326 * @return string Either asc or desc
327 */
328 public function fetchSortDirection()
329 {
330 return $this->direction;
331 }
332
333 /**
334 * Returns the HTML code for bug listing table column headers
335 *
336 * @param bool Include the sort links/image?
337 * @param string Additional GET params to pass to fetchSortLink()
338 *
339 * @return string HTML code
340 */
341 public function constructColumnHeaders($sortable, $params = null)
342 {
343 $this->_processColumns();
344
345 $names = self::fetch_by_text(false);
346
347 $output = '';
348 foreach ($this->columns as $columns)
349 {
350 $build = array();
351 foreach ($columns as $column)
352 {
353 $build[] = ($sortable ? '<a href="' . $this->fetchSortLink($column, $params, true) . '">' . $names[$column] . '</a>' : $names[$column]);
354 }
355 $image = ((in_array($this->sortkey, $columns) && $sortable) ? $this->fetchSortImage() : '');
356 $name = implode(' / ', $build);
357
358 $tpl = new BSTemplate('list_head');
359 $tpl->vars = array(
360 'name' => $name,
361 'image' => $image
362 );
363 $output .= $tpl->evaluate()->getTemplate();
364 }
365
366 return $output;
367 }
368
369 /**
370 * Returns the HTML code for a row of data for the bug listing
371 *
372 * @param array Bug data array
373 * @param string Additional link params
374 *
375 * @return string Row HTML
376 */
377 function constructRow($bug, $params = null)
378 {
379 global $bugsys;
380
381 $this->_processColumns();
382
383 foreach ($this->columns as $columns)
384 {
385 if (sizeof($columns) > 1)
386 {
387 $build = array();
388 foreach ($columns as $column)
389 {
390 $build[] = $this->_processDataForColumn($bug, $column, $params, true);
391 }
392 $data = "\n\t\t" . implode("\n\t\t", $build) . "\n\t";
393 }
394 else
395 {
396 $data = $this->_processDataForColumn($bug, $columns[0], $params, false);
397 }
398 $fields .= "\n\t<td>$data</td>";
399 }
400
401 $tpl = new BSTemplate('trackerhome_bits');
402 $tpl->vars = array(
403 'bug' => $bug,
404 'fields'=> $fields
405 );
406 return $tpl->evaluate()->getTemplate();
407 }
408
409 /**
410 * Handler for special-case column data
411 *
412 * @param array Bug data
413 * @param string Column name
414 * @param string Additional URL params
415 * @param bool Will this column have multiple data sets?
416 *
417 * @return string Processed column data
418 */
419 private function _processDataForColumn($bug, $column, $params = null, $multi = false)
420 {
421 $open = ($multi ? '<div>' : '');
422 $close = ($multi ? '</div>' : '');
423 switch ($column)
424 {
425 case 'summary':
426 return $open . '<a href="showreport.php?bugid=' . $bug['bugid'] . $params . '">' . $bug['summary'] . '</a>' . $close;
427 case 'reporter':
428 return $open . ($bug['userid'] ? $bug['username'] : T('Guest')) . $close;
429 case 'lastpost':
430 return "\n\t\t<div>" . $bug['lastposttime'] . "</div>\n\t\t<div>" . T('by') . ' ' . ($bug['lastpost'] ? $bug['lastpost'] : T('Guest')) . "</div>\n\t";
431 case 'votes':
432 return "\n\t\t<div>" . T('For:') . ' ' . $bug['votefor'] . "</div>\n\t\t<div>" . T('Against:') . ' ' . $bug['voteagainst'] . "</div>\n\t";
433 default:
434 return $open . $bug["$column"] . $close;
435 }
436 }
437
438 /**
439 * Sets up $this->columns so that the data can be processed more
440 * easily
441 */
442 private function _processColumns()
443 {
444 if (is_array($this->columns))
445 {
446 return;
447 }
448
449 $columns = self::fetch_by_text(false);
450
451 $array = ((bugdar::$userinfo['userid'] && is_array(bugdar::$userinfo['columnoptions'])) ? bugdar::$userinfo['columnoptions'] : bugdar::$options['columnoptions']);
452
453 foreach ($array as $column => $position)
454 {
455 // the column doesn't exist, or more likely, we don't have permission to view it
456 if (!isset($columns[$column]))
457 {
458 continue;
459 }
460 if ($position > 0)
461 {
462 $this->columns["$position"][] = $column;
463 }
464 }
465
466 ksort($this->columns);
467 }
468
469 /**
470 * Returns an array of all the custom fields that the current user
471 * has permission to use
472 *
473 * @param boolean Ignore permissions?
474 *
475 * @return array
476 */
477 private static function _fetch_custom_fields($doPerm = true)
478 {
479 static $fields = array(), $fieldsPerm = array();
480
481 if ($doPerm && !empty($fieldsPerm))
482 {
483 return $fieldsPerm;
484 }
485 else if (!$doPerm && !empty($fields))
486 {
487 return $fields;
488 }
489
490 if ($doPerm)
491 {
492 $fields_fetch = BSApp::$db->query("
493 SELECT bugfield.*, MAX(permission.mask) AS mask
494 FROM " . TABLE_PREFIX . "bugfield AS bugfield
495 LEFT JOIN " . TABLE_PREFIX . "bugfieldpermission AS permission
496 ON (bugfield.fieldid = permission.fieldid)
497 WHERE (permission.mask = 2 OR permission.mask = 1)
498 AND permission.usergroupid IN (" . bugdar::$userinfo['usergroupid'] . (sizeof(bugdar::$userinfo['groupids']) != 0 ? ',' . implode(',', bugdar::$userinfo['groupids']) : '') . ")
499 GROUP BY (bugfield.fieldid)
500 ");
501 }
502 else
503 {
504 $fields_fetch = BSApp::$db->query("SELECT * FROM " . TABLE_PREFIX . "bugfield");
505 }
506
507 foreach ($fields_fetch as $field)
508 {
509 if ($doPerm)
510 {
511 $fieldsPerm[$field['fieldid']] = $field;
512 }
513 else
514 {
515 $fields[$field['fieldid']] = $field;
516 }
517 }
518 return $fields;
519 }
520 }
521
522 /*=====================================================================*\
523 || ###################################################################
524 || # $HeadURL$
525 || # $Id$
526 || ###################################################################
527 \*=====================================================================*/
528 ?>