[Rt-commit] rt branch, master, updated. rt-4.1.17-137-g5deb0f8

Alex Vandiver alexmv at bestpractical.com
Thu Jul 25 18:25:27 EDT 2013


The branch, master has been updated
       via  5deb0f83745332407c70096b8a2c4cafaf0e047e (commit)
       via  a5b36b6fdc82a2106d0689e322e2c3c027701fdf (commit)
       via  beb46a94645a87c335a01c697930769958337797 (commit)
       via  59defae4e321233595d7e5ea1be46318f7561452 (commit)
       via  452f006abdfedd6eecaa9c0a76ee91d833433549 (commit)
       via  46662060794bad3e1ac4a4c155fe1f5b05de267b (commit)
       via  6425b2c9065844e3f55bad4a0f0675571051e395 (commit)
       via  63341f6e643173454c0050ff534785ed74c11863 (commit)
       via  9dbdaa7082468d7c0fa85266255171975ba00f32 (commit)
       via  cf20f759faf725ffe9ea74c4e3da097f0e9d2443 (commit)
       via  bc93185a292966d8a223cfabc8b5e8f301d2115d (commit)
       via  8d66d316d0ba0ba1a82c1cb6b3b7c7a62e3d1abb (commit)
       via  a691bb0971a0b48739af286ef12a22dc6936d4b1 (commit)
       via  9f49ac9dd0e2e07a61a0bb33e63d2761bc420691 (commit)
       via  ce343490f8f396116dc6e90ee6e0bd21f2d6b4a3 (commit)
       via  31b2380cd44f70473f7b4bcd6acf1ee6f7d39fc7 (commit)
       via  5b4141dd72647b770ae1108696d674a494cad3a2 (commit)
       via  72503a3028c41c5b17dbcb688b65f7d1894cc25a (commit)
       via  d45d3cb14d7a0c8b0dc0dfffc9dbe381ec810809 (commit)
       via  ab8211351559fc59aafdb8bb0f03f0f545661cf1 (commit)
       via  9d4533a91e7a7717309255c7f0f3bba88f32c8ac (commit)
       via  1030b884c928f309e6e9261c45986399d0e1455f (commit)
       via  39c62c66bc27c9e3ec9355c5326e192df58e867a (commit)
       via  2156e32e1553e154295ad3fd7a498ccf6271af5d (commit)
       via  772feccad7d55dc7b39e93dfae5f0a977cd97ed5 (commit)
       via  d3ee8fd07e6226b454e769f634294ca6d7df42fc (commit)
       via  1f5be1a0a149bfaa0eeafe14e92b3757a886aaa0 (commit)
       via  805834eada398c8e50a455b2765473af498e6a98 (commit)
       via  74da405f494e9c5461837cfee1354aca093c0651 (commit)
       via  eb9164d961a028c0918fa497e7f8caba2b0e0e99 (commit)
       via  cdd71ccbc41d7a292b81230a42e80dbc1409fb75 (commit)
       via  830c7b2920d5cc5a9f34e9b978246f09a53f1f88 (commit)
       via  0a56a31cda982e4224cdd9e2f0959f05a03109e8 (commit)
       via  9ec0680a872dc9b5971d47046bd80698be78d9ac (commit)
       via  713e32366cd5992bf3cb55845844d1147dbb7038 (commit)
       via  980dd65594d9befb1ce092a196769b8328c1bff6 (commit)
       via  7d33c38ca271d86c2c0e9f1b1adcd36cca2ffd35 (commit)
       via  8ae031e9b7b61d1749992f38d9f9079100d9e931 (commit)
       via  098bdcc7f92b391a09a290e1da5f19cd15e3327b (commit)
       via  ca1239c4c92ef5103dd7a6beb65d329de4a66d24 (commit)
       via  8b7db2b6c8ca4f8af1a19f9aa0f7557b79dc7d80 (commit)
       via  9c5490c05de820643cc231926cf74af0f699a637 (commit)
       via  cea5e1e5fe222687570430591e9fe9fee65ea53d (commit)
       via  228451ce0c60bac20bf329d0896c7312db3af318 (commit)
       via  565b40eaf184d846c20da315b5247fd7c86c2e17 (commit)
       via  94aeca9f47f7923b8fe82c02e02d7f9e892716a7 (commit)
       via  be665348b78679d0a31900afb788517f9a10fda6 (commit)
       via  daf0a6c18f1ef822349e1d89d12be827b356d02e (commit)
       via  d39521de41ec430ddcaab92f7efbad0d93de77ac (commit)
       via  eb7dff164d3a37da0da73ca414c37fb91f42c4ba (commit)
       via  a9ae79a1ee31dff81b9712493da1cf9bbd39f488 (commit)
       via  f0ea5922e8c14114e3d1698f526634acff567dff (commit)
       via  5088509209ccc5ecdfe9e905959c3f4b096cb5ae (commit)
       via  7e5b1ccb0955d8c730d99a6a14b6cc05ec4b6960 (commit)
       via  e37801b72138e97fd23ad9e2a6e95d8ef96032d5 (commit)
       via  e4cabdf4a090e3cf6c057579b0b6a14bf7899c49 (commit)
       via  7d68f9b9d85f3dea8d6405037e2a9c3028cf5bed (commit)
       via  dccc6574dc5773ebff6f8d0db7fdd391032e2140 (commit)
       via  3943679cecb96805ee07a9f6f81fc2843f470384 (commit)
       via  38c965f5726218bbd6636f6455f07241d980cff2 (commit)
       via  afadbcaa47a44d9cf97f2b2e9df1d766bc3fc43b (commit)
       via  944109e78909187e4427557380f16f34a93c9727 (commit)
       via  22314efd0e70949bd26e6248d3bc18165f87194b (commit)
       via  8bca9b4b4fac320b89073b08fd6f921d1dcb1c42 (commit)
       via  f34d637c757ba5116411ecd63a45400c6a362d9c (commit)
       via  3a4ce59eabbd89f77f6d48efa38bfbe968b28dc5 (commit)
      from  7fcebae387bf1f26bedf35df6308bfdffd7e4ebe (commit)

