diff --git a/src/IDF/Diff.php b/src/IDF/Diff.php
index 8e91a68..baa90ff 100644
--- a/src/IDF/Diff.php
+++ b/src/IDF/Diff.php
@@ -169,4 +169,162 @@ class IDF_Diff
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 = ' ';
+ $line2 = ' ';
+ $line[2] = (strlen($line[2])) ? self::padLine(Pluf_esc($line[2])) : ' ';
+ 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('
%s %s
%s %s
'."\n", $line[0], $class, $pretty, $line1, $line[1], $class, $pretty, $line2);
+ }
+ if (count($chunks) > $cc)
+ $out .= '... ... '."\n";
+ $cc++;
+ $i++;
+ }
+ return Pluf_Template::markSafe($out);
+
+ }
}
\ No newline at end of file
diff --git a/src/IDF/Form/ReviewCommentFile.php b/src/IDF/Form/ReviewCommentFile.php
new file mode 100644
index 0000000..e7c90a1
--- /dev/null
+++ b/src/IDF/Form/ReviewCommentFile.php
@@ -0,0 +1,203 @@
+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;
+ }
+}
diff --git a/src/IDF/Form/ReviewCreate.php b/src/IDF/Form/ReviewCreate.php
new file mode 100644
index 0000000..57a8ca7
--- /dev/null
+++ b/src/IDF/Form/ReviewCreate.php
@@ -0,0 +1,205 @@
+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;
+ }
+}
diff --git a/src/IDF/Form/ReviewFileComment.php b/src/IDF/Form/ReviewFileComment.php
new file mode 100644
index 0000000..12d3604
--- /dev/null
+++ b/src/IDF/Form/ReviewFileComment.php
@@ -0,0 +1,94 @@
+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;
+ }
+}
diff --git a/src/IDF/Form/TabsConf.php b/src/IDF/Form/TabsConf.php
index 66dde89..e973b36 100644
--- a/src/IDF/Form/TabsConf.php
+++ b/src/IDF/Form/TabsConf.php
@@ -36,6 +36,7 @@ class IDF_Form_TabsConf extends Pluf_Form
$this->project = $extra['project'];
$ak = array('downloads_access_rights' => __('Downloads'),
+ 'review_access_rights' => __('Code Review'),
'wiki_access_rights' => __('Documentation'),
'source_access_rights' => __('Source'),
'issues_access_rights' => __('Issues'),);
diff --git a/src/IDF/Middleware.php b/src/IDF/Middleware.php
index 798a068..0cf5cd0 100644
--- a/src/IDF/Middleware.php
+++ b/src/IDF/Middleware.php
@@ -54,6 +54,7 @@ class IDF_Middleware
$request->conf->setProject($request->project);
$ak = array('downloads_access_rights' => 'hasDownloadsAccess',
'wiki_access_rights' => 'hasWikiAccess',
+ 'review_access_rights' => 'hasReviewAccess',
'source_access_rights' => 'hasSourceAccess',
'issues_access_rights' => 'hasIssuesAccess');
$request->rights = array();
diff --git a/src/IDF/Migrations/8CodeReview.php b/src/IDF/Migrations/8CodeReview.php
new file mode 100644
index 0000000..2b0bff9
--- /dev/null
+++ b/src/IDF/Migrations/8CodeReview.php
@@ -0,0 +1,56 @@
+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();
+ }
+}
\ No newline at end of file
diff --git a/src/IDF/Migrations/Install.php b/src/IDF/Migrations/Install.php
index f855c3f..ecd442f 100644
--- a/src/IDF/Migrations/Install.php
+++ b/src/IDF/Migrations/Install.php
@@ -42,6 +42,9 @@ function IDF_Migrations_Install_setup($params=null)
'IDF_Timeline',
'IDF_WikiPage',
'IDF_WikiRevision',
+ 'IDF_Review',
+ 'IDF_Review_Patch',
+ 'IDF_Review_FileComment',
);
$db = Pluf::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');
if ($perm) $perm->delete();
$models = array(
+ 'IDF_Review_FileComment',
+ 'IDF_Review_Patch',
+ 'IDF_Review',
'IDF_WikiRevision',
'IDF_WikiPage',
'IDF_Timeline',
diff --git a/src/IDF/Precondition.php b/src/IDF/Precondition.php
index 2738e27..018a1c2 100644
--- a/src/IDF/Precondition.php
+++ b/src/IDF/Precondition.php
@@ -152,7 +152,16 @@ class IDF_Precondition
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.
*
* API calls are not translated.
diff --git a/src/IDF/Review.php b/src/IDF/Review.php
new file mode 100644
index 0000000..5e04be8
--- /dev/null
+++ b/src/IDF/Review.php
@@ -0,0 +1,168 @@
+_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 '';
+ }
+}
\ No newline at end of file
diff --git a/src/IDF/Review/FileComment.php b/src/IDF/Review/FileComment.php
new file mode 100644
index 0000000..9f0f223
--- /dev/null
+++ b/src/IDF/Review/FileComment.php
@@ -0,0 +1,113 @@
+_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 '';
+ }
+}
diff --git a/src/IDF/Review/Patch.php b/src/IDF/Review/Patch.php
new file mode 100644
index 0000000..f25765c
--- /dev/null
+++ b/src/IDF/Review/Patch.php
@@ -0,0 +1,118 @@
+_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)
+ {
+ }
+}
diff --git a/src/IDF/Scm.php b/src/IDF/Scm.php
index 8c98f4a..d314bb8 100644
--- a/src/IDF/Scm.php
+++ b/src/IDF/Scm.php
@@ -55,7 +55,7 @@ class IDF_Scm
$cache = Pluf_Cache::factory();
if (null === ($res=$cache->get($key))) {
$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));
}
$cache->set($key, array($ll, $return, $output));
diff --git a/src/IDF/Tests/TestDiff.php b/src/IDF/Tests/TestDiff.php
index 06f07d3..a926d3d 100644
--- a/src/IDF/Tests/TestDiff.php
+++ b/src/IDF/Tests/TestDiff.php
@@ -54,4 +54,21 @@ class IDF_Tests_TestDiff extends UnitTestCase
$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));
+ }
}
\ No newline at end of file
diff --git a/src/IDF/Tests/test-diff-view.html b/src/IDF/Tests/test-diff-view.html
new file mode 100644
index 0000000..cb9e085
--- /dev/null
+++ b/src/IDF/Tests/test-diff-view.html
@@ -0,0 +1,125 @@
+{extends "idf/issues/base.html"}
+{block titleicon}{if $form} {/if}{/block}
+{block body}
+{assign $i = 0}
+{assign $nc = $comments.count()}
+{foreach $comments as $c}
+{assign $i = $i + 1}{if $i == $nc and false == $form}
+
+{/if}
+{/foreach}
+
+{if $form}
+
+
+{if $form.errors}
+
+
{trans 'The form contains some errors. Please correct them to change the issue.'}
+{if $form.get_top_errors}
+{$form.render_top_errors|unsafe}
+{/if}
+
+{/if}
+
+{if $closed and (!$isOwner and !$isMember)}
+ {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}
+
+{/if}
+
+{/if}
+{/block}
+{block context}
+
+{assign $submitter = $issue.get_submitter()}{aurl 'url', 'IDF_Views_User::view', array($submitter.login)}
+
{trans 'Created:'} {$issue.creation_dtime|dateago} {blocktrans}by {$submitter} {/blocktrans}
+{if $issue.modif_dtime != $issue.creation_dtime}
+{trans 'Updated:'} {$issue.modif_dtime|dateago}
{/if}
+
+{trans 'Status:'} {$issue.get_status.name}
+{if $issue.get_owner != null}
{aurl 'url', 'IDF_Views_User::view', array($issue.get_owner().login)}
+{trans 'Owner:'} {$issue.get_owner}
+
{/if}{assign $tags = $issue.get_tags_list()}{if $tags.count()}
+
+{trans 'Labels:'}
+{foreach $tags as $tag}{aurl 'url', 'IDF_Views_Issue::listLabel', array($project.shortname, $tag.id, 'open')}
+{$tag.class}: {$tag.name}
+{/foreach}
+
{/if}
+
+{/block}
+{block javascript}{if $form}{include 'idf/issues/js-autocomplete.html'}{/if}{/block}
diff --git a/src/IDF/Tests/test-diff.diff b/src/IDF/Tests/test-diff.diff
new file mode 100644
index 0000000..cd16d08
--- /dev/null
+++ b/src/IDF/Tests/test-diff.diff
@@ -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}
+
+
+-
+-{$form.f.attachment.labelTag}:
+-{if $form.f.attachment.errors}{$form.f.attachment.fieldErrors}{/if}
+-{$form.f.attachment|unsafe}
++
++{$form.f.attachment1.labelTag}:
++{if $form.f.attachment1.errors}{$form.f.attachment1.fieldErrors}{/if}
++{$form.f.attachment1|unsafe}
++
++
++
++{$form.f.attachment2.labelTag}:
++{if $form.f.attachment2.errors}{$form.f.attachment2.fieldErrors}{/if}
++{$form.f.attachment2|unsafe}
++
++
++
++{$form.f.attachment3.labelTag}:
++{if $form.f.attachment3.errors}{$form.f.attachment3.fieldErrors}{/if}
++{$form.f.attachment3|unsafe}
+
+ {if $isOwner or $isMember}
+
+@@ -74,7 +86,34 @@
+ {/block}
+ {block javascript}
+
++{/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}
+
+
+-
+-{$form.f.attachment.labelTag}:
+-{if $form.f.attachment.errors}{$form.f.attachment.fieldErrors}{/if}
+-{$form.f.attachment|unsafe}
++
++{$form.f.attachment1.labelTag}:
++{if $form.f.attachment1.errors}{$form.f.attachment1.fieldErrors}{/if}
++{$form.f.attachment1|unsafe}
++
++
++
++{$form.f.attachment2.labelTag}:
++{if $form.f.attachment2.errors}{$form.f.attachment2.fieldErrors}{/if}
++{$form.f.attachment2|unsafe}
++
++
++
++{$form.f.attachment3.labelTag}:
++{if $form.f.attachment3.errors}{$form.f.attachment3.fieldErrors}{/if}
++{$form.f.attachment3|unsafe}
+
+ {if $isOwner or $isMember}
+
+@@ -122,4 +134,33 @@
+ {/if}
+
+ {/block}
+-{block javascript}{if $form}{include 'idf/issues/js-autocomplete.html'}{/if}{/block}
++{block javascript}{if $form}{include 'idf/issues/js-autocomplete.html'}
++
++{/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
\ No newline at end of file
diff --git a/src/IDF/Views/Project.php b/src/IDF/Views/Project.php
index 5c64ed5..97048c6 100644
--- a/src/IDF/Views/Project.php
+++ b/src/IDF/Views/Project.php
@@ -347,7 +347,7 @@ class IDF_Views_Project
$params = array();
$keys = array('downloads_access_rights', 'source_access_rights',
'issues_access_rights', 'private_project',
- 'wiki_access_rights');
+ 'review_access_rights', 'wiki_access_rights');
foreach ($keys as $key) {
$_val = $request->conf->getVal($key, false);
if ($_val !== false) {
diff --git a/src/IDF/Views/Review.php b/src/IDF/Views/Review.php
new file mode 100644
index 0000000..b183df7
--- /dev/null
+++ b/src/IDF/Views/Review.php
@@ -0,0 +1,227 @@
+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 code review %d 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 %d : %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 code review %d 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 = ''.implode(', ', $tags).' ';
+ }
+ return sprintf('%s ', $edit, Pluf_esc($review->summary)).$out;
+}
+
diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist
index fc13603..65ef933 100644
--- a/src/IDF/conf/idf.php-dist
+++ b/src/IDF/conf/idf.php-dist
@@ -24,9 +24,12 @@
$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;
+# 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
# without trailing slash. The path is the path to the git database,
diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php
index 9a69c51..105b490 100644
--- a/src/IDF/conf/urls.php
+++ b/src/IDF/conf/urls.php
@@ -283,6 +283,32 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/downloads/(\d+)/delete/$#',
'model' => 'IDF_Views_Download',
'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 --------------------------------------
diff --git a/src/IDF/relations.php b/src/IDF/relations.php
index a007306..9f38880 100644
--- a/src/IDF/relations.php
+++ b/src/IDF/relations.php
@@ -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'),
'relate_to_many' => array('IDF_Tag', '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;
diff --git a/src/IDF/templates/idf/admin/tabs.html b/src/IDF/templates/idf/admin/tabs.html
index 40dfe30..94611a9 100644
--- a/src/IDF/templates/idf/admin/tabs.html
+++ b/src/IDF/templates/idf/admin/tabs.html
@@ -36,6 +36,12 @@
+{$form.f.review_access_rights.labelTag}:
+{if $form.f.review_access_rights.errors}{$form.f.review_access_rights.fieldErrors}{/if}
+{$form.f.review_access_rights|unsafe}
+
+
+
{if $form.f.private_project.errors}{$form.f.private_project.fieldErrors}{/if}
{$form.f.private_project|unsafe}
diff --git a/src/IDF/templates/idf/base-full.html b/src/IDF/templates/idf/base-full.html
new file mode 100644
index 0000000..a91d174
--- /dev/null
+++ b/src/IDF/templates/idf/base-full.html
@@ -0,0 +1,87 @@
+
+{*
+# ***** 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 *****
+*}
+
+
+
+
+
+ {block extraheader}{/block}
+ {block pagetitle}{$page_title|strip_tags}{/block}
+
+
+
+
+
+
+
+
+ {if $user and $user.id}{getmsgs $user}{/if}
+
{block body}{/block}
+
+
+
+
+
{block foot}{/block}
+
+
+{include 'idf/js-hotkeys.html'}
+{block javascript}{/block}
+{if $project}
+{/if}
+
+
diff --git a/src/IDF/templates/idf/base-simple.html b/src/IDF/templates/idf/base-simple.html
index 40d56be..113d331 100644
--- a/src/IDF/templates/idf/base-simple.html
+++ b/src/IDF/templates/idf/base-simple.html
@@ -51,7 +51,7 @@
{block context}{/block}
- {block foot}
{/block}
+ {block foot}{/block}
{include 'idf/js-hotkeys.html'}
diff --git a/src/IDF/templates/idf/base.html b/src/IDF/templates/idf/base.html
index f18d168..1c58ad3 100644
--- a/src/IDF/templates/idf/base.html
+++ b/src/IDF/templates/idf/base.html
@@ -47,6 +47,7 @@
{if $hasWikiAccess} {trans 'Documentation'} {/if}
{if $hasIssuesAccess} {trans 'Issues'} {/if}
{if $hasSourceAccess} {trans 'Source'} {/if}
+{if $hasReviewAccess} {trans 'Code Review'} {/if}
{if $isOwner}
{trans 'Administer'} {/if}{/if}
diff --git a/src/IDF/templates/idf/index.html b/src/IDF/templates/idf/index.html
index cd59ae5..b0d1ea3 100644
--- a/src/IDF/templates/idf/index.html
+++ b/src/IDF/templates/idf/index.html
@@ -13,3 +13,4 @@
{block context}
{trans 'Managed Projects:'} {$projects.count()}
{/block}
+{block foot}{/block}
diff --git a/src/IDF/templates/idf/issues/create.html b/src/IDF/templates/idf/issues/create.html
index faaa743..966b156 100644
--- a/src/IDF/templates/idf/issues/create.html
+++ b/src/IDF/templates/idf/issues/create.html
@@ -113,7 +113,6 @@ $(document).ready(function(){
}
});
-{/literal}{/block}
-
+{/literal}
{include 'idf/issues/js-autocomplete.html'}{/block}
diff --git a/src/IDF/templates/idf/project/home.html b/src/IDF/templates/idf/project/home.html
index 101b0bc..ba1fdb6 100644
--- a/src/IDF/templates/idf/project/home.html
+++ b/src/IDF/templates/idf/project/home.html
@@ -40,4 +40,4 @@
{/block}
-
+{block foot}{/block}
diff --git a/src/IDF/templates/idf/review/base-full.html b/src/IDF/templates/idf/review/base-full.html
new file mode 100644
index 0000000..4d2b360
--- /dev/null
+++ b/src/IDF/templates/idf/review/base-full.html
@@ -0,0 +1,15 @@
+{extends "idf/base-full.html"}
+{block tabreview} class="active"{/block}
+{block subtabs}
+
+{/block}
diff --git a/src/IDF/templates/idf/review/base.html b/src/IDF/templates/idf/review/base.html
new file mode 100644
index 0000000..74a2396
--- /dev/null
+++ b/src/IDF/templates/idf/review/base.html
@@ -0,0 +1,9 @@
+{extends "idf/base.html"}
+{block tabreview} class="active"{/block}
+{block subtabs}
+
+{/block}
diff --git a/src/IDF/templates/idf/review/create.html b/src/IDF/templates/idf/review/create.html
new file mode 100644
index 0000000..e54d80a
--- /dev/null
+++ b/src/IDF/templates/idf/review/create.html
@@ -0,0 +1,69 @@
+{extends "idf/review/base.html"}
+{block docclass}yui-t3{assign $inCreate = true}{/block}
+{block body}
+{if $form.errors}
+
+
{trans 'The form contains some errors. Please correct them to submit the code review.'}
+{if $form.get_top_errors}
+{$form.render_top_errors|unsafe}
+{/if}
+
+{/if}
+
+
+{/block}
+{block context}
+
+{blocktrans}
To start a code review, you need to provide:
+
+A commit or revision of the current code in the repository from which you started your work.
+A patch describing your changes with respect to the reference commit.
+Check your patch to not provide any password or confidential information!
+ {/blocktrans}
+
+{/block}
+{block javascript}
+{/block}
+
+
diff --git a/src/IDF/templates/idf/review/index.html b/src/IDF/templates/idf/review/index.html
new file mode 100644
index 0000000..18a7b8a
--- /dev/null
+++ b/src/IDF/templates/idf/review/index.html
@@ -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)}
+ {trans 'New Code Review'}
{/if}
+
+{/block}
+{block context}
+{*
+{aurl 'open_url', 'IDF_Views_Issue::index', array($project.shortname)}
+{aurl 'closed_url', 'IDF_Views_Issue::listStatus', array($project.shortname, 'closed')}
+{blocktrans}Open issues: {$open}
+Closed issues: {$closed}
{/blocktrans}
+{assign $class = ''}{assign $i = 0}
+{foreach $project.getTagCloud($cloud) as $label}
+{aurl 'url', 'IDF_Views_Issue::listLabel', array($project.shortname, $label.id, 'open')}
+{if $class != $label.class}{if $i != 0} {/if}{$label.class}: {/if}
+{$label.name} ,{assign $class = $label.class}{assign $i = $i + 1}{/foreach}
+*}{/block}
diff --git a/src/IDF/templates/idf/review/view.html b/src/IDF/templates/idf/review/view.html
new file mode 100644
index 0000000..1ac07cd
--- /dev/null
+++ b/src/IDF/templates/idf/review/view.html
@@ -0,0 +1,116 @@
+{extends "idf/review/base-full.html"}
+{block extraheader} {/block}
+{block docclass}yui-t1{assign $inCreate = true}{/block}
+{block body}
+{if $form.errors}
+
+
{trans 'The form contains some errors. Please correct them to submit your review.'}
+{if $form.get_top_errors}
+{$form.render_top_errors|unsafe}
+{/if}
+
+{/if}
+
+
+{if !$user.isAnonymous()}
+
+
{trans 'How to Participate in a Code Review'}
+
+
{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 improve the quality of the code and the
+contributions , 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}
+
{blocktrans}
+Proposing code for review is intimidating , you know
+you will receive critics, so please, as a reviewer, keep this
+process fun , use it to help your contributor learn your
+coding standards and the structure of the code and make them want
+to propose more contributions .
+{/blocktrans}
+{/if}
+
+
+
+{/block}
+u
+{block javascript}
+
+
+{/block}
diff --git a/www/media/idf/css/style.css b/www/media/idf/css/style.css
index b251637..90a2075 100644
--- a/www/media/idf/css/style.css
+++ b/www/media/idf/css/style.css
@@ -69,6 +69,14 @@ a.userw {
color: #777;
}
+a.soft {
+ color: #777;
+}
+
+a.soft:visited {
+ color: #777;
+}
+
div.context {
padding-left: 1em;
}
@@ -518,6 +526,7 @@ table.diff tr.diff-next td {
padding: 1px 5px;
}
+
/**
* view file content
*/
@@ -633,3 +642,35 @@ div.deprecated-page {
.delp a {
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;
+}
diff --git a/www/media/idf/img/ceondo.png b/www/media/idf/img/ceondo.png
new file mode 100644
index 0000000..7499ed6
Binary files /dev/null and b/www/media/idf/img/ceondo.png differ
{blocktrans}Reported by {$who}, {$c.creation_dtime|date}{/blocktrans}
+{else} +{aurl 'url', 'IDF_Views_Issue::view', array($project.shortname, $issue.id)} +{assign $id = $c.id} +{assign $url = $url~'#ic'~$c.id} +{blocktrans}Comment {$i} by {$who}, {$c.creation_dtime|date}{/blocktrans}
+{/if} + + +{assign $attachments = $c.get_attachment_list()} +{if $attachments.count() > 0} ++
+{foreach $attachments as $a}- {$a.filename} - {$a.filesize|size}
{/foreach}
+
{/if} +{if $i> 0 and $c.changedIssue()} ++{/foreach} +