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

Shawn Moore shawn at bestpractical.com
Wed Jun 8 14:22:17 EDT 2016


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

- Log -----------------------------------------------------------------
commit aa6e1a0830736aec829ed625c0d6f4d3479aa23e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Jun 8 18:16:54 2016 +0000

    Docs and logging for CF ValuesClass and RenderType

diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm
index 1c0f715..ce28df3 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -737,7 +737,7 @@ to this Custom Field.
 
 sub IsSelectionType {
     my $self = shift;
-    my $type = @_? shift : $self->Type;
+    my $type = @_ ? shift : $self->Type;
     return undef unless $type;
     return $FieldTypes{$type}->{selection_type};
 }
@@ -759,6 +759,14 @@ sub ValuesClass {
     return $self->_Value( ValuesClass => @_ ) || 'RT::CustomFieldValues';
 }
 
+=head2 SetValuesClass CLASS
+
+Writer method for the ValuesClass field; validates that the custom field can
+use a ValuesClass, and that the provided ValuesClass passes
+L</ValidateValuesClass>.
+
+=cut
+
 sub SetValuesClass {
     my $self = shift;
     my $class = shift || 'RT::CustomFieldValues';
@@ -776,6 +784,17 @@ sub SetValuesClass {
     return $self->_Set( Field => 'ValuesClass', Value => $class, @_ );
 }
 
+=head2 ValidateValuesClass CLASS
+
+Validates a potential ValuesClass value; the ValuesClass may be C<undef> or
+the string C<"RT::CustomFieldValues"> (both of which make this custom field
+use the ordinary values implementation), or a class name in the listed in
+the L<RT_Config/@CustomFieldValuesSources> setting.
+
+Returns true if valid; false if invalid.
+
+=cut
+
 sub ValidateValuesClass {
     my $self = shift;
     my $class = shift;
diff --git a/share/html/Admin/CustomFields/Modify.html b/share/html/Admin/CustomFields/Modify.html
index a954fdd..a9d638c 100644
--- a/share/html/Admin/CustomFields/Modify.html
+++ b/share/html/Admin/CustomFields/Modify.html
@@ -257,6 +257,9 @@ if ( $ARGS{'Update'} && $id ne 'new' ) {
             $msg = loc("[_1] changed from '[_2]' to '[_3]'",
                         loc("Field values source"), $original, $ValuesClass );
         }
+        else {
+            RT->Logger->debug("Unable to SetValuesClass to '$ValuesClass': $msg");
+        }
         push @results, $msg;
     }
 
@@ -273,6 +276,9 @@ if ( $ARGS{'Update'} && $id ne 'new' ) {
                 $msg = loc("[_1] changed from '[_2]' to '[_3]'",
                             loc("Render Type"), $original, $RenderType );
             }
+            else {
+                RT->Logger->debug("Unable to SetRenderType to '$RenderType': $msg");
+            }
 
             push @results, $msg;
         }

commit 029b886b1f67e1c5137f621e3a8b2f559acd60a4
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 ce28df3..9149c68 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -2487,6 +2487,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 53599b17f47e388af025319d099874de0c8d8989
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 9149c68..043c942 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
@@ -804,6 +840,50 @@ sub ValidateValuesClass {
     return undef;
 }
 
+=head2 SetCanonicalizeClass CLASS
+
+Writer method for the CanonicalizeClass field; validates that the custom
+field can use a CanonicalizeClass, and that the provided CanonicalizeClass
+passes L</ValidateCanonicalizeClass>.
+
+=cut
+
+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, @_ );
+}
+
+=head2 ValidateCanonicalizeClass CLASS
+
+Validates a potential CanonicalizeClass value; the CanonicalizeClass may be
+C<undef> (which make this custom field use no special canonicalization), or
+a class name in the listed in the
+L<RT_Config/@CustomFieldValuesCanonicalizers> setting.
+
+Returns true if valid; false if invalid.
+
+=cut
+
+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..1bb2df5
--- /dev/null
+++ b/lib/RT/CustomFieldValues/Canonicalizer.pm
@@ -0,0 +1,118 @@
+# 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> (an L<RT::CustomField>
+object) and C<Content> (a string of user-provided 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 b9918899557c7c7f6d6e063eb250e396f001af9c
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 a9d638c..cd49eb7 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", 
@@ -262,6 +268,35 @@ 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 );
+            }
+        }
+        else {
+            RT->Logger->debug("Unable to SetCanonicalizeClass to '$CanonicalizeClass': $msg");
+        }
+
+        push @results, $msg;
+    }
 
     # Set the render type if we have it, but unset it if the new type doesn't
     # support render types
