Mailing List Archive

rt branch 5.0/add-article-portlet2 created. rt-5.0.3-299-g35cff41eb3
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@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@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@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@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@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 @@
-% 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@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@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@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
_______________________________________________
rt-commit mailing list
rt-commit@lists.bestpractical.com
https://lists.bestpractical.com/mailman/listinfo/rt-commit