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 @@ -234,6 +234,22 @@ class IDF_Project extends Pluf_Model } /** + * 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. * * The array has 3 keys: 'members', 'owners' and 'authorized'. 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; @@ -644,6 +644,35 @@ class IDF_Views_Issue } /** + * 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. */ public $star_precond = array('IDF_Precondition::accessIssues', @@ -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 @@