Started ticket 39, add code review.

We now have a limited support of the code review. Still some work to be
done to allow the submission of new patches on a given review and update
the status. For the moment, only pre-commit review is supported.
This commit is contained in:
Loic d'Anterroches 2008-11-30 10:26:05 +01:00
parent fbe364462d
commit f690968b11
35 changed files with 2422 additions and 8 deletions

View File

@ -169,4 +169,162 @@ class IDF_Diff
return $res; return $res;
} }
/**
* Review patch.
*
* Given the original file as a string and the parsed
* corresponding diff chunks, generate a side by side view of the
* original file and new file with added/removed lines.
*
* Example of use:
*
* $diff = new IDF_Diff(file_get_contents($diff_file));
* $orig = file_get_contents($orig_file);
* $diff->parse();
* echo $diff->fileCompare($orig, $diff->files[$orig_file], $diff_file);
*
* @param string Original file
* @param array Chunk description of the diff corresponding to the file
* @param string Original file name
* @param int Number of lines before/after the chunk to be displayed (10)
* @return Pluf_Template_SafeString The table body
*/
public function fileCompare($orig, $chunks, $filename, $context=10)
{
$orig_lines = preg_split("/\015\012|\015|\012/", $orig);
$new_chunks = $this->mergeChunks($orig_lines, $chunks, $context);
return $this->renderCompared($new_chunks, $filename);
}
public function mergeChunks($orig_lines, $chunks, $context=10)
{
$spans = array();
$new_chunks = array();
$min_line = 0;
$max_line = 0;
//if (count($chunks['chunks_def']) == 0) return '';
foreach ($chunks['chunks_def'] as $chunk) {
$start = ($chunk[0][0] > $context) ? $chunk[0][0]-$context : 0;
$end = (($chunk[0][0]+$chunk[0][1]+$context-1) < count($orig_lines)) ? $chunk[0][0]+$chunk[0][1]+$context-1 : count($orig_lines);
$spans[] = array($start, $end);
}
// merge chunks/get the chunk lines
// these are reference lines
$chunk_lines = array();
foreach ($chunks['chunks'] as $chunk) {
foreach ($chunk as $line) {
$chunk_lines[] = $line;
}
}
$i = 0;
foreach ($chunks['chunks'] as $chunk) {
$n_chunk = array();
// add lines before
if ($chunk[0][0] > $spans[$i][0]) {
for ($lc=$spans[$i][0];$lc<$chunk[0][0];$lc++) {
$exists = false;
foreach ($chunk_lines as $line) {
if ($lc == $line[0] or ($chunk[0][1]-$chunk[0][0]+$lc) == $line[1]) {
$exists = true;
break;
}
}
if (!$exists) {
$n_chunk[] = array(
$lc,
$chunk[0][1]-$chunk[0][0]+$lc,
$orig_lines[$lc-1]
);
}
}
}
// add chunk lines
foreach ($chunk as $line) {
$n_chunk[] = $line;
}
// add lines after
$lline = $line;
if (!empty($lline[0]) and $lline[0] < $spans[$i][1]) {
for ($lc=$lline[0];$lc<=$spans[$i][1];$lc++) {
$exists = false;
foreach ($chunk_lines as $line) {
if ($lc == $line[0] or ($lline[1]-$lline[0]+$lc) == $line[1]) {
$exists = true;
break;
}
}
if (!$exists) {
$n_chunk[] = array(
$lc,
$lline[1]-$lline[0]+$lc,
$orig_lines[$lc-1]
);
}
}
}
$new_chunks[] = $n_chunk;
$i++;
}
// Now, each chunk has the right length, we need to merge them
// when needed
$nnew_chunks = array();
$i = 0;
foreach ($new_chunks as $chunk) {
if ($i>0) {
$lline = end($nnew_chunks[$i-1]);
if ($chunk[0][0] <= $lline[0]+1) {
// need merging
foreach ($chunk as $line) {
if ($line[0] > $lline[0] or empty($line[0])) {
$nnew_chunks[$i-1][] = $line;
}
}
} else {
$nnew_chunks[] = $chunk;
$i++;
}
} else {
$nnew_chunks[] = $chunk;
$i++;
}
}
return $nnew_chunks;
}
public function renderCompared($chunks, $filename)
{
$fileinfo = IDF_Views_Source::getMimeType($filename);
$pretty = '';
if (IDF_Views_Source::isSupportedExtension($fileinfo[2])) {
$pretty = ' prettyprint';
}
$out = '';
$cc = 1;
$i = 0;
foreach ($chunks as $chunk) {
foreach ($chunk as $line) {
$line1 = '&nbsp;';
$line2 = '&nbsp;';
$line[2] = (strlen($line[2])) ? self::padLine(Pluf_esc($line[2])) : '&nbsp;';
if ($line[0] and $line[1]) {
$class = 'diff-c';
$line1 = $line2 = $line[2];
} elseif ($line[0]) {
$class = 'diff-r';
$line1 = $line[2];
} else {
$class = 'diff-a';
$line2 = $line[2];
}
$out .= sprintf('<tr class="diff-line"><td class="diff-lc">%s</td><td class="%s mono%s"><code>%s</code></td><td class="diff-lc">%s</td><td class="%s mono%s"><code>%s</code></td></tr>'."\n", $line[0], $class, $pretty, $line1, $line[1], $class, $pretty, $line2);
}
if (count($chunks) > $cc)
$out .= '<tr class="diff-next"><td>...</td><td>&nbsp;</td><td>...</td><td>&nbsp;</td></tr>'."\n";
$cc++;
$i++;
}
return Pluf_Template::markSafe($out);
}
} }

View File

@ -0,0 +1,203 @@
<?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 ***** */
/**
* Create a new documentation page.
*
* This create a new page and the corresponding revision.
*
*/
class IDF_Form_WikiCreate extends Pluf_Form
{
public $user = null;
public $project = null;
public $show_full = false;
public function initFields($extra=array())
{
$initial = __('# Introduction
Add your content here.
# Details
Add your content here. Format your content with:
* Text in **bold** or *italic*.
* Headings, paragraphs, and lists.
* Links to other [[WikiPage]].
');
$this->user = $extra['user'];
$this->project = $extra['project'];
if ($this->user->hasPerm('IDF.project-owner', $this->project)
or $this->user->hasPerm('IDF.project-member', $this->project)) {
$this->show_full = true;
}
$this->fields['title'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Page title'),
'initial' => __('PageName'),
'widget_attrs' => array(
'maxlength' => 200,
'size' => 67,
),
'help_text' => __('The page name must contains only letters, digits and the dash (-) character.'),
));
$this->fields['summary'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Description'),
'help_text' => __('This one line description is displayed in the list of pages.'),
'initial' => '',
'widget_attrs' => array(
'maxlength' => 200,
'size' => 67,
),
));
$this->fields['content'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Content'),
'initial' => $initial,
'widget' => 'Pluf_Form_Widget_TextareaInput',
'widget_attrs' => array(
'cols' => 58,
'rows' => 26,
),
));
if ($this->show_full) {
for ($i=1;$i<4;$i++) {
$this->fields['label'.$i] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Labels'),
'initial' => '',
'widget_attrs' => array(
'maxlength' => 50,
'size' => 20,
),
));
}
}
}
public function clean_title()
{
$title = $this->cleaned_data['title'];
if (preg_match('/[^a-zA-Z0-9\-]/', $title)) {
throw new Pluf_Form_Invalid(__('The title contains invalid characters.'));
}
$sql = new Pluf_SQL('project=%s AND title=%s',
array($this->project->id, $title));
$pages = Pluf::factory('IDF_WikiPage')->getList(array('filter'=>$sql->gen()));
if ($pages->count() > 0) {
throw new Pluf_Form_Invalid(__('A page with this title already exists.'));
}
return $title;
}
/**
* Validate the interconnection in the form.
*/
public function clean()
{
if (!$this->show_full) {
return $this->cleaned_data;
}
$conf = new IDF_Conf();
$conf->setProject($this->project);
$onemax = array();
foreach (split(',', $conf->getVal('labels_wiki_one_max', IDF_Form_WikiConf::init_one_max)) as $class) {
if (trim($class) != '') {
$onemax[] = mb_strtolower(trim($class));
}
}
$count = array();
for ($i=1;$i<4;$i++) {
$this->cleaned_data['label'.$i] = trim($this->cleaned_data['label'.$i]);
if (strpos($this->cleaned_data['label'.$i], ':') !== false) {
list($class, $name) = explode(':', $this->cleaned_data['label'.$i], 2);
list($class, $name) = array(mb_strtolower(trim($class)),
trim($name));
} else {
$class = 'other';
$name = $this->cleaned_data['label'.$i];
}
if (!isset($count[$class])) $count[$class] = 1;
else $count[$class] += 1;
if (in_array($class, $onemax) and $count[$class] > 1) {
if (!isset($this->errors['label'.$i])) $this->errors['label'.$i] = array();
$this->errors['label'.$i][] = sprintf(__('You cannot provide more than label from the %s class to a page.'), $class);
throw new Pluf_Form_Invalid(__('You provided an invalid label.'));
}
}
return $this->cleaned_data;
}
/**
* Save the model in the database.
*
* @param bool Commit in the database or not. If not, the object
* is returned but not saved in the database.
* @return Object Model with data set from the form.
*/
function save($commit=true)
{
if (!$this->isValid()) {
throw new Exception(__('Cannot save the model from an invalid form.'));
}
// Add a tag for each label
$tags = array();
if ($this->show_full) {
for ($i=1;$i<4;$i++) {
if (strlen($this->cleaned_data['label'.$i]) > 0) {
if (strpos($this->cleaned_data['label'.$i], ':') !== false) {
list($class, $name) = explode(':', $this->cleaned_data['label'.$i], 2);
list($class, $name) = array(trim($class), trim($name));
} else {
$class = 'Other';
$name = trim($this->cleaned_data['label'.$i]);
}
$tags[] = IDF_Tag::add($name, $this->project, $class);
}
}
}
// Create the page
$page = new IDF_WikiPage();
$page->project = $this->project;
$page->submitter = $this->user;
$page->summary = trim($this->cleaned_data['summary']);
$page->title = trim($this->cleaned_data['title']);
$page->create();
foreach ($tags as $tag) {
$page->setAssoc($tag);
}
// add the first revision
$rev = new IDF_WikiRevision();
$rev->wikipage = $page;
$rev->content = $this->cleaned_data['content'];
$rev->submitter = $this->user;
$rev->summary = __('Initial page creation');
$rev->create();
return $page;
}
}

View File

