1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2012 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($RT::SystemUser);
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 $uri_obj->FromURI( $args{'Target'}||$args{'Base'} );
403 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
404 my $msg = $self->loc( "Couldn't resolve '[_1]' into a Link.", $args{'Target'} );
405 $RT::Logger->warning( $msg );
410 $self->_AddLink(%args);
416 unless ( $self->CurrentUserHasRight('ShowArticle') ) {
417 return $self->loc("Permission Denied");
420 my $uri = RT::URI::fsck_com_article->new( $self->CurrentUser );
421 return ( $uri->URIForObject($self) );
430 Returns this article's URI
437 my $uri = RT::URI->new( $self->CurrentUser );
438 if ( $self->CurrentUserHasRight('ShowArticle') ) {
439 $uri->FromObject($self);
454 my $topics = RT::ObjectTopics->new( $self->CurrentUser );
455 if ( $self->CurrentUserHasRight('ShowArticle') ) {
456 $topics->LimitToObject($self);
468 unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
469 return ( 0, $self->loc("Permission Denied") );
472 my $t = RT::ObjectTopic->new( $self->CurrentUser );
473 my ($tid) = $t->Create(
474 Topic => $args{'Topic'},
475 ObjectType => ref($self),
476 ObjectId => $self->Id
479 return ( $tid, $self->loc("Topic membership added") );
482 return ( 0, $self->loc("Unable to add topic membership") );
492 unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
493 return ( 0, $self->loc("Permission Denied") );
496 my $t = RT::ObjectTopic->new( $self->CurrentUser );
498 Topic => $args{'Topic'},
499 ObjectId => $self->Id,
500 ObjectType => ref($self)
503 my $del = $t->Delete;
508 "Unable to delete topic membership in [_1]",
514 return ( 1, $self->loc("Topic membership removed") );
521 "Couldn't load topic membership while trying to delete it")
526 =head2 CurrentUserHasRight
528 Returns true if the current user has the right for this article, for the whole system or for this article's class
532 sub CurrentUserHasRight {
537 $self->CurrentUser->HasRight(
540 EquivObjects => [ $RT::System, $RT::System, $self->ClassObj ]
546 =head2 CurrentUserCanSee
548 Returns true if the current user can see the article, using ShowArticle
552 sub CurrentUserCanSee {
554 return $self->CurrentUserHasRight('ShowArticle');
561 =head2 _Set { Field => undef, Value => undef
563 Internal helper method to record a transaction as we update some core field of the article
576 unless ( $self->CurrentUserHasRight('ModifyArticle') ) {
577 return ( 0, $self->loc("Permission Denied") );
580 $self->_NewTransaction(
582 Field => $args{'Field'},
583 NewValue => $args{'Value'},
584 OldValue => $self->__Value( $args{'Field'} )
587 return ( $self->SUPER::_Set(%args) );
593 Return "PARAM" for this object. if the current user doesn't have rights, returns undef
600 unless ( ( $arg eq 'Class' )
601 || ( $self->CurrentUserHasRight('ShowArticle') ) )
605 return $self->SUPER::_Value($arg);
610 sub CustomFieldLookupType {
611 "RT::Class-RT::Article";
614 # _LookupId is the id of the toplevel type object the customfield is joined to
615 # in this case, that's an RT::Class.
619 return $self->ClassObj->id;
623 =head2 LoadByInclude Field Value
625 Takes the name of a form field from "Include Article"
626 and the value submitted by the browser and attempts to load an Article.
628 This handles Articles included by searching, by the Name and via
631 If you optionaly pass an id as the Queue argument, this will check that
632 the Article's Class is applied to that Queue.
639 my $Field = $args{Field};
640 my $Value = $args{Value};
641 my $Queue = $args{Queue};
643 return unless $Field;
646 if ( $Field eq 'Articles-Include-Article' && $Value ) {
647 ($ok, $msg) = $self->Load( $Value );
648 } elsif ( $Field =~ /^Articles-Include-Article-(\d+)$/ ) {
649 ($ok, $msg) = $self->Load( $1 );
650 } elsif ( $Field =~ /^Articles-Include-Article-Named/ && $Value ) {
651 if ( $Value =~ /\D/ ) {
652 ($ok, $msg) = $self->LoadByCols( Name => $Value );
654 ($ok, $msg) = $self->LoadByCols( id => $Value );
658 unless ($ok) { # load failed, don't check Class
662 unless ($Queue) { # we haven't requested extra sanity checking
666 # ensure that this article is available for the Queue we're
668 my $class = $self->ClassObj;
669 unless ($class->IsApplied(0) || $class->IsApplied($Queue)) {
671 return (0, $self->loc("The Class of the Article identified by [_1] is not applied to the current Queue",$Value));
681 Returns the current value of id.
682 (In the database, id is stored as int(11).)
690 Returns the current value of Name.
691 (In the database, Name is stored as varchar(255).)
699 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
700 (In the database, Name will be stored as a varchar(255).)
708 Returns the current value of Summary.
709 (In the database, Summary is stored as varchar(255).)
713 =head2 SetSummary VALUE
716 Set Summary to VALUE.
717 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
718 (In the database, Summary will be stored as a varchar(255).)
726 Returns the current value of SortOrder.
727 (In the database, SortOrder is stored as int(11).)
731 =head2 SetSortOrder VALUE
734 Set SortOrder to VALUE.
735 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
736 (In the database, SortOrder will be stored as a int(11).)
744 Returns the current value of Class.
745 (In the database, Class is stored as int(11).)
749 =head2 SetClass VALUE
753 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
754 (In the database, Class will be stored as a int(11).)
762 Returns the Class Object which has the id returned by Class
769 my $Class = RT::Class->new($self->CurrentUser);
770 $Class->Load($self->Class());
776 Returns the current value of Parent.
777 (In the database, Parent is stored as int(11).)
781 =head2 SetParent VALUE
785 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
786 (In the database, Parent will be stored as a int(11).)
794 Returns the current value of URI.
795 (In the database, URI is stored as varchar(255).)
803 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
804 (In the database, URI will be stored as a varchar(255).)
812 Returns the current value of Creator.
813 (In the database, Creator is stored as int(11).)
821 Returns the current value of Created.
822 (In the database, Created is stored as datetime.)
830 Returns the current value of LastUpdatedBy.
831 (In the database, LastUpdatedBy is stored as int(11).)
839 Returns the current value of LastUpdated.
840 (In the database, LastUpdated is stored as datetime.)
847 sub _CoreAccessible {
851 {read => 1, type => 'int(11)', default => ''},
853 {read => 1, write => 1, type => 'varchar(255)', default => ''},
855 {read => 1, write => 1, type => 'varchar(255)', default => ''},
857 {read => 1, write => 1, type => 'int(11)', default => '0'},
859 {read => 1, write => 1, type => 'int(11)', default => '0'},
861 {read => 1, write => 1, type => 'int(11)', default => '0'},
863 {read => 1, write => 1, type => 'varchar(255)', default => ''},
865 {read => 1, auto => 1, type => 'int(11)', default => '0'},
867 {read => 1, auto => 1, type => 'datetime', default => ''},
869 {read => 1, auto => 1, type => 'int(11)', default => '0'},
871 {read => 1, auto => 1, type => 'datetime', default => ''},
876 RT::Base->_ImportOverlays();