[Rt-commit] rt branch, 4.4/canonicalize-custom-fields, created. rt-4.4.0-233-g3d339db

Shawn Moore shawn at bestpractical.com
Fri May 27 17:35:58 EDT 2016


The branch, 4.4/canonicalize-custom-fields has been created
        at  3d339db96c00b480ff3c2dab5bdbcdcfb8ed5d0d (commit)

- Log -----------------------------------------------------------------
commit a851ab5453db3baaabd2350c43afa39cb57615a4
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 17 16:03:09 2016 -0400

    Limit ExternalAuth enabled setting to ExternalSettings
    
    While ExternalAuthPriority and ExternalInfoPriority are
    important, the ExternalSettings config is essential to making
    ExternalAuth work, so automatically enable it based on
    the presence of that configuration setting only.

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 6c2c842..8fef004 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1078,8 +1078,6 @@ our %META;
             my $self = shift;
             my @values = @{ shift || [] };
 
-            $self->EnableExternalAuth() if @values;
-
             if (not @values) {
                 $RT::Logger->debug("ExternalAuthPriority not defined. Attempting to create based on ExternalSettings");
                 $self->Set( 'ExternalAuthPriority', \@values );
@@ -1106,8 +1104,6 @@ our %META;
             my $self = shift;
             my @values = @{ shift || [] };
 
-            $self->EnableExternalAuth() if @values;
-
             if (not @values) {
                 $RT::Logger->debug("ExternalInfoPriority not defined. User information (including user enabled/disabled) cannot be externally-sourced");
                 $self->Set( 'ExternalInfoPriority', \@values );

commit 434b36bea5b34799713a87832c927d4c87409472
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Tue May 17 16:06:49 2016 -0400

    Only attempt to set External Info/Auth if values are available
    
    The ExternalAuthPriority and ExternalInfoPriority options
    attempt to set values based on configuration from the
    ExternalSettings configuration option if no values are passed
    explicitly. If no values are passed and the ExternalSettings
    option is not set, bail and don't issue debug messages since
    the user clearly isn't using ExternalAuth.

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 8fef004..70df38f 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1078,6 +1078,8 @@ our %META;
             my $self = shift;
             my @values = @{ shift || [] };
 
+            return unless @values or $self->Get('ExternalSettings');
+
             if (not @values) {
                 $RT::Logger->debug("ExternalAuthPriority not defined. Attempting to create based on ExternalSettings");
                 $self->Set( 'ExternalAuthPriority', \@values );
@@ -1104,6 +1106,8 @@ our %META;
             my $self = shift;
             my @values = @{ shift || [] };
 
+            return unless @values or $self->Get('ExternalSettings');
+
             if (not @values) {
                 $RT::Logger->debug("ExternalInfoPriority not defined. User information (including user enabled/disabled) cannot be externally-sourced");
                 $self->Set( 'ExternalInfoPriority', \@values );

commit 9f2d35241c9058f5235efa349fc395b99971bbe4
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 20 20:06:31 2016 +0000

    Add UniqueValues column to Custom Fields
    
        It's not enforced yet, but it has an Admin UI checkbox

diff --git a/etc/schema.Oracle b/etc/schema.Oracle
index 16ae12b..9a75f4b 100644
--- a/etc/schema.Oracle
+++ b/etc/schema.Oracle
@@ -357,6 +357,7 @@ CREATE TABLE CustomFields (
         SortOrder       NUMBER(11,0) DEFAULT 0 NOT NULL,
         LookupType      VARCHAR2(255),
         EntryHint       VARCHAR2(255) NULL,
+        UniqueValues    NUMBER(11,0) DEFAULT 0 NOT NULL,
         Creator         NUMBER(11,0) DEFAULT 0 NOT NULL,
         Created         DATE,
         LastUpdatedBy   NUMBER(11,0) DEFAULT 0 NOT NULL,
diff --git a/etc/schema.Pg b/etc/schema.Pg
index b23dbf0..1c4f5a8 100644
--- a/etc/schema.Pg
+++ b/etc/schema.Pg
@@ -541,6 +541,7 @@ CREATE TABLE CustomFields (
   EntryHint varchar(255) NULL,
   Description varchar(255) NULL  ,
   SortOrder integer NOT NULL DEFAULT 0  ,
+  UniqueValues integer NOT NULL DEFAULT 0 ,
 
   Creator integer NOT NULL DEFAULT 0  ,
   Created TIMESTAMP NULL  ,
diff --git a/etc/schema.SQLite b/etc/schema.SQLite
index acf3b3f..4080598 100644
--- a/etc/schema.SQLite
+++ b/etc/schema.SQLite
@@ -387,6 +387,7 @@ CREATE TABLE CustomFields (
   SortOrder integer NOT NULL DEFAULT 0  ,
   LookupType varchar(255) collate NOCASE NOT NULL,
   EntryHint varchar(255) NULL,
+  UniqueValues int2 NOT NULL DEFAULT 0,
 
   Creator integer NOT NULL DEFAULT 0  ,
   Created DATETIME NULL  ,
diff --git a/etc/schema.mysql b/etc/schema.mysql
index 9b239ad..f03862d 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -359,6 +359,7 @@ CREATE TABLE CustomFields (
   SortOrder integer NOT NULL DEFAULT 0  ,
   LookupType varchar(255) CHARACTER SET ascii NOT NULL,
   EntryHint varchar(255) NULL,
+  UniqueValues int2 NOT NULL DEFAULT 0 ,
 
   Creator integer NOT NULL DEFAULT 0  ,
   Created DATETIME NULL  ,
diff --git a/etc/upgrade/4.4.2/schema.Oracle b/etc/upgrade/4.4.2/schema.Oracle
new file mode 100644
index 0000000..b7ce195
--- /dev/null
+++ b/etc/upgrade/4.4.2/schema.Oracle
@@ -0,0 +1 @@
+ALTER TABLE CustomFields ADD UniqueValues NUMBER(11,0) DEFAULT 0 NOT NULL;
diff --git a/etc/upgrade/4.4.2/schema.Pg b/etc/upgrade/4.4.2/schema.Pg
new file mode 100644
index 0000000..6da8b60
--- /dev/null
+++ b/etc/upgrade/4.4.2/schema.Pg
@@ -0,0 +1 @@
+ALTER TABLE CustomFields ADD COLUMN UniqueValues integer NOT NULL DEFAULT 0;
diff --git a/etc/upgrade/4.4.2/schema.SQLite b/etc/upgrade/4.4.2/schema.SQLite
new file mode 100644
index 0000000..33540e7
--- /dev/null
+++ b/etc/upgrade/4.4.2/schema.SQLite
@@ -0,0 +1 @@
+ALTER TABLE CustomFields ADD COLUMN UniqueValues int2 NOT NULL DEFAULT 0;
diff --git a/etc/upgrade/4.4.2/schema.mysql b/etc/upgrade/4.4.2/schema.mysql
new file mode 100644
index 0000000..33540e7
--- /dev/null
+++ b/etc/upgrade/4.4.2/schema.mysql
@@ -0,0 +1 @@
+ALTER TABLE CustomFields ADD COLUMN UniqueValues int2 NOT NULL DEFAULT 0;
diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index ee56a25..328fcc5 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -246,16 +246,17 @@ C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType
 sub Create {
     my $self = shift;
     my %args = (
-        Name        => '',
-        Type        => '',
-        MaxValues   => 0,
-        Pattern     => '',
-        Description => '',
-        Disabled    => 0,
-        LookupType  => '',
-        LinkValueTo => '',
+        Name         => '',
+        Type         => '',
+        MaxValues    => 0,
+        Pattern      => '',
+        Description  => '',
+        Disabled     => 0,
+        LookupType   => '',
+        LinkValueTo  => '',
         IncludeContentForValue => '',
-        EntryHint   => undef,
+        EntryHint    => undef,
+        UniqueValues => 0,
         @_,
     );
 
@@ -328,16 +329,17 @@ sub Create {
     $args{'Disabled'} ||= 0;
 
     (my $rv, $msg) = $self->SUPER::Create(
-        Name        => $args{'Name'},
-        Type        => $args{'Type'},
-        RenderType  => $args{'RenderType'},
-        MaxValues   => $args{'MaxValues'},
-        Pattern     => $args{'Pattern'},
-        BasedOn     => $args{'BasedOn'},
-        ValuesClass => $args{'ValuesClass'},
-        Description => $args{'Description'},
-        Disabled    => $args{'Disabled'},
-        LookupType  => $args{'LookupType'},
+        Name         => $args{'Name'},
+        Type         => $args{'Type'},
+        RenderType   => $args{'RenderType'},
+        MaxValues    => $args{'MaxValues'},
+        Pattern      => $args{'Pattern'},
+        BasedOn      => $args{'BasedOn'},
+        ValuesClass  => $args{'ValuesClass'},
+        Description  => $args{'Description'},
+        Disabled     => $args{'Disabled'},
+        LookupType   => $args{'LookupType'},
+        UniqueValues => $args{'UniqueValues'},
     );
 
     if ($rv) {
@@ -2448,6 +2450,8 @@ sub _CoreAccessible {
         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
         EntryHint =>
         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0, is_numeric => 0,  type => 'varchar(255)', default => undef },
+        UniqueValues =>
+        {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
         Creator => 
         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
         Created => 
diff --git a/share/html/Admin/CustomFields/Modify.html b/share/html/Admin/CustomFields/Modify.html
index f9a7324..4c357ee 100644
--- a/share/html/Admin/CustomFields/Modify.html
+++ b/share/html/Admin/CustomFields/Modify.html
@@ -163,6 +163,12 @@ jQuery( function() {
 </td></tr>
 % }
 
+<tr><td class="label"> </td><td>
+<input type="hidden" class="hidden" name="SetUniqueValues" value="1" />
+<input type="checkbox" class="checkbox" id="UniqueValues" name="UniqueValues" value="1" <% $UniqueValuesChecked |n %> />
+<label for="UniqueValues"><&|/l&>Values must be unique</&></label>
+</td></tr>
+
 % $m->callback(CallbackName => 'BeforeEnabled', CustomField => $CustomFieldObj, CFvalidations => \@CFvalidations);
 
 <tr><td class="label"> </td><td>
@@ -211,6 +217,7 @@ else {
             BasedOn       => $BasedOn,
             Disabled      => ($Enabled ? 0 : 1),
             EntryHint     => $EntryHint,
+            UniqueValues  => $UniqueValues,
         );
         if (!$val) {
             push @results, loc("Could not create CustomField: [_1]", $msg);
@@ -233,7 +240,11 @@ if ( $ARGS{'Update'} && $id ne 'new' ) {
     #we're asking about enabled on the web page but really care about disabled.
     $ARGS{'Disabled'} = $Enabled? 0 : 1;
 
-    my @attribs = qw(Disabled Pattern Name TypeComposite LookupType Description LinkValueTo IncludeContentForValue EntryHint);
+    # make sure the unchecked checkbox still causes an update
+    $ARGS{UniqueValues} ||= 0 if $SetUniqueValues;
+
+    my @attribs = qw(Disabled Pattern Name TypeComposite LookupType Description LinkValueTo IncludeContentForValue EntryHint UniqueValues);
+
     push @results, UpdateRecordObject(
         AttributesRef => \@attribs,
         Object        => $CustomFieldObj,
@@ -354,6 +365,9 @@ MaybeRedirectForResults(
 my $EnabledChecked = qq[checked="checked"];
 $EnabledChecked = '' if $CustomFieldObj->Disabled;
 
+my $UniqueValuesChecked = qq[checked="checked"];
+$UniqueValuesChecked = '' if !$CustomFieldObj->UniqueValues;
+
 my @CFvalidations = (
     '(?#Mandatory).',
     '(?#Digits)^[\d.]+$',
@@ -374,6 +388,8 @@ $Pattern => undef
 $Name => undef
 $SetEnabled => undef
 $Enabled => 0
+$SetUniqueValues => undef
+$UniqueValues => 0
 $ValuesClass => 'RT::CustomFieldValues'
 $RenderType => undef
 $LinkValueTo => undef

commit b522bba765e05327e00c793b535199996d85fa97
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 20 20:14:30 2016 +0000

    Enforce CustomField UniqueValues

diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index 328fcc5..1c0f715 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -1683,6 +1683,22 @@ sub AddValueForObject {
         }
     }
 
+    if ($self->UniqueValues) {
+        my $existing = RT::ObjectCustomFieldValue->new(RT->SystemUser);
+        $existing->LoadByCols(
+            CustomField  => $self->Id,
+            Content      => $args{'Content'},
+            LargeContent => $args{'LargeContent'},
+            ContentType  => $args{'ContentType'},
+            Disabled     => 0,
+        );
+        if ($existing->Id) {
+            $RT::Logger->debug( "Non-unique custom field value for CF #" . $self->Id ." with object custom field value " . $existing->Id );
+            $RT::Handle->Rollback();
+            return ( 0, $self->loc('That is not a unique value') );
+        }
+    }
+
     my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
     my ($val, $msg) = $newval->Create(
         ObjectType   => ref($obj),

commit 49134782626d1ebf2032f5a12545bb3307f9250d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 20 20:32:41 2016 +0000

    Basic tests for unique custom field values

diff --git a/t/customfields/unique_values.t b/t/customfields/unique_values.t
new file mode 100644
index 0000000..9751cea
--- /dev/null
+++ b/t/customfields/unique_values.t
@@ -0,0 +1,51 @@
+use warnings;
+use strict;
+
+use RT::Test tests => undef;
+
+
+my $alpha = RT::Test->create_ticket( Subject => 'test unique values alpha', Queue => 'General' );
+my $beta = RT::Test->create_ticket( Subject => 'test unique values beta', Queue => 'General' );
+my ( $ret, $msg );
+
+{
+    diag "testing freeform single cf";
+    my $unique_single = RT::Test->load_or_create_custom_field(
+        Name         => 'unique single',
+        Type         => 'FreeformSingle',
+        Queue        => 0,
+        UniqueValues => 1,
+    );
+    ok($unique_single->UniqueValues, 'unique values for this CF');
+
+    ( $ret, $msg ) =
+      $alpha->AddCustomFieldValue( Field => $unique_single, Value => 'foo' );
+    ok( $ret, $msg );
+    is( $alpha->FirstCustomFieldValue($unique_single), 'foo', 'value is foo' );
+
+    ( $ret, $msg ) =
+      $beta->AddCustomFieldValue( Field => $unique_single, Value => 'foo' );
+    ok( !$ret, "can't reuse the OCFV 'foo'");
+    like($msg, qr/That is not a unique value/);
+    is( $beta->FirstCustomFieldValue($unique_single), undef, 'no value since it was a duplicate' );
+
+    ( $ret, $msg ) =
+      $alpha->AddCustomFieldValue( Field => $unique_single, Value => 'bar' );
+    ok( $ret, $msg );
+
+    is( $alpha->FirstCustomFieldValue($unique_single), 'bar', 'value is now bar' );
+
+    ( $ret, $msg ) =
+      $beta->AddCustomFieldValue( Field => $unique_single, Value => 'foo' );
+    ok( $ret, "can reuse foo since alpha switched away");
+    is( $beta->FirstCustomFieldValue($unique_single), 'foo', 'now beta has foo' );
+
+    ( $ret, $msg ) =
+      $alpha->AddCustomFieldValue( Field => $unique_single, Value => 'foo' );
+    ok( !$ret, "alpha can't switch back to foo since beta uses it");
+
+    is( $alpha->FirstCustomFieldValue($unique_single), 'bar', 'value is still bar' );
+}
+
+done_testing;
+

commit 7784c0422ac1b3497f8e3ab4ae956a3823609d82
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 27 19:21:13 2016 +0000

    Add CanonicalizeClass to CustomField table

diff --git a/etc/schema.Oracle b/etc/schema.Oracle
index 9a75f4b..afa2c94 100644
--- a/etc/schema.Oracle
+++ b/etc/schema.Oracle
@@ -344,25 +344,26 @@ CREATE INDEX ObjectCustomFieldValues2 ON ObjectCustomFieldValues (CustomField,Ob
 
 CREATE SEQUENCE CUSTOMFIELDS_seq;
 CREATE TABLE CustomFields (
-        id              NUMBER(11,0) 
+        id                NUMBER(11,0) 
                 CONSTRAINT CustomFields_Key PRIMARY KEY,
-        Name            VARCHAR2(200),
-        Type            VARCHAR2(200),
-        RenderType      VARCHAR2(64),
-        MaxValues       NUMBER(11,0) DEFAULT 0 NOT NULL,
-        Pattern         CLOB,
-        ValuesClass     VARCHAR2(64),
-        BasedOn         NUMBER(11,0) NULL,
-        Description     VARCHAR2(255),
-        SortOrder       NUMBER(11,0) DEFAULT 0 NOT NULL,
-        LookupType      VARCHAR2(255),
-        EntryHint       VARCHAR2(255) NULL,
-        UniqueValues    NUMBER(11,0) DEFAULT 0 NOT NULL,
-        Creator         NUMBER(11,0) DEFAULT 0 NOT NULL,
-        Created         DATE,
-        LastUpdatedBy   NUMBER(11,0) DEFAULT 0 NOT NULL,
-        LastUpdated     DATE,
-        Disabled        NUMBER(11,0) DEFAULT 0 NOT NULL
+        Name              VARCHAR2(200),
+        Type              VARCHAR2(200),
+        RenderType        VARCHAR2(64),
+        MaxValues         NUMBER(11,0) DEFAULT 0 NOT NULL,
+        Pattern           CLOB,
+        ValuesClass       VARCHAR2(64),
+        BasedOn           NUMBER(11,0) NULL,
+        Description       VARCHAR2(255),
+        SortOrder         NUMBER(11,0) DEFAULT 0 NOT NULL,
+        LookupType        VARCHAR2(255),
+        EntryHint         VARCHAR2(255) NULL,
+        UniqueValues      NUMBER(11,0) DEFAULT 0 NOT NULL,
+        CanonicalizeClass VARCHAR2(64),
+        Creator           NUMBER(11,0) DEFAULT 0 NOT NULL,
+        Created           DATE,
+        LastUpdatedBy     NUMBER(11,0) DEFAULT 0 NOT NULL,
+        LastUpdated       DATE,
+        Disabled          NUMBER(11,0) DEFAULT 0 NOT NULL
 );
 
 
diff --git a/etc/schema.Pg b/etc/schema.Pg
index 1c4f5a8..c758284 100644
--- a/etc/schema.Pg
+++ b/etc/schema.Pg
@@ -542,6 +542,7 @@ CREATE TABLE CustomFields (
   Description varchar(255) NULL  ,
   SortOrder integer NOT NULL DEFAULT 0  ,
   UniqueValues integer NOT NULL DEFAULT 0 ,
+  CanonicalizeClass varchar(64) NULL  ,
 
   Creator integer NOT NULL DEFAULT 0  ,
   Created TIMESTAMP NULL  ,
diff --git a/etc/schema.SQLite b/etc/schema.SQLite
index 4080598..3288a57 100644
--- a/etc/schema.SQLite
+++ b/etc/schema.SQLite
@@ -388,6 +388,7 @@ CREATE TABLE CustomFields (
   LookupType varchar(255) collate NOCASE NOT NULL,
   EntryHint varchar(255) NULL,
   UniqueValues int2 NOT NULL DEFAULT 0,
+  CanonicalizeClass varchar(64) collate NOCASE NULL  ,
 
   Creator integer NOT NULL DEFAULT 0  ,
   Created DATETIME NULL  ,
diff --git a/etc/schema.mysql b/etc/schema.mysql
index f03862d..4baf28d 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -360,6 +360,7 @@ CREATE TABLE CustomFields (
   LookupType varchar(255) CHARACTER SET ascii NOT NULL,
   EntryHint varchar(255) NULL,
   UniqueValues int2 NOT NULL DEFAULT 0 ,
+  CanonicalizeClass varchar(64) CHARACTER SET ascii NULL  ,
 
   Creator integer NOT NULL DEFAULT 0  ,
   Created DATETIME NULL  ,
diff --git a/etc/upgrade/4.4.2/schema.Oracle b/etc/upgrade/4.4.2/schema.Oracle
index b7ce195..095797e 100644
--- a/etc/upgrade/4.4.2/schema.Oracle
+++ b/etc/upgrade/4.4.2/schema.Oracle
@@ -1 +1,2 @@
 ALTER TABLE CustomFields ADD UniqueValues NUMBER(11,0) DEFAULT 0 NOT NULL;
+ALTER TABLE CustomFields ADD CanonicalizeClass VARCHAR2(64);
diff --git a/etc/upgrade/4.4.2/schema.Pg b/etc/upgrade/4.4.2/schema.Pg
index 6da8b60..ce1825d 100644
--- a/etc/upgrade/4.4.2/schema.Pg
+++ b/etc/upgrade/4.4.2/schema.Pg
@@ -1 +1,2 @@
 ALTER TABLE CustomFields ADD COLUMN UniqueValues integer NOT NULL DEFAULT 0;
+ALTER TABLE CustomFields ADD COLUMN CanonicalizeClass varchar(64) NULL;
diff --git a/etc/upgrade/4.4.2/schema.SQLite b/etc/upgrade/4.4.2/schema.SQLite
index 33540e7..4966485 100644
--- a/etc/upgrade/4.4.2/schema.SQLite
+++ b/etc/upgrade/4.4.2/schema.SQLite
@@ -1 +1,2 @@
 ALTER TABLE CustomFields ADD COLUMN UniqueValues int2 NOT NULL DEFAULT 0;
+ALTER TABLE CustomFields ADD COLUMN CanonicalizeClass varchar(64) collate NOCASE NULL;
diff --git a/etc/upgrade/4.4.2/schema.mysql b/etc/upgrade/4.4.2/schema.mysql
index 33540e7..e359ded 100644
--- a/etc/upgrade/4.4.2/schema.mysql
+++ b/etc/upgrade/4.4.2/schema.mysql
@@ -1 +1,2 @@
 ALTER TABLE CustomFields ADD COLUMN UniqueValues int2 NOT NULL DEFAULT 0;
+ALTER TABLE CustomFields ADD COLUMN CanonicalizeClass varchar(64) CHARACTER SET ascii NULL;
diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index 1c0f715..2318e53 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -2468,6 +2468,8 @@ sub _CoreAccessible {
         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0, is_numeric => 0,  type => 'varchar(255)', default => undef },
         UniqueValues =>
         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
+        CanonicalizeClass =>
+        {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
         Creator => 
         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
         Created => 

commit 1437cfd8b98b4de705697e7d9f54988c217a9cf0
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 27 19:31:37 2016 +0000

    Scaffolding for CustomField CanonicalizeClass

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 76c337d..3c62781 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -909,6 +909,19 @@ custom field values from external sources at runtime.
 
 Set(@CustomFieldValuesSources, ());
 
+=item C<@CustomFieldValuesCanonicalizers>
+
+Set C<@CustomFieldValuesCanonicalizers> to a list of class names which extend
+L<RT::CustomFieldValues::Canonicalizer>. This can be used to rewrite
+(canonicalize) values entered by users to fit some defined format.
+
+See the documentation in L<RT::CustomFieldValues::Canonicalizer> for adding
+your own canonicalizers.
+
+=cut
+
+Set(@CustomFieldValuesCanonicalizers, ());
+
 =item C<%CustomFieldGroupings>
 
 This option affects the display of ticket and user custom fields in the
diff --git a/lib/RT.pm b/lib/RT.pm
index ccf3c54..09d6873 100644
--- a/lib/RT.pm
+++ b/lib/RT.pm
@@ -484,6 +484,7 @@ sub InitClasses {
     require RT::Catalogs;
     require RT::Asset;
     require RT::Assets;
+    require RT::CustomFieldValues::Canonicalizer;
 
     _BuildTableAttributes();
 
diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 70df38f..9db2980 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -805,6 +805,7 @@ our %META;
     GnuPGOptions => { Type => 'HASH' },
     ReferrerWhitelist => { Type => 'ARRAY' },
     EmailDashboardLanguageOrder  => { Type => 'ARRAY' },
+    CustomFieldValuesCanonicalizers => { Type => 'ARRAY' },
     WebPath => {
         PostLoadCheck => sub {
             my $self  = shift;
diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index 2318e53..6bc30bd 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -70,6 +70,7 @@ our %FieldTypes = (
     Select => {
         sort_order => 10,
         selection_type => 1,
+        canonicalizes => 0,
 
         labels => [ 'Select multiple values',               # loc
                     'Select one value',                     # loc
@@ -93,6 +94,7 @@ our %FieldTypes = (
     Freeform => {
         sort_order => 20,
         selection_type => 0,
+        canonicalizes => 1,
 
         labels => [ 'Enter multiple values',               # loc
                     'Enter one value',                     # loc
@@ -102,6 +104,7 @@ our %FieldTypes = (
     Text => {
         sort_order => 30,
         selection_type => 0,
+        canonicalizes => 1,
         labels         => [
                     'Fill in multiple text areas',                   # loc
                     'Fill in one text area',                         # loc
@@ -111,6 +114,7 @@ our %FieldTypes = (
     Wikitext => {
         sort_order => 40,
         selection_type => 0,
+        canonicalizes => 1,
         labels         => [
                     'Fill in multiple wikitext areas',                       # loc
                     'Fill in one wikitext area',                             # loc
@@ -121,6 +125,7 @@ our %FieldTypes = (
     Image => {
         sort_order => 50,
         selection_type => 0,
+        canonicalizes => 0,
         labels         => [
                     'Upload multiple images',               # loc
                     'Upload one image',                     # loc
@@ -130,6 +135,7 @@ our %FieldTypes = (
     Binary => {
         sort_order => 60,
         selection_type => 0,
+        canonicalizes => 0,
         labels         => [
                     'Upload multiple files',              # loc
                     'Upload one file',                    # loc
@@ -140,6 +146,7 @@ our %FieldTypes = (
     Combobox => {
         sort_order => 70,
         selection_type => 1,
+        canonicalizes => 1,
         labels         => [
                     'Combobox: Select or enter multiple values',               # loc
                     'Combobox: Select or enter one value',                     # loc
@@ -149,6 +156,7 @@ our %FieldTypes = (
     Autocomplete => {
         sort_order => 80,
         selection_type => 1,
+        canonicalizes => 1,
         labels         => [
                     'Enter multiple values with autocompletion',               # loc
                     'Enter one value with autocompletion',                     # loc
@@ -159,6 +167,7 @@ our %FieldTypes = (
     Date => {
         sort_order => 90,
         selection_type => 0,
+        canonicalizes => 0,
         labels         => [
                     'Select multiple dates',              # loc
                     'Select date',                        # loc
@@ -168,6 +177,7 @@ our %FieldTypes = (
     DateTime => {
         sort_order => 100,
         selection_type => 0,
+        canonicalizes => 0,
         labels         => [
                     'Select multiple datetimes',                  # loc
                     'Select datetime',                            # loc
@@ -178,6 +188,7 @@ our %FieldTypes = (
     IPAddress => {
         sort_order => 110,
         selection_type => 0,
+        canonicalizes => 0,
 
         labels => [ 'Enter multiple IP addresses',                    # loc
                     'Enter one IP address',                           # loc
@@ -187,6 +198,7 @@ our %FieldTypes = (
     IPAddressRange => {
         sort_order => 120,
         selection_type => 0,
+        canonicalizes => 0,
 
         labels => [ 'Enter multiple IP address ranges',                          # loc
                     'Enter one IP address range',                                # loc
@@ -246,17 +258,18 @@ C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType
 sub Create {
     my $self = shift;
     my %args = (
-        Name         => '',
-        Type         => '',
-        MaxValues    => 0,
-        Pattern      => '',
-        Description  => '',
-        Disabled     => 0,
-        LookupType   => '',
-        LinkValueTo  => '',
+        Name                   => '',
+        Type                   => '',
+        MaxValues              => 0,
+        Pattern                => '',
+        Description            => '',
+        Disabled               => 0,
+        LookupType             => '',
+        LinkValueTo            => '',
         IncludeContentForValue => '',
-        EntryHint    => undef,
-        UniqueValues => 0,
+        EntryHint              => undef,
+        UniqueValues           => 0,
+        CanonicalizeClass      => undef,
         @_,
     );
 
@@ -326,20 +339,30 @@ sub Create {
         }
     }
 
+    if ( $args{'CanonicalizeClass'} ||= undef ) {
+        return (0, $self->loc("This custom field can not have a canonicalizer"))
+            unless $self->IsCanonicalizeType( $args{'Type'} );
+
+        unless ( $self->ValidateCanonicalizeClass( $args{'CanonicalizeClass'} ) ) {
+            return (0, $self->loc("Invalid custom field values canonicalizer"));
+        }
+    }
+
     $args{'Disabled'} ||= 0;
 
     (my $rv, $msg) = $self->SUPER::Create(
-        Name         => $args{'Name'},
-        Type         => $args{'Type'},
-        RenderType   => $args{'RenderType'},
-        MaxValues    => $args{'MaxValues'},
-        Pattern      => $args{'Pattern'},
-        BasedOn      => $args{'BasedOn'},
-        ValuesClass  => $args{'ValuesClass'},
-        Description  => $args{'Description'},
-        Disabled     => $args{'Disabled'},
-        LookupType   => $args{'LookupType'},
-        UniqueValues => $args{'UniqueValues'},
+        Name              => $args{'Name'},
+        Type              => $args{'Type'},
+        RenderType        => $args{'RenderType'},
+        MaxValues         => $args{'MaxValues'},
+        Pattern           => $args{'Pattern'},
+        BasedOn           => $args{'BasedOn'},
+        ValuesClass       => $args{'ValuesClass'},
+        Description       => $args{'Description'},
+        Disabled          => $args{'Disabled'},
+        LookupType        => $args{'LookupType'},
+        UniqueValues      => $args{'UniqueValues'},
+        CanonicalizeClass => $args{'CanonicalizeClass'},
     );
 
     if ($rv) {
@@ -730,7 +753,7 @@ sub Types {
 
 =head2 IsSelectionType 
 
-Retuns a boolean value indicating whether the C<Values> method makes sense
+Returns a boolean value indicating whether the C<Values> method makes sense
 to this Custom Field.
 
 =cut
@@ -742,6 +765,19 @@ sub IsSelectionType {
     return $FieldTypes{$type}->{selection_type};
 }
 
+=head2 IsCanonicalizeType
+
+Returns a boolean value indicating whether the type of this custom field
+permits using a canonicalizer.
+
+=cut
+
+sub IsCanonicalizeType {
+    my $self = shift;
+    my $type = @_? shift : $self->Type;
+    return undef unless $type;
+    return $FieldTypes{$type}->{canonicalizes};
+}
 
 
 =head2 IsExternalValues
@@ -785,6 +821,31 @@ sub ValidateValuesClass {
     return undef;
 }
 
+sub SetCanonicalizeClass {
+    my $self = shift;
+    my $class = shift;
+
+    if ( !$class ) {
+        return $self->_Set( Field => 'CanonicalizeClass', Value => undef, @_ );
+    }
+
+    return (0, $self->loc("This custom field can not have a canonicalizer"))
+        unless $self->IsCanonicalizeType;
+
+    unless ( $self->ValidateCanonicalizeClass( $class ) ) {
+        return (0, $self->loc("Invalid custom field values canonicalizer"));
+    }
+    return $self->_Set( Field => 'CanonicalizeClass', Value => $class, @_ );
+}
+
+sub ValidateCanonicalizeClass {
+    my $self = shift;
+    my $class = shift;
+
+    return 1 if !$class;
+    return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesCanonicalizers');
+    return undef;
+}
 
 =head2 FriendlyType [TYPE, MAX_VALUES]
 
diff --git a/lib/RT/CustomFieldValues/Canonicalizer.pm b/lib/RT/CustomFieldValues/Canonicalizer.pm
new file mode 100644
index 0000000..10a6b5d
--- /dev/null
+++ b/lib/RT/CustomFieldValues/Canonicalizer.pm
@@ -0,0 +1,117 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2016 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 }}}
+
+package RT::CustomFieldValues::Canonicalizer;
+
+use strict;
+use warnings;
+use base 'RT::Base';
+
+=head1 NAME
+
+RT::CustomFieldValues::Canonicalizer - base class for custom field value
+canonicalizers
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+This class is the base class for custom field value canonicalizers. To
+implement a new canonicalizer, you must create a new class that subclasses
+this class. Your subclass must implement the methods L</CanonicalizeValue>
+and L</Description> as documented below. Finally, add the new class name to
+L<RT_Config/@CustomFieldValuesCanonicalizers>.
+
+=head2 new
+
+The object constructor takes one argument: L<RT::CurrentUser> object.
+
+=cut
+
+sub new {
+    my $proto = shift;
+    my $class = ref($proto) || $proto;
+    my $self  = {};
+    bless ($self, $class);
+    $self->CurrentUser(@_);
+    return $self;
+}
+
+=head2 CanonicalizeValue
+
+Receives a parameter hash including C<CustomField> and C<Content>.
+
+You may also access C<< $self->CurrentUser >> in case you need the user's
+language or locale.
+
+This method is expected to return the canonicalized C<Content>.
+
+=cut
+
+sub CanonicalizeValue {
+    my $self = shift;
+    die "Subclass " . ref($self) . " of " . __PACKAGE__ . " does not implement required method CanonicalizeValue";
+}
+
+=head2 Description
+
+A class method that returns the human-friendly name for this canonicalizer
+which appears in the admin UI. By default it is the class name, which is
+not so human friendly. You should override this in your subclass.
+
+=cut
+
+sub Description {
+    my $class = shift;
+    return $class;
+}
+
+RT::Base->_ImportOverlays();
+
+1;
+

commit a768c489317e95db86771a4cd7ec8ccda5e4b244
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 27 19:51:59 2016 +0000

    Admin UI for CF canonicalizer

diff --git a/share/html/Admin/CustomFields/Modify.html b/share/html/Admin/CustomFields/Modify.html
index 4c357ee..082270c 100644
--- a/share/html/Admin/CustomFields/Modify.html
+++ b/share/html/Admin/CustomFields/Modify.html
@@ -91,6 +91,12 @@
 </td></tr>
 % }
 
+% if ( $CustomFieldObj->Id and $CustomFieldObj->IsCanonicalizeType and RT->Config->Get('CustomFieldValuesCanonicalizers') and ( scalar(@{RT->Config->Get('CustomFieldValuesCanonicalizers')}) > 0 ) ) {
+<tr><td class="label"><&|/l&>Canonicalizer:</&></td><td>
+<& /Admin/Elements/EditCustomFieldValuesCanonicalizer, CustomField => $CustomFieldObj &>
+</td></tr>
+% }
+
 <tr><td class="label"><&|/l&>Applies to</&></td>
 <td><& /Admin/Elements/SelectCustomFieldLookupType, 
         Name => "LookupType", 
@@ -259,6 +265,31 @@ if ( $ARGS{'Update'} && $id ne 'new' ) {
         }
         push @results, $msg;
     }
+    if ( ($CanonicalizeClass||'') ne $CustomFieldObj->CanonicalizeClass ) {
+        my $original = $CustomFieldObj->CanonicalizeClass;
+        my ($good, $msg) = $CustomFieldObj->SetCanonicalizeClass( $CanonicalizeClass );
+        if ( $good ) {
+            # Improve message from class names to their friendly descriptions
+            $original = $original->Description
+                if $original && $original->require;
+            $CanonicalizeClass = $CanonicalizeClass->Description
+                if $CanonicalizeClass && $CanonicalizeClass->require;
+
+            if (!$original) {
+                $msg = loc("[_1] '[_2]' added",
+                            loc("Canonicalizer"), $CanonicalizeClass);
+            }
+            elsif (!$CanonicalizeClass) {
+                $msg = loc("[_1] '[_2]' removed",
+                            loc("Canonicalizer"), $original);
+            }
+            else {
+                $msg = loc("[_1] changed from '[_2]' to '[_3]'",
+                            loc("Canonicalizer"), $original, $CanonicalizeClass );
+            }
+        }
+        push @results, $msg;
+    }
 
     # Set the render type if we have it, but unset it if the new type doesn't
     # support render types
@@ -391,6 +422,7 @@ $Enabled => 0
 $SetUniqueValues => undef
 $UniqueValues => 0
 $ValuesClass => 'RT::CustomFieldValues'
+$CanonicalizeClass => undef
 $RenderType => undef
 $LinkValueTo => undef
 $IncludeContentForValue => undef
diff --git a/share/html/Admin/Elements/EditCustomFieldValuesCanonicalizer b/share/html/Admin/Elements/EditCustomFieldValuesCanonicalizer
new file mode 100644
index 0000000..541fdb0
--- /dev/null
+++ b/share/html/Admin/Elements/EditCustomFieldValuesCanonicalizer
@@ -0,0 +1,77 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 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 }}}
+<div id="canonicalize-class-block">
+<select name="CanonicalizeClass">
+<option value="">-</option>
+% foreach my $canonicalizer (@canonicalizers) {
+<option value="<% $canonicalizer %>" <% $canonicalizer eq ($CustomField->CanonicalizeClass||'') && 'selected="selected"' %>><% $canonicalizer->Description %></option>
+% }
+</select>
+</div>
+
+<%INIT>
+return unless $CustomField->IsCanonicalizeType;
+
+my @canonicalizers;
+foreach my $class( RT->Config->Get('CustomFieldValuesCanonicalizers') ) {
+    next unless $class;
+
+    unless ($class->require) {
+        $RT::Logger->crit("Couldn't load class '$class': $@");
+        next;
+    }
+
+    push @canonicalizers, $class;
+}
+
+return if !@canonicalizers;
+
+</%INIT>
+<%ARGS>
+$CustomField => undef
+</%ARGS>

commit 21b769d2468e7c41527e81b85acff7e337948592
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 27 21:25:06 2016 +0000

    Implement custom field canonicalization

diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index 6bc30bd..a77d66b 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -1788,11 +1788,31 @@ sub _CanonicalizeValue {
     my $type = $self->__Value('Type');
     return 1 unless $type;
 
+    $self->_CanonicalizeValueWithCanonicalizer($args);
+
     my $method = '_CanonicalizeValue'. $type;
     return 1 unless $self->can($method);
     $self->$method($args);
 }
 
+sub _CanonicalizeValueWithCanonicalizer {
+    my $self = shift;
+    my $args = shift;
+
+    return 1 if !$self->CanonicalizeClass;
+
+    my $class = $self->CanonicalizeClass;
+    $class->require or die "Can't load $class: $@";
+    my $canonicalizer = $class->new($self->CurrentUser);
+
+    $args->{'Content'} = $canonicalizer->CanonicalizeValue(
+        CustomField => $self,
+        Content     => $args->{'Content'},
+    );
+
+    return 1;
+}
+
 sub _CanonicalizeValueDateTime {
     my $self    = shift;
     my $args    = shift;

commit e4f0138b4d5b9a01ce53b336c005559c67ca7213
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 27 21:25:20 2016 +0000

    Ship Uppercase and Lowercase canonicalizers in core

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 3c62781..a3de206 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -920,7 +920,10 @@ your own canonicalizers.
 
 =cut
 
-Set(@CustomFieldValuesCanonicalizers, ());
+Set(@CustomFieldValuesCanonicalizers, qw(
+    RT::CustomFieldValues::Canonicalizer::Uppercase
+    RT::CustomFieldValues::Canonicalizer::Lowercase
+));
 
 =item C<%CustomFieldGroupings>
 
diff --git a/lib/RT/CustomFieldValues/Canonicalizer.pm b/lib/RT/CustomFieldValues/Canonicalizer.pm
index 10a6b5d..5265a75 100644
--- a/lib/RT/CustomFieldValues/Canonicalizer.pm
+++ b/lib/RT/CustomFieldValues/Canonicalizer.pm
@@ -67,6 +67,9 @@ this class. Your subclass must implement the methods L</CanonicalizeValue>
 and L</Description> as documented below. Finally, add the new class name to
 L<RT_Config/@CustomFieldValuesCanonicalizers>.
 
+See L<RT::CustomFieldValues::Canonicalizer::Uppercase> for a complete
+example.
+
 =head2 new
 
 The object constructor takes one argument: L<RT::CurrentUser> object.
diff --git a/lib/RT/CustomFieldValues/Canonicalizer.pm b/lib/RT/CustomFieldValues/Canonicalizer/Lowercase.pm
similarity index 59%
copy from lib/RT/CustomFieldValues/Canonicalizer.pm
copy to lib/RT/CustomFieldValues/Canonicalizer/Lowercase.pm
index 10a6b5d..6217527 100644
--- a/lib/RT/CustomFieldValues/Canonicalizer.pm
+++ b/lib/RT/CustomFieldValues/Canonicalizer/Lowercase.pm
@@ -46,71 +46,37 @@
 #
 # END BPS TAGGED BLOCK }}}
 
-package RT::CustomFieldValues::Canonicalizer;
+package RT::CustomFieldValues::Canonicalizer::Lowercase;
 
 use strict;
 use warnings;
-use base 'RT::Base';
 
-=head1 NAME
+use base qw(RT::CustomFieldValues::Canonicalizer);
 
-RT::CustomFieldValues::Canonicalizer - base class for custom field value
-canonicalizers
+=head1 NAME
 
-=head1 SYNOPSIS
+RT::CustomFieldValues::Canonicalizer::Lowercase - lowercase custom field values
 
 =head1 DESCRIPTION
 
-This class is the base class for custom field value canonicalizers. To
-implement a new canonicalizer, you must create a new class that subclasses
-this class. Your subclass must implement the methods L</CanonicalizeValue>
-and L</Description> as documented below. Finally, add the new class name to
-L<RT_Config/@CustomFieldValuesCanonicalizers>.
-
-=head2 new
-
-The object constructor takes one argument: L<RT::CurrentUser> object.
-
-=cut
-
-sub new {
-    my $proto = shift;
-    my $class = ref($proto) || $proto;
-    my $self  = {};
-    bless ($self, $class);
-    $self->CurrentUser(@_);
-    return $self;
-}
-
-=head2 CanonicalizeValue
-
-Receives a parameter hash including C<CustomField> and C<Content>.
-
-You may also access C<< $self->CurrentUser >> in case you need the user's
-language or locale.
-
-This method is expected to return the canonicalized C<Content>.
+This canonicalizer adjusts the custom field value to have all lowercase
+characters. It is Unicode-aware, so for example "Ω" will become "ω".
 
 =cut
 
 sub CanonicalizeValue {
     my $self = shift;
-    die "Subclass " . ref($self) . " of " . __PACKAGE__ . " does not implement required method CanonicalizeValue";
-}
-
-=head2 Description
+    my %args = (
+        CustomField => undef,
+        Content     => undef,
+        @_,
+    );
 
-A class method that returns the human-friendly name for this canonicalizer
-which appears in the admin UI. By default it is the class name, which is
-not so human friendly. You should override this in your subclass.
-
-=cut
-
-sub Description {
-    my $class = shift;
-    return $class;
+    return lc $args{Content};
 }
 
+sub Description { "Lowercase" }
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/lib/RT/CustomFieldValues/Canonicalizer.pm b/lib/RT/CustomFieldValues/Canonicalizer/Uppercase.pm
similarity index 59%
copy from lib/RT/CustomFieldValues/Canonicalizer.pm
copy to lib/RT/CustomFieldValues/Canonicalizer/Uppercase.pm
index 10a6b5d..32079d9 100644
--- a/lib/RT/CustomFieldValues/Canonicalizer.pm
+++ b/lib/RT/CustomFieldValues/Canonicalizer/Uppercase.pm
@@ -46,71 +46,37 @@
 #
 # END BPS TAGGED BLOCK }}}
 
-package RT::CustomFieldValues::Canonicalizer;
+package RT::CustomFieldValues::Canonicalizer::Uppercase;
 
 use strict;
 use warnings;
-use base 'RT::Base';
 
-=head1 NAME
+use base qw(RT::CustomFieldValues::Canonicalizer);
 
-RT::CustomFieldValues::Canonicalizer - base class for custom field value
-canonicalizers
+=head1 NAME
 
-=head1 SYNOPSIS
+RT::CustomFieldValues::Canonicalizer::Uppercase - uppercase custom field values
 
 =head1 DESCRIPTION
 
-This class is the base class for custom field value canonicalizers. To
-implement a new canonicalizer, you must create a new class that subclasses
-this class. Your subclass must implement the methods L</CanonicalizeValue>
-and L</Description> as documented below. Finally, add the new class name to
-L<RT_Config/@CustomFieldValuesCanonicalizers>.
-
-=head2 new
-
-The object constructor takes one argument: L<RT::CurrentUser> object.
-
-=cut
-
-sub new {
-    my $proto = shift;
-    my $class = ref($proto) || $proto;
-    my $self  = {};
-    bless ($self, $class);
-    $self->CurrentUser(@_);
-    return $self;
-}
-
-=head2 CanonicalizeValue
-
-Receives a parameter hash including C<CustomField> and C<Content>.
-
-You may also access C<< $self->CurrentUser >> in case you need the user's
-language or locale.
-
-This method is expected to return the canonicalized C<Content>.
+This canonicalizer adjusts the custom field value to have all uppercase
+characters. It is Unicode-aware, so for example "ω" will become "Ω".
 
 =cut
 
 sub CanonicalizeValue {
     my $self = shift;
-    die "Subclass " . ref($self) . " of " . __PACKAGE__ . " does not implement required method CanonicalizeValue";
-}
-
-=head2 Description
+    my %args = (
+        CustomField => undef,
+        Content     => undef,
+        @_,
+    );
 
-A class method that returns the human-friendly name for this canonicalizer
-which appears in the admin UI. By default it is the class name, which is
-not so human friendly. You should override this in your subclass.
-
-=cut
-
-sub Description {
-    my $class = shift;
-    return $class;
+    return uc $args{Content};
 }
 
+sub Description { "Uppercase" }
+
 RT::Base->_ImportOverlays();
 
 1;

commit 3d339db96c00b480ff3c2dab5bdbcdcfb8ed5d0d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Fri May 27 21:35:34 2016 +0000

    Canonicalize default values on custom fields

diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index a77d66b..fe6affe 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -1093,7 +1093,7 @@ sub _Set {
         return ( 0, $self->loc('Permission Denied') );
     }
     my ($ret, $msg) = $self->SUPER::_Set( @_ );
-    if ( $args{Field} =~ /^(?:MaxValues|Type|LookupType|ValuesClass)$/ ) {
+    if ( $args{Field} =~ /^(?:MaxValues|Type|LookupType|ValuesClass|CanonicalizeClass)$/ ) {
         $self->CleanupDefaultValues;
     }
     return ($ret, $msg);
@@ -2271,6 +2271,30 @@ sub CleanupDefaultValues {
                         $content->{ $self->id } = join "\n", @$default_values;
                         $changed = 1;
                     }
+
+                    if ($self->MaxValues == 1) {
+                        my $args = { Content => $default_values };
+                        $self->_CanonicalizeValueWithCanonicalizer($args);
+                        if ($args->{Content} ne $default_values) {
+                            $content->{ $self->id } = $default_values;
+                            $changed = 1;
+                        }
+                    }
+                    else {
+                        my @new_values;
+                        my $multi_changed = 0;
+                        for my $value (split /\s*\n+\s*/, $default_values) {
+                            my $args = { Content => $value };
+                            $self->_CanonicalizeValueWithCanonicalizer($args);
+                            push @new_values, $args->{Content};
+                            $multi_changed = 1 if $args->{Content} ne $value;
+                        }
+
+                        if ($multi_changed) {
+                            $content->{ $self->id } = join "\n", @new_values;
+                            $changed = 1;
+                        }
+                    }
                 }
             }
         }

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


More information about the rt-commit mailing list