From 85978a4d18c0d86a62022d28858ae361f7d33bd6 Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Mon, 3 Oct 2011 01:54:01 +0200 Subject: [PATCH 1/4] 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}

From 160d11b89be35086ae0e132c4a1f090ad3ceb767 Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Tue, 4 Oct 2011 00:37:28 +0200 Subject: [PATCH 2/4] Properly initialize arrays with 0 for IN conditions --- src/IDF/Views/Issue.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IDF/Views/Issue.php b/src/IDF/Views/Issue.php index 03850dd..854b9b9 100644 --- a/src/IDF/Views/Issue.php +++ b/src/IDF/Views/Issue.php @@ -477,7 +477,7 @@ class IDF_Views_Issue // and other things in the next round $results = IDF_Search::mySearch($query, $prj, 'IDF_Issue'); - $issue_ids = array(); + $issue_ids = array(0); foreach ($results as $result) { $issue_ids[] = $result['model_id']; } @@ -495,7 +495,7 @@ class IDF_Views_Issue // we unfortunately loose the original sort order, // so we manually have to apply it here again $sorted_issues = new ArrayObject(); - $filtered_issue_ids = array(); + $filtered_issue_ids = array(0); foreach ($issue_ids as $issue_id) { foreach ($issues as $issue) { if ($issue->id != $issue_id) From 9e2ea7404b46d53b558c290613e6cf85fe9e0f40 Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Tue, 4 Oct 2011 00:46:48 +0200 Subject: [PATCH 3/4] Tags are now returned grouped by class, I seem to have missed this call when I changed the API... --- src/IDF/Views/Issue.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/IDF/Views/Issue.php b/src/IDF/Views/Issue.php index 854b9b9..a8e52fd 100644 --- a/src/IDF/Views/Issue.php +++ b/src/IDF/Views/Issue.php @@ -119,9 +119,11 @@ class IDF_Views_Issue } // Issue class tag statistics - $tags = $prj->getTagCloud(); - foreach ($tags as $t) { - $tagStatistics[$t->class][$t->name] = array($t->nb_use, $t->id); + $grouped_tags = $prj->getTagCloud(); + foreach ($grouped_tags as $class => $tags) { + foreach ($tags as $tag) { + $tagStatistics[$class][$tag->name] = array($tag->nb_use, $tag->id); + } } foreach($tagStatistics as $k => $v) { $nbIssueInClass = 0; From 9a8bd464a33774741133eb1a36b6d11c8800dfc0 Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Tue, 4 Oct 2011 00:47:31 +0200 Subject: [PATCH 4/4] Remove a couple of unused calls to get a list of project tags; these calls are currently done from within the tags-cloud template most of the time, which is ugly, but the way it works as of now. --- src/IDF/Views/Download.php | 2 -- src/IDF/Views/Wiki.php | 2 -- src/IDF/templates/idf/review/index.html | 13 ------------- 3 files changed, 17 deletions(-) diff --git a/src/IDF/Views/Download.php b/src/IDF/Views/Download.php index 97e0ad4..5fb49e8 100644 --- a/src/IDF/Views/Download.php +++ b/src/IDF/Views/Download.php @@ -319,13 +319,11 @@ class IDF_Views_Download $pag->no_results_text = __('No downloads were found.'); $pag->sort_order = array('creation_dtime', 'DESC'); $pag->setFromRequest($request); - $tags = $prj->getTagCloud('downloads'); return Pluf_Shortcuts_RenderToResponse('idf/downloads/index.html', array( 'page_title' => $title, 'label' => $tag, 'downloads' => $pag, - 'tags' => $tags, 'dlabel' => $dtag, ), $request); diff --git a/src/IDF/Views/Wiki.php b/src/IDF/Views/Wiki.php index 1104b51..6880f4c 100644 --- a/src/IDF/Views/Wiki.php +++ b/src/IDF/Views/Wiki.php @@ -152,13 +152,11 @@ class IDF_Views_Wiki $pag->items_per_page = 25; $pag->no_results_text = __('No documentation pages were found.'); $pag->setFromRequest($request); - $tags = $prj->getTagCloud('wiki'); return Pluf_Shortcuts_RenderToResponse('idf/wiki/index.html', array( 'page_title' => $title, 'label' => $tag, 'pages' => $pag, - 'tags' => $tags, 'dlabel' => $dtag, ), $request); diff --git a/src/IDF/templates/idf/review/index.html b/src/IDF/templates/idf/review/index.html index d73dbaa..876f606 100644 --- a/src/IDF/templates/idf/review/index.html +++ b/src/IDF/templates/idf/review/index.html @@ -5,17 +5,4 @@ {if !$user.isAnonymous()} {aurl 'url', 'IDF_Views_Review::create', array($project.shortname)}

+ {trans 'Start Code Review'}

{/if} - {/block} -{block context} -{* -{aurl 'open_url', 'IDF_Views_Issue::index', array($project.shortname)} -{aurl 'closed_url', 'IDF_Views_Issue::listStatus', array($project.shortname, 'closed')} -{blocktrans}

Open issues: {$open}

-

Closed issues: {$closed}

{/blocktrans} -{assign $class = ''}{assign $i = 0} -

{foreach $project.getTagCloud($cloud) as $label} -{aurl 'url', 'IDF_Views_Issue::listLabel', array($project.shortname, $label.id, 'open')} -{if $class != $label.class}{if $i != 0}
{/if}{$label.class}: {/if} -{$label.name},{assign $class = $label.class}{assign $i = $i + 1}{/foreach}

-*}{/block}