From 85978a4d18c0d86a62022d28858ae361f7d33bd6 Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Mon, 3 Oct 2011 01:54:01 +0200 Subject: [PATCH] Implement basic filtering capabilities in the issue search view. Since IDF's text search component does not allow further restrictions on the result set, we make a second, filtered query to restrict to the item state ('open' or 'closed') and optionally a label. All in all this is all harder than it could be, especially the tag cloud is very monolithic and should be replaced by a data-driven component that is less dependent on a single data / query and link usage, but this would for now require too many changes. Similar questionable is the code duplication for the index, listStatus and listLabel view implementations that all do more or less the same. The search implementation now only uses one implementation for a very similar use case. It also removes the artificial restriction to 100 results we had previously there and does not query a record for each single result (as was done with Pluf_Search_ResultSet previously). On my way through this I tried to generalize a couple of i18n texts and removed smaller issues like the "trailing comma" in label lists. This partially fixes issue 548. --- src/IDF/Project.php | 49 ++++++- src/IDF/Views/Issue.php | 151 +++++++++++++++++---- src/IDF/conf/urls.php | 12 +- src/IDF/templates/idf/issues/base.html | 2 +- src/IDF/templates/idf/issues/by-label.html | 11 +- src/IDF/templates/idf/issues/search.html | 22 ++- src/IDF/templates/idf/tags-cloud.html | 12 +- 7 files changed, 212 insertions(+), 47 deletions(-) diff --git a/src/IDF/Project.php b/src/IDF/Project.php index 66a070e..f23b34b 100644 --- a/src/IDF/Project.php +++ b/src/IDF/Project.php @@ -132,7 +132,7 @@ class IDF_Project extends Pluf_Model } return $projects[0]; } - + /** * Returns the number of open/closed issues. * @@ -167,7 +167,7 @@ GROUP BY uid"; $key = ($v['id'] === '-1') ? null : $v['id']; $ownerStatistics[$key] = (int)$v['nb']; } - + arsort($ownerStatistics); return $ownerStatistics; @@ -178,9 +178,10 @@ GROUP BY uid"; * * @param string Status ('open'), 'closed' * @param IDF_Tag Subfilter with a label (null) + * @param array Restrict further to a list of ids * @return int Count */ - public function getIssueCountByStatus($status='open', $label=null) + public function getIssueCountByStatus($status='open', $label=null, $ids=array()) { switch ($status) { case 'open': @@ -203,12 +204,48 @@ GROUP BY uid"; $sql2 = new Pluf_SQL('idf_tag_id=%s', array($label->id)); $sql->SAnd($sql2); } + if (count($ids) > 0) { + $sql2 = new Pluf_SQL(sprintf('id IN (%s)', implode(', ', $ids))); + $sql->SAnd($sql2); + } $params = array('filter' => $sql->gen()); if (!is_null($label)) { $params['view'] = 'join_tags'; } $gissue = new IDF_Issue(); return $gissue->getCount($params); } + /** + * Get the tags for a specific list of issues. + * + * @param string Status ('open') or 'closed' + * @param array A list of issue ids + * @return array An array of tag objects + */ + public function getTagsByIssues($issue_ids=array()) + { + // make the below query always a valid one + if (count($issue_ids) == 0) $issue_ids[] = 0; + + $assocTable = $this->_con->pfx.'idf_issue_idf_tag_assoc'; + $query = sprintf( + 'SELECT DISTINCT idf_tag_id FROM %s '. + 'WHERE idf_issue_id IN (%s) '. + 'GROUP BY idf_tag_id', + $assocTable, implode(',', $issue_ids) + ); + + $db = Pluf::db(); + $dbData = $db->select($query); + $ids = array(0); + foreach ($dbData as $data) { + $ids[] = $data['idf_tag_id']; + } + + $sql = new Pluf_SQL(sprintf('id IN (%s)', implode(', ', $ids))); + $model = new IDF_Tag(); + return $model->getList(array('filter' => $sql->gen())); + } + /** * Get the open/closed tag ids as they are often used when doing * listings. @@ -415,7 +452,11 @@ GROUP BY uid"; foreach ($this->_con->select($sql) as $idc) { $tag = new IDF_Tag($idc['id']); $tag->nb_use = $idc['nb_use']; - $tags[] = $tag; + // group by class + if (!array_key_exists($tag->class, $tags)) { + $tags[$tag->class] = array(); + } + $tags[$tag->class][] = $tag; } return new Pluf_Template_ContextVars($tags); } diff --git a/src/IDF/Views/Issue.php b/src/IDF/Views/Issue.php index 953557c..03850dd 100644 --- a/src/IDF/Views/Issue.php +++ b/src/IDF/Views/Issue.php @@ -431,45 +431,142 @@ class IDF_Views_Issue public $search_precond = array('IDF_Precondition::accessIssues'); public function search($request, $match) + { + $query = !isset($request->REQUEST['q']) ? '' : $request->REQUEST['q']; + return $this->doSearch($request, $query, 'open'); + } + + public $searchStatus_precond = array('IDF_Precondition::accessIssues'); + public function searchStatus($request, $match) + { + $query = !isset($request->REQUEST['q']) ? '' : $request->REQUEST['q']; + $status = in_array($match[2], array('open', 'closed')) ? $match[2] : 'open'; + return $this->doSearch($request, $query, $status); + } + + public $searchLabel_precond = array('IDF_Precondition::accessIssues'); + public function searchLabel($request, $match) + { + $query = !isset($request->REQUEST['q']) ? '' : $request->REQUEST['q']; + $tag_id = intval($match[2]); + $status = in_array($match[3], array('open', 'closed')) ? $match[3] : 'open'; + return $this->doSearch($request, $query, $status, $tag_id); + } + + private function doSearch($request, $query, $status, $tag_id=null) { $prj = $request->project; - if (!isset($request->REQUEST['q']) or trim($request->REQUEST['q']) == '') { - $url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::index', - array($prj->shortname)); + if (trim($query) == '') { + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::index', array($prj->shortname)); return new Pluf_HTTP_Response_Redirect($url); } - $q = $request->REQUEST['q']; - $title = sprintf(__('Search Issues - %s'), $q); - $issues = new Pluf_Search_ResultSet(IDF_Search::mySearch($q, $prj, 'IDF_Issue')); - if (count($issues) > 100) { - // no more than 100 results as we do not care - $issues->results = array_slice($issues->results, 0, 100); + + $tag = null; + if ($tag_id !== null) { + $tag = Pluf_Shortcuts_GetObjectOr404('IDF_Tag', $tag_id); } + + $title = sprintf(__('Search issues - %s'), $query); + if ($status === 'closed') { + $title = sprintf(__('Search closed issues - %s'), $query); + } + + // using Plufs ResultSet implementation here is inefficient, because + // it makes a SELECT for each item and does not allow for further + // filtering neither, so we just return the ids and filter by them + // and other things in the next round + $results = IDF_Search::mySearch($query, $prj, 'IDF_Issue'); + + $issue_ids = array(); + foreach ($results as $result) { + $issue_ids[] = $result['model_id']; + } + + $otags = $prj->getTagIdsByStatus($status); + if (count($otags) == 0) $otags[] = 0; + $sql = new Pluf_SQL( + 'id IN ('.implode(',', $issue_ids).') '. + 'AND status IN ('.implode(', ', $otags).') '. + ($tag_id !== null ? 'AND idf_tag_id='.$tag_id.' ' : '') + ); + $model = new IDF_Issue(); + $issues = $model->getList(array('filter' => $sql->gen(), 'view' => 'join_tags')); + + // we unfortunately loose the original sort order, + // so we manually have to apply it here again + $sorted_issues = new ArrayObject(); + $filtered_issue_ids = array(); + foreach ($issue_ids as $issue_id) { + foreach ($issues as $issue) { + if ($issue->id != $issue_id) + continue; + if (array_key_exists($issue_id, $sorted_issues)) + continue; + $sorted_issues[$issue_id] = $issue; + $filtered_issue_ids[] = $issue_id; + } + } + $pag = new Pluf_Paginator(); - $pag->items = $issues; $pag->class = 'recent-issues'; - $pag->item_extra_props = array('project_m' => $prj, - 'shortname' => $prj->shortname, - 'current_user' => $request->user); + $pag->items = $sorted_issues; + $pag->item_extra_props = array( + 'project_m' => $prj, + 'shortname' => $prj->shortname, + 'current_user' => $request->user + ); $pag->summary = __('This table shows the found issues.'); - $pag->action = array('IDF_Views_Issue::search', array($prj->shortname), array('q'=> $q)); $pag->extra_classes = array('a-c', '', 'a-c', ''); - $list_display = array( - 'id' => __('Id'), - array('summary', 'IDF_Views_Issue_SummaryAndLabels', __('Summary')), - array('status', 'IDF_Views_Issue_ShowStatus', __('Status')), - array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')), - ); - $pag->configure($list_display); - $pag->items_per_page = 100; + $pag->configure(array( + 'id' => __('Id'), + array('summary', 'IDF_Views_Issue_SummaryAndLabels', __('Summary')), + array('status', 'IDF_Views_Issue_ShowStatus', __('Status')), + array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')), + )); + // disable paginating + $pag->items_per_page = PHP_INT_MAX; $pag->no_results_text = __('No issues were found.'); $pag->setFromRequest($request); - $params = array('page_title' => $title, - 'issues' => $pag, - 'q' => $q, - ); - return Pluf_Shortcuts_RenderToResponse('idf/issues/search.html', $params, $request); + if ($tag_id === null) { + $pag->action = array('IDF_Views_Issue::searchStatus', + array($prj->shortname, $status), + array('q'=> $query), + ); + } else { + $pag->action = array('IDF_Views_Issue::searchLabel', + array($prj->shortname, $tag_id, $status), + array('q'=> $query), + ); + } + + // get stats about the issues + $open = $prj->getIssueCountByStatus('open', $tag, $issue_ids); + $closed = $prj->getIssueCountByStatus('closed', $tag, $issue_ids); + + // query the available tags for this search result + $all_tags = $prj->getTagsByIssues($filtered_issue_ids); + $grouped_tags = array(); + foreach ($all_tags as $atag) { + // group by class + if (!array_key_exists($atag->class, $grouped_tags)) { + $grouped_tags[$atag->class] = array(); + } + $grouped_tags[$atag->class][] = $atag; + } + + $params = array( + 'page_title' => $title, + 'issues' => $pag, + 'query' => $query, + 'status' => $status, + 'open' => $open, + 'closed' => $closed, + 'tag' => $tag, + 'all_tags' => $grouped_tags, + ); + + return Pluf_Shortcuts_RenderToResponse('idf/issues/search.html', $params, $request); } public $view_precond = array('IDF_Precondition::accessIssues'); diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index 03779c0..e5b8081 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -117,7 +117,7 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/$#', 'base' => $base, 'model' => 'IDF_Views_Issue', 'method' => 'index'); - + $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/summary/$#', 'base' => $base, 'model' => 'IDF_Views_Issue', @@ -128,6 +128,16 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/search/$#', 'model' => 'IDF_Views_Issue', 'method' => 'search'); +$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/search/status/(\w+)/$#', + 'base' => $base, + 'model' => 'IDF_Views_Issue', + 'method' => 'searchStatus'); + +$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/search/label/(\d+)/(\w+)/$#', + 'base' => $base, + 'model' => 'IDF_Views_Issue', + 'method' => 'searchLabel'); + $ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/(\d+)/$#', 'base' => $base, 'model' => 'IDF_Views_Issue', diff --git a/src/IDF/templates/idf/issues/base.html b/src/IDF/templates/idf/issues/base.html index 636d5da..60f9d85 100644 --- a/src/IDF/templates/idf/issues/base.html +++ b/src/IDF/templates/idf/issues/base.html @@ -7,7 +7,7 @@ {if !$user.isAnonymous()} | {trans 'New Issue'} | {trans 'My Issues'} | {trans 'My watch list'}{/if} |
- +
{if $inIssue} | diff --git a/src/IDF/templates/idf/issues/by-label.html b/src/IDF/templates/idf/issues/by-label.html index 3d57a3b..fdac576 100644 --- a/src/IDF/templates/idf/issues/by-label.html +++ b/src/IDF/templates/idf/issues/by-label.html @@ -8,16 +8,15 @@ {/block} {block context} -

{trans 'Label:'} -{aurl 'url', 'IDF_Views_Issue::listLabel', array($project.shortname, $label.id, 'open')} -{$label.class}:{$label.name}

{aurl 'open_url', 'IDF_Views_Issue::listLabel', array($project.shortname, $label.id, 'open')} {aurl 'closed_url', 'IDF_Views_Issue::listLabel', array($project.shortname, $label.id, 'closed')} {blocktrans}

Open issues: {$open}

Closed issues: {$closed}

-{/blocktrans}{if $completion} +{/blocktrans} +

{trans 'Label:'} +{aurl 'url', 'IDF_Views_Issue::listLabel', array($project.shortname, $label.id, 'open')} +{$label.class}:{$label.name}

+{if $completion}

{trans 'Completion:'} {$completion}

{/if} - - {/block} diff --git a/src/IDF/templates/idf/issues/search.html b/src/IDF/templates/idf/issues/search.html index 71388ed..4c0b720 100644 --- a/src/IDF/templates/idf/issues/search.html +++ b/src/IDF/templates/idf/issues/search.html @@ -8,5 +8,25 @@ {/block} {block context} -

{trans 'Found issues:'} {$issues.nb_items}

+{aurl 'open_url', 'IDF_Views_Issue::searchStatus', array($project.shortname, 'open'), array('q' => $query)} +{aurl 'closed_url', 'IDF_Views_Issue::searchStatus', array($project.shortname, 'closed'), array('q' => $query)} +{if $tag != null} +{aurl 'open_url', 'IDF_Views_Issue::searchLabel', array($project.shortname, $tag.id, 'open'), array('q' => $query)} +{aurl 'closed_url', 'IDF_Views_Issue::searchLabel', array($project.shortname, $tag.id, 'closed'), array('q' => $query)} +{/if} +{blocktrans} +

Found open issues: {$open}

+

Found closed issues: {$closed}

{/blocktrans} +{if $tag !== null} +{blocktrans}

Label: +{$tag.class}:{$tag.name}

{/blocktrans} +{else} +{* yes, this is duplicated from tags-cloud.html, but the code there cannot be easily overridden *} +
{foreach $all_tags as $class => $labels} +
{$class}
+{foreach $labels as $idx => $label} +{aurl 'url', 'IDF_Views_Issue::searchLabel', array($project.shortname, $label.id, $status), array('q'=> $query)} +
{$label.name}{if $idx != count($labels) - 1},{/if}
+{/foreach}{/foreach}

+{/if} {/block} diff --git a/src/IDF/templates/idf/tags-cloud.html b/src/IDF/templates/idf/tags-cloud.html index 353b9c5..e230268 100644 --- a/src/IDF/templates/idf/tags-cloud.html +++ b/src/IDF/templates/idf/tags-cloud.html @@ -1,8 +1,6 @@ -{assign $class = ''}{assign $i = 0} -
{foreach $project.getTagCloud($cloud) as $label} +
{foreach $project.getTagCloud($cloud) as $class => $labels} +
{$class}
+{foreach $labels as $idx => $label} {aurl 'url', $cloud_url, array($project.shortname, $label.id, 'open')} -{if $class != $label.class}
{$label.class}
{assign $i = 0}{/if} -
{$label.name},
-{assign $class = $label.class} -{assign $i = $i + 1} -{/foreach}

+
{$label.name}{if $idx != count($labels) - 1},{/if}
+{/foreach}{/foreach}