[Rt-commit] rt branch, 4.2-trunk, updated. rt-4.2.0-137-g0c8855f

Alex Vandiver alexmv at bestpractical.com
Tue Nov 12 17:06:39 EST 2013


The branch, 4.2-trunk has been updated
       via  0c8855f47933531c7743f479105c37b28fae8027 (commit)
       via  c0ddb0a3a5c93505c1bf473233c6b11945af6bee (commit)
       via  234bd3fa1570ea424dfd55ef0ee8540f9b1315d5 (commit)
       via  86d1b740610437556e99dc70d2d76f5879599dc7 (commit)
       via  df28251b999943063ed9d8357310118c19180663 (commit)
       via  1b8eddbb21b304b7a2f98449e2671334307053ea (commit)
       via  47396796a467719ef74a80a415e2d47be49fff40 (commit)
       via  abdc9f83b5b512a818287559955b19ad238e3555 (commit)
       via  4b7842dc0297c37b8ed9b986cfa90e1f4c504f6e (commit)
       via  10df0c99667b358ddc08489d37ae716abb4223b4 (commit)
       via  a79bd50d0658a5be314443fb13ab0354d6d206c7 (commit)
       via  a401cec82c43f01a418411e8addfd19acf384942 (commit)
       via  84b2b8fa39cdb87b0680fc4fd8ab8374aab33c07 (commit)
       via  11b8e03b08e0e27642987f9e0f3cc5a65b522dd8 (commit)
       via  06e6e90ea9c434cda0d2f16ddae94d019c6dc2ae (commit)
       via  f8512519102264bdd5a049a8b1523bffeec14824 (commit)
       via  996420dbc41bf27528ad3af55d1398c231ba2b8e (commit)
       via  11b0efca83aa408bbf6cf631521dd34dd3f1bcec (commit)
       via  9e3fd223173f3994fac3da62fefb985e613d7d61 (commit)
       via  94f99e840766e929be7cb231c0c54e23b2647919 (commit)
       via  b97865dd18f434585964403ac6c099a1109cae56 (commit)
       via  4b53069ee84f61201dafe2269c8e3d8a78f90b65 (commit)
       via  6dcfa73702f253a1a8a61e60a5d1447694c5269f (commit)
       via  a6e417fee855433ffc4e6ce5f1ce176668a4b0fa (commit)
       via  5c37f5100210ef6f103d9feb361ea4a6fdcfc956 (commit)
       via  fe3cdc3a8644a3de6c566b305781cf49feded6e4 (commit)
       via  e5f431120ccadce6831f305843e3ee219e0127de (commit)
       via  15dd4e79edfb3b840df8dc2d77dab97012ee63c8 (commit)
       via  043b314e189a0d12355c130a0182d0684dfbbbad (commit)
       via  485069c86569a9f59eefe21f27639e08db7bbddd (commit)
       via  310bd0799f3c94bd0e0960e8467c4ce238e10e74 (commit)
       via  af6cd52f4da30251eae554d69915d1581acb12bb (commit)
       via  a30cd04a70e3a1448ccdc49e32cd4b0dff242755 (commit)
       via  285297ae0a837a93f39c8612114040efdcf71b78 (commit)
       via  7ea8cefb2b7707301b77256ed59a17661fdbbc45 (commit)
       via  e4579a869f0034f4907a9439cf443a558ab1a7eb (commit)
       via  27a20661ed556a8b85e7bb90d383225dbfeee327 (commit)
       via  1940ebaf4e50eafc20c2bd149d63a627bfe80ecf (commit)
       via  167e933fc003c4ea8fbb14a824ca008540c88576 (commit)
       via  6f3570c568dc52bbee3dadd0d5f656704e6dd273 (commit)
       via  7768ad3990db4fbd7f4f2df305f4e001492fb12c (commit)
       via  4ef418698d79bdbd2e0580c5b1dabfd27ac6003e (commit)
       via  1727ca14c26b8a1ee92c8d39711ae2fb005833d0 (commit)
       via  d7061cd16374efb65fe6f2d39742e2e5c78a26ed (commit)
       via  b1586549149d86e718a1a7406752c830aa93b587 (commit)
       via  348fb134aabdc6dfbac6539c43123946cdbad031 (commit)
       via  b9cb82bf28b1e5e9b4a22c772bf619a824de345f (commit)
       via  e775cc6ca3824f57a2053811ccf1fbfb68e1099b (commit)
       via  d469cacc105aeb21999aaee151253485d9f48e99 (commit)
       via  a87a48ac1a24b71b9b87041dc7aebc755ce7cd44 (commit)
       via  dc1ea730d5c365ca80524213953eb131fc209039 (commit)
       via  36a824fd54afc3e2de3f7eaf55ee40f90fb0f0ba (commit)
       via  f87a39ce4b4e3824f4cc3730751aa539295e63f6 (commit)
       via  949af6145b51a4d58e407eb18f7122898e59846d (commit)
       via  b690c840eccdbfd9213af31c3374393c4a41281c (commit)
       via  9c767507c42adf806da4925c8f28412d0a748c08 (commit)
       via  3a1c5348d72842b7cb6ed3f72c5243a2cbbcde78 (commit)
       via  0650ceb883ccbc12e3661204319a9fc09922f7df (commit)
       via  da424fccbf3f3ca2c24291a6f201f46ba66708bd (commit)
       via  db3e18a3c44abc2dabd435c349b4a279b4a8f294 (commit)
       via  baa9833ad095f89035d9ae9bf58f9b22dcffda60 (commit)
       via  2573218dda5a5cf426e46264ac34e0ed927fed3b (commit)
       via  88ed6d1a3af5ce346a86f8a6721c00317bb59251 (commit)
       via  1fcfcd5a9af2dd4ff4a214b02fd79f0a3b03805b (commit)
       via  a562763b413fb4781cb4a674163fbe881945d033 (commit)
       via  2292483dc2750ba41bd49e1bed3cbc6de525ff80 (commit)
      from  8b208ca95a46989ac09ddb15d21f9adf3fe76c5e (commit)