@ -0,0 +1,205 @@
<?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 ***** */
/**
* Create a new code review.
*
* This creates an IDF_Review and the corresponding IDF_Review_Patch.
*/
class IDF_Form_ReviewCreate extends Pluf_Form
{
public $user = null;
public $project = null;
public $show_full = false;
public function initFields($extra=array())
{
$this->user = $extra['user'];
$this->project = $extra['project'];
if ($this->user->hasPerm('IDF.project-owner', $this->project)
or $this->user->hasPerm('IDF.project-member', $this->project)) {
$this->show_full = true;
}
$this->fields['summary'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Summary'),
'initial' => '',
'widget_attrs' => array(
'maxlength' => 200,
'size' => 67,
),
));
$this->fields['description'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Description'),
'initial' => '',
'widget' => 'Pluf_Form_Widget_TextareaInput',
'widget_attrs' => array(
'cols' => 58,
'rows' => 7,
),
));
$this->fields['commit'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Commit'),
'initial' => '',
'widget_attrs' => array(
'size' => 42,
),
));
$upload_path = Pluf::f('upload_issue_path', false);
if (false === $upload_path) {
throw new Pluf_Exception_SettingError(__('The "upload_issue_path" configuration variable was not set.'));
}
$md5 = md5(rand().microtime().Pluf_Utils::getRandomString());
// We add .dummy to try to mitigate security issues in the
// case of someone allowing the upload path to be accessible
// to everybody.
$filename = substr($md5, 0, 2).'/'.substr($md5, 2, 2).'/'.substr($md5, 4).'/%s.dummy';
$this->fields['patch'] = new Pluf_Form_Field_File(
array('required' => true,
'label' => __('Patch'),
'move_function_params' =>
array('upload_path' => $upload_path,
'upload_path_create' => true,
'file_name' => $filename,
)
)
);
if ($this->show_full) {
$this->fields['status'] = new Pluf_Form_Field_Varchar(
array('required' => true,
'label' => __('Status'),
'initial' => 'New',
'widget_attrs' => array(
'maxlength' => 20,
'size' => 15,
),
));
}
}
public function clean_commit()
{
$commit = self::findCommit($this->cleaned_data['commit']);
if (null == $commit) {
throw new Pluf_Form_Invalid(__('You provided an invalid commit.'));
}
return $this->cleaned_data['commit'];
}
/**
* Validate the interconnection in the form.
*/
public function clean()
{
return $this->cleaned_data;
}
function clean_status()
{
// Check that the status is in the list of official status
$tags = $this->project->getTagsFromConfig('labels_issue_open',
IDF_Form_IssueTrackingConf::init_open,
'Status');
$tags = array_merge($this->project->getTagsFromConfig('labels_issue_closed',
IDF_Form_IssueTrackingConf::init_closed,
'Status')
, $tags);
$found = false;
foreach ($tags as $tag) {
if ($tag->name == trim($this->cleaned_data['status'])) {
$found = true;
break;
}
}
if (!$found) {
throw new Pluf_Form_Invalid(__('You provided an invalid status.'));
}
return $this->cleaned_data['status'];
}
/**
* Clean the attachments post failure.
*/
function failed()
{
$upload_path = Pluf::f('upload_issue_path', false);
if ($upload_path == false) return;
if (!empty($this->cleaned_data['patch']) and
file_exists($upload_path.'/'.$this->cleaned_data['patch'])) {
@unlink($upload_path.'/'.$this->cleaned_data['patch']);
}
}
/**
* Save the model in the database.
*
* @param bool Commit in the database or not. If not, the object
* is returned but not saved in the database.
* @return Object Model with data set from the form.
*/
function save($commit=true)
{
if (!$this->isValid()) {
throw new Exception(__('Cannot save the model from an invalid form.'));
}
// Create the review
$review = new IDF_Review();
$review->project = $this->project;
$review->summary = $this->cleaned_data['summary'];
$review->submitter = $this->user;
$review->status = IDF_Tag::add(trim($this->cleaned_data['status']), $this->project, 'Status');
$review->create();
// add the first patch
$patch = new IDF_Review_Patch();
$patch->review = $review;
$patch->summary = __('Initial patch to be reviewed.');
$patch->description = $this->cleaned_data['description'];
$patch->commit = self::findCommit($this->cleaned_data['commit']);
$patch->patch = $this->cleaned_data['patch'];
$patch->create();
return $review;
}
/**
* Based on the given string, try to find the matching commit.
*
* If no user found, simply returns null.
*
* @param string Commit
* @return IDF_Commit or null
*/
public static function findCommit($string)
{
$string = trim($string);
if (strlen($string) == 0) return null;
$gc = new IDF_Commit();
$sql = new Pluf_SQL('scm_id=%s', array($string));
$gcs = $gc->getList(array('filter' => $sql->gen()));
if ($gcs->count() > 0) {
return $gcs[0];
}
return null;
}
}

View File

@ -0,0 +1,94 @@
<?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 comments to files in a review.
*
*/
class IDF_Form_ReviewFileComment extends Pluf_Form
{
public $files = null;
public $patch = null;
public $user = null;
public function initFields($extra=array())
{
$this->files = $extra['files'];
$this->patch = $extra['patch'];
$this->user = $extra['user'];
foreach ($this->files as $filename => $def) {
$this->fields[md5($filename)] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('Comment'),
'initial' => '',
'widget' => 'Pluf_Form_Widget_TextareaInput',
'widget_attrs' => array(
'cols' => 58,
'rows' => 9,
),
));
}
}
/**
* Validate the interconnection in the form.
*/
public function clean()
{
foreach ($this->files as $filename => $def) {
if (!empty($this->cleaned_data[md5($filename)])) {
return $this->cleaned_data;
}
}
throw new Pluf_Form_Invalid(__('You need to provide comments on at least one file.'));
}
/**
* Save the model in the database.
*
* @param bool Commit in the database or not. If not, the object
* is returned but not saved in the database.
* @return Object Model with data set from the form.
*/
function save($commit=true)
{
if (!$this->isValid()) {
throw new Exception(__('Cannot save the model from an invalid form.'));
}
foreach ($this->files as $filename => $def) {
if (!empty($this->cleaned_data[md5($filename)])) {
// Add a comment.
$c = new IDF_Review_FileComment();
$c->patch = $this->patch;
$c->cfile = $filename;
$c->submitter = $this->user;
$c->content = $this->cleaned_data[md5($filename)];
$c->create();
}
}
$this->patch->get_review()->update(); // reindex and put up in
// the list.
return $this->patch;
}
}

View File

@ -36,6 +36,7 @@ class IDF_Form_TabsConf extends Pluf_Form
$this->project = $extra['project']; $this->project = $extra['project'];
$ak = array('downloads_access_rights' => __('Downloads'), $ak = array('downloads_access_rights' => __('Downloads'),
'review_access_rights' => __('Code Review'),
'wiki_access_rights' => __('Documentation'), 'wiki_access_rights' => __('Documentation'),
'source_access_rights' => __('Source'), 'source_access_rights' => __('Source'),
'issues_access_rights' => __('Issues'),); 'issues_access_rights' => __('Issues'),);

View File

@ -54,6 +54,7 @@ class IDF_Middleware
$request->conf->setProject($request->project); $request->conf->setProject($request->project);
$ak = array('downloads_access_rights' => 'hasDownloadsAccess', $ak = array('downloads_access_rights' => 'hasDownloadsAccess',
'wiki_access_rights' => 'hasWikiAccess', 'wiki_access_rights' => 'hasWikiAccess',
'review_access_rights' => 'hasReviewAccess',
'source_access_rights' => 'hasSourceAccess', 'source_access_rights' => 'hasSourceAccess',
'issues_access_rights' => 'hasIssuesAccess'); 'issues_access_rights' => 'hasIssuesAccess');
$request->rights = array(); $request->rights = array();

View File

@ -0,0 +1,56 @@
<?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 code review.
*/
function IDF_Migrations_8CodeReview_up($params=null)
{
$models = array(
'IDF_Review',
'IDF_Review_Patch',
'IDF_Review_FileComment',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);
foreach ($models as $model) {
$schema->model = new $model();
$schema->createTables();
}
}
function IDF_Migrations_8CodeReview_down($params=null)
{
$models = array(
'IDF_Review_FileComment',
'IDF_Review_Patch',
'IDF_Review',
);
$db = Pluf::db();
$schema = new Pluf_DB_Schema($db);
foreach ($models as $model) {
$schema->model = new $model();
$schema->dropTables();
}
}

View File

@ -42,6 +42,9 @@ function IDF_Migrations_Install_setup($params=null)
'IDF_Timeline', 'IDF_Timeline',
'IDF_WikiPage', 'IDF_WikiPage',
'IDF_WikiRevision', 'IDF_WikiRevision',
'IDF_Review',
'IDF_Review_Patch',
'IDF_Review_FileComment',
); );
$db = Pluf::db(); $db = Pluf::db();
$schema = new Pluf_DB_Schema($db); $schema = new Pluf_DB_Schema($db);
@ -79,6 +82,9 @@ function IDF_Migrations_Install_teardown($params=null)
$perm = Pluf_Permission::getFromString('IDF.project-authorized-user'); $perm = Pluf_Permission::getFromString('IDF.project-authorized-user');
if ($perm) $perm->delete(); if ($perm) $perm->delete();
$models = array( $models = array(
'IDF_Review_FileComment',
'IDF_Review_Patch',
'IDF_Review',
'IDF_WikiRevision', 'IDF_WikiRevision',
'IDF_WikiPage', 'IDF_WikiPage',
'IDF_Timeline', 'IDF_Timeline',

View File

@ -152,6 +152,15 @@ class IDF_Precondition
return self::accessTabGeneric($request, 'wiki_access_rights'); return self::accessTabGeneric($request, 'wiki_access_rights');
} }
static public function accessReview($request)
{
$res = self::baseAccess($request);
if (true !== $res) {
return $res;
}
return self::accessTabGeneric($request, 'review_access_rights');
}
/** /**
* Based on the request, it is automatically setting the user. * Based on the request, it is automatically setting the user.
* *

168
src/IDF/Review.php Normal file
View File

@ -0,0 +1,168 @@
<?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');
Pluf::loadFunction('Pluf_Template_dateAgo');
/**
* Base definition of a code review.
*
* A code review has a status, submitter, summary, description and is
* associated to a project.
*
* The real content of the review is in the IDF_Review_Patch which
* contains a given patch and associated comments from reviewers.
*/
class IDF_Review extends Pluf_Model
{
public $_model = __CLASS__;
function init()
{
$this->_a['table'] = 'idf_reviews';
$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' => 'reviews',
),
'summary' =>
array(
'type' => 'Pluf_DB_Field_Varchar',
'blank' => false,
'size' => 250,
'verbose' => __('summary'),
),
'submitter' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
'blank' => false,
'verbose' => __('submitter'),
'relate_name' => 'submitted_review',
),
'reviewers' =>
array(
'type' => 'Pluf_DB_Field_Manytomany',
'model' => 'Pluf_User',
'blank' => true,
'help_text' => 'Reviewers will get an email notification when the review is changed.',
),
'tags' =>
array(
'type' => 'Pluf_DB_Field_Manytomany',
'blank' => true,
'model' => 'IDF_Tag',
'verbose' => __('labels'),
),
'status' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'blank' => false,
'model' => 'IDF_Tag',
'verbose' => __('status'),
),
'creation_dtime' =>
array(
'type' => 'Pluf_DB_Field_Datetime',
'blank' => true,
'verbose' => __('creation date'),
),
'modif_dtime' =>
array(
'type' => 'Pluf_DB_Field_Datetime',
'blank' => true,
'verbose' => __('modification date'),
),
);
$this->_a['idx'] = array(
'modif_dtime_idx' =>
array(
'col' => 'modif_dtime',
'type' => 'normal',
),
);
$table = $this->_con->pfx.'idf_review_idf_tag_assoc';
$this->_a['views'] = array(
'join_tags' =>
array(
'join' => 'LEFT JOIN '.$table
.' ON idf_issue_id=id',
),
);
}
function __toString()
{
return $this->id.' - '.$this->summary;
}
function _toIndex()
{
return '';
}
function preDelete()
{
IDF_Timeline::remove($this);
IDF_Search::remove($this);
}
function preSave($create=false)
{
if ($this->id == '') {
$this->creation_dtime = gmdate('Y-m-d H:i:s');
}
$this->modif_dtime = gmdate('Y-m-d H:i:s');
}
function postSave($create=false)
{
}
/**
* Returns an HTML fragment used to display this review 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)
{
return '';
}
}

View File

@ -0,0 +1,113 @@
<?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 ***** */
/**
* A comment to a file affected by a patch.
*
*/
class IDF_Review_FileComment extends Pluf_Model
{
public $_model = __CLASS__;
function init()
{
$this->_a['table'] = 'idf_review_filecomments';
$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,
),
'patch' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Review_Patch',
'blank' => false,
'verbose' => __('patch'),
'relate_name' => 'filecomments',
),
'cfile' =>
array(
'type' => 'Pluf_DB_Field_Varchar',
'blank' => false,
'size' => 250,
'help_text' => 'The changed file, for example src/foo/bar.txt, this is the path to access it in the repository.',
),
'content' =>
array(
'type' => 'Pluf_DB_Field_Text',
'blank' => false,
'verbose' => __('comment'),
),
'submitter' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'Pluf_User',
'blank' => false,
'verbose' => __('submitter'),
'relate_name' => 'commented_patched_files',
),
'creation_dtime' =>
array(
'type' => 'Pluf_DB_Field_Datetime',
'blank' => true,
'verbose' => __('creation date'),
),
);
$this->_a['idx'] = array(
'creation_dtime_idx' =>
array(
'col' => 'creation_dtime',
'type' => 'normal',
),
);
}
function _toIndex()
{
return $this->cfile.' '.$this->content;
}
function preDelete()
{
IDF_Timeline::remove($this);
}
function preSave($create=false)
{
if ($this->id == '') {
$this->creation_dtime = gmdate('Y-m-d H:i:s');
}
}
function postSave($create=false)
{
}
public function timelineFragment($request)
{
return '';
}
}

