X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FArticles.pm;fp=rt%2Flib%2FRT%2FArticles.pm;h=8dd661d2e4c79d88b95b408be31471c9eda5cf1a;hb=6587f6ba7d047ddc1686c080090afe7d53365bd4;hp=0000000000000000000000000000000000000000;hpb=47153aae5c2fc00316654e7277fccd45f72ff611;p=freeside.git diff --git a/rt/lib/RT/Articles.pm b/rt/lib/RT/Articles.pm new file mode 100644 index 000000000..8dd661d2e --- /dev/null +++ b/rt/lib/RT/Articles.pm @@ -0,0 +1,925 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + +package RT::Articles; + +use base 'RT::SearchBuilder'; + +sub Table {'Articles'} + +sub _Init { + my $self = shift; + $self->OrderByCols( + { FIELD => 'SortOrder', ORDER => 'ASC' }, + { FIELD => 'Name', ORDER => 'ASC' }, + ); + return $self->SUPER::_Init( @_ ); +} + +=head2 Next + +Returns the next article that this user can see. + +=cut + +sub Next { + my $self = shift; + + my $Object = $self->SUPER::Next(); + if ( ( defined($Object) ) and ( ref($Object) ) ) { + + if ( $Object->CurrentUserHasRight('ShowArticle') ) { + return ($Object); + } + + #If the user doesn't have the right to show this Object + else { + return ( $self->Next() ); + } + } + + #if there never was any queue + else { + return (undef); + } + +} + +=head2 Limit { FIELD => undef, OPERATOR => '=', VALUE => 'undef'} + +Limit the result set. See DBIx::SearchBuilder docs +In addition to the "normal" stuff, value can be an array. + +=cut + +sub Limit { + my $self = shift; + my %ARGS = ( + OPERATOR => '=', + @_ + ); + + if ( ref( $ARGS{'VALUE'} ) ) { + my @values = $ARGS{'VALUE'}; + delete $ARGS{'VALUE'}; + foreach my $v (@values) { + $self->SUPER::Limit( %ARGS, VALUE => $v ); + } + } + else { + $self->SUPER::Limit(%ARGS); + } +} + +=head2 LimitName { OPERATOR => 'LIKE', VALUE => undef } + +Find all articles with Name fields which satisfy OPERATOR for VALUE + +=cut + +sub LimitName { + my $self = shift; + + my %args = ( + FIELD => 'Name', + OPERATOR => 'LIKE', + CASESENSITIVE => 0, + VALUE => undef, + @_ + ); + + $self->Limit(%args); + +} + +=head2 LimitSummary { OPERATOR => 'LIKE', VALUE => undef } + +Find all articles with summary fields which satisfy OPERATOR for VALUE + +=cut + +sub LimitSummary { + my $self = shift; + + my %args = ( + FIELD => 'Summary', + OPERATOR => 'LIKE', + CASESENSITIVE => 0, + VALUE => undef, + @_ + ); + + $self->Limit(%args); + +} + +sub LimitCreated { + my $self = shift; + my %args = ( + FIELD => 'Created', + OPERATOR => undef, + VALUE => undef, + @_ + ); + + $self->Limit(%args); + +} + +sub LimitCreatedBy { + my $self = shift; + my %args = ( + FIELD => 'CreatedBy', + OPERATOR => '=', + VALUE => undef, + @_ + ); + + $self->Limit(%args); + +} + +sub LimitUpdated { + + my $self = shift; + my %args = ( + FIELD => 'Updated', + OPERATOR => undef, + VALUE => undef, + @_ + ); + + $self->Limit(%args); + +} + +sub LimitUpdatedBy { + my $self = shift; + my %args = ( + FIELD => 'UpdatedBy', + OPERATOR => '=', + VALUE => undef, + @_ + ); + + $self->Limit(%args); + +} + +# {{{ LimitToParent ID + +=head2 LimitToParent ID + +Limit the returned set of articles to articles which are children +of article ID. +This does not recurse. + +=cut + +sub LimitToParent { + my $self = shift; + my $parent = shift; + $self->Limit( + FIELD => 'Parent', + OPERATOR => '=', + VALUE => $parent + ); + +} + +# }}} +# {{{ LimitCustomField + +=head2 LimitCustomField HASH + +Limit the result set to articles which have or do not have the custom field +value listed, using a left join to catch things where no rows match. + +HASH needs the following fields: + FIELD (A custom field id) or undef for any custom field + ENTRYAGGREGATOR => (AND, OR) + OPERATOR ('=', 'LIKE', '!=', 'NOT LIKE') + VALUE ( a single scalar value or a list of possible values to be concatenated with ENTRYAGGREGATOR) + +The subclause that the LIMIT statement(s) should be done in can also +be passed in with a SUBCLAUSE parameter. + +=cut + +sub LimitCustomField { + my $self = shift; + my %args = ( + FIELD => undef, + ENTRYAGGREGATOR => 'OR', + OPERATOR => '=', + QUOTEVALUE => 1, + VALUE => undef, + SUBCLAUSE => undef, + @_ + ); + + my $value = $args{'VALUE'}; + # XXX: this work in a different way than RT + return unless $value; #strip out total blank wildcards + + my $ObjectValuesAlias = $self->Join( + TYPE => 'left', + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => 'ObjectCustomFieldValues', + FIELD2 => 'ObjectId', + EXPRESSION => 'main.id' + ); + + $self->Limit( + LEFTJOIN => $ObjectValuesAlias, + FIELD => 'Disabled', + VALUE => '0' + ); + + if ( $args{'FIELD'} ) { + + my $field_id; + if (UNIVERSAL::isa($args{'FIELD'} ,'RT::CustomField')) { + $field_id = $args{'FIELD'}->id; + } elsif($args{'FIELD'} =~ /^\d+$/) { + $field_id = $args{'FIELD'}; + } + if ($field_id) { + $self->Limit( LEFTJOIN => $ObjectValuesAlias, + FIELD => 'CustomField', + VALUE => $field_id, + ENTRYAGGREGATOR => 'AND'); + # Could convert the above to a non-left join and also enable the thing below + # $self->SUPER::Limit( ALIAS => $ObjectValuesAlias, + # FIELD => 'CustomField', + # OPERATOR => 'IS', + # VALUE => 'NULL', + # QUOTEVALUE => 0, + # ENTRYAGGREGATOR => 'OR',); + } else { + # Search for things by name if the cf was specced by name. + my $fields = $self->NewAlias('CustomFields'); + $self->Join( TYPE => 'left', + ALIAS1 => $ObjectValuesAlias , FIELD1 => 'CustomField', + ALIAS2 => $fields, FIELD2=> 'id'); + $self->Limit( ALIAS => $fields, + FIELD => 'Name', + VALUE => $args{'FIELD'}, + ENTRYAGGREGATOR => 'OR'); + $self->Limit( + ALIAS => $fields, + FIELD => 'LookupType', + VALUE => + RT::Article->new($RT::SystemUser)->CustomFieldLookupType() + ); + + } + } + # If we're trying to find articles where a custom field value + # doesn't match something, be sure to find things where it's null + + # basically, we do a left join on the value being applicable to + # the article and then we turn around and make sure that it's + # actually null in practise + + # TODO this should deal with starts with and ends with + + my $fix_op = sub { + my $op = shift; + return $op unless RT->Config->Get('DatabaseType') eq 'Oracle'; + return 'MATCHES' if $op eq '='; + return 'NOT MATCHES' if $op eq '!='; + return $op; + }; + + my $clause = $args{'SUBCLAUSE'} || $ObjectValuesAlias; + + if ( $args{'OPERATOR'} eq '!=' || $args{'OPERATOR'} =~ /^not like$/i ) { + my $op; + if ( $args{'OPERATOR'} eq '!=' ) { + $op = "="; + } + elsif ( $args{'OPERATOR'} =~ /^not like$/i ) { + $op = 'LIKE'; + } + + $self->SUPER::Limit( + LEFTJOIN => $ObjectValuesAlias, + FIELD => 'Content', + OPERATOR => $op, + VALUE => $value, + QUOTEVALUE => $args{'QUOTEVALUE'}, + ENTRYAGGREGATOR => 'AND', #$args{'ENTRYAGGREGATOR'}, + SUBCLAUSE => $clause, + ); + $self->SUPER::Limit( + ALIAS => $ObjectValuesAlias, + FIELD => 'Content', + OPERATOR => 'IS', + VALUE => 'NULL', + QUOTEVALUE => 0, + ENTRYAGGREGATOR => 'AND', + SUBCLAUSE => $clause, + ); + } + else { + $self->SUPER::Limit( + ALIAS => $ObjectValuesAlias, + FIELD => 'LargeContent', + OPERATOR => $fix_op->($args{'OPERATOR'}), + VALUE => $value, + QUOTEVALUE => $args{'QUOTEVALUE'}, + ENTRYAGGREGATOR => $args{'ENTRYAGGREGATOR'}, + SUBCLAUSE => $clause, + ); + $self->SUPER::Limit( + ALIAS => $ObjectValuesAlias, + FIELD => 'Content', + OPERATOR => $args{'OPERATOR'}, + VALUE => $value, + QUOTEVALUE => $args{'QUOTEVALUE'}, + ENTRYAGGREGATOR => $args{'ENTRYAGGREGATOR'}, + SUBCLAUSE => $clause, + ); + } +} + +# }}} + +# {{{ LimitTopics +sub LimitTopics { + my $self = shift; + my @topics = @_; + + my $topics = $self->NewAlias('ObjectTopics'); + $self->Limit( + ALIAS => $topics, + FIELD => 'Topic', + VALUE => $_, + ENTRYAGGREGATOR => 'OR' + ) + for @topics; + + $self->Limit( + ALIAS => $topics, + FIELD => 'ObjectType', + VALUE => 'RT::Article', + ); + $self->Join( + ALIAS1 => 'main', + FIELD1 => 'id', + ALIAS2 => $topics, + FIELD2 => 'ObjectId', + ); +} + +# }}} + +# {{{ LimitRefersTo URI + +=head2 LimitRefersTo URI + +Limit the result set to only articles which are referred to by the URI passed in. + +=cut + +sub LimitRefersTo { + my $self = shift; + my $uri = shift; + + my $uri_obj = RT::URI->new($self->CurrentUser); + $uri_obj->FromURI($uri); + my $links = $self->NewAlias('Links'); + $self->Limit( + ALIAS => $links, + FIELD => 'Target', + VALUE => $uri_obj->URI + ); + + $self->Join( + ALIAS1 => 'main', + FIELD1 => 'URI', + ALIAS2 => $links, + FIELD2 => 'Base' + ); + +} + +# }}} + +# {{{ LimitReferredToBy URI + +=head2 LimitReferredToBy URI + +Limit the result set to only articles which are referred to by the URI passed in. + +=cut + +sub LimitReferredToBy { + my $self = shift; + my $uri = shift; + + my $uri_obj = RT::URI->new($self->CurrentUser); + $uri_obj->FromURI($uri); + my $links = $self->NewAlias('Links'); + $self->Limit( + ALIAS => $links, + FIELD => 'Base', + VALUE => $uri_obj->URI + ); + + $self->Join( + ALIAS1 => 'main', + FIELD1 => 'URI', + ALIAS2 => $links, + FIELD2 => 'Target' + ); + +} + +# }}} + +=head2 LimitHostlistClasses + +Only fetch Articles from classes where Hotlist is true. + +=cut + +sub LimitHotlistClasses { + my $self = shift; + + my $classes = $self->Join( + ALIAS1 => 'main', + FIELD1 => 'Class', + TABLE2 => 'Classes', + FIELD2 => 'id', + ); + $self->Limit( ALIAS => $classes, FIELD => 'HotList', VALUE => 1 ); +} + +=head2 LimitAppliedClasses Queue => QueueObj + +Takes a Queue and limits articles returned to classes which are applied to that Queue + +Accepts either a Queue obj or a Queue id + +=cut + +sub LimitAppliedClasses { + my $self = shift; + my %args = @_; + + unless (ref $args{Queue} || $args{Queue} =~/^[0-9]+$/) { + $RT::Logger->error("Not a valid Queue: $args{Queue}"); + return; + } + + my $queue = ( ref $args{Queue} ? $args{Queue}->Id : $args{Queue} ); + + my $oc_alias = $self->Join( + ALIAS1 => 'main', + FIELD1 => 'Class', + TABLE2 => 'ObjectClasses', + FIELD2 => 'Class' + ); + + my $subclause = "possibleobjectclasses"; + $self->_OpenParen($subclause); + $self->Limit( ALIAS => $oc_alias, + FIELD => 'ObjectId', + VALUE => $queue, + SUBCLAUSE => $subclause, + ENTRYAGGREGATOR => 'OR' ); + $self->Limit( ALIAS => $oc_alias, + FIELD => 'ObjectType', + VALUE => 'RT::Queue', + SUBCLAUSE => $subclause, + ENTRYAGGREGATOR => 'AND' ); + $self->_CloseParen($subclause); + + $self->_OpenParen($subclause); + $self->Limit( ALIAS => $oc_alias, + FIELD => 'ObjectId', + VALUE => 0, + SUBCLAUSE => $subclause, + ENTRYAGGREGATOR => 'OR' ); + $self->Limit( ALIAS => $oc_alias, + FIELD => 'ObjectType', + VALUE => 'RT::System', + SUBCLAUSE => $subclause, + ENTRYAGGREGATOR => 'AND' ); + $self->_CloseParen($subclause); + + return $self; + +} + +sub Search { + my $self = shift; + my %args = @_; + my $customfields = $args{CustomFields} + || RT::CustomFields->new( $self->CurrentUser ); + my $dates = $args{Dates} || {}; + my $order_by = $args{OrderBy}; + my $order = $args{Order}; + if ( $args{'q'} ) { + $self->Limit( + FIELD => 'Name', + SUBCLAUSE => 'NameOrSummary', + OPERATOR => 'LIKE', + ENTRYAGGREGATOR => 'OR', + CASESENSITIVE => 0, + VALUE => $args{'q'} + ); + $self->Limit( + FIELD => 'Summary', + SUBCLAUSE => 'NameOrSummary', + OPERATOR => 'LIKE', + ENTRYAGGREGATOR => 'OR', + CASESENSITIVE => 0, + VALUE => $args{'q'} + ); + } + + + require Time::ParseDate; + foreach my $date (qw(Created< Created> LastUpdated< LastUpdated>)) { + next unless ( $args{$date} ); + my $seconds = Time::ParseDate::parsedate( $args{$date}, FUZZY => 1, PREFER_PAST => 1 ); + my $date_obj = RT::Date->new( $self->CurrentUser ); + $date_obj->Set( Format => 'unix', Value => $seconds ); + $dates->{$date} = $date_obj; + + if ( $date =~ /^(.*?)<$/i ) { + $self->Limit( + FIELD => $1, + OPERATOR => "<=", + ENTRYAGGREGATOR => "AND", + VALUE => $date_obj->ISO + ); + } + + if ( $date =~ /^(.*?)>$/i ) { + $self->Limit( + FIELD => $1, + OPERATOR => ">=", + ENTRYAGGREGATOR => "AND", + VALUE => $date_obj->ISO + ); + } + + } + + if ($args{'RefersTo'}) { + foreach my $link ( split( /\s+/, $args{'RefersTo'} ) ) { + next unless ($link); + $self->LimitRefersTo($link); + } + } + + if ($args{'ReferredToBy'}) { + foreach my $link ( split( /\s+/, $args{'ReferredToBy'} ) ) { + next unless ($link); + $self->LimitReferredToBy($link); + } + } + + if ( $args{'Topics'} ) { + my @Topics = + ( ref $args{'Topics'} eq 'ARRAY' ) + ? @{ $args{'Topics'} } + : ( $args{'Topics'} ); + @Topics = map { split } @Topics; + if ( $args{'ExpandTopics'} ) { + my %topics; + while (@Topics) { + my $id = shift @Topics; + next if $topics{$id}; + my $Topics = + RT::Topics->new( $self->CurrentUser ); + $Topics->Limit( FIELD => 'Parent', VALUE => $id ); + push @Topics, $_->Id while $_ = $Topics->Next; + $topics{$id}++; + } + @Topics = keys %topics; + $args{'Topics'} = \@Topics; + } + $self->LimitTopics(@Topics); + } + + my %cfs; + $customfields->LimitToLookupType( + RT::Article->new( $self->CurrentUser ) + ->CustomFieldLookupType ); + if ( $args{'Class'} ) { + my @Classes = + ( ref $args{'Class'} eq 'ARRAY' ) + ? @{ $args{'Class'} } + : ( $args{'Class'} ); + foreach my $class (@Classes) { + $customfields->LimitToGlobalOrObjectId($class); + } + } + else { + $customfields->LimitToGlobalOrObjectId(); + } + while ( my $cf = $customfields->Next ) { + $cfs{ $cf->Name } = $cf->Id; + } + + # reset the iterator because we use this to build the UI + $customfields->GotoFirstItem; + + foreach my $field ( keys %cfs ) { + + my @MatchLike = + ( ref $args{ $field . "~" } eq 'ARRAY' ) + ? @{ $args{ $field . "~" } } + : ( $args{ $field . "~" } ); + my @NoMatchLike = + ( ref $args{ $field . "!~" } eq 'ARRAY' ) + ? @{ $args{ $field . "!~" } } + : ( $args{ $field . "!~" } ); + + my @Match = + ( ref $args{$field} eq 'ARRAY' ) + ? @{ $args{$field} } + : ( $args{$field} ); + my @NoMatch = + ( ref $args{ $field . "!" } eq 'ARRAY' ) + ? @{ $args{ $field . "!" } } + : ( $args{ $field . "!" } ); + + foreach my $val (@MatchLike) { + next unless $val; + push @Match, "~" . $val; + } + + foreach my $val (@NoMatchLike) { + next unless $val; + push @NoMatch, "~" . $val; + } + + foreach my $value (@Match) { + next unless $value; + my $op; + if ( $value =~ /^~(.*)$/ ) { + $value = "%$1%"; + $op = 'LIKE'; + } + else { + $op = '='; + } + $self->LimitCustomField( + FIELD => $cfs{$field}, + VALUE => $value, + CASESENSITIVE => 0, + ENTRYAGGREGATOR => 'OR', + OPERATOR => $op + ); + } + foreach my $value (@NoMatch) { + next unless $value; + my $op; + if ( $value =~ /^~(.*)$/ ) { + $value = "%$1%"; + $op = 'NOT LIKE'; + } + else { + $op = '!='; + } + $self->LimitCustomField( + FIELD => $cfs{$field}, + VALUE => $value, + CASESENSITIVE => 0, + ENTRYAGGREGATOR => 'OR', + OPERATOR => $op + ); + } + } + +### Searches for any field + + if ( $args{'Article~'} ) { + $self->LimitCustomField( + VALUE => $args{'Article~'}, + ENTRYAGGREGATOR => 'OR', + OPERATOR => 'LIKE', + CASESENSITIVE => 0, + SUBCLAUSE => 'SearchAll' + ); + $self->Limit( + SUBCLAUSE => 'SearchAll', + FIELD => "Name", + VALUE => $args{'Article~'}, + ENTRYAGGREGATOR => 'OR', + CASESENSITIVE => 0, + OPERATOR => 'LIKE' + ); + $self->Limit( + SUBCLAUSE => 'SearchAll', + FIELD => "Summary", + VALUE => $args{'Article~'}, + ENTRYAGGREGATOR => 'OR', + CASESENSITIVE => 0, + OPERATOR => 'LIKE' + ); + } + + if ( $args{'Article!~'} ) { + $self->LimitCustomField( + VALUE => $args{'Article!~'}, + OPERATOR => 'NOT LIKE', + CASESENSITIVE => 0, + SUBCLAUSE => 'SearchAll' + ); + $self->Limit( + SUBCLAUSE => 'SearchAll', + FIELD => "Name", + VALUE => $args{'Article!~'}, + ENTRYAGGREGATOR => 'AND', + CASESENSITIVE => 0, + OPERATOR => 'NOT LIKE' + ); + $self->Limit( + SUBCLAUSE => 'SearchAll', + FIELD => "Summary", + VALUE => $args{'Article!~'}, + ENTRYAGGREGATOR => 'AND', + CASESENSITIVE => 0, + OPERATOR => 'NOT LIKE' + ); + } + + foreach my $field (qw(Name Summary Class)) { + + my @MatchLike = + ( ref $args{ $field . "~" } eq 'ARRAY' ) + ? @{ $args{ $field . "~" } } + : ( $args{ $field . "~" } ); + my @NoMatchLike = + ( ref $args{ $field . "!~" } eq 'ARRAY' ) + ? @{ $args{ $field . "!~" } } + : ( $args{ $field . "!~" } ); + + my @Match = + ( ref $args{$field} eq 'ARRAY' ) + ? @{ $args{$field} } + : ( $args{$field} ); + my @NoMatch = + ( ref $args{ $field . "!" } eq 'ARRAY' ) + ? @{ $args{ $field . "!" } } + : ( $args{ $field . "!" } ); + + foreach my $val (@MatchLike) { + next unless $val; + push @Match, "~" . $val; + } + + foreach my $val (@NoMatchLike) { + next unless $val; + push @NoMatch, "~" . $val; + } + + my $op; + foreach my $value (@Match) { + if ( $value && $value =~ /^~(.*)$/ ) { + $value = "%$1%"; + $op = 'LIKE'; + } + else { + $op = '='; + } + + # preprocess Classes, so we can search on class + if ( $field eq 'Class' && $value ) { + my $class = RT::Class->new($RT::SystemUser); + $class->Load($value); + $value = $class->Id; + } + + # now that we've pruned the value, get out if it's different. + next unless $value; + + $self->Limit( + SUBCLAUSE => $field . 'Match', + FIELD => $field, + OPERATOR => $op, + CASESENSITIVE => 0, + VALUE => $value, + ENTRYAGGREGATOR => 'OR' + ); + + } + foreach my $value (@NoMatch) { + + # preprocess Classes, so we can search on class + if ( $value && $value =~ /^~(.*)/ ) { + $value = "%$1%"; + $op = 'NOT LIKE'; + } + else { + $op = '!='; + } + if ( $field eq 'Class' ) { + my $class = RT::Class->new($RT::SystemUser); + $class->Load($value); + $value = $class->Id; + } + + # now that we've pruned the value, get out if it's different. + next unless $value; + + $self->Limit( + SUBCLAUSE => $field . 'NoMatch', + OPERATOR => $op, + VALUE => $value, + CASESENSITIVE => 0, + FIELD => $field, + ENTRYAGGREGATOR => 'AND' + ); + + } + } + + if ($order_by && @$order_by) { + if ( $order_by->[0] && $order_by->[0] =~ /\|/ ) { + @$order_by = split '|', $order_by->[0]; + @$order = split '|', $order->[0]; + } + my @tmp = + map { { FIELD => $order_by->[$_], ORDER => $order->[$_] } } 0 .. $#{$order_by}; + $self->OrderByCols(@tmp); + } + + return 1; +} + + +=head2 NewItem + +Returns an empty new RT::Article item + +=cut + +sub NewItem { + my $self = shift; + return(RT::Article->new($self->CurrentUser)); +} + + + +RT::Base->_ImportOverlays(); + +1; + +1;