Summary of changes:
 .gitignore                                         |   1 -
 configure.ac                                       |   1 -
 docs/UPGRADING-3.8                                 |   6 +-
 docs/automating_rt.pod                             | 234 +++++++++++++++++++++
 etc/RT_Config.pm.in                                |   2 +-
 etc/upgrade/3.8-branded-queues-extension.in        |  95 ---------
 etc/upgrade/{4.2.1 => 4.0.18}/content              |   0
 lib/RT/CustomField.pm                              |   5 -
 lib/RT/Date.pm                                     |  31 +--
 lib/RT/Interface/Web.pm                            |   3 +-
 lib/RT/Template.pm                                 |   8 +-
 lib/RT/Test.pm                                     |   4 +-
 lib/RT/Transaction.pm                              |   3 +-
 sbin/rt-dump-metadata.in                           | 191 +++++++++++++----
 share/html/Admin/Elements/EditRightsCategoryTabs   |  12 +-
 .../Admin/Elements/SelectCustomFieldRenderType     |   5 -
 share/html/Articles/Article/ExtractIntoClass.html  |   2 +-
 share/html/Elements/CollectionAsTable/ParseFormat  |   1 +
 share/html/Elements/EditCustomFieldSelect          |  58 +++--
 share/html/Elements/HeaderJavascript               |   2 +-
 share/html/Elements/MessageBox                     |   3 +
 share/html/Elements/SelectCustomFieldValue         |  13 ++
 share/html/Install/DatabaseDetails.html            |  12 +-
 share/html/Search/Elements/BuildFormatString       |  37 ++--
 share/html/SelfService/Display.html                |  10 +
 share/html/Ticket/Elements/EditBasics              |   3 +-
 share/html/Ticket/Elements/ShowSummary             |   4 +-
 share/html/Ticket/Elements/ShowUpdateStatus        |   6 +-
 share/html/Ticket/Update.html                      |   9 +
 share/static/js/cascaded.js                        |  31 ++-
 share/static/js/event-registration.js              |   2 +-
 share/static/js/util.js                            |  21 +-
 t/api/date.t                                       |  24 +--
 t/api/group.t                                      |  22 +-
 t/api/rights_show_ticket.t                         |   1 -
 t/fts/indexed_oracle.t                             |   2 +-
 t/web/helpers-http-cache-headers.t                 |   6 +-
 t/web/installer.t                                  |   1 +
 t/web/self_service.t                               |  43 +++-
 t/web/user_update.t                                |   2 +-
 40 files changed, 635 insertions(+), 281 deletions(-)
 create mode 100644 docs/automating_rt.pod
 delete mode 100755 etc/upgrade/3.8-branded-queues-extension.in
 copy etc/upgrade/{4.2.1 => 4.0.18}/content (100%)

- Log -----------------------------------------------------------------
commit 0c8855f47933531c7743f479105c37b28fae8027
Merge: 8b208ca c0ddb0a
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Nov 12 16:13:23 2013 -0500

    Merge branch '4.0-trunk' into 4.2-trunk
    
    Conflicts:
    	lib/RT/Date.pm
    	lib/RT/Template.pm
    	lib/RT/Test.pm
    	lib/RT/Tickets_SQL.pm
    	lib/RT/Transaction.pm
    	sbin/rt-dump-metadata.in
    	share/html/Elements/EditCustomFieldSelect
    	share/html/Elements/SelectCustomFieldValue
    	share/html/SelfService/Display.html
    	share/html/Ticket/Elements/ShowSummary
    	share/static/js/cascaded.js
    	t/web/user_update.t

