From f690968b1149970a989bfa5e6114032b65d7f8f4 Mon Sep 17 00:00:00 2001 From: Loic d'Anterroches Date: Sun, 30 Nov 2008 10:26:05 +0100 Subject: [PATCH] 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. --- src/IDF/Diff.php | 158 ++++++ src/IDF/Form/ReviewCommentFile.php | 203 ++++++++ src/IDF/Form/ReviewCreate.php | 205 ++++++++ src/IDF/Form/ReviewFileComment.php | 94 ++++ src/IDF/Form/TabsConf.php | 1 + src/IDF/Middleware.php | 1 + src/IDF/Migrations/8CodeReview.php | 56 +++ src/IDF/Migrations/Install.php | 6 + src/IDF/Precondition.php | 11 +- src/IDF/Review.php | 168 +++++++ src/IDF/Review/FileComment.php | 113 +++++ src/IDF/Review/Patch.php | 118 +++++ src/IDF/Scm.php | 2 +- src/IDF/Tests/TestDiff.php | 17 + src/IDF/Tests/test-diff-view.html | 125 +++++ src/IDF/Tests/test-diff.diff | 515 ++++++++++++++++++++ src/IDF/Views/Project.php | 2 +- src/IDF/Views/Review.php | 227 +++++++++ src/IDF/conf/idf.php-dist | 5 +- src/IDF/conf/urls.php | 26 + src/IDF/relations.php | 4 + src/IDF/templates/idf/admin/tabs.html | 6 + src/IDF/templates/idf/base-full.html | 87 ++++ src/IDF/templates/idf/base-simple.html | 2 +- src/IDF/templates/idf/base.html | 1 + src/IDF/templates/idf/index.html | 1 + src/IDF/templates/idf/issues/create.html | 3 +- src/IDF/templates/idf/project/home.html | 2 +- src/IDF/templates/idf/review/base-full.html | 15 + src/IDF/templates/idf/review/base.html | 9 + src/IDF/templates/idf/review/create.html | 69 +++ src/IDF/templates/idf/review/index.html | 21 + src/IDF/templates/idf/review/view.html | 116 +++++ www/media/idf/css/style.css | 41 ++ www/media/idf/img/ceondo.png | Bin 0 -> 662 bytes 35 files changed, 2422 insertions(+), 8 deletions(-) create mode 100644 src/IDF/Form/ReviewCommentFile.php create mode 100644 src/IDF/Form/ReviewCreate.php create mode 100644 src/IDF/Form/ReviewFileComment.php create mode 100644 src/IDF/Migrations/8CodeReview.php create mode 100644 src/IDF/Review.php create mode 100644 src/IDF/Review/FileComment.php create mode 100644 src/IDF/Review/Patch.php create mode 100644 src/IDF/Tests/test-diff-view.html create mode 100644 src/IDF/Tests/test-diff.diff create mode 100644 src/IDF/Views/Review.php create mode 100644 src/IDF/templates/idf/base-full.html create mode 100644 src/IDF/templates/idf/review/base-full.html create mode 100644 src/IDF/templates/idf/review/base.html create mode 100644 src/IDF/templates/idf/review/create.html create mode 100644 src/IDF/templates/idf/review/index.html create mode 100644 src/IDF/templates/idf/review/view.html create mode 100644 www/media/idf/img/ceondo.png 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 $who = $c.get_submitter()}{aurl 'whourl', 'IDF_Views_User::view', array($who.login)} +{if $i == 0} +

{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} + +
{if strlen($c.content) > 0}{issuetext $c.content, $request}{else}{trans '(No comments were given for this change.)'}{/if}
+{assign $attachments = $c.get_attachment_list()} +{if $attachments.count() > 0} +
+{/if} +{if $i> 0 and $c.changedIssue()} +
+{foreach $c.changes as $w => $v} +{if $w == 'su'}{trans 'Summary:'}{/if}{if $w == 'st'}{trans 'Status:'}{/if}{if $w == 'ow'}{trans 'Owner:'}{/if}{if $w == 'lb'}{trans 'Labels:'}{/if} {if $w == 'lb'}{assign $l = implode(', ', $v)}{$l}{else}{$v}{/if}
+{/foreach} +
+{/if} +
{assign $i = $i + 1}{if $i == $nc and false == $form} +
+{aurl 'url', 'IDF_Views::login'}{blocktrans}Sign in to reply to this comment.{/blocktrans} +
+{/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 $isOwner or $isMember} + + + + + + + + + + + + + + + +{/if} + + + + +
{$form.f.content.labelTag}:{if $form.f.content.errors}{$form.f.content.fieldErrors}{/if} +{$form.f.content|unsafe} +
{$form.f.attachment.labelTag}:{if $form.f.attachment.errors}{$form.f.attachment.fieldErrors}{/if} +{$form.f.attachment|unsafe} +
{$form.f.summary.labelTag}:{if $form.f.summary.errors}{$form.f.summary.fieldErrors}{/if} +{$form.f.summary|unsafe} +
{$form.f.status.labelTag}:{if $form.f.status.errors}{$form.f.status.fieldErrors}{/if} +{$form.f.status|unsafe} +
{$form.f.owner.labelTag}:{if $form.f.owner.errors}{$form.f.owner.fieldErrors}{/if} +{$form.f.owner|unsafe} +
{$form.f.label1.labelTag}: +{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}
+{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} +
  | {trans 'Cancel'} +
+
+{/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 $project}

{$project}

