]> git.uio.no Git - usit-rt.git/blame - lib/RT/Article.pm
Merge branch 'master' of git.uio.no:usit-rt
[usit-rt.git] / lib / RT / Article.pm
CommitLineData
84fb5b46
MKG
1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
320f0092 5# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
84fb5b46
MKG
6# <sales@bestpractical.com>
7#
8# (Except where explicitly superseded by other copyright notices)
9#
10#
11# LICENSE:
12#
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
16# from www.gnu.org.
17#
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.
22#
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.
28#
29#
30# CONTRIBUTION SUBMISSION POLICY:
31#
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.)
37#
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.
46#
47# END BPS TAGGED BLOCK }}}
48
49use strict;
50use warnings;
51
52package RT::Article;
84fb5b46
MKG
53use base 'RT::Record';
54
af59614d
MKG
55use Role::Basic 'with';
56with "RT::Record::Role::Links" => { -excludes => ["AddLink", "_AddLinksOnCreate"] };
57
84fb5b46
MKG
58use RT::Articles;
59use RT::ObjectTopics;
60use RT::Classes;
61use RT::Links;
62use RT::CustomFields;
63use RT::URI::fsck_com_article;
64use RT::Transactions;
65
66
67sub Table {'Articles'}
68
69# This object takes custom fields
70
71use RT::CustomField;
af59614d 72RT::CustomField->RegisterLookupType( CustomFieldLookupType() => 'Articles' ); #loc
84fb5b46
MKG
73
74# {{{ Create
75
76=head2 Create PARAMHASH
77
78Create takes a hash of values and creates a row in the database:
79
80 varchar(200) 'Name'.
81 varchar(200) 'Summary'.
82 int(11) 'Content'.
83 Class ID 'Class'
84
85 A paramhash called 'CustomFields', which contains
86 arrays of values for each custom field you want to fill in.
87 Arrays aRe ordered.
88
89
90
91
92=cut
93
94sub Create {
95 my $self = shift;
96 my %args = (
97 Name => '',
98 Summary => '',
99 Class => '0',
100 CustomFields => {},
101 Links => {},
102 Topics => [],
103 @_
104 );
105
dab09ea8 106 my $class = RT::Class->new( $self->CurrentUser );
84fb5b46
MKG
107 $class->Load( $args{'Class'} );
108 unless ( $class->Id ) {
109 return ( 0, $self->loc('Invalid Class') );
110 }
111
112 unless ( $class->CurrentUserHasRight('CreateArticle') ) {
113 return ( 0, $self->loc("Permission Denied") );
114 }
115
116 return ( undef, $self->loc('Name in use') )
117 unless $self->ValidateName( $args{'Name'} );
118
119 $RT::Handle->BeginTransaction();
120 my ( $id, $msg ) = $self->SUPER::Create(
121 Name => $args{'Name'},
122 Class => $class->Id,
123 Summary => $args{'Summary'},
124 );
125 unless ($id) {
126 $RT::Handle->Rollback();
127 return ( undef, $msg );
128 }
129
130 # {{{ Add custom fields
131
132 foreach my $key ( keys %args ) {
133 next unless ( $key =~ /CustomField-(.*)$/ );
134 my $cf = $1;
135 my @vals = ref( $args{$key} ) eq 'ARRAY' ? @{ $args{$key} } : ( $args{$key} );
136 foreach my $value (@vals) {
137
138 my ( $cfid, $cfmsg ) = $self->_AddCustomFieldValue(
139 (UNIVERSAL::isa( $value => 'HASH' )
140 ? %$value
141 : (Value => $value)
142 ),
143 Field => $cf,
144 RecordTransaction => 0
145 );
146
147 unless ($cfid) {
148 $RT::Handle->Rollback();
149 return ( undef, $cfmsg );
150 }
151 }
152
153 }
154
155 # }}}
156 # {{{ Add topics
157
158 foreach my $topic ( @{ $args{Topics} } ) {
159 my ( $cfid, $cfmsg ) = $self->AddTopic( Topic => $topic );
160
161 unless ($cfid) {
162 $RT::Handle->Rollback();
163 return ( undef, $cfmsg );
164 }
165 }
166
167 # }}}
168 # {{{ Add relationships
169
170 foreach my $type ( keys %args ) {
171 next unless ( $type =~ /^(RefersTo-new|new-RefersTo)$/ );
172 my @vals =
173 ref( $args{$type} ) eq 'ARRAY' ? @{ $args{$type} } : ( $args{$type} );
174 foreach my $val (@vals) {
175 my ( $base, $target );
176 if ( $type =~ /^new-(.*)$/ ) {
177 $type = $1;
178 $base = undef;
179 $target = $val;
180 }
181 elsif ( $type =~ /^(.*)-new$/ ) {
182 $type = $1;
183 $base = $val;
184 $target = undef;
185 }
186
187 my ( $linkid, $linkmsg ) = $self->AddLink(
188 Type => $type,
189 Target => $target,
190 Base => $base,
191 RecordTransaction => 0
192 );
193
194 unless ($linkid) {
195 $RT::Handle->Rollback();
196 return ( undef, $linkmsg );
197 }
198 }
199
200 }
201
202 # }}}
203
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 );
208
209 my ( $txn_id, $txn_msg, $txn ) = $self->_NewTransaction( Type => 'Create' );
210 unless ($txn_id) {
211 $RT::Handle->Rollback();
212 return ( undef, $self->loc( 'Internal error: [_1]', $txn_msg ) );
213 }
214 $RT::Handle->Commit();
215
216 return ( $id, $self->loc('Article [_1] created',$self->id ));
217}
218
219# }}}
220
221# {{{ ValidateName
222
223=head2 ValidateName NAME
224
225Takes a string name. Returns true if that name isn't in use by another article
226
227Empty names are permitted.
228
229
230=cut
231
232sub ValidateName {
233 my $self = shift;
234 my $name = shift;
235
236 if ( !$name ) {
237 return (1);
238 }
239
240 my $temp = RT::Article->new($RT::SystemUser);
241 $temp->LoadByCols( Name => $name );
242 if ( $temp->id &&
243 (!$self->id || ($temp->id != $self->id ))) {
244 return (undef);
245 }
246
247 return (1);
248
249}
250
251# }}}
252
253# {{{ Delete
254
255=head2 Delete
256
257Delete all its transactions
258Delete all its custom field values
259Delete all its relationships
260Delete this article.
261
262=cut
263
264sub Delete {
265 my $self = shift;
266 unless ( $self->CurrentUserHasRight('DeleteArticle') ) {
267 return ( 0, $self->loc("Permission Denied") );
268 }
269
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;
276
277 while ( my $item = $linksto->Next ) {
278 my ( $val, $msg ) = $item->Delete();
279 unless ($val) {
280 $RT::Logger->crit( ref($item) . ": $msg" );
281 $RT::Handle->Rollback();
282 return ( 0, $self->loc('Internal Error') );
283 }
284 }
285
286 while ( my $item = $linksfrom->Next ) {
287 my ( $val, $msg ) = $item->Delete();
288 unless ($val) {
289 $RT::Logger->crit( ref($item) . ": $msg" );
290 $RT::Handle->Rollback();
291 return ( 0, $self->loc('Internal Error') );
292 }
293 }
294
295 while ( my $item = $txns->Next ) {
296 my ( $val, $msg ) = $item->Delete();
297 unless ($val) {
298 $RT::Logger->crit( ref($item) . ": $msg" );
299 $RT::Handle->Rollback();
300 return ( 0, $self->loc('Internal Error') );
301 }
302 }
303
304 while ( my $item = $cfvalues->Next ) {
305 my ( $val, $msg ) = $item->Delete();
306 unless ($val) {
307 $RT::Logger->crit( ref($item) . ": $msg" );
308 $RT::Handle->Rollback();
309 return ( 0, $self->loc('Internal Error') );
310 }
311 }
312
313 while ( my $item = $topics->Next ) {
314 my ( $val, $msg ) = $item->Delete();
315 unless ($val) {
316 $RT::Logger->crit( ref($item) . ": $msg" );
317 $RT::Handle->Rollback();
318 return ( 0, $self->loc('Internal Error') );
319 }
320 }
321
322 $self->SUPER::Delete();
323 $RT::Handle->Commit();
324 return ( 1, $self->loc('Article Deleted') );
325
326}
327
328# }}}
329
330# {{{ Children
331
332=head2 Children
333
334Returns an RT::Articles object which contains
335all articles which have this article as their parent. This
336routine will not recurse and will not find grandchildren, great-grandchildren, uncles, aunts, nephews or any other such thing.
337
338=cut
339
340sub Children {
341 my $self = shift;
342 my $kids = RT::Articles->new( $self->CurrentUser );
343
344 unless ( $self->CurrentUserHasRight('ShowArticle') ) {
345 $kids->LimitToParent( $self->Id );
346 }
347 return ($kids);
348}
349
350# }}}
351
352# {{{ sub AddLink
353
354=head2 AddLink
355
af59614d 356Takes a paramhash of Type and one of Base or Target. Adds that link to this article.
84fb5b46 357
af59614d 358Prevents the use of plain numbers to avoid confusing behaviour.
84fb5b46 359
af59614d 360=cut
84fb5b46
MKG
361
362sub AddLink {
363 my $self = shift;
364 my %args = (
365 Target => '',
366 Base => '',
367 Type => '',
368 Silent => undef,
369 @_
370 );
371
372 unless ( $self->CurrentUserHasRight('ModifyArticle') ) {
373 return ( 0, $self->loc("Permission Denied") );
374 }
375
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+$/ )
381 {
382 return ( 0, $self->loc("Cannot add link to plain number") );
383 }
384
84fb5b46
MKG
385 $self->_AddLink(%args);
386}
387
388sub URI {
389 my $self = shift;
390
391 unless ( $self->CurrentUserHasRight('ShowArticle') ) {
392 return $self->loc("Permission Denied");
393 }
394
395 my $uri = RT::URI::fsck_com_article->new( $self->CurrentUser );
396 return ( $uri->URIForObject($self) );
397}
398
399# }}}
400
401# {{{ sub URIObj
402
403=head2 URIObj
404
405Returns this article's URI
406
407
408=cut
409
410sub URIObj {
411 my $self = shift;
412 my $uri = RT::URI->new( $self->CurrentUser );
413 if ( $self->CurrentUserHasRight('ShowArticle') ) {
414 $uri->FromObject($self);
415 }
416
417 return ($uri);
418}
419
420# }}}
421# }}}
422
423# {{{ Topics
424
425# {{{ Topics
426sub Topics {
427 my $self = shift;
428
429 my $topics = RT::ObjectTopics->new( $self->CurrentUser );
430 if ( $self->CurrentUserHasRight('ShowArticle') ) {
431 $topics->LimitToObject($self);
432 }
433 return $topics;
434}
435
436# }}}
437
438# {{{ AddTopic
439sub AddTopic {
440 my $self = shift;
441 my %args = (@_);
442
443 unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
444 return ( 0, $self->loc("Permission Denied") );
445 }
446
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
452 );
453 if ($tid) {
454 return ( $tid, $self->loc("Topic membership added") );
455 }
456 else {
457 return ( 0, $self->loc("Unable to add topic membership") );
458 }
459}
460
461# }}}
462
463sub DeleteTopic {
464 my $self = shift;
465 my %args = (@_);
466
467 unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
468 return ( 0, $self->loc("Permission Denied") );
469 }
470
471 my $t = RT::ObjectTopic->new( $self->CurrentUser );
472 $t->LoadByCols(
473 Topic => $args{'Topic'},
474 ObjectId => $self->Id,
475 ObjectType => ref($self)
476 );
477 if ( $t->Id ) {
478 my $del = $t->Delete;
479 unless ($del) {
480 return (
481 undef,
482 $self->loc(
483 "Unable to delete topic membership in [_1]",
484 $t->TopicObj->Name
485 )
486 );
487 }
488 else {
489 return ( 1, $self->loc("Topic membership removed") );
490 }
491 }
492 else {
493 return (
494 undef,
495 $self->loc(
496 "Couldn't load topic membership while trying to delete it")
497 );
498 }
499}
500
84fb5b46
MKG
501=head2 CurrentUserCanSee
502
503Returns true if the current user can see the article, using ShowArticle
504
505=cut
506
507sub CurrentUserCanSee {
508 my $self = shift;
509 return $self->CurrentUserHasRight('ShowArticle');
510}
511
512# }}}
513
514# {{{ _Set
515
516=head2 _Set { Field => undef, Value => undef
517
518Internal helper method to record a transaction as we update some core field of the article
519
520
521=cut
522
523sub _Set {
524 my $self = shift;
525 my %args = (
526 Field => undef,
527 Value => undef,
528 @_
529 );
530
531 unless ( $self->CurrentUserHasRight('ModifyArticle') ) {
532 return ( 0, $self->loc("Permission Denied") );
533 }
534
535 $self->_NewTransaction(
536 Type => 'Set',
537 Field => $args{'Field'},
538 NewValue => $args{'Value'},
539 OldValue => $self->__Value( $args{'Field'} )
540 );
541
542 return ( $self->SUPER::_Set(%args) );
543
544}
545
546=head2 _Value PARAM
547
548Return "PARAM" for this object. if the current user doesn't have rights, returns undef
549
550=cut
551
552sub _Value {
553 my $self = shift;
554 my $arg = shift;
555 unless ( ( $arg eq 'Class' )
556 || ( $self->CurrentUserHasRight('ShowArticle') ) )
557 {
558 return (undef);
559 }
560 return $self->SUPER::_Value($arg);
561}
562
563# }}}
564
565sub CustomFieldLookupType {
566 "RT::Class-RT::Article";
567}
568
af59614d
MKG
569
570sub ACLEquivalenceObjects {
571 my $self = shift;
572 return $self->ClassObj;
573}
574
575sub ModifyLinkRight { "ModifyArticle" }
576
84fb5b46
MKG
577=head2 LoadByInclude Field Value
578
579Takes the name of a form field from "Include Article"
580and the value submitted by the browser and attempts to load an Article.
581
582This handles Articles included by searching, by the Name and via
583the hotlist.
584
585If you optionaly pass an id as the Queue argument, this will check that
586the Article's Class is applied to that Queue.
587
588=cut
589
590sub LoadByInclude {
591 my $self = shift;
592 my %args = @_;
593 my $Field = $args{Field};
594 my $Value = $args{Value};
595 my $Queue = $args{Queue};
596
597 return unless $Field;
598
599 my ($ok, $msg);
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 );
607 } else {
608 ($ok, $msg) = $self->LoadByCols( id => $Value );
609 }
610 }
611
612 unless ($ok) { # load failed, don't check Class
af59614d 613 return wantarray ? ($ok, $msg) : $ok;
84fb5b46
MKG
614 }
615
616 unless ($Queue) { # we haven't requested extra sanity checking
af59614d 617 return wantarray ? ($ok, $msg) : $ok;
84fb5b46
MKG
618 }
619
620 # ensure that this article is available for the Queue we're
621 # operating under.
622 my $class = $self->ClassObj;
623 unless ($class->IsApplied(0) || $class->IsApplied($Queue)) {
624 $self->LoadById(0);
af59614d 625 return wantarray ? (0, $self->loc("The Class of the Article identified by [_1] is not applied to the current Queue",$Value)) : 0;
84fb5b46
MKG
626 }
627
af59614d 628 return wantarray ? ($ok, $msg) : $ok;
84fb5b46
MKG
629
630}
631
632
633=head2 id
634
635Returns the current value of id.
636(In the database, id is stored as int(11).)
637
638
639=cut
640
641
642=head2 Name
643
644Returns the current value of Name.
645(In the database, Name is stored as varchar(255).)
646
647
648
649=head2 SetName VALUE
650
651
652Set Name to VALUE.
653Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
654(In the database, Name will be stored as a varchar(255).)
655
656
657=cut
658
659
660=head2 Summary
661
662Returns the current value of Summary.
663(In the database, Summary is stored as varchar(255).)
664
665
666
667=head2 SetSummary VALUE
668
669
670Set Summary to VALUE.
671Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
672(In the database, Summary will be stored as a varchar(255).)
673
674
675=cut
676
677
678=head2 SortOrder
679
680Returns the current value of SortOrder.
681(In the database, SortOrder is stored as int(11).)
682
683
684
685=head2 SetSortOrder VALUE
686
687
688Set SortOrder to VALUE.
689Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
690(In the database, SortOrder will be stored as a int(11).)
691
692
693=cut
694
695
696=head2 Class
697
698Returns the current value of Class.
699(In the database, Class is stored as int(11).)
700
701
702
703=head2 SetClass VALUE
704
705
706Set Class to VALUE.
707Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
708(In the database, Class will be stored as a int(11).)
709
710
711=cut
712
713
714=head2 ClassObj
715
716Returns the Class Object which has the id returned by Class
717
718
719=cut
720
721sub ClassObj {
af59614d
MKG
722 my $self = shift;
723 my $Class = RT::Class->new($self->CurrentUser);
724 $Class->Load($self->Class());
725 return($Class);
84fb5b46
MKG
726}
727
728=head2 Parent
729
730Returns the current value of Parent.
731(In the database, Parent is stored as int(11).)
732
733
734
735=head2 SetParent VALUE
736
737
738Set Parent to VALUE.
739Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
740(In the database, Parent will be stored as a int(11).)
741
742
743=cut
744
745
746=head2 URI
747
748Returns the current value of URI.
749(In the database, URI is stored as varchar(255).)
750
751
752
753=head2 SetURI VALUE
754
755
756Set URI to VALUE.
757Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
758(In the database, URI will be stored as a varchar(255).)
759
760
761=cut
762
763
764=head2 Creator
765
766Returns the current value of Creator.
767(In the database, Creator is stored as int(11).)
768
769
770=cut
771
772
773=head2 Created
774
775Returns the current value of Created.
776(In the database, Created is stored as datetime.)
777
778
779=cut
780
781
782=head2 LastUpdatedBy
783
784Returns the current value of LastUpdatedBy.
785(In the database, LastUpdatedBy is stored as int(11).)
786
787
788=cut
789
790
791=head2 LastUpdated
792
793Returns the current value of LastUpdated.
794(In the database, LastUpdated is stored as datetime.)
795
796
797=cut
798
799
800
801sub _CoreAccessible {
802 {
803
804 id =>
af59614d 805 {read => 1, type => 'int(11)', default => ''},
84fb5b46 806 Name =>
af59614d 807 {read => 1, write => 1, type => 'varchar(255)', default => ''},
84fb5b46 808 Summary =>
af59614d 809 {read => 1, write => 1, type => 'varchar(255)', default => ''},
84fb5b46 810 SortOrder =>
af59614d 811 {read => 1, write => 1, type => 'int(11)', default => '0'},
84fb5b46 812 Class =>
af59614d 813 {read => 1, write => 1, type => 'int(11)', default => '0'},
84fb5b46 814 Parent =>
af59614d 815 {read => 1, write => 1, type => 'int(11)', default => '0'},
84fb5b46 816 URI =>
af59614d 817 {read => 1, write => 1, type => 'varchar(255)', default => ''},
84fb5b46 818 Creator =>
af59614d 819 {read => 1, auto => 1, type => 'int(11)', default => '0'},
84fb5b46 820 Created =>
af59614d 821 {read => 1, auto => 1, type => 'datetime', default => ''},
84fb5b46 822 LastUpdatedBy =>
af59614d 823 {read => 1, auto => 1, type => 'int(11)', default => '0'},
84fb5b46 824 LastUpdated =>
af59614d 825 {read => 1, auto => 1, type => 'datetime', default => ''},
84fb5b46
MKG
826
827 }
828};
829
af59614d
MKG
830sub FindDependencies {
831 my $self = shift;
832 my ($walker, $deps) = @_;
833
834 $self->SUPER::FindDependencies($walker, $deps);
835
836 # Links
837 my $links = RT::Links->new( $self->CurrentUser );
838 $links->Limit(
839 SUBCLAUSE => "either",
840 FIELD => $_,
841 VALUE => $self->URI,
842 ENTRYAGGREGATOR => 'OR'
843 ) for qw/Base Target/;
844 $deps->Add( in => $links );
845
846 $deps->Add( out => $self->ClassObj );
847 $deps->Add( in => $self->Topics );
848}
849
850sub PostInflate {
851 my $self = shift;
852
853 $self->__Set( Field => 'URI', Value => $self->URI );
854}
855
84fb5b46
MKG
856RT::Base->_ImportOverlays();
857
8581;
859
860
8611;