Rework the notification functionality in all areas:

- for object changes in each tab / section we send out notifications to
  project owners, members and / or additional addresses (all this is
  configurable) (fixes issues 334, 452, 480 and possible others)
- one can now also receive notifications about download updates
- the notification template that informs about issue updates is no
  longer confusing the reader with the "a new issue has been created
  and assigned to you" phrase if the user who is notified is not
  actually the (new) owner (fixes issue 562)
- send-out notification emails for reviews, wiki updates and review
  updates are now linked via a unique message id to support a threaded
  view in email clients like Thunderbird (this was previously only
  implemented for issue notifications for issue 414)

This commit has been sponsored by SciLab.
This commit is contained in:
Thomas Keller 2011-11-05 14:15:43 +01:00
parent 2b5efb7fee
commit 82a2d6a39c
11 changed files with 336 additions and 230 deletions

View File

@ -312,33 +312,42 @@ class IDF_Commit extends Pluf_Model
$item->payload = $payload;
$item->create();
if ('' == $conf->getVal('source_notification_email', '')) {
return;
}
$current_locale = Pluf_Translation::getLocale();
$langs = Pluf::f('languages', array('en'));
Pluf_Translation::loadSetLocale($langs[0]);
$context = new Pluf_Template_Context(
array(
'c' => $this,
'project' => $this->get_project(),
'url_base' => Pluf::f('url_base'),
)
);
$tmpl = new Pluf_Template('idf/source/commit-created-email.txt');
$text_email = $tmpl->render($context);
$addresses = explode(',', $conf->getVal('source_notification_email'));
foreach ($addresses as $address) {
$email = new Pluf_Mail(Pluf::f('from_email'),
$from_email = Pluf::f('from_email');
$recipients = $project->getNotificationRecipientsForTab('source');
foreach ($recipients as $address => $language) {
if (!empty($this->author) && $this->author->email === $address) {
continue;
}
Pluf_Translation::loadSetLocale($language);
$context = new Pluf_Template_Context(array(
'commit' => $this,
'project' => $project,
'url_base' => Pluf::f('url_base'),
));
// commits are usually not updated, therefor we do not
// distinguish between create and update here
$tplfile = 'idf/source/commit-created-email.txt';
$subject = __('New commit %s - %s (%s)');
$tmpl = new Pluf_Template($tplfile);
$text_email = $tmpl->render($context);
$email = new Pluf_Mail($from_email,
$address,
sprintf(__('New Commit %s - %s (%s)'),
sprintf($subject,
$this->scm_id, $this->summary,
$this->get_project()->shortname));
$project->shortname));
$email->addTextMessage($text_email);
$email->sendMail();
}
Pluf_Translation::loadSetLocale($current_locale);
}
}

View File

