From 9171bfd1abea5e00aea6a440c88919b3c31012a0 Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Mon, 9 May 2011 13:52:09 +0200 Subject: [PATCH 1/8] Start with issue 638 (issue links); nothing workable yet. --- src/IDF/Form/Admin/ProjectCreate.php | 1 + src/IDF/Form/IssueCreate.php | 25 +++- src/IDF/Form/IssueTrackingConf.php | 14 ++- src/IDF/Project.php | 16 +++ src/IDF/Views/Issue.php | 70 +++++++++--- src/IDF/Views/Project.php | 21 ++-- src/IDF/conf/urls.php | 7 +- .../templates/idf/admin/issue-tracking.html | 9 +- src/IDF/templates/idf/issues/create.html | 23 ++-- .../templates/idf/issues/js-autocomplete.html | 108 +++++++++++------- 10 files changed, 213 insertions(+), 81 deletions(-) diff --git a/src/IDF/Form/Admin/ProjectCreate.php b/src/IDF/Form/Admin/ProjectCreate.php index f111654..c39a028 100644 --- a/src/IDF/Form/Admin/ProjectCreate.php +++ b/src/IDF/Form/Admin/ProjectCreate.php @@ -319,6 +319,7 @@ class IDF_Form_Admin_ProjectCreate extends Pluf_Form 'labels_issue_closed' => IDF_Form_IssueTrackingConf::init_closed, 'labels_issue_predefined' => IDF_Form_IssueTrackingConf::init_predefined, 'labels_issue_one_max' => IDF_Form_IssueTrackingConf::init_one_max, + 'issue_relations' => IDF_Form_IssueTrackingConf::init_relations, 'webhook_url' => '', 'downloads_access_rights' => 'all', 'review_access_rights' => 'all', diff --git a/src/IDF/Form/IssueCreate.php b/src/IDF/Form/IssueCreate.php index b52987c..c6f3aef 100644 --- a/src/IDF/Form/IssueCreate.php +++ b/src/IDF/Form/IssueCreate.php @@ -76,11 +76,11 @@ class IDF_Form_IssueCreate extends Pluf_Form // case of someone allowing the upload path to be accessible // to everybody. for ($i=1;$i<4;$i++) { - $filename = substr($md5, 0, 2).'/'.substr($md5, 2, 2).'/'.substr($md5, 4).'/%s.dummy'; + $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' => + 'move_function_params' => array('upload_path' => $upload_path, 'upload_path_create' => true, 'file_name' => $filename, @@ -109,6 +109,21 @@ class IDF_Form_IssueCreate extends Pluf_Form ), )); + $relation_types = $extra['project']->getRelationsFromConfig(); + $this->fields['relation_type'] = new Pluf_Form_Field_Varchar( + array('required' => false, + 'label' => __('This issue'), + 'initial' => $relation_types[0], + 'widget_attrs' => array('size' => 15), + )); + + $this->fields['relation_issue'] = new Pluf_Form_Field_Varchar( + array('required' => false, + 'label' => null, + 'initial' => '', + 'widget_attrs' => array('size' => 10), + )); + /* * get predefined tags for issues from current project * @@ -181,7 +196,7 @@ class IDF_Form_IssueCreate extends Pluf_Form $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)), + list($class, $name) = array(mb_strtolower(trim($class)), trim($name)); } else { $class = 'other'; @@ -215,10 +230,10 @@ class IDF_Form_IssueCreate extends Pluf_Form function clean_status() { // Check that the status is in the list of official status - $tags = $this->project->getTagsFromConfig('labels_issue_open', + $tags = $this->project->getTagsFromConfig('labels_issue_open', IDF_Form_IssueTrackingConf::init_open, 'Status'); - $tags = array_merge($this->project->getTagsFromConfig('labels_issue_closed', + $tags = array_merge($this->project->getTagsFromConfig('labels_issue_closed', IDF_Form_IssueTrackingConf::init_closed, 'Status') , $tags); diff --git a/src/IDF/Form/IssueTrackingConf.php b/src/IDF/Form/IssueTrackingConf.php index 34dce61..c62a10d 100644 --- a/src/IDF/Form/IssueTrackingConf.php +++ b/src/IDF/Form/IssueTrackingConf.php @@ -72,6 +72,9 @@ Performance = Performance issue Usability = Affects program usability Maintainability = Hinders future changes'; const init_one_max = 'Type, Priority, Milestone'; + const init_relations = 'is related to +blocks, is blocked by +duplicates, is duplicated by'; public function initFields($extra=array()) { @@ -114,10 +117,19 @@ Maintainability = Hinders future changes'; $this->fields['labels_issue_one_max'] = new Pluf_Form_Field_Varchar( array('required' => false, 'label' => __('Each issue may have at most one label with each of these classes'), - 'initial' => self::init_one_max, + 'initial' => self::init_one_max, 'widget_attrs' => array('size' => 60), )); + $this->fields['issue_relations'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('Issue relations'), + 'initial' => self::init_relations, + 'help_text' => __('You can define bidirectional relations like "is related to" or "blocks, is blocked by".'), + 'widget_attrs' => array('rows' => 7, + 'cols' => 75), + 'widget' => 'Pluf_Form_Widget_TextareaInput', + )); } } diff --git a/src/IDF/Project.php b/src/IDF/Project.php index 3a542c6..211a9ed 100644 --- a/src/IDF/Project.php +++ b/src/IDF/Project.php @@ -233,6 +233,22 @@ class IDF_Project extends Pluf_Model return $tags; } + /** + * Returns a list of relations which are available in this project + * + * @return array List of relation names + */ + public function getRelationsFromConfig() + { + $conf = $this->getConf(); + $rel = $conf->getVal('issue_relations', IDF_Form_IssueTrackingConf::init_relations); + $relations = array(); + foreach (preg_split("/\015\012|\015|\012/", $rel, -1, PREG_SPLIT_NO_EMPTY) as $s) { + $relations = array_merge($relations, preg_split("/\s*,\s*/", $s, 2)); + } + return $relations; + } + /** * Return membership data. * diff --git a/src/IDF/Views/Issue.php b/src/IDF/Views/Issue.php index e2b121c..7224347 100644 --- a/src/IDF/Views/Issue.php +++ b/src/IDF/Views/Issue.php @@ -90,37 +90,37 @@ class IDF_Views_Issue $ctags = $prj->getTagIdsByStatus('closed'); if (count($otags) == 0) $otags[] = 0; if (count($ctags) == 0) $ctags[] = 0; - + // Get the id list of issue in the user watch list (for all projects !) $db =& Pluf::db(); $sql_results = $db->select('SELECT idf_issue_id as id FROM '.Pluf::f('db_table_prefix', '').'idf_issue_pluf_user_assoc WHERE pluf_user_id='.$request->user->id); $issue_ids = array(0); foreach ($sql_results as $id) { $issue_ids[] = $id['id']; - } + } $issue_ids = implode (',', $issue_ids); - + // Count open and close issues $sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array($prj->id)); $nb_open = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen())); $sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array($prj->id)); $nb_closed = Pluf::factory('IDF_Issue')->getCount(array('filter'=>$sql->gen())); - + // Generate a filter for the paginator switch ($match[2]) { case 'closed': $title = sprintf(__('Watch List: Closed Issues for %s'), (string) $prj); $summary = __('This table shows the closed issues in your watch list for %s project.', (string) $prj); - $f_sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array($prj->id)); - break; + $f_sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array($prj->id)); + break; case 'open': default: $title = sprintf(__('Watch List: Open Issues for %s'), (string) $prj); $summary = __('This table shows the open issues in your watch list for %s project.', (string) $prj); $f_sql = new Pluf_SQL('project=%s AND id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array($prj->id)); - break; + break; } - + // Paginator to paginate the issues $pag = new Pluf_Paginator(new IDF_Issue()); $pag->class = 'recent-issues'; @@ -170,17 +170,17 @@ class IDF_Views_Issue } foreach (IDF_Views::getProjects($request->user) as $project) { $ctags = array_merge($ctags, $project->getTagIdsByStatus('closed')); - } + } if (count($otags) == 0) $otags[] = 0; if (count($ctags) == 0) $ctags[] = 0; - + // Get the id list of issue in the user watch list (for all projects !) $db =& Pluf::db(); $sql_results = $db->select('SELECT idf_issue_id as id FROM '.Pluf::f('db_table_prefix', '').'idf_issue_pluf_user_assoc WHERE pluf_user_id='.$request->user->id); $issue_ids = array(0); foreach ($sql_results as $id) { $issue_ids[] = $id['id']; - } + } $issue_ids = implode (',', $issue_ids); // Count open and close issues @@ -194,16 +194,16 @@ class IDF_Views_Issue case 'closed': $title = sprintf(__('Watch List: Closed Issues')); $summary = __('This table shows the closed issues in your watch list.'); - $f_sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array()); - break; + $f_sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $ctags).')', array()); + break; case 'open': default: $title = sprintf(__('Watch List: Open Issues')); $summary = __('This table shows the open issues in your watch list.'); $f_sql = new Pluf_SQL('id IN ('.$issue_ids.') AND status IN ('.implode(', ', $otags).')', array()); - break; + break; } - + // Paginator to paginate the issues $pag = new Pluf_Paginator(new IDF_Issue()); $pag->class = 'recent-issues'; @@ -453,7 +453,7 @@ class IDF_Views_Issue $next_issue = Pluf::factory('IDF_Issue')->getList(array('filter' => $sql_next->gen(), 'order' => 'id ASC', 'nb' => 1 - )); + )); $previous_issue_id = (isset($previous_issue[0])) ? $previous_issue[0]->id : 0; $next_issue_id = (isset($next_issue[0])) ? $next_issue[0]->id : 0; @@ -643,6 +643,35 @@ class IDF_Views_Issue $request); } + /** + * Renders a JSON string containing completed issue information + * based on the queried / partial string + */ + public $autoCompleteIssueList_precond = array('IDF_Precondition::accessIssues'); + public function autoCompleteIssueList($request, $match) + { + $prj = $request->project; + + // Autocomplete from jQuery UI works with JSON, this old one still + // expects a parsable string; since we'd need to bump jQuery beyond + // 1.2.6 for this to use as well, we're trying to cope with the old format. + // see http://www.learningjquery.com/2010/06/autocomplete-migration-guide + + $arr = array( + 'Fo|o' => 110, + 'Bar' => 111, + 'Baz' => 112, + ); + + $out = ''; + foreach ($arr as $key => $val) + { + $out .= str_replace('|', '|', $key).'|'.$val."\n"; + } + + return new Pluf_HTTP_Response($out); + } + /** * Star/Unstar an issue. */ @@ -715,6 +744,15 @@ class IDF_Views_Issue } $auto['auto_owner'] = substr($auto['auto_owner'], 0, -2); unset($auto['_auto_owner']); + // Get issue relations + $r = $project->getRelationsFromConfig(); + $auto['auto_relation_types'] = ''; + foreach ($r as $rt) { + $esc = Pluf_esc($rt); + $auto['auto_relation_types'] .= sprintf('{ name: "%s", to: "%s" }, ', + $esc, $esc); + } + $auto['auto_relation_types'] = substr($auto['auto_relation_types'], 0, -2); return $auto; } } diff --git a/src/IDF/Views/Project.php b/src/IDF/Views/Project.php index 70e79a4..11c08f4 100644 --- a/src/IDF/Views/Project.php +++ b/src/IDF/Views/Project.php @@ -38,18 +38,18 @@ class IDF_Views_Project public function logo($request, $match) { $prj = $request->project; - + $logo = $prj->getConf()->getVal('logo'); if (empty($logo)) { $url = Pluf::f('url_media') . '/idf/img/no_logo.png'; return new Pluf_HTTP_Response_Redirect($url); } - + $info = IDF_FileUtil::getMimeType($logo); return new Pluf_HTTP_Response_File(Pluf::f('upload_path') . '/' . $prj->shortname . $logo, $info[0]); } - + /** * Home page of a project. */ @@ -291,12 +291,12 @@ class IDF_Views_Project public function admin($request, $match) { $prj = $request->project; - $title = sprintf(__('%s Project Summary'), (string) $prj); - $extra = array('project' => $prj); + $title = sprintf(__('%s Project Summary'), (string) $prj); + $extra = array('project' => $prj); if ($request->method == 'POST') { $form = new IDF_Form_ProjectConf(array_merge($request->POST, $request->FILES), - $extra); + $extra); if ($form->isValid()) { $form->save(); $request->user->setMessage(__('The project has been updated.')); @@ -305,9 +305,9 @@ class IDF_Views_Project return new Pluf_HTTP_Response_Redirect($url); } } else { - $form = new IDF_Form_ProjectConf($prj->getData(), $extra); + $form = new IDF_Form_ProjectConf($prj->getData(), $extra); } - + $logo = $prj->getConf()->getVal('logo'); return Pluf_Shortcuts_RenderToResponse('idf/admin/summary.html', array( @@ -316,7 +316,7 @@ class IDF_Views_Project 'project' => $prj, 'logo' => $logo, ), - $request); + $request); } /** @@ -344,7 +344,8 @@ class IDF_Views_Project $params = array(); $keys = array('labels_issue_template', 'labels_issue_open', 'labels_issue_closed', - 'labels_issue_predefined', 'labels_issue_one_max'); + 'labels_issue_predefined', 'labels_issue_one_max', + 'issue_relations'); foreach ($keys as $key) { $_val = $conf->getVal($key, false); if ($_val !== false) { diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index 365536a..b218e95 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -73,7 +73,7 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/$#', 'base' => $base, 'model' => 'IDF_Views_Project', 'method' => 'home'); - + $ctl[] = array('regex' => '#^/p/([\-\w]+)/logo/$#', 'base' => $base, 'model' => 'IDF_Views_Project', @@ -173,6 +173,11 @@ $ctl[] = array('regex' => '#^/watchlist/(\w+)$#', 'model' => 'IDF_Views_Issue', 'method' => 'forgeWatchList'); +$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/autocomplete/$#', + 'base' => $base, + 'model' => 'IDF_Views_Issue', + 'method' => 'autoCompleteIssueList'); + // ---------- SCM ---------------------------------------- $ctl[] = array('regex' => '#^/p/([\-\w]+)/source/help/$#', diff --git a/src/IDF/templates/idf/admin/issue-tracking.html b/src/IDF/templates/idf/admin/issue-tracking.html index 057b3c7..0059130 100644 --- a/src/IDF/templates/idf/admin/issue-tracking.html +++ b/src/IDF/templates/idf/admin/issue-tracking.html @@ -35,8 +35,15 @@ +{$form.f.issue_relations.labelTag}:
+{if $form.f.issue_relations.errors}{$form.f.issue_relations.fieldErrors}{/if} +{$form.f.issue_relations|unsafe}
+{$form.f.issue_relations.help_text} + + + - + diff --git a/src/IDF/templates/idf/issues/create.html b/src/IDF/templates/idf/issues/create.html index c2dba27..e204a54 100644 --- a/src/IDF/templates/idf/issues/create.html +++ b/src/IDF/templates/idf/issues/create.html @@ -63,6 +63,15 @@ +{$form.f.relation_type.labelTag}: + +{if $form.f.relation_type.errors}{$form.f.relation_type.fieldErrors}{/if} +{if $form.f.relation_issue.errors}{$form.f.relation_issue.fieldErrors}{/if} +{$form.f.relation_type|unsafe} +{$form.f.relation_issue|unsafe} + + + {$form.f.label1.labelTag}: {if $form.f.label1.errors}{$form.f.label1.fieldErrors}{/if}{$form.f.label1|unsafe} @@ -76,8 +85,8 @@   - - | + + | {trans 'Cancel'} @@ -123,11 +132,11 @@ $(document).ready(function(){ }); var j=0; for (j=1;j<4;j=j+1) { - if($("tr#form-attachment-"+j+" > td > ul.errorlist").length == 0){ - $("#form-attachment-"+j).hide(); - }else{ - $("#form-block-"+(j-1)).remove(); - } + if($("tr#form-attachment-"+j+" > td > ul.errorlist").length == 0){ + $("#form-attachment-"+j).hide(); + }else{ + $("#form-block-"+(j-1)).remove(); + } } }); diff --git a/src/IDF/templates/idf/issues/js-autocomplete.html b/src/IDF/templates/idf/issues/js-autocomplete.html index 589d178..24b27f8 100644 --- a/src/IDF/templates/idf/issues/js-autocomplete.html +++ b/src/IDF/templates/idf/issues/js-autocomplete.html @@ -2,54 +2,82 @@ From 7e226b43d33351622f06bc52ea18c9918b1c270f Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Tue, 10 May 2011 16:21:29 +0200 Subject: [PATCH 2/8] More work on the issue relation infrastructure - actually query data for the incoming query - exclude the current issue from being linked with itself - allow multiple issues to be given in the second input field - add the form fields to the ticket update view as well --- src/IDF/Form/IssueUpdate.php | 20 ++++++- src/IDF/Views/Issue.php | 57 ++++++++++++++++--- src/IDF/conf/urls.php | 2 +- .../templates/idf/issues/js-autocomplete.html | 3 +- src/IDF/templates/idf/issues/view.html | 9 +++ 5 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/IDF/Form/IssueUpdate.php b/src/IDF/Form/IssueUpdate.php index b453b9a..9835bc3 100644 --- a/src/IDF/Form/IssueUpdate.php +++ b/src/IDF/Form/IssueUpdate.php @@ -69,11 +69,11 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate // case of someone allowing the upload path to be accessible // to everybody. for ($i=1;$i<4;$i++) { - $filename = substr($md5, 0, 2).'/'.substr($md5, 2, 2).'/'.substr($md5, 4).'/%s.dummy'; + $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' => + 'move_function_params' => array('upload_path' => $upload_path, 'upload_path_create' => true, 'file_name' => $filename, @@ -102,6 +102,22 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate 'size' => 15, ), )); + + $relation_types = $extra['project']->getRelationsFromConfig(); + $this->fields['relation_type'] = new Pluf_Form_Field_Varchar( + array('required' => false, + 'label' => __('This issue'), + 'initial' => $relation_types[0], + 'widget_attrs' => array('size' => 15), + )); + + $this->fields['relation_issue'] = new Pluf_Form_Field_Varchar( + array('required' => false, + 'label' => null, + 'initial' => '', + 'widget_attrs' => array('size' => 10), + )); + $tags = $this->issue->get_tags_list(); for ($i=1;$i<7;$i++) { $initial = ''; diff --git a/src/IDF/Views/Issue.php b/src/IDF/Views/Issue.php index 7224347..21800ff 100644 --- a/src/IDF/Views/Issue.php +++ b/src/IDF/Views/Issue.php @@ -345,6 +345,7 @@ class IDF_Views_Issue 'form' => $form, 'page_title' => $title, 'preview' => $preview, + 'issue' => new IDF_Issue(), ), self::autoCompleteArrays($prj) ); @@ -651,22 +652,60 @@ class IDF_Views_Issue public function autoCompleteIssueList($request, $match) { $prj = $request->project; + $issue_id = !empty($match[2]) ? intval($match[2]) : 0; + $query = trim($request->REQUEST['q']); + $limit = !empty($request->REQUEST['limit']) ? intval($request->REQUEST['limit']) : 0; + $limit = max(10, $limit); + + $issues = array(); + + // empty search, return the most recently updated issues + if (empty($query)) { + $sql = new Pluf_SQL('project=%s', array($prj->id)); + $tmp = Pluf::factory('IDF_Issue')->getList(array( + 'filter' => $sql->gen(), + 'order' => 'modif_dtime DESC' + )); + $issues += $tmp->getArrayCopy(); + } + else { + // ID-based search + if (is_numeric($query)) { + $sql = new Pluf_SQL('project=%s AND id LIKE %s', array($prj->id, $query.'%')); + $tmp = Pluf::factory('IDF_Issue')->getList(array( + 'filter' => $sql->gen(), + 'order' => 'id ASC' + )); + $issues += $tmp->getArrayCopy(); + } + + // text-based search + $res = new Pluf_Search_ResultSet( + IDF_Search::mySearch($query, $prj, 'IDF_Issue') + ); + foreach ($res as $issue) + $issues[] = $issue; + } // Autocomplete from jQuery UI works with JSON, this old one still // expects a parsable string; since we'd need to bump jQuery beyond // 1.2.6 for this to use as well, we're trying to cope with the old format. // see http://www.learningjquery.com/2010/06/autocomplete-migration-guide - - $arr = array( - 'Fo|o' => 110, - 'Bar' => 111, - 'Baz' => 112, - ); - $out = ''; - foreach ($arr as $key => $val) + $ids = array(); + foreach ($issues as $issue) { - $out .= str_replace('|', '|', $key).'|'.$val."\n"; + if ($issue->id == $issue_id) + continue; + + if (in_array($issue->id, $ids)) + continue; + + if (--$limit < 0) + break; + + $out .= str_replace('|', '|', $issue->summary) .'|'.$issue->id."\n"; + $ids[] = $issue->id; } return new Pluf_HTTP_Response($out); diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index b218e95..642ace7 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -173,7 +173,7 @@ $ctl[] = array('regex' => '#^/watchlist/(\w+)$#', 'model' => 'IDF_Views_Issue', 'method' => 'forgeWatchList'); -$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/autocomplete/$#', +$ctl[] = array('regex' => '#^/p/([\-\w]+)/issues/autocomplete/(\d*)$#', 'base' => $base, 'model' => 'IDF_Views_Issue', 'method' => 'autoCompleteIssueList'); diff --git a/src/IDF/templates/idf/issues/js-autocomplete.html b/src/IDF/templates/idf/issues/js-autocomplete.html index 24b27f8..8506783 100644 --- a/src/IDF/templates/idf/issues/js-autocomplete.html +++ b/src/IDF/templates/idf/issues/js-autocomplete.html @@ -64,11 +64,12 @@ return row.to; } }); - $("#id_relation_issue").autocomplete("{/literal}{url 'IDF_Views_Issue::autoCompleteIssueList', array($project.shortname)}{literal}", { + $("#id_relation_issue").autocomplete("{/literal}{url 'IDF_Views_Issue::autoCompleteIssueList', array($project.shortname, $issue.id)}{literal}", { minChars: 0, width: 310, matchContains: true, max: 10, + multiple: true, delay: 500, highlightItem: false, formatItem: function(row, i, max, term) { diff --git a/src/IDF/templates/idf/issues/view.html b/src/IDF/templates/idf/issues/view.html index 050460d..e27be87 100644 --- a/src/IDF/templates/idf/issues/view.html +++ b/src/IDF/templates/idf/issues/view.html @@ -120,6 +120,15 @@ +{$form.f.relation_type.labelTag}: + +{if $form.f.relation_type.errors}{$form.f.relation_type.fieldErrors}{/if} +{if $form.f.relation_issue.errors}{$form.f.relation_issue.fieldErrors}{/if} +{$form.f.relation_type|unsafe} +{$form.f.relation_issue|unsafe} + + + {$form.f.label1.labelTag}: {if $form.f.label1.errors}{$form.f.label1.fieldErrors}{/if}{$form.f.label1|unsafe} From e40d922eef67e5c1743b5930afccf8e557782cb0 Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Tue, 10 May 2011 17:21:56 +0200 Subject: [PATCH 3/8] Add two targets to easily install and update the IDF database --- Makefile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Makefile b/Makefile index 942b144..8ec8daa 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,10 @@ help: @printf "\tpo-push - Send the all PO files to the transifex server.\n" @printf "\tpo-pull - Get all PO files from the transifex server.\n" @printf "\tpo-stats - Show translation statistics of all PO files.\n" + @printf "\nMisc Rules :\n"; + @printf "\tdb-install - Install the database schema.\n" + @printf "\tdb-update - Update the database schema.\n" + # # Internationalization rule, POT & PO file manipulation @@ -139,3 +143,8 @@ po-stats: > indefero-$(@:-zipfile=)-`git log $(@:-zipfile=) -n 1 \ --pretty=format:%h`.zip +db-install: + @cd src && php $(PLUF_PATH)/migrate.php --conf=IDF/conf/idf.php -a -d -i + +db-update: + @cd src && php $(PLUF_PATH)/migrate.php --conf=IDF/conf/idf.php -a -d From bcba64b2a10b29e06472f2dc6bb021e9eacb4645 Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Tue, 10 May 2011 17:22:32 +0200 Subject: [PATCH 4/8] Basic model files added. --- src/IDF/IssueRelation.php | 93 ++++++++++++++++++++++ src/IDF/Migrations/17AddIssueRelations.php | 44 ++++++++++ src/IDF/Migrations/Backup.php | 2 + src/IDF/Migrations/Install.php | 2 + src/IDF/relations.php | 1 + 5 files changed, 142 insertions(+) create mode 100644 src/IDF/IssueRelation.php create mode 100644 src/IDF/Migrations/17AddIssueRelations.php diff --git a/src/IDF/IssueRelation.php b/src/IDF/IssueRelation.php new file mode 100644 index 0000000..f918631 --- /dev/null +++ b/src/IDF/IssueRelation.php @@ -0,0 +1,93 @@ +_a['table'] = 'idf_issuerelations'; + $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, + ), + 'issue' => + array( + 'type' => 'Pluf_DB_Field_Foreignkey', + 'model' => 'IDF_Issue', + 'blank' => false, + 'verbose' => __('issue'), + 'relate_name' => 'issues', + ), + 'verb' => + array( + 'type' => 'Pluf_DB_Field_Text', + 'blank' => false, + 'verbose' => __('verb'), + ), + 'other_issue' => + array( + 'type' => 'Pluf_DB_Field_Foreignkey', + 'model' => 'IDF_Issue', + 'blank' => false, + 'verbose' => __('other issue'), + 'relate_name' => 'other_issues', + ), + 'submitter' => + array( + 'type' => 'Pluf_DB_Field_Foreignkey', + 'model' => 'Pluf_User', + 'blank' => false, + 'verbose' => __('submitter'), + ), + '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 preSave($create=false) + { + if ($this->id == '') { + $this->creation_dtime = gmdate('Y-m-d H:i:s'); + } + } +} diff --git a/src/IDF/Migrations/17AddIssueRelations.php b/src/IDF/Migrations/17AddIssueRelations.php new file mode 100644 index 0000000..0bf7700 --- /dev/null +++ b/src/IDF/Migrations/17AddIssueRelations.php @@ -0,0 +1,44 @@ +model = new IDF_IssueRelation(); + $schema->createTables(); +} + +function IDF_Migrations_17AddIssueRelations_down($params=null) +{ + $db = Pluf::db(); + $schema = new Pluf_DB_Schema($db); + $schema->model = new IDF_IssueRelation(); + $schema->dropTables(); +} + diff --git a/src/IDF/Migrations/Backup.php b/src/IDF/Migrations/Backup.php index 406b086..32fcb9e 100644 --- a/src/IDF/Migrations/Backup.php +++ b/src/IDF/Migrations/Backup.php @@ -54,6 +54,7 @@ function IDF_Migrations_Backup_run($folder, $name=null) 'IDF_Queue', 'IDF_Gconf', 'IDF_EmailAddress', + 'IDF_IssueRelation', ); $db = Pluf::db(); // Now, for each table, we dump the content in json, this is a @@ -100,6 +101,7 @@ function IDF_Migrations_Backup_restore($folder, $name) 'IDF_Queue', 'IDF_Gconf', 'IDF_EmailAddress', + 'IDF_IssueRelation', ); $db = Pluf::db(); $schema = new Pluf_DB_Schema($db); diff --git a/src/IDF/Migrations/Install.php b/src/IDF/Migrations/Install.php index 98c62fc..aea5f00 100644 --- a/src/IDF/Migrations/Install.php +++ b/src/IDF/Migrations/Install.php @@ -51,6 +51,7 @@ function IDF_Migrations_Install_setup($params=null) 'IDF_Queue', 'IDF_Gconf', 'IDF_EmailAddress', + 'IDF_IssueRelation', ); $db = Pluf::db(); $schema = new Pluf_DB_Schema($db); @@ -109,6 +110,7 @@ function IDF_Migrations_Install_teardown($params=null) 'IDF_Commit', 'IDF_Project', 'IDF_EmailAddress', + 'IDF_IssueRelation', ); $db = Pluf::db(); $schema = new Pluf_DB_Schema($db); diff --git a/src/IDF/relations.php b/src/IDF/relations.php index dc951e7..38f0ea1 100644 --- a/src/IDF/relations.php +++ b/src/IDF/relations.php @@ -45,6 +45,7 @@ $m['IDF_Scm_Cache_Git'] = array('relate_to' => array('IDF_Project')); $m['IDF_UserData'] = array('relate_to' => array('Pluf_User')); $m['IDF_EmailAddress'] = array('relate_to' => array('Pluf_User')); +$m['IDF_IssueRelation'] = array('relate_to' => array('IDF_Issue', 'Pluf_User')); Pluf_Signal::connect('Pluf_Template_Compiler::construct_template_tags_modifiers', array('IDF_Middleware', 'updateTemplateTagsModifiers')); From 16dda0743c805fa01bc2d0a313c89436f5b3080c Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Sat, 28 May 2011 23:48:00 +0200 Subject: [PATCH 5/8] Basic storage of relations for new issues has been done; the relations are also properly displayed at the left side in the issue's detail view. --- src/IDF/Form/IssueCreate.php | 71 +++++++++++++++++++++++++- src/IDF/Form/IssueTrackingConf.php | 14 +++++ src/IDF/Form/IssueUpdate.php | 4 +- src/IDF/Issue.php | 18 +++++++ src/IDF/IssueRelation.php | 11 +++- src/IDF/Project.php | 11 +++- src/IDF/Views/Issue.php | 5 +- src/IDF/templates/idf/issues/view.html | 13 +++++ 8 files changed, 138 insertions(+), 9 deletions(-) diff --git a/src/IDF/Form/IssueCreate.php b/src/IDF/Form/IssueCreate.php index c6f3aef..859adb1 100644 --- a/src/IDF/Form/IssueCreate.php +++ b/src/IDF/Form/IssueCreate.php @@ -36,6 +36,7 @@ class IDF_Form_IssueCreate extends Pluf_Form public $user = null; public $project = null; public $show_full = false; + public $relation_types = null; public function initFields($extra=array()) { @@ -45,9 +46,12 @@ class IDF_Form_IssueCreate extends Pluf_Form or $this->user->hasPerm('IDF.project-member', $this->project)) { $this->show_full = true; } + $this->relation_types = $this->project->getRelationsFromConfig(); + $contentTemplate = $this->project->getConf()->getVal( 'labels_issue_template', IDF_Form_IssueTrackingConf::init_template ); + $this->fields['summary'] = new Pluf_Form_Field_Varchar( array('required' => true, 'label' => __('Summary'), @@ -109,11 +113,10 @@ class IDF_Form_IssueCreate extends Pluf_Form ), )); - $relation_types = $extra['project']->getRelationsFromConfig(); $this->fields['relation_type'] = new Pluf_Form_Field_Varchar( array('required' => false, 'label' => __('This issue'), - 'initial' => $relation_types[0], + 'initial' => current($this->relation_types), 'widget_attrs' => array('size' => 15), )); @@ -250,6 +253,49 @@ class IDF_Form_IssueCreate extends Pluf_Form return $this->cleaned_data['status']; } + function clean_relation_type() + { + $relation_type = trim($this->cleaned_data['relation_type']); + if (empty($relation_type)) + return ''; + + $found = false; + foreach ($this->relation_types as $type) { + if ($type == $relation_type) { + $found = true; + break; + } + } + if (!$found) { + throw new Pluf_Form_Invalid(__('You provided an invalid relation type.')); + } + return $relation_type; + } + + function clean_relation_issue() + { + $issues = trim($this->cleaned_data['relation_issue']); + if (empty($issues)) + return ''; + + $issue_ids = preg_split('/\s*,\s*/', $issues, -1, PREG_SPLIT_NO_EMPTY); + foreach ($issue_ids as $issue_id) { + if (!ctype_digit($issue_id) || (int)$issue_id < 1) { + throw new Pluf_Form_Invalid(sprintf( + __('The value "%s" is not a valid issue id.'), $issue_id + )); + } + $issue = new IDF_Issue($issue_id); + if ($issue->id != $issue_id || $issue->project != $this->project->id) { + throw new Pluf_Form_Invalid(sprintf( + __('The issue "%s" does not exist.'), $issue_id + )); + } + } + + return implode(', ', $issue_ids); + } + /** * Clean the attachments post failure. */ @@ -313,6 +359,27 @@ class IDF_Form_IssueCreate extends Pluf_Form foreach ($tags as $tag) { $issue->setAssoc($tag); } + // add relations + $verb = $this->cleaned_data['relation_type']; + $other_verb = $this->relation_types[$verb]; + $related_issues = preg_split('/\s*,\s*/', $this->cleaned_data['relation_issue'], -1, PREG_SPLIT_NO_EMPTY); + foreach ($related_issues as $related_issue_id) { + $related_issue = new IDF_Issue($related_issue_id); + $rel = new IDF_IssueRelation(); + $rel->issue = $issue; + $rel->verb = $verb; + $rel->other_issue = $related_issue; + $rel->submitter = $this->user; + $rel->create(); + + $other_rel = new IDF_IssueRelation(); + $other_rel->issue = $related_issue; + $other_rel->verb = $other_verb; + $other_rel->other_issue = $issue; + $other_rel->submitter = $this->user; + $other_rel->create(); + } + // add the first comment $comment = new IDF_IssueComment(); $comment->issue = $issue; diff --git a/src/IDF/Form/IssueTrackingConf.php b/src/IDF/Form/IssueTrackingConf.php index c62a10d..738c3e4 100644 --- a/src/IDF/Form/IssueTrackingConf.php +++ b/src/IDF/Form/IssueTrackingConf.php @@ -72,10 +72,24 @@ Performance = Performance issue Usability = Affects program usability Maintainability = Hinders future changes'; const init_one_max = 'Type, Priority, Milestone'; + // ATTENTION: if you change something here, change the values below as well! const init_relations = 'is related to blocks, is blocked by duplicates, is duplicated by'; + // These are actually all noop's, but we have no other chance to + // tell IDF's translation mechanism to mark the strings as translatable + // FIXME: IDF should get a internal translation system for strings like + // that, that can also be easily expanded by users + private function noop() + { + __('is related to'); + __('blocks'); + __('is blocked by'); + __('duplicates'); + __('is duplicated by'); + } + public function initFields($extra=array()) { $this->fields['labels_issue_template'] = new Pluf_Form_Field_Varchar( diff --git a/src/IDF/Form/IssueUpdate.php b/src/IDF/Form/IssueUpdate.php index 9835bc3..4571ab1 100644 --- a/src/IDF/Form/IssueUpdate.php +++ b/src/IDF/Form/IssueUpdate.php @@ -39,6 +39,7 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate or $this->user->hasPerm('IDF.project-member', $this->project)) { $this->show_full = true; } + $this->relation_types = $this->project->getRelationsFromConfig(); if ($this->show_full) { $this->fields['summary'] = new Pluf_Form_Field_Varchar( array('required' => true, @@ -103,11 +104,10 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate ), )); - $relation_types = $extra['project']->getRelationsFromConfig(); $this->fields['relation_type'] = new Pluf_Form_Field_Varchar( array('required' => false, 'label' => __('This issue'), - 'initial' => $relation_types[0], + 'initial' => current($this->relation_types), 'widget_attrs' => array('size' => 15), )); diff --git a/src/IDF/Issue.php b/src/IDF/Issue.php index b173209..6346621 100644 --- a/src/IDF/Issue.php +++ b/src/IDF/Issue.php @@ -169,6 +169,24 @@ class IDF_Issue extends Pluf_Model } } + function getGroupedRelatedIssues($opts = array()) + { + $rels = $this->get_related_issues_list(array_merge($opts, array( + 'view' => 'with_other_issue', + ))); + + $res = array(); + foreach ($rels as $rel) { + $verb = $rel->verb; + if (!array_key_exists($verb, $res)) { + $res[$verb] = array(); + } + $res[$verb][] = $rel; + } + + return $res; + } + /** * Returns an HTML fragment used to display this issue in the * timeline. diff --git a/src/IDF/IssueRelation.php b/src/IDF/IssueRelation.php index f918631..b0048dd 100644 --- a/src/IDF/IssueRelation.php +++ b/src/IDF/IssueRelation.php @@ -45,7 +45,7 @@ class IDF_IssueRelation extends Pluf_Model 'model' => 'IDF_Issue', 'blank' => false, 'verbose' => __('issue'), - 'relate_name' => 'issues', + 'relate_name' => 'related_issues', ), 'verb' => array( @@ -59,7 +59,7 @@ class IDF_IssueRelation extends Pluf_Model 'model' => 'IDF_Issue', 'blank' => false, 'verbose' => __('other issue'), - 'relate_name' => 'other_issues', + 'relate_name' => 'related_other_issues', ), 'submitter' => array( @@ -82,6 +82,13 @@ class IDF_IssueRelation extends Pluf_Model 'type' => 'normal', ), ); + $issuetbl = $this->_con->pfx.'idf_issues'; + $this->_a['views'] = array( + 'with_other_issue' => array( + 'join' => 'INNER JOIN '.$issuetbl.' ON other_issue='.$issuetbl.'.id', + 'select' => $this->getSelect().', summary', + 'props' => array('summary' => 'other_summary'), + )); } function preSave($create=false) diff --git a/src/IDF/Project.php b/src/IDF/Project.php index 211a9ed..2f09219 100644 --- a/src/IDF/Project.php +++ b/src/IDF/Project.php @@ -234,7 +234,10 @@ class IDF_Project extends Pluf_Model } /** - * Returns a list of relations which are available in this project + * Returns a list of relations which are available in this project as + * associative array. Each key-value pair marks a set of orthogonal + * relations. To ease processing, each of these pairs is included twice + * in the array, once as key1 => key2 and once as key2 => key1. * * @return array List of relation names */ @@ -244,7 +247,11 @@ class IDF_Project extends Pluf_Model $rel = $conf->getVal('issue_relations', IDF_Form_IssueTrackingConf::init_relations); $relations = array(); foreach (preg_split("/\015\012|\015|\012/", $rel, -1, PREG_SPLIT_NO_EMPTY) as $s) { - $relations = array_merge($relations, preg_split("/\s*,\s*/", $s, 2)); + $verbs = preg_split("/\s*,\s*/", $s, 2); + if (count($verbs) == 1) + $relations += array($verbs[0] => $verbs[0]); + else + $relations += array($verbs[0] => $verbs[1], $verbs[1] => $verbs[0]); } return $relations; } diff --git a/src/IDF/Views/Issue.php b/src/IDF/Views/Issue.php index 21800ff..90c8de8 100644 --- a/src/IDF/Views/Issue.php +++ b/src/IDF/Views/Issue.php @@ -404,6 +404,8 @@ class IDF_Views_Issue $issue = Pluf_Shortcuts_GetObjectOr404('IDF_Issue', $match[2]); $prj->inOr404($issue); $comments = $issue->get_comments_list(array('order' => 'id ASC')); + $related_issues = $issue->getGroupedRelatedIssues(array('order' => 'creation_dtime DESC')); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view', array($prj->shortname, $issue->id)); $title = Pluf_Template::markSafe(sprintf(__('Issue %d: %s'), $url, $issue->id, $issue->summary)); @@ -471,7 +473,8 @@ class IDF_Views_Issue 'preview' => $preview, 'interested' => $interested->count(), 'previous_issue_id' => $previous_issue_id, - 'next_issue_id' => $next_issue_id + 'next_issue_id' => $next_issue_id, + 'related_issues' => $related_issues, ), $arrays), $request); diff --git a/src/IDF/templates/idf/issues/view.html b/src/IDF/templates/idf/issues/view.html index e27be87..069cd0b 100644 --- a/src/IDF/templates/idf/issues/view.html +++ b/src/IDF/templates/idf/issues/view.html @@ -170,6 +170,19 @@ {$tag.class}:{$tag.name}
{/foreach}

