Added ticket 45, base implementation of a timeline.

Still some cleaning of the code to have a nicer display of the timeline
especially for the issue updates.
dev
Loic d'Anterroches 2008-11-14 15:41:51 +01:00
parent 386ff894fc
commit b85da85dfe
13 changed files with 585 additions and 32 deletions

172
src/IDF/Commit.php 100644
View File

@ -0,0 +1,172 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2008 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
Pluf::loadFunction('Pluf_HTTP_URL_urlForView');
/**
* Base definition of a commit.
*
* By having a reference in the database for each commit, one can
* easily generate a timeline or use the search engine. Commit details
* are normally always taken from the underlining SCM.
*/
class IDF_Commit extends Pluf_Model
{
public $_model = __CLASS__;
function init()
{
$this->_a['table'] = 'idf_commits';
$this->_a['model'] = __CLASS__;
$this->_a['cols'] = array(
// It is mandatory to have an "id" column.
'id' =>
array(
'type' => 'Pluf_DB_Field_Sequence',
'blank' => true,
),
'project' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Project',
'blank' => false,
'verbose' => __('project'),
'relate_name' => 'commits',
),
'author' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
'is_null' => true,
'verbose' => __('submitter'),
'relate_name' => 'submitted_commit',
'help_text' => 'This will allow us to list the latest commits of a user in its profile.',
),
'origauthor' =>
array(
'type' => 'Pluf_DB_Field_Varchar',
'blank' => false,
'size' => 150,
'help_text' => 'As we do not necessary have the mapping between the author in the database and the scm, we store the scm author commit information here. That way we can update the author info later in the process.',
),
'scm_id' =>
array(
'type' => 'Pluf_DB_Field_Varchar',
'blank' => false,
'size' => 50,
'index' => true,
'help_text' => 'The id of the commit. For git, it will be the SHA1 hash, for subversion it will be the revision id.',
),
'summary' =>
array(
'type' => 'Pluf_DB_Field_Varchar',
'blank' => false,
'size' => 250,
'verbose' => __('summary'),
),
'fullmessage' =>
array(
'type' => 'Pluf_DB_Field_Compressed',
'blank' => true,
'verbose' => __('changelog'),
'help_text' => 'This is the full message of the commit.',
),
'creation_dtime' =>
array(
'type' => 'Pluf_DB_Field_Datetime',
'blank' => true,
'verbose' => __('creation date'),
'index' => true,
'help_text' => 'Date of creation by the scm',
),
);
}
function __toString()
{
return $this->summary.' - ('.$this->scm_id.')';
}
function _toIndex()
{
$str = str_repeat($this->summary.' ', 4).' '.$this->fullmessage;
return Pluf_Text::cleanString(html_entity_decode($str, ENT_QUOTES, 'UTF-8'));
}
function postSave($create=false)
{
IDF_Search::index($this);
if ($create) {
IDF_Timeline::insert($this, $this->get_project(),
$this->get_author(), $this->creation_dtime);
}
}
/**
* Create a commit from a simple class commit info of a changelog.
*
* @param stdClass Commit info
* @param IDF_Project Current project
* @return IDF_Commit
*/
public static function getOrAdd($change, $project)
{
$sql = new Pluf_SQL('project=%s AND scm_id=%s',
array($project->id, $change->commit));
$r = Pluf::factory('IDF_Commit')->getList(array('filter'=>$sql->gen()));
if ($r->count()) {
return $r[0];
}
$commit = new IDF_Commit();
$commit->project = $project;
$commit->scm_id = $change->commit;
$commit->summary = $change->title;
$commit->fullmessage = $change->full_message;
$commit->author = null;
$commit->origauthor = $change->author;
$commit->creation_dtime = $change->date;
$commit->create();
return $commit;
}
/**
* Returns the timeline fragment for the commit.
*
*
* @param Pluf_HTTP_Request
* @return Pluf_Template_SafeString
*/
public function timelineFragment($request)
{
$tag = new IDF_Template_IssueComment();
$out = $tag->start($this->summary, $request, false);
if ($this->fullmessage) {
$out .= '<br /><br />'.$tag->start($this->fullmessage, $request, false);
}
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Source::commit',
array($request->project->shortname,
$this->scm_id));
$out .= '<div class="helptext right"><br />'.__('Commit:').'&nbsp;<a href="'.$url.'" class="mono">'.$this->scm_id.'</a>, '.__('by').' '.strip_tags($this->origauthor).'</div>';
return Pluf_Template::markSafe($out);
}
}

