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