PageRenderTime 51ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/inc/report.php

https://bitbucket.org/wez/mtrack/
PHP | 811 lines | 673 code | 88 blank | 50 comment | 147 complexity | 4aa176894e12025f5103a5d7076a8af8 MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0
  1. <?php # vim:ts=2:sw=2:et:
  2. /* For licensing and copyright terms, see the file named LICENSE */
  3. class MTrackReport {
  4. public $rid = null;
  5. public $summary = null;
  6. public $description = null;
  7. public $query = null;
  8. public $changed = null;
  9. static function loadByID($id) {
  10. return new MTrackReport($id);
  11. }
  12. static function loadBySummary($summary) {
  13. list($row) = MTrackDB::q('select rid from reports where summary = ?',
  14. $summary)->fetchAll();
  15. if (isset($row[0])) {
  16. return new MTrackReport($row[0]);
  17. }
  18. return null;
  19. }
  20. function __construct($id = null) {
  21. $this->rid = $id;
  22. if ($this->rid) {
  23. $q = MTrackDB::q('select * from reports where rid = ?', $this->rid);
  24. foreach ($q->fetchAll() as $row) {
  25. $this->summary = $row['summary'];
  26. $this->description = $row['description'];
  27. $this->query = $row['query'];
  28. $this->changed = (int)$row['changed'];
  29. return;
  30. }
  31. throw new Exception("report $id not found");
  32. }
  33. }
  34. function save(MTrackChangeset $changeset) {
  35. if ($this->rid) {
  36. /* figure what we actually changed */
  37. $q = MTrackDB::q('select * from reports where rid = ?', $this->rid);
  38. list($row) = $q->fetchAll();
  39. $changeset->add("report:" . $this->rid . ":summary",
  40. $row['summary'], $this->summary);
  41. $changeset->add("report:" . $this->rid . ":description",
  42. $row['description'], $this->description);
  43. $changeset->add("report:" . $this->rid . ":query",
  44. $row['query'], $this->query);
  45. $q = MTrackDB::q('update reports set summary = ?, description = ?, query = ?, changed = ? where rid = ?',
  46. $this->summary, $this->description, $this->query,
  47. $changeset->cid, $this->rid);
  48. } else {
  49. $q = MTrackDB::q('insert into reports (summary, description, query, changed) values (?, ?, ?, ?)',
  50. $this->summary, $this->description, $this->query,
  51. $changeset->cid);
  52. $this->rid = MTrackDB::lastInsertId('reports', 'rid');
  53. $changeset->add("report:" . $this->rid . ":summary",
  54. null, $this->summary);
  55. $changeset->add("report:" . $this->rid . ":description",
  56. null, $this->description);
  57. $changeset->add("report:" . $this->rid . ":query",
  58. null, $this->query);
  59. }
  60. }
  61. static $reportFormats = array(
  62. 'html' => array(
  63. 'downloadable' => false,
  64. 'render' => 'MTrackReport::renderReportHTML'
  65. ),
  66. 'tab' => array(
  67. 'downloadable' => 'Tab-delimited Text',
  68. 'render' => 'MTrackReport::renderReportTab',
  69. 'mimetype' => 'text/plain',
  70. ),
  71. 'csv' => array(
  72. 'downloadable' => 'CSV',
  73. 'render' => 'MTrackReport::renderReportCSV',
  74. 'mimetype' => 'text/csv',
  75. ),
  76. );
  77. static function emitReportDownloadHeaders($name, $format) {
  78. header("Content-Disposition: attachment;filename=\"$name.$format\"");
  79. if (isset(self::$reportFormats[$format]['mimetype'])) {
  80. $mime = self::$reportFormats[$format]['mimetype'];
  81. header("Content-Type: $mime;charset=UTF-8");
  82. } else {
  83. header("Content-Type: text/plain;charset=UTF-8");
  84. }
  85. }
  86. /* runs the report and returns the raw rowset */
  87. static function executeReportQuery($repstring, $passed_params = null) {
  88. $db = MTrackDB::get();
  89. /* process the report string; any $PARAM in there is recognized
  90. * as a parameter and the query munged accordingly to pass in the data */
  91. $params = array();
  92. $n = preg_match_all("/\\$([A-Z]+)/m", $repstring, $matches);
  93. for ($i = 0; $i < $n; $i++) {
  94. $pname = $matches[1][$i];
  95. /* default the parameter to no value */
  96. $params[$pname] = '';
  97. /* replace with query placeholder */
  98. $repstring = str_replace('$' . $pname, ':' . $pname,
  99. $repstring);
  100. }
  101. /* now to summon parameters */
  102. if (isset($params['USER'])) {
  103. $params['USER'] = MTrackAuth::whoami();
  104. }
  105. foreach ($params as $p => $v) {
  106. if (isset($_GET[$p])) {
  107. $params[$p] = $_GET[$p];
  108. }
  109. }
  110. if (is_array($passed_params)) {
  111. foreach ($params as $p => $v) {
  112. if (isset($passed_params[$p])) {
  113. $params[$p] = $passed_params[$p];
  114. }
  115. }
  116. }
  117. $q = $db->prepare($repstring);
  118. $q->execute($params);
  119. return $q->fetchAll(PDO::FETCH_ASSOC);
  120. }
  121. static function renderReport($repstring, $passed_params = null,
  122. $format = 'html') {
  123. global $ABSWEB;
  124. try {
  125. $results = self::executeReportQuery($repstring, $passed_params);
  126. } catch (Exception $e) {
  127. return "<div class='error'>" . $e->getMessage() . "<br>" .
  128. htmlentities($repstring, ENT_QUOTES, 'utf-8') . "</div>";
  129. }
  130. if (count($results) == 0) {
  131. return "No records matched";
  132. }
  133. $CF = MTrackTicket_CustomFields::getInstance();
  134. /* figure out the table headings */
  135. $captions = array();
  136. foreach ($results[0] as $name => $value) {
  137. if (preg_match("/^__.*__$/", $name)) {
  138. if ($format == 'html') {
  139. /* special meaning, not a column */
  140. continue;
  141. }
  142. }
  143. $caption = preg_replace("/^_(.*)_$/", "\\1", $name);
  144. if (!strncmp($caption, "x_", 2)) {
  145. $CFI = $CF->fieldByName($caption);
  146. if ($CFI) {
  147. $caption = $CFI->label;
  148. }
  149. }
  150. $captions[$name] = $caption;
  151. }
  152. $render = self::$reportFormats[$format]['render'];
  153. if (is_string($render) &&
  154. preg_match("/^(.*)::(.*)$/", $render, $M)) {
  155. $render = array($M[1], $M[2]);
  156. }
  157. if (!is_callable($render)) {
  158. return "Cannot render reports in " .
  159. htmlentities($format, ENT_QUOTES, 'utf-8');
  160. }
  161. return call_user_func($render, $captions, $results);
  162. }
  163. static function renderReportCSV($captions, $results) {
  164. $out = '';
  165. $t = fopen("php://temp", 'r+');
  166. $c = array();
  167. foreach ($captions as $name => $caption) {
  168. $caption = ucfirst($caption);
  169. if ($name[0] == '_' && substr($name,-1) == '_') {
  170. $c[] = $caption;
  171. } elseif ($name[0] == '_') {
  172. $c[] = substr($caption, 1);
  173. } else {
  174. $c[] = $caption;
  175. }
  176. }
  177. fputcsv($t, $c);
  178. foreach ($results as $nrow => $row) {
  179. $c = array();
  180. foreach ($captions as $name => $caption) {
  181. $c[] = trim(preg_replace("/[\t\n\r]+/sm", " ", $row[$name]));
  182. }
  183. fputcsv($t, $c);
  184. }
  185. fseek($t, 0);
  186. return stream_get_contents($t);
  187. }
  188. static function renderReportTab($captions, $results) {
  189. $out = '';
  190. foreach ($captions as $name => $caption) {
  191. $caption = ucfirst($caption);
  192. if ($name[0] == '_' && substr($name,-1) == '_') {
  193. $out .= "$caption\t";
  194. } elseif ($name[0] == '_') {
  195. $out .= substr($caption, 1) . "\t";
  196. } else {
  197. $out .= "$caption\t";
  198. }
  199. }
  200. $out .= "\n";
  201. foreach ($results as $nrow => $row) {
  202. foreach ($captions as $name => $caption) {
  203. $v = trim(preg_replace("/[\t\n\r]+/sm", " ", $row[$name]));
  204. $out .= "$v\t";
  205. }
  206. $out .= "\n";
  207. }
  208. $out = str_replace("\t\n", "\n", $out);
  209. return $out;
  210. }
  211. static function renderReportHTML($captions, $results) {
  212. global $ABSWEB;
  213. $out = '';
  214. /* for spanning purposes, calculate the longest row */
  215. $max_width = 0;
  216. $width = 0;
  217. foreach ($captions as $name => $caption) {
  218. if ($name[0] == '_' && substr($name, -1) == '_') {
  219. $width = 1;
  220. } else {
  221. $width++;
  222. }
  223. if ($width > $max_width) {
  224. $max_width = $width;
  225. }
  226. if (substr($name, -1) == '_') {
  227. $width = 1;
  228. }
  229. }
  230. $group = null;
  231. foreach ($results as $nrow => $row) {
  232. $starting_new_group = false;
  233. if ($nrow == 0) {
  234. $starting_new_group = true;
  235. } else if (
  236. (isset($row['__group__']) && $group !== $row['__group__'])) {
  237. $starting_new_group = true;
  238. }
  239. if ($starting_new_group) {
  240. /* starting a new group */
  241. if ($nrow) {
  242. /* close the old one */
  243. $out .= "</tbody></table>\n";
  244. }
  245. if (isset($row['__group__'])) {
  246. $out .= "<h2 class='reportgroup'>" .
  247. htmlentities($row['__group__'], ENT_COMPAT, 'utf-8') .
  248. "</h2>\n";
  249. $group = $row['__group__'];
  250. }
  251. $out .= "<table class='report'><thead><tr>";
  252. foreach ($captions as $name => $caption) {
  253. /* figure out sort info for javascript bits */
  254. $sort = null;
  255. switch (strtolower($caption)) {
  256. case 'priority':
  257. case 'ticket':
  258. case 'severity':
  259. case 'ord':
  260. $sort = strtolower($caption);
  261. break;
  262. case 'created':
  263. case 'modified':
  264. case 'date':
  265. case 'due':
  266. $sort = 'mtrackdate';
  267. break;
  268. case 'remaining':
  269. case 'estimated':
  270. $sort = 'digit';
  271. break;
  272. case 'is_child':
  273. continue 2;
  274. case 'updated':
  275. case 'time':
  276. case 'content':
  277. case 'summary':
  278. default:
  279. break;
  280. }
  281. $caption = ucfirst($caption);
  282. if ($name[0] == '_' && substr($name,-1) == '_') {
  283. $out .= "</tr><tr><th colspan='$max_width'>$caption</th></tr><tr>";
  284. } elseif ($name[0] == '_') {
  285. continue;
  286. } else {
  287. $out .= "<th";
  288. if ($sort !== null) {
  289. $out .= " class=\"{sorter: '$sort'}\"";
  290. }
  291. $out .= ">$caption</th>";
  292. if (substr($name, -1) == '_') {
  293. $out .= "</tr><tr>";
  294. }
  295. }
  296. }
  297. $out .= "</tr></thead><tbody>\n";
  298. }
  299. /* and now the column data itself */
  300. if (isset($row['__style__'])) {
  301. $style = " style=\"$row[__style__]\"";
  302. } else {
  303. $style = "";
  304. }
  305. $class = $nrow % 2 ? "even" : "odd";
  306. if (isset($row['__color__'])) {
  307. $class .= " color$row[__color__]";
  308. }
  309. if (isset($row['__status__'])) {
  310. $class .= " status$row[__status__]";
  311. }
  312. if (isset($row['is_child']) && (int)$row['is_child']) {
  313. $class .= " is_child";
  314. }
  315. $begin_row = "<tr class=\"$class\"$style>";
  316. $out .= $begin_row;
  317. $href = null;
  318. /* determine if we should link to something for this row */
  319. if (isset($row['ticket'])) {
  320. $href = $ABSWEB . "ticket.php/$row[ticket]";
  321. }
  322. foreach ($captions as $name => $caption) {
  323. $v = $row[$name];
  324. /* apply special formatting rules */
  325. switch (strtolower($caption)) {
  326. case 'created':
  327. case 'modified':
  328. case 'date':
  329. case 'due':
  330. case 'updated':
  331. case 'time':
  332. if ($v !== null) {
  333. $v = mtrack_date($v);
  334. }
  335. break;
  336. case 'content':
  337. $v = MTrackWiki::format_to_html($v);
  338. break;
  339. case 'owner':
  340. $v = mtrack_username($v, array('no_image' => true));
  341. break;
  342. case 'is_child':
  343. continue 2;
  344. case 'docid':
  345. case 'ticket':
  346. if (isset($row['is_child']) && (int)$row['is_child']) {
  347. $caption .= " is_child";
  348. }
  349. $v = mtrack_ticket($row);
  350. break;
  351. case 'summary':
  352. if ($href) {
  353. $v = htmlentities($v, ENT_QUOTES, 'utf-8');
  354. $v = "<a href=\"$href\">$v</a>";
  355. } else {
  356. $v = htmlentities($v, ENT_QUOTES, 'utf-8');
  357. }
  358. break;
  359. case 'milestone':
  360. $oldv = $v;
  361. $v = '';
  362. foreach (preg_split("/\s*,\s*/", $oldv) as $m) {
  363. if (!strlen($m)) continue;
  364. $v .= "<span class='milestone'>" .
  365. "<a href=\"{$ABSWEB}milestone.php/" .
  366. urlencode($m) . "\">" .
  367. htmlentities($m, ENT_QUOTES, 'utf-8') .
  368. "</a></span> ";
  369. }
  370. break;
  371. case 'keyword':
  372. $oldv = $v;
  373. $v = '';
  374. foreach (preg_split("/\s*,\s*/", $oldv) as $m) {
  375. if (!strlen($m)) continue;
  376. $v .= mtrack_keyword($m) . ' ';
  377. }
  378. break;
  379. default:
  380. $v = htmlentities($v, ENT_QUOTES, 'utf-8');
  381. }
  382. if ($name[0] == '_' && substr($name, -1) == '_') {
  383. $out .= "</tr>$begin_row<td class='$caption' colspan='$max_width'>$v</td></tr>$begin_row";
  384. } elseif ($name[0] == '_') {
  385. continue;
  386. } else {
  387. $out .= "<td class='$caption'>$v</td>";
  388. if (substr($name, -1) == '_') {
  389. $out .= "</tr>$begin_row";
  390. }
  391. }
  392. }
  393. $out .= "</tr>\n";
  394. }
  395. $out .= "</tbody></table>";
  396. return $out;
  397. }
  398. /** Run a saved report and render it as HTML */
  399. static function macro_RunReport($name, $url_style_params = null) {
  400. $params = array();
  401. parse_str($url_style_params, $params);
  402. $rep = self::loadBySummary($name);
  403. if ($rep) {
  404. if (MTrackACL::hasAllRights("report:" . $rep->rid, 'read')) {
  405. return $rep->renderReport($rep->query, $params);
  406. } else {
  407. return "Not authorized to run report $name";
  408. }
  409. } else {
  410. return "Unable to find report $name";
  411. }
  412. }
  413. static function parseQuery()
  414. {
  415. $macro_params = array(
  416. 'group' => true,
  417. 'col' => true,
  418. 'order' => true,
  419. 'desc' => true,
  420. 'format' => true,
  421. 'compact' => true,
  422. 'count' => true,
  423. 'max' => true
  424. );
  425. $mparams = array(
  426. 'col' => array('ticket', 'summary', 'state',
  427. 'priority',
  428. 'owner', 'type', 'component',
  429. 'remaining'),
  430. 'desc' => array('0'),
  431. );
  432. $params = array();
  433. $have_milestone = false;
  434. $args = func_get_args();
  435. foreach ($args as $arg) {
  436. if ($arg === null) continue;
  437. $p = explode('&', $arg);
  438. foreach ($p as $a) {
  439. $a = urldecode($a);
  440. preg_match('/^([a-zA-Z_]+)(!?(?:=|~=|\^=|\$=))(.*)$/', $a, $M);
  441. $k = $M[1];
  442. $op = $M[2];
  443. $pat = explode('|', $M[3]);
  444. if (isset($macro_params[$k])) {
  445. $mparams[$k] = $pat;
  446. } else if (isset($params[$k])) {
  447. if ($params[$k][0] == $op) {
  448. // compatible operator; add $pat to possible set
  449. $params[$k][1] = array_merge($pat, $params[$k][1]);
  450. } else {
  451. // ignore
  452. }
  453. } else {
  454. if ($k == 'milestone') {
  455. $have_milestone = true;
  456. }
  457. $params[$k] = array($op, $pat);
  458. }
  459. }
  460. }
  461. if (!isset($mparams['order'])) {
  462. /* if they specified a milestone, we can use the established
  463. * stack rank order from the planning screen, else the priority
  464. * property */
  465. if ($have_milestone) {
  466. $mparams['order'] = array('pri_ord', 'pri.value');
  467. $col = $mparams['col'];
  468. if (!in_array('pri_ord', $col)) {
  469. array_unshift($col, 'pri_ord');
  470. $mparams['col'] = $col;
  471. }
  472. } else {
  473. $mparams['order'] = array('pri.value');
  474. }
  475. }
  476. if (!count($params)) {
  477. $me = MTrackAuth::whoami();
  478. $params['status'] = array('!=', array('closed'));
  479. if ($me != 'anonymous') {
  480. $params['owner'] = array('=', array($me));
  481. }
  482. }
  483. return array($params, $mparams);
  484. }
  485. /** Run a ticket query and render it as HTML */
  486. static function macro_TicketQuery()
  487. {
  488. $args = func_get_args();
  489. $sql = call_user_func_array(array('MTrackReport', 'TicketQueryToSQL'),
  490. $args);
  491. # return htmlentities($sql) . "<br>" . self::renderReport($sql);
  492. # return var_export($sql, true);
  493. return self::renderReport($sql);
  494. }
  495. static function TicketQueryToSQL()
  496. {
  497. $args = func_get_args();
  498. list($params, $mparams) = call_user_func_array(array(
  499. 'MTrackReport', 'parseQuery'), $args);
  500. return self::composeParsedQuery($params, $mparams);
  501. }
  502. static function composeParsedQuery($params, $mparams)
  503. {
  504. /* compose that info into a query */
  505. $sql = 'select t.ptid is not null as is_child, ';
  506. $colmap = array(
  507. 'ticket' => '(case when t.nsident is null then t.tid else t.nsident end) as ticket',
  508. 'component' => '(select mtrack_group_concat(name) from ticket_components
  509. tcm left join components c on (tcm.compid = c.compid)
  510. where tcm.tid = t.tid) as component',
  511. 'keyword' => '(select mtrack_group_concat(keyword) from ticket_keywords
  512. tk left join keywords k on (tk.kid = k.kid)
  513. where tk.tid = t.tid) as keyword',
  514. 'type' => 'classification as type',
  515. 'remaining' =>
  516. // This is much more complex than I'd like it to be :-/
  517. // This logic MUST be equivalent to that of MTrackIssue::getRemaining
  518. // Logic is: if we have any non-zero effort entries, we sum them to
  519. // get the remaining time, otherwise we use the estimated value.
  520. // Except when the ticket is closed: show 0 then.
  521. <<<SQL
  522. (
  523. case when
  524. t.status = 'closed' then
  525. 0
  526. else (
  527. select
  528. greatest(
  529. round(
  530. cast(t.estimated as numeric) +
  531. cast(coalesce(sum(remaining), 0) as numeric
  532. ), 2),
  533. 0
  534. )
  535. from effort where effort.tid = t.tid and remaining != 0
  536. )
  537. end
  538. ) as remaining
  539. SQL
  540. ,
  541. 'state' => "(case when t.status = 'closed' then coalesce(t.resolution, 'closed') else t.status end) as state",
  542. 'milestone' => '(select mtrack_group_concat(name) from ticket_milestones
  543. tmm left join milestones tmmm on (tmm.mid = tmmm.mid)
  544. where tmm.tid = t.tid) as milestone',
  545. 'depends' => '(select mtrack_group_concat(ot.nsident) from
  546. ticket_deps tdep left join tickets ot on (tdep.depends_on =
  547. ot.tid) where tdep.tid = t.tid) as depends',
  548. 'blocks' => '(select mtrack_group_concat(ot.nsident) from
  549. ticket_deps tdep left join tickets ot on (tdep.tid =
  550. ot.tid) where tdep.depends_on = t.tid) as blocks',
  551. 'pri_ord' => 'pri_ord as ord',
  552. );
  553. $cols = array(
  554. ' pri.value as __color__ ',
  555. ' (case when t.nsident is null then t.tid else t.nsident end) as ticket ',
  556. " t.status as __status__ ",
  557. );
  558. foreach ($mparams['col'] as $colname) {
  559. if ($colname == 'ticket') {
  560. continue;
  561. }
  562. if ($colname == 'pri_ord' && !isset($params['milestone'])) {
  563. continue;
  564. }
  565. if (isset($colmap[$colname])) {
  566. $cols[$colname] = $colmap[$colname];
  567. } else {
  568. if (!preg_match("/^[a-zA-Z_]+$/", $colname)) {
  569. throw new Exception("column name $colname is invalid");
  570. }
  571. $cols[$colname] = $colname;
  572. }
  573. }
  574. $sql .= join(', ', $cols);
  575. if (!isset($params['milestone'])) {
  576. $sql .= <<<SQL
  577. FROM
  578. tickets t
  579. left join priorities pri on (t.priority = pri.priorityname)
  580. left join severities sev on (t.severity = sev.sevname)
  581. WHERE
  582. 1 = 1
  583. SQL;
  584. } else {
  585. $sql .= <<<SQL
  586. FROM milestones m
  587. left join ticket_milestones tm on (m.mid = tm.mid)
  588. left join tickets t on (tm.tid = t.tid)
  589. left join priorities pri on (t.priority = pri.priorityname)
  590. left join severities sev on (t.severity = sev.sevname)
  591. WHERE
  592. 1 = 1
  593. SQL;
  594. }
  595. $critmap = array(
  596. 'milestone' => 'm.name',
  597. 'tid' => 't.nsident',
  598. 'id' => 't.nsident',
  599. 'ticket' => 't.nsident',
  600. 'type' => 't.classification',
  601. );
  602. foreach ($params as $k => $v) {
  603. list($op, $values) = $v;
  604. if (isset($critmap[$k])) {
  605. $k = $critmap[$k];
  606. }
  607. $sql .= " AND ";
  608. if ($op[0] == '!') {
  609. $sql .= " NOT ";
  610. $op = substr($op, 1);
  611. }
  612. $sql .= "(";
  613. if ($op == '=') {
  614. /* Allow "100,200" to pick out 100 and 200.
  615. * Allow "100-110" to pick out the range 100-110 inclusive.
  616. * Allow "100-110,200" to pick out the range 100-110 and 200
  617. *
  618. * Made more interesting by namespace prefixes and text/integer
  619. * conversions in the database (postgres is more pedantic than
  620. * sqlite!), so we handle the range expansion
  621. * in the query that we generate and build a set that we query
  622. * using the "IN" clause
  623. */
  624. if (count($values) == 1 && $k == 't.nsident' &&
  625. preg_match('/[,-]/', $values[0])) {
  626. $crit = array();
  627. foreach (explode(',', $values[0]) as $range) {
  628. list($rfrom, $rto) = explode('-', $range, 2);
  629. if (!$rto) {
  630. $crit[] = " $k = " . MTrackDB::esc($rfrom) . " ";
  631. continue;
  632. }
  633. $critset = array();
  634. /* if it's a range, we look for the numeric portion of it
  635. * (recall that it may have a prefix and be something like
  636. * "mc123" */
  637. $rfromint = (int)$rfrom;
  638. if (preg_match("/(\d+)/", $rfrom, $M)) {
  639. $rfromint = (int)$M[1];
  640. }
  641. $rtoint = (int)$rto;
  642. /* note that if the the namespace prefixes don't match between
  643. * the from and the to, you'll get undefined behavior; we use
  644. * the rfrom value as the template for the range that we generate.
  645. */
  646. if (preg_match("/(\d+)/", $rto, $M)) {
  647. $rtoint = (int)$M[1];
  648. }
  649. for ($i = $rfromint; $i <= $rtoint; $i++) {
  650. $critset[] = MTrackDB::esc(preg_replace("/(\d+)/", $i, $rfrom));
  651. }
  652. $crit[] = "$k in (" . join($critset, ",") . ")";
  653. }
  654. $sql .= join(' OR ', $crit);
  655. } else if (count($values) == 1) {
  656. $sql .= " $k = " . MTrackDB::esc($values[0]) . " ";
  657. } else {
  658. $sql .= " $k in (";
  659. foreach ($values as $i => $val) {
  660. $values[$i] = MTrackDB::esc($val);
  661. }
  662. $sql .= join(', ', $values) . ") ";
  663. }
  664. } else {
  665. /* variations on like */
  666. if ($op == '~=') {
  667. $start = '%';
  668. $end = '%';
  669. } else if ($op == '^=') {
  670. $start = '';
  671. $end = '%';
  672. } else {
  673. $start = '%';
  674. $end = '';
  675. }
  676. $crit = array();
  677. foreach ($values as $val) {
  678. $crit[] = "($k LIKE " . MTrackDB::esc("$start$val$end") . ")";
  679. }
  680. $sql .= join(" OR ", $crit);
  681. }
  682. $sql .= ") ";
  683. }
  684. if (isset($mparams['group'])) {
  685. $g = $mparams['group'][0];
  686. if (!ctype_alpha($g)) {
  687. throw new Exception("group $g is not alpha");
  688. }
  689. $sql .= ' GROUP BY ' . $g;
  690. }
  691. if (isset($mparams['order'])) {
  692. $k = $mparams['order'][0];
  693. if ($k == 'tid') {
  694. $k = 't.tid';
  695. }
  696. $sql .= ' ORDER BY ' . $k;
  697. if (isset($mparams['desc']) && $mparams['desc'][0]) {
  698. $sql .= ' DESC';
  699. }
  700. }
  701. if (isset($mparams['max'])) {
  702. $sql .= ' LIMIT ' . (int)$mparams['max'][0];
  703. }
  704. return $sql;
  705. }
  706. static function resolve_report_link(MTrackLink $link)
  707. {
  708. $link->url = $GLOBALS['ABSWEB'] . 'report.php/' .
  709. $link->target;
  710. }
  711. static function resolve_query_link(MTrackLink $link)
  712. {
  713. $link->url = $GLOBALS['ABSWEB'] . 'query.php?' .
  714. $link->target;
  715. }
  716. };
  717. MTrackWiki::register_macro('RunReport',
  718. array('MTrackReport', 'macro_RunReport'));
  719. MTrackWiki::register_macro('TicketQuery',
  720. array('MTrackReport', 'macro_TicketQuery'));
  721. MTrackACL::registerAncestry('report', 'Reports');
  722. MTrackLink::register('query', 'MTrackReport::resolve_query_link');
  723. MTrackLink::register('report', 'MTrackReport::resolve_report_link');