{/if} +{if count($related_issues) > 0} +{foreach $related_issues as $verb => $rel_issues} +{blocktrans}This issue {$verb}{/blocktrans}
+ {foreach $rel_issues as $rel_issue} + + + {$rel_issue.other_issue} - {$rel_issue.other_summary|shorten:30} + +
+ {/foreach} +{/foreach} +{/if} {/block} {block javascript}{if $form}{include 'idf/issues/js-autocomplete.html'} From 0aa5999bb37698513d66453d98101232942a6af3 Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Mon, 30 May 2011 14:02:10 +0200 Subject: [PATCH 6/8] Finalize ticket relations (closes issue 638) - IssueUpdate.php: use dynamically set field validators for dynamically created fields; let relation_type0 and relation_issue0 exist at any time; check the validity of a user selection and combine the various input fields if possible; do the database updates for links; change the "change" format for labels to a more precise structure and no longer trust on a leading dash for removed labels - IssueCreate.php: change the validator calls and field names - Issue.php (getGroupedRelatedIssues): make it possible to return only a flat list of integers for easier processing - 17AddIssueRelations.php: migrate the previous serialized "changes" format for issue comments to the new, more structured format (up and down) - js-autocomplete.html: add support for multiple input fields - view.html: output relation changes and wrap the related issues stanzas into paragraphs - NEWS.mdtext: note the addition and the need for a specific version of Pluf --- NEWS.mdtext | 8 +- src/IDF/Form/IssueCreate.php | 30 ++-- src/IDF/Form/IssueUpdate.php | 133 +++++++++++++++++- src/IDF/Issue.php | 4 +- src/IDF/IssueComment.php | 12 +- src/IDF/Migrations/17AddIssueRelations.php | 46 ++++++ src/IDF/Views/Issue.php | 2 +- src/IDF/templates/idf/issues/create.html | 10 +- .../templates/idf/issues/js-autocomplete.html | 61 ++++---- src/IDF/templates/idf/issues/view.html | 33 ++++- 10 files changed, 279 insertions(+), 60 deletions(-) diff --git a/NEWS.mdtext b/NEWS.mdtext index 1acf206..7675253 100644 --- a/NEWS.mdtext +++ b/NEWS.mdtext @@ -1,8 +1,14 @@ # InDefero 1.2 - xxx xxx xx xx:xx 2011 UTC +ATTENTION: You need Pluf [324ae60b](http://projects.ceondo.com/p/pluf/source/commit/324ae60b) +or newer to properly run this version of Indefero! + ## New Features -## Bugfixes +- Indefero's issue tracker can now bi-directionally link issues with variable, configurable + terms, such as "is related to", "is blocked by" or "is duplicated by" (issue 638) + +## Bugfixes - monotone zip archive entries now all carry the revision date as mtime (issue 645) - Timeline only displays filter options for items a user has actually access to (issue 655) diff --git a/src/IDF/Form/IssueCreate.php b/src/IDF/Form/IssueCreate.php index 859adb1..c295ca1 100644 --- a/src/IDF/Form/IssueCreate.php +++ b/src/IDF/Form/IssueCreate.php @@ -113,14 +113,14 @@ class IDF_Form_IssueCreate extends Pluf_Form ), )); - $this->fields['relation_type'] = new Pluf_Form_Field_Varchar( + $this->fields['relation_type0'] = new Pluf_Form_Field_Varchar( array('required' => false, 'label' => __('This issue'), 'initial' => current($this->relation_types), 'widget_attrs' => array('size' => 15), )); - $this->fields['relation_issue'] = new Pluf_Form_Field_Varchar( + $this->fields['relation_issue0'] = new Pluf_Form_Field_Varchar( array('required' => false, 'label' => null, 'initial' => '', @@ -253,9 +253,11 @@ class IDF_Form_IssueCreate extends Pluf_Form return $this->cleaned_data['status']; } - function clean_relation_type() + // this method is not called from Pluf_Form directly, but shared for + // among all similar fields + function clean_relation_type($value) { - $relation_type = trim($this->cleaned_data['relation_type']); + $relation_type = trim($value); if (empty($relation_type)) return ''; @@ -272,9 +274,16 @@ class IDF_Form_IssueCreate extends Pluf_Form return $relation_type; } - function clean_relation_issue() + function clean_relation_type0() { - $issues = trim($this->cleaned_data['relation_issue']); + return $this->clean_relation_type($this->cleaned_data['relation_type0']); + } + + // this method is not called from Pluf_Form directly, but shared for + // among all similar fields + function clean_relation_issue($value) + { + $issues = trim($value); if (empty($issues)) return ''; @@ -296,6 +305,11 @@ class IDF_Form_IssueCreate extends Pluf_Form return implode(', ', $issue_ids); } + function clean_relation_issue0() + { + return $this->clean_relation_issue($this->cleaned_data['relation_issue0']); + } + /** * Clean the attachments post failure. */ @@ -360,9 +374,9 @@ class IDF_Form_IssueCreate extends Pluf_Form $issue->setAssoc($tag); } // add relations - $verb = $this->cleaned_data['relation_type']; + $verb = $this->cleaned_data['relation_type0']; $other_verb = $this->relation_types[$verb]; - $related_issues = preg_split('/\s*,\s*/', $this->cleaned_data['relation_issue'], -1, PREG_SPLIT_NO_EMPTY); + $related_issues = preg_split('/\s*,\s*/', $this->cleaned_data['relation_issue0'], -1, PREG_SPLIT_NO_EMPTY); foreach ($related_issues as $related_issue_id) { $related_issue = new IDF_Issue($related_issue_id); $rel = new IDF_IssueRelation(); diff --git a/src/IDF/Form/IssueUpdate.php b/src/IDF/Form/IssueUpdate.php index 4571ab1..c4c7df0 100644 --- a/src/IDF/Form/IssueUpdate.php +++ b/src/IDF/Form/IssueUpdate.php @@ -104,20 +104,51 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate ), )); - $this->fields['relation_type'] = new Pluf_Form_Field_Varchar( + $idx = 0; + // note: clean_relation_type0 and clean_relation_issue0 already + // exist in the base class + $this->fields['relation_type'.$idx] = new Pluf_Form_Field_Varchar( array('required' => false, 'label' => __('This issue'), 'initial' => current($this->relation_types), 'widget_attrs' => array('size' => 15), )); - $this->fields['relation_issue'] = new Pluf_Form_Field_Varchar( + $this->fields['relation_issue'.$idx] = new Pluf_Form_Field_Varchar( array('required' => false, 'label' => null, 'initial' => '', 'widget_attrs' => array('size' => 10), )); + ++$idx; + $relatedIssues = $this->issue->getGroupedRelatedIssues(array(), true); + foreach ($relatedIssues as $verb => $ids) { + $this->fields['relation_type'.$idx] = new Pluf_Form_Field_Varchar( + array('required' => false, + 'label' => __('This issue'), + 'initial' => $verb, + 'widget_attrs' => array('size' => 15), + )); + $m = 'clean_relation_type'.$idx; + $this->$m = create_function('$form', ' + return $form->clean_relation_type($form->cleaned_data["relation_type'.$idx.'"]); + '); + + $this->fields['relation_issue'.$idx] = new Pluf_Form_Field_Varchar( + array('required' => false, + 'label' => null, + 'initial' => implode(', ', $ids), + 'widget_attrs' => array('size' => 10), + )); + $m = 'clean_relation_issue'.$idx; + $this->$m = create_function('$form', ' + return $form->clean_relation_issue($form->cleaned_data["relation_issue'.$idx.'"]); + '); + + ++$idx; + } + $tags = $this->issue->get_tags_list(); for ($i=1;$i<7;$i++) { $initial = ''; @@ -171,6 +202,48 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate public function clean() { $this->cleaned_data = parent::clean(); + + // normalize the user's input by removing dublettes and by combining + // ids from identical verbs in different input fields into one array + $normRelatedIssues = array(); + for ($idx = 0; isset($this->cleaned_data['relation_type'.$idx]); ++$idx) { + $verb = $this->cleaned_data['relation_type'.$idx]; + $ids = preg_split('/\s*,\s*/', $this->cleaned_data['relation_issue'.$idx], + -1, PREG_SPLIT_NO_EMPTY); + if (count($ids) == 0) + continue; + + if (!array_key_exists($verb, $normRelatedIssues)) + $normRelatedIssues[$verb] = array(); + foreach ($ids as $id) { + if (!in_array($id, $normRelatedIssues[$verb])) + $normRelatedIssues[$verb][] = $id; + } + } + + // now look at any added / removed ids + $added = $removed = array(); + $relatedIssues = $this->issue->getGroupedRelatedIssues(array(), true); + $added = array_diff_key($normRelatedIssues, $relatedIssues); + $removed = array_diff_key($relatedIssues, $normRelatedIssues); + + $keysToLookAt = array_keys( + array_intersect_key($relatedIssues, $normRelatedIssues) + ); + foreach ($keysToLookAt as $key) { + $a = array_diff($normRelatedIssues[$key], $relatedIssues[$key]); + if (count($a) > 0) + $added[$key] = $a; + $r = array_diff($relatedIssues[$key], $normRelatedIssues[$key]); + if (count($r) > 0) + $removed[$key] = $r; + } + + // cache the added / removed data, so we do not have to + // calculate that again + $this->cleaned_data['_added_issue_relations'] = $added; + $this->cleaned_data['_removed_issue_relations'] = $removed; + // As soon as we know that at least one change was done, we // return the cleaned data and do not go further. if (strlen(trim($this->cleaned_data['content']))) { @@ -230,6 +303,11 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate return $this->cleaned_data; } } + + if (count($this->cleaned_data['_added_issue_relations']) != 0 || + count($this->cleaned_data['_removed_issue_relations']) != 0) { + return $this->cleaned_data; + } } // no changes! throw new Pluf_Form_Invalid(__('No changes were entered.')); @@ -271,20 +349,22 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate foreach ($tags as $tag) { if (!Pluf_Model_InArray($tag, $oldtags)) { if (!isset($changes['lb'])) $changes['lb'] = array(); + if (!isset($changes['lb']['add'])) $changes['lb']['add'] = array(); if ($tag->class != 'Other') { - $changes['lb'][] = (string) $tag; //new tag + $changes['lb']['add'][] = (string) $tag; //new tag } else { - $changes['lb'][] = (string) $tag->name; + $changes['lb']['add'][] = (string) $tag->name; } } } foreach ($oldtags as $tag) { if (!Pluf_Model_InArray($tag, $tags)) { if (!isset($changes['lb'])) $changes['lb'] = array(); + if (!isset($changes['lb']['rem'])) $changes['lb']['rem'] = array(); if ($tag->class != 'Other') { - $changes['lb'][] = '-'.(string) $tag; //new tag + $changes['lb']['rem'][] = (string) $tag; //new tag } else { - $changes['lb'][] = '-'.(string) $tag->name; + $changes['lb']['rem'][] = (string) $tag->name; } } } @@ -302,6 +382,47 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate 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; } + // Issue relations - additions + foreach ($this->cleaned_data['_added_issue_relations'] as $verb => $ids) { + $other_verb = $this->relation_types[$verb]; + foreach ($ids as $id) { + $related_issue = new IDF_Issue($id); + $rel = new IDF_IssueRelation(); + $rel->issue = $this->issue; + $rel->verb = $verb; + $rel->other_issue = $related_issue; + $rel->submitter = $this->user; + $rel->create(); + + $other_rel = new IDF_IssueRelation(); + $other_rel->issue = $related_issue; + $other_rel->verb = $other_verb; + $other_rel->other_issue = $this->issue; + $other_rel->submitter = $this->user; + $other_rel->create(); + } + if (!isset($changes['rel'])) $changes['rel'] = array(); + if (!isset($changes['rel']['add'])) $changes['rel']['add'] = array(); + $changes['rel']['add'][] = $verb.' '.implode(', ', $ids); + } + // Issue relations - removals + foreach ($this->cleaned_data['_removed_issue_relations'] as $verb => $ids) { + foreach ($ids as $id) { + $db = &Pluf::db(); + $table = Pluf::factory('IDF_IssueRelation')->getSqlTable(); + $sql = new Pluf_SQL('verb=%s AND ( + (issue=%s AND other_issue=%s) OR + (other_issue=%s AND issue=%s))', + array($verb, + $this->issue->id, $id, + $this->issue->id, $id)); + $db->execute('DELETE FROM '.$table.' WHERE '.$sql->gen()); + } + + if (!isset($changes['rel'])) $changes['rel'] = array(); + if (!isset($changes['rel']['rem'])) $changes['rel']['rem'] = array(); + $changes['rel']['rem'][] = $verb.' '.implode(', ', $ids); + } // Update the issue $this->issue->batchAssoc('IDF_Tag', $tagids); $this->issue->summary = trim($this->cleaned_data['summary']); diff --git a/src/IDF/Issue.php b/src/IDF/Issue.php index 6346621..8d1ed34 100644 --- a/src/IDF/Issue.php +++ b/src/IDF/Issue.php @@ -169,7 +169,7 @@ class IDF_Issue extends Pluf_Model } } - function getGroupedRelatedIssues($opts = array()) + function getGroupedRelatedIssues($opts = array(), $idsOnly = false) { $rels = $this->get_related_issues_list(array_merge($opts, array( 'view' => 'with_other_issue', @@ -181,7 +181,7 @@ class IDF_Issue extends Pluf_Model if (!array_key_exists($verb, $res)) { $res[$verb] = array(); } - $res[$verb][] = $rel; + $res[$verb][] = $idsOnly ? $rel->other_issue : $rel; } return $res; diff --git a/src/IDF/IssueComment.php b/src/IDF/IssueComment.php index 366b3e2..20c8339 100644 --- a/src/IDF/IssueComment.php +++ b/src/IDF/IssueComment.php @@ -155,10 +155,18 @@ class IDF_IssueComment extends Pluf_Model $out .= __('Owner:'); break; case 'lb': $out .= __('Labels:'); break; + case 'rel': + $out .= __('Relations:'); break; } $out .= ' '; - if ($w == 'lb') { - $out .= Pluf_esc(implode(', ', $v)); + if ($w == 'lb' || $w == 'rel') { + foreach ($v as $t => $ls) { + foreach ($ls as $l) { + if ($t == 'rem') $out .= ''; + $out .= Pluf_esc($l); + if ($t == 'rem') $out .= ' '; + } + } } else { $out .= Pluf_esc($v); } diff --git a/src/IDF/Migrations/17AddIssueRelations.php b/src/IDF/Migrations/17AddIssueRelations.php index 0bf7700..e9f3636 100644 --- a/src/IDF/Migrations/17AddIssueRelations.php +++ b/src/IDF/Migrations/17AddIssueRelations.php @@ -32,6 +32,27 @@ function IDF_Migrations_17AddIssueRelations_up($params=null) $schema = new Pluf_DB_Schema($db); $schema->model = new IDF_IssueRelation(); $schema->createTables(); + + // change the serialization format for added / removed labels in IDF_IssueComment + $comments = Pluf::factory('IDF_IssueComment')->getList(); + foreach ($comments as $comment) { + if (!isset($comment->changes['lb'])) continue; + $changes = $comment->changes; + $adds = $removals = array(); + foreach ($comment->changes['lb'] as $lb) { + if (substr($lb, 0, 1) == '-') + $removals[] = substr($lb, 1); + else + $adds[] = $lb; + } + $changes['lb'] = array(); + if (count($adds) > 0) + $changes['lb']['add'] = $adds; + if (count($removals) > 0) + $changes['lb']['rem'] = $removals; + $comment->changes = $changes; + $comment->update(); + } } function IDF_Migrations_17AddIssueRelations_down($params=null) @@ -40,5 +61,30 @@ function IDF_Migrations_17AddIssueRelations_down($params=null) $schema = new Pluf_DB_Schema($db); $schema->model = new IDF_IssueRelation(); $schema->dropTables(); + + // change the serialization format for added / removed labels in IDF_IssueComment + $comments = Pluf::factory('IDF_IssueComment')->getList(); + foreach ($comments as $comment) { + $changes = $comment->changes; + if (empty($changes)) + continue; + if (isset($changes['lb'])) { + $labels = array(); + foreach ($changes['lb'] as $type => $lbs) { + if (!is_array($lbs)) { + $labels[] = $lbs; + continue; + } + foreach ($lbs as $lb) { + $labels[] = ($type == 'rem' ? '-' : '') . $lb; + } + } + $changes['lb'] = $labels; + } + // while we're at it, remove any 'rel' changes + unset($changes['rel']); + $comment->changes = $changes; + $comment->update(); + } } diff --git a/src/IDF/Views/Issue.php b/src/IDF/Views/Issue.php index 90c8de8..91fdc40 100644 --- a/src/IDF/Views/Issue.php +++ b/src/IDF/Views/Issue.php @@ -404,7 +404,7 @@ class IDF_Views_Issue $issue = Pluf_Shortcuts_GetObjectOr404('IDF_Issue', $match[2]); $prj->inOr404($issue); $comments = $issue->get_comments_list(array('order' => 'id ASC')); - $related_issues = $issue->getGroupedRelatedIssues(array('order' => 'creation_dtime DESC')); + $related_issues = $issue->getGroupedRelatedIssues(array('order' => 'other_issue ASC')); $url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view', array($prj->shortname, $issue->id)); diff --git a/src/IDF/templates/idf/issues/create.html b/src/IDF/templates/idf/issues/create.html index e204a54..9d681a7 100644 --- a/src/IDF/templates/idf/issues/create.html +++ b/src/IDF/templates/idf/issues/create.html @@ -63,12 +63,12 @@ -{$form.f.relation_type.labelTag}: +{$form.f.relation_type0.labelTag}: -{if $form.f.relation_type.errors}{$form.f.relation_type.fieldErrors}{/if} -{if $form.f.relation_issue.errors}{$form.f.relation_issue.fieldErrors}{/if} -{$form.f.relation_type|unsafe} -{$form.f.relation_issue|unsafe} +{if $form.f.relation_type0.errors}{$form.f.relation_type0.fieldErrors}{/if} +{if $form.f.relation_issue0.errors}{$form.f.relation_issue0.fieldErrors}{/if} +{$form.f.relation_type0|unsafe} +{$form.f.relation_issue0|unsafe} diff --git a/src/IDF/templates/idf/issues/js-autocomplete.html b/src/IDF/templates/idf/issues/js-autocomplete.html index 8506783..575e1a5 100644 --- a/src/IDF/templates/idf/issues/js-autocomplete.html +++ b/src/IDF/templates/idf/issues/js-autocomplete.html @@ -51,34 +51,39 @@ return row.to; } }); - $("#id_relation_type").autocomplete(auto_relation_types, { - minChars: 0, - width: 310, - matchContains: true, - max: 50, - highlightItem: false, - formatItem: function(row, i, max, term) { - return row.to.replace(new RegExp("(" + term + ")", "gi"), "$1") + " " + row.name + ""; - }, - formatResult: function(row) { - return row.to; - } - }); - $("#id_relation_issue").autocomplete("{/literal}{url 'IDF_Views_Issue::autoCompleteIssueList', array($project.shortname, $issue.id)}{literal}", { - minChars: 0, - width: 310, - matchContains: true, - max: 10, - multiple: true, - delay: 500, - highlightItem: false, - formatItem: function(row, i, max, term) { - return row[1].replace(new RegExp("(" + term + ")", "gi"), "$1") + " " + row[0] + ""; - }, - formatResult: function(row) { - return row[1]; - } - }); + for (var idx = 0; ; ++idx) { + if ($("#id_relation_type" + idx).length == 0) + break; + + $("#id_relation_type" + idx).autocomplete(auto_relation_types, { + minChars: 0, + width: 310, + matchContains: true, + max: 50, + highlightItem: false, + formatItem: function(row, i, max, term) { + return row.to.replace(new RegExp("(" + term + ")", "gi"), "$1") + " " + row.name + ""; + }, + formatResult: function(row) { + return row.to; + } + }); + $("#id_relation_issue" + idx).autocomplete("{/literal}{url 'IDF_Views_Issue::autoCompleteIssueList', array($project.shortname, $issue.id)}{literal}", { + minChars: 0, + width: 310, + matchContains: true, + max: 10, + multiple: true, + delay: 500, + highlightItem: false, + formatItem: function(row, i, max, term) { + return row[1].replace(new RegExp("(" + term + ")", "gi"), "$1") + " " + row[0] + ""; + }, + formatResult: function(row) { + return row[1]; + } + }); + } }); {/literal} //--> diff --git a/src/IDF/templates/idf/issues/view.html b/src/IDF/templates/idf/issues/view.html index 069cd0b..8694725 100644 --- a/src/IDF/templates/idf/issues/view.html +++ b/src/IDF/templates/idf/issues/view.html @@ -40,7 +40,16 @@ {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}
+{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 == 'rel'}{trans 'Relations:'}{/if} +{if $w == 'lb' or $w == 'rel'} + {foreach $v as $t => $ls} + {foreach $ls as $l} + {if $t == 'rem'}{/if}{$l}{if $t == 'rem'} {/if} + {/foreach} + {/foreach} +{else} + {$v} +{/if}
{/foreach}
{/if} @@ -120,12 +129,20 @@ -{$form.f.relation_type.labelTag}: +{$form.f.relation_type0.labelTag}: -{if $form.f.relation_type.errors}{$form.f.relation_type.fieldErrors}{/if} -{if $form.f.relation_issue.errors}{$form.f.relation_issue.fieldErrors}{/if} -{$form.f.relation_type|unsafe} -{$form.f.relation_issue|unsafe} +{assign $prevField} +{foreach $form as $field} + {if strpos($field.name, 'relation_type') === 0} + {$field|unsafe} + {assign $prevField = $field} + {/if} + {if strpos($field.name, 'relation_issue') === 0} + {$field|unsafe}
+ {if $prevField.errors}{$prevField.fieldErrors}{/if} + {if $field.errors}{$field.fieldErrors}{/if} + {/if} +{/foreach} @@ -172,15 +189,17 @@