@ -256,91 +256,72 @@ class IDF_Issue extends Pluf_Model
*/
public function notify($conf, $create=true)
{
$prj = $this->get_project();
$to_email = array();
if ('' != $conf->getVal('issues_notification_email', '')) {
$langs = Pluf::f('languages', array('en'));
$addresses = explode(',', $conf->getVal('issues_notification_email'));
foreach ($addresses as $address) {
$to_email[] = array($address, $langs[0]);
}
}
$project = $this->get_project();
$current_locale = Pluf_Translation::getLocale();
$id = '<'.md5($this->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>';
if ($create) {
if (null != $this->get_owner() and $this->owner != $this->submitter) {
$email_lang = array($this->get_owner()->email,
$this->get_owner()->language);
if (!in_array($email_lang, $to_email)) {
$to_email[] = $email_lang;
}
}
$comments = $this->get_comments_list(array('order' => 'id ASC'));
$context = new Pluf_Template_Context(
array(
'issue' => $this,
'comment' => $comments[0],
'project' => $prj,
'url_base' => Pluf::f('url_base'),
)
);
foreach ($to_email as $email_lang) {
Pluf_Translation::loadSetLocale($email_lang[1]);
$email = new Pluf_Mail(Pluf::f('from_email'), $email_lang[0],
sprintf(__('Issue %s - %s (%s)'),
$this->id, $this->summary, $prj->shortname));
$tmpl = new Pluf_Template('idf/issues/issue-created-email.txt');
$email->addTextMessage($tmpl->render($context));
$email->addHeaders(array('Message-ID'=>$id));
$email->sendMail();
}
} else {
$comments = $this->get_comments_list(array('order' => 'id DESC'));
$email_sender = '';
if (isset($comments[0])) {
$email_sender = $comments[0]->get_submitter()->email;
}
foreach ($this->get_interested_list() as $interested) {
$email_lang = array($interested->email,
$interested->language);
if (!in_array($email_lang, $to_email)) {
$to_email[] = $email_lang;
}
}
$email_lang = array($this->get_submitter()->email,
$this->get_submitter()->language);
if (!in_array($email_lang, $to_email)) {
$to_email[] = $email_lang;
}
if (null != $this->get_owner()) {
$email_lang = array($this->get_owner()->email,
$this->get_owner()->language);
if (!in_array($email_lang, $to_email)) {
$to_email[] = $email_lang;
}
}
$context = new Pluf_Template_Context(
array(
'issue' => $this,
'comments' => $comments,
'project' => $prj,
'url_base' => Pluf::f('url_base'),
));
foreach ($to_email as $email_lang) {
if ($email_lang[0] == $email_sender) {
continue; // Do not notify the one having created
// the comment
}
Pluf_Translation::loadSetLocale($email_lang[1]);
$email = new Pluf_Mail(Pluf::f('from_email'), $email_lang[0],
sprintf(__('Updated Issue %s - %s (%s)'),
$this->id, $this->summary, $prj->shortname));
$tmpl = new Pluf_Template('idf/issues/issue-updated-email.txt');
$email->addTextMessage($tmpl->render($context));
$email->addHeaders(array('References'=>$id));
$email->sendMail();
}
$from_email = Pluf::f('from_email');
$comments = $this->get_comments_list(array('order' => 'id DESC'));
$messageId = '<'.md5('issue'.$this->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>';
$recipients = $project->getNotificationRecipientsForTab('issues');
// the submitter (might be skipped later on if he is the one who also
// submitted the last comment)
if (!array_key_exists($this->get_submitter()->email, $recipients)) {
$recipients[$this->get_submitter()->email] = $this->get_submitter()->language;
}
// the owner of the issue, if we have one
$owner = $this->get_owner();
if (null != $owner && !array_key_exists($owner->email, $recipients)) {
$recipients[$owner->email] = $owner->language;
}
// additional users who starred the issue
foreach ($this->get_interested_list() as $interested) {
if (array_key_exists($interested->email, $recipients))
continue;
$recipients[$interested->email] = $interested->language;
}
foreach ($recipients as $address => $language) {
// do not notify the creator of the last comment,
// i.e. the user who triggered this notification
if ($comments[0]->get_submitter()->email === $address) {
continue;
}
Pluf_Translation::loadSetLocale($language);
$context = new Pluf_Template_Context(array(
'issue' => $this,
'owns_issue' => $owner !== null && $owner->email === $address,
// the initial comment for create, the last for update
'comment' => $comments[0],
'comments' => $comments,
'project' => $project,
'url_base' => Pluf::f('url_base'),
));
$tplfile = 'idf/issues/issue-created-email.txt';
$subject = __('Issue %s - %s (%s)');
$headers = array('Message-ID' => $messageId);
if (!$create) {
$tplfile = 'idf/issues/issue-updated-email.txt';
$subject = __('Updated Issue %s - %s (%s)');
$headers = array('References' => $messageId);
}
$tmpl = new Pluf_Template($tplfile);
$text_email = $tmpl->render($context);
$email = new Pluf_Mail($from_email, $address,
sprintf($subject, $this->id, $this->summary, $project->shortname));
$email->addTextMessage($text_email);
$email->addHeaders($headers);
$email->sendMail();
}
Pluf_Translation::loadSetLocale($current_locale);
}
}

View File

@ -796,4 +796,52 @@ GROUP BY uid";
$this->_isRestricted = false;
return false;
}
/**
* Returns an associative array of email addresses to notify about changes
* in a certain tab like 'issues', 'source', and so on.
*
* @param string $tab
* @return array Key is the email address, value is the preferred language setting
*/
public function getNotificationRecipientsForTab($tab)
{
if (!in_array($tab, array('source', 'issues', 'downloads', 'wiki', 'review'))) {
throw new Exception(sprintf('unknown tab %s', $tab));
}
$conf = $this->getConf();
$recipients = array();
$membership_data = $this->getMembershipData();
if ($conf->getVal($tab.'_notification_owners_enabled', false)) {
foreach ($membership_data['owners'] as $owner) {
$recipients[$owner->email] = $owner->language;
}
}
if ($conf->getVal($tab.'_notification_members_enabled', false)) {
foreach ($membership_data['members'] as $member) {
$recipients[$member->email] = $member->language;
}
}
if ($conf->getVal($tab.'_notification_email_enabled', false)) {
$addresses = preg_split('/\s*,\s*/',
$conf->getVal($tab.'_notification_email', ''),
-1, PREG_SPLIT_NO_EMPTY);
// we use a default language setting for this plain list of
// addresses, but we ensure that we do not overwrite an existing
// address which might come with a proper setting already
$languages = Pluf::f('languages', array('en'));
foreach ($addresses as $address) {
if (array_key_exists($address, $recipients))
continue;
$recipients[$address] = $languages[0];
}
}
return $recipients;
}
}

