"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;