Summary of changes:
 bin/rt.in                                  |  8 ++--
 devel/tools/rt-static-docs                 | 24 ++++++----
 docs/UPGRADING-4.0                         | 22 +++++++++
 docs/initialdata.pod                       |  2 +-
 etc/upgrade/3.9.8/content                  |  6 +--
 etc/upgrade/upgrade-articles.in            |  4 +-
 lib/RT/Action/SendEmail.pm                 |  2 +-
 lib/RT/CustomField.pm                      |  2 +-
 lib/RT/CustomFieldValues/Groups.pm         | 32 +++++++++++++
 lib/RT/Handle.pm                           | 25 ++++++++++
 lib/RT/Pod/HTML.pm                         | 27 +++++++++--
 lib/RT/Pod/HTMLBatch.pm                    | 15 +++++-
 lib/RT/Principal.pm                        |  5 +-
 lib/RT/Record.pm                           |  2 +-
 lib/RT/Test/Shredder.pm                    |  3 +-
 lib/RT/Transaction.pm                      | 41 ++++++++++++++++-
 sbin/rt-email-digest.in                    |  6 ++-
 sbin/rt-email-group-admin.in               | 16 ++++---
 share/html/Admin/CustomFields/Modify.html  |  2 +-
 share/html/Admin/Queues/index.html         |  1 +
 share/html/Articles/Article/PreCreate.html |  2 +-
 share/html/Elements/EditCustomFieldSelect  |  2 +-
 share/html/Search/Bulk.html                |  4 +-
 share/static/js/cascaded.js                | 73 +++++++++++++++++++++---------
 share/static/js/util.js                    | 10 ++++
 t/api/rights.t                             | 50 ++++++++++++++++----
 t/mail/digest-attributes.t                 |  4 +-
 27 files changed, 311 insertions(+), 79 deletions(-)

- Log -----------------------------------------------------------------
commit 5deb0f83745332407c70096b8a2c4cafaf0e047e
Merge: 7fcebae a5b36b6
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Thu Jul 25 18:18:15 2013 -0400

    Merge branch '4.0-trunk'
    
    Conflicts:
    	etc/upgrade/3.9.8/content
    	lib/RT/CustomField.pm
    	lib/RT/CustomFields.pm
    	lib/RT/Handle.pm
    	lib/RT/Test/Shredder.pm
    	lib/RT/Tickets.pm
    	share/html/Admin/Global/DashboardsInMenu.html
    	share/html/Elements/ColumnMap
    	share/html/Elements/EditCustomFieldSelect
    	share/html/Helpers/Autocomplete/Groups
    	share/html/Helpers/Autocomplete/Users
    	share/html/Search/Build.html
    	share/html/Search/Elements/PickCFs
    	share/html/Search/Elements/PickCriteria
    	t/web/case-sensitivity.t

