"Fossies" - the Fresh Open Source Software Archive 
Member "RT-Extension-Assets-1.05/lib/RT/Extension/Assets.pm" (6 May 2015, 26763 Bytes) of package /linux/misc/RT-Extension-Assets-1.05.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Perl source code syntax highlighting (style:
standard) with prefixed line numbers and
code folding option.
Alternatively you can here
view or
download the uninterpreted source code file.
For more information about "Assets.pm" see the
Fossies "Dox" file reference documentation and the latest
Fossies "Diffs" side-by-side code changes report:
1.04_vs_1.05.
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
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
49 use strict;
50 use warnings;
51 package RT::Extension::Assets;
52
53 our $VERSION = '1.05';
54
55 # Loaded so they're available and rights are injected.
56 use RT::Catalog;
57 use RT::Catalogs;
58 use RT::Asset;
59 use RT::Assets;
60
61 =head1 NAME
62
63 RT-Extension-Assets - Asset management for RT
64
65 =cut
66
67 RT->AddStyleSheets("RTx-Assets.css");
68 RT->AddJavaScript("RTx-Assets.js");
69
70 {
71 package RT::Transaction;
72 our %_BriefDescriptions;
73
74 $_BriefDescriptions{"RT::Asset-Set-Catalog"} = sub {
75 my $self = shift;
76 return ("[_1] changed from [_2] to [_3]", #loc
77 $self->loc($self->Field), map {
78 my $c = RT::Catalog->new($self->CurrentUser);
79 $c->Load($_);
80 $c->Name || $self->loc("~[a hidden catalog~]")
81 } $self->OldValue, $self->NewValue);
82 };
83 }
84
85 {
86 require RT::Interface::Web;
87 package HTML::Mason::Commands;
88
89 sub LoadCatalog {
90 my $id = shift
91 or Abort(loc("No catalog specified."));
92
93 my $catalog = RT::Catalog->new( $session{CurrentUser} );
94 $catalog->Load($id);
95
96 Abort(loc("Unable to find catalog [_1]", $id))
97 unless $catalog->id;
98
99 Abort(loc("You don't have permission to view this catalog."))
100 unless $catalog->CurrentUserCanSee;
101
102 return $catalog;
103 }
104
105 sub LoadAsset {
106 my $id = shift
107 or Abort(loc("No asset ID specified."));
108
109 my $asset = RT::Asset->new( $session{CurrentUser} );
110 $asset->Load($id);
111
112 Abort(loc("Unable to find asset #[_1]", $id))
113 unless $asset->id;
114
115 Abort(loc("You don't have permission to view this asset."))
116 unless $asset->CurrentUserCanSee;
117
118 return $asset;
119 }
120
121 sub ProcessRoleMembers {
122 my $object = shift;
123 my %ARGS = (@_);
124 my @results;
125
126 for my $arg (keys %ARGS) {
127 if ($arg =~ /^Add(User|Group)RoleMember$/) {
128 next unless $ARGS{$arg} and $ARGS{"$arg-Role"};
129
130 my ($ok, $msg) = $object->AddRoleMember(
131 Type => $ARGS{"$arg-Role"},
132 $1 => $ARGS{$arg},
133 );
134 push @results, $msg;
135 }
136 elsif ($arg =~ /^SetRoleMember-(.+)$/) {
137 my $role = $1;
138 my $group = $object->RoleGroup($role);
139 next unless $group->id and $group->SingleMemberRoleGroup;
140 next if $ARGS{$arg} eq $group->UserMembersObj->First->Name;
141 my ($ok, $msg) = $object->AddRoleMember(
142 Type => $role,
143 User => $ARGS{$arg} || 'Nobody',
144 );
145 push @results, $msg;
146 }
147 elsif ($arg =~ /^(Add|Remove)RoleMember-(.+)$/) {
148 my $role = $2;
149 my $method = $1 eq 'Add'? 'AddRoleMember' : 'DeleteRoleMember';
150
151 my $is = 'User';
152 if ( ($ARGS{"$arg-Type"}||'') =~ /^(User|Group)$/ ) {
153 $is = $1;
154 }
155
156 my ($ok, $msg) = $object->$method(
157 Type => $role,
158 ($ARGS{$arg} =~ /\D/
159 ? ($is => $ARGS{$arg})
160 : (PrincipalId => $ARGS{$arg})
161 ),
162 );
163 push @results, $msg;
164 }
165 elsif ($arg =~ /^RemoveAllRoleMembers-(.+)$/) {
166 my $role = $1;
167 my $group = $object->RoleGroup($role);
168 next unless $group->id;
169
170 my $gms = $group->MembersObj;
171 while ( my $gm = $gms->Next ) {
172 my ($ok, $msg) = $object->DeleteRoleMember(
173 Type => $role,
174 PrincipalId => $gm->MemberId,
175 );
176 push @results, $msg;
177 }
178 }
179 }
180 return @results;
181 }
182
183
184 # If provided a catalog, load it and return the object.
185 # If no catalog is passed, load the first active catalog.
186
187 sub LoadDefaultCatalog {
188 my $catalog = shift;
189 my $catalog_obj = RT::Catalog->new($session{CurrentUser});
190
191 if ( $catalog ){
192 $catalog_obj->Load($catalog);
193 RT::Logger->error("Unable to load catalog: " . $catalog)
194 unless $catalog_obj->Id;
195 }
196 elsif ( $session{'DefaultCatalog'} ){
197 $catalog_obj->Load($session{'DefaultCatalog'});
198 RT::Logger->error("Unable to load remembered catalog: " .
199 $session{'DefaultCatalog'})
200 unless $catalog_obj->Id;
201 }
202 elsif ( RT->Config->Get("DefaultCatalog") ){
203 $catalog_obj->Load( RT->Config->Get("DefaultCatalog") );
204 RT::Logger->error("Unable to load default catalog: "
205 . RT->Config->Get("DefaultCatalog"))
206 unless $catalog_obj->Id;
207 }
208 else {
209 # If no catalog, default to the first active catalog
210 my $catalogs = RT::Catalogs->new($session{CurrentUser});
211 $catalogs->UnLimit;
212 $catalog_obj = $catalogs->First();
213 RT::Logger->error("No active catalogs.")
214 unless $catalog_obj and $catalog_obj->Id;
215 }
216
217 return $catalog_obj;
218 }
219
220 sub ProcessAssetsSearchArguments {
221 my %args = (
222 Catalog => undef,
223 Assets => undef,
224 ARGSRef => undef,
225 @_
226 );
227 my $ARGSRef = $args{'ARGSRef'};
228
229 my @PassArguments;
230
231 if ($ARGSRef->{q}) {
232 if ($ARGSRef->{q} =~ /^\d+$/) {
233 my $asset = RT::Asset->new( $session{CurrentUser} );
234 $asset->Load( $ARGSRef->{q} );
235 RT::Interface::Web::Redirect(
236 RT->Config->Get('WebURL')."Asset/Display.html?id=".$ARGSRef->{q}
237 ) if $asset->id;
238 }
239 $args{'Assets'}->SimpleSearch( Term => $ARGSRef->{q}, Catalog => $args{Catalog} );
240 push @PassArguments, "q";
241 } elsif ( $ARGSRef->{'SearchAssets'} ){
242 for my $key (keys %$ARGSRef) {
243 my $value = ref $ARGSRef->{$key} ? $ARGSRef->{$key}[0] : $ARGSRef->{$key};
244 next unless defined $value and length $value;
245
246 my $orig_key = $key;
247 my $negative = ($key =~ s/^!// ? 1 : 0);
248 if ($key =~ /^(Name|Description)$/) {
249 $args{'Assets'}->Limit(
250 FIELD => $key,
251 OPERATOR => ($negative ? 'NOT LIKE' : 'LIKE'),
252 VALUE => $value,
253 ENTRYAGGREGATOR => "AND",
254 );
255 } elsif ($key eq 'Catalog') {
256 $args{'Assets'}->LimitCatalog(
257 OPERATOR => ($negative ? '!=' : '='),
258 VALUE => $value,
259 ENTRYAGGREGATOR => "AND",
260 );
261 } elsif ($key eq 'Status') {
262 $args{'Assets'}->Limit(
263 FIELD => $key,
264 OPERATOR => ($negative ? '!=' : '='),
265 VALUE => $value,
266 ENTRYAGGREGATOR => "AND",
267 );
268 } elsif ($key =~ /^Role\.(.+)/) {
269 my $role = $1;
270 $args{'Assets'}->RoleLimit(
271 TYPE => $role,
272 FIELD => $_,
273 OPERATOR => ($negative ? '!=' : '='),
274 VALUE => $value,
275 SUBCLAUSE => $role,
276 ENTRYAGGREGATOR => ($negative ? "AND" : "OR"),
277 CASESENSITIVE => 0,
278 ) for qw/EmailAddress Name/;
279 } elsif ($key =~ /^CF\.\{(.+?)\}$/ or $key =~ /^CF\.(.*)/) {
280 my $cf = RT::Asset->new( $session{CurrentUser} )
281 ->LoadCustomFieldByIdentifier( $1 );
282 next unless $cf->id;
283 if ( $value eq 'NULL' ) {
284 $args{'Assets'}->LimitCustomField(
285 CUSTOMFIELD => $cf->Id,
286 OPERATOR => ($negative ? "IS NOT" : "IS"),
287 VALUE => 'NULL',
288 QUOTEVALUE => 0,
289 ENTRYAGGREGATOR => "AND",
290 );
291 } else {
292 $args{'Assets'}->LimitCustomField(
293 CUSTOMFIELD => $cf->Id,
294 OPERATOR => ($negative ? "NOT LIKE" : "LIKE"),
295 VALUE => $value,
296 ENTRYAGGREGATOR => "AND",
297 );
298 }
299 }
300 else {
301 next;
302 }
303 push @PassArguments, $orig_key;
304 }
305 push @PassArguments, 'SearchAssets';
306 }
307
308 my $Format = RT->Config->Get('AssetSearchFormat');
309 $Format = $Format->{$args{'Catalog'}->id}
310 || $Format->{$args{'Catalog'}->Name}
311 || $Format->{''} if ref $Format;
312 $Format ||= q[
313 '<b><a href="__WebPath__/Asset/Display.html?id=__id__">__id__</a></b>/TITLE:#',
314 '<b><a href="__WebPath__/Asset/Display.html?id=__id__">__Name__</a></b>/TITLE:Name',
315 Description,
316 Status,
317 ];
318
319 $ARGSRef->{OrderBy} ||= 'id';
320
321 push @PassArguments, qw/OrderBy Order Page/;
322
323 return (
324 OrderBy => 'id',
325 Order => 'ASC',
326 Rows => 50,
327 (map { $_ => $ARGSRef->{$_} } grep { defined $ARGSRef->{$_} } @PassArguments),
328 PassArguments => \@PassArguments,
329 Format => $Format,
330 );
331 }
332 }
333
334 {
335 package RT::CustomField;
336
337 # To someday be merged into RT::CustomField::LoadByName
338 sub LoadByNameAndCatalog {
339 my $self = shift;
340 my %args = (
341 Catalog => undef,
342 Name => undef,
343 @_,
344 );
345
346 unless ( defined $args{'Name'} && length $args{'Name'} ) {
347 $RT::Logger->error("Couldn't load Custom Field without Name");
348 return wantarray ? (0, $self->loc("No name provided")) : 0;
349 }
350
351 # if we're looking for a catalog by name, make it a number
352 if ( defined $args{'Catalog'} && ($args{'Catalog'} =~ /\D/ || !$self->ContextObject) ) {
353 my $CatalogObj = RT::Catalog->new( $self->CurrentUser );
354 my ($ok, $msg) = $CatalogObj->Load( $args{'Catalog'} );
355 if ( $ok ){
356 $args{'Catalog'} = $CatalogObj->Id;
357 }
358 elsif ($args{'Catalog'}) {
359 RT::Logger->error("Unable to load catalog " . $args{'Catalog'} . $msg);
360 return (0, $msg);
361 }
362 $self->SetContextObject( $CatalogObj )
363 unless $self->ContextObject;
364 }
365
366 my $CFs = RT::CustomFields->new( $self->CurrentUser );
367 $CFs->SetContextObject( $self->ContextObject );
368 my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
369 $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
370
371 # Limit to catalog, if provided. This will also limit to RT::Asset types.
372 $CFs->LimitToCatalog( $args{'Catalog'} );
373
374 # When loading by name, we _can_ load disabled fields, but prefer
375 # non-disabled fields.
376 $CFs->FindAllRows;
377 $CFs->OrderByCols(
378 {
379 FIELD => "Disabled", ORDER => 'ASC' },
380 );
381
382 # We only want one entry.
383 $CFs->RowsPerPage(1);
384
385 return (0, $self->loc("Not found")) unless my $first = $CFs->First;
386 return $self->LoadById( $first->id );
387 }
388
389 }
390
391 {
392 package RT::CustomFields;
393
394 sub LimitToCatalog {
395 my $self = shift;
396 my $catalog = shift;
397
398 $self->Limit (ALIAS => $self->_OCFAlias,
399 ENTRYAGGREGATOR => 'OR',
400 FIELD => 'ObjectId',
401 VALUE => "$catalog")
402 if defined $catalog;
403
404 $self->LimitToLookupType( RT::Asset->CustomFieldLookupType );
405 $self->ApplySortOrder;
406
407 unless ($self->ContextObject) {
408 my $obj = RT::Catalog->new( $self->CurrentUser );
409 $obj->Load( $catalog );
410 $self->SetContextObject( $obj );
411 }
412 }
413 }
414
415 =head1 INSTALLATION
416
417 Assets requires version 4.2.1 or higher of RT.
418
419 =over
420
421 =item perl Makefile.PL
422
423 =item make
424
425 =item make install
426
427 This step may require root permissions.
428
429 =item Patch your RT
430
431 Assets requires a small patch to work on versions of RT prior to 4.2.3.
432 To patch RT, run:
433
434 patch -d /opt/rt4 -p1 < patches/rt-4.2.1-4.2.2.patch
435
436 RT version 4.2.3 and above already contain this patch.
437
438 =item make initdb
439
440 Only run this the first time you install this module.
441
442 If you run this twice, you will end up with duplicate data
443 in your database.
444
445 If you are upgrading this module, check for upgrading instructions
446 in case changes need to be made to your database.
447
448 =item Edit your /opt/rt4/etc/RT_SiteConfig.pm
449
450 Add this line:
451
452 Plugin( "RT::Extension::Assets" );
453
454 =item Configure portlets for RT's Homepage and User Summary
455
456 If you would like the MyAssets or FindAsset portlets to be available on
457 RT at a Glance and Dashboards, you will need to copy $HomepageComponents
458 from RT_Config.pm to RT_SiteConfig.pm and add them to the list.
459
460 If you would like the UserAssets portlet to show up on the User
461 Summary page, you will need to copy @UserSummaryPortlets from RT_Config.pm to
462 RT_SiteConfig.pm and add UserAssets to the list.
463
464 =item Clear your mason cache
465
466 rm -rf /opt/rt4/var/mason_data/obj
467
468 =item Restart your webserver
469
470
471 =back
472
473 =encoding utf8
474
475 =head1 CONFIGURATION
476
477 See L<Assets_Config.pm> for documentation on the available configuration
478 parameters.
479
480 =head1 USAGE
481
482 Assets start as a small set of fundamental record data upon which you build
483 custom fields (CFs) of any type you like to describe the assets you want to
484 track. By themselves, before you setup any CFs, assets are not very useful.
485
486 Just like tickets are assigned to queues, assets are assigned to B<catalogs>.
487 The default catalog is named "General assets", but we suggest you rename it and
488 add additional catalogs to better fit your organization.
489
490 You may want to use catalogs to separate assets into departments, general type
491 of asset, hardware vs. software, etc. Catalogs, like queues, are generally
492 easiest to work with when there's more than few but less than many dozen. The
493 catalog of an asset should represent some fundamental quality of it (and many
494 other assets!), that could just as easily be expressed as a custom field, but
495 which is more important than other qualities for categorizing, sorting, and
496 searching.
497
498 =head2 Managing catalogs
499
500 Catalogs are managed by RT administrators, or anyone with the L</AdminCatalog>
501 right. You can find the list of catalogs, create new catalogs, and manage
502 existing ones under the Tools → Configuration → Assets → Catalogs menu.
503
504 Currently you need to log out and log back in to see changes to catalogs in any
505 of the catalog selection dropdowns. This doesn't affect the catalog name
506 displayed on individual asset pages.
507
508 =head2 Adding fields
509
510 You can see the current asset CFs by navigating to Admin >
511 Assets > Custom Fields. From there you can use the "Create" link to create a
512 new asset CF. If you know you want to create a new CF right away, you can do
513 so via Admin > Assets > Custom Fields > Create.
514
515 When creating a CF, be sure to select "Assets" in the "Applies To" dropdown.
516 You'll also need to grant rights to the groups and/or roles which need to see
517 the fields, otherwise they'll be hidden. See the following section.
518
519 Similar to ticket CFs, asset custom fields are added globally or to specific
520 catalogs. Only assets within those specific catalogs will have the CFs
521 available. After creating a CF, you'll need to visit the "Applies To" page to
522 add it to the catalogs you want or make it global.
523
524 =head2 Rights
525
526 There are three rights controlling basic access to assets and two for
527 catalogs. Each right is grantable at the global level or individual catalog
528 level, and grantable to system groups, asset roles, user groups, and individual
529 users (just like ticket and queue rights).
530
531 =head3 ShowAsset
532
533 Allows viewing an asset record and it's core fields (but not CFs). Without
534 it, no assets can be seen. Similar to ShowTicket.
535
536 =head3 CreateAsset
537
538 Allows creating assets and filling in the core fields (but not CFs). Without
539 it, no assets can be created. Similar to CreateTicket.
540
541 =head3 ModifyAsset
542
543 Allows modifying existing assets and their core fields (but not CFs). Without
544 it, basic asset data cannot be modified after creation. Similar to
545 ModifyTicket.
546
547 Most of your rights configuration will be on the CFs, and will likely need to
548 be done for each CF. This lets you fine tune which fields are visible to
549 individual groups and/or roles of users. Relevant CF rights are
550 B<SeeCustomField> and B<ModifyCustomField>.
551
552 Rights related to assets may also come from the L</Lifecycle statuses>
553 configuration and restrict status transitions.
554
555 =head3 ShowCatalog
556
557 Allows seeing a catalog's name and other details when associated with assets.
558 Without it, users will see "[a hidden catalog]" or a blank space where the
559 catalog name would normally be. Similar to SeeQueue.
560
561 =head3 AdminCatalog
562
563 Allows creating new catalogs and modifying all aspects of existing catalogs,
564 including changing the CFs associated with the catalog, granting/revoking
565 rights, and adding/removing role members. This right should only be granted to
566 administrators of RT. Similar to AdminQueue.
567
568 =head3 Typical configuration
569
570 A typical configuration grants the system Privileged group the following:
571 B<ShowAsset>, B<CreateAsset>, B<ModifyAsset>, and B<ShowCatalog> globally, and
572 B<SeeCustomField> and B<ModifyCustomField> globally on all asset CFs.
573
574 If you want self service users (Unprivileged) to be able to view the assets
575 they hold, grant the Held By role B<ShowAsset> and B<ShowCatalog> globally and
576 B<SeeCustomField> on the necessary asset CFs.
577
578 =head2 People and Roles
579
580 Just like tickets, assets have various roles which users and groups may be
581 assigned to. The intended usages of these roles are described below, but
582 you're free to use them for whatever you'd like, of course.
583
584 The roles provide ways to keep track of who is involved with each asset, as
585 well as providing a place to grant rights that depend on the user's association
586 with each asset.
587
588 In addition to adding people to individual asset roles, you can also add role
589 members at an entire catalog level. These catalog-level roles are useful in
590 cases when you might have an entire catalog of assets for which the same people
591 should be the Contacts, or which are Held By the same group. Unlike tickets
592 where the queue watchers are invisible, catalog role members are visible
593 because assets are generally much longer lived than tickets. When a problem
594 with an asset arises, it's easier to see who to create a ticket for. On
595 individual asset pages, catalog role members are shown with the text "(via this
596 asset's catalog)" following each name.
597
598 =head3 Owner
599
600 The person responsible for the asset, perhaps the purchaser or manager.
601
602 Restricted to a single user. Not available at a catalog level.
603
604 =head3 Held By
605
606 The person or people who physically possess the asset or are actively using the
607 asset (if it isn't physical). This may be the same as the Contacts or may be
608 different. For example, a computer workstation may be "held by" a university
609 professor, but the contact may be the IT staff member responsible for all
610 assets in the professor's department. This role is most similar to Requestor
611 on tickets, although not equivalent.
612
613 May be multiple users and/or groups.
614
615 =head3 Contact
616
617 The person or people who should be contacted with questions, problems,
618 notifications, etc. about the asset. Contacts share some of the same intended
619 usages of both Requestors and Ccs on tickets.
620
621 May be multiple users and/or groups.
622
623 =head2 Lifecycle statuses
624
625 One of the basic asset fields is "Status". Similar to tickets, the valid
626 statuses and their transitions and actions can be customized via RT's standard
627 Lifecycles configuration (see "Lifecycles" in F<RT_Config.pm>). The default
628 lifecycle is named "assets". You're free to modify it as much as you'd like,
629 or add your own lifecycles. Each catalog may have its own lifecycle.
630
631 For the default "assets" configuration, see F<etc/Assets_Config.pm>.
632
633 =head2 Field organization
634
635 =head3 Groupings
636
637 You can organize your asset CFs into visual and logical "groupings" as you see
638 fit. These groupings appear as separate boxes on the asset display page and
639 become separate pages for editing (showing up in the per-asset menu).
640
641 By default your CFs will appear in a B<Custom Fields> box on the asset display
642 page and will be editable from a box of the same name on the B<Basics> editing
643 page.
644
645 Using the C<%CustomFieldGroupings> option (documented in F<etc/RT_Config.pm>),
646 you can move individual CFs by name into one of the four built-in groupings
647 (B<Basics>, B<People>, B<Dates>, and B<Links>) or create your own just by
648 naming it. An example, assuming a date CF named "Purchased" and two "enter one
649 value" CFs named "Weight" and "Color":
650
651 # In etc/RT_SiteConfig.pm
652 Set(%CustomFieldGroupings,
653 'RT::Asset' => {
654 'Dates' => ['Purchased'],
655 'Physical Properties' => ['Weight', 'Color'],
656 },
657 );
658
659 This configuration snippet will move all three CFs out of the generic B<Custom
660 Fields> box and into the B<Dates> box and a new box titled B<Physical
661 Properties>. The "Purchased" CF will be editable from the Dates page and a new
662 page titled "Physical Properties" will appear in the menu to allow editing of
663 the "Weight" and "Color" CFs.
664
665 =head3 Ordering
666
667 Within a box, CFs come after any built-in asset fields such as Name,
668 Description, Created, Last Updated, etc. The CFs themselves are ordered
669 according to the sorting seen (and adjustable) on the global Asset Custom
670 Fields page (Tools → Configuration → Global → Custom Fields → Assets) and the
671 individual catalog Custom Fields pages (Tools → Configuration → Assets →
672 Catalogs → (Pick one) → Custom Fields).
673
674 Global asset CFs may be intermixed with per-catalog CFs with ordering.
675
676 =head2 Importing existing data
677
678 Another extension, L<RT::Extension::Assets::Import::CSV> provides tools
679 to import new and update existing assets from a CSV dump. Its
680 configuration lets you map the fields in the CSV to the asset fields
681 you've already created in RT. L<RT::Extension::Assets::AppleGSX> also
682 provides tools for looking up data associated with an Apple product.
683
684 =head1 METHODS ADDED TO OTHER CLASSES
685
686 =head2 L<RT::CustomField>
687
688 =head3 LoadByNameAndCatalog
689
690 Loads the described asset custom field, if one is found, into the current
691 object. This method only consults custom fields applied to L<RT::Catalog> for
692 L<RT::Asset> objects.
693
694 Takes a hash with the keys:
695
696 =over
697
698 =item Name
699
700 A L<RT::CustomField> ID or Name which applies to L<assets|RT::Asset>.
701
702 =item Catalog
703
704 Optional. An L<RT::Catalog> ID or Name.
705
706 =back
707
708 If Catalog is specified, only a custom field added to that Catalog will be loaded.
709
710 If Catalog is C<0>, only global asset custom fields will be loaded.
711
712 If no Catalog is specified, all asset custom fields are searched including
713 global and catalog-specific CFs.
714
715 Please note that this method may load a Disabled custom field if no others
716 matching the same criteria are found. Enabled CFs are preferentially loaded.
717
718 =head2 RT::CustomFields
719
720 =head3 LimitToCatalog
721
722 Takes a numeric L<RT::Catalog> ID. Limits the L<RT::CustomFields> collection
723 to only those fields applied directly to the specified catalog. This limit is
724 OR'd with other L</LimitToCatalog> and L<RT::CustomFields/LimitToObjectId>
725 calls.
726
727 Note that this will cause the collection to only return asset CFs.
728
729 =head1 BUGS
730
731 Please report bugs to assets-bugs@bestpractical.com; if you're not sure
732 if what you've discovered is a bug, please discuss it on
733 rt-users@lists.bestpractical.com before reporting it.
734
735 =head1 LICENSE AND COPYRIGHT
736
737 This software is Copyright (c) 2014 by Best Practical Solutions
738
739 This is free software, licensed under:
740
741 The GNU General Public License, Version 2, June 1991
742
743 =cut
744
745 1;