summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMax Kanat-Alexander <mkanat@bugzilla.org>2010-06-22 04:10:21 +0200
committerMax Kanat-Alexander <mkanat@bugzilla.org>2010-06-22 04:10:21 +0200
commit4acb2424e62cbd64bc92a5dec2cbe1e2b7096157 (patch)
tree9076b3809846e8fc58ce720e0cd32dc0d3c8ff77
parent601bda78fa436e7030ebcefc589d930c99f1373e (diff)
downloadbugzilla-4acb2424e62cbd64bc92a5dec2cbe1e2b7096157.tar.gz
bugzilla-4acb2424e62cbd64bc92a5dec2cbe1e2b7096157.tar.xz
Bug 22353: Automatic duplicate bug detection on enter_bug.cgi
r=glob, a=mkanat
-rw-r--r--.bzrignore1
-rw-r--r--Bugzilla/Bug.pm133
-rw-r--r--Bugzilla/Constants.pm5
-rw-r--r--Bugzilla/DB.pm5
-rw-r--r--Bugzilla/DB/Mysql.pm5
-rw-r--r--Bugzilla/DB/Oracle.pm5
-rw-r--r--Bugzilla/User.pm2
-rw-r--r--Bugzilla/WebService/Bug.pm35
-rw-r--r--js/bug.js117
-rw-r--r--skins/standard/enter_bug.css65
-rw-r--r--skins/standard/global.css39
-rw-r--r--template/en/default/bug/create/create.html.tmpl46
-rw-r--r--template/en/default/global/header.html.tmpl21
13 files changed, 400 insertions, 79 deletions
diff --git a/.bzrignore b/.bzrignore
index 277148f38..7ab83e7ad 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -19,6 +19,7 @@
/skins/contrib/Dusk/dependency-tree.css
/skins/contrib/Dusk/duplicates.css
/skins/contrib/Dusk/editusers.css
+/skins/contrib/Dusk/enter_bug.css
/skins/contrib/Dusk/help.css
/skins/contrib/Dusk/panel.css
/skins/contrib/Dusk/page.css
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 6df7363d5..80a4b5933 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -49,7 +49,7 @@ use Bugzilla::Group;
use Bugzilla::Status;
use Bugzilla::Comment;
-use List::MoreUtils qw(firstidx);
+use List::MoreUtils qw(firstidx uniq);
use List::Util qw(min first);
use Storable qw(dclone);
use URI;
@@ -446,6 +446,87 @@ sub match {
return $class->SUPER::match(@_);
}
+sub possible_duplicates {
+ my ($class, $params) = @_;
+ my $short_desc = $params->{summary};
+ my $products = $params->{products} || [];
+ my $limit = $params->{limit} || MAX_POSSIBLE_DUPLICATES;
+ $limit = MAX_POSSIBLE_DUPLICATES if $limit > MAX_POSSIBLE_DUPLICATES;
+ $products = [$products] if !ref($products) eq 'ARRAY';
+
+ my $orig_limit = $limit;
+ detaint_natural($limit)
+ || ThrowCodeError('param_must_be_numeric',
+ { function => 'possible_duplicates',
+ param => $orig_limit });
+
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+ my @words = split(/[\b\s]+/, $short_desc || '');
+ # Exclude punctuation from the array.
+ @words = map { /(\w+)/; $1 } @words;
+ # And make sure that each word is longer than 2 characters.
+ @words = grep { defined $_ and length($_) > 2 } @words;
+
+ return [] if !@words;
+
+ my ($where_sql, $relevance_sql);
+ if ($dbh->FULLTEXT_OR) {
+ my $joined_terms = join($dbh->FULLTEXT_OR, @words);
+ ($where_sql, $relevance_sql) =
+ $dbh->sql_fulltext_search('bugs_fulltext.short_desc',
+ $joined_terms, 1);
+ $relevance_sql ||= $where_sql;
+ }
+ else {
+ my (@where, @relevance);
+ my $count = 0;
+ foreach my $word (@words) {
+ $count++;
+ my ($term, $rel_term) = $dbh->sql_fulltext_search(
+ 'bugs_fulltext.short_desc', $word, $count);
+ push(@where, $term);
+ push(@relevance, $rel_term || $term);
+ }
+
+ $where_sql = join(' OR ', @where);
+ $relevance_sql = join(' + ', @relevance);
+ }
+
+ my $product_ids = join(',', map { $_->id } @$products);
+ my $product_sql = $product_ids ? "AND product_id IN ($product_ids)" : "";
+
+ # Because we collapse duplicates, we want to get slightly more bugs
+ # than were actually asked for.
+ my $sql_limit = $limit + 5;
+
+ my $possible_dupes = $dbh->selectall_arrayref(
+ "SELECT bugs.bug_id AS bug_id, bugs.resolution AS resolution,
+ ($relevance_sql) AS relevance
+ FROM bugs
+ INNER JOIN bugs_fulltext ON bugs.bug_id = bugs_fulltext.bug_id
+ WHERE ($where_sql) $product_sql
+ ORDER BY relevance DESC, bug_id DESC
+ LIMIT $sql_limit", {Slice=>{}});
+
+ my @actual_dupe_ids;
+ # Resolve duplicates into their ultimate target duplicates.
+ foreach my $bug (@$possible_dupes) {
+ my $push_id = $bug->{bug_id};
+ if ($bug->{resolution} && $bug->{resolution} eq 'DUPLICATE') {
+ $push_id = _resolve_ultimate_dup_id($bug->{bug_id});
+ }
+ push(@actual_dupe_ids, $push_id);
+ }
+ @actual_dupe_ids = uniq @actual_dupe_ids;
+ if (scalar @actual_dupe_ids > $limit) {
+ @actual_dupe_ids = @actual_dupe_ids[0..($limit-1)];
+ }
+
+ my $visible = $user->visible_bugs(\@actual_dupe_ids);
+ return $class->new_from_list($visible);
+}
+
# Docs for create() (there's no POD in this file yet, but we very
# much need this documented right now):
#
@@ -1426,23 +1507,7 @@ sub _check_dup_id {
# Make sure a loop isn't created when marking this bug
# as duplicate.
- my %dupes;
- my $this_dup = $dupe_of;
- my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?');
-
- while ($this_dup) {
- if ($this_dup == $self->id) {
- ThrowUserError('dupe_loop_detected', { bug_id => $self->id,
- dupe_of => $dupe_of });
- }
- # If $dupes{$this_dup} is already set to 1, then a loop
- # already exists which does not involve this bug.
- # As the user is not responsible for this loop, do not
- # prevent him from marking this bug as a duplicate.
- last if exists $dupes{$this_dup};
- $dupes{$this_dup} = 1;
- $this_dup = $dbh->selectrow_array($sth, undef, $this_dup);
- }
+ _resolve_ultimate_dup_id($self->id, $dupe_of, 1);
my $cur_dup = $self->dup_id || 0;
if ($cur_dup != $dupe_of && Bugzilla->params->{'commentonduplicate'}
@@ -2843,6 +2908,38 @@ sub dup_id {
return $self->{'dup_id'};
}
+sub _resolve_ultimate_dup_id {
+ my ($bug_id, $dupe_of, $loops_are_an_error) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?');
+
+ my $this_dup = $dupe_of || $dbh->selectrow_array($sth, undef, $bug_id);
+ my $last_dup = $bug_id;
+
+ my %dupes;
+ while ($this_dup) {
+ if ($this_dup == $bug_id) {
+ if ($loops_are_an_error) {
+ ThrowUserError('dupe_loop_detected', { bug_id => $bug_id,
+ dupe_of => $dupe_of });
+ }
+ else {
+ return $last_dup;
+ }
+ }
+ # If $dupes{$this_dup} is already set to 1, then a loop
+ # already exists which does not involve this bug.
+ # As the user is not responsible for this loop, do not
+ # prevent him from marking this bug as a duplicate.
+ return $last_dup if exists $dupes{$this_dup};
+ $dupes{$this_dup} = 1;
+ $last_dup = $this_dup;
+ $this_dup = $dbh->selectrow_array($sth, undef, $this_dup);
+ }
+
+ return $last_dup;
+}
+
sub actual_time {
my ($self) = @_;
return $self->{'actual_time'} if exists $self->{'actual_time'};
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 55ef4a0e3..9af9e7b72 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -175,6 +175,7 @@ use File::Basename;
MAX_FIELD_VALUE_SIZE
MAX_FREETEXT_LENGTH
MAX_BUG_URL_LENGTH
+ MAX_POSSIBLE_DUPLICATES
PASSWORD_DIGEST_ALGORITHM
PASSWORD_SALT_LENGTH
@@ -527,6 +528,10 @@ use constant MAX_FREETEXT_LENGTH => 255;
# The longest a bug URL in a BUG_URLS field can be.
use constant MAX_BUG_URL_LENGTH => 255;
+# The largest number of possible duplicates that Bug::possible_duplicates
+# will return.
+use constant MAX_POSSIBLE_DUPLICATES => 25;
+
# This is the name of the algorithm used to hash passwords before storing
# them in the database. This can be any string that is valid to pass to
# Perl's "Digest" module. Note that if you change this, it won't take
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index 8b8d74c90..8c1aba8dd 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -73,6 +73,11 @@ use constant ENUM_DEFAULTS => {
resolution => ["","FIXED","INVALID","WONTFIX", "DUPLICATE","WORKSFORME"],
};
+# The character that means "OR" in a boolean fulltext search. If empty,
+# the database doesn't support OR searches in fulltext searches.
+# Used by Bugzilla::Bug::possible_duplicates.
+use constant FULLTEXT_OR => '';
+
#####################################################################
# Connection Methods
#####################################################################
diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm
index 13069a78a..4b90a2a34 100644
--- a/Bugzilla/DB/Mysql.pm
+++ b/Bugzilla/DB/Mysql.pm
@@ -40,8 +40,8 @@ For interface details see L<Bugzilla::DB> and L<DBI>.
=cut
package Bugzilla::DB::Mysql;
-
use strict;
+use base qw(Bugzilla::DB);
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(install_string);
@@ -57,8 +57,7 @@ use Text::ParseWords;
# MAX_COMMENT_LENGTH is big.
use constant MAX_COMMENTS => 50;
-# This module extends the DB interface via inheritance
-use base qw(Bugzilla::DB);
+use constant FULLTEXT_OR => '|';
sub new {
my ($class, $params) = @_;
diff --git a/Bugzilla/DB/Oracle.pm b/Bugzilla/DB/Oracle.pm
index 6fa7a9869..a671a0e68 100644
--- a/Bugzilla/DB/Oracle.pm
+++ b/Bugzilla/DB/Oracle.pm
@@ -35,16 +35,14 @@ For interface details see L<Bugzilla::DB> and L<DBI>.
=cut
package Bugzilla::DB::Oracle;
-
use strict;
+use base qw(Bugzilla::DB);
use DBD::Oracle;
use DBD::Oracle qw(:ora_types);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
-# This module extends the DB interface via inheritance
-use base qw(Bugzilla::DB);
#####################################################################
# Constants
@@ -52,6 +50,7 @@ use base qw(Bugzilla::DB);
use constant EMPTY_STRING => '__BZ_EMPTY_STR__';
use constant ISOLATION_LEVEL => 'READ COMMITTED';
use constant BLOB_TYPE => { ora_type => ORA_BLOB };
+use constant FULLTEXT_OR => ' OR ';
sub new {
my ($class, $params) = @_;
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index aa01d93a5..6c7be2241 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -900,7 +900,7 @@ sub can_enter_product {
$product && grep($_->name eq $product->name,
@{ $self->get_enterable_products });
- return 1 if $can_enter;
+ return $product if $can_enter;
return 0 unless $warn == THROW_ERROR;
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index ee5591027..d5c1fab74 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -36,6 +36,7 @@ use Bugzilla::Util qw(trick_taint trim);
use Bugzilla::Version;
use Bugzilla::Milestone;
use Bugzilla::Status;
+use Bugzilla::Token qw(issue_hash_token);
#############
# Constants #
@@ -322,7 +323,7 @@ sub get {
else {
$bug = Bugzilla::Bug->check($bug_id);
}
- push(@bugs, $self->_bug_to_hash($bug));
+ push(@bugs, $self->_bug_to_hash($bug, $params));
}
return { bugs => \@bugs, faults => \@faults };
@@ -421,7 +422,28 @@ sub search {
my $bugs = Bugzilla::Bug->match($params);
my $visible = Bugzilla->user->visible_bugs($bugs);
- my @hashes = map { $self->_bug_to_hash($_) } @$visible;
+ my @hashes = map { $self->_bug_to_hash($_, $params) } @$visible;
+ return { bugs => \@hashes };
+}
+
+sub possible_duplicates {
+ my ($self, $params) = validate(@_, 'product');
+ my $user = Bugzilla->user;
+
+ # Undo the array-ification that validate() does, for "summary".
+ $params->{summary} || ThrowCodeError('param_required',
+ { function => 'Bug.possible_duplicates', param => 'summary' });
+
+ my @products;
+ foreach my $name (@{ $params->{'product'} || [] }) {
+ my $object = $user->can_enter_product($name, THROW_ERROR);
+ push(@products, $object);
+ }
+
+ my $possible_dupes = Bugzilla::Bug->possible_duplicates(
+ { summary => $params->{summary}, products => \@products,
+ limit => $params->{limit} });
+ my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes;
return { bugs => \@hashes };
}
@@ -617,7 +639,7 @@ sub attachments {
# A helper for get() and search().
sub _bug_to_hash {
- my ($self, $bug) = @_;
+ my ($self, $bug, $filters) = @_;
# Timetracking fields are deleted if the user doesn't belong to
# the corresponding group.
@@ -646,6 +668,11 @@ sub _bug_to_hash {
$item{'component'} = $self->type('string', $bug->component);
$item{'dupe_of'} = $self->type('int', $bug->dup_id);
+ if (Bugzilla->user->id) {
+ my $token = issue_hash_token([$bug->id, $bug->delta_ts]);
+ $item{'update_token'} = $self->type('string', $token);
+ }
+
# if we do not delete this key, additional user info, including their
# real name, etc, will wind up in the 'internals' hashref
delete $item{internals}->{assigned_to_obj};
@@ -659,7 +686,7 @@ sub _bug_to_hash {
$item{'alias'} = undef;
}
- return \%item;
+ return filter $filters, \%item;
}
sub _attachment_to_hash {
diff --git a/js/bug.js b/js/bug.js
new file mode 100644
index 000000000..8cee68efc
--- /dev/null
+++ b/js/bug.js
@@ -0,0 +1,117 @@
+/* The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * rights and limitations under the License.
+ *
+ * The Original Code is the Bugzilla Bug Tracking System.
+ *
+ * The Initial Developer of the Original Code is Everything Solved, Inc.
+ * Portions created by Everything Solved are Copyright (C) 2010 Everything
+ * Solved, Inc. All Rights Reserved.
+ *
+ * Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
+ */
+
+/* This library assumes that the needed YUI libraries have been loaded
+ already. */
+
+YAHOO.bugzilla.dupTable = {
+ counter: 0,
+ dataSource: null,
+ updateTable: function(dataTable, product_name, summary_field) {
+ if (summary_field.value.length < 4) return;
+
+ YAHOO.bugzilla.dupTable.counter = YAHOO.bugzilla.dupTable.counter + 1;
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ var json_object = {
+ version : "1.1",
+ method : "Bug.possible_duplicates",
+ id : YAHOO.bugzilla.dupTable.counter,
+ params : {
+ product : product_name,
+ summary : summary_field.value,
+ limit : 7,
+ include_fields : [ "id", "summary", "status", "resolution",
+ "update_token" ]
+ }
+ };
+ var post_data = YAHOO.lang.JSON.stringify(json_object);
+
+ var callback = {
+ success: dataTable.onDataReturnInitializeTable,
+ failure: dataTable.onDataReturnInitializeTable,
+ scope: dataTable,
+ argument: dataTable.getState()
+ };
+ dataTable.showTableMessage(dataTable.get("MSG_LOADING"),
+ YAHOO.widget.DataTable.CLASS_LOADING);
+ YAHOO.util.Dom.removeClass('possible_duplicates_container',
+ 'bz_default_hidden');
+ dataTable.getDataSource().sendRequest(post_data, callback);
+ },
+ formatBugLink: function(el, oRecord, oColumn, oData) {
+ el.innerHTML = '<a href="show_bug.cgi?id=' + oData + '">'
+ + oData + '</a>';
+ },
+ formatStatus: function(el, oRecord, oColumn, oData) {
+ var resolution = oRecord.getData('resolution');
+ if (resolution) {
+ el.innerHTML = oData + ' ' + resolution;
+ }
+ else {
+ el.innerHTML = oData;
+ }
+ },
+ formatCcButton: function(el, oRecord, oColumn, oData) {
+ var url = 'process_bug.cgi?id=' + oRecord.getData('id')
+ + '&addselfcc=1&token=' + escape(oData);
+ var button = document.createElement('button');
+ button.setAttribute('type', 'button');
+ button.innerHTML = YAHOO.bugzilla.dupTable.addCcMessage;
+ button.onclick = function() { window.location = url; return false; };
+ el.appendChild(button);
+ },
+ init_ds: function() {
+ var new_ds = new YAHOO.util.XHRDataSource("jsonrpc.cgi");
+ new_ds.connTimeout = 30000;
+ new_ds.connMethodPost = true;
+ new_ds.connXhrMode = "cancelStaleRequests";
+ new_ds.maxCacheEntries = 3;
+ new_ds.responseSchema = {
+ resultsList : "result.bugs",
+ metaFields : { error: "error", jsonRpcId: "id" },
+ };
+ // DataSource can't understand a JSON-RPC error response, so
+ // we have to modify the result data if we get one.
+ new_ds.doBeforeParseData =
+ function(oRequest, oFullResponse, oCallback) {
+ if (oFullResponse.error) {
+ oFullResponse.result = {};
+ oFullResponse.result.bugs = [];
+ if (console) {
+ console.log("JSON-RPC error:", oFullResponse.error);
+ }
+ }
+ return oFullResponse;
+ }
+
+ this.dataSource = new_ds;
+ },
+ init: function(data) {
+ if (this.dataSource == null) this.init_ds();
+ data.options.initialLoad = false;
+ var dt = new YAHOO.widget.DataTable(data.container, data.columns,
+ this.dataSource, data.options);
+ YAHOO.util.Event.on(data.summary_field, 'blur',
+ function(e) {
+ YAHOO.bugzilla.dupTable.updateTable(dt, data.product_name,
+ YAHOO.util.Event.getTarget(e))
+ }
+ );
+ },
+};
diff --git a/skins/standard/enter_bug.css b/skins/standard/enter_bug.css
new file mode 100644
index 000000000..2fd79baa4
--- /dev/null
+++ b/skins/standard/enter_bug.css
@@ -0,0 +1,65 @@
+/* The contents of this file are subject to the Mozilla Public
+ * License Version 1.1 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS
+ * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * rights and limitations under the License.
+ *
+ * The Original Code is the Bugzilla Bug Tracking System.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation. Portions created by Netscape are Copyright (C) 1998
+ * Netscape Communications Corporation. All Rights Reserved.
+ *
+ * Contributor(s): Byron Jones <bugzilla@glob.com.au>
+ * Christian Reis <kiko@async.com.br>
+ * Vitaly Harisov <vitaly@rathedg.com>
+ * Svetlana Harisova <light@rathedg.com>
+ * Marc Schumann <wurblzap@gmail.com>
+ * Pascal Held <paheld@gmail.com>
+ * Max Kanat-Alexander <mkanat@bugzilla.org>
+ */
+
+/* These are specified using the class instead of the id so that they
+ don't override the YUI CSS. */
+.enter_bug_form table {
+ border-spacing: 0;
+ border-width: 0;
+}
+.enter_bug_form td, .enter_bug_form th { padding: .25em; }
+.enter_bug_form th { text-align: right; }
+
+/* This makes the "component" column as small as possible (since it
+ * contains only fixed-width content) and the Reporter column
+ * as large as possible, which makes the form not jump around
+ * when the Component Description changes size. This works
+ * pretty well on all browsers except IE 8.
+ */
+#Create #field_container_component { width: 1px; }
+#Create #field_container_reporter { width: 100%; }
+
+#Create .comment {
+ vertical-align: top;
+ overflow: auto;
+ color: green;
+}
+#Create #comp_desc_container td { padding: 0; }
+#Create #comp_desc { height: 11ex; }
+#Create #os_guess_note {
+ padding-top: 0;
+}
+#Create #os_guess_note div {
+ max-width: 35em;
+}
+
+/* The Possible Duplicates table on enter_bug. */
+#possible_duplicates th {
+ text-align: center;
+ background: none;
+ border-collapse: collapse;
+}
+/* Make the Add Me to CC button never wrap. */
+#possible_duplicates .yui-dt-col-update_token { white-space: nowrap; }
diff --git a/skins/standard/global.css b/skins/standard/global.css
index 0f64fcab4..5cc71ef53 100644
--- a/skins/standard/global.css
+++ b/skins/standard/global.css
@@ -504,45 +504,6 @@ input.required, select.required, span.required_explanation {
list-style-type: none;
}
-/*************/
-/* enter_bug */
-/*************/
-
-form#Create table {
- border-spacing: 0;
- border-width: 0;
-}
-form#Create td, form#Create th {
- padding: .25em;
-}
-form#Create th {
- text-align: right;
-}
-
-/* This makes the "component" column as small as possible (since it
- * contains only fixed-width content) and the Reporter column
- * as large as possible, which makes the form not jump around
- * when the Component Description changes size. This works
- * pretty well on all browsers except IE 8.
- */
-form#Create #field_container_component { width: 1px; }
-form#Create #field_container_reporter { width: 100%; }
-
-form#Create .comment {
- vertical-align: top;
- overflow: auto;
- color: green;
-}
-form#Create #comp_desc_container td { padding: 0; }
-form#Create #comp_desc { height: 11ex; }
-form#Create #os_guess_note {
- padding-top: 0;
-}
-form#Create #os_guess_note div {
- max-width: 35em;
-}
-
-
.image_button {
background-repeat: no-repeat;
background-position: center center;
diff --git a/template/en/default/bug/create/create.html.tmpl b/template/en/default/bug/create/create.html.tmpl
index a59fe9112..0733de02a 100644
--- a/template/en/default/bug/create/create.html.tmpl
+++ b/template/en/default/bug/create/create.html.tmpl
@@ -30,10 +30,11 @@
[% PROCESS global/header.html.tmpl
title = title
- yui = [ 'autocomplete', 'calendar' ]
- style_urls = [ 'skins/standard/attachment.css' ]
+ yui = [ 'autocomplete', 'calendar', 'datatable' ]
+ style_urls = [ 'skins/standard/attachment.css',
+ 'skins/standard/enter_bug.css' ]
javascript_urls = [ "js/attachment.js", "js/util.js",
- "js/field.js", "js/TUI.js" ]
+ "js/field.js", "js/TUI.js", "js/bug.js" ]
onload = 'set_assign_to();'
%]
@@ -169,7 +170,7 @@ TUI_hide_default('expert_fields');
</script>
<form name="Create" id="Create" method="post" action="post_bug.cgi"
- enctype="multipart/form-data">
+ class="enter_bug_form" enctype="multipart/form-data">
<input type="hidden" name="product" value="[% product.name FILTER html %]">
<input type="hidden" name="token" value="[% token FILTER html %]">
@@ -508,13 +509,48 @@ TUI_hide_default('expert_fields');
<td colspan="3">
<input name="short_desc" size="70" value="[% short_desc FILTER html %]"
maxlength="255" spellcheck="true" aria-required="true"
- class="required">
+ class="required" id="short_desc">
</td>
</tr>
+ [% IF feature_enabled('jsonrpc') AND !cloned_bug_id %]
+ <tr id="possible_duplicates_container" class="bz_default_hidden">
+ <th>Possible<br>Duplicates:</th>
+ <td colspan="3">
+ <div id="possible_duplicates"></div>
+ <script type="text/javascript">
+ var dt_columns = [
+ { key: "id", label: "[% field_descs.bug_id FILTER js %]",
+ formatter: YAHOO.bugzilla.dupTable.formatBugLink },
+ { key: "summary",
+ label: "[% field_descs.short_desc FILTER js %]" },
+ { key: "status",
+ label: "[% field_descs.bug_status FILTER js %]",
+ formatter: YAHOO.bugzilla.dupTable.formatStatus },
+ { key: "update_token", label: '',
+ formatter: YAHOO.bugzilla.dupTable.formatCcButton }
+ ];
+ YAHOO.bugzilla.dupTable.addCcMessage = "Add Me to the CC List";
+ YAHOO.bugzilla.dupTable.init({
+ container: 'possible_duplicates',
+ columns: dt_columns,
+ product_name: '[% product.name FILTER js %]',
+ summary_field: 'short_desc',
+ options: {
+ MSG_LOADING: 'Searching for possible duplicates...',
+ MSG_EMPTY: 'No possible duplicates found.',
+ SUMMARY: 'Possible Duplicates'
+ },
+ });
+ </script>
+ </td>
+ </tr>
+ [% END %]
+
<tr>
<th>Description:</th>
<td colspan="3">
+
[% defaultcontent = BLOCK %]
[% IF cloned_bug_id %]
+++ This [% terms.bug %] was initially created as a clone of [% terms.Bug %] #[% cloned_bug_id %] +++
diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl
index 3d7fc2e68..75fa71825 100644
--- a/template/en/default/global/header.html.tmpl
+++ b/template/en/default/global/header.html.tmpl
@@ -52,6 +52,7 @@
[% SET yui_css = {
autocomplete => 1,
calendar => 1,
+ datatable => 1,
} %]
[%# Note: This is simple dependency resolution--you can't have dependencies
@@ -60,6 +61,7 @@
#%]
[% SET yui_deps = {
autocomplete => ['json', 'connection', 'datasource'],
+ datatable => ['json', 'connection', 'datasource', 'element'],
} %]
@@ -99,7 +101,20 @@
[% END %]
[% style_urls.unshift('skins/standard/global.css') %]
+ [%# YUI dependency resolution %]
+ [%# We have to do this in a separate array, because modifying the
+ # existing array by unshift'ing dependencies confuses FOREACH.
+ #%]
+ [% SET yui_resolved = [] %]
+ [% FOREACH yui_name = yui %]
+ [% FOREACH yui_dep = yui_deps.${yui_name}.reverse %]
+ [% yui_resolved.push(yui_dep) IF NOT yui_resolved.contains(yui_dep) %]
+ [% END %]
+ [% yui_resolved.push(yui_name) IF NOT yui_resolved.contains(yui_name) %]
+ [% END %]
+ [% SET yui = yui_resolved %]
+ [%# YUI CSS %]
[% FOREACH yui_name = yui %]
[% IF yui_css.$yui_name %]
<link rel="stylesheet" type="text/css"
@@ -218,12 +233,6 @@
<script src="js/yui/yahoo-dom-event/yahoo-dom-event.js"
type="text/javascript"></script>
<script src="js/yui/cookie/cookie-min.js" type="text/javascript"></script>
- [%# Resolve YUI dependencies. Note that CSS was already done above. %]
- [% FOREACH yui_name = yui %]
- [% IF yui_deps.$yui_name %]
- [% yui = yui_deps.${yui_name}.merge(yui) %]
- [% END %]
- [% END %]
[% FOREACH yui_name = yui %]
<script type="text/javascript"
src="js/yui/[% yui_name FILTER html %]/