View File

@ -21,6 +21,8 @@
#
# ***** END LICENSE BLOCK ***** */
Pluf::loadFunction('Pluf_HTTP_URL_urlForView');
/**
* Base definition of an issue.
*
@ -152,5 +154,30 @@ class IDF_Issue extends Pluf_Model
function postSave($create=false)
{
IDF_Search::index($this);
if ($create) {
IDF_Timeline::insert($this, $this->get_project(),
$this->get_submitter());
}
}
/**
* Returns an HTML fragment used to display this issue in the
* timeline.
*
* The request object is given to be able to check the rights and
* as such create links to other items etc. You can consider that
* if displayed, you can create a link to it.
*
* @param Pluf_HTTP_Request
* @return Pluf_Template_SafeString
*/
public function timelineFragment($request)
{
$submitter = $this->get_submitter();
$ic = (in_array($this->status, $request->project->getTagIdsByStatus('closed'))) ? 'issue-c' : 'issue-o';
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
array($request->project->shortname,
$this->id));
return Pluf_Template::markSafe(sprintf(__('<a href="%1$s" class="%2$s" title="View issue">Issue %3$d</a> <em>%4$s</em> created by %5$s'), $url, $ic, $this->id, Pluf_esc($this->summary), Pluf_esc($submitter)));
}
}

View File

@ -107,5 +107,27 @@ class IDF_IssueComment extends Pluf_Model
function postSave($create=false)
{
if ($create) {
// Check if more than one comment for this issue. We do
// not want to insert the first comment in the timeline as
// the issue itself is inserted.
$sql = new Pluf_SQL('issue=%s', array($this->issue));
$co = Pluf::factory('IDF_IssueComment')->getList(array('filter'=>$sql->gen()));
if ($co->count() > 1) {
IDF_Timeline::insert($this, $this->get_issue()->get_project(),
$this->get_submitter());
}
}
}
public function timelineFragment($request)
{
$submitter = $this->get_submitter();
$issue = $this->get_issue();
$ic = (in_array($issue->status, $request->project->getTagIdsByStatus('closed'))) ? 'issue-c' : 'issue-o';
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
array($request->project->shortname,
$issue->id));
return Pluf_Template::markSafe(sprintf(__('<a href="%1$s" class="%2$s" title="View issue">Issue %3$d</a> <em>%4$s</em> updated by %5$s'), $url, $ic, $issue->id, Pluf_esc($issue->summary), Pluf_esc($submitter)));
}
}

View File

@ -0,0 +1,54 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2008 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Add the download of files.
*/
function IDF_Migrations_4Timeline_up($params=null)
{
$models = array(
'IDF_Commit',
'IDF_Timeline',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);
foreach ($models as $model) {
$schema->model = new $model();
$schema->createTables();
}
}
function IDF_Migrations_4Timeline_down($params=null)
{
$models = array(
'IDF_Timeline',
'IDF_Commit',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);
foreach ($models as $model) {
$schema->model = new $model();
$schema->dropTables();
}
}

View File

@ -32,7 +32,7 @@ class IDF_Template_IssueComment extends Pluf_Template_Tag
private $request = null;
private $scm = null;
function start($text, $request)
function start($text, $request, $echo=true)
{
$this->project = $request->project;
$this->request = $request;
@ -50,7 +50,11 @@ class IDF_Template_IssueComment extends Pluf_Template_Tag
$text = preg_replace_callback('#(commit\s+)([0-9a-f]{1,40})#im',
array($this, 'callbackCommit'), $text);
}
echo $text;
if ($echo) {
echo $text;
} else {
return $text;
}
}
/**

View File

@ -0,0 +1,34 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2008 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Make the links to issues and commits.
*/
class IDF_Template_TimelineFragment extends Pluf_Template_Tag
{
function start($item, $request)
{
$m = Pluf::factory($item->model_class, $item->model_id);
echo $m->timelineFragment($request);
}
}

View File