@@ -397,6 +432,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 d2857302617599e6037da8b23a075a42bfe34ec7
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 043c942..fee8419 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -1826,11 +1826,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 30c7bf5db081b61336db9563d3c4f4f4431229b7
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 1bb2df5..03afbc1 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 58%
copy from lib/RT/CustomFieldValues/Canonicalizer.pm
copy to lib/RT/CustomFieldValues/Canonicalizer/Lowercase.pm
index 1bb2df5..d1e8e1a 100644
--- a/lib/RT/CustomFieldValues/Canonicalizer.pm
+++ b/lib/RT/CustomFieldValues/Canonicalizer/Lowercase.pm
@@ -46,72 +46,39 @@
 #
 # END BPS TAGGED BLOCK }}}
 
-package RT::CustomFieldValues::Canonicalizer;
+package RT::CustomFieldValues::Canonicalizer::Lowercase;
 
 use strict;
 use warnings;
-use base 'RT::Base';
 
-=head1 NAME
-
-RT::CustomFieldValues::Canonicalizer - base class for custom field value
-canonicalizers
-
-=head1 SYNOPSIS
-
-=head1 DESCRIPTION
+use base qw(RT::CustomFieldValues::Canonicalizer);
 
-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>.
+=encoding utf-8
 
-=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
+=head1 NAME
 
-Receives a parameter hash including C<CustomField> (an L<RT::CustomField>
-object) and C<Content> (a string of user-provided content).
+RT::CustomFieldValues::Canonicalizer::Lowercase - lowercase custom field values
 
-You may also access C<< $self->CurrentUser >> in case you need the user's
-language or locale.
+=head1 DESCRIPTION
 
-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 58%
copy from lib/RT/CustomFieldValues/Canonicalizer.pm
copy to lib/RT/CustomFieldValues/Canonicalizer/Uppercase.pm
index 1bb2df5..d54f399 100644
--- a/lib/RT/CustomFieldValues/Canonicalizer.pm
+++ b/lib/RT/CustomFieldValues/Canonicalizer/Uppercase.pm
@@ -46,72 +46,39 @@
 #
 # END BPS TAGGED BLOCK }}}
 
-package RT::CustomFieldValues::Canonicalizer;
+package RT::CustomFieldValues::Canonicalizer::Uppercase;
 
 use strict;
 use warnings;
-use base 'RT::Base';
 
-=head1 NAME
-
-RT::CustomFieldValues::Canonicalizer - base class for custom field value
-canonicalizers
-
-=head1 SYNOPSIS
-
-=head1 DESCRIPTION
+use base qw(RT::CustomFieldValues::Canonicalizer);
 
-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>.
+=encoding utf-8
 
-=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
+=head1 NAME
 
-Receives a parameter hash including C<CustomField> (an L<RT::CustomField>
-object) and C<Content> (a string of user-provided content).
+RT::CustomFieldValues::Canonicalizer::Uppercase - uppercase custom field values
 
-You may also access C<< $self->CurrentUser >> in case you need the user's
-language or locale.
+=head1 DESCRIPTION
 
-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 baa8e1f7f3a5cf6140abee74864cf86aff4a78a8
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 fee8419..4cbdbab 100644
--- a/lib/RT/CustomField.pm
+++ b/lib/RT/CustomField.pm
@@ -1131,7 +1131,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);
@@ -2309,6 +2309,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;
+                        }
+                    }
                 }
             }
         }

commit f7f2d9070e61198627fbea4dff679549717f3703
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed Jun 1 16:54:35 2016 +0000

    Tests for custom field canonicalizers

