]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 251556: Allow "Bug ID" fields to have one-way mutual relationships (like blocks...
authorJesse Clark <jjclark1982@gmail.com>
Mon, 8 Feb 2010 00:10:03 +0000 (16:10 -0800)
committerMax Kanat-Alexander <mkanat@bugzilla.org>
Mon, 8 Feb 2010 00:10:03 +0000 (16:10 -0800)
r=mkanat, a=mkanat

14 files changed:
.bzrignore
Bugzilla/Bug.pm
Bugzilla/DB/Schema.pm
Bugzilla/Field.pm
Bugzilla/Install/DB.pm
editfields.cgi
skins/standard/admin.css
template/en/default/admin/custom_fields/cf-js.js.tmpl
template/en/default/admin/custom_fields/create.html.tmpl
template/en/default/admin/custom_fields/edit.html.tmpl
template/en/default/bug/edit.html.tmpl
template/en/default/bug/field.html.tmpl
template/en/default/bug/show-multiple.html.tmpl
template/en/default/global/user-error.html.tmpl

index e42398ecc9d342b5147f9e825db13c610e09cc41..8415e356ba6fb2438232b0e85a91a609b57c3a89 100644 (file)
@@ -29,3 +29,4 @@
 /skins/contrib/Dusk/summarize-time.css
 /skins/contrib/Dusk/voting.css
 /skins/contrib/Dusk/yui
+.DS_Store
index fd28b5b82c2324896d4f8f4c16d24a2b9de26f5f..1e418aa18b4a3f5ce047abcbb89ac62ffae381fa 100644 (file)
@@ -25,6 +25,7 @@
 #                 Max Kanat-Alexander <mkanat@bugzilla.org>
 #                 Frédéric Buclin <LpSolit@gmail.com>
 #                 Lance Larsh <lance.larsh@oracle.com>
+#                 Elliotte Martin <elliotte_martin@yahoo.com>
 
 package Bugzilla::Bug;
 
@@ -1761,7 +1762,42 @@ sub _check_select_field {
 sub _check_bugid_field {
     my ($invocant, $value, $field) = @_;
     return undef if !$value;
-    return $invocant->check($value, $field)->id;
+    
+    # check that the value is a valid, visible bug id
+    my $checked_id = $invocant->check($value, $field)->id;
+    
+    # check for loop (can't have a loop if this is a new bug)
+    if (ref $invocant) {
+        _check_relationship_loop($field, $invocant->bug_id, $checked_id);
+    }
+
+    return $checked_id;
+}
+
+sub _check_relationship_loop {
+    # Generates a dependency tree for a given bug.  Calls itself recursively
+    # to generate sub-trees for the bug's dependencies.
+    my ($field, $bug_id, $dep_id, $ids) = @_;
+
+    # Don't do anything if this bug doesn't have any dependencies.
+    return unless defined($dep_id);
+
+    # Check whether we have seen this bug yet
+    $ids = {} unless defined $ids;
+    $ids->{$bug_id} = 1;
+    if ($ids->{$dep_id}) {
+        ThrowUserError("relationship_loop_single", {
+            'bug_id' => $bug_id,
+            'dep_id' => $dep_id,
+            'field_name' => $field});
+    }
+    
+    # Get this dependency's record from the database
+    my $dbh = Bugzilla->dbh;
+    my $next_dep_id = $dbh->selectrow_array(
+        "SELECT $field FROM bugs WHERE bug_id = ?", undef, $dep_id);
+
+    _check_relationship_loop($field, $dep_id, $next_dep_id, $ids);
 }
 
 #####################################################################
@@ -2553,6 +2589,15 @@ sub blocked {
 # Even bugs in an error state always have a bug_id.
 sub bug_id { $_[0]->{'bug_id'}; }
 
+sub related_bugs {
+    my ($self, $relationship) = @_;
+    return [] if $self->{'error'};
+
+    my $field_name = $relationship->name;
+    $self->{'related_bugs'}->{$field_name} ||= $self->match({$field_name => $self->id});
+    return $self->{'related_bugs'}->{$field_name}; 
+}
+
 sub cc {
     my ($self) = @_;
     return $self->{'cc'} if exists $self->{'cc'};
index 8bee5dfe13d043e007e912573d3e7d71e9b0ea49..44d224d5798469a11164b4b5984bc0b529e230ec 100644 (file)
@@ -693,6 +693,7 @@ use constant ABSTRACT_SCHEMA => {
             value_field_id => {TYPE => 'INT3',
                                REFERENCES => {TABLE  => 'fielddefs',
                                               COLUMN => 'id'}},
+            reverse_desc => {TYPE => 'TINYTEXT'},
         ],
         INDEXES => [
             fielddefs_name_idx    => {FIELDS => ['name'],
index 2f14037abc5f1720268875c09d0107aecaedb5c6..17e4194c2a648411306e74cd3ea1dc240d17d094 100644 (file)
@@ -101,6 +101,7 @@ use constant DB_COLUMNS => qw(
     visibility_field_id
     visibility_value_id
     value_field_id
+    reverse_desc
 );
 
 use constant REQUIRED_CREATE_FIELDS => qw(name description);
@@ -120,6 +121,7 @@ use constant VALIDATORS => {
 use constant UPDATE_VALIDATORS => {
     value_field_id      => \&_check_value_field_id,
     visibility_value_id => \&_check_control_value,
+    reverse_desc        => \&_check_reverse_desc,
 };
 
 use constant UPDATE_COLUMNS => qw(
@@ -132,6 +134,7 @@ use constant UPDATE_COLUMNS => qw(
     visibility_field_id
     visibility_value_id
     value_field_id
+    reverse_desc
 
     type
 );
@@ -357,6 +360,22 @@ sub _check_control_value {
     return $value_obj->id;
 }
 
+sub _check_reverse_desc {
+    my ($invocant, $reverse_desc, $type) = @_;
+    
+    if (blessed $invocant) {
+        $type = $invocant->type;
+    }
+    
+    if ($type != FIELD_TYPE_BUG_ID) {
+        return undef; # store NULL for non-reversible field types
+    }
+    
+    $reverse_desc = clean_text($reverse_desc);
+    return $reverse_desc;
+}
+
+
 =pod
 
 =head2 Instance Properties
@@ -637,6 +656,44 @@ sub controls_values_of {
     return $self->{controls_values_of};
 }
 
+=over
+
+=item C<is_relationship>
+
+Applies only to fields of type FIELD_TYPE_BUG_ID.
+Checks to see if a reverse relationship description has been set.
+This is the canonical condition to enable reverse link display,
+dependency tree display, and similar functionality.
+
+=back
+
+=cut
+
+sub is_relationship  {     
+    my $self = shift;
+    my $desc = $self->reverse_desc;
+    if (defined $desc && $desc ne "") {
+        return 1;
+    }
+    return 0;
+}
+
+=over
+
+=item C<reverse_desc>
+
+Applies only to fields of type FIELD_TYPE_BUG_ID.
+Describes the reverse relationship of this field.
+For example, if a BUG_ID field is called "Is a duplicate of",
+the reverse description would be "Duplicates of this bug".
+
+=back
+
+=cut
+
+sub reverse_desc { return $_[0]->{reverse_desc} }
+
+
 =pod
 
 =head2 Instance Mutators
@@ -661,6 +718,8 @@ They will throw an error if you try to set the values to something invalid.
 
 =item C<set_buglist>
 
+=item C<set_reverse_desc>
+
 =item C<set_visibility_field>
 
 =item C<set_visibility_value>
@@ -677,6 +736,7 @@ sub set_obsolete       { $_[0]->set('obsolete',    $_[1]); }
 sub set_sortkey        { $_[0]->set('sortkey',     $_[1]); }
 sub set_in_new_bugmail { $_[0]->set('mailhead',    $_[1]); }
 sub set_buglist        { $_[0]->set('buglist',     $_[1]); }
+sub set_reverse_desc    { $_[0]->set('reverse_desc', $_[1]); }
 sub set_visibility_field {
     my ($self, $value) = @_;
     $self->set('visibility_field_id', $value);
@@ -868,6 +928,12 @@ sub run_create_validators {
         $class->_check_value_field_id($params->{value_field_id},
             ($type == FIELD_TYPE_SINGLE_SELECT 
              || $type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0);
+
+    $params->{reverse_desc} = $class->_check_reverse_desc(
+        $params->{reverse_desc}, $type);
+
+
+
     return $params;
 }
 
index f6d6edcb1dc9f397481d407ff381370fd91a7b36..d150a4e9b994f05ad91652c2ba77a59d8cef71e8 100644 (file)
@@ -102,6 +102,9 @@ sub update_fielddefs_definition {
         $dbh->do('UPDATE fielddefs SET buglist = 1 WHERE custom = 1 AND type != ' . FIELD_TYPE_MULTI_SELECT);
     }
 
+    #2008-08-26 elliotte_martin@yahoo.com - Bug 251556
+    $dbh->bz_add_column('fielddefs', 'reverse_desc', {TYPE => 'TINYTEXT'});
+
 
     # Remember, this is not the function for adding general table changes.
     # That is below. Add new changes to the fielddefs table above this
index 09147d69ab6b0d289ca93c525648603468de0f77..4e8733752db35cefea371e8f88d9934b245723ee 100755 (executable)
@@ -68,6 +68,7 @@ elsif ($action eq 'new') {
         visibility_field_id => scalar $cgi->param('visibility_field_id'),
         visibility_value_id => scalar $cgi->param('visibility_value_id'),
         value_field_id => scalar $cgi->param('value_field_id'),
+        reverse_desc => scalar $cgi->param('reverse_desc'),
     });
 
     delete_token($token);
@@ -113,6 +114,7 @@ elsif ($action eq 'update') {
     $field->set_visibility_field($cgi->param('visibility_field_id'));
     $field->set_visibility_value($cgi->param('visibility_value_id'));
     $field->set_value_field($cgi->param('value_field_id'));
+    $field->set_reverse_desc($cgi->param('reverse_desc'));
     $field->update();
 
     delete_token($token);
index 184c4961af513cbb10c124892a7fda927accc9ed..6b330ef6d4ae45a63dc676e97d15a6a34c03e5ae 100644 (file)
@@ -113,3 +113,15 @@ th.title {
     text-align: center;
     vertical-align: middle;
 }
+
+#edit_custom_field tr {
+    vertical-align: top;
+}
+
+#edit_custom_field th {
+    text-align: right;
+}
+
+#edit_custom_field th.narrow_label {
+    white-space: normal;
+}
index 778dd3373b951a9142ce061933938eb5441b1fe3..4c95a169087bf72788dd258904b116b153662439 100644 (file)
@@ -50,6 +50,16 @@ function onChangeType(type_field) {
     else {
         value_field.disabled = true;
     }
+
+   var reverse_desc = document.getElementById('reverse_desc');
+   if (type_field.value == [% constants.FIELD_TYPE_BUG_ID %])
+   {
+       reverse_desc.disabled = false;
+   }
+   else {
+       reverse_desc.disabled = true;
+       reverse_desc.value = '';
+   }
 }
 
 function onChangeVisibilityField() {
index a2db4708be807e6c1fab2776a6aa56515f5b8988..d23be0fe2ad59eaf0464663ca62a29250b1dc782 100644 (file)
            onload = "document.getElementById('new_bugmail').disabled = true;"
            javascript_urls = [ 'js/util.js' ]
            doc_section = "custom-fields.html#add-custom-fields"
+           style_urls = ['skins/standard/admin.css']
 %]
 
+[%# set initial editability of fields such as Reverse Relationship Description %]
+<script type="text/javascript">
+YAHOO.util.Event.onDOMReady(function() {onChangeType(document.getElementById('type'))});
+</script>
+
 <p>
   Adding custom fields can make the interface of [% terms.Bugzilla %] very
   complicated. Many admins who are new to [% terms.Bugzilla %] start off
 </ul>
 
 <form id="add_field" action="editfields.cgi" method="GET">
-  <table border="0" cellspacing="0" cellpadding="5">
+  <table border="0" cellspacing="0" cellpadding="5" id="edit_custom_field">
     <tr>
-      <th align="right"><label for="name">Name:</label></th>
+      <th class="narrow_label"><label for="name">Name:</label></th>
       <td>
         <input type="text" id="name" name="name" value="cf_" size="40" maxlength="64">
       </td>
 
-      <th align="right">
+      <th>
         <label for="enter_bug">Can be set on [% terms.bug %] creation:</label>
       </th>
       <td>
       </td>
     </tr>
     <tr>
-      <th align="right"><label for="desc">Description:</label></th>
+      <th class="narrow_label"><label for="desc">Description:</label></th>
       <td><input type="text" id="desc" name="desc" value="" size="40"></td>
 
-      <th align="right">
+      <th>
         <label for="new_bugmail">Displayed in [% terms.bug %]mail for new [% terms.bugs %]:</label>
       </th>
       <td><input type="checkbox" id="new_bugmail" name="new_bugmail" value="1"></td>
     </tr>
     <tr>
-      <th align="right"><label for="type">Type:</label></th>
+      <th class="narrow_label"><label for="type">Type:</label></th>
       <td>
         <select id="type" name="type" onchange="onChangeType(this)">
           [% FOREACH type = field_types.keys %]
         </select>
       </td>
 
-      <th align="right"><label for="obsolete">Is obsolete:</label></th>
+      <th><label for="obsolete">Is obsolete:</label></th>
       <td><input type="checkbox" id="obsolete" name="obsolete" value="1"></td>
     </tr>
     <tr>
-      <th align="right"><label for="sortkey">Sortkey:</label></th>
+      <th class="narrow_label"><label for="sortkey">Sortkey:</label></th>
       <td>
         <input type="text" id="sortkey" name="sortkey" size="6" maxlength="6">
       </td>
 
-      <th align="right">
+      <th>
         <label for="visibility_field_id">Field only appears when:</label>
       </th>
       <td>
     </tr>
 
     <tr>
-      <td colspan="2">&nbsp;</td>
+      <th class="narrow_label">
+        <label for="reverse_desc">Reverse Relationship Description:</label>
+      </th>
+      <td>
+        <input type="text" id="reverse_desc" name="reverse_desc" value="" size="40" disabled="disabled">
+        <br/>
+        Use this label for the list of [% terms.bugs %] that link to a [% terms.bug %]
+        with this [% field_types.${constants.FIELD_TYPE_BUG_ID} %] field.
+        For example, if the description is "Is a duplicate of",
+        the reverse description would be "Duplicates of this [% terms.bug %]".
+        Leave blank to disable the list for this field.
+      </td>
       <th>
         <label for="value_field_id">
           Field that controls the values<br>
index 981668260f142ce787c5ac342a34cb2127905f06..01a8dcccbf33e9bcf6509aa69ad2c93a1387faa2 100644 (file)
@@ -32,6 +32,7 @@
            onload = "toggleCheckbox(document.getElementById('enter_bug'), 'new_bugmail');"
            javascript_urls = [ 'js/util.js' ]
            doc_section = "custom-fields.html#edit-custom-fields"
+           style_urls = ['skins/standard/admin.css']
 %]
 
 <p>
 </p>
 
 <form id="edit_field" action="editfields.cgi" method="GET">
-  <table border="0" cellspacing="0" cellpadding="5">
+  <table border="0" cellspacing="0" cellpadding="5" id="edit_custom_field">
     <tr>
-      <th align="right">Name:</th>
+      <th class="narrow_label">Name:</th>
       <td>[% field.name FILTER html %]</td>
 
-      <th align="right">
+      <th>
         <label for="enter_bug">Can be set on [% terms.bug %] creation:</label>
       </th>
       <td><input type="checkbox" id="enter_bug" name="enter_bug" value="1"
                  onchange="toggleCheckbox(this, 'new_bugmail');"></td>
     </tr>
     <tr>
-      <th align="right"><label for="desc">Description:</label></th>
+      <th class="narrow_label"><label for="desc">Description:</label></th>
       <td><input type="text" id="desc" name="desc" size="40"
                  value="[% field.description FILTER html %]"></td>
 
-      <th align="right">
+      <th>
         <label for="new_bugmail">Displayed in [% terms.bug %]mail for new [% terms.bugs %]:</label>
       </th>
       <td><input type="checkbox" id="new_bugmail" name="new_bugmail" value="1"
                  [%- " checked" IF field.mailhead %]></td>
     </tr>
     <tr>
-      <th align="right">Type:</th>
+      <th class="narrow_label">Type:</th>
       <td>[% field_types.${field.type} FILTER html %]</td>
 
-      <th align="right"><label for="obsolete">Is obsolete:</label></th>
+      <th><label for="obsolete">Is obsolete:</label></th>
       <td><input type="checkbox" id="obsolete" name="obsolete" value="1"
                  [%- " checked" IF field.obsolete %]></td>
     </tr>
     <tr>
-      <th align="right"><label for="sortkey">Sortkey:</label></th>
+      <th class="narrow_label"><label for="sortkey">Sortkey:</label></th>
       <td>
         <input type="text" id="sortkey" name="sortkey" size="6" maxlength="6"
                value="[% field.sortkey FILTER html %]">
       </td>
-      <th align="right">
+      <th>
         <label for="visibility_field_id">Field only appears when:</label>
       </th>
       <td>
         </td>
       </tr>
     [% END %]
+    [% IF field.type == constants.FIELD_TYPE_BUG_ID %]
+       <tr>
+        <th class="narrow_label">
+          <label for="reverse_desc">Reverse Relationship Description:</label>
+        </th>
+        <td>
+          <input type="text" id="reverse_desc" name="reverse_desc" size="40"
+                 value="[% field.reverse_desc FILTER html %]">
+          <br/>
+          Use this label for the list of [% terms.bugs %] that link to a [% terms.bug %]
+          with this [% field_types.${constants.FIELD_TYPE_BUG_ID} %] field.
+          For example, if the description is "Is a duplicate of",
+          the reverse description would be "Duplicates of this [% terms.bug %]".
+          Leave blank to disable the list for this field.
+        </td>
+       </tr>
+    [% END %]
   </table>
   <br>
   <input type="hidden" name="action" value="update">
index 63b81d733789837a24da2455edc40a317c1c06dc..1a35df916a0c1887dab8dff8487dad29a47a77b4 100644 (file)
   [% USE Bugzilla %]
   [% FOREACH field = Bugzilla.active_custom_fields %]
     <tr>
-      [% PROCESS bug/field.html.tmpl value=bug.${field.name}
+      [% PROCESS bug/field.html.tmpl value = bug.${field.name}
                                      editable = bug.check_can_change_field(field.name, 0, 1)
                                      value_span = 2 %]
     </tr>
+    [% IF extra_field_item %]
+      <tr>
+        <th class="field_label">[% extra_field_item.header FILTER none %]</th>
+        <td>[% extra_field_item.data FILTER none %]</td>
+      </tr>
+    [% END %]
   [% END %]
 [% END %]
 
index ac62bf7ba4730c604d3659dcccf84ce3f47ef362..482f88219824aa058cc122eb1e947c555b1a2246 100644 (file)
@@ -99,8 +99,8 @@
                  value="[% value FILTER html %]" size="7">
         </span>
 
-        [% IF bug.${field.name} %]  
-          [% bug.${field.name} FILTER bug_link(bug.${field.name}) FILTER none %]
+        [% IF value %]  
+          [% value FILTER bug_link(value, use_alias => 1) FILTER none %]
         [% END %]
         <span id="[% field.name FILTER html %]_edit_container" class="edit_me bz_default_hidden">
           (<a href="#" id="[% field.name FILTER html %]_edit_action">edit</a>)
                           '[% field.name FILTER js %]_input_area',
                           '[% field.name FILTER js %]_edit_action',
                           '[% field.name FILTER js %]',
-                          "[% bug.${field.name} FILTER js %]");
+                          "[% value FILTER js %]");
         </script>
     [% CASE [ constants.FIELD_TYPE_SINGLE_SELECT 
               constants.FIELD_TYPE_MULTI_SELECT ] %]
   <div class="uneditable_textarea">[% value FILTER wrap_comment(60)
                                             FILTER html %]</div>
 [% ELSIF field.type == constants.FIELD_TYPE_BUG_ID %]
-    [% IF bug.${field.name} %]  
-        [% bug.${field.name} FILTER bug_link(bug.${field.name}) FILTER none %]
+    [% IF value %]  
+        [% value FILTER bug_link(value, use_alias => 1) FILTER none %]
     [% END %]
 [% ELSE %]
   [% value.join(', ') FILTER html %]
 [% END %]
 [% Hook.process('end_field_column') %]
 [% '</td>' IF NOT no_tds %]
+
+[%# for reverse relationships, we show this pseudo-field after the main field %]
+[% IF bug.id && field.is_relationship %]
+    [% extra_field_item = {} %]
+    [% extra_field_item.header = field.reverse_desc _ ":" FILTER html %]
+    [% extra_field_item.data = BLOCK %]
+        [% FOREACH depbug = bug.related_bugs(field) %]
+            [% depbug.id FILTER bug_link(depbug, use_alias => 1) FILTER none %][% " " %]
+        [% END %]
+    [% END %]
+[% ELSE %]
+    [% extra_field_item = '' %]
+[% END %]
index 177bea14f0c66ce6dddf864f2b65cbbfcfdc8616..0949ffc2069490bbd307cd6c42da73d583372a34 100644 (file)
         [% PROCESS bug/field.html.tmpl value=bug.${field.name} editable=0 %]
         [%# Even-numbered fields get a closing <tr> %]
         [% '</tr>' IF !(field_counter % 2) %]
+        [% IF extra_field_item %]
+          [% field_counter = field_counter + 1 %]
+          [% '<tr>' IF field_counter % 2 %]
+          <th>[% extra_field_item.header FILTER none %]</th>
+          <td>[% extra_field_item.data FILTER none %]</td>
+          [% '</tr>' IF !(field_counter % 2) %]
+        [% END %]
     [% END %]
     [%# And we have to finish the row if we ended on an odd number. %]
     [% '<th></th><td></td></tr>' IF field_counter % 2 %]
index eca7ef058de8be5f34d57abc77ce1f4c1ab1b14a..bdedc41729c651c687b3a16fc29676a89afb5fe4 100644 (file)
     To file this [% terms.bug %], you must first choose a component.
     If necessary, just guess.
 
+  [% ELSIF error == "relationship_loop_single" %]
+    [% title = "Relationship Loop Detected" %]
+    [% field_descs.$field_name FILTER html %]
+    for [% terms.bug %] [%+ bug_id FILTER html %]
+    has a circular dependency on [% terms.bug %] [%+ dep_id FILTER html %].
+
   [% ELSIF error == "require_new_password" %]
     [% title = "New Password Needed" %]
     You cannot change your password without choosing a new one.