diff --cc etc/upgrade/3.9.8/content
index 9ade6a1,24242fd..0305aa9
--- a/etc/upgrade/3.9.8/content
+++ b/etc/upgrade/3.9.8/content
@@@ -1,12 -1,6 +1,9 @@@
 - at Initial = sub {
 +use strict;
 +use warnings;
 +
 +our @Initial = sub {
-     my $dbh = RT->DatabaseHandle->dbh;
-     my $sth = $dbh->table_info( '', undef, undef, "'TABLE'");
      my $found_fm_tables = {};
-     while ( my $table = $sth->fetchrow_hashref ) {
-         my $name = $table->{TABLE_NAME} || $table->{table_name};
+     foreach my $name ( $RT::Handle->_TableNames ) {
          next unless $name =~ /^fm_/i;
          $found_fm_tables->{lc $name}++;
      }
@@@ -14,11 -8,12 +11,12 @@@
      return unless %$found_fm_tables;
  
      unless ( $found_fm_tables->{fm_topics} && $found_fm_tables->{fm_objecttopics} ) {
 -        $RT::Logger->error("You appear to be upgrading from RTFM 2.0 - We don't support upgrading this old of an RTFM yet");
 +        RT->Logger->error("You appear to be upgrading from RTFM 2.0 - We don't support upgrading this old of an RTFM yet");
      }
  
 -    $RT::Logger->error("We found RTFM tables in your database.  Checking for content.");
 +    RT->Logger->error("We found RTFM tables in your database.  Checking for content.");
  
+     my $dbh = $RT::Handle->dbh;
      my $result = $dbh->selectall_arrayref("SELECT count(*) AS articlecount FROM FM_Articles", { Slice => {} } );
  
      if ($result->[0]{articlecount} > 0) {
diff --cc lib/RT/CustomField.pm
index ede4b5c,13eb7ef..113c75f
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@@ -1132,9 -1176,15 +1132,9 @@@ Returns an array of LookupTypes availab
  
  sub LookupTypes {
      my $self = shift;
-     return keys %FRIENDLY_LOOKUP_TYPES;
 -    return sort keys %FRIENDLY_OBJECT_TYPES;
++    return sort keys %FRIENDLY_LOOKUP_TYPES;
  }
  
 -my @FriendlyObjectTypes = (
 -    "[_1] objects",            # loc
 -    "[_1]'s [_2] objects",        # loc
 -    "[_1]'s [_2]'s [_3] objects",   # loc
 -);
 -
  =head2 FriendlyLookupType
  
  Returns a localized description of the type of this custom field
diff --cc lib/RT/Handle.pm
index 99363b0,ca6f2e4..ccc24ed
--- a/lib/RT/Handle.pm
+++ b/lib/RT/Handle.pm
@@@ -1269,14 -1203,32 +1269,39 @@@ sub _LogSQLStatement 
      push @{$self->{'StatementLog'}} , ([Time::HiRes::time(), $statement, [@bind], $duration, HTML::Mason::Exception->new->as_string]);
  }
  
 +# helper in a few cases where we do SQL by hand
 +sub __MakeClauseCaseInsensitive {
 +    my $self = shift;
 +    return join ' ', @_ unless $self->CaseSensitive;
 +    my ($field, $op, $value) = $self->_MakeClauseCaseInsensitive(@_);
 +    return "$field $op $value";
 +}
  
+ sub _TableNames {
+     my $self = shift;
+     my $dbh = shift || $self->dbh;
+ 
+     {
+         local $@;
+         if (
+             $dbh->{Driver}->{Name} eq 'Pg'
+             && $dbh->{'pg_server_version'} >= 90200
+             && !eval { DBD::Pg->VERSION('2.19.3'); 1 }
+         ) {
+             die "You're using PostgreSQL 9.2 or newer. You have to upgrade DBD::Pg module to 2.19.3 or newer: $@";
+         }
+     }
+ 
+     my @res;
+ 
+     my $sth = $dbh->table_info( '', undef, undef, "'TABLE'");
+     while ( my $table = $sth->fetchrow_hashref ) {
+         push @res, $table->{TABLE_NAME} || $table->{table_name};
+     }
+ 
+     return @res;
+ }
+ 
  __PACKAGE__->FinalizeDatabaseType;
  
  RT::Base->_ImportOverlays();
diff --cc lib/RT/Test/Shredder.pm
index dfbc90b,0000000..13c56d9
mode 100644,000000..100644
--- a/lib/RT/Test/Shredder.pm
+++ b/lib/RT/Test/Shredder.pm
@@@ -1,341 -1,0 +1,340 @@@
 +# BEGIN BPS TAGGED BLOCK {{{
 +#
 +# COPYRIGHT:
 +#
 +# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
 +#                                          <sales at bestpractical.com>
 +#
 +# (Except where explicitly superseded by other copyright notices)
 +#
 +#
 +# LICENSE:
 +#
 +# This work is made available to you under the terms of Version 2 of
 +# the GNU General Public License. A copy of that license should have
 +# been provided with this software, but in any event can be snarfed
 +# from www.gnu.org.
 +#
 +# This work is distributed in the hope that it will be useful, but
 +# WITHOUT ANY WARRANTY; without even the implied warranty of
 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 +# General Public License for more details.
 +#
 +# You should have received a copy of the GNU General Public License
 +# along with this program; if not, write to the Free Software
 +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 +# 02110-1301 or visit their web page on the internet at
 +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
 +#
 +#
 +# CONTRIBUTION SUBMISSION POLICY:
 +#
 +# (The following paragraph is not intended to limit the rights granted
 +# to you to modify and distribute this software under the terms of
 +# the GNU General Public License and is only of importance to you if
 +# you choose to contribute your changes and enhancements to the
 +# community by submitting them to Best Practical Solutions, LLC.)
 +#
 +# By intentionally submitting any modifications, corrections or
 +# derivatives to this work, or any other work intended for use with
 +# Request Tracker, to Best Practical Solutions, LLC, you confirm that
 +# you are the copyright holder for those contributions and you grant
 +# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
 +# royalty-free, perpetual, license to use, copy, create derivative
 +# works based on those contributions, and sublicense and distribute
 +# those contributions and any derivatives thereof.
 +#
 +# END BPS TAGGED BLOCK }}}
 +
 +use strict;
 +use warnings;
 +
 +package RT::Test::Shredder;
 +use base 'RT::Test';
 +
 +require File::Copy;
 +require Cwd;
 +
 +=head1 DESCRIPTION
 +
 +RT::Shredder test suite utilities
 +
 +=head1 TESTING
 +
 +Since RT:Shredder 0.01_03 we have a test suite. You
 +can run tests and see if everything works as expected
 +before you try shredder on your actual data.
 +Tests also help in the development process.
 +
 +The test suite uses SQLite databases to store data in individual files,
 +so you could sun tests on your production servers without risking
 +damage to your production data.
 +
 +You'll want to run the test suite almost every time you install or update
 +the shredder distribution, especialy if you have local customizations of
 +the DB schema and/or RT code.
 +
 +Tests are one thing you can write even if you don't know much perl,
 +but want to learn more about RT's internals. New tests are very welcome.
 +
 +=head2 WRITING TESTS
 +
 +The shredder distribution has several files to help write new tests.
 +
 +  t/shredder/utils.pl - this file, utilities
 +  t/00skeleton.t - skeleteton .t file for new tests
 +
 +All tests follow this algorithm:
 +
 +  require "t/shredder/utils.pl"; # plug in utilities
 +  init_db(); # create new tmp RT DB and init RT API
 +  # create RT data you want to be always in the RT DB
 +  # ...
 +  create_savepoint('mysp'); # create DB savepoint
 +  # create data you want delete with shredder
 +  # ...
 +  # run shredder on the objects you've created
 +  # ...
 +  # check that shredder deletes things you want
 +  # this command will compare savepoint DB with current
 +  cmp_deeply( dump_current_and_savepoint('mysp'), "current DB equal to savepoint");
 +  # then you can create another object and delete it, then check again
 +
 +Savepoints are named and you can create two or more savepoints.
 +
 +=cut
 +
 +sub import {
 +    my $class = shift;
 +    $class->SUPER::import(@_);
 +    $class->export_to_level(1);
 +}
 +
 +=head1 FUNCTIONS
 +
 +=head2 RT CONFIG
 +
 +=head3 rewrite_rtconfig
 +
 +Call this sub after C<RT::LoadConfig>. It changes the RT config
 +options necessary to switch to a local SQLite database.
 +
 +=cut
 +
 +sub bootstrap_more_config {
 +    my $self = shift;
 +    my $config = shift;
 +
 +    print $config <<'END';
 +Set($DatabaseType       , 'SQLite');
 +Set($DatabaseHost       , 'localhost' );
 +Set($DatabaseRTHost     , 'localhost' );
 +Set($DatabasePort       , '' );
 +END
 +
 +    print $config "Set(\$DatabaseName, '". $self->db_name ."');\n";
 +    return;
 +}
 +
 +=head2 DATABASES
 +
 +=head3 db_name
 +
 +Returns the absolute file path to the current DB.
 +It is C<<RT::Test->temp_directory . 'main.db'>>.
 +
 +=cut
 +
 +sub db_name { return File::Spec->catfile((shift)->temp_directory, "main.db") }
 +
 +=head3 connect_sqlite
 +
 +Returns connected DBI DB handle.
 +
 +Takes path to sqlite db.
 +
 +=cut
 +
 +sub connect_sqlite
 +{
 +    my $self = shift;
 +    return DBI->connect("dbi:SQLite:dbname=". shift, "", "");
 +}
 +
 +=head2 SHREDDER
 +
 +=head3 shredder_new
 +
 +Creates and returns a new RT::Shredder object.
 +
 +=cut
 +
 +sub shredder_new
 +{
 +    my $self = shift;
 +
 +    require RT::Shredder;
 +    my $obj = RT::Shredder->new;
 +
 +    my $file = File::Spec->catfile( $self->temp_directory, 'dump.XXXX.sql' );
 +    $obj->AddDumpPlugin( Arguments => {
 +        file_name    => $file,
 +        from_storage => 0,
 +    } );
 +
 +    return $obj;
 +}
 +
 +
 +=head2 SAVEPOINTS
 +
 +=head3 savepoint_name
 +
 +Returns the absolute path to the named savepoint DB file.
 +Takes one argument - savepoint name, by default C<sp>.
 +
 +=cut
 +
 +sub savepoint_name
 +{
 +    my $self  = shift;
 +    my $name = shift || 'default';
 +    return File::Spec->catfile( $self->temp_directory, "sp.$name.db" );
 +}
 +
 +=head3 create_savepoint
 +
 +Creates savepoint DB from the current DB.
 +Takes name of the savepoint as argument.
 +
 +=head3 restore_savepoint
 +
 +Restores current DB to savepoint state.
 +Takes name of the savepoint as argument.
 +
 +=cut
 +
 +sub create_savepoint {
 +    my $self = shift;
 +    return $self->__cp_db( $self->db_name => $self->savepoint_name( shift ) );
 +}
 +sub restore_savepoint {
 +    my $self = shift;
 +    return $self->__cp_db( $self->savepoint_name( shift ) => $self->db_name );
 +}
 +sub __cp_db
 +{
 +    my $self  = shift;
 +    my( $orig, $dest ) = @_;
 +    RT::Test::__disconnect_rt();
 +    File::Copy::copy( $orig, $dest ) or die "Couldn't copy '$orig' => '$dest': $!";
 +    RT::Test::__reconnect_rt();
 +    return;
 +}
 +
 +
 +=head2 DUMPS
 +
 +=head3 dump_sqlite
 +
 +Returns DB dump as a complex hash structure:
 +    {
 +    TableName => {
 +        #id => {
 +            lc_field => 'value',
 +        }
 +    }
 +    }
 +
 +Takes named argument C<CleanDates>. If true, clean all date fields from
 +dump. True by default.
 +
 +=cut
 +
 +sub dump_sqlite
 +{
 +    my $self = shift;
 +    my $dbh = shift;
 +    my %args = ( CleanDates => 1, @_ );
 +
 +    my $old_fhkn = $dbh->{'FetchHashKeyName'};
 +    $dbh->{'FetchHashKeyName'} = 'NAME_lc';
 +
-     my $sth = $dbh->table_info( '', '%', '%', 'TABLE' ) || die $DBI::err;
-     my @tables = keys %{$sth->fetchall_hashref( 'table_name' )};
++    my @tables = $RT::Handle->_TableNames( $dbh );
 +
 +    my $res = {};
 +    foreach my $t( @tables ) {
 +        next if lc($t) eq 'sessions';
 +        $res->{$t} = $dbh->selectall_hashref(
 +            "SELECT * FROM $t". $self->dump_sqlite_exceptions($t), 'id'
 +        );
 +        $self->clean_dates( $res->{$t} ) if $args{'CleanDates'};
 +        die $DBI::err if $DBI::err;
 +    }
 +
 +    $dbh->{'FetchHashKeyName'} = $old_fhkn;
 +    return $res;
 +}
 +
 +=head3 dump_sqlite_exceptions
 +
 +If there are parts of the DB which can change from creating and deleting
 +a queue, skip them when doing the comparison.  One example is the global
 +queue cache attribute on RT::System which will be updated on Queue creation
 +and can't be rolled back by the shredder.  It may actually make sense for
 +Shredder to be updating this at some point in the future.
 +
 +=cut
 +
 +sub dump_sqlite_exceptions {
 +    my $self = shift;
 +    my $table = shift;
 +
 +    my $special_wheres = {
 +        attributes => " WHERE Name != 'QueueCacheNeedsUpdate'"
 +    };
 +
 +    return $special_wheres->{lc $table}||'';
 +
 +}
 +
 +=head3 dump_current_and_savepoint
 +
 +Returns dump of the current DB and of the named savepoint.
 +Takes one argument - savepoint name.
 +
 +=cut
 +
 +sub dump_current_and_savepoint
 +{
 +    my $self = shift;
 +    my $orig = $self->savepoint_name( shift );
 +    die "Couldn't find savepoint file" unless -f $orig && -r _;
 +    my $odbh = $self->connect_sqlite( $orig );
 +    return ( $self->dump_sqlite( $RT::Handle->dbh, @_ ), $self->dump_sqlite( $odbh, @_ ) );
 +}
 +
 +=head3 dump_savepoint_and_current
 +
 +Returns the same data as C<dump_current_and_savepoint> function,
 +but in reversed order.
 +
 +=cut
 +
 +sub dump_savepoint_and_current { return reverse (shift)->dump_current_and_savepoint(@_) }
 +
 +sub clean_dates
 +{
 +    my $self = shift;
 +    my $h = shift;
 +    my $date_re = qr/^\d\d\d\d\-\d\d\-\d\d\s*\d\d\:\d\d(\:\d\d)?$/i;
 +    foreach my $id ( keys %{ $h } ) {
 +        next unless $h->{ $id };
 +        foreach ( keys %{ $h->{ $id } } ) {
 +            delete $h->{$id}{$_} if $h->{$id}{$_} &&
 +              $h->{$id}{$_} =~ /$date_re/;
 +        }
 +    }
 +}
 +
 +1;
diff --cc lib/RT/Transaction.pm
index a7fe4c8,eab7192..81a3dc7
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@@ -870,14 -795,52 +871,52 @@@ sub _FormatUser 
          my $new = $self->NewValue;
          my $old = $self->OldValue;
  
+         if ( $cf ) {
+ 
+             if ( $cf->Type eq 'DateTime' ) {
+                 if ($old) {
+                     my $date = RT::Date->new( $self->CurrentUser );
+                     $date->Set( Format => 'ISO', Value => $old );
+                     $old = $date->AsString;
+                 }
+ 
+                 if ($new) {
+                     my $date = RT::Date->new( $self->CurrentUser );
+                     $date->Set( Format => 'ISO', Value => $new );
+                     $new = $date->AsString;
+                 }
+             }
+             elsif ( $cf->Type eq 'Date' ) {
+                 if ($old) {
+                     my $date = RT::Date->new( $self->CurrentUser );
+                     $date->Set(
+                         Format   => 'unknown',
+                         Value    => $old,
+                         Timezone => 'UTC',
+                     );
+                     $old = $date->AsString( Time => 0, Timezone => 'UTC' );
+                 }
+ 
+                 if ($new) {
+                     my $date = RT::Date->new( $self->CurrentUser );
+                     $date->Set(
+                         Format   => 'unknown',
+                         Value    => $new,
+                         Timezone => 'UTC',
+                     );
+                     $new = $date->AsString( Time => 0, Timezone => 'UTC' );
+                 }
+             }
+         }
+ 
          if ( !defined($old) || $old eq '' ) {
 -            return $self->loc("[_1] [_2] added", $field, $new);
 +            return ("[_1] [_2] added", $field, $new);   #loc
          }
          elsif ( !defined($new) || $new eq '' ) {
 -            return $self->loc("[_1] [_2] deleted", $field, $old);
 +            return ("[_1] [_2] deleted", $field, $old); #loc
          }
          else {
 -            return $self->loc("[_1] [_2] changed to [_3]", $field, $old, $new);
 +            return ("[_1] [_2] changed to [_3]", $field, $old, $new);   #loc
          }
      },
      Untake => sub {
diff --cc share/html/Elements/EditCustomFieldSelect
index cf10cc8,6cceb5a..5d1ceed
--- a/share/html/Elements/EditCustomFieldSelect
+++ b/share/html/Elements/EditCustomFieldSelect
@@@ -68,10 -70,10 +68,10 @@@ jQuery(  function () 
      if (basedon != null) {
          var oldchange = basedon.onchange;
          basedon.onchange = function () {
 -            filter_cascade(
 +            filter_cascade_by_id(
                  <% "$id-Values" |n,j%>,
-                 basedon.value,
+                 jQuery(basedon).val(),
 -                1
 +                true
              );
              if (oldchange != null)
                  oldchange();
diff --cc share/html/Search/Bulk.html
index d62828a,5cc4000..eb03b7c
--- a/share/html/Search/Bulk.html
+++ b/share/html/Search/Bulk.html
@@@ -282,13 -292,11 +281,12 @@@ unless ( $ARGS{'AddMoreAttach'} ) 
  
          #Update the links
          $ARGS{'id'} = $Ticket->id;
-         $queues{ $Ticket->QueueObj->Id }++;
  
          my @updateresults = ProcessUpdateMessage(
 -                TicketObj => $Ticket,
 -                ARGSRef   => \%ARGS,
 -            );
 +            TicketObj       => $Ticket,
 +            ARGSRef         => \%ARGS,
 +            KeepAttachments => 1,
 +        );
  
          #Update the basics.
          my @basicresults =
diff --cc share/static/js/cascaded.js
index 25b259a,0000000..065a398
mode 100644,000000..100644
--- a/share/static/js/cascaded.js
+++ b/share/static/js/cascaded.js
@@@ -1,60 -1,0 +1,91 @@@
- function filter_cascade_by_id (id, val, is_hierarchical) {
++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, val, is_hierarchical);
++    return filter_cascade(select, complete_select, vals, is_hierarchical);
 +}
 +
- function filter_cascade (select, complete_select, val, is_hierarchical) {
++function filter_cascade (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;
 +
-         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 in complete_children) {
-                 if ( complete_children[i].cloneNode ) {
-                     new_option = complete_children[i].cloneNode(true);
-                     select.appendChild(new_option);
++        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 {
-             for (i in complete_children) {
-                 if (!complete_children[i].label ||
-                       (complete_children[i].hasAttribute &&
-                             !complete_children[i].hasAttribute('label') ) ||
-                         complete_children[i].label === val ) {
++            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 ) {
-                         new_option = complete_children[i].cloneNode(true);
++                        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 in children) {
++        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/util.js
index bd78392,0000000..50df9f9
mode 100644,000000..100644
--- a/share/static/js/util.js
+++ b/share/static/js/util.js
@@@ -1,340 -1,0 +1,350 @@@
 +/* 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;
 +}
 +
 +/* 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').attr('checked');
 +    var elements = jQuery('#shredder-search-form :checkbox[name=WipeoutObject]');
 +
 +    if( check ) {
 +        elements.attr('checked', true);
 +    } else {
 +        elements.attr('checked', false);
 +    }
 +}
 +
 +function checkboxToInput(target,checkbox,val){    
 +    var tar = jQuery('#' + escapeCssSelector(target));
 +    var box = jQuery('#' + escapeCssSelector(checkbox));
 +    if(box.attr('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) {
 +    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) {
 +                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);
 +
 +
 +            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;
 +        });
 +    });
 +});

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


More information about the Rt-commit mailing list