@ -0,0 +1,133 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2008 Céondo Ltd and contributors.
#
# InDefero is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# InDefero is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Log of what is going on in the project.
*
* We log here what is going on. It is important that creation_dtime
* must be set at the *real* date time of the creation of the object,
* which is not necessarily the date of the insert in the
* database. For example, code can be created 3 days ago and committed
* in the main repository.
*
* The public_dtime is the date at which the information is being made
* public and here that would be the commit date time of the code.
*
*/
class IDF_Timeline extends Pluf_Model
{
public $_model = __CLASS__;
function init()
{
$this->_a['table'] = 'idf_timeline';
$this->_a['model'] = __CLASS__;
$this->_a['cols'] = array(
// It is mandatory to have an "id" column.
'id' =>
array(
'type' => 'Pluf_DB_Field_Sequence',
'blank' => true,
),
'project' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Project',
'blank' => false,
'relate_name' => 'thumbroll',
),
'author' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
'is_null' => true,
'help_text' => 'This will allow us to list the latest commits of a user in its profile.',
),
'model_class' =>
array(
'type' => 'Pluf_DB_Field_Varchar',
'blank' => false,
'size' => 150,
),
'model_id' =>
array(
'type' => 'Pluf_DB_Field_Integer',
'blank' => false,
),
'creation_dtime' =>
array(
'type' => 'Pluf_DB_Field_Datetime',
'blank' => true,
'index' => true,
),
'public_dtime' =>
array(
'type' => 'Pluf_DB_Field_Datetime',
'blank' => true,
'index' => true,
),
);
}
function __toString()
{
return $this->summary.' - ('.$this->scm_id.')';
}
function _toIndex()
{
$str = str_repeat($this->summary.' ', 4).' '.$this->fullmessage;
return Pluf_Text::cleanString(html_entity_decode($str, ENT_QUOTES, 'UTF-8'));
}
function preSave($create=false)
{
if ($this->id == '') {
$this->public_dtime = gmdate('Y-m-d H:i:s');
}
if ($this->creation_dtime == '') {
$this->creation_dtime = gmdate('Y-m-d H:i:s');
}
}
/**
* Easily insert an item in the timeline.
*
* @param mixed Item to be inserted
* @param IDF_Project Project of the item
* @param Pluf_User Author of the item (null)
* @param string GMT creation date time (null)
* @return bool Success
*/
public static function insert($item, $project, $author=null, $creation=null)
{
$t = new IDF_Timeline();
$t->project = $project;
$t->author = $author;
$t->creation_dtime = (is_null($creation)) ? '' : $creation;
$t->model_id = $item->id;
$t->model_class = $item->_model;
$t->create();
return true;
}
}

View File

