1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2017 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
53 use base 'RT::Record';
55 use Role::Basic 'with';
56 with "RT::Record::Role::Links" => { -excludes => ["AddLink", "_AddLinksOnCreate"] };
63 use RT::URI::fsck_com_article;
67 sub Table {'Articles'}
69 # This object takes custom fields
72 RT::CustomField->RegisterLookupType( CustomFieldLookupType() => 'Articles' ); #loc
76 =head2 Create PARAMHASH
78 Create takes a hash of values and creates a row in the database:
81 varchar(200) 'Summary'.
85 A paramhash called 'CustomFields', which contains
86 arrays of values for each custom field you want to fill in.
106 my $class = RT::Class->new( $self->CurrentUser );
107 $class->Load( $args{'Class'} );
108 unless ( $class->Id ) {
109 return ( 0, $self->loc('Invalid Class') );
112 unless ( $class->CurrentUserHasRight('CreateArticle') ) {
113 return ( 0, $self->loc("Permission Denied") );
116 return ( undef, $self->loc('Name in use') )
117 unless $self->ValidateName( $args{'Name'} );
119 $RT::Handle->BeginTransaction();
120 my ( $id, $msg ) = $self->SUPER::Create(
121 Name => $args{'Name'},
123 Summary => $args{'Summary'},
126 $RT::Handle->Rollback();
127 return ( undef, $msg );
130 # {{{ Add custom fields
132 foreach my $key ( keys %args ) {
133 next unless ( $key =~ /CustomField-(.*)$/ );
135 my @vals = ref( $args{$key} ) eq 'ARRAY' ? @{ $args{$key} } : ( $args{$key} );
136 foreach my $value (@vals) {
138 my ( $cfid, $cfmsg ) = $self->_AddCustomFieldValue(
139 (UNIVERSAL::isa( $value => 'HASH' )
144 RecordTransaction => 0
148 $RT::Handle->Rollback();
149 return ( undef, $cfmsg );
158 foreach my $topic ( @{ $args{Topics} } ) {
159 my ( $cfid, $cfmsg ) = $self->AddTopic( Topic => $topic );
162 $RT::Handle->Rollback();
163 return ( undef, $cfmsg );
168 # {{{ Add relationships
170 foreach my $type ( keys %args ) {
171 next unless ( $type =~ /^(RefersTo-new|new-RefersTo)$/ );
173 ref( $args{$type} ) eq 'ARRAY' ? @{ $args{$type} } : ( $args{$type} );
174 foreach my $val (@vals) {
175 my ( $base, $target );
176 if ( $type =~ /^new-(.*)$/ ) {
181 elsif ( $type =~ /^(.*)-new$/ ) {
187 my ( $linkid, $linkmsg ) = $self->AddLink(
191 RecordTransaction => 0
195 $RT::Handle->Rollback();
196 return ( undef, $linkmsg );
204 # We override the URI lookup. the whole reason
205 # we have a URI column is so that joins on the links table
206 # aren't expensive and stupid
207 $self->__Set( Field => 'URI', Value => $self->URI );
209 my ( $txn_id, $txn_msg, $txn ) = $self->_NewTransaction( Type => 'Create' );
211 $RT::Handle->Rollback();
212 return ( undef, $self->loc( 'Internal error: [_1]', $txn_msg ) );
214 $RT::Handle->Commit();
216 return ( $id, $self->loc('Article [_1] created',$self->id ));
223 =head2 ValidateName NAME
225 Takes a string name. Returns true if that name isn't in use by another article
227 Empty names are permitted.
240 my $temp = RT::Article->new($RT::SystemUser);
241 $temp->LoadByCols( Name => $name );
243 (!$self->id || ($temp->id != $self->id ))) {
257 Delete all its transactions
258 Delete all its custom field values
259 Delete all its relationships
266 unless ( $self->CurrentUserHasRight('DeleteArticle') ) {
267 return ( 0, $self->loc("Permission Denied") );
270 $RT::Handle->BeginTransaction();
271 my $linksto = $self->_Links( 'Target' );
272 my $linksfrom = $self->_Links( 'Base' );
273 my $cfvalues = $self->CustomFieldValues;
274 my $txns = $self->Transactions;
275 my $topics = $self->Topics;
277 while ( my $item = $linksto->Next ) {
278 my ( $val, $msg ) = $item->Delete();
280 $RT::Logger->crit( ref($item) . ": $msg" );
281 $RT::Handle->Rollback();
282 return ( 0, $self->loc('Internal Error') );
286 while ( my $item = $linksfrom->Next ) {
287 my ( $val, $msg ) = $item->Delete();
289 $RT::Logger->crit( ref($item) . ": $msg" );
290 $RT::Handle->Rollback();
291 return ( 0, $self->loc('Internal Error') );
295 while ( my $item = $txns->Next ) {
296 my ( $val, $msg ) = $item->Delete();
298 $RT::Logger->crit( ref($item) . ": $msg" );
299 $RT::Handle->Rollback();
300 return ( 0, $self->loc('Internal Error') );
304 while ( my $item = $cfvalues->Next ) {
305 my ( $val, $msg ) = $item->Delete();
307 $RT::Logger->crit( ref($item) . ": $msg" );
308 $RT::Handle->Rollback();
309 return ( 0, $self->loc('Internal Error') );
313 while ( my $item = $topics->Next ) {
314 my ( $val, $msg ) = $item->Delete();
316 $RT::Logger->crit( ref($item) . ": $msg" );
317 $RT::Handle->Rollback();
318 return ( 0, $self->loc('Internal Error') );
322 $self->SUPER::Delete();
323 $RT::Handle->Commit();
324 return ( 1, $self->loc('Article Deleted') );
334 Returns an RT::Articles object which contains
335 all articles which have this article as their parent. This
336 routine will not recurse and will not find grandchildren, great-grandchildren, uncles, aunts, nephews or any other such thing.
342 my $kids = RT::Articles->new( $self->CurrentUser );
344 unless ( $self->CurrentUserHasRight('ShowArticle') ) {
345 $kids->LimitToParent( $self->Id );
356 Takes a paramhash of Type and one of Base or Target. Adds that link to this article.
358 Prevents the use of plain numbers to avoid confusing behaviour.
372 unless ( $self->CurrentUserHasRight('ModifyArticle') ) {
373 return ( 0, $self->loc("Permission Denied") );
376 # Disallow parsing of plain numbers in article links. If they are
377 # allowed, they default to being tickets instead of articles, which
378 # is counterintuitive.
379 if ( $args{'Target'} && $args{'Target'} =~ /^\d+$/
380 || $args{'Base'} && $args{'Base'} =~ /^\d+$/ )
382 return ( 0, $self->loc("Cannot add link to plain number") );
385 $self->_AddLink(%args);
391 unless ( $self->CurrentUserHasRight('ShowArticle') ) {
392 return $self->loc("Permission Denied");
395 my $uri = RT::URI::fsck_com_article->new( $self->CurrentUser );
396 return ( $uri->URIForObject($self) );
405 Returns this article's URI
412 my $uri = RT::URI->new( $self->CurrentUser );
413 if ( $self->CurrentUserHasRight('ShowArticle') ) {
414 $uri->FromObject($self);
429 my $topics = RT::ObjectTopics->new( $self->CurrentUser );
430 if ( $self->CurrentUserHasRight('ShowArticle') ) {
431 $topics->LimitToObject($self);
443 unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
444 return ( 0, $self->loc("Permission Denied") );
447 my $t = RT::ObjectTopic->new( $self->CurrentUser );
448 my ($tid) = $t->Create(
449 Topic => $args{'Topic'},
450 ObjectType => ref($self),
451 ObjectId => $self->Id
454 return ( $tid, $self->loc("Topic membership added") );
457 return ( 0, $self->loc("Unable to add topic membership") );
467 unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
468 return ( 0, $self->loc("Permission Denied") );
471 my $t = RT::ObjectTopic->new( $self->CurrentUser );
473 Topic => $args{'Topic'},
474 ObjectId => $self->Id,
475 ObjectType => ref($self)
478 my $del = $t->Delete;
483 "Unable to delete topic membership in [_1]",
489 return ( 1, $self->loc("Topic membership removed") );
496 "Couldn't load topic membership while trying to delete it")
501 =head2 CurrentUserCanSee
503 Returns true if the current user can see the article, using ShowArticle
507 sub CurrentUserCanSee {
509 return $self->CurrentUserHasRight('ShowArticle');
516 =head2 _Set { Field => undef, Value => undef
518 Internal helper method to record a transaction as we update some core field of the article
531 unless ( $self->CurrentUserHasRight('ModifyArticle') ) {
532 return ( 0, $self->loc("Permission Denied") );
535 $self->_NewTransaction(
537 Field => $args{'Field'},
538 NewValue => $args{'Value'},
539 OldValue => $self->__Value( $args{'Field'} )
542 return ( $self->SUPER::_Set(%args) );
548 Return "PARAM" for this object. if the current user doesn't have rights, returns undef
555 unless ( ( $arg eq 'Class' )
556 || ( $self->CurrentUserHasRight('ShowArticle') ) )
560 return $self->SUPER::_Value($arg);
565 sub CustomFieldLookupType {
566 "RT::Class-RT::Article";
570 sub ACLEquivalenceObjects {
572 return $self->ClassObj;
575 sub ModifyLinkRight { "ModifyArticle" }
577 =head2 LoadByInclude Field Value
579 Takes the name of a form field from "Include Article"
580 and the value submitted by the browser and attempts to load an Article.
582 This handles Articles included by searching, by the Name and via
585 If you optionaly pass an id as the Queue argument, this will check that
586 the Article's Class is applied to that Queue.
593 my $Field = $args{Field};
594 my $Value = $args{Value};
595 my $Queue = $args{Queue};
597 return unless $Field;
600 if ( $Field eq 'Articles-Include-Article' && $Value ) {
601 ($ok, $msg) = $self->Load( $Value );
602 } elsif ( $Field =~ /^Articles-Include-Article-(\d+)$/ ) {
603 ($ok, $msg) = $self->Load( $1 );
604 } elsif ( $Field =~ /^Articles-Include-Article-Named/ && $Value ) {
605 if ( $Value =~ /\D/ ) {
606 ($ok, $msg) = $self->LoadByCols( Name => $Value );
608 ($ok, $msg) = $self->LoadByCols( id => $Value );
612 unless ($ok) { # load failed, don't check Class
613 return wantarray ? ($ok, $msg) : $ok;
616 unless ($Queue) { # we haven't requested extra sanity checking
617 return wantarray ? ($ok, $msg) : $ok;
620 # ensure that this article is available for the Queue we're
622 my $class = $self->ClassObj;
623 unless ($class->IsApplied(0) || $class->IsApplied($Queue)) {
625 return wantarray ? (0, $self->loc("The Class of the Article identified by [_1] is not applied to the current Queue",$Value)) : 0;
628 return wantarray ? ($ok, $msg) : $ok;
635 Returns the current value of id.
636 (In the database, id is stored as int(11).)
644 Returns the current value of Name.
645 (In the database, Name is stored as varchar(255).)
653 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
654 (In the database, Name will be stored as a varchar(255).)
662 Returns the current value of Summary.
663 (In the database, Summary is stored as varchar(255).)
667 =head2 SetSummary VALUE
670 Set Summary to VALUE.
671 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
672 (In the database, Summary will be stored as a varchar(255).)
680 Returns the current value of SortOrder.
681 (In the database, SortOrder is stored as int(11).)
685 =head2 SetSortOrder VALUE
688 Set SortOrder to VALUE.
689 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
690 (In the database, SortOrder will be stored as a int(11).)
698 Returns the current value of Class.
699 (In the database, Class is stored as int(11).)
703 =head2 SetClass VALUE
707 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
708 (In the database, Class will be stored as a int(11).)
716 Returns the Class Object which has the id returned by Class
723 my $Class = RT::Class->new($self->CurrentUser);
724 $Class->Load($self->Class());
730 Returns the current value of Parent.
731 (In the database, Parent is stored as int(11).)
735 =head2 SetParent VALUE
739 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
740 (In the database, Parent will be stored as a int(11).)
748 Returns the current value of URI.
749 (In the database, URI is stored as varchar(255).)
757 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
758 (In the database, URI will be stored as a varchar(255).)
766 Returns the current value of Creator.
767 (In the database, Creator is stored as int(11).)
775 Returns the current value of Created.
776 (In the database, Created is stored as datetime.)
784 Returns the current value of LastUpdatedBy.
785 (In the database, LastUpdatedBy is stored as int(11).)
793 Returns the current value of LastUpdated.
794 (In the database, LastUpdated is stored as datetime.)
801 sub _CoreAccessible {
805 {read => 1, type => 'int(11)', default => ''},
807 {read => 1, write => 1, type => 'varchar(255)', default => ''},
809 {read => 1, write => 1, type => 'varchar(255)', default => ''},
811 {read => 1, write => 1, type => 'int(11)', default => '0'},
813 {read => 1, write => 1, type => 'int(11)', default => '0'},
815 {read => 1, write => 1, type => 'int(11)', default => '0'},
817 {read => 1, write => 1, type => 'varchar(255)', default => ''},
819 {read => 1, auto => 1, type => 'int(11)', default => '0'},
821 {read => 1, auto => 1, type => 'datetime', default => ''},
823 {read => 1, auto => 1, type => 'int(11)', default => '0'},
825 {read => 1, auto => 1, type => 'datetime', default => ''},
830 sub FindDependencies {
832 my ($walker, $deps) = @_;
834 $self->SUPER::FindDependencies($walker, $deps);
837 my $links = RT::Links->new( $self->CurrentUser );
839 SUBCLAUSE => "either",
842 ENTRYAGGREGATOR => 'OR'
843 ) for qw/Base Target/;
844 $deps->Add( in => $links );
846 $deps->Add( out => $self->ClassObj );
847 $deps->Add( in => $self->Topics );
853 $self->__Set( Field => 'URI', Value => $self->URI );
856 RT::Base->_ImportOverlays();