From 6e305eb5418a8690b9b6f5de2a0b457954376a88 Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Sat, 24 Dec 2011 02:45:01 +0100 Subject: [PATCH] Add filtering and sorting capabilities to the project list page and also render the project activity with a small bar graph below the logo image. Sanitize the tagcloud css. --- src/IDF/Forge.php | 19 ++++++++ src/IDF/Project.php | 10 +++-- src/IDF/Tag.php | 12 +++++ src/IDF/Views.php | 45 ++++++++++++++----- src/IDF/conf/urls.php | 2 +- src/IDF/templates/idf/issues/search.html | 4 +- src/IDF/templates/idf/listProjects.html | 56 ++++++++++++++++++------ src/IDF/templates/idf/tags-cloud.html | 4 +- www/media/idf/css/style.css | 28 +++++++++--- 9 files changed, 138 insertions(+), 42 deletions(-) diff --git a/src/IDF/Forge.php b/src/IDF/Forge.php index 97ce9c1..fc4d430 100644 --- a/src/IDF/Forge.php +++ b/src/IDF/Forge.php @@ -51,4 +51,23 @@ class IDF_Forge public function setProjectLabels($labels) { $this->conf->setVal('project_labels', $labels); } + + public function getProjectLabelsWithCounts() { + $sql = new Pluf_SQL('project=0'); + $tagList = Pluf::factory('IDF_Tag')->getList(array( + 'filter' => $sql->gen(), + 'view' => 'join_projects', + 'order' => 'class ASC, lcname ASC' + )); + + $tags = array(); + foreach ($tagList as $tag) { + // group by class + if (!array_key_exists($tag->class, $tags)) { + $tags[$tag->class] = array(); + } + $tags[$tag->class][] = $tag; + } + return $tags; + } } \ No newline at end of file diff --git a/src/IDF/Project.php b/src/IDF/Project.php index fc8a340..8cafd2c 100644 --- a/src/IDF/Project.php +++ b/src/IDF/Project.php @@ -108,13 +108,15 @@ class IDF_Project extends Pluf_Model 'verbose' => __('current project activity'), ), ); - $table = $this->_con->pfx.'idf_projectactivities'; + $activityTable = $this->_con->pfx.'idf_projectactivities'; + $tagTable = $this->_con->pfx.'idf_project_idf_tag_assoc'; $this->_a['views'] = array( - 'join_activities' => + 'join_activities_and_tags' => array( - 'join' => 'LEFT JOIN '.$table - .' ON current_activity='.$table.'.id', + 'join' => 'LEFT JOIN '.$activityTable.' ON current_activity='.$activityTable.'.id ' + .'LEFT JOIN '.$tagTable.' ON idf_project_id='.$this->getSqlTable().'.id', 'select' => $this->getSelect().', date, value', + 'group' => $this->getSqlTable().'.id', 'props' => array( 'date' => 'current_activity_date', 'value' => 'current_activity_value' diff --git a/src/IDF/Tag.php b/src/IDF/Tag.php index 24441a6..b9fa25d 100644 --- a/src/IDF/Tag.php +++ b/src/IDF/Tag.php @@ -75,6 +75,18 @@ class IDF_Tag extends Pluf_Model ), ); + $table = $this->_con->pfx.'idf_project_idf_tag_assoc'; + $this->_a['views'] = array( + 'join_projects' => + array( + 'join' => 'LEFT JOIN '.$table + .' ON idf_tag_id=id', + 'select' => $this->getSelect().',COUNT(idf_project_id) as project_count', + 'group' => 'idf_tag_id', + 'props' => array('project_count' => 'project_count'), + ), + ); + $this->_a['idx'] = array( 'lcname_idx' => array( diff --git a/src/IDF/Views.php b/src/IDF/Views.php index bd1569f..3eaa01c 100644 --- a/src/IDF/Views.php +++ b/src/IDF/Views.php @@ -38,6 +38,7 @@ class IDF_Views { // TODO: add a switch here later on to determine whether the project list // or a custom start page should be displayed + $match = array('', 'all', 'name'); return $this->listProjects($request, $match); } @@ -47,15 +48,30 @@ class IDF_Views * Only the public projects are listed or the private with correct * rights. */ - public function listProjects($request, $match, $api=false) + public function listProjects($request, $match) { - $projects = self::getProjects($request->user); - $stats = self::getProjectsStatistics($projects); + list(, $tagId, $order) = $match; + + $tag = false; + if ($tagId !== 'all') { + $tag = Pluf::factory('IDF_Tag')->get($match[1]); + // ignore non-global tags + if ($tag !== false && $tag->project > 0) { + $tag = false; + } + } + $order = in_array($order, array('name', 'activity')) ? $order : 'name'; + + $projects = self::getProjects($request->user, $tag, $order); + $stats = self::getProjectsStatistics($projects); + $projectLabels = IDF_Forge::instance()->getProjectLabelsWithCounts(); - if ($api == true) return $projects; return Pluf_Shortcuts_RenderToResponse('idf/listProjects.html', array('page_title' => __('Projects'), 'projects' => $projects, + 'projectLabels' => $projectLabels, + 'tag' => $tag, + 'order' => $order, 'stats' => new Pluf_Template_ContextVars($stats)), $request); } @@ -335,16 +351,20 @@ class IDF_Views } /** - * Returns a list of projects accessible for the user. + * Returns a list of projects accessible for the user and optionally filtered by tag. * * @param Pluf_User + * @param IDF_Tag * @return ArrayObject IDF_Project */ - public static function getProjects($user) + public static function getProjects($user, $tag = false, $order = 'name') { $db =& Pluf::db(); $false = Pluf_DB_BooleanToDb(false, $db); $sql = new Pluf_SQL(1); + if ($tag !== false) { + $sql->SAnd(new Pluf_SQL('idf_tag_id=%s', $tag->id)); + } if ($user->isAnonymous()) { @@ -373,10 +393,14 @@ class IDF_Views $sql->SAnd($authSql); } + $orderTypes = array( + 'name' => 'name ASC', + 'activity' => 'value DESC, name ASC', + ); return Pluf::factory('IDF_Project')->getList(array( 'filter'=> $sql->gen(), - 'view' => 'join_activities', - 'order' => 'name ASC' + 'view' => 'join_activities_and_tags', + 'order' => $orderTypes[$order], )); } @@ -397,7 +421,7 @@ class IDF_Views // Count for each projects foreach ($projects as $p) { - $pstats = $p->getStats (); + $pstats = $p->getStats(); $forgestats['downloads'] += $pstats['downloads']; $forgestats['reviews'] += $pstats['reviews']; $forgestats['issues'] += $pstats['issues']; @@ -405,9 +429,6 @@ class IDF_Views $forgestats['commits'] += $pstats['commits']; } - // Count projects - $forgestats['projects'] = count($projects); - // Count members $sql = new Pluf_SQL('first_name != %s', array('---')); $forgestats['members'] = Pluf::factory('Pluf_User') diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index 1755ae6..5995e5b 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -29,7 +29,7 @@ $ctl[] = array('regex' => '#^/$#', 'model' => 'IDF_Views', 'method' => 'index'); -$ctl[] = array('regex' => '#^/label/(\d+)/$#', +$ctl[] = array('regex' => '#^/label/(\w+)/(\w+)/$#', 'base' => $base, 'model' => 'IDF_Views', 'method' => 'listProjects'); diff --git a/src/IDF/templates/idf/issues/search.html b/src/IDF/templates/idf/issues/search.html index 4c0b720..4ffce24 100644 --- a/src/IDF/templates/idf/issues/search.html +++ b/src/IDF/templates/idf/issues/search.html @@ -22,11 +22,11 @@ {$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} +
{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}

+{/foreach}{/foreach}
{/if} {/block} diff --git a/src/IDF/templates/idf/listProjects.html b/src/IDF/templates/idf/listProjects.html index 52cb37b..dccb732 100644 --- a/src/IDF/templates/idf/listProjects.html +++ b/src/IDF/templates/idf/listProjects.html @@ -38,9 +38,9 @@

{trans 'Labels:'} {assign $tags = $p.get_tags_list()} {if count($tags) == 0}{trans 'n/a'}{else} - {foreach $p.get_tags_list() as $idx => $tag} + {foreach $p.get_tags_list() as $idx => $label} {if $idx != 0}, {/if} - {$tag} + {$label} {/foreach} {/if}

@@ -48,18 +48,46 @@ {/foreach} {/if} {/block} + {block context} -
-

{trans 'Forge statistics'}

- - - - - - - - -
{trans 'Projects:'}{$stats.projects}
{trans 'Members:'}{$stats.members}
{trans 'Issues:'}{$stats.issues}
{trans 'Commits:'}{$stats.commits}
{trans 'Documentations:'}{$stats.docpages}
{trans 'Downloads:'}{$stats.downloads}
{trans 'Code reviews:'}{$stats.reviews}
-
+{trans 'Filter projects by label'} +{if count($projectLabels) == 0} +

{trans 'No projects with labels found.'}

+{else} +
{foreach $projectLabels as $class => $labels} +
{$class}
+ {foreach $labels as $idx => $label} +
{$label.name}{if $idx != count($labels) - 1},{/if}
+ {/foreach} + {/foreach}
+ {if $tag} +

+ {blocktrans}Remove filter for {$tag}{/blocktrans}

+ {/if} +{/if} +
+ +{trans 'Order'} +{assign $labelPart = 'all'} +{if $tag}{assign $labelPart = $tag->id}{/if} +

+{if $order != 'name'}{/if} +{trans 'By name'}{if $order != 'name'}{/if} + – +{if $order != 'activity'}{/if} +{trans 'By activity'}{if $order != 'activity'}{/if} +

+
+ +{trans 'Filtered project stats'} +
+
{trans 'Members:'}
{$stats.members}
+
{trans 'Issues:'}
{$stats.issues}
+
{trans 'Commits:'}
{$stats.commits}
+
{trans 'Documentations:'}
{$stats.docpages}
+
{trans 'Downloads:'}
{$stats.downloads}
+
{trans 'Code reviews:'}
{$stats.reviews}
+
{/block} + {block foot}
Powered by InDefero,
a CĂ©ondo Ltd initiative.
{/block} diff --git a/src/IDF/templates/idf/tags-cloud.html b/src/IDF/templates/idf/tags-cloud.html index e230268..7034c6c 100644 --- a/src/IDF/templates/idf/tags-cloud.html +++ b/src/IDF/templates/idf/tags-cloud.html @@ -1,6 +1,6 @@ -
{foreach $project.getTagCloud($cloud) as $class => $labels} +
{foreach $project.getTagCloud($cloud) as $class => $labels}
{$class}
{foreach $labels as $idx => $label} {aurl 'url', $cloud_url, array($project.shortname, $label.id, 'open')}
{$label.name}{if $idx != count($labels) - 1},{/if}
-{/foreach}{/foreach}

+{/foreach}{/foreach}
diff --git a/www/media/idf/css/style.css b/www/media/idf/css/style.css index 7e6f860..d8c43f1 100644 --- a/www/media/idf/css/style.css +++ b/www/media/idf/css/style.css @@ -213,23 +213,37 @@ span.px-header-title a, span.px-header-title a:link, span.px-header-title a:visi color: #000; } -/** - * Issue - */ -#tagscloud dl { - margin: 0; +dl.tagscloud, +dl.statistics { + margin: 0; + margin-bottom: 1em; } -#tagscloud dt { +dl.tagscloud dt { margin-top: .5em; font-weight: bold; } -#tagscloud dd { +dl.tagscloud dd { margin: 0; display: inline; } +dl.statistics dt { + margin-top: .5em; + font-style: italic; +} + +dl.statistics dd { + margin: 0; + float: right; + margin-top: -1.2em; + margin-right: 2em; +} + +/** + * Issue + */ a.issue-c { text-decoration: line-through; }