[Rt-commit] rt branch 5.0/add-article-portlet2 created. rt-5.0.3-299-g35cff41eb3

BPS Git Server git at git.bestpractical.com
Wed Mar 29 19:18:30 UTC 2023


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "rt".

The branch, 5.0/add-article-portlet2 has been created
        at  35cff41eb3bd93d0bfd9a15d34851fb29a4880cc (commit)

- Log -----------------------------------------------------------------
commit 35cff41eb3bd93d0bfd9a15d34851fb29a4880cc
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Mar 17 11:35:19 2023 -0400

    Add docs for new process articles feature

diff --git a/docs/customizing/articles_introduction.pod b/docs/customizing/articles_introduction.pod
index ba4cf90d1a..cb493c8e1a 100644
--- a/docs/customizing/articles_introduction.pod
+++ b/docs/customizing/articles_introduction.pod
@@ -180,6 +180,55 @@ L<$SelfServiceShowArticleSearch|RT_Config/SelfServiceShowArticleSearch>
 to C<1> to enable an article search box at the top of the page
 in self service.
 
+=head1 Process Articles
+
+Some work managed on tickets in RT will have a defined process or
+set of steps to take when handling the ticket. Articles are a
+convenient place to capture this process and you can then display
+these steps right on the ticket to make it easy for users to follow.
+
+A queue typically can accept multiple types of tickets, and each
+of these might have a different process. For example, you might have
+a queue called "Product Backlog" that tracks work on some software.
+This queue could receive tickets from multiple sources, some
+new feature requests and some bug reports.
+
+You might have a different process to handle these two types
+of tickets. To start, you might categorize these using a custom field
+on the queue called "Ticket Type". Working from this custom field,
+RT provides a way to display a different article for each of the
+options you add for these types.
+
+For example, if the type is set to "Feature Request", the article
+"Feature Request Process" would be displayed for the ticket. If the
+type is changed to "Bug Report", the article "Bug Report Process"
+would be displayed. The next section describes how to configure
+RT to show these process articles.
+
+=head2 Configuring Process Articles
+
+To enable process articles on a queue, first you need to decide which
+field on that queue to use to drive which article is shown. Create a
+new custom field with ticket types or categories if you don't already
+have one.
+
+Next create a new class to contain your process articles. Putting them in
+their own class allows you to manage them separate from other FAQ type
+articles you might have for email replies.
+
+Once you have these two created, set L<%ProcessArticleFields|RT_Config/%ProcessArticleFields>
+with the custom field, and class to use for your queue.
+
+In the class you created, you can now create a new article for each
+entry in your custom field. Once you have them created, you will
+configure the mapping from custom field value to article name using
+the configuration L<%ProcessArticleMapping|RT_Config/%ProcessArticleMapping>.
+
+Article content can then be updated at any time by modifying the source
+article. If you add a new option to your custom field, update the
+configuration to map it to a new article with the corresponding
+process details.
+
 =head1 Configuration Options
 
 =head2 ArticleOnTicketCreate

commit 977ea3263368db6e9b4536a457ceeaa92370c95d
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu Mar 16 13:16:20 2023 -0400

    Create optional article portlet for ticket display page

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 33e4822d7e..d7eaba92b0 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -3202,6 +3202,52 @@ to Articles when they are included in a message.
 
 Set($LinkArticlesOnInclude, 1);
 
+=item C<%ProcessArticleFields>
+
+To show process articles on ticket display pages, for each queue define
+which ticket field will determine the article to show and which article
+class to use when loading the article.
+
+    Set( %ProcessArticleFields, (
+        General => { Field => 'Status', Class => 'Processes' },
+        Support => { Field => 'CF.Severity', Class => 'Processes' },
+    ));
+
+To enable process articles by default, add a "Default" entry accordingly.
+E.g.
+
+    Set( %ProcessArticleFields, (
+        General => { Field => 'Status', Class => 'Processes' },
+        Support => { Field => 'CF.Severity', Class => 'Processes' },
+        Default => { Field => 'CF.{Ticket Type}', Class => 'Processes' },
+    ));
+
+To explicitly disable process articles for a queue, set its value to 0.
+E.g.
+
+    Set( %ProcessArticleFields, (
+        General => 0,
+        Support => { Field => 'CF.Severity', Class => 'Processes' },
+        Default => { Field => 'CF.{Ticket Type}', Class => 'Processes' },
+    ));
+
+=item C<%ProcessArticleMapping>
+
+After defining the field to use, you can then set which article to
+display for each value for that field. For example, if you have a
+custom field "Ticket Type" and you want to show the
+"Feature Request Process" article if the Ticket Type value is
+"Feature Request", you would set the following:
+
+    Set( %ProcessArticleMapping, (
+        'CF.{Ticket Type}' => {
+            'Feature Request' => 'Feature Request Process',
+            'Bug Report' => 'Bug Report Process',
+        },
+    ));
+
+=cut
+
 =back
 
 =head2 Assets