{/if} +

+{if !$user.isAnonymous()}{aurl 'url', 'IDF_Views_User::myAccount'}{blocktrans}Welcome, {$user}.{/blocktrans} {trans 'Sign Out'}{else}{trans 'Sign in or create your account'}{/if} +{if $project} | {trans 'Project List'}{/if} +| {trans 'Help'} +

+ + +

{block titleicon}{/block}{block title}{$page_title}{/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}InDefero Logo{/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}
Powered by InDefero,
a Céondo Ltd initiative.
{/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}
Powered by InDefero,
a Céondo Ltd initiative.
{/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} +
+{trans 'Open Reviews'} {* + +{if !$user.isAnonymous()} | {trans 'New Issue'} | {trans 'My Issues'}{/if} | +
+ + +
+*} +{superblock} +
+{/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} +
+{trans 'Open Reviews'} +{if !$user.isAnonymous()} | {trans 'Start Code Review'} {/if} +{superblock} +
+{/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} + +
+ + + + + + + + + + + + + + + + +{if $isOwner or $isMember} + + + +{/if} + + + + +
{$form.f.summary.labelTag}:{if $form.f.summary.errors}{$form.f.summary.fieldErrors}{/if} +{$form.f.summary|unsafe} +
{$form.f.description.labelTag}:{if $form.f.description.errors}{$form.f.description.fieldErrors}{/if} +{$form.f.description|unsafe} +
{$form.f.commit.labelTag}:{if $form.f.commit.errors}{$form.f.commit.fieldErrors}{/if} +{$form.f.commit|unsafe}
+{trans 'Be sure to provide the right commit/revision reference for your patch to correctly apply.'} +
{$form.f.patch.labelTag}:{if $form.f.patch.errors}{$form.f.patch.fieldErrors}{/if} +{$form.f.patch|unsafe} +
{$form.f.status.labelTag}:{if $form.f.status.errors}{$form.f.status.fieldErrors}{/if} +{$form.f.status|unsafe} +
  | {trans 'Cancel'} +
+
+{/block} +{block context} +
+{blocktrans}

To start a code review, you need to provide:

+{/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 count($diff.files)} + + + +{/if} +{aurl 'url', 'IDF_Views_Review::getPatch', array($project.shortname, $patch.id)} + + +
{trans 'Created:'}{$patch.creation_dtime|date:"%Y-%m-%d %H:%M:%S"} ({$patch.creation_dtime|dateago})
{trans 'Updated:'}{$review.modif_dtime|dateago}
{trans 'Author:'}{$review.get_submitter()}
{trans 'Commit:'}{$patch.get_commit().scm_id}
{trans 'Description:'}{issuetext $review.summary, $request}

{issuetext $patch.summary, $request}
{trans 'Reviewers:'}{if count($reviewers)}{foreach $reviewers as $r}{$r}, {/foreach}{else}{trans 'No reviewers at the moment.'}{/if}
{trans 'Files:'} +{foreach $diff.files as $filename=>$diffdef} +{assign $ndiff = count($diffdef['chunks'])} +{assign $nc = $files[$filename][2]->count()} +{$filename} ({blocktrans $ndiff}{$ndiff} diff{plural}{$ndiff} diffs{/blocktrans}{if $nc}, {blocktrans $nc}{$nc} comment{plural}{$nc} comments{/blocktrans}{/if})
+{/foreach} +
 {trans 'Archive'} {trans 'Download the corresponding diff file'}
+ +{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} + + +
+{foreach $files as $file=>$def} + + + + +{$def[0]} + +
{$file}
{trans 'Old'}{trans 'New'}
+{assign $comments = $def[2]} +{assign $nc = $comments.count()} +{assign $i = 1} +{foreach $comments as $c} +
{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} +{blocktrans}Comment {$i} by {$who}, {$c.creation_dtime|date}{/blocktrans}

+ +
{issuetext $c.content, $request}
+
{assign $i = $i + 1} +{/foreach} +{if !$user.isAnonymous()} + + + + +
  +

{blocktrans}Your comments on the changes in file {$file}:{/blocktrans}
{$def[1]|safe}

+
{/if} +{/foreach} +{if !$user.isAnonymous()} + + + + +
  | {trans 'Cancel'} +
+{/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 0000000000000000000000000000000000000000..7499ed661a528e6f1c1f133a5a7605f211424bfd GIT binary patch literal 662 zcmV;H0%`q;P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L00#2_00#2`A+Ltz00007bV*G`2iOZ8 z6E-b1xmuV200I_CL_t(Y$E}uGPuoBgM$e45kl2X>qD2%4TUh%4|4EA!R4P&Rp~VSF zY=<}=8&4mAR){Pv@%JAjSa4Eu1S*V`r!V3fbG}-Se8YzgF1PI{i1OlXxn9HD(hY6--z0F1lnyu!-IIZ<^EBb>woM#*UM?#3jV68RN zWQsK>jQ`TM?_&n zmSxp;UcKJn{PdD^rkEwOYKM?%nw*`SW0^K_6r;5+4NcQ@`Jfjw4w>6#%a5p_H1tY2qkGAh|gEjO%*k zKP&)EuSK4{?oOo?;W%PW#l`ueD|pPq1x=i|*yzg-{wSr9=op wnx-VvgipKs_)UNHJT8~DOO~nUESHD=0ZeStm_MHkZU6uP07*qoM6N<$f(|7!G5`Po literal 0 HcmV?d00001