118
src/IDF/Review/Patch.php Normal file
View File

@ -0,0 +1,118 @@
<?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 ***** */
/**
* A patch to be reviewed.
*
*/
class IDF_Review_Patch extends Pluf_Model
{
public $_model = __CLASS__;
function init()
{
$this->_a['table'] = 'idf_review_patches';
$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,
),
'review' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Review',
'blank' => false,
'verbose' => __('review'),
'relate_name' => 'patches',
),
'summary' =>
array(
'type' => 'Pluf_DB_Field_Varchar',
'blank' => false,
'size' => 250,
'verbose' => __('summary'),
),
'commit' =>
array(
'type' => 'Pluf_DB_Field_Foreignkey',
'model' => 'IDF_Commit',
'blank' => false,
'verbose' => __('commit'),
'relate_name' => 'patches',
),
'description' =>
array(
'type' => 'Pluf_DB_Field_Text',
'blank' => false,
'verbose' => __('description'),
),
'patch' =>
array(
'type' => 'Pluf_DB_Field_File',
'blank' => false,
'verbose' => __('patch'),
'help_text' => 'The patch is stored at the same place as the issue attachments with the same approach for the name.',
),
'creation_dtime' =>
array(
'type' => 'Pluf_DB_Field_Datetime',
'blank' => true,
'verbose' => __('creation date'),
),
);
$this->_a['idx'] = array(
'creation_dtime_idx' =>
array(
'col' => 'creation_dtime',
'type' => 'normal',
),
);
}
function _toIndex()
{
return '';
}
function preDelete()
{
}
function preSave($create=false)
{
if ($this->id == '') {
$this->creation_dtime = gmdate('Y-m-d H:i:s');
}
}
function postSave($create=false)
{
}
public function timelineFragment($request)
{
}
}

View File

@ -55,7 +55,7 @@ class IDF_Scm
$cache = Pluf_Cache::factory(); $cache = Pluf_Cache::factory();
if (null === ($res=$cache->get($key))) { if (null === ($res=$cache->get($key))) {
$ll = exec($command, $output, $return); $ll = exec($command, $output, $return);
if ($return != 0 and Pluf::f('debug', false)) { if ($return != 0 and Pluf::f('debug_scm', false)) {
throw new IDF_Scm_Exception(sprintf('Error when running command: "%s", return code: %d', $command, $return)); throw new IDF_Scm_Exception(sprintf('Error when running command: "%s", return code: %d', $command, $return));
} }
$cache->set($key, array($ll, $return, $output)); $cache->set($key, array($ll, $return, $output));

View File

@ -54,4 +54,21 @@ class IDF_Tests_TestDiff extends UnitTestCase
$i++; $i++;
} }
} }
public function testBinaryDiff()
{
$diff_content = file_get_contents(dirname(__FILE__).'/test-diff.diff');
$orig = file_get_contents(dirname(__FILE__).'/test-diff-view.html');
$diff = new IDF_Diff($diff_content);
$diff->parse();
$def = $diff->files['src/IDF/templates/idf/issues/view.html'];
$orig_lines = preg_split("/\015\012|\015|\012/", $orig);
$merged = $diff->mergeChunks($orig_lines, $def, 10);
$lchunk = end($merged);
$lline = end($lchunk);
$this->assertEqual(array('', '166', '{/if}{/block}'),
$lline);
//print_r($diff->mergeChunks($orig_lines, $def, 10));
}
} }

View File

@ -0,0 +1,125 @@
{extends "idf/issues/base.html"}
{block titleicon}{if $form}<form class="star" method="post" action="{url 'IDF_Views_Issue::star', array($project.shortname, $issue.id)}"><input type="image" src="{if $starred}{media '/idf/img/star.png'}{else}{media '/idf/img/star-grey.png'}{/if}" name="submit" /></form> {/if}{/block}
{block body}
{assign $i = 0}
{assign $nc = $comments.count()}
{foreach $comments as $c}
<div class="issue-comment{if $i == 0} issue-comment-first{/if}{if $i == ($nc-1)} issue-comment-last{/if}" id="ic{$c.id}">{assign $who = $c.get_submitter()}{aurl 'whourl', 'IDF_Views_User::view', array($who.login)}
{if $i == 0}
<p>{blocktrans}Reported by <a href="{$whourl}">{$who}</a>, {$c.creation_dtime|date}{/blocktrans}</p>
{else}
{aurl 'url', 'IDF_Views_Issue::view', array($project.shortname, $issue.id)}
{assign $id = $c.id}
{assign $url = $url~'#ic'~$c.id}
<p>{blocktrans}Comment <a href="{$url}">{$i}</a> by <a href="{$whourl}">{$who}</a>, {$c.creation_dtime|date}{/blocktrans}</p>
{/if}
<pre class="issue-comment-text">{if strlen($c.content) > 0}{issuetext $c.content, $request}{else}<i>{trans '(No comments were given for this change.)'}</i>{/if}</pre>
{assign $attachments = $c.get_attachment_list()}
{if $attachments.count() > 0}
<hr align="left" class="attach" />
<ul>
{foreach $attachments as $a}<li><a href="{url 'IDF_Views_Issue::getAttachment', array($project.shortname, $a.id, $a.filename)}">{$a.filename}</a> - {$a.filesize|size}</li>{/foreach}
</ul>{/if}
{if $i> 0 and $c.changedIssue()}
<div class="issue-changes">
{foreach $c.changes as $w => $v}
<strong>{if $w == 'su'}{trans 'Summary:'}{/if}{if $w == 'st'}{trans 'Status:'}{/if}{if $w == 'ow'}{trans 'Owner:'}{/if}{if $w == 'lb'}{trans 'Labels:'}{/if}</strong> {if $w == 'lb'}{assign $l = implode(', ', $v)}{$l}{else}{$v}{/if}<br />
{/foreach}
</div>
{/if}
</div>{assign $i = $i + 1}{if $i == $nc and false == $form}
<div class="issue-comment-signin">
{aurl 'url', 'IDF_Views::login'}{blocktrans}<a href="{$url}">Sign in</a> to reply to this comment.{/blocktrans}
</div>
{/if}
{/foreach}
{if $form}
<hr />
{if $form.errors}
<div class="px-message-error">
<p>{trans 'The form contains some errors. Please correct them to change the issue.'}</p>
{if $form.get_top_errors}
{$form.render_top_errors|unsafe}
{/if}
</div>
{/if}
{if $closed and (!$isOwner and !$isMember)}
<p><img src="{media '/idf/img/warning.png'}" style="vertical-align: text-bottom;" alt=" " /> {blocktrans}This issue is marked as closed, add a comment only if you think this issue is still valid and more work is needed to fully fix it.{/blocktrans}</p>
{/if}
<form method="post" enctype="multipart/form-data" action="{url 'IDF_Views_Issue::view', array($project.shortname, $issue.id)}" >
<table class="form" summary="">
<tr>
<th><strong>{$form.f.content.labelTag}:</strong></th>
<td>{if $form.f.content.errors}{$form.f.content.fieldErrors}{/if}
{$form.f.content|unsafe}
</td>
</tr>
<tr>
<th>{$form.f.attachment.labelTag}:</th>
<td>{if $form.f.attachment.errors}{$form.f.attachment.fieldErrors}{/if}
{$form.f.attachment|unsafe}
</td>
</tr>{if $isOwner or $isMember}
<tr>
<th><strong>{$form.f.summary.labelTag}:</strong></th>
<td>{if $form.f.summary.errors}{$form.f.summary.fieldErrors}{/if}
{$form.f.summary|unsafe}
</td>
</tr>
<tr>
<th><strong>{$form.f.status.labelTag}:</strong></th>
<td>{if $form.f.status.errors}{$form.f.status.fieldErrors}{/if}
{$form.f.status|unsafe}
</td>
</tr>
<tr>
<th>{$form.f.owner.labelTag}:</th>
<td>{if $form.f.owner.errors}{$form.f.owner.fieldErrors}{/if}
{$form.f.owner|unsafe}
</td>
</tr>
<tr>
<th>{$form.f.label1.labelTag}:</th>
<td>
{if $form.f.label1.errors}{$form.f.label1.fieldErrors}{/if}{$form.f.label1|unsafe}
{if $form.f.label2.errors}{$form.f.label2.fieldErrors}{/if}{$form.f.label2|unsafe}
{if $form.f.label3.errors}{$form.f.label3.fieldErrors}{/if}{$form.f.label3|unsafe}<br />
{if $form.f.label4.errors}{$form.f.label4.fieldErrors}{/if}{$form.f.label4|unsafe}
{if $form.f.label5.errors}{$form.f.label5.fieldErrors}{/if}{$form.f.label5|unsafe}
{if $form.f.label6.errors}{$form.f.label6.fieldErrors}{/if}{$form.f.label6|unsafe}
</td>
</tr>{/if}
<tr>
<td>&nbsp;</td>
<td><input type="submit" value="{trans 'Submit Changes'}" name="submit" /> | <a href="{url 'IDF_Views_Issue::view', array($project.shortname, $issue.id)}">{trans 'Cancel'}</a>
</td>
</tr>
</table>
</form>
{/if}
{/block}
{block context}
<div class="issue-info">
{assign $submitter = $issue.get_submitter()}{aurl 'url', 'IDF_Views_User::view', array($submitter.login)}
<p><strong>{trans 'Created:'}</strong> <span class="nobrk">{$issue.creation_dtime|dateago}</span> <span class="nobrk">{blocktrans}by <a href="{$url}">{$submitter}</a>{/blocktrans}</span></p>
{if $issue.modif_dtime != $issue.creation_dtime}<p>
<strong>{trans 'Updated:'}</strong> <span class="nobrk">{$issue.modif_dtime|dateago}</span></p>{/if}
<p>
<strong>{trans 'Status:'}</strong> {$issue.get_status.name}</p>
{if $issue.get_owner != null}<p>{aurl 'url', 'IDF_Views_User::view', array($issue.get_owner().login)}
<strong>{trans 'Owner:'}</strong> <a href="{$url}">{$issue.get_owner}</a>
</p>{/if}{assign $tags = $issue.get_tags_list()}{if $tags.count()}
<p>
<strong>{trans 'Labels:'}</strong><br />
{foreach $tags as $tag}{aurl 'url', 'IDF_Views_Issue::listLabel', array($project.shortname, $tag.id, 'open')}
<span class="label"><a href="{$url}" class="label"><strong>{$tag.class}:</strong>{$tag.name}</a></span><br />
{/foreach}
</p>{/if}
</div>
{/block}
{block javascript}{if $form}{include 'idf/issues/js-autocomplete.html'}{/if}{/block}