diff --git a/lib/RT/Article.pm b/lib/RT/Article.pm
index de69cb4456..2937f86a17 100644
--- a/lib/RT/Article.pm
+++ b/lib/RT/Article.pm
@@ -684,6 +684,57 @@ sub LoadByInclude {
 
 }
 
+=head2 LoadByNameAndClass
+
+Loads the requested article from the provided class. If found,
+it is loaded into the current object.
+
+Article names must be unique within a class, but can be
+duplicated across different classes. This method is helpful
+for loading the correct article by name if a name might be
+duplicated in different classes.
+
+Takes a hash with the keys:
+
+=over
+
+=item Name
+
+An L<RT::Article> ID or Name.
+
+=item Class
+
+An L<RT::Class> ID or Name.
+
+=back
+
+=cut
+
+sub LoadByNameAndClass {
+    my $self = shift;
+    my %args = (
+                Class => undef,
+                Name  => undef,
+                @_,
+               );
+
+    unless ( defined $args{'Name'} && length $args{'Name'} ) {
+        RT->Logger->error("Unable to load article without Name");
+        return wantarray ? (0, $self->loc("No name provided")) : 0;
+    }
+
+    my $class_obj;
+    if ( defined $args{'Class'} ) {
+        $class_obj = RT::Class->new( $self->CurrentUser );
+        my ($ok, $msg) = $class_obj->Load( $args{'Class'} );
+        unless ( $ok ){
+            RT->Logger->error("Unable to load class " . $args{'Class'} . $msg);
+            return (0, $msg);
+        }
+    }
+
+    return $self->LoadByCols( Name => $args{'Name'}, Class => $class_obj->Id );
+}
 
 =head2 id
 
diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index c0e14cd93c..ae39d57094 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1567,6 +1567,45 @@ our %META;
             }
         },
     },