View File

@ -175,50 +175,63 @@ class IDF_Review_Comment extends Pluf_Model
*/
public function notify($conf, $create=true)
{
$patch = $this->get_patch();
$review = $patch->get_review();
$prj = $review->get_project();
$to_email = array();
if ('' != $conf->getVal('review_notification_email', '')) {
$langs = Pluf::f('languages', array('en'));
$to_email[] = array($conf->getVal('issues_notification_email'),
$langs[0]);
}
$current_locale = Pluf_Translation::getLocale();
$reviewers = $review->getReviewers();
$patch = $this->get_patch();
$review = $patch->get_review();
$prj = $review->get_project();
$reviewers = $review->getReviewers();
if (!Pluf_Model_InArray($review->get_submitter(), $reviewers)) {
$reviewers[] = $review->get_submitter();
}
$comments = $patch->getFileComments(array('order' => 'id DESC'));
$gcomments = $patch->get_comments_list(array('order' => 'id DESC'));
$context = new Pluf_Template_Context(
array(
'review' => $review,
'patch' => $patch,
'comments' => $comments,
'gcomments' => $gcomments,
'project' => $prj,
'url_base' => Pluf::f('url_base'),
)
);
// build the list of emails and lang
foreach ($reviewers as $user) {
$email_lang = array($user->email,
$user->language);
if (!in_array($email_lang, $to_email)) {
$to_email[] = $email_lang;
}
}
$tmpl = new Pluf_Template('idf/review/review-updated-email.txt');
foreach ($to_email as $email_lang) {
Pluf_Translation::loadSetLocale($email_lang[1]);
$email = new Pluf_Mail(Pluf::f('from_email'), $email_lang[0],
sprintf(__('Updated Code Review %s - %s (%s)'),
$review->id, $review->summary, $prj->shortname));
$email->addTextMessage($tmpl->render($context));
$recipients = $prj->getNotificationRecipientsForTab('review');
foreach ($reviewers as $user) {
if (array_key_exists($user->email, $recipients))
continue;
$recipients[$user->email] = $user->language;
}
$current_locale = Pluf_Translation::getLocale();
$from_email = Pluf::f('from_email');
$messageId = '<'.md5('review'.$review->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>';
foreach ($recipients as $address => $language) {
if ($this->get_submitter()->email === $address) {
continue;
}
Pluf_Translation::loadSetLocale($language);
$context = new Pluf_Template_Context(array(
'review' => $review,
'patch' => $patch,
'comments' => $comments,
'gcomments' => $gcomments,
'project' => $prj,
'url_base' => Pluf::f('url_base'),
));
// reviews only updated through comments, see IDF_Review_Patch::notify()
$tplfile = 'idf/review/review-updated-email.txt';
$subject = __('Updated Code Review %s - %s (%s)');
$headers = array('References' => $messageId);
$tmpl = new Pluf_Template($tplfile);
$text_email = $tmpl->render($context);
$email = new Pluf_Mail($from_email, $address,
sprintf($subject, $review->id, $review->summary, $prj->shortname));
$email->addTextMessage($text_email);
$email->addHeaders($headers);
$email->sendMail();
}
Pluf_Translation::loadSetLocale($current_locale);
}

View File

@ -42,9 +42,9 @@ class IDF_Review_Patch extends Pluf_Model
'id' =>
array(
'type' => 'Pluf_DB_Field_Sequence',
'blank' => true,
'blank' => true,
),
'review' =>
'review' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Review',
@ -59,7 +59,7 @@ class IDF_Review_Patch extends Pluf_Model
'size' => 250,
'verbose' => __('summary'),
),
'commit' =>
'commit' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Commit',
@ -129,8 +129,8 @@ class IDF_Review_Patch extends Pluf_Model
function postSave($create=false)
{
if ($create) {
IDF_Timeline::insert($this,
$this->get_review()->get_project(),
IDF_Timeline::insert($this,
$this->get_review()->get_project(),
$this->get_review()->get_submitter());
IDF_Search::index($this->get_review());
}
@ -139,7 +139,7 @@ class IDF_Review_Patch extends Pluf_Model
public function timelineFragment($request)
{
$review = $this->get_review();
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
array($request->project->shortname,
$review->id));
$out = '<tr class="log"><td><a href="'.$url.'">'.
@ -150,14 +150,14 @@ class IDF_Review_Patch extends Pluf_Model
$ic = (in_array($review->status, $request->project->getTagIdsByStatus('closed'))) ? 'issue-c' : 'issue-o';
$out .= sprintf(__('<a href="%1$s" class="%2$s" title="View review">Review %3$d</a>, %4$s'), $url, $ic, $review->id, Pluf_esc($review->summary)).'</td>';
$out .= "\n".'<tr class="extra"><td colspan="2">
<div class="helptext right">'.sprintf(__('Creation of <a href="%s" class="%s">review&nbsp;%d</a>, by %s'), $url, $ic, $review->id, $user).'</div></td></tr>';
<div class="helptext right">'.sprintf(__('Creation of <a href="%s" class="%s">review&nbsp;%d</a>, by %s'), $url, $ic, $review->id, $user).'</div></td></tr>';
return Pluf_Template::markSafe($out);
}
public function feedFragment($request)
{
$review = $this->get_review();
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
array($request->project->shortname,
$review->id));
$title = sprintf(__('%s: Creation of Review %d - %s'),
@ -179,35 +179,49 @@ class IDF_Review_Patch extends Pluf_Model
public function notify($conf, $create=true)
{
if ('' == $conf->getVal('review_notification_email', '')) {
return;
}
$review = $this->get_review();
$project = $review->get_project();
$current_locale = Pluf_Translation::getLocale();
$langs = Pluf::f('languages', array('en'));
Pluf_Translation::loadSetLocale($langs[0]);
$context = new Pluf_Template_Context(
array(
'review' => $this->get_review(),
'patch' => $this,
'comments' => array(),
'project' => $this->get_review()->get_project(),
'url_base' => Pluf::f('url_base'),
)
);
$tmpl = new Pluf_Template('idf/review/review-created-email.txt');
$text_email = $tmpl->render($context);
$addresses = explode(';',$conf->getVal('review_notification_email'));
foreach ($addresses as $address) {
$email = new Pluf_Mail(Pluf::f('from_email'),
$from_email = Pluf::f('from_email');
$messageId = '<'.md5('review'.$review->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>';
$recipients = $project->getNotificationRecipientsForTab('review');
foreach ($recipients as $address => $language) {
if ($review->get_submitter()->email === $address) {
continue;
}
Pluf_Translation::loadSetLocale($language);
$context = new Pluf_Template_Context(array(
'review' => $review,
'patch' => $this,
'comments' => array(),
'project' => $project,
'url_base' => Pluf::f('url_base'),
));
// reviews are updated through comments, see IDF_Review_Comment::notify()
$tplfile = 'idf/review/review-created-email.txt';
$subject = __('New Code Review %s - %s (%s)');
$headers = array('Message-ID' => $messageId);
$tmpl = new Pluf_Template($tplfile);
$text_email = $tmpl->render($context);
$email = new Pluf_Mail($from_email,
$address,
sprintf(__('New Code Review %s - %s (%s)'),
$this->get_review()->id,
$this->get_review()->summary,
$this->get_review()->get_project()->shortname));
sprintf($subject,
$review->id,
$review->summary,
$project->shortname));
$email->addTextMessage($text_email);
$email->addHeaders($headers);
$email->sendMail();
}
Pluf_Translation::loadSetLocale($current_locale);
}
}

View File

@ -273,31 +273,49 @@ class IDF_Upload extends Pluf_Model
$item->payload = $payload;
$item->create();
if ('' == $conf->getVal('downloads_notification_email', '')) {
return;
}
$current_locale = Pluf_Translation::getLocale();
$langs = Pluf::f('languages', array('en'));
Pluf_Translation::loadSetLocale($langs[0]);
$context = new Pluf_Template_Context(
array('file' => $this,
'urlfile' => $this->getAbsoluteUrl($this->get_project()),
'project' => $this->get_project(),
'tags' => $this->get_tags_list(),
));
$tmpl = new Pluf_Template('idf/downloads/download-created-email.txt');
$text_email = $tmpl->render($context);
$addresses = explode(',', $conf->getVal('downloads_notification_email'));
foreach ($addresses as $address) {
$email = new Pluf_Mail(Pluf::f('from_email'),
$from_email = Pluf::f('from_email');
$messageId = '<'.md5('upload'.$this->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>';
$recipients = $project->getNotificationRecipientsForTab('downloads');
foreach ($recipients as $address => $language) {
if ($this->get_submitter()->email === $address) {
continue;
}
Pluf_Translation::loadSetLocale($language);
$context = new Pluf_Template_Context(array(
'file' => $this,
'urlfile' => $this->getAbsoluteUrl($project),
'project' => $project,
'tags' => $this->get_tags_list(),
));
$tplfile = 'idf/downloads/download-created-email.txt';
$subject = __('New download - %s (%s)');
$headers = array('Message-ID' => $messageId);
if (!$create) {
$tplfile = 'idf/downloads/download-updated-email.txt';
$subject = __('Updated download - %s (%s)');
$headers = array('References' => $messageId);
}
$tmpl = new Pluf_Template($tplfile);
$text_email = $tmpl->render($context);
$email = new Pluf_Mail($from_email,
$address,
sprintf(__('New download - %s (%s)'),
sprintf($subject,
$this->summary,
$this->get_project()->shortname));
$project->shortname));
$email->addTextMessage($text_email);
$email->addHeaders($headers);
$email->sendMail();
}
Pluf_Translation::loadSetLocale($current_locale);
}
}

View File

@ -243,42 +243,49 @@ class IDF_WikiRevision extends Pluf_Model
*/
public function notify($conf, $create=true)
{
if ('' == $conf->getVal('wiki_notification_email', '')) {
return;
}
$wikipage = $this->get_wikipage();
$project = $wikipage->get_project();
$current_locale = Pluf_Translation::getLocale();
$langs = Pluf::f('languages', array('en'));
Pluf_Translation::loadSetLocale($langs[0]);
$context = new Pluf_Template_Context(
array(
'page' => $this->get_wikipage(),
'rev' => $this,
'project' => $this->get_wikipage()->get_project(),
'url_base' => Pluf::f('url_base'),
)
);
if ($create) {
$template = 'idf/wiki/wiki-created-email.txt';
$title = sprintf(__('New Documentation Page %s - %s (%s)'),
$this->get_wikipage()->title,
$this->get_wikipage()->summary,
$this->get_wikipage()->get_project()->shortname);
} else {
$template = 'idf/wiki/wiki-updated-email.txt';
$title = sprintf(__('Documentation Page Changed %s - %s (%s)'),
$this->get_wikipage()->title,
$this->get_wikipage()->summary,
$this->get_wikipage()->get_project()->shortname);
}
$tmpl = new Pluf_Template($template);
$text_email = $tmpl->render($context);
$addresses = explode(',', $conf->getVal('wiki_notification_email'));
foreach ($addresses as $address) {
$email = new Pluf_Mail(Pluf::f('from_email'),
$from_email = Pluf::f('from_email');
$messageId = '<'.md5('wiki'.$wikipage->id.md5(Pluf::f('secret_key'))).'@'.Pluf::f('mail_host', 'localhost').'>';
$recipients = $project->getNotificationRecipientsForTab('wiki');
foreach ($recipients as $address => $language) {
if ($this->get_submitter()->email === $address) {
continue;
}
Pluf_Translation::loadSetLocale($language);
$context = new Pluf_Template_Context(array(
'page' => $wikipage,
'rev' => $this,
'project' => $project,
'url_base' => Pluf::f('url_base'),
));
$tplfile = 'idf/wiki/wiki-created-email.txt';
$subject = __('New Documentation Page %s - %s (%s)');
$headers = array('Message-ID' => $messageId);
if (!$create) {
$tplfile = 'idf/wiki/wiki-updated-email.txt';
$subject = __('Documentation Page Changed %s - %s (%s)');
$headers = array('References' => $messageId);
}
$tmpl = new Pluf_Template($tplfile);
$text_email = $tmpl->render($context);
$email = new Pluf_Mail($from_email,
$address,
$title);
sprintf($subject,
$wikipage->title,
$wikipage->summary,
$project->shortname));
$email->addTextMessage($text_email);
$email->addHeaders($headers);
$email->sendMail();
}

View File

@ -0,0 +1,17 @@
{trans 'Hello,'}
{blocktrans}A file download was updated:{/blocktrans}
{$file.summary|safe}
{$file} - {$file.filesize|ssize}
{trans 'Project:'} {$project.name|safe}
{trans 'Submitted by:'} {$file.get_submitter|safe}
{if $tags.count()}{trans 'Labels:'}
{foreach $tags as $tag} {$tag.class|safe}:{$tag.name|safe}
{/foreach}{/if}
{trans 'Download:'} {$urlfile}
{if $file.changelog}
{trans 'Description:'}
{$file.changelog}
{/if}

View File

@ -1,7 +1,6 @@
{trans 'Hello,'}
{blocktrans}A new issue has been created and assigned
to you:{/blocktrans}
{if $owns_issue}{blocktrans}A new issue has been created and assigned to you:{/blocktrans}{else}{blocktrans}A new issue has been created:{/blocktrans}{/if}
{$issue.id} - {$issue.summary|safe}
{trans 'Project:'} {$project.name|safe}

View File

@ -1,6 +1,6 @@
{trans 'Hello,'}
{blocktrans}The following issue has been updated:{/blocktrans}
{if $owns_issue}{blocktrans}The following issue you are owning has been updated:{/blocktrans}{else}{blocktrans}The following issue has been updated:{/blocktrans}{/if}
{$issue.id} - {$issue.summary|safe}
{trans 'Project:'} {$project.name|safe}

View File

@ -2,16 +2,16 @@
{blocktrans}A new commit has been created:{/blocktrans}
{$c.summary|safe}
{$commit.summary|safe}
{trans 'Commit:'} {$c.scm_id|safe}
{trans 'Commit:'} {$commit.scm_id|safe}
{trans 'Project:'} {$project.name|safe}
{trans 'Created by:'} {$c.get_author|safe}
{trans 'Created at:'} {$c.creation_dtime|date:"%Y-%m-%d %H:%M:%S"}
{if $c.fullmessage}
{trans 'Created by:'} {$commit.get_author|safe}
{trans 'Created at:'} {$commit.creation_dtime|date:"%Y-%m-%d %H:%M:%S"}
{if $commit.fullmessage}
{trans 'Content:'}
{$c.fullmessage}
{$commit.fullmessage}
{/if}
--
{trans 'Commit details:'} {$url_base}{url 'IDF_Views_Source::commit', array($project.shortname, $c.scm_id)}
{trans 'Commit details:'} {$url_base}{url 'IDF_Views_Source::commit', array($project.shortname, $commit.scm_id)}