diff --cc etc/RT_Config.pm.in
index c74e5b6,62563aa..5ec562c
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@@ -1296,10 -1202,10 +1296,10 @@@ Set ($DefaultSearchResultFormat, qq
     '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a></B>/TITLE:Subject',
     Status,
     QueueName,
 -   OwnerName,
 +   Owner,
     Priority,
     '__NEWLINE__',
-    '',
+    '__NBSP__',
     '<small>__Requestors__</small>',
     '<small>__CreatedRelative__</small>',
     '<small>__ToldRelative__</small>',
diff --cc etc/upgrade/4.0.18/content
index 0000000,5a3c864..64eea9a
mode 000000,100644..100644
--- a/etc/upgrade/4.0.18/content
+++ b/etc/upgrade/4.0.18/content
@@@ -1,0 -1,11 +1,14 @@@
 - at Initial = (
++use strict;
++use warnings;
++
++our @Initial = (
+     sub {
+         my $attr = RT->System->FirstAttribute('BrandedSubjectTag');
+         return 1 unless $attr;
+         my ( $status, $msg ) = $attr->Delete;
+         unless ( $status ) {
+             RT->Logger->error("Couldn't delete System BrandedSubjectTag: $msg");
+         }
+         return 1;
+     },
+ );
diff --cc lib/RT/Template.pm
index 9b9ca3f,120d412..43c08ba
--- a/lib/RT/Template.pm
+++ b/lib/RT/Template.pm
@@@ -278,17 -256,7 +278,17 @@@ sub Create 
          $args{'Queue'} = $QueueObj->Id;
      }
  
 +    return ( undef, $self->loc('Name is required') )
 +        unless $args{Name};
 +
 +    {
 +        my $tmp = $self->new( RT->SystemUser );
 +        $tmp->LoadByCols( Name => $args{'Name'}, Queue => $args{'Queue'} );
 +        return ( undef, $self->loc('A Template with that name already exists') )
 +            if $tmp->id;
 +    }
 +
-     my $result = $self->SUPER::Create(
+     my ( $result, $msg ) = $self->SUPER::Create(
          Content     => $args{'Content'},
          Queue       => $args{'Queue'},
          Description => $args{'Description'},
diff --cc lib/RT/Transaction.pm
index 479519d,6e9f78f..3b17319
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@@ -1124,23 -1033,10 +1124,24 @@@ sub _FormatUser 
              }
          }
          else {
 -            return $self->loc( "[_1] changed from [_2] to [_3]",
 -                               $self->loc($self->Field),
 -                               ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")),
 -                               ($self->NewValue? "'".$self->NewValue ."'" : $self->loc("(no value)")));
 +            return ( "[_1] changed from [_2] to [_3]",
 +                    $self->loc($self->Field),
-                     ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );  #loc()
++                    ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")),
++                    ($self->NewValue? "'".$self->NewValue ."'" : $self->loc("(no value)")));  #loc()
 +        }
 +    },
 +    "Set-TimeWorked" => sub {
 +        my $self = shift;
 +        my $old  = $self->OldValue || 0;
 +        my $new  = $self->NewValue || 0;
 +        my $duration = $new - $old;
 +        if ($duration < 0) {
 +            return ("Adjusted time worked by [quant,_1,minute,minutes]", $duration);
 +        }
 +        elsif ($duration < 60) {
 +            return ("Worked [quant,_1,minute,minutes]", $duration);
 +        } else {
 +            return ("Worked [quant,_1,hour,hours] ([numf,_2] minutes)", sprintf("%.1f", $duration / 60), $duration);
          }
      },
      PurgeTransaction => sub {
diff --cc sbin/rt-dump-metadata.in
index 8c05ed7,798e0e4..cc72eb9
--- a/sbin/rt-dump-metadata.in
+++ b/sbin/rt-dump-metadata.in
@@@ -105,8 -119,15 +107,16 @@@ my @classes      = qw
  foreach my $class (@classes) {
      require "RT/$class.pm";
      my $objects = "RT::$class"->new( RT->SystemUser );
-     $objects->{find_disabled_rows} = 1;
+     $objects->{find_disabled_rows} = 1 unless $opt{'skip-disabled'};
      $objects->UnLimit;
+     $objects->LimitToPrivileged if $class eq 'Users'
+         && $opt{'limit-to-privileged'};
+     $objects->Limit(
+         FIELD    => 'Domain',
+         OPERATOR => '=',
 -        VALUE    => 'UserDefined'
++        VALUE    => 'UserDefined',
++        CASESENSITIVE => 0,
+     ) if $class eq 'Groups';
  
      if ( $class eq 'CustomFields' ) {
          $objects->OrderByCols(
diff --cc share/html/Elements/EditCustomFieldSelect
index 5d1ceed,b7a3112..8f0af4b
--- a/share/html/Elements/EditCustomFieldSelect
+++ b/share/html/Elements/EditCustomFieldSelect
@@@ -62,16 -63,29 +62,28 @@@
  %   }
      </select><br />
  % } elsif ($CustomField->BasedOnObj->id) {
 -<script type="text/javascript" src="<%RT->Config->Get('WebPath')%>/NoAuth/js/cascaded.js"></script>
  <script type="text/javascript"><!--
  jQuery(  function () {
-     var basedon = document.getElementById(<% $NamePrefix . $CustomField->BasedOnObj->id . "-Values" |n,j%>);
-     if (basedon != null) {
-         var oldchange = basedon.onchange;
-         basedon.onchange = function () {
 -    var basedon = jQuery('[name^=<% $NamePrefix .  $CustomField->BasedOnObj->id %>-Value][type!=hidden]:input');
++    var basedon = jQuery('[name^="<% $NamePrefix .  $CustomField->BasedOnObj->id %>-Value"][type!="hidden"]:input');
+     basedon.each( function() {
+         var oldchange = jQuery(this).onchange;
+         jQuery(this).change( function () {
+             var vals;
+             if ( jQuery(this).is('select') ) {
+                 vals = basedon.first().val();
+             }
+             else {
+                 vals = [];
+                 jQuery(basedon).each( function() {
+                     if ( jQuery(this).is(':checked') ) {
+                         vals.push(jQuery(this).val());
+                     }
+                 });
+             }
 -            filter_cascade(
 +            filter_cascade_by_id(
                  <% "$id-Values" |n,j%>,
-                 jQuery(basedon).val(),
+                 vals,
 -                1
 +                true
              );
              if (oldchange != null)
                  oldchange();
@@@ -100,14 -115,25 +113,25 @@@
  %   while ( my $value = $CFVs->Next ) {
  %     my $content = $value->Name;
  %     my $labelid = "$name-". $value->id;
+ <div name="<% $value->Category %>">
    <input type="<% $checktype %>" name="<% $name %>" id="<% $labelid %>" value="<% $content %>" <% $default{ lc $content }? ' checked="checked"' : '' |n%> />
    <label for="<% $labelid %>"><% $content %></label><br />
+ </div>
  %   }
+ </div>
  </fieldset>
  % } else {
+ % if (@category) {
+ %# this hidden select is to supply a full list of values,
 -%# see filter_cascade() in js/cascaded.js
++%# see filter_cascade_select() in js/cascaded.js
+       <select name="<%$id%>-Values-Complete" id="<%$id%>-Values-Complete" class="hidden" disabled="disabled">
+         <option value=""<% !$selected && qq[ selected="selected"] |n %>><&|/l&>(no value)</&></option>
+ %       $m->out($out);
+       </select>
+ % }
  <select
    name="<%$id%>-Values" id="<%$id%>-Values" class="CF-<%$CustomField->id%>-Edit"
- % if ( $Rows && ( $Multiple || !@category ) ) {
+ % if ( $Rows && ( $Multiple || !@category || $RenderType eq 'Select box') ) {
    size="<% $Rows %>"
  % }
  <% $Multiple && qq[multiple="multiple"] |n %> >
diff --cc share/html/Elements/SelectCustomFieldValue
index cd3829e,27710d9..4da14df
--- a/share/html/Elements/SelectCustomFieldValue
+++ b/share/html/Elements/SelectCustomFieldValue
@@@ -53,13 -52,25 +53,26 @@@
  <option value="" selected="selected">-</option>
  <option value="NULL"><&|/l&>(no value)</&></option>
  % while (my $value = $values->Next) {
 -<option value="<%$value->Name%>"><%$value->Name%></option>
 +<option value="<%$value->Name%>"<% ($value->Name eq $Default) ? q[ selected="selected"] : ''%>><%$value->Name%></option>
  % }
  </select>
 -% }
 -% elsif ( $CustomField->Type eq 'Autocomplete' )  {
 -<input type="text" id="CF-<% $CustomField->id %>" name="<% $Name %>" size="20" />
 +% } elsif ($CustomField->Type =~ /^Date(Time)?$/) {
 +<& /Elements/SelectDate, ShowTime => ($1 ? 1 : 0), Name => $Name, Value => $Default &>
++% } elsif ( $CustomField->Type eq 'Autocomplete' )  {
++<input type="text" id="CF-<% $CustomField->id %>" name="<% $Name %>" size="20" value="<% $Default %>" />
+ <script type="text/javascript">
+ % my @options;
+ % my $values = $CustomField->Values;
+ % while (my $value = $values->Next) {
+ %   push @options, {
+ %       value => $value->Name,
+ %       label => $value->Description ? $value->Name . ' (' . $value->Description . ')' : $value->Name,
+ %   };
+ % }
+ jQuery('#'+'CF-' + <% $CustomField->id %>).autocomplete({ source: <% JSON::to_json(\@options) |n %> });
+ </script>
  % } else {
 -<input name="<%$Name%>" size="20" />
 +<input name="<%$Name%>" size="20" value="<% $Default %>" type="text" />
  % }
  <%args>
  $Name => undef
diff --cc share/html/SelfService/Display.html
index e9c2eb4,b2fdb53..95d3982
--- a/share/html/SelfService/Display.html
+++ b/share/html/SelfService/Display.html
@@@ -50,8 -50,9 +50,9 @@@
  % $m->callback(CallbackName => 'BeforeActionList', %ARGS, Actions => \@results, ARGSRef => \%ARGS, Ticket => $Ticket );
  
  <& /Elements/ListActions, actions => \@results &>
+ <& /Ticket/Elements/ShowUpdateStatus, Ticket => $Ticket &>
  
 -  <table width="100%" class="ticketsummary" >
 +  <table width="100%" class="ticket-summary" >
        <tr>
          <td valign="top" width="50%" class="boxcontainer">
            <&| /Widgets/TitleBox, title => loc('The Basics'),
@@@ -136,24 -166,43 +137,33 @@@ else 
          push @results, "$msg";
      }
  
 +}
  
 -    }
 +# This code does automatic redirection if any updates happen.
  
 -    # This code does automatic redirection if any updates happen.
 +unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
 +    Abort( loc("No permission to display that ticket") );
 +}
  
 -    unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
 -        $m->comp( 'Error.html',
 -            Why => loc("No permission to display that ticket") );
 -        # XXX: Why abort? then we loose footer //ruz
 -        $m->abort();
 -    }
 -
 -    if ( $ARGS{'MarkAsSeen'} ) {
 -        $Ticket->SetAttribute(
 -            Name    => 'User-'. $Ticket->CurrentUser->id .'-SeenUpTo',
 -            Content => $Ticket->LastUpdated,
 -        );
 -        push @results, loc('Marked all messages as seen');
 -    }
 -
 -    # This code does automatic redirection if any updates happen.
 -    MaybeRedirectForResults(
 -        Actions   => \@results,
 -        Path      => '/SelfService/Display.html',
 -        Anchor    => $ARGS{'Anchor'},
 -        Arguments => { id => $Ticket->id },
++if ( $ARGS{'MarkAsSeen'} ) {
++    $Ticket->SetAttribute(
++        Name    => 'User-'. $Ticket->CurrentUser->id .'-SeenUpTo',
++        Content => $Ticket->LastUpdated,
+     );
 -
 -    my $Transactions = $Ticket->Transactions;
 -
 -    my $attachments =
 -      $m->comp( '/Ticket/Elements/FindAttachments', Ticket => $Ticket );
 -
 -    my $LinkBasicsTitle = $Ticket->CurrentUserHasRight('ModifyTicket')
 -                          || $Ticket->CurrentUserHasRight('ReplyToTicket');
 -    my $title_box_link = RT->Config->Get('WebPath')."/SelfService/Update.html?id=".$Ticket->Id;
 -    $m->callback(CallbackName => 'BeforeDisplay', Ticket => \$Ticket, ARGSRef => \%ARGS, title_box_link => \$title_box_link);
++    push @results, loc('Marked all messages as seen');
++}
++
 +MaybeRedirectForResults(
 +    Actions   => \@results,
 +    Path      => '/SelfService/Display.html',
++    Anchor    => $ARGS{'Anchor'},
 +    Arguments => { 'id' => $Ticket->id },
 +);
 +
 +my $LinkBasicsTitle = $Ticket->CurrentUserHasRight('ModifyTicket')
 +                      || $Ticket->CurrentUserHasRight('ReplyToTicket');
 +my $title_box_link = RT->Config->Get('WebPath')."/SelfService/Update.html?id=".$Ticket->Id;
 +$m->callback(CallbackName => 'BeforeDisplay', Ticket => \$Ticket, ARGSRef => \%ARGS, title_box_link => \$title_box_link);
  </%INIT>
  
  
diff --cc share/html/Ticket/Elements/EditBasics
index 1d111f0,95f49d3..68920b0
--- a/share/html/Ticket/Elements/EditBasics
+++ b/share/html/Ticket/Elements/EditBasics
@@@ -53,17 -53,19 +53,18 @@@ $InTable => 
  </%ARGS>
  <%INIT>
  unless ( @fields ) {
+     my $subject = $defaults{'Subject'} || $TicketObj->Subject;
      @fields = (
          {   name => 'Subject',
-             html => '<input name="Subject" value="'.$m->interp->apply_escapes( $defaults{'Subject'} || $TicketObj->Subject, 'h' ).'" />',
+             html => '<input name="Subject" value="'.(defined($subject) ? $m->interp->apply_escapes( $subject, 'h' ) : '').'" />',
          },
          {   name => 'Status',
 -            comp => '/Elements/SelectStatus',
 +            comp => '/Ticket/Elements/SelectStatus',
              args => {
                  Name => 'Status',
 -                DefaultLabel => loc("[_1] (Unchanged)",loc($TicketObj->Status)),
 -                Default => $defaults{'Status'} || undef,
 +                Default => $defaults{Status},
 +                DefaultFromArgs => 0,
                  TicketObj => $TicketObj,
 -                QueueObj => $TicketObj->QueueObj,
              },
          },
          {   name => 'Queue',
diff --cc share/html/Ticket/Elements/ShowSummary
index b7981e3,7241268..7e2fbc9
--- a/share/html/Ticket/Elements/ShowSummary
+++ b/share/html/Ticket/Elements/ShowSummary
@@@ -53,14 -53,14 +53,14 @@@
          (($can_modify || $can_modify_cf) ? (title_href => RT->Config->Get('WebPath')."/Ticket/Modify.html?id=".$Ticket->Id) : ()),
          class => 'ticket-info-basics',
      &><& /Ticket/Elements/ShowBasics, Ticket => $Ticket &></&>
 -    <&| /Widgets/TitleBox, title => loc('Custom Fields'),
 -        (($can_modify || $can_modify_cf) ? (title_href => RT->Config->Get('WebPath')."/Ticket/Modify.html?id=".$Ticket->Id) : ()),
 -        class => 'ticket-info-cfs',
 -        hide_empty => 1,
 -    &><& /Ticket/Elements/ShowCustomFields, Ticket => $Ticket &></&>
 +
 +    <& /Elements/ShowCustomFieldCustomGroupings,
 +        Object       => $Ticket,
 +        title_href   => ($can_modify || $can_modify_cf) ? RT->Config->Get('WebPath')."/Ticket/Modify.html" : "",
 +        &>
  
      <&| /Widgets/TitleBox, title => loc('People'),
-         (($can_modify || $can_modify_owner) ? (title_href => RT->Config->Get('WebPath')."/Ticket/ModifyPeople.html?id=".$Ticket->Id) : ()),
+         (($can_modify || $can_modify_owner || $can_modify_people) ? (title_href => RT->Config->Get('WebPath')."/Ticket/ModifyPeople.html?id=".$Ticket->Id) : ()),
          class => 'ticket-info-people',
      &><& /Ticket/Elements/ShowPeople, Ticket => $Ticket &></&>
      <& /Ticket/Elements/ShowAttachments, Ticket => $Ticket, Attachments => $Attachments &>
@@@ -104,5 -106,9 +104,7 @@@ $Attachments => unde
  <%INIT>
  my $can_modify = $Ticket->CurrentUserHasRight('ModifyTicket');
  my $can_modify_cf = $Ticket->CurrentUserHasRight('ModifyCustomField');
 -my $can_modify_owner = $Ticket->CurrentUserHasRight('OwnTicket')
 -                    || $Ticket->CurrentUserHasRight('TakeTicket')
 -                    || $Ticket->CurrentUserHasRight('StealTicket');
 +my $can_modify_owner = $Ticket->CurrentUserCanSetOwner();
+ my $can_modify_people = $Ticket->CurrentUserHasRight('Watch')
+                      || $Ticket->CurrentUserHasRight('WatchAsAdminCc');
  </%INIT>
diff --cc share/static/js/cascaded.js
index 065a398,0000000..2377a33
mode 100644,000000..100644
--- a/share/static/js/cascaded.js
+++ b/share/static/js/cascaded.js
@@@ -1,91 -1,0 +1,114 @@@
 +function filter_cascade_by_id (id, vals, is_hierarchical) {
-     var select = document.getElementById(id);
-     var complete_select = document.getElementById(id + "-Complete" );
-     return filter_cascade(select, complete_select, vals, is_hierarchical);
++    var element = document.getElementById(id);
++    if (!element) { return };
++
++    if ( element.tagName == 'SELECT' ) {
++        var complete_select = document.getElementById(id + "-Complete" );
++        return filter_cascade_select(element, complete_select, vals, is_hierarchical);
++    }
++    else {
++        if ( !( vals instanceof Array ) ) {
++            vals = [vals];
++        }
++
++        if ( is_hierarchical && (vals.length == 0 || (vals.length == 1 && vals[0] == '')) ) {
++            // no category, and the category is from a hierchical cf;
++            // leave it empty
++            jQuery(element).find('div').hide();
++        }
++        else {
++            jQuery(element).find('div').hide().find('input').prop('disabled', true);
++            jQuery(element).find('div[name=]').show().find('input').prop('disabled', false);
++            jQuery(element).find('div.none').show().find('input').prop('disabled',false);
++            for ( var j = 0; j < vals.length; j++ ) {
++                jQuery(element).find('div[name^=' + vals[j] + ']').show().find('input').prop('disabled', false);
++            }
++        }
++    }
 +}
 +
- function filter_cascade (select, complete_select, vals, is_hierarchical) {
++function filter_cascade_select (select, complete_select, vals, is_hierarchical) {
 +    if ( !( vals instanceof Array ) ) {
 +        vals = [vals];
 +    }
 +
 +    if (!select) { return };
 +    var i;
 +    var children = select.childNodes;
 +
 +    if ( complete_select ) {
 +        while (select.hasChildNodes()){
 +            select.removeChild(select.firstChild);
 +        }
 +
 +        var complete_children = complete_select.childNodes;
 +
 +        var cloned_labels = {};
 +        var cloned_empty_label;
 +        for ( var j = 0; j < vals.length; j++ ) {
 +            var val = vals[j];
 +            if ( val == '' && is_hierarchical ) {
 +                // no category, and the category is from a hierchical cf;
 +                // leave this set of options empty
 +            } else if ( val == '' ) {
 +                // no category, let's clone all node
 +                for (i = 0; i < complete_children.length; i++) {
 +                    if ( complete_children[i].cloneNode ) {
 +                        var new_option = complete_children[i].cloneNode(true);
 +                        select.appendChild(new_option);
 +                    }
 +                }
 +                break;
 +            }
 +            else {
 +                var labels_to_clone = {};
 +                for (i = 0; i < complete_children.length; i++) {
 +                    if (!complete_children[i].label ||
 +                          (complete_children[i].hasAttribute &&
 +                                !complete_children[i].hasAttribute('label') ) ) {
 +                        if ( cloned_empty_label ) {
 +                            continue;
 +                        }
 +                    }
 +                    else if ( complete_children[i].label == val ) {
 +                        if ( cloned_labels[complete_children[i].label] ) {
 +                            continue;
 +                        }
 +                        labels_to_clone[complete_children[i].label] = true;
 +                    }
 +                    else {
 +                        continue;
 +                    }
 +
 +                    if ( complete_children[i].cloneNode ) {
 +                        var new_option = complete_children[i].cloneNode(true);
 +                        select.appendChild(new_option);
 +                    }
 +                }
 +
 +                if ( !cloned_empty_label )
 +                    cloned_empty_label = true;
 +
 +                for ( label in labels_to_clone ) {
 +                    if ( !cloned_labels[label] )
 +                        cloned_labels[label] = true;
 +                }
 +            }
 +        }
 +    }
 +    else {
 +// for back compatibility
 +        for (i = 0; i < children.length; i++) {
 +            if (!children[i].label) { continue };
 +            if ( val == '' && is_hierarchical ) {
 +                hide(children[i]);
 +                continue;
 +            }
 +            if ( val == '' || children[i].label.substr(0, val.length) == val) {
 +                show(children[i]);
 +                continue;
 +            }
 +            hide(children[i]);
 +        }
 +    }
 +}
diff --cc share/static/js/event-registration.js
index de51fcb,0000000..aec1742
mode 100644,000000..100644
--- a/share/static/js/event-registration.js
+++ b/share/static/js/event-registration.js
@@@ -1,102 -1,0 +1,102 @@@
 +// Disable chosing individual objects when a scrip is applied globally
 +jQuery(function() {
 +    var global_checkboxes = [
 +        "form[name=AddRemoveScrip] input[type=checkbox][name^=AddScrip-][value=0]",
 +        "form input[type=checkbox][name^=AddCustomField-][value=0]"
 +    ];
 +    jQuery(global_checkboxes.join(", "))
 +        .change(function(){
 +            var self    = jQuery(this);
 +            var checked = self.prop("checked");
 +
 +            self.closest("form")
 +                .find("table.collection input[type=checkbox]")
 +                .prop("disabled", checked);
 +        });
 +});
 +
 +// Replace user references in history with the HTML versions
 +function ReplaceUserReferences() {
 +    var users = jQuery(".user[data-replace=user]");
 +    var ids   = users.map(function(){
 +        return "id=" + encodeURIComponent(jQuery(this).attr("data-user-id"))
 +    }).toArray().join(";");
 +
 +    if (!ids.length)
 +        return
 +
 +    jQuery.get(
 +        RT.Config.WebPath + "/Helpers/UserInfo?" + ids,
 +        function(json) {
 +            users.each(function() {
 +                var user = jQuery(this);
 +                var uid  = user.attr("data-user-id");
 +                if (!json[uid])
 +                    return
 +                user.removeAttr("data-replace")
 +                    .html( jQuery(json[uid]._html).html() );
 +            });
 +        }
 +    );
 +}
 +jQuery(ReplaceUserReferences);
 +
 +// Cascaded selects
 +jQuery(function() {
 +    jQuery("select.cascade-by-optgroup").each(function(){
 +        var name = this.name;
 +        if (!name) return;
 +
 +        // Generate elements for cascading based on the master <select> ...
 +        var complete = jQuery(this)
 +            .clone(true, true)
 +            .attr("name", name + "-Complete")
 +            .attr("disabled", "disabled")
 +            .hide()
 +            .insertAfter(this);
 +
 +        var groups = jQuery(this)
 +            .clone(true, true)
 +            .attr("name", name + "-Groups")
 +            .find("option").remove().end()
 +            .find("optgroup").replaceWith(function(){
 +                return jQuery("<option>").val(this.label).text(this.label);
 +            }).end()
 +            .prepend( complete.find("option[value='']") )
 +            .insertBefore(this);
 +
 +        // Synchronize the <select> we just generated
 +        var selected = jQuery("option[selected]", this).parent().attr("label");
 +        jQuery('option[value="' + selected + '"]', groups).attr("selected", "selected");
 +
 +        // Wire it all up
 +        groups.change(function(){
 +            var name     = this.name.replace(/-Groups$/, '');
 +            var field    = jQuery(this);
 +            var subfield = field.next("select[name=" + name + "]");
 +            var complete = subfield.next("select[name=" + name + "-Complete]");
 +            var value    = field.val();
-             filter_cascade( subfield[0], complete[0], value, true );
++            filter_cascade_select( subfield[0], complete[0], value, true );
 +        }).change();
 +    });
 +});
 +
 +jQuery( function() {
 +    jQuery("input[type=file]").change( function() {
 +        var input = jQuery(this);
 +        var warning = input.next(".invalid");
 +
 +        if ( !input.val().match(/"/) ) {
 +            warning.hide();
 +        } else {
 +            if (warning.length) {
 +                warning.show();
 +            } else {
 +                input.val("");
 +                jQuery("<span class='invalid'>")
 +                    .text(loc_key("quote_in_filename"))
 +                    .insertAfter(input);
 +            }
 +        }
 +    });
 +});
diff --cc share/static/js/util.js
index 15669fc,0000000..d4a53c8
mode 100644,000000..100644
--- a/share/static/js/util.js
+++ b/share/static/js/util.js
@@@ -1,355 -1,0 +1,344 @@@
 +/* Visibility */
 +
 +function show(id) { delClass( id, 'hidden' ) }
 +function hide(id) { addClass( id, 'hidden' ) }
 +
 +function hideshow(id) { return toggleVisibility( id ) }
 +function toggleVisibility(id) {
 +    var e = jQuery('#' + id);
 +
 +    if ( e.hasClass('hidden') ) {
 +        e.removeClass('hidden');
 +    }
 +    else {
 +        e.addClass('hidden');
 +    }
 +
 +    return false;
 +}
 +
 +function setVisibility(id, visibility) {
 +    if ( visibility ) show(id);
 +    else hide(id);
 +}
 +
 +function switchVisibility(id1, id2) {
 +    // Show both and then hide the one we want
 +    show(id1);
 +    show(id2);
 +    hide(id2);
 +    return false;
 +}
 +
 +function toggle_upgrade_history(widget, selector) {
 +    jQuery(selector).toggle();
 +    jQuery(widget).toggleClass("rolled-up");
 +}
 +
 +/* Classes */
 +function jQueryWrap( id ) {
 +    return typeof id == 'object' ? jQuery(id) : jQuery('#'+id);
 +}
 +
 +function addClass(id, value) {
 +    jQueryWrap(id).addClass(value);
 +}
 +
 +function delClass(id, value) {
 +    jQueryWrap(id).removeClass(value);
 +}
 +
 +/* Rollups */
 +
 +function rollup(id) {
 +    var e = jQueryWrap(id);
 +    var e2  = e.parent();
 +    
 +    if (e.hasClass('hidden')) {
 +        set_rollup_state(e,e2,'shown');
 +        createCookie(id,1,365);
 +    }
 +    else {
 +        set_rollup_state(e,e2,'hidden');
 +        createCookie(id,0,365);
 +    }
 +    return false;
 +}
 +
 +function set_rollup_state(e,e2,state) {
 +    if (e && e2) {
 +        if (state == 'shown') {
 +            show(e);
 +            delClass( e2, 'rolled-up' );
 +        }
 +        else if (state == 'hidden') {
 +            hide(e);
 +            addClass( e2, 'rolled-up' );
 +        }
 +    }
 +}
 +
 +/* other utils */
 +
 +function setCheckbox(input, name, val) {
 +    if (val == null) val = input.checked;
 +
 +    // Find inputs within the current form or collection list, whichever is closest.
 +    var container = jQuery(input).closest("form, table.collection-as-table").get(0);
 +    var myfield   = container.getElementsByTagName('input');
 +    for ( var i = 0; i < myfield.length; i++ ) {
 +        if ( myfield[i].type != 'checkbox' ) continue;
 +        if ( name ) {
 +            if ( name instanceof RegExp ) {
 +                if ( ! myfield[i].name.match( name ) ) continue;
 +            }
 +            else {
 +                if ( myfield[i].name != name ) continue;
 +            }
 +
 +        }
 +
 +        myfield[i].checked = val;
 +    }
 +}
 +
 +/* apply callback to nodes or elements */
 +
 +function walkChildNodes(parent, callback)
 +{
 +    if( !parent || !parent.childNodes ) return;
 +    var list = parent.childNodes;
 +    for( var i = 0; i < list.length; i++ ) {
 +        callback( list[i] );
 +    }
 +}
 +
 +function walkChildElements(parent, callback)
 +{
 +    walkChildNodes( parent, function(node) {
 +        if( node.nodeType != 1 ) return;
 +        return callback( node );
 +    } );
 +}
 +
 +/* shredder things */
 +
 +function showShredderPluginTab( plugin )
 +{
 +    var plugin_tab_id = 'shredder-plugin-'+ plugin +'-tab';
 +    var root = jQuery('#shredder-plugin-tabs');
 +    
 +    root.children(':not(.hidden)').addClass('hidden');
 +    root.children('#' + plugin_tab_id).removeClass('hidden');
 +
 +    if( plugin ) {
 +        show('shredder-submit-button');
 +    } else {
 +        hide('shredder-submit-button');
 +    }
 +}
 +
 +function checkAllObjects()
 +{
 +    var check = jQuery('#shredder-select-all-objects-checkbox').prop('checked');
 +    var elements = jQuery('#shredder-search-form :checkbox[name=WipeoutObject]');
 +
 +    if( check ) {
 +        elements.prop('checked', true);
 +    } else {
 +        elements.prop('checked', false);
 +    }
 +}
 +
 +function checkboxToInput(target,checkbox,val){    
 +    var tar = jQuery('#' + escapeCssSelector(target));
 +    var box = jQuery('#' + escapeCssSelector(checkbox));
 +    if(box.prop('checked')){
 +        if (tar.val()==''){
 +            tar.val(val);
 +        }
 +        else{
 +            tar.val( val+', '+ tar.val() );        
 +        }
 +    }
 +    else{
 +        tar.val(tar.val().replace(val+', ',''));
 +        tar.val(tar.val().replace(val,''));
 +    }
 +    jQuery('#UpdateIgnoreAddressCheckboxes').val(true);
 +}
 +
 +// ahah for back compatibility as plugins may still use it
 +function ahah( url, id ) {
 +    jQuery('#'+id).load(url);
 +}
 +
 +// only for back compatibility, please JQuery() instead
 +function doOnLoad( js ) {
 +    jQuery(js);
 +}
 +
 +jQuery(function() {
 +    var opts = {
 +        dateFormat: 'yy-mm-dd',
 +        constrainInput: false,
 +        showButtonPanel: true,
 +        changeMonth: true,
 +        changeYear: true,
 +        showOtherMonths: true,
 +        selectOtherMonths: true
 +    };
 +    jQuery(".datepicker:not(.withtime)").datepicker(opts);
 +    jQuery(".datepicker.withtime").datetimepicker( jQuery.extend({}, opts, {
 +        stepHour: 1,
 +        // We fake this by snapping below for the minute slider
 +        //stepMinute: 5,
 +        hourGrid: 6,
 +        minuteGrid: 15,
 +        showSecond: false,
 +        timeFormat: 'HH:mm:ss'
 +    }) ).each(function(index, el) {
 +        var tp = jQuery.datepicker._get( jQuery.datepicker._getInst(el), 'timepicker');
 +        if (!tp) return;
 +
 +        // Hook after _injectTimePicker so we can modify the minute_slider
 +        // right after it's first created
 +        tp._base_injectTimePicker = tp._injectTimePicker;
 +        tp._injectTimePicker = function() {
 +            this._base_injectTimePicker.apply(this, arguments);
 +
 +            // Now that we have minute_slider, modify it to be stepped for mouse movements
 +            var slider = jQuery.data(this.minute_slider[0], "ui-slider");
 +            slider._base_normValueFromMouse = slider._normValueFromMouse;
 +            slider._normValueFromMouse = function() {
 +                var value           = this._base_normValueFromMouse.apply(this, arguments);
 +                var old_step        = this.options.step;
 +                this.options.step   = 5;
 +                var aligned         = this._trimAlignValue( value );
 +                this.options.step   = old_step;
 +                return aligned;
 +            };
 +        };
 +    });
 +});
 +
 +function textToHTML(value) {
 +    return value.replace(/&/g,    "&")
 +                .replace(/</g,    "<")
 +                .replace(/>/g,    ">")
 +                .replace(/-- \n/g,"-- \n")
 +                .replace(/\n/g,   "\n<br />");
 +};
 +
- function ReplaceAllTextareas(encoded) {
++function ReplaceAllTextareas() {
 +    var sAgent = navigator.userAgent.toLowerCase();
 +    if (!CKEDITOR.env.isCompatible ||
 +        sAgent.indexOf('iphone') != -1 ||
 +        sAgent.indexOf('ipad') != -1 ||
 +        sAgent.indexOf('android') != -1 )
 +        return false;
 +
 +    // replace all content and signature message boxes
 +    var allTextAreas = document.getElementsByTagName("textarea");
 +
 +    for (var i=0; i < allTextAreas.length; i++) {
 +        var textArea = allTextAreas[i];
 +        if (jQuery(textArea).hasClass("messagebox")) {
 +            // Turn the original plain text content into HTML
-             if (encoded == 0) {
++            var type = jQuery("#"+textArea.name+"Type");
++            if (type.val() != "text/html")
 +                textArea.value = textToHTML(textArea.value);
-             }
-             // For this javascript
-             var CKeditorEncoded = document.createElement('input');
-             CKeditorEncoded.setAttribute('type', 'hidden');
-             CKeditorEncoded.setAttribute('name', 'CKeditorEncoded');
-             CKeditorEncoded.setAttribute('value', '1');
-             textArea.parentNode.appendChild(CKeditorEncoded);
- 
-             // For fckeditor
-             var typeField = document.createElement('input');
-             typeField.setAttribute('type', 'hidden');
-             typeField.setAttribute('name', textArea.name + 'Type');
-             typeField.setAttribute('value', 'text/html');
-             textArea.parentNode.appendChild(typeField);
 +
++            // Set the type
++            type.val("text/html");
 +
 +            CKEDITOR.replace(textArea.name,{ width: '100%', height: RT.Config.MessageBoxRichTextHeight });
 +            CKEDITOR.basePath = RT.Config.WebPath + "/static/RichText/";
 +
 +            jQuery("#" + textArea.name + "___Frame").addClass("richtext-editor");
 +        }
 +    }
 +};
 +
 +function toggle_addprincipal_validity(input, good, title) {
 +    if (good) {
 +        jQuery(input).nextAll(".warning").hide();
 +        jQuery("#acl-AddPrincipal input[type=checkbox]").removeAttr("disabled");
 +    } else {
 +        jQuery(input).nextAll(".warning").css("display", "block");
 +        jQuery("#acl-AddPrincipal input[type=checkbox]").attr("disabled", "disabled");
 +    }
 +
 +    if (title == null)
 +        title = jQuery(input).val();
 +
 +    update_addprincipal_title( title );
 +}
 +
 +function update_addprincipal_title(title) {
 +    var h3 = jQuery("#acl-AddPrincipal h3");
 +    h3.html( h3.text().replace(/: .*$/,'') + ": " + title );
 +}
 +
 +// when a value is selected from the autocompleter
 +function addprincipal_onselect(ev, ui) {
 +
 +    // if principal link exists, we shall go there instead
 +    var principal_link = jQuery(ev.target).closest('form').find('ul.ui-tabs-nav a[href="#acl-' + ui.item.id + '"]:first');
 +    if (principal_link.size()) {
 +        jQuery(this).val('').blur();
 +        update_addprincipal_title( '' ); // reset title to blank for #acl-AddPrincipal
 +        principal_link.click();
 +        return false;
 +    }
 +
 +    // pass the item's value along as the title since the input's value
 +    // isn't actually updated yet
 +    toggle_addprincipal_validity(this, true, ui.item.value);
 +}
 +
 +// when the input is actually changed, through typing or autocomplete
 +function addprincipal_onchange(ev, ui) {
 +    // if we have a ui.item, then they selected from autocomplete and it's good
 +    if (!ui.item) {
 +        var input = jQuery(this);
 +        // Check using the same autocomplete source if the value typed would
 +        // have been autocompleted and is therefore valid
 +        jQuery.ajax({
 +            url: input.autocomplete("option", "source"),
 +            data: {
 +                op: "=",
 +                term: input.val()
 +            },
 +            dataType: "json",
 +            success: function(data) {
 +                if (data)
 +                    toggle_addprincipal_validity(input, data.length ? true : false );
 +                else
 +                    toggle_addprincipal_validity(input, true);
 +            }
 +        });
 +    } else {
 +        toggle_addprincipal_validity(this, true);
 +    }
 +}
 +
 +
 +function escapeCssSelector(str) {
 +    return str.replace(/([^A-Za-z0-9_-])/g,'\\$1');
 +}
 +
 +
 +jQuery(function() {
 +    jQuery(".user-accordion").each(function(){
 +        jQuery(this).accordion({
 +            active: (jQuery(this).find("h3").length == 1 ? 0 : false),
 +            collapsible: true,
 +            heightStyle: "content",
 +            header: "h3"
 +        }).find("h3 a.user-summary").click(function(ev){
 +            ev.stopPropagation();
 +            return true;
 +        });
 +    });
 +});
diff --cc t/web/self_service.t
index 49d9e37,adc90d7..7afc008
--- a/t/web/self_service.t
+++ b/t/web/self_service.t
@@@ -4,10 -8,30 +8,29 @@@ use RT::Tes
  
  my ($url, $m) = RT::Test->started_ok;
  
- my ($ticket) =
-   RT::Test->create_ticket( Queue => 'General', Subject => 'test subject' );
+ my $user_a = RT::Test->load_or_create_user(
+     Name         => 'user_a',
+     Password     => 'password',
+     EmailAddress => 'user_a at example.com',
+     Privileged   => 0,
+ );
+ ok( $user_a && $user_a->id, 'loaded or created user' );
+ ok( ! $user_a->Privileged, 'user is not privileged' );
+ 
+ # Load Cc group
 -my $Cc = RT::Group->new( RT->SystemUser );
 -my($ok, $msg) = $Cc->LoadSystemRoleGroup( 'Cc' );
 -ok($ok, $msg);
++my $Cc = RT::System->RoleGroup( 'Cc' );
++ok($Cc->id);
+ RT::Test->add_rights( { Principal => $Cc, Right => ['ShowTicket'] } );
+ 
+ my ($ticket) = RT::Test->create_ticket(
+     Queue   => 'General',
+     Subject => 'test subject',
+     Cc      => 'user_a at example.com',
+ );
+ 
+ my @results = $ticket->Correspond( Content => 'sample correspondence' );
  
- $m->login();
+ ok( $m->login('user_a' => 'password'), 'unprivileged user logged in' );
  
  $m->get_ok( '/SelfService/Display.html?id=' . $ticket->id,
      'got selfservice display page' );
diff --cc t/web/user_update.t
index f6fc66b,c0e9e52..74de868
--- a/t/web/user_update.t
+++ b/t/web/user_update.t
@@@ -33,7 -35,7 +33,7 @@@ $m->text_contains("Langは「'en_us'」
  $m->text_contains("実名", "Page content is japanese");
  $m->submit_form_ok({ with_fields => { Lang => ''} },
                     "And set to the default");
- $m->text_contains("Lang changed from 'ja' to ''");
 -$m->text_contains("Langは「'ja'」から「(値なし)」に変更されました");
++$m->text_contains("Lang changed from 'ja' to (no value)");
  $m->text_contains("Real Name", "Page content is english");
  
  undef $m;

-----------------------------------------------------------------------


More information about the rt-commit mailing list