{/if} {if count($related_issues) > 0} {foreach $related_issues as $verb => $rel_issues} +

{blocktrans}This issue {$verb}{/blocktrans}
{foreach $rel_issues as $rel_issue} + class="label" title="{$rel_issue.other_summary}"> {$rel_issue.other_issue} - {$rel_issue.other_summary|shorten:30}
{/foreach} +

{/foreach} {/if} From f412099f690debaab49e220e9fab70ae75ccbd6b Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Mon, 30 May 2011 14:48:36 +0200 Subject: [PATCH 7/8] Fix whitespace issues in the timeline. --- src/IDF/IssueComment.php | 3 ++- src/IDF/templates/idf/issues/view.html | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/IDF/IssueComment.php b/src/IDF/IssueComment.php index 20c8339..dd4edd8 100644 --- a/src/IDF/IssueComment.php +++ b/src/IDF/IssueComment.php @@ -164,7 +164,8 @@ class IDF_IssueComment extends Pluf_Model foreach ($ls as $l) { if ($t == 'rem') $out .= ''; $out .= Pluf_esc($l); - if ($t == 'rem') $out .= ' '; + if ($t == 'rem') $out .= ''; + $out .= ' '; } } } else { diff --git a/src/IDF/templates/idf/issues/view.html b/src/IDF/templates/idf/issues/view.html index 8694725..488819c 100644 --- a/src/IDF/templates/idf/issues/view.html +++ b/src/IDF/templates/idf/issues/view.html @@ -44,7 +44,7 @@ {if $w == 'lb' or $w == 'rel'} {foreach $v as $t => $ls} {foreach $ls as $l} - {if $t == 'rem'}{/if}{$l}{if $t == 'rem'} {/if} + {if $t == 'rem'}{/if}{$l}{if $t == 'rem'}{/if} {/foreach} {/foreach} {else} From 9bcb5f9456b72b565faf56b3c97bb71fbf98c565 Mon Sep 17 00:00:00 2001 From: Thomas Keller Date: Tue, 31 May 2011 10:51:24 +0200 Subject: [PATCH 8/8] Fix a PHP notice / Pluf exception in case any issue type field is cleared. --- src/IDF/Form/IssueCreate.php | 39 +++++++++++++++++++----------------- src/IDF/Form/IssueUpdate.php | 3 +++ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/IDF/Form/IssueCreate.php b/src/IDF/Form/IssueCreate.php index c295ca1..3f89988 100644 --- a/src/IDF/Form/IssueCreate.php +++ b/src/IDF/Form/IssueCreate.php @@ -373,25 +373,28 @@ class IDF_Form_IssueCreate extends Pluf_Form foreach ($tags as $tag) { $issue->setAssoc($tag); } - // add relations - $verb = $this->cleaned_data['relation_type0']; - $other_verb = $this->relation_types[$verb]; - $related_issues = preg_split('/\s*,\s*/', $this->cleaned_data['relation_issue0'], -1, PREG_SPLIT_NO_EMPTY); - foreach ($related_issues as $related_issue_id) { - $related_issue = new IDF_Issue($related_issue_id); - $rel = new IDF_IssueRelation(); - $rel->issue = $issue; - $rel->verb = $verb; - $rel->other_issue = $related_issue; - $rel->submitter = $this->user; - $rel->create(); + // add relations (if any) + if (!empty($this->cleaned_data['relation_type0'])) { + $verb = $this->cleaned_data['relation_type0']; + $other_verb = $this->relation_types[$verb]; + $related_issues = preg_split('/\s*,\s*/', $this->cleaned_data['relation_issue0'], -1, PREG_SPLIT_NO_EMPTY); - $other_rel = new IDF_IssueRelation(); - $other_rel->issue = $related_issue; - $other_rel->verb = $other_verb; - $other_rel->other_issue = $issue; - $other_rel->submitter = $this->user; - $other_rel->create(); + foreach ($related_issues as $related_issue_id) { + $related_issue = new IDF_Issue($related_issue_id); + $rel = new IDF_IssueRelation(); + $rel->issue = $issue; + $rel->verb = $verb; + $rel->other_issue = $related_issue; + $rel->submitter = $this->user; + $rel->create(); + + $other_rel = new IDF_IssueRelation(); + $other_rel->issue = $related_issue; + $other_rel->verb = $other_verb; + $other_rel->other_issue = $issue; + $other_rel->submitter = $this->user; + $other_rel->create(); + } } // add the first comment diff --git a/src/IDF/Form/IssueUpdate.php b/src/IDF/Form/IssueUpdate.php index c4c7df0..0362eb3 100644 --- a/src/IDF/Form/IssueUpdate.php +++ b/src/IDF/Form/IssueUpdate.php @@ -208,6 +208,9 @@ class IDF_Form_IssueUpdate extends IDF_Form_IssueCreate $normRelatedIssues = array(); for ($idx = 0; isset($this->cleaned_data['relation_type'.$idx]); ++$idx) { $verb = $this->cleaned_data['relation_type'.$idx]; + if (empty($verb)) + continue; + $ids = preg_split('/\s*,\s*/', $this->cleaned_data['relation_issue'.$idx], -1, PREG_SPLIT_NO_EMPTY); if (count($ids) == 0)