+    ProcessArticleFields => {
+        Type          => 'HASH',
+        PostLoadCheck => sub {
+            my $self = shift;
+            my $config = $self->Get('ProcessArticleFields') or return;
+
+            for my $name ( keys %$config ) {
+                if ( my $value = $config->{$name} ) {
+                    if ( ref $value eq 'HASH' ) {
+                        for my $field ( qw/Field Class/ ) {
+                            unless ( defined $value->{$field} && length $value->{$field} ) {
+                                RT->Logger->error("Invalid empty $field value for $name in ProcessArticleFields");
+                                $config->{$name} = 0; # Disable the queue
+                            }
+                        }
+
+                        if ( my $field = $value->{Field} ) {
+                            unless ( $field =~ /^CF\./
+                                || RT::Ticket->can($field)
+                                || RT::Ticket->_Accessible( $field => 'read' ) )
+                            {
+                                RT->Logger->error("Invalid Field value($field) for $name in ProcessArticleFields");
+                                $config->{$name} = 0;    # Disable the queue
+                            }
+                        }
+                    }
+                    else {
+                        if ( $value ) {
+                            RT->Logger->error("Invalid value for $name in ProcessArticleFields");
+                            $config->{$name} = 0; # Disable the queue
+                        }
+                    }
+                }
+            }
+        },
+    },
+    ProcessArticleMapping => {
+        Type          => 'HASH',
+    },
     ServiceBusinessHours => {
         Type => 'HASH',
         PostLoadCheck   => sub {
diff --git a/share/html/Elements/ShowArticle b/share/html/Elements/ShowArticle
new file mode 100644
index 0000000000..bd2e8e578c
--- /dev/null
+++ b/share/html/Elements/ShowArticle
@@ -0,0 +1,128 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2022 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 }}}
+
+<&| /Widgets/TitleBox,
+    title => $article->Name,
+    class => 'ticket-info-article',
+    title_class => "inverse",
+    &>
+<div id="" class="process-article-display">
+% $m->callback( CallbackName => "Start", Ticket => $Ticket, Article => $article );
+
+% if ( $article->IncludeSummary && ( $article->Summary || '' ) =~ /\S/ ) {
+<div id="<% 'process-article-summary-class-' . $article->ClassObj->Id %>" class="process-article-summary">
+<% $article->Summary %>
+</div>
+% }
+
+<& /Elements/ShowArticleCustomFields, Object => $article, CustomFields => $custom_fields &>
+
+% $m->callback( CallbackName => "End", Ticket => $Ticket, Article => $article );
+</div>
+</&>
+<%init>
+# Figure out which article to load based on configuration
+my %article_fields = RT->Config->Get('ProcessArticleFields');
+return unless %article_fields;
+
+my $queue_config = $article_fields{$Ticket->QueueObj->Name} // $article_fields{Default} or return;
+my $article_field = $queue_config->{Field} or return;
+my $article_class = $queue_config->{Class} or return;
+
+my $article_field_value;
+if ( $article_field =~ /^CF\.\{(.+)\}$/ || $article_field =~ /^CF\.(.+)/ ) {
+    $article_field_value = $Ticket->FirstCustomFieldValue($1);
+}
+elsif ( $Ticket->can( $article_field ) ) {
+    $article_field_value = $Ticket->$article_field;
+}
+elsif ( $Ticket->_Accessible( $article_field => 'read' ) ) {
+    $article_field_value = $Ticket->_Value( $article_field );
+}
+else {
+    # Shouldn't be here as the config is validated.
+}
+
+unless ( $article_field_value and length $article_field_value ) {
+    # If the field has no value, we can't do the lookup.
+    # This is normal if a value hasn't been set yet, so no error logging.
+    return;
+}
+
+my %article_mapping = RT->Config->Get('ProcessArticleMapping');
+my $article_name;
+
+if ( exists $article_mapping{$article_field} ) {
+    $article_name = $article_mapping{$article_field}->{$article_field_value};
+}
+else {
+    RT->Logger->error("No article defined for field value " . $article_field_value);
+    return;
+}
+
+
+my $article = RT::Article->new($session{'CurrentUser'});
+my ($ok, $msg) = $article->LoadByNameAndClass( Name => $article_name, Class => $article_class );
+
+unless ( $ok ) {
+    RT->Logger->error("Unable to load article $article_name in class $article_class: $msg");
+    return;
+}
+
+my $custom_fields = $article->IncludedCustomFields;
+
+$m->callback(
+    CallbackName    => 'ModifyCollection',
+    Ticket          => $Ticket,
+    ArticleCFs      => $custom_fields,
+);
+
+</%init>
+<%args>
+$Ticket
+</%args>
diff --git a/share/html/Ticket/Elements/ShowSummary b/share/html/Ticket/Elements/ShowSummary
index 944cbf01ca..ec162c3aef 100644
--- a/share/html/Ticket/Elements/ShowSummary
+++ b/share/html/Ticket/Elements/ShowSummary
@@ -142,6 +142,7 @@ my $people_behavior = $InlineEdit ? ($inline_edit_behavior{People} || $inline_ed
 % }
 % $m->callback( %ARGS, CallbackName => 'AfterReminders' );
 
+<& /Elements/ShowArticle, Ticket => $Ticket &>
 <%PERL>
 my $dates_url = RT->Config->Get('WebPath')."/Ticket/ModifyDates.html?id=".$Ticket->Id;
 my $dates_inline = sprintf( $modify_inline, $m->interp->apply_escapes( $dates_url, 'h' ) );