@ -45,7 +45,7 @@ class IDF_Views_Project
// the first tag is the featured, the last is the deprecated.
$downloads = $tags[0]->get_idf_upload_list();
}
return Pluf_Shortcuts_RenderToResponse('project-home.html',
return Pluf_Shortcuts_RenderToResponse('project/home.html',
array(
'page_title' => $title,
'team' => $team,
@ -54,6 +54,32 @@ class IDF_Views_Project
$request);
}
/**
* Timeline of the project.
*/
public function timeline($request, $match)
{
$prj = $request->project;
$title = sprintf(__('%s Timeline'), (string) $prj);
$team = $prj->getMembershipData();
$sql = new Pluf_SQL('project=%s', array($prj->id));
$timeline = Pluf::factory('IDF_Timeline')->getList(array('filter'=>$sql->gen(), 'order' => 'creation_dtime DESC'));
$downloads = array();
if ($request->rights['hasDownloadsAccess']) {
$tags = IDF_Views_Download::getDownloadTags($prj);
// the first tag is the featured, the last is the deprecated.
$downloads = $tags[0]->get_idf_upload_list();
}
return Pluf_Shortcuts_RenderToResponse('project/timeline.html',
array(
'page_title' => $title,
'timeline' => $timeline,
'team' => $team,
'downloads' => $downloads,
),
$request);
}
/**
* Administrate the summary of a project.

View File

@ -46,13 +46,18 @@ class IDF_Views_Source
$branches[0]));
return new Pluf_HTTP_Response_Redirect($url);
}
$res = new Pluf_Template_ContextVars($scm->getChangeLog($commit, 25));
$changes = $scm->getChangeLog($commit, 25);
// Sync with the database
foreach ($changes as $change) {
IDF_Commit::getOrAdd($change, $request->project);
}
$changes = new Pluf_Template_ContextVars($changes);
$scmConf = $request->conf->getVal('scm', 'git');
return Pluf_Shortcuts_RenderToResponse('source/changelog.html',
array(
'page_title' => $title,
'title' => $title,
'changes' => $res,
'changes' => $changes,
'commit' => $commit,
'branches' => $branches,
'scm' => $scmConf,

View File

@ -26,18 +26,10 @@ $cfg = array();
// to start with, it can be practical.
$cfg['debug'] = false;
// available languages
$cfg['languages'] = array('en', 'fr');
# SCM base configuration
$cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git',
'svn' => 'IDF_Scm_Svn',
);
// if you have a single git repository, just put the full path to it
// without trailing slash.
// If within a folder you have a series of git repository, just put
// the folder without a trailing slash.
// If within a folder you have a series of bare git repository, just
// put the folder without a trailing slash.
// InDefero will automatically append a slash, the project shortname
// and .git to create the name of the repository.
$cfg['git_repositories'] = '/home/git/repositories/indefero.git';
@ -51,11 +43,25 @@ $cfg['git_remote_url'] = 'git://projects.ceondo.com/indefero.git';
//$cfg['git_remote_url'] = 'git://projects.ceondo.com';
// Same as for git, you can have multiple repositories, one for each
// project or a single one for all the projects.
// project or a single one for all the projects.
//
// In the case of subversion, the admin of a project can also select a
// remote repository from the web interface. From the web interface
// you can define a local repository, local repositories are defined
// here. This if for security reasons.
$cfg['svn_repositories'] = 'file:///home/svn/repositories/indefero';
$cfg['svn_repositories_unique'] = true;
$cfg['svn_remote_url'] = 'http://projects.ceondo.com/svn/indefero';
// Example of one *local* subversion repository for each project:
// the path to the repository on disk will automatically created to be
// 'file:///home/svn/repositories'.'/'.$project->shortname
// the url will be generated the same way:
// 'http://projects.ceondo.com/svn'.'/'.$project->shortname
// $cfg['svn_repositories'] = 'file:///home/svn/repositories';
// $cfg['svn_repositories_unique'] = false;
// $cfg['svn_remote_url'] = 'http://projects.ceondo.com/svn';
// admins will get an email in case of errors in the system in non
// debug mode.
@ -68,7 +74,6 @@ $cfg['send_emails'] = true;
$cfg['mail_backend'] = 'smtp';
$cfg['mail_host'] = 'localhost';
$cfg['mail_port'] = 25;
$cfg['pear_path'] = '/usr/share/php';
// Paths/Url configuration
#
@ -94,9 +99,6 @@ $cfg['upload_path'] = '/path/to/media/upload';
#
$cfg['upload_issue_path'] = '/path/to/attachments';
$cfg['login_success_url'] = $cfg['url_base'].$cfg['idf_base'];
$cfg['after_logout_page'] = $cfg['url_base'].$cfg['idf_base'];
// write here a long random string unique for this installation. This
// is critical to put a long string.
$cfg['secret_key'] = '';
@ -113,6 +115,23 @@ $cfg['bounce_email'] = 'no-reply@example.com';
// It is mandatory if you are using the template system.
$cfg['tmp_folder'] = '/tmp';
// Database configuration
// For testing we are using in memory SQLite database.
$cfg['db_login'] = 'www';
$cfg['db_password'] = '';
$cfg['db_server'] = '';
$cfg['db_version'] = '';
$cfg['db_table_prefix'] = '';
$cfg['db_engine'] = 'PostgreSQL'; // SQLite is also well tested or MySQL
$cfg['db_database'] = 'website'; // put absolute path to the db if you
// are using SQLite
// -- From this point you should not need to update anything. --
$cfg['pear_path'] = '/usr/share/php';
$cfg['login_success_url'] = $cfg['url_base'].$cfg['idf_base'];
$cfg['after_logout_page'] = $cfg['url_base'].$cfg['idf_base'];
// Caching of the scm commands.
$cfg['cache_engine'] = 'Pluf_Cache_File';
$cfg['cache_timeout'] = 300;
@ -123,17 +142,6 @@ $cfg['template_folders'] = array(
dirname(__FILE__).'/../templates',
);
// Database configuration
// For testing we are using in memory SQLite database.
$cfg['db_login'] = 'www';
$cfg['db_password'] = '';
$cfg['db_server'] = '';
$cfg['db_version'] = '';
$cfg['db_table_prefix'] = '';
$cfg['db_engine'] = 'PostgreSQL'; // SQLite is also well tested or MySQL
$cfg['db_database'] = 'website';
// From this point you should not need to update anything.
$cfg['installed_apps'] = array('Pluf', 'IDF');
$cfg['pluf_use_rowpermission'] = true;
$cfg['middleware_classes'] = array(
@ -146,9 +154,20 @@ $cfg['idf_views'] = dirname(__FILE__).'/views.php';
$cfg['template_tags'] = array(
'hotkey' => 'IDF_Template_HotKey',
'issuetext' => 'IDF_Template_IssueComment',
'timeline' => 'IDF_Template_TimelineFragment',
);
$cfg['template_modifiers'] = array(
'size' => 'IDF_Views_Source_PrettySize',
'markdown' => 'IDF_Template_Markdown_filter',
);
// available languages
$cfg['languages'] = array('en', 'fr');
# SCM base configuration
$cfg['allowed_scm'] = array('git' => 'IDF_Scm_Git',
'svn' => 'IDF_Scm_Svn',
);
return $cfg;

View File

@ -85,6 +85,12 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/$#',
'model' => 'IDF_Views_Project',
'method' => 'home');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/timeline/$#',
'base' => $base,
'priority' => 4,
'model' => 'IDF_Views_Project',
'method' => 'timeline');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/$#',
'base' => $base,
'priority' => 4,

View File

@ -3,7 +3,7 @@
{block tabhome} class="active"{/block}
{block subtabs}
<div id="sub-tabs">
{trans 'Welcome'} {superblock}
{trans 'Welcome'} | <a href="{url 'IDF_Views_Project::timeline', array($project.shortname)}">{trans 'Latest Changes'}</a>{superblock}
</div>
{/block}
{block body}

View File

@ -0,0 +1,51 @@
{extends "base.html"}
{block docclass}yui-t2{/block}
{block tabhome} class="active"{/block}
{block subtabs}
<div id="sub-tabs">
<a href="{url 'IDF_Views_Project::home', array($project.shortname)}">{trans 'Welcome'}</a> | {trans 'Latest Changes'}{superblock}
</div>
{/block}
{block body}
<table summary="" class="tree-list">
<thead>
<tr>
<th>{trans 'Age'}</th>
<th>{trans 'Change'}</th>
</tr>
</thead>
<tbody>
{foreach $timeline as $item}
<tr class="log">
<td>{$item.creation_dtime|dateago:"wihtout"}</td>
<td>{timeline $item, $request}</td>
</tr>
{/foreach}
</tbody>
</table>
{/block}
{block context}
{if count($downloads) > 0}
<p><strong>{trans 'Featured Downloads'}</strong><br />
{foreach $downloads as $download}
<span class="label"><a href="{url 'IDF_Views_Download::view', array($project.shortname, $download.id)}" title="{$download.summary}">{$download}</a></span><br />
{/foreach}
<span class="label"> </span><span class="note"><a href="{url 'IDF_Views_Download::index', array($project.shortname)}">{trans 'show more...'}</a></span>
{/if}
{assign $ko = 'owners'}
{assign $km = 'members'}
<p><strong>{trans 'Development Team'}</strong><br />
{trans 'Admins'}<br />
{foreach $team[$ko] as $owner}{aurl 'url', 'IDF_Views_User::view', array($owner.login)}
<span class="label"><a class="label" href="{$url}">{$owner}</a></span><br />
{/foreach}
{if count($team[$km]) > 0}
{trans 'Happy Crew'}<br />
{foreach $team[$km] as $member}{aurl 'url', 'IDF_Views_User::view', array($member.login)}
<span class="label"><a class="label" href="{$url}">{$member}</a></span><br />
{/foreach}
{/if}
</p>
{/block}