View File

@ -0,0 +1,515 @@
diff --git a/src/IDF/Form/IssueCreate.php b/src/IDF/Form/IssueCreate.php
index 0743e72..67afca7 100644
--- a/src/IDF/Form/IssueCreate.php
+++ b/src/IDF/Form/IssueCreate.php
@@ -72,8 +72,9 @@ class IDF_Form_IssueCreate extends Pluf_Form
// We add .dummy to try to mitigate security issues in the
// case of someone allowing the upload path to be accessible
// to everybody.
- $filename = substr($md5, 0, 2).'/'.substr($md5, 2, 2).'/'.substr($md5, 4).'/%s.dummy';
- $this->fields['attachment'] = new Pluf_Form_Field_File(
+ for ($i=1;$i<4;$i++) {
+ $filename = substr($md5, 0, 2).'/'.substr($md5, 2, 2).'/'.substr($md5, 4).'/%s.dummy';
+ $this->fields['attachment'.$i] = new Pluf_Form_Field_File(
array('required' => false,
'label' => __('Attach a file'),
'move_function_params' =>
@@ -83,6 +84,7 @@ class IDF_Form_IssueCreate extends Pluf_Form
)
)
);
+ }
if ($this->show_full) {
$this->fields['status'] = new Pluf_Form_Field_Varchar(
@@ -195,6 +197,21 @@ class IDF_Form_IssueCreate extends Pluf_Form
}
/**
+ * Clean the attachments post failure.
+ */
+ function failed()
+ {
+ $upload_path = Pluf::f('upload_issue_path', false);
+ if ($upload_path == false) return;
+ for ($i=1;$i<4;$i++) {
+ if (!empty($this->cleaned_data['attachment'.$i]) and
+ file_exists($upload_path.'/'.$this->cleaned_data['attachment'.$i])) {
+ @unlink($upload_path.'/'.$this->cleaned_data['attachment'.$i]);
+ }
+ }
+ }
+
+ /**
* Save the model in the database.
*
* @param bool Commit in the database or not. If not, the object
@@ -203,61 +220,63 @@ class IDF_Form_IssueCreate extends Pluf_Form
*/
function save($commit=true)
{
- if ($this->isValid()) {
- // Add a tag for each label
- $tags = array();
- if ($this->show_full) {
- for ($i=1;$i<7;$i++) {
- if (strlen($this->cleaned_data['label'.$i]) > 0) {
- if (strpos($this->cleaned_data['label'.$i], ':') !== false) {
- list($class, $name) = explode(':', $this->cleaned_data['label'.$i], 2);
- list($class, $name) = array(trim($class), trim($name));
- } else {
- $class = 'Other';
- $name = trim($this->cleaned_data['label'.$i]);
- }
- $tags[] = IDF_Tag::add($name, $this->project, $class);
+ if (!$this->isValid()) {
+ throw new Exception(__('Cannot save the model from an invalid form.'));
+ }
+ // Add a tag for each label
+ $tags = array();
+ if ($this->show_full) {
+ for ($i=1;$i<7;$i++) {
+ if (strlen($this->cleaned_data['label'.$i]) > 0) {
+ if (strpos($this->cleaned_data['label'.$i], ':') !== false) {
+ list($class, $name) = explode(':', $this->cleaned_data['label'.$i], 2);
+ list($class, $name) = array(trim($class), trim($name));
+ } else {
+ $class = 'Other';
+ $name = trim($this->cleaned_data['label'.$i]);
}
+ $tags[] = IDF_Tag::add($name, $this->project, $class);
}
- } else {
- $tags[] = IDF_Tag::add('Medium', $this->project, 'Priority');
- $tags[] = IDF_Tag::add('Defect', $this->project, 'Type');
- }
- // Create the issue
- $issue = new IDF_Issue();
- $issue->project = $this->project;
- $issue->submitter = $this->user;
- if ($this->show_full) {
- $issue->status = IDF_Tag::add(trim($this->cleaned_data['status']), $this->project, 'Status');
- $issue->owner = self::findUser($this->cleaned_data['owner']);
- } else {
- $_t = $this->project->getTagIdsByStatus('open');
- $issue->status = new IDF_Tag($_t[0]); // first one is the default
- $issue->owner = null;
- }
- $issue->summary = trim($this->cleaned_data['summary']);
- $issue->create();
- foreach ($tags as $tag) {
- $issue->setAssoc($tag);
}
- // add the first comment
- $comment = new IDF_IssueComment();
- $comment->issue = $issue;
- $comment->content = $this->cleaned_data['content'];
- $comment->submitter = $this->user;
- $comment->create();
- // If we have a file, create the IDF_IssueFile and attach
- // it to the comment.
- if ($this->cleaned_data['attachment']) {
+ } else {
+ $tags[] = IDF_Tag::add('Medium', $this->project, 'Priority');
+ $tags[] = IDF_Tag::add('Defect', $this->project, 'Type');
+ }
+ // Create the issue
+ $issue = new IDF_Issue();
+ $issue->project = $this->project;
+ $issue->submitter = $this->user;
+ if ($this->show_full) {
+ $issue->status = IDF_Tag::add(trim($this->cleaned_data['status']), $this->project, 'Status');
+ $issue->owner = self::findUser($this->cleaned_data['owner']);
+ } else {
+ $_t = $this->project->getTagIdsByStatus('open');
+ $issue->status = new IDF_Tag($_t[0]); // first one is the default
+ $issue->owner = null;
+ }
+ $issue->summary = trim($this->cleaned_data['summary']);
+ $issue->create();
+ foreach ($tags as $tag) {
+ $issue->setAssoc($tag);
+ }
+ // add the first comment
+ $comment = new IDF_IssueComment();
+ $comment->issue = $issue;
+ $comment->content = $this->cleaned_data['content'];
+ $comment->submitter = $this->user;
+ $comment->create();
+ // If we have a file, create the IDF_IssueFile and attach
+ // it to the comment.
+ for ($i=1;$i<4;$i++) {
+ if ($this->cleaned_data['attachment'.$i]) {
$file = new IDF_IssueFile();
- $file->attachment = $this->cleaned_data['attachment'];
+ $file->attachment = $this->cleaned_data['attachment'.$i];
$file->submitter = $this->user;
$file->comment = $comment;
$file->create();
}
- return $issue;
}
- throw new Exception(__('Cannot save the model from an invalid form.'));
+ return $issue;
}
/**
diff --git a/src/IDF/Form/IssueUpdate.php b/src/IDF/Form/IssueUpdate.php
index 550889e..0d36e72 100644
--- a/src/IDF/Form/IssueUpdate.php
+++ b/src/IDF/Form/IssueUpdate.php
@@ -68,8 +68,9 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate
// We add .dummy to try to mitigate security issues in the
// case of someone allowing the upload path to be accessible
// to everybody.
- $filename = substr($md5, 0, 2).'/'.substr($md5, 2, 2).'/'.substr($md5, 4).'/%s.dummy';
- $this->fields['attachment'] = new Pluf_Form_Field_File(
+ for ($i=1;$i<4;$i++) {
+ $filename = substr($md5, 0, 2).'/'.substr($md5, 2, 2).'/'.substr($md5, 4).'/%s.dummy';
+ $this->fields['attachment'.$i] = new Pluf_Form_Field_File(
array('required' => false,
'label' => __('Attach a file'),
'move_function_params' =>
@@ -79,6 +80,7 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate
)
)
);
+ }
if ($this->show_full) {
$this->fields['status'] = new Pluf_Form_Field_Varchar(
@@ -124,6 +126,21 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate
}
/**
+ * Clean the attachments post failure.
+ */
+ function failed()
+ {
+ $upload_path = Pluf::f('upload_issue_path', false);
+ if ($upload_path == false) return;
+ for ($i=1;$i<4;$i++) {
+ if (!empty($this->cleaned_data['attachment'.$i]) and
+ file_exists($upload_path.'/'.$this->cleaned_data['attachment'.$i])) {
+ @unlink($upload_path.'/'.$this->cleaned_data['attachment'.$i]);
+ }
+ }
+ }
+
+ /**
* We check that something is really changed.
*/
public function clean()
@@ -202,90 +219,92 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate
*/
function save($commit=true)
{
- if ($this->isValid()) {
- if ($this->show_full) {
- // Add a tag for each label
- $tags = array();
- $tagids = array();
- for ($i=1;$i<7;$i++) {
- if (strlen($this->cleaned_data['label'.$i]) > 0) {
- if (strpos($this->cleaned_data['label'.$i], ':') !== false) {
- list($class, $name) = explode(':', $this->cleaned_data['label'.$i], 2);
- list($class, $name) = array(trim($class), trim($name));
- } else {
- $class = 'Other';
- $name = trim($this->cleaned_data['label'.$i]);
- }
- $tag = IDF_Tag::add($name, $this->project, $class);
- $tags[] = $tag;
- $tagids[] = $tag->id;
+ if (!$this->isValid()) {
+ throw new Exception(__('Cannot save the model from an invalid form.'));
+ }
+ if ($this->show_full) {
+ // Add a tag for each label
+ $tags = array();
+ $tagids = array();
+ for ($i=1;$i<7;$i++) {
+ if (strlen($this->cleaned_data['label'.$i]) > 0) {
+ if (strpos($this->cleaned_data['label'.$i], ':') !== false) {
+ list($class, $name) = explode(':', $this->cleaned_data['label'.$i], 2);
+ list($class, $name) = array(trim($class), trim($name));
+ } else {
+ $class = 'Other';
+ $name = trim($this->cleaned_data['label'.$i]);
}
+ $tag = IDF_Tag::add($name, $this->project, $class);
+ $tags[] = $tag;
+ $tagids[] = $tag->id;
}
- // Compare between the old and the new data
- $changes = array();
- $oldtags = $this->issue->get_tags_list();
- foreach ($tags as $tag) {
- if (!Pluf_Model_InArray($tag, $oldtags)) {
- if (!isset($changes['lb'])) $changes['lb'] = array();
- if ($tag->class != 'Other') {
- $changes['lb'][] = (string) $tag; //new tag
- } else {
- $changes['lb'][] = (string) $tag->name;
- }
+ }
+ // Compare between the old and the new data
+ $changes = array();
+ $oldtags = $this->issue->get_tags_list();
+ foreach ($tags as $tag) {
+ if (!Pluf_Model_InArray($tag, $oldtags)) {
+ if (!isset($changes['lb'])) $changes['lb'] = array();
+ if ($tag->class != 'Other') {
+ $changes['lb'][] = (string) $tag; //new tag
+ } else {
+ $changes['lb'][] = (string) $tag->name;
}
}
- foreach ($oldtags as $tag) {
- if (!Pluf_Model_InArray($tag, $tags)) {
- if (!isset($changes['lb'])) $changes['lb'] = array();
- if ($tag->class != 'Other') {
- $changes['lb'][] = '-'.(string) $tag; //new tag
- } else {
- $changes['lb'][] = '-'.(string) $tag->name;
- }
+ }
+ foreach ($oldtags as $tag) {
+ if (!Pluf_Model_InArray($tag, $tags)) {
+ if (!isset($changes['lb'])) $changes['lb'] = array();
+ if ($tag->class != 'Other') {
+ $changes['lb'][] = '-'.(string) $tag; //new tag
+ } else {
+ $changes['lb'][] = '-'.(string) $tag->name;
}
}
- // Status, summary and owner
- $status = IDF_Tag::add(trim($this->cleaned_data['status']), $this->project, 'Status');
- if ($status->id != $this->issue->status) {
- $changes['st'] = $status->name;
- }
- if (trim($this->issue->summary) != trim($this->cleaned_data['summary'])) {
- $changes['su'] = trim($this->cleaned_data['summary']);
- }
- $owner = self::findUser($this->cleaned_data['owner']);
- if ((is_null($owner) and !is_null($this->issue->get_owner()))
- or (!is_null($owner) and is_null($this->issue->get_owner()))
- or ((!is_null($owner) and !is_null($this->issue->get_owner())) and $owner->id != $this->issue->get_owner()->id)) {
- $changes['ow'] = (is_null($owner)) ? '---' : $owner->login;
- }
- // Update the issue
- $this->issue->batchAssoc('IDF_Tag', $tagids);
- $this->issue->summary = trim($this->cleaned_data['summary']);
- $this->issue->status = $status;
- $this->issue->owner = $owner;
}
- // Create the comment
- $comment = new IDF_IssueComment();
- $comment->issue = $this->issue;
- $comment->content = $this->cleaned_data['content'];
- $comment->submitter = $this->user;
- if (!$this->show_full) $changes = array();
- $comment->changes = $changes;
- $comment->create();
- $this->issue->update();
- if ($this->issue->owner != $this->user->id and
- $this->issue->submitter != $this->user->id) {
- $this->issue->setAssoc($this->user); // interested user.
+ // Status, summary and owner
+ $status = IDF_Tag::add(trim($this->cleaned_data['status']), $this->project, 'Status');
+ if ($status->id != $this->issue->status) {
+ $changes['st'] = $status->name;
}
- if ($this->cleaned_data['attachment']) {
+ if (trim($this->issue->summary) != trim($this->cleaned_data['summary'])) {
+ $changes['su'] = trim($this->cleaned_data['summary']);
+ }
+ $owner = self::findUser($this->cleaned_data['owner']);
+ if ((is_null($owner) and !is_null($this->issue->get_owner()))
+ or (!is_null($owner) and is_null($this->issue->get_owner()))
+ or ((!is_null($owner) and !is_null($this->issue->get_owner())) and $owner->id != $this->issue->get_owner()->id)) {
+ $changes['ow'] = (is_null($owner)) ? '---' : $owner->login;
+ }
+ // Update the issue
+ $this->issue->batchAssoc('IDF_Tag', $tagids);
+ $this->issue->summary = trim($this->cleaned_data['summary']);
+ $this->issue->status = $status;
+ $this->issue->owner = $owner;
+ }
+ // Create the comment
+ $comment = new IDF_IssueComment();
+ $comment->issue = $this->issue;
+ $comment->content = $this->cleaned_data['content'];
+ $comment->submitter = $this->user;
+ if (!$this->show_full) $changes = array();
+ $comment->changes = $changes;
+ $comment->create();
+ $this->issue->update();
+ if ($this->issue->owner != $this->user->id and
+ $this->issue->submitter != $this->user->id) {
+ $this->issue->setAssoc($this->user); // interested user.
+ }
+ for ($i=1;$i<4;$i++) {
+ if ($this->cleaned_data['attachment'.$i]) {
$file = new IDF_IssueFile();
- $file->attachment = $this->cleaned_data['attachment'];
+ $file->attachment = $this->cleaned_data['attachment'.$i];
$file->submitter = $this->user;
$file->comment = $comment;
$file->create();
}
- return $this->issue;
}
- throw new Exception(__('Cannot save the model from an invalid form.'));
+ return $this->issue;
}
}
diff --git a/src/IDF/IssueFile.php b/src/IDF/IssueFile.php
index f4367dd..f0745e8 100644
--- a/src/IDF/IssueFile.php
+++ b/src/IDF/IssueFile.php
@@ -114,6 +114,7 @@ class IDF_IssueFile extends Pluf_Model
$this->filename = substr(basename($file), 0, -6);
$img_extensions = array('jpeg', 'jpg', 'png', 'gif');
$info = pathinfo($this->filename);
+ if (!isset($info['extension'])) $info['extension'] = '';
if (in_array(strtolower($info['extension']), $img_extensions)) {
$this->type = 'img';
} else {
diff --git a/src/IDF/templates/idf/issues/create.html b/src/IDF/templates/idf/issues/create.html
index e8f4a5b..faaa743 100644
--- a/src/IDF/templates/idf/issues/create.html
+++ b/src/IDF/templates/idf/issues/create.html
@@ -24,10 +24,22 @@
{$form.f.content|unsafe}
</td>
</tr>
-<tr>
-<th>{$form.f.attachment.labelTag}:</th>
-<td>{if $form.f.attachment.errors}{$form.f.attachment.fieldErrors}{/if}
-{$form.f.attachment|unsafe}
+<tr id="form-attachment-1">
+<th>{$form.f.attachment1.labelTag}:</th>
+<td>{if $form.f.attachment1.errors}{$form.f.attachment1.fieldErrors}{/if}
+{$form.f.attachment1|unsafe}
+</td>
+</tr>
+<tr id="form-attachment-2">
+<th>{$form.f.attachment2.labelTag}:</th>
+<td>{if $form.f.attachment2.errors}{$form.f.attachment2.fieldErrors}{/if}
+{$form.f.attachment2|unsafe}
+</td>
+</tr>
+<tr id="form-attachment-3">
+<th>{$form.f.attachment3.labelTag}:</th>
+<td>{if $form.f.attachment3.errors}{$form.f.attachment3.fieldErrors}{/if}
+{$form.f.attachment3|unsafe}
</td>
</tr>{if $isOwner or $isMember}
<tr>
@@ -74,7 +86,34 @@
{/block}
{block javascript}
<script type="text/javascript">
-document.getElementById('id_summary').focus()
+document.getElementById('id_summary').focus();{literal}
+$(document).ready(function(){
+
+ // Hide the upload forms, we insert before the first attach file
+ // row an "Attach File" little link.
+ // We hide all the rows.
+ $("#form-attachment-1").before("{/literal}<tr id=\"form-block-0\"><td>&nbsp;</td><td><img style=\"vertical-align: text-bottom;\" src=\"{media '/idf/img/attachment.png'}\" alt=\" \" align=\"bottom\" /><a id=\"form-show-0\" href=\"#\">{trans 'Attach file'}{literal}</a></td></tr>");
+ $("#form-show-0").click(function(){
+ $("#form-attachment-1").show();
+ $("#form-block-0").hide();
+ });
+ $("#form-attachment-1 td").append("<span id=\"form-block-1\"><a id=\"form-show-1\" href=\"#\">{/literal}{trans 'Attach another file'}{literal}</a></span>");
+ $("#form-show-1").click(function(){
+ $("#form-attachment-2").show();
+ $("#form-block-1").hide();
+ });
+ $("#form-attachment-2 td").append("<span id=\"form-block-2\"><a id=\"form-show-2\" href=\"#\">{/literal}{trans 'Attach another file'}{literal}</a></span>");
+ $("#form-show-2").click(function(){
+ $("#form-attachment-3").show();
+ $("#form-block-2").hide();
+ });
+ var j=0;
+ for (j=1;j<4;j=j+1) {
+ $("#form-attachment-"+j).hide();
+ }
+ });
</script>
+{/literal}{/block}
+
{include 'idf/issues/js-autocomplete.html'}{/block}
diff --git a/src/IDF/templates/idf/issues/view.html b/src/IDF/templates/idf/issues/view.html
index cb9e085..ad56d05 100644
--- a/src/IDF/templates/idf/issues/view.html
+++ b/src/IDF/templates/idf/issues/view.html
@@ -59,10 +59,22 @@
{$form.f.content|unsafe}
</td>
</tr>
-<tr>
-<th>{$form.f.attachment.labelTag}:</th>
-<td>{if $form.f.attachment.errors}{$form.f.attachment.fieldErrors}{/if}
-{$form.f.attachment|unsafe}
+<tr id="form-attachment-1">
+<th>{$form.f.attachment1.labelTag}:</th>
+<td>{if $form.f.attachment1.errors}{$form.f.attachment1.fieldErrors}{/if}
+{$form.f.attachment1|unsafe}
+</td>
+</tr>
+<tr id="form-attachment-2">
+<th>{$form.f.attachment2.labelTag}:</th>
+<td>{if $form.f.attachment2.errors}{$form.f.attachment2.fieldErrors}{/if}
+{$form.f.attachment2|unsafe}
+</td>
+</tr>
+<tr id="form-attachment-3">
+<th>{$form.f.attachment3.labelTag}:</th>
+<td>{if $form.f.attachment3.errors}{$form.f.attachment3.fieldErrors}{/if}
+{$form.f.attachment3|unsafe}
</td>
</tr>{if $isOwner or $isMember}
<tr>
@@ -122,4 +134,33 @@
</p>{/if}
</div>
{/block}
-{block javascript}{if $form}{include 'idf/issues/js-autocomplete.html'}{/if}{/block}
+{block javascript}{if $form}{include 'idf/issues/js-autocomplete.html'}
+<script type="text/javascript">
+{literal}
+$(document).ready(function(){
+
+ // Hide the upload forms, we insert before the first attach file
+ // row an "Attach File" little link.
+ // We hide all the rows.
+ $("#form-attachment-1").before("{/literal}<tr id=\"form-block-0\"><td>&nbsp;</td><td><img style=\"vertical-align: text-bottom;\" src=\"{media '/idf/img/attachment.png'}\" alt=\" \" align=\"bottom\" /><a id=\"form-show-0\" href=\"#\">{trans 'Attach file'}{literal}</a></td></tr>");
+ $("#form-show-0").click(function(){
+ $("#form-attachment-1").show();
+ $("#form-block-0").hide();
+ });
+ $("#form-attachment-1 td").append("<span id=\"form-block-1\"><a id=\"form-show-1\" href=\"#\">{/literal}{trans 'Attach another file'}{literal}</a></span>");
+ $("#form-show-1").click(function(){
+ $("#form-attachment-2").show();
+ $("#form-block-1").hide();
+ });
+ $("#form-attachment-2 td").append("<span id=\"form-block-2\"><a id=\"form-show-2\" href=\"#\">{/literal}{trans 'Attach another file'}{literal}</a></span>");
+ $("#form-show-2").click(function(){
+ $("#form-attachment-3").show();
+ $("#form-block-2").hide();
+ });
+ var j=0;
+ for (j=1;j<4;j=j+1) {
+ $("#form-attachment-"+j).hide();
+ }
+ });{/literal}
+</script>
+{/if}{/block}
diff --git a/www/media/idf/img/attachment.png b/www/media/idf/img/attachment.png
new file mode 100644
index 0000000..529bb7f
Binary files /dev/null and b/www/media/idf/img/attachment.png differ

View File

@ -347,7 +347,7 @@ class IDF_Views_Project
$params = array(); $params = array();
$keys = array('downloads_access_rights', 'source_access_rights', $keys = array('downloads_access_rights', 'source_access_rights',
'issues_access_rights', 'private_project', 'issues_access_rights', 'private_project',
'wiki_access_rights'); 'review_access_rights', 'wiki_access_rights');
foreach ($keys as $key) { foreach ($keys as $key) {
$_val = $request->conf->getVal($key, false); $_val = $request->conf->getVal($key, false);
if ($_val !== false) { if ($_val !== false) {

227
src/IDF/Views/Review.php Normal file
View File

@ -0,0 +1,227 @@
<?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');
Pluf::loadFunction('Pluf_Shortcuts_RenderToResponse');
Pluf::loadFunction('Pluf_Shortcuts_GetObjectOr404');
Pluf::loadFunction('Pluf_Shortcuts_GetFormForModel');
/**
* Review views.
*/
class IDF_Views_Review
{
/**
* View list of reviews for a given project.
*/
public $index_precond = array('IDF_Precondition::accessReview');
public function index($request, $match)
{
$prj = $request->project;
$title = sprintf(__('%s Code Reviews'), (string) $prj);
// Paginator to paginate the pages
$pag = new Pluf_Paginator(new IDF_Review());
$pag->class = 'recent-issues';
$pag->item_extra_props = array('project_m' => $prj,
'shortname' => $prj->shortname,
'current_user' => $request->user);
$pag->summary = __('This table shows the latest reviews.');
$pag->action = array('IDF_Views_Review::index', array($prj->shortname));
$otags = $prj->getTagIdsByStatus('open');
if (count($otags) == 0) $otags[] = 0;
$pag->forced_where = new Pluf_SQL('project=%s AND status IN ('.implode(', ', $otags).')', array($prj->id));
$pag->action = array('IDF_Views_Issue::index', array($prj->shortname));
$pag->sort_order = array('modif_dtime', 'ASC'); // will be reverted
$pag->sort_reverse_order = array('modif_dtime');
$list_display = array(
'id' => __('Id'),
array('summary', 'IDF_Views_Review_SummaryAndLabels', __('Summary')),
array('status', 'IDF_Views_Issue_ShowStatus', __('Status')),
array('modif_dtime', 'Pluf_Paginator_DateAgo', __('Last Updated')),
);
$pag->configure($list_display, array(), array('title', 'modif_dtime'));
$pag->items_per_page = 25;
$pag->no_results_text = __('No reviews were found.');
$pag->sort_order = array('modif_dtime', 'ASC');
$pag->setFromRequest($request);
return Pluf_Shortcuts_RenderToResponse('idf/review/index.html',
array(
'page_title' => $title,
'reviews' => $pag,
),
$request);
}
/**
* Create a new code review.
*/
public $create_precond = array('IDF_Precondition::accessReview',
'Pluf_Precondition::loginRequired');
public function create($request, $match)
{
$prj = $request->project;
$title = __('Start Code Review');
if ($request->method == 'POST') {
$form = new IDF_Form_ReviewCreate(array_merge($request->POST,
$request->FILES),
array('project' => $prj,
'user' => $request->user
));
if ($form->isValid()) {
$review = $form->save();
$urlr = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
array($prj->shortname, $review->id));
$request->user->setMessage(sprintf(__('The <a href="%s">code review %d</a> has been created.'), $urlr, $review->id));
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::index',
array($prj->shortname));
return new Pluf_HTTP_Response_Redirect($url);
}
} else {
$form = new IDF_Form_ReviewCreate(null,
array('project' => $prj,
'user' => $request->user));
}
return Pluf_Shortcuts_RenderToResponse('idf/review/create.html',
array(
'page_title' => $title,
'form' => $form,
),
$request);
}
/**
* Download the patch of a review.
*/
public $getPatch_precond = array('IDF_Precondition::accessReview');
public function getPatch($request, $match)
{
$prj = $request->project;
$patch = Pluf_Shortcuts_GetObjectOr404('IDF_Review_Patch', $match[2]);
$prj->inOr404($patch->get_review());
$file = Pluf::f('upload_issue_path').'/'.$patch->patch;
$rep = new Pluf_HTTP_Response_File($file, 'text/plain');
$rep->headers['Content-Disposition'] = 'attachment; filename="'.$patch->id.'.diff"';
return $rep;
}
/**
* View a code review.
*/
public $view_precond = array('IDF_Precondition::accessReview');
public function view($request, $match)
{
$prj = $request->project;
$review = Pluf_Shortcuts_GetObjectOr404('IDF_Review', $match[2]);
$prj->inOr404($review);
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
array($prj->shortname, $review->id));
$title = Pluf_Template::markSafe(sprintf(__('Review <a href="%s">%d</a>: %s'), $url, $review->id, $review->summary));
$patches = $review->get_patches_list();
$patch = $patches[0];
$diff = new IDF_Diff(file_get_contents(Pluf::f('upload_issue_path').'/'.$patch->patch));
$diff->parse();
// The form to submit comments is based on the files in the
// diff
if ($request->method == 'POST' and !$request->user->isAnonymous()) {
$form = new IDF_Form_ReviewFileComment($request->POST,
array('files' => $diff->files,
'user' => $request->user,
'patch' => $patch,
));
if ($form->isValid()) {
$patch = $form->save();
$urlr = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
array($prj->shortname, $patch->get_review()->id));
$request->user->setMessage(sprintf(__('Your <a href="%s">code review %d</a> has been published.'), $urlr, $patch->get_review()->id));
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Review::index',
array($prj->shortname));
return new Pluf_HTTP_Response_Redirect($url);
}
} else {
$form = new IDF_Form_ReviewFileComment(null,
array('files' => $diff->files,
'user' => $request->user,
'patch' => $patch,));
}
$scm = IDF_Scm::get($request);
$files = array();
$reviewers = array();
foreach ($diff->files as $filename => $def) {
$fileinfo = $scm->getFileInfo($filename, $patch->get_commit()->scm_id);
$sql = new Pluf_SQL('cfile=%s', array($filename));
$cts = $patch->get_filecomments_list(array('filter'=>$sql->gen(),
'order'=>'creation_dtime ASC'));
foreach ($cts as $ct) {
$reviewers[] = $ct->get_submitter();
}
if (count($def['chunks'])) {
$orig_file = $scm->getBlob($fileinfo);
$files[$filename] = array(
$diff->fileCompare($orig_file, $def, $filename),
$form->f->{md5($filename)},
$cts,
);
} else {
$files[$filename] = array('', $form->f->{md5($filename)}, $cts);
}
}
$reviewers = Pluf_Model_RemoveDuplicates($reviewers);
return Pluf_Shortcuts_RenderToResponse('idf/review/view.html',
array(
'page_title' => $title,
'review' => $review,
'files' => $files,
'diff' => $diff,
'patch' => $patch,
'form' => $form,
'reviewers' => $reviewers,
),
$request);
}
}
/**
* Display the summary of an review, then on a new line, display the
* list of labels with a link to a view "by label only".
*
* The summary of the review is linking to the review.
*/
function IDF_Views_Review_SummaryAndLabels($field, $review, $extra='')
{
$edit = Pluf_HTTP_URL_urlForView('IDF_Views_Review::view',
array($review->shortname, $review->id));
$tags = array();
foreach ($review->get_tags_list() as $tag) {
$tags[] = Pluf_esc($tag);
}
$out = '';
if (count($tags)) {
$out = '<br /><span class="label note">'.implode(', ', $tags).'</span>';
}
return sprintf('<a href="%s">%s</a>', $edit, Pluf_esc($review->summary)).$out;
}

View File

@ -24,9 +24,12 @@
$cfg = array(); $cfg = array();
# #
# You must set it to false once everything is running ok. # You must set them to false once everything is running ok.
# #
$cfg['debug'] = true; $cfg['debug'] = true;
# It will help you catch errors at beginning when configuring your
# SCM backend. It must be turned off in production.
$cfg['debug_scm'] = true;
# If you have a single git repository, just put the full path to it # If you have a single git repository, just put the full path to it
# without trailing slash. The path is the path to the git database, # without trailing slash. The path is the path to the git database,

View File

@ -283,6 +283,32 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/downloads/(\d+)/delete/$#',
'model' => 'IDF_Views_Download', 'model' => 'IDF_Views_Download',
'method' => 'delete'); 'method' => 'delete');
// ---------- CODE REVIEW --------------------------------
$ctl[] = array('regex' => '#^/p/([\-\w]+)/review/$#',
'base' => $base,
'priority' => 4,
'model' => 'IDF_Views_Review',
'method' => 'index');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/review/(\d+)/$#',
'base' => $base,
'priority' => 4,
'model' => 'IDF_Views_Review',
'method' => 'view');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/review/create/$#',
'base' => $base,
'priority' => 4,
'model' => 'IDF_Views_Review',
'method' => 'create');
$ctl[] = array('regex' => '#^/p/([\-\w]+)/review/getpatch/(\d+)/$#',
'base' => $base,
'priority' => 4,
'model' => 'IDF_Views_Review',
'method' => 'getPatch');
// ---------- ADMIN -------------------------------------- // ---------- ADMIN --------------------------------------

View File

@ -33,5 +33,9 @@ $m['IDF_Search_Occ'] = array('relate_to' => array('IDF_Project'),);
$m['IDF_WikiPage'] = array('relate_to' => array('IDF_Project', 'Pluf_User'), $m['IDF_WikiPage'] = array('relate_to' => array('IDF_Project', 'Pluf_User'),
'relate_to_many' => array('IDF_Tag', 'Pluf_User')); 'relate_to_many' => array('IDF_Tag', 'Pluf_User'));
$m['IDF_WikiRevision'] = array('relate_to' => array('IDF_WikiPage', 'Pluf_User')); $m['IDF_WikiRevision'] = array('relate_to' => array('IDF_WikiPage', 'Pluf_User'));
$m['IDF_Review'] = array('relate_to' => array('IDF_Project', 'Pluf_User', 'IDF_Tag'),
'relate_to_many' => array('IDF_Tag', 'Pluf_User'));
$m['IDF_Review_Patch'] = array('relate_to' => array('IDF_Review', 'Pluf_User'));
$m['IDF_Review_FileComment'] = array('relate_to' => array('IDF_Review_Patch', 'Pluf_User'));
return $m; return $m;

View File

@ -36,6 +36,12 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th><strong>{$form.f.review_access_rights.labelTag}:</strong></th>
<td>{if $form.f.review_access_rights.errors}{$form.f.review_access_rights.fieldErrors}{/if}
{$form.f.review_access_rights|unsafe}
</td>
</tr>
<tr>
<th>{if $form.f.private_project.errors}{$form.f.private_project.fieldErrors}{/if} <th>{if $form.f.private_project.errors}{$form.f.private_project.fieldErrors}{/if}
{$form.f.private_project|unsafe} {$form.f.private_project|unsafe}
</th> </th>

View File

@ -0,0 +1,87 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
{*
# ***** 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 *****
*}<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="{media '/idf/css/yui.css'}" />
<link rel="stylesheet" type="text/css" href="{media '/idf/css/style.css'}" />
<!--[if lt IE 7]>
<link rel="stylesheet" type="text/css" href="{media '/idf/css/ie6.css'}" />
<![endif]-->
{block extraheader}{/block}
<title>{block pagetitle}{$page_title|strip_tags}{/block}</title>
</head>
<body>
<div id="{block docid}doc3{/block}">
<div id="hd">
{if $project}<h1 class="project-title">{$project}</h1>{/if}
<p class="top"><a href="#title" accesskey="2"></a>
{if !$user.isAnonymous()}{aurl 'url', 'IDF_Views_User::myAccount'}{blocktrans}Welcome, <strong><a class="userw" href="{$url}">{$user}</a></strong>.{/blocktrans} <a href="{url 'IDF_Views::logout'}">{trans 'Sign Out'}</a>{else}<a href="{url 'IDF_Views::login'}">{trans 'Sign in or create your account'}</a>{/if}
{if $project} | <a href="{url 'IDF_Views::index'}">{trans 'Project List'}</a>{/if}
| <a href="{url 'IDF_Views::faq'}" title="{trans 'Help and accessibility features'}">{trans 'Help'}</a>
</p>
<div id="header">
<div id="main-tabs">
{if $project}
<a accesskey="1" href="{url 'IDF_Views_Project::home', array($project.shortname)}"{block tabhome}{/block}>{trans 'Project Home'}</a>
{if $hasDownloadsAccess} <a href="{url 'IDF_Views_Download::index', array($project.shortname)}"{block tabdownloads}{/block}>{trans 'Downloads'}</a>{/if}
{if $hasWikiAccess} <a href="{url 'IDF_Views_Wiki::index', array($project.shortname)}"{block tabwiki}{/block}>{trans 'Documentation'}</a>{/if}
{if $hasIssuesAccess} <a href="{url 'IDF_Views_Issue::index', array($project.shortname)}"{block tabissues}{/block}>{trans 'Issues'}</a>{/if}
{if $hasSourceAccess} <a href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $project.getScmRoot())}"{block tabsource}{/block}>{trans 'Source'}</a>{/if}
{if $hasReviewAccess} <a href="{url 'IDF_Views_Review::index', array($project.shortname)}"{block tabreview}{/block}>{trans 'Code Review'}</a>{/if}
{if $isOwner}
<a href="{url 'IDF_Views_Project::admin', array($project.shortname)}"{block tabadmin}{/block}>{trans 'Administer'}</a>{/if}{/if}
</div>
{block subtabs}{if $user.isAnonymous()} | {aurl 'url', 'IDF_Views::login'}{blocktrans}<a href="{$url}">Sign in or create your account</a> to create issues or add comments{/blocktrans}{/if}{/block}
</div>
<h1 class="title" id="title">{block titleicon}{/block}{block title}{$page_title}{/block}</h1>
</div>
<div id="bd">
<div id="yui-main">
<div class="yui-b">
<div class="yui-g">
{if $user and $user.id}{getmsgs $user}{/if}
<div class="content">{block body}{/block}</div>
</div>
</div>
</div>
</div>
<div id="ft">{block foot}{/block}</div>
</div>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
{include 'idf/js-hotkeys.html'}
{block javascript}{/block}
{if $project}
<script type="text/javascript">{literal}
<!-- //
$(document).ready(function(){
var frag = location.hash;
if (frag.length > 3 && frag.substring(0, 3) == '#ic') {
$(frag).addClass("issue-comment-focus");
}
});
// -->{/literal}
</script>{/if}
</body>
</html>

View File

@ -51,7 +51,7 @@
</div> </div>
<div class="yui-b context">{block context}{/block}</div> <div class="yui-b context">{block context}{/block}</div>
</div> </div>
<div id="ft">{block foot}<a href="http://www.indefero.net" title="InDefero, bug tracking and more"><img src="{media '/idf/img/powered-by-indefero.png'}" alt="InDefero Logo" /></a>{/block}</div> <div id="ft">{block foot}{/block}</div>
</div> </div>
<script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script> <script type="text/javascript" src="{media '/idf/js/jquery-1.2.6.min.js'}"></script>
{include 'idf/js-hotkeys.html'} {include 'idf/js-hotkeys.html'}

View File

@ -47,6 +47,7 @@
{if $hasWikiAccess} <a href="{url 'IDF_Views_Wiki::index', array($project.shortname)}"{block tabwiki}{/block}>{trans 'Documentation'}</a>{/if} {if $hasWikiAccess} <a href="{url 'IDF_Views_Wiki::index', array($project.shortname)}"{block tabwiki}{/block}>{trans 'Documentation'}</a>{/if}
{if $hasIssuesAccess} <a href="{url 'IDF_Views_Issue::index', array($project.shortname)}"{block tabissues}{/block}>{trans 'Issues'}</a>{/if} {if $hasIssuesAccess} <a href="{url 'IDF_Views_Issue::index', array($project.shortname)}"{block tabissues}{/block}>{trans 'Issues'}</a>{/if}
{if $hasSourceAccess} <a href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $project.getScmRoot())}"{block tabsource}{/block}>{trans 'Source'}</a>{/if} {if $hasSourceAccess} <a href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $project.getScmRoot())}"{block tabsource}{/block}>{trans 'Source'}</a>{/if}
{if $hasReviewAccess} <a href="{url 'IDF_Views_Review::index', array($project.shortname)}"{block tabreview}{/block}>{trans 'Code Review'}</a>{/if}
{if $isOwner} {if $isOwner}
<a href="{url 'IDF_Views_Project::admin', array($project.shortname)}"{block tabadmin}{/block}>{trans 'Administer'}</a>{/if}{/if} <a href="{url 'IDF_Views_Project::admin', array($project.shortname)}"{block tabadmin}{/block}>{trans 'Administer'}</a>{/if}{/if}
</div> </div>

View File

@ -13,3 +13,4 @@
{block context} {block context}
<p><strong>{trans 'Managed Projects:'}</strong> {$projects.count()}</p> <p><strong>{trans 'Managed Projects:'}</strong> {$projects.count()}</p>
{/block} {/block}
{block foot}<div id="branding">Powered by <a href="http://www.indefero.net" title="InDefero, bug tracking and more">InDefero</a>,<br />a <a href="http://www.ceondo.com">Céondo Ltd</a> initiative.</div>{/block}

View File

@ -113,7 +113,6 @@ $(document).ready(function(){
} }
}); });
</script> </script>
{/literal}{/block} {/literal}
{include 'idf/issues/js-autocomplete.html'}{/block} {include 'idf/issues/js-autocomplete.html'}{/block}

View File

@ -40,4 +40,4 @@
</p> </p>
{/block} {/block}
{block foot}<div id="branding">Powered by <a href="http://www.indefero.net" title="InDefero, bug tracking and more">InDefero</a>,<br />a <a href="http://www.ceondo.com">Céondo Ltd</a> initiative.</div>{/block}

View File

@ -0,0 +1,15 @@
{extends "idf/base-full.html"}
{block tabreview} class="active"{/block}
{block subtabs}
<div id="sub-tabs">
<a {if $inOpenReviews}class="active" {/if}href="{url 'IDF_Views_Review::index', array($project.shortname)}">{trans 'Open Reviews'}</a> {*
{if !$user.isAnonymous()} | <a {if $inCreate}class="active" {/if}href="{url 'IDF_Views_Issue::create', array($project.shortname)}">{trans 'New Issue'}</a> | <a {if $inMyIssues}class="active" {/if}href="{url 'IDF_Views_Issue::myIssues', array($project.shortname, 'submit')}">{trans 'My Issues'}</a>{/if} |
<form class="star" action="{url 'IDF_Views_Issue::search', array($project.shortname)}" method="get">
<input accesskey="4" type="text" value="{$q}" name="q" size="20" />
<input type="submit" name="s" value="{trans 'Search'}" />
</form>
*}
{superblock}
</div>
{/block}

View File

@ -0,0 +1,9 @@
{extends "idf/base.html"}
{block tabreview} class="active"{/block}
{block subtabs}
<div id="sub-tabs">
<a {if $inOpenReviews}class="active" {/if}href="{url 'IDF_Views_Review::index', array($project.shortname)}">{trans 'Open Reviews'}</a>
{if !$user.isAnonymous()} | <a {if $inCreate}class="active" {/if}href="{url 'IDF_Views_Review::create', array($project.shortname)}">{trans 'Start Code Review'}</a> {/if}
{superblock}
</div>
{/block}

View File

@ -0,0 +1,69 @@
{extends "idf/review/base.html"}
{block docclass}yui-t3{assign $inCreate = true}{/block}
{block body}
{if $form.errors}
<div class="px-message-error">
<p>{trans 'The form contains some errors. Please correct them to submit the code review.'}</p>
{if $form.get_top_errors}
{$form.render_top_errors|unsafe}
{/if}
</div>
{/if}
<form method="post" enctype="multipart/form-data" action=".">
<table class="form" summary="">
<tr>
<th><strong>{$form.f.summary.labelTag}:</strong></th>
<td>{if $form.f.summary.errors}{$form.f.summary.fieldErrors}{/if}
{$form.f.summary|unsafe}
</td>
</tr>
<tr>
<th><strong>{$form.f.description.labelTag}:</strong></th>
<td>{if $form.f.description.errors}{$form.f.description.fieldErrors}{/if}
{$form.f.description|unsafe}
</td>
</tr>
<tr>
<th><strong>{$form.f.commit.labelTag}:</strong></th>
<td>{if $form.f.commit.errors}{$form.f.commit.fieldErrors}{/if}
{$form.f.commit|unsafe}<br />
<span class="helptext">{trans 'Be sure to provide the right commit/revision reference for your patch to correctly apply.'}</span>
</td>
</tr>
<tr>
<th><strong>{$form.f.patch.labelTag}:</strong></th>
<td>{if $form.f.patch.errors}{$form.f.patch.fieldErrors}{/if}
{$form.f.patch|unsafe}
</td>
</tr>{if $isOwner or $isMember}
<tr>
<th><strong>{$form.f.status.labelTag}:</strong></th>
<td>{if $form.f.status.errors}{$form.f.status.fieldErrors}{/if}
{$form.f.status|unsafe}
</td>
</tr>{/if}
<tr>
<td>&nbsp;</td>
<td><input type="submit" value="{trans 'Start Code Review'}" name="submit" /> | <a href="{url 'IDF_Views_Review::index', array($project.shortname)}">{trans 'Cancel'}</a>
</td>
</tr>
</table>
</form>
{/block}
{block context}
<div class="issue-submit-info">
{blocktrans}<p>To start a code review, you need to provide:</p>
<ul>
<li>A commit or revision of the current code in the repository from which you started your work.</li>
<li>A patch describing your changes with respect to the reference commit.</li>
<li><strong>Check your patch to not provide any password or confidential information!</strong></li>
</ul>{/blocktrans}
</div>
{/block}
{block javascript}
<script type="text/javascript">
document.getElementById('id_summary').focus();
</script>{/block}

View File

@ -0,0 +1,21 @@
{extends "idf/review/base.html"}
{block docclass}yui-t2{assign $inOpenReviews=true}{/block}
{block body}
{$reviews.render}
{if !$user.isAnonymous()}
{aurl 'url', 'IDF_Views_Review::create', array($project.shortname)}
<p><a href="{$url}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/add.png'}" alt="+" align="bottom" /></a> <a href="{$url}">{trans 'New Code Review'}</a></p>{/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}<p><strong>Open issues:</strong> <a href="{$open_url}">{$open}</a></p>
<p><strong>Closed issues:</strong> <a href="{$closed_url}">{$closed}</a></p>{/blocktrans}
{assign $class = ''}{assign $i = 0}
<p class="smaller">{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}<br />{/if}<strong class="label">{$label.class}:</strong> {/if}
<a href="{$url}" class="label">{$label.name}</a>,{assign $class = $label.class}{assign $i = $i + 1}{/foreach}</p>
*}{/block}

View File

@ -0,0 +1,116 @@
{extends "idf/review/base-full.html"}
{block extraheader}<link rel="stylesheet" type="text/css" href="{media '/idf/css/prettify.css'}" />{/block}
{block docclass}yui-t1{assign $inCreate = true}{/block}
{block body}
{if $form.errors}
<div class="px-message-error">
<p>{trans 'The form contains some errors. Please correct them to submit your review.'}</p>
{if $form.get_top_errors}
{$form.render_top_errors|unsafe}
{/if}
</div>
{/if}
<table class="commit" summary="">
<tr>
<th><strong>{trans 'Created:'}</strong></th><td>{$patch.creation_dtime|date:"%Y-%m-%d %H:%M:%S"} ({$patch.creation_dtime|dateago})</td>
</tr>
<tr>
<th><strong>{trans 'Updated:'}</strong></th><td>{$review.modif_dtime|dateago}</td>
</tr>
<tr>
<th><strong>{trans 'Author:'}</strong></th><td>{$review.get_submitter()}</td>
</tr>
<tr>
<th><strong>{trans 'Commit:'}</strong></th><td class="mono"><a href="{url 'IDF_Views_Source::treeBase', array($project.shortname, $patch.get_commit().scm_id)}" title="{trans 'View corresponding source tree'}">{$patch.get_commit().scm_id}</a></td>
</tr>
<tr>
<th><strong>{trans 'Description:'}</strong></th><td>{issuetext $review.summary, $request}<br /><br />{issuetext $patch.summary, $request}</td>
</tr>
<tr>
<th><strong>{trans 'Reviewers:'}</strong></th><td>{if count($reviewers)}{foreach $reviewers as $r}{$r}, {/foreach}{else}{trans 'No reviewers at the moment.'}{/if}</td>
</tr>{if count($diff.files)}
<tr>
<th><strong>{trans 'Files:'}</strong></th>
<td>
{foreach $diff.files as $filename=>$diffdef}
{assign $ndiff = count($diffdef['chunks'])}
{assign $nc = $files[$filename][2]->count()}
<a href="{url 'IDF_Views_Source::tree', array($project.shortname, $patch.get_commit().scm_id, $filename)}">{$filename}</a> (<a href="#diff-{$filename|md5}">{blocktrans $ndiff}{$ndiff} diff{plural}{$ndiff} diffs{/blocktrans}</a>{if $nc}, <a href="#ct-{$filename|md5}">{blocktrans $nc}{$nc} comment{plural}{$nc} comments{/blocktrans}</a>{/if})<br />
{/foreach}
</td>
</tr>{/if}
<tr>{aurl 'url', 'IDF_Views_Review::getPatch', array($project.shortname, $patch.id)}
<th>&nbsp;</th><td><a href="{$url}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/package-grey.png'}" alt="{trans 'Archive'}" align="bottom" /></a> <a href="{$url}" class="soft">{trans 'Download the corresponding diff file'}</a></td>
</tr>
</table>
{if !$user.isAnonymous()}
<div class="issue-submit-info" style="width: 50%">
<p><strong>{trans 'How to Participate in a Code Review'}</strong></p>
<p>{blocktrans}Code review is a process in which
after or before changes are commited into the code repository,
different people discuss the code changes. The goal is
to <strong>improve the quality of the code and the
contributions</strong>, as such, you must be pragmatic when writing
your review. Correctly mention the line numbers (in the old or in the
new file) and try to keep a good balance between seriousness and fun.
{/blocktrans}</p>
<p>{blocktrans}
<strong>Proposing code for review is intimidating</strong>, you know
you will receive critics, so please, as a reviewer, <strong>keep this
process fun</strong>, use it to help your contributor learn your
coding standards and the structure of the code and <strong>make them want
to propose more contributions</strong>.
{/blocktrans}</p></div>
{/if}
<form method="post" action=".">
{foreach $files as $file=>$def}
<table class="diff" summary=" ">
<tbody>
<tr id="diff-{$file|md5}"><th colspan="4">{$file}</th></tr>
<tr><th colspan="2">{trans 'Old'}</th><th colspan="2">{trans 'New'}</th></tr>
{$def[0]}
</tbody>
</table>
{assign $comments = $def[2]}
{assign $nc = $comments.count()}
{assign $i = 1}
{foreach $comments as $c}
<div class="issue-comment{if $i == 1} issue-comment-first{/if}{if $i == $nc} issue-comment-last{/if}" id="ic{$c.id}">{assign $who = $c.get_submitter()}{aurl 'whourl', 'IDF_Views_User::view', array($who.login)}
{aurl 'url', 'IDF_Views_Review::view', array($project.shortname, $review.id)}
{assign $id = $c.id}
{assign $url = $url~'#ic'~$c.id}
<p{if $i == 1} id="ct-{$file|md5}"{/if}>{blocktrans}Comment <a href="{$url}">{$i}</a> by <a href="{$whourl}">{$who}</a>, {$c.creation_dtime|date}{/blocktrans}</p>
<pre class="issue-comment-text">{issuetext $c.content, $request}</pre>
</div> {assign $i = $i + 1}
{/foreach}
{if !$user.isAnonymous()}
<table class="form" summary=" ">
<tr>
<td>&nbsp;</td>
<td>
<p>{blocktrans}Your comments on the changes in file <em>{$file}</em>:{/blocktrans}<br />{$def[1]|safe}</p>
</td>
</tr></table>{/if}
{/foreach}
{if !$user.isAnonymous()}
<table class="form" summary=" ">
<tr>
<td>&nbsp;</td>
<td><input type="submit" value="{trans 'Submit Code Review'}" name="submit" /> | <a href="{url 'IDF_Views_Review::index', array($project.shortname)}">{trans 'Cancel'}</a>
</td>
</tr></table>
{/if}
</form>
{/block}
u
{block javascript}
<script type="text/javascript" src="{media '/idf/js/prettify.js'}"></script>
<script type="text/javascript">
prettyPrint();
</script>
{/block}

View File

@ -69,6 +69,14 @@ a.userw {
color: #777; color: #777;
} }
a.soft {
color: #777;
}
a.soft:visited {
color: #777;
}
div.context { div.context {
padding-left: 1em; padding-left: 1em;
} }
@ -518,6 +526,7 @@ table.diff tr.diff-next td {
padding: 1px 5px; padding: 1px 5px;
} }
/** /**
* view file content * view file content
*/ */
@ -633,3 +642,35 @@ div.deprecated-page {
.delp a { .delp a {
color: #a00; color: #a00;
} }
#branding {
float: right;
position: relative;
margin-right: -10px;
width: 115px;
font-size: 8px;
text-align: right;
padding-right: 20px;
padding-left: 0px;
background-color: #eeeeec;
-moz-border-radius: 3px 0 0 3px;
-webkit-border-radius: 3px 0 0 3px;
color: #888a85;
clear: both;
background-image: url("../img/ceondo.png");
background-repeat: no-repeat;
background-position: top right;
}
#branding a {
color: #777;
}
#branding a:visited {
color: #777;
}
#ft {
padding: 0px;
margin: 0px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B