diff --git a/share/static/css/elevator-light/boxes.css b/share/static/css/elevator-light/boxes.css
index d2d4f029f7..99334d42be 100644
--- a/share/static/css/elevator-light/boxes.css
+++ b/share/static/css/elevator-light/boxes.css
@@ -139,6 +139,9 @@ div.results .titlebox .titlebox-content {
 .titlebox.card.ticket-info-cfs, .titlebox.card.asset-info-cfs {
     border-top: 3px solid #D32F2F;
 }
+.titlebox.card.ticket-info-article {
+    border-top: 3px solid #E58F45;
+}
 /* reset to default border for .card-body */
 #comp-Admin-Queues-DefaultValues .titlebox.card.ticket-info-cfs {
     border-top: 1px solid #dde4eb;
diff --git a/share/static/css/elevator-light/ticket.css b/share/static/css/elevator-light/ticket.css
index 1aeacc09ea..23d1531a1e 100644
--- a/share/static/css/elevator-light/ticket.css
+++ b/share/static/css/elevator-light/ticket.css
@@ -166,3 +166,11 @@ ul.select-queue li a:visited {
 .TicketFinalPriority {
     display: none;
 }
+
+.process-article-display {
+    margin-top: 0.5em;
+}
+
+.process-article-summary {
+    font-weight: bold;
+}

commit 96be52bc593188ce36a6f0843e38d8b15ad05de1
Author: Brian Conry <bconry at bestpractical.com>
Date:   Mon Jul 18 08:48:58 2022 -0500

    Update /SelfService/Article/Display.html to new API
    
    This change converts /SelfService/Article/Display.html, and
    /Elements/ShowArticleCustomFields, which is currently only used by
    SelfService, to use the new helper API on RT::Article instead of
    querying all of the attributes of the Class directly.

diff --git a/share/html/Elements/ShowArticleCustomFields b/share/html/Elements/ShowArticleCustomFields
index 253ededef1..76858dd169 100644
--- a/share/html/Elements/ShowArticleCustomFields
+++ b/share/html/Elements/ShowArticleCustomFields
@@ -49,6 +49,7 @@
 %               ARGSRef => \%ARGS, CustomFields => $CustomFields);
 <div>
 % while ( my $CustomField = $CustomFields->Next ) {
+% next unless $Object->IncludeCFValue($CustomField);
 % my $Values = $Object->CustomFieldValues( $CustomField->Id );
 % my $count = $Values->Count;
 % next if $HideEmpty and not $count;
@@ -65,12 +66,13 @@
 % $m->callback( CallbackName => 'ModifyFieldClasses', CustomField => $CustomField,
 %               Object => $Object, Classes => \@classes );
   <div class="form-row <% join(' ', @classes) %>" id="CF-<%$CustomField->id%>-ShowRow">
-% if ($HideFieldNames->{$CustomField->id}) {
+% if ($HideFieldNames->{$CustomField->id} || ! $Object->IncludeCFTitle($CustomField)) {
     <div class="value col-12 <% $count ? '' : ' no-value' %>">
 %} else {
     <div class="label col-3"><% $CustomField->Name %>:</div>
     <div class="value col-9 <% $count ? '' : ' no-value' %>">
 % }
+% if ($Object->IncludeCFValue($CustomField)) {
       <span class="current-value">
 % unless ( $count ) {
 <&|/l&>(no value)</&>
@@ -86,6 +88,7 @@
 </ul>
 % }
       </span>
+% }
     </div>
 % $m->callback( CallbackName => 'AfterCustomFieldValue', CustomField => $CustomField,
 %               Object => $Object );
diff --git a/share/html/SelfService/Article/Display.html b/share/html/SelfService/Article/Display.html
index 6b94b950f6..af3ff52926 100644
--- a/share/html/SelfService/Article/Display.html
+++ b/share/html/SelfService/Article/Display.html
@@ -48,9 +48,9 @@
 <& /SelfService/Elements/Header, Title => $article->Name &>
 <div id="article-display-container" class="mx-auto max-width-md">
 % my $title = "";
-% $title = $article->Summary if $include{Summary};
+% $title = $article->Summary if $article->IncludeSummary;
 <&| /Widgets/TitleBox, title => $title, class => 'article-display-simple', content_class => 'mx-auto width-md' &>
-<& /Elements/ShowArticleCustomFields, Object => $article, CustomFields => $custom_fields, HideFieldNames => \%hide_field_names &>
+<& /Elements/ShowArticleCustomFields, Object => $article, CustomFields => $custom_fields &>
 </&>
 </div>
 <%init>
@@ -70,35 +70,7 @@ unless ( $article->CurrentUserHasRight('ShowArticle') ) {
 $id = $article->id;
 my $class = $article->ClassObj;
 
-# Build up the hash of things to include/exclude, as well
-# as the array of custom field IDs to limit by
-my %include = (Name => 1, Summary => 1, EscapeHTML => 1);
-my @exclude_cf_ids;
-my %hide_field_names;
-
-my $cfs = $article->CustomFields;
-while ( my $cf = $cfs->Next ) {
-    $include{"CF-Title-" . $cf->Id} = 1;
-    $include{"CF-Value-" . $cf->Id} = 1;
-}
-
-# Load show/hide settings from class configuration
-foreach my $key ( keys %include ) {
-    $include{$key} = not $class->FirstAttribute("Skip-$key");
-    if ($key =~ /^CF-Value-(\d+)$/ && !$include{$key}) {
-        push(@exclude_cf_ids, $1);
-    }
-    if ($key =~ /^CF-Title-(\d+)$/ && !$include{$key}) {
-        $hide_field_names{$1} = 1;
-    }
-}
-
-my $custom_fields = $article->CustomFields;
-if ( scalar(@exclude_cf_ids) ) {
-    $custom_fields->Limit( FIELD    => 'id',
-                           OPERATOR => 'NOT IN',
-                           VALUE    => \@exclude_cf_ids );
-}
+my $custom_fields = $article->IncludedCustomFields;
 
 </%init>
 <%args>

commit e073b26a9c193d53f3649aa229795c1ef7bb9c20
Author: Brian Conry <bconry at bestpractical.com>
Date:   Fri Jul 15 15:08:19 2022 -0500

    Convert Preformatted template to use new article API

diff --git a/share/html/Articles/Article/Elements/Preformatted b/share/html/Articles/Article/Elements/Preformatted
index 36b12eb339..edd3c22549 100644
--- a/share/html/Articles/Article/Elements/Preformatted
+++ b/share/html/Articles/Article/Elements/Preformatted
@@ -45,54 +45,45 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-% if ($include{Name}) {
+% if ($Article->IncludeName) {
 #<%$Article->Id%>: <%$Article->Name || loc('(no name)')%>
 <%'-' x length("#".$Article->Id.": ".($Article->Name || loc('(no name)'))) %>
 % }
-% if ( $include{Summary} && ($Article->Summary||'') =~ /\S/ ) {
+% if ( $Article->IncludeSummary && ($Article->Summary||'') =~ /\S/ ) {
 <% $Article->Summary %>
 % }
 % while (my $cf = $cfs->Next) {
-%   next unless $include{"CF-Title-".$cf->Id} or $include{"CF-Value-".$cf->Id};
 %   my $values = $Article->CustomFieldValues($cf->Id);
 %   if ($values->Count == 1) {
 %     my $value = $values->First; 
-%     if ($include{"CF-Title-".$cf->Id}) {
+%     if ($Article->IncludeCFTitle($cf)) {
 <%      $cf->Name%>:
 <%      '-' x length($cf->Name) %>
 %     }
-%     if ($value && $include{"CF-Value-".$cf->Id}) {
+%     if ($value && $Article->IncludeCFValue($cf)) {
 <%      $get_content->( $value ) %>
 %     }
 %   } else {
 %     my $val = $values->Next;
-%     if ($include{"CF-Title-".$cf->Id}) {
+%     if ($Article->IncludeCFTitle($cf)) {
 <%      $cf->Name%>: \
 %     }
-%     if ($val && $include{"CF-Value-".$cf->Id}) {
+%     if ($val && $Article->IncludeCFValue($cf)) {
 <%      $get_content->( $val ) %>
 %     }
 %     while ($val = $values->Next) { 
-%       if ($include{"CF-Title-".$cf->Id}) {
+%       if ($Article->IncludeCFTitle($cf)) {
 <%        ' ' x length($cf->Name)%>  \
 %       }
-%       if ($include{"CF-Value-".$cf->Id}) {
+%       if ($Article->IncludeCFValue($cf)) {
 <%        $get_content->( $val ) %>
 %       }
 %     } 
 %   }
 % }
 <%init>
-my $class = $Article->ClassObj;
-my %include = (Name => 1, Summary => 1, EscapeHTML => 1);
-my $cfs = $class->ArticleCustomFields;
-while ( my $cf = $cfs->Next ) {
-    $include{"CF-Title-" . $cf->Id} = 1;
-    $include{"CF-Value-" . $cf->Id} = 1;
-}
-foreach my $key ( keys %include ) {
-    $include{$key} = not $class->FirstAttribute("Skip-$key");
-}
+my $escape_html = $Article->EscapeHTML;
+my $cfs = $Article->IncludedCustomFields;
 
 my $get_content = sub {
     my $value = shift;
@@ -107,7 +98,7 @@ my $get_content = sub {
         content => \$content,
     );
 
-    if ( $include{'EscapeHTML'} && $content =~ /<.{1,5}>/ ) {
+    if ( $escape_html && $content =~ /<.{1,5}>/ ) {
         $content = RT::Interface::Email::ConvertHTMLToText( $content );
     }
     return $content;

commit ff9e553ccf825086d219ef795ab4b68e14e38139
Author: Brian Conry <bconry at bestpractical.com>
Date:   Fri Jul 15 14:55:56 2022 -0500

    Add pass-through methods for class-level display flags

diff --git a/lib/RT/Article.pm b/lib/RT/Article.pm
index 590b4fb989..de69cb4456 100644
--- a/lib/RT/Article.pm
+++ b/lib/RT/Article.pm
@@ -581,6 +581,44 @@ sub CustomFieldLookupType {
     "RT::Class-RT::Article";
 }
 
+sub IncludedCustomFields {
+    my $self = shift;
+
+    my $cfs = $self->ClassObj->IncludedArticleCustomFields;
+
+    $cfs->SetContextObject( $self );
+
+    return $cfs;
+}
+
+sub IncludeName {
+    my $self = shift;
+    return $self->ClassObj->IncludeName;
+}
+
+sub IncludeSummary {
+    my $self = shift;
+    return $self->ClassObj->IncludeSummary;
+}
+
+sub EscapeHTML {
+    my $self = shift;
+    return $self->ClassObj->EscapeHTML;
+}
+
+sub IncludeCFTitle {
+    my $self = shift;
+    my $cf_obj = shift;
+
+    return $self->ClassObj->IncludeArticleCFTitle( $cf_obj );
+}
+
+sub IncludeCFValue {
+    my $self = shift;
+    my $cf_obj = shift;
+
+    return $self->ClassObj->IncludeArticleCFValue( $cf_obj );
+}
 
 sub ACLEquivalenceObjects {
     my $self = shift;

commit d017c9b183e087bc0f67ddd4382c1949c3e0ea34
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Mar 10 14:59:01 2023 -0500

    Add tests for new article methods

diff --git a/t/articles/class.t b/t/articles/class.t
index cf2ea13d68..3211ea2eb9 100644
--- a/t/articles/class.t
+++ b/t/articles/class.t
@@ -39,6 +39,9 @@ ok (!$id, $msg);
 
 $cl->Load('Test-'.$$);
 ok($cl->id, "Loaded the class we want");
+ok($cl->IncludeName, 'Class will include article names');
+ok($cl->IncludeSummary, 'Class will include article summary');
+ok($cl->EscapeHTML, 'Class will escape HTML content');
 
 diag('Test class custom fields');
 
@@ -100,4 +103,9 @@ $m->content_like(qr/Description changed from.*no value.*to .*Test Description/,'
 $m->form_number(3);
 is($m->current_form->find_input('Include-Name')->value,undef,'Disabled Including Names for this Class');
 
+my $class_reload = RT::Class->new(RT->SystemUser);
+($ok, $msg) = $class_reload->Load('Test Redirect');
+ok($ok, $msg);
+ok(!$class_reload->IncludeName, 'Class will not include article names');
+
 done_testing();

commit 8b091c0f7f8803f5d1cf892763376c14a7335c13
Author: Brian Conry <bconry at bestpractical.com>
Date:   Fri Jul 15 14:48:22 2022 -0500

    Add helper methods on Class for article display settings
    
    These methods provide a way to query the display configuration of the
    class without having to check the attributes directly.
    
    Also provides a convenience method to get an RT::CustomFields collection
    object that is pre-filtered according to these display flags.

diff --git a/lib/RT/Class.pm b/lib/RT/Class.pm
index 236d8548d9..f9f06092e6 100644
--- a/lib/RT/Class.pm
+++ b/lib/RT/Class.pm
@@ -70,6 +70,8 @@ sub Table {'Classes'}
 use RT::CustomField;
 RT::CustomField->RegisterLookupType( CustomFieldLookupType() => 'Classes' );    #loc
 
+=head1 METHODS
+
 =head2 Load IDENTIFIER
 
 Loads a class, either by name or by id
@@ -202,7 +204,7 @@ sub ArticleCustomFields {
 }
 
 
-=head1 AppliedTo
+=head2 AppliedTo
 
 Returns collection of Queues this Class is applied to.
 Doesn't takes into account if object is applied globally.
@@ -225,7 +227,7 @@ sub AppliedTo {
     return $res;
 }
 
-=head1 NotAppliedTo
+=head2 NotAppliedTo
 
 Returns collection of Queues this Class is not applied to.
 
@@ -396,6 +398,115 @@ sub SetSubjectOverride {
     }
 }
 
+=head2 IncludeName
+
+Returns 1 if the class is configured for the article Name to
+be included with article content, 0 otherwise.
+
+=cut
+
+sub IncludeName {
+    my $self = shift;
+    return $self->FirstAttribute('Skip-Name') ? 0 : 1;
+}
+
+=head2 IncludeSummary
+
+Returns 1 if the class is configured for the article Summary to
+be included with article content, 0 otherwise.
+
+=cut
+
+sub IncludeSummary {
+    my $self = shift;
+    return $self->FirstAttribute('Skip-Summary') ? 0 : 1;
+}
+
+=head2 EscapeHTML
+
+Returns 1 if the content of custom fields should be filtered
+through EscapeHTML, 0 otherwise.
+
+=cut
+
+sub EscapeHTML {
+    my $self = shift;
+    return $self->FirstAttribute('Skip-EscapeHTML') ? 0 : 1;
+}
+
+sub _BuildCFInclusionData {
+    my $self = shift;
+
+    # Return immediately if we already populated the info
+    return if $self->{'_cf_include_hash'};
+
+    my $include = $self->{'_cf_include_hash'} = {};
+    my $excludes = $self->{'_cf_exclude_list'} = [];
+
+    my $cfs = $self->ArticleCustomFields;
+
+    while ( my $cf = $cfs->Next ) {
+        my $cfid = $cf->Id;
+        $include->{"Title-$cfid"} = not $self->FirstAttribute("Skip-CF-Title-$cfid");
+        $include->{"Value-$cfid"} = not $self->FirstAttribute("Skip-CF-Value-$cfid");
+        push @$excludes, $cfid unless $include->{"Title-$cfid"} or $include->{"Value-$cfid"};
+    }
+}
+
+=head2 IncludedArticleCustomFields
+
+As ArticleCustomFields, but filtered to only include those
+that should have either their Title (Name) or Value included
+in content.
+
+=cut
+
+sub IncludedArticleCustomFields {
+    my $self = shift;
+
+    $self->_BuildCFInclusionData;
+
+    my $cfs = $self->ArticleCustomFields;
+
+    if ( @{ $self->{'_cf_exclude_list'} } ) {
+        $cfs->Limit( FIELD => 'id', OPERATOR => 'NOT IN', VALUE => $self->{'_cf_exclude_list'} );
+    }
+
+    return $cfs;
+}
+
+=head2 IncludeArticleCFTitle CustomFieldObject
+
+Returns true if the title of the custom field should
+be included in article content, and false otherwise.
+
+=cut
+
+sub IncludeArticleCFTitle {
+    my $self = shift;
+    my $cfobj = shift;
+
+    $self->_BuildCFInclusionData;
+
+    return $self->{'_cf_include_hash'}{"Title-".$cfobj->Id};
+}
+
+=head2 IncludeArticleCFValue CustomFieldObject
+
+Returns true if the value of the custom field should
+be included in article content, and false otherwise.
+
+=cut
+
+sub IncludeArticleCFValue {
+    my $self = shift;
+    my $cfobj = shift;
+
+    $self->_BuildCFInclusionData;
+
+    return $self->{'_cf_include_hash'}{"Value-".$cfobj->Id};
+}
+
 =head2 id
 
 Returns the current value of id. 

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


hooks/post-receive
-- 
rt


More information about the rt-commit mailing list