diff --git a/t/customfields/canonicalizer.t b/t/customfields/canonicalizer.t
new file mode 100644
index 0000000..5451de9
--- /dev/null
+++ b/t/customfields/canonicalizer.t
@@ -0,0 +1,113 @@
+use utf8;
+use warnings;
+use strict;
+
+use RT::Test tests => undef;
+
+my $t = RT::Test->create_ticket( Subject => 'test canonicalize values', Queue => 'General' );
+
+{
+    diag "testing invalid canonicalizer";
+    my $invalid = RT::CustomField->new(RT->SystemUser);
+    my ($ok, $msg) = $invalid->Create(
+        Name              => 'uppercase',
+        Type              => 'FreeformSingle',
+        Queue             => 0,
+        CanonicalizeClass => 'RT::CustomFieldValues::Canonicalizer::NonExistent',
+    );
+    ok(!$ok, "Didn't create CF");
+    like($msg, qr/Invalid custom field values canonicalizer/);
+}
+
+{
+    diag "testing uppercase canonicalizer";
+    my $uppercase = RT::Test->load_or_create_custom_field(
+        Name              => 'uppercase',
+        Type              => 'FreeformSingle',
+        Queue             => 0,
+        CanonicalizeClass => 'RT::CustomFieldValues::Canonicalizer::Uppercase',
+    );
+    is($uppercase->CanonicalizeClass, 'RT::CustomFieldValues::Canonicalizer::Uppercase', 'CanonicalizeClass');
+
+    my @tests = (
+        'hello world'          => 'HELLO WORLD',
+        'Hello World'          => 'HELLO WORLD',
+        'ABC 123 xyz !@#'      => 'ABC 123 XYZ !@#',
+        'Unicode aware: "ω Ω"' => 'UNICODE AWARE: "Ω Ω"',
+        'てすとテスト'         => 'てすとテスト',
+    );
+
+    while (my ($input, $expected) = splice @tests, 0, 2) {
+        my ($ok, $msg) = $t->AddCustomFieldValue(
+            Field => $uppercase,
+            Value => $input,
+        );
+        ok( $ok, $msg );
+        is( $t->FirstCustomFieldValue($uppercase), $expected, 'canonicalized to uppercase' );
+     }
+}
+
+{
+    diag "testing lowercase canonicalizer";
+    my $lowercase = RT::Test->load_or_create_custom_field(
+        Name              => 'lowercase',
+        Type              => 'FreeformSingle',
+        Queue             => 0,
+        CanonicalizeClass => 'RT::CustomFieldValues::Canonicalizer::Lowercase',
+    );
+    is($lowercase->CanonicalizeClass, 'RT::CustomFieldValues::Canonicalizer::Lowercase', 'CanonicalizeClass');
+
+    my @tests = (
+        'hello world'          => 'hello world',
+        'Hello World'          => 'hello world',
+        'ABC 123 xyz !@#'      => 'abc 123 xyz !@#',
+        'Unicode aware: "ω Ω"' => 'unicode aware: "ω ω"',
+        'てすとテスト'         => 'てすとテスト',
+    );
+
+    while (my ($input, $expected) = splice @tests, 0, 2) {
+        my ($ok, $msg) = $t->AddCustomFieldValue(
+            Field => $lowercase,
+            Value => $input,
+        );
+        ok( $ok, $msg );
+        is( $t->FirstCustomFieldValue($lowercase), $expected, 'canonicalized to lowercase' );
+     }
+}
+
+{
+    diag "testing asset canonicalizer";
+
+    my $assetcf = RT::Test->load_or_create_custom_field(
+        Name              => 'assetcf',
+        Type              => 'FreeformSingle',
+        LookupType        => RT::Asset->CustomFieldLookupType,
+        CanonicalizeClass => 'RT::CustomFieldValues::Canonicalizer::Uppercase',
+    );
+    $assetcf->AddToObject(RT::Catalog->new(RT->SystemUser));
+    is($assetcf->CanonicalizeClass, 'RT::CustomFieldValues::Canonicalizer::Uppercase', 'CanonicalizeClass');
+
+    my $asset = RT::Asset->new(RT->SystemUser);
+    my ($ok, $msg) = $asset->Create(Subject => 'test canonicalizers', Catalog => 'General assets');
+    ok($ok, $msg);
+
+    my @tests = (
+        'hello world'          => 'HELLO WORLD',
+        'Hello World'          => 'HELLO WORLD',
+        'ABC 123 xyz !@#'      => 'ABC 123 XYZ !@#',
+        'Unicode aware: "ω Ω"' => 'UNICODE AWARE: "Ω Ω"',
+        'てすとテスト'         => 'てすとテスト',
+    );
+
+    while (my ($input, $expected) = splice @tests, 0, 2) {
+        my ($ok, $msg) = $asset->AddCustomFieldValue(
+            Field => $assetcf,
+            Value => $input,
+        );
+        ok( $ok, $msg );
+        is( $asset->FirstCustomFieldValue($assetcf), $expected, 'canonicalized to uppercase' );
+     }
+}
+
+done_testing;
+

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


More information about the rt-commit mailing list