1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2014 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 }}}
54 use base 'RT::Record';
61 use RT::URI::fsck_com_article;
65 sub Table {'Articles'}
67 # This object takes custom fields
70 RT::CustomField->_ForObjectType( CustomFieldLookupType() => 'Articles' )
75 =head2 Create PARAMHASH
77 Create takes a hash of values and creates a row in the database:
80 varchar(200) 'Summary'.
84 A paramhash called 'CustomFields', which contains
85 arrays of values for each custom field you want to fill in.
105 my $class = RT::Class->new( $self->CurrentUser );
106 $class->Load( $args{'Class'} );
107 unless ( $class->Id ) {
108 return ( 0, $self->loc('Invalid Class') );
111 unless ( $class->CurrentUserHasRight('CreateArticle') ) {
112 return ( 0, $self->loc("Permission Denied") );
115 return ( undef, $self->loc('Name in use') )
116 unless $self->ValidateName( $args{'Name'} );
118 $RT::Handle->BeginTransaction();
119 my ( $id, $msg ) = $self->SUPER::Create(
120 Name => $args{'Name'},
122 Summary => $args{'Summary'},
125 $RT::Handle->Rollback();
126 return ( undef, $msg );
129 # {{{ Add custom fields
131 foreach my $key ( keys %args ) {
132 next unless ( $key =~ /CustomField-(.*)$/ );
134 my @vals = ref( $args{$key} ) eq 'ARRAY' ? @{ $args{$key} } : ( $args{$key} );
135 foreach my $value (@vals) {
137 my ( $cfid, $cfmsg ) = $self->_AddCustomFieldValue(
138 (UNIVERSAL::isa( $value => 'HASH' )
143 RecordTransaction => 0
147 $RT::Handle->Rollback();
148 return ( undef, $cfmsg );
157 foreach my $topic ( @{ $args{Topics} } ) {
158 my ( $cfid, $cfmsg ) = $self->AddTopic( Topic => $topic );
161 $RT::Handle->Rollback();
162 return ( undef, $cfmsg );
167 # {{{ Add relationships
169 foreach my $type ( keys %args ) {
170 next unless ( $type =~ /^(RefersTo-new|new-RefersTo)$/ );
172 ref( $args{$type} ) eq 'ARRAY' ? @{ $args{$type} } : ( $args{$type} );
173 foreach my $val (@vals) {
174 my ( $base, $target );
175 if ( $type =~ /^new-(.*)$/ ) {
180 elsif ( $type =~ /^(.*)-new$/ ) {
186 my ( $linkid, $linkmsg ) = $self->AddLink(
190 RecordTransaction => 0
194 $RT::Handle->Rollback();
195 return ( undef, $linkmsg );
203 # We override the URI lookup. the whole reason
204 # we have a URI column is so that joins on the links table
205 # aren't expensive and stupid
206 $self->__Set( Field => 'URI', Value => $self->URI );
208 my ( $txn_id, $txn_msg, $txn ) = $self->_NewTransaction( Type => 'Create' );
210 $RT::Handle->Rollback();
211 return ( undef, $self->loc( 'Internal error: [_1]', $txn_msg ) );
213 $RT::Handle->Commit();
215 return ( $id, $self->loc('Article [_1] created',$self->id ));
222 =head2 ValidateName NAME
224 Takes a string name. Returns true if that name isn't in use by another article
226 Empty names are permitted.
239 my $temp = RT::Article->new($RT::SystemUser);
240 $temp->LoadByCols( Name => $name );
242 (!$self->id || ($temp->id != $self->id ))) {
256 Delete all its transactions
257 Delete all its custom field values
258 Delete all its relationships
265 unless ( $self->CurrentUserHasRight('DeleteArticle') ) {
266 return ( 0, $self->loc("Permission Denied") );
269 $RT::Handle->BeginTransaction();
270 my $linksto = $self->_Links( 'Target' );
271 my $linksfrom = $self->_Links( 'Base' );
272 my $cfvalues = $self->CustomFieldValues;
273 my $txns = $self->Transactions;
274 my $topics = $self->Topics;
276 while ( my $item = $linksto->Next ) {
277 my ( $val, $msg ) = $item->Delete();
279 $RT::Logger->crit( ref($item) . ": $msg" );
280 $RT::Handle->Rollback();
281 return ( 0, $self->loc('Internal Error') );
285 while ( my $item = $linksfrom->Next ) {
286 my ( $val, $msg ) = $item->Delete();
288 $RT::Logger->crit( ref($item) . ": $msg" );
289 $RT::Handle->Rollback();
290 return ( 0, $self->loc('Internal Error') );
294 while ( my $item = $txns->Next ) {
295 my ( $val, $msg ) = $item->Delete();
297 $RT::Logger->crit( ref($item) . ": $msg" );
298 $RT::Handle->Rollback();
299 return ( 0, $self->loc('Internal Error') );
303 while ( my $item = $cfvalues->Next ) {
304 my ( $val, $msg ) = $item->Delete();
306 $RT::Logger->crit( ref($item) . ": $msg" );
307 $RT::Handle->Rollback();
308 return ( 0, $self->loc('Internal Error') );
312 while ( my $item = $topics->Next ) {
313 my ( $val, $msg ) = $item->Delete();
315 $RT::Logger->crit( ref($item) . ": $msg" );
316 $RT::Handle->Rollback();
317 return ( 0, $self->loc('Internal Error') );
321 $self->SUPER::Delete();
322 $RT::Handle->Commit();
323 return ( 1, $self->loc('Article Deleted') );
333 Returns an RT::Articles object which contains
334 all articles which have this article as their parent. This
335 routine will not recurse and will not find grandchildren, great-grandchildren, uncles, aunts, nephews or any other such thing.
341 my $kids = RT::Articles->new( $self->CurrentUser );
343 unless ( $self->CurrentUserHasRight('ShowArticle') ) {
344 $kids->LimitToParent( $self->Id );
355 Takes a paramhash of Type and one of Base or Target. Adds that link to this tick
370 unless ( $self->CurrentUserHasRight('ModifyArticle') ) {
371 return ( 0, $self->loc("Permission Denied") );
374 $self->_DeleteLink(%args);
387 unless ( $self->CurrentUserHasRight('ModifyArticle') ) {
388 return ( 0, $self->loc("Permission Denied") );
391 # Disallow parsing of plain numbers in article links. If they are
392 # allowed, they default to being tickets instead of articles, which
393 # is counterintuitive.
394 if ( $args{'Target'} && $args{'Target'} =~ /^\d+$/
395 || $args{'Base'} && $args{'Base'} =~ /^\d+$/ )
397 return ( 0, $self->loc("Cannot add link to plain number") );
400 # Check that we're actually getting a valid URI
401 my $uri_obj = RT::URI->new( $self->CurrentUser );
402 unless ( $uri_obj->FromURI( $args{'Target'}||$args{'Base'} )) {
403 my $msg = $self->loc( "Couldn't resolve '[_1]' into a Link.", $args{'Target'} || $args{'Base'} );
404 $RT::Logger->warning( $msg );
409 $self->_AddLink(%args);
415 unless ( $self->CurrentUserHasRight('ShowArticle') ) {
416 return $self->loc("Permission Denied");
419 my $uri = RT::URI::fsck_com_article->new( $self->CurrentUser );
420 return ( $uri->URIForObject($self) );
429 Returns this article's URI
436 my $uri = RT::URI->new( $self->CurrentUser );
437 if ( $self->CurrentUserHasRight('ShowArticle') ) {
438 $uri->FromObject($self);
453 my $topics = RT::ObjectTopics->new( $self->CurrentUser );
454 if ( $self->CurrentUserHasRight('ShowArticle') ) {
455 $topics->LimitToObject($self);
467 unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
468 return ( 0, $self->loc("Permission Denied") );
471 my $t = RT::ObjectTopic->new( $self->CurrentUser );
472 my ($tid) = $t->Create(
473 Topic => $args{'Topic'},
474 ObjectType => ref($self),
475 ObjectId => $self->Id
478 return ( $tid, $self->loc("Topic membership added") );
481 return ( 0, $self->loc("Unable to add topic membership") );
491 unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
492 return ( 0, $self->loc("Permission Denied") );
495 my $t = RT::ObjectTopic->new( $self->CurrentUser );
497 Topic => $args{'Topic'},
498 ObjectId => $self->Id,
499 ObjectType => ref($self)
502 my $del = $t->Delete;
507 "Unable to delete topic membership in [_1]",
513 return ( 1, $self->loc("Topic membership removed") );
520 "Couldn't load topic membership while trying to delete it")
525 =head2 CurrentUserHasRight
527 Returns true if the current user has the right for this article, for the whole system or for this article's class
531 sub CurrentUserHasRight {
536 $self->CurrentUser->HasRight(
539 EquivObjects => [ $RT::System, $RT::System, $self->ClassObj ]
545 =head2 CurrentUserCanSee
547 Returns true if the current user can see the article, using ShowArticle
551 sub CurrentUserCanSee {
553 return $self->CurrentUserHasRight('ShowArticle');
560 =head2 _Set { Field => undef, Value => undef
562 Internal helper method to record a transaction as we update some core field of the article
575 unless ( $self->CurrentUserHasRight('ModifyArticle') ) {
576 return ( 0, $self->loc("Permission Denied") );
579 $self->_NewTransaction(
581 Field => $args{'Field'},
582 NewValue => $args{'Value'},
583 OldValue => $self->__Value( $args{'Field'} )
586 return ( $self->SUPER::_Set(%args) );
592 Return "PARAM" for this object. if the current user doesn't have rights, returns undef
599 unless ( ( $arg eq 'Class' )
600 || ( $self->CurrentUserHasRight('ShowArticle') ) )
604 return $self->SUPER::_Value($arg);
609 sub CustomFieldLookupType {
610 "RT::Class-RT::Article";
613 =head2 LoadByInclude Field Value
615 Takes the name of a form field from "Include Article"
616 and the value submitted by the browser and attempts to load an Article.
618 This handles Articles included by searching, by the Name and via
621 If you optionaly pass an id as the Queue argument, this will check that
622 the Article's Class is applied to that Queue.
629 my $Field = $args{Field};
630 my $Value = $args{Value};
631 my $Queue = $args{Queue};
633 return unless $Field;
636 if ( $Field eq 'Articles-Include-Article' && $Value ) {
637 ($ok, $msg) = $self->Load( $Value );
638 } elsif ( $Field =~ /^Articles-Include-Article-(\d+)$/ ) {
639 ($ok, $msg) = $self->Load( $1 );
640 } elsif ( $Field =~ /^Articles-Include-Article-Named/ && $Value ) {
641 if ( $Value =~ /\D/ ) {
642 ($ok, $msg) = $self->LoadByCols( Name => $Value );
644 ($ok, $msg) = $self->LoadByCols( id => $Value );
648 unless ($ok) { # load failed, don't check Class
652 unless ($Queue) { # we haven't requested extra sanity checking
656 # ensure that this article is available for the Queue we're
658 my $class = $self->ClassObj;
659 unless ($class->IsApplied(0) || $class->IsApplied($Queue)) {
661 return (0, $self->loc("The Class of the Article identified by [_1] is not applied to the current Queue",$Value));
671 Returns the current value of id.
672 (In the database, id is stored as int(11).)
680 Returns the current value of Name.
681 (In the database, Name is stored as varchar(255).)
689 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
690 (In the database, Name will be stored as a varchar(255).)
698 Returns the current value of Summary.
699 (In the database, Summary is stored as varchar(255).)
703 =head2 SetSummary VALUE
706 Set Summary to VALUE.
707 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
708 (In the database, Summary will be stored as a varchar(255).)
716 Returns the current value of SortOrder.
717 (In the database, SortOrder is stored as int(11).)
721 =head2 SetSortOrder VALUE
724 Set SortOrder to VALUE.
725 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
726 (In the database, SortOrder will be stored as a int(11).)
734 Returns the current value of Class.
735 (In the database, Class is stored as int(11).)
739 =head2 SetClass VALUE
743 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
744 (In the database, Class will be stored as a int(11).)
752 Returns the Class Object which has the id returned by Class
759 my $Class = RT::Class->new($self->CurrentUser);
760 $Class->Load($self->Class());
766 Returns the current value of Parent.
767 (In the database, Parent is stored as int(11).)
771 =head2 SetParent VALUE
775 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
776 (In the database, Parent will be stored as a int(11).)
784 Returns the current value of URI.
785 (In the database, URI is stored as varchar(255).)
793 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
794 (In the database, URI will be stored as a varchar(255).)
802 Returns the current value of Creator.
803 (In the database, Creator is stored as int(11).)
811 Returns the current value of Created.
812 (In the database, Created is stored as datetime.)
820 Returns the current value of LastUpdatedBy.
821 (In the database, LastUpdatedBy is stored as int(11).)
829 Returns the current value of LastUpdated.
830 (In the database, LastUpdated is stored as datetime.)
837 sub _CoreAccessible {
841 {read => 1, type => 'int(11)', default => ''},
843 {read => 1, write => 1, type => 'varchar(255)', default => ''},
845 {read => 1, write => 1, type => 'varchar(255)', default => ''},
847 {read => 1, write => 1, type => 'int(11)', default => '0'},
849 {read => 1, write => 1, type => 'int(11)', default => '0'},
851 {read => 1, write => 1, type => 'int(11)', default => '0'},
853 {read => 1, write => 1, type => 'varchar(255)', default => ''},
855 {read => 1, auto => 1, type => 'int(11)', default => '0'},
857 {read => 1, auto => 1, type => 'datetime', default => ''},
859 {read => 1, auto => 1, type => 'int(11)', default => '0'},
861 {read => 1, auto => 1, type => 'datetime', default => ''},
866 RT::Base->_ImportOverlays();